@mermanjs/node 0.8.0-alpha.5
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/CHANGELOG.md +13 -0
- package/LICENSE-APACHE +201 -0
- package/LICENSE-MIT +21 -0
- package/README.md +11 -0
- package/THIRD_PARTY_LICENSES/beautiful-mermaid/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cose-base-v1/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cose-base-v2/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cytoscape/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/cytoscape-cose-bilkent/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/cytoscape-fcose/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/d3-shape/LICENSE +13 -0
- package/THIRD_PARTY_LICENSES/dagre/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/dompurify/LICENSE +202 -0
- package/THIRD_PARTY_LICENSES/eclipse-elk/LICENSE.md +264 -0
- package/THIRD_PARTY_LICENSES/elkjs/LICENSE.md +264 -0
- package/THIRD_PARTY_LICENSES/fmin/LICENSE +27 -0
- package/THIRD_PARTY_LICENSES/graphlib/LICENSE +19 -0
- package/THIRD_PARTY_LICENSES/katex-fonts/FONT_NOTICE.txt +12 -0
- package/THIRD_PARTY_LICENSES/katex-fonts/OFL.txt +85 -0
- package/THIRD_PARTY_LICENSES/layout-base-v1/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/layout-base-v2/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/mermaid/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/mermaid-ascii/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/mermaid-rs-renderer/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/ratex/LICENSE +27 -0
- package/THIRD_PARTY_LICENSES/ratex/THIRD_PARTY_NOTICES.txt +19 -0
- package/THIRD_PARTY_LICENSES/rough-rs/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/roughjs/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/rust-cargo-dependencies.json +5562 -0
- package/THIRD_PARTY_LICENSES/sanitize-url/LICENSE +21 -0
- package/THIRD_PARTY_LICENSES/venn-js/LICENSE +22 -0
- package/THIRD_PARTY_LICENSES/wasm-minimal-protocol/LICENSE +24 -0
- package/THIRD_PARTY_LICENSES/zenuml-core/LICENSE +21 -0
- package/THIRD_PARTY_NOTICES.md +524 -0
- package/dist/bounded-executor.mjs +132 -0
- package/dist/candidates/native.mjs +51 -0
- package/dist/candidates/wrap-engine.mjs +28 -0
- package/dist/engine.mjs +1255 -0
- package/dist/errors.mjs +1199 -0
- package/dist/generated/binding-contract.mjs +873 -0
- package/dist/generated/capability-surface.mjs +66 -0
- package/dist/generated/node-wire-contract.json +122 -0
- package/dist/index.d.ts +239 -0
- package/dist/index.mjs +22 -0
- package/dist/native-loader.mjs +112 -0
- package/dist/transport-contract.mjs +87 -0
- package/package.json +50 -0
package/dist/engine.mjs
ADDED
|
@@ -0,0 +1,1255 @@
|
|
|
1
|
+
import { BoundedExecutor } from "./bounded-executor.mjs";
|
|
2
|
+
import {
|
|
3
|
+
MermanInvalidTransportError,
|
|
4
|
+
NODE_TRANSPORT_FIELD_LIMITS,
|
|
5
|
+
NODE_TRANSPORT_LIMITS,
|
|
6
|
+
NODE_WIRE_CONTRACT,
|
|
7
|
+
assertUtf8Field,
|
|
8
|
+
decodeWireInvocationError,
|
|
9
|
+
decodeWireResponse,
|
|
10
|
+
encodeTransportJson,
|
|
11
|
+
parseRuntimeCatalogJsonText,
|
|
12
|
+
parseTransportJsonText,
|
|
13
|
+
sameJsonValue,
|
|
14
|
+
} from "./errors.mjs";
|
|
15
|
+
import {
|
|
16
|
+
BINDING_OPTION_GROUP_SPECS,
|
|
17
|
+
BINDING_OPERATION_EXPECTATIONS,
|
|
18
|
+
BINDING_OPTIONS_SCHEMA_VERSION,
|
|
19
|
+
BINDING_PAYLOAD_SCHEMAS,
|
|
20
|
+
BINDING_TRANSPORT_EXPOSURE_SPECS,
|
|
21
|
+
CAPABILITY_SPECS,
|
|
22
|
+
CONSTRUCTOR_SERVICE_SPECS,
|
|
23
|
+
METADATA_SPECS,
|
|
24
|
+
RUNTIME_CATALOG_FIELD_IDENTIFIER_PATTERN,
|
|
25
|
+
RUNTIME_CATALOG_IDENTIFIER_PATTERN,
|
|
26
|
+
RUNTIME_CATALOG_MAX_SAFE_INTEGER,
|
|
27
|
+
RUNTIME_CATALOG_SCHEMA_VERSION,
|
|
28
|
+
TEXT_MEASUREMENT_PROTOCOL_VERSION,
|
|
29
|
+
TEXT_MEASUREMENT_PROVIDER_IDS,
|
|
30
|
+
TEXT_MEASUREMENT_PROVIDER_SPECS,
|
|
31
|
+
VENDORED_TEXT_MEASUREMENT_PROVIDER_ID,
|
|
32
|
+
} from "./generated/binding-contract.mjs";
|
|
33
|
+
|
|
34
|
+
const RESOURCE_PROFILES = new Set([
|
|
35
|
+
"interactive",
|
|
36
|
+
"constrained",
|
|
37
|
+
"trusted-native",
|
|
38
|
+
"unbounded-for-trusted-input",
|
|
39
|
+
]);
|
|
40
|
+
const RUNTIME_CATALOG_IDENTIFIER = new RegExp(RUNTIME_CATALOG_IDENTIFIER_PATTERN);
|
|
41
|
+
const RUNTIME_CATALOG_FIELD_IDENTIFIER = new RegExp(
|
|
42
|
+
RUNTIME_CATALOG_FIELD_IDENTIFIER_PATTERN,
|
|
43
|
+
);
|
|
44
|
+
const NODE_TRANSPORT_EXPOSURE = BINDING_TRANSPORT_EXPOSURE_SPECS.find(
|
|
45
|
+
(spec) => spec.id === "node",
|
|
46
|
+
);
|
|
47
|
+
if (NODE_TRANSPORT_EXPOSURE === undefined) {
|
|
48
|
+
throw new Error("Generated binding contract is missing the Node transport exposure.");
|
|
49
|
+
}
|
|
50
|
+
const PAYLOAD_SCHEMA_SPEC_BY_ID = new Map(
|
|
51
|
+
BINDING_PAYLOAD_SCHEMAS.map((spec) => [spec.id, spec]),
|
|
52
|
+
);
|
|
53
|
+
const CAPABILITY_SPEC_BY_ID = new Map(CAPABILITY_SPECS.map((spec) => [spec.id, spec]));
|
|
54
|
+
const REQUIRED_PAYLOAD_SCHEMAS = new Map(
|
|
55
|
+
NODE_TRANSPORT_EXPOSURE.payload_schema_ids.map((id) => [
|
|
56
|
+
id,
|
|
57
|
+
PAYLOAD_SCHEMA_SPEC_BY_ID.get(id).version,
|
|
58
|
+
]),
|
|
59
|
+
);
|
|
60
|
+
const OPTION_GROUP_SPEC_BY_ID = new Map(
|
|
61
|
+
BINDING_OPTION_GROUP_SPECS.map((spec) => [spec.id, spec]),
|
|
62
|
+
);
|
|
63
|
+
const METADATA_SPEC_BY_ID = new Map(METADATA_SPECS.map((spec) => [spec.id, spec]));
|
|
64
|
+
const OPERATION_EXPECTATION_BY_ID = new Map(
|
|
65
|
+
BINDING_OPERATION_EXPECTATIONS.map((expectation) => [
|
|
66
|
+
expectation.operation_id,
|
|
67
|
+
expectation,
|
|
68
|
+
]),
|
|
69
|
+
);
|
|
70
|
+
const CONSTRUCTOR_SERVICE_SPEC_BY_ID = new Map(
|
|
71
|
+
CONSTRUCTOR_SERVICE_SPECS.map((spec) => [spec.id, spec]),
|
|
72
|
+
);
|
|
73
|
+
const NODE_CONSTRUCTOR_SERVICE_CANDIDATE_IDS = new Set(
|
|
74
|
+
NODE_TRANSPORT_EXPOSURE.constructor_service_candidate_ids,
|
|
75
|
+
);
|
|
76
|
+
const TEXT_MEASUREMENT_PROVIDER_SPEC_BY_ID = new Map(
|
|
77
|
+
TEXT_MEASUREMENT_PROVIDER_SPECS.map((spec) => [spec.id, spec]),
|
|
78
|
+
);
|
|
79
|
+
const KNOWN_TEXT_MEASUREMENT_PROVIDER_IDS = new Set(TEXT_MEASUREMENT_PROVIDER_IDS);
|
|
80
|
+
const COMPILED_PREREQUISITES_BY_OPERATION_ID = new Map(
|
|
81
|
+
BINDING_OPERATION_EXPECTATIONS.map(({ operation_id, compiled_prerequisite_ids }) => [
|
|
82
|
+
operation_id,
|
|
83
|
+
compiled_prerequisite_ids,
|
|
84
|
+
]),
|
|
85
|
+
);
|
|
86
|
+
const KNOWN_OUTPUT_CONTRACT_BY_ID = new Map(
|
|
87
|
+
NODE_WIRE_CONTRACT.artifact.output_contracts.map((contract) => [contract.id, contract]),
|
|
88
|
+
);
|
|
89
|
+
const MERMAN_ENGINE_CONSTRUCTION_TOKEN = Symbol("MermanEngine construction");
|
|
90
|
+
|
|
91
|
+
export async function createNodeEngine(
|
|
92
|
+
{
|
|
93
|
+
bindingOptions = {},
|
|
94
|
+
concurrency = 1,
|
|
95
|
+
maxQueue = 64,
|
|
96
|
+
} = {},
|
|
97
|
+
{ loadTransport } = {},
|
|
98
|
+
) {
|
|
99
|
+
if (typeof loadTransport !== "function") {
|
|
100
|
+
throw new MermanInvalidTransportError("A concrete Node candidate transport loader is required.");
|
|
101
|
+
}
|
|
102
|
+
const normalizedOptions = normalizeBindingOptions(bindingOptions);
|
|
103
|
+
const optionsJson = encodeTransportJson(
|
|
104
|
+
normalizedOptions,
|
|
105
|
+
"binding options",
|
|
106
|
+
NODE_TRANSPORT_LIMITS.binding_options,
|
|
107
|
+
);
|
|
108
|
+
const transport = await loadTransport(optionsJson);
|
|
109
|
+
try {
|
|
110
|
+
assertTransport(transport);
|
|
111
|
+
const runtimeCatalog = validateRuntimeCatalog(await transport.runtimeCatalogJson());
|
|
112
|
+
return new MermanEngine(
|
|
113
|
+
MERMAN_ENGINE_CONSTRUCTION_TOKEN,
|
|
114
|
+
transport,
|
|
115
|
+
runtimeCatalog,
|
|
116
|
+
{ concurrency, maxQueue },
|
|
117
|
+
);
|
|
118
|
+
} catch (error) {
|
|
119
|
+
await disposeUnusableTransport(transport);
|
|
120
|
+
throw error;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export function normalizeBindingOptions(value = {}) {
|
|
125
|
+
if (!isPlainObject(value)) throw new TypeError("bindingOptions must be a plain object.");
|
|
126
|
+
const normalized = cloneBoundedJsonValue(value, "bindingOptions");
|
|
127
|
+
normalized.version ??= BINDING_OPTIONS_SCHEMA_VERSION;
|
|
128
|
+
if (normalized.version !== BINDING_OPTIONS_SCHEMA_VERSION) {
|
|
129
|
+
throw new RangeError(
|
|
130
|
+
`Unsupported binding options schema version \`${normalized.version}\`; expected ${BINDING_OPTIONS_SCHEMA_VERSION}.`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
normalized.runtime_policy ??= "deterministic";
|
|
134
|
+
if (normalized.runtime_policy !== "deterministic") {
|
|
135
|
+
throw new RangeError(
|
|
136
|
+
"bindingOptions.runtime_policy must be `deterministic` for @mermanjs/node.",
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
normalized.resources ??= {};
|
|
140
|
+
if (!isPlainObject(normalized.resources)) {
|
|
141
|
+
throw new TypeError("bindingOptions.resources must be a plain object.");
|
|
142
|
+
}
|
|
143
|
+
normalized.resources.profile ??= "interactive";
|
|
144
|
+
if (!RESOURCE_PROFILES.has(normalized.resources.profile)) {
|
|
145
|
+
throw new RangeError(`Unknown resource profile \`${normalized.resources.profile}\`.`);
|
|
146
|
+
}
|
|
147
|
+
return normalized;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export class MermanEngine {
|
|
151
|
+
#disposePromise = null;
|
|
152
|
+
#executor;
|
|
153
|
+
#metadataIds;
|
|
154
|
+
#operationIds;
|
|
155
|
+
#runtimeCatalog;
|
|
156
|
+
#transport;
|
|
157
|
+
|
|
158
|
+
constructor(constructionToken, transport, runtimeCatalog, queueOptions) {
|
|
159
|
+
if (constructionToken !== MERMAN_ENGINE_CONSTRUCTION_TOKEN) {
|
|
160
|
+
throw new MermanInvalidTransportError(
|
|
161
|
+
"MermanEngine instances must be created with createNodeEngine().",
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
this.#transport = transport;
|
|
165
|
+
this.#runtimeCatalog = runtimeCatalog;
|
|
166
|
+
this.#metadataIds = new Set(
|
|
167
|
+
runtimeCatalog.metadata_ids.filter((id) => METADATA_SPEC_BY_ID.has(id)),
|
|
168
|
+
);
|
|
169
|
+
this.#operationIds = new Set(runtimeCatalog.capabilities.operation_ids);
|
|
170
|
+
this.#executor = new BoundedExecutor(queueOptions);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
get queueState() {
|
|
174
|
+
return this.#executor.snapshot;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
get runtimeCatalog() {
|
|
178
|
+
return structuredClone(this.#runtimeCatalog);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
renderSvg(source, options = {}) {
|
|
182
|
+
return this.executeOperation(
|
|
183
|
+
{ operationId: "svg", source, optionsJson: options.optionsJson },
|
|
184
|
+
{ signal: options.signal },
|
|
185
|
+
).then((result) => result.data);
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
renderSvgSync(source, options = {}) {
|
|
189
|
+
return this.executeOperationSync({
|
|
190
|
+
operationId: "svg",
|
|
191
|
+
source,
|
|
192
|
+
optionsJson: options.optionsJson,
|
|
193
|
+
}).data;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
svgPlanJson(source, options = {}) {
|
|
197
|
+
return this.executeOperation(
|
|
198
|
+
{ operationId: "svg-plan-json", source, optionsJson: options.optionsJson },
|
|
199
|
+
{ signal: options.signal },
|
|
200
|
+
).then((result) => result.data);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
svgPlanJsonSync(source, options = {}) {
|
|
204
|
+
return this.executeOperationSync({
|
|
205
|
+
operationId: "svg-plan-json",
|
|
206
|
+
source,
|
|
207
|
+
optionsJson: options.optionsJson,
|
|
208
|
+
}).data;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
metadataJson(id) {
|
|
212
|
+
this.#executor.assertOpen();
|
|
213
|
+
if (typeof id !== "string" || id.length === 0) {
|
|
214
|
+
throw new TypeError("metadata id must be a non-empty string.");
|
|
215
|
+
}
|
|
216
|
+
if (!this.#metadataIds.has(id)) {
|
|
217
|
+
throw new RangeError(
|
|
218
|
+
`metadata id \`${id}\` is not callable through this SDK and artifact contract.`,
|
|
219
|
+
);
|
|
220
|
+
}
|
|
221
|
+
let value;
|
|
222
|
+
try {
|
|
223
|
+
value = this.#transport.metadataJson(id);
|
|
224
|
+
} catch (cause) {
|
|
225
|
+
throw decodeWireInvocationError(cause, `Merman metadata \`${id}\``);
|
|
226
|
+
}
|
|
227
|
+
parseTransportJsonText(
|
|
228
|
+
value,
|
|
229
|
+
`metadata \`${id}\``,
|
|
230
|
+
NODE_TRANSPORT_LIMITS.metadata,
|
|
231
|
+
);
|
|
232
|
+
return value;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
executeOperation(request, { signal } = {}) {
|
|
236
|
+
const encoded = operationRequestJson(request);
|
|
237
|
+
return this.#executor.submit(
|
|
238
|
+
async () => {
|
|
239
|
+
let responseJson;
|
|
240
|
+
try {
|
|
241
|
+
responseJson = await this.#transport.execute(encoded.json);
|
|
242
|
+
} catch (cause) {
|
|
243
|
+
throw decodeWireInvocationError(cause, "Merman operation");
|
|
244
|
+
}
|
|
245
|
+
return decodeWireResponse(responseJson, encoded.expectation, {
|
|
246
|
+
requireUnavailable: !this.#operationIds.has(encoded.expectation.operation_id),
|
|
247
|
+
});
|
|
248
|
+
},
|
|
249
|
+
{ signal },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
executeOperationSync(request) {
|
|
254
|
+
this.#executor.assertSyncAvailable();
|
|
255
|
+
const encoded = operationRequestJson(request);
|
|
256
|
+
let responseJson;
|
|
257
|
+
try {
|
|
258
|
+
responseJson = this.#transport.executeSync(encoded.json);
|
|
259
|
+
} catch (cause) {
|
|
260
|
+
throw decodeWireInvocationError(cause, "Merman operation");
|
|
261
|
+
}
|
|
262
|
+
return decodeWireResponse(responseJson, encoded.expectation, {
|
|
263
|
+
requireUnavailable: !this.#operationIds.has(encoded.expectation.operation_id),
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
dispose() {
|
|
268
|
+
if (this.#disposePromise) return this.#disposePromise;
|
|
269
|
+
this.#disposePromise = this.#executor.dispose().then(async () => {
|
|
270
|
+
await this.#transport.dispose?.();
|
|
271
|
+
});
|
|
272
|
+
return this.#disposePromise;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
function operationRequestJson(value) {
|
|
277
|
+
if (!isPlainObject(value)) throw new TypeError("operation request must be a plain object.");
|
|
278
|
+
const knownFields = new Set(["operationId", "source", "uri", "optionsJson"]);
|
|
279
|
+
for (const field of Object.keys(value)) {
|
|
280
|
+
if (!knownFields.has(field)) {
|
|
281
|
+
throw new TypeError(`unknown operation request field \`${field}\`.`);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
const {
|
|
285
|
+
operationId,
|
|
286
|
+
source,
|
|
287
|
+
uri = null,
|
|
288
|
+
optionsJson = undefined,
|
|
289
|
+
} = value;
|
|
290
|
+
if (typeof operationId !== "string" || operationId.length === 0) {
|
|
291
|
+
throw new TypeError("operationId must be a non-empty string.");
|
|
292
|
+
}
|
|
293
|
+
if (!OPERATION_EXPECTATION_BY_ID.has(operationId)) {
|
|
294
|
+
throw new RangeError(
|
|
295
|
+
`operation id \`${operationId}\` is not callable through this SDK version.`,
|
|
296
|
+
);
|
|
297
|
+
}
|
|
298
|
+
const expectation = OPERATION_EXPECTATION_BY_ID.get(operationId);
|
|
299
|
+
if (typeof source !== "string") throw new TypeError("source must be a string.");
|
|
300
|
+
if (uri !== null && typeof uri !== "string") throw new TypeError("uri must be a string or null.");
|
|
301
|
+
const normalizedUri = uri === "" ? null : uri;
|
|
302
|
+
if (expectation.requires_uri && normalizedUri === null) {
|
|
303
|
+
throw new TypeError(`operation \`${operationId}\` requires a non-empty uri.`);
|
|
304
|
+
}
|
|
305
|
+
if (!expectation.requires_uri && normalizedUri !== null) {
|
|
306
|
+
throw new TypeError(`operation \`${operationId}\` does not accept a uri.`);
|
|
307
|
+
}
|
|
308
|
+
assertUtf8Field(
|
|
309
|
+
operationId,
|
|
310
|
+
"operation request operationId",
|
|
311
|
+
NODE_TRANSPORT_FIELD_LIMITS.operation_id_utf8_bytes,
|
|
312
|
+
);
|
|
313
|
+
assertUtf8Field(
|
|
314
|
+
source,
|
|
315
|
+
"operation request source",
|
|
316
|
+
NODE_TRANSPORT_FIELD_LIMITS.source_utf8_bytes,
|
|
317
|
+
);
|
|
318
|
+
if (normalizedUri !== null) {
|
|
319
|
+
assertUtf8Field(
|
|
320
|
+
normalizedUri,
|
|
321
|
+
"operation request uri",
|
|
322
|
+
NODE_TRANSPORT_FIELD_LIMITS.uri_utf8_bytes,
|
|
323
|
+
);
|
|
324
|
+
}
|
|
325
|
+
const request = {
|
|
326
|
+
operation_id: operationId,
|
|
327
|
+
source,
|
|
328
|
+
uri: normalizedUri,
|
|
329
|
+
};
|
|
330
|
+
if (optionsJson !== undefined) {
|
|
331
|
+
if (typeof optionsJson !== "string") {
|
|
332
|
+
throw new TypeError("optionsJson must be a JSON string when provided.");
|
|
333
|
+
}
|
|
334
|
+
assertUtf8Field(
|
|
335
|
+
optionsJson,
|
|
336
|
+
"operation request optionsJson",
|
|
337
|
+
NODE_TRANSPORT_FIELD_LIMITS.options_json_utf8_bytes,
|
|
338
|
+
);
|
|
339
|
+
parseTransportJsonText(
|
|
340
|
+
optionsJson,
|
|
341
|
+
"operation request optionsJson",
|
|
342
|
+
NODE_TRANSPORT_LIMITS.binding_options,
|
|
343
|
+
);
|
|
344
|
+
request.options_json = optionsJson;
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
expectation,
|
|
348
|
+
json: encodeTransportJson(request, "operation request", NODE_TRANSPORT_LIMITS.request),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function assertTransport(transport) {
|
|
353
|
+
if (
|
|
354
|
+
!transport ||
|
|
355
|
+
typeof transport.execute !== "function" ||
|
|
356
|
+
typeof transport.executeSync !== "function" ||
|
|
357
|
+
typeof transport.runtimeCatalogJson !== "function" ||
|
|
358
|
+
typeof transport.metadataJson !== "function"
|
|
359
|
+
) {
|
|
360
|
+
throw new MermanInvalidTransportError(
|
|
361
|
+
"Merman transport must provide runtimeCatalogJson(), metadataJson(), execute(), and executeSync().",
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
async function disposeUnusableTransport(transport) {
|
|
367
|
+
try {
|
|
368
|
+
await transport?.dispose?.();
|
|
369
|
+
} catch {
|
|
370
|
+
// Preserve the construction failure that made the transport unusable.
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
export function validateRuntimeCatalog(value) {
|
|
375
|
+
if (typeof value !== "string") {
|
|
376
|
+
throw new MermanInvalidTransportError(
|
|
377
|
+
"Merman transport runtime catalog must be JSON text.",
|
|
378
|
+
);
|
|
379
|
+
}
|
|
380
|
+
const catalog = parseRuntimeCatalogJsonText(value);
|
|
381
|
+
if (!isPlainObject(catalog)) {
|
|
382
|
+
throw new MermanInvalidTransportError("Merman transport returned an invalid runtime catalog.");
|
|
383
|
+
}
|
|
384
|
+
if (
|
|
385
|
+
catalog.schema_version !== RUNTIME_CATALOG_SCHEMA_VERSION ||
|
|
386
|
+
catalog.transport_api_version !== 1
|
|
387
|
+
) {
|
|
388
|
+
throw new MermanInvalidTransportError("Merman transport returned an unsupported runtime catalog version.");
|
|
389
|
+
}
|
|
390
|
+
if (typeof catalog.package_version !== "string" || catalog.package_version.length === 0) {
|
|
391
|
+
throw new MermanInvalidTransportError("Merman runtime catalog has an invalid package version.");
|
|
392
|
+
}
|
|
393
|
+
assertUtf8Field(
|
|
394
|
+
catalog.package_version,
|
|
395
|
+
"runtime catalog package_version",
|
|
396
|
+
NODE_TRANSPORT_FIELD_LIMITS.package_version_utf8_bytes,
|
|
397
|
+
);
|
|
398
|
+
const optionsSchemaVersions = sortedUniquePositiveIntegers(
|
|
399
|
+
catalog.options_schema_versions,
|
|
400
|
+
"options_schema_versions",
|
|
401
|
+
);
|
|
402
|
+
if (!optionsSchemaVersions.includes(BINDING_OPTIONS_SCHEMA_VERSION)) {
|
|
403
|
+
throw new MermanInvalidTransportError(
|
|
404
|
+
`Merman runtime catalog does not advertise options schema ${BINDING_OPTIONS_SCHEMA_VERSION}.`,
|
|
405
|
+
);
|
|
406
|
+
}
|
|
407
|
+
validatePayloadSchemas(catalog.payload_schemas);
|
|
408
|
+
|
|
409
|
+
const capabilities = catalog.capabilities;
|
|
410
|
+
const registry = catalog.registry;
|
|
411
|
+
const resources = catalog.resources;
|
|
412
|
+
if (!isPlainObject(capabilities) || !isPlainObject(registry) || !isPlainObject(resources)) {
|
|
413
|
+
throw new MermanInvalidTransportError("Merman runtime catalog has invalid required sections.");
|
|
414
|
+
}
|
|
415
|
+
const capabilityIds = sortedUniqueStrings(capabilities.capability_ids, "capability_ids");
|
|
416
|
+
const outputIds = sortedUniqueStrings(capabilities.output_ids, "output_ids");
|
|
417
|
+
const operationIds = sortedUniqueStrings(capabilities.operation_ids, "operation_ids");
|
|
418
|
+
const systemAdapterIds = sortedUniqueStrings(
|
|
419
|
+
capabilities.system_adapter_ids,
|
|
420
|
+
"system_adapter_ids",
|
|
421
|
+
);
|
|
422
|
+
if (!systemAdapterIds.every((id) => capabilityIds.includes(id))) {
|
|
423
|
+
throw new MermanInvalidTransportError(
|
|
424
|
+
"Merman runtime catalog contains invalid capability relations.",
|
|
425
|
+
);
|
|
426
|
+
}
|
|
427
|
+
validateCapabilityImplications(capabilityIds);
|
|
428
|
+
validateOperationRelations({ capabilityIds, operationIds, outputIds });
|
|
429
|
+
const metadataIds = validateMetadataIds(catalog.metadata_ids, new Set(capabilityIds));
|
|
430
|
+
validateKnownArtifactIdentity({
|
|
431
|
+
capabilityIds,
|
|
432
|
+
metadataIds,
|
|
433
|
+
operationIds,
|
|
434
|
+
outputIds,
|
|
435
|
+
systemAdapterIds,
|
|
436
|
+
});
|
|
437
|
+
const requiresSvgPipeline = capabilityIds.includes("svg") || operationIds.some((id) =>
|
|
438
|
+
COMPILED_PREREQUISITES_BY_OPERATION_ID.get(id)?.includes("svg")
|
|
439
|
+
);
|
|
440
|
+
const textMeasurementProviderIds = validateTextMeasurement(capabilities.text_measurement, {
|
|
441
|
+
requiresSvgPipeline,
|
|
442
|
+
});
|
|
443
|
+
validateKnownIds(
|
|
444
|
+
textMeasurementProviderIds,
|
|
445
|
+
NODE_WIRE_CONTRACT.artifact.text_measurement_provider_ids,
|
|
446
|
+
(id) => TEXT_MEASUREMENT_PROVIDER_SPEC_BY_ID.has(id),
|
|
447
|
+
"text measurement providers",
|
|
448
|
+
);
|
|
449
|
+
// A valid provider record may describe a future operation whose prerequisite is unknown to this
|
|
450
|
+
// SDK. Preserve that additive discovery while requiring the known SVG closure when recognized.
|
|
451
|
+
const usesSvgPipeline = requiresSvgPipeline || capabilities.text_measurement !== null;
|
|
452
|
+
const optionGroupIds = validateOptionGroupIds(catalog, {
|
|
453
|
+
capabilityIds: new Set(capabilityIds),
|
|
454
|
+
usesSvgPipeline,
|
|
455
|
+
});
|
|
456
|
+
const constructorServices = validateConstructorServices(catalog, {
|
|
457
|
+
textMeasurementProviderIds: new Set(textMeasurementProviderIds),
|
|
458
|
+
usesSvgPipeline,
|
|
459
|
+
});
|
|
460
|
+
validateOutputContracts(catalog.output_contracts, outputIds);
|
|
461
|
+
validateRegistry(registry);
|
|
462
|
+
validateResources(resources, new Set(operationIds));
|
|
463
|
+
const normalized = structuredClone(catalog);
|
|
464
|
+
normalized.option_group_ids = optionGroupIds;
|
|
465
|
+
normalized.constructor_service_ids = constructorServices.ids;
|
|
466
|
+
normalized.constructor_service_contracts = constructorServices.contracts;
|
|
467
|
+
return normalized;
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
function validateKnownArtifactIdentity({
|
|
471
|
+
capabilityIds,
|
|
472
|
+
metadataIds,
|
|
473
|
+
operationIds,
|
|
474
|
+
outputIds,
|
|
475
|
+
systemAdapterIds,
|
|
476
|
+
}) {
|
|
477
|
+
const expected = NODE_WIRE_CONTRACT.artifact;
|
|
478
|
+
validateKnownIds(
|
|
479
|
+
capabilityIds,
|
|
480
|
+
expected.capability_ids,
|
|
481
|
+
(id) => CAPABILITY_SPEC_BY_ID.has(id),
|
|
482
|
+
"capabilities",
|
|
483
|
+
);
|
|
484
|
+
validateKnownIds(
|
|
485
|
+
operationIds,
|
|
486
|
+
expected.operation_ids,
|
|
487
|
+
(id) => OPERATION_EXPECTATION_BY_ID.has(id),
|
|
488
|
+
"operations",
|
|
489
|
+
);
|
|
490
|
+
validateKnownIds(
|
|
491
|
+
metadataIds,
|
|
492
|
+
expected.metadata_ids,
|
|
493
|
+
(id) => METADATA_SPEC_BY_ID.has(id),
|
|
494
|
+
"metadata IDs",
|
|
495
|
+
);
|
|
496
|
+
const knownOutputIds = new Set(
|
|
497
|
+
BINDING_OPERATION_EXPECTATIONS
|
|
498
|
+
.map((expectation) => expectation.output_id)
|
|
499
|
+
.filter((id) => id !== null),
|
|
500
|
+
);
|
|
501
|
+
validateKnownIds(
|
|
502
|
+
outputIds,
|
|
503
|
+
expected.output_ids,
|
|
504
|
+
(id) => knownOutputIds.has(id),
|
|
505
|
+
"outputs",
|
|
506
|
+
);
|
|
507
|
+
validateKnownIds(
|
|
508
|
+
systemAdapterIds,
|
|
509
|
+
expected.system_adapter_ids,
|
|
510
|
+
(id) => CAPABILITY_SPEC_BY_ID.has(id),
|
|
511
|
+
"system adapters",
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
function validateKnownIds(actual, expected, isKnown, label) {
|
|
516
|
+
const actualKnown = actual.filter(isKnown);
|
|
517
|
+
if (!sameOrderedStrings(actualKnown, expected)) {
|
|
518
|
+
throw new MermanInvalidTransportError(
|
|
519
|
+
`Merman runtime catalog known ${label} do not match the Node artifact contract.`,
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
function validatePayloadSchemas(value) {
|
|
525
|
+
if (!Array.isArray(value)) {
|
|
526
|
+
throw new MermanInvalidTransportError("Merman runtime catalog has invalid payload schemas.");
|
|
527
|
+
}
|
|
528
|
+
const ids = [];
|
|
529
|
+
for (const schema of value) {
|
|
530
|
+
if (
|
|
531
|
+
!isPlainObject(schema) ||
|
|
532
|
+
typeof schema.id !== "string" ||
|
|
533
|
+
schema.id.length === 0 ||
|
|
534
|
+
!positiveSafeInteger(schema.version)
|
|
535
|
+
) {
|
|
536
|
+
throw new MermanInvalidTransportError(
|
|
537
|
+
"Merman runtime catalog has an invalid payload schema.",
|
|
538
|
+
);
|
|
539
|
+
}
|
|
540
|
+
ids.push(schema.id);
|
|
541
|
+
}
|
|
542
|
+
sortedUniqueStrings(ids, "payload_schemas.id");
|
|
543
|
+
const actualKnownIds = ids.filter((id) => PAYLOAD_SCHEMA_SPEC_BY_ID.has(id));
|
|
544
|
+
if (!sameStringSet(actualKnownIds, new Set(REQUIRED_PAYLOAD_SCHEMAS.keys()))) {
|
|
545
|
+
throw new MermanInvalidTransportError(
|
|
546
|
+
"Merman runtime catalog payload schemas do not match the Node transport exposure.",
|
|
547
|
+
);
|
|
548
|
+
}
|
|
549
|
+
for (const [id, version] of REQUIRED_PAYLOAD_SCHEMAS) {
|
|
550
|
+
const schema = value.find((candidate) => candidate.id === id);
|
|
551
|
+
if (!schema || schema.version !== version) {
|
|
552
|
+
throw new MermanInvalidTransportError(
|
|
553
|
+
`Merman runtime catalog must advertise payload schema ${id} version ${version}.`,
|
|
554
|
+
);
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
function validateOperationRelations({ capabilityIds, operationIds, outputIds }) {
|
|
560
|
+
const capabilityIdSet = new Set(capabilityIds);
|
|
561
|
+
const outputIdSet = new Set(outputIds);
|
|
562
|
+
for (const id of operationIds) {
|
|
563
|
+
const expectation = OPERATION_EXPECTATION_BY_ID.get(id);
|
|
564
|
+
if (!expectation) continue;
|
|
565
|
+
if (
|
|
566
|
+
expectation.availability_capability_id !== null &&
|
|
567
|
+
!capabilityIdSet.has(expectation.availability_capability_id)
|
|
568
|
+
) {
|
|
569
|
+
throw new MermanInvalidTransportError(
|
|
570
|
+
`Merman runtime catalog operation ${id} is missing its advertised capability.`,
|
|
571
|
+
);
|
|
572
|
+
}
|
|
573
|
+
if (expectation.output_id !== null && !outputIdSet.has(expectation.output_id)) {
|
|
574
|
+
throw new MermanInvalidTransportError(
|
|
575
|
+
`Merman runtime catalog operation ${id} is missing its output contract.`,
|
|
576
|
+
);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
function validateCapabilityImplications(capabilityIds) {
|
|
582
|
+
const capabilityIdSet = new Set(capabilityIds);
|
|
583
|
+
for (const capabilityId of capabilityIds) {
|
|
584
|
+
const spec = CAPABILITY_SPEC_BY_ID.get(capabilityId);
|
|
585
|
+
if (spec === undefined) continue;
|
|
586
|
+
for (const implicationId of spec.implication_ids) {
|
|
587
|
+
if (!capabilityIdSet.has(implicationId)) {
|
|
588
|
+
throw new MermanInvalidTransportError(
|
|
589
|
+
`Merman runtime capability ${capabilityId} is missing implied capability ${implicationId}.`,
|
|
590
|
+
);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function validateMetadataIds(value, capabilityIds) {
|
|
597
|
+
const ids = sortedUniqueStrings(value, "metadata_ids");
|
|
598
|
+
for (const id of ids) {
|
|
599
|
+
const spec = METADATA_SPEC_BY_ID.get(id);
|
|
600
|
+
if (
|
|
601
|
+
spec?.required_capability_id !== null &&
|
|
602
|
+
spec?.required_capability_id !== undefined &&
|
|
603
|
+
!capabilityIds.has(spec.required_capability_id)
|
|
604
|
+
) {
|
|
605
|
+
throw new MermanInvalidTransportError(
|
|
606
|
+
`Merman runtime metadata ${id} requires capability ${spec.required_capability_id}.`,
|
|
607
|
+
);
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
return ids;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
function validateOptionGroupIds(catalog, { capabilityIds, usesSvgPipeline }) {
|
|
614
|
+
if (!Object.hasOwn(catalog, "option_group_ids")) return [];
|
|
615
|
+
const ids = sortedUniqueFieldIdentifiers(catalog.option_group_ids, "option_group_ids");
|
|
616
|
+
const applicable = new Set();
|
|
617
|
+
for (const spec of BINDING_OPTION_GROUP_SPECS) {
|
|
618
|
+
if (
|
|
619
|
+
spec.always_available ||
|
|
620
|
+
(spec.requires_svg_pipeline && usesSvgPipeline) ||
|
|
621
|
+
spec.any_capability_ids.some((id) => capabilityIds.has(id))
|
|
622
|
+
) {
|
|
623
|
+
applicable.add(spec.id);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
|
|
627
|
+
const knownIds = ids.filter((id) => OPTION_GROUP_SPEC_BY_ID.has(id));
|
|
628
|
+
if (
|
|
629
|
+
!knownIds.every((id) => applicable.has(id)) ||
|
|
630
|
+
!sameOrderedStrings(knownIds, NODE_WIRE_CONTRACT.artifact.option_group_ids)
|
|
631
|
+
) {
|
|
632
|
+
throw new MermanInvalidTransportError(
|
|
633
|
+
"Merman runtime catalog option groups do not match the artifact capability closure.",
|
|
634
|
+
);
|
|
635
|
+
}
|
|
636
|
+
return ids;
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
function validateConstructorServices(catalog, {
|
|
640
|
+
textMeasurementProviderIds,
|
|
641
|
+
usesSvgPipeline,
|
|
642
|
+
}) {
|
|
643
|
+
const hasIds = Object.hasOwn(catalog, "constructor_service_ids");
|
|
644
|
+
const hasContracts = Object.hasOwn(catalog, "constructor_service_contracts");
|
|
645
|
+
if (!hasIds && !hasContracts) return { ids: [], contracts: [] };
|
|
646
|
+
if (!hasIds || !hasContracts) {
|
|
647
|
+
throw new MermanInvalidTransportError(
|
|
648
|
+
"Merman runtime catalog must advertise constructor service IDs and contracts together.",
|
|
649
|
+
);
|
|
650
|
+
}
|
|
651
|
+
const ids = sortedUniqueStrings(
|
|
652
|
+
catalog.constructor_service_ids,
|
|
653
|
+
"constructor_service_ids",
|
|
654
|
+
);
|
|
655
|
+
validateKnownIds(
|
|
656
|
+
ids,
|
|
657
|
+
NODE_WIRE_CONTRACT.artifact.constructor_service_ids,
|
|
658
|
+
(id) => CONSTRUCTOR_SERVICE_SPEC_BY_ID.has(id),
|
|
659
|
+
"constructor services",
|
|
660
|
+
);
|
|
661
|
+
const contracts = catalog.constructor_service_contracts;
|
|
662
|
+
if (!Array.isArray(contracts)) {
|
|
663
|
+
throw new MermanInvalidTransportError(
|
|
664
|
+
"Merman runtime catalog has invalid constructor service contracts.",
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
const contractIds = [];
|
|
668
|
+
const providerOwners = new Map();
|
|
669
|
+
for (const contract of contracts) {
|
|
670
|
+
if (
|
|
671
|
+
!isPlainObject(contract) ||
|
|
672
|
+
typeof contract.id !== "string" ||
|
|
673
|
+
contract.id.length === 0 ||
|
|
674
|
+
!Array.isArray(contract.resource_limits)
|
|
675
|
+
) {
|
|
676
|
+
throw new MermanInvalidTransportError(
|
|
677
|
+
"Merman runtime catalog has an invalid constructor service contract.",
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
const providerIds = sortedUniqueStrings(
|
|
681
|
+
contract.provided_text_measurement_provider_ids,
|
|
682
|
+
`constructor_service_contracts.${contract.id}.provided_text_measurement_provider_ids`,
|
|
683
|
+
);
|
|
684
|
+
validateConstructorResourceLimits(contract.resource_limits, contract.id);
|
|
685
|
+
for (const providerId of providerIds) {
|
|
686
|
+
if (providerOwners.has(providerId)) {
|
|
687
|
+
throw new MermanInvalidTransportError(
|
|
688
|
+
"Merman runtime catalog text measurement providers must have one constructor service owner.",
|
|
689
|
+
);
|
|
690
|
+
}
|
|
691
|
+
providerOwners.set(providerId, contract.id);
|
|
692
|
+
if (!textMeasurementProviderIds.has(providerId)) {
|
|
693
|
+
throw new MermanInvalidTransportError(
|
|
694
|
+
"Merman runtime catalog constructor services name an unavailable text measurement provider.",
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const providerSpec = TEXT_MEASUREMENT_PROVIDER_SPEC_BY_ID.get(providerId);
|
|
698
|
+
if (
|
|
699
|
+
providerSpec !== undefined &&
|
|
700
|
+
(providerSpec.source !== "constructor-service" ||
|
|
701
|
+
providerSpec.constructor_service_id !== contract.id)
|
|
702
|
+
) {
|
|
703
|
+
throw new MermanInvalidTransportError(
|
|
704
|
+
"Merman runtime catalog constructor service provider ownership is inconsistent.",
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
const knownSpec = CONSTRUCTOR_SERVICE_SPEC_BY_ID.get(contract.id);
|
|
709
|
+
if (knownSpec?.requires_svg_pipeline && !usesSvgPipeline) {
|
|
710
|
+
throw new MermanInvalidTransportError(
|
|
711
|
+
"Merman runtime catalog advertises a constructor service without an SVG pipeline.",
|
|
712
|
+
);
|
|
713
|
+
}
|
|
714
|
+
if (
|
|
715
|
+
knownSpec !== undefined &&
|
|
716
|
+
!sameStringSet(
|
|
717
|
+
providerIds,
|
|
718
|
+
new Set(knownSpec.provided_text_measurement_provider_ids),
|
|
719
|
+
)
|
|
720
|
+
) {
|
|
721
|
+
throw new MermanInvalidTransportError(
|
|
722
|
+
"Merman runtime catalog constructor service providers do not match the generated contract.",
|
|
723
|
+
);
|
|
724
|
+
}
|
|
725
|
+
contractIds.push(contract.id);
|
|
726
|
+
}
|
|
727
|
+
sortedUniqueStrings(contractIds, "constructor_service_contracts.id");
|
|
728
|
+
if (!sameOrderedStrings(contractIds, ids)) {
|
|
729
|
+
throw new MermanInvalidTransportError(
|
|
730
|
+
"Merman runtime catalog constructor service contracts do not match constructor_service_ids.",
|
|
731
|
+
);
|
|
732
|
+
}
|
|
733
|
+
for (const id of ids) {
|
|
734
|
+
if (
|
|
735
|
+
CONSTRUCTOR_SERVICE_SPEC_BY_ID.has(id) &&
|
|
736
|
+
!NODE_CONSTRUCTOR_SERVICE_CANDIDATE_IDS.has(id)
|
|
737
|
+
) {
|
|
738
|
+
throw new MermanInvalidTransportError(
|
|
739
|
+
"Merman runtime catalog advertises a constructor service unavailable through the Node facade.",
|
|
740
|
+
);
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
return { ids, contracts };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function validateConstructorResourceLimits(limits, serviceId) {
|
|
747
|
+
const ids = [];
|
|
748
|
+
for (const limit of limits) {
|
|
749
|
+
if (
|
|
750
|
+
!isPlainObject(limit) ||
|
|
751
|
+
typeof limit.id !== "string" ||
|
|
752
|
+
limit.id.length === 0 ||
|
|
753
|
+
typeof limit.phase !== "string" ||
|
|
754
|
+
limit.phase.length === 0 ||
|
|
755
|
+
typeof limit.unit !== "string" ||
|
|
756
|
+
limit.unit.length === 0 ||
|
|
757
|
+
typeof limit.description !== "string" ||
|
|
758
|
+
limit.description.length === 0 ||
|
|
759
|
+
!safeInteger(limit.value) ||
|
|
760
|
+
limit.value < 0
|
|
761
|
+
) {
|
|
762
|
+
throw new MermanInvalidTransportError(
|
|
763
|
+
`Merman runtime catalog constructor service ${serviceId} has an invalid resource limit.`,
|
|
764
|
+
);
|
|
765
|
+
}
|
|
766
|
+
ids.push(limit.id);
|
|
767
|
+
}
|
|
768
|
+
sortedUniqueFieldIdentifiers(
|
|
769
|
+
ids,
|
|
770
|
+
`constructor_service_contracts.${serviceId}.resource_limits.id`,
|
|
771
|
+
);
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function validateOutputContracts(value, outputIds) {
|
|
775
|
+
if (!Array.isArray(value)) {
|
|
776
|
+
throw new MermanInvalidTransportError(
|
|
777
|
+
"Merman runtime catalog has invalid output contracts.",
|
|
778
|
+
);
|
|
779
|
+
}
|
|
780
|
+
const contractIds = [];
|
|
781
|
+
for (const contract of value) {
|
|
782
|
+
if (
|
|
783
|
+
!isPlainObject(contract) ||
|
|
784
|
+
typeof contract.id !== "string" ||
|
|
785
|
+
contract.id.length === 0 ||
|
|
786
|
+
typeof contract.media_type !== "string" ||
|
|
787
|
+
contract.media_type.length === 0
|
|
788
|
+
) {
|
|
789
|
+
throw new MermanInvalidTransportError(
|
|
790
|
+
"Merman runtime catalog has an invalid output contract.",
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
contractIds.push(contract.id);
|
|
794
|
+
assertUtf8Field(
|
|
795
|
+
contract.media_type,
|
|
796
|
+
`runtime catalog output_contracts.${contract.id}.media_type`,
|
|
797
|
+
NODE_TRANSPORT_FIELD_LIMITS.media_type_utf8_bytes,
|
|
798
|
+
);
|
|
799
|
+
validateSystemFonts(contract.system_fonts);
|
|
800
|
+
validateEmbeddedImages(contract.embedded_images);
|
|
801
|
+
const known = KNOWN_OUTPUT_CONTRACT_BY_ID.get(contract.id);
|
|
802
|
+
if (
|
|
803
|
+
known !== undefined &&
|
|
804
|
+
(contract.media_type !== known.media_type ||
|
|
805
|
+
!sameJsonValue(contract.system_fonts, known.system_fonts) ||
|
|
806
|
+
!sameJsonValue(contract.embedded_images, known.embedded_images))
|
|
807
|
+
) {
|
|
808
|
+
throw new MermanInvalidTransportError(
|
|
809
|
+
`Merman runtime catalog output contract ${contract.id} disagrees with the generated artifact profile.`,
|
|
810
|
+
);
|
|
811
|
+
}
|
|
812
|
+
}
|
|
813
|
+
if (
|
|
814
|
+
contractIds.some((id, index) => index > 0 && contractIds[index - 1] >= id) ||
|
|
815
|
+
contractIds.length !== outputIds.length ||
|
|
816
|
+
contractIds.some((id, index) => id !== outputIds[index])
|
|
817
|
+
) {
|
|
818
|
+
throw new MermanInvalidTransportError(
|
|
819
|
+
"Merman runtime catalog output contracts do not match output_ids.",
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
function validateSystemFonts(value) {
|
|
825
|
+
if (value === null) return;
|
|
826
|
+
if (
|
|
827
|
+
!isPlainObject(value) ||
|
|
828
|
+
typeof value.source_id !== "string" ||
|
|
829
|
+
!RUNTIME_CATALOG_IDENTIFIER.test(value.source_id) ||
|
|
830
|
+
typeof value.discovery !== "string" ||
|
|
831
|
+
!RUNTIME_CATALOG_IDENTIFIER.test(value.discovery) ||
|
|
832
|
+
typeof value.cache_scope !== "string" ||
|
|
833
|
+
!RUNTIME_CATALOG_IDENTIFIER.test(value.cache_scope) ||
|
|
834
|
+
typeof value.host_dependent !== "boolean" ||
|
|
835
|
+
typeof value.caller_configurable !== "boolean" ||
|
|
836
|
+
typeof value.resource_bounded !== "boolean"
|
|
837
|
+
) {
|
|
838
|
+
throw new MermanInvalidTransportError(
|
|
839
|
+
"Merman runtime catalog has an invalid system-font output contract.",
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
function validateEmbeddedImages(value) {
|
|
845
|
+
if (value === null) return;
|
|
846
|
+
if (
|
|
847
|
+
!isPlainObject(value) ||
|
|
848
|
+
typeof value.filesystem_access !== "boolean" ||
|
|
849
|
+
typeof value.network_access !== "boolean" ||
|
|
850
|
+
typeof value.caller_configurable !== "boolean" ||
|
|
851
|
+
!isPlainObject(value.limits)
|
|
852
|
+
) {
|
|
853
|
+
throw new MermanInvalidTransportError(
|
|
854
|
+
"Merman runtime catalog has an invalid embedded-image output contract.",
|
|
855
|
+
);
|
|
856
|
+
}
|
|
857
|
+
sortedUniqueStrings(value.source_ids, "embedded_images.source_ids");
|
|
858
|
+
for (const field of [
|
|
859
|
+
"max_bytes_per_image",
|
|
860
|
+
"max_total_bytes",
|
|
861
|
+
"max_pixels_per_image",
|
|
862
|
+
"max_total_pixels",
|
|
863
|
+
]) {
|
|
864
|
+
const limit = value.limits[field];
|
|
865
|
+
if (limit !== null && !positiveSafeInteger(limit)) {
|
|
866
|
+
throw new MermanInvalidTransportError(
|
|
867
|
+
`Merman runtime catalog embedded-image ${field} must be null or a positive safe integer.`,
|
|
868
|
+
);
|
|
869
|
+
}
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
function validateTextMeasurement(value, { requiresSvgPipeline }) {
|
|
874
|
+
if (value === null && !requiresSvgPipeline) return [];
|
|
875
|
+
if (
|
|
876
|
+
!isPlainObject(value) ||
|
|
877
|
+
value.protocol_version !== TEXT_MEASUREMENT_PROTOCOL_VERSION
|
|
878
|
+
) {
|
|
879
|
+
throw new MermanInvalidTransportError(
|
|
880
|
+
"Merman runtime catalog has invalid text measurement metadata.",
|
|
881
|
+
);
|
|
882
|
+
}
|
|
883
|
+
const providerIds = sortedUniqueStrings(
|
|
884
|
+
value.provider_ids,
|
|
885
|
+
"text_measurement.provider_ids",
|
|
886
|
+
);
|
|
887
|
+
if (providerIds.length === 0) {
|
|
888
|
+
throw new MermanInvalidTransportError(
|
|
889
|
+
"Merman runtime catalog text measurement must advertise at least one provider.",
|
|
890
|
+
);
|
|
891
|
+
}
|
|
892
|
+
if (
|
|
893
|
+
providerIds.some(
|
|
894
|
+
(id) =>
|
|
895
|
+
KNOWN_TEXT_MEASUREMENT_PROVIDER_IDS.has(id) &&
|
|
896
|
+
id !== VENDORED_TEXT_MEASUREMENT_PROVIDER_ID,
|
|
897
|
+
)
|
|
898
|
+
) {
|
|
899
|
+
throw new MermanInvalidTransportError(
|
|
900
|
+
"Merman runtime catalog advertises a known text measurement provider unavailable through the Node facade.",
|
|
901
|
+
);
|
|
902
|
+
}
|
|
903
|
+
if (
|
|
904
|
+
requiresSvgPipeline &&
|
|
905
|
+
!providerIds.includes(VENDORED_TEXT_MEASUREMENT_PROVIDER_ID)
|
|
906
|
+
) {
|
|
907
|
+
throw new MermanInvalidTransportError(
|
|
908
|
+
"Merman runtime catalog must expose the callable vendored text measurement provider for the SVG pipeline.",
|
|
909
|
+
);
|
|
910
|
+
}
|
|
911
|
+
return providerIds;
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
function validateRegistry(value) {
|
|
915
|
+
if (
|
|
916
|
+
!safeInteger(value.diagram_family_count) ||
|
|
917
|
+
value.diagram_family_count < 0
|
|
918
|
+
) {
|
|
919
|
+
throw new MermanInvalidTransportError(
|
|
920
|
+
"Merman runtime catalog has invalid registry metadata.",
|
|
921
|
+
);
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
function validateResources(value, operationIds) {
|
|
926
|
+
if (
|
|
927
|
+
typeof value.general_binding_default_profile !== "string" ||
|
|
928
|
+
!RUNTIME_CATALOG_IDENTIFIER.test(value.general_binding_default_profile) ||
|
|
929
|
+
typeof value.cli_default_profile !== "string" ||
|
|
930
|
+
!RUNTIME_CATALOG_IDENTIFIER.test(value.cli_default_profile) ||
|
|
931
|
+
!Array.isArray(value.limits) ||
|
|
932
|
+
value.limits.length === 0 ||
|
|
933
|
+
!Array.isArray(value.profiles) ||
|
|
934
|
+
value.profiles.length === 0
|
|
935
|
+
) {
|
|
936
|
+
throw new MermanInvalidTransportError(
|
|
937
|
+
"Merman runtime catalog has invalid resource metadata.",
|
|
938
|
+
);
|
|
939
|
+
}
|
|
940
|
+
const limitIds = new Set();
|
|
941
|
+
const limitMinimums = new Map();
|
|
942
|
+
const hardCapIds = new Set();
|
|
943
|
+
for (const limit of value.limits) {
|
|
944
|
+
if (
|
|
945
|
+
!isPlainObject(limit) ||
|
|
946
|
+
typeof limit.id !== "string" ||
|
|
947
|
+
!RUNTIME_CATALOG_FIELD_IDENTIFIER.test(limit.id) ||
|
|
948
|
+
limitIds.has(limit.id) ||
|
|
949
|
+
typeof limit.phase !== "string" ||
|
|
950
|
+
limit.phase.length === 0 ||
|
|
951
|
+
typeof limit.description !== "string" ||
|
|
952
|
+
limit.description.length === 0 ||
|
|
953
|
+
typeof limit.overridable !== "boolean" ||
|
|
954
|
+
typeof limit.hard_cap !== "boolean" ||
|
|
955
|
+
!safeInteger(limit.minimum_value) ||
|
|
956
|
+
limit.minimum_value < 0 ||
|
|
957
|
+
!Array.isArray(limit.operation_ids)
|
|
958
|
+
) {
|
|
959
|
+
throw new MermanInvalidTransportError(
|
|
960
|
+
"Merman runtime catalog has an invalid resource limit.",
|
|
961
|
+
);
|
|
962
|
+
}
|
|
963
|
+
const limitOperationIds = sortedUniqueStrings(
|
|
964
|
+
limit.operation_ids,
|
|
965
|
+
`resources.limits.${limit.id}.operation_ids`,
|
|
966
|
+
);
|
|
967
|
+
if (!limitOperationIds.every((id) => operationIds.has(id))) {
|
|
968
|
+
throw new MermanInvalidTransportError(
|
|
969
|
+
"Merman runtime catalog resource limit names an unavailable operation.",
|
|
970
|
+
);
|
|
971
|
+
}
|
|
972
|
+
if (limit.hard_cap && limit.overridable) {
|
|
973
|
+
throw new MermanInvalidTransportError(
|
|
974
|
+
"Merman runtime catalog hard resource limits cannot be overridable.",
|
|
975
|
+
);
|
|
976
|
+
}
|
|
977
|
+
limitIds.add(limit.id);
|
|
978
|
+
limitMinimums.set(limit.id, limit.minimum_value);
|
|
979
|
+
if (limit.hard_cap) hardCapIds.add(limit.id);
|
|
980
|
+
}
|
|
981
|
+
const profileIds = new Set();
|
|
982
|
+
for (const profile of value.profiles) {
|
|
983
|
+
if (
|
|
984
|
+
!isPlainObject(profile) ||
|
|
985
|
+
typeof profile.id !== "string" ||
|
|
986
|
+
!RUNTIME_CATALOG_IDENTIFIER.test(profile.id) ||
|
|
987
|
+
profileIds.has(profile.id) ||
|
|
988
|
+
typeof profile.purpose !== "string" ||
|
|
989
|
+
profile.purpose.length === 0 ||
|
|
990
|
+
typeof profile.trust_assumption !== "string" ||
|
|
991
|
+
profile.trust_assumption.length === 0 ||
|
|
992
|
+
typeof profile.recommended_binding_default !== "boolean" ||
|
|
993
|
+
!isPlainObject(profile.limits)
|
|
994
|
+
) {
|
|
995
|
+
throw new MermanInvalidTransportError(
|
|
996
|
+
"Merman runtime catalog has an invalid resource profile.",
|
|
997
|
+
);
|
|
998
|
+
}
|
|
999
|
+
const profileLimitIds = Object.keys(profile.limits);
|
|
1000
|
+
if (!sameStringSet(profileLimitIds, limitIds)) {
|
|
1001
|
+
throw new MermanInvalidTransportError(
|
|
1002
|
+
"Merman runtime catalog resource profile does not cover the declared limits.",
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
for (const [id, limit] of Object.entries(profile.limits)) {
|
|
1006
|
+
if (
|
|
1007
|
+
(limit === null && hardCapIds.has(id)) ||
|
|
1008
|
+
(limit !== null &&
|
|
1009
|
+
(!safeInteger(limit) || limit < limitMinimums.get(id)))
|
|
1010
|
+
) {
|
|
1011
|
+
throw new MermanInvalidTransportError(
|
|
1012
|
+
"Merman runtime catalog resource profile has an invalid limit.",
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
profileIds.add(profile.id);
|
|
1017
|
+
}
|
|
1018
|
+
if (
|
|
1019
|
+
!profileIds.has(value.general_binding_default_profile) ||
|
|
1020
|
+
!profileIds.has(value.cli_default_profile)
|
|
1021
|
+
) {
|
|
1022
|
+
throw new MermanInvalidTransportError(
|
|
1023
|
+
"Merman runtime catalog resource defaults do not name declared profiles.",
|
|
1024
|
+
);
|
|
1025
|
+
}
|
|
1026
|
+
const generalDefault = value.profiles.find(
|
|
1027
|
+
(profile) => profile.id === value.general_binding_default_profile,
|
|
1028
|
+
);
|
|
1029
|
+
const recommendedProfiles = value.profiles.filter(
|
|
1030
|
+
(profile) => profile.recommended_binding_default === true,
|
|
1031
|
+
);
|
|
1032
|
+
if (
|
|
1033
|
+
recommendedProfiles.length !== 1 ||
|
|
1034
|
+
recommendedProfiles[0].id !== generalDefault.id
|
|
1035
|
+
) {
|
|
1036
|
+
throw new MermanInvalidTransportError(
|
|
1037
|
+
"Merman runtime catalog must recommend exactly the general binding default profile.",
|
|
1038
|
+
);
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function sameStringSet(values, expected) {
|
|
1043
|
+
return values.length === expected.size && values.every((value) => expected.has(value));
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function sameOrderedStrings(left, right) {
|
|
1047
|
+
return left.length === right.length && left.every((value, index) => value === right[index]);
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
function positiveSafeInteger(value) {
|
|
1051
|
+
return safeInteger(value) && value > 0;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function safeInteger(value) {
|
|
1055
|
+
return Number.isInteger(value) && Math.abs(value) <= RUNTIME_CATALOG_MAX_SAFE_INTEGER;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
function sortedUniquePositiveIntegers(value, field) {
|
|
1059
|
+
if (
|
|
1060
|
+
!Array.isArray(value) ||
|
|
1061
|
+
value.some((item) => !positiveSafeInteger(item)) ||
|
|
1062
|
+
value.some((item, index) => index > 0 && value[index - 1] >= item)
|
|
1063
|
+
) {
|
|
1064
|
+
throw new MermanInvalidTransportError(
|
|
1065
|
+
`Merman runtime catalog field ${field} must be a sorted unique array of positive integers.`,
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1068
|
+
return value;
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
function sortedUniqueStrings(value, field) {
|
|
1072
|
+
sortedUniqueNonEmptyStrings(value, field);
|
|
1073
|
+
if (value.some((item) => !RUNTIME_CATALOG_IDENTIFIER.test(item))) {
|
|
1074
|
+
throw new MermanInvalidTransportError(
|
|
1075
|
+
`Merman runtime catalog ${field} must contain valid identifiers.`,
|
|
1076
|
+
);
|
|
1077
|
+
}
|
|
1078
|
+
for (const item of value) {
|
|
1079
|
+
assertUtf8Field(
|
|
1080
|
+
item,
|
|
1081
|
+
`runtime catalog ${field}`,
|
|
1082
|
+
NODE_TRANSPORT_FIELD_LIMITS.capability_id_utf8_bytes,
|
|
1083
|
+
);
|
|
1084
|
+
}
|
|
1085
|
+
return value;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function sortedUniqueNonEmptyStrings(value, field) {
|
|
1089
|
+
if (
|
|
1090
|
+
!Array.isArray(value) ||
|
|
1091
|
+
value.some((item) => typeof item !== "string" || item.length === 0) ||
|
|
1092
|
+
value.some((item, index) => index > 0 && value[index - 1] >= item)
|
|
1093
|
+
) {
|
|
1094
|
+
throw new MermanInvalidTransportError(
|
|
1095
|
+
`Merman runtime catalog ${field} must contain sorted unique non-empty strings.`,
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
return value;
|
|
1099
|
+
}
|
|
1100
|
+
|
|
1101
|
+
function sortedUniqueFieldIdentifiers(value, field) {
|
|
1102
|
+
sortedUniqueNonEmptyStrings(value, field);
|
|
1103
|
+
if (value.some((item) => !RUNTIME_CATALOG_FIELD_IDENTIFIER.test(item))) {
|
|
1104
|
+
throw new MermanInvalidTransportError(
|
|
1105
|
+
`Merman runtime catalog ${field} must contain valid field identifiers.`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
for (const item of value) {
|
|
1109
|
+
assertUtf8Field(
|
|
1110
|
+
item,
|
|
1111
|
+
`runtime catalog ${field}`,
|
|
1112
|
+
NODE_TRANSPORT_FIELD_LIMITS.capability_id_utf8_bytes,
|
|
1113
|
+
);
|
|
1114
|
+
}
|
|
1115
|
+
return value;
|
|
1116
|
+
}
|
|
1117
|
+
|
|
1118
|
+
function cloneBoundedJsonValue(root, label) {
|
|
1119
|
+
const limits = NODE_TRANSPORT_LIMITS.binding_options;
|
|
1120
|
+
const active = new WeakSet();
|
|
1121
|
+
const clone = jsonContainerClone(root, label);
|
|
1122
|
+
const stack = [{ depth: 1, source: root, target: clone, exiting: false }];
|
|
1123
|
+
let members = 0;
|
|
1124
|
+
let tokens = 1;
|
|
1125
|
+
|
|
1126
|
+
while (stack.length > 0) {
|
|
1127
|
+
const frame = stack.pop();
|
|
1128
|
+
if (frame.exiting) {
|
|
1129
|
+
active.delete(frame.source);
|
|
1130
|
+
continue;
|
|
1131
|
+
}
|
|
1132
|
+
if (active.has(frame.source)) {
|
|
1133
|
+
throw new TypeError(`${label} must not contain cyclic references.`);
|
|
1134
|
+
}
|
|
1135
|
+
if (frame.depth > limits.max_depth) {
|
|
1136
|
+
throw new RangeError(`${label} exceeds the structural depth limit ${limits.max_depth}.`);
|
|
1137
|
+
}
|
|
1138
|
+
active.add(frame.source);
|
|
1139
|
+
stack.push({ ...frame, exiting: true });
|
|
1140
|
+
|
|
1141
|
+
const entries = jsonContainerEntries(
|
|
1142
|
+
frame.source,
|
|
1143
|
+
label,
|
|
1144
|
+
limits.max_members - members,
|
|
1145
|
+
limits.max_members,
|
|
1146
|
+
);
|
|
1147
|
+
members += entries.length;
|
|
1148
|
+
if (members > limits.max_members) {
|
|
1149
|
+
throw new RangeError(`${label} exceeds the member-work limit ${limits.max_members}.`);
|
|
1150
|
+
}
|
|
1151
|
+
|
|
1152
|
+
for (let index = entries.length - 1; index >= 0; index -= 1) {
|
|
1153
|
+
const [key, child] = entries[index];
|
|
1154
|
+
assertUtf8Field(key, `${label} object key`, limits.max_string_utf8_bytes);
|
|
1155
|
+
tokens += 1;
|
|
1156
|
+
if (tokens > limits.max_tokens) {
|
|
1157
|
+
throw new RangeError(`${label} exceeds the token-work limit ${limits.max_tokens}.`);
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
if (child === null || typeof child === "boolean") {
|
|
1161
|
+
defineJsonValue(frame.target, key, child);
|
|
1162
|
+
} else if (typeof child === "string") {
|
|
1163
|
+
assertUtf8Field(child, `${label}.${key}`, limits.max_string_utf8_bytes);
|
|
1164
|
+
defineJsonValue(frame.target, key, child);
|
|
1165
|
+
} else if (typeof child === "number") {
|
|
1166
|
+
if (!Number.isFinite(child)) {
|
|
1167
|
+
throw new TypeError(`${label}.${key} must be a finite JSON number.`);
|
|
1168
|
+
}
|
|
1169
|
+
defineJsonValue(frame.target, key, child);
|
|
1170
|
+
} else if (typeof child === "object") {
|
|
1171
|
+
const childClone = jsonContainerClone(child, `${label}.${key}`);
|
|
1172
|
+
defineJsonValue(frame.target, key, childClone);
|
|
1173
|
+
stack.push({
|
|
1174
|
+
depth: frame.depth + 1,
|
|
1175
|
+
source: child,
|
|
1176
|
+
target: childClone,
|
|
1177
|
+
exiting: false,
|
|
1178
|
+
});
|
|
1179
|
+
} else {
|
|
1180
|
+
throw new TypeError(`${label}.${key} is not a JSON wire value.`);
|
|
1181
|
+
}
|
|
1182
|
+
}
|
|
1183
|
+
}
|
|
1184
|
+
return clone;
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function jsonContainerClone(value, label) {
|
|
1188
|
+
if (Array.isArray(value)) return [];
|
|
1189
|
+
if (!isPlainObject(value)) {
|
|
1190
|
+
throw new TypeError(`${label} must contain only arrays and plain objects.`);
|
|
1191
|
+
}
|
|
1192
|
+
return Object.getPrototypeOf(value) === null ? Object.create(null) : {};
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function jsonContainerEntries(value, label, remainingMembers, maxMembers) {
|
|
1196
|
+
if (Array.isArray(value)) {
|
|
1197
|
+
if (value.length > remainingMembers) {
|
|
1198
|
+
throw new RangeError(`${label} exceeds the member-work limit ${maxMembers}.`);
|
|
1199
|
+
}
|
|
1200
|
+
const keys = Reflect.ownKeys(value);
|
|
1201
|
+
if (keys.some((key) => typeof key === "symbol")) {
|
|
1202
|
+
throw new TypeError(`${label} must not contain symbol keys.`);
|
|
1203
|
+
}
|
|
1204
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1205
|
+
if (!Object.hasOwn(value, index)) {
|
|
1206
|
+
throw new TypeError(`${label} arrays must not contain holes.`);
|
|
1207
|
+
}
|
|
1208
|
+
}
|
|
1209
|
+
if (keys.length !== value.length + 1) {
|
|
1210
|
+
throw new TypeError(`${label} arrays must not contain custom properties.`);
|
|
1211
|
+
}
|
|
1212
|
+
const entries = new Array(value.length);
|
|
1213
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1214
|
+
const key = String(index);
|
|
1215
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1216
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) {
|
|
1217
|
+
throw new TypeError(`${label}.${key} must be an enumerable data property.`);
|
|
1218
|
+
}
|
|
1219
|
+
entries[index] = [key, descriptor.value];
|
|
1220
|
+
}
|
|
1221
|
+
return entries;
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
const entries = [];
|
|
1225
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
1226
|
+
if (typeof key === "symbol") {
|
|
1227
|
+
throw new TypeError(`${label} must not contain symbol keys.`);
|
|
1228
|
+
}
|
|
1229
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1230
|
+
if (!descriptor?.enumerable || !("value" in descriptor)) {
|
|
1231
|
+
throw new TypeError(`${label}.${key} must be an enumerable data property.`);
|
|
1232
|
+
}
|
|
1233
|
+
entries.push([key, descriptor.value]);
|
|
1234
|
+
}
|
|
1235
|
+
return entries;
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
function defineJsonValue(target, key, value) {
|
|
1239
|
+
if (Array.isArray(target)) {
|
|
1240
|
+
target[Number(key)] = value;
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
Object.defineProperty(target, key, {
|
|
1244
|
+
configurable: true,
|
|
1245
|
+
enumerable: true,
|
|
1246
|
+
value,
|
|
1247
|
+
writable: true,
|
|
1248
|
+
});
|
|
1249
|
+
}
|
|
1250
|
+
|
|
1251
|
+
function isPlainObject(value) {
|
|
1252
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
1253
|
+
const prototype = Object.getPrototypeOf(value);
|
|
1254
|
+
return prototype === Object.prototype || prototype === null;
|
|
1255
|
+
}
|