use-everywhere 0.13.0 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,418 +1,485 @@
1
1
  'use client';
2
- import {
3
- DEFAULT_NAME,
4
- configureChannel,
5
- configureStore,
6
- getChannel,
7
- getLeader,
8
- getReducer,
9
- getSharedStore,
10
- getStore,
11
- useClientId,
12
- usePeers,
13
- usePresenceMetadata,
14
- warnOnInitialMismatch,
15
- warnRenamed
16
- } from "./chunk-HZ5EI66W.js";
17
-
18
- // src/use-shared-state.ts
19
- import { useCallback, useSyncExternalStore } from "react";
2
+ import { a as configureChannel, c as getLeader, d as getStore, f as warnOnInitialMismatch, i as DEFAULT_NAME, l as getReducer, n as usePeers, o as configureStore, r as usePresenceMetadata, s as getChannel, t as useClientId, u as getSharedStore } from "./use-peers-DtpHGzXE.js";
3
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
4
+ import { BroadcastChannelTransport, CID_PARAM, HandshakeTimeoutError, NoopTransport, SharedWorkerTransport, StorageTransport, WIRE_VERSION, WindowClosedError, WindowClosedError as WindowClosedError$1, connectToOpener, createChannel, createLeader, createNamespace as createNamespace$1, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isSharedWorkerAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from "@use-everywhere/core";
5
+ //#region src/use-shared-state.ts
6
+ /**
7
+ * Like useState, but the value exists in every tab, window, and worker on
8
+ * this origin. Late-joining tabs hydrate to the current value; concurrent
9
+ * writes converge last-writer-wins. Pass options.scope to delimit how much
10
+ * is shared ('everywhere' | 'tabs' | 'tab').
11
+ *
12
+ * Note the convergence rule is last-writer-wins per key, not per operation:
13
+ * two tabs running `set(n => n + 1)` at the same moment agree on one value,
14
+ * and one of the increments is lost. For counters and other accumulating
15
+ * writes, drive them from a single tab (useLeaderEffect) until an op-based
16
+ * primitive lands.
17
+ */
20
18
  function useSharedState(key, initial, options) {
21
- const storeName = options?.store ?? DEFAULT_NAME;
22
- const store = getStore(storeName, options?.scope ?? "everywhere");
23
- store.registerKey(key, initial);
24
- if (process.env.NODE_ENV !== "production") {
25
- warnOnInitialMismatch(storeName, key, initial);
26
- }
27
- const value = useSyncExternalStore(
28
- useCallback((onChange) => store.subscribeKey(key, onChange), [store, key]),
29
- () => store.getSnapshot()[key],
30
- () => initial
31
- );
32
- const setValue = useCallback(
33
- (next) => store.set(key, next),
34
- [store, key]
35
- );
36
- return [value, setValue];
19
+ const storeName = options?.store ?? DEFAULT_NAME;
20
+ const store = getStore(storeName, options?.scope ?? "everywhere");
21
+ store.registerKey(key, initial);
22
+ if (process.env.NODE_ENV !== "production") warnOnInitialMismatch(storeName, key, initial);
23
+ return [useSyncExternalStore(useCallback((onChange) => store.subscribeKey(key, onChange), [store, key]), () => store.getSnapshot()[key], () => initial), useCallback((next) => store.set(key, next), [store, key])];
37
24
  }
38
-
39
- // src/use-shared-reducer.ts
40
- import { useCallback as useCallback2, useSyncExternalStore as useSyncExternalStore2 } from "react";
25
+ //#endregion
26
+ //#region src/use-shared-reducer.ts
27
+ /**
28
+ * Like `useReducer`, but every tab, window, and worker on this origin applies
29
+ * the same actions in the same order.
30
+ *
31
+ * ```tsx
32
+ * const [count, dispatch] = useSharedReducer((n, action) => n + action.by, 0);
33
+ * <button onClick={() => dispatch({ by: 1 })}>{count}</button>;
34
+ * ```
35
+ *
36
+ * Reach for this instead of `useSharedState` whenever a write is *relative to
37
+ * what is already there* — a counter, a total, a list you append to. Shared
38
+ * state converges last-writer-wins on the value, so two tabs incrementing at
39
+ * once both write the same result and one increment vanishes. A reducer sends
40
+ * the action rather than the result, and two increments are two actions.
41
+ *
42
+ * For a plain register — a theme, a selection, a draft — `useSharedState` is
43
+ * still the right tool and the cheaper one.
44
+ *
45
+ * ## The rules
46
+ *
47
+ * **Actions must survive the wire.** They are structured-cloned to every peer,
48
+ * so no functions, no class instances, no DOM nodes.
49
+ *
50
+ * **The reducer must be pure**, and it must be the *same* reducer everywhere.
51
+ * Every client folds the same actions itself; a client whose fold differs gets
52
+ * a different answer, and nothing can detect that. The first caller's function
53
+ * is the one used for the life of the page — a re-render passes a new identity
54
+ * every time, and swapping the fold under a history already applied is exactly
55
+ * the divergence this exists to prevent.
56
+ *
57
+ * **Ordering needs a leader**, which this shares with `useLeader` on the same
58
+ * bus rather than electing a second one. Before a seat is filled — the first
59
+ * moments of the first tab — dispatches are held locally and ordered once it
60
+ * is. They are never lost, only pending.
61
+ */
41
62
  function useSharedReducer(reducer, initial, options) {
42
- const engine = getReducer(
43
- options?.name ?? DEFAULT_NAME,
44
- options?.key ?? "default",
45
- reducer,
46
- initial
47
- );
48
- const value = useSyncExternalStore2(
49
- useCallback2((onChange) => engine.subscribe(onChange), [engine]),
50
- () => engine.getSnapshot(),
51
- () => initial
52
- );
53
- const dispatch = useCallback2((action) => engine.dispatch(action), [engine]);
54
- return [value, dispatch];
63
+ const engine = getReducer(options?.name ?? DEFAULT_NAME, options?.key ?? "default", reducer, initial);
64
+ return [useSyncExternalStore(useCallback((onChange) => engine.subscribe(onChange), [engine]), () => engine.getSnapshot(), () => initial), useCallback((action) => engine.dispatch(action), [engine])];
55
65
  }
56
-
57
- // src/use-shared-selector.ts
58
- import { useCallback as useCallback3, useRef, useSyncExternalStore as useSyncExternalStore3 } from "react";
66
+ //#endregion
67
+ //#region src/use-shared-selector.ts
68
+ /**
69
+ * Read a derived value from a shared store, re-rendering only when it changes.
70
+ *
71
+ * `useSharedState` subscribes to one key, which is the right shape for reading
72
+ * one thing. Reading *across* keys meant either one hook per key or a
73
+ * subscription to the whole store, and the latter re-renders on every write to
74
+ * anything in it.
75
+ *
76
+ * ```tsx
77
+ * const total = useSharedSelector<Cart, number>(
78
+ * (cart) => cart.items.length + cart.saved.length,
79
+ * );
80
+ * ```
81
+ *
82
+ * The selector runs on every store change; the component only re-renders when
83
+ * its result changes by `equal`.
84
+ *
85
+ * ## It reads, it does not declare
86
+ *
87
+ * `useSharedState(key, initial)` registers `key` with a default. A selector
88
+ * does not — it sees whatever is in the store, so a key nothing has registered
89
+ * or written yet is `undefined`. Write selectors that tolerate that, or declare
90
+ * the defaults with `useSharedState` (or a store initial) somewhere that
91
+ * mounts first.
92
+ *
93
+ * The same applies on a server, where the store is inert and empty: the
94
+ * selector runs against `{}`, and its result is what the server-rendered markup
95
+ * shows.
96
+ */
59
97
  function useSharedSelector(selector, options) {
60
- const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
61
- const equal = options?.equal ?? Object.is;
62
- const selectorRef = useRef(selector);
63
- selectorRef.current = selector;
64
- const equalRef = useRef(equal);
65
- equalRef.current = equal;
66
- const cache = useRef(null);
67
- const getSelection = useCallback3(() => {
68
- const source = store.getSnapshot();
69
- const select = selectorRef.current;
70
- const previous = cache.current;
71
- if (previous && previous.source === source && previous.select === select) return previous.value;
72
- const next = select(source);
73
- const value = previous && equalRef.current(previous.value, next) ? previous.value : next;
74
- cache.current = { source, select, value };
75
- return value;
76
- }, [store]);
77
- return useSyncExternalStore3(
78
- useCallback3((onChange) => store.subscribe(onChange), [store]),
79
- getSelection,
80
- getSelection
81
- );
98
+ const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
99
+ const equal = options?.equal ?? Object.is;
100
+ const selectorRef = useRef(selector);
101
+ selectorRef.current = selector;
102
+ const equalRef = useRef(equal);
103
+ equalRef.current = equal;
104
+ /**
105
+ * The last selection, and what produced it.
106
+ *
107
+ * `useSyncExternalStore` re-renders whenever the snapshot getter returns a
108
+ * value it has not seen, so a selector building a fresh object each call
109
+ * would loop forever. Caching against the *store* snapshot answers the common
110
+ * case in one identity check; caching against the selector as well is what
111
+ * keeps a changed selector from returning last render's answer.
112
+ */
113
+ const cache = useRef(null);
114
+ const getSelection = useCallback(() => {
115
+ const source = store.getSnapshot();
116
+ const select = selectorRef.current;
117
+ const previous = cache.current;
118
+ if (previous && previous.source === source && previous.select === select) return previous.value;
119
+ const next = select(source);
120
+ const value = previous && equalRef.current(previous.value, next) ? previous.value : next;
121
+ cache.current = {
122
+ source,
123
+ select,
124
+ value
125
+ };
126
+ return value;
127
+ }, [store]);
128
+ return useSyncExternalStore(useCallback((onChange) => store.subscribe(onChange), [store]), getSelection, getSelection);
82
129
  }
130
+ /** Shallow equality over an object or array, for selectors that build one. */
83
131
  function shallowEqual(a, b) {
84
- if (Object.is(a, b)) return true;
85
- if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
86
- const left = a;
87
- const right = b;
88
- const keys = Object.keys(left);
89
- if (keys.length !== Object.keys(right).length) return false;
90
- return keys.every((key) => Object.is(left[key], right[key]));
132
+ if (Object.is(a, b)) return true;
133
+ if (typeof a !== "object" || a === null || typeof b !== "object" || b === null) return false;
134
+ const left = a;
135
+ const right = b;
136
+ const keys = Object.keys(left);
137
+ if (keys.length !== Object.keys(right).length) return false;
138
+ return keys.every((key) => Object.is(left[key], right[key]));
91
139
  }
92
-
93
- // src/use-on-message.ts
94
- import { useEffect, useRef as useRef2 } from "react";
140
+ //#endregion
141
+ //#region src/use-on-message.ts
142
+ /** Get the page-wide typed channel for `name` (one instance per name). */
95
143
  function useChannel(name) {
96
- return getChannel(name);
144
+ return getChannel(name);
97
145
  }
146
+ /**
147
+ * Subscribe to one message type. The handler is kept fresh without
148
+ * resubscribing, so it may close over render state.
149
+ */
98
150
  function useOnMessage(channel, type, handler, options) {
99
- const handlerRef = useRef2(handler);
100
- useEffect(() => {
101
- handlerRef.current = handler;
102
- });
103
- const enabled = options?.enabled ?? true;
104
- const once = options?.once ?? false;
105
- useEffect(() => {
106
- if (!enabled) return;
107
- return channel.on(type, (payload, meta) => handlerRef.current(payload, meta), { once });
108
- }, [channel, type, enabled, once]);
151
+ const handlerRef = useRef(handler);
152
+ useEffect(() => {
153
+ handlerRef.current = handler;
154
+ });
155
+ const enabled = options?.enabled ?? true;
156
+ const once = options?.once ?? false;
157
+ useEffect(() => {
158
+ if (!enabled) return;
159
+ return channel.on(type, (payload, meta) => handlerRef.current(payload, meta), { once });
160
+ }, [
161
+ channel,
162
+ type,
163
+ enabled,
164
+ once
165
+ ]);
109
166
  }
167
+ /**
168
+ * Answer `ask`s of one type for as long as this component is mounted.
169
+ *
170
+ * A hook rather than a bare `channel.answer` call because a responder is a
171
+ * subscription: registering one in a render would leave the last unmounted
172
+ * component answering for the page.
173
+ *
174
+ * One responder per type per channel — registering a second replaces the first,
175
+ * which is the same first-wins-then-replace rule `answer` has in core.
176
+ */
110
177
  function useAnswer(channel, type, responder, options) {
111
- const responderRef = useRef2(responder);
112
- useEffect(() => {
113
- responderRef.current = responder;
114
- });
115
- const enabled = options?.enabled ?? true;
116
- useEffect(() => {
117
- if (!enabled) return;
118
- return channel.answer(type, (payload, meta) => responderRef.current(payload, meta));
119
- }, [channel, type, enabled]);
178
+ const responderRef = useRef(responder);
179
+ useEffect(() => {
180
+ responderRef.current = responder;
181
+ });
182
+ const enabled = options?.enabled ?? true;
183
+ useEffect(() => {
184
+ if (!enabled) return;
185
+ return channel.answer(type, (payload, meta) => responderRef.current(payload, meta));
186
+ }, [
187
+ channel,
188
+ type,
189
+ enabled
190
+ ]);
120
191
  }
192
+ /** The channel's post function (stable identity per channel). */
121
193
  function useSend(channel) {
122
- return channel.post;
194
+ return channel.post;
123
195
  }
196
+ /** The channel's ask function (stable identity per channel). */
124
197
  function useAsk(channel) {
125
- return channel.ask;
198
+ return channel.ask;
126
199
  }
127
-
128
- // src/define-channel.ts
200
+ //#endregion
201
+ //#region src/define-channel.ts
202
+ /**
203
+ * Bind a channel name and message map once, at module level, and get fully
204
+ * typed hooks back. Sugar over useChannel/useOnMessage/useSend — the same
205
+ * page-wide channel singleton is shared, so mixing bound and standalone
206
+ * hooks for one name is safe.
207
+ *
208
+ * Options are registered here and applied when the channel is first needed, so
209
+ * declaring a `schema` at module scope still constructs nothing on import.
210
+ */
129
211
  function defineChannel(name, options) {
130
- if (options) configureChannel(name, options);
131
- const useBoundSend = () => useSend(useChannel(name));
132
- const useBoundMessage = (type, handler) => useOnMessage(useChannel(name), type, handler);
133
- return {
134
- get: () => getChannel(name),
135
- useSend: useBoundSend,
136
- useOnMessage: useBoundMessage,
137
- // Removed in 1.0 along with the standalone renames; see deprecated.ts.
138
- useMessage: (type, handler) => {
139
- warnRenamed("ChannelHooks.useMessage", "ChannelHooks.useOnMessage");
140
- return useBoundMessage(type, handler);
141
- }
142
- };
212
+ if (options) configureChannel(name, options);
213
+ const useBoundSend = () => useSend(useChannel(name));
214
+ const useBoundMessage = (type, handler) => useOnMessage(useChannel(name), type, handler);
215
+ return {
216
+ get: () => getChannel(name),
217
+ useSend: useBoundSend,
218
+ useOnMessage: useBoundMessage
219
+ };
143
220
  }
144
-
145
- // src/create-store-hooks.ts
221
+ //#endregion
222
+ //#region src/create-store-hooks.ts
223
+ /**
224
+ * Bind a store name — and optionally persistence — once, at module level.
225
+ *
226
+ * Like defineChannel, this does not construct anything: it registers the
227
+ * options the registry will use when the store is first needed, so importing
228
+ * the module has no side effect. The store stays a singleton per name, so
229
+ * `createStoreHooks('settings', { persist })` and a bare
230
+ * `useSharedState('theme', 'dark', { store: 'settings' })` elsewhere resolve to
231
+ * the same store, and both get persistence.
232
+ *
233
+ * Must run before that store exists. Module evaluation always precedes render,
234
+ * so intended usage is automatic; if it does run late it throws rather than
235
+ * quietly handing back a store with no persistence.
236
+ */
146
237
  function createStoreHooks(name, options = {}) {
147
- const scope = options.scope ?? "everywhere";
148
- if (options.persist) {
149
- configureStore(name, scope, {
150
- persist: {
151
- adapter: options.persist,
152
- ...options.persistKeys ? { keys: options.persistKeys } : {},
153
- ...options.persistDebounceMs === void 0 ? {} : { debounceMs: options.persistDebounceMs },
154
- ...options.persistVersion === void 0 ? {} : { version: options.persistVersion },
155
- ...options.migrate ? { migrate: options.migrate } : {},
156
- ...options.onRestoreError ? { onRestoreError: options.onRestoreError } : {}
157
- }
158
- });
159
- }
160
- return {
161
- store: () => getStore(name, scope),
162
- // Removed in 1.0 along with the standalone renames; see deprecated.ts.
163
- get: () => {
164
- warnRenamed("StoreHooks.get()", "StoreHooks.store()");
165
- return getStore(name, scope);
166
- },
167
- useSharedState: (key, initial) => useSharedState(key, initial, { store: name, scope })
168
- };
238
+ const scope = options.scope ?? "everywhere";
239
+ if (options.persist) configureStore(name, scope, { persist: {
240
+ adapter: options.persist,
241
+ ...options.persistKeys ? { keys: options.persistKeys } : {},
242
+ ...options.persistDebounceMs === void 0 ? {} : { debounceMs: options.persistDebounceMs },
243
+ ...options.persistVersion === void 0 ? {} : { version: options.persistVersion },
244
+ ...options.migrate ? { migrate: options.migrate } : {},
245
+ ...options.onRestoreError ? { onRestoreError: options.onRestoreError } : {}
246
+ } });
247
+ return {
248
+ store: () => getStore(name, scope),
249
+ useSharedState: (key, initial) => useSharedState(key, initial, {
250
+ store: name,
251
+ scope
252
+ })
253
+ };
169
254
  }
170
-
171
- // src/use-hydrated.ts
172
- import { useEffect as useEffect2, useState } from "react";
255
+ //#endregion
256
+ //#region src/use-hydrated.ts
257
+ /**
258
+ * Whether a persisted store has finished restoring.
259
+ *
260
+ * `false` on the first render, then `true` once the restore lands — including
261
+ * the cases where there is nothing to restore, which settle immediately.
262
+ *
263
+ * Deliberately `false` first even for a synchronous adapter that has already
264
+ * finished. The alternative is a value that differs between the server render
265
+ * and the browser's hydrating render, which is a hydration mismatch on every
266
+ * app that uses it — the same reason `useClientId` reports `''` until the
267
+ * commit after hydration.
268
+ *
269
+ * The gap this closes only exists for **async** adapters, and it is one
270
+ * last-writer-wins makes invisible: a keystroke landing before the restore
271
+ * writes at counter 1, the restore arrives holding counter 5, and the newer
272
+ * keystroke is correctly discarded. The behaviour is right; the surprise is
273
+ * total. Gate the input and there is no gap:
274
+ *
275
+ * ```tsx
276
+ * const ready = useHydrated({ store: 'settings' });
277
+ * return <input disabled={!ready} value={draft} onChange={…} />;
278
+ * ```
279
+ *
280
+ * A synchronous adapter — `localStorageAdapter` and friends — has already
281
+ * restored by the time the store is handed back, so there is no gap to guard
282
+ * and this simply flips to `true` in the commit after mount.
283
+ */
173
284
  function useHydrated(options) {
174
- const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
175
- const [ready, setReady] = useState(false);
176
- useEffect2(() => {
177
- let cancelled = false;
178
- void store.hydrated.then(() => {
179
- if (!cancelled) setReady(true);
180
- });
181
- return () => {
182
- cancelled = true;
183
- };
184
- }, [store]);
185
- return ready;
285
+ const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
286
+ const [ready, setReady] = useState(false);
287
+ useEffect(() => {
288
+ let cancelled = false;
289
+ store.hydrated.then(() => {
290
+ if (!cancelled) setReady(true);
291
+ });
292
+ return () => {
293
+ cancelled = true;
294
+ };
295
+ }, [store]);
296
+ return ready;
186
297
  }
187
-
188
- // src/use-leader.ts
189
- import { useCallback as useCallback4, useEffect as useEffect3, useRef as useRef3, useSyncExternalStore as useSyncExternalStore4 } from "react";
190
- var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
298
+ //#endregion
299
+ //#region src/use-leader.ts
300
+ const NO_LEADER = Object.freeze({
301
+ leaderId: null,
302
+ isLeader: false
303
+ });
304
+ /**
305
+ * Who currently holds the seat on this bus, and whether it is us. Exactly one
306
+ * tab leads; opening another does not steal it, and closing the leader hands it
307
+ * over at once.
308
+ */
191
309
  function useLeader(options) {
192
- const leader = getLeader(options?.name ?? DEFAULT_NAME, options);
193
- const eligible = options?.eligible;
194
- useEffect3(() => {
195
- if (eligible === void 0) return;
196
- leader.setEligible(eligible);
197
- }, [leader, eligible]);
198
- return useSyncExternalStore4(
199
- useCallback4((onChange) => leader.subscribe(onChange), [leader]),
200
- () => leader.getSnapshot(),
201
- () => NO_LEADER
202
- );
310
+ const leader = getLeader(options?.name ?? DEFAULT_NAME, options);
311
+ const eligible = options?.eligible;
312
+ useEffect(() => {
313
+ if (eligible === void 0) return;
314
+ leader.setEligible(eligible);
315
+ }, [leader, eligible]);
316
+ return useSyncExternalStore(useCallback((onChange) => leader.subscribe(onChange), [leader]), () => leader.getSnapshot(), () => NO_LEADER);
203
317
  }
318
+ /** Is this tab the leader? */
204
319
  function useIsLeader(options) {
205
- return useLeader(options).isLeader;
320
+ return useLeader(options).isLeader;
206
321
  }
322
+ /**
323
+ * Run an effect only in the tab that holds the seat, and tear it down when the
324
+ * seat moves. The one place to put "exactly one tab owns the socket".
325
+ */
207
326
  function useLeaderEffect(effect, options) {
208
- const { isLeader } = useLeader(options);
209
- const effectRef = useRef3(effect);
210
- useEffect3(() => {
211
- effectRef.current = effect;
212
- });
213
- useEffect3(() => {
214
- if (!isLeader) return;
215
- return effectRef.current();
216
- }, [isLeader]);
327
+ const { isLeader } = useLeader(options);
328
+ const effectRef = useRef(effect);
329
+ useEffect(() => {
330
+ effectRef.current = effect;
331
+ });
332
+ useEffect(() => {
333
+ if (!isLeader) return;
334
+ return effectRef.current();
335
+ }, [isLeader]);
217
336
  }
218
-
219
- // src/namespace.ts
220
- import {
221
- createNamespace as createCoreNamespace
222
- } from "@use-everywhere/core";
337
+ //#endregion
338
+ //#region src/namespace.ts
339
+ /**
340
+ * Namespaced hooks and factories, so two independently deployed apps on one
341
+ * origin cannot collide by both taking the defaults.
342
+ *
343
+ * ```ts
344
+ * // checkout/bus.ts
345
+ * export const checkout = createNamespace('checkout');
346
+ *
347
+ * // anywhere in the checkout app
348
+ * const [items, setItems] = checkout.useSharedState('items', []);
349
+ * ```
350
+ *
351
+ * Call it **at module scope**, like `createStoreHooks` and `defineChannel`. It
352
+ * builds a small object of bound functions, and rebuilding that on every render
353
+ * would hand React a new `useSharedState` identity each time — harmless for the
354
+ * hooks themselves, which key off the bus name, but pointless work and a
355
+ * confusing thing to see in a profile.
356
+ *
357
+ * See the core `createNamespace` for what a namespace does and does not
358
+ * guarantee. In short: it prevents collision, not access.
359
+ */
223
360
  function createNamespace(namespace) {
224
- const core = createCoreNamespace(namespace);
225
- const { busName } = core;
226
- return {
227
- ...core,
228
- createStoreHooks: (name, options) => createStoreHooks(busName(name), options),
229
- defineChannel: (name, options) => defineChannel(busName(name), options),
230
- getSharedStore: (name, scope) => getSharedStore(busName(name), scope),
231
- useSharedState: (key, initial, options) => useSharedState(key, initial, { ...options, store: busName(options?.store) }),
232
- useSharedSelector: (selector, options) => useSharedSelector(selector, { ...options, store: busName(options?.store) }),
233
- // Removed in 1.0 along with the standalone renames; see deprecated.ts.
234
- defineStore: (name, options) => {
235
- warnRenamed("Namespace.defineStore", "Namespace.createStoreHooks");
236
- return createStoreHooks(busName(name), options);
237
- },
238
- useSharedStore: (selector, options) => {
239
- warnRenamed("Namespace.useSharedStore", "Namespace.useSharedSelector");
240
- return useSharedSelector(selector, { ...options, store: busName(options?.store) });
241
- },
242
- usePeers: (options) => usePeers({ ...options, name: busName(options?.name) }),
243
- usePresenceMetadata: (metadata, options) => usePresenceMetadata(metadata, { ...options, name: busName(options?.name) }),
244
- useHydrated: (options) => useHydrated({ ...options, store: busName(options?.store) }),
245
- useClientId: (options) => useClientId({ name: busName(options?.name) }),
246
- useLeader: (options) => useLeader({ ...options, name: busName(options?.name) }),
247
- useIsLeader: (options) => useIsLeader({ ...options, name: busName(options?.name) }),
248
- useLeaderEffect: (effect, options) => useLeaderEffect(effect, { ...options, name: busName(options?.name) })
249
- };
361
+ const core = createNamespace$1(namespace);
362
+ const { busName } = core;
363
+ return {
364
+ ...core,
365
+ createStoreHooks: (name, options) => createStoreHooks(busName(name), options),
366
+ defineChannel: (name, options) => defineChannel(busName(name), options),
367
+ getSharedStore: (name, scope) => getSharedStore(busName(name), scope),
368
+ useSharedState: (key, initial, options) => useSharedState(key, initial, {
369
+ ...options,
370
+ store: busName(options?.store)
371
+ }),
372
+ useSharedSelector: (selector, options) => useSharedSelector(selector, {
373
+ ...options,
374
+ store: busName(options?.store)
375
+ }),
376
+ usePeers: (options) => usePeers({
377
+ ...options,
378
+ name: busName(options?.name)
379
+ }),
380
+ usePresenceMetadata: (metadata, options) => usePresenceMetadata(metadata, {
381
+ ...options,
382
+ name: busName(options?.name)
383
+ }),
384
+ useHydrated: (options) => useHydrated({
385
+ ...options,
386
+ store: busName(options?.store)
387
+ }),
388
+ useClientId: (options) => useClientId({ name: busName(options?.name) }),
389
+ useLeader: (options) => useLeader({
390
+ ...options,
391
+ name: busName(options?.name)
392
+ }),
393
+ useIsLeader: (options) => useIsLeader({
394
+ ...options,
395
+ name: busName(options?.name)
396
+ }),
397
+ useLeaderEffect: (effect, options) => useLeaderEffect(effect, {
398
+ ...options,
399
+ name: busName(options?.name)
400
+ })
401
+ };
250
402
  }
251
-
252
- // src/use-window-result.ts
253
- import { useCallback as useCallback5, useEffect as useEffect4, useRef as useRef4, useState as useState2 } from "react";
254
- import { WindowClosedError } from "@use-everywhere/core";
255
- var IDLE = Object.freeze({
256
- status: "idle",
257
- result: void 0,
258
- error: void 0
403
+ //#endregion
404
+ //#region src/use-window-result.ts
405
+ const IDLE = Object.freeze({
406
+ status: "idle",
407
+ result: void 0,
408
+ error: void 0
259
409
  });
410
+ /**
411
+ * Drive an openWindow() flow from a component: the factory is called on
412
+ * open(), and the child window's lifecycle is folded into render state.
413
+ * Reopening replaces the previous window; a stale window's outcome is ignored.
414
+ *
415
+ * The child window deliberately outlives an unmount: closing a payment page
416
+ * because a route changed would lose the user's in-flight transaction. Call
417
+ * close() if the component owns the window's lifetime.
418
+ */
260
419
  function useWindowResult(factory) {
261
- const [state, setState] = useState2(IDLE);
262
- const current = useRef4(null);
263
- const factoryRef = useRef4(factory);
264
- useEffect4(() => {
265
- factoryRef.current = factory;
266
- });
267
- const open = useCallback5(() => {
268
- current.current?.close();
269
- let opened;
270
- try {
271
- opened = factoryRef.current();
272
- } catch (err) {
273
- setState({ status: "error", result: void 0, error: err });
274
- return;
275
- }
276
- current.current = opened;
277
- setState({ status: "opening", result: void 0, error: void 0 });
278
- const fresh = () => current.current === opened;
279
- opened.ready.then(
280
- () => {
281
- if (!fresh()) return;
282
- setState(
283
- (s) => s.status === "opening" ? { status: "connected", result: void 0, error: void 0 } : s
284
- );
285
- },
286
- () => {
287
- }
288
- // surfaced through result below
289
- );
290
- opened.result.then(
291
- (value) => {
292
- if (fresh()) setState({ status: "done", result: value, error: void 0 });
293
- },
294
- (err) => {
295
- if (!fresh()) return;
296
- setState(
297
- err instanceof WindowClosedError ? { status: "closed-early", result: void 0, error: err } : { status: "error", result: void 0, error: err }
298
- );
299
- }
300
- );
301
- }, []);
302
- const post = useCallback5((type, payload) => {
303
- current.current?.post(type, payload);
304
- }, []);
305
- const close = useCallback5(() => current.current?.close(), []);
306
- return { ...state, open, post, close };
307
- }
308
-
309
- // src/deprecated.ts
310
- function useMessage(channel, type, handler, options) {
311
- warnRenamed("useMessage", "useOnMessage");
312
- useOnMessage(channel, type, handler, options);
313
- }
314
- function useOpenedWindow(factory) {
315
- warnRenamed("useOpenedWindow", "useWindowResult");
316
- return useWindowResult(factory);
317
- }
318
- function useSharedStore(selector, options) {
319
- warnRenamed("useSharedStore", "useSharedSelector");
320
- return useSharedSelector(selector, options);
321
- }
322
- function defineStore(name, options = {}) {
323
- warnRenamed("defineStore", "createStoreHooks");
324
- return createStoreHooks(name, options);
420
+ const [state, setState] = useState(IDLE);
421
+ const current = useRef(null);
422
+ const factoryRef = useRef(factory);
423
+ useEffect(() => {
424
+ factoryRef.current = factory;
425
+ });
426
+ const open = useCallback(() => {
427
+ current.current?.close();
428
+ let opened;
429
+ try {
430
+ opened = factoryRef.current();
431
+ } catch (err) {
432
+ setState({
433
+ status: "error",
434
+ result: void 0,
435
+ error: err
436
+ });
437
+ return;
438
+ }
439
+ current.current = opened;
440
+ setState({
441
+ status: "opening",
442
+ result: void 0,
443
+ error: void 0
444
+ });
445
+ const fresh = () => current.current === opened;
446
+ opened.ready.then(() => {
447
+ if (!fresh()) return;
448
+ setState((s) => s.status === "opening" ? {
449
+ status: "connected",
450
+ result: void 0,
451
+ error: void 0
452
+ } : s);
453
+ }, () => {});
454
+ opened.result.then((value) => {
455
+ if (fresh()) setState({
456
+ status: "done",
457
+ result: value,
458
+ error: void 0
459
+ });
460
+ }, (err) => {
461
+ if (!fresh()) return;
462
+ setState(err instanceof WindowClosedError$1 ? {
463
+ status: "closed-early",
464
+ result: void 0,
465
+ error: err
466
+ } : {
467
+ status: "error",
468
+ result: void 0,
469
+ error: err
470
+ });
471
+ });
472
+ }, []);
473
+ const post = useCallback((type, payload) => {
474
+ current.current?.post(type, payload);
475
+ }, []);
476
+ const close = useCallback(() => current.current?.close(), []);
477
+ return {
478
+ ...state,
479
+ open,
480
+ post,
481
+ close
482
+ };
325
483
  }
326
-
327
- // src/index.ts
328
- import {
329
- createChannel,
330
- createSharedStore,
331
- createSharedReducer,
332
- createPresence,
333
- createLeader,
334
- webStorageAdapter,
335
- localStorageAdapter,
336
- sessionStorageAdapter,
337
- indexedDbAdapter,
338
- jsonSerializer,
339
- openWindow,
340
- connectToOpener,
341
- CID_PARAM,
342
- WindowClosedError as WindowClosedError2,
343
- HandshakeTimeoutError,
344
- newer,
345
- observeBus,
346
- enableDebug,
347
- getBusNames,
348
- getTransportKind,
349
- getWireSkew,
350
- WIRE_VERSION,
351
- BroadcastChannelTransport,
352
- NoopTransport,
353
- StorageTransport,
354
- SharedWorkerTransport,
355
- defaultTransport,
356
- isBroadcastChannelAvailable,
357
- isStorageEventAvailable,
358
- isSharedWorkerAvailable
359
- } from "@use-everywhere/core";
360
- export {
361
- BroadcastChannelTransport,
362
- CID_PARAM,
363
- DEFAULT_NAME,
364
- HandshakeTimeoutError,
365
- NoopTransport,
366
- SharedWorkerTransport,
367
- StorageTransport,
368
- WIRE_VERSION,
369
- WindowClosedError2 as WindowClosedError,
370
- connectToOpener,
371
- createChannel,
372
- createLeader,
373
- createNamespace,
374
- createPresence,
375
- createSharedReducer,
376
- createSharedStore,
377
- createStoreHooks,
378
- defaultTransport,
379
- defineChannel,
380
- defineStore,
381
- enableDebug,
382
- getBusNames,
383
- getLeader,
384
- getSharedStore,
385
- getTransportKind,
386
- getWireSkew,
387
- indexedDbAdapter,
388
- isBroadcastChannelAvailable,
389
- isSharedWorkerAvailable,
390
- isStorageEventAvailable,
391
- jsonSerializer,
392
- localStorageAdapter,
393
- newer,
394
- observeBus,
395
- openWindow,
396
- sessionStorageAdapter,
397
- shallowEqual,
398
- useAnswer,
399
- useAsk,
400
- useChannel,
401
- useClientId,
402
- useHydrated,
403
- useIsLeader,
404
- useLeader,
405
- useLeaderEffect,
406
- useMessage,
407
- useOnMessage,
408
- useOpenedWindow,
409
- usePeers,
410
- usePresenceMetadata,
411
- useSend,
412
- useSharedReducer,
413
- useSharedSelector,
414
- useSharedState,
415
- useSharedStore,
416
- useWindowResult,
417
- webStorageAdapter
418
- };
484
+ //#endregion
485
+ export { BroadcastChannelTransport, CID_PARAM, DEFAULT_NAME, HandshakeTimeoutError, NoopTransport, SharedWorkerTransport, StorageTransport, WIRE_VERSION, WindowClosedError, connectToOpener, createChannel, createLeader, createNamespace, createPresence, createSharedReducer, createSharedStore, createStoreHooks, defaultTransport, defineChannel, enableDebug, getBusNames, getLeader, getSharedStore, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isSharedWorkerAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, shallowEqual, useAnswer, useAsk, useChannel, useClientId, useHydrated, useIsLeader, useLeader, useLeaderEffect, useOnMessage, usePeers, usePresenceMetadata, useSend, useSharedReducer, useSharedSelector, useSharedState, useWindowResult, webStorageAdapter };