@lokvis/runtime 0.1.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 (61) hide show
  1. package/dist/asset-store.d.ts +72 -0
  2. package/dist/asset-store.d.ts.map +1 -0
  3. package/dist/asset-store.js +167 -0
  4. package/dist/asset-store.js.map +1 -0
  5. package/dist/capability-registry.d.ts +38 -0
  6. package/dist/capability-registry.d.ts.map +1 -0
  7. package/dist/capability-registry.js +100 -0
  8. package/dist/capability-registry.js.map +1 -0
  9. package/dist/event-bus.d.ts +9 -0
  10. package/dist/event-bus.d.ts.map +1 -0
  11. package/dist/event-bus.js +33 -0
  12. package/dist/event-bus.js.map +1 -0
  13. package/dist/executor.d.ts +42 -0
  14. package/dist/executor.d.ts.map +1 -0
  15. package/dist/executor.js +237 -0
  16. package/dist/executor.js.map +1 -0
  17. package/dist/history.d.ts +92 -0
  18. package/dist/history.d.ts.map +1 -0
  19. package/dist/history.js +171 -0
  20. package/dist/history.js.map +1 -0
  21. package/dist/idb-asset-store.d.ts +46 -0
  22. package/dist/idb-asset-store.d.ts.map +1 -0
  23. package/dist/idb-asset-store.js +76 -0
  24. package/dist/idb-asset-store.js.map +1 -0
  25. package/dist/index.d.ts +19 -0
  26. package/dist/index.d.ts.map +1 -0
  27. package/dist/index.js +19 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/opfs-asset-store.d.ts +38 -0
  30. package/dist/opfs-asset-store.d.ts.map +1 -0
  31. package/dist/opfs-asset-store.js +122 -0
  32. package/dist/opfs-asset-store.js.map +1 -0
  33. package/dist/runtime.d.ts +87 -0
  34. package/dist/runtime.d.ts.map +1 -0
  35. package/dist/runtime.js +475 -0
  36. package/dist/runtime.js.map +1 -0
  37. package/dist/types.d.ts +99 -0
  38. package/dist/types.d.ts.map +1 -0
  39. package/dist/types.js +8 -0
  40. package/dist/types.js.map +1 -0
  41. package/dist/worker-host.d.ts +154 -0
  42. package/dist/worker-host.d.ts.map +1 -0
  43. package/dist/worker-host.js +394 -0
  44. package/dist/worker-host.js.map +1 -0
  45. package/dist/worker-protocol.d.ts +103 -0
  46. package/dist/worker-protocol.d.ts.map +1 -0
  47. package/dist/worker-protocol.js +87 -0
  48. package/dist/worker-protocol.js.map +1 -0
  49. package/package.json +49 -0
  50. package/src/asset-store.ts +222 -0
  51. package/src/capability-registry.ts +135 -0
  52. package/src/event-bus.ts +41 -0
  53. package/src/executor.ts +303 -0
  54. package/src/history.ts +229 -0
  55. package/src/idb-asset-store.ts +121 -0
  56. package/src/index.ts +20 -0
  57. package/src/opfs-asset-store.ts +167 -0
  58. package/src/runtime.ts +564 -0
  59. package/src/types.ts +121 -0
  60. package/src/worker-host.ts +531 -0
  61. package/src/worker-protocol.ts +187 -0
@@ -0,0 +1,237 @@
1
+ /**
2
+ * Lokvis Workflow Executor
3
+ *
4
+ * 第一年只支持 Linear Workflow(线性链)。
5
+ * 不支持:Branch / Loop / Condition / Parallel。
6
+ */
7
+ /** 创建执行上下文 */
8
+ function createExecutionContext(workflowId, nodeId, signal, onProgress) {
9
+ return {
10
+ workflowId,
11
+ nodeId,
12
+ signal,
13
+ onProgress,
14
+ log: (level, message) => {
15
+ // 日志输出,可被 Analytics 监听
16
+ if (level === 'error') {
17
+ console.error(`[lokvis:${workflowId}:${nodeId}] ${message}`);
18
+ }
19
+ else if (level === 'warn') {
20
+ console.warn(`[lokvis:${workflowId}:${nodeId}] ${message}`);
21
+ }
22
+ },
23
+ };
24
+ }
25
+ /** 拓扑排序:将 edges 转换为线性节点序列 */
26
+ function topologicalSort(nodes, edges) {
27
+ const nodeMap = new Map(nodes.map((n) => [n.id, n]));
28
+ const inDegree = new Map();
29
+ const adjacency = new Map();
30
+ for (const node of nodes) {
31
+ inDegree.set(node.id, 0);
32
+ adjacency.set(node.id, []);
33
+ }
34
+ for (const edge of edges) {
35
+ inDegree.set(edge.to, (inDegree.get(edge.to) ?? 0) + 1);
36
+ adjacency.get(edge.from)?.push(edge.to);
37
+ }
38
+ const queue = [];
39
+ for (const [id, deg] of inDegree) {
40
+ if (deg === 0)
41
+ queue.push(id);
42
+ }
43
+ const sorted = [];
44
+ while (queue.length > 0) {
45
+ const id = queue.shift();
46
+ const node = nodeMap.get(id);
47
+ if (node)
48
+ sorted.push(node);
49
+ for (const next of adjacency.get(id) ?? []) {
50
+ const newDeg = (inDegree.get(next) ?? 0) - 1;
51
+ inDegree.set(next, newDeg);
52
+ if (newDeg === 0)
53
+ queue.push(next);
54
+ }
55
+ }
56
+ if (sorted.length !== nodes.length) {
57
+ throw new Error('Workflow contains a cycle, cannot execute');
58
+ }
59
+ return sorted;
60
+ }
61
+ /** Workflow 执行器 */
62
+ export class WorkflowExecutor {
63
+ config;
64
+ running = new Map();
65
+ /** 暂停时挂起的 resolve:resume/cancel 唤醒之,避免轮询 */
66
+ resumeResolvers = new Map();
67
+ constructor(config) {
68
+ this.config = config;
69
+ }
70
+ /** 执行工作流 */
71
+ async execute(workflow, inputs) {
72
+ const workflowId = workflow.id;
73
+ const abortController = new AbortController();
74
+ const startTime = Date.now();
75
+ // 解析输入 Asset
76
+ let currentAssets;
77
+ if (inputs.length === 0 || typeof inputs[0] === 'string') {
78
+ // AssetId[] - 从 store 加载
79
+ const ids = inputs;
80
+ currentAssets = await Promise.all(ids.map(async (id) => {
81
+ const asset = await this.config.assetStore.get(id);
82
+ if (!asset)
83
+ throw new Error(`Asset not found: ${id}`);
84
+ return asset;
85
+ }));
86
+ }
87
+ else {
88
+ // Asset[]
89
+ currentAssets = inputs;
90
+ }
91
+ // 注册运行状态
92
+ const state = {
93
+ id: workflowId,
94
+ workflow,
95
+ status: 'running',
96
+ abortController,
97
+ };
98
+ this.running.set(workflowId, state);
99
+ this.config.eventBus.emit({
100
+ type: 'workflow:started',
101
+ workflowId,
102
+ workflow,
103
+ });
104
+ try {
105
+ // 拓扑排序节点
106
+ const sortedNodes = topologicalSort(workflow.nodes, workflow.edges);
107
+ // 过滤掉 load 和 export 节点(load 由输入处理,export 由结果处理)
108
+ const transformNodes = sortedNodes.filter((n) => n.type === 'transform');
109
+ // 依次执行每个 transform 节点
110
+ for (const node of transformNodes) {
111
+ // 暂停则挂起,等待 resume/cancel 唤醒(Promise resolver,无轮询)
112
+ if (state.status === 'paused') {
113
+ await this.waitForResume(workflowId);
114
+ }
115
+ // 唤醒后或每轮起始,若已被取消立即跳出(避免执行多余节点)
116
+ if (state.status === 'cancelled') {
117
+ break;
118
+ }
119
+ state.currentNodeId = node.id;
120
+ const nodeStart = Date.now();
121
+ this.config.eventBus.emit({
122
+ type: 'node:started',
123
+ workflowId,
124
+ nodeId: node.id,
125
+ inputs: currentAssets,
126
+ });
127
+ // 解析能力实现(transform 节点必须有 capability)
128
+ const capability = node.capability;
129
+ if (!capability) {
130
+ throw new Error(`Transform node "${node.id}" has no capability`);
131
+ }
132
+ const impl = this.config.capabilityRegistry.resolve(capability);
133
+ if (!impl) {
134
+ throw new Error(`No implementation registered for capability "${capability}"`);
135
+ }
136
+ // 执行能力
137
+ const ctx = createExecutionContext(workflowId, node.id, abortController.signal);
138
+ const outputs = await impl.execute(currentAssets, node.params ?? {}, ctx);
139
+ currentAssets = outputs;
140
+ this.config.eventBus.emit({
141
+ type: 'node:finished',
142
+ workflowId,
143
+ nodeId: node.id,
144
+ capability,
145
+ params: node.params ?? {},
146
+ outputs: currentAssets,
147
+ duration: Date.now() - nodeStart,
148
+ });
149
+ }
150
+ const result = {
151
+ workflowId,
152
+ outputs: currentAssets.map((a) => a.id),
153
+ duration: Date.now() - startTime,
154
+ status: state.status === 'cancelled' ? 'cancelled' : 'completed',
155
+ };
156
+ this.config.eventBus.emit({
157
+ type: 'workflow:completed',
158
+ workflowId,
159
+ result,
160
+ });
161
+ return result;
162
+ }
163
+ catch (error) {
164
+ const result = {
165
+ workflowId,
166
+ outputs: [],
167
+ duration: Date.now() - startTime,
168
+ status: 'failed',
169
+ error: error instanceof Error ? error.message : String(error),
170
+ };
171
+ this.config.eventBus.emit({
172
+ type: 'node:failed',
173
+ workflowId,
174
+ nodeId: state.currentNodeId ?? 'unknown',
175
+ error: error instanceof Error ? error : new Error(String(error)),
176
+ });
177
+ this.config.eventBus.emit({
178
+ type: 'workflow:completed',
179
+ workflowId,
180
+ result,
181
+ });
182
+ return result;
183
+ }
184
+ finally {
185
+ this.running.delete(workflowId);
186
+ this.resumeResolvers.delete(workflowId);
187
+ }
188
+ }
189
+ /** 取消执行 */
190
+ async cancel(workflowId) {
191
+ const state = this.running.get(workflowId);
192
+ if (!state)
193
+ return;
194
+ state.status = 'cancelled';
195
+ state.abortController.abort();
196
+ // 唤醒可能暂停中的执行循环,使其跳出
197
+ this.resolveResume(workflowId);
198
+ this.config.eventBus.emit({ type: 'workflow:cancelled', workflowId });
199
+ }
200
+ /** 暂停执行 */
201
+ async pause(workflowId) {
202
+ const state = this.running.get(workflowId);
203
+ if (!state || state.status !== 'running')
204
+ return;
205
+ state.status = 'paused';
206
+ this.config.eventBus.emit({ type: 'workflow:paused', workflowId });
207
+ }
208
+ /** 恢复执行 */
209
+ async resume(workflowId) {
210
+ const state = this.running.get(workflowId);
211
+ if (!state || state.status !== 'paused')
212
+ return;
213
+ state.status = 'running';
214
+ // 唤醒挂起的 waitForResume,无需轮询
215
+ this.resolveResume(workflowId);
216
+ this.config.eventBus.emit({ type: 'workflow:resumed', workflowId });
217
+ }
218
+ /**
219
+ * 挂起当前执行直到 resume/cancel 唤醒(Promise resolver 模式)。
220
+ * 相比轮询(setTimeout)无延迟、无 CPU 占用;resume/cancel 通过
221
+ * resolveResume 触发 resolver。
222
+ */
223
+ waitForResume(workflowId) {
224
+ return new Promise((resolve) => {
225
+ this.resumeResolvers.set(workflowId, resolve);
226
+ });
227
+ }
228
+ /** 唤醒挂起的 waitForResume(若存在) */
229
+ resolveResume(workflowId) {
230
+ const resolve = this.resumeResolvers.get(workflowId);
231
+ if (resolve) {
232
+ this.resumeResolvers.delete(workflowId);
233
+ resolve();
234
+ }
235
+ }
236
+ }
237
+ //# sourceMappingURL=executor.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"executor.js","sourceRoot":"","sources":["../src/executor.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Lokvis History Stack
3
+ *
4
+ * 每个工作流拥有独立的 HistoryStack,记录每一步 transform 的 HistoryEntry。
5
+ * undo/redo 通过 cursor(游标)实现:
6
+ * - cursor 指向最后一条已应用的 entry
7
+ * - append 新操作时截断 cursor 之后的 redo 分支
8
+ * - 超出上限(默认 10 步)时 LRU 淘汰最旧条目并回调 onEvict 通知清理
9
+ *
10
+ * 每次变更通过回调通知调用方(由 Runtime 转发为 eventBus 的 history:changed)。
11
+ *
12
+ * 对应 PROJECT_PLAN W2 任务 2.1 / 2.2 / 2.4。
13
+ */
14
+ import type { HistoryEntry } from '@lokvis/schema';
15
+ /** HistoryStack 配置 */
16
+ export interface HistoryStackConfig {
17
+ /** 历史记录上限(默认 10 步) */
18
+ maxEntries: number;
19
+ /** 淘汰条目时的回调,用于清理 OPFS 资产 */
20
+ onEvict?: (entry: HistoryEntry) => void;
21
+ /** 历史变更时的回调,用于转发为 eventBus 事件 */
22
+ onChanged?: (workflowId: string, entries: HistoryEntry[]) => void;
23
+ }
24
+ /** HistoryStack 状态(供序列化/恢复使用) */
25
+ export interface HistoryStackSnapshot {
26
+ entries: HistoryEntry[];
27
+ cursor: number;
28
+ }
29
+ export declare class HistoryStack {
30
+ private readonly workflowId;
31
+ private entries;
32
+ /** 游标:指向最后一条已应用的 entry;-1 表示尚未应用任何条目 */
33
+ private cursor;
34
+ private readonly config;
35
+ constructor(workflowId: string, config?: Partial<HistoryStackConfig>);
36
+ /** 当前可应用的条目数(cursor 之后已截断) */
37
+ get length(): number;
38
+ /** cursor 位置(指向最后已应用条目,-1 表示空) */
39
+ get currentIndex(): number;
40
+ /** 是否可以 undo */
41
+ get canUndo(): boolean;
42
+ /** 是否可以 redo */
43
+ get canRedo(): boolean;
44
+ /** 所有历史条目(只读视图) */
45
+ list(): HistoryEntry[];
46
+ /** 仅返回已应用的条目(cursor 及之前) */
47
+ applied(): HistoryEntry[];
48
+ /** 仅返回 redo 分支中尚未应用的条目 */
49
+ redoBranch(): HistoryEntry[];
50
+ /**
51
+ * 追加一条历史记录。
52
+ * 若当前存在 redo 分支(cursor 之后的条目),则截断之,
53
+ * 并对被丢弃的条目触发 onEvict(清理其 outputs 资产,避免 OPFS/IDB 泄漏)。
54
+ * 若超过上限,从最旧端 LRU 淘汰,并触发 onEvict 回调。
55
+ */
56
+ append(entry: HistoryEntry): void;
57
+ /**
58
+ * 撤销一步:cursor 前移,返回当前应应用的 entry(即 undo 后的"当前"输出)。
59
+ * 返回值含义:
60
+ * - 如果 undo 后 cursor >= 0:返回该 entry(调用方应将其 outputs 作为当前状态)
61
+ * - 如果 undo 后 cursor < 0(回到初始):返回 null(调用方应恢复初始 inputs)
62
+ * 如果不能 undo(cursor < 0),返回 undefined 表示无操作。
63
+ */
64
+ undo(): HistoryEntry | null | undefined;
65
+ /**
66
+ * 重做一步:cursor 后移,返回重做后应应用的 entry。
67
+ * 如果不能 redo,返回 undefined 表示无操作。
68
+ */
69
+ redo(): HistoryEntry | undefined;
70
+ /** 跳转到指定条目(按 timestamp 顺序的索引) */
71
+ jumpTo(index: number): HistoryEntry | null | undefined;
72
+ /** 清空所有历史(不触发 onEvict,用于工作流销毁) */
73
+ clear(): void;
74
+ /**
75
+ * 重置历史:清空所有条目并对每条触发 onEvict(清理其 outputs 资产)。
76
+ * 用于工作流重新执行(run)时丢弃上一次(可能失败的)历史,避免资产泄漏。
77
+ * 与 clear() 的区别:clear() 仅重置内存状态,reset() 同时回收资产。
78
+ */
79
+ reset(): void;
80
+ /** 导出快照(用于持久化到 IndexedDB) */
81
+ snapshot(): HistoryStackSnapshot;
82
+ /** 从快照恢复 */
83
+ restore(snapshot: HistoryStackSnapshot): void;
84
+ /** 对一批被丢弃的条目触发 onEvict(忽略回调抛错) */
85
+ private notifyEvict;
86
+ /** 超过 maxEntries 时从最旧端淘汰,并触发 onEvict 回调 */
87
+ private evictIfNeeded;
88
+ private notifyChanged;
89
+ }
90
+ /** 工厂:创建 HistoryStack */
91
+ export declare function createHistoryStack(workflowId: string, config?: Partial<HistoryStackConfig>): HistoryStack;
92
+ //# sourceMappingURL=history.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.d.ts","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAEnD,sBAAsB;AACtB,MAAM,WAAW,kBAAkB;IACjC,sBAAsB;IACtB,UAAU,EAAE,MAAM,CAAC;IACnB,4BAA4B;IAC5B,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,YAAY,KAAK,IAAI,CAAC;IACxC,iCAAiC;IACjC,SAAS,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,YAAY,EAAE,KAAK,IAAI,CAAC;CACnE;AAED,iCAAiC;AACjC,MAAM,WAAW,oBAAoB;IACnC,OAAO,EAAE,YAAY,EAAE,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,qBAAa,YAAY;IAQrB,OAAO,CAAC,QAAQ,CAAC,UAAU;IAP7B,OAAO,CAAC,OAAO,CAAsB;IACrC,wCAAwC;IACxC,OAAO,CAAC,MAAM,CAAM;IACpB,OAAO,CAAC,QAAQ,CAAC,MAAM,CAC6B;gBAGjC,UAAU,EAAE,MAAM,EACnC,MAAM,GAAE,OAAO,CAAC,kBAAkB,CAAM;IAS1C,8BAA8B;IAC9B,IAAI,MAAM,IAAI,MAAM,CAEnB;IAED,kCAAkC;IAClC,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED,gBAAgB;IAChB,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,gBAAgB;IAChB,IAAI,OAAO,IAAI,OAAO,CAErB;IAED,mBAAmB;IACnB,IAAI,IAAI,YAAY,EAAE;IAItB,4BAA4B;IAC5B,OAAO,IAAI,YAAY,EAAE;IAIzB,0BAA0B;IAC1B,UAAU,IAAI,YAAY,EAAE;IAI5B;;;;;OAKG;IACH,MAAM,CAAC,KAAK,EAAE,YAAY,GAAG,IAAI;IAuBjC;;;;;;OAMG;IACH,IAAI,IAAI,YAAY,GAAG,IAAI,GAAG,SAAS;IAOvC;;;OAGG;IACH,IAAI,IAAI,YAAY,GAAG,SAAS;IAOhC,iCAAiC;IACjC,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,YAAY,GAAG,IAAI,GAAG,SAAS;IAOtD,kCAAkC;IAClC,KAAK,IAAI,IAAI;IAMb;;;;OAIG;IACH,KAAK,IAAI,IAAI;IASb,6BAA6B;IAC7B,QAAQ,IAAI,oBAAoB;IAOhC,YAAY;IACZ,OAAO,CAAC,QAAQ,EAAE,oBAAoB,GAAG,IAAI;IAQ7C,kCAAkC;IAClC,OAAO,CAAC,WAAW;IAYnB,2CAA2C;IAC3C,OAAO,CAAC,aAAa;IAmBrB,OAAO,CAAC,aAAa;CAGtB;AAED,yBAAyB;AACzB,wBAAgB,kBAAkB,CAChC,UAAU,EAAE,MAAM,EAClB,MAAM,CAAC,EAAE,OAAO,CAAC,kBAAkB,CAAC,GACnC,YAAY,CAEd"}
@@ -0,0 +1,171 @@
1
+ export class HistoryStack {
2
+ workflowId;
3
+ entries = [];
4
+ /** 游标:指向最后一条已应用的 entry;-1 表示尚未应用任何条目 */
5
+ cursor = -1;
6
+ config;
7
+ constructor(workflowId, config = {}) {
8
+ this.workflowId = workflowId;
9
+ this.config = {
10
+ maxEntries: config.maxEntries ?? 10,
11
+ onEvict: config.onEvict,
12
+ onChanged: config.onChanged,
13
+ };
14
+ }
15
+ /** 当前可应用的条目数(cursor 之后已截断) */
16
+ get length() {
17
+ return this.entries.length;
18
+ }
19
+ /** cursor 位置(指向最后已应用条目,-1 表示空) */
20
+ get currentIndex() {
21
+ return this.cursor;
22
+ }
23
+ /** 是否可以 undo */
24
+ get canUndo() {
25
+ return this.cursor >= 0;
26
+ }
27
+ /** 是否可以 redo */
28
+ get canRedo() {
29
+ return this.cursor < this.entries.length - 1;
30
+ }
31
+ /** 所有历史条目(只读视图) */
32
+ list() {
33
+ return [...this.entries];
34
+ }
35
+ /** 仅返回已应用的条目(cursor 及之前) */
36
+ applied() {
37
+ return this.entries.slice(0, this.cursor + 1);
38
+ }
39
+ /** 仅返回 redo 分支中尚未应用的条目 */
40
+ redoBranch() {
41
+ return this.entries.slice(this.cursor + 1);
42
+ }
43
+ /**
44
+ * 追加一条历史记录。
45
+ * 若当前存在 redo 分支(cursor 之后的条目),则截断之,
46
+ * 并对被丢弃的条目触发 onEvict(清理其 outputs 资产,避免 OPFS/IDB 泄漏)。
47
+ * 若超过上限,从最旧端 LRU 淘汰,并触发 onEvict 回调。
48
+ */
49
+ append(entry) {
50
+ if (entry.workflowId !== this.workflowId) {
51
+ throw new Error(`HistoryEntry.workflowId mismatch: expected "${this.workflowId}", got "${entry.workflowId}"`);
52
+ }
53
+ // 截断 redo 分支:cursor 之后的条目全部丢弃,并通知清理其 outputs 资产
54
+ if (this.cursor < this.entries.length - 1) {
55
+ const dropped = this.entries.slice(this.cursor + 1);
56
+ this.entries = this.entries.slice(0, this.cursor + 1);
57
+ this.notifyEvict(dropped);
58
+ }
59
+ this.entries.push(entry);
60
+ this.cursor = this.entries.length - 1;
61
+ // LRU 淘汰最旧条目
62
+ this.evictIfNeeded();
63
+ this.notifyChanged();
64
+ }
65
+ /**
66
+ * 撤销一步:cursor 前移,返回当前应应用的 entry(即 undo 后的"当前"输出)。
67
+ * 返回值含义:
68
+ * - 如果 undo 后 cursor >= 0:返回该 entry(调用方应将其 outputs 作为当前状态)
69
+ * - 如果 undo 后 cursor < 0(回到初始):返回 null(调用方应恢复初始 inputs)
70
+ * 如果不能 undo(cursor < 0),返回 undefined 表示无操作。
71
+ */
72
+ undo() {
73
+ if (!this.canUndo)
74
+ return undefined;
75
+ this.cursor -= 1;
76
+ this.notifyChanged();
77
+ return this.cursor >= 0 ? this.entries[this.cursor] : null;
78
+ }
79
+ /**
80
+ * 重做一步:cursor 后移,返回重做后应应用的 entry。
81
+ * 如果不能 redo,返回 undefined 表示无操作。
82
+ */
83
+ redo() {
84
+ if (!this.canRedo)
85
+ return undefined;
86
+ this.cursor += 1;
87
+ this.notifyChanged();
88
+ return this.entries[this.cursor];
89
+ }
90
+ /** 跳转到指定条目(按 timestamp 顺序的索引) */
91
+ jumpTo(index) {
92
+ if (index < -1 || index >= this.entries.length)
93
+ return undefined;
94
+ this.cursor = index;
95
+ this.notifyChanged();
96
+ return this.cursor >= 0 ? this.entries[this.cursor] : null;
97
+ }
98
+ /** 清空所有历史(不触发 onEvict,用于工作流销毁) */
99
+ clear() {
100
+ this.entries = [];
101
+ this.cursor = -1;
102
+ this.notifyChanged();
103
+ }
104
+ /**
105
+ * 重置历史:清空所有条目并对每条触发 onEvict(清理其 outputs 资产)。
106
+ * 用于工作流重新执行(run)时丢弃上一次(可能失败的)历史,避免资产泄漏。
107
+ * 与 clear() 的区别:clear() 仅重置内存状态,reset() 同时回收资产。
108
+ */
109
+ reset() {
110
+ if (this.entries.length > 0) {
111
+ this.notifyEvict(this.entries);
112
+ }
113
+ this.entries = [];
114
+ this.cursor = -1;
115
+ this.notifyChanged();
116
+ }
117
+ /** 导出快照(用于持久化到 IndexedDB) */
118
+ snapshot() {
119
+ return {
120
+ entries: [...this.entries],
121
+ cursor: this.cursor,
122
+ };
123
+ }
124
+ /** 从快照恢复 */
125
+ restore(snapshot) {
126
+ this.entries = [...snapshot.entries];
127
+ this.cursor = Math.max(-1, Math.min(snapshot.cursor, this.entries.length - 1));
128
+ this.notifyChanged();
129
+ }
130
+ // ─── 内部方法 ──────────────────────────────────────
131
+ /** 对一批被丢弃的条目触发 onEvict(忽略回调抛错) */
132
+ notifyEvict(entries) {
133
+ const onEvict = this.config.onEvict;
134
+ if (!onEvict || entries.length === 0)
135
+ return;
136
+ for (const entry of entries) {
137
+ try {
138
+ onEvict(entry);
139
+ }
140
+ catch {
141
+ // onEvict 失败不应阻断历史操作,由调用方日志记录
142
+ }
143
+ }
144
+ }
145
+ /** 超过 maxEntries 时从最旧端淘汰,并触发 onEvict 回调 */
146
+ evictIfNeeded() {
147
+ // 无 onEvict 回调时仍需维护上限(静默丢弃,调用方无法清理 OPFS,但内存不爆)
148
+ if (!this.config.onEvict) {
149
+ while (this.entries.length > this.config.maxEntries) {
150
+ this.entries.shift();
151
+ // cursor 跟随左移,但不低于 -1
152
+ this.cursor = Math.max(-1, this.cursor - 1);
153
+ }
154
+ return;
155
+ }
156
+ const evicted = [];
157
+ while (this.entries.length > this.config.maxEntries) {
158
+ evicted.push(this.entries.shift());
159
+ this.cursor = Math.max(-1, this.cursor - 1);
160
+ }
161
+ this.notifyEvict(evicted);
162
+ }
163
+ notifyChanged() {
164
+ this.config.onChanged?.(this.workflowId, this.list());
165
+ }
166
+ }
167
+ /** 工厂:创建 HistoryStack */
168
+ export function createHistoryStack(workflowId, config) {
169
+ return new HistoryStack(workflowId, config);
170
+ }
171
+ //# sourceMappingURL=history.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"history.js","sourceRoot":"","sources":["../src/history.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * IndexedDB Asset Store
3
+ *
4
+ * 元数据 + Blob 全部存 IndexedDB(通过 Dexie)。
5
+ * 当 OPFS 不可用时作为降级方案,数据持久化且支持跨刷新恢复。
6
+ *
7
+ * 降级链(W2.8 工厂):OPFS → IndexedDB(本实现) → Memory
8
+ */
9
+ import Dexie, { type Table } from 'dexie';
10
+ import type { Asset, AssetId } from '@lokvis/schema';
11
+ import type { AssetStore } from './asset-store.js';
12
+ /** IndexedDB 不可用时抛出 */
13
+ export declare class IdbUnavailableError extends Error {
14
+ constructor(message: string);
15
+ }
16
+ /** IDB 文件存储路径前缀(出现在 BlobHandle.path 中) */
17
+ export declare const IDB_PATH_PREFIX = "idb";
18
+ /** IDB AssetStore 配置 */
19
+ export interface IdbAssetStoreOptions {
20
+ /** 数据库名(默认 'lokvis-assets') */
21
+ dbName?: string;
22
+ /**
23
+ * 测试注入:自定义 Dexie 实例。
24
+ * 默认创建新实例。
25
+ */
26
+ dbInstance?: AssetDatabase;
27
+ }
28
+ /** IndexedDB 中的资产记录(Asset 元数据 + Blob 数据) */
29
+ export interface AssetRecord {
30
+ /** 主键 = Asset.id */
31
+ id: AssetId;
32
+ /** 资产元数据 */
33
+ asset: Asset;
34
+ /** 原始 Blob 数据(结构化克隆存储) */
35
+ blob: Blob;
36
+ }
37
+ /** Dexie 数据库封装 */
38
+ export declare class AssetDatabase extends Dexie {
39
+ assets: Table<AssetRecord, string>;
40
+ constructor(name?: string);
41
+ }
42
+ /** 检测当前环境是否支持 IndexedDB */
43
+ export declare function isIdbSupported(): boolean;
44
+ /** 创建 IndexedDB 版 AssetStore */
45
+ export declare function createIdbAssetStore(options?: IdbAssetStoreOptions): Promise<AssetStore>;
46
+ //# sourceMappingURL=idb-asset-store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb-asset-store.d.ts","sourceRoot":"","sources":["../src/idb-asset-store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,OAAO,KAAK,EAAE,EAAE,KAAK,KAAK,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,gBAAgB,CAAC;AACrD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,kBAAkB,CAAC;AAQnD,uBAAuB;AACvB,qBAAa,mBAAoB,SAAQ,KAAK;gBAChC,OAAO,EAAE,MAAM;CAI5B;AAED,0CAA0C;AAC1C,eAAO,MAAM,eAAe,QAAQ,CAAC;AAErC,wBAAwB;AACxB,MAAM,WAAW,oBAAoB;IACnC,+BAA+B;IAC/B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;OAGG;IACH,UAAU,CAAC,EAAE,aAAa,CAAC;CAC5B;AAED,4CAA4C;AAC5C,MAAM,WAAW,WAAW;IAC1B,oBAAoB;IACpB,EAAE,EAAE,OAAO,CAAC;IACZ,YAAY;IACZ,KAAK,EAAE,KAAK,CAAC;IACb,0BAA0B;IAC1B,IAAI,EAAE,IAAI,CAAC;CACZ;AAED,kBAAkB;AAClB,qBAAa,aAAc,SAAQ,KAAK;IACtC,MAAM,EAAG,KAAK,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC;gBAExB,IAAI,SAAkB;CAOnC;AAED,2BAA2B;AAC3B,wBAAgB,cAAc,IAAI,OAAO,CAGxC;AAED,gCAAgC;AAChC,wBAAsB,mBAAmB,CACvC,OAAO,GAAE,oBAAyB,GACjC,OAAO,CAAC,UAAU,CAAC,CA+CrB"}
@@ -0,0 +1,76 @@
1
+ /**
2
+ * IndexedDB Asset Store
3
+ *
4
+ * 元数据 + Blob 全部存 IndexedDB(通过 Dexie)。
5
+ * 当 OPFS 不可用时作为降级方案,数据持久化且支持跨刷新恢复。
6
+ *
7
+ * 降级链(W2.8 工厂):OPFS → IndexedDB(本实现) → Memory
8
+ */
9
+ import Dexie, {} from 'dexie';
10
+ import { buildAsset, generateId, parseBlobPath, prepareImport, } from './asset-store.js';
11
+ /** IndexedDB 不可用时抛出 */
12
+ export class IdbUnavailableError extends Error {
13
+ constructor(message) {
14
+ super(message);
15
+ this.name = 'IdbUnavailableError';
16
+ }
17
+ }
18
+ /** IDB 文件存储路径前缀(出现在 BlobHandle.path 中) */
19
+ export const IDB_PATH_PREFIX = 'idb';
20
+ /** Dexie 数据库封装 */
21
+ export class AssetDatabase extends Dexie {
22
+ assets;
23
+ constructor(name = 'lokvis-assets') {
24
+ super(name);
25
+ this.version(1).stores({
26
+ // 仅主键 id;按 type 查询由上层过滤 list() 实现,无需二级索引
27
+ assets: 'id',
28
+ });
29
+ }
30
+ }
31
+ /** 检测当前环境是否支持 IndexedDB */
32
+ export function isIdbSupported() {
33
+ // Dexie 始终可 import,但实际使用需要全局 indexedDB
34
+ return typeof indexedDB !== 'undefined';
35
+ }
36
+ /** 创建 IndexedDB 版 AssetStore */
37
+ export async function createIdbAssetStore(options = {}) {
38
+ if (!isIdbSupported() && !options.dbInstance) {
39
+ throw new IdbUnavailableError('IndexedDB is not available in this environment');
40
+ }
41
+ const db = options.dbInstance ?? new AssetDatabase(options.dbName);
42
+ return {
43
+ async import(source) {
44
+ const { id, blob, metadata, type } = prepareImport(source);
45
+ const asset = buildAsset(id, blob, metadata, type, IDB_PATH_PREFIX);
46
+ await db.assets.put({ id, asset, blob });
47
+ return asset;
48
+ },
49
+ async get(id) {
50
+ const record = await db.assets.get(id);
51
+ return record?.asset;
52
+ },
53
+ async getBlob(handle) {
54
+ const id = parseBlobPath(handle.path, IDB_PATH_PREFIX);
55
+ const record = await db.assets.get(id);
56
+ if (!record) {
57
+ throw new Error(`Blob not found in IndexedDB for path: ${handle.path}`);
58
+ }
59
+ return record.blob;
60
+ },
61
+ async remove(id) {
62
+ await db.assets.delete(id);
63
+ },
64
+ async list() {
65
+ const records = await db.assets.toArray();
66
+ return records.map((r) => r.asset);
67
+ },
68
+ async create(blob, metadata, type) {
69
+ const id = generateId();
70
+ const asset = buildAsset(id, blob, metadata, type, IDB_PATH_PREFIX);
71
+ await db.assets.put({ id, asset, blob });
72
+ return asset;
73
+ },
74
+ };
75
+ }
76
+ //# sourceMappingURL=idb-asset-store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"idb-asset-store.js","sourceRoot":"","sources":["../src/idb-asset-store.ts"],"names":[],"mappings":"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"}