circuitjson-toolkit 1.2.1 → 1.4.0

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.
@@ -0,0 +1,975 @@
1
+ import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
2
+ import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
3
+ import { StructuredCloneCollectionNormalizer } from './StructuredCloneCollectionNormalizer.mjs'
4
+ import { StructuredCloneTextAccounting } from './StructuredCloneTextAccounting.mjs'
5
+
6
+ const MAX_DEPTH = 256
7
+ const MAX_SLICE_WORK = 1_024
8
+ const MAX_COOPERATIVE_RECORD_KEYS = 16_384
9
+ const ADOPTION_PLANS = new WeakMap()
10
+ const TRUSTED_PLANS = new WeakSet()
11
+
12
+ /**
13
+ * Traverses one standard structured-clone graph and records safe sealing work.
14
+ */
15
+ class StructuredCloneAdoptionTraversal {
16
+ #adopted
17
+ #capture
18
+ #cooperative
19
+ #plan
20
+ #replacements
21
+ #root
22
+ #state
23
+ #work
24
+
25
+ /**
26
+ * Creates one isolated traversal over candidate accounting state.
27
+ * @param {unknown} root Structured-clone graph root.
28
+ * @param {Record<string, any>} state Candidate accounting state.
29
+ * @param {(value: unknown, state: Record<string, any>, depth: number) => unknown} capture Built-in fallback capture.
30
+ * @param {boolean} cooperative Whether traversal may lock and yield within containers.
31
+ */
32
+ constructor(root, state, capture, cooperative) {
33
+ this.#root = root
34
+ this.#state = state
35
+ this.#capture = capture
36
+ this.#cooperative = cooperative
37
+ this.#adopted = undefined
38
+ this.#replacements = []
39
+ this.#plan = {
40
+ binaryCaptures: [],
41
+ seals: [],
42
+ seen: new Set()
43
+ }
44
+ this.#work = 0
45
+ }
46
+
47
+ /**
48
+ * Returns the adopted graph after traversal has completed.
49
+ * @returns {unknown} Adopted graph root.
50
+ */
51
+ get adopted() {
52
+ return this.#adopted
53
+ }
54
+
55
+ /**
56
+ * Returns the reusable descriptor-checked sealing plan.
57
+ * @returns {object} Structured-clone sealing plan.
58
+ */
59
+ get plan() {
60
+ return this.#plan
61
+ }
62
+
63
+ /**
64
+ * Traverses the graph and captures all descriptors and binary payloads.
65
+ * @returns {Generator<void, unknown, void>} Bounded-work traversal.
66
+ */
67
+ *traverse() {
68
+ this.#adopted = yield* this.#adoptValue(this.#root, 0)
69
+ return this.#adopted
70
+ }
71
+
72
+ /**
73
+ * Applies normalized built-in replacements after the full graph validates.
74
+ * @returns {Generator<void, void, void>} Bounded-work replacement pass.
75
+ */
76
+ *applyReplacements() {
77
+ for (const replacement of this.#replacements) {
78
+ const descriptor = StructuredCloneAdoptionTraversal.#descriptor(
79
+ replacement.owner,
80
+ replacement.key,
81
+ 'Canonical asset source could not be adopted safely.'
82
+ )
83
+ if (
84
+ !StructuredCloneAdoptionTraversal.#sameDataDescriptor(
85
+ descriptor,
86
+ replacement.originalDescriptor
87
+ )
88
+ ) {
89
+ throw new TypeError(
90
+ 'Canonical asset source changed during adoption.'
91
+ )
92
+ }
93
+ try {
94
+ Object.defineProperty(replacement.owner, replacement.key, {
95
+ ...replacement.finalDescriptor
96
+ })
97
+ } catch {
98
+ throw new TypeError(
99
+ 'Canonical asset source could not be adopted safely.'
100
+ )
101
+ }
102
+ yield* this.#checkpoint()
103
+ }
104
+ }
105
+
106
+ /**
107
+ * Installs isolated binary getters and freezes validated targets in chunks.
108
+ * @returns {Generator<void, void, void>} Bounded-work sealing pass.
109
+ */
110
+ *seal() {
111
+ for (const captured of this.#plan.binaryCaptures) {
112
+ ProtectedExtensionBinaryBoundary.protectCapturedProperty(captured)
113
+ yield* this.#checkpoint()
114
+ }
115
+ for (const seal of this.#plan.seals) {
116
+ yield* this.#sealTargetCooperatively(seal)
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Adopts one structured-clone value and records its final descriptors.
122
+ * @param {unknown} value Candidate value.
123
+ * @param {number} depth Current container depth.
124
+ * @returns {Generator<void, unknown, void>} Bounded-work value traversal.
125
+ */
126
+ *#adoptValue(value, depth) {
127
+ yield* this.#checkpoint()
128
+ const type = typeof value
129
+ if (type === 'string') {
130
+ yield* StructuredCloneTextAccounting.reserve(value, this.#state, {
131
+ checkpoint: () => this.#checkpoint(MAX_SLICE_WORK),
132
+ reserve: (count) =>
133
+ StructuredCloneAdoptionTraversal.#reserveBytes(
134
+ this.#state,
135
+ count
136
+ )
137
+ })
138
+ return value
139
+ }
140
+ if (
141
+ value === null ||
142
+ ['undefined', 'boolean', 'number', 'bigint'].includes(type)
143
+ ) {
144
+ return value
145
+ }
146
+ if (type !== 'object') {
147
+ throw new TypeError(
148
+ 'Canonical asset source must contain clone-safe data.'
149
+ )
150
+ }
151
+ if (depth > MAX_DEPTH) {
152
+ throw new TypeError('Canonical asset source is nested too deeply.')
153
+ }
154
+ if (this.#state.seen.has(value)) return this.#state.seen.get(value)
155
+
156
+ const binary = BinaryDataSnapshot.describeStandard(value)
157
+ if (binary) {
158
+ StructuredCloneAdoptionTraversal.#reserve(this.#state, 1)
159
+ StructuredCloneAdoptionTraversal.#reserveBytes(
160
+ this.#state,
161
+ binary.byteLength
162
+ )
163
+ this.#state.seen.set(value, value)
164
+ return value
165
+ }
166
+
167
+ let prototype
168
+ let keys
169
+ let extensible
170
+ try {
171
+ prototype = Object.getPrototypeOf(value)
172
+ extensible = Object.isExtensible(value)
173
+ keys =
174
+ this.#cooperative && Array.isArray(value)
175
+ ? null
176
+ : Reflect.ownKeys(value)
177
+ } catch {
178
+ throw new TypeError(
179
+ 'Canonical asset source could not be inspected safely.'
180
+ )
181
+ }
182
+ const array = Array.isArray(value)
183
+ const plain = array
184
+ ? prototype === Array.prototype
185
+ : prototype === Object.prototype || prototype === null
186
+ const collection = yield* StructuredCloneCollectionNormalizer.normalize(
187
+ value,
188
+ prototype,
189
+ keys || [],
190
+ depth,
191
+ {
192
+ adopt: (child, childDepth) =>
193
+ this.#adoptValue(child, childDepth),
194
+ checkpoint: () => this.#checkpoint(),
195
+ collect: (child) => this.#collectCapturedValue(child),
196
+ remember: (source, result) =>
197
+ this.#state.seen.set(source, result),
198
+ reserve: (count) =>
199
+ StructuredCloneAdoptionTraversal.#reserve(
200
+ this.#state,
201
+ count
202
+ )
203
+ }
204
+ )
205
+ if (collection !== StructuredCloneCollectionNormalizer.notHandled) {
206
+ return collection
207
+ }
208
+ if (!plain || !extensible) {
209
+ const captured = this.#capture(value, this.#state, depth)
210
+ yield* this.#collectCapturedValue(captured)
211
+ return captured
212
+ }
213
+
214
+ const lengthDescriptor = array
215
+ ? StructuredCloneAdoptionTraversal.#arrayLengthDescriptor(value)
216
+ : null
217
+ const target =
218
+ this.#cooperative && array
219
+ ? new Array(lengthDescriptor.value)
220
+ : value
221
+ StructuredCloneAdoptionTraversal.#reserve(this.#state, 1)
222
+ this.#state.seen.set(value, target)
223
+ this.#plan.seen.add(target)
224
+ if (this.#cooperative) {
225
+ try {
226
+ Object.preventExtensions(value)
227
+ } catch {
228
+ throw new TypeError(
229
+ 'Canonical asset source could not be adopted safely.'
230
+ )
231
+ }
232
+ }
233
+ const seal = {
234
+ array,
235
+ keys: keys ? [...keys] : null,
236
+ properties: [],
237
+ prototype,
238
+ shapeLocked: this.#cooperative,
239
+ target
240
+ }
241
+ if (array) {
242
+ const length = lengthDescriptor.value
243
+ if (!this.#cooperative && keys.length !== length + 1) {
244
+ throw new TypeError(
245
+ 'Canonical asset source arrays must be dense and plain.'
246
+ )
247
+ }
248
+ StructuredCloneAdoptionTraversal.#reserve(this.#state, length)
249
+ if (this.#cooperative) {
250
+ let index = 0
251
+ for (const key in value) {
252
+ if (!Object.hasOwn(value, key)) continue
253
+ if (key !== String(index) || index >= length) {
254
+ throw new TypeError(
255
+ 'Canonical asset source arrays must be dense and plain.'
256
+ )
257
+ }
258
+ seal.properties.push(
259
+ yield* this.#adoptProperty(value, key, depth, target)
260
+ )
261
+ index += 1
262
+ }
263
+ if (index !== length) {
264
+ throw new TypeError(
265
+ 'Canonical asset source arrays must be dense and plain.'
266
+ )
267
+ }
268
+ try {
269
+ Object.preventExtensions(target)
270
+ } catch {
271
+ throw new TypeError(
272
+ 'Canonical asset source could not be adopted safely.'
273
+ )
274
+ }
275
+ } else {
276
+ for (let index = 0; index < length; index += 1) {
277
+ seal.properties.push(
278
+ yield* this.#adoptProperty(value, String(index), depth)
279
+ )
280
+ }
281
+ }
282
+ seal.properties.push({
283
+ binary: false,
284
+ descriptor: {
285
+ ...StructuredCloneAdoptionTraversal.#arrayLengthDescriptor(
286
+ target
287
+ )
288
+ },
289
+ key: 'length'
290
+ })
291
+ this.#plan.seals.push(seal)
292
+ return target
293
+ }
294
+
295
+ if (this.#cooperative && keys.length > MAX_COOPERATIVE_RECORD_KEYS) {
296
+ throw new TypeError(
297
+ 'Canonical extension records have too many properties for cooperative preparation.'
298
+ )
299
+ }
300
+ StructuredCloneAdoptionTraversal.#reserve(this.#state, keys.length)
301
+ for (const key of keys) {
302
+ if (typeof key !== 'string') {
303
+ throw new TypeError(
304
+ 'Canonical asset source keys must be strings.'
305
+ )
306
+ }
307
+ seal.properties.push(yield* this.#adoptProperty(value, key, depth))
308
+ }
309
+ this.#plan.seals.push(seal)
310
+ return value
311
+ }
312
+
313
+ /**
314
+ * Validates and adopts one ordinary parent property.
315
+ * @param {object} owner Structured-clone parent.
316
+ * @param {string} key Property name.
317
+ * @param {number} depth Parent depth.
318
+ * @param {object} [target] Optional clean property owner.
319
+ * @returns {Generator<void, object, void>} Final property snapshot.
320
+ */
321
+ *#adoptProperty(owner, key, depth, target = owner) {
322
+ const descriptor = StructuredCloneAdoptionTraversal.#dataDescriptor(
323
+ owner,
324
+ key
325
+ )
326
+ if (descriptor.configurable !== true || descriptor.writable !== true) {
327
+ throw new TypeError(
328
+ 'Structured-cloned metadata must expose ordinary data properties.'
329
+ )
330
+ }
331
+ const adopted = yield* this.#adoptValue(descriptor.value, depth + 1)
332
+ const finalDescriptor = { ...descriptor, value: adopted }
333
+ if (target !== owner) {
334
+ try {
335
+ Object.defineProperty(target, key, finalDescriptor)
336
+ } catch {
337
+ throw new TypeError(
338
+ 'Canonical asset source could not be adopted safely.'
339
+ )
340
+ }
341
+ } else if (adopted !== descriptor.value) {
342
+ this.#replacements.push({
343
+ finalDescriptor,
344
+ key,
345
+ originalDescriptor: { ...descriptor },
346
+ owner
347
+ })
348
+ }
349
+ const binary = Boolean(
350
+ adopted &&
351
+ typeof adopted === 'object' &&
352
+ BinaryDataSnapshot.describeStandard(adopted)
353
+ )
354
+ if (binary) {
355
+ this.#plan.binaryCaptures.push(
356
+ yield* this.#captureBinaryProperty(target, key, finalDescriptor)
357
+ )
358
+ }
359
+ return {
360
+ binary,
361
+ descriptor: finalDescriptor,
362
+ key
363
+ }
364
+ }
365
+
366
+ /**
367
+ * Collects sealing snapshots for one normalized built-in subtree.
368
+ * @param {unknown} value Owned normalized value.
369
+ * @returns {Generator<void, void, void>} Bounded-work collection pass.
370
+ */
371
+ *#collectCapturedValue(value) {
372
+ yield* this.#checkpoint()
373
+ if (!value || typeof value !== 'object') return
374
+ if (BinaryDataSnapshot.describeStandard(value)) return
375
+ if (this.#plan.seen.has(value)) return
376
+ this.#plan.seen.add(value)
377
+
378
+ let prototype
379
+ let keys
380
+ const array = Array.isArray(value)
381
+ try {
382
+ prototype = Object.getPrototypeOf(value)
383
+ keys = this.#cooperative && array ? null : Reflect.ownKeys(value)
384
+ } catch {
385
+ throw new TypeError(
386
+ 'Canonical asset source could not be inspected safely.'
387
+ )
388
+ }
389
+ const plain = array
390
+ ? prototype === Array.prototype
391
+ : prototype === Object.prototype || prototype === null
392
+ if (!plain) {
393
+ throw new TypeError(
394
+ 'Canonical asset source must contain plain data objects.'
395
+ )
396
+ }
397
+ if (
398
+ this.#cooperative &&
399
+ !array &&
400
+ keys.length > MAX_COOPERATIVE_RECORD_KEYS
401
+ ) {
402
+ throw new TypeError(
403
+ 'Canonical extension records have too many properties for cooperative preparation.'
404
+ )
405
+ }
406
+ if (this.#cooperative) {
407
+ try {
408
+ Object.preventExtensions(value)
409
+ } catch {
410
+ throw new TypeError(
411
+ 'Canonical asset source could not be adopted safely.'
412
+ )
413
+ }
414
+ }
415
+ const seal = {
416
+ array,
417
+ keys: keys ? [...keys] : null,
418
+ properties: [],
419
+ prototype,
420
+ shapeLocked: this.#cooperative,
421
+ target: value
422
+ }
423
+ let lengthDescriptor = null
424
+ if (array && this.#cooperative) {
425
+ lengthDescriptor =
426
+ StructuredCloneAdoptionTraversal.#arrayLengthDescriptor(value)
427
+ for (let index = 0; index < lengthDescriptor.value; index += 1) {
428
+ yield* this.#collectCapturedProperty(value, String(index), seal)
429
+ }
430
+ } else {
431
+ for (const key of keys) {
432
+ if (array && key === 'length') continue
433
+ yield* this.#collectCapturedProperty(value, key, seal)
434
+ }
435
+ }
436
+ if (array) {
437
+ lengthDescriptor ||=
438
+ StructuredCloneAdoptionTraversal.#arrayLengthDescriptor(value)
439
+ seal.properties.push({
440
+ binary: false,
441
+ descriptor: { ...lengthDescriptor },
442
+ key: 'length'
443
+ })
444
+ }
445
+ this.#plan.seals.push(seal)
446
+ }
447
+
448
+ /**
449
+ * Collects one property from an internally normalized container.
450
+ * @param {object} owner Normalized property owner.
451
+ * @param {PropertyKey} key Property key.
452
+ * @param {object} seal Mutable sealing plan.
453
+ * @returns {Generator<void, void, void>} Bounded child collection pass.
454
+ */
455
+ *#collectCapturedProperty(owner, key, seal) {
456
+ if (typeof key !== 'string') {
457
+ throw new TypeError('Canonical asset source keys must be strings.')
458
+ }
459
+ const descriptor = StructuredCloneAdoptionTraversal.#dataDescriptor(
460
+ owner,
461
+ key
462
+ )
463
+ const child = descriptor.value
464
+ const binary = Boolean(
465
+ child &&
466
+ typeof child === 'object' &&
467
+ BinaryDataSnapshot.describeStandard(child)
468
+ )
469
+ if (binary) {
470
+ this.#plan.binaryCaptures.push(
471
+ yield* this.#captureBinaryProperty(owner, key, descriptor)
472
+ )
473
+ } else {
474
+ yield* this.#collectCapturedValue(child)
475
+ }
476
+ seal.properties.push({
477
+ binary,
478
+ descriptor: { ...descriptor },
479
+ key
480
+ })
481
+ }
482
+
483
+ /**
484
+ * Copies one binary property through an opaque branded capture in bounded
485
+ * byte ranges under the exclusive-ownership contract.
486
+ * @param {object} owner Binary property owner.
487
+ * @param {PropertyKey} key Binary property key.
488
+ * @param {PropertyDescriptor} descriptor Validated data descriptor.
489
+ * @returns {Generator<void, object, void>} Completed binary capture.
490
+ */
491
+ *#captureBinaryProperty(owner, key, descriptor) {
492
+ const capture = ProtectedExtensionBinaryBoundary.beginPropertyCapture(
493
+ owner,
494
+ key,
495
+ descriptor
496
+ )
497
+ let complete = false
498
+ while (!complete) {
499
+ complete =
500
+ ProtectedExtensionBinaryBoundary.copyPropertyCaptureChunk(
501
+ capture
502
+ )
503
+ yield* this.#checkpoint(MAX_SLICE_WORK)
504
+ }
505
+ return ProtectedExtensionBinaryBoundary.finishPropertyCapture(capture)
506
+ }
507
+
508
+ /**
509
+ * Locks one target property-by-property so large dense containers never
510
+ * require one monolithic Object.freeze operation.
511
+ * @param {object} seal Target sealing snapshot.
512
+ * @returns {Generator<void, void, void>} Bounded-work locking pass.
513
+ */
514
+ *#sealTargetCooperatively(seal) {
515
+ StructuredCloneAdoptionTraversal.#requireTargetShape(seal)
516
+ for (const property of seal.properties) {
517
+ const descriptor = StructuredCloneAdoptionTraversal.#descriptor(
518
+ seal.target,
519
+ property.key,
520
+ 'Canonical asset source changed during adoption.'
521
+ )
522
+ const matches = property.binary
523
+ ? ProtectedExtensionBinaryBoundary.isProtected(descriptor)
524
+ : StructuredCloneAdoptionTraversal.#sameDataDescriptor(
525
+ descriptor,
526
+ property.descriptor
527
+ )
528
+ if (!matches) {
529
+ throw new TypeError(
530
+ 'Canonical asset source changed during adoption.'
531
+ )
532
+ }
533
+ if (!property.binary) {
534
+ try {
535
+ Object.defineProperty(seal.target, property.key, {
536
+ ...descriptor,
537
+ configurable: false,
538
+ writable: false
539
+ })
540
+ } catch {
541
+ throw new TypeError(
542
+ 'Canonical document values could not be frozen safely.'
543
+ )
544
+ }
545
+ }
546
+ yield* this.#checkpoint()
547
+ }
548
+ StructuredCloneAdoptionTraversal.#requireTargetShape(seal)
549
+ if (!seal.shapeLocked) {
550
+ try {
551
+ Object.preventExtensions(seal.target)
552
+ } catch {
553
+ throw new TypeError(
554
+ 'Canonical document values could not be frozen safely.'
555
+ )
556
+ }
557
+ }
558
+ }
559
+
560
+ /**
561
+ * Creates a scheduling boundary after a bounded amount of traversal work.
562
+ * @param {number} [units] Additional work units.
563
+ * @returns {Generator<void, void, void>} Optional scheduling boundary.
564
+ */
565
+ *#checkpoint(units = 1) {
566
+ this.#work += units
567
+ if (this.#work < MAX_SLICE_WORK) return
568
+ this.#work = 0
569
+ yield undefined
570
+ }
571
+
572
+ /**
573
+ * Reads and validates one standard dense-array length descriptor.
574
+ * @param {unknown[]} owner Array candidate.
575
+ * @returns {PropertyDescriptor} Validated length descriptor.
576
+ */
577
+ static #arrayLengthDescriptor(owner) {
578
+ const descriptor = StructuredCloneAdoptionTraversal.#descriptor(
579
+ owner,
580
+ 'length',
581
+ 'Canonical asset source array could not be inspected.'
582
+ )
583
+ if (
584
+ !Object.hasOwn(descriptor, 'value') ||
585
+ !Number.isSafeInteger(descriptor.value) ||
586
+ descriptor.value < 0
587
+ ) {
588
+ throw new TypeError(
589
+ 'Canonical asset source arrays must be dense and plain.'
590
+ )
591
+ }
592
+ return descriptor
593
+ }
594
+
595
+ /**
596
+ * Reads one enumerable own data descriptor.
597
+ * @param {object} owner Property owner.
598
+ * @param {string} key Property name.
599
+ * @returns {PropertyDescriptor} Validated descriptor.
600
+ */
601
+ static #dataDescriptor(owner, key) {
602
+ const descriptor = StructuredCloneAdoptionTraversal.#descriptor(
603
+ owner,
604
+ key,
605
+ 'Canonical asset source properties could not be inspected.'
606
+ )
607
+ if (
608
+ !Object.hasOwn(descriptor, 'value') ||
609
+ descriptor.enumerable !== true
610
+ ) {
611
+ throw new TypeError(
612
+ 'Canonical asset source may contain only enumerable data properties.'
613
+ )
614
+ }
615
+ return descriptor
616
+ }
617
+
618
+ /**
619
+ * Reads one exact own descriptor with normalized failures.
620
+ * @param {object} owner Property owner.
621
+ * @param {PropertyKey} key Property key.
622
+ * @param {string} message Failure message.
623
+ * @returns {PropertyDescriptor} Existing descriptor.
624
+ */
625
+ static #descriptor(owner, key, message) {
626
+ let descriptor
627
+ try {
628
+ descriptor = Object.getOwnPropertyDescriptor(owner, key)
629
+ } catch {
630
+ throw new TypeError(message)
631
+ }
632
+ if (!descriptor) throw new TypeError(message)
633
+ return descriptor
634
+ }
635
+
636
+ /**
637
+ * Freezes one target only when its complete validated shape still matches.
638
+ * @param {object} seal Target sealing snapshot.
639
+ * @returns {void}
640
+ */
641
+ static sealTarget(seal) {
642
+ let prototype
643
+ let keys
644
+ let extensible
645
+ try {
646
+ prototype = Object.getPrototypeOf(seal.target)
647
+ keys = Reflect.ownKeys(seal.target)
648
+ extensible = Object.isExtensible(seal.target)
649
+ } catch {
650
+ throw new TypeError(
651
+ 'Canonical asset source changed during adoption.'
652
+ )
653
+ }
654
+ if (
655
+ prototype !== seal.prototype ||
656
+ extensible !== true ||
657
+ !StructuredCloneAdoptionTraversal.#sameKeys(keys, seal.keys)
658
+ ) {
659
+ throw new TypeError(
660
+ 'Canonical asset source changed during adoption.'
661
+ )
662
+ }
663
+ for (const property of seal.properties) {
664
+ const descriptor = StructuredCloneAdoptionTraversal.#descriptor(
665
+ seal.target,
666
+ property.key,
667
+ 'Canonical asset source changed during adoption.'
668
+ )
669
+ const matches = property.binary
670
+ ? ProtectedExtensionBinaryBoundary.isProtected(descriptor)
671
+ : StructuredCloneAdoptionTraversal.#sameDataDescriptor(
672
+ descriptor,
673
+ property.descriptor
674
+ )
675
+ if (!matches) {
676
+ throw new TypeError(
677
+ 'Canonical asset source changed during adoption.'
678
+ )
679
+ }
680
+ }
681
+ try {
682
+ Object.freeze(seal.target)
683
+ } catch {
684
+ throw new TypeError(
685
+ 'Canonical document values could not be frozen safely.'
686
+ )
687
+ }
688
+ }
689
+
690
+ /**
691
+ * Requires an unchanged prototype, extensibility state, and own-key set.
692
+ * @param {object} seal Target sealing snapshot.
693
+ * @returns {void}
694
+ */
695
+ static #requireTargetShape(seal) {
696
+ let prototype
697
+ let keys = null
698
+ let extensible
699
+ try {
700
+ prototype = Object.getPrototypeOf(seal.target)
701
+ extensible = Object.isExtensible(seal.target)
702
+ if (!seal.shapeLocked) keys = Reflect.ownKeys(seal.target)
703
+ } catch {
704
+ throw new TypeError(
705
+ 'Canonical asset source changed during adoption.'
706
+ )
707
+ }
708
+ if (
709
+ prototype !== seal.prototype ||
710
+ extensible !== !seal.shapeLocked ||
711
+ (!seal.shapeLocked &&
712
+ !StructuredCloneAdoptionTraversal.#sameKeys(keys, seal.keys))
713
+ ) {
714
+ throw new TypeError(
715
+ 'Canonical asset source changed during adoption.'
716
+ )
717
+ }
718
+ }
719
+
720
+ /**
721
+ * Compares exact data descriptor semantics without coercion.
722
+ * @param {PropertyDescriptor | undefined} current Current descriptor.
723
+ * @param {PropertyDescriptor} expected Captured descriptor.
724
+ * @returns {boolean} Whether both descriptors match.
725
+ */
726
+ static #sameDataDescriptor(current, expected) {
727
+ return Boolean(
728
+ current &&
729
+ Object.hasOwn(current, 'value') &&
730
+ Object.hasOwn(expected, 'value') &&
731
+ Object.is(current.value, expected.value) &&
732
+ current.configurable === expected.configurable &&
733
+ current.enumerable === expected.enumerable &&
734
+ current.writable === expected.writable
735
+ )
736
+ }
737
+
738
+ /**
739
+ * Compares an exact own-key snapshot without string coercion.
740
+ * @param {PropertyKey[]} current Current keys.
741
+ * @param {PropertyKey[]} expected Captured keys.
742
+ * @returns {boolean} Whether both key sequences match.
743
+ */
744
+ static #sameKeys(current, expected) {
745
+ if (current.length !== expected.length) return false
746
+ return current.every((key, index) => key === expected[index])
747
+ }
748
+
749
+ /**
750
+ * Reserves bounded traversal work.
751
+ * @param {Record<string, any>} state Capture state.
752
+ * @param {number} count Additional items.
753
+ * @returns {void}
754
+ */
755
+ static #reserve(state, count) {
756
+ if (
757
+ !Number.isSafeInteger(count) ||
758
+ count < 0 ||
759
+ state.items + count > state.maxItems
760
+ ) {
761
+ throw new TypeError(`${state.label} is too large.`)
762
+ }
763
+ state.items += count
764
+ }
765
+
766
+ /**
767
+ * Reserves bounded binary or text bytes.
768
+ * @param {Record<string, any>} state Capture state.
769
+ * @param {number} count Additional bytes.
770
+ * @returns {void}
771
+ */
772
+ static #reserveBytes(state, count) {
773
+ if (state.maxBytes === Number.MAX_SAFE_INTEGER) return
774
+ if (
775
+ !Number.isSafeInteger(count) ||
776
+ count < 0 ||
777
+ state.bytes + count > state.maxBytes
778
+ ) {
779
+ throw new TypeError(`${state.label} is too large.`)
780
+ }
781
+ state.bytes += count
782
+ }
783
+ }
784
+
785
+ /**
786
+ * Adopts structured-clone graphs synchronously or in bounded async slices.
787
+ */
788
+ export class StructuredCloneAdoption {
789
+ /**
790
+ * Adopts a graph synchronously while preserving the existing API contract.
791
+ * @param {unknown} value Structured-clone graph root.
792
+ * @param {Record<string, any>} state Shared capture state.
793
+ * @param {(value: unknown, state: Record<string, any>, depth: number) => unknown} capture Built-in fallback capture.
794
+ * @returns {unknown} Adopted graph.
795
+ */
796
+ static adopt(value, state, capture) {
797
+ StructuredCloneAdoption.#requireInputs(state, capture)
798
+ const candidate = StructuredCloneAdoption.#candidateState(state)
799
+ const traversal = new StructuredCloneAdoptionTraversal(
800
+ value,
801
+ candidate,
802
+ capture,
803
+ false
804
+ )
805
+ StructuredCloneAdoption.#drain(traversal.traverse())
806
+ StructuredCloneAdoption.#drain(traversal.applyReplacements())
807
+ StructuredCloneAdoption.#commitState(state, candidate)
808
+ const adopted = traversal.adopted
809
+ if (adopted && typeof adopted === 'object') {
810
+ TRUSTED_PLANS.add(traversal.plan)
811
+ ADOPTION_PLANS.set(adopted, traversal.plan)
812
+ }
813
+ return adopted
814
+ }
815
+
816
+ /**
817
+ * Adopts and seals a graph while yielding between bounded work slices.
818
+ * @param {unknown} value Structured-clone graph root.
819
+ * @param {Record<string, any>} state Shared capture state.
820
+ * @param {(value: unknown, state: Record<string, any>, depth: number) => unknown} capture Built-in fallback capture.
821
+ * @param {() => Promise<void> | void} yieldControl Host scheduler.
822
+ * @returns {Promise<unknown>} Adopted and sealed graph.
823
+ */
824
+ static async adoptAndSealAsync(value, state, capture, yieldControl) {
825
+ StructuredCloneAdoption.#requireInputs(state, capture)
826
+ if (typeof yieldControl !== 'function') {
827
+ throw new TypeError(
828
+ 'Structured-clone yield control must be a function.'
829
+ )
830
+ }
831
+ const candidate = StructuredCloneAdoption.#candidateState(state)
832
+ const traversal = new StructuredCloneAdoptionTraversal(
833
+ value,
834
+ candidate,
835
+ capture,
836
+ true
837
+ )
838
+ const traversalPass = await StructuredCloneAdoption.#drainAsync(
839
+ traversal.traverse(),
840
+ yieldControl
841
+ )
842
+ const replacementPass = await StructuredCloneAdoption.#drainAsync(
843
+ traversal.applyReplacements(),
844
+ yieldControl
845
+ )
846
+ const sealingPass = await StructuredCloneAdoption.#drainAsync(
847
+ traversal.seal(),
848
+ yieldControl
849
+ )
850
+ if (
851
+ traversalPass.yields + replacementPass.yields + sealingPass.yields >
852
+ 0
853
+ ) {
854
+ await yieldControl()
855
+ }
856
+ StructuredCloneAdoption.#commitState(state, candidate)
857
+ return traversal.adopted
858
+ }
859
+
860
+ /**
861
+ * Consumes one sync adoption plan for immediate sealing.
862
+ * @param {unknown} value Adopted graph root.
863
+ * @returns {object | null} Trusted one-use sealing plan.
864
+ */
865
+ static consume(value) {
866
+ if (!value || typeof value !== 'object') return null
867
+ const plan = ADOPTION_PLANS.get(value) || null
868
+ ADOPTION_PLANS.delete(value)
869
+ return plan
870
+ }
871
+
872
+ /**
873
+ * Seals one synchronously adopted graph from its trusted plan.
874
+ * @param {object} plan Sealing plan returned by consume.
875
+ * @returns {void}
876
+ */
877
+ static seal(plan) {
878
+ if (!TRUSTED_PLANS.has(plan)) {
879
+ throw new TypeError(
880
+ 'Structured-clone sealing requires a trusted adoption plan.'
881
+ )
882
+ }
883
+ TRUSTED_PLANS.delete(plan)
884
+ for (const captured of plan.binaryCaptures) {
885
+ ProtectedExtensionBinaryBoundary.protectCapturedProperty(captured)
886
+ }
887
+ for (const seal of plan.seals) {
888
+ StructuredCloneAdoptionTraversal.sealTarget(seal)
889
+ }
890
+ }
891
+
892
+ /**
893
+ * Validates shared state and the private fallback capture callback.
894
+ * @param {Record<string, any>} state Shared capture state.
895
+ * @param {unknown} capture Capture callback.
896
+ * @returns {void}
897
+ */
898
+ static #requireInputs(state, capture) {
899
+ if (
900
+ !(state?.seen instanceof Map) ||
901
+ !(state?.accounted instanceof Set) ||
902
+ !Number.isSafeInteger(state.bytes) ||
903
+ !Number.isSafeInteger(state.items) ||
904
+ !Number.isSafeInteger(state.maxBytes) ||
905
+ !Number.isSafeInteger(state.maxItems) ||
906
+ typeof state.label !== 'string' ||
907
+ typeof state.preserveBinary !== 'boolean' ||
908
+ state.standardBuiltins !== true ||
909
+ typeof capture !== 'function'
910
+ ) {
911
+ throw new TypeError(
912
+ 'Structured-clone adoption requires a proven capture state.'
913
+ )
914
+ }
915
+ }
916
+
917
+ /**
918
+ * Copies mutable accounting collections for atomic state publication.
919
+ * @param {Record<string, any>} state Shared capture state.
920
+ * @returns {Record<string, any>} Candidate state.
921
+ */
922
+ static #candidateState(state) {
923
+ return {
924
+ ...state,
925
+ accounted: new Set(state.accounted),
926
+ seen: new Map(state.seen)
927
+ }
928
+ }
929
+
930
+ /**
931
+ * Publishes successful candidate accounting into the shared state.
932
+ * @param {Record<string, any>} state Shared state.
933
+ * @param {Record<string, any>} candidate Candidate state.
934
+ * @returns {void}
935
+ */
936
+ static #commitState(state, candidate) {
937
+ state.accounted = candidate.accounted
938
+ state.bytes = candidate.bytes
939
+ state.items = candidate.items
940
+ state.seen = candidate.seen
941
+ }
942
+
943
+ /**
944
+ * Exhausts a bounded-work generator without scheduling boundaries.
945
+ * @param {Generator<void, unknown, void>} iterator Work iterator.
946
+ * @returns {unknown} Generator return value.
947
+ */
948
+ static #drain(iterator) {
949
+ let step = iterator.next()
950
+ while (!step.done) step = iterator.next()
951
+ return step.value
952
+ }
953
+
954
+ /**
955
+ * Steps a bounded-work generator across host scheduling boundaries.
956
+ * @param {Generator<void, unknown, void>} iterator Work iterator.
957
+ * @param {() => Promise<void> | void} yieldControl Host scheduler.
958
+ * @returns {Promise<{ value: unknown, yields: number }>} Generator result and boundary count.
959
+ */
960
+ static async #drainAsync(iterator, yieldControl) {
961
+ let yields = 0
962
+ let step = iterator.next()
963
+ while (!step.done) {
964
+ await yieldControl()
965
+ yields += 1
966
+ step = iterator.next()
967
+ }
968
+ return { value: step.value, yields }
969
+ }
970
+ }
971
+
972
+ Object.freeze(StructuredCloneAdoptionTraversal.prototype)
973
+ Object.freeze(StructuredCloneAdoptionTraversal)
974
+ Object.freeze(StructuredCloneAdoption.prototype)
975
+ Object.freeze(StructuredCloneAdoption)