use-everywhere 0.4.1 → 0.5.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.
@@ -0,0 +1,187 @@
1
+ 'use client';
2
+
3
+ // src/registry.ts
4
+ import {
5
+ createChannel,
6
+ createLeader,
7
+ createPresence,
8
+ createSharedStore,
9
+ DEFAULT_NAME,
10
+ NoopTransport
11
+ } from "@use-everywhere/core";
12
+
13
+ // src/dev.ts
14
+ var inDev = false;
15
+ try {
16
+ inDev = process.env.NODE_ENV !== "production";
17
+ } catch {
18
+ }
19
+ var warned = /* @__PURE__ */ new Set();
20
+ function devWarn(message) {
21
+ if (!inDev || warned.has(message)) return;
22
+ warned.add(message);
23
+ console.warn(message);
24
+ }
25
+ var seenInitials = /* @__PURE__ */ new Map();
26
+ function warnOnInitialMismatch(storeName, key, initial) {
27
+ if (!inDev) return;
28
+ const id = `${storeName} ${key}`;
29
+ if (!seenInitials.has(id)) {
30
+ seenInitials.set(id, initial);
31
+ return;
32
+ }
33
+ const first = seenInitials.get(id);
34
+ const comparable = (v) => v === null || typeof v !== "object";
35
+ if (comparable(first) && comparable(initial) && !Object.is(first, initial)) {
36
+ devWarn(
37
+ `[use-everywhere] useSharedState('${key}') was called with different initial values (${String(first)} and ${String(initial)}). The first registration wins, so the second is ignored. Define the default once \u2014 defineStore, or a shared constant.`
38
+ );
39
+ }
40
+ }
41
+
42
+ // src/server-stubs.ts
43
+ var SERVER_CLIENT_ID = "";
44
+ var EMPTY = Object.freeze({});
45
+ var NO_PEERS = Object.freeze([]);
46
+ var noop = () => {
47
+ };
48
+ var unsubscribe = () => noop;
49
+ var INERT = Object.freeze({
50
+ clientId: SERVER_CLIENT_ID,
51
+ // store
52
+ state: EMPTY,
53
+ getVersions: () => EMPTY,
54
+ set: noop,
55
+ subscribeKey: unsubscribe,
56
+ registerKey: noop,
57
+ // store + leader share getSnapshot; presence has getPeers
58
+ getSnapshot: () => EMPTY,
59
+ getPeers: () => NO_PEERS,
60
+ // leader
61
+ resign: noop,
62
+ setEligible: noop,
63
+ // common
64
+ subscribe: unsubscribe,
65
+ close: noop
66
+ });
67
+ var createServerStore = () => INERT;
68
+ var createServerPresence = () => INERT;
69
+ var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
70
+ var createServerLeader = () => ({ ...INERT, getSnapshot: () => NO_LEADER });
71
+ var createServerChannel = (name) => ({ ...INERT, name, post: noop, on: unsubscribe });
72
+
73
+ // src/registry.ts
74
+ var stores = /* @__PURE__ */ new Map();
75
+ var presences = /* @__PURE__ */ new Map();
76
+ var channels = /* @__PURE__ */ new Map();
77
+ var leaders = /* @__PURE__ */ new Map();
78
+ var storeConfig = /* @__PURE__ */ new Map();
79
+ var isServer = () => typeof window === "undefined";
80
+ var scopeOptions = {
81
+ everywhere: {},
82
+ tabs: { accept: (meta) => meta.kind !== "worker" },
83
+ tab: { transport: () => new NoopTransport() }
84
+ };
85
+ function getSharedStore(name = DEFAULT_NAME, scope = "everywhere") {
86
+ return getStore(name, scope);
87
+ }
88
+ function configSignature(options) {
89
+ const persist = options?.persist;
90
+ if (!persist) return "none";
91
+ return `persist:${persist.keys?.join(",") ?? "*"}:${persist.debounceMs ?? "default"}`;
92
+ }
93
+ function configureStore(name, scope, options) {
94
+ const key = `${scope} ${name}`;
95
+ if (stores.has(key)) {
96
+ if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
97
+ devWarn(
98
+ `[use-everywhere] defineStore('${name}') ran after that store was already created, with different options. The live store keeps the configuration it was built with. Move defineStore to module scope, before any component reads the store.`
99
+ );
100
+ return;
101
+ }
102
+ storeConfig.set(key, options);
103
+ }
104
+ function getStore(name, scope = "everywhere") {
105
+ const key = `${scope} ${name}`;
106
+ let store = stores.get(key);
107
+ if (!store) {
108
+ store = isServer() ? createServerStore() : createSharedStore(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
109
+ stores.set(key, store);
110
+ }
111
+ return store;
112
+ }
113
+ function getPresence(name) {
114
+ let presence = presences.get(name);
115
+ if (!presence) {
116
+ presence = isServer() ? createServerPresence() : createPresence(name);
117
+ presences.set(name, presence);
118
+ }
119
+ return presence;
120
+ }
121
+ function getLeader(name, options) {
122
+ let leader = leaders.get(name);
123
+ if (!leader) {
124
+ leader = isServer() ? createServerLeader() : createLeader(name, options);
125
+ leaders.set(name, leader);
126
+ if (options) leaderOptions.set(name, options);
127
+ } else if (options) {
128
+ warnOnLeaderOptionConflict(name, options);
129
+ }
130
+ return leader;
131
+ }
132
+ var leaderOptions = /* @__PURE__ */ new Map();
133
+ function warnOnLeaderOptionConflict(name, options) {
134
+ const first = leaderOptions.get(name);
135
+ for (const key of ["heartbeatMs", "leaseMs"]) {
136
+ const requested = options[key];
137
+ if (requested !== void 0 && requested !== first?.[key]) {
138
+ devWarn(
139
+ `[use-everywhere] leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
140
+ );
141
+ }
142
+ }
143
+ }
144
+ function getChannel(name) {
145
+ let channel = channels.get(name);
146
+ if (!channel) {
147
+ channel = isServer() ? createServerChannel(name) : createChannel(name);
148
+ channels.set(name, channel);
149
+ }
150
+ return channel;
151
+ }
152
+
153
+ // src/use-peers.ts
154
+ import { useCallback, useSyncExternalStore } from "react";
155
+ var NO_PEERS2 = Object.freeze([]);
156
+ function usePeers(options) {
157
+ const presence = getPresence(options?.name ?? DEFAULT_NAME);
158
+ return useSyncExternalStore(
159
+ useCallback((onChange) => presence.subscribe(onChange), [presence]),
160
+ () => presence.getPeers(),
161
+ () => NO_PEERS2
162
+ );
163
+ }
164
+ function useClientId(options) {
165
+ const presence = getPresence(options?.name ?? DEFAULT_NAME);
166
+ return useSyncExternalStore(
167
+ // The id never changes for the life of a presence engine, so there is
168
+ // nothing to subscribe to — but uSES still needs the post-hydration read,
169
+ // which it performs when it attaches this subscription.
170
+ useCallback(() => () => {
171
+ }, [presence]),
172
+ () => presence.clientId,
173
+ () => SERVER_CLIENT_ID
174
+ );
175
+ }
176
+
177
+ export {
178
+ warnOnInitialMismatch,
179
+ DEFAULT_NAME,
180
+ getSharedStore,
181
+ configureStore,
182
+ getStore,
183
+ getLeader,
184
+ getChannel,
185
+ usePeers,
186
+ useClientId
187
+ };
@@ -31,9 +31,41 @@ var import_core2 = require("@use-everywhere/core");
31
31
 
32
32
  // src/registry.ts
33
33
  var import_core = require("@use-everywhere/core");
34
+
35
+ // src/server-stubs.ts
36
+ var SERVER_CLIENT_ID = "";
37
+ var EMPTY = Object.freeze({});
38
+ var NO_PEERS = Object.freeze([]);
39
+ var noop = () => {
40
+ };
41
+ var unsubscribe = () => noop;
42
+ var INERT = Object.freeze({
43
+ clientId: SERVER_CLIENT_ID,
44
+ // store
45
+ state: EMPTY,
46
+ getVersions: () => EMPTY,
47
+ set: noop,
48
+ subscribeKey: unsubscribe,
49
+ registerKey: noop,
50
+ // store + leader share getSnapshot; presence has getPeers
51
+ getSnapshot: () => EMPTY,
52
+ getPeers: () => NO_PEERS,
53
+ // leader
54
+ resign: noop,
55
+ setEligible: noop,
56
+ // common
57
+ subscribe: unsubscribe,
58
+ close: noop
59
+ });
60
+ var createServerStore = () => INERT;
61
+ var createServerPresence = () => INERT;
62
+ var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
63
+
64
+ // src/registry.ts
34
65
  var stores = /* @__PURE__ */ new Map();
35
66
  var presences = /* @__PURE__ */ new Map();
36
67
  var storeConfig = /* @__PURE__ */ new Map();
68
+ var isServer = () => typeof window === "undefined";
37
69
  var scopeOptions = {
38
70
  everywhere: {},
39
71
  tabs: { accept: (meta) => meta.kind !== "worker" },
@@ -46,7 +78,7 @@ function getStore(name, scope = "everywhere") {
46
78
  const key = `${scope} ${name}`;
47
79
  let store = stores.get(key);
48
80
  if (!store) {
49
- store = (0, import_core.createSharedStore)(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
81
+ store = isServer() ? createServerStore() : (0, import_core.createSharedStore)(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
50
82
  stores.set(key, store);
51
83
  }
52
84
  return store;
@@ -54,7 +86,7 @@ function getStore(name, scope = "everywhere") {
54
86
  function getPresence(name) {
55
87
  let presence = presences.get(name);
56
88
  if (!presence) {
57
- presence = (0, import_core.createPresence)(name);
89
+ presence = isServer() ? createServerPresence() : (0, import_core.createPresence)(name);
58
90
  presences.set(name, presence);
59
91
  }
60
92
  return presence;
@@ -62,13 +94,13 @@ function getPresence(name) {
62
94
 
63
95
  // src/use-peers.ts
64
96
  var import_react = require("react");
65
- var NO_PEERS = Object.freeze([]);
97
+ var NO_PEERS2 = Object.freeze([]);
66
98
  function usePeers(options) {
67
99
  const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME);
68
100
  return (0, import_react.useSyncExternalStore)(
69
101
  (0, import_react.useCallback)((onChange) => presence.subscribe(onChange), [presence]),
70
102
  () => presence.getPeers(),
71
- () => NO_PEERS
103
+ () => NO_PEERS2
72
104
  );
73
105
  }
74
106
 
@@ -2,7 +2,7 @@
2
2
  import {
3
3
  getSharedStore,
4
4
  usePeers
5
- } from "../chunk-PNFJW7QK.js";
5
+ } from "../chunk-YOXUZYKA.js";
6
6
 
7
7
  // src/devtools/inspector.tsx
8
8
  import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
package/dist/index.cjs CHANGED
@@ -43,13 +43,76 @@ module.exports = __toCommonJS(src_exports);
43
43
  // src/use-shared-state.ts
44
44
  var import_react = require("react");
45
45
 
46
+ // src/dev.ts
47
+ var inDev = false;
48
+ try {
49
+ inDev = process.env.NODE_ENV !== "production";
50
+ } catch {
51
+ }
52
+ var warned = /* @__PURE__ */ new Set();
53
+ function devWarn(message) {
54
+ if (!inDev || warned.has(message)) return;
55
+ warned.add(message);
56
+ console.warn(message);
57
+ }
58
+ var seenInitials = /* @__PURE__ */ new Map();
59
+ function warnOnInitialMismatch(storeName, key, initial) {
60
+ if (!inDev) return;
61
+ const id = `${storeName} ${key}`;
62
+ if (!seenInitials.has(id)) {
63
+ seenInitials.set(id, initial);
64
+ return;
65
+ }
66
+ const first = seenInitials.get(id);
67
+ const comparable = (v) => v === null || typeof v !== "object";
68
+ if (comparable(first) && comparable(initial) && !Object.is(first, initial)) {
69
+ devWarn(
70
+ `[use-everywhere] useSharedState('${key}') was called with different initial values (${String(first)} and ${String(initial)}). The first registration wins, so the second is ignored. Define the default once \u2014 defineStore, or a shared constant.`
71
+ );
72
+ }
73
+ }
74
+
46
75
  // src/registry.ts
47
76
  var import_core = require("@use-everywhere/core");
77
+
78
+ // src/server-stubs.ts
79
+ var SERVER_CLIENT_ID = "";
80
+ var EMPTY = Object.freeze({});
81
+ var NO_PEERS = Object.freeze([]);
82
+ var noop = () => {
83
+ };
84
+ var unsubscribe = () => noop;
85
+ var INERT = Object.freeze({
86
+ clientId: SERVER_CLIENT_ID,
87
+ // store
88
+ state: EMPTY,
89
+ getVersions: () => EMPTY,
90
+ set: noop,
91
+ subscribeKey: unsubscribe,
92
+ registerKey: noop,
93
+ // store + leader share getSnapshot; presence has getPeers
94
+ getSnapshot: () => EMPTY,
95
+ getPeers: () => NO_PEERS,
96
+ // leader
97
+ resign: noop,
98
+ setEligible: noop,
99
+ // common
100
+ subscribe: unsubscribe,
101
+ close: noop
102
+ });
103
+ var createServerStore = () => INERT;
104
+ var createServerPresence = () => INERT;
105
+ var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
106
+ var createServerLeader = () => ({ ...INERT, getSnapshot: () => NO_LEADER });
107
+ var createServerChannel = (name) => ({ ...INERT, name, post: noop, on: unsubscribe });
108
+
109
+ // src/registry.ts
48
110
  var stores = /* @__PURE__ */ new Map();
49
111
  var presences = /* @__PURE__ */ new Map();
50
112
  var channels = /* @__PURE__ */ new Map();
51
113
  var leaders = /* @__PURE__ */ new Map();
52
114
  var storeConfig = /* @__PURE__ */ new Map();
115
+ var isServer = () => typeof window === "undefined";
53
116
  var scopeOptions = {
54
117
  everywhere: {},
55
118
  tabs: { accept: (meta) => meta.kind !== "worker" },
@@ -58,12 +121,19 @@ var scopeOptions = {
58
121
  function getSharedStore(name = import_core.DEFAULT_NAME, scope = "everywhere") {
59
122
  return getStore(name, scope);
60
123
  }
124
+ function configSignature(options) {
125
+ const persist = options?.persist;
126
+ if (!persist) return "none";
127
+ return `persist:${persist.keys?.join(",") ?? "*"}:${persist.debounceMs ?? "default"}`;
128
+ }
61
129
  function configureStore(name, scope, options) {
62
130
  const key = `${scope} ${name}`;
63
131
  if (stores.has(key)) {
64
- throw new Error(
65
- `defineStore('${name}') ran after that store was already created. Move it to module scope \u2014 configuring a live store would silently hand you one without persistence.`
132
+ if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
133
+ devWarn(
134
+ `[use-everywhere] defineStore('${name}') ran after that store was already created, with different options. The live store keeps the configuration it was built with. Move defineStore to module scope, before any component reads the store.`
66
135
  );
136
+ return;
67
137
  }
68
138
  storeConfig.set(key, options);
69
139
  }
@@ -71,7 +141,7 @@ function getStore(name, scope = "everywhere") {
71
141
  const key = `${scope} ${name}`;
72
142
  let store = stores.get(key);
73
143
  if (!store) {
74
- store = (0, import_core.createSharedStore)(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
144
+ store = isServer() ? createServerStore() : (0, import_core.createSharedStore)(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
75
145
  stores.set(key, store);
76
146
  }
77
147
  return store;
@@ -79,7 +149,7 @@ function getStore(name, scope = "everywhere") {
79
149
  function getPresence(name) {
80
150
  let presence = presences.get(name);
81
151
  if (!presence) {
82
- presence = (0, import_core.createPresence)(name);
152
+ presence = isServer() ? createServerPresence() : (0, import_core.createPresence)(name);
83
153
  presences.set(name, presence);
84
154
  }
85
155
  return presence;
@@ -87,15 +157,30 @@ function getPresence(name) {
87
157
  function getLeader(name, options) {
88
158
  let leader = leaders.get(name);
89
159
  if (!leader) {
90
- leader = (0, import_core.createLeader)(name, options);
160
+ leader = isServer() ? createServerLeader() : (0, import_core.createLeader)(name, options);
91
161
  leaders.set(name, leader);
162
+ if (options) leaderOptions.set(name, options);
163
+ } else if (options) {
164
+ warnOnLeaderOptionConflict(name, options);
92
165
  }
93
166
  return leader;
94
167
  }
168
+ var leaderOptions = /* @__PURE__ */ new Map();
169
+ function warnOnLeaderOptionConflict(name, options) {
170
+ const first = leaderOptions.get(name);
171
+ for (const key of ["heartbeatMs", "leaseMs"]) {
172
+ const requested = options[key];
173
+ if (requested !== void 0 && requested !== first?.[key]) {
174
+ devWarn(
175
+ `[use-everywhere] leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
176
+ );
177
+ }
178
+ }
179
+ }
95
180
  function getChannel(name) {
96
181
  let channel = channels.get(name);
97
182
  if (!channel) {
98
- channel = (0, import_core.createChannel)(name);
183
+ channel = isServer() ? createServerChannel(name) : (0, import_core.createChannel)(name);
99
184
  channels.set(name, channel);
100
185
  }
101
186
  return channel;
@@ -103,8 +188,10 @@ function getChannel(name) {
103
188
 
104
189
  // src/use-shared-state.ts
105
190
  function useSharedState(key, initial, options) {
106
- const store = getStore(options?.store ?? import_core.DEFAULT_NAME, options?.scope ?? "everywhere");
191
+ const storeName = options?.store ?? import_core.DEFAULT_NAME;
192
+ const store = getStore(storeName, options?.scope ?? "everywhere");
107
193
  store.registerKey(key, initial);
194
+ warnOnInitialMismatch(storeName, key, initial);
108
195
  const value = (0, import_react.useSyncExternalStore)(
109
196
  (0, import_react.useCallback)((onChange) => store.subscribeKey(key, onChange), [store, key]),
110
197
  () => store.getSnapshot()[key],
@@ -167,22 +254,31 @@ function defineStore(name, options = {}) {
167
254
 
168
255
  // src/use-peers.ts
169
256
  var import_react3 = require("react");
170
- var NO_PEERS = Object.freeze([]);
257
+ var NO_PEERS2 = Object.freeze([]);
171
258
  function usePeers(options) {
172
259
  const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME);
173
260
  return (0, import_react3.useSyncExternalStore)(
174
261
  (0, import_react3.useCallback)((onChange) => presence.subscribe(onChange), [presence]),
175
262
  () => presence.getPeers(),
176
- () => NO_PEERS
263
+ () => NO_PEERS2
177
264
  );
178
265
  }
179
266
  function useClientId(options) {
180
- return getPresence(options?.name ?? import_core.DEFAULT_NAME).clientId;
267
+ const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME);
268
+ return (0, import_react3.useSyncExternalStore)(
269
+ // The id never changes for the life of a presence engine, so there is
270
+ // nothing to subscribe to — but uSES still needs the post-hydration read,
271
+ // which it performs when it attaches this subscription.
272
+ (0, import_react3.useCallback)(() => () => {
273
+ }, [presence]),
274
+ () => presence.clientId,
275
+ () => SERVER_CLIENT_ID
276
+ );
181
277
  }
182
278
 
183
279
  // src/use-leader.ts
184
280
  var import_react4 = require("react");
185
- var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
281
+ var NO_LEADER2 = Object.freeze({ leaderId: null, isLeader: false });
186
282
  function useLeader(options) {
187
283
  const leader = getLeader(options?.name ?? import_core.DEFAULT_NAME, options);
188
284
  const eligible = options?.eligible;
@@ -193,7 +289,7 @@ function useLeader(options) {
193
289
  return (0, import_react4.useSyncExternalStore)(
194
290
  (0, import_react4.useCallback)((onChange) => leader.subscribe(onChange), [leader]),
195
291
  () => leader.getSnapshot(),
196
- () => NO_LEADER
292
+ () => NO_LEADER2
197
293
  );
198
294
  }
199
295
  function useIsLeader(options) {
@@ -214,31 +310,36 @@ function useLeaderEffect(effect, options) {
214
310
  // src/use-opened-window.ts
215
311
  var import_react5 = require("react");
216
312
  var import_core2 = require("@use-everywhere/core");
313
+ var IDLE = Object.freeze({
314
+ status: "idle",
315
+ result: void 0,
316
+ error: void 0
317
+ });
217
318
  function useOpenedWindow(factory) {
218
- const [status, setStatus] = (0, import_react5.useState)("idle");
219
- const [result, setResult] = (0, import_react5.useState)(void 0);
220
- const [error, setError] = (0, import_react5.useState)(void 0);
319
+ const [state, setState] = (0, import_react5.useState)(IDLE);
221
320
  const current = (0, import_react5.useRef)(null);
222
321
  const factoryRef = (0, import_react5.useRef)(factory);
223
- factoryRef.current = factory;
322
+ (0, import_react5.useEffect)(() => {
323
+ factoryRef.current = factory;
324
+ });
224
325
  const open = (0, import_react5.useCallback)(() => {
225
326
  current.current?.close();
226
327
  let opened;
227
328
  try {
228
329
  opened = factoryRef.current();
229
330
  } catch (err) {
230
- setStatus("error");
231
- setError(err);
331
+ setState({ status: "error", result: void 0, error: err });
232
332
  return;
233
333
  }
234
334
  current.current = opened;
235
- setStatus("opening");
236
- setResult(void 0);
237
- setError(void 0);
335
+ setState({ status: "opening", result: void 0, error: void 0 });
238
336
  const fresh = () => current.current === opened;
239
337
  opened.ready.then(
240
338
  () => {
241
- if (fresh()) setStatus((s) => s === "opening" ? "connected" : s);
339
+ if (!fresh()) return;
340
+ setState(
341
+ (s) => s.status === "opening" ? { status: "connected", result: void 0, error: void 0 } : s
342
+ );
242
343
  },
243
344
  () => {
244
345
  }
@@ -246,14 +347,13 @@ function useOpenedWindow(factory) {
246
347
  );
247
348
  opened.result.then(
248
349
  (value) => {
249
- if (!fresh()) return;
250
- setResult(value);
251
- setStatus("done");
350
+ if (fresh()) setState({ status: "done", result: value, error: void 0 });
252
351
  },
253
352
  (err) => {
254
353
  if (!fresh()) return;
255
- setError(err);
256
- setStatus(err instanceof import_core2.WindowClosedError ? "closed-early" : "error");
354
+ setState(
355
+ err instanceof import_core2.WindowClosedError ? { status: "closed-early", result: void 0, error: err } : { status: "error", result: void 0, error: err }
356
+ );
257
357
  }
258
358
  );
259
359
  }, []);
@@ -261,7 +361,7 @@ function useOpenedWindow(factory) {
261
361
  current.current?.post(type, payload);
262
362
  }, []);
263
363
  const close = (0, import_react5.useCallback)(() => current.current?.close(), []);
264
- return { open, status, result, error, post, close };
364
+ return { ...state, open, post, close };
265
365
  }
266
366
 
267
367
  // src/index.ts
package/dist/index.d.cts CHANGED
@@ -1,4 +1,4 @@
1
- import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow } from '@use-everywhere/core';
1
+ import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
2
  export * from '@use-everywhere/core';
3
3
  export { DEFAULT_NAME } from '@use-everywhere/core';
4
4
 
@@ -21,6 +21,12 @@ interface UseSharedStateOptions {
21
21
  * this origin. Late-joining tabs hydrate to the current value; concurrent
22
22
  * writes converge last-writer-wins. Pass options.scope to delimit how much
23
23
  * is shared ('everywhere' | 'tabs' | 'tab').
24
+ *
25
+ * Note the convergence rule is last-writer-wins per key, not per operation:
26
+ * two tabs running `set(n => n + 1)` at the same moment agree on one value,
27
+ * and one of the increments is lost. For counters and other accumulating
28
+ * writes, drive them from a single tab (useLeaderEffect) until an op-based
29
+ * primitive lands.
24
30
  */
25
31
  declare function useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
26
32
 
@@ -105,7 +111,16 @@ declare function defineStore<S extends Record<string, unknown> = Record<string,
105
111
  declare function usePeers(options?: {
106
112
  name?: string;
107
113
  }): readonly Peer[];
108
- /** This client's own id on the presence bus (matches patch origin ids). */
114
+ /**
115
+ * This client's own id on the presence bus (matches patch origin ids).
116
+ *
117
+ * Read through useSyncExternalStore rather than returned straight from render,
118
+ * because the id is minted per environment: a server would render one value and
119
+ * the browser a different one, and any component that puts it in the DOM would
120
+ * mismatch on hydration. The server snapshot is a constant empty string, which
121
+ * React also uses for the client's hydrating render, so markup matches; the
122
+ * real id arrives in the commit straight after. Treat `''` as "not known yet".
123
+ */
109
124
  declare function useClientId(options?: {
110
125
  name?: string;
111
126
  }): string;
@@ -130,23 +145,59 @@ declare function useIsLeader(options?: UseLeaderOptions): boolean;
130
145
  declare function useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
131
146
 
132
147
  type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
133
- interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
148
+ /**
149
+ * The flow's state as a discriminated union, so `status` narrows `result` and
150
+ * `error` for you. Three independent fields could not: `result` was `R |
151
+ * undefined` even in the 'done' branch, which is why reading it needed a
152
+ * non-null assertion at every call site.
153
+ */
154
+ type OpenedWindowState<R> = {
155
+ status: 'idle';
156
+ result: undefined;
157
+ error: undefined;
158
+ } | {
159
+ status: 'opening';
160
+ result: undefined;
161
+ error: undefined;
162
+ } | {
163
+ status: 'connected';
164
+ result: undefined;
165
+ error: undefined;
166
+ } | {
167
+ status: 'done';
168
+ result: R;
169
+ error: undefined;
170
+ }
171
+ /** The child window went away before calling finish(). */
172
+ | {
173
+ status: 'closed-early';
174
+ result: undefined;
175
+ error: WindowClosedError;
176
+ }
177
+ /** The handshake timed out, the popup was blocked, or the factory threw. */
178
+ | {
179
+ status: 'error';
180
+ result: undefined;
181
+ error: unknown;
182
+ };
183
+ interface OpenedWindowControls<Out extends MessageMap, In extends MessageMap, R> {
134
184
  /** Call from a click handler (popup blockers require a user gesture). */
135
185
  open: () => void;
136
- status: OpenedWindowStatus;
137
- /** The child's finish() value, once status is 'done'. */
138
- result: R | undefined;
139
- error: unknown;
140
186
  /** Post to the child; no-op while nothing is open. */
141
187
  post: OpenedWindow<Out, In, R>['post'];
142
188
  close: () => void;
143
189
  }
190
+ type UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> = OpenedWindowState<R> & OpenedWindowControls<Out, In, R>;
144
191
 
145
192
  /**
146
193
  * Drive an openWindow() flow from a component: the factory is called on
147
194
  * open(), and the child window's lifecycle is folded into render state.
148
195
  * Reopening replaces the previous window; a stale window's outcome is ignored.
196
+ *
197
+ * The child window deliberately outlives an unmount: closing a payment page
198
+ * because a route changed would lose the user's in-flight transaction. Call
199
+ * close() if the component owns the window's lifetime.
149
200
  */
150
201
  declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
151
202
 
152
- export { type AnyStore, type ChannelHooks, type DefineStoreOptions, 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 };
203
+ 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 };
package/dist/index.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow } from '@use-everywhere/core';
1
+ import { SharedStore, LeaderOptions, Leader, MessageMap, Channel, MessageMeta, PersistAdapter, Peer, LeaderSnapshot, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
2
  export * from '@use-everywhere/core';
3
3
  export { DEFAULT_NAME } from '@use-everywhere/core';
4
4
 
@@ -21,6 +21,12 @@ interface UseSharedStateOptions {
21
21
  * this origin. Late-joining tabs hydrate to the current value; concurrent
22
22
  * writes converge last-writer-wins. Pass options.scope to delimit how much
23
23
  * is shared ('everywhere' | 'tabs' | 'tab').
24
+ *
25
+ * Note the convergence rule is last-writer-wins per key, not per operation:
26
+ * two tabs running `set(n => n + 1)` at the same moment agree on one value,
27
+ * and one of the increments is lost. For counters and other accumulating
28
+ * writes, drive them from a single tab (useLeaderEffect) until an op-based
29
+ * primitive lands.
24
30
  */
25
31
  declare function useSharedState<T>(key: string, initial: T, options?: UseSharedStateOptions): [T, (next: T | ((prev: T) => T)) => void];
26
32
 
@@ -105,7 +111,16 @@ declare function defineStore<S extends Record<string, unknown> = Record<string,
105
111
  declare function usePeers(options?: {
106
112
  name?: string;
107
113
  }): readonly Peer[];
108
- /** This client's own id on the presence bus (matches patch origin ids). */
114
+ /**
115
+ * This client's own id on the presence bus (matches patch origin ids).
116
+ *
117
+ * Read through useSyncExternalStore rather than returned straight from render,
118
+ * because the id is minted per environment: a server would render one value and
119
+ * the browser a different one, and any component that puts it in the DOM would
120
+ * mismatch on hydration. The server snapshot is a constant empty string, which
121
+ * React also uses for the client's hydrating render, so markup matches; the
122
+ * real id arrives in the commit straight after. Treat `''` as "not known yet".
123
+ */
109
124
  declare function useClientId(options?: {
110
125
  name?: string;
111
126
  }): string;
@@ -130,23 +145,59 @@ declare function useIsLeader(options?: UseLeaderOptions): boolean;
130
145
  declare function useLeaderEffect(effect: () => void | (() => void), options?: UseLeaderOptions): void;
131
146
 
132
147
  type OpenedWindowStatus = 'idle' | 'opening' | 'connected' | 'done' | 'closed-early' | 'error';
133
- interface UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> {
148
+ /**
149
+ * The flow's state as a discriminated union, so `status` narrows `result` and
150
+ * `error` for you. Three independent fields could not: `result` was `R |
151
+ * undefined` even in the 'done' branch, which is why reading it needed a
152
+ * non-null assertion at every call site.
153
+ */
154
+ type OpenedWindowState<R> = {
155
+ status: 'idle';
156
+ result: undefined;
157
+ error: undefined;
158
+ } | {
159
+ status: 'opening';
160
+ result: undefined;
161
+ error: undefined;
162
+ } | {
163
+ status: 'connected';
164
+ result: undefined;
165
+ error: undefined;
166
+ } | {
167
+ status: 'done';
168
+ result: R;
169
+ error: undefined;
170
+ }
171
+ /** The child window went away before calling finish(). */
172
+ | {
173
+ status: 'closed-early';
174
+ result: undefined;
175
+ error: WindowClosedError;
176
+ }
177
+ /** The handshake timed out, the popup was blocked, or the factory threw. */
178
+ | {
179
+ status: 'error';
180
+ result: undefined;
181
+ error: unknown;
182
+ };
183
+ interface OpenedWindowControls<Out extends MessageMap, In extends MessageMap, R> {
134
184
  /** Call from a click handler (popup blockers require a user gesture). */
135
185
  open: () => void;
136
- status: OpenedWindowStatus;
137
- /** The child's finish() value, once status is 'done'. */
138
- result: R | undefined;
139
- error: unknown;
140
186
  /** Post to the child; no-op while nothing is open. */
141
187
  post: OpenedWindow<Out, In, R>['post'];
142
188
  close: () => void;
143
189
  }
190
+ type UseOpenedWindow<Out extends MessageMap, In extends MessageMap, R> = OpenedWindowState<R> & OpenedWindowControls<Out, In, R>;
144
191
 
145
192
  /**
146
193
  * Drive an openWindow() flow from a component: the factory is called on
147
194
  * open(), and the child window's lifecycle is folded into render state.
148
195
  * Reopening replaces the previous window; a stale window's outcome is ignored.
196
+ *
197
+ * The child window deliberately outlives an unmount: closing a payment page
198
+ * because a route changed would lose the user's in-flight transaction. Call
199
+ * close() if the component owns the window's lifetime.
149
200
  */
150
201
  declare function useOpenedWindow<Out extends MessageMap, In extends MessageMap, R = unknown>(factory: () => OpenedWindow<Out, In, R>): UseOpenedWindow<Out, In, R>;
151
202
 
152
- export { type AnyStore, type ChannelHooks, type DefineStoreOptions, 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 };
203
+ 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 };
package/dist/index.js CHANGED
@@ -7,14 +7,17 @@ import {
7
7
  getSharedStore,
8
8
  getStore,
9
9
  useClientId,
10
- usePeers
11
- } from "./chunk-PNFJW7QK.js";
10
+ usePeers,
11
+ warnOnInitialMismatch
12
+ } from "./chunk-YOXUZYKA.js";
12
13
 
13
14
  // src/use-shared-state.ts
14
15
  import { useCallback, useSyncExternalStore } from "react";
15
16
  function useSharedState(key, initial, options) {
16
- const store = getStore(options?.store ?? DEFAULT_NAME, options?.scope ?? "everywhere");
17
+ const storeName = options?.store ?? DEFAULT_NAME;
18
+ const store = getStore(storeName, options?.scope ?? "everywhere");
17
19
  store.registerKey(key, initial);
20
+ warnOnInitialMismatch(storeName, key, initial);
18
21
  const value = useSyncExternalStore(
19
22
  useCallback((onChange) => store.subscribeKey(key, onChange), [store, key]),
20
23
  () => store.getSnapshot()[key],
@@ -107,33 +110,38 @@ function useLeaderEffect(effect, options) {
107
110
  }
108
111
 
109
112
  // src/use-opened-window.ts
110
- import { useCallback as useCallback3, useRef as useRef3, useState } from "react";
113
+ import { useCallback as useCallback3, useEffect as useEffect3, useRef as useRef3, useState } from "react";
111
114
  import { WindowClosedError } from "@use-everywhere/core";
115
+ var IDLE = Object.freeze({
116
+ status: "idle",
117
+ result: void 0,
118
+ error: void 0
119
+ });
112
120
  function useOpenedWindow(factory) {
113
- const [status, setStatus] = useState("idle");
114
- const [result, setResult] = useState(void 0);
115
- const [error, setError] = useState(void 0);
121
+ const [state, setState] = useState(IDLE);
116
122
  const current = useRef3(null);
117
123
  const factoryRef = useRef3(factory);
118
- factoryRef.current = factory;
124
+ useEffect3(() => {
125
+ factoryRef.current = factory;
126
+ });
119
127
  const open = useCallback3(() => {
120
128
  current.current?.close();
121
129
  let opened;
122
130
  try {
123
131
  opened = factoryRef.current();
124
132
  } catch (err) {
125
- setStatus("error");
126
- setError(err);
133
+ setState({ status: "error", result: void 0, error: err });
127
134
  return;
128
135
  }
129
136
  current.current = opened;
130
- setStatus("opening");
131
- setResult(void 0);
132
- setError(void 0);
137
+ setState({ status: "opening", result: void 0, error: void 0 });
133
138
  const fresh = () => current.current === opened;
134
139
  opened.ready.then(
135
140
  () => {
136
- if (fresh()) setStatus((s) => s === "opening" ? "connected" : s);
141
+ if (!fresh()) return;
142
+ setState(
143
+ (s) => s.status === "opening" ? { status: "connected", result: void 0, error: void 0 } : s
144
+ );
137
145
  },
138
146
  () => {
139
147
  }
@@ -141,14 +149,13 @@ function useOpenedWindow(factory) {
141
149
  );
142
150
  opened.result.then(
143
151
  (value) => {
144
- if (!fresh()) return;
145
- setResult(value);
146
- setStatus("done");
152
+ if (fresh()) setState({ status: "done", result: value, error: void 0 });
147
153
  },
148
154
  (err) => {
149
155
  if (!fresh()) return;
150
- setError(err);
151
- setStatus(err instanceof WindowClosedError ? "closed-early" : "error");
156
+ setState(
157
+ err instanceof WindowClosedError ? { status: "closed-early", result: void 0, error: err } : { status: "error", result: void 0, error: err }
158
+ );
152
159
  }
153
160
  );
154
161
  }, []);
@@ -156,7 +163,7 @@ function useOpenedWindow(factory) {
156
163
  current.current?.post(type, payload);
157
164
  }, []);
158
165
  const close = useCallback3(() => current.current?.close(), []);
159
- return { open, status, result, error, post, close };
166
+ return { ...state, open, post, close };
160
167
  }
161
168
 
162
169
  // src/index.ts
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-everywhere",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "description": "React hooks for state and messages shared across tabs, windows, and workers",
5
5
  "license": "MIT",
6
6
  "author": "Jonatan Kruszewski <jonakrusze@gmail.com>",
@@ -59,43 +59,43 @@
59
59
  "name": "everything (import *)",
60
60
  "path": "dist/index.js",
61
61
  "import": "*",
62
- "limit": "5.4 kB"
62
+ "limit": "6.6 kB"
63
63
  },
64
64
  {
65
65
  "name": "useSharedState",
66
66
  "path": "dist/index.js",
67
67
  "import": "{ useSharedState }",
68
- "limit": "1.8 kB"
68
+ "limit": "2.85 kB"
69
69
  },
70
70
  {
71
71
  "name": "useChannel + useMessage + useSend",
72
72
  "path": "dist/index.js",
73
73
  "import": "{ useChannel, useMessage, useSend }",
74
- "limit": "1 kB"
74
+ "limit": "1.6 kB"
75
75
  },
76
76
  {
77
77
  "name": "usePeers + useClientId",
78
78
  "path": "dist/index.js",
79
79
  "import": "{ usePeers, useClientId }",
80
- "limit": "1.1 kB"
80
+ "limit": "1.7 kB"
81
81
  },
82
82
  {
83
83
  "name": "useOpenedWindow + openWindow",
84
84
  "path": "dist/index.js",
85
85
  "import": "{ useOpenedWindow, openWindow }",
86
- "limit": "1.4 kB"
86
+ "limit": "1.5 kB"
87
87
  },
88
88
  {
89
89
  "name": "useLeader + useLeaderEffect",
90
90
  "path": "dist/index.js",
91
91
  "import": "{ useLeader, useLeaderEffect }",
92
- "limit": "1.8 kB"
92
+ "limit": "2.15 kB"
93
93
  },
94
94
  {
95
95
  "name": "defineStore (persisted)",
96
96
  "path": "dist/index.js",
97
97
  "import": "{ defineStore, localStorageAdapter }",
98
- "limit": "2.3 kB"
98
+ "limit": "3.3 kB"
99
99
  },
100
100
  {
101
101
  "name": "Inspector (devtools subpath)",
@@ -105,7 +105,7 @@
105
105
  }
106
106
  ],
107
107
  "dependencies": {
108
- "@use-everywhere/core": "0.4.1"
108
+ "@use-everywhere/core": "0.5.0"
109
109
  },
110
110
  "peerDependencies": {
111
111
  "react": ">=18"
@@ -1,92 +0,0 @@
1
- 'use client';
2
-
3
- // src/registry.ts
4
- import {
5
- createChannel,
6
- createLeader,
7
- createPresence,
8
- createSharedStore,
9
- DEFAULT_NAME,
10
- NoopTransport
11
- } from "@use-everywhere/core";
12
- var stores = /* @__PURE__ */ new Map();
13
- var presences = /* @__PURE__ */ new Map();
14
- var channels = /* @__PURE__ */ new Map();
15
- var leaders = /* @__PURE__ */ new Map();
16
- var storeConfig = /* @__PURE__ */ new Map();
17
- var scopeOptions = {
18
- everywhere: {},
19
- tabs: { accept: (meta) => meta.kind !== "worker" },
20
- tab: { transport: () => new NoopTransport() }
21
- };
22
- function getSharedStore(name = DEFAULT_NAME, scope = "everywhere") {
23
- return getStore(name, scope);
24
- }
25
- function configureStore(name, scope, options) {
26
- const key = `${scope} ${name}`;
27
- if (stores.has(key)) {
28
- throw new Error(
29
- `defineStore('${name}') ran after that store was already created. Move it to module scope \u2014 configuring a live store would silently hand you one without persistence.`
30
- );
31
- }
32
- storeConfig.set(key, options);
33
- }
34
- function getStore(name, scope = "everywhere") {
35
- const key = `${scope} ${name}`;
36
- let store = stores.get(key);
37
- if (!store) {
38
- store = createSharedStore(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
39
- stores.set(key, store);
40
- }
41
- return store;
42
- }
43
- function getPresence(name) {
44
- let presence = presences.get(name);
45
- if (!presence) {
46
- presence = createPresence(name);
47
- presences.set(name, presence);
48
- }
49
- return presence;
50
- }
51
- function getLeader(name, options) {
52
- let leader = leaders.get(name);
53
- if (!leader) {
54
- leader = createLeader(name, options);
55
- leaders.set(name, leader);
56
- }
57
- return leader;
58
- }
59
- function getChannel(name) {
60
- let channel = channels.get(name);
61
- if (!channel) {
62
- channel = createChannel(name);
63
- channels.set(name, channel);
64
- }
65
- return channel;
66
- }
67
-
68
- // src/use-peers.ts
69
- import { useCallback, useSyncExternalStore } from "react";
70
- var NO_PEERS = Object.freeze([]);
71
- function usePeers(options) {
72
- const presence = getPresence(options?.name ?? DEFAULT_NAME);
73
- return useSyncExternalStore(
74
- useCallback((onChange) => presence.subscribe(onChange), [presence]),
75
- () => presence.getPeers(),
76
- () => NO_PEERS
77
- );
78
- }
79
- function useClientId(options) {
80
- return getPresence(options?.name ?? DEFAULT_NAME).clientId;
81
- }
82
-
83
- export {
84
- DEFAULT_NAME,
85
- getSharedStore,
86
- configureStore,
87
- getStore,
88
- getLeader,
89
- getChannel,
90
- usePeers,
91
- useClientId
92
- };