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,743 @@
1
+ import { v } from "convex/values";
2
+ import { query, type QueryCtx } from "./_generated/server.js";
3
+ import {
4
+ DEFAULT_EXECUTIONS_PAGE_SIZE,
5
+ DEFAULT_LOGS_PAGE_SIZE,
6
+ DEFAULT_SPANS_PAGE_SIZE,
7
+ DEFAULT_STEPS_PAGE_SIZE,
8
+ MAX_PAGE_SIZE,
9
+ } from "../shared/constants.js";
10
+ import {
11
+ vDeterminismSignature,
12
+ vExecutionStatus,
13
+ vLogLevel,
14
+ vLogSource,
15
+ vResultEnvelope,
16
+ vSortOrder,
17
+ vSpanKind,
18
+ vSpanStatus,
19
+ vStepKind,
20
+ vStepState,
21
+ } from "../shared/validators.js";
22
+ import type { ExecutionStatus, SortOrder } from "../shared/validators.js";
23
+
24
+ const DEFAULT_SCAN_MULTIPLIER = 8;
25
+ const MAX_SCAN_ROWS = 2000;
26
+
27
+ const vExecutionDoc = v.object({
28
+ _id: v.id("executions"),
29
+ _creationTime: v.number(),
30
+ workflowName: v.string(),
31
+ executionId: v.string(),
32
+ status: vExecutionStatus,
33
+ generation: v.number(),
34
+ tenantId: v.optional(v.string()),
35
+ parentExecutionId: v.optional(v.string()),
36
+ payloadRef: v.optional(v.id("payloads")),
37
+ payloadPreview: v.optional(v.any()),
38
+ payloadSize: v.number(),
39
+ resultKind: v.optional(v.union(v.literal("success"), v.literal("failure"))),
40
+ resultRef: v.optional(v.id("payloads")),
41
+ resultPreview: v.optional(v.any()),
42
+ error: v.optional(v.string()),
43
+ traceId: v.string(),
44
+ runnerHandle: v.optional(v.string()),
45
+ startedAt: v.number(),
46
+ completedAt: v.optional(v.number()),
47
+ lastResumedAt: v.optional(v.number()),
48
+ lastSuspendedAt: v.optional(v.number()),
49
+ });
50
+
51
+ const vJournalStepDoc = v.object({
52
+ _id: v.id("journalSteps"),
53
+ _creationTime: v.number(),
54
+ executionId: v.string(),
55
+ stepNumber: v.number(),
56
+ kind: vStepKind,
57
+ name: v.string(),
58
+ signature: vDeterminismSignature,
59
+ state: vStepState,
60
+ attempt: v.optional(v.number()),
61
+ workId: v.optional(v.string()),
62
+ spanId: v.optional(v.string()),
63
+ inputRef: v.optional(v.id("payloads")),
64
+ inputPreview: v.optional(v.any()),
65
+ inputSize: v.number(),
66
+ runResult: v.optional(vResultEnvelope),
67
+ startedAt: v.number(),
68
+ completedAt: v.optional(v.number()),
69
+ resumeToken: v.optional(v.string()),
70
+ });
71
+
72
+ const vSpanDoc = v.object({
73
+ _id: v.id("spans"),
74
+ _creationTime: v.number(),
75
+ executionId: v.string(),
76
+ traceId: v.string(),
77
+ spanId: v.string(),
78
+ parentSpanId: v.optional(v.string()),
79
+ stepNumber: v.optional(v.number()),
80
+ name: v.string(),
81
+ kind: vSpanKind,
82
+ status: vSpanStatus,
83
+ startTime: v.number(),
84
+ endTime: v.optional(v.number()),
85
+ attempt: v.optional(v.number()),
86
+ attributes: v.optional(v.any()),
87
+ inputRef: v.optional(v.id("payloads")),
88
+ inputPreview: v.optional(v.any()),
89
+ inputSize: v.optional(v.number()),
90
+ outputRef: v.optional(v.id("payloads")),
91
+ outputPreview: v.optional(v.any()),
92
+ outputSize: v.optional(v.number()),
93
+ error: v.optional(v.string()),
94
+ });
95
+
96
+ const vLogDoc = v.object({
97
+ _id: v.id("logs"),
98
+ _creationTime: v.number(),
99
+ executionId: v.string(),
100
+ traceId: v.optional(v.string()),
101
+ spanId: v.optional(v.string()),
102
+ stepNumber: v.optional(v.number()),
103
+ level: vLogLevel,
104
+ message: v.string(),
105
+ data: v.optional(v.any()),
106
+ source: vLogSource,
107
+ timestamp: v.number(),
108
+ });
109
+
110
+ const vExecutionListResult = v.object({
111
+ page: v.array(vExecutionDoc),
112
+ cursor: v.union(
113
+ v.object({
114
+ startedAt: v.number(),
115
+ executionId: v.string(),
116
+ }),
117
+ v.null(),
118
+ ),
119
+ isDone: v.boolean(),
120
+ });
121
+
122
+ const vExecutionDetailsResult = v.union(
123
+ v.object({
124
+ execution: vExecutionDoc,
125
+ steps: v.array(vJournalStepDoc),
126
+ spans: v.array(vSpanDoc),
127
+ logs: v.array(vLogDoc),
128
+ }),
129
+ v.null(),
130
+ );
131
+
132
+ const vExecutionStepsResult = v.object({
133
+ page: v.array(vJournalStepDoc),
134
+ cursor: v.union(v.number(), v.null()),
135
+ isDone: v.boolean(),
136
+ });
137
+
138
+ const vExecutionSpansResult = v.object({
139
+ page: v.array(vSpanDoc),
140
+ cursor: v.union(
141
+ v.object({
142
+ startTime: v.number(),
143
+ spanId: v.id("spans"),
144
+ }),
145
+ v.null(),
146
+ ),
147
+ isDone: v.boolean(),
148
+ });
149
+
150
+ const vExecutionLogsResult = v.object({
151
+ page: v.array(vLogDoc),
152
+ cursor: v.union(
153
+ v.object({
154
+ timestamp: v.number(),
155
+ logId: v.id("logs"),
156
+ }),
157
+ v.null(),
158
+ ),
159
+ isDone: v.boolean(),
160
+ });
161
+
162
+ const vExecutionMetricsResult = v.object({
163
+ total: v.number(),
164
+ completed: v.number(),
165
+ failed: v.number(),
166
+ interrupted: v.number(),
167
+ active: v.number(),
168
+ successRate: v.number(),
169
+ avgDurationMs: v.number(),
170
+ p50DurationMs: v.number(),
171
+ p95DurationMs: v.number(),
172
+ });
173
+
174
+ function clampLimit(limit: number | undefined, fallback: number) {
175
+ return Math.max(1, Math.min(limit ?? fallback, MAX_PAGE_SIZE));
176
+ }
177
+
178
+ function scanLimitFor(limit: number, multiplier = DEFAULT_SCAN_MULTIPLIER) {
179
+ return Math.min(MAX_SCAN_ROWS, Math.max(limit + 1, limit * multiplier));
180
+ }
181
+
182
+ function idCmp(a: string, b: string) {
183
+ return a < b ? -1 : a > b ? 1 : 0;
184
+ }
185
+
186
+ function applyExecutionCursor(
187
+ row: { startedAt: number; executionId: string },
188
+ args: {
189
+ cursorStartedAt?: number;
190
+ cursorExecutionId?: string;
191
+ order: SortOrder;
192
+ },
193
+ ) {
194
+ if (args.cursorStartedAt === undefined) {
195
+ return true;
196
+ }
197
+
198
+ if (args.order === "asc") {
199
+ if (row.startedAt < args.cursorStartedAt) return false;
200
+ if (
201
+ row.startedAt === args.cursorStartedAt &&
202
+ args.cursorExecutionId &&
203
+ idCmp(row.executionId, args.cursorExecutionId) <= 0
204
+ ) {
205
+ return false;
206
+ }
207
+ return true;
208
+ }
209
+
210
+ if (row.startedAt > args.cursorStartedAt) return false;
211
+ if (
212
+ row.startedAt === args.cursorStartedAt &&
213
+ args.cursorExecutionId &&
214
+ idCmp(row.executionId, args.cursorExecutionId) >= 0
215
+ ) {
216
+ return false;
217
+ }
218
+ return true;
219
+ }
220
+
221
+ async function executionWindow(
222
+ ctx: QueryCtx,
223
+ args: {
224
+ workflowName?: string;
225
+ status?: ExecutionStatus;
226
+ tenantId?: string;
227
+ startedAfter?: number;
228
+ startedBefore?: number;
229
+ order: SortOrder;
230
+ scanLimit: number;
231
+ },
232
+ ) {
233
+ if (args.tenantId) {
234
+ if (args.startedAfter !== undefined && args.startedBefore !== undefined) {
235
+ return await ctx.db
236
+ .query("executions")
237
+ .withIndex("by_tenant_started", (q) =>
238
+ q
239
+ .eq("tenantId", args.tenantId!)
240
+ .gte("startedAt", args.startedAfter!)
241
+ .lte("startedAt", args.startedBefore!),
242
+ )
243
+ .order(args.order)
244
+ .take(args.scanLimit);
245
+ }
246
+ if (args.startedAfter !== undefined) {
247
+ return await ctx.db
248
+ .query("executions")
249
+ .withIndex("by_tenant_started", (q) =>
250
+ q.eq("tenantId", args.tenantId!).gte("startedAt", args.startedAfter!),
251
+ )
252
+ .order(args.order)
253
+ .take(args.scanLimit);
254
+ }
255
+ if (args.startedBefore !== undefined) {
256
+ return await ctx.db
257
+ .query("executions")
258
+ .withIndex("by_tenant_started", (q) =>
259
+ q.eq("tenantId", args.tenantId!).lte("startedAt", args.startedBefore!),
260
+ )
261
+ .order(args.order)
262
+ .take(args.scanLimit);
263
+ }
264
+ return await ctx.db
265
+ .query("executions")
266
+ .withIndex("by_tenant_started", (q) => q.eq("tenantId", args.tenantId!))
267
+ .order(args.order)
268
+ .take(args.scanLimit);
269
+ }
270
+
271
+ if (args.workflowName) {
272
+ if (args.startedAfter !== undefined && args.startedBefore !== undefined) {
273
+ return await ctx.db
274
+ .query("executions")
275
+ .withIndex("by_workflow_started", (q) =>
276
+ q
277
+ .eq("workflowName", args.workflowName!)
278
+ .gte("startedAt", args.startedAfter!)
279
+ .lte("startedAt", args.startedBefore!),
280
+ )
281
+ .order(args.order)
282
+ .take(args.scanLimit);
283
+ }
284
+ if (args.startedAfter !== undefined) {
285
+ return await ctx.db
286
+ .query("executions")
287
+ .withIndex("by_workflow_started", (q) =>
288
+ q.eq("workflowName", args.workflowName!).gte("startedAt", args.startedAfter!),
289
+ )
290
+ .order(args.order)
291
+ .take(args.scanLimit);
292
+ }
293
+ if (args.startedBefore !== undefined) {
294
+ return await ctx.db
295
+ .query("executions")
296
+ .withIndex("by_workflow_started", (q) =>
297
+ q.eq("workflowName", args.workflowName!).lte("startedAt", args.startedBefore!),
298
+ )
299
+ .order(args.order)
300
+ .take(args.scanLimit);
301
+ }
302
+ return await ctx.db
303
+ .query("executions")
304
+ .withIndex("by_workflow_started", (q) => q.eq("workflowName", args.workflowName!))
305
+ .order(args.order)
306
+ .take(args.scanLimit);
307
+ }
308
+
309
+ if (args.status) {
310
+ if (args.startedAfter !== undefined && args.startedBefore !== undefined) {
311
+ return await ctx.db
312
+ .query("executions")
313
+ .withIndex("by_status_started", (q) =>
314
+ q
315
+ .eq("status", args.status!)
316
+ .gte("startedAt", args.startedAfter!)
317
+ .lte("startedAt", args.startedBefore!),
318
+ )
319
+ .order(args.order)
320
+ .take(args.scanLimit);
321
+ }
322
+ if (args.startedAfter !== undefined) {
323
+ return await ctx.db
324
+ .query("executions")
325
+ .withIndex("by_status_started", (q) =>
326
+ q.eq("status", args.status!).gte("startedAt", args.startedAfter!),
327
+ )
328
+ .order(args.order)
329
+ .take(args.scanLimit);
330
+ }
331
+ if (args.startedBefore !== undefined) {
332
+ return await ctx.db
333
+ .query("executions")
334
+ .withIndex("by_status_started", (q) =>
335
+ q.eq("status", args.status!).lte("startedAt", args.startedBefore!),
336
+ )
337
+ .order(args.order)
338
+ .take(args.scanLimit);
339
+ }
340
+ return await ctx.db
341
+ .query("executions")
342
+ .withIndex("by_status_started", (q) => q.eq("status", args.status!))
343
+ .order(args.order)
344
+ .take(args.scanLimit);
345
+ }
346
+
347
+ if (args.startedAfter !== undefined && args.startedBefore !== undefined) {
348
+ return await ctx.db
349
+ .query("executions")
350
+ .withIndex("by_started", (q) =>
351
+ q.gte("startedAt", args.startedAfter!).lte("startedAt", args.startedBefore!),
352
+ )
353
+ .order(args.order)
354
+ .take(args.scanLimit);
355
+ }
356
+ if (args.startedAfter !== undefined) {
357
+ return await ctx.db
358
+ .query("executions")
359
+ .withIndex("by_started", (q) => q.gte("startedAt", args.startedAfter!))
360
+ .order(args.order)
361
+ .take(args.scanLimit);
362
+ }
363
+ if (args.startedBefore !== undefined) {
364
+ return await ctx.db
365
+ .query("executions")
366
+ .withIndex("by_started", (q) => q.lte("startedAt", args.startedBefore!))
367
+ .order(args.order)
368
+ .take(args.scanLimit);
369
+ }
370
+
371
+ return await ctx.db
372
+ .query("executions")
373
+ .withIndex("by_started")
374
+ .order(args.order)
375
+ .take(args.scanLimit);
376
+ }
377
+
378
+ export const listExecutions = query({
379
+ args: {
380
+ workflowName: v.optional(v.string()),
381
+ status: v.optional(vExecutionStatus),
382
+ tenantId: v.optional(v.string()),
383
+ startedAfter: v.optional(v.number()),
384
+ startedBefore: v.optional(v.number()),
385
+ limit: v.optional(v.number()),
386
+ cursorStartedAt: v.optional(v.number()),
387
+ cursorExecutionId: v.optional(v.string()),
388
+ order: v.optional(vSortOrder),
389
+ },
390
+ returns: vExecutionListResult,
391
+ handler: async (ctx, args) => {
392
+ const limit = clampLimit(args.limit, DEFAULT_EXECUTIONS_PAGE_SIZE);
393
+ const order = args.order ?? "desc";
394
+ const scanLimit = scanLimitFor(limit, 10);
395
+
396
+ const rows = await executionWindow(ctx, {
397
+ workflowName: args.workflowName,
398
+ status: args.status,
399
+ tenantId: args.tenantId,
400
+ startedAfter: args.startedAfter,
401
+ startedBefore: args.startedBefore,
402
+ order,
403
+ scanLimit,
404
+ });
405
+
406
+ const filtered = rows
407
+ .filter((row) => {
408
+ if (args.workflowName && row.workflowName !== args.workflowName) return false;
409
+ if (args.status && row.status !== args.status) return false;
410
+ if (args.tenantId && row.tenantId !== args.tenantId) return false;
411
+ return applyExecutionCursor(row, {
412
+ cursorStartedAt: args.cursorStartedAt,
413
+ cursorExecutionId: args.cursorExecutionId,
414
+ order,
415
+ });
416
+ })
417
+ .sort((a, b) => {
418
+ if (a.startedAt !== b.startedAt) {
419
+ return order === "asc" ? a.startedAt - b.startedAt : b.startedAt - a.startedAt;
420
+ }
421
+ return order === "asc"
422
+ ? idCmp(a.executionId, b.executionId)
423
+ : idCmp(b.executionId, a.executionId);
424
+ });
425
+
426
+ const page = filtered.slice(0, limit);
427
+ const scannedFullWindow = rows.length >= scanLimit;
428
+ const hasMore = filtered.length > limit || (page.length === limit && scannedFullWindow);
429
+ const next = hasMore ? page[page.length - 1] : undefined;
430
+
431
+ return {
432
+ page,
433
+ cursor: next
434
+ ? {
435
+ startedAt: next.startedAt,
436
+ executionId: next.executionId,
437
+ }
438
+ : null,
439
+ isDone: !hasMore,
440
+ };
441
+ },
442
+ });
443
+
444
+ export const getExecution = query({
445
+ args: {
446
+ executionId: v.string(),
447
+ },
448
+ returns: vExecutionDetailsResult,
449
+ handler: async (ctx, { executionId }) => {
450
+ const execution = await ctx.db
451
+ .query("executions")
452
+ .withIndex("by_execution_id", (q) => q.eq("executionId", executionId))
453
+ .unique();
454
+
455
+ if (!execution) {
456
+ return null;
457
+ }
458
+
459
+ const [steps, spans, logs] = await Promise.all([
460
+ ctx.db
461
+ .query("journalSteps")
462
+ .withIndex("by_execution_step", (q) => q.eq("executionId", executionId))
463
+ .collect(),
464
+ ctx.db
465
+ .query("spans")
466
+ .withIndex("by_execution_start", (q) => q.eq("executionId", executionId))
467
+ .collect(),
468
+ ctx.db
469
+ .query("logs")
470
+ .withIndex("by_execution_time", (q) => q.eq("executionId", executionId))
471
+ .order("desc")
472
+ .take(DEFAULT_LOGS_PAGE_SIZE),
473
+ ]);
474
+
475
+ steps.sort((a, b) => a.stepNumber - b.stepNumber);
476
+ spans.sort((a, b) => a.startTime - b.startTime);
477
+
478
+ return {
479
+ execution,
480
+ steps,
481
+ spans,
482
+ logs,
483
+ };
484
+ },
485
+ });
486
+
487
+ export const listExecutionSteps = query({
488
+ args: {
489
+ executionId: v.string(),
490
+ state: v.optional(vStepState),
491
+ limit: v.optional(v.number()),
492
+ afterStepNumber: v.optional(v.number()),
493
+ },
494
+ returns: vExecutionStepsResult,
495
+ handler: async (ctx, args) => {
496
+ const limit = clampLimit(args.limit, DEFAULT_STEPS_PAGE_SIZE);
497
+ const scanLimit = scanLimitFor(limit, 10);
498
+
499
+ const rows =
500
+ args.afterStepNumber !== undefined
501
+ ? await ctx.db
502
+ .query("journalSteps")
503
+ .withIndex("by_execution_step", (q) =>
504
+ q.eq("executionId", args.executionId).gt("stepNumber", args.afterStepNumber!),
505
+ )
506
+ .take(scanLimit)
507
+ : await ctx.db
508
+ .query("journalSteps")
509
+ .withIndex("by_execution_step", (q) => q.eq("executionId", args.executionId))
510
+ .take(scanLimit);
511
+
512
+ const filtered = rows.filter((row) => (args.state ? row.state === args.state : true));
513
+
514
+ const page = filtered.slice(0, limit);
515
+ const hasMore = filtered.length > limit || (page.length === limit && rows.length >= scanLimit);
516
+ const nextAfterStepNumber = hasMore ? page[page.length - 1]?.stepNumber : undefined;
517
+
518
+ return {
519
+ page,
520
+ cursor: nextAfterStepNumber ?? null,
521
+ isDone: !hasMore,
522
+ };
523
+ },
524
+ });
525
+
526
+ export const listExecutionSpans = query({
527
+ args: {
528
+ executionId: v.string(),
529
+ kind: v.optional(vSpanKind),
530
+ status: v.optional(vSpanStatus),
531
+ limit: v.optional(v.number()),
532
+ afterStartTime: v.optional(v.number()),
533
+ afterSpanId: v.optional(v.string()),
534
+ },
535
+ returns: vExecutionSpansResult,
536
+ handler: async (ctx, args) => {
537
+ const limit = clampLimit(args.limit, DEFAULT_SPANS_PAGE_SIZE);
538
+ const scanLimit = scanLimitFor(limit, 10);
539
+
540
+ const rows =
541
+ args.afterStartTime !== undefined
542
+ ? await ctx.db
543
+ .query("spans")
544
+ .withIndex("by_execution_start", (q) =>
545
+ q.eq("executionId", args.executionId).gte("startTime", args.afterStartTime!),
546
+ )
547
+ .take(scanLimit)
548
+ : await ctx.db
549
+ .query("spans")
550
+ .withIndex("by_execution_start", (q) => q.eq("executionId", args.executionId))
551
+ .take(scanLimit);
552
+
553
+ const filtered = rows
554
+ .filter((row) => {
555
+ if (args.kind && row.kind !== args.kind) return false;
556
+ if (args.status && row.status !== args.status) return false;
557
+ if (args.afterStartTime === undefined) return true;
558
+ if (row.startTime > args.afterStartTime) return true;
559
+ if (row.startTime < args.afterStartTime) return false;
560
+ if (!args.afterSpanId) return false;
561
+ return idCmp(row._id, args.afterSpanId) > 0;
562
+ })
563
+ .sort((a, b) => {
564
+ if (a.startTime !== b.startTime) return a.startTime - b.startTime;
565
+ return idCmp(a._id, b._id);
566
+ });
567
+
568
+ const page = filtered.slice(0, limit);
569
+ const hasMore = filtered.length > limit || (page.length === limit && rows.length >= scanLimit);
570
+ const next = hasMore ? page[page.length - 1] : undefined;
571
+
572
+ return {
573
+ page,
574
+ cursor: next
575
+ ? {
576
+ startTime: next.startTime,
577
+ spanId: next._id,
578
+ }
579
+ : null,
580
+ isDone: !hasMore,
581
+ };
582
+ },
583
+ });
584
+
585
+ export const listExecutionLogs = query({
586
+ args: {
587
+ executionId: v.string(),
588
+ level: v.optional(vLogLevel),
589
+ source: v.optional(vLogSource),
590
+ spanId: v.optional(v.string()),
591
+ limit: v.optional(v.number()),
592
+ afterTimestamp: v.optional(v.number()),
593
+ afterLogId: v.optional(v.string()),
594
+ order: v.optional(vSortOrder),
595
+ },
596
+ returns: vExecutionLogsResult,
597
+ handler: async (ctx, args) => {
598
+ const limit = clampLimit(args.limit, DEFAULT_LOGS_PAGE_SIZE);
599
+ const order = args.order ?? "desc";
600
+ const scanLimit = scanLimitFor(limit, 12);
601
+
602
+ const rows =
603
+ args.afterTimestamp === undefined
604
+ ? await ctx.db
605
+ .query("logs")
606
+ .withIndex("by_execution_time", (q) => q.eq("executionId", args.executionId))
607
+ .order(order)
608
+ .take(scanLimit)
609
+ : order === "asc"
610
+ ? await ctx.db
611
+ .query("logs")
612
+ .withIndex("by_execution_time", (q) =>
613
+ q.eq("executionId", args.executionId).gte("timestamp", args.afterTimestamp!),
614
+ )
615
+ .order(order)
616
+ .take(scanLimit)
617
+ : await ctx.db
618
+ .query("logs")
619
+ .withIndex("by_execution_time", (q) =>
620
+ q.eq("executionId", args.executionId).lte("timestamp", args.afterTimestamp!),
621
+ )
622
+ .order(order)
623
+ .take(scanLimit);
624
+
625
+ const filtered = rows
626
+ .filter((row) => {
627
+ if (args.level && row.level !== args.level) return false;
628
+ if (args.source && row.source !== args.source) return false;
629
+ if (args.spanId && row.spanId !== args.spanId) return false;
630
+
631
+ if (args.afterTimestamp === undefined) return true;
632
+ if (row.timestamp !== args.afterTimestamp) {
633
+ return order === "asc"
634
+ ? row.timestamp > args.afterTimestamp
635
+ : row.timestamp < args.afterTimestamp;
636
+ }
637
+ if (!args.afterLogId) {
638
+ return false;
639
+ }
640
+ return order === "asc"
641
+ ? idCmp(row._id, args.afterLogId) > 0
642
+ : idCmp(row._id, args.afterLogId) < 0;
643
+ })
644
+ .sort((a, b) => {
645
+ if (a.timestamp !== b.timestamp) {
646
+ return order === "asc" ? a.timestamp - b.timestamp : b.timestamp - a.timestamp;
647
+ }
648
+ return order === "asc" ? idCmp(a._id, b._id) : idCmp(b._id, a._id);
649
+ });
650
+
651
+ const page = filtered.slice(0, limit);
652
+ const hasMore = filtered.length > limit || (page.length === limit && rows.length >= scanLimit);
653
+ const next = hasMore ? page[page.length - 1] : undefined;
654
+
655
+ return {
656
+ page,
657
+ cursor: next
658
+ ? {
659
+ timestamp: next.timestamp,
660
+ logId: next._id,
661
+ }
662
+ : null,
663
+ isDone: !hasMore,
664
+ };
665
+ },
666
+ });
667
+
668
+ export const executionMetrics = query({
669
+ args: {
670
+ workflowName: v.optional(v.string()),
671
+ startedAfter: v.optional(v.number()),
672
+ startedBefore: v.optional(v.number()),
673
+ },
674
+ returns: vExecutionMetricsResult,
675
+ handler: async (ctx, args) => {
676
+ const rows =
677
+ args.startedAfter !== undefined && args.startedBefore !== undefined
678
+ ? await ctx.db
679
+ .query("executions")
680
+ .withIndex("by_started", (q) =>
681
+ q.gte("startedAt", args.startedAfter!).lte("startedAt", args.startedBefore!),
682
+ )
683
+ .collect()
684
+ : args.startedAfter !== undefined
685
+ ? await ctx.db
686
+ .query("executions")
687
+ .withIndex("by_started", (q) => q.gte("startedAt", args.startedAfter!))
688
+ .collect()
689
+ : args.startedBefore !== undefined
690
+ ? await ctx.db
691
+ .query("executions")
692
+ .withIndex("by_started", (q) => q.lte("startedAt", args.startedBefore!))
693
+ .collect()
694
+ : await ctx.db.query("executions").withIndex("by_started").collect();
695
+
696
+ const filtered = rows.filter((row) => {
697
+ if (args.workflowName && row.workflowName !== args.workflowName) return false;
698
+ return true;
699
+ });
700
+
701
+ let completed = 0;
702
+ let failed = 0;
703
+ let interrupted = 0;
704
+ let active = 0;
705
+ const durations: number[] = [];
706
+
707
+ for (const row of filtered) {
708
+ if (row.status === "completed") {
709
+ completed += 1;
710
+ } else if (row.status === "failed") {
711
+ failed += 1;
712
+ } else if (row.status === "interrupted") {
713
+ interrupted += 1;
714
+ } else if (row.status === "pending" || row.status === "running" || row.status === "suspended") {
715
+ active += 1;
716
+ }
717
+
718
+ if (row.completedAt !== undefined) {
719
+ durations.push(Math.max(0, row.completedAt - row.startedAt));
720
+ }
721
+ }
722
+
723
+ durations.sort((a, b) => a - b);
724
+ const avgDurationMs = durations.length
725
+ ? Math.round(durations.reduce((sum, v0) => sum + v0, 0) / durations.length)
726
+ : 0;
727
+
728
+ const p50DurationMs = durations.length ? durations[Math.floor(durations.length * 0.5)] : 0;
729
+ const p95DurationMs = durations.length ? durations[Math.floor(durations.length * 0.95)] : 0;
730
+
731
+ return {
732
+ total: filtered.length,
733
+ completed,
734
+ failed,
735
+ interrupted,
736
+ active,
737
+ successRate: filtered.length ? completed / filtered.length : 0,
738
+ avgDurationMs,
739
+ p50DurationMs,
740
+ p95DurationMs,
741
+ };
742
+ },
743
+ });