circuitjson-toolkit 1.4.1 → 1.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -0
- package/docs/api.md +19 -3
- package/docs/model-format.md +4 -4
- package/docs/release-notes-v1.4.2.md +32 -0
- package/package.json +2 -1
- package/src/core/SelfAdjustingComputation.mjs +89 -100
- package/src/core/context/CircuitJsonExtensionBoundary.mjs +1 -1
- package/src/core/context/StructuredCloneAdoption.mjs +9 -25
- package/src/core/context/StructuredCloneTextAccounting.mjs +38 -0
- package/src/core/contracts/DocumentResult.mjs +9 -0
- package/src/core/worker/NativeParserWorkerTransport.mjs +201 -0
- package/src/core/worker/ParserWorkerClient.mjs +18 -20
- package/src/core/worker/WorkerBinaryData.mjs +201 -0
- package/src/core/worker/WorkerRequestData.mjs +64 -213
- package/src/core/worker/WorkerResponseData.mjs +9 -2
- package/src/core/worker/WorkerResultShape.mjs +6 -1
package/README.md
CHANGED
|
@@ -83,6 +83,12 @@ dynamic data/control dependencies, changed-root reader lists, stale-trace
|
|
|
83
83
|
replacement, explicit reclamation, and from-scratch consistency coverage. See
|
|
84
84
|
the [1.4.1 release notes](docs/release-notes-v1.4.1.md).
|
|
85
85
|
|
|
86
|
+
Version 1.4.2 accelerates immutable document rebuilding and cooperative
|
|
87
|
+
structured-clone adoption. Toolkit-owned frozen extension roots retain their
|
|
88
|
+
identity, and cooperative finalization freezes each validated container
|
|
89
|
+
atomically instead of redefining every property. See the
|
|
90
|
+
[1.4.2 release notes](docs/release-notes-v1.4.2.md).
|
|
91
|
+
|
|
86
92
|
Before 1.1.0:
|
|
87
93
|
|
|
88
94
|
```js
|
|
@@ -393,6 +399,7 @@ copy while keeping sync, direct async, and worker results mutation-isolated.
|
|
|
393
399
|
- [1.3.0 release notes](docs/release-notes-v1.3.0.md)
|
|
394
400
|
- [1.4.0 release notes](docs/release-notes-v1.4.0.md)
|
|
395
401
|
- [1.4.1 release notes](docs/release-notes-v1.4.1.md)
|
|
402
|
+
- [1.4.2 release notes](docs/release-notes-v1.4.2.md)
|
|
396
403
|
- [Library scope](spec/library-scope.md)
|
|
397
404
|
|
|
398
405
|
## Package scope
|
package/docs/api.md
CHANGED
|
@@ -85,11 +85,12 @@ make the common shape smaller.
|
|
|
85
85
|
|
|
86
86
|
Validated source documents own selected extension values as one deeply
|
|
87
87
|
immutable, descriptor-safe snapshot. This uses a distinct 128 MiB payload and
|
|
88
|
-
|
|
88
|
+
8,000,000-item ceiling so large native renderer graphs do not inherit the
|
|
89
89
|
compact metadata limit. The shared worker protocol applies the same ownership
|
|
90
90
|
boundary after structured cloning. Canonical standalone documents retain a
|
|
91
|
-
250 MB byte ceiling; exact multi-document projects use a
|
|
92
|
-
ceiling with independent 250 MB document
|
|
91
|
+
10,000,000-value and 250 MB byte ceiling; exact multi-document projects use a
|
|
92
|
+
16,000,000-value and 256 MiB aggregate ceiling with independent 250 MB document
|
|
93
|
+
and project-metadata limits. Binary
|
|
93
94
|
values preserve their `ArrayBuffer`/typed-view shape behind defensive copy
|
|
94
95
|
access, so payload bytes do not inflate into one JavaScript number per byte.
|
|
95
96
|
|
|
@@ -318,6 +319,15 @@ The parser subpath additionally exports `ParserWorkerClient`,
|
|
|
318
319
|
`ToolkitWorkerProtocol`, and the `TOOLKIT_WORKER_PROTOCOL` version constant for
|
|
319
320
|
hosts that own a custom worker lifecycle.
|
|
320
321
|
|
|
322
|
+
`ParserWorkerClient.fromWorkerUrl(url)` creates a lazy module-worker client
|
|
323
|
+
that privately owns its native worker and received result graphs. Native
|
|
324
|
+
message provenance avoids repeated binary brand probing on ordinary records;
|
|
325
|
+
all result shape, model, depth, count, and byte limits still apply. Shared and
|
|
326
|
+
resizable buffers remain isolated. `new ParserWorkerClient({ createWorker })`
|
|
327
|
+
continues to fully inspect custom-worker results, including genuine events
|
|
328
|
+
whose payloads an earlier listener may have changed. Replacing the host Worker
|
|
329
|
+
constructor also retains that defensive path.
|
|
330
|
+
|
|
321
331
|
### `ToolkitAsset.measure(asset)`
|
|
322
332
|
|
|
323
333
|
Returns the exact resident payload byte length without copying. Binary lengths
|
|
@@ -711,3 +721,9 @@ circuitjson-toolkit/testing
|
|
|
711
721
|
circuitjson-toolkit/workers/parser.worker.mjs
|
|
712
722
|
circuitjson-toolkit/styles/renderers.css
|
|
713
723
|
```
|
|
724
|
+
|
|
725
|
+
Worker payload preparation uses captured array/view brand checks before raw
|
|
726
|
+
buffer probes. Short strings are counted directly in UTF-8, including surrogate
|
|
727
|
+
pairs and replacement characters, without allocating encoded byte buffers.
|
|
728
|
+
Cross-realm buffers, changed prototypes, and bounded binary ownership retain
|
|
729
|
+
the same validation rules.
|
package/docs/model-format.md
CHANGED
|
@@ -102,12 +102,12 @@ must not duplicate the CircuitJSON model or keep a renamed renderer model.
|
|
|
102
102
|
Default parsing excludes raw/base64/full-payload graphs.
|
|
103
103
|
|
|
104
104
|
When a caller explicitly selects a native extension, the toolkit captures and
|
|
105
|
-
freezes that graph once under a distinct ceiling of
|
|
105
|
+
freezes that graph once under a distinct ceiling of 8,000,000 structured items
|
|
106
106
|
and 128 MiB of string or binary content. The ceiling is shared across the
|
|
107
107
|
selected namespace and applies equally after worker transfer. Canonical
|
|
108
|
-
standalone documents allow the extension plus envelope overhead within
|
|
109
|
-
million result values and 250 MB. Exact project envelopes allow
|
|
110
|
-
aggregate values and 256 MiB, while each document stays within
|
|
108
|
+
standalone documents allow the extension plus envelope overhead within ten
|
|
109
|
+
million result values and 250 MB. Exact project envelopes allow sixteen million
|
|
110
|
+
aggregate values and 256 MiB, while each document stays within ten million
|
|
111
111
|
values and 250 MB and non-document project metadata stays within two million
|
|
112
112
|
values and 250 MB. Reused graphs are charged independently to each local scope,
|
|
113
113
|
and repeated accounting work is itself bounded by the aggregate project
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# circuitjson-toolkit 1.4.2
|
|
2
|
+
|
|
3
|
+
This patch release removes repeated immutable-graph work from toolkit-owned
|
|
4
|
+
document rebuilds and from cooperative structured-clone adoption. Public
|
|
5
|
+
document shapes, validation rules, cancellation behavior, and extension
|
|
6
|
+
selection semantics remain unchanged.
|
|
7
|
+
|
|
8
|
+
## Owned extension root reuse
|
|
9
|
+
|
|
10
|
+
- `DocumentResult.createValidated()` retains an extension namespace that was
|
|
11
|
+
already captured, bounded, sealed, and branded by the same toolkit runtime.
|
|
12
|
+
- Rebuilding a canonical document no longer traverses or copies a large frozen
|
|
13
|
+
native extension graph when only other document fields changed.
|
|
14
|
+
- Arbitrary caller objects still use the defensive validation and ownership
|
|
15
|
+
path; frozen state alone never grants trusted ownership.
|
|
16
|
+
|
|
17
|
+
## Atomic cooperative finalization
|
|
18
|
+
|
|
19
|
+
- Cooperative structured-clone adoption validates a container's descriptors
|
|
20
|
+
before freezing the container in one atomic operation.
|
|
21
|
+
- A yield can no longer expose a container with only some properties locked.
|
|
22
|
+
- Large graphs avoid one `defineProperty()` call per ordinary property while
|
|
23
|
+
retaining the same deeply immutable result and cancellation checkpoints.
|
|
24
|
+
|
|
25
|
+
## Verification
|
|
26
|
+
|
|
27
|
+
- Ownership tests cover identity reuse for toolkit-built frozen namespaces and
|
|
28
|
+
defensive behavior for untrusted objects.
|
|
29
|
+
- Cooperative snapshot tests prove that no partial property locking is visible
|
|
30
|
+
across yields.
|
|
31
|
+
- The complete package suite, formatting check, packed-entrypoint checks, and
|
|
32
|
+
npm dry-run gate the release.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circuitjson-toolkit",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.3",
|
|
4
4
|
"description": "Canonical CircuitJSON parsing, project, rendering, query, manufacturing, simulation, and scene contracts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"circuitjson",
|
|
@@ -58,6 +58,7 @@
|
|
|
58
58
|
"docs/release-notes-v1.3.0.md",
|
|
59
59
|
"docs/release-notes-v1.4.0.md",
|
|
60
60
|
"docs/release-notes-v1.4.1.md",
|
|
61
|
+
"docs/release-notes-v1.4.2.md",
|
|
61
62
|
"docs/testing.md",
|
|
62
63
|
"spec",
|
|
63
64
|
"LICENSE",
|
|
@@ -17,15 +17,9 @@ export class SelfAdjustingComputation {
|
|
|
17
17
|
/** @type {Set<string> | null} */
|
|
18
18
|
#affectedComputations
|
|
19
19
|
|
|
20
|
-
/** @type {(value: object, path: PropertyKey[]) => boolean} */
|
|
20
|
+
/** @type {((value: object, path: PropertyKey[]) => boolean) | null} */
|
|
21
21
|
#isAtomic
|
|
22
22
|
|
|
23
|
-
/** @type {Map<symbol, number>} */
|
|
24
|
-
#symbolIds
|
|
25
|
-
|
|
26
|
-
/** @type {number} */
|
|
27
|
-
#nextSymbolId
|
|
28
|
-
|
|
29
23
|
/**
|
|
30
24
|
* @param {{ isAtomic?: (value: object, path: PropertyKey[]) => boolean }} [options] Dependency-tracking options.
|
|
31
25
|
*/
|
|
@@ -36,11 +30,7 @@ export class SelfAdjustingComputation {
|
|
|
36
30
|
this.#propagationInput = null
|
|
37
31
|
this.#affectedComputations = null
|
|
38
32
|
this.#isAtomic =
|
|
39
|
-
typeof options.isAtomic === 'function'
|
|
40
|
-
? options.isAtomic
|
|
41
|
-
: () => false
|
|
42
|
-
this.#symbolIds = new Map()
|
|
43
|
-
this.#nextSymbolId = 1
|
|
33
|
+
typeof options.isAtomic === 'function' ? options.isAtomic : null
|
|
44
34
|
}
|
|
45
35
|
|
|
46
36
|
/**
|
|
@@ -264,16 +254,14 @@ export class SelfAdjustingComputation {
|
|
|
264
254
|
* @returns {{ dependencies: object[], value: any }} Successful trace.
|
|
265
255
|
*/
|
|
266
256
|
#trace(input, computation) {
|
|
267
|
-
const dependencies =
|
|
257
|
+
const dependencies = []
|
|
268
258
|
const proxyCache = new WeakMap()
|
|
269
259
|
const proxyMetadata = new WeakMap()
|
|
270
|
-
const trackedInput = this.#createProxy(
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
proxyMetadata
|
|
276
|
-
})
|
|
260
|
+
const trackedInput = this.#createProxy(
|
|
261
|
+
{ dependencies, proxyCache, proxyMetadata },
|
|
262
|
+
input,
|
|
263
|
+
{ path: [], children: null, observations: null }
|
|
264
|
+
)
|
|
277
265
|
const trackedValue = computation(trackedInput)
|
|
278
266
|
if (SelfAdjustingComputation.#isPromiseLike(trackedValue)) {
|
|
279
267
|
throw new TypeError(
|
|
@@ -287,39 +275,44 @@ export class SelfAdjustingComputation {
|
|
|
287
275
|
? proxyMetadata.get(trackedValue)
|
|
288
276
|
: null
|
|
289
277
|
if (metadata) {
|
|
290
|
-
this.#record(
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
278
|
+
this.#record(
|
|
279
|
+
dependencies,
|
|
280
|
+
metadata.pathNode,
|
|
281
|
+
'value',
|
|
282
|
+
metadata.target
|
|
283
|
+
)
|
|
295
284
|
}
|
|
296
285
|
|
|
297
286
|
return {
|
|
298
|
-
|
|
287
|
+
// Later reads of a returned container must not rewrite this trace.
|
|
288
|
+
dependencies: dependencies.map((dependency) => ({ ...dependency })),
|
|
299
289
|
value: metadata?.target ?? trackedValue
|
|
300
290
|
}
|
|
301
291
|
}
|
|
302
292
|
|
|
303
293
|
/**
|
|
304
294
|
* Creates a read-tracking proxy for one traversable input container.
|
|
305
|
-
* @param {{
|
|
295
|
+
* @param {{ dependencies: object[], proxyCache: WeakMap<object, Map<object, object>>, proxyMetadata: WeakMap<object, { target: object, pathNode: object }> }} context Shared trace context.
|
|
296
|
+
* @param {object} target Traversable raw input container.
|
|
297
|
+
* @param {object} pathNode Interned input path in this trace.
|
|
306
298
|
* @returns {object} Read-tracking proxy.
|
|
307
299
|
*/
|
|
308
|
-
#createProxy(context) {
|
|
309
|
-
const
|
|
310
|
-
const
|
|
311
|
-
if (
|
|
312
|
-
return
|
|
300
|
+
#createProxy(context, target, pathNode) {
|
|
301
|
+
const cachedByPath = context.proxyCache.get(target)
|
|
302
|
+
const cached = cachedByPath?.get(pathNode)
|
|
303
|
+
if (cached) {
|
|
304
|
+
return cached
|
|
313
305
|
}
|
|
314
306
|
|
|
315
|
-
const
|
|
307
|
+
const proxyContext = { ...context, pathNode }
|
|
308
|
+
const proxy = new Proxy(target, {
|
|
316
309
|
get: (target, property) =>
|
|
317
|
-
this.#readProperty(
|
|
310
|
+
this.#readProperty(proxyContext, target, property),
|
|
318
311
|
has: (target, property) =>
|
|
319
|
-
this.#readPresence(
|
|
320
|
-
ownKeys: (target) => this.#readKeys(
|
|
312
|
+
this.#readPresence(proxyContext, target, property),
|
|
313
|
+
ownKeys: (target) => this.#readKeys(proxyContext, target),
|
|
321
314
|
getOwnPropertyDescriptor: (target, property) =>
|
|
322
|
-
this.#readDescriptor(
|
|
315
|
+
this.#readDescriptor(proxyContext, target, property),
|
|
323
316
|
set: () => this.#rejectWrite(),
|
|
324
317
|
defineProperty: () => this.#rejectWrite(),
|
|
325
318
|
deleteProperty: () => this.#rejectWrite(),
|
|
@@ -327,13 +320,13 @@ export class SelfAdjustingComputation {
|
|
|
327
320
|
preventExtensions: () => this.#rejectWrite()
|
|
328
321
|
})
|
|
329
322
|
const nextCachedByPath = cachedByPath || new Map()
|
|
330
|
-
nextCachedByPath.set(
|
|
323
|
+
nextCachedByPath.set(pathNode, proxy)
|
|
331
324
|
if (!cachedByPath) {
|
|
332
|
-
context.proxyCache.set(
|
|
325
|
+
context.proxyCache.set(target, nextCachedByPath)
|
|
333
326
|
}
|
|
334
327
|
context.proxyMetadata.set(proxy, {
|
|
335
|
-
target
|
|
336
|
-
|
|
328
|
+
target,
|
|
329
|
+
pathNode
|
|
337
330
|
})
|
|
338
331
|
return proxy
|
|
339
332
|
}
|
|
@@ -347,21 +340,18 @@ export class SelfAdjustingComputation {
|
|
|
347
340
|
*/
|
|
348
341
|
#readProperty(context, target, property) {
|
|
349
342
|
const value = Reflect.get(target, property, target)
|
|
350
|
-
const
|
|
351
|
-
if (this.#isTraversable(value, path)) {
|
|
352
|
-
this.#record(
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
343
|
+
const pathNode = this.#childPath(context.pathNode, property)
|
|
344
|
+
if (this.#isTraversable(value, pathNode.path)) {
|
|
345
|
+
this.#record(
|
|
346
|
+
context.dependencies,
|
|
347
|
+
pathNode,
|
|
348
|
+
'kind',
|
|
349
|
+
SelfAdjustingComputation.#valueKind(value)
|
|
350
|
+
)
|
|
351
|
+
return this.#createProxy(context, value, pathNode)
|
|
358
352
|
}
|
|
359
353
|
|
|
360
|
-
this.#record(context.dependencies,
|
|
361
|
-
type: 'value',
|
|
362
|
-
path,
|
|
363
|
-
expected: value
|
|
364
|
-
})
|
|
354
|
+
this.#record(context.dependencies, pathNode, 'value', value)
|
|
365
355
|
return value
|
|
366
356
|
}
|
|
367
357
|
|
|
@@ -373,13 +363,9 @@ export class SelfAdjustingComputation {
|
|
|
373
363
|
* @returns {boolean} Whether the property exists.
|
|
374
364
|
*/
|
|
375
365
|
#readPresence(context, target, property) {
|
|
376
|
-
const
|
|
366
|
+
const pathNode = this.#childPath(context.pathNode, property)
|
|
377
367
|
const expected = Reflect.has(target, property)
|
|
378
|
-
this.#record(context.dependencies,
|
|
379
|
-
type: 'has',
|
|
380
|
-
path,
|
|
381
|
-
expected
|
|
382
|
-
})
|
|
368
|
+
this.#record(context.dependencies, pathNode, 'has', expected)
|
|
383
369
|
return expected
|
|
384
370
|
}
|
|
385
371
|
|
|
@@ -391,11 +377,7 @@ export class SelfAdjustingComputation {
|
|
|
391
377
|
*/
|
|
392
378
|
#readKeys(context, target) {
|
|
393
379
|
const expected = Reflect.ownKeys(target)
|
|
394
|
-
this.#record(context.dependencies,
|
|
395
|
-
type: 'keys',
|
|
396
|
-
path: [...context.path],
|
|
397
|
-
expected
|
|
398
|
-
})
|
|
380
|
+
this.#record(context.dependencies, context.pathNode, 'keys', expected)
|
|
399
381
|
return expected
|
|
400
382
|
}
|
|
401
383
|
|
|
@@ -408,11 +390,12 @@ export class SelfAdjustingComputation {
|
|
|
408
390
|
*/
|
|
409
391
|
#readDescriptor(context, target, property) {
|
|
410
392
|
const descriptor = Reflect.getOwnPropertyDescriptor(target, property)
|
|
411
|
-
this.#record(
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
393
|
+
this.#record(
|
|
394
|
+
context.dependencies,
|
|
395
|
+
this.#childPath(context.pathNode, property),
|
|
396
|
+
'descriptor',
|
|
397
|
+
SelfAdjustingComputation.#descriptorState(descriptor)
|
|
398
|
+
)
|
|
416
399
|
return descriptor
|
|
417
400
|
}
|
|
418
401
|
|
|
@@ -424,23 +407,50 @@ export class SelfAdjustingComputation {
|
|
|
424
407
|
*/
|
|
425
408
|
#isTraversable(value, path) {
|
|
426
409
|
if (!value || typeof value !== 'object') return false
|
|
427
|
-
if (this.#isAtomic(value, [...path])) return false
|
|
410
|
+
if (this.#isAtomic?.(value, [...path])) return false
|
|
428
411
|
if (Array.isArray(value)) return true
|
|
429
412
|
const prototype = Reflect.getPrototypeOf(value)
|
|
430
413
|
return prototype === Object.prototype || prototype === null
|
|
431
414
|
}
|
|
432
415
|
|
|
433
416
|
/**
|
|
434
|
-
*
|
|
435
|
-
*
|
|
436
|
-
* @param {
|
|
417
|
+
* Interns one child path using property identity without encoding its ancestors.
|
|
418
|
+
* Path and proxy caches belong to one trace and are never reused by a later snapshot.
|
|
419
|
+
* @param {object} parent Parent path node.
|
|
420
|
+
* @param {PropertyKey} property Observed property key.
|
|
421
|
+
* @returns {object} Canonical child path node for this trace.
|
|
422
|
+
*/
|
|
423
|
+
#childPath(parent, property) {
|
|
424
|
+
const previous = parent.children?.get(property)
|
|
425
|
+
if (previous) return previous
|
|
426
|
+
const child = {
|
|
427
|
+
path: [...parent.path, property],
|
|
428
|
+
children: null,
|
|
429
|
+
observations: null
|
|
430
|
+
}
|
|
431
|
+
parent.children ??= new Map()
|
|
432
|
+
parent.children.set(property, child)
|
|
433
|
+
return child
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
/**
|
|
437
|
+
* Stores the last observation of each dependency type at an interned path.
|
|
438
|
+
* @param {object[]} dependencies Dependency registry in first-observed order.
|
|
439
|
+
* @param {object} pathNode Interned path node.
|
|
440
|
+
* @param {string} type Dependency comparison category.
|
|
441
|
+
* @param {any} expected Most recently observed value or structural state.
|
|
437
442
|
* @returns {void}
|
|
438
443
|
*/
|
|
439
|
-
#record(dependencies,
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
+
#record(dependencies, pathNode, type, expected) {
|
|
445
|
+
const previous = pathNode.observations?.get(type)
|
|
446
|
+
if (previous) {
|
|
447
|
+
previous.expected = expected
|
|
448
|
+
return
|
|
449
|
+
}
|
|
450
|
+
const dependency = { type, path: pathNode.path, expected }
|
|
451
|
+
pathNode.observations ??= new Map()
|
|
452
|
+
pathNode.observations.set(type, dependency)
|
|
453
|
+
dependencies.push(dependency)
|
|
444
454
|
}
|
|
445
455
|
|
|
446
456
|
/**
|
|
@@ -530,27 +540,6 @@ export class SelfAdjustingComputation {
|
|
|
530
540
|
return { found: true, value }
|
|
531
541
|
}
|
|
532
542
|
|
|
533
|
-
/**
|
|
534
|
-
* Creates a collision-free string key for a property path.
|
|
535
|
-
* @param {PropertyKey[]} path Input path.
|
|
536
|
-
* @returns {string} Registry key.
|
|
537
|
-
*/
|
|
538
|
-
#pathKey(path) {
|
|
539
|
-
return path
|
|
540
|
-
.map((property) => {
|
|
541
|
-
if (typeof property === 'symbol') {
|
|
542
|
-
if (!this.#symbolIds.has(property)) {
|
|
543
|
-
this.#symbolIds.set(property, this.#nextSymbolId)
|
|
544
|
-
this.#nextSymbolId += 1
|
|
545
|
-
}
|
|
546
|
-
return 'y' + this.#symbolIds.get(property)
|
|
547
|
-
}
|
|
548
|
-
const text = String(property)
|
|
549
|
-
return 's' + text.length + ':' + text
|
|
550
|
-
})
|
|
551
|
-
.join('|')
|
|
552
|
-
}
|
|
553
|
-
|
|
554
543
|
/**
|
|
555
544
|
* Rejects mutation through a tracked snapshot.
|
|
556
545
|
* @returns {never}
|
|
@@ -506,10 +506,9 @@ class StructuredCloneAdoptionTraversal {
|
|
|
506
506
|
}
|
|
507
507
|
|
|
508
508
|
/**
|
|
509
|
-
*
|
|
510
|
-
* require one monolithic Object.freeze operation.
|
|
509
|
+
* Validates and atomically freezes one acquired target.
|
|
511
510
|
* @param {object} seal Target sealing snapshot.
|
|
512
|
-
* @returns {Generator<void, void, void>}
|
|
511
|
+
* @returns {Generator<void, void, void>} Cooperative locking pass.
|
|
513
512
|
*/
|
|
514
513
|
*#sealTargetCooperatively(seal) {
|
|
515
514
|
StructuredCloneAdoptionTraversal.#requireTargetShape(seal)
|
|
@@ -530,31 +529,16 @@ class StructuredCloneAdoptionTraversal {
|
|
|
530
529
|
'Canonical asset source changed during adoption.'
|
|
531
530
|
)
|
|
532
531
|
}
|
|
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
532
|
}
|
|
548
533
|
StructuredCloneAdoptionTraversal.#requireTargetShape(seal)
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
)
|
|
556
|
-
}
|
|
534
|
+
try {
|
|
535
|
+
Object.freeze(seal.target)
|
|
536
|
+
} catch {
|
|
537
|
+
throw new TypeError(
|
|
538
|
+
'Canonical document values could not be frozen safely.'
|
|
539
|
+
)
|
|
557
540
|
}
|
|
541
|
+
yield* this.#checkpoint(seal.properties.length || 1)
|
|
558
542
|
}
|
|
559
543
|
|
|
560
544
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const TEXT_ACCOUNTING_CHUNK_CHARACTERS = 64 * 1_024
|
|
2
|
+
const SHORT_TEXT_CHARACTERS = 256
|
|
2
3
|
const STRING_CHAR_CODE_AT = String.prototype.charCodeAt
|
|
3
4
|
const STRING_SLICE = String.prototype.slice
|
|
4
5
|
const TEXT_ENCODER = new TextEncoder()
|
|
@@ -17,6 +18,14 @@ export class StructuredCloneTextAccounting {
|
|
|
17
18
|
*/
|
|
18
19
|
static *reserve(value, state, operations) {
|
|
19
20
|
if (state.maxBytes === Number.MAX_SAFE_INTEGER) return
|
|
21
|
+
if (value.length <= SHORT_TEXT_CHARACTERS) {
|
|
22
|
+
const length = StructuredCloneTextAccounting.#shortByteLength(value)
|
|
23
|
+
if (state.bytes + length > state.maxBytes) {
|
|
24
|
+
throw new TypeError(`${state.label} is too large.`)
|
|
25
|
+
}
|
|
26
|
+
operations.reserve(length)
|
|
27
|
+
return
|
|
28
|
+
}
|
|
20
29
|
let length = 0
|
|
21
30
|
let offset = 0
|
|
22
31
|
while (offset < value.length) {
|
|
@@ -49,6 +58,35 @@ export class StructuredCloneTextAccounting {
|
|
|
49
58
|
}
|
|
50
59
|
operations.reserve(length)
|
|
51
60
|
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Counts bounded short text without allocating an encoded byte buffer.
|
|
64
|
+
* Lone surrogates consume the three UTF-8 bytes of a replacement character.
|
|
65
|
+
* @param {string} value Short immutable text.
|
|
66
|
+
* @returns {number} UTF-8 byte length.
|
|
67
|
+
*/
|
|
68
|
+
static #shortByteLength(value) {
|
|
69
|
+
let length = 0
|
|
70
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
71
|
+
const code = STRING_CHAR_CODE_AT.call(value, index)
|
|
72
|
+
if (code <= 0x7f) {
|
|
73
|
+
length += 1
|
|
74
|
+
} else if (code <= 0x7ff) {
|
|
75
|
+
length += 2
|
|
76
|
+
} else if (code >= 0xd800 && code <= 0xdbff) {
|
|
77
|
+
const next = STRING_CHAR_CODE_AT.call(value, index + 1)
|
|
78
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
79
|
+
length += 4
|
|
80
|
+
index += 1
|
|
81
|
+
} else {
|
|
82
|
+
length += 3
|
|
83
|
+
}
|
|
84
|
+
} else {
|
|
85
|
+
length += 3
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return length
|
|
89
|
+
}
|
|
52
90
|
}
|
|
53
91
|
|
|
54
92
|
Object.freeze(StructuredCloneTextAccounting.prototype)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { CircuitJsonValidationProof } from '../context/CircuitJsonValidationProof.mjs'
|
|
2
|
+
import { CircuitJsonExtensionBoundary } from '../context/CircuitJsonExtensionBoundary.mjs'
|
|
2
3
|
import { CircuitJsonReadOnlyDocument } from '../context/CircuitJsonReadOnlyDocument.mjs'
|
|
3
4
|
import { CircuitJsonSerializedInputAudit } from '../CircuitJsonSerializedInputAudit.mjs'
|
|
4
5
|
import { ToolkitAsset } from './ToolkitAsset.mjs'
|
|
@@ -147,6 +148,14 @@ export class DocumentResult {
|
|
|
147
148
|
const hasCandidate = candidate && typeof candidate === 'object'
|
|
148
149
|
if (!hasCandidate) return {}
|
|
149
150
|
if (options.readonly === true && hasCandidate) {
|
|
151
|
+
// This private brand proves the frozen namespace was already
|
|
152
|
+
// captured, bounded, and sealed by this runtime.
|
|
153
|
+
if (
|
|
154
|
+
CircuitJsonExtensionBoundary.owns(extensions) &&
|
|
155
|
+
Object.isFrozen(extensions)
|
|
156
|
+
) {
|
|
157
|
+
return extensions
|
|
158
|
+
}
|
|
150
159
|
if (options.standardBuiltins === true) {
|
|
151
160
|
return CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
|
|
152
161
|
DocumentResult.#normalizedExtension(format, candidate),
|