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.
- package/NOTICE.md +31 -0
- package/README.md +267 -107
- package/docs/api.md +501 -96
- package/docs/capabilities.md +70 -0
- package/docs/migration/behaviors.md +45 -0
- package/docs/migration/parser.md +60 -0
- package/docs/migration/renderers.md +515 -0
- package/docs/migration/root.md +740 -0
- package/docs/migration.md +120 -0
- package/docs/model-format.md +175 -57
- package/docs/provenance.md +206 -0
- package/docs/release-notes-v1.1.0.md +154 -0
- package/docs/testing.md +117 -7
- package/package.json +31 -5
- package/spec/api-baseline-v1.0.17.json +1 -0
- package/spec/baseline-provenance-v1.0.17.json +7 -0
- package/spec/circuitjson-schema-snapshot.json +321 -0
- package/spec/circuitjson-schema-source.json +28 -0
- package/spec/feature-preservation.json +1 -0
- package/spec/library-scope.md +27 -20
- package/src/capabilities.mjs +1 -0
- package/src/core/ArchiveEntryPath.mjs +93 -0
- package/src/core/ArchiveLimits.mjs +31 -0
- package/src/core/ArchiveLimitsValidator.mjs +107 -0
- package/src/core/AsyncInputOwnership.mjs +56 -0
- package/src/core/AttachedValueLimits.mjs +67 -0
- package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
- package/src/core/CircuitJsonDocument.mjs +19 -61
- package/src/core/CircuitJsonElementTypes.mjs +10 -0
- package/src/core/CircuitJsonElementValidator.mjs +98 -847
- package/src/core/CircuitJsonIndexer.mjs +274 -194
- package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
- package/src/core/CircuitJsonParser.mjs +75 -13
- package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
- package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
- package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
- package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
- package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
- package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
- package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
- package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
- package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
- package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
- package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
- package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
- package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
- package/src/core/CircuitJsonUnitParsers.mjs +101 -0
- package/src/core/CircuitJsonUnits.mjs +13 -87
- package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
- package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
- package/src/core/CircuitJsonValidationUnits.mjs +6 -0
- package/src/core/ManufacturingService.mjs +323 -0
- package/src/core/Parser.mjs +343 -0
- package/src/core/ParserOptions.mjs +333 -0
- package/src/core/PcbBoundsSelectionModel.mjs +55 -19
- package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
- package/src/core/PcbInteractionIndex.mjs +368 -0
- package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
- package/src/core/ProjectAsyncInputOwner.mjs +70 -0
- package/src/core/ProjectLoader.mjs +975 -0
- package/src/core/SimulationService.mjs +790 -0
- package/src/core/ToolkitCapabilities.mjs +130 -0
- package/src/core/ZipArchiveInspector.mjs +649 -0
- package/src/core/context/BinaryDataSnapshot.mjs +217 -0
- package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
- package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
- package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
- package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
- package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
- package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
- package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
- package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
- package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
- package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
- package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
- package/src/core/context/PcbSpatialIndex.mjs +701 -0
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
- package/src/core/context/StructuredDataSnapshot.mjs +683 -0
- package/src/core/contracts/DocumentResult.mjs +198 -0
- package/src/core/contracts/ProjectResult.mjs +96 -0
- package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
- package/src/core/contracts/ToolkitAsset.mjs +493 -0
- package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
- package/src/core/contracts/ToolkitError.mjs +176 -0
- package/src/core/contracts/ToolkitProgress.mjs +89 -0
- package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
- package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
- package/src/core/query/CircuitTraversal.mjs +343 -0
- package/src/core/query/ComponentGrouping.mjs +275 -0
- package/src/core/query/QueryNetlistBuilder.mjs +306 -0
- package/src/core/query/QueryService.mjs +435 -0
- package/src/core/query/RegexPattern.mjs +75 -0
- package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
- package/src/core/rendering/CanonicalBomRows.mjs +92 -0
- package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
- package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
- package/src/core/rendering/PcbRenderPlan.mjs +429 -0
- package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
- package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
- package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
- package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
- package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
- package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
- package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
- package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
- package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
- package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
- package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
- package/src/core/scene3d/Scene3dOptions.mjs +363 -0
- package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
- package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
- package/src/core/worker/ParserWorkerClient.mjs +997 -0
- package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
- package/src/core/worker/WorkerRequestData.mjs +623 -0
- package/src/core/worker/WorkerResponseData.mjs +905 -0
- package/src/extensions.mjs +37 -0
- package/src/index.mjs +14 -9
- package/src/interaction.mjs +2 -0
- package/src/manufacturing.mjs +1 -0
- package/src/parser.mjs +12 -2
- package/src/project.mjs +5 -0
- package/src/query.mjs +1 -0
- package/src/renderers.mjs +3 -29
- package/src/scene3d.mjs +3 -0
- package/src/simulation.mjs +1 -0
- package/src/styles/renderers.css +24 -0
- package/src/testing/ToolkitContractFixtures.mjs +124 -0
- package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
- package/src/testing/runToolkitContract.mjs +705 -0
- package/src/testing.mjs +3 -0
- package/src/ui/BomTableRenderer.mjs +304 -0
- package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
- package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
- package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
- package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
- package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
- package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
- package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
- package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
- package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
- package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
- package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
- package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
- package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
- package/src/ui/PcbSvgRenderer.mjs +41 -0
- package/src/ui/SafeSvgPaint.mjs +26 -0
- package/src/ui/SafeXmlText.mjs +60 -0
- package/src/ui/SchematicGeometryBounds.mjs +540 -0
- package/src/ui/SchematicSvgRenderer.mjs +110 -0
- package/src/ui/SchematicTableGeometry.mjs +319 -0
- package/src/ui/SchematicTextAnchor.mjs +55 -0
- package/src/ui/SchematicTextBounds.mjs +98 -0
- package/src/workers/parser.worker.mjs +59 -0
|
@@ -0,0 +1,493 @@
|
|
|
1
|
+
import { BinaryDataSnapshot } from '../context/BinaryDataSnapshot.mjs'
|
|
2
|
+
import { CircuitJsonReadOnlyDocument } from '../context/CircuitJsonReadOnlyDocument.mjs'
|
|
3
|
+
import { AttachedValueLimits } from '../AttachedValueLimits.mjs'
|
|
4
|
+
|
|
5
|
+
const ASSET_MODES = new Set(['none', 'metadata', 'full'])
|
|
6
|
+
const PREPARED_ASSETS = new WeakMap()
|
|
7
|
+
const SCALAR_FIELDS = ['id', 'kind', 'name', 'mediaType', 'byteLength']
|
|
8
|
+
const MEDIA_TYPES_BY_SUFFIX = new Map([
|
|
9
|
+
['3mf', 'model/3mf'],
|
|
10
|
+
['glb', 'model/gltf-binary'],
|
|
11
|
+
['gltf', 'model/gltf+json'],
|
|
12
|
+
['iges', 'model/iges'],
|
|
13
|
+
['igs', 'model/iges'],
|
|
14
|
+
['obj', 'model/obj'],
|
|
15
|
+
['step', 'model/step'],
|
|
16
|
+
['stl', 'model/stl'],
|
|
17
|
+
['stp', 'model/step'],
|
|
18
|
+
['vrml', 'model/vrml'],
|
|
19
|
+
['wrl', 'model/vrml'],
|
|
20
|
+
['bmp', 'image/bmp'],
|
|
21
|
+
['gif', 'image/gif'],
|
|
22
|
+
['jpeg', 'image/jpeg'],
|
|
23
|
+
['jpg', 'image/jpeg'],
|
|
24
|
+
['png', 'image/png'],
|
|
25
|
+
['svg', 'image/svg+xml'],
|
|
26
|
+
['webp', 'image/webp'],
|
|
27
|
+
['json', 'application/json'],
|
|
28
|
+
['pdf', 'application/pdf'],
|
|
29
|
+
['zip', 'application/zip']
|
|
30
|
+
])
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Normalizes embedded and external asset records through one shared ownership
|
|
34
|
+
* and accounting boundary.
|
|
35
|
+
*/
|
|
36
|
+
export class ToolkitAsset {
|
|
37
|
+
/**
|
|
38
|
+
* Creates one full clone-safe asset record.
|
|
39
|
+
* @param {Record<string, any>} [fields] Asset fields.
|
|
40
|
+
* @returns {{ id: string, kind: string, name: string, mediaType: string, byteLength: number, data: any, source: any }} Normalized asset.
|
|
41
|
+
*/
|
|
42
|
+
static create(fields = {}) {
|
|
43
|
+
const prepared =
|
|
44
|
+
fields && typeof fields === 'object'
|
|
45
|
+
? PREPARED_ASSETS.get(fields)
|
|
46
|
+
: null
|
|
47
|
+
return ToolkitAsset.prepare(
|
|
48
|
+
prepared ? fields : ToolkitAsset.#legacyPayload(fields),
|
|
49
|
+
{ mode: 'full' }
|
|
50
|
+
)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Measures one asset's resident payload without copying it.
|
|
55
|
+
* @param {unknown} fields Asset candidate.
|
|
56
|
+
* @returns {number} Exact resident or declared payload byte length.
|
|
57
|
+
*/
|
|
58
|
+
static measure(fields) {
|
|
59
|
+
const prepared =
|
|
60
|
+
fields && typeof fields === 'object'
|
|
61
|
+
? PREPARED_ASSETS.get(fields)
|
|
62
|
+
: null
|
|
63
|
+
if (prepared) return prepared.byteLength
|
|
64
|
+
const descriptors = ToolkitAsset.#descriptors(fields)
|
|
65
|
+
return ToolkitAsset.#payloadByteLength(
|
|
66
|
+
ToolkitAsset.#dataValue(descriptors.data),
|
|
67
|
+
ToolkitAsset.#dataValue(descriptors.byteLength)
|
|
68
|
+
)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Prepares one descriptor-safe asset for a requested decode mode.
|
|
73
|
+
* @param {unknown} fields Asset candidate.
|
|
74
|
+
* @param {{ mode?: 'none' | 'metadata' | 'full', metadataBudget?: object, acceptPayload?: (byteLength: number, identity: { id: unknown, name: unknown }) => void }} [options] Preparation options.
|
|
75
|
+
* @returns {Record<string, any> | null} Prepared asset or null for none.
|
|
76
|
+
*/
|
|
77
|
+
static prepare(fields = {}, options = {}) {
|
|
78
|
+
const normalizedOptions = ToolkitAsset.#options(options)
|
|
79
|
+
const existing =
|
|
80
|
+
fields && typeof fields === 'object'
|
|
81
|
+
? PREPARED_ASSETS.get(fields)
|
|
82
|
+
: null
|
|
83
|
+
if (existing) {
|
|
84
|
+
ToolkitAsset.#acceptPrepared(
|
|
85
|
+
normalizedOptions.acceptPayload,
|
|
86
|
+
existing,
|
|
87
|
+
fields
|
|
88
|
+
)
|
|
89
|
+
if (normalizedOptions.mode === 'none') return null
|
|
90
|
+
if (
|
|
91
|
+
existing.mode === 'full' &&
|
|
92
|
+
normalizedOptions.mode === 'metadata'
|
|
93
|
+
) {
|
|
94
|
+
return ToolkitAsset.prepare(
|
|
95
|
+
CircuitJsonReadOnlyDocument.copyAssetMetadataFields(fields),
|
|
96
|
+
{ ...normalizedOptions, acceptPayload: null }
|
|
97
|
+
)
|
|
98
|
+
}
|
|
99
|
+
return fields
|
|
100
|
+
}
|
|
101
|
+
if (normalizedOptions.mode === 'none') {
|
|
102
|
+
const descriptors = ToolkitAsset.#descriptors(fields)
|
|
103
|
+
const byteLength = ToolkitAsset.#payloadByteLength(
|
|
104
|
+
ToolkitAsset.#dataValue(descriptors.data),
|
|
105
|
+
ToolkitAsset.#dataValue(descriptors.byteLength)
|
|
106
|
+
)
|
|
107
|
+
ToolkitAsset.#acceptPayload(
|
|
108
|
+
normalizedOptions.acceptPayload,
|
|
109
|
+
byteLength,
|
|
110
|
+
descriptors
|
|
111
|
+
)
|
|
112
|
+
return null
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const descriptors = ToolkitAsset.#descriptors(fields)
|
|
116
|
+
const data = ToolkitAsset.#dataValue(descriptors.data)
|
|
117
|
+
const byteLength = ToolkitAsset.#payloadByteLength(
|
|
118
|
+
data,
|
|
119
|
+
ToolkitAsset.#dataValue(descriptors.byteLength)
|
|
120
|
+
)
|
|
121
|
+
ToolkitAsset.#acceptPayload(
|
|
122
|
+
normalizedOptions.acceptPayload,
|
|
123
|
+
byteLength,
|
|
124
|
+
descriptors
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
const kind = ToolkitAsset.#stringScalar(
|
|
128
|
+
ToolkitAsset.#dataValue(descriptors.kind),
|
|
129
|
+
'asset',
|
|
130
|
+
'kind'
|
|
131
|
+
)
|
|
132
|
+
const name = ToolkitAsset.#stringScalar(
|
|
133
|
+
ToolkitAsset.#dataValue(descriptors.name),
|
|
134
|
+
'',
|
|
135
|
+
'name'
|
|
136
|
+
)
|
|
137
|
+
const budget =
|
|
138
|
+
normalizedOptions.metadataBudget ||
|
|
139
|
+
CircuitJsonReadOnlyDocument.createMetadataBudget()
|
|
140
|
+
const source = CircuitJsonReadOnlyDocument.copyReadonlyMetadataValue(
|
|
141
|
+
ToolkitAsset.#dataValue(descriptors.source) ?? null,
|
|
142
|
+
budget
|
|
143
|
+
)
|
|
144
|
+
const id = ToolkitAsset.#stringScalar(
|
|
145
|
+
ToolkitAsset.#dataValue(descriptors.id),
|
|
146
|
+
ToolkitAsset.#id(kind, name, source),
|
|
147
|
+
'id'
|
|
148
|
+
)
|
|
149
|
+
const normalized = {
|
|
150
|
+
id,
|
|
151
|
+
kind,
|
|
152
|
+
name,
|
|
153
|
+
mediaType: ToolkitAsset.#stringScalar(
|
|
154
|
+
ToolkitAsset.#dataValue(descriptors.mediaType),
|
|
155
|
+
ToolkitAsset.#mediaTypeForName(name),
|
|
156
|
+
'mediaType'
|
|
157
|
+
),
|
|
158
|
+
byteLength,
|
|
159
|
+
data: normalizedOptions.mode === 'full' ? (data ?? null) : null,
|
|
160
|
+
source
|
|
161
|
+
}
|
|
162
|
+
const captured = CircuitJsonReadOnlyDocument.captureAsset(
|
|
163
|
+
normalized,
|
|
164
|
+
budget
|
|
165
|
+
)
|
|
166
|
+
PREPARED_ASSETS.set(captured, {
|
|
167
|
+
byteLength,
|
|
168
|
+
mode: normalizedOptions.mode
|
|
169
|
+
})
|
|
170
|
+
return captured
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Prepares one dense asset list through data descriptors only.
|
|
175
|
+
* @param {unknown} assets Asset list candidate.
|
|
176
|
+
* @param {{ mode?: 'none' | 'metadata' | 'full', metadataBudget?: object, acceptPayload?: (byteLength: number, identity: { id: unknown, name: unknown }) => void }} [options] Preparation options.
|
|
177
|
+
* @returns {Record<string, any>[]} Prepared assets.
|
|
178
|
+
*/
|
|
179
|
+
static prepareAll(assets, options = {}) {
|
|
180
|
+
AttachedValueLimits.add(assets)
|
|
181
|
+
const normalizedOptions = ToolkitAsset.#options(options)
|
|
182
|
+
const descriptors = ToolkitAsset.#arrayDescriptors(assets)
|
|
183
|
+
const length = descriptors.length.value
|
|
184
|
+
const metadataBudget =
|
|
185
|
+
normalizedOptions.metadataBudget ||
|
|
186
|
+
CircuitJsonReadOnlyDocument.createMetadataBudget()
|
|
187
|
+
const result = []
|
|
188
|
+
for (let index = 0; index < length; index += 1) {
|
|
189
|
+
const asset = ToolkitAsset.prepare(
|
|
190
|
+
descriptors[String(index)].value,
|
|
191
|
+
{
|
|
192
|
+
...normalizedOptions,
|
|
193
|
+
metadataBudget
|
|
194
|
+
}
|
|
195
|
+
)
|
|
196
|
+
if (asset) result.push(asset)
|
|
197
|
+
}
|
|
198
|
+
return result
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/**
|
|
202
|
+
* Reads and validates one options record without invoking accessors.
|
|
203
|
+
* @param {unknown} options Options candidate.
|
|
204
|
+
* @returns {{ mode: 'none' | 'metadata' | 'full', metadataBudget: object | null, acceptPayload: Function | null }} Normalized options.
|
|
205
|
+
*/
|
|
206
|
+
static #options(options) {
|
|
207
|
+
const descriptors = ToolkitAsset.#plainDescriptors(
|
|
208
|
+
options,
|
|
209
|
+
'Toolkit asset options must be a plain object.'
|
|
210
|
+
)
|
|
211
|
+
const allowed = new Set(['mode', 'metadataBudget', 'acceptPayload'])
|
|
212
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
213
|
+
if (typeof key !== 'string' || !allowed.has(key)) {
|
|
214
|
+
throw new TypeError(
|
|
215
|
+
`Unsupported ToolkitAsset option: ${String(key)}.`
|
|
216
|
+
)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
const mode = ToolkitAsset.#dataValue(descriptors.mode) ?? 'full'
|
|
220
|
+
if (!ASSET_MODES.has(mode)) {
|
|
221
|
+
throw new TypeError(
|
|
222
|
+
'ToolkitAsset mode must be none, metadata, or full.'
|
|
223
|
+
)
|
|
224
|
+
}
|
|
225
|
+
const acceptPayload = ToolkitAsset.#dataValue(descriptors.acceptPayload)
|
|
226
|
+
if (
|
|
227
|
+
acceptPayload !== undefined &&
|
|
228
|
+
acceptPayload !== null &&
|
|
229
|
+
typeof acceptPayload !== 'function'
|
|
230
|
+
) {
|
|
231
|
+
throw new TypeError(
|
|
232
|
+
'ToolkitAsset acceptPayload must be a function.'
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
return {
|
|
236
|
+
mode,
|
|
237
|
+
metadataBudget:
|
|
238
|
+
ToolkitAsset.#dataValue(descriptors.metadataBudget) || null,
|
|
239
|
+
acceptPayload: acceptPayload || null
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/**
|
|
244
|
+
* Reads one exact dense plain asset array.
|
|
245
|
+
* @param {unknown} value Array candidate.
|
|
246
|
+
* @returns {Record<string, PropertyDescriptor>} Array descriptors.
|
|
247
|
+
*/
|
|
248
|
+
static #arrayDescriptors(value) {
|
|
249
|
+
if (!Array.isArray(value)) {
|
|
250
|
+
throw new TypeError('Toolkit assets must be a dense plain array.')
|
|
251
|
+
}
|
|
252
|
+
let prototype
|
|
253
|
+
let descriptors
|
|
254
|
+
try {
|
|
255
|
+
prototype = Object.getPrototypeOf(value)
|
|
256
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
257
|
+
} catch {
|
|
258
|
+
throw new TypeError('Toolkit assets could not be inspected safely.')
|
|
259
|
+
}
|
|
260
|
+
const length = ToolkitAsset.#dataValue(descriptors.length)
|
|
261
|
+
if (
|
|
262
|
+
prototype !== Array.prototype ||
|
|
263
|
+
!Number.isSafeInteger(length) ||
|
|
264
|
+
length < 0 ||
|
|
265
|
+
Reflect.ownKeys(descriptors).length !== length + 1
|
|
266
|
+
) {
|
|
267
|
+
throw new TypeError('Toolkit assets must be a dense plain array.')
|
|
268
|
+
}
|
|
269
|
+
for (let index = 0; index < length; index += 1) {
|
|
270
|
+
const descriptor = descriptors[String(index)]
|
|
271
|
+
if (
|
|
272
|
+
!descriptor ||
|
|
273
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
274
|
+
descriptor.enumerable !== true
|
|
275
|
+
) {
|
|
276
|
+
throw new TypeError(
|
|
277
|
+
'Toolkit assets must contain enumerable data properties.'
|
|
278
|
+
)
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return descriptors
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Reads one asset's own descriptors.
|
|
286
|
+
* @param {unknown} fields Asset candidate.
|
|
287
|
+
* @returns {Record<string, PropertyDescriptor>} Field descriptors.
|
|
288
|
+
*/
|
|
289
|
+
static #descriptors(fields) {
|
|
290
|
+
const descriptors = ToolkitAsset.#plainDescriptors(
|
|
291
|
+
fields,
|
|
292
|
+
'Toolkit asset must be a plain object.'
|
|
293
|
+
)
|
|
294
|
+
const allowed = new Set([...SCALAR_FIELDS, 'data', 'source'])
|
|
295
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
296
|
+
const descriptor = descriptors[key]
|
|
297
|
+
if (
|
|
298
|
+
typeof key !== 'string' ||
|
|
299
|
+
!allowed.has(key) ||
|
|
300
|
+
descriptor.enumerable !== true
|
|
301
|
+
) {
|
|
302
|
+
throw new TypeError(
|
|
303
|
+
'Toolkit assets may contain only canonical enumerable fields.'
|
|
304
|
+
)
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return descriptors
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* Returns data descriptors for one plain object.
|
|
312
|
+
* @param {unknown} value Record candidate.
|
|
313
|
+
* @param {string} message Type error message.
|
|
314
|
+
* @returns {Record<string, PropertyDescriptor>} Data descriptors.
|
|
315
|
+
*/
|
|
316
|
+
static #plainDescriptors(value, message) {
|
|
317
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
318
|
+
throw new TypeError(message)
|
|
319
|
+
}
|
|
320
|
+
let prototype
|
|
321
|
+
let descriptors
|
|
322
|
+
try {
|
|
323
|
+
prototype = Object.getPrototypeOf(value)
|
|
324
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
325
|
+
} catch {
|
|
326
|
+
throw new TypeError(message)
|
|
327
|
+
}
|
|
328
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
329
|
+
throw new TypeError(message)
|
|
330
|
+
}
|
|
331
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
332
|
+
if (!Object.hasOwn(descriptor, 'value')) {
|
|
333
|
+
throw new TypeError(
|
|
334
|
+
'Toolkit assets may contain only data properties.'
|
|
335
|
+
)
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
return descriptors
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
/**
|
|
342
|
+
* Reads one descriptor's value.
|
|
343
|
+
* @param {PropertyDescriptor | undefined} descriptor Descriptor.
|
|
344
|
+
* @returns {unknown} Data value.
|
|
345
|
+
*/
|
|
346
|
+
static #dataValue(descriptor) {
|
|
347
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
348
|
+
? descriptor.value
|
|
349
|
+
: undefined
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Measures supported binary, textual, or external metadata payloads.
|
|
354
|
+
* @param {unknown} data Asset payload.
|
|
355
|
+
* @param {unknown} declaredByteLength Declared metadata byte length.
|
|
356
|
+
* @returns {number} Exact byte length.
|
|
357
|
+
*/
|
|
358
|
+
static #payloadByteLength(data, declaredByteLength) {
|
|
359
|
+
const binaryLength = BinaryDataSnapshot.byteLength(data)
|
|
360
|
+
if (binaryLength !== null) return binaryLength
|
|
361
|
+
if (typeof data === 'string') {
|
|
362
|
+
return CircuitJsonReadOnlyDocument.utf8ByteLength(data)
|
|
363
|
+
}
|
|
364
|
+
if (data !== null && data !== undefined) {
|
|
365
|
+
throw new TypeError(
|
|
366
|
+
'Toolkit asset data must be text, binary data, or null.'
|
|
367
|
+
)
|
|
368
|
+
}
|
|
369
|
+
if (declaredByteLength === undefined) return 0
|
|
370
|
+
if (
|
|
371
|
+
!Number.isSafeInteger(declaredByteLength) ||
|
|
372
|
+
declaredByteLength < 0
|
|
373
|
+
) {
|
|
374
|
+
throw new TypeError(
|
|
375
|
+
'Toolkit asset byteLength must be a non-negative safe integer.'
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
return declaredByteLength
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
/**
|
|
382
|
+
* Retains the historical create() fallback for unsupported payload values.
|
|
383
|
+
* @param {unknown} fields Asset fields.
|
|
384
|
+
* @returns {unknown} Original fields or a descriptor-preserving null payload.
|
|
385
|
+
*/
|
|
386
|
+
static #legacyPayload(fields) {
|
|
387
|
+
const descriptors = ToolkitAsset.#descriptors(fields)
|
|
388
|
+
const data = ToolkitAsset.#dataValue(descriptors.data)
|
|
389
|
+
if (
|
|
390
|
+
data === null ||
|
|
391
|
+
data === undefined ||
|
|
392
|
+
typeof data === 'string' ||
|
|
393
|
+
BinaryDataSnapshot.byteLength(data) !== null
|
|
394
|
+
) {
|
|
395
|
+
return fields
|
|
396
|
+
}
|
|
397
|
+
const normalized = Object.create(Object.getPrototypeOf(fields))
|
|
398
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
399
|
+
const descriptor = descriptors[key]
|
|
400
|
+
Object.defineProperty(normalized, key, {
|
|
401
|
+
...descriptor,
|
|
402
|
+
value: key === 'data' ? null : descriptor.value
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
return normalized
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Normalizes one primitive scalar without object coercion.
|
|
410
|
+
* @param {unknown} value Scalar candidate.
|
|
411
|
+
* @param {string} fallback Missing fallback.
|
|
412
|
+
* @param {string} field Field name.
|
|
413
|
+
* @returns {string} String value.
|
|
414
|
+
*/
|
|
415
|
+
static #stringScalar(value, fallback, field) {
|
|
416
|
+
if (value === undefined || value === null || value === '') {
|
|
417
|
+
return fallback
|
|
418
|
+
}
|
|
419
|
+
if (!['string', 'number', 'boolean', 'bigint'].includes(typeof value)) {
|
|
420
|
+
throw new TypeError(
|
|
421
|
+
`Toolkit asset ${field} must be a primitive scalar.`
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
return String(value)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Infers a stable media type from a canonical asset path.
|
|
429
|
+
* @param {string} name Asset name.
|
|
430
|
+
* @returns {string} Inferred type or the binary fallback.
|
|
431
|
+
*/
|
|
432
|
+
static #mediaTypeForName(name) {
|
|
433
|
+
const path = name.split(/[?#]/u, 1)[0].toLowerCase()
|
|
434
|
+
const suffix = path.split('.').at(-1) || ''
|
|
435
|
+
return MEDIA_TYPES_BY_SUFFIX.get(suffix) || 'application/octet-stream'
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Runs a pre-copy payload acceptance callback.
|
|
440
|
+
* @param {Function | null} acceptPayload Acceptance callback.
|
|
441
|
+
* @param {number} byteLength Measured payload bytes.
|
|
442
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Asset descriptors.
|
|
443
|
+
* @returns {void}
|
|
444
|
+
*/
|
|
445
|
+
static #acceptPayload(acceptPayload, byteLength, descriptors) {
|
|
446
|
+
if (!acceptPayload) return
|
|
447
|
+
acceptPayload(byteLength, {
|
|
448
|
+
id: ToolkitAsset.#dataValue(descriptors.id),
|
|
449
|
+
name: ToolkitAsset.#dataValue(descriptors.name)
|
|
450
|
+
})
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Applies accounting to an already prepared asset without recopying it.
|
|
455
|
+
* @param {Function | null} acceptPayload Acceptance callback.
|
|
456
|
+
* @param {{ byteLength: number }} prepared Private preparation state.
|
|
457
|
+
* @param {Record<string, any>} fields Prepared asset.
|
|
458
|
+
* @returns {void}
|
|
459
|
+
*/
|
|
460
|
+
static #acceptPrepared(acceptPayload, prepared, fields) {
|
|
461
|
+
if (!acceptPayload) return
|
|
462
|
+
const descriptors = Object.getOwnPropertyDescriptors(fields)
|
|
463
|
+
ToolkitAsset.#acceptPayload(
|
|
464
|
+
acceptPayload,
|
|
465
|
+
prepared.byteLength,
|
|
466
|
+
descriptors
|
|
467
|
+
)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Creates a deterministic source-identity asset id.
|
|
472
|
+
* @param {string} kind Asset kind.
|
|
473
|
+
* @param {string} name Asset name.
|
|
474
|
+
* @param {unknown} source Source reference.
|
|
475
|
+
* @returns {string} Stable id.
|
|
476
|
+
*/
|
|
477
|
+
static #id(kind, name, source) {
|
|
478
|
+
let sourceText = ''
|
|
479
|
+
try {
|
|
480
|
+
sourceText = JSON.stringify(source, (_key, value) =>
|
|
481
|
+
typeof value === 'bigint' ? `${value}n` : value
|
|
482
|
+
)
|
|
483
|
+
} catch {
|
|
484
|
+
sourceText = '[cyclic-source]'
|
|
485
|
+
}
|
|
486
|
+
let hash = 2166136261
|
|
487
|
+
for (const character of `${kind}\u0000${name}\u0000${sourceText}`) {
|
|
488
|
+
hash ^= character.codePointAt(0)
|
|
489
|
+
hash = Math.imul(hash, 16777619)
|
|
490
|
+
}
|
|
491
|
+
return `asset-${(hash >>> 0).toString(16).padStart(8, '0')}`
|
|
492
|
+
}
|
|
493
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Creates a structured-clone-safe copy of one contract value.
|
|
3
|
+
* @param {unknown} value Candidate value.
|
|
4
|
+
* @param {unknown} fallback Value used when cloning is impossible.
|
|
5
|
+
* @returns {any} Clone-safe value.
|
|
6
|
+
*/
|
|
7
|
+
export function cloneSafeValue(value, fallback = null) {
|
|
8
|
+
if (value === undefined) return fallback
|
|
9
|
+
try {
|
|
10
|
+
return structuredClone(value)
|
|
11
|
+
} catch {
|
|
12
|
+
return fallback
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Normalizes toolkit diagnostics into the shared clone-safe record shape.
|
|
18
|
+
*/
|
|
19
|
+
export class ToolkitDiagnostic {
|
|
20
|
+
/**
|
|
21
|
+
* Creates one diagnostic record.
|
|
22
|
+
* @param {Record<string, any>} [fields] Diagnostic fields.
|
|
23
|
+
* @returns {{ code: string, severity: string, message: string, source: string, location: any, details: any }} Normalized diagnostic.
|
|
24
|
+
*/
|
|
25
|
+
static create(fields = {}) {
|
|
26
|
+
const severity = String(fields.severity || 'info')
|
|
27
|
+
return {
|
|
28
|
+
code: String(fields.code || 'TOOLKIT_DIAGNOSTIC'),
|
|
29
|
+
severity: ['info', 'warning', 'error'].includes(severity)
|
|
30
|
+
? severity
|
|
31
|
+
: 'info',
|
|
32
|
+
message: String(fields.message || ''),
|
|
33
|
+
source: String(fields.source || ''),
|
|
34
|
+
location: cloneSafeValue(fields.location, null),
|
|
35
|
+
details: cloneSafeValue(fields.details, {})
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
import { cloneSafeValue } from './ToolkitDiagnostic.mjs'
|
|
2
|
+
import { RuntimeProxyBoundary } from './RuntimeProxyBoundary.mjs'
|
|
3
|
+
|
|
4
|
+
const ERROR_CATEGORIES = new Set([
|
|
5
|
+
'parse',
|
|
6
|
+
'validation',
|
|
7
|
+
'unsupported',
|
|
8
|
+
'cancelled',
|
|
9
|
+
'runtime'
|
|
10
|
+
])
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Shared typed toolkit failure with clone-safe serialization.
|
|
14
|
+
*/
|
|
15
|
+
export class ToolkitError extends Error {
|
|
16
|
+
#trustedRecord
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Creates a typed toolkit error.
|
|
20
|
+
* @param {unknown} message Error message.
|
|
21
|
+
* @param {Record<string, any>} [fields] Structured failure fields.
|
|
22
|
+
*/
|
|
23
|
+
constructor(message, fields = {}) {
|
|
24
|
+
super(String(message || 'Toolkit operation failed.'))
|
|
25
|
+
this.name = 'ToolkitError'
|
|
26
|
+
this.code = String(fields.code || 'ERR_TOOLKIT_RUNTIME')
|
|
27
|
+
const category = String(fields.category || 'runtime')
|
|
28
|
+
this.category = ERROR_CATEGORIES.has(category) ? category : 'runtime'
|
|
29
|
+
this.format = String(fields.format || 'circuitjson')
|
|
30
|
+
this.source = String(fields.source || '')
|
|
31
|
+
this.location = cloneSafeValue(fields.location, null)
|
|
32
|
+
this.details = cloneSafeValue(fields.details, {})
|
|
33
|
+
this.cause = ToolkitError.cloneSafeCause(fields.cause)
|
|
34
|
+
this.#trustedRecord = {
|
|
35
|
+
name: this.name,
|
|
36
|
+
message: this.message,
|
|
37
|
+
code: this.code,
|
|
38
|
+
category: this.category,
|
|
39
|
+
format: this.format,
|
|
40
|
+
source: this.source,
|
|
41
|
+
location: cloneSafeValue(this.location, null),
|
|
42
|
+
details: cloneSafeValue(this.details, {}),
|
|
43
|
+
cause: cloneSafeValue(this.cause, null)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Normalizes any thrown value into ToolkitError.
|
|
49
|
+
* @param {unknown} error Error candidate.
|
|
50
|
+
* @param {Record<string, any>} [fields] Override fields.
|
|
51
|
+
* @returns {ToolkitError} Typed error.
|
|
52
|
+
*/
|
|
53
|
+
static from(error, fields = {}) {
|
|
54
|
+
if (ToolkitError.trustedRecord(error)) return error
|
|
55
|
+
const cause = ToolkitError.cloneSafeCause(error)
|
|
56
|
+
return new ToolkitError(cause?.message || 'Toolkit operation failed.', {
|
|
57
|
+
...fields,
|
|
58
|
+
cause
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Creates a clone-safe cause summary.
|
|
64
|
+
* @param {unknown} error Cause candidate.
|
|
65
|
+
* @returns {{ name: string, message: string, code: string | null } | null} Cause summary.
|
|
66
|
+
*/
|
|
67
|
+
static cloneSafeCause(error) {
|
|
68
|
+
if (!error) return null
|
|
69
|
+
if (!['object', 'function'].includes(typeof error)) {
|
|
70
|
+
return {
|
|
71
|
+
name: 'Error',
|
|
72
|
+
message: ToolkitError.#primitiveText(error, 'Error'),
|
|
73
|
+
code: null
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
RuntimeProxyBoundary.assert(error, 'Toolkit error cause')
|
|
78
|
+
} catch {
|
|
79
|
+
return {
|
|
80
|
+
name: 'Error',
|
|
81
|
+
message: 'Toolkit operation failed.',
|
|
82
|
+
code: null
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
const name = ToolkitError.#dataField(error, 'name')
|
|
86
|
+
const message = ToolkitError.#dataField(error, 'message')
|
|
87
|
+
const code = ToolkitError.#dataField(error, 'code')
|
|
88
|
+
return {
|
|
89
|
+
name: ToolkitError.#primitiveText(name, 'Error'),
|
|
90
|
+
message: ToolkitError.#primitiveText(
|
|
91
|
+
message,
|
|
92
|
+
'Toolkit operation failed.'
|
|
93
|
+
),
|
|
94
|
+
code:
|
|
95
|
+
code === null || code === undefined
|
|
96
|
+
? null
|
|
97
|
+
: ToolkitError.#primitiveText(code, null)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Reads one inherited data property without invoking an accessor.
|
|
103
|
+
* @param {object | Function} value Property owner.
|
|
104
|
+
* @param {string} key Property key.
|
|
105
|
+
* @returns {unknown} Data value or undefined.
|
|
106
|
+
*/
|
|
107
|
+
static #dataField(value, key) {
|
|
108
|
+
let owner = value
|
|
109
|
+
for (let depth = 0; owner && depth < 16; depth += 1) {
|
|
110
|
+
let descriptor
|
|
111
|
+
try {
|
|
112
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, key)
|
|
113
|
+
owner = Object.getPrototypeOf(owner)
|
|
114
|
+
} catch {
|
|
115
|
+
return undefined
|
|
116
|
+
}
|
|
117
|
+
if (!descriptor) continue
|
|
118
|
+
return Object.hasOwn(descriptor, 'value')
|
|
119
|
+
? descriptor.value
|
|
120
|
+
: undefined
|
|
121
|
+
}
|
|
122
|
+
return undefined
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Converts only primitive values to bounded error text.
|
|
127
|
+
* @param {unknown} value Text candidate.
|
|
128
|
+
* @param {string | null} fallback Missing or unsafe fallback.
|
|
129
|
+
* @returns {string | null} Safe text.
|
|
130
|
+
*/
|
|
131
|
+
static #primitiveText(value, fallback) {
|
|
132
|
+
if (
|
|
133
|
+
value === null ||
|
|
134
|
+
value === undefined ||
|
|
135
|
+
['object', 'function'].includes(typeof value)
|
|
136
|
+
) {
|
|
137
|
+
return fallback
|
|
138
|
+
}
|
|
139
|
+
try {
|
|
140
|
+
return String(value)
|
|
141
|
+
} catch {
|
|
142
|
+
return fallback
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Returns construction-time fields only for a genuine unproxied instance.
|
|
148
|
+
* @param {unknown} error Error candidate.
|
|
149
|
+
* @returns {ReturnType<ToolkitError['toJSON']> | null} Trusted record.
|
|
150
|
+
*/
|
|
151
|
+
static trustedRecord(error) {
|
|
152
|
+
try {
|
|
153
|
+
return cloneSafeValue(error.#trustedRecord, null)
|
|
154
|
+
} catch {
|
|
155
|
+
return null
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Serializes the error without prototype-only state.
|
|
161
|
+
* @returns {{ name: string, message: string, code: string, category: string, format: string, source: string, location: any, details: any, cause: any }} Clone-safe error record.
|
|
162
|
+
*/
|
|
163
|
+
toJSON() {
|
|
164
|
+
return {
|
|
165
|
+
name: this.name,
|
|
166
|
+
message: this.message,
|
|
167
|
+
code: this.code,
|
|
168
|
+
category: this.category,
|
|
169
|
+
format: this.format,
|
|
170
|
+
source: this.source,
|
|
171
|
+
location: this.location,
|
|
172
|
+
details: this.details,
|
|
173
|
+
cause: this.cause
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
}
|