@use-everywhere/core 0.4.1 → 0.6.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.
package/dist/index.js CHANGED
@@ -7,7 +7,13 @@ function emitBusEvent(name, direction, wire) {
7
7
  const set = observers.get(name);
8
8
  if (!set) return;
9
9
  const event = { name, direction, wire };
10
- for (const fn of set) fn(event);
10
+ for (const fn of set) {
11
+ try {
12
+ fn(event);
13
+ } catch (error) {
14
+ console.error(`[use-everywhere] a bus observer for "${name}" threw`, error);
15
+ }
16
+ }
11
17
  }
12
18
  function observeBus(name, fn) {
13
19
  let set = observers.get(name);
@@ -32,12 +38,41 @@ function enableDebug(options = {}) {
32
38
  });
33
39
  }
34
40
 
41
+ // src/dev.ts
42
+ var inDev = false;
43
+ try {
44
+ inDev = process.env.NODE_ENV !== "production";
45
+ } catch {
46
+ }
47
+ var warned = /* @__PURE__ */ new Set();
48
+ function devWarn(message) {
49
+ if (!inDev || warned.has(message)) return;
50
+ warned.add(message);
51
+ console.warn(message);
52
+ }
53
+
35
54
  // src/ids.ts
55
+ function randomHex(bytes) {
56
+ const crypto = globalThis.crypto;
57
+ if (crypto?.getRandomValues) {
58
+ const buf = crypto.getRandomValues(new Uint8Array(bytes));
59
+ let out2 = "";
60
+ for (const b of buf) out2 += b.toString(16).padStart(2, "0");
61
+ return out2;
62
+ }
63
+ devWarn(
64
+ "[use-everywhere] crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
65
+ );
66
+ let out = "";
67
+ while (out.length < bytes * 2)
68
+ out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
69
+ return out;
70
+ }
36
71
  function newClientId() {
37
- return Math.random().toString(36).slice(2, 8);
72
+ return randomHex(8);
38
73
  }
39
74
  function newMsgId() {
40
- return Math.random().toString(36).slice(2, 10);
75
+ return randomHex(16);
41
76
  }
42
77
 
43
78
  // src/transport/broadcast-channel-transport.ts
@@ -117,12 +152,20 @@ function createBus(name, options, onShutdown) {
117
152
  options.heartbeatMs ?? 2e3
118
153
  );
119
154
  const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
155
+ const onPageShow = (event) => {
156
+ if (!event.persisted) return;
157
+ post({ v: 1, scope: "presence", type: "hello", clientId, kind });
158
+ };
120
159
  const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
121
- if (hasWindow) addEventListener("pagehide", sayBye);
160
+ if (hasWindow) {
161
+ addEventListener("pagehide", sayBye);
162
+ addEventListener("pageshow", onPageShow);
163
+ }
122
164
  return {
123
165
  name,
124
166
  clientId,
125
167
  kind,
168
+ heartbeatMs: options.heartbeatMs ?? 2e3,
126
169
  post,
127
170
  subscribe(fn) {
128
171
  listeners.add(fn);
@@ -132,12 +175,16 @@ function createBus(name, options, onShutdown) {
132
175
  refs++;
133
176
  },
134
177
  release() {
178
+ if (closed) return;
135
179
  refs--;
136
180
  if (refs > 0) return;
137
181
  sayBye();
138
182
  closed = true;
139
183
  clearInterval(heartbeat);
140
- if (hasWindow) removeEventListener("pagehide", sayBye);
184
+ if (hasWindow) {
185
+ removeEventListener("pagehide", sayBye);
186
+ removeEventListener("pageshow", onPageShow);
187
+ }
141
188
  unsubscribe();
142
189
  transport.close();
143
190
  listeners.clear();
@@ -159,6 +206,15 @@ function getBus(name, options = {}) {
159
206
  if (!bus) {
160
207
  bus = createBus(name, options, () => registry.delete(name));
161
208
  registry.set(name, bus);
209
+ } else {
210
+ if (options.heartbeatMs !== void 0 && options.heartbeatMs !== bus.heartbeatMs) {
211
+ devWarn(
212
+ `[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
213
+ );
214
+ }
215
+ if (options.kind !== void 0 && options.kind !== bus.kind) {
216
+ devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
217
+ }
162
218
  }
163
219
  bus.acquire();
164
220
  return bus;
@@ -175,6 +231,7 @@ function createChannel(name, options = {}) {
175
231
  const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
176
232
  for (const fn of set) fn(wire.payload, meta);
177
233
  });
234
+ let closed = false;
178
235
  return {
179
236
  name,
180
237
  clientId: bus.clientId,
@@ -199,6 +256,8 @@ function createChannel(name, options = {}) {
199
256
  return () => set.delete(handler);
200
257
  },
201
258
  close() {
259
+ if (closed) return;
260
+ closed = true;
202
261
  unsubscribe();
203
262
  handlers.clear();
204
263
  bus.release();
@@ -212,25 +271,48 @@ function newer(a, b) {
212
271
  }
213
272
 
214
273
  // src/dev-freeze.ts
215
- var inDev = false;
274
+ var inDev2 = false;
216
275
  try {
217
- inDev = process.env.NODE_ENV !== "production";
276
+ inDev2 = process.env.NODE_ENV !== "production";
218
277
  } catch {
219
278
  }
220
279
  function deepFreeze(value) {
221
280
  if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
281
+ if (ArrayBuffer.isView(value)) return;
222
282
  Object.freeze(value);
283
+ if (value instanceof Map) {
284
+ for (const [k, v] of value) {
285
+ deepFreeze(k);
286
+ deepFreeze(v);
287
+ }
288
+ return;
289
+ }
290
+ if (value instanceof Set) {
291
+ for (const entry of value) deepFreeze(entry);
292
+ return;
293
+ }
223
294
  for (const key of Object.keys(value)) {
224
295
  deepFreeze(value[key]);
225
296
  }
226
297
  }
227
298
  function freezeShared(value) {
228
- if (inDev) deepFreeze(value);
299
+ if (inDev2) deepFreeze(value);
229
300
  return value;
230
301
  }
231
302
 
232
303
  // src/shared-store.ts
304
+ var liveStores = /* @__PURE__ */ new Map();
233
305
  function createSharedStore(name, initial, options = {}) {
306
+ const onSharedBus = !options.transport;
307
+ if (onSharedBus) {
308
+ const live = liveStores.get(name) ?? 0;
309
+ if (live > 0) {
310
+ devWarn(
311
+ `[use-everywhere] second shared store for "${name}" in this tab \u2014 stores on one page never hear each other and will diverge. Reuse one per name.`
312
+ );
313
+ }
314
+ liveStores.set(name, live + 1);
315
+ }
234
316
  const bus = getBus(name, options);
235
317
  const clientId = bus.clientId;
236
318
  const accept = options.accept;
@@ -284,18 +366,25 @@ function createSharedStore(name, initial, options = {}) {
284
366
  });
285
367
  function setKey(key, value) {
286
368
  const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
369
+ try {
370
+ bus.post({
371
+ v: 1,
372
+ scope: "state",
373
+ type: "patch",
374
+ key,
375
+ value,
376
+ version,
377
+ clientId,
378
+ kind: bus.kind
379
+ });
380
+ } catch (error) {
381
+ throw new TypeError(
382
+ `use-everywhere: the value for key "${key}" cannot cross the wire (structured clone failed); the write was not applied. ${String(error)}`,
383
+ { cause: error }
384
+ );
385
+ }
287
386
  versions[key] = version;
288
387
  state[key] = freezeShared(value);
289
- bus.post({
290
- v: 1,
291
- scope: "state",
292
- type: "patch",
293
- key,
294
- value,
295
- version,
296
- clientId,
297
- kind: bus.kind
298
- });
299
388
  notify(key, value, { clientId, kind: bus.kind, self: true });
300
389
  }
301
390
  const proxy = new Proxy(state, {
@@ -358,7 +447,15 @@ function createSharedStore(name, initial, options = {}) {
358
447
  addEventListener("pagehide", flushPersist);
359
448
  }
360
449
  }
361
- bus.post({ v: 1, scope: "state", type: "hello", clientId, kind: bus.kind });
450
+ const sayHello = () => bus.post({ v: 1, scope: "state", type: "hello", clientId, kind: bus.kind });
451
+ const onPageShow = (event) => {
452
+ if (!event.persisted) return;
453
+ sayHello();
454
+ };
455
+ const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
456
+ if (hasWindow) addEventListener("pageshow", onPageShow);
457
+ sayHello();
458
+ let storeClosed = false;
362
459
  return {
363
460
  clientId,
364
461
  state: proxy,
@@ -379,16 +476,27 @@ function createSharedStore(name, initial, options = {}) {
379
476
  keyListeners.set(key, set);
380
477
  }
381
478
  set.add(fn);
382
- return () => set.delete(fn);
479
+ return () => {
480
+ set.delete(fn);
481
+ if (set.size === 0) keyListeners.delete(key);
482
+ };
383
483
  },
384
484
  registerKey(key, initialValue) {
385
485
  if (key in versions) return;
386
486
  versions[key] = [0, clientId];
387
- state[key] = initialValue;
487
+ state[key] = freezeShared(initialValue);
388
488
  snapshot = Object.freeze({ ...state });
389
489
  versionsSnapshot = Object.freeze({ ...versions });
390
490
  },
391
491
  close() {
492
+ if (storeClosed) return;
493
+ storeClosed = true;
494
+ if (onSharedBus) {
495
+ const live = (liveStores.get(name) ?? 1) - 1;
496
+ if (live > 0) liveStores.set(name, live);
497
+ else liveStores.delete(name);
498
+ }
499
+ if (hasWindow) removeEventListener("pageshow", onPageShow);
392
500
  if (flushPersist) {
393
501
  flushPersist();
394
502
  if (typeof document !== "undefined" && typeof removeEventListener === "function") {
@@ -437,6 +545,7 @@ function createPresence(name, options = {}) {
437
545
  },
438
546
  Math.max(500, Math.floor(pruneAfterMs / 2))
439
547
  );
548
+ let closed = false;
440
549
  return {
441
550
  clientId: bus.clientId,
442
551
  getPeers: () => snapshot,
@@ -445,6 +554,8 @@ function createPresence(name, options = {}) {
445
554
  return () => listeners.delete(fn);
446
555
  },
447
556
  close() {
557
+ if (closed) return;
558
+ closed = true;
448
559
  clearInterval(prune);
449
560
  unsubscribe();
450
561
  listeners.clear();
@@ -533,10 +644,19 @@ function createLeader(name, options = {}) {
533
644
  bus.post({ v: 1, scope: "leader", type: "resign", term: resigning, clientId, kind: bus.kind });
534
645
  armLease(leaseMs);
535
646
  }
647
+ const sayHello = () => bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
536
648
  const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
537
649
  const onPageHide = () => resign();
538
- if (hasWindow) addEventListener("pagehide", onPageHide);
539
- bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
650
+ const onPageShow = (event) => {
651
+ if (!event.persisted) return;
652
+ sayHello();
653
+ armLease(heartbeatMs);
654
+ };
655
+ if (hasWindow) {
656
+ addEventListener("pagehide", onPageHide);
657
+ addEventListener("pageshow", onPageShow);
658
+ }
659
+ sayHello();
540
660
  armLease(heartbeatMs);
541
661
  return {
542
662
  clientId,
@@ -556,11 +676,15 @@ function createLeader(name, options = {}) {
556
676
  if (leaderId === null) armLease(0);
557
677
  },
558
678
  close() {
679
+ if (closed) return;
559
680
  closed = true;
560
681
  resign();
561
682
  clearInterval(beat);
562
683
  clearTimeout(lease);
563
- if (hasWindow) removeEventListener("pagehide", onPageHide);
684
+ if (hasWindow) {
685
+ removeEventListener("pagehide", onPageHide);
686
+ removeEventListener("pageshow", onPageShow);
687
+ }
564
688
  unsubscribe();
565
689
  listeners.clear();
566
690
  bus.release();
@@ -574,44 +698,54 @@ function isPersisted(value) {
574
698
  const candidate = value;
575
699
  return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
576
700
  }
577
- function webStorageAdapter(storage, key) {
578
- const resolve = () => {
701
+ function webStorageAdapter(storage, key, options = {}) {
702
+ const report = (error, operation) => {
579
703
  try {
580
- return typeof storage === "function" ? storage() : storage;
704
+ options.onError?.(error, operation);
581
705
  } catch {
706
+ }
707
+ };
708
+ const resolve = (operation) => {
709
+ try {
710
+ return typeof storage === "function" ? storage() : storage;
711
+ } catch (error) {
712
+ report(error, operation);
582
713
  return void 0;
583
714
  }
584
715
  };
585
716
  return {
586
717
  read() {
587
718
  try {
588
- const raw = resolve()?.getItem(key);
719
+ const raw = resolve("read")?.getItem(key);
589
720
  if (!raw) return void 0;
590
721
  const parsed = JSON.parse(raw);
591
722
  return isPersisted(parsed) ? parsed : void 0;
592
- } catch {
723
+ } catch (error) {
724
+ report(error, "read");
593
725
  return void 0;
594
726
  }
595
727
  },
596
728
  write(snapshot) {
597
729
  try {
598
- resolve()?.setItem(key, JSON.stringify(snapshot));
599
- } catch {
730
+ resolve("write")?.setItem(key, JSON.stringify(snapshot));
731
+ } catch (error) {
732
+ report(error, "write");
600
733
  }
601
734
  },
602
735
  remove() {
603
736
  try {
604
- resolve()?.removeItem(key);
605
- } catch {
737
+ resolve("remove")?.removeItem(key);
738
+ } catch (error) {
739
+ report(error, "remove");
606
740
  }
607
741
  }
608
742
  };
609
743
  }
610
- function localStorageAdapter(key) {
611
- return webStorageAdapter(() => globalThis.localStorage, key);
744
+ function localStorageAdapter(key, options) {
745
+ return webStorageAdapter(() => globalThis.localStorage, key, options);
612
746
  }
613
- function sessionStorageAdapter(key) {
614
- return webStorageAdapter(() => globalThis.sessionStorage, key);
747
+ function sessionStorageAdapter(key, options) {
748
+ return webStorageAdapter(() => globalThis.sessionStorage, key, options);
615
749
  }
616
750
 
617
751
  // src/errors/handshake-timeout-error.ts
@@ -738,6 +872,7 @@ function openWindow(url, options) {
738
872
  }, 400);
739
873
  const readyTimer = setTimeout(() => {
740
874
  if (isReady || closedSettled) return;
875
+ localWindow.removeEventListener("message", onMessage);
741
876
  const err = new HandshakeTimeoutError();
742
877
  rejectReady(err);
743
878
  if (!resultSettled) {
@@ -791,6 +926,7 @@ function connectToOpener(options) {
791
926
  markHandled(ready);
792
927
  const onMessage = (event) => {
793
928
  if (!allowAnyOrigin && event.origin !== peerOrigin) return;
929
+ if (event.source !== opener) return;
794
930
  if (!isWindowWire(event.data, cid)) return;
795
931
  const wire = event.data;
796
932
  if (wire.t === "ready-ack") {
@@ -846,64 +982,11 @@ function connectToOpener(options) {
846
982
  }
847
983
  };
848
984
  }
849
-
850
- // src/transport/memory-transport.ts
851
- var MemoryTransport = class {
852
- constructor(hub) {
853
- this.hub = hub;
854
- this.listeners = /* @__PURE__ */ new Set();
855
- this.closed = false;
856
- }
857
- post(data) {
858
- if (this.closed) return;
859
- this.hub.broadcast(this, data);
860
- }
861
- subscribe(listener) {
862
- this.listeners.add(listener);
863
- return () => this.listeners.delete(listener);
864
- }
865
- close() {
866
- this.closed = true;
867
- this.listeners.clear();
868
- this.hub.disconnect(this);
869
- }
870
- /** @internal */
871
- deliver(data) {
872
- if (this.closed) return;
873
- for (const listener of this.listeners) listener(data);
874
- }
875
- };
876
-
877
- // src/transport/memory-hub.ts
878
- var MemoryHub = class {
879
- constructor() {
880
- this.transports = /* @__PURE__ */ new Set();
881
- }
882
- connect() {
883
- const transport = new MemoryTransport(this);
884
- this.transports.add(transport);
885
- return transport;
886
- }
887
- /** @internal */
888
- broadcast(from, data) {
889
- for (const transport of this.transports) {
890
- if (transport !== from) {
891
- queueMicrotask(() => transport.deliver(data));
892
- }
893
- }
894
- }
895
- /** @internal */
896
- disconnect(transport) {
897
- this.transports.delete(transport);
898
- }
899
- };
900
985
  export {
901
986
  BroadcastChannelTransport,
902
987
  CID_PARAM,
903
988
  DEFAULT_NAME,
904
989
  HandshakeTimeoutError,
905
- MemoryHub,
906
- MemoryTransport,
907
990
  NoopTransport,
908
991
  WindowClosedError,
909
992
  connectToOpener,
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/testing.ts
21
+ var testing_exports = {};
22
+ __export(testing_exports, {
23
+ MemoryHub: () => MemoryHub,
24
+ MemoryTransport: () => MemoryTransport
25
+ });
26
+ module.exports = __toCommonJS(testing_exports);
27
+
28
+ // src/transport/memory-transport.ts
29
+ var MemoryTransport = class {
30
+ constructor(hub) {
31
+ this.hub = hub;
32
+ this.listeners = /* @__PURE__ */ new Set();
33
+ this.closed = false;
34
+ }
35
+ post(data) {
36
+ if (this.closed) return;
37
+ this.hub.broadcast(this, data);
38
+ }
39
+ subscribe(listener) {
40
+ this.listeners.add(listener);
41
+ return () => this.listeners.delete(listener);
42
+ }
43
+ close() {
44
+ this.closed = true;
45
+ this.listeners.clear();
46
+ this.hub.disconnect(this);
47
+ }
48
+ /** @internal */
49
+ deliver(data) {
50
+ if (this.closed) return;
51
+ for (const listener of this.listeners) listener(data);
52
+ }
53
+ };
54
+
55
+ // src/transport/memory-hub.ts
56
+ var MemoryHub = class {
57
+ constructor() {
58
+ this.transports = /* @__PURE__ */ new Set();
59
+ }
60
+ connect() {
61
+ const transport = new MemoryTransport(this);
62
+ this.transports.add(transport);
63
+ return transport;
64
+ }
65
+ /** @internal */
66
+ broadcast(from, data) {
67
+ structuredClone(data);
68
+ for (const transport of this.transports) {
69
+ if (transport !== from) {
70
+ const copy = structuredClone(data);
71
+ queueMicrotask(() => transport.deliver(copy));
72
+ }
73
+ }
74
+ }
75
+ /** @internal */
76
+ disconnect(transport) {
77
+ this.transports.delete(transport);
78
+ }
79
+ };
80
+ // Annotate the CommonJS export names for ESM import in node:
81
+ 0 && (module.exports = {
82
+ MemoryHub,
83
+ MemoryTransport
84
+ });
@@ -0,0 +1,30 @@
1
+ import { T as Transport } from './transport.types-tQ1cu6Xm.cjs';
2
+
3
+ /** One simulated client on a MemoryHub. Create via hub.connect(). */
4
+ declare class MemoryTransport implements Transport {
5
+ private hub;
6
+ private listeners;
7
+ private closed;
8
+ constructor(hub: MemoryHub);
9
+ post(data: unknown): void;
10
+ subscribe(listener: (data: unknown) => void): () => void;
11
+ close(): void;
12
+ /** @internal */
13
+ deliver(data: unknown): void;
14
+ }
15
+
16
+ /**
17
+ * In-memory hub for tests: N transports attached to one hub, each post is
18
+ * delivered to every *other* transport on a microtask (mirrors BroadcastChannel's
19
+ * async, no-self-echo delivery).
20
+ */
21
+ declare class MemoryHub {
22
+ private transports;
23
+ connect(): MemoryTransport;
24
+ /** @internal */
25
+ broadcast(from: MemoryTransport, data: unknown): void;
26
+ /** @internal */
27
+ disconnect(transport: MemoryTransport): void;
28
+ }
29
+
30
+ export { MemoryHub, MemoryTransport };
@@ -0,0 +1,30 @@
1
+ import { T as Transport } from './transport.types-tQ1cu6Xm.js';
2
+
3
+ /** One simulated client on a MemoryHub. Create via hub.connect(). */
4
+ declare class MemoryTransport implements Transport {
5
+ private hub;
6
+ private listeners;
7
+ private closed;
8
+ constructor(hub: MemoryHub);
9
+ post(data: unknown): void;
10
+ subscribe(listener: (data: unknown) => void): () => void;
11
+ close(): void;
12
+ /** @internal */
13
+ deliver(data: unknown): void;
14
+ }
15
+
16
+ /**
17
+ * In-memory hub for tests: N transports attached to one hub, each post is
18
+ * delivered to every *other* transport on a microtask (mirrors BroadcastChannel's
19
+ * async, no-self-echo delivery).
20
+ */
21
+ declare class MemoryHub {
22
+ private transports;
23
+ connect(): MemoryTransport;
24
+ /** @internal */
25
+ broadcast(from: MemoryTransport, data: unknown): void;
26
+ /** @internal */
27
+ disconnect(transport: MemoryTransport): void;
28
+ }
29
+
30
+ export { MemoryHub, MemoryTransport };
@@ -0,0 +1,56 @@
1
+ // src/transport/memory-transport.ts
2
+ var MemoryTransport = class {
3
+ constructor(hub) {
4
+ this.hub = hub;
5
+ this.listeners = /* @__PURE__ */ new Set();
6
+ this.closed = false;
7
+ }
8
+ post(data) {
9
+ if (this.closed) return;
10
+ this.hub.broadcast(this, data);
11
+ }
12
+ subscribe(listener) {
13
+ this.listeners.add(listener);
14
+ return () => this.listeners.delete(listener);
15
+ }
16
+ close() {
17
+ this.closed = true;
18
+ this.listeners.clear();
19
+ this.hub.disconnect(this);
20
+ }
21
+ /** @internal */
22
+ deliver(data) {
23
+ if (this.closed) return;
24
+ for (const listener of this.listeners) listener(data);
25
+ }
26
+ };
27
+
28
+ // src/transport/memory-hub.ts
29
+ var MemoryHub = class {
30
+ constructor() {
31
+ this.transports = /* @__PURE__ */ new Set();
32
+ }
33
+ connect() {
34
+ const transport = new MemoryTransport(this);
35
+ this.transports.add(transport);
36
+ return transport;
37
+ }
38
+ /** @internal */
39
+ broadcast(from, data) {
40
+ structuredClone(data);
41
+ for (const transport of this.transports) {
42
+ if (transport !== from) {
43
+ const copy = structuredClone(data);
44
+ queueMicrotask(() => transport.deliver(copy));
45
+ }
46
+ }
47
+ }
48
+ /** @internal */
49
+ disconnect(transport) {
50
+ this.transports.delete(transport);
51
+ }
52
+ };
53
+ export {
54
+ MemoryHub,
55
+ MemoryTransport
56
+ };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Minimal message bus. Implementations: BroadcastChannelTransport (same-origin),
3
+ * NoopTransport (SSR / local-only), MemoryTransport (tests). A transport never
4
+ * echoes a client's own posts back to it.
5
+ */
6
+ interface Transport {
7
+ post(data: unknown): void;
8
+ subscribe(listener: (data: unknown) => void): () => void;
9
+ close(): void;
10
+ }
11
+
12
+ export type { Transport as T };
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Minimal message bus. Implementations: BroadcastChannelTransport (same-origin),
3
+ * NoopTransport (SSR / local-only), MemoryTransport (tests). A transport never
4
+ * echoes a client's own posts back to it.
5
+ */
6
+ interface Transport {
7
+ post(data: unknown): void;
8
+ subscribe(listener: (data: unknown) => void): () => void;
9
+ close(): void;
10
+ }
11
+
12
+ export type { Transport as T };