v3: better task metadata errors (#991)

* v3: better handle task metadata parse errors, and display nicely formatted errors (dev, deploy, UI)

* Add changeset
This commit is contained in:
Eric Allam
2024-03-31 21:47:05 +01:00
committed by GitHub
parent 9af2570da6
commit c702d6a9ca
21 changed files with 360 additions and 29 deletions
+6
View File
@@ -0,0 +1,6 @@
---
"trigger.dev": patch
"@trigger.dev/core": patch
---
better handle task metadata parse errors, and display nicely formatted errors
+1 -1
View File
@@ -41,7 +41,7 @@
"type": "node-terminal", "type": "node-terminal",
"request": "launch", "request": "launch",
"name": "Debug V3 Deploy CLI", "name": "Debug V3 Deploy CLI",
"command": "pnpm exec trigger.dev deploy --skip-deploy", "command": "pnpm exec trigger.dev deploy",
"cwd": "${workspaceFolder}/references/v3-catalog", "cwd": "${workspaceFolder}/references/v3-catalog",
"sourceMaps": true "sourceMaps": true
}, },
@@ -1,8 +1,15 @@
import {
DeploymentErrorData,
TaskMetadataFailedToParseData,
groupTaskMetadataIssuesByTask,
} from "@trigger.dev/core/v3";
import { WorkerDeployment, WorkerDeploymentStatus } from "@trigger.dev/database"; import { WorkerDeployment, WorkerDeploymentStatus } from "@trigger.dev/database";
import { z } from "zod";
import { PrismaClient, prisma } from "~/db.server"; import { PrismaClient, prisma } from "~/db.server";
import { Organization } from "~/models/organization.server"; import { Organization } from "~/models/organization.server";
import { Project } from "~/models/project.server"; import { Project } from "~/models/project.server";
import { User } from "~/models/user.server"; import { User } from "~/models/user.server";
import { safeJsonParse } from "~/utils/json";
import { getUsername } from "~/utils/username"; import { getUsername } from "~/utils/username";
export class DeploymentPresenter { export class DeploymentPresenter {
@@ -51,6 +58,7 @@ export class DeploymentPresenter {
id: true, id: true,
shortCode: true, shortCode: true,
version: true, version: true,
errorData: true,
environment: { environment: {
select: { select: {
id: true, id: true,
@@ -120,7 +128,81 @@ export class DeploymentPresenter {
userName: getUsername(deployment.environment.orgMember?.user), userName: getUsername(deployment.environment.orgMember?.user),
}, },
deployedBy: deployment.triggeredBy, deployedBy: deployment.triggeredBy,
errorData: this.#prepareErrorData(deployment.errorData),
}, },
}; };
} }
#prepareErrorData(errorData: WorkerDeployment["errorData"]) {
if (!errorData) {
return;
}
const parsedErrorData = DeploymentErrorData.safeParse(errorData);
if (!parsedErrorData.success) {
return;
}
if (parsedErrorData.data.name === "TaskMetadataParseError") {
const errorJson = safeJsonParse(parsedErrorData.data.stack);
if (errorJson) {
const parsedError = TaskMetadataFailedToParseData.safeParse(errorJson);
if (parsedError.success) {
return {
name: parsedErrorData.data.name,
message: parsedErrorData.data.message,
stack: createTaskMetadataFailedErrorStack(parsedError.data),
};
} else {
return {
name: parsedErrorData.data.name,
message: parsedErrorData.data.message,
};
}
} else {
return {
name: parsedErrorData.data.name,
message: parsedErrorData.data.message,
};
}
}
return {
name: parsedErrorData.data.name,
message: parsedErrorData.data.message,
stack: parsedErrorData.data.stack,
};
}
}
function createTaskMetadataFailedErrorStack(
data: z.infer<typeof TaskMetadataFailedToParseData>
): string {
const stack = [];
const groupedIssues = groupTaskMetadataIssuesByTask(data.tasks, data.zodIssues);
for (const key in groupedIssues) {
const taskWithIssues = groupedIssues[key];
if (!taskWithIssues) {
continue;
}
stack.push("\n");
stack.push(` ${taskWithIssues.exportName} in ${taskWithIssues.filePath}`);
for (const issue of taskWithIssues.issues) {
if (issue.path) {
stack.push(` x ${issue.path} ${issue.message}`);
} else {
stack.push(` x ${issue.message}`);
}
}
}
return stack.join("\n");
} }
@@ -3,6 +3,7 @@ import { LoaderFunctionArgs } from "@remix-run/server-runtime";
import { typedjson, useTypedLoaderData } from "remix-typedjson"; import { typedjson, useTypedLoaderData } from "remix-typedjson";
import { ExitIcon } from "~/assets/icons/ExitIcon"; import { ExitIcon } from "~/assets/icons/ExitIcon";
import { UserAvatar } from "~/components/UserProfilePhoto"; import { UserAvatar } from "~/components/UserProfilePhoto";
import { CodeBlock } from "~/components/code/CodeBlock";
import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel"; import { EnvironmentLabel } from "~/components/environments/EnvironmentLabel";
import { Badge } from "~/components/primitives/Badge"; import { Badge } from "~/components/primitives/Badge";
import { LinkButton } from "~/components/primitives/Buttons"; import { LinkButton } from "~/components/primitives/Buttons";
@@ -156,6 +157,26 @@ export default function Page() {
</TableBody> </TableBody>
</Table> </Table>
</div> </div>
) : deployment.errorData ? (
<div className="flex flex-col">
{deployment.errorData.stack ? (
<CodeBlock
language="markdown"
rowTitle={deployment.errorData.message}
code={deployment.errorData.stack}
maxLines={20}
/>
) : (
<div className="flex flex-col">
<Paragraph
variant="base/bright"
className="w-full border-b border-grid-dimmed py-2.5"
>
{deployment.errorData.message}
</Paragraph>
</div>
)}
</div>
) : null} ) : null}
</div> </div>
</div> </div>
+5 -1
View File
@@ -1,6 +1,10 @@
import { z } from "zod"; import { z } from "zod";
export function safeJsonParse(json: string): unknown { export function safeJsonParse(json?: string): unknown {
if (!json) {
return;
}
try { try {
return JSON.parse(json); return JSON.parse(json);
} catch (e) { } catch (e) {
@@ -8,6 +8,7 @@ import { marqs, sanitizeQueueName } from "~/v3/marqs/index.server";
import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion"; import { calculateNextBuildVersion } from "../utils/calculateNextBuildVersion";
import { BaseService } from "./baseService.server"; import { BaseService } from "./baseService.server";
import { projectPubSub } from "./projectPubSub.server"; import { projectPubSub } from "./projectPubSub.server";
import { env } from "~/env.server";
export class CreateBackgroundWorkerService extends BaseService { export class CreateBackgroundWorkerService extends BaseService {
public async call( public async call(
@@ -110,7 +111,7 @@ export class CreateBackgroundWorkerService extends BaseService {
export async function createBackgroundTasks( export async function createBackgroundTasks(
tasks: TaskResource[], tasks: TaskResource[],
worker: BackgroundWorker, worker: BackgroundWorker,
env: AuthenticatedEnvironment, environment: AuthenticatedEnvironment,
prisma: PrismaClientOrTransaction prisma: PrismaClientOrTransaction
) { ) {
for (const task of tasks) { for (const task of tasks) {
@@ -137,6 +138,18 @@ export async function createBackgroundTasks(
queueName = sanitizeQueueName(`task/${task.id}`); queueName = sanitizeQueueName(`task/${task.id}`);
} }
const concurrencyLimit =
typeof task.queue?.concurrencyLimit === "number"
? Math.max(
Math.min(
task.queue.concurrencyLimit,
environment.maximumConcurrencyLimit,
environment.organization.maximumConcurrencyLimit
),
0
)
: null;
const taskQueue = await prisma.taskQueue.upsert({ const taskQueue = await prisma.taskQueue.upsert({
where: { where: {
runtimeEnvironmentId_name: { runtimeEnvironmentId_name: {
@@ -145,13 +158,13 @@ export async function createBackgroundTasks(
}, },
}, },
update: { update: {
concurrencyLimit: task.queue?.concurrencyLimit, concurrencyLimit,
rateLimit: task.queue?.rateLimit, rateLimit: task.queue?.rateLimit,
}, },
create: { create: {
friendlyId: generateFriendlyId("queue"), friendlyId: generateFriendlyId("queue"),
name: queueName, name: queueName,
concurrencyLimit: task.queue?.concurrencyLimit, concurrencyLimit,
runtimeEnvironmentId: worker.runtimeEnvironmentId, runtimeEnvironmentId: worker.runtimeEnvironmentId,
projectId: worker.projectId, projectId: worker.projectId,
rateLimit: task.queue?.rateLimit, rateLimit: task.queue?.rateLimit,
@@ -160,7 +173,11 @@ export async function createBackgroundTasks(
}); });
if (taskQueue.concurrencyLimit) { if (taskQueue.concurrencyLimit) {
await marqs?.updateQueueConcurrencyLimits(env, taskQueue.name, taskQueue.concurrencyLimit); await marqs?.updateQueueConcurrencyLimits(
environment,
taskQueue.name,
taskQueue.concurrencyLimit
);
} }
} catch (error) { } catch (error) {
if (error instanceof Prisma.PrismaClientKnownRequestError) { if (error instanceof Prisma.PrismaClientKnownRequestError) {
+21
View File
@@ -3,6 +3,7 @@ import { depot } from "@depot/cli";
import { context, trace } from "@opentelemetry/api"; import { context, trace } from "@opentelemetry/api";
import { import {
ResolvedConfig, ResolvedConfig,
TaskMetadataFailedToParseData,
detectDependencyVersion, detectDependencyVersion,
flattenAttributes, flattenAttributes,
recordSpanException, recordSpanException,
@@ -49,9 +50,11 @@ import { bundleDependenciesPlugin, workerSetupImportConfigPlugin } from "../util
import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput"; import { chalkError, chalkPurple, chalkWarning } from "../utilities/cliOutput";
import { import {
logESMRequireError, logESMRequireError,
logTaskMetadataParseError,
parseBuildErrorStack, parseBuildErrorStack,
parseNpmInstallError, parseNpmInstallError,
} from "../utilities/deployErrors"; } from "../utilities/deployErrors";
import { safeJsonParse } from "../utilities/safeJsonParse";
const DeployCommandOptions = CommonCommandOptions.extend({ const DeployCommandOptions = CommonCommandOptions.extend({
skipTypecheck: z.boolean().default(false), skipTypecheck: z.boolean().default(false),
@@ -401,6 +404,24 @@ async function _deployCommand(dir: string, options: DeployCommandOptions) {
} }
case "FAILED": { case "FAILED": {
if (finishedDeployment.errorData) { if (finishedDeployment.errorData) {
if (finishedDeployment.errorData.name === "TaskMetadataParseError") {
const errorJson = safeJsonParse(finishedDeployment.errorData.stack);
if (errorJson) {
const parsedError = TaskMetadataFailedToParseData.safeParse(errorJson);
if (parsedError.success) {
deploymentSpinner.stop(`Deployment encountered an error. ${deploymentLink}`);
logTaskMetadataParseError(parsedError.data.zodIssues, parsedError.data.tasks);
throw new SkipLoggingError(
`Deployment encountered an error: ${finishedDeployment.errorData.name}`
);
}
}
}
const parsedError = finishedDeployment.errorData.stack const parsedError = finishedDeployment.errorData.stack
? parseBuildErrorStack(finishedDeployment.errorData) ? parseBuildErrorStack(finishedDeployment.errorData)
: finishedDeployment.errorData.message; : finishedDeployment.errorData.message;
+6 -2
View File
@@ -38,11 +38,12 @@ import {
import { logger } from "../utilities/logger.js"; import { logger } from "../utilities/logger.js";
import { isLoggedIn } from "../utilities/session.js"; import { isLoggedIn } from "../utilities/session.js";
import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles"; import { createTaskFileImports, gatherTaskFiles } from "../utilities/taskFiles";
import { UncaughtExceptionError } from "../workers/common/errors"; import { TaskMetadataParseError, UncaughtExceptionError } from "../workers/common/errors";
import { BackgroundWorker, BackgroundWorkerCoordinator } from "../workers/dev/backgroundWorker.js"; import { BackgroundWorker, BackgroundWorkerCoordinator } from "../workers/dev/backgroundWorker.js";
import { runtimeCheck } from "../utilities/runtimeCheck"; import { runtimeCheck } from "../utilities/runtimeCheck";
import { import {
logESMRequireError, logESMRequireError,
logTaskMetadataParseError,
parseBuildErrorStack, parseBuildErrorStack,
parseNpmInstallError, parseNpmInstallError,
} from "../utilities/deployErrors"; } from "../utilities/deployErrors";
@@ -548,7 +549,10 @@ function useDev({
backgroundWorker backgroundWorker
); );
} catch (e) { } catch (e) {
if (e instanceof UncaughtExceptionError) { if (e instanceof TaskMetadataParseError) {
logTaskMetadataParseError(e.zodIssues, e.tasks);
return;
} else if (e instanceof UncaughtExceptionError) {
const parsedBuildError = parseBuildErrorStack(e.originalError); const parsedBuildError = parseBuildErrorStack(e.originalError);
if (typeof parsedBuildError !== "string") { if (typeof parsedBuildError !== "string") {
+36 -1
View File
@@ -1,8 +1,11 @@
import chalk from "chalk"; import chalk from "chalk";
import { relative } from "node:path"; import { relative } from "node:path";
import { chalkError, chalkPurple, chalkGrey, chalkGreen } from "./cliOutput"; import { chalkError, chalkPurple, chalkGrey, chalkGreen, chalkWarning } from "./cliOutput";
import { logger } from "./logger"; import { logger } from "./logger";
import { ReadConfigResult } from "./configFiles"; import { ReadConfigResult } from "./configFiles";
import { TaskMetadataParseError } from "../workers/common/errors";
import { z } from "zod";
import { groupTaskMetadataIssuesByTask } from "@trigger.dev/core/v3";
export type ESMRequireError = { export type ESMRequireError = {
type: "esm-require-error"; type: "esm-require-error";
@@ -144,3 +147,35 @@ export function parseNpmInstallError(error: unknown): NpmInstallError {
return "Unknown error"; return "Unknown error";
} }
export function logTaskMetadataParseError(zodIssues: z.ZodIssue[], tasks: any) {
logger.log(
`\n${chalkError("X Error:")} Failed to start. The following ${
zodIssues.length === 1 ? "task issue was" : "task issues were"
} found:`
);
const groupedIssues = groupTaskMetadataIssuesByTask(tasks, zodIssues);
for (const key in groupedIssues) {
const taskWithIssues = groupedIssues[key];
if (!taskWithIssues) {
continue;
}
logger.log(
`\n ${chalkWarning("")} ${taskWithIssues.exportName} ${chalkGrey("in")} ${
taskWithIssues.filePath
}`
);
for (const issue of taskWithIssues.issues) {
if (issue.path) {
logger.log(` ${chalkError("x")} ${issue.path} ${chalkGrey(issue.message)}`);
} else {
logger.log(` ${chalkError("x")} ${chalkGrey(issue.message)}`);
}
}
}
}
@@ -0,0 +1,11 @@
export function safeJsonParse(json?: string): unknown {
if (!json) {
return undefined;
}
try {
return JSON.parse(json);
} catch {
return undefined;
}
}
@@ -1,3 +1,5 @@
import { z } from "zod";
export class UncaughtExceptionError extends Error { export class UncaughtExceptionError extends Error {
constructor( constructor(
public readonly originalError: { name: string; message: string; stack?: string }, public readonly originalError: { name: string; message: string; stack?: string },
@@ -8,3 +10,14 @@ export class UncaughtExceptionError extends Error {
this.name = "UncaughtExceptionError"; this.name = "UncaughtExceptionError";
} }
} }
export class TaskMetadataParseError extends Error {
constructor(
public readonly zodIssues: z.ZodIssue[],
public readonly tasks: any
) {
super(`Failed to parse task metadata`);
this.name = "TaskMetadataParseError";
}
}
@@ -37,7 +37,7 @@ import {
import { safeDeleteFileSync } from "../../utilities/fileSystem.js"; import { safeDeleteFileSync } from "../../utilities/fileSystem.js";
import { installPackages } from "../../utilities/installPackages.js"; import { installPackages } from "../../utilities/installPackages.js";
import { logger } from "../../utilities/logger.js"; import { logger } from "../../utilities/logger.js";
import { UncaughtExceptionError } from "../common/errors.js"; import { TaskMetadataParseError, UncaughtExceptionError } from "../common/errors.js";
export type CurrentWorkers = BackgroundWorkerCoordinator["currentWorkers"]; export type CurrentWorkers = BackgroundWorkerCoordinator["currentWorkers"];
export class BackgroundWorkerCoordinator { export class BackgroundWorkerCoordinator {
@@ -352,6 +352,11 @@ export class BackgroundWorker {
resolved = true; resolved = true;
reject(new UncaughtExceptionError(message.payload.error, message.payload.origin)); reject(new UncaughtExceptionError(message.payload.error, message.payload.origin));
child.kill(); child.kill();
} else if (message.type === "TASKS_FAILED_TO_PARSE") {
clearTimeout(timeout);
resolved = true;
reject(new TaskMetadataParseError(message.payload.zodIssues, message.payload.tasks));
child.kill();
} }
}); });
@@ -9,6 +9,7 @@ import {
logLevels, logLevels,
LogLevel, LogLevel,
getEnvVar, getEnvVar,
ZodSchemaParsedError,
} from "@trigger.dev/core/v3"; } from "@trigger.dev/core/v3";
__WORKER_SETUP__; __WORKER_SETUP__;
@@ -219,8 +220,14 @@ process.on("message", async (msg: any) => {
await handler.handleMessage(msg); await handler.handleMessage(msg);
}); });
sender.send("TASKS_READY", { tasks: getTaskMetadata() }).catch((err) => { const TASK_METADATA = getTaskMetadata();
console.error("Failed to send TASKS_READY message", err);
sender.send("TASKS_READY", { tasks: TASK_METADATA }).catch((err) => {
if (err instanceof ZodSchemaParsedError) {
sender.send("TASKS_FAILED_TO_PARSE", { zodIssues: err.error.issues, tasks: TASK_METADATA });
} else {
console.error("Failed to send TASKS_READY message", err);
}
}); });
process.title = "trigger-dev-worker"; process.title = "trigger-dev-worker";
@@ -19,7 +19,7 @@ import {
} from "@trigger.dev/core/v3"; } from "@trigger.dev/core/v3";
import { Evt } from "evt"; import { Evt } from "evt";
import { ChildProcess, fork } from "node:child_process"; import { ChildProcess, fork } from "node:child_process";
import { UncaughtExceptionError } from "../common/errors"; import { TaskMetadataParseError, UncaughtExceptionError } from "../common/errors";
class UnexpectedExitError extends Error { class UnexpectedExitError extends Error {
constructor(public code: number) { constructor(public code: number) {
@@ -149,6 +149,14 @@ export class ProdBackgroundWorker {
child.kill(); child.kill();
} }
}, },
TASKS_FAILED_TO_PARSE: async (message) => {
if (!resolved) {
clearTimeout(timeout);
resolved = true;
reject(new TaskMetadataParseError(message.zodIssues, message.tasks));
child.kill();
}
},
}, },
}); });
@@ -13,7 +13,7 @@ import { readFile } from "node:fs/promises";
import { createServer } from "node:http"; import { createServer } from "node:http";
import { z } from "zod"; import { z } from "zod";
import { ProdBackgroundWorker } from "./backgroundWorker"; import { ProdBackgroundWorker } from "./backgroundWorker";
import { UncaughtExceptionError } from "../common/errors"; import { TaskMetadataParseError, UncaughtExceptionError } from "../common/errors";
import { setTimeout } from "node:timers/promises"; import { setTimeout } from "node:timers/promises";
declare const __PROJECT_CONFIG__: Config; declare const __PROJECT_CONFIG__: Config;
@@ -416,7 +416,19 @@ class ProdWorker {
process.exit(1); process.exit(1);
} }
} catch (e) { } catch (e) {
if (e instanceof UncaughtExceptionError) { if (e instanceof TaskMetadataParseError) {
logger.error("tasks metadata parse error", { message: e.zodIssues, tasks: e.tasks });
socket.emit("INDEXING_FAILED", {
version: "v1",
deploymentId: this.deploymentId,
error: {
name: "TaskMetadataParseError",
message: "There was an error parsing the task metadata",
stack: JSON.stringify({ zodIssues: e.zodIssues, tasks: e.tasks }),
},
});
} else if (e instanceof UncaughtExceptionError) {
logger.error("uncaught exception", { message: e.originalError.message }); logger.error("uncaught exception", { message: e.originalError.message });
socket.emit("INDEXING_FAILED", { socket.emit("INDEXING_FAILED", {
@@ -12,6 +12,7 @@ import {
getEnvVar, getEnvVar,
logLevels, logLevels,
LogLevel, LogLevel,
ZodSchemaParsedError,
} from "@trigger.dev/core/v3"; } from "@trigger.dev/core/v3";
import "source-map-support/register.js"; import "source-map-support/register.js";
@@ -219,8 +220,14 @@ const prodRuntimeManager = new ProdRuntimeManager(zodIpc, {
runtime.setGlobalRuntimeManager(prodRuntimeManager); runtime.setGlobalRuntimeManager(prodRuntimeManager);
zodIpc.send("TASKS_READY", { tasks: getTaskMetadata() }).catch((err) => { const TASK_METADATA = getTaskMetadata();
console.error("Failed to send TASKS_READY message", err);
zodIpc.send("TASKS_READY", { tasks: TASK_METADATA }).catch((err) => {
if (err instanceof ZodSchemaParsedError) {
zodIpc.send("TASKS_FAILED_TO_PARSE", { zodIssues: err.error.issues, tasks: TASK_METADATA });
} else {
console.error("Failed to send TASKS_READY message", err);
}
}); });
process.title = "trigger-prod-worker"; process.title = "trigger-prod-worker";
+57
View File
@@ -1,3 +1,4 @@
import { z } from "zod";
import { TaskRunError } from "./schemas/common"; import { TaskRunError } from "./schemas/common";
import nodePath from "node:path"; import nodePath from "node:path";
@@ -95,3 +96,59 @@ function correctStackTraceLine(line: string, projectDir?: string) {
return line; return line;
} }
export function groupTaskMetadataIssuesByTask(tasks: any, issues: z.ZodIssue[]) {
return issues.reduce(
(acc, issue) => {
if (issue.path.length === 0) {
return acc;
}
const taskIndex = issue.path[1];
if (typeof taskIndex !== "number") {
return acc;
}
const task = tasks[taskIndex];
if (!task) {
return acc;
}
const restOfPath = issue.path.slice(2);
const taskId = task.id;
const taskName = task.exportName;
const filePath = task.filePath;
const key = taskIndex;
const existing = acc[key] ?? {
id: taskId,
exportName: taskName,
filePath,
issues: [] as Array<{ message: string; path?: string }>,
};
existing.issues.push({
message: issue.message,
path: restOfPath.length === 0 ? undefined : restOfPath.join("."),
});
return {
...acc,
[key]: existing,
};
},
{} as Record<
number,
{
id: any;
exportName: string;
filePath: string;
issues: Array<{ message: string; path?: string }>;
}
>
);
}
+7 -8
View File
@@ -153,6 +153,12 @@ export const InitializeDeploymentRequestBody = z.object({
export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymentRequestBody>; export type InitializeDeploymentRequestBody = z.infer<typeof InitializeDeploymentRequestBody>;
export const DeploymentErrorData = z.object({
name: z.string(),
message: z.string(),
stack: z.string().optional(),
});
export const GetDeploymentResponseBody = z.object({ export const GetDeploymentResponseBody = z.object({
id: z.string(), id: z.string(),
status: z.enum([ status: z.enum([
@@ -168,14 +174,7 @@ export const GetDeploymentResponseBody = z.object({
shortCode: z.string(), shortCode: z.string(),
version: z.string(), version: z.string(),
imageReference: z.string().optional(), imageReference: z.string().optional(),
errorData: z errorData: DeploymentErrorData.optional().nullable(),
.object({
name: z.string(),
message: z.string(),
stack: z.string().optional(),
})
.optional()
.nullable(),
worker: z worker: z
.object({ .object({
id: z.string(), id: z.string(),
+14 -2
View File
@@ -1,7 +1,7 @@
import { z } from "zod"; import { z } from "zod";
import { TaskRunExecution, TaskRunExecutionResult } from "./common"; import { TaskRunExecution, TaskRunExecutionResult } from "./common";
export const EnvironmentType = z.enum(["PRODUCTION", "STAGING", "DEVELOPMENT", "PREVIEW"]) export const EnvironmentType = z.enum(["PRODUCTION", "STAGING", "DEVELOPMENT", "PREVIEW"]);
export type EnvironmentType = z.infer<typeof EnvironmentType>; export type EnvironmentType = z.infer<typeof EnvironmentType>;
export const MachineCpu = z export const MachineCpu = z
@@ -244,7 +244,7 @@ export const QueueOptions = z.object({
/** An optional property that specifies the maximum number of concurrent run executions. /** An optional property that specifies the maximum number of concurrent run executions.
* *
* If this property is omitted, the task can potentially use up the full concurrency of an environment. */ * If this property is omitted, the task can potentially use up the full concurrency of an environment. */
concurrencyLimit: z.number().int().min(1).max(1000).optional(), concurrencyLimit: z.number().int().min(0).max(1000).optional(),
/** @deprecated This feature is coming soon */ /** @deprecated This feature is coming soon */
rateLimit: RateLimitOptions.optional(), rateLimit: RateLimitOptions.optional(),
}); });
@@ -278,6 +278,14 @@ export const UncaughtExceptionMessage = z.object({
origin: z.enum(["uncaughtException", "unhandledRejection"]), origin: z.enum(["uncaughtException", "unhandledRejection"]),
}); });
export const TaskMetadataFailedToParseData = z.object({
version: z.literal("v1").default("v1"),
tasks: z.unknown(),
zodIssues: z.custom<z.ZodIssue[]>((v) => {
return Array.isArray(v) && v.every((issue) => typeof issue === "object" && "message" in issue);
}),
});
export const childToWorkerMessages = { export const childToWorkerMessages = {
TASK_RUN_COMPLETED: z.object({ TASK_RUN_COMPLETED: z.object({
version: z.literal("v1").default("v1"), version: z.literal("v1").default("v1"),
@@ -288,6 +296,7 @@ export const childToWorkerMessages = {
version: z.literal("v1").default("v1"), version: z.literal("v1").default("v1"),
tasks: TaskMetadataWithFilePath.array(), tasks: TaskMetadataWithFilePath.array(),
}), }),
TASKS_FAILED_TO_PARSE: TaskMetadataFailedToParseData,
TASK_HEARTBEAT: z.object({ TASK_HEARTBEAT: z.object({
version: z.literal("v1").default("v1"), version: z.literal("v1").default("v1"),
id: z.string(), id: z.string(),
@@ -323,6 +332,9 @@ export const ProdChildToWorkerMessages = {
tasks: TaskMetadataWithFilePath.array(), tasks: TaskMetadataWithFilePath.array(),
}), }),
}, },
TASKS_FAILED_TO_PARSE: {
message: TaskMetadataFailedToParseData,
},
TASK_HEARTBEAT: { TASK_HEARTBEAT: {
message: z.object({ message: z.object({
version: z.literal("v1").default("v1"), version: z.literal("v1").default("v1"),
+2 -1
View File
@@ -9,6 +9,7 @@ import {
ZodSocketMessageCatalogSchema, ZodSocketMessageCatalogSchema,
} from "./zodSocket"; } from "./zodSocket";
import { z } from "zod"; import { z } from "zod";
import { ZodSchemaParsedError } from "./zodMessageHandler";
interface ZodIpcMessageSender<TEmitCatalog extends ZodSocketMessageCatalogSchema> { interface ZodIpcMessageSender<TEmitCatalog extends ZodSocketMessageCatalogSchema> {
send<K extends GetSocketMessagesWithoutCallback<TEmitCatalog>>( send<K extends GetSocketMessagesWithoutCallback<TEmitCatalog>>(
@@ -272,7 +273,7 @@ export class ZodIpcConnection<
const parsedPayload = schema.safeParse(payload); const parsedPayload = schema.safeParse(payload);
if (!parsedPayload.success) { if (!parsedPayload.success) {
throw new Error(`Failed to parse message payload: ${JSON.stringify(parsedPayload.error)}`); throw new ZodSchemaParsedError(parsedPayload.error, payload);
} }
await this.#sendPacket({ await this.#sendPacket({
+10 -1
View File
@@ -1,6 +1,15 @@
import { z } from "zod"; import { z } from "zod";
import { StructuredLogger } from "./zodNamespace"; import { StructuredLogger } from "./zodNamespace";
export class ZodSchemaParsedError extends Error {
constructor(
public error: z.ZodError,
public payload: unknown
) {
super(error.message);
}
}
export type ZodMessageValueSchema<TDiscriminatedUnion extends z.ZodDiscriminatedUnion<any, any>> = export type ZodMessageValueSchema<TDiscriminatedUnion extends z.ZodDiscriminatedUnion<any, any>> =
| z.ZodFirstPartySchemaTypes | z.ZodFirstPartySchemaTypes
| TDiscriminatedUnion; | TDiscriminatedUnion;
@@ -160,7 +169,7 @@ export class ZodMessageSender<TMessageCatalog extends ZodMessageCatalogSchema> {
const parsedPayload = schema.safeParse(payload); const parsedPayload = schema.safeParse(payload);
if (!parsedPayload.success) { if (!parsedPayload.success) {
throw new Error(`Failed to parse message payload: ${JSON.stringify(parsedPayload.error)}`); throw new ZodSchemaParsedError(parsedPayload.error, payload);
} }
await this.#sender({ type, payload, version: "v1" }); await this.#sender({ type, payload, version: "v1" });