svelte-adapter-uws 0.4.6 → 0.4.7
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 +44 -1
- package/client.d.ts +8 -0
- package/client.js +5 -4
- package/files/handler.js +9 -6
- package/files/index.js +16 -6
- package/index.js +14 -1
- package/package.json +6 -2
- package/plugins/presence/server.js +5 -0
- package/plugins/replay/server.js +5 -1
- package/vite.js +19 -7
package/README.md
CHANGED
|
@@ -1237,7 +1237,7 @@ await ready();
|
|
|
1237
1237
|
// connection is now open, safe to send messages
|
|
1238
1238
|
```
|
|
1239
1239
|
|
|
1240
|
-
In SSR (no browser WebSocket), `ready()` resolves immediately and is a no-op.
|
|
1240
|
+
In SSR (no browser WebSocket and no explicit `url`), `ready()` resolves immediately and is a no-op. In native app environments where `window` doesn't exist but you passed a `url` to `connect()`, `ready()` correctly waits for the connection to open.
|
|
1241
1241
|
|
|
1242
1242
|
`ready()` rejects if the connection is permanently closed before it opens. This happens when the server sends a terminal close code (1008/4401/4403), retries are exhausted, or `close()` is called explicitly. If you call `ready()` in a context where permanent closure is possible, add a `.catch()` handler or use `try/await/catch`.
|
|
1243
1243
|
|
|
@@ -1251,6 +1251,7 @@ Most users don't need this - `on()` and `status` auto-connect. Use `connect()` w
|
|
|
1251
1251
|
import { connect } from 'svelte-adapter-uws/client';
|
|
1252
1252
|
|
|
1253
1253
|
const ws = connect({
|
|
1254
|
+
url: 'wss://my-app.com/ws', // full URL for cross-origin / native app usage (overrides path)
|
|
1254
1255
|
path: '/ws', // default: '/ws'
|
|
1255
1256
|
reconnectInterval: 3000, // default: 3000 ms
|
|
1256
1257
|
maxReconnectInterval: 30000, // default: 30000 ms
|
|
@@ -1294,6 +1295,22 @@ The client handles several edge cases automatically, with no configuration requi
|
|
|
1294
1295
|
|
|
1295
1296
|
**Zombie detection**: the client checks every 30 seconds whether the server has been completely silent for more than 150 seconds (2.5x the server's idle timeout). If so, it forces a close and reconnects. This catches connections that appear open but were silently dropped by the server, which is common on mobile after wake from sleep.
|
|
1296
1297
|
|
|
1298
|
+
### Cross-origin and native app usage
|
|
1299
|
+
|
|
1300
|
+
By default, the client derives the WebSocket URL from `window.location`. If your client runs on a different origin -- a mobile app (Svelte Native, React Native), a standalone Node.js script, or any context where the backend lives elsewhere -- pass a `url` to connect to it directly:
|
|
1301
|
+
|
|
1302
|
+
```js
|
|
1303
|
+
import { connect, on } from 'svelte-adapter-uws/client';
|
|
1304
|
+
|
|
1305
|
+
connect({ url: 'wss://my-app.com/ws' });
|
|
1306
|
+
|
|
1307
|
+
const todos = on('todos');
|
|
1308
|
+
```
|
|
1309
|
+
|
|
1310
|
+
When `url` is set, `path` is ignored and the `window` check is bypassed, so the client works in environments without a browser DOM. All other features (reconnect, backoff, batch resubscription, topic stores) work the same way.
|
|
1311
|
+
|
|
1312
|
+
> **Note:** Your server's `allowedOrigins` config must include the origin your client connects from (or `'*'` during development). See the [origin validation](#origin-validation) section.
|
|
1313
|
+
|
|
1297
1314
|
---
|
|
1298
1315
|
|
|
1299
1316
|
## Seeding initial state
|
|
@@ -2870,6 +2887,32 @@ Or if you're using `on()` directly (which auto-connects), call `connect()` first
|
|
|
2870
2887
|
|
|
2871
2888
|
---
|
|
2872
2889
|
|
|
2890
|
+
## Testing
|
|
2891
|
+
|
|
2892
|
+
```bash
|
|
2893
|
+
npm test # 711 unit tests (vitest, ~2s)
|
|
2894
|
+
npm run test:e2e # 25 e2e tests (playwright, ~13s)
|
|
2895
|
+
npm run test:coverage # both + coverage reports (~30s)
|
|
2896
|
+
```
|
|
2897
|
+
|
|
2898
|
+
Unit tests cover store patterns, adapter options, plugin logic, and client behavior using mocked WebSocket globals. They run in vitest with the `vmForks` pool.
|
|
2899
|
+
|
|
2900
|
+
E2e tests start a real SvelteKit app (`test/fixture/`) with the adapter installed via `file:../..`. Playwright runs two projects:
|
|
2901
|
+
|
|
2902
|
+
- **dev** -- `vite dev` with the Vite plugin. Tests SSR, static files, WebSocket pub/sub (via `ws` clients), and the real `client.js` running in Chromium.
|
|
2903
|
+
- **prod** -- `vite build` + `node build/index.js` through uWebSockets.js. Tests the same surface against the production runtime, plus the health check endpoint and 404 handling.
|
|
2904
|
+
|
|
2905
|
+
The coverage script collects V8 coverage from both the Playwright server processes (vite.js, handler.js) and the browser (client.js via Chrome DevTools Protocol), then reports them alongside the vitest unit coverage.
|
|
2906
|
+
|
|
2907
|
+
First-time setup for e2e:
|
|
2908
|
+
|
|
2909
|
+
```bash
|
|
2910
|
+
cd test/fixture && npm install && cd ../..
|
|
2911
|
+
npx playwright install chromium
|
|
2912
|
+
```
|
|
2913
|
+
|
|
2914
|
+
---
|
|
2915
|
+
|
|
2873
2916
|
## Related projects
|
|
2874
2917
|
|
|
2875
2918
|
- [svelte-adapter-uws-extensions](https://github.com/lanteanio/svelte-adapter-uws-extensions) -- Redis-backed extensions for multi-server deployments: persistent presence, distributed pub/sub, session storage, and more.
|
package/client.d.ts
CHANGED
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
import type { Readable } from 'svelte/store';
|
|
2
2
|
|
|
3
3
|
export interface ConnectOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Full WebSocket URL to connect to.
|
|
6
|
+
* When set, `path` is ignored and the client connects to this URL directly.
|
|
7
|
+
* Enables cross-origin usage (e.g. svelte-native, React Native, standalone clients).
|
|
8
|
+
* @example 'wss://my-app.com/ws'
|
|
9
|
+
*/
|
|
10
|
+
url?: string;
|
|
11
|
+
|
|
4
12
|
/**
|
|
5
13
|
* WebSocket endpoint path. Must match the adapter config.
|
|
6
14
|
* @default '/ws'
|
package/client.js
CHANGED
|
@@ -132,9 +132,7 @@ export const status = {
|
|
|
132
132
|
* @returns {Promise<void>}
|
|
133
133
|
*/
|
|
134
134
|
export function ready() {
|
|
135
|
-
|
|
136
|
-
// will never reach 'open'. Resolve immediately so await ready() is a no-op.
|
|
137
|
-
if (typeof window === 'undefined') return Promise.resolve();
|
|
135
|
+
if (typeof window === 'undefined' && !(singleton && singleton._hasUrl)) return Promise.resolve();
|
|
138
136
|
|
|
139
137
|
const conn = ensureConnection();
|
|
140
138
|
return new Promise((resolve, reject) => {
|
|
@@ -506,6 +504,7 @@ const THROTTLE_CLOSE_CODES = new Set([
|
|
|
506
504
|
*/
|
|
507
505
|
function createConnection(options) {
|
|
508
506
|
const {
|
|
507
|
+
url,
|
|
509
508
|
path = '/ws',
|
|
510
509
|
reconnectInterval = 3000,
|
|
511
510
|
maxReconnectInterval = 30000,
|
|
@@ -565,13 +564,14 @@ function createConnection(options) {
|
|
|
565
564
|
const permaClosedStore = writable(false);
|
|
566
565
|
|
|
567
566
|
function getUrl() {
|
|
567
|
+
if (url) return url;
|
|
568
568
|
if (typeof window === 'undefined') return '';
|
|
569
569
|
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
|
570
570
|
return `${protocol}//${window.location.host}${path}`;
|
|
571
571
|
}
|
|
572
572
|
|
|
573
573
|
function doConnect() {
|
|
574
|
-
if (typeof window === 'undefined') return;
|
|
574
|
+
if (!url && typeof window === 'undefined') return;
|
|
575
575
|
if (ws && (ws.readyState === WebSocket.CONNECTING || ws.readyState === WebSocket.OPEN)) return;
|
|
576
576
|
|
|
577
577
|
statusStore.set('connecting');
|
|
@@ -1006,6 +1006,7 @@ function createConnection(options) {
|
|
|
1006
1006
|
events: { subscribe: eventsStore.subscribe },
|
|
1007
1007
|
status: { subscribe: statusStore.subscribe },
|
|
1008
1008
|
_permaClosed: { subscribe: permaClosedStore.subscribe },
|
|
1009
|
+
_hasUrl: !!url,
|
|
1009
1010
|
on: onTopic,
|
|
1010
1011
|
_onEvent: onEvent,
|
|
1011
1012
|
_release: release,
|
package/files/handler.js
CHANGED
|
@@ -408,7 +408,7 @@ const platform = {
|
|
|
408
408
|
* No-op if no clients are subscribed - safe to call unconditionally.
|
|
409
409
|
*/
|
|
410
410
|
publish(topic, event, data, options) {
|
|
411
|
-
const envelope = envelopePrefix(topic, event) + JSON.stringify(data) + '}';
|
|
411
|
+
const envelope = envelopePrefix(topic, event) + JSON.stringify(data ?? null) + '}';
|
|
412
412
|
const result = app.publish(topic, envelope, false, false);
|
|
413
413
|
// Relay to other workers via main thread (no-op in single-process mode).
|
|
414
414
|
// Pass { relay: false } when the message originates from an external
|
|
@@ -433,7 +433,7 @@ const platform = {
|
|
|
433
433
|
* Wraps in the same { topic, event, data } envelope as publish().
|
|
434
434
|
*/
|
|
435
435
|
send(ws, topic, event, data) {
|
|
436
|
-
return ws.send(envelopePrefix(topic, event) + JSON.stringify(data) + '}', false, false);
|
|
436
|
+
return ws.send(envelopePrefix(topic, event) + JSON.stringify(data ?? null) + '}', false, false);
|
|
437
437
|
},
|
|
438
438
|
|
|
439
439
|
/**
|
|
@@ -442,7 +442,7 @@ const platform = {
|
|
|
442
442
|
* Returns the number of connections the message was sent to.
|
|
443
443
|
*/
|
|
444
444
|
sendTo(filter, topic, event, data) {
|
|
445
|
-
const envelope = envelopePrefix(topic, event) + JSON.stringify(data) + '}';
|
|
445
|
+
const envelope = envelopePrefix(topic, event) + JSON.stringify(data ?? null) + '}';
|
|
446
446
|
let count = 0;
|
|
447
447
|
for (const ws of wsConnections) {
|
|
448
448
|
if (filter(ws.getUserData())) {
|
|
@@ -1381,8 +1381,11 @@ if (WS_ENABLED) {
|
|
|
1381
1381
|
upgradeRateMap.set(clientIp, rateEntry);
|
|
1382
1382
|
} else {
|
|
1383
1383
|
const elapsed = now - rateEntry.windowStart;
|
|
1384
|
-
if (elapsed >= UPGRADE_WINDOW_MS) {
|
|
1385
|
-
|
|
1384
|
+
if (elapsed >= 2 * UPGRADE_WINDOW_MS) {
|
|
1385
|
+
rateEntry.prev = 0;
|
|
1386
|
+
rateEntry.curr = 0;
|
|
1387
|
+
rateEntry.windowStart = now;
|
|
1388
|
+
} else if (elapsed >= UPGRADE_WINDOW_MS) {
|
|
1386
1389
|
rateEntry.prev = rateEntry.curr;
|
|
1387
1390
|
rateEntry.curr = 0;
|
|
1388
1391
|
rateEntry.windowStart = now;
|
|
@@ -1423,7 +1426,7 @@ if (WS_ENABLED) {
|
|
|
1423
1426
|
const parsed = new URL(reqOrigin);
|
|
1424
1427
|
const requestHost = (host_header && headers[host_header]) || headers['host'];
|
|
1425
1428
|
if (!requestHost) {
|
|
1426
|
-
allowed =
|
|
1429
|
+
allowed = false;
|
|
1427
1430
|
} else {
|
|
1428
1431
|
const requestScheme = protocol_header
|
|
1429
1432
|
? (headers[protocol_header] || (is_tls ? 'https' : 'http'))
|
package/files/index.js
CHANGED
|
@@ -4,9 +4,19 @@ import { fileURLToPath } from 'node:url';
|
|
|
4
4
|
import { env } from 'ENV';
|
|
5
5
|
|
|
6
6
|
const host = env('HOST', '0.0.0.0');
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
const port_raw = env('PORT', '3000');
|
|
8
|
+
|
|
9
|
+
function parseIntEnv(name, raw, min) {
|
|
10
|
+
const trimmed = raw.trim();
|
|
11
|
+
const n = Number(trimmed);
|
|
12
|
+
if (trimmed === '' || !Number.isInteger(n)) throw new Error(`${name} must be a valid integer, got "${raw}"`);
|
|
13
|
+
if (n < min) throw new Error(`${name} must be >= ${min}, got ${n}`);
|
|
14
|
+
return n;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const port = parseIntEnv('PORT', port_raw, 0);
|
|
18
|
+
const shutdown_timeout = parseIntEnv('SHUTDOWN_TIMEOUT', env('SHUTDOWN_TIMEOUT', '30'), 0);
|
|
19
|
+
const shutdown_delay = parseIntEnv('SHUTDOWN_DELAY_MS', env('SHUTDOWN_DELAY_MS', '0'), 0);
|
|
10
20
|
const cluster_workers = env('CLUSTER_WORKERS', '');
|
|
11
21
|
|
|
12
22
|
const is_primary = cluster_workers && isMainThread;
|
|
@@ -126,7 +136,7 @@ if (is_primary) {
|
|
|
126
136
|
// Start (or resume) listening once a worker is ready to handle requests
|
|
127
137
|
if (!listening) {
|
|
128
138
|
listening = true;
|
|
129
|
-
const portNum =
|
|
139
|
+
const portNum = port;
|
|
130
140
|
acceptorApp.listen(host, portNum, (socket) => {
|
|
131
141
|
if (socket) {
|
|
132
142
|
listen_socket = socket;
|
|
@@ -256,13 +266,13 @@ if (is_primary) {
|
|
|
256
266
|
|
|
257
267
|
if (isMainThread) {
|
|
258
268
|
// Single-process mode (no clustering)
|
|
259
|
-
start(host,
|
|
269
|
+
start(host, port);
|
|
260
270
|
} else {
|
|
261
271
|
// Worker thread startup depends on clustering mode
|
|
262
272
|
if (workerData?.mode === 'reuseport') {
|
|
263
273
|
// Reuseport: each worker listens on the shared port directly.
|
|
264
274
|
// The kernel distributes incoming connections via SO_REUSEPORT.
|
|
265
|
-
start(host,
|
|
275
|
+
start(host, port);
|
|
266
276
|
parentPort.postMessage({ type: 'ready' });
|
|
267
277
|
} else {
|
|
268
278
|
// Acceptor: register with the main thread's acceptor app
|
package/index.js
CHANGED
|
@@ -102,13 +102,23 @@ export default function (opts = {}) {
|
|
|
102
102
|
const allEnv = loadEnv('production', process.cwd(), '');
|
|
103
103
|
const version = builder.config.kit.version?.name ?? '';
|
|
104
104
|
|
|
105
|
+
const aliasMap = { '$lib': libDir };
|
|
106
|
+
const kitAliases = builder.config.kit.alias;
|
|
107
|
+
if (kitAliases) {
|
|
108
|
+
for (const [key, value] of Object.entries(kitAliases)) {
|
|
109
|
+
if (!(key in aliasMap)) {
|
|
110
|
+
aliasMap[key] = path.resolve(value);
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
105
115
|
await esbuild.build({
|
|
106
116
|
entryPoints: [path.resolve(handlerFile)],
|
|
107
117
|
bundle: true,
|
|
108
118
|
format: 'esm',
|
|
109
119
|
platform: 'node',
|
|
110
120
|
outfile: `${tmp}/ws-handler.js`,
|
|
111
|
-
alias:
|
|
121
|
+
alias: aliasMap,
|
|
112
122
|
packages: 'external',
|
|
113
123
|
plugins: [{
|
|
114
124
|
name: 'sveltekit-virtual-modules',
|
|
@@ -124,6 +134,9 @@ export default function (opts = {}) {
|
|
|
124
134
|
const isPublic = args.path.includes('/public');
|
|
125
135
|
const isStatic = args.path.includes('/static');
|
|
126
136
|
if (!isStatic) {
|
|
137
|
+
if (isPublic) {
|
|
138
|
+
return { contents: `export const env = new Proxy(process.env, { get(t, k) { return typeof k === 'string' && k.startsWith(${JSON.stringify(publicPrefix)}) ? t[k] : undefined; }, ownKeys(t) { return Object.keys(t).filter(k => k.startsWith(${JSON.stringify(publicPrefix)})); }, has(t, k) { return typeof k === 'string' && k.startsWith(${JSON.stringify(publicPrefix)}) && k in t; }, getOwnPropertyDescriptor(t, k) { if (typeof k === 'string' && k.startsWith(${JSON.stringify(publicPrefix)}) && k in t) return { value: t[k], enumerable: true, configurable: true }; return undefined; } });` };
|
|
139
|
+
}
|
|
127
140
|
return { contents: 'export const env = process.env;' };
|
|
128
141
|
}
|
|
129
142
|
const entries = Object.entries(allEnv).filter(([k]) =>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-adapter-uws",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.7",
|
|
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",
|
|
@@ -98,7 +98,9 @@
|
|
|
98
98
|
],
|
|
99
99
|
"scripts": {
|
|
100
100
|
"test": "vitest run",
|
|
101
|
-
"test:watch": "vitest"
|
|
101
|
+
"test:watch": "vitest",
|
|
102
|
+
"test:e2e": "npx playwright test --config test/e2e/playwright.config.js",
|
|
103
|
+
"test:coverage": "node scripts/coverage.js"
|
|
102
104
|
},
|
|
103
105
|
"engines": {
|
|
104
106
|
"node": ">=20.0.0"
|
|
@@ -132,8 +134,10 @@
|
|
|
132
134
|
"websocket"
|
|
133
135
|
],
|
|
134
136
|
"devDependencies": {
|
|
137
|
+
"@playwright/test": "^1.59.1",
|
|
135
138
|
"@vitest/coverage-v8": "^4.0.18",
|
|
136
139
|
"autocannon": "^8.0.0",
|
|
140
|
+
"c8": "^11.0.0",
|
|
137
141
|
"polka": "^0.5.2",
|
|
138
142
|
"socket.io": "^4.8.3",
|
|
139
143
|
"socket.io-client": "^4.8.3",
|
|
@@ -275,6 +275,11 @@ export function createPresence(options = {}) {
|
|
|
275
275
|
if (connTopics && connTopics.has(topic)) return;
|
|
276
276
|
|
|
277
277
|
const data = select(ws.getUserData());
|
|
278
|
+
if (!data || typeof data !== 'object') {
|
|
279
|
+
throw new TypeError(
|
|
280
|
+
`presence select() must return a plain object, got ${data === null ? 'null' : typeof data}`
|
|
281
|
+
);
|
|
282
|
+
}
|
|
278
283
|
const key = resolveKey(data);
|
|
279
284
|
|
|
280
285
|
// Track per-connection
|
package/plugins/replay/server.js
CHANGED
|
@@ -205,7 +205,11 @@ export function createReplay(options = {}) {
|
|
|
205
205
|
publish(platform, topic, event, data) {
|
|
206
206
|
const state = getTopic(topic);
|
|
207
207
|
state.seq++;
|
|
208
|
-
|
|
208
|
+
let snapshot = data;
|
|
209
|
+
if (data != null && typeof data === 'object') {
|
|
210
|
+
try { snapshot = structuredClone(data); } catch { snapshot = JSON.parse(JSON.stringify(data)); }
|
|
211
|
+
}
|
|
212
|
+
pushMessage(state, { seq: state.seq, topic, event, data: snapshot });
|
|
209
213
|
return platform.publish(topic, event, data);
|
|
210
214
|
},
|
|
211
215
|
|
package/vite.js
CHANGED
|
@@ -80,12 +80,22 @@ export default function uws(options = {}) {
|
|
|
80
80
|
getBufferedAmount() { return rawWs.bufferedAmount || 0; },
|
|
81
81
|
getRemoteAddress() {
|
|
82
82
|
// uWS returns raw binary bytes (4 for IPv4, 16 for IPv6).
|
|
83
|
-
// Dev only handles IPv4; exotic addresses fall back to text encoding.
|
|
84
83
|
const ip = rawWs._socket?.remoteAddress || '127.0.0.1';
|
|
85
84
|
const v4 = ip.replace(/^::ffff:/, '');
|
|
86
85
|
const parts = v4.split('.');
|
|
87
86
|
if (parts.length === 4) return new Uint8Array(parts.map(Number)).buffer;
|
|
88
|
-
|
|
87
|
+
// IPv6: expand :: into zeroes, pack 8 groups into 16 bytes
|
|
88
|
+
const halves = v4.split('::');
|
|
89
|
+
const left = halves[0] ? halves[0].split(':') : [];
|
|
90
|
+
const right = halves.length > 1 && halves[1] ? halves[1].split(':') : [];
|
|
91
|
+
const pad = Array(8 - left.length - right.length).fill('0');
|
|
92
|
+
const groups = [...left, ...pad, ...right].map(g => parseInt(g, 16));
|
|
93
|
+
const buf = new Uint8Array(16);
|
|
94
|
+
for (let i = 0; i < 8; i++) {
|
|
95
|
+
buf[i * 2] = (groups[i] >> 8) & 0xff;
|
|
96
|
+
buf[i * 2 + 1] = groups[i] & 0xff;
|
|
97
|
+
}
|
|
98
|
+
return buf.buffer;
|
|
89
99
|
},
|
|
90
100
|
getRemoteAddressAsText() {
|
|
91
101
|
return new TextEncoder().encode(rawWs._socket?.remoteAddress || '127.0.0.1').buffer;
|
|
@@ -103,7 +113,7 @@ export default function uws(options = {}) {
|
|
|
103
113
|
* @returns {boolean}
|
|
104
114
|
*/
|
|
105
115
|
function publish(topic, event, data, _options) {
|
|
106
|
-
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}';
|
|
116
|
+
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data ?? null) + '}';
|
|
107
117
|
let sent = false;
|
|
108
118
|
for (const [ws, topics] of subscriptions) {
|
|
109
119
|
if (topics.has(topic) && ws.readyState === 1) {
|
|
@@ -123,7 +133,7 @@ export default function uws(options = {}) {
|
|
|
123
133
|
* @returns {number}
|
|
124
134
|
*/
|
|
125
135
|
function send(ws, topic, event, data) {
|
|
126
|
-
return ws.send('{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}', false, false) ?? 1;
|
|
136
|
+
return ws.send('{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data ?? null) + '}', false, false) ?? 1;
|
|
127
137
|
}
|
|
128
138
|
|
|
129
139
|
/**
|
|
@@ -135,7 +145,7 @@ export default function uws(options = {}) {
|
|
|
135
145
|
* @returns {number}
|
|
136
146
|
*/
|
|
137
147
|
function sendTo(filter, topic, event, data) {
|
|
138
|
-
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}';
|
|
148
|
+
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data ?? null) + '}';
|
|
139
149
|
let count = 0;
|
|
140
150
|
for (const [, wrapped] of wsWrappers) {
|
|
141
151
|
if (filter(wrapped.getUserData())) {
|
|
@@ -198,7 +208,8 @@ export default function uws(options = {}) {
|
|
|
198
208
|
message: mod.message,
|
|
199
209
|
close: mod.close,
|
|
200
210
|
drain: mod.drain,
|
|
201
|
-
subscribe: mod.subscribe
|
|
211
|
+
subscribe: mod.subscribe,
|
|
212
|
+
unsubscribe: mod.unsubscribe
|
|
202
213
|
};
|
|
203
214
|
}
|
|
204
215
|
|
|
@@ -480,7 +491,8 @@ export default function uws(options = {}) {
|
|
|
480
491
|
mod.message !== userHandlers.message ||
|
|
481
492
|
mod.close !== userHandlers.close ||
|
|
482
493
|
mod.drain !== userHandlers.drain ||
|
|
483
|
-
mod.subscribe !== userHandlers.subscribe
|
|
494
|
+
mod.subscribe !== userHandlers.subscribe ||
|
|
495
|
+
mod.unsubscribe !== userHandlers.unsubscribe) {
|
|
484
496
|
applyHandlers(mod);
|
|
485
497
|
// Close existing connections so they reconnect with the new handler.
|
|
486
498
|
// 1012 = "Service Restart" - clients with auto-reconnect will reconnect.
|