Skip to content

Conversation

dummdidumm
Copy link
Member

  • streams are deduplicated on the client, i.e. same resource+payload == same stream instance
  • a stream is kept open as long as it's either in a reactive context or someone iterates over it. Streams can close when noone listens anymore and open back up when someone does again
  • cannot iterate on the server right now, only retrieve the first value via promise
  • cannot refresh/override (doesn't make sense)

WIP, builds on top of the query-batch PR to avoid merge conflicts like crazy since they touch the same files; the batch PR should be merged first


Please don't delete this checklist! Before submitting the PR, please make sure you do the following:

  • It's really useful if your PR references an issue where it is discussed ahead of time. In many cases, features are absent for a reason. For large changes, please create an RFC: https://github.com/sveltejs/rfcs
  • This message body should clearly illustrate what problems it solves.
  • Ideally, include a test that fails without this PR but passes with it.

Tests

  • Run the tests with pnpm test and lint the project with pnpm lint and pnpm check

Changesets

  • If your PR makes a change that should be noted in one or more packages' changelogs, generate a changeset by running pnpm changeset and following the prompts. Changesets that add features should be minor and those that fix bugs should be patch. Please prefix changeset messages with feat:, fix:, or chore:.

Implements `query.batch` to address the n+1 problem
- streams are deduplicated on the client, i.e. same resource+payload == same stream instance
- a stream is kept open as long as it's either in a reactive context or someone iterates over it. Streams can close when noone listens anymore and open back up when someone does again
- cannot iterate on the server right now, only retrieve the first value via promise
- cannot refresh/override (doesn't make sense)
Copy link

changeset-bot bot commented Aug 21, 2025

🦋 Changeset detected

Latest commit: cead45e

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 Minor

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

@Rich-Harris
Copy link
Member

How confident are we about the deduplication part? For cases where you're streaming a live view of some database query, that makes sense, but are there cases more like this?

for await (const n of countToTen()) {
  console.log(n);
}

It would be weird if I did that and it started at 7 because of something happening in an unrelated component. Genuinely not sure what the right way to think about this is.

@dummdidumm
Copy link
Member Author

dummdidumm commented Aug 22, 2025

yeah I had the same thought already. Hard to tell, both ways make sense - which maybe means we need to give people some way to tell what it is?

Update: maybe it should be "each invocation is a separate stream". Then you can still deduplicate yourself if you want to.

@benmccann
Copy link
Member

I see 'content-type': 'text/event-stream' in the code, so I guess this is an implementation of Server Sent Events (SSE). However, it's surprising to me that "Server Sent Events" doesn't appear in the docs or PR description or anything. It'd probably be helpful to include that

I wonder about the name. I'd expect query.stream to be used for sending back a large file like a video. Perhaps this should be called query.sse to better indicate what it is and also to leave the query.stream name available for streaming large responses in the future.

@Rich-Harris
Copy link
Member

SSEs are just an implementation detail. We could theoretically swap it out for some other mechanism in future (a non-SSE streaming HTTP response, or WebSockets, or IPC, or whatever else depending on platform) and so it wouldn't make sense to bake it into the name of the API.

For large files I would expect to just be able to return a ReadableStream from a query. You can't do that yet because we haven't taught SvelteKit how to stream responses from remote functions (and, subsequently, how to serialize a ReadableStream).

So the name doesn't have to be query.stream, but it shouldn't be query.sse. query.live? query.realtime? query.iterable (since it returns an AsyncIterable, albeit one that is also a Promise)?

Whatever name we pick would have to make sense for both of these cases:

<script>
  import { getLikes } from './data.remote';
</script>

<!-- updates in realtime as the server pushes more data -->
<p>likes: {await getLikes()}</p>
<script>
  import { ask } from './ai.remote';

  let { question } = $props();

  let answer = $state('');

  $effect(async () => {
    for await (const chunk of ask(question)) {
      answer += chunk;
    }
  });
</script>

<div>{answer}</div>

(very incomplete/buggy example but you get the idea — in one case each chunk is standalone and is thus suitable to be treated like any other query, in the other it's a series of events that need to be handled one at a time)

@benmccann
Copy link
Member

benmccann commented Aug 29, 2025

Between SSE and web sockets there are differences such as SSE having auto-reconnect. How do you know how to handle a flaky connection without being aware of the underlying implementation?

@Rich-Harris
Copy link
Member

The framework takes care of that on your behalf

@benmccann
Copy link
Member

I guess retrying with exponential back off would probably work well enough for most websocket applications.

If we do want to have websockets as part of query.stream in the future, we might want an option to disable websockets and only use SSE. Websockets can be more complicated to deploy as they require additional configuration in your load balancer or proxy and can also cost more as they're only available in higher level Cloudflare plans.

@Rich-Harris
Copy link
Member

Rich-Harris commented Aug 29, 2025

It would most likely be configured through your adapter

const stream = oneToTen();
</script>

<!-- these are one single ReadableStream request since they share the same stream instance -->
Copy link
Member

Choose a reason for hiding this comment

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

ReadableStream?

Copy link
Member Author

Choose a reason for hiding this comment

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

https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream - the comment is meant as "these are not two network requests, it's one because they share the same stream instance"

I guess I could've written that instead ... 😅

Comment on lines +340 to +343
// TODO how would we subscribe to the stream on the server while deduplicating calls and knowing when to stop?
throw new Error(
'Cannot iterate over a stream on the server. This restriction may be lifted in a future version.'
);
Copy link
Member

Choose a reason for hiding this comment

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

Given that we no longer deduplicate, is this still a concern? If you for-awaited an infinite async iterable during render then you'd have the same problem whether it's the client or the server, and the solution is 'don't do that'. So I'm not sure if we need this restriction

Copy link
Member Author

Choose a reason for hiding this comment

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

true, let me take a look

@benmccann
Copy link
Member

Even if we consider SSE to be an implementation detail, I still think it could be helpful to mention in the docs or faq so that it's easier to find when people have the question, "how do I do SSE with SvelteKit?"

@Rich-Harris
Copy link
Member

It is mentioned, in the inline query.stream docs

@benmccann
Copy link
Member

It's probably worth putting " (SSE)" after it as I was searching for the abbreviation and didn't find it

Base automatically changed from query-batch to main September 10, 2025 11:16
@svelte-docs-bot
Copy link

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.

3 participants