lightflow-engine 0.2.3 → 0.2.5
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/compat/api.d.ts +34 -0
- package/dist/compat/fetch.d.ts +1 -0
- package/dist/compat/metadata.d.ts +3 -0
- package/dist/compat/next.d.ts +15 -0
- package/dist/compat/workflow.d.ts +33 -0
- package/dist/index.d.ts +189 -0
- package/dist/pg-store.d.ts +11 -0
- package/package.json +3 -3
- package/dist/test/bench-long.mjs +0 -24
- package/dist/test/bench.js +0 -51
- package/dist/test/compact-check.mjs +0 -27
- package/dist/test/compat.js +0 -87
- package/dist/test/coverage.js +0 -107
- package/dist/test/crash-snap.mjs +0 -22
- package/dist/test/dbg.mjs +0 -28
- package/dist/test/dbg2.mjs +0 -34
- package/dist/test/dbg3.mjs +0 -23
- package/dist/test/drain.js +0 -21
- package/dist/test/entry-parity.js +0 -115
- package/dist/test/race.js +0 -43
- package/dist/test/resume-all.js +0 -14
- package/dist/test/resume.js +0 -12
- package/dist/test/simulate.js +0 -46
- package/dist/test/smoke.js +0 -157
- package/dist/test/workflow-def.js +0 -54
- /package/dist/{src/compat → compat}/api.js +0 -0
- /package/dist/{src/compat → compat}/fetch.js +0 -0
- /package/dist/{src/compat → compat}/metadata.js +0 -0
- /package/dist/{src/compat → compat}/next.js +0 -0
- /package/dist/{src/compat → compat}/workflow.js +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
- /package/dist/{src/pg-store.js → pg-store.js} +0 -0
|
@@ -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>;
|
package/dist/index.d.ts
ADDED
|
@@ -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>;
|
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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.5",
|
|
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
|
-
"main": "dist/index.js",
|
|
7
|
+
"main": "dist/index.js", "types": "dist/index.d.ts",
|
|
8
8
|
"types": "dist/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
package/dist/test/bench-long.mjs
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Engine, step, registerWorkflow } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
-
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
-
const url = process.env.LIGHTFLOW_PG_URL;
|
|
5
|
-
const store = await createPostgresStore(url);
|
|
6
|
-
const N = Number(process.env.RN ?? 100); // steps per workflow
|
|
7
|
-
registerWorkflow("long", async () => {
|
|
8
|
-
for (let i = 0; i < N; i++) await step(async () => i);
|
|
9
|
-
return N;
|
|
10
|
-
});
|
|
11
|
-
const engine = new Engine(store, { pollMs: 50 });
|
|
12
|
-
// throughput pass
|
|
13
|
-
const t0 = performance.now();
|
|
14
|
-
const RUNS = 50;
|
|
15
|
-
await Promise.all(Array.from({length: 10}, async () => {
|
|
16
|
-
for (let i = 0; i < RUNS/10; i++) {
|
|
17
|
-
const { runId } = await engine.start("long", []);
|
|
18
|
-
const run = await engine.getRun(runId);
|
|
19
|
-
await run.returnValue;
|
|
20
|
-
}
|
|
21
|
-
}));
|
|
22
|
-
const dt = (performance.now()-t0)/1000;
|
|
23
|
-
console.log(JSON.stringify({ stepsPerWorkflow: N, runs: RUNS, seconds: +dt.toFixed(2), runsPerSec: +(RUNS/dt).toFixed(1), stepsPerSec: +(RUNS*N/dt).toFixed(1) }));
|
|
24
|
-
process.exit(0);
|
package/dist/test/bench.js
DELETED
|
@@ -1,51 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Throughput benchmark for lightflow.
|
|
3
|
-
* Runs N workflows, each with M steps, in-flight concurrency C.
|
|
4
|
-
* Reports runs/sec, steps/sec, and SQL statements per run (from pg_stat_statements if available).
|
|
5
|
-
*/
|
|
6
|
-
import { Engine, step, registerWorkflow } from "../src/index.js";
|
|
7
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
8
|
-
const PG_URL = process.env.LIGHTFLOW_PG_URL;
|
|
9
|
-
const N = Number(process.env.BENCH_N ?? 200);
|
|
10
|
-
const M = Number(process.env.BENCH_M ?? 5);
|
|
11
|
-
const CONC = Number(process.env.BENCH_CONC ?? 20);
|
|
12
|
-
async function main() {
|
|
13
|
-
const store = await createPostgresStore(PG_URL);
|
|
14
|
-
let calls = 0;
|
|
15
|
-
registerWorkflow("bench", async () => {
|
|
16
|
-
"use workflow";
|
|
17
|
-
for (let i = 0; i < M; i++) {
|
|
18
|
-
await step(async () => {
|
|
19
|
-
"use step";
|
|
20
|
-
return i * 2 + (calls++, 0);
|
|
21
|
-
});
|
|
22
|
-
}
|
|
23
|
-
return M;
|
|
24
|
-
});
|
|
25
|
-
const engine = new Engine(store, { pollMs: 100 });
|
|
26
|
-
// reset counters
|
|
27
|
-
await store.appendEvent === undefined; // noop
|
|
28
|
-
const t0 = performance.now();
|
|
29
|
-
const handles = [];
|
|
30
|
-
let done = 0;
|
|
31
|
-
const queue = Array.from({ length: N }, (_, i) => i);
|
|
32
|
-
async function worker() {
|
|
33
|
-
while (queue.length) {
|
|
34
|
-
const i = queue.shift();
|
|
35
|
-
const { runId } = await engine.start("bench", []);
|
|
36
|
-
const run = await engine.getRun(runId);
|
|
37
|
-
await run.returnValue;
|
|
38
|
-
done++;
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
await Promise.all(Array.from({ length: CONC }, worker));
|
|
42
|
-
const dt = (performance.now() - t0) / 1000;
|
|
43
|
-
console.log(JSON.stringify({
|
|
44
|
-
runs: N, stepsPerRun: M, concurrency: CONC,
|
|
45
|
-
seconds: +dt.toFixed(2),
|
|
46
|
-
runsPerSec: +(N / dt).toFixed(1),
|
|
47
|
-
stepsPerSec: +((N * M) / dt).toFixed(1),
|
|
48
|
-
}, null, 2));
|
|
49
|
-
process.exit(0);
|
|
50
|
-
}
|
|
51
|
-
main().catch((e) => { console.error(e); process.exit(1); });
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Engine, step, registerWorkflow } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
-
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
-
import pg from "pg";
|
|
5
|
-
const url = process.env.LIGHTFLOW_PG_URL;
|
|
6
|
-
const store = await createPostgresStore(url);
|
|
7
|
-
const pool = new pg.Pool({ connectionString: url });
|
|
8
|
-
const N = 600;
|
|
9
|
-
let calls = 0;
|
|
10
|
-
registerWorkflow("compact", async () => {
|
|
11
|
-
const vals = [];
|
|
12
|
-
for (let i = 0; i < N; i++) vals.push(await step(async () => { calls++; return i; }));
|
|
13
|
-
return vals.length;
|
|
14
|
-
});
|
|
15
|
-
const engine = new Engine(store);
|
|
16
|
-
const { runId } = await engine.start("compact", []);
|
|
17
|
-
const run = await engine.getRun(runId);
|
|
18
|
-
const ret = await run.returnValue;
|
|
19
|
-
// count events + snapshots
|
|
20
|
-
const evs = await pool.query("SELECT type, count(*) c FROM lightflow_events WHERE run_id=$1 GROUP BY 1", [runId]);
|
|
21
|
-
const snaps = await pool.query("SELECT count(*) c FROM lightflow_events WHERE run_id=$1 AND type='snapshot'", [runId]);
|
|
22
|
-
console.log("result:", ret, "calls:", calls, "snapshots:", snaps.rows[0].c);
|
|
23
|
-
console.log(evs.rows.map(r=>`${r.type}=${r.c}`).join(", "));
|
|
24
|
-
// replay from snapshot: simulate resume on completed run (should be no-op) - measure getEvents size returned
|
|
25
|
-
const log = await store.getEvents(runId);
|
|
26
|
-
console.log("getEvents rows returned (should be << total):", log.length);
|
|
27
|
-
await pool.end(); process.exit(0);
|
package/dist/test/compat.js
DELETED
|
@@ -1,87 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Drop-in verification: the exact Vercel Workflow surface entry-agents uses,
|
|
3
|
-
* exercised against lightflow's compat layer:
|
|
4
|
-
* start(fn, args) / getRun(id) / run.status (Promise) / run.runId /
|
|
5
|
-
* run.returnValue / run.cancel() / run.getReadable({startIndex}) /
|
|
6
|
-
* getTailIndex() / getWritable() as WritableStream / getWorkflowMetadata()
|
|
7
|
-
* -> { workflowRunId } / sleep(Date) / FatalError.
|
|
8
|
-
*/
|
|
9
|
-
import { test } from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import pg from "pg";
|
|
12
|
-
import { Engine, step, sleep, FatalError } from "../src/index.js";
|
|
13
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
14
|
-
import { initWorkflowApi, start } from "../src/compat/api.js";
|
|
15
|
-
import { getWritable, getWorkflowMetadata } from "../src/compat/workflow.js";
|
|
16
|
-
const PG_URL = process.env.LIGHTFLOW_PG_URL;
|
|
17
|
-
if (!PG_URL)
|
|
18
|
-
throw new Error("LIGHTFLOW_PG_URL is required");
|
|
19
|
-
test("compat: vercel-style agent chat workflow end-to-end", async () => {
|
|
20
|
-
const store = await createPostgresStore(PG_URL);
|
|
21
|
-
const engine = new Engine(store, { pollMs: 25 });
|
|
22
|
-
initWorkflowApi(store, engine);
|
|
23
|
-
const workerPromise = engine.startWorker();
|
|
24
|
-
setTimeout(() => engine.stopWorker(), 60_000);
|
|
25
|
-
const pool = new pg.Pool({ connectionString: PG_URL });
|
|
26
|
-
// A workflow shaped like entry-agents' runAgentWorkflow:
|
|
27
|
-
// - "use step"-style direct calls via step()
|
|
28
|
-
// - getWritable() as a real WritableStream, written via getWriter()
|
|
29
|
-
// - getWorkflowMetadata() -> { workflowRunId }
|
|
30
|
-
// - sleep(new Date(...)) for durable timers
|
|
31
|
-
// - FatalError for non-retryable failures
|
|
32
|
-
const runAgentWorkflow = async (opts) => {
|
|
33
|
-
"use workflow";
|
|
34
|
-
const { workflowRunId } = getWorkflowMetadata();
|
|
35
|
-
const writable = getWritable();
|
|
36
|
-
const writer = writable.getWriter();
|
|
37
|
-
await step(async () => {
|
|
38
|
-
await writer.write(`start:${opts.chatId}`);
|
|
39
|
-
});
|
|
40
|
-
const answer = await step(async () => `reply-to:${opts.messages.join(",")}`);
|
|
41
|
-
// mid-run durable timer like sandbox-lifecycle
|
|
42
|
-
await sleep(new Date(Date.now() + 50));
|
|
43
|
-
await step(async () => { await writer.write(`chunk:${answer}`); });
|
|
44
|
-
await step(async () => { await writer.write("done-marker"); });
|
|
45
|
-
await writer.close();
|
|
46
|
-
if (opts.messages.includes("boom"))
|
|
47
|
-
throw new FatalError("nope");
|
|
48
|
-
return { workflowRunId, answer };
|
|
49
|
-
};
|
|
50
|
-
// Vercel-style start(fn, args) — function, not id
|
|
51
|
-
const run = await start(runAgentWorkflow, [
|
|
52
|
-
{ messages: ["hello", "world"], chatId: "c1" },
|
|
53
|
-
]);
|
|
54
|
-
assert.ok(run.runId.startsWith("lrun_"));
|
|
55
|
-
// status is a live getter; await terminal state via returnValue
|
|
56
|
-
await run.returnValue;
|
|
57
|
-
const status = await run.status;
|
|
58
|
-
assert.equal(status, "completed");
|
|
59
|
-
const ret = (await run.returnValue);
|
|
60
|
-
assert.equal(ret.answer, "reply-to:hello,world");
|
|
61
|
-
assert.equal(ret.workflowRunId, run.runId);
|
|
62
|
-
// getReadable() with startIndex + getTailIndex
|
|
63
|
-
const readable = run.getReadable({ startIndex: 0 });
|
|
64
|
-
const tail = await readable.getTailIndex();
|
|
65
|
-
const parts = [];
|
|
66
|
-
const reader = readable.getReader();
|
|
67
|
-
for (;;) {
|
|
68
|
-
const { done, value } = await reader.read();
|
|
69
|
-
if (done)
|
|
70
|
-
break;
|
|
71
|
-
parts.push(value);
|
|
72
|
-
}
|
|
73
|
-
assert.deepEqual(parts, ["start:c1", "chunk:reply-to:hello,world", "done-marker"]);
|
|
74
|
-
assert.ok(tail >= 1);
|
|
75
|
-
// resume from startIndex: skipping the first chunk
|
|
76
|
-
const resumed = run.getReadable({ startIndex: 1 });
|
|
77
|
-
const rreader = resumed.getReader();
|
|
78
|
-
const r1 = await rreader.read();
|
|
79
|
-
assert.equal(r1.value, "chunk:reply-to:hello,world");
|
|
80
|
-
// cancel semantics: getRun + status + cancel()
|
|
81
|
-
const dup = await start(runAgentWorkflow, [{ messages: ["x"], chatId: "c2" }]);
|
|
82
|
-
await dup.cancel();
|
|
83
|
-
assert.equal(await dup.status, "cancelled"); // live getter reflects cancel
|
|
84
|
-
await pool.end();
|
|
85
|
-
engine.stopWorker();
|
|
86
|
-
await workerPromise;
|
|
87
|
-
});
|
package/dist/test/coverage.js
DELETED
|
@@ -1,107 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Coverage suite: every primitive Entry's workflows use, plus cancel,
|
|
3
|
-
* hooks, retry-then-FatalError, and durable timers — all crash-tested.
|
|
4
|
-
*/
|
|
5
|
-
import { Engine, step, sleep, getWritable, FatalError, getWorkflowMetadata, defineHook, workflowFetch, } from "../src/index.js";
|
|
6
|
-
const URL = process.env.LIGHTFLOW_PG_URL;
|
|
7
|
-
export async function coverageWorkflow(opts) {
|
|
8
|
-
"use workflow";
|
|
9
|
-
const { runId } = getWorkflowMetadata();
|
|
10
|
-
const writable = getWritable();
|
|
11
|
-
const observed = { runId };
|
|
12
|
-
await writable.write("stage:start;");
|
|
13
|
-
// 1. step retries a transient error, then succeeds
|
|
14
|
-
let attempts = 0;
|
|
15
|
-
const resilient = await step(async () => {
|
|
16
|
-
attempts += 1;
|
|
17
|
-
if (attempts < 3)
|
|
18
|
-
throw new Error("transient");
|
|
19
|
-
return "ok-after-2-retries";
|
|
20
|
-
});
|
|
21
|
-
observed.resilient = resilient;
|
|
22
|
-
// 2. FatalError is NOT retried and aborts the run
|
|
23
|
-
let fatalMessage = null;
|
|
24
|
-
try {
|
|
25
|
-
await step(async () => { throw new FatalError("hard stop"); });
|
|
26
|
-
}
|
|
27
|
-
catch (e) {
|
|
28
|
-
fatalMessage = e instanceof FatalError ? e.message : "unknown";
|
|
29
|
-
}
|
|
30
|
-
observed.fatalMessage = fatalMessage;
|
|
31
|
-
// 3. durable sleep (Date)
|
|
32
|
-
for (let i = 0; i < opts.ticks; i += 1) {
|
|
33
|
-
await sleep(new Date(Date.now() + 700));
|
|
34
|
-
await writable.write(`stage:tick:${i + 1};`);
|
|
35
|
-
}
|
|
36
|
-
observed.ticks = opts.ticks;
|
|
37
|
-
// 4. durable sleep (ms overload)
|
|
38
|
-
await sleep(300);
|
|
39
|
-
observed.sleptMsOverload = true;
|
|
40
|
-
// 5. hooks
|
|
41
|
-
if (opts.useHook) {
|
|
42
|
-
const { token } = await defineHook().create();
|
|
43
|
-
observed.hookToken = token;
|
|
44
|
-
}
|
|
45
|
-
// 6. durable fetch (memoized)
|
|
46
|
-
try {
|
|
47
|
-
const r = await workflowFetch("http://127.0.0.1:9/nope");
|
|
48
|
-
observed.fetchStatus = r.status;
|
|
49
|
-
}
|
|
50
|
-
catch (e) {
|
|
51
|
-
observed.fetchError = e instanceof Error ? e.message : String(e);
|
|
52
|
-
}
|
|
53
|
-
await writable.write("stage:done;");
|
|
54
|
-
await writable.close();
|
|
55
|
-
return observed;
|
|
56
|
-
}
|
|
57
|
-
export async function register() {
|
|
58
|
-
const { registerWorkflow } = await import("../src/index.js");
|
|
59
|
-
registerWorkflow("coverage", coverageWorkflow);
|
|
60
|
-
}
|
|
61
|
-
/* ---------------- runner ---------------- */
|
|
62
|
-
const { createPostgresStore } = await import("../src/pg-store.js");
|
|
63
|
-
const mode = process.argv[2] ?? "run";
|
|
64
|
-
if (mode === "register-only")
|
|
65
|
-
process.exit(0);
|
|
66
|
-
const store = await createPostgresStore(URL);
|
|
67
|
-
const engine = new Engine(store, { pollMs: 200, staleRunMs: 15_000 });
|
|
68
|
-
await register();
|
|
69
|
-
void engine.startWorker(() => { });
|
|
70
|
-
if (mode === "run") {
|
|
71
|
-
const { runId } = await engine.start("coverage", [
|
|
72
|
-
{ ticks: Number(process.argv[3] ?? 3), shouldFailFatally: false, useHook: true },
|
|
73
|
-
]);
|
|
74
|
-
console.log("STARTED", runId);
|
|
75
|
-
const run = await engine.getRun(runId);
|
|
76
|
-
const rv = await run.returnValue;
|
|
77
|
-
console.log("RESULT", JSON.stringify(rv));
|
|
78
|
-
process.exit(0);
|
|
79
|
-
}
|
|
80
|
-
if (mode === "cancel") {
|
|
81
|
-
const { runId } = await engine.start("coverage", [
|
|
82
|
-
{ ticks: 12, shouldFailFatally: false, useHook: false },
|
|
83
|
-
]);
|
|
84
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
85
|
-
const run = await engine.getRun(runId);
|
|
86
|
-
await run.cancel();
|
|
87
|
-
const after = await store.getRun(runId);
|
|
88
|
-
console.log("CANCELLED", JSON.stringify(after));
|
|
89
|
-
process.exit(0);
|
|
90
|
-
}
|
|
91
|
-
if (mode === "resume") {
|
|
92
|
-
const runId = process.argv[3];
|
|
93
|
-
await engine.resume(runId);
|
|
94
|
-
const row = await store.getRun(runId);
|
|
95
|
-
console.log("RESUMED", row?.status, JSON.stringify(row?.output));
|
|
96
|
-
process.exit(0);
|
|
97
|
-
}
|
|
98
|
-
if (mode === "hook") {
|
|
99
|
-
const runId = process.argv[3];
|
|
100
|
-
const token = process.argv[4];
|
|
101
|
-
const target = await store.resolveHook(token, { ok: true });
|
|
102
|
-
console.log("HOOK_RESOLVED", target);
|
|
103
|
-
await engine.resume(runId);
|
|
104
|
-
const row = await store.getRun(runId);
|
|
105
|
-
console.log("AFTER_HOOK", row?.status, JSON.stringify(row?.output));
|
|
106
|
-
process.exit(0);
|
|
107
|
-
}
|
package/dist/test/crash-snap.mjs
DELETED
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Engine, step, registerWorkflow, sleep } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
-
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
-
const url = process.env.LIGHTFLOW_PG_URL;
|
|
5
|
-
const store = await createPostgresStore(url);
|
|
6
|
-
const N = 700;
|
|
7
|
-
registerWorkflow("crashy", async () => {
|
|
8
|
-
const vals = [];
|
|
9
|
-
for (let i = 0; i < N; i++) {
|
|
10
|
-
vals.push(await step(async () => i));
|
|
11
|
-
if (i === 350) await sleep(120); // suspend mid-run; snapshots exist by now
|
|
12
|
-
}
|
|
13
|
-
return vals.length;
|
|
14
|
-
});
|
|
15
|
-
const engine = new Engine(store, { pollMs: 50 });
|
|
16
|
-
const wp = engine.startWorker();
|
|
17
|
-
setTimeout(() => engine.stopWorker(), 60000);
|
|
18
|
-
const { runId } = await engine.start("crashy", []);
|
|
19
|
-
const run = await engine.getRun(runId);
|
|
20
|
-
const ret = await run.returnValue;
|
|
21
|
-
console.log("completed after suspend+resume:", ret, ret === N ? "OK" : "FAIL"); engine.stopWorker();
|
|
22
|
-
process.exit(0);
|
package/dist/test/dbg.mjs
DELETED
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Engine, step, sleep, registerWorkflow } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
-
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
-
import { getWritable } from "/home/agentuser/lightflow/dist/src/compat/workflow.js";
|
|
5
|
-
import { initWorkflowApi, start } from "/home/agentuser/lightflow/dist/src/compat/api.js";
|
|
6
|
-
import pg from "pg";
|
|
7
|
-
const url = process.env.LIGHTFLOW_PG_URL;
|
|
8
|
-
const store = await createPostgresStore(url);
|
|
9
|
-
const engine = new Engine(store, { pollMs: 25 });
|
|
10
|
-
initWorkflowApi(store, engine);
|
|
11
|
-
const wp = engine.startWorker();
|
|
12
|
-
setTimeout(() => engine.stopWorker(), 30000);
|
|
13
|
-
const wf = async () => {
|
|
14
|
-
const wr = getWritable();
|
|
15
|
-
const writer = wr.getWriter();
|
|
16
|
-
await step(async () => { await writer.write("A"); });
|
|
17
|
-
await sleep(new Date(Date.now() + 50));
|
|
18
|
-
await step(async () => { await writer.write("B"); });
|
|
19
|
-
await step(async () => { await writer.write("C"); });
|
|
20
|
-
await writer.close();
|
|
21
|
-
return "ok";
|
|
22
|
-
};
|
|
23
|
-
const run = await start(wf, []);
|
|
24
|
-
console.log("status:", await run.status);
|
|
25
|
-
const pool = new pg.Pool({ connectionString: url });
|
|
26
|
-
const rows = await pool.query("SELECT seq, payload->>'key' k, payload->>'value' v FROM lightflow_events WHERE type='chunk' ORDER BY seq");
|
|
27
|
-
console.log(rows.rows.map(r=>`${r.k}=${r.v}`).join(" | "));
|
|
28
|
-
await pool.end(); process.exit(0);
|
package/dist/test/dbg2.mjs
DELETED
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Engine, step, sleep, registerWorkflow } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
-
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
-
import { getWritable } from "/home/agentuser/lightflow/dist/src/compat/workflow.js";
|
|
5
|
-
import { initWorkflowApi, start } from "/home/agentuser/lightflow/dist/src/compat/api.js";
|
|
6
|
-
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
7
|
-
const engine = new Engine(store, { pollMs: 25 });
|
|
8
|
-
initWorkflowApi(store, engine);
|
|
9
|
-
const wp = engine.startWorker();
|
|
10
|
-
setTimeout(() => engine.stopWorker(), 60000);
|
|
11
|
-
const wf = async () => {
|
|
12
|
-
const writer = getWritable().getWriter();
|
|
13
|
-
for (let i = 0; i < 205; i++) {
|
|
14
|
-
await step(async () => { await writer.write("c" + i); });
|
|
15
|
-
if (i === 100) await sleep(new Date(Date.now() + 40));
|
|
16
|
-
}
|
|
17
|
-
await writer.close();
|
|
18
|
-
return "done";
|
|
19
|
-
};
|
|
20
|
-
const run = await start(wf, []);
|
|
21
|
-
console.log("status:", await run.status);
|
|
22
|
-
const readable = run.getReadable({ startIndex: 0 });
|
|
23
|
-
const reader = readable.getReader();
|
|
24
|
-
const parts = [];
|
|
25
|
-
for (;;) { const { done, value } = await reader.read(); if (done) break; parts.push(value); }
|
|
26
|
-
const missing = []; for (let i=0;i<205;i++) if (!parts.includes("c"+i)) missing.push(i); console.log("chunks:", parts.length, "nmissing:", missing.length, "m0:", missing[0], "mLast:", missing[missing.length-1]);
|
|
27
|
-
console.log("tailIndex:", await readable.getTailIndex());
|
|
28
|
-
// resume from index 200
|
|
29
|
-
const r2 = run.getReadable({ startIndex: 200 });
|
|
30
|
-
const rd2 = r2.getReader();
|
|
31
|
-
const p2 = [];
|
|
32
|
-
for (;;) { const { done, value } = await rd2.read(); if (done) break; p2.push(value); }
|
|
33
|
-
console.log("resumed-from-200:", p2.length, p2[0], p2.at(-1));
|
|
34
|
-
process.exit(0);
|
package/dist/test/dbg3.mjs
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
|
|
2
|
-
import { Engine, step, sleep } from "/home/agentuser/lightflow/dist/src/index.js";
|
|
3
|
-
import { createPostgresStore } from "/home/agentuser/lightflow/dist/src/pg-store.js";
|
|
4
|
-
import { initWorkflowApi, start } from "/home/agentuser/lightflow/dist/src/compat/api.js";
|
|
5
|
-
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
6
|
-
const engine = new Engine(store, { pollMs: 25 });
|
|
7
|
-
initWorkflowApi(store, engine);
|
|
8
|
-
const wp = engine.startWorker();
|
|
9
|
-
setTimeout(() => engine.stopWorker(), 20000);
|
|
10
|
-
const wf = async () => {
|
|
11
|
-
await sleep(new Date(Date.now() + 300));
|
|
12
|
-
await step(async () => "x");
|
|
13
|
-
return "ok";
|
|
14
|
-
};
|
|
15
|
-
const run = await start(wf, []);
|
|
16
|
-
const s1 = await run.status; // live snapshot (running/pending)
|
|
17
|
-
const s2 = await run.status; // fresh promise each access
|
|
18
|
-
console.log("live status:", s1, s2);
|
|
19
|
-
const t = await run.status && await (async()=>{ // wait terminal
|
|
20
|
-
for(;;){ const st = await run.status; if(st!=="running"&&st!=="pending") return st; await new Promise(r=>setTimeout(r,25)); }
|
|
21
|
-
})();
|
|
22
|
-
console.log("terminal:", t);
|
|
23
|
-
process.exit(0);
|
package/dist/test/drain.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { Engine } from "../src/index.js";
|
|
2
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
3
|
-
import { registerAll } from "./workflow-def.js";
|
|
4
|
-
import pg from "pg";
|
|
5
|
-
registerAll();
|
|
6
|
-
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
7
|
-
const engine = new Engine(store, { pollMs: 300, staleRunMs: 10_000 });
|
|
8
|
-
const p = new pg.Pool({ connectionString: process.env.LIGHTFLOW_PG_URL });
|
|
9
|
-
// persistent driver: keeps resuming until nothing is running
|
|
10
|
-
void engine.startWorker(() => { });
|
|
11
|
-
const deadline = Date.now() + 9 * 60_000;
|
|
12
|
-
while (Date.now() < deadline) {
|
|
13
|
-
const rows = await p.query("SELECT run_id FROM lightflow_runs WHERE status='running'");
|
|
14
|
-
if (rows.rows.length === 0)
|
|
15
|
-
break;
|
|
16
|
-
await Promise.all(rows.rows.slice(0, 12).map((r) => engine.resume(r.run_id).catch(() => { })));
|
|
17
|
-
await new Promise((r) => setTimeout(r, 1200));
|
|
18
|
-
}
|
|
19
|
-
const done = await p.query("SELECT status,count(*) FROM lightflow_runs GROUP BY status");
|
|
20
|
-
console.log("FINAL", JSON.stringify(done.rows));
|
|
21
|
-
process.exit(0);
|
|
@@ -1,115 +0,0 @@
|
|
|
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
|
-
import { Engine, step, sleep, getWritable, FatalError, getWorkflowMetadata } from "../src/index.js";
|
|
7
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
8
|
-
const URL = process.env.LIGHTFLOW_PG_URL ?? "postgres://entry:entry@localhost:5432/entry_lightflow";
|
|
9
|
-
/* ---------- steps (side effects) ---------- */
|
|
10
|
-
async function claimLease(sessionId, value) {
|
|
11
|
-
"use step";
|
|
12
|
-
const pg = (await import("pg")).default;
|
|
13
|
-
const pool = new pg.Pool({ connectionString: process.env.APP_PG_URL, max: 4 });
|
|
14
|
-
const r = await pool.query(`UPDATE sessions SET lifecycle_run_id=$2 WHERE id=$1 AND lifecycle_run_id IS NULL`, [sessionId, value]);
|
|
15
|
-
await pool.end();
|
|
16
|
-
return (r.rowCount ?? 0) > 0;
|
|
17
|
-
}
|
|
18
|
-
async function clearLease(sessionId) {
|
|
19
|
-
"use step";
|
|
20
|
-
const pg = (await import("pg")).default;
|
|
21
|
-
const pool = new pg.Pool({ connectionString: process.env.APP_PG_URL, max: 4 });
|
|
22
|
-
await pool.query(`UPDATE sessions SET lifecycle_run_id=NULL WHERE id=$1`, [sessionId]);
|
|
23
|
-
await pool.end();
|
|
24
|
-
}
|
|
25
|
-
async function persistsFinish(runId, chatId, durationMs) {
|
|
26
|
-
"use step";
|
|
27
|
-
const pg = (await import("pg")).default;
|
|
28
|
-
const pool = new pg.Pool({ connectionString: process.env.APP_PG_URL, max: 4 });
|
|
29
|
-
await pool.query(`INSERT INTO workflow_runs (id, chat_id, session_id, user_id, status, started_at, finished_at, total_duration_ms)
|
|
30
|
-
VALUES ($1,$2,'selftest-session','selftest-user','completed',now(),now(),$3)
|
|
31
|
-
ON CONFLICT (id) DO NOTHING`, [runId, chatId, Math.round(durationMs)]);
|
|
32
|
-
await pool.end();
|
|
33
|
-
return true;
|
|
34
|
-
}
|
|
35
|
-
async function flakyStep() {
|
|
36
|
-
"use step";
|
|
37
|
-
throw new Error("transient failure");
|
|
38
|
-
}
|
|
39
|
-
/* ---------- the workflow ---------- */
|
|
40
|
-
async function parityWorkflow(opts) {
|
|
41
|
-
"use workflow";
|
|
42
|
-
const { runId } = getWorkflowMetadata();
|
|
43
|
-
const writable = getWritable();
|
|
44
|
-
const started = Date.now();
|
|
45
|
-
let fatalCaught = false;
|
|
46
|
-
await writable.write("stage:start;");
|
|
47
|
-
// --- retry semantics: transient error retried, FatalError not
|
|
48
|
-
try {
|
|
49
|
-
await step(flakyStep);
|
|
50
|
-
}
|
|
51
|
-
catch (e) {
|
|
52
|
-
if (e instanceof FatalError)
|
|
53
|
-
fatalCaught = true;
|
|
54
|
-
else
|
|
55
|
-
fatalCaught = true; // engine treats unknown as non-fatal retry-then-fail
|
|
56
|
-
}
|
|
57
|
-
const lease = `lrun-${runId}`;
|
|
58
|
-
const claimed = await step(() => claimLease(opts.sessionId, lease));
|
|
59
|
-
await writable.write(`stage:lease:${claimed};`);
|
|
60
|
-
// --- durable timer loop (mirrors sandbox-lifecycle's while(true))
|
|
61
|
-
for (let i = 0; i < opts.ticks; i += 1) {
|
|
62
|
-
await sleep(new Date(Date.now() + 1000));
|
|
63
|
-
await writable.write(`stage:tick:${i + 1};`);
|
|
64
|
-
}
|
|
65
|
-
const persisted = await step(() => persistsFinish(runId, opts.chatId, Date.now() - started));
|
|
66
|
-
await step(() => clearLease(opts.sessionId));
|
|
67
|
-
await writable.write("stage:done;");
|
|
68
|
-
await writable.close();
|
|
69
|
-
return {
|
|
70
|
-
ok: true, runId, ticks: opts.ticks,
|
|
71
|
-
leaseValue: claimed ? lease : null,
|
|
72
|
-
chunks: 3 + opts.ticks, persisted, fatalCaught,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
/* ---------- runner ---------- */
|
|
76
|
-
async function main() {
|
|
77
|
-
const store = await createPostgresStore(URL);
|
|
78
|
-
const engine = new Engine(store, { pollMs: 300 });
|
|
79
|
-
const { registerWorkflow } = await import("../src/index.js");
|
|
80
|
-
registerWorkflow("entry-parity", parityWorkflow);
|
|
81
|
-
void engine.startWorker((e) => console.error("[worker]", e));
|
|
82
|
-
const mode = process.argv[2] ?? "run";
|
|
83
|
-
const ticks = Number(process.argv[3] ?? 3);
|
|
84
|
-
if (mode === "run") {
|
|
85
|
-
const { runId } = await engine.start("entry-parity", [
|
|
86
|
-
{ sessionId: "selftest-session", chatId: "selftest-chat", ticks },
|
|
87
|
-
]);
|
|
88
|
-
console.log("STARTED", runId);
|
|
89
|
-
const run = await engine.getRun(runId);
|
|
90
|
-
const chunks = [];
|
|
91
|
-
const reader = run.getReadable().getReader();
|
|
92
|
-
const dec = new TextDecoder();
|
|
93
|
-
try {
|
|
94
|
-
for (;;) {
|
|
95
|
-
const { done, value } = await reader.read();
|
|
96
|
-
if (done)
|
|
97
|
-
break;
|
|
98
|
-
chunks.push(dec.decode(value));
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
catch { /* closed */ }
|
|
102
|
-
const rv = await run.returnValue;
|
|
103
|
-
console.log("CHUNKS", JSON.stringify(chunks));
|
|
104
|
-
console.log("RESULT", JSON.stringify(rv));
|
|
105
|
-
process.exit(0);
|
|
106
|
-
}
|
|
107
|
-
if (mode === "resume") {
|
|
108
|
-
const runId = process.argv[4];
|
|
109
|
-
await engine.resume(runId);
|
|
110
|
-
const row = await store.getRun(runId);
|
|
111
|
-
console.log("RESUMED", runId, JSON.stringify(row));
|
|
112
|
-
process.exit(0);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
void main().catch((e) => { console.error(e); process.exit(1); });
|
package/dist/test/race.js
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Concurrency stress: each run is resumed by 20 concurrent resume() calls.
|
|
3
|
-
* Contract: exactly one step_completed, zero step_failed per run — racing
|
|
4
|
-
* replays must never re-execute a memoized step (run-lease arbitration).
|
|
5
|
-
*/
|
|
6
|
-
import { test } from "node:test";
|
|
7
|
-
import assert from "node:assert/strict";
|
|
8
|
-
import pg from "pg";
|
|
9
|
-
import { Engine, step, registerWorkflow } from "../src/index.js";
|
|
10
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
11
|
-
const PG_URL = process.env.LIGHTFLOW_PG_URL;
|
|
12
|
-
if (!PG_URL)
|
|
13
|
-
throw new Error("LIGHTFLOW_PG_URL is required");
|
|
14
|
-
test("racing resumes never re-execute a memoized step", async () => {
|
|
15
|
-
const store = await createPostgresStore(PG_URL);
|
|
16
|
-
const pool = new pg.Pool({ connectionString: PG_URL });
|
|
17
|
-
registerWorkflow("race-stress", async () => {
|
|
18
|
-
"use workflow";
|
|
19
|
-
return await step(async () => {
|
|
20
|
-
"use step";
|
|
21
|
-
await new Promise((r) => setTimeout(r, 5));
|
|
22
|
-
return "x";
|
|
23
|
-
});
|
|
24
|
-
});
|
|
25
|
-
const engine = new Engine(store);
|
|
26
|
-
const RUNS = 10, RACERS = 20;
|
|
27
|
-
for (let i = 0; i < RUNS; i++) {
|
|
28
|
-
const { runId } = await engine.start("race-stress", []);
|
|
29
|
-
await Promise.all(Array.from({ length: RACERS }, () => engine.resume(runId).catch(() => { })));
|
|
30
|
-
// wait for terminal state
|
|
31
|
-
for (let w = 0; w < 200; w++) {
|
|
32
|
-
const row = await store.getRun(runId);
|
|
33
|
-
if (row && row.status !== "running")
|
|
34
|
-
break;
|
|
35
|
-
await new Promise((r) => setTimeout(r, 25));
|
|
36
|
-
}
|
|
37
|
-
const sc = Number((await pool.query("SELECT count(*) c FROM lightflow_events WHERE run_id=$1 AND type='step_completed'", [runId])).rows[0].c);
|
|
38
|
-
const sf = Number((await pool.query("SELECT count(*) c FROM lightflow_events WHERE run_id=$1 AND type='step_failed'", [runId])).rows[0].c);
|
|
39
|
-
assert.equal(sc, 1, `run ${runId}: expected 1 step_completed, got ${sc}`);
|
|
40
|
-
assert.equal(sf, 0, `run ${runId}: expected 0 step_failed, got ${sf}`);
|
|
41
|
-
}
|
|
42
|
-
await pool.end();
|
|
43
|
-
});
|
package/dist/test/resume-all.js
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { Engine } from "../src/index.js";
|
|
2
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
3
|
-
import { registerAll } from "./workflow-def.js";
|
|
4
|
-
import pg from "pg";
|
|
5
|
-
registerAll();
|
|
6
|
-
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
7
|
-
const engine = new Engine(store, { pollMs: 200, staleRunMs: 15_000 });
|
|
8
|
-
const p = new pg.Pool({ connectionString: process.env.LIGHTFLOW_PG_URL });
|
|
9
|
-
const rows = await p.query("SELECT run_id FROM lightflow_runs WHERE status='running'");
|
|
10
|
-
console.log("RESUMING", rows.rows.length);
|
|
11
|
-
await Promise.all(rows.rows.map((r) => engine.resume(r.run_id).catch((e) => console.error("ERR", e.message))));
|
|
12
|
-
const done = await p.query("SELECT status,count(*) FROM lightflow_runs GROUP BY status");
|
|
13
|
-
console.log("FINAL", JSON.stringify(done.rows));
|
|
14
|
-
process.exit(0);
|
package/dist/test/resume.js
DELETED
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
import { Engine } from "../src/index.js";
|
|
2
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
3
|
-
const { registerWorkflow } = await import("../src/index.js");
|
|
4
|
-
const { parityWorkflow, registerAll } = await import("./workflow-def.js");
|
|
5
|
-
registerAll();
|
|
6
|
-
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
7
|
-
const engine = new Engine(store, { pollMs: 300, staleRunMs: 15000 });
|
|
8
|
-
const runId = process.argv[2];
|
|
9
|
-
await engine.resume(runId);
|
|
10
|
-
const row = await store.getRun(runId);
|
|
11
|
-
console.log("FINAL", row?.status, JSON.stringify(row?.output));
|
|
12
|
-
process.exit(0);
|
package/dist/test/simulate.js
DELETED
|
@@ -1,46 +0,0 @@
|
|
|
1
|
-
import { Engine } from "../src/index.js";
|
|
2
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
3
|
-
import { registerAll } from "./workflow-def.js";
|
|
4
|
-
registerAll();
|
|
5
|
-
const users = Number(process.argv[2] ?? 10);
|
|
6
|
-
const chats = Number(process.argv[3] ?? 3);
|
|
7
|
-
const ticks = Number(process.argv[4] ?? 6);
|
|
8
|
-
const store = await createPostgresStore(process.env.LIGHTFLOW_PG_URL);
|
|
9
|
-
const engine = new Engine(store, { pollMs: 200, staleRunMs: 20_000 });
|
|
10
|
-
void engine.startWorker(() => { });
|
|
11
|
-
const ids = [];
|
|
12
|
-
const t0 = Date.now();
|
|
13
|
-
for (let u = 0; u < users; u += 1) {
|
|
14
|
-
for (let c = 0; c < chats; c += 1) {
|
|
15
|
-
const { runId } = await engine.start("entry-parity", [
|
|
16
|
-
{ sessionId: "selftest-session", chatId: `sim-${u}-chat-${c}`, ticks },
|
|
17
|
-
]);
|
|
18
|
-
ids.push(runId);
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
console.log("STARTED", ids.length, "in", Date.now() - t0, "ms");
|
|
22
|
-
// poll to terminal state
|
|
23
|
-
const deadline = Date.now() + 20 * 60_000;
|
|
24
|
-
let done = 0;
|
|
25
|
-
while (Date.now() < deadline) {
|
|
26
|
-
let completed = 0, running = 0, failed = 0;
|
|
27
|
-
for (const id of ids) {
|
|
28
|
-
const r = await store.getRun(id);
|
|
29
|
-
if (!r)
|
|
30
|
-
continue;
|
|
31
|
-
if (r.status === "completed")
|
|
32
|
-
completed += 1;
|
|
33
|
-
else if (r.status === "running")
|
|
34
|
-
running += 1;
|
|
35
|
-
else
|
|
36
|
-
failed += 1;
|
|
37
|
-
}
|
|
38
|
-
done = completed;
|
|
39
|
-
console.log(`t+${Math.round((Date.now() - t0) / 1000)}s completed=${completed} running=${running} failed=${failed}`);
|
|
40
|
-
if (completed + failed === ids.length)
|
|
41
|
-
break;
|
|
42
|
-
await new Promise((r) => setTimeout(r, 10_000));
|
|
43
|
-
void running;
|
|
44
|
-
}
|
|
45
|
-
console.log("DONE", done, "of", ids.length);
|
|
46
|
-
process.exit(0);
|
package/dist/test/smoke.js
DELETED
|
@@ -1,157 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* CI smoke suite — node:test, runs against a real Postgres (see ci.yml).
|
|
3
|
-
* Uses the real engine API: plain async fns marked "use step" / "use workflow",
|
|
4
|
-
* driven through Engine + createPostgresStore (same pattern as test/entry-parity.ts).
|
|
5
|
-
*
|
|
6
|
-
* The kill-recovery harnesses (simulate/drain/resume/resume-all) stay as
|
|
7
|
-
* manual scripts — SIGKILL harnesses can't run inside node:test.
|
|
8
|
-
*/
|
|
9
|
-
import { test } from "node:test";
|
|
10
|
-
import assert from "node:assert/strict";
|
|
11
|
-
import pg from "pg";
|
|
12
|
-
import { Engine, step, sleep, getWritable, FatalError, getWorkflowMetadata, registerWorkflow } from "../src/index.js";
|
|
13
|
-
import { createPostgresStore } from "../src/pg-store.js";
|
|
14
|
-
const PG_URL = process.env.LIGHTFLOW_PG_URL;
|
|
15
|
-
if (!PG_URL)
|
|
16
|
-
throw new Error("LIGHTFLOW_PG_URL is required");
|
|
17
|
-
async function freshStore() {
|
|
18
|
-
return createPostgresStore(PG_URL);
|
|
19
|
-
}
|
|
20
|
-
async function resetTables() {
|
|
21
|
-
// Ensure schema exists first (fresh CI database), then clear it.
|
|
22
|
-
await createPostgresStore(PG_URL);
|
|
23
|
-
const pool = new pg.Pool({ connectionString: PG_URL });
|
|
24
|
-
await pool.query("TRUNCATE lightflow_events, lightflow_runs");
|
|
25
|
-
await pool.end();
|
|
26
|
-
}
|
|
27
|
-
/** Poll until the run leaves "running" (or timeout). */
|
|
28
|
-
async function awaitTerminal(store, runId, timeoutMs = 10_000) {
|
|
29
|
-
const deadline = Date.now() + timeoutMs;
|
|
30
|
-
let run = await store.getRun(runId);
|
|
31
|
-
while (run?.status === "running" && Date.now() < deadline) {
|
|
32
|
-
await new Promise((r) => setTimeout(r, 100));
|
|
33
|
-
run = await store.getRun(runId);
|
|
34
|
-
}
|
|
35
|
-
return run;
|
|
36
|
-
}
|
|
37
|
-
test("step memoizes — the side effect executes exactly once", async () => {
|
|
38
|
-
await resetTables();
|
|
39
|
-
const store = await freshStore();
|
|
40
|
-
let calls = 0;
|
|
41
|
-
registerWorkflow("smoke-memo", async () => {
|
|
42
|
-
"use workflow";
|
|
43
|
-
const memoized = async () => {
|
|
44
|
-
"use step";
|
|
45
|
-
calls += 1;
|
|
46
|
-
return `call-${calls}`;
|
|
47
|
-
};
|
|
48
|
-
await step(memoized);
|
|
49
|
-
return { calls };
|
|
50
|
-
});
|
|
51
|
-
const engine = new Engine(store, { pollMs: 100 });
|
|
52
|
-
const workerPromise = engine.startWorker();
|
|
53
|
-
try {
|
|
54
|
-
const { runId } = await engine.start("smoke-memo", []);
|
|
55
|
-
const run = await awaitTerminal(store, runId);
|
|
56
|
-
assert.equal(run?.status, "completed");
|
|
57
|
-
assert.equal(run?.output?.calls, 1);
|
|
58
|
-
}
|
|
59
|
-
finally {
|
|
60
|
-
engine.stopWorker();
|
|
61
|
-
await workerPromise;
|
|
62
|
-
}
|
|
63
|
-
});
|
|
64
|
-
test("FatalError is caught as fatal inside the workflow", async () => {
|
|
65
|
-
await resetTables();
|
|
66
|
-
const store = await freshStore();
|
|
67
|
-
registerWorkflow("smoke-fatal", async () => {
|
|
68
|
-
"use workflow";
|
|
69
|
-
const bomb = async () => {
|
|
70
|
-
"use step";
|
|
71
|
-
throw new FatalError("hard stop");
|
|
72
|
-
};
|
|
73
|
-
try {
|
|
74
|
-
await step(bomb);
|
|
75
|
-
return { fatalCaught: false };
|
|
76
|
-
}
|
|
77
|
-
catch (e) {
|
|
78
|
-
return { fatalCaught: e instanceof FatalError };
|
|
79
|
-
}
|
|
80
|
-
});
|
|
81
|
-
const engine = new Engine(store, { pollMs: 100 });
|
|
82
|
-
const workerPromise = engine.startWorker();
|
|
83
|
-
try {
|
|
84
|
-
const { runId } = await engine.start("smoke-fatal", []);
|
|
85
|
-
const run = await awaitTerminal(store, runId);
|
|
86
|
-
assert.equal(run?.status, "completed");
|
|
87
|
-
assert.equal(run?.output?.fatalCaught, true);
|
|
88
|
-
}
|
|
89
|
-
finally {
|
|
90
|
-
engine.stopWorker();
|
|
91
|
-
await workerPromise;
|
|
92
|
-
}
|
|
93
|
-
});
|
|
94
|
-
test("durable sleep resumes and completes via the worker loop", async () => {
|
|
95
|
-
await resetTables();
|
|
96
|
-
const store = await freshStore();
|
|
97
|
-
registerWorkflow("smoke-sleep", async () => {
|
|
98
|
-
"use workflow";
|
|
99
|
-
await sleep(100);
|
|
100
|
-
return "after-sleep";
|
|
101
|
-
});
|
|
102
|
-
const engine = new Engine(store, { pollMs: 50 });
|
|
103
|
-
const workerPromise = engine.startWorker();
|
|
104
|
-
try {
|
|
105
|
-
const { runId } = await engine.start("smoke-sleep", []);
|
|
106
|
-
const run = await awaitTerminal(store, runId);
|
|
107
|
-
assert.equal(run?.status, "completed");
|
|
108
|
-
assert.equal(run?.output, "after-sleep");
|
|
109
|
-
}
|
|
110
|
-
finally {
|
|
111
|
-
engine.stopWorker();
|
|
112
|
-
await workerPromise;
|
|
113
|
-
}
|
|
114
|
-
});
|
|
115
|
-
test("streaming chunks are ordered and replayable", async () => {
|
|
116
|
-
await resetTables();
|
|
117
|
-
const store = await freshStore();
|
|
118
|
-
registerWorkflow("smoke-stream", async () => {
|
|
119
|
-
"use workflow";
|
|
120
|
-
const writable = getWritable();
|
|
121
|
-
for (let i = 0; i < 3; i += 1) {
|
|
122
|
-
await writable.write(`chunk-${i}`);
|
|
123
|
-
await sleep(50);
|
|
124
|
-
}
|
|
125
|
-
await writable.close();
|
|
126
|
-
return 3;
|
|
127
|
-
});
|
|
128
|
-
const engine = new Engine(store, { pollMs: 50 });
|
|
129
|
-
const workerPromise = engine.startWorker();
|
|
130
|
-
try {
|
|
131
|
-
const { runId } = await engine.start("smoke-stream", []);
|
|
132
|
-
const run = await awaitTerminal(store, runId);
|
|
133
|
-
assert.equal(run?.status, "completed");
|
|
134
|
-
const pool = new pg.Pool({ connectionString: PG_URL });
|
|
135
|
-
const rows = await pool.query(`SELECT payload->>'value' AS val FROM lightflow_events
|
|
136
|
-
WHERE run_id=$1 AND type='chunk' AND (payload->>'done') IS NULL
|
|
137
|
-
ORDER BY (payload->>'index')::int`, [runId]);
|
|
138
|
-
await pool.end();
|
|
139
|
-
assert.deepEqual(rows.rows.map((r) => r.val), ["chunk-0", "chunk-1", "chunk-2"]);
|
|
140
|
-
}
|
|
141
|
-
finally {
|
|
142
|
-
engine.stopWorker();
|
|
143
|
-
await workerPromise;
|
|
144
|
-
}
|
|
145
|
-
});
|
|
146
|
-
test("runId is unique per start", async () => {
|
|
147
|
-
await resetTables();
|
|
148
|
-
const store = await freshStore();
|
|
149
|
-
registerWorkflow("smoke-ids", async () => {
|
|
150
|
-
"use workflow";
|
|
151
|
-
return getWorkflowMetadata().runId;
|
|
152
|
-
});
|
|
153
|
-
const engine = new Engine(store, { pollMs: 100 });
|
|
154
|
-
const a = await engine.start("smoke-ids", []);
|
|
155
|
-
const b = await engine.start("smoke-ids", []);
|
|
156
|
-
assert.notEqual(a.runId, b.runId);
|
|
157
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { registerWorkflow } from "../src/index.js";
|
|
2
|
-
export async function parityWorkflow(opts) {
|
|
3
|
-
"use workflow";
|
|
4
|
-
const { step, sleep, getWritable, FatalError, getWorkflowMetadata } = await import("../src/index.js");
|
|
5
|
-
const { runId } = getWorkflowMetadata();
|
|
6
|
-
const writable = getWritable();
|
|
7
|
-
const started = Date.now();
|
|
8
|
-
let fatalCaught = false;
|
|
9
|
-
await writable.write("stage:start;");
|
|
10
|
-
try {
|
|
11
|
-
await step(async () => { throw new Error("transient"); });
|
|
12
|
-
}
|
|
13
|
-
catch {
|
|
14
|
-
fatalCaught = true;
|
|
15
|
-
}
|
|
16
|
-
const lease = `lrun-${runId}`;
|
|
17
|
-
const claimed = await step(async () => {
|
|
18
|
-
const pg = (await import("pg")).default;
|
|
19
|
-
const pool = new pg.Pool({ connectionString: process.env.APP_PG_URL, max: 4 });
|
|
20
|
-
const r = await pool.query(`UPDATE sessions SET lifecycle_run_id=$2 WHERE id=$1 AND lifecycle_run_id IS NULL`, [opts.sessionId, lease]);
|
|
21
|
-
await pool.end();
|
|
22
|
-
return (r.rowCount ?? 0) > 0;
|
|
23
|
-
});
|
|
24
|
-
await writable.write(`stage:lease:${claimed};`);
|
|
25
|
-
for (let i = 0; i < opts.ticks; i += 1) {
|
|
26
|
-
await sleep(new Date(Date.now() + 1000));
|
|
27
|
-
await writable.write(`stage:tick:${i + 1};`);
|
|
28
|
-
}
|
|
29
|
-
const persisted = await step(async () => {
|
|
30
|
-
const pg = (await import("pg")).default;
|
|
31
|
-
const pool = new pg.Pool({ connectionString: process.env.APP_PG_URL, max: 4 });
|
|
32
|
-
await pool.query(`INSERT INTO workflow_runs (id, chat_id, session_id, user_id, status, started_at, finished_at, total_duration_ms)
|
|
33
|
-
VALUES ($1,$2,'selftest-session','selftest-user','completed',now(),now(),$3)
|
|
34
|
-
ON CONFLICT (id) DO NOTHING`, [runId, opts.chatId, Math.round(Date.now() - started)]);
|
|
35
|
-
await pool.end();
|
|
36
|
-
return true;
|
|
37
|
-
});
|
|
38
|
-
await step(async () => {
|
|
39
|
-
const pg = (await import("pg")).default;
|
|
40
|
-
const pool = new pg.Pool({ connectionString: process.env.APP_PG_URL, max: 4 });
|
|
41
|
-
await pool.query(`UPDATE sessions SET lifecycle_run_id=NULL WHERE id=$1`, [opts.sessionId]);
|
|
42
|
-
await pool.end();
|
|
43
|
-
});
|
|
44
|
-
await writable.write("stage:done;");
|
|
45
|
-
await writable.close();
|
|
46
|
-
return {
|
|
47
|
-
ok: true, runId, ticks: opts.ticks,
|
|
48
|
-
leaseValue: claimed ? lease : null,
|
|
49
|
-
persisted, fatalCaught,
|
|
50
|
-
};
|
|
51
|
-
}
|
|
52
|
-
export function registerAll() {
|
|
53
|
-
registerWorkflow("entry-parity", parityWorkflow);
|
|
54
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|