circuitjson-toolkit 1.0.17 → 1.1.1

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