@octanejs/devtools 0.0.36 → 0.0.38

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -7,6 +7,7 @@ streamed over `@tanstack/devtools-event-client` from a dev-only runtime hook.
7
7
  ## Installation
8
8
 
9
9
  ```bash
10
+ npm install @octanejs/devtools
10
11
  pnpm add @octanejs/devtools
11
12
  ```
12
13
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@octanejs/devtools",
3
- "version": "0.0.36",
3
+ "version": "0.0.38",
4
4
  "license": "MIT",
5
5
  "type": "module",
6
6
  "engines": {
@@ -42,14 +42,14 @@
42
42
  "@tanstack/devtools-event-client": "0.5.0"
43
43
  },
44
44
  "peerDependencies": {
45
- "octane": "0.1.48"
45
+ "octane": "0.1.50"
46
46
  },
47
47
  "devDependencies": {
48
48
  "@tanstack/devtools-event-bus": "0.4.2",
49
49
  "@testing-library/jest-dom": "^6.9.1",
50
50
  "vitest": "^4.1.10",
51
- "@octanejs/testing-library": "0.1.45",
52
- "octane": "0.1.48"
51
+ "@octanejs/testing-library": "0.1.47",
52
+ "octane": "0.1.50"
53
53
  },
54
54
  "scripts": {
55
55
  "test": "vitest run"
package/src/bridge.ts CHANGED
@@ -43,6 +43,7 @@ const DEVTOOLS_OWN_COMPONENTS = new Set([
43
43
  'TransitionsTab',
44
44
  'PerfModelTab',
45
45
  'TreeRow',
46
+ 'NativeReadRows',
46
47
  'DevtoolsPortal',
47
48
  'TanStackDevtools',
48
49
  ]);
@@ -114,6 +115,24 @@ export function startBridge(
114
115
  let lastTree = '';
115
116
  let lastProfile = '';
116
117
  let lastTransition = '';
118
+ let selectedId: number | null = null;
119
+ let lastInspect = '';
120
+
121
+ const emitInspect = (force = false) => {
122
+ if (selectedId === null) return;
123
+ const detail = hook.inspect(selectedId);
124
+ if (detail === null) {
125
+ client.emit('inspect-clear', { id: selectedId });
126
+ selectedId = null;
127
+ lastInspect = '';
128
+ return;
129
+ }
130
+ const serialized = JSON.stringify(detail);
131
+ if (force || serialized !== lastInspect) {
132
+ lastInspect = serialized;
133
+ client.emit('inspect', detail);
134
+ }
135
+ };
117
136
 
118
137
  const emitNow = () => {
119
138
  if (trailing !== null) {
@@ -144,6 +163,9 @@ export function startBridge(
144
163
  client.emit('transition', transition);
145
164
  }
146
165
  }
166
+ // Detail remains lazy: refresh only the selected Scope, even when its
167
+ // native revisions change without changing the component tree shape.
168
+ emitInspect();
147
169
  };
148
170
 
149
171
  const schedule = () => {
@@ -156,8 +178,8 @@ export function startBridge(
156
178
  const offFlush = hook.subscribe(schedule);
157
179
 
158
180
  const offRequest = client.on('inspect-request', (e) => {
159
- const detail = hook.inspect(e.payload.id);
160
- if (detail) client.emit('inspect', detail);
181
+ selectedId = e.payload.id;
182
+ emitInspect(true);
161
183
  });
162
184
 
163
185
  // The panel asks for a fresh snapshot when it mounts; force a full re-emit
@@ -167,6 +189,7 @@ export function startBridge(
167
189
  lastTree = '';
168
190
  lastProfile = '';
169
191
  lastTransition = '';
192
+ lastInspect = '';
170
193
  emitNow();
171
194
  });
172
195
 
package/src/client.ts CHANGED
@@ -22,6 +22,44 @@ export interface NodeDetail {
22
22
  hooks: WireHookCell[];
23
23
  context: Array<{ name: string; value: unknown }>;
24
24
  effectCount: number;
25
+ /** Optional capability: older runtimes and scopes without native reads omit it. */
26
+ nativeReads?: NativeReadOwnerWire;
27
+ }
28
+
29
+ export interface NativeReadSourceWire {
30
+ scopeKey: string;
31
+ key: string;
32
+ read: 'value' | 'latest' | 'snapshot';
33
+ kind: 'signal' | 'derived' | 'async';
34
+ status: 'ready' | 'pending' | 'error' | 'unevaluated';
35
+ revision: number;
36
+ generation?: number;
37
+ epoch: number;
38
+ retired: boolean;
39
+ historical: boolean;
40
+ retained: boolean;
41
+ refreshing: boolean;
42
+ connection: 'none' | 'connecting' | 'open' | 'closed';
43
+ complete: boolean;
44
+ dependencies: readonly { scopeKey: string; key: string }[];
45
+ }
46
+
47
+ export interface NativeReadWire {
48
+ observedVersion: number | null;
49
+ currentVersion: number;
50
+ source: NativeReadSourceWire | null;
51
+ }
52
+
53
+ export interface NativeReadAttemptWire {
54
+ mixed: boolean;
55
+ reads: readonly NativeReadWire[];
56
+ }
57
+
58
+ export interface NativeReadOwnerWire {
59
+ ownerId: number;
60
+ committed: NativeReadAttemptWire | null;
61
+ pending: readonly NativeReadAttemptWire[];
62
+ retry: readonly NativeReadWire[];
25
63
  }
26
64
  export interface InspectRequest {
27
65
  id: number;
@@ -70,6 +108,7 @@ type OctaneDevtoolsEventMap = {
70
108
  tree: TreeSnapshot;
71
109
  inspect: NodeDetail;
72
110
  'inspect-request': InspectRequest;
111
+ 'inspect-clear': InspectRequest;
73
112
  profile: ProfileSnapshot;
74
113
  transition: TransitionSnapshot;
75
114
  // Panel → bridge: "send me a fresh snapshot now" (emitted when the panel
@@ -4,7 +4,71 @@
4
4
  // (`inspect-request`); it never mutates app state (see docs/devtools.md P1 scope).
5
5
  import { useEffect, useState } from 'octane';
6
6
  import type { OctaneNode } from 'octane';
7
- import { OctaneDevtoolsEventClient, type NodeDetail, type WireTreeNode } from '../client';
7
+ import {
8
+ OctaneDevtoolsEventClient,
9
+ type NativeReadWire,
10
+ type NodeDetail,
11
+ type WireTreeNode,
12
+ } from '../client';
13
+
14
+ function NativeReadRows(props: { label: string; reads: readonly NativeReadWire[] }) @{
15
+ <div>
16
+ <div class="odt-h">{props.label}</div>
17
+ @if (props.reads.length === 0) {
18
+ <div class="odt-empty odt-muted">No native reads.</div>
19
+ } @else {
20
+ @for (const read of props.reads; index i) {
21
+ <div class="odt-kv">
22
+ @if (read.source) {
23
+ <div>
24
+ <div class="odt-kv-key">{read.source.scopeKey + ':' + read.source.key}</div>
25
+ <div class="odt-muted">
26
+ {read.source.kind + ' · ' + read.source.read + ' · ' + read.source.status}
27
+ </div>
28
+ @if (read.source.retired) {
29
+ <span class="odt-badge">retired</span>
30
+ }
31
+ @if (read.source.historical) {
32
+ <span class="odt-badge">historical</span>
33
+ }
34
+ @if (read.source.retained) {
35
+ <span class="odt-badge">retained value</span>
36
+ }
37
+ @if (read.source.refreshing) {
38
+ <span class="odt-badge">refreshing</span>
39
+ }
40
+ @if (read.source.connection !== 'none') {
41
+ <span class="odt-badge">{'stream: ' + read.source.connection}</span>
42
+ }
43
+ @if (read.source.complete) {
44
+ <span class="odt-badge">complete</span>
45
+ }
46
+ <div class="odt-muted">
47
+ {'Node revision ' + read.source.revision + ', scope epoch ' + read.source.epoch}
48
+ </div>
49
+ @if (read.source.dependencies.length > 0) {
50
+ <div class="odt-muted">
51
+ {'Dependencies: ' +
52
+ read.source.dependencies.map(
53
+ (dependency) => dependency.scopeKey + ':' + dependency.key,
54
+ ).join(', ')}
55
+ </div>
56
+ }
57
+ </div>
58
+ } @else {
59
+ <span class="odt-kv-key">Native source</span>
60
+ }
61
+ <span class="odt-kv-val">
62
+ {'Read revision ' +
63
+ (read.observedVersion === null ? 'uncommitted' : read.observedVersion) +
64
+ ' → ' +
65
+ read.currentVersion}
66
+ </span>
67
+ </div>
68
+ }
69
+ }
70
+ </div>
71
+ }
8
72
 
9
73
  function TreeRow(props: {
10
74
  node: WireTreeNode;
@@ -34,20 +98,30 @@ function TreeRow(props: {
34
98
  export function ComponentsTab(props: { client: OctaneDevtoolsEventClient }): OctaneNode {
35
99
  const { client } = props;
36
100
  const [nodes, setNodes] = useState<WireTreeNode[]>([]);
37
- const [selected, setSelected] = useState<number | null>(null);
101
+ const [selected, setSelected, getSelected] = useState<number | null>(null);
38
102
  const [detail, setDetail] = useState<NodeDetail | null>(null);
39
103
 
40
104
  useEffect(() => {
41
105
  const offTree = client.on('tree', (e) => setNodes(e.payload.nodes));
42
- const offInspect = client.on('inspect', (e) => setDetail(e.payload));
106
+ const offInspect = client.on('inspect', (e) => {
107
+ if (e.payload.id === getSelected()) setDetail(e.payload);
108
+ });
109
+ const offClear = client.on('inspect-clear', (e) => {
110
+ if (e.payload.id === getSelected()) {
111
+ setDetail(null);
112
+ setSelected(null);
113
+ }
114
+ });
43
115
  return () => {
44
116
  offTree();
45
117
  offInspect();
118
+ offClear();
46
119
  };
47
120
  });
48
121
 
49
122
  const onSelect = (id: number) => {
50
123
  setSelected(id);
124
+ setDetail(null);
51
125
  client.emit('inspect-request', { id });
52
126
  };
53
127
 
@@ -91,6 +165,30 @@ export function ComponentsTab(props: { client: OctaneDevtoolsEventClient }): Oct
91
165
  }
92
166
  </>
93
167
  }
168
+ @if (detail.nativeReads) {
169
+ <div>
170
+ <div class="odt-h">Native reads</div>
171
+ <div class="odt-muted">{'Scheduled owner #' + detail.nativeReads.ownerId}</div>
172
+ <NativeReadRows
173
+ label="Accepted reads"
174
+ reads={detail.nativeReads.committed?.reads ?? []}
175
+ />
176
+ @if (detail.nativeReads.committed?.mixed) {
177
+ <div class="odt-muted">Mixed revisions in this attempt.</div>
178
+ }
179
+ @for (const attempt of detail.nativeReads.pending; index i) {
180
+ <div>
181
+ <NativeReadRows label="Pending attempt" reads={attempt.reads} />
182
+ @if (attempt.mixed) {
183
+ <div class="odt-muted">Mixed revisions in this attempt.</div>
184
+ }
185
+ </div>
186
+ }
187
+ @if (detail.nativeReads.retry.length > 0) {
188
+ <NativeReadRows label="Suspended retry sources" reads={detail.nativeReads.retry} />
189
+ }
190
+ </div>
191
+ }
94
192
  </div>
95
193
  } @else {
96
194
  <div class="odt-empty">Select a component.</div>