@victframework/runtime 0.1.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/dist/agent-governance.d.ts +285 -0
- package/dist/agent-governance.js +438 -0
- package/dist/agent-governance.js.map +1 -0
- package/dist/agent-registry.d.ts +103 -0
- package/dist/agent-registry.js +758 -0
- package/dist/agent-registry.js.map +1 -0
- package/dist/agent-types.d.ts +433 -0
- package/dist/agent-types.js +480 -0
- package/dist/agent-types.js.map +1 -0
- package/dist/authority.d.ts +83 -0
- package/dist/authority.js +170 -0
- package/dist/authority.js.map +1 -0
- package/dist/boundary-conformance.d.ts +51 -0
- package/dist/boundary-conformance.js +364 -0
- package/dist/boundary-conformance.js.map +1 -0
- package/dist/control-conformance.d.ts +68 -0
- package/dist/control-conformance.js +1131 -0
- package/dist/control-conformance.js.map +1 -0
- package/dist/control-in-memory.d.ts +257 -0
- package/dist/control-in-memory.js +886 -0
- package/dist/control-in-memory.js.map +1 -0
- package/dist/control-types.d.ts +1017 -0
- package/dist/control-types.js +779 -0
- package/dist/control-types.js.map +1 -0
- package/dist/durable-run.d.ts +73 -0
- package/dist/durable-run.js +262 -0
- package/dist/durable-run.js.map +1 -0
- package/dist/effect-policy.d.ts +22 -0
- package/dist/effect-policy.js +45 -0
- package/dist/effect-policy.js.map +1 -0
- package/dist/errors.d.ts +36 -0
- package/dist/errors.js +71 -0
- package/dist/errors.js.map +1 -0
- package/dist/in-memory-stores.d.ts +16 -0
- package/dist/in-memory-stores.js +522 -0
- package/dist/in-memory-stores.js.map +1 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +26 -0
- package/dist/index.js.map +1 -0
- package/dist/operator-config.d.ts +141 -0
- package/dist/operator-config.js +289 -0
- package/dist/operator-config.js.map +1 -0
- package/dist/orchestration-activation.d.ts +116 -0
- package/dist/orchestration-activation.js +223 -0
- package/dist/orchestration-activation.js.map +1 -0
- package/dist/orchestration-canary-conformance.d.ts +10 -0
- package/dist/orchestration-canary-conformance.js +726 -0
- package/dist/orchestration-canary-conformance.js.map +1 -0
- package/dist/orchestration-commands.d.ts +47 -0
- package/dist/orchestration-commands.js +538 -0
- package/dist/orchestration-commands.js.map +1 -0
- package/dist/orchestration-conformance.d.ts +59 -0
- package/dist/orchestration-conformance.js +594 -0
- package/dist/orchestration-conformance.js.map +1 -0
- package/dist/orchestration-driver-types.d.ts +163 -0
- package/dist/orchestration-driver-types.js +21 -0
- package/dist/orchestration-driver-types.js.map +1 -0
- package/dist/orchestration-driver.d.ts +48 -0
- package/dist/orchestration-driver.js +1030 -0
- package/dist/orchestration-driver.js.map +1 -0
- package/dist/orchestration-in-memory.d.ts +18 -0
- package/dist/orchestration-in-memory.js +1313 -0
- package/dist/orchestration-in-memory.js.map +1 -0
- package/dist/orchestration-join-conformance.d.ts +20 -0
- package/dist/orchestration-join-conformance.js +542 -0
- package/dist/orchestration-join-conformance.js.map +1 -0
- package/dist/orchestration-plan.d.ts +52 -0
- package/dist/orchestration-plan.js +265 -0
- package/dist/orchestration-plan.js.map +1 -0
- package/dist/orchestration-race-conformance.d.ts +35 -0
- package/dist/orchestration-race-conformance.js +1265 -0
- package/dist/orchestration-race-conformance.js.map +1 -0
- package/dist/orchestration-remediation-conformance.d.ts +54 -0
- package/dist/orchestration-remediation-conformance.js +632 -0
- package/dist/orchestration-remediation-conformance.js.map +1 -0
- package/dist/orchestration-store-types.d.ts +568 -0
- package/dist/orchestration-store-types.js +2 -0
- package/dist/orchestration-store-types.js.map +1 -0
- package/dist/pack-conformance.d.ts +59 -0
- package/dist/pack-conformance.js +222 -0
- package/dist/pack-conformance.js.map +1 -0
- package/dist/pack-install.d.ts +44 -0
- package/dist/pack-install.js +126 -0
- package/dist/pack-install.js.map +1 -0
- package/dist/registry.d.ts +107 -0
- package/dist/registry.js +481 -0
- package/dist/registry.js.map +1 -0
- package/dist/runtime.d.ts +158 -0
- package/dist/runtime.js +1341 -0
- package/dist/runtime.js.map +1 -0
- package/dist/serialization.d.ts +26 -0
- package/dist/serialization.js +162 -0
- package/dist/serialization.js.map +1 -0
- package/dist/store-conformance.d.ts +45 -0
- package/dist/store-conformance.js +876 -0
- package/dist/store-conformance.js.map +1 -0
- package/dist/store-errors.d.ts +49 -0
- package/dist/store-errors.js +48 -0
- package/dist/store-errors.js.map +1 -0
- package/dist/store-types.d.ts +331 -0
- package/dist/store-types.js +14 -0
- package/dist/store-types.js.map +1 -0
- package/dist/store-validation.d.ts +63 -0
- package/dist/store-validation.js +164 -0
- package/dist/store-validation.js.map +1 -0
- package/dist/stream-hub.d.ts +151 -0
- package/dist/stream-hub.js +402 -0
- package/dist/stream-hub.js.map +1 -0
- package/dist/testing.d.ts +25 -0
- package/dist/testing.js +19 -0
- package/dist/testing.js.map +1 -0
- package/dist/types.d.ts +221 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1313 @@
|
|
|
1
|
+
import { canonicalJoinOutput, canTransitionAttempt, canTransitionRun, canTransitionToken, } from '@victframework/kernel';
|
|
2
|
+
import { VictStoreError } from './store-errors.js';
|
|
3
|
+
import { canonicalPersistedValue, immutableSnapshot } from './serialization.js';
|
|
4
|
+
import { assertEventMatchesRun } from './store-validation.js';
|
|
5
|
+
function wrap(cause, operation, runId) {
|
|
6
|
+
if (cause instanceof VictStoreError) {
|
|
7
|
+
return cause;
|
|
8
|
+
}
|
|
9
|
+
return new VictStoreError('VICT_STORE_UNAVAILABLE', 'The storage operation failed and the transition was not committed.', { operation, runId }, cause);
|
|
10
|
+
}
|
|
11
|
+
function stripCheckpoint(token) {
|
|
12
|
+
const { checkpoint, ...rest } = token;
|
|
13
|
+
void checkpoint;
|
|
14
|
+
return rest;
|
|
15
|
+
}
|
|
16
|
+
/** Deterministic ready-token order: creation instant, then token id. */
|
|
17
|
+
function readyOrder(tokens) {
|
|
18
|
+
return [...tokens]
|
|
19
|
+
.filter((token) => token.status === 'ready')
|
|
20
|
+
.sort((a, b) => a.createdAt - b.createdAt || (a.tokenId < b.tokenId ? -1 : a.tokenId > b.tokenId ? 1 : 0));
|
|
21
|
+
}
|
|
22
|
+
function nextEventSeq(stored) {
|
|
23
|
+
const last = stored.events.at(-1);
|
|
24
|
+
return last === undefined ? 0 : last.seq + 1;
|
|
25
|
+
}
|
|
26
|
+
export function createInMemoryOrchestrationStore(options = {}) {
|
|
27
|
+
const faults = options.faults;
|
|
28
|
+
const runs = new Map();
|
|
29
|
+
const requireRun = (runId, operation) => {
|
|
30
|
+
const found = runs.get(runId);
|
|
31
|
+
if (!found) {
|
|
32
|
+
throw new VictStoreError('VICT_STORE_RUN_NOT_FOUND', 'Run not found.', { operation, runId });
|
|
33
|
+
}
|
|
34
|
+
return found;
|
|
35
|
+
};
|
|
36
|
+
/** Append the ordered safe events with dense sequence numbers (validated against the run identity). */
|
|
37
|
+
const appendEvents = (stored, events) => {
|
|
38
|
+
let seq = nextEventSeq(stored);
|
|
39
|
+
for (const event of events) {
|
|
40
|
+
const full = { ...event, seq };
|
|
41
|
+
assertEventMatchesRun(full, stored.run);
|
|
42
|
+
stored.events.push(full);
|
|
43
|
+
seq += 1;
|
|
44
|
+
}
|
|
45
|
+
return seq;
|
|
46
|
+
};
|
|
47
|
+
const stageCheckpoint = (run, tokenId, payload) => {
|
|
48
|
+
// Private operational boundary: validate against the persisted-value
|
|
49
|
+
// domain and canonicalize before it becomes durable.
|
|
50
|
+
const validated = canonicalPersistedValue(payload);
|
|
51
|
+
const token = run.tokens.get(tokenId);
|
|
52
|
+
if (!token) {
|
|
53
|
+
throw new VictStoreError('VICT_STORE_INVALID_COMMAND', 'A checkpoint references an unknown token.', { operation: 'orchestration.checkpoint', runId: run.run.runId, tokenId });
|
|
54
|
+
}
|
|
55
|
+
token.checkpoint = validated;
|
|
56
|
+
};
|
|
57
|
+
const derive = (stored) => {
|
|
58
|
+
const branchOutputs = {};
|
|
59
|
+
for (const [forkId, byKey] of stored.branchOutputs) {
|
|
60
|
+
branchOutputs[forkId] = Object.fromEntries(byKey);
|
|
61
|
+
}
|
|
62
|
+
return immutableSnapshot({
|
|
63
|
+
run: stored.run,
|
|
64
|
+
tokens: [...stored.tokens.values()].map(stripCheckpoint),
|
|
65
|
+
attempts: [...stored.attempts.values()],
|
|
66
|
+
waits: [...stored.waits.values()],
|
|
67
|
+
timers: [...stored.timers.values()],
|
|
68
|
+
branchResults: [...stored.branchResults],
|
|
69
|
+
branchOutputs,
|
|
70
|
+
nextEventSeq: nextEventSeq(stored),
|
|
71
|
+
});
|
|
72
|
+
};
|
|
73
|
+
const store = {
|
|
74
|
+
async createOrchestrationRun(command) {
|
|
75
|
+
if (runs.has(command.runId)) {
|
|
76
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'A run with this id already exists.', {
|
|
77
|
+
operation: 'orchestration.createRun',
|
|
78
|
+
runId: command.runId,
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
const now = command.now;
|
|
82
|
+
const rootToken = {
|
|
83
|
+
tokenId: command.rootTokenId,
|
|
84
|
+
runId: command.runId,
|
|
85
|
+
activationVersion: command.activationVersion,
|
|
86
|
+
nodeId: command.entryNodeId,
|
|
87
|
+
status: 'ready',
|
|
88
|
+
parentTokenId: null,
|
|
89
|
+
lineage: '',
|
|
90
|
+
forkId: null,
|
|
91
|
+
branchKey: null,
|
|
92
|
+
revision: 1,
|
|
93
|
+
createdAt: now,
|
|
94
|
+
updatedAt: now,
|
|
95
|
+
};
|
|
96
|
+
const stored = {
|
|
97
|
+
run: {
|
|
98
|
+
runId: command.runId,
|
|
99
|
+
graphId: command.graphId,
|
|
100
|
+
graphVersion: command.graphVersion,
|
|
101
|
+
capabilitySetVersion: command.capabilitySetVersion,
|
|
102
|
+
activationVersion: command.activationVersion,
|
|
103
|
+
status: 'running',
|
|
104
|
+
mode: command.mode,
|
|
105
|
+
retention: command.retention,
|
|
106
|
+
steps: 0,
|
|
107
|
+
currentNodeId: command.entryNodeId,
|
|
108
|
+
recordRevision: 1,
|
|
109
|
+
cancellation: null,
|
|
110
|
+
createdAt: now,
|
|
111
|
+
updatedAt: now,
|
|
112
|
+
completedAt: null,
|
|
113
|
+
},
|
|
114
|
+
tokens: new Map([[command.rootTokenId, rootToken]]),
|
|
115
|
+
attempts: new Map(),
|
|
116
|
+
waits: new Map(),
|
|
117
|
+
timers: new Map(),
|
|
118
|
+
branchResults: [],
|
|
119
|
+
branchOutputs: new Map(),
|
|
120
|
+
signalReceipts: new Map(),
|
|
121
|
+
cancellationRequests: new Map(),
|
|
122
|
+
resolutionRequests: new Map(),
|
|
123
|
+
events: [],
|
|
124
|
+
};
|
|
125
|
+
runs.set(command.runId, stored);
|
|
126
|
+
try {
|
|
127
|
+
stageCheckpoint(stored, command.rootTokenId, command.checkpoint);
|
|
128
|
+
appendEvents(stored, command.events);
|
|
129
|
+
faults?.afterStateStage?.('orchestration.createRun');
|
|
130
|
+
faults?.beforeCommit?.('orchestration.createRun');
|
|
131
|
+
return immutableSnapshot(stored.run);
|
|
132
|
+
}
|
|
133
|
+
catch (cause) {
|
|
134
|
+
runs.delete(command.runId);
|
|
135
|
+
throw wrap(cause, 'orchestration.createRun', command.runId);
|
|
136
|
+
}
|
|
137
|
+
},
|
|
138
|
+
async getOrchestrationRun(runId) {
|
|
139
|
+
const stored = runs.get(runId);
|
|
140
|
+
return stored ? immutableSnapshot(stored.run) : undefined;
|
|
141
|
+
},
|
|
142
|
+
async listOrchestrationRuns(query = {}) {
|
|
143
|
+
const all = [...runs.values()]
|
|
144
|
+
.map((entry) => entry.run)
|
|
145
|
+
.filter((run) => (query.graphId === undefined || run.graphId === query.graphId) &&
|
|
146
|
+
(query.activationVersion === undefined ||
|
|
147
|
+
run.activationVersion === query.activationVersion) &&
|
|
148
|
+
(query.status === undefined || run.status === query.status))
|
|
149
|
+
.sort((a, b) => a.createdAt - b.createdAt);
|
|
150
|
+
const limited = query.limit !== undefined ? all.slice(0, Math.max(0, Math.floor(query.limit))) : all;
|
|
151
|
+
return limited.map((run) => immutableSnapshot(run));
|
|
152
|
+
},
|
|
153
|
+
async getOrchestrationSnapshot(runId) {
|
|
154
|
+
const stored = runs.get(runId);
|
|
155
|
+
return stored ? derive(stored) : undefined;
|
|
156
|
+
},
|
|
157
|
+
async claimReadyToken(command) {
|
|
158
|
+
const stored = requireRun(command.runId, 'orchestration.claimReadyToken');
|
|
159
|
+
const run = stored.run;
|
|
160
|
+
if (run.status !== 'running' && run.status !== 'waiting') {
|
|
161
|
+
return { claimed: false, reason: run.status === 'blocked' ? 'quiescent' : 'terminal' };
|
|
162
|
+
}
|
|
163
|
+
if (run.cancellation !== null) {
|
|
164
|
+
return { claimed: false, reason: 'cancelled' };
|
|
165
|
+
}
|
|
166
|
+
const token = readyOrder(stored.tokens.values())[0];
|
|
167
|
+
if (!token) {
|
|
168
|
+
return { claimed: false, reason: 'quiescent' };
|
|
169
|
+
}
|
|
170
|
+
if (!canTransitionToken(token.status, 'claimed')) {
|
|
171
|
+
return { claimed: false, reason: 'conflict' };
|
|
172
|
+
}
|
|
173
|
+
const plan = command.planner.planFor(token);
|
|
174
|
+
const invocationId = command.planner.invocationIdFor(token);
|
|
175
|
+
const attemptNumber = [...stored.attempts.values()].filter((candidate) => candidate.invocationId === invocationId)
|
|
176
|
+
.length + 1;
|
|
177
|
+
const attemptId = command.planner.attemptIdFor(token, attemptNumber);
|
|
178
|
+
const now = command.now;
|
|
179
|
+
const previousRun = stored.run;
|
|
180
|
+
const previousStatus = token.status;
|
|
181
|
+
const previousRevision = token.revision;
|
|
182
|
+
const previousUpdatedAt = token.updatedAt;
|
|
183
|
+
const attempt = {
|
|
184
|
+
attemptId,
|
|
185
|
+
invocationId,
|
|
186
|
+
runId: command.runId,
|
|
187
|
+
tokenId: token.tokenId,
|
|
188
|
+
nodeId: token.nodeId,
|
|
189
|
+
capabilityId: plan.capabilityId,
|
|
190
|
+
attemptNumber,
|
|
191
|
+
effectClass: plan.effectClass,
|
|
192
|
+
idempotencyKey: plan.idempotencyKey,
|
|
193
|
+
state: 'started',
|
|
194
|
+
ownerId: command.ownerId,
|
|
195
|
+
leaseExpiresAt: command.leaseExpiresAt,
|
|
196
|
+
deadlineAt: plan.deadlineAt,
|
|
197
|
+
fence: attemptNumber,
|
|
198
|
+
retryDueAt: null,
|
|
199
|
+
createdAt: now,
|
|
200
|
+
updatedAt: now,
|
|
201
|
+
};
|
|
202
|
+
stored.attempts.set(attemptId, attempt);
|
|
203
|
+
token.status = 'claimed';
|
|
204
|
+
token.revision += 1;
|
|
205
|
+
token.updatedAt = now;
|
|
206
|
+
stored.run = {
|
|
207
|
+
...stored.run,
|
|
208
|
+
status: 'running',
|
|
209
|
+
steps: stored.run.steps + 1,
|
|
210
|
+
currentNodeId: token.nodeId,
|
|
211
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
212
|
+
updatedAt: now,
|
|
213
|
+
};
|
|
214
|
+
try {
|
|
215
|
+
appendEvents(stored, [
|
|
216
|
+
{
|
|
217
|
+
type: 'node.started',
|
|
218
|
+
nodeId: token.nodeId,
|
|
219
|
+
capabilityId: plan.capabilityId,
|
|
220
|
+
runId: command.runId,
|
|
221
|
+
graphId: stored.run.graphId,
|
|
222
|
+
graphVersion: stored.run.graphVersion,
|
|
223
|
+
capabilitySetVersion: stored.run.capabilitySetVersion,
|
|
224
|
+
activationVersion: stored.run.activationVersion,
|
|
225
|
+
timestamp: now,
|
|
226
|
+
},
|
|
227
|
+
]);
|
|
228
|
+
faults?.afterStateStage?.('orchestration.claimReadyToken');
|
|
229
|
+
faults?.beforeCommit?.('orchestration.claimReadyToken');
|
|
230
|
+
const claimed = {
|
|
231
|
+
token: immutableSnapshot(stripCheckpoint(token)),
|
|
232
|
+
attempt: immutableSnapshot(attempt),
|
|
233
|
+
invocationId,
|
|
234
|
+
checkpoint: token.checkpoint === undefined
|
|
235
|
+
? undefined
|
|
236
|
+
: structuredClone(token.checkpoint),
|
|
237
|
+
deadlineAt: plan.deadlineAt,
|
|
238
|
+
idempotencyKey: plan.idempotencyKey,
|
|
239
|
+
runRecordRevision: stored.run.recordRevision,
|
|
240
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
241
|
+
};
|
|
242
|
+
return { claimed: true, claim: claimed };
|
|
243
|
+
}
|
|
244
|
+
catch (cause) {
|
|
245
|
+
stored.attempts.delete(attemptId);
|
|
246
|
+
stored.run = previousRun;
|
|
247
|
+
token.status = previousStatus;
|
|
248
|
+
token.revision = previousRevision;
|
|
249
|
+
token.updatedAt = previousUpdatedAt;
|
|
250
|
+
throw wrap(cause, 'orchestration.claimReadyToken', command.runId);
|
|
251
|
+
}
|
|
252
|
+
},
|
|
253
|
+
async completeAttempt(command) {
|
|
254
|
+
const stored = requireRun(command.runId, 'orchestration.completeAttempt');
|
|
255
|
+
const attempt = stored.attempts.get(command.attemptId);
|
|
256
|
+
if (!attempt) {
|
|
257
|
+
throw new VictStoreError('VICT_STORE_INVALID_COMMAND', 'Unknown attempt.', {
|
|
258
|
+
operation: 'orchestration.completeAttempt',
|
|
259
|
+
runId: command.runId,
|
|
260
|
+
attemptId: command.attemptId,
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
if (attempt.fence !== command.expectedAttemptFence || attempt.ownerId !== command.ownerId) {
|
|
264
|
+
throw new VictStoreError('VICT_STORE_ATTEMPT_FENCE_CONFLICT', 'The attempt completion carries a stale owner or fence.', {
|
|
265
|
+
operation: 'orchestration.completeAttempt',
|
|
266
|
+
runId: command.runId,
|
|
267
|
+
attemptId: command.attemptId,
|
|
268
|
+
expectedFence: command.expectedAttemptFence,
|
|
269
|
+
actualFence: attempt.fence,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (!canTransitionAttempt(attempt.state, outcomeState(command.outcome))) {
|
|
273
|
+
throw new VictStoreError('VICT_STORE_ATTEMPT_STATE_CONFLICT', `Attempt '${command.attemptId}' is in state '${attempt.state}' and cannot accept this outcome.`, {
|
|
274
|
+
operation: 'orchestration.completeAttempt',
|
|
275
|
+
runId: command.runId,
|
|
276
|
+
attemptId: command.attemptId,
|
|
277
|
+
state: attempt.state,
|
|
278
|
+
});
|
|
279
|
+
}
|
|
280
|
+
const token = stored.tokens.get(attempt.tokenId);
|
|
281
|
+
if (!token) {
|
|
282
|
+
throw new VictStoreError('VICT_STORE_INVALID_COMMAND', 'The attempt references an unknown token.', {
|
|
283
|
+
operation: 'orchestration.completeAttempt',
|
|
284
|
+
runId: command.runId,
|
|
285
|
+
attemptId: command.attemptId,
|
|
286
|
+
});
|
|
287
|
+
}
|
|
288
|
+
if (command.run.status !== undefined &&
|
|
289
|
+
!canTransitionRun(stored.run.status, command.run.status)) {
|
|
290
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', `Run status '${stored.run.status}' cannot transition to '${command.run.status}'.`, { operation: 'orchestration.completeAttempt', runId: command.runId });
|
|
291
|
+
}
|
|
292
|
+
const now = command.now;
|
|
293
|
+
const previousRun = stored.run;
|
|
294
|
+
const previousAttemptState = attempt.state;
|
|
295
|
+
const previousToken = { ...token };
|
|
296
|
+
const previousChildTokens = [];
|
|
297
|
+
const previousSiblingTokens = [];
|
|
298
|
+
const previousWaits = [];
|
|
299
|
+
const previousTimers = [];
|
|
300
|
+
const previousBranchOutputs = new Map(stored.branchOutputs);
|
|
301
|
+
let joinFired = false;
|
|
302
|
+
const outcomeStateValue = command.outcome.kind === 'completed'
|
|
303
|
+
? 'completed'
|
|
304
|
+
: command.outcome.kind === 'failed'
|
|
305
|
+
? 'failed'
|
|
306
|
+
: command.outcome.kind === 'timed_out'
|
|
307
|
+
? 'timed_out'
|
|
308
|
+
: command.outcome.kind === 'cancelled'
|
|
309
|
+
? 'cancelled'
|
|
310
|
+
: 'outcome_unknown';
|
|
311
|
+
try {
|
|
312
|
+
// 1. Attempt outcome.
|
|
313
|
+
attempt.state = outcomeStateValue;
|
|
314
|
+
attempt.updatedAt = now;
|
|
315
|
+
// 2. Token movement per continuation.
|
|
316
|
+
const continuation = command.continuation;
|
|
317
|
+
if (continuation.kind === 'advance') {
|
|
318
|
+
token.nodeId = continuation.toNodeId;
|
|
319
|
+
token.status = 'ready';
|
|
320
|
+
token.revision += 1;
|
|
321
|
+
token.updatedAt = now;
|
|
322
|
+
}
|
|
323
|
+
else if (continuation.kind === 'wait') {
|
|
324
|
+
if (!canTransitionToken(token.status, 'waiting')) {
|
|
325
|
+
throw new VictStoreError('VICT_STORE_TOKEN_CONFLICT', `Token '${token.tokenId}' cannot wait from state '${token.status}'.`, { operation: 'orchestration.completeAttempt', runId: command.runId });
|
|
326
|
+
}
|
|
327
|
+
const wait = {
|
|
328
|
+
waitId: continuation.wait.waitId,
|
|
329
|
+
runId: command.runId,
|
|
330
|
+
tokenId: token.tokenId,
|
|
331
|
+
nodeId: token.nodeId,
|
|
332
|
+
activationVersion: stored.run.activationVersion,
|
|
333
|
+
kind: continuation.wait.kind,
|
|
334
|
+
signalName: continuation.wait.signalName,
|
|
335
|
+
contractId: continuation.wait.contractId,
|
|
336
|
+
contractRevision: continuation.wait.contractRevision,
|
|
337
|
+
dueAt: continuation.wait.dueAt,
|
|
338
|
+
timeoutAt: continuation.wait.timeoutAt,
|
|
339
|
+
status: 'open',
|
|
340
|
+
revision: 1,
|
|
341
|
+
createdAt: now,
|
|
342
|
+
resolvedAt: null,
|
|
343
|
+
resolvedBy: null,
|
|
344
|
+
};
|
|
345
|
+
stored.waits.set(wait.waitId, wait);
|
|
346
|
+
token.status = 'waiting';
|
|
347
|
+
token.revision += 1;
|
|
348
|
+
token.updatedAt = now;
|
|
349
|
+
if (continuation.wait.dueAt !== null) {
|
|
350
|
+
const timerId = `timer_${continuation.wait.waitId}`;
|
|
351
|
+
stored.timers.set(timerId, {
|
|
352
|
+
timerId,
|
|
353
|
+
runId: command.runId,
|
|
354
|
+
kind: 'wait',
|
|
355
|
+
waitId: continuation.wait.waitId,
|
|
356
|
+
attemptId: null,
|
|
357
|
+
tokenId: token.tokenId,
|
|
358
|
+
dueAt: continuation.wait.dueAt,
|
|
359
|
+
status: 'scheduled',
|
|
360
|
+
ownerId: null,
|
|
361
|
+
leaseExpiresAt: null,
|
|
362
|
+
revision: 1,
|
|
363
|
+
createdAt: now,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
if (continuation.wait.timeoutAt !== null) {
|
|
367
|
+
const timerId = `timer_timeout_${continuation.wait.waitId}`;
|
|
368
|
+
stored.timers.set(timerId, {
|
|
369
|
+
timerId,
|
|
370
|
+
runId: command.runId,
|
|
371
|
+
kind: 'wait-timeout',
|
|
372
|
+
waitId: continuation.wait.waitId,
|
|
373
|
+
attemptId: null,
|
|
374
|
+
tokenId: token.tokenId,
|
|
375
|
+
dueAt: continuation.wait.timeoutAt,
|
|
376
|
+
status: 'scheduled',
|
|
377
|
+
ownerId: null,
|
|
378
|
+
leaseExpiresAt: null,
|
|
379
|
+
revision: 1,
|
|
380
|
+
createdAt: now,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
else if (continuation.kind === 'fork') {
|
|
385
|
+
token.status = 'completed';
|
|
386
|
+
token.revision += 1;
|
|
387
|
+
token.updatedAt = now;
|
|
388
|
+
for (const child of continuation.children) {
|
|
389
|
+
const childToken = {
|
|
390
|
+
tokenId: child.tokenId,
|
|
391
|
+
runId: command.runId,
|
|
392
|
+
activationVersion: stored.run.activationVersion,
|
|
393
|
+
nodeId: child.toNodeId,
|
|
394
|
+
status: 'ready',
|
|
395
|
+
parentTokenId: token.tokenId,
|
|
396
|
+
lineage: child.lineage,
|
|
397
|
+
forkId: continuation.joinId !== undefined ? child.forkId : child.forkId,
|
|
398
|
+
branchKey: child.branchKey,
|
|
399
|
+
revision: 1,
|
|
400
|
+
createdAt: now,
|
|
401
|
+
updatedAt: now,
|
|
402
|
+
};
|
|
403
|
+
previousChildTokens.push(childToken);
|
|
404
|
+
stored.tokens.set(childToken.tokenId, childToken);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
else if (continuation.kind === 'branchArrival') {
|
|
408
|
+
// The branch token is consumed: its result + output are recorded.
|
|
409
|
+
token.status = 'completed';
|
|
410
|
+
token.revision += 1;
|
|
411
|
+
token.updatedAt = now;
|
|
412
|
+
const arrival = continuation;
|
|
413
|
+
stored.branchResults.push({
|
|
414
|
+
runId: command.runId,
|
|
415
|
+
forkId: arrival.forkId,
|
|
416
|
+
joinId: arrival.joinId,
|
|
417
|
+
branchKey: arrival.branchKey,
|
|
418
|
+
tokenId: token.tokenId,
|
|
419
|
+
failed: false,
|
|
420
|
+
createdAt: now,
|
|
421
|
+
});
|
|
422
|
+
if (command.branchOutput !== undefined) {
|
|
423
|
+
let byKey = stored.branchOutputs.get(arrival.forkId);
|
|
424
|
+
if (!byKey) {
|
|
425
|
+
byKey = new Map();
|
|
426
|
+
stored.branchOutputs.set(arrival.forkId, byKey);
|
|
427
|
+
}
|
|
428
|
+
byKey.set(arrival.branchKey, canonicalPersistedValue(command.branchOutput));
|
|
429
|
+
}
|
|
430
|
+
const completedKeys = new Set(stored.branchResults
|
|
431
|
+
.filter((result) => result.forkId === arrival.forkId && !result.failed)
|
|
432
|
+
.map((result) => result.branchKey));
|
|
433
|
+
const declared = command.declaredBranchKeys ?? [];
|
|
434
|
+
if (declared.length > 0 && declared.every((key) => completedKeys.has(key))) {
|
|
435
|
+
joinFired = true;
|
|
436
|
+
// Final arrival: create exactly ONE join-ready token AT THE
|
|
437
|
+
// JOIN NODE with the canonical (lexicographically sorted)
|
|
438
|
+
// join output as its private checkpoint. The runtime claims
|
|
439
|
+
// this token, validates the join's declared output contract
|
|
440
|
+
// outside the store, and completes the join in one atomic
|
|
441
|
+
// transition (advance downstream, terminal completion, or a
|
|
442
|
+
// sanitized contract failure). This keeps author-controlled
|
|
443
|
+
// parsers out of the persistence layer and makes terminal
|
|
444
|
+
// joins (zero success edges) well-defined.
|
|
445
|
+
if (arrival.joinContinuation !== undefined) {
|
|
446
|
+
const joinPayload = canonicalJoinOutput(Object.fromEntries(stored.branchOutputs.get(arrival.forkId) ?? new Map()));
|
|
447
|
+
const postJoinToken = {
|
|
448
|
+
tokenId: arrival.joinContinuation.tokenId,
|
|
449
|
+
runId: command.runId,
|
|
450
|
+
activationVersion: stored.run.activationVersion,
|
|
451
|
+
nodeId: arrival.joinContinuation.toNodeId,
|
|
452
|
+
status: 'ready',
|
|
453
|
+
parentTokenId: null,
|
|
454
|
+
lineage: arrival.forkId,
|
|
455
|
+
forkId: null,
|
|
456
|
+
branchKey: null,
|
|
457
|
+
revision: 1,
|
|
458
|
+
createdAt: now,
|
|
459
|
+
updatedAt: now,
|
|
460
|
+
};
|
|
461
|
+
previousChildTokens.push(postJoinToken);
|
|
462
|
+
stored.tokens.set(postJoinToken.tokenId, postJoinToken);
|
|
463
|
+
stageCheckpoint(stored, postJoinToken.tokenId, joinPayload);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
else if (continuation.kind === 'branchFailure') {
|
|
468
|
+
token.status = 'completed';
|
|
469
|
+
token.revision += 1;
|
|
470
|
+
token.updatedAt = now;
|
|
471
|
+
// Cancel every unfinished sibling branch token and close their waits.
|
|
472
|
+
for (const other of stored.tokens.values()) {
|
|
473
|
+
if (other.tokenId !== token.tokenId &&
|
|
474
|
+
(other.status === 'ready' || other.status === 'claimed' || other.status === 'waiting')) {
|
|
475
|
+
previousSiblingTokens.push({ token: { ...other }, status: other.status });
|
|
476
|
+
other.status = 'cancelled';
|
|
477
|
+
other.revision += 1;
|
|
478
|
+
other.updatedAt = now;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
for (const wait of stored.waits.values()) {
|
|
482
|
+
if (wait.status === 'open') {
|
|
483
|
+
previousWaits.push({ wait, status: wait.status });
|
|
484
|
+
wait.status = 'cancelled';
|
|
485
|
+
wait.revision += 1;
|
|
486
|
+
wait.resolvedAt = now;
|
|
487
|
+
wait.resolvedBy = command.attemptId;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
for (const timer of stored.timers.values()) {
|
|
491
|
+
if (timer.status === 'scheduled') {
|
|
492
|
+
previousTimers.push({ timer, status: timer.status });
|
|
493
|
+
timer.status = 'cancelled';
|
|
494
|
+
timer.revision += 1;
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
else if (continuation.kind === 'retry') {
|
|
499
|
+
// The token stays claimed (ineligible) until its durable retry
|
|
500
|
+
// timer fires; the timer resolution makes it ready again.
|
|
501
|
+
if (token.status !== 'claimed') {
|
|
502
|
+
throw new VictStoreError('VICT_STORE_TOKEN_CONFLICT', `Token '${token.tokenId}' cannot be rescheduled from state '${token.status}'.`, { operation: 'orchestration.completeAttempt', runId: command.runId });
|
|
503
|
+
}
|
|
504
|
+
const timerId = `timer_retry_${command.attemptId}`;
|
|
505
|
+
stored.timers.set(timerId, {
|
|
506
|
+
timerId,
|
|
507
|
+
runId: command.runId,
|
|
508
|
+
kind: 'retry',
|
|
509
|
+
waitId: null,
|
|
510
|
+
attemptId: attempt.attemptId,
|
|
511
|
+
tokenId: token.tokenId,
|
|
512
|
+
dueAt: continuation.dueAt,
|
|
513
|
+
status: 'scheduled',
|
|
514
|
+
ownerId: null,
|
|
515
|
+
leaseExpiresAt: null,
|
|
516
|
+
revision: 1,
|
|
517
|
+
createdAt: now,
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
else if (continuation.kind === 'block') {
|
|
521
|
+
token.status = 'blocked';
|
|
522
|
+
token.revision += 1;
|
|
523
|
+
token.updatedAt = now;
|
|
524
|
+
}
|
|
525
|
+
else {
|
|
526
|
+
// 'none': the token's work is over (run completed or failed).
|
|
527
|
+
token.status = command.outcome.kind === 'completed' ? 'completed' : token.status;
|
|
528
|
+
}
|
|
529
|
+
// 3. Run state.
|
|
530
|
+
const nextStatus = command.run.status ?? 'running';
|
|
531
|
+
if (!canTransitionRun(previousRun.status, nextStatus)) {
|
|
532
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', `Run status '${previousRun.status}' cannot transition to '${nextStatus}'.`, { operation: 'orchestration.completeAttempt', runId: command.runId });
|
|
533
|
+
}
|
|
534
|
+
stored.run = {
|
|
535
|
+
...stored.run,
|
|
536
|
+
...(command.run.steps !== undefined ? { steps: command.run.steps } : {}),
|
|
537
|
+
...(command.run.currentNodeId !== undefined
|
|
538
|
+
? { currentNodeId: command.run.currentNodeId }
|
|
539
|
+
: {}),
|
|
540
|
+
...(command.run.outputSummary !== undefined
|
|
541
|
+
? { outputSummary: canonicalPersistedValue(command.run.outputSummary) }
|
|
542
|
+
: {}),
|
|
543
|
+
...(command.run.output !== undefined
|
|
544
|
+
? { output: canonicalPersistedValue(command.run.output) }
|
|
545
|
+
: {}),
|
|
546
|
+
...(command.run.error !== undefined
|
|
547
|
+
? { error: canonicalPersistedValue(command.run.error) }
|
|
548
|
+
: {}),
|
|
549
|
+
status: nextStatus,
|
|
550
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
551
|
+
updatedAt: now,
|
|
552
|
+
completedAt: command.run.completedAt !== undefined
|
|
553
|
+
? command.run.completedAt
|
|
554
|
+
: nextStatus === 'running' || nextStatus === 'waiting' || nextStatus === 'blocked'
|
|
555
|
+
? previousRun.completedAt
|
|
556
|
+
: now,
|
|
557
|
+
};
|
|
558
|
+
// 4. Checkpoint lifecycle.
|
|
559
|
+
if (command.checkpoint !== undefined && command.checkpoint !== null) {
|
|
560
|
+
stageCheckpoint(stored, command.checkpoint.tokenId, command.checkpoint.payload);
|
|
561
|
+
}
|
|
562
|
+
for (const child of command.childCheckpoints ?? []) {
|
|
563
|
+
stageCheckpoint(stored, child.tokenId, child.payload);
|
|
564
|
+
}
|
|
565
|
+
for (const tokenId of command.removeCheckpoints ?? []) {
|
|
566
|
+
const token = stored.tokens.get(tokenId);
|
|
567
|
+
if (token) {
|
|
568
|
+
// Tombstone: the token row stays (audit), the private payload goes.
|
|
569
|
+
token.checkpoint = undefined;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
if (['completed', 'failed', 'cancelled'].includes(stored.run.status)) {
|
|
573
|
+
// Terminal cleanup: no private operational payload survives a
|
|
574
|
+
// terminal transition (tested lifecycle rule).
|
|
575
|
+
for (const token of stored.tokens.values()) {
|
|
576
|
+
token.checkpoint = undefined;
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
faults?.afterStateStage?.('orchestration.completeAttempt');
|
|
580
|
+
// 5. Events (dense, atomic with the state). The join.completed fact
|
|
581
|
+
// is supplied by the runtime in the atomic transition that records
|
|
582
|
+
// the VALIDATED join completion — never by the store, because the
|
|
583
|
+
// join's declared output contract must be validated outside the
|
|
584
|
+
// persistence adapter. A final branch arrival only creates the
|
|
585
|
+
// join-ready token (joinFired marks that durable fact).
|
|
586
|
+
appendEvents(stored, command.events);
|
|
587
|
+
faults?.beforeCommit?.('orchestration.completeAttempt');
|
|
588
|
+
return {
|
|
589
|
+
attempt: immutableSnapshot(attempt),
|
|
590
|
+
token: immutableSnapshot(stripCheckpoint(token)),
|
|
591
|
+
branchResult: command.continuation.kind === 'branchArrival'
|
|
592
|
+
? immutableSnapshot(stored.branchResults.at(-1))
|
|
593
|
+
: null,
|
|
594
|
+
joinFired,
|
|
595
|
+
runRecordRevision: stored.run.recordRevision,
|
|
596
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
catch (cause) {
|
|
600
|
+
// Roll the staged transition back: no half-state becomes visible.
|
|
601
|
+
stored.run = previousRun;
|
|
602
|
+
attempt.state = previousAttemptState;
|
|
603
|
+
const restored = { ...previousToken };
|
|
604
|
+
token.status = restored.status;
|
|
605
|
+
token.nodeId = restored.nodeId;
|
|
606
|
+
token.revision = restored.revision;
|
|
607
|
+
token.updatedAt = restored.updatedAt;
|
|
608
|
+
token.checkpoint = restored.checkpoint;
|
|
609
|
+
for (const child of previousChildTokens) {
|
|
610
|
+
stored.tokens.delete(child.tokenId);
|
|
611
|
+
}
|
|
612
|
+
for (const { token: sibling, status } of previousSiblingTokens) {
|
|
613
|
+
const current = stored.tokens.get(sibling.tokenId);
|
|
614
|
+
if (current) {
|
|
615
|
+
current.status = status;
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
if (command.continuation.kind === 'branchArrival') {
|
|
619
|
+
stored.branchResults.pop();
|
|
620
|
+
stored.branchOutputs = previousBranchOutputs;
|
|
621
|
+
}
|
|
622
|
+
for (const { wait, status } of previousWaits) {
|
|
623
|
+
wait.status = status;
|
|
624
|
+
}
|
|
625
|
+
const rolledBack = command.continuation;
|
|
626
|
+
if (rolledBack.kind === 'wait') {
|
|
627
|
+
stored.waits.delete(rolledBack.wait.waitId);
|
|
628
|
+
stored.timers.delete(`timer_${rolledBack.wait.waitId}`);
|
|
629
|
+
stored.timers.delete(`timer_timeout_${rolledBack.wait.waitId}`);
|
|
630
|
+
}
|
|
631
|
+
if (rolledBack.kind === 'retry') {
|
|
632
|
+
stored.timers.delete(`timer_retry_${command.attemptId}`);
|
|
633
|
+
}
|
|
634
|
+
for (const { timer, status } of previousTimers) {
|
|
635
|
+
timer.status = status;
|
|
636
|
+
}
|
|
637
|
+
throw wrap(cause, 'orchestration.completeAttempt', command.runId);
|
|
638
|
+
}
|
|
639
|
+
},
|
|
640
|
+
async signalWait(command) {
|
|
641
|
+
const stored = requireRun(command.runId, 'orchestration.signalWait');
|
|
642
|
+
const existing = stored.signalReceipts.get(command.signalId);
|
|
643
|
+
if (existing) {
|
|
644
|
+
if (existing.commandHash === command.commandHash) {
|
|
645
|
+
return {
|
|
646
|
+
status: 'duplicate',
|
|
647
|
+
signalId: command.signalId,
|
|
648
|
+
waitId: existing.waitId ?? command.waitId,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
return { status: 'conflict', signalId: command.signalId };
|
|
652
|
+
}
|
|
653
|
+
const wait = stored.waits.get(command.waitId);
|
|
654
|
+
if (!wait) {
|
|
655
|
+
throw new VictStoreError('VICT_STORE_WAIT_NOT_FOUND', 'Wait not found.', {
|
|
656
|
+
operation: 'orchestration.signalWait',
|
|
657
|
+
runId: command.runId,
|
|
658
|
+
waitId: command.waitId,
|
|
659
|
+
});
|
|
660
|
+
}
|
|
661
|
+
if (wait.status !== 'open') {
|
|
662
|
+
return { status: 'already_resolved', waitId: wait.waitId };
|
|
663
|
+
}
|
|
664
|
+
if (command.signalName !== undefined &&
|
|
665
|
+
wait.signalName !== null &&
|
|
666
|
+
wait.signalName !== command.signalName) {
|
|
667
|
+
throw new VictStoreError('VICT_STORE_SIGNAL_NAME_MISMATCH', 'The signal name does not match the open wait.', { operation: 'orchestration.signalWait', runId: command.runId, waitId: command.waitId });
|
|
668
|
+
}
|
|
669
|
+
if (command.expectedWaitRevision !== undefined &&
|
|
670
|
+
wait.revision !== command.expectedWaitRevision) {
|
|
671
|
+
return { status: 'already_resolved', waitId: wait.waitId };
|
|
672
|
+
}
|
|
673
|
+
const token = stored.tokens.get(wait.tokenId);
|
|
674
|
+
if (!token) {
|
|
675
|
+
throw new VictStoreError('VICT_STORE_INVALID_RECORD', 'The wait references an unknown token.', {
|
|
676
|
+
operation: 'orchestration.signalWait',
|
|
677
|
+
runId: command.runId,
|
|
678
|
+
waitId: command.waitId,
|
|
679
|
+
});
|
|
680
|
+
}
|
|
681
|
+
const now = command.now;
|
|
682
|
+
const previousRun = stored.run;
|
|
683
|
+
try {
|
|
684
|
+
wait.status = 'resolved';
|
|
685
|
+
wait.revision += 1;
|
|
686
|
+
wait.resolvedAt = now;
|
|
687
|
+
wait.resolvedBy = command.signalId;
|
|
688
|
+
if (!canTransitionToken(token.status, 'ready')) {
|
|
689
|
+
throw new VictStoreError('VICT_STORE_WAIT_CONFLICT', 'The wait lost the race against a concurrent resolution.', { operation: 'orchestration.signalWait', runId: command.runId, waitId: command.waitId });
|
|
690
|
+
}
|
|
691
|
+
token.status = 'ready';
|
|
692
|
+
token.revision += 1;
|
|
693
|
+
token.updatedAt = now;
|
|
694
|
+
// The resolved signal payload becomes the token's private
|
|
695
|
+
// operational checkpoint (it is the continuation input).
|
|
696
|
+
token.checkpoint = canonicalPersistedValue(command.payload);
|
|
697
|
+
// Cancel the wait's outstanding timeout timer, if any.
|
|
698
|
+
for (const timer of stored.timers.values()) {
|
|
699
|
+
if (timer.waitId === command.waitId && timer.status === 'scheduled') {
|
|
700
|
+
timer.status = 'cancelled';
|
|
701
|
+
timer.revision += 1;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
stored.signalReceipts.set(command.signalId, {
|
|
705
|
+
signalId: command.signalId,
|
|
706
|
+
runId: command.runId,
|
|
707
|
+
waitId: command.waitId,
|
|
708
|
+
signalName: wait.signalName,
|
|
709
|
+
commandHash: command.commandHash,
|
|
710
|
+
status: 'accepted',
|
|
711
|
+
eventSeq: null,
|
|
712
|
+
createdAt: now,
|
|
713
|
+
});
|
|
714
|
+
stored.run = {
|
|
715
|
+
...stored.run,
|
|
716
|
+
status: 'running',
|
|
717
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
718
|
+
updatedAt: now,
|
|
719
|
+
};
|
|
720
|
+
faults?.afterStateStage?.('orchestration.signalWait');
|
|
721
|
+
appendEvents(stored, command.events);
|
|
722
|
+
faults?.beforeCommit?.('orchestration.signalWait');
|
|
723
|
+
return {
|
|
724
|
+
status: 'accepted',
|
|
725
|
+
waitId: command.waitId,
|
|
726
|
+
token: immutableSnapshot(stripCheckpoint(token)),
|
|
727
|
+
runRecordRevision: stored.run.recordRevision,
|
|
728
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
729
|
+
waitRevision: wait.revision,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
catch (cause) {
|
|
733
|
+
stored.run = previousRun;
|
|
734
|
+
wait.status = 'open';
|
|
735
|
+
wait.resolvedAt = null;
|
|
736
|
+
wait.resolvedBy = null;
|
|
737
|
+
wait.revision = Math.max(1, wait.revision - 1);
|
|
738
|
+
token.status = 'waiting';
|
|
739
|
+
stored.signalReceipts.delete(command.signalId);
|
|
740
|
+
throw wrap(cause, 'orchestration.signalWait', command.runId);
|
|
741
|
+
}
|
|
742
|
+
},
|
|
743
|
+
async claimDueTimers(command) {
|
|
744
|
+
const now = command.now;
|
|
745
|
+
const pool = command.runId !== undefined
|
|
746
|
+
? [requireRun(command.runId, 'orchestration.claimDueTimers')]
|
|
747
|
+
: [...runs.values()];
|
|
748
|
+
const candidates = [];
|
|
749
|
+
for (const stored of pool) {
|
|
750
|
+
for (const timer of stored.timers.values()) {
|
|
751
|
+
if (timer.status === 'scheduled' && timer.dueAt <= now) {
|
|
752
|
+
candidates.push(timer);
|
|
753
|
+
}
|
|
754
|
+
else if (
|
|
755
|
+
// A pump that failed mid-resolution leaves the timer 'firing'
|
|
756
|
+
// with a held lease; once that lease lapses the timer must be
|
|
757
|
+
// reclaimable, or the wake would be lost forever.
|
|
758
|
+
timer.status === 'firing' &&
|
|
759
|
+
timer.leaseExpiresAt !== null &&
|
|
760
|
+
timer.leaseExpiresAt <= now) {
|
|
761
|
+
candidates.push(timer);
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
candidates.sort((a, b) => a.dueAt - b.dueAt || (a.timerId < b.timerId ? -1 : a.timerId > b.timerId ? 1 : 0));
|
|
766
|
+
const due = [];
|
|
767
|
+
for (const timer of candidates) {
|
|
768
|
+
if (due.length >= Math.max(1, command.limit)) {
|
|
769
|
+
break;
|
|
770
|
+
}
|
|
771
|
+
timer.status = 'firing';
|
|
772
|
+
timer.ownerId = command.ownerId;
|
|
773
|
+
timer.leaseExpiresAt = command.leaseExpiresAt;
|
|
774
|
+
timer.revision += 1;
|
|
775
|
+
due.push({
|
|
776
|
+
timerId: timer.timerId,
|
|
777
|
+
runId: timer.runId,
|
|
778
|
+
kind: timer.kind,
|
|
779
|
+
waitId: timer.waitId,
|
|
780
|
+
attemptId: timer.attemptId,
|
|
781
|
+
tokenId: timer.tokenId,
|
|
782
|
+
dueAt: timer.dueAt,
|
|
783
|
+
revision: timer.revision,
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
return { timers: due.map((timer) => immutableSnapshot(timer)) };
|
|
787
|
+
},
|
|
788
|
+
async resolveDueTimer(command) {
|
|
789
|
+
const stored = requireRun(command.runId, 'orchestration.resolveDueTimer');
|
|
790
|
+
const timer = stored.timers.get(command.timerId);
|
|
791
|
+
if (!timer) {
|
|
792
|
+
throw new VictStoreError('VICT_STORE_TIMER_NOT_FOUND', 'Timer not found.', {
|
|
793
|
+
operation: 'orchestration.resolveDueTimer',
|
|
794
|
+
runId: command.runId,
|
|
795
|
+
timerId: command.timerId,
|
|
796
|
+
});
|
|
797
|
+
}
|
|
798
|
+
if (timer.revision !== command.expectedTimerFence || timer.ownerId !== command.ownerId) {
|
|
799
|
+
return {
|
|
800
|
+
runRecordRevision: stored.run.recordRevision,
|
|
801
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
802
|
+
applied: false,
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
if (timer.status !== 'firing') {
|
|
806
|
+
return {
|
|
807
|
+
runRecordRevision: stored.run.recordRevision,
|
|
808
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
809
|
+
applied: false,
|
|
810
|
+
};
|
|
811
|
+
}
|
|
812
|
+
const now = command.now;
|
|
813
|
+
const previousRun = stored.run;
|
|
814
|
+
const targetWait = timer.waitId !== null ? stored.waits.get(timer.waitId) : undefined;
|
|
815
|
+
const targetToken = timer.tokenId !== null ? stored.tokens.get(timer.tokenId) : undefined;
|
|
816
|
+
const previousWaitStatus = targetWait?.status;
|
|
817
|
+
const previousTokenStatus = targetToken?.status;
|
|
818
|
+
try {
|
|
819
|
+
if (command.resolution.kind === 'wake' || command.resolution.kind === 'waitTimeout') {
|
|
820
|
+
if (!targetWait || !targetToken || targetWait.status !== 'open') {
|
|
821
|
+
return {
|
|
822
|
+
runRecordRevision: stored.run.recordRevision,
|
|
823
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
824
|
+
applied: false,
|
|
825
|
+
};
|
|
826
|
+
}
|
|
827
|
+
targetWait.status = 'resolved';
|
|
828
|
+
targetWait.revision += 1;
|
|
829
|
+
targetWait.resolvedAt = now;
|
|
830
|
+
targetWait.resolvedBy = timer.timerId;
|
|
831
|
+
if (command.resolution.kind === 'wake') {
|
|
832
|
+
targetToken.status = 'ready';
|
|
833
|
+
}
|
|
834
|
+
else if (command.resolution.toNodeId !== null) {
|
|
835
|
+
targetToken.nodeId = command.resolution.toNodeId;
|
|
836
|
+
targetToken.status = 'ready';
|
|
837
|
+
}
|
|
838
|
+
else {
|
|
839
|
+
targetToken.status = 'blocked';
|
|
840
|
+
}
|
|
841
|
+
targetToken.revision += 1;
|
|
842
|
+
targetToken.updatedAt = now;
|
|
843
|
+
}
|
|
844
|
+
if (command.resolution.kind === 'retry') {
|
|
845
|
+
if (!targetToken ||
|
|
846
|
+
(targetToken.status !== 'claimed' && targetToken.status !== 'blocked')) {
|
|
847
|
+
return {
|
|
848
|
+
runRecordRevision: stored.run.recordRevision,
|
|
849
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
850
|
+
applied: false,
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
targetToken.status = 'ready';
|
|
854
|
+
targetToken.revision += 1;
|
|
855
|
+
targetToken.updatedAt = now;
|
|
856
|
+
}
|
|
857
|
+
timer.status = command.resolution.kind === 'cancel' ? 'cancelled' : 'fired';
|
|
858
|
+
if (command.checkpoint !== undefined && command.checkpoint !== null && targetToken) {
|
|
859
|
+
stageCheckpoint(stored, targetToken.tokenId, command.checkpoint.payload);
|
|
860
|
+
}
|
|
861
|
+
stored.run = {
|
|
862
|
+
...stored.run,
|
|
863
|
+
...(command.run ?? {}),
|
|
864
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
865
|
+
updatedAt: now,
|
|
866
|
+
};
|
|
867
|
+
faults?.afterStateStage?.('orchestration.resolveDueTimer');
|
|
868
|
+
appendEvents(stored, command.events);
|
|
869
|
+
faults?.beforeCommit?.('orchestration.resolveDueTimer');
|
|
870
|
+
return {
|
|
871
|
+
runRecordRevision: stored.run.recordRevision,
|
|
872
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
873
|
+
applied: true,
|
|
874
|
+
};
|
|
875
|
+
}
|
|
876
|
+
catch (cause) {
|
|
877
|
+
stored.run = previousRun;
|
|
878
|
+
timer.status = 'firing';
|
|
879
|
+
if (targetWait && previousWaitStatus) {
|
|
880
|
+
targetWait.status = previousWaitStatus;
|
|
881
|
+
targetWait.resolvedAt = null;
|
|
882
|
+
targetWait.resolvedBy = null;
|
|
883
|
+
}
|
|
884
|
+
if (targetToken && previousTokenStatus) {
|
|
885
|
+
targetToken.status = previousTokenStatus;
|
|
886
|
+
}
|
|
887
|
+
throw wrap(cause, 'orchestration.resolveDueTimer', command.runId);
|
|
888
|
+
}
|
|
889
|
+
},
|
|
890
|
+
async requestCancellation(command) {
|
|
891
|
+
const stored = requireRun(command.runId, 'orchestration.requestCancellation');
|
|
892
|
+
const existing = stored.cancellationRequests.get(command.requestId);
|
|
893
|
+
if (existing) {
|
|
894
|
+
if (existing.commandHash === command.commandHash) {
|
|
895
|
+
return {
|
|
896
|
+
status: 'duplicate',
|
|
897
|
+
runId: command.runId,
|
|
898
|
+
runCancelledNow: false,
|
|
899
|
+
runRecordRevision: stored.run.recordRevision,
|
|
900
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
return { status: 'conflict', requestId: command.requestId };
|
|
904
|
+
}
|
|
905
|
+
const runStatus = stored.run.status;
|
|
906
|
+
if (runStatus === 'completed' || runStatus === 'failed' || runStatus === 'cancelled') {
|
|
907
|
+
return { status: 'already_terminal', runId: command.runId, runStatus };
|
|
908
|
+
}
|
|
909
|
+
const now = command.now;
|
|
910
|
+
const previousRun = stored.run;
|
|
911
|
+
const touchedWaits = [];
|
|
912
|
+
const touchedTimers = [];
|
|
913
|
+
const touchedTokens = [];
|
|
914
|
+
try {
|
|
915
|
+
stored.cancellationRequests.set(command.requestId, {
|
|
916
|
+
requestId: command.requestId,
|
|
917
|
+
reasonCode: command.reasonCode,
|
|
918
|
+
commandHash: command.commandHash,
|
|
919
|
+
});
|
|
920
|
+
stored.run = {
|
|
921
|
+
...stored.run,
|
|
922
|
+
cancellation: { requestId: command.requestId, reasonCode: command.reasonCode },
|
|
923
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
924
|
+
updatedAt: now,
|
|
925
|
+
};
|
|
926
|
+
for (const wait of stored.waits.values()) {
|
|
927
|
+
if (wait.status === 'open') {
|
|
928
|
+
touchedWaits.push({ wait, status: wait.status });
|
|
929
|
+
wait.status = 'cancelled';
|
|
930
|
+
wait.revision += 1;
|
|
931
|
+
wait.resolvedAt = now;
|
|
932
|
+
wait.resolvedBy = command.requestId;
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
for (const timer of stored.timers.values()) {
|
|
936
|
+
if (timer.status === 'scheduled') {
|
|
937
|
+
touchedTimers.push({ timer, status: timer.status });
|
|
938
|
+
timer.status = 'cancelled';
|
|
939
|
+
timer.revision += 1;
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
let hasInFlight = false;
|
|
943
|
+
for (const token of stored.tokens.values()) {
|
|
944
|
+
if (token.status === 'ready') {
|
|
945
|
+
touchedTokens.push({ token, status: token.status });
|
|
946
|
+
token.status = 'cancelled';
|
|
947
|
+
token.revision += 1;
|
|
948
|
+
token.updatedAt = now;
|
|
949
|
+
}
|
|
950
|
+
if (token.status === 'claimed') {
|
|
951
|
+
hasInFlight = true;
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
let runCancelledNow = false;
|
|
955
|
+
if (!hasInFlight) {
|
|
956
|
+
if (!canTransitionRun(stored.run.status, 'cancelled')) {
|
|
957
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'The run cannot be cancelled from its current status.', {
|
|
958
|
+
operation: 'orchestration.requestCancellation',
|
|
959
|
+
runId: command.runId,
|
|
960
|
+
status: stored.run.status,
|
|
961
|
+
});
|
|
962
|
+
}
|
|
963
|
+
stored.run = { ...stored.run, status: 'cancelled', completedAt: now };
|
|
964
|
+
runCancelledNow = true;
|
|
965
|
+
}
|
|
966
|
+
faults?.afterStateStage?.('orchestration.requestCancellation');
|
|
967
|
+
const allEvents = runCancelledNow && command.terminalCancelEvent !== undefined
|
|
968
|
+
? [...command.events, command.terminalCancelEvent]
|
|
969
|
+
: command.events;
|
|
970
|
+
appendEvents(stored, allEvents);
|
|
971
|
+
faults?.beforeCommit?.('orchestration.requestCancellation');
|
|
972
|
+
return {
|
|
973
|
+
status: 'accepted',
|
|
974
|
+
runId: command.runId,
|
|
975
|
+
runCancelledNow,
|
|
976
|
+
runRecordRevision: stored.run.recordRevision,
|
|
977
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
978
|
+
};
|
|
979
|
+
}
|
|
980
|
+
catch (cause) {
|
|
981
|
+
stored.run = previousRun;
|
|
982
|
+
stored.cancellationRequests.delete(command.requestId);
|
|
983
|
+
for (const { wait, status } of touchedWaits) {
|
|
984
|
+
wait.status = status;
|
|
985
|
+
wait.resolvedAt = null;
|
|
986
|
+
wait.resolvedBy = null;
|
|
987
|
+
}
|
|
988
|
+
for (const { timer, status } of touchedTimers) {
|
|
989
|
+
timer.status = status;
|
|
990
|
+
}
|
|
991
|
+
for (const { token, status } of touchedTokens) {
|
|
992
|
+
token.status = status;
|
|
993
|
+
}
|
|
994
|
+
throw wrap(cause, 'orchestration.requestCancellation', command.runId);
|
|
995
|
+
}
|
|
996
|
+
},
|
|
997
|
+
async applyCancellation(command) {
|
|
998
|
+
const stored = requireRun(command.runId, 'orchestration.applyCancellation');
|
|
999
|
+
const now = command.now;
|
|
1000
|
+
const previousRun = stored.run;
|
|
1001
|
+
try {
|
|
1002
|
+
for (const token of stored.tokens.values()) {
|
|
1003
|
+
if (['ready', 'claimed', 'waiting', 'blocked'].includes(token.status)) {
|
|
1004
|
+
token.status = 'cancelled';
|
|
1005
|
+
token.revision += 1;
|
|
1006
|
+
token.updatedAt = now;
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
for (const wait of stored.waits.values()) {
|
|
1010
|
+
if (wait.status === 'open') {
|
|
1011
|
+
wait.status = 'cancelled';
|
|
1012
|
+
wait.revision += 1;
|
|
1013
|
+
wait.resolvedAt = now;
|
|
1014
|
+
wait.resolvedBy = command.requestId;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
for (const timer of stored.timers.values()) {
|
|
1018
|
+
if (timer.status === 'scheduled' || timer.status === 'firing') {
|
|
1019
|
+
timer.status = 'cancelled';
|
|
1020
|
+
timer.revision += 1;
|
|
1021
|
+
}
|
|
1022
|
+
}
|
|
1023
|
+
if (stored.run.status === 'cancelled') {
|
|
1024
|
+
// Already finalized: idempotent no-op.
|
|
1025
|
+
return {
|
|
1026
|
+
runRecordRevision: stored.run.recordRevision,
|
|
1027
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
1028
|
+
};
|
|
1029
|
+
}
|
|
1030
|
+
if (!canTransitionRun(stored.run.status, 'cancelled')) {
|
|
1031
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'The run cannot be cancelled from its current status.', {
|
|
1032
|
+
operation: 'orchestration.applyCancellation',
|
|
1033
|
+
runId: command.runId,
|
|
1034
|
+
status: stored.run.status,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
stored.run = {
|
|
1038
|
+
...stored.run,
|
|
1039
|
+
status: 'cancelled',
|
|
1040
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
1041
|
+
updatedAt: now,
|
|
1042
|
+
completedAt: now,
|
|
1043
|
+
};
|
|
1044
|
+
faults?.afterStateStage?.('orchestration.applyCancellation');
|
|
1045
|
+
appendEvents(stored, command.events);
|
|
1046
|
+
for (const token of stored.tokens.values()) {
|
|
1047
|
+
// Terminal cleanup tombstones every private operational payload.
|
|
1048
|
+
token.checkpoint = undefined;
|
|
1049
|
+
}
|
|
1050
|
+
faults?.beforeCommit?.('orchestration.applyCancellation');
|
|
1051
|
+
return {
|
|
1052
|
+
runRecordRevision: stored.run.recordRevision,
|
|
1053
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
1054
|
+
};
|
|
1055
|
+
}
|
|
1056
|
+
catch (cause) {
|
|
1057
|
+
stored.run = previousRun;
|
|
1058
|
+
throw wrap(cause, 'orchestration.applyCancellation', command.runId);
|
|
1059
|
+
}
|
|
1060
|
+
},
|
|
1061
|
+
async findRecoverableClaims(_command) {
|
|
1062
|
+
const recoverable = [];
|
|
1063
|
+
for (const stored of runs.values()) {
|
|
1064
|
+
if (stored.run.status !== 'running') {
|
|
1065
|
+
continue;
|
|
1066
|
+
}
|
|
1067
|
+
for (const token of stored.tokens.values()) {
|
|
1068
|
+
if (token.status !== 'claimed') {
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
const attempt = [...stored.attempts.values()]
|
|
1072
|
+
.filter((candidate) => candidate.tokenId === token.tokenId && candidate.state === 'started')
|
|
1073
|
+
.sort((a, b) => b.attemptNumber - a.attemptNumber)[0];
|
|
1074
|
+
if (attempt && attempt.leaseExpiresAt !== null) {
|
|
1075
|
+
recoverable.push({
|
|
1076
|
+
runId: stored.run.runId,
|
|
1077
|
+
token: immutableSnapshot(stripCheckpoint(token)),
|
|
1078
|
+
attempt: immutableSnapshot(attempt),
|
|
1079
|
+
leaseExpiresAt: attempt.leaseExpiresAt,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
}
|
|
1084
|
+
recoverable.sort((a, b) => a.leaseExpiresAt < b.leaseExpiresAt ? -1 : a.leaseExpiresAt > b.leaseExpiresAt ? 1 : 0);
|
|
1085
|
+
return recoverable;
|
|
1086
|
+
},
|
|
1087
|
+
async recoverAttempt(command) {
|
|
1088
|
+
const stored = requireRun(command.runId, 'orchestration.recoverAttempt');
|
|
1089
|
+
const attempt = stored.attempts.get(command.attemptId);
|
|
1090
|
+
if (!attempt) {
|
|
1091
|
+
throw new VictStoreError('VICT_STORE_INVALID_COMMAND', 'Unknown attempt.', {
|
|
1092
|
+
operation: 'orchestration.recoverAttempt',
|
|
1093
|
+
runId: command.runId,
|
|
1094
|
+
attemptId: command.attemptId,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
if (attempt.fence !== command.expectedAttemptFence) {
|
|
1098
|
+
throw new VictStoreError('VICT_STORE_ATTEMPT_FENCE_CONFLICT', 'The recovery carries a stale fence.', {
|
|
1099
|
+
operation: 'orchestration.recoverAttempt',
|
|
1100
|
+
runId: command.runId,
|
|
1101
|
+
attemptId: command.attemptId,
|
|
1102
|
+
});
|
|
1103
|
+
}
|
|
1104
|
+
const now = command.now;
|
|
1105
|
+
const previousRun = stored.run;
|
|
1106
|
+
const previousAttemptState = attempt.state;
|
|
1107
|
+
const token = stored.tokens.get(attempt.tokenId);
|
|
1108
|
+
const previousTokenStatus = token?.status;
|
|
1109
|
+
try {
|
|
1110
|
+
if (!canTransitionAttempt(attempt.state, 'outcome_unknown')) {
|
|
1111
|
+
throw new VictStoreError('VICT_STORE_ATTEMPT_STATE_CONFLICT', `Attempt '${attempt.attemptId}' is in state '${attempt.state}' and cannot be recovered.`, {
|
|
1112
|
+
operation: 'orchestration.recoverAttempt',
|
|
1113
|
+
runId: command.runId,
|
|
1114
|
+
attemptId: command.attemptId,
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
attempt.state = 'outcome_unknown';
|
|
1118
|
+
attempt.updatedAt = now;
|
|
1119
|
+
if (token) {
|
|
1120
|
+
if (command.action.kind === 'reclaim') {
|
|
1121
|
+
if (!canTransitionToken(token.status, 'ready')) {
|
|
1122
|
+
throw new VictStoreError('VICT_STORE_TOKEN_CONFLICT', `Token '${token.tokenId}' is in state '${token.status}' and cannot be reclaimed.`, { operation: 'orchestration.recoverAttempt', runId: command.runId });
|
|
1123
|
+
}
|
|
1124
|
+
token.status = 'ready';
|
|
1125
|
+
}
|
|
1126
|
+
else {
|
|
1127
|
+
if (!canTransitionToken(token.status, 'blocked')) {
|
|
1128
|
+
throw new VictStoreError('VICT_STORE_TOKEN_CONFLICT', `Token '${token.tokenId}' is in state '${token.status}' and cannot be blocked.`, { operation: 'orchestration.recoverAttempt', runId: command.runId });
|
|
1129
|
+
}
|
|
1130
|
+
token.status = 'blocked';
|
|
1131
|
+
}
|
|
1132
|
+
token.revision += 1;
|
|
1133
|
+
token.updatedAt = now;
|
|
1134
|
+
}
|
|
1135
|
+
stored.run = {
|
|
1136
|
+
...stored.run,
|
|
1137
|
+
...(command.run ?? {}),
|
|
1138
|
+
status: command.action.kind === 'block' ? 'blocked' : 'running',
|
|
1139
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
1140
|
+
updatedAt: now,
|
|
1141
|
+
};
|
|
1142
|
+
faults?.afterStateStage?.('orchestration.recoverAttempt');
|
|
1143
|
+
appendEvents(stored, command.events);
|
|
1144
|
+
faults?.beforeCommit?.('orchestration.recoverAttempt');
|
|
1145
|
+
return {
|
|
1146
|
+
runRecordRevision: stored.run.recordRevision,
|
|
1147
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
catch (cause) {
|
|
1151
|
+
stored.run = previousRun;
|
|
1152
|
+
attempt.state = previousAttemptState;
|
|
1153
|
+
if (token && previousTokenStatus) {
|
|
1154
|
+
token.status = previousTokenStatus;
|
|
1155
|
+
}
|
|
1156
|
+
throw wrap(cause, 'orchestration.recoverAttempt', command.runId);
|
|
1157
|
+
}
|
|
1158
|
+
},
|
|
1159
|
+
async resolveBlocked(command) {
|
|
1160
|
+
const stored = runs.get(command.runId);
|
|
1161
|
+
if (!stored) {
|
|
1162
|
+
return { status: 'unknown_run' };
|
|
1163
|
+
}
|
|
1164
|
+
const existing = stored.resolutionRequests.get(command.resolutionId);
|
|
1165
|
+
if (existing) {
|
|
1166
|
+
if (existing.commandHash === command.commandHash) {
|
|
1167
|
+
return {
|
|
1168
|
+
status: 'duplicate',
|
|
1169
|
+
runRecordRevision: stored.run.recordRevision,
|
|
1170
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
1171
|
+
runStatus: stored.run.status,
|
|
1172
|
+
};
|
|
1173
|
+
}
|
|
1174
|
+
return { status: 'conflict', resolutionId: command.resolutionId };
|
|
1175
|
+
}
|
|
1176
|
+
if (stored.run.status !== 'blocked') {
|
|
1177
|
+
return { status: 'not_blocked', runId: command.runId, runStatus: stored.run.status };
|
|
1178
|
+
}
|
|
1179
|
+
if (stored.run.recordRevision !== command.expectedRunRevision) {
|
|
1180
|
+
return {
|
|
1181
|
+
status: 'stale_revision',
|
|
1182
|
+
runId: command.runId,
|
|
1183
|
+
actualRunRevision: stored.run.recordRevision,
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const now = command.now;
|
|
1187
|
+
const previousRun = stored.run;
|
|
1188
|
+
const blockedToken = [...stored.tokens.values()].find((token) => token.status === 'blocked');
|
|
1189
|
+
const previousBlockedStatus = blockedToken?.status;
|
|
1190
|
+
try {
|
|
1191
|
+
stored.resolutionRequests.set(command.resolutionId, {
|
|
1192
|
+
resolutionId: command.resolutionId,
|
|
1193
|
+
commandHash: command.commandHash,
|
|
1194
|
+
});
|
|
1195
|
+
if (command.action === 'cancel') {
|
|
1196
|
+
if (!canTransitionRun(stored.run.status, 'cancelled')) {
|
|
1197
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'The run cannot be cancelled from its status.', { operation: 'orchestration.resolveBlocked', runId: command.runId });
|
|
1198
|
+
}
|
|
1199
|
+
stored.run = { ...stored.run, status: 'cancelled', completedAt: now };
|
|
1200
|
+
for (const token of stored.tokens.values()) {
|
|
1201
|
+
if (['blocked', 'ready', 'claimed', 'waiting'].includes(token.status)) {
|
|
1202
|
+
token.status = 'cancelled';
|
|
1203
|
+
token.revision += 1;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
}
|
|
1207
|
+
else if (command.action === 'fail') {
|
|
1208
|
+
if (!canTransitionRun(stored.run.status, 'failed')) {
|
|
1209
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'The run cannot fail from its status.', { operation: 'orchestration.resolveBlocked', runId: command.runId });
|
|
1210
|
+
}
|
|
1211
|
+
stored.run = { ...stored.run, status: 'failed', completedAt: now };
|
|
1212
|
+
if (blockedToken) {
|
|
1213
|
+
blockedToken.status = 'cancelled';
|
|
1214
|
+
blockedToken.revision += 1;
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
else {
|
|
1218
|
+
if (!blockedToken) {
|
|
1219
|
+
throw new VictStoreError('VICT_STORE_INVALID_COMMAND', 'The run is blocked without a blocked token.', { operation: 'orchestration.resolveBlocked', runId: command.runId });
|
|
1220
|
+
}
|
|
1221
|
+
if (!canTransitionRun(stored.run.status, 'running')) {
|
|
1222
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'The run cannot resume from its status.', { operation: 'orchestration.resolveBlocked', runId: command.runId });
|
|
1223
|
+
}
|
|
1224
|
+
stored.run = { ...stored.run, status: 'running' };
|
|
1225
|
+
if (command.action === 'retry') {
|
|
1226
|
+
if (!canTransitionToken(blockedToken.status, 'ready')) {
|
|
1227
|
+
throw new VictStoreError('VICT_STORE_TOKEN_CONFLICT', 'The blocked token cannot be retried.', { operation: 'orchestration.resolveBlocked', runId: command.runId });
|
|
1228
|
+
}
|
|
1229
|
+
blockedToken.status = 'ready';
|
|
1230
|
+
blockedToken.revision += 1;
|
|
1231
|
+
}
|
|
1232
|
+
if (command.action === 'confirm_applied' && command.continuation) {
|
|
1233
|
+
if (command.continuation.kind === 'advance') {
|
|
1234
|
+
blockedToken.nodeId = command.continuation.toNodeId;
|
|
1235
|
+
blockedToken.status = 'ready';
|
|
1236
|
+
blockedToken.revision += 1;
|
|
1237
|
+
if (command.checkpoint) {
|
|
1238
|
+
stageCheckpoint(stored, command.checkpoint.tokenId, command.checkpoint.payload);
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
else if (command.continuation.kind === 'complete') {
|
|
1242
|
+
blockedToken.status = 'completed';
|
|
1243
|
+
blockedToken.revision += 1;
|
|
1244
|
+
if (!canTransitionRun('running', 'completed')) {
|
|
1245
|
+
throw new VictStoreError('VICT_STORE_RUN_CONFLICT', 'The run cannot complete from its status.', { operation: 'orchestration.resolveBlocked', runId: command.runId });
|
|
1246
|
+
}
|
|
1247
|
+
stored.run = {
|
|
1248
|
+
...stored.run,
|
|
1249
|
+
status: 'completed',
|
|
1250
|
+
completedAt: now,
|
|
1251
|
+
...(command.continuation.outputSummary !== undefined
|
|
1252
|
+
? {
|
|
1253
|
+
outputSummary: canonicalPersistedValue(command.continuation.outputSummary),
|
|
1254
|
+
}
|
|
1255
|
+
: {}),
|
|
1256
|
+
};
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
}
|
|
1260
|
+
stored.run = {
|
|
1261
|
+
...stored.run,
|
|
1262
|
+
recordRevision: stored.run.recordRevision + 1,
|
|
1263
|
+
updatedAt: now,
|
|
1264
|
+
};
|
|
1265
|
+
faults?.afterStateStage?.('orchestration.resolveBlocked');
|
|
1266
|
+
appendEvents(stored, command.events);
|
|
1267
|
+
faults?.beforeCommit?.('orchestration.resolveBlocked');
|
|
1268
|
+
return {
|
|
1269
|
+
status: 'accepted',
|
|
1270
|
+
runRecordRevision: stored.run.recordRevision,
|
|
1271
|
+
runNextEventSeq: nextEventSeq(stored),
|
|
1272
|
+
runStatus: stored.run.status,
|
|
1273
|
+
};
|
|
1274
|
+
}
|
|
1275
|
+
catch (cause) {
|
|
1276
|
+
stored.run = previousRun;
|
|
1277
|
+
stored.resolutionRequests.delete(command.resolutionId);
|
|
1278
|
+
if (blockedToken && previousBlockedStatus) {
|
|
1279
|
+
blockedToken.status = previousBlockedStatus;
|
|
1280
|
+
}
|
|
1281
|
+
throw wrap(cause, 'orchestration.resolveBlocked', command.runId);
|
|
1282
|
+
}
|
|
1283
|
+
},
|
|
1284
|
+
async listWaits(runId) {
|
|
1285
|
+
const stored = requireRun(runId, 'orchestration.listWaits');
|
|
1286
|
+
return [...stored.waits.values()].map((wait) => immutableSnapshot(wait));
|
|
1287
|
+
},
|
|
1288
|
+
async listSignalReceipts(runId) {
|
|
1289
|
+
const stored = requireRun(runId, 'orchestration.listSignalReceipts');
|
|
1290
|
+
return [...stored.signalReceipts.values()].map((receipt) => immutableSnapshot(receipt));
|
|
1291
|
+
},
|
|
1292
|
+
async listOrchestrationEvents(runId) {
|
|
1293
|
+
const stored = requireRun(runId, 'orchestration.listEvents');
|
|
1294
|
+
return stored.events.map((event) => immutableSnapshot(event));
|
|
1295
|
+
},
|
|
1296
|
+
};
|
|
1297
|
+
return store;
|
|
1298
|
+
}
|
|
1299
|
+
function outcomeState(outcome) {
|
|
1300
|
+
switch (outcome.kind) {
|
|
1301
|
+
case 'completed':
|
|
1302
|
+
return 'completed';
|
|
1303
|
+
case 'failed':
|
|
1304
|
+
return 'failed';
|
|
1305
|
+
case 'timed_out':
|
|
1306
|
+
return 'timed_out';
|
|
1307
|
+
case 'cancelled':
|
|
1308
|
+
return 'cancelled';
|
|
1309
|
+
default:
|
|
1310
|
+
return 'outcome_unknown';
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
//# sourceMappingURL=orchestration-in-memory.js.map
|