@hydraharness/harness-code-runtime-worker-thread 0.1.1-rc.6
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/LICENSE +21 -0
- package/README.md +52 -0
- package/lib/index.js +941 -0
- package/lib/invariant.js +23 -0
- package/lib/types/bootstrap.d.ts +145 -0
- package/lib/types/index.d.ts +78 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/output-json.d.ts +26 -0
- package/lib/types/protocol.d.ts +76 -0
- package/lib/types/worker-json.d.ts +46 -0
- package/lib/types/worker.d.ts +7 -0
- package/lib/worker.cjs +909 -0
- package/package.json +60 -0
package/lib/index.js
ADDED
|
@@ -0,0 +1,941 @@
|
|
|
1
|
+
import { stripTypeScriptTypes } from "node:module";
|
|
2
|
+
import { Worker } from "node:worker_threads";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import z from "@hydraharness/schemastery";
|
|
5
|
+
import { MAX_TIMER_DELAY_MS } from "@hydraharness/harness-timeout";
|
|
6
|
+
import { CodeRuntime, DUNDER_MEMBER, PORTABLE_RESERVED_WORDS, RESERVED_BINDING_GLOBALS, RESERVED_ERROR_MEMBERS } from "@hydraharness/harness-code-runtime";
|
|
7
|
+
import { snapshotJsonValue } from "@hydraharness/harness-session";
|
|
8
|
+
//#region lib/types/output-json.js
|
|
9
|
+
/** JSON string-prefix accounting for the outer-output ledger. @module @hydraharness/harness-code-runtime-worker-thread/output-json */
|
|
10
|
+
const intrinsicReflectApply$1 = Reflect.apply;
|
|
11
|
+
const intrinsicArrayIsArray$1 = Array.isArray;
|
|
12
|
+
const IntrinsicBuffer = Buffer;
|
|
13
|
+
const intrinsicBufferByteLength = Reflect.get(Buffer, "byteLength");
|
|
14
|
+
const intrinsicObjectCreate$1 = Object.create;
|
|
15
|
+
const intrinsicObjectDefineProperty$1 = Object.defineProperty;
|
|
16
|
+
const intrinsicObjectKeys$1 = Object.keys;
|
|
17
|
+
const intrinsicString = String;
|
|
18
|
+
const intrinsicStringCharCodeAt = Reflect.get(String.prototype, "charCodeAt");
|
|
19
|
+
const intrinsicStringCodePointAt = Reflect.get(String.prototype, "codePointAt");
|
|
20
|
+
const intrinsicStringSlice = Reflect.get(String.prototype, "slice");
|
|
21
|
+
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
|
|
22
|
+
function dataDescriptor$1(value) {
|
|
23
|
+
const descriptor = intrinsicObjectCreate$1(null);
|
|
24
|
+
descriptor.value = value;
|
|
25
|
+
return descriptor;
|
|
26
|
+
}
|
|
27
|
+
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
|
|
28
|
+
function defineEnumerableDataProperty$1(target, key, value) {
|
|
29
|
+
const descriptor = dataDescriptor$1(value);
|
|
30
|
+
descriptor.enumerable = true;
|
|
31
|
+
descriptor.configurable = true;
|
|
32
|
+
descriptor.writable = true;
|
|
33
|
+
intrinsicObjectDefineProperty$1(target, key, descriptor);
|
|
34
|
+
}
|
|
35
|
+
/** UTF-8 byte length through the module-captured Node intrinsic. */
|
|
36
|
+
function byteLength(text) {
|
|
37
|
+
return intrinsicReflectApply$1(intrinsicBufferByteLength, IntrinsicBuffer, [text, "utf8"]);
|
|
38
|
+
}
|
|
39
|
+
/** Append without consulting a model-mutated `Array.prototype`. */
|
|
40
|
+
function append$1(target, value) {
|
|
41
|
+
defineEnumerableDataProperty$1(target, target.length, value);
|
|
42
|
+
}
|
|
43
|
+
/** Pop without consulting a model-mutated `Array.prototype`. */
|
|
44
|
+
function takeLast$1(target) {
|
|
45
|
+
if (target.length === 0) return void 0;
|
|
46
|
+
const index = target.length - 1;
|
|
47
|
+
const value = target[index];
|
|
48
|
+
intrinsicObjectDefineProperty$1(target, "length", dataDescriptor$1(index));
|
|
49
|
+
return value;
|
|
50
|
+
}
|
|
51
|
+
/** One code-point-aligned character from a string. */
|
|
52
|
+
function characterAt(text, index) {
|
|
53
|
+
return intrinsicReflectApply$1(intrinsicStringSlice, text, [index, index + (intrinsicReflectApply$1(intrinsicStringCodePointAt, text, [index]) > 65535 ? 2 : 1)]);
|
|
54
|
+
}
|
|
55
|
+
/** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
|
|
56
|
+
function serializedCharacterBytes(character) {
|
|
57
|
+
if (character.length === 2) return 4;
|
|
58
|
+
if (character === "\"" || character === "\\") return 2;
|
|
59
|
+
const code = intrinsicReflectApply$1(intrinsicStringCharCodeAt, character, [0]);
|
|
60
|
+
if (code >= 55296 && code <= 57343) return 6;
|
|
61
|
+
if (code < 32) return code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
|
|
62
|
+
return byteLength(character);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Measure one JSON string without materializing its complete escaped form.
|
|
66
|
+
* @param text - the candidate string.
|
|
67
|
+
* @param maxBytes - largest serialized size the caller can admit.
|
|
68
|
+
* @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
|
|
69
|
+
*/
|
|
70
|
+
function jsonStringBytesUpTo(text, maxBytes) {
|
|
71
|
+
if (maxBytes < 2) return void 0;
|
|
72
|
+
let bytes = 2;
|
|
73
|
+
for (let index = 0; index < text.length;) {
|
|
74
|
+
const character = characterAt(text, index);
|
|
75
|
+
bytes += serializedCharacterBytes(character);
|
|
76
|
+
if (bytes > maxBytes) return void 0;
|
|
77
|
+
index += character.length;
|
|
78
|
+
}
|
|
79
|
+
return bytes;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Measure one lossless JSON value without allocating its serialized form.
|
|
83
|
+
* @param value - already validated lossless JSON.
|
|
84
|
+
* @param maxBytes - largest serialized size the caller can admit.
|
|
85
|
+
* @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
|
|
86
|
+
*/
|
|
87
|
+
function jsonValueBytesUpTo(value, maxBytes) {
|
|
88
|
+
let bytes = 0;
|
|
89
|
+
const add = (cost) => {
|
|
90
|
+
bytes += cost;
|
|
91
|
+
return bytes <= maxBytes;
|
|
92
|
+
};
|
|
93
|
+
const tasks = [{
|
|
94
|
+
kind: "value",
|
|
95
|
+
value
|
|
96
|
+
}];
|
|
97
|
+
for (let task = takeLast$1(tasks); task !== void 0; task = takeLast$1(tasks)) {
|
|
98
|
+
if (task.kind === "value") {
|
|
99
|
+
const current = task.value;
|
|
100
|
+
if (current === null) {
|
|
101
|
+
if (!add(4)) return void 0;
|
|
102
|
+
} else if (typeof current === "string") {
|
|
103
|
+
const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes);
|
|
104
|
+
if (stringBytes === void 0) return void 0;
|
|
105
|
+
bytes += stringBytes;
|
|
106
|
+
} else if (typeof current === "number") {
|
|
107
|
+
if (!add(byteLength(intrinsicString(current)))) return void 0;
|
|
108
|
+
} else if (typeof current === "boolean") {
|
|
109
|
+
if (!add(current ? 4 : 5)) return void 0;
|
|
110
|
+
} else if (intrinsicArrayIsArray$1(current)) {
|
|
111
|
+
if (!add(2)) return void 0;
|
|
112
|
+
if (current.length > 0) append$1(tasks, {
|
|
113
|
+
kind: "array",
|
|
114
|
+
value: current,
|
|
115
|
+
index: 0
|
|
116
|
+
});
|
|
117
|
+
} else {
|
|
118
|
+
if (!add(2)) return void 0;
|
|
119
|
+
const keys = intrinsicObjectKeys$1(current);
|
|
120
|
+
if (keys.length > 0) append$1(tasks, {
|
|
121
|
+
kind: "object",
|
|
122
|
+
value: current,
|
|
123
|
+
keys,
|
|
124
|
+
index: 0
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (task.index > 0 && !add(1)) return void 0;
|
|
130
|
+
if (task.kind === "array") {
|
|
131
|
+
const item = task.value[task.index];
|
|
132
|
+
if (item === void 0) return void 0;
|
|
133
|
+
if (task.index + 1 < task.value.length) append$1(tasks, {
|
|
134
|
+
...task,
|
|
135
|
+
index: task.index + 1
|
|
136
|
+
});
|
|
137
|
+
append$1(tasks, {
|
|
138
|
+
kind: "value",
|
|
139
|
+
value: item
|
|
140
|
+
});
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
const key = task.keys[task.index];
|
|
144
|
+
/* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
|
|
145
|
+
if (key === void 0) return void 0;
|
|
146
|
+
const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes);
|
|
147
|
+
if (keyBytes === void 0) return void 0;
|
|
148
|
+
if (!add(keyBytes + 1)) return void 0;
|
|
149
|
+
const item = task.value[key];
|
|
150
|
+
if (item === void 0) return void 0;
|
|
151
|
+
if (task.index + 1 < task.keys.length) append$1(tasks, {
|
|
152
|
+
...task,
|
|
153
|
+
index: task.index + 1
|
|
154
|
+
});
|
|
155
|
+
append$1(tasks, {
|
|
156
|
+
kind: "value",
|
|
157
|
+
value: item
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return bytes;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Return the longest code-point-aligned prefix whose JSON string encoding,
|
|
164
|
+
* including its surrounding quotes, fits `maxBytes`.
|
|
165
|
+
*
|
|
166
|
+
* @param text - the candidate string.
|
|
167
|
+
* @param maxBytes - serialized JSON-string bytes available.
|
|
168
|
+
* @returns the fitting prefix, or an empty string when even useful content cannot fit.
|
|
169
|
+
*/
|
|
170
|
+
function truncateJsonStringBytes(text, maxBytes) {
|
|
171
|
+
if (maxBytes < 2) return "";
|
|
172
|
+
let bytes = 2;
|
|
173
|
+
let end = 0;
|
|
174
|
+
for (let index = 0; index < text.length;) {
|
|
175
|
+
const character = characterAt(text, index);
|
|
176
|
+
const cost = serializedCharacterBytes(character);
|
|
177
|
+
if (bytes + cost > maxBytes) break;
|
|
178
|
+
bytes += cost;
|
|
179
|
+
end += character.length;
|
|
180
|
+
index += character.length;
|
|
181
|
+
}
|
|
182
|
+
return end === text.length ? text : intrinsicReflectApply$1(intrinsicStringSlice, text, [0, end]);
|
|
183
|
+
}
|
|
184
|
+
//#endregion
|
|
185
|
+
//#region lib/types/worker-json.js
|
|
186
|
+
/**
|
|
187
|
+
* Lossless-JSON snapshots for the dependency-free source worker closure.
|
|
188
|
+
* @module @hydraharness/harness-code-runtime-worker-thread/worker-json
|
|
189
|
+
*/
|
|
190
|
+
const intrinsicFunctionToString = Reflect.get(Function.prototype, "toString");
|
|
191
|
+
const intrinsicReflectApply = Reflect.get(Reflect, "apply");
|
|
192
|
+
const IntrinsicError = Error;
|
|
193
|
+
const IntrinsicSet = Set;
|
|
194
|
+
const intrinsicArrayIsArray = Array.isArray;
|
|
195
|
+
const intrinsicArrayPrototype = Array.prototype;
|
|
196
|
+
const intrinsicNumberIsFinite = Number.isFinite;
|
|
197
|
+
const intrinsicNumberIsSafeInteger = Number.isSafeInteger;
|
|
198
|
+
const intrinsicObjectCreate = Object.create;
|
|
199
|
+
const intrinsicObjectDefineProperty = Object.defineProperty;
|
|
200
|
+
const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
201
|
+
const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf;
|
|
202
|
+
const intrinsicObjectHasOwn = Object.hasOwn;
|
|
203
|
+
const intrinsicObjectIs = Object.is;
|
|
204
|
+
const intrinsicObjectKeys = Object.keys;
|
|
205
|
+
const intrinsicObjectPrototype = Object.prototype;
|
|
206
|
+
const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, "propertyIsEnumerable");
|
|
207
|
+
const intrinsicReflectOwnKeys = Reflect.ownKeys;
|
|
208
|
+
const intrinsicSetAdd = Reflect.get(Set.prototype, "add");
|
|
209
|
+
Reflect.get(Set.prototype, "delete");
|
|
210
|
+
const intrinsicSetHas = Reflect.get(Set.prototype, "has");
|
|
211
|
+
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
|
|
212
|
+
function dataDescriptor(value) {
|
|
213
|
+
const descriptor = intrinsicObjectCreate(null);
|
|
214
|
+
descriptor.value = value;
|
|
215
|
+
return descriptor;
|
|
216
|
+
}
|
|
217
|
+
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
|
|
218
|
+
function defineEnumerableDataProperty(target, key, value) {
|
|
219
|
+
const descriptor = dataDescriptor(value);
|
|
220
|
+
descriptor.enumerable = true;
|
|
221
|
+
descriptor.configurable = true;
|
|
222
|
+
descriptor.writable = true;
|
|
223
|
+
intrinsicObjectDefineProperty(target, key, descriptor);
|
|
224
|
+
}
|
|
225
|
+
/** Append without consulting a model-mutated `Array.prototype`. */
|
|
226
|
+
function append(target, value) {
|
|
227
|
+
defineEnumerableDataProperty(target, target.length, value);
|
|
228
|
+
}
|
|
229
|
+
/** Pop without consulting a model-mutated `Array.prototype`. */
|
|
230
|
+
function takeLast(target) {
|
|
231
|
+
if (target.length === 0) return void 0;
|
|
232
|
+
const index = target.length - 1;
|
|
233
|
+
const value = target[index];
|
|
234
|
+
intrinsicObjectDefineProperty(target, "length", dataDescriptor(index));
|
|
235
|
+
return value;
|
|
236
|
+
}
|
|
237
|
+
/** Whether one captured-intrinsic Set contains a value. */
|
|
238
|
+
function setHas(target, value) {
|
|
239
|
+
return intrinsicReflectApply(intrinsicSetHas, target, [value]);
|
|
240
|
+
}
|
|
241
|
+
/** Add to one captured-intrinsic Set. */
|
|
242
|
+
function setAdd(target, value) {
|
|
243
|
+
intrinsicReflectApply(intrinsicSetAdd, target, [value]);
|
|
244
|
+
}
|
|
245
|
+
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
|
246
|
+
function hasIntrinsicConstructor(prototype, name) {
|
|
247
|
+
const constructor = intrinsicObjectGetOwnPropertyDescriptor(prototype, "constructor")?.value;
|
|
248
|
+
if (typeof constructor !== "function") return false;
|
|
249
|
+
try {
|
|
250
|
+
return constructor.name === name && constructor.prototype === prototype && intrinsicReflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`;
|
|
251
|
+
} catch {
|
|
252
|
+
return false;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
/** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
|
|
256
|
+
function isForeignIntrinsicObjectPrototype(value) {
|
|
257
|
+
return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
|
|
258
|
+
}
|
|
259
|
+
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
|
|
260
|
+
function hasPlainArrayPrototype(value) {
|
|
261
|
+
const prototype = intrinsicObjectGetPrototypeOf(value);
|
|
262
|
+
if (prototype === intrinsicArrayPrototype) return true;
|
|
263
|
+
if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
|
|
264
|
+
const objectPrototype = intrinsicObjectGetPrototypeOf(prototype);
|
|
265
|
+
return typeof objectPrototype === "object" && objectPrototype !== null && isForeignIntrinsicObjectPrototype(objectPrototype);
|
|
266
|
+
}
|
|
267
|
+
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
|
268
|
+
function hasPlainObjectPrototype(value) {
|
|
269
|
+
const prototype = intrinsicObjectGetPrototypeOf(value);
|
|
270
|
+
return prototype === null || prototype === intrinsicObjectPrototype || typeof prototype === "object" && isForeignIntrinsicObjectPrototype(prototype);
|
|
271
|
+
}
|
|
272
|
+
/** Return every JSON-visible object key, or reject own data JSON would discard. */
|
|
273
|
+
function enumerableStringKeys(value) {
|
|
274
|
+
const keys = intrinsicReflectOwnKeys(value);
|
|
275
|
+
for (let index = 0; index < keys.length; index++) {
|
|
276
|
+
const key = keys[index];
|
|
277
|
+
if (typeof key !== "string" || !intrinsicReflectApply(intrinsicObjectPropertyIsEnumerable, value, [key])) return void 0;
|
|
278
|
+
}
|
|
279
|
+
return keys;
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Flatten one validated JSON value for the worker-thread message port.
|
|
283
|
+
* @param value - the lossless JSON value to transport.
|
|
284
|
+
* @returns a pre-order token stream whose own nesting is bounded.
|
|
285
|
+
*/
|
|
286
|
+
function encodeWorkerJson(value) {
|
|
287
|
+
const wire = [];
|
|
288
|
+
const pending = [value];
|
|
289
|
+
for (let current = takeLast(pending); current !== void 0; current = takeLast(pending)) {
|
|
290
|
+
if (current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string") {
|
|
291
|
+
append(wire, current);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
if (intrinsicArrayIsArray(current)) {
|
|
295
|
+
append(wire, {
|
|
296
|
+
kind: "array",
|
|
297
|
+
length: current.length
|
|
298
|
+
});
|
|
299
|
+
for (let index = current.length - 1; index >= 0; index--) {
|
|
300
|
+
const item = current[index];
|
|
301
|
+
if (item === void 0) throw new IntrinsicError("cannot encode a sparse JSON array");
|
|
302
|
+
append(pending, item);
|
|
303
|
+
}
|
|
304
|
+
continue;
|
|
305
|
+
}
|
|
306
|
+
const keys = intrinsicObjectKeys(current);
|
|
307
|
+
append(wire, {
|
|
308
|
+
kind: "object",
|
|
309
|
+
keys
|
|
310
|
+
});
|
|
311
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
312
|
+
const key = keys[index];
|
|
313
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
314
|
+
if (key === void 0) throw new IntrinsicError("cannot encode a missing JSON object key");
|
|
315
|
+
const item = current[key];
|
|
316
|
+
if (item === void 0) throw new IntrinsicError("cannot encode an undefined JSON object property");
|
|
317
|
+
append(pending, item);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
return wire;
|
|
321
|
+
}
|
|
322
|
+
/** Whether an array contains exactly its dense indexed slots and `length`. */
|
|
323
|
+
function isDenseArray(value) {
|
|
324
|
+
if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false;
|
|
325
|
+
for (let index = 0; index < value.length; index++) if (!intrinsicObjectHasOwn(value, index)) return false;
|
|
326
|
+
return true;
|
|
327
|
+
}
|
|
328
|
+
/** Whether one exact string-key list contains a key, without consulting its prototype. */
|
|
329
|
+
function keysContain(keys, expected) {
|
|
330
|
+
for (let index = 0; index < keys.length; index++) if (keys[index] === expected) return true;
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
/** Return one exact container marker, or reject any extra/missing fields. */
|
|
334
|
+
function containerToken(value) {
|
|
335
|
+
if (intrinsicArrayIsArray(value) || !hasPlainObjectPrototype(value)) return void 0;
|
|
336
|
+
const keys = enumerableStringKeys(value);
|
|
337
|
+
if (keys === void 0) return void 0;
|
|
338
|
+
const token = value;
|
|
339
|
+
if (token.kind === "array") {
|
|
340
|
+
if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "length")) return void 0;
|
|
341
|
+
const length = token.length;
|
|
342
|
+
return typeof length === "number" && intrinsicNumberIsSafeInteger(length) && length >= 0 ? {
|
|
343
|
+
kind: "array",
|
|
344
|
+
length
|
|
345
|
+
} : void 0;
|
|
346
|
+
}
|
|
347
|
+
if (token.kind === "object") {
|
|
348
|
+
if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "keys")) return void 0;
|
|
349
|
+
const objectKeys = token.keys;
|
|
350
|
+
if (!intrinsicArrayIsArray(objectKeys) || !isDenseArray(objectKeys)) return void 0;
|
|
351
|
+
const unique = new IntrinsicSet();
|
|
352
|
+
const normalizedKeys = [];
|
|
353
|
+
const objectKeyValues = objectKeys;
|
|
354
|
+
for (let index = 0; index < objectKeyValues.length; index++) {
|
|
355
|
+
const key = objectKeyValues[index];
|
|
356
|
+
if (typeof key !== "string" || setHas(unique, key)) return void 0;
|
|
357
|
+
setAdd(unique, key);
|
|
358
|
+
append(normalizedKeys, key);
|
|
359
|
+
}
|
|
360
|
+
return {
|
|
361
|
+
kind: "object",
|
|
362
|
+
keys: normalizedKeys
|
|
363
|
+
};
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Rebuild one lossless JSON value from the flat worker-thread wire format.
|
|
368
|
+
* Malformed or incomplete traffic returns `undefined`; traversal is iterative
|
|
369
|
+
* and therefore independent of the transported value's application depth.
|
|
370
|
+
* @param input - untrusted message-port payload.
|
|
371
|
+
* @returns the detached JSON value, or `undefined` when the wire is invalid.
|
|
372
|
+
*/
|
|
373
|
+
function decodeWorkerJson(input) {
|
|
374
|
+
try {
|
|
375
|
+
if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return void 0;
|
|
376
|
+
const wire = input;
|
|
377
|
+
const frames = [];
|
|
378
|
+
let root;
|
|
379
|
+
let rootAssigned = false;
|
|
380
|
+
const attach = (value) => {
|
|
381
|
+
const parent = frames[frames.length - 1];
|
|
382
|
+
if (!parent) {
|
|
383
|
+
if (rootAssigned) return false;
|
|
384
|
+
root = value;
|
|
385
|
+
rootAssigned = true;
|
|
386
|
+
return true;
|
|
387
|
+
}
|
|
388
|
+
/* v8 ignore next -- completed frames are popped before another token can attach. */
|
|
389
|
+
if (parent.index >= (parent.kind === "array" ? parent.length : parent.keys.length)) return false;
|
|
390
|
+
if (parent.kind === "array") append(parent.target, value);
|
|
391
|
+
else {
|
|
392
|
+
const key = parent.keys[parent.index];
|
|
393
|
+
/* v8 ignore next -- object frames are built from validated keys and their exact length. */
|
|
394
|
+
if (key === void 0) return false;
|
|
395
|
+
defineEnumerableDataProperty(parent.target, key, value);
|
|
396
|
+
}
|
|
397
|
+
parent.index += 1;
|
|
398
|
+
return true;
|
|
399
|
+
};
|
|
400
|
+
for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
|
|
401
|
+
const token = wire[tokenIndex];
|
|
402
|
+
let value;
|
|
403
|
+
let frame;
|
|
404
|
+
if (token === null || typeof token === "boolean" || typeof token === "string") value = token;
|
|
405
|
+
else if (typeof token === "number") {
|
|
406
|
+
if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return void 0;
|
|
407
|
+
value = token;
|
|
408
|
+
} else {
|
|
409
|
+
if (typeof token !== "object") return void 0;
|
|
410
|
+
const marker = containerToken(token);
|
|
411
|
+
if (!marker) return void 0;
|
|
412
|
+
const remainingTokens = wire.length - tokenIndex - 1;
|
|
413
|
+
if (marker.kind === "array") {
|
|
414
|
+
if (marker.length > remainingTokens) return void 0;
|
|
415
|
+
const target = [];
|
|
416
|
+
value = target;
|
|
417
|
+
if (marker.length > 0) frame = {
|
|
418
|
+
kind: "array",
|
|
419
|
+
target,
|
|
420
|
+
length: marker.length,
|
|
421
|
+
index: 0
|
|
422
|
+
};
|
|
423
|
+
} else {
|
|
424
|
+
if (marker.keys.length > remainingTokens) return void 0;
|
|
425
|
+
const target = {};
|
|
426
|
+
value = target;
|
|
427
|
+
if (marker.keys.length > 0) frame = {
|
|
428
|
+
kind: "object",
|
|
429
|
+
target,
|
|
430
|
+
keys: marker.keys,
|
|
431
|
+
index: 0
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
if (!attach(value)) return void 0;
|
|
436
|
+
if (frame) append(frames, frame);
|
|
437
|
+
while (frames.length > 0) {
|
|
438
|
+
const current = frames[frames.length - 1];
|
|
439
|
+
/* v8 ignore next -- the loop condition guarantees a final frame. */
|
|
440
|
+
if (current === void 0) break;
|
|
441
|
+
if (current.index < (current.kind === "array" ? current.length : current.keys.length)) break;
|
|
442
|
+
takeLast(frames);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
return frames.length === 0 ? root : void 0;
|
|
446
|
+
} catch {
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
//#endregion
|
|
451
|
+
//#region lib/types/index.js
|
|
452
|
+
/**
|
|
453
|
+
* Worker-thread code runtime: a fresh worker runs each host-type-stripped TypeScript program
|
|
454
|
+
* and bridges bindings over its message port. This is containment, not a security boundary:
|
|
455
|
+
* model code has bash-equivalent trust despite an empty environment, a heap cap, measured
|
|
456
|
+
* event-loop busy-time and wall-time budgets, and termination that also stops synchronous loops.
|
|
457
|
+
* @module @hydraharness/harness-code-runtime-worker-thread
|
|
458
|
+
*/
|
|
459
|
+
/**
|
|
460
|
+
* How often the host samples the worker's event-loop utilization for the
|
|
461
|
+
* `computeMs` budget. An internal cadence, not config: the only effect of
|
|
462
|
+
* the interval is budget-expiry granularity (a run can overshoot by up to
|
|
463
|
+
* one interval), and nothing a deployment could tune here improves that
|
|
464
|
+
* without burning host CPU.
|
|
465
|
+
*/
|
|
466
|
+
const ELU_POLL_INTERVAL_MS = 25;
|
|
467
|
+
/** Smallest cap that can represent the counted payloads: an empty logs array plus an empty JSON failure message. */
|
|
468
|
+
const MIN_OUTPUT_BYTES = 4;
|
|
469
|
+
/**
|
|
470
|
+
* The seam's language-portable identifier subset (see
|
|
471
|
+
* `CodeBindingNamespace.global`): no `$`, which is JS-only spelling — the same
|
|
472
|
+
* namespace list must be usable against every backend regardless of language.
|
|
473
|
+
*/
|
|
474
|
+
const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
475
|
+
/**
|
|
476
|
+
* The shell a program is wrapped in for the type-strip, matching the
|
|
477
|
+
* grammatical context it will execute in (an async function body, where
|
|
478
|
+
* top-level `return` and `await` are legal — a bare module parse would
|
|
479
|
+
* reject the `return`). Strip mode is position-preserving (removed syntax
|
|
480
|
+
* becomes whitespace, nothing shifts), so the wrapper survives the strip
|
|
481
|
+
* byte-identical and the body slices back out with the model's own
|
|
482
|
+
* line/column positions intact.
|
|
483
|
+
*/
|
|
484
|
+
const STRIP_WRAP = {
|
|
485
|
+
prefix: "async function __hydra_program__() {\n",
|
|
486
|
+
suffix: "\n}"
|
|
487
|
+
};
|
|
488
|
+
/**
|
|
489
|
+
* The worker entry path. Source runs unbuilt (`src/worker.ts`, loadable
|
|
490
|
+
* directly on this repo's Node range via native type stripping — the file
|
|
491
|
+
* is erasable-only with type-only relative imports); the built package
|
|
492
|
+
* ships it as a sibling CommonJS bundle (`lib/worker.cjs`, its own tsdown
|
|
493
|
+
* entry) because pkg's VFS Worker hook compiles string-path entries as
|
|
494
|
+
* CommonJS.
|
|
495
|
+
* The URL *pathname*'s extension says which world this module is in —
|
|
496
|
+
* pathname, because dev-time module runners (vitest) may suffix
|
|
497
|
+
* `import.meta.url` with a query string; relative resolution drops it. Worker
|
|
498
|
+
* receives a filesystem string so pkg's VFS Worker hook can resolve it.
|
|
499
|
+
*/
|
|
500
|
+
/* v8 ignore next -- the './worker.cjs' arm is the built-lib world, unreachable unbuilt by construction; the built-lib e2e pins it. */
|
|
501
|
+
const WORKER_PATH = fileURLToPath(new URL(new URL(import.meta.url).pathname.endsWith(".ts") ? "./worker.ts" : "./worker.cjs", import.meta.url));
|
|
502
|
+
/** Render an unknown thrown value as a message, `Error` or not. */
|
|
503
|
+
function messageOf(error) {
|
|
504
|
+
return error instanceof Error ? error.message : String(error);
|
|
505
|
+
}
|
|
506
|
+
/** Resolve after a worker pipe emits all queued data, or closes/errors during termination. */
|
|
507
|
+
function waitForPipeDrain(stream) {
|
|
508
|
+
if (stream.readableEnded || stream.destroyed) return Promise.resolve();
|
|
509
|
+
return new Promise((resolve) => {
|
|
510
|
+
const done = () => {
|
|
511
|
+
stream.off("end", done);
|
|
512
|
+
stream.off("close", done);
|
|
513
|
+
stream.off("error", done);
|
|
514
|
+
resolve();
|
|
515
|
+
};
|
|
516
|
+
stream.once("end", done);
|
|
517
|
+
stream.once("close", done);
|
|
518
|
+
stream.once("error", done);
|
|
519
|
+
/* v8 ignore next -- this race cannot be scheduled deterministically between the adjacent state check and listener registration. */
|
|
520
|
+
if (stream.readableEnded || stream.destroyed) done();
|
|
521
|
+
});
|
|
522
|
+
}
|
|
523
|
+
/**
|
|
524
|
+
* Runtime shape gate for inbound port traffic. The peer runs MODEL CODE and
|
|
525
|
+
* can post anything — `null`, primitives, objects with poisoned fields — so
|
|
526
|
+
* the compile-time `WorkerToHost` type means nothing here: everything is
|
|
527
|
+
* re-validated and REBUILT field by field (a forged extra field never rides
|
|
528
|
+
* along; a non-number call id can never be echoed into a reply). Junk returns
|
|
529
|
+
* `undefined` and is dropped — a throw in the host's `message` listener would
|
|
530
|
+
* crash the host process.
|
|
531
|
+
*/
|
|
532
|
+
function parseWorkerMessage(raw) {
|
|
533
|
+
if (typeof raw !== "object" || raw === null) return void 0;
|
|
534
|
+
const m = raw;
|
|
535
|
+
switch (m.type) {
|
|
536
|
+
case "call":
|
|
537
|
+
if (typeof m.id !== "number" || typeof m.global !== "string" || typeof m.name !== "string") return void 0;
|
|
538
|
+
return {
|
|
539
|
+
type: "call",
|
|
540
|
+
id: m.id,
|
|
541
|
+
global: m.global,
|
|
542
|
+
name: m.name,
|
|
543
|
+
args: m.args
|
|
544
|
+
};
|
|
545
|
+
case "log":
|
|
546
|
+
if (typeof m.text !== "string") return void 0;
|
|
547
|
+
return {
|
|
548
|
+
type: "log",
|
|
549
|
+
text: m.text
|
|
550
|
+
};
|
|
551
|
+
case "output-limit": return { type: "output-limit" };
|
|
552
|
+
case "done": {
|
|
553
|
+
if (m.error === void 0) return {
|
|
554
|
+
type: "done",
|
|
555
|
+
...m.value !== void 0 ? { value: m.value } : {}
|
|
556
|
+
};
|
|
557
|
+
const error = m.error;
|
|
558
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
559
|
+
const { kind, message } = error;
|
|
560
|
+
if (kind !== "exception" && kind !== "invalid-output" && kind !== "output-limit" || typeof message !== "string") return void 0;
|
|
561
|
+
return {
|
|
562
|
+
type: "done",
|
|
563
|
+
error: {
|
|
564
|
+
kind,
|
|
565
|
+
message
|
|
566
|
+
}
|
|
567
|
+
};
|
|
568
|
+
}
|
|
569
|
+
default: return;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
/** One run's combined outer-output ledger; binding values never enter it. */
|
|
573
|
+
var OutputLedger = class {
|
|
574
|
+
maxBytes;
|
|
575
|
+
bytes = 2;
|
|
576
|
+
entries = 0;
|
|
577
|
+
constructor(maxBytes) {
|
|
578
|
+
this.maxBytes = maxBytes;
|
|
579
|
+
}
|
|
580
|
+
/** Admit one exact log entry, or report that the hard cap was crossed. */
|
|
581
|
+
admit(text, sink) {
|
|
582
|
+
const separatorBytes = this.entries > 0 ? 1 : 0;
|
|
583
|
+
const stringBytes = jsonStringBytesUpTo(text, this.maxBytes - this.bytes - separatorBytes);
|
|
584
|
+
if (stringBytes === void 0) return false;
|
|
585
|
+
this.bytes += stringBytes + separatorBytes;
|
|
586
|
+
this.entries += 1;
|
|
587
|
+
sink.push(text);
|
|
588
|
+
return true;
|
|
589
|
+
}
|
|
590
|
+
/** Finalize a successful absent-or-JSON completion against the combined cap. */
|
|
591
|
+
success(logs, value) {
|
|
592
|
+
if (value !== void 0 && jsonValueBytesUpTo(value, this.maxBytes - this.bytes) === void 0) return this.limit(logs);
|
|
593
|
+
return {
|
|
594
|
+
logs,
|
|
595
|
+
...value !== void 0 ? { value } : {}
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
/** Finalize a failure diagnostic, with output-limit taking precedence when combined bytes exceed the cap. */
|
|
599
|
+
failure(logs, error) {
|
|
600
|
+
if (jsonStringBytesUpTo(error.message, this.maxBytes - this.bytes) === void 0) return this.limit(logs);
|
|
601
|
+
return {
|
|
602
|
+
logs,
|
|
603
|
+
error
|
|
604
|
+
};
|
|
605
|
+
}
|
|
606
|
+
/** Build the explicit output-limit failure while retaining a fitting prefix of the final log. */
|
|
607
|
+
limit(logs) {
|
|
608
|
+
const fullMessage = `outer output exceeded ${this.maxBytes} bytes`;
|
|
609
|
+
const messageBytes = fullMessage.length + 2;
|
|
610
|
+
const retained = [];
|
|
611
|
+
let retainedBytes = 2;
|
|
612
|
+
const logBudget = this.maxBytes - messageBytes;
|
|
613
|
+
for (const text of logs) {
|
|
614
|
+
const separatorBytes = retained.length > 0 ? 1 : 0;
|
|
615
|
+
const availableBytes = logBudget - retainedBytes - separatorBytes;
|
|
616
|
+
const stringBytes = jsonStringBytesUpTo(text, availableBytes);
|
|
617
|
+
if (stringBytes !== void 0) {
|
|
618
|
+
retained.push(text);
|
|
619
|
+
retainedBytes += stringBytes + separatorBytes;
|
|
620
|
+
continue;
|
|
621
|
+
}
|
|
622
|
+
const prefix = truncateJsonStringBytes(text, availableBytes);
|
|
623
|
+
if (prefix.length > 0) {
|
|
624
|
+
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes);
|
|
625
|
+
/* v8 ignore next -- truncateJsonStringBytes guarantees its returned prefix fits the same budget. */
|
|
626
|
+
if (prefixBytes === void 0) throw new Error("output ledger produced an oversized log prefix");
|
|
627
|
+
retained.push(prefix);
|
|
628
|
+
retainedBytes += prefixBytes + separatorBytes;
|
|
629
|
+
}
|
|
630
|
+
break;
|
|
631
|
+
}
|
|
632
|
+
return {
|
|
633
|
+
logs: retained,
|
|
634
|
+
error: {
|
|
635
|
+
kind: "output-limit",
|
|
636
|
+
message: truncateJsonStringBytes(fullMessage, this.maxBytes - retainedBytes)
|
|
637
|
+
}
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
};
|
|
641
|
+
/**
|
|
642
|
+
* The shipped {@link CodeRuntime} backend (`ctx.codeRuntime`). Registers as
|
|
643
|
+
* the `codeRuntime` service; every cap comes from validated config. See the
|
|
644
|
+
* module doc for the containment model and the Service Definition's class JSDoc for
|
|
645
|
+
* the contract this implements (error-as-field, hostile-peer port,
|
|
646
|
+
* no cross-run state, dispose to quiescence).
|
|
647
|
+
*/
|
|
648
|
+
var WorkerThreadCodeRuntime = class extends CodeRuntime {
|
|
649
|
+
static Config = z.object({
|
|
650
|
+
computeMs: z.number().default(6e4),
|
|
651
|
+
maxWallMs: z.number().default(6e5),
|
|
652
|
+
maxOutputBytes: z.number().default(67108864),
|
|
653
|
+
maxOldGenerationSizeMb: z.number().default(512)
|
|
654
|
+
});
|
|
655
|
+
language = "typescript";
|
|
656
|
+
isolation = "worker-thread";
|
|
657
|
+
config;
|
|
658
|
+
live = /* @__PURE__ */ new Set();
|
|
659
|
+
disposed = false;
|
|
660
|
+
constructor(ctx, config) {
|
|
661
|
+
super(ctx);
|
|
662
|
+
this.config = config;
|
|
663
|
+
for (const [key, value] of Object.entries(this.config)) if (!(Number.isFinite(value) && value > 0)) throw new Error(`hydra-code-runtime-worker-thread: config.${key} must be a positive number, got ${String(value)}`);
|
|
664
|
+
if (!Number.isSafeInteger(this.config.maxOutputBytes) || this.config.maxOutputBytes < MIN_OUTPUT_BYTES) throw new Error(`hydra-code-runtime-worker-thread: config.maxOutputBytes must be a safe integer of at least ${MIN_OUTPUT_BYTES}, got ${String(this.config.maxOutputBytes)}`);
|
|
665
|
+
if (this.config.maxWallMs > MAX_TIMER_DELAY_MS) throw new Error(`hydra-code-runtime-worker-thread: config.maxWallMs must be at most ${MAX_TIMER_DELAY_MS} (Node clamps a longer setTimeout delay to 1ms), got ${String(this.config.maxWallMs)}`);
|
|
666
|
+
ctx.effect(() => () => this.teardown(), "worker code-runtime teardown");
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* Dispose to quiescence: mark the service unusable, fail every in-flight
|
|
670
|
+
* run as aborted, and AWAIT each worker's exit so no worker outlives the
|
|
671
|
+
* fiber.
|
|
672
|
+
*/
|
|
673
|
+
async teardown() {
|
|
674
|
+
this.disposed = true;
|
|
675
|
+
const runs = [...this.live];
|
|
676
|
+
for (const run of runs) run.settle({
|
|
677
|
+
kind: "abort",
|
|
678
|
+
message: "runtime disposed"
|
|
679
|
+
});
|
|
680
|
+
await Promise.all(runs.map((run) => run.finished));
|
|
681
|
+
}
|
|
682
|
+
/**
|
|
683
|
+
* Execute one program in a fresh worker. Program outcomes — including a
|
|
684
|
+
* type-strip syntax error, which never spawns a worker — resolve with
|
|
685
|
+
* `result.error`; the method rejects only for Service Definition contract misuse (a disposed
|
|
686
|
+
* runtime, an invalid binding namespace).
|
|
687
|
+
* @param request - the program, its bindings, and the abort signal.
|
|
688
|
+
* @returns the run's outcome per the seam contract.
|
|
689
|
+
*/
|
|
690
|
+
async run(request) {
|
|
691
|
+
if (this.disposed) throw new Error("hydra-code-runtime-worker-thread: run() after disposal");
|
|
692
|
+
const bindings = this.validateBindings(request);
|
|
693
|
+
if (request.signal?.aborted) return this.failureBeforeWorker({
|
|
694
|
+
kind: "abort",
|
|
695
|
+
message: String(request.signal.reason)
|
|
696
|
+
});
|
|
697
|
+
let code;
|
|
698
|
+
try {
|
|
699
|
+
const stripped = stripTypeScriptTypes(STRIP_WRAP.prefix + request.program + STRIP_WRAP.suffix);
|
|
700
|
+
code = stripped.slice(STRIP_WRAP.prefix.length, stripped.length - STRIP_WRAP.suffix.length);
|
|
701
|
+
} catch (error) {
|
|
702
|
+
return this.failureBeforeWorker({
|
|
703
|
+
kind: "exception",
|
|
704
|
+
message: messageOf(error)
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
return await this.execute(request, code, bindings);
|
|
708
|
+
}
|
|
709
|
+
/** Apply the outer-output ledger to failures that occur before a worker owns one. */
|
|
710
|
+
failureBeforeWorker(error) {
|
|
711
|
+
return new OutputLedger(this.config.maxOutputBytes).failure([], error);
|
|
712
|
+
}
|
|
713
|
+
/** Reject malformed binding globals or typed-error declarations as Service Definition contract misuse. */
|
|
714
|
+
validateBindings(request) {
|
|
715
|
+
const bindings = /* @__PURE__ */ new Map();
|
|
716
|
+
for (const namespace of request.bindings) {
|
|
717
|
+
if (!IDENTIFIER.test(namespace.global) || PORTABLE_RESERVED_WORDS.has(namespace.global)) throw new Error(`hydra-code-runtime-worker-thread: binding global ${JSON.stringify(namespace.global)} is not a usable identifier`);
|
|
718
|
+
if (RESERVED_BINDING_GLOBALS.has(namespace.global)) throw new Error(`hydra-code-runtime-worker-thread: reserved binding global ${JSON.stringify(namespace.global)}`);
|
|
719
|
+
if (bindings.has(namespace.global)) throw new Error(`hydra-code-runtime-worker-thread: duplicate binding global ${JSON.stringify(namespace.global)}`);
|
|
720
|
+
bindings.set(namespace.global, namespace);
|
|
721
|
+
}
|
|
722
|
+
const errorClassNames = /* @__PURE__ */ new Set();
|
|
723
|
+
for (const namespace of request.bindings) {
|
|
724
|
+
const descriptor = namespace.errorClass;
|
|
725
|
+
if (!descriptor) continue;
|
|
726
|
+
if (!IDENTIFIER.test(descriptor.name) || PORTABLE_RESERVED_WORDS.has(descriptor.name)) throw new Error(`hydra-code-runtime-worker-thread: binding error class ${JSON.stringify(descriptor.name)} is not a usable identifier`);
|
|
727
|
+
if (RESERVED_BINDING_GLOBALS.has(descriptor.name)) throw new Error(`hydra-code-runtime-worker-thread: reserved binding global ${JSON.stringify(descriptor.name)}`);
|
|
728
|
+
if (bindings.has(descriptor.name) || errorClassNames.has(descriptor.name)) throw new Error(`hydra-code-runtime-worker-thread: duplicate injected global ${JSON.stringify(descriptor.name)}`);
|
|
729
|
+
const member = descriptor.memberNameProperty;
|
|
730
|
+
if (member.length === 0 || RESERVED_ERROR_MEMBERS.has(member) || DUNDER_MEMBER.test(member)) throw new Error(`hydra-code-runtime-worker-thread: binding error member property ${JSON.stringify(descriptor.memberNameProperty)} is not usable`);
|
|
731
|
+
errorClassNames.add(descriptor.name);
|
|
732
|
+
}
|
|
733
|
+
return bindings;
|
|
734
|
+
}
|
|
735
|
+
/** Spawn the worker for one validated, type-stripped run and drive it to settlement. */
|
|
736
|
+
execute(request, code, bindings) {
|
|
737
|
+
const worker = new Worker(WORKER_PATH, {
|
|
738
|
+
workerData: {
|
|
739
|
+
code,
|
|
740
|
+
namespaces: [...bindings].map(([global, namespace]) => ({
|
|
741
|
+
global,
|
|
742
|
+
names: Object.keys(namespace.functions),
|
|
743
|
+
...namespace.errorClass ? { errorClass: namespace.errorClass } : {}
|
|
744
|
+
})),
|
|
745
|
+
maxOutputBytes: this.config.maxOutputBytes
|
|
746
|
+
},
|
|
747
|
+
env: {},
|
|
748
|
+
execArgv: [],
|
|
749
|
+
resourceLimits: { maxOldGenerationSizeMb: this.config.maxOldGenerationSizeMb },
|
|
750
|
+
stdout: true,
|
|
751
|
+
stderr: true
|
|
752
|
+
});
|
|
753
|
+
return new Promise((resolve) => {
|
|
754
|
+
let settled = false;
|
|
755
|
+
const answered = /* @__PURE__ */ new Set();
|
|
756
|
+
const logs = [];
|
|
757
|
+
const strayLogs = [];
|
|
758
|
+
const output = new OutputLedger(this.config.maxOutputBytes);
|
|
759
|
+
let terminalOverride;
|
|
760
|
+
const captureStray = (chunk) => {
|
|
761
|
+
/* v8 ignore next -- a second post-overflow chunk races immediate worker termination; the first overflow path is covered. */
|
|
762
|
+
if (terminalOverride !== void 0) return;
|
|
763
|
+
const text = chunk.toString("utf8");
|
|
764
|
+
if (!output.admit(text, strayLogs)) {
|
|
765
|
+
const limited = output.limit([
|
|
766
|
+
...logs,
|
|
767
|
+
...strayLogs,
|
|
768
|
+
text
|
|
769
|
+
]);
|
|
770
|
+
terminalOverride = limited;
|
|
771
|
+
finish(limited);
|
|
772
|
+
}
|
|
773
|
+
};
|
|
774
|
+
worker.stdout.on("data", captureStray);
|
|
775
|
+
worker.stderr.on("data", captureStray);
|
|
776
|
+
let finishResolve;
|
|
777
|
+
const finished = new Promise((done) => {
|
|
778
|
+
finishResolve = done;
|
|
779
|
+
});
|
|
780
|
+
const finish = (finalize) => {
|
|
781
|
+
if (settled) return;
|
|
782
|
+
settled = true;
|
|
783
|
+
clearInterval(eluTimer);
|
|
784
|
+
clearTimeout(wallTimer);
|
|
785
|
+
request.signal?.removeEventListener("abort", onAbort);
|
|
786
|
+
this.live.delete(live);
|
|
787
|
+
new Promise((resume) => {
|
|
788
|
+
setImmediate(resume);
|
|
789
|
+
}).then(async () => {
|
|
790
|
+
const stdoutDrained = waitForPipeDrain(worker.stdout);
|
|
791
|
+
const stderrDrained = waitForPipeDrain(worker.stderr);
|
|
792
|
+
await Promise.all([
|
|
793
|
+
worker.terminate(),
|
|
794
|
+
stdoutDrained,
|
|
795
|
+
stderrDrained
|
|
796
|
+
]);
|
|
797
|
+
const result = terminalOverride ?? (typeof finalize === "function" ? finalize() : finalize);
|
|
798
|
+
finishResolve();
|
|
799
|
+
resolve(result);
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
const onDone = (message) => {
|
|
803
|
+
if (message.type !== "done") return;
|
|
804
|
+
if (message.error) {
|
|
805
|
+
const error = message.error;
|
|
806
|
+
finish(() => output.failure([...logs, ...strayLogs], error));
|
|
807
|
+
return;
|
|
808
|
+
}
|
|
809
|
+
if (message.value === void 0) {
|
|
810
|
+
finish(() => output.success([...logs, ...strayLogs]));
|
|
811
|
+
return;
|
|
812
|
+
}
|
|
813
|
+
const value = decodeWorkerJson(message.value);
|
|
814
|
+
if (value === void 0) finish(() => output.failure([...logs, ...strayLogs], {
|
|
815
|
+
kind: "invalid-output",
|
|
816
|
+
message: "program completion must be lossless JSON"
|
|
817
|
+
}));
|
|
818
|
+
else finish(() => output.success([...logs, ...strayLogs], value));
|
|
819
|
+
};
|
|
820
|
+
const onCall = (message) => {
|
|
821
|
+
if (message.type !== "call" || settled) return;
|
|
822
|
+
if (answered.has(message.id)) return;
|
|
823
|
+
answered.add(message.id);
|
|
824
|
+
const reply = (payload) => {
|
|
825
|
+
if (settled) return;
|
|
826
|
+
worker.postMessage(payload);
|
|
827
|
+
};
|
|
828
|
+
const record = bindings.get(message.global)?.functions;
|
|
829
|
+
const fn = record && Object.hasOwn(record, message.name) ? record[message.name] : void 0;
|
|
830
|
+
if (typeof fn !== "function") {
|
|
831
|
+
reply({
|
|
832
|
+
type: "reply",
|
|
833
|
+
id: message.id,
|
|
834
|
+
ok: false,
|
|
835
|
+
message: `unknown binding ${JSON.stringify(`${message.global}.${message.name}`)}`
|
|
836
|
+
});
|
|
837
|
+
return;
|
|
838
|
+
}
|
|
839
|
+
const args = decodeWorkerJson(message.args);
|
|
840
|
+
if (args === void 0) {
|
|
841
|
+
reply({
|
|
842
|
+
type: "reply",
|
|
843
|
+
id: message.id,
|
|
844
|
+
ok: false,
|
|
845
|
+
message: "binding arguments must be lossless JSON"
|
|
846
|
+
});
|
|
847
|
+
return;
|
|
848
|
+
}
|
|
849
|
+
(async () => {
|
|
850
|
+
try {
|
|
851
|
+
const resolved = await fn(args);
|
|
852
|
+
let value;
|
|
853
|
+
try {
|
|
854
|
+
value = snapshotJsonValue(resolved);
|
|
855
|
+
} catch {
|
|
856
|
+
value = void 0;
|
|
857
|
+
}
|
|
858
|
+
if (value === void 0) reply({
|
|
859
|
+
type: "reply",
|
|
860
|
+
id: message.id,
|
|
861
|
+
ok: false,
|
|
862
|
+
message: "binding resolution must be lossless JSON"
|
|
863
|
+
});
|
|
864
|
+
else reply({
|
|
865
|
+
type: "reply",
|
|
866
|
+
id: message.id,
|
|
867
|
+
ok: true,
|
|
868
|
+
value: encodeWorkerJson(value)
|
|
869
|
+
});
|
|
870
|
+
} catch (error) {
|
|
871
|
+
reply({
|
|
872
|
+
type: "reply",
|
|
873
|
+
id: message.id,
|
|
874
|
+
ok: false,
|
|
875
|
+
message: messageOf(error)
|
|
876
|
+
});
|
|
877
|
+
}
|
|
878
|
+
})();
|
|
879
|
+
};
|
|
880
|
+
worker.on("message", (raw) => {
|
|
881
|
+
const message = parseWorkerMessage(raw);
|
|
882
|
+
if (!message) return;
|
|
883
|
+
if (message.type === "log" && !settled && !output.admit(message.text, logs)) {
|
|
884
|
+
finish(output.limit([
|
|
885
|
+
...logs,
|
|
886
|
+
...strayLogs,
|
|
887
|
+
message.text
|
|
888
|
+
]));
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
if (message.type === "output-limit" && !settled) {
|
|
892
|
+
finish(output.limit([...logs, ...strayLogs]));
|
|
893
|
+
return;
|
|
894
|
+
}
|
|
895
|
+
onCall(message);
|
|
896
|
+
onDone(message);
|
|
897
|
+
});
|
|
898
|
+
worker.on("error", (error) => {
|
|
899
|
+
finish(() => output.failure([...logs, ...strayLogs], {
|
|
900
|
+
kind: "worker-exit",
|
|
901
|
+
message: `worker error: ${error.message}`
|
|
902
|
+
}));
|
|
903
|
+
});
|
|
904
|
+
worker.on("exit", (exitCode) => {
|
|
905
|
+
finish(() => output.failure([...logs, ...strayLogs], {
|
|
906
|
+
kind: "worker-exit",
|
|
907
|
+
message: `worker exited with code ${exitCode} before completing`
|
|
908
|
+
}));
|
|
909
|
+
});
|
|
910
|
+
const eluTimer = setInterval(() => {
|
|
911
|
+
if (worker.performance.eventLoopUtilization().active > this.config.computeMs) finish(() => output.failure([...logs, ...strayLogs], {
|
|
912
|
+
kind: "timeout",
|
|
913
|
+
message: `compute budget exhausted (${this.config.computeMs}ms busy)`
|
|
914
|
+
}));
|
|
915
|
+
}, ELU_POLL_INTERVAL_MS);
|
|
916
|
+
const wallTimer = setTimeout(() => {
|
|
917
|
+
finish(() => output.failure([...logs, ...strayLogs], {
|
|
918
|
+
kind: "timeout",
|
|
919
|
+
message: `wall-clock ceiling reached (${this.config.maxWallMs}ms)`
|
|
920
|
+
}));
|
|
921
|
+
}, this.config.maxWallMs);
|
|
922
|
+
const onAbort = () => {
|
|
923
|
+
finish(() => output.failure([...logs, ...strayLogs], {
|
|
924
|
+
kind: "abort",
|
|
925
|
+
message: String(request.signal?.reason)
|
|
926
|
+
}));
|
|
927
|
+
};
|
|
928
|
+
request.signal?.addEventListener("abort", onAbort, { once: true });
|
|
929
|
+
const live = {
|
|
930
|
+
worker,
|
|
931
|
+
finished,
|
|
932
|
+
settle: (failure) => {
|
|
933
|
+
finish(() => output.failure([...logs, ...strayLogs], failure));
|
|
934
|
+
}
|
|
935
|
+
};
|
|
936
|
+
this.live.add(live);
|
|
937
|
+
});
|
|
938
|
+
}
|
|
939
|
+
};
|
|
940
|
+
//#endregion
|
|
941
|
+
export { WorkerThreadCodeRuntime, WorkerThreadCodeRuntime as default };
|