@affordance/core 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/LICENSE +21 -0
- package/README.md +52 -0
- package/dist/engine/compute.d.ts +147 -0
- package/dist/engine/compute.js +152 -0
- package/dist/engine/compute.js.map +1 -0
- package/dist/engine/engine.d.ts +162 -0
- package/dist/engine/engine.js +115 -0
- package/dist/engine/engine.js.map +1 -0
- package/dist/engine/errors.d.ts +6 -0
- package/dist/engine/errors.js +11 -0
- package/dist/engine/errors.js.map +1 -0
- package/dist/engine/index.d.ts +16 -0
- package/dist/engine/index.js +15 -0
- package/dist/engine/index.js.map +1 -0
- package/dist/errors.d.ts +46 -0
- package/dist/errors.js +42 -0
- package/dist/errors.js.map +1 -0
- package/dist/execution/delta.d.ts +42 -0
- package/dist/execution/delta.js +93 -0
- package/dist/execution/delta.js.map +1 -0
- package/dist/execution/errors.d.ts +94 -0
- package/dist/execution/errors.js +132 -0
- package/dist/execution/errors.js.map +1 -0
- package/dist/execution/execute.d.ts +179 -0
- package/dist/execution/execute.js +356 -0
- package/dist/execution/execute.js.map +1 -0
- package/dist/execution/index.d.ts +25 -0
- package/dist/execution/index.js +20 -0
- package/dist/execution/index.js.map +1 -0
- package/dist/execution/journal.d.ts +190 -0
- package/dist/execution/journal.js +192 -0
- package/dist/execution/journal.js.map +1 -0
- package/dist/execution/port.d.ts +81 -0
- package/dist/execution/port.js +101 -0
- package/dist/execution/port.js.map +1 -0
- package/dist/execution/replay.d.ts +58 -0
- package/dist/execution/replay.js +59 -0
- package/dist/execution/replay.js.map +1 -0
- package/dist/execution/timers.d.ts +21 -0
- package/dist/execution/timers.js +26 -0
- package/dist/execution/timers.js.map +1 -0
- package/dist/execution/transaction.d.ts +24 -0
- package/dist/execution/transaction.js +49 -0
- package/dist/execution/transaction.js.map +1 -0
- package/dist/guards/condition.d.ts +86 -0
- package/dist/guards/condition.js +11 -0
- package/dist/guards/condition.js.map +1 -0
- package/dist/guards/evaluate.d.ts +143 -0
- package/dist/guards/evaluate.js +167 -0
- package/dist/guards/evaluate.js.map +1 -0
- package/dist/guards/guard.d.ts +136 -0
- package/dist/guards/guard.js +118 -0
- package/dist/guards/guard.js.map +1 -0
- package/dist/guards/index.d.ts +20 -0
- package/dist/guards/index.js +17 -0
- package/dist/guards/index.js.map +1 -0
- package/dist/guards/time.d.ts +32 -0
- package/dist/guards/time.js +45 -0
- package/dist/guards/time.js.map +1 -0
- package/dist/index.d.ts +31 -0
- package/dist/index.js +20 -0
- package/dist/index.js.map +1 -0
- package/dist/ingestion/correlation.d.ts +54 -0
- package/dist/ingestion/correlation.js +78 -0
- package/dist/ingestion/correlation.js.map +1 -0
- package/dist/ingestion/index.d.ts +16 -0
- package/dist/ingestion/index.js +15 -0
- package/dist/ingestion/index.js.map +1 -0
- package/dist/ingestion/ingest.d.ts +196 -0
- package/dist/ingestion/ingest.js +275 -0
- package/dist/ingestion/ingest.js.map +1 -0
- package/dist/migration/index.d.ts +10 -0
- package/dist/migration/index.js +10 -0
- package/dist/migration/index.js.map +1 -0
- package/dist/migration/migrate.d.ts +119 -0
- package/dist/migration/migrate.js +195 -0
- package/dist/migration/migrate.js.map +1 -0
- package/dist/model/casetype.d.ts +53 -0
- package/dist/model/casetype.js +51 -0
- package/dist/model/casetype.js.map +1 -0
- package/dist/model/errors.d.ts +28 -0
- package/dist/model/errors.js +39 -0
- package/dist/model/errors.js.map +1 -0
- package/dist/model/handler.d.ts +120 -0
- package/dist/model/handler.js +16 -0
- package/dist/model/handler.js.map +1 -0
- package/dist/model/index.d.ts +24 -0
- package/dist/model/index.js +19 -0
- package/dist/model/index.js.map +1 -0
- package/dist/model/retry.d.ts +40 -0
- package/dist/model/retry.js +51 -0
- package/dist/model/retry.js.map +1 -0
- package/dist/model/scope.d.ts +67 -0
- package/dist/model/scope.js +19 -0
- package/dist/model/scope.js.map +1 -0
- package/dist/model/step.d.ts +232 -0
- package/dist/model/step.js +210 -0
- package/dist/model/step.js.map +1 -0
- package/dist/model/target.d.ts +175 -0
- package/dist/model/target.js +236 -0
- package/dist/model/target.js.map +1 -0
- package/dist/store/bootstrap.d.ts +57 -0
- package/dist/store/bootstrap.js +268 -0
- package/dist/store/bootstrap.js.map +1 -0
- package/dist/store/errors.d.ts +18 -0
- package/dist/store/errors.js +26 -0
- package/dist/store/errors.js.map +1 -0
- package/dist/store/ids.d.ts +15 -0
- package/dist/store/ids.js +12 -0
- package/dist/store/ids.js.map +1 -0
- package/dist/store/index.d.ts +12 -0
- package/dist/store/index.js +9 -0
- package/dist/store/index.js.map +1 -0
- package/dist/store/queryable.d.ts +60 -0
- package/dist/store/queryable.js +7 -0
- package/dist/store/queryable.js.map +1 -0
- package/dist/store/resolve.d.ts +66 -0
- package/dist/store/resolve.js +71 -0
- package/dist/store/resolve.js.map +1 -0
- package/dist/store/sql.d.ts +26 -0
- package/dist/store/sql.js +21 -0
- package/dist/store/sql.js.map +1 -0
- package/dist/store/store.d.ts +66 -0
- package/dist/store/store.js +108 -0
- package/dist/store/store.js.map +1 -0
- package/package.json +61 -0
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution lifecycle: **claim → run → commit**.
|
|
3
|
+
*
|
|
4
|
+
* ```
|
|
5
|
+
* ┌── transaction ──────────────┐ ┌── transaction ──────────────┐
|
|
6
|
+
* │ lock the case row │ │ lock the case row │
|
|
7
|
+
* │ take over an expired claim │ handler runs │ verify the claim is ours │
|
|
8
|
+
* │ re-evaluate the guard ←────┼── here, outside ───┼→ write state, bump seq │
|
|
9
|
+
* │ insert the claim (lease) │ any transaction │ run ctx.onCommit writes │
|
|
10
|
+
* │ journal `claimed` │ (heartbeating) │ journal `completed` │
|
|
11
|
+
* └─────────────────────────────┘ └─────────────────────────────┘
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Three things this shape buys, each load-bearing:
|
|
15
|
+
*
|
|
16
|
+
* 1. **Guards advise, handlers enforce.** The affordance a client rendered is
|
|
17
|
+
* advice; the claim's transactional re-evaluation is the enforcement
|
|
18
|
+
* moment. State may have moved (another Execution committed) or the
|
|
19
|
+
* definitions may have (a deploy — definition drift is the
|
|
20
|
+
* same race, handled the same way). A claim that fails rejects with the
|
|
21
|
+
* *current* unmet conditions and writes nothing at all.
|
|
22
|
+
* 2. **No transaction spans a handler.** Handlers call the outside world;
|
|
23
|
+
* a transaction held across an escrow API call would tie up a pooled
|
|
24
|
+
* connection and the case row's lock for as long as the external service
|
|
25
|
+
* takes to answer. The lease (a `claims` row keyed by case id) carries
|
|
26
|
+
* the exclusivity instead, and a heartbeat keeps it alive.
|
|
27
|
+
* 3. **A crashed handler cannot deadlock a case.** The lease expires. The
|
|
28
|
+
* next claimant takes the case over, journaling the abandonment; if the
|
|
29
|
+
* zombie ever comes back to commit, it is refused ({@link ClaimLostError})
|
|
30
|
+
* — at-least-once effects are the handler's problem to deduplicate on
|
|
31
|
+
* `ctx.executionId`, but a stale state write is the framework's to refuse.
|
|
32
|
+
*/
|
|
33
|
+
import { thrownMessage, toError } from '../errors.js';
|
|
34
|
+
import { toIso } from '../guards/index.js';
|
|
35
|
+
import { registerCorrelation } from '../ingestion/correlation.js';
|
|
36
|
+
import { evaluateTarget, normalizeRetry, resolveTarget, validateStepInput, } from '../model/index.js';
|
|
37
|
+
import { mintId, validateCaseState } from '../store/index.js';
|
|
38
|
+
import { diffState } from './delta.js';
|
|
39
|
+
import { CaseBusyError, ClaimLostError, StepExecutionError, StepNotAvailableError, } from './errors.js';
|
|
40
|
+
import { pgLifecyclePort } from './port.js';
|
|
41
|
+
/** How long a claim survives without a heartbeat, and how often the heartbeat beats. */
|
|
42
|
+
export const DEFAULT_CLAIM_TTL_MS = 30_000;
|
|
43
|
+
export const DEFAULT_HEARTBEAT_MS = 5_000;
|
|
44
|
+
/**
|
|
45
|
+
* Wraps a failure that must not be retried because retrying is guaranteed to
|
|
46
|
+
* reproduce it: a handler returning a Case State its schema rejects, or a
|
|
47
|
+
* commit refused because the claim is gone.
|
|
48
|
+
*/
|
|
49
|
+
class NonRetryable extends Error {
|
|
50
|
+
reason;
|
|
51
|
+
constructor(reason) {
|
|
52
|
+
super(thrownMessage(reason));
|
|
53
|
+
this.name = 'NonRetryable';
|
|
54
|
+
this.reason = reason;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
const toJournalError = (error) => error instanceof Error
|
|
58
|
+
? { name: error.name, message: error.message }
|
|
59
|
+
: { name: 'UnknownError', message: String(error) };
|
|
60
|
+
/**
|
|
61
|
+
* Resolve whatever claim already sits on this case, inside the claim
|
|
62
|
+
* transaction (the case row is locked, so no two claimants decide this at
|
|
63
|
+
* once): a live claim means the case is busy; an expired one is a crashed
|
|
64
|
+
* handler's abandoned lease — journal the abandonment and take it.
|
|
65
|
+
*
|
|
66
|
+
* Both the `expired` entry and the takeover ride on this transaction, so a
|
|
67
|
+
* claim that goes on to fail its guard rolls the abandonment record back and
|
|
68
|
+
* leaves the dead lease in place. That is deliberate: an expired lease blocks
|
|
69
|
+
* nothing, and the abandonment is only a fact worth recording when somebody
|
|
70
|
+
* actually took the case over.
|
|
71
|
+
*/
|
|
72
|
+
const clearStaleClaim = async (tx, caseId, claimant) => {
|
|
73
|
+
const held = await tx.currentClaim();
|
|
74
|
+
if (!held)
|
|
75
|
+
return;
|
|
76
|
+
if (!held.expired) {
|
|
77
|
+
throw new CaseBusyError(caseId, {
|
|
78
|
+
executionId: held.executionId,
|
|
79
|
+
stepName: held.step,
|
|
80
|
+
scopeKey: held.scopeKey,
|
|
81
|
+
expiresAt: held.expiresAt,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
await tx.appendEntry({
|
|
85
|
+
caseId,
|
|
86
|
+
executionId: held.executionId,
|
|
87
|
+
entry: 'expired',
|
|
88
|
+
attempt: held.attempt,
|
|
89
|
+
step: held.step,
|
|
90
|
+
scopeKey: held.scopeKey,
|
|
91
|
+
error: {
|
|
92
|
+
name: 'ClaimExpiredError',
|
|
93
|
+
message: `claim expired at ${held.expiresAt} with no terminal entry; case taken over by execution ${claimant}`,
|
|
94
|
+
},
|
|
95
|
+
});
|
|
96
|
+
await tx.deleteClaim(held.executionId);
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* The claim: one short transaction that either takes the case and journals a
|
|
100
|
+
* `claimed` entry, or writes nothing and throws the reason. Loading, guard
|
|
101
|
+
* re-evaluation, and the exclusivity decision all happen under the case row's
|
|
102
|
+
* lock, which is what makes "exactly one of N concurrent attempts claims"
|
|
103
|
+
* true rather than likely.
|
|
104
|
+
*/
|
|
105
|
+
const claimCase = async (port, deps, caseId, stepName, options, executionId, claimTtlMs) => port.withCaseLock(caseId, async (tx) => {
|
|
106
|
+
const { definition, handle, state } = await tx.loadCase();
|
|
107
|
+
await clearStaleClaim(tx, caseId, executionId);
|
|
108
|
+
const target = resolveTarget(definition, state, stepName, options.scopeKey);
|
|
109
|
+
const scopeKey = target.binding?.key ?? null;
|
|
110
|
+
const input = await validateStepInput(target.step, options.input);
|
|
111
|
+
const asOf = toIso(options.asOf ?? deps.now());
|
|
112
|
+
const guard = evaluateTarget(target, { actor: options.actor, asOf });
|
|
113
|
+
if (!guard.available)
|
|
114
|
+
throw new StepNotAvailableError(caseId, stepName, scopeKey, guard);
|
|
115
|
+
const { claimedAt } = await tx.insertClaim(executionId, stepName, scopeKey, claimTtlMs);
|
|
116
|
+
await tx.appendEntry({
|
|
117
|
+
caseId,
|
|
118
|
+
executionId,
|
|
119
|
+
entry: 'claimed',
|
|
120
|
+
attempt: 1,
|
|
121
|
+
step: stepName,
|
|
122
|
+
scopeKey,
|
|
123
|
+
actor: options.actor,
|
|
124
|
+
input,
|
|
125
|
+
asOf,
|
|
126
|
+
guard,
|
|
127
|
+
// The Case State the guard was evaluated against, stored so audit
|
|
128
|
+
// reconstruction is exact rather than approximate.
|
|
129
|
+
state,
|
|
130
|
+
});
|
|
131
|
+
return {
|
|
132
|
+
executionId,
|
|
133
|
+
caseTypeName: handle.caseTypeName,
|
|
134
|
+
definition,
|
|
135
|
+
target,
|
|
136
|
+
state,
|
|
137
|
+
input,
|
|
138
|
+
asOf,
|
|
139
|
+
guard,
|
|
140
|
+
claimedAt,
|
|
141
|
+
scopeKey,
|
|
142
|
+
actor: options.actor,
|
|
143
|
+
};
|
|
144
|
+
});
|
|
145
|
+
/**
|
|
146
|
+
* The commit: the new Case State, the app's own writes, and the `completed`
|
|
147
|
+
* journal entry, all in one transaction — or none of them.
|
|
148
|
+
*/
|
|
149
|
+
const commitExecution = async (port, claim, caseId, stepName, nextState, attempt, dormancy, writes) => {
|
|
150
|
+
// Both inputs are fixed before the transaction opens, so the deep diff —
|
|
151
|
+
// pure CPU over the whole state document — runs before the row lock is
|
|
152
|
+
// taken, not while holding it.
|
|
153
|
+
const delta = diffState(claim.state, nextState);
|
|
154
|
+
return port.withCaseLock(caseId, async (tx) => {
|
|
155
|
+
await tx.lockCase();
|
|
156
|
+
// The one check that makes a stale write impossible. Changing Case State
|
|
157
|
+
// requires holding the claim, and taking the claim over replaces this row
|
|
158
|
+
// — so finding our own execution id here means nothing has committed on
|
|
159
|
+
// this case since we claimed it, and the state the handler computed from
|
|
160
|
+
// is still current. An expired-but-undisturbed lease therefore commits
|
|
161
|
+
// quite legitimately: a handler that overran its lease with no contender
|
|
162
|
+
// has raced nobody.
|
|
163
|
+
const holder = (await tx.currentClaim())?.executionId ?? null;
|
|
164
|
+
if (holder !== claim.executionId) {
|
|
165
|
+
throw new ClaimLostError(caseId, claim.executionId, holder);
|
|
166
|
+
}
|
|
167
|
+
const updated = await tx.updateCaseState(nextState, dormancy);
|
|
168
|
+
await tx.appWrites(writes);
|
|
169
|
+
const entry = await tx.appendEntry({
|
|
170
|
+
caseId,
|
|
171
|
+
executionId: claim.executionId,
|
|
172
|
+
entry: 'completed',
|
|
173
|
+
attempt,
|
|
174
|
+
step: stepName,
|
|
175
|
+
scopeKey: claim.scopeKey,
|
|
176
|
+
actor: claim.actor,
|
|
177
|
+
delta,
|
|
178
|
+
dormancy,
|
|
179
|
+
});
|
|
180
|
+
await tx.deleteClaim(claim.executionId);
|
|
181
|
+
return {
|
|
182
|
+
executionId: claim.executionId,
|
|
183
|
+
caseId,
|
|
184
|
+
caseTypeName: claim.caseTypeName,
|
|
185
|
+
step: stepName,
|
|
186
|
+
...(claim.scopeKey !== null && { scopeKey: claim.scopeKey }),
|
|
187
|
+
attempts: attempt,
|
|
188
|
+
guard: claim.guard,
|
|
189
|
+
state: nextState,
|
|
190
|
+
delta,
|
|
191
|
+
seq: updated.seq,
|
|
192
|
+
dormancy,
|
|
193
|
+
endedAt: updated.endedAt,
|
|
194
|
+
claimedAt: claim.claimedAt,
|
|
195
|
+
committedAt: entry.recordedAt,
|
|
196
|
+
};
|
|
197
|
+
});
|
|
198
|
+
};
|
|
199
|
+
/**
|
|
200
|
+
* The lifecycle core: claim → run → commit against a {@link LifecyclePort}.
|
|
201
|
+
*
|
|
202
|
+
* {@link executeStep} binds this to the pg port; the claim state machine's
|
|
203
|
+
* own tests bind it to an in-memory port instead. Same body either way — the
|
|
204
|
+
* port is the only storage the lifecycle knows.
|
|
205
|
+
*/
|
|
206
|
+
export const runLifecycle = async (port, deps, caseId, stepName, options) => {
|
|
207
|
+
const executionId = mintId('execution');
|
|
208
|
+
const claimTtlMs = options.claimTtlMs ?? deps.claimTtlMs;
|
|
209
|
+
const heartbeatMs = options.heartbeatMs ?? deps.heartbeatMs;
|
|
210
|
+
const claim = await claimCase(port, deps, caseId, stepName, options, executionId, claimTtlMs);
|
|
211
|
+
const policy = options.retry === undefined
|
|
212
|
+
? claim.target.step.retry
|
|
213
|
+
: normalizeRetry(stepName, options.retry);
|
|
214
|
+
// Keep the lease alive while the handler runs. Best-effort: a failed beat
|
|
215
|
+
// just lets the claim age.
|
|
216
|
+
const stopHeartbeat = deps.timers.every(heartbeatMs, () => {
|
|
217
|
+
void port.heartbeat(caseId, executionId, claimTtlMs);
|
|
218
|
+
});
|
|
219
|
+
const journal = (entry, attempt, error) => port.appendEntry({
|
|
220
|
+
caseId,
|
|
221
|
+
executionId,
|
|
222
|
+
entry,
|
|
223
|
+
attempt,
|
|
224
|
+
step: stepName,
|
|
225
|
+
scopeKey: claim.scopeKey,
|
|
226
|
+
actor: claim.actor,
|
|
227
|
+
error,
|
|
228
|
+
});
|
|
229
|
+
try {
|
|
230
|
+
for (let attempt = 1;; attempt += 1) {
|
|
231
|
+
// Per-attempt, never carried over: a failed attempt's registered writes
|
|
232
|
+
// and dormancy intent are discarded with the attempt that made them.
|
|
233
|
+
const writes = [];
|
|
234
|
+
let dormancy = null;
|
|
235
|
+
const context = {
|
|
236
|
+
executionId,
|
|
237
|
+
caseId,
|
|
238
|
+
actor: options.actor,
|
|
239
|
+
input: claim.input,
|
|
240
|
+
attempt,
|
|
241
|
+
maxAttempts: policy.maxAttempts,
|
|
242
|
+
onCommit: (write) => {
|
|
243
|
+
writes.push(write);
|
|
244
|
+
},
|
|
245
|
+
// Correlation is an ordinary commit write: the mapping
|
|
246
|
+
// lands in the same transaction as the state that says the external
|
|
247
|
+
// interaction was started. A scoped step's registration defaults to
|
|
248
|
+
// its own element — an envelope sent for buyer #7 belongs to
|
|
249
|
+
// buyer #7 unless the handler says otherwise.
|
|
250
|
+
correlate: (request) => {
|
|
251
|
+
writes.push(async (tx) => {
|
|
252
|
+
await registerCorrelation(tx, {
|
|
253
|
+
...request,
|
|
254
|
+
caseId,
|
|
255
|
+
scopeKey: request.scopeKey === undefined
|
|
256
|
+
? claim.scopeKey
|
|
257
|
+
: request.scopeKey,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
end: () => {
|
|
262
|
+
dormancy = 'ended';
|
|
263
|
+
},
|
|
264
|
+
reopen: () => {
|
|
265
|
+
dormancy = 'reopened';
|
|
266
|
+
},
|
|
267
|
+
...(claim.target.binding !== null && {
|
|
268
|
+
scope: claim.target.binding.element,
|
|
269
|
+
scopeKey: claim.target.binding.key,
|
|
270
|
+
}),
|
|
271
|
+
};
|
|
272
|
+
try {
|
|
273
|
+
const returned = await claim.target.step.handler(claim.state, context);
|
|
274
|
+
const nextState = await validateCaseState(claim.definition, returned, `state returned by step '${stepName}'`).catch((error) => {
|
|
275
|
+
// A handler that returns state its own schema rejects is a
|
|
276
|
+
// deterministic defect: the next attempt would return it again.
|
|
277
|
+
throw new NonRetryable(error);
|
|
278
|
+
});
|
|
279
|
+
return await commitExecution(port, claim, caseId, stepName, nextState, attempt, dormancy, writes);
|
|
280
|
+
}
|
|
281
|
+
catch (error) {
|
|
282
|
+
const fatal = error instanceof NonRetryable || error instanceof ClaimLostError;
|
|
283
|
+
const cause = error instanceof NonRetryable ? error.reason : error;
|
|
284
|
+
const journalError = toJournalError(cause);
|
|
285
|
+
if (!fatal && attempt < policy.maxAttempts) {
|
|
286
|
+
// The attempt-failed entry and the lease's attempt counter (kept
|
|
287
|
+
// current so a takeover's `expired` entry names the right attempt)
|
|
288
|
+
// are independent writes — one round trip, not two.
|
|
289
|
+
await Promise.all([
|
|
290
|
+
journal('attempt-failed', attempt, journalError),
|
|
291
|
+
port.bumpAttempt(caseId, executionId, attempt + 1),
|
|
292
|
+
]);
|
|
293
|
+
await deps.timers.sleep(policy.delayMs(attempt));
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
296
|
+
await Promise.all([
|
|
297
|
+
journal('failed', attempt, journalError),
|
|
298
|
+
port.releaseClaim(caseId, executionId),
|
|
299
|
+
]);
|
|
300
|
+
if (cause instanceof ClaimLostError)
|
|
301
|
+
throw cause;
|
|
302
|
+
throw new StepExecutionError(caseId, executionId, stepName, claim.scopeKey, attempt, cause);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
finally {
|
|
307
|
+
stopHeartbeat();
|
|
308
|
+
}
|
|
309
|
+
};
|
|
310
|
+
/**
|
|
311
|
+
* Execute one step on one case: claim it, run its handler, commit the result.
|
|
312
|
+
*
|
|
313
|
+
* Rejections before anything runs — {@link StepNotAvailableError} (the guard
|
|
314
|
+
* said no), {@link CaseBusyError} (another Execution holds the case),
|
|
315
|
+
* `UnknownStepError` / `ScopeKeyError` (bad address) — write nothing, not
|
|
316
|
+
* even a journal entry: the journal records Executions, and a refused claim
|
|
317
|
+
* never became one.
|
|
318
|
+
*
|
|
319
|
+
* A handler that throws is retried per the step's retry policy (same
|
|
320
|
+
* `executionId`, same claim, same starting state — nothing else can have
|
|
321
|
+
* moved it), each failure journaled as `attempt-failed`. When the attempts
|
|
322
|
+
* run out, a `failed` entry is journaled, the case is released, and
|
|
323
|
+
* {@link StepExecutionError} is thrown.
|
|
324
|
+
*
|
|
325
|
+
* This is {@link runLifecycle} bound to the pg port.
|
|
326
|
+
*/
|
|
327
|
+
export const executeStep = async (env, caseId, stepName, options) => runLifecycle(pgLifecyclePort(env.db, env.caseTypeFor), env, caseId, stepName, options);
|
|
328
|
+
/**
|
|
329
|
+
* Turn a throw into the settled value. No classification happens here —
|
|
330
|
+
* "is this a Refusal" is asked exactly once, where a consumer needs the
|
|
331
|
+
* distinction (ingestion's `classifyDeadLetter`), via `isAffordanceError`.
|
|
332
|
+
*/
|
|
333
|
+
export const settleSystemRun = (error) => ({
|
|
334
|
+
outcome: 'settled',
|
|
335
|
+
error: toError(error),
|
|
336
|
+
});
|
|
337
|
+
/**
|
|
338
|
+
* Run one step on behalf of the system and say how it ended. Never throws:
|
|
339
|
+
* a sweep ranges over many cases with no caller waiting on any single one,
|
|
340
|
+
* so the answer is a value — committed (with the result) or settled (with
|
|
341
|
+
* the error) — and each sweep decides what its kind of sweep does with it.
|
|
342
|
+
*/
|
|
343
|
+
export const runAsSystem = async (env, caseId, stepName, options) => {
|
|
344
|
+
const { definition, ...lifecycleOptions } = options;
|
|
345
|
+
const environment = definition === undefined ? env : { ...env, caseTypeFor: () => definition };
|
|
346
|
+
try {
|
|
347
|
+
return {
|
|
348
|
+
outcome: 'committed',
|
|
349
|
+
result: await executeStep(environment, caseId, stepName, lifecycleOptions),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
catch (error) {
|
|
353
|
+
return settleSystemRun(error);
|
|
354
|
+
}
|
|
355
|
+
};
|
|
356
|
+
//# sourceMappingURL=execute.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execute.js","sourceRoot":"","sources":["../../src/execution/execute.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * The execution lifecycle: **claim → run → commit**.\n *\n * ```\n * ┌── transaction ──────────────┐ ┌── transaction ──────────────┐\n * │ lock the case row │ │ lock the case row │\n * │ take over an expired claim │ handler runs │ verify the claim is ours │\n * │ re-evaluate the guard ←────┼── here, outside ───┼→ write state, bump seq │\n * │ insert the claim (lease) │ any transaction │ run ctx.onCommit writes │\n * │ journal `claimed` │ (heartbeating) │ journal `completed` │\n * └─────────────────────────────┘ └─────────────────────────────┘\n * ```\n *\n * Three things this shape buys, each load-bearing:\n *\n * 1. **Guards advise, handlers enforce.** The affordance a client rendered is\n * advice; the claim's transactional re-evaluation is the enforcement\n * moment. State may have moved (another Execution committed) or the\n * definitions may have (a deploy — definition drift is the\n * same race, handled the same way). A claim that fails rejects with the\n * *current* unmet conditions and writes nothing at all.\n * 2. **No transaction spans a handler.** Handlers call the outside world;\n * a transaction held across an escrow API call would tie up a pooled\n * connection and the case row's lock for as long as the external service\n * takes to answer. The lease (a `claims` row keyed by case id) carries\n * the exclusivity instead, and a heartbeat keeps it alive.\n * 3. **A crashed handler cannot deadlock a case.** The lease expires. The\n * next claimant takes the case over, journaling the abandonment; if the\n * zombie ever comes back to commit, it is refused ({@link ClaimLostError})\n * — at-least-once effects are the handler's problem to deduplicate on\n * `ctx.executionId`, but a stale state write is the framework's to refuse.\n */\n\nimport { thrownMessage, toError } from '../errors.js'\nimport type { GuardEvaluation, Instant } from '../guards/index.js'\nimport { toIso } from '../guards/index.js'\nimport { registerCorrelation } from '../ingestion/correlation.js'\nimport type {\n AnyCaseType,\n CommitWrite,\n CorrelationRequest,\n RetryOptions,\n RetryPolicy,\n StepTarget,\n} from '../model/index.js'\nimport {\n evaluateTarget,\n normalizeRetry,\n resolveTarget,\n validateStepInput,\n} from '../model/index.js'\nimport type { DatabaseAccess, Dormancy } from '../store/index.js'\nimport { mintId, validateCaseState } from '../store/index.js'\nimport type { StateDelta } from './delta.js'\nimport { diffState } from './delta.js'\nimport {\n CaseBusyError,\n ClaimLostError,\n StepExecutionError,\n StepNotAvailableError,\n} from './errors.js'\nimport type { JournalError } from './journal.js'\nimport type { LifecyclePort, LifecycleTx } from './port.js'\nimport { pgLifecyclePort } from './port.js'\nimport type { Timers } from './timers.js'\n\n/** How long a claim survives without a heartbeat, and how often the heartbeat beats. */\nexport const DEFAULT_CLAIM_TTL_MS = 30_000\nexport const DEFAULT_HEARTBEAT_MS = 5_000\n\n/** What {@link executeStep} needs from its caller (the engine supplies all of it). */\nexport interface ExecutionEnvironment {\n readonly db: DatabaseAccess\n /** Resolve a persisted `case_type` name to its registered definition; throws if unknown. */\n readonly caseTypeFor: (caseTypeName: string) => AnyCaseType\n readonly claimTtlMs: number\n readonly heartbeatMs: number\n /**\n * The clock. Conditions never read it; every process-side \"as of now\"\n * comes through here, so a test can hand the lifecycle a deterministic\n * instant instead of building fixtures around far-future dates. (Lease\n * expiry is judged by the storage adapter's own clock — see\n * {@link LifecyclePort}.)\n */\n readonly now: () => Date\n /** The process timers — retry delays and the heartbeat. See {@link Timers}. */\n readonly timers: Timers\n}\n\n/** Options for one execute call. */\nexport interface ExecuteOptions<TActor = unknown> {\n /** The Actor executing the step; `permits` conditions are evaluated against it. */\n readonly actor: TActor\n /** Required for a scoped step, forbidden otherwise — the element's scope key. */\n readonly scopeKey?: string\n /** The step's input, validated against its declared input schema before the handler runs. */\n readonly input?: unknown\n /** The instant to re-evaluate the guard as of; defaults to now. */\n readonly asOf?: Instant\n /** Override the claim lease for this Execution (long-running handler). */\n readonly claimTtlMs?: number\n /** Override the heartbeat interval for this Execution. */\n readonly heartbeatMs?: number\n /** Override the step's declared retry policy for this Execution. */\n readonly retry?: RetryOptions\n}\n\n/** A committed Execution — what `execute` resolves to. */\nexport interface ExecutionResult<TState = unknown> {\n readonly executionId: string\n readonly caseId: string\n readonly caseTypeName: string\n readonly step: string\n /** Present iff the step is scoped. */\n readonly scopeKey?: string\n /** How many attempts ran, including the one that succeeded. */\n readonly attempts: number\n /** The claim-time guard evaluation — the enforcement moment, as journaled. */\n readonly guard: GuardEvaluation\n /** The committed Case State. */\n readonly state: TState\n /** What changed, as JSON Patch. */\n readonly delta: StateDelta\n /** The case's sequence counter after this Execution. */\n readonly seq: number\n /** `end()` / `reopen()` called by the handler, if either was. */\n readonly dormancy: Dormancy | null\n /** The case's dormancy marker after this Execution (ISO-8601 UTC), `null` while active. */\n readonly endedAt: string | null\n readonly claimedAt: string\n readonly committedAt: string\n}\n\n/** A held claim plus everything the run and commit phases need from the claim transaction. */\ninterface Claim {\n readonly executionId: string\n readonly caseTypeName: string\n readonly definition: AnyCaseType\n readonly target: StepTarget<unknown, unknown>\n readonly state: unknown\n readonly input: unknown\n readonly asOf: string\n readonly guard: GuardEvaluation\n readonly claimedAt: string\n readonly scopeKey: string | null\n readonly actor: unknown\n}\n\n/**\n * Wraps a failure that must not be retried because retrying is guaranteed to\n * reproduce it: a handler returning a Case State its schema rejects, or a\n * commit refused because the claim is gone.\n */\nclass NonRetryable extends Error {\n readonly reason: unknown\n constructor(reason: unknown) {\n super(thrownMessage(reason))\n this.name = 'NonRetryable'\n this.reason = reason\n }\n}\n\nconst toJournalError = (error: unknown): JournalError =>\n error instanceof Error\n ? { name: error.name, message: error.message }\n : { name: 'UnknownError', message: String(error) }\n\n/**\n * Resolve whatever claim already sits on this case, inside the claim\n * transaction (the case row is locked, so no two claimants decide this at\n * once): a live claim means the case is busy; an expired one is a crashed\n * handler's abandoned lease — journal the abandonment and take it.\n *\n * Both the `expired` entry and the takeover ride on this transaction, so a\n * claim that goes on to fail its guard rolls the abandonment record back and\n * leaves the dead lease in place. That is deliberate: an expired lease blocks\n * nothing, and the abandonment is only a fact worth recording when somebody\n * actually took the case over.\n */\nconst clearStaleClaim = async (\n tx: LifecycleTx,\n caseId: string,\n claimant: string,\n): Promise<void> => {\n const held = await tx.currentClaim()\n if (!held) return\n if (!held.expired) {\n throw new CaseBusyError(caseId, {\n executionId: held.executionId,\n stepName: held.step,\n scopeKey: held.scopeKey,\n expiresAt: held.expiresAt,\n })\n }\n await tx.appendEntry({\n caseId,\n executionId: held.executionId,\n entry: 'expired',\n attempt: held.attempt,\n step: held.step,\n scopeKey: held.scopeKey,\n error: {\n name: 'ClaimExpiredError',\n message: `claim expired at ${held.expiresAt} with no terminal entry; case taken over by execution ${claimant}`,\n },\n })\n await tx.deleteClaim(held.executionId)\n}\n\n/**\n * What the lifecycle core needs besides storage: the clock, the timers, and\n * the lease timings. Definition resolution is the port's own (its `loadCase`\n * returns the case resolved whole), so it is no part of this interface.\n */\nexport interface LifecycleDeps {\n readonly now: () => Date\n readonly timers: Timers\n readonly claimTtlMs: number\n readonly heartbeatMs: number\n}\n\n/**\n * The claim: one short transaction that either takes the case and journals a\n * `claimed` entry, or writes nothing and throws the reason. Loading, guard\n * re-evaluation, and the exclusivity decision all happen under the case row's\n * lock, which is what makes \"exactly one of N concurrent attempts claims\"\n * true rather than likely.\n */\nconst claimCase = async (\n port: LifecyclePort,\n deps: LifecycleDeps,\n caseId: string,\n stepName: string,\n options: ExecuteOptions,\n executionId: string,\n claimTtlMs: number,\n): Promise<Claim> =>\n port.withCaseLock(caseId, async (tx) => {\n const { definition, handle, state } = await tx.loadCase()\n\n await clearStaleClaim(tx, caseId, executionId)\n\n const target = resolveTarget(\n definition,\n state,\n stepName,\n options.scopeKey,\n ) as StepTarget<unknown, unknown>\n const scopeKey = target.binding?.key ?? null\n const input = await validateStepInput(target.step, options.input)\n\n const asOf = toIso(options.asOf ?? deps.now())\n const guard = evaluateTarget(target, { actor: options.actor, asOf })\n if (!guard.available)\n throw new StepNotAvailableError(caseId, stepName, scopeKey, guard)\n\n const { claimedAt } = await tx.insertClaim(\n executionId,\n stepName,\n scopeKey,\n claimTtlMs,\n )\n\n await tx.appendEntry({\n caseId,\n executionId,\n entry: 'claimed',\n attempt: 1,\n step: stepName,\n scopeKey,\n actor: options.actor,\n input,\n asOf,\n guard,\n // The Case State the guard was evaluated against, stored so audit\n // reconstruction is exact rather than approximate.\n state,\n })\n\n return {\n executionId,\n caseTypeName: handle.caseTypeName,\n definition,\n target,\n state,\n input,\n asOf,\n guard,\n claimedAt,\n scopeKey,\n actor: options.actor,\n }\n })\n\n/**\n * The commit: the new Case State, the app's own writes, and the `completed`\n * journal entry, all in one transaction — or none of them.\n */\nconst commitExecution = async (\n port: LifecyclePort,\n claim: Claim,\n caseId: string,\n stepName: string,\n nextState: unknown,\n attempt: number,\n dormancy: Dormancy | null,\n writes: readonly CommitWrite[],\n): Promise<ExecutionResult> => {\n // Both inputs are fixed before the transaction opens, so the deep diff —\n // pure CPU over the whole state document — runs before the row lock is\n // taken, not while holding it.\n const delta = diffState(claim.state, nextState)\n return port.withCaseLock(caseId, async (tx) => {\n await tx.lockCase()\n // The one check that makes a stale write impossible. Changing Case State\n // requires holding the claim, and taking the claim over replaces this row\n // — so finding our own execution id here means nothing has committed on\n // this case since we claimed it, and the state the handler computed from\n // is still current. An expired-but-undisturbed lease therefore commits\n // quite legitimately: a handler that overran its lease with no contender\n // has raced nobody.\n const holder = (await tx.currentClaim())?.executionId ?? null\n if (holder !== claim.executionId) {\n throw new ClaimLostError(caseId, claim.executionId, holder)\n }\n\n const updated = await tx.updateCaseState(nextState, dormancy)\n await tx.appWrites(writes)\n\n const entry = await tx.appendEntry({\n caseId,\n executionId: claim.executionId,\n entry: 'completed',\n attempt,\n step: stepName,\n scopeKey: claim.scopeKey,\n actor: claim.actor,\n delta,\n dormancy,\n })\n await tx.deleteClaim(claim.executionId)\n\n return {\n executionId: claim.executionId,\n caseId,\n caseTypeName: claim.caseTypeName,\n step: stepName,\n ...(claim.scopeKey !== null && { scopeKey: claim.scopeKey }),\n attempts: attempt,\n guard: claim.guard,\n state: nextState,\n delta,\n seq: updated.seq,\n dormancy,\n endedAt: updated.endedAt,\n claimedAt: claim.claimedAt,\n committedAt: entry.recordedAt,\n }\n })\n}\n\n/**\n * The lifecycle core: claim → run → commit against a {@link LifecyclePort}.\n *\n * {@link executeStep} binds this to the pg port; the claim state machine's\n * own tests bind it to an in-memory port instead. Same body either way — the\n * port is the only storage the lifecycle knows.\n */\nexport const runLifecycle = async (\n port: LifecyclePort,\n deps: LifecycleDeps,\n caseId: string,\n stepName: string,\n options: ExecuteOptions,\n): Promise<ExecutionResult> => {\n const executionId = mintId('execution')\n const claimTtlMs = options.claimTtlMs ?? deps.claimTtlMs\n const heartbeatMs = options.heartbeatMs ?? deps.heartbeatMs\n\n const claim = await claimCase(\n port,\n deps,\n caseId,\n stepName,\n options,\n executionId,\n claimTtlMs,\n )\n const policy: RetryPolicy =\n options.retry === undefined\n ? claim.target.step.retry\n : normalizeRetry(stepName, options.retry)\n // Keep the lease alive while the handler runs. Best-effort: a failed beat\n // just lets the claim age.\n const stopHeartbeat = deps.timers.every(heartbeatMs, () => {\n void port.heartbeat(caseId, executionId, claimTtlMs)\n })\n\n const journal = (\n entry: 'attempt-failed' | 'failed',\n attempt: number,\n error: JournalError,\n ): Promise<unknown> =>\n port.appendEntry({\n caseId,\n executionId,\n entry,\n attempt,\n step: stepName,\n scopeKey: claim.scopeKey,\n actor: claim.actor,\n error,\n })\n\n try {\n for (let attempt = 1; ; attempt += 1) {\n // Per-attempt, never carried over: a failed attempt's registered writes\n // and dormancy intent are discarded with the attempt that made them.\n const writes: CommitWrite[] = []\n let dormancy: Dormancy | null = null\n\n const context = {\n executionId,\n caseId,\n actor: options.actor,\n input: claim.input,\n attempt,\n maxAttempts: policy.maxAttempts,\n onCommit: (write: CommitWrite) => {\n writes.push(write)\n },\n // Correlation is an ordinary commit write: the mapping\n // lands in the same transaction as the state that says the external\n // interaction was started. A scoped step's registration defaults to\n // its own element — an envelope sent for buyer #7 belongs to\n // buyer #7 unless the handler says otherwise.\n correlate: (request: CorrelationRequest) => {\n writes.push(async (tx) => {\n await registerCorrelation(tx, {\n ...request,\n caseId,\n scopeKey:\n request.scopeKey === undefined\n ? claim.scopeKey\n : request.scopeKey,\n })\n })\n },\n end: () => {\n dormancy = 'ended'\n },\n reopen: () => {\n dormancy = 'reopened'\n },\n ...(claim.target.binding !== null && {\n scope: claim.target.binding.element,\n scopeKey: claim.target.binding.key,\n }),\n }\n\n try {\n const returned = await claim.target.step.handler(claim.state, context)\n const nextState = await validateCaseState(\n claim.definition,\n returned,\n `state returned by step '${stepName}'`,\n ).catch((error) => {\n // A handler that returns state its own schema rejects is a\n // deterministic defect: the next attempt would return it again.\n throw new NonRetryable(error)\n })\n return await commitExecution(\n port,\n claim,\n caseId,\n stepName,\n nextState,\n attempt,\n dormancy,\n writes,\n )\n } catch (error) {\n const fatal =\n error instanceof NonRetryable || error instanceof ClaimLostError\n const cause = error instanceof NonRetryable ? error.reason : error\n const journalError = toJournalError(cause)\n\n if (!fatal && attempt < policy.maxAttempts) {\n // The attempt-failed entry and the lease's attempt counter (kept\n // current so a takeover's `expired` entry names the right attempt)\n // are independent writes — one round trip, not two.\n await Promise.all([\n journal('attempt-failed', attempt, journalError),\n port.bumpAttempt(caseId, executionId, attempt + 1),\n ])\n await deps.timers.sleep(policy.delayMs(attempt))\n continue\n }\n await Promise.all([\n journal('failed', attempt, journalError),\n port.releaseClaim(caseId, executionId),\n ])\n if (cause instanceof ClaimLostError) throw cause\n throw new StepExecutionError(\n caseId,\n executionId,\n stepName,\n claim.scopeKey,\n attempt,\n cause,\n )\n }\n }\n } finally {\n stopHeartbeat()\n }\n}\n\n/**\n * Execute one step on one case: claim it, run its handler, commit the result.\n *\n * Rejections before anything runs — {@link StepNotAvailableError} (the guard\n * said no), {@link CaseBusyError} (another Execution holds the case),\n * `UnknownStepError` / `ScopeKeyError` (bad address) — write nothing, not\n * even a journal entry: the journal records Executions, and a refused claim\n * never became one.\n *\n * A handler that throws is retried per the step's retry policy (same\n * `executionId`, same claim, same starting state — nothing else can have\n * moved it), each failure journaled as `attempt-failed`. When the attempts\n * run out, a `failed` entry is journaled, the case is released, and\n * {@link StepExecutionError} is thrown.\n *\n * This is {@link runLifecycle} bound to the pg port.\n */\nexport const executeStep = async (\n env: ExecutionEnvironment,\n caseId: string,\n stepName: string,\n options: ExecuteOptions,\n): Promise<ExecutionResult> =>\n runLifecycle(\n pgLifecyclePort(env.db, env.caseTypeFor),\n env,\n caseId,\n stepName,\n options,\n )\n\n// ── The system runner ──────────────────────────────────────────────────────\n//\n// `docs/architecture.md` draws a lenient/loud pair wherever the same need\n// recurs: loud for an addressed request, lenient for a sweep. For running a\n// step, loud is `Engine.execute` — it propagates, because somebody named a\n// case and a step and is owed the refusal. The lenient half is here: every\n// sweep (ingestion, migration) runs steps *as the system*, with no caller\n// an exception could reach, and each would otherwise wrap the lifecycle in\n// its own try/catch classification. `runAsSystem` is the lenient half\n// stated once — a total, named outcome the sweeps filter, the same way\n// listings are filters over `selectTargets`.\n\n/** A committed system run. */\nexport interface SystemCommit {\n readonly outcome: 'committed'\n readonly result: ExecutionResult\n}\n\n/**\n * A system run that did not commit, carrying whatever the lifecycle threw —\n * a Refusal keeps its identity (its code is the answer, projectable by\n * `isAffordanceError`, never re-derived from a class), a bug or an\n * infrastructure failure passes through as itself. One settled variant, not\n * a refused/failed pair: no sweep ever treated the halves differently, and a\n * discriminant nobody branches on is interface without behaviour.\n */\nexport interface SystemSettled {\n readonly outcome: 'settled'\n readonly error: Error\n}\n\n/** How a step the system ran ended — total over every way the lifecycle can answer. */\nexport type SystemRunOutcome = SystemCommit | SystemSettled\n\n/** What a sweep may ask for beyond the lifecycle's own execute options. */\nexport interface SystemRunOptions extends ExecuteOptions {\n /**\n * Run against this definition instead of the registry's — how a migration\n * executes its synthetic `migrate:<name>` step. First-class here so no\n * sweep has to smuggle a definition in by rewriting `caseTypeFor`.\n */\n readonly definition?: AnyCaseType\n}\n\n/**\n * Turn a throw into the settled value. No classification happens here —\n * \"is this a Refusal\" is asked exactly once, where a consumer needs the\n * distinction (ingestion's `classifyDeadLetter`), via `isAffordanceError`.\n */\nexport const settleSystemRun = (error: unknown): SystemSettled => ({\n outcome: 'settled',\n error: toError(error),\n})\n\n/**\n * Run one step on behalf of the system and say how it ended. Never throws:\n * a sweep ranges over many cases with no caller waiting on any single one,\n * so the answer is a value — committed (with the result) or settled (with\n * the error) — and each sweep decides what its kind of sweep does with it.\n */\nexport const runAsSystem = async (\n env: ExecutionEnvironment,\n caseId: string,\n stepName: string,\n options: SystemRunOptions,\n): Promise<SystemRunOutcome> => {\n const { definition, ...lifecycleOptions } = options\n const environment: ExecutionEnvironment =\n definition === undefined ? env : { ...env, caseTypeFor: () => definition }\n try {\n return {\n outcome: 'committed',\n result: await executeStep(\n environment,\n caseId,\n stepName,\n lifecycleOptions,\n ),\n }\n } catch (error) {\n return settleSystemRun(error)\n }\n}\n"]}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution lifecycle and the journal.
|
|
3
|
+
*
|
|
4
|
+
* An **Execution** is one recorded run of a step on a case (CONTEXT.md): a
|
|
5
|
+
* claim that re-evaluates the guard transactionally, a handler run outside
|
|
6
|
+
* any transaction, and a commit that writes the new Case State together with
|
|
7
|
+
* the journal entry describing it. The **journal** is the immutable,
|
|
8
|
+
* append-only record those Executions leave behind.
|
|
9
|
+
*
|
|
10
|
+
* See `execute.ts` for the lifecycle's shape and the reasoning behind it.
|
|
11
|
+
*/
|
|
12
|
+
export type { PatchOp, StateDelta } from './delta.js';
|
|
13
|
+
export { diffState, jsonEqual } from './delta.js';
|
|
14
|
+
export { CaseBusyError, ClaimLostError, StepExecutionError, StepNotAvailableError, stepLabel, } from './errors.js';
|
|
15
|
+
export type { ExecuteOptions, ExecutionEnvironment, ExecutionResult, LifecycleDeps, SystemCommit, SystemRunOptions, SystemRunOutcome, SystemSettled, } from './execute.js';
|
|
16
|
+
export { DEFAULT_CLAIM_TTL_MS, DEFAULT_HEARTBEAT_MS, executeStep, runAsSystem, runLifecycle, settleSystemRun, } from './execute.js';
|
|
17
|
+
export type { ClaimedEntryInput, ClaimedJournalEntry, CompletedEntryInput, ExecutionRecord, ExecutionStatus, FailureEntryInput, JournalEntry, JournalEntryColumns, JournalEntryInput, JournalEntryType, JournalError, JournalFilter, } from './journal.js';
|
|
18
|
+
export { appendEntry, foldExecutions, isClaimedEntry, projectEntry, readJournal, } from './journal.js';
|
|
19
|
+
export type { HeldClaim, LifecyclePort, LifecycleTx } from './port.js';
|
|
20
|
+
export { pgLifecyclePort } from './port.js';
|
|
21
|
+
export type { GuardReplay } from './replay.js';
|
|
22
|
+
export { replayGuard } from './replay.js';
|
|
23
|
+
export type { Timers } from './timers.js';
|
|
24
|
+
export { realTimers } from './timers.js';
|
|
25
|
+
export { withTransaction } from './transaction.js';
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The execution lifecycle and the journal.
|
|
3
|
+
*
|
|
4
|
+
* An **Execution** is one recorded run of a step on a case (CONTEXT.md): a
|
|
5
|
+
* claim that re-evaluates the guard transactionally, a handler run outside
|
|
6
|
+
* any transaction, and a commit that writes the new Case State together with
|
|
7
|
+
* the journal entry describing it. The **journal** is the immutable,
|
|
8
|
+
* append-only record those Executions leave behind.
|
|
9
|
+
*
|
|
10
|
+
* See `execute.ts` for the lifecycle's shape and the reasoning behind it.
|
|
11
|
+
*/
|
|
12
|
+
export { diffState, jsonEqual } from './delta.js';
|
|
13
|
+
export { CaseBusyError, ClaimLostError, StepExecutionError, StepNotAvailableError, stepLabel, } from './errors.js';
|
|
14
|
+
export { DEFAULT_CLAIM_TTL_MS, DEFAULT_HEARTBEAT_MS, executeStep, runAsSystem, runLifecycle, settleSystemRun, } from './execute.js';
|
|
15
|
+
export { appendEntry, foldExecutions, isClaimedEntry, projectEntry, readJournal, } from './journal.js';
|
|
16
|
+
export { pgLifecyclePort } from './port.js';
|
|
17
|
+
export { replayGuard } from './replay.js';
|
|
18
|
+
export { realTimers } from './timers.js';
|
|
19
|
+
export { withTransaction } from './transaction.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/execution/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,YAAY,CAAA;AACjD,OAAO,EACL,aAAa,EACb,cAAc,EACd,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,GACV,MAAM,aAAa,CAAA;AAWpB,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,YAAY,EACZ,eAAe,GAChB,MAAM,cAAc,CAAA;AAerB,OAAO,EACL,WAAW,EACX,cAAc,EACd,cAAc,EACd,YAAY,EACZ,WAAW,GACZ,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,eAAe,EAAE,MAAM,WAAW,CAAA;AAE3C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAA;AAEzC,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAA;AACxC,OAAO,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAA","sourcesContent":["/**\n * The execution lifecycle and the journal.\n *\n * An **Execution** is one recorded run of a step on a case (CONTEXT.md): a\n * claim that re-evaluates the guard transactionally, a handler run outside\n * any transaction, and a commit that writes the new Case State together with\n * the journal entry describing it. The **journal** is the immutable,\n * append-only record those Executions leave behind.\n *\n * See `execute.ts` for the lifecycle's shape and the reasoning behind it.\n */\n\nexport type { PatchOp, StateDelta } from './delta.js'\nexport { diffState, jsonEqual } from './delta.js'\nexport {\n CaseBusyError,\n ClaimLostError,\n StepExecutionError,\n StepNotAvailableError,\n stepLabel,\n} from './errors.js'\nexport type {\n ExecuteOptions,\n ExecutionEnvironment,\n ExecutionResult,\n LifecycleDeps,\n SystemCommit,\n SystemRunOptions,\n SystemRunOutcome,\n SystemSettled,\n} from './execute.js'\nexport {\n DEFAULT_CLAIM_TTL_MS,\n DEFAULT_HEARTBEAT_MS,\n executeStep,\n runAsSystem,\n runLifecycle,\n settleSystemRun,\n} from './execute.js'\nexport type {\n ClaimedEntryInput,\n ClaimedJournalEntry,\n CompletedEntryInput,\n ExecutionRecord,\n ExecutionStatus,\n FailureEntryInput,\n JournalEntry,\n JournalEntryColumns,\n JournalEntryInput,\n JournalEntryType,\n JournalError,\n JournalFilter,\n} from './journal.js'\nexport {\n appendEntry,\n foldExecutions,\n isClaimedEntry,\n projectEntry,\n readJournal,\n} from './journal.js'\nexport type { HeldClaim, LifecyclePort, LifecycleTx } from './port.js'\nexport { pgLifecyclePort } from './port.js'\nexport type { GuardReplay } from './replay.js'\nexport { replayGuard } from './replay.js'\nexport type { Timers } from './timers.js'\nexport { realTimers } from './timers.js'\nexport { withTransaction } from './transaction.js'\n"]}
|