Skip to content

Commit c47a8ee

Browse files
refactor: store the last 3 navigations in PageCollector
1 parent 7d78e9c commit c47a8ee

File tree

4 files changed

+99
-33
lines changed

4 files changed

+99
-33
lines changed

src/McpContext.ts

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -95,13 +95,7 @@ export class McpContext implements Context {
9595
this.browser = browser;
9696
this.logger = logger;
9797

98-
this.#networkCollector = new NetworkCollector(this.browser, collect => {
99-
return {
100-
request: request => {
101-
collect(request);
102-
},
103-
} as ListenerMap;
104-
});
98+
this.#networkCollector = new NetworkCollector(this.browser);
10599

106100
this.#consoleCollector = new PageCollector(this.browser, collect => {
107101
return {

src/PageCollector.ts

Lines changed: 57 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -38,12 +38,14 @@ export class PageCollector<T> {
3838
collector: (item: T) => void,
3939
) => ListenerMap<PageEvents>;
4040
#listeners = new WeakMap<Page, ListenerMap>();
41+
#maxNavigationSaved = 3;
42+
4143
/**
42-
* The Array in this map should only be set once
43-
* As we use the reference to it.
44-
* Use methods that manipulate the array in place.
44+
* This maps a Page to a list of navigations with a sub-list
45+
* of all collected resources.
46+
* The newer navigations come first.
4547
*/
46-
protected storage = new WeakMap<Page, Array<WithSymbolId<T>>>();
48+
protected storage = new WeakMap<Page, Array<Array<WithSymbolId<T>>>>();
4749

4850
constructor(
4951
browser: Browser,
@@ -85,20 +87,23 @@ export class PageCollector<T> {
8587
}
8688

8789
const idGenerator = createIdGenerator();
88-
const stored: Array<WithSymbolId<T>> = [];
89-
this.storage.set(page, stored);
90+
const storedLists: Array<Array<WithSymbolId<T>>> = [[]];
91+
this.storage.set(page, storedLists);
9092

9193
const listeners = this.#listenersInitializer(value => {
9294
const withId = value as WithSymbolId<T>;
9395
withId[stableIdSymbol] = idGenerator();
94-
stored.push(withId);
96+
97+
const navigations = this.storage.get(page) ?? [[]];
98+
navigations[0].push(withId);
9599
});
100+
96101
listeners['framenavigated'] = (frame: Frame) => {
97-
// Only reset the storage on main frame navigation
102+
// Only split the storage on main frame navigation
98103
if (frame !== page.mainFrame()) {
99104
return;
100105
}
101-
this.cleanupAfterNavigation(page);
106+
this.splitAfterNavigation(page);
102107
};
103108

104109
for (const [name, listener] of Object.entries(listeners)) {
@@ -108,12 +113,14 @@ export class PageCollector<T> {
108113
this.#listeners.set(page, listeners);
109114
}
110115

111-
protected cleanupAfterNavigation(page: Page) {
112-
const collection = this.storage.get(page);
113-
if (collection) {
114-
// Keep the reference alive
115-
collection.length = 0;
116+
protected splitAfterNavigation(page: Page) {
117+
const navigations = this.storage.get(page);
118+
if (!navigations) {
119+
return;
116120
}
121+
// Add the latest navigation first
122+
navigations.unshift([]);
123+
navigations.splice(this.#maxNavigationSaved);
117124
}
118125

119126
#cleanupPageDestroyed(page: Page) {
@@ -127,22 +134,28 @@ export class PageCollector<T> {
127134
}
128135

129136
getData(page: Page): T[] {
130-
return this.storage.get(page) ?? [];
137+
const navigations = this.storage.get(page);
138+
if (!navigations) {
139+
return [];
140+
}
141+
return navigations[0];
131142
}
132143

133144
getIdForResource(resource: WithSymbolId<T>): number {
134145
return resource[stableIdSymbol] ?? -1;
135146
}
136147

137148
getById(page: Page, stableId: number): T {
138-
const data = this.storage.get(page);
139-
if (!data || !data.length) {
149+
const navigations = this.storage.get(page);
150+
if (!navigations) {
140151
throw new Error('No requests found for selected page');
141152
}
142153

143-
for (const collected of data) {
144-
if (collected[stableIdSymbol] === stableId) {
145-
return collected;
154+
for (const navigation of navigations) {
155+
for (const collected of navigation) {
156+
if (collected[stableIdSymbol] === stableId) {
157+
return collected;
158+
}
146159
}
147160
}
148161

@@ -151,19 +164,39 @@ export class PageCollector<T> {
151164
}
152165

153166
export class NetworkCollector extends PageCollector<HTTPRequest> {
154-
override cleanupAfterNavigation(page: Page) {
155-
const requests = this.storage.get(page) ?? [];
156-
if (!requests) {
167+
constructor(browser: Browser) {
168+
super(browser, collect => {
169+
return {
170+
request: req => {
171+
collect(req);
172+
},
173+
} as ListenerMap;
174+
});
175+
}
176+
override splitAfterNavigation(page: Page) {
177+
const navigations = this.storage.get(page) ?? [];
178+
if (!navigations) {
157179
return;
158180
}
181+
182+
const requests = navigations[0];
183+
159184
const lastRequestIdx = requests.findLastIndex(request => {
160185
return request.frame() === page.mainFrame()
161186
? request.isNavigationRequest()
162187
: false;
163188
});
189+
164190
// Keep all requests since the last navigation request including that
165191
// navigation request itself.
166192
// Keep the reference
167-
requests.splice(0, Math.max(lastRequestIdx, 0));
193+
if (lastRequestIdx) {
194+
const fromCurrentNavigation = requests.splice(
195+
Math.min(lastRequestIdx, 0),
196+
);
197+
navigations.unshift(fromCurrentNavigation);
198+
} else {
199+
navigations.unshift([]);
200+
}
168201
}
169202
}

tests/PageCollector.test.ts

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import {describe, it} from 'node:test';
99
import type {Browser, Frame, HTTPRequest, Page, Target} from 'puppeteer-core';
1010

1111
import type {ListenerMap} from '../src/PageCollector.js';
12-
import {PageCollector} from '../src/PageCollector.js';
12+
import {NetworkCollector, PageCollector} from '../src/PageCollector.js';
1313

1414
import {getMockRequest} from './utils.js';
1515

@@ -220,3 +220,34 @@ describe('PageCollector', () => {
220220
assert.equal(collector.getIdForResource(request2), 2);
221221
});
222222
});
223+
224+
describe('NetworkCollector', () => {
225+
it('clean up after navigation and be able to add data after', async () => {
226+
const browser = getMockBrowser();
227+
const page = (await browser.pages())[0];
228+
const mainFrame = page.mainFrame();
229+
const request = getMockRequest();
230+
const navRequest = getMockRequest({
231+
navigationRequest: true,
232+
frame: page.mainFrame(),
233+
});
234+
const request2 = getMockRequest();
235+
const collector = new NetworkCollector(browser);
236+
await collector.init();
237+
page.emit('request', request);
238+
page.emit('request', navRequest);
239+
240+
assert.equal(collector.getData(page)[0], request);
241+
assert.equal(collector.getData(page)[1], navRequest);
242+
page.emit('framenavigated', mainFrame);
243+
244+
assert.equal(collector.getData(page).length, 1);
245+
assert.equal(collector.getData(page)[0], navRequest);
246+
247+
page.emit('request', request2);
248+
249+
assert.equal(collector.getData(page).length, 2);
250+
assert.equal(collector.getData(page)[0], navRequest);
251+
assert.equal(collector.getData(page)[0], request2);
252+
});
253+
});

tests/utils.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import logger from 'debug';
77
import type {Browser} from 'puppeteer';
88
import puppeteer from 'puppeteer';
9-
import type {HTTPRequest, HTTPResponse} from 'puppeteer-core';
9+
import type {Frame, HTTPRequest, HTTPResponse} from 'puppeteer-core';
1010

1111
import {McpContext} from '../src/McpContext.js';
1212
import {McpResponse} from '../src/McpResponse.js';
@@ -51,6 +51,8 @@ export function getMockRequest(
5151
postData?: string;
5252
fetchPostData?: Promise<string>;
5353
stableId?: number;
54+
navigationRequest?: boolean;
55+
frame?: Frame;
5456
} = {},
5557
): HTTPRequest {
5658
return {
@@ -86,6 +88,12 @@ export function getMockRequest(
8688
redirectChain(): HTTPRequest[] {
8789
return [];
8890
},
91+
isNavigationRequest() {
92+
return options.navigationRequest ?? false;
93+
},
94+
frame() {
95+
return options.frame ?? ({} as Frame);
96+
},
8997
[stableIdSymbol]: options.stableId ?? 1,
9098
} as unknown as HTTPRequest;
9199
}

0 commit comments

Comments
 (0)