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.
- package/dist/devtools/index.cjs +436 -436
- package/dist/devtools/index.d.cts +19 -18
- package/dist/devtools/index.d.ts +19 -18
- package/dist/devtools/index.js +434 -327
- package/dist/index.cjs +665 -621
- package/dist/index.d.cts +177 -160
- package/dist/index.d.ts +177 -160
- package/dist/index.js +449 -340
- package/dist/shared-worker.cjs +12 -30
- package/dist/shared-worker.d.cts +3 -2
- package/dist/shared-worker.d.ts +3 -2
- package/dist/shared-worker.js +2 -7
- package/dist/testing.cjs +12 -30
- package/dist/testing.d.cts +3 -1
- package/dist/testing.d.ts +3 -1
- package/dist/testing.js +1 -6
- package/dist/use-peers-DtpHGzXE.js +315 -0
- package/dist/use-peers-RhIB77TV.cjs +380 -0
- package/package.json +6 -5
- package/dist/chunk-YPQ6PLD5.js +0 -259
|
@@ -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.
|
|
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.
|
|
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
|
-
"
|
|
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": "
|
|
215
|
+
"build": "tsdown",
|
|
215
216
|
"test": "vitest run --coverage",
|
|
216
217
|
"typecheck": "tsc --noEmit",
|
|
217
218
|
"size": "size-limit",
|
package/dist/chunk-YPQ6PLD5.js
DELETED
|
@@ -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
|
-
};
|