Skip to content

feat: native support for Websockets #12973

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 232 commits into
base: main
Choose a base branch
from
Draft

Conversation

LukeHagar
Copy link
Contributor

@LukeHagar LukeHagar commented Nov 8, 2024

This PR is a replacement to #12961 with a completely different Websocket implementation using crossws that should be fully compatible with all major runtimes.

Functionality has been validated locally using basic tests in the options-2 test app.

Here is the new usage experience.
+server.js

import { error, accept } from '@sveltejs/kit';

export const socket = {
	upgrade(req) {
		 // Accept the websocket connection with a return
		return accept();

                // Reject the websocket connection with an error
                error(401, 'unauthorized');
	},

	open(peer) {
		//... handle socket open
	},

	message(peer, message) {
		//... handle socket message
	},

	close(peer, event) {
		//... handle socket close
	},

	error(peer, error) {
		//... handle socket error
	}
};

The newest implementation allows different sets of handlers to be implemented on a per-route basis. I have tested some basic uses of websockets locally to much success.

This PR is intended to:
Resolve #12358
Resolve #1491

Steps left

  • Ensure handle runs before upgrading requests
  • Gather feedback
  • Add or update tests
  • Fix the types
  • Update the adapters
  • Update the documentation
  • Add a changeset
  • Update language tools +server exports validation
  • Automatic typing for sockets

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:.

Edits

  • Please ensure that 'Allow edits from maintainers' is checked. PRs without this option may be closed.

…ionality for different handlers at different URLs, added example use cases to options-2 test app, added upgrade function for supporting additional adapters, and much more.
Copy link

changeset-bot bot commented Nov 8, 2024

🦋 Changeset detected

Latest commit: d8d803f

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

This PR includes changesets to release 4 packages
Name Type
@sveltejs/adapter-cloudflare Minor
@sveltejs/adapter-node Minor
@sveltejs/kit Minor
@sveltejs/adapter-auto 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

@LukeHagar LukeHagar mentioned this pull request Nov 8, 2024
6 tasks
@Rich-Harris
Copy link
Member

preview: https://svelte-dev-git-preview-kit-12973-svelte.vercel.app/

this is an automated message

@eltigerchino eltigerchino changed the title Native support for Websockets feat: native support for Websockets Nov 11, 2024
@eltigerchino eltigerchino added the feature / enhancement New feature or request label Nov 11, 2024
@eltigerchino eltigerchino marked this pull request as draft November 11, 2024 03:18
@pi0
Copy link

pi0 commented Jan 22, 2025

@LukeHagar LMK, if you need any help with this (you can reach me also on discord @pi0)

@LukeHagar LukeHagar marked this pull request as ready for review January 23, 2025 14:52
@Sillyvan
Copy link

Sillyvan commented May 5, 2025

Small input to the Durable Object thematic. No there shouldn't be any magic needed. a simple browser WebSocket can connect to it even with hibernation.

BUT, due to websockets not supporting reconnecting out of the box, partysocket is often recommended. maybe thats a good reference if we want to add that as well

@Rich-Harris
Copy link
Member

Hey everyone, little update here. A group of maintainers are having a team offsite ahead of Svelte Summit later this week, and we devoted much of today's session to talking about websockets. It was a great conversation and we had some realisations that would have been much harder to reach without the in-person aspect.

We concluded that this PR makes some great headway BUT from an authoring perspective there's a bit of an impedance mismatch between the crossws design and where we'd ultimately like to be, so we need to iterate on the design some more. Specifically:

  • peers is a global set of all peers, even though it looks like they belong to the current hook. But even that's not really what you want — you want your peers to be other clients that are connected to the same URL. In other words if I have a /chat/[id]/+server.ts file that exports a socket, and I connect to /chat/1, I don't want my set of peers to include /chat/2 and /friends or whatever other sockets I've created
  • the crossws approach to this problem is topics/channels. but that's not totally ideal — it's a global namespace which is prone to clobbering, and it means (AFAICT) you have to do clunky things like peer.context.key = 'chat-' + event.params.id
  • combine the previous two bullet points, and it becomes obvious that the url.pathname of the initial request is the topic/channel. so we shouldn't expose peer, we should expose an abstraction that manages that for us
  • by extension, it should be possible to e.g. publish('/chat/1', 'hello') from a POST handler etc
  • an app might have many streams of realtime information (different stocks we're tracking the prices of or whatever). conceptually these are distinct, but treating each of them as an individual websocket is suboptimal. ideally you only need to create the actual connection once, and each socket is a purely logical construct
  • on the client side, this means exposing something like chat = connect('/chat/' + id)
  • while crossws is useful for implementing this stuff, from an API standpoint we don't really want to be tied to it
  • in fact it's advantageous if we're not tied to websockets at all! websockets are kinda difficult to scale horizontally. it would be great if an adapter could specify (for example) a POST+SSE mechanism that achieved the same goal with the same API

We have some ideas we really like on what connect and an updated socket API could look like, but they're not fleshed out enough to share just yet. Please bear with us!

@LukeHagar
Copy link
Contributor Author

I appreciate the thought being put in here, and I can agree with all of the points on the interface and usage experience.

And I'm very happy the conversation is ongoing :D

Thanks for the update!

@tcurdt
Copy link

tcurdt commented May 6, 2025

Cool this is being worked on. Enjoy the summit.
And sorry for jumping in here with a question but...

combine the previous two bullet points, and it becomes obvious that the url.pathname of the initial request is the topic/channel.

I am just thinking about a notification counter that sits in the page header on all pages.
What would be the channel name be with that approach?

publish('/notifications', 'hello')
notifications = connect('/notifications')

with or without requiring /notifications/+server.ts to exist?

@philholden
Copy link

an app might have many streams of realtime information (different stocks we're tracking the prices of or whatever). conceptually these are distinct, but treating each of them as an individual websocket is suboptimal. ideally you only need to create the actual connection once, and each socket is a purely logical construct.

websockets are kinda difficult to scale horizontally.

This really depends on your backend and usecase. Durable Objects are near perfect for scaling many kinds of collaborative apps. E.g. game servers, or an educational apps designed to serve a classroom where all the pupils are in the same location. The DO servers will end up getting automatically located at the edge near the users. There is no real penalty in terms of pricing for having one Durable Object per game or classroom instance. game/123 would be one DO lobby/345 would be another. This makes WebSockets pretty horizontally scalable. True it is hard to have a server single server with more than 100,000 WebSocket requests per second. But if you have one server per room then I think you mostly don't need that.

One of the problems I see for crossws is that while it did allow for a Durable Object backed DO. I think it is hard to make this work for different classes of DO in the same app E.g. the game server and the lobby server (each requires their own DO). The web socket adapter would need to be set up in adapter-cloudflare so it is unclear how you would route thing so the correct class of DO is backing your new WebSocket from SvelteKit.

https://crossws.h3.dev/adapters/cloudflare#durable-objects

So for me there are two problems how to hear a wss// request from SvelteKit and forward to my DO and secondly how to bundle DOs with my SvelteKit app (and have them work in dev #1712). CrossWs seems like a too high level abstraction for what I am trying to achieve. Not against a highlevel abstraction that makes an app portable across different server platforms. But would be good to have some escape hatches for those building against a specific platform and wanting to use its features.

@pi0
Copy link

pi0 commented May 7, 2025

Thanks for the feedback @Rich-Harris (just found about updates from a chat with @benmccann)

Understandably, you might prefer to implement WebSockets standalone and design differently.

I would love to have a chat to discuss more with svelekit team. There are a lot of compatibility details part from end user API that could be reused.

Only to answer some of your concerns:

peers is a global set of all peers, even though it looks like they belong to the current hook.

Peers only get notified about the topics they subscribe, but this is true; they are not isolated (between different resolved hook-sets). I had been talking with other folks before, and we might change that.

by extension, it should be possible to e.g. publish('/chat/1', 'hello') from a POST handler, etc

The global publish method is available on the main instance. Peers can auto-join topics mapped from their routes to have an API like publish('/chat/1')

Great if an adapter could specify (for example) a POST+SSE mechanism that achieved the same goal with the same API

CrossWS also exposes an abstraction over SSE to give the same API: https://crossws.h3.dev/adapters/sse

--

@philholden Re CF DO, we recently added new resolver option that gives full flexibility on routing upgrade request, to Durable instance. h3js/crossws#130

@arxpoetica
Copy link
Member

Hell to the yes about a << POST+SSE mechanism >>

@philholden
Copy link

Thanks @pi0 the routing looks helpful.

@eltigerchino eltigerchino marked this pull request as draft June 4, 2025 01:37
@alanxp
Copy link

alanxp commented Jun 16, 2025

Sorry to reopen the thread again. I was very excited about WebSockets, and even if it’s not as perfect as intended, there should at least be a branch for testing and using crossws in the meantime, while the perfect solution is being worked on.
I was using the temporary commits, and they gave me hope — not just hope, they worked perfectly for me. No downsides whatsoever. They kept me within the Svelte ecosystem.
I really hope WebSockets get the attention they deserve and don’t end up being forgotten.

@Rich-Harris
Copy link
Member

We'll be sharing some thoughts very soon on how we plan to evolve data loading in SvelteKit, and real-time is part of that, but we're mainly thinking about unidirectional stuff (i.e. SSEs). Curious if people have use cases for WebSockets specifically, rather than real-time more broadly?

@ugackMiner53
Copy link

Personally, I use SvelteKit quite a bit with real-time games, where data needs to be sent between the client and server bidirectionally at a fairly constant rate. To my knowledge, using a technique like SSE would require every client to server interaction to create a separate request, adding a lot of overhead for updating variables that need to change quickly, like position. I'm currently using this PR to develop another game, and I've found it to be a very smooth experience when compared to external methods that I've used in the past, which add many complications in server setup and deployment.

(This comment isn't really about the PR, just answering the use-cases question above)

@alanxp
Copy link

alanxp commented Jun 16, 2025

Personally, I use SvelteKit quite a bit with real-time games, where data needs to be sent between the client and server bidirectionally at a fairly constant rate. To my knowledge, using a technique like SSE would require every client to server interaction to create a separate request, adding a lot of overhead for updating variables that need to change quickly, like position. I'm currently using this PR to develop another game, and I've found it to be a very smooth experience when compared to external methods that I've used in the past, which add many complications in server setup and deployment.

(This comment isn't really about the PR, just answering the use-cases question above)

I will use this PR as well. I haven’t had any issues with it so far — I almost finished a game engine using it. I thought it was heading in a good direction.

To answer the question: I use bidirectional communication 99.9% of the time, much more than SSE — not just for games, but also for other projects I’m starting to develop for the company I work for where we need websockets. This was something no other framework offered, and it was truly revolutionary.

@t1u1
Copy link

t1u1 commented Jun 16, 2025

We are using Sveltekit + websockets in two different projects. One is a semi-real time gaming system. The other project is an MVP that needs to handle continuous interaction with thousands of remote systems (which themselves are in production).

We are not using this PR build directly, but I used it as an inspiration to write a custom vite plugin which works mostly fine.

The former project (gaming system) was earlier using SSE (since I hadn't figured out sveltekit+websocket back then). But SSE was a pain to work with. The connection sometimes doesn't close cleanly on the server, eating server resources. And because it's unidirectional, you need to have a separate route for client initiated communication. I don't really see the point of going back to SSE, when a much more capable alternative exists.

@LukeHagar
Copy link
Contributor Author

I would definitely agree that a perfect solution here would allow for native support for SSE, WebSockets, and WebTransport, and the ability to use them independently.

I am greatly excited to hear more about the Svelte team's plans for the direction here, and excited for a sveltey WebSocket experience

@codeit-ninja
Copy link

I actually agree with @Rich-Harris — a lot of developers aren’t even aware that things like Server-Sent Events exist. Most just default to WebSockets and assume that’s the only option for real-time communication. Since SvelteKit is positioning itself as the go-to framework, it really makes sense for it to support all the common real-time patterns — not just WebSockets, but also SSE and others.

@Rich-Harris
Copy link
Member

Here's the RFC for Remote Functions we just published, which hopefully gives you an idea of the broader context.

Essentially, we want to separate data fetching from routing. load functions and actions have a number of awkward quirks and limitations that are solved with remote functions, which, in turn, are made possible by Asynchronous Svelte. Real-time data is no different — there's no real reason it should be tied to a specific route. (If this seems like it contradicts #12973 (comment), that's because the discussion that led to that comment immediately preceded the discussion that lead to the Remote Functions RFC. Both took place at a team offsite in Barcelona immediately before Svelte Summit.)

Not shown in the RFC is this idea for streaming real-time data from the server — query.stream. Quoting from an internal design document:

const sleep = (ms: number) => new Promise((f) => setTimeout(f, ms));

export const time = query.stream(async function* () {
  while (true) {
    yield new Date();
    await sleep(1000);
  }
});

In the client, calling time() would give you the same AsyncIterable, so you could do this...

for await (const t of time()) {
  console.log(`the time is ${t}`);
}

...but time() could also return a promise that resolves to the current value (this sounds weird from a types perspective but it's actually totally fine!):

<script lang="ts">
  import { time } from '$lib/data.remote.ts';
</script>

<p>the time is {await time()}</p>

When new data comes in, the query refreshes itself, causing the expression to be re-evaluated. Because of our global caching for queries read in effects, we don't recreate the query, we just return a promise that resolves to the new value.

Unlike other queries, streams are not batched. This means that when the effect containing await time() is destroyed, we can close the HTTP connection powering the query regardless of which other queries are active — in turn, this causes the stream to end on the server.

This would be powered by Server Sent Events, and would thus be available on every platform that supports streaming responses. It's a good approach because it works the same way everywhere and works very naturally with serverless platforms.

It also has limitations — there's per-request overhead, and it's one-way only. So my question in #12973 (comment) was trying to gauge how many use cases would be satisfied by this design, vs something that is specifically backed by WebSocket, and it sounds like the answer is 'some, but not all'. So the next step would be to figure out how to design something around WebSocket that fits with this broader design direction.

Hope this provides some clarity — I realise that many of you are chomping at the bit for this stuff to be built in!

@samal-rasmussen
Copy link

samal-rasmussen commented Jun 19, 2025

Awesome. This sound like something that is trending in the direction I enjoy.

This would be powered by Server Sent Events, and would thus be available on every platform that supports streaming responses.

This seems relevant:
https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API#browser_compatibility
https://caniuse.com/websockets

I work on apps that push data pretty fast, so keeping a websocket open makes sense. I really like doing bidirectional communication over persistent websockets, because opening new connections for every request seems like not the best thing, when you are receiving lots of data regularly anyways.

@LukeHagar
Copy link
Contributor Author

I believe the statement regarding server sent events and platform compatibility is referring to there being hosting platforms that do not support web sockets server side, Vercel being a good example.

@maietta
Copy link

maietta commented Jun 19, 2025

I believe the statement regarding server sent events and platform compatibility is referring to there being hosting platforms that do not support web sockets server side, Vercel being a good example.

I am not sure, but I think it boils down to long running processes are not typical of most hosting options that uses runtime like workerd. If you want them, you need to host it yourself using Bun, Node, Deno, etc. So far, I have not been able to get SSE or WebSockets to work at all with these platforms like Vercel or Cloudflare, but have running my apps via the Bun runtime in an OCI container like Docker with zero issues. Going serverless means restricting what runtime features are present. To leverage long-running process type hosting (not serverless), I typically use Svetle's node adapter even though I am using bun, then pass the --bun flag during runtime like this bun --bun /app/build/index.js.

@nerg4l
Copy link

nerg4l commented Jun 19, 2025

[...] I have not been able to get SSE or WebSockets to work at all with these platforms like Vercel or Cloudflare, [...]

The functionality is available in Cloudflare Workers (link). I personally had no issues when I tried it out, but I did not try it with a complex application.

@weepy
Copy link

weepy commented Jul 1, 2025

I'm building a multiplayer music studio. I use WS for signalling for WebRTC and keeping a general tab on users in rooms etc. I use Socket.io mainly because it provides a bunch of niceties such as room management etc that is actually quite fiddly to get right just using vanillla JS. |

I also use it to keep track in memory of the current "music game state" which is then periodically saved to Mongo based on updates from users and provided to users when they join.

I also use websockets to determine the current "time" - ie User pings GET_TIME and then adds half of the round trip.

Not sure if there's anything there that goes beyond typically "live multiplayer game", but that's above my pay grade ;-)

Currently this is all node as a separate node server which adds a bit of faff and deployment fiddling, though the separation of concerns is a positive.

@humanshield89
Copy link

humanshield89 commented Jul 2, 2025

Here's the RFC for Remote Functions we just published, which hopefully gives you an idea of the broader context.

Essentially, we want to separate data fetching from routing. load functions and actions have a number of awkward quirks and limitations that are solved with remote functions, which, in turn, are made possible by Asynchronous Svelte. Real-time data is no different — there's no real reason it should be tied to a specific route. (If this seems like it contradicts #12973 (comment), that's because the discussion that led to that comment immediately preceded the discussion that lead to the Remote Functions RFC. Both took place at a team offsite in Barcelona immediately before Svelte Summit.)

Not shown in the RFC is this idea for streaming real-time data from the server — query.stream. Quoting from an internal design document:

const sleep = (ms: number) => new Promise((f) => setTimeout(f, ms));

export const time = query.stream(async function* () {
  while (true) {
    yield new Date();
    await sleep(1000);
  }
});

In the client, calling time() would give you the same AsyncIterable, so you could do this...

for await (const t of time()) {
  console.log(`the time is ${t}`);
}

...but time() could also return a promise that resolves to the current value (this sounds weird from a types perspective but it's actually totally fine!):

<script lang="ts">
  import { time } from '$lib/data.remote.ts';
</script>

<p>the time is {await time()}</p>

When new data comes in, the query refreshes itself, causing the expression to be re-evaluated. Because of our global caching for queries read in effects, we don't recreate the query, we just return a promise that resolves to the new value.
Unlike other queries, streams are not batched. This means that when the effect containing await time() is destroyed, we can close the HTTP connection powering the query regardless of which other queries are active — in turn, this causes the stream to end on the server.

This would be powered by Server Sent Events, and would thus be available on every platform that supports streaming responses. It's a good approach because it works the same way everywhere and works very naturally with serverless platforms.

It also has limitations — there's per-request overhead, and it's one-way only. So my question in #12973 (comment) was trying to gauge how many use cases would be satisfied by this design, vs something that is specifically backed by WebSocket, and it sounds like the answer is 'some, but not all'. So the next step would be to figure out how to design something around WebSocket that fits with this broader design direction.

Hope this provides some clarity — I realise that many of you are chomping at the bit for this stuff to be built in!

As someone who has been using SSE with Sveltekit for a couple years now, this sounds great. I have yet to face a problem that required me to use WebSocket.

I have a question will every query.stream have it's own endpoint and connection or will sveltekit group all of them under the hood ? I am assuming it's the managed by sveltekit.

Since if each stream uses it's own endpoint and connection, having multiple real time streams is inefficient, especially on servers.

@eltigerchino
Copy link
Member

I have a question will every query.stream have it's own endpoint and connection or will sveltekit group all of them under the hood ? I am assuming it's the managed by sveltekit.

Since if each stream uses it's own endpoint and connection, having multiple real time streams is inefficient, especially on servers.

Yes, the plan is to re-use the same connection for multiple query.stream calls. In contrast, this current PR does not do that, so it is inefficient.

@philholden
Copy link

Websockets allow binary data SSE does not. Automerge uses binary data to sync efficent conflict free replication data structures (Google docs style multi user experiences). Svelte's reactive nature makes it a good fit for these kinds of realtime multi user apps.

@NickClark
Copy link

This is looking great!

Please make sure there's a way to catch client disconnects on the server. In ORTC we are given an AbortSignal that we can use to cancel/cleanup the server side resources if the SSE connection is dropped.

export const time = query.stream(async function* ({signal}) {
  signal.addEventListener("abort", () => { /* ...cleanup... */ })
  //...
});

This also very useful for all the other libraries that use the same pattern, you can just pass this along instead of making your own AbortController.

@atomtables
Copy link

atomtables commented Jul 10, 2025

This looks really nice!

Is there a method to get URL slugs from a peer connection? Right now the only way to get the slug is during the upgrade connection and by manually parsing the peer.request.url string (as far as I know).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
feature / enhancement New feature or request
Projects
None yet
Development

Successfully merging this pull request may close these issues.

implement "Upgrade" as a function in a api route to support websocket Native support for web sockets