@gwakko/shared-websocket 0.2.1 → 0.6.1

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.
@@ -7,6 +7,10 @@ interface SharedSocketOptions {
7
7
  heartbeatInterval?: number;
8
8
  sendBuffer?: number;
9
9
  auth?: () => string | Promise<string>;
10
+ authToken?: string;
11
+ authParam?: string;
12
+ /** Heartbeat payload (default: { type: "ping" }). */
13
+ pingPayload?: unknown;
10
14
  }
11
15
  export declare class SharedSocket implements Disposable {
12
16
  private url;
@@ -31,6 +35,7 @@ export declare class SharedSocket implements Disposable {
31
35
  private startHeartbeat;
32
36
  private stopHeartbeat;
33
37
  private clearReconnect;
38
+ private buildUrl;
34
39
  private setState;
35
40
  [Symbol.dispose](): void;
36
41
  }
@@ -1,13 +1,20 @@
1
1
  import './utils/disposable';
2
- import type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler } from './types';
2
+ import type { SharedWebSocketOptions, TabRole, Unsubscribe, EventHandler, Channel, EventMap, Middleware } from './types';
3
3
  /**
4
4
  * SharedWebSocket — shares ONE WebSocket connection across browser tabs.
5
5
  *
6
- * One tab becomes the "leader" and holds the WebSocket.
7
- * Other tabs are "followers" receiving data via BroadcastChannel.
8
- * If the leader closes, a new leader is elected automatically.
6
+ * @typeParam TEvents - Event map for type-safe subscriptions.
7
+ *
8
+ * @example
9
+ * // Typed events
10
+ * type Events = {
11
+ * 'chat.message': { text: string; userId: string };
12
+ * 'order.created': { id: string; total: number };
13
+ * };
14
+ * const ws = new SharedWebSocket<Events>(url);
15
+ * ws.on('chat.message', (msg) => msg.text); // ← msg: { text, userId }
9
16
  */
10
- export declare class SharedWebSocket implements Disposable {
17
+ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implements Disposable {
11
18
  private readonly url;
12
19
  private readonly options;
13
20
  private bus;
@@ -18,18 +25,53 @@ export declare class SharedWebSocket implements Disposable {
18
25
  private tabId;
19
26
  private cleanups;
20
27
  private disposed;
21
- constructor(url: string, options?: SharedWebSocketOptions);
28
+ private readonly proto;
29
+ private readonly log;
30
+ private outgoingMiddleware;
31
+ private incomingMiddleware;
32
+ constructor(url: string, options?: SharedWebSocketOptions<TEvents>);
22
33
  get connected(): boolean;
23
34
  get tabRole(): TabRole;
24
35
  /** Start leader election and connect. */
25
36
  connect(): Promise<void>;
26
- /** Subscribe to server events (works in ALL tabs). */
27
- on(event: string, handler: EventHandler): Unsubscribe;
28
- once(event: string, handler: EventHandler): Unsubscribe;
37
+ /** Called when WebSocket connection opens (broadcast to all tabs). */
38
+ onConnect(fn: () => void): Unsubscribe;
39
+ /** Called when WebSocket connection closes (broadcast to all tabs). */
40
+ onDisconnect(fn: () => void): Unsubscribe;
41
+ /** Called when WebSocket starts reconnecting (broadcast to all tabs). */
42
+ onReconnecting(fn: () => void): Unsubscribe;
43
+ /** Called when this tab becomes leader or loses leadership. */
44
+ onLeaderChange(fn: (isLeader: boolean) => void): Unsubscribe;
45
+ /** Called on WebSocket or network error (broadcast to all tabs). */
46
+ onError(fn: (error: unknown) => void): Unsubscribe;
47
+ /**
48
+ * Add middleware to transform messages before send or after receive.
49
+ * Return null from middleware to drop the message.
50
+ *
51
+ * @example
52
+ * // Add timestamp to every outgoing message
53
+ * ws.use('outgoing', (msg) => ({ ...msg, timestamp: Date.now() }));
54
+ *
55
+ * @example
56
+ * // Decrypt incoming messages
57
+ * ws.use('incoming', (msg) => ({ ...msg, data: decrypt(msg.data) }));
58
+ *
59
+ * @example
60
+ * // Drop messages from blocked users
61
+ * ws.use('incoming', (msg) => blockedUsers.has(msg.userId) ? null : msg);
62
+ */
63
+ use(direction: 'outgoing' | 'incoming', fn: Middleware): this;
64
+ /** Subscribe to server events (works in ALL tabs). Type-safe with EventMap. */
65
+ on<K extends string & keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): Unsubscribe;
66
+ on(event: string, handler: EventHandler<unknown>): Unsubscribe;
67
+ once<K extends string & keyof TEvents>(event: K, handler: EventHandler<TEvents[K]>): Unsubscribe;
68
+ once(event: string, handler: EventHandler<unknown>): Unsubscribe;
29
69
  off(event: string, handler?: EventHandler): void;
30
- /** Async generator for consuming events. */
70
+ /** Async generator for consuming events. Type-safe with EventMap. */
71
+ stream<K extends string & keyof TEvents>(event: K, signal?: AbortSignal): AsyncGenerator<TEvents[K]>;
31
72
  stream(event: string, signal?: AbortSignal): AsyncGenerator<unknown>;
32
- /** Send message to server (auto-routed through leader). */
73
+ /** Send message to server (auto-routed through leader). Type-safe with EventMap. */
74
+ send<K extends string & keyof TEvents>(event: K, data: TEvents[K]): void;
33
75
  send(event: string, data: unknown): void;
34
76
  /** Request/response through server via leader. */
35
77
  request<T>(event: string, data: unknown, timeout?: number): Promise<T>;
@@ -37,9 +79,25 @@ export declare class SharedWebSocket implements Disposable {
37
79
  sync<T>(key: string, value: T): void;
38
80
  getSync<T>(key: string): T | undefined;
39
81
  onSync<T>(key: string, fn: (value: T) => void): Unsubscribe;
82
+ /**
83
+ * Subscribe to a private/scoped channel. Returns a channel handle with
84
+ * scoped on/send/stream methods. Sends join on subscribe, leave on unsubscribe.
85
+ *
86
+ * @example
87
+ * const chat = ws.channel('chat:room_123');
88
+ * chat.on('message', (msg) => render(msg));
89
+ * chat.send('message', { text: 'Hello' });
90
+ * chat.leave(); // sends leave + unsubscribes
91
+ *
92
+ * @example
93
+ * // Private notifications for tenant
94
+ * const notifications = ws.channel(`tenant:${tenantId}:notifications`);
95
+ * notifications.on('alert', (alert) => showToast(alert));
96
+ */
97
+ channel(name: string): Channel;
40
98
  disconnect(): void;
41
99
  private createSocket;
42
- private onBecomeLeader;
43
- private onLoseLeadership;
100
+ private handleBecomeLeader;
101
+ private handleLoseLeadership;
44
102
  [Symbol.dispose](): void;
45
103
  }
@@ -1,6 +1,6 @@
1
1
  import { type ReactNode } from 'react';
2
2
  import { SharedWebSocket } from '../SharedWebSocket';
3
- import type { SharedWebSocketOptions, TabRole } from '../types';
3
+ import type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';
4
4
  /**
5
5
  * Provider props — pass URL and options as props for flexibility.
6
6
  *
@@ -32,7 +32,7 @@ export interface SharedWebSocketProviderProps {
32
32
  * );
33
33
  * }
34
34
  */
35
- export declare function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SharedWebSocket | null>>;
35
+ export declare function SharedWebSocketProvider({ url, options, children }: SharedWebSocketProviderProps): import("react").FunctionComponentElement<import("react").ProviderProps<SharedWebSocket<import("..").EventMap> | null>>;
36
36
  /**
37
37
  * Access the SharedWebSocket instance from context.
38
38
  *
@@ -137,3 +137,30 @@ export declare function useSocketStatus(): {
137
137
  connected: boolean;
138
138
  tabRole: TabRole;
139
139
  };
140
+ /**
141
+ * Lifecycle hooks — react to connection state changes.
142
+ *
143
+ * @example
144
+ * useSocketLifecycle({
145
+ * onConnect: () => console.log('Connected!'),
146
+ * onDisconnect: () => console.log('Disconnected'),
147
+ * onReconnecting: () => showSpinner(),
148
+ * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),
149
+ * onError: (err) => reportError(err),
150
+ * });
151
+ */
152
+ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): void;
153
+ /**
154
+ * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.
155
+ *
156
+ * @example
157
+ * const chat = useChannel('chat:room_123');
158
+ * const message = useSocketEvent('chat:room_123:message');
159
+ * chat.send('message', { text: 'Hello' });
160
+ *
161
+ * @example
162
+ * // Tenant notifications
163
+ * const notifications = useChannel(`tenant:${tenantId}:notifications`);
164
+ * useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);
165
+ */
166
+ export declare function useChannel(name: string): import("..").Channel;
@@ -1,6 +1,6 @@
1
1
  import { type Ref, type InjectionKey, type App } from 'vue';
2
2
  import { SharedWebSocket } from '../SharedWebSocket';
3
- import type { SharedWebSocketOptions, TabRole } from '../types';
3
+ import type { SharedWebSocketOptions, TabRole, SocketLifecycleHandlers } from '../types';
4
4
  export declare const SharedWebSocketKey: InjectionKey<SharedWebSocket>;
5
5
  /**
6
6
  * Vue 3 plugin for SharedWebSocket.
@@ -101,3 +101,25 @@ export declare function useSocketStatus(): {
101
101
  connected: Ref<boolean>;
102
102
  tabRole: Ref<TabRole>;
103
103
  };
104
+ /**
105
+ * Lifecycle hooks — react to connection state changes.
106
+ *
107
+ * @example
108
+ * useSocketLifecycle({
109
+ * onConnect: () => console.log('Connected!'),
110
+ * onDisconnect: () => showOfflineBanner(),
111
+ * onReconnecting: () => showSpinner(),
112
+ * onLeaderChange: (isLeader) => console.log('Leader:', isLeader),
113
+ * onError: (err) => reportError(err),
114
+ * });
115
+ */
116
+ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): void;
117
+ /**
118
+ * Subscribe to a private channel. Auto-joins on mount, leaves on unmount.
119
+ *
120
+ * @example
121
+ * const chat = useChannel('chat:room_123');
122
+ * // Listen via useSocketEvent('chat:room_123:message')
123
+ * // Send via chat.send('message', { text: 'Hello' })
124
+ */
125
+ export declare function useChannel(name: string): import("..").Channel;
@@ -251,7 +251,10 @@ var SharedSocket = class {
251
251
  reconnectMaxDelay: options.reconnectMaxDelay ?? 3e4,
252
252
  heartbeatInterval: options.heartbeatInterval ?? 3e4,
253
253
  sendBuffer: options.sendBuffer ?? 100,
254
- auth: options.auth
254
+ auth: options.auth,
255
+ authToken: options.authToken,
256
+ authParam: options.authParam ?? "token",
257
+ pingPayload: options.pingPayload ?? { type: "ping" }
255
258
  };
256
259
  }
257
260
  url;
@@ -270,12 +273,7 @@ var SharedSocket = class {
270
273
  async connect() {
271
274
  if (this.disposed) return;
272
275
  this.setState("connecting");
273
- let connectUrl = this.url;
274
- if (this.opts.auth) {
275
- const token = await this.opts.auth();
276
- const sep = connectUrl.includes("?") ? "&" : "?";
277
- connectUrl = `${connectUrl}${sep}token=${encodeURIComponent(token)}`;
278
- }
276
+ const connectUrl = await this.buildUrl();
279
277
  this.ws = new WebSocket(connectUrl, this.opts.protocols);
280
278
  this.ws.onopen = () => {
281
279
  this.setState("connected");
@@ -356,7 +354,7 @@ var SharedSocket = class {
356
354
  this.stopHeartbeat();
357
355
  this.heartbeatTimer = setInterval(() => {
358
356
  if (this.ws?.readyState === WebSocket.OPEN) {
359
- this.ws.send(JSON.stringify({ type: "ping" }));
357
+ this.ws.send(JSON.stringify(this.opts.pingPayload));
360
358
  }
361
359
  }, this.opts.heartbeatInterval);
362
360
  }
@@ -372,6 +370,19 @@ var SharedSocket = class {
372
370
  this.reconnectTimer = null;
373
371
  }
374
372
  }
373
+ async buildUrl() {
374
+ let token;
375
+ if (this.opts.auth) {
376
+ token = await this.opts.auth();
377
+ } else if (this.opts.authToken) {
378
+ token = this.opts.authToken;
379
+ }
380
+ if (!token) return this.url;
381
+ const httpUrl = this.url.replace(/^ws(s?):\/\//, "http$1://");
382
+ const parsed = new URL(httpUrl);
383
+ parsed.searchParams.set(this.opts.authParam, token);
384
+ return parsed.toString().replace(/^http(s?):\/\//, "ws$1://");
385
+ }
375
386
  setState(state) {
376
387
  this._state = state;
377
388
  for (const fn of this.onStateChangeFns) fn(state);
@@ -566,11 +577,32 @@ var SubscriptionManager = class {
566
577
  };
567
578
 
568
579
  // src/SharedWebSocket.ts
580
+ var DEFAULT_PROTOCOL = {
581
+ eventField: "event",
582
+ dataField: "data",
583
+ channelJoin: "$channel:join",
584
+ channelLeave: "$channel:leave",
585
+ ping: { type: "ping" },
586
+ defaultEvent: "message"
587
+ };
588
+ var NOOP_LOGGER = {
589
+ debug() {
590
+ },
591
+ info() {
592
+ },
593
+ warn() {
594
+ },
595
+ error() {
596
+ }
597
+ };
569
598
  var SharedWebSocket = class {
570
599
  constructor(url, options = {}) {
571
600
  this.url = url;
572
601
  this.options = options;
602
+ this.proto = { ...DEFAULT_PROTOCOL, ...options.events };
603
+ this.log = options.debug ? options.logger ?? console : NOOP_LOGGER;
573
604
  this.tabId = generateId();
605
+ this.log.debug("[SharedWS] init", { tabId: this.tabId, url });
574
606
  this.bus = new MessageBus("shared-ws", this.tabId);
575
607
  this.coordinator = new TabCoordinator(this.bus, this.tabId, {
576
608
  electionTimeout: options.electionTimeout,
@@ -585,7 +617,7 @@ var SharedWebSocket = class {
585
617
  this.cleanups.push(
586
618
  this.bus.subscribe("ws:send", (msg) => {
587
619
  if (this.coordinator.isLeader && this.socket) {
588
- this.socket.send({ event: msg.event, data: msg.data });
620
+ this.socket.send({ [this.proto.eventField]: msg.event, [this.proto.dataField]: msg.data });
589
621
  }
590
622
  })
591
623
  );
@@ -595,8 +627,35 @@ var SharedWebSocket = class {
595
627
  this.subs.emit(`sync:${msg.key}`, msg.value);
596
628
  })
597
629
  );
598
- this.coordinator.onBecomeLeader(() => this.onBecomeLeader());
599
- this.coordinator.onLoseLeadership(() => this.onLoseLeadership());
630
+ this.coordinator.onBecomeLeader(() => {
631
+ this.handleBecomeLeader();
632
+ this.bus.broadcast("ws:lifecycle", { type: "leader", isLeader: true });
633
+ });
634
+ this.coordinator.onLoseLeadership(() => {
635
+ this.handleLoseLeadership();
636
+ this.bus.broadcast("ws:lifecycle", { type: "leader", isLeader: false });
637
+ });
638
+ this.cleanups.push(
639
+ this.bus.subscribe("ws:lifecycle", (msg) => {
640
+ switch (msg.type) {
641
+ case "connect":
642
+ this.subs.emit("$lifecycle:connect", void 0);
643
+ break;
644
+ case "disconnect":
645
+ this.subs.emit("$lifecycle:disconnect", void 0);
646
+ break;
647
+ case "reconnecting":
648
+ this.subs.emit("$lifecycle:reconnecting", void 0);
649
+ break;
650
+ case "leader":
651
+ this.subs.emit("$lifecycle:leader", msg.isLeader);
652
+ break;
653
+ case "error":
654
+ this.subs.emit("$lifecycle:error", msg.error);
655
+ break;
656
+ }
657
+ })
658
+ );
600
659
  if (typeof window !== "undefined") {
601
660
  const onBeforeUnload = () => this[Symbol.dispose]();
602
661
  window.addEventListener("beforeunload", onBeforeUnload);
@@ -613,6 +672,10 @@ var SharedWebSocket = class {
613
672
  tabId;
614
673
  cleanups = [];
615
674
  disposed = false;
675
+ proto;
676
+ log;
677
+ outgoingMiddleware = [];
678
+ incomingMiddleware = [];
616
679
  get connected() {
617
680
  return this.socket?.state === "connected" || !this.coordinator.isLeader;
618
681
  }
@@ -623,24 +686,78 @@ var SharedWebSocket = class {
623
686
  async connect() {
624
687
  await this.coordinator.elect();
625
688
  }
626
- /** Subscribe to server events (works in ALL tabs). */
689
+ // ─── Lifecycle Hooks ─────────────────────────────────
690
+ /** Called when WebSocket connection opens (broadcast to all tabs). */
691
+ onConnect(fn) {
692
+ return this.subs.on("$lifecycle:connect", fn);
693
+ }
694
+ /** Called when WebSocket connection closes (broadcast to all tabs). */
695
+ onDisconnect(fn) {
696
+ return this.subs.on("$lifecycle:disconnect", fn);
697
+ }
698
+ /** Called when WebSocket starts reconnecting (broadcast to all tabs). */
699
+ onReconnecting(fn) {
700
+ return this.subs.on("$lifecycle:reconnecting", fn);
701
+ }
702
+ /** Called when this tab becomes leader or loses leadership. */
703
+ onLeaderChange(fn) {
704
+ return this.subs.on("$lifecycle:leader", fn);
705
+ }
706
+ /** Called on WebSocket or network error (broadcast to all tabs). */
707
+ onError(fn) {
708
+ return this.subs.on("$lifecycle:error", fn);
709
+ }
710
+ // ─── Middleware ───────────────────────────────────────
711
+ /**
712
+ * Add middleware to transform messages before send or after receive.
713
+ * Return null from middleware to drop the message.
714
+ *
715
+ * @example
716
+ * // Add timestamp to every outgoing message
717
+ * ws.use('outgoing', (msg) => ({ ...msg, timestamp: Date.now() }));
718
+ *
719
+ * @example
720
+ * // Decrypt incoming messages
721
+ * ws.use('incoming', (msg) => ({ ...msg, data: decrypt(msg.data) }));
722
+ *
723
+ * @example
724
+ * // Drop messages from blocked users
725
+ * ws.use('incoming', (msg) => blockedUsers.has(msg.userId) ? null : msg);
726
+ */
727
+ use(direction, fn) {
728
+ if (direction === "outgoing") {
729
+ this.outgoingMiddleware.push(fn);
730
+ } else {
731
+ this.incomingMiddleware.push(fn);
732
+ }
733
+ return this;
734
+ }
735
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
627
736
  on(event, handler) {
628
737
  return this.subs.on(event, handler);
629
738
  }
739
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
630
740
  once(event, handler) {
631
741
  return this.subs.once(event, handler);
632
742
  }
633
743
  off(event, handler) {
634
744
  this.subs.off(event, handler);
635
745
  }
636
- /** Async generator for consuming events. */
637
746
  stream(event, signal) {
638
747
  return this.subs.stream(event, signal);
639
748
  }
640
- /** Send message to server (auto-routed through leader). */
641
749
  send(event, data) {
750
+ let payload = { [this.proto.eventField]: event, [this.proto.dataField]: data };
751
+ for (const mw of this.outgoingMiddleware) {
752
+ payload = mw(payload);
753
+ if (payload === null) {
754
+ this.log.debug("[SharedWS] \u2717 outgoing dropped by middleware", event);
755
+ return;
756
+ }
757
+ }
758
+ this.log.debug("[SharedWS] \u2192 send", event, data);
642
759
  if (this.coordinator.isLeader && this.socket) {
643
- this.socket.send({ event, data });
760
+ this.socket.send(payload);
644
761
  } else {
645
762
  this.bus.publish("ws:send", { event, data });
646
763
  }
@@ -660,6 +777,50 @@ var SharedWebSocket = class {
660
777
  onSync(key, fn) {
661
778
  return this.subs.on(`sync:${key}`, fn);
662
779
  }
780
+ /**
781
+ * Subscribe to a private/scoped channel. Returns a channel handle with
782
+ * scoped on/send/stream methods. Sends join on subscribe, leave on unsubscribe.
783
+ *
784
+ * @example
785
+ * const chat = ws.channel('chat:room_123');
786
+ * chat.on('message', (msg) => render(msg));
787
+ * chat.send('message', { text: 'Hello' });
788
+ * chat.leave(); // sends leave + unsubscribes
789
+ *
790
+ * @example
791
+ * // Private notifications for tenant
792
+ * const notifications = ws.channel(`tenant:${tenantId}:notifications`);
793
+ * notifications.on('alert', (alert) => showToast(alert));
794
+ */
795
+ channel(name) {
796
+ this.send(this.proto.channelJoin, { channel: name });
797
+ const self = this;
798
+ const unsubs = [];
799
+ return {
800
+ name,
801
+ on(event, handler) {
802
+ const unsub = self.subs.on(`${name}:${event}`, handler);
803
+ unsubs.push(unsub);
804
+ return unsub;
805
+ },
806
+ once(event, handler) {
807
+ const unsub = self.subs.once(`${name}:${event}`, handler);
808
+ unsubs.push(unsub);
809
+ return unsub;
810
+ },
811
+ send(event, data) {
812
+ self.send(`${name}:${event}`, data);
813
+ },
814
+ stream(event, signal) {
815
+ return self.subs.stream(`${name}:${event}`, signal);
816
+ },
817
+ leave() {
818
+ self.send(self.proto.channelLeave, { channel: name });
819
+ for (const unsub of unsubs) unsub();
820
+ unsubs.length = 0;
821
+ }
822
+ };
823
+ }
663
824
  disconnect() {
664
825
  this[Symbol.dispose]();
665
826
  }
@@ -669,7 +830,8 @@ var SharedWebSocket = class {
669
830
  reconnect: this.options.reconnect,
670
831
  reconnectMaxDelay: this.options.reconnectMaxDelay,
671
832
  heartbeatInterval: this.options.heartbeatInterval,
672
- sendBuffer: this.options.sendBuffer
833
+ sendBuffer: this.options.sendBuffer,
834
+ pingPayload: this.proto.ping
673
835
  };
674
836
  if (this.options.useWorker) {
675
837
  return new WorkerSocket(this.url, {
@@ -679,23 +841,51 @@ var SharedWebSocket = class {
679
841
  }
680
842
  return new SharedSocket(this.url, {
681
843
  ...socketOptions,
682
- auth: this.options.auth
844
+ auth: this.options.auth,
845
+ authToken: this.options.authToken,
846
+ authParam: this.options.authParam
683
847
  });
684
848
  }
685
- onBecomeLeader() {
849
+ handleBecomeLeader() {
850
+ this.log.info("[SharedWS] \u{1F451} became leader");
686
851
  this.socket = this.createSocket();
687
- this.socket.onMessage((data) => {
688
- const event = data?.event ?? "message";
689
- const payload = data?.data ?? data;
852
+ this.socket.onMessage((raw) => {
853
+ let data = raw;
854
+ for (const mw of this.incomingMiddleware) {
855
+ data = mw(data);
856
+ if (data === null) {
857
+ this.log.debug("[SharedWS] \u2717 incoming dropped by middleware");
858
+ return;
859
+ }
860
+ }
861
+ const msg = data;
862
+ const event = msg?.[this.proto.eventField] ?? this.proto.defaultEvent;
863
+ const payload = msg?.[this.proto.dataField] ?? data;
864
+ this.log.debug("[SharedWS] \u2190 recv", event, payload);
690
865
  this.bus.broadcast("ws:message", { event, data: payload });
691
866
  });
867
+ this.socket.onStateChange((state) => {
868
+ this.log.info("[SharedWS]", state === "connected" ? "\u2713 connected" : state === "reconnecting" ? "\u{1F504} reconnecting" : `state: ${state}`);
869
+ switch (state) {
870
+ case "connected":
871
+ this.bus.broadcast("ws:lifecycle", { type: "connect" });
872
+ break;
873
+ case "closed":
874
+ this.bus.broadcast("ws:lifecycle", { type: "disconnect" });
875
+ break;
876
+ case "reconnecting":
877
+ this.bus.broadcast("ws:lifecycle", { type: "reconnecting" });
878
+ break;
879
+ }
880
+ });
692
881
  this.cleanups.push(
693
882
  this.bus.respond("ws:request", async (req) => {
694
883
  return new Promise((resolve) => {
695
884
  const unsub = this.socket.onMessage((response) => {
696
- if (response?.event === req.event || response?.requestId) {
885
+ const res = response;
886
+ if (res?.[this.proto.eventField] === req.event || res?.requestId) {
697
887
  unsub();
698
- resolve(response?.data ?? response);
888
+ resolve(res?.[this.proto.dataField] ?? response);
699
889
  }
700
890
  });
701
891
  this.socket.send({ event: req.event, data: req.data });
@@ -704,7 +894,7 @@ var SharedWebSocket = class {
704
894
  );
705
895
  this.socket.connect();
706
896
  }
707
- onLoseLeadership() {
897
+ handleLoseLeadership() {
708
898
  if (this.socket) {
709
899
  this.socket[Symbol.dispose]();
710
900
  this.socket = null;
@@ -734,4 +924,4 @@ export {
734
924
  SubscriptionManager,
735
925
  SharedWebSocket
736
926
  };
737
- //# sourceMappingURL=chunk-TNEMKPGP.js.map
927
+ //# sourceMappingURL=chunk-4D2ZDCA6.js.map