@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.cjs CHANGED
@@ -18,14 +18,12 @@ var __copyProps = (to, from, except, desc) => {
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
19
 
20
20
  // src/index.ts
21
- var index_exports = {};
22
- __export(index_exports, {
21
+ var src_exports = {};
22
+ __export(src_exports, {
23
23
  BroadcastChannelTransport: () => BroadcastChannelTransport,
24
24
  CID_PARAM: () => CID_PARAM,
25
25
  DEFAULT_NAME: () => DEFAULT_NAME,
26
26
  HandshakeTimeoutError: () => HandshakeTimeoutError,
27
- MemoryHub: () => MemoryHub,
28
- MemoryTransport: () => MemoryTransport,
29
27
  NoopTransport: () => NoopTransport,
30
28
  WindowClosedError: () => WindowClosedError,
31
29
  connectToOpener: () => connectToOpener,
@@ -44,7 +42,7 @@ __export(index_exports, {
44
42
  sessionStorageAdapter: () => sessionStorageAdapter,
45
43
  webStorageAdapter: () => webStorageAdapter
46
44
  });
47
- module.exports = __toCommonJS(index_exports);
45
+ module.exports = __toCommonJS(src_exports);
48
46
 
49
47
  // src/defaults.ts
50
48
  var DEFAULT_NAME = "use-everywhere";
@@ -55,7 +53,13 @@ function emitBusEvent(name, direction, wire) {
55
53
  const set = observers.get(name);
56
54
  if (!set) return;
57
55
  const event = { name, direction, wire };
58
- for (const fn of set) fn(event);
56
+ for (const fn of set) {
57
+ try {
58
+ fn(event);
59
+ } catch (error) {
60
+ console.error(`[use-everywhere] a bus observer for "${name}" threw`, error);
61
+ }
62
+ }
59
63
  }
60
64
  function observeBus(name, fn) {
61
65
  let set = observers.get(name);
@@ -80,12 +84,41 @@ function enableDebug(options = {}) {
80
84
  });
81
85
  }
82
86
 
87
+ // src/dev.ts
88
+ var inDev = false;
89
+ try {
90
+ inDev = process.env.NODE_ENV !== "production";
91
+ } catch {
92
+ }
93
+ var warned = /* @__PURE__ */ new Set();
94
+ function devWarn(message) {
95
+ if (!inDev || warned.has(message)) return;
96
+ warned.add(message);
97
+ console.warn(message);
98
+ }
99
+
83
100
  // src/ids.ts
101
+ function randomHex(bytes) {
102
+ const crypto = globalThis.crypto;
103
+ if (crypto?.getRandomValues) {
104
+ const buf = crypto.getRandomValues(new Uint8Array(bytes));
105
+ let out2 = "";
106
+ for (const b of buf) out2 += b.toString(16).padStart(2, "0");
107
+ return out2;
108
+ }
109
+ devWarn(
110
+ "[use-everywhere] crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
111
+ );
112
+ let out = "";
113
+ while (out.length < bytes * 2)
114
+ out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
115
+ return out;
116
+ }
84
117
  function newClientId() {
85
- return Math.random().toString(36).slice(2, 8);
118
+ return randomHex(8);
86
119
  }
87
120
  function newMsgId() {
88
- return Math.random().toString(36).slice(2, 10);
121
+ return randomHex(16);
89
122
  }
90
123
 
91
124
  // src/transport/broadcast-channel-transport.ts
@@ -165,12 +198,20 @@ function createBus(name, options, onShutdown) {
165
198
  options.heartbeatMs ?? 2e3
166
199
  );
167
200
  const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
201
+ const onPageShow = (event) => {
202
+ if (!event.persisted) return;
203
+ post({ v: 1, scope: "presence", type: "hello", clientId, kind });
204
+ };
168
205
  const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
169
- if (hasWindow) addEventListener("pagehide", sayBye);
206
+ if (hasWindow) {
207
+ addEventListener("pagehide", sayBye);
208
+ addEventListener("pageshow", onPageShow);
209
+ }
170
210
  return {
171
211
  name,
172
212
  clientId,
173
213
  kind,
214
+ heartbeatMs: options.heartbeatMs ?? 2e3,
174
215
  post,
175
216
  subscribe(fn) {
176
217
  listeners.add(fn);
@@ -180,12 +221,16 @@ function createBus(name, options, onShutdown) {
180
221
  refs++;
181
222
  },
182
223
  release() {
224
+ if (closed) return;
183
225
  refs--;
184
226
  if (refs > 0) return;
185
227
  sayBye();
186
228
  closed = true;
187
229
  clearInterval(heartbeat);
188
- if (hasWindow) removeEventListener("pagehide", sayBye);
230
+ if (hasWindow) {
231
+ removeEventListener("pagehide", sayBye);
232
+ removeEventListener("pageshow", onPageShow);
233
+ }
189
234
  unsubscribe();
190
235
  transport.close();
191
236
  listeners.clear();
@@ -207,6 +252,15 @@ function getBus(name, options = {}) {
207
252
  if (!bus) {
208
253
  bus = createBus(name, options, () => registry.delete(name));
209
254
  registry.set(name, bus);
255
+ } else {
256
+ if (options.heartbeatMs !== void 0 && options.heartbeatMs !== bus.heartbeatMs) {
257
+ devWarn(
258
+ `[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
259
+ );
260
+ }
261
+ if (options.kind !== void 0 && options.kind !== bus.kind) {
262
+ devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
263
+ }
210
264
  }
211
265
  bus.acquire();
212
266
  return bus;
@@ -223,6 +277,7 @@ function createChannel(name, options = {}) {
223
277
  const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
224
278
  for (const fn of set) fn(wire.payload, meta);
225
279
  });
280
+ let closed = false;
226
281
  return {
227
282
  name,
228
283
  clientId: bus.clientId,
@@ -247,6 +302,8 @@ function createChannel(name, options = {}) {
247
302
  return () => set.delete(handler);
248
303
  },
249
304
  close() {
305
+ if (closed) return;
306
+ closed = true;
250
307
  unsubscribe();
251
308
  handlers.clear();
252
309
  bus.release();
@@ -260,25 +317,48 @@ function newer(a, b) {
260
317
  }
261
318
 
262
319
  // src/dev-freeze.ts
263
- var inDev = false;
320
+ var inDev2 = false;
264
321
  try {
265
- inDev = process.env.NODE_ENV !== "production";
322
+ inDev2 = process.env.NODE_ENV !== "production";
266
323
  } catch {
267
324
  }
268
325
  function deepFreeze(value) {
269
326
  if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
327
+ if (ArrayBuffer.isView(value)) return;
270
328
  Object.freeze(value);
329
+ if (value instanceof Map) {
330
+ for (const [k, v] of value) {
331
+ deepFreeze(k);
332
+ deepFreeze(v);
333
+ }
334
+ return;
335
+ }
336
+ if (value instanceof Set) {
337
+ for (const entry of value) deepFreeze(entry);
338
+ return;
339
+ }
271
340
  for (const key of Object.keys(value)) {
272
341
  deepFreeze(value[key]);
273
342
  }
274
343
  }
275
344
  function freezeShared(value) {
276
- if (inDev) deepFreeze(value);
345
+ if (inDev2) deepFreeze(value);
277
346
  return value;
278
347
  }
279
348
 
280
349
  // src/shared-store.ts
350
+ var liveStores = /* @__PURE__ */ new Map();
281
351
  function createSharedStore(name, initial, options = {}) {
352
+ const onSharedBus = !options.transport;
353
+ if (onSharedBus) {
354
+ const live = liveStores.get(name) ?? 0;
355
+ if (live > 0) {
356
+ devWarn(
357
+ `[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.`
358
+ );
359
+ }
360
+ liveStores.set(name, live + 1);
361
+ }
282
362
  const bus = getBus(name, options);
283
363
  const clientId = bus.clientId;
284
364
  const accept = options.accept;
@@ -332,18 +412,25 @@ function createSharedStore(name, initial, options = {}) {
332
412
  });
333
413
  function setKey(key, value) {
334
414
  const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
415
+ try {
416
+ bus.post({
417
+ v: 1,
418
+ scope: "state",
419
+ type: "patch",
420
+ key,
421
+ value,
422
+ version,
423
+ clientId,
424
+ kind: bus.kind
425
+ });
426
+ } catch (error) {
427
+ throw new TypeError(
428
+ `use-everywhere: the value for key "${key}" cannot cross the wire (structured clone failed); the write was not applied. ${String(error)}`,
429
+ { cause: error }
430
+ );
431
+ }
335
432
  versions[key] = version;
336
433
  state[key] = freezeShared(value);
337
- bus.post({
338
- v: 1,
339
- scope: "state",
340
- type: "patch",
341
- key,
342
- value,
343
- version,
344
- clientId,
345
- kind: bus.kind
346
- });
347
434
  notify(key, value, { clientId, kind: bus.kind, self: true });
348
435
  }
349
436
  const proxy = new Proxy(state, {
@@ -406,7 +493,15 @@ function createSharedStore(name, initial, options = {}) {
406
493
  addEventListener("pagehide", flushPersist);
407
494
  }
408
495
  }
409
- bus.post({ v: 1, scope: "state", type: "hello", clientId, kind: bus.kind });
496
+ const sayHello = () => bus.post({ v: 1, scope: "state", type: "hello", clientId, kind: bus.kind });
497
+ const onPageShow = (event) => {
498
+ if (!event.persisted) return;
499
+ sayHello();
500
+ };
501
+ const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
502
+ if (hasWindow) addEventListener("pageshow", onPageShow);
503
+ sayHello();
504
+ let storeClosed = false;
410
505
  return {
411
506
  clientId,
412
507
  state: proxy,
@@ -427,16 +522,27 @@ function createSharedStore(name, initial, options = {}) {
427
522
  keyListeners.set(key, set);
428
523
  }
429
524
  set.add(fn);
430
- return () => set.delete(fn);
525
+ return () => {
526
+ set.delete(fn);
527
+ if (set.size === 0) keyListeners.delete(key);
528
+ };
431
529
  },
432
530
  registerKey(key, initialValue) {
433
531
  if (key in versions) return;
434
532
  versions[key] = [0, clientId];
435
- state[key] = initialValue;
533
+ state[key] = freezeShared(initialValue);
436
534
  snapshot = Object.freeze({ ...state });
437
535
  versionsSnapshot = Object.freeze({ ...versions });
438
536
  },
439
537
  close() {
538
+ if (storeClosed) return;
539
+ storeClosed = true;
540
+ if (onSharedBus) {
541
+ const live = (liveStores.get(name) ?? 1) - 1;
542
+ if (live > 0) liveStores.set(name, live);
543
+ else liveStores.delete(name);
544
+ }
545
+ if (hasWindow) removeEventListener("pageshow", onPageShow);
440
546
  if (flushPersist) {
441
547
  flushPersist();
442
548
  if (typeof document !== "undefined" && typeof removeEventListener === "function") {
@@ -485,6 +591,7 @@ function createPresence(name, options = {}) {
485
591
  },
486
592
  Math.max(500, Math.floor(pruneAfterMs / 2))
487
593
  );
594
+ let closed = false;
488
595
  return {
489
596
  clientId: bus.clientId,
490
597
  getPeers: () => snapshot,
@@ -493,6 +600,8 @@ function createPresence(name, options = {}) {
493
600
  return () => listeners.delete(fn);
494
601
  },
495
602
  close() {
603
+ if (closed) return;
604
+ closed = true;
496
605
  clearInterval(prune);
497
606
  unsubscribe();
498
607
  listeners.clear();
@@ -581,10 +690,19 @@ function createLeader(name, options = {}) {
581
690
  bus.post({ v: 1, scope: "leader", type: "resign", term: resigning, clientId, kind: bus.kind });
582
691
  armLease(leaseMs);
583
692
  }
693
+ const sayHello = () => bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
584
694
  const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
585
695
  const onPageHide = () => resign();
586
- if (hasWindow) addEventListener("pagehide", onPageHide);
587
- bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
696
+ const onPageShow = (event) => {
697
+ if (!event.persisted) return;
698
+ sayHello();
699
+ armLease(heartbeatMs);
700
+ };
701
+ if (hasWindow) {
702
+ addEventListener("pagehide", onPageHide);
703
+ addEventListener("pageshow", onPageShow);
704
+ }
705
+ sayHello();
588
706
  armLease(heartbeatMs);
589
707
  return {
590
708
  clientId,
@@ -604,11 +722,15 @@ function createLeader(name, options = {}) {
604
722
  if (leaderId === null) armLease(0);
605
723
  },
606
724
  close() {
725
+ if (closed) return;
607
726
  closed = true;
608
727
  resign();
609
728
  clearInterval(beat);
610
729
  clearTimeout(lease);
611
- if (hasWindow) removeEventListener("pagehide", onPageHide);
730
+ if (hasWindow) {
731
+ removeEventListener("pagehide", onPageHide);
732
+ removeEventListener("pageshow", onPageShow);
733
+ }
612
734
  unsubscribe();
613
735
  listeners.clear();
614
736
  bus.release();
@@ -622,44 +744,54 @@ function isPersisted(value) {
622
744
  const candidate = value;
623
745
  return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
624
746
  }
625
- function webStorageAdapter(storage, key) {
626
- const resolve = () => {
747
+ function webStorageAdapter(storage, key, options = {}) {
748
+ const report = (error, operation) => {
627
749
  try {
628
- return typeof storage === "function" ? storage() : storage;
750
+ options.onError?.(error, operation);
629
751
  } catch {
752
+ }
753
+ };
754
+ const resolve = (operation) => {
755
+ try {
756
+ return typeof storage === "function" ? storage() : storage;
757
+ } catch (error) {
758
+ report(error, operation);
630
759
  return void 0;
631
760
  }
632
761
  };
633
762
  return {
634
763
  read() {
635
764
  try {
636
- const raw = resolve()?.getItem(key);
765
+ const raw = resolve("read")?.getItem(key);
637
766
  if (!raw) return void 0;
638
767
  const parsed = JSON.parse(raw);
639
768
  return isPersisted(parsed) ? parsed : void 0;
640
- } catch {
769
+ } catch (error) {
770
+ report(error, "read");
641
771
  return void 0;
642
772
  }
643
773
  },
644
774
  write(snapshot) {
645
775
  try {
646
- resolve()?.setItem(key, JSON.stringify(snapshot));
647
- } catch {
776
+ resolve("write")?.setItem(key, JSON.stringify(snapshot));
777
+ } catch (error) {
778
+ report(error, "write");
648
779
  }
649
780
  },
650
781
  remove() {
651
782
  try {
652
- resolve()?.removeItem(key);
653
- } catch {
783
+ resolve("remove")?.removeItem(key);
784
+ } catch (error) {
785
+ report(error, "remove");
654
786
  }
655
787
  }
656
788
  };
657
789
  }
658
- function localStorageAdapter(key) {
659
- return webStorageAdapter(() => globalThis.localStorage, key);
790
+ function localStorageAdapter(key, options) {
791
+ return webStorageAdapter(() => globalThis.localStorage, key, options);
660
792
  }
661
- function sessionStorageAdapter(key) {
662
- return webStorageAdapter(() => globalThis.sessionStorage, key);
793
+ function sessionStorageAdapter(key, options) {
794
+ return webStorageAdapter(() => globalThis.sessionStorage, key, options);
663
795
  }
664
796
 
665
797
  // src/errors/handshake-timeout-error.ts
@@ -786,6 +918,7 @@ function openWindow(url, options) {
786
918
  }, 400);
787
919
  const readyTimer = setTimeout(() => {
788
920
  if (isReady || closedSettled) return;
921
+ localWindow.removeEventListener("message", onMessage);
789
922
  const err = new HandshakeTimeoutError();
790
923
  rejectReady(err);
791
924
  if (!resultSettled) {
@@ -839,6 +972,7 @@ function connectToOpener(options) {
839
972
  markHandled(ready);
840
973
  const onMessage = (event) => {
841
974
  if (!allowAnyOrigin && event.origin !== peerOrigin) return;
975
+ if (event.source !== opener) return;
842
976
  if (!isWindowWire(event.data, cid)) return;
843
977
  const wire = event.data;
844
978
  if (wire.t === "ready-ack") {
@@ -894,65 +1028,12 @@ function connectToOpener(options) {
894
1028
  }
895
1029
  };
896
1030
  }
897
-
898
- // src/transport/memory-transport.ts
899
- var MemoryTransport = class {
900
- constructor(hub) {
901
- this.hub = hub;
902
- this.listeners = /* @__PURE__ */ new Set();
903
- this.closed = false;
904
- }
905
- post(data) {
906
- if (this.closed) return;
907
- this.hub.broadcast(this, data);
908
- }
909
- subscribe(listener) {
910
- this.listeners.add(listener);
911
- return () => this.listeners.delete(listener);
912
- }
913
- close() {
914
- this.closed = true;
915
- this.listeners.clear();
916
- this.hub.disconnect(this);
917
- }
918
- /** @internal */
919
- deliver(data) {
920
- if (this.closed) return;
921
- for (const listener of this.listeners) listener(data);
922
- }
923
- };
924
-
925
- // src/transport/memory-hub.ts
926
- var MemoryHub = class {
927
- constructor() {
928
- this.transports = /* @__PURE__ */ new Set();
929
- }
930
- connect() {
931
- const transport = new MemoryTransport(this);
932
- this.transports.add(transport);
933
- return transport;
934
- }
935
- /** @internal */
936
- broadcast(from, data) {
937
- for (const transport of this.transports) {
938
- if (transport !== from) {
939
- queueMicrotask(() => transport.deliver(data));
940
- }
941
- }
942
- }
943
- /** @internal */
944
- disconnect(transport) {
945
- this.transports.delete(transport);
946
- }
947
- };
948
1031
  // Annotate the CommonJS export names for ESM import in node:
949
1032
  0 && (module.exports = {
950
1033
  BroadcastChannelTransport,
951
1034
  CID_PARAM,
952
1035
  DEFAULT_NAME,
953
1036
  HandshakeTimeoutError,
954
- MemoryHub,
955
- MemoryTransport,
956
1037
  NoopTransport,
957
1038
  WindowClosedError,
958
1039
  connectToOpener,
package/dist/index.d.cts CHANGED
@@ -1,13 +1,4 @@
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
- }
1
+ import { T as Transport } from './transport.types-tQ1cu6Xm.cjs';
11
2
 
12
3
  type MessageMap = Record<string, unknown>;
13
4
  type PeerKind = 'tab' | 'worker' | (string & {});
@@ -230,6 +221,15 @@ interface Leader {
230
221
  declare function createLeader(name: string, options?: LeaderOptions): Leader;
231
222
 
232
223
  type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
224
+ interface WebStorageAdapterOptions {
225
+ /**
226
+ * Called when a storage operation fails: blocked storage, corrupt JSON, a
227
+ * full quota. Persistence stays best-effort either way — this is the
228
+ * observability seam (telemetry, a "your changes may not be saved" notice),
229
+ * not a recovery path. Errors thrown by the callback itself are swallowed.
230
+ */
231
+ onError?: (error: unknown, operation: 'read' | 'write' | 'remove') => void;
232
+ }
233
233
  /**
234
234
  * Persist to any Storage-shaped thing.
235
235
  *
@@ -240,13 +240,14 @@ type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
240
240
  * Behind a thunk, every access happens inside one.
241
241
  *
242
242
  * Blocked storage, corrupt JSON, a foreign schema, or a full quota all degrade
243
- * to a silent no-op. Persistence is best-effort; it must never break the store.
243
+ * to a silent no-op persistence is best-effort; it must never break the
244
+ * store. Pass `onError` to observe the failures anyway.
244
245
  */
245
- declare function webStorageAdapter(storage: StorageLike | (() => StorageLike | undefined), key: string): PersistAdapter;
246
+ declare function webStorageAdapter(storage: StorageLike | (() => StorageLike | undefined), key: string, options?: WebStorageAdapterOptions): PersistAdapter;
246
247
  /** Survives closing every tab. */
247
- declare function localStorageAdapter(key: string): PersistAdapter;
248
+ declare function localStorageAdapter(key: string, options?: WebStorageAdapterOptions): PersistAdapter;
248
249
  /** Survives reloads, but dies with the tab. */
249
- declare function sessionStorageAdapter(key: string): PersistAdapter;
250
+ declare function sessionStorageAdapter(key: string, options?: WebStorageAdapterOptions): PersistAdapter;
250
251
 
251
252
  interface MessageEventLike {
252
253
  data: unknown;
@@ -415,31 +416,4 @@ declare function isBroadcastChannelAvailable(): boolean;
415
416
  /** Default factory: real BroadcastChannel when available, otherwise a local no-op. */
416
417
  declare function defaultTransport(name: string): Transport;
417
418
 
418
- /** One simulated client on a MemoryHub. Create via hub.connect(). */
419
- declare class MemoryTransport implements Transport {
420
- private hub;
421
- private listeners;
422
- private closed;
423
- constructor(hub: MemoryHub);
424
- post(data: unknown): void;
425
- subscribe(listener: (data: unknown) => void): () => void;
426
- close(): void;
427
- /** @internal */
428
- deliver(data: unknown): void;
429
- }
430
-
431
- /**
432
- * In-memory hub for tests: N transports attached to one hub, each post is
433
- * delivered to every *other* transport on a microtask (mirrors BroadcastChannel's
434
- * async, no-self-echo delivery).
435
- */
436
- declare class MemoryHub {
437
- private transports;
438
- connect(): MemoryTransport;
439
- /** @internal */
440
- broadcast(from: MemoryTransport, data: unknown): void;
441
- /** @internal */
442
- disconnect(transport: MemoryTransport): void;
443
- }
444
-
445
- export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, MemoryHub, MemoryTransport, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, type Transport, type Version, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
419
+ export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, Transport, type Version, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
package/dist/index.d.ts CHANGED
@@ -1,13 +1,4 @@
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
- }
1
+ import { T as Transport } from './transport.types-tQ1cu6Xm.js';
11
2
 
12
3
  type MessageMap = Record<string, unknown>;
13
4
  type PeerKind = 'tab' | 'worker' | (string & {});
@@ -230,6 +221,15 @@ interface Leader {
230
221
  declare function createLeader(name: string, options?: LeaderOptions): Leader;
231
222
 
232
223
  type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
224
+ interface WebStorageAdapterOptions {
225
+ /**
226
+ * Called when a storage operation fails: blocked storage, corrupt JSON, a
227
+ * full quota. Persistence stays best-effort either way — this is the
228
+ * observability seam (telemetry, a "your changes may not be saved" notice),
229
+ * not a recovery path. Errors thrown by the callback itself are swallowed.
230
+ */
231
+ onError?: (error: unknown, operation: 'read' | 'write' | 'remove') => void;
232
+ }
233
233
  /**
234
234
  * Persist to any Storage-shaped thing.
235
235
  *
@@ -240,13 +240,14 @@ type StorageLike = Pick<Storage, 'getItem' | 'setItem' | 'removeItem'>;
240
240
  * Behind a thunk, every access happens inside one.
241
241
  *
242
242
  * Blocked storage, corrupt JSON, a foreign schema, or a full quota all degrade
243
- * to a silent no-op. Persistence is best-effort; it must never break the store.
243
+ * to a silent no-op persistence is best-effort; it must never break the
244
+ * store. Pass `onError` to observe the failures anyway.
244
245
  */
245
- declare function webStorageAdapter(storage: StorageLike | (() => StorageLike | undefined), key: string): PersistAdapter;
246
+ declare function webStorageAdapter(storage: StorageLike | (() => StorageLike | undefined), key: string, options?: WebStorageAdapterOptions): PersistAdapter;
246
247
  /** Survives closing every tab. */
247
- declare function localStorageAdapter(key: string): PersistAdapter;
248
+ declare function localStorageAdapter(key: string, options?: WebStorageAdapterOptions): PersistAdapter;
248
249
  /** Survives reloads, but dies with the tab. */
249
- declare function sessionStorageAdapter(key: string): PersistAdapter;
250
+ declare function sessionStorageAdapter(key: string, options?: WebStorageAdapterOptions): PersistAdapter;
250
251
 
251
252
  interface MessageEventLike {
252
253
  data: unknown;
@@ -415,31 +416,4 @@ declare function isBroadcastChannelAvailable(): boolean;
415
416
  /** Default factory: real BroadcastChannel when available, otherwise a local no-op. */
416
417
  declare function defaultTransport(name: string): Transport;
417
418
 
418
- /** One simulated client on a MemoryHub. Create via hub.connect(). */
419
- declare class MemoryTransport implements Transport {
420
- private hub;
421
- private listeners;
422
- private closed;
423
- constructor(hub: MemoryHub);
424
- post(data: unknown): void;
425
- subscribe(listener: (data: unknown) => void): () => void;
426
- close(): void;
427
- /** @internal */
428
- deliver(data: unknown): void;
429
- }
430
-
431
- /**
432
- * In-memory hub for tests: N transports attached to one hub, each post is
433
- * delivered to every *other* transport on a microtask (mirrors BroadcastChannel's
434
- * async, no-self-echo delivery).
435
- */
436
- declare class MemoryHub {
437
- private transports;
438
- connect(): MemoryTransport;
439
- /** @internal */
440
- broadcast(from: MemoryTransport, data: unknown): void;
441
- /** @internal */
442
- disconnect(transport: MemoryTransport): void;
443
- }
444
-
445
- export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, MemoryHub, MemoryTransport, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, type Transport, type Version, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
419
+ export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, Transport, type Version, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };