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,997 @@
|
|
|
1
|
+
import { ToolkitError } from '../contracts/ToolkitError.mjs'
|
|
2
|
+
import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
3
|
+
import { TOOLKIT_WORKER_PROTOCOL } from './ToolkitWorkerProtocol.mjs'
|
|
4
|
+
import { WorkerRequestData } from './WorkerRequestData.mjs'
|
|
5
|
+
import { WorkerResponseData } from './WorkerResponseData.mjs'
|
|
6
|
+
|
|
7
|
+
const ABORTED_GETTER = Object.getOwnPropertyDescriptor(
|
|
8
|
+
AbortSignal.prototype,
|
|
9
|
+
'aborted'
|
|
10
|
+
)?.get
|
|
11
|
+
const ADD_EVENT_LISTENER = EventTarget.prototype.addEventListener
|
|
12
|
+
const REMOVE_EVENT_LISTENER = EventTarget.prototype.removeEventListener
|
|
13
|
+
const ERROR_EVENT_ERROR_GETTER =
|
|
14
|
+
typeof ErrorEvent === 'function'
|
|
15
|
+
? Object.getOwnPropertyDescriptor(ErrorEvent.prototype, 'error')?.get
|
|
16
|
+
: null
|
|
17
|
+
const MESSAGE_EVENT_DATA_GETTER =
|
|
18
|
+
typeof MessageEvent === 'function'
|
|
19
|
+
? Object.getOwnPropertyDescriptor(MessageEvent.prototype, 'data')?.get
|
|
20
|
+
: null
|
|
21
|
+
const MAX_PENDING_REQUESTS = 1024
|
|
22
|
+
const MAX_PROJECT_ENTRIES = 4096
|
|
23
|
+
const ATTEMPT_ERRORS = new WeakMap()
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Owns one lazily-created worker speaking the common toolkit protocol.
|
|
27
|
+
*/
|
|
28
|
+
export class ParserWorkerClient {
|
|
29
|
+
static #defaultClient = null
|
|
30
|
+
|
|
31
|
+
#activeRequestId = null
|
|
32
|
+
#createWorker
|
|
33
|
+
#disposed = false
|
|
34
|
+
#nextRequestId = 1
|
|
35
|
+
#onError = null
|
|
36
|
+
#onMessage = null
|
|
37
|
+
#onMessageError = null
|
|
38
|
+
#pending = new Map()
|
|
39
|
+
#postMessage = null
|
|
40
|
+
#queueHead = null
|
|
41
|
+
#queueTail = null
|
|
42
|
+
#queued = new Map()
|
|
43
|
+
#removeEventListener = null
|
|
44
|
+
#terminateWorker = null
|
|
45
|
+
#worker = null
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Creates one client with an injected browser-compatible worker factory.
|
|
49
|
+
* @param {{ createWorker: Function }} options Construction options.
|
|
50
|
+
*/
|
|
51
|
+
constructor(options) {
|
|
52
|
+
const fields = ParserWorkerClient.#record(
|
|
53
|
+
options,
|
|
54
|
+
'Parser worker client options'
|
|
55
|
+
)
|
|
56
|
+
if (typeof fields.createWorker !== 'function') {
|
|
57
|
+
throw new TypeError(
|
|
58
|
+
'Parser worker client createWorker must be a function.'
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
this.#createWorker = fields.createWorker
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Returns true when the host exposes a browser Worker constructor.
|
|
66
|
+
* @returns {boolean} Whether the default client can create a worker.
|
|
67
|
+
*/
|
|
68
|
+
static isDefaultAvailable() {
|
|
69
|
+
try {
|
|
70
|
+
return typeof globalThis.Worker === 'function'
|
|
71
|
+
} catch {
|
|
72
|
+
return false
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Returns the process-local default client, creating it lazily.
|
|
78
|
+
* @returns {ParserWorkerClient} Default worker client.
|
|
79
|
+
*/
|
|
80
|
+
static defaultClient() {
|
|
81
|
+
return ParserWorkerClient.#defaultClientFor(null)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Returns the process-local default client for one internal attempt.
|
|
86
|
+
* @param {object | null} attemptToken Internal attempt identity.
|
|
87
|
+
* @returns {ParserWorkerClient} Default worker client.
|
|
88
|
+
*/
|
|
89
|
+
static #defaultClientFor(attemptToken) {
|
|
90
|
+
if (!ParserWorkerClient.isDefaultAvailable()) {
|
|
91
|
+
throw ParserWorkerClient.#unavailableError(attemptToken)
|
|
92
|
+
}
|
|
93
|
+
if (!ParserWorkerClient.#defaultClient) {
|
|
94
|
+
ParserWorkerClient.#defaultClient = new ParserWorkerClient({
|
|
95
|
+
createWorker: () => {
|
|
96
|
+
const WorkerConstructor = globalThis.Worker
|
|
97
|
+
return Reflect.construct(WorkerConstructor, [
|
|
98
|
+
new URL(
|
|
99
|
+
'../../workers/parser.worker.mjs',
|
|
100
|
+
import.meta.url
|
|
101
|
+
),
|
|
102
|
+
{ type: 'module' }
|
|
103
|
+
])
|
|
104
|
+
}
|
|
105
|
+
})
|
|
106
|
+
}
|
|
107
|
+
return ParserWorkerClient.#defaultClient
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Disposes the process-local default worker client when one exists.
|
|
112
|
+
* @returns {void}
|
|
113
|
+
*/
|
|
114
|
+
static disposeDefault() {
|
|
115
|
+
ParserWorkerClient.#defaultClient?.dispose()
|
|
116
|
+
ParserWorkerClient.#defaultClient = null
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Runs one parser request as a request-scoped default-worker attempt.
|
|
121
|
+
* @param {unknown} input Parser input.
|
|
122
|
+
* @param {unknown} [options] Parser options.
|
|
123
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, error: unknown, unavailable: boolean }>} Attempt result.
|
|
124
|
+
*/
|
|
125
|
+
static async parseDefault(input, options = {}) {
|
|
126
|
+
return await ParserWorkerClient.#defaultAttempt('parse', input, options)
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Runs one project request as a request-scoped default-worker attempt.
|
|
131
|
+
* @param {unknown} entries Project entries.
|
|
132
|
+
* @param {unknown} [options] Project options.
|
|
133
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, error: unknown, unavailable: boolean }>} Attempt result.
|
|
134
|
+
*/
|
|
135
|
+
static async loadProjectDefault(entries, options = {}) {
|
|
136
|
+
return await ParserWorkerClient.#defaultAttempt(
|
|
137
|
+
'loadProject',
|
|
138
|
+
entries,
|
|
139
|
+
options
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Parses one source input in the owned worker.
|
|
145
|
+
* @param {unknown} input Parser input.
|
|
146
|
+
* @param {unknown} [options] Parser options.
|
|
147
|
+
* @returns {Promise<object>} Canonical document result.
|
|
148
|
+
*/
|
|
149
|
+
async parse(input, options = {}) {
|
|
150
|
+
const prepared = ParserWorkerClient.#parsePayload(input, options)
|
|
151
|
+
return await this.#request('parse', prepared)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/** Runs one parser request as an instance-scoped worker attempt. */
|
|
155
|
+
async parseAttempt(input, options = {}) {
|
|
156
|
+
return await this.#attempt('parse', input, options)
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Loads one project entry list in the owned worker.
|
|
161
|
+
* @param {unknown} entries Project entries.
|
|
162
|
+
* @param {unknown} [options] Project options.
|
|
163
|
+
* @returns {Promise<object>} Canonical project result.
|
|
164
|
+
*/
|
|
165
|
+
async loadProject(entries, options = {}) {
|
|
166
|
+
const prepared = ParserWorkerClient.#projectPayload(entries, options)
|
|
167
|
+
return await this.#request('loadProject', prepared)
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Runs one project request as an instance-scoped worker attempt. */
|
|
171
|
+
async loadProjectAttempt(entries, options = {}) {
|
|
172
|
+
return await this.#attempt('loadProject', entries, options)
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/**
|
|
176
|
+
* Cancels one active request and immediately replaces its owned worker.
|
|
177
|
+
* @param {unknown} requestId Request id.
|
|
178
|
+
* @returns {boolean} Whether an active request was cancelled.
|
|
179
|
+
*/
|
|
180
|
+
cancel(requestId) {
|
|
181
|
+
const id = typeof requestId === 'string' ? requestId : ''
|
|
182
|
+
if (!this.#pending.has(id)) return false
|
|
183
|
+
const error = ParserWorkerClient.#cancelledError(id)
|
|
184
|
+
if (this.#activeRequestId === id) {
|
|
185
|
+
try {
|
|
186
|
+
this.#postMessage?.({
|
|
187
|
+
protocol: TOOLKIT_WORKER_PROTOCOL,
|
|
188
|
+
type: 'cancel',
|
|
189
|
+
requestId: id
|
|
190
|
+
})
|
|
191
|
+
} catch {
|
|
192
|
+
// Worker termination below is authoritative.
|
|
193
|
+
}
|
|
194
|
+
this.#settle(id, 'reject', error, false)
|
|
195
|
+
this.#resetWorker()
|
|
196
|
+
this.#drain()
|
|
197
|
+
return true
|
|
198
|
+
}
|
|
199
|
+
this.#settle(id, 'reject', error)
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Terminates the worker and permanently rejects future work.
|
|
205
|
+
* @returns {void}
|
|
206
|
+
*/
|
|
207
|
+
dispose() {
|
|
208
|
+
if (this.#disposed) return
|
|
209
|
+
this.#disposed = true
|
|
210
|
+
const error = ParserWorkerClient.#disposedError()
|
|
211
|
+
for (const requestId of [...this.#pending.keys()]) {
|
|
212
|
+
this.#settle(requestId, 'reject', error, false)
|
|
213
|
+
}
|
|
214
|
+
this.#queueHead = null
|
|
215
|
+
this.#queueTail = null
|
|
216
|
+
this.#queued.clear()
|
|
217
|
+
this.#resetWorker()
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Posts one prepared operation and observes progress/cancellation.
|
|
222
|
+
* @param {'parse' | 'loadProject'} operation Worker operation.
|
|
223
|
+
* @param {{ payload: object, transfer: Transferable[], signal: AbortSignal | null, onProgress: Function | null }} prepared Prepared request.
|
|
224
|
+
* @param {object | null} attemptToken Request-scoped attempt identity.
|
|
225
|
+
* @returns {Promise<object>} Worker result.
|
|
226
|
+
*/
|
|
227
|
+
async #request(operation, prepared, attemptToken = null) {
|
|
228
|
+
if (this.#disposed) throw ParserWorkerClient.#disposedError()
|
|
229
|
+
if (prepared.signal && ParserWorkerClient.#isAborted(prepared.signal)) {
|
|
230
|
+
throw ParserWorkerClient.#cancelledError('')
|
|
231
|
+
}
|
|
232
|
+
if (this.#pending.size >= MAX_PENDING_REQUESTS) {
|
|
233
|
+
throw ParserWorkerClient.#queueError()
|
|
234
|
+
}
|
|
235
|
+
const requestId = `worker-${this.#nextRequestId}`
|
|
236
|
+
this.#nextRequestId += 1
|
|
237
|
+
|
|
238
|
+
return await new Promise((resolve, reject) => {
|
|
239
|
+
const pending = {
|
|
240
|
+
operation,
|
|
241
|
+
prepared,
|
|
242
|
+
resolve,
|
|
243
|
+
reject,
|
|
244
|
+
signal: prepared.signal,
|
|
245
|
+
onAbort: null,
|
|
246
|
+
onProgress: prepared.onProgress,
|
|
247
|
+
previousProgress: null,
|
|
248
|
+
attemptToken
|
|
249
|
+
}
|
|
250
|
+
if (prepared.signal) {
|
|
251
|
+
pending.onAbort = () => this.cancel(requestId)
|
|
252
|
+
ADD_EVENT_LISTENER.call(
|
|
253
|
+
prepared.signal,
|
|
254
|
+
'abort',
|
|
255
|
+
pending.onAbort,
|
|
256
|
+
{ once: true }
|
|
257
|
+
)
|
|
258
|
+
}
|
|
259
|
+
this.#pending.set(requestId, pending)
|
|
260
|
+
this.#enqueue(requestId)
|
|
261
|
+
this.#drain()
|
|
262
|
+
})
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Posts the next queued request so worker termination stays request-scoped.
|
|
267
|
+
* @returns {void}
|
|
268
|
+
*/
|
|
269
|
+
#drain() {
|
|
270
|
+
if (this.#disposed || this.#activeRequestId) return
|
|
271
|
+
let requestId = this.#dequeue()
|
|
272
|
+
let pending = requestId ? this.#pending.get(requestId) || null : null
|
|
273
|
+
while (requestId && !pending) {
|
|
274
|
+
requestId = this.#dequeue()
|
|
275
|
+
pending = requestId ? this.#pending.get(requestId) || null : null
|
|
276
|
+
}
|
|
277
|
+
if (!pending) return
|
|
278
|
+
if (pending.signal && ParserWorkerClient.#isAborted(pending.signal)) {
|
|
279
|
+
this.#settle(
|
|
280
|
+
requestId,
|
|
281
|
+
'reject',
|
|
282
|
+
ParserWorkerClient.#cancelledError(requestId)
|
|
283
|
+
)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
this.#activeRequestId = requestId
|
|
287
|
+
let phase = 'construct'
|
|
288
|
+
try {
|
|
289
|
+
this.#ensureWorker()
|
|
290
|
+
phase = 'post'
|
|
291
|
+
this.#postMessage(
|
|
292
|
+
{
|
|
293
|
+
protocol: TOOLKIT_WORKER_PROTOCOL,
|
|
294
|
+
type: pending.operation,
|
|
295
|
+
requestId,
|
|
296
|
+
...pending.prepared.payload
|
|
297
|
+
},
|
|
298
|
+
pending.prepared.transfer
|
|
299
|
+
)
|
|
300
|
+
if (
|
|
301
|
+
pending.signal &&
|
|
302
|
+
ParserWorkerClient.#isAborted(pending.signal)
|
|
303
|
+
) {
|
|
304
|
+
this.cancel(requestId)
|
|
305
|
+
}
|
|
306
|
+
} catch (error) {
|
|
307
|
+
const requestError = ParserWorkerClient.#requestError(error, phase)
|
|
308
|
+
if (phase === 'construct' && pending.attemptToken) {
|
|
309
|
+
ATTEMPT_ERRORS.set(requestError, pending.attemptToken)
|
|
310
|
+
}
|
|
311
|
+
this.#settle(requestId, 'reject', requestError, false)
|
|
312
|
+
this.#resetWorker()
|
|
313
|
+
this.#drain()
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/**
|
|
318
|
+
* Creates and binds the worker on first use.
|
|
319
|
+
* @returns {void}
|
|
320
|
+
*/
|
|
321
|
+
#ensureWorker() {
|
|
322
|
+
if (this.#worker) return
|
|
323
|
+
let worker
|
|
324
|
+
try {
|
|
325
|
+
worker = Reflect.apply(this.#createWorker, undefined, [])
|
|
326
|
+
} catch (error) {
|
|
327
|
+
throw ParserWorkerClient.#runtimeError(error)
|
|
328
|
+
}
|
|
329
|
+
RuntimeProxyBoundary.assert(worker, 'Parser worker')
|
|
330
|
+
const addEventListener = ParserWorkerClient.#method(
|
|
331
|
+
worker,
|
|
332
|
+
'addEventListener'
|
|
333
|
+
)
|
|
334
|
+
const removeEventListener = ParserWorkerClient.#method(
|
|
335
|
+
worker,
|
|
336
|
+
'removeEventListener'
|
|
337
|
+
)
|
|
338
|
+
const postMessage = ParserWorkerClient.#method(worker, 'postMessage')
|
|
339
|
+
const terminate = ParserWorkerClient.#method(worker, 'terminate')
|
|
340
|
+
if (!addEventListener || !postMessage || !terminate) {
|
|
341
|
+
throw new TypeError(
|
|
342
|
+
'Parser worker must expose message event, post, and terminate methods.'
|
|
343
|
+
)
|
|
344
|
+
}
|
|
345
|
+
this.#worker = worker
|
|
346
|
+
this.#postMessage = postMessage
|
|
347
|
+
this.#removeEventListener = removeEventListener
|
|
348
|
+
this.#terminateWorker = terminate
|
|
349
|
+
this.#onMessage = (event) => this.#handleMessage(event)
|
|
350
|
+
this.#onError = (event) =>
|
|
351
|
+
this.#failActive(
|
|
352
|
+
ParserWorkerClient.#runtimeError(
|
|
353
|
+
ParserWorkerClient.#eventField(
|
|
354
|
+
event,
|
|
355
|
+
'error',
|
|
356
|
+
ERROR_EVENT_ERROR_GETTER
|
|
357
|
+
)
|
|
358
|
+
)
|
|
359
|
+
)
|
|
360
|
+
this.#onMessageError = () =>
|
|
361
|
+
this.#failActive(
|
|
362
|
+
ParserWorkerClient.#protocolError(
|
|
363
|
+
'Toolkit worker message could not be cloned.'
|
|
364
|
+
)
|
|
365
|
+
)
|
|
366
|
+
addEventListener('message', this.#onMessage)
|
|
367
|
+
addEventListener('error', this.#onError)
|
|
368
|
+
addEventListener('messageerror', this.#onMessageError)
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
/**
|
|
372
|
+
* Routes one worker response to its pending request.
|
|
373
|
+
* @param {{ data?: unknown }} event Worker message event.
|
|
374
|
+
* @returns {void}
|
|
375
|
+
*/
|
|
376
|
+
#handleMessage(event) {
|
|
377
|
+
let message
|
|
378
|
+
try {
|
|
379
|
+
message = WorkerResponseData.message(
|
|
380
|
+
ParserWorkerClient.#eventField(
|
|
381
|
+
event,
|
|
382
|
+
'data',
|
|
383
|
+
MESSAGE_EVENT_DATA_GETTER
|
|
384
|
+
)
|
|
385
|
+
)
|
|
386
|
+
if (message.requestId !== this.#activeRequestId) {
|
|
387
|
+
return
|
|
388
|
+
}
|
|
389
|
+
if (message.type === 'progress') {
|
|
390
|
+
this.#progress(message.requestId, message.progress)
|
|
391
|
+
return
|
|
392
|
+
}
|
|
393
|
+
if (message.type === 'result') {
|
|
394
|
+
const pending = this.#pending.get(message.requestId)
|
|
395
|
+
const value = WorkerResponseData.result(
|
|
396
|
+
pending?.operation,
|
|
397
|
+
message.value
|
|
398
|
+
)
|
|
399
|
+
this.#settle(message.requestId, 'resolve', value)
|
|
400
|
+
return
|
|
401
|
+
}
|
|
402
|
+
if (message.type === 'error') {
|
|
403
|
+
this.#settle(
|
|
404
|
+
message.requestId,
|
|
405
|
+
'reject',
|
|
406
|
+
WorkerResponseData.remoteError(
|
|
407
|
+
message.error,
|
|
408
|
+
message.diagnostics
|
|
409
|
+
)
|
|
410
|
+
)
|
|
411
|
+
return
|
|
412
|
+
}
|
|
413
|
+
} catch (error) {
|
|
414
|
+
this.#failActive(
|
|
415
|
+
ToolkitError.trustedRecord(error)
|
|
416
|
+
? error
|
|
417
|
+
: ParserWorkerClient.#protocolError(
|
|
418
|
+
'Toolkit worker response is invalid.',
|
|
419
|
+
error
|
|
420
|
+
)
|
|
421
|
+
)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Validates and emits one ordered worker progress row.
|
|
427
|
+
* @param {string} requestId Request id.
|
|
428
|
+
* @param {unknown} candidate Progress candidate.
|
|
429
|
+
* @returns {void}
|
|
430
|
+
*/
|
|
431
|
+
#progress(requestId, candidate) {
|
|
432
|
+
const pending = this.#pending.get(requestId)
|
|
433
|
+
if (!pending) return
|
|
434
|
+
try {
|
|
435
|
+
const row = WorkerResponseData.progress(
|
|
436
|
+
candidate,
|
|
437
|
+
pending.previousProgress
|
|
438
|
+
)
|
|
439
|
+
pending.previousProgress = row
|
|
440
|
+
pending.onProgress?.(row)
|
|
441
|
+
} catch (error) {
|
|
442
|
+
this.#failActive(error)
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
/**
|
|
447
|
+
* Settles and removes one pending request.
|
|
448
|
+
* @param {string} requestId Request id.
|
|
449
|
+
* @param {'resolve' | 'reject'} action Settlement action.
|
|
450
|
+
* @param {unknown} value Settlement value.
|
|
451
|
+
* @param {boolean} [drain] Whether to continue queued work.
|
|
452
|
+
* @returns {void}
|
|
453
|
+
*/
|
|
454
|
+
#settle(requestId, action, value, drain = true) {
|
|
455
|
+
const pending = this.#pending.get(requestId)
|
|
456
|
+
if (!pending) return
|
|
457
|
+
this.#pending.delete(requestId)
|
|
458
|
+
this.#removeQueued(requestId)
|
|
459
|
+
if (this.#activeRequestId === requestId) {
|
|
460
|
+
this.#activeRequestId = null
|
|
461
|
+
}
|
|
462
|
+
ParserWorkerClient.#removeAbortListener(pending)
|
|
463
|
+
pending[action](value)
|
|
464
|
+
if (drain) this.#drain()
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/**
|
|
468
|
+
* Appends one request id to the constant-time pending queue.
|
|
469
|
+
* @param {string} requestId Request id.
|
|
470
|
+
* @returns {void}
|
|
471
|
+
*/
|
|
472
|
+
#enqueue(requestId) {
|
|
473
|
+
const node = { requestId, previous: this.#queueTail, next: null }
|
|
474
|
+
if (this.#queueTail) this.#queueTail.next = node
|
|
475
|
+
else this.#queueHead = node
|
|
476
|
+
this.#queueTail = node
|
|
477
|
+
this.#queued.set(requestId, node)
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Removes and returns the next queued request id.
|
|
482
|
+
* @returns {string | null} Next request id.
|
|
483
|
+
*/
|
|
484
|
+
#dequeue() {
|
|
485
|
+
const node = this.#queueHead
|
|
486
|
+
if (!node) return null
|
|
487
|
+
this.#unlinkQueued(node)
|
|
488
|
+
return node.requestId
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Removes one cancelled request from the pending queue in constant time.
|
|
493
|
+
* @param {string} requestId Request id.
|
|
494
|
+
* @returns {void}
|
|
495
|
+
*/
|
|
496
|
+
#removeQueued(requestId) {
|
|
497
|
+
const node = this.#queued.get(requestId)
|
|
498
|
+
if (node) this.#unlinkQueued(node)
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
/**
|
|
502
|
+
* Unlinks one known queue node.
|
|
503
|
+
* @param {{ requestId: string, previous: object | null, next: object | null }} node Queue node.
|
|
504
|
+
* @returns {void}
|
|
505
|
+
*/
|
|
506
|
+
#unlinkQueued(node) {
|
|
507
|
+
if (node.previous) node.previous.next = node.next
|
|
508
|
+
else this.#queueHead = node.next
|
|
509
|
+
if (node.next) node.next.previous = node.previous
|
|
510
|
+
else this.#queueTail = node.previous
|
|
511
|
+
this.#queued.delete(node.requestId)
|
|
512
|
+
node.previous = null
|
|
513
|
+
node.next = null
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* Rejects the active request, replaces the failed worker, and continues.
|
|
518
|
+
* @param {unknown} error Active request failure.
|
|
519
|
+
* @returns {void}
|
|
520
|
+
*/
|
|
521
|
+
#failActive(error) {
|
|
522
|
+
const requestId = this.#activeRequestId
|
|
523
|
+
if (requestId) {
|
|
524
|
+
this.#settle(requestId, 'reject', error, false)
|
|
525
|
+
}
|
|
526
|
+
this.#resetWorker()
|
|
527
|
+
this.#drain()
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Detaches listeners and terminates only the current worker instance.
|
|
532
|
+
* @returns {void}
|
|
533
|
+
*/
|
|
534
|
+
#resetWorker() {
|
|
535
|
+
if (this.#worker && this.#removeEventListener) {
|
|
536
|
+
this.#removeEventListener('message', this.#onMessage)
|
|
537
|
+
this.#removeEventListener('error', this.#onError)
|
|
538
|
+
this.#removeEventListener('messageerror', this.#onMessageError)
|
|
539
|
+
}
|
|
540
|
+
try {
|
|
541
|
+
this.#terminateWorker?.()
|
|
542
|
+
} catch {
|
|
543
|
+
// The worker is discarded even if a host terminate hook fails.
|
|
544
|
+
}
|
|
545
|
+
this.#worker = null
|
|
546
|
+
this.#postMessage = null
|
|
547
|
+
this.#removeEventListener = null
|
|
548
|
+
this.#terminateWorker = null
|
|
549
|
+
this.#onMessage = null
|
|
550
|
+
this.#onError = null
|
|
551
|
+
this.#onMessageError = null
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
/**
|
|
555
|
+
* Builds a parse payload and exact transfer list.
|
|
556
|
+
* @param {unknown} input Parser input.
|
|
557
|
+
* @param {unknown} options Parser options.
|
|
558
|
+
* @returns {{ payload: object, transfer: Transferable[], signal: AbortSignal | null, onProgress: Function | null }} Prepared payload.
|
|
559
|
+
*/
|
|
560
|
+
static #parsePayload(input, options) {
|
|
561
|
+
const fields = ParserWorkerClient.#record(input, 'Worker parser input')
|
|
562
|
+
if (!Object.hasOwn(fields, 'data')) {
|
|
563
|
+
throw new TypeError('Worker parser input requires data.')
|
|
564
|
+
}
|
|
565
|
+
const request = ParserWorkerClient.#selectFields(fields, [
|
|
566
|
+
'fileName',
|
|
567
|
+
'data',
|
|
568
|
+
'assets'
|
|
569
|
+
])
|
|
570
|
+
const preparedOptions = ParserWorkerClient.#options(options)
|
|
571
|
+
const prepared = WorkerRequestData.prepare(
|
|
572
|
+
{ input: request, options: preparedOptions.posted },
|
|
573
|
+
{ transferInput: preparedOptions.transferInput }
|
|
574
|
+
)
|
|
575
|
+
return {
|
|
576
|
+
payload: prepared.value,
|
|
577
|
+
transfer: prepared.transfer,
|
|
578
|
+
signal: preparedOptions.signal,
|
|
579
|
+
onProgress: preparedOptions.onProgress
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
/**
|
|
584
|
+
* Builds a project payload and exact transfer list.
|
|
585
|
+
* @param {unknown} entries Project entries.
|
|
586
|
+
* @param {unknown} options Project options.
|
|
587
|
+
* @returns {{ payload: object, transfer: Transferable[], signal: AbortSignal | null, onProgress: Function | null }} Prepared payload.
|
|
588
|
+
*/
|
|
589
|
+
static #projectPayload(entries, options) {
|
|
590
|
+
const values = ParserWorkerClient.#array(
|
|
591
|
+
entries,
|
|
592
|
+
'Worker project entries',
|
|
593
|
+
MAX_PROJECT_ENTRIES
|
|
594
|
+
)
|
|
595
|
+
const preparedOptions = ParserWorkerClient.#options(options)
|
|
596
|
+
const preparedEntries = values.map((entry) => {
|
|
597
|
+
const fields = ParserWorkerClient.#record(
|
|
598
|
+
entry,
|
|
599
|
+
'Worker project entry'
|
|
600
|
+
)
|
|
601
|
+
if (!Object.hasOwn(fields, 'data')) {
|
|
602
|
+
throw new TypeError('Worker project entry requires data.')
|
|
603
|
+
}
|
|
604
|
+
return ParserWorkerClient.#selectFields(fields, [
|
|
605
|
+
'name',
|
|
606
|
+
'data',
|
|
607
|
+
'assets',
|
|
608
|
+
'compressedByteLength',
|
|
609
|
+
'archiveDepth'
|
|
610
|
+
])
|
|
611
|
+
})
|
|
612
|
+
const prepared = WorkerRequestData.prepare(
|
|
613
|
+
{
|
|
614
|
+
entries: preparedEntries,
|
|
615
|
+
options: preparedOptions.posted
|
|
616
|
+
},
|
|
617
|
+
{ transferInput: preparedOptions.transferInput }
|
|
618
|
+
)
|
|
619
|
+
return {
|
|
620
|
+
payload: prepared.value,
|
|
621
|
+
transfer: prepared.transfer,
|
|
622
|
+
signal: preparedOptions.signal,
|
|
623
|
+
onProgress: preparedOptions.onProgress
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* Selects only fields consumed by the direct parser/project contract.
|
|
629
|
+
* @param {Record<string, any>} fields Input fields.
|
|
630
|
+
* @param {string[]} names Canonical field names.
|
|
631
|
+
* @returns {Record<string, any>} Selected data record.
|
|
632
|
+
*/
|
|
633
|
+
static #selectFields(fields, names) {
|
|
634
|
+
const selected = {}
|
|
635
|
+
for (const name of names) {
|
|
636
|
+
if (Object.hasOwn(fields, name)) selected[name] = fields[name]
|
|
637
|
+
}
|
|
638
|
+
return selected
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/**
|
|
642
|
+
* Separates runtime-only options from posted data.
|
|
643
|
+
* @param {unknown} options Options candidate.
|
|
644
|
+
* @returns {{ posted: object, signal: AbortSignal | null, onProgress: Function | null, transferInput: boolean }} Prepared options.
|
|
645
|
+
*/
|
|
646
|
+
static #options(options) {
|
|
647
|
+
const fields = ParserWorkerClient.#record(
|
|
648
|
+
options,
|
|
649
|
+
'Worker request options'
|
|
650
|
+
)
|
|
651
|
+
const signal = ParserWorkerClient.#signal(fields.signal)
|
|
652
|
+
const onProgress =
|
|
653
|
+
fields.onProgress === undefined ? null : fields.onProgress
|
|
654
|
+
if (onProgress !== null && typeof onProgress !== 'function') {
|
|
655
|
+
throw new TypeError('Worker onProgress must be a function.')
|
|
656
|
+
}
|
|
657
|
+
if (fields.retainSource === 'reference') {
|
|
658
|
+
throw new ToolkitError(
|
|
659
|
+
'Worker requests cannot retain caller source references.',
|
|
660
|
+
{
|
|
661
|
+
code: 'ERR_CAPABILITY_UNAVAILABLE',
|
|
662
|
+
category: 'unsupported',
|
|
663
|
+
details: { capability: 'parser.retainSource.reference' }
|
|
664
|
+
}
|
|
665
|
+
)
|
|
666
|
+
}
|
|
667
|
+
const posted = { ...fields }
|
|
668
|
+
delete posted.signal
|
|
669
|
+
delete posted.onProgress
|
|
670
|
+
delete posted.worker
|
|
671
|
+
delete posted.transferInput
|
|
672
|
+
return {
|
|
673
|
+
posted,
|
|
674
|
+
signal,
|
|
675
|
+
onProgress,
|
|
676
|
+
transferInput: fields.transferInput === true
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
/**
|
|
681
|
+
* Executes one exact default-client attempt and consumes only its own
|
|
682
|
+
* construction-failure authorization.
|
|
683
|
+
* @param {'parse' | 'loadProject'} operation Worker operation.
|
|
684
|
+
* @param {unknown} input Operation input.
|
|
685
|
+
* @param {unknown} options Operation options.
|
|
686
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, error: unknown, unavailable: boolean }>} Attempt result.
|
|
687
|
+
*/
|
|
688
|
+
static async #defaultAttempt(operation, input, options) {
|
|
689
|
+
const token = {}
|
|
690
|
+
try {
|
|
691
|
+
const client = ParserWorkerClient.#defaultClientFor(token)
|
|
692
|
+
return await client.#attempt(operation, input, options, token)
|
|
693
|
+
} catch (error) {
|
|
694
|
+
return ParserWorkerClient.#attemptFailure(error, token)
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
/**
|
|
699
|
+
* Executes one exact request and consumes only its own authorization.
|
|
700
|
+
* @param {'parse' | 'loadProject'} operation Worker operation.
|
|
701
|
+
* @param {unknown} input Operation input.
|
|
702
|
+
* @param {unknown} options Operation options.
|
|
703
|
+
* @param {object} [token] Request-scoped attempt identity.
|
|
704
|
+
* @returns {Promise<{ ok: true, value: object } | { ok: false, error: unknown, unavailable: boolean }>} Attempt result.
|
|
705
|
+
*/
|
|
706
|
+
async #attempt(operation, input, options, token = {}) {
|
|
707
|
+
try {
|
|
708
|
+
const prepared =
|
|
709
|
+
operation === 'parse'
|
|
710
|
+
? ParserWorkerClient.#parsePayload(input, options)
|
|
711
|
+
: ParserWorkerClient.#projectPayload(input, options)
|
|
712
|
+
const value = await this.#request(operation, prepared, token)
|
|
713
|
+
return { ok: true, value }
|
|
714
|
+
} catch (error) {
|
|
715
|
+
return ParserWorkerClient.#attemptFailure(error, token)
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
/** @param {unknown} error Failure. @param {object} token Attempt identity. @returns {{ ok: false, error: unknown, unavailable: boolean }} Attempt failure. */
|
|
720
|
+
static #attemptFailure(error, token) {
|
|
721
|
+
const unavailable =
|
|
722
|
+
Boolean(error && typeof error === 'object') &&
|
|
723
|
+
ATTEMPT_ERRORS.get(error) === token
|
|
724
|
+
if (unavailable) ATTEMPT_ERRORS.delete(error)
|
|
725
|
+
return { ok: false, error, unavailable }
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
/**
|
|
729
|
+
* Validates one genuine optional AbortSignal.
|
|
730
|
+
* @param {unknown} value Signal candidate.
|
|
731
|
+
* @returns {AbortSignal | null} Signal or null.
|
|
732
|
+
*/
|
|
733
|
+
static #signal(value) {
|
|
734
|
+
if (value === undefined || value === null) return null
|
|
735
|
+
try {
|
|
736
|
+
ABORTED_GETTER.call(value)
|
|
737
|
+
return value
|
|
738
|
+
} catch {
|
|
739
|
+
throw new TypeError('Worker signal must be an AbortSignal.')
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
/** @param {AbortSignal} signal Signal. @returns {boolean} Aborted state. */
|
|
744
|
+
static #isAborted(signal) {
|
|
745
|
+
return Boolean(ABORTED_GETTER.call(signal))
|
|
746
|
+
}
|
|
747
|
+
|
|
748
|
+
/**
|
|
749
|
+
* Removes a captured signal listener.
|
|
750
|
+
* @param {{ signal: AbortSignal | null, onAbort: Function | null }} pending Pending row.
|
|
751
|
+
* @returns {void}
|
|
752
|
+
*/
|
|
753
|
+
static #removeAbortListener(pending) {
|
|
754
|
+
if (!pending.signal || !pending.onAbort) return
|
|
755
|
+
REMOVE_EVENT_LISTENER.call(pending.signal, 'abort', pending.onAbort)
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
/**
|
|
759
|
+
* Reads one dense plain array through data descriptors.
|
|
760
|
+
* @param {unknown} value Array candidate.
|
|
761
|
+
* @param {string} label Human-readable label.
|
|
762
|
+
* @param {number} maximum Maximum length.
|
|
763
|
+
* @returns {any[]} Item values.
|
|
764
|
+
*/
|
|
765
|
+
static #array(value, label, maximum) {
|
|
766
|
+
RuntimeProxyBoundary.assert(value, label)
|
|
767
|
+
if (!Array.isArray(value)) {
|
|
768
|
+
throw new TypeError(`${label} must be an array.`)
|
|
769
|
+
}
|
|
770
|
+
let prototype
|
|
771
|
+
let keys
|
|
772
|
+
try {
|
|
773
|
+
prototype = Object.getPrototypeOf(value)
|
|
774
|
+
keys = Reflect.ownKeys(value)
|
|
775
|
+
} catch {
|
|
776
|
+
throw new TypeError(`${label} could not be inspected safely.`)
|
|
777
|
+
}
|
|
778
|
+
const length = Object.getOwnPropertyDescriptor(value, 'length')?.value
|
|
779
|
+
if (
|
|
780
|
+
prototype !== Array.prototype ||
|
|
781
|
+
!Number.isSafeInteger(length) ||
|
|
782
|
+
length < 0 ||
|
|
783
|
+
length > maximum ||
|
|
784
|
+
keys.length !== length + 1
|
|
785
|
+
) {
|
|
786
|
+
throw new TypeError(`${label} must be a bounded dense plain array.`)
|
|
787
|
+
}
|
|
788
|
+
const result = new Array(length)
|
|
789
|
+
for (let index = 0; index < length; index += 1) {
|
|
790
|
+
const descriptor = Object.getOwnPropertyDescriptor(
|
|
791
|
+
value,
|
|
792
|
+
String(index)
|
|
793
|
+
)
|
|
794
|
+
if (!descriptor || !Object.hasOwn(descriptor, 'value')) {
|
|
795
|
+
throw new TypeError(`${label} must contain data properties.`)
|
|
796
|
+
}
|
|
797
|
+
result[index] = descriptor.value
|
|
798
|
+
}
|
|
799
|
+
return result
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
/**
|
|
803
|
+
* Reads one accessor-free plain record.
|
|
804
|
+
* @param {unknown} value Record candidate.
|
|
805
|
+
* @param {string} label Human-readable label.
|
|
806
|
+
* @returns {Record<string, any>} Null-prototype field map.
|
|
807
|
+
*/
|
|
808
|
+
static #record(value, label) {
|
|
809
|
+
RuntimeProxyBoundary.assert(value, label)
|
|
810
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
811
|
+
throw new TypeError(`${label} must be a plain object.`)
|
|
812
|
+
}
|
|
813
|
+
let prototype
|
|
814
|
+
let descriptors
|
|
815
|
+
try {
|
|
816
|
+
prototype = Object.getPrototypeOf(value)
|
|
817
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
818
|
+
} catch {
|
|
819
|
+
throw new TypeError(`${label} could not be inspected safely.`)
|
|
820
|
+
}
|
|
821
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
822
|
+
throw new TypeError(`${label} must be a plain object.`)
|
|
823
|
+
}
|
|
824
|
+
const result = Object.create(null)
|
|
825
|
+
for (const key of Reflect.ownKeys(descriptors)) {
|
|
826
|
+
const descriptor = descriptors[key]
|
|
827
|
+
if (
|
|
828
|
+
typeof key !== 'string' ||
|
|
829
|
+
descriptor.get ||
|
|
830
|
+
descriptor.set ||
|
|
831
|
+
descriptor.enumerable !== true
|
|
832
|
+
) {
|
|
833
|
+
throw new TypeError(
|
|
834
|
+
`${label} may contain only enumerable data properties.`
|
|
835
|
+
)
|
|
836
|
+
}
|
|
837
|
+
result[key] = descriptor.value
|
|
838
|
+
}
|
|
839
|
+
return result
|
|
840
|
+
}
|
|
841
|
+
|
|
842
|
+
/**
|
|
843
|
+
* Resolves one callable method without reading callable properties.
|
|
844
|
+
* @param {unknown} target Method owner.
|
|
845
|
+
* @param {string} name Method name.
|
|
846
|
+
* @returns {Function | null} Invocation closure.
|
|
847
|
+
*/
|
|
848
|
+
static #method(target, name) {
|
|
849
|
+
if (!target || !['object', 'function'].includes(typeof target)) {
|
|
850
|
+
return null
|
|
851
|
+
}
|
|
852
|
+
let owner = target
|
|
853
|
+
for (let depth = 0; owner && depth < 16; depth += 1) {
|
|
854
|
+
let descriptor
|
|
855
|
+
try {
|
|
856
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, name)
|
|
857
|
+
owner = Object.getPrototypeOf(owner)
|
|
858
|
+
} catch {
|
|
859
|
+
return null
|
|
860
|
+
}
|
|
861
|
+
if (!descriptor) continue
|
|
862
|
+
if (descriptor.get || descriptor.set) return null
|
|
863
|
+
if (typeof descriptor.value !== 'function') return null
|
|
864
|
+
const method = descriptor.value
|
|
865
|
+
return (...args) => Reflect.apply(method, target, args)
|
|
866
|
+
}
|
|
867
|
+
return null
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
/**
|
|
871
|
+
* Reads a genuine platform event getter or an inherited data property.
|
|
872
|
+
* @param {unknown} event Event candidate.
|
|
873
|
+
* @param {string} name Field name.
|
|
874
|
+
* @param {Function | null} intrinsicGetter Captured platform getter.
|
|
875
|
+
* @returns {unknown} Safely read field or undefined.
|
|
876
|
+
*/
|
|
877
|
+
static #eventField(event, name, intrinsicGetter) {
|
|
878
|
+
RuntimeProxyBoundary.assert(event, 'Parser worker event')
|
|
879
|
+
if (!event || !['object', 'function'].includes(typeof event)) {
|
|
880
|
+
return undefined
|
|
881
|
+
}
|
|
882
|
+
if (intrinsicGetter) {
|
|
883
|
+
try {
|
|
884
|
+
return Reflect.apply(intrinsicGetter, event, [])
|
|
885
|
+
} catch {
|
|
886
|
+
// Plain test/event records are handled by descriptors below.
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
let owner = event
|
|
890
|
+
for (let depth = 0; owner && depth < 16; depth += 1) {
|
|
891
|
+
let descriptor
|
|
892
|
+
try {
|
|
893
|
+
descriptor = Object.getOwnPropertyDescriptor(owner, name)
|
|
894
|
+
owner = Object.getPrototypeOf(owner)
|
|
895
|
+
} catch {
|
|
896
|
+
return undefined
|
|
897
|
+
}
|
|
898
|
+
if (!descriptor) continue
|
|
899
|
+
return Object.hasOwn(descriptor, 'value')
|
|
900
|
+
? descriptor.value
|
|
901
|
+
: undefined
|
|
902
|
+
}
|
|
903
|
+
return undefined
|
|
904
|
+
}
|
|
905
|
+
|
|
906
|
+
/** @returns {ToolkitError} Disposed-client failure. */
|
|
907
|
+
static #disposedError() {
|
|
908
|
+
return new ToolkitError('Parser worker client is disposed.', {
|
|
909
|
+
code: 'ERR_WORKER_DISPOSED',
|
|
910
|
+
category: 'runtime'
|
|
911
|
+
})
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
/** @returns {ToolkitError} Bounded-queue failure. */
|
|
915
|
+
static #queueError() {
|
|
916
|
+
return new ToolkitError('Parser worker request queue is full.', {
|
|
917
|
+
code: 'ERR_WORKER_QUEUE_LIMIT',
|
|
918
|
+
category: 'validation',
|
|
919
|
+
details: { maximum: MAX_PENDING_REQUESTS }
|
|
920
|
+
})
|
|
921
|
+
}
|
|
922
|
+
|
|
923
|
+
/** @param {string} requestId Request id. @returns {ToolkitError} Error. */
|
|
924
|
+
static #cancelledError(requestId) {
|
|
925
|
+
return new ToolkitError('Toolkit worker request was cancelled.', {
|
|
926
|
+
code: 'ERR_CANCELLED',
|
|
927
|
+
category: 'cancelled',
|
|
928
|
+
details: requestId ? { requestId } : {}
|
|
929
|
+
})
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
/**
|
|
933
|
+
* Creates an invalid-request error.
|
|
934
|
+
* @param {unknown} cause Failure cause.
|
|
935
|
+
* @param {'construct' | 'post'} phase Failed request phase.
|
|
936
|
+
* @returns {ToolkitError} Typed error.
|
|
937
|
+
*/
|
|
938
|
+
static #requestError(cause, phase) {
|
|
939
|
+
const error = new ToolkitError(
|
|
940
|
+
'Toolkit worker request could not be posted.',
|
|
941
|
+
{
|
|
942
|
+
code: 'ERR_WORKER_REQUEST',
|
|
943
|
+
category: 'validation',
|
|
944
|
+
details: { phase },
|
|
945
|
+
cause
|
|
946
|
+
}
|
|
947
|
+
)
|
|
948
|
+
return error
|
|
949
|
+
}
|
|
950
|
+
|
|
951
|
+
/**
|
|
952
|
+
* Creates one one-shot default-worker availability authorization.
|
|
953
|
+
* @param {object | null} attemptToken Internal attempt identity.
|
|
954
|
+
* @returns {ToolkitError} Typed unavailable error.
|
|
955
|
+
*/
|
|
956
|
+
static #unavailableError(attemptToken) {
|
|
957
|
+
const error = new ToolkitError(
|
|
958
|
+
'Toolkit parser workers are not available in this host.',
|
|
959
|
+
{
|
|
960
|
+
code: 'ERR_CAPABILITY_UNAVAILABLE',
|
|
961
|
+
category: 'unsupported',
|
|
962
|
+
details: { capability: 'parser.worker' }
|
|
963
|
+
}
|
|
964
|
+
)
|
|
965
|
+
if (attemptToken) {
|
|
966
|
+
ATTEMPT_ERRORS.set(error, attemptToken)
|
|
967
|
+
}
|
|
968
|
+
return error
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Creates a worker runtime error.
|
|
973
|
+
* @param {unknown} cause Failure cause.
|
|
974
|
+
* @returns {ToolkitError} Typed error.
|
|
975
|
+
*/
|
|
976
|
+
static #runtimeError(cause) {
|
|
977
|
+
return new ToolkitError('Toolkit parser worker failed.', {
|
|
978
|
+
code: 'ERR_WORKER_RUNTIME',
|
|
979
|
+
category: 'runtime',
|
|
980
|
+
cause
|
|
981
|
+
})
|
|
982
|
+
}
|
|
983
|
+
|
|
984
|
+
/**
|
|
985
|
+
* Creates a malformed-protocol error.
|
|
986
|
+
* @param {string} message Failure message.
|
|
987
|
+
* @param {unknown} [cause] Failure cause.
|
|
988
|
+
* @returns {ToolkitError} Typed error.
|
|
989
|
+
*/
|
|
990
|
+
static #protocolError(message, cause = null) {
|
|
991
|
+
return new ToolkitError(message, {
|
|
992
|
+
code: 'ERR_WORKER_MESSAGE',
|
|
993
|
+
category: 'runtime',
|
|
994
|
+
cause
|
|
995
|
+
})
|
|
996
|
+
}
|
|
997
|
+
}
|