@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/worker.cjs
ADDED
|
@@ -0,0 +1,909 @@
|
|
|
1
|
+
let node_worker_threads = require("node:worker_threads");
|
|
2
|
+
let node_util = require("node:util");
|
|
3
|
+
//#region lib/types/output-json.js
|
|
4
|
+
/** JSON string-prefix accounting for the outer-output ledger. @module @hydraharness/harness-code-runtime-worker-thread/output-json */
|
|
5
|
+
const intrinsicReflectApply$1 = Reflect.apply;
|
|
6
|
+
const intrinsicArrayIsArray$1 = Array.isArray;
|
|
7
|
+
const IntrinsicBuffer = Buffer;
|
|
8
|
+
const intrinsicBufferByteLength = Reflect.get(Buffer, "byteLength");
|
|
9
|
+
const intrinsicObjectCreate$1 = Object.create;
|
|
10
|
+
const intrinsicObjectDefineProperty$1 = Object.defineProperty;
|
|
11
|
+
const intrinsicObjectKeys$1 = Object.keys;
|
|
12
|
+
const intrinsicString = String;
|
|
13
|
+
const intrinsicStringCharCodeAt = Reflect.get(String.prototype, "charCodeAt");
|
|
14
|
+
const intrinsicStringCodePointAt = Reflect.get(String.prototype, "codePointAt");
|
|
15
|
+
const intrinsicStringSlice = Reflect.get(String.prototype, "slice");
|
|
16
|
+
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
|
|
17
|
+
function dataDescriptor$1(value) {
|
|
18
|
+
const descriptor = intrinsicObjectCreate$1(null);
|
|
19
|
+
descriptor.value = value;
|
|
20
|
+
return descriptor;
|
|
21
|
+
}
|
|
22
|
+
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
|
|
23
|
+
function defineEnumerableDataProperty$1(target, key, value) {
|
|
24
|
+
const descriptor = dataDescriptor$1(value);
|
|
25
|
+
descriptor.enumerable = true;
|
|
26
|
+
descriptor.configurable = true;
|
|
27
|
+
descriptor.writable = true;
|
|
28
|
+
intrinsicObjectDefineProperty$1(target, key, descriptor);
|
|
29
|
+
}
|
|
30
|
+
/** UTF-8 byte length through the module-captured Node intrinsic. */
|
|
31
|
+
function byteLength(text) {
|
|
32
|
+
return intrinsicReflectApply$1(intrinsicBufferByteLength, IntrinsicBuffer, [text, "utf8"]);
|
|
33
|
+
}
|
|
34
|
+
/** Append without consulting a model-mutated `Array.prototype`. */
|
|
35
|
+
function append$1(target, value) {
|
|
36
|
+
defineEnumerableDataProperty$1(target, target.length, value);
|
|
37
|
+
}
|
|
38
|
+
/** Pop without consulting a model-mutated `Array.prototype`. */
|
|
39
|
+
function takeLast$1(target) {
|
|
40
|
+
if (target.length === 0) return void 0;
|
|
41
|
+
const index = target.length - 1;
|
|
42
|
+
const value = target[index];
|
|
43
|
+
intrinsicObjectDefineProperty$1(target, "length", dataDescriptor$1(index));
|
|
44
|
+
return value;
|
|
45
|
+
}
|
|
46
|
+
/** One code-point-aligned character from a string. */
|
|
47
|
+
function characterAt(text, index) {
|
|
48
|
+
return intrinsicReflectApply$1(intrinsicStringSlice, text, [index, index + (intrinsicReflectApply$1(intrinsicStringCodePointAt, text, [index]) > 65535 ? 2 : 1)]);
|
|
49
|
+
}
|
|
50
|
+
/** Serialized bytes contributed by one complete Unicode code point inside JSON quotes. */
|
|
51
|
+
function serializedCharacterBytes(character) {
|
|
52
|
+
if (character.length === 2) return 4;
|
|
53
|
+
if (character === "\"" || character === "\\") return 2;
|
|
54
|
+
const code = intrinsicReflectApply$1(intrinsicStringCharCodeAt, character, [0]);
|
|
55
|
+
if (code >= 55296 && code <= 57343) return 6;
|
|
56
|
+
if (code < 32) return code === 8 || code === 9 || code === 10 || code === 12 || code === 13 ? 2 : 6;
|
|
57
|
+
return byteLength(character);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Measure one JSON string without materializing its complete escaped form.
|
|
61
|
+
* @param text - the candidate string.
|
|
62
|
+
* @param maxBytes - largest serialized size the caller can admit.
|
|
63
|
+
* @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
|
|
64
|
+
*/
|
|
65
|
+
function jsonStringBytesUpTo(text, maxBytes) {
|
|
66
|
+
if (maxBytes < 2) return void 0;
|
|
67
|
+
let bytes = 2;
|
|
68
|
+
for (let index = 0; index < text.length;) {
|
|
69
|
+
const character = characterAt(text, index);
|
|
70
|
+
bytes += serializedCharacterBytes(character);
|
|
71
|
+
if (bytes > maxBytes) return void 0;
|
|
72
|
+
index += character.length;
|
|
73
|
+
}
|
|
74
|
+
return bytes;
|
|
75
|
+
}
|
|
76
|
+
/**
|
|
77
|
+
* Measure one lossless JSON value without allocating its serialized form.
|
|
78
|
+
* @param value - already validated lossless JSON.
|
|
79
|
+
* @param maxBytes - largest serialized size the caller can admit.
|
|
80
|
+
* @returns Exact serialized bytes, or `undefined` as soon as the cap is crossed.
|
|
81
|
+
*/
|
|
82
|
+
function jsonValueBytesUpTo(value, maxBytes) {
|
|
83
|
+
let bytes = 0;
|
|
84
|
+
const add = (cost) => {
|
|
85
|
+
bytes += cost;
|
|
86
|
+
return bytes <= maxBytes;
|
|
87
|
+
};
|
|
88
|
+
const tasks = [{
|
|
89
|
+
kind: "value",
|
|
90
|
+
value
|
|
91
|
+
}];
|
|
92
|
+
for (let task = takeLast$1(tasks); task !== void 0; task = takeLast$1(tasks)) {
|
|
93
|
+
if (task.kind === "value") {
|
|
94
|
+
const current = task.value;
|
|
95
|
+
if (current === null) {
|
|
96
|
+
if (!add(4)) return void 0;
|
|
97
|
+
} else if (typeof current === "string") {
|
|
98
|
+
const stringBytes = jsonStringBytesUpTo(current, maxBytes - bytes);
|
|
99
|
+
if (stringBytes === void 0) return void 0;
|
|
100
|
+
bytes += stringBytes;
|
|
101
|
+
} else if (typeof current === "number") {
|
|
102
|
+
if (!add(byteLength(intrinsicString(current)))) return void 0;
|
|
103
|
+
} else if (typeof current === "boolean") {
|
|
104
|
+
if (!add(current ? 4 : 5)) return void 0;
|
|
105
|
+
} else if (intrinsicArrayIsArray$1(current)) {
|
|
106
|
+
if (!add(2)) return void 0;
|
|
107
|
+
if (current.length > 0) append$1(tasks, {
|
|
108
|
+
kind: "array",
|
|
109
|
+
value: current,
|
|
110
|
+
index: 0
|
|
111
|
+
});
|
|
112
|
+
} else {
|
|
113
|
+
if (!add(2)) return void 0;
|
|
114
|
+
const keys = intrinsicObjectKeys$1(current);
|
|
115
|
+
if (keys.length > 0) append$1(tasks, {
|
|
116
|
+
kind: "object",
|
|
117
|
+
value: current,
|
|
118
|
+
keys,
|
|
119
|
+
index: 0
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
continue;
|
|
123
|
+
}
|
|
124
|
+
if (task.index > 0 && !add(1)) return void 0;
|
|
125
|
+
if (task.kind === "array") {
|
|
126
|
+
const item = task.value[task.index];
|
|
127
|
+
if (item === void 0) return void 0;
|
|
128
|
+
if (task.index + 1 < task.value.length) append$1(tasks, {
|
|
129
|
+
...task,
|
|
130
|
+
index: task.index + 1
|
|
131
|
+
});
|
|
132
|
+
append$1(tasks, {
|
|
133
|
+
kind: "value",
|
|
134
|
+
value: item
|
|
135
|
+
});
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
const key = task.keys[task.index];
|
|
139
|
+
/* v8 ignore next -- an object frame is created and advanced only for an existing Object.keys entry. */
|
|
140
|
+
if (key === void 0) return void 0;
|
|
141
|
+
const keyBytes = jsonStringBytesUpTo(key, maxBytes - bytes);
|
|
142
|
+
if (keyBytes === void 0) return void 0;
|
|
143
|
+
if (!add(keyBytes + 1)) return void 0;
|
|
144
|
+
const item = task.value[key];
|
|
145
|
+
if (item === void 0) return void 0;
|
|
146
|
+
if (task.index + 1 < task.keys.length) append$1(tasks, {
|
|
147
|
+
...task,
|
|
148
|
+
index: task.index + 1
|
|
149
|
+
});
|
|
150
|
+
append$1(tasks, {
|
|
151
|
+
kind: "value",
|
|
152
|
+
value: item
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
return bytes;
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Return the longest code-point-aligned prefix whose JSON string encoding,
|
|
159
|
+
* including its surrounding quotes, fits `maxBytes`.
|
|
160
|
+
*
|
|
161
|
+
* @param text - the candidate string.
|
|
162
|
+
* @param maxBytes - serialized JSON-string bytes available.
|
|
163
|
+
* @returns the fitting prefix, or an empty string when even useful content cannot fit.
|
|
164
|
+
*/
|
|
165
|
+
function truncateJsonStringBytes(text, maxBytes) {
|
|
166
|
+
if (maxBytes < 2) return "";
|
|
167
|
+
let bytes = 2;
|
|
168
|
+
let end = 0;
|
|
169
|
+
for (let index = 0; index < text.length;) {
|
|
170
|
+
const character = characterAt(text, index);
|
|
171
|
+
const cost = serializedCharacterBytes(character);
|
|
172
|
+
if (bytes + cost > maxBytes) break;
|
|
173
|
+
bytes += cost;
|
|
174
|
+
end += character.length;
|
|
175
|
+
index += character.length;
|
|
176
|
+
}
|
|
177
|
+
return end === text.length ? text : intrinsicReflectApply$1(intrinsicStringSlice, text, [0, end]);
|
|
178
|
+
}
|
|
179
|
+
//#endregion
|
|
180
|
+
//#region lib/types/worker-json.js
|
|
181
|
+
/**
|
|
182
|
+
* Lossless-JSON snapshots for the dependency-free source worker closure.
|
|
183
|
+
* @module @hydraharness/harness-code-runtime-worker-thread/worker-json
|
|
184
|
+
*/
|
|
185
|
+
const intrinsicFunctionToString = Reflect.get(Function.prototype, "toString");
|
|
186
|
+
const intrinsicReflectApply = Reflect.get(Reflect, "apply");
|
|
187
|
+
const IntrinsicError = Error;
|
|
188
|
+
const IntrinsicSet = Set;
|
|
189
|
+
const intrinsicArrayIsArray = Array.isArray;
|
|
190
|
+
const intrinsicArrayPrototype = Array.prototype;
|
|
191
|
+
const intrinsicNumberIsFinite = Number.isFinite;
|
|
192
|
+
const intrinsicNumberIsSafeInteger = Number.isSafeInteger;
|
|
193
|
+
const intrinsicObjectCreate = Object.create;
|
|
194
|
+
const intrinsicObjectDefineProperty = Object.defineProperty;
|
|
195
|
+
const intrinsicObjectGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
|
|
196
|
+
const intrinsicObjectGetPrototypeOf = Object.getPrototypeOf;
|
|
197
|
+
const intrinsicObjectHasOwn = Object.hasOwn;
|
|
198
|
+
const intrinsicObjectIs = Object.is;
|
|
199
|
+
const intrinsicObjectKeys = Object.keys;
|
|
200
|
+
const intrinsicObjectPrototype = Object.prototype;
|
|
201
|
+
const intrinsicObjectPropertyIsEnumerable = Reflect.get(intrinsicObjectPrototype, "propertyIsEnumerable");
|
|
202
|
+
const intrinsicReflectOwnKeys = Reflect.ownKeys;
|
|
203
|
+
const intrinsicSetAdd = Reflect.get(Set.prototype, "add");
|
|
204
|
+
const intrinsicSetDelete = Reflect.get(Set.prototype, "delete");
|
|
205
|
+
const intrinsicSetHas = Reflect.get(Set.prototype, "has");
|
|
206
|
+
/** Build a data descriptor that cannot inherit model-defined accessor fields. */
|
|
207
|
+
function dataDescriptor(value) {
|
|
208
|
+
const descriptor = intrinsicObjectCreate(null);
|
|
209
|
+
descriptor.value = value;
|
|
210
|
+
return descriptor;
|
|
211
|
+
}
|
|
212
|
+
/** Define an ordinary enumerable data slot without a prototype-bearing descriptor. */
|
|
213
|
+
function defineEnumerableDataProperty(target, key, value) {
|
|
214
|
+
const descriptor = dataDescriptor(value);
|
|
215
|
+
descriptor.enumerable = true;
|
|
216
|
+
descriptor.configurable = true;
|
|
217
|
+
descriptor.writable = true;
|
|
218
|
+
intrinsicObjectDefineProperty(target, key, descriptor);
|
|
219
|
+
}
|
|
220
|
+
/** Append without consulting a model-mutated `Array.prototype`. */
|
|
221
|
+
function append(target, value) {
|
|
222
|
+
defineEnumerableDataProperty(target, target.length, value);
|
|
223
|
+
}
|
|
224
|
+
/** Pop without consulting a model-mutated `Array.prototype`. */
|
|
225
|
+
function takeLast(target) {
|
|
226
|
+
if (target.length === 0) return void 0;
|
|
227
|
+
const index = target.length - 1;
|
|
228
|
+
const value = target[index];
|
|
229
|
+
intrinsicObjectDefineProperty(target, "length", dataDescriptor(index));
|
|
230
|
+
return value;
|
|
231
|
+
}
|
|
232
|
+
/** Whether one captured-intrinsic Set contains a value. */
|
|
233
|
+
function setHas(target, value) {
|
|
234
|
+
return intrinsicReflectApply(intrinsicSetHas, target, [value]);
|
|
235
|
+
}
|
|
236
|
+
/** Add to one captured-intrinsic Set. */
|
|
237
|
+
function setAdd(target, value) {
|
|
238
|
+
intrinsicReflectApply(intrinsicSetAdd, target, [value]);
|
|
239
|
+
}
|
|
240
|
+
/** Delete from one captured-intrinsic Set. */
|
|
241
|
+
function setDelete(target, value) {
|
|
242
|
+
intrinsicReflectApply(intrinsicSetDelete, target, [value]);
|
|
243
|
+
}
|
|
244
|
+
/** Whether a realm-owned intrinsic prototype is backed by its native constructor. */
|
|
245
|
+
function hasIntrinsicConstructor(prototype, name) {
|
|
246
|
+
const constructor = intrinsicObjectGetOwnPropertyDescriptor(prototype, "constructor")?.value;
|
|
247
|
+
if (typeof constructor !== "function") return false;
|
|
248
|
+
try {
|
|
249
|
+
return constructor.name === name && constructor.prototype === prototype && intrinsicReflectApply(intrinsicFunctionToString, constructor, []) === `function ${name}() { [native code] }`;
|
|
250
|
+
} catch {
|
|
251
|
+
return false;
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/** Whether a candidate is a foreign realm's intrinsic `Object.prototype`. */
|
|
255
|
+
function isForeignIntrinsicObjectPrototype(value) {
|
|
256
|
+
return intrinsicObjectGetPrototypeOf(value) === null && hasIntrinsicConstructor(value, "Object");
|
|
257
|
+
}
|
|
258
|
+
/** Whether an array uses one realm's intrinsic `Array.prototype`, not a subclass or forged prototype. */
|
|
259
|
+
function hasPlainArrayPrototype(value) {
|
|
260
|
+
const prototype = intrinsicObjectGetPrototypeOf(value);
|
|
261
|
+
if (prototype === intrinsicArrayPrototype) return true;
|
|
262
|
+
if (!intrinsicArrayIsArray(prototype) || !hasIntrinsicConstructor(prototype, "Array")) return false;
|
|
263
|
+
const objectPrototype = intrinsicObjectGetPrototypeOf(prototype);
|
|
264
|
+
return typeof objectPrototype === "object" && objectPrototype !== null && isForeignIntrinsicObjectPrototype(objectPrototype);
|
|
265
|
+
}
|
|
266
|
+
/** Whether an object is a plain or null-prototype record from any JavaScript realm. */
|
|
267
|
+
function hasPlainObjectPrototype(value) {
|
|
268
|
+
const prototype = intrinsicObjectGetPrototypeOf(value);
|
|
269
|
+
return prototype === null || prototype === intrinsicObjectPrototype || typeof prototype === "object" && isForeignIntrinsicObjectPrototype(prototype);
|
|
270
|
+
}
|
|
271
|
+
/** Return every JSON-visible object key, or reject own data JSON would discard. */
|
|
272
|
+
function enumerableStringKeys(value) {
|
|
273
|
+
const keys = intrinsicReflectOwnKeys(value);
|
|
274
|
+
for (let index = 0; index < keys.length; index++) {
|
|
275
|
+
const key = keys[index];
|
|
276
|
+
if (typeof key !== "string" || !intrinsicReflectApply(intrinsicObjectPropertyIsEnumerable, value, [key])) return void 0;
|
|
277
|
+
}
|
|
278
|
+
return keys;
|
|
279
|
+
}
|
|
280
|
+
/**
|
|
281
|
+
* Validate and detach one worker-boundary value without loading another
|
|
282
|
+
* workspace package at runtime. This mirrors the session-owned canonical
|
|
283
|
+
* JSON boundary while remaining safe to import from the unbuilt worker.
|
|
284
|
+
* Its iterative traversal adds no JavaScript call-stack depth limit.
|
|
285
|
+
*
|
|
286
|
+
* @param value - the candidate completion value.
|
|
287
|
+
* @returns a detached lossless-JSON snapshot, or `undefined` when invalid.
|
|
288
|
+
*/
|
|
289
|
+
function snapshotCodeJsonValue(value) {
|
|
290
|
+
const active = new IntrinsicSet();
|
|
291
|
+
let root;
|
|
292
|
+
const assign = (destination, item) => {
|
|
293
|
+
if (destination.kind === "root") root = item;
|
|
294
|
+
else if (destination.kind === "array") defineEnumerableDataProperty(destination.target, destination.index, item);
|
|
295
|
+
else defineEnumerableDataProperty(destination.target, destination.key, item);
|
|
296
|
+
};
|
|
297
|
+
const tasks = [{
|
|
298
|
+
kind: "visit",
|
|
299
|
+
value,
|
|
300
|
+
destination: { kind: "root" }
|
|
301
|
+
}];
|
|
302
|
+
for (let task = takeLast(tasks); task !== void 0; task = takeLast(tasks)) {
|
|
303
|
+
if (task.kind === "leave") {
|
|
304
|
+
setDelete(active, task.source);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
if (task.kind === "array-item") {
|
|
308
|
+
if (!intrinsicObjectHasOwn(task.source, task.index)) return void 0;
|
|
309
|
+
append(tasks, {
|
|
310
|
+
kind: "visit",
|
|
311
|
+
value: task.source[task.index],
|
|
312
|
+
destination: {
|
|
313
|
+
kind: "array",
|
|
314
|
+
target: task.target,
|
|
315
|
+
index: task.index
|
|
316
|
+
}
|
|
317
|
+
});
|
|
318
|
+
continue;
|
|
319
|
+
}
|
|
320
|
+
if (task.kind === "object-property") {
|
|
321
|
+
append(tasks, {
|
|
322
|
+
kind: "visit",
|
|
323
|
+
value: task.source[task.key],
|
|
324
|
+
destination: {
|
|
325
|
+
kind: "object",
|
|
326
|
+
target: task.target,
|
|
327
|
+
key: task.key
|
|
328
|
+
}
|
|
329
|
+
});
|
|
330
|
+
continue;
|
|
331
|
+
}
|
|
332
|
+
const candidate = task.value;
|
|
333
|
+
if (candidate === null) {
|
|
334
|
+
assign(task.destination, null);
|
|
335
|
+
continue;
|
|
336
|
+
}
|
|
337
|
+
if (typeof candidate === "boolean" || typeof candidate === "string") {
|
|
338
|
+
assign(task.destination, candidate);
|
|
339
|
+
continue;
|
|
340
|
+
}
|
|
341
|
+
if (typeof candidate === "number") {
|
|
342
|
+
if (!intrinsicNumberIsFinite(candidate) || intrinsicObjectIs(candidate, -0)) return void 0;
|
|
343
|
+
assign(task.destination, candidate);
|
|
344
|
+
continue;
|
|
345
|
+
}
|
|
346
|
+
if (typeof candidate !== "object") return void 0;
|
|
347
|
+
if (setHas(active, candidate)) return void 0;
|
|
348
|
+
if (intrinsicArrayIsArray(candidate)) {
|
|
349
|
+
if (!hasPlainArrayPrototype(candidate)) return void 0;
|
|
350
|
+
const length = candidate.length;
|
|
351
|
+
if (intrinsicReflectOwnKeys(candidate).length !== length + 1) return void 0;
|
|
352
|
+
const target = [];
|
|
353
|
+
assign(task.destination, target);
|
|
354
|
+
setAdd(active, candidate);
|
|
355
|
+
append(tasks, {
|
|
356
|
+
kind: "leave",
|
|
357
|
+
source: candidate
|
|
358
|
+
});
|
|
359
|
+
for (let index = length - 1; index >= 0; index--) append(tasks, {
|
|
360
|
+
kind: "array-item",
|
|
361
|
+
source: candidate,
|
|
362
|
+
index,
|
|
363
|
+
target
|
|
364
|
+
});
|
|
365
|
+
continue;
|
|
366
|
+
}
|
|
367
|
+
if (!hasPlainObjectPrototype(candidate)) return void 0;
|
|
368
|
+
const keys = enumerableStringKeys(candidate);
|
|
369
|
+
if (keys === void 0) return void 0;
|
|
370
|
+
const target = {};
|
|
371
|
+
assign(task.destination, target);
|
|
372
|
+
setAdd(active, candidate);
|
|
373
|
+
append(tasks, {
|
|
374
|
+
kind: "leave",
|
|
375
|
+
source: candidate
|
|
376
|
+
});
|
|
377
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
378
|
+
const key = keys[index];
|
|
379
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
380
|
+
if (key === void 0) return void 0;
|
|
381
|
+
append(tasks, {
|
|
382
|
+
kind: "object-property",
|
|
383
|
+
source: candidate,
|
|
384
|
+
key,
|
|
385
|
+
target
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
return root;
|
|
390
|
+
}
|
|
391
|
+
/**
|
|
392
|
+
* Flatten one validated JSON value for the worker-thread message port.
|
|
393
|
+
* @param value - the lossless JSON value to transport.
|
|
394
|
+
* @returns a pre-order token stream whose own nesting is bounded.
|
|
395
|
+
*/
|
|
396
|
+
function encodeWorkerJson(value) {
|
|
397
|
+
const wire = [];
|
|
398
|
+
const pending = [value];
|
|
399
|
+
for (let current = takeLast(pending); current !== void 0; current = takeLast(pending)) {
|
|
400
|
+
if (current === null || typeof current === "boolean" || typeof current === "number" || typeof current === "string") {
|
|
401
|
+
append(wire, current);
|
|
402
|
+
continue;
|
|
403
|
+
}
|
|
404
|
+
if (intrinsicArrayIsArray(current)) {
|
|
405
|
+
append(wire, {
|
|
406
|
+
kind: "array",
|
|
407
|
+
length: current.length
|
|
408
|
+
});
|
|
409
|
+
for (let index = current.length - 1; index >= 0; index--) {
|
|
410
|
+
const item = current[index];
|
|
411
|
+
if (item === void 0) throw new IntrinsicError("cannot encode a sparse JSON array");
|
|
412
|
+
append(pending, item);
|
|
413
|
+
}
|
|
414
|
+
continue;
|
|
415
|
+
}
|
|
416
|
+
const keys = intrinsicObjectKeys(current);
|
|
417
|
+
append(wire, {
|
|
418
|
+
kind: "object",
|
|
419
|
+
keys
|
|
420
|
+
});
|
|
421
|
+
for (let index = keys.length - 1; index >= 0; index--) {
|
|
422
|
+
const key = keys[index];
|
|
423
|
+
/* v8 ignore next -- the loop is bounded by the captured key count. */
|
|
424
|
+
if (key === void 0) throw new IntrinsicError("cannot encode a missing JSON object key");
|
|
425
|
+
const item = current[key];
|
|
426
|
+
if (item === void 0) throw new IntrinsicError("cannot encode an undefined JSON object property");
|
|
427
|
+
append(pending, item);
|
|
428
|
+
}
|
|
429
|
+
}
|
|
430
|
+
return wire;
|
|
431
|
+
}
|
|
432
|
+
/** Whether an array contains exactly its dense indexed slots and `length`. */
|
|
433
|
+
function isDenseArray(value) {
|
|
434
|
+
if (!hasPlainArrayPrototype(value) || intrinsicReflectOwnKeys(value).length !== value.length + 1) return false;
|
|
435
|
+
for (let index = 0; index < value.length; index++) if (!intrinsicObjectHasOwn(value, index)) return false;
|
|
436
|
+
return true;
|
|
437
|
+
}
|
|
438
|
+
/** Whether one exact string-key list contains a key, without consulting its prototype. */
|
|
439
|
+
function keysContain(keys, expected) {
|
|
440
|
+
for (let index = 0; index < keys.length; index++) if (keys[index] === expected) return true;
|
|
441
|
+
return false;
|
|
442
|
+
}
|
|
443
|
+
/** Return one exact container marker, or reject any extra/missing fields. */
|
|
444
|
+
function containerToken(value) {
|
|
445
|
+
if (intrinsicArrayIsArray(value) || !hasPlainObjectPrototype(value)) return void 0;
|
|
446
|
+
const keys = enumerableStringKeys(value);
|
|
447
|
+
if (keys === void 0) return void 0;
|
|
448
|
+
const token = value;
|
|
449
|
+
if (token.kind === "array") {
|
|
450
|
+
if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "length")) return void 0;
|
|
451
|
+
const length = token.length;
|
|
452
|
+
return typeof length === "number" && intrinsicNumberIsSafeInteger(length) && length >= 0 ? {
|
|
453
|
+
kind: "array",
|
|
454
|
+
length
|
|
455
|
+
} : void 0;
|
|
456
|
+
}
|
|
457
|
+
if (token.kind === "object") {
|
|
458
|
+
if (keys.length !== 2 || !keysContain(keys, "kind") || !keysContain(keys, "keys")) return void 0;
|
|
459
|
+
const objectKeys = token.keys;
|
|
460
|
+
if (!intrinsicArrayIsArray(objectKeys) || !isDenseArray(objectKeys)) return void 0;
|
|
461
|
+
const unique = new IntrinsicSet();
|
|
462
|
+
const normalizedKeys = [];
|
|
463
|
+
const objectKeyValues = objectKeys;
|
|
464
|
+
for (let index = 0; index < objectKeyValues.length; index++) {
|
|
465
|
+
const key = objectKeyValues[index];
|
|
466
|
+
if (typeof key !== "string" || setHas(unique, key)) return void 0;
|
|
467
|
+
setAdd(unique, key);
|
|
468
|
+
append(normalizedKeys, key);
|
|
469
|
+
}
|
|
470
|
+
return {
|
|
471
|
+
kind: "object",
|
|
472
|
+
keys: normalizedKeys
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Rebuild one lossless JSON value from the flat worker-thread wire format.
|
|
478
|
+
* Malformed or incomplete traffic returns `undefined`; traversal is iterative
|
|
479
|
+
* and therefore independent of the transported value's application depth.
|
|
480
|
+
* @param input - untrusted message-port payload.
|
|
481
|
+
* @returns the detached JSON value, or `undefined` when the wire is invalid.
|
|
482
|
+
*/
|
|
483
|
+
function decodeWorkerJson(input) {
|
|
484
|
+
try {
|
|
485
|
+
if (!intrinsicArrayIsArray(input) || !isDenseArray(input) || input.length === 0) return void 0;
|
|
486
|
+
const wire = input;
|
|
487
|
+
const frames = [];
|
|
488
|
+
let root;
|
|
489
|
+
let rootAssigned = false;
|
|
490
|
+
const attach = (value) => {
|
|
491
|
+
const parent = frames[frames.length - 1];
|
|
492
|
+
if (!parent) {
|
|
493
|
+
if (rootAssigned) return false;
|
|
494
|
+
root = value;
|
|
495
|
+
rootAssigned = true;
|
|
496
|
+
return true;
|
|
497
|
+
}
|
|
498
|
+
/* v8 ignore next -- completed frames are popped before another token can attach. */
|
|
499
|
+
if (parent.index >= (parent.kind === "array" ? parent.length : parent.keys.length)) return false;
|
|
500
|
+
if (parent.kind === "array") append(parent.target, value);
|
|
501
|
+
else {
|
|
502
|
+
const key = parent.keys[parent.index];
|
|
503
|
+
/* v8 ignore next -- object frames are built from validated keys and their exact length. */
|
|
504
|
+
if (key === void 0) return false;
|
|
505
|
+
defineEnumerableDataProperty(parent.target, key, value);
|
|
506
|
+
}
|
|
507
|
+
parent.index += 1;
|
|
508
|
+
return true;
|
|
509
|
+
};
|
|
510
|
+
for (let tokenIndex = 0; tokenIndex < wire.length; tokenIndex++) {
|
|
511
|
+
const token = wire[tokenIndex];
|
|
512
|
+
let value;
|
|
513
|
+
let frame;
|
|
514
|
+
if (token === null || typeof token === "boolean" || typeof token === "string") value = token;
|
|
515
|
+
else if (typeof token === "number") {
|
|
516
|
+
if (!intrinsicNumberIsFinite(token) || intrinsicObjectIs(token, -0)) return void 0;
|
|
517
|
+
value = token;
|
|
518
|
+
} else {
|
|
519
|
+
if (typeof token !== "object") return void 0;
|
|
520
|
+
const marker = containerToken(token);
|
|
521
|
+
if (!marker) return void 0;
|
|
522
|
+
const remainingTokens = wire.length - tokenIndex - 1;
|
|
523
|
+
if (marker.kind === "array") {
|
|
524
|
+
if (marker.length > remainingTokens) return void 0;
|
|
525
|
+
const target = [];
|
|
526
|
+
value = target;
|
|
527
|
+
if (marker.length > 0) frame = {
|
|
528
|
+
kind: "array",
|
|
529
|
+
target,
|
|
530
|
+
length: marker.length,
|
|
531
|
+
index: 0
|
|
532
|
+
};
|
|
533
|
+
} else {
|
|
534
|
+
if (marker.keys.length > remainingTokens) return void 0;
|
|
535
|
+
const target = {};
|
|
536
|
+
value = target;
|
|
537
|
+
if (marker.keys.length > 0) frame = {
|
|
538
|
+
kind: "object",
|
|
539
|
+
target,
|
|
540
|
+
keys: marker.keys,
|
|
541
|
+
index: 0
|
|
542
|
+
};
|
|
543
|
+
}
|
|
544
|
+
}
|
|
545
|
+
if (!attach(value)) return void 0;
|
|
546
|
+
if (frame) append(frames, frame);
|
|
547
|
+
while (frames.length > 0) {
|
|
548
|
+
const current = frames[frames.length - 1];
|
|
549
|
+
/* v8 ignore next -- the loop condition guarantees a final frame. */
|
|
550
|
+
if (current === void 0) break;
|
|
551
|
+
if (current.index < (current.kind === "array" ? current.length : current.keys.length)) break;
|
|
552
|
+
takeLast(frames);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
return frames.length === 0 ? root : void 0;
|
|
556
|
+
} catch {
|
|
557
|
+
return;
|
|
558
|
+
}
|
|
559
|
+
}
|
|
560
|
+
//#endregion
|
|
561
|
+
//#region lib/types/bootstrap.js
|
|
562
|
+
/**
|
|
563
|
+
* Worker-side execution logic, written as plain functions over an injected port so the unit
|
|
564
|
+
* suite can run every line IN-PROCESS against a fake port (a real worker thread is a separate
|
|
565
|
+
* V8 isolate the coverage provider cannot observe).
|
|
566
|
+
* @module @hydraharness/harness-code-runtime-worker-thread/src/bootstrap
|
|
567
|
+
*/
|
|
568
|
+
const CapturedError = Error;
|
|
569
|
+
const capturedObjectCreate = Object.create;
|
|
570
|
+
const capturedObjectDefineProperty = Object.defineProperty;
|
|
571
|
+
/** Define one public binding-error field without consulting mutable globals or descriptor prototypes. */
|
|
572
|
+
function defineBindingErrorField(error, key, value) {
|
|
573
|
+
const attributes = capturedObjectCreate(null);
|
|
574
|
+
attributes.enumerable = true;
|
|
575
|
+
attributes.value = value;
|
|
576
|
+
capturedObjectDefineProperty(error, key, attributes);
|
|
577
|
+
}
|
|
578
|
+
/**
|
|
579
|
+
* Ordered text capture under the shared outer JSON-byte budget, delivered to
|
|
580
|
+
* a sink as each item lands (the real sink streams text over the port eagerly,
|
|
581
|
+
* so captured output survives a mid-run termination). It includes the log
|
|
582
|
+
* array syntax and string escaping in its accounting. Once exhausted it emits
|
|
583
|
+
* the fitting prefix and reports the limit once; the host turns that condition
|
|
584
|
+
* into an explicit `output-limit` run failure.
|
|
585
|
+
*/
|
|
586
|
+
var LogBuffer = class {
|
|
587
|
+
bytes = 2;
|
|
588
|
+
entries = 0;
|
|
589
|
+
truncated = false;
|
|
590
|
+
sink;
|
|
591
|
+
onLimit;
|
|
592
|
+
maxBytes;
|
|
593
|
+
constructor(maxBytes, sink, onLimit = () => {}) {
|
|
594
|
+
this.maxBytes = maxBytes;
|
|
595
|
+
this.sink = sink;
|
|
596
|
+
this.onLimit = onLimit;
|
|
597
|
+
}
|
|
598
|
+
/**
|
|
599
|
+
* Emit text to the sink, charging it against the budget (drops + marks once exhausted).
|
|
600
|
+
* @param text - the captured text to deliver.
|
|
601
|
+
*/
|
|
602
|
+
push(text) {
|
|
603
|
+
if (this.truncated) return;
|
|
604
|
+
const separatorBytes = this.entries > 0 ? 1 : 0;
|
|
605
|
+
const availableBytes = this.maxBytes - this.bytes - separatorBytes;
|
|
606
|
+
const stringBytes = jsonStringBytesUpTo(text, availableBytes);
|
|
607
|
+
if (stringBytes === void 0) {
|
|
608
|
+
this.truncated = true;
|
|
609
|
+
const prefix = truncateJsonStringBytes(text, availableBytes);
|
|
610
|
+
if (prefix.length > 0) {
|
|
611
|
+
const prefixBytes = jsonStringBytesUpTo(prefix, availableBytes);
|
|
612
|
+
/* v8 ignore next -- truncateJsonStringBytes guarantees the returned prefix fits. */
|
|
613
|
+
if (prefixBytes === void 0) throw new CapturedError("worker output ledger produced an oversized log prefix");
|
|
614
|
+
this.bytes += prefixBytes + separatorBytes;
|
|
615
|
+
this.entries += 1;
|
|
616
|
+
this.sink(prefix);
|
|
617
|
+
}
|
|
618
|
+
this.onLimit();
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
this.bytes += stringBytes + separatorBytes;
|
|
622
|
+
this.entries += 1;
|
|
623
|
+
this.sink(text);
|
|
624
|
+
}
|
|
625
|
+
/** Remaining exact JSON-byte budget for the completion value or failure message. */
|
|
626
|
+
remainingOutputBytes() {
|
|
627
|
+
return this.maxBytes - this.bytes;
|
|
628
|
+
}
|
|
629
|
+
};
|
|
630
|
+
/** The five console methods the shim captures, in the seam's level vocabulary. */
|
|
631
|
+
const CONSOLE_LEVELS = [
|
|
632
|
+
"log",
|
|
633
|
+
"info",
|
|
634
|
+
"warn",
|
|
635
|
+
"error",
|
|
636
|
+
"debug"
|
|
637
|
+
];
|
|
638
|
+
/**
|
|
639
|
+
* A `console` replacement whose five leveled methods render their arguments
|
|
640
|
+
* `util.inspect`-style (matching real console formatting closely enough for
|
|
641
|
+
* a model to recognize its own output) into the buffer. Only these five
|
|
642
|
+
* exist — the program gets a deliberately small console, not Node's full
|
|
643
|
+
* console API.
|
|
644
|
+
* @param logs - the buffer every rendered line is pushed into.
|
|
645
|
+
* @returns the five-method console object handed to the program.
|
|
646
|
+
*/
|
|
647
|
+
function makeConsoleShim(logs) {
|
|
648
|
+
const render = (args) => args.map((arg) => typeof arg === "string" ? arg : (0, node_util.inspect)(arg, INSPECT_OPTIONS)).join(" ");
|
|
649
|
+
const shim = Object.create(null);
|
|
650
|
+
for (const level of CONSOLE_LEVELS) shim[level] = (...args) => {
|
|
651
|
+
logs.push(render(args));
|
|
652
|
+
};
|
|
653
|
+
return shim;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Redirect a stream's `write` into the log buffer (the program-visible
|
|
657
|
+
* `process.stdout`/`process.stderr` in the real worker), so raw writes land in emission order
|
|
658
|
+
* alongside console output instead of racing down a pipe. It preserves Node's optional callback
|
|
659
|
+
* contract: the callback runs asynchronously after admission, even when the log budget drops
|
|
660
|
+
* the write.
|
|
661
|
+
*
|
|
662
|
+
* @param logs - the buffer captured writes are pushed into.
|
|
663
|
+
* @param stream - the stream whose `write` slot is patched.
|
|
664
|
+
* @returns the restore function (the in-process tests un-patch; the real
|
|
665
|
+
* worker never needs to).
|
|
666
|
+
*/
|
|
667
|
+
function captureStreamWrites(logs, stream) {
|
|
668
|
+
const original = stream.write;
|
|
669
|
+
stream.write = (chunk, ...rest) => {
|
|
670
|
+
logs.push(typeof chunk === "string" ? chunk : String(chunk));
|
|
671
|
+
const callback = [rest[0], rest[1]].find((arg) => typeof arg === "function");
|
|
672
|
+
if (callback) queueMicrotask(() => {
|
|
673
|
+
callback(null);
|
|
674
|
+
});
|
|
675
|
+
return true;
|
|
676
|
+
};
|
|
677
|
+
return () => {
|
|
678
|
+
stream.write = original;
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
/** Bounded inspect options: deep enough to be useful, bounded so a pathological value cannot explode the rendering. */
|
|
682
|
+
const INSPECT_OPTIONS = {
|
|
683
|
+
depth: 4,
|
|
684
|
+
maxArrayLength: 100,
|
|
685
|
+
maxStringLength: 1e4
|
|
686
|
+
};
|
|
687
|
+
/**
|
|
688
|
+
* Prepare the program's completion value for the done message. Only lossless
|
|
689
|
+
* JSON crosses, and a value that does not fit the remaining combined outer
|
|
690
|
+
* budget reports `output-limit`; the host revalidates hostile traffic and
|
|
691
|
+
* remains authoritative for native pipe writes the worker cannot observe.
|
|
692
|
+
*
|
|
693
|
+
* @param value - the program's completion value.
|
|
694
|
+
* @param remainingOutputBytes - exact bytes left after captured logs.
|
|
695
|
+
* @param maxOutputBytes - the configured cap named in an overflow diagnostic.
|
|
696
|
+
* @returns the done-message fragment: `{}` for `undefined`, else a flat wire `{ value }`.
|
|
697
|
+
*/
|
|
698
|
+
function prepareCompletion(value, remainingOutputBytes, maxOutputBytes = remainingOutputBytes) {
|
|
699
|
+
if (value === void 0) return {};
|
|
700
|
+
let snapshot;
|
|
701
|
+
try {
|
|
702
|
+
snapshot = snapshotCodeJsonValue(value);
|
|
703
|
+
} catch {
|
|
704
|
+
snapshot = void 0;
|
|
705
|
+
}
|
|
706
|
+
if (snapshot === void 0) return prepareFailure("invalid-output", "program completion must be lossless JSON", remainingOutputBytes, maxOutputBytes);
|
|
707
|
+
if (jsonValueBytesUpTo(snapshot, remainingOutputBytes) === void 0) return outputLimit(maxOutputBytes);
|
|
708
|
+
return { value: encodeWorkerJson(snapshot) };
|
|
709
|
+
}
|
|
710
|
+
/** Build the fixed overflow fragment without carrying rejected variable bytes. */
|
|
711
|
+
function outputLimit(maxOutputBytes) {
|
|
712
|
+
return { error: {
|
|
713
|
+
kind: "output-limit",
|
|
714
|
+
message: `outer output exceeded ${maxOutputBytes} bytes`
|
|
715
|
+
} };
|
|
716
|
+
}
|
|
717
|
+
/** Admit one bounded failure message or replace it with the fixed overflow diagnostic. */
|
|
718
|
+
function prepareFailure(kind, message, remainingOutputBytes, maxOutputBytes) {
|
|
719
|
+
if (jsonStringBytesUpTo(message, remainingOutputBytes) === void 0) return outputLimit(maxOutputBytes);
|
|
720
|
+
return { error: {
|
|
721
|
+
kind,
|
|
722
|
+
message
|
|
723
|
+
} };
|
|
724
|
+
}
|
|
725
|
+
/**
|
|
726
|
+
* Prepare a thrown program value without sending an unbounded stack or
|
|
727
|
+
* string across the worker port.
|
|
728
|
+
* @param error - the value thrown by the program.
|
|
729
|
+
* @param remainingOutputBytes - exact bytes left after captured logs.
|
|
730
|
+
* @param maxOutputBytes - the configured cap named in an overflow diagnostic.
|
|
731
|
+
* @returns a bounded exception or fixed output-limit fragment.
|
|
732
|
+
*/
|
|
733
|
+
function prepareException(error, remainingOutputBytes, maxOutputBytes = remainingOutputBytes) {
|
|
734
|
+
let message;
|
|
735
|
+
try {
|
|
736
|
+
const detail = error instanceof CapturedError ? error.stack ?? error.message : error;
|
|
737
|
+
message = typeof detail === "string" ? detail : String(detail);
|
|
738
|
+
} catch {
|
|
739
|
+
message = "program threw an unrenderable value";
|
|
740
|
+
}
|
|
741
|
+
return prepareFailure("exception", message, remainingOutputBytes, maxOutputBytes);
|
|
742
|
+
}
|
|
743
|
+
/**
|
|
744
|
+
* Materialize the real error constructor declared by one namespace.
|
|
745
|
+
* @param descriptor - program-global class name and member-name property.
|
|
746
|
+
* @returns the constructor injected into the program and used for rejections.
|
|
747
|
+
*/
|
|
748
|
+
function makeBindingErrorClass(descriptor) {
|
|
749
|
+
return class BindingCallError extends CapturedError {
|
|
750
|
+
constructor(memberName, message) {
|
|
751
|
+
super(message);
|
|
752
|
+
defineBindingErrorField(this, "name", descriptor.name);
|
|
753
|
+
defineBindingErrorField(this, descriptor.memberNameProperty, memberName);
|
|
754
|
+
}
|
|
755
|
+
};
|
|
756
|
+
}
|
|
757
|
+
/** Create the namespace-specific rejection for one failed binding call. */
|
|
758
|
+
function bindingFailure(errorClass, memberName, message) {
|
|
759
|
+
return errorClass ? new errorClass(memberName, message) : new CapturedError(message);
|
|
760
|
+
}
|
|
761
|
+
/**
|
|
762
|
+
* Build each declared error class once so calls and `instanceof` share constructor identity.
|
|
763
|
+
* @param data - binding namespace declarations from the boot payload.
|
|
764
|
+
* @returns constructors keyed by their owning namespace global.
|
|
765
|
+
*/
|
|
766
|
+
function makeBindingErrorClasses(data) {
|
|
767
|
+
const classes = /* @__PURE__ */ new Map();
|
|
768
|
+
for (const namespace of data.namespaces) if (namespace.errorClass) classes.set(namespace.global, makeBindingErrorClass(namespace.errorClass));
|
|
769
|
+
return classes;
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Route host replies into the pending-call map: each reply settles its call
|
|
773
|
+
* at most once, and a reply for an unknown id (stray, or a duplicate answer
|
|
774
|
+
* to an id already settled) is ignored. Shared wiring between
|
|
775
|
+
* {@link runWorkerMain} and the tests that exercise {@link makeNamespaces}
|
|
776
|
+
* standalone.
|
|
777
|
+
* @param port - the port whose `message` events carry the replies.
|
|
778
|
+
* @param pending - the id-keyed map of unsettled binding calls.
|
|
779
|
+
*/
|
|
780
|
+
function wireReplies(port, pending) {
|
|
781
|
+
port.on("message", (message) => {
|
|
782
|
+
const entry = pending.get(message.id);
|
|
783
|
+
if (!entry) return;
|
|
784
|
+
pending.delete(message.id);
|
|
785
|
+
if (message.ok) {
|
|
786
|
+
const value = decodeWorkerJson(message.value);
|
|
787
|
+
if (value === void 0) entry.reject(new CapturedError("binding resolution must be lossless JSON"));
|
|
788
|
+
else entry.resolve(value);
|
|
789
|
+
} else entry.reject(new CapturedError(message.message));
|
|
790
|
+
});
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* Build the binding namespace objects the program sees: one null-prototype global per
|
|
794
|
+
* namespace, each declared name an own enumerable async function that bridges over the port
|
|
795
|
+
* (`__proto__`/`constructor`/`toString` are ordinary keys, never prototype collisions).
|
|
796
|
+
* Lossy arguments reject before posting; clone failures and host failure
|
|
797
|
+
* replies reject only the corresponding call.
|
|
798
|
+
*
|
|
799
|
+
* @param data - the boot payload's namespace declarations (globals + names).
|
|
800
|
+
* @param port - the port binding calls are posted to.
|
|
801
|
+
* @param pending - the id-keyed map each posted call parks its handles in.
|
|
802
|
+
* @param nextId - the shared mutable id counter (worker-issued correlation ids).
|
|
803
|
+
* @param errorClasses - per-namespace constructors shared with program globals.
|
|
804
|
+
* @returns one namespace object per declaration, in declaration order.
|
|
805
|
+
*/
|
|
806
|
+
function makeNamespaces(data, port, pending, nextId, errorClasses = makeBindingErrorClasses(data)) {
|
|
807
|
+
return data.namespaces.map(({ global, names }) => {
|
|
808
|
+
const errorClass = errorClasses.get(global);
|
|
809
|
+
const namespace = Object.create(null);
|
|
810
|
+
for (const name of names) Object.defineProperty(namespace, name, {
|
|
811
|
+
enumerable: true,
|
|
812
|
+
value: (args) => {
|
|
813
|
+
let detached;
|
|
814
|
+
try {
|
|
815
|
+
detached = snapshotCodeJsonValue(args);
|
|
816
|
+
} catch {
|
|
817
|
+
detached = void 0;
|
|
818
|
+
}
|
|
819
|
+
if (detached === void 0) return Promise.reject(bindingFailure(errorClass, name, "binding arguments must be lossless JSON"));
|
|
820
|
+
return new Promise((resolve, reject) => {
|
|
821
|
+
const id = nextId.value++;
|
|
822
|
+
pending.set(id, {
|
|
823
|
+
resolve,
|
|
824
|
+
reject: (error) => {
|
|
825
|
+
reject(bindingFailure(errorClass, name, error.message));
|
|
826
|
+
}
|
|
827
|
+
});
|
|
828
|
+
try {
|
|
829
|
+
port.postMessage({
|
|
830
|
+
type: "call",
|
|
831
|
+
id,
|
|
832
|
+
global,
|
|
833
|
+
name,
|
|
834
|
+
args: encodeWorkerJson(detached)
|
|
835
|
+
});
|
|
836
|
+
} catch (error) {
|
|
837
|
+
pending.delete(id);
|
|
838
|
+
reject(bindingFailure(errorClass, name, `binding arguments must be structured-cloneable: ${error instanceof CapturedError ? error.message : String(error)}`));
|
|
839
|
+
}
|
|
840
|
+
});
|
|
841
|
+
}
|
|
842
|
+
});
|
|
843
|
+
return namespace;
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
/**
|
|
847
|
+
* Run one strict async-function body, allowing top-level `await` and `return`, and post exactly
|
|
848
|
+
* one terminal {@link DoneMessage}; a thrown program error becomes its `error` field.
|
|
849
|
+
* @param port - host message port or test double.
|
|
850
|
+
* @param data - the boot payload the host sent.
|
|
851
|
+
* @param streams - stdout/stderr objects captured as program logs.
|
|
852
|
+
* @returns after posting the done message.
|
|
853
|
+
*/
|
|
854
|
+
async function runWorkerMain(port, data, streams) {
|
|
855
|
+
const logs = new LogBuffer(data.maxOutputBytes, (text) => {
|
|
856
|
+
port.postMessage({
|
|
857
|
+
type: "log",
|
|
858
|
+
text
|
|
859
|
+
});
|
|
860
|
+
}, () => {
|
|
861
|
+
port.postMessage({ type: "output-limit" });
|
|
862
|
+
});
|
|
863
|
+
captureStreamWrites(logs, streams.stdout);
|
|
864
|
+
captureStreamWrites(logs, streams.stderr);
|
|
865
|
+
const pending = /* @__PURE__ */ new Map();
|
|
866
|
+
wireReplies(port, pending);
|
|
867
|
+
const nextId = { value: 1 };
|
|
868
|
+
const errorClasses = makeBindingErrorClasses(data);
|
|
869
|
+
const namespaces = makeNamespaces(data, port, pending, nextId, errorClasses);
|
|
870
|
+
const errorClassParameters = [];
|
|
871
|
+
const errorClassValues = [];
|
|
872
|
+
for (const namespace of data.namespaces) {
|
|
873
|
+
if (!namespace.errorClass) continue;
|
|
874
|
+
errorClassParameters.push(namespace.errorClass.name);
|
|
875
|
+
const errorClass = errorClasses.get(namespace.global);
|
|
876
|
+
/* v8 ignore next -- makeBindingErrorClasses covers every declaration in the same data. */
|
|
877
|
+
if (!errorClass) throw new CapturedError(`missing binding error class for ${namespace.global}`);
|
|
878
|
+
errorClassValues.push(errorClass);
|
|
879
|
+
}
|
|
880
|
+
const consoleShim = makeConsoleShim(logs);
|
|
881
|
+
let done;
|
|
882
|
+
try {
|
|
883
|
+
/* v8 ignore next -- the arrow exists only to reach the AsyncFunction constructor; it is never invoked. */
|
|
884
|
+
const AsyncFunction = (async () => {}).constructor;
|
|
885
|
+
done = {
|
|
886
|
+
type: "done",
|
|
887
|
+
...prepareCompletion(await new AsyncFunction(...data.namespaces.map((namespace) => namespace.global), ...errorClassParameters, "console", `'use strict';\n${data.code}`)(...namespaces, ...errorClassValues, consoleShim), logs.remainingOutputBytes(), data.maxOutputBytes)
|
|
888
|
+
};
|
|
889
|
+
} catch (error) {
|
|
890
|
+
done = {
|
|
891
|
+
type: "done",
|
|
892
|
+
...prepareException(error, logs.remainingOutputBytes(), data.maxOutputBytes)
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
port.postMessage(done);
|
|
896
|
+
}
|
|
897
|
+
//#endregion
|
|
898
|
+
//#region lib/types/worker.js
|
|
899
|
+
/**
|
|
900
|
+
* Spawn-only worker entrypoint over {@link runWorkerMain}. Executable logic stays in
|
|
901
|
+
* `bootstrap.ts` for in-process coverage; real-worker tests cover this glue.
|
|
902
|
+
* @module @hydraharness/harness-code-runtime-worker-thread/src/worker
|
|
903
|
+
*/
|
|
904
|
+
if (!node_worker_threads.parentPort) throw new Error("hydra-code-runtime-worker-thread: worker entry loaded outside a worker thread");
|
|
905
|
+
runWorkerMain(node_worker_threads.parentPort, node_worker_threads.workerData, {
|
|
906
|
+
stdout: process.stdout,
|
|
907
|
+
stderr: process.stderr
|
|
908
|
+
});
|
|
909
|
+
//#endregion
|