use-everywhere 1.0.0 → 1.0.2

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,380 @@
1
+ 'use client';
2
+ let react = require("react");
3
+ let _use_everywhere_core = require("@use-everywhere/core");
4
+ //#region src/dev.ts
5
+ let inDev = false;
6
+ try {
7
+ inDev = process.env.NODE_ENV !== "production";
8
+ } catch {}
9
+ const warned = /* @__PURE__ */ new Set();
10
+ const DOCS = "https://rxova.org/packages/use-everywhere/errors";
11
+ /**
12
+ * Stamp a diagnostic with its code and the page that explains it. Core's twin
13
+ * of this, for the same reason the twin of `devWarn` exists — see above.
14
+ *
15
+ * Codes are permanent, and a retired one is never reused: an old build in
16
+ * somebody's browser is still emitting it. Core owns UE1xxx, this package
17
+ * UE2xxx.
18
+ */
19
+ function diagnostic(code, message) {
20
+ return `[use-everywhere] ${code}: ${message}\n → ${DOCS}/#${code.toLowerCase()}`;
21
+ }
22
+ /** Warn once per distinct message, development only. */
23
+ function devWarn(code, message) {
24
+ if (!inDev) return;
25
+ const line = diagnostic(code, message);
26
+ if (warned.has(line)) return;
27
+ warned.add(line);
28
+ console.warn(line);
29
+ }
30
+ /** The initial each key was first registered with. Populated only in development — dynamic keys would otherwise grow it without bound. */
31
+ const seenInitials = /* @__PURE__ */ new Map();
32
+ /**
33
+ * Catch two callers registering one key with different defaults. The first
34
+ * registration wins and the second is silently discarded, which is the kind of
35
+ * disagreement that surfaces much later as "why is this value not what I set".
36
+ */
37
+ function warnOnInitialMismatch(storeName, key, initial) {
38
+ if (!inDev) return;
39
+ const id = `${storeName} ${key}`;
40
+ if (!seenInitials.has(id)) {
41
+ seenInitials.set(id, initial);
42
+ return;
43
+ }
44
+ const first = seenInitials.get(id);
45
+ const comparable = (v) => v === null || typeof v !== "object";
46
+ if (comparable(first) && comparable(initial) && !Object.is(first, initial)) devWarn("UE2001", `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 — createStoreHooks, or a shared constant.`);
47
+ }
48
+ const EMPTY = Object.freeze({});
49
+ const NO_PEERS$1 = Object.freeze([]);
50
+ const noop = () => {};
51
+ const unsubscribe = () => noop;
52
+ /**
53
+ * One frozen object serving as store, presence and leader. Their surfaces do
54
+ * not collide, and a server render never distinguishes two inert engines by
55
+ * identity — so this is a single shared constant rather than three allocations
56
+ * in every bundle that imports a hook.
57
+ */
58
+ const INERT = Object.freeze({
59
+ clientId: "",
60
+ hydrated: Promise.resolve(),
61
+ state: EMPTY,
62
+ getVersions: () => EMPTY,
63
+ set: noop,
64
+ subscribeKey: unsubscribe,
65
+ registerKey: noop,
66
+ getSnapshot: () => EMPTY,
67
+ getPeers: () => NO_PEERS$1,
68
+ setMetadata: noop,
69
+ resign: noop,
70
+ setEligible: noop,
71
+ subscribe: unsubscribe,
72
+ close: noop
73
+ });
74
+ const createServerStore = () => INERT;
75
+ const createServerPresence = () => INERT;
76
+ /**
77
+ * Leadership needs its own snapshot shape, so it wraps the shared constant.
78
+ * `waitForLeadership` never settles on a server: there is no election to win,
79
+ * and resolving would run leader-only work during a render that is about to be
80
+ * thrown away. A pending promise is the honest answer.
81
+ */
82
+ const NO_LEADER = Object.freeze({
83
+ leaderId: null,
84
+ isLeader: false
85
+ });
86
+ const NEVER = () => new Promise(() => {});
87
+ const createServerLeader = () => ({
88
+ ...INERT,
89
+ strategy: "heartbeat",
90
+ getSnapshot: () => NO_LEADER,
91
+ waitForLeadership: NEVER
92
+ });
93
+ /**
94
+ * A reducer carries its initial value, so it allocates too. `dispatch` is a
95
+ * no-op: a server render has no peers to order anything with, and an action
96
+ * applied here would show a value the browser is about to disagree with.
97
+ */
98
+ const createServerReducer = (initial) => ({
99
+ ...INERT,
100
+ getSnapshot: () => initial,
101
+ dispatch: noop,
102
+ pendingCount: () => 0
103
+ });
104
+ /** Channels carry their name, so this is the one double that allocates. */
105
+ const createServerChannel = (name) => ({
106
+ ...INERT,
107
+ name,
108
+ post: noop,
109
+ on: unsubscribe
110
+ });
111
+ //#endregion
112
+ //#region src/registry.ts
113
+ const stores = /* @__PURE__ */ new Map();
114
+ const presences = /* @__PURE__ */ new Map();
115
+ const channels = /* @__PURE__ */ new Map();
116
+ const leaders = /* @__PURE__ */ new Map();
117
+ const storeConfig = /* @__PURE__ */ new Map();
118
+ /**
119
+ * Checked per call, not captured once: a module evaluated during SSR and a
120
+ * module evaluated in the browser are different module instances, so there is
121
+ * no cache to invalidate — but reading it lazily keeps the bundler from
122
+ * folding the branch away in a build that serves both.
123
+ */
124
+ const isServer = () => typeof window === "undefined";
125
+ /** Per-scope store creation: what leaves this tab and what is let back in. */
126
+ const scopeOptions = {
127
+ everywhere: {},
128
+ tabs: { accept: (meta) => meta.kind !== "worker" },
129
+ tab: { transport: () => new _use_everywhere_core.NoopTransport() }
130
+ };
131
+ /** Imperative access to the store behind useSharedState (patch logs, non-React code). */
132
+ function getSharedStore(name = _use_everywhere_core.DEFAULT_NAME, scope = "everywhere") {
133
+ return getStore(name, scope);
134
+ }
135
+ /**
136
+ * Options to build a store with when it is first needed. Registered by
137
+ * createStoreHooks at module scope and consumed by getStore on creation — which is
138
+ * what lets persistence be declared in one place without constructing anything
139
+ * on import.
140
+ */
141
+ /**
142
+ * A configuration's shape, ignoring the adapter's identity. Hot Module
143
+ * Replacement re-evaluates the defining module and builds a *new* adapter
144
+ * object each time, so identity comparison would call every hot edit a
145
+ * conflict; what actually matters is whether the store would be built
146
+ * differently.
147
+ */
148
+ function configSignature(options) {
149
+ const persist = options?.persist;
150
+ if (!persist) return "none";
151
+ return `persist:${persist.keys?.join(",") ?? "*"}:${persist.debounceMs ?? "default"}:v${persist.version ?? 0}`;
152
+ }
153
+ function configureStore(name, scope, options) {
154
+ const key = `${scope} ${name}`;
155
+ if (stores.has(key)) {
156
+ if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
157
+ if (process.env.NODE_ENV !== "production") devWarn("UE2002", `createStoreHooks('${name}') ran after that store was already created, with different options. The live store keeps the configuration it was built with. Move createStoreHooks to module scope, before any component reads the store.`);
158
+ return;
159
+ }
160
+ storeConfig.set(key, options);
161
+ }
162
+ function getStore(name, scope = "everywhere") {
163
+ const key = `${scope} ${name}`;
164
+ let store = stores.get(key);
165
+ if (!store) {
166
+ store = isServer() ? createServerStore() : (0, _use_everywhere_core.createSharedStore)(name, {}, {
167
+ ...scopeOptions[scope],
168
+ ...storeConfig.get(key)
169
+ });
170
+ stores.set(key, store);
171
+ }
172
+ return store;
173
+ }
174
+ /**
175
+ * One Presence per name per tab.
176
+ *
177
+ * `includeSelf` is part of the key rather than the options, because it changes
178
+ * what the roster *is*: two components on one name disagreeing about it would
179
+ * otherwise silently get whichever answer was built first.
180
+ */
181
+ function getPresence(name, includeSelf = false) {
182
+ const key = includeSelf ? `self ${name}` : name;
183
+ let presence = presences.get(key);
184
+ if (!presence) {
185
+ presence = isServer() ? createServerPresence() : (0, _use_everywhere_core.createPresence)(name, { includeSelf });
186
+ presences.set(key, presence);
187
+ }
188
+ return presence;
189
+ }
190
+ /**
191
+ * One Leader per name per tab. Eligibility is deliberately *not* part of the
192
+ * key: two Leaders on one name would share a bus and a clientId, and since a
193
+ * post never loops back locally, neither would ever see the other's claims.
194
+ * Timing options are first-wins, like every other engine here.
195
+ */
196
+ function getLeader(name, options) {
197
+ let leader = leaders.get(name);
198
+ if (!leader) {
199
+ leader = isServer() ? createServerLeader() : (0, _use_everywhere_core.createLeader)(name, options);
200
+ leaders.set(name, leader);
201
+ if (options) leaderOptions.set(name, options);
202
+ } else if (options) warnOnLeaderOptionConflict(name, options);
203
+ return leader;
204
+ }
205
+ /** What the first caller elected with, so a later caller asking for different timings can be told it was ignored. */
206
+ const leaderOptions = /* @__PURE__ */ new Map();
207
+ function warnOnLeaderOptionConflict(name, options) {
208
+ const first = leaderOptions.get(name);
209
+ for (const key of ["heartbeatMs", "leaseMs"]) {
210
+ const requested = options[key];
211
+ if (requested !== void 0 && requested !== first?.[key]) {
212
+ if (process.env.NODE_ENV !== "production") devWarn("UE2003", `leader "${name}": ${key} ignored — the first useLeader/getLeader call fixes the election timings for this tab.`);
213
+ }
214
+ }
215
+ }
216
+ /**
217
+ * Options to build a channel with when it is first needed. Registered by
218
+ * defineChannel at module scope and consumed by getChannel on creation — the
219
+ * same deferral createStoreHooks uses, so declaring a schema constructs nothing on
220
+ * import.
221
+ */
222
+ const channelConfig = /* @__PURE__ */ new Map();
223
+ function configureChannel(name, options) {
224
+ if (channels.has(name)) {
225
+ const before = Object.keys(channelConfig.get(name)?.schema ?? {}).sort();
226
+ const after = Object.keys(options.schema ?? {}).sort();
227
+ if (before.join() === after.join()) return;
228
+ if (process.env.NODE_ENV !== "production") devWarn("UE2004", `defineChannel('${name}') ran after that channel was already created, with different options. The live channel keeps the configuration it was built with. Move defineChannel to module scope, before any component sends or receives on it.`);
229
+ return;
230
+ }
231
+ channelConfig.set(name, options);
232
+ }
233
+ const reducers = /* @__PURE__ */ new Map();
234
+ /**
235
+ * One reducer per name+key per tab, like every other engine here.
236
+ *
237
+ * The reducer is handed this tab's existing `Leader` rather than electing its
238
+ * own: the leader is the sequencer, and a page that already has a seat for this
239
+ * bus must not run a second election to get another one.
240
+ *
241
+ * The first caller's reducer function wins. A hook re-renders with a new
242
+ * function identity every time, and swapping the fold under a history that has
243
+ * already been applied would give this tab a different answer from its peers —
244
+ * which is the one thing an ordered reducer exists to prevent.
245
+ */
246
+ function getReducer(name, key, reducer, initial) {
247
+ const id = `${name} ${key}`;
248
+ let existing = reducers.get(id);
249
+ if (!existing) {
250
+ existing = isServer() ? createServerReducer(initial) : (0, _use_everywhere_core.createSharedReducer)(name, reducer, initial, {
251
+ key,
252
+ leader: getLeader(name)
253
+ });
254
+ reducers.set(id, existing);
255
+ }
256
+ return existing;
257
+ }
258
+ function getChannel(name) {
259
+ let channel = channels.get(name);
260
+ if (!channel) {
261
+ channel = isServer() ? createServerChannel(name) : (0, _use_everywhere_core.createChannel)(name, channelConfig.get(name));
262
+ channels.set(name, channel);
263
+ }
264
+ return channel;
265
+ }
266
+ //#endregion
267
+ //#region src/use-peers.ts
268
+ const NO_PEERS = Object.freeze([]);
269
+ /**
270
+ * The tabs/windows/workers currently alive on this origin.
271
+ *
272
+ * Each peer carries whatever it published about itself as `metadata` — see
273
+ * {@link usePresenceMetadata} for publishing this client's.
274
+ */
275
+ function usePeers(options) {
276
+ const presence = getPresence(options?.name ?? _use_everywhere_core.DEFAULT_NAME, options?.includeSelf ?? false);
277
+ return (0, react.useSyncExternalStore)((0, react.useCallback)((onChange) => presence.subscribe(onChange), [presence]), () => presence.getPeers(), () => NO_PEERS);
278
+ }
279
+ /**
280
+ * This client's own id on the presence bus (matches patch origin ids).
281
+ *
282
+ * Read through useSyncExternalStore rather than returned straight from render,
283
+ * because the id is minted per environment: a server would render one value and
284
+ * the browser a different one, and any component that puts it in the DOM would
285
+ * mismatch on hydration. The server snapshot is a constant empty string, which
286
+ * React also uses for the client's hydrating render, so markup matches; the
287
+ * real id arrives in the commit straight after. Treat `''` as "not known yet".
288
+ */
289
+ function useClientId(options) {
290
+ const presence = getPresence(options?.name ?? _use_everywhere_core.DEFAULT_NAME);
291
+ return (0, react.useSyncExternalStore)((0, react.useCallback)(() => () => {}, [presence]), () => presence.clientId, () => "");
292
+ }
293
+ /**
294
+ * Publish what this client wants peers to know about it — a display name, a tab
295
+ * title, a cursor.
296
+ *
297
+ * Safe to call with a fresh object every render: the value is compared by
298
+ * contents, so an unchanged one announces nothing and re-renders nobody.
299
+ *
300
+ * ```tsx
301
+ * usePresenceMetadata({ name: user.name, editing: currentDocId });
302
+ * ```
303
+ *
304
+ * Published in an effect rather than during render, because announcing is a
305
+ * side effect on every other tab — and a render that React throws away must not
306
+ * be one other tabs already saw.
307
+ */
308
+ function usePresenceMetadata(metadata, options) {
309
+ const presence = getPresence(options?.name ?? _use_everywhere_core.DEFAULT_NAME, options?.includeSelf ?? false);
310
+ (0, react.useEffect)(() => {
311
+ presence.setMetadata(metadata);
312
+ }, [presence, metadata]);
313
+ }
314
+ //#endregion
315
+ Object.defineProperty(exports, "configureChannel", {
316
+ enumerable: true,
317
+ get: function() {
318
+ return configureChannel;
319
+ }
320
+ });
321
+ Object.defineProperty(exports, "configureStore", {
322
+ enumerable: true,
323
+ get: function() {
324
+ return configureStore;
325
+ }
326
+ });
327
+ Object.defineProperty(exports, "getChannel", {
328
+ enumerable: true,
329
+ get: function() {
330
+ return getChannel;
331
+ }
332
+ });
333
+ Object.defineProperty(exports, "getLeader", {
334
+ enumerable: true,
335
+ get: function() {
336
+ return getLeader;
337
+ }
338
+ });
339
+ Object.defineProperty(exports, "getReducer", {
340
+ enumerable: true,
341
+ get: function() {
342
+ return getReducer;
343
+ }
344
+ });
345
+ Object.defineProperty(exports, "getSharedStore", {
346
+ enumerable: true,
347
+ get: function() {
348
+ return getSharedStore;
349
+ }
350
+ });
351
+ Object.defineProperty(exports, "getStore", {
352
+ enumerable: true,
353
+ get: function() {
354
+ return getStore;
355
+ }
356
+ });
357
+ Object.defineProperty(exports, "useClientId", {
358
+ enumerable: true,
359
+ get: function() {
360
+ return useClientId;
361
+ }
362
+ });
363
+ Object.defineProperty(exports, "usePeers", {
364
+ enumerable: true,
365
+ get: function() {
366
+ return usePeers;
367
+ }
368
+ });
369
+ Object.defineProperty(exports, "usePresenceMetadata", {
370
+ enumerable: true,
371
+ get: function() {
372
+ return usePresenceMetadata;
373
+ }
374
+ });
375
+ Object.defineProperty(exports, "warnOnInitialMismatch", {
376
+ enumerable: true,
377
+ get: function() {
378
+ return warnOnInitialMismatch;
379
+ }
380
+ });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-everywhere",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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>",
@@ -170,7 +170,7 @@
170
170
  }
171
171
  ],
172
172
  "dependencies": {
173
- "@use-everywhere/core": "1.0.0"
173
+ "@use-everywhere/core": "1.0.2"
174
174
  },
175
175
  "peerDependencies": {
176
176
  "react": ">=18",
@@ -193,9 +193,10 @@
193
193
  "react": "^19.2.8",
194
194
  "react-dom": "^19.2.8",
195
195
  "size-limit": "^13.0.3",
196
- "tsup": "^8.5.1",
196
+ "tsdown": "^0.23.0",
197
197
  "typescript": "^6.0.3",
198
- "vitest": "^4.1.11"
198
+ "vitest": "^4.1.11",
199
+ "@repo/config": "0.0.0"
199
200
  },
200
201
  "typesVersions": {
201
202
  "*": {
@@ -211,7 +212,7 @@
211
212
  }
212
213
  },
213
214
  "scripts": {
214
- "build": "tsup",
215
+ "build": "tsdown",
215
216
  "test": "vitest run --coverage",
216
217
  "typecheck": "tsc --noEmit",
217
218
  "size": "size-limit",
@@ -1,259 +0,0 @@
1
- 'use client';
2
-
3
- // src/registry.ts
4
- import {
5
- createChannel,
6
- createLeader,
7
- createPresence,
8
- createSharedReducer,
9
- createSharedStore,
10
- DEFAULT_NAME,
11
- NoopTransport
12
- } from "@use-everywhere/core";
13
-
14
- // src/dev.ts
15
- var inDev = false;
16
- try {
17
- inDev = process.env.NODE_ENV !== "production";
18
- } catch {
19
- }
20
- var warned = /* @__PURE__ */ new Set();
21
- var DOCS = "https://rxova.org/packages/use-everywhere/errors";
22
- function diagnostic(code, message) {
23
- return `[use-everywhere] ${code}: ${message}
24
- \u2192 ${DOCS}/#${code.toLowerCase()}`;
25
- }
26
- function devWarn(code, message) {
27
- if (!inDev) return;
28
- const line = diagnostic(code, message);
29
- if (warned.has(line)) return;
30
- warned.add(line);
31
- console.warn(line);
32
- }
33
- var seenInitials = /* @__PURE__ */ new Map();
34
- function warnOnInitialMismatch(storeName, key, initial) {
35
- if (!inDev) return;
36
- const id = `${storeName} ${key}`;
37
- if (!seenInitials.has(id)) {
38
- seenInitials.set(id, initial);
39
- return;
40
- }
41
- const first = seenInitials.get(id);
42
- const comparable = (v) => v === null || typeof v !== "object";
43
- if (comparable(first) && comparable(initial) && !Object.is(first, initial)) {
44
- devWarn(
45
- "UE2001",
46
- `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 createStoreHooks, or a shared constant.`
47
- );
48
- }
49
- }
50
-
51
- // src/server-stubs.ts
52
- var SERVER_CLIENT_ID = "";
53
- var EMPTY = Object.freeze({});
54
- var NO_PEERS = Object.freeze([]);
55
- var noop = () => {
56
- };
57
- var unsubscribe = () => noop;
58
- var INERT = Object.freeze({
59
- clientId: SERVER_CLIENT_ID,
60
- // store
61
- //
62
- // Resolved, unlike the leader's `waitForLeadership` below. A server render
63
- // has nothing to restore and is documented to render defaults, so `await
64
- // store.hydrated` must proceed — a never-settling promise would hang the
65
- // render rather than describe it honestly.
66
- hydrated: Promise.resolve(),
67
- state: EMPTY,
68
- getVersions: () => EMPTY,
69
- set: noop,
70
- subscribeKey: unsubscribe,
71
- registerKey: noop,
72
- // store + leader share getSnapshot; presence has getPeers
73
- getSnapshot: () => EMPTY,
74
- getPeers: () => NO_PEERS,
75
- // presence
76
- setMetadata: noop,
77
- // leader
78
- resign: noop,
79
- setEligible: noop,
80
- // common
81
- subscribe: unsubscribe,
82
- close: noop
83
- });
84
- var createServerStore = () => INERT;
85
- var createServerPresence = () => INERT;
86
- var NO_LEADER = Object.freeze({ leaderId: null, isLeader: false });
87
- var NEVER = () => new Promise(() => {
88
- });
89
- var createServerLeader = () => ({
90
- ...INERT,
91
- strategy: "heartbeat",
92
- getSnapshot: () => NO_LEADER,
93
- waitForLeadership: NEVER
94
- });
95
- var createServerReducer = (initial) => ({
96
- ...INERT,
97
- getSnapshot: () => initial,
98
- dispatch: noop,
99
- pendingCount: () => 0
100
- });
101
- var createServerChannel = (name) => ({ ...INERT, name, post: noop, on: unsubscribe });
102
-
103
- // src/registry.ts
104
- var stores = /* @__PURE__ */ new Map();
105
- var presences = /* @__PURE__ */ new Map();
106
- var channels = /* @__PURE__ */ new Map();
107
- var leaders = /* @__PURE__ */ new Map();
108
- var storeConfig = /* @__PURE__ */ new Map();
109
- var isServer = () => typeof window === "undefined";
110
- var scopeOptions = {
111
- everywhere: {},
112
- tabs: { accept: (meta) => meta.kind !== "worker" },
113
- tab: { transport: () => new NoopTransport() }
114
- };
115
- function getSharedStore(name = DEFAULT_NAME, scope = "everywhere") {
116
- return getStore(name, scope);
117
- }
118
- function configSignature(options) {
119
- const persist = options?.persist;
120
- if (!persist) return "none";
121
- return `persist:${persist.keys?.join(",") ?? "*"}:${persist.debounceMs ?? "default"}:v${persist.version ?? 0}`;
122
- }
123
- function configureStore(name, scope, options) {
124
- const key = `${scope} ${name}`;
125
- if (stores.has(key)) {
126
- if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
127
- if (process.env.NODE_ENV !== "production") {
128
- devWarn(
129
- "UE2002",
130
- `createStoreHooks('${name}') ran after that store was already created, with different options. The live store keeps the configuration it was built with. Move createStoreHooks to module scope, before any component reads the store.`
131
- );
132
- }
133
- return;
134
- }
135
- storeConfig.set(key, options);
136
- }
137
- function getStore(name, scope = "everywhere") {
138
- const key = `${scope} ${name}`;
139
- let store = stores.get(key);
140
- if (!store) {
141
- store = isServer() ? createServerStore() : createSharedStore(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
142
- stores.set(key, store);
143
- }
144
- return store;
145
- }
146
- function getPresence(name, includeSelf = false) {
147
- const key = includeSelf ? `self ${name}` : name;
148
- let presence = presences.get(key);
149
- if (!presence) {
150
- presence = isServer() ? createServerPresence() : createPresence(name, { includeSelf });
151
- presences.set(key, presence);
152
- }
153
- return presence;
154
- }
155
- function getLeader(name, options) {
156
- let leader = leaders.get(name);
157
- if (!leader) {
158
- leader = isServer() ? createServerLeader() : createLeader(name, options);
159
- leaders.set(name, leader);
160
- if (options) leaderOptions.set(name, options);
161
- } else if (options) {
162
- warnOnLeaderOptionConflict(name, options);
163
- }
164
- return leader;
165
- }
166
- var leaderOptions = /* @__PURE__ */ new Map();
167
- function warnOnLeaderOptionConflict(name, options) {
168
- const first = leaderOptions.get(name);
169
- for (const key of ["heartbeatMs", "leaseMs"]) {
170
- const requested = options[key];
171
- if (requested !== void 0 && requested !== first?.[key]) {
172
- if (process.env.NODE_ENV !== "production") {
173
- devWarn(
174
- "UE2003",
175
- `leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
176
- );
177
- }
178
- }
179
- }
180
- }
181
- var channelConfig = /* @__PURE__ */ new Map();
182
- function configureChannel(name, options) {
183
- if (channels.has(name)) {
184
- const before = Object.keys(channelConfig.get(name)?.schema ?? {}).sort();
185
- const after = Object.keys(options.schema ?? {}).sort();
186
- if (before.join() === after.join()) return;
187
- if (process.env.NODE_ENV !== "production") {
188
- devWarn(
189
- "UE2004",
190
- `defineChannel('${name}') ran after that channel was already created, with different options. The live channel keeps the configuration it was built with. Move defineChannel to module scope, before any component sends or receives on it.`
191
- );
192
- }
193
- return;
194
- }
195
- channelConfig.set(name, options);
196
- }
197
- var reducers = /* @__PURE__ */ new Map();
198
- function getReducer(name, key, reducer, initial) {
199
- const id = `${name} ${key}`;
200
- let existing = reducers.get(id);
201
- if (!existing) {
202
- existing = isServer() ? createServerReducer(initial) : createSharedReducer(name, reducer, initial, { key, leader: getLeader(name) });
203
- reducers.set(id, existing);
204
- }
205
- return existing;
206
- }
207
- function getChannel(name) {
208
- let channel = channels.get(name);
209
- if (!channel) {
210
- channel = isServer() ? createServerChannel(name) : createChannel(name, channelConfig.get(name));
211
- channels.set(name, channel);
212
- }
213
- return channel;
214
- }
215
-
216
- // src/use-peers.ts
217
- import { useCallback, useEffect, useSyncExternalStore } from "react";
218
- var NO_PEERS2 = Object.freeze([]);
219
- function usePeers(options) {
220
- const presence = getPresence(options?.name ?? DEFAULT_NAME, options?.includeSelf ?? false);
221
- return useSyncExternalStore(
222
- useCallback((onChange) => presence.subscribe(onChange), [presence]),
223
- () => presence.getPeers(),
224
- () => NO_PEERS2
225
- );
226
- }
227
- function useClientId(options) {
228
- const presence = getPresence(options?.name ?? DEFAULT_NAME);
229
- return useSyncExternalStore(
230
- // The id never changes for the life of a presence engine, so there is
231
- // nothing to subscribe to — but uSES still needs the post-hydration read,
232
- // which it performs when it attaches this subscription.
233
- useCallback(() => () => {
234
- }, [presence]),
235
- () => presence.clientId,
236
- () => SERVER_CLIENT_ID
237
- );
238
- }
239
- function usePresenceMetadata(metadata, options) {
240
- const presence = getPresence(options?.name ?? DEFAULT_NAME, options?.includeSelf ?? false);
241
- useEffect(() => {
242
- presence.setMetadata(metadata);
243
- }, [presence, metadata]);
244
- }
245
-
246
- export {
247
- warnOnInitialMismatch,
248
- DEFAULT_NAME,
249
- getSharedStore,
250
- configureStore,
251
- getStore,
252
- getLeader,
253
- configureChannel,
254
- getReducer,
255
- getChannel,
256
- usePeers,
257
- useClientId,
258
- usePresenceMetadata
259
- };