@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.
@@ -0,0 +1,33 @@
1
+ import { RegisteredStore } from '../types';
2
+ /**
3
+ * Tracks connected stores from `STORE_REGISTRY` hub broadcasts.
4
+ *
5
+ * @remarks
6
+ * The hook listens for three message types:
7
+ * - `STORE_REGISTRY` -- replaces the full store list (sent on initial connect).
8
+ * - `STORE_CONNECTED` -- adds or updates a single store entry.
9
+ * - `STORE_DISCONNECTED` -- marks a store as `"disconnected"`.
10
+ *
11
+ * The returned array is referentially stable unless the underlying data changes.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { useStoreRegistry } from "@yoltra/devtools-ui";
16
+ *
17
+ * function StoreList() {
18
+ * const stores = useStoreRegistry();
19
+ * return (
20
+ * <ul>
21
+ * {stores.map((s) => (
22
+ * <li key={s.id}>{s.name} ({s.status})</li>
23
+ * ))}
24
+ * </ul>
25
+ * );
26
+ * }
27
+ * ```
28
+ *
29
+ * @returns The current list of registered stores (see {@link RegisteredStore}).
30
+ *
31
+ * @public
32
+ */
33
+ export declare function useStoreRegistry(): RegisteredStore[];
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Hook that provides a live, incrementally-patched view of a store's state.
3
+ *
4
+ * @remarks
5
+ * On activation, the hook requests a full `STATE_SNAPSHOT` from the hub.
6
+ * While waiting, any `STORE_EVENT` patches that arrive are buffered and
7
+ * replayed once the snapshot lands. After that, each committed patch is
8
+ * applied via {@link applyPatches} so the UI always reflects the latest
9
+ * store state without requiring full snapshots.
10
+ *
11
+ * @module @yoltra/devtools-ui
12
+ */
13
+ /**
14
+ * Lazily fetches a store's full state and keeps it up-to-date via patches.
15
+ *
16
+ * @remarks
17
+ * The state is initially `null` until a `STATE_SNAPSHOT` is received.
18
+ * After that, incoming `STORE_EVENT` patches are applied incrementally
19
+ * using {@link applyPatches}. Patches that arrive before the snapshot are
20
+ * buffered and replayed in version order once the snapshot is available.
21
+ *
22
+ * Call `refresh()` to discard the current state and re-fetch from scratch.
23
+ *
24
+ * @example
25
+ * ```tsx
26
+ * import { useStoreState } from "@yoltra/devtools-ui";
27
+ *
28
+ * function StateInspector({ storeId }: { storeId: string }) {
29
+ * const { state, version, loading, refresh } = useStoreState(storeId);
30
+ * if (loading) return <p>Loading...</p>;
31
+ * return (
32
+ * <div>
33
+ * <p>Version: {version}</p>
34
+ * <pre>{JSON.stringify(state, null, 2)}</pre>
35
+ * <button onClick={refresh}>Refresh</button>
36
+ * </div>
37
+ * );
38
+ * }
39
+ * ```
40
+ *
41
+ * @param storeId - The store ID to track state for, or `null` to disable.
42
+ * @returns An object with `state`, `version`, `loading`, and `refresh`.
43
+ *
44
+ * @public
45
+ */
46
+ export declare function useStoreState(storeId: string | null): {
47
+ state: unknown;
48
+ version: number;
49
+ loading: boolean;
50
+ refresh: () => void;
51
+ };
@@ -0,0 +1,45 @@
1
+ import { StoreSubscriptions } from '@yoltra/devtools-protocol';
2
+ /**
3
+ * Subscription data payload with protocol envelope fields stripped.
4
+ *
5
+ * @internal
6
+ */
7
+ type SubscriptionData = Omit<StoreSubscriptions, "type" | "timestamp" | "sourceId" | "sourceRole" | "storeId">;
8
+ /**
9
+ * Fetches and caches subscription/consumer info for a store.
10
+ *
11
+ * @remarks
12
+ * On mount (or when `storeId` changes) the hook sends a
13
+ * `REQUEST_SUBSCRIPTIONS` message and sets `loading` to `true`. When the
14
+ * matching `STORE_SUBSCRIPTIONS` response arrives the data is cached and
15
+ * `loading` flips to `false`.
16
+ *
17
+ * @example
18
+ * ```tsx
19
+ * import { useStoreSubscriptions } from "@yoltra/devtools-ui";
20
+ *
21
+ * function SubscriptionPanel({ storeId }: { storeId: string }) {
22
+ * const { data, loading, refresh } = useStoreSubscriptions(storeId);
23
+ * if (loading || !data) return <p>Loading...</p>;
24
+ * return (
25
+ * <div>
26
+ * <p>Atomic: {data.atomic.length}</p>
27
+ * <p>Event: {data.event.length}</p>
28
+ * <button onClick={refresh}>Refresh</button>
29
+ * </div>
30
+ * );
31
+ * }
32
+ * ```
33
+ *
34
+ * @param storeId - The store ID to query, or `null` to disable.
35
+ * @returns An object with `data` ({@link SubscriptionData} or `null`),
36
+ * `loading`, and `refresh`.
37
+ *
38
+ * @public
39
+ */
40
+ export declare function useStoreSubscriptions(storeId: string | null): {
41
+ data: SubscriptionData | null;
42
+ loading: boolean;
43
+ refresh: () => void;
44
+ };
45
+ export {};
@@ -0,0 +1,67 @@
1
+ import { EventLogEntry } from '../types';
2
+ /**
3
+ * Provides time-travel navigation through the event log.
4
+ *
5
+ * @remarks
6
+ * The hook tracks whether the user is actively time-traveling via
7
+ * `isTimeTraveling`. While traveling, `currentIndex` indicates the position
8
+ * within the `entries` array. The `stepBack` / `stepForward` helpers move
9
+ * one entry at a time, while `jumpTo` allows arbitrary positioning. Calling
10
+ * `resume()` exits time-travel mode, jumps to the latest entry, and resets
11
+ * the index to `-1`.
12
+ *
13
+ * @example
14
+ * ```tsx
15
+ * import { useEventLog, useTimeTravel } from "@yoltra/devtools-ui";
16
+ *
17
+ * function TimeTravelControls({ storeId }: { storeId: string }) {
18
+ * const { entries } = useEventLog(storeId);
19
+ * const { currentIndex, isTimeTraveling, stepBack, stepForward, resume } =
20
+ * useTimeTravel(storeId, entries);
21
+ *
22
+ * return (
23
+ * <div>
24
+ * <button onClick={stepBack} disabled={currentIndex <= 0}>Back</button>
25
+ * <button onClick={stepForward}>Forward</button>
26
+ * {isTimeTraveling && <button onClick={resume}>Resume Live</button>}
27
+ * <span>Index: {currentIndex} / {entries.length - 1}</span>
28
+ * </div>
29
+ * );
30
+ * }
31
+ * ```
32
+ *
33
+ * @param storeId - The store ID to time-travel, or `null` to disable.
34
+ * @param entries - The event log entries (typically from {@link useEventLog}).
35
+ * @returns An object with `currentIndex`, `isTimeTraveling`, `jumpTo`,
36
+ * `stepBack`, `stepForward`, and `resume`.
37
+ *
38
+ * @public
39
+ */
40
+ export declare function useTimeTravel(storeId: string | null, entries: EventLogEntry[],
41
+ /**
42
+ * Whether the selected store advertised the `replay` capability. Time-travel
43
+ * commands are gated on it (DEV-4): the agent/core enforce it too, but the UI
44
+ * should not send commands that will be dropped. Defaults to `true`.
45
+ */
46
+ canReplay?: boolean): {
47
+ currentIndex: number;
48
+ isTimeTraveling: boolean;
49
+ /**
50
+ * The store state reconstructed at the currently-viewed position (the
51
+ * scrubbed index while traveling, otherwise the latest entry). `null` until
52
+ * the baseline snapshot has been captured. Lets consumers render a live
53
+ * preview of the state at any point in history.
54
+ */
55
+ previewState: unknown;
56
+ /**
57
+ * The event count the timeline is measured against: frozen at travel-start
58
+ * so a live-emitting store cannot shift the scrubber, or `null` when live
59
+ * (use `entries.length`). Consumers should size the slider/label off
60
+ * `frameCount ?? entries.length`.
61
+ */
62
+ frameCount: number | null;
63
+ jumpTo: (index: number) => void;
64
+ stepBack: () => void;
65
+ stepForward: () => void;
66
+ resume: () => void;
67
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * @module @yoltra/devtools-ui
3
+ *
4
+ * Shared React hooks and business logic for Yoltra DevTools UIs.
5
+ * Used by both `@yoltra/devtools-storeview` (React DOM) and `@yoltra/devtools-cli` (Ink).
6
+ * This package contains no UI components — only logic.
7
+ */
8
+ export { HubContext } from './context/HubContext';
9
+ export { HubProvider } from './context/HubProvider';
10
+ export { useEventEmitter } from './hooks/useEventEmitter';
11
+ export { useEventLog } from './hooks/useEventLog';
12
+ export { useEventReplay } from './hooks/useEventReplay';
13
+ export { useHubConnection } from './hooks/useHubConnection';
14
+ export { useStoreMetrics } from './hooks/useStoreMetrics';
15
+ export { useStoreRegistry } from './hooks/useStoreRegistry';
16
+ export { useStoreState } from './hooks/useStoreState';
17
+ export { useStoreSubscriptions } from './hooks/useStoreSubscriptions';
18
+ export { useTimeTravel } from './hooks/useTimeTravel';
19
+ export { applyPatches } from './utils/apply-patch';
20
+ export { createLoopbackHub } from './transport/loopback';
21
+ export type { LoopbackHub } from './transport/loopback';
22
+ export type { EventLogEntry, HubConnectionConfig, HubConnectionStatus, HubContextValue, RegisteredStore, } from './types';
23
+ export type { UseEventLogOptions } from './hooks/useEventLog';
24
+ export type { UseStoreMetricsOptions } from './hooks/useStoreMetrics';
@@ -0,0 +1,30 @@
1
+ import { DevtoolsSocketFactory } from '@yoltra/devtools-protocol';
2
+ /**
3
+ * A loopback hub instance. Wire the agent and the panel to the *same* instance.
4
+ *
5
+ * @public
6
+ */
7
+ export interface LoopbackHub {
8
+ /** Inject into the browser agent: `withDevtools(store, { socketFactory })`. */
9
+ agentSocketFactory: DevtoolsSocketFactory;
10
+ /** Pass to the DevTools UI as `config.WebSocket` (e.g. `<DevtoolsApp>`). */
11
+ WebSocket: {
12
+ new (url: string): WebSocket;
13
+ };
14
+ }
15
+ /**
16
+ * Creates a self-contained in-memory DevTools hub plus the two client transports
17
+ * that connect to it — a `socketFactory` for the store agent and a
18
+ * `WebSocket`-compatible class for the panel UI. No ports, no server, no
19
+ * extension: everything runs in the current process.
20
+ *
21
+ * @example
22
+ * ```ts
23
+ * const hub = createLoopbackHub();
24
+ * withDevtools(store, { port: 0, socketFactory: hub.agentSocketFactory });
25
+ * // <DevtoolsApp config={{ port: 0, WebSocket: hub.WebSocket }} />
26
+ * ```
27
+ *
28
+ * @public
29
+ */
30
+ export declare function createLoopbackHub(): LoopbackHub;
@@ -0,0 +1,138 @@
1
+ import { DevtoolsMessage, StoreCapabilities, StoreEvent } from '@yoltra/devtools-protocol';
2
+ /**
3
+ * Connection configuration for the DevTools hub.
4
+ *
5
+ * @remarks
6
+ * Pass this to {@link HubProvider} to control how the extension connects to
7
+ * the hub server. The only required field is `port`; all other fields have
8
+ * sensible defaults.
9
+ *
10
+ * @example
11
+ * ```tsx
12
+ * const config: HubConnectionConfig = {
13
+ * port: 8900,
14
+ * extensionName: "My Panel",
15
+ * autoReconnect: true,
16
+ * };
17
+ *
18
+ * <HubProvider config={config}>
19
+ * <App />
20
+ * </HubProvider>
21
+ * ```
22
+ *
23
+ * @public
24
+ */
25
+ export interface HubConnectionConfig {
26
+ /** Hub server host. @defaultValue `"localhost"` */
27
+ host?: string;
28
+ /** Hub server port. */
29
+ port: number;
30
+ /** Display name for this extension instance. */
31
+ extensionName?: string;
32
+ /** Auto-reconnect on disconnect. @defaultValue `true` */
33
+ autoReconnect?: boolean;
34
+ /** Maximum reconnect attempts. @defaultValue `Infinity` */
35
+ maxReconnectAttempts?: number;
36
+ /**
37
+ * Custom WebSocket constructor for Node.js environments.
38
+ *
39
+ * @remarks
40
+ * In Node.js 18, the global `WebSocket` is not available. Pass the `WebSocket`
41
+ * class from the `ws` package to enable connectivity:
42
+ *
43
+ * ```ts
44
+ * import WebSocket from "ws";
45
+ * config.WebSocket = WebSocket as any;
46
+ * ```
47
+ *
48
+ * In browsers or Node.js 21+, this is not needed — the native `WebSocket` is
49
+ * used automatically.
50
+ */
51
+ WebSocket?: {
52
+ new (url: string): WebSocket;
53
+ };
54
+ }
55
+ /**
56
+ * Connection status for the hub WebSocket.
57
+ *
58
+ * @remarks
59
+ * - `"disconnected"` -- no active connection.
60
+ * - `"connecting"` -- WebSocket handshake in progress.
61
+ * - `"connected"` -- handshake complete, messages can be sent and received.
62
+ *
63
+ * @public
64
+ */
65
+ export type HubConnectionStatus = "disconnected" | "connecting" | "connected";
66
+ /**
67
+ * Registered store entry tracked by the store registry.
68
+ *
69
+ * @remarks
70
+ * Populated automatically by the {@link useStoreRegistry} hook in response to
71
+ * `STORE_REGISTRY`, `STORE_CONNECTED`, and `STORE_DISCONNECTED` hub messages.
72
+ *
73
+ * @public
74
+ */
75
+ export interface RegisteredStore {
76
+ /** Unique store identifier assigned by the hub. */
77
+ id: string;
78
+ /** Human-readable store name. */
79
+ name: string;
80
+ /** Current connectivity status of the store. */
81
+ status: "connected" | "connecting" | "disconnected";
82
+ /** Capabilities advertised by the store during handshake. */
83
+ capabilities: StoreCapabilities;
84
+ /** ISO-8601 timestamp of when the store first connected. */
85
+ connectedAt: string;
86
+ }
87
+ /**
88
+ * A logged event entry in the event log.
89
+ *
90
+ * @remarks
91
+ * Each entry captures a single `STORE_EVENT` message received from the hub,
92
+ * including the event descriptor, resulting patches, and the snapshot version
93
+ * after the event was applied. The {@link useEventLog} hook collects these
94
+ * entries in chronological order.
95
+ *
96
+ * @public
97
+ */
98
+ export interface EventLogEntry {
99
+ /** The event descriptor (channel, type, payload). */
100
+ event: StoreEvent["event"];
101
+ /** Identifier of the store that emitted the event. */
102
+ storeId: string;
103
+ /** JSON Patch operations produced by the event. */
104
+ patches: StoreEvent["patches"];
105
+ /** Store snapshot version after this event was applied. */
106
+ snapshotVersion: number;
107
+ /** Whether the event was committed to the store. */
108
+ committed: boolean;
109
+ /** ISO-8601 timestamp of the event. */
110
+ timestamp: string;
111
+ }
112
+ /**
113
+ * Hub connection context value provided to consumers.
114
+ *
115
+ * @remarks
116
+ * This is the shape of the value exposed by {@link HubContext} and consumed
117
+ * via {@link useHubConnection}. It contains methods for sending messages,
118
+ * subscribing to incoming messages, and controlling the connection lifecycle.
119
+ *
120
+ * @public
121
+ */
122
+ export interface HubContextValue {
123
+ /** Current connection status. */
124
+ status: HubConnectionStatus;
125
+ /** Send a protocol message to the hub. */
126
+ send: (message: DevtoolsMessage) => void;
127
+ /**
128
+ * Subscribe to incoming hub messages.
129
+ *
130
+ * @param handler - Callback invoked for every incoming message.
131
+ * @returns An unsubscribe function.
132
+ */
133
+ subscribe: (handler: (message: DevtoolsMessage) => void) => () => void;
134
+ /** Manually disconnect from the hub and cancel auto-reconnect. */
135
+ disconnect: () => void;
136
+ /** Reset reconnect attempts and establish a fresh connection. */
137
+ reconnect: () => void;
138
+ }
@@ -0,0 +1,30 @@
1
+ import { JsonPatch } from '@yoltra/devtools-protocol';
2
+ /**
3
+ * Apply an array of RFC 6902 JSON Patch operations to a value.
4
+ * Returns a new object -- does not mutate the input.
5
+ *
6
+ * @remarks
7
+ * Only the `add`, `remove`, and `replace` operations are implemented because
8
+ * the v1 devtools protocol does not emit `move`, `copy`, or `test` patches.
9
+ * Each operation is applied sequentially in array order. Path segments are
10
+ * resolved according to RFC 6901 (JSON Pointer), including `~0` / `~1`
11
+ * escape handling.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * import { applyPatches } from "@yoltra/devtools-ui";
16
+ *
17
+ * const next = applyPatches(
18
+ * { counter: { value: 0 } },
19
+ * [{ op: "replace", path: "/counter/value", value: 1 }],
20
+ * );
21
+ * // next => { counter: { value: 1 } }
22
+ * ```
23
+ *
24
+ * @param target - The value to patch.
25
+ * @param patches - RFC 6902 operations.
26
+ * @returns A new patched value.
27
+ *
28
+ * @public
29
+ */
30
+ export declare function applyPatches<T = unknown>(target: T, patches: JsonPatch[]): T;
package/package.json ADDED
@@ -0,0 +1,73 @@
1
+ {
2
+ "name": "@yoltra/devtools-ui",
3
+ "version": "0.2.0",
4
+ "description": "Shared React hooks and business logic for Yoltra DevTools UIs",
5
+ "license": "MIT",
6
+ "author": {
7
+ "name": "Manu Ramirez <@pixerael>",
8
+ "email": "manu@yoltra.dev"
9
+ },
10
+ "maintainers": [],
11
+ "homepage": "https://yoltra.dev",
12
+ "keywords": [
13
+ "yoltra",
14
+ "devtools",
15
+ "ui",
16
+ "hooks"
17
+ ],
18
+ "repository": {
19
+ "type": "git",
20
+ "url": "https://github.com/yoltra/yoltra.git"
21
+ },
22
+ "bugs": {
23
+ "url": "https://github.com/yoltra/yoltra/issues"
24
+ },
25
+ "type": "module",
26
+ "main": "dist/devtools-ui.cjs.js",
27
+ "module": "dist/devtools-ui.esm.js",
28
+ "types": "dist/types/index.d.ts",
29
+ "exports": {
30
+ ".": {
31
+ "types": "./dist/types/index.d.ts",
32
+ "import": "./dist/devtools-ui.esm.js",
33
+ "require": "./dist/devtools-ui.cjs.js"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "sideEffects": false,
40
+ "peerDependencies": {
41
+ "react": "^18 || ^19"
42
+ },
43
+ "dependencies": {
44
+ "@yoltra/devtools-protocol": "0.2.0"
45
+ },
46
+ "devDependencies": {
47
+ "react": "19.1.1",
48
+ "@types/react": "^19.1.8",
49
+ "typedoc": "^0.28.13",
50
+ "typedoc-plugin-markdown": "4.9.0",
51
+ "typedoc-plugin-localization": "3.0.6",
52
+ "typescript": "5.9.3",
53
+ "vite": "^7.1.11",
54
+ "vite-plugin-dts": "^4.5.4",
55
+ "vite-plugin-banner": "0.8.1",
56
+ "vitest": "3.2.4"
57
+ },
58
+ "engines": {
59
+ "node": ">=18.18"
60
+ },
61
+ "publishConfig": {
62
+ "access": "public"
63
+ },
64
+ "scripts": {
65
+ "build": "vite build",
66
+ "test": "vitest --watch=false",
67
+ "lint": "node ../../tools/repo-tools/bin/repo-eslint.cjs --report-unused-disable-directives --max-warnings 0",
68
+ "typecheck": "tsc --noEmit",
69
+ "docs": "rushx docs:js && rushx docs:md",
70
+ "docs:md": "pnpm typedoc --options ./typedoc.json",
71
+ "docs:js": "pnpm typedoc --options ./typedoc.json --json ./.typedoc/devtools-ui-en.json"
72
+ }
73
+ }