use-everywhere 0.8.0 → 0.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -142,13 +142,13 @@ per-connection nonce, and source window.
142
142
  - Values must survive structured clone (no functions, DOM nodes); state lives
143
143
  as long as at least one context holds it — nothing is persisted.
144
144
  - Testing is first-class: inject a `MemoryHub` transport to simulate many tabs
145
- in one test. See the [testing guide](https://rxova.github.io/use-everywhere/guides/testing).
145
+ in one test. See the [testing guide](https://rxova.org/packages/use-everywhere/guides/testing).
146
146
 
147
147
  This package re-exports the full framework-agnostic surface of
148
148
  [`@use-everywhere/core`](https://www.npmjs.com/package/@use-everywhere/core),
149
149
  so you never need to install core directly.
150
150
 
151
- 📖 **[Documentation](https://rxova.github.io/use-everywhere/)** — mental
151
+ 📖 **[Documentation](https://rxova.org/packages/use-everywhere/)** — mental
152
152
  model, how sync works, security model, recipes, and generated API reference.
153
153
  Source and demo app: [github.com/rxova/use-everywhere](https://github.com/rxova/use-everywhere)
154
154
 
@@ -18,10 +18,17 @@ try {
18
18
  } catch {
19
19
  }
20
20
  var warned = /* @__PURE__ */ new Set();
21
- function devWarn(message) {
22
- if (!inDev || warned.has(message)) return;
23
- warned.add(message);
24
- console.warn(message);
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);
25
32
  }
26
33
  var seenInitials = /* @__PURE__ */ new Map();
27
34
  function warnOnInitialMismatch(storeName, key, initial) {
@@ -35,7 +42,8 @@ function warnOnInitialMismatch(storeName, key, initial) {
35
42
  const comparable = (v) => v === null || typeof v !== "object";
36
43
  if (comparable(first) && comparable(initial) && !Object.is(first, initial)) {
37
44
  devWarn(
38
- `[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.`
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 defineStore, or a shared constant.`
39
47
  );
40
48
  }
41
49
  }
@@ -118,7 +126,8 @@ function configureStore(name, scope, options) {
118
126
  if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
119
127
  if (process.env.NODE_ENV !== "production") {
120
128
  devWarn(
121
- `[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.`
129
+ "UE2002",
130
+ `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.`
122
131
  );
123
132
  }
124
133
  return;
@@ -162,7 +171,8 @@ function warnOnLeaderOptionConflict(name, options) {
162
171
  if (requested !== void 0 && requested !== first?.[key]) {
163
172
  if (process.env.NODE_ENV !== "production") {
164
173
  devWarn(
165
- `[use-everywhere] leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
174
+ "UE2003",
175
+ `leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
166
176
  );
167
177
  }
168
178
  }
@@ -176,7 +186,8 @@ function configureChannel(name, options) {
176
186
  if (before.join() === after.join()) return;
177
187
  if (process.env.NODE_ENV !== "production") {
178
188
  devWarn(
179
- `[use-everywhere] 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.`
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.`
180
191
  );
181
192
  }
182
193
  return;
@@ -26,6 +26,11 @@ __export(devtools_exports, {
26
26
  module.exports = __toCommonJS(devtools_exports);
27
27
 
28
28
  // src/devtools/inspector.tsx
29
+ var import_react3 = require("react");
30
+ var import_react_dom = require("react-dom");
31
+ var import_core3 = require("@use-everywhere/core");
32
+
33
+ // src/devtools/panel.tsx
29
34
  var import_react2 = require("react");
30
35
  var import_core2 = require("@use-everywhere/core");
31
36
 
@@ -113,85 +118,34 @@ function usePeers(options) {
113
118
  );
114
119
  }
115
120
 
116
- // src/devtools/styles.ts
117
- var STYLES = `
118
- .ue-ins {
119
- position: fixed;
120
- z-index: 2147483000;
121
- font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
122
- color: #e6edf3;
123
- background: #0d1117;
124
- border: 1px solid #30363d;
125
- border-radius: 8px;
126
- box-shadow: 0 8px 32px rgb(0 0 0 / 0.4);
127
- max-width: min(420px, calc(100vw - 24px));
128
- overflow: hidden;
129
- }
130
- .ue-ins--bottom-right { bottom: 12px; right: 12px; }
131
- .ue-ins--bottom-left { bottom: 12px; left: 12px; }
132
- .ue-ins--top-right { top: 12px; right: 12px; }
133
- .ue-ins--top-left { top: 12px; left: 12px; }
134
-
135
- .ue-ins__bar {
136
- display: flex;
137
- align-items: center;
138
- gap: 8px;
139
- width: 100%;
140
- padding: 7px 10px;
141
- background: #161b22;
142
- border: 0;
143
- color: inherit;
144
- font: inherit;
145
- cursor: pointer;
146
- text-align: left;
147
- }
148
- .ue-ins__dot { width: 7px; height: 7px; border-radius: 50%; background: #3fb950; flex: none; }
149
- .ue-ins__title { font-weight: 600; }
150
- .ue-ins__muted { color: #8b949e; }
151
- .ue-ins__crown { margin-left: auto; color: #d29922; }
152
-
153
- .ue-ins__body { max-height: 60vh; overflow-y: auto; }
154
- .ue-ins__section { border-top: 1px solid #21262d; padding: 8px 10px; }
155
- .ue-ins__h {
156
- color: #8b949e;
157
- text-transform: uppercase;
158
- letter-spacing: 0.06em;
159
- font-size: 10px;
160
- margin-bottom: 5px;
161
- }
162
- .ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
163
- .ue-ins__k { color: #79c0ff; flex: none; }
164
- .ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
165
- .ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
166
- .ue-ins__empty { color: #6e7681; }
167
-
168
- .ue-ins__log { display: flex; flex-direction: column-reverse; max-height: 190px; overflow-y: auto; }
169
- .ue-ins__wire { display: flex; gap: 7px; padding: 1px 0; white-space: nowrap; }
170
- .ue-ins__dir { flex: none; width: 9px; }
171
- .ue-ins__dir--out { color: #d29922; }
172
- .ue-ins__dir--in { color: #3fb950; }
173
- .ue-ins__scope { color: #e6edf3; }
174
- .ue-ins__from { color: #6e7681; margin-left: auto; }
175
- `;
176
-
177
- // src/devtools/inspector.tsx
121
+ // src/devtools/panel.tsx
178
122
  var import_jsx_runtime = require("react/jsx-runtime");
179
123
  var short = (id) => id.slice(0, 6);
180
124
  function wireLabel(wire) {
181
125
  return `${wire.scope}/${wire.type}`;
182
126
  }
183
- function Inspector({
184
- name = import_core2.DEFAULT_NAME,
185
- position = "bottom-right",
186
- limit = 50,
187
- defaultOpen = false,
188
- leaseMs = 3e3
189
- } = {}) {
127
+ var SCOPES = ["all", "state", "leader", "presence", "channel"];
128
+ function Panel({
129
+ name,
130
+ limit,
131
+ leaseMs,
132
+ position,
133
+ defaultOpen
134
+ }) {
190
135
  const [open, setOpen] = (0, import_react2.useState)(defaultOpen);
191
136
  const [wires, setWires] = (0, import_react2.useState)([]);
192
137
  const [crown, setCrown] = (0, import_react2.useState)(null);
138
+ const [paused, setPaused] = (0, import_react2.useState)(false);
139
+ const [filter, setFilter] = (0, import_react2.useState)("");
140
+ const [scopeView, setScopeView] = (0, import_react2.useState)("all");
141
+ const [editing, setEditing] = (0, import_react2.useState)(null);
142
+ const [frames, setFrames] = (0, import_react2.useState)([]);
193
143
  const nextId = (0, import_react2.useRef)(0);
194
144
  const crownAt = (0, import_react2.useRef)(0);
145
+ const pausedRef = (0, import_react2.useRef)(false);
146
+ (0, import_react2.useEffect)(() => {
147
+ pausedRef.current = paused;
148
+ }, [paused]);
195
149
  const peers = usePeers({ name });
196
150
  const store = getSharedStore(name);
197
151
  const subscribe = (0, import_react2.useCallback)((onChange) => store.subscribe(onChange), [store]);
@@ -217,19 +171,22 @@ function Inspector({
217
171
  crownAt.current = Date.now();
218
172
  }
219
173
  }
174
+ if (pausedRef.current) return;
175
+ const id = nextId.current++;
176
+ const label = wireLabel(wire);
220
177
  setWires(
221
178
  (prev) => [
222
179
  ...prev.slice(-(limit - 1)),
223
- {
224
- id: nextId.current++,
225
- direction,
226
- label: wireLabel(wire),
227
- from: short(wire.clientId)
228
- }
180
+ { id, direction, scope: wire.scope, label, from: short(wire.clientId) }
229
181
  ].slice(-limit)
230
182
  );
183
+ if (wire.scope === "state") {
184
+ setFrames(
185
+ (prev) => [...prev, { wireId: id, label, snapshot: { ...store.getSnapshot() } }].slice(-limit)
186
+ );
187
+ }
231
188
  });
232
- }, [name, limit]);
189
+ }, [name, limit, store]);
233
190
  (0, import_react2.useEffect)(() => {
234
191
  const timer = setInterval(
235
192
  () => {
@@ -238,17 +195,43 @@ function Inspector({
238
195
  crownAt.current = 0;
239
196
  }
240
197
  },
241
- // Quarter of the lease, so a vacated crown clears well within it. The
242
- // floor is low enough that a short lease still works rather than being
243
- // silently rounded up to something coarser than the lease itself.
244
198
  Math.max(50, Math.floor(leaseMs / 4))
245
199
  );
246
200
  return () => clearInterval(timer);
247
201
  }, [leaseMs]);
248
202
  const selfId = store.clientId;
249
203
  const entries = Object.entries(versions);
204
+ const needle = filter.trim().toLowerCase();
205
+ const shown = wires.filter((wire) => {
206
+ if (scopeView !== "all" && wire.scope !== scopeView) return false;
207
+ if (!needle) return true;
208
+ return wire.label.toLowerCase().includes(needle) || wire.from.toLowerCase().includes(needle);
209
+ });
210
+ const commit = (key, draft) => {
211
+ let value;
212
+ try {
213
+ value = JSON.parse(draft);
214
+ } catch {
215
+ return;
216
+ }
217
+ store.set(key, value);
218
+ setEditing(null);
219
+ };
220
+ const draftIsValid = (draft) => {
221
+ try {
222
+ JSON.parse(draft);
223
+ return true;
224
+ } catch {
225
+ return false;
226
+ }
227
+ };
228
+ const restore = (frame) => {
229
+ for (const [key, value] of Object.entries(frame.snapshot)) {
230
+ if (Object.is(store.getSnapshot()[key], value)) continue;
231
+ store.set(key, value);
232
+ }
233
+ };
250
234
  return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
251
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: STYLES }),
252
235
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
253
236
  "button",
254
237
  {
@@ -294,7 +277,31 @@ function Inspector({
294
277
  ] }),
295
278
  entries.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__empty", children: "no keys yet" }) : entries.map(([key, version]) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__row", children: [
296
279
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__k", children: key }),
297
- /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__v", children: JSON.stringify(snapshot[key]) }),
280
+ editing?.key === key ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
281
+ "input",
282
+ {
283
+ className: `ue-ins__edit${draftIsValid(editing.draft) ? "" : " ue-ins__v--invalid"}`,
284
+ value: editing.draft,
285
+ autoFocus: true,
286
+ "aria-label": `Value of ${key}`,
287
+ onChange: (event) => setEditing({ key, draft: event.target.value }),
288
+ onKeyDown: (event) => {
289
+ if (event.key === "Enter") commit(key, editing.draft);
290
+ if (event.key === "Escape") setEditing(null);
291
+ },
292
+ onBlur: () => setEditing(null),
293
+ "data-testid": `ue-edit-${key}`
294
+ }
295
+ ) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
296
+ "button",
297
+ {
298
+ type: "button",
299
+ className: "ue-ins__v ue-ins__v--editable",
300
+ onClick: () => setEditing({ key, draft: JSON.stringify(snapshot[key]) ?? "" }),
301
+ "data-testid": `ue-value-${key}`,
302
+ children: JSON.stringify(snapshot[key])
303
+ }
304
+ ),
298
305
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ue-ins__ver", children: [
299
306
  version[0],
300
307
  "\xB7",
@@ -305,18 +312,231 @@ function Inspector({
305
312
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
306
313
  /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__h", children: [
307
314
  "Wires (",
308
- wires.length,
309
- ")"
315
+ shown.length,
316
+ shown.length === wires.length ? "" : ` of ${wires.length}`,
317
+ ")",
318
+ paused ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__paused", children: " \xB7 paused" }) : null
319
+ ] }),
320
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__tools", children: [
321
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
322
+ "button",
323
+ {
324
+ type: "button",
325
+ className: "ue-ins__btn",
326
+ "aria-pressed": paused,
327
+ onClick: () => setPaused((value) => !value),
328
+ "data-testid": "ue-pause",
329
+ children: paused ? "resume" : "pause"
330
+ }
331
+ ),
332
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
333
+ "button",
334
+ {
335
+ type: "button",
336
+ className: "ue-ins__btn",
337
+ onClick: () => {
338
+ setWires([]);
339
+ setFrames([]);
340
+ },
341
+ "data-testid": "ue-clear",
342
+ children: "clear"
343
+ }
344
+ ),
345
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
346
+ "input",
347
+ {
348
+ className: "ue-ins__filter",
349
+ value: filter,
350
+ placeholder: "filter",
351
+ "aria-label": "Filter wires",
352
+ onChange: (event) => setFilter(event.target.value),
353
+ "data-testid": "ue-filter"
354
+ }
355
+ )
310
356
  ] }),
311
- wires.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__empty", children: "nothing yet" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__log", children: wires.map((wire) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__wire", children: [
357
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__tools", role: "group", "aria-label": "Scope", children: SCOPES.map((scope) => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
358
+ "button",
359
+ {
360
+ type: "button",
361
+ className: "ue-ins__btn",
362
+ "aria-pressed": scopeView === scope,
363
+ onClick: () => setScopeView(scope),
364
+ "data-testid": `ue-scope-${scope}`,
365
+ children: scope
366
+ },
367
+ scope
368
+ )) }),
369
+ shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__empty", "data-testid": "ue-wires", children: wires.length === 0 ? "nothing yet" : "no matches" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__log", "data-testid": "ue-wires", children: shown.map((wire) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__wire", children: [
312
370
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
313
371
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__scope", children: wire.label }),
314
372
  /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__from", children: wire.from })
315
373
  ] }, wire.id)) })
374
+ ] }),
375
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
376
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__h", children: [
377
+ "Timeline (",
378
+ frames.length,
379
+ ")"
380
+ ] }),
381
+ frames.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__empty", children: "no state on the wire yet" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__log", children: frames.map((frame) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__wire", children: [
382
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
383
+ "button",
384
+ {
385
+ type: "button",
386
+ className: "ue-ins__btn",
387
+ onClick: () => restore(frame),
388
+ "data-testid": `ue-restore-${String(frame.wireId)}`,
389
+ children: "restore"
390
+ }
391
+ ),
392
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__scope", children: frame.label }),
393
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ue-ins__from", children: [
394
+ Object.keys(frame.snapshot).length,
395
+ " key",
396
+ Object.keys(frame.snapshot).length === 1 ? "" : "s"
397
+ ] })
398
+ ] }, frame.wireId)) })
316
399
  ] })
317
400
  ] }) : null
318
401
  ] });
319
402
  }
403
+
404
+ // src/devtools/styles.ts
405
+ var STYLES = `
406
+ .ue-ins {
407
+ position: fixed;
408
+ z-index: 2147483000;
409
+ font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
410
+ color: #e6edf3;
411
+ background: #0d1117;
412
+ border: 1px solid #30363d;
413
+ border-radius: 8px;
414
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.4);
415
+ max-width: min(420px, calc(100vw - 24px));
416
+ overflow: hidden;
417
+ }
418
+ .ue-ins--bottom-right { bottom: 12px; right: 12px; }
419
+ .ue-ins--bottom-left { bottom: 12px; left: 12px; }
420
+ .ue-ins--top-right { top: 12px; right: 12px; }
421
+ .ue-ins--top-left { top: 12px; left: 12px; }
422
+
423
+ .ue-ins__bar {
424
+ display: flex;
425
+ align-items: center;
426
+ gap: 8px;
427
+ width: 100%;
428
+ padding: 7px 10px;
429
+ background: #161b22;
430
+ border: 0;
431
+ color: inherit;
432
+ font: inherit;
433
+ cursor: pointer;
434
+ text-align: left;
435
+ }
436
+ .ue-ins__dot { width: 7px; height: 7px; border-radius: 50%; background: #3fb950; flex: none; }
437
+ .ue-ins__title { font-weight: 600; }
438
+ .ue-ins__muted { color: #8b949e; }
439
+ .ue-ins__crown { margin-left: auto; color: #d29922; }
440
+
441
+ .ue-ins__body { max-height: 60vh; overflow-y: auto; }
442
+ .ue-ins__section { border-top: 1px solid #21262d; padding: 8px 10px; }
443
+ .ue-ins__h {
444
+ color: #8b949e;
445
+ text-transform: uppercase;
446
+ letter-spacing: 0.06em;
447
+ font-size: 10px;
448
+ margin-bottom: 5px;
449
+ }
450
+ .ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
451
+ .ue-ins__k { color: #79c0ff; flex: none; }
452
+ .ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
453
+ button.ue-ins__v {
454
+ background: none;
455
+ border: 0;
456
+ padding: 0;
457
+ font: inherit;
458
+ text-align: left;
459
+ }
460
+ .ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
461
+ .ue-ins__empty { color: #6e7681; }
462
+
463
+ .ue-ins__log { display: flex; flex-direction: column-reverse; max-height: 190px; overflow-y: auto; }
464
+ .ue-ins__wire { display: flex; gap: 7px; padding: 1px 0; white-space: nowrap; }
465
+ .ue-ins__dir { flex: none; width: 9px; }
466
+ .ue-ins__dir--out { color: #d29922; }
467
+ .ue-ins__dir--in { color: #3fb950; }
468
+ .ue-ins__scope { color: #e6edf3; }
469
+ .ue-ins__from { color: #6e7681; margin-left: auto; }
470
+
471
+ .ue-ins__tools { display: flex; gap: 6px; align-items: center; margin-bottom: 5px; }
472
+ .ue-ins__btn {
473
+ background: #21262d;
474
+ border: 1px solid #30363d;
475
+ border-radius: 5px;
476
+ color: #c9d1d9;
477
+ font: inherit;
478
+ font-size: 10px;
479
+ padding: 1px 7px;
480
+ cursor: pointer;
481
+ }
482
+ .ue-ins__btn[aria-pressed='true'] { background: #1f6feb; border-color: #1f6feb; color: #fff; }
483
+ .ue-ins__filter {
484
+ flex: 1;
485
+ min-width: 0;
486
+ background: #0d1117;
487
+ border: 1px solid #30363d;
488
+ border-radius: 5px;
489
+ color: #c9d1d9;
490
+ font: inherit;
491
+ font-size: 10px;
492
+ padding: 1px 6px;
493
+ }
494
+ .ue-ins__edit {
495
+ flex: 1;
496
+ min-width: 0;
497
+ background: #0d1117;
498
+ border: 1px solid #1f6feb;
499
+ border-radius: 4px;
500
+ color: #e6edf3;
501
+ font: inherit;
502
+ padding: 0 4px;
503
+ }
504
+ .ue-ins__v--editable { cursor: text; text-decoration: underline dotted #30363d; }
505
+ .ue-ins__v--invalid { color: #f85149; }
506
+ .ue-ins__paused { color: #d29922; }
507
+ `;
508
+
509
+ // src/devtools/inspector.tsx
510
+ var import_jsx_runtime2 = require("react/jsx-runtime");
511
+ function Inspector({
512
+ name = import_core3.DEFAULT_NAME,
513
+ position = "bottom-right",
514
+ limit = 50,
515
+ defaultOpen = false,
516
+ leaseMs = 3e3
517
+ } = {}) {
518
+ const [root, setRoot] = (0, import_react3.useState)(null);
519
+ const attach = (host) => {
520
+ if (!host || root) return;
521
+ setRoot(host.shadowRoot ?? host.attachShadow({ mode: "open" }));
522
+ };
523
+ return /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("div", { ref: attach, "data-testid": "ue-inspector-host", children: root ? (0, import_react_dom.createPortal)(
524
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsxs)(import_jsx_runtime2.Fragment, { children: [
525
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)("style", { children: STYLES }),
526
+ /* @__PURE__ */ (0, import_jsx_runtime2.jsx)(
527
+ Panel,
528
+ {
529
+ name,
530
+ position,
531
+ limit,
532
+ defaultOpen,
533
+ leaseMs
534
+ }
535
+ )
536
+ ] }),
537
+ root
538
+ ) : null });
539
+ }
320
540
  // Annotate the CommonJS export names for ESM import in node:
321
541
  0 && (module.exports = {
322
542
  Inspector
@@ -18,8 +18,8 @@ interface InspectorProps {
18
18
 
19
19
  /**
20
20
  * A floating panel showing what this tab is saying and hearing on the bus:
21
- * peers, the leader, store keys with their version clocks, and a live wire log
22
- * in both directions.
21
+ * peers, the leader, store keys with their version clocks, a live wire log in
22
+ * both directions, and a timeline of the state each wire produced.
23
23
  *
24
24
  * It deliberately does **not** create a Leader. Under dynamic eligibility,
25
25
  * mounting one with `eligible: false` would disable candidacy for the whole
@@ -29,6 +29,17 @@ interface InspectorProps {
29
29
  *
30
30
  * Presence is fine to use: the bus heartbeats regardless of whether anything
31
31
  * created a Presence, so usePeers observes rather than perturbs.
32
+ *
33
+ * **The panel lives in a shadow root.** Its styles used to be a `<style>` tag
34
+ * in the page, which is only ever half true: `.ue-ins` could not leak out, but
35
+ * everything in the host document leaked *in* — a `* { box-sizing }` reset, an
36
+ * app-wide `button { text-transform: uppercase }`, a Tailwind preflight, any
37
+ * `!important`. A devtool that renders differently depending on whose app it is
38
+ * mounted in is a devtool you cannot trust when it looks wrong. Inside a shadow
39
+ * root, page CSS does not apply and the panel's own CSS cannot escape.
40
+ *
41
+ * A consequence worth knowing when testing an app that mounts it: the panel is
42
+ * **not** in `document`. Reach it through `host.shadowRoot`.
32
43
  */
33
44
  declare function Inspector({ name, position, limit, defaultOpen, leaseMs, }?: InspectorProps): react.JSX.Element;
34
45
 
@@ -18,8 +18,8 @@ interface InspectorProps {
18
18
 
19
19
  /**
20
20
  * A floating panel showing what this tab is saying and hearing on the bus:
21
- * peers, the leader, store keys with their version clocks, and a live wire log
22
- * in both directions.
21
+ * peers, the leader, store keys with their version clocks, a live wire log in
22
+ * both directions, and a timeline of the state each wire produced.
23
23
  *
24
24
  * It deliberately does **not** create a Leader. Under dynamic eligibility,
25
25
  * mounting one with `eligible: false` would disable candidacy for the whole
@@ -29,6 +29,17 @@ interface InspectorProps {
29
29
  *
30
30
  * Presence is fine to use: the bus heartbeats regardless of whether anything
31
31
  * created a Presence, so usePeers observes rather than perturbs.
32
+ *
33
+ * **The panel lives in a shadow root.** Its styles used to be a `<style>` tag
34
+ * in the page, which is only ever half true: `.ue-ins` could not leak out, but
35
+ * everything in the host document leaked *in* — a `* { box-sizing }` reset, an
36
+ * app-wide `button { text-transform: uppercase }`, a Tailwind preflight, any
37
+ * `!important`. A devtool that renders differently depending on whose app it is
38
+ * mounted in is a devtool you cannot trust when it looks wrong. Inside a shadow
39
+ * root, page CSS does not apply and the panel's own CSS cannot escape.
40
+ *
41
+ * A consequence worth knowing when testing an app that mounts it: the panel is
42
+ * **not** in `document`. Reach it through `host.shadowRoot`.
32
43
  */
33
44
  declare function Inspector({ name, position, limit, defaultOpen, leaseMs, }?: InspectorProps): react.JSX.Element;
34
45
 
@@ -2,91 +2,43 @@
2
2
  import {
3
3
  getSharedStore,
4
4
  usePeers
5
- } from "../chunk-75TICQ7E.js";
5
+ } from "../chunk-R7PBPLF7.js";
6
6
 
7
7
  // src/devtools/inspector.tsx
8
- import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
9
- import { DEFAULT_NAME, observeBus } from "@use-everywhere/core";
10
-
11
- // src/devtools/styles.ts
12
- var STYLES = `
13
- .ue-ins {
14
- position: fixed;
15
- z-index: 2147483000;
16
- font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
17
- color: #e6edf3;
18
- background: #0d1117;
19
- border: 1px solid #30363d;
20
- border-radius: 8px;
21
- box-shadow: 0 8px 32px rgb(0 0 0 / 0.4);
22
- max-width: min(420px, calc(100vw - 24px));
23
- overflow: hidden;
24
- }
25
- .ue-ins--bottom-right { bottom: 12px; right: 12px; }
26
- .ue-ins--bottom-left { bottom: 12px; left: 12px; }
27
- .ue-ins--top-right { top: 12px; right: 12px; }
28
- .ue-ins--top-left { top: 12px; left: 12px; }
29
-
30
- .ue-ins__bar {
31
- display: flex;
32
- align-items: center;
33
- gap: 8px;
34
- width: 100%;
35
- padding: 7px 10px;
36
- background: #161b22;
37
- border: 0;
38
- color: inherit;
39
- font: inherit;
40
- cursor: pointer;
41
- text-align: left;
42
- }
43
- .ue-ins__dot { width: 7px; height: 7px; border-radius: 50%; background: #3fb950; flex: none; }
44
- .ue-ins__title { font-weight: 600; }
45
- .ue-ins__muted { color: #8b949e; }
46
- .ue-ins__crown { margin-left: auto; color: #d29922; }
8
+ import { useState as useState2 } from "react";
9
+ import { createPortal } from "react-dom";
10
+ import { DEFAULT_NAME } from "@use-everywhere/core";
47
11
 
48
- .ue-ins__body { max-height: 60vh; overflow-y: auto; }
49
- .ue-ins__section { border-top: 1px solid #21262d; padding: 8px 10px; }
50
- .ue-ins__h {
51
- color: #8b949e;
52
- text-transform: uppercase;
53
- letter-spacing: 0.06em;
54
- font-size: 10px;
55
- margin-bottom: 5px;
56
- }
57
- .ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
58
- .ue-ins__k { color: #79c0ff; flex: none; }
59
- .ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
60
- .ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
61
- .ue-ins__empty { color: #6e7681; }
62
-
63
- .ue-ins__log { display: flex; flex-direction: column-reverse; max-height: 190px; overflow-y: auto; }
64
- .ue-ins__wire { display: flex; gap: 7px; padding: 1px 0; white-space: nowrap; }
65
- .ue-ins__dir { flex: none; width: 9px; }
66
- .ue-ins__dir--out { color: #d29922; }
67
- .ue-ins__dir--in { color: #3fb950; }
68
- .ue-ins__scope { color: #e6edf3; }
69
- .ue-ins__from { color: #6e7681; margin-left: auto; }
70
- `;
71
-
72
- // src/devtools/inspector.tsx
12
+ // src/devtools/panel.tsx
13
+ import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
14
+ import { observeBus } from "@use-everywhere/core";
73
15
  import { jsx, jsxs } from "react/jsx-runtime";
74
16
  var short = (id) => id.slice(0, 6);
75
17
  function wireLabel(wire) {
76
18
  return `${wire.scope}/${wire.type}`;
77
19
  }
78
- function Inspector({
79
- name = DEFAULT_NAME,
80
- position = "bottom-right",
81
- limit = 50,
82
- defaultOpen = false,
83
- leaseMs = 3e3
84
- } = {}) {
20
+ var SCOPES = ["all", "state", "leader", "presence", "channel"];
21
+ function Panel({
22
+ name,
23
+ limit,
24
+ leaseMs,
25
+ position,
26
+ defaultOpen
27
+ }) {
85
28
  const [open, setOpen] = useState(defaultOpen);
86
29
  const [wires, setWires] = useState([]);
87
30
  const [crown, setCrown] = useState(null);
31
+ const [paused, setPaused] = useState(false);
32
+ const [filter, setFilter] = useState("");
33
+ const [scopeView, setScopeView] = useState("all");
34
+ const [editing, setEditing] = useState(null);
35
+ const [frames, setFrames] = useState([]);
88
36
  const nextId = useRef(0);
89
37
  const crownAt = useRef(0);
38
+ const pausedRef = useRef(false);
39
+ useEffect(() => {
40
+ pausedRef.current = paused;
41
+ }, [paused]);
90
42
  const peers = usePeers({ name });
91
43
  const store = getSharedStore(name);
92
44
  const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
@@ -112,19 +64,22 @@ function Inspector({
112
64
  crownAt.current = Date.now();
113
65
  }
114
66
  }
67
+ if (pausedRef.current) return;
68
+ const id = nextId.current++;
69
+ const label = wireLabel(wire);
115
70
  setWires(
116
71
  (prev) => [
117
72
  ...prev.slice(-(limit - 1)),
118
- {
119
- id: nextId.current++,
120
- direction,
121
- label: wireLabel(wire),
122
- from: short(wire.clientId)
123
- }
73
+ { id, direction, scope: wire.scope, label, from: short(wire.clientId) }
124
74
  ].slice(-limit)
125
75
  );
76
+ if (wire.scope === "state") {
77
+ setFrames(
78
+ (prev) => [...prev, { wireId: id, label, snapshot: { ...store.getSnapshot() } }].slice(-limit)
79
+ );
80
+ }
126
81
  });
127
- }, [name, limit]);
82
+ }, [name, limit, store]);
128
83
  useEffect(() => {
129
84
  const timer = setInterval(
130
85
  () => {
@@ -133,17 +88,43 @@ function Inspector({
133
88
  crownAt.current = 0;
134
89
  }
135
90
  },
136
- // Quarter of the lease, so a vacated crown clears well within it. The
137
- // floor is low enough that a short lease still works rather than being
138
- // silently rounded up to something coarser than the lease itself.
139
91
  Math.max(50, Math.floor(leaseMs / 4))
140
92
  );
141
93
  return () => clearInterval(timer);
142
94
  }, [leaseMs]);
143
95
  const selfId = store.clientId;
144
96
  const entries = Object.entries(versions);
97
+ const needle = filter.trim().toLowerCase();
98
+ const shown = wires.filter((wire) => {
99
+ if (scopeView !== "all" && wire.scope !== scopeView) return false;
100
+ if (!needle) return true;
101
+ return wire.label.toLowerCase().includes(needle) || wire.from.toLowerCase().includes(needle);
102
+ });
103
+ const commit = (key, draft) => {
104
+ let value;
105
+ try {
106
+ value = JSON.parse(draft);
107
+ } catch {
108
+ return;
109
+ }
110
+ store.set(key, value);
111
+ setEditing(null);
112
+ };
113
+ const draftIsValid = (draft) => {
114
+ try {
115
+ JSON.parse(draft);
116
+ return true;
117
+ } catch {
118
+ return false;
119
+ }
120
+ };
121
+ const restore = (frame) => {
122
+ for (const [key, value] of Object.entries(frame.snapshot)) {
123
+ if (Object.is(store.getSnapshot()[key], value)) continue;
124
+ store.set(key, value);
125
+ }
126
+ };
145
127
  return /* @__PURE__ */ jsxs("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
146
- /* @__PURE__ */ jsx("style", { children: STYLES }),
147
128
  /* @__PURE__ */ jsxs(
148
129
  "button",
149
130
  {
@@ -189,7 +170,31 @@ function Inspector({
189
170
  ] }),
190
171
  entries.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "no keys yet" }) : entries.map(([key, version]) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
191
172
  /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: key }),
192
- /* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: JSON.stringify(snapshot[key]) }),
173
+ editing?.key === key ? /* @__PURE__ */ jsx(
174
+ "input",
175
+ {
176
+ className: `ue-ins__edit${draftIsValid(editing.draft) ? "" : " ue-ins__v--invalid"}`,
177
+ value: editing.draft,
178
+ autoFocus: true,
179
+ "aria-label": `Value of ${key}`,
180
+ onChange: (event) => setEditing({ key, draft: event.target.value }),
181
+ onKeyDown: (event) => {
182
+ if (event.key === "Enter") commit(key, editing.draft);
183
+ if (event.key === "Escape") setEditing(null);
184
+ },
185
+ onBlur: () => setEditing(null),
186
+ "data-testid": `ue-edit-${key}`
187
+ }
188
+ ) : /* @__PURE__ */ jsx(
189
+ "button",
190
+ {
191
+ type: "button",
192
+ className: "ue-ins__v ue-ins__v--editable",
193
+ onClick: () => setEditing({ key, draft: JSON.stringify(snapshot[key]) ?? "" }),
194
+ "data-testid": `ue-value-${key}`,
195
+ children: JSON.stringify(snapshot[key])
196
+ }
197
+ ),
193
198
  /* @__PURE__ */ jsxs("span", { className: "ue-ins__ver", children: [
194
199
  version[0],
195
200
  "\xB7",
@@ -200,18 +205,231 @@ function Inspector({
200
205
  /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
201
206
  /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
202
207
  "Wires (",
203
- wires.length,
204
- ")"
208
+ shown.length,
209
+ shown.length === wires.length ? "" : ` of ${wires.length}`,
210
+ ")",
211
+ paused ? /* @__PURE__ */ jsx("span", { className: "ue-ins__paused", children: " \xB7 paused" }) : null
205
212
  ] }),
206
- wires.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "nothing yet" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", children: wires.map((wire) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
213
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__tools", children: [
214
+ /* @__PURE__ */ jsx(
215
+ "button",
216
+ {
217
+ type: "button",
218
+ className: "ue-ins__btn",
219
+ "aria-pressed": paused,
220
+ onClick: () => setPaused((value) => !value),
221
+ "data-testid": "ue-pause",
222
+ children: paused ? "resume" : "pause"
223
+ }
224
+ ),
225
+ /* @__PURE__ */ jsx(
226
+ "button",
227
+ {
228
+ type: "button",
229
+ className: "ue-ins__btn",
230
+ onClick: () => {
231
+ setWires([]);
232
+ setFrames([]);
233
+ },
234
+ "data-testid": "ue-clear",
235
+ children: "clear"
236
+ }
237
+ ),
238
+ /* @__PURE__ */ jsx(
239
+ "input",
240
+ {
241
+ className: "ue-ins__filter",
242
+ value: filter,
243
+ placeholder: "filter",
244
+ "aria-label": "Filter wires",
245
+ onChange: (event) => setFilter(event.target.value),
246
+ "data-testid": "ue-filter"
247
+ }
248
+ )
249
+ ] }),
250
+ /* @__PURE__ */ jsx("div", { className: "ue-ins__tools", role: "group", "aria-label": "Scope", children: SCOPES.map((scope) => /* @__PURE__ */ jsx(
251
+ "button",
252
+ {
253
+ type: "button",
254
+ className: "ue-ins__btn",
255
+ "aria-pressed": scopeView === scope,
256
+ onClick: () => setScopeView(scope),
257
+ "data-testid": `ue-scope-${scope}`,
258
+ children: scope
259
+ },
260
+ scope
261
+ )) }),
262
+ shown.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", "data-testid": "ue-wires", children: wires.length === 0 ? "nothing yet" : "no matches" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", "data-testid": "ue-wires", children: shown.map((wire) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
207
263
  /* @__PURE__ */ jsx("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
208
264
  /* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: wire.label }),
209
265
  /* @__PURE__ */ jsx("span", { className: "ue-ins__from", children: wire.from })
210
266
  ] }, wire.id)) })
267
+ ] }),
268
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
269
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
270
+ "Timeline (",
271
+ frames.length,
272
+ ")"
273
+ ] }),
274
+ frames.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "no state on the wire yet" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", children: frames.map((frame) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
275
+ /* @__PURE__ */ jsx(
276
+ "button",
277
+ {
278
+ type: "button",
279
+ className: "ue-ins__btn",
280
+ onClick: () => restore(frame),
281
+ "data-testid": `ue-restore-${String(frame.wireId)}`,
282
+ children: "restore"
283
+ }
284
+ ),
285
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: frame.label }),
286
+ /* @__PURE__ */ jsxs("span", { className: "ue-ins__from", children: [
287
+ Object.keys(frame.snapshot).length,
288
+ " key",
289
+ Object.keys(frame.snapshot).length === 1 ? "" : "s"
290
+ ] })
291
+ ] }, frame.wireId)) })
211
292
  ] })
212
293
  ] }) : null
213
294
  ] });
214
295
  }
296
+
297
+ // src/devtools/styles.ts
298
+ var STYLES = `
299
+ .ue-ins {
300
+ position: fixed;
301
+ z-index: 2147483000;
302
+ font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
303
+ color: #e6edf3;
304
+ background: #0d1117;
305
+ border: 1px solid #30363d;
306
+ border-radius: 8px;
307
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.4);
308
+ max-width: min(420px, calc(100vw - 24px));
309
+ overflow: hidden;
310
+ }
311
+ .ue-ins--bottom-right { bottom: 12px; right: 12px; }
312
+ .ue-ins--bottom-left { bottom: 12px; left: 12px; }
313
+ .ue-ins--top-right { top: 12px; right: 12px; }
314
+ .ue-ins--top-left { top: 12px; left: 12px; }
315
+
316
+ .ue-ins__bar {
317
+ display: flex;
318
+ align-items: center;
319
+ gap: 8px;
320
+ width: 100%;
321
+ padding: 7px 10px;
322
+ background: #161b22;
323
+ border: 0;
324
+ color: inherit;
325
+ font: inherit;
326
+ cursor: pointer;
327
+ text-align: left;
328
+ }
329
+ .ue-ins__dot { width: 7px; height: 7px; border-radius: 50%; background: #3fb950; flex: none; }
330
+ .ue-ins__title { font-weight: 600; }
331
+ .ue-ins__muted { color: #8b949e; }
332
+ .ue-ins__crown { margin-left: auto; color: #d29922; }
333
+
334
+ .ue-ins__body { max-height: 60vh; overflow-y: auto; }
335
+ .ue-ins__section { border-top: 1px solid #21262d; padding: 8px 10px; }
336
+ .ue-ins__h {
337
+ color: #8b949e;
338
+ text-transform: uppercase;
339
+ letter-spacing: 0.06em;
340
+ font-size: 10px;
341
+ margin-bottom: 5px;
342
+ }
343
+ .ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
344
+ .ue-ins__k { color: #79c0ff; flex: none; }
345
+ .ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
346
+ button.ue-ins__v {
347
+ background: none;
348
+ border: 0;
349
+ padding: 0;
350
+ font: inherit;
351
+ text-align: left;
352
+ }
353
+ .ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
354
+ .ue-ins__empty { color: #6e7681; }
355
+
356
+ .ue-ins__log { display: flex; flex-direction: column-reverse; max-height: 190px; overflow-y: auto; }
357
+ .ue-ins__wire { display: flex; gap: 7px; padding: 1px 0; white-space: nowrap; }
358
+ .ue-ins__dir { flex: none; width: 9px; }
359
+ .ue-ins__dir--out { color: #d29922; }
360
+ .ue-ins__dir--in { color: #3fb950; }
361
+ .ue-ins__scope { color: #e6edf3; }
362
+ .ue-ins__from { color: #6e7681; margin-left: auto; }
363
+
364
+ .ue-ins__tools { display: flex; gap: 6px; align-items: center; margin-bottom: 5px; }
365
+ .ue-ins__btn {
366
+ background: #21262d;
367
+ border: 1px solid #30363d;
368
+ border-radius: 5px;
369
+ color: #c9d1d9;
370
+ font: inherit;
371
+ font-size: 10px;
372
+ padding: 1px 7px;
373
+ cursor: pointer;
374
+ }
375
+ .ue-ins__btn[aria-pressed='true'] { background: #1f6feb; border-color: #1f6feb; color: #fff; }
376
+ .ue-ins__filter {
377
+ flex: 1;
378
+ min-width: 0;
379
+ background: #0d1117;
380
+ border: 1px solid #30363d;
381
+ border-radius: 5px;
382
+ color: #c9d1d9;
383
+ font: inherit;
384
+ font-size: 10px;
385
+ padding: 1px 6px;
386
+ }
387
+ .ue-ins__edit {
388
+ flex: 1;
389
+ min-width: 0;
390
+ background: #0d1117;
391
+ border: 1px solid #1f6feb;
392
+ border-radius: 4px;
393
+ color: #e6edf3;
394
+ font: inherit;
395
+ padding: 0 4px;
396
+ }
397
+ .ue-ins__v--editable { cursor: text; text-decoration: underline dotted #30363d; }
398
+ .ue-ins__v--invalid { color: #f85149; }
399
+ .ue-ins__paused { color: #d29922; }
400
+ `;
401
+
402
+ // src/devtools/inspector.tsx
403
+ import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
404
+ function Inspector({
405
+ name = DEFAULT_NAME,
406
+ position = "bottom-right",
407
+ limit = 50,
408
+ defaultOpen = false,
409
+ leaseMs = 3e3
410
+ } = {}) {
411
+ const [root, setRoot] = useState2(null);
412
+ const attach = (host) => {
413
+ if (!host || root) return;
414
+ setRoot(host.shadowRoot ?? host.attachShadow({ mode: "open" }));
415
+ };
416
+ return /* @__PURE__ */ jsx2("div", { ref: attach, "data-testid": "ue-inspector-host", children: root ? createPortal(
417
+ /* @__PURE__ */ jsxs2(Fragment, { children: [
418
+ /* @__PURE__ */ jsx2("style", { children: STYLES }),
419
+ /* @__PURE__ */ jsx2(
420
+ Panel,
421
+ {
422
+ name,
423
+ position,
424
+ limit,
425
+ defaultOpen,
426
+ leaseMs
427
+ }
428
+ )
429
+ ] }),
430
+ root
431
+ ) : null });
432
+ }
215
433
  export {
216
434
  Inspector
217
435
  };
package/dist/index.cjs CHANGED
@@ -26,6 +26,7 @@ __export(src_exports, {
26
26
  DEFAULT_NAME: () => import_core.DEFAULT_NAME,
27
27
  HandshakeTimeoutError: () => import_core4.HandshakeTimeoutError,
28
28
  NoopTransport: () => import_core4.NoopTransport,
29
+ SharedWorkerTransport: () => import_core4.SharedWorkerTransport,
29
30
  StorageTransport: () => import_core4.StorageTransport,
30
31
  WIRE_VERSION: () => import_core4.WIRE_VERSION,
31
32
  WindowClosedError: () => import_core4.WindowClosedError,
@@ -47,6 +48,7 @@ __export(src_exports, {
47
48
  getWireSkew: () => import_core4.getWireSkew,
48
49
  indexedDbAdapter: () => import_core4.indexedDbAdapter,
49
50
  isBroadcastChannelAvailable: () => import_core4.isBroadcastChannelAvailable,
51
+ isSharedWorkerAvailable: () => import_core4.isSharedWorkerAvailable,
50
52
  isStorageEventAvailable: () => import_core4.isStorageEventAvailable,
51
53
  jsonSerializer: () => import_core4.jsonSerializer,
52
54
  localStorageAdapter: () => import_core4.localStorageAdapter,
@@ -85,10 +87,17 @@ try {
85
87
  } catch {
86
88
  }
87
89
  var warned = /* @__PURE__ */ new Set();
88
- function devWarn(message) {
89
- if (!inDev || warned.has(message)) return;
90
- warned.add(message);
91
- console.warn(message);
90
+ var DOCS = "https://rxova.org/packages/use-everywhere/errors";
91
+ function diagnostic(code, message) {
92
+ return `[use-everywhere] ${code}: ${message}
93
+ \u2192 ${DOCS}/#${code.toLowerCase()}`;
94
+ }
95
+ function devWarn(code, message) {
96
+ if (!inDev) return;
97
+ const line = diagnostic(code, message);
98
+ if (warned.has(line)) return;
99
+ warned.add(line);
100
+ console.warn(line);
92
101
  }
93
102
  var seenInitials = /* @__PURE__ */ new Map();
94
103
  function warnOnInitialMismatch(storeName, key, initial) {
@@ -102,7 +111,8 @@ function warnOnInitialMismatch(storeName, key, initial) {
102
111
  const comparable = (v) => v === null || typeof v !== "object";
103
112
  if (comparable(first) && comparable(initial) && !Object.is(first, initial)) {
104
113
  devWarn(
105
- `[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.`
114
+ "UE2001",
115
+ `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.`
106
116
  );
107
117
  }
108
118
  }
@@ -188,7 +198,8 @@ function configureStore(name, scope, options) {
188
198
  if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
189
199
  if (process.env.NODE_ENV !== "production") {
190
200
  devWarn(
191
- `[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.`
201
+ "UE2002",
202
+ `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.`
192
203
  );
193
204
  }
194
205
  return;
@@ -232,7 +243,8 @@ function warnOnLeaderOptionConflict(name, options) {
232
243
  if (requested !== void 0 && requested !== first?.[key]) {
233
244
  if (process.env.NODE_ENV !== "production") {
234
245
  devWarn(
235
- `[use-everywhere] leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
246
+ "UE2003",
247
+ `leader "${name}": ${key} ignored \u2014 the first useLeader/getLeader call fixes the election timings for this tab.`
236
248
  );
237
249
  }
238
250
  }
@@ -246,7 +258,8 @@ function configureChannel(name, options) {
246
258
  if (before.join() === after.join()) return;
247
259
  if (process.env.NODE_ENV !== "production") {
248
260
  devWarn(
249
- `[use-everywhere] 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.`
261
+ "UE2004",
262
+ `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.`
250
263
  );
251
264
  }
252
265
  return;
@@ -580,6 +593,7 @@ var import_core4 = require("@use-everywhere/core");
580
593
  DEFAULT_NAME,
581
594
  HandshakeTimeoutError,
582
595
  NoopTransport,
596
+ SharedWorkerTransport,
583
597
  StorageTransport,
584
598
  WIRE_VERSION,
585
599
  WindowClosedError,
@@ -601,6 +615,7 @@ var import_core4 = require("@use-everywhere/core");
601
615
  getWireSkew,
602
616
  indexedDbAdapter,
603
617
  isBroadcastChannelAvailable,
618
+ isSharedWorkerAvailable,
604
619
  isStorageEventAvailable,
605
620
  jsonSerializer,
606
621
  localStorageAdapter,
package/dist/index.d.cts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SharedStore, LeaderOptions, Leader, MessageMap, ReplyMap, Channel, MessageMeta, ChannelOptions, PersistAdapter, RestoreError, Peer, LeaderSnapshot, Namespace, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
- export { AskOptions, BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, ChannelOptions, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, IndexedDbAdapterOptions, InvalidPayload, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, Namespace, NoopTransport, OnInvalid, OnOptions, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, PostOptions, Presence, PresenceOptions, ReplyMap, RestoreError, SchemaMap, SchemaOptions, Serializer, SharedReducer, SharedReducerOptions, SharedStore, SharedStoreOptions, StandardSchemaV1, StorageLike, StorageTransport, Transport, TransportKind, Version, WIRE_VERSION, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
2
+ export { AskOptions, BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, ChannelOptions, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, IndexedDbAdapterOptions, InvalidPayload, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, Namespace, NoopTransport, OnInvalid, OnOptions, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, PostOptions, Presence, PresenceOptions, ReplyMap, RestoreError, SchemaMap, SchemaOptions, Serializer, SharedReducer, SharedReducerOptions, SharedStore, SharedStoreOptions, SharedWorkerTransport, SharedWorkerTransportOptions, StandardSchemaV1, StorageLike, StorageTransport, Transport, TransportKind, Version, WIRE_VERSION, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isSharedWorkerAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
3
3
 
4
4
  /**
5
5
  * How far a shared value travels:
package/dist/index.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { SharedStore, LeaderOptions, Leader, MessageMap, ReplyMap, Channel, MessageMeta, ChannelOptions, PersistAdapter, RestoreError, Peer, LeaderSnapshot, Namespace, OpenedWindow, WindowClosedError } from '@use-everywhere/core';
2
- export { AskOptions, BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, ChannelOptions, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, IndexedDbAdapterOptions, InvalidPayload, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, Namespace, NoopTransport, OnInvalid, OnOptions, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, PostOptions, Presence, PresenceOptions, ReplyMap, RestoreError, SchemaMap, SchemaOptions, Serializer, SharedReducer, SharedReducerOptions, SharedStore, SharedStoreOptions, StandardSchemaV1, StorageLike, StorageTransport, Transport, TransportKind, Version, WIRE_VERSION, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
2
+ export { AskOptions, BroadcastChannelTransport, BusEvent, BusObserver, BusWire, CID_PARAM, Channel, ChannelOptions, CommonOptions, ConnectToOpenerOptions, DEFAULT_NAME, DebugOptions, HandshakeTimeoutError, IndexedDbAdapterOptions, InvalidPayload, Leader, LeaderOptions, LeaderSnapshot, LeaderStrategy, MessageEventLike, MessageMap, MessageMeta, Namespace, NoopTransport, OnInvalid, OnOptions, OpenWindowOptions, OpenedWindow, OpenerConnection, Peer, PeerKind, PersistAdapter, PersistOptions, Persisted, PostOptions, Presence, PresenceOptions, ReplyMap, RestoreError, SchemaMap, SchemaOptions, Serializer, SharedReducer, SharedReducerOptions, SharedStore, SharedStoreOptions, SharedWorkerTransport, SharedWorkerTransportOptions, StandardSchemaV1, StorageLike, StorageTransport, Transport, TransportKind, Version, WIRE_VERSION, WebStorageAdapterOptions, WindowClosedError, WindowEventTarget, WindowLike, connectToOpener, createChannel, createLeader, createPresence, createSharedReducer, createSharedStore, defaultTransport, enableDebug, getBusNames, getTransportKind, getWireSkew, indexedDbAdapter, isBroadcastChannelAvailable, isSharedWorkerAvailable, isStorageEventAvailable, jsonSerializer, localStorageAdapter, newer, observeBus, openWindow, sessionStorageAdapter, webStorageAdapter } from '@use-everywhere/core';
3
3
 
4
4
  /**
5
5
  * How far a shared value travels:
package/dist/index.js CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  usePeers,
13
13
  usePresenceMetadata,
14
14
  warnOnInitialMismatch
15
- } from "./chunk-75TICQ7E.js";
15
+ } from "./chunk-R7PBPLF7.js";
16
16
 
17
17
  // src/use-shared-state.ts
18
18
  import { useCallback, useSyncExternalStore } from "react";
@@ -313,9 +313,11 @@ import {
313
313
  BroadcastChannelTransport,
314
314
  NoopTransport,
315
315
  StorageTransport,
316
+ SharedWorkerTransport,
316
317
  defaultTransport,
317
318
  isBroadcastChannelAvailable,
318
- isStorageEventAvailable
319
+ isStorageEventAvailable,
320
+ isSharedWorkerAvailable
319
321
  } from "@use-everywhere/core";
320
322
  export {
321
323
  BroadcastChannelTransport,
@@ -323,6 +325,7 @@ export {
323
325
  DEFAULT_NAME,
324
326
  HandshakeTimeoutError,
325
327
  NoopTransport,
328
+ SharedWorkerTransport,
326
329
  StorageTransport,
327
330
  WIRE_VERSION,
328
331
  WindowClosedError2 as WindowClosedError,
@@ -344,6 +347,7 @@ export {
344
347
  getWireSkew,
345
348
  indexedDbAdapter,
346
349
  isBroadcastChannelAvailable,
350
+ isSharedWorkerAvailable,
347
351
  isStorageEventAvailable,
348
352
  jsonSerializer,
349
353
  localStorageAdapter,
@@ -0,0 +1,31 @@
1
+ 'use client';
2
+ "use strict";
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
20
+
21
+ // src/shared-worker.ts
22
+ var shared_worker_exports = {};
23
+ __export(shared_worker_exports, {
24
+ startRelay: () => import_shared_worker.startRelay
25
+ });
26
+ module.exports = __toCommonJS(shared_worker_exports);
27
+ var import_shared_worker = require("@use-everywhere/core/shared-worker");
28
+ // Annotate the CommonJS export names for ESM import in node:
29
+ 0 && (module.exports = {
30
+ startRelay
31
+ });
@@ -0,0 +1,2 @@
1
+ export { RelayPort, RelayScope, startRelay } from '@use-everywhere/core/shared-worker';
2
+ import '@use-everywhere/core/testing';
@@ -0,0 +1,2 @@
1
+ export { RelayPort, RelayScope, startRelay } from '@use-everywhere/core/shared-worker';
2
+ import '@use-everywhere/core/testing';
@@ -0,0 +1,7 @@
1
+ 'use client';
2
+
3
+ // src/shared-worker.ts
4
+ import { startRelay } from "@use-everywhere/core/shared-worker";
5
+ export {
6
+ startRelay
7
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "use-everywhere",
3
- "version": "0.8.0",
3
+ "version": "0.10.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>",
@@ -25,7 +25,10 @@
25
25
  "access": "public"
26
26
  },
27
27
  "type": "module",
28
- "sideEffects": false,
28
+ "sideEffects": [
29
+ "./dist/shared-worker.js",
30
+ "./dist/shared-worker.cjs"
31
+ ],
29
32
  "main": "./dist/index.cjs",
30
33
  "module": "./dist/index.js",
31
34
  "types": "./dist/index.d.ts",
@@ -50,6 +53,16 @@
50
53
  "default": "./dist/devtools/index.cjs"
51
54
  }
52
55
  },
56
+ "./shared-worker": {
57
+ "import": {
58
+ "types": "./dist/shared-worker.d.ts",
59
+ "default": "./dist/shared-worker.js"
60
+ },
61
+ "require": {
62
+ "types": "./dist/shared-worker.d.cts",
63
+ "default": "./dist/shared-worker.cjs"
64
+ }
65
+ },
53
66
  "./testing": {
54
67
  "import": {
55
68
  "types": "./dist/testing.d.ts",
@@ -123,7 +136,13 @@
123
136
  "name": "Inspector (devtools subpath)",
124
137
  "path": "dist/devtools/index.js",
125
138
  "import": "{ Inspector }",
126
- "limit": "6.32 kB"
139
+ "limit": "7.20 kB"
140
+ },
141
+ {
142
+ "name": "SharedWorker relay (shared-worker subpath)",
143
+ "path": "dist/shared-worker.js",
144
+ "import": "*",
145
+ "limit": "280 B"
127
146
  },
128
147
  {
129
148
  "name": "MemoryHub (testing subpath)",
@@ -133,10 +152,16 @@
133
152
  }
134
153
  ],
135
154
  "dependencies": {
136
- "@use-everywhere/core": "0.8.0"
155
+ "@use-everywhere/core": "0.10.0"
137
156
  },
138
157
  "peerDependencies": {
139
- "react": ">=18"
158
+ "react": ">=18",
159
+ "react-dom": ">=18"
160
+ },
161
+ "peerDependenciesMeta": {
162
+ "react-dom": {
163
+ "optional": true
164
+ }
140
165
  },
141
166
  "devDependencies": {
142
167
  "@arethetypeswrong/cli": "^0.18.5",
@@ -159,6 +184,9 @@
159
184
  "devtools": [
160
185
  "./dist/devtools/index.d.ts"
161
186
  ],
187
+ "shared-worker": [
188
+ "./dist/shared-worker.d.ts"
189
+ ],
162
190
  "testing": [
163
191
  "./dist/testing.d.ts"
164
192
  ]