Last active
December 2, 2024 14:56
-
-
Save maietta/456f3716ce0f94b19d06dee627eeb510 to your computer and use it in GitHub Desktop.
WebSocket for SvelteKit using Bun.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<script lang="ts"> | |
import { browser } from '$app/environment'; | |
let ws: WebSocket | null = null; | |
let connecting = true; | |
let connected = false; | |
let messages: string[] = []; | |
let sendValue: string = ''; | |
let reconnectInterval: any; | |
let pingInterval: any; | |
function connectWebSocket() { | |
console.log('Attempting WebSocket connection...'); | |
ws = new WebSocket(`ws://${location.host}/ws`); // NOTE: Need to switch to wss:// if using HTTPS in production. (Recommended) | |
ws.addEventListener('open', () => { | |
console.log('WebSocket connected'); | |
connected = true; | |
connecting = false; | |
clearInterval(reconnectInterval); | |
startPinging(); | |
}); | |
ws.addEventListener('close', () => { | |
console.log('WebSocket disconnected'); | |
connected = false; | |
connecting = false; | |
stopPinging(); | |
if (!reconnectInterval) { | |
reconnectInterval = setInterval(connectWebSocket, 5000); // Retry every 5 seconds | |
} | |
}); | |
ws.addEventListener('message', (ev) => { | |
const msg = ev.data; | |
messages = [...messages, msg].slice(-50); // Keep only the last 50 messages | |
}); | |
ws.addEventListener('error', (ev) => { | |
console.error('WebSocket error:', ev); | |
}); | |
} | |
function startPinging() { | |
if (ws && connected) { | |
pingInterval = setInterval(() => { | |
ws?.send('ping'); | |
console.log('Sent ping'); | |
}, 30000); // Ping every 30 seconds | |
} | |
} | |
function stopPinging() { | |
if (pingInterval) { | |
clearInterval(pingInterval); | |
pingInterval = null; | |
} | |
} | |
function sendMsg() { | |
if (!ws || ws.readyState !== WebSocket.OPEN) { | |
console.warn('WebSocket is not open. Cannot send message.'); | |
return; | |
} | |
ws.send(sendValue); | |
sendValue = ''; | |
} | |
if (browser) { | |
connectWebSocket(); | |
} | |
</script> | |
<h1>Demo page</h1> | |
<div> | |
<h3>Connection status</h3> | |
<div>Connecting: <span data-testid="connecting">{connecting.toString()}</span></div> | |
<div>Connected: <span data-testid="connected">{connected.toString()}</span></div> | |
</div> | |
<div> | |
<h3>Messages</h3> | |
<ul data-testid="messages"> | |
{#each messages as message, index} | |
<li>{message}</li> | |
{/each} | |
</ul> | |
</div> | |
<div> | |
<h3>Send</h3> | |
<form on:submit|preventDefault={sendMsg}> | |
<input data-testid="send" placeholder="payload" bind:value={sendValue} /> | |
<button data-testid="submit" type="submit" disabled={!connected || !sendValue}>Send</button> | |
</form> | |
</div> |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* | |
For local development with Vite, requests get proxied to this server. | |
You must run this independently from the Vite dev server. | |
Run this server with `bun --bun run bunServer.ts`, in a separate terminal. The --bun flag is required to use the bun runtime. | |
*/ | |
import {handleWebsocket} from "./src/hooks.server" | |
const server = Bun.serve({ | |
port: 9998, | |
fetch(req, server) { | |
// upgrade the request to a WebSocket | |
const ok = handleWebsocket.upgrade(req, server.upgrade.bind(server)) | |
if (ok) | |
return; | |
return new Response('Upgrade failed :(', { status: 500 }); | |
}, | |
websocket: handleWebsocket as any | |
}); | |
console.log(`Helper Bun server listening on ${server.hostname + ":" + server.port}`); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import type { WebSocketHandler } from 'svelte-adapter-bun'; | |
import type { Handle } from '@sveltejs/kit'; | |
export const handle: Handle = async ({ event, resolve }) => { | |
const response = await resolve(event); | |
return response; | |
}; | |
export const handleWebsocket: WebSocketHandler = { | |
open(ws) { | |
console.log('Client connected'); | |
ws.send('[init]: Hello from server!'); | |
// Start server-side ping to keep the connection alive | |
const interval = setInterval(() => { | |
if (ws.readyState === WebSocket.OPEN) { | |
ws.send('ping'); | |
} | |
}, 30000); // Ping every 30 seconds | |
}, | |
message(ws, message) { | |
console.log('Client sent message:', message); | |
ws.send(`[pong]: ${message}`); | |
}, | |
close(code, reason) { | |
console.log(`Client disconnected. Code: ${code}, Reason: ${reason}`); | |
}, | |
upgrade(request, upgrade) { | |
const url = new URL(request.url); | |
console.log('Client upgrade request for:', url.pathname); | |
if (url.pathname.startsWith('/ws')) { | |
return upgrade(request); | |
} | |
return false; | |
} | |
}; |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { sveltekit } from '@sveltejs/kit/vite'; | |
import { defineConfig } from 'vite'; | |
export default defineConfig({ | |
plugins: [sveltekit()], | |
server: { | |
// add ws proxy to from vite to bun | |
proxy: { | |
'/ws': { | |
target: 'http://localhost:9998', | |
ws: true, | |
} | |
} | |
} | |
}); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/* /src/lib/server/webSocketUtils.ts */ | |
import { parse } from 'url'; | |
import { WebSocketServer } from 'ws'; | |
import { nanoid } from 'nanoid'; | |
import type { WebSocket as WebSocketBase } from 'ws'; | |
import type { IncomingMessage } from 'http'; | |
import type { Duplex } from 'stream'; | |
export const GlobalThisWSS = Symbol.for('sveltekit.wss'); | |
export interface ExtendedWebSocket extends WebSocketBase { | |
socketId: string; | |
// userId: string; | |
}; | |
// You can define server-wide functions or class instances here | |
// export interface ExtendedServer extends Server<ExtendedWebSocket> {}; | |
export type ExtendedWebSocketServer = WebSocketServer & { | |
clients: Set<ExtendedWebSocket>; | |
} | |
export type ExtendedGlobal = typeof globalThis & { | |
[GlobalThisWSS]: ExtendedWebSocketServer; | |
}; | |
export const onHttpServerUpgrade = (req: IncomingMessage, sock: Duplex, head: Buffer) => { | |
const pathname = req.url ? parse(req.url).pathname : null; | |
if (pathname !== '/websocket') return; | |
const wss = (globalThis as ExtendedGlobal)[GlobalThisWSS]; | |
wss.handleUpgrade(req, sock, head, (ws: any) => { | |
console.log('[handleUpgrade] creating new connecttion'); | |
wss.emit('connection', ws, req); | |
}); | |
}; | |
export const createWSSGlobalInstance = () => { | |
const wss = new WebSocketServer({ noServer: true }) as ExtendedWebSocketServer; | |
(globalThis as ExtendedGlobal)[GlobalThisWSS] = wss; | |
wss.on('connection', (ws: { socketId: string; on: (arg0: string, arg1: () => void) => void; }) => { | |
ws.socketId = nanoid(); | |
console.log(`[wss:global] client connected (${ws.socketId})`); | |
ws.on('close', () => { | |
console.log(`[wss:global] client disconnected (${ws.socketId})`); | |
}); | |
}); | |
return wss; | |
}; |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment