@yoltra/devtools-ui 0.2.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.es.md ADDED
@@ -0,0 +1,224 @@
1
+ ![Yoltra logo](../../assets/yoltra-logo.png)
2
+
3
+ # @yoltra/devtools-ui
4
+
5
+ > 👉 🇲🇽 Versión en Español | [🇺🇸 English Version](./README.md) 
6
+
7
+ **Shared React hooks and business logic for Yoltra DevTools UIs.**
8
+
9
+ `@yoltra/devtools-ui` is a headless logic layer that provides React hooks for connecting to the
10
+ DevTools hub, tracking store state, browsing events, and controlling time travel. It contains
11
+ **no UI components** — rendering is handled by downstream packages like
12
+ `@yoltra/devtools-storeview` (React DOM) and `@yoltra/devtools-cli` (Ink).
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @yoltra/devtools-ui
20
+ ```
21
+
22
+ **Peer dependency:** `react` ^18
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ Wrap your DevTools UI in a `HubProvider` and use the hooks:
29
+
30
+ ```tsx
31
+ import {
32
+ HubProvider,
33
+ useHubConnection,
34
+ useStoreRegistry,
35
+ useEventLog,
36
+ useStoreState,
37
+ } from "@yoltra/devtools-ui";
38
+
39
+ function App() {
40
+ return (
41
+ <HubProvider config={{ port: 9800, extensionName: "My Panel" }}>
42
+ <Dashboard />
43
+ </HubProvider>
44
+ );
45
+ }
46
+
47
+ function Dashboard() {
48
+ const { status } = useHubConnection();
49
+ const stores = useStoreRegistry();
50
+ const storeId = stores[0]?.id ?? null;
51
+
52
+ const { entries } = useEventLog(storeId);
53
+ const { state, loading, refresh } = useStoreState(storeId);
54
+
55
+ if (status !== "connected") return <p>Connecting...</p>;
56
+ if (!storeId) return <p>Waiting for stores...</p>;
57
+
58
+ return (
59
+ <div>
60
+ <h2>Events: {entries.length}</h2>
61
+ <pre>{JSON.stringify(state, null, 2)}</pre>
62
+ <button onClick={refresh}>Refresh State</button>
63
+ </div>
64
+ );
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Hooks
71
+
72
+ ### Connection & Registry
73
+
74
+ | Hook | Description |
75
+ | -------------------- | ------------------------------------------------------------------------- |
76
+ | `useHubConnection()` | Connection status, `send()`, `subscribe()`, `disconnect()`, `reconnect()` |
77
+ | `useStoreRegistry()` | Live list of connected stores with capabilities |
78
+
79
+ ### Data
80
+
81
+ | Hook | Description |
82
+ | -------------------------------- | --------------------------------------------------------------- |
83
+ | `useEventLog(storeId)` | Chronological event log with `clear()` |
84
+ | `useStoreState(storeId)` | Live state tree, incrementally patched via JSON Patches |
85
+ | `useStoreSubscriptions(storeId)` | Reducer/effect/middleware inventory |
86
+ | `useStoreMetrics(storeId)` | Performance counters (event rate, processing time, queue depth) |
87
+
88
+ ### Actions
89
+
90
+ | Hook | Description |
91
+ | --------------------------------- | --------------------------------------------------- |
92
+ | `useTimeTravel(storeId, entries)` | Jump to any event index, step forward/back, resume |
93
+ | `useEventReplay(storeId)` | Replay events through reducers without side effects |
94
+ | `useEventEmitter(storeId)` | Emit synthetic events to a store |
95
+
96
+ ---
97
+
98
+ ## Context
99
+
100
+ ### `HubProvider`
101
+
102
+ Wraps child components in a WebSocket connection context:
103
+
104
+ ```tsx
105
+ <HubProvider
106
+ config={{
107
+ port: 9800,
108
+ host: "localhost",
109
+ extensionName: "My DevTools",
110
+ autoReconnect: true,
111
+ maxReconnectAttempts: 10,
112
+ }}
113
+ >
114
+ {children}
115
+ </HubProvider>
116
+ ```
117
+
118
+ ### `HubConnectionConfig`
119
+
120
+ ```typescript
121
+ interface HubConnectionConfig {
122
+ port: number;
123
+ host?: string; // default: "localhost"
124
+ extensionName?: string; // display name for this extension
125
+ autoReconnect?: boolean; // default: true
126
+ maxReconnectAttempts?: number; // default: Infinity
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## State Synchronization
133
+
134
+ `useStoreState` uses an efficient incremental patching strategy:
135
+
136
+ 1. Requests a full `STATE_SNAPSHOT` on mount
137
+ 2. Buffers any `STORE_EVENT` patches that arrive before the snapshot
138
+ 3. Replays buffered patches in version order once the snapshot lands
139
+ 4. Applies subsequent patches incrementally via `applyPatches`
140
+
141
+ This means the UI always reflects the latest store state without repeated full snapshots.
142
+
143
+ ---
144
+
145
+ ## Time Travel
146
+
147
+ ```tsx
148
+ function TimeTravelControls({ storeId, entries }) {
149
+ const { currentIndex, isTimeTraveling, jumpTo, stepBack, stepForward, resume } =
150
+ useTimeTravel(storeId, entries);
151
+
152
+ return (
153
+ <div>
154
+ <button onClick={stepBack} disabled={currentIndex <= 0}>
155
+ Back
156
+ </button>
157
+ <span>
158
+ {currentIndex + 1} / {entries.length}
159
+ </span>
160
+ <button onClick={stepForward} disabled={currentIndex >= entries.length - 1}>
161
+ Forward
162
+ </button>
163
+ {isTimeTraveling && <button onClick={resume}>Resume</button>}
164
+ </div>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## API Reference
172
+
173
+ ### Context
174
+
175
+ | Export | Description |
176
+ | ------------- | ------------------------------------------------ |
177
+ | `HubProvider` | React context provider wrapping a hub connection |
178
+ | `HubContext` | The raw React context (for advanced use) |
179
+
180
+ ### Hooks
181
+
182
+ | Export | Returns |
183
+ | --------------------------------- | -------------------------------------------------------------------------- |
184
+ | `useHubConnection()` | `{ status, send, subscribe, disconnect, reconnect }` |
185
+ | `useStoreRegistry()` | `RegisteredStore[]` |
186
+ | `useEventLog(storeId)` | `{ entries, clear }` |
187
+ | `useStoreState(storeId)` | `{ state, version, loading, refresh }` |
188
+ | `useStoreSubscriptions(storeId)` | `{ data, loading }` |
189
+ | `useStoreMetrics(storeId)` | `{ metrics, loading }` |
190
+ | `useTimeTravel(storeId, entries)` | `{ currentIndex, isTimeTraveling, jumpTo, stepBack, stepForward, resume }` |
191
+ | `useEventReplay(storeId)` | `{ replay }` |
192
+ | `useEventEmitter(storeId)` | `{ emit }` |
193
+
194
+ ### Utilities
195
+
196
+ | Export | Description |
197
+ | ------------------------------ | ------------------------------------------- |
198
+ | `applyPatches(state, patches)` | Apply RFC 6902 JSON Patches to a state tree |
199
+
200
+ ### Types
201
+
202
+ | Export | Description |
203
+ | --------------------- | ----------------------------------------------- |
204
+ | `HubConnectionConfig` | Provider configuration |
205
+ | `HubConnectionStatus` | `"disconnected" \| "connecting" \| "connected"` |
206
+ | `HubContextValue` | Full context value shape |
207
+ | `RegisteredStore` | Store entry from the registry |
208
+ | `EventLogEntry` | Single event in the log |
209
+
210
+ ---
211
+
212
+ ## Related Packages
213
+
214
+ - **[@yoltra/devtools-protocol](../devtools-protocol/README.md)** — Wire format consumed by
215
+ these hooks
216
+ - **[@yoltra/devtools-storeview](../devtools-storeview/README.md)** — React DOM UI built on
217
+ these hooks
218
+ - **[@yoltra/devtools-server](../devtools-server/README.md)** — The hub these hooks connect to
219
+
220
+ ---
221
+
222
+ ## License
223
+
224
+ **MIT** — Free to use in commercial and open-source projects.
package/README.md ADDED
@@ -0,0 +1,224 @@
1
+ ![Yoltra logo](../../assets/yoltra-logo.png)
2
+
3
+ # @yoltra/devtools-ui
4
+
5
+ > [ 🇲🇽 Versión en Español](./README.es.md)&nbsp; | 👉 🇺🇸 English Version &nbsp;
6
+
7
+ **Shared React hooks and business logic for Yoltra DevTools UIs.**
8
+
9
+ `@yoltra/devtools-ui` is a headless logic layer that provides React hooks for connecting to the
10
+ Yoltra DevTools hub, tracking store state, browsing events, and controlling time travel. It
11
+ contains **no UI components** — rendering is handled by downstream packages like
12
+ `@yoltra/devtools-storeview` (React DOM) and `@yoltra/devtools-cli` (Ink).
13
+
14
+ ---
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ npm install @yoltra/devtools-ui
20
+ ```
21
+
22
+ **Peer dependency:** `react` ^18
23
+
24
+ ---
25
+
26
+ ## Quick Start
27
+
28
+ Wrap your DevTools UI in a `HubProvider` and use the hooks:
29
+
30
+ ```tsx
31
+ import {
32
+ HubProvider,
33
+ useHubConnection,
34
+ useStoreRegistry,
35
+ useEventLog,
36
+ useStoreState,
37
+ } from "@yoltra/devtools-ui";
38
+
39
+ function App() {
40
+ return (
41
+ <HubProvider config={{ port: 9800, extensionName: "My Panel" }}>
42
+ <Dashboard />
43
+ </HubProvider>
44
+ );
45
+ }
46
+
47
+ function Dashboard() {
48
+ const { status } = useHubConnection();
49
+ const stores = useStoreRegistry();
50
+ const storeId = stores[0]?.id ?? null;
51
+
52
+ const { entries } = useEventLog(storeId);
53
+ const { state, loading, refresh } = useStoreState(storeId);
54
+
55
+ if (status !== "connected") return <p>Connecting...</p>;
56
+ if (!storeId) return <p>Waiting for stores...</p>;
57
+
58
+ return (
59
+ <div>
60
+ <h2>Events: {entries.length}</h2>
61
+ <pre>{JSON.stringify(state, null, 2)}</pre>
62
+ <button onClick={refresh}>Refresh State</button>
63
+ </div>
64
+ );
65
+ }
66
+ ```
67
+
68
+ ---
69
+
70
+ ## Hooks
71
+
72
+ ### Connection & Registry
73
+
74
+ | Hook | Description |
75
+ | -------------------- | ------------------------------------------------------------------------- |
76
+ | `useHubConnection()` | Connection status, `send()`, `subscribe()`, `disconnect()`, `reconnect()` |
77
+ | `useStoreRegistry()` | Live list of connected stores with capabilities |
78
+
79
+ ### Data
80
+
81
+ | Hook | Description |
82
+ | -------------------------------- | --------------------------------------------------------------- |
83
+ | `useEventLog(storeId)` | Chronological event log with `clear()` |
84
+ | `useStoreState(storeId)` | Live state tree, incrementally patched via JSON Patches |
85
+ | `useStoreSubscriptions(storeId)` | Reducer/effect/middleware inventory |
86
+ | `useStoreMetrics(storeId)` | Performance counters (event rate, processing time, queue depth) |
87
+
88
+ ### Actions
89
+
90
+ | Hook | Description |
91
+ | --------------------------------- | --------------------------------------------------- |
92
+ | `useTimeTravel(storeId, entries)` | Jump to any event index, step forward/back, resume |
93
+ | `useEventReplay(storeId)` | Replay events through reducers without side effects |
94
+ | `useEventEmitter(storeId)` | Emit synthetic events to a store |
95
+
96
+ ---
97
+
98
+ ## Context
99
+
100
+ ### `HubProvider`
101
+
102
+ Wraps child components in a WebSocket connection context:
103
+
104
+ ```tsx
105
+ <HubProvider
106
+ config={{
107
+ port: 9800,
108
+ host: "localhost",
109
+ extensionName: "My DevTools",
110
+ autoReconnect: true,
111
+ maxReconnectAttempts: 10,
112
+ }}
113
+ >
114
+ {children}
115
+ </HubProvider>
116
+ ```
117
+
118
+ ### `HubConnectionConfig`
119
+
120
+ ```typescript
121
+ interface HubConnectionConfig {
122
+ port: number;
123
+ host?: string; // default: "localhost"
124
+ extensionName?: string; // display name for this extension
125
+ autoReconnect?: boolean; // default: true
126
+ maxReconnectAttempts?: number; // default: Infinity
127
+ }
128
+ ```
129
+
130
+ ---
131
+
132
+ ## State Synchronization
133
+
134
+ `useStoreState` uses an efficient incremental patching strategy:
135
+
136
+ 1. Requests a full `STATE_SNAPSHOT` on mount
137
+ 2. Buffers any `STORE_EVENT` patches that arrive before the snapshot
138
+ 3. Replays buffered patches in version order once the snapshot lands
139
+ 4. Applies subsequent patches incrementally via `applyPatches`
140
+
141
+ This means the UI always reflects the latest store state without repeated full snapshots.
142
+
143
+ ---
144
+
145
+ ## Time Travel
146
+
147
+ ```tsx
148
+ function TimeTravelControls({ storeId, entries }) {
149
+ const { currentIndex, isTimeTraveling, jumpTo, stepBack, stepForward, resume } =
150
+ useTimeTravel(storeId, entries);
151
+
152
+ return (
153
+ <div>
154
+ <button onClick={stepBack} disabled={currentIndex <= 0}>
155
+ Back
156
+ </button>
157
+ <span>
158
+ {currentIndex + 1} / {entries.length}
159
+ </span>
160
+ <button onClick={stepForward} disabled={currentIndex >= entries.length - 1}>
161
+ Forward
162
+ </button>
163
+ {isTimeTraveling && <button onClick={resume}>Resume</button>}
164
+ </div>
165
+ );
166
+ }
167
+ ```
168
+
169
+ ---
170
+
171
+ ## API Reference
172
+
173
+ ### Context
174
+
175
+ | Export | Description |
176
+ | ------------- | ------------------------------------------------ |
177
+ | `HubProvider` | React context provider wrapping a hub connection |
178
+ | `HubContext` | The raw React context (for advanced use) |
179
+
180
+ ### Hooks
181
+
182
+ | Export | Returns |
183
+ | --------------------------------- | -------------------------------------------------------------------------- |
184
+ | `useHubConnection()` | `{ status, send, subscribe, disconnect, reconnect }` |
185
+ | `useStoreRegistry()` | `RegisteredStore[]` |
186
+ | `useEventLog(storeId)` | `{ entries, clear }` |
187
+ | `useStoreState(storeId)` | `{ state, version, loading, refresh }` |
188
+ | `useStoreSubscriptions(storeId)` | `{ data, loading }` |
189
+ | `useStoreMetrics(storeId)` | `{ metrics, loading }` |
190
+ | `useTimeTravel(storeId, entries)` | `{ currentIndex, isTimeTraveling, jumpTo, stepBack, stepForward, resume }` |
191
+ | `useEventReplay(storeId)` | `{ replay }` |
192
+ | `useEventEmitter(storeId)` | `{ emit }` |
193
+
194
+ ### Utilities
195
+
196
+ | Export | Description |
197
+ | ------------------------------ | ------------------------------------------- |
198
+ | `applyPatches(state, patches)` | Apply RFC 6902 JSON Patches to a state tree |
199
+
200
+ ### Types
201
+
202
+ | Export | Description |
203
+ | --------------------- | ----------------------------------------------- |
204
+ | `HubConnectionConfig` | Provider configuration |
205
+ | `HubConnectionStatus` | `"disconnected" \| "connecting" \| "connected"` |
206
+ | `HubContextValue` | Full context value shape |
207
+ | `RegisteredStore` | Store entry from the registry |
208
+ | `EventLogEntry` | Single event in the log |
209
+
210
+ ---
211
+
212
+ ## Related Packages
213
+
214
+ - **[@yoltra/devtools-protocol](../devtools-protocol/README.md)** — Wire format consumed by
215
+ these hooks
216
+ - **[@yoltra/devtools-storeview](../devtools-storeview/README.md)** — React DOM UI built on
217
+ these hooks
218
+ - **[@yoltra/devtools-server](../devtools-server/README.md)** — The hub these hooks connect to
219
+
220
+ ---
221
+
222
+ ## License
223
+
224
+ **MIT** — Free to use in commercial and open-source projects.
@@ -0,0 +1,7 @@
1
+ "use strict";Object.defineProperty(exports,Symbol.toStringTag,{value:"Module"});const a=require("react"),b=require("@yoltra/devtools-protocol"),Y=()=>{},Q=a.createContext({status:"disconnected",send:Y,subscribe:()=>Y,disconnect:Y,reconnect:Y});var U={exports:{}},M={};var se;function pe(){if(se)return M;se=1;var e=Symbol.for("react.transitional.element"),s=Symbol.for("react.fragment");function n(o,r,c){var i=null;if(c!==void 0&&(i=""+c),r.key!==void 0&&(i=""+r.key),"key"in r){c={};for(var u in r)u!=="key"&&(c[u]=r[u])}else c=r;return r=c.ref,{$$typeof:e,type:o,key:i,ref:r!==void 0?r:null,props:c}}return M.Fragment=s,M.jsx=n,M.jsxs=n,M}var V={};var oe;function Re(){return oe||(oe=1,process.env.NODE_ENV!=="production"&&(function(){function e(t){if(t==null)return null;if(typeof t=="function")return t.$$typeof===de?null:t.displayName||t.name||null;if(typeof t=="string")return t;switch(t){case p:return"Fragment";case N:return"Profiler";case v:return"StrictMode";case k:return"Suspense";case x:return"SuspenseList";case fe:return"Activity"}if(typeof t=="object")switch(typeof t.tag=="number"&&console.error("Received an unexpected object in getComponentNameFromType(). This is likely a bug in React. Please file an issue."),t.$$typeof){case C:return"Portal";case E:return(t.displayName||"Context")+".Provider";case w:return(t._context.displayName||"Context")+".Consumer";case m:var l=t.render;return t=t.displayName,t||(t=l.displayName||l.name||"",t=t!==""?"ForwardRef("+t+")":"ForwardRef"),t;case A:return l=t.displayName||null,l!==null?l:e(t.type)||"Memo";case D:l=t._payload,t=t._init;try{return e(t(l))}catch{}}return null}function s(t){return""+t}function n(t){try{s(t);var l=!1}catch{l=!0}if(l){l=console;var T=l.error,y=typeof Symbol=="function"&&Symbol.toStringTag&&t[Symbol.toStringTag]||t.constructor.name||"Object";return T.call(l,"The provided key is an unsupported type %s. This value must be coerced to a string before using it here.",y),s(t)}}function o(t){if(t===p)return"<>";if(typeof t=="object"&&t!==null&&t.$$typeof===D)return"<...>";try{var l=e(t);return l?"<"+l+">":"<...>"}catch{return"<...>"}}function r(){var t=q.A;return t===null?null:t.getOwner()}function c(){return Error("react-stack-top-frame")}function i(t){if(K.call(t,"key")){var l=Object.getOwnPropertyDescriptor(t,"key").get;if(l&&l.isReactWarning)return!1}return t.key!==void 0}function u(t,l){function T(){Z||(Z=!0,console.error("%s: `key` is not a prop. Trying to access it will result in `undefined` being returned. If you need to access the same value within the child component, you should pass it as a different prop. (https://react.dev/link/special-props)",l))}T.isReactWarning=!0,Object.defineProperty(t,"key",{get:T,configurable:!0})}function d(){var t=e(this.type);return ee[t]||(ee[t]=!0,console.error("Accessing element.ref was removed in React 19. ref is now a regular prop. It will be removed from the JSX Element type in a future release.")),t=this.props.ref,t!==void 0?t:null}function f(t,l,T,y,g,I,$,J){return T=I.ref,t={$$typeof:O,type:t,key:l,props:I,_owner:g},(T!==void 0?T:null)!==null?Object.defineProperty(t,"ref",{enumerable:!1,get:d}):Object.defineProperty(t,"ref",{enumerable:!1,value:null}),t._store={},Object.defineProperty(t._store,"validated",{configurable:!1,enumerable:!1,writable:!0,value:0}),Object.defineProperty(t,"_debugInfo",{configurable:!1,enumerable:!1,writable:!0,value:null}),Object.defineProperty(t,"_debugStack",{configurable:!1,enumerable:!1,writable:!0,value:$}),Object.defineProperty(t,"_debugTask",{configurable:!1,enumerable:!1,writable:!0,value:J}),Object.freeze&&(Object.freeze(t.props),Object.freeze(t)),t}function S(t,l,T,y,g,I,$,J){var _=l.children;if(_!==void 0)if(y)if(Ee(_)){for(y=0;y<_.length;y++)h(_[y]);Object.freeze&&Object.freeze(_)}else console.error("React.jsx: Static children should always be an array. You are likely explicitly calling React.jsxs or React.jsxDEV. Use the Babel transform instead.");else h(_);if(K.call(l,"key")){_=e(t);var j=Object.keys(l).filter(function(Se){return Se!=="key"});y=0<j.length?"{key: someKey, "+j.join(": ..., ")+": ...}":"{key: someKey}",ne[_+y]||(j=0<j.length?"{"+j.join(": ..., ")+": ...}":"{}",console.error(`A props object containing a "key" prop is being spread into JSX:
2
+ let props = %s;
3
+ <%s {...props} />
4
+ React keys must be passed directly to JSX without using spread:
5
+ let props = %s;
6
+ <%s key={someKey} {...props} />`,y,_,j,_),ne[_+y]=!0)}if(_=null,T!==void 0&&(n(T),_=""+T),i(l)&&(n(l.key),_=""+l.key),"key"in l){T={};for(var W in l)W!=="key"&&(T[W]=l[W])}else T=l;return _&&u(T,typeof t=="function"?t.displayName||t.name||"Unknown":t),f(t,_,I,g,r(),T,$,J)}function h(t){typeof t=="object"&&t!==null&&t.$$typeof===O&&t._store&&(t._store.validated=1)}var R=a,O=Symbol.for("react.transitional.element"),C=Symbol.for("react.portal"),p=Symbol.for("react.fragment"),v=Symbol.for("react.strict_mode"),N=Symbol.for("react.profiler"),w=Symbol.for("react.consumer"),E=Symbol.for("react.context"),m=Symbol.for("react.forward_ref"),k=Symbol.for("react.suspense"),x=Symbol.for("react.suspense_list"),A=Symbol.for("react.memo"),D=Symbol.for("react.lazy"),fe=Symbol.for("react.activity"),de=Symbol.for("react.client.reference"),q=R.__CLIENT_INTERNALS_DO_NOT_USE_OR_WARN_USERS_THEY_CANNOT_UPGRADE,K=Object.prototype.hasOwnProperty,Ee=Array.isArray,X=console.createTask?console.createTask:function(){return null};R={react_stack_bottom_frame:function(t){return t()}};var Z,ee={},te=R.react_stack_bottom_frame.bind(R,c)(),re=X(o(c)),ne={};V.Fragment=p,V.jsx=function(t,l,T,y,g){var I=1e4>q.recentlyCreatedOwnerStacks++;return S(t,l,T,!1,y,g,I?Error("react-stack-top-frame"):te,I?X(o(t)):re)},V.jsxs=function(t,l,T,y,g){var I=1e4>q.recentlyCreatedOwnerStacks++;return S(t,l,T,!0,y,g,I?Error("react-stack-top-frame"):te,I?X(o(t)):re)}})()),V}var ce;function Te(){return ce||(ce=1,process.env.NODE_ENV==="production"?U.exports=pe():U.exports=Re()),U.exports}var ve=Te();const be={timeTravel:!0,eventReplay:!0,stateExplorer:!0,eventEmit:!0,performanceMetrics:!0},he=750,me=3e4;function Oe({config:e,children:s}){const[n,o]=a.useState("disconnected"),r=a.useRef(null),c=a.useRef(new Set),i=a.useRef(0),u=a.useRef(null),d=a.useRef(crypto.randomUUID()),f=a.useRef(e);f.current=e;const S=a.useCallback(()=>{const v=f.current,w=`ws://${v.host??"localhost"}:${v.port}`,E=v.WebSocket??(typeof globalThis.WebSocket<"u"?globalThis.WebSocket:void 0);if(!E){console.error("[Yoltra DevTools] No WebSocket implementation available. In Node.js, pass { WebSocket } from 'ws' via config.WebSocket.");return}o("connecting");try{const m=new E(w);r.current=m,m.onopen=()=>{i.current=0;const k=JSON.stringify({type:"HANDSHAKE_REQUEST",protocolVersion:b.PROTOCOL_VERSION,role:b.DevtoolsRole.EXTENSION,extension:{id:d.current,name:v.extensionName??"DevTools UI",capabilities:be}});m.send(k)},m.onmessage=k=>{try{const x=typeof k.data=="string"?k.data:String(k.data),A=JSON.parse(x);if(A.type==="HANDSHAKE_RESPONSE"){A.success?o("connected"):m.close();return}c.current.forEach(D=>D(A))}catch{}},m.onclose=()=>{r.current=null,o("disconnected");const k=v.autoReconnect??!0,x=v.maxReconnectAttempts??1/0;if(k&&i.current<x){const A=i.current++,D=Math.min(he*Math.pow(2,A)+Math.random()*500,me);u.current=setTimeout(S,D)}},m.onerror=()=>{}}catch{o("disconnected")}},[]),h=a.useCallback(()=>{u.current&&(clearTimeout(u.current),u.current=null),i.current=1/0,r.current?.close(),r.current=null,o("disconnected")},[]),R=a.useCallback(()=>{i.current=0,r.current?.close(),r.current=null,S()},[S]),O=a.useCallback(v=>{const N=r.current;N&&N.readyState===1&&N.send(JSON.stringify(v))},[]),C=a.useCallback(v=>(c.current.add(v),()=>{c.current.delete(v)}),[]);a.useEffect(()=>(S(),()=>{u.current&&clearTimeout(u.current),i.current=1/0,r.current?.close()}),[S]);const p={status:n,send:O,subscribe:C,disconnect:h,reconnect:R};return ve.jsx(Q.Provider,{value:p,children:s})}function P(){return a.useContext(Q)}function ye(e){const{send:s}=P();return{emit:a.useCallback((o,r,c)=>{e&&s({type:"EMIT_TO_STORE",storeId:e,event:{channel:o,type:r,payload:c},timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION})},[e,s])}}const _e=2e3;function Ne(e,s){const{subscribe:n}=P(),o=s?.maxEntries??_e,r=a.useRef(new Map),c=a.useRef(o);c.current=o;const[,i]=a.useState(0);a.useEffect(()=>n(S=>{if(S.type!=="STORE_EVENT")return;const h=S.storeId,R={event:S.event,storeId:h,patches:S.patches,snapshotVersion:S.snapshotVersion,committed:S.committed,timestamp:S.timestamp},C=[...r.current.get(h)??[],R];r.current.set(h,C.length>c.current?C.slice(C.length-c.current):C),i(p=>p+1)}),[n]);const u=a.useCallback(()=>{e&&(r.current.delete(e),i(f=>f+1))},[e]);return{entries:e?r.current.get(e)??[]:[],clear:u}}function ke(e){const{send:s}=P();return{replay:a.useCallback((o,r)=>{e&&s({type:"EVENT_REPLAY",storeId:e,snapshot:o,events:r.map(c=>({id:c.event.id,channel:c.event.channel,type:c.event.type,payload:c.event.payload})),timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION})},[e,s])}}function Ce(e,s){const{send:n,subscribe:o}=P(),[r,c]=a.useState(null),[i,u]=a.useState(!1),d=s?.refreshIntervalMs??2e3,f=a.useRef(null),S=a.useCallback(()=>{e&&(u(!0),n({type:"REQUEST_METRICS",storeId:e,timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION}))},[e,n]);return a.useEffect(()=>{if(!e){c(null);return}S();const h=o(R=>{R.type==="STORE_METRICS"&&R.storeId===e&&(c(R.metrics),u(!1))});return d>0&&(f.current=setInterval(S,d)),()=>{h(),f.current&&(clearInterval(f.current),f.current=null)}},[e,o,S,d]),{metrics:r,loading:i,refresh:S}}function Ae(){const{subscribe:e}=P(),[s,n]=a.useState([]);return a.useEffect(()=>e(r=>{switch(r.type){case"STORE_REGISTRY":n(r.stores);break;case"STORE_CONNECTED":n(c=>c.some(u=>u.id===r.store.id)?c.map(u=>u.id===r.store.id?{...u,status:"connected"}:u):[...c,{id:r.store.id,name:r.store.name,status:"connected",capabilities:r.store.capabilities,connectedAt:r.timestamp}]);break;case"STORE_DISCONNECTED":n(c=>c.map(i=>i.id===r.storeId?{...i,status:"disconnected"}:i));break}}),[e]),s}function F(e,s){let n=structuredClone(e);for(const o of s){const r=Ie(o.path);switch(o.op){case"add":n=z(n,r,o.value,!0);break;case"replace":n=z(n,r,o.value,!1);break;case"remove":n=le(n,r);break}}return n}function Ie(e){return e===""||e==="/"?[]:e.slice(1).split("/").map(s=>s.replace(/~1/g,"/").replace(/~0/g,"~"))}const ie=new Set(["__proto__","constructor","prototype"]);function z(e,s,n,o){if(s.length===0)return n;const r=Array.isArray(e)?[...e]:{...e},[c,...i]=s;if(ie.has(c))return r;if(i.length===0)if(o&&Array.isArray(r)){const u=c==="-"?r.length:Number(c);Number.isInteger(u)&&u>=0&&u<=r.length?r.splice(u,0,n):r[c]=n}else r[c]=n;else r[c]=z(r[c]??{},i,n,o);return r}function le(e,s){if(s.length===0)return;const n=Array.isArray(e)?[...e]:{...e},[o,...r]=s;return ie.has(o)||(r.length===0?Array.isArray(n)?n.splice(Number(o),1):delete n[o]:n[o]=le(n[o],r)),n}function we(e){const s=e.scheduler??{setInterval:(r,c)=>setInterval(r,c),clearInterval:r=>clearInterval(r)};let n=s.setInterval(()=>{if(e.isSettled()){o();return}e.request()},e.intervalMs);function o(){n!==null&&(s.clearInterval(n),n=null)}return o}const xe=1500;function Pe(e){const{send:s,subscribe:n}=P(),[o,r]=a.useState(null),[c,i]=a.useState(0),[u,d]=a.useState(!1),f=a.useRef(0),S=a.useRef([]),h=a.useRef(null),R=a.useCallback(()=>{h.current?.(),h.current=null},[]),O=a.useCallback(()=>{e&&(d(!0),s({type:"REQUEST_STATE",storeId:e,timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION}))},[e,s]);return a.useEffect(()=>{if(!e){r(null),i(0),f.current=0;return}O(),h.current=we({request:O,isSettled:()=>f.current!==0,intervalMs:xe});const C=n(p=>{if(p.type==="STATE_SNAPSHOT"&&p.storeId===e){R(),r(p.state),i(p.version),f.current=p.version,d(!1);const v=S.current.filter(N=>N.version>p.version).sort((N,w)=>N.version-w.version);if(S.current=[],v.length>0){r(w=>{let E=w;for(const m of v)E=F(E,m.patches);return E});const N=v[v.length-1].version;i(N),f.current=N}return}if(p.type==="STORE_EVENT"&&p.storeId===e&&p.committed){if(f.current===0){S.current.push({patches:p.patches,version:p.snapshotVersion});return}if(p.snapshotVersion<=f.current)return;r(v=>F(v,p.patches)),i(p.snapshotVersion),f.current=p.snapshotVersion}});return()=>{C(),R(),S.current=[]}},[e,n,O,R]),{state:o,version:c,loading:u,refresh:O}}function ge(e){const{send:s,subscribe:n}=P(),[o,r]=a.useState(null),[c,i]=a.useState(!1),u=a.useCallback(()=>{e&&(i(!0),s({type:"REQUEST_SUBSCRIPTIONS",storeId:e,timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION}))},[e,s]);return a.useEffect(()=>{if(!e){r(null);return}return u(),n(f=>{f.type==="STORE_SUBSCRIPTIONS"&&f.storeId===e&&(r({atomic:f.atomic,event:f.event,coarse:f.coarse,effects:f.effects,middleware:f.middleware,reducers:f.reducers}),i(!1))})},[e,n,u]),{data:o,loading:c,refresh:u}}function ae(e,s){const{isTimeTraveling:n,currentIndex:o,entryCount:r}=s,c=r-1;if(e==="back"){const d=(n?o:c)-1;return d>=0?{kind:"jump",index:d}:{kind:"none"}}if(!n)return{kind:"none"};const i=o+1;return i>c?{kind:"resume"}:{kind:"jump",index:i}}function De(e,s,n=!0){const{send:o,subscribe:r}=P(),[c,i]=a.useState(-1),[u,d]=a.useState(!1),[f,S]=a.useState(null),h=a.useRef(null);a.useEffect(()=>{if(h.current=null,S(null),!!e)return r(E=>{E.type==="STATE_SNAPSHOT"&&E.storeId===e&&h.current===null&&(h.current={state:E.state,version:E.version})})},[e,r]);const R=a.useCallback(E=>{const m=h.current;if(!m)return null;let k=m.state;for(let x=0;x<=E;x++){const A=s[x];if(!A)break;A.snapshotVersion<=m.version||(k=F(k,A.patches))}return k},[s]),O=a.useCallback(E=>{if(!e||!n||E<0||E>=s.length)return;S(k=>k??s.length),i(E),d(!0);const m=s[E];o({type:"TIME_TRAVEL",storeId:e,state:R(E),snapshotVersion:m.snapshotVersion,timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION})},[e,s,o,R,n]),C=a.useCallback(()=>{if(d(!1),i(-1),S(null),n&&e&&s.length>0){const E=s.length-1,m=s[E];o({type:"TIME_TRAVEL",storeId:e,state:R(E),snapshotVersion:m.snapshotVersion,timestamp:new Date().toISOString(),sourceId:"",sourceRole:b.DevtoolsRole.EXTENSION})}},[e,s,o,R,n]),p=f??s.length,v=a.useCallback(()=>{const E=ae("back",{isTimeTraveling:u,currentIndex:c,entryCount:p});E.kind==="jump"&&O(E.index)},[u,c,p,O]),N=a.useCallback(()=>{const E=ae("forward",{isTimeTraveling:u,currentIndex:c,entryCount:p});E.kind==="jump"?O(E.index):E.kind==="resume"&&C()},[u,c,p,O,C]),w=a.useMemo(()=>R(u?c:s.length-1),[R,u,c,s.length]);return{currentIndex:c,isTimeTraveling:u,previewState:w,frameCount:f,jumpTo:O,stepBack:v,stepForward:N,resume:C}}const G=0,B=1,je=2,L=3;function H(){return{timestamp:new Date().toISOString(),sourceId:"loopback-hub",sourceRole:b.DevtoolsRole.HUB}}function ue(e){return e.split(".")[0]??"0"}class Me{constructor(){this.peers=new Set}add(s){const n={role:null,id:null,deliver:s,closed:!1};return this.peers.add(n),n}remove(s){if(this.peers.delete(s)&&(s.closed=!0,s.role===b.DevtoolsRole.STORE&&s.id)){const n=JSON.stringify({type:"STORE_DISCONNECTED",storeId:s.id,reason:"disconnected",...H()});this.eachExtension(o=>o.deliver(n))}}receive(s,n){if(s.closed)return;let o;try{o=JSON.parse(n)}catch{return}if(!(o===null||typeof o!="object"||typeof o.type!="string")){if(s.role===null){this.handshake(s,o);return}if(s.role===b.DevtoolsRole.STORE)this.eachExtension(r=>r.deliver(n));else if(typeof o.storeId=="string")for(const r of this.peers)r.role===b.DevtoolsRole.STORE&&r.id===o.storeId&&r.deliver(n)}}handshake(s,n){if(n.type!=="HANDSHAKE_REQUEST")return;const o=n.role,r=o===b.DevtoolsRole.STORE?n.store?.id:o===b.DevtoolsRole.EXTENSION?n.extension?.id:void 0,i=ue(String(n.protocolVersion??""))===ue(b.PROTOCOL_VERSION)&&typeof r=="string";if(s.deliver(JSON.stringify({type:"HANDSHAKE_RESPONSE",success:i,protocolVersion:b.PROTOCOL_VERSION,...H(),...i?{}:{error:"Loopback handshake rejected (version or id mismatch)"}})),!!i)if(s.role=o,s.id=r,o===b.DevtoolsRole.STORE){s.storeInfo={name:n.store?.name??r,capabilities:n.store?.capabilities};const u=JSON.stringify({type:"STORE_CONNECTED",store:{id:r,name:s.storeInfo.name,capabilities:s.storeInfo.capabilities},...H()});this.eachExtension(d=>d.deliver(u))}else{const u=[...this.peers].filter(d=>d.role===b.DevtoolsRole.STORE&&d.storeInfo).map(d=>({id:d.id,name:d.storeInfo.name,status:"connected",capabilities:d.storeInfo.capabilities,connectedAt:new Date().toISOString()}));s.deliver(JSON.stringify({type:"STORE_REGISTRY",stores:u,...H()}))}}eachExtension(s){for(const n of this.peers)n.role===b.DevtoolsRole.EXTENSION&&s(n)}}function Ve(){const e=new Me,s=(r,c)=>{let i=G;const u=e.add(d=>c.onMessage(d));return queueMicrotask(()=>{i=B,c.onOpen()}),{get readyState(){return i},send:d=>e.receive(u,d),close:()=>{i!==L&&(i=L,e.remove(u),c.onClose())},dispose:()=>{u.closed=!0}}},o=class o{constructor(c){this.onopen=null,this.onmessage=null,this.onclose=null,this.onerror=null,this.readyState=G,this.peer=e.add(i=>this.onmessage?.({data:i})),queueMicrotask(()=>{this.readyState=B,this.onopen?.()})}send(c){e.receive(this.peer,c)}close(){this.readyState!==L&&(this.readyState=L,e.remove(this.peer),this.onclose?.())}};o.CONNECTING=G,o.OPEN=B,o.CLOSING=je,o.CLOSED=L;let n=o;return{agentSocketFactory:s,WebSocket:n}}exports.HubContext=Q;exports.HubProvider=Oe;exports.applyPatches=F;exports.createLoopbackHub=Ve;exports.useEventEmitter=ye;exports.useEventLog=Ne;exports.useEventReplay=ke;exports.useHubConnection=P;exports.useStoreMetrics=Ce;exports.useStoreRegistry=Ae;exports.useStoreState=Pe;exports.useStoreSubscriptions=ge;exports.useTimeTravel=De;
7
+ //# sourceMappingURL=devtools-ui.cjs.js.map