circuitjson-toolkit 1.3.0 → 1.4.1
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 -2
- package/docs/api.md +118 -1
- package/docs/release-notes-v1.4.0.md +84 -0
- package/docs/release-notes-v1.4.1.md +27 -0
- package/package.json +3 -1
- package/src/core/Parser.mjs +1 -1
- package/src/core/SelfAdjustingComputation.mjs +610 -0
- 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
- package/src/index.mjs +1 -0
- package/src/testing/ToolkitContractFixtures.mjs +1 -0
|
@@ -88,6 +88,31 @@ export class CircuitJsonDocumentContext {
|
|
|
88
88
|
return CircuitJsonDocumentContext.#prepare(input, options, true)
|
|
89
89
|
}
|
|
90
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Destructively adopts an exclusively transferred structured-clone graph
|
|
93
|
+
* while allowing hosts to service input and paint between bounded slices.
|
|
94
|
+
* @param {unknown} input Structured-cloned document result or existing context.
|
|
95
|
+
* @param {{ indexes?: unknown, ownership: 'exclusive', yield?: () => Promise<void> | void }} [options] Requested indexes, required exclusive ownership authority, and host scheduler.
|
|
96
|
+
* @returns {Promise<CircuitJsonDocumentContext>} Prepared request-scoped context.
|
|
97
|
+
*/
|
|
98
|
+
static async prepareStructuredCloneAsync(input, options = {}) {
|
|
99
|
+
if (CircuitJsonDocumentContext.#isContext(input)) {
|
|
100
|
+
return CircuitJsonDocumentContext.#prepare(input, options, true)
|
|
101
|
+
}
|
|
102
|
+
if (options?.ownership !== 'exclusive') {
|
|
103
|
+
throw new TypeError(
|
|
104
|
+
'Cooperative structured-clone preparation requires exclusive ownership.'
|
|
105
|
+
)
|
|
106
|
+
}
|
|
107
|
+
const context = await CircuitJsonDocumentContext.#fromInputAsync(
|
|
108
|
+
input,
|
|
109
|
+
true,
|
|
110
|
+
options?.yield
|
|
111
|
+
)
|
|
112
|
+
context.#indexes.ensure(options?.indexes || [])
|
|
113
|
+
return context
|
|
114
|
+
}
|
|
115
|
+
|
|
91
116
|
/**
|
|
92
117
|
* Prepares one context with explicit metadata provenance.
|
|
93
118
|
* @param {unknown} input Document result, CircuitJSON model, or context.
|
|
@@ -229,6 +254,36 @@ export class CircuitJsonDocumentContext {
|
|
|
229
254
|
)
|
|
230
255
|
}
|
|
231
256
|
|
|
257
|
+
/**
|
|
258
|
+
* Creates one context while yielding between model validation and envelope
|
|
259
|
+
* sealing for a proven structured-clone graph.
|
|
260
|
+
* @param {unknown} input Document result or CircuitJSON model.
|
|
261
|
+
* @param {boolean} standardBuiltins Whether metadata built-ins have standard local prototypes.
|
|
262
|
+
* @param {(() => Promise<void> | void) | undefined} yieldControl Host scheduler.
|
|
263
|
+
* @returns {Promise<CircuitJsonDocumentContext>} New context.
|
|
264
|
+
*/
|
|
265
|
+
static async #fromInputAsync(input, standardBuiltins, yieldControl) {
|
|
266
|
+
const document = CircuitJsonDocumentContext.#normalizeDocument(input)
|
|
267
|
+
const validationPasses = CircuitJsonValidationProof.has(document)
|
|
268
|
+
? 0
|
|
269
|
+
: 1
|
|
270
|
+
const readonlyDocument =
|
|
271
|
+
await CircuitJsonValidationProof.validateAndAttachAsync(document, {
|
|
272
|
+
standardBuiltins,
|
|
273
|
+
yield: yieldControl
|
|
274
|
+
})
|
|
275
|
+
const model = CircuitJsonDocumentContext.#ownData(
|
|
276
|
+
readonlyDocument,
|
|
277
|
+
'model'
|
|
278
|
+
)
|
|
279
|
+
return new CircuitJsonDocumentContext(
|
|
280
|
+
readonlyDocument,
|
|
281
|
+
model,
|
|
282
|
+
validationPasses,
|
|
283
|
+
CONTEXT_CONSTRUCTION_AUTHORITY
|
|
284
|
+
)
|
|
285
|
+
}
|
|
286
|
+
|
|
232
287
|
/**
|
|
233
288
|
* Normalizes supported context input into a canonical document envelope.
|
|
234
289
|
* @param {unknown} input Document result or CircuitJSON model.
|
|
@@ -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',
|
|
@@ -82,6 +77,53 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
82
77
|
)
|
|
83
78
|
}
|
|
84
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
|
|
124
|
+
)
|
|
125
|
+
}
|
|
126
|
+
|
|
85
127
|
/**
|
|
86
128
|
* Captures owned boundaries and freezes an envelope with known frozen roots.
|
|
87
129
|
* @param {Record<string, any>} document Canonical document envelope.
|
|
@@ -111,6 +153,43 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
111
153
|
return document
|
|
112
154
|
}
|
|
113
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
|
+
)
|
|
180
|
+
if (extensions && typeof extensions === 'object') {
|
|
181
|
+
frozenRoots.add(extensions)
|
|
182
|
+
}
|
|
183
|
+
CircuitJsonMetadataBoundary.normalize(document, assets, (value) =>
|
|
184
|
+
CircuitJsonReadOnlyDocument.#captureMetadataRoot(
|
|
185
|
+
value,
|
|
186
|
+
metadataState
|
|
187
|
+
)
|
|
188
|
+
)
|
|
189
|
+
CircuitJsonReadOnlyDocument.#freezeValue(document, frozenRoots)
|
|
190
|
+
return document
|
|
191
|
+
}
|
|
192
|
+
|
|
114
193
|
/**
|
|
115
194
|
* Reads only an internally-created defensive asset data getter.
|
|
116
195
|
* @param {PropertyDescriptor | undefined} descriptor Data descriptor.
|
|
@@ -184,7 +263,8 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
184
263
|
if (
|
|
185
264
|
value &&
|
|
186
265
|
typeof value === 'object' &&
|
|
187
|
-
OWNED_METADATA_ROOTS.has(value)
|
|
266
|
+
(OWNED_METADATA_ROOTS.has(value) ||
|
|
267
|
+
CircuitJsonExtensionBoundary.owns(value)) &&
|
|
188
268
|
Object.isFrozen(value)
|
|
189
269
|
) {
|
|
190
270
|
return value
|
|
@@ -209,35 +289,11 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
209
289
|
* @returns {unknown} Deeply immutable owned extension metadata.
|
|
210
290
|
*/
|
|
211
291
|
static copyReadonlyExtensionValue(value, normalize = null, options = {}) {
|
|
212
|
-
|
|
213
|
-
value &&
|
|
214
|
-
typeof value === 'object' &&
|
|
215
|
-
OWNED_METADATA_ROOTS.has(value) &&
|
|
216
|
-
Object.isFrozen(value) &&
|
|
217
|
-
normalize === null
|
|
218
|
-
) {
|
|
219
|
-
return value
|
|
220
|
-
}
|
|
221
|
-
if (normalize !== null && typeof normalize !== 'function') {
|
|
222
|
-
throw new TypeError('Extension normalizer must be a function.')
|
|
223
|
-
}
|
|
224
|
-
const snapshot = StructuredDataSnapshot.capture(
|
|
292
|
+
return CircuitJsonExtensionBoundary.copyReadonly(
|
|
225
293
|
value,
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
standardBuiltins: options?.standardBuiltins === true
|
|
229
|
-
})
|
|
294
|
+
normalize,
|
|
295
|
+
options
|
|
230
296
|
)
|
|
231
|
-
ProtectedExtensionBinaryBoundary.protect(snapshot)
|
|
232
|
-
const normalized = normalize ? normalize(snapshot) : snapshot
|
|
233
|
-
if (normalized !== snapshot) {
|
|
234
|
-
ProtectedExtensionBinaryBoundary.protect(normalized)
|
|
235
|
-
}
|
|
236
|
-
CircuitJsonReadOnlyDocument.#freezeValue(normalized, new Set())
|
|
237
|
-
if (normalized && typeof normalized === 'object') {
|
|
238
|
-
OWNED_METADATA_ROOTS.add(normalized)
|
|
239
|
-
}
|
|
240
|
-
return normalized
|
|
241
297
|
}
|
|
242
298
|
|
|
243
299
|
/**
|
|
@@ -341,31 +397,30 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
341
397
|
* @returns {unknown} Owned extension root or undefined.
|
|
342
398
|
*/
|
|
343
399
|
static #captureDocumentExtensions(document, standardBuiltins) {
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
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
|
|
362
423
|
)
|
|
363
|
-
if (captured === descriptor.value) return captured
|
|
364
|
-
Object.defineProperty(document, 'extensions', {
|
|
365
|
-
...descriptor,
|
|
366
|
-
value: captured
|
|
367
|
-
})
|
|
368
|
-
return captured
|
|
369
424
|
}
|
|
370
425
|
|
|
371
426
|
/**
|
|
@@ -684,7 +739,8 @@ export class CircuitJsonReadOnlyDocument {
|
|
|
684
739
|
if (
|
|
685
740
|
value &&
|
|
686
741
|
typeof value === 'object' &&
|
|
687
|
-
OWNED_METADATA_ROOTS.has(value)
|
|
742
|
+
(OWNED_METADATA_ROOTS.has(value) ||
|
|
743
|
+
CircuitJsonExtensionBoundary.owns(value))
|
|
688
744
|
) {
|
|
689
745
|
return value
|
|
690
746
|
}
|