svelte-adapter-uws-extensions 0.5.8 → 0.5.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/package.json +2 -2
- package/redis/cursor.js +94 -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.10",
|
|
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/cursor.js
CHANGED
|
@@ -233,6 +233,13 @@ export function createCursor(client, options = {}) {
|
|
|
233
233
|
enqueueInbound(parsed.topic, entry.key, entry.data, activePlatform);
|
|
234
234
|
}
|
|
235
235
|
}
|
|
236
|
+
} else if (parsed.event === EVENTS.REMOVE && parsed.payload && typeof parsed.payload.key === 'string') {
|
|
237
|
+
// Peer-relayed cursor removes are coalesced through the
|
|
238
|
+
// same tick buffer the local close path uses. Without
|
|
239
|
+
// this, a mass-disconnect on one instance produces an
|
|
240
|
+
// equally-sized O(N) immediate-publish storm on every
|
|
241
|
+
// other instance, propagating the OOM risk cluster-wide.
|
|
242
|
+
queueRemove(parsed.topic, parsed.payload.key, activePlatform);
|
|
236
243
|
} else {
|
|
237
244
|
activePlatform.publish(
|
|
238
245
|
'__cursor:' + parsed.topic,
|
|
@@ -443,6 +450,75 @@ export function createCursor(client, options = {}) {
|
|
|
443
450
|
let driftMax = 0;
|
|
444
451
|
let flushCount = 0;
|
|
445
452
|
|
|
453
|
+
// Pending REMOVE keys per topic, coalesced into one wire frame per
|
|
454
|
+
// subscriber per event-loop iteration. Mass-disconnect scenarios (e.g.
|
|
455
|
+
// 5K cursors closing in one tick during a stress test or browser-tab
|
|
456
|
+
// teardown of a packed board) used to fire 5K immediate publishes per
|
|
457
|
+
// topic, each fanning out to every remaining subscriber. The resulting
|
|
458
|
+
// O(N^2) uWS send queue allocations OOM-killed workers ~18s into the
|
|
459
|
+
// cleanup cascade. With this buffer, every subscriber sees one frame
|
|
460
|
+
// per topic per tick containing the full leave list, regardless of
|
|
461
|
+
// how many sockets dropped together. Symmetric to the presence
|
|
462
|
+
// `pendingDiffs` buffer.
|
|
463
|
+
//
|
|
464
|
+
// Why `setTimeout(0)` over `queueMicrotask`: uWS dispatches each WS
|
|
465
|
+
// close as its own JS task, and N-API drains microtasks at the C++/JS
|
|
466
|
+
// boundary between tasks. A microtask-deferred flush fires BEFORE the
|
|
467
|
+
// next socket's close handler runs, so cross-socket coalescing is
|
|
468
|
+
// impossible at the microtask level. `setTimeout(0)` lands in libuv's
|
|
469
|
+
// timers phase, which fires only after the poll phase has dispatched
|
|
470
|
+
// every ready socket event in the current iteration.
|
|
471
|
+
/** @type {Map<string, Set<string>>} */
|
|
472
|
+
const pendingRemoves = new Map();
|
|
473
|
+
/** @type {ReturnType<typeof setTimeout> | null} */
|
|
474
|
+
let removeFlushTimer = null;
|
|
475
|
+
/** @type {import('svelte-adapter-uws').Platform | null} */
|
|
476
|
+
let removeFlushPlatform = null;
|
|
477
|
+
|
|
478
|
+
function queueRemove(topic, key, platform) {
|
|
479
|
+
let set = pendingRemoves.get(topic);
|
|
480
|
+
if (!set) {
|
|
481
|
+
set = new Set();
|
|
482
|
+
pendingRemoves.set(topic, set);
|
|
483
|
+
}
|
|
484
|
+
set.add(key);
|
|
485
|
+
removeFlushPlatform = platform;
|
|
486
|
+
if (removeFlushTimer === null) {
|
|
487
|
+
removeFlushTimer = setTimeout(flushPendingRemoves, 0);
|
|
488
|
+
if (removeFlushTimer.unref) removeFlushTimer.unref();
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
function flushPendingRemoves() {
|
|
493
|
+
removeFlushTimer = null;
|
|
494
|
+
const platform = removeFlushPlatform;
|
|
495
|
+
removeFlushPlatform = null;
|
|
496
|
+
if (!platform) {
|
|
497
|
+
pendingRemoves.clear();
|
|
498
|
+
return;
|
|
499
|
+
}
|
|
500
|
+
for (const [topic, keys] of pendingRemoves) {
|
|
501
|
+
if (keys.size === 0) continue;
|
|
502
|
+
// publishBatched (adapter >= 0.5.0-next.5) bundles N events into
|
|
503
|
+
// one wire frame per subscriber. Each subscriber decodes the
|
|
504
|
+
// individual REMOVE events from the frame, so no client change
|
|
505
|
+
// is required. Fall back to per-event publishes when the
|
|
506
|
+
// adapter does not expose publishBatched.
|
|
507
|
+
if (typeof platform.publishBatched === 'function') {
|
|
508
|
+
const messages = [];
|
|
509
|
+
for (const key of keys) {
|
|
510
|
+
messages.push({ topic: '__cursor:' + topic, event: EVENTS.REMOVE, data: { key } });
|
|
511
|
+
}
|
|
512
|
+
try { platform.publishBatched(messages); } catch { /* platform unavailable mid-flight */ }
|
|
513
|
+
} else {
|
|
514
|
+
for (const key of keys) {
|
|
515
|
+
try { platform.publish('__cursor:' + topic, EVENTS.REMOVE, { key }); } catch { /* swallow */ }
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
pendingRemoves.clear();
|
|
520
|
+
}
|
|
521
|
+
|
|
446
522
|
function relay(topic, event, payload) {
|
|
447
523
|
if (b) { try { b.guard(); } catch { return; } }
|
|
448
524
|
const msg = JSON.stringify({ instanceId, topic, event, payload });
|
|
@@ -694,7 +770,7 @@ export function createCursor(client, options = {}) {
|
|
|
694
770
|
if (topicPending.size === 0) redisPending.delete(topic);
|
|
695
771
|
}
|
|
696
772
|
|
|
697
|
-
|
|
773
|
+
queueRemove(topic, key, platform);
|
|
698
774
|
relay(topic, EVENTS.REMOVE, { key });
|
|
699
775
|
return true;
|
|
700
776
|
}
|
|
@@ -870,7 +946,7 @@ export function createCursor(client, options = {}) {
|
|
|
870
946
|
}
|
|
871
947
|
|
|
872
948
|
for (const t of removedTopics) {
|
|
873
|
-
|
|
949
|
+
queueRemove(t, state.key, platform);
|
|
874
950
|
const topicMap = topics.get(t);
|
|
875
951
|
if (topicMap) {
|
|
876
952
|
topicMap.delete(state.key);
|
|
@@ -961,6 +1037,9 @@ export function createCursor(client, options = {}) {
|
|
|
961
1037
|
}
|
|
962
1038
|
// Tracker-level scheduler timer + dirty-topic set.
|
|
963
1039
|
if (tickTimer !== null) { clearTimeout(tickTimer); tickTimer = null; }
|
|
1040
|
+
if (removeFlushTimer !== null) { clearTimeout(removeFlushTimer); removeFlushTimer = null; }
|
|
1041
|
+
removeFlushPlatform = null;
|
|
1042
|
+
pendingRemoves.clear();
|
|
964
1043
|
dirtyTopics.clear();
|
|
965
1044
|
topics.clear();
|
|
966
1045
|
topicFlush.clear();
|
|
@@ -982,6 +1061,9 @@ export function createCursor(client, options = {}) {
|
|
|
982
1061
|
}
|
|
983
1062
|
}
|
|
984
1063
|
if (tickTimer !== null) { clearTimeout(tickTimer); tickTimer = null; }
|
|
1064
|
+
if (removeFlushTimer !== null) { clearTimeout(removeFlushTimer); removeFlushTimer = null; }
|
|
1065
|
+
removeFlushPlatform = null;
|
|
1066
|
+
pendingRemoves.clear();
|
|
985
1067
|
dirtyTopics.clear();
|
|
986
1068
|
topicFlush.clear();
|
|
987
1069
|
if (subscriber) {
|
|
@@ -1048,6 +1130,16 @@ export function createCursor(client, options = {}) {
|
|
|
1048
1130
|
tracker.snapshot(ws, data.topic, platform);
|
|
1049
1131
|
return;
|
|
1050
1132
|
}
|
|
1133
|
+
// Silent no-op when the caller dispatches a parsed object whose
|
|
1134
|
+
// `type` is not ours. The dispatch-to-all pattern (e.g. forwarding
|
|
1135
|
+
// every unhandled frame to cursor.hooks.message AND
|
|
1136
|
+
// presence.hooks.message and letting each ignore frames not
|
|
1137
|
+
// addressed to it) is legitimate. Only warn on shapes that
|
|
1138
|
+
// indicate the wrong wiring (raw bytes, non-object) where the
|
|
1139
|
+
// developer almost certainly intended a parsed envelope.
|
|
1140
|
+
if (data && typeof data === 'object' && !Array.isArray(data) && typeof data.type === 'string') {
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1051
1143
|
_warnCursorHooksMessageShape(data);
|
|
1052
1144
|
},
|
|
1053
1145
|
close(ws, { platform }) {
|
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
|
}
|