@hops-ops/distributed 0.0.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +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 +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { receiptMismatch
|
|
1
|
+
import { receiptMismatch } from './util.js';
|
|
2
2
|
export function verifyReplicaCommandReceipt(prepared, receipt) {
|
|
3
3
|
if (receipt.commandId !== prepared.commandId) {
|
|
4
4
|
receiptMismatch('receipt.commandId');
|
|
@@ -6,31 +6,45 @@ export function verifyReplicaCommandReceipt(prepared, receipt) {
|
|
|
6
6
|
if (receipt.consistency !== prepared.consistency) {
|
|
7
7
|
receiptMismatch('receipt.consistency');
|
|
8
8
|
}
|
|
9
|
-
|
|
10
|
-
|
|
9
|
+
if (receipt.projectionDisposition === 'revalidate') {
|
|
10
|
+
if (prepared.projection === undefined &&
|
|
11
|
+
!prepared.revalidation.required) {
|
|
12
|
+
receiptMismatch('receipt.projectionDisposition');
|
|
13
|
+
}
|
|
11
14
|
return Object.freeze({
|
|
12
|
-
kind: '
|
|
13
|
-
|
|
14
|
-
|
|
15
|
+
kind: receipt.state === 'succeeded_pending_projection'
|
|
16
|
+
? 'deferred'
|
|
17
|
+
: 'matched',
|
|
18
|
+
revalidate: true
|
|
15
19
|
});
|
|
16
20
|
}
|
|
17
|
-
const expected = contract?.kind === 'finite'
|
|
18
|
-
? contract.expected.map(({ projector, model }) => ({
|
|
19
|
-
projection: projector,
|
|
20
|
-
model
|
|
21
|
-
}))
|
|
22
|
-
: [];
|
|
23
21
|
if (receipt.state === 'in_progress' && receipt.expects.length === 0) {
|
|
24
22
|
return Object.freeze({
|
|
25
23
|
kind: 'deferred',
|
|
26
|
-
revalidate: prepared.revalidation.required
|
|
24
|
+
revalidate: prepared.revalidation.required ||
|
|
25
|
+
prepared.projection?.revalidate === true
|
|
27
26
|
});
|
|
28
27
|
}
|
|
29
|
-
if (
|
|
30
|
-
|
|
28
|
+
if (prepared.projection === undefined) {
|
|
29
|
+
if (receipt.projection !== undefined || receipt.expects.length !== 0) {
|
|
30
|
+
receiptMismatch('receipt.expects');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
else if (receipt.projection === undefined) {
|
|
34
|
+
/*
|
|
35
|
+
* Ship contract (see server routes: Atomic never persists eventual
|
|
36
|
+
* modeled projection metadata):
|
|
37
|
+
* - Eventual: response carries projection-delta (+ expects) when modeled.
|
|
38
|
+
* - Atomic: response carries typed row + direct `records`; optional
|
|
39
|
+
* `.applies` previews use the same IR but seal via the row, not a delta.
|
|
40
|
+
*/
|
|
41
|
+
if (prepared.consistency !== 'atomic') {
|
|
42
|
+
receiptMismatch('receipt.expects');
|
|
43
|
+
}
|
|
31
44
|
}
|
|
32
45
|
return Object.freeze({
|
|
33
46
|
kind: receipt.state === 'in_progress' ? 'deferred' : 'matched',
|
|
34
|
-
revalidate: prepared.revalidation.required
|
|
47
|
+
revalidate: prepared.revalidation.required ||
|
|
48
|
+
prepared.projection?.revalidate === true
|
|
35
49
|
});
|
|
36
50
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { DistributedCommandConsistency, DistributedTrustedPreset, DistributedTrustedPresetCodec } from '../../protocol.js';
|
|
2
2
|
import type { ReplicaClientSurface, ReplicaValue } from '../types.js';
|
|
3
|
+
import type { PreparedCommandProjection, PreparedProjectionOperation, ReplicaCommandProjection } from '../projection-delta/index.js';
|
|
3
4
|
export type ReplicaCommandScalarCodec = DistributedTrustedPresetCodec;
|
|
4
5
|
/** Static name/codec declaration emitted without a server-derived value. */
|
|
5
6
|
export type ReplicaTrustedPresetDescriptor = {
|
|
@@ -127,14 +128,14 @@ export type ReplicaCommandRevalidationPlan = {
|
|
|
127
128
|
readonly relationships: readonly ReplicaCommandEffectRelationship[];
|
|
128
129
|
};
|
|
129
130
|
/**
|
|
130
|
-
* Framework-neutral executable command descriptor emitted by `
|
|
131
|
+
* Framework-neutral executable command descriptor emitted by `distributed client`.
|
|
131
132
|
*
|
|
132
133
|
* The compiler has already validated the Rust-owned declaration. Runtime
|
|
133
134
|
* validation remains fail-closed so a stale or hand-edited artifact cannot
|
|
134
135
|
* create optimistic cache truth.
|
|
135
136
|
*/
|
|
136
137
|
export type ReplicaCommandArtifact<TInput = void, TOutput = unknown> = {
|
|
137
|
-
readonly version:
|
|
138
|
+
readonly version: 2;
|
|
138
139
|
readonly name: string;
|
|
139
140
|
readonly mutationField: string;
|
|
140
141
|
readonly document: string;
|
|
@@ -152,8 +153,11 @@ export type ReplicaCommandArtifact<TInput = void, TOutput = unknown> = {
|
|
|
152
153
|
readonly output: ReplicaCommandShape;
|
|
153
154
|
readonly inputDefaults?: ReplicaCommandInputDefaults;
|
|
154
155
|
readonly consistency: DistributedCommandConsistency;
|
|
155
|
-
|
|
156
|
-
readonly
|
|
156
|
+
/** Compiler-owned projection contract. Absence means no invented delta. */
|
|
157
|
+
readonly projection?: ReplicaCommandProjection;
|
|
158
|
+
/** Artifact v1 effects and confirmations are deliberately not decoded. */
|
|
159
|
+
readonly effects?: never;
|
|
160
|
+
readonly confirmations?: never;
|
|
157
161
|
readonly directProjection?: ReplicaCommandDirectProjection;
|
|
158
162
|
readonly trustedPresets?: readonly ReplicaTrustedPresetDescriptor[];
|
|
159
163
|
readonly revalidation: ReplicaCommandRevalidationPlan;
|
|
@@ -225,9 +229,10 @@ export type ReplicaPreparedCommand<TInput = void, TOutput = unknown> = {
|
|
|
225
229
|
};
|
|
226
230
|
readonly optimistic: {
|
|
227
231
|
readonly version: 1;
|
|
228
|
-
readonly operations: readonly
|
|
232
|
+
readonly operations: readonly PreparedProjectionOperation[];
|
|
229
233
|
readonly fallback: 'revalidate';
|
|
230
234
|
};
|
|
235
|
+
readonly projection?: PreparedCommandProjection;
|
|
231
236
|
readonly confirmations?: ReplicaPreparedConfirmations;
|
|
232
237
|
readonly directProjection?: {
|
|
233
238
|
readonly topology: ReplicaCommandDirectProjection['topology'];
|
|
@@ -7,7 +7,11 @@ export declare function collectTrustedPresetReferences<TInput, TOutput>(artifact
|
|
|
7
7
|
export declare function validateShape(shape: ReplicaCommandShape, path: string, depth: number, active: Set<ReplicaCommandTypeDefinition>): void;
|
|
8
8
|
export declare function validateDefinition(definition: ReplicaCommandTypeDefinition, path: string, depth: number, active: Set<ReplicaCommandTypeDefinition>): void;
|
|
9
9
|
export declare function validateDefaults(shape: ReplicaCommandShape, defaults: ReplicaCommandInputDefaults | undefined): void;
|
|
10
|
-
export declare function validateConfirmations<TInput, TOutput>(artifact:
|
|
10
|
+
export declare function validateConfirmations<TInput, TOutput>(artifact: Readonly<{
|
|
11
|
+
confirmations?: import('./types.js').ReplicaCommandConfirmations;
|
|
12
|
+
consistency: ReplicaCommandArtifact<TInput, TOutput>['consistency'];
|
|
13
|
+
input: ReplicaCommandArtifact<TInput, TOutput>['input'];
|
|
14
|
+
}>): void;
|
|
11
15
|
export declare function validateDirectProjection<TInput, TOutput>(artifact: ReplicaCommandArtifact<TInput, TOutput>): void;
|
|
12
16
|
export declare function validateRevalidation<TInput, TOutput>(artifact: ReplicaCommandArtifact<TInput, TOutput>): void;
|
|
13
17
|
export declare function validateEffectArtifact(effect: ReplicaCommandEffect, input: ReplicaCommandShape, path: string): void;
|
|
@@ -3,9 +3,14 @@ import { GRAPHQL_NAME, MAX_TYPE_DEPTH } from './constants.js';
|
|
|
3
3
|
import { artifactInvalid, hash, isSupportedCodec, nonempty, requiredString } from './util.js';
|
|
4
4
|
import { parseReplicaTrustedPresetDescriptors, trustedPresetName } from './presets.js';
|
|
5
5
|
import { fieldAtPath } from './resolve.js';
|
|
6
|
+
import { ProjectionDeltaValidationError, validateCommandProjectionArtifact } from '../projection-delta/index.js';
|
|
6
7
|
export function validateArtifact(artifact, allowTrustedPresets) {
|
|
7
|
-
if (artifact.version !==
|
|
8
|
+
if (artifact.version !== 2)
|
|
8
9
|
artifactInvalid('artifact.version');
|
|
10
|
+
if (Object.prototype.hasOwnProperty.call(artifact, 'effects') ||
|
|
11
|
+
Object.prototype.hasOwnProperty.call(artifact, 'confirmations')) {
|
|
12
|
+
artifactInvalid('artifact.version');
|
|
13
|
+
}
|
|
9
14
|
nonempty(artifact.name, 'artifact.name');
|
|
10
15
|
if (!GRAPHQL_NAME.test(artifact.mutationField)) {
|
|
11
16
|
artifactInvalid('artifact.mutationField');
|
|
@@ -37,23 +42,22 @@ export function validateArtifact(artifact, allowTrustedPresets) {
|
|
|
37
42
|
if (artifact.output.kind === 'none')
|
|
38
43
|
artifactInvalid('artifact.output.kind');
|
|
39
44
|
validateDefaults(artifact.input, artifact.inputDefaults);
|
|
40
|
-
if (artifact.consistency !== '
|
|
41
|
-
artifact.consistency !== '
|
|
42
|
-
artifact.consistency !== '
|
|
45
|
+
if (artifact.consistency !== 'succeeded' &&
|
|
46
|
+
artifact.consistency !== 'eventual' &&
|
|
47
|
+
artifact.consistency !== 'atomic') {
|
|
43
48
|
artifactInvalid('artifact.consistency');
|
|
44
49
|
}
|
|
45
|
-
if (artifact.
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
50
|
+
if (artifact.projection !== undefined) {
|
|
51
|
+
try {
|
|
52
|
+
validateProjectionCapabilities(validateCommandProjectionArtifact(artifact.projection), artifact);
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
if (error instanceof ProjectionDeltaValidationError) {
|
|
56
|
+
artifactInvalid(error.path);
|
|
57
|
+
}
|
|
58
|
+
throw error;
|
|
59
|
+
}
|
|
55
60
|
}
|
|
56
|
-
validateConfirmations(artifact);
|
|
57
61
|
validateDirectProjection(artifact);
|
|
58
62
|
validateRevalidation(artifact);
|
|
59
63
|
validateTrustedPresetReferences(artifact, trustedPresets, allowTrustedPresets);
|
|
@@ -66,16 +70,33 @@ export function validateClientSurface(surface, path) {
|
|
|
66
70
|
if (surface.kind === 'role')
|
|
67
71
|
return;
|
|
68
72
|
if (surface.kind !== 'application' ||
|
|
69
|
-
!Array.isArray(surface.
|
|
70
|
-
surface.
|
|
73
|
+
!Array.isArray(surface.eligible_roles) ||
|
|
74
|
+
!Array.isArray(surface.schema_roles) ||
|
|
75
|
+
surface.eligible_roles.length === 0 ||
|
|
76
|
+
surface.schema_roles.length === 0) {
|
|
71
77
|
artifactInvalid(path);
|
|
72
78
|
}
|
|
73
|
-
const
|
|
74
|
-
for (let index = 0; index < surface.
|
|
75
|
-
const role = requiredString(surface.
|
|
76
|
-
if (
|
|
77
|
-
artifactInvalid(`${path}.
|
|
78
|
-
|
|
79
|
+
const eligibleRoles = new Set();
|
|
80
|
+
for (let index = 0; index < surface.eligible_roles.length; index += 1) {
|
|
81
|
+
const role = requiredString(surface.eligible_roles[index], `${path}.eligible_roles[${index}]`);
|
|
82
|
+
if (eligibleRoles.has(role))
|
|
83
|
+
artifactInvalid(`${path}.eligible_roles[${index}]`);
|
|
84
|
+
if (index > 0 && surface.eligible_roles[index - 1] >= role) {
|
|
85
|
+
artifactInvalid(`${path}.eligible_roles[${index}]`);
|
|
86
|
+
}
|
|
87
|
+
eligibleRoles.add(role);
|
|
88
|
+
}
|
|
89
|
+
const schemaRoles = new Set();
|
|
90
|
+
for (let index = 0; index < surface.schema_roles.length; index += 1) {
|
|
91
|
+
const role = requiredString(surface.schema_roles[index], `${path}.schema_roles[${index}]`);
|
|
92
|
+
if (schemaRoles.has(role))
|
|
93
|
+
artifactInvalid(`${path}.schema_roles[${index}]`);
|
|
94
|
+
if (index > 0 && surface.schema_roles[index - 1] >= role) {
|
|
95
|
+
artifactInvalid(`${path}.schema_roles[${index}]`);
|
|
96
|
+
}
|
|
97
|
+
if (!eligibleRoles.has(role))
|
|
98
|
+
artifactInvalid(`${path}.schema_roles[${index}]`);
|
|
99
|
+
schemaRoles.add(role);
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
export function validateTrustedPresetReferences(artifact, descriptors, allowTrustedPresets) {
|
|
@@ -102,52 +123,99 @@ export function collectTrustedPresetReferences(artifact) {
|
|
|
102
123
|
out.push(Object.freeze({ name: value.name, path }));
|
|
103
124
|
}
|
|
104
125
|
};
|
|
105
|
-
|
|
106
|
-
for (let index = 0; index <
|
|
107
|
-
|
|
126
|
+
if (artifact.projection !== undefined) {
|
|
127
|
+
for (let index = 0; index < artifact.projection.preview.operations.length; index += 1) {
|
|
128
|
+
const mutation = artifact.projection.preview.operations[index].mutation;
|
|
129
|
+
collectPreviewMutationPresets(mutation, `artifact.projection.preview.operations[${index}].mutation`, out);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
if (artifact.directProjection?.partition !== undefined) {
|
|
133
|
+
expression(artifact.directProjection.partition, 'artifact.directProjection.partition');
|
|
134
|
+
}
|
|
135
|
+
return Object.freeze(out);
|
|
136
|
+
}
|
|
137
|
+
function collectPreviewMutationPresets(mutation, path, out) {
|
|
138
|
+
const value = (expression, valuePath) => {
|
|
139
|
+
switch (expression.kind) {
|
|
140
|
+
case 'trusted_preset':
|
|
141
|
+
out.push(Object.freeze({ name: expression.name, path: valuePath }));
|
|
142
|
+
break;
|
|
143
|
+
case 'list':
|
|
144
|
+
expression.values.forEach((item, index) => value(item, `${valuePath}.values[${index}]`));
|
|
145
|
+
break;
|
|
146
|
+
case 'object':
|
|
147
|
+
expression.fields.forEach((field, index) => value(field.value, `${valuePath}.fields[${index}].value`));
|
|
148
|
+
break;
|
|
149
|
+
case 'transform':
|
|
150
|
+
expression.arguments.forEach((item, index) => value(item, `${valuePath}.arguments[${index}]`));
|
|
151
|
+
break;
|
|
152
|
+
default:
|
|
153
|
+
break;
|
|
108
154
|
}
|
|
109
155
|
};
|
|
110
|
-
const
|
|
111
|
-
|
|
156
|
+
const scope = (candidate, scopePath) => {
|
|
157
|
+
if (candidate.partition.kind === 'expression') {
|
|
158
|
+
value(candidate.partition.expression, `${scopePath}.partition.expression`);
|
|
159
|
+
}
|
|
160
|
+
candidate.key.forEach((field, index) => value(field.value, `${scopePath}.key[${index}].value`));
|
|
112
161
|
};
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
162
|
+
switch (mutation.op) {
|
|
163
|
+
case 'upsert':
|
|
164
|
+
case 'patch':
|
|
165
|
+
scope(mutation.scope, `${path}.scope`);
|
|
166
|
+
(mutation.op === 'upsert' ? mutation.fields : mutation.set).forEach((field, index) => value(field.value, `${path}.fields[${index}].value`));
|
|
167
|
+
break;
|
|
168
|
+
case 'delete':
|
|
169
|
+
scope(mutation.scope, `${path}.scope`);
|
|
170
|
+
break;
|
|
171
|
+
case 'link':
|
|
172
|
+
case 'unlink':
|
|
173
|
+
scope(mutation.source, `${path}.source`);
|
|
174
|
+
scope(mutation.target, `${path}.target`);
|
|
175
|
+
break;
|
|
176
|
+
case 'invalidate_relationship':
|
|
177
|
+
scope(mutation.source, `${path}.source`);
|
|
178
|
+
break;
|
|
179
|
+
case 'invalidate_model':
|
|
180
|
+
if (mutation.partition?.kind === 'expression') {
|
|
181
|
+
value(mutation.partition.expression, `${path}.partition.expression`);
|
|
182
|
+
}
|
|
183
|
+
break;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function validateProjectionCapabilities(projection, artifact) {
|
|
187
|
+
const models = new Set(artifact.revalidation.models);
|
|
188
|
+
const relationships = new Set(artifact.revalidation.relationships.map((item) => `${item.sourceModel}\0${item.field}\0${item.targetModel}`));
|
|
189
|
+
const requireModel = (model, path) => {
|
|
190
|
+
if (!models.has(model))
|
|
191
|
+
artifactInvalid(path);
|
|
192
|
+
};
|
|
193
|
+
for (let index = 0; index < projection.preview.operations.length; index += 1) {
|
|
194
|
+
const mutation = projection.preview.operations[index].mutation;
|
|
195
|
+
const path = `artifact.projection.preview.operations[${index}].mutation`;
|
|
196
|
+
switch (mutation.op) {
|
|
117
197
|
case 'upsert':
|
|
118
198
|
case 'patch':
|
|
119
|
-
key(effect.key, `${path}.key`);
|
|
120
|
-
fields(effect.fields, `${path}.fields`);
|
|
121
|
-
break;
|
|
122
199
|
case 'delete':
|
|
123
|
-
|
|
200
|
+
requireModel(mutation.scope.model, `${path}.scope.model`);
|
|
124
201
|
break;
|
|
125
202
|
case 'link':
|
|
126
203
|
case 'unlink':
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
key(effect.source, `${path}.source`);
|
|
204
|
+
requireModel(mutation.source.model, `${path}.source.model`);
|
|
205
|
+
requireModel(mutation.target.model, `${path}.target.model`);
|
|
206
|
+
if (!relationships.has(`${mutation.source.model}\0${mutation.relationship}\0${mutation.target.model}`))
|
|
207
|
+
artifactInvalid(`${path}.relationship`);
|
|
132
208
|
break;
|
|
133
209
|
case 'invalidate_model':
|
|
210
|
+
requireModel(mutation.model, `${path}.model`);
|
|
211
|
+
break;
|
|
212
|
+
case 'invalidate_relationship':
|
|
213
|
+
requireModel(mutation.source.model, `${path}.source.model`);
|
|
214
|
+
if (![...relationships].some((relationship) => relationship.startsWith(`${mutation.source.model}\0${mutation.relationship}\0`)))
|
|
215
|
+
artifactInvalid(`${path}.relationship`);
|
|
134
216
|
break;
|
|
135
217
|
}
|
|
136
218
|
}
|
|
137
|
-
if (artifact.confirmations?.kind === 'finite') {
|
|
138
|
-
for (let index = 0; index < artifact.confirmations.expected.length; index += 1) {
|
|
139
|
-
const confirmation = artifact.confirmations.expected[index];
|
|
140
|
-
const path = `artifact.confirmations.expected[${index}]`;
|
|
141
|
-
key(confirmation.key, `${path}.key`);
|
|
142
|
-
if (confirmation.partition !== undefined) {
|
|
143
|
-
expression(confirmation.partition, `${path}.partition`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
if (artifact.directProjection?.partition !== undefined) {
|
|
148
|
-
expression(artifact.directProjection.partition, 'artifact.directProjection.partition');
|
|
149
|
-
}
|
|
150
|
-
return Object.freeze(out);
|
|
151
219
|
}
|
|
152
220
|
export function validateShape(shape, path, depth, active) {
|
|
153
221
|
if (depth > MAX_TYPE_DEPTH)
|
|
@@ -236,7 +304,7 @@ export function validateDefaults(shape, defaults) {
|
|
|
236
304
|
export function validateConfirmations(artifact) {
|
|
237
305
|
const confirmations = artifact.confirmations;
|
|
238
306
|
if (confirmations === undefined) {
|
|
239
|
-
if (artifact.consistency === '
|
|
307
|
+
if (artifact.consistency === 'eventual') {
|
|
240
308
|
artifactInvalid('artifact.confirmations');
|
|
241
309
|
}
|
|
242
310
|
return;
|
|
@@ -259,7 +327,7 @@ export function validateConfirmations(artifact) {
|
|
|
259
327
|
else {
|
|
260
328
|
artifactInvalid('artifact.confirmations.kind');
|
|
261
329
|
}
|
|
262
|
-
if (artifact.consistency === '
|
|
330
|
+
if (artifact.consistency === 'atomic') {
|
|
263
331
|
artifactInvalid('artifact.confirmations');
|
|
264
332
|
}
|
|
265
333
|
if (confirmations.kind === 'finite') {
|
|
@@ -277,10 +345,10 @@ export function validateConfirmations(artifact) {
|
|
|
277
345
|
}
|
|
278
346
|
export function validateDirectProjection(artifact) {
|
|
279
347
|
const direct = artifact.directProjection;
|
|
280
|
-
if (artifact.consistency === '
|
|
348
|
+
if (artifact.consistency === 'atomic' && direct === undefined) {
|
|
281
349
|
artifactInvalid('artifact.directProjection');
|
|
282
350
|
}
|
|
283
|
-
if (artifact.consistency !== '
|
|
351
|
+
if (artifact.consistency !== 'atomic' && direct !== undefined) {
|
|
284
352
|
artifactInvalid('artifact.directProjection');
|
|
285
353
|
}
|
|
286
354
|
if (direct === undefined)
|
|
@@ -338,9 +406,8 @@ export function validateRevalidation(artifact) {
|
|
|
338
406
|
for (const [index, relationship] of plan.relationships.entries()) {
|
|
339
407
|
validateRelationshipArtifact(relationship, `artifact.revalidation.relationships[${index}]`);
|
|
340
408
|
}
|
|
341
|
-
if ((artifact.
|
|
342
|
-
|
|
343
|
-
artifact.confirmations?.kind !== 'finite')) &&
|
|
409
|
+
if (((artifact.projection?.preview.operations.length ?? 0) === 0 &&
|
|
410
|
+
artifact.consistency !== 'atomic') &&
|
|
344
411
|
!plan.required) {
|
|
345
412
|
artifactInvalid('artifact.revalidation.required');
|
|
346
413
|
}
|
|
@@ -29,43 +29,47 @@ export function inspectReplicaOperationArtifact(artifact) {
|
|
|
29
29
|
});
|
|
30
30
|
}
|
|
31
31
|
export function inspectReplicaCommandArtifact(artifact) {
|
|
32
|
-
const effects = artifact.
|
|
32
|
+
const effects = (artifact.projection?.preview.operations ?? []).map(({ mutation: effect }) => {
|
|
33
33
|
const models = new Set();
|
|
34
34
|
const fields = new Set();
|
|
35
35
|
const sources = new Set();
|
|
36
|
-
if ('
|
|
36
|
+
if (effect.op === 'invalidate_model')
|
|
37
37
|
models.add(effect.model);
|
|
38
|
-
if ('
|
|
39
|
-
models.add(effect.
|
|
40
|
-
|
|
41
|
-
fields.add(effect.relationship.field);
|
|
42
|
-
}
|
|
43
|
-
if ('key' in effect) {
|
|
44
|
-
for (const field of effect.key.fields) {
|
|
38
|
+
if ('scope' in effect) {
|
|
39
|
+
models.add(effect.scope.model);
|
|
40
|
+
for (const field of effect.scope.key) {
|
|
45
41
|
fields.add(field.field);
|
|
46
|
-
|
|
42
|
+
collectPreviewValueSources(field.value, sources);
|
|
47
43
|
}
|
|
48
44
|
}
|
|
49
45
|
if ('source' in effect) {
|
|
50
|
-
|
|
46
|
+
models.add(effect.source.model);
|
|
47
|
+
for (const field of effect.source.key) {
|
|
51
48
|
fields.add(field.field);
|
|
52
|
-
|
|
49
|
+
collectPreviewValueSources(field.value, sources);
|
|
53
50
|
}
|
|
54
51
|
}
|
|
55
52
|
if ('target' in effect) {
|
|
56
|
-
|
|
53
|
+
models.add(effect.target.model);
|
|
54
|
+
for (const field of effect.target.key) {
|
|
57
55
|
fields.add(field.field);
|
|
58
|
-
|
|
56
|
+
collectPreviewValueSources(field.value, sources);
|
|
59
57
|
}
|
|
60
58
|
}
|
|
61
59
|
if ('fields' in effect) {
|
|
62
60
|
for (const field of effect.fields) {
|
|
63
61
|
fields.add(field.field);
|
|
64
|
-
|
|
62
|
+
collectPreviewValueSources(field.value, sources);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
if ('set' in effect) {
|
|
66
|
+
for (const field of effect.set) {
|
|
67
|
+
fields.add(field.field);
|
|
68
|
+
collectPreviewValueSources(field.value, sources);
|
|
65
69
|
}
|
|
66
70
|
}
|
|
67
71
|
return Object.freeze({
|
|
68
|
-
kind: effect.
|
|
72
|
+
kind: effect.op,
|
|
69
73
|
models: Object.freeze([...models].sort()),
|
|
70
74
|
fields: Object.freeze([...fields].sort()),
|
|
71
75
|
valueSources: Object.freeze([...sources].sort())
|
|
@@ -84,6 +88,37 @@ export function inspectReplicaCommandArtifact(artifact) {
|
|
|
84
88
|
})
|
|
85
89
|
});
|
|
86
90
|
}
|
|
91
|
+
function collectPreviewValueSources(value, out) {
|
|
92
|
+
switch (value.kind) {
|
|
93
|
+
case 'input':
|
|
94
|
+
case 'generated_default':
|
|
95
|
+
out.add('input');
|
|
96
|
+
return;
|
|
97
|
+
case 'trusted_preset':
|
|
98
|
+
out.add('trusted_preset');
|
|
99
|
+
return;
|
|
100
|
+
case 'constant':
|
|
101
|
+
out.add('constant');
|
|
102
|
+
return;
|
|
103
|
+
case 'null':
|
|
104
|
+
out.add('null');
|
|
105
|
+
return;
|
|
106
|
+
case 'list':
|
|
107
|
+
for (const item of value.values)
|
|
108
|
+
collectPreviewValueSources(item, out);
|
|
109
|
+
return;
|
|
110
|
+
case 'object':
|
|
111
|
+
for (const field of value.fields) {
|
|
112
|
+
collectPreviewValueSources(field.value, out);
|
|
113
|
+
}
|
|
114
|
+
return;
|
|
115
|
+
case 'transform':
|
|
116
|
+
for (const argument of value.arguments) {
|
|
117
|
+
collectPreviewValueSources(argument, out);
|
|
118
|
+
}
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
87
122
|
function inspectRoot(root, path, injected, dependencies, indexes) {
|
|
88
123
|
for (const dependency of root.dependencies)
|
|
89
124
|
dependencies.add(dependency);
|
|
@@ -165,6 +200,3 @@ function inspectSelection(selection, path, injected, dependencies, indexes) {
|
|
|
165
200
|
inspectSelection(member.selection, memberPath, injected, dependencies, indexes);
|
|
166
201
|
}
|
|
167
202
|
}
|
|
168
|
-
function expressionSource(expression) {
|
|
169
|
-
return expression.kind;
|
|
170
|
-
}
|
|
@@ -99,8 +99,8 @@ export type ReplicaDiagnosticReceiptExpectationInput = Readonly<{
|
|
|
99
99
|
}>;
|
|
100
100
|
export type ReplicaDiagnosticReceiptInput = Readonly<{
|
|
101
101
|
commandId: string;
|
|
102
|
-
state: 'optimistic' | '
|
|
103
|
-
consistency?: '
|
|
102
|
+
state: 'optimistic' | 'succeeded' | 'succeeded_pending_projection';
|
|
103
|
+
consistency?: 'succeeded' | 'eventual' | 'atomic';
|
|
104
104
|
expectations: readonly ReplicaDiagnosticReceiptExpectationInput[];
|
|
105
105
|
}>;
|
|
106
106
|
/** Engine-independent state contract implemented by DistributedReplica. */
|
|
@@ -138,7 +138,7 @@ export type ReplicaDiagnosticEventInput = Readonly<{
|
|
|
138
138
|
}> | Readonly<{
|
|
139
139
|
kind: 'receipt';
|
|
140
140
|
command: string;
|
|
141
|
-
state: 'optimistic' | '
|
|
141
|
+
state: 'optimistic' | 'succeeded' | 'succeeded_pending_projection' | 'atomic' | 'rejected';
|
|
142
142
|
obligations: number;
|
|
143
143
|
observed: number;
|
|
144
144
|
}> | Readonly<{
|
|
@@ -152,7 +152,7 @@ export type ReplicaDiagnosticEventInput = Readonly<{
|
|
|
152
152
|
}> | Readonly<{
|
|
153
153
|
kind: 'hydration';
|
|
154
154
|
action: 'accepted' | 'rejected';
|
|
155
|
-
reason: 'accepted' | 'invalid' | 'scope-mismatch' | 'artifact-mismatch' | 'active-scope-mismatch' | 'metadata-mismatch';
|
|
155
|
+
reason: 'accepted' | 'same-scope-merge' | 'invalid' | 'scope-mismatch' | 'artifact-mismatch' | 'active-scope-mismatch' | 'metadata-mismatch';
|
|
156
156
|
}> | Readonly<{
|
|
157
157
|
kind: 'scope';
|
|
158
158
|
action: 'established' | 'changed' | 'invalidated';
|
|
@@ -200,8 +200,8 @@ export type ReplicaDiagnosticLayer = Readonly<{
|
|
|
200
200
|
}>;
|
|
201
201
|
export type ReplicaDiagnosticReceipt = Readonly<{
|
|
202
202
|
commandId: string;
|
|
203
|
-
state: 'optimistic' | '
|
|
204
|
-
consistency?: '
|
|
203
|
+
state: 'optimistic' | 'succeeded' | 'succeeded_pending_projection';
|
|
204
|
+
consistency?: 'succeeded' | 'eventual' | 'atomic';
|
|
205
205
|
expectations: readonly ReplicaDiagnosticReceiptExpectationInput[];
|
|
206
206
|
}>;
|
|
207
207
|
export type ReplicaArtifactSourceLocation = Readonly<{
|
|
@@ -246,7 +246,7 @@ export type ReplicaCommandArtifactInspection = Readonly<{
|
|
|
246
246
|
kind: 'command';
|
|
247
247
|
name: string;
|
|
248
248
|
operation: string;
|
|
249
|
-
consistency: '
|
|
249
|
+
consistency: 'succeeded' | 'eventual' | 'atomic';
|
|
250
250
|
effects: readonly ReplicaCommandEffectInspection[];
|
|
251
251
|
revalidation: Readonly<{
|
|
252
252
|
required: boolean;
|
|
@@ -272,7 +272,10 @@ export function replicaClientRequestExtensions(artifact) {
|
|
|
272
272
|
: Object.freeze({
|
|
273
273
|
kind: 'application',
|
|
274
274
|
name: protocol.surface.name,
|
|
275
|
-
|
|
275
|
+
eligible_roles: Object.freeze([
|
|
276
|
+
...protocol.surface.eligible_roles
|
|
277
|
+
]),
|
|
278
|
+
schema_roles: Object.freeze([...protocol.surface.schema_roles])
|
|
276
279
|
});
|
|
277
280
|
return Object.freeze({
|
|
278
281
|
extensions: Object.freeze({
|
|
@@ -364,14 +367,25 @@ export function validatedSurfaceIdentity(value) {
|
|
|
364
367
|
return JSON.stringify(['role', value.name]);
|
|
365
368
|
}
|
|
366
369
|
if (value.kind !== 'application' ||
|
|
367
|
-
!Array.isArray(value.
|
|
368
|
-
value.
|
|
369
|
-
value.
|
|
370
|
-
|
|
371
|
-
|
|
370
|
+
!Array.isArray(value.eligible_roles) ||
|
|
371
|
+
!Array.isArray(value.schema_roles) ||
|
|
372
|
+
value.eligible_roles.length === 0 ||
|
|
373
|
+
value.schema_roles.length === 0 ||
|
|
374
|
+
value.eligible_roles.some((role) => typeof role !== 'string' || role.length === 0) ||
|
|
375
|
+
value.schema_roles.some((role) => typeof role !== 'string' || role.length === 0) ||
|
|
376
|
+
new Set(value.eligible_roles).size !== value.eligible_roles.length ||
|
|
377
|
+
new Set(value.schema_roles).size !== value.schema_roles.length ||
|
|
378
|
+
[...value.eligible_roles].sort().some((role, index) => role !== value.eligible_roles[index]) ||
|
|
379
|
+
[...value.schema_roles].sort().some((role, index) => role !== value.schema_roles[index]) ||
|
|
380
|
+
value.schema_roles.some((role) => !value.eligible_roles.includes(role))) {
|
|
372
381
|
throw new TypeError('replica artifact client surface is invalid');
|
|
373
382
|
}
|
|
374
|
-
return JSON.stringify([
|
|
383
|
+
return JSON.stringify([
|
|
384
|
+
'application',
|
|
385
|
+
value.name,
|
|
386
|
+
value.eligible_roles,
|
|
387
|
+
value.schema_roles
|
|
388
|
+
]);
|
|
375
389
|
}
|
|
376
390
|
export function snapshotFrom(materialized, state) {
|
|
377
391
|
const status = state.errors.length > 0
|
|
@@ -434,7 +448,7 @@ export function graphqlError(error) {
|
|
|
434
448
|
});
|
|
435
449
|
}
|
|
436
450
|
export function assertWriteSource(source) {
|
|
437
|
-
if (!['network', 'live', 'ssr', 'restore', '
|
|
451
|
+
if (!['network', 'live', 'ssr', 'restore', 'atomic'].includes(source)) {
|
|
438
452
|
throw new TypeError(`unsupported replica write source: ${source}`);
|
|
439
453
|
}
|
|
440
454
|
}
|
|
@@ -45,13 +45,19 @@ export function serializeOperationProtocolState(state) {
|
|
|
45
45
|
}
|
|
46
46
|
export function parseAuthoritativeScope(value) {
|
|
47
47
|
try {
|
|
48
|
-
const scope = hydrationRecord(value, 'authoritativeScope', [
|
|
48
|
+
const scope = hydrationRecord(value, 'authoritativeScope', [
|
|
49
|
+
'protocolVersion',
|
|
50
|
+
'schemaHash',
|
|
51
|
+
'authorizationGeneration',
|
|
52
|
+
'cacheScope'
|
|
53
|
+
]);
|
|
49
54
|
if (scope.protocolVersion !== 1) {
|
|
50
55
|
hydrationInvalid('authoritativeScope.protocolVersion');
|
|
51
56
|
}
|
|
52
57
|
return Object.freeze({
|
|
53
58
|
protocolVersion: 1,
|
|
54
59
|
schemaHash: hydrationString(scope.schemaHash, 'authoritativeScope.schemaHash'),
|
|
60
|
+
authorizationGeneration: hydrationString(scope.authorizationGeneration, 'authoritativeScope.authorizationGeneration'),
|
|
55
61
|
cacheScope: hydrationOpaque(scope.cacheScope, 'authoritativeScope.cacheScope')
|
|
56
62
|
});
|
|
57
63
|
}
|
|
@@ -133,13 +139,19 @@ export function parseReplicaHydration(value) {
|
|
|
133
139
|
const state = hydrationRecord(value, 'state', ['version', 'scope', 'payload']);
|
|
134
140
|
if (state.version !== 1)
|
|
135
141
|
hydrationInvalid('state.version');
|
|
136
|
-
const scopeValue = hydrationRecord(state.scope, 'state.scope', [
|
|
142
|
+
const scopeValue = hydrationRecord(state.scope, 'state.scope', [
|
|
143
|
+
'protocolVersion',
|
|
144
|
+
'schemaHash',
|
|
145
|
+
'authorizationGeneration',
|
|
146
|
+
'cacheScope'
|
|
147
|
+
]);
|
|
137
148
|
if (scopeValue.protocolVersion !== 1) {
|
|
138
149
|
hydrationInvalid('state.scope.protocolVersion');
|
|
139
150
|
}
|
|
140
151
|
const scope = Object.freeze({
|
|
141
152
|
protocolVersion: 1,
|
|
142
153
|
schemaHash: hydrationString(scopeValue.schemaHash, 'state.scope.schemaHash'),
|
|
154
|
+
authorizationGeneration: hydrationString(scopeValue.authorizationGeneration, 'state.scope.authorizationGeneration'),
|
|
143
155
|
cacheScope: hydrationOpaque(scopeValue.cacheScope, 'state.scope.cacheScope')
|
|
144
156
|
});
|
|
145
157
|
const payload = hydrationRecord(state.payload, 'state.payload', [
|