-
Notifications
You must be signed in to change notification settings - Fork 13.8k
Add defer functionality to generator executor #64157
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -22,82 +22,145 @@ export function apiRequest<Method extends keyof APIMethodInfo>( | |
| export function* apiRequest(method: PropertyKey, params: unknown): Generator<{ method: PropertyKey; params: unknown; }, unknown, unknown> { | ||
| return yield { method, params }; | ||
| } | ||
| export type APIRequestGenerator<Return = any> = Generator<APIRequest | readonly APIRequest[], Return, any>; | ||
| const deferredGeneratorMarker: unique symbol = Symbol(); | ||
| interface DeferredAPIRequest { | ||
| readonly deferred: UndeferredAPIRequestGenerator; | ||
| } | ||
| type APIRequestGeneratorYield = APIRequest | readonly APIRequest[] | DeferredAPIRequest; | ||
| export type UndeferredAPIRequestGenerator<Return = any> = Generator<APIRequestGeneratorYield, Return, any>; | ||
| export type DeferredAPIRequestGenerator = Generator<DeferredAPIRequest, void, unknown> & { readonly [deferredGeneratorMarker]: true; }; | ||
| export type APIRequestGenerator<Return = any> = UndeferredAPIRequestGenerator<Return> | DeferredAPIRequestGenerator; | ||
| type GeneratorReturn<T> = T extends Generator<any, infer R, any> ? R : never; | ||
| export type ExecutedGeneratorsResults<T extends readonly APIRequestGenerator[]> = { | ||
| [K in keyof T]: GeneratorReturn<T[K]>; | ||
| }; | ||
| export type ExecutedGeneratorsResults<T extends readonly APIRequestGenerator[]> = number extends T["length"] ? GeneratorReturn<Exclude<T[number], DeferredAPIRequestGenerator>>[] | ||
| : T extends readonly [infer Head extends APIRequestGenerator, ...infer Tail extends readonly APIRequestGenerator[]] ? Head extends DeferredAPIRequestGenerator ? ExecutedGeneratorsResults<Tail> : [GeneratorReturn<Head>, ...ExecutedGeneratorsResults<Tail>] | ||
| : []; | ||
|
|
||
| interface GeneratorResponse { | ||
| result: unknown; | ||
| error?: string | undefined; | ||
| } | ||
|
|
||
| export function all<const T extends readonly APIRequestGenerator[]>( | ||
| ...requestGenerators: T | ||
| ): Generator<APIRequest[], ExecutedGeneratorsResults<T>, readonly GeneratorResponse[]>; | ||
| export function* all<T extends readonly APIRequestGenerator[]>( | ||
| ...requestGenerators: T | ||
| ): Generator<APIRequest[], ExecutedGeneratorsResults<T>, readonly GeneratorResponse[]> { | ||
| const results: any[] = []; | ||
| const requestObjects: (APIRequest | readonly APIRequest[] | undefined)[] = []; | ||
| const completedIndices = new Set<number>(); | ||
| for (let i = 0; i < requestGenerators.length; i++) { | ||
| const state = requestGenerators[i].next(); | ||
| interface RequestRunnerOptions { | ||
| executeDeferred?: boolean; | ||
| getDeduplicationKey?: (request: APIRequest) => string | undefined; | ||
| } | ||
|
|
||
| function createRequestRunner<T extends readonly APIRequestGenerator[]>(requestGenerators: T, options: RequestRunnerOptions = {}) { | ||
| const registeredGenerators = new Set<APIRequestGenerator>(); | ||
| const requestsByGenerator = new Map<APIRequestGenerator, APIRequestGeneratorYield>(); | ||
| const resultsByGenerator = new Map<APIRequestGenerator, unknown>(); | ||
| for (const generator of requestGenerators) { | ||
| addGenerator(generator); | ||
| } | ||
| const requestRounds = runRequestRounds(); | ||
| return { requestRounds, getResults }; | ||
|
|
||
| function advanceGenerator(generator: APIRequestGenerator, value?: unknown, error?: string): void { | ||
| let state = error === undefined | ||
| ? generator.next(value) | ||
| : generator.throw(new Error(error)); | ||
| while (!state.done && isDeferredAPIRequest(state.value) && options.executeDeferred) { | ||
| addGenerator(state.value.deferred); | ||
| state = generator.next(); | ||
| } | ||
| if (state.done) { | ||
| results[i] = state.value; | ||
| completedIndices.add(i); | ||
| requestsByGenerator.delete(generator); | ||
| resultsByGenerator.set(generator, state.value); | ||
| } | ||
| else { | ||
| requestObjects[i] = state.value; | ||
| requestsByGenerator.set(generator, state.value); | ||
| } | ||
| } | ||
| while (completedIndices.size < requestGenerators.length) { | ||
| const requests: APIRequest[] = []; | ||
| const responseIndices: (number | readonly number[] | undefined)[] = []; | ||
| const responseIndexByDeduplicationKey = new Map<string, number>(); | ||
| const addRequest = (request: APIRequest): number => { | ||
| const deduplicationKey = getRequestDeduplicationKey(request); | ||
| let responseIndex = deduplicationKey === undefined ? undefined : responseIndexByDeduplicationKey.get(deduplicationKey); | ||
| if (responseIndex === undefined) { | ||
| responseIndex = requests.length; | ||
| requests.push(request); | ||
| if (deduplicationKey !== undefined) responseIndexByDeduplicationKey.set(deduplicationKey, responseIndex); | ||
| } | ||
| return responseIndex; | ||
| }; | ||
| for (let i = 0; i < requestGenerators.length; i++) { | ||
| if (completedIndices.has(i)) continue; | ||
|
|
||
| const request = requestObjects[i]!; | ||
| responseIndices[i] = isRequestGroup(request) ? request.map(addRequest) : addRequest(request); | ||
| } | ||
| function addGenerator(generator: APIRequestGenerator): void { | ||
| if (registeredGenerators.has(generator)) throw new Error("Cannot execute the same generator instance more than once"); | ||
| registeredGenerators.add(generator); | ||
| advanceGenerator(generator); | ||
| } | ||
|
|
||
| const responses = yield requests; | ||
| for (let i = 0; i < requestGenerators.length; i++) { | ||
| if (completedIndices.has(i)) continue; | ||
|
|
||
| const requestGenerator = requestGenerators[i]; | ||
| const responseIndex = responseIndices[i]!; | ||
| let state: IteratorResult<APIRequest | readonly APIRequest[], any>; | ||
| if (typeof responseIndex === "number") { | ||
| const result = responses[responseIndex]; | ||
| state = result.error | ||
| ? requestGenerator.throw(new Error(result.error)) | ||
| : requestGenerator.next(result.result); | ||
| function* runRequestRounds(): Generator<APIRequest[] | DeferredAPIRequest, void, readonly GeneratorResponse[]> { | ||
| while (requestsByGenerator.size) { | ||
| for (const generator of registeredGenerators) { | ||
| let request = requestsByGenerator.get(generator); | ||
| while (request && isDeferredAPIRequest(request)) { | ||
| yield request; | ||
| advanceGenerator(generator); | ||
| request = requestsByGenerator.get(generator); | ||
| } | ||
| } | ||
| else { | ||
| state = requestGenerator.next(responseIndex.map(index => responses[index])); | ||
| if (!requestsByGenerator.size) break; | ||
|
|
||
| const requests: APIRequest[] = []; | ||
| const responseIndexByDeduplicationKey = new Map<string, number>(); | ||
| const addRequest = (request: APIRequest): number => { | ||
| const deduplicationKey = options.getDeduplicationKey?.(request); | ||
| let responseIndex = deduplicationKey === undefined ? undefined : responseIndexByDeduplicationKey.get(deduplicationKey); | ||
| if (responseIndex === undefined) { | ||
| responseIndex = requests.length; | ||
| requests.push(request); | ||
| if (deduplicationKey !== undefined) responseIndexByDeduplicationKey.set(deduplicationKey, responseIndex); | ||
| } | ||
| return responseIndex; | ||
| }; | ||
| // TODO: Use Iterator.prototype.filter when target >= ES2025 | ||
| const roundGenerators = [...registeredGenerators].filter(generator => requestsByGenerator.has(generator)); | ||
|
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It does, but... nah. Weak set for duplicate detection is like.... sure? But also no? Technically correct but accomplishes very little in practice - GC pressure of generator objects shouldn't be a real concern for any real code. |
||
| const responseIndices = new Map<APIRequestGenerator, number | readonly number[]>(); | ||
| for (const generator of roundGenerators) { | ||
| const request = requestsByGenerator.get(generator) as APIRequest | readonly APIRequest[]; | ||
| responseIndices.set(generator, isRequestGroup(request) ? request.map(addRequest) : addRequest(request)); | ||
| } | ||
| if (state.done) { | ||
| results[i] = state.value; | ||
| completedIndices.add(i); | ||
| requestObjects[i] = undefined; | ||
|
|
||
| const responses = yield requests; | ||
| for (const generator of roundGenerators) { | ||
| const responseIndex = responseIndices.get(generator)!; | ||
| if (typeof responseIndex === "number") { | ||
| const result = responses[responseIndex]; | ||
| advanceGenerator(generator, result.result, result.error || undefined); | ||
|
|
||
| } | ||
| else { | ||
| advanceGenerator(generator, responseIndex.map(index => responses[index])); | ||
| } | ||
| } | ||
| else requestObjects[i] = state.value; | ||
| } | ||
| } | ||
| return results as any[] as ExecutedGeneratorsResults<T>; | ||
|
|
||
| function getResults(): ExecutedGeneratorsResults<T> { | ||
| return requestGenerators | ||
| .filter(generator => !isDeferredGenerator(generator)) | ||
| .map(generator => resultsByGenerator.get(generator)) as ExecutedGeneratorsResults<T>; | ||
| } | ||
| } | ||
|
|
||
| export function all<const T extends readonly APIRequestGenerator[]>( | ||
| ...requestGenerators: T | ||
| ): UndeferredAPIRequestGenerator<ExecutedGeneratorsResults<T>>; | ||
| export function* all<T extends readonly APIRequestGenerator[]>( | ||
| ...requestGenerators: T | ||
| ): UndeferredAPIRequestGenerator<ExecutedGeneratorsResults<T>> { | ||
| const { requestRounds, getResults } = createRequestRunner(requestGenerators); | ||
| yield* requestRounds; | ||
| return getResults(); | ||
| } | ||
|
|
||
| export function executeRequestGenerators<T extends readonly APIRequestGenerator[]>( | ||
| requestGenerators: T, | ||
| executeRequests: (requests: APIRequest[]) => readonly GeneratorResponse[], | ||
| ): ExecutedGeneratorsResults<T> { | ||
| const { requestRounds, getResults } = createRequestRunner(requestGenerators, { executeDeferred: true, getDeduplicationKey: getRequestDeduplicationKey }); | ||
| let state = requestRounds.next(); | ||
| while (!state.done) { | ||
| if (isDeferredAPIRequest(state.value)) throw new Error("Unexpected deferred request"); | ||
| state = requestRounds.next(executeRequests(state.value)); | ||
| } | ||
| return getResults(); | ||
| } | ||
|
|
||
| function isDeferredAPIRequest(request: APIRequestGeneratorYield): request is DeferredAPIRequest { | ||
| return !Array.isArray(request) && "deferred" in request; | ||
| } | ||
|
|
||
| function isDeferredGenerator(generator: APIRequestGenerator): generator is DeferredAPIRequestGenerator { | ||
| return deferredGeneratorMarker in generator; | ||
| } | ||
|
|
||
| function isRequestGroup(request: APIRequest | readonly APIRequest[]): request is readonly APIRequest[] { | ||
|
|
@@ -112,3 +175,11 @@ function getRequestDeduplicationKey(request: APIRequest): string | undefined { | |
| return undefined; | ||
| } | ||
| } | ||
|
|
||
| export function defer(gen: UndeferredAPIRequestGenerator): DeferredAPIRequestGenerator { | ||
| const deferred = (function* (): Generator<DeferredAPIRequest, void, unknown> { | ||
| yield { deferred: gen }; | ||
| })() as DeferredAPIRequestGenerator; | ||
| Object.defineProperty(deferred, deferredGeneratorMarker, { value: true }); | ||
| return deferred; | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we expose
UndeferredAPIRequestGeneratorandAPIRequestGenerator. If the generators are recursive they need a type. We could redefine some of these types on the usage side but they are now more complicated that a simple generator.If we expose it, I'm not sure
UndeferredAPIRequestGeneratoris the best name for the common case of the generator, and this is the type that is needed as the return type sinceAPIRequestGeneratoris a union and does not work well as a generator return type: