@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,531 @@
1
+ /**
2
+ * WorkerHost —— 主线程侧的 Worker 管理器
3
+ *
4
+ * 职责(对应 PROJECT_PLAN 1.6):
5
+ * - 生成 / 重启 Worker,完成 ready 握手。
6
+ * - Request/Response 按 id 关联,每请求独立超时。
7
+ * - 心跳(Heartbeat):周期 ping,超时未 pong 视为崩溃。
8
+ * - 崩溃重启:传输层 error / 心跳超时 → 终止并重建(最多 maxRestarts 次)。
9
+ * - 事件分发:Worker 主动 event + 生命周期事件(ready/restart/crash/dead)。
10
+ *
11
+ * 传输层抽象(WorkerTransport)使其与具体 Worker 实现解耦:
12
+ * - 浏览器:createBrowserWorkerTransport(url)
13
+ * - 测试:注入 Fake 传输,无需真实 Worker。
14
+ */
15
+
16
+ import {
17
+ WORKER_PROTOCOL_VERSION,
18
+ DEFAULT_HEARTBEAT_INTERVAL_MS,
19
+ DEFAULT_HEARTBEAT_TIMEOUT_MS,
20
+ DEFAULT_REQUEST_TIMEOUT_MS,
21
+ DEFAULT_MAX_RESTARTS,
22
+ DEFAULT_READY_TIMEOUT_MS,
23
+ createRequestId,
24
+ isWorkerResponse,
25
+ isWorkerPong,
26
+ isWorkerReady,
27
+ isWorkerEvent,
28
+ isWorkerFatalError,
29
+ type WorkerRequest,
30
+ type WorkerPing,
31
+ } from './worker-protocol.js';
32
+
33
+ // ─── 传输层抽象 ─────────────────────────────────────────────────
34
+
35
+ /** 规范化的传输层错误 */
36
+ export interface WorkerTransportError {
37
+ message: string;
38
+ filename?: string;
39
+ lineno?: number;
40
+ stack?: string;
41
+ }
42
+
43
+ /**
44
+ * 传输层接口:屏蔽 浏览器 Worker / Node worker_threads / Fake 差异。
45
+ * Host 仅依赖该接口,便于单测注入 Fake。
46
+ */
47
+ export interface WorkerTransport {
48
+ /** 向 Worker 发消息(可附带可转移对象) */
49
+ send(message: unknown, transfer?: Transferable[]): void;
50
+ /** 监听 Worker → Host 消息,返回取消订阅 */
51
+ onMessage(handler: (data: unknown) => void): () => void;
52
+ /** 监听传输层错误(脚本加载失败 / 未捕获异常 / 异常退出),返回取消订阅 */
53
+ onError(handler: (err: WorkerTransportError) => void): () => void;
54
+ /** 终止 Worker */
55
+ terminate(): void;
56
+ }
57
+
58
+ // ─── 错误类型 ───────────────────────────────────────────────────
59
+
60
+ /** Worker 崩溃(传输层错误或心跳超时) */
61
+ export class WorkerCrashedError extends Error {
62
+ readonly reason: string;
63
+ readonly restartCount: number;
64
+ constructor(reason: string, restartCount: number) {
65
+ super(`Worker crashed: ${reason}`);
66
+ this.name = 'WorkerCrashedError';
67
+ this.reason = reason;
68
+ this.restartCount = restartCount;
69
+ }
70
+ }
71
+
72
+ /** Worker 正在重启,拒绝新请求 */
73
+ export class WorkerRestartingError extends Error {
74
+ constructor() {
75
+ super('Worker is restarting');
76
+ this.name = 'WorkerRestartingError';
77
+ }
78
+ }
79
+
80
+ /** Worker 已死亡(超过最大重启次数) */
81
+ export class WorkerDeadError extends Error {
82
+ readonly restartCount: number;
83
+ constructor(restartCount: number) {
84
+ super(`Worker is dead after ${restartCount} restart attempts`);
85
+ this.name = 'WorkerDeadError';
86
+ this.restartCount = restartCount;
87
+ }
88
+ }
89
+
90
+ /** 单个请求超时 */
91
+ export class WorkerRequestTimeoutError extends Error {
92
+ readonly method: string;
93
+ constructor(method: string, timeoutMs: number) {
94
+ super(`Request "${method}" timed out after ${timeoutMs}ms`);
95
+ this.name = 'WorkerRequestTimeoutError';
96
+ this.method = method;
97
+ }
98
+ }
99
+
100
+ /** ready 握手失败(协议版本不匹配或超时) */
101
+ export class WorkerHandshakeError extends Error {
102
+ constructor(message: string) {
103
+ super(message);
104
+ this.name = 'WorkerHandshakeError';
105
+ }
106
+ }
107
+
108
+ // ─── Host 选项与事件 ────────────────────────────────────────────
109
+
110
+ export interface WorkerHostOptions {
111
+ /** 心跳发送间隔 */
112
+ heartbeatIntervalMs?: number;
113
+ /** 心跳超时(未收到 pong 视为崩溃) */
114
+ heartbeatTimeoutMs?: number;
115
+ /** 单个请求默认超时 */
116
+ requestTimeoutMs?: number;
117
+ /** 等待 Worker ready 的超时 */
118
+ readyTimeoutMs?: number;
119
+ /** 崩溃后最大重启次数 */
120
+ maxRestarts?: number;
121
+ /** 传输层工厂(重启时复用) */
122
+ createTransport: () => WorkerTransport;
123
+ /** 是否输出日志 */
124
+ enableLog?: boolean;
125
+ }
126
+
127
+ export type WorkerHostStatus =
128
+ | 'idle' // 未启动
129
+ | 'ready' // 可用
130
+ | 'restarting' // 崩溃后重启中
131
+ | 'dead' // 超过最大重启次数
132
+ | 'disposed'; // 已销毁
133
+
134
+ /** 生命周期 + Worker 主动事件 */
135
+ export interface WorkerHostEventMap {
136
+ ready: { restartCount: number };
137
+ restart: { reason: string; restartCount: number };
138
+ crash: { reason: string; restartCount: number };
139
+ dead: { reason: string; restartCount: number };
140
+ event: { event: string; payload?: unknown };
141
+ }
142
+
143
+ export type WorkerHostEventType = keyof WorkerHostEventMap;
144
+
145
+ type Listener<T> = (payload: T) => void;
146
+
147
+ interface PendingRequest {
148
+ resolve: (value: unknown) => void;
149
+ reject: (error: Error) => void;
150
+ timer: ReturnType<typeof setTimeout>;
151
+ method: string;
152
+ }
153
+
154
+ // ─── WorkerHost ─────────────────────────────────────────────────
155
+
156
+ export class WorkerHost {
157
+ private opts: Required<Omit<WorkerHostOptions, 'createTransport' | 'enableLog'>> &
158
+ Pick<WorkerHostOptions, 'createTransport' | 'enableLog'>;
159
+
160
+ private status: WorkerHostStatus = 'idle';
161
+ private transport: WorkerTransport | null = null;
162
+ private pending = new Map<string, PendingRequest>();
163
+ private listeners = new Map<WorkerHostEventType, Set<Listener<unknown>>>();
164
+
165
+ private restartCount = 0;
166
+ private heartbeatTimer: ReturnType<typeof setInterval> | null = null;
167
+ private pongTimer: ReturnType<typeof setTimeout> | null = null;
168
+ private awaitingPong = false;
169
+ private offMessage: (() => void) | null = null;
170
+ private offError: (() => void) | null = null;
171
+
172
+ constructor(opts: WorkerHostOptions) {
173
+ this.opts = {
174
+ heartbeatIntervalMs: opts.heartbeatIntervalMs ?? DEFAULT_HEARTBEAT_INTERVAL_MS,
175
+ heartbeatTimeoutMs: opts.heartbeatTimeoutMs ?? DEFAULT_HEARTBEAT_TIMEOUT_MS,
176
+ requestTimeoutMs: opts.requestTimeoutMs ?? DEFAULT_REQUEST_TIMEOUT_MS,
177
+ readyTimeoutMs: opts.readyTimeoutMs ?? DEFAULT_READY_TIMEOUT_MS,
178
+ maxRestarts: opts.maxRestarts ?? DEFAULT_MAX_RESTARTS,
179
+ createTransport: opts.createTransport,
180
+ enableLog: opts.enableLog ?? false,
181
+ };
182
+ }
183
+
184
+ get currentStatus(): WorkerHostStatus {
185
+ return this.status;
186
+ }
187
+
188
+ /** 已发生的重启次数 */
189
+ get currentRestartCount(): number {
190
+ return this.restartCount;
191
+ }
192
+
193
+ /** 订阅事件,返回取消订阅 */
194
+ on<K extends WorkerHostEventType>(
195
+ type: K,
196
+ handler: Listener<WorkerHostEventMap[K]>
197
+ ): () => void {
198
+ let set = this.listeners.get(type);
199
+ if (!set) {
200
+ set = new Set();
201
+ this.listeners.set(type, set);
202
+ }
203
+ set.add(handler as Listener<unknown>);
204
+ return () => set!.delete(handler as Listener<unknown>);
205
+ }
206
+
207
+ private emit<K extends WorkerHostEventType>(
208
+ type: K,
209
+ payload: WorkerHostEventMap[K]
210
+ ): void {
211
+ const set = this.listeners.get(type);
212
+ if (!set) return;
213
+ for (const h of set) (h as Listener<WorkerHostEventMap[K]>)(payload);
214
+ }
215
+
216
+ private log(message: string): void {
217
+ if (this.opts.enableLog) console.debug(`[WorkerHost] ${message}`);
218
+ }
219
+
220
+ // ─── 生命周期 ───────────────────────────────────────────────
221
+
222
+ /** 启动并完成 ready 握手 */
223
+ async init(): Promise<void> {
224
+ if (this.status === 'disposed') throw new Error('WorkerHost is disposed');
225
+ if (this.status !== 'idle') return; // 已启动
226
+ await this.spawn();
227
+ }
228
+
229
+ /** 销毁:终止 Worker、清理定时器、拒绝所有 pending */
230
+ async dispose(): Promise<void> {
231
+ if (this.status === 'disposed') return;
232
+ this.status = 'disposed';
233
+ this.clearHeartbeat();
234
+ this.rejectAllPending(new Error('WorkerHost disposed'));
235
+ this.offMessage?.();
236
+ this.offError?.();
237
+ this.offMessage = null;
238
+ this.offError = null;
239
+ this.transport?.terminate();
240
+ this.transport = null;
241
+ this.listeners.clear();
242
+ }
243
+
244
+ // ─── 请求 ───────────────────────────────────────────────────
245
+
246
+ /** 调用 Worker 方法并等待响应 */
247
+ async request<T = unknown>(
248
+ method: string,
249
+ params?: unknown,
250
+ options?: { transfer?: Transferable[]; timeoutMs?: number }
251
+ ): Promise<T> {
252
+ if (this.status === 'disposed') throw new Error('WorkerHost is disposed');
253
+ if (this.status === 'dead') throw new WorkerDeadError(this.restartCount);
254
+ if (this.status === 'restarting' || this.status === 'idle') {
255
+ throw new WorkerRestartingError();
256
+ }
257
+
258
+ const id = createRequestId();
259
+ const timeoutMs = options?.timeoutMs ?? this.opts.requestTimeoutMs;
260
+ const msg: WorkerRequest = { id, type: 'request', method, params };
261
+
262
+ return new Promise<T>((resolve, reject) => {
263
+ const timer = setTimeout(() => {
264
+ if (this.pending.delete(id)) {
265
+ reject(new WorkerRequestTimeoutError(method, timeoutMs));
266
+ }
267
+ }, timeoutMs);
268
+
269
+ this.pending.set(id, {
270
+ resolve: (v) => resolve(v as T),
271
+ reject,
272
+ timer,
273
+ method,
274
+ });
275
+
276
+ this.send(msg, options?.transfer);
277
+ });
278
+ }
279
+
280
+ // ─── 内部:spawn / 消息处理 / 崩溃 ──────────────────────────
281
+
282
+ /** 创建传输层、绑定监听、等待 ready */
283
+ private async spawn(): Promise<void> {
284
+ const transport = this.opts.createTransport();
285
+ this.transport = transport;
286
+
287
+ this.offMessage = transport.onMessage((data) => this.handleMessage(data));
288
+ this.offError = transport.onError((err) =>
289
+ this.handleCrash(`transport error: ${err.message}`)
290
+ );
291
+
292
+ // 等待 ready 握手(超时或协议不匹配则拒绝,由 catch 清理 transport)
293
+ let readyTimer: ReturnType<typeof setTimeout>;
294
+ try {
295
+ await new Promise<void>((resolve, reject) => {
296
+ let done = false;
297
+ readyTimer = setTimeout(() => {
298
+ if (done) return;
299
+ done = true;
300
+ reject(
301
+ new WorkerHandshakeError(
302
+ `Worker did not send ready within ${this.opts.readyTimeoutMs}ms`
303
+ )
304
+ );
305
+ }, this.opts.readyTimeoutMs);
306
+ readyTimer.unref?.();
307
+
308
+ const off = this.on('ready', () => {
309
+ if (done) return;
310
+ done = true;
311
+ clearTimeout(readyTimer);
312
+ off();
313
+ resolve();
314
+ });
315
+ });
316
+ } catch (err) {
317
+ clearTimeout(readyTimer!);
318
+ this.offMessage?.();
319
+ this.offError?.();
320
+ this.offMessage = null;
321
+ this.offError = null;
322
+ transport.terminate();
323
+ this.transport = null;
324
+ throw err as Error;
325
+ }
326
+
327
+ this.status = 'ready';
328
+ this.startHeartbeat();
329
+ }
330
+
331
+ private handleMessage(data: unknown): void {
332
+ // ready(可能在握手期间或重启后)
333
+ if (isWorkerReady(data)) {
334
+ if (!data.protocolVersion || data.protocolVersion !== WORKER_PROTOCOL_VERSION) {
335
+ this.handleCrash(
336
+ `protocol mismatch: worker=${data.protocolVersion} host=${WORKER_PROTOCOL_VERSION}`
337
+ );
338
+ return;
339
+ }
340
+ // ready 事件由 spawn() 的监听器消费;重启后再次 ready 也触发
341
+ this.emit('ready', { restartCount: this.restartCount });
342
+ return;
343
+ }
344
+
345
+ if (isWorkerPong(data)) {
346
+ this.awaitingPong = false;
347
+ if (this.pongTimer) {
348
+ clearTimeout(this.pongTimer);
349
+ this.pongTimer = null;
350
+ }
351
+ return;
352
+ }
353
+
354
+ if (isWorkerResponse(data)) {
355
+ const pending = this.pending.get(data.id);
356
+ if (!pending) return; // 已超时或已取消
357
+ this.pending.delete(data.id);
358
+ clearTimeout(pending.timer);
359
+ if (data.ok) pending.resolve(data.result);
360
+ else {
361
+ const e = data.error;
362
+ const err = new Error(e.message);
363
+ err.name = e.code ?? 'WorkerError';
364
+ if (e.stack) err.stack = e.stack;
365
+ pending.reject(err);
366
+ }
367
+ return;
368
+ }
369
+
370
+ if (isWorkerEvent(data)) {
371
+ this.emit('event', { event: data.event, payload: data.payload });
372
+ return;
373
+ }
374
+
375
+ if (isWorkerFatalError(data)) {
376
+ // Worker 主动上报致命错误 → 视为崩溃
377
+ this.handleCrash(`worker fatal: ${data.message}`);
378
+ return;
379
+ }
380
+ }
381
+
382
+ /** 崩溃处理:拒绝 pending → 终止 → 重启(受 maxRestarts 限制) */
383
+ private handleCrash(reason: string): void {
384
+ // 正在重启/已死亡/已销毁时,忽略额外的崩溃信号(避免重入)
385
+ if (
386
+ this.status === 'disposed' ||
387
+ this.status === 'dead' ||
388
+ this.status === 'restarting'
389
+ ) {
390
+ return;
391
+ }
392
+
393
+ this.log(`crash: ${reason}`);
394
+ this.status = 'restarting';
395
+ this.teardownTransport();
396
+ this.rejectAllPending(new WorkerCrashedError(reason, this.restartCount));
397
+ this.emit('crash', { reason, restartCount: this.restartCount });
398
+ this.tryRestart(reason);
399
+ }
400
+
401
+ /**
402
+ * 重启尝试循环。spawn 失败(ready 超时)时,直接 teardown 并重试,
403
+ * 不经过 handleCrash 的 restarting 守卫——否则永远到不了 dead。
404
+ */
405
+ private tryRestart(reason: string): void {
406
+ if (this.status === 'disposed') return;
407
+ if (this.restartCount >= this.opts.maxRestarts) {
408
+ this.status = 'dead';
409
+ this.emit('dead', { reason, restartCount: this.restartCount });
410
+ return;
411
+ }
412
+ this.restartCount++;
413
+ this.log(`restarting (attempt ${this.restartCount}/${this.opts.maxRestarts})`);
414
+ this.spawn()
415
+ .then(() => {
416
+ this.emit('restart', { reason, restartCount: this.restartCount });
417
+ })
418
+ .catch((err: Error) => {
419
+ // 重启失败:终止本次(未握手成功的)transport,继续下一轮重试
420
+ this.teardownTransport();
421
+ this.tryRestart(`restart failed: ${err.message}`);
422
+ });
423
+ }
424
+
425
+ /** 解绑当前 transport 的监听并终止 */
426
+ private teardownTransport(): void {
427
+ this.clearHeartbeat();
428
+ this.offMessage?.();
429
+ this.offError?.();
430
+ this.offMessage = null;
431
+ this.offError = null;
432
+ this.transport?.terminate();
433
+ this.transport = null;
434
+ }
435
+
436
+ // ─── 心跳 ───────────────────────────────────────────────────
437
+
438
+ private startHeartbeat(): void {
439
+ this.clearHeartbeat();
440
+ this.awaitingPong = false;
441
+ this.heartbeatTimer = setInterval(() => {
442
+ this.sendPing();
443
+ }, this.opts.heartbeatIntervalMs);
444
+ this.heartbeatTimer.unref?.();
445
+ }
446
+
447
+ private sendPing(): void {
448
+ if (this.awaitingPong) return; // 上一轮 pong 未回,等超时触发崩溃
449
+ this.awaitingPong = true;
450
+ const msg: WorkerPing = {
451
+ id: createRequestId(),
452
+ type: 'ping',
453
+ ts: Date.now(),
454
+ };
455
+ this.send(msg);
456
+ this.pongTimer = setTimeout(() => {
457
+ this.handleCrash(`heartbeat timeout (no pong in ${this.opts.heartbeatTimeoutMs}ms)`);
458
+ }, this.opts.heartbeatTimeoutMs);
459
+ this.pongTimer.unref?.();
460
+ }
461
+
462
+ private clearHeartbeat(): void {
463
+ if (this.heartbeatTimer) {
464
+ clearInterval(this.heartbeatTimer);
465
+ this.heartbeatTimer = null;
466
+ }
467
+ if (this.pongTimer) {
468
+ clearTimeout(this.pongTimer);
469
+ this.pongTimer = null;
470
+ }
471
+ this.awaitingPong = false;
472
+ }
473
+
474
+ // ─── 工具 ───────────────────────────────────────────────────
475
+
476
+ private send(message: unknown, transfer?: Transferable[]): void {
477
+ if (!this.transport) return;
478
+ this.transport.send(message, transfer);
479
+ }
480
+
481
+ private rejectAllPending(error: Error): void {
482
+ for (const [, p] of this.pending) {
483
+ clearTimeout(p.timer);
484
+ p.reject(error);
485
+ }
486
+ this.pending.clear();
487
+ }
488
+ }
489
+
490
+ // ─── 浏览器默认传输层 ───────────────────────────────────────────
491
+
492
+ /**
493
+ * 创建浏览器 Web Worker 传输层。
494
+ * 仅在浏览器主线程可用(Node 测试应注入 Fake 传输)。
495
+ */
496
+ export function createBrowserWorkerTransport(url: string | URL): WorkerTransport {
497
+ const worker = new Worker(url, { type: 'module' });
498
+ const messageHandlers = new Set<(data: unknown) => void>();
499
+ const errorHandlers = new Set<(err: WorkerTransportError) => void>();
500
+
501
+ worker.addEventListener('message', (ev: MessageEvent) => {
502
+ for (const h of messageHandlers) h(ev.data);
503
+ });
504
+ worker.addEventListener('error', (ev: ErrorEvent) => {
505
+ const err: WorkerTransportError = {
506
+ message: ev.message || 'Worker error',
507
+ filename: ev.filename,
508
+ lineno: ev.lineno,
509
+ stack: ev.error instanceof Error ? ev.error.stack : undefined,
510
+ };
511
+ for (const h of errorHandlers) h(err);
512
+ });
513
+
514
+ return {
515
+ send(message, transfer) {
516
+ if (transfer && transfer.length > 0) worker.postMessage(message, transfer);
517
+ else worker.postMessage(message);
518
+ },
519
+ onMessage(handler) {
520
+ messageHandlers.add(handler);
521
+ return () => messageHandlers.delete(handler);
522
+ },
523
+ onError(handler) {
524
+ errorHandlers.add(handler);
525
+ return () => errorHandlers.delete(handler);
526
+ },
527
+ terminate() {
528
+ worker.terminate();
529
+ },
530
+ };
531
+ }