Skip to content

Commit d09d316

Browse files
authored
feat: support multiple fetch_factory instances (#579)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit * **Refactor** * Enhanced internal request handling by encapsulating dispatcher and local storage within a singleton instance for improved reliability. * **Tests** * Added tests validating fetch operations with the new instance-based design, including diagnostics and connection reuse verification. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
1 parent b867e20 commit d09d316

File tree

2 files changed

+83
-19
lines changed

2 files changed

+83
-19
lines changed

src/fetch.ts

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -61,20 +61,14 @@ export type FetchResponseDiagnosticsMessage = {
6161
};
6262

6363
export class FetchFactory {
64-
static #dispatcher: Dispatcher.ComposedDispatcher;
65-
static #opaqueLocalStorage = new AsyncLocalStorage<FetchOpaque>();
64+
#dispatcher?: Dispatcher.ComposedDispatcher;
65+
#opaqueLocalStorage = new AsyncLocalStorage<FetchOpaque>();
6666

67-
static getDispatcher() {
68-
return FetchFactory.#dispatcher ?? getGlobalDispatcher();
69-
}
67+
static #instance = new FetchFactory();
7068

71-
static setDispatcher(dispatcher: Agent) {
72-
FetchFactory.#dispatcher = dispatcher;
73-
}
74-
75-
static setClientOptions(clientOptions: ClientOptions) {
69+
setClientOptions(clientOptions: ClientOptions) {
7670
let dispatcherOption: BaseAgentOptions = {
77-
opaqueLocalStorage: FetchFactory.#opaqueLocalStorage,
71+
opaqueLocalStorage: this.#opaqueLocalStorage,
7872
};
7973
let dispatcherClazz: new (options: BaseAgentOptions) => BaseAgent = BaseAgent;
8074
if (clientOptions?.lookup || clientOptions?.checkAddress) {
@@ -101,12 +95,20 @@ export class FetchFactory {
10195
} as HttpAgentOptions;
10296
dispatcherClazz = BaseAgent;
10397
}
104-
FetchFactory.#dispatcher = new dispatcherClazz(dispatcherOption);
98+
this.#dispatcher = new dispatcherClazz(dispatcherOption);
10599
initDiagnosticsChannel();
106100
}
107101

108-
static getDispatcherPoolStats() {
109-
const agent = FetchFactory.getDispatcher();
102+
getDispatcher() {
103+
return this.#dispatcher ?? getGlobalDispatcher();
104+
}
105+
106+
setDispatcher(dispatcher: Agent) {
107+
this.#dispatcher = dispatcher;
108+
}
109+
110+
getDispatcherPoolStats() {
111+
const agent = this.getDispatcher();
110112
// origin => Pool Instance
111113
const clients: Map<string, WeakRef<Pool>> | undefined = Reflect.get(agent, undiciSymbols.kClients);
112114
const poolStatsMap: Record<string, PoolStat> = {};
@@ -131,10 +133,18 @@ export class FetchFactory {
131133
return poolStatsMap;
132134
}
133135

134-
static async fetch(input: RequestInfo, init?: UrllibRequestInit): Promise<Response> {
136+
static setClientOptions(clientOptions: ClientOptions) {
137+
FetchFactory.#instance.setClientOptions(clientOptions);
138+
}
139+
140+
static getDispatcherPoolStats() {
141+
return FetchFactory.#instance.getDispatcherPoolStats();
142+
}
143+
144+
async fetch(input: RequestInfo, init?: UrllibRequestInit): Promise<Response> {
135145
const requestStartTime = performance.now();
136146
init = init ?? {};
137-
init.dispatcher = init.dispatcher ?? FetchFactory.#dispatcher;
147+
init.dispatcher = init.dispatcher ?? this.#dispatcher;
138148
const request = new Request(input, init);
139149
const requestId = globalId('HttpClientRequest');
140150
// https://developer.chrome.com/docs/devtools/network/reference/?utm_source=devtools#timing-explanation
@@ -219,7 +229,7 @@ export class FetchFactory {
219229
socketErrorRetries: 0,
220230
} as any as RawResponseWithMeta;
221231
try {
222-
await FetchFactory.#opaqueLocalStorage.run(internalOpaque, async () => {
232+
await this.#opaqueLocalStorage.run(internalOpaque, async () => {
223233
res = await UndiciFetch(request);
224234
});
225235
} catch (e: any) {
@@ -262,6 +272,18 @@ export class FetchFactory {
262272
} as ResponseDiagnosticsMessage);
263273
return res!;
264274
}
275+
276+
static getDispatcher() {
277+
return FetchFactory.#instance.getDispatcher();
278+
}
279+
280+
static setDispatcher(dispatcher: Agent) {
281+
FetchFactory.#instance.setDispatcher(dispatcher);
282+
}
283+
284+
static async fetch(input: RequestInfo, init?: UrllibRequestInit): Promise<Response> {
285+
return FetchFactory.#instance.fetch(input, init);
286+
}
265287
}
266288

267289
export const fetch = FetchFactory.fetch;

test/fetch.test.ts

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,6 @@ describe('fetch.test.ts', () => {
5858
await sleep(1);
5959
// again, keep alive
6060
response = await fetch(`${_url}html`);
61-
// console.log(responseDiagnosticsMessage!.response.socket);
6261
assert(responseDiagnosticsMessage!.response.socket.handledRequests > 1);
6362
assert(responseDiagnosticsMessage!.response.socket.handledResponses > 1);
6463

@@ -99,7 +98,6 @@ describe('fetch.test.ts', () => {
9998
assert(requestDiagnosticsMessage!.request);
10099
assert(responseDiagnosticsMessage!.request);
101100
assert(responseDiagnosticsMessage!.response);
102-
// console.log(responseDiagnosticsMessage!.response.socket);
103101
assert(responseDiagnosticsMessage!.response.socket.localAddress);
104102
assert([ '127.0.0.1', '::1' ].includes(responseDiagnosticsMessage!.response.socket.localAddress));
105103

@@ -120,4 +118,48 @@ describe('fetch.test.ts', () => {
120118
await fetch(request);
121119
}, /Cannot construct a Request with a Request object that has already been used/);
122120
});
121+
122+
it('fetch with new FetchFactory instance should work', async () => {
123+
let requestDiagnosticsMessage: RequestDiagnosticsMessage;
124+
let responseDiagnosticsMessage: ResponseDiagnosticsMessage;
125+
let fetchDiagnosticsMessage: FetchDiagnosticsMessage;
126+
let fetchResponseDiagnosticsMessage: FetchResponseDiagnosticsMessage;
127+
diagnosticsChannel.subscribe('urllib:request', msg => {
128+
requestDiagnosticsMessage = msg as RequestDiagnosticsMessage;
129+
});
130+
diagnosticsChannel.subscribe('urllib:response', msg => {
131+
responseDiagnosticsMessage = msg as ResponseDiagnosticsMessage;
132+
});
133+
diagnosticsChannel.subscribe('urllib:fetch:request', msg => {
134+
fetchDiagnosticsMessage = msg as FetchDiagnosticsMessage;
135+
});
136+
diagnosticsChannel.subscribe('urllib:fetch:response', msg => {
137+
fetchResponseDiagnosticsMessage = msg as FetchResponseDiagnosticsMessage;
138+
});
139+
const factory = new FetchFactory();
140+
factory.setClientOptions({});
141+
let response = await factory.fetch(`${_url}html`);
142+
143+
assert(response);
144+
assert(requestDiagnosticsMessage!.request);
145+
assert(responseDiagnosticsMessage!.request);
146+
assert(responseDiagnosticsMessage!.response);
147+
assert(responseDiagnosticsMessage!.response.socket.localAddress);
148+
assert([ '127.0.0.1', '::1' ].includes(responseDiagnosticsMessage!.response.socket.localAddress));
149+
150+
assert(fetchDiagnosticsMessage!.fetch);
151+
assert(fetchResponseDiagnosticsMessage!.fetch);
152+
assert(fetchResponseDiagnosticsMessage!.response);
153+
assert(fetchResponseDiagnosticsMessage!.timingInfo);
154+
155+
await sleep(1);
156+
// again, keep alive
157+
response = await factory.fetch(`${_url}html`);
158+
assert(responseDiagnosticsMessage!.response.socket.handledRequests > 1);
159+
assert(responseDiagnosticsMessage!.response.socket.handledResponses > 1);
160+
161+
const stats = factory.getDispatcherPoolStats();
162+
assert(stats);
163+
assert(Object.keys(stats).length > 0);
164+
});
123165
});

0 commit comments

Comments
 (0)