circuitjson-toolkit 1.4.2 → 1.4.3
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/docs/api.md +19 -3
- package/docs/model-format.md +4 -4
- package/package.json +1 -1
- package/src/core/SelfAdjustingComputation.mjs +89 -100
- package/src/core/context/CircuitJsonExtensionBoundary.mjs +1 -1
- package/src/core/context/StructuredCloneTextAccounting.mjs +38 -0
- package/src/core/worker/NativeParserWorkerTransport.mjs +201 -0
- package/src/core/worker/ParserWorkerClient.mjs +18 -20
- package/src/core/worker/WorkerBinaryData.mjs +201 -0
- package/src/core/worker/WorkerRequestData.mjs +64 -213
- package/src/core/worker/WorkerResponseData.mjs +9 -2
- package/src/core/worker/WorkerResultShape.mjs +6 -1
package/docs/api.md
CHANGED
|
@@ -85,11 +85,12 @@ make the common shape smaller.
|
|
|
85
85
|
|
|
86
86
|
Validated source documents own selected extension values as one deeply
|
|
87
87
|
immutable, descriptor-safe snapshot. This uses a distinct 128 MiB payload and
|
|
88
|
-
|
|
88
|
+
8,000,000-item ceiling so large native renderer graphs do not inherit the
|
|
89
89
|
compact metadata limit. The shared worker protocol applies the same ownership
|
|
90
90
|
boundary after structured cloning. Canonical standalone documents retain a
|
|
91
|
-
250 MB byte ceiling; exact multi-document projects use a
|
|
92
|
-
ceiling with independent 250 MB document
|
|
91
|
+
10,000,000-value and 250 MB byte ceiling; exact multi-document projects use a
|
|
92
|
+
16,000,000-value and 256 MiB aggregate ceiling with independent 250 MB document
|
|
93
|
+
and project-metadata limits. Binary
|
|
93
94
|
values preserve their `ArrayBuffer`/typed-view shape behind defensive copy
|
|
94
95
|
access, so payload bytes do not inflate into one JavaScript number per byte.
|
|
95
96
|
|
|
@@ -318,6 +319,15 @@ The parser subpath additionally exports `ParserWorkerClient`,
|
|
|
318
319
|
`ToolkitWorkerProtocol`, and the `TOOLKIT_WORKER_PROTOCOL` version constant for
|
|
319
320
|
hosts that own a custom worker lifecycle.
|
|
320
321
|
|
|
322
|
+
`ParserWorkerClient.fromWorkerUrl(url)` creates a lazy module-worker client
|
|
323
|
+
that privately owns its native worker and received result graphs. Native
|
|
324
|
+
message provenance avoids repeated binary brand probing on ordinary records;
|
|
325
|
+
all result shape, model, depth, count, and byte limits still apply. Shared and
|
|
326
|
+
resizable buffers remain isolated. `new ParserWorkerClient({ createWorker })`
|
|
327
|
+
continues to fully inspect custom-worker results, including genuine events
|
|
328
|
+
whose payloads an earlier listener may have changed. Replacing the host Worker
|
|
329
|
+
constructor also retains that defensive path.
|
|
330
|
+
|
|
321
331
|
### `ToolkitAsset.measure(asset)`
|
|
322
332
|
|
|
323
333
|
Returns the exact resident payload byte length without copying. Binary lengths
|
|
@@ -711,3 +721,9 @@ circuitjson-toolkit/testing
|
|
|
711
721
|
circuitjson-toolkit/workers/parser.worker.mjs
|
|
712
722
|
circuitjson-toolkit/styles/renderers.css
|
|
713
723
|
```
|
|
724
|
+
|
|
725
|
+
Worker payload preparation uses captured array/view brand checks before raw
|
|
726
|
+
buffer probes. Short strings are counted directly in UTF-8, including surrogate
|
|
727
|
+
pairs and replacement characters, without allocating encoded byte buffers.
|
|
728
|
+
Cross-realm buffers, changed prototypes, and bounded binary ownership retain
|
|
729
|
+
the same validation rules.
|
package/docs/model-format.md
CHANGED
|
@@ -102,12 +102,12 @@ must not duplicate the CircuitJSON model or keep a renamed renderer model.
|
|
|
102
102
|
Default parsing excludes raw/base64/full-payload graphs.
|
|
103
103
|
|
|
104
104
|
When a caller explicitly selects a native extension, the toolkit captures and
|
|
105
|
-
freezes that graph once under a distinct ceiling of
|
|
105
|
+
freezes that graph once under a distinct ceiling of 8,000,000 structured items
|
|
106
106
|
and 128 MiB of string or binary content. The ceiling is shared across the
|
|
107
107
|
selected namespace and applies equally after worker transfer. Canonical
|
|
108
|
-
standalone documents allow the extension plus envelope overhead within
|
|
109
|
-
million result values and 250 MB. Exact project envelopes allow
|
|
110
|
-
aggregate values and 256 MiB, while each document stays within
|
|
108
|
+
standalone documents allow the extension plus envelope overhead within ten
|
|
109
|
+
million result values and 250 MB. Exact project envelopes allow sixteen million
|
|
110
|
+
aggregate values and 256 MiB, while each document stays within ten million
|
|
111
111
|
values and 250 MB and non-document project metadata stays within two million
|
|
112
112
|
values and 250 MB. Reused graphs are charged independently to each local scope,
|
|
113
113
|
and repeated accounting work is itself bounded by the aggregate project
|
package/package.json
CHANGED
|
@@ -17,15 +17,9 @@ export class SelfAdjustingComputation {
|
|
|
17
17
|
/** @type {Set<string> | null} */
|
|
18
18
|
#affectedComputations
|
|
19
19
|
|
|
20
|
-
/** @type {(value: object, path: PropertyKey[]) => boolean} */
|
|
20
|
+
/** @type {((value: object, path: PropertyKey[]) => boolean) | null} */
|
|
21
21
|
#isAtomic
|
|
22
22
|
|
|
23
|
-
/** @type {Map<symbol, number>} */
|
|
24
|
-
#symbolIds
|
|
25
|
-
|
|
26
|
-
/** @type {number} */
|
|
27
|
-
#nextSymbolId
|
|
28
|
-
|
|
29
23
|
/**
|
|
30
24
|
* @param {{ isAtomic?: (value: object, path: PropertyKey[]) => boolean }} [options] Dependency-tracking options.
|
|
31
25
|
*/
|
|
@@ -36,11 +30,7 @@ export class SelfAdjustingComputation {
|
|
|
36
30
|
this.#propagationInput = null
|
|
37
31
|
this.#affectedComputations = null
|
|
38
32
|
this.#isAtomic =
|
|
39
|
-
typeof options.isAtomic === 'function'
|
|
40
|
-
? options.isAtomic
|
|
41
|
-
: () => false
|
|
42
|
-
this.#symbolIds = new Map()
|
|
43
|
-
this.#nextSymbolId = 1
|
|
33
|
+
typeof options.isAtomic === 'function' ? options.isAtomic : null
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
/**
|
|
@@ -264,16 +254,14 @@ export class SelfAdjustingComputation {
|
|
|
264
254
|
* @returns {{ dependencies: object[], value: any }} Successful trace.
|
|
265
255
|
*/
|
|
266
256
|
#trace(input, computation) {
|
|
267
|
-
const dependencies =
|
|
257
|
+
const dependencies = []
|
|
268
258
|
const proxyCache = new WeakMap()
|
|
269
259
|
const proxyMetadata = new WeakMap()
|
|
270
|
-
const trackedInput = this.#createProxy(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
proxyMetadata
|
|
276
|
-
})
|
|
260
|
+
const trackedInput = this.#createProxy(
|
|
261
|
+
{ dependencies, proxyCache, proxyMetadata },
|
|
262
|
+
input,
|
|
263
|
+
{ path: [], children: null, observations: null }
|
|
264
|
+
)
|
|
277
265
|
const trackedValue = computation(trackedInput)
|
|
278
266
|
if (SelfAdjustingComputation.#isPromiseLike(trackedValue)) {
|
|
279
267
|
throw new TypeError(
|
|
@@ -287,39 +275,44 @@ export class SelfAdjustingComputation {
|
|
|
287
275
|
? proxyMetadata.get(trackedValue)
|
|
288
276
|
: null
|
|
289
277
|
if (metadata) {
|
|
290
|
-
this.#record(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
278
|
+
this.#record(
|
|
279
|
+
dependencies,
|
|
280
|
+
metadata.pathNode,
|
|
281
|
+
'value',
|
|
282
|
+
metadata.target
|
|
283
|
+
)
|
|
295
284
|
}
|
|
296
285
|
|
|
297
286
|
return {
|
|
298
|
-
|
|
287
|
+
// Later reads of a returned container must not rewrite this trace.
|
|
288
|
+
dependencies: dependencies.map((dependency) => ({ ...dependency })),
|
|
299
289
|
value: metadata?.target ?? trackedValue
|
|
300
290
|
}
|
|
301
291
|
}
|
|
302
292
|
|
|
303
293
|
/**
|
|
304
294
|
* Creates a read-tracking proxy for one traversable input container.
|
|
305
|
-
* @param {{
|
|
295
|
+
* @param {{ dependencies: object[], proxyCache: WeakMap<object, Map<object, object>>, proxyMetadata: WeakMap<object, { target: object, pathNode: object }> }} context Shared trace context.
|
|
296
|
+
* @param {object} target Traversable raw input container.
|
|
297
|
+
* @param {object} pathNode Interned input path in this trace.
|
|
306
298
|
* @returns {object} Read-tracking proxy.
|
|
307
299
|
*/
|
|
308
|
-
#createProxy(context) {
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
return
|
|
300
|
+
#createProxy(context, target, pathNode) {
|
|
301
|
+
const cachedByPath = context.proxyCache.get(target)
|
|
302
|
+
const cached = cachedByPath?.get(pathNode)
|
|
303
|
+
if (cached) {
|
|
304
|
+
return cached
|
|
313
305
|
}
|
|
314
306
|
|
|
315
|
-
const
|
|
307
|
+
const proxyContext = { ...context, pathNode }
|
|
308
|
+
const proxy = new Proxy(target, {
|
|
316
309
|
get: (target, property) =>
|
|
317
|
-
this.#readProperty(
|
|
310
|
+
this.#readProperty(proxyContext, target, property),
|
|
318
311
|
has: (target, property) =>
|
|
319
|
-
this.#readPresence(
|
|
320
|
-
ownKeys: (target) => this.#readKeys(
|
|
312
|
+
this.#readPresence(proxyContext, target, property),
|
|
313
|
+
ownKeys: (target) => this.#readKeys(proxyContext, target),
|
|
321
314
|
getOwnPropertyDescriptor: (target, property) =>
|
|
322
|
-
this.#readDescriptor(
|
|
315
|
+
this.#readDescriptor(proxyContext, target, property),
|
|
323
316
|
set: () => this.#rejectWrite(),
|
|
324
317
|
defineProperty: () => this.#rejectWrite(),
|
|
325
318
|
deleteProperty: () => this.#rejectWrite(),
|
|
@@ -327,13 +320,13 @@ export class SelfAdjustingComputation {
|
|
|
327
320
|
preventExtensions: () => this.#rejectWrite()
|
|
328
321
|
})
|
|
329
322
|
const nextCachedByPath = cachedByPath || new Map()
|
|
330
|
-
nextCachedByPath.set(
|
|
323
|
+
nextCachedByPath.set(pathNode, proxy)
|
|
331
324
|
if (!cachedByPath) {
|
|
332
|
-
context.proxyCache.set(
|
|
325
|
+
context.proxyCache.set(target, nextCachedByPath)
|
|
333
326
|
}
|
|
334
327
|
context.proxyMetadata.set(proxy, {
|
|
335
|
-
target
|
|
336
|
-
|
|
328
|
+
target,
|
|
329
|
+
pathNode
|
|
337
330
|
})
|
|
338
331
|
return proxy
|
|
339
332
|
}
|
|
@@ -347,21 +340,18 @@ export class SelfAdjustingComputation {
|
|
|
347
340
|
*/
|
|
348
341
|
#readProperty(context, target, property) {
|
|
349
342
|
const value = Reflect.get(target, property, target)
|
|
350
|
-
const
|
|
351
|
-
if (this.#isTraversable(value, path)) {
|
|
352
|
-
this.#record(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
343
|
+
const pathNode = this.#childPath(context.pathNode, property)
|
|
344
|
+
if (this.#isTraversable(value, pathNode.path)) {
|
|
345
|
+
this.#record(
|
|
346
|
+
context.dependencies,
|
|
347
|
+
pathNode,
|
|
348
|
+
'kind',
|
|
349
|
+
SelfAdjustingComputation.#valueKind(value)
|
|
350
|
+
)
|
|
351
|
+
return this.#createProxy(context, value, pathNode)
|
|
358
352
|
}
|
|
359
353
|
|
|
360
|
-
this.#record(context.dependencies,
|
|
361
|
-
type: 'value',
|
|
362
|
-
path,
|
|
363
|
-
expected: value
|
|
364
|
-
})
|
|
354
|
+
this.#record(context.dependencies, pathNode, 'value', value)
|
|
365
355
|
return value
|
|
366
356
|
}
|
|
367
357
|
|
|
@@ -373,13 +363,9 @@ export class SelfAdjustingComputation {
|
|
|
373
363
|
* @returns {boolean} Whether the property exists.
|
|
374
364
|
*/
|
|
375
365
|
#readPresence(context, target, property) {
|
|
376
|
-
const
|
|
366
|
+
const pathNode = this.#childPath(context.pathNode, property)
|
|
377
367
|
const expected = Reflect.has(target, property)
|
|
378
|
-
this.#record(context.dependencies,
|
|
379
|
-
type: 'has',
|
|
380
|
-
path,
|
|
381
|
-
expected
|
|
382
|
-
})
|
|
368
|
+
this.#record(context.dependencies, pathNode, 'has', expected)
|
|
383
369
|
return expected
|
|
384
370
|
}
|
|
385
371
|
|
|
@@ -391,11 +377,7 @@ export class SelfAdjustingComputation {
|
|
|
391
377
|
*/
|
|
392
378
|
#readKeys(context, target) {
|
|
393
379
|
const expected = Reflect.ownKeys(target)
|
|
394
|
-
this.#record(context.dependencies,
|
|
395
|
-
type: 'keys',
|
|
396
|
-
path: [...context.path],
|
|
397
|
-
expected
|
|
398
|
-
})
|
|
380
|
+
this.#record(context.dependencies, context.pathNode, 'keys', expected)
|
|
399
381
|
return expected
|
|
400
382
|
}
|
|
401
383
|
|
|
@@ -408,11 +390,12 @@ export class SelfAdjustingComputation {
|
|
|
408
390
|
*/
|
|
409
391
|
#readDescriptor(context, target, property) {
|
|
410
392
|
const descriptor = Reflect.getOwnPropertyDescriptor(target, property)
|
|
411
|
-
this.#record(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
393
|
+
this.#record(
|
|
394
|
+
context.dependencies,
|
|
395
|
+
this.#childPath(context.pathNode, property),
|
|
396
|
+
'descriptor',
|
|
397
|
+
SelfAdjustingComputation.#descriptorState(descriptor)
|
|
398
|
+
)
|
|
416
399
|
return descriptor
|
|
417
400
|
}
|
|
418
401
|
|
|
@@ -424,23 +407,50 @@ export class SelfAdjustingComputation {
|
|
|
424
407
|
*/
|
|
425
408
|
#isTraversable(value, path) {
|
|
426
409
|
if (!value || typeof value !== 'object') return false
|
|
427
|
-
if (this.#isAtomic(value, [...path])) return false
|
|
410
|
+
if (this.#isAtomic?.(value, [...path])) return false
|
|
428
411
|
if (Array.isArray(value)) return true
|
|
429
412
|
const prototype = Reflect.getPrototypeOf(value)
|
|
430
413
|
return prototype === Object.prototype || prototype === null
|
|
431
414
|
}
|
|
432
415
|
|
|
433
416
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* @param {
|
|
417
|
+
* Interns one child path using property identity without encoding its ancestors.
|
|
418
|
+
* Path and proxy caches belong to one trace and are never reused by a later snapshot.
|
|
419
|
+
* @param {object} parent Parent path node.
|
|
420
|
+
* @param {PropertyKey} property Observed property key.
|
|
421
|
+
* @returns {object} Canonical child path node for this trace.
|
|
422
|
+
*/
|
|
423
|
+
#childPath(parent, property) {
|
|
424
|
+
const previous = parent.children?.get(property)
|
|
425
|
+
if (previous) return previous
|
|
426
|
+
const child = {
|
|
427
|
+
path: [...parent.path, property],
|
|
428
|
+
children: null,
|
|
429
|
+
observations: null
|
|
430
|
+
}
|
|
431
|
+
parent.children ??= new Map()
|
|
432
|
+
parent.children.set(property, child)
|
|
433
|
+
return child
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Stores the last observation of each dependency type at an interned path.
|
|
438
|
+
* @param {object[]} dependencies Dependency registry in first-observed order.
|
|
439
|
+
* @param {object} pathNode Interned path node.
|
|
440
|
+
* @param {string} type Dependency comparison category.
|
|
441
|
+
* @param {any} expected Most recently observed value or structural state.
|
|
437
442
|
* @returns {void}
|
|
438
443
|
*/
|
|
439
|
-
#record(dependencies,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
+
#record(dependencies, pathNode, type, expected) {
|
|
445
|
+
const previous = pathNode.observations?.get(type)
|
|
446
|
+
if (previous) {
|
|
447
|
+
previous.expected = expected
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
const dependency = { type, path: pathNode.path, expected }
|
|
451
|
+
pathNode.observations ??= new Map()
|
|
452
|
+
pathNode.observations.set(type, dependency)
|
|
453
|
+
dependencies.push(dependency)
|
|
444
454
|
}
|
|
445
455
|
|
|
446
456
|
/**
|
|
@@ -530,27 +540,6 @@ export class SelfAdjustingComputation {
|
|
|
530
540
|
return { found: true, value }
|
|
531
541
|
}
|
|
532
542
|
|
|
533
|
-
/**
|
|
534
|
-
* Creates a collision-free string key for a property path.
|
|
535
|
-
* @param {PropertyKey[]} path Input path.
|
|
536
|
-
* @returns {string} Registry key.
|
|
537
|
-
*/
|
|
538
|
-
#pathKey(path) {
|
|
539
|
-
return path
|
|
540
|
-
.map((property) => {
|
|
541
|
-
if (typeof property === 'symbol') {
|
|
542
|
-
if (!this.#symbolIds.has(property)) {
|
|
543
|
-
this.#symbolIds.set(property, this.#nextSymbolId)
|
|
544
|
-
this.#nextSymbolId += 1
|
|
545
|
-
}
|
|
546
|
-
return 'y' + this.#symbolIds.get(property)
|
|
547
|
-
}
|
|
548
|
-
const text = String(property)
|
|
549
|
-
return 's' + text.length + ':' + text
|
|
550
|
-
})
|
|
551
|
-
.join('|')
|
|
552
|
-
}
|
|
553
|
-
|
|
554
543
|
/**
|
|
555
544
|
* Rejects mutation through a tracked snapshot.
|
|
556
545
|
* @returns {never}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const TEXT_ACCOUNTING_CHUNK_CHARACTERS = 64 * 1_024
|
|
2
|
+
const SHORT_TEXT_CHARACTERS = 256
|
|
2
3
|
const STRING_CHAR_CODE_AT = String.prototype.charCodeAt
|
|
3
4
|
const STRING_SLICE = String.prototype.slice
|
|
4
5
|
const TEXT_ENCODER = new TextEncoder()
|
|
@@ -17,6 +18,14 @@ export class StructuredCloneTextAccounting {
|
|
|
17
18
|
*/
|
|
18
19
|
static *reserve(value, state, operations) {
|
|
19
20
|
if (state.maxBytes === Number.MAX_SAFE_INTEGER) return
|
|
21
|
+
if (value.length <= SHORT_TEXT_CHARACTERS) {
|
|
22
|
+
const length = StructuredCloneTextAccounting.#shortByteLength(value)
|
|
23
|
+
if (state.bytes + length > state.maxBytes) {
|
|
24
|
+
throw new TypeError(`${state.label} is too large.`)
|
|
25
|
+
}
|
|
26
|
+
operations.reserve(length)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
20
29
|
let length = 0
|
|
21
30
|
let offset = 0
|
|
22
31
|
while (offset < value.length) {
|
|
@@ -49,6 +58,35 @@ export class StructuredCloneTextAccounting {
|
|
|
49
58
|
}
|
|
50
59
|
operations.reserve(length)
|
|
51
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Counts bounded short text without allocating an encoded byte buffer.
|
|
64
|
+
* Lone surrogates consume the three UTF-8 bytes of a replacement character.
|
|
65
|
+
* @param {string} value Short immutable text.
|
|
66
|
+
* @returns {number} UTF-8 byte length.
|
|
67
|
+
*/
|
|
68
|
+
static #shortByteLength(value) {
|
|
69
|
+
let length = 0
|
|
70
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
71
|
+
const code = STRING_CHAR_CODE_AT.call(value, index)
|
|
72
|
+
if (code <= 0x7f) {
|
|
73
|
+
length += 1
|
|
74
|
+
} else if (code <= 0x7ff) {
|
|
75
|
+
length += 2
|
|
76
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
77
|
+
const next = STRING_CHAR_CODE_AT.call(value, index + 1)
|
|
78
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
79
|
+
length += 4
|
|
80
|
+
index += 1
|
|
81
|
+
} else {
|
|
82
|
+
length += 3
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
length += 3
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return length
|
|
89
|
+
}
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
Object.freeze(StructuredCloneTextAccounting.prototype)
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
|
|
2
|
+
|
|
3
|
+
const REFLECT_APPLY = Reflect.apply
|
|
4
|
+
const REFLECT_CONSTRUCT = Reflect.construct
|
|
5
|
+
const REFLECT_OWN_KEYS = Reflect.ownKeys
|
|
6
|
+
const GET_PROTOTYPE = Object.getPrototypeOf
|
|
7
|
+
const GET_DESCRIPTORS = Object.getOwnPropertyDescriptors
|
|
8
|
+
const HAS_OWN = Object.hasOwn
|
|
9
|
+
const OBJECT_PROTOTYPE = Object.prototype
|
|
10
|
+
const WEAK_MAP_GET = WeakMap.prototype.get
|
|
11
|
+
const WEAK_MAP_SET = WeakMap.prototype.set
|
|
12
|
+
const WEAK_MAP_DELETE = WeakMap.prototype.delete
|
|
13
|
+
const WORKER_CONSTRUCTOR = globalThis.Worker
|
|
14
|
+
const WORKER_POST = WORKER_CONSTRUCTOR?.prototype?.postMessage
|
|
15
|
+
const WORKER_TERMINATE = WORKER_CONSTRUCTOR?.prototype?.terminate
|
|
16
|
+
const ADD_EVENT_LISTENER = EventTarget.prototype.addEventListener
|
|
17
|
+
const REMOVE_EVENT_LISTENER = EventTarget.prototype.removeEventListener
|
|
18
|
+
const MESSAGE_DATA = Object.getOwnPropertyDescriptor(
|
|
19
|
+
MessageEvent.prototype,
|
|
20
|
+
'data'
|
|
21
|
+
)?.get
|
|
22
|
+
const EVENT_CURRENT_TARGET = Object.getOwnPropertyDescriptor(
|
|
23
|
+
Event.prototype,
|
|
24
|
+
'currentTarget'
|
|
25
|
+
)?.get
|
|
26
|
+
const EVENT_TRUSTED =
|
|
27
|
+
Object.getOwnPropertyDescriptor(new Event('message'), 'isTrusted')?.get ||
|
|
28
|
+
Object.getOwnPropertyDescriptor(Event.prototype, 'isTrusted')?.get
|
|
29
|
+
const ERROR_DATA =
|
|
30
|
+
typeof ErrorEvent === 'function'
|
|
31
|
+
? Object.getOwnPropertyDescriptor(ErrorEvent.prototype, 'error')?.get
|
|
32
|
+
: null
|
|
33
|
+
const RECEIVED_RESULTS = new WeakMap()
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Keeps native workers and their unexposed structured-clone results private.
|
|
37
|
+
* A custom factory cannot mint the one-use result capabilities created here.
|
|
38
|
+
*/
|
|
39
|
+
export class NativeParserWorkerTransport {
|
|
40
|
+
#worker
|
|
41
|
+
#listeners = new Map()
|
|
42
|
+
#nativeListeners = new Map()
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Constructs an isolated transport using the captured native constructor.
|
|
46
|
+
* @param {string | URL} url Worker module URL.
|
|
47
|
+
*/
|
|
48
|
+
constructor(url) {
|
|
49
|
+
this.#worker = REFLECT_CONSTRUCT(WORKER_CONSTRUCTOR, [
|
|
50
|
+
url,
|
|
51
|
+
{ type: 'module' }
|
|
52
|
+
])
|
|
53
|
+
for (const type of ['message', 'error', 'messageerror']) {
|
|
54
|
+
const listener = (event) => this.#receive(type, event)
|
|
55
|
+
this.#nativeListeners.set(type, listener)
|
|
56
|
+
REFLECT_APPLY(ADD_EVENT_LISTENER, this.#worker, [type, listener])
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Uses private native transport only while the captured host is unchanged.
|
|
62
|
+
* Replaced or emulated Worker constructors retain generic validation.
|
|
63
|
+
* @param {string | URL} url Worker module URL.
|
|
64
|
+
* @returns {object} Browser-compatible worker transport.
|
|
65
|
+
*/
|
|
66
|
+
static create(url) {
|
|
67
|
+
const Constructor = globalThis.Worker
|
|
68
|
+
if (
|
|
69
|
+
Constructor !== WORKER_CONSTRUCTOR ||
|
|
70
|
+
typeof WORKER_POST !== 'function' ||
|
|
71
|
+
typeof WORKER_TERMINATE !== 'function' ||
|
|
72
|
+
!MESSAGE_DATA ||
|
|
73
|
+
!EVENT_CURRENT_TARGET ||
|
|
74
|
+
!EVENT_TRUSTED
|
|
75
|
+
) {
|
|
76
|
+
return REFLECT_CONSTRUCT(Constructor, [url, { type: 'module' }])
|
|
77
|
+
}
|
|
78
|
+
return new NativeParserWorkerTransport(url)
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Consumes an opaque result exactly once. The returned data has never been
|
|
83
|
+
* exposed outside this transport; exposing it here destroys its capability.
|
|
84
|
+
* @param {unknown} token Result candidate.
|
|
85
|
+
* @returns {{ value: unknown } | null} Privately received data, if proven.
|
|
86
|
+
*/
|
|
87
|
+
static consumeResult(token) {
|
|
88
|
+
const received = REFLECT_APPLY(WEAK_MAP_GET, RECEIVED_RESULTS, [token])
|
|
89
|
+
if (!received) return null
|
|
90
|
+
REFLECT_APPLY(WEAK_MAP_DELETE, RECEIVED_RESULTS, [token])
|
|
91
|
+
return received
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** @param {string} type Event type. @param {Function} listener Listener. @returns {void} */
|
|
95
|
+
addEventListener(type, listener) {
|
|
96
|
+
if (!this.#listeners.has(type)) this.#listeners.set(type, new Set())
|
|
97
|
+
this.#listeners.get(type).add(listener)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** @param {string} type Event type. @param {Function} listener Listener. @returns {void} */
|
|
101
|
+
removeEventListener(type, listener) {
|
|
102
|
+
this.#listeners.get(type)?.delete(listener)
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/** @param {unknown} message Request. @param {Transferable[]} [transfer] Transfer list. @returns {void} */
|
|
106
|
+
postMessage(message, transfer = []) {
|
|
107
|
+
REFLECT_APPLY(WORKER_POST, this.#worker, [message, transfer])
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Detaches native listeners and terminates the private worker. @returns {void} */
|
|
111
|
+
terminate() {
|
|
112
|
+
for (const [type, listener] of this.#nativeListeners) {
|
|
113
|
+
REFLECT_APPLY(REMOVE_EVENT_LISTENER, this.#worker, [type, listener])
|
|
114
|
+
}
|
|
115
|
+
this.#nativeListeners.clear()
|
|
116
|
+
this.#listeners.clear()
|
|
117
|
+
REFLECT_APPLY(WORKER_TERMINATE, this.#worker, [])
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Hides the native target and result graph before invoking any consumer.
|
|
122
|
+
* Synthetic events and invalid envelopes never receive a capability.
|
|
123
|
+
* @param {string} type Event type.
|
|
124
|
+
* @param {Event} event Native event.
|
|
125
|
+
* @returns {void}
|
|
126
|
+
*/
|
|
127
|
+
#receive(type, event) {
|
|
128
|
+
let forwarded = {}
|
|
129
|
+
if (type === 'message') {
|
|
130
|
+
const data = REFLECT_APPLY(MESSAGE_DATA, event, [])
|
|
131
|
+
forwarded = { data }
|
|
132
|
+
if (
|
|
133
|
+
REFLECT_APPLY(EVENT_TRUSTED, event, []) === true &&
|
|
134
|
+
REFLECT_APPLY(EVENT_CURRENT_TARGET, event, []) === this.#worker
|
|
135
|
+
) {
|
|
136
|
+
try {
|
|
137
|
+
const message =
|
|
138
|
+
NativeParserWorkerTransport.#resultMessage(data)
|
|
139
|
+
if (message) {
|
|
140
|
+
const token = Object.freeze(Object.create(null))
|
|
141
|
+
REFLECT_APPLY(WEAK_MAP_SET, RECEIVED_RESULTS, [
|
|
142
|
+
token,
|
|
143
|
+
{ value: message.value }
|
|
144
|
+
])
|
|
145
|
+
message.value = token
|
|
146
|
+
forwarded = { data: message }
|
|
147
|
+
}
|
|
148
|
+
} catch {
|
|
149
|
+
// The ordinary client boundary reports malformed envelopes.
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
} else if (type === 'error') {
|
|
153
|
+
forwarded = {
|
|
154
|
+
error: ERROR_DATA ? REFLECT_APPLY(ERROR_DATA, event, []) : null
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
for (const listener of this.#listeners.get(type) || []) {
|
|
158
|
+
REFLECT_APPLY(listener, undefined, [forwarded])
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Captures only exact native result headers without calling replaceable
|
|
164
|
+
* exported helpers that could introduce caller-owned values into the graph.
|
|
165
|
+
* @param {unknown} data Privately received native event data.
|
|
166
|
+
* @returns {object | null} Result envelope safe to hide behind a capability.
|
|
167
|
+
*/
|
|
168
|
+
static #resultMessage(data) {
|
|
169
|
+
if (!data || typeof data !== 'object') return null
|
|
170
|
+
const prototype = GET_PROTOTYPE(data)
|
|
171
|
+
if (prototype !== OBJECT_PROTOTYPE && prototype !== null) return null
|
|
172
|
+
const descriptors = GET_DESCRIPTORS(data)
|
|
173
|
+
if (REFLECT_OWN_KEYS(descriptors).length !== 4) return null
|
|
174
|
+
for (const key of ['protocol', 'type', 'requestId', 'value']) {
|
|
175
|
+
const descriptor = descriptors[key]
|
|
176
|
+
if (
|
|
177
|
+
!descriptor ||
|
|
178
|
+
!HAS_OWN(descriptor, 'value') ||
|
|
179
|
+
descriptor.enumerable !== true
|
|
180
|
+
) {
|
|
181
|
+
return null
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
const protocol = descriptors.protocol.value
|
|
185
|
+
const type = descriptors.type.value
|
|
186
|
+
const requestId = descriptors.requestId.value
|
|
187
|
+
if (
|
|
188
|
+
protocol !== TOOLKIT_WORKER_PROTOCOL ||
|
|
189
|
+
type !== 'result' ||
|
|
190
|
+
typeof requestId !== 'string' ||
|
|
191
|
+
!requestId ||
|
|
192
|
+
requestId.length > 256
|
|
193
|
+
) {
|
|
194
|
+
return null
|
|
195
|
+
}
|
|
196
|
+
return { protocol, type, requestId, value: descriptors.value.value }
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Object.freeze(NativeParserWorkerTransport.prototype)
|
|
201
|
+
Object.freeze(NativeParserWorkerTransport)
|
|
@@ -3,6 +3,7 @@ import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
|
3
3
|
import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
|
|
4
4
|
import { WorkerRequestData } from './WorkerRequestData.mjs'
|
|
5
5
|
import { WorkerResponseData } from './WorkerResponseData.mjs'
|
|
6
|
+
import { NativeParserWorkerTransport } from './NativeParserWorkerTransport.mjs'
|
|
6
7
|
|
|
7
8
|
const ABORTED_GETTER = Object.getOwnPropertyDescriptor(
|
|
8
9
|
AbortSignal.prototype,
|
|
@@ -73,14 +74,22 @@ export class ParserWorkerClient {
|
|
|
73
74
|
}
|
|
74
75
|
}
|
|
75
76
|
|
|
76
|
-
/**
|
|
77
|
-
* Returns the process-local default client, creating it lazily.
|
|
78
|
-
* @returns {ParserWorkerClient} Default worker client.
|
|
79
|
-
*/
|
|
77
|
+
/** @returns {ParserWorkerClient} Lazily-created process-local default client. */
|
|
80
78
|
static defaultClient() {
|
|
81
79
|
return ParserWorkerClient.#defaultClientFor(null)
|
|
82
80
|
}
|
|
83
81
|
|
|
82
|
+
/**
|
|
83
|
+
* Creates a client whose native worker and received graphs remain private.
|
|
84
|
+
* @param {string | URL} workerUrl Worker module URL.
|
|
85
|
+
* @returns {ParserWorkerClient} Client using native clone provenance.
|
|
86
|
+
*/
|
|
87
|
+
static fromWorkerUrl(workerUrl) {
|
|
88
|
+
return new ParserWorkerClient({
|
|
89
|
+
createWorker: () => NativeParserWorkerTransport.create(workerUrl)
|
|
90
|
+
})
|
|
91
|
+
}
|
|
92
|
+
|
|
84
93
|
/**
|
|
85
94
|
* Returns the process-local default client for one internal attempt.
|
|
86
95
|
* @param {object | null} attemptToken Internal attempt identity.
|
|
@@ -91,26 +100,15 @@ export class ParserWorkerClient {
|
|
|
91
100
|
throw ParserWorkerClient.#unavailableError(attemptToken)
|
|
92
101
|
}
|
|
93
102
|
if (!ParserWorkerClient.#defaultClient) {
|
|
94
|
-
ParserWorkerClient.#defaultClient =
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
new URL(
|
|
99
|
-
'../../workers/parser.worker.mjs',
|
|
100
|
-
import.meta.url
|
|
101
|
-
),
|
|
102
|
-
{ type: 'module' }
|
|
103
|
-
])
|
|
104
|
-
}
|
|
105
|
-
})
|
|
103
|
+
ParserWorkerClient.#defaultClient =
|
|
104
|
+
ParserWorkerClient.fromWorkerUrl(
|
|
105
|
+
new URL('../../workers/parser.worker.mjs', import.meta.url)
|
|
106
|
+
)
|
|
106
107
|
}
|
|
107
108
|
return ParserWorkerClient.#defaultClient
|
|
108
109
|
}
|
|
109
110
|
|
|
110
|
-
/**
|
|
111
|
-
* Disposes the process-local default worker client when one exists.
|
|
112
|
-
* @returns {void}
|
|
113
|
-
*/
|
|
111
|
+
/** Disposes the process-local default worker client. @returns {void} */
|
|
114
112
|
static disposeDefault() {
|
|
115
113
|
ParserWorkerClient.#defaultClient?.dispose()
|
|
116
114
|
ParserWorkerClient.#defaultClient = null
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
const ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
2
|
+
ArrayBuffer.prototype,
|
|
3
|
+
'byteLength'
|
|
4
|
+
)?.get
|
|
5
|
+
const ARRAY_BUFFER_RESIZABLE_GETTER = Object.getOwnPropertyDescriptor(
|
|
6
|
+
ArrayBuffer.prototype,
|
|
7
|
+
'resizable'
|
|
8
|
+
)?.get
|
|
9
|
+
const SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER =
|
|
10
|
+
typeof SharedArrayBuffer === 'function'
|
|
11
|
+
? Object.getOwnPropertyDescriptor(
|
|
12
|
+
SharedArrayBuffer.prototype,
|
|
13
|
+
'byteLength'
|
|
14
|
+
)?.get
|
|
15
|
+
: null
|
|
16
|
+
const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype)
|
|
17
|
+
const TYPED_ARRAY_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
|
|
18
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
19
|
+
'buffer'
|
|
20
|
+
)?.get
|
|
21
|
+
const TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
22
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
23
|
+
'byteLength'
|
|
24
|
+
)?.get
|
|
25
|
+
const TYPED_ARRAY_BYTE_OFFSET_GETTER = Object.getOwnPropertyDescriptor(
|
|
26
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
27
|
+
'byteOffset'
|
|
28
|
+
)?.get
|
|
29
|
+
const TYPED_ARRAY_TAG_GETTER = Object.getOwnPropertyDescriptor(
|
|
30
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
31
|
+
Symbol.toStringTag
|
|
32
|
+
)?.get
|
|
33
|
+
const DATA_VIEW_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
|
|
34
|
+
DataView.prototype,
|
|
35
|
+
'buffer'
|
|
36
|
+
)?.get
|
|
37
|
+
const DATA_VIEW_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
38
|
+
DataView.prototype,
|
|
39
|
+
'byteLength'
|
|
40
|
+
)?.get
|
|
41
|
+
const DATA_VIEW_BYTE_OFFSET_GETTER = Object.getOwnPropertyDescriptor(
|
|
42
|
+
DataView.prototype,
|
|
43
|
+
'byteOffset'
|
|
44
|
+
)?.get
|
|
45
|
+
const DATA_VIEW_CONSTRUCTOR = DataView
|
|
46
|
+
const UINT8_ARRAY_CONSTRUCTOR = Uint8Array
|
|
47
|
+
const UINT8_ARRAY_SET = Uint8Array.prototype.set
|
|
48
|
+
const ARRAY_BUFFER_IS_VIEW = ArrayBuffer.isView
|
|
49
|
+
const OBJECT_GET_PROTOTYPE_OF = Object.getPrototypeOf
|
|
50
|
+
const OBJECT_PROTOTYPE = Object.prototype
|
|
51
|
+
const TYPED_ARRAYS = new Map(
|
|
52
|
+
[
|
|
53
|
+
Int8Array,
|
|
54
|
+
Uint8Array,
|
|
55
|
+
Uint8ClampedArray,
|
|
56
|
+
Int16Array,
|
|
57
|
+
Uint16Array,
|
|
58
|
+
Int32Array,
|
|
59
|
+
Uint32Array,
|
|
60
|
+
Float32Array,
|
|
61
|
+
Float64Array,
|
|
62
|
+
typeof BigInt64Array === 'function' ? BigInt64Array : null,
|
|
63
|
+
typeof BigUint64Array === 'function' ? BigUint64Array : null
|
|
64
|
+
]
|
|
65
|
+
.filter(Boolean)
|
|
66
|
+
.map((Constructor) => [Constructor.name, Constructor])
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Inspects and isolates worker binary data through captured intrinsic slots.
|
|
71
|
+
*/
|
|
72
|
+
export class WorkerBinaryData {
|
|
73
|
+
/**
|
|
74
|
+
* Classifies records only in a privately received native clone graph.
|
|
75
|
+
* Generic inputs must still use exact brands, even with these prototypes.
|
|
76
|
+
* @param {object} value Proven native-clone value.
|
|
77
|
+
* @returns {boolean} Whether it has a normalized plain-record prototype.
|
|
78
|
+
*/
|
|
79
|
+
static isStandardRecord(value) {
|
|
80
|
+
const prototype = OBJECT_GET_PROTOTYPE_OF(value)
|
|
81
|
+
return prototype === OBJECT_PROTOTYPE || prototype === null
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Recreates one intrinsic view on an exact isolated backing buffer.
|
|
86
|
+
* @param {{ byteLength: number, tag: string }} view View fields.
|
|
87
|
+
* @param {ArrayBuffer} buffer Exact copied buffer.
|
|
88
|
+
* @returns {ArrayBufferView} Recreated view.
|
|
89
|
+
*/
|
|
90
|
+
static recreateView(view, buffer) {
|
|
91
|
+
if (view.tag === 'DataView') return new DATA_VIEW_CONSTRUCTOR(buffer)
|
|
92
|
+
const Constructor = TYPED_ARRAYS.get(view.tag)
|
|
93
|
+
if (!Constructor || view.byteLength % Constructor.BYTES_PER_ELEMENT) {
|
|
94
|
+
throw new TypeError('Worker request binary view is unsupported.')
|
|
95
|
+
}
|
|
96
|
+
return new Constructor(
|
|
97
|
+
buffer,
|
|
98
|
+
0,
|
|
99
|
+
view.byteLength / Constructor.BYTES_PER_ELEMENT
|
|
100
|
+
)
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Reads genuine typed-array or DataView internal slots.
|
|
105
|
+
* @param {unknown} value View candidate.
|
|
106
|
+
* @returns {{ buffer: ArrayBufferLike, byteOffset: number, byteLength: number, tag: string } | null} Intrinsic view fields.
|
|
107
|
+
*/
|
|
108
|
+
static view(value) {
|
|
109
|
+
if (!ARRAY_BUFFER_IS_VIEW(value)) return null
|
|
110
|
+
try {
|
|
111
|
+
const tag = TYPED_ARRAY_TAG_GETTER?.call(value)
|
|
112
|
+
if (TYPED_ARRAYS.has(tag)) {
|
|
113
|
+
return {
|
|
114
|
+
buffer: TYPED_ARRAY_BUFFER_GETTER.call(value),
|
|
115
|
+
byteOffset: TYPED_ARRAY_BYTE_OFFSET_GETTER.call(value),
|
|
116
|
+
byteLength: TYPED_ARRAY_BYTE_LENGTH_GETTER.call(value),
|
|
117
|
+
tag
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
if (
|
|
121
|
+
DATA_VIEW_BUFFER_GETTER &&
|
|
122
|
+
DATA_VIEW_BYTE_OFFSET_GETTER &&
|
|
123
|
+
DATA_VIEW_BYTE_LENGTH_GETTER
|
|
124
|
+
) {
|
|
125
|
+
const buffer = DATA_VIEW_BUFFER_GETTER.call(value)
|
|
126
|
+
return {
|
|
127
|
+
buffer,
|
|
128
|
+
byteOffset: DATA_VIEW_BYTE_OFFSET_GETTER.call(value),
|
|
129
|
+
byteLength: DATA_VIEW_BYTE_LENGTH_GETTER.call(value),
|
|
130
|
+
tag: 'DataView'
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
} catch {
|
|
134
|
+
return null
|
|
135
|
+
}
|
|
136
|
+
return null
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** @param {unknown} value Candidate. @returns {number | null} Byte length. */
|
|
140
|
+
static bufferLength(value) {
|
|
141
|
+
if (!ARRAY_BUFFER_BYTE_LENGTH_GETTER) return null
|
|
142
|
+
try {
|
|
143
|
+
return ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value)
|
|
144
|
+
} catch {
|
|
145
|
+
return null
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** @param {unknown} value Candidate. @returns {number | null} Byte length. */
|
|
150
|
+
static sharedBufferLength(value) {
|
|
151
|
+
if (!SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER) return null
|
|
152
|
+
try {
|
|
153
|
+
return SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value)
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Returns whether a genuine ArrayBuffer can change size after accounting.
|
|
161
|
+
* @param {unknown} value Buffer candidate.
|
|
162
|
+
* @returns {boolean} Whether the buffer is resizable.
|
|
163
|
+
*/
|
|
164
|
+
static isResizableBuffer(value) {
|
|
165
|
+
if (!ARRAY_BUFFER_RESIZABLE_GETTER) return false
|
|
166
|
+
try {
|
|
167
|
+
return ARRAY_BUFFER_RESIZABLE_GETTER.call(value) === true
|
|
168
|
+
} catch {
|
|
169
|
+
return false
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Copies one already-accounted intrinsic byte range into a fixed buffer.
|
|
175
|
+
* The explicit range prevents a growable backing store from widening the
|
|
176
|
+
* copy between its limit check and snapshot.
|
|
177
|
+
* @param {ArrayBufferLike} buffer Source buffer.
|
|
178
|
+
* @param {number} byteOffset Captured byte offset.
|
|
179
|
+
* @param {number} byteLength Captured byte length.
|
|
180
|
+
* @returns {ArrayBuffer} Fixed owned snapshot.
|
|
181
|
+
*/
|
|
182
|
+
static copyBuffer(buffer, byteOffset, byteLength) {
|
|
183
|
+
try {
|
|
184
|
+
const source = new UINT8_ARRAY_CONSTRUCTOR(
|
|
185
|
+
buffer,
|
|
186
|
+
byteOffset,
|
|
187
|
+
byteLength
|
|
188
|
+
)
|
|
189
|
+
const copy = new UINT8_ARRAY_CONSTRUCTOR(byteLength)
|
|
190
|
+
UINT8_ARRAY_SET.call(copy, source)
|
|
191
|
+
return TYPED_ARRAY_BUFFER_GETTER.call(copy)
|
|
192
|
+
} catch {
|
|
193
|
+
throw new TypeError(
|
|
194
|
+
'Worker request binary data changed while it was copied.'
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
Object.freeze(WorkerBinaryData.prototype)
|
|
201
|
+
Object.freeze(WorkerBinaryData)
|
|
@@ -3,82 +3,21 @@ import { CircuitJsonReadOnlyDocument } from '../context/CircuitJsonReadOnlyDocum
|
|
|
3
3
|
import { ProtectedExtensionBinaryBoundary } from '../context/ProtectedExtensionBinaryBoundary.mjs'
|
|
4
4
|
import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
5
5
|
import { WorkerResultShape } from './WorkerResultShape.mjs'
|
|
6
|
+
import { WorkerBinaryData } from './WorkerBinaryData.mjs'
|
|
7
|
+
import { NativeParserWorkerTransport } from './NativeParserWorkerTransport.mjs'
|
|
6
8
|
|
|
7
|
-
const
|
|
8
|
-
ArrayBuffer.prototype,
|
|
9
|
-
'byteLength'
|
|
10
|
-
)?.get
|
|
11
|
-
const ARRAY_BUFFER_RESIZABLE_GETTER = Object.getOwnPropertyDescriptor(
|
|
12
|
-
ArrayBuffer.prototype,
|
|
13
|
-
'resizable'
|
|
14
|
-
)?.get
|
|
15
|
-
const SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER =
|
|
16
|
-
typeof SharedArrayBuffer === 'function'
|
|
17
|
-
? Object.getOwnPropertyDescriptor(
|
|
18
|
-
SharedArrayBuffer.prototype,
|
|
19
|
-
'byteLength'
|
|
20
|
-
)?.get
|
|
21
|
-
: null
|
|
22
|
-
const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype)
|
|
23
|
-
const TYPED_ARRAY_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
|
|
24
|
-
TYPED_ARRAY_PROTOTYPE,
|
|
25
|
-
'buffer'
|
|
26
|
-
)?.get
|
|
27
|
-
const TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
28
|
-
TYPED_ARRAY_PROTOTYPE,
|
|
29
|
-
'byteLength'
|
|
30
|
-
)?.get
|
|
31
|
-
const TYPED_ARRAY_BYTE_OFFSET_GETTER = Object.getOwnPropertyDescriptor(
|
|
32
|
-
TYPED_ARRAY_PROTOTYPE,
|
|
33
|
-
'byteOffset'
|
|
34
|
-
)?.get
|
|
35
|
-
const TYPED_ARRAY_TAG_GETTER = Object.getOwnPropertyDescriptor(
|
|
36
|
-
TYPED_ARRAY_PROTOTYPE,
|
|
37
|
-
Symbol.toStringTag
|
|
38
|
-
)?.get
|
|
39
|
-
const DATA_VIEW_BUFFER_GETTER = Object.getOwnPropertyDescriptor(
|
|
40
|
-
DataView.prototype,
|
|
41
|
-
'buffer'
|
|
42
|
-
)?.get
|
|
43
|
-
const DATA_VIEW_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
44
|
-
DataView.prototype,
|
|
45
|
-
'byteLength'
|
|
46
|
-
)?.get
|
|
47
|
-
const DATA_VIEW_BYTE_OFFSET_GETTER = Object.getOwnPropertyDescriptor(
|
|
48
|
-
DataView.prototype,
|
|
49
|
-
'byteOffset'
|
|
50
|
-
)?.get
|
|
51
|
-
const DATA_VIEW_CONSTRUCTOR = DataView
|
|
52
|
-
const UINT8_ARRAY_CONSTRUCTOR = Uint8Array
|
|
53
|
-
const UINT8_ARRAY_SET = Uint8Array.prototype.set
|
|
9
|
+
const ARRAY_IS_ARRAY = Array.isArray
|
|
54
10
|
const STRUCTURED_CLONE = globalThis.structuredClone
|
|
55
11
|
const MAX_REQUEST_BYTES = 100_000_000
|
|
56
12
|
const MAX_REQUEST_DEPTH = 256
|
|
57
13
|
const MAX_REQUEST_VALUES = 100_000
|
|
58
14
|
const MAX_RESULT_BYTES = 250_000_000
|
|
59
15
|
const MAX_RESULT_VALUES = 2_000_000
|
|
60
|
-
const MAX_DOCUMENT_RESULT_VALUES =
|
|
16
|
+
const MAX_DOCUMENT_RESULT_VALUES = 10_000_000
|
|
61
17
|
// Project documents retain the single-result ceiling independently while this
|
|
62
18
|
// aggregate ceiling bounds total worker traversal regardless of document count.
|
|
63
|
-
const MAX_PROJECT_RESULT_VALUES =
|
|
19
|
+
const MAX_PROJECT_RESULT_VALUES = 16_000_000
|
|
64
20
|
const MAX_PROJECT_RESULT_BYTES = 256 * 1024 * 1024
|
|
65
|
-
const TYPED_ARRAYS = new Map(
|
|
66
|
-
[
|
|
67
|
-
Int8Array,
|
|
68
|
-
Uint8Array,
|
|
69
|
-
Uint8ClampedArray,
|
|
70
|
-
Int16Array,
|
|
71
|
-
Uint16Array,
|
|
72
|
-
Int32Array,
|
|
73
|
-
Uint32Array,
|
|
74
|
-
Float32Array,
|
|
75
|
-
Float64Array,
|
|
76
|
-
typeof BigInt64Array === 'function' ? BigInt64Array : null,
|
|
77
|
-
typeof BigUint64Array === 'function' ? BigUint64Array : null
|
|
78
|
-
]
|
|
79
|
-
.filter(Boolean)
|
|
80
|
-
.map((Constructor) => [Constructor.name, Constructor])
|
|
81
|
-
)
|
|
82
21
|
|
|
83
22
|
/**
|
|
84
23
|
* Validates and prepares bounded structured-clone worker request data.
|
|
@@ -151,10 +90,12 @@ export class WorkerRequestData {
|
|
|
151
90
|
* @returns {unknown} Owned clone-safe result data.
|
|
152
91
|
*/
|
|
153
92
|
static prepareResponse(value) {
|
|
154
|
-
|
|
93
|
+
const received = NativeParserWorkerTransport.consumeResult(value)
|
|
94
|
+
return WorkerRequestData.#prepare(received ? received.value : value, {
|
|
155
95
|
bytes: MAX_RESULT_BYTES,
|
|
156
|
-
copyBinary:
|
|
96
|
+
copyBinary: !received,
|
|
157
97
|
output: true,
|
|
98
|
+
standardBuiltins: Boolean(received),
|
|
158
99
|
strictDescriptors: true,
|
|
159
100
|
transferInput: false,
|
|
160
101
|
trustProof: false,
|
|
@@ -165,7 +106,7 @@ export class WorkerRequestData {
|
|
|
165
106
|
/**
|
|
166
107
|
* Runs the shared bounded structured-data preparation pass.
|
|
167
108
|
* @param {unknown} value Data candidate.
|
|
168
|
-
* @param {{ bytes: number, copyBinary: boolean, output: boolean, strictDescriptors: boolean, transferInput: boolean, trustProof: boolean, values: number }} limits Preparation limits.
|
|
109
|
+
* @param {{ bytes: number, copyBinary: boolean, output: boolean, standardBuiltins?: boolean, strictDescriptors: boolean, transferInput: boolean, trustProof: boolean, values: number }} limits Preparation limits.
|
|
169
110
|
* @returns {{ value: unknown, transfer: Transferable[] }} Prepared data.
|
|
170
111
|
*/
|
|
171
112
|
static #prepare(value, limits) {
|
|
@@ -187,6 +128,7 @@ export class WorkerRequestData {
|
|
|
187
128
|
projectResult: false,
|
|
188
129
|
reaccountValues: 0,
|
|
189
130
|
strictDescriptors: limits.strictDescriptors,
|
|
131
|
+
standardBuiltins: limits.standardBuiltins === true,
|
|
190
132
|
transfer: { items: [], seen: new Set() },
|
|
191
133
|
transferInput: limits.transferInput,
|
|
192
134
|
trustProof: limits.trustProof,
|
|
@@ -265,14 +207,36 @@ export class WorkerRequestData {
|
|
|
265
207
|
WorkerRequestData.#accountedScope(state)?.add(value)
|
|
266
208
|
state.visiting.add(value)
|
|
267
209
|
try {
|
|
268
|
-
|
|
210
|
+
// Array and view brands are disjoint from raw buffers, including
|
|
211
|
+
// cross-realm values and values with replaced prototypes.
|
|
212
|
+
if (ARRAY_IS_ARRAY(value)) {
|
|
213
|
+
return WorkerRequestData.#prepareContainer(value, state, depth)
|
|
214
|
+
}
|
|
215
|
+
if (
|
|
216
|
+
state.standardBuiltins &&
|
|
217
|
+
WorkerBinaryData.isStandardRecord(value)
|
|
218
|
+
) {
|
|
219
|
+
return WorkerRequestData.#prepareContainer(value, state, depth)
|
|
220
|
+
}
|
|
221
|
+
const view = WorkerBinaryData.view(value)
|
|
222
|
+
if (view) {
|
|
223
|
+
WorkerRequestData.#reserveBytes(state, view.byteLength)
|
|
224
|
+
const prepared = WorkerRequestData.#prepareView(
|
|
225
|
+
value,
|
|
226
|
+
view,
|
|
227
|
+
state
|
|
228
|
+
)
|
|
229
|
+
state.prepared.set(value, prepared)
|
|
230
|
+
return prepared
|
|
231
|
+
}
|
|
232
|
+
const bufferLength = WorkerBinaryData.bufferLength(value)
|
|
269
233
|
if (bufferLength !== null) {
|
|
270
234
|
WorkerRequestData.#reserveBytes(state, bufferLength)
|
|
271
235
|
const isolate =
|
|
272
236
|
state.copyBinary ||
|
|
273
|
-
|
|
237
|
+
WorkerBinaryData.isResizableBuffer(value)
|
|
274
238
|
const prepared = isolate
|
|
275
|
-
?
|
|
239
|
+
? WorkerBinaryData.copyBuffer(value, 0, bufferLength)
|
|
276
240
|
: value
|
|
277
241
|
state.prepared.set(value, prepared)
|
|
278
242
|
if (state.transferInput) {
|
|
@@ -280,10 +244,10 @@ export class WorkerRequestData {
|
|
|
280
244
|
}
|
|
281
245
|
return prepared
|
|
282
246
|
}
|
|
283
|
-
const sharedLength =
|
|
247
|
+
const sharedLength = WorkerBinaryData.sharedBufferLength(value)
|
|
284
248
|
if (sharedLength !== null) {
|
|
285
249
|
WorkerRequestData.#reserveBytes(state, sharedLength)
|
|
286
|
-
const copied =
|
|
250
|
+
const copied = WorkerBinaryData.copyBuffer(
|
|
287
251
|
value,
|
|
288
252
|
0,
|
|
289
253
|
sharedLength
|
|
@@ -294,17 +258,6 @@ export class WorkerRequestData {
|
|
|
294
258
|
}
|
|
295
259
|
return copied
|
|
296
260
|
}
|
|
297
|
-
const view = WorkerRequestData.#view(value)
|
|
298
|
-
if (view) {
|
|
299
|
-
WorkerRequestData.#reserveBytes(state, view.byteLength)
|
|
300
|
-
const prepared = WorkerRequestData.#prepareView(
|
|
301
|
-
value,
|
|
302
|
-
view,
|
|
303
|
-
state
|
|
304
|
-
)
|
|
305
|
-
state.prepared.set(value, prepared)
|
|
306
|
-
return prepared
|
|
307
|
-
}
|
|
308
261
|
return WorkerRequestData.#prepareContainer(value, state, depth)
|
|
309
262
|
} finally {
|
|
310
263
|
state.visiting.delete(value)
|
|
@@ -598,21 +551,32 @@ export class WorkerRequestData {
|
|
|
598
551
|
if (accounted.has(value)) return
|
|
599
552
|
accounted.add(value)
|
|
600
553
|
WorkerRequestData.#reserveAccountedValues(state, 1)
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
WorkerRequestData.#reserveAccountedBytes(state, bufferLength)
|
|
554
|
+
if (ARRAY_IS_ARRAY(value)) {
|
|
555
|
+
WorkerRequestData.#accountPreparedContainer(value, state, depth)
|
|
604
556
|
return
|
|
605
557
|
}
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
558
|
+
if (
|
|
559
|
+
state.standardBuiltins &&
|
|
560
|
+
WorkerBinaryData.isStandardRecord(value)
|
|
561
|
+
) {
|
|
562
|
+
WorkerRequestData.#accountPreparedContainer(value, state, depth)
|
|
609
563
|
return
|
|
610
564
|
}
|
|
611
|
-
const view =
|
|
565
|
+
const view = WorkerBinaryData.view(value)
|
|
612
566
|
if (view) {
|
|
613
567
|
WorkerRequestData.#reserveAccountedBytes(state, view.byteLength)
|
|
614
568
|
return
|
|
615
569
|
}
|
|
570
|
+
const bufferLength = WorkerBinaryData.bufferLength(value)
|
|
571
|
+
if (bufferLength !== null) {
|
|
572
|
+
WorkerRequestData.#reserveAccountedBytes(state, bufferLength)
|
|
573
|
+
return
|
|
574
|
+
}
|
|
575
|
+
const sharedLength = WorkerBinaryData.sharedBufferLength(value)
|
|
576
|
+
if (sharedLength !== null) {
|
|
577
|
+
WorkerRequestData.#reserveAccountedBytes(state, sharedLength)
|
|
578
|
+
return
|
|
579
|
+
}
|
|
616
580
|
WorkerRequestData.#accountPreparedContainer(value, state, depth)
|
|
617
581
|
}
|
|
618
582
|
|
|
@@ -708,8 +672,11 @@ export class WorkerRequestData {
|
|
|
708
672
|
* @returns {ArrayBufferView} Prepared exact view.
|
|
709
673
|
*/
|
|
710
674
|
static #prepareView(original, view, state) {
|
|
711
|
-
const backingLength =
|
|
712
|
-
const sharedLength =
|
|
675
|
+
const backingLength = WorkerBinaryData.bufferLength(view.buffer)
|
|
676
|
+
const sharedLength =
|
|
677
|
+
backingLength === null
|
|
678
|
+
? WorkerBinaryData.sharedBufferLength(view.buffer)
|
|
679
|
+
: null
|
|
713
680
|
const exactArrayBuffer =
|
|
714
681
|
backingLength !== null &&
|
|
715
682
|
view.byteOffset === 0 &&
|
|
@@ -717,7 +684,7 @@ export class WorkerRequestData {
|
|
|
717
684
|
if (
|
|
718
685
|
exactArrayBuffer &&
|
|
719
686
|
!state.copyBinary &&
|
|
720
|
-
!
|
|
687
|
+
!WorkerBinaryData.isResizableBuffer(view.buffer)
|
|
721
688
|
) {
|
|
722
689
|
if (state.transferInput) {
|
|
723
690
|
WorkerRequestData.#addTransfer(state.transfer, view.buffer)
|
|
@@ -727,134 +694,18 @@ export class WorkerRequestData {
|
|
|
727
694
|
if (backingLength === null && sharedLength === null) {
|
|
728
695
|
throw new TypeError('Worker request binary view is invalid.')
|
|
729
696
|
}
|
|
730
|
-
const copy =
|
|
697
|
+
const copy = WorkerBinaryData.copyBuffer(
|
|
731
698
|
view.buffer,
|
|
732
699
|
view.byteOffset,
|
|
733
700
|
view.byteLength
|
|
734
701
|
)
|
|
735
|
-
const prepared =
|
|
702
|
+
const prepared = WorkerBinaryData.recreateView(view, copy)
|
|
736
703
|
if (state.transferInput) {
|
|
737
704
|
WorkerRequestData.#addTransfer(state.transfer, copy)
|
|
738
705
|
}
|
|
739
706
|
return prepared
|
|
740
707
|
}
|
|
741
708
|
|
|
742
|
-
/**
|
|
743
|
-
* Recreates one intrinsic view on an exact isolated backing buffer.
|
|
744
|
-
* @param {{ byteLength: number, tag: string }} view View fields.
|
|
745
|
-
* @param {ArrayBuffer} buffer Exact copied buffer.
|
|
746
|
-
* @returns {ArrayBufferView} Recreated view.
|
|
747
|
-
*/
|
|
748
|
-
static #recreateView(view, buffer) {
|
|
749
|
-
if (view.tag === 'DataView') return new DATA_VIEW_CONSTRUCTOR(buffer)
|
|
750
|
-
const Constructor = TYPED_ARRAYS.get(view.tag)
|
|
751
|
-
if (!Constructor || view.byteLength % Constructor.BYTES_PER_ELEMENT) {
|
|
752
|
-
throw new TypeError('Worker request binary view is unsupported.')
|
|
753
|
-
}
|
|
754
|
-
return new Constructor(
|
|
755
|
-
buffer,
|
|
756
|
-
0,
|
|
757
|
-
view.byteLength / Constructor.BYTES_PER_ELEMENT
|
|
758
|
-
)
|
|
759
|
-
}
|
|
760
|
-
|
|
761
|
-
/**
|
|
762
|
-
* Reads genuine typed-array or DataView internal slots.
|
|
763
|
-
* @param {unknown} value View candidate.
|
|
764
|
-
* @returns {{ buffer: ArrayBufferLike, byteOffset: number, byteLength: number, tag: string } | null} Intrinsic view fields.
|
|
765
|
-
*/
|
|
766
|
-
static #view(value) {
|
|
767
|
-
try {
|
|
768
|
-
if (
|
|
769
|
-
DATA_VIEW_BUFFER_GETTER &&
|
|
770
|
-
DATA_VIEW_BYTE_OFFSET_GETTER &&
|
|
771
|
-
DATA_VIEW_BYTE_LENGTH_GETTER
|
|
772
|
-
) {
|
|
773
|
-
const buffer = DATA_VIEW_BUFFER_GETTER.call(value)
|
|
774
|
-
return {
|
|
775
|
-
buffer,
|
|
776
|
-
byteOffset: DATA_VIEW_BYTE_OFFSET_GETTER.call(value),
|
|
777
|
-
byteLength: DATA_VIEW_BYTE_LENGTH_GETTER.call(value),
|
|
778
|
-
tag: 'DataView'
|
|
779
|
-
}
|
|
780
|
-
}
|
|
781
|
-
} catch {
|
|
782
|
-
// The typed-array intrinsic check below is independent.
|
|
783
|
-
}
|
|
784
|
-
try {
|
|
785
|
-
const tag = TYPED_ARRAY_TAG_GETTER?.call(value)
|
|
786
|
-
if (!TYPED_ARRAYS.has(tag)) return null
|
|
787
|
-
return {
|
|
788
|
-
buffer: TYPED_ARRAY_BUFFER_GETTER.call(value),
|
|
789
|
-
byteOffset: TYPED_ARRAY_BYTE_OFFSET_GETTER.call(value),
|
|
790
|
-
byteLength: TYPED_ARRAY_BYTE_LENGTH_GETTER.call(value),
|
|
791
|
-
tag
|
|
792
|
-
}
|
|
793
|
-
} catch {
|
|
794
|
-
return null
|
|
795
|
-
}
|
|
796
|
-
}
|
|
797
|
-
|
|
798
|
-
/** @param {unknown} value Candidate. @returns {number | null} Byte length. */
|
|
799
|
-
static #bufferLength(value) {
|
|
800
|
-
if (!ARRAY_BUFFER_BYTE_LENGTH_GETTER) return null
|
|
801
|
-
try {
|
|
802
|
-
return ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value)
|
|
803
|
-
} catch {
|
|
804
|
-
return null
|
|
805
|
-
}
|
|
806
|
-
}
|
|
807
|
-
|
|
808
|
-
/** @param {unknown} value Candidate. @returns {number | null} Byte length. */
|
|
809
|
-
static #sharedBufferLength(value) {
|
|
810
|
-
if (!SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER) return null
|
|
811
|
-
try {
|
|
812
|
-
return SHARED_ARRAY_BUFFER_BYTE_LENGTH_GETTER.call(value)
|
|
813
|
-
} catch {
|
|
814
|
-
return null
|
|
815
|
-
}
|
|
816
|
-
}
|
|
817
|
-
|
|
818
|
-
/**
|
|
819
|
-
* Returns whether a genuine ArrayBuffer can change size after accounting.
|
|
820
|
-
* @param {unknown} value Buffer candidate.
|
|
821
|
-
* @returns {boolean} Whether the buffer is resizable.
|
|
822
|
-
*/
|
|
823
|
-
static #isResizableBuffer(value) {
|
|
824
|
-
if (!ARRAY_BUFFER_RESIZABLE_GETTER) return false
|
|
825
|
-
try {
|
|
826
|
-
return ARRAY_BUFFER_RESIZABLE_GETTER.call(value) === true
|
|
827
|
-
} catch {
|
|
828
|
-
return false
|
|
829
|
-
}
|
|
830
|
-
}
|
|
831
|
-
|
|
832
|
-
/**
|
|
833
|
-
* Copies one already-accounted intrinsic byte range into a fixed buffer.
|
|
834
|
-
* The explicit range prevents a growable backing store from widening the
|
|
835
|
-
* copy between its limit check and snapshot.
|
|
836
|
-
* @param {ArrayBufferLike} buffer Source buffer.
|
|
837
|
-
* @param {number} byteOffset Captured byte offset.
|
|
838
|
-
* @param {number} byteLength Captured byte length.
|
|
839
|
-
* @returns {ArrayBuffer} Fixed owned snapshot.
|
|
840
|
-
*/
|
|
841
|
-
static #copyBuffer(buffer, byteOffset, byteLength) {
|
|
842
|
-
try {
|
|
843
|
-
const source = new UINT8_ARRAY_CONSTRUCTOR(
|
|
844
|
-
buffer,
|
|
845
|
-
byteOffset,
|
|
846
|
-
byteLength
|
|
847
|
-
)
|
|
848
|
-
const copy = new UINT8_ARRAY_CONSTRUCTOR(byteLength)
|
|
849
|
-
UINT8_ARRAY_SET.call(copy, source)
|
|
850
|
-
return TYPED_ARRAY_BUFFER_GETTER.call(copy)
|
|
851
|
-
} catch {
|
|
852
|
-
throw new TypeError(
|
|
853
|
-
'Worker request binary data changed while it was copied.'
|
|
854
|
-
)
|
|
855
|
-
}
|
|
856
|
-
}
|
|
857
|
-
|
|
858
709
|
/** @param {PropertyDescriptor | undefined} descriptor Descriptor. @returns {unknown} Data value. */
|
|
859
710
|
static #dataValue(descriptor) {
|
|
860
711
|
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
@@ -5,6 +5,9 @@ import { CircuitJsonValidationProof } from '../context/CircuitJsonValidationProo
|
|
|
5
5
|
import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
|
|
6
6
|
import { WorkerRequestData } from './WorkerRequestData.mjs'
|
|
7
7
|
|
|
8
|
+
const PREPARE_RESPONSE = WorkerRequestData.prepareResponse
|
|
9
|
+
const VALIDATE_AND_ATTACH = CircuitJsonValidationProof.validateAndAttach
|
|
10
|
+
|
|
8
11
|
const ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
9
12
|
ArrayBuffer.prototype,
|
|
10
13
|
'byteLength'
|
|
@@ -154,7 +157,7 @@ export class WorkerResponseData {
|
|
|
154
157
|
* @returns {object} Original validated result.
|
|
155
158
|
*/
|
|
156
159
|
static result(operation, value) {
|
|
157
|
-
const prepared =
|
|
160
|
+
const prepared = PREPARE_RESPONSE(value)
|
|
158
161
|
const fields = WorkerResponseData.#record(
|
|
159
162
|
prepared,
|
|
160
163
|
'Toolkit worker result'
|
|
@@ -330,7 +333,11 @@ export class WorkerResponseData {
|
|
|
330
333
|
'statistics'
|
|
331
334
|
])
|
|
332
335
|
try {
|
|
333
|
-
|
|
336
|
+
// Preparation allocated every container and normalized every binary
|
|
337
|
+
// built-in without exposing the owned graph to caller callbacks.
|
|
338
|
+
VALIDATE_AND_ATTACH(document, {
|
|
339
|
+
standardBuiltins: true
|
|
340
|
+
})
|
|
334
341
|
} catch (error) {
|
|
335
342
|
throw WorkerResponseData.#error(
|
|
336
343
|
'Toolkit worker document model is invalid.',
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
2
2
|
|
|
3
|
+
const ARRAY_IS_ARRAY = Array.isArray
|
|
4
|
+
|
|
3
5
|
const DOCUMENT_FIELDS = Object.freeze([
|
|
4
6
|
'schema',
|
|
5
7
|
'id',
|
|
@@ -64,7 +66,10 @@ export class WorkerResultShape {
|
|
|
64
66
|
)
|
|
65
67
|
: keys
|
|
66
68
|
if (
|
|
67
|
-
|
|
69
|
+
// A foreign intrinsic Array.prototype has the same array brand.
|
|
70
|
+
// Only the receiver's own descriptors are used below; inherited
|
|
71
|
+
// properties are never read or copied into the normalized array.
|
|
72
|
+
(prototype !== Array.prototype && !ARRAY_IS_ARRAY(prototype)) ||
|
|
68
73
|
!Number.isSafeInteger(length) ||
|
|
69
74
|
length < 0 ||
|
|
70
75
|
(output && !strictDescriptors
|