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,162 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server.js";
|
|
3
|
+
import { scheduleRunnerFanIn } from "./boundaries.js";
|
|
4
|
+
import { persistBoundedPayload, hydratePayload } from "./payloads.js";
|
|
5
|
+
import { nowTs } from "./utils.js";
|
|
6
|
+
|
|
7
|
+
type DbReaderCtx = Pick<QueryCtx, "db">;
|
|
8
|
+
|
|
9
|
+
async function getDeferredRow(
|
|
10
|
+
ctx: DbReaderCtx,
|
|
11
|
+
executionId: string,
|
|
12
|
+
deferredName: string,
|
|
13
|
+
) {
|
|
14
|
+
return await ctx.db
|
|
15
|
+
.query("deferreds")
|
|
16
|
+
.withIndex("by_execution_name", (q) =>
|
|
17
|
+
q.eq("executionId", executionId).eq("deferredName", deferredName),
|
|
18
|
+
)
|
|
19
|
+
.unique();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const deferredResult = query({
|
|
23
|
+
args: {
|
|
24
|
+
executionId: v.string(),
|
|
25
|
+
deferredName: v.string(),
|
|
26
|
+
},
|
|
27
|
+
returns: v.any(),
|
|
28
|
+
handler: async (ctx, args) => {
|
|
29
|
+
const deferred = await getDeferredRow(ctx, args.executionId, args.deferredName);
|
|
30
|
+
if (!deferred || !deferred.completed) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
return await hydratePayload(ctx, deferred.exitRef, deferred.exitPreview);
|
|
34
|
+
},
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
export const deferredDone = mutation({
|
|
38
|
+
args: {
|
|
39
|
+
executionId: v.string(),
|
|
40
|
+
deferredName: v.string(),
|
|
41
|
+
exit: v.any(),
|
|
42
|
+
},
|
|
43
|
+
returns: v.object({ changed: v.boolean() }),
|
|
44
|
+
handler: async (ctx, args) => {
|
|
45
|
+
const existing = await getDeferredRow(ctx, args.executionId, args.deferredName);
|
|
46
|
+
if (existing?.completed) {
|
|
47
|
+
return { changed: false };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const exitPayload = await persistBoundedPayload(ctx, "deferredExit", args.exit);
|
|
51
|
+
|
|
52
|
+
if (existing) {
|
|
53
|
+
await ctx.db.patch(existing._id, {
|
|
54
|
+
completed: true,
|
|
55
|
+
completedAt: nowTs(),
|
|
56
|
+
exitRef: exitPayload.ref,
|
|
57
|
+
exitPreview: exitPayload.preview,
|
|
58
|
+
});
|
|
59
|
+
} else {
|
|
60
|
+
await ctx.db.insert("deferreds", {
|
|
61
|
+
executionId: args.executionId,
|
|
62
|
+
deferredName: args.deferredName,
|
|
63
|
+
completed: true,
|
|
64
|
+
completedAt: nowTs(),
|
|
65
|
+
exitRef: exitPayload.ref,
|
|
66
|
+
exitPreview: exitPayload.preview,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const execution = await ctx.db
|
|
71
|
+
.query("executions")
|
|
72
|
+
.withIndex("by_execution_id", (q) => q.eq("executionId", args.executionId))
|
|
73
|
+
.unique();
|
|
74
|
+
|
|
75
|
+
if (
|
|
76
|
+
execution &&
|
|
77
|
+
execution.runnerHandle &&
|
|
78
|
+
execution.status !== "completed" &&
|
|
79
|
+
execution.status !== "failed" &&
|
|
80
|
+
execution.status !== "interrupted"
|
|
81
|
+
) {
|
|
82
|
+
await scheduleRunnerFanIn(ctx.scheduler, {
|
|
83
|
+
executionId: execution.executionId,
|
|
84
|
+
generation: execution.generation,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return { changed: true };
|
|
89
|
+
},
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
export const registerDeferred = mutation({
|
|
93
|
+
args: {
|
|
94
|
+
executionId: v.string(),
|
|
95
|
+
deferredName: v.string(),
|
|
96
|
+
},
|
|
97
|
+
returns: v.null(),
|
|
98
|
+
handler: async (ctx, args) => {
|
|
99
|
+
const existing = await getDeferredRow(ctx, args.executionId, args.deferredName);
|
|
100
|
+
if (!existing) {
|
|
101
|
+
await ctx.db.insert("deferreds", {
|
|
102
|
+
executionId: args.executionId,
|
|
103
|
+
deferredName: args.deferredName,
|
|
104
|
+
completed: false,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
return null;
|
|
108
|
+
},
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Test helper for creating a deferred record.
|
|
113
|
+
*/
|
|
114
|
+
export const createDeferred = mutation({
|
|
115
|
+
args: {
|
|
116
|
+
executionId: v.string(),
|
|
117
|
+
deferredName: v.string(),
|
|
118
|
+
},
|
|
119
|
+
returns: v.null(),
|
|
120
|
+
handler: async (ctx, args) => {
|
|
121
|
+
const existing = await getDeferredRow(ctx, args.executionId, args.deferredName);
|
|
122
|
+
if (existing) {
|
|
123
|
+
return null;
|
|
124
|
+
}
|
|
125
|
+
await ctx.db.insert("deferreds", {
|
|
126
|
+
executionId: args.executionId,
|
|
127
|
+
deferredName: args.deferredName,
|
|
128
|
+
completed: false,
|
|
129
|
+
});
|
|
130
|
+
return null;
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Test helper for completing a deferred with exit value.
|
|
136
|
+
*/
|
|
137
|
+
export const completeDeferred = mutation({
|
|
138
|
+
args: {
|
|
139
|
+
executionId: v.string(),
|
|
140
|
+
deferredName: v.string(),
|
|
141
|
+
exit: v.any(),
|
|
142
|
+
},
|
|
143
|
+
returns: v.null(),
|
|
144
|
+
handler: async (ctx, args) => {
|
|
145
|
+
const exitPayload = await persistBoundedPayload(
|
|
146
|
+
ctx,
|
|
147
|
+
"deferredExit",
|
|
148
|
+
args.exit,
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
const existing = await getDeferredRow(ctx, args.executionId, args.deferredName);
|
|
152
|
+
if (existing) {
|
|
153
|
+
await ctx.db.patch(existing._id, {
|
|
154
|
+
completed: true,
|
|
155
|
+
completedAt: nowTs(),
|
|
156
|
+
exitRef: exitPayload.ref,
|
|
157
|
+
exitPreview: exitPayload.preview,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
return null;
|
|
161
|
+
},
|
|
162
|
+
});
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server.js";
|
|
3
|
+
import { nowTs, generateTraceId } from "./utils.js";
|
|
4
|
+
import { persistBoundedPayload } from "./payloads.js";
|
|
5
|
+
import { vExecutionStatus } from "../shared/validators.js";
|
|
6
|
+
import type { Doc } from "./_generated/dataModel.js";
|
|
7
|
+
|
|
8
|
+
type DbReaderCtx = Pick<QueryCtx, "db">;
|
|
9
|
+
|
|
10
|
+
async function getExecutionByExecutionId(
|
|
11
|
+
ctx: DbReaderCtx,
|
|
12
|
+
executionId: string,
|
|
13
|
+
) {
|
|
14
|
+
return await ctx.db
|
|
15
|
+
.query("executions")
|
|
16
|
+
.withIndex("by_execution_id", (q) => q.eq("executionId", executionId))
|
|
17
|
+
.unique();
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function mustGetExecution(
|
|
21
|
+
ctx: DbReaderCtx,
|
|
22
|
+
executionId: string,
|
|
23
|
+
): Promise<Doc<"executions">> {
|
|
24
|
+
const execution = await getExecutionByExecutionId(ctx, executionId);
|
|
25
|
+
if (!execution) {
|
|
26
|
+
throw new Error(`Execution not found: ${executionId}`);
|
|
27
|
+
}
|
|
28
|
+
return execution;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export const createExecution = mutation({
|
|
32
|
+
args: {
|
|
33
|
+
workflowName: v.string(),
|
|
34
|
+
executionId: v.string(),
|
|
35
|
+
payload: v.any(),
|
|
36
|
+
tenantId: v.optional(v.string()),
|
|
37
|
+
parentExecutionId: v.optional(v.string()),
|
|
38
|
+
traceId: v.optional(v.string()),
|
|
39
|
+
runnerHandle: v.optional(v.string()),
|
|
40
|
+
},
|
|
41
|
+
returns: v.object({
|
|
42
|
+
created: v.boolean(),
|
|
43
|
+
executionId: v.id("executions"),
|
|
44
|
+
}),
|
|
45
|
+
handler: async (ctx, args) => {
|
|
46
|
+
const existing = await getExecutionByExecutionId(ctx, args.executionId);
|
|
47
|
+
if (existing) {
|
|
48
|
+
return { created: false, executionId: existing._id };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const payload = await persistBoundedPayload(
|
|
52
|
+
ctx,
|
|
53
|
+
"executionPayload",
|
|
54
|
+
args.payload,
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const id = await ctx.db.insert("executions", {
|
|
58
|
+
workflowName: args.workflowName,
|
|
59
|
+
executionId: args.executionId,
|
|
60
|
+
status: "pending",
|
|
61
|
+
generation: 0,
|
|
62
|
+
tenantId: args.tenantId,
|
|
63
|
+
parentExecutionId: args.parentExecutionId,
|
|
64
|
+
payloadRef: payload.ref,
|
|
65
|
+
payloadPreview: payload.preview,
|
|
66
|
+
payloadSize: payload.size,
|
|
67
|
+
traceId: args.traceId ?? generateTraceId(),
|
|
68
|
+
runnerHandle: args.runnerHandle,
|
|
69
|
+
startedAt: nowTs(),
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return { created: true, executionId: id };
|
|
73
|
+
},
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
export const setExecutionRunning = mutation({
|
|
77
|
+
args: {
|
|
78
|
+
executionId: v.string(),
|
|
79
|
+
generation: v.number(),
|
|
80
|
+
},
|
|
81
|
+
returns: v.object({ changed: v.boolean() }),
|
|
82
|
+
handler: async (ctx, { executionId, generation }) => {
|
|
83
|
+
const execution = await mustGetExecution(ctx, executionId);
|
|
84
|
+
if (execution.generation !== generation) {
|
|
85
|
+
return { changed: false };
|
|
86
|
+
}
|
|
87
|
+
if (
|
|
88
|
+
execution.status === "completed" ||
|
|
89
|
+
execution.status === "failed" ||
|
|
90
|
+
execution.status === "interrupted"
|
|
91
|
+
) {
|
|
92
|
+
return { changed: false };
|
|
93
|
+
}
|
|
94
|
+
await ctx.db.patch(execution._id, {
|
|
95
|
+
status: "running",
|
|
96
|
+
lastResumedAt: nowTs(),
|
|
97
|
+
});
|
|
98
|
+
return { changed: true };
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
export const setExecutionSuspended = mutation({
|
|
103
|
+
args: {
|
|
104
|
+
executionId: v.string(),
|
|
105
|
+
generation: v.number(),
|
|
106
|
+
},
|
|
107
|
+
returns: v.object({ changed: v.boolean() }),
|
|
108
|
+
handler: async (ctx, { executionId, generation }) => {
|
|
109
|
+
const execution = await mustGetExecution(ctx, executionId);
|
|
110
|
+
if (execution.generation !== generation) {
|
|
111
|
+
return { changed: false };
|
|
112
|
+
}
|
|
113
|
+
if (
|
|
114
|
+
execution.status === "completed" ||
|
|
115
|
+
execution.status === "failed" ||
|
|
116
|
+
execution.status === "interrupted"
|
|
117
|
+
) {
|
|
118
|
+
return { changed: false };
|
|
119
|
+
}
|
|
120
|
+
await ctx.db.patch(execution._id, {
|
|
121
|
+
status: "suspended",
|
|
122
|
+
lastSuspendedAt: nowTs(),
|
|
123
|
+
});
|
|
124
|
+
return { changed: true };
|
|
125
|
+
},
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
export const completeExecution = mutation({
|
|
129
|
+
args: {
|
|
130
|
+
executionId: v.string(),
|
|
131
|
+
generation: v.number(),
|
|
132
|
+
kind: v.union(v.literal("success"), v.literal("failure")),
|
|
133
|
+
result: v.optional(v.any()),
|
|
134
|
+
error: v.optional(v.string()),
|
|
135
|
+
},
|
|
136
|
+
returns: v.object({ changed: v.boolean() }),
|
|
137
|
+
handler: async (ctx, args) => {
|
|
138
|
+
const execution = await mustGetExecution(ctx, args.executionId);
|
|
139
|
+
if (execution.generation !== args.generation) {
|
|
140
|
+
return { changed: false };
|
|
141
|
+
}
|
|
142
|
+
if (
|
|
143
|
+
execution.status === "completed" ||
|
|
144
|
+
execution.status === "failed" ||
|
|
145
|
+
execution.status === "interrupted"
|
|
146
|
+
) {
|
|
147
|
+
return { changed: false };
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
let resultRef = execution.resultRef;
|
|
151
|
+
let resultPreview = execution.resultPreview;
|
|
152
|
+
if (args.result !== undefined) {
|
|
153
|
+
const resultPayload = await persistBoundedPayload(
|
|
154
|
+
ctx,
|
|
155
|
+
"stepOutput",
|
|
156
|
+
args.result,
|
|
157
|
+
);
|
|
158
|
+
resultRef = resultPayload.ref;
|
|
159
|
+
resultPreview = resultPayload.preview;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
await ctx.db.patch(execution._id, {
|
|
163
|
+
status: args.kind === "success" ? "completed" : "failed",
|
|
164
|
+
resultKind: args.kind,
|
|
165
|
+
resultRef,
|
|
166
|
+
resultPreview,
|
|
167
|
+
error: args.error,
|
|
168
|
+
completedAt: nowTs(),
|
|
169
|
+
});
|
|
170
|
+
return { changed: true };
|
|
171
|
+
},
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
export const interruptExecution = mutation({
|
|
175
|
+
args: {
|
|
176
|
+
executionId: v.string(),
|
|
177
|
+
reason: v.optional(v.string()),
|
|
178
|
+
},
|
|
179
|
+
returns: v.object({ generation: v.number() }),
|
|
180
|
+
handler: async (ctx, { executionId, reason }) => {
|
|
181
|
+
const execution = await mustGetExecution(ctx, executionId);
|
|
182
|
+
if (
|
|
183
|
+
execution.status === "completed" ||
|
|
184
|
+
execution.status === "failed" ||
|
|
185
|
+
execution.status === "interrupted"
|
|
186
|
+
) {
|
|
187
|
+
return { generation: execution.generation };
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const generation = execution.generation + 1;
|
|
191
|
+
await ctx.db.patch(execution._id, {
|
|
192
|
+
status: "interrupted",
|
|
193
|
+
generation,
|
|
194
|
+
error: reason ?? "Interrupted",
|
|
195
|
+
completedAt: nowTs(),
|
|
196
|
+
});
|
|
197
|
+
return { generation };
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
export const getExecution = query({
|
|
202
|
+
args: {
|
|
203
|
+
executionId: v.string(),
|
|
204
|
+
},
|
|
205
|
+
returns: v.any(),
|
|
206
|
+
handler: async (ctx, { executionId }) => {
|
|
207
|
+
return await getExecutionByExecutionId(ctx, executionId);
|
|
208
|
+
},
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Test helper for patching execution startedAt (simulates old executions).
|
|
213
|
+
*/
|
|
214
|
+
export const internalPatchExecution = mutation({
|
|
215
|
+
args: {
|
|
216
|
+
executionId: v.string(),
|
|
217
|
+
startedAt: v.number(),
|
|
218
|
+
},
|
|
219
|
+
returns: v.null(),
|
|
220
|
+
handler: async (ctx, { executionId, startedAt }) => {
|
|
221
|
+
const execution = await mustGetExecution(ctx, executionId);
|
|
222
|
+
await ctx.db.patch(execution._id, { startedAt });
|
|
223
|
+
return null;
|
|
224
|
+
},
|
|
225
|
+
});
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server.js";
|
|
3
|
+
import {
|
|
4
|
+
vDeterminismSignature,
|
|
5
|
+
vResultEnvelope,
|
|
6
|
+
vStepKind,
|
|
7
|
+
vStepState,
|
|
8
|
+
} from "../shared/validators.js";
|
|
9
|
+
import { nowTs } from "./utils.js";
|
|
10
|
+
import { persistBoundedPayload } from "./payloads.js";
|
|
11
|
+
|
|
12
|
+
type DbReaderCtx = Pick<QueryCtx, "db">;
|
|
13
|
+
|
|
14
|
+
async function getStep(
|
|
15
|
+
ctx: DbReaderCtx,
|
|
16
|
+
executionId: string,
|
|
17
|
+
stepNumber: number,
|
|
18
|
+
) {
|
|
19
|
+
return await ctx.db
|
|
20
|
+
.query("journalSteps")
|
|
21
|
+
.withIndex("by_execution_step", (q) =>
|
|
22
|
+
q.eq("executionId", executionId).eq("stepNumber", stepNumber),
|
|
23
|
+
)
|
|
24
|
+
.unique();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const appendJournalStep = mutation({
|
|
28
|
+
args: {
|
|
29
|
+
executionId: v.string(),
|
|
30
|
+
stepNumber: v.number(),
|
|
31
|
+
kind: vStepKind,
|
|
32
|
+
name: v.string(),
|
|
33
|
+
signature: vDeterminismSignature,
|
|
34
|
+
attempt: v.optional(v.number()),
|
|
35
|
+
spanId: v.optional(v.string()),
|
|
36
|
+
input: v.any(),
|
|
37
|
+
},
|
|
38
|
+
returns: v.any(),
|
|
39
|
+
handler: async (ctx, args) => {
|
|
40
|
+
const existing = await getStep(ctx, args.executionId, args.stepNumber);
|
|
41
|
+
if (existing) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
`Duplicate journal step: ${args.executionId}/${args.stepNumber}`,
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const inputPayload = await persistBoundedPayload(ctx, "stepInput", args.input);
|
|
48
|
+
|
|
49
|
+
const id = await ctx.db.insert("journalSteps", {
|
|
50
|
+
executionId: args.executionId,
|
|
51
|
+
stepNumber: args.stepNumber,
|
|
52
|
+
kind: args.kind,
|
|
53
|
+
name: args.name,
|
|
54
|
+
signature: args.signature,
|
|
55
|
+
state: "started",
|
|
56
|
+
attempt: args.attempt,
|
|
57
|
+
spanId: args.spanId,
|
|
58
|
+
inputRef: inputPayload.ref,
|
|
59
|
+
inputPreview: inputPayload.preview,
|
|
60
|
+
inputSize: inputPayload.size,
|
|
61
|
+
startedAt: nowTs(),
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
return await ctx.db.get(id);
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
export const suspendJournalStep = mutation({
|
|
69
|
+
args: {
|
|
70
|
+
executionId: v.string(),
|
|
71
|
+
stepNumber: v.number(),
|
|
72
|
+
workId: v.string(),
|
|
73
|
+
resumeToken: v.optional(v.string()),
|
|
74
|
+
},
|
|
75
|
+
returns: v.object({ changed: v.boolean() }),
|
|
76
|
+
handler: async (ctx, args) => {
|
|
77
|
+
const step = await getStep(ctx, args.executionId, args.stepNumber);
|
|
78
|
+
if (!step) {
|
|
79
|
+
throw new Error(
|
|
80
|
+
`Journal step not found: ${args.executionId}/${args.stepNumber}`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
if (step.state === "completed" || step.state === "failed" || step.state === "canceled") {
|
|
84
|
+
return { changed: false };
|
|
85
|
+
}
|
|
86
|
+
await ctx.db.patch(step._id, {
|
|
87
|
+
state: "suspended",
|
|
88
|
+
workId: args.workId,
|
|
89
|
+
resumeToken: args.resumeToken,
|
|
90
|
+
});
|
|
91
|
+
return { changed: true };
|
|
92
|
+
},
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
export const completeJournalStep = mutation({
|
|
96
|
+
args: {
|
|
97
|
+
executionId: v.string(),
|
|
98
|
+
stepNumber: v.number(),
|
|
99
|
+
runResult: vResultEnvelope,
|
|
100
|
+
},
|
|
101
|
+
returns: v.object({ changed: v.boolean() }),
|
|
102
|
+
handler: async (ctx, args) => {
|
|
103
|
+
const step = await getStep(ctx, args.executionId, args.stepNumber);
|
|
104
|
+
if (!step) {
|
|
105
|
+
throw new Error(
|
|
106
|
+
`Journal step not found: ${args.executionId}/${args.stepNumber}`,
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
if (step.state === "completed" || step.state === "failed" || step.state === "canceled") {
|
|
110
|
+
return { changed: false };
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let runResult = args.runResult;
|
|
114
|
+
if (runResult.kind !== "canceled" && runResult.valuePreview !== undefined) {
|
|
115
|
+
const outputPayload = await persistBoundedPayload(
|
|
116
|
+
ctx,
|
|
117
|
+
"stepOutput",
|
|
118
|
+
runResult.valuePreview,
|
|
119
|
+
);
|
|
120
|
+
runResult = {
|
|
121
|
+
...runResult,
|
|
122
|
+
valueRef: outputPayload.ref,
|
|
123
|
+
valuePreview: outputPayload.preview,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
await ctx.db.patch(step._id, {
|
|
128
|
+
state:
|
|
129
|
+
runResult.kind === "success"
|
|
130
|
+
? "completed"
|
|
131
|
+
: runResult.kind === "failed"
|
|
132
|
+
? "failed"
|
|
133
|
+
: "canceled",
|
|
134
|
+
runResult,
|
|
135
|
+
completedAt: nowTs(),
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return { changed: true };
|
|
139
|
+
},
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
export const failJournalStep = mutation({
|
|
143
|
+
args: {
|
|
144
|
+
executionId: v.string(),
|
|
145
|
+
stepNumber: v.number(),
|
|
146
|
+
error: v.string(),
|
|
147
|
+
},
|
|
148
|
+
returns: v.object({ changed: v.boolean() }),
|
|
149
|
+
handler: async (ctx, args) => {
|
|
150
|
+
const step = await getStep(ctx, args.executionId, args.stepNumber);
|
|
151
|
+
if (!step) {
|
|
152
|
+
throw new Error(
|
|
153
|
+
`Journal step not found: ${args.executionId}/${args.stepNumber}`,
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
if (step.state === "completed" || step.state === "failed" || step.state === "canceled") {
|
|
157
|
+
return { changed: false };
|
|
158
|
+
}
|
|
159
|
+
await ctx.db.patch(step._id, {
|
|
160
|
+
state: "failed",
|
|
161
|
+
runResult: { kind: "failed", error: args.error },
|
|
162
|
+
completedAt: nowTs(),
|
|
163
|
+
});
|
|
164
|
+
return { changed: true };
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
export const getJournalStep = query({
|
|
169
|
+
args: {
|
|
170
|
+
executionId: v.string(),
|
|
171
|
+
stepNumber: v.number(),
|
|
172
|
+
},
|
|
173
|
+
returns: v.any(),
|
|
174
|
+
handler: async (ctx, args) => {
|
|
175
|
+
return await getStep(ctx, args.executionId, args.stepNumber);
|
|
176
|
+
},
|
|
177
|
+
});
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Validate deterministic replay signature for a specific step.
|
|
181
|
+
* Throws on mismatch to make determinism violations explicit.
|
|
182
|
+
*/
|
|
183
|
+
export const assertReplayStepSignature = query({
|
|
184
|
+
args: {
|
|
185
|
+
executionId: v.string(),
|
|
186
|
+
stepNumber: v.number(),
|
|
187
|
+
expectedSignature: vDeterminismSignature,
|
|
188
|
+
},
|
|
189
|
+
returns: v.any(),
|
|
190
|
+
handler: async (ctx, args) => {
|
|
191
|
+
const step = await getStep(ctx, args.executionId, args.stepNumber);
|
|
192
|
+
if (!step) {
|
|
193
|
+
return null;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
const actual = JSON.stringify(step.signature);
|
|
197
|
+
const expected = JSON.stringify(args.expectedSignature);
|
|
198
|
+
if (actual !== expected) {
|
|
199
|
+
throw new Error(
|
|
200
|
+
`Determinism violation for ${args.executionId}/${args.stepNumber}: expected ${expected}, got ${actual}`,
|
|
201
|
+
);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
return step;
|
|
205
|
+
},
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
export const loadJournalForExecution = query({
|
|
209
|
+
args: {
|
|
210
|
+
executionId: v.string(),
|
|
211
|
+
},
|
|
212
|
+
returns: v.any(),
|
|
213
|
+
handler: async (ctx, args) => {
|
|
214
|
+
const rows = await ctx.db
|
|
215
|
+
.query("journalSteps")
|
|
216
|
+
.withIndex("by_execution_step", (q) => q.eq("executionId", args.executionId))
|
|
217
|
+
.collect();
|
|
218
|
+
rows.sort((a, b) => a.stepNumber - b.stepNumber);
|
|
219
|
+
return rows;
|
|
220
|
+
},
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
export const listJournalSteps = query({
|
|
224
|
+
args: {
|
|
225
|
+
executionId: v.string(),
|
|
226
|
+
state: v.optional(vStepState),
|
|
227
|
+
limit: v.number(),
|
|
228
|
+
afterStepNumber: v.optional(v.number()),
|
|
229
|
+
},
|
|
230
|
+
returns: v.any(),
|
|
231
|
+
handler: async (ctx, args) => {
|
|
232
|
+
const limit = Math.max(1, Math.min(args.limit, 500));
|
|
233
|
+
|
|
234
|
+
let rows = await ctx.db
|
|
235
|
+
.query("journalSteps")
|
|
236
|
+
.withIndex("by_execution_step", (q) => q.eq("executionId", args.executionId))
|
|
237
|
+
.take(limit + 1);
|
|
238
|
+
|
|
239
|
+
if (args.afterStepNumber !== undefined) {
|
|
240
|
+
rows = rows.filter((row) => row.stepNumber > args.afterStepNumber!);
|
|
241
|
+
}
|
|
242
|
+
if (args.state) {
|
|
243
|
+
rows = rows.filter((row) => row.state === args.state);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const page = rows.slice(0, limit);
|
|
247
|
+
const nextAfterStepNumber =
|
|
248
|
+
rows.length > limit ? page[page.length - 1]?.stepNumber : undefined;
|
|
249
|
+
|
|
250
|
+
return { page, nextAfterStepNumber };
|
|
251
|
+
},
|
|
252
|
+
});
|