@lokvis/runtime 0.2.2 → 0.4.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 (125) hide show
  1. package/dist/asset-store.d.ts +13 -0
  2. package/dist/asset-store.d.ts.map +1 -1
  3. package/dist/asset-store.js +15 -2
  4. package/dist/asset-store.js.map +1 -1
  5. package/dist/batch-processor.d.ts +32 -161
  6. package/dist/batch-processor.d.ts.map +1 -1
  7. package/dist/batch-processor.js +107 -333
  8. package/dist/batch-processor.js.map +1 -1
  9. package/dist/batch-progress.d.ts +94 -0
  10. package/dist/batch-progress.d.ts.map +1 -0
  11. package/dist/batch-progress.js +128 -0
  12. package/dist/batch-progress.js.map +1 -0
  13. package/dist/batch-scheduler.d.ts +70 -0
  14. package/dist/batch-scheduler.d.ts.map +1 -0
  15. package/dist/batch-scheduler.js +147 -0
  16. package/dist/batch-scheduler.js.map +1 -0
  17. package/dist/browser-detect.d.ts +115 -0
  18. package/dist/browser-detect.d.ts.map +1 -0
  19. package/dist/browser-detect.js +160 -0
  20. package/dist/browser-detect.js.map +1 -0
  21. package/dist/concurrency-controller.d.ts +31 -0
  22. package/dist/concurrency-controller.d.ts.map +1 -0
  23. package/dist/concurrency-controller.js +33 -0
  24. package/dist/concurrency-controller.js.map +1 -0
  25. package/dist/errors.d.ts +51 -0
  26. package/dist/errors.d.ts.map +1 -0
  27. package/dist/errors.js +74 -0
  28. package/dist/errors.js.map +1 -0
  29. package/dist/executor.d.ts +9 -0
  30. package/dist/executor.d.ts.map +1 -1
  31. package/dist/executor.js +87 -48
  32. package/dist/executor.js.map +1 -1
  33. package/dist/history.d.ts.map +1 -1
  34. package/dist/history.js +3 -2
  35. package/dist/history.js.map +1 -1
  36. package/dist/idb-asset-store.d.ts.map +1 -1
  37. package/dist/idb-asset-store.js +6 -1
  38. package/dist/idb-asset-store.js.map +1 -1
  39. package/dist/index.d.ts +17 -15
  40. package/dist/index.d.ts.map +1 -1
  41. package/dist/index.js +20 -15
  42. package/dist/index.js.map +1 -1
  43. package/dist/managers/asset-manager.d.ts +114 -0
  44. package/dist/managers/asset-manager.d.ts.map +1 -0
  45. package/dist/managers/asset-manager.js +238 -0
  46. package/dist/managers/asset-manager.js.map +1 -0
  47. package/dist/managers/history-manager.d.ts +100 -0
  48. package/dist/managers/history-manager.d.ts.map +1 -0
  49. package/dist/managers/history-manager.js +296 -0
  50. package/dist/managers/history-manager.js.map +1 -0
  51. package/dist/managers/mcp-manifest-builder.d.ts +18 -0
  52. package/dist/managers/mcp-manifest-builder.d.ts.map +1 -0
  53. package/dist/managers/mcp-manifest-builder.js +99 -0
  54. package/dist/managers/mcp-manifest-builder.js.map +1 -0
  55. package/dist/managers/quota-manager.d.ts +42 -0
  56. package/dist/managers/quota-manager.d.ts.map +1 -0
  57. package/dist/managers/quota-manager.js +126 -0
  58. package/dist/managers/quota-manager.js.map +1 -0
  59. package/dist/managers/workflow-coordinator.d.ts +62 -0
  60. package/dist/managers/workflow-coordinator.d.ts.map +1 -0
  61. package/dist/managers/workflow-coordinator.js +125 -0
  62. package/dist/managers/workflow-coordinator.js.map +1 -0
  63. package/dist/opfs-asset-store.d.ts.map +1 -1
  64. package/dist/opfs-asset-store.js +12 -2
  65. package/dist/opfs-asset-store.js.map +1 -1
  66. package/dist/plugin-context.d.ts +35 -0
  67. package/dist/plugin-context.d.ts.map +1 -0
  68. package/dist/plugin-context.js +62 -0
  69. package/dist/plugin-context.js.map +1 -0
  70. package/dist/plugin-permissions.d.ts +99 -0
  71. package/dist/plugin-permissions.d.ts.map +1 -0
  72. package/dist/plugin-permissions.js +205 -0
  73. package/dist/plugin-permissions.js.map +1 -0
  74. package/dist/runtime-impl.d.ts +116 -0
  75. package/dist/runtime-impl.d.ts.map +1 -0
  76. package/dist/runtime-impl.js +270 -0
  77. package/dist/runtime-impl.js.map +1 -0
  78. package/dist/runtime.d.ts +10 -233
  79. package/dist/runtime.d.ts.map +1 -1
  80. package/dist/runtime.js +12 -978
  81. package/dist/runtime.js.map +1 -1
  82. package/dist/test-utils/fakes.d.ts +35 -0
  83. package/dist/test-utils/fakes.d.ts.map +1 -0
  84. package/dist/test-utils/fakes.js +60 -0
  85. package/dist/test-utils/fakes.js.map +1 -0
  86. package/dist/types.d.ts +113 -1
  87. package/dist/types.d.ts.map +1 -1
  88. package/dist/worker-host.d.ts.map +1 -1
  89. package/dist/worker-host.js +32 -9
  90. package/dist/worker-host.js.map +1 -1
  91. package/dist/worker-protocol.d.ts +30 -0
  92. package/dist/worker-protocol.d.ts.map +1 -1
  93. package/dist/worker-protocol.js +24 -0
  94. package/dist/worker-protocol.js.map +1 -1
  95. package/package.json +2 -2
  96. package/src/asset-store.ts +32 -2
  97. package/src/batch-processor.ts +133 -510
  98. package/src/batch-progress.ts +228 -0
  99. package/src/batch-scheduler.ts +209 -0
  100. package/src/browser-detect.ts +221 -0
  101. package/src/concurrency-controller.ts +52 -0
  102. package/src/errors.ts +82 -0
  103. package/src/executor.ts +106 -55
  104. package/src/history.ts +3 -2
  105. package/src/idb-asset-store.ts +7 -1
  106. package/src/index.ts +50 -15
  107. package/src/managers/asset-manager.ts +259 -0
  108. package/src/managers/history-manager.ts +327 -0
  109. package/src/managers/mcp-manifest-builder.ts +108 -0
  110. package/src/managers/quota-manager.ts +149 -0
  111. package/src/managers/workflow-coordinator.ts +158 -0
  112. package/src/opfs-asset-store.ts +16 -2
  113. package/src/plugin-context.ts +87 -0
  114. package/src/plugin-permissions.ts +228 -0
  115. package/src/runtime-impl.ts +298 -0
  116. package/src/runtime.ts +16 -1116
  117. package/src/test-utils/fakes.ts +72 -0
  118. package/src/types.ts +116 -0
  119. package/src/worker-host.ts +35 -7
  120. package/src/worker-protocol.ts +47 -0
  121. package/dist/workflow-builder.d.ts +0 -124
  122. package/dist/workflow-builder.d.ts.map +0 -1
  123. package/dist/workflow-builder.js +0 -240
  124. package/dist/workflow-builder.js.map +0 -1
  125. package/src/workflow-builder.ts +0 -303
@@ -0,0 +1,298 @@
1
+ /**
2
+ * Lokvis Runtime Implementation(W1.7 从 runtime.ts 拆出)
3
+ *
4
+ * 实现 LokvisRuntime 接口,持有 5 个 managers + plugin-context,所有方法转发到
5
+ * 对应 manager。runtime.ts 仅 re-export 本模块,保持外部导入路径不变。
6
+ */
7
+
8
+ import type {
9
+ Asset, AssetId, AssetSource, Capability, ExifData,
10
+ HistoryEntry, ImageMetadata, McpManifest, MetadataReader, PdfInfo,
11
+ } from '@lokvis/schema';
12
+ import type { Workflow, WorkflowResult } from '@lokvis/schema';
13
+ import type { EventBus } from '@lokvis/schema';
14
+ import type {
15
+ InternalRuntimeInit, LokvisRuntime, Plan, PluginInstallEntry, RunOptions, RuntimeConfig,
16
+ RuntimeStatus, ToMcpManifestOptions,
17
+ } from './types.js';
18
+ import { createEventBus } from './event-bus.js';
19
+ import {
20
+ createAssetStore, createMemoryAssetStore, type AssetStore,
21
+ } from './asset-store.js';
22
+ import { CapabilityRegistry } from './capability-registry.js';
23
+ import { WorkflowExecutor } from './executor.js';
24
+ import { createHistoryStore, type HistoryStore } from './history-store.js';
25
+ import { BatchProcessor } from './batch-processor.js';
26
+ import { MemoryGuard, DEFAULT_MEMORY_BUDGET } from './memory-guard.js';
27
+ import {
28
+ wrapAssetStoreWithQuota, type QuotaAwareAssetStore,
29
+ } from './managers/quota-manager.js';
30
+ import { AssetManager } from './managers/asset-manager.js';
31
+ import { HistoryManager } from './managers/history-manager.js';
32
+ import { WorkflowCoordinator } from './managers/workflow-coordinator.js';
33
+ import { toMcpManifest as buildMcpManifest } from './managers/mcp-manifest-builder.js';
34
+ import { createPluginContext } from './plugin-context.js';
35
+ import { PluginPermissionSandbox } from './plugin-permissions.js';
36
+
37
+ export const RUNTIME_VERSION = '0.1.0';
38
+
39
+ /** Runtime 实现类,持有 5 个 managers 并委托方法 */
40
+ export class LokvisRuntimeImpl implements LokvisRuntime {
41
+ readonly version = RUNTIME_VERSION;
42
+ readonly eventBus: EventBus;
43
+ private config: Required<Omit<RuntimeConfig, 'assetStore' | 'historyStore' | 'historyStoreOptions'>>;
44
+ private assetStore: QuotaAwareAssetStore; // wrapAssetStoreWithQuota 无条件包裹(含注入路径)
45
+ private assetManager: AssetManager;
46
+ private capabilityRegistry: CapabilityRegistry;
47
+ private executor: WorkflowExecutor;
48
+ private memoryGuard: MemoryGuard;
49
+ private batchProcessor: BatchProcessor;
50
+ private historyManager: HistoryManager; // W1.3:历史栈/initialInputs/currentOutputs/持久化守卫
51
+ private workflowCoordinator: WorkflowCoordinator; // W1.4:status 状态机 + run/cancel/pause/resume/disposeWorkflow
52
+ private historyStore: HistoryStore | undefined; // W7.2 历史持久化;undefined 时退化为仅内存历史
53
+ private metadataReaders = new Map<string, MetadataReader>(); // W7.3/7.4 MetadataReader 依赖反转
54
+ // W21.6: dispose 守卫,防止重复 dispose + 阻止后续 run/cancel 调用
55
+ private disposed = false;
56
+ // W21.6: 标记 assetStore 是否由 Runtime 拥有(工厂创建而非注入)。
57
+ // 仅在 ownsAssetStore=true 时 dispose() 才会调用 assetStore.dispose?.()。
58
+ private readonly ownsAssetStore: boolean;
59
+
60
+ /** 注册元数据读取器(由 PluginContext.registerMetadataReader 转发,内部 API) */
61
+ _registerMetadataReader(name: string, reader: MetadataReader): void {
62
+ this.metadataReaders.set(name, reader);
63
+ }
64
+
65
+ constructor(config: RuntimeConfig & InternalRuntimeInit = {}) {
66
+ this.config = {
67
+ enableOpfs: config.enableOpfs ?? true,
68
+ enableIndexedDB: config.enableIndexedDB ?? true,
69
+ storageQuota: config.storageQuota ?? 1024 * 1024 * 1024, // 1GB
70
+ enableLog: config.enableLog ?? true,
71
+ engineStrategy: config.engineStrategy ?? 'first',
72
+ isPro: config.isPro ?? false,
73
+ plan: config.plan ?? 'free',
74
+ memoryBudget: config.memoryBudget ?? DEFAULT_MEMORY_BUDGET,
75
+ };
76
+ this.eventBus = createEventBus();
77
+ // enableOpfs/enableIndexedDB 仅在 createLokvis()/createRuntime 工厂中生效
78
+ // (工厂按 flag 调 createAssetStore 选择 OPFS→IDB→Memory)。直接 new Impl
79
+ // 且未传 assetStore 时,无法同步等待 createAssetStore,兜底用 Memory store。
80
+ if (!config.assetStore && (this.config.enableOpfs || this.config.enableIndexedDB)) {
81
+ console.warn(
82
+ '[lokvis] LokvisRuntimeImpl constructed without assetStore: ' +
83
+ 'enableOpfs/enableIndexedDB flags are ignored. ' +
84
+ 'Use createLokvis() to respect these flags, or pass an assetStore explicitly.'
85
+ );
86
+ }
87
+ const rawStore = config.assetStore ?? createMemoryAssetStore();
88
+ // W21.6 + review fix: ownsAssetStore 已从公共 RuntimeConfig 移到
89
+ // InternalRuntimeInit,SDK 用户类型层面无法传此字段。工厂 createRuntime
90
+ // 在工厂创建路径下显式传 ownsAssetStore: true,注入路径下显式传 false,
91
+ // 并用 `ownsAssetStore: !injectedAssetStore` 覆盖用户传入的值(防止 JS
92
+ // 用户绕过类型系统)。Impl 信任工厂传入的 ownsAssetStore,不再做冗余守卫。
93
+ this.ownsAssetStore = config.ownsAssetStore ?? !config.assetStore;
94
+ this.assetStore = wrapAssetStoreWithQuota(rawStore, this.config.storageQuota);
95
+ // AssetManager:metadataReaders 由 Runtime 持有,Plugin 注册后立即可见
96
+ this.assetManager = new AssetManager({
97
+ assetStore: this.assetStore, eventBus: this.eventBus,
98
+ metadataReaders: this.metadataReaders,
99
+ storageQuota: this.config.storageQuota,
100
+ });
101
+ this.historyStore = config.historyStore;
102
+ this.historyManager = new HistoryManager({
103
+ eventBus: this.eventBus, assetStore: this.assetStore,
104
+ historyStore: this.historyStore,
105
+ });
106
+ this.capabilityRegistry = new CapabilityRegistry(this.config.engineStrategy);
107
+ this.executor = new WorkflowExecutor({
108
+ assetStore: this.assetStore, capabilityRegistry: this.capabilityRegistry,
109
+ eventBus: this.eventBus, enableLog: this.config.enableLog,
110
+ });
111
+ this.workflowCoordinator = new WorkflowCoordinator({
112
+ executor: this.executor, capabilityRegistry: this.capabilityRegistry,
113
+ eventBus: this.eventBus, historyManager: this.historyManager,
114
+ });
115
+ this.memoryGuard = new MemoryGuard({
116
+ budget: this.config.memoryBudget, assetStore: this.assetStore,
117
+ });
118
+ this.batchProcessor = new BatchProcessor({
119
+ runtime: this, eventBus: this.eventBus,
120
+ isPro: this.config.isPro, memoryGuard: this.memoryGuard,
121
+ });
122
+ }
123
+
124
+ get status(): RuntimeStatus { return this.workflowCoordinator.status; }
125
+ get isPro(): boolean { return this.config.isPro; }
126
+ get plan(): Plan { return this.config.plan; }
127
+ get batch(): BatchProcessor { return this.batchProcessor; }
128
+
129
+ // ─── 工作流执行(委托 WorkflowCoordinator) ──────────────
130
+ async run(workflow: Workflow, inputs: AssetId[] | Asset[], options?: RunOptions): Promise<WorkflowResult> {
131
+ if (this.disposed) throw new Error('LokvisRuntime is disposed');
132
+ return this.workflowCoordinator.run(workflow, inputs, options);
133
+ }
134
+ async cancel(workflowId: string): Promise<void> {
135
+ if (this.disposed) throw new Error('LokvisRuntime is disposed');
136
+ return this.workflowCoordinator.cancel(workflowId);
137
+ }
138
+ async pause(workflowId: string): Promise<void> {
139
+ if (this.disposed) throw new Error('LokvisRuntime is disposed');
140
+ return this.workflowCoordinator.pause(workflowId);
141
+ }
142
+ async resume(workflowId: string): Promise<void> {
143
+ if (this.disposed) throw new Error('LokvisRuntime is disposed');
144
+ return this.workflowCoordinator.resume(workflowId);
145
+ }
146
+ async disposeWorkflow(workflowId: string): Promise<void> {
147
+ if (this.disposed) throw new Error('LokvisRuntime is disposed');
148
+ return this.workflowCoordinator.disposeWorkflow(workflowId);
149
+ }
150
+
151
+ /**
152
+ * 销毁整个 Runtime(W21.6)。
153
+ *
154
+ * 顺序:
155
+ * 1. 标记 disposed(阻止后续 run/cancel,防止清理期间新请求进入)
156
+ * 2. executor.cancelAll() —— 取消所有运行中 workflow 的 AbortController
157
+ * 3. batchProcessor.dispose() —— 标记所有非终态 job 为 cancelled + 清理订阅
158
+ * 4. historyManager.disposeAll() —— 清空所有历史栈(reset 触发 onEvict
159
+ * → assetStore.remove 回收 outputs 资产);TD-2.1 改为 await 等待
160
+ * persistHistory 删除 IDB 记录落地
161
+ * 5. 清理 metadataReaders
162
+ * 6. 若 ownsAssetStore(工厂创建而非注入):调用 assetStore.dispose?.()
163
+ * 关闭 Dexie 连接 / 清空内存 Map。注入路径由消费方自行管理。
164
+ *
165
+ * 不清理:
166
+ * - historyStore(由消费方注入或工厂创建,Dexie 连接由浏览器 GC 处理)
167
+ * - eventBus listeners(允许外部已订阅的 listener 仍收到最后一批
168
+ * workflow:cancelled 等事件;若需要清空可单独调 eventBus.clear)
169
+ *
170
+ * 幂等:重复调用为 no-op。
171
+ */
172
+ async dispose(): Promise<void> {
173
+ if (this.disposed) return;
174
+ this.disposed = true;
175
+ // 1. 取消所有运行中 workflow(同步 abort + 唤醒 resume resolver)
176
+ this.executor.cancelAll();
177
+ // 2. 取消所有非终态 batch job + 清理 progress 订阅
178
+ await this.batchProcessor.dispose();
179
+ // 3. 清空所有历史栈(触发 outputs 资产回收);TD-2.1: await 等待持久化删除落地
180
+ await this.historyManager.disposeAll();
181
+ // 4. 清理 metadataReaders(释放插件注册的 reader 引用)
182
+ this.metadataReaders.clear();
183
+ // 5. 若 Runtime 拥有 assetStore(工厂创建),释放底层资源
184
+ // (注入路径由消费方自行管理生命周期,避免越权清理)
185
+ if (this.ownsAssetStore) {
186
+ await this.assetStore.dispose?.();
187
+ }
188
+ }
189
+
190
+ // ─── 历史与撤销(委托 HistoryManager) ──────────────────
191
+ async history(workflowId: string): Promise<HistoryEntry[]> { return this.historyManager.history(workflowId); }
192
+ async getHistoryState(workflowId: string): Promise<{ entries: HistoryEntry[]; cursor: number }> {
193
+ return this.historyManager.getHistoryState(workflowId);
194
+ }
195
+ async undo(workflowId: string): Promise<void> { return this.historyManager.undo(workflowId); }
196
+ async redo(workflowId: string): Promise<void> { return this.historyManager.redo(workflowId); }
197
+ async jumpTo(workflowId: string, index: number): Promise<void> { return this.historyManager.jumpTo(workflowId, index); }
198
+
199
+ // ─── Asset 管理(委托 AssetManager) ─────────────────
200
+ async importAsset(source: AssetSource): Promise<AssetId> { return this.assetManager.importAsset(source); }
201
+ async getAsset(id: AssetId): Promise<Asset> { return this.assetManager.getAsset(id); }
202
+ async exportAsset(id: AssetId, format?: string): Promise<Blob> { return this.assetManager.exportAsset(id, format); }
203
+ async readAssetExif(id: AssetId): Promise<ExifData | null> { return this.assetManager.readAssetExif(id); }
204
+ async readAssetImageMetadata(id: AssetId): Promise<ImageMetadata | null> { return this.assetManager.readAssetImageMetadata(id); }
205
+ async readAssetPdfInfo(id: AssetId): Promise<PdfInfo | null> { return this.assetManager.readAssetPdfInfo(id); }
206
+ async removeAsset(id: AssetId): Promise<void> { return this.assetManager.removeAsset(id); }
207
+ async listAssets(): Promise<Asset[]> { return this.assetManager.listAssets(); }
208
+ async getStorageUsage(): Promise<{ usage: number; quota: number }> { return this.assetManager.getStorageUsage(); }
209
+
210
+ // ─── 能力查询(委托 CapabilityRegistry) ────────────────
211
+ async capabilities(): Promise<Capability[]> { return this.capabilityRegistry.list(); }
212
+ async hasCapability(name: string): Promise<boolean> { return this.capabilityRegistry.has(name); }
213
+ async isStubOnly(name: string): Promise<boolean> { return this.capabilityRegistry.isStubOnly(name); }
214
+
215
+ // ─── MCP 暴露(委托 mcp-manifest-builder) ──────────────
216
+ toMcpManifest(options: ToMcpManifestOptions = {}): McpManifest {
217
+ return buildMcpManifest(this.capabilityRegistry.list(), options, RUNTIME_VERSION);
218
+ }
219
+
220
+ // ─── 插件安装 ────────────────────────────────────────
221
+ /**
222
+ * 安装插件:① 注册能力声明 → ② 构造权限沙箱 → ③ 构造受限 PluginContext
223
+ * → ④ 应用 network guard(声明 network:none 时 monkey-patch 全局
224
+ * fetch/XHR/WebSocket/EventSource)→ ⑤ 调用 plugin.install(ctx)
225
+ * → ⑥ restore 全局 API → ⑦ 发射 plugin:loaded 事件。不吞错。
226
+ *
227
+ * network guard 仅在 plugin.install() 期间生效;install 后插件若异步调用
228
+ * 网络 API(如 setTimeout 回调)无法覆盖 —— plugin 作者应据 ctx.sandbox
229
+ * 主动断言(best-effort 守卫,见 docs/PROJECT_PLAN.md W18.6)。
230
+ */
231
+ async installPlugin(plugin: PluginInstallEntry): Promise<void> {
232
+ for (const capability of plugin.config.capabilities) {
233
+ this.capabilityRegistry.registerCapability(capability);
234
+ }
235
+ const sandbox = new PluginPermissionSandbox(
236
+ plugin.config.name, plugin.config.permissions
237
+ );
238
+ const ctx = createPluginContext(plugin.config.name, {
239
+ eventBus: this.eventBus, assetStore: this.assetStore,
240
+ capabilityRegistry: this.capabilityRegistry,
241
+ registerMetadataReader: (name, reader) => this._registerMetadataReader(name, reader),
242
+ sandbox,
243
+ });
244
+ const restore = sandbox.applyNetworkGuard();
245
+ try {
246
+ await plugin.install(ctx);
247
+ } finally {
248
+ restore();
249
+ }
250
+ this.eventBus.emit({
251
+ type: 'plugin:loaded', name: plugin.config.name, version: plugin.config.version,
252
+ });
253
+ }
254
+
255
+ // ─── 内部 API(供测试使用,SDK 不再调用) ──────────────
256
+ _getAssetStore(): AssetStore { return this.assetStore; }
257
+ _getCapabilityRegistry(): CapabilityRegistry { return this.capabilityRegistry; }
258
+ _getMemoryGuard(): MemoryGuard { return this.memoryGuard; }
259
+ _getCurrentOutputs(workflowId: string): AssetId[] { return this.historyManager.getCurrentOutputs(workflowId); }
260
+ async getCurrentOutputs(workflowId: string): Promise<AssetId[]> { return this._getCurrentOutputs(workflowId); }
261
+
262
+ /** 从 historyStore 预加载持久化历史快照(由 createRuntime 工厂在构造后调用一次,impl 初始化钩子)。 */
263
+ async loadPersistedHistory(): Promise<void> { return this.historyManager.loadPersistedHistory(); }
264
+ }
265
+
266
+ /**
267
+ * 创建 Runtime 实例(W2.8 + W2.9)。默认通过 createAssetStore 工厂按 OPFS →
268
+ * IndexedDB → Memory 降级创建 AssetStore 并用配额校验包裹;也可通过
269
+ * config.assetStore 注入自定义 store。async(工厂需异步探测环境)。
270
+ */
271
+ export async function createRuntime(config?: RuntimeConfig): Promise<LokvisRuntime> {
272
+ // W21.6 + review fix: 判断 assetStore 是否由工厂创建(未注入即工厂创建)。
273
+ // 工厂创建的 store 由 Runtime 拥有,dispose() 时负责调用 assetStore.dispose?.()。
274
+ //
275
+ // 运行时守卫:此处的 `ownsAssetStore: !injectedAssetStore` 会覆盖 `...config`
276
+ // 中可能存在的 ownsAssetStore 字段(虽然 TypeScript 层面 RuntimeConfig 已不含
277
+ // 此字段,但 JS 用户可能绕过类型系统传入)。这样确保注入路径下 ownsAssetStore
278
+ // 始终为 false,防止 Runtime 越权清理注入的 store。
279
+ const injectedAssetStore = config?.assetStore;
280
+ const assetStore =
281
+ injectedAssetStore ??
282
+ (await createAssetStore({ preferOpfs: config?.enableOpfs ?? true }));
283
+ // W7.2 历史持久化:优先用注入的 historyStore;否则在 enableIndexedDB 时
284
+ // 通过 createHistoryStore 自动创建(IDB 不可用时返回 undefined,退化仅内存)
285
+ const historyStore =
286
+ config?.historyStore ??
287
+ ((config?.enableIndexedDB ?? true)
288
+ ? createHistoryStore(config?.historyStoreOptions)
289
+ : undefined);
290
+ const impl = new LokvisRuntimeImpl({
291
+ ...config,
292
+ assetStore,
293
+ historyStore,
294
+ ownsAssetStore: !injectedAssetStore,
295
+ });
296
+ await impl.loadPersistedHistory(); // 预加载持久化历史快照(跨会话恢复 undo/redo 链)
297
+ return impl;
298
+ }