@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.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as Transport } from './transport.types-tQ1cu6Xm.cjs';
1
+ import { T as Transport, a as TransportKind } from './transport.types-CV1WZOhy.cjs';
2
2
 
3
3
  type MessageMap = Record<string, unknown>;
4
4
  type PeerKind = 'tab' | 'worker' | (string & {});
@@ -157,8 +157,15 @@ interface BusOptions extends CommonOptions {
157
157
  }
158
158
 
159
159
  interface PresenceOptions extends BusOptions {
160
- /** Peers silent for longer than this are dropped. Default 5000ms. */
160
+ /** How much silence makes a peer suspect. Default 5000ms. It is then probed, not dropped. */
161
161
  pruneAfterMs?: number;
162
+ /**
163
+ * How long a probed peer has to answer before it is dropped. Default 1000ms.
164
+ *
165
+ * This is a round trip on a same-origin channel, not a heartbeat interval, so
166
+ * it can be short: a peer that is merely throttled still answers at once.
167
+ */
168
+ probeGraceMs?: number;
162
169
  }
163
170
  interface Presence {
164
171
  readonly clientId: string;
@@ -171,7 +178,19 @@ interface Presence {
171
178
  /**
172
179
  * Tracks the other tabs/windows/workers on this bus. Any message from a peer
173
180
  * counts as a liveness signal (state patches, events, and presence pings all
174
- * piggyback); explicit 'bye' or silence past pruneAfterMs removes them.
181
+ * piggyback); an explicit 'bye' removes them at once.
182
+ *
183
+ * Silence, though, is not proof of death — and treating it that way is what
184
+ * made the roster flap. Browsers clamp a hidden tab's timers to roughly one
185
+ * tick a minute, so a perfectly healthy backgrounded peer stops heartbeating,
186
+ * gets pruned, pings once, is re-added, and disappears again: a peer count
187
+ * oscillating once a minute for no reason. Waking a laptop does it to every
188
+ * peer at once.
189
+ *
190
+ * What saves it is that *message handlers are not throttled* — only timers are.
191
+ * A hidden tab still answers a hello the instant it arrives. So a peer that
192
+ * goes quiet is probed rather than dropped, and only silence that survives the
193
+ * probe counts as gone.
175
194
  */
176
195
  declare function createPresence(name: string, options?: PresenceOptions): Presence;
177
196
 
@@ -180,13 +199,35 @@ declare function createPresence(name: string, options?: PresenceOptions): Presen
180
199
  * the leader's re-announce interval, which is a different thing from the bus's
181
200
  * presence ping. See the note in leader.ts about forwarding to getBus.
182
201
  */
202
+ /**
203
+ * How the seat is arbitrated.
204
+ *
205
+ * - `'web-locks'` — the browser's Web Locks API owns the queue. The lock is
206
+ * released by the browser itself when a tab dies, and holding it does not
207
+ * depend on a timer, so a backgrounded tab cannot be deposed for being
208
+ * throttled. No heartbeat traffic at all.
209
+ * - `'heartbeat'` — lease-and-claim over the bus. Works anywhere, including
210
+ * plain-http origins where `navigator.locks` does not exist.
211
+ * - `'auto'` (default) — Web Locks when available, heartbeat otherwise.
212
+ */
213
+ type LeaderStrategy = 'auto' | 'web-locks' | 'heartbeat';
183
214
  interface LeaderOptions extends CommonOptions {
184
- /** How often the leader re-announces itself, in ms. Default 1000. */
215
+ /** How often the leader re-announces itself, in ms. Default 1000. Heartbeat strategy only. */
185
216
  heartbeatMs?: number;
186
- /** How long a follower tolerates silence before calling the seat empty, in ms. Default 3000. */
217
+ /** How long a follower tolerates silence before calling the seat empty, in ms. Default 3000. Heartbeat strategy only. */
187
218
  leaseMs?: number;
188
219
  /** May this client hold the leadership? Default true. */
189
220
  eligible?: boolean;
221
+ /** How to arbitrate the seat. Default 'auto'. */
222
+ strategy?: LeaderStrategy;
223
+ /** @internal Test seam for the Web Locks manager. Defaults to navigator.locks. */
224
+ locks?: LockManagerLike;
225
+ }
226
+ /** The slice of the Web Locks API this library uses. */
227
+ interface LockManagerLike {
228
+ request(name: string, options: {
229
+ signal?: AbortSignal;
230
+ }, callback: () => Promise<void>): Promise<void>;
190
231
  }
191
232
  interface LeaderSnapshot {
192
233
  /** The current leader's clientId, or null while the seat is empty. */
@@ -195,9 +236,17 @@ interface LeaderSnapshot {
195
236
  }
196
237
  interface Leader {
197
238
  readonly clientId: string;
239
+ /** Which mechanism arbitrates this seat — useful in devtools and bug reports. */
240
+ readonly strategy: Exclude<LeaderStrategy, 'auto'>;
198
241
  /** Frozen; a new object only when the leader actually changes. */
199
242
  getSnapshot(): LeaderSnapshot;
200
243
  subscribe(fn: () => void): () => void;
244
+ /**
245
+ * Resolves the moment this client holds the seat, or immediately if it
246
+ * already does. Rejects if the leader is closed while still waiting — so an
247
+ * `await` in a torn-down tab does not hang forever.
248
+ */
249
+ waitForLeadership(): Promise<void>;
201
250
  /** Give up the seat now. Peers take over immediately rather than waiting for the lease. */
202
251
  resign(): void;
203
252
  /** Turn candidacy on or off. Eligibility is a property of the tab, not a component. */
@@ -381,19 +430,23 @@ declare function observeBus(name: string, fn: BusObserver): () => void;
381
430
  /** Log every wire on a bus to the console. Returns a function to stop. */
382
431
  declare function enableDebug(options?: DebugOptions): () => void;
383
432
 
384
- /**
385
- * Get the shared bus for `name`, creating it on first use. Callers must call
386
- * bus.release() exactly once when done. When a custom transport factory is
387
- * given (tests), every call creates an isolated bus — one call = one simulated client.
388
- */
389
433
  /**
390
434
  * Names of the buses currently alive on this page. Buses built with a custom
391
- * transport (tests) bypass the registry, so they are not listed.
435
+ * transport (tests) bypass the table, so they are not listed.
392
436
  */
393
437
  declare function getBusNames(): string[];
438
+ /**
439
+ * What is actually carrying this bus's traffic, or null if it has no bus yet.
440
+ *
441
+ * Answers the question a developer asks when nothing is syncing and the code
442
+ * looks right: *is anything even connected?* `'none'` means no — writes are
443
+ * local and no peer will ever see them.
444
+ */
445
+ declare function getTransportKind(name: string): TransportKind | null;
394
446
 
395
447
  /** Same-origin transport over a real BroadcastChannel. */
396
448
  declare class BroadcastChannelTransport implements Transport {
449
+ readonly kind: TransportKind;
397
450
  private bc;
398
451
  private listeners;
399
452
  constructor(name: string);
@@ -407,13 +460,65 @@ declare class BroadcastChannelTransport implements Transport {
407
460
  * Used for SSR and for state scoped to a single tab.
408
461
  */
409
462
  declare class NoopTransport implements Transport {
463
+ readonly kind: TransportKind;
410
464
  post(): void;
411
465
  subscribe(): () => void;
412
466
  close(): void;
413
467
  }
414
468
 
469
+ /**
470
+ * Cross-tab delivery over the `storage` event, for browsers with no
471
+ * `BroadcastChannel`.
472
+ *
473
+ * The mechanism is a quirk turned to advantage: writing to `localStorage` fires
474
+ * a `storage` event in *every other* same-origin tab and never in the writer —
475
+ * exactly BroadcastChannel's no-self-echo semantics, for free.
476
+ *
477
+ * Two differences from the real thing, both deliberate and both documented:
478
+ *
479
+ * 1. **Fidelity is JSON, not structured clone.** `localStorage` holds strings.
480
+ * A `Date` arrives as an ISO string and a `Map` as `{}`. Values that cannot
481
+ * be represented at all — functions, symbols — are rejected rather than
482
+ * silently dropped, so a write still cannot leave this tab holding something
483
+ * its peers never received.
484
+ * 2. **The entry is removed immediately after writing.** Peers have already been
485
+ * notified by then (the event carries the value), and leaving application
486
+ * state sitting in `localStorage` would be both a quota cost and a privacy
487
+ * one. The removal fires a second event with a null `newValue`, which
488
+ * receivers ignore.
489
+ */
490
+ declare class StorageTransport implements Transport {
491
+ readonly kind: TransportKind;
492
+ private key;
493
+ private storage;
494
+ private listeners;
495
+ private onStorage;
496
+ private seq;
497
+ constructor(name: string, storage?: Storage);
498
+ post(data: unknown): void;
499
+ subscribe(listener: (data: unknown) => void): () => void;
500
+ close(): void;
501
+ }
502
+
415
503
  declare function isBroadcastChannelAvailable(): boolean;
416
- /** Default factory: real BroadcastChannel when available, otherwise a local no-op. */
504
+ /**
505
+ * Can we hear other tabs through the `storage` event?
506
+ *
507
+ * Reading `localStorage` is itself what throws when storage is blocked — a
508
+ * sandboxed iframe, third-party cookies off — so the check has to happen inside
509
+ * a try, not around a property test. Availability is also not writability:
510
+ * Safari's old private mode exposed the object and threw on every setItem.
511
+ */
512
+ declare function isStorageEventAvailable(): boolean;
513
+ /**
514
+ * Pick the best wire this browser can offer, and say so when it is not the
515
+ * good one.
516
+ *
517
+ * The chain matters more than it looks. Before it existed, a context without
518
+ * `BroadcastChannel` got a silent no-op: every hook kept working, every write
519
+ * appeared to succeed, and nothing was ever shared with anybody. That is the
520
+ * worst failure this library can have, because it looks exactly like success.
521
+ */
417
522
  declare function defaultTransport(name: string): Transport;
418
523
 
419
- export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, Transport, type Version, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
524
+ export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, type LeaderStrategy, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, StorageTransport, Transport, TransportKind, type Version, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, isBroadcastChannelAvailable, isStorageEventAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { T as Transport } from './transport.types-tQ1cu6Xm.js';
1
+ import { T as Transport, a as TransportKind } from './transport.types-CV1WZOhy.js';
2
2
 
3
3
  type MessageMap = Record<string, unknown>;
4
4
  type PeerKind = 'tab' | 'worker' | (string & {});
@@ -157,8 +157,15 @@ interface BusOptions extends CommonOptions {
157
157
  }
158
158
 
159
159
  interface PresenceOptions extends BusOptions {
160
- /** Peers silent for longer than this are dropped. Default 5000ms. */
160
+ /** How much silence makes a peer suspect. Default 5000ms. It is then probed, not dropped. */
161
161
  pruneAfterMs?: number;
162
+ /**
163
+ * How long a probed peer has to answer before it is dropped. Default 1000ms.
164
+ *
165
+ * This is a round trip on a same-origin channel, not a heartbeat interval, so
166
+ * it can be short: a peer that is merely throttled still answers at once.
167
+ */
168
+ probeGraceMs?: number;
162
169
  }
163
170
  interface Presence {
164
171
  readonly clientId: string;
@@ -171,7 +178,19 @@ interface Presence {
171
178
  /**
172
179
  * Tracks the other tabs/windows/workers on this bus. Any message from a peer
173
180
  * counts as a liveness signal (state patches, events, and presence pings all
174
- * piggyback); explicit 'bye' or silence past pruneAfterMs removes them.
181
+ * piggyback); an explicit 'bye' removes them at once.
182
+ *
183
+ * Silence, though, is not proof of death — and treating it that way is what
184
+ * made the roster flap. Browsers clamp a hidden tab's timers to roughly one
185
+ * tick a minute, so a perfectly healthy backgrounded peer stops heartbeating,
186
+ * gets pruned, pings once, is re-added, and disappears again: a peer count
187
+ * oscillating once a minute for no reason. Waking a laptop does it to every
188
+ * peer at once.
189
+ *
190
+ * What saves it is that *message handlers are not throttled* — only timers are.
191
+ * A hidden tab still answers a hello the instant it arrives. So a peer that
192
+ * goes quiet is probed rather than dropped, and only silence that survives the
193
+ * probe counts as gone.
175
194
  */
176
195
  declare function createPresence(name: string, options?: PresenceOptions): Presence;
177
196
 
@@ -180,13 +199,35 @@ declare function createPresence(name: string, options?: PresenceOptions): Presen
180
199
  * the leader's re-announce interval, which is a different thing from the bus's
181
200
  * presence ping. See the note in leader.ts about forwarding to getBus.
182
201
  */
202
+ /**
203
+ * How the seat is arbitrated.
204
+ *
205
+ * - `'web-locks'` — the browser's Web Locks API owns the queue. The lock is
206
+ * released by the browser itself when a tab dies, and holding it does not
207
+ * depend on a timer, so a backgrounded tab cannot be deposed for being
208
+ * throttled. No heartbeat traffic at all.
209
+ * - `'heartbeat'` — lease-and-claim over the bus. Works anywhere, including
210
+ * plain-http origins where `navigator.locks` does not exist.
211
+ * - `'auto'` (default) — Web Locks when available, heartbeat otherwise.
212
+ */
213
+ type LeaderStrategy = 'auto' | 'web-locks' | 'heartbeat';
183
214
  interface LeaderOptions extends CommonOptions {
184
- /** How often the leader re-announces itself, in ms. Default 1000. */
215
+ /** How often the leader re-announces itself, in ms. Default 1000. Heartbeat strategy only. */
185
216
  heartbeatMs?: number;
186
- /** How long a follower tolerates silence before calling the seat empty, in ms. Default 3000. */
217
+ /** How long a follower tolerates silence before calling the seat empty, in ms. Default 3000. Heartbeat strategy only. */
187
218
  leaseMs?: number;
188
219
  /** May this client hold the leadership? Default true. */
189
220
  eligible?: boolean;
221
+ /** How to arbitrate the seat. Default 'auto'. */
222
+ strategy?: LeaderStrategy;
223
+ /** @internal Test seam for the Web Locks manager. Defaults to navigator.locks. */
224
+ locks?: LockManagerLike;
225
+ }
226
+ /** The slice of the Web Locks API this library uses. */
227
+ interface LockManagerLike {
228
+ request(name: string, options: {
229
+ signal?: AbortSignal;
230
+ }, callback: () => Promise<void>): Promise<void>;
190
231
  }
191
232
  interface LeaderSnapshot {
192
233
  /** The current leader's clientId, or null while the seat is empty. */
@@ -195,9 +236,17 @@ interface LeaderSnapshot {
195
236
  }
196
237
  interface Leader {
197
238
  readonly clientId: string;
239
+ /** Which mechanism arbitrates this seat — useful in devtools and bug reports. */
240
+ readonly strategy: Exclude<LeaderStrategy, 'auto'>;
198
241
  /** Frozen; a new object only when the leader actually changes. */
199
242
  getSnapshot(): LeaderSnapshot;
200
243
  subscribe(fn: () => void): () => void;
244
+ /**
245
+ * Resolves the moment this client holds the seat, or immediately if it
246
+ * already does. Rejects if the leader is closed while still waiting — so an
247
+ * `await` in a torn-down tab does not hang forever.
248
+ */
249
+ waitForLeadership(): Promise<void>;
201
250
  /** Give up the seat now. Peers take over immediately rather than waiting for the lease. */
202
251
  resign(): void;
203
252
  /** Turn candidacy on or off. Eligibility is a property of the tab, not a component. */
@@ -381,19 +430,23 @@ declare function observeBus(name: string, fn: BusObserver): () => void;
381
430
  /** Log every wire on a bus to the console. Returns a function to stop. */
382
431
  declare function enableDebug(options?: DebugOptions): () => void;
383
432
 
384
- /**
385
- * Get the shared bus for `name`, creating it on first use. Callers must call
386
- * bus.release() exactly once when done. When a custom transport factory is
387
- * given (tests), every call creates an isolated bus — one call = one simulated client.
388
- */
389
433
  /**
390
434
  * Names of the buses currently alive on this page. Buses built with a custom
391
- * transport (tests) bypass the registry, so they are not listed.
435
+ * transport (tests) bypass the table, so they are not listed.
392
436
  */
393
437
  declare function getBusNames(): string[];
438
+ /**
439
+ * What is actually carrying this bus's traffic, or null if it has no bus yet.
440
+ *
441
+ * Answers the question a developer asks when nothing is syncing and the code
442
+ * looks right: *is anything even connected?* `'none'` means no — writes are
443
+ * local and no peer will ever see them.
444
+ */
445
+ declare function getTransportKind(name: string): TransportKind | null;
394
446
 
395
447
  /** Same-origin transport over a real BroadcastChannel. */
396
448
  declare class BroadcastChannelTransport implements Transport {
449
+ readonly kind: TransportKind;
397
450
  private bc;
398
451
  private listeners;
399
452
  constructor(name: string);
@@ -407,13 +460,65 @@ declare class BroadcastChannelTransport implements Transport {
407
460
  * Used for SSR and for state scoped to a single tab.
408
461
  */
409
462
  declare class NoopTransport implements Transport {
463
+ readonly kind: TransportKind;
410
464
  post(): void;
411
465
  subscribe(): () => void;
412
466
  close(): void;
413
467
  }
414
468
 
469
+ /**
470
+ * Cross-tab delivery over the `storage` event, for browsers with no
471
+ * `BroadcastChannel`.
472
+ *
473
+ * The mechanism is a quirk turned to advantage: writing to `localStorage` fires
474
+ * a `storage` event in *every other* same-origin tab and never in the writer —
475
+ * exactly BroadcastChannel's no-self-echo semantics, for free.
476
+ *
477
+ * Two differences from the real thing, both deliberate and both documented:
478
+ *
479
+ * 1. **Fidelity is JSON, not structured clone.** `localStorage` holds strings.
480
+ * A `Date` arrives as an ISO string and a `Map` as `{}`. Values that cannot
481
+ * be represented at all — functions, symbols — are rejected rather than
482
+ * silently dropped, so a write still cannot leave this tab holding something
483
+ * its peers never received.
484
+ * 2. **The entry is removed immediately after writing.** Peers have already been
485
+ * notified by then (the event carries the value), and leaving application
486
+ * state sitting in `localStorage` would be both a quota cost and a privacy
487
+ * one. The removal fires a second event with a null `newValue`, which
488
+ * receivers ignore.
489
+ */
490
+ declare class StorageTransport implements Transport {
491
+ readonly kind: TransportKind;
492
+ private key;
493
+ private storage;
494
+ private listeners;
495
+ private onStorage;
496
+ private seq;
497
+ constructor(name: string, storage?: Storage);
498
+ post(data: unknown): void;
499
+ subscribe(listener: (data: unknown) => void): () => void;
500
+ close(): void;
501
+ }
502
+
415
503
  declare function isBroadcastChannelAvailable(): boolean;
416
- /** Default factory: real BroadcastChannel when available, otherwise a local no-op. */
504
+ /**
505
+ * Can we hear other tabs through the `storage` event?
506
+ *
507
+ * Reading `localStorage` is itself what throws when storage is blocked — a
508
+ * sandboxed iframe, third-party cookies off — so the check has to happen inside
509
+ * a try, not around a property test. Availability is also not writability:
510
+ * Safari's old private mode exposed the object and threw on every setItem.
511
+ */
512
+ declare function isStorageEventAvailable(): boolean;
513
+ /**
514
+ * Pick the best wire this browser can offer, and say so when it is not the
515
+ * good one.
516
+ *
517
+ * The chain matters more than it looks. Before it existed, a context without
518
+ * `BroadcastChannel` got a silent no-op: every hook kept working, every write
519
+ * appeared to succeed, and nothing was ever shared with anybody. That is the
520
+ * worst failure this library can have, because it looks exactly like success.
521
+ */
417
522
  declare function defaultTransport(name: string): Transport;
418
523
 
419
- export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, Transport, type Version, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, isBroadcastChannelAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };
524
+ export { BroadcastChannelTransport, type BusEvent, type BusObserver, type BusWire, CID_PARAM, type Channel, type CommonOptions, type ConnectToOpenerOptions, DEFAULT_NAME, type DebugOptions, HandshakeTimeoutError, type Leader, type LeaderOptions, type LeaderSnapshot, type LeaderStrategy, type MessageEventLike, type MessageMap, type MessageMeta, NoopTransport, type OpenWindowOptions, type OpenedWindow, type OpenerConnection, type Peer, type PeerKind, type PersistAdapter, type PersistOptions, type Persisted, type Presence, type PresenceOptions, type SharedStore, type SharedStoreOptions, type StorageLike, StorageTransport, Transport, TransportKind, type Version, type WebStorageAdapterOptions, WindowClosedError, type WindowEventTarget, type WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, isBroadcastChannelAvailable, isStorageEventAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter };