@gwakko/shared-websocket 0.1.0

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.
Files changed (51) hide show
  1. package/LICENSE +9 -0
  2. package/README.md +381 -0
  3. package/dist/MessageBus.d.ts +20 -0
  4. package/dist/SharedSocket.d.ts +37 -0
  5. package/dist/SharedWebSocket.d.ts +45 -0
  6. package/dist/SubscriptionManager.d.ts +14 -0
  7. package/dist/TabCoordinator.d.ts +36 -0
  8. package/dist/WorkerSocket.d.ts +42 -0
  9. package/dist/adapters/index.d.ts +0 -0
  10. package/dist/adapters/react.d.ts +79 -0
  11. package/dist/adapters/vue.d.ts +53 -0
  12. package/dist/chunk-SMH3X34N.cjs +737 -0
  13. package/dist/chunk-SMH3X34N.cjs.map +1 -0
  14. package/dist/chunk-TNEMKPGP.js +737 -0
  15. package/dist/chunk-TNEMKPGP.js.map +1 -0
  16. package/dist/index.cjs +46 -0
  17. package/dist/index.cjs.map +1 -0
  18. package/dist/index.d.ts +8 -0
  19. package/dist/index.js +46 -0
  20. package/dist/index.js.map +1 -0
  21. package/dist/react.cjs +100 -0
  22. package/dist/react.cjs.map +1 -0
  23. package/dist/react.js +100 -0
  24. package/dist/react.js.map +1 -0
  25. package/dist/types.d.ts +27 -0
  26. package/dist/utils/backoff.d.ts +2 -0
  27. package/dist/utils/disposable.d.ts +0 -0
  28. package/dist/utils/id.d.ts +1 -0
  29. package/dist/vue.cjs +93 -0
  30. package/dist/vue.cjs.map +1 -0
  31. package/dist/vue.js +93 -0
  32. package/dist/vue.js.map +1 -0
  33. package/dist/withSocket.d.ts +51 -0
  34. package/dist/worker/socket.worker.d.ts +51 -0
  35. package/package.json +74 -0
  36. package/src/MessageBus.ts +112 -0
  37. package/src/SharedSocket.ts +183 -0
  38. package/src/SharedWebSocket.ts +225 -0
  39. package/src/SubscriptionManager.ts +86 -0
  40. package/src/TabCoordinator.ts +162 -0
  41. package/src/WorkerSocket.ts +149 -0
  42. package/src/adapters/index.ts +3 -0
  43. package/src/adapters/react.ts +189 -0
  44. package/src/adapters/vue.ts +149 -0
  45. package/src/index.ts +8 -0
  46. package/src/types.ts +29 -0
  47. package/src/utils/backoff.ts +9 -0
  48. package/src/utils/disposable.ts +4 -0
  49. package/src/utils/id.ts +6 -0
  50. package/src/withSocket.ts +89 -0
  51. package/src/worker/socket.worker.ts +205 -0
@@ -0,0 +1,737 @@
1
+ // src/utils/disposable.ts
2
+ if (typeof Symbol.dispose === "undefined") {
3
+ Symbol.dispose = /* @__PURE__ */ Symbol("Symbol.dispose");
4
+ }
5
+
6
+ // src/utils/id.ts
7
+ function generateId() {
8
+ if (typeof crypto !== "undefined" && crypto.randomUUID) {
9
+ return crypto.randomUUID();
10
+ }
11
+ return `${Date.now()}-${Math.random().toString(36).slice(2, 11)}`;
12
+ }
13
+
14
+ // src/MessageBus.ts
15
+ var MessageBus = class {
16
+ constructor(channelName, tabId) {
17
+ this.tabId = tabId;
18
+ this.channel = new BroadcastChannel(channelName);
19
+ this.channel.onmessage = (ev) => {
20
+ this.handleMessage(ev.data);
21
+ };
22
+ }
23
+ tabId;
24
+ channel;
25
+ listeners = /* @__PURE__ */ new Map();
26
+ pendingRequests = /* @__PURE__ */ new Map();
27
+ subscribe(topic, fn) {
28
+ const wrapper = (msg) => {
29
+ if (msg.source !== this.tabId) fn(msg.data);
30
+ };
31
+ this.addListener(topic, wrapper);
32
+ return () => this.removeListener(topic, wrapper);
33
+ }
34
+ publish(topic, data) {
35
+ this.postMessage({ topic, type: "publish", data });
36
+ }
37
+ broadcast(topic, data) {
38
+ const msg = this.createMessage(topic, "broadcast", data);
39
+ this.channel.postMessage(msg);
40
+ this.handleMessage(msg);
41
+ }
42
+ async request(topic, data, timeout = 5e3) {
43
+ const msg = this.createMessage(topic, "request", data);
44
+ return new Promise((resolve, reject) => {
45
+ const timer = setTimeout(() => {
46
+ this.pendingRequests.delete(msg.id);
47
+ reject(new Error(`MessageBus.request: timeout for topic "${topic}"`));
48
+ }, timeout);
49
+ this.pendingRequests.set(msg.id, { resolve, reject, timer });
50
+ this.channel.postMessage(msg);
51
+ });
52
+ }
53
+ respond(topic, fn) {
54
+ const wrapper = async (msg) => {
55
+ if (msg.type !== "request" || msg.source === this.tabId) return;
56
+ const result = await fn(msg.data);
57
+ this.postMessage({ topic, type: "response", data: { requestId: msg.id, result } });
58
+ };
59
+ this.addListener(topic, wrapper);
60
+ return () => this.removeListener(topic, wrapper);
61
+ }
62
+ handleMessage(msg) {
63
+ if (msg.type === "response") {
64
+ const payload = msg.data;
65
+ const pending = this.pendingRequests.get(payload.requestId);
66
+ if (pending) {
67
+ clearTimeout(pending.timer);
68
+ this.pendingRequests.delete(payload.requestId);
69
+ pending.resolve(payload.result);
70
+ return;
71
+ }
72
+ }
73
+ const listeners = this.listeners.get(msg.topic);
74
+ if (listeners) {
75
+ for (const fn of listeners) fn(msg);
76
+ }
77
+ }
78
+ postMessage(partial) {
79
+ this.channel.postMessage(this.createMessage(partial.topic, partial.type, partial.data));
80
+ }
81
+ createMessage(topic, type, data) {
82
+ return { id: generateId(), source: this.tabId, topic, type, data, timestamp: Date.now() };
83
+ }
84
+ addListener(topic, fn) {
85
+ let set = this.listeners.get(topic);
86
+ if (!set) {
87
+ set = /* @__PURE__ */ new Set();
88
+ this.listeners.set(topic, set);
89
+ }
90
+ set.add(fn);
91
+ }
92
+ removeListener(topic, fn) {
93
+ this.listeners.get(topic)?.delete(fn);
94
+ }
95
+ [Symbol.dispose]() {
96
+ for (const pending of this.pendingRequests.values()) {
97
+ clearTimeout(pending.timer);
98
+ pending.reject(new Error("MessageBus disposed"));
99
+ }
100
+ this.pendingRequests.clear();
101
+ this.listeners.clear();
102
+ this.channel.close();
103
+ }
104
+ };
105
+
106
+ // src/TabCoordinator.ts
107
+ var TabCoordinator = class {
108
+ constructor(bus, tabId, options = {}) {
109
+ this.bus = bus;
110
+ this.tabId = tabId;
111
+ this.electionTimeout = options.electionTimeout ?? 200;
112
+ this.heartbeatInterval = options.heartbeatInterval ?? 2e3;
113
+ this.leaderTimeout = options.leaderTimeout ?? 5e3;
114
+ this.cleanups.push(
115
+ this.bus.subscribe("coord:election", () => {
116
+ if (this._isLeader) {
117
+ this.bus.publish("coord:reject", { tabId: this.tabId });
118
+ }
119
+ })
120
+ );
121
+ this.cleanups.push(
122
+ this.bus.subscribe("coord:heartbeat", () => {
123
+ this.lastHeartbeat = Date.now();
124
+ })
125
+ );
126
+ this.cleanups.push(
127
+ this.bus.subscribe("coord:abdicate", () => {
128
+ if (!this._isLeader && !this.disposed) {
129
+ this.elect();
130
+ }
131
+ })
132
+ );
133
+ }
134
+ bus;
135
+ tabId;
136
+ _isLeader = false;
137
+ heartbeatTimer = null;
138
+ leaderCheckTimer = null;
139
+ lastHeartbeat = 0;
140
+ disposed = false;
141
+ onBecomeLeaderFns = /* @__PURE__ */ new Set();
142
+ onLoseLeadershipFns = /* @__PURE__ */ new Set();
143
+ cleanups = [];
144
+ electionTimeout;
145
+ heartbeatInterval;
146
+ leaderTimeout;
147
+ get isLeader() {
148
+ return this._isLeader;
149
+ }
150
+ async elect() {
151
+ if (this.disposed) return;
152
+ return new Promise((resolve) => {
153
+ let rejected = false;
154
+ const unsub = this.bus.subscribe("coord:reject", () => {
155
+ rejected = true;
156
+ unsub();
157
+ this.startLeaderCheck();
158
+ resolve();
159
+ });
160
+ this.bus.publish("coord:election", { tabId: this.tabId });
161
+ setTimeout(() => {
162
+ unsub();
163
+ if (!rejected && !this.disposed) {
164
+ this.becomeLeader();
165
+ }
166
+ resolve();
167
+ }, this.electionTimeout);
168
+ });
169
+ }
170
+ abdicate() {
171
+ if (!this._isLeader) return;
172
+ this._isLeader = false;
173
+ this.stopHeartbeat();
174
+ this.bus.publish("coord:abdicate", { tabId: this.tabId });
175
+ for (const fn of this.onLoseLeadershipFns) fn();
176
+ }
177
+ onBecomeLeader(fn) {
178
+ this.onBecomeLeaderFns.add(fn);
179
+ return () => this.onBecomeLeaderFns.delete(fn);
180
+ }
181
+ onLoseLeadership(fn) {
182
+ this.onLoseLeadershipFns.add(fn);
183
+ return () => this.onLoseLeadershipFns.delete(fn);
184
+ }
185
+ becomeLeader() {
186
+ this._isLeader = true;
187
+ this.stopLeaderCheck();
188
+ this.startHeartbeat();
189
+ for (const fn of this.onBecomeLeaderFns) fn();
190
+ }
191
+ startHeartbeat() {
192
+ this.stopHeartbeat();
193
+ this.heartbeatTimer = setInterval(() => {
194
+ this.bus.publish("coord:heartbeat", { tabId: this.tabId });
195
+ }, this.heartbeatInterval);
196
+ this.bus.publish("coord:heartbeat", { tabId: this.tabId });
197
+ }
198
+ stopHeartbeat() {
199
+ if (this.heartbeatTimer) {
200
+ clearInterval(this.heartbeatTimer);
201
+ this.heartbeatTimer = null;
202
+ }
203
+ }
204
+ startLeaderCheck() {
205
+ this.stopLeaderCheck();
206
+ this.lastHeartbeat = Date.now();
207
+ this.leaderCheckTimer = setInterval(() => {
208
+ if (Date.now() - this.lastHeartbeat > this.leaderTimeout && !this.disposed) {
209
+ this.stopLeaderCheck();
210
+ this.elect();
211
+ }
212
+ }, 1e3);
213
+ }
214
+ stopLeaderCheck() {
215
+ if (this.leaderCheckTimer) {
216
+ clearInterval(this.leaderCheckTimer);
217
+ this.leaderCheckTimer = null;
218
+ }
219
+ }
220
+ [Symbol.dispose]() {
221
+ this.disposed = true;
222
+ if (this._isLeader) {
223
+ this.abdicate();
224
+ }
225
+ this.stopHeartbeat();
226
+ this.stopLeaderCheck();
227
+ for (const unsub of this.cleanups) unsub();
228
+ this.cleanups = [];
229
+ this.onBecomeLeaderFns.clear();
230
+ this.onLoseLeadershipFns.clear();
231
+ }
232
+ };
233
+
234
+ // src/utils/backoff.ts
235
+ function* backoff(base = 1e3, max = 3e4) {
236
+ let delay = base;
237
+ while (true) {
238
+ const jitter = delay * 0.25 * (Math.random() * 2 - 1);
239
+ yield Math.min(delay + jitter, max);
240
+ delay = Math.min(delay * 2, max);
241
+ }
242
+ }
243
+
244
+ // src/SharedSocket.ts
245
+ var SharedSocket = class {
246
+ constructor(url, options = {}) {
247
+ this.url = url;
248
+ this.opts = {
249
+ protocols: options.protocols ?? [],
250
+ reconnect: options.reconnect ?? true,
251
+ reconnectMaxDelay: options.reconnectMaxDelay ?? 3e4,
252
+ heartbeatInterval: options.heartbeatInterval ?? 3e4,
253
+ sendBuffer: options.sendBuffer ?? 100,
254
+ auth: options.auth
255
+ };
256
+ }
257
+ url;
258
+ ws = null;
259
+ _state = "closed";
260
+ buffer = [];
261
+ disposed = false;
262
+ heartbeatTimer = null;
263
+ reconnectTimer = null;
264
+ onMessageFns = /* @__PURE__ */ new Set();
265
+ onStateChangeFns = /* @__PURE__ */ new Set();
266
+ opts;
267
+ get state() {
268
+ return this._state;
269
+ }
270
+ async connect() {
271
+ if (this.disposed) return;
272
+ 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
+ }
279
+ this.ws = new WebSocket(connectUrl, this.opts.protocols);
280
+ this.ws.onopen = () => {
281
+ this.setState("connected");
282
+ this.flushBuffer();
283
+ this.startHeartbeat();
284
+ };
285
+ this.ws.onmessage = (ev) => {
286
+ let data;
287
+ try {
288
+ data = JSON.parse(ev.data);
289
+ } catch {
290
+ data = ev.data;
291
+ }
292
+ for (const fn of this.onMessageFns) fn(data);
293
+ };
294
+ this.ws.onclose = () => {
295
+ this.stopHeartbeat();
296
+ if (!this.disposed && this.opts.reconnect) {
297
+ this.reconnect();
298
+ } else {
299
+ this.setState("closed");
300
+ }
301
+ };
302
+ this.ws.onerror = () => {
303
+ };
304
+ }
305
+ disconnect() {
306
+ this.disposed = true;
307
+ this.stopHeartbeat();
308
+ this.clearReconnect();
309
+ if (this.ws) {
310
+ this.ws.onclose = null;
311
+ this.ws.onmessage = null;
312
+ this.ws.onerror = null;
313
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
314
+ this.ws.close(1e3, "client disconnect");
315
+ }
316
+ this.ws = null;
317
+ }
318
+ this.setState("closed");
319
+ }
320
+ send(data) {
321
+ if (this._state === "connected" && this.ws?.readyState === WebSocket.OPEN) {
322
+ this.ws.send(JSON.stringify(data));
323
+ } else if (this._state === "reconnecting" || this._state === "connecting") {
324
+ if (this.buffer.length < this.opts.sendBuffer) {
325
+ this.buffer.push(data);
326
+ }
327
+ }
328
+ }
329
+ onMessage(fn) {
330
+ this.onMessageFns.add(fn);
331
+ return () => this.onMessageFns.delete(fn);
332
+ }
333
+ onStateChange(fn) {
334
+ this.onStateChangeFns.add(fn);
335
+ return () => this.onStateChangeFns.delete(fn);
336
+ }
337
+ reconnect() {
338
+ this.setState("reconnecting");
339
+ const gen = backoff(1e3, this.opts.reconnectMaxDelay);
340
+ const attempt = () => {
341
+ if (this.disposed) return;
342
+ const delay = gen.next().value;
343
+ this.reconnectTimer = setTimeout(() => {
344
+ if (!this.disposed) this.connect();
345
+ }, delay);
346
+ };
347
+ attempt();
348
+ }
349
+ flushBuffer() {
350
+ const pending = this.buffer.splice(0);
351
+ for (const item of pending) {
352
+ this.send(item);
353
+ }
354
+ }
355
+ startHeartbeat() {
356
+ this.stopHeartbeat();
357
+ this.heartbeatTimer = setInterval(() => {
358
+ if (this.ws?.readyState === WebSocket.OPEN) {
359
+ this.ws.send(JSON.stringify({ type: "ping" }));
360
+ }
361
+ }, this.opts.heartbeatInterval);
362
+ }
363
+ stopHeartbeat() {
364
+ if (this.heartbeatTimer) {
365
+ clearInterval(this.heartbeatTimer);
366
+ this.heartbeatTimer = null;
367
+ }
368
+ }
369
+ clearReconnect() {
370
+ if (this.reconnectTimer) {
371
+ clearTimeout(this.reconnectTimer);
372
+ this.reconnectTimer = null;
373
+ }
374
+ }
375
+ setState(state) {
376
+ this._state = state;
377
+ for (const fn of this.onStateChangeFns) fn(state);
378
+ }
379
+ [Symbol.dispose]() {
380
+ this.disconnect();
381
+ this.onMessageFns.clear();
382
+ this.onStateChangeFns.clear();
383
+ this.buffer = [];
384
+ }
385
+ };
386
+
387
+ // src/WorkerSocket.ts
388
+ var WorkerSocket = class {
389
+ constructor(url, options = {}) {
390
+ this.url = url;
391
+ this.options = options;
392
+ }
393
+ url;
394
+ options;
395
+ worker = null;
396
+ _state = "closed";
397
+ onMessageFns = /* @__PURE__ */ new Set();
398
+ onStateChangeFns = /* @__PURE__ */ new Set();
399
+ get state() {
400
+ return this._state;
401
+ }
402
+ connect() {
403
+ const workerUrl = this.options.workerUrl ?? this.createWorkerBlob();
404
+ this.worker = new Worker(workerUrl, { type: "module" });
405
+ this.worker.onmessage = (ev) => {
406
+ const msg = ev.data;
407
+ switch (msg.type) {
408
+ case "state":
409
+ this._state = msg.state;
410
+ for (const fn of this.onStateChangeFns) fn(msg.state);
411
+ break;
412
+ case "message":
413
+ for (const fn of this.onMessageFns) fn(msg.data);
414
+ break;
415
+ case "open":
416
+ break;
417
+ case "close":
418
+ break;
419
+ case "error":
420
+ console.error("WorkerSocket error:", msg.message);
421
+ break;
422
+ }
423
+ };
424
+ this.worker.postMessage({
425
+ type: "connect",
426
+ url: this.url,
427
+ protocols: this.options.protocols ?? [],
428
+ reconnect: this.options.reconnect ?? true,
429
+ reconnectMaxDelay: this.options.reconnectMaxDelay ?? 3e4,
430
+ heartbeatInterval: this.options.heartbeatInterval ?? 3e4,
431
+ bufferSize: this.options.sendBuffer ?? 100
432
+ });
433
+ }
434
+ send(data) {
435
+ this.worker?.postMessage({ type: "send", data });
436
+ }
437
+ disconnect() {
438
+ this.worker?.postMessage({ type: "disconnect" });
439
+ setTimeout(() => {
440
+ this.worker?.terminate();
441
+ this.worker = null;
442
+ }, 100);
443
+ this._state = "closed";
444
+ }
445
+ onMessage(fn) {
446
+ this.onMessageFns.add(fn);
447
+ return () => this.onMessageFns.delete(fn);
448
+ }
449
+ onStateChange(fn) {
450
+ this.onStateChangeFns.add(fn);
451
+ return () => this.onStateChangeFns.delete(fn);
452
+ }
453
+ createWorkerBlob() {
454
+ const code = `
455
+ let ws = null, state = 'closed', buffer = [], disposed = false;
456
+ let heartbeatTimer = null, reconnectTimer = null;
457
+ let url = '', protocols = [], shouldReconnect = true;
458
+ let maxDelay = 30000, hbInterval = 30000, maxBuf = 100, delay = 1000;
459
+
460
+ function setState(s) { state = s; self.postMessage({ type: 'state', state: s }); }
461
+ function connect() {
462
+ if (disposed) return;
463
+ setState('connecting');
464
+ ws = new WebSocket(url, protocols);
465
+ ws.onopen = () => { setState('connected'); delay = 1000; self.postMessage({ type: 'open' }); flush(); startHB(); };
466
+ ws.onmessage = (e) => { let d; try { d = JSON.parse(e.data); } catch { d = e.data; } self.postMessage({ type: 'message', data: d }); };
467
+ ws.onclose = (e) => { stopHB(); self.postMessage({ type: 'close', code: e.code, reason: e.reason }); if (!disposed && shouldReconnect && e.code !== 1000) reconnect(); else setState('closed'); };
468
+ ws.onerror = () => { self.postMessage({ type: 'error', message: 'error' }); };
469
+ }
470
+ function send(d) { if (state === 'connected' && ws?.readyState === 1) ws.send(JSON.stringify(d)); else if (buffer.length < maxBuf) buffer.push(d); }
471
+ function flush() { const p = buffer.splice(0); p.forEach(send); }
472
+ function startHB() { stopHB(); heartbeatTimer = setInterval(() => { if (ws?.readyState === 1) ws.send('{"type":"ping"}'); }, hbInterval); }
473
+ function stopHB() { if (heartbeatTimer) { clearInterval(heartbeatTimer); heartbeatTimer = null; } }
474
+ function reconnect() { setState('reconnecting'); const j = delay * 0.25 * (Math.random() * 2 - 1); reconnectTimer = setTimeout(() => { if (!disposed) connect(); }, Math.min(delay + j, maxDelay)); delay = Math.min(delay * 2, maxDelay); }
475
+ self.onmessage = (e) => {
476
+ const c = e.data;
477
+ if (c.type === 'connect') { url = c.url; protocols = c.protocols || []; shouldReconnect = c.reconnect ?? true; maxDelay = c.reconnectMaxDelay || 30000; hbInterval = c.heartbeatInterval || 30000; maxBuf = c.bufferSize || 100; connect(); }
478
+ if (c.type === 'send') send(c.data);
479
+ 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'); }
480
+ };
481
+ `;
482
+ const blob = new Blob([code], { type: "application/javascript" });
483
+ return new URL(URL.createObjectURL(blob));
484
+ }
485
+ [Symbol.dispose]() {
486
+ this.disconnect();
487
+ this.onMessageFns.clear();
488
+ this.onStateChangeFns.clear();
489
+ }
490
+ };
491
+
492
+ // src/SubscriptionManager.ts
493
+ var SubscriptionManager = class {
494
+ handlers = /* @__PURE__ */ new Map();
495
+ lastMessages = /* @__PURE__ */ new Map();
496
+ on(event, handler) {
497
+ let set = this.handlers.get(event);
498
+ if (!set) {
499
+ set = /* @__PURE__ */ new Set();
500
+ this.handlers.set(event, set);
501
+ }
502
+ set.add(handler);
503
+ return () => set.delete(handler);
504
+ }
505
+ once(event, handler) {
506
+ const wrapper = (data) => {
507
+ unsub();
508
+ handler(data);
509
+ };
510
+ const unsub = this.on(event, wrapper);
511
+ return unsub;
512
+ }
513
+ off(event, handler) {
514
+ if (handler) {
515
+ this.handlers.get(event)?.delete(handler);
516
+ } else {
517
+ this.handlers.delete(event);
518
+ }
519
+ }
520
+ emit(event, data) {
521
+ this.lastMessages.set(event, data);
522
+ const set = this.handlers.get(event);
523
+ if (set) {
524
+ for (const fn of set) fn(data);
525
+ }
526
+ }
527
+ getLastMessage(event) {
528
+ return this.lastMessages.get(event);
529
+ }
530
+ async *stream(event, signal) {
531
+ const queue = [];
532
+ let resolve = null;
533
+ let done = false;
534
+ const unsub = this.on(event, (data) => {
535
+ queue.push(data);
536
+ resolve?.();
537
+ });
538
+ const onAbort = () => {
539
+ done = true;
540
+ resolve?.();
541
+ };
542
+ signal?.addEventListener("abort", onAbort);
543
+ try {
544
+ while (!done) {
545
+ if (queue.length > 0) {
546
+ yield queue.shift();
547
+ } else {
548
+ await new Promise((r) => {
549
+ resolve = r;
550
+ });
551
+ resolve = null;
552
+ }
553
+ }
554
+ } finally {
555
+ unsub();
556
+ signal?.removeEventListener("abort", onAbort);
557
+ }
558
+ }
559
+ offAll() {
560
+ this.handlers.clear();
561
+ this.lastMessages.clear();
562
+ }
563
+ [Symbol.dispose]() {
564
+ this.offAll();
565
+ }
566
+ };
567
+
568
+ // src/SharedWebSocket.ts
569
+ var SharedWebSocket = class {
570
+ constructor(url, options = {}) {
571
+ this.url = url;
572
+ this.options = options;
573
+ this.tabId = generateId();
574
+ this.bus = new MessageBus("shared-ws", this.tabId);
575
+ this.coordinator = new TabCoordinator(this.bus, this.tabId, {
576
+ electionTimeout: options.electionTimeout,
577
+ heartbeatInterval: options.leaderHeartbeat,
578
+ leaderTimeout: options.leaderTimeout
579
+ });
580
+ this.cleanups.push(
581
+ this.bus.subscribe("ws:message", (msg) => {
582
+ this.subs.emit(msg.event, msg.data);
583
+ })
584
+ );
585
+ this.cleanups.push(
586
+ this.bus.subscribe("ws:send", (msg) => {
587
+ if (this.coordinator.isLeader && this.socket) {
588
+ this.socket.send({ event: msg.event, data: msg.data });
589
+ }
590
+ })
591
+ );
592
+ this.cleanups.push(
593
+ this.bus.subscribe("ws:sync", (msg) => {
594
+ this.syncStore.set(msg.key, msg.value);
595
+ this.subs.emit(`sync:${msg.key}`, msg.value);
596
+ })
597
+ );
598
+ this.coordinator.onBecomeLeader(() => this.onBecomeLeader());
599
+ this.coordinator.onLoseLeadership(() => this.onLoseLeadership());
600
+ if (typeof window !== "undefined") {
601
+ const onBeforeUnload = () => this[Symbol.dispose]();
602
+ window.addEventListener("beforeunload", onBeforeUnload);
603
+ this.cleanups.push(() => window.removeEventListener("beforeunload", onBeforeUnload));
604
+ }
605
+ }
606
+ url;
607
+ options;
608
+ bus;
609
+ coordinator;
610
+ socket = null;
611
+ subs = new SubscriptionManager();
612
+ syncStore = /* @__PURE__ */ new Map();
613
+ tabId;
614
+ cleanups = [];
615
+ disposed = false;
616
+ get connected() {
617
+ return this.socket?.state === "connected" || !this.coordinator.isLeader;
618
+ }
619
+ get tabRole() {
620
+ return this.coordinator.isLeader ? "leader" : "follower";
621
+ }
622
+ /** Start leader election and connect. */
623
+ async connect() {
624
+ await this.coordinator.elect();
625
+ }
626
+ /** Subscribe to server events (works in ALL tabs). */
627
+ on(event, handler) {
628
+ return this.subs.on(event, handler);
629
+ }
630
+ once(event, handler) {
631
+ return this.subs.once(event, handler);
632
+ }
633
+ off(event, handler) {
634
+ this.subs.off(event, handler);
635
+ }
636
+ /** Async generator for consuming events. */
637
+ stream(event, signal) {
638
+ return this.subs.stream(event, signal);
639
+ }
640
+ /** Send message to server (auto-routed through leader). */
641
+ send(event, data) {
642
+ if (this.coordinator.isLeader && this.socket) {
643
+ this.socket.send({ event, data });
644
+ } else {
645
+ this.bus.publish("ws:send", { event, data });
646
+ }
647
+ }
648
+ /** Request/response through server via leader. */
649
+ async request(event, data, timeout = 5e3) {
650
+ return this.bus.request("ws:request", { event, data }, timeout);
651
+ }
652
+ /** Sync state across tabs (no server roundtrip). */
653
+ sync(key, value) {
654
+ this.syncStore.set(key, value);
655
+ this.bus.broadcast("ws:sync", { key, value });
656
+ }
657
+ getSync(key) {
658
+ return this.syncStore.get(key);
659
+ }
660
+ onSync(key, fn) {
661
+ return this.subs.on(`sync:${key}`, fn);
662
+ }
663
+ disconnect() {
664
+ this[Symbol.dispose]();
665
+ }
666
+ createSocket() {
667
+ const socketOptions = {
668
+ protocols: this.options.protocols,
669
+ reconnect: this.options.reconnect,
670
+ reconnectMaxDelay: this.options.reconnectMaxDelay,
671
+ heartbeatInterval: this.options.heartbeatInterval,
672
+ sendBuffer: this.options.sendBuffer
673
+ };
674
+ if (this.options.useWorker) {
675
+ return new WorkerSocket(this.url, {
676
+ ...socketOptions,
677
+ workerUrl: this.options.workerUrl
678
+ });
679
+ }
680
+ return new SharedSocket(this.url, {
681
+ ...socketOptions,
682
+ auth: this.options.auth
683
+ });
684
+ }
685
+ onBecomeLeader() {
686
+ this.socket = this.createSocket();
687
+ this.socket.onMessage((data) => {
688
+ const event = data?.event ?? "message";
689
+ const payload = data?.data ?? data;
690
+ this.bus.broadcast("ws:message", { event, data: payload });
691
+ });
692
+ this.cleanups.push(
693
+ this.bus.respond("ws:request", async (req) => {
694
+ return new Promise((resolve) => {
695
+ const unsub = this.socket.onMessage((response) => {
696
+ if (response?.event === req.event || response?.requestId) {
697
+ unsub();
698
+ resolve(response?.data ?? response);
699
+ }
700
+ });
701
+ this.socket.send({ event: req.event, data: req.data });
702
+ });
703
+ })
704
+ );
705
+ this.socket.connect();
706
+ }
707
+ onLoseLeadership() {
708
+ if (this.socket) {
709
+ this.socket[Symbol.dispose]();
710
+ this.socket = null;
711
+ }
712
+ }
713
+ [Symbol.dispose]() {
714
+ if (this.disposed) return;
715
+ this.disposed = true;
716
+ this.coordinator[Symbol.dispose]();
717
+ if (this.socket) {
718
+ this.socket[Symbol.dispose]();
719
+ this.socket = null;
720
+ }
721
+ for (const unsub of this.cleanups) unsub();
722
+ this.cleanups = [];
723
+ this.subs[Symbol.dispose]();
724
+ this.bus[Symbol.dispose]();
725
+ this.syncStore.clear();
726
+ }
727
+ };
728
+
729
+ export {
730
+ MessageBus,
731
+ TabCoordinator,
732
+ SharedSocket,
733
+ WorkerSocket,
734
+ SubscriptionManager,
735
+ SharedWebSocket
736
+ };
737
+ //# sourceMappingURL=chunk-TNEMKPGP.js.map