fix(core): keep Hyperdrive pool alive through layout prefetch - #2409
Conversation
🦋 Changeset detectedLatest commit: 84fe141 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-cache | 84fe141 | Aug 10 2026, 01:00 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-playground | 84fe141 | Aug 10 2026, 01:00 PM |
Deploying with
|
| Status | Name | Latest Commit | Updated (UTC) |
|---|---|---|---|
| ✅ Deployment successful! View logs |
emdash-demo-do | 84fe141 | Aug 10 2026, 01:00 PM |
There was a problem hiding this comment.
This PR correctly targets the root cause of #2133: on connection-backed adapters, the request-scoped pool was being closed as soon as the response stream finished, while the concurrently-running layout prefetch still held pool clients. The fix introduces a small lifecycle wrapper that keeps the real adapter close() deferred under after()/waitUntil until both the response stream and the prefetch promise have settled. The approach is idiomatic for EmDash's middleware lifecycle and reuses the existing after() utility, so no new platform concepts or query patterns are introduced.
I read the full changed files and traced the call flow through finishScoped, wrapResponseForScopedClose, and prefetchLayoutData. The helper correctly handles adapters with and without a close, preserves the commit-before-response / close-after-stream invariant, and the error path still resolves the response-settlement signal so the pool isn't torn down underneath in-flight prefetch work. The added unit tests cover bodyless responses, streaming responses, and the no-close adapter path. No new queries are added to the logged-out hot path, no admin UI strings are touched, and a changeset is present. I found no issues to fix.
There was a problem hiding this comment.
This is the right fix for the described race.
The old code deferred prefetchLayoutData() inside after(() => ...), so the prefetch still owned pool clients while the response stream was flushing. For Hyperdrive-backed requests, finishScoped() closed the request-scoped pool at stream-end, which could race with the still-running prefetch and leave the Worker invocation stuck until waitUntil cancellation. The new helper makes the prefetch start immediately, then hands a single after() task that waits for both the response stream to finish and the prefetch to settle before calling the real adapter close(). That matches the existing scoped-db lifecycle model and doesn’t change query counts for the logged-out hot path.
I read the diff, the full changed files, after.ts, prefetch.ts, and the AGENTS.md conventions. The changeset is present, user-facing, and targeted. The tests add focused coverage for the new helper. No new admin UI strings, SQL, or locale-filtered queries are involved. The code is clean, and I have no blocking or suggested changes.
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
Pull request overview
Fixes a Cloudflare Workers + Hyperdrive lifecycle race where request-scoped pg.Pool teardown could start while anonymous layout prefetch was still using pool clients (especially visible on bodyless responses like redirects), leading to waitUntil() cancellation after ~30s.
Changes:
- Add
deferScopedCloseUntilSettled()to defer adapter teardown until both the response has settled and background prefetch work has completed. - Wire anonymous HTML layout prefetch to keep the request-scoped DB lifecycle alive through both response completion and prefetch completion.
- Add unit tests covering bodyless and streaming responses for the new lifecycle deferral behavior, plus a changeset for a patch release.
Reviewed changes
Copilot reviewed 4 out of 4 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| packages/core/src/astro/middleware/scoped-db.ts | Introduces lifecycle deferral helper and refactors lifecycle typing used by finishScoped. |
| packages/core/src/astro/middleware.ts | Uses the new deferral helper when anonymous HTML layout prefetch is enabled. |
| packages/core/tests/unit/middleware/scoped-db.test.ts | Adds unit tests verifying teardown deferral behavior for bodyless/streaming responses. |
| .changeset/fix-hyperdrive-prefetch-teardown.md | Adds a patch changeset describing the Hyperdrive teardown fix. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| const close = scoped.close; | ||
| defer(async () => { | ||
| await Promise.allSettled([pending, responseSettled]); | ||
| close(); | ||
| }); | ||
|
|
||
| return { commit: scoped.commit, close: settleResponse }; | ||
| } |
What does this PR do?
Keeps the anonymous request-scoped database alive until both the response body and the concurrent layout prefetch have settled. This prevents Hyperdrive pool shutdown from racing queries that still own pool clients, which left successful Worker invocations pending until
waitUntil()cancellation.Closes #2133
Type of change
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs — a workflow extracts catalogs on merge tomain.The i18n and Discussion checklist items are not applicable: this changes no admin UI strings and fixes an existing bug.
AI-generated code disclosure
Screenshots / test output
Non-visual change. Verified with:
pnpm lintpnpm typecheckpnpm --dir packages/core exec vitest run tests/unit/middleware/scoped-db.test.ts(18 passed)pnpm buildpnpm format