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.
Files changed (155) hide show
  1. package/AGENTS.md +15 -0
  2. package/NOTICE.md +31 -0
  3. package/README.md +267 -107
  4. package/docs/api.md +501 -96
  5. package/docs/capabilities.md +70 -0
  6. package/docs/migration/behaviors.md +45 -0
  7. package/docs/migration/parser.md +60 -0
  8. package/docs/migration/renderers.md +515 -0
  9. package/docs/migration/root.md +740 -0
  10. package/docs/migration.md +120 -0
  11. package/docs/model-format.md +175 -57
  12. package/docs/provenance.md +206 -0
  13. package/docs/release-notes-v1.1.0.md +154 -0
  14. package/docs/testing.md +117 -7
  15. package/package.json +31 -5
  16. package/spec/api-baseline-v1.0.17.json +1 -0
  17. package/spec/baseline-provenance-v1.0.17.json +7 -0
  18. package/spec/circuitjson-schema-snapshot.json +321 -0
  19. package/spec/circuitjson-schema-source.json +28 -0
  20. package/spec/feature-preservation.json +1 -0
  21. package/spec/library-scope.md +27 -20
  22. package/src/capabilities.mjs +1 -0
  23. package/src/core/ArchiveEntryPath.mjs +93 -0
  24. package/src/core/ArchiveLimits.mjs +31 -0
  25. package/src/core/ArchiveLimitsValidator.mjs +107 -0
  26. package/src/core/AsyncInputOwnership.mjs +56 -0
  27. package/src/core/AttachedValueLimits.mjs +67 -0
  28. package/src/core/CircuitJsonDiagnosticIndexer.mjs +184 -0
  29. package/src/core/CircuitJsonDocument.mjs +19 -61
  30. package/src/core/CircuitJsonElementTypes.mjs +10 -0
  31. package/src/core/CircuitJsonElementValidator.mjs +98 -847
  32. package/src/core/CircuitJsonIndexer.mjs +274 -194
  33. package/src/core/CircuitJsonManufacturingBuilder.mjs +167 -164
  34. package/src/core/CircuitJsonParser.mjs +75 -13
  35. package/src/core/CircuitJsonPcbClearanceDiagnostics.mjs +12 -6
  36. package/src/core/CircuitJsonPcbHolePrimitiveModel.mjs +108 -10
  37. package/src/core/CircuitJsonPcbPadPrimitiveModel.mjs +1 -1
  38. package/src/core/CircuitJsonPcbPrimitiveArtwork.mjs +44 -38
  39. package/src/core/CircuitJsonPcbPrimitiveBuilder.mjs +146 -28
  40. package/src/core/CircuitJsonPcbPrimitiveFields.mjs +70 -4
  41. package/src/core/CircuitJsonPcbPrimitiveIndex.mjs +18 -2
  42. package/src/core/CircuitJsonPcbPrimitiveOverlays.mjs +26 -9
  43. package/src/core/CircuitJsonPcbZonePrimitiveBuilder.mjs +7 -6
  44. package/src/core/CircuitJsonSerializedInputAudit.mjs +87 -0
  45. package/src/core/CircuitJsonSourceMetadata.mjs +5 -1
  46. package/src/core/CircuitJsonSupportMatrixBuilder.mjs +3 -1
  47. package/src/core/CircuitJsonToolkitElementSchema.mjs +218 -0
  48. package/src/core/CircuitJsonUnitParsers.mjs +101 -0
  49. package/src/core/CircuitJsonUnits.mjs +13 -87
  50. package/src/core/CircuitJsonUpstreamSchema.mjs +9 -0
  51. package/src/core/CircuitJsonUpstreamValidator.mjs +418 -0
  52. package/src/core/CircuitJsonValidationUnits.mjs +6 -0
  53. package/src/core/ManufacturingService.mjs +323 -0
  54. package/src/core/Parser.mjs +343 -0
  55. package/src/core/ParserOptions.mjs +333 -0
  56. package/src/core/PcbBoundsSelectionModel.mjs +55 -19
  57. package/src/core/PcbDiagnosticFocusModel.mjs +42 -11
  58. package/src/core/PcbInteractionIndex.mjs +368 -0
  59. package/src/core/PcbInteractionPrimitiveModel.mjs +393 -62
  60. package/src/core/ProjectAsyncInputOwner.mjs +70 -0
  61. package/src/core/ProjectLoader.mjs +975 -0
  62. package/src/core/SimulationService.mjs +790 -0
  63. package/src/core/ToolkitCapabilities.mjs +130 -0
  64. package/src/core/ZipArchiveInspector.mjs +649 -0
  65. package/src/core/context/BinaryDataSnapshot.mjs +217 -0
  66. package/src/core/context/CircuitJsonContextIndexes.mjs +96 -0
  67. package/src/core/context/CircuitJsonDerivedCache.mjs +114 -0
  68. package/src/core/context/CircuitJsonDocumentContext.mjs +353 -0
  69. package/src/core/context/CircuitJsonLegacyModel.mjs +147 -0
  70. package/src/core/context/CircuitJsonLegacyNormalizer.mjs +847 -0
  71. package/src/core/context/CircuitJsonMetadataBoundary.mjs +76 -0
  72. package/src/core/context/CircuitJsonModelFreezeTraversal.mjs +179 -0
  73. package/src/core/context/CircuitJsonReadOnlyDocument.mjs +920 -0
  74. package/src/core/context/CircuitJsonSchematicTableNormalizer.mjs +314 -0
  75. package/src/core/context/CircuitJsonValidationAuthority.mjs +39 -0
  76. package/src/core/context/CircuitJsonValidationProof.mjs +217 -0
  77. package/src/core/context/PcbPrimitivePreparation.mjs +198 -0
  78. package/src/core/context/PcbSpatialIndex.mjs +701 -0
  79. package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +128 -0
  80. package/src/core/context/StructuredDataSnapshot.mjs +683 -0
  81. package/src/core/contracts/DocumentResult.mjs +198 -0
  82. package/src/core/contracts/ProjectResult.mjs +96 -0
  83. package/src/core/contracts/RuntimeProxyBoundary.mjs +48 -0
  84. package/src/core/contracts/ToolkitAsset.mjs +493 -0
  85. package/src/core/contracts/ToolkitDiagnostic.mjs +38 -0
  86. package/src/core/contracts/ToolkitError.mjs +176 -0
  87. package/src/core/contracts/ToolkitProgress.mjs +89 -0
  88. package/src/core/interaction/CanonicalInteractionOptions.mjs +246 -0
  89. package/src/core/interaction/PcbInteractionBounds.mjs +167 -0
  90. package/src/core/query/CircuitTraversal.mjs +343 -0
  91. package/src/core/query/ComponentGrouping.mjs +275 -0
  92. package/src/core/query/QueryNetlistBuilder.mjs +306 -0
  93. package/src/core/query/QueryService.mjs +435 -0
  94. package/src/core/query/RegexPattern.mjs +75 -0
  95. package/src/core/rendering/CanonicalBomOrder.mjs +81 -0
  96. package/src/core/rendering/CanonicalBomRows.mjs +92 -0
  97. package/src/core/rendering/CanonicalRenderOptions.mjs +498 -0
  98. package/src/core/rendering/CanonicalSvgDocument.mjs +102 -0
  99. package/src/core/rendering/PcbRenderPlan.mjs +429 -0
  100. package/src/core/rendering/SchematicSheetSelector.mjs +335 -0
  101. package/src/core/scene3d/PcbScene3dBuilder.mjs +906 -0
  102. package/src/core/scene3d/PcbScene3dPreparator.mjs +47 -0
  103. package/src/core/scene3d/Scene3dAssetIndex.mjs +284 -0
  104. package/src/core/scene3d/Scene3dBoardModel.mjs +596 -0
  105. package/src/core/scene3d/Scene3dDocumentMetadata.mjs +167 -0
  106. package/src/core/scene3d/Scene3dFreeze.mjs +37 -0
  107. package/src/core/scene3d/Scene3dIdRegistry.mjs +34 -0
  108. package/src/core/scene3d/Scene3dInputPreflight.mjs +193 -0
  109. package/src/core/scene3d/Scene3dMaterials.mjs +58 -0
  110. package/src/core/scene3d/Scene3dModelReference.mjs +134 -0
  111. package/src/core/scene3d/Scene3dOptions.mjs +363 -0
  112. package/src/core/scene3d/SceneAssetResolver.mjs +441 -0
  113. package/src/core/simulation/SimulationParameterCloner.mjs +543 -0
  114. package/src/core/worker/ParserWorkerClient.mjs +997 -0
  115. package/src/core/worker/ToolkitWorkerProtocol.mjs +412 -0
  116. package/src/core/worker/WorkerRequestData.mjs +623 -0
  117. package/src/core/worker/WorkerResponseData.mjs +905 -0
  118. package/src/extensions.mjs +37 -0
  119. package/src/index.mjs +14 -9
  120. package/src/interaction.mjs +2 -0
  121. package/src/manufacturing.mjs +1 -0
  122. package/src/parser.mjs +12 -2
  123. package/src/project.mjs +5 -0
  124. package/src/query.mjs +1 -0
  125. package/src/renderers.mjs +3 -29
  126. package/src/scene3d.mjs +3 -0
  127. package/src/simulation.mjs +1 -0
  128. package/src/styles/renderers.css +24 -0
  129. package/src/testing/ToolkitContractFixtures.mjs +124 -0
  130. package/src/testing/ToolkitLoopbackWorker.mjs +174 -0
  131. package/src/testing/runToolkitContract.mjs +705 -0
  132. package/src/testing.mjs +3 -0
  133. package/src/ui/BomTableRenderer.mjs +304 -0
  134. package/src/ui/CircuitJsonPcbBoardSvgRenderer.mjs +80 -0
  135. package/src/ui/CircuitJsonPcbPrimitiveAttributeRenderer.mjs +3 -5
  136. package/src/ui/CircuitJsonPcbSvgRenderer.mjs +63 -43
  137. package/src/ui/CircuitJsonPcbViaSvgRenderer.mjs +3 -5
  138. package/src/ui/CircuitJsonSchematicDebugRenderer.mjs +164 -0
  139. package/src/ui/CircuitJsonSchematicImageSvgRenderer.mjs +210 -0
  140. package/src/ui/CircuitJsonSchematicLineRenderer.mjs +86 -0
  141. package/src/ui/CircuitJsonSchematicSheetSymbolSvgRenderer.mjs +98 -0
  142. package/src/ui/CircuitJsonSchematicSvgArcPath.mjs +117 -17
  143. package/src/ui/CircuitJsonSchematicSvgPortMetadata.mjs +67 -20
  144. package/src/ui/CircuitJsonSchematicSvgPrimitiveAttributes.mjs +45 -9
  145. package/src/ui/CircuitJsonSchematicSvgRenderer.mjs +151 -148
  146. package/src/ui/CircuitJsonSchematicTableSvgRenderer.mjs +4 -292
  147. package/src/ui/PcbSvgRenderer.mjs +41 -0
  148. package/src/ui/SafeSvgPaint.mjs +26 -0
  149. package/src/ui/SafeXmlText.mjs +60 -0
  150. package/src/ui/SchematicGeometryBounds.mjs +540 -0
  151. package/src/ui/SchematicSvgRenderer.mjs +110 -0
  152. package/src/ui/SchematicTableGeometry.mjs +319 -0
  153. package/src/ui/SchematicTextAnchor.mjs +55 -0
  154. package/src/ui/SchematicTextBounds.mjs +98 -0
  155. 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
+ }