Skip to main content
Glama
PageCollector.ts4.21 kB
/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { type Browser, type Frame, type Handler, type HTTPRequest, type Page, type PageEvents, } from 'puppeteer-core'; export type ListenerMap<EventMap extends PageEvents = PageEvents> = { [K in keyof EventMap]?: (event: EventMap[K]) => void; }; function createIdGenerator() { let i = 1; return () => { if (i === Number.MAX_SAFE_INTEGER) { i = 0; } return i++; }; } export const stableIdSymbol = Symbol('stableIdSymbol'); type WithSymbolId<T> = T & { [stableIdSymbol]?: number; }; export class PageCollector<T> { #browser: Browser; #listenersInitializer: ( collector: (item: T) => void, ) => ListenerMap<PageEvents>; #listeners = new WeakMap<Page, ListenerMap>(); /** * The Array in this map should only be set once * As we use the reference to it. * Use methods that manipulate the array in place. */ protected storage = new WeakMap<Page, Array<WithSymbolId<T>>>(); constructor( browser: Browser, listeners: (collector: (item: T) => void) => ListenerMap<PageEvents>, ) { this.#browser = browser; this.#listenersInitializer = listeners; } async init() { const pages = await this.#browser.pages(); for (const page of pages) { this.#initializePage(page); } this.#browser.on('targetcreated', async target => { const page = await target.page(); if (!page) { return; } this.#initializePage(page); }); this.#browser.on('targetdestroyed', async target => { const page = await target.page(); if (!page) { return; } this.#cleanupPageDestroyed(page); }); } public addPage(page: Page) { this.#initializePage(page); } #initializePage(page: Page) { if (this.storage.has(page)) { return; } const idGenerator = createIdGenerator(); const stored: Array<WithSymbolId<T>> = []; this.storage.set(page, stored); const listeners = this.#listenersInitializer(value => { const withId = value as WithSymbolId<T>; withId[stableIdSymbol] = idGenerator(); stored.push(withId); }); listeners['framenavigated'] = (frame: Frame) => { // Only reset the storage on main frame navigation if (frame !== page.mainFrame()) { return; } this.cleanupAfterNavigation(page); }; for (const [name, listener] of Object.entries(listeners)) { page.on(name, listener as Handler<unknown>); } this.#listeners.set(page, listeners); } protected cleanupAfterNavigation(page: Page) { const collection = this.storage.get(page); if (collection) { // Keep the reference alive collection.length = 0; } } #cleanupPageDestroyed(page: Page) { const listeners = this.#listeners.get(page); if (listeners) { for (const [name, listener] of Object.entries(listeners)) { page.off(name, listener as Handler<unknown>); } } this.storage.delete(page); } getData(page: Page): T[] { return this.storage.get(page) ?? []; } getIdForResource(resource: WithSymbolId<T>): number { return resource[stableIdSymbol] ?? -1; } getById(page: Page, stableId: number): T { const data = this.storage.get(page); if (!data || !data.length) { throw new Error('No requests found for selected page'); } for (const collected of data) { if (collected[stableIdSymbol] === stableId) { return collected; } } throw new Error('Request not found for selected page'); } } export class NetworkCollector extends PageCollector<HTTPRequest> { override cleanupAfterNavigation(page: Page) { const requests = this.storage.get(page) ?? []; if (!requests) { return; } const lastRequestIdx = requests.findLastIndex(request => { return request.frame() === page.mainFrame() ? request.isNavigationRequest() : false; }); // Keep all requests since the last navigation request including that // navigation request itself. // Keep the reference requests.splice(0, Math.max(lastRequestIdx, 0)); } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SHAY5555-gif/chrome-devtools-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server