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
|
@@ -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
|
+
}
|
|
@@ -241,6 +241,44 @@ export class BinaryDataSnapshot {
|
|
|
241
241
|
}
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
/**
|
|
245
|
+
* Copies one bounded byte range into an existing isolated byte array.
|
|
246
|
+
* @param {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number }} range Captured intrinsic range.
|
|
247
|
+
* @param {Uint8Array} target Isolated destination bytes.
|
|
248
|
+
* @param {number} offset Visible-range byte offset.
|
|
249
|
+
* @param {number} count Number of bytes to copy.
|
|
250
|
+
* @returns {void}
|
|
251
|
+
* @internal
|
|
252
|
+
*/
|
|
253
|
+
static copyBytesInto(range, target, offset, count) {
|
|
254
|
+
if (
|
|
255
|
+
!(target instanceof Uint8Array) ||
|
|
256
|
+
!Number.isSafeInteger(offset) ||
|
|
257
|
+
!Number.isSafeInteger(count) ||
|
|
258
|
+
offset < 0 ||
|
|
259
|
+
count < 0 ||
|
|
260
|
+
offset + count > range?.byteLength ||
|
|
261
|
+
target.byteLength !== range?.byteLength
|
|
262
|
+
) {
|
|
263
|
+
throw new TypeError('Invalid binary copy range.')
|
|
264
|
+
}
|
|
265
|
+
try {
|
|
266
|
+
const source = new Uint8Array(
|
|
267
|
+
range.buffer,
|
|
268
|
+
range.byteOffset + offset,
|
|
269
|
+
count
|
|
270
|
+
)
|
|
271
|
+
const destination = new Uint8Array(
|
|
272
|
+
target.buffer,
|
|
273
|
+
target.byteOffset + offset,
|
|
274
|
+
count
|
|
275
|
+
)
|
|
276
|
+
UINT8_ARRAY_SET.call(destination, source)
|
|
277
|
+
} catch {
|
|
278
|
+
throw new TypeError('Binary data changed during capture.')
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
|
|
244
282
|
/**
|
|
245
283
|
* Copies one binary metadata value while retaining its common view type.
|
|
246
284
|
* @param {unknown} value Binary metadata.
|
|
@@ -261,6 +299,27 @@ export class BinaryDataSnapshot {
|
|
|
261
299
|
return new Constructor(bytes.buffer)
|
|
262
300
|
}
|
|
263
301
|
|
|
302
|
+
/**
|
|
303
|
+
* Restores one common binary view type from completely isolated bytes.
|
|
304
|
+
* @param {unknown} value Original binary value used only for view type.
|
|
305
|
+
* @param {{ kind: 'buffer' | 'typed-array' | 'data-view' }} range Captured intrinsic kind.
|
|
306
|
+
* @param {Uint8Array} bytes Completely copied isolated bytes.
|
|
307
|
+
* @returns {ArrayBuffer | Uint8Array | DataView} Isolated binary clone.
|
|
308
|
+
* @internal
|
|
309
|
+
*/
|
|
310
|
+
static cloneFromBytes(value, range, bytes) {
|
|
311
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
312
|
+
throw new TypeError('Expected isolated binary bytes.')
|
|
313
|
+
}
|
|
314
|
+
if (range.kind === 'buffer') return bytes.buffer
|
|
315
|
+
if (range.kind === 'data-view') return new DataView(bytes.buffer)
|
|
316
|
+
const Constructor = BinaryDataSnapshot.#typedArrayConstructor(value)
|
|
317
|
+
if (!Constructor || Constructor === Uint8Array) return bytes
|
|
318
|
+
const bytesPerElement = Constructor.BYTES_PER_ELEMENT
|
|
319
|
+
if (bytes.byteLength % bytesPerElement !== 0) return bytes
|
|
320
|
+
return new Constructor(bytes.buffer)
|
|
321
|
+
}
|
|
322
|
+
|
|
264
323
|
/**
|
|
265
324
|
* Calls a captured buffer length getter as a brand check.
|
|
266
325
|
* @param {Function | null | undefined} getter Intrinsic getter.
|