@spine-event-engine/core 2.0.0-snapshot.2
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/LICENSE +201 -0
- package/README.md +151 -0
- package/REFERENCE.md +85 -0
- package/dist/index.d.ts +682 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1183 -0
- package/dist/index.js.map +1 -0
- package/dist/internal/subscription-lifecycle.d.ts +7 -0
- package/dist/internal/subscription-lifecycle.d.ts.map +1 -0
- package/dist/internal/subscription-lifecycle.js +20 -0
- package/dist/internal/subscription-lifecycle.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -0
- package/package.json +37 -0
package/dist/index.js
ADDED
|
@@ -0,0 +1,1183 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* Copyright 2026, CodeMatters. All rights reserved.
|
|
3
|
+
*
|
|
4
|
+
* Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except
|
|
5
|
+
* in compliance with the License. You may obtain a copy of the License at
|
|
6
|
+
*
|
|
7
|
+
* https://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
*
|
|
9
|
+
* Unless required by applicable law or agreed to in writing, software distributed under the License
|
|
10
|
+
* is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
|
|
11
|
+
* or implied. See the License for the specific language governing permissions and limitations under
|
|
12
|
+
* the License.
|
|
13
|
+
*/
|
|
14
|
+
import { clone, create, createRegistry, fromBinary, fromJsonString, getOption, hasOption, ScalarType, toBinary, toJsonString, } from "@bufbuild/protobuf";
|
|
15
|
+
import { base64Decode, base64Encode } from "@bufbuild/protobuf/wire";
|
|
16
|
+
import { AnySchema, Int32ValueSchema, Int64ValueSchema, StringValueSchema, } from "@bufbuild/protobuf/wkt";
|
|
17
|
+
import { validate as validateWithSpine } from "@spine-event-engine/validation";
|
|
18
|
+
import { ActorContextSchema, CommandContextSchema, CommandIdSchema, CommandSchema, CommandContext_ScheduleSchema, Command_SystemPropertiesSchema, ConstraintViolationSchema, EmailAddressSchema, EnrichmentSchema, Enrichment_ContainerSchema, EventContextSchema, EventIdSchema, EventSchema, FieldPathSchema, InternetDomainSchema, LocalDateSchema, LocalDateTimeSchema, LocalTimeSchema, MessageIdSchema, OriginSchema, RejectionEventContextSchema, TemplateStringSchema, TenantIdSchema, UserIdSchema, ValidationErrorSchema, VersionSchema, YearMonthSchema, ZoneIdSchema, ZonedDateTimeSchema, type_url_prefix, } from "@spine-event-engine/proto";
|
|
19
|
+
const EMPTY_VIOLATIONS = Object.freeze([]);
|
|
20
|
+
const REDACTED_VALIDATION_DETAIL = "[redacted]";
|
|
21
|
+
const VALIDATION_RUNTIME_FAILURE_MESSAGE = "Validation runtime failed.";
|
|
22
|
+
const TRANSITION_RULE_FAILURE_MESSAGE = "Transition validation rule failed.";
|
|
23
|
+
const REJECTION_CONSTRUCTOR = Symbol("RejectionThrowable");
|
|
24
|
+
const REJECTION_THROWABLES = new WeakSet();
|
|
25
|
+
/**
|
|
26
|
+
* Standard Protobuf `Any` prefix used when a file has no Spine type URL option.
|
|
27
|
+
*/
|
|
28
|
+
export const DEFAULT_TYPE_URL_PREFIX = "type.googleapis.com";
|
|
29
|
+
const MESSAGE_INTERFACE_TOKENS = new WeakSet();
|
|
30
|
+
function dataProperty(value, name) {
|
|
31
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, name);
|
|
32
|
+
return descriptor !== undefined && "value" in descriptor ? descriptor.value : undefined;
|
|
33
|
+
}
|
|
34
|
+
function indexedDataProperties(values) {
|
|
35
|
+
const length = dataProperty(values, "length");
|
|
36
|
+
if (typeof length !== "number" || !Number.isSafeInteger(length) || length < 0 || length > 1_000)
|
|
37
|
+
return undefined;
|
|
38
|
+
const indexed = [];
|
|
39
|
+
for (let index = 0; index < length; index += 1) {
|
|
40
|
+
const descriptor = Object.getOwnPropertyDescriptor(values, String(index));
|
|
41
|
+
if (descriptor === undefined || !("value" in descriptor))
|
|
42
|
+
return undefined;
|
|
43
|
+
indexed.push(descriptor.value);
|
|
44
|
+
}
|
|
45
|
+
return indexed;
|
|
46
|
+
}
|
|
47
|
+
function descriptorTreeIncludes(roots, target) {
|
|
48
|
+
const initial = indexedDataProperties(roots);
|
|
49
|
+
if (initial === undefined)
|
|
50
|
+
return false;
|
|
51
|
+
const pending = initial;
|
|
52
|
+
const visited = new Set();
|
|
53
|
+
let entries = 0;
|
|
54
|
+
while (pending.length > 0) {
|
|
55
|
+
if (pending.length > 1_000 || entries > 10_000)
|
|
56
|
+
return false;
|
|
57
|
+
const value = pending.pop();
|
|
58
|
+
entries += 1;
|
|
59
|
+
if (value === target)
|
|
60
|
+
return true;
|
|
61
|
+
if (typeof value !== "object" || value === null || visited.has(value))
|
|
62
|
+
continue;
|
|
63
|
+
visited.add(value);
|
|
64
|
+
const nested = dataProperty(value, "nestedMessages");
|
|
65
|
+
if (nested === undefined)
|
|
66
|
+
continue;
|
|
67
|
+
if (!Array.isArray(nested))
|
|
68
|
+
return false;
|
|
69
|
+
const children = indexedDataProperties(nested);
|
|
70
|
+
if (children === undefined || pending.length + children.length > 1_000)
|
|
71
|
+
return false;
|
|
72
|
+
pending.push(...children);
|
|
73
|
+
}
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
function isMessageSchema(value) {
|
|
77
|
+
try {
|
|
78
|
+
if (typeof value !== "object" || value === null)
|
|
79
|
+
return false;
|
|
80
|
+
const schema = value;
|
|
81
|
+
const file = dataProperty(schema, "file");
|
|
82
|
+
const proto = dataProperty(schema, "proto");
|
|
83
|
+
if (dataProperty(schema, "kind") !== "message" ||
|
|
84
|
+
typeof dataProperty(schema, "typeName") !== "string" ||
|
|
85
|
+
typeof proto !== "object" ||
|
|
86
|
+
proto === null ||
|
|
87
|
+
dataProperty(proto, "$typeName") !== "google.protobuf.DescriptorProto" ||
|
|
88
|
+
typeof file !== "object" ||
|
|
89
|
+
file === null ||
|
|
90
|
+
dataProperty(file, "kind") !== "file")
|
|
91
|
+
return false;
|
|
92
|
+
const fileProto = dataProperty(file, "proto");
|
|
93
|
+
const messages = dataProperty(file, "messages");
|
|
94
|
+
return (typeof fileProto === "object" &&
|
|
95
|
+
fileProto !== null &&
|
|
96
|
+
dataProperty(fileProto, "$typeName") === "google.protobuf.FileDescriptorProto" &&
|
|
97
|
+
Array.isArray(messages) &&
|
|
98
|
+
descriptorTreeIncludes(messages, schema));
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Creates and validates nominal generated message-interface tokens.
|
|
106
|
+
*/
|
|
107
|
+
export const MessageInterfaces = Object.freeze({
|
|
108
|
+
// prettier-ignore
|
|
109
|
+
/**
|
|
110
|
+
* Creates a nominal token from a non-empty tuple of generated message schemas.
|
|
111
|
+
*
|
|
112
|
+
* Every schema's message shape must implement `TInterface`. At runtime this
|
|
113
|
+
* factory rejects empty and malformed membership, deduplicates it, then copies
|
|
114
|
+
* and freezes the retained sequence. Factory availability is not an
|
|
115
|
+
* authenticity boundary: use `is` when accepting a runtime token candidate.
|
|
116
|
+
*
|
|
117
|
+
* @typeParam TInterface The common object shape of all member messages.
|
|
118
|
+
* @typeParam Schemas The concrete non-empty member-schema tuple.
|
|
119
|
+
* @param schemas Generated message schemas belonging to the interface.
|
|
120
|
+
* @returns A frozen nominal token with concrete schema membership.
|
|
121
|
+
*/
|
|
122
|
+
define(schemas) {
|
|
123
|
+
if (schemas.length === 0)
|
|
124
|
+
throw new Error("A message interface requires at least one schema.");
|
|
125
|
+
const uniqueSchemas = [];
|
|
126
|
+
const seen = new Set();
|
|
127
|
+
for (const schema of schemas) {
|
|
128
|
+
if (!isMessageSchema(schema)) {
|
|
129
|
+
throw new TypeError("A message interface requires generated message schemas.");
|
|
130
|
+
}
|
|
131
|
+
if (!seen.has(schema)) {
|
|
132
|
+
seen.add(schema);
|
|
133
|
+
uniqueSchemas.push(schema);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
const token = Object.freeze({ schemas: Object.freeze(uniqueSchemas) });
|
|
137
|
+
MESSAGE_INTERFACE_TOKENS.add(token);
|
|
138
|
+
return token;
|
|
139
|
+
},
|
|
140
|
+
/**
|
|
141
|
+
* Determines whether a value is the exact token instance created by this factory.
|
|
142
|
+
*
|
|
143
|
+
* Structural copies, prototype copies, serialized values, and hand-built
|
|
144
|
+
* lookalikes are rejected even when they expose matching schema membership.
|
|
145
|
+
*
|
|
146
|
+
* @param value The runtime value to inspect.
|
|
147
|
+
* @returns Whether `value` is a factory-created message-interface token.
|
|
148
|
+
*/
|
|
149
|
+
is(value) {
|
|
150
|
+
return typeof value === "object" && value !== null && MESSAGE_INTERFACE_TOKENS.has(value);
|
|
151
|
+
},
|
|
152
|
+
});
|
|
153
|
+
/**
|
|
154
|
+
* Error thrown when a Protobuf message fails Spine single-message validation.
|
|
155
|
+
*/
|
|
156
|
+
export class ValidationException extends Error {
|
|
157
|
+
// prettier-ignore
|
|
158
|
+
/**
|
|
159
|
+
* Constraint violations captured from the structured validation error.
|
|
160
|
+
*/
|
|
161
|
+
violations;
|
|
162
|
+
#messageData;
|
|
163
|
+
/**
|
|
164
|
+
* Creates an exception from structured Spine validation error data.
|
|
165
|
+
* @param messageData The validation error represented by this exception.
|
|
166
|
+
*/
|
|
167
|
+
constructor(messageData) {
|
|
168
|
+
super(`Message validation failed with ${String(messageData.constraintViolation.length)} violation(s).`);
|
|
169
|
+
this.name = "ValidationException";
|
|
170
|
+
this.#messageData = messageData;
|
|
171
|
+
this.violations = messageData.constraintViolation;
|
|
172
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
173
|
+
}
|
|
174
|
+
/**
|
|
175
|
+
* Returns the structured Spine `ValidationError` message data.
|
|
176
|
+
* @returns The validation error message.
|
|
177
|
+
*/
|
|
178
|
+
asMessage() {
|
|
179
|
+
return this.#messageData;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
let instantiateRejection;
|
|
183
|
+
/**
|
|
184
|
+
* A nominal domain rejection carrying its generated Protobuf message.
|
|
185
|
+
*/
|
|
186
|
+
export class RejectionThrowable extends Error {
|
|
187
|
+
#schema;
|
|
188
|
+
#messageData;
|
|
189
|
+
constructor(schema, messageData, token) {
|
|
190
|
+
super(`Rejected: ${schema.typeName}`);
|
|
191
|
+
if (token !== REJECTION_CONSTRUCTOR) {
|
|
192
|
+
throw new TypeError("RejectionThrowable must be created by its validated factory.");
|
|
193
|
+
}
|
|
194
|
+
this.name = "RejectionThrowable";
|
|
195
|
+
this.#schema = schema;
|
|
196
|
+
this.#messageData = RejectionThrowable.snapshot(schema, messageData);
|
|
197
|
+
Object.setPrototypeOf(this, new.target.prototype);
|
|
198
|
+
REJECTION_THROWABLES.add(this);
|
|
199
|
+
Object.preventExtensions(this);
|
|
200
|
+
}
|
|
201
|
+
static {
|
|
202
|
+
instantiateRejection = (schema, messageData) => new RejectionThrowable(schema, messageData, REJECTION_CONSTRUCTOR);
|
|
203
|
+
}
|
|
204
|
+
/**
|
|
205
|
+
* Returns the generated Protobuf-ES schema for the rejected domain signal.
|
|
206
|
+
* @returns The rejection schema.
|
|
207
|
+
*/
|
|
208
|
+
get schema() {
|
|
209
|
+
return this.#schema;
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Returns a defensive clone of the snapshotted rejection message.
|
|
213
|
+
* @returns The cloned rejection message.
|
|
214
|
+
*/
|
|
215
|
+
get messageData() {
|
|
216
|
+
return RejectionThrowable.snapshot(this.#schema, this.#messageData);
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Returns a defensive clone matching Spine JVM's throwable contract.
|
|
220
|
+
* @returns The cloned rejection message.
|
|
221
|
+
*/
|
|
222
|
+
messageThrown() {
|
|
223
|
+
return RejectionThrowable.snapshot(this.#schema, this.#messageData);
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Creates a nominal throwable from a validated generated rejection message.
|
|
227
|
+
* @param schema The generated rejection schema.
|
|
228
|
+
* @param input The rejection message fields.
|
|
229
|
+
* @returns The validated nominal rejection throwable.
|
|
230
|
+
*/
|
|
231
|
+
static create(schema, input) {
|
|
232
|
+
RejectionThrowable.assertSchema(schema);
|
|
233
|
+
return instantiateRejection(schema, Validate.check(schema, create(schema, input)));
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Checks whether a value is a factory-created domain rejection throwable.
|
|
237
|
+
* @param value The value to inspect.
|
|
238
|
+
* @returns Whether the value is a trusted rejection throwable.
|
|
239
|
+
*/
|
|
240
|
+
static is(value) {
|
|
241
|
+
return typeof value === "object" && value !== null && REJECTION_THROWABLES.has(value);
|
|
242
|
+
}
|
|
243
|
+
static assertSchema(schema) {
|
|
244
|
+
const basename = schema.file.proto.name.split("/").at(-1);
|
|
245
|
+
const rejectionSource = basename === "rejections.proto" || basename?.endsWith("_rejections.proto") === true;
|
|
246
|
+
if (schema.parent !== undefined || !rejectionSource) {
|
|
247
|
+
throw new TypeError(`Rejection schema "${schema.typeName}" must be a top-level message declared in a rejections.proto file.`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
static snapshot(schema, message) {
|
|
251
|
+
return fromBinary(schema, toBinary(schema, message));
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* Validates Spine messages and proposed state transitions.
|
|
256
|
+
*/
|
|
257
|
+
export const Validate = {
|
|
258
|
+
// prettier-ignore
|
|
259
|
+
/**
|
|
260
|
+
* Creates a repo-local validation error from constraint violations.
|
|
261
|
+
* @param violations The violations to include.
|
|
262
|
+
* @returns The validation error.
|
|
263
|
+
*/
|
|
264
|
+
createError(violations) {
|
|
265
|
+
return ValidationResults.error(violations);
|
|
266
|
+
},
|
|
267
|
+
/**
|
|
268
|
+
* Validates one Protobuf message through the Spine TS validation facade.
|
|
269
|
+
* @param schema The message schema.
|
|
270
|
+
* @param message The message to validate.
|
|
271
|
+
* @returns The sanitized validation result.
|
|
272
|
+
*/
|
|
273
|
+
message(schema, message) {
|
|
274
|
+
try {
|
|
275
|
+
return ValidationResults.from(validateWithSpine(schema, message).map((violation) => ValidationResults.violation(violation)));
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
return ValidationResults.from([
|
|
279
|
+
ValidationResults.failure(schema.typeName, VALIDATION_RUNTIME_FAILURE_MESSAGE),
|
|
280
|
+
]);
|
|
281
|
+
}
|
|
282
|
+
},
|
|
283
|
+
/**
|
|
284
|
+
* Validates one Protobuf message and throws for constraint violations.
|
|
285
|
+
* @param schema The message schema.
|
|
286
|
+
* @param message The message to validate.
|
|
287
|
+
* @returns The validated message.
|
|
288
|
+
*/
|
|
289
|
+
check(schema, message) {
|
|
290
|
+
const result = Validate.message(schema, message);
|
|
291
|
+
if (!result.valid)
|
|
292
|
+
throw new ValidationException(result.error);
|
|
293
|
+
return message;
|
|
294
|
+
},
|
|
295
|
+
/**
|
|
296
|
+
* Validates a previous/next state pair with framework-owned transition rules.
|
|
297
|
+
* @param request The state transition.
|
|
298
|
+
* @param rules The rules to apply.
|
|
299
|
+
* @returns The sanitized transition result.
|
|
300
|
+
*/
|
|
301
|
+
transition(request, rules = []) {
|
|
302
|
+
const violations = [];
|
|
303
|
+
for (const rule of rules) {
|
|
304
|
+
try {
|
|
305
|
+
violations.push(...rule
|
|
306
|
+
.validateTransition(request)
|
|
307
|
+
.map((violation) => ValidationResults.violation(violation)));
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
violations.push(ValidationResults.failure(request.schema.typeName, TRANSITION_RULE_FAILURE_MESSAGE));
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
return ValidationResults.from(violations);
|
|
314
|
+
},
|
|
315
|
+
};
|
|
316
|
+
Object.freeze(Validate);
|
|
317
|
+
/**
|
|
318
|
+
* Derives canonical Spine type URLs and validates explicit URLs.
|
|
319
|
+
*/
|
|
320
|
+
export const TypeUrls = {
|
|
321
|
+
// prettier-ignore
|
|
322
|
+
/**
|
|
323
|
+
* Calculates the deterministic type URL for a Protobuf-ES message schema.
|
|
324
|
+
* @param schema The message schema.
|
|
325
|
+
* @param options The fallback options.
|
|
326
|
+
* @returns The canonical type URL.
|
|
327
|
+
*/
|
|
328
|
+
derive(schema, options = {}) {
|
|
329
|
+
return `${TypeUrls.prefix(schema, options.fallbackPrefix).replace(/\/+$/u, "")}/${schema.typeName}`;
|
|
330
|
+
},
|
|
331
|
+
/**
|
|
332
|
+
* Returns the type URL prefix that applies to a schema.
|
|
333
|
+
* @param schema The message schema.
|
|
334
|
+
* @param fallbackPrefix The fallback prefix.
|
|
335
|
+
* @returns The canonical prefix.
|
|
336
|
+
*/
|
|
337
|
+
prefix(schema, fallbackPrefix = DEFAULT_TYPE_URL_PREFIX) {
|
|
338
|
+
if (hasOption(schema.file, type_url_prefix))
|
|
339
|
+
return getOption(schema.file, type_url_prefix);
|
|
340
|
+
const normalizedFallbackPrefix = fallbackPrefix.replace(/\/+$/u, "");
|
|
341
|
+
if (normalizedFallbackPrefix.length === 0 || /\s/u.test(normalizedFallbackPrefix)) {
|
|
342
|
+
throw new TypeError("Fallback type URL prefix must be non-empty and contain no whitespace.");
|
|
343
|
+
}
|
|
344
|
+
return normalizedFallbackPrefix;
|
|
345
|
+
},
|
|
346
|
+
/**
|
|
347
|
+
* Resolves an explicit or derived type URL for a schema registration.
|
|
348
|
+
* @param schema The message schema.
|
|
349
|
+
* @param explicitTypeUrl The explicit type URL.
|
|
350
|
+
* @returns The resolved type URL.
|
|
351
|
+
*/
|
|
352
|
+
resolve(schema, explicitTypeUrl) {
|
|
353
|
+
if (explicitTypeUrl === undefined)
|
|
354
|
+
return TypeUrls.derive(schema);
|
|
355
|
+
TypeUrls.validate(schema, explicitTypeUrl);
|
|
356
|
+
return explicitTypeUrl;
|
|
357
|
+
},
|
|
358
|
+
/**
|
|
359
|
+
* Validates an explicit type URL for a schema registration.
|
|
360
|
+
* @param schema The message schema.
|
|
361
|
+
* @param typeUrl The type URL to validate.
|
|
362
|
+
*/
|
|
363
|
+
validate(schema, typeUrl) {
|
|
364
|
+
const expectedSuffix = `/${schema.typeName}`;
|
|
365
|
+
const prefix = typeUrl.slice(0, typeUrl.length - expectedSuffix.length);
|
|
366
|
+
if (!typeUrl.endsWith(expectedSuffix) || prefix.length === 0) {
|
|
367
|
+
throw new Error(`Explicit type URL "${typeUrl}" must have the form "<prefix>/${schema.typeName}".`);
|
|
368
|
+
}
|
|
369
|
+
},
|
|
370
|
+
};
|
|
371
|
+
Object.freeze(TypeUrls);
|
|
372
|
+
/**
|
|
373
|
+
* Packs and unpacks Spine messages in `google.protobuf.Any` envelopes.
|
|
374
|
+
*/
|
|
375
|
+
export const AnyMessages = {
|
|
376
|
+
// prettier-ignore
|
|
377
|
+
/**
|
|
378
|
+
* Packs a message into `Any`, omitting unknown fields from binary output.
|
|
379
|
+
* @param schema The message schema.
|
|
380
|
+
* @param message The message to pack.
|
|
381
|
+
* @param options The packing options.
|
|
382
|
+
* @returns The packed Any message.
|
|
383
|
+
*/
|
|
384
|
+
pack(schema, message, options = {}) {
|
|
385
|
+
if (options.validate !== false)
|
|
386
|
+
Validate.check(schema, message);
|
|
387
|
+
return create(AnySchema, {
|
|
388
|
+
typeUrl: TypeUrls.derive(schema),
|
|
389
|
+
value: toBinary(schema, message, { writeUnknownFields: false }),
|
|
390
|
+
});
|
|
391
|
+
},
|
|
392
|
+
/**
|
|
393
|
+
* Unpacks an `Any` when its type URL exactly matches the requested schema.
|
|
394
|
+
* @param packed The packed message.
|
|
395
|
+
* @param schema The expected schema.
|
|
396
|
+
* @returns The unpacked message, when valid.
|
|
397
|
+
*/
|
|
398
|
+
unpack(packed, schema) {
|
|
399
|
+
if (packed.typeUrl !== TypeUrls.derive(schema))
|
|
400
|
+
return undefined;
|
|
401
|
+
try {
|
|
402
|
+
return fromBinary(schema, packed.value);
|
|
403
|
+
}
|
|
404
|
+
catch {
|
|
405
|
+
return undefined;
|
|
406
|
+
}
|
|
407
|
+
},
|
|
408
|
+
/**
|
|
409
|
+
* Unpacks an `Any` when its exact type URL is registered.
|
|
410
|
+
* @param registry The schema registry.
|
|
411
|
+
* @param packed The packed message.
|
|
412
|
+
* @returns The unpacked message, when valid.
|
|
413
|
+
*/
|
|
414
|
+
unpackUsing(registry, packed) {
|
|
415
|
+
const metadata = registry.findByTypeUrl(packed.typeUrl);
|
|
416
|
+
if (metadata === undefined)
|
|
417
|
+
return undefined;
|
|
418
|
+
try {
|
|
419
|
+
return fromBinary(metadata.schema, packed.value);
|
|
420
|
+
}
|
|
421
|
+
catch {
|
|
422
|
+
return undefined;
|
|
423
|
+
}
|
|
424
|
+
},
|
|
425
|
+
};
|
|
426
|
+
Object.freeze(AnyMessages);
|
|
427
|
+
function isProtobufRegistry(types) {
|
|
428
|
+
return "kind" in types;
|
|
429
|
+
}
|
|
430
|
+
function defaultMessageStringifier(schema, registry, typeUrls) {
|
|
431
|
+
return Object.freeze({
|
|
432
|
+
fromString(value) {
|
|
433
|
+
const message = registry === undefined
|
|
434
|
+
? fromJsonString(schema, value)
|
|
435
|
+
: fromJsonString(schema, value, { registry });
|
|
436
|
+
restoreAnyTypeUrls(message, typeUrls);
|
|
437
|
+
return message;
|
|
438
|
+
},
|
|
439
|
+
toString(value) {
|
|
440
|
+
return registry === undefined
|
|
441
|
+
? toJsonString(schema, value)
|
|
442
|
+
: toJsonString(schema, value, { registry });
|
|
443
|
+
},
|
|
444
|
+
});
|
|
445
|
+
}
|
|
446
|
+
function restoreAnyTypeUrls(value, typeUrls) {
|
|
447
|
+
if (typeof value !== "object" || value === null || value instanceof Uint8Array)
|
|
448
|
+
return;
|
|
449
|
+
if (Array.isArray(value)) {
|
|
450
|
+
for (const item of value)
|
|
451
|
+
restoreAnyTypeUrls(item, typeUrls);
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
const record = value;
|
|
455
|
+
if (record.$typeName === AnySchema.typeName) {
|
|
456
|
+
const typeUrl = record.typeUrl;
|
|
457
|
+
if (typeof typeUrl === "string") {
|
|
458
|
+
const canonical = typeUrls.get(typeUrl.slice(typeUrl.lastIndexOf("/") + 1));
|
|
459
|
+
if (canonical !== undefined)
|
|
460
|
+
record.typeUrl = canonical;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
for (const item of Object.values(record))
|
|
464
|
+
restoreAnyTypeUrls(item, typeUrls);
|
|
465
|
+
}
|
|
466
|
+
const FieldStringifiers = Object.freeze({
|
|
467
|
+
create(field, messageStringifier) {
|
|
468
|
+
switch (field.fieldKind) {
|
|
469
|
+
case "message":
|
|
470
|
+
return messageStringifier(field.message);
|
|
471
|
+
case "enum":
|
|
472
|
+
return this.enum(field);
|
|
473
|
+
case "scalar":
|
|
474
|
+
return this.scalar(field);
|
|
475
|
+
case "list":
|
|
476
|
+
case "map":
|
|
477
|
+
throw new Error("Stringifiers support only singular Protobuf fields.");
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
enum(field) {
|
|
481
|
+
return Object.freeze({
|
|
482
|
+
fromString(value) {
|
|
483
|
+
const named = field.enum.values.find((candidate) => candidate.name === value);
|
|
484
|
+
return (named?.number ??
|
|
485
|
+
Number(FieldStringifiers.integerText(value, -(2n ** 31n), 2n ** 31n - 1n)));
|
|
486
|
+
},
|
|
487
|
+
toString(value) {
|
|
488
|
+
if (typeof value !== "number" || !Number.isInteger(value)) {
|
|
489
|
+
throw new TypeError("Enum field value must be an integer number.");
|
|
490
|
+
}
|
|
491
|
+
const number = FieldStringifiers.integerValue(value, -(2n ** 31n), 2n ** 31n - 1n, "number");
|
|
492
|
+
return field.enum.value[Number(number)]?.name ?? number.toString();
|
|
493
|
+
},
|
|
494
|
+
});
|
|
495
|
+
},
|
|
496
|
+
scalar(field) {
|
|
497
|
+
switch (field.scalar) {
|
|
498
|
+
case ScalarType.STRING:
|
|
499
|
+
return this.string;
|
|
500
|
+
case ScalarType.BOOL:
|
|
501
|
+
return this.boolean;
|
|
502
|
+
case ScalarType.BYTES:
|
|
503
|
+
return this.bytes;
|
|
504
|
+
case ScalarType.DOUBLE:
|
|
505
|
+
return this.number;
|
|
506
|
+
case ScalarType.FLOAT:
|
|
507
|
+
return this.float;
|
|
508
|
+
case ScalarType.INT32:
|
|
509
|
+
case ScalarType.SFIXED32:
|
|
510
|
+
case ScalarType.SINT32:
|
|
511
|
+
return this.integer(-(2n ** 31n), 2n ** 31n - 1n, "number");
|
|
512
|
+
case ScalarType.FIXED32:
|
|
513
|
+
case ScalarType.UINT32:
|
|
514
|
+
return this.integer(0n, 2n ** 32n - 1n, "number");
|
|
515
|
+
case ScalarType.INT64:
|
|
516
|
+
case ScalarType.SFIXED64:
|
|
517
|
+
case ScalarType.SINT64:
|
|
518
|
+
return this.integer(-(2n ** 63n), 2n ** 63n - 1n, field.longAsString ? "string" : "bigint");
|
|
519
|
+
case ScalarType.FIXED64:
|
|
520
|
+
case ScalarType.UINT64:
|
|
521
|
+
return this.integer(0n, 2n ** 64n - 1n, field.longAsString ? "string" : "bigint");
|
|
522
|
+
}
|
|
523
|
+
},
|
|
524
|
+
string: Object.freeze({
|
|
525
|
+
fromString(value) {
|
|
526
|
+
return value;
|
|
527
|
+
},
|
|
528
|
+
toString(value) {
|
|
529
|
+
if (typeof value !== "string")
|
|
530
|
+
throw new TypeError("Field value must be a string.");
|
|
531
|
+
return value;
|
|
532
|
+
},
|
|
533
|
+
}),
|
|
534
|
+
boolean: Object.freeze({
|
|
535
|
+
fromString(value) {
|
|
536
|
+
if (value === "true")
|
|
537
|
+
return true;
|
|
538
|
+
if (value === "false")
|
|
539
|
+
return false;
|
|
540
|
+
throw new Error("Field value must be a canonical boolean.");
|
|
541
|
+
},
|
|
542
|
+
toString(value) {
|
|
543
|
+
if (typeof value !== "boolean")
|
|
544
|
+
throw new TypeError("Field value must be a boolean.");
|
|
545
|
+
return value ? "true" : "false";
|
|
546
|
+
},
|
|
547
|
+
}),
|
|
548
|
+
bytes: Object.freeze({
|
|
549
|
+
fromString(value) {
|
|
550
|
+
let decoded;
|
|
551
|
+
try {
|
|
552
|
+
decoded = base64Decode(value);
|
|
553
|
+
}
|
|
554
|
+
catch {
|
|
555
|
+
throw new Error("Field value must be canonical base64.");
|
|
556
|
+
}
|
|
557
|
+
if (base64Encode(decoded, "std") !== value) {
|
|
558
|
+
throw new Error("Field value must be canonical base64.");
|
|
559
|
+
}
|
|
560
|
+
return decoded;
|
|
561
|
+
},
|
|
562
|
+
toString(value) {
|
|
563
|
+
if (!(value instanceof Uint8Array)) {
|
|
564
|
+
throw new TypeError("Field value must be a byte array.");
|
|
565
|
+
}
|
|
566
|
+
return base64Encode(value, "std");
|
|
567
|
+
},
|
|
568
|
+
}),
|
|
569
|
+
number: Object.freeze({
|
|
570
|
+
fromString(value) {
|
|
571
|
+
const parsed = Number(value);
|
|
572
|
+
if (!Number.isFinite(parsed))
|
|
573
|
+
throw new Error("Field value must be a finite number.");
|
|
574
|
+
const canonical = Object.is(parsed, -0) ? "-0" : String(parsed);
|
|
575
|
+
if (canonical !== value)
|
|
576
|
+
throw new Error("Field value must be a canonical number.");
|
|
577
|
+
return parsed;
|
|
578
|
+
},
|
|
579
|
+
toString(value) {
|
|
580
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
581
|
+
throw new TypeError("Field value must be a finite number.");
|
|
582
|
+
}
|
|
583
|
+
return Object.is(value, -0) ? "-0" : String(value);
|
|
584
|
+
},
|
|
585
|
+
}),
|
|
586
|
+
float: Object.freeze({
|
|
587
|
+
fromString(value) {
|
|
588
|
+
const parsed = Number(value);
|
|
589
|
+
if (!Number.isFinite(parsed))
|
|
590
|
+
throw new Error("Field value must be a finite number.");
|
|
591
|
+
const restored = Math.fround(parsed);
|
|
592
|
+
if (!Number.isFinite(restored)) {
|
|
593
|
+
throw new Error("Field value is outside the float32 range.");
|
|
594
|
+
}
|
|
595
|
+
if (FieldStringifiers.floatText(restored) !== value) {
|
|
596
|
+
throw new Error("Field value must be a canonical float32 number.");
|
|
597
|
+
}
|
|
598
|
+
return restored;
|
|
599
|
+
},
|
|
600
|
+
toString(value) {
|
|
601
|
+
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
602
|
+
throw new TypeError("Field value must be a finite number.");
|
|
603
|
+
}
|
|
604
|
+
const normalized = Math.fround(value);
|
|
605
|
+
if (!Number.isFinite(normalized)) {
|
|
606
|
+
throw new Error("Field value is outside the float32 range.");
|
|
607
|
+
}
|
|
608
|
+
return FieldStringifiers.floatText(normalized);
|
|
609
|
+
},
|
|
610
|
+
}),
|
|
611
|
+
floatText(value) {
|
|
612
|
+
if (Object.is(value, -0))
|
|
613
|
+
return "-0";
|
|
614
|
+
if (value === 0)
|
|
615
|
+
return "0";
|
|
616
|
+
for (let precision = 1; precision <= 9; precision += 1) {
|
|
617
|
+
const candidate = String(Number(value.toPrecision(precision)));
|
|
618
|
+
if (Object.is(Math.fround(Number(candidate)), value))
|
|
619
|
+
return candidate;
|
|
620
|
+
}
|
|
621
|
+
throw new Error("Unable to format the float32 field value.");
|
|
622
|
+
},
|
|
623
|
+
integer(min, max, representation) {
|
|
624
|
+
return Object.freeze({
|
|
625
|
+
fromString(value) {
|
|
626
|
+
const restored = FieldStringifiers.integerText(value, min, max);
|
|
627
|
+
switch (representation) {
|
|
628
|
+
case "number":
|
|
629
|
+
return Number(restored);
|
|
630
|
+
case "bigint":
|
|
631
|
+
return restored;
|
|
632
|
+
case "string":
|
|
633
|
+
return restored.toString();
|
|
634
|
+
}
|
|
635
|
+
},
|
|
636
|
+
toString(value) {
|
|
637
|
+
const restored = FieldStringifiers.integerValue(value, min, max, representation);
|
|
638
|
+
return restored.toString();
|
|
639
|
+
},
|
|
640
|
+
});
|
|
641
|
+
},
|
|
642
|
+
integerText(value, min, max) {
|
|
643
|
+
if (!/^(?:0|-?[1-9]\d*)$/u.test(value)) {
|
|
644
|
+
throw new Error("Field value must be a canonical integer.");
|
|
645
|
+
}
|
|
646
|
+
const restored = BigInt(value);
|
|
647
|
+
if (restored < min || restored > max) {
|
|
648
|
+
const kind = min === 0n && max === 2n ** 64n - 1n ? "uint64" : "declared integer";
|
|
649
|
+
throw new Error(`Field value is outside the ${kind} range.`);
|
|
650
|
+
}
|
|
651
|
+
return restored;
|
|
652
|
+
},
|
|
653
|
+
integerValue(value, min, max, representation) {
|
|
654
|
+
let converted;
|
|
655
|
+
if (representation === "string") {
|
|
656
|
+
if (typeof value !== "string")
|
|
657
|
+
throw new TypeError("Field value must be an integer string.");
|
|
658
|
+
converted = this.integerText(value, min, max);
|
|
659
|
+
}
|
|
660
|
+
else if (representation === "bigint" && typeof value === "bigint") {
|
|
661
|
+
converted = value;
|
|
662
|
+
}
|
|
663
|
+
else if (representation === "number" &&
|
|
664
|
+
typeof value === "number" &&
|
|
665
|
+
Number.isSafeInteger(value)) {
|
|
666
|
+
converted = BigInt(value);
|
|
667
|
+
}
|
|
668
|
+
else {
|
|
669
|
+
throw new TypeError("Field value must be an integer.");
|
|
670
|
+
}
|
|
671
|
+
if (converted < min || converted > max) {
|
|
672
|
+
const kind = min === 0n && max === 2n ** 64n - 1n ? "uint64" : "declared integer";
|
|
673
|
+
throw new Error(`Field value is outside the ${kind} range.`);
|
|
674
|
+
}
|
|
675
|
+
return converted;
|
|
676
|
+
},
|
|
677
|
+
});
|
|
678
|
+
/**
|
|
679
|
+
* Supplies reversible default stringifiers for generated Protobuf messages.
|
|
680
|
+
*/
|
|
681
|
+
export const Stringifiers = {
|
|
682
|
+
// prettier-ignore
|
|
683
|
+
/**
|
|
684
|
+
* Creates the default compact Proto JSON stringifier for a message schema.
|
|
685
|
+
* @param schema The generated message schema.
|
|
686
|
+
* @param types The optional generated-type registry used to expand `Any` values.
|
|
687
|
+
* @returns A reversible schema-bound stringifier.
|
|
688
|
+
*/
|
|
689
|
+
forMessage(schema, types) {
|
|
690
|
+
const registry = types === undefined
|
|
691
|
+
? undefined
|
|
692
|
+
: isProtobufRegistry(types)
|
|
693
|
+
? types
|
|
694
|
+
: createRegistry(...types.list().map((metadata) => metadata.descriptor));
|
|
695
|
+
const typeUrls = types === undefined || isProtobufRegistry(types)
|
|
696
|
+
? new Map()
|
|
697
|
+
: new Map(types.list().map((metadata) => [metadata.schema.typeName, metadata.typeUrl]));
|
|
698
|
+
return defaultMessageStringifier(schema, registry, typeUrls);
|
|
699
|
+
},
|
|
700
|
+
/**
|
|
701
|
+
* Creates a reversible stringifier for one supported singular field.
|
|
702
|
+
*
|
|
703
|
+
* Scalar, bytes, enum, and message fields are supported. Numeric text is
|
|
704
|
+
* canonical, finite, and range-checked; `float` values are normalized to
|
|
705
|
+
* binary32. Repeated and map fields are rejected.
|
|
706
|
+
*
|
|
707
|
+
* @param field The Protobuf field descriptor.
|
|
708
|
+
* @param types The optional generated-type registry used by message fields.
|
|
709
|
+
* @returns A stringifier for the field's runtime value.
|
|
710
|
+
*/
|
|
711
|
+
forField(field, types) {
|
|
712
|
+
return FieldStringifiers.create(field, (schema) => this.forMessage(schema, types));
|
|
713
|
+
},
|
|
714
|
+
};
|
|
715
|
+
Object.freeze(Stringifiers);
|
|
716
|
+
/**
|
|
717
|
+
* Holds schema-bound custom stringifiers with Proto JSON defaults.
|
|
718
|
+
*/
|
|
719
|
+
export class StringifierRegistry {
|
|
720
|
+
#registered = new Map();
|
|
721
|
+
#types;
|
|
722
|
+
#typeUrls = new Map();
|
|
723
|
+
/**
|
|
724
|
+
* Creates an empty registry or a snapshot of another registry.
|
|
725
|
+
*
|
|
726
|
+
* @param source The optional registry to copy.
|
|
727
|
+
*/
|
|
728
|
+
constructor(source) {
|
|
729
|
+
if (source !== undefined) {
|
|
730
|
+
for (const [typeName, stringifier] of source.#registered) {
|
|
731
|
+
this.#registered.set(typeName, stringifier);
|
|
732
|
+
}
|
|
733
|
+
this.#types = source.#types;
|
|
734
|
+
this.#typeUrls = new Map(source.#typeUrls);
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
/**
|
|
738
|
+
* Registers or replaces the stringifier for one generated message type.
|
|
739
|
+
* @param schema The generated message schema.
|
|
740
|
+
* @param stringifier The reversible stringifier.
|
|
741
|
+
*/
|
|
742
|
+
register(schema, stringifier) {
|
|
743
|
+
this.#registered.set(schema.typeName, stringifier);
|
|
744
|
+
}
|
|
745
|
+
/**
|
|
746
|
+
* Sets the generated-type registry used by default message stringifiers.
|
|
747
|
+
*
|
|
748
|
+
* @param types Resolves message types embedded in `Any` values.
|
|
749
|
+
*/
|
|
750
|
+
setTypeRegistry(types) {
|
|
751
|
+
const metadata = types.list();
|
|
752
|
+
this.#types = createRegistry(...metadata.map((item) => item.descriptor));
|
|
753
|
+
this.#typeUrls = new Map(metadata.map((item) => [item.schema.typeName, item.typeUrl]));
|
|
754
|
+
}
|
|
755
|
+
/**
|
|
756
|
+
* Returns the custom stringifier or the default compact Proto JSON mapping.
|
|
757
|
+
* @param schema The generated message schema.
|
|
758
|
+
* @returns The schema-bound stringifier.
|
|
759
|
+
*/
|
|
760
|
+
forMessage(schema) {
|
|
761
|
+
const registered = this.#registered.get(schema.typeName);
|
|
762
|
+
return registered === undefined
|
|
763
|
+
? defaultMessageStringifier(schema, this.#types, this.#typeUrls)
|
|
764
|
+
: registered;
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* Returns the configured reversible mapping for one supported singular field.
|
|
768
|
+
*
|
|
769
|
+
* Scalar, bytes, enum, and message fields are supported. Numeric text is
|
|
770
|
+
* canonical, finite, and range-checked; `float` values are normalized to
|
|
771
|
+
* binary32. Repeated and map fields are rejected. Registered message mappings
|
|
772
|
+
* take precedence over compact Proto JSON defaults.
|
|
773
|
+
*
|
|
774
|
+
* @param field The Protobuf field descriptor.
|
|
775
|
+
* @returns A stringifier for the field's runtime value.
|
|
776
|
+
*/
|
|
777
|
+
forField(field) {
|
|
778
|
+
return FieldStringifiers.create(field, (schema) => this.forMessage(schema));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Packs and unpacks the identifier types supported by Spine JVM storage.
|
|
783
|
+
*/
|
|
784
|
+
export const Identifiers = {
|
|
785
|
+
// prettier-ignore
|
|
786
|
+
/**
|
|
787
|
+
* Packs a supported typed identifier.
|
|
788
|
+
*/
|
|
789
|
+
pack: packIdentifier,
|
|
790
|
+
/**
|
|
791
|
+
* Unpacks a supported typed identifier.
|
|
792
|
+
*/
|
|
793
|
+
unpack: unpackIdentifier,
|
|
794
|
+
};
|
|
795
|
+
Object.freeze(Identifiers);
|
|
796
|
+
const IdentifierValues = Object.freeze({
|
|
797
|
+
int32(value) {
|
|
798
|
+
if (typeof value !== "number" ||
|
|
799
|
+
!Number.isInteger(value) ||
|
|
800
|
+
value < -(2 ** 31) ||
|
|
801
|
+
value >= 2 ** 31) {
|
|
802
|
+
throw new RangeError("Identifier is outside the int32 range.");
|
|
803
|
+
}
|
|
804
|
+
return value;
|
|
805
|
+
},
|
|
806
|
+
int64(value) {
|
|
807
|
+
if (typeof value !== "bigint" || value < -(1n << 63n) || value >= 1n << 63n) {
|
|
808
|
+
throw new RangeError("Identifier is outside the int64 range.");
|
|
809
|
+
}
|
|
810
|
+
return value;
|
|
811
|
+
},
|
|
812
|
+
});
|
|
813
|
+
function packIdentifier(type, value) {
|
|
814
|
+
if (typeof type !== "string") {
|
|
815
|
+
return AnyMessages.pack(type, value, { validate: false });
|
|
816
|
+
}
|
|
817
|
+
switch (type) {
|
|
818
|
+
case "string":
|
|
819
|
+
if (typeof value !== "string")
|
|
820
|
+
throw new TypeError("Identifier must be a string.");
|
|
821
|
+
return AnyMessages.pack(StringValueSchema, create(StringValueSchema, { value }), {
|
|
822
|
+
validate: false,
|
|
823
|
+
});
|
|
824
|
+
case "int32":
|
|
825
|
+
return AnyMessages.pack(Int32ValueSchema, create(Int32ValueSchema, { value: IdentifierValues.int32(value) }), { validate: false });
|
|
826
|
+
case "int64":
|
|
827
|
+
return AnyMessages.pack(Int64ValueSchema, create(Int64ValueSchema, { value: IdentifierValues.int64(value) }), { validate: false });
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
function unpackIdentifier(type, value) {
|
|
831
|
+
if (typeof type !== "string")
|
|
832
|
+
return AnyMessages.unpack(value, type);
|
|
833
|
+
switch (type) {
|
|
834
|
+
case "string":
|
|
835
|
+
return AnyMessages.unpack(value, StringValueSchema)?.value;
|
|
836
|
+
case "int32":
|
|
837
|
+
return AnyMessages.unpack(value, Int32ValueSchema)?.value;
|
|
838
|
+
case "int64":
|
|
839
|
+
return AnyMessages.unpack(value, Int64ValueSchema)?.value;
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
/**
|
|
843
|
+
* Creates generated Spine command and event envelopes.
|
|
844
|
+
*/
|
|
845
|
+
export const SignalEnvelopes = {
|
|
846
|
+
// prettier-ignore
|
|
847
|
+
/**
|
|
848
|
+
* Packs a generated Spine command envelope from caller-supplied data.
|
|
849
|
+
* @param input The command envelope input.
|
|
850
|
+
* @returns The packed command.
|
|
851
|
+
*/
|
|
852
|
+
command(input) {
|
|
853
|
+
return create(CommandSchema, {
|
|
854
|
+
id: clone(CommandIdSchema, input.id),
|
|
855
|
+
message: AnyMessages.pack(input.schema, input.message, input),
|
|
856
|
+
context: clone(CommandContextSchema, input.context),
|
|
857
|
+
});
|
|
858
|
+
},
|
|
859
|
+
/**
|
|
860
|
+
* Packs a generated Spine event envelope from caller-supplied data.
|
|
861
|
+
* @param input The event envelope input.
|
|
862
|
+
* @returns The packed event.
|
|
863
|
+
*/
|
|
864
|
+
event(input) {
|
|
865
|
+
return create(EventSchema, {
|
|
866
|
+
id: clone(EventIdSchema, input.id),
|
|
867
|
+
message: AnyMessages.pack(input.schema, input.message, input),
|
|
868
|
+
context: clone(EventContextSchema, input.context),
|
|
869
|
+
});
|
|
870
|
+
},
|
|
871
|
+
};
|
|
872
|
+
Object.freeze(SignalEnvelopes);
|
|
873
|
+
/**
|
|
874
|
+
* Registry for Protobuf schemas, Spine type URLs, and descriptor metadata.
|
|
875
|
+
*/
|
|
876
|
+
export class TypeRegistry {
|
|
877
|
+
#byFullName = new Map();
|
|
878
|
+
#byTypeUrl = new Map();
|
|
879
|
+
#bySchema = new WeakMap();
|
|
880
|
+
#bySchemaDescriptor = new WeakMap();
|
|
881
|
+
/**
|
|
882
|
+
* Creates a registry and optionally registers schemas immediately.
|
|
883
|
+
* @param schemas The schemas to register.
|
|
884
|
+
*/
|
|
885
|
+
constructor(schemas = []) {
|
|
886
|
+
for (const schema of schemas) {
|
|
887
|
+
this.register(schema);
|
|
888
|
+
}
|
|
889
|
+
}
|
|
890
|
+
/**
|
|
891
|
+
* Creates a registry from modules in deterministic dependency-first order.
|
|
892
|
+
* @param modules The modules to compose.
|
|
893
|
+
* @returns The composed registry.
|
|
894
|
+
*/
|
|
895
|
+
static from(...modules) {
|
|
896
|
+
const definitions = new Map();
|
|
897
|
+
const visiting = new Set();
|
|
898
|
+
const verified = new WeakSet();
|
|
899
|
+
const schemas = [];
|
|
900
|
+
for (const module of modules) {
|
|
901
|
+
RegistryLookups.compose(module, definitions, visiting, verified, schemas);
|
|
902
|
+
}
|
|
903
|
+
return new TypeRegistry(schemas);
|
|
904
|
+
}
|
|
905
|
+
/**
|
|
906
|
+
* Creates a registry containing the currently curated Spine schemas.
|
|
907
|
+
* @returns The mutable curated registry.
|
|
908
|
+
*/
|
|
909
|
+
static spineCore() {
|
|
910
|
+
return new TypeRegistry([
|
|
911
|
+
FieldPathSchema,
|
|
912
|
+
TemplateStringSchema,
|
|
913
|
+
ActorContextSchema,
|
|
914
|
+
CommandIdSchema,
|
|
915
|
+
CommandSchema,
|
|
916
|
+
Command_SystemPropertiesSchema,
|
|
917
|
+
CommandContextSchema,
|
|
918
|
+
CommandContext_ScheduleSchema,
|
|
919
|
+
MessageIdSchema,
|
|
920
|
+
OriginSchema,
|
|
921
|
+
EnrichmentSchema,
|
|
922
|
+
Enrichment_ContainerSchema,
|
|
923
|
+
EventIdSchema,
|
|
924
|
+
EventSchema,
|
|
925
|
+
EventContextSchema,
|
|
926
|
+
RejectionEventContextSchema,
|
|
927
|
+
TenantIdSchema,
|
|
928
|
+
UserIdSchema,
|
|
929
|
+
VersionSchema,
|
|
930
|
+
EmailAddressSchema,
|
|
931
|
+
InternetDomainSchema,
|
|
932
|
+
YearMonthSchema,
|
|
933
|
+
LocalDateSchema,
|
|
934
|
+
LocalTimeSchema,
|
|
935
|
+
LocalDateTimeSchema,
|
|
936
|
+
ZoneIdSchema,
|
|
937
|
+
ZonedDateTimeSchema,
|
|
938
|
+
ValidationErrorSchema,
|
|
939
|
+
ConstraintViolationSchema,
|
|
940
|
+
]);
|
|
941
|
+
}
|
|
942
|
+
/**
|
|
943
|
+
* Registers one schema and returns its immutable metadata.
|
|
944
|
+
* @param schema The generated message schema.
|
|
945
|
+
* @param options Optional explicit type URL.
|
|
946
|
+
* @returns The registered schema metadata.
|
|
947
|
+
*/
|
|
948
|
+
register(schema, options = {}) {
|
|
949
|
+
const fullTypeName = schema.typeName;
|
|
950
|
+
const typeUrl = TypeUrls.resolve(schema, options.typeUrl);
|
|
951
|
+
const duplicateFullName = this.#byFullName.get(fullTypeName);
|
|
952
|
+
const duplicateTypeUrl = this.#byTypeUrl.get(typeUrl);
|
|
953
|
+
const schemaIdentityConflict = this.#bySchemaDescriptor.get(schema.proto);
|
|
954
|
+
if (options.typeUrl !== undefined && duplicateTypeUrl !== undefined) {
|
|
955
|
+
throw new Error(`Duplicate type URL "${typeUrl}" already registered for Protobuf type ` +
|
|
956
|
+
`"${duplicateTypeUrl.fullTypeName}".`);
|
|
957
|
+
}
|
|
958
|
+
if (duplicateFullName !== undefined) {
|
|
959
|
+
throw new Error(`Duplicate Protobuf type name "${fullTypeName}" already registered with type URL ` +
|
|
960
|
+
`"${duplicateFullName.typeUrl}".`);
|
|
961
|
+
}
|
|
962
|
+
if (duplicateTypeUrl !== undefined) {
|
|
963
|
+
throw new Error(`Duplicate type URL "${typeUrl}" already registered for Protobuf type ` +
|
|
964
|
+
`"${duplicateTypeUrl.fullTypeName}".`);
|
|
965
|
+
}
|
|
966
|
+
if (schemaIdentityConflict !== undefined) {
|
|
967
|
+
throw new Error(`Schema identity conflict for "${schemaIdentityConflict.fullTypeName}": ` +
|
|
968
|
+
`the same descriptor identity was registered as "${fullTypeName}".`);
|
|
969
|
+
}
|
|
970
|
+
const metadata = RegistryLookups.metadata(schema, typeUrl);
|
|
971
|
+
this.#byFullName.set(metadata.fullTypeName, metadata);
|
|
972
|
+
this.#byTypeUrl.set(metadata.typeUrl, metadata);
|
|
973
|
+
this.#bySchema.set(schema, metadata);
|
|
974
|
+
this.#bySchemaDescriptor.set(schema.proto, metadata);
|
|
975
|
+
return metadata;
|
|
976
|
+
}
|
|
977
|
+
/**
|
|
978
|
+
* Finds metadata by fully qualified Protobuf type name.
|
|
979
|
+
* @param fullTypeName The Protobuf type name.
|
|
980
|
+
* @returns Matching metadata, if registered.
|
|
981
|
+
*/
|
|
982
|
+
findByFullName(fullTypeName) {
|
|
983
|
+
return this.#byFullName.get(fullTypeName);
|
|
984
|
+
}
|
|
985
|
+
/**
|
|
986
|
+
* Finds metadata by canonical type URL.
|
|
987
|
+
* @param typeUrl The canonical type URL.
|
|
988
|
+
* @returns Matching metadata, if registered.
|
|
989
|
+
*/
|
|
990
|
+
findByTypeUrl(typeUrl) {
|
|
991
|
+
return this.#byTypeUrl.get(typeUrl);
|
|
992
|
+
}
|
|
993
|
+
/**
|
|
994
|
+
* Finds metadata by generated schema identity.
|
|
995
|
+
* @param schema The generated message schema.
|
|
996
|
+
* @returns Matching metadata, if registered.
|
|
997
|
+
*/
|
|
998
|
+
findBySchema(schema) {
|
|
999
|
+
return this.#bySchema.get(schema);
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* Gets metadata by fully qualified Protobuf type name or throws a descriptive error.
|
|
1003
|
+
* @param fullTypeName The Protobuf type name.
|
|
1004
|
+
* @returns The registered metadata.
|
|
1005
|
+
*/
|
|
1006
|
+
getByFullName(fullTypeName) {
|
|
1007
|
+
const metadata = this.findByFullName(fullTypeName);
|
|
1008
|
+
if (metadata === undefined) {
|
|
1009
|
+
throw new Error(`No schema registered for Protobuf type name "${fullTypeName}".`);
|
|
1010
|
+
}
|
|
1011
|
+
return metadata;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Gets metadata by canonical type URL or throws a descriptive error.
|
|
1015
|
+
* @param typeUrl The canonical type URL.
|
|
1016
|
+
* @returns The registered metadata.
|
|
1017
|
+
*/
|
|
1018
|
+
getByTypeUrl(typeUrl) {
|
|
1019
|
+
const metadata = this.findByTypeUrl(typeUrl);
|
|
1020
|
+
if (metadata === undefined) {
|
|
1021
|
+
throw new Error(`No schema registered for type URL "${typeUrl}".`);
|
|
1022
|
+
}
|
|
1023
|
+
return metadata;
|
|
1024
|
+
}
|
|
1025
|
+
/**
|
|
1026
|
+
* Gets metadata by generated schema identity or throws a descriptive error.
|
|
1027
|
+
* @param schema The generated message schema.
|
|
1028
|
+
* @returns The registered metadata.
|
|
1029
|
+
*/
|
|
1030
|
+
getBySchema(schema) {
|
|
1031
|
+
const metadata = this.findBySchema(schema);
|
|
1032
|
+
if (metadata === undefined) {
|
|
1033
|
+
throw new Error(`No metadata registered for schema "${schema.typeName}".`);
|
|
1034
|
+
}
|
|
1035
|
+
return metadata;
|
|
1036
|
+
}
|
|
1037
|
+
/**
|
|
1038
|
+
* Returns all registered metadata in registration order.
|
|
1039
|
+
* @returns The registered metadata entries.
|
|
1040
|
+
*/
|
|
1041
|
+
list() {
|
|
1042
|
+
return [...this.#byFullName.values()];
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
const RegistryLookups = {
|
|
1046
|
+
// prettier-ignore
|
|
1047
|
+
/**
|
|
1048
|
+
* Composes modules in deterministic dependency-first order.
|
|
1049
|
+
*/
|
|
1050
|
+
compose(root, definitions, visiting, verified, schemas) {
|
|
1051
|
+
const frames = [{ module: root, appendSchemas: false }];
|
|
1052
|
+
while (frames.length > 0) {
|
|
1053
|
+
const frame = frames.pop();
|
|
1054
|
+
if (frame === undefined) {
|
|
1055
|
+
continue;
|
|
1056
|
+
}
|
|
1057
|
+
const { module } = frame;
|
|
1058
|
+
if (frame.appendSchemas) {
|
|
1059
|
+
visiting.delete(module.name);
|
|
1060
|
+
verified.add(module);
|
|
1061
|
+
if (definitions.get(module.name) === module) {
|
|
1062
|
+
schemas.push(...module.schemas);
|
|
1063
|
+
}
|
|
1064
|
+
continue;
|
|
1065
|
+
}
|
|
1066
|
+
if (visiting.has(module.name)) {
|
|
1067
|
+
throw new Error(`Proto module dependency cycle at "${module.name}".`);
|
|
1068
|
+
}
|
|
1069
|
+
const existing = definitions.get(module.name);
|
|
1070
|
+
if (existing !== undefined && !RegistryLookups.sameModule(existing, module)) {
|
|
1071
|
+
throw new Error(`Proto module conflict for "${module.name}".`);
|
|
1072
|
+
}
|
|
1073
|
+
if (existing !== undefined && verified.has(module)) {
|
|
1074
|
+
continue;
|
|
1075
|
+
}
|
|
1076
|
+
if (existing === undefined) {
|
|
1077
|
+
definitions.set(module.name, module);
|
|
1078
|
+
}
|
|
1079
|
+
visiting.add(module.name);
|
|
1080
|
+
frames.push({ module, appendSchemas: true });
|
|
1081
|
+
for (let index = module.dependencies.length - 1; index >= 0; index -= 1) {
|
|
1082
|
+
const dependency = module.dependencies[index];
|
|
1083
|
+
if (dependency !== undefined) {
|
|
1084
|
+
frames.push({ module: dependency, appendSchemas: false });
|
|
1085
|
+
}
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
},
|
|
1089
|
+
/**
|
|
1090
|
+
* Compares two module definitions for same-name conflicts.
|
|
1091
|
+
*/
|
|
1092
|
+
sameModule(left, right) {
|
|
1093
|
+
if (left === right) {
|
|
1094
|
+
return true;
|
|
1095
|
+
}
|
|
1096
|
+
if (left.name !== right.name ||
|
|
1097
|
+
left.schemas.length !== right.schemas.length ||
|
|
1098
|
+
left.dependencies.length !== right.dependencies.length) {
|
|
1099
|
+
return false;
|
|
1100
|
+
}
|
|
1101
|
+
return (left.schemas.every((schema, index) => schema === right.schemas[index]) &&
|
|
1102
|
+
left.dependencies.every((dependency, index) => dependency.name === right.dependencies[index]?.name));
|
|
1103
|
+
},
|
|
1104
|
+
/**
|
|
1105
|
+
* Creates immutable descriptor-backed schema metadata.
|
|
1106
|
+
*/
|
|
1107
|
+
metadata(schema, typeUrl) {
|
|
1108
|
+
const firstField = schema.fields[0];
|
|
1109
|
+
return Object.freeze({
|
|
1110
|
+
fullTypeName: schema.typeName,
|
|
1111
|
+
typeUrl,
|
|
1112
|
+
schema,
|
|
1113
|
+
descriptor: schema,
|
|
1114
|
+
fileDescriptor: schema.file,
|
|
1115
|
+
fileName: `${schema.file.name}.proto`,
|
|
1116
|
+
typeUrlPrefix: typeUrl.slice(0, typeUrl.length - schema.typeName.length - 1),
|
|
1117
|
+
firstField,
|
|
1118
|
+
firstFieldName: firstField?.name,
|
|
1119
|
+
hasFileOption(option) {
|
|
1120
|
+
return hasOption(schema.file, option);
|
|
1121
|
+
},
|
|
1122
|
+
getFileOption(option) {
|
|
1123
|
+
return getOption(schema.file, option);
|
|
1124
|
+
},
|
|
1125
|
+
});
|
|
1126
|
+
},
|
|
1127
|
+
/**
|
|
1128
|
+
* Creates an immutable registry lookup view.
|
|
1129
|
+
*/
|
|
1130
|
+
lookup(registry) {
|
|
1131
|
+
return Object.freeze({
|
|
1132
|
+
findByFullName: (fullTypeName) => registry.findByFullName(fullTypeName),
|
|
1133
|
+
findByTypeUrl: (typeUrl) => registry.findByTypeUrl(typeUrl),
|
|
1134
|
+
findBySchema: (schema) => registry.findBySchema(schema),
|
|
1135
|
+
getByFullName: (fullTypeName) => registry.getByFullName(fullTypeName),
|
|
1136
|
+
getByTypeUrl: (typeUrl) => registry.getByTypeUrl(typeUrl),
|
|
1137
|
+
getBySchema: (schema) => registry.getBySchema(schema),
|
|
1138
|
+
list: () => registry.list(),
|
|
1139
|
+
});
|
|
1140
|
+
},
|
|
1141
|
+
};
|
|
1142
|
+
/**
|
|
1143
|
+
* Shared registry for the first curated Spine schema set.
|
|
1144
|
+
*/
|
|
1145
|
+
export const spineCoreRegistry = RegistryLookups.lookup(TypeRegistry.spineCore());
|
|
1146
|
+
/**
|
|
1147
|
+
* Constructs and sanitizes internal message-validation results.
|
|
1148
|
+
*/
|
|
1149
|
+
const ValidationResults = {
|
|
1150
|
+
from(violations) {
|
|
1151
|
+
if (violations.length === 0)
|
|
1152
|
+
return { valid: true, violations: EMPTY_VIOLATIONS, error: undefined };
|
|
1153
|
+
const nonEmpty = violations;
|
|
1154
|
+
return { valid: false, violations: nonEmpty, error: ValidationResults.error(nonEmpty) };
|
|
1155
|
+
},
|
|
1156
|
+
error(violations) {
|
|
1157
|
+
return create(ValidationErrorSchema, { constraintViolation: [...violations] });
|
|
1158
|
+
},
|
|
1159
|
+
failure(typeName, message) {
|
|
1160
|
+
return create(ConstraintViolationSchema, {
|
|
1161
|
+
typeName,
|
|
1162
|
+
message: create(TemplateStringSchema, { withPlaceholders: message }),
|
|
1163
|
+
});
|
|
1164
|
+
},
|
|
1165
|
+
violation(violation) {
|
|
1166
|
+
return create(ConstraintViolationSchema, {
|
|
1167
|
+
message: violation.message === undefined
|
|
1168
|
+
? undefined
|
|
1169
|
+
: create(TemplateStringSchema, {
|
|
1170
|
+
withPlaceholders: violation.message.withPlaceholders,
|
|
1171
|
+
placeholderValue: ValidationResults.redact(violation.message.placeholderValue),
|
|
1172
|
+
}),
|
|
1173
|
+
typeName: violation.typeName,
|
|
1174
|
+
fieldPath: violation.fieldPath === undefined
|
|
1175
|
+
? undefined
|
|
1176
|
+
: create(FieldPathSchema, { fieldName: [...violation.fieldPath.fieldName] }),
|
|
1177
|
+
});
|
|
1178
|
+
},
|
|
1179
|
+
redact(values) {
|
|
1180
|
+
return Object.fromEntries(Object.keys(values ?? {}).map((key) => [key, REDACTED_VALIDATION_DETAIL]));
|
|
1181
|
+
},
|
|
1182
|
+
};
|
|
1183
|
+
//# sourceMappingURL=index.js.map
|