openworkflow 0.5.0 → 0.6.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.
- package/README.md +40 -347
- package/dist/{testing → backend-test}/backend.testsuite.d.ts +1 -1
- package/dist/backend-test/backend.testsuite.d.ts.map +1 -0
- package/dist/{testing → backend-test}/backend.testsuite.js +8 -9
- package/dist/backend-test/index.d.ts +2 -0
- package/dist/backend-test/index.d.ts.map +1 -0
- package/dist/{testing → backend-test}/index.js +0 -1
- package/dist/backend.js +0 -1
- package/dist/backend.testsuite.d.ts +20 -0
- package/dist/backend.testsuite.d.ts.map +1 -0
- package/dist/backend.testsuite.js +1090 -0
- package/dist/bin/openworkflow.js +0 -1
- package/dist/chaos.test.d.ts +2 -0
- package/dist/chaos.test.d.ts.map +1 -0
- package/dist/chaos.test.js +88 -0
- package/dist/client.js +0 -1
- package/dist/client.test.d.ts +2 -0
- package/dist/client.test.d.ts.map +1 -0
- package/dist/client.test.js +311 -0
- package/dist/core/duration.js +0 -1
- package/dist/core/duration.test.d.ts +2 -0
- package/dist/core/duration.test.d.ts.map +1 -0
- package/dist/core/duration.test.js +263 -0
- package/dist/core/error.js +0 -1
- package/dist/core/error.test.d.ts +2 -0
- package/dist/core/error.test.d.ts.map +1 -0
- package/dist/core/error.test.js +60 -0
- package/dist/core/json.js +0 -1
- package/dist/core/result.js +0 -1
- package/dist/core/result.test.d.ts +2 -0
- package/dist/core/result.test.d.ts.map +1 -0
- package/dist/core/result.test.js +11 -0
- package/dist/core/retry.js +0 -1
- package/dist/core/schema.js +0 -1
- package/dist/core/step.js +0 -1
- package/dist/core/step.test.d.ts +2 -0
- package/dist/core/step.test.d.ts.map +1 -0
- package/dist/core/step.test.js +266 -0
- package/dist/core/workflow.js +0 -1
- package/dist/core/workflow.test.d.ts +2 -0
- package/dist/core/workflow.test.d.ts.map +1 -0
- package/dist/core/workflow.test.js +113 -0
- package/dist/driver.d.ts +116 -0
- package/dist/driver.d.ts.map +1 -0
- package/dist/driver.js +1 -0
- package/dist/execution.js +0 -1
- package/dist/execution.test.d.ts +2 -0
- package/dist/execution.test.d.ts.map +1 -0
- package/dist/execution.test.js +381 -0
- package/dist/factory.d.ts +74 -0
- package/dist/factory.d.ts.map +1 -0
- package/dist/factory.js +72 -0
- package/dist/index.js +0 -1
- package/dist/internal.d.ts +4 -5
- package/dist/internal.d.ts.map +1 -1
- package/dist/internal.js +1 -4
- package/dist/{backend-sqlite/index.d.ts → node-sqlite/backend.d.ts} +14 -4
- package/dist/node-sqlite/backend.d.ts.map +1 -0
- package/dist/{backend-sqlite/index.js → node-sqlite/backend.js} +23 -5
- package/dist/node-sqlite/index.d.ts +11 -0
- package/dist/node-sqlite/index.d.ts.map +1 -0
- package/dist/node-sqlite/index.js +7 -0
- package/dist/{backend-sqlite → node-sqlite}/sqlite.d.ts +2 -3
- package/dist/node-sqlite/sqlite.d.ts.map +1 -0
- package/dist/{backend-sqlite → node-sqlite}/sqlite.js +0 -1
- package/dist/{pg → postgres}/backend.d.ts +3 -1
- package/dist/postgres/backend.d.ts.map +1 -0
- package/dist/{pg → postgres}/backend.js +5 -5
- package/dist/postgres/backend.test.d.ts +2 -0
- package/dist/postgres/backend.test.d.ts.map +1 -0
- package/dist/postgres/backend.test.js +19 -0
- package/dist/postgres/driver.d.ts +81 -0
- package/dist/postgres/driver.d.ts.map +1 -0
- package/dist/postgres/driver.js +63 -0
- package/dist/postgres/index.d.ts +11 -0
- package/dist/postgres/index.d.ts.map +1 -0
- package/dist/postgres/index.js +7 -0
- package/dist/postgres/internal.d.ts +2 -0
- package/dist/postgres/internal.d.ts.map +1 -0
- package/dist/postgres/internal.js +1 -0
- package/dist/postgres/postgres.d.ts.map +1 -0
- package/dist/{backend-postgres → postgres}/postgres.js +0 -1
- package/dist/postgres/postgres.test.d.ts +2 -0
- package/dist/postgres/postgres.test.d.ts.map +1 -0
- package/dist/postgres/postgres.test.js +45 -0
- package/dist/postgres/scripts/db-migrate.d.ts.map +1 -0
- package/dist/{pg → postgres}/scripts/db-migrate.js +0 -1
- package/dist/postgres/scripts/db-reset.d.ts.map +1 -0
- package/dist/{pg → postgres}/scripts/db-reset.js +0 -1
- package/dist/{pg → postgres}/scripts/squawk.d.ts.map +1 -1
- package/dist/{pg → postgres}/scripts/squawk.js +0 -1
- package/dist/postgres/vitest.global-setup.d.ts.map +1 -0
- package/dist/{pg → postgres}/vitest.global-setup.js +0 -1
- package/dist/postgres.d.ts +2 -0
- package/dist/postgres.d.ts.map +1 -0
- package/dist/postgres.js +1 -0
- package/dist/registry.js +0 -1
- package/dist/registry.test.d.ts +2 -0
- package/dist/registry.test.d.ts.map +1 -0
- package/dist/registry.test.js +109 -0
- package/dist/sqlite/backend.d.ts +3 -1
- package/dist/sqlite/backend.d.ts.map +1 -1
- package/dist/sqlite/backend.js +5 -5
- package/dist/sqlite/backend.test.d.ts +2 -0
- package/dist/sqlite/backend.test.d.ts.map +1 -0
- package/dist/sqlite/backend.test.js +50 -0
- package/dist/sqlite/driver.d.ts +79 -0
- package/dist/sqlite/driver.d.ts.map +1 -0
- package/dist/sqlite/driver.js +62 -0
- package/dist/sqlite/index.d.ts +12 -2
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/index.js +11 -3
- package/dist/sqlite/internal.d.ts +2 -0
- package/dist/sqlite/internal.d.ts.map +1 -0
- package/dist/sqlite/internal.js +1 -0
- package/dist/sqlite/sqlite.js +0 -1
- package/dist/sqlite/sqlite.test.d.ts +2 -0
- package/dist/sqlite/sqlite.test.d.ts.map +1 -0
- package/dist/sqlite/sqlite.test.js +171 -0
- package/dist/sqlite.d.ts +2 -0
- package/dist/sqlite.d.ts.map +1 -0
- package/dist/sqlite.js +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/worker.js +0 -1
- package/dist/worker.test.d.ts +2 -0
- package/dist/worker.test.d.ts.map +1 -0
- package/dist/worker.test.js +900 -0
- package/dist/workflow.js +0 -1
- package/dist/workflow.test.d.ts +2 -0
- package/dist/workflow.test.d.ts.map +1 -0
- package/dist/workflow.test.js +84 -0
- package/package.json +19 -5
- package/dist/backend-postgres/index.d.ts +0 -44
- package/dist/backend-postgres/index.d.ts.map +0 -1
- package/dist/backend-postgres/index.js +0 -535
- package/dist/backend-postgres/index.js.map +0 -1
- package/dist/backend-postgres/postgres.d.ts.map +0 -1
- package/dist/backend-postgres/postgres.js.map +0 -1
- package/dist/backend-sqlite/index.d.ts.map +0 -1
- package/dist/backend-sqlite/index.js.map +0 -1
- package/dist/backend-sqlite/sqlite.d.ts.map +0 -1
- package/dist/backend-sqlite/sqlite.js.map +0 -1
- package/dist/backend.js.map +0 -1
- package/dist/bin/openworkflow.js.map +0 -1
- package/dist/client.js.map +0 -1
- package/dist/config.d.ts +0 -34
- package/dist/config.d.ts.map +0 -1
- package/dist/config.js +0 -49
- package/dist/config.js.map +0 -1
- package/dist/core/duration.js.map +0 -1
- package/dist/core/error.js.map +0 -1
- package/dist/core/json.js.map +0 -1
- package/dist/core/result.js.map +0 -1
- package/dist/core/retry.js.map +0 -1
- package/dist/core/schema.js.map +0 -1
- package/dist/core/step.js.map +0 -1
- package/dist/core/workflow.js.map +0 -1
- package/dist/execution.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/internal.js.map +0 -1
- package/dist/pg/backend.d.ts.map +0 -1
- package/dist/pg/backend.js.map +0 -1
- package/dist/pg/index.d.ts +0 -3
- package/dist/pg/index.d.ts.map +0 -1
- package/dist/pg/index.js +0 -3
- package/dist/pg/index.js.map +0 -1
- package/dist/pg/postgres.d.ts +0 -42
- package/dist/pg/postgres.d.ts.map +0 -1
- package/dist/pg/postgres.js +0 -234
- package/dist/pg/postgres.js.map +0 -1
- package/dist/pg/scripts/db-migrate.d.ts.map +0 -1
- package/dist/pg/scripts/db-migrate.js.map +0 -1
- package/dist/pg/scripts/db-reset.d.ts.map +0 -1
- package/dist/pg/scripts/db-reset.js.map +0 -1
- package/dist/pg/scripts/squawk.js.map +0 -1
- package/dist/pg/vitest.global-setup.d.ts.map +0 -1
- package/dist/pg/vitest.global-setup.js.map +0 -1
- package/dist/registry.js.map +0 -1
- package/dist/sqlite/backend.js.map +0 -1
- package/dist/sqlite/index.js.map +0 -1
- package/dist/sqlite/sqlite.js.map +0 -1
- package/dist/testing/backend.testsuite.d.ts.map +0 -1
- package/dist/testing/backend.testsuite.js.map +0 -1
- package/dist/testing/index.d.ts +0 -2
- package/dist/testing/index.d.ts.map +0 -1
- package/dist/testing/index.js.map +0 -1
- package/dist/worker.js.map +0 -1
- package/dist/workflow.js.map +0 -1
- /package/dist/{backend-postgres → postgres}/postgres.d.ts +0 -0
- /package/dist/{pg → postgres}/scripts/db-migrate.d.ts +0 -0
- /package/dist/{pg → postgres}/scripts/db-reset.d.ts +0 -0
- /package/dist/{pg → postgres}/scripts/squawk.d.ts +0 -0
- /package/dist/{pg → postgres}/vitest.global-setup.d.ts +0 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow.test.d.ts","sourceRoot":"","sources":["../../core/workflow.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import { validateInput } from "./workflow.js";
|
|
2
|
+
import { describe, expect, test } from "vitest";
|
|
3
|
+
describe("validateInput", () => {
|
|
4
|
+
test("returns success with input when no schema provided (null)", async () => {
|
|
5
|
+
const input = { name: "test", value: 42 };
|
|
6
|
+
const result = await validateInput(null, input);
|
|
7
|
+
expect(result.success).toBe(true);
|
|
8
|
+
if (result.success) {
|
|
9
|
+
expect(result.value).toBe(input);
|
|
10
|
+
}
|
|
11
|
+
});
|
|
12
|
+
test("returns success with input when no schema provided (undefined)", async () => {
|
|
13
|
+
const input = "string input";
|
|
14
|
+
const result = await validateInput(undefined, input);
|
|
15
|
+
expect(result.success).toBe(true);
|
|
16
|
+
if (result.success) {
|
|
17
|
+
expect(result.value).toBe(input);
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
test("validates input successfully against schema", async () => {
|
|
21
|
+
const schema = createMockSchema({
|
|
22
|
+
validate: (input) => ({ value: input }),
|
|
23
|
+
});
|
|
24
|
+
const input = { name: "test" };
|
|
25
|
+
const result = await validateInput(schema, input);
|
|
26
|
+
expect(result.success).toBe(true);
|
|
27
|
+
if (result.success) {
|
|
28
|
+
expect(result.value).toEqual({ name: "test" });
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
test("transforms input using schema", async () => {
|
|
32
|
+
const schema = createMockSchema({
|
|
33
|
+
validate: (input) => ({ value: Number.parseInt(input, 10) }),
|
|
34
|
+
});
|
|
35
|
+
const result = await validateInput(schema, "42");
|
|
36
|
+
expect(result.success).toBe(true);
|
|
37
|
+
if (result.success) {
|
|
38
|
+
expect(result.value).toBe(42);
|
|
39
|
+
}
|
|
40
|
+
});
|
|
41
|
+
test("returns failure with error message when validation fails", async () => {
|
|
42
|
+
const schema = createMockSchema({
|
|
43
|
+
validate: () => ({
|
|
44
|
+
issues: [{ message: "Invalid input" }],
|
|
45
|
+
}),
|
|
46
|
+
});
|
|
47
|
+
const result = await validateInput(schema, "bad input");
|
|
48
|
+
expect(result.success).toBe(false);
|
|
49
|
+
if (!result.success) {
|
|
50
|
+
expect(result.error).toBe("Invalid input");
|
|
51
|
+
}
|
|
52
|
+
});
|
|
53
|
+
test("combines multiple validation error messages", async () => {
|
|
54
|
+
const schema = createMockSchema({
|
|
55
|
+
validate: () => ({
|
|
56
|
+
issues: [
|
|
57
|
+
{ message: "Invalid email format" },
|
|
58
|
+
{ message: "Age must be positive" },
|
|
59
|
+
],
|
|
60
|
+
}),
|
|
61
|
+
});
|
|
62
|
+
const result = await validateInput(schema, {
|
|
63
|
+
email: "invalid",
|
|
64
|
+
age: -5,
|
|
65
|
+
});
|
|
66
|
+
expect(result.success).toBe(false);
|
|
67
|
+
if (!result.success) {
|
|
68
|
+
expect(result.error).toBe("Invalid email format; Age must be positive");
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
test("returns generic message when issues array is empty", async () => {
|
|
72
|
+
const schema = createMockSchema({
|
|
73
|
+
validate: () => ({
|
|
74
|
+
issues: [],
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
const result = await validateInput(schema, "test");
|
|
78
|
+
expect(result.success).toBe(false);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
expect(result.error).toBe("Validation failed");
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
test("handles async schema validation", async () => {
|
|
84
|
+
const schema = createMockSchema({
|
|
85
|
+
validate: async (input) => {
|
|
86
|
+
await new Promise((resolve) => setTimeout(resolve, 1));
|
|
87
|
+
return { value: input.toUpperCase() };
|
|
88
|
+
},
|
|
89
|
+
});
|
|
90
|
+
const result = await validateInput(schema, "hello");
|
|
91
|
+
expect(result.success).toBe(true);
|
|
92
|
+
if (result.success) {
|
|
93
|
+
expect(result.value).toBe("HELLO");
|
|
94
|
+
}
|
|
95
|
+
});
|
|
96
|
+
test("handles undefined input when no schema", async () => {
|
|
97
|
+
// eslint-disable-next-line unicorn/no-useless-undefined
|
|
98
|
+
const result = await validateInput(null, undefined);
|
|
99
|
+
expect(result.success).toBe(true);
|
|
100
|
+
if (result.success) {
|
|
101
|
+
expect(result.value).toBeUndefined();
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
function createMockSchema(options) {
|
|
106
|
+
return {
|
|
107
|
+
"~standard": {
|
|
108
|
+
version: 1,
|
|
109
|
+
vendor: "test",
|
|
110
|
+
validate: options.validate,
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
package/dist/driver.d.ts
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Drizzle-style driver model for OpenWorkflow backends.
|
|
3
|
+
*
|
|
4
|
+
* This module provides a driver abstraction layer that separates the database
|
|
5
|
+
* client (driver) from the backend operations. It follows the Drizzle ORM
|
|
6
|
+
* pattern where:
|
|
7
|
+
*
|
|
8
|
+
* 1. A `Driver` wraps the low-level database client
|
|
9
|
+
* 2. A `DriverConfig` specifies connection and namespace options
|
|
10
|
+
* 3. The `openworkflow()` factory function creates an `OpenWorkflow` instance
|
|
11
|
+
*
|
|
12
|
+
* Example usage with PostgreSQL:
|
|
13
|
+
* ```ts
|
|
14
|
+
* import { openworkflow } from 'openworkflow';
|
|
15
|
+
* import { postgres } from 'openworkflow/postgres';
|
|
16
|
+
*
|
|
17
|
+
* // Simple usage with connection string
|
|
18
|
+
* const ow = openworkflow(postgres(process.env.DATABASE_URL));
|
|
19
|
+
*
|
|
20
|
+
* // With config options
|
|
21
|
+
* const ow = openworkflow(postgres({
|
|
22
|
+
* connection: process.env.DATABASE_URL,
|
|
23
|
+
* namespace: 'my-app',
|
|
24
|
+
* }));
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* Example usage with SQLite:
|
|
28
|
+
* ```ts
|
|
29
|
+
* import { openworkflow } from 'openworkflow';
|
|
30
|
+
* import { sqlite } from 'openworkflow/sqlite';
|
|
31
|
+
*
|
|
32
|
+
* const ow = openworkflow(sqlite(':memory:'));
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
import type { Backend } from "./backend.js";
|
|
36
|
+
/**
|
|
37
|
+
* A driver creates Backend instances from database connections.
|
|
38
|
+
* This is the core abstraction that allows OpenWorkflow to work with
|
|
39
|
+
* different database backends using the same API.
|
|
40
|
+
*
|
|
41
|
+
* Drivers are typically created using helper functions like:
|
|
42
|
+
* - `postgres()` from 'openworkflow/postgres'
|
|
43
|
+
* - `sqlite()` from 'openworkflow/sqlite'
|
|
44
|
+
*/
|
|
45
|
+
export interface Driver {
|
|
46
|
+
/**
|
|
47
|
+
* Unique identifier for this driver type.
|
|
48
|
+
*/
|
|
49
|
+
readonly dialect: DriverDialect;
|
|
50
|
+
/**
|
|
51
|
+
* Creates a new Backend instance from this driver.
|
|
52
|
+
* This method handles connection setup and migrations.
|
|
53
|
+
*/
|
|
54
|
+
createBackend(): Promise<Backend> | Backend;
|
|
55
|
+
/**
|
|
56
|
+
* The underlying database client, if available.
|
|
57
|
+
* This can be used for advanced operations or debugging.
|
|
58
|
+
*/
|
|
59
|
+
readonly $client?: unknown;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Supported driver dialects.
|
|
63
|
+
*/
|
|
64
|
+
export type DriverDialect = "postgres" | "sqlite";
|
|
65
|
+
/**
|
|
66
|
+
* Common configuration options for all drivers.
|
|
67
|
+
*/
|
|
68
|
+
export interface DriverConfig {
|
|
69
|
+
/**
|
|
70
|
+
* The namespace for workflow runs. Defaults to 'default'.
|
|
71
|
+
* This allows multiple applications to share the same database.
|
|
72
|
+
*/
|
|
73
|
+
namespace?: string;
|
|
74
|
+
/**
|
|
75
|
+
* Whether to automatically run migrations on connection.
|
|
76
|
+
* Defaults to true.
|
|
77
|
+
*/
|
|
78
|
+
runMigrations?: boolean;
|
|
79
|
+
/**
|
|
80
|
+
* Optional logger for debugging.
|
|
81
|
+
*/
|
|
82
|
+
logger?: DriverLogger;
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Logger interface for driver operations.
|
|
86
|
+
*/
|
|
87
|
+
export interface DriverLogger {
|
|
88
|
+
/**
|
|
89
|
+
* Log a message at info level.
|
|
90
|
+
*/
|
|
91
|
+
logQuery?(query: string, params?: unknown[]): void;
|
|
92
|
+
/**
|
|
93
|
+
* Log a message at error level.
|
|
94
|
+
*/
|
|
95
|
+
error?(message: string, error?: unknown): void;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Configuration options for the openworkflow factory function.
|
|
99
|
+
*/
|
|
100
|
+
export interface OpenWorkflowConfig {
|
|
101
|
+
/**
|
|
102
|
+
* Optional logger for debugging.
|
|
103
|
+
*/
|
|
104
|
+
logger?: DriverLogger;
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* A driver that is ready to create backends.
|
|
108
|
+
* This type is used to ensure the driver has been properly configured.
|
|
109
|
+
*/
|
|
110
|
+
export interface PreparedDriver<TClient = unknown> extends Driver {
|
|
111
|
+
/**
|
|
112
|
+
* The underlying database client.
|
|
113
|
+
*/
|
|
114
|
+
readonly $client: TClient;
|
|
115
|
+
}
|
|
116
|
+
//# sourceMappingURL=driver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"driver.d.ts","sourceRoot":"","sources":["../driver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C;;;;;;;;GAQG;AACH,MAAM,WAAW,MAAM;IACrB;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,aAAa,CAAC;IAEhC;;;OAGG;IACH,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,GAAG,OAAO,CAAC;IAE5C;;;OAGG;IACH,QAAQ,CAAC,OAAO,CAAC,EAAE,OAAO,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,MAAM,aAAa,GAAG,UAAU,GAAG,QAAQ,CAAC;AAElD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,SAAS,CAAC,EAAE,MAAM,CAAC;IAEnB;;;OAGG;IACH,aAAa,CAAC,EAAE,OAAO,CAAC;IAExB;;OAEG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;OAEG;IACH,QAAQ,CAAC,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,CAAC;IAEnD;;OAEG;IACH,KAAK,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO,GAAG,IAAI,CAAC;CAChD;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;OAEG;IACH,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAED;;;GAGG;AACH,MAAM,WAAW,cAAc,CAAC,OAAO,GAAG,OAAO,CAAE,SAAQ,MAAM;IAC/D;;OAEG;IACH,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;CAC3B"}
|
package/dist/driver.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
package/dist/execution.js
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"execution.test.d.ts","sourceRoot":"","sources":["../execution.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,381 @@
|
|
|
1
|
+
import { OpenWorkflow } from "./client.js";
|
|
2
|
+
import { BackendPostgres } from "./postgres.js";
|
|
3
|
+
import { DEFAULT_POSTGRES_URL } from "./postgres/postgres.js";
|
|
4
|
+
import { randomUUID } from "node:crypto";
|
|
5
|
+
import { describe, test, expect } from "vitest";
|
|
6
|
+
describe("StepExecutor", () => {
|
|
7
|
+
test("executes step and returns result", async () => {
|
|
8
|
+
const backend = await createBackend();
|
|
9
|
+
const client = new OpenWorkflow({ backend });
|
|
10
|
+
const workflow = client.defineWorkflow({ name: "executor-basic" }, async ({ step }) => {
|
|
11
|
+
const result = await step.run({ name: "add" }, () => 5 + 3);
|
|
12
|
+
return result;
|
|
13
|
+
});
|
|
14
|
+
const worker = client.newWorker();
|
|
15
|
+
const handle = await workflow.run();
|
|
16
|
+
await worker.tick();
|
|
17
|
+
const result = await handle.result();
|
|
18
|
+
expect(result).toBe(8);
|
|
19
|
+
});
|
|
20
|
+
test("caches step results for same step name", async () => {
|
|
21
|
+
const backend = await createBackend();
|
|
22
|
+
const client = new OpenWorkflow({ backend });
|
|
23
|
+
let executionCount = 0;
|
|
24
|
+
const workflow = client.defineWorkflow({ name: "executor-cached" }, async ({ step }) => {
|
|
25
|
+
const first = await step.run({ name: "cached-step" }, () => {
|
|
26
|
+
executionCount++;
|
|
27
|
+
return "first-execution";
|
|
28
|
+
});
|
|
29
|
+
const second = await step.run({ name: "cached-step" }, () => {
|
|
30
|
+
executionCount++;
|
|
31
|
+
return "second-execution";
|
|
32
|
+
});
|
|
33
|
+
return { first, second };
|
|
34
|
+
});
|
|
35
|
+
const worker = client.newWorker();
|
|
36
|
+
const handle = await workflow.run();
|
|
37
|
+
await worker.tick();
|
|
38
|
+
const result = await handle.result();
|
|
39
|
+
expect(result).toEqual({
|
|
40
|
+
first: "first-execution",
|
|
41
|
+
second: "first-execution",
|
|
42
|
+
});
|
|
43
|
+
expect(executionCount).toBe(1);
|
|
44
|
+
});
|
|
45
|
+
test("different step names execute independently", async () => {
|
|
46
|
+
const backend = await createBackend();
|
|
47
|
+
const client = new OpenWorkflow({ backend });
|
|
48
|
+
let executionCount = 0;
|
|
49
|
+
const workflow = client.defineWorkflow({ name: "executor-different-steps" }, async ({ step }) => {
|
|
50
|
+
const first = await step.run({ name: "step-1" }, () => {
|
|
51
|
+
executionCount++;
|
|
52
|
+
return "a";
|
|
53
|
+
});
|
|
54
|
+
const second = await step.run({ name: "step-2" }, () => {
|
|
55
|
+
executionCount++;
|
|
56
|
+
return "b";
|
|
57
|
+
});
|
|
58
|
+
return { first, second };
|
|
59
|
+
});
|
|
60
|
+
const worker = client.newWorker();
|
|
61
|
+
const handle = await workflow.run();
|
|
62
|
+
await worker.tick();
|
|
63
|
+
const result = await handle.result();
|
|
64
|
+
expect(result).toEqual({ first: "a", second: "b" });
|
|
65
|
+
expect(executionCount).toBe(2);
|
|
66
|
+
});
|
|
67
|
+
test("propagates step errors with deadline exceeded", async () => {
|
|
68
|
+
const backend = await createBackend();
|
|
69
|
+
const client = new OpenWorkflow({ backend });
|
|
70
|
+
const workflow = client.defineWorkflow({ name: "executor-error" }, async ({ step }) => {
|
|
71
|
+
await step.run({ name: "failing-step" }, () => {
|
|
72
|
+
throw new Error("Step failed intentionally");
|
|
73
|
+
});
|
|
74
|
+
return "should not reach";
|
|
75
|
+
});
|
|
76
|
+
const worker = client.newWorker();
|
|
77
|
+
// Use deadline to force immediate failure without retries
|
|
78
|
+
const handle = await workflow.run({}, { deadlineAt: new Date() });
|
|
79
|
+
await worker.tick();
|
|
80
|
+
await sleep(100);
|
|
81
|
+
await expect(handle.result()).rejects.toThrow(/deadline exceeded/);
|
|
82
|
+
});
|
|
83
|
+
test("sleep puts workflow in sleeping status", async () => {
|
|
84
|
+
const backend = await createBackend();
|
|
85
|
+
const client = new OpenWorkflow({ backend });
|
|
86
|
+
const workflow = client.defineWorkflow({ name: "executor-sleep" }, async ({ step }) => {
|
|
87
|
+
await step.sleep("sleep-1", "5s");
|
|
88
|
+
return "after sleep";
|
|
89
|
+
});
|
|
90
|
+
const handle = await workflow.run();
|
|
91
|
+
const worker = client.newWorker();
|
|
92
|
+
await worker.tick();
|
|
93
|
+
const workflowRun = await backend.getWorkflowRun({
|
|
94
|
+
workflowRunId: handle.workflowRun.id,
|
|
95
|
+
});
|
|
96
|
+
expect(workflowRun?.status).toBe("sleeping");
|
|
97
|
+
expect(workflowRun?.availableAt).not.toBeNull();
|
|
98
|
+
});
|
|
99
|
+
test("workflow resumes after sleep duration", async () => {
|
|
100
|
+
const backend = await createBackend();
|
|
101
|
+
const client = new OpenWorkflow({ backend });
|
|
102
|
+
const workflow = client.defineWorkflow({ name: "resume-after-sleep" }, async ({ step }) => {
|
|
103
|
+
const value = await step.run({ name: "before" }, () => 5);
|
|
104
|
+
await step.sleep("wait", "10ms");
|
|
105
|
+
return value + 10;
|
|
106
|
+
});
|
|
107
|
+
const handle = await workflow.run();
|
|
108
|
+
const worker = client.newWorker();
|
|
109
|
+
// First tick - hits sleep
|
|
110
|
+
await worker.tick();
|
|
111
|
+
await sleep(50); // Wait for tick to complete
|
|
112
|
+
const sleeping = await backend.getWorkflowRun({
|
|
113
|
+
workflowRunId: handle.workflowRun.id,
|
|
114
|
+
});
|
|
115
|
+
expect(sleeping?.status).toBe("sleeping");
|
|
116
|
+
// Wait for sleep to elapse
|
|
117
|
+
await sleep(50);
|
|
118
|
+
// Second tick - completes
|
|
119
|
+
await worker.tick();
|
|
120
|
+
const result = await handle.result();
|
|
121
|
+
expect(result).toBe(15);
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
describe("executeWorkflow", () => {
|
|
125
|
+
describe("successful execution", () => {
|
|
126
|
+
test("executes a simple workflow", async () => {
|
|
127
|
+
const backend = await createBackend();
|
|
128
|
+
const client = new OpenWorkflow({ backend });
|
|
129
|
+
const workflow = client.defineWorkflow({ name: "simple-workflow" }, ({ input }) => {
|
|
130
|
+
return input.a + input.b;
|
|
131
|
+
});
|
|
132
|
+
const worker = client.newWorker();
|
|
133
|
+
const handle = await workflow.run({ a: 10, b: 5 });
|
|
134
|
+
await worker.tick();
|
|
135
|
+
const result = await handle.result();
|
|
136
|
+
expect(result).toBe(15);
|
|
137
|
+
});
|
|
138
|
+
test("executes a multi-step workflow", async () => {
|
|
139
|
+
const backend = await createBackend();
|
|
140
|
+
const client = new OpenWorkflow({ backend });
|
|
141
|
+
const workflow = client.defineWorkflow({ name: "multi-step-workflow" }, async ({ input, step }) => {
|
|
142
|
+
const sum = await step.run({ name: "add" }, () => input.value + 5);
|
|
143
|
+
const product = await step.run({ name: "multiply" }, () => sum * 2);
|
|
144
|
+
return product;
|
|
145
|
+
});
|
|
146
|
+
const worker = client.newWorker();
|
|
147
|
+
const handle = await workflow.run({ value: 10 });
|
|
148
|
+
await worker.tick();
|
|
149
|
+
const result = await handle.result();
|
|
150
|
+
expect(result).toBe(30);
|
|
151
|
+
});
|
|
152
|
+
test("returns null for workflows without return", async () => {
|
|
153
|
+
const backend = await createBackend();
|
|
154
|
+
const client = new OpenWorkflow({ backend });
|
|
155
|
+
const workflow = client.defineWorkflow({ name: "void-workflow" }, () => null);
|
|
156
|
+
const worker = client.newWorker();
|
|
157
|
+
const handle = await workflow.run();
|
|
158
|
+
await worker.tick();
|
|
159
|
+
const result = await handle.result();
|
|
160
|
+
expect(result).toBeNull();
|
|
161
|
+
});
|
|
162
|
+
test("returns null from workflow", async () => {
|
|
163
|
+
const backend = await createBackend();
|
|
164
|
+
const client = new OpenWorkflow({ backend });
|
|
165
|
+
const workflow = client.defineWorkflow({ name: "null-workflow" }, () => null);
|
|
166
|
+
const worker = client.newWorker();
|
|
167
|
+
const handle = await workflow.run();
|
|
168
|
+
await worker.tick();
|
|
169
|
+
const result = await handle.result();
|
|
170
|
+
expect(result).toBeNull();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
describe("error handling", () => {
|
|
174
|
+
test("handles workflow errors with deadline exceeded", async () => {
|
|
175
|
+
const backend = await createBackend();
|
|
176
|
+
const client = new OpenWorkflow({ backend });
|
|
177
|
+
const workflow = client.defineWorkflow({ name: "failing-workflow" }, () => {
|
|
178
|
+
throw new Error("Workflow error");
|
|
179
|
+
});
|
|
180
|
+
const worker = client.newWorker();
|
|
181
|
+
// Use deadline to skip retries - fails with deadline exceeded
|
|
182
|
+
const handle = await workflow.run({}, { deadlineAt: new Date() });
|
|
183
|
+
await worker.tick();
|
|
184
|
+
await sleep(100);
|
|
185
|
+
await expect(handle.result()).rejects.toThrow(/deadline exceeded/);
|
|
186
|
+
});
|
|
187
|
+
test("handles step errors with deadline exceeded", async () => {
|
|
188
|
+
const backend = await createBackend();
|
|
189
|
+
const client = new OpenWorkflow({ backend });
|
|
190
|
+
const workflow = client.defineWorkflow({ name: "step-error-workflow" }, async ({ step }) => {
|
|
191
|
+
await step.run({ name: "failing" }, () => {
|
|
192
|
+
throw new Error("Step error");
|
|
193
|
+
});
|
|
194
|
+
return "unreachable";
|
|
195
|
+
});
|
|
196
|
+
const worker = client.newWorker();
|
|
197
|
+
const handle = await workflow.run({}, { deadlineAt: new Date() });
|
|
198
|
+
await worker.tick();
|
|
199
|
+
await sleep(100);
|
|
200
|
+
await expect(handle.result()).rejects.toThrow(/deadline exceeded/);
|
|
201
|
+
});
|
|
202
|
+
test("serializes non-Error exceptions", async () => {
|
|
203
|
+
const backend = await createBackend();
|
|
204
|
+
const client = new OpenWorkflow({ backend });
|
|
205
|
+
const workflow = client.defineWorkflow({ name: "non-error-workflow" }, async ({ step }) => {
|
|
206
|
+
await step.run({ name: "throw-object" }, () => {
|
|
207
|
+
// eslint-disable-next-line @typescript-eslint/only-throw-error
|
|
208
|
+
throw { custom: "error", code: 500 };
|
|
209
|
+
});
|
|
210
|
+
return "nope";
|
|
211
|
+
});
|
|
212
|
+
const worker = client.newWorker();
|
|
213
|
+
const handle = await workflow.run({}, { deadlineAt: new Date() });
|
|
214
|
+
await worker.tick();
|
|
215
|
+
await sleep(100);
|
|
216
|
+
await expect(handle.result()).rejects.toThrow();
|
|
217
|
+
});
|
|
218
|
+
});
|
|
219
|
+
describe("sleep handling", () => {
|
|
220
|
+
test("workflow enters sleeping status", async () => {
|
|
221
|
+
const backend = await createBackend();
|
|
222
|
+
const client = new OpenWorkflow({ backend });
|
|
223
|
+
const workflow = client.defineWorkflow({ name: "sleep-workflow" }, async ({ step }) => {
|
|
224
|
+
await step.sleep("wait", "5s");
|
|
225
|
+
return "after sleep";
|
|
226
|
+
});
|
|
227
|
+
const handle = await workflow.run();
|
|
228
|
+
const worker = client.newWorker();
|
|
229
|
+
await worker.tick();
|
|
230
|
+
const workflowRun = await backend.getWorkflowRun({
|
|
231
|
+
workflowRunId: handle.workflowRun.id,
|
|
232
|
+
});
|
|
233
|
+
expect(workflowRun?.status).toBe("sleeping");
|
|
234
|
+
});
|
|
235
|
+
test("resumes workflow after sleep duration", async () => {
|
|
236
|
+
const backend = await createBackend();
|
|
237
|
+
const client = new OpenWorkflow({ backend });
|
|
238
|
+
const workflow = client.defineWorkflow({ name: "resume-after-sleep" }, async ({ input, step }) => {
|
|
239
|
+
const sum = await step.run({ name: "add" }, () => input.value + 1);
|
|
240
|
+
await step.sleep("wait", "10ms");
|
|
241
|
+
return sum + 10;
|
|
242
|
+
});
|
|
243
|
+
const handle = await workflow.run({ value: 5 });
|
|
244
|
+
const worker = client.newWorker();
|
|
245
|
+
// first tick - hits sleep
|
|
246
|
+
await worker.tick();
|
|
247
|
+
await sleep(50);
|
|
248
|
+
const sleeping = await backend.getWorkflowRun({
|
|
249
|
+
workflowRunId: handle.workflowRun.id,
|
|
250
|
+
});
|
|
251
|
+
expect(sleeping?.status).toBe("sleeping");
|
|
252
|
+
// wait for sleep
|
|
253
|
+
await sleep(50);
|
|
254
|
+
await worker.tick();
|
|
255
|
+
const result = await handle.result();
|
|
256
|
+
expect(result).toBe(16);
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
describe("workflow with complex data", () => {
|
|
260
|
+
test("handles objects as input and output", async () => {
|
|
261
|
+
const backend = await createBackend();
|
|
262
|
+
const client = new OpenWorkflow({ backend });
|
|
263
|
+
const workflow = client.defineWorkflow({ name: "user-workflow" }, ({ input }) => {
|
|
264
|
+
return {
|
|
265
|
+
greeting: `Hello, ${input.name}! You are ${String(input.age)} years old.`,
|
|
266
|
+
processed: true,
|
|
267
|
+
};
|
|
268
|
+
});
|
|
269
|
+
const worker = client.newWorker();
|
|
270
|
+
const handle = await workflow.run({ name: "Alice", age: 30 });
|
|
271
|
+
await worker.tick();
|
|
272
|
+
const result = await handle.result();
|
|
273
|
+
expect(result).toEqual({
|
|
274
|
+
greeting: "Hello, Alice! You are 30 years old.",
|
|
275
|
+
processed: true,
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
test("handles arrays in workflow", async () => {
|
|
279
|
+
const backend = await createBackend();
|
|
280
|
+
const client = new OpenWorkflow({ backend });
|
|
281
|
+
const workflow = client.defineWorkflow({ name: "array-workflow" }, ({ input }) => {
|
|
282
|
+
return input.numbers.reduce((a, b) => a + b, 0);
|
|
283
|
+
});
|
|
284
|
+
const worker = client.newWorker();
|
|
285
|
+
const handle = await workflow.run({ numbers: [1, 2, 3, 4, 5] });
|
|
286
|
+
await worker.tick();
|
|
287
|
+
const result = await handle.result();
|
|
288
|
+
expect(result).toBe(15);
|
|
289
|
+
});
|
|
290
|
+
});
|
|
291
|
+
describe("result type handling", () => {
|
|
292
|
+
test("returns success with numeric result", async () => {
|
|
293
|
+
const backend = await createBackend();
|
|
294
|
+
const client = new OpenWorkflow({ backend });
|
|
295
|
+
const workflow = client.defineWorkflow({ name: "numeric-result" }, async ({ step }) => {
|
|
296
|
+
return await step.run({ name: "compute" }, () => 100 + 200);
|
|
297
|
+
});
|
|
298
|
+
const worker = client.newWorker();
|
|
299
|
+
const handle = await workflow.run();
|
|
300
|
+
await worker.tick();
|
|
301
|
+
const result = await handle.result();
|
|
302
|
+
expect(result).toBe(300);
|
|
303
|
+
});
|
|
304
|
+
test("returns success with string result", async () => {
|
|
305
|
+
const backend = await createBackend();
|
|
306
|
+
const client = new OpenWorkflow({ backend });
|
|
307
|
+
const workflow = client.defineWorkflow({ name: "string-result" }, ({ input }) => {
|
|
308
|
+
return input.text.toUpperCase();
|
|
309
|
+
});
|
|
310
|
+
const worker = client.newWorker();
|
|
311
|
+
const handle = await workflow.run({ text: "hello world" });
|
|
312
|
+
await worker.tick();
|
|
313
|
+
const result = await handle.result();
|
|
314
|
+
expect(result).toBe("HELLO WORLD");
|
|
315
|
+
});
|
|
316
|
+
test("returns success with boolean result", async () => {
|
|
317
|
+
const backend = await createBackend();
|
|
318
|
+
const client = new OpenWorkflow({ backend });
|
|
319
|
+
const workflow = client.defineWorkflow({ name: "bool-result" }, ({ input }) => {
|
|
320
|
+
return input.value > 0;
|
|
321
|
+
});
|
|
322
|
+
const worker = client.newWorker();
|
|
323
|
+
const handle = await workflow.run({ value: 42 });
|
|
324
|
+
await worker.tick();
|
|
325
|
+
const result = await handle.result();
|
|
326
|
+
expect(result).toBe(true);
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
describe("step execution order", () => {
|
|
330
|
+
test("executes steps in sequence", async () => {
|
|
331
|
+
const backend = await createBackend();
|
|
332
|
+
const client = new OpenWorkflow({ backend });
|
|
333
|
+
const order = [];
|
|
334
|
+
const workflow = client.defineWorkflow({ name: "sequence-workflow" }, async ({ step }) => {
|
|
335
|
+
await step.run({ name: "first" }, () => order.push("first"));
|
|
336
|
+
await step.run({ name: "second" }, () => order.push("second"));
|
|
337
|
+
await step.run({ name: "third" }, () => order.push("third"));
|
|
338
|
+
return order;
|
|
339
|
+
});
|
|
340
|
+
const worker = client.newWorker();
|
|
341
|
+
const handle = await workflow.run();
|
|
342
|
+
await worker.tick();
|
|
343
|
+
const result = await handle.result();
|
|
344
|
+
expect(result).toEqual(["first", "second", "third"]);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
describe("version handling", () => {
|
|
348
|
+
test("passes version to workflow function", async () => {
|
|
349
|
+
const backend = await createBackend();
|
|
350
|
+
const client = new OpenWorkflow({ backend });
|
|
351
|
+
const workflow = client.defineWorkflow({ name: "version-workflow", version: "1.0.0" }, ({ version }) => {
|
|
352
|
+
return { receivedVersion: version };
|
|
353
|
+
});
|
|
354
|
+
const worker = client.newWorker();
|
|
355
|
+
const handle = await workflow.run();
|
|
356
|
+
await worker.tick();
|
|
357
|
+
const result = await handle.result();
|
|
358
|
+
expect(result).toEqual({ receivedVersion: "1.0.0" });
|
|
359
|
+
});
|
|
360
|
+
test("passes null version when not specified", async () => {
|
|
361
|
+
const backend = await createBackend();
|
|
362
|
+
const client = new OpenWorkflow({ backend });
|
|
363
|
+
const workflow = client.defineWorkflow({ name: "no-version-workflow" }, ({ version }) => {
|
|
364
|
+
return { receivedVersion: version };
|
|
365
|
+
});
|
|
366
|
+
const worker = client.newWorker();
|
|
367
|
+
const handle = await workflow.run();
|
|
368
|
+
await worker.tick();
|
|
369
|
+
const result = await handle.result();
|
|
370
|
+
expect(result).toEqual({ receivedVersion: null });
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
});
|
|
374
|
+
async function createBackend() {
|
|
375
|
+
return await BackendPostgres.connect(DEFAULT_POSTGRES_URL, {
|
|
376
|
+
namespaceId: randomUUID(),
|
|
377
|
+
});
|
|
378
|
+
}
|
|
379
|
+
function sleep(ms) {
|
|
380
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
381
|
+
}
|