@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
@@ -1,10 +1,15 @@
1
1
  /**
2
2
  * OPFS Asset Store
3
3
  *
4
- * 大文件(blob)存 OPFS(Origin Private File System),元数据存内存 Map
5
- * OPFS 是浏览器私有文件系统,blob 持久化(刷新后文件仍在),但元数据仅在内存 ——
6
- * 刷新后 list() 会返回空、get(id) 返回 undefined(除非元数据由上层持久化)。
7
- * 完整元数据持久化建议叠加 IdbAssetStore 或在 W3 补强。
4
+ * 大文件(blob)存 OPFS(Origin Private File System),元数据存内存 Map +
5
+ * IndexedDB(Dexie)持久化。OPFS 是浏览器私有文件系统,blob 持久化(刷新后
6
+ * 文件仍在);W6.6 起元数据也通过 Dexie 持久化,刷新后 list()/get() 能恢复。
7
+ *
8
+ * 持久化策略(W6.6):
9
+ * - 启动时:db.metadata.toArray() 预加载内存 Map(后续 get/list 零 IO)
10
+ * - import/create:写 OPFS 文件 + db.metadata.put + 内存 Map.set
11
+ * - remove:删 OPFS 文件 + db.metadata.delete + 内存 Map.delete
12
+ * - IndexedDB 不可用时降级为仅内存模式(刷新后丢失,与 W2 行为一致)
8
13
  *
9
14
  * 注:任务规格提及 FileSystemSyncAccessHandle(Worker 内同步句柄),
10
15
  * 但 AssetStore 接口本身为 async,主线程仅可使用 FileSystemFileHandle 异步 API,
@@ -13,6 +18,7 @@
13
18
  * 降级链(W2.8 工厂):OPFS → IndexedDB → Memory
14
19
  */
15
20
 
21
+ import Dexie, { type Table } from 'dexie';
16
22
  import type { Asset, AssetId } from '@lokvis/schema';
17
23
  import type { AssetStore } from './asset-store.js';
18
24
  import {
@@ -21,6 +27,7 @@ import {
21
27
  parseBlobPath,
22
28
  prepareImport,
23
29
  } from './asset-store.js';
30
+ import { isIdbSupported } from './idb-asset-store.js';
24
31
 
25
32
  /** OPFS 不可用或初始化失败时抛出 */
26
33
  export class OpfsUnavailableError extends Error {
@@ -36,6 +43,31 @@ export const OPFS_PATH_PREFIX = 'opfs';
36
43
  /** OPFS 文件名后缀 */
37
44
  const OPFS_FILE_SUFFIX = '.bin';
38
45
 
46
+ /**
47
+ * OPFS 元数据持久化数据库(W6.6)。
48
+ *
49
+ * 仅存 { id, asset }(不含 blob,blob 在 OPFS 文件里),用于刷新后恢复内存 Map。
50
+ * 数据库名独立于 IdbAssetStore 的 'lokvis-assets',避免与全持久化 store 冲突。
51
+ */
52
+ export class OpfsMetadataDatabase extends Dexie {
53
+ metadata!: Table<OpfsMetadataRecord, AssetId>;
54
+
55
+ constructor(name = 'lokvis-opfs-metadata') {
56
+ super(name);
57
+ this.version(1).stores({
58
+ metadata: 'id',
59
+ });
60
+ }
61
+ }
62
+
63
+ /** OPFS 元数据记录 */
64
+ export interface OpfsMetadataRecord {
65
+ /** 主键 = Asset.id */
66
+ id: AssetId;
67
+ /** 资产元数据(blob 在 OPFS 文件,不在此存) */
68
+ asset: Asset;
69
+ }
70
+
39
71
  /** OPFS AssetStore 配置 */
40
72
  export interface OpfsAssetStoreOptions {
41
73
  /** OPFS 根目录下的命名空间目录名(默认 'lokvis') */
@@ -47,6 +79,13 @@ export interface OpfsAssetStoreOptions {
47
79
  * 默认使用 navigator.storage.getDirectory()。
48
80
  */
49
81
  rootHandle?: FileSystemDirectoryHandle;
82
+ /**
83
+ * 测试注入:自定义元数据库实例(W6.6 持久化)。
84
+ * 默认在 IndexedDB 可用时自动创建 OpfsMetadataDatabase。
85
+ */
86
+ metadataDb?: OpfsMetadataDatabase;
87
+ /** 元数据库名(默认 'lokvis-opfs-metadata';仅 metadataDb 未注入时生效) */
88
+ metadataDbName?: string;
50
89
  }
51
90
 
52
91
  /** 检测当前环境是否支持 OPFS */
@@ -98,26 +137,85 @@ async function writeOpfsFile(
98
137
  return fileHandle;
99
138
  }
100
139
 
140
+ /**
141
+ * 初始化元数据库(W6.6)。
142
+ *
143
+ * 优先用 options.metadataDb(测试注入);否则在 IndexedDB 可用时创建新实例。
144
+ * IndexedDB 不可用时返回 undefined,调用方降级为仅内存模式。
145
+ */
146
+ function resolveMetadataDb(
147
+ options: OpfsAssetStoreOptions
148
+ ): OpfsMetadataDatabase | undefined {
149
+ if (options.metadataDb) return options.metadataDb;
150
+ if (!isIdbSupported()) return undefined;
151
+ try {
152
+ return new OpfsMetadataDatabase(options.metadataDbName);
153
+ } catch (err) {
154
+ console.warn('[lokvis] OPFS metadata db init failed, falling back to memory-only:', err);
155
+ return undefined;
156
+ }
157
+ }
158
+
159
+ /** 安全写入元数据库(失败仅 warn,不阻断主流程) */
160
+ async function persistMetadata(
161
+ db: OpfsMetadataDatabase | undefined,
162
+ id: AssetId,
163
+ asset: Asset
164
+ ): Promise<void> {
165
+ if (!db) return;
166
+ try {
167
+ await db.metadata.put({ id, asset });
168
+ } catch (err) {
169
+ console.warn(`[lokvis] OPFS metadata persist failed for ${id}:`, err);
170
+ }
171
+ }
172
+
173
+ /** 安全删除元数据(失败仅 warn) */
174
+ async function deleteMetadata(
175
+ db: OpfsMetadataDatabase | undefined,
176
+ id: AssetId
177
+ ): Promise<void> {
178
+ if (!db) return;
179
+ try {
180
+ await db.metadata.delete(id);
181
+ } catch (err) {
182
+ console.warn(`[lokvis] OPFS metadata delete failed for ${id}:`, err);
183
+ }
184
+ }
185
+
101
186
  /** 创建 OPFS 版 AssetStore */
102
187
  export async function createOpfsAssetStore(
103
188
  options: OpfsAssetStoreOptions = {}
104
189
  ): Promise<AssetStore> {
105
190
  const assetsDir = await resolveAssetsDir(options);
106
- /** 元数据缓存(内存中;持久化由 IdbAssetStore 负责) */
191
+ /** 元数据缓存(内存中;W6.6 起启动时从 Dexie 预加载) */
107
192
  const assets = new Map<AssetId, Asset>();
108
- /** id → 文件句柄(用于 getBlob/remove) */
193
+ /** id → 文件句柄(用于 getBlob/remove;刷新后丢失,按需重建) */
109
194
  const fileHandles = new Map<AssetId, FileSystemFileHandle>();
195
+ /** 元数据库(W6.6 持久化;undefined 时仅内存模式) */
196
+ const db = resolveMetadataDb(options);
197
+
198
+ // 启动时预加载已持久化的元数据(刷新后恢复 list/get)
199
+ if (db) {
200
+ try {
201
+ const records = await db.metadata.toArray();
202
+ for (const r of records) assets.set(r.id, r.asset);
203
+ } catch (err) {
204
+ console.warn('[lokvis] OPFS metadata preload failed:', err);
205
+ }
206
+ }
110
207
 
111
208
  /** 从 id 派生 OPFS 文件名 */
112
209
  const fileName = (id: AssetId) => `${id}${OPFS_FILE_SUFFIX}`;
113
210
 
114
211
  return {
115
212
  async import(source) {
116
- const { id, blob, metadata, type } = prepareImport(source);
213
+ const { id, blob, metadata, type } = await prepareImport(source);
117
214
  const handle = await writeOpfsFile(assetsDir, fileName(id), blob);
118
215
  fileHandles.set(id, handle);
119
216
  const asset = buildAsset(id, blob, metadata, type, OPFS_PATH_PREFIX);
120
217
  assets.set(id, asset);
218
+ await persistMetadata(db, id, asset);
121
219
  return asset;
122
220
  },
123
221
 
@@ -129,7 +227,7 @@ export async function createOpfsAssetStore(
129
227
  const id = parseBlobPath(handle.path, OPFS_PATH_PREFIX);
130
228
  let fileHandle = fileHandles.get(id);
131
229
  if (!fileHandle) {
132
- // 可能是进程重启后元数据丢失但句柄仍可恢复
230
+ // 可能是进程重启后内存句柄丢失,从 OPFS 重新获取
133
231
  try {
134
232
  fileHandle = await assetsDir.getFileHandle(fileName(id));
135
233
  fileHandles.set(id, fileHandle);
@@ -146,9 +244,18 @@ export async function createOpfsAssetStore(
146
244
  fileHandles.delete(id);
147
245
  try {
148
246
  await assetsDir.removeEntry(fileName(id));
149
- } catch {
150
- // 文件不存在视为已删除,静默忽略
247
+ } catch (err) {
248
+ // NotFoundError 是幂等删除的预期场景(文件已不存在),静默忽略;
249
+ // 其他错误(权限/IO 等)记录 warn,便于排查孤儿 OPFS 文件长期累积
250
+ // 无论文件删除是否成功,都继续清理 IDB metadata(见下方)
251
+ if (!(err instanceof DOMException && err.name === 'NotFoundError')) {
252
+ console.warn(
253
+ `[lokvis] OPFS removeEntry unexpected failure for asset ${id}:`,
254
+ err
255
+ );
256
+ }
151
257
  }
258
+ await deleteMetadata(db, id);
152
259
  },
153
260
 
154
261
  async list() {
@@ -161,6 +268,7 @@ export async function createOpfsAssetStore(
161
268
  fileHandles.set(id, handle);
162
269
  const asset = buildAsset(id, blob, metadata, type, OPFS_PATH_PREFIX);
163
270
  assets.set(id, asset);
271
+ await persistMetadata(db, id, asset);
164
272
  return asset;
165
273
  },
166
274
  };