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.
@@ -0,0 +1,86 @@
1
+ const NO_STANDARD_COLLECTION = Symbol('NoStandardCollection')
2
+ const MAP_ENTRIES = Map.prototype.entries
3
+ const MAP_ITERATOR_NEXT = Object.getPrototypeOf(new Map().entries()).next
4
+ const MAP_SIZE = Object.getOwnPropertyDescriptor(Map.prototype, 'size')?.get
5
+ const SET_ITERATOR_NEXT = Object.getPrototypeOf(new Set().values()).next
6
+ const SET_SIZE = Object.getOwnPropertyDescriptor(Set.prototype, 'size')?.get
7
+ const SET_VALUES = Set.prototype.values
8
+
9
+ /**
10
+ * Normalizes standard structured-clone collections in bounded entry slices.
11
+ */
12
+ export class StructuredCloneCollectionNormalizer {
13
+ /**
14
+ * Returns the private sentinel for a non-collection candidate.
15
+ * @returns {symbol} Non-collection sentinel.
16
+ */
17
+ static get notHandled() {
18
+ return NO_STANDARD_COLLECTION
19
+ }
20
+
21
+ /**
22
+ * Normalizes one Map or Set through captured platform iteration.
23
+ * @param {object} value Collection candidate.
24
+ * @param {object | null} prototype Captured prototype.
25
+ * @param {PropertyKey[]} keys Captured own keys.
26
+ * @param {number} depth Current collection depth.
27
+ * @param {{ adopt: (value: unknown, depth: number) => Generator<void, unknown, void>, checkpoint: () => Generator<void, void, void>, collect: (value: unknown) => Generator<void, void, void>, remember: (source: object, result: unknown[]) => void, reserve: (count: number) => void }} operations Traversal operations.
28
+ * @returns {Generator<void, unknown, void>} Normalized collection or sentinel.
29
+ */
30
+ static *normalize(value, prototype, keys, depth, operations) {
31
+ const map = prototype === Map.prototype
32
+ const set = prototype === Set.prototype
33
+ if (!map && !set) return NO_STANDARD_COLLECTION
34
+ if (keys.length) {
35
+ throw new TypeError(
36
+ `Canonical asset source ${map ? 'maps' : 'sets'} may not have custom properties.`
37
+ )
38
+ }
39
+ const sizeGetter = map ? MAP_SIZE : SET_SIZE
40
+ if (typeof sizeGetter !== 'function') {
41
+ throw new TypeError(
42
+ `Canonical asset source contains an invalid ${map ? 'map' : 'set'}.`
43
+ )
44
+ }
45
+ let size
46
+ let iterator
47
+ try {
48
+ size = Reflect.apply(sizeGetter, value, [])
49
+ iterator = Reflect.apply(map ? MAP_ENTRIES : SET_VALUES, value, [])
50
+ } catch {
51
+ throw new TypeError(
52
+ `Canonical asset source contains an invalid ${map ? 'map' : 'set'}.`
53
+ )
54
+ }
55
+ operations.reserve(1)
56
+ operations.reserve(map ? size * 2 : size)
57
+ const result = []
58
+ operations.remember(value, result)
59
+ const iteratorNext = map ? MAP_ITERATOR_NEXT : SET_ITERATOR_NEXT
60
+ for (let index = 0; index < size; index += 1) {
61
+ const row = Reflect.apply(iteratorNext, iterator, [])
62
+ if (row.done || (map && !Array.isArray(row.value))) {
63
+ throw new TypeError(
64
+ `Canonical asset source ${map ? 'map' : 'set'} changed during inspection.`
65
+ )
66
+ }
67
+ result[index] = map
68
+ ? [
69
+ yield* operations.adopt(row.value[0], depth + 1),
70
+ yield* operations.adopt(row.value[1], depth + 1)
71
+ ]
72
+ : yield* operations.adopt(row.value, depth + 1)
73
+ yield* operations.checkpoint()
74
+ }
75
+ if (!Reflect.apply(iteratorNext, iterator, []).done) {
76
+ throw new TypeError(
77
+ `Canonical asset source ${map ? 'map' : 'set'} changed during inspection.`
78
+ )
79
+ }
80
+ yield* operations.collect(result)
81
+ return result
82
+ }
83
+ }
84
+
85
+ Object.freeze(StructuredCloneCollectionNormalizer.prototype)
86
+ Object.freeze(StructuredCloneCollectionNormalizer)
@@ -0,0 +1,55 @@
1
+ const TEXT_ACCOUNTING_CHUNK_CHARACTERS = 64 * 1_024
2
+ const STRING_CHAR_CODE_AT = String.prototype.charCodeAt
3
+ const STRING_SLICE = String.prototype.slice
4
+ const TEXT_ENCODER = new TextEncoder()
5
+ const TEXT_ENCODER_ENCODE = TextEncoder.prototype.encode
6
+
7
+ /**
8
+ * Accounts immutable structured-clone text without one long UTF-8 scan.
9
+ */
10
+ export class StructuredCloneTextAccounting {
11
+ /**
12
+ * Measures one string across bounded encoding slices.
13
+ * @param {string} value Immutable text value.
14
+ * @param {{ bytes: number, label: string, maxBytes: number }} state Shared accounting state.
15
+ * @param {{ checkpoint: () => Generator<void, void, void>, reserve: (count: number) => void }} operations Traversal operations.
16
+ * @returns {Generator<void, void, void>} Bounded text accounting pass.
17
+ */
18
+ static *reserve(value, state, operations) {
19
+ if (state.maxBytes === Number.MAX_SAFE_INTEGER) return
20
+ let length = 0
21
+ let offset = 0
22
+ while (offset < value.length) {
23
+ let end = Math.min(
24
+ value.length,
25
+ offset + TEXT_ACCOUNTING_CHUNK_CHARACTERS
26
+ )
27
+ const trailing = Reflect.apply(STRING_CHAR_CODE_AT, value, [
28
+ end - 1
29
+ ])
30
+ const next = Reflect.apply(STRING_CHAR_CODE_AT, value, [end])
31
+ if (
32
+ end < value.length &&
33
+ trailing >= 0xd800 &&
34
+ trailing <= 0xdbff &&
35
+ next >= 0xdc00 &&
36
+ next <= 0xdfff
37
+ ) {
38
+ end -= 1
39
+ }
40
+ const chunk = Reflect.apply(STRING_SLICE, value, [offset, end])
41
+ length += Reflect.apply(TEXT_ENCODER_ENCODE, TEXT_ENCODER, [
42
+ chunk
43
+ ]).byteLength
44
+ if (state.bytes + length > state.maxBytes) {
45
+ throw new TypeError(`${state.label} is too large.`)
46
+ }
47
+ offset = end
48
+ if (offset < value.length) yield* operations.checkpoint()
49
+ }
50
+ operations.reserve(length)
51
+ }
52
+ }
53
+
54
+ Object.freeze(StructuredCloneTextAccounting.prototype)
55
+ Object.freeze(StructuredCloneTextAccounting)
@@ -1,4 +1,5 @@
1
1
  import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
2
+ import { StructuredCloneAdoption } from './StructuredCloneAdoption.mjs'
2
3
 
3
4
  const METADATA_MAX_DEPTH = 256
4
5
  const METADATA_MAX_ITEMS = 100_000
@@ -80,6 +81,58 @@ export class StructuredDataSnapshot {
80
81
  return StructuredDataSnapshot.#capture(value, state, 0)
81
82
  }
82
83
 
84
+ /**
85
+ * Adopts a proven standard structured-clone graph synchronously.
86
+ * @param {unknown} value Structured-cloned metadata candidate.
87
+ * @param {Record<string, any>} state Shared capture state.
88
+ * @returns {unknown} Adopted graph.
89
+ */
90
+ static adoptStructuredClone(value, state) {
91
+ return StructuredCloneAdoption.adopt(
92
+ value,
93
+ state,
94
+ (candidate, captureState, depth) =>
95
+ StructuredDataSnapshot.#capture(candidate, captureState, depth)
96
+ )
97
+ }
98
+
99
+ /**
100
+ * Adopts and seals a proven structured-clone graph in bounded work slices.
101
+ * @param {unknown} value Structured-cloned metadata candidate.
102
+ * @param {Record<string, any>} state Shared capture state.
103
+ * @param {() => Promise<void> | void} yieldControl Host scheduler.
104
+ * @returns {Promise<unknown>} Adopted and sealed graph.
105
+ */
106
+ static async adoptStructuredCloneAsync(value, state, yieldControl) {
107
+ return StructuredCloneAdoption.adoptAndSealAsync(
108
+ value,
109
+ state,
110
+ (candidate, captureState, depth) =>
111
+ StructuredDataSnapshot.#capture(candidate, captureState, depth),
112
+ yieldControl
113
+ )
114
+ }
115
+
116
+ /**
117
+ * Consumes the sealing plan produced by synchronous clone adoption.
118
+ * @param {unknown} value Adopted graph root.
119
+ * @returns {object | null} Trusted one-use sealing plan.
120
+ * @internal
121
+ */
122
+ static consumeStructuredCloneAdoption(value) {
123
+ return StructuredCloneAdoption.consume(value)
124
+ }
125
+
126
+ /**
127
+ * Seals one synchronously adopted graph from its trusted plan.
128
+ * @param {object} plan Trusted adoption plan.
129
+ * @returns {void}
130
+ * @internal
131
+ */
132
+ static sealStructuredCloneAdoption(plan) {
133
+ StructuredCloneAdoption.seal(plan)
134
+ }
135
+
83
136
  /**
84
137
  * Accounts an already normalized metadata snapshot without cloning it.
85
138
  * @param {unknown} value Owned metadata snapshot.
@@ -17,16 +17,17 @@ export class DocumentResult {
17
17
  * @returns {Record<string, any>} Canonical document result.
18
18
  */
19
19
  static create(fields = {}) {
20
- return DocumentResult.#create(fields, false)
20
+ return DocumentResult.#create(fields, false, false)
21
21
  }
22
22
 
23
23
  /**
24
24
  * Creates the common envelope with the requested extension ownership mode.
25
25
  * @param {Record<string, any>} fields Document fields.
26
26
  * @param {boolean} readonlyExtensions Whether to capture immutable extensions now.
27
+ * @param {boolean} standardBuiltins Whether extension data is a toolkit-owned ordinary graph.
27
28
  * @returns {Record<string, any>} Canonical document result.
28
29
  */
29
- static #create(fields, readonlyExtensions) {
30
+ static #create(fields, readonlyExtensions, standardBuiltins) {
30
31
  const source = DocumentResult.#source(fields)
31
32
  const audit = CircuitJsonSerializedInputAudit.inspect(
32
33
  fields.model,
@@ -46,7 +47,7 @@ export class DocumentResult {
46
47
  extensions: DocumentResult.extensionForSource(
47
48
  source.format,
48
49
  fields.extensions,
49
- { readonly: readonlyExtensions }
50
+ { readonly: readonlyExtensions, standardBuiltins }
50
51
  ),
51
52
  assets: Array.isArray(fields.assets)
52
53
  ? fields.assets.map((asset) =>
@@ -75,7 +76,29 @@ export class DocumentResult {
75
76
  * @returns {Record<string, any>} Proven canonical document result.
76
77
  */
77
78
  static createValidated(fields = {}, runtime = {}) {
78
- const document = DocumentResult.#create(fields, true)
79
+ return DocumentResult.#createValidated(fields, runtime, false)
80
+ }
81
+
82
+ /**
83
+ * Creates a validated envelope by consuming toolkit-owned ordinary
84
+ * extension data without an intermediate defensive graph copy.
85
+ * @param {Record<string, any>} [fields] Toolkit-owned document fields.
86
+ * @param {{ sourceReference?: object }} [runtime] Runtime-only fields.
87
+ * @returns {Record<string, any>} Proven canonical document result.
88
+ */
89
+ static createValidatedOwned(fields = {}, runtime = {}) {
90
+ return DocumentResult.#createValidated(fields, runtime, true)
91
+ }
92
+
93
+ /**
94
+ * Creates one validated envelope with explicit extension provenance.
95
+ * @param {Record<string, any>} fields Document fields.
96
+ * @param {{ sourceReference?: object }} runtime Runtime-only fields.
97
+ * @param {boolean} standardBuiltins Whether extension data is toolkit-owned.
98
+ * @returns {Record<string, any>} Proven canonical document result.
99
+ */
100
+ static #createValidated(fields, runtime, standardBuiltins) {
101
+ const document = DocumentResult.#create(fields, true, standardBuiltins)
79
102
  if (Object.hasOwn(runtime || {}, 'sourceReference')) {
80
103
  Object.defineProperty(document, 'sourceReference', {
81
104
  configurable: false,
@@ -84,7 +107,9 @@ export class DocumentResult {
84
107
  writable: false
85
108
  })
86
109
  }
87
- return CircuitJsonValidationProof.validateAndAttach(document)
110
+ return CircuitJsonValidationProof.validateAndAttach(document, {
111
+ standardBuiltins
112
+ })
88
113
  }
89
114
 
90
115
  /**
@@ -113,7 +138,7 @@ export class DocumentResult {
113
138
  * Selects and normalizes the extension namespace owned by a source.
114
139
  * @param {string} format Source format.
115
140
  * @param {unknown} extensions Extension candidates.
116
- * @param {{ readonly?: boolean }} [options] Extension ownership options.
141
+ * @param {{ readonly?: boolean, standardBuiltins?: boolean }} [options] Extension ownership options.
117
142
  * @returns {Record<string, any>} Normalized extension map.
118
143
  */
119
144
  static extensionForSource(format, extensions, options = {}) {
@@ -122,6 +147,13 @@ export class DocumentResult {
122
147
  const hasCandidate = candidate && typeof candidate === 'object'
123
148
  if (!hasCandidate) return {}
124
149
  if (options.readonly === true && hasCandidate) {
150
+ if (options.standardBuiltins === true) {
151
+ return CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
152
+ DocumentResult.#normalizedExtension(format, candidate),
153
+ null,
154
+ { standardBuiltins: true }
155
+ )
156
+ }
125
157
  return CircuitJsonReadOnlyDocument.copyReadonlyExtensionValue(
126
158
  candidate,
127
159
  (snapshot) =>
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'
@@ -11,6 +11,7 @@ const CANONICAL_CLASS_NAMES = Object.freeze([
11
11
  'SimulationService',
12
12
  'PcbScene3dBuilder',
13
13
  'PcbScene3dPreparator',
14
+ 'SelfAdjustingComputation',
14
15
  'ToolkitCapabilities',
15
16
  'ToolkitError'
16
17
  ])