@trigger.dev/convex 0.0.1 → 4.8.2
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/LICENSE +201 -0
- package/README.md +170 -1
- package/dist/client/index.d.ts +34 -0
- package/dist/client/index.js +45 -0
- package/dist/component/_generated/component.d.ts +11 -0
- package/dist/component/_generated/component.js +1 -0
- package/dist/component/api.d.ts +9 -0
- package/dist/component/api.js +2 -0
- package/dist/component/apiClient.d.ts +10 -0
- package/dist/component/apiClient.js +48 -0
- package/dist/component/convex.config.d.ts +2 -0
- package/dist/component/convex.config.js +2 -0
- package/dist/component/runs.d.ts +132 -0
- package/dist/component/runs.js +196 -0
- package/dist/component/schema.d.ts +100 -0
- package/dist/component/schema.js +18 -0
- package/dist/component/server.d.ts +8 -0
- package/dist/component/server.js +6 -0
- package/dist/component/shared.d.ts +91 -0
- package/dist/component/shared.js +47 -0
- package/dist/component/worker.d.ts +14 -0
- package/dist/component/worker.js +105 -0
- package/package.json +39 -6
- package/src/component/_generated/component.ts +18 -0
- package/src/component/api.ts +6 -0
- package/src/component/apiClient.ts +57 -0
- package/src/component/convex.config.ts +3 -0
- package/src/component/runs.test.ts +354 -0
- package/src/component/runs.ts +227 -0
- package/src/component/schema.ts +19 -0
- package/src/component/server.ts +19 -0
- package/src/component/shared.ts +61 -0
- package/src/component/worker.ts +131 -0
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
import {
|
|
2
|
+
paginationOptsValidator,
|
|
3
|
+
type FunctionHandle,
|
|
4
|
+
type GenericMutationCtx,
|
|
5
|
+
} from "convex/server";
|
|
6
|
+
import { v } from "convex/values";
|
|
7
|
+
import { internal } from "./api.js";
|
|
8
|
+
import { internalMutation, internalQuery, mutation, query, type DataModel } from "./server.js";
|
|
9
|
+
import {
|
|
10
|
+
MAX_INLINE_BYTES,
|
|
11
|
+
MAX_PAGE_BYTES,
|
|
12
|
+
SUBMISSION_WINDOW_MS,
|
|
13
|
+
vConfig,
|
|
14
|
+
vRun,
|
|
15
|
+
vTriggerOptions,
|
|
16
|
+
type Run,
|
|
17
|
+
} from "./shared.js";
|
|
18
|
+
|
|
19
|
+
type RunDoc = DataModel["runs"]["document"];
|
|
20
|
+
|
|
21
|
+
export function toRun(run: RunDoc): Run {
|
|
22
|
+
const { _id, _creationTime, generation, checks, failures, callback, ...fields } = run;
|
|
23
|
+
return { id: _id, createdAt: _creationTime, ...fields };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export const trigger = mutation({
|
|
27
|
+
args: {
|
|
28
|
+
taskId: v.string(),
|
|
29
|
+
payload: v.optional(v.any()),
|
|
30
|
+
options: vTriggerOptions,
|
|
31
|
+
config: vConfig,
|
|
32
|
+
idempotencyKey: v.optional(v.string()),
|
|
33
|
+
callback: v.optional(v.string()),
|
|
34
|
+
},
|
|
35
|
+
returns: v.string(),
|
|
36
|
+
handler: async (ctx, args): Promise<string> => {
|
|
37
|
+
if (!args.taskId.trim()) throw new Error("taskId is required");
|
|
38
|
+
if (
|
|
39
|
+
new TextEncoder().encode(JSON.stringify({ payload: args.payload, options: args.options }))
|
|
40
|
+
.byteLength > MAX_INLINE_BYTES
|
|
41
|
+
) {
|
|
42
|
+
throw new Error(
|
|
43
|
+
"Payload and options must fit within 256 KiB; pass a storage reference for larger inputs"
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
if (args.idempotencyKey !== undefined) {
|
|
47
|
+
const existing = await ctx.db
|
|
48
|
+
.query("runs")
|
|
49
|
+
.withIndex("by_key", (q) => q.eq("idempotencyKey", args.idempotencyKey))
|
|
50
|
+
.unique();
|
|
51
|
+
if (existing) return existing._id;
|
|
52
|
+
}
|
|
53
|
+
const id = await ctx.db.insert("runs", {
|
|
54
|
+
taskId: args.taskId,
|
|
55
|
+
idempotencyKey: args.idempotencyKey,
|
|
56
|
+
status: "PENDING",
|
|
57
|
+
syncStatus: "pending",
|
|
58
|
+
updatedAt: Date.now(),
|
|
59
|
+
generation: 0,
|
|
60
|
+
checks: 0,
|
|
61
|
+
failures: 0,
|
|
62
|
+
callback: args.callback,
|
|
63
|
+
});
|
|
64
|
+
await ctx.db.insert("requests", {
|
|
65
|
+
run: id,
|
|
66
|
+
payload: args.payload,
|
|
67
|
+
options: args.options,
|
|
68
|
+
config: args.config,
|
|
69
|
+
});
|
|
70
|
+
await ctx.scheduler.runAfter(0, internal.worker.tick, { id, generation: 0 });
|
|
71
|
+
return id;
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
export const get = query({
|
|
76
|
+
args: { id: v.string() },
|
|
77
|
+
returns: v.union(vRun, v.null()),
|
|
78
|
+
handler: async (ctx, { id }): Promise<Run | null> => {
|
|
79
|
+
const runId = ctx.db.normalizeId("runs", id);
|
|
80
|
+
const run = runId ? await ctx.db.get("runs", runId) : null;
|
|
81
|
+
return run ? toRun(run) : null;
|
|
82
|
+
},
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
export const list = query({
|
|
86
|
+
args: { paginationOpts: paginationOptsValidator },
|
|
87
|
+
returns: v.object({ page: v.array(vRun), isDone: v.boolean(), continueCursor: v.string() }),
|
|
88
|
+
handler: async (
|
|
89
|
+
ctx,
|
|
90
|
+
{ paginationOpts }
|
|
91
|
+
): Promise<{ page: Run[]; isDone: boolean; continueCursor: string }> => {
|
|
92
|
+
if (paginationOpts.numItems < 1 || paginationOpts.numItems > 100) {
|
|
93
|
+
throw new Error("Request between 1 and 100 runs per page");
|
|
94
|
+
}
|
|
95
|
+
const result = await ctx.db
|
|
96
|
+
.query("runs")
|
|
97
|
+
.order("desc")
|
|
98
|
+
.paginate({
|
|
99
|
+
...paginationOpts,
|
|
100
|
+
maximumBytesRead: Math.min(
|
|
101
|
+
paginationOpts.maximumBytesRead ?? MAX_PAGE_BYTES,
|
|
102
|
+
MAX_PAGE_BYTES
|
|
103
|
+
),
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
page: result.page.map(toRun),
|
|
107
|
+
isDone: result.isDone,
|
|
108
|
+
continueCursor: result.continueCursor,
|
|
109
|
+
};
|
|
110
|
+
},
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
export const resume = mutation({
|
|
114
|
+
args: { id: v.string(), config: vConfig },
|
|
115
|
+
returns: v.null(),
|
|
116
|
+
handler: async (ctx, { id, config }): Promise<null> => {
|
|
117
|
+
const runId = ctx.db.normalizeId("runs", id);
|
|
118
|
+
const run = runId ? await ctx.db.get("runs", runId) : null;
|
|
119
|
+
if (!run) throw new Error("Run not found");
|
|
120
|
+
if (run.syncStatus !== "paused") return null;
|
|
121
|
+
if (!run.runId && Date.now() - run._creationTime >= SUBMISSION_WINDOW_MS) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
"Submission retry window expired; reconcile the run in Trigger before triggering again"
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
const request = await ctx.db
|
|
127
|
+
.query("requests")
|
|
128
|
+
.withIndex("by_run", (q) => q.eq("run", run._id))
|
|
129
|
+
.unique();
|
|
130
|
+
if (!request) throw new Error("Run request not found");
|
|
131
|
+
await ctx.db.patch("requests", request._id, { config });
|
|
132
|
+
const generation = run.generation + 1;
|
|
133
|
+
await ctx.db.patch("runs", run._id, {
|
|
134
|
+
syncStatus: run.runId ? "active" : "pending",
|
|
135
|
+
syncError: undefined,
|
|
136
|
+
failures: 0,
|
|
137
|
+
checks: 0,
|
|
138
|
+
generation,
|
|
139
|
+
updatedAt: Date.now(),
|
|
140
|
+
});
|
|
141
|
+
await ctx.scheduler.runAfter(0, internal.worker.tick, { id: run._id, generation });
|
|
142
|
+
return null;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
export const load = internalQuery({
|
|
147
|
+
args: { id: v.id("runs") },
|
|
148
|
+
handler: async (ctx, { id }) => {
|
|
149
|
+
const run = await ctx.db.get("runs", id);
|
|
150
|
+
const request = await ctx.db
|
|
151
|
+
.query("requests")
|
|
152
|
+
.withIndex("by_run", (q) => q.eq("run", id))
|
|
153
|
+
.unique();
|
|
154
|
+
return run && request ? { run, request } : null;
|
|
155
|
+
},
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
export const recordSubmission = internalMutation({
|
|
159
|
+
args: { id: v.id("runs"), generation: v.number(), runId: v.string() },
|
|
160
|
+
returns: v.null(),
|
|
161
|
+
handler: async (ctx, args): Promise<null> => {
|
|
162
|
+
const run = await ctx.db.get("runs", args.id);
|
|
163
|
+
if (!run || run.generation !== args.generation || run.syncStatus === "complete") return null;
|
|
164
|
+
await ctx.db.patch("runs", args.id, {
|
|
165
|
+
runId: args.runId,
|
|
166
|
+
status: "QUEUED",
|
|
167
|
+
syncStatus: "active",
|
|
168
|
+
syncError: undefined,
|
|
169
|
+
failures: 0,
|
|
170
|
+
checks: 0,
|
|
171
|
+
updatedAt: Date.now(),
|
|
172
|
+
});
|
|
173
|
+
return null;
|
|
174
|
+
},
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
export const recordStatus = internalMutation({
|
|
178
|
+
args: {
|
|
179
|
+
id: v.id("runs"),
|
|
180
|
+
generation: v.number(),
|
|
181
|
+
status: v.string(),
|
|
182
|
+
completed: v.boolean(),
|
|
183
|
+
output: v.optional(v.any()),
|
|
184
|
+
outputPresignedUrl: v.optional(v.string()),
|
|
185
|
+
outputOmitted: v.optional(v.boolean()),
|
|
186
|
+
error: v.optional(v.any()),
|
|
187
|
+
metadata: v.optional(v.any()),
|
|
188
|
+
},
|
|
189
|
+
returns: v.null(),
|
|
190
|
+
handler: async (ctx, { id, generation, completed, ...result }): Promise<null> => {
|
|
191
|
+
const run = await ctx.db.get("runs", id);
|
|
192
|
+
if (!run || run.generation !== generation || run.syncStatus === "complete") return null;
|
|
193
|
+
const updates = {
|
|
194
|
+
...result,
|
|
195
|
+
syncStatus: completed ? ("complete" as const) : ("active" as const),
|
|
196
|
+
syncError: undefined,
|
|
197
|
+
failures: 0,
|
|
198
|
+
updatedAt: Date.now(),
|
|
199
|
+
};
|
|
200
|
+
await ctx.db.patch("runs", id, updates);
|
|
201
|
+
if (completed) {
|
|
202
|
+
if (run.callback) {
|
|
203
|
+
await ctx.runMutation(run.callback as FunctionHandle<"mutation", { run: Run }>, {
|
|
204
|
+
run: toRun({ ...run, ...updates }),
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
const request = await ctx.db
|
|
208
|
+
.query("requests")
|
|
209
|
+
.withIndex("by_run", (q) => q.eq("run", id))
|
|
210
|
+
.unique();
|
|
211
|
+
if (request) await ctx.db.delete("requests", request._id);
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
export async function pause(
|
|
218
|
+
ctx: GenericMutationCtx<DataModel>,
|
|
219
|
+
run: RunDoc,
|
|
220
|
+
message: string
|
|
221
|
+
): Promise<void> {
|
|
222
|
+
await ctx.db.patch("runs", run._id, {
|
|
223
|
+
syncStatus: "paused",
|
|
224
|
+
syncError: message,
|
|
225
|
+
updatedAt: Date.now(),
|
|
226
|
+
});
|
|
227
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { defineSchema, defineTable } from "convex/server";
|
|
2
|
+
import { v } from "convex/values";
|
|
3
|
+
import { runFields, vConfig, vTriggerOptions } from "./shared.js";
|
|
4
|
+
|
|
5
|
+
export default defineSchema({
|
|
6
|
+
runs: defineTable({
|
|
7
|
+
...runFields,
|
|
8
|
+
generation: v.number(),
|
|
9
|
+
checks: v.number(),
|
|
10
|
+
failures: v.number(),
|
|
11
|
+
callback: v.optional(v.string()),
|
|
12
|
+
}).index("by_key", ["idempotencyKey"]),
|
|
13
|
+
requests: defineTable({
|
|
14
|
+
run: v.id("runs"),
|
|
15
|
+
payload: v.optional(v.any()),
|
|
16
|
+
options: vTriggerOptions,
|
|
17
|
+
config: vConfig,
|
|
18
|
+
}).index("by_run", ["run"]),
|
|
19
|
+
});
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import {
|
|
2
|
+
internalActionGeneric,
|
|
3
|
+
internalMutationGeneric,
|
|
4
|
+
internalQueryGeneric,
|
|
5
|
+
mutationGeneric,
|
|
6
|
+
queryGeneric,
|
|
7
|
+
type ActionBuilder,
|
|
8
|
+
type MutationBuilder,
|
|
9
|
+
type QueryBuilder,
|
|
10
|
+
type DataModelFromSchemaDefinition,
|
|
11
|
+
} from "convex/server";
|
|
12
|
+
import type schema from "./schema.js";
|
|
13
|
+
|
|
14
|
+
export type DataModel = DataModelFromSchemaDefinition<typeof schema>;
|
|
15
|
+
export const mutation: MutationBuilder<DataModel, "public"> = mutationGeneric;
|
|
16
|
+
export const query: QueryBuilder<DataModel, "public"> = queryGeneric;
|
|
17
|
+
export const internalMutation: MutationBuilder<DataModel, "internal"> = internalMutationGeneric;
|
|
18
|
+
export const internalQuery: QueryBuilder<DataModel, "internal"> = internalQueryGeneric;
|
|
19
|
+
export const internalAction: ActionBuilder<DataModel, "internal"> = internalActionGeneric;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { v, type Infer } from "convex/values";
|
|
2
|
+
|
|
3
|
+
export const vTriggerOptions = v.object({
|
|
4
|
+
tags: v.optional(v.array(v.string())),
|
|
5
|
+
delay: v.optional(v.string()),
|
|
6
|
+
queue: v.optional(v.object({ name: v.string() })),
|
|
7
|
+
concurrencyKey: v.optional(v.string()),
|
|
8
|
+
machine: v.optional(v.string()),
|
|
9
|
+
metadata: v.optional(v.any()),
|
|
10
|
+
maxAttempts: v.optional(v.number()),
|
|
11
|
+
maxDuration: v.optional(v.number()),
|
|
12
|
+
ttl: v.optional(v.string()),
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export const vConfig = v.object({
|
|
16
|
+
secretKey: v.string(),
|
|
17
|
+
baseUrl: v.string(),
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
export const runFields = {
|
|
21
|
+
taskId: v.string(),
|
|
22
|
+
runId: v.optional(v.string()),
|
|
23
|
+
idempotencyKey: v.optional(v.string()),
|
|
24
|
+
status: v.string(),
|
|
25
|
+
syncStatus: v.union(
|
|
26
|
+
v.literal("pending"),
|
|
27
|
+
v.literal("active"),
|
|
28
|
+
v.literal("paused"),
|
|
29
|
+
v.literal("complete")
|
|
30
|
+
),
|
|
31
|
+
output: v.optional(v.any()),
|
|
32
|
+
outputPresignedUrl: v.optional(v.string()),
|
|
33
|
+
outputOmitted: v.optional(v.boolean()),
|
|
34
|
+
error: v.optional(v.any()),
|
|
35
|
+
metadata: v.optional(v.any()),
|
|
36
|
+
syncError: v.optional(v.string()),
|
|
37
|
+
updatedAt: v.number(),
|
|
38
|
+
};
|
|
39
|
+
export const vRun = v.object({
|
|
40
|
+
id: v.string(),
|
|
41
|
+
createdAt: v.number(),
|
|
42
|
+
...runFields,
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
export type Run = Infer<typeof vRun>;
|
|
46
|
+
export type TriggerOptions = Infer<typeof vTriggerOptions>;
|
|
47
|
+
export type Config = Infer<typeof vConfig>;
|
|
48
|
+
|
|
49
|
+
const FIRST_POLL_MS = 15_000;
|
|
50
|
+
const MAX_POLL_MS = 60_000;
|
|
51
|
+
export const REQUEST_TIMEOUT_MS = 10_000;
|
|
52
|
+
export const WATCHDOG_MARGIN_MS = 5_000;
|
|
53
|
+
export const MAX_SYNC_FAILURES = 8;
|
|
54
|
+
export const SUBMISSION_WINDOW_MS = 60 * 60 * 1_000;
|
|
55
|
+
export const IDEMPOTENCY_TTL = "24h";
|
|
56
|
+
export const MAX_INLINE_BYTES = 256 * 1_024;
|
|
57
|
+
export const MAX_PAGE_BYTES = 4 * 1_024 * 1_024;
|
|
58
|
+
|
|
59
|
+
export function pollDelay(checks: number): number {
|
|
60
|
+
return Math.min(FIRST_POLL_MS * 2 ** Math.min(checks, 4), MAX_POLL_MS);
|
|
61
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { v } from "convex/values";
|
|
2
|
+
import { internal } from "./api.js";
|
|
3
|
+
import { internalAction, internalMutation } from "./server.js";
|
|
4
|
+
import { pause } from "./runs.js";
|
|
5
|
+
import { ApiError, inlineValue, request } from "./apiClient.js";
|
|
6
|
+
import {
|
|
7
|
+
IDEMPOTENCY_TTL,
|
|
8
|
+
MAX_SYNC_FAILURES,
|
|
9
|
+
REQUEST_TIMEOUT_MS,
|
|
10
|
+
SUBMISSION_WINDOW_MS,
|
|
11
|
+
WATCHDOG_MARGIN_MS,
|
|
12
|
+
pollDelay,
|
|
13
|
+
} from "./shared.js";
|
|
14
|
+
|
|
15
|
+
const jobArgs = { id: v.id("runs"), generation: v.number() };
|
|
16
|
+
|
|
17
|
+
export const tick = internalMutation({
|
|
18
|
+
args: jobArgs,
|
|
19
|
+
returns: v.null(),
|
|
20
|
+
handler: async (ctx, { id, generation }): Promise<null> => {
|
|
21
|
+
const run = await ctx.db.get("runs", id);
|
|
22
|
+
if (
|
|
23
|
+
!run ||
|
|
24
|
+
run.generation !== generation ||
|
|
25
|
+
run.syncStatus === "complete" ||
|
|
26
|
+
run.syncStatus === "paused"
|
|
27
|
+
)
|
|
28
|
+
return null;
|
|
29
|
+
if (!run.runId && Date.now() - run._creationTime >= SUBMISSION_WINDOW_MS) {
|
|
30
|
+
await pause(
|
|
31
|
+
ctx,
|
|
32
|
+
run,
|
|
33
|
+
"Submission retry window expired; check Trigger before triggering again"
|
|
34
|
+
);
|
|
35
|
+
return null;
|
|
36
|
+
}
|
|
37
|
+
const nextGeneration = generation + 1;
|
|
38
|
+
await ctx.db.patch("runs", id, { generation: nextGeneration, checks: run.checks + 1 });
|
|
39
|
+
// Schedule the watchdog transactionally before starting an action that can be interrupted.
|
|
40
|
+
await ctx.scheduler.runAfter(
|
|
41
|
+
Math.max(pollDelay(run.checks), REQUEST_TIMEOUT_MS + WATCHDOG_MARGIN_MS),
|
|
42
|
+
internal.worker.tick,
|
|
43
|
+
{ id, generation: nextGeneration }
|
|
44
|
+
);
|
|
45
|
+
await ctx.scheduler.runAfter(0, internal.worker.step, { id, generation: nextGeneration });
|
|
46
|
+
return null;
|
|
47
|
+
},
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export const step = internalAction({
|
|
51
|
+
args: jobArgs,
|
|
52
|
+
returns: v.null(),
|
|
53
|
+
handler: async (ctx, { id, generation }): Promise<null> => {
|
|
54
|
+
const loaded = await ctx.runQuery(internal.runs.load, { id });
|
|
55
|
+
if (
|
|
56
|
+
!loaded ||
|
|
57
|
+
loaded.run.generation !== generation ||
|
|
58
|
+
loaded.run.syncStatus === "complete" ||
|
|
59
|
+
loaded.run.syncStatus === "paused"
|
|
60
|
+
)
|
|
61
|
+
return null;
|
|
62
|
+
const { run, request: saved } = loaded;
|
|
63
|
+
try {
|
|
64
|
+
if (!run.runId) {
|
|
65
|
+
const result = await request(
|
|
66
|
+
saved.config,
|
|
67
|
+
`/api/v1/tasks/${encodeURIComponent(run.taskId)}/trigger`,
|
|
68
|
+
{
|
|
69
|
+
payload: saved.payload,
|
|
70
|
+
options: {
|
|
71
|
+
...saved.options,
|
|
72
|
+
idempotencyKey: `convex:${id}`,
|
|
73
|
+
idempotencyKeyTTL: IDEMPOTENCY_TTL,
|
|
74
|
+
},
|
|
75
|
+
}
|
|
76
|
+
);
|
|
77
|
+
if (typeof result.id !== "string")
|
|
78
|
+
throw new ApiError("Missing run ID in Trigger.dev response", true);
|
|
79
|
+
await ctx.runMutation(internal.runs.recordSubmission, { id, generation, runId: result.id });
|
|
80
|
+
} else {
|
|
81
|
+
const result = await request(saved.config, `/api/v3/runs/${encodeURIComponent(run.runId)}`);
|
|
82
|
+
if (typeof result.status !== "string" || typeof result.isCompleted !== "boolean") {
|
|
83
|
+
throw new ApiError("Missing run status in Trigger.dev response", true);
|
|
84
|
+
}
|
|
85
|
+
const output = inlineValue(result.output);
|
|
86
|
+
await ctx.runMutation(internal.runs.recordStatus, {
|
|
87
|
+
id,
|
|
88
|
+
generation,
|
|
89
|
+
status: result.status,
|
|
90
|
+
completed:
|
|
91
|
+
result.isCompleted || result.status === "EXPIRED" || result.status === "TIMED_OUT",
|
|
92
|
+
output: output.value,
|
|
93
|
+
outputOmitted: output.omitted,
|
|
94
|
+
outputPresignedUrl:
|
|
95
|
+
typeof result.outputPresignedUrl === "string" ? result.outputPresignedUrl : undefined,
|
|
96
|
+
error: inlineValue(result.error).value,
|
|
97
|
+
metadata: inlineValue(result.metadata).value,
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
} catch (error) {
|
|
101
|
+
await ctx.runMutation(internal.worker.recordError, {
|
|
102
|
+
id,
|
|
103
|
+
generation,
|
|
104
|
+
message:
|
|
105
|
+
error instanceof ApiError
|
|
106
|
+
? error.message
|
|
107
|
+
: "Run synchronization failed; inspect Convex function logs",
|
|
108
|
+
permanent: error instanceof ApiError && !error.retryable,
|
|
109
|
+
});
|
|
110
|
+
// Preserve diagnostic detail in server logs without including the request config.
|
|
111
|
+
console.error("Trigger.dev synchronization failed", {
|
|
112
|
+
id,
|
|
113
|
+
error: error instanceof Error ? error.name : "UnknownError",
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
return null;
|
|
117
|
+
},
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
export const recordError = internalMutation({
|
|
121
|
+
args: { ...jobArgs, message: v.string(), permanent: v.boolean() },
|
|
122
|
+
returns: v.null(),
|
|
123
|
+
handler: async (ctx, { id, generation, message, permanent }): Promise<null> => {
|
|
124
|
+
const run = await ctx.db.get("runs", id);
|
|
125
|
+
if (!run || run.generation !== generation || run.syncStatus === "complete") return null;
|
|
126
|
+
const failures = run.failures + 1;
|
|
127
|
+
await ctx.db.patch("runs", id, { failures, syncError: message, updatedAt: Date.now() });
|
|
128
|
+
if (permanent || failures >= MAX_SYNC_FAILURES) await pause(ctx, run, message);
|
|
129
|
+
return null;
|
|
130
|
+
},
|
|
131
|
+
});
|