@use-everywhere/core 0.5.0 → 0.7.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,15 +18,14 @@ 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,
28
+ StorageTransport: () => StorageTransport,
30
29
  WindowClosedError: () => WindowClosedError,
31
30
  connectToOpener: () => connectToOpener,
32
31
  createChannel: () => createChannel,
@@ -36,7 +35,9 @@ __export(index_exports, {
36
35
  defaultTransport: () => defaultTransport,
37
36
  enableDebug: () => enableDebug,
38
37
  getBusNames: () => getBusNames,
38
+ getTransportKind: () => getTransportKind,
39
39
  isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
40
+ isStorageEventAvailable: () => isStorageEventAvailable,
40
41
  localStorageAdapter: () => localStorageAdapter,
41
42
  newer: () => newer,
42
43
  observeBus: () => observeBus,
@@ -44,7 +45,7 @@ __export(index_exports, {
44
45
  sessionStorageAdapter: () => sessionStorageAdapter,
45
46
  webStorageAdapter: () => webStorageAdapter
46
47
  });
47
- module.exports = __toCommonJS(index_exports);
48
+ module.exports = __toCommonJS(src_exports);
48
49
 
49
50
  // src/defaults.ts
50
51
  var DEFAULT_NAME = "use-everywhere";
@@ -123,9 +124,31 @@ function newMsgId() {
123
124
  return randomHex(16);
124
125
  }
125
126
 
127
+ // src/rendezvous.ts
128
+ var PROTOCOL = 1;
129
+ var TABLE = /* @__PURE__ */ Symbol.for(`use-everywhere.rendezvous.${PROTOCOL}`);
130
+ var CENSUS = /* @__PURE__ */ Symbol.for("use-everywhere.rendezvous.census");
131
+ function announce() {
132
+ const g = globalThis;
133
+ const census = g[CENSUS] ?? (g[CENSUS] = { protocols: [] });
134
+ if (census.protocols.includes(PROTOCOL)) return;
135
+ census.protocols.push(PROTOCOL);
136
+ 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
+ );
140
+ }
141
+ }
142
+ function busTable() {
143
+ announce();
144
+ const g = globalThis;
145
+ return g[TABLE] ?? (g[TABLE] = /* @__PURE__ */ new Map());
146
+ }
147
+
126
148
  // src/transport/broadcast-channel-transport.ts
127
149
  var BroadcastChannelTransport = class {
128
150
  constructor(name) {
151
+ this.kind = "broadcast-channel";
129
152
  this.listeners = /* @__PURE__ */ new Set();
130
153
  this.bc = new BroadcastChannel(name);
131
154
  this.bc.onmessage = (event) => {
@@ -147,6 +170,9 @@ var BroadcastChannelTransport = class {
147
170
 
148
171
  // src/transport/noop-transport.ts
149
172
  var NoopTransport = class {
173
+ constructor() {
174
+ this.kind = "none";
175
+ }
150
176
  post() {
151
177
  }
152
178
  subscribe() {
@@ -157,115 +183,217 @@ var NoopTransport = class {
157
183
  }
158
184
  };
159
185
 
186
+ // src/transport/storage-transport.ts
187
+ var StorageTransport = class {
188
+ constructor(name, storage = globalThis.localStorage) {
189
+ this.kind = "storage";
190
+ this.listeners = /* @__PURE__ */ new Set();
191
+ this.seq = 0;
192
+ if (!storage) {
193
+ throw new Error(
194
+ "[use-everywhere] StorageTransport needs localStorage; workers have none. Use BroadcastChannel there."
195
+ );
196
+ }
197
+ this.key = `use-everywhere:bus:${name}`;
198
+ this.storage = storage;
199
+ this.onStorage = (event) => {
200
+ if (event.key !== this.key || event.newValue === null) return;
201
+ let payload;
202
+ try {
203
+ payload = JSON.parse(event.newValue);
204
+ } catch {
205
+ return;
206
+ }
207
+ for (const listener of this.listeners) listener(payload.data);
208
+ };
209
+ addEventListener("storage", this.onStorage);
210
+ }
211
+ post(data) {
212
+ const envelope = JSON.stringify({ seq: this.seq++, data }, rejectUnrepresentable);
213
+ try {
214
+ this.storage.setItem(this.key, envelope);
215
+ this.storage.removeItem(this.key);
216
+ } catch {
217
+ }
218
+ }
219
+ subscribe(listener) {
220
+ this.listeners.add(listener);
221
+ return () => this.listeners.delete(listener);
222
+ }
223
+ close() {
224
+ this.listeners.clear();
225
+ removeEventListener("storage", this.onStorage);
226
+ }
227
+ };
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
+
160
235
  // src/transport/default-transport.ts
161
236
  function isBroadcastChannelAvailable() {
162
237
  return typeof BroadcastChannel !== "undefined";
163
238
  }
239
+ function isStorageEventAvailable() {
240
+ if (typeof addEventListener !== "function") return false;
241
+ try {
242
+ const storage = globalThis.localStorage;
243
+ if (!storage) return false;
244
+ const probe = "use-everywhere:probe";
245
+ storage.setItem(probe, "1");
246
+ storage.removeItem(probe);
247
+ return true;
248
+ } catch {
249
+ return false;
250
+ }
251
+ }
164
252
  function defaultTransport(name) {
165
- return isBroadcastChannelAvailable() ? new BroadcastChannelTransport(name) : new NoopTransport();
253
+ if (isBroadcastChannelAvailable()) return new BroadcastChannelTransport(name);
254
+ if (isStorageEventAvailable()) {
255
+ devWarn(
256
+ "[use-everywhere] no BroadcastChannel; using the storage-event fallback. Values serialise as JSON, not structured clone \u2014 keep them JSON-shaped."
257
+ );
258
+ return new StorageTransport(name);
259
+ }
260
+ devWarn(
261
+ "[use-everywhere] no BroadcastChannel and no usable localStorage: nothing is shared between tabs. Storage is probably blocked."
262
+ );
263
+ return new NoopTransport();
166
264
  }
167
265
 
168
266
  // src/bus.ts
169
267
  function isBusWire(data) {
170
- return typeof data === "object" && data !== null && data.v === 1;
268
+ if (typeof data !== "object" || data === null) return false;
269
+ const wire = data;
270
+ return wire.v === 1 && typeof wire.scope === "string" && typeof wire.type === "string" && typeof wire.clientId === "string";
171
271
  }
172
272
  function defaultKind() {
173
273
  return typeof document === "undefined" ? "worker" : "tab";
174
274
  }
175
- var registry = /* @__PURE__ */ new Map();
176
- function createBus(name, options, onShutdown) {
275
+ var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
276
+ function createBusCore(name, options, onShutdown) {
177
277
  const transport = (options.transport ?? defaultTransport)(name);
178
278
  const clientId = newClientId();
179
279
  const kind = options.kind ?? defaultKind();
180
- const listeners = /* @__PURE__ */ new Set();
280
+ const handles = /* @__PURE__ */ new Set();
181
281
  let refs = 0;
182
282
  let closed = false;
183
- const post = (wire) => {
283
+ const deliver = (wire, from) => {
284
+ for (const handle of handles) {
285
+ if (handle === from) continue;
286
+ for (const fn of handle.listeners) fn(wire);
287
+ }
288
+ };
289
+ const post = (wire, from) => {
184
290
  if (closed) return;
185
291
  emitBusEvent(name, "out", wire);
186
292
  transport.post(wire);
293
+ if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
187
294
  };
188
295
  const unsubscribe = transport.subscribe((data) => {
189
296
  if (!isBusWire(data)) return;
190
297
  if (data.clientId === clientId) return;
191
298
  emitBusEvent(name, "in", data);
192
299
  if (data.scope === "presence" && data.type === "hello") {
193
- post({ v: 1, scope: "presence", type: "ping", clientId, kind });
300
+ post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null);
194
301
  }
195
- for (const fn of listeners) fn(data);
302
+ deliver(data, null);
196
303
  });
197
- post({ v: 1, scope: "presence", type: "hello", clientId, kind });
304
+ post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
198
305
  const heartbeat = setInterval(
199
- () => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
306
+ () => post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null),
200
307
  options.heartbeatMs ?? 2e3
201
308
  );
202
- const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
309
+ const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind }, null);
203
310
  const onPageShow = (event) => {
204
311
  if (!event.persisted) return;
205
- post({ v: 1, scope: "presence", type: "hello", clientId, kind });
312
+ post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
313
+ };
314
+ const onVisible = () => {
315
+ if (document.visibilityState === "visible") {
316
+ post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
317
+ }
206
318
  };
207
319
  const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
208
320
  if (hasWindow) {
209
321
  addEventListener("pagehide", sayBye);
210
322
  addEventListener("pageshow", onPageShow);
323
+ addEventListener("visibilitychange", onVisible);
211
324
  }
325
+ const shutdown = () => {
326
+ sayBye();
327
+ closed = true;
328
+ clearInterval(heartbeat);
329
+ if (hasWindow) {
330
+ removeEventListener("pagehide", sayBye);
331
+ removeEventListener("pageshow", onPageShow);
332
+ removeEventListener("visibilitychange", onVisible);
333
+ }
334
+ unsubscribe();
335
+ transport.close();
336
+ handles.clear();
337
+ onShutdown();
338
+ };
212
339
  return {
213
340
  name,
214
341
  clientId,
215
342
  kind,
343
+ transportKind: transport.kind ?? "custom",
216
344
  heartbeatMs: options.heartbeatMs ?? 2e3,
217
- post,
218
- subscribe(fn) {
219
- listeners.add(fn);
220
- return () => listeners.delete(fn);
221
- },
222
- acquire() {
345
+ connect() {
346
+ const handle = { listeners: /* @__PURE__ */ new Set() };
347
+ handles.add(handle);
223
348
  refs++;
224
- },
225
- release() {
226
- if (closed) return;
227
- refs--;
228
- if (refs > 0) return;
229
- sayBye();
230
- closed = true;
231
- clearInterval(heartbeat);
232
- if (hasWindow) {
233
- removeEventListener("pagehide", sayBye);
234
- removeEventListener("pageshow", onPageShow);
235
- }
236
- unsubscribe();
237
- transport.close();
238
- listeners.clear();
239
- onShutdown();
349
+ let released = false;
350
+ return {
351
+ name,
352
+ clientId,
353
+ kind,
354
+ transportKind: transport.kind ?? "custom",
355
+ post: (wire) => post(wire, handle),
356
+ subscribe(fn) {
357
+ handle.listeners.add(fn);
358
+ return () => handle.listeners.delete(fn);
359
+ },
360
+ release() {
361
+ if (released || closed) return;
362
+ released = true;
363
+ handles.delete(handle);
364
+ handle.listeners.clear();
365
+ refs--;
366
+ if (refs === 0) shutdown();
367
+ }
368
+ };
240
369
  }
241
370
  };
242
371
  }
243
372
  function getBusNames() {
244
- return [...registry.keys()];
373
+ return [...busTable().keys()];
374
+ }
375
+ function getTransportKind(name) {
376
+ return busTable().get(name)?.transportKind ?? null;
245
377
  }
246
378
  function getBus(name, options = {}) {
247
- if (options.transport) {
248
- const bus2 = createBus(name, options, () => {
249
- });
250
- bus2.acquire();
251
- return bus2;
252
- }
253
- let bus = registry.get(name);
254
- if (!bus) {
255
- bus = createBus(name, options, () => registry.delete(name));
256
- registry.set(name, bus);
379
+ if (options.transport) return createBusCore(name, options, () => {
380
+ }).connect();
381
+ const table = busTable();
382
+ let core = table.get(name);
383
+ if (!core) {
384
+ core = createBusCore(name, options, () => table.delete(name));
385
+ table.set(name, core);
257
386
  } else {
258
- if (options.heartbeatMs !== void 0 && options.heartbeatMs !== bus.heartbeatMs) {
387
+ if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
259
388
  devWarn(
260
389
  `[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
261
390
  );
262
391
  }
263
- if (options.kind !== void 0 && options.kind !== bus.kind) {
392
+ if (options.kind !== void 0 && options.kind !== core.kind) {
264
393
  devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
265
394
  }
266
395
  }
267
- bus.acquire();
268
- return bus;
396
+ return core.connect();
269
397
  }
270
398
 
271
399
  // src/channel.ts
@@ -317,6 +445,9 @@ function createChannel(name, options = {}) {
317
445
  function newer(a, b) {
318
446
  return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
319
447
  }
448
+ function isVersion(value) {
449
+ return Array.isArray(value) && value.length === 2 && typeof value[0] === "number" && Number.isFinite(value[0]) && typeof value[1] === "string";
450
+ }
320
451
 
321
452
  // src/dev-freeze.ts
322
453
  var inDev2 = false;
@@ -356,7 +487,7 @@ function createSharedStore(name, initial, options = {}) {
356
487
  const live = liveStores.get(name) ?? 0;
357
488
  if (live > 0) {
358
489
  devWarn(
359
- `[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.`
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.`
360
491
  );
361
492
  }
362
493
  liveStores.set(name, live + 1);
@@ -404,11 +535,14 @@ function createSharedStore(name, initial, options = {}) {
404
535
  }
405
536
  if (accept && !accept(meta)) return;
406
537
  if (wire.type === "patch") {
538
+ if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
407
539
  applyRemote(wire.key, wire.value, wire.version, meta);
408
540
  } else {
541
+ const versions2 = wire.versions;
542
+ if (typeof versions2 !== "object" || versions2 === null) return;
409
543
  for (const k in wire.state) {
410
- const version = wire.versions[k];
411
- if (version) applyRemote(k, wire.state[k], version, meta);
544
+ const version = versions2[k];
545
+ if (isVersion(version)) applyRemote(k, wire.state[k], version, meta);
412
546
  }
413
547
  }
414
548
  });
@@ -562,6 +696,7 @@ function createSharedStore(name, initial, options = {}) {
562
696
  // src/presence.ts
563
697
  function createPresence(name, options = {}) {
564
698
  const pruneAfterMs = options.pruneAfterMs ?? 5e3;
699
+ const probeGraceMs = options.probeGraceMs ?? 1e3;
565
700
  const bus = getBus(name, options);
566
701
  const peers = /* @__PURE__ */ new Map();
567
702
  const listeners = /* @__PURE__ */ new Set();
@@ -571,6 +706,7 @@ function createPresence(name, options = {}) {
571
706
  for (const fn of listeners) fn();
572
707
  }
573
708
  const unsubscribe = bus.subscribe((wire) => {
709
+ if (wire.clientId === bus.clientId) return;
574
710
  if (wire.scope === "presence" && wire.type === "bye") {
575
711
  if (peers.delete(wire.clientId)) notify();
576
712
  return;
@@ -579,19 +715,41 @@ function createPresence(name, options = {}) {
579
715
  peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
580
716
  if (!existing) notify();
581
717
  });
718
+ bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
719
+ const probed = /* @__PURE__ */ new Map();
582
720
  const prune = setInterval(
583
721
  () => {
584
- const cutoff = Date.now() - pruneAfterMs;
722
+ const now = Date.now();
723
+ const silentSince = now - pruneAfterMs;
585
724
  let changed = false;
725
+ let needProbe = false;
586
726
  for (const [id, peer] of peers) {
587
- if (peer.lastSeen < cutoff) {
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) {
588
736
  peers.delete(id);
737
+ probed.delete(id);
589
738
  changed = true;
590
739
  }
591
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
+ }
592
750
  if (changed) notify();
593
751
  },
594
- Math.max(500, Math.floor(pruneAfterMs / 2))
752
+ Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
595
753
  );
596
754
  let closed = false;
597
755
  return {
@@ -612,9 +770,149 @@ function createPresence(name, options = {}) {
612
770
  };
613
771
  }
614
772
 
615
- // src/leader.ts
773
+ // src/leader-web-locks.ts
616
774
  var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
775
+ function createWebLocksLeader(name, options, locks) {
776
+ const busOptions = {
777
+ ...options.transport ? { transport: options.transport } : {},
778
+ ...options.kind ? { kind: options.kind } : {}
779
+ };
780
+ const bus = getBus(name, busOptions);
781
+ const clientId = bus.clientId;
782
+ let eligible = options.eligible ?? true;
783
+ let leaderId = null;
784
+ let snapshot = NO_LEADER;
785
+ let closed = false;
786
+ const listeners = /* @__PURE__ */ new Set();
787
+ const waiters = /* @__PURE__ */ new Set();
788
+ let releaseHeld = null;
789
+ let pending = null;
790
+ function setLeader(id) {
791
+ if (id === leaderId) return;
792
+ leaderId = id;
793
+ snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
794
+ for (const fn of listeners) fn();
795
+ if (id === clientId) {
796
+ for (const waiter of waiters) waiter.resolve();
797
+ waiters.clear();
798
+ }
799
+ }
800
+ const term = [1, clientId];
801
+ const announce2 = () => bus.post({ v: 1, scope: "leader", type: "heartbeat", term, clientId, kind: bus.kind });
802
+ const unsubscribe = bus.subscribe((wire) => {
803
+ if (wire.scope !== "leader") return;
804
+ if (wire.type === "hello") {
805
+ if (leaderId === clientId) announce2();
806
+ return;
807
+ }
808
+ if (wire.type === "resign") {
809
+ if (wire.clientId === leaderId) setLeader(null);
810
+ return;
811
+ }
812
+ setLeader(wire.clientId);
813
+ });
814
+ function joinQueue() {
815
+ if (closed || !eligible || releaseHeld || pending) return;
816
+ const controller = new AbortController();
817
+ pending = controller;
818
+ const held = new Promise((resolve) => {
819
+ releaseHeld = resolve;
820
+ });
821
+ void locks.request(name, { signal: controller.signal }, async () => {
822
+ pending = null;
823
+ if (closed || !eligible) return;
824
+ setLeader(clientId);
825
+ announce2();
826
+ await held;
827
+ }).catch(() => {
828
+ });
829
+ }
830
+ function letGo() {
831
+ const release = releaseHeld;
832
+ releaseHeld = null;
833
+ release?.();
834
+ }
835
+ function resign() {
836
+ if (leaderId !== clientId) return;
837
+ letGo();
838
+ setLeader(null);
839
+ bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
840
+ joinQueue();
841
+ }
842
+ const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
843
+ const onPageHide = () => resign();
844
+ if (hasWindow) addEventListener("pagehide", onPageHide);
845
+ bus.post({ v: 1, scope: "leader", type: "hello", clientId, kind: bus.kind });
846
+ joinQueue();
847
+ return {
848
+ clientId,
849
+ strategy: "web-locks",
850
+ getSnapshot: () => snapshot,
851
+ subscribe(fn) {
852
+ listeners.add(fn);
853
+ return () => listeners.delete(fn);
854
+ },
855
+ waitForLeadership() {
856
+ if (leaderId === clientId) return Promise.resolve();
857
+ if (closed) return Promise.reject(new Error("leader is closed"));
858
+ return new Promise((resolve, reject) => {
859
+ waiters.add({ resolve, reject });
860
+ });
861
+ },
862
+ resign,
863
+ setEligible(next) {
864
+ if (next === eligible) return;
865
+ eligible = next;
866
+ if (next) {
867
+ joinQueue();
868
+ return;
869
+ }
870
+ if (leaderId === clientId) resignWithoutRequeue();
871
+ pending?.abort();
872
+ pending = null;
873
+ },
874
+ close() {
875
+ if (closed) return;
876
+ closed = true;
877
+ if (leaderId === clientId) resignWithoutRequeue();
878
+ pending?.abort();
879
+ pending = null;
880
+ if (hasWindow) removeEventListener("pagehide", onPageHide);
881
+ for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
882
+ waiters.clear();
883
+ unsubscribe();
884
+ listeners.clear();
885
+ bus.release();
886
+ }
887
+ };
888
+ function resignWithoutRequeue() {
889
+ letGo();
890
+ setLeader(null);
891
+ bus.post({ v: 1, scope: "leader", type: "resign", term, clientId, kind: bus.kind });
892
+ }
893
+ }
894
+
895
+ // src/leader.ts
896
+ var NO_LEADER2 = Object.freeze({ leaderId: null, isLeader: false });
897
+ function availableLocks(options) {
898
+ if (options.locks) return options.locks;
899
+ const locks = globalThis.navigator?.locks;
900
+ return typeof locks?.request === "function" ? locks : void 0;
901
+ }
617
902
  function createLeader(name, options = {}) {
903
+ const strategy = options.strategy ?? "auto";
904
+ if (strategy !== "heartbeat") {
905
+ const locks = availableLocks(options);
906
+ if (locks) return createWebLocksLeader(name, options, locks);
907
+ if (strategy === "web-locks") {
908
+ throw new Error(
909
+ 'strategy: "web-locks" was requested but navigator.locks is unavailable. Web Locks needs a secure context (https, or localhost) \u2014 use "auto" to fall back to the heartbeat election.'
910
+ );
911
+ }
912
+ }
913
+ return createHeartbeatLeader(name, options);
914
+ }
915
+ function createHeartbeatLeader(name, options) {
618
916
  const heartbeatMs = options.heartbeatMs ?? 1e3;
619
917
  const leaseMs = options.leaseMs ?? 3e3;
620
918
  const busOptions = {
@@ -626,16 +924,21 @@ function createLeader(name, options = {}) {
626
924
  let eligible = options.eligible ?? true;
627
925
  let leaderId = null;
628
926
  let term = [0, clientId];
629
- let snapshot = NO_LEADER;
927
+ let snapshot = NO_LEADER2;
630
928
  let beat;
631
929
  let lease;
632
930
  let closed = false;
633
931
  const listeners = /* @__PURE__ */ new Set();
932
+ const waiters = /* @__PURE__ */ new Set();
634
933
  function setLeader(id) {
635
934
  if (id === leaderId) return;
636
935
  leaderId = id;
637
936
  snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
638
937
  for (const fn of listeners) fn();
938
+ if (id === clientId) {
939
+ for (const waiter of waiters) waiter.resolve();
940
+ waiters.clear();
941
+ }
639
942
  }
640
943
  function armLease(delay) {
641
944
  clearTimeout(lease);
@@ -673,6 +976,7 @@ function createLeader(name, options = {}) {
673
976
  armLease(0);
674
977
  return;
675
978
  }
979
+ if (!isVersion(wire.term)) return;
676
980
  if (newer(wire.term, term)) {
677
981
  term = wire.term;
678
982
  stepDown(wire.clientId);
@@ -708,11 +1012,19 @@ function createLeader(name, options = {}) {
708
1012
  armLease(heartbeatMs);
709
1013
  return {
710
1014
  clientId,
1015
+ strategy: "heartbeat",
711
1016
  getSnapshot: () => snapshot,
712
1017
  subscribe(fn) {
713
1018
  listeners.add(fn);
714
1019
  return () => listeners.delete(fn);
715
1020
  },
1021
+ waitForLeadership() {
1022
+ if (leaderId === clientId) return Promise.resolve();
1023
+ if (closed) return Promise.reject(new Error("leader is closed"));
1024
+ return new Promise((resolve, reject) => {
1025
+ waiters.add({ resolve, reject });
1026
+ });
1027
+ },
716
1028
  resign,
717
1029
  setEligible(next) {
718
1030
  if (next === eligible) return;
@@ -727,6 +1039,8 @@ function createLeader(name, options = {}) {
727
1039
  if (closed) return;
728
1040
  closed = true;
729
1041
  resign();
1042
+ for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
1043
+ waiters.clear();
730
1044
  clearInterval(beat);
731
1045
  clearTimeout(lease);
732
1046
  if (hasWindow) {
@@ -1030,68 +1344,14 @@ function connectToOpener(options) {
1030
1344
  }
1031
1345
  };
1032
1346
  }
1033
-
1034
- // src/transport/memory-transport.ts
1035
- var MemoryTransport = class {
1036
- constructor(hub) {
1037
- this.hub = hub;
1038
- this.listeners = /* @__PURE__ */ new Set();
1039
- this.closed = false;
1040
- }
1041
- post(data) {
1042
- if (this.closed) return;
1043
- this.hub.broadcast(this, data);
1044
- }
1045
- subscribe(listener) {
1046
- this.listeners.add(listener);
1047
- return () => this.listeners.delete(listener);
1048
- }
1049
- close() {
1050
- this.closed = true;
1051
- this.listeners.clear();
1052
- this.hub.disconnect(this);
1053
- }
1054
- /** @internal */
1055
- deliver(data) {
1056
- if (this.closed) return;
1057
- for (const listener of this.listeners) listener(data);
1058
- }
1059
- };
1060
-
1061
- // src/transport/memory-hub.ts
1062
- var MemoryHub = class {
1063
- constructor() {
1064
- this.transports = /* @__PURE__ */ new Set();
1065
- }
1066
- connect() {
1067
- const transport = new MemoryTransport(this);
1068
- this.transports.add(transport);
1069
- return transport;
1070
- }
1071
- /** @internal */
1072
- broadcast(from, data) {
1073
- structuredClone(data);
1074
- for (const transport of this.transports) {
1075
- if (transport !== from) {
1076
- const copy = structuredClone(data);
1077
- queueMicrotask(() => transport.deliver(copy));
1078
- }
1079
- }
1080
- }
1081
- /** @internal */
1082
- disconnect(transport) {
1083
- this.transports.delete(transport);
1084
- }
1085
- };
1086
1347
  // Annotate the CommonJS export names for ESM import in node:
1087
1348
  0 && (module.exports = {
1088
1349
  BroadcastChannelTransport,
1089
1350
  CID_PARAM,
1090
1351
  DEFAULT_NAME,
1091
1352
  HandshakeTimeoutError,
1092
- MemoryHub,
1093
- MemoryTransport,
1094
1353
  NoopTransport,
1354
+ StorageTransport,
1095
1355
  WindowClosedError,
1096
1356
  connectToOpener,
1097
1357
  createChannel,
@@ -1101,7 +1361,9 @@ var MemoryHub = class {
1101
1361
  defaultTransport,
1102
1362
  enableDebug,
1103
1363
  getBusNames,
1364
+ getTransportKind,
1104
1365
  isBroadcastChannelAvailable,
1366
+ isStorageEventAvailable,
1105
1367
  localStorageAdapter,
1106
1368
  newer,
1107
1369
  observeBus,