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.
Files changed (190) hide show
  1. package/README.md +107 -0
  2. package/dist/client/ConvexCtx.d.ts +12 -0
  3. package/dist/client/ConvexCtx.d.ts.map +1 -0
  4. package/dist/client/ConvexCtx.js +6 -0
  5. package/dist/client/ConvexCtx.js.map +1 -0
  6. package/dist/client/ConvexLogger.d.ts +7 -0
  7. package/dist/client/ConvexLogger.d.ts.map +1 -0
  8. package/dist/client/ConvexLogger.js +39 -0
  9. package/dist/client/ConvexLogger.js.map +1 -0
  10. package/dist/client/ConvexTracer.d.ts +7 -0
  11. package/dist/client/ConvexTracer.d.ts.map +1 -0
  12. package/dist/client/ConvexTracer.js +60 -0
  13. package/dist/client/ConvexTracer.js.map +1 -0
  14. package/dist/client/ConvexWorkflowEngine.d.ts +308 -0
  15. package/dist/client/ConvexWorkflowEngine.d.ts.map +1 -0
  16. package/dist/client/ConvexWorkflowEngine.js +88 -0
  17. package/dist/client/ConvexWorkflowEngine.js.map +1 -0
  18. package/dist/client/activityWorker.d.ts +23 -0
  19. package/dist/client/activityWorker.d.ts.map +1 -0
  20. package/dist/client/activityWorker.js +41 -0
  21. package/dist/client/activityWorker.js.map +1 -0
  22. package/dist/client/boundaries.d.ts +27 -0
  23. package/dist/client/boundaries.d.ts.map +1 -0
  24. package/dist/client/boundaries.js +17 -0
  25. package/dist/client/boundaries.js.map +1 -0
  26. package/dist/client/encoded.d.ts +22 -0
  27. package/dist/client/encoded.d.ts.map +1 -0
  28. package/dist/client/encoded.js +276 -0
  29. package/dist/client/encoded.js.map +1 -0
  30. package/dist/client/index.d.ts +13 -0
  31. package/dist/client/index.d.ts.map +1 -0
  32. package/dist/client/index.js +11 -0
  33. package/dist/client/index.js.map +1 -0
  34. package/dist/client/registry.d.ts +17 -0
  35. package/dist/client/registry.d.ts.map +1 -0
  36. package/dist/client/registry.js +21 -0
  37. package/dist/client/registry.js.map +1 -0
  38. package/dist/client/runner.d.ts +27 -0
  39. package/dist/client/runner.d.ts.map +1 -0
  40. package/dist/client/runner.js +90 -0
  41. package/dist/client/runner.js.map +1 -0
  42. package/dist/client/runtime.d.ts +10 -0
  43. package/dist/client/runtime.d.ts.map +1 -0
  44. package/dist/client/runtime.js +15 -0
  45. package/dist/client/runtime.js.map +1 -0
  46. package/dist/component/_generated/api.d.ts +148 -0
  47. package/dist/component/_generated/api.d.ts.map +1 -0
  48. package/dist/component/_generated/api.js +31 -0
  49. package/dist/component/_generated/api.js.map +1 -0
  50. package/dist/component/_generated/component.d.ts +921 -0
  51. package/dist/component/_generated/component.d.ts.map +1 -0
  52. package/dist/component/_generated/component.js +11 -0
  53. package/dist/component/_generated/component.js.map +1 -0
  54. package/dist/component/_generated/dataModel.d.ts +46 -0
  55. package/dist/component/_generated/dataModel.d.ts.map +1 -0
  56. package/dist/component/_generated/dataModel.js +11 -0
  57. package/dist/component/_generated/dataModel.js.map +1 -0
  58. package/dist/component/_generated/server.d.ts +121 -0
  59. package/dist/component/_generated/server.d.ts.map +1 -0
  60. package/dist/component/_generated/server.js +78 -0
  61. package/dist/component/_generated/server.js.map +1 -0
  62. package/dist/component/activityCompletions.d.ts +27 -0
  63. package/dist/component/activityCompletions.d.ts.map +1 -0
  64. package/dist/component/activityCompletions.js +70 -0
  65. package/dist/component/activityCompletions.js.map +1 -0
  66. package/dist/component/boundaries.d.ts +20 -0
  67. package/dist/component/boundaries.d.ts.map +1 -0
  68. package/dist/component/boundaries.js +17 -0
  69. package/dist/component/boundaries.js.map +1 -0
  70. package/dist/component/cleanup.d.ts +11 -0
  71. package/dist/component/cleanup.d.ts.map +1 -0
  72. package/dist/component/cleanup.js +163 -0
  73. package/dist/component/cleanup.js.map +1 -0
  74. package/dist/component/clocks.d.ts +12 -0
  75. package/dist/component/clocks.d.ts.map +1 -0
  76. package/dist/component/clocks.js +26 -0
  77. package/dist/component/clocks.js.map +1 -0
  78. package/dist/component/config.d.ts +25 -0
  79. package/dist/component/config.d.ts.map +1 -0
  80. package/dist/component/config.js +110 -0
  81. package/dist/component/config.js.map +1 -0
  82. package/dist/component/convex.config.d.ts +3 -0
  83. package/dist/component/convex.config.d.ts.map +1 -0
  84. package/dist/component/convex.config.js +6 -0
  85. package/dist/component/convex.config.js.map +1 -0
  86. package/dist/component/dashboard.d.ts +268 -0
  87. package/dist/component/dashboard.d.ts.map +1 -0
  88. package/dist/component/dashboard.js +622 -0
  89. package/dist/component/dashboard.js.map +1 -0
  90. package/dist/component/deferreds.d.ts +31 -0
  91. package/dist/component/deferreds.d.ts.map +1 -0
  92. package/dist/component/deferreds.js +138 -0
  93. package/dist/component/deferreds.js.map +1 -0
  94. package/dist/component/executions.d.ts +77 -0
  95. package/dist/component/executions.d.ts.map +1 -0
  96. package/dist/component/executions.js +186 -0
  97. package/dist/component/executions.js.map +1 -0
  98. package/dist/component/journalSteps.d.ts +261 -0
  99. package/dist/component/journalSteps.d.ts.map +1 -0
  100. package/dist/component/journalSteps.js +203 -0
  101. package/dist/component/journalSteps.js.map +1 -0
  102. package/dist/component/logs.d.ts +68 -0
  103. package/dist/component/logs.d.ts.map +1 -0
  104. package/dist/component/logs.js +123 -0
  105. package/dist/component/logs.js.map +1 -0
  106. package/dist/component/onComplete.d.ts +31 -0
  107. package/dist/component/onComplete.d.ts.map +1 -0
  108. package/dist/component/onComplete.js +146 -0
  109. package/dist/component/onComplete.js.map +1 -0
  110. package/dist/component/payloads.d.ts +26 -0
  111. package/dist/component/payloads.d.ts.map +1 -0
  112. package/dist/component/payloads.js +57 -0
  113. package/dist/component/payloads.js.map +1 -0
  114. package/dist/component/queries.d.ts +2 -0
  115. package/dist/component/queries.d.ts.map +1 -0
  116. package/dist/component/queries.js +2 -0
  117. package/dist/component/queries.js.map +1 -0
  118. package/dist/component/runner.d.ts +31 -0
  119. package/dist/component/runner.d.ts.map +1 -0
  120. package/dist/component/runner.js +87 -0
  121. package/dist/component/runner.js.map +1 -0
  122. package/dist/component/schema.d.ts +282 -0
  123. package/dist/component/schema.d.ts.map +1 -0
  124. package/dist/component/schema.js +119 -0
  125. package/dist/component/schema.js.map +1 -0
  126. package/dist/component/spans.d.ts +105 -0
  127. package/dist/component/spans.d.ts.map +1 -0
  128. package/dist/component/spans.js +190 -0
  129. package/dist/component/spans.js.map +1 -0
  130. package/dist/component/utils.d.ts +15 -0
  131. package/dist/component/utils.d.ts.map +1 -0
  132. package/dist/component/utils.js +53 -0
  133. package/dist/component/utils.js.map +1 -0
  134. package/dist/shared/constants.d.ts +12 -0
  135. package/dist/shared/constants.d.ts.map +1 -0
  136. package/dist/shared/constants.js +12 -0
  137. package/dist/shared/constants.js.map +1 -0
  138. package/dist/shared/validators.d.ts +69 -0
  139. package/dist/shared/validators.d.ts.map +1 -0
  140. package/dist/shared/validators.js +30 -0
  141. package/dist/shared/validators.js.map +1 -0
  142. package/package.json +74 -0
  143. package/src/client/ConvexCtx.ts +21 -0
  144. package/src/client/ConvexLogger.ts +52 -0
  145. package/src/client/ConvexTracer.ts +75 -0
  146. package/src/client/ConvexWorkflowEngine.test.ts +124 -0
  147. package/src/client/ConvexWorkflowEngine.ts +209 -0
  148. package/src/client/activityWorker.ts +62 -0
  149. package/src/client/boundaries.test.ts +83 -0
  150. package/src/client/boundaries.ts +79 -0
  151. package/src/client/encoded.lifecycle.test.ts +336 -0
  152. package/src/client/encoded.test.ts +153 -0
  153. package/src/client/encoded.ts +484 -0
  154. package/src/client/index.ts +47 -0
  155. package/src/client/registry.ts +35 -0
  156. package/src/client/runner.ts +165 -0
  157. package/src/client/runtime.ts +30 -0
  158. package/src/component/_generated/api.ts +179 -0
  159. package/src/component/_generated/component.ts +1216 -0
  160. package/src/component/_generated/dataModel.ts +60 -0
  161. package/src/component/_generated/server.ts +156 -0
  162. package/src/component/activityCompletions.ts +73 -0
  163. package/src/component/boundaries.ts +55 -0
  164. package/src/component/cleanup.test.ts +219 -0
  165. package/src/component/cleanup.ts +218 -0
  166. package/src/component/clocks.ts +26 -0
  167. package/src/component/config.test.ts +159 -0
  168. package/src/component/config.ts +145 -0
  169. package/src/component/convex.config.ts +7 -0
  170. package/src/component/core.test.ts +829 -0
  171. package/src/component/dashboard.scaling.test.ts +268 -0
  172. package/src/component/dashboard.ts +743 -0
  173. package/src/component/deferreds.ts +162 -0
  174. package/src/component/executions.ts +225 -0
  175. package/src/component/journalSteps.ts +252 -0
  176. package/src/component/logs.ts +152 -0
  177. package/src/component/onComplete.ts +170 -0
  178. package/src/component/payloads.ts +83 -0
  179. package/src/component/queries.ts +8 -0
  180. package/src/component/runner.ts +122 -0
  181. package/src/component/schema.ts +155 -0
  182. package/src/component/setup.test.ts +15 -0
  183. package/src/component/spans.ts +241 -0
  184. package/src/component/utils.test.ts +32 -0
  185. package/src/component/utils.ts +73 -0
  186. package/src/shared/constants.test.ts +14 -0
  187. package/src/shared/constants.ts +15 -0
  188. package/src/shared/validators.ts +98 -0
  189. package/src/test.d.ts +8 -0
  190. package/src/test.ts +17 -0
@@ -0,0 +1,152 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query } from "./_generated/server.js";
3
+ import { vLogLevel, vLogSource } from "../shared/validators.js";
4
+ import { nowTs } from "./utils.js";
5
+ import { persistBoundedPayload } from "./payloads.js";
6
+ import { MAX_LOG_DATA_BYTES } from "../shared/constants.js";
7
+
8
+ export const appendLog = mutation({
9
+ args: {
10
+ executionId: v.string(),
11
+ traceId: v.optional(v.string()),
12
+ spanId: v.optional(v.string()),
13
+ stepNumber: v.optional(v.number()),
14
+ level: vLogLevel,
15
+ message: v.string(),
16
+ source: vLogSource,
17
+ data: v.optional(v.any()),
18
+ timestamp: v.optional(v.number()),
19
+ },
20
+ returns: v.null(),
21
+ handler: async (ctx, args) => {
22
+ const dataPayload =
23
+ args.data === undefined
24
+ ? undefined
25
+ : await persistBoundedPayload(
26
+ ctx,
27
+ "logData",
28
+ args.data,
29
+ MAX_LOG_DATA_BYTES,
30
+ );
31
+
32
+ await ctx.db.insert("logs", {
33
+ executionId: args.executionId,
34
+ traceId: args.traceId,
35
+ spanId: args.spanId,
36
+ stepNumber: args.stepNumber,
37
+ level: args.level,
38
+ message: args.message,
39
+ source: args.source,
40
+ data: dataPayload?.preview,
41
+ timestamp: args.timestamp ?? nowTs(),
42
+ });
43
+ return null;
44
+ },
45
+ });
46
+
47
+ export const listExecutionLogs = query({
48
+ args: {
49
+ executionId: v.string(),
50
+ limit: v.number(),
51
+ afterTimestamp: v.optional(v.number()),
52
+ level: v.optional(vLogLevel),
53
+ source: v.optional(vLogSource),
54
+ spanId: v.optional(v.string()),
55
+ },
56
+ returns: v.any(),
57
+ handler: async (ctx, args) => {
58
+ const limit = Math.max(1, Math.min(args.limit, 500));
59
+
60
+ let rows = await ctx.db
61
+ .query("logs")
62
+ .withIndex("by_execution_time", (q) => q.eq("executionId", args.executionId))
63
+ .take(limit + 500);
64
+
65
+ if (args.afterTimestamp !== undefined) {
66
+ rows = rows.filter((row) => row.timestamp > args.afterTimestamp!);
67
+ }
68
+ if (args.level) {
69
+ rows = rows.filter((row) => row.level === args.level);
70
+ }
71
+ if (args.source) {
72
+ rows = rows.filter((row) => row.source === args.source);
73
+ }
74
+ if (args.spanId) {
75
+ rows = rows.filter((row) => row.spanId === args.spanId);
76
+ }
77
+
78
+ rows.sort((a, b) => a.timestamp - b.timestamp);
79
+ const page = rows.slice(0, limit);
80
+ const nextAfterTimestamp =
81
+ rows.length > limit ? page[page.length - 1]?.timestamp : undefined;
82
+
83
+ return { page, nextAfterTimestamp };
84
+ },
85
+ });
86
+
87
+ export const listSpanLogs = query({
88
+ args: {
89
+ spanId: v.string(),
90
+ limit: v.number(),
91
+ afterTimestamp: v.optional(v.number()),
92
+ },
93
+ returns: v.any(),
94
+ handler: async (ctx, args) => {
95
+ const limit = Math.max(1, Math.min(args.limit, 500));
96
+ let rows = await ctx.db
97
+ .query("logs")
98
+ .withIndex("by_span_time", (q) => q.eq("spanId", args.spanId))
99
+ .take(limit + 200);
100
+
101
+ if (args.afterTimestamp !== undefined) {
102
+ rows = rows.filter((row) => row.timestamp > args.afterTimestamp!);
103
+ }
104
+
105
+ rows.sort((a, b) => a.timestamp - b.timestamp);
106
+ const page = rows.slice(0, limit);
107
+ const nextAfterTimestamp =
108
+ rows.length > limit ? page[page.length - 1]?.timestamp : undefined;
109
+
110
+ return { page, nextAfterTimestamp };
111
+ },
112
+ });
113
+
114
+ /**
115
+ * Test helper for creating a simple log entry.
116
+ */
117
+ export const createLog = mutation({
118
+ args: {
119
+ executionId: v.string(),
120
+ traceId: v.optional(v.string()),
121
+ level: v.union(
122
+ v.literal("debug"),
123
+ v.literal("info"),
124
+ v.literal("warn"),
125
+ v.literal("error"),
126
+ ),
127
+ message: v.string(),
128
+ source: v.union(
129
+ v.literal("workflow"),
130
+ v.literal("activity"),
131
+ v.literal("engine"),
132
+ v.literal("system"),
133
+ ),
134
+ timestamp: v.number(),
135
+ spanId: v.optional(v.string()),
136
+ stepNumber: v.optional(v.number()),
137
+ },
138
+ returns: v.null(),
139
+ handler: async (ctx, args) => {
140
+ await ctx.db.insert("logs", {
141
+ executionId: args.executionId,
142
+ traceId: args.traceId,
143
+ spanId: args.spanId,
144
+ stepNumber: args.stepNumber,
145
+ level: args.level,
146
+ message: args.message,
147
+ source: args.source,
148
+ timestamp: args.timestamp,
149
+ });
150
+ return null;
151
+ },
152
+ });
@@ -0,0 +1,170 @@
1
+ import { vResult } from "@convex-dev/workpool";
2
+ import { v } from "convex/values";
3
+ import { mutation } from "./_generated/server.js";
4
+ import { scheduleRunnerFanIn } from "./boundaries.js";
5
+ import { nowTs } from "./utils.js";
6
+
7
+ const vCompletionContext = v.object({
8
+ executionId: v.string(),
9
+ stepNumber: v.number(),
10
+ spanId: v.optional(v.string()),
11
+ generation: v.number(),
12
+ });
13
+
14
+ /**
15
+ * Workpool onComplete handler for activity attempts.
16
+ *
17
+ * Invariants:
18
+ * - idempotent by workId (activityCompletions table)
19
+ * - stale generation callbacks are ignored
20
+ * - step and span completion are terminal/idempotent
21
+ */
22
+ export const handleActivityCompletion = mutation({
23
+ args: {
24
+ workId: v.string(),
25
+ context: vCompletionContext,
26
+ result: vResult,
27
+ },
28
+ returns: v.object({
29
+ processed: v.boolean(),
30
+ resumed: v.boolean(),
31
+ stale: v.boolean(),
32
+ }),
33
+ handler: async (ctx, args) => {
34
+ const completion = await ctx.db
35
+ .query("activityCompletions")
36
+ .withIndex("by_work_id", (q) => q.eq("workId", args.workId))
37
+ .unique();
38
+ if (completion) {
39
+ return { processed: false, resumed: false, stale: false };
40
+ }
41
+
42
+ await ctx.db.insert("activityCompletions", {
43
+ workId: args.workId,
44
+ executionId: args.context.executionId,
45
+ stepNumber: args.context.stepNumber,
46
+ processedAt: nowTs(),
47
+ });
48
+
49
+ const execution = await ctx.db
50
+ .query("executions")
51
+ .withIndex("by_execution_id", (q) =>
52
+ q.eq("executionId", args.context.executionId),
53
+ )
54
+ .unique();
55
+ if (!execution) {
56
+ throw new Error(`Execution not found: ${args.context.executionId}`);
57
+ }
58
+
59
+ if (execution.generation !== args.context.generation) {
60
+ await ctx.db.insert("logs", {
61
+ executionId: execution.executionId,
62
+ traceId: execution.traceId,
63
+ spanId: args.context.spanId,
64
+ stepNumber: args.context.stepNumber,
65
+ level: "warn",
66
+ source: "engine",
67
+ message: "Ignoring stale activity completion due to generation mismatch",
68
+ data: {
69
+ callbackGeneration: args.context.generation,
70
+ executionGeneration: execution.generation,
71
+ },
72
+ timestamp: nowTs(),
73
+ });
74
+ return { processed: true, resumed: false, stale: true };
75
+ }
76
+
77
+ const step = await ctx.db
78
+ .query("journalSteps")
79
+ .withIndex("by_execution_step", (q) =>
80
+ q
81
+ .eq("executionId", args.context.executionId)
82
+ .eq("stepNumber", args.context.stepNumber),
83
+ )
84
+ .unique();
85
+ if (!step) {
86
+ throw new Error(
87
+ `Journal step not found: ${args.context.executionId}/${args.context.stepNumber}`,
88
+ );
89
+ }
90
+
91
+ if (step.state !== "completed" && step.state !== "failed" && step.state !== "canceled") {
92
+ await ctx.db.patch(step._id, {
93
+ state:
94
+ args.result.kind === "success"
95
+ ? "completed"
96
+ : args.result.kind === "failed"
97
+ ? "failed"
98
+ : "canceled",
99
+ runResult:
100
+ args.result.kind === "success"
101
+ ? {
102
+ kind: "success",
103
+ valuePreview: args.result.returnValue,
104
+ }
105
+ : args.result.kind === "failed"
106
+ ? {
107
+ kind: "failed",
108
+ error: args.result.error,
109
+ }
110
+ : {
111
+ kind: "canceled",
112
+ },
113
+ completedAt: nowTs(),
114
+ });
115
+ }
116
+
117
+ if (args.context.spanId) {
118
+ const span = await ctx.db
119
+ .query("spans")
120
+ .withIndex("by_execution_span", (q) =>
121
+ q
122
+ .eq("executionId", args.context.executionId)
123
+ .eq("spanId", args.context.spanId!),
124
+ )
125
+ .unique();
126
+ if (span && span.status !== "ended") {
127
+ await ctx.db.patch(span._id, {
128
+ status: "ended",
129
+ endTime: nowTs(),
130
+ outputPreview:
131
+ args.result.kind === "success" ? args.result.returnValue : undefined,
132
+ error: args.result.kind === "failed" ? args.result.error : undefined,
133
+ });
134
+ }
135
+ }
136
+
137
+ await ctx.db.insert("logs", {
138
+ executionId: args.context.executionId,
139
+ traceId: execution.traceId,
140
+ spanId: args.context.spanId,
141
+ stepNumber: args.context.stepNumber,
142
+ level: args.result.kind === "failed" ? "error" : "info",
143
+ source: "activity",
144
+ message:
145
+ args.result.kind === "success"
146
+ ? "Activity completed"
147
+ : args.result.kind === "failed"
148
+ ? "Activity failed"
149
+ : "Activity canceled",
150
+ data: args.result,
151
+ timestamp: nowTs(),
152
+ });
153
+
154
+ let resumed = false;
155
+ if (
156
+ execution.runnerHandle &&
157
+ execution.status !== "interrupted" &&
158
+ execution.status !== "failed" &&
159
+ execution.status !== "completed"
160
+ ) {
161
+ await scheduleRunnerFanIn(ctx.scheduler, {
162
+ executionId: args.context.executionId,
163
+ generation: execution.generation,
164
+ });
165
+ resumed = true;
166
+ }
167
+
168
+ return { processed: true, resumed, stale: false };
169
+ },
170
+ });
@@ -0,0 +1,83 @@
1
+ import { v } from "convex/values";
2
+ import { mutation, query, type MutationCtx, type QueryCtx } from "./_generated/server.js";
3
+ import {
4
+ MAX_INLINE_PAYLOAD_BYTES,
5
+ MAX_PREVIEW_STRING_BYTES,
6
+ } from "../shared/constants.js";
7
+ import { valueSize, toPreview, nowTs } from "./utils.js";
8
+ import type { Doc, Id } from "./_generated/dataModel.js";
9
+ import { vPayloadKind } from "../shared/validators.js";
10
+
11
+ type PayloadRef = {
12
+ size: number;
13
+ ref: Id<"payloads"> | undefined;
14
+ preview: unknown;
15
+ };
16
+
17
+ type DbReaderCtx = Pick<QueryCtx, "db">;
18
+
19
+ export async function persistBoundedPayload(
20
+ ctx: MutationCtx,
21
+ kind: Doc<"payloads">["kind"],
22
+ value: unknown,
23
+ inlineLimit = MAX_INLINE_PAYLOAD_BYTES,
24
+ ): Promise<PayloadRef> {
25
+ const size = valueSize(value);
26
+ if (size <= inlineLimit) {
27
+ return { size, ref: undefined, preview: value };
28
+ }
29
+ const ref = await ctx.db.insert("payloads", {
30
+ kind,
31
+ data: value,
32
+ size,
33
+ createdAt: nowTs(),
34
+ });
35
+ return {
36
+ size,
37
+ ref,
38
+ preview: toPreview(value, MAX_PREVIEW_STRING_BYTES),
39
+ };
40
+ }
41
+
42
+ export async function hydratePayload<T>(
43
+ ctx: DbReaderCtx,
44
+ ref: Id<"payloads"> | undefined,
45
+ preview: T,
46
+ ): Promise<T> {
47
+ if (!ref) {
48
+ return preview;
49
+ }
50
+
51
+ const payload = await ctx.db.get(ref);
52
+ if (!payload || !("data" in payload)) {
53
+ return preview;
54
+ }
55
+
56
+ // Boundary note: payload rows store opaque user values. This helper trusts
57
+ // caller context for T and falls back to preview when offloaded data is missing.
58
+ return payload.data as T;
59
+ }
60
+
61
+ export const getPayload = query({
62
+ args: { payloadId: v.id("payloads") },
63
+ returns: v.any(),
64
+ handler: async (ctx, { payloadId }) => {
65
+ return await ctx.db.get(payloadId);
66
+ },
67
+ });
68
+
69
+ export const createPayload = mutation({
70
+ args: {
71
+ kind: vPayloadKind,
72
+ data: v.any(),
73
+ },
74
+ returns: v.id("payloads"),
75
+ handler: async (ctx, { kind, data }) => {
76
+ return await ctx.db.insert("payloads", {
77
+ kind,
78
+ data,
79
+ size: valueSize(data),
80
+ createdAt: nowTs(),
81
+ });
82
+ },
83
+ });
@@ -0,0 +1,8 @@
1
+ export {
2
+ listExecutions,
3
+ getExecution,
4
+ listExecutionSteps,
5
+ listExecutionSpans,
6
+ listExecutionLogs,
7
+ executionMetrics,
8
+ } from "./dashboard.js";
@@ -0,0 +1,122 @@
1
+ import { Workpool, vLogLevel, vRetryBehavior } from "@convex-dev/workpool";
2
+ import type { ComponentApi as WorkpoolComponentApi } from "@convex-dev/workpool/_generated/component.js";
3
+ import { componentsGeneric, makeFunctionReference, type FunctionHandle } from "convex/server";
4
+ import { v } from "convex/values";
5
+ import { mutation } from "./_generated/server.js";
6
+ import { deserializeRunnerHandle } from "./boundaries.js";
7
+
8
+ /**
9
+ * Fan-in runner trigger for workflow resumption.
10
+ *
11
+ * This mutation centralizes generation/terminal guards and schedules
12
+ * the host-provided runner handle stored on the execution row.
13
+ */
14
+ const components = componentsGeneric() as unknown as {
15
+ workpool: WorkpoolComponentApi<"workpool">;
16
+ };
17
+
18
+ const vWorkpoolOptions = v.optional(
19
+ v.object({
20
+ maxParallelism: v.optional(v.number()),
21
+ logLevel: v.optional(vLogLevel),
22
+ defaultRetryBehavior: v.optional(vRetryBehavior),
23
+ retryActionsByDefault: v.optional(v.boolean()),
24
+ }),
25
+ );
26
+
27
+ export const run = mutation({
28
+ args: {
29
+ executionId: v.string(),
30
+ generation: v.number(),
31
+ },
32
+ returns: v.object({
33
+ enqueued: v.boolean(),
34
+ reason: v.optional(v.string()),
35
+ }),
36
+ handler: async (ctx, args) => {
37
+ const execution = await ctx.db
38
+ .query("executions")
39
+ .withIndex("by_execution_id", (q) => q.eq("executionId", args.executionId))
40
+ .unique();
41
+
42
+ if (!execution) {
43
+ return { enqueued: false, reason: "execution_not_found" };
44
+ }
45
+
46
+ if (args.generation < execution.generation) {
47
+ return { enqueued: false, reason: "stale_generation" };
48
+ }
49
+
50
+ if (
51
+ execution.status === "completed" ||
52
+ execution.status === "failed" ||
53
+ execution.status === "interrupted"
54
+ ) {
55
+ return { enqueued: false, reason: "terminal_execution" };
56
+ }
57
+
58
+ if (!execution.runnerHandle) {
59
+ return { enqueued: false, reason: "runner_handle_missing" };
60
+ }
61
+
62
+ await ctx.scheduler.runAfter(
63
+ 0,
64
+ deserializeRunnerHandle(execution.runnerHandle),
65
+ {
66
+ executionId: execution.executionId,
67
+ generation: execution.generation,
68
+ },
69
+ );
70
+
71
+ return { enqueued: true };
72
+ },
73
+ });
74
+
75
+ export const enqueueActivity = mutation({
76
+ args: {
77
+ executionId: v.string(),
78
+ generation: v.number(),
79
+ workflowName: v.string(),
80
+ activityName: v.string(),
81
+ attempt: v.number(),
82
+ stepNumber: v.number(),
83
+ spanId: v.optional(v.string()),
84
+ activityWorkerHandle: v.string(),
85
+ workpoolOptions: vWorkpoolOptions,
86
+ },
87
+ returns: v.string(),
88
+ handler: async (ctx, args) => {
89
+ const workpool = new Workpool(components.workpool, args.workpoolOptions ?? {});
90
+ const activityHandle = args.activityWorkerHandle as FunctionHandle<
91
+ "action",
92
+ {
93
+ workflowName: string;
94
+ activityName: string;
95
+ executionId: string;
96
+ attempt: number;
97
+ },
98
+ unknown
99
+ >;
100
+
101
+ return await workpool.enqueueAction(
102
+ ctx,
103
+ activityHandle,
104
+ {
105
+ workflowName: args.workflowName,
106
+ activityName: args.activityName,
107
+ executionId: args.executionId,
108
+ attempt: args.attempt,
109
+ },
110
+ {
111
+ retry: false,
112
+ onComplete: makeFunctionReference("onComplete:handleActivityCompletion"),
113
+ context: {
114
+ executionId: args.executionId,
115
+ stepNumber: args.stepNumber,
116
+ spanId: args.spanId,
117
+ generation: args.generation,
118
+ },
119
+ },
120
+ );
121
+ },
122
+ });
@@ -0,0 +1,155 @@
1
+ import { defineSchema, defineTable } from "convex/server";
2
+ import { v } from "convex/values";
3
+ import {
4
+ vDeterminismSignature,
5
+ vExecutionStatus,
6
+ vLogLevel,
7
+ vLogSource,
8
+ vPayloadKind,
9
+ vResultEnvelope,
10
+ vSpanKind,
11
+ vSpanStatus,
12
+ vStepKind,
13
+ vStepState,
14
+ } from "../shared/validators.js";
15
+
16
+ export default defineSchema({
17
+ executions: defineTable({
18
+ workflowName: v.string(),
19
+ executionId: v.string(),
20
+ status: vExecutionStatus,
21
+ generation: v.number(),
22
+
23
+ tenantId: v.optional(v.string()),
24
+ parentExecutionId: v.optional(v.string()),
25
+
26
+ payloadRef: v.optional(v.id("payloads")),
27
+ payloadPreview: v.optional(v.any()),
28
+ payloadSize: v.number(),
29
+
30
+ resultKind: v.optional(v.union(v.literal("success"), v.literal("failure"))),
31
+ resultRef: v.optional(v.id("payloads")),
32
+ resultPreview: v.optional(v.any()),
33
+ error: v.optional(v.string()),
34
+
35
+ traceId: v.string(),
36
+ runnerHandle: v.optional(v.string()),
37
+ startedAt: v.number(),
38
+ completedAt: v.optional(v.number()),
39
+ lastResumedAt: v.optional(v.number()),
40
+ lastSuspendedAt: v.optional(v.number()),
41
+ })
42
+ .index("by_execution_id", ["executionId"])
43
+ .index("by_workflow_started", ["workflowName", "startedAt"])
44
+ .index("by_status_started", ["status", "startedAt"])
45
+ .index("by_started", ["startedAt"])
46
+ .index("by_tenant_started", ["tenantId", "startedAt"]),
47
+
48
+ journalSteps: defineTable({
49
+ executionId: v.string(),
50
+ stepNumber: v.number(),
51
+
52
+ kind: vStepKind,
53
+ name: v.string(),
54
+ signature: vDeterminismSignature,
55
+ state: vStepState,
56
+
57
+ attempt: v.optional(v.number()),
58
+ workId: v.optional(v.string()),
59
+ spanId: v.optional(v.string()),
60
+
61
+ inputRef: v.optional(v.id("payloads")),
62
+ inputPreview: v.optional(v.any()),
63
+ inputSize: v.number(),
64
+
65
+ runResult: v.optional(vResultEnvelope),
66
+
67
+ startedAt: v.number(),
68
+ completedAt: v.optional(v.number()),
69
+ resumeToken: v.optional(v.string()),
70
+ })
71
+ .index("by_execution_step", ["executionId", "stepNumber"])
72
+ .index("by_execution_state", ["executionId", "state"])
73
+ .index("by_work_id", ["workId"]),
74
+
75
+ activityCompletions: defineTable({
76
+ workId: v.string(),
77
+ executionId: v.string(),
78
+ stepNumber: v.number(),
79
+ processedAt: v.number(),
80
+ }).index("by_work_id", ["workId"]),
81
+
82
+ spans: defineTable({
83
+ executionId: v.string(),
84
+ traceId: v.string(),
85
+ spanId: v.string(),
86
+ parentSpanId: v.optional(v.string()),
87
+
88
+ stepNumber: v.optional(v.number()),
89
+ name: v.string(),
90
+ kind: vSpanKind,
91
+ status: vSpanStatus,
92
+
93
+ startTime: v.number(),
94
+ endTime: v.optional(v.number()),
95
+ attempt: v.optional(v.number()),
96
+
97
+ attributes: v.optional(v.any()),
98
+
99
+ inputRef: v.optional(v.id("payloads")),
100
+ inputPreview: v.optional(v.any()),
101
+ inputSize: v.optional(v.number()),
102
+
103
+ outputRef: v.optional(v.id("payloads")),
104
+ outputPreview: v.optional(v.any()),
105
+ outputSize: v.optional(v.number()),
106
+
107
+ error: v.optional(v.string()),
108
+ })
109
+ .index("by_execution_start", ["executionId", "startTime"])
110
+ .index("by_trace_start", ["traceId", "startTime"])
111
+ .index("by_execution_span", ["executionId", "spanId"]),
112
+
113
+ logs: defineTable({
114
+ executionId: v.string(),
115
+ traceId: v.optional(v.string()),
116
+ spanId: v.optional(v.string()),
117
+ stepNumber: v.optional(v.number()),
118
+
119
+ level: vLogLevel,
120
+ message: v.string(),
121
+ data: v.optional(v.any()),
122
+ source: vLogSource,
123
+
124
+ timestamp: v.number(),
125
+ })
126
+ .index("by_execution_time", ["executionId", "timestamp"])
127
+ .index("by_span_time", ["spanId", "timestamp"])
128
+ .index("by_level_time", ["level", "timestamp"]),
129
+
130
+ deferreds: defineTable({
131
+ executionId: v.string(),
132
+ deferredName: v.string(),
133
+
134
+ completed: v.boolean(),
135
+ exitRef: v.optional(v.id("payloads")),
136
+ exitPreview: v.optional(v.any()),
137
+ completedAt: v.optional(v.number()),
138
+ }).index("by_execution_name", ["executionId", "deferredName"]),
139
+
140
+ payloads: defineTable({
141
+ kind: vPayloadKind,
142
+ data: v.any(),
143
+ size: v.number(),
144
+ createdAt: v.number(),
145
+ }).index("by_created", ["createdAt"]),
146
+
147
+ config: defineTable({
148
+ key: v.string(),
149
+ maxInlineBytes: v.optional(v.number()),
150
+ maxLogDataBytes: v.optional(v.number()),
151
+ maxSpanAttrBytes: v.optional(v.number()),
152
+ retentionDays: v.optional(v.number()),
153
+ defaultLogLevel: v.optional(vLogLevel),
154
+ }).index("by_key", ["key"]),
155
+ });
@@ -0,0 +1,15 @@
1
+ /// <reference types="vite/client" />
2
+ import { convexTest } from "convex-test";
3
+ import workpool from "@convex-dev/workpool/test";
4
+ import schema from "./schema.js";
5
+
6
+ const modules = import.meta.glob("./**/*.ts");
7
+
8
+ export function initConvexTest() {
9
+ const t = convexTest(schema, modules);
10
+ workpool.register(t);
11
+ return t;
12
+ }
13
+
14
+ import { test } from "vitest";
15
+ test("setup", () => {});