circuitjson-toolkit 1.4.0 → 1.4.2
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 +16 -2
- package/docs/api.md +34 -1
- package/docs/release-notes-v1.4.1.md +27 -0
- package/docs/release-notes-v1.4.2.md +32 -0
- package/package.json +3 -1
- package/src/core/SelfAdjustingComputation.mjs +610 -0
- package/src/core/context/StructuredCloneAdoption.mjs +9 -25
- package/src/core/contracts/DocumentResult.mjs +9 -0
- package/src/index.mjs +1 -0
- package/src/testing/ToolkitContractFixtures.mjs +1 -0
package/README.md
CHANGED
|
@@ -8,8 +8,9 @@ SPDX-License-Identifier: CC-BY-SA-4.0
|
|
|
8
8
|
|
|
9
9
|
CircuitJSON Toolkit is the dependency-free common runtime for the ECAD toolkit
|
|
10
10
|
family. It provides the same parser, project, rendering, interaction, query,
|
|
11
|
-
manufacturing, simulation, 3D scene,
|
|
12
|
-
used by `gerber-toolkit`, `altium-toolkit`, and
|
|
11
|
+
manufacturing, simulation, 3D scene, self-adjusting computation, capability,
|
|
12
|
+
error, and worker contracts used by `gerber-toolkit`, `altium-toolkit`, and
|
|
13
|
+
`kicad-toolkit`.
|
|
13
14
|
|
|
14
15
|
CircuitJSON is the shared immutable model. Source-format packages keep their
|
|
15
16
|
native decoders and fidelity data in explicit extension namespaces while
|
|
@@ -77,6 +78,17 @@ of defensively copying them again. The resulting model, extensions,
|
|
|
77
78
|
parameters, and `DocumentResult` shape are unchanged. See the
|
|
78
79
|
[1.4.0 release notes](docs/release-notes-v1.4.0.md).
|
|
79
80
|
|
|
81
|
+
Version 1.4.1 adds the canonical `SelfAdjustingComputation` runtime with
|
|
82
|
+
dynamic data/control dependencies, changed-root reader lists, stale-trace
|
|
83
|
+
replacement, explicit reclamation, and from-scratch consistency coverage. See
|
|
84
|
+
the [1.4.1 release notes](docs/release-notes-v1.4.1.md).
|
|
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
|
+
|
|
80
92
|
Before 1.1.0:
|
|
81
93
|
|
|
82
94
|
```js
|
|
@@ -386,6 +398,8 @@ copy while keeping sync, direct async, and worker results mutation-isolated.
|
|
|
386
398
|
- [1.2.1 release notes](docs/release-notes-v1.2.1.md)
|
|
387
399
|
- [1.3.0 release notes](docs/release-notes-v1.3.0.md)
|
|
388
400
|
- [1.4.0 release notes](docs/release-notes-v1.4.0.md)
|
|
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)
|
|
389
403
|
- [Library scope](spec/library-scope.md)
|
|
390
404
|
|
|
391
405
|
## Package scope
|
package/docs/api.md
CHANGED
|
@@ -16,6 +16,38 @@ this document for new code. Thirty-seven previous CircuitJSON-specific classes
|
|
|
16
16
|
remain under `circuitjson-toolkit/extensions`; the three documented viewer
|
|
17
17
|
compatibility classes remain on the root. See [migration.md](migration.md).
|
|
18
18
|
|
|
19
|
+
### `SelfAdjustingComputation`
|
|
20
|
+
|
|
21
|
+
The root API exports the shared synchronous change-propagation runtime used by
|
|
22
|
+
ECAD Forge and reusable toolkit consumers. It records dynamic property reads
|
|
23
|
+
for stable named computations, maintains reverse reader lists, validates only
|
|
24
|
+
the traces reached from explicit changed roots, and replaces stale control-flow
|
|
25
|
+
dependencies after re-execution.
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
import { SelfAdjustingComputation } from 'circuitjson-toolkit'
|
|
29
|
+
|
|
30
|
+
const runtime = new SelfAdjustingComputation()
|
|
31
|
+
const results = runtime.propagate(
|
|
32
|
+
{ locale: 'en', status: 'ready' },
|
|
33
|
+
[['status']],
|
|
34
|
+
[
|
|
35
|
+
{
|
|
36
|
+
name: 'status-label',
|
|
37
|
+
computation: (state) => state.locale + ':' + state.status
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
)
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
Computations must be synchronous and treat their tracked input as read-only.
|
|
44
|
+
`forget(name)` reclaims one trace, `clear()` reclaims the graph, and
|
|
45
|
+
`getStatistics()` exposes bounded trace counts. Plain objects and arrays are
|
|
46
|
+
traversed; non-plain objects are atomic identity dependencies unless the
|
|
47
|
+
constructor's `isAtomic(value, path)` option selects an earlier boundary.
|
|
48
|
+
Callers must supply conservative changed paths and test propagated results
|
|
49
|
+
against a fresh execution for the same input.
|
|
50
|
+
|
|
19
51
|
## Common conventions
|
|
20
52
|
|
|
21
53
|
### Document input
|
|
@@ -171,7 +203,7 @@ adoption or `prepare()` for arbitrary caller-owned graphs.
|
|
|
171
203
|
|
|
172
204
|
## Root entrypoint
|
|
173
205
|
|
|
174
|
-
`circuitjson-toolkit` has an exact
|
|
206
|
+
`circuitjson-toolkit` has an exact 18-class root. The 15 canonical classes are:
|
|
175
207
|
|
|
176
208
|
- `Parser`
|
|
177
209
|
- `ProjectLoader`
|
|
@@ -185,6 +217,7 @@ adoption or `prepare()` for arbitrary caller-owned graphs.
|
|
|
185
217
|
- `SimulationService`
|
|
186
218
|
- `PcbScene3dBuilder`
|
|
187
219
|
- `PcbScene3dPreparator`
|
|
220
|
+
- `SelfAdjustingComputation`
|
|
188
221
|
- `ToolkitCapabilities`
|
|
189
222
|
- `ToolkitError`
|
|
190
223
|
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# circuitjson-toolkit 1.4.1
|
|
2
|
+
|
|
3
|
+
This patch release adds a shared self-adjusting-computation runtime for
|
|
4
|
+
persistent toolkit and application state. It implements dynamic dependency
|
|
5
|
+
tracing and ordered change propagation without coupling the common package to
|
|
6
|
+
DOM, Three.js, or source-format parser state.
|
|
7
|
+
|
|
8
|
+
## Self-adjusting computation
|
|
9
|
+
|
|
10
|
+
- `SelfAdjustingComputation` is available from the package root and is shared
|
|
11
|
+
by identity through the Gerber, Altium, KiCad, and PCB Scene3D packages.
|
|
12
|
+
- Named synchronous computations record the data and control-flow paths they
|
|
13
|
+
observe. Explicit changed roots start propagation from reverse reader lists.
|
|
14
|
+
- Potentially affected computations compare their previous observations and
|
|
15
|
+
reuse successful results when values, presence, key structure, and selected
|
|
16
|
+
atomic identities remain unchanged.
|
|
17
|
+
- Re-execution replaces the previous trace and its abandoned reader edges.
|
|
18
|
+
`forget()` and `clear()` reclaim trace storage explicitly.
|
|
19
|
+
- Tracked snapshots reject mutation and asynchronous trace escape. Callers can
|
|
20
|
+
choose an atomic boundary for immutable documents and native objects.
|
|
21
|
+
|
|
22
|
+
## Verification
|
|
23
|
+
|
|
24
|
+
The unit suite covers nested and structural reads, control-flow replacement,
|
|
25
|
+
stale reader removal, atomic document identity, failed and asynchronous
|
|
26
|
+
computations, write rejection, explicit trace reclamation, and equality with a
|
|
27
|
+
fresh runtime after each propagated change.
|
|
@@ -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.2",
|
|
4
4
|
"description": "Canonical CircuitJSON parsing, project, rendering, query, manufacturing, simulation, and scene contracts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"circuitjson",
|
|
@@ -57,6 +57,8 @@
|
|
|
57
57
|
"docs/release-notes-v1.2.1.md",
|
|
58
58
|
"docs/release-notes-v1.3.0.md",
|
|
59
59
|
"docs/release-notes-v1.4.0.md",
|
|
60
|
+
"docs/release-notes-v1.4.1.md",
|
|
61
|
+
"docs/release-notes-v1.4.2.md",
|
|
60
62
|
"docs/testing.md",
|
|
61
63
|
"spec",
|
|
62
64
|
"LICENSE",
|
|
@@ -0,0 +1,610 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memoizes named computations and dynamically tracks their observed input paths.
|
|
3
|
+
*/
|
|
4
|
+
export class SelfAdjustingComputation {
|
|
5
|
+
/** @type {Map<string, { dependencies: object[], value: any, readerRoots: Set<PropertyKey>, readsWholeInput: boolean }>} */
|
|
6
|
+
#computations
|
|
7
|
+
|
|
8
|
+
/** @type {Map<PropertyKey, Set<string>>} */
|
|
9
|
+
#readersByRoot
|
|
10
|
+
|
|
11
|
+
/** @type {Set<string>} */
|
|
12
|
+
#wholeInputReaders
|
|
13
|
+
|
|
14
|
+
/** @type {object | null} */
|
|
15
|
+
#propagationInput
|
|
16
|
+
|
|
17
|
+
/** @type {Set<string> | null} */
|
|
18
|
+
#affectedComputations
|
|
19
|
+
|
|
20
|
+
/** @type {(value: object, path: PropertyKey[]) => boolean} */
|
|
21
|
+
#isAtomic
|
|
22
|
+
|
|
23
|
+
/** @type {Map<symbol, number>} */
|
|
24
|
+
#symbolIds
|
|
25
|
+
|
|
26
|
+
/** @type {number} */
|
|
27
|
+
#nextSymbolId
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* @param {{ isAtomic?: (value: object, path: PropertyKey[]) => boolean }} [options] Dependency-tracking options.
|
|
31
|
+
*/
|
|
32
|
+
constructor(options = {}) {
|
|
33
|
+
this.#computations = new Map()
|
|
34
|
+
this.#readersByRoot = new Map()
|
|
35
|
+
this.#wholeInputReaders = new Set()
|
|
36
|
+
this.#propagationInput = null
|
|
37
|
+
this.#affectedComputations = null
|
|
38
|
+
this.#isAtomic =
|
|
39
|
+
typeof options.isAtomic === 'function'
|
|
40
|
+
? options.isAtomic
|
|
41
|
+
: () => false
|
|
42
|
+
this.#symbolIds = new Map()
|
|
43
|
+
this.#nextSymbolId = 1
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Propagates changed modifiable roots through named computations in order.
|
|
48
|
+
* A null change set performs dependency validation for every existing trace.
|
|
49
|
+
* @param {object} input Current input snapshot.
|
|
50
|
+
* @param {PropertyKey[][] | null} changedPaths Changed input paths, or null when unknown.
|
|
51
|
+
* @param {{ name: string, computation: (trackedInput: object) => any }[]} computations Ordered computations.
|
|
52
|
+
* @returns {Map<string, { value: any, recomputed: boolean }>} Results by computation name.
|
|
53
|
+
*/
|
|
54
|
+
propagate(input, changedPaths, computations) {
|
|
55
|
+
this.#validateInput(input)
|
|
56
|
+
if (!Array.isArray(computations)) {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
'Self-adjusting propagation requires an ordered computation array.'
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
this.#propagationInput = input
|
|
63
|
+
this.#affectedComputations =
|
|
64
|
+
changedPaths === null
|
|
65
|
+
? null
|
|
66
|
+
: this.#findAffectedComputations(changedPaths)
|
|
67
|
+
const results = new Map()
|
|
68
|
+
try {
|
|
69
|
+
for (const entry of computations) {
|
|
70
|
+
if (!entry || typeof entry !== 'object') {
|
|
71
|
+
throw new TypeError(
|
|
72
|
+
'Self-adjusting propagation entries must be objects.'
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
const key = String(entry.name)
|
|
76
|
+
results.set(key, this.evaluate(key, input, entry.computation))
|
|
77
|
+
}
|
|
78
|
+
return results
|
|
79
|
+
} finally {
|
|
80
|
+
this.#propagationInput = null
|
|
81
|
+
this.#affectedComputations = null
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Evaluates one named computation or reuses its last successful result.
|
|
87
|
+
* @param {string} name Stable computation name.
|
|
88
|
+
* @param {object} input Current input snapshot.
|
|
89
|
+
* @param {(trackedInput: object) => any} computation Synchronous computation.
|
|
90
|
+
* @returns {{ value: any, recomputed: boolean }} Evaluation result.
|
|
91
|
+
*/
|
|
92
|
+
evaluate(name, input, computation) {
|
|
93
|
+
this.#validateInput(input)
|
|
94
|
+
if (typeof computation !== 'function') {
|
|
95
|
+
throw new TypeError(
|
|
96
|
+
'Self-adjusting computation requires a computation function.'
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const key = String(name)
|
|
101
|
+
const previous = this.#computations.get(key)
|
|
102
|
+
if (previous && !this.#isPotentiallyAffected(key, input)) {
|
|
103
|
+
return { value: previous.value, recomputed: false }
|
|
104
|
+
}
|
|
105
|
+
if (previous && this.#dependenciesMatch(previous.dependencies, input)) {
|
|
106
|
+
return { value: previous.value, recomputed: false }
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const trace = this.#trace(input, computation)
|
|
110
|
+
this.#replaceTrace(key, trace)
|
|
111
|
+
return { value: trace.value, recomputed: true }
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Removes one named trace and its reader-list entries.
|
|
116
|
+
* @param {string} name Stable computation name.
|
|
117
|
+
* @returns {boolean} Whether a trace was removed.
|
|
118
|
+
*/
|
|
119
|
+
forget(name) {
|
|
120
|
+
const key = String(name)
|
|
121
|
+
if (!this.#computations.has(key)) return false
|
|
122
|
+
this.#removeTrace(key)
|
|
123
|
+
return true
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Removes all traces and reader-list entries.
|
|
128
|
+
* @returns {void}
|
|
129
|
+
*/
|
|
130
|
+
clear() {
|
|
131
|
+
this.#computations.clear()
|
|
132
|
+
this.#readersByRoot.clear()
|
|
133
|
+
this.#wholeInputReaders.clear()
|
|
134
|
+
this.#propagationInput = null
|
|
135
|
+
this.#affectedComputations = null
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Returns bounded trace-storage counts for diagnostics and tests.
|
|
140
|
+
* @returns {{ computations: number, dependencies: number, readerEdges: number }} Trace statistics.
|
|
141
|
+
*/
|
|
142
|
+
getStatistics() {
|
|
143
|
+
return {
|
|
144
|
+
computations: this.#computations.size,
|
|
145
|
+
dependencies: [...this.#computations.values()].reduce(
|
|
146
|
+
(total, trace) => total + trace.dependencies.length,
|
|
147
|
+
0
|
|
148
|
+
),
|
|
149
|
+
readerEdges:
|
|
150
|
+
[...this.#readersByRoot.values()].reduce(
|
|
151
|
+
(total, readers) => total + readers.size,
|
|
152
|
+
0
|
|
153
|
+
) + this.#wholeInputReaders.size
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Validates one root input snapshot.
|
|
159
|
+
* @param {unknown} input Candidate input.
|
|
160
|
+
* @returns {void}
|
|
161
|
+
*/
|
|
162
|
+
#validateInput(input) {
|
|
163
|
+
if (!input || typeof input !== 'object') {
|
|
164
|
+
throw new TypeError(
|
|
165
|
+
'Self-adjusting computation input must be an object.'
|
|
166
|
+
)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Finds prior computations that read a changed root.
|
|
172
|
+
* @param {PropertyKey[][]} changedPaths Changed input paths.
|
|
173
|
+
* @returns {Set<string>} Potentially affected computation names.
|
|
174
|
+
*/
|
|
175
|
+
#findAffectedComputations(changedPaths) {
|
|
176
|
+
if (!Array.isArray(changedPaths)) {
|
|
177
|
+
throw new TypeError(
|
|
178
|
+
'Self-adjusting change sets must be arrays of property paths.'
|
|
179
|
+
)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const affected = new Set(this.#wholeInputReaders)
|
|
183
|
+
for (const path of changedPaths) {
|
|
184
|
+
if (!Array.isArray(path)) {
|
|
185
|
+
throw new TypeError(
|
|
186
|
+
'Self-adjusting change-set entries must be property paths.'
|
|
187
|
+
)
|
|
188
|
+
}
|
|
189
|
+
if (path.length === 0) {
|
|
190
|
+
return new Set(this.#computations.keys())
|
|
191
|
+
}
|
|
192
|
+
const readers = this.#readersByRoot.get(path[0])
|
|
193
|
+
readers?.forEach((name) => affected.add(name))
|
|
194
|
+
}
|
|
195
|
+
return affected
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Returns whether a prior trace must be checked in the active propagation.
|
|
200
|
+
* @param {string} name Computation name.
|
|
201
|
+
* @param {object} input Current input snapshot.
|
|
202
|
+
* @returns {boolean} Whether dependency validation is required.
|
|
203
|
+
*/
|
|
204
|
+
#isPotentiallyAffected(name, input) {
|
|
205
|
+
return !(
|
|
206
|
+
this.#propagationInput === input &&
|
|
207
|
+
this.#affectedComputations instanceof Set &&
|
|
208
|
+
!this.#affectedComputations.has(name)
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Replaces a trace and its dynamic reader-list entries atomically.
|
|
214
|
+
* @param {string} name Computation name.
|
|
215
|
+
* @param {{ dependencies: object[], value: any }} trace New successful trace.
|
|
216
|
+
* @returns {void}
|
|
217
|
+
*/
|
|
218
|
+
#replaceTrace(name, trace) {
|
|
219
|
+
this.#removeTrace(name)
|
|
220
|
+
const readerRoots = new Set()
|
|
221
|
+
let readsWholeInput = false
|
|
222
|
+
for (const dependency of trace.dependencies) {
|
|
223
|
+
if (dependency.path.length === 0) {
|
|
224
|
+
readsWholeInput = true
|
|
225
|
+
} else {
|
|
226
|
+
readerRoots.add(dependency.path[0])
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
const storedTrace = {
|
|
230
|
+
...trace,
|
|
231
|
+
readerRoots,
|
|
232
|
+
readsWholeInput
|
|
233
|
+
}
|
|
234
|
+
this.#computations.set(name, storedTrace)
|
|
235
|
+
readerRoots.forEach((root) => {
|
|
236
|
+
const readers = this.#readersByRoot.get(root) || new Set()
|
|
237
|
+
readers.add(name)
|
|
238
|
+
this.#readersByRoot.set(root, readers)
|
|
239
|
+
})
|
|
240
|
+
if (readsWholeInput) this.#wholeInputReaders.add(name)
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Removes one stored trace and all of its reverse dependency edges.
|
|
245
|
+
* @param {string} name Computation name.
|
|
246
|
+
* @returns {void}
|
|
247
|
+
*/
|
|
248
|
+
#removeTrace(name) {
|
|
249
|
+
const previous = this.#computations.get(name)
|
|
250
|
+
if (!previous) return
|
|
251
|
+
previous.readerRoots.forEach((root) => {
|
|
252
|
+
const readers = this.#readersByRoot.get(root)
|
|
253
|
+
readers?.delete(name)
|
|
254
|
+
if (readers?.size === 0) this.#readersByRoot.delete(root)
|
|
255
|
+
})
|
|
256
|
+
if (previous.readsWholeInput) this.#wholeInputReaders.delete(name)
|
|
257
|
+
this.#computations.delete(name)
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* Executes one computation while collecting its dynamic dependencies.
|
|
262
|
+
* @param {object} input Raw input snapshot.
|
|
263
|
+
* @param {(trackedInput: object) => any} computation Computation callback.
|
|
264
|
+
* @returns {{ dependencies: object[], value: any }} Successful trace.
|
|
265
|
+
*/
|
|
266
|
+
#trace(input, computation) {
|
|
267
|
+
const dependencies = new Map()
|
|
268
|
+
const proxyCache = new WeakMap()
|
|
269
|
+
const proxyMetadata = new WeakMap()
|
|
270
|
+
const trackedInput = this.#createProxy({
|
|
271
|
+
target: input,
|
|
272
|
+
path: [],
|
|
273
|
+
dependencies,
|
|
274
|
+
proxyCache,
|
|
275
|
+
proxyMetadata
|
|
276
|
+
})
|
|
277
|
+
const trackedValue = computation(trackedInput)
|
|
278
|
+
if (SelfAdjustingComputation.#isPromiseLike(trackedValue)) {
|
|
279
|
+
throw new TypeError(
|
|
280
|
+
'Self-adjusting computations must be synchronous so dependency tracing cannot escape.'
|
|
281
|
+
)
|
|
282
|
+
}
|
|
283
|
+
const metadata =
|
|
284
|
+
trackedValue &&
|
|
285
|
+
(typeof trackedValue === 'object' ||
|
|
286
|
+
typeof trackedValue === 'function')
|
|
287
|
+
? proxyMetadata.get(trackedValue)
|
|
288
|
+
: null
|
|
289
|
+
if (metadata) {
|
|
290
|
+
this.#record(dependencies, {
|
|
291
|
+
type: 'value',
|
|
292
|
+
path: metadata.path,
|
|
293
|
+
expected: metadata.target
|
|
294
|
+
})
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return {
|
|
298
|
+
dependencies: [...dependencies.values()],
|
|
299
|
+
value: metadata?.target ?? trackedValue
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Creates a read-tracking proxy for one traversable input container.
|
|
305
|
+
* @param {{ target: object, path: PropertyKey[], dependencies: Map<string, object>, proxyCache: WeakMap<object, Map<string, object>>, proxyMetadata: WeakMap<object, { target: object, path: PropertyKey[] }> }} context Tracking context.
|
|
306
|
+
* @returns {object} Read-tracking proxy.
|
|
307
|
+
*/
|
|
308
|
+
#createProxy(context) {
|
|
309
|
+
const pathKey = this.#pathKey(context.path)
|
|
310
|
+
const cachedByPath = context.proxyCache.get(context.target)
|
|
311
|
+
if (cachedByPath?.has(pathKey)) {
|
|
312
|
+
return cachedByPath.get(pathKey)
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
const proxy = new Proxy(context.target, {
|
|
316
|
+
get: (target, property) =>
|
|
317
|
+
this.#readProperty(context, target, property),
|
|
318
|
+
has: (target, property) =>
|
|
319
|
+
this.#readPresence(context, target, property),
|
|
320
|
+
ownKeys: (target) => this.#readKeys(context, target),
|
|
321
|
+
getOwnPropertyDescriptor: (target, property) =>
|
|
322
|
+
this.#readDescriptor(context, target, property),
|
|
323
|
+
set: () => this.#rejectWrite(),
|
|
324
|
+
defineProperty: () => this.#rejectWrite(),
|
|
325
|
+
deleteProperty: () => this.#rejectWrite(),
|
|
326
|
+
setPrototypeOf: () => this.#rejectWrite(),
|
|
327
|
+
preventExtensions: () => this.#rejectWrite()
|
|
328
|
+
})
|
|
329
|
+
const nextCachedByPath = cachedByPath || new Map()
|
|
330
|
+
nextCachedByPath.set(pathKey, proxy)
|
|
331
|
+
if (!cachedByPath) {
|
|
332
|
+
context.proxyCache.set(context.target, nextCachedByPath)
|
|
333
|
+
}
|
|
334
|
+
context.proxyMetadata.set(proxy, {
|
|
335
|
+
target: context.target,
|
|
336
|
+
path: [...context.path]
|
|
337
|
+
})
|
|
338
|
+
return proxy
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reads and records one property dependency.
|
|
343
|
+
* @param {object} context Tracking context.
|
|
344
|
+
* @param {object} target Current raw target.
|
|
345
|
+
* @param {PropertyKey} property Requested property.
|
|
346
|
+
* @returns {any} Raw atomic value or tracked container.
|
|
347
|
+
*/
|
|
348
|
+
#readProperty(context, target, property) {
|
|
349
|
+
const value = Reflect.get(target, property, target)
|
|
350
|
+
const path = [...context.path, property]
|
|
351
|
+
if (this.#isTraversable(value, path)) {
|
|
352
|
+
this.#record(context.dependencies, {
|
|
353
|
+
type: 'kind',
|
|
354
|
+
path,
|
|
355
|
+
expected: SelfAdjustingComputation.#valueKind(value)
|
|
356
|
+
})
|
|
357
|
+
return this.#createProxy({ ...context, target: value, path })
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this.#record(context.dependencies, {
|
|
361
|
+
type: 'value',
|
|
362
|
+
path,
|
|
363
|
+
expected: value
|
|
364
|
+
})
|
|
365
|
+
return value
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
/**
|
|
369
|
+
* Reads and records a property-existence dependency.
|
|
370
|
+
* @param {object} context Tracking context.
|
|
371
|
+
* @param {object} target Current raw target.
|
|
372
|
+
* @param {PropertyKey} property Requested property.
|
|
373
|
+
* @returns {boolean} Whether the property exists.
|
|
374
|
+
*/
|
|
375
|
+
#readPresence(context, target, property) {
|
|
376
|
+
const path = [...context.path, property]
|
|
377
|
+
const expected = Reflect.has(target, property)
|
|
378
|
+
this.#record(context.dependencies, {
|
|
379
|
+
type: 'has',
|
|
380
|
+
path,
|
|
381
|
+
expected
|
|
382
|
+
})
|
|
383
|
+
return expected
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Reads and records the ordered own-key set for one container.
|
|
388
|
+
* @param {object} context Tracking context.
|
|
389
|
+
* @param {object} target Current raw target.
|
|
390
|
+
* @returns {PropertyKey[]} Own keys.
|
|
391
|
+
*/
|
|
392
|
+
#readKeys(context, target) {
|
|
393
|
+
const expected = Reflect.ownKeys(target)
|
|
394
|
+
this.#record(context.dependencies, {
|
|
395
|
+
type: 'keys',
|
|
396
|
+
path: [...context.path],
|
|
397
|
+
expected
|
|
398
|
+
})
|
|
399
|
+
return expected
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
/**
|
|
403
|
+
* Reads and records whether one own property is enumerable.
|
|
404
|
+
* @param {object} context Tracking context.
|
|
405
|
+
* @param {object} target Current raw target.
|
|
406
|
+
* @param {PropertyKey} property Requested property.
|
|
407
|
+
* @returns {PropertyDescriptor | undefined} Raw property descriptor.
|
|
408
|
+
*/
|
|
409
|
+
#readDescriptor(context, target, property) {
|
|
410
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(target, property)
|
|
411
|
+
this.#record(context.dependencies, {
|
|
412
|
+
type: 'descriptor',
|
|
413
|
+
path: [...context.path, property],
|
|
414
|
+
expected: SelfAdjustingComputation.#descriptorState(descriptor)
|
|
415
|
+
})
|
|
416
|
+
return descriptor
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
/**
|
|
420
|
+
* Returns whether a value should expose nested dependency reads.
|
|
421
|
+
* @param {any} value Candidate value.
|
|
422
|
+
* @param {PropertyKey[]} path Input path.
|
|
423
|
+
* @returns {boolean} Whether the value is a traversable container.
|
|
424
|
+
*/
|
|
425
|
+
#isTraversable(value, path) {
|
|
426
|
+
if (!value || typeof value !== 'object') return false
|
|
427
|
+
if (this.#isAtomic(value, [...path])) return false
|
|
428
|
+
if (Array.isArray(value)) return true
|
|
429
|
+
const prototype = Reflect.getPrototypeOf(value)
|
|
430
|
+
return prototype === Object.prototype || prototype === null
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Stores or replaces one dependency by type and input path.
|
|
435
|
+
* @param {Map<string, object>} dependencies Dependency registry.
|
|
436
|
+
* @param {{ type: string, path: PropertyKey[], expected: any }} dependency Dependency record.
|
|
437
|
+
* @returns {void}
|
|
438
|
+
*/
|
|
439
|
+
#record(dependencies, dependency) {
|
|
440
|
+
dependencies.set(
|
|
441
|
+
dependency.type + ':' + this.#pathKey(dependency.path),
|
|
442
|
+
dependency
|
|
443
|
+
)
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Returns whether every dependency still matches a new input.
|
|
448
|
+
* @param {object[]} dependencies Previous successful dependencies.
|
|
449
|
+
* @param {object} input Current raw input.
|
|
450
|
+
* @returns {boolean} Whether the computation can be reused.
|
|
451
|
+
*/
|
|
452
|
+
#dependenciesMatch(dependencies, input) {
|
|
453
|
+
return dependencies.every((dependency) =>
|
|
454
|
+
this.#dependencyMatches(dependency, input)
|
|
455
|
+
)
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
/**
|
|
459
|
+
* Compares one recorded dependency against a new input.
|
|
460
|
+
* @param {{ type: string, path: PropertyKey[], expected: any }} dependency Recorded dependency.
|
|
461
|
+
* @param {object} input Current raw input.
|
|
462
|
+
* @returns {boolean} Whether the dependency is unchanged.
|
|
463
|
+
*/
|
|
464
|
+
#dependencyMatches(dependency, input) {
|
|
465
|
+
if (dependency.type === 'has') {
|
|
466
|
+
const parent = this.#readPath(input, dependency.path.slice(0, -1))
|
|
467
|
+
return (
|
|
468
|
+
parent.found &&
|
|
469
|
+
Reflect.has(Object(parent.value), dependency.path.at(-1)) ===
|
|
470
|
+
dependency.expected
|
|
471
|
+
)
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const current = this.#readPath(input, dependency.path)
|
|
475
|
+
if (dependency.type === 'value') {
|
|
476
|
+
return (
|
|
477
|
+
current.found && Object.is(current.value, dependency.expected)
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
if (dependency.type === 'kind') {
|
|
481
|
+
return (
|
|
482
|
+
current.found &&
|
|
483
|
+
SelfAdjustingComputation.#valueKind(current.value) ===
|
|
484
|
+
dependency.expected
|
|
485
|
+
)
|
|
486
|
+
}
|
|
487
|
+
if (dependency.type === 'keys') {
|
|
488
|
+
return (
|
|
489
|
+
current.found &&
|
|
490
|
+
current.value !== null &&
|
|
491
|
+
typeof current.value === 'object' &&
|
|
492
|
+
SelfAdjustingComputation.#sameKeys(
|
|
493
|
+
Reflect.ownKeys(current.value),
|
|
494
|
+
dependency.expected
|
|
495
|
+
)
|
|
496
|
+
)
|
|
497
|
+
}
|
|
498
|
+
if (dependency.type === 'descriptor') {
|
|
499
|
+
const parent = this.#readPath(input, dependency.path.slice(0, -1))
|
|
500
|
+
if (!parent.found || parent.value === null) return false
|
|
501
|
+
const descriptor = Reflect.getOwnPropertyDescriptor(
|
|
502
|
+
Object(parent.value),
|
|
503
|
+
dependency.path.at(-1)
|
|
504
|
+
)
|
|
505
|
+
return (
|
|
506
|
+
SelfAdjustingComputation.#descriptorState(descriptor) ===
|
|
507
|
+
dependency.expected
|
|
508
|
+
)
|
|
509
|
+
}
|
|
510
|
+
return false
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Resolves one raw input path without invoking dependency tracking.
|
|
515
|
+
* @param {object} input Root input.
|
|
516
|
+
* @param {PropertyKey[]} path Input path.
|
|
517
|
+
* @returns {{ found: boolean, value: any }} Resolved value.
|
|
518
|
+
*/
|
|
519
|
+
#readPath(input, path) {
|
|
520
|
+
let value = input
|
|
521
|
+
for (const property of path) {
|
|
522
|
+
if (
|
|
523
|
+
value === null ||
|
|
524
|
+
(typeof value !== 'object' && typeof value !== 'function')
|
|
525
|
+
) {
|
|
526
|
+
return { found: false, value: undefined }
|
|
527
|
+
}
|
|
528
|
+
value = Reflect.get(value, property, value)
|
|
529
|
+
}
|
|
530
|
+
return { found: true, value }
|
|
531
|
+
}
|
|
532
|
+
|
|
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
|
+
/**
|
|
555
|
+
* Rejects mutation through a tracked snapshot.
|
|
556
|
+
* @returns {never}
|
|
557
|
+
*/
|
|
558
|
+
#rejectWrite() {
|
|
559
|
+
throw new TypeError(
|
|
560
|
+
'Self-adjusting computation inputs are read-only while tracked.'
|
|
561
|
+
)
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Returns the comparison category for one container value.
|
|
566
|
+
* @param {any} value Candidate value.
|
|
567
|
+
* @returns {string} Value category.
|
|
568
|
+
*/
|
|
569
|
+
static #valueKind(value) {
|
|
570
|
+
if (value === null) return 'null'
|
|
571
|
+
if (Array.isArray(value)) return 'array'
|
|
572
|
+
return typeof value
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
/**
|
|
576
|
+
* Returns the dependency-relevant state of a property descriptor.
|
|
577
|
+
* @param {PropertyDescriptor | undefined} descriptor Property descriptor.
|
|
578
|
+
* @returns {string} Descriptor state.
|
|
579
|
+
*/
|
|
580
|
+
static #descriptorState(descriptor) {
|
|
581
|
+
if (!descriptor) return 'missing'
|
|
582
|
+
return descriptor.enumerable ? 'enumerable' : 'non-enumerable'
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
/**
|
|
586
|
+
* Compares two ordered property-key arrays.
|
|
587
|
+
* @param {PropertyKey[]} left Current keys.
|
|
588
|
+
* @param {PropertyKey[]} right Recorded keys.
|
|
589
|
+
* @returns {boolean} Whether both key sets and orders match.
|
|
590
|
+
*/
|
|
591
|
+
static #sameKeys(left, right) {
|
|
592
|
+
return (
|
|
593
|
+
left.length === right.length &&
|
|
594
|
+
left.every((key, index) => Object.is(key, right[index]))
|
|
595
|
+
)
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Returns whether a computation result is a promise or thenable.
|
|
600
|
+
* @param {unknown} value Candidate result.
|
|
601
|
+
* @returns {boolean} Whether the result is asynchronous.
|
|
602
|
+
*/
|
|
603
|
+
static #isPromiseLike(value) {
|
|
604
|
+
return Boolean(
|
|
605
|
+
value &&
|
|
606
|
+
(typeof value === 'object' || typeof value === 'function') &&
|
|
607
|
+
typeof value.then === 'function'
|
|
608
|
+
)
|
|
609
|
+
}
|
|
610
|
+
}
|
|
@@ -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
|
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),
|
package/src/index.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export { CircuitJsonDocument } from './core/CircuitJsonDocument.mjs'
|
|
2
2
|
export { CircuitJsonIndexer } from './core/CircuitJsonIndexer.mjs'
|
|
3
3
|
export { CircuitJsonUnits } from './core/CircuitJsonUnits.mjs'
|
|
4
|
+
export { SelfAdjustingComputation } from './core/SelfAdjustingComputation.mjs'
|
|
4
5
|
export { Parser } from './core/Parser.mjs'
|
|
5
6
|
export { ProjectLoader } from './core/ProjectLoader.mjs'
|
|
6
7
|
export { CircuitJsonDocumentContext } from './core/context/CircuitJsonDocumentContext.mjs'
|