@lokvis/runtime 0.2.0-beta.0 → 0.2.1

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 (62) hide show
  1. package/LICENSE +21 -0
  2. package/dist/asset-store.d.ts +5 -2
  3. package/dist/asset-store.d.ts.map +1 -1
  4. package/dist/asset-store.js +93 -3
  5. package/dist/asset-store.js.map +1 -1
  6. package/dist/batch-processor.d.ts +200 -0
  7. package/dist/batch-processor.d.ts.map +1 -0
  8. package/dist/batch-processor.js +505 -0
  9. package/dist/batch-processor.js.map +1 -0
  10. package/dist/degradation.d.ts +109 -0
  11. package/dist/degradation.d.ts.map +1 -0
  12. package/dist/degradation.js +184 -0
  13. package/dist/degradation.js.map +1 -0
  14. package/dist/history-store.d.ts +76 -0
  15. package/dist/history-store.d.ts.map +1 -0
  16. package/dist/history-store.js +109 -0
  17. package/dist/history-store.js.map +1 -0
  18. package/dist/idb-asset-store.js +1 -1
  19. package/dist/idb-asset-store.js.map +1 -1
  20. package/dist/index.d.ts +5 -0
  21. package/dist/index.d.ts.map +1 -1
  22. package/dist/index.js +5 -0
  23. package/dist/index.js.map +1 -1
  24. package/dist/memory-guard.d.ts +109 -0
  25. package/dist/memory-guard.d.ts.map +1 -0
  26. package/dist/memory-guard.js +161 -0
  27. package/dist/memory-guard.js.map +1 -0
  28. package/dist/opfs-asset-store.d.ts +35 -4
  29. package/dist/opfs-asset-store.d.ts.map +1 -1
  30. package/dist/opfs-asset-store.js +94 -10
  31. package/dist/opfs-asset-store.js.map +1 -1
  32. package/dist/runtime.d.ts +130 -6
  33. package/dist/runtime.d.ts.map +1 -1
  34. package/dist/runtime.js +404 -16
  35. package/dist/runtime.js.map +1 -1
  36. package/dist/types.d.ts +106 -1
  37. package/dist/types.d.ts.map +1 -1
  38. package/dist/worker-host.d.ts +13 -0
  39. package/dist/worker-host.d.ts.map +1 -1
  40. package/dist/worker-host.js +47 -15
  41. package/dist/worker-host.js.map +1 -1
  42. package/dist/worker-protocol.d.ts +10 -1
  43. package/dist/worker-protocol.d.ts.map +1 -1
  44. package/dist/worker-protocol.js.map +1 -1
  45. package/dist/workflow-builder.d.ts +124 -0
  46. package/dist/workflow-builder.d.ts.map +1 -0
  47. package/dist/workflow-builder.js +240 -0
  48. package/dist/workflow-builder.js.map +1 -0
  49. package/package.json +3 -2
  50. package/src/asset-store.ts +103 -3
  51. package/src/batch-processor.ts +699 -0
  52. package/src/degradation.ts +241 -0
  53. package/src/history-store.ts +159 -0
  54. package/src/idb-asset-store.ts +1 -1
  55. package/src/index.ts +5 -0
  56. package/src/memory-guard.ts +202 -0
  57. package/src/opfs-asset-store.ts +118 -10
  58. package/src/runtime.ts +445 -12
  59. package/src/types.ts +107 -0
  60. package/src/worker-host.ts +56 -16
  61. package/src/worker-protocol.ts +11 -1
  62. package/src/workflow-builder.ts +303 -0
@@ -0,0 +1,240 @@
1
+ /**
2
+ * WorkflowBuilder - 线性工作流链式构造器(W10.1)
3
+ *
4
+ * 提供链式 API 构造线性 Workflow(第一年不支持 Branch/Loop/Condition/Parallel)。
5
+ *
6
+ * 限制:
7
+ * - 最多 5 步(M1 MVP 约束,免费用户定位,避免复杂度过高)
8
+ * - 节点必须为 transform 类型(load/export 由执行器隐式处理)
9
+ * - 自动生成线性 edges:node[i] → node[i+1]
10
+ *
11
+ * 使用示例:
12
+ * ```typescript
13
+ * const workflow = new WorkflowBuilder('my-workflow', 'Web 优化')
14
+ * .setInput({ type: 'image', multiple: true })
15
+ * .setOutput({ type: 'image', format: 'webp' })
16
+ * .add('image.resize', { width: 1920, fit: 'inside' })
17
+ * .add('image.compress', { format: 'webp', quality: 80 })
18
+ * .add('image.watermark', { text: '@lokvis' })
19
+ * .build();
20
+ * ```
21
+ *
22
+ * 与 workflow-slice 的 buildLinearWorkflow 区别:
23
+ * - buildLinearWorkflow 是内部工具函数,从 WorkspaceNode[] 构造无校验
24
+ * - WorkflowBuilder 是公开 API,链式调用 + 5 步上限校验 + 元数据完善
25
+ *
26
+ * @module workflow-builder
27
+ */
28
+ /** 工作流最大节点数(M1 MVP 约束) */
29
+ export const MAX_WORKFLOW_STEPS = 5;
30
+ /**
31
+ * WorkflowBuilder - 链式构造线性 Workflow。
32
+ *
33
+ * 抛错时机:
34
+ * - add() 超过 maxSteps 时立即抛错(避免构造完才发现步数超限)
35
+ * - build() 在节点为空 / 输入输出未设置时抛错
36
+ * - remove()/move() 在节点 id 不存在时静默(链式 API 容错)
37
+ */
38
+ export class WorkflowBuilder {
39
+ options;
40
+ maxSteps;
41
+ nodes = [];
42
+ input = null;
43
+ output = null;
44
+ nodeCounter = 0;
45
+ constructor(options) {
46
+ if (!options.id)
47
+ throw new Error('WorkflowBuilder: id is required');
48
+ if (!options.name)
49
+ throw new Error('WorkflowBuilder: name is required');
50
+ this.options = options;
51
+ this.maxSteps = options.maxSteps ?? MAX_WORKFLOW_STEPS;
52
+ }
53
+ /** 设置工作流输入定义 */
54
+ setInput(input) {
55
+ this.input = input;
56
+ return this;
57
+ }
58
+ /** 设置工作流输出定义 */
59
+ setOutput(output) {
60
+ this.output = output;
61
+ return this;
62
+ }
63
+ /**
64
+ * 添加一个节点到链尾。
65
+ *
66
+ * @param capability 能力名,如 `image.resize`
67
+ * @param params 能力参数(可选,默认空对象)
68
+ * @param label 节点标签(可选,UI 显示用)
69
+ * @throws Error 当节点数达到 maxSteps 时
70
+ */
71
+ add(capability, params = {}, label) {
72
+ if (!capability)
73
+ throw new Error('WorkflowBuilder.add: capability is required');
74
+ if (this.nodes.length >= this.maxSteps) {
75
+ throw new Error(`WorkflowBuilder.add: max steps (${this.maxSteps}) reached; ` +
76
+ `cannot add more nodes. Adjust maxSteps option if needed.`);
77
+ }
78
+ this.nodeCounter += 1;
79
+ this.nodes.push({
80
+ id: `${this.options.id}-node-${this.nodeCounter}`,
81
+ capability,
82
+ params,
83
+ label,
84
+ });
85
+ return this;
86
+ }
87
+ /**
88
+ * 按 capability 或节点 id 移除节点。
89
+ * 若有多个同 capability 节点,只移除第一个。
90
+ * 若不存在则静默(链式 API 容错)。
91
+ *
92
+ * @param capabilityOrId 能力名或节点 id
93
+ */
94
+ remove(capabilityOrId) {
95
+ const idx = this.nodes.findIndex((n) => n.id === capabilityOrId || n.capability === capabilityOrId);
96
+ if (idx >= 0) {
97
+ this.nodes.splice(idx, 1);
98
+ }
99
+ return this;
100
+ }
101
+ /**
102
+ * 移动节点到新位置(线性链中重排)。
103
+ *
104
+ * @param from 源位置(0-based 索引)
105
+ * @param to 目标位置(0-based 索引,移动后该节点的新位置)
106
+ * @throws Error 当索引越界
107
+ */
108
+ move(from, to) {
109
+ if (from < 0 || from >= this.nodes.length) {
110
+ throw new Error(`WorkflowBuilder.move: from index ${from} out of range`);
111
+ }
112
+ if (to < 0 || to >= this.nodes.length) {
113
+ throw new Error(`WorkflowBuilder.move: to index ${to} out of range`);
114
+ }
115
+ if (from === to)
116
+ return this;
117
+ const [node] = this.nodes.splice(from, 1);
118
+ // splice(from, 1) 在已校验的 in-range 索引上必返回 1 元素;
119
+ // 此处显式检查以满足 noUncheckedIndexedAccess,并在不变量被打破时报错
120
+ if (!node) {
121
+ throw new Error(`WorkflowBuilder.move: source node at index ${from} missing`);
122
+ }
123
+ this.nodes.splice(to, 0, node);
124
+ return this;
125
+ }
126
+ /** 交换两个节点位置 */
127
+ swap(i, j) {
128
+ if (i < 0 || i >= this.nodes.length || j < 0 || j >= this.nodes.length) {
129
+ throw new Error('WorkflowBuilder.swap: index out of range');
130
+ }
131
+ if (i === j)
132
+ return this;
133
+ const a = this.nodes[i];
134
+ const b = this.nodes[j];
135
+ // 上方范围校验保证 i / j 在界内;显式检查以满足 noUncheckedIndexedAccess
136
+ if (!a || !b) {
137
+ throw new Error('WorkflowBuilder.swap: node missing (invariant violated)');
138
+ }
139
+ this.nodes[i] = b;
140
+ this.nodes[j] = a;
141
+ return this;
142
+ }
143
+ /** 更新指定节点的参数 */
144
+ updateParams(nodeId, params) {
145
+ const node = this.nodes.find((n) => n.id === nodeId);
146
+ if (node) {
147
+ node.params = { ...node.params, ...params };
148
+ }
149
+ return this;
150
+ }
151
+ /** 当前节点数 */
152
+ get size() {
153
+ return this.nodes.length;
154
+ }
155
+ /** 是否已达最大节点数 */
156
+ get isFull() {
157
+ return this.nodes.length >= this.maxSteps;
158
+ }
159
+ /** 获取节点的只读副本(用于 UI 预览) */
160
+ getNodes() {
161
+ return this.nodes.map((n) => ({ ...n }));
162
+ }
163
+ /**
164
+ * 构造 Workflow 实例。
165
+ *
166
+ * @throws Error 当节点为空 / 输入未设置 / 输出未设置
167
+ */
168
+ build() {
169
+ if (this.nodes.length === 0) {
170
+ throw new Error('WorkflowBuilder.build: no nodes added');
171
+ }
172
+ if (!this.input) {
173
+ throw new Error('WorkflowBuilder.build: input not set (call setInput first)');
174
+ }
175
+ if (!this.output) {
176
+ throw new Error('WorkflowBuilder.build: output not set (call setOutput first)');
177
+ }
178
+ const workflowNodes = this.nodes.map((n) => ({
179
+ id: n.id,
180
+ type: 'transform',
181
+ capability: n.capability,
182
+ params: n.params,
183
+ label: n.label,
184
+ }));
185
+ const edges = [];
186
+ for (let i = 0; i < workflowNodes.length - 1; i++) {
187
+ const fromNode = workflowNodes[i];
188
+ const toNode = workflowNodes[i + 1];
189
+ // 循环边界 i < length - 1 保证 i 与 i+1 均在界内;显式检查以满足 noUncheckedIndexedAccess
190
+ if (!fromNode || !toNode) {
191
+ throw new Error('WorkflowBuilder.build: edge node missing (invariant violated)');
192
+ }
193
+ edges.push({
194
+ from: fromNode.id,
195
+ to: toNode.id,
196
+ });
197
+ }
198
+ const now = Date.now();
199
+ return {
200
+ $schema: `https://lokvis.dev/schemas/workflow.json`,
201
+ id: this.options.id,
202
+ version: '1.0.0',
203
+ name: this.options.name,
204
+ description: this.options.description ?? '',
205
+ author: this.options.author ?? { id: 'local', name: 'Local User' },
206
+ category: this.options.category ?? 'image',
207
+ tags: this.options.tags ?? [],
208
+ nodes: workflowNodes,
209
+ edges,
210
+ inputs: this.input,
211
+ outputs: this.output,
212
+ official: this.options.official ?? false,
213
+ createdAt: now,
214
+ updatedAt: now,
215
+ };
216
+ }
217
+ }
218
+ /** 从 Workflow 反向构造 Builder(用于编辑已有工作流) */
219
+ export function workflowToBuilder(workflow, maxSteps) {
220
+ const builder = new WorkflowBuilder({
221
+ id: workflow.id,
222
+ name: workflow.name,
223
+ description: workflow.description,
224
+ author: workflow.author,
225
+ category: workflow.category,
226
+ tags: workflow.tags,
227
+ official: workflow.official,
228
+ maxSteps,
229
+ });
230
+ builder.setInput(workflow.inputs);
231
+ builder.setOutput(workflow.outputs);
232
+ // 按拓扑顺序添加节点(线性链 nodes 数组即顺序)
233
+ for (const node of workflow.nodes) {
234
+ if (node.type === 'transform' && node.capability) {
235
+ builder.add(node.capability, node.params ?? {}, node.label);
236
+ }
237
+ }
238
+ return builder;
239
+ }
240
+ //# sourceMappingURL=workflow-builder.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"workflow-builder.js","sourceRoot":"","sources":["../src/workflow-builder.ts"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,9 +1,10 @@
1
1
  {
2
2
  "name": "@lokvis/runtime",
3
- "version": "0.2.0-beta.0",
3
+ "version": "0.2.1",
4
4
  "description": "Lokvis Runtime - Browser-local workflow execution engine",
5
5
  "license": "MIT",
6
6
  "type": "module",
7
+ "sideEffects": false,
7
8
  "main": "./dist/index.js",
8
9
  "module": "./dist/index.js",
9
10
  "types": "./dist/index.d.ts",
@@ -25,7 +26,7 @@
25
26
  "dependencies": {
26
27
  "dexie": "^4.0.10",
27
28
  "mitt": "^3.0.1",
28
- "@lokvis/schema": "0.2.0-beta.0"
29
+ "@lokvis/schema": "0.2.1"
29
30
  },
30
31
  "devDependencies": {
31
32
  "fake-indexeddb": "^6.0.0",
@@ -78,21 +78,121 @@ export interface PreparedImport {
78
78
  type: Asset['type'];
79
79
  }
80
80
 
81
+ /** 富元数据提取结果(基础 metadata 之外的可选字段) */
82
+ interface RichMetadata {
83
+ dimensions?: { width: number; height: number };
84
+ duration?: number;
85
+ pages?: number;
86
+ }
87
+
88
+ /**
89
+ * 从 Blob 提取富元数据(dimensions/duration/pages)。
90
+ *
91
+ * 提取策略:
92
+ * - image: createImageBitmap → width/height
93
+ * - video/audio: HTMLMediaElement + loadedmetadata → duration
94
+ * - pdf: 暂不提取(需 pdf.js,Phase 2 补全)
95
+ * - 其余: 返回空
96
+ *
97
+ * 所有提取均 try/catch:失败时返回空对象,不阻断 import。
98
+ * Node.js / 测试环境可能无 createImageBitmap / document,自然降级为空。
99
+ */
100
+ async function extractRichMetadata(
101
+ blob: Blob,
102
+ type: Asset['type']
103
+ ): Promise<RichMetadata> {
104
+ try {
105
+ switch (type) {
106
+ case 'image':
107
+ return await extractImageDimensions(blob);
108
+ case 'video':
109
+ case 'audio':
110
+ return await extractMediaDuration(blob, type);
111
+ default:
112
+ return {};
113
+ }
114
+ } catch {
115
+ return {};
116
+ }
117
+ }
118
+
119
+ /** 用 createImageBitmap 提取图片尺寸(浏览器原生 API,失败返回空) */
120
+ async function extractImageDimensions(blob: Blob): Promise<RichMetadata> {
121
+ if (typeof createImageBitmap !== 'function') return {};
122
+ const bitmap = await createImageBitmap(blob);
123
+ try {
124
+ return { dimensions: { width: bitmap.width, height: bitmap.height } };
125
+ } finally {
126
+ // 释放 ImageBitmap 资源,避免内存泄漏(浏览器 GC 不保证立即回收)
127
+ if (typeof bitmap.close === 'function') bitmap.close();
128
+ }
129
+ }
130
+
131
+ /** 用 HTMLMediaElement 提取视频/音频时长(浏览器环境,失败返回空) */
132
+ async function extractMediaDuration(
133
+ blob: Blob,
134
+ type: 'video' | 'audio'
135
+ ): Promise<RichMetadata> {
136
+ if (typeof document === 'undefined') return {};
137
+ const url = URL.createObjectURL(blob);
138
+ try {
139
+ const el = document.createElement(type === 'video' ? 'video' : 'audio');
140
+ el.preload = 'metadata';
141
+ el.src = url;
142
+ return await new Promise<RichMetadata>((resolve) => {
143
+ let settled = false;
144
+ const finish = (result: RichMetadata) => {
145
+ if (settled) return;
146
+ settled = true;
147
+ el.onloadedmetadata = null;
148
+ el.onerror = null;
149
+ el.removeAttribute('src');
150
+ clearTimeout(timer);
151
+ resolve(result);
152
+ };
153
+ el.onloadedmetadata = () => {
154
+ const duration = el.duration;
155
+ finish(Number.isFinite(duration) ? { duration } : {});
156
+ };
157
+ el.onerror = () => finish({});
158
+ // M2 修复:超时兜底,防止坏文件既不触发 loadedmetadata 也不触发 onerror
159
+ // 导致 Promise 永久挂起 + 外层 finally 的 revokeObjectURL 永不执行(泄漏)
160
+ const timer = setTimeout(() => finish({}), MEDIA_DURATION_TIMEOUT_MS);
161
+ });
162
+ } finally {
163
+ URL.revokeObjectURL(url);
164
+ }
165
+ }
166
+
167
+ /** 媒体时长提取超时(5s,足够解码大多数媒体头) */
168
+ const MEDIA_DURATION_TIMEOUT_MS = 5000;
169
+
81
170
  /**
82
171
  * 从 AssetSource 准备导入数据(共享逻辑,供 Memory/OPFS/IDB store 复用):
83
- * 提取 blob + MIME、生成 id、推断 type、构造 metadata。
172
+ * 提取 blob + MIME、生成 id、推断 type、构造 metadata(含富元数据)
84
173
  * 各 store 只需负责"写入 blob + 存元数据"。
174
+ *
175
+ * W6.4:异步提取 dimensions(image)/ duration(video/audio)/ pages(pdf,暂 stub)。
176
+ * 富元数据提取失败时静默降级为 undefined,不阻断 import。
85
177
  */
86
- export function prepareImport(source: AssetSource): PreparedImport {
178
+ export async function prepareImport(source: AssetSource): Promise<PreparedImport> {
87
179
  const { blob, mimeType: rawMime } = extractBlobFromSource(source);
88
180
  const id = generateId();
89
181
  const mimeType = rawMime || 'application/octet-stream';
90
182
  const type = inferAssetType(mimeType);
183
+
91
184
  const metadata: AssetMetadata = {
92
185
  mimeType,
93
186
  size: blob.size,
94
187
  format: getFormatFromMime(mimeType),
95
188
  };
189
+
190
+ // 富元数据(异步,失败静默降级)
191
+ const rich = await extractRichMetadata(blob, type);
192
+ if (rich.dimensions) metadata.dimensions = rich.dimensions;
193
+ if (rich.duration !== undefined) metadata.duration = rich.duration;
194
+ if (rich.pages !== undefined) metadata.pages = rich.pages;
195
+
96
196
  return { id, blob, metadata, type };
97
197
  }
98
198
 
@@ -140,7 +240,7 @@ export function createMemoryAssetStore(): AssetStore {
140
240
 
141
241
  return {
142
242
  async import(source) {
143
- const { id, blob, metadata, type } = prepareImport(source);
243
+ const { id, blob, metadata, type } = await prepareImport(source);
144
244
  blobs.set(id, blob);
145
245
  const asset = buildAsset(id, blob, metadata, type, MEMORY_PATH_PREFIX);
146
246
  assets.set(id, asset);