@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
+ "use strict";Object.defineProperty(exports, "__esModule", {value: true}); function _nullishCoalesce(lhs, rhsFn) { if (lhs != null) { return lhs; } else { return rhsFn(); } } function _optionalChain(ops) { let lastAccessLHS = undefined; let value = ops[0]; let i = 1; while (i < ops.length) { const op = ops[i]; const fn = ops[i + 1]; i += 2; if ((op === 'optionalAccess' || op === 'optionalCall') && value == null) { return undefined; } if (op === 'access' || op === 'optionalAccess') { lastAccessLHS = value; value = fn(value); } else if (op === 'call' || op === 'optionalCall') { value = fn((...args) => value.call(lastAccessLHS, ...args)); lastAccessLHS = undefined; } } return value; } var _class; var _class2; var _class3; var _class4; var _class5; var _class6;// 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 = class {
16
+ constructor(channelName, tabId) {;_class.prototype.__init.call(this);_class.prototype.__init2.call(this);
17
+ this.tabId = tabId;
18
+ this.channel = new BroadcastChannel(channelName);
19
+ this.channel.onmessage = (ev) => {
20
+ this.handleMessage(ev.data);
21
+ };
22
+ }
23
+
24
+
25
+ __init() {this.listeners = /* @__PURE__ */ new Map()}
26
+ __init2() {this.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
+ _optionalChain([this, 'access', _ => _.listeners, 'access', _2 => _2.get, 'call', _3 => _3(topic), 'optionalAccess', _4 => _4.delete, 'call', _5 => _5(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
+ }, _class);
105
+
106
+ // src/TabCoordinator.ts
107
+ var TabCoordinator = (_class2 = class {
108
+ constructor(bus, tabId, options = {}) {;_class2.prototype.__init3.call(this);_class2.prototype.__init4.call(this);_class2.prototype.__init5.call(this);_class2.prototype.__init6.call(this);_class2.prototype.__init7.call(this);_class2.prototype.__init8.call(this);_class2.prototype.__init9.call(this);_class2.prototype.__init10.call(this);
109
+ this.bus = bus;
110
+ this.tabId = tabId;
111
+ this.electionTimeout = _nullishCoalesce(options.electionTimeout, () => ( 200));
112
+ this.heartbeatInterval = _nullishCoalesce(options.heartbeatInterval, () => ( 2e3));
113
+ this.leaderTimeout = _nullishCoalesce(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
+
135
+
136
+ __init3() {this._isLeader = false}
137
+ __init4() {this.heartbeatTimer = null}
138
+ __init5() {this.leaderCheckTimer = null}
139
+ __init6() {this.lastHeartbeat = 0}
140
+ __init7() {this.disposed = false}
141
+ __init8() {this.onBecomeLeaderFns = /* @__PURE__ */ new Set()}
142
+ __init9() {this.onLoseLeadershipFns = /* @__PURE__ */ new Set()}
143
+ __init10() {this.cleanups = []}
144
+
145
+
146
+
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
+ }, _class2);
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 = (_class3 = class {
246
+ constructor(url, options = {}) {;_class3.prototype.__init11.call(this);_class3.prototype.__init12.call(this);_class3.prototype.__init13.call(this);_class3.prototype.__init14.call(this);_class3.prototype.__init15.call(this);_class3.prototype.__init16.call(this);_class3.prototype.__init17.call(this);_class3.prototype.__init18.call(this);
247
+ this.url = url;
248
+ this.opts = {
249
+ protocols: _nullishCoalesce(options.protocols, () => ( [])),
250
+ reconnect: _nullishCoalesce(options.reconnect, () => ( true)),
251
+ reconnectMaxDelay: _nullishCoalesce(options.reconnectMaxDelay, () => ( 3e4)),
252
+ heartbeatInterval: _nullishCoalesce(options.heartbeatInterval, () => ( 3e4)),
253
+ sendBuffer: _nullishCoalesce(options.sendBuffer, () => ( 100)),
254
+ auth: options.auth
255
+ };
256
+ }
257
+
258
+ __init11() {this.ws = null}
259
+ __init12() {this._state = "closed"}
260
+ __init13() {this.buffer = []}
261
+ __init14() {this.disposed = false}
262
+ __init15() {this.heartbeatTimer = null}
263
+ __init16() {this.reconnectTimer = null}
264
+ __init17() {this.onMessageFns = /* @__PURE__ */ new Set()}
265
+ __init18() {this.onStateChangeFns = /* @__PURE__ */ new Set()}
266
+
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 (e) {
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" && _optionalChain([this, 'access', _6 => _6.ws, 'optionalAccess', _7 => _7.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 (_optionalChain([this, 'access', _8 => _8.ws, 'optionalAccess', _9 => _9.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
+ }, _class3);
386
+
387
+ // src/WorkerSocket.ts
388
+ var WorkerSocket = (_class4 = class {
389
+ constructor(url, options = {}) {;_class4.prototype.__init19.call(this);_class4.prototype.__init20.call(this);_class4.prototype.__init21.call(this);_class4.prototype.__init22.call(this);
390
+ this.url = url;
391
+ this.options = options;
392
+ }
393
+
394
+
395
+ __init19() {this.worker = null}
396
+ __init20() {this._state = "closed"}
397
+ __init21() {this.onMessageFns = /* @__PURE__ */ new Set()}
398
+ __init22() {this.onStateChangeFns = /* @__PURE__ */ new Set()}
399
+ get state() {
400
+ return this._state;
401
+ }
402
+ connect() {
403
+ const workerUrl = _nullishCoalesce(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: _nullishCoalesce(this.options.protocols, () => ( [])),
428
+ reconnect: _nullishCoalesce(this.options.reconnect, () => ( true)),
429
+ reconnectMaxDelay: _nullishCoalesce(this.options.reconnectMaxDelay, () => ( 3e4)),
430
+ heartbeatInterval: _nullishCoalesce(this.options.heartbeatInterval, () => ( 3e4)),
431
+ bufferSize: _nullishCoalesce(this.options.sendBuffer, () => ( 100))
432
+ });
433
+ }
434
+ send(data) {
435
+ _optionalChain([this, 'access', _10 => _10.worker, 'optionalAccess', _11 => _11.postMessage, 'call', _12 => _12({ type: "send", data })]);
436
+ }
437
+ disconnect() {
438
+ _optionalChain([this, 'access', _13 => _13.worker, 'optionalAccess', _14 => _14.postMessage, 'call', _15 => _15({ type: "disconnect" })]);
439
+ setTimeout(() => {
440
+ _optionalChain([this, 'access', _16 => _16.worker, 'optionalAccess', _17 => _17.terminate, 'call', _18 => _18()]);
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
+ }, _class4);
491
+
492
+ // src/SubscriptionManager.ts
493
+ var SubscriptionManager = (_class5 = class {constructor() { _class5.prototype.__init23.call(this);_class5.prototype.__init24.call(this); }
494
+ __init23() {this.handlers = /* @__PURE__ */ new Map()}
495
+ __init24() {this.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
+ _optionalChain([this, 'access', _19 => _19.handlers, 'access', _20 => _20.get, 'call', _21 => _21(event), 'optionalAccess', _22 => _22.delete, 'call', _23 => _23(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
+ _optionalChain([resolve, 'optionalCall', _24 => _24()]);
537
+ });
538
+ const onAbort = () => {
539
+ done = true;
540
+ _optionalChain([resolve, 'optionalCall', _25 => _25()]);
541
+ };
542
+ _optionalChain([signal, 'optionalAccess', _26 => _26.addEventListener, 'call', _27 => _27("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
+ _optionalChain([signal, 'optionalAccess', _28 => _28.removeEventListener, 'call', _29 => _29("abort", onAbort)]);
557
+ }
558
+ }
559
+ offAll() {
560
+ this.handlers.clear();
561
+ this.lastMessages.clear();
562
+ }
563
+ [Symbol.dispose]() {
564
+ this.offAll();
565
+ }
566
+ }, _class5);
567
+
568
+ // src/SharedWebSocket.ts
569
+ var SharedWebSocket = (_class6 = class {
570
+ 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);
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
+
607
+
608
+
609
+
610
+ __init25() {this.socket = null}
611
+ __init26() {this.subs = new SubscriptionManager()}
612
+ __init27() {this.syncStore = /* @__PURE__ */ new Map()}
613
+
614
+ __init28() {this.cleanups = []}
615
+ __init29() {this.disposed = false}
616
+ get connected() {
617
+ return _optionalChain([this, 'access', _30 => _30.socket, 'optionalAccess', _31 => _31.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 = _nullishCoalesce(_optionalChain([data, 'optionalAccess', _32 => _32.event]), () => ( "message"));
689
+ const payload = _nullishCoalesce(_optionalChain([data, 'optionalAccess', _33 => _33.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 (_optionalChain([response, 'optionalAccess', _34 => _34.event]) === req.event || _optionalChain([response, 'optionalAccess', _35 => _35.requestId])) {
697
+ unsub();
698
+ resolve(_nullishCoalesce(_optionalChain([response, 'optionalAccess', _36 => _36.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
+ }, _class6);
728
+
729
+
730
+
731
+
732
+
733
+
734
+
735
+
736
+ exports.MessageBus = MessageBus; exports.TabCoordinator = TabCoordinator; exports.SharedSocket = SharedSocket; exports.WorkerSocket = WorkerSocket; exports.SubscriptionManager = SubscriptionManager; exports.SharedWebSocket = SharedWebSocket;
737
+ //# sourceMappingURL=chunk-SMH3X34N.cjs.map