Skip to content

Commit 401584b

Browse files
refactor: dispose listeners on page destroyed
1 parent 1faec78 commit 401584b

File tree

3 files changed

+95
-49
lines changed

3 files changed

+95
-49
lines changed

src/McpContext.ts

Lines changed: 15 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ import type {
1919
PredefinedNetworkConditions,
2020
} from 'puppeteer-core';
2121

22+
import type {ListenerMap} from './PageCollector.js';
2223
import {NetworkCollector, PageCollector} from './PageCollector.js';
2324
import {listPages} from './tools/pages.js';
2425
import {takeSnapshot} from './tools/snapshot.js';
@@ -94,26 +95,24 @@ export class McpContext implements Context {
9495
this.browser = browser;
9596
this.logger = logger;
9697

97-
this.#networkCollector = new NetworkCollector(
98-
this.browser,
99-
(page, collect) => {
100-
page.on('request', request => {
98+
this.#networkCollector = new NetworkCollector(this.browser, collect => {
99+
return {
100+
request: request => {
101101
collect(request);
102-
});
103-
},
104-
);
102+
},
103+
} as ListenerMap;
104+
});
105105

106-
this.#consoleCollector = new PageCollector(
107-
this.browser,
108-
(page, collect) => {
109-
page.on('console', event => {
106+
this.#consoleCollector = new PageCollector(this.browser, collect => {
107+
return {
108+
console: event => {
110109
collect(event);
111-
});
112-
page.on('pageerror', event => {
110+
},
111+
pageerror: event => {
113112
collect(event);
114-
});
115-
},
116-
);
113+
},
114+
} as ListenerMap;
115+
});
117116
}
118117

119118
async #init() {

src/PageCollector.ts

Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,26 @@
44
* SPDX-License-Identifier: Apache-2.0
55
*/
66

7-
import type {Browser, HTTPRequest, Page} from 'puppeteer-core';
7+
import {
8+
type Browser,
9+
type Frame,
10+
type Handler,
11+
type HTTPRequest,
12+
type Page,
13+
type PageEvents,
14+
} from 'puppeteer-core';
15+
16+
export type ListenerMap<EventMap extends PageEvents = PageEvents> = {
17+
[K in keyof EventMap]?: (event: EventMap[K]) => void;
18+
// request: (event: PageEvents['request']) => void;
19+
};
820

921
export class PageCollector<T> {
1022
#browser: Browser;
11-
#initializer: (page: Page, collector: (item: T) => void) => void;
23+
#listenersInitializer: (
24+
collector: (item: T) => void,
25+
) => ListenerMap<PageEvents>;
26+
#listeners = new WeakMap<Page, ListenerMap>();
1227
/**
1328
* The Array in this map should only be set once
1429
* As we use the reference to it.
@@ -18,10 +33,10 @@ export class PageCollector<T> {
1833

1934
constructor(
2035
browser: Browser,
21-
initializer: (page: Page, collector: (item: T) => void) => void,
36+
listeners: (collector: (item: T) => void) => ListenerMap<PageEvents>,
2237
) {
2338
this.#browser = browser;
24-
this.#initializer = initializer;
39+
this.#listenersInitializer = listeners;
2540
}
2641

2742
async init() {
@@ -37,6 +52,13 @@ export class PageCollector<T> {
3752
}
3853
this.#initializePage(page);
3954
});
55+
this.#browser.on('targetdestroyed', async target => {
56+
const page = await target.page();
57+
if (!page) {
58+
return;
59+
}
60+
this.#cleanupPageDestroyed(page);
61+
});
4062
}
4163

4264
public addPage(page: Page) {
@@ -50,34 +72,48 @@ export class PageCollector<T> {
5072

5173
const stored: T[] = [];
5274
this.storage.set(page, stored);
53-
54-
page.on('framenavigated', frame => {
75+
const listeners = this.#listenersInitializer(value => {
76+
stored.push(value);
77+
});
78+
listeners['framenavigated'] = (frame: Frame) => {
5579
// Only reset the storage on main frame navigation
5680
if (frame !== page.mainFrame()) {
5781
return;
5882
}
59-
this.cleanup(page);
60-
});
61-
this.#initializer(page, value => {
62-
stored.push(value);
63-
});
83+
this.cleanupAfterNavigation(page);
84+
};
85+
86+
for (const [name, listener] of Object.entries(listeners)) {
87+
page.on(name, listener as Handler<unknown>);
88+
}
89+
90+
this.#listeners.set(page, listeners);
6491
}
6592

66-
protected cleanup(page: Page) {
93+
protected cleanupAfterNavigation(page: Page) {
6794
const collection = this.storage.get(page);
6895
if (collection) {
6996
// Keep the reference alive
7097
collection.length = 0;
7198
}
7299
}
73100

101+
#cleanupPageDestroyed(page: Page) {
102+
const listeners = this.#listeners.get(page);
103+
if (listeners) {
104+
for (const [name, listener] of Object.entries(listeners)) {
105+
page.off(name, listener as Handler<unknown>);
106+
}
107+
}
108+
}
109+
74110
getData(page: Page): T[] {
75111
return this.storage.get(page) ?? [];
76112
}
77113
}
78114

79115
export class NetworkCollector extends PageCollector<HTTPRequest> {
80-
override cleanup(page: Page) {
116+
override cleanupAfterNavigation(page: Page) {
81117
const requests = this.storage.get(page) ?? [];
82118
if (!requests) {
83119
return;

tests/PageCollector.test.ts

Lines changed: 31 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {describe, it} from 'node:test';
88

99
import type {Browser, Frame, Page, Target} from 'puppeteer-core';
1010

11+
import type {ListenerMap} from '../src/PageCollector.js';
1112
import {PageCollector} from '../src/PageCollector.js';
1213

1314
import {getMockRequest} from './utils.js';
@@ -55,10 +56,12 @@ describe('PageCollector', () => {
5556
const browser = getMockBrowser();
5657
const page = (await browser.pages())[0];
5758
const request = getMockRequest();
58-
const collector = new PageCollector(browser, (page, collect) => {
59-
page.on('request', req => {
60-
collect(req);
61-
});
59+
const collector = new PageCollector(browser, collect => {
60+
return {
61+
request: req => {
62+
collect(req);
63+
},
64+
} as ListenerMap;
6265
});
6366
await collector.init();
6467
page.emit('request', request);
@@ -71,10 +74,12 @@ describe('PageCollector', () => {
7174
const page = (await browser.pages())[0];
7275
const mainFrame = page.mainFrame();
7376
const request = getMockRequest();
74-
const collector = new PageCollector(browser, (page, collect) => {
75-
page.on('request', req => {
76-
collect(req);
77-
});
77+
const collector = new PageCollector(browser, collect => {
78+
return {
79+
request: req => {
80+
collect(req);
81+
},
82+
} as ListenerMap;
7883
});
7984
await collector.init();
8085
page.emit('request', request);
@@ -89,10 +94,12 @@ describe('PageCollector', () => {
8994
const browser = getMockBrowser();
9095
const page = (await browser.pages())[0];
9196
const request = getMockRequest();
92-
const collector = new PageCollector(browser, (page, collect) => {
93-
page.on('request', req => {
94-
collect(req);
95-
});
97+
const collector = new PageCollector(browser, collect => {
98+
return {
99+
request: req => {
100+
collect(req);
101+
},
102+
} as ListenerMap;
96103
});
97104
await collector.init();
98105
page.emit('request', request);
@@ -106,10 +113,12 @@ describe('PageCollector', () => {
106113
const page = (await browser.pages())[0];
107114
const mainFrame = page.mainFrame();
108115
const request = getMockRequest();
109-
const collector = new PageCollector(browser, (page, collect) => {
110-
page.on('request', req => {
111-
collect(req);
112-
});
116+
const collector = new PageCollector(browser, collect => {
117+
return {
118+
request: req => {
119+
collect(req);
120+
},
121+
} as ListenerMap;
113122
});
114123
await collector.init();
115124
page.emit('request', request);
@@ -128,10 +137,12 @@ describe('PageCollector', () => {
128137
const browser = getMockBrowser();
129138
const page = (await browser.pages())[0];
130139
const request = getMockRequest();
131-
const collector = new PageCollector(browser, (pageListener, collect) => {
132-
pageListener.on('request', req => {
133-
collect(req);
134-
});
140+
const collector = new PageCollector(browser, collect => {
141+
return {
142+
request: req => {
143+
collect(req);
144+
},
145+
} as ListenerMap;
135146
});
136147
await collector.init();
137148
browser.emit('targetcreated', {

0 commit comments

Comments
 (0)