chore: import upstream snapshot with attribution
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
import type { Dispatcher } from "undici";
|
||||
|
||||
const DISPATCHER_CACHE_KEY = Symbol.for("omniroute.proxyDispatcher.cache");
|
||||
const DEFAULT_DISPATCHER_KEY = Symbol.for("omniroute.proxyDispatcher.default");
|
||||
const RETRY_DISPATCHER_KEY = Symbol.for("omniroute.proxyDispatcher.retry");
|
||||
|
||||
type DispatcherCache = Map<string, Dispatcher>;
|
||||
type GlobalWithDispatcherCache = typeof globalThis & {
|
||||
[DISPATCHER_CACHE_KEY]?: DispatcherCache;
|
||||
[DEFAULT_DISPATCHER_KEY]?: Dispatcher;
|
||||
[RETRY_DISPATCHER_KEY]?: Dispatcher;
|
||||
};
|
||||
|
||||
/**
|
||||
* Direct upstream fan-out dispatcher.
|
||||
*
|
||||
* A single Undici Agent configured with `connections > 1` should be enough in
|
||||
* theory, but real Codex `/backend-api/codex/responses` streams on Node 24 have
|
||||
* still been observed queuing every subsequent same-origin request until the
|
||||
* previous stream emits trailers. Using several one-connection Agents gives
|
||||
* each long SSE stream an independent pool/client and prevents one stream from
|
||||
* monopolizing the effective queue while keeping pipelining disabled.
|
||||
*/
|
||||
class RoundRobinDispatcher {
|
||||
private readonly dispatchers: Dispatcher[];
|
||||
private nextIndex = 0;
|
||||
|
||||
constructor(dispatchers: Dispatcher[]) {
|
||||
this.dispatchers = dispatchers;
|
||||
}
|
||||
|
||||
dispatch(options: Dispatcher.DispatchOptions, handler: Dispatcher.DispatchHandler): boolean {
|
||||
const dispatcher = this.dispatchers[this.nextIndex % this.dispatchers.length];
|
||||
this.nextIndex = (this.nextIndex + 1) % this.dispatchers.length;
|
||||
return dispatcher.dispatch(options, handler);
|
||||
}
|
||||
|
||||
close(callback?: () => void): Promise<void> | void {
|
||||
const done = Promise.all(this.dispatchers.map((dispatcher) => dispatcher.close())).then(
|
||||
() => undefined
|
||||
);
|
||||
if (callback) {
|
||||
done.then(callback);
|
||||
return;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
|
||||
destroy(
|
||||
errorOrCallback?: Error | null | (() => void),
|
||||
callback?: () => void
|
||||
): Promise<void> | void {
|
||||
const callbackFn = typeof errorOrCallback === "function" ? errorOrCallback : callback;
|
||||
const error = typeof errorOrCallback === "function" ? null : (errorOrCallback ?? null);
|
||||
const done = Promise.all(this.dispatchers.map((dispatcher) => dispatcher.destroy(error))).then(
|
||||
() => undefined
|
||||
);
|
||||
if (callbackFn) {
|
||||
done.then(callbackFn);
|
||||
return;
|
||||
}
|
||||
return done;
|
||||
}
|
||||
}
|
||||
|
||||
export function createRoundRobinDispatcher(dispatchers: Dispatcher[]): Dispatcher {
|
||||
return new RoundRobinDispatcher(dispatchers) as unknown as Dispatcher;
|
||||
}
|
||||
|
||||
export function getDispatcherCache(): DispatcherCache {
|
||||
const globalWithCache = globalThis as GlobalWithDispatcherCache;
|
||||
if (!globalWithCache[DISPATCHER_CACHE_KEY]) {
|
||||
globalWithCache[DISPATCHER_CACHE_KEY] = new Map();
|
||||
}
|
||||
return globalWithCache[DISPATCHER_CACHE_KEY];
|
||||
}
|
||||
|
||||
export function getDefaultCachedDispatcher(): Dispatcher | undefined {
|
||||
return (globalThis as GlobalWithDispatcherCache)[DEFAULT_DISPATCHER_KEY];
|
||||
}
|
||||
|
||||
export function setDefaultCachedDispatcher(dispatcher: Dispatcher): void {
|
||||
(globalThis as GlobalWithDispatcherCache)[DEFAULT_DISPATCHER_KEY] = dispatcher;
|
||||
}
|
||||
|
||||
export function getRetryCachedDispatcher(): Dispatcher | undefined {
|
||||
return (globalThis as GlobalWithDispatcherCache)[RETRY_DISPATCHER_KEY];
|
||||
}
|
||||
|
||||
export function setRetryCachedDispatcher(dispatcher: Dispatcher): void {
|
||||
(globalThis as GlobalWithDispatcherCache)[RETRY_DISPATCHER_KEY] = dispatcher;
|
||||
}
|
||||
|
||||
function closeDispatcher(dispatcher: Dispatcher | undefined): void {
|
||||
if (!dispatcher) return;
|
||||
try {
|
||||
const result = dispatcher.close();
|
||||
if (result && typeof (result as Promise<void>).catch === "function") {
|
||||
void (result as Promise<void>).catch(() => {});
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all cached proxy dispatchers.
|
||||
* Call this when proxy configuration changes to avoid stale connections.
|
||||
*/
|
||||
export function clearDispatcherCache(): void {
|
||||
const cache = getDispatcherCache();
|
||||
for (const dispatcher of cache.values()) {
|
||||
closeDispatcher(dispatcher);
|
||||
}
|
||||
cache.clear();
|
||||
|
||||
const globalWithCache = globalThis as GlobalWithDispatcherCache;
|
||||
closeDispatcher(globalWithCache[DEFAULT_DISPATCHER_KEY]);
|
||||
closeDispatcher(globalWithCache[RETRY_DISPATCHER_KEY]);
|
||||
delete globalWithCache[DEFAULT_DISPATCHER_KEY];
|
||||
delete globalWithCache[RETRY_DISPATCHER_KEY];
|
||||
}
|
||||
|
||||
export function __cacheProxyDispatcherForTest(key: string, dispatcher: Dispatcher): void {
|
||||
getDispatcherCache().set(key, dispatcher);
|
||||
}
|
||||
Reference in New Issue
Block a user