convex-effect-workflows 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.
- package/README.md +107 -0
- package/dist/client/ConvexCtx.d.ts +12 -0
- package/dist/client/ConvexCtx.d.ts.map +1 -0
- package/dist/client/ConvexCtx.js +6 -0
- package/dist/client/ConvexCtx.js.map +1 -0
- package/dist/client/ConvexLogger.d.ts +7 -0
- package/dist/client/ConvexLogger.d.ts.map +1 -0
- package/dist/client/ConvexLogger.js +39 -0
- package/dist/client/ConvexLogger.js.map +1 -0
- package/dist/client/ConvexTracer.d.ts +7 -0
- package/dist/client/ConvexTracer.d.ts.map +1 -0
- package/dist/client/ConvexTracer.js +60 -0
- package/dist/client/ConvexTracer.js.map +1 -0
- package/dist/client/ConvexWorkflowEngine.d.ts +308 -0
- package/dist/client/ConvexWorkflowEngine.d.ts.map +1 -0
- package/dist/client/ConvexWorkflowEngine.js +88 -0
- package/dist/client/ConvexWorkflowEngine.js.map +1 -0
- package/dist/client/activityWorker.d.ts +23 -0
- package/dist/client/activityWorker.d.ts.map +1 -0
- package/dist/client/activityWorker.js +41 -0
- package/dist/client/activityWorker.js.map +1 -0
- package/dist/client/boundaries.d.ts +27 -0
- package/dist/client/boundaries.d.ts.map +1 -0
- package/dist/client/boundaries.js +17 -0
- package/dist/client/boundaries.js.map +1 -0
- package/dist/client/encoded.d.ts +22 -0
- package/dist/client/encoded.d.ts.map +1 -0
- package/dist/client/encoded.js +276 -0
- package/dist/client/encoded.js.map +1 -0
- package/dist/client/index.d.ts +13 -0
- package/dist/client/index.d.ts.map +1 -0
- package/dist/client/index.js +11 -0
- package/dist/client/index.js.map +1 -0
- package/dist/client/registry.d.ts +17 -0
- package/dist/client/registry.d.ts.map +1 -0
- package/dist/client/registry.js +21 -0
- package/dist/client/registry.js.map +1 -0
- package/dist/client/runner.d.ts +27 -0
- package/dist/client/runner.d.ts.map +1 -0
- package/dist/client/runner.js +90 -0
- package/dist/client/runner.js.map +1 -0
- package/dist/client/runtime.d.ts +10 -0
- package/dist/client/runtime.d.ts.map +1 -0
- package/dist/client/runtime.js +15 -0
- package/dist/client/runtime.js.map +1 -0
- package/dist/component/_generated/api.d.ts +148 -0
- package/dist/component/_generated/api.d.ts.map +1 -0
- package/dist/component/_generated/api.js +31 -0
- package/dist/component/_generated/api.js.map +1 -0
- package/dist/component/_generated/component.d.ts +921 -0
- package/dist/component/_generated/component.d.ts.map +1 -0
- package/dist/component/_generated/component.js +11 -0
- package/dist/component/_generated/component.js.map +1 -0
- package/dist/component/_generated/dataModel.d.ts +46 -0
- package/dist/component/_generated/dataModel.d.ts.map +1 -0
- package/dist/component/_generated/dataModel.js +11 -0
- package/dist/component/_generated/dataModel.js.map +1 -0
- package/dist/component/_generated/server.d.ts +121 -0
- package/dist/component/_generated/server.d.ts.map +1 -0
- package/dist/component/_generated/server.js +78 -0
- package/dist/component/_generated/server.js.map +1 -0
- package/dist/component/activityCompletions.d.ts +27 -0
- package/dist/component/activityCompletions.d.ts.map +1 -0
- package/dist/component/activityCompletions.js +70 -0
- package/dist/component/activityCompletions.js.map +1 -0
- package/dist/component/boundaries.d.ts +20 -0
- package/dist/component/boundaries.d.ts.map +1 -0
- package/dist/component/boundaries.js +17 -0
- package/dist/component/boundaries.js.map +1 -0
- package/dist/component/cleanup.d.ts +11 -0
- package/dist/component/cleanup.d.ts.map +1 -0
- package/dist/component/cleanup.js +163 -0
- package/dist/component/cleanup.js.map +1 -0
- package/dist/component/clocks.d.ts +12 -0
- package/dist/component/clocks.d.ts.map +1 -0
- package/dist/component/clocks.js +26 -0
- package/dist/component/clocks.js.map +1 -0
- package/dist/component/config.d.ts +25 -0
- package/dist/component/config.d.ts.map +1 -0
- package/dist/component/config.js +110 -0
- package/dist/component/config.js.map +1 -0
- package/dist/component/convex.config.d.ts +3 -0
- package/dist/component/convex.config.d.ts.map +1 -0
- package/dist/component/convex.config.js +6 -0
- package/dist/component/convex.config.js.map +1 -0
- package/dist/component/dashboard.d.ts +268 -0
- package/dist/component/dashboard.d.ts.map +1 -0
- package/dist/component/dashboard.js +622 -0
- package/dist/component/dashboard.js.map +1 -0
- package/dist/component/deferreds.d.ts +31 -0
- package/dist/component/deferreds.d.ts.map +1 -0
- package/dist/component/deferreds.js +138 -0
- package/dist/component/deferreds.js.map +1 -0
- package/dist/component/executions.d.ts +77 -0
- package/dist/component/executions.d.ts.map +1 -0
- package/dist/component/executions.js +186 -0
- package/dist/component/executions.js.map +1 -0
- package/dist/component/journalSteps.d.ts +261 -0
- package/dist/component/journalSteps.d.ts.map +1 -0
- package/dist/component/journalSteps.js +203 -0
- package/dist/component/journalSteps.js.map +1 -0
- package/dist/component/logs.d.ts +68 -0
- package/dist/component/logs.d.ts.map +1 -0
- package/dist/component/logs.js +123 -0
- package/dist/component/logs.js.map +1 -0
- package/dist/component/onComplete.d.ts +31 -0
- package/dist/component/onComplete.d.ts.map +1 -0
- package/dist/component/onComplete.js +146 -0
- package/dist/component/onComplete.js.map +1 -0
- package/dist/component/payloads.d.ts +26 -0
- package/dist/component/payloads.d.ts.map +1 -0
- package/dist/component/payloads.js +57 -0
- package/dist/component/payloads.js.map +1 -0
- package/dist/component/queries.d.ts +2 -0
- package/dist/component/queries.d.ts.map +1 -0
- package/dist/component/queries.js +2 -0
- package/dist/component/queries.js.map +1 -0
- package/dist/component/runner.d.ts +31 -0
- package/dist/component/runner.d.ts.map +1 -0
- package/dist/component/runner.js +87 -0
- package/dist/component/runner.js.map +1 -0
- package/dist/component/schema.d.ts +282 -0
- package/dist/component/schema.d.ts.map +1 -0
- package/dist/component/schema.js +119 -0
- package/dist/component/schema.js.map +1 -0
- package/dist/component/spans.d.ts +105 -0
- package/dist/component/spans.d.ts.map +1 -0
- package/dist/component/spans.js +190 -0
- package/dist/component/spans.js.map +1 -0
- package/dist/component/utils.d.ts +15 -0
- package/dist/component/utils.d.ts.map +1 -0
- package/dist/component/utils.js +53 -0
- package/dist/component/utils.js.map +1 -0
- package/dist/shared/constants.d.ts +12 -0
- package/dist/shared/constants.d.ts.map +1 -0
- package/dist/shared/constants.js +12 -0
- package/dist/shared/constants.js.map +1 -0
- package/dist/shared/validators.d.ts +69 -0
- package/dist/shared/validators.d.ts.map +1 -0
- package/dist/shared/validators.js +30 -0
- package/dist/shared/validators.js.map +1 -0
- package/package.json +74 -0
- package/src/client/ConvexCtx.ts +21 -0
- package/src/client/ConvexLogger.ts +52 -0
- package/src/client/ConvexTracer.ts +75 -0
- package/src/client/ConvexWorkflowEngine.test.ts +124 -0
- package/src/client/ConvexWorkflowEngine.ts +209 -0
- package/src/client/activityWorker.ts +62 -0
- package/src/client/boundaries.test.ts +83 -0
- package/src/client/boundaries.ts +79 -0
- package/src/client/encoded.lifecycle.test.ts +336 -0
- package/src/client/encoded.test.ts +153 -0
- package/src/client/encoded.ts +484 -0
- package/src/client/index.ts +47 -0
- package/src/client/registry.ts +35 -0
- package/src/client/runner.ts +165 -0
- package/src/client/runtime.ts +30 -0
- package/src/component/_generated/api.ts +179 -0
- package/src/component/_generated/component.ts +1216 -0
- package/src/component/_generated/dataModel.ts +60 -0
- package/src/component/_generated/server.ts +156 -0
- package/src/component/activityCompletions.ts +73 -0
- package/src/component/boundaries.ts +55 -0
- package/src/component/cleanup.test.ts +219 -0
- package/src/component/cleanup.ts +218 -0
- package/src/component/clocks.ts +26 -0
- package/src/component/config.test.ts +159 -0
- package/src/component/config.ts +145 -0
- package/src/component/convex.config.ts +7 -0
- package/src/component/core.test.ts +829 -0
- package/src/component/dashboard.scaling.test.ts +268 -0
- package/src/component/dashboard.ts +743 -0
- package/src/component/deferreds.ts +162 -0
- package/src/component/executions.ts +225 -0
- package/src/component/journalSteps.ts +252 -0
- package/src/component/logs.ts +152 -0
- package/src/component/onComplete.ts +170 -0
- package/src/component/payloads.ts +83 -0
- package/src/component/queries.ts +8 -0
- package/src/component/runner.ts +122 -0
- package/src/component/schema.ts +155 -0
- package/src/component/setup.test.ts +15 -0
- package/src/component/spans.ts +241 -0
- package/src/component/utils.test.ts +32 -0
- package/src/component/utils.ts +73 -0
- package/src/shared/constants.test.ts +14 -0
- package/src/shared/constants.ts +15 -0
- package/src/shared/validators.ts +98 -0
- package/src/test.d.ts +8 -0
- package/src/test.ts +17 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import * as Context from "effect/Context";
|
|
2
|
+
import * as Exit from "effect/Exit";
|
|
3
|
+
import * as Option from "effect/Option";
|
|
4
|
+
import * as Tracer from "effect/Tracer";
|
|
5
|
+
import type { GenericDataModel, GenericMutationCtx } from "convex/server";
|
|
6
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
7
|
+
|
|
8
|
+
type MutationRunCtx = Pick<GenericMutationCtx<GenericDataModel>, "runMutation">;
|
|
9
|
+
|
|
10
|
+
export function makeConvexTracer(
|
|
11
|
+
component: ComponentApi,
|
|
12
|
+
ctx: MutationRunCtx,
|
|
13
|
+
executionId: string,
|
|
14
|
+
traceId: string,
|
|
15
|
+
): Tracer.Tracer {
|
|
16
|
+
return Tracer.make({
|
|
17
|
+
span: (name, parent, _context, _links, _startTime, kind, options) => {
|
|
18
|
+
const parentSpanId = Option.match(parent, {
|
|
19
|
+
onNone: () => undefined,
|
|
20
|
+
onSome: (span) => span.spanId,
|
|
21
|
+
});
|
|
22
|
+
const spanId = crypto.randomUUID().replaceAll("-", "").slice(0, 16);
|
|
23
|
+
|
|
24
|
+
void ctx
|
|
25
|
+
.runMutation(component.spans.createSpan, {
|
|
26
|
+
executionId,
|
|
27
|
+
traceId,
|
|
28
|
+
spanId,
|
|
29
|
+
parentSpanId,
|
|
30
|
+
name,
|
|
31
|
+
kind: "system",
|
|
32
|
+
input: undefined,
|
|
33
|
+
attributes: options?.attributes,
|
|
34
|
+
})
|
|
35
|
+
.catch(() => {});
|
|
36
|
+
|
|
37
|
+
const attributes = new Map<string, unknown>(
|
|
38
|
+
options?.attributes ? Object.entries(options.attributes) : [],
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
const span: Tracer.Span = {
|
|
42
|
+
_tag: "Span",
|
|
43
|
+
name,
|
|
44
|
+
spanId,
|
|
45
|
+
traceId,
|
|
46
|
+
parent,
|
|
47
|
+
context: Context.empty(),
|
|
48
|
+
status: { _tag: "Started", startTime: 0n },
|
|
49
|
+
attributes,
|
|
50
|
+
links: [],
|
|
51
|
+
sampled: true,
|
|
52
|
+
kind,
|
|
53
|
+
end: (_endTime, exit) => {
|
|
54
|
+
void ctx
|
|
55
|
+
.runMutation(component.spans.endSpan, {
|
|
56
|
+
executionId,
|
|
57
|
+
spanId,
|
|
58
|
+
output: Exit.isSuccess(exit) ? exit.value : undefined,
|
|
59
|
+
error: Exit.isFailure(exit) ? String(exit.cause) : undefined,
|
|
60
|
+
attributes: Object.fromEntries(attributes.entries()),
|
|
61
|
+
})
|
|
62
|
+
.catch(() => {});
|
|
63
|
+
},
|
|
64
|
+
attribute: (key, value) => {
|
|
65
|
+
attributes.set(key, value);
|
|
66
|
+
},
|
|
67
|
+
event: () => {},
|
|
68
|
+
addLinks: () => {},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
return span;
|
|
72
|
+
},
|
|
73
|
+
context: (f) => f(),
|
|
74
|
+
});
|
|
75
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { ConvexWorkflowEngine } from "./ConvexWorkflowEngine.js";
|
|
3
|
+
|
|
4
|
+
describe("ConvexWorkflowEngine runner dispatch", () => {
|
|
5
|
+
test("start schedules component runner fan-in when execution is newly created", async () => {
|
|
6
|
+
const createExecutionRef = Symbol("createExecution");
|
|
7
|
+
const runnerRunRef = Symbol("runner.run");
|
|
8
|
+
|
|
9
|
+
const scheduled: Array<{ delay: number; fn: unknown; args: unknown }> = [];
|
|
10
|
+
|
|
11
|
+
const engine = new ConvexWorkflowEngine({
|
|
12
|
+
executions: { createExecution: createExecutionRef },
|
|
13
|
+
runner: { run: runnerRunRef },
|
|
14
|
+
} as any);
|
|
15
|
+
|
|
16
|
+
const ctx = {
|
|
17
|
+
runMutation: async (fn: unknown) => {
|
|
18
|
+
if (fn === createExecutionRef) {
|
|
19
|
+
return { created: true };
|
|
20
|
+
}
|
|
21
|
+
throw new Error("unexpected mutation");
|
|
22
|
+
},
|
|
23
|
+
runQuery: async () => null,
|
|
24
|
+
scheduler: {
|
|
25
|
+
runAfter: async (delay: number, fn: unknown, args: unknown) => {
|
|
26
|
+
scheduled.push({ delay, fn, args });
|
|
27
|
+
return "scheduled" as any;
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
} as any;
|
|
31
|
+
|
|
32
|
+
const executionId = await engine.start(ctx, {
|
|
33
|
+
workflowName: "orderWorkflow",
|
|
34
|
+
executionId: "exec-1",
|
|
35
|
+
payload: { id: 1 },
|
|
36
|
+
runner: "function://runner" as any,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
expect(executionId).toBe("exec-1");
|
|
40
|
+
expect(scheduled).toHaveLength(1);
|
|
41
|
+
expect(scheduled[0]).toEqual({
|
|
42
|
+
delay: 0,
|
|
43
|
+
fn: runnerRunRef,
|
|
44
|
+
args: { executionId: "exec-1", generation: 0 },
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test("start does not schedule runner when execution already exists", async () => {
|
|
49
|
+
const createExecutionRef = Symbol("createExecution");
|
|
50
|
+
const runnerRunRef = Symbol("runner.run");
|
|
51
|
+
|
|
52
|
+
const scheduled: Array<unknown> = [];
|
|
53
|
+
|
|
54
|
+
const engine = new ConvexWorkflowEngine({
|
|
55
|
+
executions: { createExecution: createExecutionRef },
|
|
56
|
+
runner: { run: runnerRunRef },
|
|
57
|
+
} as any);
|
|
58
|
+
|
|
59
|
+
const ctx = {
|
|
60
|
+
runMutation: async () => ({ created: false }),
|
|
61
|
+
runQuery: async () => null,
|
|
62
|
+
scheduler: {
|
|
63
|
+
runAfter: async (_delay: number, _fn: unknown, _args: unknown) => {
|
|
64
|
+
scheduled.push(true);
|
|
65
|
+
return "scheduled" as any;
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
} as any;
|
|
69
|
+
|
|
70
|
+
await engine.start(ctx, {
|
|
71
|
+
workflowName: "orderWorkflow",
|
|
72
|
+
executionId: "exec-2",
|
|
73
|
+
payload: { id: 2 },
|
|
74
|
+
runner: "function://runner" as any,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
expect(scheduled).toHaveLength(0);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
test("resume schedules runner fan-in for active execution and skips terminal", async () => {
|
|
81
|
+
const getExecutionRef = Symbol("getExecution");
|
|
82
|
+
const runnerRunRef = Symbol("runner.run");
|
|
83
|
+
|
|
84
|
+
const scheduled: Array<{ fn: unknown; args: unknown }> = [];
|
|
85
|
+
|
|
86
|
+
const engine = new ConvexWorkflowEngine({
|
|
87
|
+
executions: { getExecution: getExecutionRef },
|
|
88
|
+
runner: { run: runnerRunRef },
|
|
89
|
+
} as any);
|
|
90
|
+
|
|
91
|
+
const ctx = {
|
|
92
|
+
runMutation: async () => null,
|
|
93
|
+
runQuery: async (_fn: unknown, args: any) => {
|
|
94
|
+
if (args.executionId === "exec-active") {
|
|
95
|
+
return { status: "running", generation: 7 };
|
|
96
|
+
}
|
|
97
|
+
if (args.executionId === "exec-terminal") {
|
|
98
|
+
return { status: "completed", generation: 9 };
|
|
99
|
+
}
|
|
100
|
+
return null;
|
|
101
|
+
},
|
|
102
|
+
scheduler: {
|
|
103
|
+
runAfter: async (_delay: number, fn: unknown, args: unknown) => {
|
|
104
|
+
scheduled.push({ fn, args });
|
|
105
|
+
return "scheduled" as any;
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
} as any;
|
|
109
|
+
|
|
110
|
+
await engine.resume(ctx, {
|
|
111
|
+
executionId: "exec-active",
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
await engine.resume(ctx, {
|
|
115
|
+
executionId: "exec-terminal",
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
expect(scheduled).toHaveLength(1);
|
|
119
|
+
expect(scheduled[0]).toEqual({
|
|
120
|
+
fn: runnerRunRef,
|
|
121
|
+
args: { executionId: "exec-active", generation: 7 },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FunctionHandle,
|
|
3
|
+
GenericDataModel,
|
|
4
|
+
GenericMutationCtx,
|
|
5
|
+
GenericQueryCtx,
|
|
6
|
+
} from "convex/server";
|
|
7
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
8
|
+
import type {
|
|
9
|
+
ExecutionStatus,
|
|
10
|
+
LogLevel,
|
|
11
|
+
LogSource,
|
|
12
|
+
SortOrder,
|
|
13
|
+
SpanKind,
|
|
14
|
+
SpanStatus,
|
|
15
|
+
StepState,
|
|
16
|
+
} from "../shared/validators.js";
|
|
17
|
+
import {
|
|
18
|
+
scheduleRunnerFanIn,
|
|
19
|
+
serializeRunnerHandle,
|
|
20
|
+
} from "./boundaries.js";
|
|
21
|
+
|
|
22
|
+
export type MutationRunCtx = Pick<
|
|
23
|
+
GenericMutationCtx<GenericDataModel>,
|
|
24
|
+
"runMutation" | "runQuery" | "scheduler"
|
|
25
|
+
>;
|
|
26
|
+
|
|
27
|
+
export type QueryRunCtx = Pick<GenericQueryCtx<GenericDataModel>, "runQuery">;
|
|
28
|
+
|
|
29
|
+
export type RunnerArgs = {
|
|
30
|
+
executionId: string;
|
|
31
|
+
generation: number;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
export type RunnerHandle = FunctionHandle<"mutation", RunnerArgs, void>;
|
|
35
|
+
|
|
36
|
+
export type StartWorkflowArgs = {
|
|
37
|
+
workflowName: string;
|
|
38
|
+
executionId: string;
|
|
39
|
+
payload: unknown;
|
|
40
|
+
runner: RunnerHandle;
|
|
41
|
+
tenantId?: string;
|
|
42
|
+
parentExecutionId?: string;
|
|
43
|
+
traceId?: string;
|
|
44
|
+
};
|
|
45
|
+
|
|
46
|
+
export type ResumeWorkflowArgs = {
|
|
47
|
+
executionId: string;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
export class ConvexWorkflowEngine {
|
|
51
|
+
constructor(public component: ComponentApi) {}
|
|
52
|
+
|
|
53
|
+
async start(ctx: MutationRunCtx, args: StartWorkflowArgs) {
|
|
54
|
+
const { created } = await ctx.runMutation(this.component.executions.createExecution, {
|
|
55
|
+
workflowName: args.workflowName,
|
|
56
|
+
executionId: args.executionId,
|
|
57
|
+
payload: args.payload,
|
|
58
|
+
tenantId: args.tenantId,
|
|
59
|
+
parentExecutionId: args.parentExecutionId,
|
|
60
|
+
traceId: args.traceId,
|
|
61
|
+
runnerHandle: serializeRunnerHandle(args.runner),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
if (created) {
|
|
65
|
+
await scheduleRunnerFanIn(ctx.scheduler, this.component, {
|
|
66
|
+
executionId: args.executionId,
|
|
67
|
+
generation: 0,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return args.executionId;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async poll(ctx: QueryRunCtx, executionId: string) {
|
|
75
|
+
return await ctx.runQuery(this.component.executions.getExecution, {
|
|
76
|
+
executionId,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async interrupt(ctx: MutationRunCtx, executionId: string, reason?: string) {
|
|
81
|
+
return await ctx.runMutation(this.component.executions.interruptExecution, {
|
|
82
|
+
executionId,
|
|
83
|
+
reason,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async resume(ctx: MutationRunCtx, args: ResumeWorkflowArgs) {
|
|
88
|
+
const execution = await ctx.runQuery(this.component.executions.getExecution, {
|
|
89
|
+
executionId: args.executionId,
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
if (
|
|
93
|
+
!execution ||
|
|
94
|
+
execution.status === "completed" ||
|
|
95
|
+
execution.status === "failed" ||
|
|
96
|
+
execution.status === "interrupted"
|
|
97
|
+
) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
await scheduleRunnerFanIn(ctx.scheduler, this.component, {
|
|
102
|
+
executionId: args.executionId,
|
|
103
|
+
generation: execution.generation,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
async listExecutions(
|
|
108
|
+
ctx: QueryRunCtx,
|
|
109
|
+
args?: {
|
|
110
|
+
workflowName?: string;
|
|
111
|
+
status?: ExecutionStatus;
|
|
112
|
+
tenantId?: string;
|
|
113
|
+
startedAfter?: number;
|
|
114
|
+
startedBefore?: number;
|
|
115
|
+
limit?: number;
|
|
116
|
+
cursorStartedAt?: number;
|
|
117
|
+
cursorExecutionId?: string;
|
|
118
|
+
order?: SortOrder;
|
|
119
|
+
},
|
|
120
|
+
) {
|
|
121
|
+
return await ctx.runQuery(this.component.dashboard.listExecutions, {
|
|
122
|
+
workflowName: args?.workflowName,
|
|
123
|
+
status: args?.status,
|
|
124
|
+
tenantId: args?.tenantId,
|
|
125
|
+
startedAfter: args?.startedAfter,
|
|
126
|
+
startedBefore: args?.startedBefore,
|
|
127
|
+
limit: args?.limit,
|
|
128
|
+
cursorStartedAt: args?.cursorStartedAt,
|
|
129
|
+
cursorExecutionId: args?.cursorExecutionId,
|
|
130
|
+
order: args?.order,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async getExecution(ctx: QueryRunCtx, executionId: string) {
|
|
135
|
+
return await ctx.runQuery(this.component.dashboard.getExecution, {
|
|
136
|
+
executionId,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async listExecutionSteps(
|
|
141
|
+
ctx: QueryRunCtx,
|
|
142
|
+
args: {
|
|
143
|
+
executionId: string;
|
|
144
|
+
state?: StepState;
|
|
145
|
+
limit?: number;
|
|
146
|
+
afterStepNumber?: number;
|
|
147
|
+
},
|
|
148
|
+
) {
|
|
149
|
+
return await ctx.runQuery(this.component.dashboard.listExecutionSteps, args);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
async listExecutionSpans(
|
|
153
|
+
ctx: QueryRunCtx,
|
|
154
|
+
args: {
|
|
155
|
+
executionId: string;
|
|
156
|
+
kind?: SpanKind;
|
|
157
|
+
status?: SpanStatus;
|
|
158
|
+
limit?: number;
|
|
159
|
+
afterStartTime?: number;
|
|
160
|
+
},
|
|
161
|
+
) {
|
|
162
|
+
return await ctx.runQuery(this.component.dashboard.listExecutionSpans, args);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async listExecutionLogs(
|
|
166
|
+
ctx: QueryRunCtx,
|
|
167
|
+
args: {
|
|
168
|
+
executionId: string;
|
|
169
|
+
level?: LogLevel;
|
|
170
|
+
source?: LogSource;
|
|
171
|
+
spanId?: string;
|
|
172
|
+
limit?: number;
|
|
173
|
+
afterTimestamp?: number;
|
|
174
|
+
order?: SortOrder;
|
|
175
|
+
},
|
|
176
|
+
) {
|
|
177
|
+
return await ctx.runQuery(this.component.dashboard.listExecutionLogs, args);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
async getConfig(ctx: QueryRunCtx) {
|
|
181
|
+
return await ctx.runQuery(this.component.config.getConfig, {});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
async setConfig(
|
|
185
|
+
ctx: MutationRunCtx,
|
|
186
|
+
args: {
|
|
187
|
+
maxInlineBytes?: number;
|
|
188
|
+
maxLogDataBytes?: number;
|
|
189
|
+
maxSpanAttrBytes?: number;
|
|
190
|
+
retentionDays?: number;
|
|
191
|
+
defaultLogLevel?: LogLevel;
|
|
192
|
+
},
|
|
193
|
+
) {
|
|
194
|
+
return await ctx.runMutation(this.component.config.setConfig, args);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
async cleanup(
|
|
198
|
+
ctx: MutationRunCtx,
|
|
199
|
+
args?: {
|
|
200
|
+
retentionDays?: number;
|
|
201
|
+
limit?: number;
|
|
202
|
+
},
|
|
203
|
+
) {
|
|
204
|
+
return await ctx.runMutation(
|
|
205
|
+
this.component.cleanup.cleanupOldExecutions,
|
|
206
|
+
args ?? {},
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import * as Effect from "effect/Effect";
|
|
2
|
+
import * as Exit from "effect/Exit";
|
|
3
|
+
import { internalActionGeneric } from "convex/server";
|
|
4
|
+
import { v } from "convex/values";
|
|
5
|
+
import { WorkflowInstance } from "@effect/workflow/WorkflowEngine";
|
|
6
|
+
import * as Activity from "@effect/workflow/Activity";
|
|
7
|
+
import { getActivity, getWorkflow } from "./registry.js";
|
|
8
|
+
|
|
9
|
+
export const activityWorkerArgs = {
|
|
10
|
+
workflowName: v.string(),
|
|
11
|
+
activityName: v.string(),
|
|
12
|
+
executionId: v.string(),
|
|
13
|
+
attempt: v.number(),
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export async function runRegisteredActivity(args: {
|
|
17
|
+
workflowName: string;
|
|
18
|
+
activityName: string;
|
|
19
|
+
executionId: string;
|
|
20
|
+
attempt: number;
|
|
21
|
+
}) {
|
|
22
|
+
const workflowEntry = getWorkflow(args.workflowName);
|
|
23
|
+
if (!workflowEntry) {
|
|
24
|
+
throw new Error(`Workflow not registered: ${args.workflowName}`);
|
|
25
|
+
}
|
|
26
|
+
const activity = getActivity(args.activityName);
|
|
27
|
+
if (!activity) {
|
|
28
|
+
throw new Error(`Activity not registered: ${args.activityName}`);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const instance = WorkflowInstance.initial(
|
|
32
|
+
workflowEntry.workflow,
|
|
33
|
+
args.executionId,
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
const exit = await Effect.runPromiseExit(
|
|
37
|
+
activity.executeEncoded.pipe(
|
|
38
|
+
Effect.provideService(WorkflowInstance, instance),
|
|
39
|
+
Effect.provideService(Activity.CurrentAttempt as never, args.attempt),
|
|
40
|
+
),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (Exit.isSuccess(exit)) {
|
|
44
|
+
return exit.value;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
throw new Error(String(exit.cause));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Create an app-local internal action worker that can execute registered activities.
|
|
52
|
+
*/
|
|
53
|
+
export function defineActivityWorker() {
|
|
54
|
+
return internalActionGeneric({
|
|
55
|
+
args: activityWorkerArgs,
|
|
56
|
+
handler: async (_ctx, args) => {
|
|
57
|
+
return await runRegisteredActivity(args);
|
|
58
|
+
},
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export type ActivityWorkerReference = ReturnType<typeof defineActivityWorker>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { describe, expect, test, vi } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
enqueueActivity,
|
|
4
|
+
scheduleDeferredCompletion,
|
|
5
|
+
scheduleRunnerFanIn,
|
|
6
|
+
} from "./boundaries.js";
|
|
7
|
+
|
|
8
|
+
describe("client boundaries", () => {
|
|
9
|
+
test("enqueueActivity delegates to component.runner.enqueueActivity", async () => {
|
|
10
|
+
const runMutation = vi.fn(async () => "work-123");
|
|
11
|
+
const runnerEnqueueRef = { _ref: "runner:enqueueActivity" };
|
|
12
|
+
|
|
13
|
+
const result = await enqueueActivity(
|
|
14
|
+
runMutation as any,
|
|
15
|
+
{
|
|
16
|
+
runner: {
|
|
17
|
+
enqueueActivity: runnerEnqueueRef,
|
|
18
|
+
},
|
|
19
|
+
} as any,
|
|
20
|
+
{
|
|
21
|
+
executionId: "exec-1",
|
|
22
|
+
generation: 0,
|
|
23
|
+
workflowName: "wf",
|
|
24
|
+
activityName: "validateOrder",
|
|
25
|
+
attempt: 1,
|
|
26
|
+
stepNumber: 0,
|
|
27
|
+
spanId: "span-1",
|
|
28
|
+
activityWorkerHandle: "function://worker",
|
|
29
|
+
},
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
expect(result).toBe("work-123");
|
|
33
|
+
expect(runMutation).toHaveBeenCalledTimes(1);
|
|
34
|
+
expect(runMutation).toHaveBeenCalledWith(runnerEnqueueRef, {
|
|
35
|
+
executionId: "exec-1",
|
|
36
|
+
generation: 0,
|
|
37
|
+
workflowName: "wf",
|
|
38
|
+
activityName: "validateOrder",
|
|
39
|
+
attempt: 1,
|
|
40
|
+
stepNumber: 0,
|
|
41
|
+
spanId: "span-1",
|
|
42
|
+
activityWorkerHandle: "function://worker",
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("scheduleRunnerFanIn uses component runner mutation reference", async () => {
|
|
47
|
+
const runAfter = vi.fn(async () => "scheduled");
|
|
48
|
+
const runnerRef = { _ref: "runner:run" };
|
|
49
|
+
|
|
50
|
+
await scheduleRunnerFanIn(
|
|
51
|
+
{ runAfter } as any,
|
|
52
|
+
{ runner: { run: runnerRef } } as any,
|
|
53
|
+
{ executionId: "exec-2", generation: 3 },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
expect(runAfter).toHaveBeenCalledWith(0, runnerRef, {
|
|
57
|
+
executionId: "exec-2",
|
|
58
|
+
generation: 3,
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test("scheduleDeferredCompletion uses component deferred mutation reference", async () => {
|
|
63
|
+
const runAfter = vi.fn(async () => "scheduled");
|
|
64
|
+
const deferredRef = { _ref: "deferreds:deferredDone" };
|
|
65
|
+
|
|
66
|
+
await scheduleDeferredCompletion(
|
|
67
|
+
{ runAfter } as any,
|
|
68
|
+
{ deferreds: { deferredDone: deferredRef } } as any,
|
|
69
|
+
500,
|
|
70
|
+
{
|
|
71
|
+
executionId: "exec-3",
|
|
72
|
+
deferredName: "clock/wake",
|
|
73
|
+
exit: { ok: true },
|
|
74
|
+
},
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
expect(runAfter).toHaveBeenCalledWith(500, deferredRef, {
|
|
78
|
+
executionId: "exec-3",
|
|
79
|
+
deferredName: "clock/wake",
|
|
80
|
+
exit: { ok: true },
|
|
81
|
+
});
|
|
82
|
+
});
|
|
83
|
+
});
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
FunctionReference,
|
|
3
|
+
GenericDataModel,
|
|
4
|
+
GenericMutationCtx,
|
|
5
|
+
} from "convex/server";
|
|
6
|
+
import type { WorkpoolOptions } from "@convex-dev/workpool";
|
|
7
|
+
import type { ComponentApi } from "../component/_generated/component.js";
|
|
8
|
+
import type { RunnerArgs, RunnerHandle } from "./ConvexWorkflowEngine.js";
|
|
9
|
+
|
|
10
|
+
type Scheduler = Pick<GenericMutationCtx<GenericDataModel>, "scheduler">["scheduler"];
|
|
11
|
+
|
|
12
|
+
type MutationRef =
|
|
13
|
+
| FunctionReference<"mutation", "public">
|
|
14
|
+
| FunctionReference<"mutation", "internal">;
|
|
15
|
+
|
|
16
|
+
function asMutationRef(reference: unknown): MutationRef {
|
|
17
|
+
return reference as MutationRef;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
type MutationRun = Pick<
|
|
21
|
+
GenericMutationCtx<GenericDataModel>,
|
|
22
|
+
"runMutation"
|
|
23
|
+
>["runMutation"];
|
|
24
|
+
|
|
25
|
+
type ComponentWithActivityEnqueue = ComponentApi & {
|
|
26
|
+
runner: {
|
|
27
|
+
enqueueActivity: MutationRef;
|
|
28
|
+
};
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
export type EnqueueActivityArgs = {
|
|
32
|
+
executionId: string;
|
|
33
|
+
generation: number;
|
|
34
|
+
workflowName: string;
|
|
35
|
+
activityName: string;
|
|
36
|
+
attempt: number;
|
|
37
|
+
stepNumber: number;
|
|
38
|
+
spanId?: string;
|
|
39
|
+
activityWorkerHandle: string;
|
|
40
|
+
workpoolOptions?: WorkpoolOptions;
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export function serializeRunnerHandle(handle: RunnerHandle): string {
|
|
44
|
+
return handle as unknown as string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function enqueueActivity(
|
|
48
|
+
runMutation: MutationRun,
|
|
49
|
+
component: ComponentApi,
|
|
50
|
+
args: EnqueueActivityArgs,
|
|
51
|
+
) {
|
|
52
|
+
const reference = (component as ComponentWithActivityEnqueue).runner.enqueueActivity;
|
|
53
|
+
return runMutation(asMutationRef(reference), args);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function scheduleRunnerFanIn(
|
|
57
|
+
scheduler: Scheduler,
|
|
58
|
+
component: ComponentApi,
|
|
59
|
+
args: RunnerArgs,
|
|
60
|
+
) {
|
|
61
|
+
return scheduler.runAfter(0, asMutationRef(component.runner.run), args);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function scheduleDeferredCompletion(
|
|
65
|
+
scheduler: Scheduler,
|
|
66
|
+
component: ComponentApi,
|
|
67
|
+
runAfterMs: number,
|
|
68
|
+
args: {
|
|
69
|
+
executionId: string;
|
|
70
|
+
deferredName: string;
|
|
71
|
+
exit: unknown;
|
|
72
|
+
},
|
|
73
|
+
) {
|
|
74
|
+
return scheduler.runAfter(
|
|
75
|
+
runAfterMs,
|
|
76
|
+
asMutationRef(component.deferreds.deferredDone),
|
|
77
|
+
args,
|
|
78
|
+
);
|
|
79
|
+
}
|