circuitjson-toolkit 1.0.16 → 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/AGENTS.md +15 -0
- 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,975 @@
|
|
|
1
|
+
import { ArchiveEntryPath } from './ArchiveEntryPath.mjs'
|
|
2
|
+
import { ArchiveLimits } from './ArchiveLimits.mjs'
|
|
3
|
+
import { AsyncInputOwnership } from './AsyncInputOwnership.mjs'
|
|
4
|
+
import { AttachedValueLimits } from './AttachedValueLimits.mjs'
|
|
5
|
+
import { Parser } from './Parser.mjs'
|
|
6
|
+
import { ParserOptions } from './ParserOptions.mjs'
|
|
7
|
+
import { ProjectAsyncInputOwner } from './ProjectAsyncInputOwner.mjs'
|
|
8
|
+
import { ProjectResult } from './contracts/ProjectResult.mjs'
|
|
9
|
+
import { ToolkitDiagnostic } from './contracts/ToolkitDiagnostic.mjs'
|
|
10
|
+
import { ToolkitError } from './contracts/ToolkitError.mjs'
|
|
11
|
+
import { ToolkitProgress } from './contracts/ToolkitProgress.mjs'
|
|
12
|
+
import { ToolkitAsset } from './contracts/ToolkitAsset.mjs'
|
|
13
|
+
import { ParserWorkerClient } from './worker/ParserWorkerClient.mjs'
|
|
14
|
+
|
|
15
|
+
const ABORTED_GETTER = Object.getOwnPropertyDescriptor(
|
|
16
|
+
AbortSignal.prototype,
|
|
17
|
+
'aborted'
|
|
18
|
+
)?.get
|
|
19
|
+
const PARSER_OPTION_KEYS = [
|
|
20
|
+
'preserveRaw',
|
|
21
|
+
'decodeAssets',
|
|
22
|
+
'extensions',
|
|
23
|
+
'reports',
|
|
24
|
+
'retainSource',
|
|
25
|
+
'worker',
|
|
26
|
+
'transferInput',
|
|
27
|
+
'signal',
|
|
28
|
+
'onProgress'
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Loads one or more named CircuitJSON entries into a canonical project result.
|
|
33
|
+
*/
|
|
34
|
+
export class ProjectLoader {
|
|
35
|
+
/**
|
|
36
|
+
* Loads supported entries synchronously with deterministic partial success.
|
|
37
|
+
* @param {Array<{ name: string, data: string | ArrayBuffer | Uint8Array }>} entries Named entries.
|
|
38
|
+
* @param {Record<string, any>} [options] Common project/parser options.
|
|
39
|
+
* @returns {Record<string, any>} Canonical project result.
|
|
40
|
+
*/
|
|
41
|
+
static load(entries, options = {}) {
|
|
42
|
+
try {
|
|
43
|
+
const normalizedOptions = ProjectLoader.#normalizeOptions(options)
|
|
44
|
+
if (normalizedOptions.worker === true) {
|
|
45
|
+
throw ProjectLoader.#workerSyncError()
|
|
46
|
+
}
|
|
47
|
+
const snapshot = ProjectLoader.#snapshotEntries(
|
|
48
|
+
entries,
|
|
49
|
+
normalizedOptions.archiveLimits.maxEntries
|
|
50
|
+
)
|
|
51
|
+
const classified = ProjectLoader.#classify(
|
|
52
|
+
snapshot,
|
|
53
|
+
normalizedOptions.archiveLimits,
|
|
54
|
+
normalizedOptions.decodeAssets
|
|
55
|
+
)
|
|
56
|
+
ProjectLoader.#assertCandidates(classified)
|
|
57
|
+
|
|
58
|
+
const documents = []
|
|
59
|
+
const diagnostics = []
|
|
60
|
+
for (const entry of classified.candidates) {
|
|
61
|
+
ProjectLoader.#parseEntry(
|
|
62
|
+
entry,
|
|
63
|
+
normalizedOptions,
|
|
64
|
+
documents,
|
|
65
|
+
diagnostics
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
return ProjectLoader.#result(
|
|
69
|
+
classified,
|
|
70
|
+
documents,
|
|
71
|
+
diagnostics,
|
|
72
|
+
normalizedOptions
|
|
73
|
+
)
|
|
74
|
+
} catch (error) {
|
|
75
|
+
throw ProjectLoader.#errorFrom(error)
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Loads entries without throwing public project-loading failures.
|
|
81
|
+
* @param {Array<{ name: string, data: string | ArrayBuffer | Uint8Array }>} entries Named entries.
|
|
82
|
+
* @param {Record<string, any>} [options] Common project/parser options.
|
|
83
|
+
* @returns {{ ok: true, value: Record<string, any> } | { ok: false, error: ToolkitError, diagnostics: object[] }} Discriminated load result.
|
|
84
|
+
*/
|
|
85
|
+
static tryLoad(entries, options = {}) {
|
|
86
|
+
try {
|
|
87
|
+
return { ok: true, value: ProjectLoader.load(entries, options) }
|
|
88
|
+
} catch (error) {
|
|
89
|
+
const normalized = ProjectLoader.#errorFrom(error)
|
|
90
|
+
const diagnostics = Array.isArray(normalized.details?.diagnostics)
|
|
91
|
+
? normalized.details.diagnostics.map((diagnostic) =>
|
|
92
|
+
ToolkitDiagnostic.create(diagnostic)
|
|
93
|
+
)
|
|
94
|
+
: []
|
|
95
|
+
if (!diagnostics.length) {
|
|
96
|
+
diagnostics.push(
|
|
97
|
+
ToolkitDiagnostic.create({
|
|
98
|
+
code: normalized.code,
|
|
99
|
+
severity: 'error',
|
|
100
|
+
message: normalized.message,
|
|
101
|
+
source: normalized.source
|
|
102
|
+
})
|
|
103
|
+
)
|
|
104
|
+
}
|
|
105
|
+
return { ok: false, error: normalized, diagnostics }
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Loads entries incrementally while yielding and checking cancellation.
|
|
111
|
+
* @param {Array<{ name: string, data: string | ArrayBuffer | Uint8Array }>} entries Named entries.
|
|
112
|
+
* @param {Record<string, any>} [options] Common project/parser options.
|
|
113
|
+
* @returns {Promise<Record<string, any>>} Canonical project result.
|
|
114
|
+
*/
|
|
115
|
+
static async loadAsync(entries, options = {}) {
|
|
116
|
+
let normalizedOptions
|
|
117
|
+
let snapshot
|
|
118
|
+
let classified
|
|
119
|
+
const entriesOwned = AsyncInputOwnership.ownsProject(entries)
|
|
120
|
+
try {
|
|
121
|
+
normalizedOptions = ProjectLoader.#normalizeOptions(options)
|
|
122
|
+
ProjectLoader.#assertNotCancelled(normalizedOptions.signal)
|
|
123
|
+
snapshot = ProjectLoader.#snapshotEntries(
|
|
124
|
+
entries,
|
|
125
|
+
normalizedOptions.archiveLimits.maxEntries
|
|
126
|
+
)
|
|
127
|
+
classified = ProjectLoader.#classify(
|
|
128
|
+
snapshot,
|
|
129
|
+
normalizedOptions.archiveLimits,
|
|
130
|
+
normalizedOptions.decodeAssets
|
|
131
|
+
)
|
|
132
|
+
ProjectLoader.#assertCandidates(classified)
|
|
133
|
+
} catch (error) {
|
|
134
|
+
throw ProjectLoader.#errorFrom(error)
|
|
135
|
+
}
|
|
136
|
+
const useWorker =
|
|
137
|
+
normalizedOptions.worker === true ||
|
|
138
|
+
(normalizedOptions.worker === 'auto' &&
|
|
139
|
+
normalizedOptions.retainSource !== 'reference' &&
|
|
140
|
+
ParserWorkerClient.isDefaultAvailable())
|
|
141
|
+
if (useWorker) {
|
|
142
|
+
const attempt = await ParserWorkerClient.loadProjectDefault(
|
|
143
|
+
snapshot,
|
|
144
|
+
normalizedOptions
|
|
145
|
+
)
|
|
146
|
+
if (attempt.ok) return attempt.value
|
|
147
|
+
if (normalizedOptions.worker !== 'auto' || !attempt.unavailable) {
|
|
148
|
+
throw attempt.error
|
|
149
|
+
}
|
|
150
|
+
ParserWorkerClient.disposeDefault()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
if (!entriesOwned) {
|
|
155
|
+
ProjectAsyncInputOwner.own(
|
|
156
|
+
classified,
|
|
157
|
+
normalizedOptions.decodeAssets
|
|
158
|
+
)
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
throw ProjectLoader.#errorFrom(error)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
let progress = ProjectLoader.#progress(
|
|
165
|
+
normalizedOptions,
|
|
166
|
+
{ stage: 'detect', message: 'Classifying project entries.' },
|
|
167
|
+
null
|
|
168
|
+
)
|
|
169
|
+
await ProjectLoader.#yieldToHost(Boolean(normalizedOptions.signal))
|
|
170
|
+
ProjectLoader.#assertNotCancelled(normalizedOptions.signal)
|
|
171
|
+
|
|
172
|
+
progress = ProjectLoader.#progress(
|
|
173
|
+
normalizedOptions,
|
|
174
|
+
{
|
|
175
|
+
stage: 'project',
|
|
176
|
+
completed: 0,
|
|
177
|
+
total: classified.candidates.length,
|
|
178
|
+
message: 'Loading CircuitJSON project entries.'
|
|
179
|
+
},
|
|
180
|
+
progress
|
|
181
|
+
)
|
|
182
|
+
|
|
183
|
+
const documents = []
|
|
184
|
+
const diagnostics = []
|
|
185
|
+
for (let index = 0; index < classified.candidates.length; index += 1) {
|
|
186
|
+
await ProjectLoader.#yieldToHost(Boolean(normalizedOptions.signal))
|
|
187
|
+
ProjectLoader.#assertNotCancelled(normalizedOptions.signal)
|
|
188
|
+
ProjectLoader.#parseEntry(
|
|
189
|
+
classified.candidates[index],
|
|
190
|
+
normalizedOptions,
|
|
191
|
+
documents,
|
|
192
|
+
diagnostics
|
|
193
|
+
)
|
|
194
|
+
progress = ProjectLoader.#progress(
|
|
195
|
+
normalizedOptions,
|
|
196
|
+
{
|
|
197
|
+
stage: 'project',
|
|
198
|
+
completed: index + 1,
|
|
199
|
+
total: classified.candidates.length,
|
|
200
|
+
detail: classified.candidates[index].name,
|
|
201
|
+
message: 'Loaded CircuitJSON project entry.'
|
|
202
|
+
},
|
|
203
|
+
progress
|
|
204
|
+
)
|
|
205
|
+
ProjectLoader.#assertNotCancelled(normalizedOptions.signal)
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const result = ProjectLoader.#result(
|
|
209
|
+
classified,
|
|
210
|
+
documents,
|
|
211
|
+
diagnostics,
|
|
212
|
+
normalizedOptions
|
|
213
|
+
)
|
|
214
|
+
ProjectLoader.#progress(
|
|
215
|
+
normalizedOptions,
|
|
216
|
+
{
|
|
217
|
+
stage: 'complete',
|
|
218
|
+
completed: classified.candidates.length,
|
|
219
|
+
total: classified.candidates.length,
|
|
220
|
+
message: 'CircuitJSON project loading complete.'
|
|
221
|
+
},
|
|
222
|
+
progress
|
|
223
|
+
)
|
|
224
|
+
ProjectLoader.#assertNotCancelled(normalizedOptions.signal)
|
|
225
|
+
return result
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Performs bounded name/prefix classification without parsing every entry.
|
|
230
|
+
* @param {unknown} entries Named entry candidates.
|
|
231
|
+
* @returns {boolean} Whether a canonical CircuitJSON candidate is present.
|
|
232
|
+
*/
|
|
233
|
+
static supports(entries) {
|
|
234
|
+
try {
|
|
235
|
+
const entryDescriptors = ProjectLoader.#entryArray(entries)
|
|
236
|
+
const length = entryDescriptors.length.value
|
|
237
|
+
if (!length || length > ArchiveLimits.defaults.maxEntries) {
|
|
238
|
+
return false
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const names = []
|
|
242
|
+
let supported = false
|
|
243
|
+
for (let index = 0; index < length; index += 1) {
|
|
244
|
+
const entry = entryDescriptors[String(index)].value
|
|
245
|
+
const fields = ProjectLoader.#entryFields(entry)
|
|
246
|
+
const name = ArchiveEntryPath.normalize(fields.name)
|
|
247
|
+
names.push(name)
|
|
248
|
+
if (
|
|
249
|
+
name.toLowerCase().endsWith('.json') &&
|
|
250
|
+
Parser.supports({ fileName: name, data: fields.data })
|
|
251
|
+
) {
|
|
252
|
+
supported = true
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
ArchiveEntryPath.unique(names)
|
|
256
|
+
return supported
|
|
257
|
+
} catch {
|
|
258
|
+
return false
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Normalizes project and parser options without decoding an entry.
|
|
264
|
+
* @param {unknown} options Caller options.
|
|
265
|
+
* @returns {Record<string, any>} Normalized options.
|
|
266
|
+
*/
|
|
267
|
+
static #normalizeOptions(options) {
|
|
268
|
+
try {
|
|
269
|
+
const descriptors = ProjectLoader.#plainDescriptors(
|
|
270
|
+
options,
|
|
271
|
+
'Project loader options must be a plain object.'
|
|
272
|
+
)
|
|
273
|
+
const parserOptions = {}
|
|
274
|
+
for (const key of PARSER_OPTION_KEYS) {
|
|
275
|
+
if (descriptors[key]) {
|
|
276
|
+
parserOptions[key] = descriptors[key].value
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
const normalized = ParserOptions.normalize(
|
|
280
|
+
{ fileName: '', data: '[]' },
|
|
281
|
+
parserOptions
|
|
282
|
+
).options
|
|
283
|
+
if (normalized.signal !== undefined && normalized.signal !== null) {
|
|
284
|
+
ProjectLoader.#signalState(normalized.signal)
|
|
285
|
+
}
|
|
286
|
+
const archiveLimits = ArchiveLimits.normalize(
|
|
287
|
+
descriptors.archiveLimits
|
|
288
|
+
? descriptors.archiveLimits.value
|
|
289
|
+
: undefined
|
|
290
|
+
)
|
|
291
|
+
return { ...normalized, archiveLimits }
|
|
292
|
+
} catch (error) {
|
|
293
|
+
throw ProjectLoader.#inputError(error)
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Validates, measures, and classifies all entries before parsing.
|
|
299
|
+
* @param {unknown} entries Named entry candidates.
|
|
300
|
+
* @param {Record<string, number>} limits Normalized safety limits.
|
|
301
|
+
* @param {'none' | 'metadata' | 'full'} assetMode Asset decode mode.
|
|
302
|
+
* @returns {{ entries: object[], candidates: object[], entryNames: string[], totalBytes: number }} Classified entries.
|
|
303
|
+
*/
|
|
304
|
+
static #classify(entries, limits, assetMode) {
|
|
305
|
+
const entryDescriptors = ProjectLoader.#entryArray(entries)
|
|
306
|
+
const entryCount = entryDescriptors.length.value
|
|
307
|
+
if (!entryCount) {
|
|
308
|
+
throw ProjectLoader.#inputError(
|
|
309
|
+
new TypeError('Project entries must be a non-empty array.')
|
|
310
|
+
)
|
|
311
|
+
}
|
|
312
|
+
ProjectLoader.#assertLimit('maxEntries', limits.maxEntries, entryCount)
|
|
313
|
+
|
|
314
|
+
const prepared = []
|
|
315
|
+
let totalBytes = 0
|
|
316
|
+
for (let index = 0; index < entryCount; index += 1) {
|
|
317
|
+
const entry = entryDescriptors[String(index)].value
|
|
318
|
+
const fields = ProjectLoader.#entryFields(entry)
|
|
319
|
+
const name = ArchiveEntryPath.normalize(fields.name)
|
|
320
|
+
const byteLength = ProjectLoader.#byteLength(fields.data)
|
|
321
|
+
let entryBytes = byteLength
|
|
322
|
+
ProjectLoader.#assertLimit(
|
|
323
|
+
'maxEntryBytes',
|
|
324
|
+
limits.maxEntryBytes,
|
|
325
|
+
entryBytes,
|
|
326
|
+
name
|
|
327
|
+
)
|
|
328
|
+
totalBytes += byteLength
|
|
329
|
+
ProjectLoader.#assertLimit(
|
|
330
|
+
'maxTotalBytes',
|
|
331
|
+
limits.maxTotalBytes,
|
|
332
|
+
totalBytes
|
|
333
|
+
)
|
|
334
|
+
|
|
335
|
+
const archiveDepth = ProjectLoader.#metadataInteger(
|
|
336
|
+
fields.archiveDepth,
|
|
337
|
+
'archiveDepth',
|
|
338
|
+
0
|
|
339
|
+
)
|
|
340
|
+
ProjectLoader.#assertLimit(
|
|
341
|
+
'maxArchiveDepth',
|
|
342
|
+
limits.maxArchiveDepth,
|
|
343
|
+
archiveDepth,
|
|
344
|
+
name
|
|
345
|
+
)
|
|
346
|
+
ProjectLoader.#assertCompressionRatio(
|
|
347
|
+
byteLength,
|
|
348
|
+
fields.compressedByteLength,
|
|
349
|
+
limits.maxCompressionRatio,
|
|
350
|
+
name
|
|
351
|
+
)
|
|
352
|
+
|
|
353
|
+
const input = { fileName: name, data: fields.data }
|
|
354
|
+
if (fields.assets !== undefined) {
|
|
355
|
+
try {
|
|
356
|
+
input.assets = ToolkitAsset.prepareAll(fields.assets, {
|
|
357
|
+
mode: assetMode,
|
|
358
|
+
acceptPayload: (assetBytes) => {
|
|
359
|
+
entryBytes += assetBytes
|
|
360
|
+
ProjectLoader.#assertLimit(
|
|
361
|
+
'maxEntryBytes',
|
|
362
|
+
limits.maxEntryBytes,
|
|
363
|
+
entryBytes,
|
|
364
|
+
name
|
|
365
|
+
)
|
|
366
|
+
totalBytes += assetBytes
|
|
367
|
+
ProjectLoader.#assertLimit(
|
|
368
|
+
'maxTotalBytes',
|
|
369
|
+
limits.maxTotalBytes,
|
|
370
|
+
totalBytes,
|
|
371
|
+
name
|
|
372
|
+
)
|
|
373
|
+
}
|
|
374
|
+
})
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error instanceof ToolkitError) throw error
|
|
377
|
+
throw ProjectLoader.#inputError(error)
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
prepared.push({ name, byteLength, input })
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
const entryNames = ArchiveEntryPath.unique(
|
|
384
|
+
prepared.map((entry) => entry.name)
|
|
385
|
+
)
|
|
386
|
+
const candidates = prepared
|
|
387
|
+
.filter((entry) => entry.name.toLowerCase().endsWith('.json'))
|
|
388
|
+
.sort((left, right) =>
|
|
389
|
+
left.name < right.name ? -1 : left.name > right.name ? 1 : 0
|
|
390
|
+
)
|
|
391
|
+
return { entries: prepared, candidates, entryNames, totalBytes }
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Captures only public project fields before any callback, yield, or worker
|
|
396
|
+
* boundary. This isolates classification from later caller mutation and
|
|
397
|
+
* gives direct and worker paths the same descriptor policy.
|
|
398
|
+
* @param {unknown} entries Named entry candidates.
|
|
399
|
+
* @param {number} maximumEntries Configured project-entry ceiling.
|
|
400
|
+
* @returns {object[]} Dense stable request snapshot.
|
|
401
|
+
*/
|
|
402
|
+
static #snapshotEntries(entries, maximumEntries) {
|
|
403
|
+
const descriptors = ProjectLoader.#entryArray(entries, maximumEntries)
|
|
404
|
+
const length = descriptors.length.value
|
|
405
|
+
let attachedValues = 0
|
|
406
|
+
const snapshot = new Array(length)
|
|
407
|
+
for (let index = 0; index < length; index += 1) {
|
|
408
|
+
const fields = ProjectLoader.#entryFields(
|
|
409
|
+
descriptors[String(index)].value
|
|
410
|
+
)
|
|
411
|
+
const captured = { name: fields.name, data: fields.data }
|
|
412
|
+
if (fields.assets !== undefined) {
|
|
413
|
+
attachedValues = AttachedValueLimits.add(
|
|
414
|
+
fields.assets,
|
|
415
|
+
attachedValues
|
|
416
|
+
)
|
|
417
|
+
captured.assets = ProjectLoader.#attachedValueSnapshot(
|
|
418
|
+
fields.assets
|
|
419
|
+
)
|
|
420
|
+
}
|
|
421
|
+
if (fields.compressedByteLength !== undefined) {
|
|
422
|
+
captured.compressedByteLength = fields.compressedByteLength
|
|
423
|
+
}
|
|
424
|
+
if (fields.archiveDepth !== undefined) {
|
|
425
|
+
captured.archiveDepth = fields.archiveDepth
|
|
426
|
+
}
|
|
427
|
+
snapshot[index] = captured
|
|
428
|
+
}
|
|
429
|
+
return snapshot
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
/**
|
|
433
|
+
* Captures one dense attached-value array without reading item properties.
|
|
434
|
+
* @param {unknown} values Attached-value array candidate.
|
|
435
|
+
* @returns {unknown[]} Stable array of attached value identities.
|
|
436
|
+
*/
|
|
437
|
+
static #attachedValueSnapshot(values) {
|
|
438
|
+
let prototype
|
|
439
|
+
let descriptors
|
|
440
|
+
try {
|
|
441
|
+
prototype = Object.getPrototypeOf(values)
|
|
442
|
+
descriptors = Object.getOwnPropertyDescriptors(values)
|
|
443
|
+
} catch {
|
|
444
|
+
throw ProjectLoader.#inputError(
|
|
445
|
+
new TypeError(
|
|
446
|
+
'Project entry assets must be a dense plain array.'
|
|
447
|
+
)
|
|
448
|
+
)
|
|
449
|
+
}
|
|
450
|
+
const length = descriptors.length?.value
|
|
451
|
+
if (
|
|
452
|
+
prototype !== Array.prototype ||
|
|
453
|
+
!Number.isSafeInteger(length) ||
|
|
454
|
+
length < 0 ||
|
|
455
|
+
Reflect.ownKeys(descriptors).length !== length + 1
|
|
456
|
+
) {
|
|
457
|
+
throw ProjectLoader.#inputError(
|
|
458
|
+
new TypeError(
|
|
459
|
+
'Project entry assets must be a dense plain array.'
|
|
460
|
+
)
|
|
461
|
+
)
|
|
462
|
+
}
|
|
463
|
+
const snapshot = new Array(length)
|
|
464
|
+
for (let index = 0; index < length; index += 1) {
|
|
465
|
+
const descriptor = descriptors[String(index)]
|
|
466
|
+
if (
|
|
467
|
+
!descriptor ||
|
|
468
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
469
|
+
descriptor.enumerable !== true
|
|
470
|
+
) {
|
|
471
|
+
throw ProjectLoader.#inputError(
|
|
472
|
+
new TypeError(
|
|
473
|
+
'Project entry assets must contain enumerable data properties.'
|
|
474
|
+
)
|
|
475
|
+
)
|
|
476
|
+
}
|
|
477
|
+
snapshot[index] = descriptor.value
|
|
478
|
+
}
|
|
479
|
+
return snapshot
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Reads one entry through own data properties only.
|
|
484
|
+
* @param {unknown} entry Entry candidate.
|
|
485
|
+
* @returns {{ name: unknown, data: unknown, assets?: unknown, compressedByteLength?: unknown, archiveDepth?: unknown }} Entry fields.
|
|
486
|
+
*/
|
|
487
|
+
static #entryFields(entry) {
|
|
488
|
+
const descriptors = ProjectLoader.#plainDescriptors(
|
|
489
|
+
entry,
|
|
490
|
+
'Each project entry must be a plain object.'
|
|
491
|
+
)
|
|
492
|
+
if (!descriptors.name || !descriptors.data) {
|
|
493
|
+
throw ProjectLoader.#inputError(
|
|
494
|
+
new TypeError('Each project entry requires name and data.')
|
|
495
|
+
)
|
|
496
|
+
}
|
|
497
|
+
if (descriptors.assets && !Array.isArray(descriptors.assets.value)) {
|
|
498
|
+
throw ProjectLoader.#inputError(
|
|
499
|
+
new TypeError('Project entry assets must be an array.')
|
|
500
|
+
)
|
|
501
|
+
}
|
|
502
|
+
return {
|
|
503
|
+
name: descriptors.name.value,
|
|
504
|
+
data: descriptors.data.value,
|
|
505
|
+
assets: descriptors.assets?.value,
|
|
506
|
+
compressedByteLength: descriptors.compressedByteLength?.value,
|
|
507
|
+
archiveDepth: descriptors.archiveDepth?.value
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Reads an exact dense project-entry array without caller iteration.
|
|
513
|
+
* @param {unknown} entries Entry array candidate.
|
|
514
|
+
* @param {number} [maximum] Maximum permitted entries.
|
|
515
|
+
* @returns {Record<string, PropertyDescriptor>} Array descriptors.
|
|
516
|
+
*/
|
|
517
|
+
static #entryArray(entries, maximum = ArchiveLimits.defaults.maxEntries) {
|
|
518
|
+
if (!Array.isArray(entries)) {
|
|
519
|
+
throw ProjectLoader.#inputError(
|
|
520
|
+
new TypeError('Project entries must be a non-empty array.')
|
|
521
|
+
)
|
|
522
|
+
}
|
|
523
|
+
let prototype
|
|
524
|
+
let lengthDescriptor
|
|
525
|
+
let descriptors
|
|
526
|
+
try {
|
|
527
|
+
prototype = Object.getPrototypeOf(entries)
|
|
528
|
+
lengthDescriptor = Object.getOwnPropertyDescriptor(
|
|
529
|
+
entries,
|
|
530
|
+
'length'
|
|
531
|
+
)
|
|
532
|
+
} catch {
|
|
533
|
+
throw ProjectLoader.#inputError(
|
|
534
|
+
new TypeError('Project entries must be a dense plain array.')
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
const length = lengthDescriptor?.value
|
|
538
|
+
if (
|
|
539
|
+
prototype !== Array.prototype ||
|
|
540
|
+
!lengthDescriptor ||
|
|
541
|
+
!Object.hasOwn(lengthDescriptor, 'value') ||
|
|
542
|
+
!Number.isSafeInteger(length) ||
|
|
543
|
+
length < 0
|
|
544
|
+
) {
|
|
545
|
+
throw ProjectLoader.#inputError(
|
|
546
|
+
new TypeError('Project entries must be a dense plain array.')
|
|
547
|
+
)
|
|
548
|
+
}
|
|
549
|
+
ProjectLoader.#assertLimit('maxEntries', maximum, length)
|
|
550
|
+
try {
|
|
551
|
+
descriptors = Object.getOwnPropertyDescriptors(entries)
|
|
552
|
+
} catch {
|
|
553
|
+
throw ProjectLoader.#inputError(
|
|
554
|
+
new TypeError('Project entries must be a dense plain array.')
|
|
555
|
+
)
|
|
556
|
+
}
|
|
557
|
+
if (Reflect.ownKeys(descriptors).length !== length + 1) {
|
|
558
|
+
throw ProjectLoader.#inputError(
|
|
559
|
+
new TypeError('Project entries must be a dense plain array.')
|
|
560
|
+
)
|
|
561
|
+
}
|
|
562
|
+
for (let index = 0; index < length; index += 1) {
|
|
563
|
+
const descriptor = descriptors[String(index)]
|
|
564
|
+
if (
|
|
565
|
+
!descriptor ||
|
|
566
|
+
!Object.hasOwn(descriptor, 'value') ||
|
|
567
|
+
descriptor.enumerable !== true
|
|
568
|
+
) {
|
|
569
|
+
throw ProjectLoader.#inputError(
|
|
570
|
+
new TypeError(
|
|
571
|
+
'Project entries must contain enumerable data properties.'
|
|
572
|
+
)
|
|
573
|
+
)
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
return descriptors
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
/**
|
|
580
|
+
* Returns own data-property descriptors for one plain record.
|
|
581
|
+
* @param {unknown} value Record candidate.
|
|
582
|
+
* @param {string} message Error message.
|
|
583
|
+
* @returns {Record<string, PropertyDescriptor>} Own descriptors.
|
|
584
|
+
*/
|
|
585
|
+
static #plainDescriptors(value, message) {
|
|
586
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
587
|
+
throw ProjectLoader.#inputError(new TypeError(message))
|
|
588
|
+
}
|
|
589
|
+
const prototype = Object.getPrototypeOf(value)
|
|
590
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
591
|
+
throw ProjectLoader.#inputError(new TypeError(message))
|
|
592
|
+
}
|
|
593
|
+
const descriptors = Object.getOwnPropertyDescriptors(value)
|
|
594
|
+
for (const descriptor of Object.values(descriptors)) {
|
|
595
|
+
if (descriptor.get || descriptor.set) {
|
|
596
|
+
throw ProjectLoader.#inputError(
|
|
597
|
+
new TypeError('Accessor-backed project fields are invalid.')
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
return descriptors
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
/**
|
|
605
|
+
* Measures a canonical parser payload without copying it.
|
|
606
|
+
* @param {unknown} data Entry payload.
|
|
607
|
+
* @returns {number} Payload byte length.
|
|
608
|
+
*/
|
|
609
|
+
static #byteLength(data) {
|
|
610
|
+
if (typeof data === 'string') {
|
|
611
|
+
return ProjectLoader.#stringByteLength(data)
|
|
612
|
+
}
|
|
613
|
+
if (data instanceof ArrayBuffer) return data.byteLength
|
|
614
|
+
if (data instanceof Uint8Array) return data.byteLength
|
|
615
|
+
throw ProjectLoader.#inputError(
|
|
616
|
+
new TypeError(
|
|
617
|
+
'Project entry data must be a string, ArrayBuffer, or Uint8Array.'
|
|
618
|
+
)
|
|
619
|
+
)
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
/**
|
|
623
|
+
* Measures UTF-8 text without allocating a duplicate encoded payload.
|
|
624
|
+
* @param {string} value Text payload.
|
|
625
|
+
* @returns {number} UTF-8 byte length.
|
|
626
|
+
*/
|
|
627
|
+
static #stringByteLength(value) {
|
|
628
|
+
let byteLength = 0
|
|
629
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
630
|
+
const codeUnit = value.charCodeAt(index)
|
|
631
|
+
if (codeUnit <= 0x7f) {
|
|
632
|
+
byteLength += 1
|
|
633
|
+
} else if (codeUnit <= 0x7ff) {
|
|
634
|
+
byteLength += 2
|
|
635
|
+
} else if (
|
|
636
|
+
codeUnit >= 0xd800 &&
|
|
637
|
+
codeUnit <= 0xdbff &&
|
|
638
|
+
index + 1 < value.length &&
|
|
639
|
+
value.charCodeAt(index + 1) >= 0xdc00 &&
|
|
640
|
+
value.charCodeAt(index + 1) <= 0xdfff
|
|
641
|
+
) {
|
|
642
|
+
byteLength += 4
|
|
643
|
+
index += 1
|
|
644
|
+
} else {
|
|
645
|
+
byteLength += 3
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
return byteLength
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/**
|
|
652
|
+
* Normalizes optional non-negative archive metadata.
|
|
653
|
+
* @param {unknown} value Metadata value.
|
|
654
|
+
* @param {string} key Metadata key.
|
|
655
|
+
* @param {number} fallback Missing-value fallback.
|
|
656
|
+
* @returns {number} Normalized integer.
|
|
657
|
+
*/
|
|
658
|
+
static #metadataInteger(value, key, fallback) {
|
|
659
|
+
if (value === undefined) return fallback
|
|
660
|
+
if (!Number.isSafeInteger(value) || value < 0) {
|
|
661
|
+
throw ProjectLoader.#inputError(
|
|
662
|
+
new TypeError(`${key} must be a non-negative safe integer.`)
|
|
663
|
+
)
|
|
664
|
+
}
|
|
665
|
+
return value
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* Enforces optional compressed-size metadata before parsing.
|
|
670
|
+
* @param {number} byteLength Uncompressed payload bytes.
|
|
671
|
+
* @param {unknown} compressedValue Optional compressed byte length.
|
|
672
|
+
* @param {number} maximum Maximum allowed ratio.
|
|
673
|
+
* @param {string} entryName Entry name.
|
|
674
|
+
* @returns {void}
|
|
675
|
+
*/
|
|
676
|
+
static #assertCompressionRatio(
|
|
677
|
+
byteLength,
|
|
678
|
+
compressedValue,
|
|
679
|
+
maximum,
|
|
680
|
+
entryName
|
|
681
|
+
) {
|
|
682
|
+
if (compressedValue === undefined) return
|
|
683
|
+
const compressed = ProjectLoader.#metadataInteger(
|
|
684
|
+
compressedValue,
|
|
685
|
+
'compressedByteLength',
|
|
686
|
+
0
|
|
687
|
+
)
|
|
688
|
+
const ratio =
|
|
689
|
+
compressed === 0
|
|
690
|
+
? byteLength === 0
|
|
691
|
+
? 1
|
|
692
|
+
: Number.POSITIVE_INFINITY
|
|
693
|
+
: byteLength / compressed
|
|
694
|
+
ProjectLoader.#assertLimit(
|
|
695
|
+
'maxCompressionRatio',
|
|
696
|
+
maximum,
|
|
697
|
+
ratio,
|
|
698
|
+
entryName
|
|
699
|
+
)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
/**
|
|
703
|
+
* Throws when one measured value exceeds its normalized limit.
|
|
704
|
+
* @param {string} limit Limit name.
|
|
705
|
+
* @param {number} maximum Maximum value.
|
|
706
|
+
* @param {number} actual Measured value.
|
|
707
|
+
* @param {string} [entryName] Associated entry name.
|
|
708
|
+
* @returns {void}
|
|
709
|
+
*/
|
|
710
|
+
static #assertLimit(limit, maximum, actual, entryName = '') {
|
|
711
|
+
if (actual <= maximum) return
|
|
712
|
+
throw new ToolkitError(`Archive limit exceeded: ${limit}.`, {
|
|
713
|
+
code: 'ERR_ARCHIVE_LIMIT_EXCEEDED',
|
|
714
|
+
category: 'validation',
|
|
715
|
+
format: 'archive',
|
|
716
|
+
source: entryName,
|
|
717
|
+
details: { limit, maximum, actual, entryName }
|
|
718
|
+
})
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Rejects projects without any source-format candidate.
|
|
723
|
+
* @param {{ candidates: object[] }} classified Classified entries.
|
|
724
|
+
* @returns {void}
|
|
725
|
+
*/
|
|
726
|
+
static #assertCandidates(classified) {
|
|
727
|
+
if (classified.candidates.length) return
|
|
728
|
+
throw new ToolkitError(
|
|
729
|
+
'No supported CircuitJSON project entry was found.',
|
|
730
|
+
{
|
|
731
|
+
code: 'ERR_PROJECT_UNSUPPORTED',
|
|
732
|
+
category: 'unsupported',
|
|
733
|
+
format: 'circuitjson'
|
|
734
|
+
}
|
|
735
|
+
)
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/**
|
|
739
|
+
* Parses one candidate and records a project-level failure diagnostic.
|
|
740
|
+
* @param {{ name: string, input: object }} entry Prepared entry.
|
|
741
|
+
* @param {Record<string, any>} options Normalized options.
|
|
742
|
+
* @param {object[]} documents Successful documents.
|
|
743
|
+
* @param {object[]} diagnostics Project diagnostics.
|
|
744
|
+
* @returns {void}
|
|
745
|
+
*/
|
|
746
|
+
static #parseEntry(entry, options, documents, diagnostics) {
|
|
747
|
+
try {
|
|
748
|
+
documents.push(
|
|
749
|
+
Parser.parse(entry.input, ProjectLoader.#parserOptions(options))
|
|
750
|
+
)
|
|
751
|
+
} catch (error) {
|
|
752
|
+
const normalized = ProjectLoader.#errorFrom(error)
|
|
753
|
+
if (normalized.code === 'ERR_CAPABILITY_UNAVAILABLE') {
|
|
754
|
+
throw normalized
|
|
755
|
+
}
|
|
756
|
+
diagnostics.push(
|
|
757
|
+
ToolkitDiagnostic.create({
|
|
758
|
+
code: normalized.code,
|
|
759
|
+
severity: 'error',
|
|
760
|
+
message: normalized.message,
|
|
761
|
+
source: entry.name,
|
|
762
|
+
location: normalized.location,
|
|
763
|
+
details: {
|
|
764
|
+
category: normalized.category,
|
|
765
|
+
format: normalized.format,
|
|
766
|
+
cause: normalized.cause
|
|
767
|
+
}
|
|
768
|
+
})
|
|
769
|
+
)
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
|
|
773
|
+
/**
|
|
774
|
+
* Selects normalized options owned by the standalone parser.
|
|
775
|
+
* @param {Record<string, any>} options Normalized project options.
|
|
776
|
+
* @returns {Record<string, any>} Parser options.
|
|
777
|
+
*/
|
|
778
|
+
static #parserOptions(options) {
|
|
779
|
+
const selected = {}
|
|
780
|
+
for (const key of PARSER_OPTION_KEYS) {
|
|
781
|
+
if (key === 'onProgress' || key === 'signal') continue
|
|
782
|
+
selected[key] = options[key]
|
|
783
|
+
}
|
|
784
|
+
return selected
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Builds the canonical result or throws the typed zero-success failure.
|
|
789
|
+
* @param {{ entries: object[], candidates: object[], entryNames: string[], totalBytes: number }} classified Classified entries.
|
|
790
|
+
* @param {object[]} documents Successful documents.
|
|
791
|
+
* @param {object[]} diagnostics Project diagnostics.
|
|
792
|
+
* @param {Record<string, any>} options Normalized options.
|
|
793
|
+
* @returns {Record<string, any>} Canonical project result.
|
|
794
|
+
*/
|
|
795
|
+
static #result(classified, documents, diagnostics, options) {
|
|
796
|
+
if (!documents.length) {
|
|
797
|
+
throw new ToolkitError(
|
|
798
|
+
'No requested CircuitJSON project document could be loaded.',
|
|
799
|
+
{
|
|
800
|
+
code: 'ERR_PROJECT_NO_DOCUMENTS',
|
|
801
|
+
category: 'parse',
|
|
802
|
+
format: 'circuitjson',
|
|
803
|
+
details: { diagnostics }
|
|
804
|
+
}
|
|
805
|
+
)
|
|
806
|
+
}
|
|
807
|
+
return ProjectResult.create({
|
|
808
|
+
source: {
|
|
809
|
+
format: 'circuitjson',
|
|
810
|
+
entryNames: classified.entryNames
|
|
811
|
+
},
|
|
812
|
+
documents,
|
|
813
|
+
project: null,
|
|
814
|
+
extensions: {},
|
|
815
|
+
assets: ProjectLoader.#companionAssets(
|
|
816
|
+
classified.entries,
|
|
817
|
+
options.decodeAssets
|
|
818
|
+
),
|
|
819
|
+
diagnostics,
|
|
820
|
+
statistics: {
|
|
821
|
+
entryCount: classified.entries.length,
|
|
822
|
+
candidateCount: classified.candidates.length,
|
|
823
|
+
documentCount: documents.length,
|
|
824
|
+
failureCount: diagnostics.length,
|
|
825
|
+
totalBytes: classified.totalBytes
|
|
826
|
+
}
|
|
827
|
+
})
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Selects non-document entries through the common asset decode modes.
|
|
832
|
+
* @param {Array<{ name: string, byteLength: number, input: { data: unknown } }>} entries Prepared entries.
|
|
833
|
+
* @param {'none' | 'metadata' | 'full'} mode Asset selection mode.
|
|
834
|
+
* @returns {object[]} Project companion assets.
|
|
835
|
+
*/
|
|
836
|
+
static #companionAssets(entries, mode) {
|
|
837
|
+
if (mode === 'none') return []
|
|
838
|
+
return entries
|
|
839
|
+
.filter((entry) => !entry.name.toLowerCase().endsWith('.json'))
|
|
840
|
+
.map(
|
|
841
|
+
(entry) =>
|
|
842
|
+
entry.companionAsset ||
|
|
843
|
+
ToolkitAsset.prepare(
|
|
844
|
+
{
|
|
845
|
+
kind: 'companion',
|
|
846
|
+
name: entry.name,
|
|
847
|
+
mediaType: 'application/octet-stream',
|
|
848
|
+
byteLength: entry.byteLength,
|
|
849
|
+
data: entry.input.data,
|
|
850
|
+
source: { entryName: entry.name }
|
|
851
|
+
},
|
|
852
|
+
{ mode }
|
|
853
|
+
)
|
|
854
|
+
)
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
/**
|
|
858
|
+
* Emits one clone-safe progress row without swallowing host callback errors.
|
|
859
|
+
* @param {Record<string, any>} options Normalized options.
|
|
860
|
+
* @param {Record<string, any>} fields Progress fields.
|
|
861
|
+
* @param {Record<string, any> | null} previous Previous row.
|
|
862
|
+
* @returns {Record<string, any> | null} Current or previous row.
|
|
863
|
+
*/
|
|
864
|
+
static #progress(options, fields, previous) {
|
|
865
|
+
if (!options.onProgress) return previous
|
|
866
|
+
const row = ToolkitProgress.create(fields, previous)
|
|
867
|
+
options.onProgress(row)
|
|
868
|
+
return row
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* Yields to a real host task so timer, I/O, and UI cancellation can run.
|
|
873
|
+
* @param {boolean} cancellationResponsive Whether timer-backed aborts must run before the next entry.
|
|
874
|
+
* @returns {Promise<void>} Yield completion.
|
|
875
|
+
*/
|
|
876
|
+
static async #yieldToHost(cancellationResponsive) {
|
|
877
|
+
if (cancellationResponsive) {
|
|
878
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
879
|
+
return
|
|
880
|
+
}
|
|
881
|
+
if (typeof globalThis.scheduler?.yield === 'function') {
|
|
882
|
+
await globalThis.scheduler.yield()
|
|
883
|
+
return
|
|
884
|
+
}
|
|
885
|
+
if (typeof setImmediate === 'function') {
|
|
886
|
+
await new Promise((resolve) => setImmediate(resolve))
|
|
887
|
+
return
|
|
888
|
+
}
|
|
889
|
+
if (typeof globalThis.MessageChannel === 'function') {
|
|
890
|
+
await new Promise((resolve) => {
|
|
891
|
+
const channel = new globalThis.MessageChannel()
|
|
892
|
+
channel.port1.onmessage = () => {
|
|
893
|
+
channel.port1.close()
|
|
894
|
+
channel.port2.close()
|
|
895
|
+
resolve()
|
|
896
|
+
}
|
|
897
|
+
channel.port2.postMessage(null)
|
|
898
|
+
})
|
|
899
|
+
return
|
|
900
|
+
}
|
|
901
|
+
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* Throws the shared cancellation error for an aborted signal.
|
|
906
|
+
* @param {unknown} signal Abort signal candidate.
|
|
907
|
+
* @returns {void}
|
|
908
|
+
*/
|
|
909
|
+
static #assertNotCancelled(signal) {
|
|
910
|
+
if (signal === undefined || signal === null) return
|
|
911
|
+
if (!ProjectLoader.#signalState(signal)) return
|
|
912
|
+
throw new ToolkitError('CircuitJSON project loading was cancelled.', {
|
|
913
|
+
code: 'ERR_CANCELLED',
|
|
914
|
+
category: 'cancelled',
|
|
915
|
+
format: 'circuitjson'
|
|
916
|
+
})
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/**
|
|
920
|
+
* Reads only a genuine AbortSignal through the captured platform getter.
|
|
921
|
+
* @param {unknown} signal Signal candidate.
|
|
922
|
+
* @returns {boolean} Aborted state.
|
|
923
|
+
*/
|
|
924
|
+
static #signalState(signal) {
|
|
925
|
+
if (!ABORTED_GETTER) {
|
|
926
|
+
throw new TypeError('AbortSignal state is unavailable.')
|
|
927
|
+
}
|
|
928
|
+
try {
|
|
929
|
+
return Boolean(Reflect.apply(ABORTED_GETTER, signal, []))
|
|
930
|
+
} catch {
|
|
931
|
+
throw new TypeError('Project signal must be an AbortSignal.')
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/**
|
|
936
|
+
* Converts option/input failures into the project validation boundary.
|
|
937
|
+
* @param {unknown} error Failure candidate.
|
|
938
|
+
* @returns {ToolkitError} Typed input error.
|
|
939
|
+
*/
|
|
940
|
+
static #inputError(error) {
|
|
941
|
+
return ToolkitError.from(error, {
|
|
942
|
+
code: 'ERR_PROJECT_INPUT',
|
|
943
|
+
category: 'validation',
|
|
944
|
+
format: 'circuitjson'
|
|
945
|
+
})
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
/**
|
|
949
|
+
* Converts unexpected public failures into clone-safe toolkit errors.
|
|
950
|
+
* @param {unknown} error Failure candidate.
|
|
951
|
+
* @returns {ToolkitError} Typed project error.
|
|
952
|
+
*/
|
|
953
|
+
static #errorFrom(error) {
|
|
954
|
+
return ToolkitError.from(error, {
|
|
955
|
+
code: 'ERR_PROJECT_LOAD',
|
|
956
|
+
category: 'runtime',
|
|
957
|
+
format: 'circuitjson'
|
|
958
|
+
})
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/**
|
|
962
|
+
* Creates the synchronous worker-mode boundary error.
|
|
963
|
+
* @returns {ToolkitError} Typed unsupported error.
|
|
964
|
+
*/
|
|
965
|
+
static #workerSyncError() {
|
|
966
|
+
return new ToolkitError(
|
|
967
|
+
'Synchronous CircuitJSON project loading cannot use a worker.',
|
|
968
|
+
{
|
|
969
|
+
code: 'ERR_WORKER_SYNC_UNAVAILABLE',
|
|
970
|
+
category: 'unsupported',
|
|
971
|
+
format: 'circuitjson'
|
|
972
|
+
}
|
|
973
|
+
)
|
|
974
|
+
}
|
|
975
|
+
}
|