@gwakko/shared-websocket 0.13.0 → 0.14.5
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 +37 -0
- package/dist/SharedSocket.d.ts +2 -0
- package/dist/SharedWebSocket.d.ts +141 -5
- package/dist/SubscriptionManager.d.ts +1 -1
- package/dist/WorkerSocket.d.ts +2 -0
- package/dist/adapters/react.d.ts +3 -3
- package/dist/adapters/vue.d.ts +3 -3
- package/dist/{chunk-RKVYLJTQ.cjs → chunk-HIKH74NQ.cjs} +505 -69
- package/dist/chunk-HIKH74NQ.cjs.map +1 -0
- package/dist/{chunk-IK4HLA3K.js → chunk-N63ZMMWV.js} +496 -60
- package/dist/chunk-N63ZMMWV.js.map +1 -0
- package/dist/index.cjs +3 -3
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/react.cjs +8 -8
- package/dist/react.cjs.map +1 -1
- package/dist/react.js +7 -7
- package/dist/react.js.map +1 -1
- package/dist/types.d.ts +152 -1
- package/dist/vue.cjs +8 -8
- package/dist/vue.cjs.map +1 -1
- package/dist/vue.js +7 -7
- package/dist/vue.js.map +1 -1
- package/dist/worker/socket.worker.d.ts +2 -0
- package/package.json +1 -1
- package/src/MessageBus.ts +8 -1
- package/src/SharedSocket.ts +28 -3
- package/src/SharedWebSocket.ts +577 -63
- package/src/SubscriptionManager.ts +4 -4
- package/src/WorkerSocket.ts +27 -3
- package/src/adapters/react.ts +9 -9
- package/src/adapters/vue.ts +9 -9
- package/src/index.ts +3 -0
- package/src/types.ts +162 -1
- package/src/worker/socket.worker.ts +7 -0
- package/dist/chunk-IK4HLA3K.js.map +0 -1
- package/dist/chunk-RKVYLJTQ.cjs.map +0 -1
|
@@ -21,7 +21,9 @@ var MessageBus = class {
|
|
|
21
21
|
pendingRequests = /* @__PURE__ */ new Map();
|
|
22
22
|
subscribe(topic, fn) {
|
|
23
23
|
const wrapper = (msg) => {
|
|
24
|
-
if (msg.source !== this.tabId)
|
|
24
|
+
if (msg.type !== "publish" || msg.source !== this.tabId) {
|
|
25
|
+
fn(msg.data);
|
|
26
|
+
}
|
|
25
27
|
};
|
|
26
28
|
this.addListener(topic, wrapper);
|
|
27
29
|
return () => this.removeListener(topic, wrapper);
|
|
@@ -245,6 +247,7 @@ var SharedSocket = class {
|
|
|
245
247
|
reconnect: options.reconnect ?? true,
|
|
246
248
|
reconnectMaxDelay: options.reconnectMaxDelay ?? 3e4,
|
|
247
249
|
reconnectMaxRetries: options.reconnectMaxRetries ?? Infinity,
|
|
250
|
+
authFailureCloseCodes: new Set(options.authFailureCloseCodes ?? [1008]),
|
|
248
251
|
heartbeatInterval: options.heartbeatInterval ?? 3e4,
|
|
249
252
|
sendBuffer: options.sendBuffer ?? 100,
|
|
250
253
|
auth: options.auth,
|
|
@@ -275,7 +278,13 @@ var SharedSocket = class {
|
|
|
275
278
|
async connect() {
|
|
276
279
|
if (this.disposed) return;
|
|
277
280
|
this.setState("connecting");
|
|
278
|
-
|
|
281
|
+
let connectUrl;
|
|
282
|
+
try {
|
|
283
|
+
connectUrl = await this.buildUrl();
|
|
284
|
+
} catch {
|
|
285
|
+
this.setState("failed");
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
279
288
|
this.ws = new WebSocket(connectUrl, this.opts.protocols);
|
|
280
289
|
this.ws.onopen = () => {
|
|
281
290
|
this.reconnectAttempts = 0;
|
|
@@ -292,8 +301,12 @@ var SharedSocket = class {
|
|
|
292
301
|
}
|
|
293
302
|
for (const fn of this.onMessageFns) fn(data);
|
|
294
303
|
};
|
|
295
|
-
this.ws.onclose = () => {
|
|
304
|
+
this.ws.onclose = (ev) => {
|
|
296
305
|
this.stopHeartbeat();
|
|
306
|
+
if (this.opts.authFailureCloseCodes.has(ev.code)) {
|
|
307
|
+
this.setState("failed");
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
297
310
|
if (!this.disposed && this.opts.reconnect) {
|
|
298
311
|
this.scheduleReconnect();
|
|
299
312
|
} else {
|
|
@@ -403,6 +416,9 @@ var SharedSocket = class {
|
|
|
403
416
|
let token;
|
|
404
417
|
if (this.opts.auth) {
|
|
405
418
|
token = await this.opts.auth();
|
|
419
|
+
if (!token) {
|
|
420
|
+
throw new Error("SharedSocket: auth() returned no token");
|
|
421
|
+
}
|
|
406
422
|
} else if (this.opts.authToken) {
|
|
407
423
|
token = this.opts.authToken;
|
|
408
424
|
}
|
|
@@ -439,10 +455,23 @@ var WorkerSocket = class {
|
|
|
439
455
|
get state() {
|
|
440
456
|
return this._state;
|
|
441
457
|
}
|
|
458
|
+
setState(s) {
|
|
459
|
+
this._state = s;
|
|
460
|
+
for (const fn of this.onStateChangeFns) fn(s);
|
|
461
|
+
}
|
|
442
462
|
async connect() {
|
|
443
463
|
let authToken;
|
|
444
464
|
if (this.options.auth) {
|
|
445
|
-
|
|
465
|
+
try {
|
|
466
|
+
authToken = await this.options.auth();
|
|
467
|
+
} catch {
|
|
468
|
+
this.setState("failed");
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
if (!authToken) {
|
|
472
|
+
this.setState("failed");
|
|
473
|
+
return;
|
|
474
|
+
}
|
|
446
475
|
} else if (this.options.authToken) {
|
|
447
476
|
authToken = this.options.authToken;
|
|
448
477
|
}
|
|
@@ -475,6 +504,7 @@ var WorkerSocket = class {
|
|
|
475
504
|
reconnect: this.options.reconnect ?? true,
|
|
476
505
|
reconnectMaxDelay: this.options.reconnectMaxDelay ?? 3e4,
|
|
477
506
|
reconnectMaxRetries: this.options.reconnectMaxRetries ?? Infinity,
|
|
507
|
+
authFailureCloseCodes: this.options.authFailureCloseCodes ?? [1008],
|
|
478
508
|
heartbeatInterval: this.options.heartbeatInterval ?? 3e4,
|
|
479
509
|
bufferSize: this.options.sendBuffer ?? 100,
|
|
480
510
|
pingPayload: this.options.pingPayload
|
|
@@ -516,6 +546,7 @@ var WorkerSocket = class {
|
|
|
516
546
|
let heartbeatTimer = null, reconnectTimer = null;
|
|
517
547
|
let url = '', protocols = [], shouldReconnect = true;
|
|
518
548
|
let maxDelay = 30000, maxRetries = Infinity, hbInterval = 30000, maxBuf = 100;
|
|
549
|
+
let authFailCodes = new Set([1008]);
|
|
519
550
|
let delay = 1000, attempts = 0, pingPayload = '{"type":"ping"}';
|
|
520
551
|
|
|
521
552
|
function setState(s) { state = s; self.postMessage({ type: 'state', state: s }); }
|
|
@@ -525,7 +556,12 @@ var WorkerSocket = class {
|
|
|
525
556
|
ws = new WebSocket(url, protocols);
|
|
526
557
|
ws.onopen = () => { attempts = 0; delay = 1000; setState('connected'); self.postMessage({ type: 'open' }); flush(); startHB(); };
|
|
527
558
|
ws.onmessage = (e) => { let d; try { d = JSON.parse(e.data); } catch { d = e.data; } self.postMessage({ type: 'message', data: d }); };
|
|
528
|
-
ws.onclose = (e) => {
|
|
559
|
+
ws.onclose = (e) => {
|
|
560
|
+
stopHB();
|
|
561
|
+
self.postMessage({ type: 'close', code: e.code, reason: e.reason });
|
|
562
|
+
if (authFailCodes.has(e.code)) { setState('failed'); return; }
|
|
563
|
+
if (!disposed && shouldReconnect && e.code !== 1000) reconnect(); else setState('closed');
|
|
564
|
+
};
|
|
529
565
|
ws.onerror = () => { self.postMessage({ type: 'error', message: 'error' }); };
|
|
530
566
|
}
|
|
531
567
|
function send(d) { if (state === 'connected' && ws?.readyState === 1) ws.send(JSON.stringify(d)); else if (buffer.length < maxBuf) buffer.push(d); }
|
|
@@ -549,7 +585,7 @@ var WorkerSocket = class {
|
|
|
549
585
|
}
|
|
550
586
|
self.onmessage = (e) => {
|
|
551
587
|
const c = e.data;
|
|
552
|
-
if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; maxRetries = c.reconnectMaxRetries ?? Infinity; hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; if (c.pingPayload) pingPayload = JSON.stringify(c.pingPayload); connect(); }
|
|
588
|
+
if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; maxRetries = c.reconnectMaxRetries ?? Infinity; if (c.authFailureCloseCodes) authFailCodes = new Set(c.authFailureCloseCodes); hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; if (c.pingPayload) pingPayload = JSON.stringify(c.pingPayload); connect(); }
|
|
553
589
|
if (c.type === 'send') send(c.data);
|
|
554
590
|
if (c.type === 'reconnect') manualReconnect();
|
|
555
591
|
if (c.type === 'disconnect') { disposed = true; stopHB(); if (reconnectTimer) clearTimeout(reconnectTimer); if (ws) { ws.onclose = null; if (ws.readyState < 2) ws.close(1000); ws = null; } buffer = []; setState('closed'); }
|
|
@@ -579,9 +615,9 @@ var SubscriptionManager = class {
|
|
|
579
615
|
return () => set.delete(handler);
|
|
580
616
|
}
|
|
581
617
|
once(event, handler) {
|
|
582
|
-
const wrapper = (data) => {
|
|
618
|
+
const wrapper = (data, raw) => {
|
|
583
619
|
unsub();
|
|
584
|
-
handler(data);
|
|
620
|
+
handler(data, raw);
|
|
585
621
|
};
|
|
586
622
|
const unsub = this.on(event, wrapper);
|
|
587
623
|
return unsub;
|
|
@@ -593,11 +629,11 @@ var SubscriptionManager = class {
|
|
|
593
629
|
this.handlers.delete(event);
|
|
594
630
|
}
|
|
595
631
|
}
|
|
596
|
-
emit(event, data) {
|
|
632
|
+
emit(event, data, raw) {
|
|
597
633
|
this.lastMessages.set(event, data);
|
|
598
634
|
const set = this.handlers.get(event);
|
|
599
635
|
if (set) {
|
|
600
|
-
for (const fn of set) fn(data);
|
|
636
|
+
for (const fn of set) fn(data, raw);
|
|
601
637
|
}
|
|
602
638
|
}
|
|
603
639
|
getLastMessage(event) {
|
|
@@ -665,6 +701,7 @@ var NOOP_LOGGER = {
|
|
|
665
701
|
error() {
|
|
666
702
|
}
|
|
667
703
|
};
|
|
704
|
+
var CHANNEL_KEY_SEP = "";
|
|
668
705
|
var SharedWebSocket = class {
|
|
669
706
|
constructor(url, options = {}) {
|
|
670
707
|
this.url = url;
|
|
@@ -681,16 +718,45 @@ var SharedWebSocket = class {
|
|
|
681
718
|
});
|
|
682
719
|
this.cleanups.push(
|
|
683
720
|
this.bus.subscribe("ws:message", (msg) => {
|
|
684
|
-
this.subs.emit(msg.event, msg.data);
|
|
721
|
+
this.subs.emit(msg.event, msg.data, msg.raw);
|
|
722
|
+
for (const channelName of this.channelRefs.keys()) {
|
|
723
|
+
const prefix = channelName + ":";
|
|
724
|
+
if (msg.event.length > prefix.length && msg.event.startsWith(prefix)) {
|
|
725
|
+
const subEvent = msg.event.slice(prefix.length);
|
|
726
|
+
this.subs.emit(`${channelName}${CHANNEL_KEY_SEP}${subEvent}`, msg.data, msg.raw);
|
|
727
|
+
}
|
|
728
|
+
}
|
|
729
|
+
if (this.rawFrameListeners.size > 0) {
|
|
730
|
+
for (const fn of this.rawFrameListeners) {
|
|
731
|
+
try {
|
|
732
|
+
fn(msg.raw);
|
|
733
|
+
} catch {
|
|
734
|
+
}
|
|
735
|
+
}
|
|
736
|
+
}
|
|
685
737
|
})
|
|
686
738
|
);
|
|
687
739
|
this.cleanups.push(
|
|
688
|
-
this.bus.subscribe("ws:
|
|
740
|
+
this.bus.subscribe("ws:dispatch", (msg) => {
|
|
689
741
|
if (this.coordinator.isLeader && this.socket) {
|
|
690
|
-
this.
|
|
742
|
+
this.transmit(msg.kind, msg.payload);
|
|
743
|
+
if (msg.id) this.bus.publish("ws:dispatch-flushed", { id: msg.id });
|
|
691
744
|
}
|
|
692
745
|
})
|
|
693
746
|
);
|
|
747
|
+
this.cleanups.push(
|
|
748
|
+
this.bus.subscribe("ws:dispatch-flushed", (msg) => {
|
|
749
|
+
this.pendingOutbound.delete(msg.id);
|
|
750
|
+
})
|
|
751
|
+
);
|
|
752
|
+
this.cleanups.push(
|
|
753
|
+
this.bus.subscribe("ws:gather-pending", (req) => {
|
|
754
|
+
if (this.pendingOutbound.size === 0) return;
|
|
755
|
+
this.bus.publish(`ws:pending:${req.replyId}`, {
|
|
756
|
+
entries: [...this.pendingOutbound.values()]
|
|
757
|
+
});
|
|
758
|
+
})
|
|
759
|
+
);
|
|
694
760
|
this.cleanups.push(
|
|
695
761
|
this.bus.subscribe("ws:reconnect", () => {
|
|
696
762
|
if (this.coordinator.isLeader && this.socket) {
|
|
@@ -699,6 +765,22 @@ var SharedWebSocket = class {
|
|
|
699
765
|
}
|
|
700
766
|
})
|
|
701
767
|
);
|
|
768
|
+
this.cleanups.push(
|
|
769
|
+
this.bus.subscribe("ws:authenticate-resume", () => {
|
|
770
|
+
if (this.coordinator.isLeader && this.socket?.state === "failed") {
|
|
771
|
+
this.log.info("[SharedWS] resume requested after auth \u2014 reconnecting failed socket");
|
|
772
|
+
this.socket.reconnect();
|
|
773
|
+
}
|
|
774
|
+
})
|
|
775
|
+
);
|
|
776
|
+
this.cleanups.push(
|
|
777
|
+
this.bus.subscribe("ws:gather-subs", (req) => {
|
|
778
|
+
this.bus.publish(`ws:subs:${req.replyId}`, {
|
|
779
|
+
channels: [...this.channelRefs.keys()],
|
|
780
|
+
topics: [...this.topics]
|
|
781
|
+
});
|
|
782
|
+
})
|
|
783
|
+
);
|
|
702
784
|
this.cleanups.push(
|
|
703
785
|
this.bus.subscribe("ws:sync", (msg) => {
|
|
704
786
|
this.syncStore.set(msg.key, msg.value);
|
|
@@ -794,6 +876,27 @@ var SharedWebSocket = class {
|
|
|
794
876
|
_isAuthenticated = false;
|
|
795
877
|
authChannels = /* @__PURE__ */ new Map();
|
|
796
878
|
authTopics = /* @__PURE__ */ new Set();
|
|
879
|
+
/**
|
|
880
|
+
* Refcount of active channel subscriptions per name. Used to route
|
|
881
|
+
* incoming events back to channel handlers via `${name}<RS>${event}`
|
|
882
|
+
* keys without colliding when names/events contain `:`, and as the
|
|
883
|
+
* source for cross-tab subscription replay on leader change.
|
|
884
|
+
*/
|
|
885
|
+
channelRefs = /* @__PURE__ */ new Map();
|
|
886
|
+
/** All topic subscriptions (auth and non-auth). Replayed on leader change. */
|
|
887
|
+
topics = /* @__PURE__ */ new Set();
|
|
888
|
+
/** Listeners for every raw incoming frame (post-deserialize, post-middleware). */
|
|
889
|
+
rawFrameListeners = /* @__PURE__ */ new Set();
|
|
890
|
+
/**
|
|
891
|
+
* Local outbound buffer of follower-originated dispatches awaiting flush
|
|
892
|
+
* confirmation from the leader. Drained when the leader broadcasts
|
|
893
|
+
* `ws:dispatch-flushed` for the entry's id; replayed by the next leader
|
|
894
|
+
* after gathering across surviving tabs. Insertion order preserved
|
|
895
|
+
* (Map) so we drop oldest on overflow.
|
|
896
|
+
*/
|
|
897
|
+
pendingOutbound = /* @__PURE__ */ new Map();
|
|
898
|
+
/** Periodic refresh timer — leader only. Recreated on each leader handover. */
|
|
899
|
+
refreshTimer = null;
|
|
797
900
|
get connected() {
|
|
798
901
|
return this.socket?.state === "connected" || !this.coordinator.isLeader;
|
|
799
902
|
}
|
|
@@ -897,9 +1000,16 @@ var SharedWebSocket = class {
|
|
|
897
1000
|
this._isAuthenticated = true;
|
|
898
1001
|
this.syncStore.set("$auth:token", token);
|
|
899
1002
|
this.bus.broadcast("ws:sync", { key: "$auth:token", value: token });
|
|
900
|
-
this.send(this.proto.authLogin, { token });
|
|
901
1003
|
this.bus.broadcast("ws:lifecycle", { type: "auth", authenticated: true });
|
|
902
1004
|
this.log.info("[SharedWS] authenticated");
|
|
1005
|
+
if (this.coordinator.isLeader && this.socket && this.socket.state === "failed") {
|
|
1006
|
+
this.reconnect();
|
|
1007
|
+
return;
|
|
1008
|
+
}
|
|
1009
|
+
if (!this.coordinator.isLeader) {
|
|
1010
|
+
this.bus.publish("ws:authenticate-resume", void 0);
|
|
1011
|
+
}
|
|
1012
|
+
this.dispatch("auth-login", { data: token });
|
|
903
1013
|
}
|
|
904
1014
|
/**
|
|
905
1015
|
* Deauthenticate — notifies server, auto-leaves all auth-required channels
|
|
@@ -914,7 +1024,7 @@ var SharedWebSocket = class {
|
|
|
914
1024
|
for (const topic of this.authTopics) this.unsubscribe(topic);
|
|
915
1025
|
this.authTopics.clear();
|
|
916
1026
|
this._isAuthenticated = false;
|
|
917
|
-
this.
|
|
1027
|
+
this.dispatch("auth-logout", {});
|
|
918
1028
|
this.syncStore.delete("$auth:token");
|
|
919
1029
|
this.bus.broadcast("ws:sync", { key: "$auth:token", value: void 0 });
|
|
920
1030
|
this.bus.broadcast("ws:lifecycle", { type: "auth", authenticated: false });
|
|
@@ -1002,22 +1112,23 @@ var SharedWebSocket = class {
|
|
|
1002
1112
|
stream(event, signal) {
|
|
1003
1113
|
return this.subs.stream(event, signal);
|
|
1004
1114
|
}
|
|
1005
|
-
send(event, data) {
|
|
1115
|
+
send(event, data, extras) {
|
|
1116
|
+
this.assertExtrasReserved(extras);
|
|
1006
1117
|
const eventSerializer = this.serializers.get(event);
|
|
1007
1118
|
const serializedData = eventSerializer ? eventSerializer(data) : data;
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1119
|
+
this.dispatch("event", { event, data: serializedData, extras });
|
|
1120
|
+
}
|
|
1121
|
+
assertExtrasReserved(extras) {
|
|
1122
|
+
if (!extras) return;
|
|
1123
|
+
if (this.proto.eventField in extras) {
|
|
1124
|
+
throw new Error(
|
|
1125
|
+
`SharedWebSocket.send: extras cannot contain reserved key "${this.proto.eventField}" (eventField). Pass the event name as the first argument instead.`
|
|
1126
|
+
);
|
|
1015
1127
|
}
|
|
1016
|
-
this.
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
this.bus.publish("ws:send", { event, data });
|
|
1128
|
+
if (this.proto.dataField in extras) {
|
|
1129
|
+
throw new Error(
|
|
1130
|
+
`SharedWebSocket.send: extras cannot contain reserved key "${this.proto.dataField}" (dataField). Pass the payload as the second argument instead.`
|
|
1131
|
+
);
|
|
1021
1132
|
}
|
|
1022
1133
|
}
|
|
1023
1134
|
/** Request/response through server via leader. */
|
|
@@ -1051,33 +1162,76 @@ var SharedWebSocket = class {
|
|
|
1051
1162
|
* notifications.on('alert', (alert) => showToast(alert));
|
|
1052
1163
|
*/
|
|
1053
1164
|
channel(name, options) {
|
|
1054
|
-
this.
|
|
1165
|
+
const matcher = this.proto.channelAckMatcher;
|
|
1166
|
+
const ackTimeout = this.proto.channelAckTimeout ?? 5e3;
|
|
1167
|
+
let cancelReady;
|
|
1168
|
+
const ready = matcher ? new Promise((resolve, reject) => {
|
|
1169
|
+
let settled = false;
|
|
1170
|
+
const settle = (fn) => {
|
|
1171
|
+
if (settled) return;
|
|
1172
|
+
settled = true;
|
|
1173
|
+
clearTimeout(timer);
|
|
1174
|
+
unsubAck();
|
|
1175
|
+
fn();
|
|
1176
|
+
};
|
|
1177
|
+
const unsubAck = this.onRawFrame((frame) => {
|
|
1178
|
+
let result;
|
|
1179
|
+
try {
|
|
1180
|
+
result = matcher(frame, name);
|
|
1181
|
+
} catch {
|
|
1182
|
+
result = "reject";
|
|
1183
|
+
}
|
|
1184
|
+
if (result === "ok") settle(() => resolve());
|
|
1185
|
+
else if (result === "reject") settle(() => reject(new Error(`SharedWebSocket: subscribe rejected for channel "${name}"`)));
|
|
1186
|
+
});
|
|
1187
|
+
const timer = setTimeout(
|
|
1188
|
+
() => settle(() => reject(new Error(`SharedWebSocket: subscribe ack timeout for channel "${name}"`))),
|
|
1189
|
+
ackTimeout
|
|
1190
|
+
);
|
|
1191
|
+
cancelReady = (err) => settle(() => reject(err));
|
|
1192
|
+
}) : Promise.resolve();
|
|
1193
|
+
if (matcher) ready.catch(() => {
|
|
1194
|
+
});
|
|
1195
|
+
this.dispatch("subscribe", { channel: name });
|
|
1196
|
+
this.channelRefs.set(name, (this.channelRefs.get(name) ?? 0) + 1);
|
|
1055
1197
|
const self = this;
|
|
1056
1198
|
const unsubs = [];
|
|
1057
1199
|
const isAuth = options?.auth ?? false;
|
|
1200
|
+
let left = false;
|
|
1201
|
+
const key = (event) => `${name}${CHANNEL_KEY_SEP}${event}`;
|
|
1058
1202
|
const ch = {
|
|
1059
1203
|
name,
|
|
1204
|
+
ready,
|
|
1060
1205
|
on(event, handler) {
|
|
1061
|
-
const unsub = self.subs.on(
|
|
1206
|
+
const unsub = self.subs.on(key(event), handler);
|
|
1062
1207
|
unsubs.push(unsub);
|
|
1063
1208
|
return unsub;
|
|
1064
1209
|
},
|
|
1065
1210
|
once(event, handler) {
|
|
1066
|
-
const unsub = self.subs.once(
|
|
1211
|
+
const unsub = self.subs.once(key(event), handler);
|
|
1067
1212
|
unsubs.push(unsub);
|
|
1068
1213
|
return unsub;
|
|
1069
1214
|
},
|
|
1070
1215
|
send(event, data) {
|
|
1071
|
-
|
|
1216
|
+
const joined = `${name}:${event}`;
|
|
1217
|
+
const eventSerializer = self.serializers.get(joined) ?? self.serializers.get(event);
|
|
1218
|
+
const serializedData = eventSerializer ? eventSerializer(data) : data;
|
|
1219
|
+
self.dispatch("event", { event, data: serializedData, channel: name });
|
|
1072
1220
|
},
|
|
1073
1221
|
stream(event, signal) {
|
|
1074
|
-
return self.subs.stream(
|
|
1222
|
+
return self.subs.stream(key(event), signal);
|
|
1075
1223
|
},
|
|
1076
1224
|
leave() {
|
|
1077
|
-
|
|
1225
|
+
if (left) return;
|
|
1226
|
+
left = true;
|
|
1227
|
+
cancelReady?.(new Error(`SharedWebSocket: channel "${name}" left before ack`));
|
|
1228
|
+
self.dispatch("unsubscribe", { channel: name });
|
|
1078
1229
|
for (const unsub of unsubs) unsub();
|
|
1079
1230
|
unsubs.length = 0;
|
|
1080
1231
|
if (isAuth) self.authChannels.delete(name);
|
|
1232
|
+
const next = (self.channelRefs.get(name) ?? 1) - 1;
|
|
1233
|
+
if (next <= 0) self.channelRefs.delete(name);
|
|
1234
|
+
else self.channelRefs.set(name, next);
|
|
1081
1235
|
}
|
|
1082
1236
|
};
|
|
1083
1237
|
if (isAuth) {
|
|
@@ -1096,7 +1250,8 @@ var SharedWebSocket = class {
|
|
|
1096
1250
|
* ws.subscribe(`user:${userId}:mentions`);
|
|
1097
1251
|
*/
|
|
1098
1252
|
subscribe(topic, options) {
|
|
1099
|
-
this.
|
|
1253
|
+
this.dispatch("topic-subscribe", { topic });
|
|
1254
|
+
this.topics.add(topic);
|
|
1100
1255
|
if (options?.auth) {
|
|
1101
1256
|
this.authTopics.add(topic);
|
|
1102
1257
|
}
|
|
@@ -1107,7 +1262,8 @@ var SharedWebSocket = class {
|
|
|
1107
1262
|
* Sends topicUnsubscribe event (default: "$topic:unsubscribe").
|
|
1108
1263
|
*/
|
|
1109
1264
|
unsubscribe(topic) {
|
|
1110
|
-
this.
|
|
1265
|
+
this.dispatch("topic-unsubscribe", { topic });
|
|
1266
|
+
this.topics.delete(topic);
|
|
1111
1267
|
this.authTopics.delete(topic);
|
|
1112
1268
|
this.log.debug("[SharedWS] unsubscribe topic", topic);
|
|
1113
1269
|
}
|
|
@@ -1188,12 +1344,154 @@ var SharedWebSocket = class {
|
|
|
1188
1344
|
disconnect() {
|
|
1189
1345
|
this[Symbol.dispose]();
|
|
1190
1346
|
}
|
|
1347
|
+
// ─── Frame Pipeline ─────────────────────────────────
|
|
1348
|
+
//
|
|
1349
|
+
// dispatch(kind, payload) is the single entry point for all outgoing
|
|
1350
|
+
// frames (events, channel join/leave, topic sub/unsub, auth login/logout).
|
|
1351
|
+
// - On the leader, it calls transmit() which builds the frame, runs
|
|
1352
|
+
// outgoing middleware, and writes to the socket.
|
|
1353
|
+
// - On followers, it forwards { kind, payload } over BroadcastChannel;
|
|
1354
|
+
// the leader's bus subscriber re-enters transmit() so middleware
|
|
1355
|
+
// runs in exactly one place regardless of which tab originated.
|
|
1356
|
+
//
|
|
1357
|
+
// The actual wire shape is decided by frameBuilder (custom) or
|
|
1358
|
+
// defaultFrameBuilder (legacy two-key { event, data } envelope).
|
|
1359
|
+
/**
|
|
1360
|
+
* Build the wire frame for a given kind. Honors custom `frameBuilder`.
|
|
1361
|
+
* Return-value contract:
|
|
1362
|
+
* - any concrete value → use as the frame
|
|
1363
|
+
* - `null` → drop the frame (intentional filter)
|
|
1364
|
+
* - `undefined` → fall back to the default builder for this kind
|
|
1365
|
+
*/
|
|
1366
|
+
buildFrame(kind, payload) {
|
|
1367
|
+
if (this.proto.frameBuilder) {
|
|
1368
|
+
const result = this.proto.frameBuilder(kind, payload);
|
|
1369
|
+
if (result !== void 0) return result;
|
|
1370
|
+
}
|
|
1371
|
+
return this.defaultFrameBuilder(kind, payload);
|
|
1372
|
+
}
|
|
1373
|
+
/**
|
|
1374
|
+
* Subscribe to every raw incoming frame (post-deserialize). Used by
|
|
1375
|
+
* `Channel.ready`'s ack matcher. Internal — not part of the public API.
|
|
1376
|
+
*/
|
|
1377
|
+
onRawFrame(fn) {
|
|
1378
|
+
this.rawFrameListeners.add(fn);
|
|
1379
|
+
return () => {
|
|
1380
|
+
this.rawFrameListeners.delete(fn);
|
|
1381
|
+
};
|
|
1382
|
+
}
|
|
1383
|
+
/** Legacy two-key builder — preserved as the default for back-compat. */
|
|
1384
|
+
defaultFrameBuilder(kind, p) {
|
|
1385
|
+
let eventName;
|
|
1386
|
+
let dataPart;
|
|
1387
|
+
switch (kind) {
|
|
1388
|
+
case "event":
|
|
1389
|
+
eventName = p.channel ? `${p.channel}:${p.event ?? ""}` : p.event ?? this.proto.defaultEvent;
|
|
1390
|
+
dataPart = p.data;
|
|
1391
|
+
break;
|
|
1392
|
+
case "subscribe":
|
|
1393
|
+
eventName = this.proto.channelJoin;
|
|
1394
|
+
dataPart = { channel: p.channel };
|
|
1395
|
+
break;
|
|
1396
|
+
case "unsubscribe":
|
|
1397
|
+
eventName = this.proto.channelLeave;
|
|
1398
|
+
dataPart = { channel: p.channel };
|
|
1399
|
+
break;
|
|
1400
|
+
case "topic-subscribe":
|
|
1401
|
+
eventName = this.proto.topicSubscribe;
|
|
1402
|
+
dataPart = { topic: p.topic };
|
|
1403
|
+
break;
|
|
1404
|
+
case "topic-unsubscribe":
|
|
1405
|
+
eventName = this.proto.topicUnsubscribe;
|
|
1406
|
+
dataPart = { topic: p.topic };
|
|
1407
|
+
break;
|
|
1408
|
+
case "auth-login":
|
|
1409
|
+
eventName = this.proto.authLogin;
|
|
1410
|
+
dataPart = { token: p.data };
|
|
1411
|
+
break;
|
|
1412
|
+
case "auth-logout":
|
|
1413
|
+
eventName = this.proto.authLogout;
|
|
1414
|
+
dataPart = {};
|
|
1415
|
+
break;
|
|
1416
|
+
}
|
|
1417
|
+
return {
|
|
1418
|
+
...p.extras ?? {},
|
|
1419
|
+
[this.proto.eventField]: eventName,
|
|
1420
|
+
[this.proto.dataField]: dataPart
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
/** Route a structured frame: leader transmits, followers forward via bus. */
|
|
1424
|
+
dispatch(kind, payload) {
|
|
1425
|
+
if (this.coordinator.isLeader && this.socket) {
|
|
1426
|
+
this.transmit(kind, payload);
|
|
1427
|
+
return;
|
|
1428
|
+
}
|
|
1429
|
+
const id = generateId();
|
|
1430
|
+
this.enqueuePending(id, kind, payload);
|
|
1431
|
+
this.bus.publish("ws:dispatch", { id, kind, payload });
|
|
1432
|
+
}
|
|
1433
|
+
enqueuePending(id, kind, payload) {
|
|
1434
|
+
const max = this.options.outboundBufferSize ?? 100;
|
|
1435
|
+
if (max <= 0) return;
|
|
1436
|
+
if (this.pendingOutbound.size >= max) {
|
|
1437
|
+
const oldestKey = this.pendingOutbound.keys().next().value;
|
|
1438
|
+
if (oldestKey !== void 0) this.pendingOutbound.delete(oldestKey);
|
|
1439
|
+
}
|
|
1440
|
+
this.pendingOutbound.set(id, { id, kind, payload, enqueuedAt: Date.now() });
|
|
1441
|
+
}
|
|
1442
|
+
/** Build, run middleware, and write to the socket. Leader-only. */
|
|
1443
|
+
transmit(kind, payload) {
|
|
1444
|
+
if (!this.socket) return;
|
|
1445
|
+
let frame = this.buildFrame(kind, payload);
|
|
1446
|
+
if (frame === null) {
|
|
1447
|
+
this.log.debug("[SharedWS] \u2717 frameBuilder dropped frame", kind, this.frameLabel(kind, payload));
|
|
1448
|
+
return;
|
|
1449
|
+
}
|
|
1450
|
+
for (const mw of this.outgoingMiddleware) {
|
|
1451
|
+
frame = mw(frame);
|
|
1452
|
+
if (frame === null) {
|
|
1453
|
+
this.log.debug("[SharedWS] \u2717 outgoing dropped by middleware", kind, this.frameLabel(kind, payload));
|
|
1454
|
+
return;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (kind === "auth-login") {
|
|
1458
|
+
this.log.debug("[SharedWS] \u2192 send", kind, "(token redacted)");
|
|
1459
|
+
} else {
|
|
1460
|
+
this.log.debug("[SharedWS] \u2192 send", kind, this.frameLabel(kind, payload), { payload, frame });
|
|
1461
|
+
}
|
|
1462
|
+
this.socket.send(frame);
|
|
1463
|
+
}
|
|
1464
|
+
/**
|
|
1465
|
+
* Human-readable headline for log lines — picks the most relevant field
|
|
1466
|
+
* out of the structured payload so log scanners aren't reading objects:
|
|
1467
|
+
* - event → event name
|
|
1468
|
+
* - subscribe → channel
|
|
1469
|
+
* - topic-* → topic
|
|
1470
|
+
* - auth-* → '(redacted)' / ''
|
|
1471
|
+
*/
|
|
1472
|
+
frameLabel(kind, p) {
|
|
1473
|
+
switch (kind) {
|
|
1474
|
+
case "event":
|
|
1475
|
+
return p.event ?? "?";
|
|
1476
|
+
case "subscribe":
|
|
1477
|
+
case "unsubscribe":
|
|
1478
|
+
return p.channel ?? "?";
|
|
1479
|
+
case "topic-subscribe":
|
|
1480
|
+
case "topic-unsubscribe":
|
|
1481
|
+
return p.topic ?? "?";
|
|
1482
|
+
case "auth-login":
|
|
1483
|
+
return "(redacted)";
|
|
1484
|
+
case "auth-logout":
|
|
1485
|
+
return "";
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1191
1488
|
createSocket() {
|
|
1192
1489
|
const socketOptions = {
|
|
1193
1490
|
protocols: this.options.protocols,
|
|
1194
1491
|
reconnect: this.options.reconnect,
|
|
1195
1492
|
reconnectMaxDelay: this.options.reconnectMaxDelay,
|
|
1196
1493
|
reconnectMaxRetries: this.options.reconnectMaxRetries,
|
|
1494
|
+
authFailureCloseCodes: this.options.authFailureCloseCodes,
|
|
1197
1495
|
heartbeatInterval: this.options.heartbeatInterval,
|
|
1198
1496
|
sendBuffer: this.options.sendBuffer,
|
|
1199
1497
|
pingPayload: this.proto.ping
|
|
@@ -1219,12 +1517,13 @@ var SharedWebSocket = class {
|
|
|
1219
1517
|
handleBecomeLeader() {
|
|
1220
1518
|
this.log.info("[SharedWS] \u{1F451} became leader");
|
|
1221
1519
|
this.socket = this.createSocket();
|
|
1520
|
+
this.startRefreshTimer();
|
|
1222
1521
|
this.socket.onMessage((raw) => {
|
|
1223
1522
|
let data = raw;
|
|
1224
1523
|
for (const mw of this.incomingMiddleware) {
|
|
1225
1524
|
data = mw(data);
|
|
1226
1525
|
if (data === null) {
|
|
1227
|
-
this.log.debug("[SharedWS] \u2717 incoming dropped by middleware");
|
|
1526
|
+
this.log.debug("[SharedWS] \u2717 incoming dropped by middleware", { raw });
|
|
1228
1527
|
return;
|
|
1229
1528
|
}
|
|
1230
1529
|
}
|
|
@@ -1235,15 +1534,15 @@ var SharedWebSocket = class {
|
|
|
1235
1534
|
if (eventDeserializer) {
|
|
1236
1535
|
payload = eventDeserializer(payload);
|
|
1237
1536
|
}
|
|
1238
|
-
this.log.debug("[SharedWS] \u2190 recv", event, payload);
|
|
1239
|
-
this.bus.broadcast("ws:message", { event, data: payload });
|
|
1537
|
+
this.log.debug("[SharedWS] \u2190 recv", event, { data: payload, raw: data });
|
|
1538
|
+
this.bus.broadcast("ws:message", { event, data: payload, raw: data });
|
|
1240
1539
|
});
|
|
1241
1540
|
this.socket.onStateChange((state) => {
|
|
1242
1541
|
this.log.info("[SharedWS]", state === "connected" ? "\u2713 connected" : state === "reconnecting" ? "\u{1F504} reconnecting" : state === "failed" ? "\u2717 reconnect failed" : `state: ${state}`);
|
|
1243
1542
|
switch (state) {
|
|
1244
1543
|
case "connected":
|
|
1245
1544
|
this.bus.broadcast("ws:lifecycle", { type: "connect" });
|
|
1246
|
-
this.
|
|
1545
|
+
void this.onConnected();
|
|
1247
1546
|
break;
|
|
1248
1547
|
case "closed":
|
|
1249
1548
|
this.bus.broadcast("ws:lifecycle", { type: "disconnect" });
|
|
@@ -1267,44 +1566,177 @@ var SharedWebSocket = class {
|
|
|
1267
1566
|
resolve(res?.[this.proto.dataField] ?? response);
|
|
1268
1567
|
}
|
|
1269
1568
|
});
|
|
1270
|
-
this.
|
|
1569
|
+
this.transmit("event", { event: req.event, data: req.data });
|
|
1271
1570
|
});
|
|
1272
1571
|
})
|
|
1273
1572
|
);
|
|
1274
1573
|
void this.socket.connect();
|
|
1275
1574
|
}
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
|
|
1575
|
+
/**
|
|
1576
|
+
* Re-establish all server-side state on the freshly connected leader socket:
|
|
1577
|
+
* 1. auth-login (so server accepts subsequent joins on auth channels)
|
|
1578
|
+
* 2. channel-join for the union of channels held by ALL surviving tabs
|
|
1579
|
+
* 3. topic-subscribe for the union of topics held by ALL surviving tabs
|
|
1580
|
+
*
|
|
1581
|
+
* The union covers leader handover: when a follower with handlers is
|
|
1582
|
+
* promoted, no tab's subscriptions get silently dropped. Frames are sent
|
|
1583
|
+
* in FIFO order over the single WebSocket, so auth precedes the joins
|
|
1584
|
+
* that depend on it.
|
|
1585
|
+
*/
|
|
1586
|
+
/**
|
|
1587
|
+
* Orchestrate post-connect recovery: replay subscriptions first (so the
|
|
1588
|
+
* server is ready to route events for any channels we still care about),
|
|
1589
|
+
* then drain follower-pending dispatches that didn't reach the previous
|
|
1590
|
+
* leader's socket.
|
|
1591
|
+
*/
|
|
1592
|
+
async onConnected() {
|
|
1593
|
+
await this.resubscribeOnConnect();
|
|
1594
|
+
await this.replayPendingDispatches();
|
|
1595
|
+
}
|
|
1596
|
+
async resubscribeOnConnect() {
|
|
1597
|
+
if (!this.socket) return;
|
|
1598
|
+
const socket = this.socket;
|
|
1599
|
+
if (this._isAuthenticated) {
|
|
1600
|
+
const token = this.syncStore.get("$auth:token");
|
|
1601
|
+
if (token) {
|
|
1602
|
+
this.transmit("auth-login", { data: token });
|
|
1603
|
+
this.log.debug("[SharedWS] re-authenticated after reconnect");
|
|
1604
|
+
}
|
|
1285
1605
|
}
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
});
|
|
1606
|
+
const { channels, topics } = await this.gatherSubscriptions();
|
|
1607
|
+
if (this.socket !== socket) return;
|
|
1608
|
+
for (const name of channels) {
|
|
1609
|
+
this.transmit("subscribe", { channel: name });
|
|
1291
1610
|
}
|
|
1292
|
-
for (const topic of
|
|
1293
|
-
this.
|
|
1294
|
-
|
|
1295
|
-
|
|
1611
|
+
for (const topic of topics) {
|
|
1612
|
+
this.transmit("topic-subscribe", { topic });
|
|
1613
|
+
}
|
|
1614
|
+
if (channels.length || topics.length) {
|
|
1615
|
+
this.log.info("[SharedWS] replayed subscriptions", {
|
|
1616
|
+
channels: channels.length,
|
|
1617
|
+
topics: topics.length
|
|
1296
1618
|
});
|
|
1297
1619
|
}
|
|
1298
1620
|
}
|
|
1621
|
+
/**
|
|
1622
|
+
* Replay buffered follower dispatches over the freshly connected socket.
|
|
1623
|
+
* Gathers from all tabs (including this one), de-dups by id, transmits,
|
|
1624
|
+
* then signals each originator to drop its local entry. Drops own-tab
|
|
1625
|
+
* entries after transmission since `bus.publish` doesn't echo to self.
|
|
1626
|
+
*/
|
|
1627
|
+
async replayPendingDispatches() {
|
|
1628
|
+
if (!this.socket) return;
|
|
1629
|
+
const socket = this.socket;
|
|
1630
|
+
const entries = await this.gatherPendingDispatches();
|
|
1631
|
+
if (this.socket !== socket) return;
|
|
1632
|
+
if (entries.length === 0) return;
|
|
1633
|
+
let sent = 0;
|
|
1634
|
+
for (const e of entries) {
|
|
1635
|
+
this.transmit(e.kind, e.payload);
|
|
1636
|
+
this.pendingOutbound.delete(e.id);
|
|
1637
|
+
this.bus.publish("ws:dispatch-flushed", { id: e.id });
|
|
1638
|
+
sent++;
|
|
1639
|
+
}
|
|
1640
|
+
this.log.info("[SharedWS] replayed pending dispatches", { count: sent });
|
|
1641
|
+
}
|
|
1642
|
+
/**
|
|
1643
|
+
* Cross-tab pending-dispatch gather. Same shape as `gatherSubscriptions`
|
|
1644
|
+
* — broadcasts a one-shot request, collects for a short window, dedups
|
|
1645
|
+
* by id (so multiple tabs holding the same id don't double-replay).
|
|
1646
|
+
*/
|
|
1647
|
+
gatherPendingDispatches(timeoutMs = 100) {
|
|
1648
|
+
const seen = /* @__PURE__ */ new Map();
|
|
1649
|
+
for (const e of this.pendingOutbound.values()) {
|
|
1650
|
+
seen.set(e.id, { id: e.id, kind: e.kind, payload: e.payload });
|
|
1651
|
+
}
|
|
1652
|
+
const replyId = generateId();
|
|
1653
|
+
return new Promise((resolve) => {
|
|
1654
|
+
const unsub = this.bus.subscribe(
|
|
1655
|
+
`ws:pending:${replyId}`,
|
|
1656
|
+
(msg) => {
|
|
1657
|
+
for (const e of msg.entries) {
|
|
1658
|
+
if (!seen.has(e.id)) seen.set(e.id, { id: e.id, kind: e.kind, payload: e.payload });
|
|
1659
|
+
}
|
|
1660
|
+
}
|
|
1661
|
+
);
|
|
1662
|
+
this.bus.publish("ws:gather-pending", { replyId });
|
|
1663
|
+
setTimeout(() => {
|
|
1664
|
+
unsub();
|
|
1665
|
+
resolve([...seen.values()]);
|
|
1666
|
+
}, timeoutMs);
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
/**
|
|
1670
|
+
* Best-effort cross-tab gather. Broadcasts a request and collects responses
|
|
1671
|
+
* for a short window. Times out gracefully — late responses are dropped.
|
|
1672
|
+
* The leader's own subs are seeded into the result to avoid relying on
|
|
1673
|
+
* BroadcastChannel echo to self.
|
|
1674
|
+
*/
|
|
1675
|
+
gatherSubscriptions(timeoutMs = 150) {
|
|
1676
|
+
const channels = new Set(this.channelRefs.keys());
|
|
1677
|
+
const topics = new Set(this.topics);
|
|
1678
|
+
const replyId = generateId();
|
|
1679
|
+
return new Promise((resolve) => {
|
|
1680
|
+
const unsub = this.bus.subscribe(
|
|
1681
|
+
`ws:subs:${replyId}`,
|
|
1682
|
+
(msg) => {
|
|
1683
|
+
for (const c of msg.channels) channels.add(c);
|
|
1684
|
+
for (const t of msg.topics) topics.add(t);
|
|
1685
|
+
}
|
|
1686
|
+
);
|
|
1687
|
+
this.bus.publish("ws:gather-subs", { replyId });
|
|
1688
|
+
setTimeout(() => {
|
|
1689
|
+
unsub();
|
|
1690
|
+
resolve({ channels: [...channels], topics: [...topics] });
|
|
1691
|
+
}, timeoutMs);
|
|
1692
|
+
});
|
|
1693
|
+
}
|
|
1299
1694
|
handleLoseLeadership() {
|
|
1695
|
+
this.stopRefreshTimer();
|
|
1300
1696
|
if (this.socket) {
|
|
1301
1697
|
this.socket[Symbol.dispose]();
|
|
1302
1698
|
this.socket = null;
|
|
1303
1699
|
}
|
|
1304
1700
|
}
|
|
1701
|
+
/**
|
|
1702
|
+
* Start a leader-only periodic refresh of the auth token. The callback
|
|
1703
|
+
* is `options.refresh` (preferred) or `options.auth` (fallback). When
|
|
1704
|
+
* the timer fires and the connection is currently authenticated, the
|
|
1705
|
+
* returned token is fed back through `authenticate()` so subscribers
|
|
1706
|
+
* stay synced and the leader's socket re-issues auth-login.
|
|
1707
|
+
*
|
|
1708
|
+
* Idempotent — calling start while already running is a no-op.
|
|
1709
|
+
*/
|
|
1710
|
+
startRefreshTimer() {
|
|
1711
|
+
if (this.refreshTimer) return;
|
|
1712
|
+
const interval = this.options.refreshTokenInterval;
|
|
1713
|
+
const refresh = this.options.refresh ?? this.options.auth;
|
|
1714
|
+
if (!interval || interval <= 0 || !refresh) return;
|
|
1715
|
+
if (!this.coordinator.isLeader) return;
|
|
1716
|
+
this.refreshTimer = setInterval(async () => {
|
|
1717
|
+
if (!this.coordinator.isLeader || !this._isAuthenticated) return;
|
|
1718
|
+
try {
|
|
1719
|
+
const token = await refresh();
|
|
1720
|
+
if (!token) {
|
|
1721
|
+
this.log.warn("[SharedWS] refresh() returned empty token \u2014 skipping");
|
|
1722
|
+
return;
|
|
1723
|
+
}
|
|
1724
|
+
this.authenticate(token);
|
|
1725
|
+
} catch (err) {
|
|
1726
|
+
this.log.warn("[SharedWS] refresh() failed", err);
|
|
1727
|
+
}
|
|
1728
|
+
}, interval);
|
|
1729
|
+
}
|
|
1730
|
+
stopRefreshTimer() {
|
|
1731
|
+
if (this.refreshTimer) {
|
|
1732
|
+
clearInterval(this.refreshTimer);
|
|
1733
|
+
this.refreshTimer = null;
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1305
1736
|
[Symbol.dispose]() {
|
|
1306
1737
|
if (this.disposed) return;
|
|
1307
1738
|
this.disposed = true;
|
|
1739
|
+
this.stopRefreshTimer();
|
|
1308
1740
|
this.coordinator[Symbol.dispose]();
|
|
1309
1741
|
if (this.socket) {
|
|
1310
1742
|
this.socket[Symbol.dispose]();
|
|
@@ -1317,6 +1749,10 @@ var SharedWebSocket = class {
|
|
|
1317
1749
|
this.syncStore.clear();
|
|
1318
1750
|
this.authChannels.clear();
|
|
1319
1751
|
this.authTopics.clear();
|
|
1752
|
+
this.channelRefs.clear();
|
|
1753
|
+
this.topics.clear();
|
|
1754
|
+
this.rawFrameListeners.clear();
|
|
1755
|
+
this.pendingOutbound.clear();
|
|
1320
1756
|
}
|
|
1321
1757
|
};
|
|
1322
1758
|
|
|
@@ -1328,4 +1764,4 @@ export {
|
|
|
1328
1764
|
SubscriptionManager,
|
|
1329
1765
|
SharedWebSocket
|
|
1330
1766
|
};
|
|
1331
|
-
//# sourceMappingURL=chunk-
|
|
1767
|
+
//# sourceMappingURL=chunk-N63ZMMWV.js.map
|