svelte-adapter-uws 0.2.15 → 0.2.16
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 +420 -2
- package/package.json +30 -1
- package/plugins/channels/client.d.ts +58 -0
- package/plugins/channels/client.js +63 -0
- package/plugins/channels/server.d.ts +88 -0
- package/plugins/channels/server.js +157 -0
- package/plugins/presence/client.d.ts +34 -0
- package/plugins/presence/client.js +107 -0
- package/plugins/presence/server.d.ts +135 -0
- package/plugins/presence/server.js +231 -0
- package/plugins/replay/client.d.ts +57 -0
- package/plugins/replay/client.js +186 -0
- package/plugins/replay/server.d.ts +101 -0
- package/plugins/replay/server.js +234 -0
- package/plugins/throttle/server.d.ts +99 -0
- package/plugins/throttle/server.js +221 -0
package/README.md
CHANGED
|
@@ -18,24 +18,39 @@ I've been loving Svelte and SvelteKit for a long time. I always wanted to expand
|
|
|
18
18
|
|
|
19
19
|
## Table of contents
|
|
20
20
|
|
|
21
|
+
**Getting started**
|
|
21
22
|
- [Installation](#installation)
|
|
22
23
|
- [Quick start: HTTP](#quick-start-http)
|
|
23
24
|
- [Quick start: HTTPS](#quick-start-https)
|
|
24
25
|
- [Quick start: WebSocket](#quick-start-websocket)
|
|
25
26
|
- [Quick start: WSS (secure WebSocket)](#quick-start-wss-secure-websocket)
|
|
26
27
|
- [Development, Preview & Production](#development-preview--production)
|
|
28
|
+
|
|
29
|
+
**Configuration**
|
|
27
30
|
- [Adapter options](#adapter-options)
|
|
28
31
|
- [Environment variables](#environment-variables)
|
|
32
|
+
- [TypeScript setup](#typescript-setup)
|
|
33
|
+
- [Svelte 4 support](#svelte-4-support)
|
|
34
|
+
|
|
35
|
+
**WebSocket deep dive**
|
|
29
36
|
- [WebSocket handler (`hooks.ws`)](#websocket-handler-hooksws)
|
|
30
37
|
- [Authentication](#authentication)
|
|
31
38
|
- [Platform API (`event.platform`)](#platform-api-eventplatform)
|
|
32
39
|
- [Client store API](#client-store-api)
|
|
33
40
|
- [Seeding initial state](#seeding-initial-state)
|
|
34
|
-
|
|
35
|
-
|
|
41
|
+
|
|
42
|
+
**Plugins**
|
|
43
|
+
- [Replay (SSR gap)](#replay-plugin-ssr-gap)
|
|
44
|
+
- [Presence](#presence-plugin)
|
|
45
|
+
- [Typed channels](#typed-channels-plugin)
|
|
46
|
+
- [Throttle/debounce](#throttledebounce-plugin)
|
|
47
|
+
|
|
48
|
+
**Deployment & scaling**
|
|
36
49
|
- [Deploying with Docker](#deploying-with-docker)
|
|
37
50
|
- [Clustering](#clustering)
|
|
38
51
|
- [Performance](#performance)
|
|
52
|
+
|
|
53
|
+
**Help**
|
|
39
54
|
- [Troubleshooting](#troubleshooting)
|
|
40
55
|
- [License](#license)
|
|
41
56
|
|
|
@@ -1165,6 +1180,409 @@ export async function subscribe(ws, topic, { platform }) {
|
|
|
1165
1180
|
|
|
1166
1181
|
---
|
|
1167
1182
|
|
|
1183
|
+
## Plugins
|
|
1184
|
+
|
|
1185
|
+
Opt-in modules that build on top of the adapter's public API. They don't change any core behavior -- if you don't import them, they don't exist. Each plugin ships in its own subdirectory under `plugins/` with separate server and client entry points.
|
|
1186
|
+
|
|
1187
|
+
### Replay plugin (SSR gap)
|
|
1188
|
+
|
|
1189
|
+
When you combine SSR with WebSocket live updates, there's a gap between server-side data loading and the moment the client's WebSocket connects. Messages published during that window are lost.
|
|
1190
|
+
|
|
1191
|
+
The replay plugin solves this without touching the adapter core. It's opt-in -- if you don't import it, it doesn't exist.
|
|
1192
|
+
|
|
1193
|
+
#### How it works
|
|
1194
|
+
|
|
1195
|
+
1. **Server:** publish through a replay buffer instead of `platform.publish()` directly -- messages get a sequence number and are stored in a ring buffer
|
|
1196
|
+
2. **SSR:** pass the current sequence number to the client via your `load()` function
|
|
1197
|
+
3. **Client:** `onReplay()` connects, requests missed messages, and switches to live mode once caught up
|
|
1198
|
+
|
|
1199
|
+
#### Setup
|
|
1200
|
+
|
|
1201
|
+
Create a shared replay instance:
|
|
1202
|
+
|
|
1203
|
+
```js
|
|
1204
|
+
// src/lib/server/replay.js
|
|
1205
|
+
import { createReplay } from 'svelte-adapter-uws/plugins/replay';
|
|
1206
|
+
|
|
1207
|
+
export const replay = createReplay({ size: 500 });
|
|
1208
|
+
```
|
|
1209
|
+
|
|
1210
|
+
Use it when publishing:
|
|
1211
|
+
|
|
1212
|
+
```js
|
|
1213
|
+
// src/routes/chat/+page.server.js
|
|
1214
|
+
import { replay } from '$lib/server/replay';
|
|
1215
|
+
|
|
1216
|
+
export async function load() {
|
|
1217
|
+
const messages = await db.getRecentMessages();
|
|
1218
|
+
return { messages, seq: replay.seq('chat') };
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
export const actions = {
|
|
1222
|
+
send: async ({ request, platform }) => {
|
|
1223
|
+
const form = await request.formData();
|
|
1224
|
+
const msg = await db.createMessage(Object.fromEntries(form));
|
|
1225
|
+
replay.publish(platform, 'chat', 'created', msg);
|
|
1226
|
+
}
|
|
1227
|
+
};
|
|
1228
|
+
```
|
|
1229
|
+
|
|
1230
|
+
Handle replay requests in your WebSocket handler:
|
|
1231
|
+
|
|
1232
|
+
```js
|
|
1233
|
+
// src/hooks.ws.js
|
|
1234
|
+
import { replay } from '$lib/server/replay';
|
|
1235
|
+
|
|
1236
|
+
export function message(ws, { data, platform }) {
|
|
1237
|
+
const msg = JSON.parse(Buffer.from(data).toString());
|
|
1238
|
+
if (msg.type === 'replay') {
|
|
1239
|
+
replay.replay(ws, msg.topic, msg.since, platform);
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
```
|
|
1244
|
+
|
|
1245
|
+
Subscribe on the client with gap-free delivery:
|
|
1246
|
+
|
|
1247
|
+
```svelte
|
|
1248
|
+
<!-- src/routes/chat/+page.svelte -->
|
|
1249
|
+
<script>
|
|
1250
|
+
import { onReplay } from 'svelte-adapter-uws/plugins/replay/client';
|
|
1251
|
+
let { data } = $props();
|
|
1252
|
+
|
|
1253
|
+
const messages = onReplay('chat', { since: data.seq }).scan(
|
|
1254
|
+
data.messages,
|
|
1255
|
+
(list, { event, data }) => {
|
|
1256
|
+
if (event === 'created') return [...list, data];
|
|
1257
|
+
return list;
|
|
1258
|
+
}
|
|
1259
|
+
);
|
|
1260
|
+
</script>
|
|
1261
|
+
|
|
1262
|
+
{#each $messages as msg}
|
|
1263
|
+
<p>{msg.text}</p>
|
|
1264
|
+
{/each}
|
|
1265
|
+
```
|
|
1266
|
+
|
|
1267
|
+
#### Server API
|
|
1268
|
+
|
|
1269
|
+
```js
|
|
1270
|
+
import { createReplay } from 'svelte-adapter-uws/plugins/replay';
|
|
1271
|
+
|
|
1272
|
+
const replay = createReplay({
|
|
1273
|
+
size: 1000, // max messages per topic (default: 1000)
|
|
1274
|
+
maxTopics: 100 // max tracked topics, oldest evicted (default: 100)
|
|
1275
|
+
});
|
|
1276
|
+
|
|
1277
|
+
replay.publish(platform, topic, event, data) // publish + buffer
|
|
1278
|
+
replay.seq(topic) // current sequence number
|
|
1279
|
+
replay.since(topic, seq) // buffered messages after seq
|
|
1280
|
+
replay.replay(ws, topic, sinceSeq, platform) // send missed messages to one client
|
|
1281
|
+
replay.clear() // reset everything
|
|
1282
|
+
replay.clearTopic(topic) // reset one topic
|
|
1283
|
+
```
|
|
1284
|
+
|
|
1285
|
+
#### Client API
|
|
1286
|
+
|
|
1287
|
+
```js
|
|
1288
|
+
import { onReplay } from 'svelte-adapter-uws/plugins/replay/client';
|
|
1289
|
+
|
|
1290
|
+
// Works exactly like on() but bridges the SSR gap
|
|
1291
|
+
const store = onReplay('chat', { since: data.seq });
|
|
1292
|
+
|
|
1293
|
+
// .scan() works the same as on().scan()
|
|
1294
|
+
const messages = onReplay('chat', { since: data.seq }).scan([], reducer);
|
|
1295
|
+
```
|
|
1296
|
+
|
|
1297
|
+
#### Limitations
|
|
1298
|
+
|
|
1299
|
+
- **In-memory only.** The ring buffer lives in the server process. A restart loses the buffer. For most apps this is fine -- the gap is typically under a second, and a page reload after a server restart gives fresh SSR data anyway.
|
|
1300
|
+
- **Single-worker only.** In clustered mode, each worker has its own buffer. If the SSR load runs on worker A and the WebSocket connects to worker B, the replay won't have the right messages. If you need replay with clustering, stick to a single worker or use an external store.
|
|
1301
|
+
- **Buffer overflow.** If more than `size` messages are published to a topic before a client requests replay, the oldest are gone. Size the buffer for your expected throughput during the SSR-to-connect window (usually well under 100 messages).
|
|
1302
|
+
|
|
1303
|
+
---
|
|
1304
|
+
|
|
1305
|
+
### Presence plugin
|
|
1306
|
+
|
|
1307
|
+
Track who's connected to a topic in real time. Handles multi-tab dedup (same user with two tabs open = one presence entry), broadcasts join/leave events, and provides a live store on the client.
|
|
1308
|
+
|
|
1309
|
+
Like the replay plugin, this is opt-in and has zero impact on the adapter core.
|
|
1310
|
+
|
|
1311
|
+
#### Setup
|
|
1312
|
+
|
|
1313
|
+
Create a shared presence instance:
|
|
1314
|
+
|
|
1315
|
+
```js
|
|
1316
|
+
// src/lib/server/presence.js
|
|
1317
|
+
import { createPresence } from 'svelte-adapter-uws/plugins/presence';
|
|
1318
|
+
|
|
1319
|
+
export const presence = createPresence({
|
|
1320
|
+
key: 'id',
|
|
1321
|
+
select: (userData) => ({ id: userData.id, name: userData.name })
|
|
1322
|
+
});
|
|
1323
|
+
```
|
|
1324
|
+
|
|
1325
|
+
Wire it into your WebSocket hooks:
|
|
1326
|
+
|
|
1327
|
+
```js
|
|
1328
|
+
// src/hooks.ws.js
|
|
1329
|
+
import { presence } from '$lib/server/presence';
|
|
1330
|
+
|
|
1331
|
+
export function upgrade({ cookies }) {
|
|
1332
|
+
const user = validateSession(cookies.session_id);
|
|
1333
|
+
if (!user) return false;
|
|
1334
|
+
return { id: user.id, name: user.name };
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
export function subscribe(ws, topic, { platform }) {
|
|
1338
|
+
presence.join(ws, topic, platform);
|
|
1339
|
+
}
|
|
1340
|
+
|
|
1341
|
+
export function close(ws, { platform }) {
|
|
1342
|
+
presence.leave(ws, platform);
|
|
1343
|
+
}
|
|
1344
|
+
```
|
|
1345
|
+
|
|
1346
|
+
Use it on the client:
|
|
1347
|
+
|
|
1348
|
+
```svelte
|
|
1349
|
+
<!-- src/routes/room/+page.svelte -->
|
|
1350
|
+
<script>
|
|
1351
|
+
import { on } from 'svelte-adapter-uws/client';
|
|
1352
|
+
import { presence } from 'svelte-adapter-uws/plugins/presence/client';
|
|
1353
|
+
|
|
1354
|
+
const messages = on('room');
|
|
1355
|
+
const users = presence('room');
|
|
1356
|
+
</script>
|
|
1357
|
+
|
|
1358
|
+
<aside>
|
|
1359
|
+
<h3>{$users.length} online</h3>
|
|
1360
|
+
{#each $users as user (user.id)}
|
|
1361
|
+
<span>{user.name}</span>
|
|
1362
|
+
{/each}
|
|
1363
|
+
</aside>
|
|
1364
|
+
```
|
|
1365
|
+
|
|
1366
|
+
Use `presence.list()` in load functions for SSR:
|
|
1367
|
+
|
|
1368
|
+
```js
|
|
1369
|
+
// +page.server.js
|
|
1370
|
+
import { presence } from '$lib/server/presence';
|
|
1371
|
+
|
|
1372
|
+
export async function load() {
|
|
1373
|
+
return { users: presence.list('room'), online: presence.count('room') };
|
|
1374
|
+
}
|
|
1375
|
+
```
|
|
1376
|
+
|
|
1377
|
+
#### Server API
|
|
1378
|
+
|
|
1379
|
+
```js
|
|
1380
|
+
import { createPresence } from 'svelte-adapter-uws/plugins/presence';
|
|
1381
|
+
|
|
1382
|
+
const presence = createPresence({
|
|
1383
|
+
key: 'id', // field for multi-tab dedup (default: 'id')
|
|
1384
|
+
select: (userData) => userData // extract public fields (default: full userData)
|
|
1385
|
+
});
|
|
1386
|
+
|
|
1387
|
+
presence.join(ws, topic, platform) // add user to topic (call from subscribe hook)
|
|
1388
|
+
presence.leave(ws, platform) // remove from all topics (call from close hook)
|
|
1389
|
+
presence.sync(ws, topic, platform) // send list without joining (for observers)
|
|
1390
|
+
presence.list(topic) // current user data array
|
|
1391
|
+
presence.count(topic) // unique user count
|
|
1392
|
+
presence.clear() // reset everything
|
|
1393
|
+
```
|
|
1394
|
+
|
|
1395
|
+
#### Client API
|
|
1396
|
+
|
|
1397
|
+
```js
|
|
1398
|
+
import { presence } from 'svelte-adapter-uws/plugins/presence/client';
|
|
1399
|
+
|
|
1400
|
+
const users = presence('room');
|
|
1401
|
+
// $users = [{ id: '1', name: 'Alice' }, { id: '2', name: 'Bob' }]
|
|
1402
|
+
```
|
|
1403
|
+
|
|
1404
|
+
#### How multi-tab dedup works
|
|
1405
|
+
|
|
1406
|
+
If user "Alice" (key `id: '1'`) has three browser tabs open, `presence.join()` is called three times with the same key. The plugin ref-counts connections per key: Alice appears once in the list. When she closes two tabs, she stays present. Only when the last tab closes does the plugin broadcast a `leave` event.
|
|
1407
|
+
|
|
1408
|
+
If no `key` field is found in the selected data (e.g. no auth), each connection is tracked separately.
|
|
1409
|
+
|
|
1410
|
+
#### Limitations
|
|
1411
|
+
|
|
1412
|
+
- **In-memory only.** Same as replay -- server restart clears presence. On restart, clients reconnect and re-subscribe, so the list rebuilds within seconds.
|
|
1413
|
+
- **Single-worker only.** Each worker tracks its own presence. In clustered mode, the list reflects only the local worker's connections.
|
|
1414
|
+
- **Requires subscription.** The client must subscribe to the topic (via `on()`, `crud()`, etc.) for the server's `subscribe` hook to fire. `presence('room')` alone shows you the list but doesn't register you as present unless you're also subscribed to `room`.
|
|
1415
|
+
|
|
1416
|
+
### Typed channels plugin
|
|
1417
|
+
|
|
1418
|
+
Define message schemas per topic so event names and data shapes are validated at publish time. Catches typos and shape mismatches before they reach the wire -- instead of silently sending garbage that the client ignores.
|
|
1419
|
+
|
|
1420
|
+
#### Setup
|
|
1421
|
+
|
|
1422
|
+
```js
|
|
1423
|
+
// src/lib/server/channels.js
|
|
1424
|
+
import { createChannel } from 'svelte-adapter-uws/plugins/channels';
|
|
1425
|
+
|
|
1426
|
+
export const todos = createChannel('todos', {
|
|
1427
|
+
created: (d) => ({ id: d.id, text: d.text, done: d.done }),
|
|
1428
|
+
updated: (d) => ({ id: d.id, text: d.text, done: d.done }),
|
|
1429
|
+
deleted: (d) => ({ id: d.id })
|
|
1430
|
+
});
|
|
1431
|
+
```
|
|
1432
|
+
|
|
1433
|
+
Each event maps to a validator function. The function receives the raw data and returns the validated (and optionally transformed) output. Throw to reject.
|
|
1434
|
+
|
|
1435
|
+
With Zod (or any library that exposes `.parse()`):
|
|
1436
|
+
|
|
1437
|
+
```js
|
|
1438
|
+
import { z } from 'zod';
|
|
1439
|
+
import { createChannel } from 'svelte-adapter-uws/plugins/channels';
|
|
1440
|
+
|
|
1441
|
+
const Todo = z.object({ id: z.string(), text: z.string(), done: z.boolean() });
|
|
1442
|
+
|
|
1443
|
+
export const todos = createChannel('todos', {
|
|
1444
|
+
created: Todo,
|
|
1445
|
+
updated: Todo,
|
|
1446
|
+
deleted: z.object({ id: z.string() })
|
|
1447
|
+
});
|
|
1448
|
+
```
|
|
1449
|
+
|
|
1450
|
+
#### Server API
|
|
1451
|
+
|
|
1452
|
+
```js
|
|
1453
|
+
import { todos } from '$lib/server/channels';
|
|
1454
|
+
|
|
1455
|
+
// In a form action or API route:
|
|
1456
|
+
export async function POST({ request, platform }) {
|
|
1457
|
+
const data = await request.json();
|
|
1458
|
+
const todo = await db.save(data);
|
|
1459
|
+
|
|
1460
|
+
todos.publish(platform, 'created', todo); // validates, then publishes
|
|
1461
|
+
todos.publish(platform, 'typo', todo); // throws: unknown event "typo"
|
|
1462
|
+
todos.publish(platform, 'created', {}); // throws: validation failed (if validator rejects)
|
|
1463
|
+
}
|
|
1464
|
+
```
|
|
1465
|
+
|
|
1466
|
+
| Method | Description |
|
|
1467
|
+
|---|---|
|
|
1468
|
+
| `channel.publish(platform, event, data)` | Validate and broadcast to all subscribers |
|
|
1469
|
+
| `channel.send(platform, ws, event, data)` | Validate and send to a single connection |
|
|
1470
|
+
| `channel.topic` | The topic string |
|
|
1471
|
+
| `channel.events` | Array of valid event names |
|
|
1472
|
+
|
|
1473
|
+
Validators can strip private fields before publishing. If your validator returns `{ id, text }` but the input had `{ id, text, secret }`, only `id` and `text` reach clients.
|
|
1474
|
+
|
|
1475
|
+
#### Client API
|
|
1476
|
+
|
|
1477
|
+
The client wrapper is optional -- it catches event name typos on the receiving side too.
|
|
1478
|
+
|
|
1479
|
+
```svelte
|
|
1480
|
+
<script>
|
|
1481
|
+
import { channel } from 'svelte-adapter-uws/plugins/channels/client';
|
|
1482
|
+
|
|
1483
|
+
const todos = channel('todos', ['created', 'updated', 'deleted']);
|
|
1484
|
+
|
|
1485
|
+
const all = todos.on(); // all events (same as on('todos'))
|
|
1486
|
+
const created = todos.on('created'); // filtered (same as on('todos', 'created'))
|
|
1487
|
+
const typo = todos.on('craeted'); // throws Error immediately
|
|
1488
|
+
</script>
|
|
1489
|
+
```
|
|
1490
|
+
|
|
1491
|
+
The `events` array is optional. Without it, `.on()` works exactly like the regular `on()` with the topic pre-filled -- no validation, just convenience.
|
|
1492
|
+
|
|
1493
|
+
You can still use `crud()`, `lookup()`, `latest()`, etc. directly with the topic string. The client channel is purely additive.
|
|
1494
|
+
|
|
1495
|
+
#### Limitations
|
|
1496
|
+
|
|
1497
|
+
- **Runtime only.** The validation happens at publish/send time, not at compile time. TypeScript generics give you autocomplete for event names, but data shape checking is runtime.
|
|
1498
|
+
- **No dependency on Zod.** The plugin accepts any validator function or any object with a `.parse()` method. You bring your own validation library (or use plain functions).
|
|
1499
|
+
|
|
1500
|
+
### Throttle/debounce plugin
|
|
1501
|
+
|
|
1502
|
+
Per-topic publish rate limiting. Wraps `platform.publish()` to coalesce rapid-fire updates (mouse position, typing indicators, live metrics). Sends the latest value at most once per interval. No timers to manage yourself.
|
|
1503
|
+
|
|
1504
|
+
Two modes:
|
|
1505
|
+
|
|
1506
|
+
- **`throttle(ms)`** -- sends immediately on first call (leading edge), then at most once per interval (trailing edge). Latest value wins within each interval.
|
|
1507
|
+
- **`debounce(ms)`** -- waits until no calls for the full interval, then sends the latest value. Each new call resets the timer.
|
|
1508
|
+
|
|
1509
|
+
#### Setup
|
|
1510
|
+
|
|
1511
|
+
```js
|
|
1512
|
+
import { throttle, debounce } from 'svelte-adapter-uws/plugins/throttle';
|
|
1513
|
+
|
|
1514
|
+
const mouse = throttle(50); // at most once per 50ms per topic
|
|
1515
|
+
const search = debounce(300); // wait for 300ms of silence
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
#### Usage
|
|
1519
|
+
|
|
1520
|
+
```js
|
|
1521
|
+
// In hooks.ws.js
|
|
1522
|
+
import { mouse, search } from '$lib/server/rate-limiters';
|
|
1523
|
+
|
|
1524
|
+
export function message(ws, { data, platform }) {
|
|
1525
|
+
const msg = JSON.parse(Buffer.from(data).toString());
|
|
1526
|
+
|
|
1527
|
+
if (msg.type === 'cursor') {
|
|
1528
|
+
// 60 mouse moves/sec from 20 users = 1200 publishes/sec
|
|
1529
|
+
// With throttle(50), each topic publishes at most 20/sec
|
|
1530
|
+
mouse.publish(platform, 'cursors', 'move', {
|
|
1531
|
+
userId: ws.getUserData().id,
|
|
1532
|
+
x: msg.x, y: msg.y
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (msg.type === 'search') {
|
|
1537
|
+
// User types fast -- only publish when they pause
|
|
1538
|
+
search.publish(platform, 'search-results', 'query', { q: msg.q });
|
|
1539
|
+
}
|
|
1540
|
+
}
|
|
1541
|
+
```
|
|
1542
|
+
|
|
1543
|
+
Rate limiting is per-topic. If you call `mouse.publish()` for topics `'room-a'` and `'room-b'`, each topic has its own independent timer.
|
|
1544
|
+
|
|
1545
|
+
#### API
|
|
1546
|
+
|
|
1547
|
+
| Method | Description |
|
|
1548
|
+
|---|---|
|
|
1549
|
+
| `limiter.publish(platform, topic, event, data)` | Publish with rate limiting |
|
|
1550
|
+
| `limiter.flush()` | Send all pending immediately, clear all timers |
|
|
1551
|
+
| `limiter.flush(topic)` | Send pending for one topic |
|
|
1552
|
+
| `limiter.cancel()` | Discard all pending, clear all timers |
|
|
1553
|
+
| `limiter.cancel(topic)` | Discard pending for one topic |
|
|
1554
|
+
| `limiter.interval` | The configured interval in ms |
|
|
1555
|
+
|
|
1556
|
+
#### How throttle works
|
|
1557
|
+
|
|
1558
|
+
```
|
|
1559
|
+
t=0 publish({x:0}) --> sends immediately (leading edge)
|
|
1560
|
+
t=10 publish({x:1}) --> stored (latest)
|
|
1561
|
+
t=30 publish({x:2}) --> stored (overwrites x:1)
|
|
1562
|
+
t=50 [timer fires] --> sends {x:2} (trailing edge)
|
|
1563
|
+
t=60 publish({x:3}) --> stored
|
|
1564
|
+
t=100 [timer fires] --> sends {x:3}
|
|
1565
|
+
t=150 [timer fires] --> nothing pending, goes idle
|
|
1566
|
+
t=200 publish({x:4}) --> sends immediately (new leading edge)
|
|
1567
|
+
```
|
|
1568
|
+
|
|
1569
|
+
#### How debounce works
|
|
1570
|
+
|
|
1571
|
+
```
|
|
1572
|
+
t=0 publish({q:"h"}) --> stored, timer starts
|
|
1573
|
+
t=80 publish({q:"he"}) --> stored, timer resets
|
|
1574
|
+
t=160 publish({q:"hel"}) --> stored, timer resets
|
|
1575
|
+
t=260 [timer fires, 100ms] --> sends {q:"hel"}
|
|
1576
|
+
```
|
|
1577
|
+
|
|
1578
|
+
#### Limitations
|
|
1579
|
+
|
|
1580
|
+
- **Server-side only.** No client component -- the client receives messages at the throttled rate naturally.
|
|
1581
|
+
- **Latest value only.** Intermediate values within an interval are discarded, not queued. If you need every message delivered, don't throttle.
|
|
1582
|
+
- **Timer-based.** Uses `setTimeout` internally. Precision depends on Node.js event loop load (typically < 1ms drift).
|
|
1583
|
+
|
|
1584
|
+
---
|
|
1585
|
+
|
|
1168
1586
|
## TypeScript setup
|
|
1169
1587
|
|
|
1170
1588
|
Add the platform type to your `src/app.d.ts`:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-adapter-uws",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
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",
|
|
@@ -25,6 +25,34 @@
|
|
|
25
25
|
"./vite": {
|
|
26
26
|
"types": "./vite.d.ts",
|
|
27
27
|
"default": "./vite.js"
|
|
28
|
+
},
|
|
29
|
+
"./plugins/replay": {
|
|
30
|
+
"types": "./plugins/replay/server.d.ts",
|
|
31
|
+
"default": "./plugins/replay/server.js"
|
|
32
|
+
},
|
|
33
|
+
"./plugins/replay/client": {
|
|
34
|
+
"types": "./plugins/replay/client.d.ts",
|
|
35
|
+
"default": "./plugins/replay/client.js"
|
|
36
|
+
},
|
|
37
|
+
"./plugins/presence": {
|
|
38
|
+
"types": "./plugins/presence/server.d.ts",
|
|
39
|
+
"default": "./plugins/presence/server.js"
|
|
40
|
+
},
|
|
41
|
+
"./plugins/presence/client": {
|
|
42
|
+
"types": "./plugins/presence/client.d.ts",
|
|
43
|
+
"default": "./plugins/presence/client.js"
|
|
44
|
+
},
|
|
45
|
+
"./plugins/channels": {
|
|
46
|
+
"types": "./plugins/channels/server.d.ts",
|
|
47
|
+
"default": "./plugins/channels/server.js"
|
|
48
|
+
},
|
|
49
|
+
"./plugins/channels/client": {
|
|
50
|
+
"types": "./plugins/channels/client.d.ts",
|
|
51
|
+
"default": "./plugins/channels/client.js"
|
|
52
|
+
},
|
|
53
|
+
"./plugins/throttle": {
|
|
54
|
+
"types": "./plugins/throttle/server.d.ts",
|
|
55
|
+
"default": "./plugins/throttle/server.js"
|
|
28
56
|
}
|
|
29
57
|
},
|
|
30
58
|
"types": "./index.d.ts",
|
|
@@ -36,6 +64,7 @@
|
|
|
36
64
|
"vite.js",
|
|
37
65
|
"vite.d.ts",
|
|
38
66
|
"files",
|
|
67
|
+
"plugins",
|
|
39
68
|
"LICENSE",
|
|
40
69
|
"README.md"
|
|
41
70
|
],
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import type { Readable } from 'svelte/store';
|
|
2
|
+
import type { WSEvent, TopicStore } from '../../client.js';
|
|
3
|
+
|
|
4
|
+
export interface ClientChannel<T = unknown> {
|
|
5
|
+
/** The topic this channel subscribes to. */
|
|
6
|
+
readonly topic: string;
|
|
7
|
+
|
|
8
|
+
/** The valid event names (if provided at creation), or null. */
|
|
9
|
+
readonly events: string[] | null;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Get a reactive store for all events on this channel's topic.
|
|
13
|
+
*
|
|
14
|
+
* Same as `on(topic)` from the client library.
|
|
15
|
+
*/
|
|
16
|
+
on(): TopicStore<WSEvent<T>>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Get a reactive store filtered to a specific event.
|
|
20
|
+
*
|
|
21
|
+
* Same as `on(topic, event)` from the client library.
|
|
22
|
+
* Throws if the event name was not in the `events` array
|
|
23
|
+
* passed to `channel()`.
|
|
24
|
+
*/
|
|
25
|
+
on<E extends string>(event: E): TopicStore<{ data: T }>;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Create a client-side typed channel for a topic.
|
|
30
|
+
*
|
|
31
|
+
* Scopes event subscriptions into a single object and validates
|
|
32
|
+
* event names at call time. If you pass an `events` array, calling
|
|
33
|
+
* `.on(event)` with an unknown name throws immediately instead of
|
|
34
|
+
* silently subscribing to a misspelled event that never fires.
|
|
35
|
+
*
|
|
36
|
+
* @param topic - Topic to subscribe to
|
|
37
|
+
* @param events - Allowed event names (omit to skip validation)
|
|
38
|
+
*
|
|
39
|
+
* @example
|
|
40
|
+
* ```svelte
|
|
41
|
+
* <script>
|
|
42
|
+
* import { channel } from 'svelte-adapter-uws/plugins/channels/client';
|
|
43
|
+
*
|
|
44
|
+
* const todos = channel('todos', ['created', 'updated', 'deleted']);
|
|
45
|
+
*
|
|
46
|
+
* const all = todos.on(); // all events
|
|
47
|
+
* const created = todos.on('created'); // just 'created' events
|
|
48
|
+
* </script>
|
|
49
|
+
*
|
|
50
|
+
* {#each $created as todo}
|
|
51
|
+
* <p>New: {todo.data.text}</p>
|
|
52
|
+
* {/each}
|
|
53
|
+
* ```
|
|
54
|
+
*/
|
|
55
|
+
export function channel<T = unknown>(
|
|
56
|
+
topic: string,
|
|
57
|
+
events?: string[]
|
|
58
|
+
): ClientChannel<T>;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Client-side typed channels for svelte-adapter-uws.
|
|
3
|
+
*
|
|
4
|
+
* Scopes a topic's event subscriptions into a single object and validates
|
|
5
|
+
* event names at call time. The runtime behavior is identical to calling
|
|
6
|
+
* `on()` directly -- this is a convenience wrapper that catches typos
|
|
7
|
+
* early and keeps topic strings DRY.
|
|
8
|
+
*
|
|
9
|
+
* @module svelte-adapter-uws/plugins/channels/client
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { on } from '../../client.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Create a client-side typed channel for a topic.
|
|
16
|
+
*
|
|
17
|
+
* If you pass an `events` array, calling `.on(event)` with an unknown
|
|
18
|
+
* event name throws immediately instead of silently subscribing to
|
|
19
|
+
* a misspelled event that never fires.
|
|
20
|
+
*
|
|
21
|
+
* @template T
|
|
22
|
+
* @param {string} topic - Topic to subscribe to
|
|
23
|
+
* @param {string[]} [events] - Allowed event names (omit to skip validation)
|
|
24
|
+
* @returns {import('./client.js').ClientChannel<T>}
|
|
25
|
+
*
|
|
26
|
+
* @example
|
|
27
|
+
* ```svelte
|
|
28
|
+
* <script>
|
|
29
|
+
* import { channel } from 'svelte-adapter-uws/plugins/channels/client';
|
|
30
|
+
*
|
|
31
|
+
* const todos = channel('todos', ['created', 'updated', 'deleted']);
|
|
32
|
+
*
|
|
33
|
+
* const all = todos.on(); // all events (same as on('todos'))
|
|
34
|
+
* const created = todos.on('created'); // filtered (same as on('todos', 'created'))
|
|
35
|
+
* const typo = todos.on('craeted'); // throws Error immediately
|
|
36
|
+
* </script>
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export function channel(topic, events) {
|
|
40
|
+
const eventSet = events ? new Set(events) : null;
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
/** The topic this channel subscribes to. */
|
|
44
|
+
topic,
|
|
45
|
+
|
|
46
|
+
/** The valid event names (if provided at creation). */
|
|
47
|
+
events: events || null,
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get a reactive store for this channel's topic.
|
|
51
|
+
*
|
|
52
|
+
* @param {string} [event] - Filter to a specific event name
|
|
53
|
+
*/
|
|
54
|
+
on(event) {
|
|
55
|
+
if (event != null && eventSet && !eventSet.has(event)) {
|
|
56
|
+
throw new Error(
|
|
57
|
+
`channel "${topic}": unknown event "${event}". Valid events: ${[...eventSet].join(', ')}`
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
return event != null ? on(topic, event) : on(topic);
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
}
|