lightflow-engine 0.2.6 → 0.2.8

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/BENCHMARKS.md CHANGED
@@ -141,18 +141,59 @@ sequential-ish workflows through `start()` + terminal-poll.
141
141
  Every version's table above was produced on the same machine and database,
142
142
  so cross-version rows are directly comparable.
143
143
 
144
- ## v0.2.0 — compat layer (perf-neutral)
144
+ ## v0.2.x — compat layer (all versions re-benchmarked)
145
145
 
146
- Focus: Vercel Workflow drop-in compat (`lightflow-engine/compat/*`).
147
- No perf regression vs v0.1.4 — two real bugs found and fixed along the way:
148
- post-resume chunk loss (chunk keys now include the durable-timer position)
149
- and chunks vanishing from streams after snapshot compaction (live chunks
150
- now fold into snapshots).
146
+ Each published version was benchmarked from its published tarball against the
147
+ same Postgres (16, local) and the same harness (200 runs x 5 steps @ conc 20;
148
+ 50 runs x 400 steps). Steps/wf = 5 for short, 400 for long.
151
149
 
152
- | Scenario | v0.1.4 | v0.2.0 |
153
- |---|---|---|
154
- | 200 runs x 5 steps, conc 20 (runs/s) | 502–512 | 504.5 |
155
- | 50 runs x 400 steps durable (steps/s) | 9,939 | 9,062–10,600 |
150
+ | Version | Short runs/s | Short steps/s | Long steps/s |
151
+ |---|---|---|---|
152
+ | 0.2.0 | 462.7 | 2,313 | 8,493 |
153
+ | 0.2.1 | 466.0 | 2,330 | 9,062 |
154
+ | 0.2.2 | 466.9 | 2,335 | 8,348 |
155
+ | 0.2.3 | 461.6 | 2,308 | 8,384 |
156
+ | 0.2.4 | 456.1 | 2,280 | 8,280 |
157
+ | 0.2.5 | 459.1 | 2,295 | 9,068 |
158
+ | 0.2.6 | 494.9–525.1 | 2,474–2,626 | 8,379–9,737 |
159
+
160
+ ### What changed per version (all perf-neutral by design)
161
+
162
+ - **0.2.0** — Vercel Workflow drop-in compat (`compat/workflow`,
163
+ `compat/api`, `compat/next`). Two data-loss bugs fixed en route: chunk
164
+ memo keys now include the durable-timer position (`w:<sleepCalls>:<writes>`)
165
+ so post-resume writes can't be skipped against pre-timer chunks; live chunk
166
+ appends now fold into snapshot compaction (chunks previously vanished from
167
+ `getReadable` after ~200 steps).
168
+ - **0.2.1** — `run.status` is a live getter (fresh promise per access).
169
+ entry-agents' startStopMonitor re-awaits it in a 150ms poll loop; a single
170
+ memoized promise left the monitor blocked until terminal.
171
+ - **0.2.2** — `makeStep(fn, {retries})`: durable wrapper for arg-taking
172
+ step functions (entry's "use step" style).
173
+ - **0.2.3–0.2.4** — packaging: explicit `types` in subpath exports,
174
+ declaration files emitted. No runtime change.
175
+ - **0.2.5** — `rootDir: "src"` so the published `dist/` layout actually
176
+ matches the subpath export map (0.2.0–0.2.4 shipped `dist/src/...` while
177
+ exports claimed `dist/...` — subpath imports still worked because Node
178
+ resolved the package root, but the types were broken).
179
+ - **0.2.6** — `export { workflowFetch as fetch }` (entry imports both
180
+ names), `WorkflowFn` accepts typed args, `run.exists: true`.
181
+
182
+ ### Spread notes
183
+
184
+ Short-run spread across versions is ~2% (456–467 runs/s) — within run-to-run
185
+ noise; the compat layer added no measurable overhead. 0.2.6 shows the best
186
+ short-run numbers (494–525 runs/s across repeated runs) and the long-run
187
+ 8.3k–9.7k steps/s band matches the v0.1.4-era 9.9k within noise.
188
+
189
+ ## Reproducing
156
190
 
157
- Verdict: perf-neutral within noise. Compat test (7/7) exercises the full
158
- entry-agents surface end-to-end.
191
+ Each version's tarball can be re-benchmarked independently:
192
+
193
+ ```bash
194
+ npm pack lightflow-engine@<version>
195
+ tar xzf lightflow-engine-<version>.tgz -C <dir> --strip-components=1
196
+ ln -s <path-to>/lightflow/node_modules <dir>/node_modules
197
+ PKG=<dir> LIGHTFLOW_PG_URL=postgres://... BENCH_N=200 RN=5 npx tsx bench-pkg.mts
198
+ PKG=<dir> LIGHTFLOW_PG_URL=postgres://... BENCH_N=50 RN=400 npx tsx bench-pkg.mts
199
+ ```
@@ -12,18 +12,19 @@
12
12
  */
13
13
  import { Engine, registerWorkflow } from "../index.js";
14
14
  import { workflowIdFor } from "./workflow.js";
15
- let sharedEngine = null;
16
- let sharedStore = null;
15
+ const g = globalThis;
16
+ if (!g.__lightflowCompat)
17
+ g.__lightflowCompat = { engine: null, store: null };
17
18
  /** Configure the compat layer with a store/engine (call once at boot). */
18
19
  export function initWorkflowApi(store, engine) {
19
- sharedStore = store;
20
- sharedEngine = engine ?? new Engine(store);
20
+ g.__lightflowCompat.store = store;
21
+ g.__lightflowCompat.engine = engine ?? new Engine(store);
21
22
  }
22
23
  export function getEngine() {
23
- if (!sharedEngine) {
24
+ if (!g.__lightflowCompat.engine) {
24
25
  throw new Error("workflow/api not initialized — call initWorkflowApi(store) at startup");
25
26
  }
26
- return sharedEngine;
27
+ return g.__lightflowCompat.engine;
27
28
  }
28
29
  /**
29
30
  * Register a workflow function under a stable id and start it. Mirrors
@@ -31,7 +32,7 @@ export function getEngine() {
31
32
  */
32
33
  function makeRunHandle(runId) {
33
34
  const engine = getEngine();
34
- const store = sharedStore;
35
+ const store = g.__lightflowCompat.store;
35
36
  /** One DB read -> Vercel status vocabulary. */
36
37
  const readStatus = async () => {
37
38
  const row = await store.getRun(runId);
@@ -65,6 +66,8 @@ function makeRunHandle(runId) {
65
66
  return loop();
66
67
  };
67
68
  const cache = {};
69
+ let lazyReturnValue;
70
+ let pendingRejection;
68
71
  const handle = {
69
72
  runId,
70
73
  exists: true,
@@ -76,10 +79,26 @@ function makeRunHandle(runId) {
76
79
  return st;
77
80
  });
78
81
  },
79
- returnValue: (async () => {
80
- await terminal(cache);
81
- return (await engine.getRun(runId)).returnValue;
82
- })(),
82
+ // Lazy: created on first access so a handle whose returnValue is never
83
+ // awaited (e.g. status-only getRun polls) can't produce an unhandled
84
+ // rejection when the run fails.
85
+ get returnValue() {
86
+ if (!lazyReturnValue) {
87
+ lazyReturnValue = (async () => {
88
+ try {
89
+ await terminal(cache);
90
+ return await (await engine.getRun(runId)).returnValue;
91
+ }
92
+ catch (e) {
93
+ pendingRejection = e;
94
+ throw e;
95
+ }
96
+ })();
97
+ if (pendingRejection)
98
+ throw pendingRejection; // unreachable
99
+ }
100
+ return lazyReturnValue;
101
+ },
83
102
  getReadable(opts) {
84
103
  const startIndex = opts?.startIndex ?? 0;
85
104
  let sent = startIndex;
package/dist/index.js CHANGED
@@ -26,8 +26,13 @@ export class FatalError extends Error {
26
26
  this.name = "FatalError";
27
27
  }
28
28
  }
29
- const workflows = new Map();
30
- const steps = new Map();
29
+ const reg = globalThis;
30
+ if (!reg.__lightflowWorkflows)
31
+ reg.__lightflowWorkflows = new Map();
32
+ if (!reg.__lightflowSteps)
33
+ reg.__lightflowSteps = new Map();
34
+ const workflows = reg.__lightflowWorkflows;
35
+ const steps = reg.__lightflowSteps;
31
36
  export function registerWorkflow(id, fn) {
32
37
  workflows.set(id, fn);
33
38
  }
@@ -229,6 +234,10 @@ export class Engine {
229
234
  let resolve;
230
235
  let reject;
231
236
  const promise = new Promise((res, rej) => { resolve = res; reject = rej; });
237
+ // Prevent unhandled-rejection crashes when a caller (e.g. compat
238
+ // returnValue path) never awaits the local deferred — status is read
239
+ // from the store instead.
240
+ promise.catch(() => { });
232
241
  const d = { promise, resolve, reject };
233
242
  this.local.set(runId, d);
234
243
  return d;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lightflow-engine",
3
- "version": "0.2.6",
3
+ "version": "0.2.8",
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",