lightflow-engine 0.2.0 → 0.2.2

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.
@@ -29,51 +29,56 @@ export function getEngine() {
29
29
  * Register a workflow function under a stable id and start it. Mirrors
30
30
  * Vercel's start(workflowFn, args).
31
31
  */
32
- export async function start(fn, args) {
33
- const engine = getEngine();
34
- const id = workflowIdFor(fn);
35
- registerWorkflow(id, fn);
36
- const { runId } = await engine.start(id, args);
37
- return makeRunHandle(runId);
38
- }
39
- export function getRun(runId) {
40
- return makeRunHandle(runId);
41
- }
42
32
  function makeRunHandle(runId) {
43
33
  const engine = getEngine();
44
34
  const store = sharedStore;
45
- let cachedStatus = null;
46
- const statusPromise = (async () => {
47
- if (cachedStatus)
48
- return cachedStatus;
35
+ /** One DB read -> Vercel status vocabulary. */
36
+ const readStatus = async () => {
37
+ const row = await store.getRun(runId);
38
+ if (!row)
39
+ return "pending";
40
+ if (row.status === "completed")
41
+ return "completed";
42
+ if (row.status === "failed") {
43
+ return row.output?.error === "cancelled"
44
+ ? "cancelled"
45
+ : "failed";
46
+ }
47
+ return "running";
48
+ };
49
+ /** Promise resolving when the run reaches a terminal state. */
50
+ const terminal = (cache) => {
49
51
  const deadline = Date.now() + 10 * 60_000;
50
- let delay = 5;
51
- let seenRow = false;
52
- while (Date.now() < deadline) {
53
- const row = await store.getRun(runId);
54
- if (row) {
55
- seenRow = true;
56
- if (row.status === "completed") {
57
- cachedStatus = "completed";
58
- return cachedStatus;
59
- }
60
- if (row.status === "failed") {
61
- // cancelled runs are 'failed' with error 'cancelled'
62
- cachedStatus =
63
- row.output?.error === "cancelled"
64
- ? "cancelled" : "failed";
65
- return cachedStatus;
52
+ let delay = 25;
53
+ const loop = async () => {
54
+ while (Date.now() < deadline) {
55
+ const st = await readStatus();
56
+ if (st === "completed" || st === "failed" || st === "cancelled") {
57
+ cache.status = st;
58
+ return st;
66
59
  }
60
+ await new Promise((r) => setTimeout(r, delay));
61
+ delay = Math.min(delay * 1.4, 250);
67
62
  }
68
- await new Promise((r) => setTimeout(r, delay));
69
- delay = seenRow ? Math.min(delay * 1.6, 250) : 5;
70
- }
71
- throw new Error(`run status timeout: ${runId}`);
72
- })();
63
+ throw new Error(`run status timeout: ${runId}`);
64
+ };
65
+ return loop();
66
+ };
67
+ const cache = {};
73
68
  const handle = {
74
69
  runId,
75
- status: statusPromise,
76
- returnValue: (async () => (await engine.getRun(runId)).returnValue)(),
70
+ get status() {
71
+ // Live snapshot: resolve immediately with the current state; the
72
+ // startStopMonitor poll loop relies on this re-reading each tick.
73
+ return readStatus().then((st) => {
74
+ cache.status = st;
75
+ return st;
76
+ });
77
+ },
78
+ returnValue: (async () => {
79
+ await terminal(cache);
80
+ return (await engine.getRun(runId)).returnValue;
81
+ })(),
77
82
  getReadable(opts) {
78
83
  const startIndex = opts?.startIndex ?? 0;
79
84
  let sent = startIndex;
@@ -109,19 +114,17 @@ function makeRunHandle(runId) {
109
114
  }
110
115
  const chunks = await collectChunks();
111
116
  const hasDone = chunks.some((c) => c.done);
112
- // chunks carry a monotonic `index`; entries after startIndex
113
117
  for (const c of chunks) {
114
- const p = c;
115
- if (p.done) {
118
+ if (c.done) {
116
119
  closed = true;
117
120
  controller.close();
118
121
  return;
119
122
  }
120
- if (p.index < startIndex)
123
+ if (c.index < startIndex)
121
124
  continue;
122
- if (p.index >= sent) {
123
- controller.enqueue(p.value);
124
- sent = p.index + 1;
125
+ if (c.index >= sent) {
126
+ controller.enqueue(c.value);
127
+ sent = c.index + 1;
125
128
  }
126
129
  }
127
130
  if (hasDone) {
@@ -155,3 +158,13 @@ function makeRunHandle(runId) {
155
158
  };
156
159
  return handle;
157
160
  }
161
+ export async function start(fn, args) {
162
+ const engine = getEngine();
163
+ const id = workflowIdFor(fn);
164
+ registerWorkflow(id, fn);
165
+ const { runId } = await engine.start(id, args);
166
+ return makeRunHandle(runId);
167
+ }
168
+ export function getRun(runId) {
169
+ return makeRunHandle(runId);
170
+ }
@@ -8,6 +8,7 @@
8
8
  * webpack loader (workflow/next) or explicit step() wrappers.
9
9
  */
10
10
  import { createHash } from "node:crypto";
11
+ import { step, registerStep, } from "../index.js";
11
12
  import { current } from "../index.js";
12
13
  export { FatalError, CancelledError, sleep } from "../index.js";
13
14
  export { getWorkflowMetadata } from "../compat/metadata.js";
@@ -69,3 +70,27 @@ export function workflowIdFor(fn) {
69
70
  return "wf_" + createHash("sha256")
70
71
  .update(String(fn).slice(0, 512)).digest("hex").slice(0, 16);
71
72
  }
73
+ /**
74
+ * makeStep — turns a plain async function into a durable step callable.
75
+ *
76
+ * Entry's "use step" functions take arguments; lightflow's step() takes a
77
+ * thunk. makeStep wraps both: the returned function is a drop-in for the
78
+ * original (same signature), but each call becomes a memoized, retrying,
79
+ * replayable step keyed by call position. Outside a workflow it degrades
80
+ * to a plain call, so tests and non-durable paths keep working.
81
+ */
82
+ export function makeStep(fn, opts) {
83
+ const wrapped = async (...args) => step(() => fn(...args));
84
+ Object.defineProperty(wrapped, "__lightflowStepId", {
85
+ value: `step_${workflowIdFor(fn)}`,
86
+ enumerable: false,
87
+ });
88
+ if (opts?.retries !== undefined) {
89
+ Object.defineProperty(wrapped, "__lightflowRetries", {
90
+ value: opts.retries,
91
+ enumerable: false,
92
+ });
93
+ }
94
+ registerStep(wrapped.__lightflowStepId, fn);
95
+ return wrapped;
96
+ }
@@ -52,7 +52,8 @@ test("compat: vercel-style agent chat workflow end-to-end", async () => {
52
52
  { messages: ["hello", "world"], chatId: "c1" },
53
53
  ]);
54
54
  assert.ok(run.runId.startsWith("lrun_"));
55
- // status is a Promise, resolvable to a terminal state
55
+ // status is a live getter; await terminal state via returnValue
56
+ await run.returnValue;
56
57
  const status = await run.status;
57
58
  assert.equal(status, "completed");
58
59
  const ret = (await run.returnValue);
@@ -79,7 +80,7 @@ test("compat: vercel-style agent chat workflow end-to-end", async () => {
79
80
  // cancel semantics: getRun + status + cancel()
80
81
  const dup = await start(runAgentWorkflow, [{ messages: ["x"], chatId: "c2" }]);
81
82
  await dup.cancel();
82
- assert.equal(await dup.status, "cancelled");
83
+ assert.equal(await dup.status, "cancelled"); // live getter reflects cancel
83
84
  await pool.end();
84
85
  engine.stopWorker();
85
86
  await workerPromise;
@@ -0,0 +1,23 @@
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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightflow-engine",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
4
4
  "description": "A tiny durable workflow engine for Node.js and Postgres \u2014 with a Vercel Workflow drop-in compat layer.",
5
5
  "license": "MIT",
6
6
  "type": "module",