use-everywhere 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.d.cts CHANGED
@@ -1,5 +1,5 @@
1
- import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
- export { BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, NoopTransport, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, Presence, PresenceOptions, SharedStore, SharedStoreOptions, StorageLike, StorageTransport, Transport, TransportKind, Version, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, isBroadcastChannelAvailable, isStorageEventAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
1
+ import { SharedStore, LeaderOptions, Leader, MessageMap, ReplyMap, Channel, MessageMeta, ChannelOptions, PersistAdapter, RestoreError, Peer, LeaderSnapshot, Namespace, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
+ export { AskOptions, BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, ChannelOptions, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, IndexedDbAdapterOptions, InvalidPayload, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, Namespace, NoopTransport, OnInvalid, OnOptions, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, PostOptions, Presence, PresenceOptions, ReplyMap, RestoreError, SchemaMap, SchemaOptions, Serializer, SharedReducer, SharedReducerOptions, SharedStore, SharedStoreOptions, StandardSchemaV1, StorageLike, StorageTransport, Transport, TransportKind, Version, WIRE_VERSION, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
3
3
 
4
4
  /**
5
5
  * How far a shared value travels:
@@ -29,6 +29,92 @@ interface UseSharedStateOptions {
29
29
  */
30
30
  declare function useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
31
31
 
32
+ interface UseSharedReducerOptions {
33
+ /** Bus name. Default 'use-everywhere'. */
34
+ name?: string;
35
+ /** Which reducer this is, when several share a bus. Default 'default'. */
36
+ key?: string;
37
+ }
38
+ /**
39
+ * Like `useReducer`, but every tab, window, and worker on this origin applies
40
+ * the same actions in the same order.
41
+ *
42
+ * ```tsx
43
+ * const [count, dispatch] = useSharedReducer((n, action) => n + action.by, 0);
44
+ * <button onClick={() => dispatch({ by: 1 })}>{count}</button>;
45
+ * ```
46
+ *
47
+ * Reach for this instead of `useSharedState` whenever a write is *relative to
48
+ * what is already there* — a counter, a total, a list you append to. Shared
49
+ * state converges last-writer-wins on the value, so two tabs incrementing at
50
+ * once both write the same result and one increment vanishes. A reducer sends
51
+ * the action rather than the result, and two increments are two actions.
52
+ *
53
+ * For a plain register — a theme, a selection, a draft — `useSharedState` is
54
+ * still the right tool and the cheaper one.
55
+ *
56
+ * ## The rules
57
+ *
58
+ * **Actions must survive the wire.** They are structured-cloned to every peer,
59
+ * so no functions, no class instances, no DOM nodes.
60
+ *
61
+ * **The reducer must be pure**, and it must be the *same* reducer everywhere.
62
+ * Every client folds the same actions itself; a client whose fold differs gets
63
+ * a different answer, and nothing can detect that. The first caller's function
64
+ * is the one used for the life of the page — a re-render passes a new identity
65
+ * every time, and swapping the fold under a history already applied is exactly
66
+ * the divergence this exists to prevent.
67
+ *
68
+ * **Ordering needs a leader**, which this shares with `useLeader` on the same
69
+ * bus rather than electing a second one. Before a seat is filled — the first
70
+ * moments of the first tab — dispatches are held locally and ordered once it
71
+ * is. They are never lost, only pending.
72
+ */
73
+ declare function useSharedReducer<S, A>(reducer: (state: S, action: A) => S, initial: S, options?: UseSharedReducerOptions): [S, (action: A) => void];
74
+
75
+ interface UseSharedSelectorOptions extends UseSharedStateOptions {
76
+ /**
77
+ * Whether two selected values count as the same. Default `Object.is`.
78
+ *
79
+ * A selector returning an object or array builds a new one every time it
80
+ * runs, so `Object.is` never matches and the component re-renders on every
81
+ * write to the store. Pass a shallow comparison for those.
82
+ */
83
+ equal?: (a: unknown, b: unknown) => boolean;
84
+ }
85
+ /**
86
+ * Read a derived value from a shared store, re-rendering only when it changes.
87
+ *
88
+ * `useSharedState` subscribes to one key, which is the right shape for reading
89
+ * one thing. Reading *across* keys meant either one hook per key or a
90
+ * subscription to the whole store, and the latter re-renders on every write to
91
+ * anything in it.
92
+ *
93
+ * ```tsx
94
+ * const total = useSharedStore<Cart, number>(
95
+ * (cart) => cart.items.length + cart.saved.length,
96
+ * );
97
+ * ```
98
+ *
99
+ * The selector runs on every store change; the component only re-renders when
100
+ * its result changes by `equal`.
101
+ *
102
+ * ## It reads, it does not declare
103
+ *
104
+ * `useSharedState(key, initial)` registers `key` with a default. A selector
105
+ * does not — it sees whatever is in the store, so a key nothing has registered
106
+ * or written yet is `undefined`. Write selectors that tolerate that, or declare
107
+ * the defaults with `useSharedState` (or a store initial) somewhere that
108
+ * mounts first.
109
+ *
110
+ * The same applies on a server, where the store is inert and empty: the
111
+ * selector runs against `{}`, and its result is what the server-rendered markup
112
+ * shows.
113
+ */
114
+ declare function useSharedStore<S extends Record<string, unknown>, T>(selector: (state: S) => T, options?: UseSharedSelectorOptions): T;
115
+ /** Shallow equality over an object or array, for selectors that build one. */
116
+ declare function shallowEqual(a: unknown, b: unknown): boolean;
117
+
32
118
  /** Registry stores hold arbitrary keys — shape is decided by the hooks using them. */
33
119
  type AnyStore = SharedStore<Record<string, unknown>>;
34
120
 
@@ -43,14 +129,41 @@ declare function getSharedStore(name?: string, scope?: ShareScope): AnyStore;
43
129
  declare function getLeader(name: string, options?: LeaderOptions): Leader;
44
130
 
45
131
  /** Get the page-wide typed channel for `name` (one instance per name). */
46
- declare function useChannel<M extends MessageMap>(name: string): Channel<M>;
132
+ declare function useChannel<M extends MessageMap, R extends ReplyMap<M> = Record<never, never>>(name: string): Channel<M, R>;
133
+ interface UseMessageOptions {
134
+ /**
135
+ * Subscribe at all. Default true.
136
+ *
137
+ * `false` unsubscribes rather than filtering inside the handler, so a
138
+ * component that is not interested costs nothing — and the alternative,
139
+ * calling the hook conditionally, is not allowed.
140
+ */
141
+ enabled?: boolean;
142
+ /** Unsubscribe after the first message. */
143
+ once?: boolean;
144
+ }
47
145
  /**
48
146
  * Subscribe to one message type. The handler is kept fresh without
49
147
  * resubscribing, so it may close over render state.
50
148
  */
51
- declare function useMessage<M extends MessageMap, K extends keyof M & string>(channel: Channel<M>, type: K, handler: (payload: M[K], meta: MessageMeta) => void): void;
149
+ declare function useMessage<M extends MessageMap, R extends ReplyMap<M>, K extends keyof M & string>(channel: Channel<M, R>, type: K, handler: (payload: M[K], meta: MessageMeta) => void, options?: UseMessageOptions): void;
150
+ /**
151
+ * Answer `ask`s of one type for as long as this component is mounted.
152
+ *
153
+ * A hook rather than a bare `channel.answer` call because a responder is a
154
+ * subscription: registering one in a render would leave the last unmounted
155
+ * component answering for the page.
156
+ *
157
+ * One responder per type per channel — registering a second replaces the first,
158
+ * which is the same first-wins-then-replace rule `answer` has in core.
159
+ */
160
+ declare function useAnswer<M extends MessageMap, R extends ReplyMap<M>, K extends keyof M & keyof R & string>(channel: Channel<M, R>, type: K, responder: (payload: M[K], meta: MessageMeta) => R[K], options?: {
161
+ enabled?: boolean;
162
+ }): void;
52
163
  /** The channel's post function (stable identity per channel). */
53
- declare function useSend<M extends MessageMap>(channel: Channel<M>): Channel<M>['post'];
164
+ declare function useSend<M extends MessageMap, R extends ReplyMap<M>>(channel: Channel<M, R>): Channel<M, R>['post'];
165
+ /** The channel's ask function (stable identity per channel). */
166
+ declare function useAsk<M extends MessageMap, R extends ReplyMap<M>>(channel: Channel<M, R>): Channel<M, R>['ask'];
54
167
 
55
168
  /** A channel bound to a name and message map: typed hooks with no per-call generics. */
56
169
  interface ChannelHooks<M extends MessageMap> {
@@ -70,8 +183,11 @@ interface ChannelHooks<M extends MessageMap> {
70
183
  * typed hooks back. Sugar over useChannel/useMessage/useSend — the same
71
184
  * page-wide channel singleton is shared, so mixing bound and standalone
72
185
  * hooks for one name is safe.
186
+ *
187
+ * Options are registered here and applied when the channel is first needed, so
188
+ * declaring a `schema` at module scope still constructs nothing on import.
73
189
  */
74
- declare function defineChannel<M extends MessageMap>(name: string): ChannelHooks<M>;
190
+ declare function defineChannel<M extends MessageMap>(name: string, options?: ChannelOptions<M>): ChannelHooks<M>;
75
191
 
76
192
  interface DefineStoreOptions {
77
193
  /** Restore this store from disk on first use, and write it back as it changes. */
@@ -80,6 +196,16 @@ interface DefineStoreOptions {
80
196
  persistKeys?: string[];
81
197
  /** Coalesce disk writes for this long. Default 100. */
82
198
  persistDebounceMs?: number;
199
+ /**
200
+ * The version of your persisted state's shape. Bump it whenever a key changes
201
+ * meaning or type, and supply `migrate` to carry old data forward. Default 0,
202
+ * which is also what anything written before this existed reads as.
203
+ */
204
+ persistVersion?: number;
205
+ /** Bring persisted state written at an older `persistVersion` up to the current one. */
206
+ migrate?: (state: Record<string, unknown>, from: number) => Record<string, unknown>;
207
+ /** Called when persisted state is refused instead of restored. Defaults to a development warning. */
208
+ onRestoreError?: (error: RestoreError) => void;
83
209
  /** How far this store is shared. Default 'everywhere'. */
84
210
  scope?: ShareScope;
85
211
  }
@@ -106,10 +232,25 @@ interface StoreHooks<S extends Record<string, unknown>> {
106
232
  */
107
233
  declare function defineStore<S extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: DefineStoreOptions): StoreHooks<S>;
108
234
 
109
- /** The other tabs/windows/workers currently alive on this origin. */
110
- declare function usePeers(options?: {
235
+ interface UsePeersOptions {
236
+ /** Bus name. Default 'use-everywhere'. */
111
237
  name?: string;
112
- }): readonly Peer[];
238
+ /**
239
+ * Include this client in the list. Default false.
240
+ *
241
+ * The default answers "who *else* is here", which is what a presence strip
242
+ * asks. Turn it on for an avatar list, where leaving yourself out means every
243
+ * tab renders a different list of the same room.
244
+ */
245
+ includeSelf?: boolean;
246
+ }
247
+ /**
248
+ * The tabs/windows/workers currently alive on this origin.
249
+ *
250
+ * Each peer carries whatever it published about itself as `metadata` — see
251
+ * {@link usePresenceMetadata} for publishing this client's.
252
+ */
253
+ declare function usePeers(options?: UsePeersOptions): readonly Peer[];
113
254
  /**
114
255
  * This client's own id on the presence bus (matches patch origin ids).
115
256
  *
@@ -123,6 +264,51 @@ declare function usePeers(options?: {
123
264
  declare function useClientId(options?: {
124
265
  name?: string;
125
266
  }): string;
267
+ /**
268
+ * Publish what this client wants peers to know about it — a display name, a tab
269
+ * title, a cursor.
270
+ *
271
+ * Safe to call with a fresh object every render: the value is compared by
272
+ * contents, so an unchanged one announces nothing and re-renders nobody.
273
+ *
274
+ * ```tsx
275
+ * usePresenceMetadata({ name: user.name, editing: currentDocId });
276
+ * ```
277
+ *
278
+ * Published in an effect rather than during render, because announcing is a
279
+ * side effect on every other tab — and a render that React throws away must not
280
+ * be one other tabs already saw.
281
+ */
282
+ declare function usePresenceMetadata(metadata: unknown, options?: UsePeersOptions): void;
283
+
284
+ /**
285
+ * Whether a persisted store has finished restoring.
286
+ *
287
+ * `false` on the first render, then `true` once the restore lands — including
288
+ * the cases where there is nothing to restore, which settle immediately.
289
+ *
290
+ * Deliberately `false` first even for a synchronous adapter that has already
291
+ * finished. The alternative is a value that differs between the server render
292
+ * and the browser's hydrating render, which is a hydration mismatch on every
293
+ * app that uses it — the same reason `useClientId` reports `''` until the
294
+ * commit after hydration.
295
+ *
296
+ * The gap this closes only exists for **async** adapters, and it is one
297
+ * last-writer-wins makes invisible: a keystroke landing before the restore
298
+ * writes at counter 1, the restore arrives holding counter 5, and the newer
299
+ * keystroke is correctly discarded. The behaviour is right; the surprise is
300
+ * total. Gate the input and there is no gap:
301
+ *
302
+ * ```tsx
303
+ * const ready = useHydrated({ store: 'settings' });
304
+ * return <input disabled={!ready} value={draft} onChange={…} />;
305
+ * ```
306
+ *
307
+ * A synchronous adapter — `localStorageAdapter` and friends — has already
308
+ * restored by the time the store is handed back, so there is no gap to guard
309
+ * and this simply flips to `true` in the commit after mount.
310
+ */
311
+ declare function useHydrated(options?: UseSharedStateOptions): boolean;
126
312
 
127
313
  interface UseLeaderOptions extends LeaderOptions {
128
314
  /** Which bus to elect on. Defaults to the shared default name. */
@@ -143,6 +329,52 @@ declare function useIsLeader(options?: UseLeaderOptions): boolean;
143
329
  */
144
330
  declare function useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
145
331
 
332
+ /**
333
+ * The React half of a namespace: the same hooks, with every bus name prefixed.
334
+ *
335
+ * Options keep their meaning — `store` and `name` are still relative names, and
336
+ * `scope` still says how far a value travels, which is a different axis from
337
+ * which namespace it lives in.
338
+ */
339
+ interface ReactNamespace extends Namespace {
340
+ defineStore<S extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: DefineStoreOptions): StoreHooks<S>;
341
+ defineChannel<M extends MessageMap>(name?: string, options?: ChannelOptions<M>): ChannelHooks<M>;
342
+ getSharedStore(name?: string, scope?: UseSharedStateOptions['scope']): AnyStore;
343
+ useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
344
+ useSharedStore<S extends Record<string, unknown>, T>(selector: (state: S) => T, options?: UseSharedSelectorOptions): T;
345
+ usePeers(options?: UsePeersOptions): readonly Peer[];
346
+ usePresenceMetadata(metadata: unknown, options?: UsePeersOptions): void;
347
+ useHydrated(options?: UseSharedStateOptions): boolean;
348
+ useClientId(options?: {
349
+ name?: string;
350
+ }): string;
351
+ useLeader(options?: UseLeaderOptions): LeaderSnapshot;
352
+ useIsLeader(options?: UseLeaderOptions): boolean;
353
+ useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
354
+ }
355
+ /**
356
+ * Namespaced hooks and factories, so two independently deployed apps on one
357
+ * origin cannot collide by both taking the defaults.
358
+ *
359
+ * ```ts
360
+ * // checkout/bus.ts
361
+ * export const checkout = createNamespace('checkout');
362
+ *
363
+ * // anywhere in the checkout app
364
+ * const [items, setItems] = checkout.useSharedState('items', []);
365
+ * ```
366
+ *
367
+ * Call it **at module scope**, like `defineStore` and `defineChannel`. It
368
+ * builds a small object of bound functions, and rebuilding that on every render
369
+ * would hand React a new `useSharedState` identity each time — harmless for the
370
+ * hooks themselves, which key off the bus name, but pointless work and a
371
+ * confusing thing to see in a profile.
372
+ *
373
+ * See the core `createNamespace` for what a namespace does and does not
374
+ * guarantee. In short: it prevents collision, not access.
375
+ */
376
+ declare function createNamespace(namespace: string): ReactNamespace;
377
+
146
378
  type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
147
379
  /**
148
380
  * The flow's state as a discriminated union, so `status` narrows `result` and
@@ -199,4 +431,4 @@ type UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> = OpenedW
199
431
  */
200
432
  declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
201
433
 
202
- export { type AnyStore, type ChannelHooks, type DefineStoreOptions, type OpenedWindowControls, type OpenedWindowState, type OpenedWindowStatus, type ShareScope, type StoreHooks, type UseLeaderOptions, type UseOpenedWindow, type UseSharedStateOptions, defineChannel, defineStore, getLeader, getSharedStore, useChannel, useClientId, useIsLeader, useLeader, useLeaderEffect, useMessage, useOpenedWindow, usePeers, useSend, useSharedState };
434
+ export { type AnyStore, type ChannelHooks, type DefineStoreOptions, type OpenedWindowControls, type OpenedWindowState, type OpenedWindowStatus, type ReactNamespace, type ShareScope, type StoreHooks, type UseLeaderOptions, type UseMessageOptions, type UseOpenedWindow, type UsePeersOptions, type UseSharedReducerOptions, type UseSharedSelectorOptions, type UseSharedStateOptions, createNamespace, defineChannel, defineStore, getLeader, getSharedStore, shallowEqual, useAnswer, useAsk, useChannel, useClientId, useHydrated, useIsLeader, useLeader, useLeaderEffect, useMessage, useOpenedWindow, usePeers, usePresenceMetadata, useSend, useSharedReducer, useSharedState, useSharedStore };
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
- import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
- export { BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, NoopTransport, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, Presence, PresenceOptions, SharedStore, SharedStoreOptions, StorageLike, StorageTransport, Transport, TransportKind, Version, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, isBroadcastChannelAvailable, isStorageEventAvailable, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
1
+ import { SharedStore, LeaderOptions, Leader, MessageMap, ReplyMap, Channel, MessageMeta, ChannelOptions, PersistAdapter, RestoreError, Peer, LeaderSnapshot, Namespace, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
+ export { AskOptions, BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, ChannelOptions, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, IndexedDbAdapterOptions, InvalidPayload, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, Namespace, NoopTransport, OnInvalid, OnOptions, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, PostOptions, Presence, PresenceOptions, ReplyMap, RestoreError, SchemaMap, SchemaOptions, Serializer, SharedReducer, SharedReducerOptions, SharedStore, SharedStoreOptions, StandardSchemaV1, StorageLike, StorageTransport, Transport, TransportKind, Version, WIRE_VERSION, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
3
3
 
4
4
  /**
5
5
  * How far a shared value travels:
@@ -29,6 +29,92 @@ interface UseSharedStateOptions {
29
29
  */
30
30
  declare function useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
31
31
 
32
+ interface UseSharedReducerOptions {
33
+ /** Bus name. Default 'use-everywhere'. */
34
+ name?: string;
35
+ /** Which reducer this is, when several share a bus. Default 'default'. */
36
+ key?: string;
37
+ }
38
+ /**
39
+ * Like `useReducer`, but every tab, window, and worker on this origin applies
40
+ * the same actions in the same order.
41
+ *
42
+ * ```tsx
43
+ * const [count, dispatch] = useSharedReducer((n, action) => n + action.by, 0);
44
+ * <button onClick={() => dispatch({ by: 1 })}>{count}</button>;
45
+ * ```
46
+ *
47
+ * Reach for this instead of `useSharedState` whenever a write is *relative to
48
+ * what is already there* — a counter, a total, a list you append to. Shared
49
+ * state converges last-writer-wins on the value, so two tabs incrementing at
50
+ * once both write the same result and one increment vanishes. A reducer sends
51
+ * the action rather than the result, and two increments are two actions.
52
+ *
53
+ * For a plain register — a theme, a selection, a draft — `useSharedState` is
54
+ * still the right tool and the cheaper one.
55
+ *
56
+ * ## The rules
57
+ *
58
+ * **Actions must survive the wire.** They are structured-cloned to every peer,
59
+ * so no functions, no class instances, no DOM nodes.
60
+ *
61
+ * **The reducer must be pure**, and it must be the *same* reducer everywhere.
62
+ * Every client folds the same actions itself; a client whose fold differs gets
63
+ * a different answer, and nothing can detect that. The first caller's function
64
+ * is the one used for the life of the page — a re-render passes a new identity
65
+ * every time, and swapping the fold under a history already applied is exactly
66
+ * the divergence this exists to prevent.
67
+ *
68
+ * **Ordering needs a leader**, which this shares with `useLeader` on the same
69
+ * bus rather than electing a second one. Before a seat is filled — the first
70
+ * moments of the first tab — dispatches are held locally and ordered once it
71
+ * is. They are never lost, only pending.
72
+ */
73
+ declare function useSharedReducer<S, A>(reducer: (state: S, action: A) => S, initial: S, options?: UseSharedReducerOptions): [S, (action: A) => void];
74
+
75
+ interface UseSharedSelectorOptions extends UseSharedStateOptions {
76
+ /**
77
+ * Whether two selected values count as the same. Default `Object.is`.
78
+ *
79
+ * A selector returning an object or array builds a new one every time it
80
+ * runs, so `Object.is` never matches and the component re-renders on every
81
+ * write to the store. Pass a shallow comparison for those.
82
+ */
83
+ equal?: (a: unknown, b: unknown) => boolean;
84
+ }
85
+ /**
86
+ * Read a derived value from a shared store, re-rendering only when it changes.
87
+ *
88
+ * `useSharedState` subscribes to one key, which is the right shape for reading
89
+ * one thing. Reading *across* keys meant either one hook per key or a
90
+ * subscription to the whole store, and the latter re-renders on every write to
91
+ * anything in it.
92
+ *
93
+ * ```tsx
94
+ * const total = useSharedStore<Cart, number>(
95
+ * (cart) => cart.items.length + cart.saved.length,
96
+ * );
97
+ * ```
98
+ *
99
+ * The selector runs on every store change; the component only re-renders when
100
+ * its result changes by `equal`.
101
+ *
102
+ * ## It reads, it does not declare
103
+ *
104
+ * `useSharedState(key, initial)` registers `key` with a default. A selector
105
+ * does not — it sees whatever is in the store, so a key nothing has registered
106
+ * or written yet is `undefined`. Write selectors that tolerate that, or declare
107
+ * the defaults with `useSharedState` (or a store initial) somewhere that
108
+ * mounts first.
109
+ *
110
+ * The same applies on a server, where the store is inert and empty: the
111
+ * selector runs against `{}`, and its result is what the server-rendered markup
112
+ * shows.
113
+ */
114
+ declare function useSharedStore<S extends Record<string, unknown>, T>(selector: (state: S) => T, options?: UseSharedSelectorOptions): T;
115
+ /** Shallow equality over an object or array, for selectors that build one. */
116
+ declare function shallowEqual(a: unknown, b: unknown): boolean;
117
+
32
118
  /** Registry stores hold arbitrary keys — shape is decided by the hooks using them. */
33
119
  type AnyStore = SharedStore<Record<string, unknown>>;
34
120
 
@@ -43,14 +129,41 @@ declare function getSharedStore(name?: string, scope?: ShareScope): AnyStore;
43
129
  declare function getLeader(name: string, options?: LeaderOptions): Leader;
44
130
 
45
131
  /** Get the page-wide typed channel for `name` (one instance per name). */
46
- declare function useChannel<M extends MessageMap>(name: string): Channel<M>;
132
+ declare function useChannel<M extends MessageMap, R extends ReplyMap<M> = Record<never, never>>(name: string): Channel<M, R>;
133
+ interface UseMessageOptions {
134
+ /**
135
+ * Subscribe at all. Default true.
136
+ *
137
+ * `false` unsubscribes rather than filtering inside the handler, so a
138
+ * component that is not interested costs nothing — and the alternative,
139
+ * calling the hook conditionally, is not allowed.
140
+ */
141
+ enabled?: boolean;
142
+ /** Unsubscribe after the first message. */
143
+ once?: boolean;
144
+ }
47
145
  /**
48
146
  * Subscribe to one message type. The handler is kept fresh without
49
147
  * resubscribing, so it may close over render state.
50
148
  */
51
- declare function useMessage<M extends MessageMap, K extends keyof M & string>(channel: Channel<M>, type: K, handler: (payload: M[K], meta: MessageMeta) => void): void;
149
+ declare function useMessage<M extends MessageMap, R extends ReplyMap<M>, K extends keyof M & string>(channel: Channel<M, R>, type: K, handler: (payload: M[K], meta: MessageMeta) => void, options?: UseMessageOptions): void;
150
+ /**
151
+ * Answer `ask`s of one type for as long as this component is mounted.
152
+ *
153
+ * A hook rather than a bare `channel.answer` call because a responder is a
154
+ * subscription: registering one in a render would leave the last unmounted
155
+ * component answering for the page.
156
+ *
157
+ * One responder per type per channel — registering a second replaces the first,
158
+ * which is the same first-wins-then-replace rule `answer` has in core.
159
+ */
160
+ declare function useAnswer<M extends MessageMap, R extends ReplyMap<M>, K extends keyof M & keyof R & string>(channel: Channel<M, R>, type: K, responder: (payload: M[K], meta: MessageMeta) => R[K], options?: {
161
+ enabled?: boolean;
162
+ }): void;
52
163
  /** The channel's post function (stable identity per channel). */
53
- declare function useSend<M extends MessageMap>(channel: Channel<M>): Channel<M>['post'];
164
+ declare function useSend<M extends MessageMap, R extends ReplyMap<M>>(channel: Channel<M, R>): Channel<M, R>['post'];
165
+ /** The channel's ask function (stable identity per channel). */
166
+ declare function useAsk<M extends MessageMap, R extends ReplyMap<M>>(channel: Channel<M, R>): Channel<M, R>['ask'];
54
167
 
55
168
  /** A channel bound to a name and message map: typed hooks with no per-call generics. */
56
169
  interface ChannelHooks<M extends MessageMap> {
@@ -70,8 +183,11 @@ interface ChannelHooks<M extends MessageMap> {
70
183
  * typed hooks back. Sugar over useChannel/useMessage/useSend — the same
71
184
  * page-wide channel singleton is shared, so mixing bound and standalone
72
185
  * hooks for one name is safe.
186
+ *
187
+ * Options are registered here and applied when the channel is first needed, so
188
+ * declaring a `schema` at module scope still constructs nothing on import.
73
189
  */
74
- declare function defineChannel<M extends MessageMap>(name: string): ChannelHooks<M>;
190
+ declare function defineChannel<M extends MessageMap>(name: string, options?: ChannelOptions<M>): ChannelHooks<M>;
75
191
 
76
192
  interface DefineStoreOptions {
77
193
  /** Restore this store from disk on first use, and write it back as it changes. */
@@ -80,6 +196,16 @@ interface DefineStoreOptions {
80
196
  persistKeys?: string[];
81
197
  /** Coalesce disk writes for this long. Default 100. */
82
198
  persistDebounceMs?: number;
199
+ /**
200
+ * The version of your persisted state's shape. Bump it whenever a key changes
201
+ * meaning or type, and supply `migrate` to carry old data forward. Default 0,
202
+ * which is also what anything written before this existed reads as.
203
+ */
204
+ persistVersion?: number;
205
+ /** Bring persisted state written at an older `persistVersion` up to the current one. */
206
+ migrate?: (state: Record<string, unknown>, from: number) => Record<string, unknown>;
207
+ /** Called when persisted state is refused instead of restored. Defaults to a development warning. */
208
+ onRestoreError?: (error: RestoreError) => void;
83
209
  /** How far this store is shared. Default 'everywhere'. */
84
210
  scope?: ShareScope;
85
211
  }
@@ -106,10 +232,25 @@ interface StoreHooks<S extends Record<string, unknown>> {
106
232
  */
107
233
  declare function defineStore<S extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: DefineStoreOptions): StoreHooks<S>;
108
234
 
109
- /** The other tabs/windows/workers currently alive on this origin. */
110
- declare function usePeers(options?: {
235
+ interface UsePeersOptions {
236
+ /** Bus name. Default 'use-everywhere'. */
111
237
  name?: string;
112
- }): readonly Peer[];
238
+ /**
239
+ * Include this client in the list. Default false.
240
+ *
241
+ * The default answers "who *else* is here", which is what a presence strip
242
+ * asks. Turn it on for an avatar list, where leaving yourself out means every
243
+ * tab renders a different list of the same room.
244
+ */
245
+ includeSelf?: boolean;
246
+ }
247
+ /**
248
+ * The tabs/windows/workers currently alive on this origin.
249
+ *
250
+ * Each peer carries whatever it published about itself as `metadata` — see
251
+ * {@link usePresenceMetadata} for publishing this client's.
252
+ */
253
+ declare function usePeers(options?: UsePeersOptions): readonly Peer[];
113
254
  /**
114
255
  * This client's own id on the presence bus (matches patch origin ids).
115
256
  *
@@ -123,6 +264,51 @@ declare function usePeers(options?: {
123
264
  declare function useClientId(options?: {
124
265
  name?: string;
125
266
  }): string;
267
+ /**
268
+ * Publish what this client wants peers to know about it — a display name, a tab
269
+ * title, a cursor.
270
+ *
271
+ * Safe to call with a fresh object every render: the value is compared by
272
+ * contents, so an unchanged one announces nothing and re-renders nobody.
273
+ *
274
+ * ```tsx
275
+ * usePresenceMetadata({ name: user.name, editing: currentDocId });
276
+ * ```
277
+ *
278
+ * Published in an effect rather than during render, because announcing is a
279
+ * side effect on every other tab — and a render that React throws away must not
280
+ * be one other tabs already saw.
281
+ */
282
+ declare function usePresenceMetadata(metadata: unknown, options?: UsePeersOptions): void;
283
+
284
+ /**
285
+ * Whether a persisted store has finished restoring.
286
+ *
287
+ * `false` on the first render, then `true` once the restore lands — including
288
+ * the cases where there is nothing to restore, which settle immediately.
289
+ *
290
+ * Deliberately `false` first even for a synchronous adapter that has already
291
+ * finished. The alternative is a value that differs between the server render
292
+ * and the browser's hydrating render, which is a hydration mismatch on every
293
+ * app that uses it — the same reason `useClientId` reports `''` until the
294
+ * commit after hydration.
295
+ *
296
+ * The gap this closes only exists for **async** adapters, and it is one
297
+ * last-writer-wins makes invisible: a keystroke landing before the restore
298
+ * writes at counter 1, the restore arrives holding counter 5, and the newer
299
+ * keystroke is correctly discarded. The behaviour is right; the surprise is
300
+ * total. Gate the input and there is no gap:
301
+ *
302
+ * ```tsx
303
+ * const ready = useHydrated({ store: 'settings' });
304
+ * return <input disabled={!ready} value={draft} onChange={…} />;
305
+ * ```
306
+ *
307
+ * A synchronous adapter — `localStorageAdapter` and friends — has already
308
+ * restored by the time the store is handed back, so there is no gap to guard
309
+ * and this simply flips to `true` in the commit after mount.
310
+ */
311
+ declare function useHydrated(options?: UseSharedStateOptions): boolean;
126
312
 
127
313
  interface UseLeaderOptions extends LeaderOptions {
128
314
  /** Which bus to elect on. Defaults to the shared default name. */
@@ -143,6 +329,52 @@ declare function useIsLeader(options?: UseLeaderOptions): boolean;
143
329
  */
144
330
  declare function useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
145
331
 
332
+ /**
333
+ * The React half of a namespace: the same hooks, with every bus name prefixed.
334
+ *
335
+ * Options keep their meaning — `store` and `name` are still relative names, and
336
+ * `scope` still says how far a value travels, which is a different axis from
337
+ * which namespace it lives in.
338
+ */
339
+ interface ReactNamespace extends Namespace {
340
+ defineStore<S extends Record<string, unknown> = Record<string, unknown>>(name: string, options?: DefineStoreOptions): StoreHooks<S>;
341
+ defineChannel<M extends MessageMap>(name?: string, options?: ChannelOptions<M>): ChannelHooks<M>;
342
+ getSharedStore(name?: string, scope?: UseSharedStateOptions['scope']): AnyStore;
343
+ useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
344
+ useSharedStore<S extends Record<string, unknown>, T>(selector: (state: S) => T, options?: UseSharedSelectorOptions): T;
345
+ usePeers(options?: UsePeersOptions): readonly Peer[];
346
+ usePresenceMetadata(metadata: unknown, options?: UsePeersOptions): void;
347
+ useHydrated(options?: UseSharedStateOptions): boolean;
348
+ useClientId(options?: {
349
+ name?: string;
350
+ }): string;
351
+ useLeader(options?: UseLeaderOptions): LeaderSnapshot;
352
+ useIsLeader(options?: UseLeaderOptions): boolean;
353
+ useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
354
+ }
355
+ /**
356
+ * Namespaced hooks and factories, so two independently deployed apps on one
357
+ * origin cannot collide by both taking the defaults.
358
+ *
359
+ * ```ts
360
+ * // checkout/bus.ts
361
+ * export const checkout = createNamespace('checkout');
362
+ *
363
+ * // anywhere in the checkout app
364
+ * const [items, setItems] = checkout.useSharedState('items', []);
365
+ * ```
366
+ *
367
+ * Call it **at module scope**, like `defineStore` and `defineChannel`. It
368
+ * builds a small object of bound functions, and rebuilding that on every render
369
+ * would hand React a new `useSharedState` identity each time — harmless for the
370
+ * hooks themselves, which key off the bus name, but pointless work and a
371
+ * confusing thing to see in a profile.
372
+ *
373
+ * See the core `createNamespace` for what a namespace does and does not
374
+ * guarantee. In short: it prevents collision, not access.
375
+ */
376
+ declare function createNamespace(namespace: string): ReactNamespace;
377
+
146
378
  type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
147
379
  /**
148
380
  * The flow's state as a discriminated union, so `status` narrows `result` and
@@ -199,4 +431,4 @@ type UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> = OpenedW
199
431
  */
200
432
  declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
201
433
 
202
- export { type AnyStore, type ChannelHooks, type DefineStoreOptions, type OpenedWindowControls, type OpenedWindowState, type OpenedWindowStatus, type ShareScope, type StoreHooks, type UseLeaderOptions, type UseOpenedWindow, type UseSharedStateOptions, defineChannel, defineStore, getLeader, getSharedStore, useChannel, useClientId, useIsLeader, useLeader, useLeaderEffect, useMessage, useOpenedWindow, usePeers, useSend, useSharedState };
434
+ export { type AnyStore, type ChannelHooks, type DefineStoreOptions, type OpenedWindowControls, type OpenedWindowState, type OpenedWindowStatus, type ReactNamespace, type ShareScope, type StoreHooks, type UseLeaderOptions, type UseMessageOptions, type UseOpenedWindow, type UsePeersOptions, type UseSharedReducerOptions, type UseSharedSelectorOptions, type UseSharedStateOptions, createNamespace, defineChannel, defineStore, getLeader, getSharedStore, shallowEqual, useAnswer, useAsk, useChannel, useClientId, useHydrated, useIsLeader, useLeader, useLeaderEffect, useMessage, useOpenedWindow, usePeers, usePresenceMetadata, useSend, useSharedReducer, useSharedState, useSharedStore };