circuitjson-toolkit 1.4.1 → 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.
@@ -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