@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,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The engine: the case-type registry bound to the case store.
|
|
3
|
+
*
|
|
4
|
+
* `createEngine({ db, caseTypes })` wires the halves together — persistence
|
|
5
|
+
* (`../store`), guard evaluation (`../guards`), the definition API
|
|
6
|
+
* (`../model`) and the execution lifecycle (`../execution`) — into the
|
|
7
|
+
* framework's public face: `affordances`, `explain`, `execute`, `journal`.
|
|
8
|
+
*
|
|
9
|
+
* The engine is where `asOf` defaults to now: conditions never read the
|
|
10
|
+
* clock, so `EngineOptions.now` — wall clock by default — is the one clock,
|
|
11
|
+
* threaded through the environment to everything below, and everything below
|
|
12
|
+
* it is pure and reconstructable.
|
|
13
|
+
*/
|
|
14
|
+
import { DEFAULT_CLAIM_TTL_MS, DEFAULT_HEARTBEAT_MS, executeStep, readJournal, realTimers, withTransaction, } from '../execution/index.js';
|
|
15
|
+
import { correlationsFor, ingest, lookupCorrelation, normalizeIngestion, readDeadLetters, registerCorrelation, } from '../ingestion/index.js';
|
|
16
|
+
import { migrate } from '../migration/index.js';
|
|
17
|
+
import { insertCase, queryableOf, resolveCase } from '../store/index.js';
|
|
18
|
+
import { computeAffordances, computeExplanation, explainContext, } from './compute.js';
|
|
19
|
+
import { UnknownCaseTypeError } from './errors.js';
|
|
20
|
+
/**
|
|
21
|
+
* Build an engine from the app's database and its case type definitions.
|
|
22
|
+
* Throws at construction on duplicate case type names — the registry is
|
|
23
|
+
* keyed by name, which is all a case row records (definitions
|
|
24
|
+
* float; only the name is persisted).
|
|
25
|
+
*/
|
|
26
|
+
export const createEngine = (options) => {
|
|
27
|
+
const registry = new Map();
|
|
28
|
+
for (const definition of options.caseTypes) {
|
|
29
|
+
if (registry.has(definition.name)) {
|
|
30
|
+
throw new TypeError(`createEngine: duplicate case type name '${definition.name}'`);
|
|
31
|
+
}
|
|
32
|
+
registry.set(definition.name, definition);
|
|
33
|
+
}
|
|
34
|
+
const caseTypeFor = (caseTypeName) => {
|
|
35
|
+
const definition = registry.get(caseTypeName);
|
|
36
|
+
if (definition === undefined) {
|
|
37
|
+
throw new UnknownCaseTypeError(caseTypeName, [...registry.keys()]);
|
|
38
|
+
}
|
|
39
|
+
return definition;
|
|
40
|
+
};
|
|
41
|
+
const now = options.now ?? (() => new Date());
|
|
42
|
+
// Single self-contained statements run against either arm alike; only
|
|
43
|
+
// transactions (and the lifecycle behind them) need the declaration itself.
|
|
44
|
+
const db = queryableOf(options.db);
|
|
45
|
+
// The widest environment any subsystem asks for (IngestionEnvironment ⊇
|
|
46
|
+
// ExecutionEnvironment), built once and handed to all of them.
|
|
47
|
+
const environment = {
|
|
48
|
+
db: options.db,
|
|
49
|
+
caseTypeFor,
|
|
50
|
+
claimTtlMs: options.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS,
|
|
51
|
+
heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,
|
|
52
|
+
ingestion: normalizeIngestion(options.ingestion),
|
|
53
|
+
now,
|
|
54
|
+
timers: realTimers,
|
|
55
|
+
};
|
|
56
|
+
/** Load the case, resolve its type from the registry, validate state against the type's schema. */
|
|
57
|
+
const load = async (caseId) => {
|
|
58
|
+
const { definition, handle, state } = await resolveCase(db, caseTypeFor, caseId);
|
|
59
|
+
return {
|
|
60
|
+
definition,
|
|
61
|
+
snapshot: { id: handle.id, state, endedAt: handle.endedAt },
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
createCase: async (caseTypeName, initialState) => {
|
|
66
|
+
const definition = caseTypeFor(caseTypeName);
|
|
67
|
+
return withTransaction(options.db, (tx) => insertCase(tx, caseTypeName, definition.state, initialState));
|
|
68
|
+
},
|
|
69
|
+
affordances: async (caseId, actor, asOf) => {
|
|
70
|
+
const { definition, snapshot } = await load(caseId);
|
|
71
|
+
return computeAffordances(definition, snapshot, {
|
|
72
|
+
actor,
|
|
73
|
+
asOf: asOf ?? now(),
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
affordancesOf: (handle, actor, asOf) => computeAffordances(caseTypeFor(handle.caseTypeName), { id: handle.id, state: handle.state, endedAt: handle.endedAt }, { actor, asOf: asOf ?? now() }),
|
|
77
|
+
explain: async (caseId, stepName, explainOptions = {}) => {
|
|
78
|
+
const { definition, snapshot } = await load(caseId);
|
|
79
|
+
// The boundary's one normalization (absent-vs-undefined actor, the
|
|
80
|
+
// asOf default) lives with the pure computation — see explainContext.
|
|
81
|
+
return computeExplanation(definition, snapshot, stepName, explainContext(explainOptions, now));
|
|
82
|
+
},
|
|
83
|
+
// Rebuilt field by field, not spread: whatever extra properties a
|
|
84
|
+
// caller's object drags along stop here, so the lifecycle only ever
|
|
85
|
+
// sees the options the public interface declares.
|
|
86
|
+
execute: (caseId, stepName, { actor, scopeKey, input, asOf, claimTtlMs, heartbeatMs, retry }) => executeStep(environment, caseId, stepName, {
|
|
87
|
+
actor,
|
|
88
|
+
scopeKey,
|
|
89
|
+
input,
|
|
90
|
+
asOf,
|
|
91
|
+
claimTtlMs,
|
|
92
|
+
heartbeatMs,
|
|
93
|
+
retry,
|
|
94
|
+
}),
|
|
95
|
+
journal: (caseId, filter) => readJournal(db, caseId, filter),
|
|
96
|
+
case: async (caseId) => {
|
|
97
|
+
const { handle, state } = await resolveCase(db, caseTypeFor, caseId);
|
|
98
|
+
return { ...handle, state };
|
|
99
|
+
},
|
|
100
|
+
correlationOf: (system, externalId) => lookupCorrelation(db, system, externalId),
|
|
101
|
+
inputSchemaFor: (caseTypeName, stepName) => caseTypeFor(caseTypeName).getStep(stepName)?.input ?? null,
|
|
102
|
+
stepMetadataFor: (caseTypeName, stepName) => {
|
|
103
|
+
const step = caseTypeFor(caseTypeName).getStep(stepName);
|
|
104
|
+
if (step === undefined)
|
|
105
|
+
return null;
|
|
106
|
+
return { title: step.title, description: step.description };
|
|
107
|
+
},
|
|
108
|
+
ingest: (event) => ingest(environment, event),
|
|
109
|
+
correlate: (registration) => registerCorrelation(db, registration),
|
|
110
|
+
correlations: (caseId, scopeKey) => correlationsFor(db, caseId, scopeKey),
|
|
111
|
+
deadLetters: (filter) => readDeadLetters(db, filter),
|
|
112
|
+
migrate: (caseTypeName, name, transform, migrationOptions) => migrate(environment, caseTypeName, name, transform, migrationOptions),
|
|
113
|
+
};
|
|
114
|
+
};
|
|
115
|
+
//# sourceMappingURL=engine.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"engine.js","sourceRoot":"","sources":["../../src/engine/engine.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AASH,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,WAAW,EACX,WAAW,EACX,UAAU,EACV,eAAe,GAChB,MAAM,uBAAuB,CAAA;AAY9B,OAAO,EACL,eAAe,EACf,MAAM,EACN,iBAAiB,EACjB,kBAAkB,EAClB,eAAe,EACf,mBAAmB,GACpB,MAAM,uBAAuB,CAAA;AAM9B,OAAO,EAAE,OAAO,EAAE,MAAM,uBAAuB,CAAA;AAG/C,OAAO,EAAE,UAAU,EAAE,WAAW,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAA;AAOxE,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAA;AACrB,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA;AA0LlD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAG,CAAC,OAAsB,EAAU,EAAE;IAC7D,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAuB,CAAA;IAC/C,KAAK,MAAM,UAAU,IAAI,OAAO,CAAC,SAAS,EAAE,CAAC;QAC3C,IAAI,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,CAAC;YAClC,MAAM,IAAI,SAAS,CACjB,2CAA2C,UAAU,CAAC,IAAI,GAAG,CAC9D,CAAA;QACH,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,UAAU,CAAC,IAAI,EAAE,UAAU,CAAC,CAAA;IAC3C,CAAC;IAED,MAAM,WAAW,GAAG,CAAC,YAAoB,EAAe,EAAE;QACxD,MAAM,UAAU,GAAG,QAAQ,CAAC,GAAG,CAAC,YAAY,CAAC,CAAA;QAC7C,IAAI,UAAU,KAAK,SAAS,EAAE,CAAC;YAC7B,MAAM,IAAI,oBAAoB,CAAC,YAAY,EAAE,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAA;QACpE,CAAC;QACD,OAAO,UAAU,CAAA;IACnB,CAAC,CAAA;IAED,MAAM,GAAG,GAAG,OAAO,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,IAAI,EAAE,CAAC,CAAA;IAE7C,sEAAsE;IACtE,4EAA4E;IAC5E,MAAM,EAAE,GAAG,WAAW,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAElC,wEAAwE;IACxE,+DAA+D;IAC/D,MAAM,WAAW,GAAyB;QACxC,EAAE,EAAE,OAAO,CAAC,EAAE;QACd,WAAW;QACX,UAAU,EAAE,OAAO,CAAC,UAAU,IAAI,oBAAoB;QACtD,WAAW,EAAE,OAAO,CAAC,WAAW,IAAI,oBAAoB;QACxD,SAAS,EAAE,kBAAkB,CAAC,OAAO,CAAC,SAAS,CAAC;QAChD,GAAG;QACH,MAAM,EAAE,UAAU;KACnB,CAAA;IAED,mGAAmG;IACnG,MAAM,IAAI,GAAG,KAAK,EAChB,MAAc,EACyD,EAAE;QACzE,MAAM,EAAE,UAAU,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CACrD,EAAE,EACF,WAAW,EACX,MAAM,CACP,CAAA;QACD,OAAO;YACL,UAAU;YACV,QAAQ,EAAE,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE;SAC5D,CAAA;IACH,CAAC,CAAA;IAED,OAAO;QACL,UAAU,EAAE,KAAK,EAAE,YAAY,EAAE,YAAY,EAAE,EAAE;YAC/C,MAAM,UAAU,GAAG,WAAW,CAAC,YAAY,CAAC,CAAA;YAC5C,OAAO,eAAe,CAAC,OAAO,CAAC,EAAE,EAAE,CAAC,EAAE,EAAE,EAAE,CACxC,UAAU,CAAC,EAAE,EAAE,YAAY,EAAE,UAAU,CAAC,KAAK,EAAE,YAAY,CAAC,CAC7D,CAAA;QACH,CAAC;QACD,WAAW,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE;YACzC,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAA;YACnD,OAAO,kBAAkB,CAAC,UAAU,EAAE,QAAQ,EAAE;gBAC9C,KAAK;gBACL,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE;aACpB,CAAC,CAAA;QACJ,CAAC;QACD,aAAa,EAAE,CAAC,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,EAAE,CACrC,kBAAkB,CAChB,WAAW,CAAC,MAAM,CAAC,YAAY,CAAC,EAChC,EAAE,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,KAAK,EAAE,MAAM,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,OAAO,EAAE,EAC/D,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,IAAI,GAAG,EAAE,EAAE,CAC/B;QACH,OAAO,EAAE,KAAK,EAAE,MAAM,EAAE,QAAQ,EAAE,cAAc,GAAG,EAAE,EAAE,EAAE;YACvD,MAAM,EAAE,UAAU,EAAE,QAAQ,EAAE,GAAG,MAAM,IAAI,CAAC,MAAM,CAAC,CAAA;YACnD,mEAAmE;YACnE,sEAAsE;YACtE,OAAO,kBAAkB,CACvB,UAAU,EACV,QAAQ,EACR,QAAQ,EACR,cAAc,CAAC,cAAc,EAAE,GAAG,CAAC,CACpC,CAAA;QACH,CAAC;QACD,kEAAkE;QAClE,oEAAoE;QACpE,kDAAkD;QAClD,OAAO,EAAE,CACP,MAAM,EACN,QAAQ,EACR,EAAE,KAAK,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,EAAE,UAAU,EAAE,WAAW,EAAE,KAAK,EAAE,EAChE,EAAE,CACF,WAAW,CAAC,WAAW,EAAE,MAAM,EAAE,QAAQ,EAAE;YACzC,KAAK;YACL,QAAQ;YACR,KAAK;YACL,IAAI;YACJ,UAAU;YACV,WAAW;YACX,KAAK;SACN,CAAC;QACJ,OAAO,EAAE,CAAC,MAAM,EAAE,MAAM,EAAE,EAAE,CAAC,WAAW,CAAC,EAAE,EAAE,MAAM,EAAE,MAAM,CAAC;QAC5D,IAAI,EAAE,KAAK,EAAE,MAAM,EAAE,EAAE;YACrB,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,WAAW,CAAC,EAAE,EAAE,WAAW,EAAE,MAAM,CAAC,CAAA;YACpE,OAAO,EAAE,GAAG,MAAM,EAAE,KAAK,EAAE,CAAA;QAC7B,CAAC;QACD,aAAa,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE,CACpC,iBAAiB,CAAC,EAAE,EAAE,MAAM,EAAE,UAAU,CAAC;QAC3C,cAAc,EAAE,CAAC,YAAY,EAAE,QAAQ,EAAE,EAAE,CACzC,WAAW,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,EAAE,KAAK,IAAI,IAAI;QAC5D,eAAe,EAAE,CAAC,YAAY,EAAE,QAAQ,EAAE,EAAE;YAC1C,MAAM,IAAI,GAAG,WAAW,CAAC,YAAY,CAAC,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAA;YACxD,IAAI,IAAI,KAAK,SAAS;gBAAE,OAAO,IAAI,CAAA;YACnC,OAAO,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAA;QAC7D,CAAC;QACD,MAAM,EAAE,CAAC,KAAK,EAAE,EAAE,CAAC,MAAM,CAAC,WAAW,EAAE,KAAK,CAAC;QAC7C,SAAS,EAAE,CAAC,YAAY,EAAE,EAAE,CAAC,mBAAmB,CAAC,EAAE,EAAE,YAAY,CAAC;QAClE,YAAY,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC;QACzE,WAAW,EAAE,CAAC,MAAM,EAAE,EAAE,CAAC,eAAe,CAAC,EAAE,EAAE,MAAM,CAAC;QACpD,OAAO,EAAE,CAAC,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,EAAE,EAAE,CAC3D,OAAO,CAAC,WAAW,EAAE,YAAY,EAAE,IAAI,EAAE,SAAS,EAAE,gBAAgB,CAAC;KACxE,CAAA;AACH,CAAC,CAAA","sourcesContent":["/**\n * The engine: the case-type registry bound to the case store.\n *\n * `createEngine({ db, caseTypes })` wires the halves together — persistence\n * (`../store`), guard evaluation (`../guards`), the definition API\n * (`../model`) and the execution lifecycle (`../execution`) — into the\n * framework's public face: `affordances`, `explain`, `execute`, `journal`.\n *\n * The engine is where `asOf` defaults to now: conditions never read the\n * clock, so `EngineOptions.now` — wall clock by default — is the one clock,\n * threaded through the environment to everything below, and everything below\n * it is pure and reconstructable.\n */\n\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type {\n ExecuteOptions,\n ExecutionResult,\n JournalEntry,\n JournalFilter,\n} from '../execution/index.js'\nimport {\n DEFAULT_CLAIM_TTL_MS,\n DEFAULT_HEARTBEAT_MS,\n executeStep,\n readJournal,\n realTimers,\n withTransaction,\n} from '../execution/index.js'\nimport type { Instant } from '../guards/index.js'\nimport type {\n Correlation,\n CorrelationRegistration,\n DeadLetter,\n DeadLetterFilter,\n ExternalEvent,\n IngestionEnvironment,\n IngestionOptions,\n IngestionResult,\n} from '../ingestion/index.js'\nimport {\n correlationsFor,\n ingest,\n lookupCorrelation,\n normalizeIngestion,\n readDeadLetters,\n registerCorrelation,\n} from '../ingestion/index.js'\nimport type {\n MigrationOptions,\n MigrationReport,\n MigrationTransform,\n} from '../migration/index.js'\nimport { migrate } from '../migration/index.js'\nimport type { AnyCaseType, StepMetadata } from '../model/index.js'\nimport type { CaseHandle, DatabaseAccess } from '../store/index.js'\nimport { insertCase, queryableOf, resolveCase } from '../store/index.js'\nimport type {\n AffordanceExplanation,\n CaseAffordances,\n CaseSnapshot,\n ExplainRequest,\n} from './compute.js'\nimport {\n computeAffordances,\n computeExplanation,\n explainContext,\n} from './compute.js'\nimport { UnknownCaseTypeError } from './errors.js'\n\n/** Options for {@link createEngine}. */\nexport interface EngineOptions {\n /**\n * The app brings its database, declaring which kind of handle it has:\n * `{ pool }` for anything that checks out connections (`pg.Pool`, or any\n * wrapper declaring `connect()`), `{ client }` for a single connection\n * dedicated to the engine. The declaration is what lets `execute` run its\n * transactions on one connection without guessing — see\n * {@link DatabaseAccess}.\n */\n readonly db: DatabaseAccess\n /** Every case type this engine serves; a loaded case's `case_type` must name one of them. */\n readonly caseTypes: readonly AnyCaseType[]\n /**\n * How long an Execution's claim survives without a heartbeat (default 30s).\n * The floor on how long a crashed handler can hold a case.\n */\n readonly claimTtlMs?: number\n /** How often a running handler refreshes its claim (default 5s). */\n readonly heartbeatMs?: number\n /**\n * Event ingestion: how an external event's Actor is derived.\n * Correlation needs no configuration — it is a registry, not a policy.\n */\n readonly ingestion?: IngestionOptions\n /**\n * The clock — every process-side \"as of now\" below the boundary reads\n * through it: guard evaluation instants, journal timestamps, ingestion\n * receipt times. Defaults to the wall clock. Two \"nows\" it deliberately\n * does not govern: lease expiry is judged by the storage adapter's own\n * clock (the one clock all competing processes share), and the retry\n * delay / heartbeat cadence run on process timers (an internal seam the\n * lifecycle's own tests drive virtually).\n */\n readonly now?: () => Date\n}\n\n/**\n * Options for {@link Engine.explain}. Omit `actor` to probe `requires`\n * alone: `permits` conditions are then reported un-evaluated (failed, with\n * the stated reason `'not evaluated: no actor supplied'`) rather than run\n * against nothing. The normalization rule is `explainContext`, stated and\n * tested beside the pure computation it feeds.\n */\nexport type ExplainOptions = ExplainRequest\n\nexport interface Engine {\n /**\n * Create a case of a registered case type. The initial state is validated\n * against the type's schema.\n */\n createCase(\n caseTypeName: string,\n initialState: unknown,\n ): Promise<CaseHandle<unknown>>\n\n /**\n * Compute the affordances record for a case: load it, evaluate every\n * step's guard for `actor` (with scope fan-out), and return the\n * serializable available + blocked answer. Dormant (ended) cases compute\n * like any other — dormancy is annotation, never a freeze.\n */\n affordances(\n caseId: string,\n actor: unknown,\n asOf?: Instant,\n ): Promise<CaseAffordances>\n\n /**\n * {@link Engine.affordances} for a case already in hand — no second load,\n * no re-validation. Synchronous: a registry read plus the pure\n * computation. The handle must be one this engine issued\n * ({@link Engine.createCase}, {@link Engine.case}) — their `state` is the\n * validated document; a hand-built handle carries no such guarantee.\n * This is how a create route answers with the fresh case's affordances\n * without re-reading what it just wrote.\n */\n affordancesOf(\n handle: CaseHandle<unknown>,\n actor: unknown,\n asOf?: Instant,\n ): CaseAffordances\n\n /** The full per-condition breakdown for one step (× scope element) of a case. */\n explain(\n caseId: string,\n stepName: string,\n options?: ExplainOptions,\n ): Promise<AffordanceExplanation>\n\n /**\n * Execute a step on a case: claim → run → commit. The claim\n * re-evaluates the guard transactionally — the enforcement moment — so an\n * affordance that has since gone away rejects with `StepNotAvailableError`\n * carrying the current unmet conditions, and a case with an Execution\n * already in flight rejects with `CaseBusyError`.\n */\n execute(\n caseId: string,\n stepName: string,\n options: ExecuteOptions,\n ): Promise<ExecutionResult>\n\n /**\n * Read a case's journal, oldest first. Filter by `scopeKey` for\n * a per-track audit — \"everything that happened on buyer #7\".\n */\n journal(\n caseId: string,\n filter?: JournalFilter,\n ): Promise<readonly JournalEntry[]>\n\n /**\n * Read one case as persisted: the row plus its Case State validated\n * against the registered schema. Loud — an addressed read owes an answer,\n * so an unknown case or a state its schema no longer accepts throws.\n * The read ops surfaces, tests and consoles would otherwise each\n * hand-write in SQL.\n */\n case(caseId: string): Promise<CaseHandle<unknown>>\n\n /** Where an external identifier routes — the reverse of {@link Engine.correlations}. */\n correlationOf(system: string, externalId: string): Promise<Correlation | null>\n\n /**\n * The declared input schema of one step of a registered case type, or\n * `null` when the step takes no input. Synchronous — a registry read. The\n * engine owns the registry and a case names its type, so an adapter never\n * needs to be handed the case types a second time to describe inputs.\n */\n inputSchemaFor(\n caseTypeName: string,\n stepName: string,\n ): StandardSchemaV1 | null\n\n /**\n * The declared human metadata of one step of a registered case type —\n * `title` and `description`, each `null` when undeclared — or `null` for\n * an unknown step. Synchronous, a registry read like\n * {@link Engine.inputSchemaFor}, and for the same reason: adapters\n * serialize step metadata from here rather than being handed the case\n * types a second time.\n */\n stepMetadataFor(caseTypeName: string, stepName: string): StepMetadata | null\n\n /**\n * Ingest one external event: dedup, correlate, then an ordinary Execution\n * with the external system as the actor. Never throws for an\n * event's own sake — an event that could not be applied is dead-lettered\n * with a reason, so a webhook endpoint can acknowledge and move on.\n */\n ingest(event: ExternalEvent): Promise<IngestionResult>\n\n /**\n * Register an external identifier against a case out of band. Handlers\n * should prefer `ctx.correlate(...)`, which rides the same commit as the\n * state recording that the interaction was started.\n */\n correlate(registration: CorrelationRegistration): Promise<Correlation>\n\n /** Every external identifier registered against a case (× scope element). */\n correlations(\n caseId: string,\n scopeKey?: string,\n ): Promise<readonly Correlation[]>\n\n /** The dead-letter surface: events that arrived and changed nothing, with why. */\n deadLetters(filter?: DeadLetterFilter): Promise<readonly DeadLetter[]>\n\n /**\n * Restructure the state of every case of a case type, as journaled system\n * Executions (float's escape hatch). Idempotent: a case that\n * already carries the migration's marker is skipped, so re-running is a\n * no-op and an interrupted run resumes. Reach for it only when no total\n * condition can read the old shape — see `docs/migration.md`.\n */\n migrate(\n caseTypeName: string,\n name: string,\n transform: MigrationTransform,\n options?: MigrationOptions,\n ): Promise<MigrationReport>\n}\n\n/**\n * Build an engine from the app's database and its case type definitions.\n * Throws at construction on duplicate case type names — the registry is\n * keyed by name, which is all a case row records (definitions\n * float; only the name is persisted).\n */\nexport const createEngine = (options: EngineOptions): Engine => {\n const registry = new Map<string, AnyCaseType>()\n for (const definition of options.caseTypes) {\n if (registry.has(definition.name)) {\n throw new TypeError(\n `createEngine: duplicate case type name '${definition.name}'`,\n )\n }\n registry.set(definition.name, definition)\n }\n\n const caseTypeFor = (caseTypeName: string): AnyCaseType => {\n const definition = registry.get(caseTypeName)\n if (definition === undefined) {\n throw new UnknownCaseTypeError(caseTypeName, [...registry.keys()])\n }\n return definition\n }\n\n const now = options.now ?? (() => new Date())\n\n // Single self-contained statements run against either arm alike; only\n // transactions (and the lifecycle behind them) need the declaration itself.\n const db = queryableOf(options.db)\n\n // The widest environment any subsystem asks for (IngestionEnvironment ⊇\n // ExecutionEnvironment), built once and handed to all of them.\n const environment: IngestionEnvironment = {\n db: options.db,\n caseTypeFor,\n claimTtlMs: options.claimTtlMs ?? DEFAULT_CLAIM_TTL_MS,\n heartbeatMs: options.heartbeatMs ?? DEFAULT_HEARTBEAT_MS,\n ingestion: normalizeIngestion(options.ingestion),\n now,\n timers: realTimers,\n }\n\n /** Load the case, resolve its type from the registry, validate state against the type's schema. */\n const load = async (\n caseId: string,\n ): Promise<{ definition: AnyCaseType; snapshot: CaseSnapshot<unknown> }> => {\n const { definition, handle, state } = await resolveCase(\n db,\n caseTypeFor,\n caseId,\n )\n return {\n definition,\n snapshot: { id: handle.id, state, endedAt: handle.endedAt },\n }\n }\n\n return {\n createCase: async (caseTypeName, initialState) => {\n const definition = caseTypeFor(caseTypeName)\n return withTransaction(options.db, (tx) =>\n insertCase(tx, caseTypeName, definition.state, initialState),\n )\n },\n affordances: async (caseId, actor, asOf) => {\n const { definition, snapshot } = await load(caseId)\n return computeAffordances(definition, snapshot, {\n actor,\n asOf: asOf ?? now(),\n })\n },\n affordancesOf: (handle, actor, asOf) =>\n computeAffordances(\n caseTypeFor(handle.caseTypeName),\n { id: handle.id, state: handle.state, endedAt: handle.endedAt },\n { actor, asOf: asOf ?? now() },\n ),\n explain: async (caseId, stepName, explainOptions = {}) => {\n const { definition, snapshot } = await load(caseId)\n // The boundary's one normalization (absent-vs-undefined actor, the\n // asOf default) lives with the pure computation — see explainContext.\n return computeExplanation(\n definition,\n snapshot,\n stepName,\n explainContext(explainOptions, now),\n )\n },\n // Rebuilt field by field, not spread: whatever extra properties a\n // caller's object drags along stop here, so the lifecycle only ever\n // sees the options the public interface declares.\n execute: (\n caseId,\n stepName,\n { actor, scopeKey, input, asOf, claimTtlMs, heartbeatMs, retry },\n ) =>\n executeStep(environment, caseId, stepName, {\n actor,\n scopeKey,\n input,\n asOf,\n claimTtlMs,\n heartbeatMs,\n retry,\n }),\n journal: (caseId, filter) => readJournal(db, caseId, filter),\n case: async (caseId) => {\n const { handle, state } = await resolveCase(db, caseTypeFor, caseId)\n return { ...handle, state }\n },\n correlationOf: (system, externalId) =>\n lookupCorrelation(db, system, externalId),\n inputSchemaFor: (caseTypeName, stepName) =>\n caseTypeFor(caseTypeName).getStep(stepName)?.input ?? null,\n stepMetadataFor: (caseTypeName, stepName) => {\n const step = caseTypeFor(caseTypeName).getStep(stepName)\n if (step === undefined) return null\n return { title: step.title, description: step.description }\n },\n ingest: (event) => ingest(environment, event),\n correlate: (registration) => registerCorrelation(db, registration),\n correlations: (caseId, scopeKey) => correlationsFor(db, caseId, scopeKey),\n deadLetters: (filter) => readDeadLetters(db, filter),\n migrate: (caseTypeName, name, transform, migrationOptions) =>\n migrate(environment, caseTypeName, name, transform, migrationOptions),\n }\n}\n"]}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import { AffordanceError } from '../errors.js';
|
|
2
|
+
/** A loaded case's `case_type` names no case type registered with the engine. */
|
|
3
|
+
export declare class UnknownCaseTypeError extends AffordanceError {
|
|
4
|
+
readonly caseTypeName: string;
|
|
5
|
+
constructor(caseTypeName: string, registered: readonly string[]);
|
|
6
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { AffordanceError } from '../errors.js';
|
|
2
|
+
/** A loaded case's `case_type` names no case type registered with the engine. */
|
|
3
|
+
export class UnknownCaseTypeError extends AffordanceError {
|
|
4
|
+
caseTypeName;
|
|
5
|
+
constructor(caseTypeName, registered) {
|
|
6
|
+
super('not-found', `unknown case type '${caseTypeName}' — registered case types: ${registered.length > 0 ? registered.join(', ') : '(none)'}`);
|
|
7
|
+
this.name = 'UnknownCaseTypeError';
|
|
8
|
+
this.caseTypeName = caseTypeName;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../../src/engine/errors.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAE9C,iFAAiF;AACjF,MAAM,OAAO,oBAAqB,SAAQ,eAAe;IAC9C,YAAY,CAAQ;IAE7B,YAAY,YAAoB,EAAE,UAA6B;QAC7D,KAAK,CACH,WAAW,EACX,sBAAsB,YAAY,8BAChC,UAAU,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,QAClD,EAAE,CACH,CAAA;QACD,IAAI,CAAC,IAAI,GAAG,sBAAsB,CAAA;QAClC,IAAI,CAAC,YAAY,GAAG,YAAY,CAAA;IAClC,CAAC;CACF","sourcesContent":["import { AffordanceError } from '../errors.js'\n\n/** A loaded case's `case_type` names no case type registered with the engine. */\nexport class UnknownCaseTypeError extends AffordanceError {\n readonly caseTypeName: string\n\n constructor(caseTypeName: string, registered: readonly string[]) {\n super(\n 'not-found',\n `unknown case type '${caseTypeName}' — registered case types: ${\n registered.length > 0 ? registered.join(', ') : '(none)'\n }`,\n )\n this.name = 'UnknownCaseTypeError'\n this.caseTypeName = caseTypeName\n }\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The affordance engine.
|
|
3
|
+
*
|
|
4
|
+
* An **affordance** is a step (× scope binding) currently available on a
|
|
5
|
+
* case for an actor — computed from guards over state, queryable,
|
|
6
|
+
* explainable (CONTEXT.md; spec §Core model). This module is the
|
|
7
|
+
* framework's public face: the pure computation (`computeAffordances`,
|
|
8
|
+
* `computeExplanation`) and the store-bound engine (`createEngine` →
|
|
9
|
+
* `affordances` / `explain` / `execute` / `journal`). Nothing in the
|
|
10
|
+
* computation ever invokes a handler — that is `../execution`'s job alone.
|
|
11
|
+
*/
|
|
12
|
+
export type { Affordance, AffordanceExplanation, BlockedStep, CaseAffordances, CaseSnapshot, ExplainRequest, } from './compute.js';
|
|
13
|
+
export { computeAffordances, computeExplanation, explainContext, } from './compute.js';
|
|
14
|
+
export type { Engine, EngineOptions, ExplainOptions } from './engine.js';
|
|
15
|
+
export { createEngine } from './engine.js';
|
|
16
|
+
export { UnknownCaseTypeError } from './errors.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The affordance engine.
|
|
3
|
+
*
|
|
4
|
+
* An **affordance** is a step (× scope binding) currently available on a
|
|
5
|
+
* case for an actor — computed from guards over state, queryable,
|
|
6
|
+
* explainable (CONTEXT.md; spec §Core model). This module is the
|
|
7
|
+
* framework's public face: the pure computation (`computeAffordances`,
|
|
8
|
+
* `computeExplanation`) and the store-bound engine (`createEngine` →
|
|
9
|
+
* `affordances` / `explain` / `execute` / `journal`). Nothing in the
|
|
10
|
+
* computation ever invokes a handler — that is `../execution`'s job alone.
|
|
11
|
+
*/
|
|
12
|
+
export { computeAffordances, computeExplanation, explainContext, } from './compute.js';
|
|
13
|
+
export { createEngine } from './engine.js';
|
|
14
|
+
export { UnknownCaseTypeError } from './errors.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/engine/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAUH,OAAO,EACL,kBAAkB,EAClB,kBAAkB,EAClB,cAAc,GACf,MAAM,cAAc,CAAA;AAErB,OAAO,EAAE,YAAY,EAAE,MAAM,aAAa,CAAA;AAC1C,OAAO,EAAE,oBAAoB,EAAE,MAAM,aAAa,CAAA","sourcesContent":["/**\n * The affordance engine.\n *\n * An **affordance** is a step (× scope binding) currently available on a\n * case for an actor — computed from guards over state, queryable,\n * explainable (CONTEXT.md; spec §Core model). This module is the\n * framework's public face: the pure computation (`computeAffordances`,\n * `computeExplanation`) and the store-bound engine (`createEngine` →\n * `affordances` / `explain` / `execute` / `journal`). Nothing in the\n * computation ever invokes a handler — that is `../execution`'s job alone.\n */\n\nexport type {\n Affordance,\n AffordanceExplanation,\n BlockedStep,\n CaseAffordances,\n CaseSnapshot,\n ExplainRequest,\n} from './compute.js'\nexport {\n computeAffordances,\n computeExplanation,\n explainContext,\n} from './compute.js'\nexport type { Engine, EngineOptions, ExplainOptions } from './engine.js'\nexport { createEngine } from './engine.js'\nexport { UnknownCaseTypeError } from './errors.js'\n"]}
|
package/dist/errors.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Every way the framework refuses or fails carries a **code**: a stable,
|
|
5
|
+
* wire-safe name for the *kind* of refusal, distinct from the message (which
|
|
6
|
+
* is prose, and which an adapter may have to rewrite for the audience it is
|
|
7
|
+
* answering — see the HTTP contract's visibility rule).
|
|
8
|
+
*
|
|
9
|
+
* The code lives here, on the error, rather than in a mapping table at each
|
|
10
|
+
* edge. An adapter that turns framework errors into responses then translates
|
|
11
|
+
* a closed set of codes instead of enumerating error classes it has to be
|
|
12
|
+
* told about: adding an error class cannot silently produce a 500, because
|
|
13
|
+
* the class cannot be constructed without declaring what it is.
|
|
14
|
+
*
|
|
15
|
+
* Anything not an {@link AffordanceError} is not the framework's refusal —
|
|
16
|
+
* it is a bug or an infrastructure failure, and edges should let it through
|
|
17
|
+
* rather than dress it up as an answer.
|
|
18
|
+
*/
|
|
19
|
+
import type { RefusalCode } from '@affordance/contract';
|
|
20
|
+
/**
|
|
21
|
+
* What kind of refusal or failure this is. Deliberately small and closed:
|
|
22
|
+
* these are the answers the affordance contract is specified in terms of, so
|
|
23
|
+
* a new code *is* a contract change — and it reads as one, because the set
|
|
24
|
+
* has exactly one declaration, in `@affordance/contract` (`REFUSAL_CODES`),
|
|
25
|
+
* from which this type derives.
|
|
26
|
+
*/
|
|
27
|
+
export type AffordanceErrorCode = RefusalCode;
|
|
28
|
+
/**
|
|
29
|
+
* The base of every error the framework raises deliberately. Subclasses
|
|
30
|
+
* declare their {@link AffordanceErrorCode} at construction — there is no
|
|
31
|
+
* default, so the taxonomy cannot be extended by accident.
|
|
32
|
+
*/
|
|
33
|
+
export declare class AffordanceError extends Error {
|
|
34
|
+
readonly code: AffordanceErrorCode;
|
|
35
|
+
constructor(code: AffordanceErrorCode, message: string, options?: ErrorOptions);
|
|
36
|
+
}
|
|
37
|
+
/** Narrow an unknown throw to a deliberate framework refusal. */
|
|
38
|
+
export declare const isAffordanceError: (error: unknown) => error is AffordanceError;
|
|
39
|
+
/**
|
|
40
|
+
* The human-readable message of whatever was thrown — Error or not. The one
|
|
41
|
+
* spelling of a conversion that guards, targets, and the lifecycle all need,
|
|
42
|
+
* because app code can throw anything.
|
|
43
|
+
*/
|
|
44
|
+
export declare const thrownMessage: (error: unknown) => string;
|
|
45
|
+
/** Convert an unknown throw into an `Error`, preserving one that already is. */
|
|
46
|
+
export declare const toError: (error: unknown) => Error;
|
package/dist/errors.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The framework's error taxonomy.
|
|
3
|
+
*
|
|
4
|
+
* Every way the framework refuses or fails carries a **code**: a stable,
|
|
5
|
+
* wire-safe name for the *kind* of refusal, distinct from the message (which
|
|
6
|
+
* is prose, and which an adapter may have to rewrite for the audience it is
|
|
7
|
+
* answering — see the HTTP contract's visibility rule).
|
|
8
|
+
*
|
|
9
|
+
* The code lives here, on the error, rather than in a mapping table at each
|
|
10
|
+
* edge. An adapter that turns framework errors into responses then translates
|
|
11
|
+
* a closed set of codes instead of enumerating error classes it has to be
|
|
12
|
+
* told about: adding an error class cannot silently produce a 500, because
|
|
13
|
+
* the class cannot be constructed without declaring what it is.
|
|
14
|
+
*
|
|
15
|
+
* Anything not an {@link AffordanceError} is not the framework's refusal —
|
|
16
|
+
* it is a bug or an infrastructure failure, and edges should let it through
|
|
17
|
+
* rather than dress it up as an answer.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* The base of every error the framework raises deliberately. Subclasses
|
|
21
|
+
* declare their {@link AffordanceErrorCode} at construction — there is no
|
|
22
|
+
* default, so the taxonomy cannot be extended by accident.
|
|
23
|
+
*/
|
|
24
|
+
export class AffordanceError extends Error {
|
|
25
|
+
code;
|
|
26
|
+
constructor(code, message, options) {
|
|
27
|
+
super(message, options);
|
|
28
|
+
this.name = 'AffordanceError';
|
|
29
|
+
this.code = code;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
/** Narrow an unknown throw to a deliberate framework refusal. */
|
|
33
|
+
export const isAffordanceError = (error) => error instanceof AffordanceError;
|
|
34
|
+
/**
|
|
35
|
+
* The human-readable message of whatever was thrown — Error or not. The one
|
|
36
|
+
* spelling of a conversion that guards, targets, and the lifecycle all need,
|
|
37
|
+
* because app code can throw anything.
|
|
38
|
+
*/
|
|
39
|
+
export const thrownMessage = (error) => error instanceof Error ? error.message : String(error);
|
|
40
|
+
/** Convert an unknown throw into an `Error`, preserving one that already is. */
|
|
41
|
+
export const toError = (error) => error instanceof Error ? error : new Error(thrownMessage(error));
|
|
42
|
+
//# sourceMappingURL=errors.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAaH;;;;GAIG;AACH,MAAM,OAAO,eAAgB,SAAQ,KAAK;IAC/B,IAAI,CAAqB;IAElC,YACE,IAAyB,EACzB,OAAe,EACf,OAAsB;QAEtB,KAAK,CAAC,OAAO,EAAE,OAAO,CAAC,CAAA;QACvB,IAAI,CAAC,IAAI,GAAG,iBAAiB,CAAA;QAC7B,IAAI,CAAC,IAAI,GAAG,IAAI,CAAA;IAClB,CAAC;CACF;AAED,iEAAiE;AACjE,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,KAAc,EAA4B,EAAE,CAC5E,KAAK,YAAY,eAAe,CAAA;AAElC;;;;GAIG;AACH,MAAM,CAAC,MAAM,aAAa,GAAG,CAAC,KAAc,EAAU,EAAE,CACtD,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AAExD,gFAAgF;AAChF,MAAM,CAAC,MAAM,OAAO,GAAG,CAAC,KAAc,EAAS,EAAE,CAC/C,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,KAAK,CAAC,aAAa,CAAC,KAAK,CAAC,CAAC,CAAA","sourcesContent":["/**\n * The framework's error taxonomy.\n *\n * Every way the framework refuses or fails carries a **code**: a stable,\n * wire-safe name for the *kind* of refusal, distinct from the message (which\n * is prose, and which an adapter may have to rewrite for the audience it is\n * answering — see the HTTP contract's visibility rule).\n *\n * The code lives here, on the error, rather than in a mapping table at each\n * edge. An adapter that turns framework errors into responses then translates\n * a closed set of codes instead of enumerating error classes it has to be\n * told about: adding an error class cannot silently produce a 500, because\n * the class cannot be constructed without declaring what it is.\n *\n * Anything not an {@link AffordanceError} is not the framework's refusal —\n * it is a bug or an infrastructure failure, and edges should let it through\n * rather than dress it up as an answer.\n */\n\nimport type { RefusalCode } from '@affordance/contract'\n\n/**\n * What kind of refusal or failure this is. Deliberately small and closed:\n * these are the answers the affordance contract is specified in terms of, so\n * a new code *is* a contract change — and it reads as one, because the set\n * has exactly one declaration, in `@affordance/contract` (`REFUSAL_CODES`),\n * from which this type derives.\n */\nexport type AffordanceErrorCode = RefusalCode\n\n/**\n * The base of every error the framework raises deliberately. Subclasses\n * declare their {@link AffordanceErrorCode} at construction — there is no\n * default, so the taxonomy cannot be extended by accident.\n */\nexport class AffordanceError extends Error {\n readonly code: AffordanceErrorCode\n\n constructor(\n code: AffordanceErrorCode,\n message: string,\n options?: ErrorOptions,\n ) {\n super(message, options)\n this.name = 'AffordanceError'\n this.code = code\n }\n}\n\n/** Narrow an unknown throw to a deliberate framework refusal. */\nexport const isAffordanceError = (error: unknown): error is AffordanceError =>\n error instanceof AffordanceError\n\n/**\n * The human-readable message of whatever was thrown — Error or not. The one\n * spelling of a conversion that guards, targets, and the lifecycle all need,\n * because app code can throw anything.\n */\nexport const thrownMessage = (error: unknown): string =>\n error instanceof Error ? error.message : String(error)\n\n/** Convert an unknown throw into an `Error`, preserving one that already is. */\nexport const toError = (error: unknown): Error =>\n error instanceof Error ? error : new Error(thrownMessage(error))\n"]}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State deltas — what an Execution changed, as RFC 6902 JSON Patch.
|
|
3
|
+
*
|
|
4
|
+
* Every committed Execution journals the delta from the previous Case State
|
|
5
|
+
* to the next. A standard patch format is deliberate: the delta is
|
|
6
|
+
* an audit artifact read by people and machines that are not this library, so
|
|
7
|
+
* it should not need a bespoke decoder. Paths are RFC 6901 JSON Pointers.
|
|
8
|
+
*
|
|
9
|
+
* Pure and total over JSON values — no clock, no I/O, no schema knowledge.
|
|
10
|
+
*/
|
|
11
|
+
/** One JSON Patch operation. */
|
|
12
|
+
export type PatchOp = {
|
|
13
|
+
readonly op: 'add';
|
|
14
|
+
readonly path: string;
|
|
15
|
+
readonly value: unknown;
|
|
16
|
+
} | {
|
|
17
|
+
readonly op: 'remove';
|
|
18
|
+
readonly path: string;
|
|
19
|
+
} | {
|
|
20
|
+
readonly op: 'replace';
|
|
21
|
+
readonly path: string;
|
|
22
|
+
readonly value: unknown;
|
|
23
|
+
};
|
|
24
|
+
/** An Execution's state delta: the ops taking the previous Case State to the next. */
|
|
25
|
+
export type StateDelta = readonly PatchOp[];
|
|
26
|
+
/**
|
|
27
|
+
* Structural equality over JSON values. `undefined` never appears in a
|
|
28
|
+
* document that round-tripped through jsonb, but a handler's return value has
|
|
29
|
+
* not round-tripped yet, so it is compared as-is.
|
|
30
|
+
*/
|
|
31
|
+
export declare const jsonEqual: (a: unknown, b: unknown) => boolean;
|
|
32
|
+
/**
|
|
33
|
+
* The delta from one Case State document to the next. An Execution that
|
|
34
|
+
* changed nothing yields an empty delta — a real and unremarkable outcome
|
|
35
|
+
* (a handler whose only effect was external, or a no-op retry landing).
|
|
36
|
+
*
|
|
37
|
+
* Arrays are diffed positionally: element *i* against element *i*, then
|
|
38
|
+
* appends and trailing removals. Case State collections are keyed by the
|
|
39
|
+
* app's own identifiers (scope keys), so a positional diff of a reordered
|
|
40
|
+
* collection is verbose but never wrong.
|
|
41
|
+
*/
|
|
42
|
+
export declare const diffState: (previous: unknown, next: unknown) => StateDelta;
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* State deltas — what an Execution changed, as RFC 6902 JSON Patch.
|
|
3
|
+
*
|
|
4
|
+
* Every committed Execution journals the delta from the previous Case State
|
|
5
|
+
* to the next. A standard patch format is deliberate: the delta is
|
|
6
|
+
* an audit artifact read by people and machines that are not this library, so
|
|
7
|
+
* it should not need a bespoke decoder. Paths are RFC 6901 JSON Pointers.
|
|
8
|
+
*
|
|
9
|
+
* Pure and total over JSON values — no clock, no I/O, no schema knowledge.
|
|
10
|
+
*/
|
|
11
|
+
/** RFC 6901 escaping: `~` → `~0`, `/` → `~1`. */
|
|
12
|
+
const escapeToken = (token) => token.replace(/~/g, '~0').replace(/\//g, '~1');
|
|
13
|
+
const isPlainObject = (value) => typeof value === 'object' && value !== null && !Array.isArray(value);
|
|
14
|
+
/**
|
|
15
|
+
* Structural equality over JSON values. `undefined` never appears in a
|
|
16
|
+
* document that round-tripped through jsonb, but a handler's return value has
|
|
17
|
+
* not round-tripped yet, so it is compared as-is.
|
|
18
|
+
*/
|
|
19
|
+
export const jsonEqual = (a, b) => {
|
|
20
|
+
if (a === b)
|
|
21
|
+
return true;
|
|
22
|
+
if (Array.isArray(a) && Array.isArray(b)) {
|
|
23
|
+
return (a.length === b.length &&
|
|
24
|
+
a.every((item, index) => jsonEqual(item, b[index])));
|
|
25
|
+
}
|
|
26
|
+
if (isPlainObject(a) && isPlainObject(b)) {
|
|
27
|
+
const keys = Object.keys(a);
|
|
28
|
+
if (keys.length !== Object.keys(b).length)
|
|
29
|
+
return false;
|
|
30
|
+
return keys.every((key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]));
|
|
31
|
+
}
|
|
32
|
+
return false;
|
|
33
|
+
};
|
|
34
|
+
const diffInto = (ops, path, previous, next) => {
|
|
35
|
+
if (previous === next)
|
|
36
|
+
return;
|
|
37
|
+
if (Array.isArray(previous) && Array.isArray(next)) {
|
|
38
|
+
const shared = Math.min(previous.length, next.length);
|
|
39
|
+
for (let index = 0; index < shared; index += 1) {
|
|
40
|
+
diffInto(ops, `${path}/${index}`, previous[index], next[index]);
|
|
41
|
+
}
|
|
42
|
+
// Appends first, then trailing removals from the end backwards, so every
|
|
43
|
+
// index a `remove` names is still valid when that op is applied.
|
|
44
|
+
for (let index = previous.length; index < next.length; index += 1) {
|
|
45
|
+
ops.push({ op: 'add', path: `${path}/-`, value: next[index] });
|
|
46
|
+
}
|
|
47
|
+
for (let index = previous.length - 1; index >= next.length; index -= 1) {
|
|
48
|
+
ops.push({ op: 'remove', path: `${path}/${index}` });
|
|
49
|
+
}
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
if (isPlainObject(previous) && isPlainObject(next)) {
|
|
53
|
+
for (const key of Object.keys(previous)) {
|
|
54
|
+
const child = `${path}/${escapeToken(key)}`;
|
|
55
|
+
if (!Object.hasOwn(next, key))
|
|
56
|
+
ops.push({ op: 'remove', path: child });
|
|
57
|
+
else
|
|
58
|
+
diffInto(ops, child, previous[key], next[key]);
|
|
59
|
+
}
|
|
60
|
+
for (const key of Object.keys(next)) {
|
|
61
|
+
if (!Object.hasOwn(previous, key)) {
|
|
62
|
+
ops.push({
|
|
63
|
+
op: 'add',
|
|
64
|
+
path: `${path}/${escapeToken(key)}`,
|
|
65
|
+
value: next[key],
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
// Differing types, or two differing scalars: the whole node is replaced.
|
|
72
|
+
// An equal pair never lands here — identical references return at the top,
|
|
73
|
+
// equal scalars are identical, and matching containers recurse above
|
|
74
|
+
// (a deeply-equal subtree just emits no ops) — so no deep comparison is
|
|
75
|
+
// needed on the way down.
|
|
76
|
+
ops.push({ op: 'replace', path, value: next });
|
|
77
|
+
};
|
|
78
|
+
/**
|
|
79
|
+
* The delta from one Case State document to the next. An Execution that
|
|
80
|
+
* changed nothing yields an empty delta — a real and unremarkable outcome
|
|
81
|
+
* (a handler whose only effect was external, or a no-op retry landing).
|
|
82
|
+
*
|
|
83
|
+
* Arrays are diffed positionally: element *i* against element *i*, then
|
|
84
|
+
* appends and trailing removals. Case State collections are keyed by the
|
|
85
|
+
* app's own identifiers (scope keys), so a positional diff of a reordered
|
|
86
|
+
* collection is verbose but never wrong.
|
|
87
|
+
*/
|
|
88
|
+
export const diffState = (previous, next) => {
|
|
89
|
+
const ops = [];
|
|
90
|
+
diffInto(ops, '', previous, next);
|
|
91
|
+
return ops;
|
|
92
|
+
};
|
|
93
|
+
//# sourceMappingURL=delta.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"delta.js","sourceRoot":"","sources":["../../src/execution/delta.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * State deltas — what an Execution changed, as RFC 6902 JSON Patch.\n *\n * Every committed Execution journals the delta from the previous Case State\n * to the next. A standard patch format is deliberate: the delta is\n * an audit artifact read by people and machines that are not this library, so\n * it should not need a bespoke decoder. Paths are RFC 6901 JSON Pointers.\n *\n * Pure and total over JSON values — no clock, no I/O, no schema knowledge.\n */\n\n/** One JSON Patch operation. */\nexport type PatchOp =\n | { readonly op: 'add'; readonly path: string; readonly value: unknown }\n | { readonly op: 'remove'; readonly path: string }\n | { readonly op: 'replace'; readonly path: string; readonly value: unknown }\n\n/** An Execution's state delta: the ops taking the previous Case State to the next. */\nexport type StateDelta = readonly PatchOp[]\n\n/** RFC 6901 escaping: `~` → `~0`, `/` → `~1`. */\nconst escapeToken = (token: string): string =>\n token.replace(/~/g, '~0').replace(/\\//g, '~1')\n\nconst isPlainObject = (value: unknown): value is Record<string, unknown> =>\n typeof value === 'object' && value !== null && !Array.isArray(value)\n\n/**\n * Structural equality over JSON values. `undefined` never appears in a\n * document that round-tripped through jsonb, but a handler's return value has\n * not round-tripped yet, so it is compared as-is.\n */\nexport const jsonEqual = (a: unknown, b: unknown): boolean => {\n if (a === b) return true\n if (Array.isArray(a) && Array.isArray(b)) {\n return (\n a.length === b.length &&\n a.every((item, index) => jsonEqual(item, b[index]))\n )\n }\n if (isPlainObject(a) && isPlainObject(b)) {\n const keys = Object.keys(a)\n if (keys.length !== Object.keys(b).length) return false\n return keys.every(\n (key) => Object.hasOwn(b, key) && jsonEqual(a[key], b[key]),\n )\n }\n return false\n}\n\nconst diffInto = (\n ops: PatchOp[],\n path: string,\n previous: unknown,\n next: unknown,\n): void => {\n if (previous === next) return\n\n if (Array.isArray(previous) && Array.isArray(next)) {\n const shared = Math.min(previous.length, next.length)\n for (let index = 0; index < shared; index += 1) {\n diffInto(ops, `${path}/${index}`, previous[index], next[index])\n }\n // Appends first, then trailing removals from the end backwards, so every\n // index a `remove` names is still valid when that op is applied.\n for (let index = previous.length; index < next.length; index += 1) {\n ops.push({ op: 'add', path: `${path}/-`, value: next[index] })\n }\n for (let index = previous.length - 1; index >= next.length; index -= 1) {\n ops.push({ op: 'remove', path: `${path}/${index}` })\n }\n return\n }\n\n if (isPlainObject(previous) && isPlainObject(next)) {\n for (const key of Object.keys(previous)) {\n const child = `${path}/${escapeToken(key)}`\n if (!Object.hasOwn(next, key)) ops.push({ op: 'remove', path: child })\n else diffInto(ops, child, previous[key], next[key])\n }\n for (const key of Object.keys(next)) {\n if (!Object.hasOwn(previous, key)) {\n ops.push({\n op: 'add',\n path: `${path}/${escapeToken(key)}`,\n value: next[key],\n })\n }\n }\n return\n }\n\n // Differing types, or two differing scalars: the whole node is replaced.\n // An equal pair never lands here — identical references return at the top,\n // equal scalars are identical, and matching containers recurse above\n // (a deeply-equal subtree just emits no ops) — so no deep comparison is\n // needed on the way down.\n ops.push({ op: 'replace', path, value: next })\n}\n\n/**\n * The delta from one Case State document to the next. An Execution that\n * changed nothing yields an empty delta — a real and unremarkable outcome\n * (a handler whose only effect was external, or a no-op retry landing).\n *\n * Arrays are diffed positionally: element *i* against element *i*, then\n * appends and trailing removals. Case State collections are keyed by the\n * app's own identifiers (scope keys), so a positional diff of a reordered\n * collection is verbose but never wrong.\n */\nexport const diffState = (previous: unknown, next: unknown): StateDelta => {\n const ops: PatchOp[] = []\n diffInto(ops, '', previous, next)\n return ops\n}\n"]}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 } from '../errors.js';
|
|
11
|
+
import type { ConditionResult, GuardEvaluation } 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 declare const stepLabel: (stepName: string, scopeKey: string | null) => string;
|
|
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 declare class StepNotAvailableError extends AffordanceError {
|
|
31
|
+
readonly caseId: string;
|
|
32
|
+
readonly stepName: string;
|
|
33
|
+
readonly scopeKey: string | null;
|
|
34
|
+
/** False when a `requires` condition is unmet: not possible on this case, for anyone. */
|
|
35
|
+
readonly possible: boolean;
|
|
36
|
+
/** False when a `permits` condition is unmet: possible, but not for this actor. */
|
|
37
|
+
readonly permitted: boolean;
|
|
38
|
+
/** The failed condition results, verbatim from the claim-time evaluation. */
|
|
39
|
+
readonly unmet: readonly ConditionResult[];
|
|
40
|
+
/** The full evaluation record, for journaling or `explain`-style rendering. */
|
|
41
|
+
readonly evaluation: GuardEvaluation;
|
|
42
|
+
constructor(caseId: string, stepName: string, scopeKey: string | null, evaluation: GuardEvaluation);
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Another Execution is already in flight on this case and its claim has not
|
|
46
|
+
* expired. Executions are serialized per case in v1 — cases advance at human
|
|
47
|
+
* pace, so one Execution at a time costs no real throughput — which makes
|
|
48
|
+
* this "not now", not "never": retry after
|
|
49
|
+
* {@link CaseBusyError.expiresAt} at the latest.
|
|
50
|
+
*/
|
|
51
|
+
export declare class CaseBusyError extends AffordanceError {
|
|
52
|
+
readonly caseId: string;
|
|
53
|
+
/** The in-flight Execution holding the case. */
|
|
54
|
+
readonly executionId: string;
|
|
55
|
+
readonly stepName: string;
|
|
56
|
+
readonly scopeKey: string | null;
|
|
57
|
+
/** When the in-flight claim lapses if its handler stops heartbeating (ISO-8601 UTC). */
|
|
58
|
+
readonly expiresAt: string;
|
|
59
|
+
constructor(caseId: string, holder: {
|
|
60
|
+
executionId: string;
|
|
61
|
+
stepName: string;
|
|
62
|
+
scopeKey: string | null;
|
|
63
|
+
expiresAt: string;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* The claim was gone (or belonged to someone else) when this Execution tried
|
|
68
|
+
* to commit: its lease expired mid-handler and another claimant took the case
|
|
69
|
+
* over. The handler's effects on the outside world already happened — they are
|
|
70
|
+
* at-least-once by contract and deduplicated on `executionId` — but
|
|
71
|
+
* its Case State write is refused, because the state it computed from is stale.
|
|
72
|
+
*/
|
|
73
|
+
export declare class ClaimLostError extends AffordanceError {
|
|
74
|
+
readonly caseId: string;
|
|
75
|
+
readonly executionId: string;
|
|
76
|
+
/** The Execution now holding the case, if any. */
|
|
77
|
+
readonly heldBy: string | null;
|
|
78
|
+
constructor(caseId: string, executionId: string, heldBy: string | null);
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The Execution ran and failed: the handler threw on every allowed attempt,
|
|
82
|
+
* or it returned a Case State the case type's schema rejects (a deterministic
|
|
83
|
+
* defect, failed without retry). A `failed` journal entry records it and the
|
|
84
|
+
* case is released.
|
|
85
|
+
*/
|
|
86
|
+
export declare class StepExecutionError extends AffordanceError {
|
|
87
|
+
readonly caseId: string;
|
|
88
|
+
readonly executionId: string;
|
|
89
|
+
readonly stepName: string;
|
|
90
|
+
readonly scopeKey: string | null;
|
|
91
|
+
/** How many attempts ran before the Execution was given up on. */
|
|
92
|
+
readonly attempts: number;
|
|
93
|
+
constructor(caseId: string, executionId: string, stepName: string, scopeKey: string | null, attempts: number, cause: unknown);
|
|
94
|
+
}
|