Skip to content

Commit ad14983

Browse files
authored
React status hooks (#493)
* Added stripInternal to SDK tsconfig * Statuses can now be set from a run, and are stored in the database * Added the key to the returned status * Made the test job have an extra step and only pass in some of the options * client.getRunStatuses() and the corresponding endpoint * client.getRun() now includes status info * Fixed circular dependency schema * Translate null to undefined * Added the react package to the nextjs-reference tsconfig * Removed unused OpenAI integration from nextjs-reference project * New hooks for getting the statuses * Disabled most of the nextjs-reference jobs * Updated the hooks UI * Updated the endpoints to deal with null statuses values * The hook is working, with an example * Changeset: “You can create statuses in your Jobs that can then be read using React hooks” * Changeset config is back to the old changelog style * WIP on new React hooks guide * Guide docs for the new hooks * Added the status hooks to the React hooks guide * Removed the links to the status hooks reference for now * Re-ordered the hooks * Fix for an error in the docs * Set a default of a blank array for the GetRunSchema
1 parent 4edc711 commit ad14983

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1221
-2072
lines changed

.changeset/config.json

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,6 @@
11
{
22
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
3-
"changelog": [
4-
"@remix-run/changelog-github",
5-
{
6-
"repo": "triggerdotdev/trigger.dev"
7-
}
8-
],
3+
"changelog": "@changesets/cli/changelog",
94
"commit": false,
105
"fixed": [
116
[

.changeset/kind-penguins-try.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
"@trigger.dev/sdk": patch
3+
"@trigger.dev/react": patch
4+
"@trigger.dev/core": patch
5+
---
6+
7+
You can create statuses in your Jobs that can then be read using React hooks
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import type { ActionArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import { TaskStatus } from "@trigger.dev/database";
4+
import {
5+
RunTaskBodyOutput,
6+
RunTaskBodyOutputSchema,
7+
ServerTask,
8+
StatusHistory,
9+
StatusHistorySchema,
10+
StatusUpdate,
11+
StatusUpdateData,
12+
StatusUpdateSchema,
13+
StatusUpdateState,
14+
} from "@trigger.dev/core";
15+
import { z } from "zod";
16+
import { $transaction, PrismaClient, prisma } from "~/db.server";
17+
import { taskWithAttemptsToServerTask } from "~/models/task.server";
18+
import { authenticateApiRequest } from "~/services/apiAuth.server";
19+
import { logger } from "~/services/logger.server";
20+
import { ulid } from "~/services/ulid.server";
21+
import { workerQueue } from "~/services/worker.server";
22+
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
23+
24+
const ParamsSchema = z.object({
25+
runId: z.string(),
26+
id: z.string(),
27+
});
28+
29+
export async function action({ request, params }: ActionArgs) {
30+
// Ensure this is a POST request
31+
if (request.method.toUpperCase() !== "PUT") {
32+
return { status: 405, body: "Method Not Allowed" };
33+
}
34+
35+
// Next authenticate the request
36+
const authenticationResult = await authenticateApiRequest(request);
37+
38+
if (!authenticationResult) {
39+
return json({ error: "Invalid or Missing API key" }, { status: 401 });
40+
}
41+
42+
const { runId, id } = ParamsSchema.parse(params);
43+
44+
// Now parse the request body
45+
const anyBody = await request.json();
46+
47+
logger.debug("SetStatusService.call() request body", {
48+
body: anyBody,
49+
runId,
50+
id,
51+
});
52+
53+
const body = StatusUpdateSchema.safeParse(anyBody);
54+
55+
if (!body.success) {
56+
return json({ error: "Invalid request body" }, { status: 400 });
57+
}
58+
59+
const service = new SetStatusService();
60+
61+
try {
62+
const statusRecord = await service.call(runId, id, body.data);
63+
64+
logger.debug("SetStatusService.call() response body", {
65+
runId,
66+
id,
67+
statusRecord,
68+
});
69+
70+
if (!statusRecord) {
71+
return json({ error: "Something went wrong" }, { status: 500 });
72+
}
73+
74+
const status = JobRunStatusRecordSchema.parse({
75+
...statusRecord,
76+
state: statusRecord.state ?? undefined,
77+
history: statusRecord.history ?? undefined,
78+
data: statusRecord.data ?? undefined,
79+
});
80+
81+
return json(status);
82+
} catch (error) {
83+
if (error instanceof Error) {
84+
return json({ error: error.message }, { status: 400 });
85+
}
86+
87+
return json({ error: "Something went wrong" }, { status: 500 });
88+
}
89+
}
90+
91+
export class SetStatusService {
92+
#prismaClient: PrismaClient;
93+
94+
constructor(prismaClient: PrismaClient = prisma) {
95+
this.#prismaClient = prismaClient;
96+
}
97+
98+
public async call(runId: string, id: string, status: StatusUpdate) {
99+
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
100+
const existingStatus = await tx.jobRunStatusRecord.findUnique({
101+
where: {
102+
runId_key: {
103+
runId,
104+
key: id,
105+
},
106+
},
107+
});
108+
109+
const history: StatusHistory = [];
110+
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
111+
if (historyResult.success) {
112+
history.push(...historyResult.data);
113+
}
114+
if (existingStatus) {
115+
history.push({
116+
label: existingStatus.label,
117+
state: (existingStatus.state ?? undefined) as StatusUpdateState,
118+
data: (existingStatus.data ?? undefined) as StatusUpdateData,
119+
});
120+
}
121+
122+
const updatedStatus = await tx.jobRunStatusRecord.upsert({
123+
where: {
124+
runId_key: {
125+
runId,
126+
key: id,
127+
},
128+
},
129+
create: {
130+
key: id,
131+
runId,
132+
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
133+
label: status.label ?? id,
134+
state: status.state,
135+
data: status.data as any,
136+
history: [],
137+
},
138+
update: {
139+
label: status.label,
140+
state: status.state,
141+
data: status.data as any,
142+
history: history as any[],
143+
},
144+
});
145+
146+
return updatedStatus;
147+
});
148+
149+
return statusRecord;
150+
}
151+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
import type { LoaderArgs } from "@remix-run/server-runtime";
2+
import { json } from "@remix-run/server-runtime";
3+
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
4+
import { z } from "zod";
5+
import { prisma } from "~/db.server";
6+
import { authenticateApiRequest } from "~/services/apiAuth.server";
7+
import { logger } from "~/services/logger.server";
8+
import { apiCors } from "~/utils/apiCors";
9+
10+
const ParamsSchema = z.object({
11+
runId: z.string(),
12+
});
13+
14+
const RecordsSchema = z.array(JobRunStatusRecordSchema);
15+
16+
export async function loader({ request, params }: LoaderArgs) {
17+
if (request.method.toUpperCase() === "OPTIONS") {
18+
return apiCors(request, json({}));
19+
}
20+
21+
// Next authenticate the request
22+
const authenticationResult = await authenticateApiRequest(request, { allowPublicKey: true });
23+
24+
if (!authenticationResult) {
25+
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
26+
}
27+
28+
const { runId } = ParamsSchema.parse(params);
29+
30+
logger.debug("Get run statuses", {
31+
runId,
32+
});
33+
34+
try {
35+
const run = await prisma.jobRun.findUnique({
36+
where: {
37+
id: runId,
38+
},
39+
select: {
40+
id: true,
41+
status: true,
42+
output: true,
43+
statuses: {
44+
orderBy: {
45+
createdAt: "asc",
46+
},
47+
},
48+
},
49+
});
50+
51+
if (!run) {
52+
return apiCors(request, json({ error: `No run found for id ${runId}` }, { status: 404 }));
53+
}
54+
55+
const parsedStatuses = RecordsSchema.parse(
56+
run.statuses.map((s) => ({
57+
...s,
58+
state: s.state ?? undefined,
59+
data: s.data ?? undefined,
60+
history: s.history ?? undefined,
61+
}))
62+
);
63+
64+
return apiCors(
65+
request,
66+
json({
67+
run: {
68+
id: run.id,
69+
status: run.status,
70+
output: run.output,
71+
},
72+
statuses: parsedStatuses,
73+
})
74+
);
75+
} catch (error) {
76+
if (error instanceof Error) {
77+
return apiCors(request, json({ error: error.message }, { status: 400 }));
78+
}
79+
80+
return apiCors(request, json({ error: "Something went wrong" }, { status: 500 }));
81+
}
82+
}

apps/webapp/app/routes/api.v1.runs.$runId.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
import type { LoaderArgs } from "@remix-run/server-runtime";
22
import { json } from "@remix-run/server-runtime";
3-
import { cors } from "remix-utils";
43
import { z } from "zod";
54
import { prisma } from "~/db.server";
6-
import { authenticateApiRequest, getApiKeyFromRequest } from "~/services/apiAuth.server";
5+
import { authenticateApiRequest } from "~/services/apiAuth.server";
76
import { apiCors } from "~/utils/apiCors";
87
import { taskListToTree } from "~/utils/taskListToTree";
98

@@ -93,6 +92,9 @@ export async function loader({ request, params }: LoaderArgs) {
9392
}
9493
: undefined,
9594
},
95+
statuses: {
96+
select: { key: true, label: true, state: true, data: true, history: true },
97+
},
9698
},
9799
});
98100

@@ -122,6 +124,12 @@ export async function loader({ request, params }: LoaderArgs) {
122124
const { parentId, ...rest } = task;
123125
return { ...rest };
124126
}),
127+
statuses: jobRun.statuses.map((s) => ({
128+
...s,
129+
state: s.state ?? undefined,
130+
data: s.data ?? undefined,
131+
history: s.history ?? undefined,
132+
})),
125133
nextCursor: nextTask ? nextTask.id : undefined,
126134
})
127135
);

docs/_snippets/how-to-get-run-id.mdx

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
<Accordion title="How do I get a Run id?">
2+
You can call [client.getRuns()](/sdk/triggerclient/instancemethods/getruns) with a Job id to get a
3+
list of the most recent Runs for that Job. You can then pass that run id to your frontend to use
4+
in the hook.
5+
</Accordion>

docs/_snippets/react-hook-types.mdx

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
## The two types of Run progress you can use
2+
3+
1. Automatic updates of Run and Task progress (no extra Job code required)
4+
2. Explicitly created and updated `statuses` (more flexible and powerful)
5+
6+
### Automatic updates
7+
8+
These require no changes inside your Job code. You can receive:
9+
10+
- Info about an event you sent, including the Runs it triggered.
11+
- The overall status of the Run (in progress, success and fail statuses).
12+
- Metadata like start and completed times.
13+
- The Run output (what is returned or an error that failed the Job)
14+
- Information about the Tasks that have completed/failed/are running.
15+
16+
### Explicit `statuses`
17+
18+
You can create `statuses` in your Job code. This gives you fine grained control over what you want to expose.
19+
20+
It allows you to:
21+
22+
- Show exactly what you want in your UI (with as many statuses as you want).
23+
- Pass arbitrary data to your UI, which you can use to render elements.
24+
- Update existing elements in your UI as the progress of the run continues.

0 commit comments

Comments
 (0)