@use-everywhere/core 0.2.0 → 0.4.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
@@ -1,3 +1,37 @@
1
+ // src/defaults.ts
2
+ var DEFAULT_NAME = "use-everywhere";
3
+
4
+ // src/debug.ts
5
+ var observers = /* @__PURE__ */ new Map();
6
+ function emitBusEvent(name, direction, wire) {
7
+ const set = observers.get(name);
8
+ if (!set) return;
9
+ const event = { name, direction, wire };
10
+ for (const fn of set) fn(event);
11
+ }
12
+ function observeBus(name, fn) {
13
+ let set = observers.get(name);
14
+ if (!set) {
15
+ set = /* @__PURE__ */ new Set();
16
+ observers.set(name, set);
17
+ }
18
+ set.add(fn);
19
+ return () => {
20
+ const current = observers.get(name);
21
+ if (!current) return;
22
+ current.delete(fn);
23
+ if (current.size === 0) observers.delete(name);
24
+ };
25
+ }
26
+ function enableDebug(options = {}) {
27
+ const name = options.name ?? DEFAULT_NAME;
28
+ const log = options.log ?? ((...args) => console.log(...args));
29
+ return observeBus(name, ({ direction, wire }) => {
30
+ const arrow = direction === "out" ? "\u2192" : "\u2190";
31
+ log(`[use-everywhere:${name}] ${arrow} ${wire.scope}/${wire.type}`, wire);
32
+ });
33
+ }
34
+
1
35
  // src/ids.ts
2
36
  function newClientId() {
3
37
  return Math.random().toString(36).slice(2, 8);
@@ -64,11 +98,14 @@ function createBus(name, options, onShutdown) {
64
98
  let refs = 0;
65
99
  let closed = false;
66
100
  const post = (wire) => {
67
- if (!closed) transport.post(wire);
101
+ if (closed) return;
102
+ emitBusEvent(name, "out", wire);
103
+ transport.post(wire);
68
104
  };
69
105
  const unsubscribe = transport.subscribe((data) => {
70
106
  if (!isBusWire(data)) return;
71
107
  if (data.clientId === clientId) return;
108
+ emitBusEvent(name, "in", data);
72
109
  if (data.scope === "presence" && data.type === "hello") {
73
110
  post({ v: 1, scope: "presence", type: "ping", clientId, kind });
74
111
  }
@@ -108,6 +145,9 @@ function createBus(name, options, onShutdown) {
108
145
  }
109
146
  };
110
147
  }
148
+ function getBusNames() {
149
+ return [...registry.keys()];
150
+ }
111
151
  function getBus(name, options = {}) {
112
152
  if (options.transport) {
113
153
  const bus2 = createBus(name, options, () => {
@@ -171,6 +211,24 @@ function newer(a, b) {
171
211
  return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
172
212
  }
173
213
 
214
+ // src/dev-freeze.ts
215
+ var inDev = false;
216
+ try {
217
+ inDev = process.env.NODE_ENV !== "production";
218
+ } catch {
219
+ }
220
+ function deepFreeze(value) {
221
+ if (value === null || typeof value !== "object" || Object.isFrozen(value)) return;
222
+ Object.freeze(value);
223
+ for (const key of Object.keys(value)) {
224
+ deepFreeze(value[key]);
225
+ }
226
+ }
227
+ function freezeShared(value) {
228
+ if (inDev) deepFreeze(value);
229
+ return value;
230
+ }
231
+
174
232
  // src/shared-store.ts
175
233
  function createSharedStore(name, initial, options = {}) {
176
234
  const bus = getBus(name, options);
@@ -178,12 +236,17 @@ function createSharedStore(name, initial, options = {}) {
178
236
  const accept = options.accept;
179
237
  const state = { ...initial };
180
238
  const versions = {};
181
- for (const k in state) versions[k] = [0, clientId];
239
+ for (const k in state) {
240
+ versions[k] = [0, clientId];
241
+ freezeShared(state[k]);
242
+ }
182
243
  let snapshot = Object.freeze({ ...state });
244
+ let versionsSnapshot = Object.freeze({ ...versions });
183
245
  const listeners = /* @__PURE__ */ new Set();
184
246
  const keyListeners = /* @__PURE__ */ new Map();
185
247
  function notify(key, value, meta) {
186
248
  snapshot = Object.freeze({ ...state });
249
+ versionsSnapshot = Object.freeze({ ...versions });
187
250
  for (const fn of listeners) fn(key, value, meta);
188
251
  const set = keyListeners.get(key);
189
252
  if (set) for (const fn of set) fn();
@@ -191,7 +254,7 @@ function createSharedStore(name, initial, options = {}) {
191
254
  function applyRemote(key, value, version, meta) {
192
255
  if (!newer(version, versions[key])) return;
193
256
  versions[key] = version;
194
- state[key] = value;
257
+ state[key] = freezeShared(value);
195
258
  notify(key, value, meta);
196
259
  }
197
260
  const unsubscribe = bus.subscribe((wire) => {
@@ -222,7 +285,7 @@ function createSharedStore(name, initial, options = {}) {
222
285
  function setKey(key, value) {
223
286
  const version = [(versions[key]?.[0] ?? 0) + 1, clientId];
224
287
  versions[key] = version;
225
- state[key] = value;
288
+ state[key] = freezeShared(value);
226
289
  bus.post({
227
290
  v: 1,
228
291
  scope: "state",
@@ -242,11 +305,65 @@ function createSharedStore(name, initial, options = {}) {
242
305
  return true;
243
306
  }
244
307
  });
308
+ const persist = options.persist;
309
+ let flushPersist;
310
+ if (persist) {
311
+ const { adapter, keys, debounceMs = 100 } = persist;
312
+ const shouldPersist = (key) => !keys || keys.includes(key);
313
+ const hydrate = (saved2) => {
314
+ if (!saved2) return;
315
+ for (const key in saved2.state) {
316
+ const version = saved2.versions[key];
317
+ if (!version || !shouldPersist(key)) continue;
318
+ applyRemote(key, saved2.state[key], version, { clientId, kind: bus.kind, self: true });
319
+ bus.post({
320
+ v: 1,
321
+ scope: "state",
322
+ type: "patch",
323
+ key,
324
+ value: saved2.state[key],
325
+ version,
326
+ clientId,
327
+ kind: bus.kind
328
+ });
329
+ }
330
+ };
331
+ const collect = () => {
332
+ const out = { v: 1, state: {}, versions: {} };
333
+ for (const key in versions) {
334
+ const version = versions[key];
335
+ if (!version || version[0] === 0 || !shouldPersist(key)) continue;
336
+ out.state[key] = state[key];
337
+ out.versions[key] = version;
338
+ }
339
+ return out;
340
+ };
341
+ let timer;
342
+ flushPersist = () => {
343
+ clearTimeout(timer);
344
+ timer = void 0;
345
+ void adapter.write(collect());
346
+ };
347
+ const saved = adapter.read();
348
+ if (saved instanceof Promise) {
349
+ void saved.then(hydrate);
350
+ } else {
351
+ hydrate(saved);
352
+ }
353
+ listeners.add(() => {
354
+ if (timer !== void 0) return;
355
+ timer = setTimeout(flushPersist, debounceMs);
356
+ });
357
+ if (typeof document !== "undefined" && typeof addEventListener === "function") {
358
+ addEventListener("pagehide", flushPersist);
359
+ }
360
+ }
245
361
  bus.post({ v: 1, scope: "state", type: "hello", clientId, kind: bus.kind });
246
362
  return {
247
363
  clientId,
248
364
  state: proxy,
249
365
  getSnapshot: () => snapshot,
366
+ getVersions: () => versionsSnapshot,
250
367
  set(key, value) {
251
368
  const next = typeof value === "function" ? value(state[key]) : value;
252
369
  setKey(key, next);
@@ -269,8 +386,15 @@ function createSharedStore(name, initial, options = {}) {
269
386
  versions[key] = [0, clientId];
270
387
  state[key] = initialValue;
271
388
  snapshot = Object.freeze({ ...state });
389
+ versionsSnapshot = Object.freeze({ ...versions });
272
390
  },
273
391
  close() {
392
+ if (flushPersist) {
393
+ flushPersist();
394
+ if (typeof document !== "undefined" && typeof removeEventListener === "function") {
395
+ removeEventListener("pagehide", flushPersist);
396
+ }
397
+ }
274
398
  unsubscribe();
275
399
  listeners.clear();
276
400
  keyListeners.clear();
@@ -329,6 +453,167 @@ function createPresence(name, options = {}) {
329
453
  };
330
454
  }
331
455
 
456
+ // src/leader.ts
457
+ var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
458
+ function createLeader(name, options = {}) {
459
+ const heartbeatMs = options.heartbeatMs ?? 1e3;
460
+ const leaseMs = options.leaseMs ?? 3e3;
461
+ const busOptions = {
462
+ ...options.transport ? { transport: options.transport } : {},
463
+ ...options.kind ? { kind: options.kind } : {}
464
+ };
465
+ const bus = getBus(name, busOptions);
466
+ const clientId = bus.clientId;
467
+ let eligible = options.eligible ?? true;
468
+ let leaderId = null;
469
+ let term = [0, clientId];
470
+ let snapshot = NO_LEADER;
471
+ let beat;
472
+ let lease;
473
+ let closed = false;
474
+ const listeners = /* @__PURE__ */ new Set();
475
+ function setLeader(id) {
476
+ if (id === leaderId) return;
477
+ leaderId = id;
478
+ snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
479
+ for (const fn of listeners) fn();
480
+ }
481
+ function armLease(delay) {
482
+ clearTimeout(lease);
483
+ lease = setTimeout(onLeaseExpired, delay);
484
+ }
485
+ function onLeaseExpired() {
486
+ if (closed) return;
487
+ setLeader(null);
488
+ if (eligible) claim();
489
+ }
490
+ function heartbeat() {
491
+ bus.post({ v: 1, scope: "leader", type: "heartbeat", term, clientId, kind: bus.kind });
492
+ }
493
+ function claim() {
494
+ term = [term[0] + 1, clientId];
495
+ setLeader(clientId);
496
+ bus.post({ v: 1, scope: "leader", type: "claim", term, clientId, kind: bus.kind });
497
+ clearInterval(beat);
498
+ beat = setInterval(heartbeat, heartbeatMs);
499
+ }
500
+ function stepDown(next) {
501
+ clearInterval(beat);
502
+ beat = void 0;
503
+ setLeader(next);
504
+ }
505
+ const unsubscribe = bus.subscribe((wire) => {
506
+ if (wire.scope !== "leader") return;
507
+ if (wire.type === "hello") {
508
+ if (leaderId === clientId) heartbeat();
509
+ return;
510
+ }
511
+ if (wire.type === "resign") {
512
+ if (wire.clientId !== leaderId) return;
513
+ setLeader(null);
514
+ armLease(0);
515
+ return;
516
+ }
517
+ if (newer(wire.term, term)) {
518
+ term = wire.term;
519
+ stepDown(wire.clientId);
520
+ armLease(leaseMs);
521
+ return;
522
+ }
523
+ if (leaderId === clientId) {
524
+ heartbeat();
525
+ return;
526
+ }
527
+ if (wire.clientId === leaderId) armLease(leaseMs);
528
+ });
529
+ function resign() {
530
+ if (leaderId !== clientId) return;
531
+ const resigning = term;
532
+ stepDown(null);
533
+ bus.post({ v: 1, scope: "leader", type: "resign", term: resigning, clientId, kind: bus.kind });
534
+ armLease(leaseMs);
535
+ }
536
+ const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
537
+ const onPageHide = () => resign();
538
+ if (hasWindow) addEventListener("pagehide", onPageHide);
539
+ bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
540
+ armLease(heartbeatMs);
541
+ return {
542
+ clientId,
543
+ getSnapshot: () => snapshot,
544
+ subscribe(fn) {
545
+ listeners.add(fn);
546
+ return () => listeners.delete(fn);
547
+ },
548
+ resign,
549
+ setEligible(next) {
550
+ if (next === eligible) return;
551
+ eligible = next;
552
+ if (!next) {
553
+ if (leaderId === clientId) resign();
554
+ return;
555
+ }
556
+ if (leaderId === null) armLease(0);
557
+ },
558
+ close() {
559
+ closed = true;
560
+ resign();
561
+ clearInterval(beat);
562
+ clearTimeout(lease);
563
+ if (hasWindow) removeEventListener("pagehide", onPageHide);
564
+ unsubscribe();
565
+ listeners.clear();
566
+ bus.release();
567
+ }
568
+ };
569
+ }
570
+
571
+ // src/persist-web-storage.ts
572
+ function isPersisted(value) {
573
+ if (typeof value !== "object" || value === null) return false;
574
+ const candidate = value;
575
+ return candidate.v === 1 && typeof candidate.state === "object" && typeof candidate.versions === "object";
576
+ }
577
+ function webStorageAdapter(storage, key) {
578
+ const resolve = () => {
579
+ try {
580
+ return typeof storage === "function" ? storage() : storage;
581
+ } catch {
582
+ return void 0;
583
+ }
584
+ };
585
+ return {
586
+ read() {
587
+ try {
588
+ const raw = resolve()?.getItem(key);
589
+ if (!raw) return void 0;
590
+ const parsed = JSON.parse(raw);
591
+ return isPersisted(parsed) ? parsed : void 0;
592
+ } catch {
593
+ return void 0;
594
+ }
595
+ },
596
+ write(snapshot) {
597
+ try {
598
+ resolve()?.setItem(key, JSON.stringify(snapshot));
599
+ } catch {
600
+ }
601
+ },
602
+ remove() {
603
+ try {
604
+ resolve()?.removeItem(key);
605
+ } catch {
606
+ }
607
+ }
608
+ };
609
+ }
610
+ function localStorageAdapter(key) {
611
+ return webStorageAdapter(() => globalThis.localStorage, key);
612
+ }
613
+ function sessionStorageAdapter(key) {
614
+ return webStorageAdapter(() => globalThis.sessionStorage, key);
615
+ }
616
+
332
617
  // src/errors/handshake-timeout-error.ts
333
618
  var HandshakeTimeoutError = class extends Error {
334
619
  constructor(message = "handshake with the other window timed out") {
@@ -615,6 +900,7 @@ var MemoryHub = class {
615
900
  export {
616
901
  BroadcastChannelTransport,
617
902
  CID_PARAM,
903
+ DEFAULT_NAME,
618
904
  HandshakeTimeoutError,
619
905
  MemoryHub,
620
906
  MemoryTransport,
@@ -622,10 +908,17 @@ export {
622
908
  WindowClosedError,
623
909
  connectToOpener,
624
910
  createChannel,
911
+ createLeader,
625
912
  createPresence,
626
913
  createSharedStore,
627
914
  defaultTransport,
915
+ enableDebug,
916
+ getBusNames,
628
917
  isBroadcastChannelAvailable,
918
+ localStorageAdapter,
629
919
  newer,
630
- openWindow
920
+ observeBus,
921
+ openWindow,
922
+ sessionStorageAdapter,
923
+ webStorageAdapter
631
924
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@use-everywhere/core",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "description": "Cross-tab shared state, events, presence, and cross-origin window channels",
5
5
  "license": "MIT",
6
6
  "author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
@@ -25,12 +25,19 @@
25
25
  },
26
26
  "type": "module",
27
27
  "sideEffects": false,
28
- "main": "./dist/index.js",
28
+ "main": "./dist/index.cjs",
29
+ "module": "./dist/index.js",
29
30
  "types": "./dist/index.d.ts",
30
31
  "exports": {
31
32
  ".": {
32
- "types": "./dist/index.d.ts",
33
- "import": "./dist/index.js"
33
+ "import": {
34
+ "types": "./dist/index.d.ts",
35
+ "default": "./dist/index.js"
36
+ },
37
+ "require": {
38
+ "types": "./dist/index.d.cts",
39
+ "default": "./dist/index.cjs"
40
+ }
34
41
  }
35
42
  },
36
43
  "files": [
@@ -41,13 +48,19 @@
41
48
  "name": "everything (import *)",
42
49
  "path": "dist/index.js",
43
50
  "import": "*",
44
- "limit": "3.5 kB"
51
+ "limit": "4.5 kB"
52
+ },
53
+ {
54
+ "name": "createLeader",
55
+ "path": "dist/index.js",
56
+ "import": "{ createLeader }",
57
+ "limit": "1.3 kB"
45
58
  },
46
59
  {
47
60
  "name": "createSharedStore",
48
61
  "path": "dist/index.js",
49
62
  "import": "{ createSharedStore }",
50
- "limit": "1.3 kB"
63
+ "limit": "1.6 kB"
51
64
  },
52
65
  {
53
66
  "name": "createChannel",
@@ -78,15 +91,27 @@
78
91
  "path": "dist/index.js",
79
92
  "import": "{ MemoryHub }",
80
93
  "limit": "300 B"
94
+ },
95
+ {
96
+ "name": "observeBus + enableDebug",
97
+ "path": "dist/index.js",
98
+ "import": "{ observeBus, enableDebug }",
99
+ "limit": "500 B"
100
+ },
101
+ {
102
+ "name": "localStorageAdapter",
103
+ "path": "dist/index.js",
104
+ "import": "{ localStorageAdapter }",
105
+ "limit": "500 B"
81
106
  }
82
107
  ],
83
108
  "devDependencies": {
84
- "@size-limit/preset-small-lib": "^12.1.0",
109
+ "@size-limit/preset-small-lib": "^13.0.1",
85
110
  "@vitest/coverage-v8": "^4.1.10",
86
- "happy-dom": "^20.10.6",
87
- "size-limit": "^12.1.0",
88
- "tsup": "^8.5.0",
89
- "typescript": "^5.8.3",
111
+ "happy-dom": "^20.11.1",
112
+ "size-limit": "^13.0.1",
113
+ "tsup": "^8.5.1",
114
+ "typescript": "^6.0.3",
90
115
  "vitest": "^4.1.10"
91
116
  },
92
117
  "scripts": {