@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,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The ways an Execution can be refused or fail.
|
|
3
|
+
*
|
|
4
|
+
* Each is a distinct answer to "why didn't this run", and callers are
|
|
5
|
+
* expected to branch on them: {@link StepNotAvailableError} is a client-facing
|
|
6
|
+
* *no* with reasons attached; {@link CaseBusyError} is "not now, try again";
|
|
7
|
+
* {@link ClaimLostError} and {@link StepExecutionError} are failures of a run
|
|
8
|
+
* that did start.
|
|
9
|
+
*/
|
|
10
|
+
import { AffordanceError, thrownMessage } from '../errors.js';
|
|
11
|
+
import { describeUnmet, unmetConditions } from '../guards/index.js';
|
|
12
|
+
/**
|
|
13
|
+
* How a step (× scope element) is named in prose: `'close-purchase'`, or
|
|
14
|
+
* `'request-re-sign' (buyer_7)` when scoped. One spelling, because refusal
|
|
15
|
+
* messages and adapter renderings must name the same affordance identically.
|
|
16
|
+
*/
|
|
17
|
+
export const stepLabel = (stepName, scopeKey) => scopeKey === null ? `'${stepName}'` : `'${stepName}' (${scopeKey})`;
|
|
18
|
+
/**
|
|
19
|
+
* The claim's transactional guard re-evaluation said no — the enforcement
|
|
20
|
+
* moment. Guards advise; handlers enforce: an affordance computed
|
|
21
|
+
* for a render is advice, and by the time the execute request arrives, state
|
|
22
|
+
* may have moved (another Execution committed) or the definitions may have
|
|
23
|
+
* (a deploy tightened the guard — definition drift is handled by the
|
|
24
|
+
* same mechanism as state races).
|
|
25
|
+
*
|
|
26
|
+
* The unmet conditions carried here are the *current* ones, evaluated inside
|
|
27
|
+
* the claim transaction, so a rejection is self-explaining: hand `unmet`
|
|
28
|
+
* straight back to the caller.
|
|
29
|
+
*/
|
|
30
|
+
export class StepNotAvailableError extends AffordanceError {
|
|
31
|
+
caseId;
|
|
32
|
+
stepName;
|
|
33
|
+
scopeKey;
|
|
34
|
+
/** False when a `requires` condition is unmet: not possible on this case, for anyone. */
|
|
35
|
+
possible;
|
|
36
|
+
/** False when a `permits` condition is unmet: possible, but not for this actor. */
|
|
37
|
+
permitted;
|
|
38
|
+
/** The failed condition results, verbatim from the claim-time evaluation. */
|
|
39
|
+
unmet;
|
|
40
|
+
/** The full evaluation record, for journaling or `explain`-style rendering. */
|
|
41
|
+
evaluation;
|
|
42
|
+
constructor(caseId, stepName, scopeKey, evaluation) {
|
|
43
|
+
const unmet = unmetConditions(evaluation);
|
|
44
|
+
const target = stepLabel(stepName, scopeKey);
|
|
45
|
+
super('step-not-available', `step ${target} is not available on case ${caseId}: ${describeUnmet(evaluation) || '(no unmet conditions reported)'}`);
|
|
46
|
+
this.name = 'StepNotAvailableError';
|
|
47
|
+
this.caseId = caseId;
|
|
48
|
+
this.stepName = stepName;
|
|
49
|
+
this.scopeKey = scopeKey;
|
|
50
|
+
this.possible = evaluation.possible;
|
|
51
|
+
this.permitted = evaluation.permitted;
|
|
52
|
+
this.unmet = unmet;
|
|
53
|
+
this.evaluation = evaluation;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Another Execution is already in flight on this case and its claim has not
|
|
58
|
+
* expired. Executions are serialized per case in v1 — cases advance at human
|
|
59
|
+
* pace, so one Execution at a time costs no real throughput — which makes
|
|
60
|
+
* this "not now", not "never": retry after
|
|
61
|
+
* {@link CaseBusyError.expiresAt} at the latest.
|
|
62
|
+
*/
|
|
63
|
+
export class CaseBusyError extends AffordanceError {
|
|
64
|
+
caseId;
|
|
65
|
+
/** The in-flight Execution holding the case. */
|
|
66
|
+
executionId;
|
|
67
|
+
stepName;
|
|
68
|
+
scopeKey;
|
|
69
|
+
/** When the in-flight claim lapses if its handler stops heartbeating (ISO-8601 UTC). */
|
|
70
|
+
expiresAt;
|
|
71
|
+
constructor(caseId, holder) {
|
|
72
|
+
super('case-busy', `case ${caseId} is busy: execution ${holder.executionId} is running step ${stepLabel(holder.stepName, holder.scopeKey)}, claim expires ${holder.expiresAt}`);
|
|
73
|
+
this.name = 'CaseBusyError';
|
|
74
|
+
this.caseId = caseId;
|
|
75
|
+
this.executionId = holder.executionId;
|
|
76
|
+
this.stepName = holder.stepName;
|
|
77
|
+
this.scopeKey = holder.scopeKey;
|
|
78
|
+
this.expiresAt = holder.expiresAt;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* The claim was gone (or belonged to someone else) when this Execution tried
|
|
83
|
+
* to commit: its lease expired mid-handler and another claimant took the case
|
|
84
|
+
* over. The handler's effects on the outside world already happened — they are
|
|
85
|
+
* at-least-once by contract and deduplicated on `executionId` — but
|
|
86
|
+
* its Case State write is refused, because the state it computed from is stale.
|
|
87
|
+
*/
|
|
88
|
+
export class ClaimLostError extends AffordanceError {
|
|
89
|
+
caseId;
|
|
90
|
+
executionId;
|
|
91
|
+
/** The Execution now holding the case, if any. */
|
|
92
|
+
heldBy;
|
|
93
|
+
constructor(caseId, executionId, heldBy) {
|
|
94
|
+
super(
|
|
95
|
+
// The case moved on under this Execution: somebody else holds it now,
|
|
96
|
+
// and the state this Execution computed from is stale. That is the
|
|
97
|
+
// same "not now" answer a busy case gives, so it reuses 'case-busy' —
|
|
98
|
+
// a code an adapter already knows how to render.
|
|
99
|
+
'case-busy', `execution ${executionId} lost its claim on case ${caseId}${heldBy === null
|
|
100
|
+
? ' (claim expired and was released)'
|
|
101
|
+
: ` (now held by ${heldBy})`} — its state write was refused`);
|
|
102
|
+
this.name = 'ClaimLostError';
|
|
103
|
+
this.caseId = caseId;
|
|
104
|
+
this.executionId = executionId;
|
|
105
|
+
this.heldBy = heldBy;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* The Execution ran and failed: the handler threw on every allowed attempt,
|
|
110
|
+
* or it returned a Case State the case type's schema rejects (a deterministic
|
|
111
|
+
* defect, failed without retry). A `failed` journal entry records it and the
|
|
112
|
+
* case is released.
|
|
113
|
+
*/
|
|
114
|
+
export class StepExecutionError extends AffordanceError {
|
|
115
|
+
caseId;
|
|
116
|
+
executionId;
|
|
117
|
+
stepName;
|
|
118
|
+
scopeKey;
|
|
119
|
+
/** How many attempts ran before the Execution was given up on. */
|
|
120
|
+
attempts;
|
|
121
|
+
constructor(caseId, executionId, stepName, scopeKey, attempts, cause) {
|
|
122
|
+
const reason = thrownMessage(cause);
|
|
123
|
+
super('execution-failed', `step ${stepLabel(stepName, scopeKey)} failed on case ${caseId} after ${attempts} attempt${attempts === 1 ? '' : 's'}: ${reason}`, { cause });
|
|
124
|
+
this.name = 'StepExecutionError';
|
|
125
|
+
this.caseId = caseId;
|
|
126
|
+
this.executionId = executionId;
|
|
127
|
+
this.stepName = stepName;
|
|
128
|
+
this.scopeKey = scopeKey;
|
|
129
|
+
this.attempts = attempts;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/execution/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,eAAe,EAAE,aAAa,EAAE,MAAM,cAAc,CAAA;AAE7D,OAAO,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,oBAAoB,CAAA;AAEnE;;;;GAIG;AACH,MAAM,CAAC,MAAM,SAAS,GAAG,CAAC,QAAgB,EAAE,QAAuB,EAAU,EAAE,CAC7E,QAAQ,KAAK,IAAI,CAAC,CAAC,CAAC,IAAI,QAAQ,GAAG,CAAC,CAAC,CAAC,IAAI,QAAQ,MAAM,QAAQ,GAAG,CAAA;AAErE;;;;;;;;;;;GAWG;AACH,MAAM,OAAO,qBAAsB,SAAQ,eAAe;IAC/C,MAAM,CAAQ;IACd,QAAQ,CAAQ;IAChB,QAAQ,CAAe;IAChC,yFAAyF;IAChF,QAAQ,CAAS;IAC1B,mFAAmF;IAC1E,SAAS,CAAS;IAC3B,6EAA6E;IACpE,KAAK,CAA4B;IAC1C,+EAA+E;IACtE,UAAU,CAAiB;IAEpC,YACE,MAAc,EACd,QAAgB,EAChB,QAAuB,EACvB,UAA2B;QAE3B,MAAM,KAAK,GAAG,eAAe,CAAC,UAAU,CAAC,CAAA;QACzC,MAAM,MAAM,GAAG,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,CAAA;QAC5C,KAAK,CACH,oBAAoB,EACpB,QAAQ,MAAM,6BAA6B,MAAM,KAC/C,aAAa,CAAC,UAAU,CAAC,IAAI,gCAC/B,EAAE,CACH,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,uBAAuB,CAAA;QACnC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,UAAU,CAAC,QAAQ,CAAA;QACnC,IAAI,CAAC,SAAS,GAAG,UAAU,CAAC,SAAS,CAAA;QACrC,IAAI,CAAC,KAAK,GAAG,KAAK,CAAA;QAClB,IAAI,CAAC,UAAU,GAAG,UAAU,CAAA;IAC9B,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,aAAc,SAAQ,eAAe;IACvC,MAAM,CAAQ;IACvB,gDAAgD;IACvC,WAAW,CAAQ;IACnB,QAAQ,CAAQ;IAChB,QAAQ,CAAe;IAChC,wFAAwF;IAC/E,SAAS,CAAQ;IAE1B,YACE,MAAc,EACd,MAKC;QAED,KAAK,CACH,WAAW,EACX,QAAQ,MAAM,uBAAuB,MAAM,CAAC,WAAW,oBAAoB,SAAS,CAClF,MAAM,CAAC,QAAQ,EACf,MAAM,CAAC,QAAQ,CAChB,mBAAmB,MAAM,CAAC,SAAS,EAAE,CACvC,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,eAAe,CAAA;QAC3B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,WAAW,GAAG,MAAM,CAAC,WAAW,CAAA;QACrC,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;QAC/B,IAAI,CAAC,QAAQ,GAAG,MAAM,CAAC,QAAQ,CAAA;QAC/B,IAAI,CAAC,SAAS,GAAG,MAAM,CAAC,SAAS,CAAA;IACnC,CAAC;CACF;AAED;;;;;;GAMG;AACH,MAAM,OAAO,cAAe,SAAQ,eAAe;IACxC,MAAM,CAAQ;IACd,WAAW,CAAQ;IAC5B,kDAAkD;IACzC,MAAM,CAAe;IAE9B,YAAY,MAAc,EAAE,WAAmB,EAAE,MAAqB;QACpE,KAAK;QACH,sEAAsE;QACtE,mEAAmE;QACnE,sEAAsE;QACtE,iDAAiD;QACjD,WAAW,EACX,aAAa,WAAW,2BAA2B,MAAM,GACvD,MAAM,KAAK,IAAI;YACb,CAAC,CAAC,mCAAmC;YACrC,CAAC,CAAC,iBAAiB,MAAM,GAC7B,gCAAgC,CACjC,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,gBAAgB,CAAA;QAC5B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;IACtB,CAAC;CACF;AAED;;;;;GAKG;AACH,MAAM,OAAO,kBAAmB,SAAQ,eAAe;IAC5C,MAAM,CAAQ;IACd,WAAW,CAAQ;IACnB,QAAQ,CAAQ;IAChB,QAAQ,CAAe;IAChC,kEAAkE;IACzD,QAAQ,CAAQ;IAEzB,YACE,MAAc,EACd,WAAmB,EACnB,QAAgB,EAChB,QAAuB,EACvB,QAAgB,EAChB,KAAc;QAEd,MAAM,MAAM,GAAG,aAAa,CAAC,KAAK,CAAC,CAAA;QACnC,KAAK,CACH,kBAAkB,EAClB,QAAQ,SAAS,CAAC,QAAQ,EAAE,QAAQ,CAAC,mBAAmB,MAAM,UAAU,QAAQ,WAC9E,QAAQ,KAAK,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GACxB,KAAK,MAAM,EAAE,EACb,EAAE,KAAK,EAAE,CACV,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAA;QAChC,IAAI,CAAC,MAAM,GAAG,MAAM,CAAA;QACpB,IAAI,CAAC,WAAW,GAAG,WAAW,CAAA;QAC9B,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;QACxB,IAAI,CAAC,QAAQ,GAAG,QAAQ,CAAA;IAC1B,CAAC;CACF","sourcesContent":["/**\n * The ways an Execution can be refused or fail.\n *\n * Each is a distinct answer to \"why didn't this run\", and callers are\n * expected to branch on them: {@link StepNotAvailableError} is a client-facing\n * *no* with reasons attached; {@link CaseBusyError} is \"not now, try again\";\n * {@link ClaimLostError} and {@link StepExecutionError} are failures of a run\n * that did start.\n */\n\nimport { AffordanceError, thrownMessage } from '../errors.js'\nimport type { ConditionResult, GuardEvaluation } from '../guards/index.js'\nimport { describeUnmet, unmetConditions } from '../guards/index.js'\n\n/**\n * How a step (× scope element) is named in prose: `'close-purchase'`, or\n * `'request-re-sign' (buyer_7)` when scoped. One spelling, because refusal\n * messages and adapter renderings must name the same affordance identically.\n */\nexport const stepLabel = (stepName: string, scopeKey: string | null): string =>\n scopeKey === null ? `'${stepName}'` : `'${stepName}' (${scopeKey})`\n\n/**\n * The claim's transactional guard re-evaluation said no — the enforcement\n * moment. Guards advise; handlers enforce: an affordance computed\n * for a render is advice, and by the time the execute request arrives, state\n * may have moved (another Execution committed) or the definitions may have\n * (a deploy tightened the guard — definition drift is handled by the\n * same mechanism as state races).\n *\n * The unmet conditions carried here are the *current* ones, evaluated inside\n * the claim transaction, so a rejection is self-explaining: hand `unmet`\n * straight back to the caller.\n */\nexport class StepNotAvailableError extends AffordanceError {\n readonly caseId: string\n readonly stepName: string\n readonly scopeKey: string | null\n /** False when a `requires` condition is unmet: not possible on this case, for anyone. */\n readonly possible: boolean\n /** False when a `permits` condition is unmet: possible, but not for this actor. */\n readonly permitted: boolean\n /** The failed condition results, verbatim from the claim-time evaluation. */\n readonly unmet: readonly ConditionResult[]\n /** The full evaluation record, for journaling or `explain`-style rendering. */\n readonly evaluation: GuardEvaluation\n\n constructor(\n caseId: string,\n stepName: string,\n scopeKey: string | null,\n evaluation: GuardEvaluation,\n ) {\n const unmet = unmetConditions(evaluation)\n const target = stepLabel(stepName, scopeKey)\n super(\n 'step-not-available',\n `step ${target} is not available on case ${caseId}: ${\n describeUnmet(evaluation) || '(no unmet conditions reported)'\n }`,\n )\n this.name = 'StepNotAvailableError'\n this.caseId = caseId\n this.stepName = stepName\n this.scopeKey = scopeKey\n this.possible = evaluation.possible\n this.permitted = evaluation.permitted\n this.unmet = unmet\n this.evaluation = evaluation\n }\n}\n\n/**\n * Another Execution is already in flight on this case and its claim has not\n * expired. Executions are serialized per case in v1 — cases advance at human\n * pace, so one Execution at a time costs no real throughput — which makes\n * this \"not now\", not \"never\": retry after\n * {@link CaseBusyError.expiresAt} at the latest.\n */\nexport class CaseBusyError extends AffordanceError {\n readonly caseId: string\n /** The in-flight Execution holding the case. */\n readonly executionId: string\n readonly stepName: string\n readonly scopeKey: string | null\n /** When the in-flight claim lapses if its handler stops heartbeating (ISO-8601 UTC). */\n readonly expiresAt: string\n\n constructor(\n caseId: string,\n holder: {\n executionId: string\n stepName: string\n scopeKey: string | null\n expiresAt: string\n },\n ) {\n super(\n 'case-busy',\n `case ${caseId} is busy: execution ${holder.executionId} is running step ${stepLabel(\n holder.stepName,\n holder.scopeKey,\n )}, claim expires ${holder.expiresAt}`,\n )\n this.name = 'CaseBusyError'\n this.caseId = caseId\n this.executionId = holder.executionId\n this.stepName = holder.stepName\n this.scopeKey = holder.scopeKey\n this.expiresAt = holder.expiresAt\n }\n}\n\n/**\n * The claim was gone (or belonged to someone else) when this Execution tried\n * to commit: its lease expired mid-handler and another claimant took the case\n * over. The handler's effects on the outside world already happened — they are\n * at-least-once by contract and deduplicated on `executionId` — but\n * its Case State write is refused, because the state it computed from is stale.\n */\nexport class ClaimLostError extends AffordanceError {\n readonly caseId: string\n readonly executionId: string\n /** The Execution now holding the case, if any. */\n readonly heldBy: string | null\n\n constructor(caseId: string, executionId: string, heldBy: string | null) {\n super(\n // The case moved on under this Execution: somebody else holds it now,\n // and the state this Execution computed from is stale. That is the\n // same \"not now\" answer a busy case gives, so it reuses 'case-busy' —\n // a code an adapter already knows how to render.\n 'case-busy',\n `execution ${executionId} lost its claim on case ${caseId}${\n heldBy === null\n ? ' (claim expired and was released)'\n : ` (now held by ${heldBy})`\n } — its state write was refused`,\n )\n this.name = 'ClaimLostError'\n this.caseId = caseId\n this.executionId = executionId\n this.heldBy = heldBy\n }\n}\n\n/**\n * The Execution ran and failed: the handler threw on every allowed attempt,\n * or it returned a Case State the case type's schema rejects (a deterministic\n * defect, failed without retry). A `failed` journal entry records it and the\n * case is released.\n */\nexport class StepExecutionError extends AffordanceError {\n readonly caseId: string\n readonly executionId: string\n readonly stepName: string\n readonly scopeKey: string | null\n /** How many attempts ran before the Execution was given up on. */\n readonly attempts: number\n\n constructor(\n caseId: string,\n executionId: string,\n stepName: string,\n scopeKey: string | null,\n attempts: number,\n cause: unknown,\n ) {\n const reason = thrownMessage(cause)\n super(\n 'execution-failed',\n `step ${stepLabel(stepName, scopeKey)} failed on case ${caseId} after ${attempts} attempt${\n attempts === 1 ? '' : 's'\n }: ${reason}`,\n { cause },\n )\n this.name = 'StepExecutionError'\n this.caseId = caseId\n this.executionId = executionId\n this.stepName = stepName\n this.scopeKey = scopeKey\n this.attempts = attempts\n }\n}\n"]}
|
|
@@ -0,0 +1,179 @@
|
|
|
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 type { GuardEvaluation, Instant } from '../guards/index.js';
|
|
34
|
+
import type { AnyCaseType, RetryOptions } from '../model/index.js';
|
|
35
|
+
import type { DatabaseAccess, Dormancy } from '../store/index.js';
|
|
36
|
+
import type { StateDelta } from './delta.js';
|
|
37
|
+
import type { LifecyclePort } from './port.js';
|
|
38
|
+
import type { Timers } from './timers.js';
|
|
39
|
+
/** How long a claim survives without a heartbeat, and how often the heartbeat beats. */
|
|
40
|
+
export declare const DEFAULT_CLAIM_TTL_MS = 30000;
|
|
41
|
+
export declare const DEFAULT_HEARTBEAT_MS = 5000;
|
|
42
|
+
/** What {@link executeStep} needs from its caller (the engine supplies all of it). */
|
|
43
|
+
export interface ExecutionEnvironment {
|
|
44
|
+
readonly db: DatabaseAccess;
|
|
45
|
+
/** Resolve a persisted `case_type` name to its registered definition; throws if unknown. */
|
|
46
|
+
readonly caseTypeFor: (caseTypeName: string) => AnyCaseType;
|
|
47
|
+
readonly claimTtlMs: number;
|
|
48
|
+
readonly heartbeatMs: number;
|
|
49
|
+
/**
|
|
50
|
+
* The clock. Conditions never read it; every process-side "as of now"
|
|
51
|
+
* comes through here, so a test can hand the lifecycle a deterministic
|
|
52
|
+
* instant instead of building fixtures around far-future dates. (Lease
|
|
53
|
+
* expiry is judged by the storage adapter's own clock — see
|
|
54
|
+
* {@link LifecyclePort}.)
|
|
55
|
+
*/
|
|
56
|
+
readonly now: () => Date;
|
|
57
|
+
/** The process timers — retry delays and the heartbeat. See {@link Timers}. */
|
|
58
|
+
readonly timers: Timers;
|
|
59
|
+
}
|
|
60
|
+
/** Options for one execute call. */
|
|
61
|
+
export interface ExecuteOptions<TActor = unknown> {
|
|
62
|
+
/** The Actor executing the step; `permits` conditions are evaluated against it. */
|
|
63
|
+
readonly actor: TActor;
|
|
64
|
+
/** Required for a scoped step, forbidden otherwise — the element's scope key. */
|
|
65
|
+
readonly scopeKey?: string;
|
|
66
|
+
/** The step's input, validated against its declared input schema before the handler runs. */
|
|
67
|
+
readonly input?: unknown;
|
|
68
|
+
/** The instant to re-evaluate the guard as of; defaults to now. */
|
|
69
|
+
readonly asOf?: Instant;
|
|
70
|
+
/** Override the claim lease for this Execution (long-running handler). */
|
|
71
|
+
readonly claimTtlMs?: number;
|
|
72
|
+
/** Override the heartbeat interval for this Execution. */
|
|
73
|
+
readonly heartbeatMs?: number;
|
|
74
|
+
/** Override the step's declared retry policy for this Execution. */
|
|
75
|
+
readonly retry?: RetryOptions;
|
|
76
|
+
}
|
|
77
|
+
/** A committed Execution — what `execute` resolves to. */
|
|
78
|
+
export interface ExecutionResult<TState = unknown> {
|
|
79
|
+
readonly executionId: string;
|
|
80
|
+
readonly caseId: string;
|
|
81
|
+
readonly caseTypeName: string;
|
|
82
|
+
readonly step: string;
|
|
83
|
+
/** Present iff the step is scoped. */
|
|
84
|
+
readonly scopeKey?: string;
|
|
85
|
+
/** How many attempts ran, including the one that succeeded. */
|
|
86
|
+
readonly attempts: number;
|
|
87
|
+
/** The claim-time guard evaluation — the enforcement moment, as journaled. */
|
|
88
|
+
readonly guard: GuardEvaluation;
|
|
89
|
+
/** The committed Case State. */
|
|
90
|
+
readonly state: TState;
|
|
91
|
+
/** What changed, as JSON Patch. */
|
|
92
|
+
readonly delta: StateDelta;
|
|
93
|
+
/** The case's sequence counter after this Execution. */
|
|
94
|
+
readonly seq: number;
|
|
95
|
+
/** `end()` / `reopen()` called by the handler, if either was. */
|
|
96
|
+
readonly dormancy: Dormancy | null;
|
|
97
|
+
/** The case's dormancy marker after this Execution (ISO-8601 UTC), `null` while active. */
|
|
98
|
+
readonly endedAt: string | null;
|
|
99
|
+
readonly claimedAt: string;
|
|
100
|
+
readonly committedAt: string;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* What the lifecycle core needs besides storage: the clock, the timers, and
|
|
104
|
+
* the lease timings. Definition resolution is the port's own (its `loadCase`
|
|
105
|
+
* returns the case resolved whole), so it is no part of this interface.
|
|
106
|
+
*/
|
|
107
|
+
export interface LifecycleDeps {
|
|
108
|
+
readonly now: () => Date;
|
|
109
|
+
readonly timers: Timers;
|
|
110
|
+
readonly claimTtlMs: number;
|
|
111
|
+
readonly heartbeatMs: number;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* The lifecycle core: claim → run → commit against a {@link LifecyclePort}.
|
|
115
|
+
*
|
|
116
|
+
* {@link executeStep} binds this to the pg port; the claim state machine's
|
|
117
|
+
* own tests bind it to an in-memory port instead. Same body either way — the
|
|
118
|
+
* port is the only storage the lifecycle knows.
|
|
119
|
+
*/
|
|
120
|
+
export declare const runLifecycle: (port: LifecyclePort, deps: LifecycleDeps, caseId: string, stepName: string, options: ExecuteOptions) => Promise<ExecutionResult>;
|
|
121
|
+
/**
|
|
122
|
+
* Execute one step on one case: claim it, run its handler, commit the result.
|
|
123
|
+
*
|
|
124
|
+
* Rejections before anything runs — {@link StepNotAvailableError} (the guard
|
|
125
|
+
* said no), {@link CaseBusyError} (another Execution holds the case),
|
|
126
|
+
* `UnknownStepError` / `ScopeKeyError` (bad address) — write nothing, not
|
|
127
|
+
* even a journal entry: the journal records Executions, and a refused claim
|
|
128
|
+
* never became one.
|
|
129
|
+
*
|
|
130
|
+
* A handler that throws is retried per the step's retry policy (same
|
|
131
|
+
* `executionId`, same claim, same starting state — nothing else can have
|
|
132
|
+
* moved it), each failure journaled as `attempt-failed`. When the attempts
|
|
133
|
+
* run out, a `failed` entry is journaled, the case is released, and
|
|
134
|
+
* {@link StepExecutionError} is thrown.
|
|
135
|
+
*
|
|
136
|
+
* This is {@link runLifecycle} bound to the pg port.
|
|
137
|
+
*/
|
|
138
|
+
export declare const executeStep: (env: ExecutionEnvironment, caseId: string, stepName: string, options: ExecuteOptions) => Promise<ExecutionResult>;
|
|
139
|
+
/** A committed system run. */
|
|
140
|
+
export interface SystemCommit {
|
|
141
|
+
readonly outcome: 'committed';
|
|
142
|
+
readonly result: ExecutionResult;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* A system run that did not commit, carrying whatever the lifecycle threw —
|
|
146
|
+
* a Refusal keeps its identity (its code is the answer, projectable by
|
|
147
|
+
* `isAffordanceError`, never re-derived from a class), a bug or an
|
|
148
|
+
* infrastructure failure passes through as itself. One settled variant, not
|
|
149
|
+
* a refused/failed pair: no sweep ever treated the halves differently, and a
|
|
150
|
+
* discriminant nobody branches on is interface without behaviour.
|
|
151
|
+
*/
|
|
152
|
+
export interface SystemSettled {
|
|
153
|
+
readonly outcome: 'settled';
|
|
154
|
+
readonly error: Error;
|
|
155
|
+
}
|
|
156
|
+
/** How a step the system ran ended — total over every way the lifecycle can answer. */
|
|
157
|
+
export type SystemRunOutcome = SystemCommit | SystemSettled;
|
|
158
|
+
/** What a sweep may ask for beyond the lifecycle's own execute options. */
|
|
159
|
+
export interface SystemRunOptions extends ExecuteOptions {
|
|
160
|
+
/**
|
|
161
|
+
* Run against this definition instead of the registry's — how a migration
|
|
162
|
+
* executes its synthetic `migrate:<name>` step. First-class here so no
|
|
163
|
+
* sweep has to smuggle a definition in by rewriting `caseTypeFor`.
|
|
164
|
+
*/
|
|
165
|
+
readonly definition?: AnyCaseType;
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Turn a throw into the settled value. No classification happens here —
|
|
169
|
+
* "is this a Refusal" is asked exactly once, where a consumer needs the
|
|
170
|
+
* distinction (ingestion's `classifyDeadLetter`), via `isAffordanceError`.
|
|
171
|
+
*/
|
|
172
|
+
export declare const settleSystemRun: (error: unknown) => SystemSettled;
|
|
173
|
+
/**
|
|
174
|
+
* Run one step on behalf of the system and say how it ended. Never throws:
|
|
175
|
+
* a sweep ranges over many cases with no caller waiting on any single one,
|
|
176
|
+
* so the answer is a value — committed (with the result) or settled (with
|
|
177
|
+
* the error) — and each sweep decides what its kind of sweep does with it.
|
|
178
|
+
*/
|
|
179
|
+
export declare const runAsSystem: (env: ExecutionEnvironment, caseId: string, stepName: string, options: SystemRunOptions) => Promise<SystemRunOutcome>;
|