Skip to content

fix: make query.live stream teardown safe - #16790

Draft
Nic-Polumeyv wants to merge 5 commits into
version-3from
live-query-teardown
Draft

fix: make query.live stream teardown safe#16790
Nic-Polumeyv wants to merge 5 commits into
version-3from
live-query-teardown

Conversation

@Nic-Polumeyv

@Nic-Polumeyv Nic-Polumeyv commented Aug 13, 2026

Copy link
Copy Markdown
Contributor

In the live query handler in runtime/server/remote-functions.js, pull checks event.request.signal.aborted once, before awaiting generator.next(). After that it uses the readable stream controller, which may be closed by then (adapters cancel the response body when the client disconnects, and that can happen while pull is suspended on the generator). Enqueueing the value throws "Invalid state: Controller is already closed", and the catch path then calls send() on the same closed controller. Separately, the generator has no way to notice the teardown: its event.request.signal belongs to the incoming request and doesn't abort when the response stream is cancelled, so a generator parked on an await keeps running and its finally never executes (generator.return() queues behind the pending next()).

The fix separates producing values from writing them to the stream. Everything the response sends now comes from a frames async generator that yields SSE strings and never sees the controller; keep-alives come from racing generator.next() against a timer instead of a controller-holding callback. The new stream_from_iterator helper in runtime/server/utils.js is the only code that touches the controller, and it re-checks that the stream is still open after each await, dropping late chunks instead of enqueueing into a closed controller. Teardown from either side (request abort or stream cancellation) aborts a single internal cancellation controller, and the user's generator runs with new Request(event.request, { signal: cancellation.signal }), so code awaiting inside it can observe the disconnect and clean up.

Fixes #16778

@pkg-svelte-dev

pkg-svelte-dev Bot commented Aug 13, 2026

Copy link
Copy Markdown

Install the latest version of @sveltejs/kit from e3b23e4:

pnpm add https://pkg.svelte.dev/@sveltejs/kit/c/e3b23e4a5546951b85bbd306b8345f13518646da

Open in pkg.svelte.dev: https://pkg.svelte.dev/repos/kit/pr/16790

@changeset-bot

changeset-bot Bot commented Aug 13, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e3b23e4

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@sveltejs/kit Patch

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

@svelte-docs-bot

Copy link
Copy Markdown

@Nic-Polumeyv
Nic-Polumeyv marked this pull request as draft August 13, 2026 21:37
Rich-Harris pushed a commit that referenced this pull request Aug 13, 2026
…6793)

A request whose body has been fully read never aborts its signal on
client disconnect, because `readableAborted` requires the stream to die
before `end`. Give `getRequest` the response so disconnects are detected
on the response side instead — `writableEnded` rather than
`writableFinished` because HTTP/2 marks cancelled streams as finished.
Split out of #16790.
@Nic-Polumeyv
Nic-Polumeyv marked this pull request as ready for review August 14, 2026 15:43
try {
while (true) {
pending ??= generator.next();
const winner = await next_or_keep_alive(pending);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re-attaching a new .then to the same unsettled pending promise every keep-alive interval accumulates unresolved reactions, leaking memory on idle long-lived query.live SSE streams.

Fix on Vercel

@Nic-Polumeyv
Nic-Polumeyv marked this pull request as draft August 14, 2026 17:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

query.live: "Invalid state: Controller is already closed" when a stream is torn down mid-generator.next()

1 participant