Skip to content
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

feat: native support for Websockets #12973

Open
wants to merge 99 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
99 commits
Select commit Hold shift + click to select a range
d0b7f09
example crossws implementation with `hooks.server.js` websocketHooks …
LukeHagar Nov 7, 2024
b69b2e0
Migrated from hooks to server.js export named socket, validated funct…
LukeHagar Nov 8, 2024
aa69e1c
Formatting and fix a test
LukeHagar Nov 8, 2024
38c919c
removed global comment
LukeHagar Nov 8, 2024
2a022b5
regenerated types
LukeHagar Nov 8, 2024
c3a0bf7
removed some log statements
LukeHagar Nov 8, 2024
c86e4e9
cleaning up previous implementation
LukeHagar Nov 8, 2024
5858b49
Thoroughly tested handle implementation
LukeHagar Nov 9, 2024
9d56c50
Cleaning
LukeHagar Nov 9, 2024
46c8682
regenerated types and ran formatter
LukeHagar Nov 9, 2024
7791759
generate types
eltigerchino Nov 11, 2024
70202e3
adjusted implementation to only use responses and the updated crossws…
LukeHagar Jan 23, 2025
92e3e41
corrected example
LukeHagar Jan 23, 2025
db517f6
swapped from browser to onMount
LukeHagar Jan 23, 2025
a43c49b
cleaned log statements
LukeHagar Jan 23, 2025
6469816
added a docs page
LukeHagar Jan 24, 2025
ee0c6ee
updated node adapter
LukeHagar Jan 24, 2025
e737e02
fixed imports and resolve type
LukeHagar Jan 24, 2025
42e2f2d
updating adapters
LukeHagar Jan 24, 2025
0abeb17
Update documentation/docs/30-advanced/15-websockets.md
LukeHagar Jan 24, 2025
2a9971f
Update packages/kit/src/exports/index.js
LukeHagar Jan 24, 2025
cc58820
Update documentation/docs/30-advanced/15-websockets.md
LukeHagar Jan 24, 2025
29fdfec
ditching reject function for existing error
LukeHagar Jan 24, 2025
fda8a68
Update documentation/docs/30-advanced/15-websockets.md
LukeHagar Jan 24, 2025
ff58988
Update documentation/docs/30-advanced/15-websockets.md
LukeHagar Jan 24, 2025
1fed29d
Update documentation/docs/30-advanced/15-websockets.md
LukeHagar Jan 24, 2025
0ea72ab
Update documentation/docs/30-advanced/15-websockets.md
LukeHagar Jan 24, 2025
6408350
moved adapter integration, added response getter to HttpError
LukeHagar Jan 24, 2025
182a666
TABS
LukeHagar Jan 24, 2025
dda7298
corrected package.json versions
LukeHagar Jan 24, 2025
192840a
normalize on error and the response prop
LukeHagar Jan 24, 2025
ec0b797
Merge branch 'main' into crossws
LukeHagar Jan 24, 2025
f3bed08
recreated lockfile
LukeHagar Jan 24, 2025
2f11dca
ran formatter
LukeHagar Jan 24, 2025
3f2679e
fix lint errors
LukeHagar Jan 24, 2025
de37505
Update documentation/docs/25-build-and-deploy/99-writing-adapters.md
LukeHagar Jan 24, 2025
8dfad0c
fix lint errors
LukeHagar Jan 24, 2025
21cb866
added an s
LukeHagar Jan 24, 2025
f70aea1
correcting lockfile issues
LukeHagar Jan 24, 2025
c0a5e3d
regenerated types
LukeHagar Jan 24, 2025
a6bcf60
adjusting types
LukeHagar Jan 25, 2025
d3a48d7
cleaning a log
LukeHagar Jan 27, 2025
fa52645
adding ts comments
LukeHagar Feb 2, 2025
e63dfc3
adjusting ts comments
LukeHagar Feb 2, 2025
a0c54ab
adjusting lib for Generic error
LukeHagar Feb 3, 2025
c9457a4
adjusting ts version for Generic error
LukeHagar Feb 3, 2025
51d4729
updating lockfile
LukeHagar Feb 3, 2025
04b011d
updating generated types
LukeHagar Feb 3, 2025
2ab789f
downgrade typescript and address some types issues
benmccann Feb 3, 2025
4a6c8b3
upgrade @types/node to fix remaining types error
benmccann Feb 3, 2025
9bdfc04
adjusting tests for updated test app
LukeHagar Feb 3, 2025
894f38b
adjusting tests for updated test app
LukeHagar Feb 3, 2025
e7726a6
Update packages/kit/types/index.d.ts
LukeHagar Feb 3, 2025
8de07d1
Update packages/kit/src/exports/index.js
LukeHagar Feb 3, 2025
c18b4e8
Update packages/adapter-node/package.json
LukeHagar Feb 3, 2025
ddc9bc1
Update packages/adapter-cloudflare/package.json
LukeHagar Feb 3, 2025
80e14fe
Update packages/adapter-cloudflare-workers/package.json
LukeHagar Feb 3, 2025
3e58143
Update packages/adapter-node/src/index.js
LukeHagar Feb 3, 2025
7563cb3
Update packages/adapter-node/src/index.js
LukeHagar Feb 3, 2025
fd5b98b
Update packages/kit/src/exports/index.js
LukeHagar Feb 3, 2025
e79716c
updating lockfile
LukeHagar Feb 3, 2025
3f884b1
updating generated types
LukeHagar Feb 3, 2025
dea0952
re-export crossws Hooks type as Socket
eltigerchino Feb 3, 2025
25f039a
oopsie this should be +server.js instead of +page.server.js
eltigerchino Feb 3, 2025
4072369
Update 15-websockets.md
LukeHagar Feb 3, 2025
875536b
re-export Peer and Message types
eltigerchino Feb 3, 2025
84a1aa1
de-duplicate test id
eltigerchino Feb 3, 2025
93d40b2
revert server initialisation timing during dev
eltigerchino Feb 4, 2025
24f3b52
Update packages/kit/src/runtime/server/resolve.js
LukeHagar Feb 4, 2025
8c10aa4
Update packages/kit/src/runtime/server/resolve.js
eltigerchino Feb 4, 2025
52d607a
Update packages/kit/src/runtime/server/resolve.js
eltigerchino Feb 4, 2025
1ac7ccf
Update packages/kit/src/runtime/server/resolve.js
eltigerchino Feb 4, 2025
d9c4b1f
moving on call to after listen
LukeHagar Feb 4, 2025
6a3581b
restoring server options back to private
LukeHagar Feb 4, 2025
e762693
adjusting exports for node adapter
LukeHagar Feb 4, 2025
5c1f334
Update documentation/docs/30-advanced/15-websockets.md
eltigerchino Feb 5, 2025
8f98d61
revert typescript changes
eltigerchino Feb 5, 2025
ac788fb
Merge branch 'main' into pr/LukeHagar/12973
eltigerchino Feb 5, 2025
0a160fd
add Socket type to test app
eltigerchino Feb 5, 2025
d6f4ee5
spruce up docs
eltigerchino Feb 5, 2025
5e07cba
re-generate types
eltigerchino Feb 5, 2025
a0629ba
bump TS to 5.7
eltigerchino Feb 5, 2025
fef9dbe
add comment
eltigerchino Feb 5, 2025
940eb3d
follow mdn recommendation
eltigerchino Feb 5, 2025
e6caf6c
socket closes by itself if we navigate away
eltigerchino Feb 5, 2025
d437299
emphasise all websocket hooks are required
eltigerchino Feb 5, 2025
f203b8d
handle protocol upgrade to websocket
eltigerchino Feb 5, 2025
e6fed10
add ws to vite preview
eltigerchino Feb 5, 2025
f13af3b
i'm dumb
eltigerchino Feb 6, 2025
278c283
clarify
eltigerchino Feb 6, 2025
a458ff3
document node adapter upgradeHandler for custom servers
eltigerchino Feb 6, 2025
b766e06
formatting pass
LukeHagar Feb 6, 2025
3dc373c
tigthen up types
eltigerchino Feb 6, 2025
9597c7f
remove accept helper
eltigerchino Feb 6, 2025
9a839b5
de-duplicate respond logic
eltigerchino Feb 6, 2025
183117f
format
eltigerchino Feb 6, 2025
a9a3f9b
dynamically resolve hooks
eltigerchino Feb 6, 2025
e201b3b
comment
eltigerchino Feb 6, 2025
74937fd
remove cast and ensure headers are added to response
eltigerchino Feb 7, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/kit/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
"dependencies": {
"@types/cookie": "^0.6.0",
"cookie": "^0.6.0",
"crossws": "^0.3.2",
"devalue": "^5.1.0",
"esm-env": "^1.0.0",
"import-meta-resolve": "^4.1.0",
Expand Down
32 changes: 32 additions & 0 deletions packages/kit/src/exports/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,38 @@ export function redirect(status, location) {
);
}

/**
* Rejects a websocket upgrade request. When called during request handling, SvelteKit will throw a rejection response.
* Make sure you're not catching the thrown rejection, which would prevent SvelteKit from handling it.
* @param {400 | 401 | 402 | 403 | 404 | 405 | 406 | 407 | 408 | 409 | 410 | 411 | 412 | 413 | 414 | 415 | 416 | 417 | 418 | 421 | 422 | 423 | 424 | 425 | 426 | 428 | 429 | 431 | 451 | 500 | 501 | 502 | 503 | 504 | 505 | 506 | 507 | 508 | 510 | 511 | ({} & number)} status The [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status#redirection_messages). Must be in the range 400-599.
LukeHagar marked this conversation as resolved.
Show resolved Hide resolved
* @param {string} message The message to send to the client.
* @throws {Response} This error instructs SvelteKit to reject the websocket upgrade request.
* @throws {Error} If the provided status is invalid.
* @return {never}
*/
export function reject(status, message) {
if ((!BROWSER || DEV) && (isNaN(status) || status < 400 || status > 599)) {
throw new Error(`HTTP error status codes must be between 400 and 599 — ${status} is invalid`);
}

throw new Response(message, {
status,
headers: {
'Content-Type': 'text/plain'
}
});
}

/**
* Accepts a websocket upgrade request. When called during request handling, SvelteKit will accept the websocket upgrade request.
* @return {Response} This response instructs SvelteKit to accept the websocket upgrade request.
LukeHagar marked this conversation as resolved.
Show resolved Hide resolved
*/
export function accept() {
return new Response(null, {
status: 200,
LukeHagar marked this conversation as resolved.
Show resolved Hide resolved
});
}

/**
* Checks whether this is a redirect thrown by {@link redirect}.
* @param {unknown} e The object to check.
Expand Down
1 change: 1 addition & 0 deletions packages/kit/src/exports/public.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import {
} from '../types/private.js';
import { BuildData, SSRNodeLoader, SSRRoute, ValidatedConfig } from 'types';
import type { PluginOptions } from '@sveltejs/vite-plugin-svelte';
import { AdapterInstance, Hooks } from 'crossws';

export { PrerenderOption } from '../types/private.js';

Expand Down
53 changes: 32 additions & 21 deletions packages/kit/src/exports/vite/dev/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import fs from 'node:fs';
import path from 'node:path';
import process from 'node:process';
import { URL } from 'node:url';
import crossws from 'crossws/adapters/node';
import { AsyncLocalStorage } from 'node:async_hooks';
import colors from 'kleur';
import sirv from 'sirv';
Expand Down Expand Up @@ -421,7 +422,7 @@ export async function dev(vite, vite_config, svelte_config) {
const env = loadEnv(vite_config.mode, svelte_config.kit.env.dir, '');
const emulator = await svelte_config.kit.adapter?.emulate?.();

return () => {
return async () => {
const serve_static_middleware = vite.middlewares.stack.find(
(middleware) =>
/** @type {function} */ (middleware.handle).name === 'viteServeStaticMiddleware'
Expand All @@ -431,6 +432,36 @@ export async function dev(vite, vite_config, svelte_config) {
// serving routes with those names. See https://github.com/vitejs/vite/issues/7363
remove_static_middlewares(vite.middlewares);

// we have to import `Server` before calling `set_assets`
const { Server } = /** @type {import('types').ServerModule} */ (
await vite.ssrLoadModule(`${runtime_base}/server/index.js`, { fixStacktrace: true })
);

const { set_fix_stack_trace } = await vite.ssrLoadModule(`${runtime_base}/shared-server.js`);
set_fix_stack_trace(fix_stack_trace);

const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths');
set_assets(assets);

const server = new Server(manifest);

// we have to initialize the server before we can call the resolve function to populate the webhook resolver in the websocket handler
await server.init({
env,
read: (file) => createReadableStream(from_fs(file))
});

/** @type {import('crossws/adapters/node').NodeAdapter} */
const ws = crossws({
resolve: server.resolve()
});

vite.httpServer?.on('upgrade', (req, socket, head) => {
if (req.headers['sec-websocket-protocol'] !== 'vite-hmr') {
ws.handleUpgrade(req, socket, head);
}
});

vite.middlewares.use(async (req, res) => {
// Vite's base middleware strips out the base path. Restore it
const original_url = req.url;
Expand Down Expand Up @@ -474,26 +505,6 @@ export async function dev(vite, vite_config, svelte_config) {
return;
}

// we have to import `Server` before calling `set_assets`
const { Server } = /** @type {import('types').ServerModule} */ (
await vite.ssrLoadModule(`${runtime_base}/server/index.js`, { fixStacktrace: true })
);

const { set_fix_stack_trace } = await vite.ssrLoadModule(
`${runtime_base}/shared-server.js`
);
set_fix_stack_trace(fix_stack_trace);

const { set_assets } = await vite.ssrLoadModule('__sveltekit/paths');
set_assets(assets);

const server = new Server(manifest);

await server.init({
env,
read: (file) => createReadableStream(from_fs(file))
});

const request = await getRequest({
base,
request: req
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/runtime/server/endpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ export async function render_endpoint(event, mod, state) {
handler = mod.GET;
}

console.log('mod', mod);
console.log('method', method);
console.log('handler', handler);

if (!handler) {
return method_not_allowed(mod, method);
}
Expand Down
30 changes: 22 additions & 8 deletions packages/kit/src/runtime/server/index.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { respond } from './respond.js';
import { resolve } from './resolve.js';
import { set_private_env, set_public_env, set_safe_public_env } from '../shared-server.js';
import { options, get_hooks } from '__SERVER__/internal.js';
import { DEV } from 'esm-env';
Expand All @@ -17,15 +18,15 @@ const prerender_env_handler = {

export class Server {
/** @type {import('types').SSROptions} */
#options;
eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
options;

/** @type {import('@sveltejs/kit').SSRManifest} */
#manifest;

/** @param {import('@sveltejs/kit').SSRManifest} manifest */
constructor(manifest) {
/** @type {import('types').SSROptions} */
this.#options = options;
this.options = options;
this.#manifest = manifest;

set_manifest(manifest);
Expand All @@ -44,8 +45,8 @@ export class Server {

// set env, in case it's used in initialisation
const prefixes = {
public_prefix: this.#options.env_public_prefix,
private_prefix: this.#options.env_private_prefix
public_prefix: this.options.env_public_prefix,
private_prefix: this.options.env_private_prefix
};

const private_env = filter_private_env(env, prefixes);
Expand All @@ -63,19 +64,19 @@ export class Server {
set_read_implementation(read);
}

if (!this.#options.hooks) {
if (!this.options.hooks) {
try {
const module = await get_hooks();

this.#options.hooks = {
this.options.hooks = {
handle: module.handle || (({ event, resolve }) => resolve(event)),
handleError: module.handleError || (({ error }) => console.error(error)),
handleFetch: module.handleFetch || (({ request, fetch }) => fetch(request)),
reroute: module.reroute || (() => {})
};
} catch (error) {
if (DEV) {
this.#options.hooks = {
this.options.hooks = {
handle: () => {
throw error;
},
Expand All @@ -95,7 +96,20 @@ export class Server {
* @param {import('types').RequestOptions} options
*/
async respond(request, options) {
return respond(request, this.#options, this.#manifest, {
return respond(request, this.options, this.#manifest, {
...options,
error: false,
depth: 0
});
}

/**
* Returns a function that resolves the websocket hooks for a given request
* @param {import('types').RequestOptions} options
* @returns {(info: Request) => import('types').MaybePromise<Partial<import('crossws').Hooks>>}
*/
resolve(options) {
return resolve(this.options, this.#manifest, {
...options,
error: false,
depth: 0
Expand Down
156 changes: 156 additions & 0 deletions packages/kit/src/runtime/server/resolve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import { DEV } from 'esm-env';
import { validate_server_exports } from '../../utils/exports.js';
import { exec } from '../../utils/routing.js';
import { decode_pathname, decode_params } from '../../utils/url.js';
import { base } from '__sveltekit/paths';

/**
* @param {import('types').SSROptions} options
* @param {import('@sveltejs/kit').SSRManifest} manifest
* @param {import('types').SSRState} state
* @returns {(info: RequestInit | import('crossws').Peer) => import('types').MaybePromise<Partial<import('crossws').Hooks>>}
*/
export function resolve(options, manifest, state) {
return async (info) => {
/** @type {RequestInit} */
let request;

// These types all need to be straightened out
if (info.request) {
request = info.request;
} else {
request = info;
}

/** URL but stripped from the potential `/__data.json` suffix and its search param */
eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
const url = new URL(request.url);

// reroute could alter the given URL, so we pass a copy
let rerouted_path;
try {
rerouted_path = options.hooks.reroute({ url }) ?? url.pathname;
} catch {
return {};
}

let decoded;
try {
decoded = decode_pathname(rerouted_path);
} catch (e) {
console.error(e);
return {};
}

if (base && decoded.startsWith(base)) {
decoded = decoded.slice(base.length) || '/';
}
LukeHagar marked this conversation as resolved.
Show resolved Hide resolved

/** @type {import('types').SSRRoute | null} */
let route = null;

/** @type {Record<string, string>} */
let params = {};

try {
// TODO this could theoretically break - should probably be inside a try-catch
const matchers = await manifest._.matchers();

for (const candidate of manifest._.routes) {
const match = candidate.pattern.exec(decoded);

if (!match) continue;

const matched = exec(match, candidate.params, matchers);
if (matched) {
route = candidate;
params = decode_params(matched);
break;
}
}
} catch (e) {
console.error(e);
return {};
}

/** @type {Record<string, string>} */
const headers = {};

try {
// determine whether we need to redirect to add/remove a trailing slash
eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
if (route && route.endpoint) {
// if `paths.base === '/a/b/c`, then the root route is `/a/b/c/`,
// regardless of the `trailingSlash` route option

eltigerchino marked this conversation as resolved.
Show resolved Hide resolved
const node = await route.endpoint();

if (DEV) {
validate_server_exports(node, /** @type {string} */ (route.endpoint_id));
}

return {
...node.socket,
upgrade: async (req) => {
/** @type {import('@sveltejs/kit').RequestEvent} */
const event = {
// @ts-expect-error `cookies` and `fetch` need to be created after the `event` itself
cookies: null,
// @ts-expect-error
fetch: null,
getClientAddress:
state.getClientAddress ||
(() => {
throw new Error(
`${__SVELTEKIT_ADAPTER_NAME__} does not specify getClientAddress. Please raise an issue`
);
}),
locals: {},
params,
platform: state.platform,
request: req,
route: { id: route?.id ?? null },
setHeaders: (new_headers) => {
for (const key in new_headers) {
const lower = key.toLowerCase();
const value = new_headers[key];

if (lower === 'set-cookie') {
throw new Error(
'Use `event.cookies.set(name, value, options)` instead of `event.setHeaders` to set cookies'
);
} else if (lower in headers) {
throw new Error(`"${key}" header is already set`);
} else {
headers[lower] = value;

if (state.prerendering && lower === 'cache-control') {
state.prerendering.cache = /** @type {string} */ (value);
}
}
}
},
url,
isDataRequest: false,
isSubRequest: state.depth > 0
};

const response = await options.hooks.handle({
event,
resolve: async (event) => {
if (node.socket && node.socket.upgrade) {
return await node.socket.upgrade(event.request);
} else {
return new Response('Not Implemented', { status: 501 });
}
}
});

return response ?? new Response('Not Implemented', { status: 501 });
}
};
}
} catch (e) {
console.error(e);
return {};
}
};
}
Loading
Loading