@use-everywhere/core 0.7.0 → 0.9.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
@@ -26,18 +26,24 @@ __export(src_exports, {
26
26
  HandshakeTimeoutError: () => HandshakeTimeoutError,
27
27
  NoopTransport: () => NoopTransport,
28
28
  StorageTransport: () => StorageTransport,
29
+ WIRE_VERSION: () => WIRE_VERSION,
29
30
  WindowClosedError: () => WindowClosedError,
30
31
  connectToOpener: () => connectToOpener,
31
32
  createChannel: () => createChannel,
32
33
  createLeader: () => createLeader,
34
+ createNamespace: () => createNamespace,
33
35
  createPresence: () => createPresence,
36
+ createSharedReducer: () => createSharedReducer,
34
37
  createSharedStore: () => createSharedStore,
35
38
  defaultTransport: () => defaultTransport,
36
39
  enableDebug: () => enableDebug,
37
40
  getBusNames: () => getBusNames,
38
41
  getTransportKind: () => getTransportKind,
42
+ getWireSkew: () => getWireSkew,
43
+ indexedDbAdapter: () => indexedDbAdapter,
39
44
  isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
40
45
  isStorageEventAvailable: () => isStorageEventAvailable,
46
+ jsonSerializer: () => jsonSerializer,
41
47
  localStorageAdapter: () => localStorageAdapter,
42
48
  newer: () => newer,
43
49
  observeBus: () => observeBus,
@@ -50,6 +56,26 @@ module.exports = __toCommonJS(src_exports);
50
56
  // src/defaults.ts
51
57
  var DEFAULT_NAME = "use-everywhere";
52
58
 
59
+ // src/dev.ts
60
+ var inDev = false;
61
+ try {
62
+ inDev = process.env.NODE_ENV !== "production";
63
+ } catch {
64
+ }
65
+ var warned = /* @__PURE__ */ new Set();
66
+ var DOCS = "https://rxova.org/packages/use-everywhere/errors";
67
+ function diagnostic(code, message) {
68
+ return `[use-everywhere] ${code}: ${message}
69
+ \u2192 ${DOCS}/#${code.toLowerCase()}`;
70
+ }
71
+ function devWarn(code, message) {
72
+ if (!inDev) return;
73
+ const line = diagnostic(code, message);
74
+ if (warned.has(line)) return;
75
+ warned.add(line);
76
+ console.warn(line);
77
+ }
78
+
53
79
  // src/debug.ts
54
80
  var observers = /* @__PURE__ */ new Map();
55
81
  function emitBusEvent(name, direction, wire) {
@@ -60,7 +86,7 @@ function emitBusEvent(name, direction, wire) {
60
86
  try {
61
87
  fn(event);
62
88
  } catch (error) {
63
- console.error(`[use-everywhere] a bus observer for "${name}" threw`, error);
89
+ console.error(diagnostic("UE1012", `a bus observer for "${name}" threw`), error);
64
90
  }
65
91
  }
66
92
  }
@@ -87,19 +113,6 @@ function enableDebug(options = {}) {
87
113
  });
88
114
  }
89
115
 
90
- // src/dev.ts
91
- var inDev = false;
92
- try {
93
- inDev = process.env.NODE_ENV !== "production";
94
- } catch {
95
- }
96
- var warned = /* @__PURE__ */ new Set();
97
- function devWarn(message) {
98
- if (!inDev || warned.has(message)) return;
99
- warned.add(message);
100
- console.warn(message);
101
- }
102
-
103
116
  // src/ids.ts
104
117
  function randomHex(bytes) {
105
118
  const crypto = globalThis.crypto;
@@ -109,9 +122,12 @@ function randomHex(bytes) {
109
122
  for (const b of buf) out2 += b.toString(16).padStart(2, "0");
110
123
  return out2;
111
124
  }
112
- devWarn(
113
- "[use-everywhere] crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
114
- );
125
+ if (process.env.NODE_ENV !== "production") {
126
+ devWarn(
127
+ "UE1006",
128
+ "crypto.getRandomValues is unavailable; falling back to Math.random for ids. Cross-origin window nonces are not cryptographically strong in this environment."
129
+ );
130
+ }
115
131
  let out = "";
116
132
  while (out.length < bytes * 2)
117
133
  out += Math.floor(Math.random() * 256).toString(16).padStart(2, "0");
@@ -128,15 +144,19 @@ function newMsgId() {
128
144
  var PROTOCOL = 1;
129
145
  var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
130
146
  var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
147
+ var SKEW = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.skew");
131
148
  function announce() {
132
149
  const g = globalThis;
133
150
  const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
134
151
  if (census.protocols.includes(PROTOCOL)) return;
135
152
  census.protocols.push(PROTOCOL);
136
153
  if (census.protocols.length > 1) {
137
- devWarn(
138
- `[use-everywhere] two incompatible versions of this library are loaded on one page (rendezvous protocols ${census.protocols.join(", ")}). They will not share a client identity: expect one presence entry per version and no synchronous delivery between them. They still sync over the bus. Align the versions to fix it.`
139
- );
154
+ if (process.env.NODE_ENV !== "production") {
155
+ devWarn(
156
+ "UE1008",
157
+ `two incompatible versions of this library are loaded on one page (rendezvous protocols ${census.protocols.join(", ")}). They will not share a client identity: expect one presence entry per version and no synchronous delivery between them. They still sync over the bus. Align the versions to fix it.`
158
+ );
159
+ }
140
160
  }
141
161
  }
142
162
  function busTable() {
@@ -144,6 +164,10 @@ function busTable() {
144
164
  const g = globalThis;
145
165
  return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
146
166
  }
167
+ function skewLedger() {
168
+ const g = globalThis;
169
+ return g[SKEW] ?? (g[SKEW] = /* @__PURE__ */ new Map());
170
+ }
147
171
 
148
172
  // src/transport/broadcast-channel-transport.ts
149
173
  var BroadcastChannelTransport = class {
@@ -183,24 +207,57 @@ var NoopTransport = class {
183
207
  }
184
208
  };
185
209
 
210
+ // src/serializer.ts
211
+ function lossyType(raw, value) {
212
+ if (value === void 0) return raw === void 0 ? "undefined" : "undefined after toJSON";
213
+ if (raw instanceof Date) return "Date";
214
+ if (raw instanceof Map) return "Map";
215
+ if (raw instanceof Set) return "Set";
216
+ if (raw instanceof RegExp) return "RegExp";
217
+ if (ArrayBuffer.isView(raw)) return "TypedArray";
218
+ const type = typeof raw;
219
+ return type === "function" || type === "symbol" ? type : void 0;
220
+ }
221
+ var jsonSerializer = {
222
+ stringify(value) {
223
+ if (value === void 0) {
224
+ throw new TypeError("use-everywhere: cannot serialize undefined");
225
+ }
226
+ return JSON.stringify(value, function replacer(key, forJson) {
227
+ const type = lossyType(this[key], forJson);
228
+ if (type) {
229
+ throw new TypeError(
230
+ `use-everywhere: ${key ? `"${key}" is ` : ""}${type}, which JSON cannot round-trip. https://rxova.org/packages/use-everywhere/guides/serialization/`
231
+ );
232
+ }
233
+ return forJson;
234
+ });
235
+ },
236
+ parse: (text) => JSON.parse(text)
237
+ };
238
+
186
239
  // src/transport/storage-transport.ts
187
240
  var StorageTransport = class {
188
- constructor(name, storage = globalThis.localStorage) {
241
+ constructor(name, storage = globalThis.localStorage, serializer = jsonSerializer) {
189
242
  this.kind = "storage";
190
243
  this.listeners = /* @__PURE__ */ new Set();
191
244
  this.seq = 0;
192
245
  if (!storage) {
193
246
  throw new Error(
194
- "[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
247
+ diagnostic(
248
+ "UE1011",
249
+ "StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
250
+ )
195
251
  );
196
252
  }
253
+ this.serializer = serializer;
197
254
  this.key = `use-everywhere:bus:${name}`;
198
255
  this.storage = storage;
199
256
  this.onStorage = (event) => {
200
257
  if (event.key !== this.key || event.newValue === null) return;
201
258
  let payload;
202
259
  try {
203
- payload = JSON.parse(event.newValue);
260
+ payload = this.serializer.parse(event.newValue);
204
261
  } catch {
205
262
  return;
206
263
  }
@@ -209,7 +266,7 @@ var StorageTransport = class {
209
266
  addEventListener("storage", this.onStorage);
210
267
  }
211
268
  post(data) {
212
- const envelope = JSON.stringify({ seq: this.seq++, data }, rejectUnrepresentable);
269
+ const envelope = this.serializer.stringify({ seq: this.seq++, data });
213
270
  try {
214
271
  this.storage.setItem(this.key, envelope);
215
272
  this.storage.removeItem(this.key);
@@ -225,12 +282,6 @@ var StorageTransport = class {
225
282
  removeEventListener("storage", this.onStorage);
226
283
  }
227
284
  };
228
- function rejectUnrepresentable(_key, value) {
229
- if (typeof value === "function" || typeof value === "symbol") {
230
- throw new TypeError(`${typeof value} values cannot be shared`);
231
- }
232
- return value;
233
- }
234
285
 
235
286
  // src/transport/default-transport.ts
236
287
  function isBroadcastChannelAvailable() {
@@ -252,27 +303,61 @@ function isStorageEventAvailable() {
252
303
  function defaultTransport(name) {
253
304
  if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
254
305
  if (isStorageEventAvailable()) {
306
+ if (process.env.NODE_ENV !== "production") {
307
+ devWarn(
308
+ "UE1009",
309
+ "no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
310
+ );
311
+ }
312
+ return new StorageTransport(name);
313
+ }
314
+ if (process.env.NODE_ENV !== "production") {
255
315
  devWarn(
256
- "[use-everywhere] no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
316
+ "UE1010",
317
+ "no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
257
318
  );
258
- return new StorageTransport(name);
259
319
  }
260
- devWarn(
261
- "[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
262
- );
263
320
  return new NoopTransport();
264
321
  }
265
322
 
266
- // src/bus.ts
323
+ // src/wire.ts
324
+ var WIRE_VERSION = 1;
325
+ function hasEnvelopeShape(wire) {
326
+ return typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
327
+ }
267
328
  function isBusWire(data) {
268
329
  if (typeof data !== "object" || data === null) return false;
269
330
  const wire = data;
270
- return wire.v === 1 && typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
331
+ return wire.v === WIRE_VERSION && hasEnvelopeShape(wire);
271
332
  }
333
+ function foreignWireVersion(data) {
334
+ if (typeof data !== "object" || data === null) return null;
335
+ const wire = data;
336
+ if (typeof wire.v !== "number" || wire.v === WIRE_VERSION) return null;
337
+ return hasEnvelopeShape(wire) ? wire.v : null;
338
+ }
339
+ function recordSkew(name, version) {
340
+ const seen = skewLedger();
341
+ const versions = seen.get(name) ?? /* @__PURE__ */ new Set();
342
+ seen.set(name, versions);
343
+ if (versions.has(version)) return;
344
+ versions.add(version);
345
+ if (process.env.NODE_ENV !== "production") {
346
+ devWarn(
347
+ "UE1007",
348
+ `bus "${name}": a peer speaks wire protocol v${version}, this build speaks v${WIRE_VERSION} \u2014 a ${version > WIRE_VERSION ? "newer" : "older"} deploy. They cannot share state, presence, or a leader seat. https://rxova.org/packages/use-everywhere/under-the-hood/version-skew/`
349
+ );
350
+ }
351
+ }
352
+ function getWireSkew(name) {
353
+ return [...skewLedger().get(name) ?? []].sort((a, b) => a - b);
354
+ }
355
+
356
+ // src/bus.ts
272
357
  function defaultKind() {
273
358
  return typeof document === "undefined" ? "worker" : "tab";
274
359
  }
275
- var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
360
+ var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event", "op"]);
276
361
  function createBusCore(name, options, onShutdown) {
277
362
  const transport = (options.transport ?? defaultTransport)(name);
278
363
  const clientId = newClientId();
@@ -293,6 +378,11 @@ function createBusCore(name, options, onShutdown) {
293
378
  if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
294
379
  };
295
380
  const unsubscribe = transport.subscribe((data) => {
381
+ const foreign = foreignWireVersion(data);
382
+ if (foreign !== null) {
383
+ recordSkew(name, foreign);
384
+ return;
385
+ }
296
386
  if (!isBusWire(data)) return;
297
387
  if (data.clientId === clientId) return;
298
388
  emitBusEvent(name, "in", data);
@@ -385,57 +475,172 @@ function getBus(name, options = {}) {
385
475
  table.set(name, core);
386
476
  } else {
387
477
  if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
388
- devWarn(
389
- `[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
390
- );
478
+ if (process.env.NODE_ENV !== "production") {
479
+ devWarn(
480
+ "UE1004",
481
+ `bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
482
+ );
483
+ }
391
484
  }
392
485
  if (options.kind !== void 0 && options.kind !== core.kind) {
393
- devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
486
+ if (process.env.NODE_ENV !== "production") {
487
+ devWarn("UE1005", `bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
488
+ }
394
489
  }
395
490
  }
396
491
  return core.connect();
397
492
  }
398
493
 
494
+ // src/schema.ts
495
+ function validate(schema, payload) {
496
+ const props = schema["~standard"];
497
+ const result = props.validate(payload);
498
+ if (typeof result.then === "function") {
499
+ return { ok: false, issues: [`the "${props.vendor}" schema validates asynchronously`] };
500
+ }
501
+ const sync = result;
502
+ if (sync.issues) return { ok: false, issues: sync.issues.map((issue) => issue.message) };
503
+ return { ok: true, value: sync.value };
504
+ }
505
+ function createGate(name, schemas, onInvalid) {
506
+ if (!schemas) return void 0;
507
+ const check = (key, payload, direction) => {
508
+ const schema = schemas[key];
509
+ if (!schema) return void 0;
510
+ const result = validate(schema, payload);
511
+ if (result.ok) return void 0;
512
+ const info = { name, key, direction, payload, issues: result.issues };
513
+ if (onInvalid) onInvalid(info);
514
+ else if (process.env.NODE_ENV !== "production") {
515
+ devWarn(
516
+ "UE1003",
517
+ `${name}/${key}: ${direction}bound payload rejected by its schema \u2014 ${result.issues.join("; ")}. https://rxova.org/packages/use-everywhere/guides/validating-payloads/`
518
+ );
519
+ }
520
+ return result.issues;
521
+ };
522
+ return {
523
+ accepts: (key, payload) => !check(key, payload, "in"),
524
+ assert(key, payload) {
525
+ const issues = check(key, payload, "out");
526
+ if (issues) {
527
+ throw new TypeError(
528
+ `use-everywhere: ${name}/${key} \u2014 the value does not match its schema, so nothing was sent or applied. ${issues.join("; ")}`
529
+ );
530
+ }
531
+ }
532
+ };
533
+ }
534
+
399
535
  // src/channel.ts
400
536
  function createChannel(name, options = {}) {
401
537
  const bus = getBus(name, options);
402
538
  const handlers = /* @__PURE__ */ new Map();
539
+ const responders = /* @__PURE__ */ new Map();
540
+ const waiting = /* @__PURE__ */ new Map();
541
+ const gate = createGate(name, options.schema, options.onInvalid);
542
+ const deliver = (type, payload, meta) => {
543
+ const set = handlers.get(type);
544
+ if (!set) return;
545
+ for (const fn of [...set]) fn(payload, meta);
546
+ };
403
547
  const unsubscribe = bus.subscribe((wire) => {
404
548
  if (wire.scope !== "event") return;
405
- const set = handlers.get(wire.type);
406
- if (!set) return;
407
549
  const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
408
- for (const fn of set) fn(wire.payload, meta);
409
- });
410
- let closed = false;
411
- return {
412
- name,
413
- clientId: bus.clientId,
414
- post(type, payload) {
550
+ if (wire.replyTo !== void 0) {
551
+ const settle = waiting.get(wire.replyTo);
552
+ if (!settle) return;
553
+ waiting.delete(wire.replyTo);
554
+ settle(wire.payload);
555
+ return;
556
+ }
557
+ const responder = responders.get(wire.type);
558
+ if (responder) {
415
559
  bus.post({
416
560
  v: 1,
417
561
  scope: "event",
418
- type,
419
- payload,
562
+ type: wire.type,
563
+ payload: responder(wire.payload, meta),
420
564
  clientId: bus.clientId,
421
565
  kind: bus.kind,
422
- msgId: newMsgId()
566
+ msgId: newMsgId(),
567
+ replyTo: wire.msgId
423
568
  });
569
+ }
570
+ if (!handlers.has(wire.type)) return;
571
+ if (gate && !gate.accepts(wire.type, wire.payload)) return;
572
+ deliver(wire.type, wire.payload, meta);
573
+ });
574
+ let closed = false;
575
+ const send = (type, payload, msgId) => {
576
+ gate?.assert(type, payload);
577
+ bus.post({
578
+ v: 1,
579
+ scope: "event",
580
+ type,
581
+ payload,
582
+ clientId: bus.clientId,
583
+ kind: bus.kind,
584
+ msgId
585
+ });
586
+ };
587
+ return {
588
+ name,
589
+ clientId: bus.clientId,
590
+ post(type, payload, options2) {
591
+ send(type, payload, newMsgId());
592
+ if (options2?.echo) {
593
+ deliver(type, payload, { clientId: bus.clientId, kind: bus.kind, self: true });
594
+ }
424
595
  },
425
- on(type, handler) {
596
+ on(type, handler, options2) {
426
597
  let set = handlers.get(type);
427
598
  if (!set) {
428
599
  set = /* @__PURE__ */ new Set();
429
600
  handlers.set(type, set);
430
601
  }
431
- set.add(handler);
432
- return () => set.delete(handler);
602
+ const entry = options2?.once ? (payload, meta) => {
603
+ set.delete(entry);
604
+ handler(payload, meta);
605
+ } : handler;
606
+ set.add(entry);
607
+ return () => set.delete(entry);
608
+ },
609
+ ask(type, payload, options2) {
610
+ const msgId = newMsgId();
611
+ return new Promise((resolve, reject) => {
612
+ const timer = setTimeout(() => {
613
+ waiting.delete(msgId);
614
+ reject(
615
+ new Error(
616
+ `use-everywhere: nobody answered "${type}" on "${name}" within ${String(options2?.timeoutMs ?? 5e3)}ms`
617
+ )
618
+ );
619
+ }, options2?.timeoutMs ?? 5e3);
620
+ waiting.set(msgId, (value) => {
621
+ clearTimeout(timer);
622
+ resolve(value);
623
+ });
624
+ try {
625
+ send(type, payload, msgId);
626
+ } catch (error) {
627
+ clearTimeout(timer);
628
+ waiting.delete(msgId);
629
+ throw error;
630
+ }
631
+ });
632
+ },
633
+ answer(type, responder) {
634
+ responders.set(type, responder);
635
+ return () => responders.delete(type);
433
636
  },
434
637
  close() {
435
638
  if (closed) return;
436
639
  closed = true;
437
640
  unsubscribe();
438
641
  handlers.clear();
642
+ responders.clear();
643
+ waiting.clear();
439
644
  bus.release();
440
645
  }
441
646
  };
@@ -486,15 +691,19 @@ function createSharedStore(name, initial, options = {}) {
486
691
  if (onSharedBus) {
487
692
  const live = liveStores.get(name) ?? 0;
488
693
  if (live > 0) {
489
- devWarn(
490
- `[use-everywhere] second shared store for "${name}" in this tab \u2014 they stay in sync, but you are paying twice for one store's state, subscriptions, and persistence writes. Reuse one per name.`
491
- );
694
+ if (process.env.NODE_ENV !== "production") {
695
+ devWarn(
696
+ "UE1001",
697
+ `second shared store for "${name}" in this tab \u2014 they stay in sync, but you are paying twice for one store's state, subscriptions, and persistence writes. Reuse one per name.`
698
+ );
699
+ }
492
700
  }
493
701
  liveStores.set(name, live + 1);
494
702
  }
495
703
  const bus = getBus(name, options);
496
704
  const clientId = bus.clientId;
497
705
  const accept = options.accept;
706
+ const gate = createGate(name, options.schema, options.onInvalid);
498
707
  const state = { ...initial };
499
708
  const versions = {};
500
709
  for (const k in state) {
@@ -505,23 +714,61 @@ function createSharedStore(name, initial, options = {}) {
505
714
  let versionsSnapshot = Object.freeze({ ...versions });
506
715
  const listeners = /* @__PURE__ */ new Set();
507
716
  const keyListeners = /* @__PURE__ */ new Map();
508
- function notify(key, value, meta) {
717
+ let batchDepth = 0;
718
+ const batched = [];
719
+ const rebuild = () => {
509
720
  snapshot = Object.freeze({ ...state });
510
721
  versionsSnapshot = Object.freeze({ ...versions });
722
+ };
723
+ const emit = (key, value, meta) => {
511
724
  for (const fn of listeners) fn(key, value, meta);
512
725
  const set = keyListeners.get(key);
513
726
  if (set) for (const fn of set) fn();
727
+ };
728
+ function notify(key, value, meta) {
729
+ if (batchDepth > 0) {
730
+ batched.push([key, value, meta]);
731
+ return;
732
+ }
733
+ rebuild();
734
+ emit(key, value, meta);
735
+ }
736
+ function batch(fn) {
737
+ batchDepth++;
738
+ try {
739
+ return fn();
740
+ } finally {
741
+ batchDepth--;
742
+ if (batchDepth === 0 && batched.length > 0) {
743
+ const changes = batched.splice(0, batched.length);
744
+ rebuild();
745
+ for (const [key, value, meta] of changes) emit(key, value, meta);
746
+ }
747
+ }
514
748
  }
515
749
  function applyRemote(key, value, version, meta) {
516
750
  if (!newer(version, versions[key])) return;
751
+ if (gate && !gate.accepts(key, value)) return;
517
752
  versions[key] = version;
518
753
  state[key] = freezeShared(value);
519
754
  notify(key, value, meta);
520
755
  }
521
- const unsubscribe = bus.subscribe((wire) => {
522
- if (wire.scope !== "state") return;
523
- const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
524
- if (wire.type === "hello") {
756
+ const snapshotDelayMs = options.snapshotDelayMs ?? 40;
757
+ let snapshotTimer;
758
+ const written = () => Object.keys(versions).filter((k) => (versions[k]?.[0] ?? 0) > 0);
759
+ const coveredBy = (theirs) => written().every((key) => {
760
+ const mine = versions[key];
761
+ return mine !== void 0 && !newer(mine, theirs[key]);
762
+ });
763
+ const cancelSnapshot = () => {
764
+ clearTimeout(snapshotTimer);
765
+ snapshotTimer = void 0;
766
+ };
767
+ const scheduleSnapshot = () => {
768
+ if (written().length === 0) return;
769
+ if (snapshotTimer !== void 0) return;
770
+ snapshotTimer = setTimeout(() => {
771
+ snapshotTimer = void 0;
525
772
  bus.post({
526
773
  v: 1,
527
774
  scope: "state",
@@ -531,22 +778,36 @@ function createSharedStore(name, initial, options = {}) {
531
778
  state: { ...state },
532
779
  versions: { ...versions }
533
780
  });
781
+ }, Math.random() * snapshotDelayMs);
782
+ };
783
+ const unsubscribe = bus.subscribe((wire) => {
784
+ if (wire.scope !== "state") return;
785
+ const meta = { clientId: wire.clientId, kind: wire.kind, self: false };
786
+ if (wire.type === "hello") {
787
+ scheduleSnapshot();
534
788
  return;
535
789
  }
536
790
  if (accept && !accept(meta)) return;
537
791
  if (wire.type === "patch") {
538
792
  if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
539
793
  applyRemote(wire.key, wire.value, wire.version, meta);
540
- } else {
794
+ return;
795
+ }
796
+ if (wire.type === "snapshot") {
541
797
  const versions2 = wire.versions;
542
798
  if (typeof versions2 !== "object" || versions2 === null) return;
543
- for (const k in wire.state) {
544
- const version = versions2[k];
545
- if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
546
- }
799
+ if (coveredBy(versions2)) cancelSnapshot();
800
+ batch(() => {
801
+ for (const k in wire.state) {
802
+ const version = versions2[k];
803
+ if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
804
+ }
805
+ });
806
+ return;
547
807
  }
548
808
  });
549
809
  function setKey(key, value) {
810
+ gate?.assert(key, value);
550
811
  const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
551
812
  try {
552
813
  bus.post({
@@ -578,29 +839,74 @@ function createSharedStore(name, initial, options = {}) {
578
839
  });
579
840
  const persist = options.persist;
580
841
  let flushPersist;
842
+ let settle = () => {
843
+ };
844
+ const hydrated = persist ? new Promise((resolve) => {
845
+ settle = resolve;
846
+ }) : Promise.resolve();
581
847
  if (persist) {
582
- const { adapter, keys, debounceMs = 100 } = persist;
848
+ const {
849
+ adapter,
850
+ keys,
851
+ debounceMs = 100,
852
+ version: schemaVersion = 0,
853
+ migrate,
854
+ onRestoreError
855
+ } = persist;
583
856
  const shouldPersist = (key) => !keys || keys.includes(key);
584
- const hydrate = (saved2) => {
585
- if (!saved2) return;
586
- for (const key in saved2.state) {
587
- const version = saved2.versions[key];
857
+ const refuse = (error) => {
858
+ if (onRestoreError) onRestoreError(error);
859
+ else if (process.env.NODE_ENV !== "production") {
860
+ devWarn(
861
+ "UE1002",
862
+ `${name}: persisted schema v${error.found} not restored, expected v${error.expected} (${error.reason}). https://rxova.org/packages/use-everywhere/guides/persistence/`
863
+ );
864
+ }
865
+ };
866
+ const reconcile = (saved2) => {
867
+ const found = saved2.schema ?? 0;
868
+ if (found === schemaVersion) return { state: saved2.state, migrated: false };
869
+ if (found > schemaVersion) {
870
+ refuse({ reason: "ahead", found, expected: schemaVersion });
871
+ return void 0;
872
+ }
873
+ if (!migrate) {
874
+ refuse({ reason: "no-migrate", found, expected: schemaVersion });
875
+ return void 0;
876
+ }
877
+ try {
878
+ return { state: migrate(saved2.state, found), migrated: true };
879
+ } catch (cause) {
880
+ refuse({ reason: "migrate-threw", found, expected: schemaVersion, cause });
881
+ return void 0;
882
+ }
883
+ };
884
+ const hydrate = (raw) => {
885
+ const reconciled = raw && reconcile(raw);
886
+ if (!reconciled || !raw) {
887
+ settle();
888
+ return;
889
+ }
890
+ const { state: restored, migrated } = reconciled;
891
+ for (const key in restored) {
892
+ const version = raw.versions[key] ?? (migrated ? [1, clientId] : void 0);
588
893
  if (!version || !shouldPersist(key)) continue;
589
- applyRemote(key, saved2.state[key], version, { clientId, kind: bus.kind, self: true });
894
+ applyRemote(key, restored[key], version, { clientId, kind: bus.kind, self: true });
590
895
  bus.post({
591
896
  v: 1,
592
897
  scope: "state",
593
898
  type: "patch",
594
899
  key,
595
- value: saved2.state[key],
900
+ value: restored[key],
596
901
  version,
597
902
  clientId,
598
903
  kind: bus.kind
599
904
  });
600
905
  }
906
+ settle();
601
907
  };
602
908
  const collect = () => {
603
- const out = { v: 1, state: {}, versions: {} };
909
+ const out = { v: 1, schema: schemaVersion, state: {}, versions: {} };
604
910
  for (const key in versions) {
605
911
  const version = versions[key];
606
912
  if (!version || version[0] === 0 || !shouldPersist(key)) continue;
@@ -640,6 +946,7 @@ function createSharedStore(name, initial, options = {}) {
640
946
  let storeClosed = false;
641
947
  return {
642
948
  clientId,
949
+ hydrated,
643
950
  state: proxy,
644
951
  getSnapshot: () => snapshot,
645
952
  getVersions: () => versionsSnapshot,
@@ -647,6 +954,9 @@ function createSharedStore(name, initial, options = {}) {
647
954
  const next = typeof value === "function" ? value(state[key]) : value;
648
955
  setKey(key, next);
649
956
  },
957
+ transaction(fn) {
958
+ return batch(fn);
959
+ },
650
960
  subscribe(fn) {
651
961
  listeners.add(fn);
652
962
  return () => listeners.delete(fn);
@@ -678,6 +988,7 @@ function createSharedStore(name, initial, options = {}) {
678
988
  if (live > 0) liveStores.set(name, live);
679
989
  else liveStores.delete(name);
680
990
  }
991
+ cancelSnapshot();
681
992
  if (hasWindow) removeEventListener("pageshow", onPageShow);
682
993
  if (flushPersist) {
683
994
  flushPersist();
@@ -693,83 +1004,6 @@ function createSharedStore(name, initial, options = {}) {
693
1004
  };
694
1005
  }
695
1006
 
696
- // src/presence.ts
697
- function createPresence(name, options = {}) {
698
- const pruneAfterMs = options.pruneAfterMs ?? 5e3;
699
- const probeGraceMs = options.probeGraceMs ?? 1e3;
700
- const bus = getBus(name, options);
701
- const peers = /* @__PURE__ */ new Map();
702
- const listeners = /* @__PURE__ */ new Set();
703
- let snapshot = [];
704
- function notify() {
705
- snapshot = Object.freeze([...peers.values()]);
706
- for (const fn of listeners) fn();
707
- }
708
- const unsubscribe = bus.subscribe((wire) => {
709
- if (wire.clientId === bus.clientId) return;
710
- if (wire.scope === "presence" && wire.type === "bye") {
711
- if (peers.delete(wire.clientId)) notify();
712
- return;
713
- }
714
- const existing = peers.get(wire.clientId);
715
- peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
716
- if (!existing) notify();
717
- });
718
- bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
719
- const probed = /* @__PURE__ */ new Map();
720
- const prune = setInterval(
721
- () => {
722
- const now = Date.now();
723
- const silentSince = now - pruneAfterMs;
724
- let changed = false;
725
- let needProbe = false;
726
- for (const [id, peer] of peers) {
727
- if (peer.lastSeen >= silentSince) {
728
- probed.delete(id);
729
- continue;
730
- }
731
- const askedAt = probed.get(id);
732
- if (askedAt === void 0) {
733
- probed.set(id, now);
734
- needProbe = true;
735
- } else if (now - askedAt >= probeGraceMs) {
736
- peers.delete(id);
737
- probed.delete(id);
738
- changed = true;
739
- }
740
- }
741
- if (needProbe) {
742
- bus.post({
743
- v: 1,
744
- scope: "presence",
745
- type: "hello",
746
- clientId: bus.clientId,
747
- kind: bus.kind
748
- });
749
- }
750
- if (changed) notify();
751
- },
752
- Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
753
- );
754
- let closed = false;
755
- return {
756
- clientId: bus.clientId,
757
- getPeers: () => snapshot,
758
- subscribe(fn) {
759
- listeners.add(fn);
760
- return () => listeners.delete(fn);
761
- },
762
- close() {
763
- if (closed) return;
764
- closed = true;
765
- clearInterval(prune);
766
- unsubscribe();
767
- listeners.clear();
768
- bus.release();
769
- }
770
- };
771
- }
772
-
773
1007
  // src/leader-web-locks.ts
774
1008
  var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
775
1009
  function createWebLocksLeader(name, options, locks) {
@@ -1054,6 +1288,246 @@ function createHeartbeatLeader(name, options) {
1054
1288
  };
1055
1289
  }
1056
1290
 
1291
+ // src/shared-reducer.ts
1292
+ function createSharedReducer(name, reducer, initial, options = {}) {
1293
+ const key = options.key ?? "default";
1294
+ const bus = getBus(name, options);
1295
+ const clientId = bus.clientId;
1296
+ const leader = options.leader ?? createLeader(name, {
1297
+ ...options.leaderOptions,
1298
+ // Spread conditionally: `exactOptionalPropertyTypes` distinguishes an
1299
+ // absent transport from one explicitly set to undefined, and the leader
1300
+ // must default its own rather than be handed a hole.
1301
+ ...options.transport ? { transport: options.transport } : {}
1302
+ });
1303
+ const ownsLeader = !options.leader;
1304
+ let committed = initial;
1305
+ let seq = 0;
1306
+ let pending = [];
1307
+ const early = /* @__PURE__ */ new Map();
1308
+ let lastIssued = 0;
1309
+ let view = committed;
1310
+ const listeners = /* @__PURE__ */ new Set();
1311
+ let closed = false;
1312
+ const notify = () => {
1313
+ const next = pending.reduce((state, op) => reducer(state, op.action), committed);
1314
+ if (Object.is(next, view)) return;
1315
+ view = next;
1316
+ for (const fn of listeners) fn();
1317
+ };
1318
+ const askForSnapshot = () => bus.post({ v: 1, scope: "op", type: "hello", key, clientId, kind: bus.kind });
1319
+ const receiveCommit = (at, action, opId) => {
1320
+ if (at <= seq) return;
1321
+ if (at > seq + 1) {
1322
+ early.set(at, { action, opId });
1323
+ askForSnapshot();
1324
+ return;
1325
+ }
1326
+ applyCommit(at, action, opId);
1327
+ notify();
1328
+ };
1329
+ const sequence = (action, opId) => {
1330
+ lastIssued = Math.max(lastIssued, seq) + 1;
1331
+ const at = lastIssued;
1332
+ bus.post({
1333
+ v: 1,
1334
+ scope: "op",
1335
+ type: "commit",
1336
+ key,
1337
+ action,
1338
+ opId,
1339
+ seq: at,
1340
+ clientId,
1341
+ kind: bus.kind
1342
+ });
1343
+ receiveCommit(at, action, opId);
1344
+ };
1345
+ const applyCommit = (at, action, opId) => {
1346
+ committed = reducer(committed, action);
1347
+ seq = at;
1348
+ pending = pending.filter((op) => op.opId !== opId);
1349
+ const next = early.get(seq + 1);
1350
+ if (next) {
1351
+ early.delete(seq + 1);
1352
+ applyCommit(seq + 1, next.action, next.opId);
1353
+ }
1354
+ };
1355
+ const unsubscribe = bus.subscribe((wire) => {
1356
+ if (wire.scope !== "op" || wire.key !== key) return;
1357
+ if (wire.type === "propose") {
1358
+ if (!leader.getSnapshot().isLeader) return;
1359
+ sequence(wire.action, wire.opId);
1360
+ return;
1361
+ }
1362
+ if (wire.type === "commit") {
1363
+ receiveCommit(wire.seq, wire.action, wire.opId);
1364
+ return;
1365
+ }
1366
+ if (wire.type === "hello") {
1367
+ if (seq === 0) return;
1368
+ bus.post({
1369
+ v: 1,
1370
+ scope: "op",
1371
+ type: "snapshot",
1372
+ key,
1373
+ state: committed,
1374
+ seq,
1375
+ clientId,
1376
+ kind: bus.kind
1377
+ });
1378
+ return;
1379
+ }
1380
+ if (wire.type === "snapshot") {
1381
+ if (wire.seq <= seq) return;
1382
+ committed = wire.state;
1383
+ seq = wire.seq;
1384
+ for (const at of [...early.keys()]) if (at <= seq) early.delete(at);
1385
+ const next = early.get(seq + 1);
1386
+ if (next) {
1387
+ early.delete(seq + 1);
1388
+ applyCommit(seq + 1, next.action, next.opId);
1389
+ }
1390
+ notify();
1391
+ return;
1392
+ }
1393
+ });
1394
+ const dispatch = (action) => {
1395
+ if (closed) return;
1396
+ const opId = newMsgId();
1397
+ pending.push({ opId, action });
1398
+ notify();
1399
+ if (leader.getSnapshot().isLeader) sequence(action, opId);
1400
+ else
1401
+ bus.post({ v: 1, scope: "op", type: "propose", key, action, opId, clientId, kind: bus.kind });
1402
+ };
1403
+ askForSnapshot();
1404
+ return {
1405
+ clientId,
1406
+ getSnapshot: () => view,
1407
+ dispatch,
1408
+ subscribe(fn) {
1409
+ listeners.add(fn);
1410
+ return () => listeners.delete(fn);
1411
+ },
1412
+ pendingCount: () => pending.length,
1413
+ close() {
1414
+ if (closed) return;
1415
+ closed = true;
1416
+ unsubscribe();
1417
+ listeners.clear();
1418
+ if (ownsLeader) leader.close();
1419
+ bus.release();
1420
+ }
1421
+ };
1422
+ }
1423
+
1424
+ // src/presence.ts
1425
+ function createPresence(name, options = {}) {
1426
+ const pruneAfterMs = options.pruneAfterMs ?? 5e3;
1427
+ const probeGraceMs = options.probeGraceMs ?? 1e3;
1428
+ const bus = getBus(name, options);
1429
+ const peers = /* @__PURE__ */ new Map();
1430
+ const listeners = /* @__PURE__ */ new Set();
1431
+ let snapshot = [];
1432
+ let metadata = options.metadata;
1433
+ const same = (a, b) => {
1434
+ if (Object.is(a, b)) return true;
1435
+ try {
1436
+ return JSON.stringify(a) === JSON.stringify(b);
1437
+ } catch {
1438
+ return false;
1439
+ }
1440
+ };
1441
+ const self = () => ({
1442
+ id: bus.clientId,
1443
+ kind: bus.kind,
1444
+ lastSeen: Date.now(),
1445
+ ...metadata === void 0 ? {} : { metadata }
1446
+ });
1447
+ function notify() {
1448
+ const roster = [...peers.values()];
1449
+ snapshot = Object.freeze(options.includeSelf ? [self(), ...roster] : roster);
1450
+ for (const fn of listeners) fn();
1451
+ }
1452
+ const announce2 = () => bus.post({
1453
+ v: 1,
1454
+ scope: "presence",
1455
+ type: "hello",
1456
+ clientId: bus.clientId,
1457
+ kind: bus.kind,
1458
+ ...metadata === void 0 ? {} : { metadata }
1459
+ });
1460
+ const unsubscribe = bus.subscribe((wire) => {
1461
+ if (wire.clientId === bus.clientId) return;
1462
+ if (wire.scope === "presence" && wire.type === "bye") {
1463
+ if (peers.delete(wire.clientId)) notify();
1464
+ return;
1465
+ }
1466
+ const existing = peers.get(wire.clientId);
1467
+ const announced = wire.scope === "presence" ? wire.metadata : void 0;
1468
+ const next = announced === void 0 ? existing?.metadata : announced;
1469
+ peers.set(wire.clientId, {
1470
+ id: wire.clientId,
1471
+ kind: wire.kind,
1472
+ lastSeen: Date.now(),
1473
+ ...next === void 0 ? {} : { metadata: next }
1474
+ });
1475
+ if (!existing || !same(existing.metadata, next)) notify();
1476
+ });
1477
+ announce2();
1478
+ if (options.includeSelf) notify();
1479
+ const probed = /* @__PURE__ */ new Map();
1480
+ const prune = setInterval(
1481
+ () => {
1482
+ const now = Date.now();
1483
+ const silentSince = now - pruneAfterMs;
1484
+ let changed = false;
1485
+ let needProbe = false;
1486
+ for (const [id, peer] of peers) {
1487
+ if (peer.lastSeen >= silentSince) {
1488
+ probed.delete(id);
1489
+ continue;
1490
+ }
1491
+ const askedAt = probed.get(id);
1492
+ if (askedAt === void 0) {
1493
+ probed.set(id, now);
1494
+ needProbe = true;
1495
+ } else if (now - askedAt >= probeGraceMs) {
1496
+ peers.delete(id);
1497
+ probed.delete(id);
1498
+ changed = true;
1499
+ }
1500
+ }
1501
+ if (needProbe) announce2();
1502
+ if (changed) notify();
1503
+ },
1504
+ Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
1505
+ );
1506
+ let closed = false;
1507
+ return {
1508
+ clientId: bus.clientId,
1509
+ getPeers: () => snapshot,
1510
+ setMetadata(next) {
1511
+ if (same(metadata, next)) return;
1512
+ metadata = next;
1513
+ announce2();
1514
+ notify();
1515
+ },
1516
+ subscribe(fn) {
1517
+ listeners.add(fn);
1518
+ return () => listeners.delete(fn);
1519
+ },
1520
+ close() {
1521
+ if (closed) return;
1522
+ closed = true;
1523
+ clearInterval(prune);
1524
+ unsubscribe();
1525
+ listeners.clear();
1526
+ bus.release();
1527
+ }
1528
+ };
1529
+ }
1530
+
1057
1531
  // src/persist-web-storage.ts
1058
1532
  function isPersisted(value) {
1059
1533
  if (typeof value !== "object" || value === null) return false;
@@ -1061,6 +1535,7 @@ function isPersisted(value) {
1061
1535
  return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
1062
1536
  }
1063
1537
  function webStorageAdapter(storage, key, options = {}) {
1538
+ const serializer = options.serializer ?? jsonSerializer;
1064
1539
  const report = (error, operation) => {
1065
1540
  try {
1066
1541
  options.onError?.(error, operation);
@@ -1080,7 +1555,7 @@ function webStorageAdapter(storage, key, options = {}) {
1080
1555
  try {
1081
1556
  const raw = resolve("read")?.getItem(key);
1082
1557
  if (!raw) return void 0;
1083
- const parsed = JSON.parse(raw);
1558
+ const parsed = serializer.parse(raw);
1084
1559
  return isPersisted(parsed) ? parsed : void 0;
1085
1560
  } catch (error) {
1086
1561
  report(error, "read");
@@ -1089,7 +1564,7 @@ function webStorageAdapter(storage, key, options = {}) {
1089
1564
  },
1090
1565
  write(snapshot) {
1091
1566
  try {
1092
- resolve("write")?.setItem(key, JSON.stringify(snapshot));
1567
+ resolve("write")?.setItem(key, serializer.stringify(snapshot));
1093
1568
  } catch (error) {
1094
1569
  report(error, "write");
1095
1570
  }
@@ -1110,6 +1585,58 @@ function sessionStorageAdapter(key, options) {
1110
1585
  return webStorageAdapter(() => globalThis.sessionStorage, key, options);
1111
1586
  }
1112
1587
 
1588
+ // src/persist-indexeddb.ts
1589
+ var STORE = "state";
1590
+ var request = (req) => new Promise((resolve, reject) => {
1591
+ req.onsuccess = () => resolve(req.result);
1592
+ req.onerror = () => reject(req.error);
1593
+ });
1594
+ function indexedDbAdapter(key, options = {}) {
1595
+ const database = options.database ?? "use-everywhere";
1596
+ let open;
1597
+ const report = (error, operation) => {
1598
+ try {
1599
+ options.onError?.(error, operation);
1600
+ } catch {
1601
+ }
1602
+ };
1603
+ const db = () => open ?? (open = new Promise((resolve, reject) => {
1604
+ const req = indexedDB.open(database, 1);
1605
+ req.onupgradeneeded = () => req.result.createObjectStore(STORE);
1606
+ req.onsuccess = () => resolve(req.result);
1607
+ req.onerror = () => reject(req.error);
1608
+ req.onblocked = () => reject(new Error("use-everywhere: IndexedDB upgrade blocked"));
1609
+ }));
1610
+ const transact = async (mode, run) => {
1611
+ const connection = await db();
1612
+ return request(run(connection.transaction(STORE, mode).objectStore(STORE)));
1613
+ };
1614
+ return {
1615
+ async read() {
1616
+ try {
1617
+ return await transact("readonly", (store) => store.get(key));
1618
+ } catch (error) {
1619
+ report(error, "read");
1620
+ return void 0;
1621
+ }
1622
+ },
1623
+ async write(snapshot) {
1624
+ try {
1625
+ await transact("readwrite", (store) => store.put(snapshot, key));
1626
+ } catch (error) {
1627
+ report(error, "write");
1628
+ }
1629
+ },
1630
+ async remove() {
1631
+ try {
1632
+ await transact("readwrite", (store) => store.delete(key));
1633
+ } catch (error) {
1634
+ report(error, "remove");
1635
+ }
1636
+ }
1637
+ };
1638
+ }
1639
+
1113
1640
  // src/errors/handshake-timeout-error.ts
1114
1641
  var HandshakeTimeoutError = class extends Error {
1115
1642
  constructor(message = "handshake with the other window timed out") {
@@ -1344,6 +1871,25 @@ function connectToOpener(options) {
1344
1871
  }
1345
1872
  };
1346
1873
  }
1874
+
1875
+ // src/namespace.ts
1876
+ var SEPARATOR = ":";
1877
+ function createNamespace(namespace) {
1878
+ if (!namespace) {
1879
+ throw new TypeError(
1880
+ "use-everywhere: createNamespace() needs a non-empty name \u2014 an empty one would put every bus back on the shared defaults, which is what it exists to prevent."
1881
+ );
1882
+ }
1883
+ const busName = (name = DEFAULT_NAME) => `${namespace}${SEPARATOR}${name}`;
1884
+ return {
1885
+ name: namespace,
1886
+ busName,
1887
+ createSharedStore: (name, initial, options) => createSharedStore(busName(name), initial, options),
1888
+ createChannel: (name, options) => createChannel(busName(name), options),
1889
+ createPresence: (name, options) => createPresence(busName(name), options),
1890
+ createLeader: (name, options) => createLeader(busName(name), options)
1891
+ };
1892
+ }
1347
1893
  // Annotate the CommonJS export names for ESM import in node:
1348
1894
  0 && (module.exports = {
1349
1895
  BroadcastChannelTransport,
@@ -1352,18 +1898,24 @@ function connectToOpener(options) {
1352
1898
  HandshakeTimeoutError,
1353
1899
  NoopTransport,
1354
1900
  StorageTransport,
1901
+ WIRE_VERSION,
1355
1902
  WindowClosedError,
1356
1903
  connectToOpener,
1357
1904
  createChannel,
1358
1905
  createLeader,
1906
+ createNamespace,
1359
1907
  createPresence,
1908
+ createSharedReducer,
1360
1909
  createSharedStore,
1361
1910
  defaultTransport,
1362
1911
  enableDebug,
1363
1912
  getBusNames,
1364
1913
  getTransportKind,
1914
+ getWireSkew,
1915
+ indexedDbAdapter,
1365
1916
  isBroadcastChannelAvailable,
1366
1917
  isStorageEventAvailable,
1918
+ jsonSerializer,
1367
1919
  localStorageAdapter,
1368
1920
  newer,
1369
1921
  observeBus,