Auto-resolve promises in TypeScript codegen to prevent silent RPC drops#15901
Auto-resolve promises in TypeScript codegen to prevent silent RPC drops#15901
Conversation
|
🚀 Dogfood this PR with:
curl -fsSL https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.sh | bash -s -- 15901Or
iex "& { $(irm https://raw.githubusercontent.com/microsoft/aspire/main/eng/scripts/get-aspire-cli-pr.ps1) } 15901" |
There was a problem hiding this comment.
Pull request overview
This PR updates the TypeScript ATS codegen/runtime to ensure fluent RPC calls execute even when users don’t await fluent chains, addressing silent drops in publish/deploy scenarios (fixes #15899).
Changes:
- Widen generated handle-typed parameters to accept
PromiseLike<T>and auto-resolve promise-like inputs before RPC argument marshalling. - Add client-side tracking of pending fluent-call promises and flush them before
build()proceeds. - Update TypeScript codegen snapshots/tests and refresh the TypeScript AppHost playground to demonstrate “no-await” chaining.
Show a summary per file
| File | Description |
|---|---|
| tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/transport.verified.ts | Snapshot updates for isPromiseLike export + promise tracking/flush on the client. |
| tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/Snapshots/AtsGeneratedAspire.verified.ts | Snapshot updates reflecting widened types, promise resolution, and PromiseImpl client threading. |
| tests/Aspire.Hosting.CodeGeneration.TypeScript.Tests/AtsTypeScriptCodeGeneratorTests.cs | Adjust assertions to expect PromiseLike<...> in generated signatures. |
| src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts | Implements trackPromise() / flushPendingPromises() and exports isPromiseLike(). |
| src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs | Core generator updates: widen inputs, resolve promises before RPC, track promises, flush before build. |
| playground/TypeScriptAppHost/vite-frontend/package-lock.json | Updates lockfile metadata (engines/peer flags). |
| playground/TypeScriptAppHost/aspire.config.json | Sets sdk version/channel for the playground. |
| playground/TypeScriptAppHost/apphost.ts | Updates sample to rely on promise-friendly chaining and build-time flushing. |
Copilot's findings
Files not reviewed (1)
- playground/TypeScriptAppHost/vite-frontend/package-lock.json: Language not supported
- Files reviewed: 5/8 changed files
- Comments generated: 3
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs
Outdated
Show resolved
Hide resolved
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs
Outdated
Show resolved
Hide resolved
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs
Show resolved
Hide resolved
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
|
Re-running the failed jobs in the CI workflow for this pull request because 1 job was identified as retry-safe transient failures in the CI run attempt.
|
JamesNK
left a comment
There was a problem hiding this comment.
3 issues found (1 deadlock bug, 1 stale snapshot, 1 potential unhandled rejection).
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs
Outdated
Show resolved
Hide resolved
src/Aspire.Hosting.CodeGeneration.TypeScript/AtsTypeScriptCodeGenerator.cs
Outdated
Show resolved
Hide resolved
src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts
Outdated
Show resolved
Hide resolved
src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts
Outdated
Show resolved
Hide resolved
45a9a68 to
00b2f0a
Compare
JamesNK
left a comment
There was a problem hiding this comment.
2 issues found:\n- 1 bug — flushPendingPromises() while loop deadlocks when the build promise is tracked after flush begins (transport.ts)\n- 1 test gap — E2E test validates type-checking only, not runtime flush behavior (TypeScriptCodegenValidationTests.cs)
b53b0d1 to
51198db
Compare
JamesNK
left a comment
There was a problem hiding this comment.
1 issue found (bug). Duplicate error recording in trackPromise/flushPendingPromises — rejected promise errors are pushed into _rejectedErrors from both the trackPromise handler and the allSettled loop.
src/Aspire.Hosting.CodeGeneration.TypeScript/Resources/transport.ts
Outdated
Show resolved
Hide resolved
7319b95 to
e61bf76
Compare
e61bf76 to
4add4d3
Compare
f187fcb to
baccbd3
Compare
When users write un-awaited fluent chains like:
const db = builder.addPostgres('pg').addDatabase('db');
builder.addContainer('c', 'nginx').withReference(db);
await builder.build().run();
the generated TypeScript now accepts PromiseLike<T> for handle parameters
(via Awaitable<T>) and automatically resolves them before RPC calls.
build() flushes all pending un-awaited promises before proceeding.
Key design decisions in transport.ts:
- trackPromise: .then(resolve, reject) removes from set on settlement.
Reject handler swallows errors to prevent Node.js unhandled-rejection
crashes. Errors are NOT eagerly collected — only promises still pending
at flush time are observed, so user-caught rejections are not re-thrown.
- flushPendingPromises: snapshots the pending set before awaiting to
prevent deadlock. The build promise is tracked by the PromiseImpl
constructor while flush is suspended — without the snapshot, a while-loop
re-check would find and await it, causing a circular await.
- Promise.allSettled collects all errors as AggregateError.
Testing:
- 8 vitest unit tests for trackPromise/flushPendingPromises covering
deadlock prevention, error aggregation, and user-caught rejection safety
- E2E test with aspire start/stop validating runtime behavior
- 73 snapshot tests for generated code shape
Fixes #15899
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
baccbd3 to
d15b24a
Compare
|
🎬 CLI E2E Test Recordings — 56 recordings uploaded (commit View recordings
📹 Recordings uploaded automatically from CI run #24229512972 |
Summary
When users write un-awaited fluent chains in TypeScript AppHosts:
the generated TypeScript now accepts
PromiseLike<T>for handle parameters (viaAwaitable<T>) and automatically resolves them before RPC calls.build()flushes all pending un-awaited promises before proceeding.Fixes #15899
Key design decisions
trackPromise/flushPendingPromises(transport.ts)flushPendingPromisessnapshots the pending set before awaiting to prevent deadlock. The build promise is tracked by thePromiseImplconstructor while flush is suspended — without the snapshot, a while-loop re-check would find it and deadlock.track = falsefor build(): ThePromiseImplconstructor accepts atrackparameter.build()passesfalseso the build promise is never in the pending set — prevents poisoning_rejectedErrorswithAggregateErrorfrom a failed flush.Set<unknown>: Rejected tracked promises are eagerly collected in_rejectedErrors(deduped by reference identity).flushPendingPromisesdrains and throws asAggregateError.throwOnPendingRejectionsoption: Defaults totrue(fail loud). Opt out viacreateBuilder({ throwOnPendingRejections: false }). JavaScript cannot distinguish user-caught rejections from unobserved ones — this is a documented tradeoff..then()callback gap: Promises tracked by.then()callbacks during flush are excluded by the snapshot. This is a known limitation — the generated fluent API tracks promises directly (resource.withX()), not via.then().Codegen (AtsTypeScriptCodeGenerator.cs)
Awaitable<T>(union ofT | PromiseLike<T>)isPromiseLikeguard resolves promise-like parameters before RPC args constructionbuild()public wrapper usesflushAndBuildasync closure patternAtsTypeRef, not string-splitTesting
Vitest unit tests (136 tests)
Runtime tests for
trackPromise/flushPendingPromisescovering:.then()callback gap (known limitation, documented in test)throwOnPendingRejectionsstrict/lenient modesE2E test
tsc --noEmitvalidatesAwaitable<T>types compileaspire start/aspire stopvalidates runtime behavior (no deadlock)AssertResourcesExistAsyncverifies un-awaited chains materialized resources viaaspire describe <resource>[CaptureWorkspaceOnFailure]+CaptureAspireDiagnosticsAsyncfor CI debuggabilitySnapshot tests (73 tests)
Generated code shape verified for
flushAndBuildpattern,track=false,Awaitable<T>wideningRelated issues