fluxion-ts 0.0.4

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 (43) hide show
  1. package/.oxlintrc.json +64 -0
  2. package/.prettierrc +6 -0
  3. package/AGENTS.md +3 -0
  4. package/Dockerfile +13 -0
  5. package/LICENSE +21 -0
  6. package/README.md +11 -0
  7. package/document/index.html +16 -0
  8. package/document/src/main.tsx +8 -0
  9. package/document/src/styles.css +321 -0
  10. package/document/src/view/App.tsx +304 -0
  11. package/document/src/view/CodeBlock.tsx +11 -0
  12. package/document/src/view/Section.tsx +18 -0
  13. package/document/vite.config.ts +23 -0
  14. package/draft/vibe.md +50 -0
  15. package/package.json +66 -0
  16. package/rollup.config.mjs +102 -0
  17. package/scripts/build-image.ts +13 -0
  18. package/scripts/bump-version.ts +12 -0
  19. package/scripts/configs.ts +79 -0
  20. package/scripts/lines.ts +54 -0
  21. package/scripts/publish.ts +6 -0
  22. package/src/common/consts.ts +30 -0
  23. package/src/common/dtm.ts +10 -0
  24. package/src/common/logger.ts +145 -0
  25. package/src/core/meta-api.ts +48 -0
  26. package/src/core/server.ts +447 -0
  27. package/src/core/types.d.ts +6 -0
  28. package/src/core/utils/headers.ts +34 -0
  29. package/src/core/utils/request.ts +145 -0
  30. package/src/core/utils/send-json.ts +21 -0
  31. package/src/index.ts +11 -0
  32. package/src/workers/file-runtime.ts +1071 -0
  33. package/src/workers/handler-worker-pool.ts +754 -0
  34. package/src/workers/handler-worker.ts +1029 -0
  35. package/src/workers/options.ts +77 -0
  36. package/src/workers/protocol.d.ts +186 -0
  37. package/tests/core/dynamic-directory.test.ts +48 -0
  38. package/tests/core/file-runtime.test.ts +347 -0
  39. package/tests/core/server-options.test.ts +204 -0
  40. package/tests/e2e/fluxion-server.e2e-spec.ts +225 -0
  41. package/tests/helpers/test-utils.ts +81 -0
  42. package/tsconfig.json +22 -0
  43. package/vitest.config.ts +24 -0
@@ -0,0 +1,754 @@
1
+ import path from 'node:path';
2
+ import { fileURLToPath } from 'node:url';
3
+ import { Worker } from 'node:worker_threads';
4
+
5
+ import { createLogger, getErrorMessage, type FluxionLogger } from '@/common/logger.js';
6
+
7
+ import type { protocol } from './protocol.js';
8
+ import type { ExecutorOptions } from './options.js';
9
+ import { resolveExecutorOptions } from './options.js';
10
+
11
+ /**
12
+ * Re-hydrated error from worker side.
13
+ */
14
+ class WorkerRuntimeError extends Error {
15
+ /**
16
+ * Worker-provided error code.
17
+ */
18
+ public readonly code?: string;
19
+
20
+ /**
21
+ * @param error Serialized worker error payload.
22
+ */
23
+ constructor(error: protocol.SerializedError) {
24
+ super(error.message);
25
+ this.name = error.name;
26
+ this.code = error.code;
27
+ this.stack = error.stack ?? this.stack;
28
+ }
29
+ }
30
+
31
+ /**
32
+ * Worker pool static metadata.
33
+ */
34
+ interface WorkerPoolMeta {
35
+ /**
36
+ * Stable worker id.
37
+ */
38
+ id: string;
39
+ /**
40
+ * Database names available in this worker.
41
+ */
42
+ dbSet: string[];
43
+ /**
44
+ * Marks the auto-added all-db fallback worker.
45
+ */
46
+ isFallbackAllDb: boolean;
47
+ }
48
+
49
+ /**
50
+ * Factory input for one worker pool.
51
+ */
52
+ export interface CreateHandlerWorkerPoolOptions {
53
+ /**
54
+ * Worker pool metadata.
55
+ */
56
+ meta: WorkerPoolMeta;
57
+ /**
58
+ * Runtime option overrides for this worker.
59
+ */
60
+ overrides?: Partial<ExecutorOptions>;
61
+ /**
62
+ * Logger implementation shared by runtime.
63
+ */
64
+ logger?: FluxionLogger;
65
+ }
66
+
67
+ /**
68
+ * Execute result payload returned by pool.
69
+ */
70
+ export interface HandlerExecuteResult {
71
+ /**
72
+ * Serialized HTTP response.
73
+ */
74
+ response: protocol.SerializedResponse;
75
+ }
76
+
77
+ /**
78
+ * Snapshot used by meta api to expose worker runtime state.
79
+ */
80
+ export interface HandlerWorkerSnapshot {
81
+ /**
82
+ * Worker lifecycle status.
83
+ */
84
+ status: 'running' | 'stopped' | 'restarting' | 'closed';
85
+ /**
86
+ * Worker id.
87
+ */
88
+ id: string;
89
+ /**
90
+ * Worker database capability set.
91
+ */
92
+ dbSet: string[];
93
+ /**
94
+ * Whether this worker is fallback all-db worker.
95
+ */
96
+ isFallbackAllDb: boolean;
97
+ /**
98
+ * Current worker thread id.
99
+ */
100
+ threadId?: number;
101
+ /**
102
+ * Number of in-flight requests.
103
+ */
104
+ inflight: number;
105
+ /**
106
+ * Number of tracked handler versions.
107
+ */
108
+ trackedHandlers: number;
109
+ /**
110
+ * Main-thread handler version table.
111
+ */
112
+ handlers: Array<{ filePath: string; version: string }>;
113
+ /**
114
+ * Total supervisor restart count.
115
+ */
116
+ restartCount: number;
117
+ /**
118
+ * Last restart reason.
119
+ */
120
+ lastRestartReason?: string;
121
+ /**
122
+ * Epoch timestamp of last restart.
123
+ */
124
+ lastRestartAt?: number;
125
+ /**
126
+ * Active runtime limits.
127
+ */
128
+ limits: {
129
+ /**
130
+ * Request timeout in milliseconds.
131
+ */
132
+ requestTimeoutMs: number;
133
+ /**
134
+ * Max parallel requests.
135
+ */
136
+ maxInflight: number;
137
+ /**
138
+ * Soft heap cap in MB.
139
+ */
140
+ memorySoftLimitMb: number;
141
+ /**
142
+ * ! Hard heap cap in MB.
143
+ */
144
+ memoryHardLimitMb: number;
145
+ /**
146
+ * ! V8 old-space cap in MB.
147
+ */
148
+ maxOldGenerationSizeMb: number;
149
+ /**
150
+ * ! V8 young-space cap in MB.
151
+ */
152
+ maxYoungGenerationSizeMb: number;
153
+ /**
154
+ * V8 stack cap in MB.
155
+ */
156
+ stackSizeMb: number;
157
+ /**
158
+ * ! Maximum worker response payload bytes.
159
+ */
160
+ maxResponseBytes: number;
161
+ };
162
+ /**
163
+ * Latest sampled memory data.
164
+ */
165
+ memory?: {
166
+ /**
167
+ * Heap used bytes.
168
+ */
169
+ heapUsed: number;
170
+ /**
171
+ * RSS bytes.
172
+ */
173
+ rss: number;
174
+ /**
175
+ * External memory bytes.
176
+ */
177
+ external: number;
178
+ /**
179
+ * ArrayBuffer bytes.
180
+ */
181
+ arrayBuffers: number;
182
+ /**
183
+ * Epoch timestamp when sample was received.
184
+ */
185
+ sampledAt: number;
186
+ };
187
+ }
188
+
189
+ /**
190
+ * Resolves worker entry for tsx dev and compiled js runtime.
191
+ */
192
+ function resolveWorkerEntryUrl(): URL {
193
+ const currentExt = path.extname(fileURLToPath(import.meta.url));
194
+ const workerFileName = currentExt === '.ts' ? 'handler-worker.ts' : 'handler-worker.js';
195
+ return new URL(`./${workerFileName}`, import.meta.url);
196
+ }
197
+
198
+ /**
199
+ * Main-thread API for worker-backed handler execution.
200
+ */
201
+ export interface HandlerWorkerPool {
202
+ /**
203
+ * Executes a handler request in worker.
204
+ */
205
+ execute(payload: protocol.Payload): Promise<HandlerExecuteResult>;
206
+ /**
207
+ * Clears tracked state and rotates worker.
208
+ */
209
+ clearCache(): Promise<void>;
210
+ /**
211
+ * Closes worker and rejects pending requests.
212
+ */
213
+ close(): Promise<void>;
214
+ /**
215
+ * Returns a runtime snapshot for diagnostics.
216
+ */
217
+ getSnapshot(): HandlerWorkerSnapshot;
218
+ }
219
+
220
+ /**
221
+ * Pending execute request state.
222
+ */
223
+ interface ExecuteInflightRequest {
224
+ resolve: (result: HandlerExecuteResult) => void;
225
+ reject: (error: Error) => void;
226
+ timer: NodeJS.Timeout;
227
+ }
228
+
229
+ type InflightRequest = ExecuteInflightRequest;
230
+
231
+ /**
232
+ * Creates a worker pool using merged runtime defaults.
233
+ */
234
+ export function createHandlerWorkerPool(options: CreateHandlerWorkerPoolOptions): HandlerWorkerPool {
235
+ return new HandlerWorkerPoolImpl(
236
+ options.meta,
237
+ resolveExecutorOptions(options.overrides),
238
+ options.logger ?? createLogger('one-line'),
239
+ );
240
+ }
241
+
242
+ class HandlerWorkerPoolImpl implements HandlerWorkerPool {
243
+ /**
244
+ * Worker pool metadata.
245
+ */
246
+ private readonly meta: WorkerPoolMeta;
247
+
248
+ /**
249
+ * Resolved runtime options.
250
+ */
251
+ private readonly options: ExecutorOptions;
252
+
253
+ /**
254
+ * Runtime logger.
255
+ */
256
+ private readonly logger: FluxionLogger;
257
+
258
+ /**
259
+ * Worker entry module url.
260
+ */
261
+ private readonly workerUrl: URL;
262
+
263
+ /**
264
+ * Pending request registry.
265
+ */
266
+ private readonly inflight = new Map<string, InflightRequest>();
267
+
268
+ /**
269
+ * File path -> version cache mirrored in main thread.
270
+ */
271
+ private readonly versionsByFilePath = new Map<string, string>();
272
+
273
+ /**
274
+ * Monotonic request id counter.
275
+ */
276
+ private requestCounter = 0;
277
+
278
+ /**
279
+ * Current worker instance.
280
+ */
281
+ private worker: Worker | undefined;
282
+
283
+ /**
284
+ * Restart mutex promise.
285
+ */
286
+ private restarting: Promise<void> | undefined;
287
+
288
+ /**
289
+ * Indicates the pool has been closed.
290
+ */
291
+ private closed = false;
292
+
293
+ /**
294
+ * Latest memory message from worker.
295
+ */
296
+ private latestMemory: protocol.MemoryMessage | undefined;
297
+
298
+ /**
299
+ * Timestamp of latest memory message.
300
+ */
301
+ private latestMemoryAt: number | undefined;
302
+
303
+ /**
304
+ * Total restart times.
305
+ */
306
+ private restartCount = 0;
307
+
308
+ /**
309
+ * Last restart reason text.
310
+ */
311
+ private lastRestartReason: string | undefined;
312
+
313
+ /**
314
+ * Timestamp of last restart.
315
+ */
316
+ private lastRestartAt: number | undefined;
317
+
318
+ /**
319
+ * @param meta Worker metadata.
320
+ * @param options Resolved runtime options.
321
+ */
322
+ constructor(meta: WorkerPoolMeta, options: ExecutorOptions, logger: FluxionLogger) {
323
+ this.meta = {
324
+ id: meta.id,
325
+ dbSet: [...meta.dbSet],
326
+ isFallbackAllDb: meta.isFallbackAllDb,
327
+ };
328
+ this.options = options;
329
+ this.logger = logger;
330
+ this.workerUrl = resolveWorkerEntryUrl();
331
+ }
332
+
333
+ /**
334
+ * Executes request and retries once when version mismatch is detected.
335
+ */
336
+ async execute(payload: protocol.Payload): Promise<HandlerExecuteResult> {
337
+ return this.executeWithRetry(payload, false);
338
+ }
339
+
340
+ /**
341
+ * Clears version cache and restarts the worker.
342
+ */
343
+ async clearCache(): Promise<void> {
344
+ this.versionsByFilePath.clear();
345
+ await this.restart('cache_cleared');
346
+ }
347
+
348
+ /**
349
+ * Stops worker and rejects all in-flight requests.
350
+ */
351
+ async close(): Promise<void> {
352
+ if (this.closed) {
353
+ return;
354
+ }
355
+
356
+ this.closed = true;
357
+ this.rejectInflight(new Error('runtime worker closed'));
358
+
359
+ const worker = this.worker;
360
+ this.worker = undefined;
361
+
362
+ if (worker === undefined) {
363
+ return;
364
+ }
365
+
366
+ try {
367
+ await worker.terminate();
368
+ } catch (error) {
369
+ this.logger.write('WARN', 'RuntimeWorkerTerminateFailed', {
370
+ workerId: this.meta.id,
371
+ error: getErrorMessage(error),
372
+ });
373
+ }
374
+ }
375
+
376
+ /**
377
+ * Builds current pool diagnostics snapshot.
378
+ */
379
+ getSnapshot(): HandlerWorkerSnapshot {
380
+ const handlers = Array.from(this.versionsByFilePath.entries())
381
+ .map(([filePath, version]) => ({ filePath, version }))
382
+ .sort((left, right) => left.filePath.localeCompare(right.filePath));
383
+
384
+ return {
385
+ status: this.getStatus(),
386
+ id: this.meta.id,
387
+ dbSet: [...this.meta.dbSet],
388
+ isFallbackAllDb: this.meta.isFallbackAllDb,
389
+ threadId: this.worker?.threadId,
390
+ inflight: this.inflight.size,
391
+ trackedHandlers: handlers.length,
392
+ handlers,
393
+ restartCount: this.restartCount,
394
+ lastRestartReason: this.lastRestartReason,
395
+ lastRestartAt: this.lastRestartAt,
396
+ limits: {
397
+ requestTimeoutMs: this.options.requestTimeoutMs,
398
+ maxInflight: this.options.maxInflight,
399
+ memorySoftLimitMb: this.options.memorySoftLimitMb,
400
+ memoryHardLimitMb: this.options.memoryHardLimitMb,
401
+ maxOldGenerationSizeMb: this.options.maxOldGenerationSizeMb,
402
+ maxYoungGenerationSizeMb: this.options.maxYoungGenerationSizeMb,
403
+ stackSizeMb: this.options.stackSizeMb,
404
+ maxResponseBytes: this.options.maxResponseBytes,
405
+ },
406
+ memory:
407
+ this.latestMemory === undefined || this.latestMemoryAt === undefined
408
+ ? undefined
409
+ : {
410
+ heapUsed: this.latestMemory.heapUsed,
411
+ rss: this.latestMemory.rss,
412
+ external: this.latestMemory.external,
413
+ arrayBuffers: this.latestMemory.arrayBuffers,
414
+ sampledAt: this.latestMemoryAt,
415
+ },
416
+ };
417
+ }
418
+
419
+ /**
420
+ * Executes one request with retry path for stale worker cache.
421
+ */
422
+ private async executeWithRetry(payload: protocol.Payload, retried: boolean): Promise<HandlerExecuteResult> {
423
+ try {
424
+ return await this.executeOnce(payload);
425
+ } catch (error) {
426
+ const code = (error as NodeJS.ErrnoException).code;
427
+
428
+ if (!retried && code === 'WORKER_VERSION_MISMATCH') {
429
+ await this.restart('worker_version_mismatch');
430
+ this.versionsByFilePath.set(payload.filePath, payload.version);
431
+ return this.executeWithRetry(payload, true);
432
+ }
433
+
434
+ throw error;
435
+ }
436
+ }
437
+
438
+ /**
439
+ * Enqueues a request into worker with timeout protection.
440
+ */
441
+ private async executeOnce(payload: protocol.Payload): Promise<HandlerExecuteResult> {
442
+ this.assertExecutable();
443
+
444
+ if (this.inflight.size >= this.options.maxInflight) {
445
+ const overloadError = new Error('runtime worker overloaded');
446
+ (overloadError as NodeJS.ErrnoException).code = 'WORKER_OVERLOADED';
447
+ throw overloadError;
448
+ }
449
+
450
+ this.assertKnownVersion(payload.filePath, payload.version);
451
+
452
+ this.versionsByFilePath.set(payload.filePath, payload.version);
453
+
454
+ const worker = this.ensureWorker();
455
+
456
+ return new Promise<HandlerExecuteResult>((resolve, reject) => {
457
+ const id = this.nextRequestId();
458
+
459
+ const timer = setTimeout(() => {
460
+ this.inflight.delete(id);
461
+
462
+ const timeoutError = new Error(`runtime worker timeout after ${this.options.requestTimeoutMs}ms`);
463
+ (timeoutError as NodeJS.ErrnoException).code = 'WORKER_TIMEOUT';
464
+
465
+ reject(timeoutError);
466
+ void this.restart('request_timeout');
467
+ }, this.options.requestTimeoutMs);
468
+
469
+ const inflight: ExecuteInflightRequest = {
470
+ resolve,
471
+ reject,
472
+ timer,
473
+ };
474
+ this.inflight.set(id, inflight);
475
+
476
+ const message: protocol.ExecuteMessage = {
477
+ type: 'execute',
478
+ id,
479
+ payload,
480
+ };
481
+
482
+ const transfer: ArrayBuffer[] = [];
483
+ if (payload.body !== undefined && payload.body.byteLength > 0) {
484
+ transfer.push(payload.body.buffer as ArrayBuffer);
485
+ }
486
+
487
+ if (transfer.length > 0) {
488
+ try {
489
+ worker.postMessage(message, transfer);
490
+ return;
491
+ } catch {
492
+ worker.postMessage(message);
493
+ return;
494
+ }
495
+ }
496
+
497
+ worker.postMessage(message);
498
+ });
499
+ }
500
+
501
+ /**
502
+ * Starts worker on demand and wires supervisor hooks.
503
+ */
504
+ private ensureWorker(): Worker {
505
+ if (this.worker !== undefined) {
506
+ return this.worker;
507
+ }
508
+
509
+ const worker = new Worker(this.workerUrl, {
510
+ workerData: {
511
+ memorySampleIntervalMs: this.options.memorySampleIntervalMs,
512
+ maxResponseBytes: this.options.maxResponseBytes,
513
+ workerId: this.meta.id,
514
+ dbSet: this.meta.dbSet,
515
+ },
516
+ resourceLimits: {
517
+ maxOldGenerationSizeMb: this.options.maxOldGenerationSizeMb,
518
+ maxYoungGenerationSizeMb: this.options.maxYoungGenerationSizeMb,
519
+ stackSizeMb: this.options.stackSizeMb,
520
+ },
521
+ });
522
+
523
+ worker.unref();
524
+
525
+ worker.on('message', (message: protocol.OutboundMessage) => {
526
+ this.handleWorkerMessage(message);
527
+ });
528
+
529
+ worker.once('error', (error) => {
530
+ this.logger.write('ERROR', 'RuntimeWorkerError', {
531
+ workerId: this.meta.id,
532
+ error: getErrorMessage(error),
533
+ });
534
+
535
+ this.rejectInflight(new Error(`runtime worker error: ${getErrorMessage(error)}`));
536
+ this.worker = undefined;
537
+
538
+ if (!this.closed) {
539
+ void this.restart('worker_error');
540
+ }
541
+ });
542
+
543
+ worker.once('exit', (code) => {
544
+ const currentWorker = this.worker;
545
+ if (currentWorker !== worker) {
546
+ return;
547
+ }
548
+
549
+ this.worker = undefined;
550
+
551
+ if (this.closed) {
552
+ return;
553
+ }
554
+
555
+ const exitError = new Error(`runtime worker exited with code ${code}`);
556
+ this.rejectInflight(exitError);
557
+ void this.restart('worker_exit');
558
+ });
559
+
560
+ this.worker = worker;
561
+ this.logger.write('INFO', 'RuntimeWorkerStarted', {
562
+ workerId: this.meta.id,
563
+ dbSet: this.meta.dbSet,
564
+ maxOldGenerationSizeMb: this.options.maxOldGenerationSizeMb,
565
+ maxYoungGenerationSizeMb: this.options.maxYoungGenerationSizeMb,
566
+ stackSizeMb: this.options.stackSizeMb,
567
+ maxResponseBytes: this.options.maxResponseBytes,
568
+ });
569
+ return worker;
570
+ }
571
+
572
+ /**
573
+ * Dispatches worker messages to result handlers.
574
+ */
575
+ private handleWorkerMessage(message: protocol.OutboundMessage): void {
576
+ if (message.type === 'memory') {
577
+ this.handleMemoryMessage(message);
578
+ return;
579
+ }
580
+
581
+ const inflight = this.inflight.get(message.id);
582
+ if (inflight === undefined) {
583
+ return;
584
+ }
585
+
586
+ this.inflight.delete(message.id);
587
+ clearTimeout(inflight.timer);
588
+
589
+ if (message.type === 'result') {
590
+ if (!message.ok) {
591
+ inflight.reject(new WorkerRuntimeError(message.error ?? { name: 'Error', message: 'Unknown worker error' }));
592
+ return;
593
+ }
594
+
595
+ if (message.response === undefined) {
596
+ inflight.reject(new Error('runtime worker missing response payload'));
597
+ return;
598
+ }
599
+
600
+ inflight.resolve({
601
+ response: message.response,
602
+ });
603
+ }
604
+ }
605
+
606
+ /**
607
+ * Updates memory sample and enforces soft/hard thresholds.
608
+ */
609
+ private handleMemoryMessage(message: protocol.MemoryMessage): void {
610
+ this.latestMemory = message;
611
+ this.latestMemoryAt = Date.now();
612
+
613
+ const softLimitBytes = this.options.memorySoftLimitMb * 1024 * 1024;
614
+ const hardLimitBytes = this.options.memoryHardLimitMb * 1024 * 1024;
615
+
616
+ if (message.heapUsed >= hardLimitBytes) {
617
+ this.logger.write('WARN', 'RuntimeWorkerMemoryHardLimit', {
618
+ workerId: this.meta.id,
619
+ heapUsed: message.heapUsed,
620
+ hardLimitBytes,
621
+ });
622
+ void this.restart('memory_hard_limit');
623
+ return;
624
+ }
625
+
626
+ if (message.heapUsed >= softLimitBytes && this.inflight.size === 0) {
627
+ this.logger.write('WARN', 'RuntimeWorkerMemorySoftLimit', {
628
+ workerId: this.meta.id,
629
+ heapUsed: message.heapUsed,
630
+ softLimitBytes,
631
+ });
632
+ void this.restart('memory_soft_limit');
633
+ }
634
+ }
635
+
636
+ /**
637
+ * Serializes restart operations to avoid duplicate restarts.
638
+ */
639
+ private async restart(reason: string): Promise<void> {
640
+ if (this.closed) {
641
+ return;
642
+ }
643
+
644
+ if (this.restarting !== undefined) {
645
+ await this.restarting;
646
+ return;
647
+ }
648
+
649
+ this.restarting = this.performRestart(reason).finally(() => {
650
+ this.restarting = undefined;
651
+ });
652
+
653
+ await this.restarting;
654
+ }
655
+
656
+ /**
657
+ * Replaces current worker instance with a fresh one.
658
+ */
659
+ private async performRestart(reason: string): Promise<void> {
660
+ const worker = this.worker;
661
+ this.worker = undefined;
662
+ this.versionsByFilePath.clear();
663
+ this.restartCount += 1;
664
+ this.lastRestartReason = reason;
665
+ this.lastRestartAt = Date.now();
666
+
667
+ this.rejectInflight(new Error(`runtime worker restarted: ${reason}`));
668
+
669
+ if (worker !== undefined) {
670
+ try {
671
+ await worker.terminate();
672
+ } catch (error) {
673
+ this.logger.write('WARN', 'RuntimeWorkerRestartTerminateFailed', {
674
+ workerId: this.meta.id,
675
+ reason,
676
+ error: getErrorMessage(error),
677
+ });
678
+ }
679
+ }
680
+
681
+ if (this.closed) {
682
+ return;
683
+ }
684
+
685
+ this.ensureWorker();
686
+ this.logger.write('WARN', 'RuntimeWorkerRestarted', {
687
+ workerId: this.meta.id,
688
+ reason,
689
+ });
690
+ }
691
+
692
+ /**
693
+ * Rejects all currently pending requests.
694
+ */
695
+ private rejectInflight(error: Error): void {
696
+ const requests = Array.from(this.inflight.values());
697
+ this.inflight.clear();
698
+
699
+ for (let i = 0; i < requests.length; i++) {
700
+ const request = requests[i];
701
+ clearTimeout(request.timer);
702
+ request.reject(error);
703
+ }
704
+ }
705
+
706
+ /**
707
+ * Computes pool status for snapshots.
708
+ */
709
+ private getStatus(): HandlerWorkerSnapshot['status'] {
710
+ if (this.closed) {
711
+ return 'closed';
712
+ }
713
+
714
+ if (this.restarting !== undefined) {
715
+ return 'restarting';
716
+ }
717
+
718
+ if (this.worker === undefined) {
719
+ return 'stopped';
720
+ }
721
+
722
+ return 'running';
723
+ }
724
+
725
+ /**
726
+ * Throws when pool cannot accept new work.
727
+ */
728
+ private assertExecutable(): void {
729
+ if (this.closed) {
730
+ throw new Error('runtime worker is closed');
731
+ }
732
+ }
733
+
734
+ /**
735
+ * Enforces version coherence within one worker lifecycle.
736
+ */
737
+ private assertKnownVersion(filePath: string, version: string): void {
738
+ const knownVersion = this.versionsByFilePath.get(filePath);
739
+ if (knownVersion === undefined || knownVersion === version) {
740
+ return;
741
+ }
742
+
743
+ const mismatchError = new Error(`handler version changed: ${filePath}`);
744
+ (mismatchError as NodeJS.ErrnoException).code = 'WORKER_VERSION_MISMATCH';
745
+ throw mismatchError;
746
+ }
747
+
748
+ /**
749
+ * Generates unique request id.
750
+ */
751
+ private nextRequestId(): string {
752
+ return `${Date.now().toString(36)}-${(this.requestCounter++).toString(36)}`;
753
+ }
754
+ }