use-everywhere 0.7.0 → 0.9.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 +2 -2
- package/dist/{chunk-LNJEF4DI.js → chunk-R7PBPLF7.js} +83 -18
- package/dist/devtools/index.cjs +153 -9
- package/dist/devtools/index.js +140 -5
- package/dist/index.cjs +275 -73
- package/dist/index.d.cts +242 -10
- package/dist/index.d.ts +242 -10
- package/dist/index.js +164 -26
- package/package.json +23 -11
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.
|
|
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.
|
|
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
|
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
createChannel,
|
|
6
6
|
createLeader,
|
|
7
7
|
createPresence,
|
|
8
|
+
createSharedReducer,
|
|
8
9
|
createSharedStore,
|
|
9
10
|
DEFAULT_NAME,
|
|
10
11
|
NoopTransport
|
|
@@ -17,10 +18,17 @@ try {
|
|
|
17
18
|
} catch {
|
|
18
19
|
}
|
|
19
20
|
var warned = /* @__PURE__ */ new Set();
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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);
|
|
24
32
|
}
|
|
25
33
|
var seenInitials = /* @__PURE__ */ new Map();
|
|
26
34
|
function warnOnInitialMismatch(storeName, key, initial) {
|
|
@@ -34,7 +42,8 @@ function warnOnInitialMismatch(storeName, key, initial) {
|
|
|
34
42
|
const comparable = (v) => v === null || typeof v !== "object";
|
|
35
43
|
if (comparable(first) && comparable(initial) && !Object.is(first, initial)) {
|
|
36
44
|
devWarn(
|
|
37
|
-
|
|
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.`
|
|
38
47
|
);
|
|
39
48
|
}
|
|
40
49
|
}
|
|
@@ -49,6 +58,12 @@ var unsubscribe = () => noop;
|
|
|
49
58
|
var INERT = Object.freeze({
|
|
50
59
|
clientId: SERVER_CLIENT_ID,
|
|
51
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(),
|
|
52
67
|
state: EMPTY,
|
|
53
68
|
getVersions: () => EMPTY,
|
|
54
69
|
set: noop,
|
|
@@ -57,6 +72,8 @@ var INERT = Object.freeze({
|
|
|
57
72
|
// store + leader share getSnapshot; presence has getPeers
|
|
58
73
|
getSnapshot: () => EMPTY,
|
|
59
74
|
getPeers: () => NO_PEERS,
|
|
75
|
+
// presence
|
|
76
|
+
setMetadata: noop,
|
|
60
77
|
// leader
|
|
61
78
|
resign: noop,
|
|
62
79
|
setEligible: noop,
|
|
@@ -75,6 +92,12 @@ var createServerLeader = () => ({
|
|
|
75
92
|
getSnapshot: () => NO_LEADER,
|
|
76
93
|
waitForLeadership: NEVER
|
|
77
94
|
});
|
|
95
|
+
var createServerReducer = (initial) => ({
|
|
96
|
+
...INERT,
|
|
97
|
+
getSnapshot: () => initial,
|
|
98
|
+
dispatch: noop,
|
|
99
|
+
pendingCount: () => 0
|
|
100
|
+
});
|
|
78
101
|
var createServerChannel = (name) => ({ ...INERT, name, post: noop, on: unsubscribe });
|
|
79
102
|
|
|
80
103
|
// src/registry.ts
|
|
@@ -95,15 +118,18 @@ function getSharedStore(name = DEFAULT_NAME, scope = "everywhere") {
|
|
|
95
118
|
function configSignature(options) {
|
|
96
119
|
const persist = options?.persist;
|
|
97
120
|
if (!persist) return "none";
|
|
98
|
-
return `persist:${persist.keys?.join(",") ?? "*"}:${persist.debounceMs ?? "default"}`;
|
|
121
|
+
return `persist:${persist.keys?.join(",") ?? "*"}:${persist.debounceMs ?? "default"}:v${persist.version ?? 0}`;
|
|
99
122
|
}
|
|
100
123
|
function configureStore(name, scope, options) {
|
|
101
124
|
const key = `${scope} ${name}`;
|
|
102
125
|
if (stores.has(key)) {
|
|
103
126
|
if (configSignature(storeConfig.get(key)) === configSignature(options)) return;
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
127
|
+
if (process.env.NODE_ENV !== "production") {
|
|
128
|
+
devWarn(
|
|
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.`
|
|
131
|
+
);
|
|
132
|
+
}
|
|
107
133
|
return;
|
|
108
134
|
}
|
|
109
135
|
storeConfig.set(key, options);
|
|
@@ -117,11 +143,12 @@ function getStore(name, scope = "everywhere") {
|
|
|
117
143
|
}
|
|
118
144
|
return store;
|
|
119
145
|
}
|
|
120
|
-
function getPresence(name) {
|
|
121
|
-
|
|
146
|
+
function getPresence(name, includeSelf = false) {
|
|
147
|
+
const key = includeSelf ? `self ${name}` : name;
|
|
148
|
+
let presence = presences.get(key);
|
|
122
149
|
if (!presence) {
|
|
123
|
-
presence = isServer() ? createServerPresence() : createPresence(name);
|
|
124
|
-
presences.set(
|
|
150
|
+
presence = isServer() ? createServerPresence() : createPresence(name, { includeSelf });
|
|
151
|
+
presences.set(key, presence);
|
|
125
152
|
}
|
|
126
153
|
return presence;
|
|
127
154
|
}
|
|
@@ -142,26 +169,55 @@ function warnOnLeaderOptionConflict(name, options) {
|
|
|
142
169
|
for (const key of ["heartbeatMs", "leaseMs"]) {
|
|
143
170
|
const requested = options[key];
|
|
144
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") {
|
|
145
188
|
devWarn(
|
|
146
|
-
|
|
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.`
|
|
147
191
|
);
|
|
148
192
|
}
|
|
193
|
+
return;
|
|
149
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;
|
|
150
206
|
}
|
|
151
207
|
function getChannel(name) {
|
|
152
208
|
let channel = channels.get(name);
|
|
153
209
|
if (!channel) {
|
|
154
|
-
channel = isServer() ? createServerChannel(name) : createChannel(name);
|
|
210
|
+
channel = isServer() ? createServerChannel(name) : createChannel(name, channelConfig.get(name));
|
|
155
211
|
channels.set(name, channel);
|
|
156
212
|
}
|
|
157
213
|
return channel;
|
|
158
214
|
}
|
|
159
215
|
|
|
160
216
|
// src/use-peers.ts
|
|
161
|
-
import { useCallback, useSyncExternalStore } from "react";
|
|
217
|
+
import { useCallback, useEffect, useSyncExternalStore } from "react";
|
|
162
218
|
var NO_PEERS2 = Object.freeze([]);
|
|
163
219
|
function usePeers(options) {
|
|
164
|
-
const presence = getPresence(options?.name ?? DEFAULT_NAME);
|
|
220
|
+
const presence = getPresence(options?.name ?? DEFAULT_NAME, options?.includeSelf ?? false);
|
|
165
221
|
return useSyncExternalStore(
|
|
166
222
|
useCallback((onChange) => presence.subscribe(onChange), [presence]),
|
|
167
223
|
() => presence.getPeers(),
|
|
@@ -180,6 +236,12 @@ function useClientId(options) {
|
|
|
180
236
|
() => SERVER_CLIENT_ID
|
|
181
237
|
);
|
|
182
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
|
+
}
|
|
183
245
|
|
|
184
246
|
export {
|
|
185
247
|
warnOnInitialMismatch,
|
|
@@ -188,7 +250,10 @@ export {
|
|
|
188
250
|
configureStore,
|
|
189
251
|
getStore,
|
|
190
252
|
getLeader,
|
|
253
|
+
configureChannel,
|
|
254
|
+
getReducer,
|
|
191
255
|
getChannel,
|
|
192
256
|
usePeers,
|
|
193
|
-
useClientId
|
|
257
|
+
useClientId,
|
|
258
|
+
usePresenceMetadata
|
|
194
259
|
};
|
package/dist/devtools/index.cjs
CHANGED
|
@@ -42,6 +42,12 @@ var unsubscribe = () => noop;
|
|
|
42
42
|
var INERT = Object.freeze({
|
|
43
43
|
clientId: SERVER_CLIENT_ID,
|
|
44
44
|
// store
|
|
45
|
+
//
|
|
46
|
+
// Resolved, unlike the leader's `waitForLeadership` below. A server render
|
|
47
|
+
// has nothing to restore and is documented to render defaults, so `await
|
|
48
|
+
// store.hydrated` must proceed — a never-settling promise would hang the
|
|
49
|
+
// render rather than describe it honestly.
|
|
50
|
+
hydrated: Promise.resolve(),
|
|
45
51
|
state: EMPTY,
|
|
46
52
|
getVersions: () => EMPTY,
|
|
47
53
|
set: noop,
|
|
@@ -50,6 +56,8 @@ var INERT = Object.freeze({
|
|
|
50
56
|
// store + leader share getSnapshot; presence has getPeers
|
|
51
57
|
getSnapshot: () => EMPTY,
|
|
52
58
|
getPeers: () => NO_PEERS,
|
|
59
|
+
// presence
|
|
60
|
+
setMetadata: noop,
|
|
53
61
|
// leader
|
|
54
62
|
resign: noop,
|
|
55
63
|
setEligible: noop,
|
|
@@ -83,11 +91,12 @@ function getStore(name, scope = "everywhere") {
|
|
|
83
91
|
}
|
|
84
92
|
return store;
|
|
85
93
|
}
|
|
86
|
-
function getPresence(name) {
|
|
87
|
-
|
|
94
|
+
function getPresence(name, includeSelf = false) {
|
|
95
|
+
const key = includeSelf ? `self ${name}` : name;
|
|
96
|
+
let presence = presences.get(key);
|
|
88
97
|
if (!presence) {
|
|
89
|
-
presence = isServer() ? createServerPresence() : (0, import_core.createPresence)(name);
|
|
90
|
-
presences.set(
|
|
98
|
+
presence = isServer() ? createServerPresence() : (0, import_core.createPresence)(name, { includeSelf });
|
|
99
|
+
presences.set(key, presence);
|
|
91
100
|
}
|
|
92
101
|
return presence;
|
|
93
102
|
}
|
|
@@ -96,7 +105,7 @@ function getPresence(name) {
|
|
|
96
105
|
var import_react = require("react");
|
|
97
106
|
var NO_PEERS2 = Object.freeze([]);
|
|
98
107
|
function usePeers(options) {
|
|
99
|
-
const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME);
|
|
108
|
+
const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME, options?.includeSelf ?? false);
|
|
100
109
|
return (0, import_react.useSyncExternalStore)(
|
|
101
110
|
(0, import_react.useCallback)((onChange) => presence.subscribe(onChange), [presence]),
|
|
102
111
|
() => presence.getPeers(),
|
|
@@ -153,6 +162,13 @@ var STYLES = `
|
|
|
153
162
|
.ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
|
|
154
163
|
.ue-ins__k { color: #79c0ff; flex: none; }
|
|
155
164
|
.ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
|
|
165
|
+
button.ue-ins__v {
|
|
166
|
+
background: none;
|
|
167
|
+
border: 0;
|
|
168
|
+
padding: 0;
|
|
169
|
+
font: inherit;
|
|
170
|
+
text-align: left;
|
|
171
|
+
}
|
|
156
172
|
.ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
|
|
157
173
|
.ue-ins__empty { color: #6e7681; }
|
|
158
174
|
|
|
@@ -163,6 +179,43 @@ var STYLES = `
|
|
|
163
179
|
.ue-ins__dir--in { color: #3fb950; }
|
|
164
180
|
.ue-ins__scope { color: #e6edf3; }
|
|
165
181
|
.ue-ins__from { color: #6e7681; margin-left: auto; }
|
|
182
|
+
|
|
183
|
+
.ue-ins__tools { display: flex; gap: 6px; align-items: center; margin-bottom: 5px; }
|
|
184
|
+
.ue-ins__btn {
|
|
185
|
+
background: #21262d;
|
|
186
|
+
border: 1px solid #30363d;
|
|
187
|
+
border-radius: 5px;
|
|
188
|
+
color: #c9d1d9;
|
|
189
|
+
font: inherit;
|
|
190
|
+
font-size: 10px;
|
|
191
|
+
padding: 1px 7px;
|
|
192
|
+
cursor: pointer;
|
|
193
|
+
}
|
|
194
|
+
.ue-ins__btn[aria-pressed='true'] { background: #1f6feb; border-color: #1f6feb; color: #fff; }
|
|
195
|
+
.ue-ins__filter {
|
|
196
|
+
flex: 1;
|
|
197
|
+
min-width: 0;
|
|
198
|
+
background: #0d1117;
|
|
199
|
+
border: 1px solid #30363d;
|
|
200
|
+
border-radius: 5px;
|
|
201
|
+
color: #c9d1d9;
|
|
202
|
+
font: inherit;
|
|
203
|
+
font-size: 10px;
|
|
204
|
+
padding: 1px 6px;
|
|
205
|
+
}
|
|
206
|
+
.ue-ins__edit {
|
|
207
|
+
flex: 1;
|
|
208
|
+
min-width: 0;
|
|
209
|
+
background: #0d1117;
|
|
210
|
+
border: 1px solid #1f6feb;
|
|
211
|
+
border-radius: 4px;
|
|
212
|
+
color: #e6edf3;
|
|
213
|
+
font: inherit;
|
|
214
|
+
padding: 0 4px;
|
|
215
|
+
}
|
|
216
|
+
.ue-ins__v--editable { cursor: text; text-decoration: underline dotted #30363d; }
|
|
217
|
+
.ue-ins__v--invalid { color: #f85149; }
|
|
218
|
+
.ue-ins__paused { color: #d29922; }
|
|
166
219
|
`;
|
|
167
220
|
|
|
168
221
|
// src/devtools/inspector.tsx
|
|
@@ -181,8 +234,15 @@ function Inspector({
|
|
|
181
234
|
const [open, setOpen] = (0, import_react2.useState)(defaultOpen);
|
|
182
235
|
const [wires, setWires] = (0, import_react2.useState)([]);
|
|
183
236
|
const [crown, setCrown] = (0, import_react2.useState)(null);
|
|
237
|
+
const [paused, setPaused] = (0, import_react2.useState)(false);
|
|
238
|
+
const [filter, setFilter] = (0, import_react2.useState)("");
|
|
239
|
+
const [editing, setEditing] = (0, import_react2.useState)(null);
|
|
184
240
|
const nextId = (0, import_react2.useRef)(0);
|
|
185
241
|
const crownAt = (0, import_react2.useRef)(0);
|
|
242
|
+
const pausedRef = (0, import_react2.useRef)(false);
|
|
243
|
+
(0, import_react2.useEffect)(() => {
|
|
244
|
+
pausedRef.current = paused;
|
|
245
|
+
}, [paused]);
|
|
186
246
|
const peers = usePeers({ name });
|
|
187
247
|
const store = getSharedStore(name);
|
|
188
248
|
const subscribe = (0, import_react2.useCallback)((onChange) => store.subscribe(onChange), [store]);
|
|
@@ -208,12 +268,14 @@ function Inspector({
|
|
|
208
268
|
crownAt.current = Date.now();
|
|
209
269
|
}
|
|
210
270
|
}
|
|
271
|
+
if (pausedRef.current) return;
|
|
211
272
|
setWires(
|
|
212
273
|
(prev) => [
|
|
213
274
|
...prev.slice(-(limit - 1)),
|
|
214
275
|
{
|
|
215
276
|
id: nextId.current++,
|
|
216
277
|
direction,
|
|
278
|
+
scope: wire.scope,
|
|
217
279
|
label: wireLabel(wire),
|
|
218
280
|
from: short(wire.clientId)
|
|
219
281
|
}
|
|
@@ -238,6 +300,28 @@ function Inspector({
|
|
|
238
300
|
}, [leaseMs]);
|
|
239
301
|
const selfId = store.clientId;
|
|
240
302
|
const entries = Object.entries(versions);
|
|
303
|
+
const needle = filter.trim().toLowerCase();
|
|
304
|
+
const shown = needle ? wires.filter(
|
|
305
|
+
(wire) => wire.label.toLowerCase().includes(needle) || wire.from.toLowerCase().includes(needle)
|
|
306
|
+
) : wires;
|
|
307
|
+
const commit = (key, draft) => {
|
|
308
|
+
let value;
|
|
309
|
+
try {
|
|
310
|
+
value = JSON.parse(draft);
|
|
311
|
+
} catch {
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
store.set(key, value);
|
|
315
|
+
setEditing(null);
|
|
316
|
+
};
|
|
317
|
+
const draftIsValid = (draft) => {
|
|
318
|
+
try {
|
|
319
|
+
JSON.parse(draft);
|
|
320
|
+
return true;
|
|
321
|
+
} catch {
|
|
322
|
+
return false;
|
|
323
|
+
}
|
|
324
|
+
};
|
|
241
325
|
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
|
|
242
326
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: STYLES }),
|
|
243
327
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
|
|
@@ -285,7 +369,31 @@ function Inspector({
|
|
|
285
369
|
] }),
|
|
286
370
|
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: [
|
|
287
371
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__k", children: key }),
|
|
288
|
-
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
372
|
+
editing?.key === key ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
373
|
+
"input",
|
|
374
|
+
{
|
|
375
|
+
className: `ue-ins__edit${draftIsValid(editing.draft) ? "" : " ue-ins__v--invalid"}`,
|
|
376
|
+
value: editing.draft,
|
|
377
|
+
autoFocus: true,
|
|
378
|
+
"aria-label": `Value of ${key}`,
|
|
379
|
+
onChange: (event) => setEditing({ key, draft: event.target.value }),
|
|
380
|
+
onKeyDown: (event) => {
|
|
381
|
+
if (event.key === "Enter") commit(key, editing.draft);
|
|
382
|
+
if (event.key === "Escape") setEditing(null);
|
|
383
|
+
},
|
|
384
|
+
onBlur: () => setEditing(null),
|
|
385
|
+
"data-testid": `ue-edit-${key}`
|
|
386
|
+
}
|
|
387
|
+
) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
388
|
+
"button",
|
|
389
|
+
{
|
|
390
|
+
type: "button",
|
|
391
|
+
className: "ue-ins__v ue-ins__v--editable",
|
|
392
|
+
onClick: () => setEditing({ key, draft: JSON.stringify(snapshot[key]) ?? "" }),
|
|
393
|
+
"data-testid": `ue-value-${key}`,
|
|
394
|
+
children: JSON.stringify(snapshot[key])
|
|
395
|
+
}
|
|
396
|
+
),
|
|
289
397
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ue-ins__ver", children: [
|
|
290
398
|
version[0],
|
|
291
399
|
"\xB7",
|
|
@@ -296,10 +404,46 @@ function Inspector({
|
|
|
296
404
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
|
|
297
405
|
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__h", children: [
|
|
298
406
|
"Wires (",
|
|
299
|
-
|
|
300
|
-
"
|
|
407
|
+
shown.length,
|
|
408
|
+
shown.length === wires.length ? "" : ` of ${wires.length}`,
|
|
409
|
+
")",
|
|
410
|
+
paused ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__paused", children: " \xB7 paused" }) : null
|
|
411
|
+
] }),
|
|
412
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__tools", children: [
|
|
413
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
414
|
+
"button",
|
|
415
|
+
{
|
|
416
|
+
type: "button",
|
|
417
|
+
className: "ue-ins__btn",
|
|
418
|
+
"aria-pressed": paused,
|
|
419
|
+
onClick: () => setPaused((value) => !value),
|
|
420
|
+
"data-testid": "ue-pause",
|
|
421
|
+
children: paused ? "resume" : "pause"
|
|
422
|
+
}
|
|
423
|
+
),
|
|
424
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
425
|
+
"button",
|
|
426
|
+
{
|
|
427
|
+
type: "button",
|
|
428
|
+
className: "ue-ins__btn",
|
|
429
|
+
onClick: () => setWires([]),
|
|
430
|
+
"data-testid": "ue-clear",
|
|
431
|
+
children: "clear"
|
|
432
|
+
}
|
|
433
|
+
),
|
|
434
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(
|
|
435
|
+
"input",
|
|
436
|
+
{
|
|
437
|
+
className: "ue-ins__filter",
|
|
438
|
+
value: filter,
|
|
439
|
+
placeholder: "filter",
|
|
440
|
+
"aria-label": "Filter wires",
|
|
441
|
+
onChange: (event) => setFilter(event.target.value),
|
|
442
|
+
"data-testid": "ue-filter"
|
|
443
|
+
}
|
|
444
|
+
)
|
|
301
445
|
] }),
|
|
302
|
-
|
|
446
|
+
shown.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__empty", children: wires.length === 0 ? "nothing yet" : "no matches" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__log", children: shown.map((wire) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__wire", children: [
|
|
303
447
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
|
|
304
448
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__scope", children: wire.label }),
|
|
305
449
|
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__from", children: wire.from })
|
package/dist/devtools/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
import {
|
|
3
3
|
getSharedStore,
|
|
4
4
|
usePeers
|
|
5
|
-
} from "../chunk-
|
|
5
|
+
} from "../chunk-R7PBPLF7.js";
|
|
6
6
|
|
|
7
7
|
// src/devtools/inspector.tsx
|
|
8
8
|
import { useCallback, useEffect, useRef, useState, useSyncExternalStore } from "react";
|
|
@@ -57,6 +57,13 @@ var STYLES = `
|
|
|
57
57
|
.ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
|
|
58
58
|
.ue-ins__k { color: #79c0ff; flex: none; }
|
|
59
59
|
.ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
|
|
60
|
+
button.ue-ins__v {
|
|
61
|
+
background: none;
|
|
62
|
+
border: 0;
|
|
63
|
+
padding: 0;
|
|
64
|
+
font: inherit;
|
|
65
|
+
text-align: left;
|
|
66
|
+
}
|
|
60
67
|
.ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
|
|
61
68
|
.ue-ins__empty { color: #6e7681; }
|
|
62
69
|
|
|
@@ -67,6 +74,43 @@ var STYLES = `
|
|
|
67
74
|
.ue-ins__dir--in { color: #3fb950; }
|
|
68
75
|
.ue-ins__scope { color: #e6edf3; }
|
|
69
76
|
.ue-ins__from { color: #6e7681; margin-left: auto; }
|
|
77
|
+
|
|
78
|
+
.ue-ins__tools { display: flex; gap: 6px; align-items: center; margin-bottom: 5px; }
|
|
79
|
+
.ue-ins__btn {
|
|
80
|
+
background: #21262d;
|
|
81
|
+
border: 1px solid #30363d;
|
|
82
|
+
border-radius: 5px;
|
|
83
|
+
color: #c9d1d9;
|
|
84
|
+
font: inherit;
|
|
85
|
+
font-size: 10px;
|
|
86
|
+
padding: 1px 7px;
|
|
87
|
+
cursor: pointer;
|
|
88
|
+
}
|
|
89
|
+
.ue-ins__btn[aria-pressed='true'] { background: #1f6feb; border-color: #1f6feb; color: #fff; }
|
|
90
|
+
.ue-ins__filter {
|
|
91
|
+
flex: 1;
|
|
92
|
+
min-width: 0;
|
|
93
|
+
background: #0d1117;
|
|
94
|
+
border: 1px solid #30363d;
|
|
95
|
+
border-radius: 5px;
|
|
96
|
+
color: #c9d1d9;
|
|
97
|
+
font: inherit;
|
|
98
|
+
font-size: 10px;
|
|
99
|
+
padding: 1px 6px;
|
|
100
|
+
}
|
|
101
|
+
.ue-ins__edit {
|
|
102
|
+
flex: 1;
|
|
103
|
+
min-width: 0;
|
|
104
|
+
background: #0d1117;
|
|
105
|
+
border: 1px solid #1f6feb;
|
|
106
|
+
border-radius: 4px;
|
|
107
|
+
color: #e6edf3;
|
|
108
|
+
font: inherit;
|
|
109
|
+
padding: 0 4px;
|
|
110
|
+
}
|
|
111
|
+
.ue-ins__v--editable { cursor: text; text-decoration: underline dotted #30363d; }
|
|
112
|
+
.ue-ins__v--invalid { color: #f85149; }
|
|
113
|
+
.ue-ins__paused { color: #d29922; }
|
|
70
114
|
`;
|
|
71
115
|
|
|
72
116
|
// src/devtools/inspector.tsx
|
|
@@ -85,8 +129,15 @@ function Inspector({
|
|
|
85
129
|
const [open, setOpen] = useState(defaultOpen);
|
|
86
130
|
const [wires, setWires] = useState([]);
|
|
87
131
|
const [crown, setCrown] = useState(null);
|
|
132
|
+
const [paused, setPaused] = useState(false);
|
|
133
|
+
const [filter, setFilter] = useState("");
|
|
134
|
+
const [editing, setEditing] = useState(null);
|
|
88
135
|
const nextId = useRef(0);
|
|
89
136
|
const crownAt = useRef(0);
|
|
137
|
+
const pausedRef = useRef(false);
|
|
138
|
+
useEffect(() => {
|
|
139
|
+
pausedRef.current = paused;
|
|
140
|
+
}, [paused]);
|
|
90
141
|
const peers = usePeers({ name });
|
|
91
142
|
const store = getSharedStore(name);
|
|
92
143
|
const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
|
|
@@ -112,12 +163,14 @@ function Inspector({
|
|
|
112
163
|
crownAt.current = Date.now();
|
|
113
164
|
}
|
|
114
165
|
}
|
|
166
|
+
if (pausedRef.current) return;
|
|
115
167
|
setWires(
|
|
116
168
|
(prev) => [
|
|
117
169
|
...prev.slice(-(limit - 1)),
|
|
118
170
|
{
|
|
119
171
|
id: nextId.current++,
|
|
120
172
|
direction,
|
|
173
|
+
scope: wire.scope,
|
|
121
174
|
label: wireLabel(wire),
|
|
122
175
|
from: short(wire.clientId)
|
|
123
176
|
}
|
|
@@ -142,6 +195,28 @@ function Inspector({
|
|
|
142
195
|
}, [leaseMs]);
|
|
143
196
|
const selfId = store.clientId;
|
|
144
197
|
const entries = Object.entries(versions);
|
|
198
|
+
const needle = filter.trim().toLowerCase();
|
|
199
|
+
const shown = needle ? wires.filter(
|
|
200
|
+
(wire) => wire.label.toLowerCase().includes(needle) || wire.from.toLowerCase().includes(needle)
|
|
201
|
+
) : wires;
|
|
202
|
+
const commit = (key, draft) => {
|
|
203
|
+
let value;
|
|
204
|
+
try {
|
|
205
|
+
value = JSON.parse(draft);
|
|
206
|
+
} catch {
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
store.set(key, value);
|
|
210
|
+
setEditing(null);
|
|
211
|
+
};
|
|
212
|
+
const draftIsValid = (draft) => {
|
|
213
|
+
try {
|
|
214
|
+
JSON.parse(draft);
|
|
215
|
+
return true;
|
|
216
|
+
} catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
};
|
|
145
220
|
return /* @__PURE__ */ jsxs("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
|
|
146
221
|
/* @__PURE__ */ jsx("style", { children: STYLES }),
|
|
147
222
|
/* @__PURE__ */ jsxs(
|
|
@@ -189,7 +264,31 @@ function Inspector({
|
|
|
189
264
|
] }),
|
|
190
265
|
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
266
|
/* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: key }),
|
|
192
|
-
/* @__PURE__ */ jsx(
|
|
267
|
+
editing?.key === key ? /* @__PURE__ */ jsx(
|
|
268
|
+
"input",
|
|
269
|
+
{
|
|
270
|
+
className: `ue-ins__edit${draftIsValid(editing.draft) ? "" : " ue-ins__v--invalid"}`,
|
|
271
|
+
value: editing.draft,
|
|
272
|
+
autoFocus: true,
|
|
273
|
+
"aria-label": `Value of ${key}`,
|
|
274
|
+
onChange: (event) => setEditing({ key, draft: event.target.value }),
|
|
275
|
+
onKeyDown: (event) => {
|
|
276
|
+
if (event.key === "Enter") commit(key, editing.draft);
|
|
277
|
+
if (event.key === "Escape") setEditing(null);
|
|
278
|
+
},
|
|
279
|
+
onBlur: () => setEditing(null),
|
|
280
|
+
"data-testid": `ue-edit-${key}`
|
|
281
|
+
}
|
|
282
|
+
) : /* @__PURE__ */ jsx(
|
|
283
|
+
"button",
|
|
284
|
+
{
|
|
285
|
+
type: "button",
|
|
286
|
+
className: "ue-ins__v ue-ins__v--editable",
|
|
287
|
+
onClick: () => setEditing({ key, draft: JSON.stringify(snapshot[key]) ?? "" }),
|
|
288
|
+
"data-testid": `ue-value-${key}`,
|
|
289
|
+
children: JSON.stringify(snapshot[key])
|
|
290
|
+
}
|
|
291
|
+
),
|
|
193
292
|
/* @__PURE__ */ jsxs("span", { className: "ue-ins__ver", children: [
|
|
194
293
|
version[0],
|
|
195
294
|
"\xB7",
|
|
@@ -200,10 +299,46 @@ function Inspector({
|
|
|
200
299
|
/* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
|
|
201
300
|
/* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
|
|
202
301
|
"Wires (",
|
|
203
|
-
|
|
204
|
-
"
|
|
302
|
+
shown.length,
|
|
303
|
+
shown.length === wires.length ? "" : ` of ${wires.length}`,
|
|
304
|
+
")",
|
|
305
|
+
paused ? /* @__PURE__ */ jsx("span", { className: "ue-ins__paused", children: " \xB7 paused" }) : null
|
|
306
|
+
] }),
|
|
307
|
+
/* @__PURE__ */ jsxs("div", { className: "ue-ins__tools", children: [
|
|
308
|
+
/* @__PURE__ */ jsx(
|
|
309
|
+
"button",
|
|
310
|
+
{
|
|
311
|
+
type: "button",
|
|
312
|
+
className: "ue-ins__btn",
|
|
313
|
+
"aria-pressed": paused,
|
|
314
|
+
onClick: () => setPaused((value) => !value),
|
|
315
|
+
"data-testid": "ue-pause",
|
|
316
|
+
children: paused ? "resume" : "pause"
|
|
317
|
+
}
|
|
318
|
+
),
|
|
319
|
+
/* @__PURE__ */ jsx(
|
|
320
|
+
"button",
|
|
321
|
+
{
|
|
322
|
+
type: "button",
|
|
323
|
+
className: "ue-ins__btn",
|
|
324
|
+
onClick: () => setWires([]),
|
|
325
|
+
"data-testid": "ue-clear",
|
|
326
|
+
children: "clear"
|
|
327
|
+
}
|
|
328
|
+
),
|
|
329
|
+
/* @__PURE__ */ jsx(
|
|
330
|
+
"input",
|
|
331
|
+
{
|
|
332
|
+
className: "ue-ins__filter",
|
|
333
|
+
value: filter,
|
|
334
|
+
placeholder: "filter",
|
|
335
|
+
"aria-label": "Filter wires",
|
|
336
|
+
onChange: (event) => setFilter(event.target.value),
|
|
337
|
+
"data-testid": "ue-filter"
|
|
338
|
+
}
|
|
339
|
+
)
|
|
205
340
|
] }),
|
|
206
|
-
|
|
341
|
+
shown.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: wires.length === 0 ? "nothing yet" : "no matches" }) : /* @__PURE__ */ jsx("div", { className: "ue-ins__log", children: shown.map((wire) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__wire", children: [
|
|
207
342
|
/* @__PURE__ */ jsx("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
|
|
208
343
|
/* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: wire.label }),
|
|
209
344
|
/* @__PURE__ */ jsx("span", { className: "ue-ins__from", children: wire.from })
|