@smithers-orchestrator/devtools 0.16.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/LICENSE +21 -0
- package/package.json +31 -0
- package/src/DevToolsDelta.ts +8 -0
- package/src/DevToolsDeltaOp.ts +8 -0
- package/src/DevToolsEngineEvent.ts +98 -0
- package/src/DevToolsEventBus.ts +9 -0
- package/src/DevToolsEventHandler.ts +6 -0
- package/src/DevToolsNode.ts +23 -0
- package/src/DevToolsRunStore.js +232 -0
- package/src/DevToolsRunStoreOptions.ts +6 -0
- package/src/DevToolsSnapshot.ts +48 -0
- package/src/DevToolsSnapshotV1.ts +9 -0
- package/src/InvalidDeltaError.js +11 -0
- package/src/RunExecutionState.ts +18 -0
- package/src/SMITHERS_NODE_ICONS.js +21 -0
- package/src/SNAPSHOT_SERIALIZER_DEFAULT_MAX_DEPTH.js +2 -0
- package/src/SmithersDevToolsCore.js +146 -0
- package/src/SmithersDevToolsOptions.ts +11 -0
- package/src/SmithersNodeType.ts +17 -0
- package/src/SnapshotSerializerOptions.ts +7 -0
- package/src/SnapshotSerializerWarning.ts +9 -0
- package/src/TaskExecutionState.ts +21 -0
- package/src/applyDelta.js +115 -0
- package/src/buildSnapshot.js +15 -0
- package/src/collectTasks.js +15 -0
- package/src/countNodes.js +16 -0
- package/src/diffSnapshots.js +211 -0
- package/src/findNodeById.js +17 -0
- package/src/index.d.ts +550 -0
- package/src/index.js +30 -0
- package/src/printTree.js +35 -0
- package/src/snapshotSerializer.js +143 -0
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { buildSnapshot } from "./buildSnapshot.js";
|
|
2
|
+
import { collectTasks } from "./collectTasks.js";
|
|
3
|
+
import { DevToolsRunStore } from "./DevToolsRunStore.js";
|
|
4
|
+
import { findNodeById } from "./findNodeById.js";
|
|
5
|
+
import { printTree } from "./printTree.js";
|
|
6
|
+
|
|
7
|
+
/** @typedef {import("./DevToolsEngineEvent.ts").DevToolsEngineEvent} DevToolsEngineEvent */
|
|
8
|
+
/** @typedef {import("./DevToolsEventBus.ts").DevToolsEventBus} DevToolsEventBus */
|
|
9
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
10
|
+
/** @typedef {import("./DevToolsSnapshot.ts").DevToolsSnapshot} DevToolsSnapshot */
|
|
11
|
+
/** @typedef {import("./RunExecutionState.ts").RunExecutionState} RunExecutionState */
|
|
12
|
+
/** @typedef {import("./SmithersDevToolsOptions.ts").SmithersDevToolsOptions} SmithersDevToolsOptions */
|
|
13
|
+
/** @typedef {import("./TaskExecutionState.ts").TaskExecutionState} TaskExecutionState */
|
|
14
|
+
|
|
15
|
+
export class SmithersDevToolsCore {
|
|
16
|
+
/** @type {SmithersDevToolsOptions} */
|
|
17
|
+
options;
|
|
18
|
+
/** @type {DevToolsSnapshot | null} */
|
|
19
|
+
_lastSnapshot = null;
|
|
20
|
+
/** @type {DevToolsRunStore} */
|
|
21
|
+
_runStore;
|
|
22
|
+
/**
|
|
23
|
+
* @param {SmithersDevToolsOptions} [options]
|
|
24
|
+
*/
|
|
25
|
+
constructor(options = {}) {
|
|
26
|
+
this.options = options;
|
|
27
|
+
this._runStore = new DevToolsRunStore(options);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* @param {DevToolsNode | null} tree
|
|
31
|
+
* @returns {DevToolsSnapshot}
|
|
32
|
+
*/
|
|
33
|
+
captureSnapshot(tree) {
|
|
34
|
+
const snapshot = buildSnapshot(tree);
|
|
35
|
+
this._lastSnapshot = snapshot;
|
|
36
|
+
return snapshot;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* @param {DevToolsSnapshot} [snapshot]
|
|
40
|
+
* @returns {DevToolsSnapshot}
|
|
41
|
+
*/
|
|
42
|
+
emitCommit(snapshot = this._lastSnapshot ?? buildSnapshot(null)) {
|
|
43
|
+
this.options.onCommit?.("commit", snapshot);
|
|
44
|
+
return snapshot;
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* @param {DevToolsNode | null} tree
|
|
48
|
+
* @returns {DevToolsSnapshot}
|
|
49
|
+
*/
|
|
50
|
+
captureCommit(tree) {
|
|
51
|
+
const snapshot = this.captureSnapshot(tree);
|
|
52
|
+
this.emitCommit(snapshot);
|
|
53
|
+
return snapshot;
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* @param {DevToolsSnapshot} [snapshot]
|
|
57
|
+
* @returns {DevToolsSnapshot}
|
|
58
|
+
*/
|
|
59
|
+
emitUnmount(snapshot = this._lastSnapshot ?? buildSnapshot(null)) {
|
|
60
|
+
this.options.onCommit?.("unmount", snapshot);
|
|
61
|
+
return snapshot;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @param {DevToolsEventBus} bus
|
|
65
|
+
* @returns {this}
|
|
66
|
+
*/
|
|
67
|
+
attachEventBus(bus) {
|
|
68
|
+
this._runStore.attachEventBus(bus);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
/** @returns {void} */
|
|
72
|
+
detachEventBuses() {
|
|
73
|
+
this._runStore.detachEventBuses();
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* @param {DevToolsEngineEvent} event
|
|
77
|
+
* @returns {void}
|
|
78
|
+
*/
|
|
79
|
+
processEngineEvent(event) {
|
|
80
|
+
this._runStore.processEngineEvent(event);
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* @param {string} runId
|
|
84
|
+
* @returns {RunExecutionState | undefined}
|
|
85
|
+
*/
|
|
86
|
+
getRun(runId) {
|
|
87
|
+
return this._runStore.getRun(runId);
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* @returns {Map<string, RunExecutionState>}
|
|
91
|
+
*/
|
|
92
|
+
get runs() {
|
|
93
|
+
return this._runStore.runs;
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* @param {string} runId
|
|
97
|
+
* @param {string} nodeId
|
|
98
|
+
* @param {number} [iteration]
|
|
99
|
+
* @returns {TaskExecutionState | undefined}
|
|
100
|
+
*/
|
|
101
|
+
getTaskState(runId, nodeId, iteration) {
|
|
102
|
+
return this._runStore.getTaskState(runId, nodeId, iteration);
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Get the last captured snapshot.
|
|
106
|
+
* @returns {DevToolsSnapshot | null}
|
|
107
|
+
*/
|
|
108
|
+
get snapshot() {
|
|
109
|
+
return this._lastSnapshot;
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Get the current tree (shorthand).
|
|
113
|
+
* @returns {DevToolsNode | null}
|
|
114
|
+
*/
|
|
115
|
+
get tree() {
|
|
116
|
+
return this._lastSnapshot?.tree ?? null;
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Pretty-print the current tree to a string.
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
printTree() {
|
|
123
|
+
if (!this._lastSnapshot?.tree)
|
|
124
|
+
return "(no tree captured yet)";
|
|
125
|
+
return printTree(this._lastSnapshot.tree);
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Find a node by task nodeId.
|
|
129
|
+
* @param {string} nodeId
|
|
130
|
+
* @returns {DevToolsNode | null}
|
|
131
|
+
*/
|
|
132
|
+
findTask(nodeId) {
|
|
133
|
+
if (!this._lastSnapshot?.tree)
|
|
134
|
+
return null;
|
|
135
|
+
return findNodeById(this._lastSnapshot.tree, nodeId);
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* List all tasks in the current tree.
|
|
139
|
+
* @returns {DevToolsNode[]}
|
|
140
|
+
*/
|
|
141
|
+
listTasks() {
|
|
142
|
+
if (!this._lastSnapshot?.tree)
|
|
143
|
+
return [];
|
|
144
|
+
return collectTasks(this._lastSnapshot.tree);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { DevToolsEngineEvent } from "./DevToolsEngineEvent.ts";
|
|
2
|
+
import type { DevToolsEventHandler } from "./DevToolsEventHandler.ts";
|
|
3
|
+
|
|
4
|
+
export type SmithersDevToolsOptions = {
|
|
5
|
+
/** Called on every renderer commit that touches the Smithers tree */
|
|
6
|
+
onCommit?: DevToolsEventHandler;
|
|
7
|
+
/** Called on every SmithersEvent from an attached EventBus */
|
|
8
|
+
onEngineEvent?: (event: DevToolsEngineEvent) => void;
|
|
9
|
+
/** Enable verbose console logging */
|
|
10
|
+
verbose?: boolean;
|
|
11
|
+
};
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export type SmithersNodeType =
|
|
2
|
+
| "workflow"
|
|
3
|
+
| "task"
|
|
4
|
+
| "sequence"
|
|
5
|
+
| "parallel"
|
|
6
|
+
| "merge-queue"
|
|
7
|
+
| "branch"
|
|
8
|
+
| "loop"
|
|
9
|
+
| "worktree"
|
|
10
|
+
| "approval"
|
|
11
|
+
| "timer"
|
|
12
|
+
| "subflow"
|
|
13
|
+
| "wait-for-event"
|
|
14
|
+
| "saga"
|
|
15
|
+
| "try-catch"
|
|
16
|
+
| "fragment"
|
|
17
|
+
| "unknown";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/** Execution state for a task, derived from SmithersEvent stream */
|
|
2
|
+
export type TaskExecutionState = {
|
|
3
|
+
nodeId: string;
|
|
4
|
+
iteration: number;
|
|
5
|
+
status:
|
|
6
|
+
| "pending"
|
|
7
|
+
| "started"
|
|
8
|
+
| "finished"
|
|
9
|
+
| "failed"
|
|
10
|
+
| "cancelled"
|
|
11
|
+
| "skipped"
|
|
12
|
+
| "waiting-approval"
|
|
13
|
+
| "waiting-event"
|
|
14
|
+
| "waiting-timer"
|
|
15
|
+
| "retrying";
|
|
16
|
+
attempt: number;
|
|
17
|
+
startedAt?: number;
|
|
18
|
+
finishedAt?: number;
|
|
19
|
+
error?: unknown;
|
|
20
|
+
toolCalls: Array<{ name: string; seq: number; status?: "success" | "error" }>;
|
|
21
|
+
};
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
2
|
+
/** @typedef {import("./DevToolsDelta.ts").DevToolsDelta} DevToolsDelta */
|
|
3
|
+
/** @typedef {import("./DevToolsSnapshotV1.ts").DevToolsSnapshotV1} DevToolsSnapshotV1 */
|
|
4
|
+
|
|
5
|
+
import { InvalidDeltaError } from "./InvalidDeltaError.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* @param {unknown} value
|
|
9
|
+
* @returns {unknown}
|
|
10
|
+
*/
|
|
11
|
+
function cloneValue(value) {
|
|
12
|
+
if (typeof structuredClone === "function") {
|
|
13
|
+
return structuredClone(value);
|
|
14
|
+
}
|
|
15
|
+
return JSON.parse(JSON.stringify(value));
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {DevToolsNode} root
|
|
20
|
+
* @param {number} id
|
|
21
|
+
* @returns {{ node: DevToolsNode; parent: DevToolsNode | null; index: number } | null}
|
|
22
|
+
*/
|
|
23
|
+
function findNode(root, id) {
|
|
24
|
+
/** @type {Array<{ node: DevToolsNode; parent: DevToolsNode | null; index: number }>} */
|
|
25
|
+
const stack = [{ node: root, parent: null, index: -1 }];
|
|
26
|
+
while (stack.length > 0) {
|
|
27
|
+
const current = stack.pop();
|
|
28
|
+
if (!current) {
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (current.node.id === id) {
|
|
32
|
+
return current;
|
|
33
|
+
}
|
|
34
|
+
for (let index = current.node.children.length - 1; index >= 0; index -= 1) {
|
|
35
|
+
const child = current.node.children[index];
|
|
36
|
+
stack.push({ node: child, parent: current.node, index });
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Apply a delta to a snapshot. Throws `InvalidDeltaError` for malformed ops.
|
|
44
|
+
*
|
|
45
|
+
* @param {DevToolsSnapshotV1} snapshot
|
|
46
|
+
* @param {DevToolsDelta} delta
|
|
47
|
+
* @returns {DevToolsSnapshotV1}
|
|
48
|
+
*/
|
|
49
|
+
export function applyDelta(snapshot, delta) {
|
|
50
|
+
if (delta.version !== 1) {
|
|
51
|
+
throw new InvalidDeltaError(`Unsupported delta version: ${String(delta.version)}`);
|
|
52
|
+
}
|
|
53
|
+
if (delta.baseSeq !== snapshot.seq) {
|
|
54
|
+
throw new InvalidDeltaError(`Delta base seq ${delta.baseSeq} does not match snapshot seq ${snapshot.seq}.`);
|
|
55
|
+
}
|
|
56
|
+
/** @type {DevToolsSnapshotV1} */
|
|
57
|
+
const next = {
|
|
58
|
+
...snapshot,
|
|
59
|
+
frameNo: delta.seq,
|
|
60
|
+
seq: delta.seq,
|
|
61
|
+
root: /** @type {DevToolsNode} */ (cloneValue(snapshot.root)),
|
|
62
|
+
};
|
|
63
|
+
for (const op of delta.ops) {
|
|
64
|
+
if (op.op === "replaceRoot") {
|
|
65
|
+
if (!op.node || typeof op.node !== "object") {
|
|
66
|
+
throw new InvalidDeltaError("replaceRoot requires a node.");
|
|
67
|
+
}
|
|
68
|
+
next.root = /** @type {DevToolsNode} */ (cloneValue(op.node));
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (op.op === "removeNode") {
|
|
72
|
+
if (op.id === next.root.id) {
|
|
73
|
+
throw new InvalidDeltaError("Cannot remove the root node.");
|
|
74
|
+
}
|
|
75
|
+
const target = findNode(next.root, op.id);
|
|
76
|
+
if (!target || !target.parent) {
|
|
77
|
+
throw new InvalidDeltaError(`Unknown node id: ${op.id}`);
|
|
78
|
+
}
|
|
79
|
+
target.parent.children.splice(target.index, 1);
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (op.op === "addNode") {
|
|
83
|
+
const parent = findNode(next.root, op.parentId);
|
|
84
|
+
if (!parent) {
|
|
85
|
+
throw new InvalidDeltaError(`Unknown parent id: ${op.parentId}`);
|
|
86
|
+
}
|
|
87
|
+
const index = Math.max(0, Math.min(op.index, parent.node.children.length));
|
|
88
|
+
parent.node.children.splice(index, 0, /** @type {DevToolsNode} */ (cloneValue(op.node)));
|
|
89
|
+
continue;
|
|
90
|
+
}
|
|
91
|
+
if (op.op === "updateProps") {
|
|
92
|
+
const target = findNode(next.root, op.id);
|
|
93
|
+
if (!target) {
|
|
94
|
+
throw new InvalidDeltaError(`Unknown node id: ${op.id}`);
|
|
95
|
+
}
|
|
96
|
+
target.node.props = /** @type {Record<string, unknown>} */ (cloneValue(op.props));
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
if (op.op === "updateTask") {
|
|
100
|
+
const target = findNode(next.root, op.id);
|
|
101
|
+
if (!target) {
|
|
102
|
+
throw new InvalidDeltaError(`Unknown node id: ${op.id}`);
|
|
103
|
+
}
|
|
104
|
+
if (op.task === undefined) {
|
|
105
|
+
delete target.node.task;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
target.node.task = /** @type {DevToolsNode["task"]} */ (cloneValue(op.task));
|
|
109
|
+
}
|
|
110
|
+
continue;
|
|
111
|
+
}
|
|
112
|
+
throw new InvalidDeltaError(`Unknown op: ${String(op?.op)}`);
|
|
113
|
+
}
|
|
114
|
+
return next;
|
|
115
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { countNodes } from "./countNodes.js";
|
|
2
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
3
|
+
/** @typedef {import("./DevToolsSnapshot.ts").DevToolsSnapshot} DevToolsSnapshot */
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @param {DevToolsNode | null} root
|
|
7
|
+
* @returns {DevToolsSnapshot}
|
|
8
|
+
*/
|
|
9
|
+
export function buildSnapshot(root) {
|
|
10
|
+
if (!root) {
|
|
11
|
+
return { tree: null, nodeCount: 0, taskCount: 0, timestamp: Date.now() };
|
|
12
|
+
}
|
|
13
|
+
const { nodes, tasks } = countNodes(root);
|
|
14
|
+
return { tree: root, nodeCount: nodes, taskCount: tasks, timestamp: Date.now() };
|
|
15
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
|
|
2
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
3
|
+
/**
|
|
4
|
+
* @param {DevToolsNode} node
|
|
5
|
+
* @param {DevToolsNode[]} [out]
|
|
6
|
+
* @returns {DevToolsNode[]}
|
|
7
|
+
*/
|
|
8
|
+
export function collectTasks(node, out = []) {
|
|
9
|
+
if (node.type === "task")
|
|
10
|
+
out.push(node);
|
|
11
|
+
for (const child of node.children) {
|
|
12
|
+
collectTasks(child, out);
|
|
13
|
+
}
|
|
14
|
+
return out;
|
|
15
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
|
|
2
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
3
|
+
/**
|
|
4
|
+
* @param {DevToolsNode} node
|
|
5
|
+
* @returns {{ nodes: number; tasks: number }}
|
|
6
|
+
*/
|
|
7
|
+
export function countNodes(node) {
|
|
8
|
+
let nodes = 1;
|
|
9
|
+
let tasks = node.type === "task" ? 1 : 0;
|
|
10
|
+
for (const child of node.children) {
|
|
11
|
+
const c = countNodes(child);
|
|
12
|
+
nodes += c.nodes;
|
|
13
|
+
tasks += c.tasks;
|
|
14
|
+
}
|
|
15
|
+
return { nodes, tasks };
|
|
16
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
2
|
+
/** @typedef {import("./DevToolsDelta.ts").DevToolsDelta} DevToolsDelta */
|
|
3
|
+
/** @typedef {import("./DevToolsDeltaOp.ts").DevToolsDeltaOp} DevToolsDeltaOp */
|
|
4
|
+
/** @typedef {import("./DevToolsSnapshotV1.ts").DevToolsSnapshotV1} DevToolsSnapshotV1 */
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* @param {unknown} value
|
|
8
|
+
* @returns {unknown}
|
|
9
|
+
*/
|
|
10
|
+
function cloneValue(value) {
|
|
11
|
+
if (typeof structuredClone === "function") {
|
|
12
|
+
return structuredClone(value);
|
|
13
|
+
}
|
|
14
|
+
return JSON.parse(JSON.stringify(value));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* @param {unknown} value
|
|
19
|
+
* @returns {boolean}
|
|
20
|
+
*/
|
|
21
|
+
function isRecord(value) {
|
|
22
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* @param {unknown} left
|
|
27
|
+
* @param {unknown} right
|
|
28
|
+
* @returns {boolean}
|
|
29
|
+
*/
|
|
30
|
+
function deepEqual(left, right) {
|
|
31
|
+
if (Object.is(left, right)) {
|
|
32
|
+
return true;
|
|
33
|
+
}
|
|
34
|
+
if (typeof left !== typeof right) {
|
|
35
|
+
return false;
|
|
36
|
+
}
|
|
37
|
+
if (left === null || right === null) {
|
|
38
|
+
return left === right;
|
|
39
|
+
}
|
|
40
|
+
if (Array.isArray(left) && Array.isArray(right)) {
|
|
41
|
+
if (left.length !== right.length) {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
for (let index = 0; index < left.length; index += 1) {
|
|
45
|
+
if (!deepEqual(left[index], right[index])) {
|
|
46
|
+
return false;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
if (isRecord(left) && isRecord(right)) {
|
|
52
|
+
const leftKeys = Object.keys(left);
|
|
53
|
+
const rightKeys = Object.keys(right);
|
|
54
|
+
if (leftKeys.length !== rightKeys.length) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
leftKeys.sort();
|
|
58
|
+
rightKeys.sort();
|
|
59
|
+
for (let index = 0; index < leftKeys.length; index += 1) {
|
|
60
|
+
if (leftKeys[index] !== rightKeys[index]) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
for (const key of leftKeys) {
|
|
65
|
+
if (!deepEqual(left[key], right[key])) {
|
|
66
|
+
return false;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return true;
|
|
70
|
+
}
|
|
71
|
+
return false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* @param {DevToolsNode} root
|
|
76
|
+
* @returns {Map<number, { node: DevToolsNode; parentId: number | null; index: number }>}
|
|
77
|
+
*/
|
|
78
|
+
function indexTree(root) {
|
|
79
|
+
/** @type {Map<number, { node: DevToolsNode; parentId: number | null; index: number }>} */
|
|
80
|
+
const indexed = new Map();
|
|
81
|
+
/** @type {Array<{ node: DevToolsNode; parentId: number | null; index: number }>} */
|
|
82
|
+
const stack = [{ node: root, parentId: null, index: 0 }];
|
|
83
|
+
while (stack.length > 0) {
|
|
84
|
+
const current = stack.pop();
|
|
85
|
+
if (!current) {
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
indexed.set(current.node.id, current);
|
|
89
|
+
for (let childIndex = current.node.children.length - 1; childIndex >= 0; childIndex -= 1) {
|
|
90
|
+
const child = current.node.children[childIndex];
|
|
91
|
+
stack.push({
|
|
92
|
+
node: child,
|
|
93
|
+
parentId: current.node.id,
|
|
94
|
+
index: childIndex,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return indexed;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* @param {DevToolsNode} left
|
|
103
|
+
* @param {DevToolsNode} right
|
|
104
|
+
* @returns {boolean}
|
|
105
|
+
*/
|
|
106
|
+
function sameNodeShape(left, right) {
|
|
107
|
+
return left.type === right.type &&
|
|
108
|
+
left.name === right.name &&
|
|
109
|
+
left.depth === right.depth;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Compute a delta from snapshot `a` to snapshot `b`.
|
|
114
|
+
*
|
|
115
|
+
* @param {DevToolsSnapshotV1} a
|
|
116
|
+
* @param {DevToolsSnapshotV1} b
|
|
117
|
+
* @returns {DevToolsDelta}
|
|
118
|
+
*/
|
|
119
|
+
export function diffSnapshots(a, b) {
|
|
120
|
+
if (a.runId !== b.runId) {
|
|
121
|
+
throw new Error("Cannot diff snapshots from different runs.");
|
|
122
|
+
}
|
|
123
|
+
// Root replacement: if the root's identity (id) changed, or its shape
|
|
124
|
+
// changed, emit a single replaceRoot op rather than trying to remove + add
|
|
125
|
+
// the root in place (the root cannot be removed).
|
|
126
|
+
if (a.root.id !== b.root.id || !sameNodeShape(a.root, b.root)) {
|
|
127
|
+
return {
|
|
128
|
+
version: 1,
|
|
129
|
+
baseSeq: a.seq,
|
|
130
|
+
seq: b.seq,
|
|
131
|
+
ops: [{
|
|
132
|
+
op: "replaceRoot",
|
|
133
|
+
node: /** @type {DevToolsNode} */ (cloneValue(b.root)),
|
|
134
|
+
}],
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
const from = indexTree(a.root);
|
|
138
|
+
const to = indexTree(b.root);
|
|
139
|
+
/** @type {Set<number>} */
|
|
140
|
+
const removeSet = new Set();
|
|
141
|
+
/** @type {Set<number>} */
|
|
142
|
+
const addSet = new Set();
|
|
143
|
+
/** @type {DevToolsDeltaOp[]} */
|
|
144
|
+
const updateOps = [];
|
|
145
|
+
for (const [id, fromEntry] of from.entries()) {
|
|
146
|
+
const toEntry = to.get(id);
|
|
147
|
+
if (!toEntry) {
|
|
148
|
+
removeSet.add(id);
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
const moved = fromEntry.parentId !== toEntry.parentId || fromEntry.index !== toEntry.index;
|
|
152
|
+
const replaced = !sameNodeShape(fromEntry.node, toEntry.node);
|
|
153
|
+
if (moved || replaced) {
|
|
154
|
+
removeSet.add(id);
|
|
155
|
+
addSet.add(id);
|
|
156
|
+
continue;
|
|
157
|
+
}
|
|
158
|
+
if (!deepEqual(fromEntry.node.props, toEntry.node.props)) {
|
|
159
|
+
updateOps.push({
|
|
160
|
+
op: "updateProps",
|
|
161
|
+
id,
|
|
162
|
+
props: /** @type {Record<string, unknown>} */ (cloneValue(toEntry.node.props)),
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
if (!deepEqual(fromEntry.node.task, toEntry.node.task)) {
|
|
166
|
+
updateOps.push({
|
|
167
|
+
op: "updateTask",
|
|
168
|
+
id,
|
|
169
|
+
task: /** @type {DevToolsNode["task"]} */ (cloneValue(toEntry.node.task)),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
for (const [id] of to.entries()) {
|
|
174
|
+
if (!from.has(id)) {
|
|
175
|
+
addSet.add(id);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** @type {DevToolsDeltaOp[]} */
|
|
179
|
+
const removeOps = [...removeSet]
|
|
180
|
+
.sort((leftId, rightId) => (from.get(rightId)?.node.depth ?? 0) - (from.get(leftId)?.node.depth ?? 0))
|
|
181
|
+
.map((id) => ({ op: "removeNode", id }));
|
|
182
|
+
const topLevelAddIds = [...addSet].filter((id) => {
|
|
183
|
+
const parentId = to.get(id)?.parentId;
|
|
184
|
+
return parentId === null || !addSet.has(parentId);
|
|
185
|
+
});
|
|
186
|
+
/** @type {DevToolsDeltaOp[]} */
|
|
187
|
+
const addOps = topLevelAddIds
|
|
188
|
+
.sort((leftId, rightId) => (to.get(leftId)?.node.depth ?? 0) - (to.get(rightId)?.node.depth ?? 0))
|
|
189
|
+
.map((id) => {
|
|
190
|
+
const entry = to.get(id);
|
|
191
|
+
if (!entry || entry.parentId === null) {
|
|
192
|
+
return /** @type {DevToolsDeltaOp} */ ({
|
|
193
|
+
op: "updateProps",
|
|
194
|
+
id,
|
|
195
|
+
props: /** @type {Record<string, unknown>} */ (cloneValue(entry?.node.props ?? {})),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
return /** @type {DevToolsDeltaOp} */ ({
|
|
199
|
+
op: "addNode",
|
|
200
|
+
parentId: entry.parentId,
|
|
201
|
+
index: entry.index,
|
|
202
|
+
node: /** @type {DevToolsNode} */ (cloneValue(entry.node)),
|
|
203
|
+
});
|
|
204
|
+
});
|
|
205
|
+
return {
|
|
206
|
+
version: 1,
|
|
207
|
+
baseSeq: a.seq,
|
|
208
|
+
seq: b.seq,
|
|
209
|
+
ops: [...removeOps, ...addOps, ...updateOps],
|
|
210
|
+
};
|
|
211
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
|
|
2
|
+
/** @typedef {import("./DevToolsNode.ts").DevToolsNode} DevToolsNode */
|
|
3
|
+
/**
|
|
4
|
+
* @param {DevToolsNode} node
|
|
5
|
+
* @param {string} nodeId
|
|
6
|
+
* @returns {DevToolsNode | null}
|
|
7
|
+
*/
|
|
8
|
+
export function findNodeById(node, nodeId) {
|
|
9
|
+
if (node.task?.nodeId === nodeId)
|
|
10
|
+
return node;
|
|
11
|
+
for (const child of node.children) {
|
|
12
|
+
const found = findNodeById(child, nodeId);
|
|
13
|
+
if (found)
|
|
14
|
+
return found;
|
|
15
|
+
}
|
|
16
|
+
return null;
|
|
17
|
+
}
|