@meridianjs/workflow-engine 0.1.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,126 @@
1
+ import { StepContext, MeridianContainer } from '@meridianjs/types';
2
+
3
+ /**
4
+ * Returned by a step's invoke function to provide a separate
5
+ * compensateInput (the data the compensate function receives on rollback).
6
+ *
7
+ * If the invoke function returns a plain value (not a StepResponse),
8
+ * that value is used as both the step output AND the compensate input.
9
+ *
10
+ * @example
11
+ * async function invoke(input, ctx) {
12
+ * const record = await svc.create(input)
13
+ * return new StepResponse(record, { id: record.id }) // slim compensate input
14
+ * }
15
+ * async function compensate({ id }, ctx) {
16
+ * await svc.delete(id)
17
+ * }
18
+ */
19
+ declare class StepResponse<TOutput, TCompInput = TOutput> {
20
+ readonly output: TOutput;
21
+ readonly compensateInput: TCompInput;
22
+ constructor(output: TOutput, compensateInput: TCompInput);
23
+ }
24
+
25
+ /**
26
+ * Wraps the final return value of a workflow constructor function.
27
+ * The `output` becomes the `result` of the workflow run.
28
+ *
29
+ * @example
30
+ * createWorkflow("create-project", async (input) => {
31
+ * const project = await createProjectStep(input)
32
+ * return new WorkflowResponse(project)
33
+ * })
34
+ */
35
+ declare class WorkflowResponse<T> {
36
+ readonly output: T;
37
+ constructor(output: T);
38
+ }
39
+
40
+ type InvokeFn<TInput, TOutput, TCompInput> = (input: TInput, ctx: StepContext) => Promise<TOutput | StepResponse<TOutput, TCompInput>>;
41
+ type CompensateFn<TCompInput> = (input: TCompInput, ctx: StepContext) => Promise<void>;
42
+ /**
43
+ * Defines a workflow step with an optional compensation (rollback) function.
44
+ *
45
+ * Returns a step function `(input: TInput) => Promise<TOutput>`.
46
+ * When called inside a workflow, automatically registers the compensation
47
+ * on the LIFO stack so it runs if a later step fails.
48
+ *
49
+ * @example
50
+ * const createProjectStep = createStep(
51
+ * "create-project",
52
+ * async (input: CreateProjectInput, { container }) => {
53
+ * const svc = container.resolve("projectModuleService") as any
54
+ * const project = await svc.createProject(input)
55
+ * return new StepResponse(project, { projectId: project.id })
56
+ * },
57
+ * async ({ projectId }, { container }) => {
58
+ * const svc = container.resolve("projectModuleService") as any
59
+ * await svc.deleteProject(projectId)
60
+ * }
61
+ * )
62
+ */
63
+ declare function createStep<TInput, TOutput, TCompInput = TOutput>(name: string, invoke: InvokeFn<TInput, TOutput, TCompInput>, compensate?: CompensateFn<TCompInput>): (input: TInput) => Promise<TOutput>;
64
+
65
+ type WorkflowTransactionStatus = "done" | "reverted" | "failed";
66
+ interface WorkflowResult<TOutput> {
67
+ result: TOutput;
68
+ errors: Error[];
69
+ transaction_status: WorkflowTransactionStatus;
70
+ }
71
+ interface WorkflowRunner<TInput, TOutput> {
72
+ run(opts: {
73
+ input: TInput;
74
+ }): Promise<WorkflowResult<TOutput>>;
75
+ }
76
+ /**
77
+ * Defines a named workflow with a constructor function that orchestrates steps.
78
+ *
79
+ * Returns a factory `(container) => WorkflowRunner` so each request gets
80
+ * its own isolated run context with a fresh compensation stack.
81
+ *
82
+ * If any step throws, compensations run in LIFO order (saga pattern).
83
+ * Compensation errors are collected but do not re-throw.
84
+ *
85
+ * @example
86
+ * export const createProjectWorkflow = createWorkflow(
87
+ * "create-project",
88
+ * async (input: CreateProjectInput) => {
89
+ * const project = await createProjectStep(input)
90
+ * const _ = await logActivityStep({ entity_type: "project", entity_id: project.id })
91
+ * return new WorkflowResponse(project)
92
+ * }
93
+ * )
94
+ *
95
+ * // In a route handler:
96
+ * const { result, errors, transaction_status } = await createProjectWorkflow(req.scope).run({ input: req.body })
97
+ */
98
+ declare function createWorkflow<TInput, TOutput>(name: string, constructorFn: (input: TInput) => Promise<WorkflowResponse<TOutput>>): (container: MeridianContainer) => WorkflowRunner<TInput, TOutput>;
99
+
100
+ /**
101
+ * Transforms a step output value using a mapping function.
102
+ * A thin utility to make data transformations explicit in workflow constructors.
103
+ *
104
+ * @example
105
+ * const project = await createProjectStep(input)
106
+ * const activityInput = transform(project, (p) => ({
107
+ * entity_type: "project" as const,
108
+ * entity_id: p.id,
109
+ * workspace_id: p.workspace_id,
110
+ * }))
111
+ * await logActivityStep(activityInput)
112
+ */
113
+ declare function transform<T, U>(input: T, fn: (value: T) => U): U;
114
+ /**
115
+ * Conditionally executes a step or block of code within a workflow.
116
+ * Returns undefined if the condition is false.
117
+ *
118
+ * @example
119
+ * const notification = await when(
120
+ * !!input.assignee_id,
121
+ * () => notifyAssigneeStep({ issue_id: issue.id, assignee_id: input.assignee_id! })
122
+ * )
123
+ */
124
+ declare function when<T>(condition: boolean, fn: () => Promise<T>): Promise<T | undefined>;
125
+
126
+ export { StepResponse, WorkflowResponse, type WorkflowResult, type WorkflowRunner, type WorkflowTransactionStatus, createStep, createWorkflow, transform, when };
@@ -0,0 +1,126 @@
1
+ import { StepContext, MeridianContainer } from '@meridianjs/types';
2
+
3
+ /**
4
+ * Returned by a step's invoke function to provide a separate
5
+ * compensateInput (the data the compensate function receives on rollback).
6
+ *
7
+ * If the invoke function returns a plain value (not a StepResponse),
8
+ * that value is used as both the step output AND the compensate input.
9
+ *
10
+ * @example
11
+ * async function invoke(input, ctx) {
12
+ * const record = await svc.create(input)
13
+ * return new StepResponse(record, { id: record.id }) // slim compensate input
14
+ * }
15
+ * async function compensate({ id }, ctx) {
16
+ * await svc.delete(id)
17
+ * }
18
+ */
19
+ declare class StepResponse<TOutput, TCompInput = TOutput> {
20
+ readonly output: TOutput;
21
+ readonly compensateInput: TCompInput;
22
+ constructor(output: TOutput, compensateInput: TCompInput);
23
+ }
24
+
25
+ /**
26
+ * Wraps the final return value of a workflow constructor function.
27
+ * The `output` becomes the `result` of the workflow run.
28
+ *
29
+ * @example
30
+ * createWorkflow("create-project", async (input) => {
31
+ * const project = await createProjectStep(input)
32
+ * return new WorkflowResponse(project)
33
+ * })
34
+ */
35
+ declare class WorkflowResponse<T> {
36
+ readonly output: T;
37
+ constructor(output: T);
38
+ }
39
+
40
+ type InvokeFn<TInput, TOutput, TCompInput> = (input: TInput, ctx: StepContext) => Promise<TOutput | StepResponse<TOutput, TCompInput>>;
41
+ type CompensateFn<TCompInput> = (input: TCompInput, ctx: StepContext) => Promise<void>;
42
+ /**
43
+ * Defines a workflow step with an optional compensation (rollback) function.
44
+ *
45
+ * Returns a step function `(input: TInput) => Promise<TOutput>`.
46
+ * When called inside a workflow, automatically registers the compensation
47
+ * on the LIFO stack so it runs if a later step fails.
48
+ *
49
+ * @example
50
+ * const createProjectStep = createStep(
51
+ * "create-project",
52
+ * async (input: CreateProjectInput, { container }) => {
53
+ * const svc = container.resolve("projectModuleService") as any
54
+ * const project = await svc.createProject(input)
55
+ * return new StepResponse(project, { projectId: project.id })
56
+ * },
57
+ * async ({ projectId }, { container }) => {
58
+ * const svc = container.resolve("projectModuleService") as any
59
+ * await svc.deleteProject(projectId)
60
+ * }
61
+ * )
62
+ */
63
+ declare function createStep<TInput, TOutput, TCompInput = TOutput>(name: string, invoke: InvokeFn<TInput, TOutput, TCompInput>, compensate?: CompensateFn<TCompInput>): (input: TInput) => Promise<TOutput>;
64
+
65
+ type WorkflowTransactionStatus = "done" | "reverted" | "failed";
66
+ interface WorkflowResult<TOutput> {
67
+ result: TOutput;
68
+ errors: Error[];
69
+ transaction_status: WorkflowTransactionStatus;
70
+ }
71
+ interface WorkflowRunner<TInput, TOutput> {
72
+ run(opts: {
73
+ input: TInput;
74
+ }): Promise<WorkflowResult<TOutput>>;
75
+ }
76
+ /**
77
+ * Defines a named workflow with a constructor function that orchestrates steps.
78
+ *
79
+ * Returns a factory `(container) => WorkflowRunner` so each request gets
80
+ * its own isolated run context with a fresh compensation stack.
81
+ *
82
+ * If any step throws, compensations run in LIFO order (saga pattern).
83
+ * Compensation errors are collected but do not re-throw.
84
+ *
85
+ * @example
86
+ * export const createProjectWorkflow = createWorkflow(
87
+ * "create-project",
88
+ * async (input: CreateProjectInput) => {
89
+ * const project = await createProjectStep(input)
90
+ * const _ = await logActivityStep({ entity_type: "project", entity_id: project.id })
91
+ * return new WorkflowResponse(project)
92
+ * }
93
+ * )
94
+ *
95
+ * // In a route handler:
96
+ * const { result, errors, transaction_status } = await createProjectWorkflow(req.scope).run({ input: req.body })
97
+ */
98
+ declare function createWorkflow<TInput, TOutput>(name: string, constructorFn: (input: TInput) => Promise<WorkflowResponse<TOutput>>): (container: MeridianContainer) => WorkflowRunner<TInput, TOutput>;
99
+
100
+ /**
101
+ * Transforms a step output value using a mapping function.
102
+ * A thin utility to make data transformations explicit in workflow constructors.
103
+ *
104
+ * @example
105
+ * const project = await createProjectStep(input)
106
+ * const activityInput = transform(project, (p) => ({
107
+ * entity_type: "project" as const,
108
+ * entity_id: p.id,
109
+ * workspace_id: p.workspace_id,
110
+ * }))
111
+ * await logActivityStep(activityInput)
112
+ */
113
+ declare function transform<T, U>(input: T, fn: (value: T) => U): U;
114
+ /**
115
+ * Conditionally executes a step or block of code within a workflow.
116
+ * Returns undefined if the condition is false.
117
+ *
118
+ * @example
119
+ * const notification = await when(
120
+ * !!input.assignee_id,
121
+ * () => notifyAssigneeStep({ issue_id: issue.id, assignee_id: input.assignee_id! })
122
+ * )
123
+ */
124
+ declare function when<T>(condition: boolean, fn: () => Promise<T>): Promise<T | undefined>;
125
+
126
+ export { StepResponse, WorkflowResponse, type WorkflowResult, type WorkflowRunner, type WorkflowTransactionStatus, createStep, createWorkflow, transform, when };
package/dist/index.js ADDED
@@ -0,0 +1,139 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/index.ts
21
+ var index_exports = {};
22
+ __export(index_exports, {
23
+ StepResponse: () => StepResponse,
24
+ WorkflowResponse: () => WorkflowResponse,
25
+ createStep: () => createStep,
26
+ createWorkflow: () => createWorkflow,
27
+ transform: () => transform,
28
+ when: () => when
29
+ });
30
+ module.exports = __toCommonJS(index_exports);
31
+
32
+ // src/step-response.ts
33
+ var StepResponse = class {
34
+ constructor(output, compensateInput) {
35
+ this.output = output;
36
+ this.compensateInput = compensateInput;
37
+ }
38
+ };
39
+
40
+ // src/workflow-response.ts
41
+ var WorkflowResponse = class {
42
+ constructor(output) {
43
+ this.output = output;
44
+ }
45
+ };
46
+
47
+ // src/run-context.ts
48
+ var import_node_async_hooks = require("async_hooks");
49
+ var workflowRunContext = new import_node_async_hooks.AsyncLocalStorage();
50
+ function getWorkflowRunContext() {
51
+ const ctx = workflowRunContext.getStore();
52
+ if (!ctx) {
53
+ throw new Error(
54
+ "No active workflow context. Steps must be called inside a createWorkflow constructor function."
55
+ );
56
+ }
57
+ return ctx;
58
+ }
59
+
60
+ // src/create-step.ts
61
+ function createStep(name, invoke, compensate) {
62
+ return async (input) => {
63
+ const runCtx = getWorkflowRunContext();
64
+ const stepCtx = { container: runCtx.container };
65
+ const result = await invoke(input, stepCtx);
66
+ let output;
67
+ let compInput;
68
+ if (result instanceof StepResponse) {
69
+ output = result.output;
70
+ compInput = result.compensateInput;
71
+ } else {
72
+ output = result;
73
+ compInput = result;
74
+ }
75
+ if (compensate) {
76
+ const capturedCompInput = compInput;
77
+ runCtx.compensationStack.push(
78
+ async () => compensate(capturedCompInput, stepCtx)
79
+ );
80
+ }
81
+ return output;
82
+ };
83
+ }
84
+
85
+ // src/create-workflow.ts
86
+ function createWorkflow(name, constructorFn) {
87
+ return (container) => ({
88
+ async run({ input }) {
89
+ const context = {
90
+ container,
91
+ compensationStack: []
92
+ };
93
+ try {
94
+ const response = await workflowRunContext.run(
95
+ context,
96
+ () => constructorFn(input)
97
+ );
98
+ return {
99
+ result: response.output,
100
+ errors: [],
101
+ transaction_status: "done"
102
+ };
103
+ } catch (error) {
104
+ const errors = [error];
105
+ const stack = [...context.compensationStack].reverse();
106
+ for (const compensate of stack) {
107
+ try {
108
+ await compensate();
109
+ } catch (compError) {
110
+ errors.push(compError);
111
+ }
112
+ }
113
+ return {
114
+ result: void 0,
115
+ errors,
116
+ transaction_status: "reverted"
117
+ };
118
+ }
119
+ }
120
+ });
121
+ }
122
+
123
+ // src/transform.ts
124
+ function transform(input, fn) {
125
+ return fn(input);
126
+ }
127
+ async function when(condition, fn) {
128
+ if (condition) return fn();
129
+ return void 0;
130
+ }
131
+ // Annotate the CommonJS export names for ESM import in node:
132
+ 0 && (module.exports = {
133
+ StepResponse,
134
+ WorkflowResponse,
135
+ createStep,
136
+ createWorkflow,
137
+ transform,
138
+ when
139
+ });
package/dist/index.mjs ADDED
@@ -0,0 +1,107 @@
1
+ // src/step-response.ts
2
+ var StepResponse = class {
3
+ constructor(output, compensateInput) {
4
+ this.output = output;
5
+ this.compensateInput = compensateInput;
6
+ }
7
+ };
8
+
9
+ // src/workflow-response.ts
10
+ var WorkflowResponse = class {
11
+ constructor(output) {
12
+ this.output = output;
13
+ }
14
+ };
15
+
16
+ // src/run-context.ts
17
+ import { AsyncLocalStorage } from "async_hooks";
18
+ var workflowRunContext = new AsyncLocalStorage();
19
+ function getWorkflowRunContext() {
20
+ const ctx = workflowRunContext.getStore();
21
+ if (!ctx) {
22
+ throw new Error(
23
+ "No active workflow context. Steps must be called inside a createWorkflow constructor function."
24
+ );
25
+ }
26
+ return ctx;
27
+ }
28
+
29
+ // src/create-step.ts
30
+ function createStep(name, invoke, compensate) {
31
+ return async (input) => {
32
+ const runCtx = getWorkflowRunContext();
33
+ const stepCtx = { container: runCtx.container };
34
+ const result = await invoke(input, stepCtx);
35
+ let output;
36
+ let compInput;
37
+ if (result instanceof StepResponse) {
38
+ output = result.output;
39
+ compInput = result.compensateInput;
40
+ } else {
41
+ output = result;
42
+ compInput = result;
43
+ }
44
+ if (compensate) {
45
+ const capturedCompInput = compInput;
46
+ runCtx.compensationStack.push(
47
+ async () => compensate(capturedCompInput, stepCtx)
48
+ );
49
+ }
50
+ return output;
51
+ };
52
+ }
53
+
54
+ // src/create-workflow.ts
55
+ function createWorkflow(name, constructorFn) {
56
+ return (container) => ({
57
+ async run({ input }) {
58
+ const context = {
59
+ container,
60
+ compensationStack: []
61
+ };
62
+ try {
63
+ const response = await workflowRunContext.run(
64
+ context,
65
+ () => constructorFn(input)
66
+ );
67
+ return {
68
+ result: response.output,
69
+ errors: [],
70
+ transaction_status: "done"
71
+ };
72
+ } catch (error) {
73
+ const errors = [error];
74
+ const stack = [...context.compensationStack].reverse();
75
+ for (const compensate of stack) {
76
+ try {
77
+ await compensate();
78
+ } catch (compError) {
79
+ errors.push(compError);
80
+ }
81
+ }
82
+ return {
83
+ result: void 0,
84
+ errors,
85
+ transaction_status: "reverted"
86
+ };
87
+ }
88
+ }
89
+ });
90
+ }
91
+
92
+ // src/transform.ts
93
+ function transform(input, fn) {
94
+ return fn(input);
95
+ }
96
+ async function when(condition, fn) {
97
+ if (condition) return fn();
98
+ return void 0;
99
+ }
100
+ export {
101
+ StepResponse,
102
+ WorkflowResponse,
103
+ createStep,
104
+ createWorkflow,
105
+ transform,
106
+ when
107
+ };
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "@meridianjs/workflow-engine",
3
+ "version": "0.1.0",
4
+ "description": "Meridian workflow engine — DAG runner with LIFO saga compensation",
5
+ "main": "./dist/index.js",
6
+ "module": "./dist/index.mjs",
7
+ "types": "./dist/index.d.ts",
8
+ "exports": {
9
+ ".": {
10
+ "import": {
11
+ "types": "./dist/index.d.mts",
12
+ "default": "./dist/index.mjs"
13
+ },
14
+ "require": {
15
+ "types": "./dist/index.d.ts",
16
+ "default": "./dist/index.js"
17
+ }
18
+ }
19
+ },
20
+ "scripts": {
21
+ "build": "tsup src/index.ts --format esm,cjs --dts --clean",
22
+ "dev": "tsup src/index.ts --format esm,cjs --dts --watch",
23
+ "typecheck": "tsc --noEmit",
24
+ "clean": "rm -rf dist",
25
+ "prepublishOnly": "npm run build"
26
+ },
27
+ "dependencies": {
28
+ "@meridianjs/types": "^0.1.0"
29
+ },
30
+ "devDependencies": {
31
+ "tsup": "^8.3.5",
32
+ "typescript": "*"
33
+ },
34
+ "files": [
35
+ "dist"
36
+ ],
37
+ "publishConfig": {
38
+ "access": "public"
39
+ }
40
+ }