Skip to content

Commit 76d5e94

Browse files
refactor: dispose listeners on page destroyed (#318)
1 parent dced0f0 commit 76d5e94

File tree

3 files changed

+128
-49
lines changed

3 files changed

+128
-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: 50 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,25 @@
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+
};
819

920
export class PageCollector<T> {
1021
#browser: Browser;
11-
#initializer: (page: Page, collector: (item: T) => void) => void;
22+
#listenersInitializer: (
23+
collector: (item: T) => void,
24+
) => ListenerMap<PageEvents>;
25+
#listeners = new WeakMap<Page, ListenerMap>();
1226
/**
1327
* The Array in this map should only be set once
1428
* As we use the reference to it.
@@ -18,10 +32,10 @@ export class PageCollector<T> {
1832

1933
constructor(
2034
browser: Browser,
21-
initializer: (page: Page, collector: (item: T) => void) => void,
35+
listeners: (collector: (item: T) => void) => ListenerMap<PageEvents>,
2236
) {
2337
this.#browser = browser;
24-
this.#initializer = initializer;
38+
this.#listenersInitializer = listeners;
2539
}
2640

2741
async init() {
@@ -37,6 +51,14 @@ export class PageCollector<T> {
3751
}
3852
this.#initializePage(page);
3953
});
54+
this.#browser.on('targetdestroyed', async target => {
55+
const page = await target.page();
56+
if (!page) {
57+
return;
58+
}
59+
console.log('destro');
60+
this.#cleanupPageDestroyed(page);
61+
});
4062
}
4163

4264
public addPage(page: Page) {
@@ -50,34 +72,49 @@ 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+
this.storage.delete(page);
109+
}
110+
74111
getData(page: Page): T[] {
75112
return this.storage.get(page) ?? [];
76113
}
77114
}
78115

79116
export class NetworkCollector extends PageCollector<HTTPRequest> {
80-
override cleanup(page: Page) {
117+
override cleanupAfterNavigation(page: Page) {
81118
const requests = this.storage.get(page) ?? [];
82119
if (!requests) {
83120
return;

tests/PageCollector.test.ts

Lines changed: 63 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';
@@ -22,6 +23,9 @@ function mockListener() {
2223
listeners[eventName] = [listener];
2324
}
2425
},
26+
off(_eventName: string, _listener: (data: unknown) => void) {
27+
// no-op
28+
},
2529
emit(eventName: string, data: unknown) {
2630
for (const listener of listeners[eventName] ?? []) {
2731
listener(data);
@@ -55,10 +59,12 @@ describe('PageCollector', () => {
5559
const browser = getMockBrowser();
5660
const page = (await browser.pages())[0];
5761
const request = getMockRequest();
58-
const collector = new PageCollector(browser, (page, collect) => {
59-
page.on('request', req => {
60-
collect(req);
61-
});
62+
const collector = new PageCollector(browser, collect => {
63+
return {
64+
request: req => {
65+
collect(req);
66+
},
67+
} as ListenerMap;
6268
});
6369
await collector.init();
6470
page.emit('request', request);
@@ -71,10 +77,12 @@ describe('PageCollector', () => {
7177
const page = (await browser.pages())[0];
7278
const mainFrame = page.mainFrame();
7379
const request = getMockRequest();
74-
const collector = new PageCollector(browser, (page, collect) => {
75-
page.on('request', req => {
76-
collect(req);
77-
});
80+
const collector = new PageCollector(browser, collect => {
81+
return {
82+
request: req => {
83+
collect(req);
84+
},
85+
} as ListenerMap;
7886
});
7987
await collector.init();
8088
page.emit('request', request);
@@ -89,10 +97,12 @@ describe('PageCollector', () => {
8997
const browser = getMockBrowser();
9098
const page = (await browser.pages())[0];
9199
const request = getMockRequest();
92-
const collector = new PageCollector(browser, (page, collect) => {
93-
page.on('request', req => {
94-
collect(req);
95-
});
100+
const collector = new PageCollector(browser, collect => {
101+
return {
102+
request: req => {
103+
collect(req);
104+
},
105+
} as ListenerMap;
96106
});
97107
await collector.init();
98108
page.emit('request', request);
@@ -106,10 +116,12 @@ describe('PageCollector', () => {
106116
const page = (await browser.pages())[0];
107117
const mainFrame = page.mainFrame();
108118
const request = getMockRequest();
109-
const collector = new PageCollector(browser, (page, collect) => {
110-
page.on('request', req => {
111-
collect(req);
112-
});
119+
const collector = new PageCollector(browser, collect => {
120+
return {
121+
request: req => {
122+
collect(req);
123+
},
124+
} as ListenerMap;
113125
});
114126
await collector.init();
115127
page.emit('request', request);
@@ -128,10 +140,12 @@ describe('PageCollector', () => {
128140
const browser = getMockBrowser();
129141
const page = (await browser.pages())[0];
130142
const request = getMockRequest();
131-
const collector = new PageCollector(browser, (pageListener, collect) => {
132-
pageListener.on('request', req => {
133-
collect(req);
134-
});
143+
const collector = new PageCollector(browser, collect => {
144+
return {
145+
request: req => {
146+
collect(req);
147+
},
148+
} as ListenerMap;
135149
});
136150
await collector.init();
137151
browser.emit('targetcreated', {
@@ -153,4 +167,33 @@ describe('PageCollector', () => {
153167

154168
assert.equal(collector.getData(page).length, 2);
155169
});
170+
171+
it('should clear data on page destroy', async () => {
172+
const browser = getMockBrowser();
173+
const page = (await browser.pages())[0];
174+
const request = getMockRequest();
175+
const collector = new PageCollector(browser, collect => {
176+
return {
177+
request: req => {
178+
collect(req);
179+
},
180+
} as ListenerMap;
181+
});
182+
await collector.init();
183+
184+
page.emit('request', request);
185+
186+
assert.equal(collector.getData(page).length, 1);
187+
188+
browser.emit('targetdestroyed', {
189+
page() {
190+
return Promise.resolve(page);
191+
},
192+
} as Target);
193+
194+
// The page inside part is async so we need to await some time
195+
await new Promise<void>(res => res());
196+
197+
assert.equal(collector.getData(page).length, 0);
198+
});
156199
});

0 commit comments

Comments
 (0)