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 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
- 4,000,000-item ceiling so large native renderer graphs do not inherit the
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 256 MiB aggregate
92
- ceiling with independent 250 MB document and project-metadata limits. Binary
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.
@@ -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 4,000,000 structured items
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 five
109
- million result values and 250 MB. Exact project envelopes allow eight million
110
- aggregate values and 256 MiB, while each document stays within five million
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "circuitjson-toolkit",
3
- "version": "1.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "Canonical CircuitJSON parsing, project, rendering, query, manufacturing, simulation, and scene contracts",
5
5
  "keywords": [
6
6
  "circuitjson",
@@ -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 = new Map()
257
+ const dependencies = []
268
258
  const proxyCache = new WeakMap()
269
259
  const proxyMetadata = new WeakMap()
270
- const trackedInput = this.#createProxy({
271
- target: input,
272
- path: [],
273
- dependencies,
274
- proxyCache,
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(dependencies, {
291
- type: 'value',
292
- path: metadata.path,
293
- expected: metadata.target
294
- })
278
+ this.#record(
279
+ dependencies,
280
+ metadata.pathNode,
281
+ 'value',
282
+ metadata.target
283
+ )
295
284
  }
296
285
 
297
286
  return {
298
- dependencies: [...dependencies.values()],
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 {{ target: object, path: PropertyKey[], dependencies: Map<string, object>, proxyCache: WeakMap<object, Map<string, object>>, proxyMetadata: WeakMap<object, { target: object, path: PropertyKey[] }> }} context Tracking context.
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 pathKey = this.#pathKey(context.path)
310
- const cachedByPath = context.proxyCache.get(context.target)
311
- if (cachedByPath?.has(pathKey)) {
312
- return cachedByPath.get(pathKey)
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 proxy = new Proxy(context.target, {
307
+ const proxyContext = { ...context, pathNode }
308
+ const proxy = new Proxy(target, {
316
309
  get: (target, property) =>
317
- this.#readProperty(context, target, property),
310
+ this.#readProperty(proxyContext, target, property),
318
311
  has: (target, property) =>
319
- this.#readPresence(context, target, property),
320
- ownKeys: (target) => this.#readKeys(context, target),
312
+ this.#readPresence(proxyContext, target, property),
313
+ ownKeys: (target) => this.#readKeys(proxyContext, target),
321
314
  getOwnPropertyDescriptor: (target, property) =>
322
- this.#readDescriptor(context, target, property),
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(pathKey, proxy)
323
+ nextCachedByPath.set(pathNode, proxy)
331
324
  if (!cachedByPath) {
332
- context.proxyCache.set(context.target, nextCachedByPath)
325
+ context.proxyCache.set(target, nextCachedByPath)
333
326
  }
334
327
  context.proxyMetadata.set(proxy, {
335
- target: context.target,
336
- path: [...context.path]
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 path = [...context.path, property]
351
- if (this.#isTraversable(value, path)) {
352
- this.#record(context.dependencies, {
353
- type: 'kind',
354
- path,
355
- expected: SelfAdjustingComputation.#valueKind(value)
356
- })
357
- return this.#createProxy({ ...context, target: value, path })
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 path = [...context.path, property]
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(context.dependencies, {
412
- type: 'descriptor',
413
- path: [...context.path, property],
414
- expected: SelfAdjustingComputation.#descriptorState(descriptor)
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
- * Stores or replaces one dependency by type and input path.
435
- * @param {Map<string, object>} dependencies Dependency registry.
436
- * @param {{ type: string, path: PropertyKey[], expected: any }} dependency Dependency record.
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, dependency) {
440
- dependencies.set(
441
- dependency.type + ':' + this.#pathKey(dependency.path),
442
- dependency
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}
@@ -6,7 +6,7 @@ const OWNED_EXTENSION_ROOTS = new WeakSet()
6
6
  const EXTENSION_METADATA_LIMITS = Object.freeze({
7
7
  label: 'Canonical extension data',
8
8
  maxBytes: 128 * 1024 * 1024,
9
- maxItems: 4_000_000,
9
+ maxItems: 8_000_000,
10
10
  preserveBinary: true
11
11
  })
12
12
 
@@ -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 = new ParserWorkerClient({
95
- createWorker: () => {
96
- const WorkerConstructor = globalThis.Worker
97
- return Reflect.construct(WorkerConstructor, [
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 ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
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 = 5_000_000
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 = 8_000_000
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
- return WorkerRequestData.#prepare(value, {
93
+ const received = NativeParserWorkerTransport.consumeResult(value)
94
+ return WorkerRequestData.#prepare(received ? received.value : value, {
155
95
  bytes: MAX_RESULT_BYTES,
156
- copyBinary: true,
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
- const bufferLength = WorkerRequestData.#bufferLength(value)
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
- WorkerRequestData.#isResizableBuffer(value)
237
+ WorkerBinaryData.isResizableBuffer(value)
274
238
  const prepared = isolate
275
- ? WorkerRequestData.#copyBuffer(value, 0, bufferLength)
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 = WorkerRequestData.#sharedBufferLength(value)
247
+ const sharedLength = WorkerBinaryData.sharedBufferLength(value)
284
248
  if (sharedLength !== null) {
285
249
  WorkerRequestData.#reserveBytes(state, sharedLength)
286
- const copied = WorkerRequestData.#copyBuffer(
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
- const bufferLength = WorkerRequestData.#bufferLength(value)
602
- if (bufferLength !== null) {
603
- WorkerRequestData.#reserveAccountedBytes(state, bufferLength)
554
+ if (ARRAY_IS_ARRAY(value)) {
555
+ WorkerRequestData.#accountPreparedContainer(value, state, depth)
604
556
  return
605
557
  }
606
- const sharedLength = WorkerRequestData.#sharedBufferLength(value)
607
- if (sharedLength !== null) {
608
- WorkerRequestData.#reserveAccountedBytes(state, sharedLength)
558
+ if (
559
+ state.standardBuiltins &&
560
+ WorkerBinaryData.isStandardRecord(value)
561
+ ) {
562
+ WorkerRequestData.#accountPreparedContainer(value, state, depth)
609
563
  return
610
564
  }
611
- const view = WorkerRequestData.#view(value)
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 = WorkerRequestData.#bufferLength(view.buffer)
712
- const sharedLength = WorkerRequestData.#sharedBufferLength(view.buffer)
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
- !WorkerRequestData.#isResizableBuffer(view.buffer)
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 = WorkerRequestData.#copyBuffer(
697
+ const copy = WorkerBinaryData.copyBuffer(
731
698
  view.buffer,
732
699
  view.byteOffset,
733
700
  view.byteLength
734
701
  )
735
- const prepared = WorkerRequestData.#recreateView(view, copy)
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 = WorkerRequestData.prepareResponse(value)
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
- CircuitJsonValidationProof.validateAndAttach(document)
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
- prototype !== Array.prototype ||
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