Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
54116a1
Added stripInternal to SDK tsconfig
matt-aitken Sep 19, 2023
22fd21e
Statuses can now be set from a run, and are stored in the database
matt-aitken Sep 19, 2023
e456d4a
Added the key to the returned status
matt-aitken Sep 19, 2023
790a689
Made the test job have an extra step and only pass in some of the opt…
matt-aitken Sep 19, 2023
b1e44e3
client.getRunStatuses() and the corresponding endpoint
matt-aitken Sep 19, 2023
08932b7
client.getRun() now includes status info
matt-aitken Sep 19, 2023
4f2b91d
Fixed circular dependency schema
matt-aitken Sep 19, 2023
ed144ba
Translate null to undefined
matt-aitken Sep 19, 2023
5ecb369
Added the react package to the nextjs-reference tsconfig
matt-aitken Sep 19, 2023
6cabf8c
Removed unused OpenAI integration from nextjs-reference project
matt-aitken Sep 19, 2023
25267d0
New hooks for getting the statuses
matt-aitken Sep 19, 2023
6e9f3c5
Disabled most of the nextjs-reference jobs
matt-aitken Sep 19, 2023
1ef4f58
Updated the hooks UI
matt-aitken Sep 19, 2023
f95a236
Updated the endpoints to deal with null statuses values
matt-aitken Sep 19, 2023
f0257cd
The hook is working, with an example
matt-aitken Sep 19, 2023
a880188
Changeset: “You can create statuses in your Jobs that can then be rea…
matt-aitken Sep 20, 2023
79670f1
Changeset config is back to the old changelog style
matt-aitken Sep 20, 2023
0a6462e
WIP on new React hooks guide
matt-aitken Sep 20, 2023
e7f53aa
Guide docs for the new hooks
matt-aitken Sep 20, 2023
543a38a
Added the status hooks to the React hooks guide
matt-aitken Sep 21, 2023
4a18f20
Removed the links to the status hooks reference for now
matt-aitken Sep 21, 2023
149a741
Re-ordered the hooks
matt-aitken Sep 21, 2023
7d5cd20
Fix for an error in the docs
matt-aitken Sep 21, 2023
460f1c4
Set a default of a blank array for the GetRunSchema
matt-aitken Sep 21, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 1 addition & 6 deletions .changeset/config.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,6 @@
{
"$schema": "https://unpkg.com/@changesets/[email protected]/schema.json",
"changelog": [
"@remix-run/changelog-github",
{
"repo": "triggerdotdev/trigger.dev"
}
],
"changelog": "@changesets/cli/changelog",
"commit": false,
"fixed": [
[
Expand Down
7 changes: 7 additions & 0 deletions .changeset/kind-penguins-try.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"@trigger.dev/sdk": patch
"@trigger.dev/react": patch
"@trigger.dev/core": patch
---

You can create statuses in your Jobs that can then be read using React hooks
151 changes: 151 additions & 0 deletions apps/webapp/app/routes/api.v1.runs.$runId.statuses.$id.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { TaskStatus } from "@trigger.dev/database";
import {
RunTaskBodyOutput,
RunTaskBodyOutputSchema,
ServerTask,
StatusHistory,
StatusHistorySchema,
StatusUpdate,
StatusUpdateData,
StatusUpdateSchema,
StatusUpdateState,
} from "@trigger.dev/core";
import { z } from "zod";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";

const ParamsSchema = z.object({
runId: z.string(),
id: z.string(),
});

export async function action({ request, params }: ActionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "PUT") {
return { status: 405, body: "Method Not Allowed" };
}

// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request);

if (!authenticationResult) {
return json({ error: "Invalid or Missing API key" }, { status: 401 });
}

const { runId, id } = ParamsSchema.parse(params);

// Now parse the request body
const anyBody = await request.json();

logger.debug("SetStatusService.call() request body", {
body: anyBody,
runId,
id,
});

const body = StatusUpdateSchema.safeParse(anyBody);

if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}

const service = new SetStatusService();

try {
const statusRecord = await service.call(runId, id, body.data);

logger.debug("SetStatusService.call() response body", {
runId,
id,
statusRecord,
});

if (!statusRecord) {
return json({ error: "Something went wrong" }, { status: 500 });
}

const status = JobRunStatusRecordSchema.parse({
...statusRecord,
state: statusRecord.state ?? undefined,
history: statusRecord.history ?? undefined,
data: statusRecord.data ?? undefined,
});

return json(status);
} catch (error) {
if (error instanceof Error) {
return json({ error: error.message }, { status: 400 });
}

return json({ error: "Something went wrong" }, { status: 500 });
}
}

export class SetStatusService {
#prismaClient: PrismaClient;

constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}

public async call(runId: string, id: string, status: StatusUpdate) {
const statusRecord = await $transaction(this.#prismaClient, async (tx) => {
const existingStatus = await tx.jobRunStatusRecord.findUnique({
where: {
runId_key: {
runId,
key: id,
},
},
});

const history: StatusHistory = [];
const historyResult = StatusHistorySchema.safeParse(existingStatus?.history);
if (historyResult.success) {
history.push(...historyResult.data);
}
if (existingStatus) {
history.push({
label: existingStatus.label,
state: (existingStatus.state ?? undefined) as StatusUpdateState,
data: (existingStatus.data ?? undefined) as StatusUpdateData,
});
}

const updatedStatus = await tx.jobRunStatusRecord.upsert({
where: {
runId_key: {
runId,
key: id,
},
},
create: {
key: id,
runId,
//this shouldn't ever use the id in reality, as the SDK makess it compulsory on the first call
label: status.label ?? id,
state: status.state,
data: status.data as any,
history: [],
},
update: {
label: status.label,
state: status.state,
data: status.data as any,
history: history as any[],
},
});

return updatedStatus;
});

return statusRecord;
}
}
82 changes: 82 additions & 0 deletions apps/webapp/app/routes/api.v1.runs.$runId.statuses.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { JobRunStatusRecordSchema } from "@trigger.dev/core";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { logger } from "~/services/logger.server";
import { apiCors } from "~/utils/apiCors";

const ParamsSchema = z.object({
runId: z.string(),
});

const RecordsSchema = z.array(JobRunStatusRecordSchema);

export async function loader({ request, params }: LoaderArgs) {
if (request.method.toUpperCase() === "OPTIONS") {
return apiCors(request, json({}));
}

// Next authenticate the request
const authenticationResult = await authenticateApiRequest(request, { allowPublicKey: true });

if (!authenticationResult) {
return apiCors(request, json({ error: "Invalid or Missing API key" }, { status: 401 }));
}

const { runId } = ParamsSchema.parse(params);

logger.debug("Get run statuses", {
runId,
});

try {
const run = await prisma.jobRun.findUnique({
where: {
id: runId,
},
select: {
id: true,
status: true,
output: true,
statuses: {
orderBy: {
createdAt: "asc",
},
},
},
});

if (!run) {
return apiCors(request, json({ error: `No run found for id ${runId}` }, { status: 404 }));
}

const parsedStatuses = RecordsSchema.parse(
run.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
}))
);

return apiCors(
request,
json({
run: {
id: run.id,
status: run.status,
output: run.output,
},
statuses: parsedStatuses,
})
);
} catch (error) {
if (error instanceof Error) {
return apiCors(request, json({ error: error.message }, { status: 400 }));
}

return apiCors(request, json({ error: "Something went wrong" }, { status: 500 }));
}
}
12 changes: 10 additions & 2 deletions apps/webapp/app/routes/api.v1.runs.$runId.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,8 @@
import type { LoaderArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { cors } from "remix-utils";
import { z } from "zod";
import { prisma } from "~/db.server";
import { authenticateApiRequest, getApiKeyFromRequest } from "~/services/apiAuth.server";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { apiCors } from "~/utils/apiCors";
import { taskListToTree } from "~/utils/taskListToTree";

Expand Down Expand Up @@ -93,6 +92,9 @@ export async function loader({ request, params }: LoaderArgs) {
}
: undefined,
},
statuses: {
select: { key: true, label: true, state: true, data: true, history: true },
},
},
});

Expand Down Expand Up @@ -122,6 +124,12 @@ export async function loader({ request, params }: LoaderArgs) {
const { parentId, ...rest } = task;
return { ...rest };
}),
statuses: jobRun.statuses.map((s) => ({
...s,
state: s.state ?? undefined,
data: s.data ?? undefined,
history: s.history ?? undefined,
})),
nextCursor: nextTask ? nextTask.id : undefined,
})
);
Expand Down
5 changes: 5 additions & 0 deletions docs/_snippets/how-to-get-run-id.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<Accordion title="How do I get a Run id?">
You can call [client.getRuns()](/sdk/triggerclient/instancemethods/getruns) with a Job id to get a
list of the most recent Runs for that Job. You can then pass that run id to your frontend to use
in the hook.
</Accordion>
24 changes: 24 additions & 0 deletions docs/_snippets/react-hook-types.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
## The two types of Run progress you can use

1. Automatic updates of Run and Task progress (no extra Job code required)
2. Explicitly created and updated `statuses` (more flexible and powerful)

### Automatic updates

These require no changes inside your Job code. You can receive:

- Info about an event you sent, including the Runs it triggered.
- The overall status of the Run (in progress, success and fail statuses).
- Metadata like start and completed times.
- The Run output (what is returned or an error that failed the Job)
- Information about the Tasks that have completed/failed/are running.

### Explicit `statuses`

You can create `statuses` in your Job code. This gives you fine grained control over what you want to expose.

It allows you to:

- Show exactly what you want in your UI (with as many statuses as you want).
- Pass arbitrary data to your UI, which you can use to render elements.
- Update existing elements in your UI as the progress of the run continues.
Loading