@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
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @affordance/core — the case framework's engine.
|
|
3
|
+
*
|
|
4
|
+
* A **case** is state plus independently-defined guarded **steps**; the
|
|
5
|
+
* engine computes the currently-available steps (**affordances**) from
|
|
6
|
+
* guards over state. No declared flow, no program counter;
|
|
7
|
+
* process changes deploy freely against in-flight cases.
|
|
8
|
+
*
|
|
9
|
+
* This barrel *is* the package's interface, and it is curated: the authoring
|
|
10
|
+
* API an app writes definitions with, the engine it runs them on, the error
|
|
11
|
+
* taxonomy an adapter translates, and the record types that cross the wire.
|
|
12
|
+
* The store loaders, the execution lifecycle's internals
|
|
13
|
+
* and the guard walk live behind the engine — submodules import them from
|
|
14
|
+
* each other's `index.js`, apps should not need to.
|
|
15
|
+
*/
|
|
16
|
+
export type { Affordance, AffordanceExplanation, BlockedStep, CaseAffordances, CaseSnapshot, Engine, EngineOptions, ExplainOptions, } from './engine/index.js';
|
|
17
|
+
export { createEngine, UnknownCaseTypeError } from './engine/index.js';
|
|
18
|
+
export type { AffordanceErrorCode } from './errors.js';
|
|
19
|
+
export { AffordanceError, isAffordanceError } from './errors.js';
|
|
20
|
+
export type { ClaimedEntryInput, ClaimedJournalEntry, CompletedEntryInput, ExecuteOptions, ExecutionRecord, ExecutionResult, ExecutionStatus, FailureEntryInput, GuardReplay, JournalEntry, JournalEntryInput, JournalEntryType, JournalError, JournalFilter, PatchOp, StateDelta, } from './execution/index.js';
|
|
21
|
+
export { CaseBusyError, ClaimLostError, diffState, foldExecutions, isClaimedEntry, jsonEqual, replayGuard, StepExecutionError, StepNotAvailableError, stepLabel, } from './execution/index.js';
|
|
22
|
+
export type { AnyOfConditionResult, Condition, ConditionContext, ConditionMap, ConditionMapEntry, ConditionOutcome, ConditionResult, ConditionVerdict, Guard, GuardEvaluation, GuardEvaluationContext, GuardSection, Instant, SingleConditionResult, } from './guards/index.js';
|
|
23
|
+
export { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js';
|
|
24
|
+
export type { Correlation, CorrelationRegistration, DeadLetter, DeadLetterFilter, DeadLetterReason, ExternalActor, ExternalEvent, IngestionOptions, IngestionResult, IngestionStatus, } from './ingestion/index.js';
|
|
25
|
+
export { externalActor, routedStep } from './ingestion/index.js';
|
|
26
|
+
export type { MigrationFailure, MigrationOptions, MigrationProgress, MigrationReport, MigrationTransform, } from './migration/index.js';
|
|
27
|
+
export { hasMigrated, migrationStepName } from './migration/index.js';
|
|
28
|
+
export type { ActorMarker, AnyCaseType, BoundStep, CaseTypeDefinition, CaseTypeOptions, CommitWrite, CorrelationRequest, HandlerContext, RetryOptions, RetryPolicy, ScopeDeclaration, ScopedConditionContext, ScopedHandlerContext, ScopedStepHandler, ScopedStepOptions, StepDefinition, StepHandler, StepMetadata, StepOptions, } from './model/index.js';
|
|
29
|
+
export { actor, caseType, DEFAULT_RETRY, SCOPE_FAILURE_CONDITION, ScopeKeyError, StepInputValidationError, stepsOf, UnknownStepError, } from './model/index.js';
|
|
30
|
+
export type { CaseHandle, DatabaseAccess, Dormancy, PoolLike, Queryable, Transaction, } from './store/index.js';
|
|
31
|
+
export { bootstrap, CASE_TABLES, CaseNotFoundError, CaseStateValidationError, FRAMEWORK_SCHEMA, queryableOf, } from './store/index.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// Copyright © 2026 Mochicode LLC — mochicode.com
|
|
2
|
+
// ── The engine ─────────────────────────────────────────────────────────────
|
|
3
|
+
export { createEngine, UnknownCaseTypeError } from './engine/index.js';
|
|
4
|
+
// ── The error taxonomy: every deliberate refusal, one closed code set ──────
|
|
5
|
+
export { AffordanceError, isAffordanceError } from './errors.js';
|
|
6
|
+
// ── Execution records: what came back, what is journaled ───────────────────
|
|
7
|
+
export { CaseBusyError, ClaimLostError, diffState, foldExecutions, isClaimedEntry, jsonEqual, replayGuard, StepExecutionError, StepNotAvailableError, stepLabel, } from './execution/index.js';
|
|
8
|
+
// ── The authoring API: conditions and guards ───────────────────────────────
|
|
9
|
+
export { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js';
|
|
10
|
+
// ── Ingestion: events in, correlations, dead letters ───────────────────────
|
|
11
|
+
export { externalActor, routedStep } from './ingestion/index.js';
|
|
12
|
+
// ── Migration: the journaled restructure ───────────────────────────────────
|
|
13
|
+
export { hasMigrated, migrationStepName } from './migration/index.js';
|
|
14
|
+
// ── The authoring API: case types and steps ────────────────────────────────
|
|
15
|
+
// `step` itself stays behind the barrel: `caseType` requires a state schema,
|
|
16
|
+
// so `stepsOf` covers every authoring case — one public way to write a step.
|
|
17
|
+
export { actor, caseType, DEFAULT_RETRY, SCOPE_FAILURE_CONDITION, ScopeKeyError, StepInputValidationError, stepsOf, UnknownStepError, } from './model/index.js';
|
|
18
|
+
// ── Persistence: what an app touches directly ──────────────────────────────
|
|
19
|
+
export { bootstrap, CASE_TABLES, CaseNotFoundError, CaseStateValidationError, FRAMEWORK_SCHEMA, queryableOf, } from './store/index.js';
|
|
20
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,iDAAiD;AA4BjD,8EAA8E;AAC9E,OAAO,EAAE,YAAY,EAAE,oBAAoB,EAAE,MAAM,mBAAmB,CAAA;AAEtE,8EAA8E;AAC9E,OAAO,EAAE,eAAe,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAA;AAmBhE,8EAA8E;AAC9E,OAAO,EACL,aAAa,EACb,cAAc,EACd,SAAS,EACT,cAAc,EACd,cAAc,EACd,SAAS,EACT,WAAW,EACX,kBAAkB,EAClB,qBAAqB,EACrB,SAAS,GACV,MAAM,sBAAsB,CAAA;AAiB7B,8EAA8E;AAC9E,OAAO,EAAE,KAAK,EAAE,aAAa,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,mBAAmB,CAAA;AAa1E,8EAA8E;AAC9E,OAAO,EAAE,aAAa,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAA;AAQhE,8EAA8E;AAC9E,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAA;AAsBrE,8EAA8E;AAC9E,6EAA6E;AAC7E,6EAA6E;AAC7E,OAAO,EACL,KAAK,EACL,QAAQ,EACR,aAAa,EACb,uBAAuB,EACvB,aAAa,EACb,wBAAwB,EACxB,OAAO,EACP,gBAAgB,GACjB,MAAM,kBAAkB,CAAA;AASzB,8EAA8E;AAC9E,OAAO,EACL,SAAS,EACT,WAAW,EACX,iBAAiB,EACjB,wBAAwB,EACxB,gBAAgB,EAChB,WAAW,GACZ,MAAM,kBAAkB,CAAA","sourcesContent":["// Copyright © 2026 Mochicode LLC — mochicode.com\n\n/**\n * @affordance/core — the case framework's engine.\n *\n * A **case** is state plus independently-defined guarded **steps**; the\n * engine computes the currently-available steps (**affordances**) from\n * guards over state. No declared flow, no program counter;\n * process changes deploy freely against in-flight cases.\n *\n * This barrel *is* the package's interface, and it is curated: the authoring\n * API an app writes definitions with, the engine it runs them on, the error\n * taxonomy an adapter translates, and the record types that cross the wire.\n * The store loaders, the execution lifecycle's internals\n * and the guard walk live behind the engine — submodules import them from\n * each other's `index.js`, apps should not need to.\n */\n\nexport type {\n Affordance,\n AffordanceExplanation,\n BlockedStep,\n CaseAffordances,\n CaseSnapshot,\n Engine,\n EngineOptions,\n ExplainOptions,\n} from './engine/index.js'\n// ── The engine ─────────────────────────────────────────────────────────────\nexport { createEngine, UnknownCaseTypeError } from './engine/index.js'\nexport type { AffordanceErrorCode } from './errors.js'\n// ── The error taxonomy: every deliberate refusal, one closed code set ──────\nexport { AffordanceError, isAffordanceError } from './errors.js'\nexport type {\n ClaimedEntryInput,\n ClaimedJournalEntry,\n CompletedEntryInput,\n ExecuteOptions,\n ExecutionRecord,\n ExecutionResult,\n ExecutionStatus,\n FailureEntryInput,\n GuardReplay,\n JournalEntry,\n JournalEntryInput,\n JournalEntryType,\n JournalError,\n JournalFilter,\n PatchOp,\n StateDelta,\n} from './execution/index.js'\n// ── Execution records: what came back, what is journaled ───────────────────\nexport {\n CaseBusyError,\n ClaimLostError,\n diffState,\n foldExecutions,\n isClaimedEntry,\n jsonEqual,\n replayGuard,\n StepExecutionError,\n StepNotAvailableError,\n stepLabel,\n} from './execution/index.js'\nexport type {\n AnyOfConditionResult,\n Condition,\n ConditionContext,\n ConditionMap,\n ConditionMapEntry,\n ConditionOutcome,\n ConditionResult,\n ConditionVerdict,\n Guard,\n GuardEvaluation,\n GuardEvaluationContext,\n GuardSection,\n Instant,\n SingleConditionResult,\n} from './guards/index.js'\n// ── The authoring API: conditions and guards ───────────────────────────────\nexport { anyOf, evaluateGuard, toEpochMs, toIso } from './guards/index.js'\nexport type {\n Correlation,\n CorrelationRegistration,\n DeadLetter,\n DeadLetterFilter,\n DeadLetterReason,\n ExternalActor,\n ExternalEvent,\n IngestionOptions,\n IngestionResult,\n IngestionStatus,\n} from './ingestion/index.js'\n// ── Ingestion: events in, correlations, dead letters ───────────────────────\nexport { externalActor, routedStep } from './ingestion/index.js'\nexport type {\n MigrationFailure,\n MigrationOptions,\n MigrationProgress,\n MigrationReport,\n MigrationTransform,\n} from './migration/index.js'\n// ── Migration: the journaled restructure ───────────────────────────────────\nexport { hasMigrated, migrationStepName } from './migration/index.js'\nexport type {\n ActorMarker,\n AnyCaseType,\n BoundStep,\n CaseTypeDefinition,\n CaseTypeOptions,\n CommitWrite,\n CorrelationRequest,\n HandlerContext,\n RetryOptions,\n RetryPolicy,\n ScopeDeclaration,\n ScopedConditionContext,\n ScopedHandlerContext,\n ScopedStepHandler,\n ScopedStepOptions,\n StepDefinition,\n StepHandler,\n StepMetadata,\n StepOptions,\n} from './model/index.js'\n// ── The authoring API: case types and steps ────────────────────────────────\n// `step` itself stays behind the barrel: `caseType` requires a state schema,\n// so `stepsOf` covers every authoring case — one public way to write a step.\nexport {\n actor,\n caseType,\n DEFAULT_RETRY,\n SCOPE_FAILURE_CONDITION,\n ScopeKeyError,\n StepInputValidationError,\n stepsOf,\n UnknownStepError,\n} from './model/index.js'\nexport type {\n CaseHandle,\n DatabaseAccess,\n Dormancy,\n PoolLike,\n Queryable,\n Transaction,\n} from './store/index.js'\n// ── Persistence: what an app touches directly ──────────────────────────────\nexport {\n bootstrap,\n CASE_TABLES,\n CaseNotFoundError,\n CaseStateValidationError,\n FRAMEWORK_SCHEMA,\n queryableOf,\n} from './store/index.js'\n"]}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation: the mapping from an external identifier to a case (and a
|
|
3
|
+
* scope element within it) — CONTEXT.md.
|
|
4
|
+
*
|
|
5
|
+
* An e-sign provider knows envelope `env_9f2`; it does not know case
|
|
6
|
+
* `4b0e…` or that the envelope is buyer #7's agreement. Something has
|
|
7
|
+
* to hold that mapping, and it has to be the framework, because it is the
|
|
8
|
+
* framework that has to route the resulting webhook to a step. This is that
|
|
9
|
+
* something — one of exactly two integration primitives in core (the other
|
|
10
|
+
* is {@link ingest}). There are no service-specific connectors here, and
|
|
11
|
+
* there never will be: DocuSign's payload shape is the app's business.
|
|
12
|
+
*
|
|
13
|
+
* The registration is written by the handler that *initiates* the external
|
|
14
|
+
* interaction, in the same commit that records having initiated it —
|
|
15
|
+
* `ctx.correlate(...)` rides the commit seam, so a case can never be
|
|
16
|
+
* left having sent an envelope it cannot route the answer for.
|
|
17
|
+
*/
|
|
18
|
+
import type { CorrelationRequest } from '../model/handler.js';
|
|
19
|
+
import type { Queryable } from '../store/index.js';
|
|
20
|
+
/**
|
|
21
|
+
* A handler's {@link CorrelationRequest} with the case made explicit — what
|
|
22
|
+
* the registry actually stores. `step` is the step an event on this
|
|
23
|
+
* identifier should execute when the event does not name one itself:
|
|
24
|
+
* typically the materializing step, "record what the provider said".
|
|
25
|
+
*/
|
|
26
|
+
export interface CorrelationRegistration extends CorrelationRequest {
|
|
27
|
+
/** The case the answer belongs to. */
|
|
28
|
+
readonly caseId: string;
|
|
29
|
+
}
|
|
30
|
+
/** A registered correlation as stored. */
|
|
31
|
+
export interface Correlation {
|
|
32
|
+
readonly id: string;
|
|
33
|
+
readonly system: string;
|
|
34
|
+
readonly externalId: string;
|
|
35
|
+
readonly caseId: string;
|
|
36
|
+
readonly scopeKey: string | null;
|
|
37
|
+
readonly step: string | null;
|
|
38
|
+
readonly metadata: unknown;
|
|
39
|
+
readonly createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Register (or re-register) an external identifier against a case.
|
|
43
|
+
*
|
|
44
|
+
* Upserts on `(system, externalId)` — insert, or update the row already
|
|
45
|
+
* there: a retried handler attempt registering the same envelope again is
|
|
46
|
+
* not an error, it is the same fact. Pass any
|
|
47
|
+
* {@link Queryable} — from a handler this is the commit transaction, via
|
|
48
|
+
* `ctx.correlate` or `ctx.onCommit`.
|
|
49
|
+
*/
|
|
50
|
+
export declare const registerCorrelation: (db: Queryable, registration: CorrelationRegistration) => Promise<Correlation>;
|
|
51
|
+
/** Look up where an external identifier routes; `null` when nothing has claimed it. */
|
|
52
|
+
export declare const lookupCorrelation: (db: Queryable, system: string, externalId: string) => Promise<Correlation | null>;
|
|
53
|
+
/** Every identifier registered against a case — the "what is this case waiting on" view. */
|
|
54
|
+
export declare const correlationsFor: (db: Queryable, caseId: string, scopeKey?: string) => Promise<readonly Correlation[]>;
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Correlation: the mapping from an external identifier to a case (and a
|
|
3
|
+
* scope element within it) — CONTEXT.md.
|
|
4
|
+
*
|
|
5
|
+
* An e-sign provider knows envelope `env_9f2`; it does not know case
|
|
6
|
+
* `4b0e…` or that the envelope is buyer #7's agreement. Something has
|
|
7
|
+
* to hold that mapping, and it has to be the framework, because it is the
|
|
8
|
+
* framework that has to route the resulting webhook to a step. This is that
|
|
9
|
+
* something — one of exactly two integration primitives in core (the other
|
|
10
|
+
* is {@link ingest}). There are no service-specific connectors here, and
|
|
11
|
+
* there never will be: DocuSign's payload shape is the app's business.
|
|
12
|
+
*
|
|
13
|
+
* The registration is written by the handler that *initiates* the external
|
|
14
|
+
* interaction, in the same commit that records having initiated it —
|
|
15
|
+
* `ctx.correlate(...)` rides the commit seam, so a case can never be
|
|
16
|
+
* left having sent an envelope it cannot route the answer for.
|
|
17
|
+
*/
|
|
18
|
+
import { FRAMEWORK_SCHEMA, mintId } from '../store/index.js';
|
|
19
|
+
const CORRELATIONS = `${FRAMEWORK_SCHEMA}.correlations`;
|
|
20
|
+
const toCorrelation = (row) => ({
|
|
21
|
+
id: row.id,
|
|
22
|
+
system: row.system,
|
|
23
|
+
externalId: row.external_id,
|
|
24
|
+
caseId: row.case_id,
|
|
25
|
+
scopeKey: row.scope_key,
|
|
26
|
+
step: row.step,
|
|
27
|
+
metadata: row.metadata,
|
|
28
|
+
createdAt: row.created_at.toISOString(),
|
|
29
|
+
});
|
|
30
|
+
/**
|
|
31
|
+
* Register (or re-register) an external identifier against a case.
|
|
32
|
+
*
|
|
33
|
+
* Upserts on `(system, externalId)` — insert, or update the row already
|
|
34
|
+
* there: a retried handler attempt registering the same envelope again is
|
|
35
|
+
* not an error, it is the same fact. Pass any
|
|
36
|
+
* {@link Queryable} — from a handler this is the commit transaction, via
|
|
37
|
+
* `ctx.correlate` or `ctx.onCommit`.
|
|
38
|
+
*/
|
|
39
|
+
export const registerCorrelation = async (db, registration) => {
|
|
40
|
+
const { rows } = await db.query(`insert into ${CORRELATIONS} (id, system, external_id, case_id, scope_key, step, metadata)
|
|
41
|
+
values ($1, $2, $3, $4, $5, $6, $7::jsonb)
|
|
42
|
+
on conflict (system, external_id) do update
|
|
43
|
+
set case_id = excluded.case_id,
|
|
44
|
+
scope_key = excluded.scope_key,
|
|
45
|
+
step = excluded.step,
|
|
46
|
+
metadata = excluded.metadata
|
|
47
|
+
returning id, system, external_id, case_id, scope_key, step, metadata, created_at`, [
|
|
48
|
+
mintId('correlation'),
|
|
49
|
+
registration.system,
|
|
50
|
+
registration.externalId,
|
|
51
|
+
registration.caseId,
|
|
52
|
+
registration.scopeKey ?? null,
|
|
53
|
+
registration.step ?? null,
|
|
54
|
+
registration.metadata === undefined
|
|
55
|
+
? null
|
|
56
|
+
: JSON.stringify(registration.metadata),
|
|
57
|
+
]);
|
|
58
|
+
const row = rows[0];
|
|
59
|
+
if (!row)
|
|
60
|
+
throw new Error(`insert into ${CORRELATIONS} returned no row`);
|
|
61
|
+
return toCorrelation(row);
|
|
62
|
+
};
|
|
63
|
+
/** Look up where an external identifier routes; `null` when nothing has claimed it. */
|
|
64
|
+
export const lookupCorrelation = async (db, system, externalId) => {
|
|
65
|
+
const { rows } = await db.query(`select id, system, external_id, case_id, scope_key, step, metadata, created_at
|
|
66
|
+
from ${CORRELATIONS} where system = $1 and external_id = $2`, [system, externalId]);
|
|
67
|
+
const row = rows[0];
|
|
68
|
+
return row === undefined ? null : toCorrelation(row);
|
|
69
|
+
};
|
|
70
|
+
/** Every identifier registered against a case — the "what is this case waiting on" view. */
|
|
71
|
+
export const correlationsFor = async (db, caseId, scopeKey) => {
|
|
72
|
+
const { rows } = await db.query(`select id, system, external_id, case_id, scope_key, step, metadata, created_at
|
|
73
|
+
from ${CORRELATIONS}
|
|
74
|
+
where case_id = $1 ${scopeKey === undefined ? '' : 'and scope_key = $2'}
|
|
75
|
+
order by created_at asc`, scopeKey === undefined ? [caseId] : [caseId, scopeKey]);
|
|
76
|
+
return rows.map(toCorrelation);
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=correlation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"correlation.js","sourceRoot":"","sources":["../../src/ingestion/correlation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAIH,OAAO,EAAE,gBAAgB,EAAE,MAAM,EAAE,MAAM,mBAAmB,CAAA;AAE5D,MAAM,YAAY,GAAG,GAAG,gBAAgB,eAAe,CAAA;AAoCvD,MAAM,aAAa,GAAG,CAAC,GAAmB,EAAe,EAAE,CAAC,CAAC;IAC3D,EAAE,EAAE,GAAG,CAAC,EAAE;IACV,MAAM,EAAE,GAAG,CAAC,MAAM;IAClB,UAAU,EAAE,GAAG,CAAC,WAAW;IAC3B,MAAM,EAAE,GAAG,CAAC,OAAO;IACnB,QAAQ,EAAE,GAAG,CAAC,SAAS;IACvB,IAAI,EAAE,GAAG,CAAC,IAAI;IACd,QAAQ,EAAE,GAAG,CAAC,QAAQ;IACtB,SAAS,EAAE,GAAG,CAAC,UAAU,CAAC,WAAW,EAAE;CACxC,CAAC,CAAA;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EACtC,EAAa,EACb,YAAqC,EACf,EAAE;IACxB,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B,eAAe,YAAY;;;;;;;uFAOwD,EACnF;QACE,MAAM,CAAC,aAAa,CAAC;QACrB,YAAY,CAAC,MAAM;QACnB,YAAY,CAAC,UAAU;QACvB,YAAY,CAAC,MAAM;QACnB,YAAY,CAAC,QAAQ,IAAI,IAAI;QAC7B,YAAY,CAAC,IAAI,IAAI,IAAI;QACzB,YAAY,CAAC,QAAQ,KAAK,SAAS;YACjC,CAAC,CAAC,IAAI;YACN,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,YAAY,CAAC,QAAQ,CAAC;KAC1C,CACF,CAAA;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IACnB,IAAI,CAAC,GAAG;QAAE,MAAM,IAAI,KAAK,CAAC,eAAe,YAAY,kBAAkB,CAAC,CAAA;IACxE,OAAO,aAAa,CAAC,GAAG,CAAC,CAAA;AAC3B,CAAC,CAAA;AAED,uFAAuF;AACvF,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,EACpC,EAAa,EACb,MAAc,EACd,UAAkB,EACW,EAAE;IAC/B,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B;YACQ,YAAY,yCAAyC,EAC7D,CAAC,MAAM,EAAE,UAAU,CAAC,CACrB,CAAA;IACD,MAAM,GAAG,GAAG,IAAI,CAAC,CAAC,CAAC,CAAA;IACnB,OAAO,GAAG,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,aAAa,CAAC,GAAG,CAAC,CAAA;AACtD,CAAC,CAAA;AAED,4FAA4F;AAC5F,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAClC,EAAa,EACb,MAAc,EACd,QAAiB,EACgB,EAAE;IACnC,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,EAAE,CAAC,KAAK,CAC7B;YACQ,YAAY;0BACE,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,oBAAoB;6BAC/C,EACzB,QAAQ,KAAK,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,MAAM,EAAE,QAAQ,CAAC,CACvD,CAAA;IACD,OAAO,IAAI,CAAC,GAAG,CAAC,aAAa,CAAC,CAAA;AAChC,CAAC,CAAA","sourcesContent":["/**\n * Correlation: the mapping from an external identifier to a case (and a\n * scope element within it) — CONTEXT.md.\n *\n * An e-sign provider knows envelope `env_9f2`; it does not know case\n * `4b0e…` or that the envelope is buyer #7's agreement. Something has\n * to hold that mapping, and it has to be the framework, because it is the\n * framework that has to route the resulting webhook to a step. This is that\n * something — one of exactly two integration primitives in core (the other\n * is {@link ingest}). There are no service-specific connectors here, and\n * there never will be: DocuSign's payload shape is the app's business.\n *\n * The registration is written by the handler that *initiates* the external\n * interaction, in the same commit that records having initiated it —\n * `ctx.correlate(...)` rides the commit seam, so a case can never be\n * left having sent an envelope it cannot route the answer for.\n */\n\nimport type { CorrelationRequest } from '../model/handler.js'\nimport type { Queryable } from '../store/index.js'\nimport { FRAMEWORK_SCHEMA, mintId } from '../store/index.js'\n\nconst CORRELATIONS = `${FRAMEWORK_SCHEMA}.correlations`\n\n/**\n * A handler's {@link CorrelationRequest} with the case made explicit — what\n * the registry actually stores. `step` is the step an event on this\n * identifier should execute when the event does not name one itself:\n * typically the materializing step, \"record what the provider said\".\n */\nexport interface CorrelationRegistration extends CorrelationRequest {\n /** The case the answer belongs to. */\n readonly caseId: string\n}\n\n/** A registered correlation as stored. */\nexport interface Correlation {\n readonly id: string\n readonly system: string\n readonly externalId: string\n readonly caseId: string\n readonly scopeKey: string | null\n readonly step: string | null\n readonly metadata: unknown\n readonly createdAt: string\n}\n\ntype CorrelationRow = {\n id: string\n system: string\n external_id: string\n case_id: string\n scope_key: string | null\n step: string | null\n metadata: unknown\n created_at: Date\n}\n\nconst toCorrelation = (row: CorrelationRow): Correlation => ({\n id: row.id,\n system: row.system,\n externalId: row.external_id,\n caseId: row.case_id,\n scopeKey: row.scope_key,\n step: row.step,\n metadata: row.metadata,\n createdAt: row.created_at.toISOString(),\n})\n\n/**\n * Register (or re-register) an external identifier against a case.\n *\n * Upserts on `(system, externalId)` — insert, or update the row already\n * there: a retried handler attempt registering the same envelope again is\n * not an error, it is the same fact. Pass any\n * {@link Queryable} — from a handler this is the commit transaction, via\n * `ctx.correlate` or `ctx.onCommit`.\n */\nexport const registerCorrelation = async (\n db: Queryable,\n registration: CorrelationRegistration,\n): Promise<Correlation> => {\n const { rows } = await db.query<CorrelationRow>(\n `insert into ${CORRELATIONS} (id, system, external_id, case_id, scope_key, step, metadata)\n values ($1, $2, $3, $4, $5, $6, $7::jsonb)\n on conflict (system, external_id) do update\n set case_id = excluded.case_id,\n scope_key = excluded.scope_key,\n step = excluded.step,\n metadata = excluded.metadata\n returning id, system, external_id, case_id, scope_key, step, metadata, created_at`,\n [\n mintId('correlation'),\n registration.system,\n registration.externalId,\n registration.caseId,\n registration.scopeKey ?? null,\n registration.step ?? null,\n registration.metadata === undefined\n ? null\n : JSON.stringify(registration.metadata),\n ],\n )\n const row = rows[0]\n if (!row) throw new Error(`insert into ${CORRELATIONS} returned no row`)\n return toCorrelation(row)\n}\n\n/** Look up where an external identifier routes; `null` when nothing has claimed it. */\nexport const lookupCorrelation = async (\n db: Queryable,\n system: string,\n externalId: string,\n): Promise<Correlation | null> => {\n const { rows } = await db.query<CorrelationRow>(\n `select id, system, external_id, case_id, scope_key, step, metadata, created_at\n from ${CORRELATIONS} where system = $1 and external_id = $2`,\n [system, externalId],\n )\n const row = rows[0]\n return row === undefined ? null : toCorrelation(row)\n}\n\n/** Every identifier registered against a case — the \"what is this case waiting on\" view. */\nexport const correlationsFor = async (\n db: Queryable,\n caseId: string,\n scopeKey?: string,\n): Promise<readonly Correlation[]> => {\n const { rows } = await db.query<CorrelationRow>(\n `select id, system, external_id, case_id, scope_key, step, metadata, created_at\n from ${CORRELATIONS}\n where case_id = $1 ${scopeKey === undefined ? '' : 'and scope_key = $2'}\n order by created_at asc`,\n scopeKey === undefined ? [caseId] : [caseId, scopeKey],\n )\n return rows.map(toCorrelation)\n}\n"]}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two integration primitives that live in core.
|
|
3
|
+
*
|
|
4
|
+
* - **Correlation** — external identifier ↔ (case, scope element). Written
|
|
5
|
+
* by the handler that initiates the external interaction.
|
|
6
|
+
* - **Ingestion** — `ingest(event)`: dedup, correlate, then an ordinary
|
|
7
|
+
* Execution with the external system as the actor (materialize-on-event).
|
|
8
|
+
*
|
|
9
|
+
* Deliberately absent, permanently: connectors. Nothing in this module knows
|
|
10
|
+
* what a DocuSign webhook looks like, and nothing in it ever will — the app
|
|
11
|
+
* owns payload shapes, the framework owns routing and exactly-once.
|
|
12
|
+
*/
|
|
13
|
+
export type { Correlation, CorrelationRegistration } from './correlation.js';
|
|
14
|
+
export { correlationsFor, lookupCorrelation, registerCorrelation, } from './correlation.js';
|
|
15
|
+
export type { DeadLetter, DeadLetterFilter, DeadLetterReason, ExternalActor, ExternalEvent, IngestionEnvironment, IngestionOptions, IngestionResult, IngestionSettings, IngestionStatus, } from './ingest.js';
|
|
16
|
+
export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY, readDeadLetters, routedStep, } from './ingest.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The two integration primitives that live in core.
|
|
3
|
+
*
|
|
4
|
+
* - **Correlation** — external identifier ↔ (case, scope element). Written
|
|
5
|
+
* by the handler that initiates the external interaction.
|
|
6
|
+
* - **Ingestion** — `ingest(event)`: dedup, correlate, then an ordinary
|
|
7
|
+
* Execution with the external system as the actor (materialize-on-event).
|
|
8
|
+
*
|
|
9
|
+
* Deliberately absent, permanently: connectors. Nothing in this module knows
|
|
10
|
+
* what a DocuSign webhook looks like, and nothing in it ever will — the app
|
|
11
|
+
* owns payload shapes, the framework owns routing and exactly-once.
|
|
12
|
+
*/
|
|
13
|
+
export { correlationsFor, lookupCorrelation, registerCorrelation, } from './correlation.js';
|
|
14
|
+
export { classifyDeadLetter, externalActor, idempotencyKeyFor, ingest, normalizeIngestion, REOPENS_ON_REDELIVERY, readDeadLetters, routedStep, } from './ingest.js';
|
|
15
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/ingestion/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAGH,OAAO,EACL,eAAe,EACf,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,kBAAkB,CAAA;AAazB,OAAO,EACL,kBAAkB,EAClB,aAAa,EACb,iBAAiB,EACjB,MAAM,EACN,kBAAkB,EAClB,qBAAqB,EACrB,eAAe,EACf,UAAU,GACX,MAAM,aAAa,CAAA","sourcesContent":["/**\n * The two integration primitives that live in core.\n *\n * - **Correlation** — external identifier ↔ (case, scope element). Written\n * by the handler that initiates the external interaction.\n * - **Ingestion** — `ingest(event)`: dedup, correlate, then an ordinary\n * Execution with the external system as the actor (materialize-on-event).\n *\n * Deliberately absent, permanently: connectors. Nothing in this module knows\n * what a DocuSign webhook looks like, and nothing in it ever will — the app\n * owns payload shapes, the framework owns routing and exactly-once.\n */\n\nexport type { Correlation, CorrelationRegistration } from './correlation.js'\nexport {\n correlationsFor,\n lookupCorrelation,\n registerCorrelation,\n} from './correlation.js'\nexport type {\n DeadLetter,\n DeadLetterFilter,\n DeadLetterReason,\n ExternalActor,\n ExternalEvent,\n IngestionEnvironment,\n IngestionOptions,\n IngestionResult,\n IngestionSettings,\n IngestionStatus,\n} from './ingest.js'\nexport {\n classifyDeadLetter,\n externalActor,\n idempotencyKeyFor,\n ingest,\n normalizeIngestion,\n REOPENS_ON_REDELIVERY,\n readDeadLetters,\n routedStep,\n} from './ingest.js'\n"]}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Ingestion: external events become Executions.
|
|
3
|
+
*
|
|
4
|
+
* A webhook arrives. Three things have to happen, in this order, and each of
|
|
5
|
+
* them has to be visible afterwards:
|
|
6
|
+
*
|
|
7
|
+
* 1. **Dedup.** Providers retry; "at least once" is the delivery guarantee
|
|
8
|
+
* every one of them offers. The gate is a unique insert into
|
|
9
|
+
* `ingested_events` — not a lookup-then-insert, which races itself — so
|
|
10
|
+
* three concurrent deliveries of one event produce exactly one Execution
|
|
11
|
+
* and two `duplicate` results.
|
|
12
|
+
* 2. **Correlate.** The external identifier is resolved to (case, scope,
|
|
13
|
+
* step) through the registry the initiating handler wrote.
|
|
14
|
+
* 3. **Execute.** Through the ordinary claim → run → commit, with the
|
|
15
|
+
* external system as the journaled actor. Ingestion has no privileged
|
|
16
|
+
* path: the guard still decides, transactionally.
|
|
17
|
+
*
|
|
18
|
+
* What is *not* allowed is a quiet drop. An event nothing can route, an event
|
|
19
|
+
* whose step the guard refuses, an event whose case is busy — each lands in
|
|
20
|
+
* the same table with a status and a reason, which is the dead-letter
|
|
21
|
+
* surface. "The webhook definitely arrived, so why is the case still waiting"
|
|
22
|
+
* is a question this table answers without anyone reading a log file.
|
|
23
|
+
*/
|
|
24
|
+
import type { DeadLetterReason as ContractDeadLetterReason, IngestionStatus as ContractIngestionStatus } from '@affordance/contract';
|
|
25
|
+
import type { ExecutionEnvironment, ExecutionResult, SystemSettled } from '../execution/index.js';
|
|
26
|
+
import type { Queryable } from '../store/index.js';
|
|
27
|
+
import type { Correlation } from './correlation.js';
|
|
28
|
+
/** An event as an external system delivered it. */
|
|
29
|
+
export interface ExternalEvent {
|
|
30
|
+
/** The system that emitted it, matching the correlation's `system`. */
|
|
31
|
+
readonly system: string;
|
|
32
|
+
/** The identifier that routes it — the one the initiating handler registered. */
|
|
33
|
+
readonly externalId: string;
|
|
34
|
+
/** What happened, in the provider's own words: `'envelope.completed'`. */
|
|
35
|
+
readonly type: string;
|
|
36
|
+
/** The provider's delivery id, when it has one — the strongest dedup key. */
|
|
37
|
+
readonly eventId?: string;
|
|
38
|
+
/** The payload, passed to the target step as its input (validated by the step's schema). */
|
|
39
|
+
readonly payload?: unknown;
|
|
40
|
+
/** Execute this step instead of the correlation's registered one. */
|
|
41
|
+
readonly step?: string;
|
|
42
|
+
/** Override the derived idempotency key — for a provider whose retries are not identical. */
|
|
43
|
+
readonly idempotencyKey?: string;
|
|
44
|
+
/** When the external system says it happened (ISO-8601); recorded, never used as `asOf`. */
|
|
45
|
+
readonly occurredAt?: string;
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* How an ingested event ended up: `executed` (the target step ran and
|
|
49
|
+
* committed), `duplicate` (already seen — this delivery changed nothing, by
|
|
50
|
+
* design), or `dead-lettered` (nothing could be done with it, and it is
|
|
51
|
+
* sitting in the dead-letter surface). Wire vocabulary, so the contract
|
|
52
|
+
* declares it and this is the same closed set.
|
|
53
|
+
*/
|
|
54
|
+
export type IngestionStatus = ContractIngestionStatus;
|
|
55
|
+
/**
|
|
56
|
+
* Why an event was dead-lettered — the operator's first question, answered.
|
|
57
|
+
*
|
|
58
|
+
* Two reasons are ingestion's own (`unrouted`: no correlation claims the
|
|
59
|
+
* identifier; `no-step`: routed, but neither the event nor the correlation
|
|
60
|
+
* names a step); every other reason **is** the `AffordanceErrorCode`
|
|
61
|
+
* the refused Execution already declared at its raise site. Ingestion does
|
|
62
|
+
* not re-derive the kind of a Refusal — it projects the code the error
|
|
63
|
+
* carries, so a new refusal class can never misroute here. Wire vocabulary,
|
|
64
|
+
* declared once by the contract; this is the same closed set.
|
|
65
|
+
*/
|
|
66
|
+
export type DeadLetterReason = ContractDeadLetterReason;
|
|
67
|
+
/**
|
|
68
|
+
* Whether a provider redelivery of the same event deserves another attempt.
|
|
69
|
+
*
|
|
70
|
+
* Total over {@link DeadLetterReason}, so a new code cannot be added without
|
|
71
|
+
* deciding its reopen policy. `true` marks the outcomes that another delivery
|
|
72
|
+
* could genuinely cure — the case was busy, the handler crashed. Everything
|
|
73
|
+
* deterministic (a payload its schema rejects, an address that does not
|
|
74
|
+
* resolve, a guard that said no) stays dead-lettered: it will be refused the
|
|
75
|
+
* same way on every retry, so reopening it would only invite endless
|
|
76
|
+
* redelivery.
|
|
77
|
+
*/
|
|
78
|
+
export declare const REOPENS_ON_REDELIVERY: Record<DeadLetterReason, boolean>;
|
|
79
|
+
/** What {@link ingest} resolves to — always a record, never a throw. */
|
|
80
|
+
export interface IngestionResult {
|
|
81
|
+
readonly id: string;
|
|
82
|
+
readonly status: IngestionStatus;
|
|
83
|
+
readonly system: string;
|
|
84
|
+
readonly externalId: string;
|
|
85
|
+
readonly idempotencyKey: string;
|
|
86
|
+
/** Where it routed, when it routed. */
|
|
87
|
+
readonly correlation: Correlation | null;
|
|
88
|
+
/** The Execution it produced, on `executed`. */
|
|
89
|
+
readonly execution: ExecutionResult | null;
|
|
90
|
+
/** Why it is in the dead-letter surface, on `dead-lettered`. */
|
|
91
|
+
readonly reason: DeadLetterReason | null;
|
|
92
|
+
/** Human-readable detail: the unmet conditions, the failure message. */
|
|
93
|
+
readonly detail: string | null;
|
|
94
|
+
readonly receivedAt: string;
|
|
95
|
+
}
|
|
96
|
+
/** A row of the dead-letter surface. */
|
|
97
|
+
export interface DeadLetter {
|
|
98
|
+
readonly id: string;
|
|
99
|
+
readonly system: string;
|
|
100
|
+
readonly externalId: string;
|
|
101
|
+
readonly type: string;
|
|
102
|
+
readonly idempotencyKey: string;
|
|
103
|
+
readonly caseId: string | null;
|
|
104
|
+
readonly scopeKey: string | null;
|
|
105
|
+
readonly step: string | null;
|
|
106
|
+
readonly reason: DeadLetterReason;
|
|
107
|
+
readonly detail: string | null;
|
|
108
|
+
readonly event: ExternalEvent;
|
|
109
|
+
readonly receivedAt: string;
|
|
110
|
+
}
|
|
111
|
+
/** Filters for {@link readDeadLetters}; all optional, all AND-ed. */
|
|
112
|
+
export interface DeadLetterFilter {
|
|
113
|
+
readonly system?: string;
|
|
114
|
+
readonly caseId?: string;
|
|
115
|
+
readonly reason?: DeadLetterReason;
|
|
116
|
+
readonly limit?: number;
|
|
117
|
+
}
|
|
118
|
+
/** How ingestion presents the external system to guards and the journal. */
|
|
119
|
+
export interface ExternalActor {
|
|
120
|
+
readonly kind: 'external';
|
|
121
|
+
readonly system: string;
|
|
122
|
+
readonly externalId: string;
|
|
123
|
+
readonly eventType: string;
|
|
124
|
+
}
|
|
125
|
+
/** Engine-level ingestion settings. */
|
|
126
|
+
export interface IngestionOptions {
|
|
127
|
+
/**
|
|
128
|
+
* Map an event to the Actor its Execution runs as. Defaults to an
|
|
129
|
+
* {@link ExternalActor}; an app whose `permits` conditions read its own
|
|
130
|
+
* actor shape supplies a mapping instead of bending that shape.
|
|
131
|
+
*/
|
|
132
|
+
readonly actor?: (event: ExternalEvent) => unknown;
|
|
133
|
+
}
|
|
134
|
+
/** The normalized settings the environment carries. */
|
|
135
|
+
export interface IngestionSettings {
|
|
136
|
+
readonly actor: (event: ExternalEvent) => unknown;
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* What {@link ingest} needs from its caller: the execution environment plus
|
|
140
|
+
* ingestion's own settings. This is the widest environment any subsystem
|
|
141
|
+
* asks for, so it is also the shape the engine builds once and hands to all
|
|
142
|
+
* of them — named here so that contract is a type, not a coincidence of an
|
|
143
|
+
* object literal.
|
|
144
|
+
*/
|
|
145
|
+
export interface IngestionEnvironment extends ExecutionEnvironment {
|
|
146
|
+
readonly ingestion: IngestionSettings;
|
|
147
|
+
}
|
|
148
|
+
export declare const externalActor: (event: ExternalEvent) => ExternalActor;
|
|
149
|
+
export declare const normalizeIngestion: (options?: IngestionOptions) => IngestionSettings;
|
|
150
|
+
/**
|
|
151
|
+
* The idempotency key an event dedups on.
|
|
152
|
+
*
|
|
153
|
+
* `(system, externalId, type)` is the key's fixed prefix, so the *same*
|
|
154
|
+
* notification about the *same* envelope produces the same key however many
|
|
155
|
+
* times it is delivered — and, because an external id resolves to one (case,
|
|
156
|
+
* scope element), the key is scoped exactly as far as the correlation is. The
|
|
157
|
+
* tail is the provider's own delivery id where there is one, and a hash of
|
|
158
|
+
* the payload where there is not: a provider that cannot tell you which
|
|
159
|
+
* delivery this is gets content-addressed dedup, which is the best anyone
|
|
160
|
+
* can do.
|
|
161
|
+
*/
|
|
162
|
+
export declare const idempotencyKeyFor: (event: ExternalEvent) => string;
|
|
163
|
+
/**
|
|
164
|
+
* Which step an ingested event executes: the event's own naming wins,
|
|
165
|
+
* falling back to what the correlation registered. The precedence is stated
|
|
166
|
+
* once so any surface that *previews* routing (a dev console's world panel)
|
|
167
|
+
* asks the same question `ingest` will answer.
|
|
168
|
+
*/
|
|
169
|
+
export declare const routedStep: (event: {
|
|
170
|
+
readonly step?: string;
|
|
171
|
+
}, correlation: {
|
|
172
|
+
readonly step: string | null;
|
|
173
|
+
}) => string | null;
|
|
174
|
+
/**
|
|
175
|
+
* Ingest one external event: dedup, correlate, execute — or dead-letter it
|
|
176
|
+
* with a reason.
|
|
177
|
+
*
|
|
178
|
+
* Never throws for an event's own sake: a webhook endpoint that 500s because
|
|
179
|
+
* a guard said no teaches the provider to retry something that will never
|
|
180
|
+
* succeed. Infrastructure failures (the database is gone) do still throw,
|
|
181
|
+
* because those the caller must not acknowledge.
|
|
182
|
+
*/
|
|
183
|
+
export declare const ingest: (env: IngestionEnvironment, event: ExternalEvent) => Promise<IngestionResult>;
|
|
184
|
+
/**
|
|
185
|
+
* Project a settled system run onto the reason an operator needs to see —
|
|
186
|
+
* the one place that asks whether what settled was a Refusal.
|
|
187
|
+
*
|
|
188
|
+
* A framework Refusal already names its own kind and explains itself — the
|
|
189
|
+
* reason is its code, the detail its message (a refused guard's message
|
|
190
|
+
* carries the unmet conditions, addressed). Anything else is a bug, not a
|
|
191
|
+
* Refusal; it lands as `execution-failed` so the event is kept, never
|
|
192
|
+
* silently dropped, with the crash's own words as the detail.
|
|
193
|
+
*/
|
|
194
|
+
export declare const classifyDeadLetter: (ran: SystemSettled) => [DeadLetterReason, string];
|
|
195
|
+
/** Read the dead-letter surface, newest first — the ops view of "arrived, did nothing". */
|
|
196
|
+
export declare const readDeadLetters: (db: Queryable, filter?: DeadLetterFilter) => Promise<readonly DeadLetter[]>;
|