@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
|
@@ -1,8 +1,185 @@
|
|
|
1
1
|
import { parseGraphqlResponseExtensions } from '../../protocol.js';
|
|
2
2
|
import { matchReplicaTrustedPresetInventory, prepareReplicaCommandWithTrustedPresets, verifyReplicaCommandReceipt } from '../commands.js';
|
|
3
|
-
import { applyOptimisticEffects, attachAuthorityAbort, callerProjectedPromise, cloneScope, commandOutput, commandStatusArtifact, commandStatusRequest, commandSurfaceContract, commandTransportRequest, confirmDirectProjection, defineBoundCommand, dispatchPrepared, failTrackedProjection, freezeCommandTree, graphqlCommandRejection, linkAbortSignals, monitorPendingProjection, normalizeInventory, normalizeRetries, pendingProjection, preparedSemanticChanges, requireCommandEnvelope, requireCommandRejectionEnvelope, requireStatusEnvelope, sameScope, settleProjectionFailure, settleProjectionSuccess, settleTrackedProjection, validateStatusProgression, waitForCommandOperation } from './lib/index.js';
|
|
3
|
+
import { applyOptimisticEffects, attachAuthorityAbort, callerProjectedPromise, cloneScope, commandOutput, commandStatusArtifact, commandStatusRequest, commandSurfaceContract, commandTransportRequest, confirmDirectProjection, defineBoundCommand, dispatchPrepared, failTrackedProjection, freezeCommandTree, graphqlCommandRejection, linkAbortSignals, monitorPendingProjection, normalizeInventory, normalizeRetries, pendingProjection, preparedDispatchKeys, preparedSemanticChanges, requireCommandEnvelope, requireCommandRejectionEnvelope, requireStatusEnvelope, sameScope, settleProjectionFailure, settleProjectionSuccess, settleTrackedProjection, validateStatusProgression, waitForCommandOperation } from './lib/index.js';
|
|
4
4
|
import { ReplicaCommandRuntimeError } from './errors.js';
|
|
5
|
-
import { replicaCommandAuthority, replicaCommandProjectedLifecycle, replicaResultObservation } from './symbols.js';
|
|
5
|
+
import { replicaCommandAuthority, replicaCommandProjectionDelta, replicaCommandProjectedLifecycle, replicaCommandReadRecord, replicaResultObservation } from './symbols.js';
|
|
6
|
+
import { operationsFromProjectionDelta, validateProjectionMetadataAuthority } from '../projection-delta/index.js';
|
|
7
|
+
function assertActualProjectionCapabilities(contract, delta) {
|
|
8
|
+
for (let ordinal = 0; ordinal < delta.occurrences.length; ordinal += 1) {
|
|
9
|
+
const references = new Set();
|
|
10
|
+
for (const item of [...delta.operations, ...delta.recoveries]) {
|
|
11
|
+
if (item.occurrence_ordinal !== ordinal)
|
|
12
|
+
continue;
|
|
13
|
+
for (const reference of item.projection_refs)
|
|
14
|
+
references.add(reference);
|
|
15
|
+
}
|
|
16
|
+
let selectedEvent;
|
|
17
|
+
for (const reference of references) {
|
|
18
|
+
const operations = delta.operations.filter((item) => item.occurrence_ordinal === ordinal &&
|
|
19
|
+
item.projection_refs.includes(reference));
|
|
20
|
+
const recoveries = delta.recoveries.filter((item) => item.occurrence_ordinal === ordinal &&
|
|
21
|
+
item.projection_refs.includes(reference));
|
|
22
|
+
const candidates = contract.capabilities.arms.filter((arm) => arm.projection_ref === reference &&
|
|
23
|
+
operations.every(({ mutation }) => capabilityAllowsMutation(arm, mutation)) &&
|
|
24
|
+
recoveries.every(({ target }) => capabilityAllowsRecovery(arm, target)));
|
|
25
|
+
if (candidates.length !== 1) {
|
|
26
|
+
throw new Error('actual projection delta does not identify one generated event arm');
|
|
27
|
+
}
|
|
28
|
+
const event = candidates[0].event;
|
|
29
|
+
if (selectedEvent !== undefined &&
|
|
30
|
+
(event.id !== selectedEvent.id ||
|
|
31
|
+
event.name !== selectedEvent.name ||
|
|
32
|
+
event.version !== selectedEvent.version)) {
|
|
33
|
+
throw new Error('actual projection refs disagree on their generated event arm');
|
|
34
|
+
}
|
|
35
|
+
selectedEvent = event;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
function capabilityAllowsMutation(arm, mutation) {
|
|
40
|
+
switch (mutation.op) {
|
|
41
|
+
case 'upsert': {
|
|
42
|
+
const capabilities = recordCapabilities(arm, mutation.scope);
|
|
43
|
+
const replace = capabilities[0]?.replace;
|
|
44
|
+
return (capabilities.some(({ upsert }) => upsert) &&
|
|
45
|
+
replace !== undefined &&
|
|
46
|
+
capabilities.every((capability) => sameStrings(capability.replace, replace)) &&
|
|
47
|
+
sameStrings(mutation.replace, replace) &&
|
|
48
|
+
sameStrings(mutation.fields.map(({ field }) => field), replace) &&
|
|
49
|
+
sameStrings(unionNames(capabilities.flatMap(({ fields }) => fields)), replace));
|
|
50
|
+
}
|
|
51
|
+
case 'patch': {
|
|
52
|
+
const capabilities = recordCapabilities(arm, mutation.scope);
|
|
53
|
+
const allowed = new Set(capabilities
|
|
54
|
+
.filter(({ patch }) => patch)
|
|
55
|
+
.flatMap(({ fields }) => fields));
|
|
56
|
+
return (allowed.size !== 0 &&
|
|
57
|
+
[...mutation.set.map(({ field }) => field), ...mutation.unset].every((field) => allowed.has(field)));
|
|
58
|
+
}
|
|
59
|
+
case 'delete':
|
|
60
|
+
return recordCapabilities(arm, mutation.scope).some(({ delete: allowed }) => allowed);
|
|
61
|
+
case 'link':
|
|
62
|
+
case 'unlink':
|
|
63
|
+
return arm.mutations.some((capability) => capability.kind === 'relationship' &&
|
|
64
|
+
capability.relationship === mutation.relationship &&
|
|
65
|
+
(mutation.op === 'link' ? capability.link : capability.unlink) &&
|
|
66
|
+
capabilityAllowsScope(arm, mutation.source, capability.source_model, capability.source_key) &&
|
|
67
|
+
capabilityAllowsScope(arm, mutation.target, capability.target_model, capability.target_key));
|
|
68
|
+
case 'invalidate_model':
|
|
69
|
+
return (capabilityAllowsPartition(arm, mutation.partition) &&
|
|
70
|
+
arm.mutations.some((capability) => capability.kind === 'model' &&
|
|
71
|
+
capability.model === mutation.model));
|
|
72
|
+
case 'invalidate_relationship':
|
|
73
|
+
return arm.mutations.some((capability) => capability.kind === 'relationship' &&
|
|
74
|
+
capability.relationship === mutation.relationship &&
|
|
75
|
+
capabilityAllowsScope(arm, mutation.source, capability.source_model, capability.source_key));
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
function capabilityAllowsRecovery(arm, target) {
|
|
79
|
+
switch (target.kind) {
|
|
80
|
+
case 'record':
|
|
81
|
+
return recordCapabilities(arm, target.scope).length !== 0;
|
|
82
|
+
case 'relationship':
|
|
83
|
+
return arm.mutations.some((capability) => capability.kind === 'relationship' &&
|
|
84
|
+
capability.relationship === target.relationship &&
|
|
85
|
+
capabilityAllowsScope(arm, target.source, capability.source_model, capability.source_key));
|
|
86
|
+
case 'model':
|
|
87
|
+
return (capabilityAllowsPartition(arm, target.partition) &&
|
|
88
|
+
arm.mutations.some((capability) => capability.kind === 'model' &&
|
|
89
|
+
capability.model === target.model));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function recordCapabilities(arm, scope) {
|
|
93
|
+
if (!capabilityAllowsPartition(arm, scope.partition))
|
|
94
|
+
return [];
|
|
95
|
+
return arm.mutations.filter((capability) => capability.kind === 'record' &&
|
|
96
|
+
capability.model === scope.model &&
|
|
97
|
+
sameStrings(capability.key, scope.key.map(({ field }) => field)));
|
|
98
|
+
}
|
|
99
|
+
function capabilityAllowsScope(arm, scope, model, key) {
|
|
100
|
+
return (capabilityAllowsPartition(arm, scope.partition) &&
|
|
101
|
+
scope.model === model &&
|
|
102
|
+
sameStrings(key, scope.key.map(({ field }) => field)));
|
|
103
|
+
}
|
|
104
|
+
function capabilityAllowsPartition(arm, partition) {
|
|
105
|
+
return arm.partition.kind === 'unit'
|
|
106
|
+
? partition?.kind === 'unit'
|
|
107
|
+
: partition === undefined || partition.kind === 'opaque';
|
|
108
|
+
}
|
|
109
|
+
function unionNames(values) {
|
|
110
|
+
return Object.freeze([...new Set(values)].sort(compareCodeUnits));
|
|
111
|
+
}
|
|
112
|
+
function compareCodeUnits(left, right) {
|
|
113
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
114
|
+
}
|
|
115
|
+
function sameStrings(left, right) {
|
|
116
|
+
return (left.length === right.length &&
|
|
117
|
+
left.every((value, index) => value === right[index]));
|
|
118
|
+
}
|
|
119
|
+
function actualProjectionRevalidation(base, delta) {
|
|
120
|
+
const models = new Set(base.models);
|
|
121
|
+
const relationships = new Map(base.relationships.map((relationship) => [
|
|
122
|
+
JSON.stringify([
|
|
123
|
+
relationship.sourceModel,
|
|
124
|
+
relationship.field,
|
|
125
|
+
relationship.targetModel
|
|
126
|
+
]),
|
|
127
|
+
relationship
|
|
128
|
+
]));
|
|
129
|
+
const addRelationship = (sourceModel, field, targetModel) => {
|
|
130
|
+
const relationship = Object.freeze({ sourceModel, field, targetModel });
|
|
131
|
+
relationships.set(JSON.stringify([sourceModel, field, targetModel]), relationship);
|
|
132
|
+
};
|
|
133
|
+
for (const { mutation } of delta.operations) {
|
|
134
|
+
switch (mutation.op) {
|
|
135
|
+
case 'upsert':
|
|
136
|
+
case 'patch':
|
|
137
|
+
case 'delete':
|
|
138
|
+
models.add(mutation.scope.model);
|
|
139
|
+
break;
|
|
140
|
+
case 'link':
|
|
141
|
+
case 'unlink':
|
|
142
|
+
models.add(mutation.source.model);
|
|
143
|
+
models.add(mutation.target.model);
|
|
144
|
+
addRelationship(mutation.source.model, mutation.relationship, mutation.target.model);
|
|
145
|
+
break;
|
|
146
|
+
case 'invalidate_model':
|
|
147
|
+
models.add(mutation.model);
|
|
148
|
+
break;
|
|
149
|
+
case 'invalidate_relationship':
|
|
150
|
+
models.add(mutation.source.model);
|
|
151
|
+
break;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
for (const { target } of delta.recoveries) {
|
|
155
|
+
switch (target.kind) {
|
|
156
|
+
case 'record':
|
|
157
|
+
models.add(target.scope.model);
|
|
158
|
+
break;
|
|
159
|
+
case 'relationship':
|
|
160
|
+
models.add(target.source.model);
|
|
161
|
+
break;
|
|
162
|
+
case 'model':
|
|
163
|
+
models.add(target.model);
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
return Object.freeze({
|
|
168
|
+
version: 1,
|
|
169
|
+
required: true,
|
|
170
|
+
dependencies: Object.freeze([...base.dependencies]),
|
|
171
|
+
models: Object.freeze([...models].sort(compareCodeUnits)),
|
|
172
|
+
relationships: Object.freeze([...relationships.values()].sort((left, right) => compareCodeUnits(JSON.stringify([
|
|
173
|
+
left.sourceModel,
|
|
174
|
+
left.field,
|
|
175
|
+
left.targetModel
|
|
176
|
+
]), JSON.stringify([
|
|
177
|
+
right.sourceModel,
|
|
178
|
+
right.field,
|
|
179
|
+
right.targetModel
|
|
180
|
+
]))))
|
|
181
|
+
});
|
|
182
|
+
}
|
|
6
183
|
export function createReplicaCommandRuntime(replica, transport, entries, options = {}) {
|
|
7
184
|
const inventory = normalizeInventory(entries);
|
|
8
185
|
if (options.diagnostics !== undefined) {
|
|
@@ -26,14 +203,145 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
26
203
|
throw new TypeError('generated command status artifact requires transport.status');
|
|
27
204
|
}
|
|
28
205
|
if (replicaRevalidate === undefined &&
|
|
29
|
-
inventory.some(({ artifact }) => artifact.
|
|
30
|
-
|
|
206
|
+
inventory.some(({ artifact }) => artifact.projection !== undefined ||
|
|
207
|
+
artifact.revalidation.required)) {
|
|
208
|
+
throw new TypeError('generated modeled projection or required revalidation plan requires replica.revalidate');
|
|
31
209
|
}
|
|
32
210
|
const registration = replica[replicaCommandAuthority]?.(contract);
|
|
33
211
|
const pending = new Map();
|
|
212
|
+
const projectionReplays = new Map();
|
|
213
|
+
const projectionRevalidations = new Map();
|
|
214
|
+
const dispatchTails = new Map();
|
|
34
215
|
const unmanagedLayers = new Set();
|
|
35
216
|
const runtimeAbort = new AbortController();
|
|
36
217
|
let disposed = false;
|
|
218
|
+
const reserveDispatch = (prepared) => {
|
|
219
|
+
const keys = preparedDispatchKeys(prepared);
|
|
220
|
+
if (keys.length === 0) {
|
|
221
|
+
return Object.freeze({
|
|
222
|
+
wait: Promise.resolve(),
|
|
223
|
+
release() { }
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
const predecessors = keys.flatMap((key) => {
|
|
227
|
+
const predecessor = dispatchTails.get(key);
|
|
228
|
+
return predecessor === undefined ? [] : [predecessor];
|
|
229
|
+
});
|
|
230
|
+
let resolve;
|
|
231
|
+
const tail = new Promise((settled) => {
|
|
232
|
+
resolve = settled;
|
|
233
|
+
});
|
|
234
|
+
for (const key of keys)
|
|
235
|
+
dispatchTails.set(key, tail);
|
|
236
|
+
let released = false;
|
|
237
|
+
return Object.freeze({
|
|
238
|
+
wait: Promise.all(predecessors).then(() => undefined),
|
|
239
|
+
release() {
|
|
240
|
+
if (released)
|
|
241
|
+
return;
|
|
242
|
+
released = true;
|
|
243
|
+
for (const key of keys) {
|
|
244
|
+
if (dispatchTails.get(key) === tail)
|
|
245
|
+
dispatchTails.delete(key);
|
|
246
|
+
}
|
|
247
|
+
resolve();
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
};
|
|
251
|
+
const validateActualProjection = (prepared, metadata, authority) => {
|
|
252
|
+
if (prepared.projection === undefined) {
|
|
253
|
+
if (metadata.projection !== undefined || metadata.expects.length !== 0) {
|
|
254
|
+
throw new Error('unmodeled command returned a projection delta');
|
|
255
|
+
}
|
|
256
|
+
return Object.freeze({ requiresRevalidation: false });
|
|
257
|
+
}
|
|
258
|
+
const actual = metadata.projection;
|
|
259
|
+
if (actual === undefined) {
|
|
260
|
+
throw new Error('modeled command omitted its projection delta');
|
|
261
|
+
}
|
|
262
|
+
const canonical = validateProjectionMetadataAuthority(actual, prepared.projection.contract, {
|
|
263
|
+
surface: prepared.transport.protocol.surface,
|
|
264
|
+
schemaHash: prepared.transport.protocol.schemaHash,
|
|
265
|
+
protocolHash: prepared.transport.protocol.protocolHash,
|
|
266
|
+
authorizationGeneration: authority.scope.authorizationGeneration,
|
|
267
|
+
cacheScope: authority.scope.cacheScope,
|
|
268
|
+
causationId: metadata.causationId
|
|
269
|
+
});
|
|
270
|
+
const expected = actual.obligations.map((obligation) => ({
|
|
271
|
+
projection: actual.delta.projections[obligation.projectionRef].program_id,
|
|
272
|
+
model: obligation.model,
|
|
273
|
+
scopeToken: obligation.scopeToken
|
|
274
|
+
}));
|
|
275
|
+
if (expected.length !== metadata.expects.length ||
|
|
276
|
+
expected.some((item, index) => {
|
|
277
|
+
const candidate = metadata.expects[index];
|
|
278
|
+
return (candidate === undefined ||
|
|
279
|
+
candidate.projection !== item.projection ||
|
|
280
|
+
candidate.model !== item.model ||
|
|
281
|
+
candidate.scopeToken !== item.scopeToken);
|
|
282
|
+
})) {
|
|
283
|
+
throw new Error('projection obligations do not match command expectations');
|
|
284
|
+
}
|
|
285
|
+
const previous = projectionReplays.get(prepared.commandId);
|
|
286
|
+
if (previous !== undefined) {
|
|
287
|
+
if (previous !== canonical) {
|
|
288
|
+
throw new Error('projection delta changed during command replay');
|
|
289
|
+
}
|
|
290
|
+
return Object.freeze({
|
|
291
|
+
requiresRevalidation: actual.revalidate || actual.obligations.length === 0
|
|
292
|
+
});
|
|
293
|
+
}
|
|
294
|
+
assertActualProjectionCapabilities(prepared.projection.contract, actual.delta);
|
|
295
|
+
const operations = operationsFromProjectionDelta(actual.delta.operations);
|
|
296
|
+
const seam = replica[replicaCommandProjectionDelta];
|
|
297
|
+
if (seam === undefined) {
|
|
298
|
+
throw new Error('replica does not implement projection delta replacement');
|
|
299
|
+
}
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
canonical,
|
|
302
|
+
operations,
|
|
303
|
+
revalidation: actual.revalidate || actual.obligations.length === 0
|
|
304
|
+
? actualProjectionRevalidation(prepared.revalidation, actual.delta)
|
|
305
|
+
: undefined,
|
|
306
|
+
requiresRevalidation: actual.revalidate || actual.obligations.length === 0
|
|
307
|
+
});
|
|
308
|
+
};
|
|
309
|
+
const commitActualProjection = (prepared, validated) => {
|
|
310
|
+
if (validated.canonical === undefined ||
|
|
311
|
+
validated.operations === undefined) {
|
|
312
|
+
return validated.requiresRevalidation;
|
|
313
|
+
}
|
|
314
|
+
const seam = replica[replicaCommandProjectionDelta];
|
|
315
|
+
const actualPrepared = Object.freeze({
|
|
316
|
+
...prepared,
|
|
317
|
+
optimistic: Object.freeze({
|
|
318
|
+
version: 1,
|
|
319
|
+
operations: validated.operations,
|
|
320
|
+
fallback: 'revalidate'
|
|
321
|
+
})
|
|
322
|
+
});
|
|
323
|
+
seam.call(replica, prepared.commandId, (writer) => applyOptimisticEffects(writer, validated.operations), preparedSemanticChanges(actualPrepared));
|
|
324
|
+
projectionReplays.set(prepared.commandId, validated.canonical);
|
|
325
|
+
if (validated.revalidation !== undefined) {
|
|
326
|
+
projectionRevalidations.set(prepared.commandId, validated.revalidation);
|
|
327
|
+
}
|
|
328
|
+
return validated.requiresRevalidation;
|
|
329
|
+
};
|
|
330
|
+
const applyActualProjection = (prepared, metadata, authority) => commitActualProjection(prepared, validateActualProjection(prepared, metadata, authority));
|
|
331
|
+
const validateProjectionForState = (prepared, metadata, authority) => {
|
|
332
|
+
switch (metadata.state) {
|
|
333
|
+
case 'succeeded':
|
|
334
|
+
case 'succeeded_pending_projection':
|
|
335
|
+
case 'atomic':
|
|
336
|
+
return validateActualProjection(prepared, metadata, authority);
|
|
337
|
+
case 'in_progress':
|
|
338
|
+
case 'rejected':
|
|
339
|
+
case 'projection_failed':
|
|
340
|
+
case 'expired':
|
|
341
|
+
case 'unknown':
|
|
342
|
+
return Object.freeze({ requiresRevalidation: false });
|
|
343
|
+
}
|
|
344
|
+
};
|
|
37
345
|
const rejectUnmanagedLayer = (commandId) => {
|
|
38
346
|
unmanagedLayers.delete(commandId);
|
|
39
347
|
return replica.rejectOptimisticLayer(commandId);
|
|
@@ -87,7 +395,8 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
87
395
|
runtimeAbort.signal
|
|
88
396
|
]);
|
|
89
397
|
try {
|
|
90
|
-
await waitForCommandOperation(replicaRevalidate(prepared.
|
|
398
|
+
await waitForCommandOperation(replicaRevalidate(projectionRevalidations.get(prepared.commandId) ??
|
|
399
|
+
prepared.revalidation), revalidationSignals.signal);
|
|
91
400
|
return true;
|
|
92
401
|
}
|
|
93
402
|
catch (error) {
|
|
@@ -119,6 +428,29 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
119
428
|
})
|
|
120
429
|
.catch(() => undefined);
|
|
121
430
|
};
|
|
431
|
+
const revalidateDispositionAndRetireInBackground = (controller) => {
|
|
432
|
+
void revalidate(controller.prepared, controller.authority.signal)
|
|
433
|
+
.then((refreshed) => {
|
|
434
|
+
if (refreshed &&
|
|
435
|
+
!controller.settled &&
|
|
436
|
+
stillCurrent(controller.authority) &&
|
|
437
|
+
pending.get(controller.commandId) === controller) {
|
|
438
|
+
retireLayerAfterAuthoritativeRevalidation(controller.commandId);
|
|
439
|
+
settleProjectionSuccess(controller);
|
|
440
|
+
pending.delete(controller.commandId);
|
|
441
|
+
}
|
|
442
|
+
})
|
|
443
|
+
.catch(() => undefined);
|
|
444
|
+
};
|
|
445
|
+
const commitStatusProgression = (tracker, status) => {
|
|
446
|
+
tracker.state = status.state;
|
|
447
|
+
if (status.metadata !== undefined) {
|
|
448
|
+
tracker.metadata = status.metadata;
|
|
449
|
+
if (tracker.pending !== undefined) {
|
|
450
|
+
tracker.pending.metadata = status.metadata;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
122
454
|
const statusReader = (prepared, authority, tracker) => {
|
|
123
455
|
const read = async () => {
|
|
124
456
|
if (disposed) {
|
|
@@ -155,21 +487,40 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
155
487
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_SCOPE_INVALIDATED', { commandId: prepared.commandId });
|
|
156
488
|
}
|
|
157
489
|
let status;
|
|
490
|
+
let statusRequiresRevalidation = false;
|
|
158
491
|
try {
|
|
159
492
|
status = requireStatusEnvelope(result, statusArtifact, prepared, authority, contract);
|
|
160
493
|
validateStatusProgression(tracker.state, tracker.metadata, status);
|
|
494
|
+
if (status.metadata !== undefined) {
|
|
495
|
+
if (tracker.pending !== undefined &&
|
|
496
|
+
status.metadata.causationId !==
|
|
497
|
+
tracker.pending.causationId) {
|
|
498
|
+
throw new Error('command status changed pending causation identity');
|
|
499
|
+
}
|
|
500
|
+
if (status.metadata.projectionDisposition === 'revalidate') {
|
|
501
|
+
/*
|
|
502
|
+
* This is current-envelope authority to refetch, never
|
|
503
|
+
* authority to validate or apply the omitted old-scope
|
|
504
|
+
* delta.
|
|
505
|
+
*/
|
|
506
|
+
statusRequiresRevalidation = true;
|
|
507
|
+
}
|
|
508
|
+
else if (prepared.consistency !== 'atomic') {
|
|
509
|
+
// Atomic rows are confirmed on the mutation response,
|
|
510
|
+
// not via async projection-delta status envelopes.
|
|
511
|
+
const validated = validateProjectionForState(prepared, status.metadata, authority);
|
|
512
|
+
statusRequiresRevalidation = commitActualProjection(prepared, validated);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
161
515
|
}
|
|
162
516
|
catch (error) {
|
|
517
|
+
const failure = new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', { commandId: prepared.commandId, cause: error });
|
|
518
|
+
rejectUnmanagedLayer(prepared.commandId);
|
|
163
519
|
revalidateInBackground(prepared, authority);
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
tracker.state = status.state;
|
|
167
|
-
if (status.metadata !== undefined) {
|
|
168
|
-
tracker.metadata = status.metadata;
|
|
169
|
-
if (tracker.pending !== undefined) {
|
|
170
|
-
tracker.pending.metadata = status.metadata;
|
|
171
|
-
}
|
|
520
|
+
failTrackedProjection(tracker, pending, failure);
|
|
521
|
+
throw failure;
|
|
172
522
|
}
|
|
523
|
+
commitStatusProgression(tracker, status);
|
|
173
524
|
switch (status.state) {
|
|
174
525
|
case 'rejected':
|
|
175
526
|
rejectUnmanagedLayer(prepared.commandId);
|
|
@@ -187,10 +538,56 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
187
538
|
state: status.state
|
|
188
539
|
}));
|
|
189
540
|
break;
|
|
190
|
-
case '
|
|
191
|
-
case '
|
|
192
|
-
case '
|
|
541
|
+
case 'succeeded':
|
|
542
|
+
case 'succeeded_pending_projection':
|
|
543
|
+
case 'atomic': {
|
|
193
544
|
const metadata = status.metadata;
|
|
545
|
+
if (metadata.projectionDisposition === 'revalidate') {
|
|
546
|
+
if (metadata.state === 'succeeded_pending_projection') {
|
|
547
|
+
/*
|
|
548
|
+
* Historical evidence is still pending. Refresh
|
|
549
|
+
* canonical current-scope queries opportunistically,
|
|
550
|
+
* but retain optimism and keep polling until the
|
|
551
|
+
* server reports a terminal state.
|
|
552
|
+
*/
|
|
553
|
+
revalidateInBackground(prepared, authority);
|
|
554
|
+
break;
|
|
555
|
+
}
|
|
556
|
+
let refreshed = false;
|
|
557
|
+
try {
|
|
558
|
+
refreshed = await revalidate(prepared, authority.signal);
|
|
559
|
+
}
|
|
560
|
+
catch (error) {
|
|
561
|
+
if (disposed) {
|
|
562
|
+
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_DISPOSED', {
|
|
563
|
+
commandId: prepared.commandId,
|
|
564
|
+
cause: error
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
if (authority.signal?.aborted ||
|
|
568
|
+
!stillCurrent(authority)) {
|
|
569
|
+
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_SCOPE_INVALIDATED', {
|
|
570
|
+
commandId: prepared.commandId,
|
|
571
|
+
cause: error
|
|
572
|
+
});
|
|
573
|
+
}
|
|
574
|
+
// The retained monitor will retry this exact
|
|
575
|
+
// current-scope revalidation on the next status read.
|
|
576
|
+
}
|
|
577
|
+
if (refreshed && stillCurrent(authority)) {
|
|
578
|
+
const controller = tracker.pending;
|
|
579
|
+
if (controller !== undefined &&
|
|
580
|
+
!controller.settled &&
|
|
581
|
+
pending.get(prepared.commandId) === controller) {
|
|
582
|
+
retireLayerAfterAuthoritativeRevalidation(prepared.commandId);
|
|
583
|
+
settleTrackedProjection(tracker, pending);
|
|
584
|
+
}
|
|
585
|
+
else if (unmanagedLayers.has(prepared.commandId)) {
|
|
586
|
+
retireLayerAfterAuthoritativeRevalidation(prepared.commandId);
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
break;
|
|
590
|
+
}
|
|
194
591
|
const remainsPending = replica.markOptimisticLayerAccepted(prepared.commandId, metadata);
|
|
195
592
|
if (!remainsPending) {
|
|
196
593
|
unmanagedLayers.delete(prepared.commandId);
|
|
@@ -198,14 +595,15 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
198
595
|
settleTrackedProjection(tracker, pending);
|
|
199
596
|
}
|
|
200
597
|
}
|
|
201
|
-
else if (metadata.state === '
|
|
202
|
-
(metadata.state === '
|
|
203
|
-
|
|
204
|
-
prepared.revalidation.required
|
|
598
|
+
else if (metadata.state === 'atomic' ||
|
|
599
|
+
(metadata.state === 'succeeded' &&
|
|
600
|
+
metadata.expects.length === 0 &&
|
|
601
|
+
(prepared.revalidation.required ||
|
|
602
|
+
statusRequiresRevalidation))) {
|
|
205
603
|
/*
|
|
206
604
|
* Status is causal truth, but it carries no canonical
|
|
207
605
|
* query payload. `projected` fences asynchronous
|
|
208
|
-
* projection; `
|
|
606
|
+
* projection; `succeeded` is terminal when the generated
|
|
209
607
|
* contract has no confirmations. In both cases, only a
|
|
210
608
|
* query started after that fence may retire optimism.
|
|
211
609
|
*/
|
|
@@ -295,7 +693,11 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
295
693
|
: 'REPLICA_COMMAND_SCOPE_INVALIDATED', { commandId: prepared.commandId, cause: request.signal.reason });
|
|
296
694
|
}
|
|
297
695
|
try {
|
|
298
|
-
|
|
696
|
+
const pureHost = {
|
|
697
|
+
readRecord: replica[replicaCommandReadRecord]?.bind(replica),
|
|
698
|
+
pureFunctions: options.pureFunctions
|
|
699
|
+
};
|
|
700
|
+
replica.createOptimisticLayer(prepared.commandId, (writer) => applyOptimisticEffects(writer, prepared.optimistic.operations, pureHost), semanticChanges);
|
|
299
701
|
}
|
|
300
702
|
catch (error) {
|
|
301
703
|
requestSignals.dispose();
|
|
@@ -326,9 +728,11 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
326
728
|
}
|
|
327
729
|
return undefined;
|
|
328
730
|
};
|
|
731
|
+
const dispatchReservation = reserveDispatch(prepared);
|
|
329
732
|
let result;
|
|
330
733
|
let dispatchAttempted = false;
|
|
331
734
|
try {
|
|
735
|
+
await dispatchReservation.wait;
|
|
332
736
|
result = await dispatchPrepared(transport, request, transportRetries, () => {
|
|
333
737
|
dispatchAttempted = true;
|
|
334
738
|
});
|
|
@@ -357,6 +761,7 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
357
761
|
});
|
|
358
762
|
}
|
|
359
763
|
finally {
|
|
764
|
+
dispatchReservation.release();
|
|
360
765
|
requestSignals.dispose();
|
|
361
766
|
}
|
|
362
767
|
if (!stillCurrent(authority)) {
|
|
@@ -397,8 +802,6 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
397
802
|
});
|
|
398
803
|
}
|
|
399
804
|
const metadata = distributed.command;
|
|
400
|
-
statusTracker.state = metadata.state;
|
|
401
|
-
statusTracker.metadata = metadata;
|
|
402
805
|
if (metadata.state === 'rejected') {
|
|
403
806
|
rejectUnmanagedLayer(prepared.commandId);
|
|
404
807
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_REJECTED', {
|
|
@@ -412,6 +815,8 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
412
815
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROJECTION_FAILED', { commandId: prepared.commandId, state: metadata.state });
|
|
413
816
|
}
|
|
414
817
|
if (metadata.state === 'unknown' || metadata.state === 'in_progress') {
|
|
818
|
+
statusTracker.state = metadata.state;
|
|
819
|
+
statusTracker.metadata = metadata;
|
|
415
820
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_OUTCOME_PENDING', {
|
|
416
821
|
commandId: prepared.commandId,
|
|
417
822
|
state: metadata.state,
|
|
@@ -424,42 +829,51 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
424
829
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROJECTION_FAILED', { commandId: prepared.commandId, state: metadata.state });
|
|
425
830
|
}
|
|
426
831
|
if ((result.errors?.length ?? 0) > 0) {
|
|
427
|
-
|
|
428
|
-
if (prepared.confirmations?.kind === 'finite') {
|
|
429
|
-
replica.markOptimisticLayerAccepted(prepared.commandId, metadata);
|
|
430
|
-
retainedRecovery = recoveryForRetainedLayer();
|
|
431
|
-
}
|
|
432
|
-
else {
|
|
433
|
-
rejectUnmanagedLayer(prepared.commandId);
|
|
434
|
-
}
|
|
832
|
+
rejectUnmanagedLayer(prepared.commandId);
|
|
435
833
|
revalidateInBackground(prepared, authority);
|
|
436
|
-
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', { commandId: prepared.commandId
|
|
834
|
+
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', { commandId: prepared.commandId });
|
|
437
835
|
}
|
|
438
836
|
let output;
|
|
439
837
|
try {
|
|
440
838
|
output = commandOutput(artifact, result.data, prepared.transport.mutationField);
|
|
441
839
|
}
|
|
442
840
|
catch (error) {
|
|
443
|
-
|
|
444
|
-
if (prepared.consistency === 'projected' ||
|
|
445
|
-
prepared.confirmations?.kind !== 'finite') {
|
|
446
|
-
rejectUnmanagedLayer(prepared.commandId);
|
|
447
|
-
}
|
|
448
|
-
else {
|
|
449
|
-
replica.markOptimisticLayerAccepted(prepared.commandId, metadata);
|
|
450
|
-
retainedRecovery = recoveryForRetainedLayer();
|
|
451
|
-
}
|
|
841
|
+
rejectUnmanagedLayer(prepared.commandId);
|
|
452
842
|
revalidateInBackground(prepared, authority);
|
|
453
843
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', {
|
|
454
844
|
commandId: prepared.commandId,
|
|
455
845
|
cause: error,
|
|
456
|
-
|
|
846
|
+
...(statusArtifact === undefined ? {} : { recovery })
|
|
457
847
|
});
|
|
458
848
|
}
|
|
849
|
+
/*
|
|
850
|
+
* Ship contract: Atomic seals from the atomic GraphQL row +
|
|
851
|
+
* direct `records` (confirmDirectProjection). Eventual applies
|
|
852
|
+
* projection-delta when present. Both strategies start from the same
|
|
853
|
+
* compiler-derived event→mutation preview — different response proof by
|
|
854
|
+
* design.
|
|
855
|
+
*/
|
|
856
|
+
let actualRequiresRevalidation = false;
|
|
857
|
+
if (prepared.consistency !== 'atomic') {
|
|
858
|
+
try {
|
|
859
|
+
actualRequiresRevalidation = applyActualProjection(prepared, metadata, authority);
|
|
860
|
+
}
|
|
861
|
+
catch (error) {
|
|
862
|
+
rejectUnmanagedLayer(prepared.commandId);
|
|
863
|
+
revalidateInBackground(prepared, authority);
|
|
864
|
+
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', {
|
|
865
|
+
commandId: prepared.commandId,
|
|
866
|
+
cause: error,
|
|
867
|
+
...(statusArtifact === undefined ? {} : { recovery })
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
}
|
|
459
871
|
let projected;
|
|
460
872
|
let projectionLifecycle;
|
|
461
|
-
if (prepared.consistency === '
|
|
462
|
-
if (metadata.state !== '
|
|
873
|
+
if (prepared.consistency === 'atomic') {
|
|
874
|
+
if (metadata.state !== 'atomic') {
|
|
875
|
+
statusTracker.state = metadata.state;
|
|
876
|
+
statusTracker.metadata = metadata;
|
|
463
877
|
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_OUTCOME_PENDING', {
|
|
464
878
|
commandId: prepared.commandId,
|
|
465
879
|
state: metadata.state,
|
|
@@ -473,19 +887,27 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
473
887
|
catch (error) {
|
|
474
888
|
rejectUnmanagedLayer(prepared.commandId);
|
|
475
889
|
revalidateInBackground(prepared, authority);
|
|
476
|
-
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', {
|
|
890
|
+
throw new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', {
|
|
891
|
+
commandId: prepared.commandId,
|
|
892
|
+
cause: error,
|
|
893
|
+
...(statusArtifact === undefined ? {} : { recovery })
|
|
894
|
+
});
|
|
477
895
|
}
|
|
896
|
+
statusTracker.state = metadata.state;
|
|
897
|
+
statusTracker.metadata = metadata;
|
|
478
898
|
projected = Promise.resolve(Object.freeze({
|
|
479
899
|
commandId: prepared.commandId,
|
|
480
|
-
state: '
|
|
900
|
+
state: 'atomic',
|
|
481
901
|
result: output,
|
|
482
902
|
metadata
|
|
483
903
|
}));
|
|
484
904
|
}
|
|
485
905
|
else {
|
|
906
|
+
statusTracker.state = metadata.state;
|
|
907
|
+
statusTracker.metadata = metadata;
|
|
486
908
|
const remainsPending = replica.markOptimisticLayerAccepted(prepared.commandId, metadata);
|
|
487
|
-
if (
|
|
488
|
-
const controller = pendingProjection(authority, metadata, prepared);
|
|
909
|
+
if (metadata.expects.length > 0) {
|
|
910
|
+
const controller = pendingProjection(authority, metadata, prepared, statusTracker);
|
|
489
911
|
statusTracker.pending = controller;
|
|
490
912
|
if (!remainsPending) {
|
|
491
913
|
unmanagedLayers.delete(prepared.commandId);
|
|
@@ -511,7 +933,7 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
511
933
|
projected = callerProjectedPromise(controller, callOptions.signal);
|
|
512
934
|
}
|
|
513
935
|
}
|
|
514
|
-
if (prepared.revalidation.required) {
|
|
936
|
+
if (prepared.revalidation.required || actualRequiresRevalidation) {
|
|
515
937
|
revalidateAndConfirmUnmanagedInBackground(prepared, authority);
|
|
516
938
|
}
|
|
517
939
|
const receiptValue = {
|
|
@@ -532,7 +954,7 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
532
954
|
}
|
|
533
955
|
const receipt = Object.freeze(receiptValue);
|
|
534
956
|
try {
|
|
535
|
-
await callOptions.
|
|
957
|
+
await callOptions.onSucceeded?.(receipt);
|
|
536
958
|
}
|
|
537
959
|
catch (error) {
|
|
538
960
|
options.onBackgroundError?.(error);
|
|
@@ -567,6 +989,8 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
567
989
|
continue;
|
|
568
990
|
}
|
|
569
991
|
if (distributed.schemaHash !== controller.authority.scope.schemaHash ||
|
|
992
|
+
distributed.authorizationGeneration !==
|
|
993
|
+
controller.authority.scope.authorizationGeneration ||
|
|
570
994
|
distributed.cacheScope !== controller.authority.scope.cacheScope) {
|
|
571
995
|
continue;
|
|
572
996
|
}
|
|
@@ -574,19 +998,28 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
574
998
|
if (command?.commandId === commandId) {
|
|
575
999
|
try {
|
|
576
1000
|
verifyReplicaCommandReceipt(controller.prepared, command);
|
|
1001
|
+
if (command.causationId !== controller.causationId) {
|
|
1002
|
+
throw new Error('live command changed pending causation identity');
|
|
1003
|
+
}
|
|
1004
|
+
const status = Object.freeze({
|
|
1005
|
+
commandId,
|
|
1006
|
+
state: command.state,
|
|
1007
|
+
metadata: command
|
|
1008
|
+
});
|
|
1009
|
+
validateStatusProgression(controller.tracker.state, controller.tracker.metadata, status);
|
|
1010
|
+
if (command.projectionDisposition !== 'revalidate') {
|
|
1011
|
+
const validated = validateProjectionForState(controller.prepared, command, controller.authority);
|
|
1012
|
+
commitActualProjection(controller.prepared, validated);
|
|
1013
|
+
}
|
|
1014
|
+
commitStatusProgression(controller.tracker, status);
|
|
577
1015
|
}
|
|
578
1016
|
catch (error) {
|
|
1017
|
+
replica.rejectOptimisticLayer(commandId);
|
|
579
1018
|
revalidateInBackground(controller.prepared, controller.authority);
|
|
580
1019
|
settleProjectionFailure(controller, new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', { commandId, cause: error }));
|
|
581
1020
|
pending.delete(commandId);
|
|
582
1021
|
continue;
|
|
583
1022
|
}
|
|
584
|
-
if (command.causationId !== controller.causationId) {
|
|
585
|
-
settleProjectionFailure(controller, new ReplicaCommandRuntimeError('REPLICA_COMMAND_PROTOCOL_INVALID', { commandId }));
|
|
586
|
-
pending.delete(commandId);
|
|
587
|
-
continue;
|
|
588
|
-
}
|
|
589
|
-
controller.metadata = command;
|
|
590
1023
|
if (command.state === 'projection_failed') {
|
|
591
1024
|
replica.rejectOptimisticLayer(commandId);
|
|
592
1025
|
revalidateInBackground(controller.prepared, controller.authority);
|
|
@@ -604,6 +1037,15 @@ export function createReplicaCommandRuntime(replica, transport, entries, options
|
|
|
604
1037
|
pending.delete(commandId);
|
|
605
1038
|
continue;
|
|
606
1039
|
}
|
|
1040
|
+
if (command.projectionDisposition === 'revalidate') {
|
|
1041
|
+
if (command.state === 'succeeded_pending_projection') {
|
|
1042
|
+
revalidateInBackground(controller.prepared, controller.authority);
|
|
1043
|
+
}
|
|
1044
|
+
else {
|
|
1045
|
+
revalidateDispositionAndRetireInBackground(controller);
|
|
1046
|
+
}
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
607
1049
|
}
|
|
608
1050
|
/*
|
|
609
1051
|
* DistributedReplica is the authority on whether this frame's
|