@use-everywhere/core 0.6.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
@@ -25,6 +25,7 @@ __export(src_exports, {
25
25
  DEFAULT_NAME: () => DEFAULT_NAME,
26
26
  HandshakeTimeoutError: () => HandshakeTimeoutError,
27
27
  NoopTransport: () => NoopTransport,
28
+ StorageTransport: () => StorageTransport,
28
29
  WindowClosedError: () => WindowClosedError,
29
30
  connectToOpener: () => connectToOpener,
30
31
  createChannel: () => createChannel,
@@ -34,7 +35,9 @@ __export(src_exports, {
34
35
  defaultTransport: () => defaultTransport,
35
36
  enableDebug: () => enableDebug,
36
37
  getBusNames: () => getBusNames,
38
+ getTransportKind: () => getTransportKind,
37
39
  isBroadcastChannelAvailable: () => isBroadcastChannelAvailable,
40
+ isStorageEventAvailable: () => isStorageEventAvailable,
38
41
  localStorageAdapter: () => localStorageAdapter,
39
42
  newer: () => newer,
40
43
  observeBus: () => observeBus,
@@ -121,9 +124,31 @@ function newMsgId() {
121
124
  return randomHex(16);
122
125
  }
123
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
+
124
148
  // src/transport/broadcast-channel-transport.ts
125
149
  var BroadcastChannelTransport = class {
126
150
  constructor(name) {
151
+ this.kind = "broadcast-channel";
127
152
  this.listeners = /* @__PURE__ */ new Set();
128
153
  this.bc = new BroadcastChannel(name);
129
154
  this.bc.onmessage = (event) => {
@@ -145,6 +170,9 @@ var BroadcastChannelTransport = class {
145
170
 
146
171
  // src/transport/noop-transport.ts
147
172
  var NoopTransport = class {
173
+ constructor() {
174
+ this.kind = "none";
175
+ }
148
176
  post() {
149
177
  }
150
178
  subscribe() {
@@ -155,115 +183,217 @@ var NoopTransport = class {
155
183
  }
156
184
  };
157
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
+
158
235
  // src/transport/default-transport.ts
159
236
  function isBroadcastChannelAvailable() {
160
237
  return typeof BroadcastChannel !== "undefined";
161
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
+ }
162
252
  function defaultTransport(name) {
163
- 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();
164
264
  }
165
265
 
166
266
  // src/bus.ts
167
267
  function isBusWire(data) {
168
- 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";
169
271
  }
170
272
  function defaultKind() {
171
273
  return typeof document === "undefined" ? "worker" : "tab";
172
274
  }
173
- var registry = /* @__PURE__ */ new Map();
174
- function createBus(name, options, onShutdown) {
275
+ var SHARED_WITHIN_CLIENT = /* @__PURE__ */ new Set(["state", "event"]);
276
+ function createBusCore(name, options, onShutdown) {
175
277
  const transport = (options.transport ?? defaultTransport)(name);
176
278
  const clientId = newClientId();
177
279
  const kind = options.kind ?? defaultKind();
178
- const listeners = /* @__PURE__ */ new Set();
280
+ const handles = /* @__PURE__ */ new Set();
179
281
  let refs = 0;
180
282
  let closed = false;
181
- 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) => {
182
290
  if (closed) return;
183
291
  emitBusEvent(name, "out", wire);
184
292
  transport.post(wire);
293
+ if (SHARED_WITHIN_CLIENT.has(wire.scope)) deliver(wire, from);
185
294
  };
186
295
  const unsubscribe = transport.subscribe((data) => {
187
296
  if (!isBusWire(data)) return;
188
297
  if (data.clientId === clientId) return;
189
298
  emitBusEvent(name, "in", data);
190
299
  if (data.scope === "presence" && data.type === "hello") {
191
- post({ v: 1, scope: "presence", type: "ping", clientId, kind });
300
+ post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null);
192
301
  }
193
- for (const fn of listeners) fn(data);
302
+ deliver(data, null);
194
303
  });
195
- post({ v: 1, scope: "presence", type: "hello", clientId, kind });
304
+ post({ v: 1, scope: "presence", type: "hello", clientId, kind }, null);
196
305
  const heartbeat = setInterval(
197
- () => post({ v: 1, scope: "presence", type: "ping", clientId, kind }),
306
+ () => post({ v: 1, scope: "presence", type: "ping", clientId, kind }, null),
198
307
  options.heartbeatMs ?? 2e3
199
308
  );
200
- const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind });
309
+ const sayBye = () => post({ v: 1, scope: "presence", type: "bye", clientId, kind }, null);
201
310
  const onPageShow = (event) => {
202
311
  if (!event.persisted) return;
203
- 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
+ }
204
318
  };
205
319
  const hasWindow = typeof document !== "undefined" && typeof addEventListener === "function";
206
320
  if (hasWindow) {
207
321
  addEventListener("pagehide", sayBye);
208
322
  addEventListener("pageshow", onPageShow);
323
+ addEventListener("visibilitychange", onVisible);
209
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
+ };
210
339
  return {
211
340
  name,
212
341
  clientId,
213
342
  kind,
343
+ transportKind: transport.kind ?? "custom",
214
344
  heartbeatMs: options.heartbeatMs ?? 2e3,
215
- post,
216
- subscribe(fn) {
217
- listeners.add(fn);
218
- return () => listeners.delete(fn);
219
- },
220
- acquire() {
345
+ connect() {
346
+ const handle = { listeners: /* @__PURE__ */ new Set() };
347
+ handles.add(handle);
221
348
  refs++;
222
- },
223
- release() {
224
- if (closed) return;
225
- refs--;
226
- if (refs > 0) return;
227
- sayBye();
228
- closed = true;
229
- clearInterval(heartbeat);
230
- if (hasWindow) {
231
- removeEventListener("pagehide", sayBye);
232
- removeEventListener("pageshow", onPageShow);
233
- }
234
- unsubscribe();
235
- transport.close();
236
- listeners.clear();
237
- 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
+ };
238
369
  }
239
370
  };
240
371
  }
241
372
  function getBusNames() {
242
- return [...registry.keys()];
373
+ return [...busTable().keys()];
374
+ }
375
+ function getTransportKind(name) {
376
+ return busTable().get(name)?.transportKind ?? null;
243
377
  }
244
378
  function getBus(name, options = {}) {
245
- if (options.transport) {
246
- const bus2 = createBus(name, options, () => {
247
- });
248
- bus2.acquire();
249
- return bus2;
250
- }
251
- let bus = registry.get(name);
252
- if (!bus) {
253
- bus = createBus(name, options, () => registry.delete(name));
254
- 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);
255
386
  } else {
256
- if (options.heartbeatMs !== void 0 && options.heartbeatMs !== bus.heartbeatMs) {
387
+ if (options.heartbeatMs !== void 0 && options.heartbeatMs !== core.heartbeatMs) {
257
388
  devWarn(
258
389
  `[use-everywhere] bus "${name}": heartbeatMs ignored \u2014 the first creator fixes bus options`
259
390
  );
260
391
  }
261
- if (options.kind !== void 0 && options.kind !== bus.kind) {
392
+ if (options.kind !== void 0 && options.kind !== core.kind) {
262
393
  devWarn(`[use-everywhere] bus "${name}": kind ignored \u2014 the first creator fixes bus options`);
263
394
  }
264
395
  }
265
- bus.acquire();
266
- return bus;
396
+ return core.connect();
267
397
  }
268
398
 
269
399
  // src/channel.ts
@@ -315,6 +445,9 @@ function createChannel(name, options = {}) {
315
445
  function newer(a, b) {
316
446
  return !b || a[0] > b[0] || a[0] === b[0] && a[1] > b[1];
317
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
+ }
318
451
 
319
452
  // src/dev-freeze.ts
320
453
  var inDev2 = false;
@@ -354,7 +487,7 @@ function createSharedStore(name, initial, options = {}) {
354
487
  const live = liveStores.get(name) ?? 0;
355
488
  if (live > 0) {
356
489
  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.`
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.`
358
491
  );
359
492
  }
360
493
  liveStores.set(name, live + 1);
@@ -402,11 +535,14 @@ function createSharedStore(name, initial, options = {}) {
402
535
  }
403
536
  if (accept && !accept(meta)) return;
404
537
  if (wire.type === "patch") {
538
+ if (typeof wire.key !== "string" || !isVersion(wire.version)) return;
405
539
  applyRemote(wire.key, wire.value, wire.version, meta);
406
540
  } else {
541
+ const versions2 = wire.versions;
542
+ if (typeof versions2 !== "object" || versions2 === null) return;
407
543
  for (const k in wire.state) {
408
- const version = wire.versions[k];
409
- 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);
410
546
  }
411
547
  }
412
548
  });
@@ -560,6 +696,7 @@ function createSharedStore(name, initial, options = {}) {
560
696
  // src/presence.ts
561
697
  function createPresence(name, options = {}) {
562
698
  const pruneAfterMs = options.pruneAfterMs ?? 5e3;
699
+ const probeGraceMs = options.probeGraceMs ?? 1e3;
563
700
  const bus = getBus(name, options);
564
701
  const peers = /* @__PURE__ */ new Map();
565
702
  const listeners = /* @__PURE__ */ new Set();
@@ -569,6 +706,7 @@ function createPresence(name, options = {}) {
569
706
  for (const fn of listeners) fn();
570
707
  }
571
708
  const unsubscribe = bus.subscribe((wire) => {
709
+ if (wire.clientId === bus.clientId) return;
572
710
  if (wire.scope === "presence" && wire.type === "bye") {
573
711
  if (peers.delete(wire.clientId)) notify();
574
712
  return;
@@ -577,19 +715,41 @@ function createPresence(name, options = {}) {
577
715
  peers.set(wire.clientId, { id: wire.clientId, kind: wire.kind, lastSeen: Date.now() });
578
716
  if (!existing) notify();
579
717
  });
718
+ bus.post({ v: 1, scope: "presence", type: "hello", clientId: bus.clientId, kind: bus.kind });
719
+ const probed = /* @__PURE__ */ new Map();
580
720
  const prune = setInterval(
581
721
  () => {
582
- const cutoff = Date.now() - pruneAfterMs;
722
+ const now = Date.now();
723
+ const silentSince = now - pruneAfterMs;
583
724
  let changed = false;
725
+ let needProbe = false;
584
726
  for (const [id, peer] of peers) {
585
- 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) {
586
736
  peers.delete(id);
737
+ probed.delete(id);
587
738
  changed = true;
588
739
  }
589
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
+ }
590
750
  if (changed) notify();
591
751
  },
592
- Math.max(500, Math.floor(pruneAfterMs / 2))
752
+ Math.max(250, Math.floor(Math.min(pruneAfterMs, probeGraceMs) / 2))
593
753
  );
594
754
  let closed = false;
595
755
  return {
@@ -610,9 +770,149 @@ function createPresence(name, options = {}) {
610
770
  };
611
771
  }
612
772
 
613
- // src/leader.ts
773
+ // src/leader-web-locks.ts
614
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
+ }
615
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) {
616
916
  const heartbeatMs = options.heartbeatMs ?? 1e3;
617
917
  const leaseMs = options.leaseMs ?? 3e3;
618
918
  const busOptions = {
@@ -624,16 +924,21 @@ function createLeader(name, options = {}) {
624
924
  let eligible = options.eligible ?? true;
625
925
  let leaderId = null;
626
926
  let term = [0, clientId];
627
- let snapshot = NO_LEADER;
927
+ let snapshot = NO_LEADER2;
628
928
  let beat;
629
929
  let lease;
630
930
  let closed = false;
631
931
  const listeners = /* @__PURE__ */ new Set();
932
+ const waiters = /* @__PURE__ */ new Set();
632
933
  function setLeader(id) {
633
934
  if (id === leaderId) return;
634
935
  leaderId = id;
635
936
  snapshot = Object.freeze({ leaderId: id, isLeader: id === clientId });
636
937
  for (const fn of listeners) fn();
938
+ if (id === clientId) {
939
+ for (const waiter of waiters) waiter.resolve();
940
+ waiters.clear();
941
+ }
637
942
  }
638
943
  function armLease(delay) {
639
944
  clearTimeout(lease);
@@ -671,6 +976,7 @@ function createLeader(name, options = {}) {
671
976
  armLease(0);
672
977
  return;
673
978
  }
979
+ if (!isVersion(wire.term)) return;
674
980
  if (newer(wire.term, term)) {
675
981
  term = wire.term;
676
982
  stepDown(wire.clientId);
@@ -706,11 +1012,19 @@ function createLeader(name, options = {}) {
706
1012
  armLease(heartbeatMs);
707
1013
  return {
708
1014
  clientId,
1015
+ strategy: "heartbeat",
709
1016
  getSnapshot: () => snapshot,
710
1017
  subscribe(fn) {
711
1018
  listeners.add(fn);
712
1019
  return () => listeners.delete(fn);
713
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
+ },
714
1028
  resign,
715
1029
  setEligible(next) {
716
1030
  if (next === eligible) return;
@@ -725,6 +1039,8 @@ function createLeader(name, options = {}) {
725
1039
  if (closed) return;
726
1040
  closed = true;
727
1041
  resign();
1042
+ for (const waiter of waiters) waiter.reject(new Error("leader is closed"));
1043
+ waiters.clear();
728
1044
  clearInterval(beat);
729
1045
  clearTimeout(lease);
730
1046
  if (hasWindow) {
@@ -1035,6 +1351,7 @@ function connectToOpener(options) {
1035
1351
  DEFAULT_NAME,
1036
1352
  HandshakeTimeoutError,
1037
1353
  NoopTransport,
1354
+ StorageTransport,
1038
1355
  WindowClosedError,
1039
1356
  connectToOpener,
1040
1357
  createChannel,
@@ -1044,7 +1361,9 @@ function connectToOpener(options) {
1044
1361
  defaultTransport,
1045
1362
  enableDebug,
1046
1363
  getBusNames,
1364
+ getTransportKind,
1047
1365
  isBroadcastChannelAvailable,
1366
+ isStorageEventAvailable,
1048
1367
  localStorageAdapter,
1049
1368
  newer,
1050
1369
  observeBus,