@gwakko/shared-websocket 0.10.2 → 0.11.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.
package/README.md CHANGED
@@ -77,6 +77,11 @@ await withSocket('wss://api.example.com/ws', {
77
77
  title: (n) => n.title, // + browser Notification
78
78
  target: 'active', // active | leader | all
79
79
  });
80
+
81
+ // Runtime auth — authenticate/deauthenticate without reconnecting
82
+ ws.authenticate(token); // → sends $auth:login to server
83
+ const chat = ws.channel('chat:private', { auth: true }); // auto-leaves on deauth
84
+ ws.deauthenticate(); // → auto-leaves auth channels/topics
80
85
  });
81
86
  ```
82
87
 
@@ -86,6 +91,7 @@ await withSocket('wss://api.example.com/ws', {
86
91
  import {
87
92
  SharedWebSocketProvider,
88
93
  useSharedWebSocket,
94
+ useSocketAuth,
89
95
  useSocketEvent,
90
96
  useSocketStream,
91
97
  useSocketSync,
@@ -110,6 +116,7 @@ function App() {
110
116
 
111
117
  function Dashboard() {
112
118
  const ws = useSharedWebSocket();
119
+ const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();
113
120
  const order = useSocketEvent<Order>('order.created');
114
121
  const [cart, setCart] = useSocketSync('cart', { items: [] });
115
122
  const { connected, tabRole } = useSocketStatus();
@@ -123,10 +130,11 @@ function Dashboard() {
123
130
  useSocketLifecycle({
124
131
  onConnect: () => toast.success('Connected'),
125
132
  onActive: () => refreshData(),
133
+ onAuthChange: (auth) => !auth && navigate('/login'),
126
134
  });
127
135
 
128
- // Channel
129
- const chat = useChannel(`chat:${roomId}`);
136
+ // Auth-aware channel — auto-leaves on deauth
137
+ const chat = useChannel(`chat:${roomId}`, { auth: true });
130
138
 
131
139
  // Topics
132
140
  useTopics(['notifications:orders']);
@@ -155,6 +163,7 @@ app.use(createSharedWebSocketPlugin('wss://api.example.com/ws', {
155
163
  <script setup lang="ts">
156
164
  import {
157
165
  useSharedWebSocket,
166
+ useSocketAuth,
158
167
  useSocketEvent,
159
168
  useSocketSync,
160
169
  useSocketLifecycle,
@@ -164,15 +173,18 @@ import {
164
173
  } from '@gwakko/shared-websocket/vue';
165
174
 
166
175
  const ws = useSharedWebSocket();
176
+ const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();
167
177
  const order = useSocketEvent<Order>('order.created');
168
178
  const cart = useSocketSync('cart', { items: [] });
169
179
 
170
180
  useSocketLifecycle({
171
181
  onConnect: () => toast.success('Connected'),
172
182
  onActive: () => refreshData(),
183
+ onAuthChange: (auth) => { if (!auth) router.push('/login'); },
173
184
  });
174
185
 
175
- const chat = useChannel(`chat:${roomId}`);
186
+ // Auth-aware channel — auto-leaves on deauth
187
+ const chat = useChannel(`chat:${roomId}`, { auth: true });
176
188
  useTopics(['notifications:orders']);
177
189
 
178
190
  usePush('notification', {
@@ -196,10 +208,11 @@ usePush('notification', {
196
208
  | **Worker Mode** | `useWorker: true` — WebSocket off main thread |
197
209
  | **Custom Serialization** | `serialize`/`deserialize` — JSON, MessagePack, Protobuf |
198
210
  | **Per-Event Serializers** | `ws.serializer(event, fn)` — binary for specific events |
199
- | **Lifecycle Hooks** | onConnect, onDisconnect, onActive, onInactive, onLeaderChange |
211
+ | **Runtime Auth** | `authenticate(token)` / `deauthenticate()` on existing connection |
212
+ | **Lifecycle Hooks** | onConnect, onDisconnect, onActive, onInactive, onLeaderChange, onAuthChange |
200
213
  | **Debug/Logger** | `debug: true` + injectable logger (pino, Sentry) |
201
214
  | **Event Protocol** | Configurable field names (Socket.IO, Phoenix, Laravel Echo) |
202
- | **Auth** | `auth` callback, `authToken` string, custom `authParam` |
215
+ | **Auth** | URL param (`auth` callback / `authToken`) + runtime `authenticate()`/`deauthenticate()` |
203
216
  | **Zero Dependencies** | Pure browser APIs |
204
217
 
205
218
  ## Processing Pipeline
@@ -31,9 +31,14 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
31
31
  private incomingMiddleware;
32
32
  private serializers;
33
33
  private deserializers;
34
+ private _isAuthenticated;
35
+ private authChannels;
36
+ private authTopics;
34
37
  constructor(url: string, options?: SharedWebSocketOptions<TEvents>);
35
38
  get connected(): boolean;
36
39
  get tabRole(): TabRole;
40
+ /** Whether the user is authenticated via runtime auth. */
41
+ get isAuthenticated(): boolean;
37
42
  /** Whether this tab is currently visible/focused. */
38
43
  get isActive(): boolean;
39
44
  /** Start leader election and connect. */
@@ -54,6 +59,37 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
54
59
  onInactive(fn: () => void): Unsubscribe;
55
60
  /** Called on any visibility change. */
56
61
  onVisibilityChange(fn: (isActive: boolean) => void): Unsubscribe;
62
+ /**
63
+ * Authenticate on an existing connection. Sends auth event to server,
64
+ * syncs auth state across all tabs. Use for login after guest connection.
65
+ *
66
+ * @example
67
+ * const token = await loginApi(email, password);
68
+ * ws.authenticate(token);
69
+ *
70
+ * @example
71
+ * // React — via useSocketAuth hook
72
+ * const { authenticate } = useSocketAuth();
73
+ * authenticate(token);
74
+ */
75
+ authenticate(token: string): void;
76
+ /**
77
+ * Deauthenticate — notifies server, auto-leaves all auth-required channels
78
+ * and topics, syncs state across tabs. Connection stays open for public events.
79
+ *
80
+ * @example
81
+ * ws.deauthenticate(); // connection stays open, auth subscriptions cleaned up
82
+ */
83
+ deauthenticate(): void;
84
+ /**
85
+ * Called when auth state changes (authenticate, deauthenticate, or server revocation).
86
+ *
87
+ * @example
88
+ * ws.onAuthChange((authenticated) => {
89
+ * if (!authenticated) router.push('/login');
90
+ * });
91
+ */
92
+ onAuthChange(fn: (authenticated: boolean) => void): Unsubscribe;
57
93
  /**
58
94
  * Add middleware to transform messages before send or after receive.
59
95
  * Return null from middleware to drop the message.
@@ -129,7 +165,9 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
129
165
  * const notifications = ws.channel(`tenant:${tenantId}:notifications`);
130
166
  * notifications.on('alert', (alert) => showToast(alert));
131
167
  */
132
- channel(name: string): Channel;
168
+ channel(name: string, options?: {
169
+ auth?: boolean;
170
+ }): Channel;
133
171
  /**
134
172
  * Subscribe to a server-side topic. Server will start sending events for this topic.
135
173
  * Sends topicSubscribe event (default: "$topic:subscribe").
@@ -139,7 +177,9 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
139
177
  * ws.subscribe('notifications:payments');
140
178
  * ws.subscribe(`user:${userId}:mentions`);
141
179
  */
142
- subscribe(topic: string): void;
180
+ subscribe(topic: string, options?: {
181
+ auth?: boolean;
182
+ }): void;
143
183
  /**
144
184
  * Unsubscribe from a server-side topic.
145
185
  * Sends topicUnsubscribe event (default: "$topic:unsubscribe").
@@ -205,6 +245,7 @@ export declare class SharedWebSocket<TEvents extends EventMap = EventMap> implem
205
245
  disconnect(): void;
206
246
  private createSocket;
207
247
  private handleBecomeLeader;
248
+ private reAuthenticateOnReconnect;
208
249
  private handleLoseLeadership;
209
250
  [Symbol.dispose](): void;
210
251
  }
@@ -41,6 +41,33 @@ export declare function SharedWebSocketProvider({ url, options, children }: Shar
41
41
  * ws.send('chat.message', { text: 'Hello' });
42
42
  */
43
43
  export declare function useSharedWebSocket(): SharedWebSocket;
44
+ /**
45
+ * Reactive auth state with authenticate/deauthenticate actions.
46
+ * Syncs across all tabs via BroadcastChannel.
47
+ *
48
+ * @example
49
+ * function LoginPage() {
50
+ * const { authenticate } = useSocketAuth();
51
+ * const login = async (email: string, password: string) => {
52
+ * const { token } = await api.login(email, password);
53
+ * authenticate(token);
54
+ * };
55
+ * return <button onClick={() => login('user@test.com', 'pass')}>Login</button>;
56
+ * }
57
+ *
58
+ * @example
59
+ * function Header() {
60
+ * const { isAuthenticated, deauthenticate } = useSocketAuth();
61
+ * return isAuthenticated
62
+ * ? <button onClick={deauthenticate}>Logout</button>
63
+ * : <Link to="/login">Login</Link>;
64
+ * }
65
+ */
66
+ export declare function useSocketAuth(): {
67
+ isAuthenticated: boolean;
68
+ authenticate: (token: string) => void;
69
+ deauthenticate: () => void;
70
+ };
44
71
  /**
45
72
  * Subscribe to a WebSocket event.
46
73
  * - Without callback: returns the latest received value (reactive state).
@@ -136,6 +163,7 @@ export declare function useSocketCallback<T>(event: string, callback: (data: T)
136
163
  export declare function useSocketStatus(): {
137
164
  connected: boolean;
138
165
  tabRole: TabRole;
166
+ isAuthenticated: boolean;
139
167
  };
140
168
  /**
141
169
  * Lifecycle hooks — react to connection state changes.
@@ -163,7 +191,9 @@ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): v
163
191
  * const notifications = useChannel(`tenant:${tenantId}:notifications`);
164
192
  * useSocketCallback(`tenant:${tenantId}:notifications:alert`, showToast);
165
193
  */
166
- export declare function useChannel(name: string): import("..").Channel;
194
+ export declare function useChannel(name: string, options?: {
195
+ auth?: boolean;
196
+ }): import("..").Channel;
167
197
  /**
168
198
  * Subscribe to server-side topics. Auto-unsubscribes on unmount.
169
199
  *
@@ -171,7 +201,9 @@ export declare function useChannel(name: string): import("..").Channel;
171
201
  * useTopics(['notifications:orders', 'notifications:payments']);
172
202
  * useTopics([`user:${userId}:mentions`]);
173
203
  */
174
- export declare function useTopics(topics: string[]): void;
204
+ export declare function useTopics(topics: string[], options?: {
205
+ auth?: boolean;
206
+ }): void;
175
207
  /**
176
208
  * Enable browser push notifications for an event. Auto-cleanup on unmount.
177
209
  *
@@ -23,6 +23,26 @@ export declare function createSharedWebSocketPlugin(url: string, options?: Share
23
23
  * ws.send('chat.message', { text: 'Hello' });
24
24
  */
25
25
  export declare function useSharedWebSocket(): SharedWebSocket;
26
+ /**
27
+ * Reactive auth state with authenticate/deauthenticate actions.
28
+ * Syncs across all tabs.
29
+ *
30
+ * @example
31
+ * const { isAuthenticated, authenticate, deauthenticate } = useSocketAuth();
32
+ *
33
+ * async function login(email: string, password: string) {
34
+ * const { token } = await api.login(email, password);
35
+ * authenticate(token);
36
+ * }
37
+ *
38
+ * @example
39
+ * // In template: <button v-if="isAuthenticated" @click="deauthenticate">Logout</button>
40
+ */
41
+ export declare function useSocketAuth(): {
42
+ isAuthenticated: Ref<boolean>;
43
+ authenticate: (token: string) => void;
44
+ deauthenticate: () => void;
45
+ };
26
46
  /**
27
47
  * Subscribe to a WebSocket event.
28
48
  * - Without callback: returns reactive ref with latest value.
@@ -100,6 +120,7 @@ export declare function useSocketCallback<T>(event: string, callback: (data: T)
100
120
  export declare function useSocketStatus(): {
101
121
  connected: Ref<boolean>;
102
122
  tabRole: Ref<TabRole>;
123
+ isAuthenticated: Ref<boolean>;
103
124
  };
104
125
  /**
105
126
  * Lifecycle hooks — react to connection state changes.
@@ -122,14 +143,18 @@ export declare function useSocketLifecycle(handlers: SocketLifecycleHandlers): v
122
143
  * // Listen via useSocketEvent('chat:room_123:message')
123
144
  * // Send via chat.send('message', { text: 'Hello' })
124
145
  */
125
- export declare function useChannel(name: string): import("..").Channel;
146
+ export declare function useChannel(name: string, options?: {
147
+ auth?: boolean;
148
+ }): import("..").Channel;
126
149
  /**
127
150
  * Subscribe to server-side topics. Auto-unsubscribes on unmount.
128
151
  *
129
152
  * @example
130
153
  * useTopics(['notifications:orders', 'notifications:payments']);
131
154
  */
132
- export declare function useTopics(topics: string[]): void;
155
+ export declare function useTopics(topics: string[], options?: {
156
+ auth?: boolean;
157
+ }): void;
133
158
  /**
134
159
  * Enable browser push notifications for an event. Auto-cleanup on unmount.
135
160
  *
@@ -590,7 +590,10 @@ var DEFAULT_PROTOCOL = {
590
590
  ping: { type: "ping" },
591
591
  defaultEvent: "message",
592
592
  topicSubscribe: "$topic:subscribe",
593
- topicUnsubscribe: "$topic:unsubscribe"
593
+ topicUnsubscribe: "$topic:unsubscribe",
594
+ authLogin: "$auth:login",
595
+ authLogout: "$auth:logout",
596
+ authRevoked: "$auth:revoked"
594
597
  };
595
598
  var NOOP_LOGGER = {
596
599
  debug() {
@@ -603,7 +606,7 @@ var NOOP_LOGGER = {
603
606
  }
604
607
  };
605
608
  var SharedWebSocket = (_class6 = class {
606
- constructor(url, options = {}) {;_class6.prototype.__init25.call(this);_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);
609
+ constructor(url, options = {}) {;_class6.prototype.__init25.call(this);_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);
607
610
  this.url = url;
608
611
  this.options = options;
609
612
  this.proto = { ...DEFAULT_PROTOCOL, ...options.events };
@@ -660,6 +663,15 @@ var SharedWebSocket = (_class6 = class {
660
663
  case "error":
661
664
  this.subs.emit("$lifecycle:error", msg.error);
662
665
  break;
666
+ case "auth": {
667
+ this._isAuthenticated = !!msg.authenticated;
668
+ if (!msg.authenticated) {
669
+ this.authChannels.clear();
670
+ this.authTopics.clear();
671
+ }
672
+ this.subs.emit("$lifecycle:auth", msg.authenticated);
673
+ break;
674
+ }
663
675
  }
664
676
  })
665
677
  );
@@ -672,6 +684,20 @@ var SharedWebSocket = (_class6 = class {
672
684
  document.addEventListener("visibilitychange", onVisibilityChange);
673
685
  this.cleanups.push(() => document.removeEventListener("visibilitychange", onVisibilityChange));
674
686
  }
687
+ this.cleanups.push(
688
+ this.subs.on(this.proto.authRevoked, () => {
689
+ if (this.coordinator.isLeader) {
690
+ for (const [, ch] of this.authChannels) ch.leave();
691
+ for (const topic of this.authTopics) this.unsubscribe(topic);
692
+ }
693
+ this.authChannels.clear();
694
+ this.authTopics.clear();
695
+ this._isAuthenticated = false;
696
+ this.syncStore.delete("$auth:token");
697
+ this.subs.emit("$lifecycle:auth", false);
698
+ this.log.warn("[SharedWS] auth revoked by server");
699
+ })
700
+ );
675
701
  if (typeof window !== "undefined") {
676
702
  const onBeforeUnload = () => this[Symbol.dispose]();
677
703
  window.addEventListener("beforeunload", onBeforeUnload);
@@ -694,12 +720,19 @@ var SharedWebSocket = (_class6 = class {
694
720
  __init31() {this.incomingMiddleware = []}
695
721
  __init32() {this.serializers = /* @__PURE__ */ new Map()}
696
722
  __init33() {this.deserializers = /* @__PURE__ */ new Map()}
723
+ __init34() {this._isAuthenticated = false}
724
+ __init35() {this.authChannels = /* @__PURE__ */ new Map()}
725
+ __init36() {this.authTopics = /* @__PURE__ */ new Set()}
697
726
  get connected() {
698
727
  return _optionalChain([this, 'access', _30 => _30.socket, 'optionalAccess', _31 => _31.state]) === "connected" || !this.coordinator.isLeader;
699
728
  }
700
729
  get tabRole() {
701
730
  return this.coordinator.isLeader ? "leader" : "follower";
702
731
  }
732
+ /** Whether the user is authenticated via runtime auth. */
733
+ get isAuthenticated() {
734
+ return this._isAuthenticated;
735
+ }
703
736
  /** Whether this tab is currently visible/focused. */
704
737
  get isActive() {
705
738
  return typeof document !== "undefined" ? !document.hidden : true;
@@ -745,6 +778,58 @@ var SharedWebSocket = (_class6 = class {
745
778
  onVisibilityChange(fn) {
746
779
  return this.subs.on("$lifecycle:active", fn);
747
780
  }
781
+ // ─── Authentication ──────────────────────────────────
782
+ /**
783
+ * Authenticate on an existing connection. Sends auth event to server,
784
+ * syncs auth state across all tabs. Use for login after guest connection.
785
+ *
786
+ * @example
787
+ * const token = await loginApi(email, password);
788
+ * ws.authenticate(token);
789
+ *
790
+ * @example
791
+ * // React — via useSocketAuth hook
792
+ * const { authenticate } = useSocketAuth();
793
+ * authenticate(token);
794
+ */
795
+ authenticate(token) {
796
+ this._isAuthenticated = true;
797
+ this.syncStore.set("$auth:token", token);
798
+ this.bus.broadcast("ws:sync", { key: "$auth:token", value: token });
799
+ this.send(this.proto.authLogin, { token });
800
+ this.bus.broadcast("ws:lifecycle", { type: "auth", authenticated: true });
801
+ this.log.info("[SharedWS] authenticated");
802
+ }
803
+ /**
804
+ * Deauthenticate — notifies server, auto-leaves all auth-required channels
805
+ * and topics, syncs state across tabs. Connection stays open for public events.
806
+ *
807
+ * @example
808
+ * ws.deauthenticate(); // connection stays open, auth subscriptions cleaned up
809
+ */
810
+ deauthenticate() {
811
+ for (const [, ch] of this.authChannels) ch.leave();
812
+ this.authChannels.clear();
813
+ for (const topic of this.authTopics) this.unsubscribe(topic);
814
+ this.authTopics.clear();
815
+ this._isAuthenticated = false;
816
+ this.send(this.proto.authLogout, {});
817
+ this.syncStore.delete("$auth:token");
818
+ this.bus.broadcast("ws:sync", { key: "$auth:token", value: void 0 });
819
+ this.bus.broadcast("ws:lifecycle", { type: "auth", authenticated: false });
820
+ this.log.info("[SharedWS] deauthenticated");
821
+ }
822
+ /**
823
+ * Called when auth state changes (authenticate, deauthenticate, or server revocation).
824
+ *
825
+ * @example
826
+ * ws.onAuthChange((authenticated) => {
827
+ * if (!authenticated) router.push('/login');
828
+ * });
829
+ */
830
+ onAuthChange(fn) {
831
+ return this.subs.on("$lifecycle:auth", fn);
832
+ }
748
833
  // ─── Middleware ───────────────────────────────────────
749
834
  /**
750
835
  * Add middleware to transform messages before send or after receive.
@@ -864,11 +949,12 @@ var SharedWebSocket = (_class6 = class {
864
949
  * const notifications = ws.channel(`tenant:${tenantId}:notifications`);
865
950
  * notifications.on('alert', (alert) => showToast(alert));
866
951
  */
867
- channel(name) {
952
+ channel(name, options) {
868
953
  this.send(this.proto.channelJoin, { channel: name });
869
954
  const self = this;
870
955
  const unsubs = [];
871
- return {
956
+ const isAuth = _nullishCoalesce(_optionalChain([options, 'optionalAccess', _32 => _32.auth]), () => ( false));
957
+ const ch = {
872
958
  name,
873
959
  on(event, handler) {
874
960
  const unsub = self.subs.on(`${name}:${event}`, handler);
@@ -890,8 +976,13 @@ var SharedWebSocket = (_class6 = class {
890
976
  self.send(self.proto.channelLeave, { channel: name });
891
977
  for (const unsub of unsubs) unsub();
892
978
  unsubs.length = 0;
979
+ if (isAuth) self.authChannels.delete(name);
893
980
  }
894
981
  };
982
+ if (isAuth) {
983
+ this.authChannels.set(name, ch);
984
+ }
985
+ return ch;
895
986
  }
896
987
  // ─── Topics ──────────────────────────────────────────
897
988
  /**
@@ -903,9 +994,12 @@ var SharedWebSocket = (_class6 = class {
903
994
  * ws.subscribe('notifications:payments');
904
995
  * ws.subscribe(`user:${userId}:mentions`);
905
996
  */
906
- subscribe(topic) {
997
+ subscribe(topic, options) {
907
998
  this.send(this.proto.topicSubscribe, { topic });
908
- this.log.debug("[SharedWS] \u{1F4CC} subscribe topic", topic);
999
+ if (_optionalChain([options, 'optionalAccess', _33 => _33.auth])) {
1000
+ this.authTopics.add(topic);
1001
+ }
1002
+ this.log.debug("[SharedWS] subscribe topic", topic);
909
1003
  }
910
1004
  /**
911
1005
  * Unsubscribe from a server-side topic.
@@ -913,7 +1007,8 @@ var SharedWebSocket = (_class6 = class {
913
1007
  */
914
1008
  unsubscribe(topic) {
915
1009
  this.send(this.proto.topicUnsubscribe, { topic });
916
- this.log.debug("[SharedWS] \u{1F4CC} unsubscribe topic", topic);
1010
+ this.authTopics.delete(topic);
1011
+ this.log.debug("[SharedWS] unsubscribe topic", topic);
917
1012
  }
918
1013
  // ─── Push Notifications ─────────────────────────────
919
1014
  /**
@@ -1029,8 +1124,8 @@ var SharedWebSocket = (_class6 = class {
1029
1124
  }
1030
1125
  }
1031
1126
  const msg = data;
1032
- const event = _nullishCoalesce(_optionalChain([msg, 'optionalAccess', _32 => _32[this.proto.eventField]]), () => ( this.proto.defaultEvent));
1033
- let payload = _nullishCoalesce(_optionalChain([msg, 'optionalAccess', _33 => _33[this.proto.dataField]]), () => ( data));
1127
+ const event = _nullishCoalesce(_optionalChain([msg, 'optionalAccess', _34 => _34[this.proto.eventField]]), () => ( this.proto.defaultEvent));
1128
+ let payload = _nullishCoalesce(_optionalChain([msg, 'optionalAccess', _35 => _35[this.proto.dataField]]), () => ( data));
1034
1129
  const eventDeserializer = this.deserializers.get(event);
1035
1130
  if (eventDeserializer) {
1036
1131
  payload = eventDeserializer(payload);
@@ -1043,6 +1138,7 @@ var SharedWebSocket = (_class6 = class {
1043
1138
  switch (state) {
1044
1139
  case "connected":
1045
1140
  this.bus.broadcast("ws:lifecycle", { type: "connect" });
1141
+ this.reAuthenticateOnReconnect();
1046
1142
  break;
1047
1143
  case "closed":
1048
1144
  this.bus.broadcast("ws:lifecycle", { type: "disconnect" });
@@ -1057,9 +1153,9 @@ var SharedWebSocket = (_class6 = class {
1057
1153
  return new Promise((resolve) => {
1058
1154
  const unsub = this.socket.onMessage((response) => {
1059
1155
  const res = response;
1060
- if (_optionalChain([res, 'optionalAccess', _34 => _34[this.proto.eventField]]) === req.event || _optionalChain([res, 'optionalAccess', _35 => _35.requestId])) {
1156
+ if (_optionalChain([res, 'optionalAccess', _36 => _36[this.proto.eventField]]) === req.event || _optionalChain([res, 'optionalAccess', _37 => _37.requestId])) {
1061
1157
  unsub();
1062
- resolve(_nullishCoalesce(_optionalChain([res, 'optionalAccess', _36 => _36[this.proto.dataField]]), () => ( response)));
1158
+ resolve(_nullishCoalesce(_optionalChain([res, 'optionalAccess', _38 => _38[this.proto.dataField]]), () => ( response)));
1063
1159
  }
1064
1160
  });
1065
1161
  this.socket.send({ event: req.event, data: req.data });
@@ -1068,6 +1164,29 @@ var SharedWebSocket = (_class6 = class {
1068
1164
  );
1069
1165
  this.socket.connect();
1070
1166
  }
1167
+ reAuthenticateOnReconnect() {
1168
+ if (!this._isAuthenticated || !this.socket) return;
1169
+ const token = this.syncStore.get("$auth:token");
1170
+ if (token) {
1171
+ this.socket.send({
1172
+ [this.proto.eventField]: this.proto.authLogin,
1173
+ [this.proto.dataField]: { token }
1174
+ });
1175
+ this.log.debug("[SharedWS] re-authenticated after reconnect");
1176
+ }
1177
+ for (const name of this.authChannels.keys()) {
1178
+ this.socket.send({
1179
+ [this.proto.eventField]: this.proto.channelJoin,
1180
+ [this.proto.dataField]: { channel: name }
1181
+ });
1182
+ }
1183
+ for (const topic of this.authTopics) {
1184
+ this.socket.send({
1185
+ [this.proto.eventField]: this.proto.topicSubscribe,
1186
+ [this.proto.dataField]: { topic }
1187
+ });
1188
+ }
1189
+ }
1071
1190
  handleLoseLeadership() {
1072
1191
  if (this.socket) {
1073
1192
  this.socket[Symbol.dispose]();
@@ -1087,6 +1206,8 @@ var SharedWebSocket = (_class6 = class {
1087
1206
  this.subs[Symbol.dispose]();
1088
1207
  this.bus[Symbol.dispose]();
1089
1208
  this.syncStore.clear();
1209
+ this.authChannels.clear();
1210
+ this.authTopics.clear();
1090
1211
  }
1091
1212
  }, _class6);
1092
1213
 
@@ -1098,4 +1219,4 @@ var SharedWebSocket = (_class6 = class {
1098
1219
 
1099
1220
 
1100
1221
  exports.MessageBus = MessageBus; exports.TabCoordinator = TabCoordinator; exports.SharedSocket = SharedSocket; exports.WorkerSocket = WorkerSocket; exports.SubscriptionManager = SubscriptionManager; exports.SharedWebSocket = SharedWebSocket;
1101
- //# sourceMappingURL=chunk-MJXKQYRZ.cjs.map
1222
+ //# sourceMappingURL=chunk-3DDE3RGB.cjs.map