@ai-digital-factory/state-flow-runtime 0.0.0-stage → 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 +18 -0
- package/README.md +146 -2
- package/dist/index.d.ts +739 -0
- package/dist/index.js +2877 -0
- package/package.json +49 -3
package/LICENSE
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
Copyright (c) 2026 coupleaddagaming@gmail.com. All rights reserved.
|
|
2
|
+
|
|
3
|
+
State Flow is proprietary software. No license is granted by downloading,
|
|
4
|
+
accessing, or receiving this software or its documentation. All third-party
|
|
5
|
+
use, including personal, noncommercial and commercial use, requires prior
|
|
6
|
+
written permission from the copyright holder. Copying, modifying, distributing,
|
|
7
|
+
sublicensing or incorporating this software into another product also requires
|
|
8
|
+
prior written permission. Request permission at coupleaddagaming@gmail.com.
|
|
9
|
+
|
|
10
|
+
Any permission granted separately is governed by its written terms. This notice
|
|
11
|
+
does not alter the licenses of third-party dependencies or bundled third-party
|
|
12
|
+
materials; their respective licenses continue to apply.
|
|
13
|
+
|
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
15
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO MERCHANTABILITY, FITNESS FOR A PARTICULAR
|
|
16
|
+
PURPOSE AND NONINFRINGEMENT. TO THE MAXIMUM EXTENT PERMITTED BY APPLICABLE LAW,
|
|
17
|
+
THE COPYRIGHT HOLDER IS NOT LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY
|
|
18
|
+
ARISING FROM THE SOFTWARE OR ITS USE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,147 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @ai-digital-factory/state-flow-runtime
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Application-owned durable execution with SQLite storage. Requires Node.js 22.13 or later.
|
|
4
|
+
|
|
5
|
+
Import from `@ai-digital-factory/state-flow-runtime`. The `/durable` subpath resolves to the same module and types. There is one execution model: callback checkpoints, request deduplication, bounded recovery and manual resolution.
|
|
6
|
+
|
|
7
|
+
Every handlers package compiles for this runtime. No execution opt-in is needed. Each callback declares how it recovers, and `state-flow compile` type-checks it against its generated contract:
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import { z } from "zod";
|
|
11
|
+
|
|
12
|
+
export const payloadSchema = z.strictObject({ amount: z.number().positive() });
|
|
13
|
+
export type Payload = z.output<typeof payloadSchema>;
|
|
14
|
+
|
|
15
|
+
export const onTransition = {
|
|
16
|
+
run: async (context, payload) => {
|
|
17
|
+
await context.config.payments.charge({
|
|
18
|
+
idempotencyKey: context.operation.operationId,
|
|
19
|
+
amount: payload.amount,
|
|
20
|
+
});
|
|
21
|
+
},
|
|
22
|
+
recovery: {
|
|
23
|
+
strategy: "reconcile",
|
|
24
|
+
reconcile: async (context) =>
|
|
25
|
+
(await context.config.payments.find(context.operation.operationId))
|
|
26
|
+
? { decision: "completed" }
|
|
27
|
+
: { decision: "safe-to-retry" },
|
|
28
|
+
},
|
|
29
|
+
} satisfies DurableTransitionCallback<TransitionContext<Config>, Payload>;
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
Strategies: `safe-replay` (running again cannot duplicate an effect), `reconcile` (check the external system and return `completed`, `safe-to-retry`, `pending` or `needs-attention`), and `manual` (stop for an operator on failure).
|
|
33
|
+
|
|
34
|
+
Services configure one shared policy for non-applied dispatch outcomes:
|
|
35
|
+
|
|
36
|
+
```ts
|
|
37
|
+
import visitorAccess from "@scope/handlers/visitor-access";
|
|
38
|
+
import {
|
|
39
|
+
createRuntime,
|
|
40
|
+
instanceId,
|
|
41
|
+
requestId,
|
|
42
|
+
} from "@ai-digital-factory/state-flow-runtime";
|
|
43
|
+
|
|
44
|
+
await using runtime = createRuntime({
|
|
45
|
+
databasePath: "state-flow.db",
|
|
46
|
+
shutdownTimeoutMs: 10_000,
|
|
47
|
+
});
|
|
48
|
+
const flow = await runtime
|
|
49
|
+
.createFlow(visitorAccess, config)
|
|
50
|
+
.handleFailure((failure) => {
|
|
51
|
+
// One application policy for flow setup, instance acquisition and dispatch.
|
|
52
|
+
throw failure;
|
|
53
|
+
})
|
|
54
|
+
.run();
|
|
55
|
+
|
|
56
|
+
const instance = await flow
|
|
57
|
+
.ensureInstance({ instanceId: instanceId("visitor-1") })
|
|
58
|
+
.run();
|
|
59
|
+
|
|
60
|
+
// No callback map is needed for each command. Fulfillment means applied.
|
|
61
|
+
const applied = await instance.when("guest").dispatch({
|
|
62
|
+
requestId: requestId("unlock-1"),
|
|
63
|
+
transition: "unlock-succeeds",
|
|
64
|
+
payload: { operatorSessionCookie: "…" },
|
|
65
|
+
});
|
|
66
|
+
console.log(applied.duplicate ? "Already applied" : "Applied");
|
|
67
|
+
|
|
68
|
+
// Runtime cleanup is awaited automatically when this scope exits.
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
- **One owner per database.** A second runtime on the same file throws `DurableOwnershipError`; a crashed process releases the lock.
|
|
72
|
+
- **Recovery** runs in the background with at most 4 attempts per callback (waits of 1 s, 5 s, 30 s), then marks the instance needs-attention. Budgets survive restarts. Opening an instance resumes interrupted work.
|
|
73
|
+
- **Observation:** `getSnapshot()`, `subscribe()` and paged `history()` per instance; `runtime.inspector` lists instances; authorized `runtime.controls` resolves work across them for operator tools such as the durable companion in `@ai-digital-factory/state-flow-observe`.
|
|
74
|
+
- **Manual recovery:** `instance.resolve({ action: "retry" | "confirm-completed" | "terminate", operationId, expectedAttempts, reason, … })`, taking `operationId` and `attempts` from the snapshot's unresolved work. An action based on an older attempt is rejected as `stale`. Your application authorizes the caller; `parseResolveInput` validates input from untrusted sources.
|
|
75
|
+
- **Handler changes:** unresolved work started under different handler code (handlers, local helpers, `config.ts`, declared dependencies) is not resumed (`definition-mismatch` with `reason: "handler-revision"`). Finish it with the previous build, or terminate it with `runtime.controls.resolve(id, { action: "terminate", … })`, which runs no handler.
|
|
76
|
+
- **Stalls:** if an infrastructure error stops execution, `snapshot.stall` shows it; fix the cause and call `instance.restartRecovery()`.
|
|
77
|
+
- **Shutdown:** `close({ timeoutMs })` rejects with `DurableShutdownTimeoutError` if callbacks ignore their `AbortSignal`; the database stays owned until they settle or the process exits.
|
|
78
|
+
|
|
79
|
+
See ADR 0075 for the exact rules and what is not covered yet.
|
|
80
|
+
|
|
81
|
+
## Schema contracts
|
|
82
|
+
|
|
83
|
+
Every durable transition module exports a Zod `payloadSchema`; use `z.null()` for an explicitly empty payload. Compile derives the service payload type from the schema and rejects missing declarations or mismatched handlers. The compiled module carries the schema so State Flow validates dynamic input before running a handler or reserving a request ID. Consumers must handle `invalid-payload` alongside every other dispatch outcome. Both invalid-payload and request-conflict contexts expose the rejected payload as unknown.
|
|
84
|
+
|
|
85
|
+
Use validation-only schemas. Async refinements are supported; State Flow rechecks admission and deduplication after validation yields. Schema output must preserve the JSON input; coercion, defaults, transformations and silently stripped fields are rejected when they change that input. A duplicate request is matched before validating it against a changed payload schema, preserving recorded outcomes across compatible handler deployments. Consequently, `applied` and `blocked` expose `input.payload` as unknown when `duplicate` is true; historic data must not be assumed to match the current schema. Schema refinements should be side-effect free; external I/O belongs in checkpointed handler callbacks. Domain-specific refinements belong in the handlers package; State Flow performs validation and maps its errors. Zod does not replace graph membership, freshness, authorization, or safe-replay decisions.
|
|
86
|
+
|
|
87
|
+
## Scoped runtime lifetime
|
|
88
|
+
|
|
89
|
+
```ts
|
|
90
|
+
import { withDurableRuntime } from "@ai-digital-factory/state-flow-runtime";
|
|
91
|
+
|
|
92
|
+
await withDurableRuntime(
|
|
93
|
+
{ databasePath: "state-flow.db", shutdownTimeoutMs: 5000 },
|
|
94
|
+
async (runtime) => {
|
|
95
|
+
const flow = await runtime
|
|
96
|
+
.createFlow(compiled, config)
|
|
97
|
+
.handleFailure((failure) => {
|
|
98
|
+
throw failure;
|
|
99
|
+
})
|
|
100
|
+
.run();
|
|
101
|
+
// Ensure instances and dispatch within this managed scope.
|
|
102
|
+
},
|
|
103
|
+
);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The helper awaits application work and closes the runtime in `finally`, including when startup or response mapping fails. If work and shutdown both fail, an `AggregateError` preserves both failures. Applications decide their lifetime and signal handling; the library does not install global process handlers. Shutdown timeout retains ownership until active work settles.
|
|
107
|
+
|
|
108
|
+
## Typed recovery and operator capabilities
|
|
109
|
+
|
|
110
|
+
```ts
|
|
111
|
+
const snapshot = instance.getSnapshot();
|
|
112
|
+
if (snapshot.status.kind === "needs-attention") {
|
|
113
|
+
const target = recoveryTarget(snapshot.status.work);
|
|
114
|
+
const resolver = instance.handleResolve({
|
|
115
|
+
accepted: ({ snapshot }) => snapshot,
|
|
116
|
+
rejected: ({ reason }) => {
|
|
117
|
+
throw new Error(reason);
|
|
118
|
+
},
|
|
119
|
+
});
|
|
120
|
+
await resolver.resolve(target.retry("external dependency restored"));
|
|
121
|
+
}
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
Import `recoveryTarget` from the durable entrypoint. It captures the operation ID and attempt count and exposes callback-specific confirmation input: boolean for a guard, an authored state for start-state resolution, and only a reason for void callbacks. Captured targets remain stale after later attempts; they never silently retarget refreshed work. `handleRestart({ restarted, "not-restarted" }).restart()` requires both explicit restart outcomes.
|
|
125
|
+
|
|
126
|
+
`runtime.inspector` exposes only listing, snapshot/history reads and subscriptions. `runtime.controls` is the explicit privileged capability used by authorized operator tooling, including companion recovery. Raw instance `resolve`/`restartRecovery` remain deprecated operator primitives; the published service lint configuration requires application instances to use the exhaustive interfaces. Application authorization remains mandatory before forwarding untrusted actions.
|
|
127
|
+
|
|
128
|
+
Transient SQLite busy/locked infrastructure errors receive at most three automatic restart attempts (1 s, 5 s, 30 s). Other infrastructure failures remain visible stalls requiring explicit intervention. These in-memory infrastructure retries neither reset durable callback budgets nor bypass a callback's declared recovery strategy.
|
|
129
|
+
|
|
130
|
+
## Consumer linting
|
|
131
|
+
|
|
132
|
+
Use `serviceConfig` or `handlersConfig` from the corresponding `@ai-digital-factory/eslint-plugin-state-flow/eslint.*.config.js` entrypoint and run lint in CI alongside compile and typecheck. These type-aware configurations enforce promise observation, require automatic runtime ownership through await using or withDurableRuntime for services, prevent legacy execution bypasses, and keep runtime construction out of handlers. Explicit framework/operator lifecycle adapters may override the constructor restriction. Prepared dispatchers and unrelated transport methods are permitted. Fire-and-forget work requires explicit rejection handling; `void` alone does not satisfy the lint rule.
|
|
133
|
+
|
|
134
|
+
`flow.ensureInstance({ instanceId }).run()` is the primary acquisition API. The advanced `handleEnsure(...).ensure({ instanceId })` interface supports explicit outcome handling. It opens an existing instance or reserves and creates it once, including overlapping callers. It starts interrupted recovery and handles the internal not-found/already-exists cases. Awaiting ensure returns the instance directly; its snapshot may be running, recovering, awaiting attention or terminated. The mandatory mismatch callback must throw/reject or transfer control (`never` or `Promise<never>`); it cannot replace the instance with another return value. Dispatch still requires a shared failure policy or exhaustive local outcome handlers. The mismatch callback receives structured identity/revision details and a library-provided diagnostic message. Use `handleCreate` or `handleOpen` when your application deliberately requires create-only or open-only behavior.
|
|
135
|
+
|
|
136
|
+
`flow.handleDispatch(onFailure)` configures the shared policy once. Every non-applied outcome (`blocked`, `invalid-payload`, `busy`, `recovery-pending`, `needs-attention`, `state-mismatch`, `request-conflict`, `terminated`) produces a `DurableDispatchError` with `outcome`, `instanceId` and correlated `context`. The callback must throw/reject or transfer control; types and runtime guards prevent it from converting a non-applied command into fulfilled success. Infrastructure and programmer errors still reject directly. Pending recovery continues independently while the runtime remains open. Direct dispatch does not automatically retry caller requests or wait for background recovery. If one call needs local result mapping, the existing exhaustive `when(state).handle(outcomes).dispatch(input)` remains available and uses those local handlers instead of the shared policy.
|
|
137
|
+
|
|
138
|
+
`createRuntime({ databasePath, shutdownTimeoutMs })` creates a standalone runtime synchronously. Use `await using runtime = createRuntime(...)` to call operations sequentially and automatically await shutdown on normal completion or exceptions. TypeScript projects should include `ESNext.Disposable` in `compilerOptions.lib`. The configured shutdown timeout applies to disposal and to `runtime.close()`; explicit `close({ timeoutMs })` overrides it. `withDurableRuntime` remains the callback-style alternative. Applications with an existing framework lifecycle can explicitly close the runtime, with a deliberate override of the managed-runtime lint rule.
|
|
139
|
+
|
|
140
|
+
The primary flow builder is `runtime.createFlow(compiled, config).handleFailure(onFailure).run()`. Await its terminal run to register the flow. Before that terminal call it neither validates the module nor registers the chart. Acquisition uses `flow.ensureInstance({ instanceId }).run()`, inheriting the same failure policy. An optional `.handleEnsureInstance({ "definition-mismatch": callback })` before run provides a local mismatch override; it must also throw/reject or transfer control.
|
|
141
|
+
|
|
142
|
+
Both terminal calls are asynchronous and each builder memoizes its run promise. New builders can acquire the same existing instance without recreating it. Failure phases are `create-flow`, `ensure-instance` and `dispatch`. Setup/acquisition operation errors retain the original cause; `DurableDefinitionMismatchError` supplies its structured mismatch context; dispatch errors retain their outcome-specific context. A local mismatch override is exclusive: its exceptions are not rerouted through the shared policy. Advanced eager registration and explicit local outcome APIs remain available for tooling.
|
|
143
|
+
|
|
144
|
+
## License
|
|
145
|
+
|
|
146
|
+
Proprietary software. All third-party use requires prior written permission from
|
|
147
|
+
coupleaddagaming@gmail.com. See LICENSE.
|