@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 @@
1
+ {"version":3,"file":"worker-protocol.d.ts","sourceRoot":"","sources":["../src/worker-protocol.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,87 @@
1
+ /**
2
+ * Worker 通信协议
3
+ *
4
+ * 定义 主线程(Host) ↔ Worker 之间的消息格式与握手/心跳规范。
5
+ * 协议本身与传输层解耦(纯类型 + 常量 + 校验助手),
6
+ * 既可用于浏览器 Web Worker,也可在测试中被 Fake 传输层复用。
7
+ *
8
+ * 设计目标(对应 whitepaper T1「浏览器内存/性能 ★★★★★」与 PROJECT_PLAN 1.6/1.7):
9
+ * - 每个请求有唯一 id,Response 按 id 关联,避免错配。
10
+ * - 心跳(Heartbeat):Host 周期性 ping,Worker 必须在超时内 pong,否则视为崩溃。
11
+ * - Ready 握手:Worker 启动后主动发 ready,Host 据此判定可用。
12
+ * - 错误通道:Worker 主动 error(可恢复) vs 传输层 error/exit(不可恢复,触发重启)。
13
+ *
14
+ * 消息流向:
15
+ * Host → Worker: request | ping
16
+ * Worker → Host: response | pong | event | ready | error
17
+ */
18
+ /** 协议版本(Host 与 Worker 握手时校验,不一致则拒绝) */
19
+ export const WORKER_PROTOCOL_VERSION = '0.1.0';
20
+ // ─── 默认参数(可通过 WorkerHostOptions 覆盖)──────────────────────
21
+ /** 心跳发送间隔 */
22
+ export const DEFAULT_HEARTBEAT_INTERVAL_MS = 5_000;
23
+ /** 心跳超时:超过该时间未收到 pong 视为 Worker 无响应 */
24
+ export const DEFAULT_HEARTBEAT_TIMEOUT_MS = 15_000;
25
+ /** 单个请求的默认超时 */
26
+ export const DEFAULT_REQUEST_TIMEOUT_MS = 60_000;
27
+ /** 崩溃后最大重启次数(超过则放弃,交由上层降级) */
28
+ export const DEFAULT_MAX_RESTARTS = 3;
29
+ /** Worker 启动后等待 ready 的超时 */
30
+ export const DEFAULT_READY_TIMEOUT_MS = 10_000;
31
+ // ─── 助手函数 ───────────────────────────────────────────────────
32
+ /** 生成唯一请求/心跳 id */
33
+ export function createRequestId() {
34
+ if (typeof crypto !== 'undefined' && typeof crypto.randomUUID === 'function') {
35
+ return crypto.randomUUID();
36
+ }
37
+ return `req_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
38
+ }
39
+ /** 类型守卫:是否为 Worker → Host 消息 */
40
+ export function isWorkerMessageToHost(data) {
41
+ if (typeof data !== 'object' || data === null)
42
+ return false;
43
+ const type = data.type;
44
+ return (type === 'response' ||
45
+ type === 'pong' ||
46
+ type === 'event' ||
47
+ type === 'ready' ||
48
+ type === 'error');
49
+ }
50
+ /** 类型守卫:是否为响应消息 */
51
+ export function isWorkerResponse(data) {
52
+ return (typeof data === 'object' &&
53
+ data !== null &&
54
+ data.type === 'response' &&
55
+ typeof data.id === 'string' &&
56
+ typeof data.ok === 'boolean');
57
+ }
58
+ /** 类型守卫:是否为 pong */
59
+ export function isWorkerPong(data) {
60
+ return (typeof data === 'object' &&
61
+ data !== null &&
62
+ data.type === 'pong');
63
+ }
64
+ /** 类型守卫:是否为 ready */
65
+ export function isWorkerReady(data) {
66
+ return (typeof data === 'object' &&
67
+ data !== null &&
68
+ data.type === 'ready');
69
+ }
70
+ /** 类型守卫:是否为 Worker 主动事件 */
71
+ export function isWorkerEvent(data) {
72
+ return (typeof data === 'object' &&
73
+ data !== null &&
74
+ data.type === 'event' &&
75
+ typeof data.event === 'string');
76
+ }
77
+ /** 类型守卫:是否为 Worker 主动上报错误 */
78
+ export function isWorkerFatalError(data) {
79
+ return (typeof data === 'object' &&
80
+ data !== null &&
81
+ data.type === 'error');
82
+ }
83
+ /** 协议版本是否兼容(目前采用严格相等) */
84
+ export function isProtocolCompatible(version) {
85
+ return version === WORKER_PROTOCOL_VERSION;
86
+ }
87
+ //# sourceMappingURL=worker-protocol.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"worker-protocol.js","sourceRoot":"","sources":["../src/worker-protocol.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAEH,uCAAuC;AACvC,MAAM,CAAC,MAAM,uBAAuB,GAAG,OAAO,CAAC;AAE/C,2DAA2D;AAC3D,aAAa;AACb,MAAM,CAAC,MAAM,6BAA6B,GAAG,KAAK,CAAC;AACnD,uCAAuC;AACvC,MAAM,CAAC,MAAM,4BAA4B,GAAG,MAAM,CAAC;AACnD,gBAAgB;AAChB,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AACjD,8BAA8B;AAC9B,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,CAAC;AACtC,6BAA6B;AAC7B,MAAM,CAAC,MAAM,wBAAwB,GAAG,MAAM,CAAC;AA+E/C,+DAA+D;AAE/D,mBAAmB;AACnB,MAAM,UAAU,eAAe;IAC7B,IAAI,OAAO,MAAM,KAAK,WAAW,IAAI,OAAO,MAAM,CAAC,UAAU,KAAK,UAAU,EAAE,CAAC;QAC7E,OAAO,MAAM,CAAC,UAAU,EAAE,CAAC;IAC7B,CAAC;IACD,OAAO,OAAO,IAAI,CAAC,GAAG,EAAE,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,EAAE,CAAC;AACxE,CAAC;AAED,gCAAgC;AAChC,MAAM,UAAU,qBAAqB,CACnC,IAAa;IAEb,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI,KAAK,IAAI;QAAE,OAAO,KAAK,CAAC;IAC5D,MAAM,IAAI,GAAI,IAA2B,CAAC,IAAI,CAAC;IAC/C,OAAO,CACL,IAAI,KAAK,UAAU;QACnB,IAAI,KAAK,MAAM;QACf,IAAI,KAAK,OAAO;QAChB,IAAI,KAAK,OAAO;QAChB,IAAI,KAAK,OAAO,CACjB,CAAC;AACJ,CAAC;AAED,mBAAmB;AACnB,MAAM,UAAU,gBAAgB,CAAC,IAAa;IAC5C,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACZ,IAA2B,CAAC,IAAI,KAAK,UAAU;QAChD,OAAQ,IAAyB,CAAC,EAAE,KAAK,QAAQ;QACjD,OAAQ,IAAyB,CAAC,EAAE,KAAK,SAAS,CACnD,CAAC;AACJ,CAAC;AAED,oBAAoB;AACpB,MAAM,UAAU,YAAY,CAAC,IAAa;IACxC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACZ,IAA2B,CAAC,IAAI,KAAK,MAAM,CAC7C,CAAC;AACJ,CAAC;AAED,qBAAqB;AACrB,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACZ,IAA2B,CAAC,IAAI,KAAK,OAAO,CAC9C,CAAC;AACJ,CAAC;AAED,2BAA2B;AAC3B,MAAM,UAAU,aAAa,CAAC,IAAa;IACzC,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACZ,IAA2B,CAAC,IAAI,KAAK,OAAO;QAC7C,OAAQ,IAA4B,CAAC,KAAK,KAAK,QAAQ,CACxD,CAAC;AACJ,CAAC;AAED,6BAA6B;AAC7B,MAAM,UAAU,kBAAkB,CAAC,IAAa;IAC9C,OAAO,CACL,OAAO,IAAI,KAAK,QAAQ;QACxB,IAAI,KAAK,IAAI;QACZ,IAA2B,CAAC,IAAI,KAAK,OAAO,CAC9C,CAAC;AACJ,CAAC;AAED,yBAAyB;AACzB,MAAM,UAAU,oBAAoB,CAAC,OAAe;IAClD,OAAO,OAAO,KAAK,uBAAuB,CAAC;AAC7C,CAAC"}
package/package.json ADDED
@@ -0,0 +1,49 @@
1
+ {
2
+ "name": "@lokvis/runtime",
3
+ "version": "0.1.0",
4
+ "description": "Lokvis Runtime - Browser-local workflow execution engine",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "main": "./dist/index.js",
8
+ "module": "./dist/index.js",
9
+ "types": "./dist/index.d.ts",
10
+ "exports": {
11
+ ".": {
12
+ "types": "./dist/index.d.ts",
13
+ "import": "./dist/index.js"
14
+ }
15
+ },
16
+ "files": [
17
+ "dist",
18
+ "src",
19
+ "!**/__tests__",
20
+ "!**/*.test.ts",
21
+ "!**/*.test.tsx",
22
+ "!**/*.spec.ts",
23
+ "!**/*.spec.tsx"
24
+ ],
25
+ "dependencies": {
26
+ "dexie": "^4.0.10",
27
+ "mitt": "^3.0.1",
28
+ "@lokvis/schema": "0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "fake-indexeddb": "^6.0.0",
32
+ "typescript": "^5.6.0"
33
+ },
34
+ "publishConfig": {
35
+ "access": "public",
36
+ "registry": "https://registry.npmjs.org/"
37
+ },
38
+ "repository": {
39
+ "type": "git",
40
+ "url": "https://github.com/lokvis/lokvis.git",
41
+ "directory": "packages/runtime"
42
+ },
43
+ "scripts": {
44
+ "build": "tsc -p tsconfig.json",
45
+ "dev": "tsc -p tsconfig.json --watch",
46
+ "typecheck": "tsc --noEmit",
47
+ "clean": "rm -rf dist"
48
+ }
49
+ }
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Lokvis Asset Store
3
+ *
4
+ * 资产存储管理。元数据存 IndexedDB,大文件存 OPFS。
5
+ * Runtime 只操作 AssetId,不直接持有 File 或 Blob。
6
+ *
7
+ * 降级链(createAssetStore 工厂,W2.8):
8
+ * preferOpfs → OPFS → IndexedDB(Dexie) → Memory(始终可用,不持久化)
9
+ */
10
+
11
+ import type { Asset, AssetId, AssetMetadata, AssetSource, BlobHandle } from '@lokvis/schema';
12
+ import { createOpfsAssetStore, isOpfsSupported } from './opfs-asset-store.js';
13
+ import { createIdbAssetStore, isIdbSupported } from './idb-asset-store.js';
14
+
15
+ /** Asset 存储接口 */
16
+ export interface AssetStore {
17
+ /** 导入资产(从 File / Blob / URL) */
18
+ import(source: AssetSource): Promise<Asset>;
19
+ /** 获取资产元数据 */
20
+ get(id: AssetId): Promise<Asset | undefined>;
21
+ /** 获取资产的 Blob 数据 */
22
+ getBlob(handle: BlobHandle): Promise<Blob>;
23
+ /** 删除资产 */
24
+ remove(id: AssetId): Promise<void>;
25
+ /** 列出所有资产 */
26
+ list(): Promise<Asset[]>;
27
+ /** 创建新 Asset(内部用,由 Capability 产出) */
28
+ create(blob: Blob, metadata: AssetMetadata, type: Asset['type']): Promise<Asset>;
29
+ }
30
+
31
+ /** 生成唯一 ID */
32
+ export function generateId(): string {
33
+ if (typeof crypto !== 'undefined' && crypto.randomUUID) {
34
+ return crypto.randomUUID();
35
+ }
36
+ return `asset_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
37
+ }
38
+
39
+ /** 从 MIME 类型推断 Asset 类型 */
40
+ export function inferAssetType(mimeType: string): Asset['type'] {
41
+ if (mimeType.startsWith('image/')) return 'image';
42
+ if (mimeType.startsWith('video/')) return 'video';
43
+ if (mimeType.startsWith('audio/')) return 'audio';
44
+ if (mimeType === 'application/pdf') return 'pdf';
45
+ if (mimeType.startsWith('text/')) return 'text';
46
+ if (mimeType === 'application/json' || mimeType.startsWith('application/'))
47
+ return 'data';
48
+ return 'unknown';
49
+ }
50
+
51
+ /** 从 MIME 类型获取格式扩展名 */
52
+ export function getFormatFromMime(mimeType: string): string {
53
+ const parts = mimeType.split('/');
54
+ return parts[1] ?? 'bin';
55
+ }
56
+
57
+ /** 从 AssetSource 提取 Blob 与 MIME(支持 file/blob,其余抛 not supported) */
58
+ export function extractBlobFromSource(source: AssetSource): {
59
+ blob: Blob;
60
+ mimeType: string;
61
+ } {
62
+ if (source.kind === 'file') {
63
+ return { blob: source.file, mimeType: source.file.type };
64
+ }
65
+ if (source.kind === 'blob') {
66
+ return { blob: source.blob, mimeType: source.blob.type };
67
+ }
68
+ throw new Error(
69
+ `Asset source kind "${source.kind}" not supported by this store`
70
+ );
71
+ }
72
+
73
+ /** prepareImport 返回值:统一构造的导入数据(供三个 store 复用) */
74
+ export interface PreparedImport {
75
+ id: AssetId;
76
+ blob: Blob;
77
+ metadata: AssetMetadata;
78
+ type: Asset['type'];
79
+ }
80
+
81
+ /**
82
+ * 从 AssetSource 准备导入数据(共享逻辑,供 Memory/OPFS/IDB store 复用):
83
+ * 提取 blob + MIME、生成 id、推断 type、构造 metadata。
84
+ * 各 store 只需负责"写入 blob + 存元数据"。
85
+ */
86
+ export function prepareImport(source: AssetSource): PreparedImport {
87
+ const { blob, mimeType: rawMime } = extractBlobFromSource(source);
88
+ const id = generateId();
89
+ const mimeType = rawMime || 'application/octet-stream';
90
+ const type = inferAssetType(mimeType);
91
+ const metadata: AssetMetadata = {
92
+ mimeType,
93
+ size: blob.size,
94
+ format: getFormatFromMime(mimeType),
95
+ };
96
+ return { id, blob, metadata, type };
97
+ }
98
+
99
+ /** 从 BlobHandle.path 解析出 AssetId(共享,各 store 按 prefix 调用) */
100
+ export function parseBlobPath(path: string, prefix: string): AssetId {
101
+ const fullPrefix = `${prefix}://`;
102
+ if (path.startsWith(fullPrefix)) {
103
+ return path.slice(fullPrefix.length);
104
+ }
105
+ return path;
106
+ }
107
+
108
+ /** 构造完整 Asset 元数据 + 默认字段(共享工厂) */
109
+ export function buildAsset(
110
+ id: AssetId,
111
+ blob: Blob,
112
+ metadata: AssetMetadata,
113
+ type: Asset['type'],
114
+ pathPrefix: string
115
+ ): Asset {
116
+ const now = Date.now();
117
+ return {
118
+ id,
119
+ type,
120
+ metadata,
121
+ blob: {
122
+ path: `${pathPrefix}://${id}`,
123
+ size: blob.size,
124
+ mimeType: metadata.mimeType,
125
+ },
126
+ history: [],
127
+ tags: [],
128
+ createdAt: now,
129
+ updatedAt: now,
130
+ };
131
+ }
132
+
133
+ /** Memory store 的 BlobHandle.path 前缀 */
134
+ const MEMORY_PATH_PREFIX = 'memory';
135
+
136
+ /** 创建内存版 AssetStore(降级方案,不持久化) */
137
+ export function createMemoryAssetStore(): AssetStore {
138
+ const assets = new Map<AssetId, Asset>();
139
+ const blobs = new Map<AssetId, Blob>();
140
+
141
+ return {
142
+ async import(source) {
143
+ const { id, blob, metadata, type } = prepareImport(source);
144
+ blobs.set(id, blob);
145
+ const asset = buildAsset(id, blob, metadata, type, MEMORY_PATH_PREFIX);
146
+ assets.set(id, asset);
147
+ return asset;
148
+ },
149
+
150
+ async get(id) {
151
+ return assets.get(id);
152
+ },
153
+
154
+ async getBlob(handle) {
155
+ const id = parseBlobPath(handle.path, MEMORY_PATH_PREFIX);
156
+ const blob = blobs.get(id);
157
+ if (!blob) throw new Error(`Blob not found for path: ${handle.path}`);
158
+ return blob;
159
+ },
160
+
161
+ async remove(id) {
162
+ assets.delete(id);
163
+ blobs.delete(id);
164
+ },
165
+
166
+ async list() {
167
+ return Array.from(assets.values());
168
+ },
169
+
170
+ async create(blob, metadata, type) {
171
+ const id = generateId();
172
+ blobs.set(id, blob);
173
+ const asset = buildAsset(id, blob, metadata, type, MEMORY_PATH_PREFIX);
174
+ assets.set(id, asset);
175
+ return asset;
176
+ },
177
+ };
178
+ }
179
+
180
+ /** 工厂配置(W2.8) */
181
+ export interface CreateAssetStoreOptions {
182
+ /** 是否优先使用 OPFS(默认 true);为 false 时跳过 OPFS */
183
+ preferOpfs?: boolean;
184
+ }
185
+
186
+ /**
187
+ * AssetStore 工厂:自动探测环境,按降级链创建存储(W2.8)。
188
+ *
189
+ * 顺序:
190
+ * 1. preferOpfs(默认 true)且 OPFS 可用 → OpfsAssetStore
191
+ * 2. IndexedDB 可用 → IdbAssetStore(Dexie)
192
+ * 3. 兜底 → MemoryAssetStore(始终可用,不持久化)
193
+ *
194
+ * 任一阶段抛错均自动降级,最终必定返回一个可用 store。
195
+ */
196
+ export async function createAssetStore(
197
+ options: CreateAssetStoreOptions = {}
198
+ ): Promise<AssetStore> {
199
+ const preferOpfs = options.preferOpfs ?? true;
200
+
201
+ // 1. OPFS
202
+ if (preferOpfs && isOpfsSupported()) {
203
+ try {
204
+ return await createOpfsAssetStore();
205
+ } catch (err) {
206
+ // OPFS 初始化失败,降级到 IndexedDB
207
+ console.warn('[lokvis] OPFS unavailable, falling back to IndexedDB:', err);
208
+ }
209
+ }
210
+
211
+ // 2. IndexedDB
212
+ if (isIdbSupported()) {
213
+ try {
214
+ return await createIdbAssetStore();
215
+ } catch (err) {
216
+ console.warn('[lokvis] IndexedDB unavailable, falling back to memory:', err);
217
+ }
218
+ }
219
+
220
+ // 3. Memory(兜底,始终可用)
221
+ return createMemoryAssetStore();
222
+ }
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Lokvis Capability Registry
3
+ *
4
+ * 能力注册中心。Runtime 永远不知道 FFmpeg,只知道 Capability。
5
+ * Capability 由 Plugin 注册,由 Engine 实现。
6
+ */
7
+
8
+ import type {
9
+ Capability,
10
+ CapabilityImplementation,
11
+ CapabilityName,
12
+ EngineSelectionStrategy,
13
+ PerformanceLevel,
14
+ } from '@lokvis/schema';
15
+
16
+ /** 能力注册表项 */
17
+ interface RegistryEntry {
18
+ /** 能力声明 */
19
+ capability: Capability;
20
+ /** 能力实现列表(同一能力可由多个引擎实现) */
21
+ implementations: CapabilityImplementation[];
22
+ }
23
+
24
+ /** 性能等级排序权重(数值越小越快) */
25
+ const PERFORMANCE_RANK: Record<PerformanceLevel, number> = {
26
+ fast: 0,
27
+ medium: 1,
28
+ slow: 2,
29
+ };
30
+
31
+ /** 能力注册中心 */
32
+ export class CapabilityRegistry {
33
+ private registry = new Map<CapabilityName, RegistryEntry>();
34
+ private readonly defaultStrategy: EngineSelectionStrategy;
35
+
36
+ constructor(defaultStrategy: EngineSelectionStrategy = 'first') {
37
+ this.defaultStrategy = defaultStrategy;
38
+ }
39
+
40
+ /** 注册一个能力声明 */
41
+ registerCapability(capability: Capability): void {
42
+ const existing = this.registry.get(capability.name);
43
+ if (existing) {
44
+ throw new Error(`Capability "${capability.name}" is already registered`);
45
+ }
46
+ this.registry.set(capability.name, { capability, implementations: [] });
47
+ }
48
+
49
+ /** 注册能力实现 */
50
+ registerImplementation(impl: CapabilityImplementation): void {
51
+ const entry = this.registry.get(impl.capability);
52
+ if (!entry) {
53
+ throw new Error(
54
+ `Cannot register implementation for unknown capability "${impl.capability}". Register the capability first.`
55
+ );
56
+ }
57
+ entry.implementations.push(impl);
58
+ }
59
+
60
+ /** 列出所有能力声明 */
61
+ list(): Capability[] {
62
+ return Array.from(this.registry.values()).map((e) => e.capability);
63
+ }
64
+
65
+ /** 检查能力是否存在 */
66
+ has(name: CapabilityName): boolean {
67
+ return this.registry.has(name);
68
+ }
69
+
70
+ /** 获取能力声明 */
71
+ get(name: CapabilityName): Capability | undefined {
72
+ return this.registry.get(name)?.capability;
73
+ }
74
+
75
+ /**
76
+ * 解析能力实现。
77
+ * - 若指定 preferredEngine 且存在匹配实现,直接返回;
78
+ * - 否则按构造时设定的 defaultStrategy 选择('first'/'fastest'/'balanced')。
79
+ */
80
+ resolve(name: CapabilityName, preferredEngine?: string): CapabilityImplementation | undefined {
81
+ const entry = this.registry.get(name);
82
+ if (!entry || entry.implementations.length === 0) return undefined;
83
+
84
+ if (preferredEngine) {
85
+ const impl = entry.implementations.find((i) => i.engine === preferredEngine);
86
+ if (impl) return impl;
87
+ }
88
+
89
+ return this.selectByStrategy(entry.implementations, entry.capability);
90
+ }
91
+
92
+ /** 按默认策略从实现列表中选择一个 */
93
+ private selectByStrategy(
94
+ impls: CapabilityImplementation[],
95
+ capability: Capability
96
+ ): CapabilityImplementation {
97
+ switch (this.defaultStrategy) {
98
+ case 'first':
99
+ return impls[0]!;
100
+ case 'fastest':
101
+ return this.fastest(impls, capability);
102
+ case 'balanced': {
103
+ // 优先取与能力声明 performance 匹配的实现(同档按注册顺序)
104
+ const target = capability.performance;
105
+ const matching = impls.filter((i) => this.perfOf(i, capability) === target);
106
+ if (matching.length > 0) return matching[0]!;
107
+ // 无匹配则退化为最快
108
+ return this.fastest(impls, capability);
109
+ }
110
+ }
111
+ }
112
+
113
+ /** 取性能等级最优的实现(同档按注册顺序,稳定排序) */
114
+ private fastest(
115
+ impls: CapabilityImplementation[],
116
+ capability: Capability
117
+ ): CapabilityImplementation {
118
+ return impls.reduce((best, cur) => {
119
+ const a = PERFORMANCE_RANK[this.perfOf(best, capability)];
120
+ const b = PERFORMANCE_RANK[this.perfOf(cur, capability)];
121
+ // 仅当严格更优时替换,保证同档保留先注册的(稳定性)
122
+ return b < a ? cur : best;
123
+ }, impls[0]!);
124
+ }
125
+
126
+ /** 实现的性能等级:优先用实现自身声明,缺省回退到能力声明 */
127
+ private perfOf(impl: CapabilityImplementation, capability: Capability): PerformanceLevel {
128
+ return impl.performance ?? capability.performance;
129
+ }
130
+
131
+ /** 清除所有注册 */
132
+ clear(): void {
133
+ this.registry.clear();
134
+ }
135
+ }
@@ -0,0 +1,41 @@
1
+ /**
2
+ * Lokvis Event Bus Implementation
3
+ *
4
+ * 基于 mitt 的轻量事件总线实现。
5
+ * 所有操作发出标准事件,Analytics/Undo/AI/Plugin 都监听事件。
6
+ */
7
+
8
+ import mitt from 'mitt';
9
+ import type { EventBus, LokvisEvent } from '@lokvis/schema';
10
+
11
+ type Events = Record<string, unknown>;
12
+
13
+ export function createEventBus(): EventBus {
14
+ const emitter = mitt<Events>();
15
+ const anyHandlers = new Set<(event: LokvisEvent) => void>();
16
+
17
+ return {
18
+ on(type, handler) {
19
+ const wrapped = (e: unknown) => handler(e as never);
20
+ emitter.on(type, wrapped);
21
+ return () => emitter.off(type, wrapped);
22
+ },
23
+
24
+ onAny(handler) {
25
+ anyHandlers.add(handler);
26
+ return () => anyHandlers.delete(handler);
27
+ },
28
+
29
+ emit(event) {
30
+ emitter.emit(event.type, event);
31
+ for (const handler of anyHandlers) {
32
+ handler(event);
33
+ }
34
+ },
35
+
36
+ clear() {
37
+ emitter.all.clear();
38
+ anyHandlers.clear();
39
+ },
40
+ };
41
+ }