circuitjson-toolkit 1.2.0 → 1.2.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.
- package/README.md +16 -6
- package/docs/api.md +6 -5
- package/docs/model-format.md +12 -8
- package/docs/release-notes-v1.2.1.md +26 -0
- package/package.json +2 -1
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +1 -1
- package/src/core/worker/WorkerRequestData.mjs +401 -55
- package/src/core/worker/WorkerResultShape.mjs +222 -0
package/README.md
CHANGED
|
@@ -55,6 +55,12 @@ metadata ownership traversals now support deeply nested, valid ECAD documents
|
|
|
55
55
|
up to 256 levels. See the
|
|
56
56
|
[1.2.0 release notes](docs/release-notes-v1.2.0.md).
|
|
57
57
|
|
|
58
|
+
Version 1.2.1 keeps large canonical documents and multi-document projects
|
|
59
|
+
inside explicit worker budgets. Selected extensions may contain up to four
|
|
60
|
+
million structured items, while exact project envelopes receive independent
|
|
61
|
+
per-document, project-metadata, aggregate-byte, and repeated-work limits. See
|
|
62
|
+
the [1.2.1 release notes](docs/release-notes-v1.2.1.md).
|
|
63
|
+
|
|
58
64
|
Before 1.1.0:
|
|
59
65
|
|
|
60
66
|
```js
|
|
@@ -100,7 +106,7 @@ const model = document.model
|
|
|
100
106
|
- Strict `ecad-toolkit.worker.v1` parsing/project protocol with progress,
|
|
101
107
|
cancellation, and controlled buffer transfer
|
|
102
108
|
- One-pass ownership for selected source extensions, with a separate 128 MiB /
|
|
103
|
-
|
|
109
|
+
4,000,000-item bound and exact direct/worker result parity
|
|
104
110
|
- Machine-readable capability inventory and packed downstream conformance
|
|
105
111
|
harness
|
|
106
112
|
- Explicit `/extensions` surface retaining every previous specialized API
|
|
@@ -201,11 +207,14 @@ falls back to direct execution only when worker construction is unavailable;
|
|
|
201
207
|
explicit worker and runtime failures remain visible.
|
|
202
208
|
|
|
203
209
|
Selected source-native extensions are captured once into an immutable owned
|
|
204
|
-
snapshot. Their separate 128 MiB payload and
|
|
205
|
-
realistic renderer/model graphs
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
210
|
+
snapshot. Their separate 128 MiB payload and 4,000,000-item ceilings permit
|
|
211
|
+
realistic renderer/model graphs. Canonical standalone documents remain within
|
|
212
|
+
a 250 MB result ceiling, while exact multi-document project envelopes use a
|
|
213
|
+
256 MiB aggregate ceiling plus independent 250 MB per-document and metadata
|
|
214
|
+
ceilings. Item and repeated-alias work budgets remain separately bounded; an
|
|
215
|
+
over-limit extension or result fails visibly instead of being silently
|
|
216
|
+
truncated. Binary extension values remain byte-backed and return defensive
|
|
217
|
+
copies instead of expanding into JavaScript number arrays.
|
|
209
218
|
When extensions are disabled, native documents and projects return the exact
|
|
210
219
|
empty map `{}`. Bounded ZIP consumers can preflight local/central filenames and
|
|
211
220
|
CRC32/size metadata with
|
|
@@ -319,6 +328,7 @@ copy while keeping sync, direct async, and worker results mutation-isolated.
|
|
|
319
328
|
- [1.1.1 release notes](docs/release-notes-v1.1.1.md)
|
|
320
329
|
- [1.1.2 release notes](docs/release-notes-v1.1.2.md)
|
|
321
330
|
- [1.2.0 release notes](docs/release-notes-v1.2.0.md)
|
|
331
|
+
- [1.2.1 release notes](docs/release-notes-v1.2.1.md)
|
|
322
332
|
- [Library scope](spec/library-scope.md)
|
|
323
333
|
|
|
324
334
|
## Package scope
|
package/docs/api.md
CHANGED
|
@@ -53,12 +53,13 @@ make the common shape smaller.
|
|
|
53
53
|
|
|
54
54
|
Validated source documents own selected extension values as one deeply
|
|
55
55
|
immutable, descriptor-safe snapshot. This uses a distinct 128 MiB payload and
|
|
56
|
-
|
|
56
|
+
4,000,000-item ceiling so large native renderer graphs do not inherit the
|
|
57
57
|
compact metadata limit. The shared worker protocol applies the same ownership
|
|
58
|
-
boundary after structured cloning
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
boundary after structured cloning. Canonical standalone documents retain a
|
|
59
|
+
250 MB byte ceiling; exact multi-document projects use a 256 MiB aggregate
|
|
60
|
+
ceiling with independent 250 MB document and project-metadata limits. Binary
|
|
61
|
+
values preserve their `ArrayBuffer`/typed-view shape behind defensive copy
|
|
62
|
+
access, so payload bytes do not inflate into one JavaScript number per byte.
|
|
62
63
|
|
|
63
64
|
### Errors
|
|
64
65
|
|
package/docs/model-format.md
CHANGED
|
@@ -102,15 +102,19 @@ must not duplicate the CircuitJSON model or keep a renamed renderer model.
|
|
|
102
102
|
Default parsing excludes raw/base64/full-payload graphs.
|
|
103
103
|
|
|
104
104
|
When a caller explicitly selects a native extension, the toolkit captures and
|
|
105
|
-
freezes that graph once under a distinct ceiling of
|
|
105
|
+
freezes that graph once under a distinct ceiling of 4,000,000 structured items
|
|
106
106
|
and 128 MiB of string or binary content. The ceiling is shared across the
|
|
107
|
-
selected namespace
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
107
|
+
selected namespace and applies equally after worker transfer. Canonical
|
|
108
|
+
standalone documents allow the extension plus envelope overhead within five
|
|
109
|
+
million result values and 250 MB. Exact project envelopes allow eight million
|
|
110
|
+
aggregate values and 256 MiB, while each document stays within five million
|
|
111
|
+
values and 250 MB and non-document project metadata stays within two million
|
|
112
|
+
values and 250 MB. Reused graphs are charged independently to each local scope,
|
|
113
|
+
and repeated accounting work is itself bounded by the aggregate project
|
|
114
|
+
budget. Oversized graphs are rejected; they are never truncated or partially
|
|
115
|
+
retained. Binary extension values remain byte-backed with their common
|
|
116
|
+
buffer/view type and are exposed as defensive copies, preserving mutation
|
|
117
|
+
isolation without expanding bytes into plain numeric arrays.
|
|
114
118
|
|
|
115
119
|
Supported pre-union CircuitJSON aliases may be projected through
|
|
116
120
|
`CircuitJsonDocument.normalizeModel()`. The copy-on-write form preserves exact
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# circuitjson-toolkit 1.2.1
|
|
2
|
+
|
|
3
|
+
## Large canonical worker results
|
|
4
|
+
|
|
5
|
+
This patch release keeps valid high-fidelity documents and multi-document
|
|
6
|
+
projects inside explicit, format-neutral worker limits:
|
|
7
|
+
|
|
8
|
+
- Selected native extensions may contain up to 4,000,000 structured items and
|
|
9
|
+
128 MiB of string or binary content.
|
|
10
|
+
- A canonical standalone document may contain up to 5,000,000 result values
|
|
11
|
+
within the existing 250 MB byte ceiling.
|
|
12
|
+
- An exact canonical project may contain up to 8,000,000 aggregate values and
|
|
13
|
+
256 MiB, while every document retains its own 5,000,000-value and 250 MB
|
|
14
|
+
limits.
|
|
15
|
+
- Non-document project metadata retains the generic 2,000,000-value and 250 MB
|
|
16
|
+
limits.
|
|
17
|
+
|
|
18
|
+
Elevated budgets require the complete canonical document or project envelope;
|
|
19
|
+
a schema string alone does not change generic result limits. Reused object
|
|
20
|
+
graphs are charged independently to document and project-metadata scopes in a
|
|
21
|
+
property-order-independent way. Repeated alias-accounting work is also capped
|
|
22
|
+
by the project aggregate budget, keeping transport time bounded.
|
|
23
|
+
|
|
24
|
+
No public class, method, package subpath, parameter, or result field is removed
|
|
25
|
+
or renamed. Oversized results continue to fail visibly rather than being
|
|
26
|
+
truncated.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "circuitjson-toolkit",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.1",
|
|
4
4
|
"description": "Canonical CircuitJSON parsing, project, rendering, query, manufacturing, simulation, and scene contracts",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"circuitjson",
|
|
@@ -54,6 +54,7 @@
|
|
|
54
54
|
"docs/release-notes-v1.1.1.md",
|
|
55
55
|
"docs/release-notes-v1.1.2.md",
|
|
56
56
|
"docs/release-notes-v1.2.0.md",
|
|
57
|
+
"docs/release-notes-v1.2.1.md",
|
|
57
58
|
"docs/testing.md",
|
|
58
59
|
"spec",
|
|
59
60
|
"LICENSE",
|
|
@@ -13,7 +13,7 @@ const METADATA_BUDGETS = new WeakMap()
|
|
|
13
13
|
const EXTENSION_METADATA_LIMITS = Object.freeze({
|
|
14
14
|
label: 'Canonical extension data',
|
|
15
15
|
maxBytes: 128 * 1024 * 1024,
|
|
16
|
-
maxItems:
|
|
16
|
+
maxItems: 4_000_000,
|
|
17
17
|
preserveBinary: true
|
|
18
18
|
})
|
|
19
19
|
const ASSET_SCALAR_FIELDS = new Set([
|
|
@@ -2,6 +2,7 @@ import { CircuitJsonValidationProof } from '../context/CircuitJsonValidationProo
|
|
|
2
2
|
import { CircuitJsonReadOnlyDocument } from '../context/CircuitJsonReadOnlyDocument.mjs'
|
|
3
3
|
import { ProtectedExtensionBinaryBoundary } from '../context/ProtectedExtensionBinaryBoundary.mjs'
|
|
4
4
|
import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
5
|
+
import { WorkerResultShape } from './WorkerResultShape.mjs'
|
|
5
6
|
|
|
6
7
|
const ARRAY_BUFFER_BYTE_LENGTH_GETTER = Object.getOwnPropertyDescriptor(
|
|
7
8
|
ArrayBuffer.prototype,
|
|
@@ -56,6 +57,11 @@ const MAX_REQUEST_DEPTH = 256
|
|
|
56
57
|
const MAX_REQUEST_VALUES = 100_000
|
|
57
58
|
const MAX_RESULT_BYTES = 250_000_000
|
|
58
59
|
const MAX_RESULT_VALUES = 2_000_000
|
|
60
|
+
const MAX_DOCUMENT_RESULT_VALUES = 5_000_000
|
|
61
|
+
// Project documents retain the single-result ceiling independently while this
|
|
62
|
+
// aggregate ceiling bounds total worker traversal regardless of document count.
|
|
63
|
+
const MAX_PROJECT_RESULT_VALUES = 8_000_000
|
|
64
|
+
const MAX_PROJECT_RESULT_BYTES = 256 * 1024 * 1024
|
|
59
65
|
const TYPED_ARRAYS = new Map(
|
|
60
66
|
[
|
|
61
67
|
Int8Array,
|
|
@@ -166,14 +172,28 @@ export class WorkerRequestData {
|
|
|
166
172
|
const state = {
|
|
167
173
|
bytes: 0,
|
|
168
174
|
copyBinary: limits.copyBinary,
|
|
175
|
+
documentAccounted: null,
|
|
176
|
+
documentBytes: null,
|
|
177
|
+
documentValues: null,
|
|
169
178
|
maxBytes: limits.bytes,
|
|
179
|
+
maxDocumentBytes: limits.bytes,
|
|
180
|
+
maxDocumentValues: MAX_DOCUMENT_RESULT_VALUES,
|
|
181
|
+
maxUnscopedBytes: limits.bytes,
|
|
182
|
+
maxUnscopedValues: limits.values,
|
|
170
183
|
maxValues: limits.values,
|
|
171
184
|
output: limits.output,
|
|
172
185
|
prepared: new WeakMap(),
|
|
186
|
+
projectDocuments: null,
|
|
187
|
+
projectResult: false,
|
|
188
|
+
reaccountValues: 0,
|
|
173
189
|
strictDescriptors: limits.strictDescriptors,
|
|
174
190
|
transfer: { items: [], seen: new Set() },
|
|
175
191
|
transferInput: limits.transferInput,
|
|
176
192
|
trustProof: limits.trustProof,
|
|
193
|
+
unscopedAccounted: null,
|
|
194
|
+
unscopedBytes: 0,
|
|
195
|
+
unscopedIncomplete: new WeakSet(),
|
|
196
|
+
unscopedValues: 0,
|
|
177
197
|
values: 0,
|
|
178
198
|
visiting: new WeakSet()
|
|
179
199
|
}
|
|
@@ -223,8 +243,26 @@ export class WorkerRequestData {
|
|
|
223
243
|
if (state.visiting.has(value)) {
|
|
224
244
|
throw new TypeError('Worker request data must not be cyclic.')
|
|
225
245
|
}
|
|
226
|
-
if (state.prepared.has(value))
|
|
246
|
+
if (state.prepared.has(value)) {
|
|
247
|
+
const accounted = WorkerRequestData.#accountedScope(state)
|
|
248
|
+
if (accounted && !accounted.has(value)) {
|
|
249
|
+
WorkerRequestData.#accountPreparedValue(value, state, depth)
|
|
250
|
+
} else if (
|
|
251
|
+
accounted === state.unscopedAccounted &&
|
|
252
|
+
state.unscopedIncomplete.delete(value)
|
|
253
|
+
) {
|
|
254
|
+
// The documents array itself was already charged unscoped.
|
|
255
|
+
WorkerRequestData.#accountPreparedContainer(
|
|
256
|
+
value,
|
|
257
|
+
state,
|
|
258
|
+
depth,
|
|
259
|
+
false
|
|
260
|
+
)
|
|
261
|
+
}
|
|
262
|
+
return state.prepared.get(value)
|
|
263
|
+
}
|
|
227
264
|
WorkerRequestData.#reserveValues(state, 1)
|
|
265
|
+
WorkerRequestData.#accountedScope(state)?.add(value)
|
|
228
266
|
state.visiting.add(value)
|
|
229
267
|
try {
|
|
230
268
|
const bufferLength = WorkerRequestData.#bufferLength(value)
|
|
@@ -305,18 +343,32 @@ export class WorkerRequestData {
|
|
|
305
343
|
'Worker requests may contain only plain data objects.'
|
|
306
344
|
)
|
|
307
345
|
}
|
|
308
|
-
const keys =
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
346
|
+
const keys = WorkerResultShape.keys(
|
|
347
|
+
descriptors,
|
|
348
|
+
state.output,
|
|
349
|
+
state.strictDescriptors
|
|
350
|
+
)
|
|
351
|
+
const projectResult =
|
|
352
|
+
depth === 0 &&
|
|
353
|
+
state.output &&
|
|
354
|
+
WorkerResultShape.project(descriptors, keys)
|
|
355
|
+
if (projectResult) {
|
|
356
|
+
state.maxBytes = MAX_PROJECT_RESULT_BYTES
|
|
357
|
+
state.maxValues = MAX_PROJECT_RESULT_VALUES
|
|
358
|
+
state.projectResult = true
|
|
359
|
+
state.unscopedAccounted = new WeakSet([value])
|
|
360
|
+
state.unscopedBytes = state.bytes
|
|
361
|
+
state.unscopedValues = state.values
|
|
362
|
+
}
|
|
363
|
+
const canonicalDocumentResult =
|
|
364
|
+
state.output && WorkerResultShape.document(descriptors, keys)
|
|
365
|
+
if (depth === 0 && canonicalDocumentResult) {
|
|
366
|
+
state.maxValues = MAX_DOCUMENT_RESULT_VALUES
|
|
367
|
+
}
|
|
368
|
+
const provenDocumentResult =
|
|
316
369
|
state.output &&
|
|
317
370
|
state.trustProof &&
|
|
318
|
-
|
|
319
|
-
'ecad-toolkit.document.v1' &&
|
|
371
|
+
canonicalDocumentResult &&
|
|
320
372
|
CircuitJsonValidationProof.has(value)
|
|
321
373
|
WorkerRequestData.#reserveValues(state, keys.length)
|
|
322
374
|
const prepared = prototype === null ? Object.create(null) : {}
|
|
@@ -330,13 +382,19 @@ export class WorkerRequestData {
|
|
|
330
382
|
configurable: true,
|
|
331
383
|
enumerable: true,
|
|
332
384
|
value:
|
|
333
|
-
|
|
334
|
-
? WorkerRequestData.#
|
|
335
|
-
: WorkerRequestData.#descriptorValue(
|
|
385
|
+
projectResult && key === 'documents'
|
|
386
|
+
? WorkerRequestData.#projectDocuments(
|
|
336
387
|
descriptors[key],
|
|
337
388
|
state,
|
|
338
389
|
depth
|
|
339
|
-
)
|
|
390
|
+
)
|
|
391
|
+
: provenDocumentResult && key === 'model'
|
|
392
|
+
? WorkerRequestData.#documentModel(descriptors[key])
|
|
393
|
+
: WorkerRequestData.#descriptorValue(
|
|
394
|
+
descriptors[key],
|
|
395
|
+
state,
|
|
396
|
+
depth
|
|
397
|
+
),
|
|
340
398
|
writable: true
|
|
341
399
|
})
|
|
342
400
|
}
|
|
@@ -353,40 +411,113 @@ export class WorkerRequestData {
|
|
|
353
411
|
* @returns {any[]} Prepared array.
|
|
354
412
|
*/
|
|
355
413
|
static #prepareArray(value, prototype, descriptors, state, depth) {
|
|
356
|
-
const
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
state.output
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
414
|
+
const length = WorkerResultShape.arrayLength(
|
|
415
|
+
prototype,
|
|
416
|
+
descriptors,
|
|
417
|
+
state.output,
|
|
418
|
+
state.strictDescriptors
|
|
419
|
+
)
|
|
420
|
+
WorkerRequestData.#reserveValues(state, length)
|
|
421
|
+
const prepared = new Array(length)
|
|
422
|
+
state.prepared.set(value, prepared)
|
|
423
|
+
const projectDocuments = state.projectDocuments === value
|
|
424
|
+
for (let index = 0; index < length; index += 1) {
|
|
425
|
+
prepared[index] = projectDocuments
|
|
426
|
+
? WorkerRequestData.#projectDocument(
|
|
427
|
+
descriptors[String(index)],
|
|
428
|
+
state,
|
|
429
|
+
depth
|
|
365
430
|
)
|
|
366
|
-
:
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
431
|
+
: WorkerRequestData.#descriptorValue(
|
|
432
|
+
descriptors[String(index)],
|
|
433
|
+
state,
|
|
434
|
+
depth
|
|
435
|
+
)
|
|
436
|
+
}
|
|
437
|
+
return prepared
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/** @param {PropertyDescriptor | undefined} descriptor Descriptor. @param {Record<string, any>} state State. @param {number} depth Depth. @returns {unknown} Documents. */
|
|
441
|
+
static #projectDocuments(descriptor, state, depth) {
|
|
442
|
+
const value = WorkerRequestData.#descriptorDataValue(descriptor, state)
|
|
443
|
+
const reused = state.prepared.has(value)
|
|
444
|
+
state.projectDocuments = value
|
|
445
|
+
try {
|
|
446
|
+
const prepared = WorkerRequestData.#prepareValue(
|
|
447
|
+
value,
|
|
448
|
+
state,
|
|
449
|
+
depth + 1
|
|
450
|
+
)
|
|
451
|
+
if (reused) {
|
|
452
|
+
WorkerRequestData.#accountProjectDocuments(
|
|
453
|
+
value,
|
|
454
|
+
state,
|
|
455
|
+
depth + 1
|
|
456
|
+
)
|
|
457
|
+
} else {
|
|
458
|
+
// Descendants were charged only to their document scopes.
|
|
459
|
+
state.unscopedIncomplete.add(value)
|
|
460
|
+
}
|
|
461
|
+
return prepared
|
|
462
|
+
} finally {
|
|
463
|
+
state.projectDocuments = null
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
/** @param {object} value Documents. @param {Record<string, any>} state State. @param {number} depth Depth. @returns {void} */
|
|
468
|
+
static #accountProjectDocuments(value, state, depth) {
|
|
469
|
+
let prototype
|
|
470
|
+
let descriptors
|
|
471
|
+
try {
|
|
472
|
+
prototype = Object.getPrototypeOf(value)
|
|
473
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
474
|
+
} catch {
|
|
375
475
|
throw new TypeError(
|
|
376
|
-
'Worker request
|
|
476
|
+
'Worker request data could not be inspected safely.'
|
|
377
477
|
)
|
|
378
478
|
}
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
479
|
+
const length = WorkerResultShape.arrayLength(
|
|
480
|
+
prototype,
|
|
481
|
+
descriptors,
|
|
482
|
+
state.output,
|
|
483
|
+
state.strictDescriptors
|
|
484
|
+
)
|
|
382
485
|
for (let index = 0; index < length; index += 1) {
|
|
383
|
-
|
|
486
|
+
WorkerRequestData.#projectDocument(
|
|
384
487
|
descriptors[String(index)],
|
|
385
488
|
state,
|
|
386
489
|
depth
|
|
387
490
|
)
|
|
388
491
|
}
|
|
389
|
-
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
/**
|
|
495
|
+
* Prepares one project document under the normal single-result ceiling.
|
|
496
|
+
* @param {PropertyDescriptor | undefined} descriptor Document descriptor.
|
|
497
|
+
* @param {Record<string, any>} state Traversal state.
|
|
498
|
+
* @param {number} depth Parent depth.
|
|
499
|
+
* @returns {unknown} Prepared document.
|
|
500
|
+
*/
|
|
501
|
+
static #projectDocument(descriptor, state, depth) {
|
|
502
|
+
const previousMaxDocumentValues = state.maxDocumentValues
|
|
503
|
+
const document = WorkerRequestData.#dataValue(descriptor)
|
|
504
|
+
state.maxDocumentValues = WorkerResultShape.documentCandidate(
|
|
505
|
+
document,
|
|
506
|
+
state.strictDescriptors
|
|
507
|
+
)
|
|
508
|
+
? MAX_DOCUMENT_RESULT_VALUES
|
|
509
|
+
: state.maxUnscopedValues
|
|
510
|
+
state.documentAccounted = new WeakSet()
|
|
511
|
+
state.documentBytes = 0
|
|
512
|
+
state.documentValues = 0
|
|
513
|
+
try {
|
|
514
|
+
return WorkerRequestData.#descriptorValue(descriptor, state, depth)
|
|
515
|
+
} finally {
|
|
516
|
+
state.documentAccounted = null
|
|
517
|
+
state.documentBytes = null
|
|
518
|
+
state.documentValues = null
|
|
519
|
+
state.maxDocumentValues = previousMaxDocumentValues
|
|
520
|
+
}
|
|
390
521
|
}
|
|
391
522
|
|
|
392
523
|
/**
|
|
@@ -397,24 +528,30 @@ export class WorkerRequestData {
|
|
|
397
528
|
* @returns {unknown} Prepared value.
|
|
398
529
|
*/
|
|
399
530
|
static #descriptorValue(descriptor, state, depth) {
|
|
531
|
+
return WorkerRequestData.#prepareValue(
|
|
532
|
+
WorkerRequestData.#descriptorDataValue(descriptor, state),
|
|
533
|
+
state,
|
|
534
|
+
depth + 1
|
|
535
|
+
)
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
/**
|
|
539
|
+
* Reads one enumerable descriptor through trusted internal boundaries only.
|
|
540
|
+
* @param {PropertyDescriptor | undefined} descriptor Descriptor candidate.
|
|
541
|
+
* @param {Record<string, any>} state Traversal state.
|
|
542
|
+
* @returns {unknown} Descriptor data value.
|
|
543
|
+
*/
|
|
544
|
+
static #descriptorDataValue(descriptor, state) {
|
|
400
545
|
if (state.trustProof) {
|
|
401
546
|
const protectedData =
|
|
402
547
|
CircuitJsonReadOnlyDocument.readProtectedAssetData(descriptor)
|
|
403
548
|
if (protectedData.trusted) {
|
|
404
|
-
return
|
|
405
|
-
protectedData.value,
|
|
406
|
-
state,
|
|
407
|
-
depth + 1
|
|
408
|
-
)
|
|
549
|
+
return protectedData.value
|
|
409
550
|
}
|
|
410
551
|
const protectedExtensionBinary =
|
|
411
552
|
ProtectedExtensionBinaryBoundary.read(descriptor)
|
|
412
553
|
if (protectedExtensionBinary.trusted) {
|
|
413
|
-
return
|
|
414
|
-
protectedExtensionBinary.value,
|
|
415
|
-
state,
|
|
416
|
-
depth + 1
|
|
417
|
-
)
|
|
554
|
+
return protectedExtensionBinary.value
|
|
418
555
|
}
|
|
419
556
|
}
|
|
420
557
|
if (
|
|
@@ -426,8 +563,138 @@ export class WorkerRequestData {
|
|
|
426
563
|
'Worker requests may contain only enumerable data properties.'
|
|
427
564
|
)
|
|
428
565
|
}
|
|
429
|
-
return
|
|
430
|
-
|
|
566
|
+
return descriptor.value
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Charges one previously prepared graph to the active project document.
|
|
571
|
+
* @param {unknown} value Reused prepared value.
|
|
572
|
+
* @param {Record<string, any>} state Traversal state.
|
|
573
|
+
* @param {number} depth Container depth.
|
|
574
|
+
* @returns {void}
|
|
575
|
+
*/
|
|
576
|
+
static #accountPreparedValue(value, state, depth) {
|
|
577
|
+
const type = typeof value
|
|
578
|
+
if (
|
|
579
|
+
value === null ||
|
|
580
|
+
['undefined', 'boolean', 'number', 'bigint'].includes(type)
|
|
581
|
+
) {
|
|
582
|
+
return
|
|
583
|
+
}
|
|
584
|
+
if (type === 'string') {
|
|
585
|
+
WorkerRequestData.#reserveAccountedBytes(state, value.length * 2)
|
|
586
|
+
return
|
|
587
|
+
}
|
|
588
|
+
if (type !== 'object') {
|
|
589
|
+
throw new TypeError(
|
|
590
|
+
'Worker requests may contain only clone-safe data.'
|
|
591
|
+
)
|
|
592
|
+
}
|
|
593
|
+
RuntimeProxyBoundary.assert(value, 'Worker request data')
|
|
594
|
+
if (depth > MAX_REQUEST_DEPTH) {
|
|
595
|
+
throw new TypeError('Worker request data is nested too deeply.')
|
|
596
|
+
}
|
|
597
|
+
const accounted = WorkerRequestData.#accountedScope(state)
|
|
598
|
+
if (accounted.has(value)) return
|
|
599
|
+
accounted.add(value)
|
|
600
|
+
WorkerRequestData.#reserveAccountedValues(state, 1)
|
|
601
|
+
const bufferLength = WorkerRequestData.#bufferLength(value)
|
|
602
|
+
if (bufferLength !== null) {
|
|
603
|
+
WorkerRequestData.#reserveAccountedBytes(state, bufferLength)
|
|
604
|
+
return
|
|
605
|
+
}
|
|
606
|
+
const sharedLength = WorkerRequestData.#sharedBufferLength(value)
|
|
607
|
+
if (sharedLength !== null) {
|
|
608
|
+
WorkerRequestData.#reserveAccountedBytes(state, sharedLength)
|
|
609
|
+
return
|
|
610
|
+
}
|
|
611
|
+
const view = WorkerRequestData.#view(value)
|
|
612
|
+
if (view) {
|
|
613
|
+
WorkerRequestData.#reserveAccountedBytes(state, view.byteLength)
|
|
614
|
+
return
|
|
615
|
+
}
|
|
616
|
+
WorkerRequestData.#accountPreparedContainer(value, state, depth)
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
/** @param {object} value Container. @param {Record<string, any>} state State. @param {number} depth Depth. @param {boolean} [accountEntries] Whether to charge its own entries. @returns {void} */
|
|
620
|
+
static #accountPreparedContainer(
|
|
621
|
+
value,
|
|
622
|
+
state,
|
|
623
|
+
depth,
|
|
624
|
+
accountEntries = true
|
|
625
|
+
) {
|
|
626
|
+
let prototype
|
|
627
|
+
let descriptors
|
|
628
|
+
try {
|
|
629
|
+
prototype = Object.getPrototypeOf(value)
|
|
630
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
631
|
+
} catch {
|
|
632
|
+
throw new TypeError(
|
|
633
|
+
'Worker request data could not be inspected safely.'
|
|
634
|
+
)
|
|
635
|
+
}
|
|
636
|
+
if (Array.isArray(value)) {
|
|
637
|
+
const length = WorkerResultShape.arrayLength(
|
|
638
|
+
prototype,
|
|
639
|
+
descriptors,
|
|
640
|
+
state.output,
|
|
641
|
+
state.strictDescriptors
|
|
642
|
+
)
|
|
643
|
+
if (accountEntries) {
|
|
644
|
+
WorkerRequestData.#reserveAccountedValues(state, length)
|
|
645
|
+
}
|
|
646
|
+
for (let index = 0; index < length; index += 1) {
|
|
647
|
+
WorkerRequestData.#accountDescriptor(
|
|
648
|
+
descriptors[String(index)],
|
|
649
|
+
state,
|
|
650
|
+
depth
|
|
651
|
+
)
|
|
652
|
+
}
|
|
653
|
+
return
|
|
654
|
+
}
|
|
655
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
656
|
+
throw new TypeError(
|
|
657
|
+
'Worker requests may contain only plain data objects.'
|
|
658
|
+
)
|
|
659
|
+
}
|
|
660
|
+
const keys = WorkerResultShape.keys(
|
|
661
|
+
descriptors,
|
|
662
|
+
state.output,
|
|
663
|
+
state.strictDescriptors
|
|
664
|
+
)
|
|
665
|
+
const provenDocumentResult =
|
|
666
|
+
state.output &&
|
|
667
|
+
state.trustProof &&
|
|
668
|
+
WorkerResultShape.document(descriptors, keys) &&
|
|
669
|
+
CircuitJsonValidationProof.has(value)
|
|
670
|
+
if (accountEntries) {
|
|
671
|
+
WorkerRequestData.#reserveAccountedValues(state, keys.length)
|
|
672
|
+
}
|
|
673
|
+
for (const key of keys) {
|
|
674
|
+
if (typeof key !== 'string') {
|
|
675
|
+
throw new TypeError('Worker request keys must be strings.')
|
|
676
|
+
}
|
|
677
|
+
WorkerRequestData.#reserveAccountedBytes(state, key.length * 2)
|
|
678
|
+
if (!(provenDocumentResult && key === 'model')) {
|
|
679
|
+
WorkerRequestData.#accountDescriptor(
|
|
680
|
+
descriptors[key],
|
|
681
|
+
state,
|
|
682
|
+
depth
|
|
683
|
+
)
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
/**
|
|
689
|
+
* Charges one reused descriptor to the active document.
|
|
690
|
+
* @param {PropertyDescriptor | undefined} descriptor Descriptor candidate.
|
|
691
|
+
* @param {Record<string, any>} state Traversal state.
|
|
692
|
+
* @param {number} depth Parent depth.
|
|
693
|
+
* @returns {void}
|
|
694
|
+
*/
|
|
695
|
+
static #accountDescriptor(descriptor, state, depth) {
|
|
696
|
+
WorkerRequestData.#accountPreparedValue(
|
|
697
|
+
WorkerRequestData.#descriptorDataValue(descriptor, state),
|
|
431
698
|
state,
|
|
432
699
|
depth + 1
|
|
433
700
|
)
|
|
@@ -624,27 +891,106 @@ export class WorkerRequestData {
|
|
|
624
891
|
transfer.items.push(value)
|
|
625
892
|
}
|
|
626
893
|
|
|
627
|
-
/** @param {
|
|
894
|
+
/** @param {Record<string, any>} state State. @returns {WeakSet<object> | null} Active identities. */
|
|
895
|
+
static #accountedScope(state) {
|
|
896
|
+
if (state.documentAccounted) return state.documentAccounted
|
|
897
|
+
return state.projectResult ? state.unscopedAccounted : null
|
|
898
|
+
}
|
|
899
|
+
|
|
900
|
+
/** @param {{ documentBytes: number | null, maxDocumentBytes: number, maxUnscopedBytes: number, projectResult: boolean, unscopedBytes: number }} state State. @param {number} count Bytes. */
|
|
901
|
+
static #reserveAccountedBytes(state, count) {
|
|
902
|
+
const invalid = !Number.isSafeInteger(count) || count < 0
|
|
903
|
+
if (state.documentBytes !== null) {
|
|
904
|
+
if (
|
|
905
|
+
invalid ||
|
|
906
|
+
state.documentBytes + count > state.maxDocumentBytes
|
|
907
|
+
) {
|
|
908
|
+
throw new TypeError(
|
|
909
|
+
'Worker request data exceeds its byte limit.'
|
|
910
|
+
)
|
|
911
|
+
}
|
|
912
|
+
state.documentBytes += count
|
|
913
|
+
return
|
|
914
|
+
}
|
|
915
|
+
if (
|
|
916
|
+
invalid ||
|
|
917
|
+
!state.projectResult ||
|
|
918
|
+
state.unscopedBytes + count > state.maxUnscopedBytes
|
|
919
|
+
) {
|
|
920
|
+
throw new TypeError('Worker request data exceeds its byte limit.')
|
|
921
|
+
}
|
|
922
|
+
state.unscopedBytes += count
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
/** @param {{ documentValues: number | null, maxDocumentValues: number, maxUnscopedValues: number, maxValues: number, projectResult: boolean, reaccountValues: number, unscopedValues: number }} state State. @param {number} count Values. */
|
|
926
|
+
static #reserveAccountedValues(state, count) {
|
|
927
|
+
const invalid =
|
|
928
|
+
!Number.isSafeInteger(count) ||
|
|
929
|
+
count < 0 ||
|
|
930
|
+
state.reaccountValues + count > state.maxValues
|
|
931
|
+
if (state.documentValues !== null) {
|
|
932
|
+
if (
|
|
933
|
+
invalid ||
|
|
934
|
+
state.documentValues + count > state.maxDocumentValues
|
|
935
|
+
) {
|
|
936
|
+
throw new TypeError('Worker request data is too large.')
|
|
937
|
+
}
|
|
938
|
+
state.documentValues += count
|
|
939
|
+
state.reaccountValues += count
|
|
940
|
+
return
|
|
941
|
+
}
|
|
942
|
+
if (
|
|
943
|
+
invalid ||
|
|
944
|
+
!state.projectResult ||
|
|
945
|
+
state.unscopedValues + count > state.maxUnscopedValues
|
|
946
|
+
) {
|
|
947
|
+
throw new TypeError('Worker request data is too large.')
|
|
948
|
+
}
|
|
949
|
+
state.unscopedValues += count
|
|
950
|
+
state.reaccountValues += count
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
/** @param {{ bytes: number, documentBytes: number | null, maxBytes: number, maxDocumentBytes: number, maxUnscopedBytes: number, projectResult: boolean, unscopedBytes: number }} state State. @param {number} count Bytes. */
|
|
628
954
|
static #reserveBytes(state, count) {
|
|
955
|
+
const documentBytes = state.documentBytes
|
|
629
956
|
if (
|
|
630
957
|
!Number.isSafeInteger(count) ||
|
|
631
958
|
count < 0 ||
|
|
632
|
-
state.bytes + count > state.maxBytes
|
|
959
|
+
state.bytes + count > state.maxBytes ||
|
|
960
|
+
(documentBytes !== null
|
|
961
|
+
? documentBytes + count > state.maxDocumentBytes
|
|
962
|
+
: state.projectResult &&
|
|
963
|
+
state.unscopedBytes + count > state.maxUnscopedBytes)
|
|
633
964
|
) {
|
|
634
965
|
throw new TypeError('Worker request data exceeds its byte limit.')
|
|
635
966
|
}
|
|
636
967
|
state.bytes += count
|
|
968
|
+
if (documentBytes !== null) {
|
|
969
|
+
state.documentBytes += count
|
|
970
|
+
} else if (state.projectResult) {
|
|
971
|
+
state.unscopedBytes += count
|
|
972
|
+
}
|
|
637
973
|
}
|
|
638
974
|
|
|
639
|
-
/** @param {{ values: number }} state State. @param {number} count Values. */
|
|
975
|
+
/** @param {{ documentValues: number | null, maxDocumentValues: number, maxUnscopedValues: number, maxValues: number, projectResult: boolean, unscopedValues: number, values: number }} state State. @param {number} count Values. */
|
|
640
976
|
static #reserveValues(state, count) {
|
|
977
|
+
const documentValues = state.documentValues
|
|
641
978
|
if (
|
|
642
979
|
!Number.isSafeInteger(count) ||
|
|
643
980
|
count < 0 ||
|
|
644
|
-
state.values + count > state.maxValues
|
|
981
|
+
state.values + count > state.maxValues ||
|
|
982
|
+
(documentValues !== null
|
|
983
|
+
? documentValues + count > state.maxDocumentValues
|
|
984
|
+
: state.projectResult &&
|
|
985
|
+
state.unscopedValues + count > state.maxUnscopedValues)
|
|
645
986
|
) {
|
|
646
987
|
throw new TypeError('Worker request data is too large.')
|
|
647
988
|
}
|
|
648
989
|
state.values += count
|
|
990
|
+
if (documentValues !== null) {
|
|
991
|
+
state.documentValues += count
|
|
992
|
+
} else if (state.projectResult) {
|
|
993
|
+
state.unscopedValues += count
|
|
994
|
+
}
|
|
649
995
|
}
|
|
650
996
|
}
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
import { RuntimeProxyBoundary } from '../contracts/RuntimeProxyBoundary.mjs'
|
|
2
|
+
|
|
3
|
+
const DOCUMENT_FIELDS = Object.freeze([
|
|
4
|
+
'schema',
|
|
5
|
+
'id',
|
|
6
|
+
'modelSchema',
|
|
7
|
+
'model',
|
|
8
|
+
'source',
|
|
9
|
+
'extensions',
|
|
10
|
+
'assets',
|
|
11
|
+
'diagnostics',
|
|
12
|
+
'statistics'
|
|
13
|
+
])
|
|
14
|
+
const PROJECT_FIELDS = Object.freeze([
|
|
15
|
+
'schema',
|
|
16
|
+
'id',
|
|
17
|
+
'source',
|
|
18
|
+
'documents',
|
|
19
|
+
'project',
|
|
20
|
+
'extensions',
|
|
21
|
+
'assets',
|
|
22
|
+
'diagnostics',
|
|
23
|
+
'statistics'
|
|
24
|
+
])
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Classifies canonical result envelopes through data descriptors only.
|
|
28
|
+
*/
|
|
29
|
+
export class WorkerResultShape {
|
|
30
|
+
/**
|
|
31
|
+
* Selects the exact property set traversed for one result container.
|
|
32
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Descriptors.
|
|
33
|
+
* @param {boolean} output Whether result-output rules apply.
|
|
34
|
+
* @param {boolean} strictDescriptors Whether hidden properties are traversed.
|
|
35
|
+
* @returns {PropertyKey[]} Traversed keys.
|
|
36
|
+
*/
|
|
37
|
+
static keys(descriptors, output, strictDescriptors) {
|
|
38
|
+
return output && !strictDescriptors
|
|
39
|
+
? Reflect.ownKeys(descriptors).filter(
|
|
40
|
+
(key) =>
|
|
41
|
+
typeof key === 'string' && descriptors[key].enumerable
|
|
42
|
+
)
|
|
43
|
+
: Reflect.ownKeys(descriptors)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Validates a dense plain array and returns its captured length.
|
|
48
|
+
* @param {object | null} prototype Array prototype.
|
|
49
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Descriptors.
|
|
50
|
+
* @param {boolean} output Whether result-output rules apply.
|
|
51
|
+
* @param {boolean} strictDescriptors Whether hidden properties are traversed.
|
|
52
|
+
* @returns {number} Dense array length.
|
|
53
|
+
*/
|
|
54
|
+
static arrayLength(prototype, descriptors, output, strictDescriptors) {
|
|
55
|
+
const keys = Reflect.ownKeys(descriptors)
|
|
56
|
+
const length = WorkerResultShape.#value(descriptors.length)
|
|
57
|
+
const visibleKeys =
|
|
58
|
+
output && !strictDescriptors
|
|
59
|
+
? keys.filter(
|
|
60
|
+
(key) =>
|
|
61
|
+
key !== 'length' &&
|
|
62
|
+
typeof key === 'string' &&
|
|
63
|
+
descriptors[key].enumerable
|
|
64
|
+
)
|
|
65
|
+
: keys
|
|
66
|
+
if (
|
|
67
|
+
prototype !== Array.prototype ||
|
|
68
|
+
!Number.isSafeInteger(length) ||
|
|
69
|
+
length < 0 ||
|
|
70
|
+
(output && !strictDescriptors
|
|
71
|
+
? visibleKeys.length !== length
|
|
72
|
+
: keys.length !== length + 1)
|
|
73
|
+
) {
|
|
74
|
+
throw new TypeError(
|
|
75
|
+
'Worker request arrays must be bounded, dense, and plain.'
|
|
76
|
+
)
|
|
77
|
+
}
|
|
78
|
+
return length
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Tests one already-inspected canonical document envelope.
|
|
83
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Descriptors.
|
|
84
|
+
* @param {PropertyKey[]} keys Traversed keys.
|
|
85
|
+
* @returns {boolean} Whether the exact structural document shape matches.
|
|
86
|
+
*/
|
|
87
|
+
static document(descriptors, keys) {
|
|
88
|
+
if (
|
|
89
|
+
!WorkerResultShape.#exactDataFields(
|
|
90
|
+
descriptors,
|
|
91
|
+
keys,
|
|
92
|
+
DOCUMENT_FIELDS
|
|
93
|
+
)
|
|
94
|
+
) {
|
|
95
|
+
return false
|
|
96
|
+
}
|
|
97
|
+
return (
|
|
98
|
+
WorkerResultShape.#value(descriptors.schema) ===
|
|
99
|
+
'ecad-toolkit.document.v1' &&
|
|
100
|
+
typeof WorkerResultShape.#value(descriptors.id) === 'string' &&
|
|
101
|
+
WorkerResultShape.#record(
|
|
102
|
+
WorkerResultShape.#value(descriptors.modelSchema)
|
|
103
|
+
) &&
|
|
104
|
+
Array.isArray(WorkerResultShape.#value(descriptors.model)) &&
|
|
105
|
+
WorkerResultShape.#record(
|
|
106
|
+
WorkerResultShape.#value(descriptors.source)
|
|
107
|
+
) &&
|
|
108
|
+
WorkerResultShape.#record(
|
|
109
|
+
WorkerResultShape.#value(descriptors.extensions)
|
|
110
|
+
) &&
|
|
111
|
+
Array.isArray(WorkerResultShape.#value(descriptors.assets)) &&
|
|
112
|
+
Array.isArray(WorkerResultShape.#value(descriptors.diagnostics)) &&
|
|
113
|
+
WorkerResultShape.#record(
|
|
114
|
+
WorkerResultShape.#value(descriptors.statistics)
|
|
115
|
+
)
|
|
116
|
+
)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Tests one already-inspected canonical project envelope.
|
|
121
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Descriptors.
|
|
122
|
+
* @param {PropertyKey[]} keys Traversed keys.
|
|
123
|
+
* @returns {boolean} Whether the exact structural project shape matches.
|
|
124
|
+
*/
|
|
125
|
+
static project(descriptors, keys) {
|
|
126
|
+
if (
|
|
127
|
+
!WorkerResultShape.#exactDataFields(
|
|
128
|
+
descriptors,
|
|
129
|
+
keys,
|
|
130
|
+
PROJECT_FIELDS
|
|
131
|
+
)
|
|
132
|
+
) {
|
|
133
|
+
return false
|
|
134
|
+
}
|
|
135
|
+
const project = WorkerResultShape.#value(descriptors.project)
|
|
136
|
+
return (
|
|
137
|
+
WorkerResultShape.#value(descriptors.schema) ===
|
|
138
|
+
'ecad-toolkit.project.v1' &&
|
|
139
|
+
typeof WorkerResultShape.#value(descriptors.id) === 'string' &&
|
|
140
|
+
WorkerResultShape.#record(
|
|
141
|
+
WorkerResultShape.#value(descriptors.source)
|
|
142
|
+
) &&
|
|
143
|
+
Array.isArray(WorkerResultShape.#value(descriptors.documents)) &&
|
|
144
|
+
(project === null || WorkerResultShape.#record(project)) &&
|
|
145
|
+
WorkerResultShape.#record(
|
|
146
|
+
WorkerResultShape.#value(descriptors.extensions)
|
|
147
|
+
) &&
|
|
148
|
+
Array.isArray(WorkerResultShape.#value(descriptors.assets)) &&
|
|
149
|
+
Array.isArray(WorkerResultShape.#value(descriptors.diagnostics)) &&
|
|
150
|
+
WorkerResultShape.#record(
|
|
151
|
+
WorkerResultShape.#value(descriptors.statistics)
|
|
152
|
+
)
|
|
153
|
+
)
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Inspects a nested project document without reading accessor values.
|
|
158
|
+
* @param {unknown} value Candidate document.
|
|
159
|
+
* @param {boolean} strictDescriptors Whether hidden properties are traversed.
|
|
160
|
+
* @returns {boolean} Whether the exact structural document shape matches.
|
|
161
|
+
*/
|
|
162
|
+
static documentCandidate(value, strictDescriptors) {
|
|
163
|
+
if (!WorkerResultShape.#record(value)) return false
|
|
164
|
+
RuntimeProxyBoundary.assert(value, 'Worker request data')
|
|
165
|
+
let prototype
|
|
166
|
+
let descriptors
|
|
167
|
+
try {
|
|
168
|
+
prototype = Object.getPrototypeOf(value)
|
|
169
|
+
descriptors = Object.getOwnPropertyDescriptors(value)
|
|
170
|
+
} catch {
|
|
171
|
+
return false
|
|
172
|
+
}
|
|
173
|
+
if (prototype !== Object.prototype && prototype !== null) return false
|
|
174
|
+
const keys = WorkerResultShape.keys(
|
|
175
|
+
descriptors,
|
|
176
|
+
true,
|
|
177
|
+
strictDescriptors
|
|
178
|
+
)
|
|
179
|
+
return WorkerResultShape.document(descriptors, keys)
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Requires the exact enumerable own data field set.
|
|
184
|
+
* @param {Record<string, PropertyDescriptor>} descriptors Descriptors.
|
|
185
|
+
* @param {PropertyKey[]} keys Traversed keys.
|
|
186
|
+
* @param {string[]} fields Expected fields.
|
|
187
|
+
* @returns {boolean} Whether the field set matches.
|
|
188
|
+
*/
|
|
189
|
+
static #exactDataFields(descriptors, keys, fields) {
|
|
190
|
+
if (
|
|
191
|
+
keys.length !== fields.length ||
|
|
192
|
+
keys.some((key) => typeof key !== 'string' || !fields.includes(key))
|
|
193
|
+
) {
|
|
194
|
+
return false
|
|
195
|
+
}
|
|
196
|
+
return fields.every((field) => {
|
|
197
|
+
const descriptor = descriptors[field]
|
|
198
|
+
return Boolean(
|
|
199
|
+
descriptor &&
|
|
200
|
+
descriptor.enumerable === true &&
|
|
201
|
+
Object.hasOwn(descriptor, 'value')
|
|
202
|
+
)
|
|
203
|
+
})
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/** @param {unknown} value Candidate. @returns {boolean} Whether value is record-shaped. */
|
|
207
|
+
static #record(value) {
|
|
208
|
+
return (
|
|
209
|
+
value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** @param {PropertyDescriptor | undefined} descriptor Descriptor. @returns {unknown} Data value. */
|
|
214
|
+
static #value(descriptor) {
|
|
215
|
+
return descriptor && Object.hasOwn(descriptor, 'value')
|
|
216
|
+
? descriptor.value
|
|
217
|
+
: undefined
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
Object.freeze(WorkerResultShape.prototype)
|
|
222
|
+
Object.freeze(WorkerResultShape)
|