circuitjson-toolkit 1.0.17 → 1.1.0

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 (154) hide show
  1. package/NOTICE.md +31 -0
  2. package/README.md +267 -107
  3. package/docs/api.md +501 -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 +120 -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/testing.md +117 -7
  14. package/package.json +31 -5
  15. package/spec/api-baseline-v1.0.17.json +1 -0
  16. package/spec/baseline-provenance-v1.0.17.json +7 -0
  17. package/spec/circuitjson-schema-snapshot.json +321 -0
  18. package/spec/circuitjson-schema-source.json +28 -0
  19. package/spec/feature-preservation.json +1 -0
  20. package/spec/library-scope.md +27 -20
  21. package/src/capabilities.mjs +1 -0
  22. package/src/core/ArchiveEntryPath.mjs +93 -0
  23. package/src/core/ArchiveLimits.mjs +31 -0
  24. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  25. package/src/core/AsyncInputOwnership.mjs +56 -0
  26. package/src/core/AttachedValueLimits.mjs +67 -0
  27. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  28. package/src/core/CircuitJsonDocument.mjs +19 -61
  29. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  30. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  31. package/src/core/CircuitJsonIndexer.mjs +274 -194
  32. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  33. package/src/core/CircuitJsonParser.mjs +75 -13
  34. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  35. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  36. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  37. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  38. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  39. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  40. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  41. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  42. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  43. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  44. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  45. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  46. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  47. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  48. package/src/core/CircuitJsonUnits.mjs +13 -87
  49. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  50. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  51. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  52. package/src/core/ManufacturingService.mjs +323 -0
  53. package/src/core/Parser.mjs +343 -0
  54. package/src/core/ParserOptions.mjs +333 -0
  55. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  56. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  57. package/src/core/PcbInteractionIndex.mjs +368 -0
  58. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  59. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  60. package/src/core/ProjectLoader.mjs +975 -0
  61. package/src/core/SimulationService.mjs +790 -0
  62. package/src/core/ToolkitCapabilities.mjs +130 -0
  63. package/src/core/ZipArchiveInspector.mjs +649 -0
  64. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  65. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  66. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  67. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  68. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  69. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  70. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  71. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  72. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  73. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  74. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  75. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  76. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  77. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  78. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  79. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  80. package/src/core/contracts/DocumentResult.mjs +198 -0
  81. package/src/core/contracts/ProjectResult.mjs +96 -0
  82. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  83. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  84. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  85. package/src/core/contracts/ToolkitError.mjs +176 -0
  86. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  87. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  88. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  89. package/src/core/query/CircuitTraversal.mjs +343 -0
  90. package/src/core/query/ComponentGrouping.mjs +275 -0
  91. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  92. package/src/core/query/QueryService.mjs +435 -0
  93. package/src/core/query/RegexPattern.mjs +75 -0
  94. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  95. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  96. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  97. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  98. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  99. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  100. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  101. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  102. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  103. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  104. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  105. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  106. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  107. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  108. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  109. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  110. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  111. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  112. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  113. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  114. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  115. package/src/core/worker/WorkerRequestData.mjs +623 -0
  116. package/src/core/worker/WorkerResponseData.mjs +905 -0
  117. package/src/extensions.mjs +37 -0
  118. package/src/index.mjs +14 -9
  119. package/src/interaction.mjs +2 -0
  120. package/src/manufacturing.mjs +1 -0
  121. package/src/parser.mjs +12 -2
  122. package/src/project.mjs +5 -0
  123. package/src/query.mjs +1 -0
  124. package/src/renderers.mjs +3 -29
  125. package/src/scene3d.mjs +3 -0
  126. package/src/simulation.mjs +1 -0
  127. package/src/styles/renderers.css +24 -0
  128. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  129. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  130. package/src/testing/runToolkitContract.mjs +705 -0
  131. package/src/testing.mjs +3 -0
  132. package/src/ui/BomTableRenderer.mjs +304 -0
  133. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  134. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  135. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  136. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  137. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  138. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  139. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  140. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  141. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  142. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  143. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  144. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  145. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  146. package/src/ui/PcbSvgRenderer.mjs +41 -0
  147. package/src/ui/SafeSvgPaint.mjs +26 -0
  148. package/src/ui/SafeXmlText.mjs +60 -0
  149. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  150. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  151. package/src/ui/SchematicTableGeometry.mjs +319 -0
  152. package/src/ui/SchematicTextAnchor.mjs +55 -0
  153. package/src/ui/SchematicTextBounds.mjs +98 -0
  154. package/src/workers/parser.worker.mjs +59 -0
@@ -0,0 +1,683 @@
1
+ import { BinaryDataSnapshot } from './BinaryDataSnapshot.mjs'
2
+
3
+ const METADATA_MAX_DEPTH = 64
4
+ const METADATA_MAX_ITEMS = 100_000
5
+ const UNBOUNDED_METADATA_BYTES = Number.MAX_SAFE_INTEGER
6
+ const DATE_GET_TIME = Date.prototype.getTime
7
+ const DATE_TO_ISO = Date.prototype.toISOString
8
+ const MAP_ENTRIES = Map.prototype.entries
9
+ const MAP_ITERATOR_NEXT = Object.getPrototypeOf(new Map().entries()).next
10
+ const MAP_SIZE = Object.getOwnPropertyDescriptor(Map.prototype, 'size')?.get
11
+ const REGEXP_FLAGS = Object.getOwnPropertyDescriptor(
12
+ RegExp.prototype,
13
+ 'flags'
14
+ )?.get
15
+ const REGEXP_SOURCE = Object.getOwnPropertyDescriptor(
16
+ RegExp.prototype,
17
+ 'source'
18
+ )?.get
19
+ const SET_ITERATOR_NEXT = Object.getPrototypeOf(new Set().values()).next
20
+ const SET_SIZE = Object.getOwnPropertyDescriptor(Set.prototype, 'size')?.get
21
+ const SET_VALUES = Set.prototype.values
22
+
23
+ /**
24
+ * Atomically validates and owns bounded clone-safe metadata graphs.
25
+ */
26
+ export class StructuredDataSnapshot {
27
+ /**
28
+ * Creates state shared by multiple metadata roots in one request.
29
+ * @param {{ label?: string, maxBytes?: number, maxItems?: number, preserveBinary?: boolean }} [limits] Capture limits.
30
+ * @returns {{ seen: Map<object, unknown>, accounted: Set<object>, bytes: number, items: number, label: string, maxBytes: number, maxItems: number, preserveBinary: boolean }} Capture state.
31
+ */
32
+ static createState(limits = {}) {
33
+ const maxBytes =
34
+ limits.maxBytes === undefined
35
+ ? UNBOUNDED_METADATA_BYTES
36
+ : limits.maxBytes
37
+ const maxItems =
38
+ limits.maxItems === undefined ? METADATA_MAX_ITEMS : limits.maxItems
39
+ if (
40
+ !Number.isSafeInteger(maxBytes) ||
41
+ maxBytes < 0 ||
42
+ !Number.isSafeInteger(maxItems) ||
43
+ maxItems < 0
44
+ ) {
45
+ throw new TypeError('Invalid source-metadata capture limits.')
46
+ }
47
+ return {
48
+ seen: new Map(),
49
+ accounted: new Set(),
50
+ bytes: 0,
51
+ items: 0,
52
+ label: String(limits.label || 'Canonical asset source'),
53
+ maxBytes,
54
+ maxItems,
55
+ preserveBinary: limits.preserveBinary === true
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Captures a bounded graph in the same descriptor traversal that validates it.
61
+ * @param {unknown} value Metadata candidate.
62
+ * @param {{ seen: Map<object, unknown>, items: number }} [state] Shared capture state.
63
+ * @returns {unknown} Isolated normalized snapshot.
64
+ */
65
+ static capture(value, state = StructuredDataSnapshot.createState()) {
66
+ if (
67
+ !(state?.seen instanceof Map) ||
68
+ !(state?.accounted instanceof Set) ||
69
+ !Number.isSafeInteger(state.bytes) ||
70
+ !Number.isSafeInteger(state.items) ||
71
+ !Number.isSafeInteger(state.maxBytes) ||
72
+ !Number.isSafeInteger(state.maxItems) ||
73
+ typeof state.label !== 'string' ||
74
+ typeof state.preserveBinary !== 'boolean'
75
+ ) {
76
+ throw new TypeError('Invalid source-metadata capture state.')
77
+ }
78
+ return StructuredDataSnapshot.#capture(value, state, 0)
79
+ }
80
+
81
+ /**
82
+ * Accounts an already normalized metadata snapshot without cloning it.
83
+ * @param {unknown} value Owned metadata snapshot.
84
+ * @param {{ seen: Map<object, unknown>, accounted: Set<object>, bytes: number, items: number, label: string, maxBytes: number, maxItems: number }} state Shared capture state.
85
+ * @returns {void}
86
+ */
87
+ static account(value, state) {
88
+ if (
89
+ !(state?.seen instanceof Map) ||
90
+ !(state?.accounted instanceof Set) ||
91
+ !Number.isSafeInteger(state.bytes) ||
92
+ !Number.isSafeInteger(state.items) ||
93
+ !Number.isSafeInteger(state.maxBytes) ||
94
+ !Number.isSafeInteger(state.maxItems) ||
95
+ typeof state.label !== 'string' ||
96
+ typeof state.preserveBinary !== 'boolean'
97
+ ) {
98
+ throw new TypeError('Invalid source-metadata capture state.')
99
+ }
100
+ const stack = [{ depth: 0, value }]
101
+ while (stack.length) {
102
+ const frame = stack.pop()
103
+ const current = frame.value
104
+ const type = typeof current
105
+ if (type === 'string') {
106
+ StructuredDataSnapshot.#reserveText(state, current)
107
+ continue
108
+ }
109
+ if (
110
+ current === null ||
111
+ ['undefined', 'boolean', 'number', 'bigint'].includes(type)
112
+ ) {
113
+ continue
114
+ }
115
+ if (type !== 'object') {
116
+ throw new TypeError(
117
+ 'Canonical asset source must contain clone-safe data.'
118
+ )
119
+ }
120
+ if (frame.depth > METADATA_MAX_DEPTH) {
121
+ throw new TypeError(
122
+ 'Canonical asset source is nested too deeply.'
123
+ )
124
+ }
125
+ if (state.accounted.has(current)) continue
126
+ let prototype
127
+ let keys
128
+ try {
129
+ prototype = Object.getPrototypeOf(current)
130
+ keys = Reflect.ownKeys(current)
131
+ } catch {
132
+ throw new TypeError(
133
+ 'Canonical asset source could not be inspected safely.'
134
+ )
135
+ }
136
+ if (
137
+ prototype !== Object.prototype &&
138
+ prototype !== null &&
139
+ !(Array.isArray(current) && prototype === Array.prototype)
140
+ ) {
141
+ throw new TypeError(
142
+ 'Canonical asset source must contain plain data objects.'
143
+ )
144
+ }
145
+ state.accounted.add(current)
146
+ StructuredDataSnapshot.#reserve(state, 1)
147
+ const children = []
148
+ if (Array.isArray(current)) {
149
+ const lengthDescriptor = StructuredDataSnapshot.#descriptor(
150
+ current,
151
+ 'length',
152
+ 'Canonical asset source array could not be inspected.'
153
+ )
154
+ const length = lengthDescriptor.value
155
+ if (
156
+ !Object.hasOwn(lengthDescriptor, 'value') ||
157
+ !Number.isSafeInteger(length) ||
158
+ length < 0 ||
159
+ keys.length !== length + 1
160
+ ) {
161
+ throw new TypeError(
162
+ 'Canonical asset source arrays must be dense and plain.'
163
+ )
164
+ }
165
+ StructuredDataSnapshot.#reserve(state, length)
166
+ for (let index = 0; index < length; index += 1) {
167
+ children.push(
168
+ StructuredDataSnapshot.#dataDescriptor(
169
+ current,
170
+ String(index)
171
+ ).value
172
+ )
173
+ }
174
+ } else {
175
+ StructuredDataSnapshot.#reserve(state, keys.length)
176
+ for (const key of keys) {
177
+ if (typeof key !== 'string') {
178
+ throw new TypeError(
179
+ 'Canonical asset source keys must be strings.'
180
+ )
181
+ }
182
+ children.push(
183
+ StructuredDataSnapshot.#dataDescriptor(current, key)
184
+ .value
185
+ )
186
+ }
187
+ }
188
+ for (let index = children.length - 1; index >= 0; index -= 1) {
189
+ stack.push({
190
+ depth: frame.depth + 1,
191
+ value: children[index]
192
+ })
193
+ }
194
+ }
195
+ }
196
+
197
+ /**
198
+ * Captures one graph node without revisiting caller-owned containers.
199
+ * @param {unknown} value Metadata candidate.
200
+ * @param {{ seen: Map<object, unknown>, bytes: number, items: number, label: string, maxBytes: number, maxItems: number }} state Capture state.
201
+ * @param {number} depth Current container depth.
202
+ * @returns {unknown} Isolated normalized value.
203
+ */
204
+ static #capture(value, state, depth) {
205
+ const type = typeof value
206
+ if (type === 'string') {
207
+ StructuredDataSnapshot.#reserveText(state, value)
208
+ return value
209
+ }
210
+ if (
211
+ value === null ||
212
+ ['undefined', 'boolean', 'number', 'bigint'].includes(type)
213
+ ) {
214
+ return value
215
+ }
216
+ if (type !== 'object') {
217
+ throw new TypeError(
218
+ 'Canonical asset source must contain clone-safe data.'
219
+ )
220
+ }
221
+ if (depth > METADATA_MAX_DEPTH) {
222
+ throw new TypeError('Canonical asset source is nested too deeply.')
223
+ }
224
+ if (state.seen.has(value)) return state.seen.get(value)
225
+ StructuredDataSnapshot.#reserve(state, 1)
226
+
227
+ const binary = BinaryDataSnapshot.describe(value)
228
+ if (binary) {
229
+ return StructuredDataSnapshot.#binary(value, binary, state)
230
+ }
231
+
232
+ let prototype
233
+ let keys
234
+ try {
235
+ prototype = Object.getPrototypeOf(value)
236
+ keys = Reflect.ownKeys(value)
237
+ } catch {
238
+ throw new TypeError(
239
+ 'Canonical asset source could not be inspected safely.'
240
+ )
241
+ }
242
+ if (prototype === Date.prototype) {
243
+ return StructuredDataSnapshot.#date(value, keys, state)
244
+ }
245
+ if (prototype === RegExp.prototype) {
246
+ return StructuredDataSnapshot.#regexp(value, keys, state)
247
+ }
248
+ if (prototype === Map.prototype) {
249
+ return StructuredDataSnapshot.#map(value, keys, state, depth)
250
+ }
251
+ if (prototype === Set.prototype) {
252
+ return StructuredDataSnapshot.#set(value, keys, state, depth)
253
+ }
254
+ if (Array.isArray(value)) {
255
+ return StructuredDataSnapshot.#array(
256
+ value,
257
+ prototype,
258
+ keys,
259
+ state,
260
+ depth
261
+ )
262
+ }
263
+ if (prototype !== Object.prototype && prototype !== null) {
264
+ throw new TypeError(
265
+ 'Canonical asset source must contain plain data objects.'
266
+ )
267
+ }
268
+ return StructuredDataSnapshot.#record(
269
+ value,
270
+ prototype,
271
+ keys,
272
+ state,
273
+ depth
274
+ )
275
+ }
276
+
277
+ /**
278
+ * Copies genuine binary data after validating its single captured key set.
279
+ * @param {ArrayBuffer | SharedArrayBuffer | ArrayBufferView} value Binary value.
280
+ * @param {{ byteLength: number }} range Captured intrinsic binary range.
281
+ * @param {{ seen: Map<object, unknown>, items: number, preserveBinary: boolean }} state Capture state.
282
+ * @returns {number[] | ArrayBuffer | ArrayBufferView} Plain isolated bytes or preserved byte-backed data.
283
+ */
284
+ static #binary(value, range, state) {
285
+ StructuredDataSnapshot.#reserveBytes(state, range.byteLength)
286
+ if (state.preserveBinary) {
287
+ const result = BinaryDataSnapshot.clone(value, range)
288
+ state.seen.set(value, result)
289
+ return result
290
+ }
291
+ // The normalized result contains one numeric item per byte. Reserve that
292
+ // output before enumerating keys or allocating/copying the payload.
293
+ StructuredDataSnapshot.#reserve(state, range.byteLength)
294
+ let keys
295
+ try {
296
+ keys = Reflect.ownKeys(value)
297
+ } catch {
298
+ throw new TypeError(
299
+ 'Canonical asset source binary data could not be inspected.'
300
+ )
301
+ }
302
+ StructuredDataSnapshot.#reserve(
303
+ state,
304
+ Math.max(0, keys.length - range.byteLength)
305
+ )
306
+ for (const key of keys) {
307
+ if (typeof key !== 'string' || !/^\d+$/u.test(key)) {
308
+ throw new TypeError(
309
+ 'Canonical asset source binary data has custom properties.'
310
+ )
311
+ }
312
+ const descriptor = StructuredDataSnapshot.#descriptor(
313
+ value,
314
+ key,
315
+ 'Canonical asset source binary data could not be inspected.'
316
+ )
317
+ if (!Object.hasOwn(descriptor, 'value')) {
318
+ throw new TypeError(
319
+ 'Canonical asset source binary data has accessors.'
320
+ )
321
+ }
322
+ }
323
+ const bytes = BinaryDataSnapshot.copyBytes(value, range)
324
+ const result = new Array(bytes.byteLength)
325
+ state.seen.set(value, result)
326
+ for (let index = 0; index < bytes.byteLength; index += 1) {
327
+ result[index] = bytes[index]
328
+ }
329
+ return result
330
+ }
331
+
332
+ /**
333
+ * Converts one genuine property-free Date to ISO text.
334
+ * @param {Date} value Date candidate.
335
+ * @param {PropertyKey[]} keys Captured own keys.
336
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
337
+ * @returns {string} ISO timestamp.
338
+ */
339
+ static #date(value, keys, state) {
340
+ if (keys.length) {
341
+ throw new TypeError(
342
+ 'Canonical asset source built-ins may not have custom properties.'
343
+ )
344
+ }
345
+ let result
346
+ try {
347
+ result = DATE_TO_ISO.call(new Date(DATE_GET_TIME.call(value)))
348
+ } catch {
349
+ throw new TypeError(
350
+ 'Canonical asset source contains an invalid built-in value.'
351
+ )
352
+ }
353
+ StructuredDataSnapshot.#reserveText(state, result)
354
+ state.seen.set(value, result)
355
+ return result
356
+ }
357
+
358
+ /**
359
+ * Converts one genuine RegExp after reading its standard slot once.
360
+ * @param {RegExp} value Regular expression candidate.
361
+ * @param {PropertyKey[]} keys Captured own keys.
362
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
363
+ * @returns {{ source: string, flags: string, lastIndex: number }} Plain regexp data.
364
+ */
365
+ static #regexp(value, keys, state) {
366
+ const descriptor = StructuredDataSnapshot.#descriptor(
367
+ value,
368
+ 'lastIndex',
369
+ 'Canonical asset source regular expression could not be inspected.'
370
+ )
371
+ if (
372
+ keys.length !== 1 ||
373
+ keys[0] !== 'lastIndex' ||
374
+ !Object.hasOwn(descriptor, 'value') ||
375
+ typeof descriptor.value !== 'number' ||
376
+ typeof REGEXP_SOURCE !== 'function' ||
377
+ typeof REGEXP_FLAGS !== 'function'
378
+ ) {
379
+ throw new TypeError(
380
+ 'Canonical asset source regular expressions may not have custom properties.'
381
+ )
382
+ }
383
+ let result
384
+ try {
385
+ result = {
386
+ source: REGEXP_SOURCE.call(value),
387
+ flags: REGEXP_FLAGS.call(value),
388
+ lastIndex: descriptor.value
389
+ }
390
+ } catch {
391
+ throw new TypeError(
392
+ 'Canonical asset source contains an invalid regular expression.'
393
+ )
394
+ }
395
+ StructuredDataSnapshot.#reserveText(state, result.source)
396
+ StructuredDataSnapshot.#reserveText(state, result.flags)
397
+ state.seen.set(value, result)
398
+ return result
399
+ }
400
+
401
+ /**
402
+ * Converts one genuine Map through captured platform iteration.
403
+ * @param {Map<any, any>} value Map candidate.
404
+ * @param {PropertyKey[]} keys Captured own keys.
405
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
406
+ * @param {number} depth Current depth.
407
+ * @returns {any[][]} Plain entry pairs.
408
+ */
409
+ static #map(value, keys, state, depth) {
410
+ if (keys.length || typeof MAP_SIZE !== 'function') {
411
+ throw new TypeError(
412
+ 'Canonical asset source maps may not have custom properties.'
413
+ )
414
+ }
415
+ let size
416
+ let iterator
417
+ try {
418
+ size = MAP_SIZE.call(value)
419
+ iterator = MAP_ENTRIES.call(value)
420
+ } catch {
421
+ throw new TypeError(
422
+ 'Canonical asset source contains an invalid map.'
423
+ )
424
+ }
425
+ StructuredDataSnapshot.#reserve(state, size * 2)
426
+ const result = []
427
+ state.seen.set(value, result)
428
+ for (let index = 0; index < size; index += 1) {
429
+ const row = MAP_ITERATOR_NEXT.call(iterator)
430
+ if (row.done || !Array.isArray(row.value)) {
431
+ throw new TypeError(
432
+ 'Canonical asset source map changed during inspection.'
433
+ )
434
+ }
435
+ result[index] = [
436
+ StructuredDataSnapshot.#capture(row.value[0], state, depth + 1),
437
+ StructuredDataSnapshot.#capture(row.value[1], state, depth + 1)
438
+ ]
439
+ }
440
+ if (!MAP_ITERATOR_NEXT.call(iterator).done) {
441
+ throw new TypeError(
442
+ 'Canonical asset source map changed during inspection.'
443
+ )
444
+ }
445
+ return result
446
+ }
447
+
448
+ /**
449
+ * Converts one genuine Set through captured platform iteration.
450
+ * @param {Set<any>} value Set candidate.
451
+ * @param {PropertyKey[]} keys Captured own keys.
452
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
453
+ * @param {number} depth Current depth.
454
+ * @returns {any[]} Plain values.
455
+ */
456
+ static #set(value, keys, state, depth) {
457
+ if (keys.length || typeof SET_SIZE !== 'function') {
458
+ throw new TypeError(
459
+ 'Canonical asset source sets may not have custom properties.'
460
+ )
461
+ }
462
+ let size
463
+ let iterator
464
+ try {
465
+ size = SET_SIZE.call(value)
466
+ iterator = SET_VALUES.call(value)
467
+ } catch {
468
+ throw new TypeError(
469
+ 'Canonical asset source contains an invalid set.'
470
+ )
471
+ }
472
+ StructuredDataSnapshot.#reserve(state, size)
473
+ const result = []
474
+ state.seen.set(value, result)
475
+ for (let index = 0; index < size; index += 1) {
476
+ const row = SET_ITERATOR_NEXT.call(iterator)
477
+ if (row.done) {
478
+ throw new TypeError(
479
+ 'Canonical asset source set changed during inspection.'
480
+ )
481
+ }
482
+ result[index] = StructuredDataSnapshot.#capture(
483
+ row.value,
484
+ state,
485
+ depth + 1
486
+ )
487
+ }
488
+ if (!SET_ITERATOR_NEXT.call(iterator).done) {
489
+ throw new TypeError(
490
+ 'Canonical asset source set changed during inspection.'
491
+ )
492
+ }
493
+ return result
494
+ }
495
+
496
+ /**
497
+ * Captures one dense plain array from its first key and descriptor view.
498
+ * @param {any[]} value Array candidate.
499
+ * @param {object | null} prototype Captured prototype.
500
+ * @param {PropertyKey[]} keys Captured own keys.
501
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
502
+ * @param {number} depth Current depth.
503
+ * @returns {any[]} Owned array.
504
+ */
505
+ static #array(value, prototype, keys, state, depth) {
506
+ const lengthDescriptor = StructuredDataSnapshot.#descriptor(
507
+ value,
508
+ 'length',
509
+ 'Canonical asset source array could not be inspected.'
510
+ )
511
+ const length = lengthDescriptor.value
512
+ if (
513
+ prototype !== Array.prototype ||
514
+ !Object.hasOwn(lengthDescriptor, 'value') ||
515
+ !Number.isSafeInteger(length) ||
516
+ length < 0 ||
517
+ keys.length !== length + 1
518
+ ) {
519
+ throw new TypeError(
520
+ 'Canonical asset source arrays must be dense and plain.'
521
+ )
522
+ }
523
+ StructuredDataSnapshot.#reserve(state, length)
524
+ const result = new Array(length)
525
+ state.seen.set(value, result)
526
+ for (let index = 0; index < length; index += 1) {
527
+ const key = String(index)
528
+ const descriptor = StructuredDataSnapshot.#dataDescriptor(
529
+ value,
530
+ key
531
+ )
532
+ Object.defineProperty(result, key, {
533
+ ...descriptor,
534
+ value: StructuredDataSnapshot.#capture(
535
+ descriptor.value,
536
+ state,
537
+ depth + 1
538
+ )
539
+ })
540
+ }
541
+ return result
542
+ }
543
+
544
+ /**
545
+ * Captures one plain record from its first key and descriptor view.
546
+ * @param {object} value Record candidate.
547
+ * @param {object | null} prototype Captured prototype.
548
+ * @param {PropertyKey[]} keys Captured own keys.
549
+ * @param {{ seen: Map<object, unknown>, items: number }} state Capture state.
550
+ * @param {number} depth Current depth.
551
+ * @returns {object} Owned record.
552
+ */
553
+ static #record(value, prototype, keys, state, depth) {
554
+ StructuredDataSnapshot.#reserve(state, keys.length)
555
+ const result = Object.create(prototype)
556
+ state.seen.set(value, result)
557
+ for (const key of keys) {
558
+ if (typeof key !== 'string') {
559
+ throw new TypeError(
560
+ 'Canonical asset source keys must be strings.'
561
+ )
562
+ }
563
+ const descriptor = StructuredDataSnapshot.#dataDescriptor(
564
+ value,
565
+ key
566
+ )
567
+ Object.defineProperty(result, key, {
568
+ ...descriptor,
569
+ value: StructuredDataSnapshot.#capture(
570
+ descriptor.value,
571
+ state,
572
+ depth + 1
573
+ )
574
+ })
575
+ }
576
+ return result
577
+ }
578
+
579
+ /**
580
+ * Reads one enumerable own data descriptor with normalized failures.
581
+ * @param {object} owner Property owner.
582
+ * @param {string} key Property name.
583
+ * @returns {PropertyDescriptor} Validated descriptor.
584
+ */
585
+ static #dataDescriptor(owner, key) {
586
+ const descriptor = StructuredDataSnapshot.#descriptor(
587
+ owner,
588
+ key,
589
+ 'Canonical asset source properties could not be inspected.'
590
+ )
591
+ if (
592
+ !Object.hasOwn(descriptor, 'value') ||
593
+ descriptor.enumerable !== true
594
+ ) {
595
+ throw new TypeError(
596
+ 'Canonical asset source may contain only enumerable data properties.'
597
+ )
598
+ }
599
+ return descriptor
600
+ }
601
+
602
+ /**
603
+ * Reads one own property descriptor and normalizes proxy failures.
604
+ * @param {object} owner Property owner.
605
+ * @param {PropertyKey} key Property key.
606
+ * @param {string} message Failure message.
607
+ * @returns {PropertyDescriptor} Existing own descriptor.
608
+ */
609
+ static #descriptor(owner, key, message) {
610
+ let descriptor
611
+ try {
612
+ descriptor = Object.getOwnPropertyDescriptor(owner, key)
613
+ } catch {
614
+ throw new TypeError(message)
615
+ }
616
+ if (!descriptor) throw new TypeError(message)
617
+ return descriptor
618
+ }
619
+
620
+ /**
621
+ * Reserves bounded graph work before child traversal begins.
622
+ * @param {{ items: number, label: string, maxItems: number }} state Capture state.
623
+ * @param {number} count Additional items.
624
+ * @returns {void}
625
+ */
626
+ static #reserve(state, count) {
627
+ if (
628
+ !Number.isSafeInteger(count) ||
629
+ count < 0 ||
630
+ state.items + count > state.maxItems
631
+ ) {
632
+ throw new TypeError(`${state.label} is too large.`)
633
+ }
634
+ state.items += count
635
+ }
636
+
637
+ /**
638
+ * Reserves bounded payload bytes before a metadata copy is allocated.
639
+ * @param {{ bytes: number, label: string, maxBytes: number }} state Capture state.
640
+ * @param {number} count Additional bytes.
641
+ * @returns {void}
642
+ */
643
+ static #reserveBytes(state, count) {
644
+ if (state.maxBytes === UNBOUNDED_METADATA_BYTES) return
645
+ if (
646
+ !Number.isSafeInteger(count) ||
647
+ count < 0 ||
648
+ state.bytes + count > state.maxBytes
649
+ ) {
650
+ throw new TypeError(`${state.label} is too large.`)
651
+ }
652
+ state.bytes += count
653
+ }
654
+
655
+ /**
656
+ * Measures and reserves UTF-8 text without allocating encoded bytes.
657
+ * @param {{ bytes: number, label: string, maxBytes: number }} state Capture state.
658
+ * @param {string} value Text value.
659
+ * @returns {void}
660
+ */
661
+ static #reserveText(state, value) {
662
+ if (state.maxBytes === UNBOUNDED_METADATA_BYTES) return
663
+ let length = 0
664
+ for (const character of value) {
665
+ const codePoint = character.codePointAt(0)
666
+ length +=
667
+ codePoint <= 0x7f
668
+ ? 1
669
+ : codePoint <= 0x7ff
670
+ ? 2
671
+ : codePoint <= 0xffff
672
+ ? 3
673
+ : 4
674
+ if (state.bytes + length > state.maxBytes) {
675
+ throw new TypeError(`${state.label} is too large.`)
676
+ }
677
+ }
678
+ StructuredDataSnapshot.#reserveBytes(state, length)
679
+ }
680
+ }
681
+
682
+ Object.freeze(StructuredDataSnapshot.prototype)
683
+ Object.freeze(StructuredDataSnapshot)