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.
- package/README.md +57 -0
- package/docs/api.md +106 -0
- package/docs/release-notes-v1.3.0.md +45 -0
- package/docs/release-notes-v1.4.0.md +84 -0
- package/docs/testing.md +14 -0
- package/package.json +4 -2
- package/src/core/Parser.mjs +1 -1
- package/src/core/context/BinaryDataSnapshot.mjs +242 -43
- package/src/core/context/CircuitJsonDocumentContext.mjs +85 -3
- package/src/core/context/CircuitJsonExtensionBoundary.mjs +312 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +129 -60
- package/src/core/context/CircuitJsonValidationProof.mjs +126 -12
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +235 -2
- 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 +64 -6
- package/src/core/contracts/DocumentResult.mjs +38 -6
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
|
|
2
|
+
import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
|
|
3
|
+
import { StructuredDataSnapshot } from './StructuredDataSnapshot.mjs'
|
|
4
|
+
|
|
5
|
+
const OWNED_EXTENSION_ROOTS = new WeakSet()
|
|
6
|
+
const EXTENSION_METADATA_LIMITS = Object.freeze({
|
|
7
|
+
label: 'Canonical extension data',
|
|
8
|
+
maxBytes: 128 * 1024 * 1024,
|
|
9
|
+
maxItems: 4_000_000,
|
|
10
|
+
preserveBinary: true
|
|
11
|
+
})
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Owns and seals canonical extension graphs independently of document fields.
|
|
15
|
+
*/
|
|
16
|
+
export class CircuitJsonExtensionBoundary {
|
|
17
|
+
/**
|
|
18
|
+
* Returns whether an object is an already owned extension root.
|
|
19
|
+
* @param {unknown} value Extension candidate.
|
|
20
|
+
* @returns {boolean} Whether this boundary owns the root.
|
|
21
|
+
*/
|
|
22
|
+
static owns(value) {
|
|
23
|
+
return Boolean(
|
|
24
|
+
value &&
|
|
25
|
+
typeof value === 'object' &&
|
|
26
|
+
OWNED_EXTENSION_ROOTS.has(value)
|
|
27
|
+
)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Creates one deeply frozen source-extension snapshot.
|
|
32
|
+
* @param {unknown} value Extension candidate.
|
|
33
|
+
* @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalizer.
|
|
34
|
+
* @param {{ standardBuiltins?: boolean }} [options] Proven graph provenance.
|
|
35
|
+
* @returns {unknown} Deeply immutable owned extension metadata.
|
|
36
|
+
*/
|
|
37
|
+
static copyReadonly(value, normalize = null, options = {}) {
|
|
38
|
+
if (
|
|
39
|
+
CircuitJsonExtensionBoundary.owns(value) &&
|
|
40
|
+
Object.isFrozen(value) &&
|
|
41
|
+
normalize === null
|
|
42
|
+
) {
|
|
43
|
+
return value
|
|
44
|
+
}
|
|
45
|
+
if (normalize !== null && typeof normalize !== 'function') {
|
|
46
|
+
throw new TypeError('Extension normalizer must be a function.')
|
|
47
|
+
}
|
|
48
|
+
const standardBuiltins = options?.standardBuiltins === true
|
|
49
|
+
const state = StructuredDataSnapshot.createState({
|
|
50
|
+
...EXTENSION_METADATA_LIMITS,
|
|
51
|
+
standardBuiltins
|
|
52
|
+
})
|
|
53
|
+
const snapshot = standardBuiltins
|
|
54
|
+
? StructuredDataSnapshot.adoptStructuredClone(value, state)
|
|
55
|
+
: StructuredDataSnapshot.capture(value, state)
|
|
56
|
+
CircuitJsonExtensionBoundary.#rejectBinaryRoot(snapshot)
|
|
57
|
+
const adoption = standardBuiltins
|
|
58
|
+
? StructuredDataSnapshot.consumeStructuredCloneAdoption(snapshot)
|
|
59
|
+
: null
|
|
60
|
+
if (adoption && normalize === null) {
|
|
61
|
+
StructuredDataSnapshot.sealStructuredCloneAdoption(adoption)
|
|
62
|
+
} else {
|
|
63
|
+
ProtectedExtensionBinaryBoundary.protect(snapshot)
|
|
64
|
+
}
|
|
65
|
+
const normalized = normalize ? normalize(snapshot) : snapshot
|
|
66
|
+
if (normalized !== snapshot) {
|
|
67
|
+
ProtectedExtensionBinaryBoundary.protect(normalized)
|
|
68
|
+
}
|
|
69
|
+
if (!adoption || normalize !== null) {
|
|
70
|
+
CircuitJsonExtensionBoundary.#freezeValue(normalized)
|
|
71
|
+
}
|
|
72
|
+
CircuitJsonExtensionBoundary.#markOwned(normalized)
|
|
73
|
+
return normalized
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Cooperatively adopts one structured-clone extension graph.
|
|
78
|
+
* @param {unknown} value Extension candidate.
|
|
79
|
+
* @param {{ standardBuiltins?: boolean, yield: () => Promise<void> | void }} options Proven provenance and host scheduler.
|
|
80
|
+
* @returns {Promise<unknown>} Deeply immutable owned extension metadata.
|
|
81
|
+
*/
|
|
82
|
+
static async copyReadonlyAsync(value, options) {
|
|
83
|
+
if (
|
|
84
|
+
CircuitJsonExtensionBoundary.owns(value) &&
|
|
85
|
+
Object.isFrozen(value)
|
|
86
|
+
) {
|
|
87
|
+
return value
|
|
88
|
+
}
|
|
89
|
+
if (
|
|
90
|
+
options?.standardBuiltins !== true ||
|
|
91
|
+
typeof options?.yield !== 'function'
|
|
92
|
+
) {
|
|
93
|
+
return CircuitJsonExtensionBoundary.copyReadonly(
|
|
94
|
+
value,
|
|
95
|
+
null,
|
|
96
|
+
options
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
const state = StructuredDataSnapshot.createState({
|
|
100
|
+
...EXTENSION_METADATA_LIMITS,
|
|
101
|
+
standardBuiltins: true
|
|
102
|
+
})
|
|
103
|
+
const snapshot = await StructuredDataSnapshot.adoptStructuredCloneAsync(
|
|
104
|
+
value,
|
|
105
|
+
state,
|
|
106
|
+
options.yield
|
|
107
|
+
)
|
|
108
|
+
CircuitJsonExtensionBoundary.#rejectBinaryRoot(snapshot)
|
|
109
|
+
CircuitJsonExtensionBoundary.#markOwned(snapshot)
|
|
110
|
+
return snapshot
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Captures one document extension field synchronously.
|
|
115
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
116
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
117
|
+
* @returns {unknown} Owned extension root or undefined.
|
|
118
|
+
*/
|
|
119
|
+
static captureDocument(document, standardBuiltins) {
|
|
120
|
+
const descriptor =
|
|
121
|
+
CircuitJsonExtensionBoundary.#documentDescriptor(document)
|
|
122
|
+
if (!descriptor) return undefined
|
|
123
|
+
const captured = CircuitJsonExtensionBoundary.copyReadonly(
|
|
124
|
+
descriptor.value,
|
|
125
|
+
null,
|
|
126
|
+
{ standardBuiltins }
|
|
127
|
+
)
|
|
128
|
+
if (captured === descriptor.value) return captured
|
|
129
|
+
Object.defineProperty(document, 'extensions', {
|
|
130
|
+
...descriptor,
|
|
131
|
+
value: captured
|
|
132
|
+
})
|
|
133
|
+
return captured
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Cooperatively captures a document extension field and rejects replacement
|
|
138
|
+
* races across scheduling boundaries.
|
|
139
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
140
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
141
|
+
* @param {() => Promise<void> | void} yieldControl Host scheduler.
|
|
142
|
+
* @returns {Promise<unknown>} Owned extension root or undefined.
|
|
143
|
+
*/
|
|
144
|
+
static async captureDocumentAsync(
|
|
145
|
+
document,
|
|
146
|
+
standardBuiltins,
|
|
147
|
+
yieldControl
|
|
148
|
+
) {
|
|
149
|
+
const descriptor =
|
|
150
|
+
CircuitJsonExtensionBoundary.#documentDescriptor(document)
|
|
151
|
+
if (!descriptor) return undefined
|
|
152
|
+
const captured = await CircuitJsonExtensionBoundary.copyReadonlyAsync(
|
|
153
|
+
descriptor.value,
|
|
154
|
+
{
|
|
155
|
+
standardBuiltins,
|
|
156
|
+
yield: yieldControl
|
|
157
|
+
}
|
|
158
|
+
)
|
|
159
|
+
const current =
|
|
160
|
+
CircuitJsonExtensionBoundary.#documentDescriptor(document)
|
|
161
|
+
if (
|
|
162
|
+
!current ||
|
|
163
|
+
!CircuitJsonExtensionBoundary.#sameDataDescriptor(
|
|
164
|
+
current,
|
|
165
|
+
descriptor
|
|
166
|
+
)
|
|
167
|
+
) {
|
|
168
|
+
throw new TypeError(
|
|
169
|
+
'Canonical document extensions changed during adoption.'
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
if (captured === descriptor.value) return captured
|
|
173
|
+
Object.defineProperty(document, 'extensions', {
|
|
174
|
+
...descriptor,
|
|
175
|
+
value: captured
|
|
176
|
+
})
|
|
177
|
+
return captured
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Reads and validates the optional document extension descriptor.
|
|
182
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
183
|
+
* @returns {PropertyDescriptor | undefined} Extension descriptor.
|
|
184
|
+
*/
|
|
185
|
+
static #documentDescriptor(document) {
|
|
186
|
+
let descriptor
|
|
187
|
+
try {
|
|
188
|
+
descriptor = Object.getOwnPropertyDescriptor(document, 'extensions')
|
|
189
|
+
} catch {
|
|
190
|
+
throw new TypeError(
|
|
191
|
+
'Canonical document extensions could not be inspected safely.'
|
|
192
|
+
)
|
|
193
|
+
}
|
|
194
|
+
if (descriptor && !Object.hasOwn(descriptor, 'value')) {
|
|
195
|
+
throw new TypeError(
|
|
196
|
+
'Canonical document extensions must be an own data property.'
|
|
197
|
+
)
|
|
198
|
+
}
|
|
199
|
+
return descriptor
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Rejects unsupported binary extension roots.
|
|
204
|
+
* @param {unknown} value Extension root.
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
static #rejectBinaryRoot(value) {
|
|
208
|
+
if (BinaryDataSnapshot.describeStandard(value)) {
|
|
209
|
+
throw new TypeError(
|
|
210
|
+
'Canonical extension root must be a plain data container.'
|
|
211
|
+
)
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
/**
|
|
216
|
+
* Marks one object root as owned by this boundary.
|
|
217
|
+
* @param {unknown} value Extension root.
|
|
218
|
+
* @returns {void}
|
|
219
|
+
*/
|
|
220
|
+
static #markOwned(value) {
|
|
221
|
+
if (value && typeof value === 'object') {
|
|
222
|
+
OWNED_EXTENSION_ROOTS.add(value)
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Deeply freezes one captured plain-data graph.
|
|
228
|
+
* @param {unknown} value Captured graph.
|
|
229
|
+
* @returns {void}
|
|
230
|
+
*/
|
|
231
|
+
static #freezeValue(value) {
|
|
232
|
+
const seen = new Set()
|
|
233
|
+
const stack = [{ exit: false, value }]
|
|
234
|
+
while (stack.length) {
|
|
235
|
+
const frame = stack.pop()
|
|
236
|
+
const current = frame.value
|
|
237
|
+
if (frame.exit) {
|
|
238
|
+
try {
|
|
239
|
+
Object.freeze(current)
|
|
240
|
+
} catch {
|
|
241
|
+
throw new TypeError(
|
|
242
|
+
'Canonical document values could not be frozen safely.'
|
|
243
|
+
)
|
|
244
|
+
}
|
|
245
|
+
continue
|
|
246
|
+
}
|
|
247
|
+
if (!CircuitJsonExtensionBoundary.#container(current)) continue
|
|
248
|
+
if (seen.has(current)) continue
|
|
249
|
+
seen.add(current)
|
|
250
|
+
let descriptors
|
|
251
|
+
try {
|
|
252
|
+
descriptors = Object.getOwnPropertyDescriptors(current)
|
|
253
|
+
} catch {
|
|
254
|
+
throw new TypeError(
|
|
255
|
+
'Canonical document values could not be inspected safely.'
|
|
256
|
+
)
|
|
257
|
+
}
|
|
258
|
+
const children = []
|
|
259
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
260
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
261
|
+
if (
|
|
262
|
+
ProtectedExtensionBinaryBoundary.isProtected(descriptor)
|
|
263
|
+
) {
|
|
264
|
+
continue
|
|
265
|
+
}
|
|
266
|
+
throw new TypeError(
|
|
267
|
+
'Canonical document may contain only data properties.'
|
|
268
|
+
)
|
|
269
|
+
}
|
|
270
|
+
children.push(descriptor.value)
|
|
271
|
+
}
|
|
272
|
+
stack.push({ exit: true, value: current })
|
|
273
|
+
for (let index = children.length - 1; index >= 0; index -= 1) {
|
|
274
|
+
stack.push({ exit: false, value: children[index] })
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Returns true for plain extension containers.
|
|
281
|
+
* @param {unknown} value Candidate value.
|
|
282
|
+
* @returns {boolean} Whether the value is a plain container.
|
|
283
|
+
*/
|
|
284
|
+
static #container(value) {
|
|
285
|
+
if (!value || typeof value !== 'object') return false
|
|
286
|
+
if (Array.isArray(value)) {
|
|
287
|
+
return Object.getPrototypeOf(value) === Array.prototype
|
|
288
|
+
}
|
|
289
|
+
const prototype = Object.getPrototypeOf(value)
|
|
290
|
+
return prototype === Object.prototype || prototype === null
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Compares exact ordinary data descriptor semantics.
|
|
295
|
+
* @param {PropertyDescriptor} current Current descriptor.
|
|
296
|
+
* @param {PropertyDescriptor} expected Captured descriptor.
|
|
297
|
+
* @returns {boolean} Whether both descriptors match.
|
|
298
|
+
*/
|
|
299
|
+
static #sameDataDescriptor(current, expected) {
|
|
300
|
+
return Boolean(
|
|
301
|
+
Object.hasOwn(current, 'value') &&
|
|
302
|
+
Object.hasOwn(expected, 'value') &&
|
|
303
|
+
Object.is(current.value, expected.value) &&
|
|
304
|
+
current.configurable === expected.configurable &&
|
|
305
|
+
current.enumerable === expected.enumerable &&
|
|
306
|
+
current.writable === expected.writable
|
|
307
|
+
)
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
Object.freeze(CircuitJsonExtensionBoundary.prototype)
|
|
312
|
+
Object.freeze(CircuitJsonExtensionBoundary)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
|
|
2
|
+
import { CircuitJsonExtensionBoundary } from './CircuitJsonExtensionBoundary.mjs'
|
|
2
3
|
import { CircuitJsonMetadataBoundary } from './CircuitJsonMetadataBoundary.mjs'
|
|
3
4
|
import { CircuitJsonValidationAuthority } from './CircuitJsonValidationAuthority.mjs'
|
|
4
5
|
import { ProtectedExtensionBinaryBoundary } from './ProtectedExtensionBinaryBoundary.mjs'
|
|
@@ -10,12 +11,6 @@ const SEALED_ASSETS = new WeakSet()
|
|
|
10
11
|
const ASSET_PAYLOAD_LENGTHS = new WeakMap()
|
|
11
12
|
const OWNED_METADATA_ROOTS = new WeakSet()
|
|
12
13
|
const METADATA_BUDGETS = new WeakMap()
|
|
13
|
-
const EXTENSION_METADATA_LIMITS = Object.freeze({
|
|
14
|
-
label: 'Canonical extension data',
|
|
15
|
-
maxBytes: 128 * 1024 * 1024,
|
|
16
|
-
maxItems: 4_000_000,
|
|
17
|
-
preserveBinary: true
|
|
18
|
-
})
|
|
19
14
|
const ASSET_SCALAR_FIELDS = new Set([
|
|
20
15
|
'id',
|
|
21
16
|
'kind',
|
|
@@ -41,9 +36,10 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
41
36
|
* Seals an envelope whose exact model was already deeply frozen by validation.
|
|
42
37
|
* @param {Record<string, any>} document Canonical document envelope.
|
|
43
38
|
* @param {object[]} model Exact deeply frozen model owned by the proof.
|
|
39
|
+
* @param {{ standardBuiltins?: boolean }} [options] Proven metadata provenance.
|
|
44
40
|
* @returns {Record<string, any>} The same read-only envelope.
|
|
45
41
|
*/
|
|
46
|
-
static freezeValidated(document, model) {
|
|
42
|
+
static freezeValidated(document, model, options = {}) {
|
|
47
43
|
if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
|
|
48
44
|
throw new TypeError(
|
|
49
45
|
'Validated document sealing requires an unforgeable validation proof.'
|
|
@@ -76,7 +72,55 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
76
72
|
}
|
|
77
73
|
return CircuitJsonReadOnlyDocument.#freezeDocument(
|
|
78
74
|
document,
|
|
79
|
-
frozenRoots
|
|
75
|
+
frozenRoots,
|
|
76
|
+
options?.standardBuiltins === true
|
|
77
|
+
)
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Cooperatively seals an envelope whose exact model is already validated.
|
|
82
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
83
|
+
* @param {object[]} model Exact deeply frozen model owned by the proof.
|
|
84
|
+
* @param {{ standardBuiltins?: boolean, yield: () => Promise<void> | void }} options Proven provenance and host scheduler.
|
|
85
|
+
* @returns {Promise<Record<string, any>>} The same read-only envelope.
|
|
86
|
+
*/
|
|
87
|
+
static async freezeValidatedAsync(document, model, options) {
|
|
88
|
+
if (!CircuitJsonValidationAuthority.permitsSeal(model)) {
|
|
89
|
+
throw new TypeError(
|
|
90
|
+
'Validated document sealing requires an unforgeable validation proof.'
|
|
91
|
+
)
|
|
92
|
+
}
|
|
93
|
+
const descriptor = Object.getOwnPropertyDescriptor(document, 'model')
|
|
94
|
+
if (
|
|
95
|
+
!descriptor ||
|
|
96
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
97
|
+
descriptor.value !== model ||
|
|
98
|
+
!Array.isArray(model) ||
|
|
99
|
+
!Object.isFrozen(model) ||
|
|
100
|
+
typeof options?.yield !== 'function'
|
|
101
|
+
) {
|
|
102
|
+
throw new TypeError(
|
|
103
|
+
'Validated document sealing requires its exact frozen model.'
|
|
104
|
+
)
|
|
105
|
+
}
|
|
106
|
+
const frozenRoots = new Set([model])
|
|
107
|
+
const sourceReference = Object.getOwnPropertyDescriptor(
|
|
108
|
+
document,
|
|
109
|
+
'sourceReference'
|
|
110
|
+
)
|
|
111
|
+
if (
|
|
112
|
+
sourceReference &&
|
|
113
|
+
Object.hasOwn(sourceReference, 'value') &&
|
|
114
|
+
sourceReference.value &&
|
|
115
|
+
typeof sourceReference.value === 'object'
|
|
116
|
+
) {
|
|
117
|
+
frozenRoots.add(sourceReference.value)
|
|
118
|
+
}
|
|
119
|
+
return CircuitJsonReadOnlyDocument.#freezeDocumentAsync(
|
|
120
|
+
document,
|
|
121
|
+
frozenRoots,
|
|
122
|
+
options?.standardBuiltins === true,
|
|
123
|
+
options.yield
|
|
80
124
|
)
|
|
81
125
|
}
|
|
82
126
|
|
|
@@ -84,14 +128,55 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
84
128
|
* Captures owned boundaries and freezes an envelope with known frozen roots.
|
|
85
129
|
* @param {Record<string, any>} document Canonical document envelope.
|
|
86
130
|
* @param {Set<object>} frozenRoots Deeply frozen roots that need no revisit.
|
|
131
|
+
* @param {boolean} [standardBuiltins] Whether extension built-ins have proven standard prototypes.
|
|
87
132
|
* @returns {Record<string, any>} The same read-only envelope.
|
|
88
133
|
*/
|
|
89
|
-
static #freezeDocument(document, frozenRoots) {
|
|
134
|
+
static #freezeDocument(document, frozenRoots, standardBuiltins = false) {
|
|
90
135
|
const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
|
|
91
136
|
const metadataState = StructuredDataSnapshot.createState()
|
|
92
137
|
CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
|
|
93
138
|
const extensions =
|
|
94
|
-
CircuitJsonReadOnlyDocument.#captureDocumentExtensions(
|
|
139
|
+
CircuitJsonReadOnlyDocument.#captureDocumentExtensions(
|
|
140
|
+
document,
|
|
141
|
+
standardBuiltins
|
|
142
|
+
)
|
|
143
|
+
if (extensions && typeof extensions === 'object') {
|
|
144
|
+
frozenRoots.add(extensions)
|
|
145
|
+
}
|
|
146
|
+
CircuitJsonMetadataBoundary.normalize(document, assets, (value) =>
|
|
147
|
+
CircuitJsonReadOnlyDocument.#captureMetadataRoot(
|
|
148
|
+
value,
|
|
149
|
+
metadataState
|
|
150
|
+
)
|
|
151
|
+
)
|
|
152
|
+
CircuitJsonReadOnlyDocument.#freezeValue(document, frozenRoots)
|
|
153
|
+
return document
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Captures ordinary document boundaries while adopting extensions in
|
|
158
|
+
* bounded work slices.
|
|
159
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
160
|
+
* @param {Set<object>} frozenRoots Deeply frozen roots.
|
|
161
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
162
|
+
* @param {() => Promise<void> | void} yieldControl Host scheduler.
|
|
163
|
+
* @returns {Promise<Record<string, any>>} The same read-only envelope.
|
|
164
|
+
*/
|
|
165
|
+
static async #freezeDocumentAsync(
|
|
166
|
+
document,
|
|
167
|
+
frozenRoots,
|
|
168
|
+
standardBuiltins,
|
|
169
|
+
yieldControl
|
|
170
|
+
) {
|
|
171
|
+
const assets = CircuitJsonReadOnlyDocument.#documentAssets(document)
|
|
172
|
+
const metadataState = StructuredDataSnapshot.createState()
|
|
173
|
+
CircuitJsonReadOnlyDocument.#captureAssetData(assets, metadataState)
|
|
174
|
+
const extensions =
|
|
175
|
+
await CircuitJsonReadOnlyDocument.#captureDocumentExtensionsAsync(
|
|
176
|
+
document,
|
|
177
|
+
standardBuiltins,
|
|
178
|
+
yieldControl
|
|
179
|
+
)
|
|
95
180
|
if (extensions && typeof extensions === 'object') {
|
|
96
181
|
frozenRoots.add(extensions)
|
|
97
182
|
}
|
|
@@ -178,7 +263,8 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
178
263
|
if (
|
|
179
264
|
value &&
|
|
180
265
|
typeof value === 'object' &&
|
|
181
|
-
OWNED_METADATA_ROOTS.has(value)
|
|
266
|
+
(OWNED_METADATA_ROOTS.has(value) ||
|
|
267
|
+
CircuitJsonExtensionBoundary.owns(value)) &&
|
|
182
268
|
Object.isFrozen(value)
|
|
183
269
|
) {
|
|
184
270
|
return value
|
|
@@ -199,35 +285,15 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
199
285
|
* transfer-sized item and byte ceilings.
|
|
200
286
|
* @param {unknown} value Extension candidate.
|
|
201
287
|
* @param {((snapshot: unknown) => unknown) | null} [normalize] Optional normalization over the owned mutable snapshot.
|
|
288
|
+
* @param {{ standardBuiltins?: boolean }} [options] Proven source-graph provenance.
|
|
202
289
|
* @returns {unknown} Deeply immutable owned extension metadata.
|
|
203
290
|
*/
|
|
204
|
-
static copyReadonlyExtensionValue(value, normalize = null) {
|
|
205
|
-
|
|
206
|
-
value &&
|
|
207
|
-
typeof value === 'object' &&
|
|
208
|
-
OWNED_METADATA_ROOTS.has(value) &&
|
|
209
|
-
Object.isFrozen(value) &&
|
|
210
|
-
normalize === null
|
|
211
|
-
) {
|
|
212
|
-
return value
|
|
213
|
-
}
|
|
214
|
-
if (normalize !== null && typeof normalize !== 'function') {
|
|
215
|
-
throw new TypeError('Extension normalizer must be a function.')
|
|
216
|
-
}
|
|
217
|
-
const snapshot = StructuredDataSnapshot.capture(
|
|
291
|
+
static copyReadonlyExtensionValue(value, normalize = null, options = {}) {
|
|
292
|
+
return CircuitJsonExtensionBoundary.copyReadonly(
|
|
218
293
|
value,
|
|
219
|
-
|
|
294
|
+
normalize,
|
|
295
|
+
options
|
|
220
296
|
)
|
|
221
|
-
ProtectedExtensionBinaryBoundary.protect(snapshot)
|
|
222
|
-
const normalized = normalize ? normalize(snapshot) : snapshot
|
|
223
|
-
if (normalized !== snapshot) {
|
|
224
|
-
ProtectedExtensionBinaryBoundary.protect(normalized)
|
|
225
|
-
}
|
|
226
|
-
CircuitJsonReadOnlyDocument.#freezeValue(normalized, new Set())
|
|
227
|
-
if (normalized && typeof normalized === 'object') {
|
|
228
|
-
OWNED_METADATA_ROOTS.add(normalized)
|
|
229
|
-
}
|
|
230
|
-
return normalized
|
|
231
297
|
}
|
|
232
298
|
|
|
233
299
|
/**
|
|
@@ -327,32 +393,34 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
327
393
|
* Captures an unowned worker or caller extension root with extension-sized
|
|
328
394
|
* bounds before the generic source metadata pass begins.
|
|
329
395
|
* @param {Record<string, any>} document Canonical document envelope.
|
|
396
|
+
* @param {boolean} standardBuiltins Whether extension built-ins have proven standard prototypes.
|
|
330
397
|
* @returns {unknown} Owned extension root or undefined.
|
|
331
398
|
*/
|
|
332
|
-
static #captureDocumentExtensions(document) {
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
399
|
+
static #captureDocumentExtensions(document, standardBuiltins) {
|
|
400
|
+
return CircuitJsonExtensionBoundary.captureDocument(
|
|
401
|
+
document,
|
|
402
|
+
standardBuiltins
|
|
403
|
+
)
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Cooperatively captures a worker extension root and rejects document-level
|
|
408
|
+
* replacement races across scheduling boundaries.
|
|
409
|
+
* @param {Record<string, any>} document Canonical document envelope.
|
|
410
|
+
* @param {boolean} standardBuiltins Whether built-ins have local prototypes.
|
|
411
|
+
* @param {() => Promise<void> | void} yieldControl Host scheduler.
|
|
412
|
+
* @returns {Promise<unknown>} Owned extension root or undefined.
|
|
413
|
+
*/
|
|
414
|
+
static async #captureDocumentExtensionsAsync(
|
|
415
|
+
document,
|
|
416
|
+
standardBuiltins,
|
|
417
|
+
yieldControl
|
|
418
|
+
) {
|
|
419
|
+
return CircuitJsonExtensionBoundary.captureDocumentAsync(
|
|
420
|
+
document,
|
|
421
|
+
standardBuiltins,
|
|
422
|
+
yieldControl
|
|
349
423
|
)
|
|
350
|
-
if (captured === descriptor.value) return captured
|
|
351
|
-
Object.defineProperty(document, 'extensions', {
|
|
352
|
-
...descriptor,
|
|
353
|
-
value: captured
|
|
354
|
-
})
|
|
355
|
-
return captured
|
|
356
424
|
}
|
|
357
425
|
|
|
358
426
|
/**
|
|
@@ -671,7 +739,8 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
671
739
|
if (
|
|
672
740
|
value &&
|
|
673
741
|
typeof value === 'object' &&
|
|
674
|
-
OWNED_METADATA_ROOTS.has(value)
|
|
742
|
+
(OWNED_METADATA_ROOTS.has(value) ||
|
|
743
|
+
CircuitJsonExtensionBoundary.owns(value))
|
|
675
744
|
) {
|
|
676
745
|
return value
|
|
677
746
|
}
|