circuitjson-toolkit 1.0.17 → 1.1.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.
Files changed (155) hide show
  1. package/NOTICE.md +31 -0
  2. package/README.md +274 -107
  3. package/docs/api.md +507 -96
  4. package/docs/capabilities.md +70 -0
  5. package/docs/migration/behaviors.md +45 -0
  6. package/docs/migration/parser.md +60 -0
  7. package/docs/migration/renderers.md +515 -0
  8. package/docs/migration/root.md +740 -0
  9. package/docs/migration.md +126 -0
  10. package/docs/model-format.md +175 -57
  11. package/docs/provenance.md +206 -0
  12. package/docs/release-notes-v1.1.0.md +154 -0
  13. package/docs/release-notes-v1.1.1.md +24 -0
  14. package/docs/testing.md +122 -7
  15. package/package.json +32 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +999 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +650 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. package/src/workers/parser.worker.mjs +59 -0
@@ -0,0 +1,217 @@
1
+ const ARRAY_BUFFER_LENGTH = Object.getOwnPropertyDescriptor(
2
+ ArrayBuffer.prototype,
3
+ 'byteLength'
4
+ )?.get
5
+ const SHARED_ARRAY_BUFFER_LENGTH =
6
+ typeof SharedArrayBuffer === 'function'
7
+ ? Object.getOwnPropertyDescriptor(
8
+ SharedArrayBuffer.prototype,
9
+ 'byteLength'
10
+ )?.get
11
+ : null
12
+ const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype)
13
+ const TYPED_ARRAY_BUFFER = Object.getOwnPropertyDescriptor(
14
+ TYPED_ARRAY_PROTOTYPE,
15
+ 'buffer'
16
+ )?.get
17
+ const TYPED_ARRAY_OFFSET = Object.getOwnPropertyDescriptor(
18
+ TYPED_ARRAY_PROTOTYPE,
19
+ 'byteOffset'
20
+ )?.get
21
+ const TYPED_ARRAY_LENGTH = Object.getOwnPropertyDescriptor(
22
+ TYPED_ARRAY_PROTOTYPE,
23
+ 'byteLength'
24
+ )?.get
25
+ const DATA_VIEW_BUFFER = Object.getOwnPropertyDescriptor(
26
+ DataView.prototype,
27
+ 'buffer'
28
+ )?.get
29
+ const DATA_VIEW_OFFSET = Object.getOwnPropertyDescriptor(
30
+ DataView.prototype,
31
+ 'byteOffset'
32
+ )?.get
33
+ const DATA_VIEW_LENGTH = Object.getOwnPropertyDescriptor(
34
+ DataView.prototype,
35
+ 'byteLength'
36
+ )?.get
37
+ const UINT8_ARRAY_SET = Uint8Array.prototype.set
38
+
39
+ /**
40
+ * Reads and copies binary platform objects through captured intrinsic slots.
41
+ */
42
+ export class BinaryDataSnapshot {
43
+ /**
44
+ * Describes one ArrayBuffer, SharedArrayBuffer, typed array, or DataView.
45
+ * @param {unknown} value Binary candidate.
46
+ * @returns {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number, kind: 'buffer' | 'typed-array' | 'data-view' } | null} Intrinsic binary range.
47
+ */
48
+ static describe(value) {
49
+ const arrayBufferLength = BinaryDataSnapshot.#callLength(
50
+ ARRAY_BUFFER_LENGTH,
51
+ value
52
+ )
53
+ if (arrayBufferLength !== null) {
54
+ return {
55
+ buffer: value,
56
+ byteOffset: 0,
57
+ byteLength: arrayBufferLength,
58
+ kind: 'buffer'
59
+ }
60
+ }
61
+ const sharedLength = BinaryDataSnapshot.#callLength(
62
+ SHARED_ARRAY_BUFFER_LENGTH,
63
+ value
64
+ )
65
+ if (sharedLength !== null) {
66
+ return {
67
+ buffer: value,
68
+ byteOffset: 0,
69
+ byteLength: sharedLength,
70
+ kind: 'buffer'
71
+ }
72
+ }
73
+
74
+ const typed = BinaryDataSnapshot.#view(
75
+ value,
76
+ TYPED_ARRAY_BUFFER,
77
+ TYPED_ARRAY_OFFSET,
78
+ TYPED_ARRAY_LENGTH,
79
+ 'typed-array'
80
+ )
81
+ if (typed) return typed
82
+ return BinaryDataSnapshot.#view(
83
+ value,
84
+ DATA_VIEW_BUFFER,
85
+ DATA_VIEW_OFFSET,
86
+ DATA_VIEW_LENGTH,
87
+ 'data-view'
88
+ )
89
+ }
90
+
91
+ /**
92
+ * Returns the exact intrinsic binary byte length.
93
+ * @param {unknown} value Binary candidate.
94
+ * @returns {number | null} Byte length or null for non-binary input.
95
+ */
96
+ static byteLength(value) {
97
+ return BinaryDataSnapshot.describe(value)?.byteLength ?? null
98
+ }
99
+
100
+ /**
101
+ * Copies the exact visible byte range into isolated non-shared memory.
102
+ * @param {unknown} value Binary candidate.
103
+ * @param {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number } | null} [capturedRange] Previously captured intrinsic range.
104
+ * @returns {Uint8Array} Isolated bytes.
105
+ */
106
+ static copyBytes(value, capturedRange = null) {
107
+ const range = capturedRange || BinaryDataSnapshot.describe(value)
108
+ if (!range) throw new TypeError('Expected binary data.')
109
+ try {
110
+ const source = new Uint8Array(
111
+ range.buffer,
112
+ range.byteOffset,
113
+ range.byteLength
114
+ )
115
+ const result = new Uint8Array(range.byteLength)
116
+ UINT8_ARRAY_SET.call(result, source)
117
+ return result
118
+ } catch {
119
+ throw new TypeError('Binary data changed during capture.')
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Copies one binary metadata value while retaining its common view type.
125
+ * @param {unknown} value Binary metadata.
126
+ * @param {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number, kind: 'buffer' | 'typed-array' | 'data-view' } | null} [capturedRange] Previously captured intrinsic range.
127
+ * @returns {ArrayBuffer | Uint8Array | DataView} Isolated binary value.
128
+ */
129
+ static clone(value, capturedRange = null) {
130
+ const range = capturedRange || BinaryDataSnapshot.describe(value)
131
+ if (!range) throw new TypeError('Expected binary data.')
132
+ const bytes = BinaryDataSnapshot.copyBytes(value, range)
133
+ if (range.kind === 'buffer') return bytes.buffer
134
+ if (range.kind === 'data-view') return new DataView(bytes.buffer)
135
+
136
+ const prototype = Object.getPrototypeOf(value)
137
+ const Constructor = BinaryDataSnapshot.#typedArrayConstructor(prototype)
138
+ if (!Constructor || Constructor === Uint8Array) return bytes
139
+ const bytesPerElement = Constructor.BYTES_PER_ELEMENT
140
+ if (bytes.byteLength % bytesPerElement !== 0) return bytes
141
+ return new Constructor(bytes.buffer)
142
+ }
143
+
144
+ /**
145
+ * Calls a captured buffer length getter as a brand check.
146
+ * @param {Function | null | undefined} getter Intrinsic getter.
147
+ * @param {unknown} value Candidate value.
148
+ * @returns {number | null} Intrinsic length or null.
149
+ */
150
+ static #callLength(getter, value) {
151
+ if (typeof getter !== 'function') return null
152
+ try {
153
+ return getter.call(value)
154
+ } catch {
155
+ return null
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Reads captured intrinsic view slots without ordinary property access.
161
+ * @param {unknown} value View candidate.
162
+ * @param {Function | undefined} bufferGetter Buffer getter.
163
+ * @param {Function | undefined} offsetGetter Offset getter.
164
+ * @param {Function | undefined} lengthGetter Length getter.
165
+ * @param {'typed-array' | 'data-view'} kind View kind.
166
+ * @returns {{ buffer: ArrayBuffer | SharedArrayBuffer, byteOffset: number, byteLength: number, kind: 'typed-array' | 'data-view' } | null} Intrinsic view range.
167
+ */
168
+ static #view(value, bufferGetter, offsetGetter, lengthGetter, kind) {
169
+ if (
170
+ typeof bufferGetter !== 'function' ||
171
+ typeof offsetGetter !== 'function' ||
172
+ typeof lengthGetter !== 'function'
173
+ ) {
174
+ return null
175
+ }
176
+ try {
177
+ return {
178
+ buffer: bufferGetter.call(value),
179
+ byteOffset: offsetGetter.call(value),
180
+ byteLength: lengthGetter.call(value),
181
+ kind
182
+ }
183
+ } catch {
184
+ return null
185
+ }
186
+ }
187
+
188
+ /**
189
+ * Maps a genuine typed-array prototype to its platform constructor.
190
+ * @param {object | null} prototype Candidate prototype.
191
+ * @returns {Function | null} Matching typed-array constructor.
192
+ */
193
+ static #typedArrayConstructor(prototype) {
194
+ const constructors = [
195
+ Int8Array,
196
+ Uint8Array,
197
+ Uint8ClampedArray,
198
+ Int16Array,
199
+ Uint16Array,
200
+ Int32Array,
201
+ Uint32Array,
202
+ Float32Array,
203
+ Float64Array,
204
+ ...(typeof BigInt64Array === 'function'
205
+ ? [BigInt64Array, BigUint64Array]
206
+ : [])
207
+ ]
208
+ return (
209
+ constructors.find(
210
+ (Constructor) => Constructor.prototype === prototype
211
+ ) || null
212
+ )
213
+ }
214
+ }
215
+
216
+ Object.freeze(BinaryDataSnapshot.prototype)
217
+ Object.freeze(BinaryDataSnapshot)
@@ -0,0 +1,96 @@
1
+ import { CircuitJsonIndexer } from '../CircuitJsonIndexer.mjs'
2
+ import { CircuitJsonValidationProof } from './CircuitJsonValidationProof.mjs'
3
+
4
+ const INDEX_NAMES = new Set([
5
+ 'elements',
6
+ 'identifiers',
7
+ 'relations',
8
+ 'connectivity',
9
+ 'spatial'
10
+ ])
11
+
12
+ /**
13
+ * Owns lazily prepared named indexes for one document context.
14
+ */
15
+ export class CircuitJsonContextIndexes {
16
+ #builds
17
+ #document
18
+ #indexes = new Map()
19
+ #model
20
+
21
+ /**
22
+ * Creates a context-owned index registry.
23
+ * @param {Record<string, any>} document Proven document envelope.
24
+ * @param {object[]} model Proven CircuitJSON model.
25
+ * @param {Record<string, number>} builds Named index build counters.
26
+ */
27
+ constructor(document, model, builds) {
28
+ this.#document = document
29
+ this.#model = model
30
+ this.#builds = builds
31
+ }
32
+
33
+ /**
34
+ * Ensures every requested named index is available.
35
+ * @param {unknown} names Requested index names.
36
+ * @returns {CircuitJsonContextIndexes} This registry.
37
+ */
38
+ ensure(names = []) {
39
+ for (const name of CircuitJsonContextIndexes.#names(names)) {
40
+ if (!INDEX_NAMES.has(name)) {
41
+ throw new RangeError(
42
+ `Unsupported CircuitJSON context index: ${name}.`
43
+ )
44
+ }
45
+ if (this.#indexes.has(name)) continue
46
+
47
+ this.#indexes.set(name, this.#build(name))
48
+ this.#builds[name] = (this.#builds[name] || 0) + 1
49
+ }
50
+ return this
51
+ }
52
+
53
+ /**
54
+ * Returns one named index, creating it when first requested.
55
+ * @param {unknown} name Requested index name.
56
+ * @returns {Record<string, any>} Prepared index.
57
+ */
58
+ get(name) {
59
+ const normalized = String(name || '').trim()
60
+ this.ensure([normalized])
61
+ return this.#indexes.get(normalized)
62
+ }
63
+
64
+ /**
65
+ * Returns true when one named index is already prepared.
66
+ * @param {unknown} name Requested index name.
67
+ * @returns {boolean} Whether the named index exists.
68
+ */
69
+ has(name) {
70
+ return this.#indexes.has(String(name || '').trim())
71
+ }
72
+
73
+ /**
74
+ * Creates only the work family requested by one named index.
75
+ * @param {string} name Requested index name.
76
+ * @returns {Record<string, any>} Named index view.
77
+ */
78
+ #build(name) {
79
+ return CircuitJsonIndexer.index(
80
+ this.#model,
81
+ CircuitJsonValidationProof.indexOptions(this.#document, [name])
82
+ )
83
+ }
84
+
85
+ /**
86
+ * Normalizes an index name request.
87
+ * @param {unknown} names Requested names.
88
+ * @returns {string[]} Normalized names.
89
+ */
90
+ static #names(names) {
91
+ const candidates = Array.isArray(names) ? names : [names]
92
+ return [
93
+ ...new Set(candidates.map(String).map((name) => name.trim()))
94
+ ].filter(Boolean)
95
+ }
96
+ }
@@ -0,0 +1,114 @@
1
+ const VALUE_OWNERS = new WeakMap()
2
+
3
+ /**
4
+ * Stores request-scoped derived values by namespace and key.
5
+ */
6
+ export class CircuitJsonDerivedCache {
7
+ #builds
8
+ #namespaces = new Map()
9
+
10
+ /**
11
+ * Creates a request-scoped derived cache.
12
+ * @param {Record<string, number>} builds Successful build counters.
13
+ */
14
+ constructor(builds) {
15
+ this.#builds = builds
16
+ }
17
+
18
+ /**
19
+ * Returns an existing value or creates and caches one successful result.
20
+ * @param {string} namespace Cache namespace.
21
+ * @param {string} key Cache key.
22
+ * @param {() => any} factory Value factory.
23
+ * @returns {any} Cached or newly built value.
24
+ */
25
+ getOrCreate(namespace, key, factory) {
26
+ if (typeof factory !== 'function') {
27
+ throw new TypeError('A derived value factory is required.')
28
+ }
29
+
30
+ const entries = this.#entries(namespace)
31
+ if (entries.has(key)) return entries.get(key)
32
+
33
+ const value = factory()
34
+ if (
35
+ value &&
36
+ (typeof value === 'object' || typeof value === 'function') &&
37
+ typeof value.then === 'function'
38
+ ) {
39
+ throw new TypeError(
40
+ 'Derived value factories must return synchronous values.'
41
+ )
42
+ }
43
+ CircuitJsonDerivedCache.#bindValue(this, namespace, key, value)
44
+ entries.set(key, value)
45
+ const statisticKey = `${namespace}:${key}`
46
+ this.#builds[statisticKey] = (this.#builds[statisticKey] || 0) + 1
47
+ return value
48
+ }
49
+
50
+ /**
51
+ * Returns whether an object value belongs to this exact cache entry.
52
+ * @param {string} namespace Cache namespace.
53
+ * @param {string} key Cache key.
54
+ * @param {unknown} value Derived value candidate.
55
+ * @returns {boolean} Whether ownership matches cache, namespace, and key.
56
+ */
57
+ owns(namespace, key, value) {
58
+ if (!CircuitJsonDerivedCache.#isObject(value)) return false
59
+ const owner = VALUE_OWNERS.get(value)
60
+ return (
61
+ owner?.cache === this &&
62
+ owner.namespace === namespace &&
63
+ owner.key === key
64
+ )
65
+ }
66
+
67
+ /**
68
+ * Returns the cache map for one namespace.
69
+ * @param {string} namespace Cache namespace.
70
+ * @returns {Map<string, any>} Namespace entries.
71
+ */
72
+ #entries(namespace) {
73
+ if (!this.#namespaces.has(namespace)) {
74
+ this.#namespaces.set(namespace, new Map())
75
+ }
76
+ return this.#namespaces.get(namespace)
77
+ }
78
+
79
+ /**
80
+ * Binds one object value to exactly one request-scoped cache entry.
81
+ * @param {CircuitJsonDerivedCache} cache Owning cache.
82
+ * @param {string} namespace Cache namespace.
83
+ * @param {string} key Cache key.
84
+ * @param {unknown} value Derived value.
85
+ * @returns {void}
86
+ */
87
+ static #bindValue(cache, namespace, key, value) {
88
+ if (!CircuitJsonDerivedCache.#isObject(value)) return
89
+ const owner = VALUE_OWNERS.get(value)
90
+ if (
91
+ owner &&
92
+ (owner.cache !== cache ||
93
+ owner.namespace !== namespace ||
94
+ owner.key !== key)
95
+ ) {
96
+ throw new TypeError(
97
+ 'Derived object values cannot be transplanted between cache entries.'
98
+ )
99
+ }
100
+ VALUE_OWNERS.set(value, { cache, namespace, key })
101
+ }
102
+
103
+ /**
104
+ * Returns whether a value can be owned through a WeakMap.
105
+ * @param {unknown} value Value candidate.
106
+ * @returns {boolean} Whether the value is an object or function.
107
+ */
108
+ static #isObject(value) {
109
+ return (
110
+ value !== null &&
111
+ (typeof value === 'object' || typeof value === 'function')
112
+ )
113
+ }
114
+ }