svelte-adapter-uws-extensions 0.5.8 → 0.5.9
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/package.json +2 -2
- package/redis/presence.js +38 -13
- package/redis/pubsub.js +31 -11
- package/redis/sharded-pubsub.js +29 -13
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "svelte-adapter-uws-extensions",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.9",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"tag": "latest"
|
|
6
6
|
},
|
|
@@ -154,7 +154,7 @@
|
|
|
154
154
|
"node": ">=22.0.0"
|
|
155
155
|
},
|
|
156
156
|
"peerDependencies": {
|
|
157
|
-
"svelte-adapter-uws": "^0.5.
|
|
157
|
+
"svelte-adapter-uws": "^0.5.8"
|
|
158
158
|
},
|
|
159
159
|
"dependencies": {
|
|
160
160
|
"ioredis": "^5.0.0"
|
package/redis/presence.js
CHANGED
|
@@ -8,9 +8,10 @@
|
|
|
8
8
|
* Wire shape clients see on `__presence:{topic}`:
|
|
9
9
|
* - `state` (sent once on subscribe to a single connection)
|
|
10
10
|
* payload: `{[userKey]: data}` flat snapshot of current presence
|
|
11
|
-
* - `diff` (broadcast to topic subscribers,
|
|
11
|
+
* - `diff` (broadcast to topic subscribers, tick-batched)
|
|
12
12
|
* payload: `{joins: {[key]: data}, leaves: {[key]: data}}`
|
|
13
|
-
*
|
|
13
|
+
* Joins+leaves on the same key in one event-loop iteration
|
|
14
|
+
* collapse: latest op wins.
|
|
14
15
|
* - `heartbeat` (broadcast to topic subscribers, per heartbeat interval)
|
|
15
16
|
* payload: array of currently-known user keys
|
|
16
17
|
*
|
|
@@ -317,14 +318,29 @@ export function createPresence(client, options = {}) {
|
|
|
317
318
|
|
|
318
319
|
/**
|
|
319
320
|
* Per-topic pending diff buffer: latest op per key wins. Joins and
|
|
320
|
-
* leaves on the same key in
|
|
321
|
-
* only sees the net change. Flushed once per
|
|
322
|
-
* `
|
|
323
|
-
*
|
|
321
|
+
* leaves on the same key in one event-loop iteration collapse so the
|
|
322
|
+
* wire only sees the net change. Flushed once per iteration via
|
|
323
|
+
* `setTimeout(flushPendingDiffs, 0)` armed when the first dirty entry
|
|
324
|
+
* lands. Mirrors the buffer model the adapter's bundled presence
|
|
325
|
+
* plugin uses, so a single client decoder handles both.
|
|
326
|
+
*
|
|
327
|
+
* Why `setTimeout(0)` and not `queueMicrotask`: uWS dispatches each WS
|
|
328
|
+
* message as its own JS task, and N-API drains microtasks at the C++/JS
|
|
329
|
+
* boundary between tasks. A microtask-deferred flush fires BEFORE the
|
|
330
|
+
* next socket's handler runs, so cross-socket coalescing is impossible
|
|
331
|
+
* at the microtask level - a mass-join into a populated topic produces
|
|
332
|
+
* O(N) one-entry publishes instead of one batched diff. `setTimeout(0)`
|
|
333
|
+
* lands in libuv's timers phase, which fires only after the poll phase
|
|
334
|
+
* has dispatched every ready socket message in the current iteration -
|
|
335
|
+
* so all joins arriving together end up in one flush regardless of how
|
|
336
|
+
* many task boundaries separate them. Same structural choice the
|
|
337
|
+
* 0.5.7 cursor always-tick rewrite locked in.
|
|
338
|
+
*
|
|
324
339
|
* @type {Map<string, Map<string, { op: 'join' | 'leave', data: Record<string, any> }>>}
|
|
325
340
|
*/
|
|
326
341
|
const pendingDiffs = new Map();
|
|
327
|
-
|
|
342
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
343
|
+
let diffFlushTimer = null;
|
|
328
344
|
/** @type {import('svelte-adapter-uws').Platform | null} */
|
|
329
345
|
let diffFlushPlatform = null;
|
|
330
346
|
|
|
@@ -339,14 +355,17 @@ export function createPresence(client, options = {}) {
|
|
|
339
355
|
}
|
|
340
356
|
entries.set(key, { op, data });
|
|
341
357
|
diffFlushPlatform = platform;
|
|
342
|
-
if (
|
|
343
|
-
|
|
344
|
-
|
|
358
|
+
if (diffFlushTimer === null) {
|
|
359
|
+
diffFlushTimer = setTimeout(flushPendingDiffs, 0);
|
|
360
|
+
if (diffFlushTimer.unref) diffFlushTimer.unref();
|
|
345
361
|
}
|
|
346
362
|
}
|
|
347
363
|
|
|
348
364
|
function flushPendingDiffs() {
|
|
349
|
-
|
|
365
|
+
if (diffFlushTimer !== null) {
|
|
366
|
+
clearTimeout(diffFlushTimer);
|
|
367
|
+
diffFlushTimer = null;
|
|
368
|
+
}
|
|
350
369
|
const platform = diffFlushPlatform;
|
|
351
370
|
diffFlushPlatform = null;
|
|
352
371
|
if (!platform) {
|
|
@@ -1359,7 +1378,10 @@ export function createPresence(client, options = {}) {
|
|
|
1359
1378
|
syncObservers.clear();
|
|
1360
1379
|
syncCounts.clear();
|
|
1361
1380
|
pendingDiffs.clear();
|
|
1362
|
-
|
|
1381
|
+
if (diffFlushTimer !== null) {
|
|
1382
|
+
clearTimeout(diffFlushTimer);
|
|
1383
|
+
diffFlushTimer = null;
|
|
1384
|
+
}
|
|
1363
1385
|
diffFlushPlatform = null;
|
|
1364
1386
|
connCounter = 0;
|
|
1365
1387
|
},
|
|
@@ -1379,7 +1401,10 @@ export function createPresence(client, options = {}) {
|
|
|
1379
1401
|
keyspaceSubscribed = false;
|
|
1380
1402
|
activePlatform = null;
|
|
1381
1403
|
pendingDiffs.clear();
|
|
1382
|
-
|
|
1404
|
+
if (diffFlushTimer !== null) {
|
|
1405
|
+
clearTimeout(diffFlushTimer);
|
|
1406
|
+
diffFlushTimer = null;
|
|
1407
|
+
}
|
|
1383
1408
|
diffFlushPlatform = null;
|
|
1384
1409
|
},
|
|
1385
1410
|
|
package/redis/pubsub.js
CHANGED
|
@@ -120,13 +120,25 @@ export function createPubSubBus(client, options = {}) {
|
|
|
120
120
|
/** @type {(() => void) | null} */
|
|
121
121
|
let unsubscribeBreaker = null;
|
|
122
122
|
|
|
123
|
-
//
|
|
124
|
-
// event-loop
|
|
125
|
-
// its underlying message count so the relayed-messages counter
|
|
126
|
-
// accurate when batch envelopes carry many messages each.
|
|
123
|
+
// Tick relay batching: coalesce Redis publishes within a single
|
|
124
|
+
// event-loop iteration into one pipelined round trip. Each envelope
|
|
125
|
+
// tracks its underlying message count so the relayed-messages counter
|
|
126
|
+
// stays accurate when batch envelopes carry many messages each.
|
|
127
|
+
//
|
|
128
|
+
// Why `setTimeout(0)` and not `queueMicrotask`: uWS dispatches each WS
|
|
129
|
+
// message as its own JS task, and N-API drains microtasks at the C++/JS
|
|
130
|
+
// boundary between tasks. A microtask-deferred flush fires BEFORE the
|
|
131
|
+
// next socket's handler runs, so cross-socket coalescing is impossible
|
|
132
|
+
// at the microtask level - N publishes from N socket handlers in the
|
|
133
|
+
// same iteration produce N Redis round-trips instead of one pipelined
|
|
134
|
+
// call. `setTimeout(0)` lands in libuv's timers phase, which fires only
|
|
135
|
+
// after the poll phase has dispatched every ready socket message in the
|
|
136
|
+
// current iteration. Same structural choice the 0.5.7 cursor always-tick
|
|
137
|
+
// rewrite locked in.
|
|
127
138
|
/** @type {Array<{msg: string, count: number}>} */
|
|
128
139
|
let relayBatch = [];
|
|
129
|
-
|
|
140
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
141
|
+
let relayTimer = null;
|
|
130
142
|
let relayBatchWarnFired = false;
|
|
131
143
|
|
|
132
144
|
function scheduleRelay(msg, count) {
|
|
@@ -135,23 +147,26 @@ export function createPubSubBus(client, options = {}) {
|
|
|
135
147
|
if (relayBatch.length >= MAX_PUBSUB_RELAY_BATCH_PER_TICK && !relayBatchWarnFired) {
|
|
136
148
|
relayBatchWarnFired = true;
|
|
137
149
|
console.warn(
|
|
138
|
-
'[pubsub]
|
|
139
|
-
' entries in one
|
|
150
|
+
'[pubsub] tick relay batch reached ' + relayBatch.length +
|
|
151
|
+
' entries in one iteration. The batch is drained every tick, so a ' +
|
|
140
152
|
'caller emitted a million publishes in one synchronous burst - likely ' +
|
|
141
153
|
'a publish-in-loop without yielding.\n' +
|
|
142
154
|
' See: https://svti.me/pubsub-burst'
|
|
143
155
|
);
|
|
144
156
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
147
|
-
|
|
157
|
+
if (relayTimer === null) {
|
|
158
|
+
relayTimer = setTimeout(flushRelay, 0);
|
|
159
|
+
if (relayTimer.unref) relayTimer.unref();
|
|
148
160
|
}
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
function flushRelay() {
|
|
152
164
|
const batch = relayBatch;
|
|
153
165
|
relayBatch = [];
|
|
154
|
-
|
|
166
|
+
if (relayTimer !== null) {
|
|
167
|
+
clearTimeout(relayTimer);
|
|
168
|
+
relayTimer = null;
|
|
169
|
+
}
|
|
155
170
|
if (b) {
|
|
156
171
|
try { b.guard(); } catch { return; }
|
|
157
172
|
}
|
|
@@ -380,6 +395,11 @@ export function createPubSubBus(client, options = {}) {
|
|
|
380
395
|
unsubscribeBreaker();
|
|
381
396
|
unsubscribeBreaker = null;
|
|
382
397
|
}
|
|
398
|
+
if (relayTimer !== null) {
|
|
399
|
+
clearTimeout(relayTimer);
|
|
400
|
+
relayTimer = null;
|
|
401
|
+
}
|
|
402
|
+
relayBatch = [];
|
|
383
403
|
if (!active || !subscriber) return;
|
|
384
404
|
active = false;
|
|
385
405
|
activePlatform = null;
|
package/redis/sharded-pubsub.js
CHANGED
|
@@ -107,16 +107,26 @@ export function createShardedBus(client, options = {}) {
|
|
|
107
107
|
/** @type {WeakMap<any, Set<string>>} ws -> set of followed topics */
|
|
108
108
|
const wsFollows = new WeakMap();
|
|
109
109
|
|
|
110
|
-
// One-shot warn flag for the per-tick
|
|
110
|
+
// One-shot warn flag for the per-tick batch cap.
|
|
111
111
|
let batchChannelsWarnFired = false;
|
|
112
112
|
|
|
113
|
-
// Per-channel
|
|
114
|
-
//
|
|
115
|
-
// tracks the underlying topic list so the relayed-messages
|
|
116
|
-
// stays accurate when batch envelopes carry many messages.
|
|
113
|
+
// Per-channel tick batch: coalesce SPUBLISHes for the same channel
|
|
114
|
+
// within one event-loop iteration into a single pipelined call. Each
|
|
115
|
+
// entry tracks the underlying topic list so the relayed-messages
|
|
116
|
+
// counter stays accurate when batch envelopes carry many messages.
|
|
117
|
+
//
|
|
118
|
+
// Why `setTimeout(0)` and not `queueMicrotask`: uWS dispatches each WS
|
|
119
|
+
// message as its own JS task, and N-API drains microtasks at the C++/JS
|
|
120
|
+
// boundary between tasks. A microtask-deferred flush fires BEFORE the
|
|
121
|
+
// next socket's handler runs, so cross-socket coalescing is impossible
|
|
122
|
+
// at the microtask level. `setTimeout(0)` lands in libuv's timers
|
|
123
|
+
// phase, which fires only after the poll phase has dispatched every
|
|
124
|
+
// ready socket message in the current iteration. Same structural
|
|
125
|
+
// choice the 0.5.7 cursor always-tick rewrite locked in.
|
|
117
126
|
/** @type {Map<string, Array<{msg: string, topics: string[]}>>} */
|
|
118
127
|
let channelBatches = new Map();
|
|
119
|
-
|
|
128
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
129
|
+
let relayTimer = null;
|
|
120
130
|
|
|
121
131
|
function scheduleRelay(channel, msg, topics) {
|
|
122
132
|
let arr = channelBatches.get(channel);
|
|
@@ -128,23 +138,26 @@ export function createShardedBus(client, options = {}) {
|
|
|
128
138
|
if (channelBatches.size >= MAX_SHARDED_BUS_BATCH_CHANNELS_PER_TICK && !batchChannelsWarnFired) {
|
|
129
139
|
batchChannelsWarnFired = true;
|
|
130
140
|
console.warn(
|
|
131
|
-
'[sharded-bus]
|
|
132
|
-
' distinct channels in one
|
|
141
|
+
'[sharded-bus] tick batch reached ' + channelBatches.size +
|
|
142
|
+
' distinct channels in one iteration. The batch is drained every tick, ' +
|
|
133
143
|
'so reaching this size means a publisher emitted a million distinct ' +
|
|
134
144
|
'channels in one synchronous burst - likely a topic-cardinality leak.\n' +
|
|
135
145
|
' See: https://svti.me/sharded-bus-burst'
|
|
136
146
|
);
|
|
137
147
|
}
|
|
138
|
-
if (
|
|
139
|
-
|
|
140
|
-
|
|
148
|
+
if (relayTimer === null) {
|
|
149
|
+
relayTimer = setTimeout(flushRelay, 0);
|
|
150
|
+
if (relayTimer.unref) relayTimer.unref();
|
|
141
151
|
}
|
|
142
152
|
}
|
|
143
153
|
|
|
144
154
|
function flushRelay() {
|
|
145
155
|
const batches = channelBatches;
|
|
146
156
|
channelBatches = new Map();
|
|
147
|
-
|
|
157
|
+
if (relayTimer !== null) {
|
|
158
|
+
clearTimeout(relayTimer);
|
|
159
|
+
relayTimer = null;
|
|
160
|
+
}
|
|
148
161
|
if (b) {
|
|
149
162
|
try { b.guard(); } catch { return; }
|
|
150
163
|
}
|
|
@@ -268,7 +281,10 @@ export function createShardedBus(client, options = {}) {
|
|
|
268
281
|
subscriber = null;
|
|
269
282
|
}
|
|
270
283
|
channelBatches = new Map();
|
|
271
|
-
|
|
284
|
+
if (relayTimer !== null) {
|
|
285
|
+
clearTimeout(relayTimer);
|
|
286
|
+
relayTimer = null;
|
|
287
|
+
}
|
|
272
288
|
followCounts.clear();
|
|
273
289
|
channelRefcounts.clear();
|
|
274
290
|
}
|