@particle-academy/fancy-flow 0.45.0 → 0.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/{ConfigFieldRenderer-Dzy4f6rG.d.cts → ConfigFieldRenderer-Cyir8-B8.d.cts} +2 -2
  2. package/dist/{ConfigFieldRenderer-WqJr-td_.d.ts → ConfigFieldRenderer-iMkZj0VK.d.ts} +2 -2
  3. package/dist/{FlowViewer-BbbUJe2b.d.cts → FlowViewer-CY3_vHSg.d.cts} +1 -1
  4. package/dist/{FlowViewer-csuaTL5t.d.ts → FlowViewer-DGW6t-80.d.ts} +1 -1
  5. package/dist/{capabilities-Brn74URS.d.ts → capabilities-DMUhnyZD.d.ts} +1 -1
  6. package/dist/{capabilities-CYCeIx6I.d.cts → capabilities-Dmd26aAs.d.cts} +1 -1
  7. package/dist/chunk-BHM5HGXH.js +79 -0
  8. package/dist/chunk-BHM5HGXH.js.map +1 -0
  9. package/dist/{chunk-DHUFUDEX.js → chunk-HFUBVT7W.js} +3 -3
  10. package/dist/{chunk-DHUFUDEX.js.map → chunk-HFUBVT7W.js.map} +1 -1
  11. package/dist/{chunk-VLATLVGH.js → chunk-L43CWD63.js} +129 -5
  12. package/dist/chunk-L43CWD63.js.map +1 -0
  13. package/dist/{chunk-A6RFLGWV.js → chunk-MH4ZW4DJ.js} +3 -3
  14. package/dist/{chunk-A6RFLGWV.js.map → chunk-MH4ZW4DJ.js.map} +1 -1
  15. package/dist/{chunk-QZTGV3ZL.js → chunk-P5SYH6M6.js} +3 -3
  16. package/dist/{chunk-QZTGV3ZL.js.map → chunk-P5SYH6M6.js.map} +1 -1
  17. package/dist/{chunk-WBXGZVLB.js → chunk-RMU6YCIQ.js} +11 -5
  18. package/dist/chunk-RMU6YCIQ.js.map +1 -0
  19. package/dist/durable/index.d.cts +490 -0
  20. package/dist/durable/index.d.ts +490 -0
  21. package/dist/durable.cjs +927 -0
  22. package/dist/durable.cjs.map +1 -0
  23. package/dist/durable.js +552 -0
  24. package/dist/durable.js.map +1 -0
  25. package/dist/engine.cjs +128 -2
  26. package/dist/engine.cjs.map +1 -1
  27. package/dist/engine.d.cts +9 -7
  28. package/dist/engine.d.ts +9 -7
  29. package/dist/engine.js +4 -4
  30. package/dist/fields/react-fancy.d.cts +4 -3
  31. package/dist/fields/react-fancy.d.ts +4 -3
  32. package/dist/index.cjs +133 -3
  33. package/dist/index.cjs.map +1 -1
  34. package/dist/index.d.cts +12 -10
  35. package/dist/index.d.ts +12 -10
  36. package/dist/index.js +13 -88
  37. package/dist/index.js.map +1 -1
  38. package/dist/layout/index.d.cts +1 -1
  39. package/dist/layout/index.d.ts +1 -1
  40. package/dist/llm/prism.d.cts +2 -2
  41. package/dist/llm/prism.d.ts +2 -2
  42. package/dist/llm/vercel-ai.d.cts +2 -2
  43. package/dist/llm/vercel-ai.d.ts +2 -2
  44. package/dist/pause-9iT4tCEV.d.cts +96 -0
  45. package/dist/pause-9iT4tCEV.d.ts +96 -0
  46. package/dist/registry/index.d.cts +7 -6
  47. package/dist/registry/index.d.ts +7 -6
  48. package/dist/{registry-Budds73w.d.cts → registry-D_kyUZEf.d.cts} +1 -1
  49. package/dist/{registry-D0I20HJ3.d.ts → registry-LHB3MlR9.d.ts} +1 -1
  50. package/dist/registry.cjs +133 -3
  51. package/dist/registry.cjs.map +1 -1
  52. package/dist/registry.js +4 -4
  53. package/dist/{run-cohort-CV7EcwGG.d.ts → run-cohort-Cyeny2Uu.d.ts} +3 -34
  54. package/dist/{run-cohort-DitsaYgS.d.cts → run-cohort-DdPPowJ3.d.cts} +3 -34
  55. package/dist/run-flow-1KiXhy59.d.cts +55 -0
  56. package/dist/run-flow-CQvTDBYk.d.ts +55 -0
  57. package/dist/runtime/index.d.cts +5 -3
  58. package/dist/runtime/index.d.ts +5 -3
  59. package/dist/runtime.cjs +128 -2
  60. package/dist/runtime.cjs.map +1 -1
  61. package/dist/runtime.js +3 -3
  62. package/dist/schema/index.d.cts +1 -1
  63. package/dist/schema/index.d.ts +1 -1
  64. package/dist/screens.d.cts +2 -2
  65. package/dist/screens.d.ts +2 -2
  66. package/dist/screens.js +6 -6
  67. package/dist/{types-D6ZSKBly.d.ts → types--S2Kzx7w.d.cts} +3 -97
  68. package/dist/{types-nQOA8jpk.d.cts → types-ChkCRonF.d.ts} +3 -97
  69. package/dist/types-Jx1TwehV.d.cts +247 -0
  70. package/dist/types-Jx1TwehV.d.ts +247 -0
  71. package/dist/ux.d.cts +3 -2
  72. package/dist/ux.d.ts +3 -2
  73. package/package.json +12 -2
  74. package/dist/chunk-VLATLVGH.js.map +0 -1
  75. package/dist/chunk-WBXGZVLB.js.map +0 -1
  76. package/dist/types-CMSrWVYM.d.cts +0 -109
  77. package/dist/types-CMSrWVYM.d.ts +0 -109
@@ -0,0 +1,927 @@
1
+ 'use strict';
2
+
3
+ // src/durable/state.ts
4
+ var NodeRunStatus = {
5
+ CLAIMED: "claimed",
6
+ COMPLETED: "completed",
7
+ SKIPPED: "skipped",
8
+ FAILED: "failed",
9
+ PAUSED: "paused"
10
+ };
11
+ var SETTLED = [
12
+ NodeRunStatus.COMPLETED,
13
+ NodeRunStatus.SKIPPED,
14
+ NodeRunStatus.FAILED
15
+ ];
16
+ var InMemoryClaimStore = class {
17
+ constructor() {
18
+ this.runs = /* @__PURE__ */ new Map();
19
+ }
20
+ claim(runKey, nodeId, owner) {
21
+ const run = this.run(runKey);
22
+ const existing = run.get(nodeId);
23
+ if (!existing) {
24
+ run.set(nodeId, {
25
+ status: NodeRunStatus.CLAIMED,
26
+ ports: [],
27
+ owner,
28
+ attempts: 1,
29
+ firstAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
30
+ });
31
+ return true;
32
+ }
33
+ const ownClaim = (existing.status === NodeRunStatus.CLAIMED || existing.status === NodeRunStatus.PAUSED) && existing.owner === owner;
34
+ if (ownClaim) {
35
+ existing.attempts += 1;
36
+ existing.status = NodeRunStatus.CLAIMED;
37
+ return true;
38
+ }
39
+ return false;
40
+ }
41
+ state(runKey) {
42
+ const out = {};
43
+ for (const [nodeId, entry] of this.run(runKey)) out[nodeId] = { ...entry, ports: [...entry.ports] };
44
+ return out;
45
+ }
46
+ complete(runKey, nodeId, output, ports) {
47
+ const entry = this.entry(runKey, nodeId);
48
+ entry.status = NodeRunStatus.COMPLETED;
49
+ entry.output = output;
50
+ entry.ports = [...ports];
51
+ entry.error = null;
52
+ }
53
+ skip(runKey, nodeId) {
54
+ const entry = this.entry(runKey, nodeId);
55
+ entry.status = NodeRunStatus.SKIPPED;
56
+ entry.ports = [];
57
+ }
58
+ fail(runKey, nodeId, error) {
59
+ const entry = this.entry(runKey, nodeId);
60
+ entry.status = NodeRunStatus.FAILED;
61
+ entry.error = error;
62
+ entry.ports = [];
63
+ }
64
+ pause(runKey, nodeId, reason) {
65
+ const entry = this.entry(runKey, nodeId);
66
+ entry.status = NodeRunStatus.PAUSED;
67
+ entry.error = reason;
68
+ entry.ports = [];
69
+ }
70
+ /**
71
+ * Drop a paused node's claim so a recorded answer can re-run it.
72
+ *
73
+ * Not part of the interface: resuming a human gate is the host's decision and
74
+ * its storage's business. Provided here because the in-memory store is also
75
+ * what the tests resume through.
76
+ */
77
+ release(runKey, nodeId) {
78
+ this.run(runKey).delete(nodeId);
79
+ }
80
+ run(runKey) {
81
+ let run = this.runs.get(runKey);
82
+ if (!run) {
83
+ run = /* @__PURE__ */ new Map();
84
+ this.runs.set(runKey, run);
85
+ }
86
+ return run;
87
+ }
88
+ /** The row for one node, created as CLAIMED if a driver never claimed it. */
89
+ entry(runKey, nodeId) {
90
+ const run = this.run(runKey);
91
+ let entry = run.get(nodeId);
92
+ if (!entry) {
93
+ entry = {
94
+ status: NodeRunStatus.CLAIMED,
95
+ ports: [],
96
+ attempts: 1,
97
+ firstAttemptAt: (/* @__PURE__ */ new Date()).toISOString()
98
+ };
99
+ run.set(nodeId, entry);
100
+ }
101
+ return entry;
102
+ }
103
+ };
104
+ function isSettled(status) {
105
+ return status !== void 0 && SETTLED.includes(status);
106
+ }
107
+
108
+ // src/registry/registry.ts
109
+ var kinds = /* @__PURE__ */ new Map();
110
+ var aliases = /* @__PURE__ */ new Map();
111
+ var overrides = /* @__PURE__ */ new Map();
112
+ function resolveKindId(id) {
113
+ if (kinds.has(id)) return id;
114
+ const canonical = aliases.get(id);
115
+ return canonical && kinds.has(canonical) ? canonical : null;
116
+ }
117
+ function withOverride(kind) {
118
+ if (!kind) return null;
119
+ const patch = overrides.get(kind.name);
120
+ return patch ? { ...kind, ...patch } : kind;
121
+ }
122
+ function getNodeKind(name) {
123
+ const canonical = resolveKindId(name);
124
+ return canonical ? withOverride(kinds.get(canonical)) : null;
125
+ }
126
+ function kindIds(kind) {
127
+ return [kind.name, ...kind.aliases ?? []];
128
+ }
129
+
130
+ // src/durable/frontier.ts
131
+ function isNote(type) {
132
+ if (!type) return false;
133
+ if (type === "note") return true;
134
+ const kind = getNodeKind(type);
135
+ return kind ? kindIds(kind).includes("note") || kind.category === "annotation" : false;
136
+ }
137
+ var Frontier = {
138
+ compute(graph, state) {
139
+ const incoming = /* @__PURE__ */ new Map();
140
+ for (const edge of graph.edges) {
141
+ const list = incoming.get(edge.target) ?? [];
142
+ list.push(edge);
143
+ incoming.set(edge.target, list);
144
+ }
145
+ const settled = /* @__PURE__ */ new Map();
146
+ const held = /* @__PURE__ */ new Set();
147
+ for (const [nodeId, entry] of Object.entries(state)) {
148
+ if (entry.status === NodeRunStatus.COMPLETED) settled.set(nodeId, entry.ports);
149
+ else if (isSettled(entry.status)) settled.set(nodeId, []);
150
+ else held.add(nodeId);
151
+ }
152
+ const ready = [];
153
+ const skipped = [];
154
+ let changed = true;
155
+ while (changed) {
156
+ changed = false;
157
+ for (const node of graph.nodes) {
158
+ const nodeId = node.id;
159
+ if (settled.has(nodeId) || held.has(nodeId) || ready.includes(nodeId)) continue;
160
+ const edges = incoming.get(nodeId) ?? [];
161
+ let blocked = false;
162
+ let active = false;
163
+ for (const edge of edges) {
164
+ if (!settled.has(edge.source)) {
165
+ blocked = true;
166
+ break;
167
+ }
168
+ if (settled.get(edge.source).includes(edge.sourceHandle ?? "out")) active = true;
169
+ }
170
+ if (blocked) continue;
171
+ if (edges.length > 0 && !active) {
172
+ settled.set(nodeId, []);
173
+ skipped.push(nodeId);
174
+ changed = true;
175
+ continue;
176
+ }
177
+ if (isNote(node.type) || getNodeKind(node.type ?? "")?.category === "layout") {
178
+ settled.set(nodeId, []);
179
+ skipped.push(nodeId);
180
+ changed = true;
181
+ continue;
182
+ }
183
+ ready.push(nodeId);
184
+ }
185
+ }
186
+ return { ready, skipped };
187
+ },
188
+ /** Has every node settled? The run is finished when it has. */
189
+ isComplete(graph, state) {
190
+ return graph.nodes.every((node) => isSettled(state[node.id]?.status));
191
+ },
192
+ /**
193
+ * Is any node still held by a worker, or parked for a person?
194
+ *
195
+ * An empty frontier means something different depending on this: with work in
196
+ * flight the run is simply waiting, and whichever job finishes will advance
197
+ * it. With nothing in flight and nodes still unsettled, the graph cannot
198
+ * progress at all — which is a stuck run, and must be reported rather than
199
+ * waited on.
200
+ */
201
+ hasWorkInFlight(state) {
202
+ return Object.values(state).some(
203
+ (entry) => entry.status === NodeRunStatus.CLAIMED || entry.status === NodeRunStatus.PAUSED
204
+ );
205
+ },
206
+ /** Persist the skip cascade so the next pass does not recompute it. */
207
+ async settleSkips(store, runKey, skipped) {
208
+ for (const nodeId of skipped) await store.skip(runKey, nodeId);
209
+ }
210
+ };
211
+
212
+ // src/registry/ports.ts
213
+ function resolvePortSpec(spec, config) {
214
+ if (spec === void 0) return void 0;
215
+ if (typeof spec !== "function") return spec;
216
+ try {
217
+ const resolved = spec(config);
218
+ return Array.isArray(resolved) ? resolved : void 0;
219
+ } catch {
220
+ return void 0;
221
+ }
222
+ }
223
+ function nodeConfig(node) {
224
+ return node.data?.config ?? {};
225
+ }
226
+ function resolveNodePorts(node, kind) {
227
+ const config = nodeConfig(node);
228
+ const data = node.data;
229
+ return {
230
+ inputs: data?.inputs ?? resolvePortSpec(kind?.inputs, config),
231
+ outputs: data?.outputs ?? resolvePortSpec(kind?.outputs, config)
232
+ };
233
+ }
234
+
235
+ // src/runtime/run-identity.ts
236
+ function escapeSegment(value) {
237
+ return value.replace(/%/g, "%25").replace(/\//g, "%2F").replace(/#/g, "%23");
238
+ }
239
+ function renderSegment(value, occurrence) {
240
+ const escaped = escapeSegment(value);
241
+ return occurrence === void 0 || occurrence === null ? escaped : `${escaped}#${occurrence}`;
242
+ }
243
+ function instant(value) {
244
+ const ms = Date.parse(value);
245
+ if (Number.isNaN(ms)) {
246
+ throw new Error(
247
+ `RunIdentity: firstAttemptAt is not a parseable timestamp: ${JSON.stringify(value)}`
248
+ );
249
+ }
250
+ return ms;
251
+ }
252
+ var RunIdentity = class _RunIdentity {
253
+ constructor(runKey, path = [], attempt = 1, firstAttemptAt = (/* @__PURE__ */ new Date()).toISOString()) {
254
+ if (!runKey || runKey.trim() === "") {
255
+ throw new Error("RunIdentity: runKey must be a non-empty string.");
256
+ }
257
+ this.runKey = runKey;
258
+ this.path = Object.freeze([...path]);
259
+ this.attempt = Math.max(1, Math.trunc(attempt));
260
+ this.firstAttemptAt = firstAttemptAt;
261
+ Object.freeze(this);
262
+ }
263
+ /**
264
+ * The identity of one execution of one node — stable across retries of that
265
+ * execution, distinct from every other execution of the same node.
266
+ *
267
+ * Pass `occurrence` when an executor runs the same node more than once at the
268
+ * same level (a loop body, one item of a fan-out it drives itself).
269
+ */
270
+ stepKey(nodeId, occurrence) {
271
+ return `${this.runKey}:${[...this.path, renderSegment(nodeId, occurrence)].join("/")}`;
272
+ }
273
+ /**
274
+ * A child identity for work nested inside this step.
275
+ *
276
+ * `subflow` pushes the invoking node's id, so a node inside the child graph
277
+ * cannot collide with a same-named node in the parent. Attempt and
278
+ * `firstAttemptAt` are carried down unchanged: the nested work happens inside
279
+ * this step's attempt, and shares its clock.
280
+ */
281
+ descend(segment, occurrence) {
282
+ return new _RunIdentity(
283
+ this.runKey,
284
+ [...this.path, renderSegment(segment, occurrence)],
285
+ this.attempt,
286
+ this.firstAttemptAt
287
+ );
288
+ }
289
+ /** A copy on a different attempt, first-attempt clock preserved. */
290
+ withAttempt(attempt, firstAttemptAt) {
291
+ return new _RunIdentity(
292
+ this.runKey,
293
+ this.path,
294
+ attempt,
295
+ firstAttemptAt ?? this.firstAttemptAt
296
+ );
297
+ }
298
+ /**
299
+ * May this attempt reuse the step key and still be deduplicated?
300
+ *
301
+ * Providers forget idempotency keys — Stripe after 24 hours. Past that
302
+ * window, resending the key creates a second charge and sending a fresh one
303
+ * creates a second charge, so **the caller must refuse rather than pick
304
+ * between them**: a loud stuck run beats a silent double write.
305
+ *
306
+ * `true` on attempt 1 whatever the elapsed time — nothing was sent on an
307
+ * earlier attempt, so there is nothing for the provider to have forgotten.
308
+ * That is what lets a run park on a human gate for a week and then write.
309
+ *
310
+ * `windowSeconds: null` means the provider does not expire keys. `0` means
311
+ * it does not dedupe at all, so no retry may reuse a key — it is a real
312
+ * window, not an absent one, and the two must not be conflated: reading `0`
313
+ * as `null` turns "this provider does not dedupe" into "this provider
314
+ * dedupes forever", which is the more dangerous of the two by a distance.
315
+ */
316
+ isReplaySafe(windowSeconds, now = /* @__PURE__ */ new Date()) {
317
+ if (this.attempt <= 1) return true;
318
+ if (windowSeconds === null || windowSeconds === void 0) return true;
319
+ if (windowSeconds <= 0) return false;
320
+ const nowMs = typeof now === "string" ? instant(now) : now.getTime();
321
+ const elapsedSeconds = Math.max(0, (nowMs - instant(this.firstAttemptAt)) / 1e3);
322
+ return elapsedSeconds <= windowSeconds;
323
+ }
324
+ toJSON() {
325
+ return {
326
+ runKey: this.runKey,
327
+ path: [...this.path],
328
+ attempt: this.attempt,
329
+ firstAttemptAt: this.firstAttemptAt
330
+ };
331
+ }
332
+ /** Rebuild from a queue payload. */
333
+ static from(value) {
334
+ if (value instanceof _RunIdentity) return value;
335
+ if (typeof value === "string") return new _RunIdentity(value);
336
+ return new _RunIdentity(
337
+ value.runKey,
338
+ value.path ?? [],
339
+ value.attempt ?? 1,
340
+ value.firstAttemptAt ?? (/* @__PURE__ */ new Date()).toISOString()
341
+ );
342
+ }
343
+ };
344
+
345
+ // src/runtime/run-flow.ts
346
+ async function runFlow(graph, executors, onEvent = () => {
347
+ }, options = {}) {
348
+ const { signal, initialInputs = {}, timeoutMs, depth = 0, resumeOutputs = {} } = options;
349
+ const run = options.run === void 0 ? void 0 : RunIdentity.from(options.run);
350
+ const outputs = {};
351
+ const portValues = /* @__PURE__ */ new Map();
352
+ const completed = /* @__PURE__ */ new Set();
353
+ const errors = [];
354
+ const order = topoSort(graph);
355
+ if (order === null) {
356
+ const msg = "Cycle detected in flow graph \u2014 aborting.";
357
+ onEvent({ type: "run-error", error: msg });
358
+ return { ok: false, outputs, error: msg };
359
+ }
360
+ const incomingByNode = indexIncoming(graph.edges);
361
+ const timer = timeoutMs ? setTimeout(() => errors.push(`Run timed out after ${timeoutMs}ms`), timeoutMs) : null;
362
+ onEvent({ type: "run-start" });
363
+ try {
364
+ for (const node of order) {
365
+ if (signal?.aborted) throw new Error("aborted");
366
+ if (errors.length) break;
367
+ if (Object.prototype.hasOwnProperty.call(resumeOutputs, node.id)) {
368
+ const stored = resumeOutputs[node.id];
369
+ outputs[node.id] = stored;
370
+ const activated = activatedPorts(node, stored);
371
+ for (const portId of activated.ports) {
372
+ portValues.set(`${node.id}:${portId}`, activated.value);
373
+ onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
374
+ }
375
+ completed.add(node.id);
376
+ onEvent({ type: "node-status", nodeId: node.id, status: "done", text: "resumed" });
377
+ continue;
378
+ }
379
+ const incoming = incomingByNode.get(node.id) ?? [];
380
+ if (incoming.length > 0) {
381
+ const anyActive = incoming.some((e) => portValues.has(`${e.source}:${e.sourceHandle ?? "out"}`));
382
+ if (!anyActive) {
383
+ onEvent({ type: "node-status", nodeId: node.id, status: "idle", text: "skipped" });
384
+ continue;
385
+ }
386
+ }
387
+ const visualKind = getNodeKind(node.type ?? "");
388
+ const isLayout = visualKind?.category === "layout";
389
+ const isAnnotation = node.type === "note" || visualKind?.category === "annotation";
390
+ if (isLayout || isAnnotation) {
391
+ onEvent({
392
+ type: "node-status",
393
+ nodeId: node.id,
394
+ status: "idle",
395
+ text: isLayout ? "lane" : "annotation"
396
+ });
397
+ continue;
398
+ }
399
+ onEvent({ type: "node-status", nodeId: node.id, status: "running" });
400
+ const inputs = collectInputs(node, incoming, portValues, initialInputs);
401
+ const exec = pickExecutor(executors, node);
402
+ if (!exec) {
403
+ const msg = `No executor registered for kind=${node.type}`;
404
+ errors.push(msg);
405
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
406
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
407
+ break;
408
+ }
409
+ try {
410
+ const result = await Promise.resolve(
411
+ exec({
412
+ node,
413
+ inputs,
414
+ abort: (reason) => {
415
+ throw new Error(reason ?? "aborted");
416
+ },
417
+ emit: onEvent,
418
+ depth,
419
+ run
420
+ })
421
+ );
422
+ outputs[node.id] = result;
423
+ const activated = activatedPorts(node, result);
424
+ for (const portId of activated.ports) {
425
+ portValues.set(`${node.id}:${portId}`, activated.value);
426
+ onEvent({ type: "node-output", nodeId: node.id, portId, value: activated.value });
427
+ }
428
+ completed.add(node.id);
429
+ onEvent({ type: "node-status", nodeId: node.id, status: "done" });
430
+ } catch (e) {
431
+ const msg = e instanceof Error ? e.message : String(e);
432
+ errors.push(msg);
433
+ onEvent({ type: "node-status", nodeId: node.id, status: "error", text: msg });
434
+ onEvent({ type: "log", nodeId: node.id, level: "error", message: msg });
435
+ break;
436
+ }
437
+ }
438
+ } finally {
439
+ if (timer) clearTimeout(timer);
440
+ }
441
+ const ok = errors.length === 0;
442
+ onEvent({ type: "run-end", ok });
443
+ return ok ? { ok, outputs } : { ok, outputs, error: errors[0] };
444
+ }
445
+ function indexIncoming(edges) {
446
+ const map = /* @__PURE__ */ new Map();
447
+ for (const e of edges) {
448
+ const list = map.get(e.target) ?? [];
449
+ list.push(e);
450
+ map.set(e.target, list);
451
+ }
452
+ return map;
453
+ }
454
+ function topoSort(graph) {
455
+ const inDegree = /* @__PURE__ */ new Map();
456
+ for (const n of graph.nodes) inDegree.set(n.id, 0);
457
+ for (const e of graph.edges) inDegree.set(e.target, (inDegree.get(e.target) ?? 0) + 1);
458
+ const queue = [];
459
+ for (const [id, d] of inDegree) if (d === 0) queue.push(id);
460
+ const ordered = [];
461
+ while (queue.length) {
462
+ const id = queue.shift();
463
+ ordered.push(id);
464
+ for (const e of graph.edges) {
465
+ if (e.source !== id) continue;
466
+ const next = (inDegree.get(e.target) ?? 0) - 1;
467
+ inDegree.set(e.target, next);
468
+ if (next === 0) queue.push(e.target);
469
+ }
470
+ }
471
+ if (ordered.length !== graph.nodes.length) return null;
472
+ const byId = new Map(graph.nodes.map((n) => [n.id, n]));
473
+ return ordered.map((id) => byId.get(id)).filter(Boolean);
474
+ }
475
+ function collectInputs(node, incoming, portValues, initial) {
476
+ const inputs = { ...initial[node.id] ?? {} };
477
+ for (const e of incoming) {
478
+ const portId = e.targetHandle ?? "in";
479
+ const key = `${e.source}:${e.sourceHandle ?? "out"}`;
480
+ if (!portValues.has(key)) continue;
481
+ inputs[portId] = portValues.get(key);
482
+ }
483
+ return inputs;
484
+ }
485
+ function pickExecutor(executors, node) {
486
+ if (executors[node.id]) return executors[node.id];
487
+ if (node.type && executors[node.type]) return executors[node.type];
488
+ const kind = node.type ? getNodeKind(node.type) : null;
489
+ if (kind) {
490
+ for (const id of kindIds(kind)) {
491
+ if (executors[id]) return executors[id];
492
+ }
493
+ }
494
+ return executors["*"];
495
+ }
496
+ function activatedPorts(node, result) {
497
+ if (result && typeof result === "object") {
498
+ const r = result;
499
+ if (typeof r.__port === "string") {
500
+ return { ports: [r.__port], value: r.value };
501
+ }
502
+ if (typeof r.branch === "string") {
503
+ return { ports: [r.branch], value: r.value ?? r };
504
+ }
505
+ }
506
+ const kind = getNodeKind(node.data?.kind ?? node.type ?? "") ?? void 0;
507
+ const declared = resolveNodePorts(node, kind).outputs?.map((p) => p.id);
508
+ return { ports: declared?.length ? declared : ["out"], value: result };
509
+ }
510
+
511
+ // src/durable/replay.ts
512
+ var BOUNDARY = "fancy-flow:node-boundary";
513
+ async function replayUpTo(graph, nodeId, executors, options = {}) {
514
+ const fenced = { ...executors };
515
+ for (const node of graph.nodes) {
516
+ if (node.id !== nodeId) {
517
+ fenced[node.id] = (ctx) => ctx.abort(BOUNDARY);
518
+ }
519
+ }
520
+ const ports = {};
521
+ const collect = (event) => {
522
+ var _a;
523
+ if (event.type === "node-output") {
524
+ (ports[_a = event.nodeId] ?? (ports[_a] = [])).push(event.portId);
525
+ }
526
+ options.onEvent?.(event);
527
+ };
528
+ const result = await runFlow(graph, fenced, collect, {
529
+ initialInputs: options.initialInputs ?? {},
530
+ resumeOutputs: options.resumeOutputs ?? {},
531
+ depth: options.depth ?? 0,
532
+ run: options.run
533
+ });
534
+ return {
535
+ result,
536
+ ports,
537
+ outputOf: (id) => result.outputs[id],
538
+ portsOf: (id) => ports[id] ?? []
539
+ };
540
+ }
541
+ function isBoundary(error) {
542
+ return error === BOUNDARY;
543
+ }
544
+
545
+ // src/durable/retry.ts
546
+ var UNSAFE_TO_REPLAY = "unsafe-to-replay";
547
+ var RetryPolicy = class _RetryPolicy {
548
+ constructor(options = {}) {
549
+ this.tries = options.tries ?? 1;
550
+ this.backoffSeconds = options.backoffSeconds ?? 0;
551
+ this.perKind = options.perKind ?? {};
552
+ }
553
+ triesFor(node) {
554
+ if (_RetryPolicy.isUnsafeToReplay(node)) return 1;
555
+ for (const id of idsFor(node)) {
556
+ if (id in this.perKind) return Math.max(1, Math.trunc(this.perKind[id]));
557
+ }
558
+ return Math.max(1, Math.trunc(this.tries));
559
+ }
560
+ backoffFor(node) {
561
+ if (_RetryPolicy.isUnsafeToReplay(node)) return 0;
562
+ return Math.max(0, this.backoffSeconds);
563
+ }
564
+ static isUnsafeToReplay(node) {
565
+ if (!node.type) return false;
566
+ return getNodeKind(node.type)?.sideEffects === UNSAFE_TO_REPLAY;
567
+ }
568
+ };
569
+ function idsFor(node) {
570
+ if (!node.type) return [];
571
+ const kind = getNodeKind(node.type);
572
+ const canonical = resolveKindId(node.type);
573
+ const ordered = [node.type, ...canonical ? [canonical] : [], ...kind ? kindIds(kind) : []];
574
+ return [...new Set(ordered)];
575
+ }
576
+
577
+ // src/registry/pause.ts
578
+ var PAUSE_PREFIX = "fancy-flow:pause:";
579
+ var LEGACY_PAUSE_PREFIXES = [
580
+ ["awaiting-approval:", "approval"],
581
+ ["awaiting-input:", "input"]
582
+ ];
583
+ function encodePause(signal) {
584
+ const { nodeId, awaiting, detail } = signal;
585
+ return PAUSE_PREFIX + JSON.stringify(detail === void 0 ? { nodeId, awaiting } : { nodeId, awaiting, detail });
586
+ }
587
+ function decodePause(reason) {
588
+ if (typeof reason !== "string") return null;
589
+ if (reason.startsWith(PAUSE_PREFIX)) {
590
+ const body = reason.slice(PAUSE_PREFIX.length);
591
+ try {
592
+ const parsed = JSON.parse(body);
593
+ if (typeof parsed?.nodeId !== "string" || typeof parsed?.awaiting !== "string") return null;
594
+ return "detail" in parsed ? { nodeId: parsed.nodeId, awaiting: parsed.awaiting, detail: parsed.detail } : { nodeId: parsed.nodeId, awaiting: parsed.awaiting };
595
+ } catch {
596
+ return null;
597
+ }
598
+ }
599
+ for (const [prefix, awaiting] of LEGACY_PAUSE_PREFIXES) {
600
+ if (reason.startsWith(prefix)) {
601
+ return { nodeId: reason.slice(prefix.length), awaiting };
602
+ }
603
+ }
604
+ return null;
605
+ }
606
+ function pauseForHuman(ctx, awaiting, detail) {
607
+ return ctx.abort(encodePause({ nodeId: ctx.node.id, awaiting, detail }));
608
+ }
609
+
610
+ // src/expressions/expr.ts
611
+ var FALSY_STRINGS = /* @__PURE__ */ new Set(["", "0", "false", "no", "off", "null"]);
612
+ function truthy(value) {
613
+ if (typeof value === "boolean") return value;
614
+ if (value === null || value === void 0) return false;
615
+ if (typeof value === "string") return !FALSY_STRINGS.has(value.trim().toLowerCase());
616
+ if (Array.isArray(value)) return value.length > 0;
617
+ if (typeof value === "number") return value !== 0;
618
+ return Boolean(value);
619
+ }
620
+
621
+ // src/durable/human.ts
622
+ var NotAwaitingHuman = class extends Error {
623
+ constructor(message) {
624
+ super(message);
625
+ this.name = "NotAwaitingHuman";
626
+ }
627
+ };
628
+ var Submissions = class {
629
+ constructor() {
630
+ this.answers = /* @__PURE__ */ new Map();
631
+ /** The node the run is currently parked on, if any. */
632
+ this.awaiting = null;
633
+ }
634
+ /**
635
+ * Record an answer for the node the run is parked on.
636
+ *
637
+ * Throws when the run is not waiting on that node. A queued answer for a node
638
+ * that never paused is a write nobody reads — and it looks, from the outside,
639
+ * exactly like a submission that worked.
640
+ */
641
+ record(nodeId, value) {
642
+ if (this.awaiting !== null && this.awaiting !== nodeId) {
643
+ throw new NotAwaitingHuman(
644
+ `This run is waiting on "${this.awaiting}", not "${nodeId}". Recording an answer for a node the run is not parked on would be stored and never read.`
645
+ );
646
+ }
647
+ if (this.awaiting === null) {
648
+ throw new NotAwaitingHuman(
649
+ `This run is not waiting for anyone, so an answer for "${nodeId}" has nothing to resume.`
650
+ );
651
+ }
652
+ this.answers.set(nodeId, value);
653
+ this.awaiting = null;
654
+ }
655
+ answered(nodeId) {
656
+ return this.answers.has(nodeId);
657
+ }
658
+ answer(nodeId) {
659
+ return this.answers.get(nodeId);
660
+ }
661
+ park(nodeId) {
662
+ this.awaiting = nodeId;
663
+ }
664
+ };
665
+ function option(ctx, key, fallback) {
666
+ const config = ctx.node.data?.config ?? {};
667
+ return config[key] ?? fallback;
668
+ }
669
+ function durableUserInput(submissions) {
670
+ return (ctx) => {
671
+ if (submissions.answered(ctx.node.id)) return submissions.answer(ctx.node.id);
672
+ if (option(ctx, "autoAnswerFromInput") === true) {
673
+ const values = ctx.inputs.values;
674
+ if (values !== void 0) return values;
675
+ }
676
+ submissions.park(ctx.node.id);
677
+ return pauseForHuman(ctx, "input", {
678
+ title: option(ctx, "title", "Need your input"),
679
+ fields: option(ctx, "fields", [])
680
+ });
681
+ };
682
+ }
683
+ function durableApproval(submissions) {
684
+ return (ctx) => {
685
+ const passthrough = ctx.inputs.in ?? ctx.inputs;
686
+ if (submissions.answered(ctx.node.id)) {
687
+ return { branch: truthy(submissions.answer(ctx.node.id)) ? "approved" : "denied", value: passthrough };
688
+ }
689
+ if (option(ctx, "autoAnswerFromInput") === true) {
690
+ const decision = ctx.inputs.approved;
691
+ if (decision !== void 0) {
692
+ return { branch: truthy(decision) ? "approved" : "denied", value: passthrough };
693
+ }
694
+ }
695
+ submissions.park(ctx.node.id);
696
+ return pauseForHuman(ctx, "approval", {
697
+ title: option(ctx, "title", "Approve action"),
698
+ description: option(ctx, "description")
699
+ });
700
+ };
701
+ }
702
+
703
+ // src/durable/coordinator.ts
704
+ var Coordinator = class {
705
+ constructor(options) {
706
+ this.graph = options.graph;
707
+ this.executors = options.executors;
708
+ this.run = RunIdentity.from(options.run);
709
+ this.store = options.store ?? new InMemoryClaimStore();
710
+ this.initialInputs = options.initialInputs ?? {};
711
+ this.retry = options.retry ?? new RetryPolicy();
712
+ this.onEvent = options.onEvent;
713
+ }
714
+ get runKey() {
715
+ return this.run.runKey;
716
+ }
717
+ // -- the two operations ----------------------------------------------
718
+ /**
719
+ * Which nodes may be dispatched right now.
720
+ *
721
+ * Also settles the skip cascade, because a skip is a decision the frontier
722
+ * just made and a second caller must not make it again.
723
+ */
724
+ async advance() {
725
+ const frontier = Frontier.compute(this.graph, await this.store.state(this.runKey));
726
+ await Frontier.settleSkips(this.store, this.runKey, frontier.skipped);
727
+ return frontier.ready;
728
+ }
729
+ /**
730
+ * Claim, execute and checkpoint one node.
731
+ *
732
+ * The claim is taken FIRST. Two workers racing for the same node produce one
733
+ * execution and one no-op, and the loser learns that from the store rather
734
+ * than from a duplicate side effect.
735
+ *
736
+ * `owner` is the token that lets a job's own retry re-enter its claim instead
737
+ * of deadlocking against the row it wrote itself — pass the SAME token across
738
+ * a job's attempts.
739
+ */
740
+ async runNode(nodeId, owner) {
741
+ const token = owner ?? cryptoRandom();
742
+ if (!await this.store.claim(this.runKey, nodeId, token)) {
743
+ return { nodeId, status: "not-claimed", claimed: false, attempt: 0 };
744
+ }
745
+ const row = (await this.store.state(this.runKey))[nodeId];
746
+ const identity = this.identityFor(row);
747
+ const replay = await replayUpTo(this.graph, nodeId, this.executors, {
748
+ resumeOutputs: await this.completedOutputs(),
749
+ initialInputs: this.initialInputs,
750
+ onEvent: this.forward(nodeId),
751
+ run: identity
752
+ });
753
+ const result = replay.result;
754
+ const attempt = identity.attempt;
755
+ if (Object.prototype.hasOwnProperty.call(result.outputs, nodeId)) {
756
+ const output = result.outputs[nodeId];
757
+ const ports = replay.portsOf(nodeId);
758
+ await this.store.complete(this.runKey, nodeId, output, ports);
759
+ return { nodeId, status: "completed", output, ports, claimed: true, attempt };
760
+ }
761
+ const pause = decodePause(result.error);
762
+ if (pause && pause.nodeId === nodeId) {
763
+ await this.store.pause(this.runKey, nodeId, result.error ?? "");
764
+ return { nodeId, status: "paused", pause, claimed: true, attempt };
765
+ }
766
+ if (isBoundary(result.error)) {
767
+ await this.store.skip(this.runKey, nodeId);
768
+ return {
769
+ nodeId,
770
+ status: "skipped",
771
+ error: "replay stopped before reaching this node",
772
+ claimed: true,
773
+ attempt
774
+ };
775
+ }
776
+ const error = result.error ?? `node ${nodeId} produced no output and no error`;
777
+ const node = this.graph.nodes.find((n) => n.id === nodeId);
778
+ const tries = node ? this.retry.triesFor(node) : 1;
779
+ if (attempt < tries) {
780
+ return { nodeId, status: "failed", error, claimed: true, attempt, retryable: true };
781
+ }
782
+ await this.store.fail(this.runKey, nodeId, error);
783
+ return { nodeId, status: "failed", error, claimed: true, attempt, retryable: false };
784
+ }
785
+ // -- an in-process driver over the two ------------------------------
786
+ /**
787
+ * Drive the graph here, in this process, one node at a time.
788
+ *
789
+ * Every checkpoint is written exactly as a queued run writes it, so a crash
790
+ * mid-loop resumes from the same place a crashed worker would.
791
+ *
792
+ * Retries honour {@link RetryPolicy}: a node declaring `unsafe-to-replay`
793
+ * gets one attempt whatever the policy says, and the retry re-enters the same
794
+ * claim with the same owner token — so the step key it derives is unchanged,
795
+ * which is what makes the retry idempotent rather than duplicative.
796
+ */
797
+ async runToCompletion(maxPasses = 1e4) {
798
+ for (let pass = 0; pass < maxPasses; pass++) {
799
+ const ready = await this.advance();
800
+ if (ready.length === 0) break;
801
+ for (const nodeId of ready) {
802
+ const outcome = await this.runNodeWithRetries(nodeId);
803
+ if (outcome.status === "paused") {
804
+ return {
805
+ ok: false,
806
+ outputs: await this.outputs(),
807
+ pause: outcome.pause,
808
+ paused: true
809
+ };
810
+ }
811
+ if (outcome.status === "failed") {
812
+ return { ok: false, outputs: await this.outputs(), error: outcome.error, paused: false };
813
+ }
814
+ }
815
+ }
816
+ const state = await this.store.state(this.runKey);
817
+ if (!Frontier.isComplete(this.graph, state)) {
818
+ if (Frontier.hasWorkInFlight(state)) {
819
+ return {
820
+ ok: false,
821
+ outputs: await this.outputs(),
822
+ error: "the run is waiting on work held elsewhere",
823
+ paused: false
824
+ };
825
+ }
826
+ const unsettled = this.graph.nodes.filter((n) => !["completed", "skipped", "failed"].includes(state[n.id]?.status ?? "")).map((n) => n.id);
827
+ return {
828
+ ok: false,
829
+ outputs: await this.outputs(),
830
+ error: `the run cannot progress; unsettled nodes: ${unsettled.join(", ")}`,
831
+ paused: false
832
+ };
833
+ }
834
+ const failed = Object.values(state).filter((e) => e.status === NodeRunStatus.FAILED).map((e) => e.error ?? "node failed");
835
+ return {
836
+ ok: failed.length === 0,
837
+ outputs: await this.outputs(),
838
+ error: failed[0],
839
+ paused: false
840
+ };
841
+ }
842
+ /**
843
+ * Checkpointed outputs, in the graph's own node order.
844
+ *
845
+ * Ordered by the graph rather than by completion so two runs of the same
846
+ * workflow produce comparable output maps even when nodes finished in a
847
+ * different order.
848
+ */
849
+ async outputs() {
850
+ const state = await this.store.state(this.runKey);
851
+ const out = {};
852
+ for (const node of this.graph.nodes) {
853
+ if (state[node.id]?.status === NodeRunStatus.COMPLETED) out[node.id] = state[node.id].output;
854
+ }
855
+ return out;
856
+ }
857
+ /** The checkpointed run, in the shape a single-process run returns. */
858
+ async asRunResult() {
859
+ const outcome = await this.runToCompletion();
860
+ return outcome.error === void 0 ? { ok: outcome.ok, outputs: outcome.outputs } : { ok: outcome.ok, outputs: outcome.outputs, error: outcome.error };
861
+ }
862
+ // -- internals -------------------------------------------------------
863
+ async runNodeWithRetries(nodeId) {
864
+ const owner = cryptoRandom();
865
+ let outcome = await this.runNode(nodeId, owner);
866
+ while (outcome.retryable) {
867
+ outcome = await this.runNode(nodeId, owner);
868
+ }
869
+ return outcome;
870
+ }
871
+ /**
872
+ * The identity handed to the node about to execute.
873
+ *
874
+ * `attempt` and `firstAttemptAt` come off the CLAIM ROW, so they describe
875
+ * this step rather than the run — which is what makes the retry window check
876
+ * exact instead of conservative.
877
+ */
878
+ identityFor(row) {
879
+ if (!row) return this.run;
880
+ return this.run.withAttempt(row.attempts, row.firstAttemptAt);
881
+ }
882
+ async completedOutputs() {
883
+ const state = await this.store.state(this.runKey);
884
+ const out = {};
885
+ for (const [nodeId, entry] of Object.entries(state)) {
886
+ if (entry.status === NodeRunStatus.COMPLETED) out[nodeId] = entry.output;
887
+ }
888
+ return out;
889
+ }
890
+ /**
891
+ * Forward only the events the target node produced.
892
+ *
893
+ * A replay re-emits the whole completed prefix. Passing that through would
894
+ * show a consumer every node running again on every job — the run feed would
895
+ * report a 20-node workflow as 200 status changes.
896
+ */
897
+ forward(nodeId) {
898
+ const sink = this.onEvent;
899
+ if (!sink) return void 0;
900
+ return (event) => {
901
+ const id = "nodeId" in event ? event.nodeId : void 0;
902
+ if (id === void 0 || id === nodeId) sink(event);
903
+ };
904
+ }
905
+ };
906
+ function cryptoRandom() {
907
+ const g = globalThis;
908
+ return g.crypto?.randomUUID?.() ?? `own_${Math.random().toString(36).slice(2)}${Date.now()}`;
909
+ }
910
+
911
+ exports.BOUNDARY = BOUNDARY;
912
+ exports.Coordinator = Coordinator;
913
+ exports.Frontier = Frontier;
914
+ exports.InMemoryClaimStore = InMemoryClaimStore;
915
+ exports.NodeRunStatus = NodeRunStatus;
916
+ exports.NotAwaitingHuman = NotAwaitingHuman;
917
+ exports.RetryPolicy = RetryPolicy;
918
+ exports.SETTLED = SETTLED;
919
+ exports.Submissions = Submissions;
920
+ exports.UNSAFE_TO_REPLAY = UNSAFE_TO_REPLAY;
921
+ exports.durableApproval = durableApproval;
922
+ exports.durableUserInput = durableUserInput;
923
+ exports.isBoundary = isBoundary;
924
+ exports.isSettled = isSettled;
925
+ exports.replayUpTo = replayUpTo;
926
+ //# sourceMappingURL=durable.cjs.map
927
+ //# sourceMappingURL=durable.cjs.map