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,905 @@
|
|
|
1
|
+
import { ToolkitError } from '../contracts/ToolkitError.mjs'
|
|
2
|
+
import { ToolkitProgress } from '../contracts/ToolkitProgress.mjs'
|
|
3
|
+
import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
4
|
+
import { CircuitJsonValidationProof } from '../context/CircuitJsonValidationProof.mjs'
|
|
5
|
+
import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
|
|
6
|
+
import { WorkerRequestData } from './WorkerRequestData.mjs'
|
|
7
|
+
|
|
8
|
+
const ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
9
|
+
ArrayBuffer.prototype,
|
|
10
|
+
'byteLength'
|
|
11
|
+
)?.get
|
|
12
|
+
const TYPED_ARRAY_PROTOTYPE = Object.getPrototypeOf(Uint8Array.prototype)
|
|
13
|
+
const TYPED_ARRAY_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
14
|
+
TYPED_ARRAY_PROTOTYPE,
|
|
15
|
+
'byteLength'
|
|
16
|
+
)?.get
|
|
17
|
+
const DATA_VIEW_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
18
|
+
DataView.prototype,
|
|
19
|
+
'byteLength'
|
|
20
|
+
)?.get
|
|
21
|
+
|
|
22
|
+
const ERROR_CATEGORIES = new Set([
|
|
23
|
+
'parse',
|
|
24
|
+
'validation',
|
|
25
|
+
'unsupported',
|
|
26
|
+
'cancelled',
|
|
27
|
+
'runtime'
|
|
28
|
+
])
|
|
29
|
+
const ERROR_FIELDS = [
|
|
30
|
+
'name',
|
|
31
|
+
'message',
|
|
32
|
+
'code',
|
|
33
|
+
'category',
|
|
34
|
+
'format',
|
|
35
|
+
'source',
|
|
36
|
+
'location',
|
|
37
|
+
'details',
|
|
38
|
+
'cause'
|
|
39
|
+
]
|
|
40
|
+
const ERROR_CAUSE_FIELDS = ['name', 'message', 'code']
|
|
41
|
+
const ASSET_FIELDS = [
|
|
42
|
+
'id',
|
|
43
|
+
'kind',
|
|
44
|
+
'name',
|
|
45
|
+
'mediaType',
|
|
46
|
+
'byteLength',
|
|
47
|
+
'data',
|
|
48
|
+
'source'
|
|
49
|
+
]
|
|
50
|
+
const DIAGNOSTIC_FIELDS = [
|
|
51
|
+
'code',
|
|
52
|
+
'severity',
|
|
53
|
+
'message',
|
|
54
|
+
'source',
|
|
55
|
+
'location',
|
|
56
|
+
'details'
|
|
57
|
+
]
|
|
58
|
+
const DIAGNOSTIC_SEVERITIES = new Set(['info', 'warning', 'error'])
|
|
59
|
+
const DOCUMENT_RESULT_FIELDS = [
|
|
60
|
+
'schema',
|
|
61
|
+
'id',
|
|
62
|
+
'modelSchema',
|
|
63
|
+
'model',
|
|
64
|
+
'source',
|
|
65
|
+
'extensions',
|
|
66
|
+
'assets',
|
|
67
|
+
'diagnostics',
|
|
68
|
+
'statistics'
|
|
69
|
+
]
|
|
70
|
+
const PROJECT_RESULT_FIELDS = [
|
|
71
|
+
'schema',
|
|
72
|
+
'id',
|
|
73
|
+
'source',
|
|
74
|
+
'documents',
|
|
75
|
+
'project',
|
|
76
|
+
'extensions',
|
|
77
|
+
'assets',
|
|
78
|
+
'diagnostics',
|
|
79
|
+
'statistics'
|
|
80
|
+
]
|
|
81
|
+
const PROJECT_DESCRIPTOR_FIELDS = [
|
|
82
|
+
'id',
|
|
83
|
+
'name',
|
|
84
|
+
'format',
|
|
85
|
+
'documentIds',
|
|
86
|
+
'relationships'
|
|
87
|
+
]
|
|
88
|
+
const PROGRESS_FIELDS = new Set([
|
|
89
|
+
'stage',
|
|
90
|
+
'detail',
|
|
91
|
+
'completed',
|
|
92
|
+
'total',
|
|
93
|
+
'message'
|
|
94
|
+
])
|
|
95
|
+
const PROGRESS_STAGES = new Set([
|
|
96
|
+
'detect',
|
|
97
|
+
'decode',
|
|
98
|
+
'project',
|
|
99
|
+
'validate',
|
|
100
|
+
'complete'
|
|
101
|
+
])
|
|
102
|
+
const RESPONSE_FIELDS = {
|
|
103
|
+
progress: ['protocol', 'type', 'requestId', 'progress'],
|
|
104
|
+
result: ['protocol', 'type', 'requestId', 'value'],
|
|
105
|
+
error: ['protocol', 'type', 'requestId', 'error', 'diagnostics']
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Validates the strict data-only responses accepted from parser workers.
|
|
110
|
+
*/
|
|
111
|
+
export class WorkerResponseData {
|
|
112
|
+
/**
|
|
113
|
+
* Validates one response header and its exact type-specific field set.
|
|
114
|
+
* @param {unknown} value Response candidate.
|
|
115
|
+
* @returns {Record<string, any>} Accessor-free response fields.
|
|
116
|
+
*/
|
|
117
|
+
static message(value) {
|
|
118
|
+
const fields = WorkerResponseData.#record(
|
|
119
|
+
value,
|
|
120
|
+
'Toolkit worker response'
|
|
121
|
+
)
|
|
122
|
+
if (fields.protocol !== TOOLKIT_WORKER_PROTOCOL) {
|
|
123
|
+
throw WorkerResponseData.#error(
|
|
124
|
+
'Toolkit worker response used an unknown protocol.'
|
|
125
|
+
)
|
|
126
|
+
}
|
|
127
|
+
const expected = RESPONSE_FIELDS[fields.type]
|
|
128
|
+
if (!expected) {
|
|
129
|
+
throw WorkerResponseData.#error(
|
|
130
|
+
'Toolkit worker response type is invalid.'
|
|
131
|
+
)
|
|
132
|
+
}
|
|
133
|
+
WorkerResponseData.#exactKeys(
|
|
134
|
+
fields,
|
|
135
|
+
expected,
|
|
136
|
+
'Toolkit worker response'
|
|
137
|
+
)
|
|
138
|
+
if (
|
|
139
|
+
typeof fields.requestId !== 'string' ||
|
|
140
|
+
!fields.requestId ||
|
|
141
|
+
fields.requestId.length > 256
|
|
142
|
+
) {
|
|
143
|
+
throw WorkerResponseData.#error(
|
|
144
|
+
'Toolkit worker response request id is invalid.'
|
|
145
|
+
)
|
|
146
|
+
}
|
|
147
|
+
return fields
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Validates an operation-specific canonical result envelope.
|
|
152
|
+
* @param {'parse' | 'loadProject' | undefined} operation Pending operation.
|
|
153
|
+
* @param {unknown} value Result candidate.
|
|
154
|
+
* @returns {object} Original validated result.
|
|
155
|
+
*/
|
|
156
|
+
static result(operation, value) {
|
|
157
|
+
const prepared = WorkerRequestData.prepareResponse(value)
|
|
158
|
+
const fields = WorkerResponseData.#record(
|
|
159
|
+
prepared,
|
|
160
|
+
'Toolkit worker result'
|
|
161
|
+
)
|
|
162
|
+
if (operation === 'parse') {
|
|
163
|
+
WorkerResponseData.#documentResult(fields, prepared)
|
|
164
|
+
return prepared
|
|
165
|
+
}
|
|
166
|
+
if (operation === 'loadProject') {
|
|
167
|
+
WorkerResponseData.#projectResult(fields, prepared)
|
|
168
|
+
return prepared
|
|
169
|
+
}
|
|
170
|
+
throw WorkerResponseData.#error(
|
|
171
|
+
'Toolkit worker result has no active operation.'
|
|
172
|
+
)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Validates and normalizes one progress response without coercion.
|
|
177
|
+
* @param {unknown} value Progress candidate.
|
|
178
|
+
* @param {Record<string, any> | null} previous Previous progress row.
|
|
179
|
+
* @returns {Record<string, any>} Canonical progress row.
|
|
180
|
+
*/
|
|
181
|
+
static progress(value, previous) {
|
|
182
|
+
const fields = WorkerResponseData.#record(
|
|
183
|
+
value,
|
|
184
|
+
'Toolkit worker progress'
|
|
185
|
+
)
|
|
186
|
+
WorkerResponseData.#allowedKeys(
|
|
187
|
+
fields,
|
|
188
|
+
PROGRESS_FIELDS,
|
|
189
|
+
new Set(['stage']),
|
|
190
|
+
'Toolkit worker progress'
|
|
191
|
+
)
|
|
192
|
+
if (
|
|
193
|
+
typeof fields.stage !== 'string' ||
|
|
194
|
+
!PROGRESS_STAGES.has(fields.stage)
|
|
195
|
+
) {
|
|
196
|
+
throw WorkerResponseData.#error(
|
|
197
|
+
'Toolkit worker progress stage is invalid.'
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
for (const key of ['detail', 'message']) {
|
|
201
|
+
if (
|
|
202
|
+
fields[key] !== undefined &&
|
|
203
|
+
(typeof fields[key] !== 'string' ||
|
|
204
|
+
!fields[key] ||
|
|
205
|
+
fields[key].length > 4096)
|
|
206
|
+
) {
|
|
207
|
+
throw WorkerResponseData.#error(
|
|
208
|
+
`Toolkit worker progress ${key} is invalid.`
|
|
209
|
+
)
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
for (const key of ['completed', 'total']) {
|
|
213
|
+
if (
|
|
214
|
+
fields[key] !== undefined &&
|
|
215
|
+
(typeof fields[key] !== 'number' ||
|
|
216
|
+
!Number.isFinite(fields[key]) ||
|
|
217
|
+
fields[key] < 0)
|
|
218
|
+
) {
|
|
219
|
+
throw WorkerResponseData.#error(
|
|
220
|
+
`Toolkit worker progress ${key} is invalid.`
|
|
221
|
+
)
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
try {
|
|
225
|
+
return ToolkitProgress.create(fields, previous)
|
|
226
|
+
} catch (error) {
|
|
227
|
+
throw WorkerResponseData.#error(
|
|
228
|
+
'Toolkit worker progress ordering is invalid.',
|
|
229
|
+
error
|
|
230
|
+
)
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Validates diagnostics and reconstructs one exact remote ToolkitError.
|
|
236
|
+
* @param {unknown} errorValue Remote error candidate.
|
|
237
|
+
* @param {unknown} diagnosticsValue Diagnostics candidate.
|
|
238
|
+
* @returns {ToolkitError} Local typed error.
|
|
239
|
+
*/
|
|
240
|
+
static remoteError(errorValue, diagnosticsValue) {
|
|
241
|
+
WorkerResponseData.#diagnostics(diagnosticsValue)
|
|
242
|
+
const fields = WorkerResponseData.#record(
|
|
243
|
+
errorValue,
|
|
244
|
+
'Toolkit worker error'
|
|
245
|
+
)
|
|
246
|
+
WorkerResponseData.#exactKeys(
|
|
247
|
+
fields,
|
|
248
|
+
ERROR_FIELDS,
|
|
249
|
+
'Toolkit worker error'
|
|
250
|
+
)
|
|
251
|
+
if (
|
|
252
|
+
fields.name !== 'ToolkitError' ||
|
|
253
|
+
!ERROR_CATEGORIES.has(fields.category)
|
|
254
|
+
) {
|
|
255
|
+
throw WorkerResponseData.#error(
|
|
256
|
+
'Toolkit worker error fields are invalid.'
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
WorkerResponseData.#boundedString(
|
|
260
|
+
fields.message,
|
|
261
|
+
4096,
|
|
262
|
+
'message',
|
|
263
|
+
false
|
|
264
|
+
)
|
|
265
|
+
WorkerResponseData.#boundedString(fields.format, 4096, 'format', false)
|
|
266
|
+
WorkerResponseData.#boundedString(fields.source, 4096, 'source', true)
|
|
267
|
+
WorkerResponseData.#boundedString(fields.code, 256, 'code', false)
|
|
268
|
+
WorkerResponseData.#cause(fields.cause)
|
|
269
|
+
WorkerRequestData.assertCloneSafe({
|
|
270
|
+
location: fields.location,
|
|
271
|
+
details: fields.details,
|
|
272
|
+
cause: fields.cause
|
|
273
|
+
})
|
|
274
|
+
return new ToolkitError(fields.message, {
|
|
275
|
+
code: fields.code,
|
|
276
|
+
category: fields.category,
|
|
277
|
+
format: fields.format,
|
|
278
|
+
source: fields.source,
|
|
279
|
+
location: fields.location,
|
|
280
|
+
details: fields.details,
|
|
281
|
+
cause: fields.cause
|
|
282
|
+
})
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Validates and proves one locally owned received document.
|
|
287
|
+
* @param {Record<string, any>} fields Document fields.
|
|
288
|
+
* @param {Record<string, any>} document Owned document result.
|
|
289
|
+
* @returns {void}
|
|
290
|
+
*/
|
|
291
|
+
static #documentResult(fields, document) {
|
|
292
|
+
WorkerResponseData.#exactKeys(
|
|
293
|
+
fields,
|
|
294
|
+
DOCUMENT_RESULT_FIELDS,
|
|
295
|
+
'Toolkit worker document result'
|
|
296
|
+
)
|
|
297
|
+
if (fields.schema !== 'ecad-toolkit.document.v1') {
|
|
298
|
+
throw WorkerResponseData.#error(
|
|
299
|
+
'Toolkit worker document schema is invalid.'
|
|
300
|
+
)
|
|
301
|
+
}
|
|
302
|
+
WorkerResponseData.#boundedString(fields.id, 4096, 'id', false)
|
|
303
|
+
const modelSchema = WorkerResponseData.#record(
|
|
304
|
+
fields.modelSchema,
|
|
305
|
+
'Toolkit worker model schema'
|
|
306
|
+
)
|
|
307
|
+
WorkerResponseData.#exactKeys(
|
|
308
|
+
modelSchema,
|
|
309
|
+
['name', 'version'],
|
|
310
|
+
'Toolkit worker model schema'
|
|
311
|
+
)
|
|
312
|
+
if (
|
|
313
|
+
modelSchema.name !== 'circuit-json' ||
|
|
314
|
+
modelSchema.version !== '0.0.446'
|
|
315
|
+
) {
|
|
316
|
+
throw WorkerResponseData.#error(
|
|
317
|
+
'Toolkit worker model schema is invalid.'
|
|
318
|
+
)
|
|
319
|
+
}
|
|
320
|
+
WorkerResponseData.#array(
|
|
321
|
+
fields.model,
|
|
322
|
+
'Toolkit worker document model',
|
|
323
|
+
2_000_000
|
|
324
|
+
)
|
|
325
|
+
WorkerResponseData.#assets(fields.assets)
|
|
326
|
+
WorkerResponseData.#diagnosticRows(fields.diagnostics, 100_000)
|
|
327
|
+
WorkerResponseData.#documentSource(fields.source)
|
|
328
|
+
WorkerResponseData.#plainResultRecords(fields, [
|
|
329
|
+
'extensions',
|
|
330
|
+
'statistics'
|
|
331
|
+
])
|
|
332
|
+
try {
|
|
333
|
+
CircuitJsonValidationProof.validateAndAttach(document)
|
|
334
|
+
} catch (error) {
|
|
335
|
+
throw WorkerResponseData.#error(
|
|
336
|
+
'Toolkit worker document model is invalid.',
|
|
337
|
+
error
|
|
338
|
+
)
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* Validates one locally owned received project and all nested documents.
|
|
344
|
+
* @param {Record<string, any>} fields Project fields.
|
|
345
|
+
* @param {Record<string, any>} projectResult Owned project result.
|
|
346
|
+
* @returns {void}
|
|
347
|
+
*/
|
|
348
|
+
static #projectResult(fields, projectResult) {
|
|
349
|
+
WorkerResponseData.#exactKeys(
|
|
350
|
+
fields,
|
|
351
|
+
PROJECT_RESULT_FIELDS,
|
|
352
|
+
'Toolkit worker project result'
|
|
353
|
+
)
|
|
354
|
+
if (fields.schema !== 'ecad-toolkit.project.v1') {
|
|
355
|
+
throw WorkerResponseData.#error(
|
|
356
|
+
'Toolkit worker project schema is invalid.'
|
|
357
|
+
)
|
|
358
|
+
}
|
|
359
|
+
WorkerResponseData.#boundedString(fields.id, 4096, 'id', false)
|
|
360
|
+
WorkerResponseData.#resultCollections(fields, ['documents'])
|
|
361
|
+
const documents = WorkerResponseData.#array(
|
|
362
|
+
fields.documents,
|
|
363
|
+
'Toolkit worker project documents',
|
|
364
|
+
4096
|
|
365
|
+
)
|
|
366
|
+
for (let index = 0; index < documents.length; index += 1) {
|
|
367
|
+
const document = documents[index]
|
|
368
|
+
WorkerResponseData.#documentResult(
|
|
369
|
+
WorkerResponseData.#record(
|
|
370
|
+
document,
|
|
371
|
+
'Toolkit worker project document'
|
|
372
|
+
),
|
|
373
|
+
projectResult.documents[index]
|
|
374
|
+
)
|
|
375
|
+
}
|
|
376
|
+
WorkerResponseData.#assets(fields.assets)
|
|
377
|
+
WorkerResponseData.#diagnosticRows(fields.diagnostics, 100_000)
|
|
378
|
+
WorkerResponseData.#projectSource(fields.source)
|
|
379
|
+
WorkerResponseData.#plainResultRecords(fields, [
|
|
380
|
+
'extensions',
|
|
381
|
+
'statistics'
|
|
382
|
+
])
|
|
383
|
+
if (fields.project !== null) {
|
|
384
|
+
const project = WorkerResponseData.#record(
|
|
385
|
+
fields.project,
|
|
386
|
+
'Toolkit worker project descriptor'
|
|
387
|
+
)
|
|
388
|
+
WorkerResponseData.#exactKeys(
|
|
389
|
+
project,
|
|
390
|
+
PROJECT_DESCRIPTOR_FIELDS,
|
|
391
|
+
'Toolkit worker project descriptor'
|
|
392
|
+
)
|
|
393
|
+
for (const key of ['id', 'name', 'format']) {
|
|
394
|
+
WorkerResponseData.#boundedString(
|
|
395
|
+
project[key],
|
|
396
|
+
4096,
|
|
397
|
+
`project ${key}`,
|
|
398
|
+
key === 'name'
|
|
399
|
+
)
|
|
400
|
+
}
|
|
401
|
+
const documentIds = WorkerResponseData.#array(
|
|
402
|
+
project.documentIds,
|
|
403
|
+
'Toolkit worker project document ids',
|
|
404
|
+
4096
|
|
405
|
+
)
|
|
406
|
+
for (const documentId of documentIds) {
|
|
407
|
+
WorkerResponseData.#boundedString(
|
|
408
|
+
documentId,
|
|
409
|
+
4096,
|
|
410
|
+
'project document id',
|
|
411
|
+
false
|
|
412
|
+
)
|
|
413
|
+
}
|
|
414
|
+
if (
|
|
415
|
+
documentIds.length !== documents.length ||
|
|
416
|
+
documentIds.some(
|
|
417
|
+
(documentId, index) => documentId !== documents[index].id
|
|
418
|
+
)
|
|
419
|
+
) {
|
|
420
|
+
throw WorkerResponseData.#error(
|
|
421
|
+
'Toolkit worker project document ids are inconsistent.'
|
|
422
|
+
)
|
|
423
|
+
}
|
|
424
|
+
const relationships = WorkerResponseData.#array(
|
|
425
|
+
project.relationships,
|
|
426
|
+
'Toolkit worker project relationships',
|
|
427
|
+
100_000
|
|
428
|
+
)
|
|
429
|
+
for (const relationship of relationships) {
|
|
430
|
+
WorkerResponseData.#record(
|
|
431
|
+
relationship,
|
|
432
|
+
'Toolkit worker project relationship'
|
|
433
|
+
)
|
|
434
|
+
}
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Requires common result collections plus operation-specific collections.
|
|
440
|
+
* @param {Record<string, any>} fields Result fields.
|
|
441
|
+
* @param {string[]} specific Operation-specific collection names.
|
|
442
|
+
* @returns {void}
|
|
443
|
+
*/
|
|
444
|
+
static #resultCollections(fields, specific) {
|
|
445
|
+
for (const key of [...specific, 'assets', 'diagnostics']) {
|
|
446
|
+
if (!Array.isArray(fields[key])) {
|
|
447
|
+
throw WorkerResponseData.#error(
|
|
448
|
+
`Toolkit worker result ${key} is invalid.`
|
|
449
|
+
)
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Requires common result map fields to be plain accessor-free records.
|
|
456
|
+
* @param {Record<string, any>} fields Result fields.
|
|
457
|
+
* @param {string[]} names Field names.
|
|
458
|
+
* @returns {void}
|
|
459
|
+
*/
|
|
460
|
+
static #plainResultRecords(fields, names) {
|
|
461
|
+
for (const name of names) {
|
|
462
|
+
WorkerResponseData.#record(
|
|
463
|
+
fields[name],
|
|
464
|
+
`Toolkit worker result ${name}`
|
|
465
|
+
)
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
/** @param {unknown} value Diagnostics candidate. @returns {void} */
|
|
470
|
+
static #diagnostics(value) {
|
|
471
|
+
WorkerRequestData.assertCloneSafe(value)
|
|
472
|
+
WorkerResponseData.#diagnosticRows(value, 1000)
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
/**
|
|
476
|
+
* Validates canonical asset records without coercion.
|
|
477
|
+
* @param {unknown} value Asset array.
|
|
478
|
+
* @returns {void}
|
|
479
|
+
*/
|
|
480
|
+
static #assets(value) {
|
|
481
|
+
const assets = WorkerResponseData.#array(
|
|
482
|
+
value,
|
|
483
|
+
'Toolkit worker assets',
|
|
484
|
+
100_000
|
|
485
|
+
)
|
|
486
|
+
for (const asset of assets) {
|
|
487
|
+
const fields = WorkerResponseData.#record(
|
|
488
|
+
asset,
|
|
489
|
+
'Toolkit worker asset'
|
|
490
|
+
)
|
|
491
|
+
WorkerResponseData.#exactKeys(
|
|
492
|
+
fields,
|
|
493
|
+
ASSET_FIELDS,
|
|
494
|
+
'Toolkit worker asset'
|
|
495
|
+
)
|
|
496
|
+
for (const key of ['id', 'kind', 'name', 'mediaType']) {
|
|
497
|
+
WorkerResponseData.#boundedString(
|
|
498
|
+
fields[key],
|
|
499
|
+
4096,
|
|
500
|
+
`asset ${key}`,
|
|
501
|
+
key === 'name'
|
|
502
|
+
)
|
|
503
|
+
}
|
|
504
|
+
if (
|
|
505
|
+
!Number.isSafeInteger(fields.byteLength) ||
|
|
506
|
+
fields.byteLength < 0
|
|
507
|
+
) {
|
|
508
|
+
throw WorkerResponseData.#error(
|
|
509
|
+
'Toolkit worker asset byteLength is invalid.'
|
|
510
|
+
)
|
|
511
|
+
}
|
|
512
|
+
if (fields.source !== null) {
|
|
513
|
+
WorkerResponseData.#record(
|
|
514
|
+
fields.source,
|
|
515
|
+
'Toolkit worker asset source'
|
|
516
|
+
)
|
|
517
|
+
}
|
|
518
|
+
if (
|
|
519
|
+
fields.data !== null &&
|
|
520
|
+
typeof fields.data !== 'string' &&
|
|
521
|
+
WorkerResponseData.#binaryByteLength(fields.data) === null
|
|
522
|
+
) {
|
|
523
|
+
throw WorkerResponseData.#error(
|
|
524
|
+
'Toolkit worker asset data is invalid.'
|
|
525
|
+
)
|
|
526
|
+
}
|
|
527
|
+
const actualByteLength = WorkerResponseData.#assetByteLength(
|
|
528
|
+
fields.data
|
|
529
|
+
)
|
|
530
|
+
if (
|
|
531
|
+
actualByteLength !== null &&
|
|
532
|
+
fields.byteLength !== actualByteLength
|
|
533
|
+
) {
|
|
534
|
+
throw WorkerResponseData.#error(
|
|
535
|
+
'Toolkit worker asset byteLength is inconsistent.'
|
|
536
|
+
)
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Validates the required canonical document-source primitives.
|
|
543
|
+
* @param {unknown} value Source candidate.
|
|
544
|
+
* @returns {void}
|
|
545
|
+
*/
|
|
546
|
+
static #documentSource(value) {
|
|
547
|
+
const fields = WorkerResponseData.#record(
|
|
548
|
+
value,
|
|
549
|
+
'Toolkit worker document source'
|
|
550
|
+
)
|
|
551
|
+
WorkerResponseData.#boundedString(
|
|
552
|
+
fields.format,
|
|
553
|
+
256,
|
|
554
|
+
'document source format',
|
|
555
|
+
false
|
|
556
|
+
)
|
|
557
|
+
WorkerResponseData.#boundedString(
|
|
558
|
+
fields.fileName,
|
|
559
|
+
4096,
|
|
560
|
+
'document source fileName',
|
|
561
|
+
true
|
|
562
|
+
)
|
|
563
|
+
WorkerResponseData.#boundedString(
|
|
564
|
+
fields.fileType,
|
|
565
|
+
256,
|
|
566
|
+
'document source fileType',
|
|
567
|
+
false
|
|
568
|
+
)
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
/**
|
|
572
|
+
* Validates the required canonical project-source primitives.
|
|
573
|
+
* @param {unknown} value Source candidate.
|
|
574
|
+
* @returns {void}
|
|
575
|
+
*/
|
|
576
|
+
static #projectSource(value) {
|
|
577
|
+
const fields = WorkerResponseData.#record(
|
|
578
|
+
value,
|
|
579
|
+
'Toolkit worker project source'
|
|
580
|
+
)
|
|
581
|
+
WorkerResponseData.#boundedString(
|
|
582
|
+
fields.format,
|
|
583
|
+
256,
|
|
584
|
+
'project source format',
|
|
585
|
+
false
|
|
586
|
+
)
|
|
587
|
+
const entryNames = WorkerResponseData.#array(
|
|
588
|
+
fields.entryNames,
|
|
589
|
+
'Toolkit worker project source entryNames',
|
|
590
|
+
4096
|
|
591
|
+
)
|
|
592
|
+
for (const entryName of entryNames) {
|
|
593
|
+
WorkerResponseData.#boundedString(
|
|
594
|
+
entryName,
|
|
595
|
+
4096,
|
|
596
|
+
'project source entryName',
|
|
597
|
+
false
|
|
598
|
+
)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Measures present canonical asset data exactly as UTF-8 or binary bytes.
|
|
604
|
+
* @param {unknown} value Asset data.
|
|
605
|
+
* @returns {number | null} Actual bytes, or null for absent/invalid data.
|
|
606
|
+
*/
|
|
607
|
+
static #assetByteLength(value) {
|
|
608
|
+
if (value === null) return null
|
|
609
|
+
if (typeof value === 'string') {
|
|
610
|
+
return WorkerResponseData.#utf8ByteLength(value)
|
|
611
|
+
}
|
|
612
|
+
return WorkerResponseData.#binaryByteLength(value)
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Reads genuine binary internal slots without trusting surface properties.
|
|
617
|
+
* @param {unknown} value Binary candidate.
|
|
618
|
+
* @returns {number | null} Intrinsic byte length.
|
|
619
|
+
*/
|
|
620
|
+
static #binaryByteLength(value) {
|
|
621
|
+
for (const getter of [
|
|
622
|
+
ARRAY_BUFFER_BYTE_LENGTH_GETTER,
|
|
623
|
+
DATA_VIEW_BYTE_LENGTH_GETTER,
|
|
624
|
+
TYPED_ARRAY_BYTE_LENGTH_GETTER
|
|
625
|
+
]) {
|
|
626
|
+
if (!getter) continue
|
|
627
|
+
try {
|
|
628
|
+
return getter.call(value)
|
|
629
|
+
} catch {
|
|
630
|
+
// Each intrinsic independently proves its corresponding brand.
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
return null
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
/**
|
|
637
|
+
* Measures a string with the replacement behavior used by UTF-8 encoders.
|
|
638
|
+
* @param {string} value Text value.
|
|
639
|
+
* @returns {number} UTF-8 byte length.
|
|
640
|
+
*/
|
|
641
|
+
static #utf8ByteLength(value) {
|
|
642
|
+
let bytes = 0
|
|
643
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
644
|
+
const first = value.charCodeAt(index)
|
|
645
|
+
let codePoint = first
|
|
646
|
+
if (first >= 0xd800 && first <= 0xdbff) {
|
|
647
|
+
const second = value.charCodeAt(index + 1)
|
|
648
|
+
if (second >= 0xdc00 && second <= 0xdfff) {
|
|
649
|
+
codePoint =
|
|
650
|
+
0x10000 + ((first - 0xd800) << 10) + (second - 0xdc00)
|
|
651
|
+
index += 1
|
|
652
|
+
} else {
|
|
653
|
+
codePoint = 0xfffd
|
|
654
|
+
}
|
|
655
|
+
} else if (first >= 0xdc00 && first <= 0xdfff) {
|
|
656
|
+
codePoint = 0xfffd
|
|
657
|
+
}
|
|
658
|
+
if (codePoint <= 0x7f) bytes += 1
|
|
659
|
+
else if (codePoint <= 0x7ff) bytes += 2
|
|
660
|
+
else if (codePoint <= 0xffff) bytes += 3
|
|
661
|
+
else bytes += 4
|
|
662
|
+
}
|
|
663
|
+
return bytes
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
/**
|
|
667
|
+
* Validates canonical diagnostic rows.
|
|
668
|
+
* @param {unknown} value Diagnostics candidate.
|
|
669
|
+
* @param {number} maximum Maximum row count.
|
|
670
|
+
* @returns {void}
|
|
671
|
+
*/
|
|
672
|
+
static #diagnosticRows(value, maximum) {
|
|
673
|
+
const diagnostics = WorkerResponseData.#array(
|
|
674
|
+
value,
|
|
675
|
+
'Toolkit worker diagnostics',
|
|
676
|
+
maximum
|
|
677
|
+
)
|
|
678
|
+
for (const diagnostic of diagnostics) {
|
|
679
|
+
const fields = WorkerResponseData.#record(
|
|
680
|
+
diagnostic,
|
|
681
|
+
'Toolkit worker diagnostic'
|
|
682
|
+
)
|
|
683
|
+
WorkerResponseData.#exactKeys(
|
|
684
|
+
fields,
|
|
685
|
+
DIAGNOSTIC_FIELDS,
|
|
686
|
+
'Toolkit worker diagnostic'
|
|
687
|
+
)
|
|
688
|
+
WorkerResponseData.#boundedString(
|
|
689
|
+
fields.code,
|
|
690
|
+
256,
|
|
691
|
+
'diagnostic code',
|
|
692
|
+
false
|
|
693
|
+
)
|
|
694
|
+
WorkerResponseData.#boundedString(
|
|
695
|
+
fields.message,
|
|
696
|
+
4096,
|
|
697
|
+
'diagnostic message',
|
|
698
|
+
true
|
|
699
|
+
)
|
|
700
|
+
WorkerResponseData.#boundedString(
|
|
701
|
+
fields.source,
|
|
702
|
+
4096,
|
|
703
|
+
'diagnostic source',
|
|
704
|
+
true
|
|
705
|
+
)
|
|
706
|
+
if (!DIAGNOSTIC_SEVERITIES.has(fields.severity)) {
|
|
707
|
+
throw WorkerResponseData.#error(
|
|
708
|
+
'Toolkit worker diagnostic severity is invalid.'
|
|
709
|
+
)
|
|
710
|
+
}
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/** @param {unknown} value Cause candidate. @returns {void} */
|
|
715
|
+
static #cause(value) {
|
|
716
|
+
if (value === null) return
|
|
717
|
+
const fields = WorkerResponseData.#record(
|
|
718
|
+
value,
|
|
719
|
+
'Toolkit worker error cause'
|
|
720
|
+
)
|
|
721
|
+
WorkerResponseData.#exactKeys(
|
|
722
|
+
fields,
|
|
723
|
+
ERROR_CAUSE_FIELDS,
|
|
724
|
+
'Toolkit worker error cause'
|
|
725
|
+
)
|
|
726
|
+
WorkerResponseData.#boundedString(
|
|
727
|
+
fields.name,
|
|
728
|
+
4096,
|
|
729
|
+
'cause name',
|
|
730
|
+
false
|
|
731
|
+
)
|
|
732
|
+
WorkerResponseData.#boundedString(
|
|
733
|
+
fields.message,
|
|
734
|
+
4096,
|
|
735
|
+
'cause message',
|
|
736
|
+
true
|
|
737
|
+
)
|
|
738
|
+
if (fields.code !== null) {
|
|
739
|
+
WorkerResponseData.#boundedString(
|
|
740
|
+
fields.code,
|
|
741
|
+
256,
|
|
742
|
+
'cause code',
|
|
743
|
+
true
|
|
744
|
+
)
|
|
745
|
+
}
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Requires one bounded string without coercion.
|
|
750
|
+
* @param {unknown} value String candidate.
|
|
751
|
+
* @param {number} maximum Maximum length.
|
|
752
|
+
* @param {string} label Field label.
|
|
753
|
+
* @param {boolean} allowEmpty Whether an empty string is valid.
|
|
754
|
+
* @returns {void}
|
|
755
|
+
*/
|
|
756
|
+
static #boundedString(value, maximum, label, allowEmpty) {
|
|
757
|
+
if (
|
|
758
|
+
typeof value !== 'string' ||
|
|
759
|
+
value.length > maximum ||
|
|
760
|
+
(!allowEmpty && !value)
|
|
761
|
+
) {
|
|
762
|
+
throw WorkerResponseData.#error(
|
|
763
|
+
`Toolkit worker ${label} is invalid.`
|
|
764
|
+
)
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Requires an exact enumerable field set.
|
|
770
|
+
* @param {Record<string, any>} fields Record fields.
|
|
771
|
+
* @param {string[]} expected Expected keys.
|
|
772
|
+
* @param {string} label Record label.
|
|
773
|
+
* @returns {void}
|
|
774
|
+
*/
|
|
775
|
+
static #exactKeys(fields, expected, label) {
|
|
776
|
+
const keys = Object.keys(fields)
|
|
777
|
+
if (
|
|
778
|
+
keys.length !== expected.length ||
|
|
779
|
+
expected.some((key) => !Object.hasOwn(fields, key))
|
|
780
|
+
) {
|
|
781
|
+
throw WorkerResponseData.#error(`${label} fields are invalid.`)
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
/**
|
|
786
|
+
* Requires an allowed key set and all required keys.
|
|
787
|
+
* @param {Record<string, any>} fields Record fields.
|
|
788
|
+
* @param {Set<string>} allowed Allowed keys.
|
|
789
|
+
* @param {Set<string>} required Required keys.
|
|
790
|
+
* @param {string} label Record label.
|
|
791
|
+
* @returns {void}
|
|
792
|
+
*/
|
|
793
|
+
static #allowedKeys(fields, allowed, required, label) {
|
|
794
|
+
const keys = Object.keys(fields)
|
|
795
|
+
if (
|
|
796
|
+
keys.some((key) => !allowed.has(key)) ||
|
|
797
|
+
[...required].some((key) => !Object.hasOwn(fields, key))
|
|
798
|
+
) {
|
|
799
|
+
throw WorkerResponseData.#error(`${label} fields are invalid.`)
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Reads one accessor-free plain record.
|
|
805
|
+
* @param {unknown} value Record candidate.
|
|
806
|
+
* @param {string} label Record label.
|
|
807
|
+
* @returns {Record<string, any>} Null-prototype data map.
|
|
808
|
+
*/
|
|
809
|
+
static #record(value, label) {
|
|
810
|
+
RuntimeProxyBoundary.assert(value, label)
|
|
811
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
812
|
+
throw WorkerResponseData.#error(`${label} must be a plain object.`)
|
|
813
|
+
}
|
|
814
|
+
let prototype
|
|
815
|
+
let descriptors
|
|
816
|
+
try {
|
|
817
|
+
prototype = Object.getPrototypeOf(value)
|
|
818
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
819
|
+
} catch (error) {
|
|
820
|
+
throw WorkerResponseData.#error(
|
|
821
|
+
`${label} could not be inspected safely.`,
|
|
822
|
+
error
|
|
823
|
+
)
|
|
824
|
+
}
|
|
825
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
826
|
+
throw WorkerResponseData.#error(`${label} must be a plain object.`)
|
|
827
|
+
}
|
|
828
|
+
const result = Object.create(null)
|
|
829
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
830
|
+
const descriptor = descriptors[key]
|
|
831
|
+
if (
|
|
832
|
+
typeof key !== 'string' ||
|
|
833
|
+
descriptor.get ||
|
|
834
|
+
descriptor.set ||
|
|
835
|
+
descriptor.enumerable !== true
|
|
836
|
+
) {
|
|
837
|
+
throw WorkerResponseData.#error(
|
|
838
|
+
`${label} may contain only enumerable data properties.`
|
|
839
|
+
)
|
|
840
|
+
}
|
|
841
|
+
Object.defineProperty(result, key, {
|
|
842
|
+
configurable: true,
|
|
843
|
+
enumerable: true,
|
|
844
|
+
value: descriptor.value,
|
|
845
|
+
writable: true
|
|
846
|
+
})
|
|
847
|
+
}
|
|
848
|
+
return result
|
|
849
|
+
}
|
|
850
|
+
|
|
851
|
+
/**
|
|
852
|
+
* Reads one bounded dense plain array.
|
|
853
|
+
* @param {unknown} value Array candidate.
|
|
854
|
+
* @param {string} label Array label.
|
|
855
|
+
* @param {number} maximum Maximum length.
|
|
856
|
+
* @returns {any[]} Array values.
|
|
857
|
+
*/
|
|
858
|
+
static #array(value, label, maximum) {
|
|
859
|
+
RuntimeProxyBoundary.assert(value, label)
|
|
860
|
+
if (!Array.isArray(value)) {
|
|
861
|
+
throw WorkerResponseData.#error(`${label} must be an array.`)
|
|
862
|
+
}
|
|
863
|
+
const length = Object.getOwnPropertyDescriptor(value, 'length')?.value
|
|
864
|
+
const keys = Reflect.ownKeys(value)
|
|
865
|
+
if (
|
|
866
|
+
Object.getPrototypeOf(value) !== Array.prototype ||
|
|
867
|
+
!Number.isSafeInteger(length) ||
|
|
868
|
+
length < 0 ||
|
|
869
|
+
length > maximum ||
|
|
870
|
+
keys.length !== length + 1
|
|
871
|
+
) {
|
|
872
|
+
throw WorkerResponseData.#error(
|
|
873
|
+
`${label} must be a bounded dense plain array.`
|
|
874
|
+
)
|
|
875
|
+
}
|
|
876
|
+
const result = new Array(length)
|
|
877
|
+
for (let index = 0; index < length; index += 1) {
|
|
878
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
879
|
+
value,
|
|
880
|
+
String(index)
|
|
881
|
+
)
|
|
882
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
883
|
+
throw WorkerResponseData.#error(
|
|
884
|
+
`${label} must contain data properties.`
|
|
885
|
+
)
|
|
886
|
+
}
|
|
887
|
+
result[index] = descriptor.value
|
|
888
|
+
}
|
|
889
|
+
return result
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
/**
|
|
893
|
+
* Creates one typed protocol error.
|
|
894
|
+
* @param {string} message Failure message.
|
|
895
|
+
* @param {unknown} [cause] Failure cause.
|
|
896
|
+
* @returns {ToolkitError} Protocol error.
|
|
897
|
+
*/
|
|
898
|
+
static #error(message, cause = null) {
|
|
899
|
+
return new ToolkitError(message, {
|
|
900
|
+
code: 'ERR_WORKER_MESSAGE',
|
|
901
|
+
category: 'runtime',
|
|
902
|
+
cause
|
|
903
|
+
})
|
|
904
|
+
}
|
|
905
|
+
}
|