Compare commits

...

1 Commits

Author SHA1 Message Date
Eric Allam ef1576c73f WIP 2023-09-15 11:02:59 +01:00
53 changed files with 2541 additions and 294 deletions
+23
View File
@@ -0,0 +1,23 @@
{
"private": true,
"name": "core-functions",
"version": "1.0.0",
"sideEffects": false,
"scripts": {},
"eslintIgnore": [
"/node_modules",
"/build",
"/public/build"
],
"dependencies": {
"@trigger.dev/functions": "workspace:*",
"@trigger.dev/functions-worker": "workspace:*"
},
"devDependencies": {
"rimraf": "^3.0.2",
"typescript": "^5.2.0"
},
"engines": {
"node": ">=18.0.0"
}
}
+1
View File
@@ -0,0 +1 @@
export {};
+32
View File
@@ -0,0 +1,32 @@
{
"include": ["**/*.ts"],
"compilerOptions": {
"lib": ["DOM", "DOM.Iterable", "ES2019"],
"isolatedModules": true,
"esModuleInterop": true,
"jsx": "react-jsx",
"moduleResolution": "node",
"resolveJsonModule": true,
"target": "ES2019",
"strict": true,
"allowJs": true,
"forceConsistentCasingInFileNames": true,
"skipLibCheck": true,
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"baseUrl": ".",
"paths": {
"~/*": ["./app/*"],
"@/*": ["./*"],
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
"@trigger.dev/core": ["../../packages/core/src/index"],
"@trigger.dev/core/*": ["../../packages/core/src/*"],
"@trigger.dev/database": ["../../packages/database/src/index"],
"@trigger.dev/database/*": ["../../packages/database/src/*"],
"emails": ["../../packages/emails/src/index"],
"emails/*": ["../../packages/emails/src/*"]
},
"noEmit": true
}
}
+18
View File
@@ -1,5 +1,6 @@
import type { Task, TaskAttempt } from "@trigger.dev/database";
import { ServerTask } from "@trigger.dev/core";
import { PrismaClientOrTransaction } from "~/db.server";
export type TaskWithAttempts = Task & { attempts: TaskAttempt[] };
@@ -25,3 +26,20 @@ export function taskWithAttemptsToServerTask(task: TaskWithAttempts): ServerTask
operation: task.operation,
};
}
export type KitchenSinkTask = NonNullable<Awaited<ReturnType<typeof findKitchenSinkTask>>>;
export async function findKitchenSinkTask(prisma: PrismaClientOrTransaction, id: string) {
return prisma.task.findUnique({
where: { id },
include: {
attempts: true,
run: {
include: {
environment: true,
queue: true,
},
},
},
});
}
@@ -0,0 +1,66 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { CreateBackgroundFunctionWorkerImageRequestBodySchema } from "@trigger.dev/core";
import { z } from "zod";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { CreateBackgroundFunctionImageService } from "~/services/backgroundFunctions/createBackgroundFunctionImage.server";
const ParamsSchema = z.object({
id: z.string(),
});
export async function action({ request, params }: ActionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
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 parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid request params" }, { status: 400 });
}
// Now parse the request body
const anyBody = await request.json();
const body = CreateBackgroundFunctionWorkerImageRequestBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new CreateBackgroundFunctionImageService();
try {
const image = await service.call(
authenticationResult.environment,
parsedParams.data.id,
body.data
);
if (!image) {
return json(
{
error: `Unable to create background function worker image`,
},
{ status: 500 }
);
}
return json(image);
} catch (error) {
if (error instanceof Error) {
return json({ error: error.message }, { status: 400 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
@@ -0,0 +1,146 @@
import { Response } from "@remix-run/node";
import { LoaderArgs, json } from "@remix-run/server-runtime";
import {
BackgroundFunction,
BackgroundFunctionArtifact,
PrismaClient,
} from "@trigger.dev/database";
import archiver from "archiver";
import { z } from "zod";
import { prisma } from "~/db.server";
const ParamsSchema = z.object({
id: z.string(),
});
export async function loader({ params }: LoaderArgs) {
const parsedParams = ParamsSchema.safeParse(params);
if (!parsedParams.success) {
return json({ error: "Invalid request params" }, { status: 400 });
}
const service = new GenerateBackgroundFunctionArtifactArchiveService();
const results = await service.call(parsedParams.data.id);
if (!results) {
return json({ error: "Something went wrong" }, { status: 500 });
}
return new Response(results.archive, {
headers: {
"Content-Disposition": `attachment; filename="${results.name}"`,
"Content-Type": "application/gzip",
},
});
}
class GenerateBackgroundFunctionArtifactArchiveService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(id: string) {
const artifact = await this.#prismaClient.backgroundFunctionArtifact.findUnique({
where: {
id,
},
include: {
backgroundFunction: true,
},
});
if (!artifact) {
return;
}
const archive = archiver("tar", {
gzip: true,
zlib: { level: 9 }, // Sets the compression level
});
function addFileToContext(contents: string, path: string) {
// Append files to the archive
archive.append(contents, { name: `ctx/${path}` });
}
// Good practice to catch warnings (ie stat failures and other non-blocking errors)
archive.on("warning", function (err) {
if (err.code === "ENOENT") {
// log warning
console.warn(err);
} else {
// throw error
throw err;
}
});
// Good practice to catch this error explicitly
archive.on("error", function (err) {
throw err;
});
addFileToContext(artifact.bundle, `src/${artifact.fileName}`);
addFileToContext(
JSON.stringify(this.#generatePackageJson(artifact, artifact.backgroundFunction)),
"package.json"
);
addFileToContext(this.#generateDockerfile(artifact, artifact.backgroundFunction), "Dockerfile");
addFileToContext(this.#generateIndexJs(artifact, artifact.backgroundFunction), "src/index.js");
// Finalize the archive
archive.finalize();
return {
archive,
name: `${artifact.id}.tar.gz`,
};
}
#generatePackageJson(artifact: BackgroundFunctionArtifact, task: BackgroundFunction) {
return {
name: task.slug,
version: artifact.version,
description: `Trigger background function ${task.slug}`,
main: "src/index.js",
scripts: {
start: "node src/index.js",
},
dependencies: artifact.dependencies,
engines: {
node: this.#getNodeVersion(artifact.nodeVersion),
},
};
}
#generateDockerfile(artifact: BackgroundFunctionArtifact, func: BackgroundFunction) {
return `FROM node:${this.#getNodeVersion(artifact.nodeVersion)}-bullseye-slim
WORKDIR /usr/src/app
COPY ctx/package*.json ./
RUN npm install
COPY ctx/. .
CMD [ "npm", "start" ]
`;
}
#generateIndexJs(artifact: BackgroundFunctionArtifact, func: BackgroundFunction) {
return `
const func = require("./${artifact.fileName}").default;
console.log(JSON.stringify(func.toJSON()));
console.log(process.env);
`;
}
// replace the v if it exists
#getNodeVersion(version: string) {
return version.replace("v", "");
}
}
@@ -0,0 +1,56 @@
import type { ActionArgs } from "@remix-run/server-runtime";
import { json } from "@remix-run/server-runtime";
import { UploadBackgroundFunctionRequestBodySchema } from "@trigger.dev/core";
import { authenticateApiRequest } from "~/services/apiAuth.server";
import { UploadBackgroundFunctionService } from "~/services/backgroundFunctions/uploadBackgroundFunction.server";
export async function action({ request, params }: ActionArgs) {
// Ensure this is a POST request
if (request.method.toUpperCase() !== "POST") {
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 });
}
// Now parse the request body
const anyBody = await request.json();
const body = UploadBackgroundFunctionRequestBodySchema.safeParse(anyBody);
if (!body.success) {
return json({ error: "Invalid request body" }, { status: 400 });
}
const service = new UploadBackgroundFunctionService();
try {
const artifact = await service.call(authenticationResult.environment, body.data);
if (!artifact) {
return json(
{
error: `Unable to upload background function, function with ID = ${body.data.id} not found`,
},
{ status: 500 }
);
}
return json({
id: artifact.id,
hash: artifact.hash,
createdAt: artifact.createdAt,
updatedAt: artifact.updatedAt,
});
} catch (error) {
if (error instanceof Error) {
return json({ error: error.message }, { status: 400 });
}
return json({ error: "Something went wrong" }, { status: 500 });
}
}
@@ -1,14 +1,10 @@
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 } from "@trigger.dev/core";
import { RunTaskBodyOutputSchema } 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 { RunTaskService } from "~/services/tasks/runTask.server";
const ParamsSchema = z.object({
runId: z.string(),
@@ -80,148 +76,3 @@ export async function action({ request, params }: ActionArgs) {
return json({ error: "Something went wrong" }, { status: 500 });
}
}
export class RunTaskService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
idempotencyKey: string,
taskBody: RunTaskBodyOutput
): Promise<ServerTask | undefined> {
const task = await $transaction(this.#prismaClient, async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
runId_idempotencyKey: {
runId,
idempotencyKey,
},
},
include: {
attempts: true,
},
});
if (existingTask) {
if (existingTask.status === "CANCELED") {
const existingTaskStatus =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
const resumedExistingTask = await tx.task.update({
where: {
id: existingTask.id,
},
data: {
status: existingTaskStatus,
startedAt: new Date(),
completedAt: existingTaskStatus === "COMPLETED" ? new Date() : undefined,
},
include: {
run: true,
attempts: true,
},
});
return resumedExistingTask;
}
return existingTask;
}
const run = await tx.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
},
});
if (!run) throw new Error("Run not found");
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
let status: TaskStatus;
if (run.status === "CANCELED") {
status = "CANCELED";
} else {
status =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
}
const task = await tx.task.create({
data: {
id: ulid(),
idempotencyKey,
displayKey: taskBody.displayKey,
runConnection: taskBody.connectionKey
? {
connect: {
runId_key: {
runId,
key: taskBody.connectionKey,
},
},
}
: undefined,
icon: taskBody.icon,
run: {
connect: {
id: runId,
},
},
parent: taskBody.parentId ? { connect: { id: taskBody.parentId } } : undefined,
name: taskBody.name ?? "Task",
description: taskBody.description,
status,
startedAt: new Date(),
completedAt: status === "COMPLETED" || status === "CANCELED" ? new Date() : undefined,
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: taskBody.properties ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
style: taskBody.style ?? { style: "normal" },
attempts: {
create: {
number: 1,
status: "PENDING",
},
},
},
include: {
run: true,
attempts: true,
},
});
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await workerQueue.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined }
);
}
return task;
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
}
}
@@ -0,0 +1,62 @@
import { CreateBackgroundFunctionWorkerImageRequestBody } from "@trigger.dev/core";
import { PrismaClient } from "@trigger.dev/database";
import { prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
export class CreateBackgroundFunctionImageService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
id: string,
payload: CreateBackgroundFunctionWorkerImageRequestBody
) {
// Find the artifact
const artifact = await this.#prismaClient.backgroundFunctionArtifact.findUnique({
where: {
id,
},
include: {
backgroundFunction: true,
},
});
if (!artifact) {
return;
}
if (artifact.backgroundFunction.projectId !== environment.projectId) {
return;
}
const image = await this.#prismaClient.backgroundFunctionImage.upsert({
where: {
backgroundFunctionArtifactId_digest: {
backgroundFunctionArtifactId: artifact.id,
digest: payload.digest,
},
},
create: {
backgroundFunctionArtifactId: artifact.id,
backgroundFunctionId: artifact.backgroundFunctionId,
digest: payload.digest,
name: payload.name,
tag: payload.tag,
size: payload.size,
registry: payload.registry,
},
update: {
name: payload.name,
tag: payload.tag,
size: payload.size,
registry: payload.registry,
},
});
return image;
}
}
@@ -0,0 +1,66 @@
import type { BackgroundFunctionVersion } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
import type { AuthenticatedEnvironment } from "../apiAuth.server";
export type DisableBackgroundFunctionServiceOptions = {
slug: string;
version: string;
};
export class DisableBackgroundFunctionService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
endpointIdOrEndpoint: string | ExtendedEndpoint,
options: DisableBackgroundFunctionServiceOptions
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
return this.#disableBackgroundFunction(endpoint.environment, options);
}
async #disableBackgroundFunction(
environment: AuthenticatedEnvironment,
options: DisableBackgroundFunctionServiceOptions
): Promise<BackgroundFunctionVersion | undefined> {
const backgroundFunction = await this.#prismaClient.backgroundFunction.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: options.slug,
},
},
});
if (!backgroundFunction) {
return;
}
const backgroundFunctionVersion = await this.#prismaClient.backgroundFunctionVersion.findUnique(
{
where: {
backgroundFunctionId_version_environmentId: {
backgroundFunctionId: backgroundFunction.id,
version: options.version,
environmentId: environment.id,
},
},
}
);
if (!backgroundFunctionVersion) {
return;
}
// TODO: Disable background task
}
}
@@ -0,0 +1,104 @@
import { BackgroundFunctionTaskParamsSchema } from "@trigger.dev/core";
import { PrismaClient, RuntimeEnvironmentType, Task } from "@trigger.dev/database";
import { $transaction, prisma } from "~/db.server";
import { enqueueRunExecutionV2 } from "~/models/jobRunExecution.server";
import { KitchenSinkTask } from "~/models/task.server";
import { workerQueue } from "../worker.server";
export class InitializeBackgroundFunctionTaskService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(task: KitchenSinkTask) {
const params = BackgroundFunctionTaskParamsSchema.safeParse(task.params);
// We need to create a new background task operation
if (!params.success) {
await this.#resumeTaskWithError(task, params.error.message);
return;
}
const backgroundFunction = await this.#prismaClient.backgroundFunction.findUnique({
where: {
projectId_slug: {
projectId: task.run.projectId,
slug: params.data.id,
},
},
include: {
versions: {
where: {
version: params.data.version,
},
},
},
});
if (!backgroundFunction) {
await this.#resumeTaskWithError(task, `Background function ${params.data.id} not found`);
return;
}
const version = backgroundFunction.versions[0];
if (!version) {
await this.#resumeTaskWithError(
task,
`Background task ${params.data.id} version ${params.data.version} not found`
);
return;
}
await $transaction(this.#prismaClient, async (tx) => {
const functionTask = await tx.backgroundFunctionTask.create({
data: {
backgroundFunctionId: backgroundFunction.id,
backgroundFunctionVersionId: version.id,
taskId: task.id,
payload: params.data.payload,
},
});
await workerQueue.enqueue(
"executeBackgroundFunctionTask",
{
id: functionTask.id,
},
{ tx }
);
return functionTask;
});
}
async #resumeTaskWithError(task: KitchenSinkTask, message: string) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.task.update({
where: { id: task.id },
data: {
status: "ERRORED",
completedAt: new Date(),
output: { message },
},
});
await tx.taskAttempt.updateMany({
where: {
taskId: task.id,
status: "PENDING",
},
data: {
status: "ERRORED",
error: message,
},
});
await enqueueRunExecutionV2(task.run, prisma, {
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
});
});
}
}
@@ -0,0 +1,155 @@
import { BackgroundFunctionMetadata } from "@trigger.dev/core";
import type { BackgroundFunctionVersion, Endpoint } from "@trigger.dev/database";
import type { PrismaClient } from "~/db.server";
import { prisma } from "~/db.server";
import { ExtendedEndpoint, findEndpoint } from "~/models/endpoint.server";
import type { AuthenticatedEnvironment } from "../apiAuth.server";
export class RegisterBackgroundFunctionService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
endpointIdOrEndpoint: string | ExtendedEndpoint,
metadata: BackgroundFunctionMetadata
) {
const endpoint =
typeof endpointIdOrEndpoint === "string"
? await findEndpoint(endpointIdOrEndpoint)
: endpointIdOrEndpoint;
return this.#upsertBackgroundFunction(endpoint, endpoint.environment, metadata);
}
async #upsertBackgroundFunction(
endpoint: Endpoint,
environment: AuthenticatedEnvironment,
metadata: BackgroundFunctionMetadata
): Promise<BackgroundFunctionVersion | undefined> {
// Check the background task doesn't already exist and is deleted
const existingBackgroundFunction = await this.#prismaClient.backgroundFunction.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: metadata.id,
},
},
});
if (existingBackgroundFunction && existingBackgroundFunction.deletedAt && !metadata.enabled) {
return;
}
const backgroundFunction = await this.#prismaClient.backgroundFunction.upsert({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: metadata.id,
},
},
create: {
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
slug: metadata.id,
title: metadata.name,
},
update: {
title: metadata.name,
deletedAt: metadata.enabled ? null : undefined,
},
});
const backgroundFunctionVersion = await this.#prismaClient.backgroundFunctionVersion.upsert({
where: {
backgroundFunctionId_version_environmentId: {
backgroundFunctionId: backgroundFunction.id,
version: metadata.version,
environmentId: environment.id,
},
},
create: {
backgroundFunction: {
connect: {
id: backgroundFunction.id,
},
},
endpoint: {
connect: {
id: endpoint.id,
},
},
environment: {
connect: {
id: environment.id,
},
},
organization: {
connect: {
id: environment.organizationId,
},
},
project: {
connect: {
id: environment.projectId,
},
},
version: metadata.version,
},
update: {
endpoint: {
connect: {
id: endpoint.id,
},
},
},
});
// Count the number of job instances that have higher version numbers
const laterVersionCount = await this.#prismaClient.backgroundFunctionVersion.count({
where: {
backgroundFunctionId: backgroundFunction.id,
version: {
gt: metadata.version,
},
environmentId: environment.id,
},
});
// If there are no later versions, then we can upsert the latest BackgroundFunctionAlias
if (laterVersionCount === 0) {
await this.#prismaClient.backgroundFunctionAlias.upsert({
where: {
backgroundFunctionId_environmentId_name: {
backgroundFunctionId: backgroundFunction.id,
environmentId: environment.id,
name: "latest",
},
},
create: {
backgroundFunctionId: backgroundFunction.id,
versionId: backgroundFunctionVersion.id,
environmentId: environment.id,
name: "latest",
value: backgroundFunctionVersion.version,
},
update: {
versionId: backgroundFunctionVersion.id,
value: backgroundFunctionVersion.version,
},
});
}
return backgroundFunctionVersion;
}
}
@@ -0,0 +1,80 @@
import { UploadBackgroundFunctionRequestBody } from "@trigger.dev/core";
import { PrismaClient } from "@trigger.dev/database";
import nodeCrypto from "node:crypto";
import { prisma } from "~/db.server";
import { AuthenticatedEnvironment } from "../apiAuth.server";
export class UploadBackgroundFunctionService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
environment: AuthenticatedEnvironment,
payload: UploadBackgroundFunctionRequestBody
) {
const hash = this.#hashPayload(payload);
const backgroundFunction = await this.#prismaClient.backgroundFunction.findUnique({
where: {
projectId_slug: {
projectId: environment.projectId,
slug: payload.id,
},
},
});
if (!backgroundFunction) {
return;
}
const artifact = await this.#prismaClient.backgroundFunctionArtifact.upsert({
where: {
backgroundFunctionId_version_hash: {
backgroundFunctionId: backgroundFunction.id,
version: payload.version,
hash,
},
},
create: {
backgroundFunctionId: backgroundFunction.id,
fileName: payload.fileName,
version: payload.version,
hash,
bundle: payload.bundle,
nodeVersion: payload.nodeVersion,
dependencies: payload.dependencies,
sourcemap: payload.sourcemap,
},
update: {
fileName: payload.fileName,
bundle: payload.bundle,
nodeVersion: payload.nodeVersion,
dependencies: payload.dependencies,
sourcemap: payload.sourcemap,
},
});
return artifact;
}
#hashPayload(payload: UploadBackgroundFunctionRequestBody) {
// Create a hash out of the bundle, the nodeVersion, and a determinstically list of dependencies
// This will allow us to determine if the bundle has changed
const hash = nodeCrypto.createHash("sha256");
hash.update(payload.bundle);
hash.update(payload.nodeVersion);
const dependencies = Object.keys(payload.dependencies).sort();
for (const dependency of dependencies) {
hash.update(dependency);
hash.update(payload.dependencies[dependency]);
}
return hash.digest("hex");
}
}
@@ -9,6 +9,8 @@ import { RegisterDynamicScheduleService } from "../triggers/registerDynamicSched
import { RegisterDynamicTriggerService } from "../triggers/registerDynamicTrigger.server";
import { DisableJobService } from "../jobs/disableJob.server";
import { RegisterSourceServiceV2 } from "../sources/registerSourceV2.server";
import { DisableBackgroundFunctionService } from "../backgroundFunctions/disableBackgroundFunction.server";
import { RegisterBackgroundFunctionService } from "../backgroundFunctions/registerBackgroundFunction.server";
export class IndexEndpointService {
#prismaClient: PrismaClient;
@@ -18,6 +20,8 @@ export class IndexEndpointService {
#registerSourceServiceV2 = new RegisterSourceServiceV2();
#registerDynamicTriggerService = new RegisterDynamicTriggerService();
#registerDynamicScheduleService = new RegisterDynamicScheduleService();
#registerBackgroundFunctionService = new RegisterBackgroundFunctionService();
#disableBackgroundFunctionService = new DisableBackgroundFunctionService();
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
@@ -40,7 +44,13 @@ export class IndexEndpointService {
throw new Error(indexResponse.error);
}
const { jobs, sources, dynamicTriggers, dynamicSchedules } = indexResponse.data;
const {
jobs,
sources,
dynamicTriggers,
dynamicSchedules,
backgroundFunctions = [],
} = indexResponse.data;
logger.debug("Indexing endpoint", {
endpointId: endpoint.id,
@@ -62,6 +72,8 @@ export class IndexEndpointService {
dynamicTriggers: 0,
dynamicSchedules: 0,
disabledJobs: 0,
backgroundFunctions: 0,
disabledBackgroundFunctions: 0,
};
const existingJobs = await this.#prismaClient.job.findMany({
@@ -208,6 +220,100 @@ export class IndexEndpointService {
}
}
const existingBackgroundFunctions = await this.#prismaClient.backgroundFunction.findMany({
where: {
projectId: endpoint.projectId,
deletedAt: null,
},
include: {
aliases: {
where: {
name: "latest",
environmentId: endpoint.environmentId,
},
include: {
version: true,
},
take: 1,
},
},
});
for (const backgroundFunction of backgroundFunctions) {
if (!backgroundFunction.enabled) {
const disabledBackgroundFunction = await this.#disableBackgroundFunctionService
.call(endpoint, { slug: backgroundFunction.id, version: backgroundFunction.version })
.catch((error) => {
logger.error("Failed to disable background task", {
endpointId: endpoint.id,
backgroundFunction,
error,
});
return;
});
if (disabledBackgroundFunction) {
indexStats.disabledJobs++;
}
} else {
try {
const registeredVersion = await this.#registerBackgroundFunctionService.call(
endpoint,
backgroundFunction
);
if (registeredVersion) {
indexStats.backgroundFunctions++;
}
} catch (error) {
logger.error("Failed to register background task", {
endpointId: endpoint.id,
backgroundFunction,
error,
});
}
}
}
const missingBackgroundFunctions = existingBackgroundFunctions.filter((backgroundFunction) => {
return !backgroundFunctions.find((b) => b.id === backgroundFunction.slug);
});
if (missingBackgroundFunctions.length > 0) {
logger.debug("Disabling missing background tasks", {
endpointId: endpoint.id,
missingIds: missingBackgroundFunctions.map((job) => job.slug),
});
for (const backgroundFunction of missingBackgroundFunctions) {
const latestVersion = backgroundFunction.aliases[0]?.version;
if (!latestVersion) {
continue;
}
const disabledBackgroundFunction = await this.#disableBackgroundFunctionService
.call(endpoint, {
slug: backgroundFunction.slug,
version: latestVersion.version,
})
.catch((error) => {
logger.error("Failed to disable background task", {
endpointId: endpoint.id,
backgroundFunction,
error,
});
return;
});
if (disabledBackgroundFunction) {
indexStats.disabledJobs++;
}
}
}
logger.debug("Endpoint indexing complete", {
endpointId: endpoint.id,
indexStats,
@@ -15,8 +15,7 @@ import { formatUnknownError } from "~/utils/formatErrors.server";
import { safeJsonFromResponse } from "~/utils/json";
import { logger } from "../logger.server";
import { workerQueue } from "../worker.server";
type FoundTask = Awaited<ReturnType<typeof findTask>>;
import { KitchenSinkTask, findKitchenSinkTask } from "~/models/task.server";
export class PerformTaskOperationService {
#prismaClient: PrismaClient;
@@ -26,7 +25,7 @@ export class PerformTaskOperationService {
}
public async call(id: string) {
const task = await findTask(this.#prismaClient, id);
const task = await findKitchenSinkTask(this.#prismaClient, id);
if (!task) {
return;
@@ -104,7 +103,7 @@ export class PerformTaskOperationService {
}
#calculateRetryForResponse(
task: NonNullable<FoundTask>,
task: KitchenSinkTask,
retry: FetchRetryOptions | undefined,
response: Response
): Date | undefined {
@@ -194,7 +193,7 @@ export class PerformTaskOperationService {
});
}
async #resumeTaskWithError(task: NonNullable<FoundTask>, output: any) {
async #resumeTaskWithError(task: KitchenSinkTask, output: any) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.task.update({
where: { id: task.id },
@@ -220,7 +219,7 @@ export class PerformTaskOperationService {
});
}
async #resumeTask(task: NonNullable<FoundTask>, output: any) {
async #resumeTask(task: KitchenSinkTask, output: any) {
await $transaction(this.#prismaClient, async (tx) => {
await tx.taskAttempt.updateMany({
where: {
@@ -245,7 +244,7 @@ export class PerformTaskOperationService {
});
}
async #resumeRunExecution(task: NonNullable<FoundTask>, prisma: PrismaClientOrTransaction) {
async #resumeRunExecution(task: KitchenSinkTask, prisma: PrismaClientOrTransaction) {
await enqueueRunExecutionV2(task.run, prisma, {
skipRetrying: task.run.environment.type === RuntimeEnvironmentType.DEVELOPMENT,
});
@@ -278,21 +277,6 @@ function hydrateRedactedString(value: RedactString): string {
return result;
}
async function findTask(prisma: PrismaClient, id: string) {
return prisma.task.findUnique({
where: { id },
include: {
attempts: true,
run: {
include: {
environment: true,
queue: true,
},
},
},
});
}
// Add a random number of ms between 0ms and 5000ms
function addJitterInMs() {
return Math.floor(Math.random() * 5000);
@@ -0,0 +1,151 @@
import { RunTaskBodyOutput, ServerTask } from "@trigger.dev/core";
import { TaskStatus } from "@trigger.dev/database";
import { $transaction, PrismaClient, prisma } from "~/db.server";
import { taskWithAttemptsToServerTask } from "~/models/task.server";
import { ulid } from "~/services/ulid.server";
import { workerQueue } from "~/services/worker.server";
export class RunTaskService {
#prismaClient: PrismaClient;
constructor(prismaClient: PrismaClient = prisma) {
this.#prismaClient = prismaClient;
}
public async call(
runId: string,
idempotencyKey: string,
taskBody: RunTaskBodyOutput
): Promise<ServerTask | undefined> {
const task = await $transaction(this.#prismaClient, async (tx) => {
const existingTask = await tx.task.findUnique({
where: {
runId_idempotencyKey: {
runId,
idempotencyKey,
},
},
include: {
attempts: true,
},
});
if (existingTask) {
if (existingTask.status === "CANCELED") {
const existingTaskStatus =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
const resumedExistingTask = await tx.task.update({
where: {
id: existingTask.id,
},
data: {
status: existingTaskStatus,
startedAt: new Date(),
completedAt: existingTaskStatus === "COMPLETED" ? new Date() : undefined,
},
include: {
run: true,
attempts: true,
},
});
return resumedExistingTask;
}
return existingTask;
}
const run = await tx.jobRun.findUnique({
where: {
id: runId,
},
select: {
status: true,
},
});
if (!run) throw new Error("Run not found");
// If task.delayUntil is set and is in the future, we'll set the task's status to "WAITING", else set it to RUNNING
let status: TaskStatus;
if (run.status === "CANCELED") {
status = "CANCELED";
} else {
status =
(taskBody.delayUntil && taskBody.delayUntil.getTime() > Date.now()) || taskBody.trigger
? "WAITING"
: taskBody.noop
? "COMPLETED"
: "RUNNING";
}
const task = await tx.task.create({
data: {
id: ulid(),
idempotencyKey,
displayKey: taskBody.displayKey,
runConnection: taskBody.connectionKey
? {
connect: {
runId_key: {
runId,
key: taskBody.connectionKey,
},
},
}
: undefined,
icon: taskBody.icon,
run: {
connect: {
id: runId,
},
},
parent: taskBody.parentId ? { connect: { id: taskBody.parentId } } : undefined,
name: taskBody.name ?? "Task",
description: taskBody.description,
status,
startedAt: new Date(),
completedAt: status === "COMPLETED" || status === "CANCELED" ? new Date() : undefined,
noop: taskBody.noop,
delayUntil: taskBody.delayUntil,
params: taskBody.params ?? undefined,
properties: taskBody.properties ?? undefined,
redact: taskBody.redact ?? undefined,
operation: taskBody.operation,
style: taskBody.style ?? { style: "normal" },
attempts: {
create: {
number: 1,
status: "PENDING",
},
},
},
include: {
run: true,
attempts: true,
},
});
if (task.status === "RUNNING" && typeof taskBody.operation === "string") {
// We need to schedule the operation
await workerQueue.enqueue(
"performTaskOperation",
{
id: task.id,
},
{ tx, runAt: task.delayUntil ?? undefined }
);
}
return task;
});
return task ? taskWithAttemptsToServerTask(task) : undefined;
}
}
@@ -68,6 +68,9 @@ const workerCatalog = {
connectionCreated: z.object({
id: z.string(),
}),
executeBackgroundFunctionTask: z.object({
id: z.string(),
}),
};
const executionWorkerCatalog = {
@@ -285,6 +288,11 @@ function getWorkerQueue() {
});
},
},
executeBackgroundFunctionTask: {
priority: 0, // smaller number = higher priority
maxAttempts: 3,
handler: async (payload, job) => {},
},
},
});
}
+3 -1
View File
@@ -61,10 +61,11 @@
"@remix-run/server-runtime": "1.19.2-pre.0",
"@team-plain/typescript-sdk": "^2.2.0",
"@trigger.dev/companyicons": "^1.5.14",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/core": "workspace:*",
"@trigger.dev/database": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@uiw/react-codemirror": "^4.19.5",
"archiver": "^6.0.1",
"class-variance-authority": "^0.5.2",
"clsx": "^1.2.1",
"compression": "^1.7.4",
@@ -134,6 +135,7 @@
"@tailwindcss/typography": "^0.5.9",
"@total-typescript/ts-reset": "^0.4.2",
"@trigger.dev/tailwind-config": "workspace:*",
"@types/archiver": "^5.3.2",
"@types/bcryptjs": "^2.4.2",
"@types/compression": "^1.7.2",
"@types/eslint": "^8.4.6",
@@ -0,0 +1,35 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# dependencies
/node_modules
/.pnp
.pnp.js
# testing
/coverage
# next.js
/.next/
/out/
# production
/build
# misc
.DS_Store
*.pem
# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# local env files
.env*.local
# vercel
.vercel
# typescript
*.tsbuildinfo
next-env.d.ts
@@ -0,0 +1,34 @@
This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app).
## Getting Started
First, run the development server:
```bash
npm run dev
# or
yarn dev
# or
pnpm dev
```
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font.
## Learn More
To learn more about Next.js, take a look at the following resources:
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome!
## Deploy on Vercel
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details.
@@ -0,0 +1,4 @@
/** @type {import('next').NextConfig} */
const nextConfig = {}
module.exports = nextConfig
@@ -0,0 +1,33 @@
{
"name": "@examples/background-functions",
"version": "0.1.0",
"private": true,
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
},
"dependencies": {
"@trigger.dev/nextjs": "workspace:*",
"@trigger.dev/sdk": "workspace:*",
"@trigger.dev/functions": "workspace:*",
"@types/node": "20.5.9",
"@types/react": "18.2.17",
"@types/react-dom": "18.2.7",
"autoprefixer": "10.4.15",
"next": "13.4.19",
"postcss": "8.4.29",
"react": "18.2.0",
"react-dom": "18.2.0",
"tailwindcss": "3.3.3",
"typescript": "5.2.2",
"zod": "3.21.4"
},
"devDependencies": {
"@trigger.dev/cli": "workspace:*"
},
"trigger.dev": {
"endpointId": "background-tasks-z2kp"
}
}
@@ -0,0 +1,6 @@
module.exports = {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
}
@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 283 64"><path fill="black" d="M141 16c-11 0-19 7-19 18s9 18 20 18c7 0 13-3 16-7l-7-5c-2 3-6 4-9 4-5 0-9-3-10-7h28v-3c0-11-8-18-19-18zm-9 15c1-4 4-7 9-7s8 3 9 7h-18zm117-15c-11 0-19 7-19 18s9 18 20 18c6 0 12-3 16-7l-8-5c-2 3-5 4-8 4-5 0-9-3-11-7h28l1-3c0-11-8-18-19-18zm-10 15c2-4 5-7 10-7s8 3 9 7h-19zm-39 3c0 6 4 10 10 10 4 0 7-2 9-5l8 5c-3 5-9 8-17 8-11 0-19-7-19-18s8-18 19-18c8 0 14 3 17 8l-8 5c-2-3-5-5-9-5-6 0-10 4-10 10zm83-29v46h-9V5h9zM37 0l37 64H0L37 0zm92 5-27 48L74 5h10l18 30 17-30h10zm59 12v10l-3-1c-6 0-10 4-10 10v15h-9V17h9v9c0-5 6-9 13-9z"/></svg>

After

Width:  |  Height:  |  Size: 629 B

@@ -0,0 +1,9 @@
import { createAppRoute } from "@trigger.dev/nextjs";
import { client } from "@/trigger";
import "@/jobs";
//this route is used to send and receive data with Trigger.dev
export const { POST, dynamic } = createAppRoute(client);
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,27 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
:root {
--foreground-rgb: 0, 0, 0;
--background-start-rgb: 214, 219, 220;
--background-end-rgb: 255, 255, 255;
}
@media (prefers-color-scheme: dark) {
:root {
--foreground-rgb: 255, 255, 255;
--background-start-rgb: 0, 0, 0;
--background-end-rgb: 0, 0, 0;
}
}
body {
color: rgb(var(--foreground-rgb));
background: linear-gradient(
to bottom,
transparent,
rgb(var(--background-end-rgb))
)
rgb(var(--background-start-rgb));
}
@@ -0,0 +1,22 @@
import './globals.css'
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
const inter = Inter({ subsets: ['latin'] })
export const metadata: Metadata = {
title: 'Create Next App',
description: 'Generated by create next app',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
)
}
@@ -0,0 +1,113 @@
import Image from 'next/image'
export default function Home() {
return (
<main className="flex min-h-screen flex-col items-center justify-between p-24">
<div className="z-10 max-w-5xl w-full items-center justify-between font-mono text-sm lg:flex">
<p className="fixed left-0 top-0 flex w-full justify-center border-b border-gray-300 bg-gradient-to-b from-zinc-200 pb-6 pt-8 backdrop-blur-2xl dark:border-neutral-800 dark:bg-zinc-800/30 dark:from-inherit lg:static lg:w-auto lg:rounded-xl lg:border lg:bg-gray-200 lg:p-4 lg:dark:bg-zinc-800/30">
Get started by editing&nbsp;
<code className="font-mono font-bold">src/app/page.tsx</code>
</p>
<div className="fixed bottom-0 left-0 flex h-48 w-full items-end justify-center bg-gradient-to-t from-white via-white dark:from-black dark:via-black lg:static lg:h-auto lg:w-auto lg:bg-none">
<a
className="pointer-events-none flex place-items-center gap-2 p-8 lg:pointer-events-auto lg:p-0"
href="https://vercel.com?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
By{' '}
<Image
src="/vercel.svg"
alt="Vercel Logo"
className="dark:invert"
width={100}
height={24}
priority
/>
</a>
</div>
</div>
<div className="relative flex place-items-center before:absolute before:h-[300px] before:w-[480px] before:-translate-x-1/2 before:rounded-full before:bg-gradient-radial before:from-white before:to-transparent before:blur-2xl before:content-[''] after:absolute after:-z-20 after:h-[180px] after:w-[240px] after:translate-x-1/3 after:bg-gradient-conic after:from-sky-200 after:via-blue-200 after:blur-2xl after:content-[''] before:dark:bg-gradient-to-br before:dark:from-transparent before:dark:to-blue-700 before:dark:opacity-10 after:dark:from-sky-900 after:dark:via-[#0141ff] after:dark:opacity-40 before:lg:h-[360px] z-[-1]">
<Image
className="relative dark:drop-shadow-[0_0_0.3rem_#ffffff70] dark:invert"
src="/next.svg"
alt="Next.js Logo"
width={180}
height={37}
priority
/>
</div>
<div className="mb-32 grid text-center lg:max-w-5xl lg:w-full lg:mb-0 lg:grid-cols-4 lg:text-left">
<a
href="https://nextjs.org/docs?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Docs{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Find in-depth information about Next.js features and API.
</p>
</a>
<a
href="https://nextjs.org/learn?utm_source=create-next-app&utm_medium=appdir-template-tw&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Learn{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Learn about Next.js in an interactive course with&nbsp;quizzes!
</p>
</a>
<a
href="https://vercel.com/templates?framework=next.js&utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Templates{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Explore the Next.js 13 playground.
</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=appdir-template&utm_campaign=create-next-app"
className="group rounded-lg border border-transparent px-5 py-4 transition-colors hover:border-gray-300 hover:bg-gray-100 hover:dark:border-neutral-700 hover:dark:bg-neutral-800/30"
target="_blank"
rel="noopener noreferrer"
>
<h2 className={`mb-3 text-2xl font-semibold`}>
Deploy{' '}
<span className="inline-block transition-transform group-hover:translate-x-1 motion-reduce:transform-none">
-&gt;
</span>
</h2>
<p className={`m-0 max-w-[30ch] text-sm opacity-50`}>
Instantly deploy your Next.js site to a shareable URL with Vercel.
</p>
</a>
</div>
</main>
)
}
@@ -0,0 +1,22 @@
// tasks.background.ts
import { client } from "@/trigger";
import { z } from "zod";
export default client.defineBackgroundFunction({
id: "function-1",
name: "Function 1",
version: "1.0.2",
schema: z.object({
userName: z.string(),
}),
run: async (payload) => {
// This code will run in the background, as it will be bundled and shipped to a trigger.dev background worker
await new Promise((resolve) => setTimeout(resolve, 100000));
return {
username: payload.userName,
foo: "bar",
message: `Function Response for user ${payload.userName}`,
};
},
});
@@ -0,0 +1,19 @@
import { eventTrigger } from "@trigger.dev/sdk";
import { client } from "@/trigger";
import { fetchFunction } from "@trigger.dev/functions";
client.defineJob({
id: "function-usage-1",
name: "Background Function Usage",
version: "0.0.1",
trigger: eventTrigger({
name: "example.event",
}),
run: async (payload, io, ctx) => {
const output = await fetchFunction.invoke("fetch-1", {
userName: "ericallam",
});
return { output };
},
});
@@ -0,0 +1,4 @@
// import all your job files here
export * from "./examples"
@@ -0,0 +1,7 @@
import { TriggerClient } from "@trigger.dev/sdk";
export const client = new TriggerClient({
id: "background-tasks-z2kp",
apiKey: process.env.TRIGGER_API_KEY,
apiUrl: process.env.TRIGGER_API_URL,
});
@@ -0,0 +1,20 @@
import type { Config } from 'tailwindcss'
const config: Config = {
content: [
'./src/pages/**/*.{js,ts,jsx,tsx,mdx}',
'./src/components/**/*.{js,ts,jsx,tsx,mdx}',
'./src/app/**/*.{js,ts,jsx,tsx,mdx}',
],
theme: {
extend: {
backgroundImage: {
'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))',
'gradient-conic':
'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))',
},
},
},
plugins: [],
}
export default config
@@ -0,0 +1,37 @@
{
"compilerOptions": {
"target": "es5",
"lib": ["dom", "dom.iterable", "esnext"],
"allowJs": true,
"skipLibCheck": true,
"strict": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"moduleResolution": "bundler",
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",
"incremental": true,
"plugins": [
{
"name": "next"
}
],
"paths": {
"@/*": ["./src/*"],
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
"@trigger.dev/nextjs": ["../../packages/nextjs/src/index"],
"@trigger.dev/nextjs/*": ["../../packages/nextjs/src/*"],
"@trigger.dev/core": ["../../packages/core/src/index"],
"@trigger.dev/core/*": ["../../packages/core/src/*"],
"@trigger.dev/integration-kit": ["../../packages/integration-kit/src/index"],
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
"@trigger.dev/functions": ["../../packages/functions/src/index"],
"@trigger.dev/functions/*": ["../../packages/functions/src/*"]
}
},
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
"exclude": ["node_modules"]
}
+65 -1
View File
@@ -281,11 +281,21 @@ export const DynamicTriggerEndpointMetadataSchema = z.object({
export type DynamicTriggerEndpointMetadata = z.infer<typeof DynamicTriggerEndpointMetadataSchema>;
export const BackgroundFunctionMetadataSchema = z.object({
id: z.string(),
name: z.string(),
version: z.string(),
enabled: z.boolean(),
});
export type BackgroundFunctionMetadata = z.infer<typeof BackgroundFunctionMetadataSchema>;
export const IndexEndpointResponseSchema = z.object({
jobs: z.array(JobMetadataSchema),
sources: z.array(SourceMetadataSchema),
dynamicTriggers: z.array(DynamicTriggerEndpointMetadataSchema),
dynamicSchedules: z.array(RegisterDynamicSchedulePayloadSchema),
backgroundFunctions: z.array(BackgroundFunctionMetadataSchema).optional(),
});
export type IndexEndpointResponse = z.infer<typeof IndexEndpointResponseSchema>;
@@ -593,7 +603,7 @@ export const RunTaskOptionsSchema = z.object({
/** Allows you to link the Integration connection in the logs. This is handled automatically in integrations. */
connectionKey: z.string().optional(),
/** An operation you want to perform on the Trigger.dev platform, current only "fetch" is supported. If you wish to `fetch` use [`io.backgroundFetch()`](https://trigger.dev/docs/sdk/io/backgroundfetch) instead. */
operation: z.enum(["fetch"]).optional(),
operation: z.enum(["fetch", "invokeBackgroundFunction"]).optional(),
/** A No Operation means that the code won't be executed. This is used internally to implement features like [io.wait()](https://trigger.dev/docs/sdk/io/wait). */
noop: z.boolean().default(false),
redact: RedactSchema.optional(),
@@ -729,3 +739,57 @@ export const CreateExternalConnectionBodySchema = z.object({
});
export type CreateExternalConnectionBody = z.infer<typeof CreateExternalConnectionBodySchema>;
export const UploadBackgroundFunctionRequestBodySchema = z.object({
id: z.string(),
version: z.string(),
fileName: z.string(),
bundle: z.string(),
sourcemap: z.string(),
dependencies: z.record(z.string()),
nodeVersion: z.string(),
});
export type UploadBackgroundFunctionRequestBody = z.infer<
typeof UploadBackgroundFunctionRequestBodySchema
>;
export const UploadBackgroundFunctionResponseBodySchema = z.object({
id: z.string(),
hash: z.string(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
export type UploadBackgroundFunctionResponseBody = z.infer<
typeof UploadBackgroundFunctionResponseBodySchema
>;
export const CreateBackgroundFunctionWorkerImageRequestBodySchema = z.object({
registry: z.string(),
name: z.string(),
tag: z.string(),
digest: z.string(),
size: z.number(),
});
export type CreateBackgroundFunctionWorkerImageRequestBody = z.infer<
typeof CreateBackgroundFunctionWorkerImageRequestBodySchema
>;
export const CreateBackgroundFunctionWorkerImageResponseBodySchema = z.object({
id: z.string(),
backgroundTaskArtifactId: z.string(),
backgroundTaskId: z.string(),
registry: z.string(),
name: z.string(),
tag: z.string(),
digest: z.string(),
size: z.number(),
createdAt: z.coerce.date(),
updatedAt: z.coerce.date(),
});
export type CreateBackgroundFunctionWorkerImageResponseBody = z.infer<
typeof CreateBackgroundFunctionWorkerImageResponseBodySchema
>;
@@ -0,0 +1,7 @@
import { z } from "zod";
export const BackgroundFunctionTaskParamsSchema = z.object({
id: z.string(),
version: z.string(),
payload: z.any(),
});
+1
View File
@@ -12,3 +12,4 @@ export * from "./fetch";
export * from "./events";
export * from "./runs";
export * from "./addMissingVersionField";
export * from "./backgroundFunctions";
@@ -0,0 +1,160 @@
-- CreateEnum
CREATE TYPE "BackgroundFunctionTaskStatus" AS ENUM ('PENDING', 'STARTED', 'SUCCESS', 'FAILURE');
-- CreateTable
CREATE TABLE "BackgroundFunction" (
"id" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"title" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"deletedAt" TIMESTAMP(3),
CONSTRAINT "BackgroundFunction_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BackgroundFunctionVersion" (
"id" TEXT NOT NULL,
"version" TEXT NOT NULL,
"backgroundFunctionId" TEXT NOT NULL,
"endpointId" TEXT NOT NULL,
"environmentId" TEXT NOT NULL,
"organizationId" TEXT NOT NULL,
"projectId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BackgroundFunctionVersion_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BackgroundFunctionAlias" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL DEFAULT 'latest',
"value" TEXT NOT NULL,
"versionId" TEXT NOT NULL,
"backgroundFunctionId" TEXT NOT NULL,
"environmentId" TEXT NOT NULL,
CONSTRAINT "BackgroundFunctionAlias_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BackgroundFunctionArtifact" (
"id" TEXT NOT NULL,
"version" TEXT NOT NULL,
"fileName" TEXT NOT NULL,
"hash" TEXT NOT NULL,
"bundle" TEXT NOT NULL,
"sourcemap" JSONB NOT NULL,
"nodeVersion" TEXT NOT NULL,
"dependencies" JSONB NOT NULL,
"backgroundFunctionId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BackgroundFunctionArtifact_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BackgroundFunctionImage" (
"id" TEXT NOT NULL,
"registry" TEXT NOT NULL,
"name" TEXT NOT NULL,
"tag" TEXT NOT NULL,
"digest" TEXT NOT NULL,
"size" INTEGER NOT NULL,
"backgroundFunctionId" TEXT NOT NULL,
"backgroundFunctionArtifactId" TEXT NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
CONSTRAINT "BackgroundFunctionImage_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "BackgroundFunctionTask" (
"id" TEXT NOT NULL,
"backgroundFunctionId" TEXT NOT NULL,
"backgroundFunctionVersionId" TEXT NOT NULL,
"taskId" TEXT NOT NULL,
"payload" JSONB NOT NULL,
"output" JSONB,
"error" JSONB,
"status" "BackgroundFunctionTaskStatus" NOT NULL DEFAULT 'PENDING',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"startedAt" TIMESTAMP(3),
"endedAt" TIMESTAMP(3),
CONSTRAINT "BackgroundFunctionTask_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "BackgroundFunction_projectId_slug_key" ON "BackgroundFunction"("projectId", "slug");
-- CreateIndex
CREATE UNIQUE INDEX "BackgroundFunctionVersion_backgroundFunctionId_version_envi_key" ON "BackgroundFunctionVersion"("backgroundFunctionId", "version", "environmentId");
-- CreateIndex
CREATE UNIQUE INDEX "BackgroundFunctionAlias_backgroundFunctionId_environmentId__key" ON "BackgroundFunctionAlias"("backgroundFunctionId", "environmentId", "name");
-- CreateIndex
CREATE UNIQUE INDEX "BackgroundFunctionArtifact_backgroundFunctionId_version_has_key" ON "BackgroundFunctionArtifact"("backgroundFunctionId", "version", "hash");
-- CreateIndex
CREATE UNIQUE INDEX "BackgroundFunctionImage_backgroundFunctionArtifactId_digest_key" ON "BackgroundFunctionImage"("backgroundFunctionArtifactId", "digest");
-- CreateIndex
CREATE UNIQUE INDEX "BackgroundFunctionTask_taskId_key" ON "BackgroundFunctionTask"("taskId");
-- AddForeignKey
ALTER TABLE "BackgroundFunction" ADD CONSTRAINT "BackgroundFunction_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunction" ADD CONSTRAINT "BackgroundFunction_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionVersion" ADD CONSTRAINT "BackgroundFunctionVersion_backgroundFunctionId_fkey" FOREIGN KEY ("backgroundFunctionId") REFERENCES "BackgroundFunction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionVersion" ADD CONSTRAINT "BackgroundFunctionVersion_endpointId_fkey" FOREIGN KEY ("endpointId") REFERENCES "Endpoint"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionVersion" ADD CONSTRAINT "BackgroundFunctionVersion_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "RuntimeEnvironment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionVersion" ADD CONSTRAINT "BackgroundFunctionVersion_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionVersion" ADD CONSTRAINT "BackgroundFunctionVersion_projectId_fkey" FOREIGN KEY ("projectId") REFERENCES "Project"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionAlias" ADD CONSTRAINT "BackgroundFunctionAlias_versionId_fkey" FOREIGN KEY ("versionId") REFERENCES "BackgroundFunctionVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionAlias" ADD CONSTRAINT "BackgroundFunctionAlias_backgroundFunctionId_fkey" FOREIGN KEY ("backgroundFunctionId") REFERENCES "BackgroundFunction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionAlias" ADD CONSTRAINT "BackgroundFunctionAlias_environmentId_fkey" FOREIGN KEY ("environmentId") REFERENCES "RuntimeEnvironment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionArtifact" ADD CONSTRAINT "BackgroundFunctionArtifact_backgroundFunctionId_fkey" FOREIGN KEY ("backgroundFunctionId") REFERENCES "BackgroundFunction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionImage" ADD CONSTRAINT "BackgroundFunctionImage_backgroundFunctionId_fkey" FOREIGN KEY ("backgroundFunctionId") REFERENCES "BackgroundFunction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionImage" ADD CONSTRAINT "BackgroundFunctionImage_backgroundFunctionArtifactId_fkey" FOREIGN KEY ("backgroundFunctionArtifactId") REFERENCES "BackgroundFunctionArtifact"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionTask" ADD CONSTRAINT "BackgroundFunctionTask_backgroundFunctionId_fkey" FOREIGN KEY ("backgroundFunctionId") REFERENCES "BackgroundFunction"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionTask" ADD CONSTRAINT "BackgroundFunctionTask_backgroundFunctionVersionId_fkey" FOREIGN KEY ("backgroundFunctionVersionId") REFERENCES "BackgroundFunctionVersion"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "BackgroundFunctionTask" ADD CONSTRAINT "BackgroundFunctionTask_taskId_fkey" FOREIGN KEY ("taskId") REFERENCES "Task"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+185 -33
View File
@@ -76,12 +76,14 @@ model Organization {
events EventRecord[]
jobRuns JobRun[]
projects Project[]
members OrgMember[]
invites OrgMemberInvite[]
externalAccounts ExternalAccount[]
integrations Integration[]
sources TriggerSource[]
projects Project[]
members OrgMember[]
invites OrgMemberInvite[]
externalAccounts ExternalAccount[]
integrations Integration[]
sources TriggerSource[]
backgroundFunctions BackgroundFunction[]
backgroundFunctionVersions BackgroundFunctionVersion[]
}
model ExternalAccount {
@@ -306,17 +308,19 @@ model RuntimeEnvironment {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
endpoints Endpoint[]
jobVersions JobVersion[]
events EventRecord[]
jobRuns JobRun[]
requestDeliveries HttpSourceRequestDelivery[]
jobAliases JobAlias[]
JobQueue JobQueue[]
sources TriggerSource[]
eventDispatchers EventDispatcher[]
scheduleSources ScheduleSource[]
ExternalAccount ExternalAccount[]
endpoints Endpoint[]
jobVersions JobVersion[]
events EventRecord[]
jobRuns JobRun[]
requestDeliveries HttpSourceRequestDelivery[]
jobAliases JobAlias[]
JobQueue JobQueue[]
sources TriggerSource[]
eventDispatchers EventDispatcher[]
scheduleSources ScheduleSource[]
ExternalAccount ExternalAccount[]
backgroundFunctionVersions BackgroundFunctionVersion[]
backgroundFunctionAliases BackgroundFunctionAlias[]
@@unique([projectId, slug, orgMemberId])
}
@@ -339,13 +343,15 @@ model Project {
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
environments RuntimeEnvironment[]
endpoints Endpoint[]
jobs Job[]
jobVersion JobVersion[]
events EventRecord[]
runs JobRun[]
sources TriggerSource[]
environments RuntimeEnvironment[]
endpoints Endpoint[]
jobs Job[]
jobVersion JobVersion[]
events EventRecord[]
runs JobRun[]
sources TriggerSource[]
backgroundFunctions BackgroundFunction[]
backgroundFunctionVersions BackgroundFunctionVersion[]
}
model Endpoint {
@@ -367,12 +373,13 @@ model Endpoint {
indexingHookIdentifier String?
jobVersions JobVersion[]
jobRuns JobRun[]
httpRequestDeliveries HttpSourceRequestDelivery[]
dynamictriggers DynamicTrigger[]
sources TriggerSource[]
indexings EndpointIndex[]
jobVersions JobVersion[]
jobRuns JobRun[]
httpRequestDeliveries HttpSourceRequestDelivery[]
dynamictriggers DynamicTrigger[]
sources TriggerSource[]
indexings EndpointIndex[]
backgroundFunctionVersions BackgroundFunctionVersion[]
@@unique([environmentId, slug])
}
@@ -807,9 +814,10 @@ model Task {
runConnection RunConnection? @relation(fields: [runConnectionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
runConnectionId String?
children Task[] @relation("TaskParent")
executions JobRunExecution[]
attempts TaskAttempt[]
children Task[] @relation("TaskParent")
executions JobRunExecution[]
attempts TaskAttempt[]
functionTask BackgroundFunctionTask?
@@unique([runId, idempotencyKey])
}
@@ -1068,3 +1076,147 @@ model ApiIntegrationVote {
@@unique([apiIdentifier, userId])
}
model BackgroundFunction {
id String @id @default(cuid())
slug String
title String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
organizationId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
projectId String
versions BackgroundFunctionVersion[]
aliases BackgroundFunctionAlias[]
artifacts BackgroundFunctionArtifact[]
createdAt DateTime @default(now())
updatedAt DateTime @default(now()) @updatedAt
deletedAt DateTime?
operations BackgroundFunctionTask[]
images BackgroundFunctionImage[]
@@unique([projectId, slug])
}
model BackgroundFunctionVersion {
id String @id @default(cuid())
version String
backgroundFunction BackgroundFunction @relation(fields: [backgroundFunctionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionId String
endpoint Endpoint @relation(fields: [endpointId], references: [id], onDelete: Cascade, onUpdate: Cascade)
endpointId String
environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
environmentId String
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
organizationId String
project Project @relation(fields: [projectId], references: [id], onDelete: Cascade, onUpdate: Cascade)
projectId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
aliases BackgroundFunctionAlias[]
tasks BackgroundFunctionTask[]
@@unique([backgroundFunctionId, version, environmentId])
}
model BackgroundFunctionAlias {
id String @id @default(cuid())
name String @default("latest")
value String
version BackgroundFunctionVersion @relation(fields: [versionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
versionId String
backgroundFunction BackgroundFunction @relation(fields: [backgroundFunctionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionId String
environment RuntimeEnvironment @relation(fields: [environmentId], references: [id], onDelete: Cascade, onUpdate: Cascade)
environmentId String
@@unique([backgroundFunctionId, environmentId, name])
}
model BackgroundFunctionArtifact {
id String @id @default(cuid())
version String
fileName String
hash String
bundle String
sourcemap Json
nodeVersion String
dependencies Json
backgroundFunction BackgroundFunction @relation(fields: [backgroundFunctionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
images BackgroundFunctionImage[]
@@unique([backgroundFunctionId, version, hash])
}
model BackgroundFunctionImage {
id String @id @default(cuid())
registry String
name String
tag String
digest String
size Int
backgroundFunction BackgroundFunction @relation(fields: [backgroundFunctionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionId String
backgroundFunctionArtifact BackgroundFunctionArtifact @relation(fields: [backgroundFunctionArtifactId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionArtifactId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@unique([backgroundFunctionArtifactId, digest])
}
model BackgroundFunctionTask {
id String @id @default(cuid())
backgroundFunction BackgroundFunction @relation(fields: [backgroundFunctionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionId String
backgroundFunctionVersion BackgroundFunctionVersion @relation(fields: [backgroundFunctionVersionId], references: [id], onDelete: Cascade, onUpdate: Cascade)
backgroundFunctionVersionId String
task Task @relation(fields: [taskId], references: [id], onDelete: Cascade, onUpdate: Cascade)
taskId String @unique
payload Json
output Json?
error Json?
status BackgroundFunctionTaskStatus @default(PENDING)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
startedAt DateTime?
endedAt DateTime?
}
enum BackgroundFunctionTaskStatus {
PENDING
STARTED
SUCCESS
FAILURE
}
+9
View File
@@ -0,0 +1,9 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"rootDir": "../src",
"target": "es2022",
"module": "nodenext",
"moduleResolution": "nodenext"
}
}
+14
View File
@@ -0,0 +1,14 @@
{
"extends": "./build.json",
"include": [
"../src/**/*.ts",
"../src/**/*.cts",
"../src/**/*.tsx"
],
"exclude": [
".../src/**/*.mts"
],
"compilerOptions": {
"outDir": "../.tshy-build-tmp/commonjs"
}
}
+11
View File
@@ -0,0 +1,11 @@
{
"extends": "./build.json",
"include": [
"../src/**/*.ts",
"../src/**/*.mts",
"../src/**/*.tsx"
],
"compilerOptions": {
"outDir": "../.tshy-build-tmp/esm"
}
}
+47
View File
@@ -0,0 +1,47 @@
{
"name": "@trigger.dev/functions",
"version": "2.1.2",
"description": "Trigger.dev Common Background Functions",
"license": "MIT",
"publishConfig": {
"access": "public"
},
"files": [
"dist"
],
"scripts": {
"clean": "rimraf dist",
"typecheck": "tsc --noEmit",
"prepare": "tshy"
},
"dependencies": {
"zod": "3.21.4"
},
"devDependencies": {
"tshy": "^1.0.0",
"typescript": "^5.2.0"
},
"engines": {
"node": ">=18.0.0"
},
"type": "module",
"tshy": {
"exports": {
"./package.json": "./package.json",
".": "./src/index.ts"
}
},
"exports": {
"./package.json": "./package.json",
".": {
"import": {
"types": "./dist/esm/index.d.ts",
"default": "./dist/esm/index.js"
},
"require": {
"types": "./dist/commonjs/index.d.ts",
"default": "./dist/commonjs/index.js"
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export type Foo = string;
export {};
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
"paths": {
"@trigger.dev/core/*": ["../core/src/*"],
"@trigger.dev/core": ["../core/src/index"]
},
"lib": ["DOM", "DOM.Iterable"],
"declaration": true,
"declarationMap": true
},
"exclude": ["node_modules"]
}
@@ -0,0 +1,88 @@
import { BackgroundFunctionMetadata, LogLevel } from "@trigger.dev/core";
import { z } from "zod";
import { TriggerClient } from "./triggerClient";
import { slugifyId } from "./utils";
import { runLocalStorage } from "./runLocalStorage";
export type BackgroundFunctionOptions<TPayload = any, TRunResult = any> = {
id: string;
name: string;
version: string;
schema?: z.Schema<TPayload>;
logLevel?: LogLevel;
enabled?: boolean;
run: (payload: TPayload) => Promise<TRunResult>;
};
export class BackgroundFunction<TPayload = any, TRunResult = any> {
readonly options: BackgroundFunctionOptions<TPayload, TRunResult>;
client: TriggerClient;
constructor(client: TriggerClient, options: BackgroundFunctionOptions<TPayload, TRunResult>) {
this.client = client;
this.options = options;
this.#validate();
client.attachBackgroundFunction(this);
}
get id() {
return slugifyId(this.options.id);
}
get enabled() {
return typeof this.options.enabled === "boolean" ? this.options.enabled : true;
}
get name() {
return this.options.name;
}
get schema() {
return this.options.schema;
}
get version() {
return this.options.version;
}
get logLevel() {
return this.options.logLevel;
}
public async invoke(key: string | string[], payload: TPayload): Promise<TRunResult> {
if (!this.enabled) {
throw new Error(`Cannot invoke a disabled background task: ${this.id}`);
}
const runStore = runLocalStorage.getStore();
if (!runStore) {
throw new Error("Cannot invoke a background task outside of a job run");
}
const { io, ctx } = runStore;
return await io.invokeBackgroundFunction(key, this.id, this.version, payload);
}
toJSON(): BackgroundFunctionMetadata {
return {
id: this.id,
name: this.name,
version: this.version,
enabled: this.enabled,
};
}
// Make sure the id is valid (must only contain alphanumeric characters and dashes)
// Make sure the version is valid (must be a valid semver version)
#validate() {
if (!this.version.match(/^(\d+)\.(\d+)\.(\d+)$/)) {
throw new Error(
`Invalid job version: "${this.version}". BackgroundFunction versions must be valid semver versions.`
);
}
}
}
+37 -2
View File
@@ -11,7 +11,6 @@ import {
RunTaskOptions,
SendEvent,
SendEventOptions,
SerializableJson,
SerializableJsonSchema,
ServerTask,
UpdateTriggerSourceBodyV2,
@@ -25,7 +24,6 @@ import {
RetryWithTaskError,
isTriggerError,
} from "./errors";
import { createIOWithIntegrations } from "./ioWithIntegrations";
import { calculateRetryAt } from "./retry";
import { TriggerClient } from "./triggerClient";
import { DynamicTrigger } from "./triggers/dynamic";
@@ -199,6 +197,43 @@ export class IO {
)) as TResponseData;
}
/** `io.invokeBackgroundFunction()` invokes a background function by id and version.
* @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
* @param id The id of the background function to invoke.
* @param version The version of the background function to invoke.
* @param payload The payload to send to the background function.
*/
async invokeBackgroundFunction<TResultData>(
key: string | any[],
id: string,
version: string,
payload: any
): Promise<TResultData> {
return (await this.runTask(
key,
async (task) => {
return task.output;
},
{
name: `invoke ${id}`,
params: { payload, id, version },
operation: "invokeBackgroundFunction",
icon: "background",
noop: false,
properties: [
{
label: "Function ID",
text: id,
},
{
label: "Version",
text: version,
},
],
}
)) as TResultData;
}
/** `io.sendEvent()` allows you to send an event from inside a Job run. The sent even will trigger any Jobs that are listening for that event (based on the name).
* @param key Should be a stable and unique key inside the `run()`. See [resumability](https://trigger.dev/docs/documentation/concepts/resumability) for more information.
* @param event The event to send. The event name must match the name of the event that your Jobs are listening for.
@@ -0,0 +1,10 @@
import { IO } from "./io";
import { TriggerContext } from "./types";
import { TypedAsyncLocalStorage } from "./utils/typedAsyncLocalStorage";
export type RunStore = {
io: IO;
ctx: TriggerContext;
};
export const runLocalStorage = new TypedAsyncLocalStorage<RunStore>();
+23 -5
View File
@@ -42,6 +42,8 @@ import type {
TriggerContext,
TriggerPreprocessContext,
} from "./types";
import { BackgroundFunction, BackgroundFunctionOptions } from "./backgroundFunction";
import { runLocalStorage } from "./runLocalStorage";
const registerSourceEvent: EventSpecification<RegisterSourceEventV2> = {
name: REGISTER_SOURCE_EVENT_V2,
@@ -93,6 +95,7 @@ export class TriggerClient {
> = {};
#jobMetadataByDynamicTriggers: Record<string, Array<{ id: string; version: string }>> = {};
#registeredSchedules: Record<string, Array<{ id: string; version: string }>> = {};
#registeredBackgroundFunctions: Record<string, BackgroundFunction<any>> = {};
#client: ApiClient;
#internalLogger: Logger;
@@ -234,6 +237,9 @@ export class TriggerClient {
id,
jobs,
})),
backgroundFunctions: Object.values(this.#registeredBackgroundFunctions).map((func) =>
func.toJSON()
),
};
// if the x-trigger-job-id header is not set, we return all jobs
@@ -426,6 +432,10 @@ export class TriggerClient {
job.trigger.attachToJob(this, job);
}
attachBackgroundFunction(func: BackgroundFunction<any>): void {
this.#registeredBackgroundFunctions[func.id] = func;
}
attachDynamicTrigger(trigger: DynamicTrigger<any, any>): void {
this.#registeredDynamicTriggers[trigger.id] = trigger;
@@ -648,11 +658,13 @@ export class TriggerClient {
);
try {
const output = await job.options.run(
job.trigger.event.parsePayload(body.event.payload ?? {}),
ioWithConnections,
context
);
const output = await runLocalStorage.runWith({ io, ctx: context }, () => {
return job.options.run(
job.trigger.event.parsePayload(body.event.payload ?? {}),
ioWithConnections,
context
);
});
return { status: "SUCCESS", output };
} catch (error) {
@@ -863,6 +875,12 @@ export class TriggerClient {
>(options: JobOptions<TTrigger, TIntegrations>) {
return new Job<TTrigger, TIntegrations>(this, options);
}
defineBackgroundFunction<TPayload = any, TRunResult = any>(
options: BackgroundFunctionOptions<TPayload, TRunResult>
) {
return new BackgroundFunction<TPayload, TRunResult>(this, options);
}
}
function dynamicTriggerRegisterSourceJobId(id: string) {
@@ -0,0 +1,17 @@
import { AsyncLocalStorage } from "node:async_hooks";
export class TypedAsyncLocalStorage<T> {
private storage: AsyncLocalStorage<T>;
constructor() {
this.storage = new AsyncLocalStorage<T>();
}
runWith<R extends (...args: any[]) => Promise<any>>(context: T, fn: R): Promise<ReturnType<R>> {
return this.storage.run(context, fn);
}
getStore(): T | undefined {
return this.storage.getStore();
}
}
+370 -78
View File
File diff suppressed because it is too large Load Diff