svelte-adapter-uws 0.2.0 → 0.2.2
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 +72 -22
- package/files/handler.js +32 -8
- package/files/index.js +16 -2
- package/index.d.ts +1 -0
- package/package.json +10 -3
- package/vite.js +25 -4
package/README.md
CHANGED
|
@@ -33,7 +33,7 @@ I've been loving Svelte and SvelteKit for a long time. I always wanted to expand
|
|
|
33
33
|
- [TypeScript setup](#typescript-setup)
|
|
34
34
|
- [Svelte 4 support](#svelte-4-support)
|
|
35
35
|
- [Deploying with Docker](#deploying-with-docker)
|
|
36
|
-
- [Clustering
|
|
36
|
+
- [Clustering](#clustering)
|
|
37
37
|
- [Performance](#performance)
|
|
38
38
|
- [Troubleshooting](#troubleshooting)
|
|
39
39
|
- [License](#license)
|
|
@@ -383,7 +383,7 @@ If you set `envPrefix: 'MY_APP_'` in the adapter config, all variables are prefi
|
|
|
383
383
|
| `XFF_DEPTH` | `1` | Position from right in `X-Forwarded-For` |
|
|
384
384
|
| `BODY_SIZE_LIMIT` | `512K` | Max request body size (supports `K`, `M`, `G` suffixes) |
|
|
385
385
|
| `SHUTDOWN_TIMEOUT` | `30` | Seconds to wait during graceful shutdown |
|
|
386
|
-
| `CLUSTER_WORKERS` | - | Number of worker
|
|
386
|
+
| `CLUSTER_WORKERS` | - | Number of worker threads (or `auto` for CPU count) |
|
|
387
387
|
|
|
388
388
|
### Graceful shutdown
|
|
389
389
|
|
|
@@ -657,7 +657,9 @@ Available in server hooks, load functions, form actions, and API routes.
|
|
|
657
657
|
|
|
658
658
|
### `platform.publish(topic, event, data)`
|
|
659
659
|
|
|
660
|
-
Send a message to all WebSocket clients subscribed to a topic
|
|
660
|
+
Send a message to all WebSocket clients subscribed to a topic.
|
|
661
|
+
|
|
662
|
+
Topic and event names are validated before being written into the JSON envelope -- quotes, backslashes, and control characters will throw. This prevents JSON injection when names are built from dynamic values like user IDs (`platform.publish(\`user:\${id}\`, ...)`). The validation is a single-pass char scan and adds no measurable overhead.
|
|
661
663
|
|
|
662
664
|
```js
|
|
663
665
|
// src/routes/todos/+page.server.js
|
|
@@ -1170,9 +1172,9 @@ docker run -p 3000:3000 \
|
|
|
1170
1172
|
|
|
1171
1173
|
---
|
|
1172
1174
|
|
|
1173
|
-
## Clustering
|
|
1175
|
+
## Clustering
|
|
1174
1176
|
|
|
1175
|
-
|
|
1177
|
+
The adapter supports multi-core scaling via uWebSockets.js worker thread distribution. A primary thread creates an acceptor app that distributes incoming connections across worker threads, each running their own uWS instance. This works on **all platforms** (Linux, macOS, Windows).
|
|
1176
1178
|
|
|
1177
1179
|
Set the `CLUSTER_WORKERS` environment variable to enable it:
|
|
1178
1180
|
|
|
@@ -1187,12 +1189,16 @@ CLUSTER_WORKERS=4 node build
|
|
|
1187
1189
|
CLUSTER_WORKERS=auto PORT=8080 ORIGIN=https://example.com node build
|
|
1188
1190
|
```
|
|
1189
1191
|
|
|
1190
|
-
|
|
1192
|
+
If a worker crashes, it is automatically restarted with exponential backoff. On `SIGTERM`/`SIGINT`, the primary tells all workers to drain in-flight requests and shut down gracefully.
|
|
1193
|
+
|
|
1194
|
+
### WebSocket + clustering
|
|
1191
1195
|
|
|
1192
|
-
|
|
1196
|
+
`platform.publish()` is automatically relayed across all workers via the primary thread, so subscribers on any worker receive the message. This is built in — no external pub/sub needed.
|
|
1193
1197
|
|
|
1194
|
-
-
|
|
1195
|
-
-
|
|
1198
|
+
Per-worker limitations (acceptable for most apps):
|
|
1199
|
+
- `platform.connections` — returns the count for the local worker only
|
|
1200
|
+
- `platform.subscribers(topic)` — returns the count for the local worker only
|
|
1201
|
+
- `platform.sendTo(filter, ...)` — only reaches connections on the local worker
|
|
1196
1202
|
|
|
1197
1203
|
---
|
|
1198
1204
|
|
|
@@ -1200,31 +1206,75 @@ The primary process forks N workers, each running their own uWS server on the sa
|
|
|
1200
1206
|
|
|
1201
1207
|
### Why uWebSockets.js?
|
|
1202
1208
|
|
|
1203
|
-
uWebSockets.js is a C++ HTTP and WebSocket server compiled to a native V8 addon.
|
|
1209
|
+
uWebSockets.js is a C++ HTTP and WebSocket server compiled to a native V8 addon. It consistently outperforms Node.js' built-in `http` module, Express, Fastify, and every other JavaScript HTTP server by a significant margin.
|
|
1210
|
+
|
|
1211
|
+
We ran a comprehensive benchmark suite isolating every layer of overhead - from barebones uWS through the full adapter pipeline - and compared against `@sveltejs/adapter-node` (Node http + Polka + sirv) and the most popular WebSocket libraries (`socket.io`, `ws`). The benchmark code is in the [`bench/`](bench/) directory so you can reproduce it yourself.
|
|
1204
1212
|
|
|
1205
|
-
###
|
|
1213
|
+
### HTTP: adapter-uws vs adapter-node
|
|
1206
1214
|
|
|
1207
|
-
|
|
1215
|
+
Tested with a trivial SvelteKit handler (isolates adapter overhead from your app code):
|
|
1208
1216
|
|
|
1209
|
-
|
|
1217
|
+
| | adapter-uws | adapter-node | Multiplier |
|
|
1218
|
+
|---|---|---|---|
|
|
1219
|
+
| **Static files** | 135,300 req/s | 20,100 req/s | **6.7x faster** |
|
|
1220
|
+
| **SSR** | 125,100 req/s | 53,900 req/s | **2.3x faster** |
|
|
1210
1221
|
|
|
1211
|
-
|
|
1222
|
+
<sup>100 connections, 10 pipelining, 10s, 2 runs averaged. Node v24, Windows 11.</sup>
|
|
1212
1223
|
|
|
1213
|
-
|
|
1224
|
+
The static file gap is the largest because `adapter-node` uses sirv which calls `fs.createReadStream().pipe(res)` per request, while we serve from an in-memory `Map` with a single `res.cork()` + `res.end()`. The SSR gap comes from uWS's C++ HTTP parsing and batched writes vs Node's async drain event cycle.
|
|
1214
1225
|
|
|
1215
|
-
|
|
1226
|
+
### WebSocket: uWS vs socket.io vs ws
|
|
1216
1227
|
|
|
1217
|
-
|
|
1228
|
+
50 connected clients, 10 senders, burst mode, 8 seconds:
|
|
1229
|
+
|
|
1230
|
+
| Server | Messages delivered/s | vs adapter-uws |
|
|
1231
|
+
|---|---|---|
|
|
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** |
|
|
1236
|
+
|
|
1237
|
+
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
|
+
|
|
1239
|
+
### Where the overhead goes
|
|
1240
|
+
|
|
1241
|
+
**HTTP (SSR path) - 23% total overhead vs barebones uWS:**
|
|
1242
|
+
|
|
1243
|
+
| Layer | Cost | Notes |
|
|
1244
|
+
|---|---|---|
|
|
1245
|
+
| `res.cork()` + status + headers | 11.4% | Writing a proper HTTP response - unavoidable |
|
|
1246
|
+
| `new Request()` construction | 9.7% | Required by SvelteKit's `server.respond()` contract |
|
|
1247
|
+
| Response body reader loop | ~2% | `getReader()` + `read()` + async scheduling |
|
|
1248
|
+
| Header collection, AbortController | ~0% | Measured at 0.08us and 0.004us per request |
|
|
1249
|
+
|
|
1250
|
+
**WebSocket - optimized down from 27% to ~4% overhead vs barebones uWS pub/sub:**
|
|
1251
|
+
|
|
1252
|
+
The two largest WebSocket costs were `JSON.parse()` on every message for the subscribe/unsubscribe check (15%) and `JSON.stringify()` for envelope wrapping (8%). Both have been optimized:
|
|
1253
|
+
|
|
1254
|
+
| Layer | Before | After | How |
|
|
1255
|
+
|---|---|---|---|
|
|
1256
|
+
| Subscribe/unsubscribe check | ~15% | ~0% | Byte-prefix discriminator: control messages start with `{"ty` (byte[3]=`y`), user envelopes start with `{"to` (byte[3]=`o`). A single byte comparison skips `JSON.parse` for all regular messages -- from 0.39us to 0.001us per message. |
|
|
1257
|
+
| Envelope wrapping | ~8% | ~4.5% | String template with `esc()` validation instead of `JSON.stringify` on a wrapper object. Topic and event names are validated with a fast char scan (~10ns) that throws on quotes, backslashes, or control characters — only `data` is stringified. From 0.135us to ~0.085us per publish. |
|
|
1258
|
+
| Connection tracking | ~2% | ~2% | Unchanged |
|
|
1259
|
+
| Origin validation, upgrade headers | ~2% | ~2% | Unchanged |
|
|
1218
1260
|
|
|
1219
1261
|
**What we don't add:**
|
|
1220
|
-
- No middleware chain
|
|
1221
|
-
- No routing layer (uWS
|
|
1222
|
-
- No per-request
|
|
1262
|
+
- No middleware chain (no Polka, no Express)
|
|
1263
|
+
- No routing layer (uWS native routing + SvelteKit's router)
|
|
1264
|
+
- No per-request stream allocation for static files (in-memory Buffer, not `fs.createReadStream`)
|
|
1223
1265
|
- No Node.js `http.IncomingMessage` shim (we construct `Request` directly from uWS)
|
|
1224
1266
|
|
|
1225
1267
|
### The bottom line
|
|
1226
1268
|
|
|
1227
|
-
|
|
1269
|
+
The adapter retains 77% of raw uWS HTTP throughput and ~96% of raw uWS WebSocket throughput. The HTTP overhead is dominated by things SvelteKit requires (`new Request()`, proper HTTP headers). The WebSocket overhead is now almost entirely the `JSON.stringify` of your `data` payload -- the adapter's own machinery costs near zero. In a real app, your load functions and component rendering will dwarf all of this -- the adapter's job is to get out of the way, and it does.
|
|
1270
|
+
|
|
1271
|
+
To run the benchmarks yourself:
|
|
1272
|
+
|
|
1273
|
+
```bash
|
|
1274
|
+
npm install # installs uWebSockets.js, autocannon, etc.
|
|
1275
|
+
node bench/run.mjs # adapter overhead breakdown
|
|
1276
|
+
node bench/run-compare.mjs # full comparison vs adapter-node + socket.io
|
|
1277
|
+
```
|
|
1228
1278
|
|
|
1229
1279
|
---
|
|
1230
1280
|
|
|
@@ -1455,7 +1505,7 @@ const todos = on('todo'); // 'todo' - WRONG, singular vs plural
|
|
|
1455
1505
|
|
|
1456
1506
|
### "How do I see what the message envelope looks like?"
|
|
1457
1507
|
|
|
1458
|
-
Every message sent through `platform.publish()` or `platform.topic().created()` arrives as JSON with this shape:
|
|
1508
|
+
Every message sent through `platform.publish()` or `platform.topic().created()` arrives as JSON with this shape. The envelope is constructed with string concatenation for speed, but `topic` and `event` are validated first -- if either contains a quote, backslash, or control character, the call throws instead of producing malformed JSON:
|
|
1459
1509
|
|
|
1460
1510
|
```json
|
|
1461
1511
|
{
|
package/files/handler.js
CHANGED
|
@@ -24,6 +24,26 @@ class PayloadTooLargeError extends Error {
|
|
|
24
24
|
constructor() { super('Payload too large'); }
|
|
25
25
|
}
|
|
26
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Safely quote a string for JSON embedding. Topics and events are
|
|
29
|
+
* developer-defined identifiers — a quote, backslash, or control character
|
|
30
|
+
* is always a bug, so we throw instead of silently escaping.
|
|
31
|
+
* @param {string} s
|
|
32
|
+
* @returns {string} JSON-quoted string, e.g. '"todos"'
|
|
33
|
+
*/
|
|
34
|
+
function esc(s) {
|
|
35
|
+
for (let i = 0; i < s.length; i++) {
|
|
36
|
+
const c = s.charCodeAt(i);
|
|
37
|
+
if (c < 32 || c === 34 || c === 92) {
|
|
38
|
+
throw new Error(
|
|
39
|
+
`Topic/event name contains invalid character at index ${i}: '${s}'. ` +
|
|
40
|
+
'Names must not contain quotes, backslashes, or control characters.'
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
return '"' + s + '"';
|
|
45
|
+
}
|
|
46
|
+
|
|
27
47
|
// -- In-memory static file cache ---------------------------------------------
|
|
28
48
|
|
|
29
49
|
/**
|
|
@@ -174,7 +194,7 @@ const platform = {
|
|
|
174
194
|
* No-op if no clients are subscribed - safe to call unconditionally.
|
|
175
195
|
*/
|
|
176
196
|
publish(topic, event, data) {
|
|
177
|
-
const envelope =
|
|
197
|
+
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}';
|
|
178
198
|
const result = app.publish(topic, envelope, false, false);
|
|
179
199
|
// Relay to other workers via main thread (no-op in single-process mode)
|
|
180
200
|
if (parentPort) {
|
|
@@ -188,7 +208,7 @@ const platform = {
|
|
|
188
208
|
* Wraps in the same { topic, event, data } envelope as publish().
|
|
189
209
|
*/
|
|
190
210
|
send(ws, topic, event, data) {
|
|
191
|
-
return ws.send(
|
|
211
|
+
return ws.send('{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}', false, false);
|
|
192
212
|
},
|
|
193
213
|
|
|
194
214
|
/**
|
|
@@ -197,7 +217,7 @@ const platform = {
|
|
|
197
217
|
* Returns the number of connections the message was sent to.
|
|
198
218
|
*/
|
|
199
219
|
sendTo(filter, topic, event, data) {
|
|
200
|
-
const envelope =
|
|
220
|
+
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}';
|
|
201
221
|
let count = 0;
|
|
202
222
|
for (const ws of wsConnections) {
|
|
203
223
|
if (filter(ws.getUserData())) {
|
|
@@ -382,7 +402,7 @@ function serveStatic(res, entry, acceptEncoding, ifNoneMatch, headOnly = false)
|
|
|
382
402
|
* @param {string} ifNoneMatch
|
|
383
403
|
* @returns {boolean}
|
|
384
404
|
*/
|
|
385
|
-
function tryPrerendered(res, pathname, search, acceptEncoding, ifNoneMatch) {
|
|
405
|
+
function tryPrerendered(res, pathname, search, acceptEncoding, ifNoneMatch, headOnly = false) {
|
|
386
406
|
// Fast path: skip decodeURIComponent when there are no encoded characters
|
|
387
407
|
const needsDecode = pathname.includes('%');
|
|
388
408
|
let decoded;
|
|
@@ -404,7 +424,7 @@ function tryPrerendered(res, pathname, search, acceptEncoding, ifNoneMatch) {
|
|
|
404
424
|
if (prerendered.has(decoded)) {
|
|
405
425
|
const entry = staticCache.get(decoded);
|
|
406
426
|
if (entry) {
|
|
407
|
-
serveStatic(res, entry, acceptEncoding, ifNoneMatch);
|
|
427
|
+
serveStatic(res, entry, acceptEncoding, ifNoneMatch, headOnly);
|
|
408
428
|
return true;
|
|
409
429
|
}
|
|
410
430
|
}
|
|
@@ -651,7 +671,7 @@ function handleRequest(res, req) {
|
|
|
651
671
|
// Lightweight: only 2 header reads, no full collection, no remoteAddress decode
|
|
652
672
|
if (METHOD === 'GET' || METHOD === 'HEAD') {
|
|
653
673
|
if (tryPrerendered(res, pathname, query ? `?${query}` : '',
|
|
654
|
-
req.getHeader('accept-encoding'), req.getHeader('if-none-match'))) {
|
|
674
|
+
req.getHeader('accept-encoding'), req.getHeader('if-none-match'), METHOD === 'HEAD')) {
|
|
655
675
|
return;
|
|
656
676
|
}
|
|
657
677
|
}
|
|
@@ -821,8 +841,12 @@ if (WS_ENABLED) {
|
|
|
821
841
|
|
|
822
842
|
message: (ws, message, isBinary) => {
|
|
823
843
|
// Built-in: handle subscribe/unsubscribe from the client store.
|
|
824
|
-
//
|
|
825
|
-
|
|
844
|
+
// Control messages are JSON text: {"type":"subscribe","topic":"..."}
|
|
845
|
+
// Byte-prefix check: {"type" has byte[3]='y' (0x79), while user
|
|
846
|
+
// envelopes {"topic" have byte[3]='o' (0x6F). Only JSON.parse when
|
|
847
|
+
// the prefix matches - skips parsing for 99%+ of messages.
|
|
848
|
+
if (!isBinary && message.byteLength < 512 &&
|
|
849
|
+
(new Uint8Array(message))[3] === 0x79 /* 'y' in {"type" */) {
|
|
826
850
|
try {
|
|
827
851
|
const msg = JSON.parse(Buffer.from(message).toString());
|
|
828
852
|
if (msg.type === 'subscribe' && typeof msg.topic === 'string') {
|
package/files/index.js
CHANGED
|
@@ -41,6 +41,12 @@ if (is_primary) {
|
|
|
41
41
|
let shutting_down = false;
|
|
42
42
|
let listen_socket = null;
|
|
43
43
|
|
|
44
|
+
// Exponential backoff for crash-looping workers
|
|
45
|
+
let restart_delay = 0;
|
|
46
|
+
const RESTART_DELAY_MAX = 5000;
|
|
47
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
48
|
+
let backoff_reset_timer = null;
|
|
49
|
+
|
|
44
50
|
function spawn_worker() {
|
|
45
51
|
const worker = new Worker(fileURLToPath(import.meta.url));
|
|
46
52
|
workers.set(worker, null);
|
|
@@ -50,6 +56,9 @@ if (is_primary) {
|
|
|
50
56
|
workers.set(worker, msg.descriptor);
|
|
51
57
|
acceptorApp.addChildAppDescriptor(msg.descriptor);
|
|
52
58
|
console.log(`Worker thread ${worker.threadId} registered`);
|
|
59
|
+
// Worker started successfully — reset backoff
|
|
60
|
+
restart_delay = 0;
|
|
61
|
+
if (backoff_reset_timer) { clearTimeout(backoff_reset_timer); backoff_reset_timer = null; }
|
|
53
62
|
} else if (msg.type === 'publish') {
|
|
54
63
|
// Relay pub/sub to all OTHER workers
|
|
55
64
|
for (const [w] of workers) {
|
|
@@ -65,8 +74,13 @@ if (is_primary) {
|
|
|
65
74
|
}
|
|
66
75
|
workers.delete(worker);
|
|
67
76
|
if (!shutting_down) {
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
restart_delay = restart_delay ? Math.min(restart_delay * 2, RESTART_DELAY_MAX) : 100;
|
|
78
|
+
console.log(`Worker thread ${worker.threadId} exited with code ${code}, restarting in ${restart_delay}ms...`);
|
|
79
|
+
backoff_reset_timer = setTimeout(() => { spawn_worker(); }, restart_delay);
|
|
80
|
+
}
|
|
81
|
+
// If shutting down and all workers have exited, exit immediately
|
|
82
|
+
if (shutting_down && workers.size === 0) {
|
|
83
|
+
process.exit(0);
|
|
70
84
|
}
|
|
71
85
|
});
|
|
72
86
|
|
package/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ import type { WebSocket } from 'uWebSockets.js';
|
|
|
15
15
|
* | `SSL_KEY` | - | Path to TLS private key file |
|
|
16
16
|
* | `PROTOCOL_HEADER` | - | Header for protocol detection (e.g. `x-forwarded-proto`) |
|
|
17
17
|
* | `HOST_HEADER` | - | Header for host detection (e.g. `x-forwarded-host`) |
|
|
18
|
+
* | `PORT_HEADER` | - | Header for port override (e.g. `x-forwarded-port`) |
|
|
18
19
|
* | `ADDRESS_HEADER` | - | Header for client IP (e.g. `x-forwarded-for`) |
|
|
19
20
|
* | `XFF_DEPTH` | `1` | Position from right in `X-Forwarded-For` |
|
|
20
21
|
* | `BODY_SIZE_LIMIT` | `512K` | Max request body size (`K`, `M`, `G` suffixes) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-adapter-uws",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
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",
|
|
@@ -49,7 +49,6 @@
|
|
|
49
49
|
"peerDependencies": {
|
|
50
50
|
"@sveltejs/kit": "^2.0.0",
|
|
51
51
|
"svelte": "^4.0.0 || ^5.0.0",
|
|
52
|
-
"uWebSockets.js": "uNetworking/uWebSockets.js#v20.60.0",
|
|
53
52
|
"ws": "^8.0.0"
|
|
54
53
|
},
|
|
55
54
|
"peerDependenciesMeta": {
|
|
@@ -76,6 +75,14 @@
|
|
|
76
75
|
"websocket"
|
|
77
76
|
],
|
|
78
77
|
"devDependencies": {
|
|
79
|
-
"
|
|
78
|
+
"autocannon": "^8.0.0",
|
|
79
|
+
"polka": "^0.5.2",
|
|
80
|
+
"socket.io": "^4.8.3",
|
|
81
|
+
"socket.io-client": "^4.8.3",
|
|
82
|
+
"vitest": "^4.0.18",
|
|
83
|
+
"ws": "^8.19.0"
|
|
84
|
+
},
|
|
85
|
+
"optionalDependencies": {
|
|
86
|
+
"uWebSockets.js": "github:uNetworking/uWebSockets.js#v20.60.0"
|
|
80
87
|
}
|
|
81
88
|
}
|
package/vite.js
CHANGED
|
@@ -3,6 +3,25 @@ import path from 'node:path';
|
|
|
3
3
|
import { existsSync } from 'node:fs';
|
|
4
4
|
import { parseCookies } from './files/cookies.js';
|
|
5
5
|
|
|
6
|
+
/**
|
|
7
|
+
* Safely quote a string for JSON embedding. Throws on invalid characters
|
|
8
|
+
* (quotes, backslashes, control chars) — these are always bugs in topic/event names.
|
|
9
|
+
* @param {string} s
|
|
10
|
+
* @returns {string}
|
|
11
|
+
*/
|
|
12
|
+
function esc(s) {
|
|
13
|
+
for (let i = 0; i < s.length; i++) {
|
|
14
|
+
const c = s.charCodeAt(i);
|
|
15
|
+
if (c < 32 || c === 34 || c === 92) {
|
|
16
|
+
throw new Error(
|
|
17
|
+
`Topic/event name contains invalid character at index ${i}: '${s}'. ` +
|
|
18
|
+
'Names must not contain quotes, backslashes, or control characters.'
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return '"' + s + '"';
|
|
23
|
+
}
|
|
24
|
+
|
|
6
25
|
/**
|
|
7
26
|
* Vite plugin that provides WebSocket support during development.
|
|
8
27
|
*
|
|
@@ -78,7 +97,7 @@ export default function uwsDev(options = {}) {
|
|
|
78
97
|
* @returns {boolean}
|
|
79
98
|
*/
|
|
80
99
|
function publish(topic, event, data) {
|
|
81
|
-
const envelope =
|
|
100
|
+
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}';
|
|
82
101
|
let sent = false;
|
|
83
102
|
for (const [ws, topics] of subscriptions) {
|
|
84
103
|
if (topics.has(topic) && ws.readyState === 1) {
|
|
@@ -98,7 +117,7 @@ export default function uwsDev(options = {}) {
|
|
|
98
117
|
* @returns {number}
|
|
99
118
|
*/
|
|
100
119
|
function send(ws, topic, event, data) {
|
|
101
|
-
return ws.send(
|
|
120
|
+
return ws.send('{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}', false, false) ?? 1;
|
|
102
121
|
}
|
|
103
122
|
|
|
104
123
|
/**
|
|
@@ -110,7 +129,7 @@ export default function uwsDev(options = {}) {
|
|
|
110
129
|
* @returns {number}
|
|
111
130
|
*/
|
|
112
131
|
function sendTo(filter, topic, event, data) {
|
|
113
|
-
const envelope =
|
|
132
|
+
const envelope = '{"topic":' + esc(topic) + ',"event":' + esc(event) + ',"data":' + JSON.stringify(data) + '}';
|
|
114
133
|
let count = 0;
|
|
115
134
|
for (const [, wrapped] of wsWrappers) {
|
|
116
135
|
if (filter(wrapped.getUserData())) {
|
|
@@ -264,7 +283,9 @@ export default function uwsDev(options = {}) {
|
|
|
264
283
|
const arrayBuffer = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
265
284
|
|
|
266
285
|
// Handle subscribe/unsubscribe from client store
|
|
267
|
-
|
|
286
|
+
// Byte-prefix check: {"type" has byte[3]='y' (0x79), user envelopes
|
|
287
|
+
// {"topic" have byte[3]='o' - skip JSON.parse for non-control messages.
|
|
288
|
+
if (!isBinary && buf.byteLength < 512 && buf[3] === 0x79) {
|
|
268
289
|
try {
|
|
269
290
|
const msg = JSON.parse(buf.toString());
|
|
270
291
|
if (msg.type === 'subscribe' && typeof msg.topic === 'string') {
|