svelte-adapter-uws 0.2.8 → 0.2.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -30,6 +30,7 @@ I've been loving Svelte and SvelteKit for a long time. I always wanted to expand
30
30
  - [Authentication](#authentication)
31
31
  - [Platform API (`event.platform`)](#platform-api-eventplatform)
32
32
  - [Client store API](#client-store-api)
33
+ - [Seeding initial state](#seeding-initial-state)
33
34
  - [TypeScript setup](#typescript-setup)
34
35
  - [Svelte 4 support](#svelte-4-support)
35
36
  - [Deploying with Docker](#deploying-with-docker)
@@ -389,8 +390,8 @@ If you set `envPrefix: 'MY_APP_'` in the adapter config, all variables are prefi
389
390
 
390
391
  On `SIGTERM` or `SIGINT`, the server:
391
392
  1. Stops accepting new connections
392
- 2. Emits a `sveltekit:shutdown` event on `process` (for cleanup hooks like closing database connections)
393
- 3. Waits for in-flight SSR requests to complete (up to `SHUTDOWN_TIMEOUT` seconds)
393
+ 2. Waits for in-flight SSR requests to complete (up to `SHUTDOWN_TIMEOUT` seconds)
394
+ 3. Emits a `sveltekit:shutdown` event on `process` (for cleanup hooks like closing database connections)
394
395
  4. Exits
395
396
 
396
397
  ```js
@@ -455,7 +456,7 @@ export async function upgrade({ headers, cookies, url, remoteAddress }) {
455
456
  }
456
457
 
457
458
  // Called when a connection is established
458
- export function open(ws) {
459
+ export function open(ws, { platform }) {
459
460
  const { userId } = ws.getUserData();
460
461
  console.log(`User ${userId} connected`);
461
462
 
@@ -466,27 +467,27 @@ export function open(ws) {
466
467
  // Called when a message is received
467
468
  // Note: subscribe/unsubscribe messages from the client store are
468
469
  // handled automatically BEFORE this function is called
469
- export function message(ws, data, isBinary) {
470
+ export function message(ws, { data, isBinary }) {
470
471
  const msg = JSON.parse(Buffer.from(data).toString());
471
472
  console.log('Got message:', msg);
472
473
  }
473
474
 
474
475
  // Called when a client tries to subscribe to a topic (optional)
475
476
  // Return false to deny the subscription
476
- export function subscribe(ws, topic) {
477
+ export function subscribe(ws, topic, { platform }) {
477
478
  const { role } = ws.getUserData();
478
479
  // Only admins can subscribe to admin topics
479
480
  if (topic.startsWith('admin') && role !== 'admin') return false;
480
481
  }
481
482
 
482
483
  // Called when the connection closes
483
- export function close(ws, code, message) {
484
+ export function close(ws, { code, message, platform }) {
484
485
  const { userId } = ws.getUserData();
485
486
  console.log(`User ${userId} disconnected`);
486
487
  }
487
488
 
488
489
  // Called when backpressure has drained (optional, for flow control)
489
- export function drain(ws) {
490
+ export function drain(ws, { platform }) {
490
491
  // You can resume sending large messages here
491
492
  }
492
493
  ```
@@ -583,7 +584,7 @@ export async function upgrade({ cookies }) {
583
584
  return { userId: user.id, name: user.name, role: user.role };
584
585
  }
585
586
 
586
- export function open(ws) {
587
+ export function open(ws, { platform }) {
587
588
  const { userId, role } = ws.getUserData();
588
589
  console.log(`${userId} connected (${role})`);
589
590
 
@@ -592,7 +593,7 @@ export function open(ws) {
592
593
  if (role === 'admin') ws.subscribe('admin');
593
594
  }
594
595
 
595
- export function close(ws) {
596
+ export function close(ws, { platform }) {
596
597
  const { userId } = ws.getUserData();
597
598
  console.log(`${userId} disconnected`);
598
599
  }
@@ -653,7 +654,7 @@ The WebSocket upgrade is an HTTP request. The browser treats it like any other r
653
654
 
654
655
  ## Platform API (`event.platform`)
655
656
 
656
- Available in server hooks, load functions, form actions, and API routes.
657
+ Available in server hooks, load functions, form actions, API routes, and WebSocket hooks (`hooks.ws`).
657
658
 
658
659
  ### `platform.publish(topic, event, data)`
659
660
 
@@ -686,12 +687,12 @@ This is useful when you store WebSocket references (e.g. in a `Map`) and need to
686
687
  // src/hooks.ws.js - store connections by user ID
687
688
  const userSockets = new Map();
688
689
 
689
- export function open(ws) {
690
+ export function open(ws, { platform }) {
690
691
  const { userId } = ws.getUserData();
691
692
  userSockets.set(userId, ws);
692
693
  }
693
694
 
694
- export function close(ws) {
695
+ export function close(ws, { platform }) {
695
696
  const { userId } = ws.getUserData();
696
697
  userSockets.delete(userId);
697
698
  }
@@ -714,13 +715,15 @@ export async function POST({ request, platform }) {
714
715
  }
715
716
  ```
716
717
 
717
- To reply directly from inside `hooks.ws.js` (where `platform` isn't available), use `ws.send()` with the envelope format:
718
+ You can also reply directly from inside `hooks.ws.js` using `platform.send()` or `ws.send()` with the envelope format:
718
719
 
719
720
  ```js
720
721
  // src/hooks.ws.js
721
- export function message(ws, rawData) {
722
- const msg = JSON.parse(Buffer.from(rawData).toString());
723
- // Reply to sender using the same envelope format the client store expects
722
+ export function message(ws, { data, platform }) {
723
+ const msg = JSON.parse(Buffer.from(data).toString());
724
+ // Using platform.send (recommended):
725
+ platform.send(ws, 'echo', 'reply', { got: msg });
726
+ // Or using ws.send with manual envelope:
724
727
  ws.send(JSON.stringify({ topic: 'echo', event: 'reply', data: { got: msg } }));
725
728
  }
726
729
  ```
@@ -980,13 +983,22 @@ Handles `set`, `increment`, and `decrement` events:
980
983
  <p>{$online} users online</p>
981
984
  ```
982
985
 
983
- Server:
986
+ Server (from any hook or handler that has `platform`):
984
987
  ```js
985
- platform.topic('online-users').increment();
986
- platform.topic('online-users').decrement();
988
+ // In hooks.ws.js - track connected users:
989
+ export function open(ws, { platform }) {
990
+ platform.topic('online-users').increment();
991
+ }
992
+ export function close(ws, { platform }) {
993
+ platform.topic('online-users').decrement();
994
+ }
995
+
996
+ // Or from a SvelteKit handler:
987
997
  platform.topic('online-users').set(42);
988
998
  ```
989
999
 
1000
+ > **Heads up:** The increment/decrement pattern above has a subtle race condition - a newly connected client won't see the current count because its `subscribe` message hasn't been processed yet when `open` fires. See [Seeding initial state](#seeding-initial-state) for the fix.
1001
+
990
1002
  ### `once(topic, event?, options?)` - wait for one event
991
1003
 
992
1004
  Returns a promise that resolves with the first matching event and then unsubscribes:
@@ -1077,6 +1089,78 @@ ws.close();
1077
1089
 
1078
1090
  ---
1079
1091
 
1092
+ ## Seeding initial state
1093
+
1094
+ When a client connects, there's a window between the WebSocket opening and the client's topic subscriptions being processed. Any `platform.publish()` calls that happen during `open` will be missed by the connecting client, because it hasn't subscribed to those topics yet.
1095
+
1096
+ This matters most with `count()`. If your `open` hook does `platform.topic('online').set(total)`, the connecting client won't see it - the `set` event is broadcast before the client's `subscribe` message arrives.
1097
+
1098
+ The fix is to use the `subscribe` hook instead of (or alongside) `open` to send the current value directly to the subscribing client:
1099
+
1100
+ ```js
1101
+ // src/hooks.ws.js
1102
+ let online = 0;
1103
+
1104
+ export function open(ws, { platform }) {
1105
+ online++;
1106
+ platform.topic('online').set(online); // broadcasts to already-subscribed clients
1107
+ }
1108
+
1109
+ export function subscribe(ws, topic, { platform }) {
1110
+ // When a client subscribes to 'online', send it the current count
1111
+ if (topic === 'online') {
1112
+ platform.send(ws, 'online', 'set', online);
1113
+ }
1114
+ }
1115
+
1116
+ export function close(ws, { platform }) {
1117
+ online--;
1118
+ platform.topic('online').set(online);
1119
+ }
1120
+ ```
1121
+
1122
+ ```svelte
1123
+ <!-- src/routes/+page.svelte -->
1124
+ <script>
1125
+ import { count } from 'svelte-adapter-uws/client';
1126
+
1127
+ const online = count('online');
1128
+ </script>
1129
+
1130
+ <p>{$online} online</p>
1131
+ ```
1132
+
1133
+ The `subscribe` hook fires at the right moment - after the client is actually subscribed to the topic. `platform.send()` sends only to that one client, so it gets the current value without waiting for the next broadcast.
1134
+
1135
+ This same pattern works for any topic where new subscribers need to see the current state. For a CRUD list, you could send the full dataset in `subscribe`:
1136
+
1137
+ ```js
1138
+ // src/hooks.ws.js
1139
+ export async function subscribe(ws, topic, { platform }) {
1140
+ if (topic === 'todos') {
1141
+ const todos = await db.getTodos();
1142
+ for (const todo of todos) {
1143
+ platform.send(ws, 'todos', 'created', todo);
1144
+ }
1145
+ }
1146
+ }
1147
+ ```
1148
+
1149
+ ```svelte
1150
+ <script>
1151
+ import { crud } from 'svelte-adapter-uws/client';
1152
+
1153
+ // No need for load() data - the subscribe hook seeds the list
1154
+ const todos = crud('todos');
1155
+ </script>
1156
+
1157
+ {#each $todos as todo (todo.id)}
1158
+ <p>{todo.text}</p>
1159
+ {/each}
1160
+ ```
1161
+
1162
+ ---
1163
+
1080
1164
  ## TypeScript setup
1081
1165
 
1082
1166
  Add the platform type to your `src/app.d.ts`:
@@ -1229,10 +1313,10 @@ The static file gap is the largest because `adapter-node` uses sirv which calls
1229
1313
 
1230
1314
  | Server | Messages delivered/s | vs adapter-uws |
1231
1315
  |---|---|---|
1232
- | **uWS native** (barebones) | 3,625,000 | 1.0x |
1233
- | **adapter-uws** (full handler) | 3,642,000 | baseline |
1234
- | **socket.io** | 177,200 | **20.5x slower** |
1235
- | **ws** library | 164,500 | **22.1x slower** |
1316
+ | **uWS native** (barebones) | 3,642,000 | baseline |
1317
+ | **adapter-uws** (full handler) | 3,625,000 | 1.0x |
1318
+ | **ws** library | 177,200 | **20.5x slower** |
1319
+ | **socket.io** | 164,500 | **22.1x slower** |
1236
1320
 
1237
1321
  uWS native pub/sub delivered 3.6M messages/s with perfect 50x fan-out. After optimization, the adapter matches it -- the byte-prefix check and string template envelope add near-zero overhead to the hot path. `socket.io` and `ws` both collapsed under the same load, delivering less than 1x fan-out (massive message loss/queueing).
1238
1322
 
package/files/handler.js CHANGED
@@ -836,7 +836,7 @@ if (WS_ENABLED) {
836
836
 
837
837
  open: (ws) => {
838
838
  wsConnections.add(ws);
839
- wsModule.open?.(ws);
839
+ wsModule.open?.(ws, { platform });
840
840
  },
841
841
 
842
842
  message: (ws, message, isBinary) => {
@@ -851,7 +851,7 @@ if (WS_ENABLED) {
851
851
  const msg = JSON.parse(Buffer.from(message).toString());
852
852
  if (msg.type === 'subscribe' && typeof msg.topic === 'string') {
853
853
  // If a subscribe hook exists, let it gate access
854
- if (wsModule.subscribe && wsModule.subscribe(ws, msg.topic) === false) {
854
+ if (wsModule.subscribe && wsModule.subscribe(ws, msg.topic, { platform }) === false) {
855
855
  return;
856
856
  }
857
857
  ws.subscribe(msg.topic);
@@ -866,14 +866,14 @@ if (WS_ENABLED) {
866
866
  }
867
867
  }
868
868
  // Delegate everything else to the user's handler (if provided)
869
- wsModule.message?.(ws, message, isBinary);
869
+ wsModule.message?.(ws, { data: message, isBinary, platform });
870
870
  },
871
871
 
872
- drain: wsModule.drain || undefined,
872
+ drain: wsModule.drain ? (ws) => wsModule.drain(ws, { platform }) : undefined,
873
873
 
874
874
  close: (ws, code, message) => {
875
875
  wsConnections.delete(ws);
876
- wsModule.close?.(ws, code, message);
876
+ wsModule.close?.(ws, { code, message, platform });
877
877
  },
878
878
 
879
879
  maxPayloadLength: wsOptions.maxPayloadLength,
package/index.d.ts CHANGED
@@ -189,6 +189,46 @@ export interface UpgradeContext {
189
189
  remoteAddress: string;
190
190
  }
191
191
 
192
+ /**
193
+ * Context passed to `open` and `drain` handlers.
194
+ */
195
+ export interface OpenContext {
196
+ /** The platform API - publish, send, topic helpers, etc. */
197
+ platform: Platform;
198
+ }
199
+
200
+ /**
201
+ * Context passed to the `message` handler.
202
+ */
203
+ export interface MessageContext {
204
+ /** The raw message data. */
205
+ data: ArrayBuffer;
206
+ /** Whether the message is binary. */
207
+ isBinary: boolean;
208
+ /** The platform API - publish, send, topic helpers, etc. */
209
+ platform: Platform;
210
+ }
211
+
212
+ /**
213
+ * Context passed to the `close` handler.
214
+ */
215
+ export interface CloseContext {
216
+ /** The WebSocket close code. */
217
+ code: number;
218
+ /** The close reason (as ArrayBuffer). */
219
+ message: ArrayBuffer;
220
+ /** The platform API - publish, send, topic helpers, etc. */
221
+ platform: Platform;
222
+ }
223
+
224
+ /**
225
+ * Context passed to the `subscribe` handler.
226
+ */
227
+ export interface SubscribeContext {
228
+ /** The platform API - publish, send, topic helpers, etc. */
229
+ platform: Platform;
230
+ }
231
+
192
232
  /**
193
233
  * Shape of the user's WebSocket handler module.
194
234
  *
@@ -196,6 +236,10 @@ export interface UpgradeContext {
196
236
  * of these functions. All are optional - the built-in handler already
197
237
  * handles subscribe/unsubscribe for the client store.
198
238
  *
239
+ * Every hook receives `(ws, context)` where context always includes `platform`
240
+ * plus any hook-specific fields. This gives you full access to publish, send,
241
+ * and topic helpers directly in your WebSocket hooks.
242
+ *
199
243
  * @example
200
244
  * ```js
201
245
  * // src/hooks.ws.js - auto-discovered, no config needed
@@ -207,8 +251,13 @@ export interface UpgradeContext {
207
251
  * return { userId: user.id }; // attach data to socket
208
252
  * }
209
253
  *
210
- * export function open(ws) {
254
+ * export function open(ws, { platform }) {
211
255
  * ws.subscribe(`user:${ws.getUserData().userId}`);
256
+ * platform.topic('users').increment();
257
+ * }
258
+ *
259
+ * export function close(ws, { platform }) {
260
+ * platform.topic('users').decrement();
212
261
  * }
213
262
  * ```
214
263
  */
@@ -225,7 +274,7 @@ export interface WebSocketHandler<UserData = unknown> {
225
274
  upgrade?: (ctx: UpgradeContext) => UserData | false | Promise<UserData | false>;
226
275
 
227
276
  /** Called when a WebSocket connection is established. */
228
- open?: (ws: WebSocket<UserData>) => void;
277
+ open?: (ws: WebSocket<UserData>, ctx: OpenContext) => void;
229
278
 
230
279
  /**
231
280
  * Called when a message is received.
@@ -234,7 +283,7 @@ export interface WebSocketHandler<UserData = unknown> {
234
283
  * handled automatically before this is called. You only need this for
235
284
  * custom application-level messages.
236
285
  */
237
- message?: (ws: WebSocket<UserData>, data: ArrayBuffer, isBinary: boolean) => void;
286
+ message?: (ws: WebSocket<UserData>, ctx: MessageContext) => void;
238
287
 
239
288
  /**
240
289
  * Called when a client tries to subscribe to a topic.
@@ -246,22 +295,22 @@ export interface WebSocketHandler<UserData = unknown> {
246
295
  *
247
296
  * @example
248
297
  * ```js
249
- * export function subscribe(ws, topic) {
298
+ * export function subscribe(ws, topic, { platform }) {
250
299
  * const { role } = ws.getUserData();
251
300
  * if (topic.startsWith('admin') && role !== 'admin') return false;
252
301
  * }
253
302
  * ```
254
303
  */
255
- subscribe?: (ws: WebSocket<UserData>, topic: string) => boolean | void;
304
+ subscribe?: (ws: WebSocket<UserData>, topic: string, ctx: SubscribeContext) => boolean | void;
256
305
 
257
306
  /**
258
307
  * Called when backpressure has drained (buffered data was sent).
259
308
  * Use this for flow control when sending large or frequent messages.
260
309
  */
261
- drain?: (ws: WebSocket<UserData>) => void;
310
+ drain?: (ws: WebSocket<UserData>, ctx: OpenContext) => void;
262
311
 
263
312
  /** Called when the connection closes. */
264
- close?: (ws: WebSocket<UserData>, code: number, message: ArrayBuffer) => void;
313
+ close?: (ws: WebSocket<UserData>, ctx: CloseContext) => void;
265
314
  }
266
315
 
267
316
  // -- Platform type for event.platform ----------------------------------------
@@ -310,8 +359,8 @@ export interface Platform {
310
359
  * @example
311
360
  * ```js
312
361
  * // In hooks.ws.js - reply to sender:
313
- * export function message(ws, rawData) {
314
- * const msg = JSON.parse(Buffer.from(rawData).toString());
362
+ * export function message(ws, { data }) {
363
+ * const msg = JSON.parse(Buffer.from(data).toString());
315
364
  * ws.send(JSON.stringify({ topic: 'echo', event: 'reply', data: { got: msg } }));
316
365
  * }
317
366
  * ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "svelte-adapter-uws",
3
- "version": "0.2.8",
3
+ "version": "0.2.10",
4
4
  "description": "SvelteKit adapter for uWebSockets.js - high-performance C++ HTTP server with built-in WebSocket support",
5
5
  "author": "Kevin Radziszewski",
6
6
  "license": "MIT",
package/vite.js CHANGED
@@ -310,7 +310,7 @@ export default function uws(options = {}) {
310
310
  wsWrappers.set(ws, wrapped);
311
311
 
312
312
  // Call user open handler
313
- userHandlers.open?.(wrapped);
313
+ userHandlers.open?.(wrapped, { platform });
314
314
 
315
315
  ws.on('message', async (raw, isBinary) => {
316
316
  // Convert to ArrayBuffer (matching uWS interface)
@@ -324,7 +324,7 @@ export default function uws(options = {}) {
324
324
  try {
325
325
  const msg = JSON.parse(buf.toString());
326
326
  if (msg.type === 'subscribe' && typeof msg.topic === 'string') {
327
- if (userHandlers.subscribe && userHandlers.subscribe(wrapped, msg.topic) === false) {
327
+ if (userHandlers.subscribe && userHandlers.subscribe(wrapped, msg.topic, { platform }) === false) {
328
328
  return;
329
329
  }
330
330
  subscriptions.get(ws)?.add(msg.topic);
@@ -342,14 +342,14 @@ export default function uws(options = {}) {
342
342
  // Delegate to user handler
343
343
  await handlerReady;
344
344
  if (userHandlers.message) {
345
- userHandlers.message(wrapped, arrayBuffer, !!isBinary);
345
+ userHandlers.message(wrapped, { data: arrayBuffer, isBinary: !!isBinary, platform });
346
346
  }
347
347
  });
348
348
 
349
349
  ws.on('close', (code, reason) => {
350
350
  const reasonBuf = reason || Buffer.alloc(0);
351
351
  const reasonAB = reasonBuf.buffer.slice(reasonBuf.byteOffset, reasonBuf.byteOffset + reasonBuf.byteLength);
352
- userHandlers.close?.(wrapped, code, reasonAB);
352
+ userHandlers.close?.(wrapped, { code, message: reasonAB, platform });
353
353
  connections.delete(ws);
354
354
  subscriptions.delete(ws);
355
355
  wsWrappers.delete(ws);