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