Skip to content

Instantly share code, notes, and snippets.

@maietta
Last active December 2, 2024 14:56
Show Gist options
  • Save maietta/456f3716ce0f94b19d06dee627eeb510 to your computer and use it in GitHub Desktop.
Save maietta/456f3716ce0f94b19d06dee627eeb510 to your computer and use it in GitHub Desktop.
WebSocket for SvelteKit using Bun.
<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>
/*
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}`);
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;
}
};
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,
}
}
}
});
/* /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