Add defer functionality to generator executor - #64157
Add defer functionality to generator executor#64157Wesley Wigham (weswigham) wants to merge 1 commit into
Conversation
There was a problem hiding this comment.
🟡 Changes recommended
Deferred failures can prematurely interrupt foreground work, and completed generators cause quadratic scanning.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
Adds deferred generator work to synchronous API batching without returning deferred results.
Changes:
- Adds
deferand deferred-work scheduling. - Refactors request execution, deduplication, and result mapping.
- Adds runtime coverage for deferred execution and errors.
File summaries
| File | Description |
|---|---|
packages/typescript/src/api/sync/generatorSupport.ts |
Implements deferred request execution. |
packages/typescript/src/api/sync/api.ts |
Exposes defer through synchronous batching. |
packages/typescript/src/api/async/api.ts |
Updates sync-generation directives. |
packages/typescript/test/sync/api-generators.test.ts |
Tests batching and deferred behavior. |
Review details
- Files reviewed: 4/4 changed files
- Comments generated: 2
- Review effort level: Balanced
💡 Add a code-review agent skill for context-aware, tailored reviews. Learn more in the docs.
| return responseIndex; | ||
| }; | ||
| // TODO: Use Iterator.prototype.filter when target >= ES2025 | ||
| const roundGenerators = [...registeredGenerators].filter(generator => requestsByGenerator.has(generator)); |
There was a problem hiding this comment.
It does, but... nah. registeredGenerators is ever-growing when deferrals can add new generators to the list each turn, so this is just required - we could track only unfinished ones and finished ones seperately, yes but then we're potentially allocating two new arrays per turn instead of allocating one and rarely growing one. Like the comment says, best would be iterator filter in the future to skip the filtered collection alloc entirely. 🤷♂️
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 responseIndex = responseIndices.get(generator)!; | ||
| if (typeof responseIndex === "number") { | ||
| const result = responses[responseIndex]; | ||
| advanceGenerator(generator, result.result, result.error || undefined); |
| export { all, defer } from "./generatorSupport.ts"; | ||
| import { | ||
| all, | ||
| type APIRequestGenerator, |
There was a problem hiding this comment.
Can we expose UndeferredAPIRequestGenerator and APIRequestGenerator. 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 UndeferredAPIRequestGenerator is the best name for the common case of the generator, and this is the type that is needed as the return type since APIRequestGenerator is a union and does not work well as a generator return type:
function *testNotOk(n: number): sync.APIRequestGenerator<number> {
if(n === 0) return 0;
return yield * testNotOk(n-1)
}
function *testOk(n: number): sync.UndeferredAPIRequestGenerator<number> {
if(n === 0) return 0;
return yield * testOk(n-1)
}
Add a
deferfunction and support to theapi.batchexecutor for it, allowing you to queue work to run within the current batching context without blocking execution of the current active generator - essentially the generator equivalent of an unawaited async function call in the async API.cc Titian Cernicova-Dragomir (@dragomirtitian) who specifically requested built-in support for this, since it's cumbersome to wire up without owning the generator executor, but when you own it, it's comparably not bad to add.