circuitjson-toolkit 1.3.0 → 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.
- package/README.md +48 -0
- package/docs/api.md +84 -0
- package/docs/release-notes-v1.4.0.md +84 -0
- package/package.json +2 -1
- package/src/core/Parser.mjs +1 -1
- package/src/core/context/BinaryDataSnapshot.mjs +59 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +55 -0
- package/src/core/context/CircuitJsonExtensionBoundary.mjs +312 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +115 -59
- package/src/core/context/CircuitJsonValidationProof.mjs +122 -10
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +233 -0
- package/src/core/context/StructuredCloneAdoption.mjs +975 -0
- package/src/core/context/StructuredCloneCollectionNormalizer.mjs +86 -0
- package/src/core/context/StructuredCloneTextAccounting.mjs +55 -0
- package/src/core/context/StructuredDataSnapshot.mjs +53 -0
- package/src/core/contracts/DocumentResult.mjs +38 -6
|
@@ -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)
|