lightflow-engine 0.2.2 → 0.2.4
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/dist/src/compat/api.d.ts +34 -0
- package/dist/src/compat/fetch.d.ts +1 -0
- package/dist/src/compat/metadata.d.ts +3 -0
- package/dist/src/compat/next.d.ts +15 -0
- package/dist/src/compat/workflow.d.ts +33 -0
- package/dist/src/index.d.ts +189 -0
- package/dist/src/pg-store.d.ts +11 -0
- package/dist/test/bench.d.ts +1 -0
- package/dist/test/compat.d.ts +1 -0
- package/dist/test/coverage.d.ts +6 -0
- package/dist/test/drain.d.ts +1 -0
- package/dist/test/entry-parity.d.ts +14 -0
- package/dist/test/race.d.ts +1 -0
- package/dist/test/resume-all.d.ts +1 -0
- package/dist/test/resume.d.ts +1 -0
- package/dist/test/simulate.d.ts +1 -0
- package/dist/test/smoke.d.ts +1 -0
- package/dist/test/workflow-def.d.ts +6 -0
- package/package.json +22 -7
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Workflow compat — `workflow/api` module surface.
|
|
3
|
+
*
|
|
4
|
+
* Differences from our core Engine API:
|
|
5
|
+
* - start(fn, args): takes the workflow FUNCTION (not an id); returns the
|
|
6
|
+
* run handle directly (not a promise).
|
|
7
|
+
* - getRun(runId): returns the handle synchronously; `status` is a Promise;
|
|
8
|
+
* status includes "pending" and "cancelled".
|
|
9
|
+
* - getReadable({ startIndex }): chunk-indexed resumable stream with
|
|
10
|
+
* getTailIndex().
|
|
11
|
+
* - run.cancel(), run.returnValue.
|
|
12
|
+
*/
|
|
13
|
+
import { Engine, type Store } from "../index.js";
|
|
14
|
+
export type VercelRunStatus = "pending" | "running" | "completed" | "failed" | "cancelled";
|
|
15
|
+
export type VercelRun = {
|
|
16
|
+
runId: string;
|
|
17
|
+
/** Live getter: each access returns a FRESH promise snapshotting the
|
|
18
|
+
* current status ("pending"/"running" until terminal). Vercel's is a
|
|
19
|
+
* getter too (chat.test.ts mocks `get status()`), and entry-agents'
|
|
20
|
+
* startStopMonitor re-awaits it in a 150ms poll loop. */
|
|
21
|
+
readonly status: Promise<VercelRunStatus>;
|
|
22
|
+
returnValue: Promise<unknown>;
|
|
23
|
+
getReadable<T = unknown>(opts?: {
|
|
24
|
+
startIndex?: number;
|
|
25
|
+
}): ReadableStream<T> & {
|
|
26
|
+
getTailIndex(): Promise<number>;
|
|
27
|
+
};
|
|
28
|
+
cancel(): Promise<void>;
|
|
29
|
+
};
|
|
30
|
+
/** Configure the compat layer with a store/engine (call once at boot). */
|
|
31
|
+
export declare function initWorkflowApi(store: Store, engine?: Engine): void;
|
|
32
|
+
export declare function getEngine(): Engine;
|
|
33
|
+
export declare function start(fn: (...args: never[]) => Promise<unknown>, args: unknown[]): Promise<VercelRun>;
|
|
34
|
+
export declare function getRun(runId: string): VercelRun;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function workflowFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Workflow compat — `workflow/next` surface.
|
|
3
|
+
*
|
|
4
|
+
* Vercel's withWorkflow wraps the Next.js config to enable the directive
|
|
5
|
+
* compiler ("use workflow" / "use step"). lightflow's runtime does not need
|
|
6
|
+
* a bundler transform: directives are inert strings, workflow functions are
|
|
7
|
+
* registered explicitly by start(), and "use step" functions can be wrapped
|
|
8
|
+
* with step() from compat/workflow for durability.
|
|
9
|
+
*
|
|
10
|
+
* This drop-in is an identity wrapper so next.config.ts keeps working
|
|
11
|
+
* unchanged:
|
|
12
|
+
* import { withWorkflow } from "workflow/next";
|
|
13
|
+
* + import { withWorkflow } from "lightflow-engine/compat/next";
|
|
14
|
+
*/
|
|
15
|
+
export declare function withWorkflow<T extends Record<string, unknown>>(config: T): T;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vercel Workflow compatibility layer — `workflow` module surface.
|
|
3
|
+
*
|
|
4
|
+
* Drop-in for entry-agents-style code: same named exports, same shapes.
|
|
5
|
+
* Directive notes:
|
|
6
|
+
* - "use workflow" / "use step" are inert strings at runtime. This layer
|
|
7
|
+
* makes directly-called "use step" functions durable via the companion
|
|
8
|
+
* webpack loader (workflow/next) or explicit step() wrappers.
|
|
9
|
+
*/
|
|
10
|
+
export { FatalError, CancelledError, sleep } from "../index.js";
|
|
11
|
+
export { getWorkflowMetadata } from "../compat/metadata.js";
|
|
12
|
+
export { workflowFetch } from "../compat/fetch.js";
|
|
13
|
+
/**
|
|
14
|
+
* Vercel's getWritable returns a web-standard WritableStream whose writes
|
|
15
|
+
* are durable chunk events. Ours maps 1:1: every writer.write(chunk) is a
|
|
16
|
+
* memoized chunk event keyed by call position; writer.close() emits the
|
|
17
|
+
* terminal done marker.
|
|
18
|
+
*/
|
|
19
|
+
export declare function getWritable<T = unknown>(): WritableStream<T>;
|
|
20
|
+
/** Auto-registration id for a workflow function (stable per code site). */
|
|
21
|
+
export declare function workflowIdFor(fn: (...a: unknown[]) => Promise<unknown>): string;
|
|
22
|
+
/**
|
|
23
|
+
* makeStep — turns a plain async function into a durable step callable.
|
|
24
|
+
*
|
|
25
|
+
* Entry's "use step" functions take arguments; lightflow's step() takes a
|
|
26
|
+
* thunk. makeStep wraps both: the returned function is a drop-in for the
|
|
27
|
+
* original (same signature), but each call becomes a memoized, retrying,
|
|
28
|
+
* replayable step keyed by call position. Outside a workflow it degrades
|
|
29
|
+
* to a plain call, so tests and non-durable paths keep working.
|
|
30
|
+
*/
|
|
31
|
+
export declare function makeStep<A extends unknown[], R>(fn: (...args: A) => Promise<R>, opts?: {
|
|
32
|
+
retries?: number;
|
|
33
|
+
}): (...args: A) => Promise<R>;
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* lightflow — a durable workflow engine, from scratch.
|
|
3
|
+
*
|
|
4
|
+
* Implements every primitive Entry's workflows rely on:
|
|
5
|
+
* - "use workflow" deterministic orchestration, replayed from an event log
|
|
6
|
+
* - "use step" at-least-once, memoized side effects
|
|
7
|
+
* - sleep(ms | Date) durable timers (survive process death)
|
|
8
|
+
* - getWritable() ordered, resumable output stream
|
|
9
|
+
* - start() / getRun() start, resume by id, await returnValue
|
|
10
|
+
* - FatalError non-retryable failure
|
|
11
|
+
* - getWorkflowMetadata() run id inside a workflow
|
|
12
|
+
*
|
|
13
|
+
* Design goals that fix the two defects found in world-postgres:
|
|
14
|
+
* 1. All timestamps are epoch milliseconds (integer). No naive/UTC skew.
|
|
15
|
+
* 2. No spec-version coupling: events are plain JSON rows with a schema
|
|
16
|
+
* version integer that the engine upgrades itself.
|
|
17
|
+
*/
|
|
18
|
+
export declare class FatalError extends Error {
|
|
19
|
+
readonly fatal = true;
|
|
20
|
+
constructor(message: string);
|
|
21
|
+
}
|
|
22
|
+
export type RunStatus = "running" | "completed" | "failed";
|
|
23
|
+
export type StepEvent = {
|
|
24
|
+
runId: string;
|
|
25
|
+
seq: number;
|
|
26
|
+
type: "step_started" | "step_completed" | "step_failed" | "sleep_created" | "sleep_completed" | "chunk" | "run_completed" | "run_failed" | "snapshot";
|
|
27
|
+
payload: unknown;
|
|
28
|
+
createdAt: number;
|
|
29
|
+
};
|
|
30
|
+
export interface Store {
|
|
31
|
+
createRun(runId: string, name: string, input: unknown): Promise<void>;
|
|
32
|
+
getRun(runId: string): Promise<{
|
|
33
|
+
status: RunStatus;
|
|
34
|
+
output?: unknown;
|
|
35
|
+
} | null>;
|
|
36
|
+
appendEvent(e: StepEvent): Promise<void>;
|
|
37
|
+
getEvents(runId: string): Promise<StepEvent[]>;
|
|
38
|
+
/** Durable timers due at or before `now`. */
|
|
39
|
+
dueTimers(now: number): Promise<{
|
|
40
|
+
runId: string;
|
|
41
|
+
seq: number;
|
|
42
|
+
key: string;
|
|
43
|
+
}[]>;
|
|
44
|
+
claimDue(now: number): Promise<boolean>;
|
|
45
|
+
/** Runs still 'running' with no activity since `cutoff` (epoch ms). */
|
|
46
|
+
staleRuns?(cutoff: number): Promise<string[]>;
|
|
47
|
+
/** Mark a run cancelled. Ignored once terminal. */
|
|
48
|
+
cancel?(runId: string): Promise<void>;
|
|
49
|
+
isCancelled?(runId: string): Promise<boolean>;
|
|
50
|
+
/** Next monotonic chunk index (max+1), atomic per run. */
|
|
51
|
+
nextChunkIndex?(runId: string): Promise<number>;
|
|
52
|
+
/** Run lease: claim (atomically) / release before executing a replay. */
|
|
53
|
+
claimRun?(runId: string, leaseMs?: number): Promise<{
|
|
54
|
+
ok: boolean;
|
|
55
|
+
cancelled: boolean;
|
|
56
|
+
}>;
|
|
57
|
+
releaseRun?(runId: string): Promise<void>;
|
|
58
|
+
/** Terminal status + lease release in one round trip. */
|
|
59
|
+
finishRun?(runId: string, status: RunStatus, output?: unknown): Promise<void>;
|
|
60
|
+
/** Optional LISTEN/NOTIFY wakeup nudge for workers. */
|
|
61
|
+
notifyWake?(): Promise<void>;
|
|
62
|
+
/** Optional dedicated LISTEN client factory (pg Pool or Client). */
|
|
63
|
+
getListenClient?(): {
|
|
64
|
+
query(sql: string): Promise<unknown>;
|
|
65
|
+
on(event: "notification", cb: () => void): void;
|
|
66
|
+
release?(): void;
|
|
67
|
+
};
|
|
68
|
+
/** Hooks: durable external callbacks a workflow can await. */
|
|
69
|
+
createHook?(runId: string, token: string, key: string): Promise<void>;
|
|
70
|
+
resolveHook?(token: string, payload: unknown): Promise<string | null>;
|
|
71
|
+
getHook?(token: string): Promise<{
|
|
72
|
+
runId: string;
|
|
73
|
+
payload: unknown;
|
|
74
|
+
} | null>;
|
|
75
|
+
setStatus(runId: string, status: RunStatus, output?: unknown): Promise<void>;
|
|
76
|
+
}
|
|
77
|
+
type WorkflowFn = (...args: unknown[]) => Promise<unknown>;
|
|
78
|
+
export declare function registerWorkflow(id: string, fn: WorkflowFn): void;
|
|
79
|
+
export declare function registerStep(id: string, fn: (...a: unknown[]) => Promise<unknown>): void;
|
|
80
|
+
type Ctx = {
|
|
81
|
+
runId: string;
|
|
82
|
+
seq: number;
|
|
83
|
+
/** deterministic call position counters — these form the replay key */
|
|
84
|
+
stepCalls: number;
|
|
85
|
+
sleepCalls: number;
|
|
86
|
+
writes: number;
|
|
87
|
+
hookCalls: number;
|
|
88
|
+
store: Store;
|
|
89
|
+
log: StepEvent[];
|
|
90
|
+
/** O(1) memo lookup: "type:key" -> event (built once at replay start) */
|
|
91
|
+
memo: Map<string, StepEvent>;
|
|
92
|
+
/** steps completed during the current execution (for snapshot trigger) */
|
|
93
|
+
completedNow: number;
|
|
94
|
+
/** results of steps completed this execution: key -> value */
|
|
95
|
+
freshResults: Map<string, unknown>;
|
|
96
|
+
/** in-flight append promises: flushed at suspension points / run end */
|
|
97
|
+
inflight: Promise<void>[];
|
|
98
|
+
/** appends an event, pipelining it with others already in flight */
|
|
99
|
+
append(e: StepEvent): Promise<void>;
|
|
100
|
+
/** index of events already consumed during replay */
|
|
101
|
+
cursor: number;
|
|
102
|
+
chunks: string[];
|
|
103
|
+
now: () => number;
|
|
104
|
+
};
|
|
105
|
+
export declare let current: Ctx | null;
|
|
106
|
+
export declare function getWorkflowMetadata(): {
|
|
107
|
+
runId: string;
|
|
108
|
+
};
|
|
109
|
+
/**
|
|
110
|
+
* Run a side effect durably. On replay the memoized result is returned and
|
|
111
|
+
* the function body is NOT re-executed.
|
|
112
|
+
*/
|
|
113
|
+
export declare function step<T>(fn: () => Promise<T>): Promise<T>;
|
|
114
|
+
/** Durable sleep. Accepts milliseconds or an absolute Date. */
|
|
115
|
+
export declare function sleep(until: number | Date): Promise<void>;
|
|
116
|
+
export declare class SuspendSignal {
|
|
117
|
+
readonly key: string;
|
|
118
|
+
readonly wakeAt: number;
|
|
119
|
+
constructor(key: string, wakeAt: number);
|
|
120
|
+
}
|
|
121
|
+
/** Ordered output stream for a run. Chunks are persisted and replayable. */
|
|
122
|
+
export declare function getWritable<T = string>(): {
|
|
123
|
+
write(chunk: T): Promise<void>;
|
|
124
|
+
close(): Promise<void>;
|
|
125
|
+
};
|
|
126
|
+
export declare class Engine {
|
|
127
|
+
private readonly store;
|
|
128
|
+
private readonly opts;
|
|
129
|
+
/** In-process run completions: runId -> deferred. Avoids polling entirely. */
|
|
130
|
+
private readonly local;
|
|
131
|
+
private defer;
|
|
132
|
+
constructor(store: Store, opts?: {
|
|
133
|
+
stepRetries?: number;
|
|
134
|
+
pollMs?: number;
|
|
135
|
+
staleRunMs?: number;
|
|
136
|
+
});
|
|
137
|
+
start(workflowId: string, args: unknown[]): Promise<{
|
|
138
|
+
runId: string;
|
|
139
|
+
}>;
|
|
140
|
+
getRun(runId: string): Promise<{
|
|
141
|
+
runId: string;
|
|
142
|
+
status: RunStatus;
|
|
143
|
+
returnValue: Promise<unknown>;
|
|
144
|
+
getReadable: () => ReadableStream<any>;
|
|
145
|
+
/** Entry calls this to kill a duplicate stream (route.ts:172). */
|
|
146
|
+
cancel: () => Promise<void>;
|
|
147
|
+
}>;
|
|
148
|
+
private waitFor;
|
|
149
|
+
private readable;
|
|
150
|
+
/** Execute (or resume) a run. Racing resumes are arbitrated by a lease. */
|
|
151
|
+
run(runId: string, workflowId: string, args: unknown[]): Promise<void>;
|
|
152
|
+
private workerStopped;
|
|
153
|
+
private listenClient?;
|
|
154
|
+
/** Signal the worker loop to exit after its current poll cycle. */
|
|
155
|
+
stopWorker(): void;
|
|
156
|
+
/** Worker loop: resume runs whose timers are due. Resolves when stopWorker() is called. */
|
|
157
|
+
startWorker(onError?: (e: unknown) => void): Promise<void>;
|
|
158
|
+
/**
|
|
159
|
+
* Built-in reaper: resume 'running' runs with no event activity in the
|
|
160
|
+
* last `staleMs`. Fixes the orphaned-run failure mode found in testing
|
|
161
|
+
* world-postgres (a run wedged in 'running' forever).
|
|
162
|
+
*/
|
|
163
|
+
private reapStale;
|
|
164
|
+
resume(runId: string): Promise<void>;
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Cancel a run (Entry's route.ts calls getRun(id).cancel() to kill duplicate
|
|
168
|
+
* streams). Terminal runs ignore this.
|
|
169
|
+
*/
|
|
170
|
+
export declare class CancelledError extends Error {
|
|
171
|
+
constructor(runId: string);
|
|
172
|
+
}
|
|
173
|
+
/** Create a durable hook a workflow can await; resolve it from outside. */
|
|
174
|
+
export declare function defineHook<T = unknown>(): {
|
|
175
|
+
create(): Promise<{
|
|
176
|
+
token: string;
|
|
177
|
+
}>;
|
|
178
|
+
};
|
|
179
|
+
/** Await a previously created hook until an external caller resolves it. */
|
|
180
|
+
export declare function hookResult<T>(token: string): Promise<T>;
|
|
181
|
+
/** Durable fetch: memoized per step position, like any other side effect. */
|
|
182
|
+
export declare function workflowFetch(input: string, init?: RequestInit): Promise<Response>;
|
|
183
|
+
/**
|
|
184
|
+
* Durable loop: executes body(i) for i in [0, n). Each iteration is guarded by
|
|
185
|
+
* a durable timer barrier; completed iterations are skipped on replay, so a
|
|
186
|
+
* resumed run continues where it stopped instead of restarting.
|
|
187
|
+
*/
|
|
188
|
+
export declare function hashId(s: string): string;
|
|
189
|
+
export {};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Postgres-backed store for lightflow.
|
|
3
|
+
*
|
|
4
|
+
* Two tables only. All times are BIGINT epoch milliseconds — no timestamp
|
|
5
|
+
* columns, so no timezone ambiguity (the bug that cost hours in world-postgres).
|
|
6
|
+
*/
|
|
7
|
+
export type Store = import("../src/index.js").Store;
|
|
8
|
+
export declare function createPostgresStore(url: string, opts?: {
|
|
9
|
+
sessionOptions?: Record<string, string>;
|
|
10
|
+
}): Promise<Store>;
|
|
11
|
+
export declare function applyTimezoneGuard(url: string): Promise<void>;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Port of Entry's sandbox-lifecycle + chat patterns onto lightflow.
|
|
3
|
+
* Mirrors: lease claim/clear, while(true) wake loop with sleep(Date),
|
|
4
|
+
* getWritable streaming, post-finish persistence, retry/FatalError.
|
|
5
|
+
*/
|
|
6
|
+
export type ParityResult = {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
runId: string;
|
|
9
|
+
ticks: number;
|
|
10
|
+
leaseValue: string | null;
|
|
11
|
+
chunks: number;
|
|
12
|
+
persisted: boolean;
|
|
13
|
+
fatalCaught: boolean;
|
|
14
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/package.json
CHANGED
|
@@ -1,17 +1,32 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "lightflow-engine",
|
|
3
|
-
"version": "0.2.
|
|
4
|
-
"description": "A tiny durable workflow engine for Node.js and Postgres
|
|
3
|
+
"version": "0.2.4",
|
|
4
|
+
"description": "A tiny durable workflow engine for Node.js and Postgres — with a Vercel Workflow drop-in compat layer.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "dist/index.js",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
|
-
".":
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
"./
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"default": "./dist/index.js"
|
|
13
|
+
},
|
|
14
|
+
"./pg": {
|
|
15
|
+
"types": "./dist/pg-store.d.ts",
|
|
16
|
+
"default": "./dist/pg-store.js"
|
|
17
|
+
},
|
|
18
|
+
"./compat/workflow": {
|
|
19
|
+
"types": "./dist/compat/workflow.d.ts",
|
|
20
|
+
"default": "./dist/compat/workflow.js"
|
|
21
|
+
},
|
|
22
|
+
"./compat/api": {
|
|
23
|
+
"types": "./dist/compat/api.d.ts",
|
|
24
|
+
"default": "./dist/compat/api.js"
|
|
25
|
+
},
|
|
26
|
+
"./compat/next": {
|
|
27
|
+
"types": "./dist/compat/next.d.ts",
|
|
28
|
+
"default": "./dist/compat/next.js"
|
|
29
|
+
}
|
|
15
30
|
},
|
|
16
31
|
"files": [
|
|
17
32
|
"dist",
|