react-websocket 4.0.0 → 4.18.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.
Files changed (52) hide show
  1. package/README.md +27 -3
  2. package/dist/ag-ui.cjs +421 -0
  3. package/dist/ag-ui.cjs.map +1 -0
  4. package/dist/ag-ui.d.cts +493 -0
  5. package/dist/ag-ui.d.ts +171 -0
  6. package/dist/ag-ui.js +418 -0
  7. package/dist/ag-ui.js.map +1 -0
  8. package/dist/core.cjs +689 -153
  9. package/dist/core.cjs.map +1 -1
  10. package/dist/core.d.cts +459 -53
  11. package/dist/core.d.ts +459 -53
  12. package/dist/core.js +686 -154
  13. package/dist/core.js.map +1 -1
  14. package/dist/devtools.cjs +186 -0
  15. package/dist/devtools.cjs.map +1 -0
  16. package/dist/devtools.d.cts +32 -0
  17. package/dist/devtools.d.ts +32 -0
  18. package/dist/devtools.js +185 -0
  19. package/dist/devtools.js.map +1 -0
  20. package/dist/index.cjs +182 -23
  21. package/dist/index.cjs.map +1 -1
  22. package/dist/index.d.cts +663 -48
  23. package/dist/index.d.ts +393 -22
  24. package/dist/index.js +178 -24
  25. package/dist/index.js.map +1 -1
  26. package/dist/outbox.cjs +222 -0
  27. package/dist/outbox.cjs.map +1 -0
  28. package/dist/outbox.d.cts +127 -0
  29. package/dist/outbox.d.ts +127 -0
  30. package/dist/outbox.js +219 -0
  31. package/dist/outbox.js.map +1 -0
  32. package/dist/replay.cjs +324 -0
  33. package/dist/replay.cjs.map +1 -0
  34. package/dist/replay.d.cts +124 -0
  35. package/dist/replay.d.ts +124 -0
  36. package/dist/replay.js +322 -0
  37. package/dist/replay.js.map +1 -0
  38. package/dist/tabs.cjs +336 -0
  39. package/dist/tabs.cjs.map +1 -0
  40. package/dist/tabs.d.cts +81 -0
  41. package/dist/tabs.d.ts +81 -0
  42. package/dist/tabs.js +335 -0
  43. package/dist/tabs.js.map +1 -0
  44. package/dist/timeline.d.cts +1 -216
  45. package/dist/types-BEecFqIB.d.cts +268 -0
  46. package/dist/webtransport.cjs +243 -0
  47. package/dist/webtransport.cjs.map +1 -0
  48. package/dist/webtransport.d.cts +67 -0
  49. package/dist/webtransport.d.ts +67 -0
  50. package/dist/webtransport.js +242 -0
  51. package/dist/webtransport.js.map +1 -0
  52. package/package.json +139 -3
@@ -0,0 +1,186 @@
1
+ 'use client';
2
+ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
3
+ let react = require("react");
4
+ //#region src/devtools.ts
5
+ const INSPECT = Symbol.for("react-websocket.inspect");
6
+ const REGISTRY = Symbol.for("react-websocket.registry");
7
+ const isClient = (value) => typeof value?.acquire === "function" && typeof value.send === "function" && "socket" in value;
8
+ const COLORS = {
9
+ idle: "#9ca3af",
10
+ connecting: "#60a5fa",
11
+ open: "#34d399",
12
+ reconnecting: "#fbbf24",
13
+ closed: "#f87171"
14
+ };
15
+ /** Every client to show, with what the sources say about it. */
16
+ function collect(sources) {
17
+ const rows = /* @__PURE__ */ new Map();
18
+ const row = (client) => {
19
+ if (!rows.has(client)) rows.set(client, {
20
+ client,
21
+ topics: [],
22
+ feeds: []
23
+ });
24
+ return rows.get(client);
25
+ };
26
+ const shared = globalThis[REGISTRY];
27
+ for (const client of shared?.values() ?? []) row(client);
28
+ for (const source of sources) {
29
+ if (isClient(source)) {
30
+ row(source);
31
+ continue;
32
+ }
33
+ if (!isClient(source?.client)) continue;
34
+ const entry = row(source.client);
35
+ if (typeof source.position === "function") for (const topic of source.active()) entry.feeds.push(`${topic} @${source.position(topic) ?? "–"}`);
36
+ else if (typeof source.active === "function") entry.topics.push(...source.active());
37
+ else if (typeof source.add === "function") entry.waiting = source.getSnapshot().length;
38
+ else if (typeof source.leave === "function") entry.peers = source.getSnapshot().length;
39
+ else if (typeof source.start === "function" && typeof source.list === "function") {
40
+ const runs = source.list();
41
+ entry.runs = `${runs.filter((run) => run.status === "streaming").length} streaming, ${runs.length} in all`;
42
+ } else if (typeof source.getSnapshot === "function" && "rtt" in source.getSnapshot()) entry.latency = source.getSnapshot();
43
+ }
44
+ return [...rows.values()];
45
+ }
46
+ const panel = {
47
+ position: "fixed",
48
+ zIndex: 2147483647,
49
+ font: "12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
50
+ color: "#e5e7eb",
51
+ background: "#111827",
52
+ border: "1px solid #374151",
53
+ borderRadius: 8,
54
+ boxShadow: "0 8px 24px rgb(0 0 0 / 0.3)"
55
+ };
56
+ const dot = (status) => ({
57
+ display: "inline-block",
58
+ width: 8,
59
+ height: 8,
60
+ borderRadius: 4,
61
+ marginRight: 6,
62
+ background: COLORS[status]
63
+ });
64
+ const noopSubscribe = () => () => {};
65
+ function Client({ row, received }) {
66
+ const { client } = row;
67
+ const { status, attempt } = client.getSnapshot();
68
+ const inspect = client[INSPECT];
69
+ const facts = [
70
+ ["status", attempt ? `${status} (attempt ${attempt})` : status],
71
+ ["queued", inspect?.().queued ?? "–"],
72
+ ["buffered", `${client.socket?.bufferedAmount ?? 0} B`],
73
+ ["received", received]
74
+ ];
75
+ if (row.latency) {
76
+ const { rtt } = row.latency;
77
+ facts.push(["rtt", rtt === void 0 ? "–" : `${Math.round(rtt)} ms`]);
78
+ }
79
+ if (row.waiting !== void 0) facts.push(["outbox", `${row.waiting} waiting`]);
80
+ if (row.peers !== void 0) facts.push(["presence", `${row.peers} peer${row.peers === 1 ? "" : "s"}`]);
81
+ if (row.runs) facts.push(["runs", row.runs]);
82
+ if (row.topics.length) facts.push(["topics", row.topics.join(", ")]);
83
+ if (row.feeds.length) facts.push(["feeds", row.feeds.join(", ")]);
84
+ const url = typeof client.url === "string" ? client.url : client.socket?.url ?? "url()";
85
+ return (0, react.createElement)("section", { style: {
86
+ padding: "8px 12px",
87
+ borderTop: "1px solid #374151"
88
+ } }, (0, react.createElement)("div", { style: {
89
+ fontWeight: 600,
90
+ wordBreak: "break-all"
91
+ } }, (0, react.createElement)("span", { style: dot(status) }), url), (0, react.createElement)("dl", { style: {
92
+ display: "grid",
93
+ gridTemplateColumns: "auto 1fr",
94
+ gap: "2px 12px",
95
+ margin: "6px 0 0"
96
+ } }, facts.flatMap(([name, value]) => [(0, react.createElement)("dt", {
97
+ key: `${name}-t`,
98
+ style: { color: "#9ca3af" }
99
+ }, name), (0, react.createElement)("dd", {
100
+ key: `${name}-d`,
101
+ style: {
102
+ margin: 0,
103
+ wordBreak: "break-all"
104
+ }
105
+ }, String(value))])));
106
+ }
107
+ /**
108
+ * A panel in a corner of the page that shows each WebSocket client: its status and attempts,
109
+ * what's queued and buffered, how many messages it received, and the round-trip time, topics,
110
+ * feeds, outbox, presence and runs of the `sources` that use it. For development: render it only
111
+ * there, for example behind `import.meta.env.DEV`.
112
+ *
113
+ * @example
114
+ * ```tsx
115
+ * import { WebSocketDevtools } from 'react-websocket/devtools';
116
+ *
117
+ * {import.meta.env.DEV && <WebSocketDevtools sources={[live, topics, latency]} />}
118
+ * ```
119
+ */
120
+ function WebSocketDevtools({ sources = [], open: initiallyOpen = false, position = "bottom-right" }) {
121
+ const [open, setOpen] = (0, react.useState)(initiallyOpen);
122
+ const [, tick] = (0, react.useReducer)((n) => n + 1, 0);
123
+ const [received] = (0, react.useState)(() => /* @__PURE__ */ new WeakMap());
124
+ const browser = (0, react.useSyncExternalStore)(noopSubscribe, () => true, () => false);
125
+ const rows = browser ? collect(sources) : [];
126
+ const clients = rows.map((row) => row.client);
127
+ (0, react.useEffect)(() => {
128
+ const timer = setInterval(tick, 500);
129
+ return () => clearInterval(timer);
130
+ }, []);
131
+ const [listening] = (0, react.useState)(() => /* @__PURE__ */ new Map());
132
+ (0, react.useEffect)(() => {
133
+ for (const client of clients) {
134
+ if (listening.has(client)) continue;
135
+ listening.set(client, client.on("message", () => received.set(client, (received.get(client) ?? 0) + 1)));
136
+ }
137
+ });
138
+ (0, react.useEffect)(() => () => {
139
+ for (const off of listening.values()) off();
140
+ listening.clear();
141
+ }, [listening]);
142
+ if (!browser) return null;
143
+ const [vertical, horizontal] = position.split("-");
144
+ const worst = [
145
+ "closed",
146
+ "reconnecting",
147
+ "connecting",
148
+ "open",
149
+ "idle"
150
+ ].find((status) => clients.some((client) => client.getSnapshot().status === status));
151
+ return (0, react.createElement)("div", { style: {
152
+ ...panel,
153
+ [vertical]: 12,
154
+ [horizontal]: 12,
155
+ maxWidth: 420
156
+ } }, (0, react.createElement)("button", {
157
+ type: "button",
158
+ "aria-expanded": open,
159
+ onClick: () => setOpen(!open),
160
+ style: {
161
+ all: "unset",
162
+ cursor: "pointer",
163
+ display: "block",
164
+ padding: "6px 12px",
165
+ fontWeight: 600
166
+ }
167
+ }, (0, react.createElement)("span", { style: dot(worst ?? "idle") }), `WebSocket · ${clients.length} client${clients.length === 1 ? "" : "s"}`), open && (0, react.createElement)("div", {
168
+ role: "region",
169
+ "aria-label": "WebSocket clients",
170
+ style: {
171
+ maxHeight: "60vh",
172
+ overflow: "auto"
173
+ }
174
+ }, rows.length ? rows.map((row, i) => (0, react.createElement)(Client, {
175
+ key: i,
176
+ row,
177
+ received: received.get(row.client) ?? 0
178
+ })) : (0, react.createElement)("p", { style: {
179
+ margin: 0,
180
+ padding: "8px 12px"
181
+ } }, "No clients yet.")));
182
+ }
183
+ //#endregion
184
+ exports.WebSocketDevtools = WebSocketDevtools;
185
+
186
+ //# sourceMappingURL=devtools.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools.cjs","names":["h","useState","useReducer","useSyncExternalStore"],"sources":["../src/devtools.ts"],"sourcesContent":["import {\n createElement as h,\n useEffect,\n useReducer,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactElement\n} from 'react';\n// Types only: the built entry imports nothing from the library at runtime.\nimport type { Latency, Status, WebSocketClient } from './core';\n\n/** Props for {@link WebSocketDevtools}. */\nexport interface WebSocketDevtoolsProps {\n /**\n * What to show: clients from `createWebSocket`, and the topics, feeds, latency trackers,\n * outboxes, presence and runs created on them, which are shown with their client. The clients\n * `useWebSocket(url)` shares are listed by themselves, in development.\n */\n sources?: readonly unknown[];\n /** Start with the panel open. @default false */\n open?: boolean;\n /** Which corner of the window it sits in. @default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n}\n\ntype AnyClient = WebSocketClient<any, any>;\n\ninterface Row {\n client: AnyClient;\n topics: string[];\n feeds: string[];\n latency?: Latency;\n waiting?: number;\n peers?: number;\n runs?: string;\n}\n\nconst INSPECT = Symbol.for('react-websocket.inspect');\nconst REGISTRY = Symbol.for('react-websocket.registry');\n\nconst isClient = (value: any): value is AnyClient =>\n typeof value?.acquire === 'function' && typeof value.send === 'function' && 'socket' in value;\n\nconst COLORS: Record<Status, string> = {\n idle: '#9ca3af',\n connecting: '#60a5fa',\n open: '#34d399',\n reconnecting: '#fbbf24',\n closed: '#f87171'\n};\n\n/** Every client to show, with what the sources say about it. */\nfunction collect(sources: readonly unknown[]): Row[] {\n const rows = new Map<AnyClient, Row>();\n const row = (client: AnyClient) => {\n if (!rows.has(client)) rows.set(client, { client, topics: [], feeds: [] });\n return rows.get(client)!;\n };\n const shared = (globalThis as Record<symbol, Map<string, AnyClient> | undefined>)[REGISTRY];\n for (const client of shared?.values() ?? []) row(client);\n for (const source of sources as any[]) {\n if (isClient(source)) {\n row(source);\n continue;\n }\n if (!isClient(source?.client)) continue;\n const entry = row(source.client);\n if (typeof source.position === 'function') {\n for (const topic of source.active())\n entry.feeds.push(`${topic} @${source.position(topic) ?? '–'}`);\n } else if (typeof source.active === 'function') entry.topics.push(...source.active());\n else if (typeof source.add === 'function') entry.waiting = source.getSnapshot().length;\n else if (typeof source.leave === 'function') entry.peers = source.getSnapshot().length;\n else if (typeof source.start === 'function' && typeof source.list === 'function') {\n const runs = source.list() as { status: string }[];\n entry.runs = `${runs.filter(run => run.status === 'streaming').length} streaming, ${runs.length} in all`;\n } else if (typeof source.getSnapshot === 'function' && 'rtt' in source.getSnapshot()) {\n entry.latency = source.getSnapshot();\n }\n }\n return [...rows.values()];\n}\n\nconst panel: CSSProperties = {\n position: 'fixed',\n zIndex: 2147483647,\n font: '12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace',\n color: '#e5e7eb',\n background: '#111827',\n border: '1px solid #374151',\n borderRadius: 8,\n boxShadow: '0 8px 24px rgb(0 0 0 / 0.3)'\n};\n\nconst dot = (status: Status): CSSProperties => ({\n display: 'inline-block',\n width: 8,\n height: 8,\n borderRadius: 4,\n marginRight: 6,\n background: COLORS[status]\n});\n\nconst noopSubscribe = () => () => {};\n\nfunction Client({ row, received }: { row: Row; received: number }) {\n const { client } = row;\n const { status, attempt } = client.getSnapshot();\n const inspect = (client as unknown as Record<symbol, (() => { queued: number }) | undefined>)[\n INSPECT\n ];\n const facts: [string, string | number][] = [\n ['status', attempt ? `${status} (attempt ${attempt})` : status],\n ['queued', inspect?.().queued ?? '–'],\n ['buffered', `${client.socket?.bufferedAmount ?? 0} B`],\n ['received', received]\n ];\n if (row.latency) {\n const { rtt } = row.latency;\n facts.push(['rtt', rtt === undefined ? '–' : `${Math.round(rtt)} ms`]);\n }\n if (row.waiting !== undefined) facts.push(['outbox', `${row.waiting} waiting`]);\n if (row.peers !== undefined) {\n facts.push(['presence', `${row.peers} peer${row.peers === 1 ? '' : 's'}`]);\n }\n if (row.runs) facts.push(['runs', row.runs]);\n if (row.topics.length) facts.push(['topics', row.topics.join(', ')]);\n if (row.feeds.length) facts.push(['feeds', row.feeds.join(', ')]);\n const url = typeof client.url === 'string' ? client.url : (client.socket?.url ?? 'url()');\n return h(\n 'section',\n { style: { padding: '8px 12px', borderTop: '1px solid #374151' } },\n h(\n 'div',\n { style: { fontWeight: 600, wordBreak: 'break-all' } },\n h('span', { style: dot(status) }),\n url\n ),\n h(\n 'dl',\n {\n style: {\n display: 'grid',\n gridTemplateColumns: 'auto 1fr',\n gap: '2px 12px',\n margin: '6px 0 0'\n }\n },\n facts.flatMap(([name, value]) => [\n h('dt', { key: `${name}-t`, style: { color: '#9ca3af' } }, name),\n h('dd', { key: `${name}-d`, style: { margin: 0, wordBreak: 'break-all' } }, String(value))\n ])\n )\n );\n}\n\n/**\n * A panel in a corner of the page that shows each WebSocket client: its status and attempts,\n * what's queued and buffered, how many messages it received, and the round-trip time, topics,\n * feeds, outbox, presence and runs of the `sources` that use it. For development: render it only\n * there, for example behind `import.meta.env.DEV`.\n *\n * @example\n * ```tsx\n * import { WebSocketDevtools } from 'react-websocket/devtools';\n *\n * {import.meta.env.DEV && <WebSocketDevtools sources={[live, topics, latency]} />}\n * ```\n */\nexport function WebSocketDevtools({\n sources = [],\n open: initiallyOpen = false,\n position = 'bottom-right'\n}: WebSocketDevtoolsProps): ReactElement | null {\n const [open, setOpen] = useState(initiallyOpen);\n const [, tick] = useReducer((n: number) => n + 1, 0);\n const [received] = useState(() => new WeakMap<AnyClient, number>());\n // Nothing on the server, so hydration matches.\n const browser = useSyncExternalStore(\n noopSubscribe,\n () => true,\n () => false\n );\n const rows = browser ? collect(sources) : [];\n const clients = rows.map(row => row.client);\n\n // Refresh twice a second: what it shows changes faster than it's worth re-rendering for.\n useEffect(() => {\n const timer = setInterval(tick, 500);\n return () => clearInterval(timer);\n }, []);\n // After every render, count the messages of clients that appeared since, such as a URL that\n // a component started using.\n const [listening] = useState(() => new Map<AnyClient, () => void>());\n useEffect(() => {\n for (const client of clients) {\n if (listening.has(client)) continue;\n listening.set(\n client,\n client.on('message', () => received.set(client, (received.get(client) ?? 0) + 1))\n );\n }\n });\n useEffect(\n () => () => {\n for (const off of listening.values()) off();\n listening.clear();\n },\n [listening]\n );\n\n if (!browser) return null;\n const [vertical, horizontal] = position.split('-') as ['top' | 'bottom', 'left' | 'right'];\n const worst = (['closed', 'reconnecting', 'connecting', 'open', 'idle'] as Status[]).find(\n status => clients.some(client => client.getSnapshot().status === status)\n );\n return h(\n 'div',\n { style: { ...panel, [vertical]: 12, [horizontal]: 12, maxWidth: 420 } },\n h(\n 'button',\n {\n type: 'button',\n 'aria-expanded': open,\n onClick: () => setOpen(!open),\n style: {\n all: 'unset',\n cursor: 'pointer',\n display: 'block',\n padding: '6px 12px',\n fontWeight: 600\n }\n },\n h('span', { style: dot(worst ?? 'idle') }),\n `WebSocket · ${clients.length} client${clients.length === 1 ? '' : 's'}`\n ),\n open &&\n h(\n 'div',\n {\n role: 'region',\n 'aria-label': 'WebSocket clients',\n style: { maxHeight: '60vh', overflow: 'auto' }\n },\n rows.length\n ? rows.map((row, i) =>\n h(Client, { key: i, row, received: received.get(row.client) ?? 0 })\n )\n : h('p', { style: { margin: 0, padding: '8px 12px' } }, 'No clients yet.')\n )\n );\n}\n"],"mappings":";;;;AAsCA,MAAM,UAAU,OAAO,IAAI,yBAAyB;AACpD,MAAM,WAAW,OAAO,IAAI,0BAA0B;AAEtD,MAAM,YAAY,UAChB,OAAO,OAAO,YAAY,cAAc,OAAO,MAAM,SAAS,cAAc,YAAY;AAE1F,MAAM,SAAiC;CACrC,MAAM;CACN,YAAY;CACZ,MAAM;CACN,cAAc;CACd,QAAQ;AACV;;AAGA,SAAS,QAAQ,SAAoC;CACnD,MAAM,uBAAO,IAAI,IAAoB;CACrC,MAAM,OAAO,WAAsB;EACjC,IAAI,CAAC,KAAK,IAAI,MAAM,GAAG,KAAK,IAAI,QAAQ;GAAE;GAAQ,QAAQ,CAAC;GAAG,OAAO,CAAC;EAAE,CAAC;EACzE,OAAO,KAAK,IAAI,MAAM;CACxB;CACA,MAAM,SAAU,WAAkE;CAClF,KAAK,MAAM,UAAU,QAAQ,OAAO,KAAK,CAAC,GAAG,IAAI,MAAM;CACvD,KAAK,MAAM,UAAU,SAAkB;EACrC,IAAI,SAAS,MAAM,GAAG;GACpB,IAAI,MAAM;GACV;EACF;EACA,IAAI,CAAC,SAAS,QAAQ,MAAM,GAAG;EAC/B,MAAM,QAAQ,IAAI,OAAO,MAAM;EAC/B,IAAI,OAAO,OAAO,aAAa,YAC7B,KAAK,MAAM,SAAS,OAAO,OAAO,GAChC,MAAM,MAAM,KAAK,GAAG,MAAM,IAAI,OAAO,SAAS,KAAK,KAAK,KAAK;OAC1D,IAAI,OAAO,OAAO,WAAW,YAAY,MAAM,OAAO,KAAK,GAAG,OAAO,OAAO,CAAC;OAC/E,IAAI,OAAO,OAAO,QAAQ,YAAY,MAAM,UAAU,OAAO,YAAY,CAAC,CAAC;OAC3E,IAAI,OAAO,OAAO,UAAU,YAAY,MAAM,QAAQ,OAAO,YAAY,CAAC,CAAC;OAC3E,IAAI,OAAO,OAAO,UAAU,cAAc,OAAO,OAAO,SAAS,YAAY;GAChF,MAAM,OAAO,OAAO,KAAK;GACzB,MAAM,OAAO,GAAG,KAAK,QAAO,QAAO,IAAI,WAAW,WAAW,CAAC,CAAC,OAAO,cAAc,KAAK,OAAO;EAClG,OAAO,IAAI,OAAO,OAAO,gBAAgB,cAAc,SAAS,OAAO,YAAY,GACjF,MAAM,UAAU,OAAO,YAAY;CAEvC;CACA,OAAO,CAAC,GAAG,KAAK,OAAO,CAAC;AAC1B;AAEA,MAAM,QAAuB;CAC3B,UAAU;CACV,QAAQ;CACR,MAAM;CACN,OAAO;CACP,YAAY;CACZ,QAAQ;CACR,cAAc;CACd,WAAW;AACb;AAEA,MAAM,OAAO,YAAmC;CAC9C,SAAS;CACT,OAAO;CACP,QAAQ;CACR,cAAc;CACd,aAAa;CACb,YAAY,OAAO;AACrB;AAEA,MAAM,4BAA4B,CAAC;AAEnC,SAAS,OAAO,EAAE,KAAK,YAA4C;CACjE,MAAM,EAAE,WAAW;CACnB,MAAM,EAAE,QAAQ,YAAY,OAAO,YAAY;CAC/C,MAAM,UAAW,OACf;CAEF,MAAM,QAAqC;EACzC,CAAC,UAAU,UAAU,GAAG,OAAO,YAAY,QAAQ,KAAK,MAAM;EAC9D,CAAC,UAAU,UAAU,CAAC,CAAC,UAAU,GAAG;EACpC,CAAC,YAAY,GAAG,OAAO,QAAQ,kBAAkB,EAAE,GAAG;EACtD,CAAC,YAAY,QAAQ;CACvB;CACA,IAAI,IAAI,SAAS;EACf,MAAM,EAAE,QAAQ,IAAI;EACpB,MAAM,KAAK,CAAC,OAAO,QAAQ,KAAA,IAAY,MAAM,GAAG,KAAK,MAAM,GAAG,EAAE,IAAI,CAAC;CACvE;CACA,IAAI,IAAI,YAAY,KAAA,GAAW,MAAM,KAAK,CAAC,UAAU,GAAG,IAAI,QAAQ,SAAS,CAAC;CAC9E,IAAI,IAAI,UAAU,KAAA,GAChB,MAAM,KAAK,CAAC,YAAY,GAAG,IAAI,MAAM,OAAO,IAAI,UAAU,IAAI,KAAK,KAAK,CAAC;CAE3E,IAAI,IAAI,MAAM,MAAM,KAAK,CAAC,QAAQ,IAAI,IAAI,CAAC;CAC3C,IAAI,IAAI,OAAO,QAAQ,MAAM,KAAK,CAAC,UAAU,IAAI,OAAO,KAAK,IAAI,CAAC,CAAC;CACnE,IAAI,IAAI,MAAM,QAAQ,MAAM,KAAK,CAAC,SAAS,IAAI,MAAM,KAAK,IAAI,CAAC,CAAC;CAChE,MAAM,MAAM,OAAO,OAAO,QAAQ,WAAW,OAAO,MAAO,OAAO,QAAQ,OAAO;CACjF,QAAA,GAAOA,MAAAA,cAAAA,CACL,WACA,EAAE,OAAO;EAAE,SAAS;EAAY,WAAW;CAAoB,EAAE,IAAA,GACjEA,MAAAA,cAAAA,CACE,OACA,EAAE,OAAO;EAAE,YAAY;EAAK,WAAW;CAAY,EAAE,IAAA,GACrDA,MAAAA,cAAAA,CAAE,QAAQ,EAAE,OAAO,IAAI,MAAM,EAAE,CAAC,GAChC,GACF,IAAA,GACAA,MAAAA,cAAAA,CACE,MACA,EACE,OAAO;EACL,SAAS;EACT,qBAAqB;EACrB,KAAK;EACL,QAAQ;CACV,EACF,GACA,MAAM,SAAS,CAAC,MAAM,WAAW,EAAA,GAC/BA,MAAAA,cAAAA,CAAE,MAAM;EAAE,KAAK,GAAG,KAAK;EAAK,OAAO,EAAE,OAAO,UAAU;CAAE,GAAG,IAAI,IAAA,GAC/DA,MAAAA,cAAAA,CAAE,MAAM;EAAE,KAAK,GAAG,KAAK;EAAK,OAAO;GAAE,QAAQ;GAAG,WAAW;EAAY;CAAE,GAAG,OAAO,KAAK,CAAC,CAC3F,CAAC,CACH,CACF;AACF;;;;;;;;;;;;;;AAeA,SAAgB,kBAAkB,EAChC,UAAU,CAAC,GACX,MAAM,gBAAgB,OACtB,WAAW,kBACmC;CAC9C,MAAM,CAAC,MAAM,YAAA,GAAWC,MAAAA,SAAAA,CAAS,aAAa;CAC9C,MAAM,GAAG,SAAA,GAAQC,MAAAA,WAAAA,EAAY,MAAc,IAAI,GAAG,CAAC;CACnD,MAAM,CAAC,aAAA,GAAYD,MAAAA,SAAAA,uBAAe,IAAI,QAA2B,CAAC;CAElE,MAAM,WAAA,GAAUE,MAAAA,qBAAAA,CACd,qBACM,YACA,KACR;CACA,MAAM,OAAO,UAAU,QAAQ,OAAO,IAAI,CAAC;CAC3C,MAAM,UAAU,KAAK,KAAI,QAAO,IAAI,MAAM;CAG1C,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,MAAM,QAAQ,YAAY,MAAM,GAAG;EACnC,aAAa,cAAc,KAAK;CAClC,GAAG,CAAC,CAAC;CAGL,MAAM,CAAC,cAAA,GAAaF,MAAAA,SAAAA,uBAAe,IAAI,IAA2B,CAAC;CACnE,CAAA,GAAA,MAAA,UAAA,OAAgB;EACd,KAAK,MAAM,UAAU,SAAS;GAC5B,IAAI,UAAU,IAAI,MAAM,GAAG;GAC3B,UAAU,IACR,QACA,OAAO,GAAG,iBAAiB,SAAS,IAAI,SAAS,SAAS,IAAI,MAAM,KAAK,KAAK,CAAC,CAAC,CAClF;EACF;CACF,CAAC;CACD,CAAA,GAAA,MAAA,UAAA,aACc;EACV,KAAK,MAAM,OAAO,UAAU,OAAO,GAAG,IAAI;EAC1C,UAAU,MAAM;CAClB,GACA,CAAC,SAAS,CACZ;CAEA,IAAI,CAAC,SAAS,OAAO;CACrB,MAAM,CAAC,UAAU,cAAc,SAAS,MAAM,GAAG;CACjD,MAAM,QAAS;EAAC;EAAU;EAAgB;EAAc;EAAQ;CAAM,CAAC,CAAc,MACnF,WAAU,QAAQ,MAAK,WAAU,OAAO,YAAY,CAAC,CAAC,WAAW,MAAM,CACzE;CACA,QAAA,GAAOD,MAAAA,cAAAA,CACL,OACA,EAAE,OAAO;EAAE,GAAG;GAAQ,WAAW;GAAK,aAAa;EAAI,UAAU;CAAI,EAAE,IAAA,GACvEA,MAAAA,cAAAA,CACE,UACA;EACE,MAAM;EACN,iBAAiB;EACjB,eAAe,QAAQ,CAAC,IAAI;EAC5B,OAAO;GACL,KAAK;GACL,QAAQ;GACR,SAAS;GACT,SAAS;GACT,YAAY;EACd;CACF,IAAA,GACAA,MAAAA,cAAAA,CAAE,QAAQ,EAAE,OAAO,IAAI,SAAS,MAAM,EAAE,CAAC,GACzC,eAAe,QAAQ,OAAO,SAAS,QAAQ,WAAW,IAAI,KAAK,KACrE,GACA,SAAA,GACEA,MAAAA,cAAAA,CACE,OACA;EACE,MAAM;EACN,cAAc;EACd,OAAO;GAAE,WAAW;GAAQ,UAAU;EAAO;CAC/C,GACA,KAAK,SACD,KAAK,KAAK,KAAK,OAAA,GACbA,MAAAA,cAAAA,CAAE,QAAQ;EAAE,KAAK;EAAG;EAAK,UAAU,SAAS,IAAI,IAAI,MAAM,KAAK;CAAE,CAAC,CACpE,KAAA,GACAA,MAAAA,cAAAA,CAAE,KAAK,EAAE,OAAO;EAAE,QAAQ;EAAG,SAAS;CAAW,EAAE,GAAG,iBAAiB,CAC7E,CACJ;AACF"}
@@ -0,0 +1,32 @@
1
+
2
+ import { ReactElement } from "react";
3
+ //#region src/devtools.d.ts
4
+ /** Props for {@link WebSocketDevtools}. */
5
+ export interface WebSocketDevtoolsProps {
6
+ /**
7
+ * What to show: clients from `createWebSocket`, and the topics, feeds, latency trackers,
8
+ * outboxes, presence and runs created on them, which are shown with their client. The clients
9
+ * `useWebSocket(url)` shares are listed by themselves, in development.
10
+ */
11
+ sources?: readonly unknown[];
12
+ /** Start with the panel open. @default false */
13
+ open?: boolean;
14
+ /** Which corner of the window it sits in. @default 'bottom-right' */
15
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
16
+ }
17
+ /**
18
+ * A panel in a corner of the page that shows each WebSocket client: its status and attempts,
19
+ * what's queued and buffered, how many messages it received, and the round-trip time, topics,
20
+ * feeds, outbox, presence and runs of the `sources` that use it. For development: render it only
21
+ * there, for example behind `import.meta.env.DEV`.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { WebSocketDevtools } from 'react-websocket/devtools';
26
+ *
27
+ * {import.meta.env.DEV && <WebSocketDevtools sources={[live, topics, latency]} />}
28
+ * ```
29
+ */
30
+ export declare function WebSocketDevtools({ sources, open: initiallyOpen, position }: WebSocketDevtoolsProps): ReactElement | null;
31
+ //#endregion
32
+ //# sourceMappingURL=devtools.d.cts.map
@@ -0,0 +1,32 @@
1
+
2
+ import { ReactElement } from "react";
3
+ //#region src/devtools.d.ts
4
+ /** Props for {@link WebSocketDevtools}. */
5
+ export interface WebSocketDevtoolsProps {
6
+ /**
7
+ * What to show: clients from `createWebSocket`, and the topics, feeds, latency trackers,
8
+ * outboxes, presence and runs created on them, which are shown with their client. The clients
9
+ * `useWebSocket(url)` shares are listed by themselves, in development.
10
+ */
11
+ sources?: readonly unknown[];
12
+ /** Start with the panel open. @default false */
13
+ open?: boolean;
14
+ /** Which corner of the window it sits in. @default 'bottom-right' */
15
+ position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';
16
+ }
17
+ /**
18
+ * A panel in a corner of the page that shows each WebSocket client: its status and attempts,
19
+ * what's queued and buffered, how many messages it received, and the round-trip time, topics,
20
+ * feeds, outbox, presence and runs of the `sources` that use it. For development: render it only
21
+ * there, for example behind `import.meta.env.DEV`.
22
+ *
23
+ * @example
24
+ * ```tsx
25
+ * import { WebSocketDevtools } from 'react-websocket/devtools';
26
+ *
27
+ * {import.meta.env.DEV && <WebSocketDevtools sources={[live, topics, latency]} />}
28
+ * ```
29
+ */
30
+ export declare function WebSocketDevtools({ sources, open: initiallyOpen, position }: WebSocketDevtoolsProps): ReactElement | null;
31
+ //#endregion
32
+ //# sourceMappingURL=devtools.d.ts.map
@@ -0,0 +1,185 @@
1
+ 'use client';
2
+ import { createElement, useEffect, useReducer, useState, useSyncExternalStore } from "react";
3
+ //#region src/devtools.ts
4
+ const INSPECT = Symbol.for("react-websocket.inspect");
5
+ const REGISTRY = Symbol.for("react-websocket.registry");
6
+ const isClient = (value) => typeof value?.acquire === "function" && typeof value.send === "function" && "socket" in value;
7
+ const COLORS = {
8
+ idle: "#9ca3af",
9
+ connecting: "#60a5fa",
10
+ open: "#34d399",
11
+ reconnecting: "#fbbf24",
12
+ closed: "#f87171"
13
+ };
14
+ /** Every client to show, with what the sources say about it. */
15
+ function collect(sources) {
16
+ const rows = /* @__PURE__ */ new Map();
17
+ const row = (client) => {
18
+ if (!rows.has(client)) rows.set(client, {
19
+ client,
20
+ topics: [],
21
+ feeds: []
22
+ });
23
+ return rows.get(client);
24
+ };
25
+ const shared = globalThis[REGISTRY];
26
+ for (const client of shared?.values() ?? []) row(client);
27
+ for (const source of sources) {
28
+ if (isClient(source)) {
29
+ row(source);
30
+ continue;
31
+ }
32
+ if (!isClient(source?.client)) continue;
33
+ const entry = row(source.client);
34
+ if (typeof source.position === "function") for (const topic of source.active()) entry.feeds.push(`${topic} @${source.position(topic) ?? "–"}`);
35
+ else if (typeof source.active === "function") entry.topics.push(...source.active());
36
+ else if (typeof source.add === "function") entry.waiting = source.getSnapshot().length;
37
+ else if (typeof source.leave === "function") entry.peers = source.getSnapshot().length;
38
+ else if (typeof source.start === "function" && typeof source.list === "function") {
39
+ const runs = source.list();
40
+ entry.runs = `${runs.filter((run) => run.status === "streaming").length} streaming, ${runs.length} in all`;
41
+ } else if (typeof source.getSnapshot === "function" && "rtt" in source.getSnapshot()) entry.latency = source.getSnapshot();
42
+ }
43
+ return [...rows.values()];
44
+ }
45
+ const panel = {
46
+ position: "fixed",
47
+ zIndex: 2147483647,
48
+ font: "12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace",
49
+ color: "#e5e7eb",
50
+ background: "#111827",
51
+ border: "1px solid #374151",
52
+ borderRadius: 8,
53
+ boxShadow: "0 8px 24px rgb(0 0 0 / 0.3)"
54
+ };
55
+ const dot = (status) => ({
56
+ display: "inline-block",
57
+ width: 8,
58
+ height: 8,
59
+ borderRadius: 4,
60
+ marginRight: 6,
61
+ background: COLORS[status]
62
+ });
63
+ const noopSubscribe = () => () => {};
64
+ function Client({ row, received }) {
65
+ const { client } = row;
66
+ const { status, attempt } = client.getSnapshot();
67
+ const inspect = client[INSPECT];
68
+ const facts = [
69
+ ["status", attempt ? `${status} (attempt ${attempt})` : status],
70
+ ["queued", inspect?.().queued ?? "–"],
71
+ ["buffered", `${client.socket?.bufferedAmount ?? 0} B`],
72
+ ["received", received]
73
+ ];
74
+ if (row.latency) {
75
+ const { rtt } = row.latency;
76
+ facts.push(["rtt", rtt === void 0 ? "–" : `${Math.round(rtt)} ms`]);
77
+ }
78
+ if (row.waiting !== void 0) facts.push(["outbox", `${row.waiting} waiting`]);
79
+ if (row.peers !== void 0) facts.push(["presence", `${row.peers} peer${row.peers === 1 ? "" : "s"}`]);
80
+ if (row.runs) facts.push(["runs", row.runs]);
81
+ if (row.topics.length) facts.push(["topics", row.topics.join(", ")]);
82
+ if (row.feeds.length) facts.push(["feeds", row.feeds.join(", ")]);
83
+ const url = typeof client.url === "string" ? client.url : client.socket?.url ?? "url()";
84
+ return createElement("section", { style: {
85
+ padding: "8px 12px",
86
+ borderTop: "1px solid #374151"
87
+ } }, createElement("div", { style: {
88
+ fontWeight: 600,
89
+ wordBreak: "break-all"
90
+ } }, createElement("span", { style: dot(status) }), url), createElement("dl", { style: {
91
+ display: "grid",
92
+ gridTemplateColumns: "auto 1fr",
93
+ gap: "2px 12px",
94
+ margin: "6px 0 0"
95
+ } }, facts.flatMap(([name, value]) => [createElement("dt", {
96
+ key: `${name}-t`,
97
+ style: { color: "#9ca3af" }
98
+ }, name), createElement("dd", {
99
+ key: `${name}-d`,
100
+ style: {
101
+ margin: 0,
102
+ wordBreak: "break-all"
103
+ }
104
+ }, String(value))])));
105
+ }
106
+ /**
107
+ * A panel in a corner of the page that shows each WebSocket client: its status and attempts,
108
+ * what's queued and buffered, how many messages it received, and the round-trip time, topics,
109
+ * feeds, outbox, presence and runs of the `sources` that use it. For development: render it only
110
+ * there, for example behind `import.meta.env.DEV`.
111
+ *
112
+ * @example
113
+ * ```tsx
114
+ * import { WebSocketDevtools } from 'react-websocket/devtools';
115
+ *
116
+ * {import.meta.env.DEV && <WebSocketDevtools sources={[live, topics, latency]} />}
117
+ * ```
118
+ */
119
+ function WebSocketDevtools({ sources = [], open: initiallyOpen = false, position = "bottom-right" }) {
120
+ const [open, setOpen] = useState(initiallyOpen);
121
+ const [, tick] = useReducer((n) => n + 1, 0);
122
+ const [received] = useState(() => /* @__PURE__ */ new WeakMap());
123
+ const browser = useSyncExternalStore(noopSubscribe, () => true, () => false);
124
+ const rows = browser ? collect(sources) : [];
125
+ const clients = rows.map((row) => row.client);
126
+ useEffect(() => {
127
+ const timer = setInterval(tick, 500);
128
+ return () => clearInterval(timer);
129
+ }, []);
130
+ const [listening] = useState(() => /* @__PURE__ */ new Map());
131
+ useEffect(() => {
132
+ for (const client of clients) {
133
+ if (listening.has(client)) continue;
134
+ listening.set(client, client.on("message", () => received.set(client, (received.get(client) ?? 0) + 1)));
135
+ }
136
+ });
137
+ useEffect(() => () => {
138
+ for (const off of listening.values()) off();
139
+ listening.clear();
140
+ }, [listening]);
141
+ if (!browser) return null;
142
+ const [vertical, horizontal] = position.split("-");
143
+ const worst = [
144
+ "closed",
145
+ "reconnecting",
146
+ "connecting",
147
+ "open",
148
+ "idle"
149
+ ].find((status) => clients.some((client) => client.getSnapshot().status === status));
150
+ return createElement("div", { style: {
151
+ ...panel,
152
+ [vertical]: 12,
153
+ [horizontal]: 12,
154
+ maxWidth: 420
155
+ } }, createElement("button", {
156
+ type: "button",
157
+ "aria-expanded": open,
158
+ onClick: () => setOpen(!open),
159
+ style: {
160
+ all: "unset",
161
+ cursor: "pointer",
162
+ display: "block",
163
+ padding: "6px 12px",
164
+ fontWeight: 600
165
+ }
166
+ }, createElement("span", { style: dot(worst ?? "idle") }), `WebSocket · ${clients.length} client${clients.length === 1 ? "" : "s"}`), open && createElement("div", {
167
+ role: "region",
168
+ "aria-label": "WebSocket clients",
169
+ style: {
170
+ maxHeight: "60vh",
171
+ overflow: "auto"
172
+ }
173
+ }, rows.length ? rows.map((row, i) => createElement(Client, {
174
+ key: i,
175
+ row,
176
+ received: received.get(row.client) ?? 0
177
+ })) : createElement("p", { style: {
178
+ margin: 0,
179
+ padding: "8px 12px"
180
+ } }, "No clients yet.")));
181
+ }
182
+ //#endregion
183
+ export { WebSocketDevtools };
184
+
185
+ //# sourceMappingURL=devtools.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"devtools.js","names":["h"],"sources":["../src/devtools.ts"],"sourcesContent":["import {\n createElement as h,\n useEffect,\n useReducer,\n useState,\n useSyncExternalStore,\n type CSSProperties,\n type ReactElement\n} from 'react';\n// Types only: the built entry imports nothing from the library at runtime.\nimport type { Latency, Status, WebSocketClient } from './core';\n\n/** Props for {@link WebSocketDevtools}. */\nexport interface WebSocketDevtoolsProps {\n /**\n * What to show: clients from `createWebSocket`, and the topics, feeds, latency trackers,\n * outboxes, presence and runs created on them, which are shown with their client. The clients\n * `useWebSocket(url)` shares are listed by themselves, in development.\n */\n sources?: readonly unknown[];\n /** Start with the panel open. @default false */\n open?: boolean;\n /** Which corner of the window it sits in. @default 'bottom-right' */\n position?: 'bottom-right' | 'bottom-left' | 'top-right' | 'top-left';\n}\n\ntype AnyClient = WebSocketClient<any, any>;\n\ninterface Row {\n client: AnyClient;\n topics: string[];\n feeds: string[];\n latency?: Latency;\n waiting?: number;\n peers?: number;\n runs?: string;\n}\n\nconst INSPECT = Symbol.for('react-websocket.inspect');\nconst REGISTRY = Symbol.for('react-websocket.registry');\n\nconst isClient = (value: any): value is AnyClient =>\n typeof value?.acquire === 'function' && typeof value.send === 'function' && 'socket' in value;\n\nconst COLORS: Record<Status, string> = {\n idle: '#9ca3af',\n connecting: '#60a5fa',\n open: '#34d399',\n reconnecting: '#fbbf24',\n closed: '#f87171'\n};\n\n/** Every client to show, with what the sources say about it. */\nfunction collect(sources: readonly unknown[]): Row[] {\n const rows = new Map<AnyClient, Row>();\n const row = (client: AnyClient) => {\n if (!rows.has(client)) rows.set(client, { client, topics: [], feeds: [] });\n return rows.get(client)!;\n };\n const shared = (globalThis as Record<symbol, Map<string, AnyClient> | undefined>)[REGISTRY];\n for (const client of shared?.values() ?? []) row(client);\n for (const source of sources as any[]) {\n if (isClient(source)) {\n row(source);\n continue;\n }\n if (!isClient(source?.client)) continue;\n const entry = row(source.client);\n if (typeof source.position === 'function') {\n for (const topic of source.active())\n entry.feeds.push(`${topic} @${source.position(topic) ?? '–'}`);\n } else if (typeof source.active === 'function') entry.topics.push(...source.active());\n else if (typeof source.add === 'function') entry.waiting = source.getSnapshot().length;\n else if (typeof source.leave === 'function') entry.peers = source.getSnapshot().length;\n else if (typeof source.start === 'function' && typeof source.list === 'function') {\n const runs = source.list() as { status: string }[];\n entry.runs = `${runs.filter(run => run.status === 'streaming').length} streaming, ${runs.length} in all`;\n } else if (typeof source.getSnapshot === 'function' && 'rtt' in source.getSnapshot()) {\n entry.latency = source.getSnapshot();\n }\n }\n return [...rows.values()];\n}\n\nconst panel: CSSProperties = {\n position: 'fixed',\n zIndex: 2147483647,\n font: '12px/1.4 ui-monospace, SFMono-Regular, Menlo, monospace',\n color: '#e5e7eb',\n background: '#111827',\n border: '1px solid #374151',\n borderRadius: 8,\n boxShadow: '0 8px 24px rgb(0 0 0 / 0.3)'\n};\n\nconst dot = (status: Status): CSSProperties => ({\n display: 'inline-block',\n width: 8,\n height: 8,\n borderRadius: 4,\n marginRight: 6,\n background: COLORS[status]\n});\n\nconst noopSubscribe = () => () => {};\n\nfunction Client({ row, received }: { row: Row; received: number }) {\n const { client } = row;\n const { status, attempt } = client.getSnapshot();\n const inspect = (client as unknown as Record<symbol, (() => { queued: number }) | undefined>)[\n INSPECT\n ];\n const facts: [string, string | number][] = [\n ['status', attempt ? `${status} (attempt ${attempt})` : status],\n ['queued', inspect?.().queued ?? '–'],\n ['buffered', `${client.socket?.bufferedAmount ?? 0} B`],\n ['received', received]\n ];\n if (row.latency) {\n const { rtt } = row.latency;\n facts.push(['rtt', rtt === undefined ? '–' : `${Math.round(rtt)} ms`]);\n }\n if (row.waiting !== undefined) facts.push(['outbox', `${row.waiting} waiting`]);\n if (row.peers !== undefined) {\n facts.push(['presence', `${row.peers} peer${row.peers === 1 ? '' : 's'}`]);\n }\n if (row.runs) facts.push(['runs', row.runs]);\n if (row.topics.length) facts.push(['topics', row.topics.join(', ')]);\n if (row.feeds.length) facts.push(['feeds', row.feeds.join(', ')]);\n const url = typeof client.url === 'string' ? client.url : (client.socket?.url ?? 'url()');\n return h(\n 'section',\n { style: { padding: '8px 12px', borderTop: '1px solid #374151' } },\n h(\n 'div',\n { style: { fontWeight: 600, wordBreak: 'break-all' } },\n h('span', { style: dot(status) }),\n url\n ),\n h(\n 'dl',\n {\n style: {\n display: 'grid',\n gridTemplateColumns: 'auto 1fr',\n gap: '2px 12px',\n margin: '6px 0 0'\n }\n },\n facts.flatMap(([name, value]) => [\n h('dt', { key: `${name}-t`, style: { color: '#9ca3af' } }, name),\n h('dd', { key: `${name}-d`, style: { margin: 0, wordBreak: 'break-all' } }, String(value))\n ])\n )\n );\n}\n\n/**\n * A panel in a corner of the page that shows each WebSocket client: its status and attempts,\n * what's queued and buffered, how many messages it received, and the round-trip time, topics,\n * feeds, outbox, presence and runs of the `sources` that use it. For development: render it only\n * there, for example behind `import.meta.env.DEV`.\n *\n * @example\n * ```tsx\n * import { WebSocketDevtools } from 'react-websocket/devtools';\n *\n * {import.meta.env.DEV && <WebSocketDevtools sources={[live, topics, latency]} />}\n * ```\n */\nexport function WebSocketDevtools({\n sources = [],\n open: initiallyOpen = false,\n position = 'bottom-right'\n}: WebSocketDevtoolsProps): ReactElement | null {\n const [open, setOpen] = useState(initiallyOpen);\n const [, tick] = useReducer((n: number) => n + 1, 0);\n const [received] = useState(() => new WeakMap<AnyClient, number>());\n // Nothing on the server, so hydration matches.\n const browser = useSyncExternalStore(\n noopSubscribe,\n () => true,\n () => false\n );\n const rows = browser ? collect(sources) : [];\n const clients = rows.map(row => row.client);\n\n // Refresh twice a second: what it shows changes faster than it's worth re-rendering for.\n useEffect(() => {\n const timer = setInterval(tick, 500);\n return () => clearInterval(timer);\n }, []);\n // After every render, count the messages of clients that appeared since, such as a URL that\n // a component started using.\n const [listening] = useState(() => new Map<AnyClient, () => void>());\n useEffect(() => {\n for (const client of clients) {\n if (listening.has(client)) continue;\n listening.set(\n client,\n client.on('message', () => received.set(client, (received.get(client) ?? 0) + 1))\n );\n }\n });\n useEffect(\n () => () => {\n for (const off of listening.values()) off();\n listening.clear();\n },\n [listening]\n );\n\n if (!browser) return null;\n const [vertical, horizontal] = position.split('-') as ['top' | 'bottom', 'left' | 'right'];\n const worst = (['closed', 'reconnecting', 'connecting', 'open', 'idle'] as Status[]).find(\n status => clients.some(client => client.getSnapshot().status === status)\n );\n return h(\n 'div',\n { style: { ...panel, [vertical]: 12, [horizontal]: 12, maxWidth: 420 } },\n h(\n 'button',\n {\n type: 'button',\n 'aria-expanded': open,\n onClick: () => setOpen(!open),\n style: {\n all: 'unset',\n cursor: 'pointer',\n display: 'block',\n padding: '6px 12px',\n fontWeight: 600\n }\n },\n h('span', { style: dot(worst ?? 'idle') }),\n `WebSocket · ${clients.length} client${clients.length === 1 ? '' : 's'}`\n ),\n open &&\n h(\n 'div',\n {\n role: 'region',\n 'aria-label': 'WebSocket clients',\n style: { maxHeight: '60vh', overflow: 'auto' }\n },\n rows.length\n ? rows.map((row, i) =>\n h(Client, { key: i, row, received: received.get(row.client) ?? 0 })\n )\n : h('p', { style: { margin: 0, padding: '8px 12px' } }, 'No clients yet.')\n )\n );\n}\n"],"mappings":"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"}