circuitjson-toolkit 1.2.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +25 -6
- package/docs/api.md +28 -5
- package/docs/model-format.md +12 -8
- package/docs/release-notes-v1.2.1.md +26 -0
- package/docs/release-notes-v1.3.0.md +45 -0
- package/docs/testing.md +14 -0
- package/package.json +4 -2
- package/src/core/context/BinaryDataSnapshot.mjs +183 -43
- package/src/core/context/CircuitJsonDocumentContext.mjs +31 -4
- package/src/core/context/CircuitJsonReadOnlyDocument.mjs +22 -9
- package/src/core/context/CircuitJsonValidationProof.mjs +4 -2
- package/src/core/context/ProtectedExtensionBinaryBoundary.mjs +2 -2
- package/src/core/context/StructuredDataSnapshot.mjs +11 -6
- package/src/core/worker/WorkerRequestData.mjs +401 -55
- package/src/core/worker/WorkerResultShape.mjs +222 -0
|
@@ -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)
|