use-everywhere 0.2.0 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,92 @@
1
+ 'use client';
2
+
3
+ // src/registry.ts
4
+ import {
5
+ createChannel,
6
+ createLeader,
7
+ createPresence,
8
+ createSharedStore,
9
+ DEFAULT_NAME,
10
+ NoopTransport
11
+ } from "@use-everywhere/core";
12
+ var stores = /* @__PURE__ */ new Map();
13
+ var presences = /* @__PURE__ */ new Map();
14
+ var channels = /* @__PURE__ */ new Map();
15
+ var leaders = /* @__PURE__ */ new Map();
16
+ var storeConfig = /* @__PURE__ */ new Map();
17
+ var scopeOptions = {
18
+ everywhere: {},
19
+ tabs: { accept: (meta) => meta.kind !== "worker" },
20
+ tab: { transport: () => new NoopTransport() }
21
+ };
22
+ function getSharedStore(name = DEFAULT_NAME, scope = "everywhere") {
23
+ return getStore(name, scope);
24
+ }
25
+ function configureStore(name, scope, options) {
26
+ const key = `${scope} ${name}`;
27
+ if (stores.has(key)) {
28
+ throw new Error(
29
+ `defineStore('${name}') ran after that store was already created. Move it to module scope \u2014 configuring a live store would silently hand you one without persistence.`
30
+ );
31
+ }
32
+ storeConfig.set(key, options);
33
+ }
34
+ function getStore(name, scope = "everywhere") {
35
+ const key = `${scope} ${name}`;
36
+ let store = stores.get(key);
37
+ if (!store) {
38
+ store = createSharedStore(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
39
+ stores.set(key, store);
40
+ }
41
+ return store;
42
+ }
43
+ function getPresence(name) {
44
+ let presence = presences.get(name);
45
+ if (!presence) {
46
+ presence = createPresence(name);
47
+ presences.set(name, presence);
48
+ }
49
+ return presence;
50
+ }
51
+ function getLeader(name, options) {
52
+ let leader = leaders.get(name);
53
+ if (!leader) {
54
+ leader = createLeader(name, options);
55
+ leaders.set(name, leader);
56
+ }
57
+ return leader;
58
+ }
59
+ function getChannel(name) {
60
+ let channel = channels.get(name);
61
+ if (!channel) {
62
+ channel = createChannel(name);
63
+ channels.set(name, channel);
64
+ }
65
+ return channel;
66
+ }
67
+
68
+ // src/use-peers.ts
69
+ import { useCallback, useSyncExternalStore } from "react";
70
+ var NO_PEERS = Object.freeze([]);
71
+ function usePeers(options) {
72
+ const presence = getPresence(options?.name ?? DEFAULT_NAME);
73
+ return useSyncExternalStore(
74
+ useCallback((onChange) => presence.subscribe(onChange), [presence]),
75
+ () => presence.getPeers(),
76
+ () => NO_PEERS
77
+ );
78
+ }
79
+ function useClientId(options) {
80
+ return getPresence(options?.name ?? DEFAULT_NAME).clientId;
81
+ }
82
+
83
+ export {
84
+ DEFAULT_NAME,
85
+ getSharedStore,
86
+ configureStore,
87
+ getStore,
88
+ getLeader,
89
+ getChannel,
90
+ usePeers,
91
+ useClientId
92
+ };
@@ -0,0 +1,282 @@
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/devtools/index.ts
22
+ var devtools_exports = {};
23
+ __export(devtools_exports, {
24
+ Inspector: () => Inspector
25
+ });
26
+ module.exports = __toCommonJS(devtools_exports);
27
+
28
+ // src/devtools/inspector.tsx
29
+ var import_react2 = require("react");
30
+ var import_core2 = require("@use-everywhere/core");
31
+
32
+ // src/registry.ts
33
+ var import_core = require("@use-everywhere/core");
34
+ var stores = /* @__PURE__ */ new Map();
35
+ var presences = /* @__PURE__ */ new Map();
36
+ var storeConfig = /* @__PURE__ */ new Map();
37
+ var scopeOptions = {
38
+ everywhere: {},
39
+ tabs: { accept: (meta) => meta.kind !== "worker" },
40
+ tab: { transport: () => new import_core.NoopTransport() }
41
+ };
42
+ function getSharedStore(name = import_core.DEFAULT_NAME, scope = "everywhere") {
43
+ return getStore(name, scope);
44
+ }
45
+ function getStore(name, scope = "everywhere") {
46
+ const key = `${scope} ${name}`;
47
+ let store = stores.get(key);
48
+ if (!store) {
49
+ store = (0, import_core.createSharedStore)(name, {}, { ...scopeOptions[scope], ...storeConfig.get(key) });
50
+ stores.set(key, store);
51
+ }
52
+ return store;
53
+ }
54
+ function getPresence(name) {
55
+ let presence = presences.get(name);
56
+ if (!presence) {
57
+ presence = (0, import_core.createPresence)(name);
58
+ presences.set(name, presence);
59
+ }
60
+ return presence;
61
+ }
62
+
63
+ // src/use-peers.ts
64
+ var import_react = require("react");
65
+ var NO_PEERS = Object.freeze([]);
66
+ function usePeers(options) {
67
+ const presence = getPresence(options?.name ?? import_core.DEFAULT_NAME);
68
+ return (0, import_react.useSyncExternalStore)(
69
+ (0, import_react.useCallback)((onChange) => presence.subscribe(onChange), [presence]),
70
+ () => presence.getPeers(),
71
+ () => NO_PEERS
72
+ );
73
+ }
74
+
75
+ // src/devtools/styles.ts
76
+ var STYLES = `
77
+ .ue-ins {
78
+ position: fixed;
79
+ z-index: 2147483000;
80
+ font: 12px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace;
81
+ color: #e6edf3;
82
+ background: #0d1117;
83
+ border: 1px solid #30363d;
84
+ border-radius: 8px;
85
+ box-shadow: 0 8px 32px rgb(0 0 0 / 0.4);
86
+ max-width: min(420px, calc(100vw - 24px));
87
+ overflow: hidden;
88
+ }
89
+ .ue-ins--bottom-right { bottom: 12px; right: 12px; }
90
+ .ue-ins--bottom-left { bottom: 12px; left: 12px; }
91
+ .ue-ins--top-right { top: 12px; right: 12px; }
92
+ .ue-ins--top-left { top: 12px; left: 12px; }
93
+
94
+ .ue-ins__bar {
95
+ display: flex;
96
+ align-items: center;
97
+ gap: 8px;
98
+ width: 100%;
99
+ padding: 7px 10px;
100
+ background: #161b22;
101
+ border: 0;
102
+ color: inherit;
103
+ font: inherit;
104
+ cursor: pointer;
105
+ text-align: left;
106
+ }
107
+ .ue-ins__dot { width: 7px; height: 7px; border-radius: 50%; background: #3fb950; flex: none; }
108
+ .ue-ins__title { font-weight: 600; }
109
+ .ue-ins__muted { color: #8b949e; }
110
+ .ue-ins__crown { margin-left: auto; color: #d29922; }
111
+
112
+ .ue-ins__body { max-height: 60vh; overflow-y: auto; }
113
+ .ue-ins__section { border-top: 1px solid #21262d; padding: 8px 10px; }
114
+ .ue-ins__h {
115
+ color: #8b949e;
116
+ text-transform: uppercase;
117
+ letter-spacing: 0.06em;
118
+ font-size: 10px;
119
+ margin-bottom: 5px;
120
+ }
121
+ .ue-ins__row { display: flex; gap: 8px; padding: 1px 0; }
122
+ .ue-ins__k { color: #79c0ff; flex: none; }
123
+ .ue-ins__v { color: #e6edf3; overflow-wrap: anywhere; }
124
+ .ue-ins__ver { color: #6e7681; margin-left: auto; flex: none; }
125
+ .ue-ins__empty { color: #6e7681; }
126
+
127
+ .ue-ins__log { display: flex; flex-direction: column-reverse; max-height: 190px; overflow-y: auto; }
128
+ .ue-ins__wire { display: flex; gap: 7px; padding: 1px 0; white-space: nowrap; }
129
+ .ue-ins__dir { flex: none; width: 9px; }
130
+ .ue-ins__dir--out { color: #d29922; }
131
+ .ue-ins__dir--in { color: #3fb950; }
132
+ .ue-ins__scope { color: #e6edf3; }
133
+ .ue-ins__from { color: #6e7681; margin-left: auto; }
134
+ `;
135
+
136
+ // src/devtools/inspector.tsx
137
+ var import_jsx_runtime = require("react/jsx-runtime");
138
+ var short = (id) => id.slice(0, 6);
139
+ function wireLabel(wire) {
140
+ return `${wire.scope}/${wire.type}`;
141
+ }
142
+ function Inspector({
143
+ name = import_core2.DEFAULT_NAME,
144
+ position = "bottom-right",
145
+ limit = 50,
146
+ defaultOpen = false,
147
+ leaseMs = 3e3
148
+ } = {}) {
149
+ const [open, setOpen] = (0, import_react2.useState)(defaultOpen);
150
+ const [wires, setWires] = (0, import_react2.useState)([]);
151
+ const [crown, setCrown] = (0, import_react2.useState)(null);
152
+ const nextId = (0, import_react2.useRef)(0);
153
+ const crownAt = (0, import_react2.useRef)(0);
154
+ const peers = usePeers({ name });
155
+ const store = getSharedStore(name);
156
+ const subscribe = (0, import_react2.useCallback)((onChange) => store.subscribe(onChange), [store]);
157
+ const snapshot = (0, import_react2.useSyncExternalStore)(
158
+ subscribe,
159
+ () => store.getSnapshot(),
160
+ () => store.getSnapshot()
161
+ );
162
+ const versions = (0, import_react2.useSyncExternalStore)(
163
+ subscribe,
164
+ () => store.getVersions(),
165
+ () => store.getVersions()
166
+ );
167
+ (0, import_react2.useEffect)(() => {
168
+ return (0, import_core2.observeBus)(name, (event) => {
169
+ const { wire, direction } = event;
170
+ if (wire.scope === "leader") {
171
+ if (wire.type === "resign") {
172
+ setCrown(null);
173
+ crownAt.current = 0;
174
+ } else if (wire.type === "claim" || wire.type === "heartbeat") {
175
+ setCrown(wire.clientId);
176
+ crownAt.current = Date.now();
177
+ }
178
+ }
179
+ setWires(
180
+ (prev) => [
181
+ ...prev.slice(-(limit - 1)),
182
+ {
183
+ id: nextId.current++,
184
+ direction,
185
+ label: wireLabel(wire),
186
+ from: short(wire.clientId)
187
+ }
188
+ ].slice(-limit)
189
+ );
190
+ });
191
+ }, [name, limit]);
192
+ (0, import_react2.useEffect)(() => {
193
+ const timer = setInterval(
194
+ () => {
195
+ if (crownAt.current && Date.now() - crownAt.current > leaseMs) {
196
+ setCrown(null);
197
+ crownAt.current = 0;
198
+ }
199
+ },
200
+ // Quarter of the lease, so a vacated crown clears well within it. The
201
+ // floor is low enough that a short lease still works rather than being
202
+ // silently rounded up to something coarser than the lease itself.
203
+ Math.max(50, Math.floor(leaseMs / 4))
204
+ );
205
+ return () => clearInterval(timer);
206
+ }, [leaseMs]);
207
+ const selfId = store.clientId;
208
+ const entries = Object.entries(versions);
209
+ return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
210
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("style", { children: STYLES }),
211
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(
212
+ "button",
213
+ {
214
+ type: "button",
215
+ className: "ue-ins__bar",
216
+ onClick: () => setOpen((v) => !v),
217
+ "aria-expanded": open,
218
+ children: [
219
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__dot" }),
220
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__title", children: "use-everywhere" }),
221
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__muted", children: name }),
222
+ crown ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ue-ins__crown", "data-testid": "ue-crown", children: [
223
+ "\u2654 ",
224
+ crown === selfId ? "this tab" : short(crown)
225
+ ] }) : null
226
+ ]
227
+ }
228
+ ),
229
+ open ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__body", children: [
230
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
231
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__h", children: "This tab" }),
232
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__row", children: [
233
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__k", children: short(selfId) }),
234
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__v", children: crown === selfId ? "leader" : crown ? "follower" : "no leader" })
235
+ ] })
236
+ ] }),
237
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
238
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__h", children: [
239
+ "Peers (",
240
+ peers.length,
241
+ ")"
242
+ ] }),
243
+ peers.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", { className: "ue-ins__empty", children: "nobody else here" }) : peers.map((peer) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__row", children: [
244
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__k", children: short(peer.id) }),
245
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__v", children: peer.kind })
246
+ ] }, peer.id))
247
+ ] }),
248
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
249
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__h", children: [
250
+ "State (",
251
+ entries.length,
252
+ ")"
253
+ ] }),
254
+ 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: [
255
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__k", children: key }),
256
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__v", children: JSON.stringify(snapshot[key]) }),
257
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", { className: "ue-ins__ver", children: [
258
+ version[0],
259
+ "\xB7",
260
+ short(version[1])
261
+ ] })
262
+ ] }, key))
263
+ ] }),
264
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__section", children: [
265
+ /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { className: "ue-ins__h", children: [
266
+ "Wires (",
267
+ wires.length,
268
+ ")"
269
+ ] }),
270
+ 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: [
271
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
272
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__scope", children: wire.label }),
273
+ /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", { className: "ue-ins__from", children: wire.from })
274
+ ] }, wire.id)) })
275
+ ] })
276
+ ] }) : null
277
+ ] });
278
+ }
279
+ // Annotate the CommonJS export names for ESM import in node:
280
+ 0 && (module.exports = {
281
+ Inspector
282
+ });
@@ -0,0 +1,35 @@
1
+ import * as react from 'react';
2
+
3
+ interface InspectorProps {
4
+ /** Which bus to watch. Defaults to the shared default name. */
5
+ name?: string;
6
+ /** Corner to dock in. Default 'bottom-right'. */
7
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
8
+ /** How many wires to keep in the log. Default 50. */
9
+ limit?: number;
10
+ /** Start expanded. Default false. */
11
+ defaultOpen?: boolean;
12
+ /**
13
+ * How long a leader wire keeps the crown before it is treated as stale.
14
+ * Should match the Leader's leaseMs. Default 3000.
15
+ */
16
+ leaseMs?: number;
17
+ }
18
+
19
+ /**
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.
23
+ *
24
+ * It deliberately does **not** create a Leader. Under dynamic eligibility,
25
+ * mounting one with `eligible: false` would disable candidacy for the whole
26
+ * tab, and mounting a plain one would enrol a tab that never asked to be a
27
+ * candidate — a devtool must not change what it measures. Instead it reads the
28
+ * crown out of the wire log, which it already sees in both directions.
29
+ *
30
+ * Presence is fine to use: the bus heartbeats regardless of whether anything
31
+ * created a Presence, so usePeers observes rather than perturbs.
32
+ */
33
+ declare function Inspector({ name, position, limit, defaultOpen, leaseMs, }?: InspectorProps): react.JSX.Element;
34
+
35
+ export { Inspector, type InspectorProps };
@@ -0,0 +1,35 @@
1
+ import * as react from 'react';
2
+
3
+ interface InspectorProps {
4
+ /** Which bus to watch. Defaults to the shared default name. */
5
+ name?: string;
6
+ /** Corner to dock in. Default 'bottom-right'. */
7
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
8
+ /** How many wires to keep in the log. Default 50. */
9
+ limit?: number;
10
+ /** Start expanded. Default false. */
11
+ defaultOpen?: boolean;
12
+ /**
13
+ * How long a leader wire keeps the crown before it is treated as stale.
14
+ * Should match the Leader's leaseMs. Default 3000.
15
+ */
16
+ leaseMs?: number;
17
+ }
18
+
19
+ /**
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.
23
+ *
24
+ * It deliberately does **not** create a Leader. Under dynamic eligibility,
25
+ * mounting one with `eligible: false` would disable candidacy for the whole
26
+ * tab, and mounting a plain one would enrol a tab that never asked to be a
27
+ * candidate — a devtool must not change what it measures. Instead it reads the
28
+ * crown out of the wire log, which it already sees in both directions.
29
+ *
30
+ * Presence is fine to use: the bus heartbeats regardless of whether anything
31
+ * created a Presence, so usePeers observes rather than perturbs.
32
+ */
33
+ declare function Inspector({ name, position, limit, defaultOpen, leaseMs, }?: InspectorProps): react.JSX.Element;
34
+
35
+ export { Inspector, type InspectorProps };
@@ -0,0 +1,217 @@
1
+ 'use client';
2
+ import {
3
+ getSharedStore,
4
+ usePeers
5
+ } from "../chunk-PNFJW7QK.js";
6
+
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; }
47
+
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
73
+ import { jsx, jsxs } from "react/jsx-runtime";
74
+ var short = (id) => id.slice(0, 6);
75
+ function wireLabel(wire) {
76
+ return `${wire.scope}/${wire.type}`;
77
+ }
78
+ function Inspector({
79
+ name = DEFAULT_NAME,
80
+ position = "bottom-right",
81
+ limit = 50,
82
+ defaultOpen = false,
83
+ leaseMs = 3e3
84
+ } = {}) {
85
+ const [open, setOpen] = useState(defaultOpen);
86
+ const [wires, setWires] = useState([]);
87
+ const [crown, setCrown] = useState(null);
88
+ const nextId = useRef(0);
89
+ const crownAt = useRef(0);
90
+ const peers = usePeers({ name });
91
+ const store = getSharedStore(name);
92
+ const subscribe = useCallback((onChange) => store.subscribe(onChange), [store]);
93
+ const snapshot = useSyncExternalStore(
94
+ subscribe,
95
+ () => store.getSnapshot(),
96
+ () => store.getSnapshot()
97
+ );
98
+ const versions = useSyncExternalStore(
99
+ subscribe,
100
+ () => store.getVersions(),
101
+ () => store.getVersions()
102
+ );
103
+ useEffect(() => {
104
+ return observeBus(name, (event) => {
105
+ const { wire, direction } = event;
106
+ if (wire.scope === "leader") {
107
+ if (wire.type === "resign") {
108
+ setCrown(null);
109
+ crownAt.current = 0;
110
+ } else if (wire.type === "claim" || wire.type === "heartbeat") {
111
+ setCrown(wire.clientId);
112
+ crownAt.current = Date.now();
113
+ }
114
+ }
115
+ setWires(
116
+ (prev) => [
117
+ ...prev.slice(-(limit - 1)),
118
+ {
119
+ id: nextId.current++,
120
+ direction,
121
+ label: wireLabel(wire),
122
+ from: short(wire.clientId)
123
+ }
124
+ ].slice(-limit)
125
+ );
126
+ });
127
+ }, [name, limit]);
128
+ useEffect(() => {
129
+ const timer = setInterval(
130
+ () => {
131
+ if (crownAt.current && Date.now() - crownAt.current > leaseMs) {
132
+ setCrown(null);
133
+ crownAt.current = 0;
134
+ }
135
+ },
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
+ Math.max(50, Math.floor(leaseMs / 4))
140
+ );
141
+ return () => clearInterval(timer);
142
+ }, [leaseMs]);
143
+ const selfId = store.clientId;
144
+ const entries = Object.entries(versions);
145
+ return /* @__PURE__ */ jsxs("div", { className: `ue-ins ue-ins--${position}`, "data-testid": "ue-inspector", children: [
146
+ /* @__PURE__ */ jsx("style", { children: STYLES }),
147
+ /* @__PURE__ */ jsxs(
148
+ "button",
149
+ {
150
+ type: "button",
151
+ className: "ue-ins__bar",
152
+ onClick: () => setOpen((v) => !v),
153
+ "aria-expanded": open,
154
+ children: [
155
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__dot" }),
156
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__title", children: "use-everywhere" }),
157
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__muted", children: name }),
158
+ crown ? /* @__PURE__ */ jsxs("span", { className: "ue-ins__crown", "data-testid": "ue-crown", children: [
159
+ "\u2654 ",
160
+ crown === selfId ? "this tab" : short(crown)
161
+ ] }) : null
162
+ ]
163
+ }
164
+ ),
165
+ open ? /* @__PURE__ */ jsxs("div", { className: "ue-ins__body", children: [
166
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
167
+ /* @__PURE__ */ jsx("div", { className: "ue-ins__h", children: "This tab" }),
168
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
169
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: short(selfId) }),
170
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: crown === selfId ? "leader" : crown ? "follower" : "no leader" })
171
+ ] })
172
+ ] }),
173
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
174
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
175
+ "Peers (",
176
+ peers.length,
177
+ ")"
178
+ ] }),
179
+ peers.length === 0 ? /* @__PURE__ */ jsx("div", { className: "ue-ins__empty", children: "nobody else here" }) : peers.map((peer) => /* @__PURE__ */ jsxs("div", { className: "ue-ins__row", children: [
180
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: short(peer.id) }),
181
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: peer.kind })
182
+ ] }, peer.id))
183
+ ] }),
184
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
185
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
186
+ "State (",
187
+ entries.length,
188
+ ")"
189
+ ] }),
190
+ 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
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__k", children: key }),
192
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__v", children: JSON.stringify(snapshot[key]) }),
193
+ /* @__PURE__ */ jsxs("span", { className: "ue-ins__ver", children: [
194
+ version[0],
195
+ "\xB7",
196
+ short(version[1])
197
+ ] })
198
+ ] }, key))
199
+ ] }),
200
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__section", children: [
201
+ /* @__PURE__ */ jsxs("div", { className: "ue-ins__h", children: [
202
+ "Wires (",
203
+ wires.length,
204
+ ")"
205
+ ] }),
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: [
207
+ /* @__PURE__ */ jsx("span", { className: `ue-ins__dir ue-ins__dir--${wire.direction}`, children: wire.direction === "out" ? "\u2192" : "\u2190" }),
208
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__scope", children: wire.label }),
209
+ /* @__PURE__ */ jsx("span", { className: "ue-ins__from", children: wire.from })
210
+ ] }, wire.id)) })
211
+ ] })
212
+ ] }) : null
213
+ ] });
214
+ }
215
+ export {
216
+ Inspector
217
+ };