@pogodisco/zephyr 1.0.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.
@@ -0,0 +1,7 @@
1
+ declare class ZephyrEventStream {
2
+ private listeners;
3
+ emit(evt: any): void;
4
+ subscribe(listener: (evt: any) => void): () => void;
5
+ }
6
+ export declare const eventStream: ZephyrEventStream;
7
+ export {};
@@ -0,0 +1,16 @@
1
+ class ZephyrEventStream {
2
+ constructor() {
3
+ this.listeners = [];
4
+ }
5
+ emit(evt) {
6
+ for (const l of this.listeners)
7
+ l(evt);
8
+ }
9
+ subscribe(listener) {
10
+ this.listeners.push(listener);
11
+ return () => {
12
+ this.listeners = this.listeners.filter((l) => l !== listener);
13
+ };
14
+ }
15
+ }
16
+ export const eventStream = new ZephyrEventStream();
@@ -0,0 +1,9 @@
1
+ export * from "./event-stream.js";
2
+ export * from "./registry.js";
3
+ export * from "./workflow-executor.js";
4
+ export * from "./workflow-composer.js";
5
+ export * from "./utils.js";
6
+ export * from "./types.js";
7
+ export * from "./utils.js";
8
+ export * from "./node/index.js";
9
+ export { useMetrics, useLog } from "./middleware.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ export * from "./event-stream.js";
2
+ export * from "./registry.js";
3
+ export * from "./workflow-executor.js";
4
+ export * from "./workflow-composer.js";
5
+ export * from "./utils.js";
6
+ export * from "./types.js";
7
+ export * from "./utils.js";
8
+ export * from "./node/index.js";
9
+ export { useMetrics, useLog } from "./middleware.js";
@@ -0,0 +1,4 @@
1
+ import { ActionRegistry, WorkflowMiddleware } from "./types.js";
2
+ export declare function composeMiddleware<Reg extends ActionRegistry>(middleware: WorkflowMiddleware<Reg>[], ctx: Parameters<WorkflowMiddleware<Reg>>[0], core: () => Promise<any>): () => Promise<any>;
3
+ export declare function useLog(): WorkflowMiddleware;
4
+ export declare function useMetrics(): WorkflowMiddleware;
@@ -0,0 +1,69 @@
1
+ import { eventStream } from "./event-stream.js";
2
+ export function composeMiddleware(middleware, ctx, core) {
3
+ let index = -1;
4
+ async function dispatch(i) {
5
+ if (i <= index)
6
+ throw new Error("next() called multiple times");
7
+ index = i;
8
+ const fn = middleware[i];
9
+ if (!fn)
10
+ return core();
11
+ return fn(ctx, () => dispatch(i + 1));
12
+ }
13
+ return () => dispatch(0);
14
+ }
15
+ export function useLog() {
16
+ return async ({ frame, stepId }, next) => {
17
+ eventStream.emit({
18
+ type: "node_start",
19
+ node: stepId,
20
+ timestamp: frame.start,
21
+ input: frame.input,
22
+ });
23
+ try {
24
+ const res = await next();
25
+ eventStream.emit({
26
+ type: "node_success",
27
+ node: stepId,
28
+ output: frame.output,
29
+ duration: frame.end - frame.start,
30
+ attempts: frame.attempts,
31
+ timestamp: Date.now(),
32
+ });
33
+ return res;
34
+ }
35
+ catch (err) {
36
+ eventStream.emit({
37
+ type: "node_fail",
38
+ node: stepId,
39
+ error: frame.error,
40
+ timestamp: Date.now(),
41
+ attempts: frame.attempts,
42
+ });
43
+ throw err;
44
+ }
45
+ };
46
+ }
47
+ export function useMetrics() {
48
+ return async ({ stepId, extras }, next) => {
49
+ if (!extras?.metrics) {
50
+ extras.metrics = {};
51
+ }
52
+ const start = Date.now();
53
+ try {
54
+ const res = await next();
55
+ extras.metrics[stepId] = {
56
+ status: "success",
57
+ duration: Date.now() - start,
58
+ };
59
+ return res;
60
+ }
61
+ catch (err) {
62
+ extras.metrics[stepId] = {
63
+ status: "fail",
64
+ duration: Date.now() - start,
65
+ };
66
+ throw err;
67
+ }
68
+ };
69
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./main.js";
2
+ export * from "./types.js";
@@ -0,0 +1,2 @@
1
+ export * from "./main.js";
2
+ export * from "./types.js";
@@ -0,0 +1,11 @@
1
+ import { TaskMap, TaskDefinition, TaskResultsData, TaskState, TaskNodeWithContracts } from "./types.js";
2
+ export declare function defineNode<T extends TaskMap, I extends Record<string, any>, O>(node: {
3
+ [K in keyof T]: T[K] extends TaskDefinition<infer F, any, any> ? TaskDefinition<F, T, I> : never;
4
+ } & {
5
+ _output: (results: TaskResultsData<T, I>, status?: Record<keyof T, TaskState>) => O;
6
+ }): TaskNodeWithContracts<T, I, O>;
7
+ export declare const execNode: <T extends TaskMap, I extends Record<string, any> | undefined, O>(node: TaskNodeWithContracts<T, I, O>, initArgs: I) => Promise<{
8
+ _output: O;
9
+ _status: Record<keyof T, TaskState>;
10
+ _results: any;
11
+ }>;
@@ -0,0 +1,99 @@
1
+ export function defineNode(node) {
2
+ preflightCheck(node);
3
+ return node;
4
+ }
5
+ // --- preflight check & circular detection ---
6
+ function preflightCheck(node) {
7
+ const keys = new Set();
8
+ for (const key in node) {
9
+ if (key === "_output" || key === "_init")
10
+ continue;
11
+ if (keys.has(key))
12
+ throw new Error(`Duplicate task key: ${key}`);
13
+ keys.add(key);
14
+ const task = node[key];
15
+ if (!task)
16
+ continue;
17
+ if (task.abort === undefined)
18
+ task.abort = true;
19
+ }
20
+ const visited = new Set();
21
+ const stack = new Set();
22
+ const visit = (taskKey) => {
23
+ if (taskKey === "_output" || taskKey === "_init")
24
+ return;
25
+ if (stack.has(taskKey))
26
+ throw new Error(`Circular dependency: ${[...stack, taskKey].join(" -> ")}`);
27
+ if (visited.has(taskKey))
28
+ return;
29
+ stack.add(taskKey);
30
+ const task = node[taskKey];
31
+ for (const dep of task?.dependencies ?? []) {
32
+ if (!(dep in node))
33
+ throw new Error(`Task "${taskKey}" depends on unknown "${String(dep)}"`);
34
+ visit(String(dep));
35
+ }
36
+ stack.delete(taskKey);
37
+ visited.add(taskKey);
38
+ };
39
+ for (const key of Object.keys(node))
40
+ visit(key);
41
+ }
42
+ function buildTopologicalBatches(node, keys) {
43
+ const remaining = new Set(keys);
44
+ const resolved = new Set();
45
+ const batches = [];
46
+ while (remaining.size > 0) {
47
+ const batch = [];
48
+ for (const key of remaining) {
49
+ const deps = node[key]?.dependencies ?? [];
50
+ if (deps.every((d) => resolved.has(d))) {
51
+ batch.push(key);
52
+ }
53
+ }
54
+ if (batch.length === 0) {
55
+ throw new Error("Unable to build execution batches (circular or unresolved deps)");
56
+ }
57
+ for (const key of batch) {
58
+ remaining.delete(key);
59
+ resolved.add(key);
60
+ }
61
+ batches.push(batch);
62
+ }
63
+ return batches;
64
+ }
65
+ export const execNode = async (node, initArgs) => {
66
+ const results = { _init: initArgs };
67
+ const status = {};
68
+ const taskKeys = Object.keys(node).filter((k) => k !== "_output");
69
+ for (const k of taskKeys)
70
+ status[k] = "pending";
71
+ const batches = buildTopologicalBatches(node, taskKeys);
72
+ for (const batch of batches) {
73
+ for (const key of batch) {
74
+ const task = node[key];
75
+ const deps = task.dependencies ?? [];
76
+ if (!deps.every((d) => status[d] === "success"))
77
+ continue;
78
+ try {
79
+ const args = task.argMap?.(results) ?? initArgs;
80
+ // Just call the function - it either returns data or throws
81
+ const raw = await task.fn(args);
82
+ // No ok check - if we get here, it succeeded
83
+ status[key] = "success";
84
+ results[key] = raw; // Store the raw return value
85
+ }
86
+ catch (err) {
87
+ status[key] = "failed";
88
+ if (task.abort !== false)
89
+ throw err;
90
+ // If abort is false, continue to next task
91
+ }
92
+ }
93
+ }
94
+ return {
95
+ _output: node._output(results, status),
96
+ _status: status,
97
+ _results: results,
98
+ };
99
+ };
@@ -0,0 +1,41 @@
1
+ export type TraceEventType = "start" | "success" | "fail" | "skipped" | "background";
2
+ export interface TraceEvent {
3
+ task: string;
4
+ type: TraceEventType;
5
+ timestamp: number;
6
+ meta?: any;
7
+ }
8
+ export interface ExecutionTrace {
9
+ batches: string[][];
10
+ events: TraceEvent[];
11
+ }
12
+ export type LogEvent = "start" | "finish" | "data" | "deferred" | "skipped" | "background" | "parallel" | "success" | "fail";
13
+ export type TaskLogger = (event: LogEvent, key: string, meta?: any) => void;
14
+ export interface NodeOptions {
15
+ log?: TaskLogger;
16
+ parallel?: boolean;
17
+ }
18
+ export type TaskState = "pending" | "skipped" | "failed" | "success";
19
+ export type TaskDefinition<F extends (...args: any) => any, T extends TaskMap, I extends Record<string, any> | undefined> = {
20
+ fn: F;
21
+ dependencies?: (keyof T)[];
22
+ abort?: boolean;
23
+ argMap?: (results: TaskResultsData<T, I>) => Parameters<F>[0];
24
+ };
25
+ export type TaskMap = Record<string, TaskDefinition<any, any, any>>;
26
+ export type TaskResultsData<T extends TaskMap, I> = {
27
+ _init: I;
28
+ } & {
29
+ [K in keyof T]: Awaited<ReturnType<T[K]["fn"]>>;
30
+ };
31
+ export type TasksFromFns<T extends Record<string, (...args: any) => any>> = {
32
+ [K in keyof T]: TaskDefinition<T[K], any, any>;
33
+ };
34
+ export type TaskNodeWithContracts<T extends TaskMap, I extends Record<string, any> | undefined, O> = {
35
+ [K in keyof T]: T[K] extends TaskDefinition<infer F, any, any> ? TaskDefinition<F, T, I> : never;
36
+ } & {
37
+ _init?: I;
38
+ _output: (results: TaskResultsData<T, I>, status?: Record<keyof T, TaskState>) => O;
39
+ _batches?: (keyof T)[][];
40
+ _trace?: ExecutionTrace;
41
+ };
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,9 @@
1
+ import { Action, ActionRegistry, MergeActionRegistries } from "./types.js";
2
+ export declare class ActionRegistryBuilder<R extends ActionRegistry = {}> {
3
+ private registry;
4
+ constructor(initial?: R);
5
+ action<K extends string, I, O>(key: K, action: Action<I, O>): ActionRegistryBuilder<MergeActionRegistries<R, Record<K, Action<I, O>>>>;
6
+ extend<Other extends ActionRegistry>(other: ActionRegistryBuilder<Other> | Other): ActionRegistryBuilder<MergeActionRegistries<R, Other>>;
7
+ build(): R;
8
+ }
9
+ export declare function createActionRegistry<R extends ActionRegistry = {}>(initial?: R): ActionRegistryBuilder<R>;
@@ -0,0 +1,24 @@
1
+ export class ActionRegistryBuilder {
2
+ constructor(initial) {
3
+ this.registry = {};
4
+ if (initial) {
5
+ this.registry = { ...initial };
6
+ }
7
+ }
8
+ action(key, action) {
9
+ this.registry[key] = action;
10
+ return this;
11
+ }
12
+ // Extend with another registry (with override)
13
+ extend(other) {
14
+ const otherRegistry = other instanceof ActionRegistryBuilder ? other.build() : other;
15
+ Object.assign(this.registry, otherRegistry);
16
+ return this;
17
+ }
18
+ build() {
19
+ return this.registry;
20
+ }
21
+ }
22
+ export function createActionRegistry(initial) {
23
+ return new ActionRegistryBuilder(initial);
24
+ }
@@ -0,0 +1,24 @@
1
+ export type Action<I = any, O = any> = (args: I) => Promise<O>;
2
+ export interface ActionRegistry {
3
+ [key: string]: Action;
4
+ }
5
+ export type MergeActionRegistries<A extends ActionRegistry, B extends ActionRegistry> = Omit<A, keyof B> & B;
6
+ export type ExecutionFrame = {
7
+ stepId: string;
8
+ attempts: number;
9
+ start: number;
10
+ end?: number;
11
+ input?: any;
12
+ output?: any;
13
+ error?: any;
14
+ };
15
+ export type WorkflowMiddleware<Reg extends ActionRegistry = any> = {
16
+ (ctx: {
17
+ stepId: string;
18
+ input: any;
19
+ results: Record<string, any>;
20
+ registry: Reg;
21
+ extras: Record<string, any>;
22
+ frame: ExecutionFrame;
23
+ }, next: () => Promise<any>): Promise<any>;
24
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,8 @@
1
+ import { TaskMap, TaskNodeWithContracts, TasksFromFns } from "./node/types.js";
2
+ export declare function createAction<FN extends (args: any) => any, // Allow sync or async
3
+ Out = Awaited<ReturnType<FN>>>(fn: FN): TaskNodeWithContracts<TasksFromFns<{
4
+ run: FN;
5
+ }>, Parameters<FN>[0], Out>;
6
+ export declare function genericAction<FN extends (args: any) => any>(fn: FN): <T = Awaited<ReturnType<FN>>>() => (initArgs: Parameters<FN>[0]) => Promise<T>;
7
+ export declare function fixedAction<FN extends (args: any) => any, T = Awaited<ReturnType<FN>>>(fn: FN): () => (args: Parameters<FN>[0]) => Promise<T>;
8
+ export declare function useAction<T extends TaskMap, I extends Record<string, any> | undefined, O>(node: TaskNodeWithContracts<T, I, O>): (initArgs: I) => Promise<O>;
package/dist/utils.js ADDED
@@ -0,0 +1,25 @@
1
+ import { defineNode, execNode } from "./node/main.js";
2
+ export function createAction(fn) {
3
+ // Just return the raw node definition, not wrapped with useNode
4
+ return defineNode({
5
+ run: {
6
+ fn, // Function can throw or return data
7
+ argMap: (r) => r._init,
8
+ },
9
+ _output: (r) => r.run,
10
+ });
11
+ }
12
+ export function genericAction(fn) {
13
+ return () => useAction(createAction(fn));
14
+ }
15
+ export function fixedAction(fn) {
16
+ return () => useAction(createAction(fn));
17
+ }
18
+ export function useAction(node) {
19
+ // Returns a function that graph can call, but WITHOUT withResponse
20
+ // Just a simple adapter that calls callNode and returns raw _output
21
+ return async (initArgs) => {
22
+ const result = await execNode(node, initArgs);
23
+ return result._output; // Just raw data, throws on error
24
+ };
25
+ }
@@ -0,0 +1,68 @@
1
+ import { ActionRegistry } from "./types.js";
2
+ type StepResult<Reg extends ActionRegistry, ActionName extends keyof Reg> = Awaited<ReturnType<Reg[ActionName]>>;
3
+ export type StepDef<Reg extends ActionRegistry, ID extends string = string, ActionName extends keyof Reg = any> = {
4
+ id: ID;
5
+ action: ActionName;
6
+ dependsOn: string[];
7
+ resolve: (ctx: any) => Parameters<Reg[ActionName]>[0];
8
+ when?: (ctx: any) => boolean;
9
+ };
10
+ export type WorkflowDef<Reg extends ActionRegistry, Input, Results, Steps extends StepDef<Reg, any, any>[] = StepDef<Reg, any, any>[], Output = undefined> = {
11
+ name: string;
12
+ steps: Steps;
13
+ entrySteps: StepDef<Reg>[];
14
+ endSteps: StepDef<Reg>[];
15
+ input: Input;
16
+ results: Results;
17
+ outputResolver?: (ctx: any) => Output;
18
+ };
19
+ type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
20
+ export declare class WorkflowBuilder<Reg extends ActionRegistry, Input = unknown, Steps extends StepDef<Reg, any, any>[] = [], Results = {}> {
21
+ private name;
22
+ private registry;
23
+ private steps;
24
+ private frontier;
25
+ private outputResolver?;
26
+ constructor(name: string, registry: Reg);
27
+ step<ID extends string, ActionName extends keyof Reg & string>(id: ID, action: ActionName, resolve: (ctx: {
28
+ input: Input;
29
+ results: Results;
30
+ }) => Parameters<Reg[ActionName]>[0], dependsOn?: string[]): WorkflowBuilder<Reg, Input, [
31
+ ...Steps,
32
+ StepDef<Reg, ID, ActionName>
33
+ ], Results & {
34
+ [K in ID]: StepResult<Reg, ActionName>;
35
+ }>;
36
+ seq<ID extends string, ActionName extends keyof Reg & string>(id: ID, action: ActionName, resolve: (ctx: {
37
+ input: Input;
38
+ results: Results;
39
+ }) => Parameters<Reg[ActionName]>[0]): WorkflowBuilder<Reg, Input, [...Steps, StepDef<Reg, ID, ActionName>], Results & { [K in ID]: Awaited<ReturnType<Reg[ActionName]>>; }>;
40
+ parallel<Branches extends WorkflowBuilder<Reg, Input, any, any>[]>(...branches: {
41
+ [K in keyof Branches]: (builder: WorkflowBuilder<Reg, Input, [], Results>) => Branches[K];
42
+ }): WorkflowBuilder<Reg, Input, [
43
+ ...Steps,
44
+ ...(Branches[number] extends WorkflowBuilder<Reg, any, infer S, any> ? S : never)
45
+ ], Results & (Branches[number] extends WorkflowBuilder<Reg, any, any, infer R> ? UnionToIntersection<R> : {})>;
46
+ join<ID extends string, ActionName extends keyof Reg & string>(id: ID, action: ActionName, resolve: (ctx: {
47
+ input: Input;
48
+ results: Results;
49
+ }) => Parameters<Reg[ActionName]>[0]): WorkflowBuilder<Reg, Input, [...Steps, StepDef<Reg, ID, ActionName>], Results & { [K in ID]: Awaited<ReturnType<Reg[ActionName]>>; }>;
50
+ subflow<Prefix extends string, SubInput, SubResults, SubSteps extends StepDef<Reg, any, any>[]>(prefix: Prefix, workflow: WorkflowDef<Reg, SubInput, SubResults, SubSteps>, resolveInput: (ctx: {
51
+ input: Input;
52
+ results: Results;
53
+ }) => SubInput): WorkflowBuilder<Reg, Input, [
54
+ ...Steps,
55
+ ...SubSteps
56
+ ], Results & {
57
+ [K in Prefix]: SubResults;
58
+ }>;
59
+ output<Output>(fn: (ctx: {
60
+ input: Input;
61
+ results: Results;
62
+ }) => Output): WorkflowDef<Reg, Input, Results, Steps, Output>;
63
+ build(): WorkflowDef<Reg, Input, Results, Steps>;
64
+ private validateDependencies;
65
+ private getEndSteps;
66
+ }
67
+ export declare function createWorkflow<Reg extends ActionRegistry>(registry: Reg): <Input = unknown>(name: string) => WorkflowBuilder<Reg, Input, [], {}>;
68
+ export {};
@@ -0,0 +1,405 @@
1
+ // import { ActionRegistry } from "../registry/types.js";
2
+ //
3
+ // type StepResult<
4
+ // Reg extends ActionRegistry,
5
+ // ActionName extends keyof Reg,
6
+ // > = Awaited<ReturnType<Reg[ActionName]>>;
7
+ //
8
+ // export type StepDef<
9
+ // Reg extends ActionRegistry,
10
+ // ID extends string = string,
11
+ // ActionName extends keyof Reg = any,
12
+ // > = {
13
+ // id: ID;
14
+ // action: ActionName;
15
+ // dependsOn: string[];
16
+ // resolve: (ctx: any) => Parameters<Reg[ActionName]>[0];
17
+ // when?: (ctx: any) => boolean;
18
+ // };
19
+ //
20
+ // export type WorkflowDef<
21
+ // Reg extends ActionRegistry,
22
+ // Input,
23
+ // Results,
24
+ // Steps extends StepDef<Reg, any, any>[] = StepDef<Reg, any, any>[],
25
+ // > = {
26
+ // name: string;
27
+ // steps: Steps;
28
+ // entrySteps: StepDef<Reg>[];
29
+ // endSteps: StepDef<Reg>[];
30
+ // input: Input;
31
+ // results: Results;
32
+ // };
33
+ //
34
+ // type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
35
+ // k: infer I,
36
+ // ) => void
37
+ // ? I
38
+ // : never;
39
+ //
40
+ // export class WorkflowBuilder<
41
+ // Reg extends ActionRegistry,
42
+ // Input = unknown,
43
+ // Steps extends StepDef<Reg, any, any>[] = [],
44
+ // Results = {},
45
+ // > {
46
+ // private steps: StepDef<Reg, any, any>[] = [];
47
+ // private frontier: string[] = [];
48
+ //
49
+ // constructor(
50
+ // private name: string,
51
+ // private registry: Reg,
52
+ // ) {}
53
+ //
54
+ // /* ------------------------------------------------ */
55
+ // /* Base Step */
56
+ // /* ------------------------------------------------ */
57
+ //
58
+ // step<ID extends string, ActionName extends keyof Reg & string>(
59
+ // id: ID,
60
+ // action: ActionName,
61
+ // resolve: (ctx: {
62
+ // input: Input;
63
+ // results: Results;
64
+ // }) => Parameters<Reg[ActionName]>[0],
65
+ // dependsOn?: string[],
66
+ // ): WorkflowBuilder<
67
+ // Reg,
68
+ // Input,
69
+ // [...Steps, StepDef<Reg, ID, ActionName>],
70
+ // Results & { [K in ID]: StepResult<Reg, ActionName> }
71
+ // > {
72
+ // const deps = dependsOn ?? [...this.frontier];
73
+ //
74
+ // this.steps.push({
75
+ // id,
76
+ // action,
77
+ // resolve,
78
+ // dependsOn: deps,
79
+ // });
80
+ //
81
+ // this.frontier = [id];
82
+ //
83
+ // return this as any;
84
+ // }
85
+ //
86
+ // /* ------------------------------------------------ */
87
+ // /* Sequential shortcut */
88
+ // /* ------------------------------------------------ */
89
+ //
90
+ // seq<ID extends string, ActionName extends keyof Reg & string>(
91
+ // id: ID,
92
+ // action: ActionName,
93
+ // resolve: (ctx: {
94
+ // input: Input;
95
+ // results: Results;
96
+ // }) => Parameters<Reg[ActionName]>[0],
97
+ // ) {
98
+ // return this.step(id, action, resolve);
99
+ // }
100
+ //
101
+ // /* ------------------------------------------------ */
102
+ // /* Parallel branches */
103
+ // /* ------------------------------------------------ */
104
+ //
105
+ // parallel<Branches extends WorkflowBuilder<Reg, Input, any, any>[]>(
106
+ // ...branches: {
107
+ // [K in keyof Branches]: (
108
+ // builder: WorkflowBuilder<Reg, Input, [], Results>,
109
+ // ) => Branches[K];
110
+ // }
111
+ // ): WorkflowBuilder<
112
+ // Reg,
113
+ // Input,
114
+ // [
115
+ // ...Steps,
116
+ // ...(Branches[number] extends WorkflowBuilder<Reg, any, infer S, any>
117
+ // ? S
118
+ // : never),
119
+ // ],
120
+ // Results &
121
+ // (Branches[number] extends WorkflowBuilder<Reg, any, any, infer R>
122
+ // ? UnionToIntersection<R>
123
+ // : {})
124
+ // > {
125
+ // const parentFrontier = [...this.frontier];
126
+ // const branchEnds: string[] = [];
127
+ //
128
+ // branches.forEach((branch) => {
129
+ // const b = new WorkflowBuilder<Reg, Input, [], Results>(
130
+ // this.name,
131
+ // this.registry,
132
+ // );
133
+ //
134
+ // b.frontier = parentFrontier;
135
+ //
136
+ // branch(b);
137
+ //
138
+ // branchEnds.push(...b.frontier);
139
+ //
140
+ // this.steps.push(...(b as any).steps);
141
+ // });
142
+ //
143
+ // this.frontier = branchEnds;
144
+ //
145
+ // return this as any;
146
+ // }
147
+ //
148
+ // /* ------------------------------------------------ */
149
+ // /* Join helper */
150
+ // /* ------------------------------------------------ */
151
+ //
152
+ // join<ID extends string, ActionName extends keyof Reg & string>(
153
+ // id: ID,
154
+ // action: ActionName,
155
+ // resolve: (ctx: {
156
+ // input: Input;
157
+ // results: Results;
158
+ // }) => Parameters<Reg[ActionName]>[0],
159
+ // ) {
160
+ // return this.step(id, action, resolve, [...this.frontier]);
161
+ // }
162
+ //
163
+ // /* ------------------------------------------------ */
164
+ // /* Subflow */
165
+ // /* ------------------------------------------------ */
166
+ //
167
+ // subflow<
168
+ // Prefix extends string,
169
+ // SubInput,
170
+ // SubResults,
171
+ // SubSteps extends StepDef<Reg, any, any>[],
172
+ // >(
173
+ // prefix: Prefix,
174
+ // workflow: WorkflowDef<Reg, SubInput, SubResults, SubSteps>,
175
+ // resolveInput: (ctx: { input: Input; results: Results }) => SubInput,
176
+ // ): WorkflowBuilder<
177
+ // Reg,
178
+ // Input,
179
+ // [...Steps, ...SubSteps],
180
+ // Results & { [K in Prefix]: SubResults }
181
+ // > {
182
+ // const idMap = new Map<string, string>();
183
+ //
184
+ // workflow.steps.forEach((step) => {
185
+ // idMap.set(step.id, `${prefix}.${step.id}`);
186
+ // });
187
+ //
188
+ // workflow.steps.forEach((step) => {
189
+ // const newStep = {
190
+ // ...step,
191
+ //
192
+ // id: idMap.get(step.id)!,
193
+ //
194
+ // dependsOn: step.dependsOn.map((d) => idMap.get(d)!),
195
+ //
196
+ // resolve: (ctx: any) => {
197
+ // const subInput = resolveInput(ctx);
198
+ //
199
+ // return step.resolve({
200
+ // input: subInput,
201
+ // results: ctx.results,
202
+ // });
203
+ // },
204
+ // };
205
+ //
206
+ // if (workflow.entrySteps.find((e) => e.id === step.id)) {
207
+ // newStep.dependsOn = [...this.frontier];
208
+ // }
209
+ //
210
+ // this.steps.push(newStep);
211
+ // });
212
+ //
213
+ // this.frontier = workflow.endSteps.map((e) => idMap.get(e.id)!);
214
+ //
215
+ // return this as any;
216
+ // }
217
+ //
218
+ // /* ------------------------------------------------ */
219
+ // /* Build */
220
+ // /* ------------------------------------------------ */
221
+ //
222
+ // build(): WorkflowDef<Reg, Input, Results, Steps> {
223
+ // this.validateDependencies();
224
+ //
225
+ // return {
226
+ // name: this.name,
227
+ //
228
+ // steps: this.steps as Steps,
229
+ //
230
+ // entrySteps: this.steps.filter((s) => s.dependsOn.length === 0),
231
+ //
232
+ // endSteps: this.getEndSteps(),
233
+ //
234
+ // input: {} as Input,
235
+ //
236
+ // results: {} as Results,
237
+ // };
238
+ // }
239
+ //
240
+ // /* ------------------------------------------------ */
241
+ //
242
+ // private validateDependencies() {
243
+ // const stepIds = new Set(this.steps.map((s) => s.id));
244
+ //
245
+ // for (const step of this.steps) {
246
+ // for (const dep of step.dependsOn) {
247
+ // if (!stepIds.has(dep)) {
248
+ // throw new Error(`Step ${step.id} depends on unknown step ${dep}`);
249
+ // }
250
+ // }
251
+ // }
252
+ // }
253
+ //
254
+ // private getEndSteps() {
255
+ // const hasDependents = new Set<string>();
256
+ //
257
+ // for (const step of this.steps) {
258
+ // for (const dep of step.dependsOn) {
259
+ // hasDependents.add(dep);
260
+ // }
261
+ // }
262
+ //
263
+ // return this.steps.filter((s) => !hasDependents.has(s.id));
264
+ // }
265
+ // }
266
+ //
267
+ // // export function createWorkflow<Reg extends ActionRegistry, Input = unknown>(
268
+ // // registry: Reg,
269
+ // // ) {
270
+ // // return (name: string) => new WorkflowBuilder<Reg, Input>(name, registry);
271
+ // // }
272
+ //
273
+ // export function createWorkflow<Reg extends ActionRegistry>(registry: Reg) {
274
+ // return function workflow<Input = unknown>(name: string) {
275
+ // return new WorkflowBuilder<Reg, Input>(name, registry);
276
+ // };
277
+ // }
278
+ //
279
+ export class WorkflowBuilder {
280
+ constructor(name, registry) {
281
+ this.name = name;
282
+ this.registry = registry;
283
+ this.steps = [];
284
+ this.frontier = [];
285
+ }
286
+ /* ------------------------------------------------ */
287
+ /* Base Step */
288
+ /* ------------------------------------------------ */
289
+ step(id, action, resolve, dependsOn) {
290
+ const deps = dependsOn ?? [...this.frontier];
291
+ this.steps.push({
292
+ id,
293
+ action,
294
+ resolve,
295
+ dependsOn: deps,
296
+ });
297
+ this.frontier = [id];
298
+ return this;
299
+ }
300
+ /* ------------------------------------------------ */
301
+ /* Sequential shortcut */
302
+ /* ------------------------------------------------ */
303
+ seq(id, action, resolve) {
304
+ return this.step(id, action, resolve);
305
+ }
306
+ /* ------------------------------------------------ */
307
+ /* Parallel branches */
308
+ parallel(...branches) {
309
+ const parentFrontier = [...this.frontier];
310
+ const branchEnds = [];
311
+ branches.forEach((branch) => {
312
+ const b = new WorkflowBuilder(this.name, this.registry);
313
+ b.frontier = parentFrontier;
314
+ branch(b);
315
+ branchEnds.push(...b.frontier);
316
+ this.steps.push(...b.steps);
317
+ });
318
+ this.frontier = branchEnds;
319
+ return this;
320
+ }
321
+ /* ------------------------------------------------ */
322
+ /* Join helper */
323
+ /* ------------------------------------------------ */
324
+ join(id, action, resolve) {
325
+ return this.step(id, action, resolve, [...this.frontier]);
326
+ }
327
+ /* ------------------------------------------------ */
328
+ /* Subflow */
329
+ /* ------------------------------------------------ */
330
+ subflow(prefix, workflow, resolveInput) {
331
+ const idMap = new Map();
332
+ workflow.steps.forEach((step) => {
333
+ idMap.set(step.id, `${prefix}.${step.id}`);
334
+ });
335
+ workflow.steps.forEach((step) => {
336
+ const newStep = {
337
+ ...step,
338
+ id: idMap.get(step.id),
339
+ dependsOn: step.dependsOn.map((d) => idMap.get(d)),
340
+ resolve: (ctx) => {
341
+ const subInput = resolveInput(ctx);
342
+ return step.resolve({
343
+ input: subInput,
344
+ results: ctx.results,
345
+ });
346
+ },
347
+ };
348
+ if (workflow.entrySteps.find((e) => e.id === step.id)) {
349
+ newStep.dependsOn = [...this.frontier];
350
+ }
351
+ this.steps.push(newStep);
352
+ });
353
+ this.frontier = workflow.endSteps.map((e) => idMap.get(e.id));
354
+ return this;
355
+ }
356
+ output(fn) {
357
+ this.outputResolver = fn;
358
+ return this.build();
359
+ }
360
+ /* ------------------------------------------------ */
361
+ /* Build */
362
+ /* ------------------------------------------------ */
363
+ build() {
364
+ this.validateDependencies();
365
+ return {
366
+ name: this.name,
367
+ steps: this.steps,
368
+ entrySteps: this.steps.filter((s) => s.dependsOn.length === 0),
369
+ endSteps: this.getEndSteps(),
370
+ input: {},
371
+ results: {},
372
+ outputResolver: this.outputResolver,
373
+ };
374
+ }
375
+ /* ------------------------------------------------ */
376
+ validateDependencies() {
377
+ const stepIds = new Set(this.steps.map((s) => s.id));
378
+ for (const step of this.steps) {
379
+ for (const dep of step.dependsOn) {
380
+ if (!stepIds.has(dep)) {
381
+ throw new Error(`Step ${step.id} depends on unknown step ${dep}`);
382
+ }
383
+ }
384
+ }
385
+ }
386
+ getEndSteps() {
387
+ const hasDependents = new Set();
388
+ for (const step of this.steps) {
389
+ for (const dep of step.dependsOn) {
390
+ hasDependents.add(dep);
391
+ }
392
+ }
393
+ return this.steps.filter((s) => !hasDependents.has(s.id));
394
+ }
395
+ }
396
+ // export function createWorkflow<Reg extends ActionRegistry, Input = unknown>(
397
+ // registry: Reg,
398
+ // ) {
399
+ // return (name: string) => new WorkflowBuilder<Reg, Input>(name, registry);
400
+ // }
401
+ export function createWorkflow(registry) {
402
+ return function workflow(name) {
403
+ return new WorkflowBuilder(name, registry);
404
+ };
405
+ }
@@ -0,0 +1,7 @@
1
+ import { ActionRegistry, WorkflowMiddleware } from "./types.js";
2
+ import { WorkflowDef } from "./workflow-composer.js";
3
+ export declare function executeWorkflow<Reg extends ActionRegistry, I, R, O = R>(workflow: WorkflowDef<Reg, I, R, any, O>, registry: Reg, input: I, middleware?: WorkflowMiddleware<Reg>[]): Promise<{
4
+ results: R;
5
+ output: O;
6
+ extras: Record<string, any>;
7
+ }>;
@@ -0,0 +1,169 @@
1
+ // import { composeMiddleware } from "../middleware/index.js";
2
+ // import { ActionRegistry } from "../registry/types.js";
3
+ // import { WorkflowDef } from "./main.js";
4
+ // import { ExecutionFrame, WorkflowMiddleware } from "./types.js";
5
+ //
6
+ // export async function executeWorkflow<Reg extends ActionRegistry, I, R>(
7
+ // workflow: WorkflowDef<Reg, I, R>,
8
+ // registry: Reg,
9
+ // input: I,
10
+ // middleware: WorkflowMiddleware<Reg>[] = [],
11
+ // ): Promise<{ results: R; extras: Record<string, any> }> {
12
+ // const results: Record<string, any> = {};
13
+ // const extras: Record<string, any> = {};
14
+ // extras.frames = {} as Record<string, ExecutionFrame>; // <-- store execution frames per step
15
+ //
16
+ // const stepById = new Map(workflow.steps.map((s) => [s.id, s]));
17
+ // const remainingDeps = new Map<string, number>();
18
+ // const dependents = new Map<string, string[]>();
19
+ // const ready: string[] = [];
20
+ //
21
+ // // Build dependency graph
22
+ // for (const step of workflow.steps) {
23
+ // remainingDeps.set(step.id, step.dependsOn.length);
24
+ // if (step.dependsOn.length === 0) ready.push(step.id);
25
+ //
26
+ // for (const dep of step.dependsOn) {
27
+ // if (!dependents.has(dep)) dependents.set(dep, []);
28
+ // dependents.get(dep)!.push(step.id);
29
+ // }
30
+ // }
31
+ //
32
+ // let completed = 0;
33
+ //
34
+ // // Scheduler loop
35
+ // while (ready.length > 0) {
36
+ // const batch = ready.splice(0);
37
+ //
38
+ // await Promise.all(
39
+ // batch.map(async (stepId) => {
40
+ // const step = stepById.get(stepId)!;
41
+ //
42
+ // const frame: ExecutionFrame = {
43
+ // stepId,
44
+ // attempts: 0,
45
+ // start: Date.now(),
46
+ // };
47
+ // extras.frames[stepId] = frame;
48
+ //
49
+ // const ctx = {
50
+ // stepId,
51
+ // input,
52
+ // results,
53
+ // registry,
54
+ // extras,
55
+ // frame,
56
+ // };
57
+ //
58
+ // const core = async () => {
59
+ // frame.attempts++;
60
+ // frame.input = step.resolve({ input, results });
61
+ //
62
+ // try {
63
+ // const action = registry[step.action];
64
+ //
65
+ // const result = await action(frame.input);
66
+ // frame.output = result;
67
+ // frame.end = Date.now();
68
+ //
69
+ // results[step.id] = result;
70
+ // return result;
71
+ // } catch (err) {
72
+ // frame.error = err;
73
+ // frame.end = Date.now();
74
+ // throw err;
75
+ // }
76
+ // };
77
+ //
78
+ // const composed = composeMiddleware(middleware, ctx, core);
79
+ // await composed();
80
+ //
81
+ // // Activate dependents
82
+ // for (const childId of dependents.get(stepId) ?? []) {
83
+ // const remaining = remainingDeps.get(childId)! - 1;
84
+ // remainingDeps.set(childId, remaining);
85
+ // if (remaining === 0) ready.push(childId);
86
+ // }
87
+ //
88
+ // completed++;
89
+ // }),
90
+ // );
91
+ // }
92
+ //
93
+ // // Deadlock detection
94
+ // if (completed !== workflow.steps.length) {
95
+ // throw new Error("Workflow execution failed (cycle or missing dependency)");
96
+ // }
97
+ //
98
+ // return { results: results as R, extras };
99
+ // }
100
+ import { composeMiddleware } from "./middleware.js";
101
+ export async function executeWorkflow(workflow, registry, input, middleware = []) {
102
+ const results = {};
103
+ const extras = {};
104
+ extras.frames = {};
105
+ // --- strongly type steps ---
106
+ const stepById = new Map(workflow.steps.map((s) => [s.id, s]));
107
+ const remainingDeps = new Map();
108
+ const dependents = new Map();
109
+ const ready = [];
110
+ // Build dependency graph
111
+ for (const step of workflow.steps) {
112
+ remainingDeps.set(step.id, step.dependsOn.length);
113
+ if (step.dependsOn.length === 0)
114
+ ready.push(step.id);
115
+ for (const dep of step.dependsOn) {
116
+ if (!dependents.has(dep))
117
+ dependents.set(dep, []);
118
+ dependents.get(dep).push(step.id);
119
+ }
120
+ }
121
+ let completed = 0;
122
+ while (ready.length > 0) {
123
+ const batch = ready.splice(0);
124
+ await Promise.all(batch.map(async (stepId) => {
125
+ const step = stepById.get(stepId);
126
+ const frame = {
127
+ stepId,
128
+ attempts: 0,
129
+ start: Date.now(),
130
+ };
131
+ extras.frames[stepId] = frame;
132
+ const ctx = { stepId, input, results, registry, extras, frame };
133
+ const core = async () => {
134
+ frame.attempts++;
135
+ frame.input = step.resolve({ input, results });
136
+ try {
137
+ const action = registry[step.action];
138
+ const result = await action(frame.input);
139
+ frame.output = result;
140
+ frame.end = Date.now();
141
+ results[step.id] = result;
142
+ return result;
143
+ }
144
+ catch (err) {
145
+ frame.error = err;
146
+ frame.end = Date.now();
147
+ throw err;
148
+ }
149
+ };
150
+ const composed = composeMiddleware(middleware, ctx, core);
151
+ await composed();
152
+ for (const childId of dependents.get(stepId) ?? []) {
153
+ const remaining = remainingDeps.get(childId) - 1;
154
+ remainingDeps.set(childId, remaining);
155
+ if (remaining === 0)
156
+ ready.push(childId);
157
+ }
158
+ completed++;
159
+ }));
160
+ }
161
+ if (completed !== workflow.steps.length) {
162
+ throw new Error("Workflow execution failed (cycle or missing dependency)");
163
+ }
164
+ // Resolve output
165
+ const output = workflow.outputResolver
166
+ ? workflow.outputResolver({ input, results: results })
167
+ : results;
168
+ return { results: results, output, extras };
169
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@pogodisco/zephyr",
3
+ "version": "1.0.0",
4
+ "publishConfig": {
5
+ "access": "public"
6
+ },
7
+ "author": "Jakub Bystroński",
8
+ "description": "dsl to dag orchestrator",
9
+ "keywords": [
10
+ "DSL",
11
+ "DAG",
12
+ "graph",
13
+ "flow",
14
+ "orchestrator",
15
+ "task",
16
+ "typescript"
17
+ ],
18
+ "type": "module",
19
+ "license": "MIT",
20
+ "main": "./dist/index.js",
21
+ "types": "./dist/index.d.ts",
22
+ "exports": {
23
+ ".": {
24
+ "import": "./dist/index.js",
25
+ "require": "./dist/index.cjs",
26
+ "types": "./dist/index.d.ts"
27
+ }
28
+ },
29
+ "files": [
30
+ "dist"
31
+ ],
32
+ "scripts": {
33
+ "prepublishOnly": "npm run build",
34
+ "build": "tsc -p tsconfig.build.json",
35
+ "dev": "tsc -w -p tsconfig.json",
36
+ "test:types": "tsd"
37
+ },
38
+ "tsd": {
39
+ "directory": "test-dts"
40
+ },
41
+ "peerDependencies": {
42
+ "typescript": "^5.0.0"
43
+ },
44
+ "devDependencies": {
45
+ "@types/node": "^25.2.3",
46
+ "chalk": "^5.6.2",
47
+ "pretty-format": "^30.2.0",
48
+ "tsd": "^0.33.0"
49
+ }
50
+ }