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.
- package/.oxlintrc.json +64 -0
- package/.prettierrc +6 -0
- package/AGENTS.md +3 -0
- package/Dockerfile +13 -0
- package/LICENSE +21 -0
- package/README.md +11 -0
- package/document/index.html +16 -0
- package/document/src/main.tsx +8 -0
- package/document/src/styles.css +321 -0
- package/document/src/view/App.tsx +304 -0
- package/document/src/view/CodeBlock.tsx +11 -0
- package/document/src/view/Section.tsx +18 -0
- package/document/vite.config.ts +23 -0
- package/draft/vibe.md +50 -0
- package/package.json +66 -0
- package/rollup.config.mjs +102 -0
- package/scripts/build-image.ts +13 -0
- package/scripts/bump-version.ts +12 -0
- package/scripts/configs.ts +79 -0
- package/scripts/lines.ts +54 -0
- package/scripts/publish.ts +6 -0
- package/src/common/consts.ts +30 -0
- package/src/common/dtm.ts +10 -0
- package/src/common/logger.ts +145 -0
- package/src/core/meta-api.ts +48 -0
- package/src/core/server.ts +447 -0
- package/src/core/types.d.ts +6 -0
- package/src/core/utils/headers.ts +34 -0
- package/src/core/utils/request.ts +145 -0
- package/src/core/utils/send-json.ts +21 -0
- package/src/index.ts +11 -0
- package/src/workers/file-runtime.ts +1071 -0
- package/src/workers/handler-worker-pool.ts +754 -0
- package/src/workers/handler-worker.ts +1029 -0
- package/src/workers/options.ts +77 -0
- package/src/workers/protocol.d.ts +186 -0
- package/tests/core/dynamic-directory.test.ts +48 -0
- package/tests/core/file-runtime.test.ts +347 -0
- package/tests/core/server-options.test.ts +204 -0
- package/tests/e2e/fluxion-server.e2e-spec.ts +225 -0
- package/tests/helpers/test-utils.ts +81 -0
- package/tsconfig.json +22 -0
- package/vitest.config.ts +24 -0
|
@@ -0,0 +1,1029 @@
|
|
|
1
|
+
import type http from 'node:http';
|
|
2
|
+
import { pathToFileURL } from 'node:url';
|
|
3
|
+
import { parentPort, workerData } from 'node:worker_threads';
|
|
4
|
+
import { Readable, Writable } from 'node:stream';
|
|
5
|
+
|
|
6
|
+
import type { protocol } from './protocol.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Worker bootstrap data injected by main thread.
|
|
10
|
+
*/
|
|
11
|
+
interface WorkerBootstrapData {
|
|
12
|
+
/**
|
|
13
|
+
* Memory telemetry interval in milliseconds.
|
|
14
|
+
*/
|
|
15
|
+
memorySampleIntervalMs?: number;
|
|
16
|
+
/**
|
|
17
|
+
* ! Maximum response payload size allowed for one request.
|
|
18
|
+
*/
|
|
19
|
+
maxResponseBytes?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Stable worker id.
|
|
22
|
+
*/
|
|
23
|
+
workerId?: string;
|
|
24
|
+
/**
|
|
25
|
+
* Database names available in this worker.
|
|
26
|
+
*/
|
|
27
|
+
dbSet?: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Worker-local connected module entry.
|
|
32
|
+
*/
|
|
33
|
+
interface WorkerModuleConnection {
|
|
34
|
+
/**
|
|
35
|
+
* Value injected into handler context.
|
|
36
|
+
*/
|
|
37
|
+
value: unknown;
|
|
38
|
+
/**
|
|
39
|
+
* Stable module signature used for conflict detection.
|
|
40
|
+
*/
|
|
41
|
+
signature: string;
|
|
42
|
+
/**
|
|
43
|
+
* Optional teardown hook for worker shutdown.
|
|
44
|
+
*/
|
|
45
|
+
close?(): Promise<void>;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Module declaration parsed from handler export.
|
|
50
|
+
*/
|
|
51
|
+
interface ParsedHandlerModuleDeclaration {
|
|
52
|
+
/**
|
|
53
|
+
* Module id used by dynamic import.
|
|
54
|
+
*/
|
|
55
|
+
module: string;
|
|
56
|
+
/**
|
|
57
|
+
* Context key where the created value will be injected.
|
|
58
|
+
*/
|
|
59
|
+
injectKey: string;
|
|
60
|
+
/**
|
|
61
|
+
* Serialized factory function source.
|
|
62
|
+
*/
|
|
63
|
+
factorySource: string;
|
|
64
|
+
/**
|
|
65
|
+
* Optional factory options.
|
|
66
|
+
*/
|
|
67
|
+
options?: unknown;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Third argument passed to dynamic handlers.
|
|
72
|
+
*/
|
|
73
|
+
interface HandlerContext {
|
|
74
|
+
/**
|
|
75
|
+
* Worker identity and capability snapshot.
|
|
76
|
+
*/
|
|
77
|
+
worker: {
|
|
78
|
+
/**
|
|
79
|
+
* Worker id.
|
|
80
|
+
*/
|
|
81
|
+
id: string;
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* Runtime can inject extra module instances by `injectKey`.
|
|
85
|
+
*/
|
|
86
|
+
[key: string]: unknown;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Handler function signature exported by dynamic modules.
|
|
91
|
+
*/
|
|
92
|
+
type ModuleDefaultHandler = (
|
|
93
|
+
req: http.IncomingMessage,
|
|
94
|
+
res: http.ServerResponse,
|
|
95
|
+
context: HandlerContext,
|
|
96
|
+
) => unknown;
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Object-style default export for handler metadata.
|
|
100
|
+
*/
|
|
101
|
+
interface ModuleDefaultHandlerObject {
|
|
102
|
+
/**
|
|
103
|
+
* Runtime handler function.
|
|
104
|
+
*/
|
|
105
|
+
handler: ModuleDefaultHandler;
|
|
106
|
+
/**
|
|
107
|
+
* Dynamic module injection declarations.
|
|
108
|
+
*/
|
|
109
|
+
modules?: unknown;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Parsed handler module export.
|
|
114
|
+
*/
|
|
115
|
+
interface ParsedModuleDefault {
|
|
116
|
+
/**
|
|
117
|
+
* Runtime handler function.
|
|
118
|
+
*/
|
|
119
|
+
handler: ModuleDefaultHandler;
|
|
120
|
+
/**
|
|
121
|
+
* Dynamic module declarations.
|
|
122
|
+
*/
|
|
123
|
+
modules: ParsedHandlerModuleDeclaration[];
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Cached handler entry inside one worker lifecycle.
|
|
128
|
+
*/
|
|
129
|
+
interface HandlerCacheEntry extends ParsedModuleDefault {
|
|
130
|
+
/**
|
|
131
|
+
* Version token used by main thread.
|
|
132
|
+
*/
|
|
133
|
+
version: string;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Worker-local handler cache.
|
|
138
|
+
*/
|
|
139
|
+
const handlerCache = new Map<string, HandlerCacheEntry>();
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* Runtime bootstrap options resolved from workerData.
|
|
143
|
+
*/
|
|
144
|
+
const bootstrapData = workerData as WorkerBootstrapData | undefined;
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Memory report interval provided by main thread.
|
|
148
|
+
*/
|
|
149
|
+
const memorySampleIntervalMs =
|
|
150
|
+
typeof bootstrapData?.memorySampleIntervalMs === 'number' && bootstrapData.memorySampleIntervalMs > 0
|
|
151
|
+
? Math.floor(bootstrapData.memorySampleIntervalMs)
|
|
152
|
+
: 5000;
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* ! Maximum response size for a single handler execution.
|
|
156
|
+
*/
|
|
157
|
+
const maxResponseBytes =
|
|
158
|
+
typeof bootstrapData?.maxResponseBytes === 'number' && bootstrapData.maxResponseBytes > 0
|
|
159
|
+
? Math.floor(bootstrapData.maxResponseBytes)
|
|
160
|
+
: 2 * 1024 * 1024;
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Worker identity string used in handler context.
|
|
164
|
+
*/
|
|
165
|
+
const workerId = typeof bootstrapData?.workerId === 'string' ? bootstrapData.workerId : 'runtime-worker';
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Database capability set available in this worker.
|
|
169
|
+
*/
|
|
170
|
+
const workerDbSet = normalizeDbList(bootstrapData?.dbSet);
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* Worker-local module connection registry.
|
|
174
|
+
*/
|
|
175
|
+
const workerModuleConnections = new Map<string, WorkerModuleConnection>();
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Converts unknown errors to protocol error payload.
|
|
179
|
+
*/
|
|
180
|
+
function toWorkerError(error: unknown): protocol.SerializedError {
|
|
181
|
+
const err = error as NodeJS.ErrnoException;
|
|
182
|
+
const isError = error instanceof Error;
|
|
183
|
+
|
|
184
|
+
return {
|
|
185
|
+
name: isError ? error.name : 'Error',
|
|
186
|
+
message: isError ? error.message : String(error),
|
|
187
|
+
stack: isError ? error.stack : undefined,
|
|
188
|
+
code: typeof err.code === 'string' ? err.code : undefined,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Normalizes database names from metadata/config input.
|
|
194
|
+
*/
|
|
195
|
+
function normalizeDbList(input: unknown): string[] {
|
|
196
|
+
if (typeof input === 'string') {
|
|
197
|
+
const normalized = input.trim();
|
|
198
|
+
return normalized.length === 0 ? [] : [normalized];
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
if (!Array.isArray(input)) {
|
|
202
|
+
return [];
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
const seen = new Set<string>();
|
|
206
|
+
const normalized: string[] = [];
|
|
207
|
+
|
|
208
|
+
for (let i = 0; i < input.length; i++) {
|
|
209
|
+
const item = input[i];
|
|
210
|
+
if (typeof item !== 'string') {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const name = item.trim();
|
|
215
|
+
if (name.length === 0 || seen.has(name)) {
|
|
216
|
+
continue;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
seen.add(name);
|
|
220
|
+
normalized.push(name);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
normalized.sort((left, right) => left.localeCompare(right));
|
|
224
|
+
return normalized;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Runtime shape guard for plain object values.
|
|
229
|
+
*/
|
|
230
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
231
|
+
return typeof value === 'object' && value !== null;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Converts values into stable JSON-compatible structure for signature generation.
|
|
236
|
+
*/
|
|
237
|
+
function toStableJsonValue(value: unknown): unknown {
|
|
238
|
+
if (value instanceof Date) {
|
|
239
|
+
return value.toISOString();
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (Array.isArray(value)) {
|
|
243
|
+
return value.map((item) => toStableJsonValue(item));
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
if (isRecord(value)) {
|
|
247
|
+
const keys = Object.keys(value).sort((left, right) => left.localeCompare(right));
|
|
248
|
+
const normalized: Record<string, unknown> = Object.create(null) as Record<string, unknown>;
|
|
249
|
+
|
|
250
|
+
for (let i = 0; i < keys.length; i++) {
|
|
251
|
+
const key = keys[i];
|
|
252
|
+
const child = toStableJsonValue(value[key]);
|
|
253
|
+
if (child !== undefined) {
|
|
254
|
+
normalized[key] = child;
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return normalized;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
if (typeof value === 'bigint') {
|
|
262
|
+
return value.toString();
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (typeof value === 'function' || typeof value === 'symbol' || value === undefined) {
|
|
266
|
+
return undefined;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* Builds stable signature string for module conflict detection.
|
|
274
|
+
*/
|
|
275
|
+
function toModuleSignature(definition: ParsedHandlerModuleDeclaration): string {
|
|
276
|
+
return JSON.stringify({
|
|
277
|
+
module: definition.module,
|
|
278
|
+
injectKey: definition.injectKey,
|
|
279
|
+
factorySource: definition.factorySource,
|
|
280
|
+
options: toStableJsonValue(definition.options),
|
|
281
|
+
});
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Validates module declaration list from handler export.
|
|
286
|
+
*/
|
|
287
|
+
function parseHandlerModuleDeclarations(input: unknown, filePath: string): ParsedHandlerModuleDeclaration[] {
|
|
288
|
+
if (input === undefined) {
|
|
289
|
+
return [];
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
if (!Array.isArray(input)) {
|
|
293
|
+
throw new TypeError(`Invalid modules declaration in ${filePath}: modules must be an array`);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
const declarations: ParsedHandlerModuleDeclaration[] = [];
|
|
297
|
+
const seenInjectKeys = new Set<string>();
|
|
298
|
+
|
|
299
|
+
for (let i = 0; i < input.length; i++) {
|
|
300
|
+
const item = input[i];
|
|
301
|
+
if (!isRecord(item)) {
|
|
302
|
+
throw new TypeError(`Invalid modules[${i}] in ${filePath}: expected object`);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
const rawModule = typeof item.module === 'string' ? item.module.trim() : '';
|
|
306
|
+
if (rawModule.length === 0) {
|
|
307
|
+
throw new TypeError(`Invalid modules[${i}] in ${filePath}: module is required`);
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
const rawInjectKey = typeof item.injectKey === 'string' ? item.injectKey.trim() : '';
|
|
311
|
+
if (rawInjectKey.length === 0) {
|
|
312
|
+
throw new TypeError(`Invalid modules[${i}] in ${filePath}: injectKey is required`);
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
if (rawInjectKey === 'worker') {
|
|
316
|
+
throw new TypeError(`Invalid modules[${i}] in ${filePath}: injectKey "${rawInjectKey}" is reserved`);
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
if (seenInjectKeys.has(rawInjectKey)) {
|
|
320
|
+
throw new TypeError(`Duplicate injectKey "${rawInjectKey}" in ${filePath}`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
const factory = item.factory;
|
|
324
|
+
if (typeof factory !== 'function') {
|
|
325
|
+
throw new TypeError(`Invalid modules[${i}] in ${filePath}: factory must be a function`);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
seenInjectKeys.add(rawInjectKey);
|
|
329
|
+
declarations.push({
|
|
330
|
+
module: rawModule,
|
|
331
|
+
injectKey: rawInjectKey,
|
|
332
|
+
factorySource: factory.toString(),
|
|
333
|
+
options: item.options,
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return declarations;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* Re-hydrates a serialized module factory source.
|
|
342
|
+
*/
|
|
343
|
+
function createModuleFactory(
|
|
344
|
+
factorySource: string,
|
|
345
|
+
filePath: string,
|
|
346
|
+
injectKey: string,
|
|
347
|
+
): (...args: unknown[]) => unknown {
|
|
348
|
+
let factory: unknown;
|
|
349
|
+
const sourceCandidates = [factorySource];
|
|
350
|
+
|
|
351
|
+
const methodPattern = /^(async\s+)?[A-Za-z_$][A-Za-z0-9_$]*\s*\(/;
|
|
352
|
+
if (methodPattern.test(factorySource)) {
|
|
353
|
+
const asyncPrefix = factorySource.startsWith('async ') ? 'async ' : '';
|
|
354
|
+
const parenIndex = factorySource.indexOf('(');
|
|
355
|
+
sourceCandidates.push(`${asyncPrefix}function ${factorySource.slice(parenIndex)}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
let restoreError: unknown;
|
|
359
|
+
for (let i = 0; i < sourceCandidates.length; i++) {
|
|
360
|
+
try {
|
|
361
|
+
factory = new Function(`return (${sourceCandidates[i]});`)();
|
|
362
|
+
restoreError = undefined;
|
|
363
|
+
break;
|
|
364
|
+
} catch (error) {
|
|
365
|
+
restoreError = error;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
if (factory === undefined || restoreError !== undefined) {
|
|
370
|
+
const reviveError = new Error(
|
|
371
|
+
`Failed to revive module factory for "${injectKey}" in ${filePath}: ${
|
|
372
|
+
restoreError instanceof Error ? restoreError.message : String(restoreError)
|
|
373
|
+
}`,
|
|
374
|
+
);
|
|
375
|
+
(reviveError as NodeJS.ErrnoException).code = 'WORKER_MODULE_FACTORY_RESTORE_FAILED';
|
|
376
|
+
throw reviveError;
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
if (typeof factory !== 'function') {
|
|
380
|
+
const typeError = new Error(`Module factory for "${injectKey}" in ${filePath} did not restore to a function`);
|
|
381
|
+
(typeError as NodeJS.ErrnoException).code = 'WORKER_MODULE_FACTORY_RESTORE_FAILED';
|
|
382
|
+
throw typeError;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return factory as (...args: unknown[]) => unknown;
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
/**
|
|
389
|
+
* Resolves close hook from one injected module value.
|
|
390
|
+
*/
|
|
391
|
+
function resolveModuleCloseHook(value: unknown): (() => Promise<void>) | undefined {
|
|
392
|
+
if (!isRecord(value)) {
|
|
393
|
+
return undefined;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
const dispose = value['dispose'];
|
|
397
|
+
if (typeof dispose === 'function') {
|
|
398
|
+
return async (): Promise<void> => {
|
|
399
|
+
await dispose.call(value);
|
|
400
|
+
};
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const close = value['close'];
|
|
404
|
+
if (typeof close === 'function') {
|
|
405
|
+
return async (): Promise<void> => {
|
|
406
|
+
await close.call(value);
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const end = value['end'];
|
|
411
|
+
if (typeof end === 'function') {
|
|
412
|
+
return async (): Promise<void> => {
|
|
413
|
+
await end.call(value);
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const destroy = value['destroy'];
|
|
418
|
+
if (typeof destroy === 'function') {
|
|
419
|
+
return async (): Promise<void> => {
|
|
420
|
+
await destroy.call(value);
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
return undefined;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Creates one worker-local module injection value.
|
|
429
|
+
*/
|
|
430
|
+
async function createModuleConnection(
|
|
431
|
+
definition: ParsedHandlerModuleDeclaration,
|
|
432
|
+
filePath: string,
|
|
433
|
+
): Promise<WorkerModuleConnection> {
|
|
434
|
+
let importedModule: unknown;
|
|
435
|
+
|
|
436
|
+
try {
|
|
437
|
+
importedModule = await import(definition.module);
|
|
438
|
+
} catch (error) {
|
|
439
|
+
const importError = new Error(
|
|
440
|
+
`Failed to import module "${definition.module}" for "${definition.injectKey}" in ${filePath}: ${
|
|
441
|
+
error instanceof Error ? error.message : String(error)
|
|
442
|
+
}`,
|
|
443
|
+
);
|
|
444
|
+
(importError as NodeJS.ErrnoException).code = 'WORKER_MODULE_IMPORT_FAILED';
|
|
445
|
+
throw importError;
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
const factory = createModuleFactory(definition.factorySource, filePath, definition.injectKey);
|
|
449
|
+
|
|
450
|
+
let createdValue: unknown;
|
|
451
|
+
try {
|
|
452
|
+
createdValue = factory(importedModule, definition.options, {
|
|
453
|
+
filePath,
|
|
454
|
+
workerId,
|
|
455
|
+
workerDbSet: [...workerDbSet],
|
|
456
|
+
injectKey: definition.injectKey,
|
|
457
|
+
moduleId: definition.module,
|
|
458
|
+
});
|
|
459
|
+
} catch (error) {
|
|
460
|
+
const factoryError = new Error(
|
|
461
|
+
`Module factory threw for "${definition.injectKey}" in ${filePath}: ${
|
|
462
|
+
error instanceof Error ? error.message : String(error)
|
|
463
|
+
}`,
|
|
464
|
+
);
|
|
465
|
+
(factoryError as NodeJS.ErrnoException).code = 'WORKER_MODULE_FACTORY_FAILED';
|
|
466
|
+
throw factoryError;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (isPromiseLike(createdValue)) {
|
|
470
|
+
createdValue = await createdValue;
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
return {
|
|
474
|
+
value: createdValue,
|
|
475
|
+
signature: toModuleSignature(definition),
|
|
476
|
+
close: resolveModuleCloseHook(createdValue),
|
|
477
|
+
};
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Ensures worker has initialized modules declared by current handler.
|
|
482
|
+
*/
|
|
483
|
+
async function ensureWorkerModuleConnections(
|
|
484
|
+
filePath: string,
|
|
485
|
+
definitions: readonly ParsedHandlerModuleDeclaration[],
|
|
486
|
+
): Promise<void> {
|
|
487
|
+
for (let i = 0; i < definitions.length; i++) {
|
|
488
|
+
const definition = definitions[i];
|
|
489
|
+
const signature = toModuleSignature(definition);
|
|
490
|
+
const existing = workerModuleConnections.get(definition.injectKey);
|
|
491
|
+
|
|
492
|
+
if (existing !== undefined) {
|
|
493
|
+
if (existing.signature !== signature) {
|
|
494
|
+
const conflictError = new Error(
|
|
495
|
+
`Conflicting module declaration for injectKey "${definition.injectKey}" in worker "${workerId}": ${filePath}`,
|
|
496
|
+
);
|
|
497
|
+
(conflictError as NodeJS.ErrnoException).code = 'WORKER_MODULE_CONFLICT';
|
|
498
|
+
throw conflictError;
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
const connection = await createModuleConnection(definition, filePath);
|
|
505
|
+
workerModuleConnections.set(definition.injectKey, connection);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
/**
|
|
510
|
+
* Closes all worker-local module connections.
|
|
511
|
+
*/
|
|
512
|
+
async function closeWorkerModuleConnections(): Promise<void> {
|
|
513
|
+
const connections = Array.from(workerModuleConnections.values());
|
|
514
|
+
workerModuleConnections.clear();
|
|
515
|
+
|
|
516
|
+
for (let i = 0; i < connections.length; i++) {
|
|
517
|
+
const close = connections[i].close;
|
|
518
|
+
if (close === undefined) {
|
|
519
|
+
continue;
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
try {
|
|
523
|
+
await close();
|
|
524
|
+
} catch {
|
|
525
|
+
// ignore close error during worker teardown
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Runtime guard for promise-like handler return values.
|
|
532
|
+
*/
|
|
533
|
+
function isPromiseLike(value: unknown): value is PromiseLike<unknown> {
|
|
534
|
+
return (
|
|
535
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
536
|
+
value !== null &&
|
|
537
|
+
typeof (value as PromiseLike<unknown>).then === 'function'
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Parses default export into handler + metadata shape.
|
|
543
|
+
*/
|
|
544
|
+
function parseModuleDefault(defaultExport: unknown, filePath: string): ParsedModuleDefault {
|
|
545
|
+
if (typeof defaultExport === 'function') {
|
|
546
|
+
return {
|
|
547
|
+
handler: defaultExport as ModuleDefaultHandler,
|
|
548
|
+
modules: [],
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
if (isRecord(defaultExport)) {
|
|
553
|
+
const objectExport = defaultExport as Partial<ModuleDefaultHandlerObject>;
|
|
554
|
+
if (typeof objectExport.handler !== 'function') {
|
|
555
|
+
throw new TypeError(
|
|
556
|
+
`Default export must be a function or { handler, modules? }: ${filePath}`,
|
|
557
|
+
);
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
if ('db' in defaultExport && (defaultExport as { db?: unknown }).db !== undefined) {
|
|
561
|
+
throw new TypeError(`Legacy db declaration is no longer supported in ${filePath}: use modules only`);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
return {
|
|
565
|
+
handler: objectExport.handler,
|
|
566
|
+
modules: parseHandlerModuleDeclarations(objectExport.modules, filePath),
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
throw new TypeError(
|
|
571
|
+
`Default export must be a function or { handler, modules? }: ${filePath}`,
|
|
572
|
+
);
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Builds request context passed as third arg to handler.
|
|
577
|
+
*/
|
|
578
|
+
function createHandlerContext(modules: readonly ParsedHandlerModuleDeclaration[]): HandlerContext {
|
|
579
|
+
const context: HandlerContext = {
|
|
580
|
+
worker: {
|
|
581
|
+
id: workerId,
|
|
582
|
+
},
|
|
583
|
+
};
|
|
584
|
+
|
|
585
|
+
for (let i = 0; i < modules.length; i++) {
|
|
586
|
+
const moduleDeclaration = modules[i];
|
|
587
|
+
const connection = workerModuleConnections.get(moduleDeclaration.injectKey);
|
|
588
|
+
if (connection !== undefined) {
|
|
589
|
+
context[moduleDeclaration.injectKey] = connection.value;
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
return context;
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
/**
|
|
597
|
+
* In-memory ServerResponse used to run handlers without socket access.
|
|
598
|
+
* & This lets existing `(req, res)` handlers run unchanged inside worker.
|
|
599
|
+
*/
|
|
600
|
+
class MemoryServerResponse extends Writable {
|
|
601
|
+
/**
|
|
602
|
+
* HTTP status code.
|
|
603
|
+
*/
|
|
604
|
+
public statusCode = 200;
|
|
605
|
+
|
|
606
|
+
/**
|
|
607
|
+
* Optional status text.
|
|
608
|
+
*/
|
|
609
|
+
public statusMessage = '';
|
|
610
|
+
|
|
611
|
+
/**
|
|
612
|
+
* ! Maximum response body bytes allowed.
|
|
613
|
+
*/
|
|
614
|
+
private readonly maxBodyBytes: number;
|
|
615
|
+
|
|
616
|
+
/**
|
|
617
|
+
* Response headers map (lowercased keys).
|
|
618
|
+
*/
|
|
619
|
+
private readonly headerMap = new Map<string, string>();
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* Buffered body chunks.
|
|
623
|
+
*/
|
|
624
|
+
private readonly bodyChunks: Buffer[] = [];
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
* Total bytes currently buffered.
|
|
628
|
+
*/
|
|
629
|
+
private totalBodyBytes = 0;
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* @param maxBodyBytes Maximum body bytes before hard-failing.
|
|
633
|
+
*/
|
|
634
|
+
constructor(maxBodyBytes: number) {
|
|
635
|
+
super();
|
|
636
|
+
this.maxBodyBytes = maxBodyBytes;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/**
|
|
640
|
+
* Sets response header.
|
|
641
|
+
*/
|
|
642
|
+
setHeader(name: string, value: string | number | readonly string[]): this {
|
|
643
|
+
const normalizedName = name.toLowerCase();
|
|
644
|
+
|
|
645
|
+
if (Array.isArray(value)) {
|
|
646
|
+
this.headerMap.set(normalizedName, value.join(', '));
|
|
647
|
+
return this;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
this.headerMap.set(normalizedName, String(value));
|
|
651
|
+
return this;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* Gets response header.
|
|
656
|
+
*/
|
|
657
|
+
getHeader(name: string): string | undefined {
|
|
658
|
+
return this.headerMap.get(name.toLowerCase());
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* Returns all response headers.
|
|
663
|
+
*/
|
|
664
|
+
getHeaders(): Record<string, string> {
|
|
665
|
+
return Object.fromEntries(this.headerMap.entries());
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Removes response header.
|
|
670
|
+
*/
|
|
671
|
+
removeHeader(name: string): void {
|
|
672
|
+
this.headerMap.delete(name.toLowerCase());
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
/**
|
|
676
|
+
* Sets status and optional headers.
|
|
677
|
+
*/
|
|
678
|
+
writeHead(
|
|
679
|
+
statusCode: number,
|
|
680
|
+
statusMessageOrHeaders?: string | http.OutgoingHttpHeaders,
|
|
681
|
+
headers?: http.OutgoingHttpHeaders,
|
|
682
|
+
): this {
|
|
683
|
+
this.statusCode = statusCode;
|
|
684
|
+
|
|
685
|
+
if (typeof statusMessageOrHeaders === 'string') {
|
|
686
|
+
this.statusMessage = statusMessageOrHeaders;
|
|
687
|
+
this.applyHeaders(headers);
|
|
688
|
+
return this;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
this.applyHeaders(statusMessageOrHeaders);
|
|
692
|
+
return this;
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
/**
|
|
696
|
+
* Ends response stream and stores optional final chunk.
|
|
697
|
+
*/
|
|
698
|
+
override end(chunk?: unknown, encoding?: BufferEncoding | (() => void), cb?: () => void): this {
|
|
699
|
+
const resolvedCallback = typeof encoding === 'function' ? encoding : cb;
|
|
700
|
+
const resolvedEncoding = typeof encoding === 'string' ? encoding : undefined;
|
|
701
|
+
|
|
702
|
+
if (chunk !== undefined && chunk !== null) {
|
|
703
|
+
this.appendChunk(toBuffer(chunk, resolvedEncoding));
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
return super.end(resolvedCallback);
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
/**
|
|
710
|
+
* Serializes buffered response back to main thread.
|
|
711
|
+
*/
|
|
712
|
+
toSerializedResponse(): protocol.SerializedResponse {
|
|
713
|
+
if (this.bodyChunks.length === 0) {
|
|
714
|
+
return {
|
|
715
|
+
statusCode: this.statusCode,
|
|
716
|
+
headers: this.getHeaders(),
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
const body = new Uint8Array(this.totalBodyBytes);
|
|
721
|
+
let offset = 0;
|
|
722
|
+
|
|
723
|
+
for (let i = 0; i < this.bodyChunks.length; i++) {
|
|
724
|
+
const chunk = this.bodyChunks[i];
|
|
725
|
+
body.set(chunk, offset);
|
|
726
|
+
offset += chunk.byteLength;
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
return {
|
|
730
|
+
statusCode: this.statusCode,
|
|
731
|
+
headers: this.getHeaders(),
|
|
732
|
+
body,
|
|
733
|
+
};
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
/**
|
|
737
|
+
* Writable sink used by `res.write`.
|
|
738
|
+
*/
|
|
739
|
+
override _write(
|
|
740
|
+
chunk: Buffer | string | Uint8Array,
|
|
741
|
+
encoding: BufferEncoding,
|
|
742
|
+
callback: (error?: Error | null) => void,
|
|
743
|
+
): void {
|
|
744
|
+
try {
|
|
745
|
+
this.appendChunk(toBuffer(chunk, encoding));
|
|
746
|
+
callback();
|
|
747
|
+
} catch (error) {
|
|
748
|
+
callback(error as Error);
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Applies OutgoingHttpHeaders into internal map.
|
|
754
|
+
*/
|
|
755
|
+
private applyHeaders(headers?: http.OutgoingHttpHeaders): void {
|
|
756
|
+
if (headers === undefined) {
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const headerKeys = Object.keys(headers);
|
|
761
|
+
for (let i = 0; i < headerKeys.length; i++) {
|
|
762
|
+
const key = headerKeys[i];
|
|
763
|
+
const value = headers[key];
|
|
764
|
+
if (value === undefined) {
|
|
765
|
+
continue;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
if (Array.isArray(value)) {
|
|
769
|
+
this.setHeader(
|
|
770
|
+
key,
|
|
771
|
+
value.map((item) => String(item)),
|
|
772
|
+
);
|
|
773
|
+
continue;
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
this.setHeader(key, String(value));
|
|
777
|
+
}
|
|
778
|
+
}
|
|
779
|
+
|
|
780
|
+
/**
|
|
781
|
+
* ! Appends one response chunk and enforces body size cap.
|
|
782
|
+
*/
|
|
783
|
+
private appendChunk(chunk: Buffer): void {
|
|
784
|
+
if (chunk.byteLength === 0) {
|
|
785
|
+
return;
|
|
786
|
+
}
|
|
787
|
+
|
|
788
|
+
const nextTotalBytes = this.totalBodyBytes + chunk.byteLength;
|
|
789
|
+
if (nextTotalBytes > this.maxBodyBytes) {
|
|
790
|
+
const sizeError = new Error(
|
|
791
|
+
`worker response too large: ${nextTotalBytes} bytes exceeds ${this.maxBodyBytes} bytes`,
|
|
792
|
+
);
|
|
793
|
+
(sizeError as NodeJS.ErrnoException).code = 'WORKER_RESPONSE_TOO_LARGE';
|
|
794
|
+
throw sizeError;
|
|
795
|
+
}
|
|
796
|
+
|
|
797
|
+
this.totalBodyBytes = nextTotalBytes;
|
|
798
|
+
this.bodyChunks.push(chunk);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Normalizes write chunks into Buffer.
|
|
804
|
+
*/
|
|
805
|
+
function toBuffer(chunk: unknown, encoding?: BufferEncoding): Buffer {
|
|
806
|
+
if (Buffer.isBuffer(chunk)) {
|
|
807
|
+
return chunk;
|
|
808
|
+
}
|
|
809
|
+
|
|
810
|
+
if (chunk instanceof Uint8Array) {
|
|
811
|
+
return Buffer.from(chunk.buffer, chunk.byteOffset, chunk.byteLength);
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
if (typeof chunk === 'string') {
|
|
815
|
+
return Buffer.from(chunk, encoding);
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
return Buffer.from(String(chunk));
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
/**
|
|
822
|
+
* Serializes handler return value as JSON response body.
|
|
823
|
+
*/
|
|
824
|
+
function toJsonResponseBody(value: unknown): string {
|
|
825
|
+
const serialized = JSON.stringify(value);
|
|
826
|
+
return serialized === undefined ? 'null' : serialized;
|
|
827
|
+
}
|
|
828
|
+
|
|
829
|
+
/**
|
|
830
|
+
* Builds a synthetic IncomingMessage from protocol payload.
|
|
831
|
+
*/
|
|
832
|
+
function createIncomingRequest(payload: protocol.Payload): http.IncomingMessage {
|
|
833
|
+
const bodyChunk = payload.body;
|
|
834
|
+
const source = bodyChunk !== undefined && bodyChunk.byteLength > 0 ? [Buffer.from(bodyChunk)] : [];
|
|
835
|
+
const request = Readable.from(source) as unknown as http.IncomingMessage;
|
|
836
|
+
|
|
837
|
+
request.method = payload.method;
|
|
838
|
+
request.url = payload.url;
|
|
839
|
+
|
|
840
|
+
const headers: http.IncomingHttpHeaders = {};
|
|
841
|
+
const headerKeys = Object.keys(payload.headers);
|
|
842
|
+
for (let i = 0; i < headerKeys.length; i++) {
|
|
843
|
+
const key = headerKeys[i];
|
|
844
|
+
const value: protocol.HeaderValue = payload.headers[key];
|
|
845
|
+
headers[key] = Array.isArray(value) ? [...value] : value;
|
|
846
|
+
}
|
|
847
|
+
request.headers = headers;
|
|
848
|
+
|
|
849
|
+
const socketLike = {
|
|
850
|
+
remoteAddress: payload.ip,
|
|
851
|
+
};
|
|
852
|
+
|
|
853
|
+
Object.defineProperty(request, 'socket', {
|
|
854
|
+
value: socketLike,
|
|
855
|
+
configurable: true,
|
|
856
|
+
enumerable: true,
|
|
857
|
+
writable: true,
|
|
858
|
+
});
|
|
859
|
+
|
|
860
|
+
Object.defineProperty(request, 'connection', {
|
|
861
|
+
value: socketLike,
|
|
862
|
+
configurable: true,
|
|
863
|
+
enumerable: true,
|
|
864
|
+
writable: true,
|
|
865
|
+
});
|
|
866
|
+
|
|
867
|
+
return request;
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Loads handler module and resolves metadata.
|
|
872
|
+
* ! If version differs inside the same worker, supervisor must restart worker first.
|
|
873
|
+
*/
|
|
874
|
+
async function loadHandler(filePath: string, version: string): Promise<HandlerCacheEntry> {
|
|
875
|
+
const cached = handlerCache.get(filePath);
|
|
876
|
+
if (cached !== undefined) {
|
|
877
|
+
if (cached.version === version) {
|
|
878
|
+
return cached;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
const versionError = new Error(`Handler version changed in worker: ${filePath}`);
|
|
882
|
+
(versionError as NodeJS.ErrnoException).code = 'WORKER_VERSION_MISMATCH';
|
|
883
|
+
throw versionError;
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
const loaded = await import(pathToFileURL(filePath).href);
|
|
887
|
+
const parsed = parseModuleDefault(loaded.default as unknown, filePath);
|
|
888
|
+
|
|
889
|
+
await ensureWorkerModuleConnections(filePath, parsed.modules);
|
|
890
|
+
|
|
891
|
+
const entry: HandlerCacheEntry = {
|
|
892
|
+
handler: parsed.handler,
|
|
893
|
+
version,
|
|
894
|
+
modules: parsed.modules,
|
|
895
|
+
};
|
|
896
|
+
|
|
897
|
+
handlerCache.set(filePath, entry);
|
|
898
|
+
return entry;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/**
|
|
902
|
+
* Executes one request and returns worker result message.
|
|
903
|
+
*/
|
|
904
|
+
async function execute(message: protocol.ExecuteMessage): Promise<protocol.ResultMessage> {
|
|
905
|
+
const startedAt = Date.now();
|
|
906
|
+
const payload = message.payload;
|
|
907
|
+
|
|
908
|
+
try {
|
|
909
|
+
const entry = await loadHandler(payload.filePath, payload.version);
|
|
910
|
+
const request = createIncomingRequest(payload);
|
|
911
|
+
const response = new MemoryServerResponse(maxResponseBytes) as unknown as http.ServerResponse;
|
|
912
|
+
let execution = entry.handler(request, response, createHandlerContext(entry.modules));
|
|
913
|
+
|
|
914
|
+
if (isPromiseLike(execution)) {
|
|
915
|
+
execution = await execution;
|
|
916
|
+
}
|
|
917
|
+
|
|
918
|
+
const writableResponse = response as unknown as MemoryServerResponse;
|
|
919
|
+
if (execution !== undefined && !writableResponse.writableEnded) {
|
|
920
|
+
if (writableResponse.getHeader('content-type') === undefined) {
|
|
921
|
+
writableResponse.setHeader('content-type', 'application/json; charset=utf-8');
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
writableResponse.statusCode = 200;
|
|
925
|
+
writableResponse.end(toJsonResponseBody(execution));
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
if (!writableResponse.writableFinished) {
|
|
929
|
+
await new Promise<void>((resolve, reject) => {
|
|
930
|
+
writableResponse.once('finish', resolve);
|
|
931
|
+
writableResponse.once('error', reject);
|
|
932
|
+
});
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
return {
|
|
936
|
+
type: 'result',
|
|
937
|
+
id: message.id,
|
|
938
|
+
ok: true,
|
|
939
|
+
elapsedMs: Date.now() - startedAt,
|
|
940
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
941
|
+
response: writableResponse.toSerializedResponse(),
|
|
942
|
+
};
|
|
943
|
+
} catch (error) {
|
|
944
|
+
return {
|
|
945
|
+
type: 'result',
|
|
946
|
+
id: message.id,
|
|
947
|
+
ok: false,
|
|
948
|
+
elapsedMs: Date.now() - startedAt,
|
|
949
|
+
heapUsed: process.memoryUsage().heapUsed,
|
|
950
|
+
error: toWorkerError(error),
|
|
951
|
+
};
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/**
|
|
956
|
+
* ! Worker must run under parentPort; standalone run is invalid.
|
|
957
|
+
*/
|
|
958
|
+
if (parentPort === null) {
|
|
959
|
+
throw new Error('runtime worker missing parent port');
|
|
960
|
+
}
|
|
961
|
+
const port = parentPort;
|
|
962
|
+
|
|
963
|
+
/**
|
|
964
|
+
* Posts outbound message and transfers body buffer when present.
|
|
965
|
+
*/
|
|
966
|
+
function postOutboundMessage(message: protocol.OutboundMessage): void {
|
|
967
|
+
if (message.type !== 'result' || !message.ok || message.response?.body === undefined) {
|
|
968
|
+
port.postMessage(message);
|
|
969
|
+
return;
|
|
970
|
+
}
|
|
971
|
+
|
|
972
|
+
const body = message.response.body;
|
|
973
|
+
if (body.byteLength === 0) {
|
|
974
|
+
port.postMessage(message);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
|
|
978
|
+
port.postMessage(message, [body.buffer as ArrayBuffer]);
|
|
979
|
+
}
|
|
980
|
+
|
|
981
|
+
/**
|
|
982
|
+
* Periodically reports worker memory usage.
|
|
983
|
+
*/
|
|
984
|
+
const memoryReporter = setInterval(() => {
|
|
985
|
+
const usage = process.memoryUsage();
|
|
986
|
+
|
|
987
|
+
const message: protocol.MemoryMessage = {
|
|
988
|
+
type: 'memory',
|
|
989
|
+
heapUsed: usage.heapUsed,
|
|
990
|
+
rss: usage.rss,
|
|
991
|
+
external: usage.external,
|
|
992
|
+
arrayBuffers: usage.arrayBuffers,
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
port.postMessage(message);
|
|
996
|
+
}, memorySampleIntervalMs);
|
|
997
|
+
|
|
998
|
+
memoryReporter.unref();
|
|
999
|
+
|
|
1000
|
+
/**
|
|
1001
|
+
* Idempotent module teardown promise.
|
|
1002
|
+
*/
|
|
1003
|
+
let moduleClosePromise: Promise<void> | undefined;
|
|
1004
|
+
|
|
1005
|
+
/**
|
|
1006
|
+
* Closes worker module connections once.
|
|
1007
|
+
*/
|
|
1008
|
+
function disposeModuleConnections(): Promise<void> {
|
|
1009
|
+
if (moduleClosePromise === undefined) {
|
|
1010
|
+
moduleClosePromise = closeWorkerModuleConnections();
|
|
1011
|
+
}
|
|
1012
|
+
|
|
1013
|
+
return moduleClosePromise;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
process.once('beforeExit', () => {
|
|
1017
|
+
void disposeModuleConnections();
|
|
1018
|
+
});
|
|
1019
|
+
|
|
1020
|
+
/**
|
|
1021
|
+
* Main worker message loop.
|
|
1022
|
+
*/
|
|
1023
|
+
port.on('message', (message: protocol.InboundMessage) => {
|
|
1024
|
+
if (message.type === 'execute') {
|
|
1025
|
+
void execute(message).then((result) => {
|
|
1026
|
+
postOutboundMessage(result);
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
});
|