@hops-ops/distributed 0.0.0 → 4.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -13
- package/dist/auth-headers.js +3 -3
- package/dist/internal/cache-engine/engine.d.ts +3 -1
- package/dist/internal/cache-engine/engine.js +137 -22
- package/dist/internal/cache-engine/errors.d.ts +4 -0
- package/dist/internal/cache-engine/errors.js +8 -0
- package/dist/internal/cache-engine/index.d.ts +2 -2
- package/dist/internal/cache-engine/index.js +1 -1
- package/dist/internal/cache-engine/types.d.ts +16 -0
- package/dist/internal/cache-engine.d.ts +2 -2
- package/dist/internal/cache-engine.js +1 -1
- package/dist/protocol.d.ts +9 -2
- package/dist/protocol.js +35 -7
- package/dist/replica/command-runtime/create.js +499 -57
- package/dist/replica/command-runtime/index.d.ts +1 -1
- package/dist/replica/command-runtime/index.js +1 -1
- package/dist/replica/command-runtime/lib/effects.d.ts +22 -9
- package/dist/replica/command-runtime/lib/effects.js +118 -20
- package/dist/replica/command-runtime/lib/projection.d.ts +2 -2
- package/dist/replica/command-runtime/lib/projection.js +14 -6
- package/dist/replica/command-runtime/lib/status.js +18 -10
- package/dist/replica/command-runtime/lib/transport.js +4 -0
- package/dist/replica/command-runtime/lib/util.js +8 -3
- package/dist/replica/command-runtime/symbols.d.ts +9 -0
- package/dist/replica/command-runtime/symbols.js +9 -0
- package/dist/replica/command-runtime/types.d.ts +15 -6
- package/dist/replica/command-runtime/types.js +1 -1
- package/dist/replica/command-runtime.d.ts +1 -1
- package/dist/replica/command-runtime.js +1 -1
- package/dist/replica/commands/clone.js +2 -1
- package/dist/replica/commands/index.d.ts +1 -0
- package/dist/replica/commands/prepare.js +7 -4
- package/dist/replica/commands/receipt.js +30 -16
- package/dist/replica/commands/types.d.ts +10 -5
- package/dist/replica/commands/validate.d.ts +5 -1
- package/dist/replica/commands/validate.js +129 -62
- package/dist/replica/diagnostics/inspect.js +51 -19
- package/dist/replica/diagnostics/types.d.ts +7 -7
- package/dist/replica/distributed-replica/helpers.js +22 -8
- package/dist/replica/distributed-replica/hydration.js +14 -2
- package/dist/replica/distributed-replica/impl-diagnostics.d.ts +1 -1
- package/dist/replica/distributed-replica/impl-diagnostics.js +4 -3
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.d.ts +1 -1
- package/dist/replica/distributed-replica/impl-hydration-orchestrate.js +76 -42
- package/dist/replica/distributed-replica/impl-optimistic.d.ts +11 -2
- package/dist/replica/distributed-replica/impl-optimistic.js +22 -6
- package/dist/replica/distributed-replica/impl-protocol.js +4 -2
- package/dist/replica/distributed-replica/impl.d.ts +8 -2
- package/dist/replica/distributed-replica/impl.js +22 -5
- package/dist/replica/distributed-replica/optimistic.js +31 -3
- package/dist/replica/distributed-replica/types.d.ts +1 -0
- package/dist/replica/index-maintenance/engine.js +14 -6
- package/dist/replica/index.d.ts +4 -0
- package/dist/replica/index.js +2 -0
- package/dist/replica/materialize.js +25 -0
- package/dist/replica/mutation-cache.d.ts +62 -0
- package/dist/replica/mutation-cache.js +91 -0
- package/dist/replica/operation-binding.js +17 -6
- package/dist/replica/persistence/state.js +9 -1
- package/dist/replica/projection-delta/canonical.d.ts +6 -0
- package/dist/replica/projection-delta/canonical.js +29 -0
- package/dist/replica/projection-delta/index.d.ts +5 -0
- package/dist/replica/projection-delta/index.js +3 -0
- package/dist/replica/projection-delta/resolve.d.ts +8 -0
- package/dist/replica/projection-delta/resolve.js +266 -0
- package/dist/replica/projection-delta/types.d.ts +415 -0
- package/dist/replica/projection-delta/types.js +6 -0
- package/dist/replica/projection-delta/validate.d.ts +18 -0
- package/dist/replica/projection-delta/validate.js +1718 -0
- package/dist/replica/projection-delta/wasm-pure.d.ts +36 -0
- package/dist/replica/projection-delta/wasm-pure.js +71 -0
- package/dist/replica/query-plan/pagination.js +18 -8
- package/dist/replica/types.d.ts +13 -2
- package/dist/sveltekit/replica.d.ts +1 -1
- package/dist/sveltekit/replica.js +3 -0
- package/dist/sveltekit/vite.d.ts +6 -6
- package/dist/sveltekit/vite.js +6 -6
- package/package.json +3 -3
|
@@ -0,0 +1,1718 @@
|
|
|
1
|
+
import { isPlainRecord } from '../../lib/is-plain-record.js';
|
|
2
|
+
import { canonicalCommandProjectionMetadata, canonicalProjectionDelta, commandProjectionMetadataByteLength, projectionDeltaByteLength } from './canonical.js';
|
|
3
|
+
import { COMMAND_PROJECTION_ARTIFACT_VERSION, COMMAND_PROJECTION_METADATA_WIRE_VERSION, PROJECTION_DELTA_WIRE_VERSION, PROJECTION_OPERATION_SEMANTICS_VERSION, PROJECTION_PROGRAM_IR_VERSION, PROJECTION_PROGRAM_VERSION } from './types.js';
|
|
4
|
+
const MAX_ITEMS = 128;
|
|
5
|
+
const MAX_DEPTH = 64;
|
|
6
|
+
const MAX_BODY_BYTES = 1024 * 1024;
|
|
7
|
+
const MAX_TRAVERSAL_NODES = MAX_BODY_BYTES;
|
|
8
|
+
const MAX_TRAVERSAL_DEPTH = MAX_DEPTH + 64;
|
|
9
|
+
const MAX_KEY_BYTES = 4 * 1024;
|
|
10
|
+
const MAX_PARTITION_BYTES = 4 * 1024;
|
|
11
|
+
const MAX_IDENTITY_BYTES = 4 * 1024;
|
|
12
|
+
const PROGRAM_ID = /^pp1:sha256:[0-9a-f]{64}$/;
|
|
13
|
+
const BINDING_ID = /^pb1:sha256:[0-9a-f]{64}$/;
|
|
14
|
+
const SHA256 = /^sha256:[0-9a-f]{64}$/;
|
|
15
|
+
const U64_MAX = 18446744073709551615n;
|
|
16
|
+
const I64_MIN = -9223372036854775808n;
|
|
17
|
+
const I64_MAX = 9223372036854775807n;
|
|
18
|
+
const encoder = new TextEncoder();
|
|
19
|
+
export class ProjectionDeltaValidationError extends TypeError {
|
|
20
|
+
path;
|
|
21
|
+
constructor(path) {
|
|
22
|
+
super(`Invalid projection delta at ${path}`);
|
|
23
|
+
this.name = 'ProjectionDeltaValidationError';
|
|
24
|
+
this.path = path;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
export function parseProjectionDelta(value) {
|
|
28
|
+
preflightProjectionPayload(value, 'projection.delta');
|
|
29
|
+
const delta = exactRecord(value, ['wire_version', 'identity', 'projections', 'occurrences', 'operations'], ['recoveries'], 'projection.delta');
|
|
30
|
+
if (delta.wire_version !== PROJECTION_DELTA_WIRE_VERSION) {
|
|
31
|
+
invalid('projection.delta.wire_version');
|
|
32
|
+
}
|
|
33
|
+
const identityValue = exactRecord(delta.identity, [
|
|
34
|
+
'manifest_version',
|
|
35
|
+
'client_protocol_version',
|
|
36
|
+
'surface',
|
|
37
|
+
'schema_fingerprint',
|
|
38
|
+
'protocol_fingerprint',
|
|
39
|
+
'authorization_generation',
|
|
40
|
+
'cache_scope_token',
|
|
41
|
+
'command_causation_id'
|
|
42
|
+
], [], 'projection.delta.identity');
|
|
43
|
+
if (identityValue.manifest_version !== 2 ||
|
|
44
|
+
identityValue.client_protocol_version !== 1) {
|
|
45
|
+
invalid('projection.delta.identity');
|
|
46
|
+
}
|
|
47
|
+
const identity = Object.freeze({
|
|
48
|
+
manifest_version: 2,
|
|
49
|
+
client_protocol_version: 1,
|
|
50
|
+
surface: parseSurface(identityValue.surface, 'projection.delta.identity.surface'),
|
|
51
|
+
schema_fingerprint: identityString(identityValue.schema_fingerprint, 'projection.delta.identity.schema_fingerprint'),
|
|
52
|
+
protocol_fingerprint: identityString(identityValue.protocol_fingerprint, 'projection.delta.identity.protocol_fingerprint'),
|
|
53
|
+
authorization_generation: identityString(identityValue.authorization_generation, 'projection.delta.identity.authorization_generation'),
|
|
54
|
+
cache_scope_token: protocolToken(identityValue.cache_scope_token, 'cache-scope', 'projection.delta.identity.cache_scope_token'),
|
|
55
|
+
command_causation_id: identityString(identityValue.command_causation_id, 'projection.delta.identity.command_causation_id')
|
|
56
|
+
});
|
|
57
|
+
const projections = boundedArray(delta.projections, 'projection.delta.projections').map((item, index) => parseProjectionIdentity(item, `projection.delta.projections[${index}]`));
|
|
58
|
+
assertStrictOrder(projections, compareProjectionIdentity, 'projection.delta.projections');
|
|
59
|
+
const occurrences = boundedArray(delta.occurrences, 'projection.delta.occurrences').map((item, index) => {
|
|
60
|
+
const path = `projection.delta.occurrences[${index}]`;
|
|
61
|
+
const occurrence = exactRecord(item, ['causation_id', 'ordinal', 'occurrence_id'], [], path);
|
|
62
|
+
const causationId = identityString(occurrence.causation_id, `${path}.causation_id`);
|
|
63
|
+
const ordinal = boundedOrdinal(occurrence.ordinal, `${path}.ordinal`);
|
|
64
|
+
const occurrenceId = identityString(occurrence.occurrence_id, `${path}.occurrence_id`);
|
|
65
|
+
if (ordinal !== index || causationId !== identity.command_causation_id) {
|
|
66
|
+
invalid(path);
|
|
67
|
+
}
|
|
68
|
+
return Object.freeze({
|
|
69
|
+
causation_id: causationId,
|
|
70
|
+
ordinal,
|
|
71
|
+
occurrence_id: occurrenceId
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
assertUnique(occurrences.map(({ occurrence_id }) => occurrence_id), 'projection.delta.occurrences');
|
|
75
|
+
const operations = boundedArray(delta.operations, 'projection.delta.operations').map((item, index) => {
|
|
76
|
+
const path = `projection.delta.operations[${index}]`;
|
|
77
|
+
const operation = exactRecord(item, ['occurrence_ordinal', 'projection_refs', 'mutation'], [], path);
|
|
78
|
+
const occurrenceOrdinal = boundedOrdinal(operation.occurrence_ordinal, `${path}.occurrence_ordinal`);
|
|
79
|
+
if (occurrenceOrdinal >= occurrences.length) {
|
|
80
|
+
invalid(`${path}.occurrence_ordinal`);
|
|
81
|
+
}
|
|
82
|
+
return Object.freeze({
|
|
83
|
+
occurrence_ordinal: occurrenceOrdinal,
|
|
84
|
+
projection_refs: projectionRefs(operation.projection_refs, projections.length, `${path}.projection_refs`),
|
|
85
|
+
mutation: parseMutation(operation.mutation, `${path}.mutation`)
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
assertStrictOrder(operations, (left, right) => compareTuple([operationScopeKey(left.mutation), left.occurrence_ordinal], [operationScopeKey(right.mutation), right.occurrence_ordinal]), 'projection.delta.operations');
|
|
89
|
+
assertUnique(operations.map(({ mutation }) => JSON.stringify(operationScopeKey(mutation))), 'projection.delta.operations');
|
|
90
|
+
const recoveries = boundedArray(delta.recoveries ?? [], 'projection.delta.recoveries').map((item, index) => parseRecovery(item, index, projections.length, occurrences.length));
|
|
91
|
+
assertStrictOrder(recoveries, (left, right) => compareTuple([recoveryTargetKey(left.target), left.occurrence_ordinal], [recoveryTargetKey(right.target), right.occurrence_ordinal]), 'projection.delta.recoveries');
|
|
92
|
+
assertUnique(recoveries.map(({ target }) => JSON.stringify(recoveryTargetKey(target))), 'projection.delta.recoveries');
|
|
93
|
+
for (const recovery of recoveries) {
|
|
94
|
+
if (recovery.condition !== 'if_record_missing')
|
|
95
|
+
continue;
|
|
96
|
+
if (recovery.target.kind !== 'record' ||
|
|
97
|
+
!operations.some(({ mutation }) => mutation.op === 'patch' &&
|
|
98
|
+
compareTuple(scopeKey(mutation.scope), scopeKey(recovery.target.kind === 'record'
|
|
99
|
+
? recovery.target.scope
|
|
100
|
+
: mutation.scope)) === 0)) {
|
|
101
|
+
invalid('projection.delta.recoveries');
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
if (projections.length === 0 &&
|
|
105
|
+
(occurrences.length !== 0 ||
|
|
106
|
+
operations.length !== 0 ||
|
|
107
|
+
recoveries.length !== 0)) {
|
|
108
|
+
invalid('projection.delta.projections');
|
|
109
|
+
}
|
|
110
|
+
const parsed = Object.freeze({
|
|
111
|
+
wire_version: 1,
|
|
112
|
+
identity,
|
|
113
|
+
projections: Object.freeze(projections),
|
|
114
|
+
occurrences: Object.freeze(occurrences),
|
|
115
|
+
operations: Object.freeze(operations),
|
|
116
|
+
recoveries: Object.freeze(recoveries)
|
|
117
|
+
});
|
|
118
|
+
if (projectionDeltaByteLength(parsed) > MAX_BODY_BYTES) {
|
|
119
|
+
invalid('projection.delta');
|
|
120
|
+
}
|
|
121
|
+
return parsed;
|
|
122
|
+
}
|
|
123
|
+
export function parseCommandProjectionMetadata(value) {
|
|
124
|
+
preflightProjectionPayload(value, 'projection');
|
|
125
|
+
const metadata = exactRecord(value, [
|
|
126
|
+
'wireVersion',
|
|
127
|
+
'issuedAtUnixMs',
|
|
128
|
+
'expiresAtUnixMs',
|
|
129
|
+
'delta',
|
|
130
|
+
'lifecycleProofs',
|
|
131
|
+
'obligations',
|
|
132
|
+
'revalidate'
|
|
133
|
+
], [], 'projection');
|
|
134
|
+
if (metadata.wireVersion !== COMMAND_PROJECTION_METADATA_WIRE_VERSION ||
|
|
135
|
+
typeof metadata.revalidate !== 'boolean') {
|
|
136
|
+
invalid('projection');
|
|
137
|
+
}
|
|
138
|
+
const issuedAtUnixMs = safeUnsignedInteger(metadata.issuedAtUnixMs, 'projection.issuedAtUnixMs');
|
|
139
|
+
const expiresAtUnixMs = safeUnsignedInteger(metadata.expiresAtUnixMs, 'projection.expiresAtUnixMs');
|
|
140
|
+
if (issuedAtUnixMs >= expiresAtUnixMs)
|
|
141
|
+
invalid('projection.expiresAtUnixMs');
|
|
142
|
+
const delta = parseProjectionDelta(metadata.delta);
|
|
143
|
+
const lifecycleProofs = boundedArray(metadata.lifecycleProofs, 'projection.lifecycleProofs').map((item, index) => {
|
|
144
|
+
const path = `projection.lifecycleProofs[${index}]`;
|
|
145
|
+
const proof = exactRecord(item, ['projectionRef', 'token'], [], path);
|
|
146
|
+
const projectionRef = boundedOrdinal(proof.projectionRef, `${path}.projectionRef`);
|
|
147
|
+
if (projectionRef !== index || projectionRef >= delta.projections.length) {
|
|
148
|
+
invalid(path);
|
|
149
|
+
}
|
|
150
|
+
return Object.freeze({
|
|
151
|
+
projectionRef,
|
|
152
|
+
token: protocolToken(proof.token, 'projection-lifecycle', `${path}.token`)
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
if (lifecycleProofs.length !== delta.projections.length) {
|
|
156
|
+
invalid('projection.lifecycleProofs');
|
|
157
|
+
}
|
|
158
|
+
const obligations = boundedArray(metadata.obligations, 'projection.obligations').map((item, index) => parseObligation(item, index, delta));
|
|
159
|
+
assertStrictOrder(obligations, (left, right) => compareTuple([left.projectionRef, left.model, left.scopeToken], [right.projectionRef, right.model, right.scopeToken]), 'projection.obligations');
|
|
160
|
+
const scopeModels = new Map();
|
|
161
|
+
for (const obligation of obligations) {
|
|
162
|
+
const key = JSON.stringify([
|
|
163
|
+
obligation.projectionRef,
|
|
164
|
+
obligation.scopeToken
|
|
165
|
+
]);
|
|
166
|
+
const previous = scopeModels.get(key);
|
|
167
|
+
if (previous !== undefined && previous !== obligation.model) {
|
|
168
|
+
invalid('projection.obligations');
|
|
169
|
+
}
|
|
170
|
+
scopeModels.set(key, obligation.model);
|
|
171
|
+
}
|
|
172
|
+
if (!metadata.revalidate && delta.recoveries.length !== 0) {
|
|
173
|
+
invalid('projection.revalidate');
|
|
174
|
+
}
|
|
175
|
+
const parsed = Object.freeze({
|
|
176
|
+
wireVersion: 1,
|
|
177
|
+
issuedAtUnixMs,
|
|
178
|
+
expiresAtUnixMs,
|
|
179
|
+
delta,
|
|
180
|
+
lifecycleProofs: Object.freeze(lifecycleProofs),
|
|
181
|
+
obligations: Object.freeze(obligations),
|
|
182
|
+
revalidate: metadata.revalidate
|
|
183
|
+
});
|
|
184
|
+
if (commandProjectionMetadataByteLength(parsed) > MAX_BODY_BYTES) {
|
|
185
|
+
invalid('projection');
|
|
186
|
+
}
|
|
187
|
+
return parsed;
|
|
188
|
+
}
|
|
189
|
+
export function validateCommandProjectionArtifact(value, path = 'artifact.projection') {
|
|
190
|
+
preflightProjectionPayload(value, path);
|
|
191
|
+
const projection = exactRecord(value, [
|
|
192
|
+
'version',
|
|
193
|
+
'deltaWireVersion',
|
|
194
|
+
'projectionProgramVersion',
|
|
195
|
+
'operationSemanticsVersion',
|
|
196
|
+
'projections',
|
|
197
|
+
'eventSet',
|
|
198
|
+
'capabilities',
|
|
199
|
+
'preview',
|
|
200
|
+
'fallback'
|
|
201
|
+
], ['pureReduces'], path);
|
|
202
|
+
if (projection.version !== COMMAND_PROJECTION_ARTIFACT_VERSION ||
|
|
203
|
+
projection.deltaWireVersion !== PROJECTION_DELTA_WIRE_VERSION ||
|
|
204
|
+
projection.projectionProgramVersion !== PROJECTION_PROGRAM_VERSION ||
|
|
205
|
+
projection.operationSemanticsVersion !==
|
|
206
|
+
PROJECTION_OPERATION_SEMANTICS_VERSION ||
|
|
207
|
+
projection.fallback !== 'revalidate') {
|
|
208
|
+
invalid(path);
|
|
209
|
+
}
|
|
210
|
+
const projections = boundedArray(projection.projections, `${path}.projections`).map((item, index) => {
|
|
211
|
+
const itemPath = `${path}.projections[${index}]`;
|
|
212
|
+
const identity = exactRecord(item, [
|
|
213
|
+
'programId',
|
|
214
|
+
'bindingId',
|
|
215
|
+
'epoch',
|
|
216
|
+
'programIrVersion',
|
|
217
|
+
'operationSemanticsVersion'
|
|
218
|
+
], [], itemPath);
|
|
219
|
+
if (!PROGRAM_ID.test(identity.programId) ||
|
|
220
|
+
!BINDING_ID.test(identity.bindingId) ||
|
|
221
|
+
identity.programIrVersion !== PROJECTION_PROGRAM_IR_VERSION ||
|
|
222
|
+
identity.operationSemanticsVersion !==
|
|
223
|
+
PROJECTION_OPERATION_SEMANTICS_VERSION) {
|
|
224
|
+
invalid(itemPath);
|
|
225
|
+
}
|
|
226
|
+
return Object.freeze({
|
|
227
|
+
programId: identityString(identity.programId, `${itemPath}.programId`),
|
|
228
|
+
bindingId: identityString(identity.bindingId, `${itemPath}.bindingId`),
|
|
229
|
+
epoch: projectionEpoch(identity.epoch, `${itemPath}.epoch`),
|
|
230
|
+
programIrVersion: 1,
|
|
231
|
+
operationSemanticsVersion: 1
|
|
232
|
+
});
|
|
233
|
+
});
|
|
234
|
+
assertStrictOrder(projections, (left, right) => compareTuple([
|
|
235
|
+
left.programId,
|
|
236
|
+
left.bindingId,
|
|
237
|
+
left.epoch,
|
|
238
|
+
left.programIrVersion,
|
|
239
|
+
left.operationSemanticsVersion
|
|
240
|
+
], [
|
|
241
|
+
right.programId,
|
|
242
|
+
right.bindingId,
|
|
243
|
+
right.epoch,
|
|
244
|
+
right.programIrVersion,
|
|
245
|
+
right.operationSemanticsVersion
|
|
246
|
+
]), `${path}.projections`);
|
|
247
|
+
const eventSet = boundedArray(projection.eventSet, `${path}.eventSet`).map((item, index) => parseEventRef(item, `${path}.eventSet[${index}]`));
|
|
248
|
+
assertStrictOrder(eventSet, (left, right) => compareTuple([left.id, left.name, left.version], [right.id, right.name, right.version]), `${path}.eventSet`);
|
|
249
|
+
const capabilitiesValue = exactRecord(projection.capabilities, ['version', 'arms'], [], `${path}.capabilities`);
|
|
250
|
+
if (capabilitiesValue.version !== 1) {
|
|
251
|
+
invalid(`${path}.capabilities.version`);
|
|
252
|
+
}
|
|
253
|
+
const capabilityArms = boundedArray(capabilitiesValue.arms, `${path}.capabilities.arms`).map((item, index) => parseCapabilityArm(item, index, projections.length, eventSet, `${path}.capabilities.arms[${index}]`));
|
|
254
|
+
if (capabilityArms.length === 0 ||
|
|
255
|
+
eventSet.some((event) => !capabilityArms.some((arm) => sameEventRef(arm.event, event)))) {
|
|
256
|
+
invalid(`${path}.capabilities.arms`);
|
|
257
|
+
}
|
|
258
|
+
assertStrictOrder(capabilityArms, (left, right) => compareTuple([
|
|
259
|
+
left.event.id,
|
|
260
|
+
left.event.name,
|
|
261
|
+
left.event.version,
|
|
262
|
+
left.projection_ref,
|
|
263
|
+
left.arm
|
|
264
|
+
], [
|
|
265
|
+
right.event.id,
|
|
266
|
+
right.event.name,
|
|
267
|
+
right.event.version,
|
|
268
|
+
right.projection_ref,
|
|
269
|
+
right.arm
|
|
270
|
+
]), `${path}.capabilities.arms`);
|
|
271
|
+
const previewValue = exactRecord(projection.preview, ['version', 'occurrences', 'operations', 'recoveries'], [], `${path}.preview`);
|
|
272
|
+
if (previewValue.version !== 1)
|
|
273
|
+
invalid(`${path}.preview.version`);
|
|
274
|
+
const occurrences = boundedArray(previewValue.occurrences, `${path}.preview.occurrences`).map((item, index) => {
|
|
275
|
+
const itemPath = `${path}.preview.occurrences[${index}]`;
|
|
276
|
+
const occurrence = exactRecord(item, ['ordinal', 'event'], [], itemPath);
|
|
277
|
+
const ordinal = boundedOrdinal(occurrence.ordinal, `${itemPath}.ordinal`);
|
|
278
|
+
const event = parseEventRef(occurrence.event, `${itemPath}.event`);
|
|
279
|
+
if (ordinal !== index ||
|
|
280
|
+
!eventSet.some((candidate) => candidate.id === event.id &&
|
|
281
|
+
candidate.name === event.name &&
|
|
282
|
+
candidate.version === event.version)) {
|
|
283
|
+
invalid(itemPath);
|
|
284
|
+
}
|
|
285
|
+
return Object.freeze({ ordinal, event });
|
|
286
|
+
});
|
|
287
|
+
const operations = boundedArray(previewValue.operations, `${path}.preview.operations`).map((item, index) => {
|
|
288
|
+
const itemPath = `${path}.preview.operations[${index}]`;
|
|
289
|
+
const operation = exactRecord(item, ['occurrence_ordinal', 'projection_refs', 'mutation'], [], itemPath);
|
|
290
|
+
const occurrenceOrdinal = boundedOrdinal(operation.occurrence_ordinal, `${itemPath}.occurrence_ordinal`);
|
|
291
|
+
if (occurrenceOrdinal >= occurrences.length)
|
|
292
|
+
invalid(itemPath);
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
occurrence_ordinal: occurrenceOrdinal,
|
|
295
|
+
projection_refs: projectionRefs(operation.projection_refs, projections.length, `${itemPath}.projection_refs`),
|
|
296
|
+
mutation: parsePreviewMutation(operation.mutation, `${itemPath}.mutation`)
|
|
297
|
+
});
|
|
298
|
+
});
|
|
299
|
+
assertStrictOrder(operations, (left, right) => compareTuple([
|
|
300
|
+
previewOperationScopeKey(left.mutation),
|
|
301
|
+
left.occurrence_ordinal
|
|
302
|
+
], [
|
|
303
|
+
previewOperationScopeKey(right.mutation),
|
|
304
|
+
right.occurrence_ordinal
|
|
305
|
+
]), `${path}.preview.operations`);
|
|
306
|
+
assertUnique(operations.map(({ mutation }) => JSON.stringify(previewOperationScopeKey(mutation))), `${path}.preview.operations`);
|
|
307
|
+
const recoveries = boundedArray(previewValue.recoveries, `${path}.preview.recoveries`).map((item, index) => {
|
|
308
|
+
const itemPath = `${path}.preview.recoveries[${index}]`;
|
|
309
|
+
const recovery = exactRecord(item, ['occurrence_ordinal', 'projection_refs', 'condition', 'target'], [], itemPath);
|
|
310
|
+
const occurrenceOrdinal = boundedOrdinal(recovery.occurrence_ordinal, `${itemPath}.occurrence_ordinal`);
|
|
311
|
+
if (occurrenceOrdinal >= occurrences.length ||
|
|
312
|
+
(recovery.condition !== 'always' &&
|
|
313
|
+
recovery.condition !== 'if_record_missing')) {
|
|
314
|
+
invalid(itemPath);
|
|
315
|
+
}
|
|
316
|
+
return Object.freeze({
|
|
317
|
+
occurrence_ordinal: occurrenceOrdinal,
|
|
318
|
+
projection_refs: projectionRefs(recovery.projection_refs, projections.length, `${itemPath}.projection_refs`),
|
|
319
|
+
condition: recovery.condition,
|
|
320
|
+
target: parsePreviewRecoveryTarget(recovery.target, `${itemPath}.target`)
|
|
321
|
+
});
|
|
322
|
+
});
|
|
323
|
+
assertStrictOrder(recoveries, (left, right) => compareTuple([
|
|
324
|
+
previewRecoveryTargetKey(left.target),
|
|
325
|
+
left.occurrence_ordinal
|
|
326
|
+
], [
|
|
327
|
+
previewRecoveryTargetKey(right.target),
|
|
328
|
+
right.occurrence_ordinal
|
|
329
|
+
]), `${path}.preview.recoveries`);
|
|
330
|
+
assertUnique(recoveries.map(({ target }) => JSON.stringify(previewRecoveryTargetKey(target))), `${path}.preview.recoveries`);
|
|
331
|
+
for (const recovery of recoveries) {
|
|
332
|
+
if (recovery.condition === 'if_record_missing' &&
|
|
333
|
+
(recovery.target.kind !== 'record' ||
|
|
334
|
+
!operations.some(({ mutation }) => mutation.op === 'patch' &&
|
|
335
|
+
compareTuple(previewScopeKey(mutation.scope), previewScopeKey(recovery.target.kind === 'record'
|
|
336
|
+
? recovery.target.scope
|
|
337
|
+
: mutation.scope)) === 0))) {
|
|
338
|
+
invalid(`${path}.preview.recoveries`);
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
const pureReduces = parsePureReduces(projection.pureReduces, occurrences.length, projections.length, `${path}.pureReduces`);
|
|
342
|
+
const parsed = Object.freeze({
|
|
343
|
+
version: 2,
|
|
344
|
+
deltaWireVersion: 1,
|
|
345
|
+
projectionProgramVersion: 2,
|
|
346
|
+
operationSemanticsVersion: 1,
|
|
347
|
+
projections: Object.freeze(projections),
|
|
348
|
+
eventSet: Object.freeze(eventSet),
|
|
349
|
+
capabilities: Object.freeze({
|
|
350
|
+
version: 1,
|
|
351
|
+
arms: Object.freeze(capabilityArms)
|
|
352
|
+
}),
|
|
353
|
+
preview: Object.freeze({
|
|
354
|
+
version: 1,
|
|
355
|
+
occurrences: Object.freeze(occurrences),
|
|
356
|
+
operations: Object.freeze(operations),
|
|
357
|
+
recoveries: Object.freeze(recoveries)
|
|
358
|
+
}),
|
|
359
|
+
...(pureReduces.length === 0 ? {} : { pureReduces }),
|
|
360
|
+
fallback: 'revalidate'
|
|
361
|
+
});
|
|
362
|
+
if (encoder.encode(JSON.stringify(parsed)).byteLength > MAX_BODY_BYTES) {
|
|
363
|
+
invalid(path);
|
|
364
|
+
}
|
|
365
|
+
return parsed;
|
|
366
|
+
}
|
|
367
|
+
function parsePureReduces(value, occurrenceCount, projectionCount, path) {
|
|
368
|
+
if (value === undefined)
|
|
369
|
+
return Object.freeze([]);
|
|
370
|
+
return Object.freeze(boundedArray(value, path).map((item, index) => {
|
|
371
|
+
const itemPath = `${path}[${index}]`;
|
|
372
|
+
const reduce = exactRecord(item, [
|
|
373
|
+
'fn',
|
|
374
|
+
'scope',
|
|
375
|
+
'args',
|
|
376
|
+
'assign',
|
|
377
|
+
'occurrence_ordinal',
|
|
378
|
+
'projection_refs'
|
|
379
|
+
], ['clientModule', 'clientExport'], itemPath);
|
|
380
|
+
const fn = reduce.fn;
|
|
381
|
+
if (typeof fn !== 'string' || fn.length === 0 || fn.length > 128) {
|
|
382
|
+
invalid(`${itemPath}.fn`);
|
|
383
|
+
}
|
|
384
|
+
const occurrenceOrdinal = boundedOrdinal(reduce.occurrence_ordinal, `${itemPath}.occurrence_ordinal`);
|
|
385
|
+
if (occurrenceOrdinal >= occurrenceCount)
|
|
386
|
+
invalid(itemPath);
|
|
387
|
+
const assign = boundedArray(reduce.assign, `${itemPath}.assign`).map((field, fieldIndex) => {
|
|
388
|
+
if (typeof field !== 'string' || field.length === 0) {
|
|
389
|
+
invalid(`${itemPath}.assign[${fieldIndex}]`);
|
|
390
|
+
}
|
|
391
|
+
return field;
|
|
392
|
+
});
|
|
393
|
+
if (assign.length === 0)
|
|
394
|
+
invalid(`${itemPath}.assign`);
|
|
395
|
+
const args = boundedArray(reduce.args, `${itemPath}.args`).map((arg, argIndex) => {
|
|
396
|
+
const argPath = `${itemPath}.args[${argIndex}]`;
|
|
397
|
+
const entry = exactRecord(arg, ['name', 'value'], [], argPath);
|
|
398
|
+
if (typeof entry.name !== 'string' || entry.name.length === 0) {
|
|
399
|
+
invalid(`${argPath}.name`);
|
|
400
|
+
}
|
|
401
|
+
return Object.freeze({
|
|
402
|
+
name: entry.name,
|
|
403
|
+
value: parsePreviewValue(entry.value, `${argPath}.value`, 0)
|
|
404
|
+
});
|
|
405
|
+
});
|
|
406
|
+
const clientModule = typeof reduce.clientModule === 'string'
|
|
407
|
+
? reduce.clientModule
|
|
408
|
+
: undefined;
|
|
409
|
+
const clientExport = typeof reduce.clientExport === 'string'
|
|
410
|
+
? reduce.clientExport
|
|
411
|
+
: undefined;
|
|
412
|
+
return Object.freeze({
|
|
413
|
+
fn: fn,
|
|
414
|
+
...(clientModule === undefined ? {} : { clientModule }),
|
|
415
|
+
...(clientExport === undefined ? {} : { clientExport }),
|
|
416
|
+
scope: parsePreviewScope(reduce.scope, `${itemPath}.scope`),
|
|
417
|
+
args: Object.freeze(args),
|
|
418
|
+
assign: Object.freeze(assign),
|
|
419
|
+
occurrence_ordinal: occurrenceOrdinal,
|
|
420
|
+
projection_refs: projectionRefs(reduce.projection_refs, projectionCount, `${itemPath}.projection_refs`)
|
|
421
|
+
});
|
|
422
|
+
}));
|
|
423
|
+
}
|
|
424
|
+
export function validateProjectionMetadataAuthority(metadata, contract, authority, nowUnixMs = Date.now()) {
|
|
425
|
+
const identity = metadata.delta.identity;
|
|
426
|
+
if (nowUnixMs >= metadata.expiresAtUnixMs ||
|
|
427
|
+
identity.surface.kind !== authority.surface.kind ||
|
|
428
|
+
identity.surface.name !== authority.surface.name ||
|
|
429
|
+
(identity.surface.kind === 'application' &&
|
|
430
|
+
(authority.surface.kind !== 'application' ||
|
|
431
|
+
compareStringArrays(identity.surface.eligible_roles, authority.surface.eligible_roles) !== 0 ||
|
|
432
|
+
compareStringArrays(identity.surface.schema_roles, authority.surface.schema_roles) !== 0)) ||
|
|
433
|
+
identity.schema_fingerprint !== authority.schemaHash ||
|
|
434
|
+
identity.protocol_fingerprint !== authority.protocolHash ||
|
|
435
|
+
identity.authorization_generation !== authority.authorizationGeneration ||
|
|
436
|
+
identity.cache_scope_token !== authority.cacheScope ||
|
|
437
|
+
identity.command_causation_id !== authority.causationId) {
|
|
438
|
+
invalid('projection.delta.identity');
|
|
439
|
+
}
|
|
440
|
+
if (metadata.delta.projections.length !== contract.projections.length) {
|
|
441
|
+
invalid('projection.delta.projections');
|
|
442
|
+
}
|
|
443
|
+
for (let index = 0; index < contract.projections.length; index += 1) {
|
|
444
|
+
const expected = contract.projections[index];
|
|
445
|
+
const actual = metadata.delta.projections[index];
|
|
446
|
+
if (expected.programId !== actual.program_id ||
|
|
447
|
+
expected.bindingId !== actual.binding_id ||
|
|
448
|
+
expected.epoch !== actual.epoch ||
|
|
449
|
+
expected.programIrVersion !== actual.program_ir_version ||
|
|
450
|
+
expected.operationSemanticsVersion !==
|
|
451
|
+
actual.operation_semantics_version) {
|
|
452
|
+
invalid(`projection.delta.projections[${index}]`);
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
return canonicalCommandProjectionMetadata(metadata);
|
|
456
|
+
}
|
|
457
|
+
function parseProjectionIdentity(value, path) {
|
|
458
|
+
const identity = exactRecord(value, [
|
|
459
|
+
'program_id',
|
|
460
|
+
'binding_id',
|
|
461
|
+
'epoch',
|
|
462
|
+
'program_ir_version',
|
|
463
|
+
'operation_semantics_version'
|
|
464
|
+
], [], path);
|
|
465
|
+
if (typeof identity.program_id !== 'string' ||
|
|
466
|
+
!PROGRAM_ID.test(identity.program_id) ||
|
|
467
|
+
typeof identity.binding_id !== 'string' ||
|
|
468
|
+
!BINDING_ID.test(identity.binding_id) ||
|
|
469
|
+
identity.program_ir_version !== PROJECTION_PROGRAM_IR_VERSION ||
|
|
470
|
+
identity.operation_semantics_version !==
|
|
471
|
+
PROJECTION_OPERATION_SEMANTICS_VERSION) {
|
|
472
|
+
invalid(path);
|
|
473
|
+
}
|
|
474
|
+
return Object.freeze({
|
|
475
|
+
program_id: identity.program_id,
|
|
476
|
+
binding_id: identity.binding_id,
|
|
477
|
+
epoch: projectionEpoch(identity.epoch, `${path}.epoch`),
|
|
478
|
+
program_ir_version: 1,
|
|
479
|
+
operation_semantics_version: 1
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function parseSurface(value, path) {
|
|
483
|
+
if (!isPlainRecord(value))
|
|
484
|
+
invalid(path);
|
|
485
|
+
if (value.kind === 'role') {
|
|
486
|
+
const role = exactRecord(value, ['kind', 'name'], [], path);
|
|
487
|
+
return Object.freeze({
|
|
488
|
+
kind: 'role',
|
|
489
|
+
name: identityString(role.name, `${path}.name`)
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
if (value.kind === 'application') {
|
|
493
|
+
const application = exactRecord(value, ['kind', 'name', 'eligible_roles', 'schema_roles'], [], path);
|
|
494
|
+
const eligibleRoles = boundedArray(application.eligible_roles, `${path}.eligible_roles`).map((role, index) => identityString(role, `${path}.eligible_roles[${index}]`));
|
|
495
|
+
const schemaRoles = boundedArray(application.schema_roles, `${path}.schema_roles`).map((role, index) => identityString(role, `${path}.schema_roles[${index}]`));
|
|
496
|
+
if (eligibleRoles.length === 0)
|
|
497
|
+
invalid(`${path}.eligible_roles`);
|
|
498
|
+
if (schemaRoles.length === 0)
|
|
499
|
+
invalid(`${path}.schema_roles`);
|
|
500
|
+
assertStrictOrder(eligibleRoles, compareUtf8, `${path}.eligible_roles`);
|
|
501
|
+
assertStrictOrder(schemaRoles, compareUtf8, `${path}.schema_roles`);
|
|
502
|
+
if (schemaRoles.some((role) => !eligibleRoles.includes(role))) {
|
|
503
|
+
invalid(`${path}.schema_roles`);
|
|
504
|
+
}
|
|
505
|
+
return Object.freeze({
|
|
506
|
+
kind: 'application',
|
|
507
|
+
name: identityString(application.name, `${path}.name`),
|
|
508
|
+
eligible_roles: Object.freeze(eligibleRoles),
|
|
509
|
+
schema_roles: Object.freeze(schemaRoles)
|
|
510
|
+
});
|
|
511
|
+
}
|
|
512
|
+
invalid(`${path}.kind`);
|
|
513
|
+
}
|
|
514
|
+
function parseMutation(value, path) {
|
|
515
|
+
if (!isPlainRecord(value) || typeof value.op !== 'string') {
|
|
516
|
+
invalid(path);
|
|
517
|
+
}
|
|
518
|
+
switch (value.op) {
|
|
519
|
+
case 'upsert': {
|
|
520
|
+
const mutation = exactRecord(value, ['op', 'scope', 'fields', 'replace'], [], path);
|
|
521
|
+
const fields = parseFields(mutation.fields, `${path}.fields`);
|
|
522
|
+
const replace = sortedNames(mutation.replace, `${path}.replace`);
|
|
523
|
+
if (fields.some(({ field }) => !replace.includes(field)))
|
|
524
|
+
invalid(path);
|
|
525
|
+
return Object.freeze({
|
|
526
|
+
op: 'upsert',
|
|
527
|
+
scope: parseScope(mutation.scope, `${path}.scope`),
|
|
528
|
+
fields,
|
|
529
|
+
replace
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
case 'patch': {
|
|
533
|
+
const mutation = exactRecord(value, ['op', 'scope', 'if_present'], ['set', 'unset'], path);
|
|
534
|
+
if (mutation.if_present !== true)
|
|
535
|
+
invalid(`${path}.if_present`);
|
|
536
|
+
const set = parseFields(mutation.set ?? [], `${path}.set`);
|
|
537
|
+
const unset = sortedNames(mutation.unset ?? [], `${path}.unset`);
|
|
538
|
+
if ((set.length === 0 && unset.length === 0) ||
|
|
539
|
+
set.some(({ field }) => unset.includes(field))) {
|
|
540
|
+
invalid(path);
|
|
541
|
+
}
|
|
542
|
+
return Object.freeze({
|
|
543
|
+
op: 'patch',
|
|
544
|
+
scope: parseScope(mutation.scope, `${path}.scope`),
|
|
545
|
+
set,
|
|
546
|
+
unset,
|
|
547
|
+
if_present: true
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
case 'delete': {
|
|
551
|
+
const mutation = exactRecord(value, ['op', 'scope'], [], path);
|
|
552
|
+
return Object.freeze({
|
|
553
|
+
op: 'delete',
|
|
554
|
+
scope: parseScope(mutation.scope, `${path}.scope`)
|
|
555
|
+
});
|
|
556
|
+
}
|
|
557
|
+
case 'link':
|
|
558
|
+
case 'unlink': {
|
|
559
|
+
const mutation = exactRecord(value, ['op', 'relationship', 'source', 'target'], [], path);
|
|
560
|
+
return Object.freeze({
|
|
561
|
+
op: value.op,
|
|
562
|
+
relationship: identityString(mutation.relationship, `${path}.relationship`),
|
|
563
|
+
source: parseScope(mutation.source, `${path}.source`),
|
|
564
|
+
target: parseScope(mutation.target, `${path}.target`)
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
case 'invalidate_model': {
|
|
568
|
+
const mutation = exactRecord(value, ['op', 'model'], ['partition'], path);
|
|
569
|
+
return Object.freeze({
|
|
570
|
+
op: 'invalidate_model',
|
|
571
|
+
...(mutation.partition === undefined
|
|
572
|
+
? {}
|
|
573
|
+
: {
|
|
574
|
+
partition: parsePartition(mutation.partition, `${path}.partition`)
|
|
575
|
+
}),
|
|
576
|
+
model: projectionModel(mutation.model, `${path}.model`)
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
case 'invalidate_relationship': {
|
|
580
|
+
const mutation = exactRecord(value, ['op', 'relationship', 'source'], [], path);
|
|
581
|
+
return Object.freeze({
|
|
582
|
+
op: 'invalidate_relationship',
|
|
583
|
+
relationship: identityString(mutation.relationship, `${path}.relationship`),
|
|
584
|
+
source: parseScope(mutation.source, `${path}.source`)
|
|
585
|
+
});
|
|
586
|
+
}
|
|
587
|
+
default:
|
|
588
|
+
invalid(`${path}.op`);
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
function parseScope(value, path) {
|
|
592
|
+
const scope = exactRecord(value, ['partition', 'model', 'key'], [], path);
|
|
593
|
+
const key = boundedArray(scope.key, `${path}.key`).map((item, index) => {
|
|
594
|
+
const itemPath = `${path}.key[${index}]`;
|
|
595
|
+
const field = exactRecord(item, ['ordinal', 'field', 'value'], [], itemPath);
|
|
596
|
+
const ordinal = boundedOrdinal(field.ordinal, `${itemPath}.ordinal`);
|
|
597
|
+
const name = identityString(field.field, `${itemPath}.field`);
|
|
598
|
+
const fieldValue = parseValue(field.value, `${itemPath}.value`, 1);
|
|
599
|
+
if (ordinal !== index ||
|
|
600
|
+
fieldValue.type === 'null' ||
|
|
601
|
+
fieldValue.type === 'list' ||
|
|
602
|
+
fieldValue.type === 'object') {
|
|
603
|
+
invalid(itemPath);
|
|
604
|
+
}
|
|
605
|
+
return Object.freeze({ ordinal, field: name, value: fieldValue });
|
|
606
|
+
});
|
|
607
|
+
if (key.length === 0)
|
|
608
|
+
invalid(`${path}.key`);
|
|
609
|
+
assertUnique(key.map(({ field }) => field), `${path}.key`);
|
|
610
|
+
if (encoder.encode(JSON.stringify(key)).byteLength > MAX_KEY_BYTES) {
|
|
611
|
+
invalid(`${path}.key`);
|
|
612
|
+
}
|
|
613
|
+
return Object.freeze({
|
|
614
|
+
partition: parsePartition(scope.partition, `${path}.partition`),
|
|
615
|
+
model: projectionModel(scope.model, `${path}.model`),
|
|
616
|
+
key: Object.freeze(key)
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
function parsePartition(value, path) {
|
|
620
|
+
if (!isPlainRecord(value))
|
|
621
|
+
invalid(path);
|
|
622
|
+
let partition;
|
|
623
|
+
if (value.kind === 'unit') {
|
|
624
|
+
exactRecord(value, ['kind'], [], path);
|
|
625
|
+
partition = Object.freeze({ kind: 'unit' });
|
|
626
|
+
}
|
|
627
|
+
else if (value.kind === 'opaque') {
|
|
628
|
+
const opaque = exactRecord(value, ['kind', 'token'], [], path);
|
|
629
|
+
partition = Object.freeze({
|
|
630
|
+
kind: 'opaque',
|
|
631
|
+
token: protocolToken(opaque.token, 'projection-partition', `${path}.token`)
|
|
632
|
+
});
|
|
633
|
+
}
|
|
634
|
+
else {
|
|
635
|
+
invalid(`${path}.kind`);
|
|
636
|
+
}
|
|
637
|
+
if (encoder.encode(JSON.stringify(partition)).byteLength > MAX_PARTITION_BYTES) {
|
|
638
|
+
invalid(path);
|
|
639
|
+
}
|
|
640
|
+
return partition;
|
|
641
|
+
}
|
|
642
|
+
function parseFields(value, path) {
|
|
643
|
+
const fields = boundedArray(value, path).map((item, index) => {
|
|
644
|
+
const itemPath = `${path}[${index}]`;
|
|
645
|
+
const field = exactRecord(item, ['field', 'value'], [], itemPath);
|
|
646
|
+
return Object.freeze({
|
|
647
|
+
field: identityString(field.field, `${itemPath}.field`),
|
|
648
|
+
value: parseValue(field.value, `${itemPath}.value`, 1)
|
|
649
|
+
});
|
|
650
|
+
});
|
|
651
|
+
assertStrictOrder(fields, (left, right) => compareUtf8(left.field, right.field), path);
|
|
652
|
+
return Object.freeze(fields);
|
|
653
|
+
}
|
|
654
|
+
function parseValue(value, path, depth) {
|
|
655
|
+
if (depth > MAX_DEPTH)
|
|
656
|
+
invalid(path);
|
|
657
|
+
if (!isPlainRecord(value) || typeof value.type !== 'string')
|
|
658
|
+
invalid(path);
|
|
659
|
+
switch (value.type) {
|
|
660
|
+
case 'null':
|
|
661
|
+
exactRecord(value, ['type'], [], path);
|
|
662
|
+
return Object.freeze({ type: 'null' });
|
|
663
|
+
case 'boolean': {
|
|
664
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
665
|
+
if (typeof typed.value !== 'boolean')
|
|
666
|
+
invalid(`${path}.value`);
|
|
667
|
+
return Object.freeze({ type: 'boolean', value: typed.value });
|
|
668
|
+
}
|
|
669
|
+
case 'i64':
|
|
670
|
+
case 'u64': {
|
|
671
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
672
|
+
const decimal = canonicalInteger(typed.value, value.type, `${path}.value`);
|
|
673
|
+
return Object.freeze({ type: value.type, value: decimal });
|
|
674
|
+
}
|
|
675
|
+
case 'f64': {
|
|
676
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
677
|
+
const decimal = canonicalFloat(typed.value, `${path}.value`);
|
|
678
|
+
return Object.freeze({ type: 'f64', value: decimal });
|
|
679
|
+
}
|
|
680
|
+
case 'string': {
|
|
681
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
682
|
+
if (typeof typed.value !== 'string' ||
|
|
683
|
+
encoder.encode(typed.value).byteLength > MAX_BODY_BYTES) {
|
|
684
|
+
invalid(`${path}.value`);
|
|
685
|
+
}
|
|
686
|
+
return Object.freeze({ type: 'string', value: typed.value });
|
|
687
|
+
}
|
|
688
|
+
case 'enum': {
|
|
689
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
690
|
+
const enumValue = exactRecord(typed.value, ['enum_type', 'variant'], [], `${path}.value`);
|
|
691
|
+
return Object.freeze({
|
|
692
|
+
type: 'enum',
|
|
693
|
+
value: Object.freeze({
|
|
694
|
+
enum_type: identityString(enumValue.enum_type, `${path}.value.enum_type`),
|
|
695
|
+
variant: identityString(enumValue.variant, `${path}.value.variant`)
|
|
696
|
+
})
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
case 'list': {
|
|
700
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
701
|
+
const values = boundedArray(typed.value, `${path}.value`).map((item, index) => parseValue(item, `${path}.value[${index}]`, depth + 1));
|
|
702
|
+
return Object.freeze({
|
|
703
|
+
type: 'list',
|
|
704
|
+
value: Object.freeze(values)
|
|
705
|
+
});
|
|
706
|
+
}
|
|
707
|
+
case 'object': {
|
|
708
|
+
const typed = exactRecord(value, ['type', 'value'], [], path);
|
|
709
|
+
const fields = boundedArray(typed.value, `${path}.value`).map((item, index) => {
|
|
710
|
+
const itemPath = `${path}.value[${index}]`;
|
|
711
|
+
const field = exactRecord(item, ['field', 'value'], [], itemPath);
|
|
712
|
+
return Object.freeze({
|
|
713
|
+
field: identityString(field.field, `${itemPath}.field`),
|
|
714
|
+
value: parseValue(field.value, `${itemPath}.value`, depth + 1)
|
|
715
|
+
});
|
|
716
|
+
});
|
|
717
|
+
assertStrictOrder(fields, (left, right) => compareUtf8(left.field, right.field), `${path}.value`);
|
|
718
|
+
return Object.freeze({
|
|
719
|
+
type: 'object',
|
|
720
|
+
value: Object.freeze(fields)
|
|
721
|
+
});
|
|
722
|
+
}
|
|
723
|
+
default:
|
|
724
|
+
invalid(`${path}.type`);
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
function parseRecovery(value, index, projectionCount, occurrenceCount) {
|
|
728
|
+
const path = `projection.delta.recoveries[${index}]`;
|
|
729
|
+
const recovery = exactRecord(value, ['occurrence_ordinal', 'projection_refs', 'condition', 'target'], [], path);
|
|
730
|
+
const occurrenceOrdinal = boundedOrdinal(recovery.occurrence_ordinal, `${path}.occurrence_ordinal`);
|
|
731
|
+
if (occurrenceOrdinal >= occurrenceCount ||
|
|
732
|
+
(recovery.condition !== 'always' &&
|
|
733
|
+
recovery.condition !== 'if_record_missing')) {
|
|
734
|
+
invalid(path);
|
|
735
|
+
}
|
|
736
|
+
return Object.freeze({
|
|
737
|
+
occurrence_ordinal: occurrenceOrdinal,
|
|
738
|
+
projection_refs: projectionRefs(recovery.projection_refs, projectionCount, `${path}.projection_refs`),
|
|
739
|
+
condition: recovery.condition,
|
|
740
|
+
target: parseRecoveryTarget(recovery.target, `${path}.target`)
|
|
741
|
+
});
|
|
742
|
+
}
|
|
743
|
+
function parseRecoveryTarget(value, path) {
|
|
744
|
+
if (!isPlainRecord(value) || typeof value.kind !== 'string')
|
|
745
|
+
invalid(path);
|
|
746
|
+
switch (value.kind) {
|
|
747
|
+
case 'record': {
|
|
748
|
+
const target = exactRecord(value, ['kind', 'scope'], [], path);
|
|
749
|
+
return Object.freeze({
|
|
750
|
+
kind: 'record',
|
|
751
|
+
scope: parseScope(target.scope, `${path}.scope`)
|
|
752
|
+
});
|
|
753
|
+
}
|
|
754
|
+
case 'relationship': {
|
|
755
|
+
const target = exactRecord(value, ['kind', 'relationship', 'source'], [], path);
|
|
756
|
+
return Object.freeze({
|
|
757
|
+
kind: 'relationship',
|
|
758
|
+
relationship: identityString(target.relationship, `${path}.relationship`),
|
|
759
|
+
source: parseScope(target.source, `${path}.source`)
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
case 'model': {
|
|
763
|
+
const target = exactRecord(value, ['kind', 'model'], ['partition'], path);
|
|
764
|
+
return Object.freeze({
|
|
765
|
+
kind: 'model',
|
|
766
|
+
...(target.partition === undefined
|
|
767
|
+
? {}
|
|
768
|
+
: {
|
|
769
|
+
partition: parsePartition(target.partition, `${path}.partition`)
|
|
770
|
+
}),
|
|
771
|
+
model: projectionModel(target.model, `${path}.model`)
|
|
772
|
+
});
|
|
773
|
+
}
|
|
774
|
+
default:
|
|
775
|
+
invalid(`${path}.kind`);
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
function parseObligation(value, index, delta) {
|
|
779
|
+
const path = `projection.obligations[${index}]`;
|
|
780
|
+
const obligation = exactRecord(value, ['projectionRef', 'model', 'scopeToken'], [], path);
|
|
781
|
+
const projectionRef = boundedOrdinal(obligation.projectionRef, `${path}.projectionRef`);
|
|
782
|
+
const model = obligationModel(obligation.model, `${path}.model`);
|
|
783
|
+
if (projectionRef >= delta.projections.length ||
|
|
784
|
+
!delta.operations.some(({ projection_refs, mutation }) => projection_refs.includes(projectionRef) &&
|
|
785
|
+
operationCanObserveModel(mutation, model))) {
|
|
786
|
+
invalid(path);
|
|
787
|
+
}
|
|
788
|
+
return Object.freeze({
|
|
789
|
+
projectionRef,
|
|
790
|
+
model,
|
|
791
|
+
scopeToken: protocolToken(obligation.scopeToken, 'projection-obligation', `${path}.scopeToken`)
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
function operationCanObserveModel(mutation, model) {
|
|
795
|
+
switch (mutation.op) {
|
|
796
|
+
case 'upsert':
|
|
797
|
+
case 'patch':
|
|
798
|
+
case 'delete':
|
|
799
|
+
return mutation.scope.model === model;
|
|
800
|
+
case 'link':
|
|
801
|
+
case 'unlink':
|
|
802
|
+
return true;
|
|
803
|
+
case 'invalidate_model':
|
|
804
|
+
case 'invalidate_relationship':
|
|
805
|
+
return false;
|
|
806
|
+
}
|
|
807
|
+
}
|
|
808
|
+
function parseCapabilityArm(value, _index, projectionCount, eventSet, path) {
|
|
809
|
+
const arm = exactRecord(value, ['event', 'projection_ref', 'arm', 'partition', 'mutations'], [], path);
|
|
810
|
+
const event = parseEventRef(arm.event, `${path}.event`);
|
|
811
|
+
const projectionRef = boundedOrdinal(arm.projection_ref, `${path}.projection_ref`);
|
|
812
|
+
if (projectionRef >= projectionCount ||
|
|
813
|
+
!eventSet.some((candidate) => sameEventRef(candidate, event))) {
|
|
814
|
+
invalid(path);
|
|
815
|
+
}
|
|
816
|
+
const mutations = capabilityArray(arm.mutations, `${path}.mutations`).map((item, index) => parseCapabilityMutation(item, `${path}.mutations[${index}]`));
|
|
817
|
+
if (mutations.length === 0)
|
|
818
|
+
invalid(`${path}.mutations`);
|
|
819
|
+
assertStrictOrder(mutations, (left, right) => compareTuple(capabilityMutationKey(left), capabilityMutationKey(right)), `${path}.mutations`);
|
|
820
|
+
return Object.freeze({
|
|
821
|
+
event,
|
|
822
|
+
projection_ref: projectionRef,
|
|
823
|
+
arm: identityString(arm.arm, `${path}.arm`),
|
|
824
|
+
partition: parseCapabilityPartition(arm.partition, `${path}.partition`),
|
|
825
|
+
mutations: Object.freeze(mutations)
|
|
826
|
+
});
|
|
827
|
+
}
|
|
828
|
+
function parseCapabilityPartition(value, path) {
|
|
829
|
+
if (!isPlainRecord(value) || typeof value.kind !== 'string')
|
|
830
|
+
invalid(path);
|
|
831
|
+
if (value.kind === 'unit') {
|
|
832
|
+
exactRecord(value, ['kind'], [], path);
|
|
833
|
+
return Object.freeze({ kind: 'unit' });
|
|
834
|
+
}
|
|
835
|
+
if (value.kind === 'opaque') {
|
|
836
|
+
const partition = exactRecord(value, ['kind', 'expression_fingerprint'], [], path);
|
|
837
|
+
if (typeof partition.expression_fingerprint !== 'string' ||
|
|
838
|
+
!SHA256.test(partition.expression_fingerprint)) {
|
|
839
|
+
invalid(`${path}.expression_fingerprint`);
|
|
840
|
+
}
|
|
841
|
+
return Object.freeze({
|
|
842
|
+
kind: 'opaque',
|
|
843
|
+
expression_fingerprint: partition.expression_fingerprint
|
|
844
|
+
});
|
|
845
|
+
}
|
|
846
|
+
invalid(`${path}.kind`);
|
|
847
|
+
}
|
|
848
|
+
function parseCapabilityMutation(value, path) {
|
|
849
|
+
if (!isPlainRecord(value) || typeof value.kind !== 'string')
|
|
850
|
+
invalid(path);
|
|
851
|
+
switch (value.kind) {
|
|
852
|
+
case 'record': {
|
|
853
|
+
const mutation = exactRecord(value, [
|
|
854
|
+
'kind',
|
|
855
|
+
'model',
|
|
856
|
+
'key',
|
|
857
|
+
'fields',
|
|
858
|
+
'replace',
|
|
859
|
+
'upsert',
|
|
860
|
+
'patch',
|
|
861
|
+
'delete'
|
|
862
|
+
], [], path);
|
|
863
|
+
if (typeof mutation.upsert !== 'boolean' ||
|
|
864
|
+
typeof mutation.patch !== 'boolean' ||
|
|
865
|
+
typeof mutation.delete !== 'boolean') {
|
|
866
|
+
invalid(path);
|
|
867
|
+
}
|
|
868
|
+
const key = uniqueNames(mutation.key, `${path}.key`);
|
|
869
|
+
const fields = sortedNames(mutation.fields, `${path}.fields`);
|
|
870
|
+
const replace = sortedNames(mutation.replace, `${path}.replace`);
|
|
871
|
+
if (key.length === 0 ||
|
|
872
|
+
(mutation.patch && fields.length === 0)) {
|
|
873
|
+
invalid(path);
|
|
874
|
+
}
|
|
875
|
+
return Object.freeze({
|
|
876
|
+
kind: 'record',
|
|
877
|
+
model: projectionModel(mutation.model, `${path}.model`),
|
|
878
|
+
key,
|
|
879
|
+
fields,
|
|
880
|
+
replace,
|
|
881
|
+
upsert: mutation.upsert,
|
|
882
|
+
patch: mutation.patch,
|
|
883
|
+
delete: mutation.delete
|
|
884
|
+
});
|
|
885
|
+
}
|
|
886
|
+
case 'relationship': {
|
|
887
|
+
const mutation = exactRecord(value, [
|
|
888
|
+
'kind',
|
|
889
|
+
'relationship',
|
|
890
|
+
'source_model',
|
|
891
|
+
'source_key',
|
|
892
|
+
'target_model',
|
|
893
|
+
'target_key',
|
|
894
|
+
'link',
|
|
895
|
+
'unlink'
|
|
896
|
+
], [], path);
|
|
897
|
+
if (typeof mutation.link !== 'boolean' ||
|
|
898
|
+
typeof mutation.unlink !== 'boolean') {
|
|
899
|
+
invalid(path);
|
|
900
|
+
}
|
|
901
|
+
const sourceKey = uniqueNames(mutation.source_key, `${path}.source_key`);
|
|
902
|
+
const targetKey = uniqueNames(mutation.target_key, `${path}.target_key`);
|
|
903
|
+
if (sourceKey.length === 0 || targetKey.length === 0)
|
|
904
|
+
invalid(path);
|
|
905
|
+
return Object.freeze({
|
|
906
|
+
kind: 'relationship',
|
|
907
|
+
relationship: identityString(mutation.relationship, `${path}.relationship`),
|
|
908
|
+
source_model: projectionModel(mutation.source_model, `${path}.source_model`),
|
|
909
|
+
source_key: sourceKey,
|
|
910
|
+
target_model: projectionModel(mutation.target_model, `${path}.target_model`),
|
|
911
|
+
target_key: targetKey,
|
|
912
|
+
link: mutation.link,
|
|
913
|
+
unlink: mutation.unlink
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
case 'model': {
|
|
917
|
+
const mutation = exactRecord(value, ['kind', 'model'], [], path);
|
|
918
|
+
return Object.freeze({
|
|
919
|
+
kind: 'model',
|
|
920
|
+
model: projectionModel(mutation.model, `${path}.model`)
|
|
921
|
+
});
|
|
922
|
+
}
|
|
923
|
+
default:
|
|
924
|
+
invalid(`${path}.kind`);
|
|
925
|
+
}
|
|
926
|
+
}
|
|
927
|
+
function capabilityMutationKey(mutation) {
|
|
928
|
+
switch (mutation.kind) {
|
|
929
|
+
case 'record':
|
|
930
|
+
return [
|
|
931
|
+
0,
|
|
932
|
+
mutation.model,
|
|
933
|
+
mutation.key,
|
|
934
|
+
mutation.fields,
|
|
935
|
+
mutation.replace,
|
|
936
|
+
mutation.upsert,
|
|
937
|
+
mutation.patch,
|
|
938
|
+
mutation.delete
|
|
939
|
+
];
|
|
940
|
+
case 'relationship':
|
|
941
|
+
return [
|
|
942
|
+
1,
|
|
943
|
+
mutation.relationship,
|
|
944
|
+
mutation.source_model,
|
|
945
|
+
mutation.source_key,
|
|
946
|
+
mutation.target_model,
|
|
947
|
+
mutation.target_key,
|
|
948
|
+
mutation.link,
|
|
949
|
+
mutation.unlink
|
|
950
|
+
];
|
|
951
|
+
case 'model':
|
|
952
|
+
return [2, mutation.model];
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
function sameEventRef(left, right) {
|
|
956
|
+
return (left.id === right.id &&
|
|
957
|
+
left.name === right.name &&
|
|
958
|
+
left.version === right.version);
|
|
959
|
+
}
|
|
960
|
+
function parsePreviewMutation(value, path) {
|
|
961
|
+
if (!isPlainRecord(value) || typeof value.op !== 'string')
|
|
962
|
+
invalid(path);
|
|
963
|
+
switch (value.op) {
|
|
964
|
+
case 'upsert': {
|
|
965
|
+
const mutation = exactRecord(value, ['op', 'scope', 'fields', 'replace'], [], path);
|
|
966
|
+
const fields = parsePreviewFields(mutation.fields, `${path}.fields`);
|
|
967
|
+
const replace = sortedNames(mutation.replace, `${path}.replace`);
|
|
968
|
+
if (fields.some(({ field }) => !replace.includes(field)))
|
|
969
|
+
invalid(path);
|
|
970
|
+
return Object.freeze({
|
|
971
|
+
op: 'upsert',
|
|
972
|
+
scope: parsePreviewScope(mutation.scope, `${path}.scope`),
|
|
973
|
+
fields,
|
|
974
|
+
replace
|
|
975
|
+
});
|
|
976
|
+
}
|
|
977
|
+
case 'patch': {
|
|
978
|
+
const mutation = exactRecord(value, ['op', 'scope', 'if_present'], ['set', 'unset'], path);
|
|
979
|
+
if (mutation.if_present !== true)
|
|
980
|
+
invalid(`${path}.if_present`);
|
|
981
|
+
const set = parsePreviewFields(mutation.set ?? [], `${path}.set`);
|
|
982
|
+
const unset = sortedNames(mutation.unset ?? [], `${path}.unset`);
|
|
983
|
+
if ((set.length === 0 && unset.length === 0) ||
|
|
984
|
+
set.some(({ field }) => unset.includes(field))) {
|
|
985
|
+
invalid(path);
|
|
986
|
+
}
|
|
987
|
+
return Object.freeze({
|
|
988
|
+
op: 'patch',
|
|
989
|
+
scope: parsePreviewScope(mutation.scope, `${path}.scope`),
|
|
990
|
+
set,
|
|
991
|
+
unset,
|
|
992
|
+
if_present: true
|
|
993
|
+
});
|
|
994
|
+
}
|
|
995
|
+
case 'delete': {
|
|
996
|
+
const mutation = exactRecord(value, ['op', 'scope'], [], path);
|
|
997
|
+
return Object.freeze({
|
|
998
|
+
op: 'delete',
|
|
999
|
+
scope: parsePreviewScope(mutation.scope, `${path}.scope`)
|
|
1000
|
+
});
|
|
1001
|
+
}
|
|
1002
|
+
case 'link':
|
|
1003
|
+
case 'unlink': {
|
|
1004
|
+
const mutation = exactRecord(value, ['op', 'relationship', 'source', 'target'], [], path);
|
|
1005
|
+
return Object.freeze({
|
|
1006
|
+
op: value.op,
|
|
1007
|
+
relationship: identityString(mutation.relationship, `${path}.relationship`),
|
|
1008
|
+
source: parsePreviewScope(mutation.source, `${path}.source`),
|
|
1009
|
+
target: parsePreviewScope(mutation.target, `${path}.target`)
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
case 'invalidate_model': {
|
|
1013
|
+
const mutation = exactRecord(value, ['op', 'model'], ['partition'], path);
|
|
1014
|
+
return Object.freeze({
|
|
1015
|
+
op: 'invalidate_model',
|
|
1016
|
+
...(mutation.partition === undefined
|
|
1017
|
+
? {}
|
|
1018
|
+
: {
|
|
1019
|
+
partition: parsePreviewPartition(mutation.partition, `${path}.partition`)
|
|
1020
|
+
}),
|
|
1021
|
+
model: projectionModel(mutation.model, `${path}.model`)
|
|
1022
|
+
});
|
|
1023
|
+
}
|
|
1024
|
+
case 'invalidate_relationship': {
|
|
1025
|
+
const mutation = exactRecord(value, ['op', 'relationship', 'source'], [], path);
|
|
1026
|
+
return Object.freeze({
|
|
1027
|
+
op: 'invalidate_relationship',
|
|
1028
|
+
relationship: identityString(mutation.relationship, `${path}.relationship`),
|
|
1029
|
+
source: parsePreviewScope(mutation.source, `${path}.source`)
|
|
1030
|
+
});
|
|
1031
|
+
}
|
|
1032
|
+
default:
|
|
1033
|
+
invalid(`${path}.op`);
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
function parsePreviewScope(value, path) {
|
|
1037
|
+
const scope = exactRecord(value, ['partition', 'model', 'key'], [], path);
|
|
1038
|
+
const key = boundedArray(scope.key, `${path}.key`).map((item, index) => {
|
|
1039
|
+
const itemPath = `${path}.key[${index}]`;
|
|
1040
|
+
const field = exactRecord(item, ['ordinal', 'field', 'value'], [], itemPath);
|
|
1041
|
+
const ordinal = boundedOrdinal(field.ordinal, `${itemPath}.ordinal`);
|
|
1042
|
+
if (ordinal !== index)
|
|
1043
|
+
invalid(`${itemPath}.ordinal`);
|
|
1044
|
+
return Object.freeze({
|
|
1045
|
+
ordinal,
|
|
1046
|
+
field: identityString(field.field, `${itemPath}.field`),
|
|
1047
|
+
value: parsePreviewValue(field.value, `${itemPath}.value`, 1)
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
if (key.length === 0)
|
|
1051
|
+
invalid(`${path}.key`);
|
|
1052
|
+
assertUnique(key.map(({ field }) => field), `${path}.key`);
|
|
1053
|
+
if (encoder.encode(JSON.stringify(key)).byteLength > MAX_KEY_BYTES) {
|
|
1054
|
+
invalid(`${path}.key`);
|
|
1055
|
+
}
|
|
1056
|
+
const partition = parsePreviewPartition(scope.partition, `${path}.partition`);
|
|
1057
|
+
return Object.freeze({
|
|
1058
|
+
partition,
|
|
1059
|
+
model: projectionModel(scope.model, `${path}.model`),
|
|
1060
|
+
key: Object.freeze(key)
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
function parsePreviewPartition(value, path) {
|
|
1064
|
+
if (!isPlainRecord(value) || typeof value.kind !== 'string')
|
|
1065
|
+
invalid(path);
|
|
1066
|
+
if (value.kind === 'unit') {
|
|
1067
|
+
exactRecord(value, ['kind'], [], path);
|
|
1068
|
+
const parsed = Object.freeze({ kind: 'unit' });
|
|
1069
|
+
if (encoder.encode(JSON.stringify(parsed)).byteLength > MAX_PARTITION_BYTES) {
|
|
1070
|
+
invalid(path);
|
|
1071
|
+
}
|
|
1072
|
+
return parsed;
|
|
1073
|
+
}
|
|
1074
|
+
if (value.kind === 'expression') {
|
|
1075
|
+
const partition = exactRecord(value, ['kind', 'expression', 'requires'], [], path);
|
|
1076
|
+
if (partition.requires !== 'current_cache_partition') {
|
|
1077
|
+
invalid(`${path}.requires`);
|
|
1078
|
+
}
|
|
1079
|
+
const parsed = Object.freeze({
|
|
1080
|
+
kind: 'expression',
|
|
1081
|
+
expression: parsePreviewValue(partition.expression, `${path}.expression`, 1),
|
|
1082
|
+
requires: 'current_cache_partition'
|
|
1083
|
+
});
|
|
1084
|
+
if (encoder.encode(JSON.stringify(parsed)).byteLength > MAX_PARTITION_BYTES) {
|
|
1085
|
+
invalid(path);
|
|
1086
|
+
}
|
|
1087
|
+
return parsed;
|
|
1088
|
+
}
|
|
1089
|
+
invalid(`${path}.kind`);
|
|
1090
|
+
}
|
|
1091
|
+
function parsePreviewValue(value, path, depth) {
|
|
1092
|
+
if (depth > MAX_DEPTH || !isPlainRecord(value) || typeof value.kind !== 'string') {
|
|
1093
|
+
invalid(path);
|
|
1094
|
+
}
|
|
1095
|
+
switch (value.kind) {
|
|
1096
|
+
case 'input':
|
|
1097
|
+
case 'generated_default': {
|
|
1098
|
+
const expression = exactRecord(value, ['kind', 'path'], [], path);
|
|
1099
|
+
return Object.freeze({
|
|
1100
|
+
kind: value.kind,
|
|
1101
|
+
path: stringPath(expression.path, `${path}.path`)
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
case 'trusted_preset': {
|
|
1105
|
+
const expression = exactRecord(value, ['kind', 'name', 'codec'], [], path);
|
|
1106
|
+
return Object.freeze({
|
|
1107
|
+
kind: 'trusted_preset',
|
|
1108
|
+
name: identityString(expression.name, `${path}.name`),
|
|
1109
|
+
codec: identityString(expression.codec, `${path}.codec`)
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
case 'constant': {
|
|
1113
|
+
const expression = exactRecord(value, ['kind', 'value'], [], path);
|
|
1114
|
+
return Object.freeze({
|
|
1115
|
+
kind: 'constant',
|
|
1116
|
+
value: parseValue(expression.value, `${path}.value`, depth)
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
case 'null':
|
|
1120
|
+
exactRecord(value, ['kind'], [], path);
|
|
1121
|
+
return Object.freeze({ kind: 'null' });
|
|
1122
|
+
case 'list': {
|
|
1123
|
+
const expression = exactRecord(value, ['kind', 'values'], [], path);
|
|
1124
|
+
return Object.freeze({
|
|
1125
|
+
kind: 'list',
|
|
1126
|
+
values: Object.freeze(boundedArray(expression.values, `${path}.values`).map((item, index) => parsePreviewValue(item, `${path}.values[${index}]`, depth + 1)))
|
|
1127
|
+
});
|
|
1128
|
+
}
|
|
1129
|
+
case 'object': {
|
|
1130
|
+
const expression = exactRecord(value, ['kind', 'fields'], [], path);
|
|
1131
|
+
const fields = boundedArray(expression.fields, `${path}.fields`).map((item, index) => {
|
|
1132
|
+
const itemPath = `${path}.fields[${index}]`;
|
|
1133
|
+
const field = exactRecord(item, ['name', 'value'], [], itemPath);
|
|
1134
|
+
return Object.freeze({
|
|
1135
|
+
name: identityString(field.name, `${itemPath}.name`),
|
|
1136
|
+
value: parsePreviewValue(field.value, `${itemPath}.value`, depth + 1)
|
|
1137
|
+
});
|
|
1138
|
+
});
|
|
1139
|
+
assertStrictOrder(fields, (left, right) => compareUtf8(left.name, right.name), `${path}.fields`);
|
|
1140
|
+
return Object.freeze({
|
|
1141
|
+
kind: 'object',
|
|
1142
|
+
fields: Object.freeze(fields)
|
|
1143
|
+
});
|
|
1144
|
+
}
|
|
1145
|
+
case 'transform': {
|
|
1146
|
+
const expression = exactRecord(value, ['kind', 'transform', 'arguments'], [], path);
|
|
1147
|
+
if (expression.transform !== 'string_concat' &&
|
|
1148
|
+
expression.transform !== 'first_present') {
|
|
1149
|
+
invalid(`${path}.transform`);
|
|
1150
|
+
}
|
|
1151
|
+
const args = boundedArray(expression.arguments, `${path}.arguments`).map((item, index) => parsePreviewValue(item, `${path}.arguments[${index}]`, depth + 1));
|
|
1152
|
+
if (args.length === 0)
|
|
1153
|
+
invalid(`${path}.arguments`);
|
|
1154
|
+
return Object.freeze({
|
|
1155
|
+
kind: 'transform',
|
|
1156
|
+
transform: expression.transform,
|
|
1157
|
+
arguments: Object.freeze(args)
|
|
1158
|
+
});
|
|
1159
|
+
}
|
|
1160
|
+
default:
|
|
1161
|
+
invalid(`${path}.kind`);
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
function parsePreviewFields(value, path) {
|
|
1165
|
+
const fields = boundedArray(value, path).map((item, index) => {
|
|
1166
|
+
const itemPath = `${path}[${index}]`;
|
|
1167
|
+
const field = exactRecord(item, ['field', 'value'], [], itemPath);
|
|
1168
|
+
return Object.freeze({
|
|
1169
|
+
field: identityString(field.field, `${itemPath}.field`),
|
|
1170
|
+
value: parsePreviewValue(field.value, `${itemPath}.value`, 1)
|
|
1171
|
+
});
|
|
1172
|
+
});
|
|
1173
|
+
assertStrictOrder(fields, (left, right) => compareUtf8(left.field, right.field), path);
|
|
1174
|
+
return Object.freeze(fields);
|
|
1175
|
+
}
|
|
1176
|
+
function parsePreviewRecoveryTarget(value, path) {
|
|
1177
|
+
if (!isPlainRecord(value) || typeof value.kind !== 'string')
|
|
1178
|
+
invalid(path);
|
|
1179
|
+
if (value.kind === 'record') {
|
|
1180
|
+
const target = exactRecord(value, ['kind', 'scope'], [], path);
|
|
1181
|
+
return Object.freeze({
|
|
1182
|
+
kind: 'record',
|
|
1183
|
+
scope: parsePreviewScope(target.scope, `${path}.scope`)
|
|
1184
|
+
});
|
|
1185
|
+
}
|
|
1186
|
+
if (value.kind === 'relationship') {
|
|
1187
|
+
const target = exactRecord(value, ['kind', 'relationship', 'source'], [], path);
|
|
1188
|
+
return Object.freeze({
|
|
1189
|
+
kind: 'relationship',
|
|
1190
|
+
relationship: identityString(target.relationship, `${path}.relationship`),
|
|
1191
|
+
source: parsePreviewScope(target.source, `${path}.source`)
|
|
1192
|
+
});
|
|
1193
|
+
}
|
|
1194
|
+
if (value.kind === 'model') {
|
|
1195
|
+
const target = exactRecord(value, ['kind', 'model'], ['partition'], path);
|
|
1196
|
+
return Object.freeze({
|
|
1197
|
+
kind: 'model',
|
|
1198
|
+
...(target.partition === undefined
|
|
1199
|
+
? {}
|
|
1200
|
+
: {
|
|
1201
|
+
partition: parsePreviewPartition(target.partition, `${path}.partition`)
|
|
1202
|
+
}),
|
|
1203
|
+
model: projectionModel(target.model, `${path}.model`)
|
|
1204
|
+
});
|
|
1205
|
+
}
|
|
1206
|
+
invalid(`${path}.kind`);
|
|
1207
|
+
}
|
|
1208
|
+
function parseEventRef(value, path) {
|
|
1209
|
+
const event = exactRecord(value, ['id', 'name', 'version'], [], path);
|
|
1210
|
+
const version = boundedOrdinal(event.version, `${path}.version`);
|
|
1211
|
+
if (version === 0)
|
|
1212
|
+
invalid(`${path}.version`);
|
|
1213
|
+
return Object.freeze({
|
|
1214
|
+
id: identityString(event.id, `${path}.id`),
|
|
1215
|
+
name: identityString(event.name, `${path}.name`),
|
|
1216
|
+
version
|
|
1217
|
+
});
|
|
1218
|
+
}
|
|
1219
|
+
function projectionRefs(value, projectionCount, path) {
|
|
1220
|
+
const refs = boundedArray(value, path).map((item, index) => boundedOrdinal(item, `${path}[${index}]`));
|
|
1221
|
+
if (refs.length === 0 ||
|
|
1222
|
+
refs.some((reference) => reference >= projectionCount)) {
|
|
1223
|
+
invalid(path);
|
|
1224
|
+
}
|
|
1225
|
+
assertStrictOrder(refs, (left, right) => left - right, path);
|
|
1226
|
+
return Object.freeze(refs);
|
|
1227
|
+
}
|
|
1228
|
+
function previewOperationScopeKey(mutation) {
|
|
1229
|
+
switch (mutation.op) {
|
|
1230
|
+
case 'upsert':
|
|
1231
|
+
case 'patch':
|
|
1232
|
+
case 'delete':
|
|
1233
|
+
return [0, previewScopeKey(mutation.scope)];
|
|
1234
|
+
case 'link':
|
|
1235
|
+
case 'unlink':
|
|
1236
|
+
return [
|
|
1237
|
+
1,
|
|
1238
|
+
mutation.relationship,
|
|
1239
|
+
previewScopeKey(mutation.source),
|
|
1240
|
+
previewScopeKey(mutation.target)
|
|
1241
|
+
];
|
|
1242
|
+
case 'invalidate_model':
|
|
1243
|
+
return [
|
|
1244
|
+
2,
|
|
1245
|
+
mutation.partition === undefined
|
|
1246
|
+
? [0]
|
|
1247
|
+
: [1, previewPartitionKey(mutation.partition)],
|
|
1248
|
+
mutation.model
|
|
1249
|
+
];
|
|
1250
|
+
case 'invalidate_relationship':
|
|
1251
|
+
return [3, mutation.relationship, previewScopeKey(mutation.source)];
|
|
1252
|
+
}
|
|
1253
|
+
}
|
|
1254
|
+
function previewRecoveryTargetKey(target) {
|
|
1255
|
+
switch (target.kind) {
|
|
1256
|
+
case 'record':
|
|
1257
|
+
return [0, previewScopeKey(target.scope)];
|
|
1258
|
+
case 'relationship':
|
|
1259
|
+
return [1, target.relationship, previewScopeKey(target.source)];
|
|
1260
|
+
case 'model':
|
|
1261
|
+
return [
|
|
1262
|
+
2,
|
|
1263
|
+
target.partition === undefined
|
|
1264
|
+
? [0]
|
|
1265
|
+
: [1, previewPartitionKey(target.partition)],
|
|
1266
|
+
target.model
|
|
1267
|
+
];
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
function previewScopeKey(scope) {
|
|
1271
|
+
return [
|
|
1272
|
+
previewPartitionKey(scope.partition),
|
|
1273
|
+
scope.model,
|
|
1274
|
+
scope.key.map((field) => [
|
|
1275
|
+
field.ordinal,
|
|
1276
|
+
field.field,
|
|
1277
|
+
JSON.stringify(field.value)
|
|
1278
|
+
])
|
|
1279
|
+
];
|
|
1280
|
+
}
|
|
1281
|
+
function previewPartitionKey(partition) {
|
|
1282
|
+
return partition.kind === 'unit'
|
|
1283
|
+
? [0]
|
|
1284
|
+
: [1, JSON.stringify(partition.expression), partition.requires];
|
|
1285
|
+
}
|
|
1286
|
+
function operationScopeKey(mutation) {
|
|
1287
|
+
switch (mutation.op) {
|
|
1288
|
+
case 'upsert':
|
|
1289
|
+
case 'patch':
|
|
1290
|
+
case 'delete':
|
|
1291
|
+
return [0, scopeKey(mutation.scope)];
|
|
1292
|
+
case 'link':
|
|
1293
|
+
case 'unlink':
|
|
1294
|
+
return [
|
|
1295
|
+
1,
|
|
1296
|
+
mutation.relationship,
|
|
1297
|
+
scopeKey(mutation.source),
|
|
1298
|
+
scopeKey(mutation.target)
|
|
1299
|
+
];
|
|
1300
|
+
case 'invalidate_model':
|
|
1301
|
+
return [
|
|
1302
|
+
2,
|
|
1303
|
+
mutation.partition === undefined
|
|
1304
|
+
? [0]
|
|
1305
|
+
: [1, partitionKey(mutation.partition)],
|
|
1306
|
+
mutation.model
|
|
1307
|
+
];
|
|
1308
|
+
case 'invalidate_relationship':
|
|
1309
|
+
return [3, mutation.relationship, scopeKey(mutation.source)];
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function recoveryTargetKey(target) {
|
|
1313
|
+
switch (target.kind) {
|
|
1314
|
+
case 'record':
|
|
1315
|
+
return [0, scopeKey(target.scope)];
|
|
1316
|
+
case 'relationship':
|
|
1317
|
+
return [1, target.relationship, scopeKey(target.source)];
|
|
1318
|
+
case 'model':
|
|
1319
|
+
return [
|
|
1320
|
+
2,
|
|
1321
|
+
target.partition === undefined
|
|
1322
|
+
? [0]
|
|
1323
|
+
: [1, partitionKey(target.partition)],
|
|
1324
|
+
target.model
|
|
1325
|
+
];
|
|
1326
|
+
}
|
|
1327
|
+
}
|
|
1328
|
+
function scopeKey(scope) {
|
|
1329
|
+
return [
|
|
1330
|
+
partitionKey(scope.partition),
|
|
1331
|
+
scope.model,
|
|
1332
|
+
scope.key.map((field) => [
|
|
1333
|
+
field.ordinal,
|
|
1334
|
+
field.field,
|
|
1335
|
+
valueKey(field.value)
|
|
1336
|
+
])
|
|
1337
|
+
];
|
|
1338
|
+
}
|
|
1339
|
+
function partitionKey(partition) {
|
|
1340
|
+
return partition.kind === 'unit' ? [0] : [1, partition.token];
|
|
1341
|
+
}
|
|
1342
|
+
function valueKey(value) {
|
|
1343
|
+
switch (value.type) {
|
|
1344
|
+
case 'null':
|
|
1345
|
+
return [0];
|
|
1346
|
+
case 'boolean':
|
|
1347
|
+
return [1, value.value];
|
|
1348
|
+
case 'i64':
|
|
1349
|
+
return [2, value.value];
|
|
1350
|
+
case 'u64':
|
|
1351
|
+
return [3, value.value];
|
|
1352
|
+
case 'f64':
|
|
1353
|
+
return [4, value.value];
|
|
1354
|
+
case 'string':
|
|
1355
|
+
return [5, value.value];
|
|
1356
|
+
case 'enum':
|
|
1357
|
+
return [6, value.value.enum_type, value.value.variant];
|
|
1358
|
+
case 'list':
|
|
1359
|
+
return [7, value.value.map(valueKey)];
|
|
1360
|
+
case 'object':
|
|
1361
|
+
return [
|
|
1362
|
+
8,
|
|
1363
|
+
value.value.map((field) => [field.field, valueKey(field.value)])
|
|
1364
|
+
];
|
|
1365
|
+
}
|
|
1366
|
+
}
|
|
1367
|
+
function compareProjectionIdentity(left, right) {
|
|
1368
|
+
return compareTuple([
|
|
1369
|
+
left.program_id,
|
|
1370
|
+
left.binding_id,
|
|
1371
|
+
left.epoch,
|
|
1372
|
+
left.program_ir_version,
|
|
1373
|
+
left.operation_semantics_version
|
|
1374
|
+
], [
|
|
1375
|
+
right.program_id,
|
|
1376
|
+
right.binding_id,
|
|
1377
|
+
right.epoch,
|
|
1378
|
+
right.program_ir_version,
|
|
1379
|
+
right.operation_semantics_version
|
|
1380
|
+
]);
|
|
1381
|
+
}
|
|
1382
|
+
function compareTuple(left, right) {
|
|
1383
|
+
for (let index = 0; index < Math.min(left.length, right.length); index += 1) {
|
|
1384
|
+
const compared = compareValue(left[index], right[index]);
|
|
1385
|
+
if (compared !== 0)
|
|
1386
|
+
return compared;
|
|
1387
|
+
}
|
|
1388
|
+
return left.length - right.length;
|
|
1389
|
+
}
|
|
1390
|
+
function compareValue(left, right) {
|
|
1391
|
+
if (Array.isArray(left) && Array.isArray(right))
|
|
1392
|
+
return compareTuple(left, right);
|
|
1393
|
+
if (typeof left === 'string' && typeof right === 'string') {
|
|
1394
|
+
return compareUtf8(left, right);
|
|
1395
|
+
}
|
|
1396
|
+
if (typeof left === 'number' && typeof right === 'number')
|
|
1397
|
+
return left - right;
|
|
1398
|
+
if (typeof left === 'boolean' && typeof right === 'boolean') {
|
|
1399
|
+
return Number(left) - Number(right);
|
|
1400
|
+
}
|
|
1401
|
+
return compareUtf8(JSON.stringify(left), JSON.stringify(right));
|
|
1402
|
+
}
|
|
1403
|
+
function compareStringArrays(left, right) {
|
|
1404
|
+
return compareTuple(left, right);
|
|
1405
|
+
}
|
|
1406
|
+
/** Rust `String::cmp` is lexicographic over UTF-8 bytes, not UTF-16 units. */
|
|
1407
|
+
function compareUtf8(left, right) {
|
|
1408
|
+
const leftBytes = encoder.encode(left);
|
|
1409
|
+
const rightBytes = encoder.encode(right);
|
|
1410
|
+
for (let index = 0; index < Math.min(leftBytes.length, rightBytes.length); index += 1) {
|
|
1411
|
+
const compared = leftBytes[index] - rightBytes[index];
|
|
1412
|
+
if (compared !== 0)
|
|
1413
|
+
return compared;
|
|
1414
|
+
}
|
|
1415
|
+
return leftBytes.length - rightBytes.length;
|
|
1416
|
+
}
|
|
1417
|
+
function exactRecord(value, required, optional, path) {
|
|
1418
|
+
if (!isPlainRecord(value))
|
|
1419
|
+
invalid(path);
|
|
1420
|
+
const keys = Reflect.ownKeys(value);
|
|
1421
|
+
if (keys.some((key) => typeof key !== 'string'))
|
|
1422
|
+
invalid(path);
|
|
1423
|
+
const allowed = new Set([...required, ...optional]);
|
|
1424
|
+
for (const key of keys) {
|
|
1425
|
+
if (!allowed.has(key))
|
|
1426
|
+
invalid(`${path}.${key}`);
|
|
1427
|
+
const descriptor = Object.getOwnPropertyDescriptor(value, key);
|
|
1428
|
+
if (descriptor === undefined || !('value' in descriptor)) {
|
|
1429
|
+
invalid(`${path}.${key}`);
|
|
1430
|
+
}
|
|
1431
|
+
}
|
|
1432
|
+
for (const key of required) {
|
|
1433
|
+
if (!Object.prototype.hasOwnProperty.call(value, key)) {
|
|
1434
|
+
invalid(`${path}.${key}`);
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
return value;
|
|
1438
|
+
}
|
|
1439
|
+
/**
|
|
1440
|
+
* Bound hostile already-decoded JSON before cloning/freezing/canonicalizing it.
|
|
1441
|
+
*
|
|
1442
|
+
* The byte counter is a lower bound (UTF-8 strings and property names only), so
|
|
1443
|
+
* it never rejects a wire body that could satisfy the exact final one-MiB
|
|
1444
|
+
* canonical check. The independent node/depth fences bound punctuation-heavy
|
|
1445
|
+
* or deeply nested inputs whose strings alone would stay below that byte limit.
|
|
1446
|
+
*/
|
|
1447
|
+
function preflightProjectionPayload(value, path) {
|
|
1448
|
+
const active = new WeakSet();
|
|
1449
|
+
const work = [{ kind: 'visit', value, depth: 0 }];
|
|
1450
|
+
let scheduledVisits = 1;
|
|
1451
|
+
let stringBytes = 0;
|
|
1452
|
+
const reserveVisits = (count) => {
|
|
1453
|
+
if (count > MAX_TRAVERSAL_NODES - scheduledVisits)
|
|
1454
|
+
invalid(path);
|
|
1455
|
+
scheduledVisits += count;
|
|
1456
|
+
};
|
|
1457
|
+
const consumeString = (candidate) => {
|
|
1458
|
+
stringBytes += utf8ByteLength(candidate, MAX_BODY_BYTES - stringBytes);
|
|
1459
|
+
if (stringBytes > MAX_BODY_BYTES)
|
|
1460
|
+
invalid(path);
|
|
1461
|
+
};
|
|
1462
|
+
const enqueueVisit = (candidate, depth) => {
|
|
1463
|
+
if (depth > MAX_TRAVERSAL_DEPTH)
|
|
1464
|
+
invalid(path);
|
|
1465
|
+
if (typeof candidate === 'string') {
|
|
1466
|
+
consumeString(candidate);
|
|
1467
|
+
}
|
|
1468
|
+
else if (candidate !== null &&
|
|
1469
|
+
(typeof candidate === 'object' || typeof candidate === 'function')) {
|
|
1470
|
+
work.push({ kind: 'visit', value: candidate, depth });
|
|
1471
|
+
}
|
|
1472
|
+
};
|
|
1473
|
+
while (work.length !== 0) {
|
|
1474
|
+
const item = work.pop();
|
|
1475
|
+
if (item.kind === 'leave') {
|
|
1476
|
+
active.delete(item.value);
|
|
1477
|
+
continue;
|
|
1478
|
+
}
|
|
1479
|
+
if (item.depth > MAX_TRAVERSAL_DEPTH) {
|
|
1480
|
+
invalid(path);
|
|
1481
|
+
}
|
|
1482
|
+
const candidate = item.value;
|
|
1483
|
+
if (typeof candidate === 'string') {
|
|
1484
|
+
consumeString(candidate);
|
|
1485
|
+
continue;
|
|
1486
|
+
}
|
|
1487
|
+
if (candidate === null ||
|
|
1488
|
+
(typeof candidate !== 'object' && typeof candidate !== 'function')) {
|
|
1489
|
+
continue;
|
|
1490
|
+
}
|
|
1491
|
+
if (active.has(candidate))
|
|
1492
|
+
invalid(path);
|
|
1493
|
+
active.add(candidate);
|
|
1494
|
+
work.push({ kind: 'leave', value: candidate });
|
|
1495
|
+
if (Array.isArray(candidate)) {
|
|
1496
|
+
reserveVisits(candidate.length);
|
|
1497
|
+
for (let index = candidate.length - 1; index >= 0; index -= 1) {
|
|
1498
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, index);
|
|
1499
|
+
if (descriptor === undefined || !('value' in descriptor))
|
|
1500
|
+
invalid(path);
|
|
1501
|
+
enqueueVisit(descriptor.value, item.depth + 1);
|
|
1502
|
+
}
|
|
1503
|
+
continue;
|
|
1504
|
+
}
|
|
1505
|
+
const keys = Reflect.ownKeys(candidate);
|
|
1506
|
+
reserveVisits(keys.length);
|
|
1507
|
+
for (let index = keys.length - 1; index >= 0; index -= 1) {
|
|
1508
|
+
const key = keys[index];
|
|
1509
|
+
if (typeof key !== 'string')
|
|
1510
|
+
invalid(path);
|
|
1511
|
+
consumeString(key);
|
|
1512
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate, key);
|
|
1513
|
+
if (descriptor === undefined || !('value' in descriptor))
|
|
1514
|
+
invalid(path);
|
|
1515
|
+
enqueueVisit(descriptor.value, item.depth + 1);
|
|
1516
|
+
}
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
function utf8ByteLength(value, remaining) {
|
|
1520
|
+
let bytes = 0;
|
|
1521
|
+
for (let index = 0; index < value.length; index += 1) {
|
|
1522
|
+
const code = value.charCodeAt(index);
|
|
1523
|
+
if (code <= 0x7f)
|
|
1524
|
+
bytes += 1;
|
|
1525
|
+
else if (code <= 0x7ff)
|
|
1526
|
+
bytes += 2;
|
|
1527
|
+
else if (code >= 0xd800 &&
|
|
1528
|
+
code <= 0xdbff &&
|
|
1529
|
+
index + 1 < value.length &&
|
|
1530
|
+
value.charCodeAt(index + 1) >= 0xdc00 &&
|
|
1531
|
+
value.charCodeAt(index + 1) <= 0xdfff) {
|
|
1532
|
+
bytes += 4;
|
|
1533
|
+
index += 1;
|
|
1534
|
+
}
|
|
1535
|
+
else {
|
|
1536
|
+
bytes += 3;
|
|
1537
|
+
}
|
|
1538
|
+
if (bytes > remaining)
|
|
1539
|
+
return bytes;
|
|
1540
|
+
}
|
|
1541
|
+
return bytes;
|
|
1542
|
+
}
|
|
1543
|
+
function boundedArray(value, path) {
|
|
1544
|
+
if (!Array.isArray(value) || value.length > MAX_ITEMS)
|
|
1545
|
+
invalid(path);
|
|
1546
|
+
return value;
|
|
1547
|
+
}
|
|
1548
|
+
function capabilityArray(value, path) {
|
|
1549
|
+
if (!Array.isArray(value) || value.length > MAX_TRAVERSAL_NODES)
|
|
1550
|
+
invalid(path);
|
|
1551
|
+
return value;
|
|
1552
|
+
}
|
|
1553
|
+
function boundedOrdinal(value, path) {
|
|
1554
|
+
if (typeof value !== 'number' ||
|
|
1555
|
+
!Number.isSafeInteger(value) ||
|
|
1556
|
+
value < 0 ||
|
|
1557
|
+
value > 0xffff_ffff) {
|
|
1558
|
+
invalid(path);
|
|
1559
|
+
}
|
|
1560
|
+
return value;
|
|
1561
|
+
}
|
|
1562
|
+
function safeUnsignedInteger(value, path) {
|
|
1563
|
+
if (typeof value !== 'number' ||
|
|
1564
|
+
!Number.isSafeInteger(value) ||
|
|
1565
|
+
value < 0) {
|
|
1566
|
+
invalid(path);
|
|
1567
|
+
}
|
|
1568
|
+
return value;
|
|
1569
|
+
}
|
|
1570
|
+
function identityString(value, path) {
|
|
1571
|
+
if (typeof value !== 'string' ||
|
|
1572
|
+
value.length === 0 ||
|
|
1573
|
+
value.trim() !== value ||
|
|
1574
|
+
encoder.encode(value).byteLength > MAX_IDENTITY_BYTES) {
|
|
1575
|
+
invalid(path);
|
|
1576
|
+
}
|
|
1577
|
+
return value;
|
|
1578
|
+
}
|
|
1579
|
+
function projectionEpoch(value, path) {
|
|
1580
|
+
const epoch = identityString(value, path);
|
|
1581
|
+
if (encoder.encode(epoch).byteLength > 128 ||
|
|
1582
|
+
[...epoch].some((character) => /\p{Cc}/u.test(character))) {
|
|
1583
|
+
invalid(path);
|
|
1584
|
+
}
|
|
1585
|
+
return epoch;
|
|
1586
|
+
}
|
|
1587
|
+
function projectionModel(value, path) {
|
|
1588
|
+
const model = identityString(value, path);
|
|
1589
|
+
if (encoder.encode(model).byteLength > 128 ||
|
|
1590
|
+
[...model].some((character) => /\p{Cc}/u.test(character) || /\p{White_Space}/u.test(character))) {
|
|
1591
|
+
invalid(path);
|
|
1592
|
+
}
|
|
1593
|
+
return model;
|
|
1594
|
+
}
|
|
1595
|
+
function obligationModel(value, path) {
|
|
1596
|
+
if (typeof value !== 'string' ||
|
|
1597
|
+
value.length === 0 ||
|
|
1598
|
+
value.trim() !== value ||
|
|
1599
|
+
encoder.encode(value).byteLength > 255) {
|
|
1600
|
+
invalid(path);
|
|
1601
|
+
}
|
|
1602
|
+
return value;
|
|
1603
|
+
}
|
|
1604
|
+
function protocolToken(value, purpose, path) {
|
|
1605
|
+
const token = identityString(value, path);
|
|
1606
|
+
const prefix = `v1.${purpose}.`;
|
|
1607
|
+
const encoded = token.startsWith(prefix) ? token.slice(prefix.length) : '';
|
|
1608
|
+
if (!/^[A-Za-z0-9_-]{43}$/.test(encoded))
|
|
1609
|
+
invalid(path);
|
|
1610
|
+
try {
|
|
1611
|
+
const standard = `${encoded.replace(/-/g, '+').replace(/_/g, '/')}=`;
|
|
1612
|
+
const decoded = atob(standard);
|
|
1613
|
+
if (decoded.length !== 32)
|
|
1614
|
+
invalid(path);
|
|
1615
|
+
const bytes = Uint8Array.from(decoded, (character) => character.charCodeAt(0));
|
|
1616
|
+
let canonical = '';
|
|
1617
|
+
for (const byte of bytes)
|
|
1618
|
+
canonical += String.fromCharCode(byte);
|
|
1619
|
+
const roundTrip = btoa(canonical)
|
|
1620
|
+
.replace(/\+/g, '-')
|
|
1621
|
+
.replace(/\//g, '_')
|
|
1622
|
+
.replace(/=+$/, '');
|
|
1623
|
+
if (roundTrip !== encoded)
|
|
1624
|
+
invalid(path);
|
|
1625
|
+
}
|
|
1626
|
+
catch {
|
|
1627
|
+
invalid(path);
|
|
1628
|
+
}
|
|
1629
|
+
return token;
|
|
1630
|
+
}
|
|
1631
|
+
function canonicalInteger(value, type, path) {
|
|
1632
|
+
if (typeof value !== 'string' || !/^-?(?:0|[1-9][0-9]*)$/.test(value)) {
|
|
1633
|
+
invalid(path);
|
|
1634
|
+
}
|
|
1635
|
+
let parsed;
|
|
1636
|
+
try {
|
|
1637
|
+
parsed = BigInt(value);
|
|
1638
|
+
}
|
|
1639
|
+
catch {
|
|
1640
|
+
invalid(path);
|
|
1641
|
+
}
|
|
1642
|
+
if (parsed.toString() !== value ||
|
|
1643
|
+
(type === 'i64' && (parsed < I64_MIN || parsed > I64_MAX)) ||
|
|
1644
|
+
(type === 'u64' && (parsed < 0n || parsed > U64_MAX))) {
|
|
1645
|
+
invalid(path);
|
|
1646
|
+
}
|
|
1647
|
+
return value;
|
|
1648
|
+
}
|
|
1649
|
+
function canonicalFloat(value, path) {
|
|
1650
|
+
if (typeof value !== 'string' || value.length === 0)
|
|
1651
|
+
invalid(path);
|
|
1652
|
+
const parsed = Number(value);
|
|
1653
|
+
if (!Number.isFinite(parsed))
|
|
1654
|
+
invalid(path);
|
|
1655
|
+
const canonical = rustRyuFloat(parsed);
|
|
1656
|
+
if (canonical !== value)
|
|
1657
|
+
invalid(path);
|
|
1658
|
+
return value;
|
|
1659
|
+
}
|
|
1660
|
+
/**
|
|
1661
|
+
* ECMAScript and Rust Ryu choose the same shortest round-tripping digits but
|
|
1662
|
+
* use different fixed/scientific notation windows. serde_json's finite f64
|
|
1663
|
+
* representation is fixed for decimal exponents -5..=15 and scientific
|
|
1664
|
+
* outside that range; integral fixed values retain `.0`.
|
|
1665
|
+
*/
|
|
1666
|
+
function rustRyuFloat(value) {
|
|
1667
|
+
if (Object.is(value, -0) || value === 0)
|
|
1668
|
+
return '0.0';
|
|
1669
|
+
const scientific = value.toExponential();
|
|
1670
|
+
const match = /^(-?)([0-9])(?:\.([0-9]+))?e([+-][0-9]+)$/.exec(scientific);
|
|
1671
|
+
if (match === null)
|
|
1672
|
+
throw new TypeError('invalid finite float');
|
|
1673
|
+
const sign = match[1];
|
|
1674
|
+
const digits = `${match[2]}${match[3] ?? ''}`;
|
|
1675
|
+
const exponent = Number(match[4]);
|
|
1676
|
+
if (exponent < -5 || exponent > 15) {
|
|
1677
|
+
const fraction = digits.slice(1);
|
|
1678
|
+
return `${sign}${digits[0]}${fraction.length === 0 ? '' : `.${fraction}`}e${exponent >= 0 ? '+' : ''}${exponent}`;
|
|
1679
|
+
}
|
|
1680
|
+
const point = exponent + 1;
|
|
1681
|
+
if (point <= 0) {
|
|
1682
|
+
return `${sign}0.${'0'.repeat(-point)}${digits}`;
|
|
1683
|
+
}
|
|
1684
|
+
if (point >= digits.length) {
|
|
1685
|
+
return `${sign}${digits}${'0'.repeat(point - digits.length)}.0`;
|
|
1686
|
+
}
|
|
1687
|
+
return `${sign}${digits.slice(0, point)}.${digits.slice(point)}`;
|
|
1688
|
+
}
|
|
1689
|
+
function sortedNames(value, path) {
|
|
1690
|
+
const names = boundedArray(value, path).map((item, index) => identityString(item, `${path}[${index}]`));
|
|
1691
|
+
assertStrictOrder(names, compareUtf8, path);
|
|
1692
|
+
return Object.freeze(names);
|
|
1693
|
+
}
|
|
1694
|
+
function uniqueNames(value, path) {
|
|
1695
|
+
const names = boundedArray(value, path).map((item, index) => identityString(item, `${path}[${index}]`));
|
|
1696
|
+
assertUnique(names, path);
|
|
1697
|
+
return Object.freeze(names);
|
|
1698
|
+
}
|
|
1699
|
+
function stringPath(value, path) {
|
|
1700
|
+
const values = boundedArray(value, path).map((item, index) => identityString(item, `${path}[${index}]`));
|
|
1701
|
+
if (values.length === 0)
|
|
1702
|
+
invalid(path);
|
|
1703
|
+
return Object.freeze(values);
|
|
1704
|
+
}
|
|
1705
|
+
function assertUnique(values, path) {
|
|
1706
|
+
if (new Set(values).size !== values.length)
|
|
1707
|
+
invalid(path);
|
|
1708
|
+
}
|
|
1709
|
+
function assertStrictOrder(values, compare, path) {
|
|
1710
|
+
for (let index = 1; index < values.length; index += 1) {
|
|
1711
|
+
if (compare(values[index - 1], values[index]) >= 0)
|
|
1712
|
+
invalid(path);
|
|
1713
|
+
}
|
|
1714
|
+
}
|
|
1715
|
+
function invalid(path) {
|
|
1716
|
+
throw new ProjectionDeltaValidationError(path);
|
|
1717
|
+
}
|
|
1718
|
+
export { canonicalCommandProjectionMetadata, canonicalProjectionDelta };
|