From f59bad922762be41d77a6c3cad243c537bc26fd1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 21 Mar 2025 17:46:15 +0000 Subject: [PATCH 01/40] Revamping the lifecycle hooks, starting with init --- .../cli-v3/src/entryPoints/dev-run-worker.ts | 13 +++ packages/core/src/v3/config.ts | 2 +- packages/core/src/v3/index.ts | 1 + packages/core/src/v3/lifecycle-hooks-api.ts | 13 +++ .../core/src/v3/lifecycleHooks/adapters.ts | 16 ++++ packages/core/src/v3/lifecycleHooks/index.ts | 57 +++++++++++++ .../core/src/v3/lifecycleHooks/manager.ts | 73 +++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 31 +++++++ packages/core/src/v3/utils/globals.ts | 2 + packages/core/src/v3/workers/index.ts | 1 + packages/core/src/v3/workers/taskExecutor.ts | 80 ++++++++++++------- packages/trigger-sdk/src/v3/hooks.ts | 10 +++ packages/trigger-sdk/src/v3/shared.ts | 15 ++++ packages/trigger-sdk/src/v3/tasks.ts | 2 + references/hello-world/src/trigger/example.ts | 34 +++++++- 15 files changed, 318 insertions(+), 32 deletions(-) create mode 100644 packages/core/src/v3/lifecycle-hooks-api.ts create mode 100644 packages/core/src/v3/lifecycleHooks/adapters.ts create mode 100644 packages/core/src/v3/lifecycleHooks/index.ts create mode 100644 packages/core/src/v3/lifecycleHooks/manager.ts create mode 100644 packages/core/src/v3/lifecycleHooks/types.ts create mode 100644 packages/trigger-sdk/src/v3/hooks.ts diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 007c001a79..fe300b598b 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -18,6 +18,8 @@ import { WorkerManifest, WorkerToExecutorMessageCatalog, runTimelineMetrics, + lifecycleHooks, + lifecycleHooksAdapters, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -38,6 +40,7 @@ import { usage, UsageTimeoutManager, StandardRunTimelineMetricsManager, + StandardLifecycleHooksManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -93,6 +96,9 @@ const standardRunTimelineMetricsManager = new StandardRunTimelineMetricsManager( runTimelineMetrics.setGlobalManager(standardRunTimelineMetricsManager); standardRunTimelineMetricsManager.seedMetricsFromEnvironment(); +const standardLifecycleHooksManager = new StandardLifecycleHooksManager(); +lifecycleHooks.setGlobalLifecycleHooksManager(standardLifecycleHooksManager); + const devUsageManager = new DevUsageManager(); usage.setGlobalUsageManager(devUsageManager); timeout.setGlobalManager(new UsageTimeoutManager(devUsageManager)); @@ -170,6 +176,13 @@ async function bootstrap() { logger.setGlobalTaskLogger(otelTaskLogger); + if (config.init) { + lifecycleHooks.registerGlobalInitHook({ + id: "trigger-dev-worker", + fn: lifecycleHooksAdapters.createInitHookAdapter(config.init), + }); + } + return { tracer, tracingSDK, diff --git a/packages/core/src/v3/config.ts b/packages/core/src/v3/config.ts index 3862a24c90..20b49d5d17 100644 --- a/packages/core/src/v3/config.ts +++ b/packages/core/src/v3/config.ts @@ -216,7 +216,7 @@ export type TriggerConfig = { /** * Run before a task is executed, for all tasks. This is useful for setting up any global state that is needed for all tasks. */ - init?: (payload: unknown, params: InitFnParams) => void | Promise; + init?: (payload: unknown, params: InitFnParams) => any | Promise; /** * onSuccess is called after the run function has successfully completed. diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts index 098362a7dc..74ef493b17 100644 --- a/packages/core/src/v3/index.ts +++ b/packages/core/src/v3/index.ts @@ -15,6 +15,7 @@ export * from "./run-metadata-api.js"; export * from "./wait-until-api.js"; export * from "./timeout-api.js"; export * from "./run-timeline-metrics-api.js"; +export * from "./lifecycle-hooks-api.js"; export * from "./schemas/index.js"; export { SemanticInternalAttributes } from "./semanticInternalAttributes.js"; export * from "./resource-catalog-api.js"; diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts new file mode 100644 index 0000000000..6dc6ee41f2 --- /dev/null +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -0,0 +1,13 @@ +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { LifecycleHooksAPI } from "./lifecycleHooks/index.js"; +/** Entrypoint for runtime API */ +export const lifecycleHooks = LifecycleHooksAPI.getInstance(); + +export type { + OnInitHookFunction, + AnyOnInitHookFunction, + RegisteredHookFunction, +} from "./lifecycleHooks/types.js"; + +export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts new file mode 100644 index 0000000000..0ac3d1fa5b --- /dev/null +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -0,0 +1,16 @@ +import { TaskOptions } from "../types/index.js"; +import { AnyOnInitHookFunction } from "./types.js"; + +export function createInitHookAdapter( + fn: NonNullable["init"]> +): AnyOnInitHookFunction { + return async (params) => { + const paramsWithoutPayload = { + ...params, + }; + + delete paramsWithoutPayload["payload"]; + + return await fn(params.payload as unknown as TPayload, paramsWithoutPayload); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/index.ts b/packages/core/src/v3/lifecycleHooks/index.ts new file mode 100644 index 0000000000..385f8bd6bc --- /dev/null +++ b/packages/core/src/v3/lifecycleHooks/index.ts @@ -0,0 +1,57 @@ +const API_NAME = "lifecycle-hooks"; + +import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js"; +import { NoopLifecycleHooksManager } from "./manager.js"; +import { + AnyOnInitHookFunction, + RegisteredHookFunction, + RegisterHookFunctionParams, + type LifecycleHooksManager, +} from "./types.js"; + +const NOOP_LIFECYCLE_HOOKS_MANAGER = new NoopLifecycleHooksManager(); + +export class LifecycleHooksAPI { + private static _instance?: LifecycleHooksAPI; + + private constructor() {} + + public static getInstance(): LifecycleHooksAPI { + if (!this._instance) { + this._instance = new LifecycleHooksAPI(); + } + + return this._instance; + } + + public setGlobalLifecycleHooksManager(lifecycleHooksManager: LifecycleHooksManager): boolean { + return registerGlobal(API_NAME, lifecycleHooksManager); + } + + public disable() { + unregisterGlobal(API_NAME); + } + + public registerGlobalInitHook(hook: RegisterHookFunctionParams): void { + this.#getManager().registerGlobalInitHook(hook); + } + + public registerTaskInitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskInitHook(taskId, hook); + } + + public getTaskInitHook(taskId: string): AnyOnInitHookFunction | undefined { + return this.#getManager().getTaskInitHook(taskId); + } + + public getGlobalInitHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalInitHooks(); + } + + #getManager(): LifecycleHooksManager { + return getGlobal(API_NAME) ?? NOOP_LIFECYCLE_HOOKS_MANAGER; + } +} diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts new file mode 100644 index 0000000000..bfbc47b979 --- /dev/null +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -0,0 +1,73 @@ +import { + AnyOnInitHookFunction, + LifecycleHooksManager, + RegisteredHookFunction, + RegisterHookFunctionParams, +} from "./types.js"; + +export class StandardLifecycleHooksManager implements LifecycleHooksManager { + private initHooks: Map> = new Map(); + private taskInitHooks: Map = new Map(); + + registerGlobalInitHook(hook: RegisterHookFunctionParams): void { + // if there is no id, lets generate one based on the contents of the function + const id = generateHookId(hook); + + const registeredHook = { + id, + name: hook.id ?? hook.fn.name, + fn: hook.fn, + }; + + this.initHooks.set(id, registeredHook); + } + + registerTaskInitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const registeredHook = { + id: generateHookId(hook), + name: taskId, + fn: hook.fn, + }; + + this.initHooks.set(registeredHook.id, registeredHook); + this.taskInitHooks.set(taskId, registeredHook.id); + } + + getTaskInitHook(taskId: string): AnyOnInitHookFunction | undefined { + const hookId = this.taskInitHooks.get(taskId); + if (!hookId) return undefined; + return this.initHooks.get(hookId)?.fn; + } + + getGlobalInitHooks(): RegisteredHookFunction[] { + return Array.from(this.initHooks.values()); + } +} + +export class NoopLifecycleHooksManager implements LifecycleHooksManager { + registerGlobalInitHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskInitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskInitHook(taskId: string): AnyOnInitHookFunction | undefined { + return undefined; + } + + getGlobalInitHooks(): RegisteredHookFunction[] { + return []; + } +} + +function generateHookId(hook: RegisterHookFunctionParams): string { + return hook.id ?? hook.fn.name ?? hook.fn.toString(); +} diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts new file mode 100644 index 0000000000..f900432079 --- /dev/null +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -0,0 +1,31 @@ +import { TaskRunContext } from "../schemas/index.js"; + +export type OnInitHookFunction = (params: { + ctx: TaskRunContext; + payload: TPayload; + task: string; + signal?: AbortSignal; +}) => TInitOutput | undefined | void | Promise; + +export type AnyOnInitHookFunction = OnInitHookFunction; + +export type RegisterHookFunctionParams any> = { + id?: string; + fn: THookFunction; +}; + +export type RegisteredHookFunction any> = { + id: string; + name?: string; + fn: THookFunction; +}; + +export interface LifecycleHooksManager { + registerGlobalInitHook(hook: RegisterHookFunctionParams): void; + registerTaskInitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskInitHook(taskId: string): AnyOnInitHookFunction | undefined; + getGlobalInitHooks(): RegisteredHookFunction[]; +} diff --git a/packages/core/src/v3/utils/globals.ts b/packages/core/src/v3/utils/globals.ts index f3ec155751..37811b2ac2 100644 --- a/packages/core/src/v3/utils/globals.ts +++ b/packages/core/src/v3/utils/globals.ts @@ -1,5 +1,6 @@ import { ApiClientConfiguration } from "../apiClientManager/types.js"; import { Clock } from "../clock/clock.js"; +import { LifecycleHooksManager } from "../lifecycleHooks/types.js"; import { ResourceCatalog } from "../resource-catalog/catalog.js"; import { RunMetadataManager } from "../runMetadata/types.js"; import type { RuntimeManager } from "../runtime/manager.js"; @@ -62,4 +63,5 @@ type TriggerDotDevGlobalAPI = { ["timeout"]?: TimeoutManager; ["wait-until"]?: WaitUntilManager; ["run-timeline-metrics"]?: RunTimelineMetricsManager; + ["lifecycle-hooks"]?: LifecycleHooksManager; }; diff --git a/packages/core/src/v3/workers/index.ts b/packages/core/src/v3/workers/index.ts index 76cdb7110f..2bbcdc7798 100644 --- a/packages/core/src/v3/workers/index.ts +++ b/packages/core/src/v3/workers/index.ts @@ -20,3 +20,4 @@ export { ManagedRuntimeManager } from "../runtime/managedRuntimeManager.js"; export * from "../runEngineWorker/index.js"; export { StandardRunTimelineMetricsManager } from "../runTimelineMetrics/runTimelineMetricsManager.js"; export { WarmStartClient, type WarmStartClientOptions } from "../workers/warmStartClient.js"; +export { StandardLifecycleHooksManager } from "../lifecycleHooks/manager.js"; diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index a8339903ba..385bda86e5 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -3,7 +3,7 @@ import { VERSION } from "../../version.js"; import { ApiError, RateLimitError } from "../apiClient/errors.js"; import { ConsoleInterceptor } from "../consoleInterceptor.js"; import { isInternalError, parseError, sanitizeError, TaskPayloadParsedError } from "../errors.js"; -import { runMetadata, TriggerConfig, waitUntil } from "../index.js"; +import { lifecycleHooks, runMetadata, TriggerConfig, waitUntil } from "../index.js"; import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; import { @@ -278,44 +278,64 @@ export class TaskExecutor { } async #callInitFunctions(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { - await this.#callConfigInit(payload, ctx, signal); + const globalInitHooks = lifecycleHooks.getGlobalInitHooks(); + const taskInitHook = lifecycleHooks.getTaskInitHook(this.task.id); - const initFn = this.task.fns.init; - - if (!initFn) { + if (globalInitHooks.length === 0 && !taskInitHook) { return {}; } return this._tracer.startActiveSpan( - "init", + "hooks.init", async (span) => { - return await runTimelineMetrics.measureMetric("trigger.dev/execution", "init", () => - initFn(payload, { ctx, signal }) - ); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", - }, - } - ); - } + return await runTimelineMetrics.measureMetric("trigger.dev/execution", "init", async () => { + // Store global hook results in an array + const globalResults = []; + for (const hook of globalInitHooks) { + const result = await this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + return await hook.fn({ payload, ctx, signal, task: this.task.id }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "function", + }, + } + ); + // Only include object results + if (result && typeof result === "object" && !Array.isArray(result)) { + globalResults.push(result); + } + } - async #callConfigInit(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { - const initFn = this._importedConfig?.init; + // Merge all global results into a single object + const mergedGlobalResults = Object.assign({}, ...globalResults); - if (!initFn) { - return {}; - } + if (taskInitHook) { + const taskResult = await this._tracer.startActiveSpan( + "task", + async (span) => { + return await taskInitHook({ payload, ctx, signal, task: this.task.id }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "function", + }, + } + ); - return this._tracer.startActiveSpan( - "config.init", - async (span) => { - return await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "config.init", - async () => initFn(payload, { ctx, signal }) - ); + // Only merge if taskResult is an object + if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { + return { ...mergedGlobalResults, ...taskResult }; + } + + // If taskResult isn't an object, return global results + return mergedGlobalResults; + } + + return mergedGlobalResults; + }); }, { attributes: { diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts new file mode 100644 index 0000000000..3e099fa862 --- /dev/null +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -0,0 +1,10 @@ +import { lifecycleHooks, type AnyOnInitHookFunction } from "@trigger.dev/core/v3"; + +export function onInit(name: string, fn: AnyOnInitHookFunction): void; +export function onInit(fn: AnyOnInitHookFunction): void; +export function onInit(fnOrName: string | AnyOnInitHookFunction, fn?: AnyOnInitHookFunction): void { + lifecycleHooks.registerGlobalInitHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 7e7ad651cb..1316a16d0e 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -26,6 +26,8 @@ import { TaskFromIdentifier, flattenIdempotencyKey, getEnvVar, + lifecycleHooks, + lifecycleHooksAdapters, } from "@trigger.dev/core/v3"; import { PollOptions, runs } from "./runs.js"; import { tracer } from "./tracer.js"; @@ -77,6 +79,7 @@ import type { AnyTaskRunResult, BatchTriggerAndWaitOptions, BatchTriggerTaskV2RequestBody, + AnyOnInitHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -184,6 +187,12 @@ export function createTask< }, }; + if (params.init) { + lifecycleHooks.registerTaskInitHook(params.id, { + fn: lifecycleHooksAdapters.createInitHookAdapter(params.init), + }); + } + resourceCatalog.registerTaskMetadata({ id: params.id, description: params.description, @@ -316,6 +325,12 @@ export function createSchemaTask< }, }; + if (params.init) { + lifecycleHooks.registerTaskInitHook(params.id, { + fn: lifecycleHooksAdapters.createInitHookAdapter(params.init), + }); + } + resourceCatalog.registerTaskMetadata({ id: params.id, description: params.description, diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index c38f77187a..11b7e019fd 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,3 +1,4 @@ +import { onInit } from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -76,4 +77,5 @@ export const tasks = { batchTrigger, triggerAndWait, batchTriggerAndWait, + onInit, }; diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts index 81c45ec32c..6740eb2cc5 100644 --- a/references/hello-world/src/trigger/example.ts +++ b/references/hello-world/src/trigger/example.ts @@ -1,6 +1,38 @@ -import { batch, logger, task, timeout, wait } from "@trigger.dev/sdk"; +import { batch, logger, task, timeout, wait, tasks } from "@trigger.dev/sdk"; import { setTimeout } from "timers/promises"; +tasks.onInit("logging", ({ ctx, payload, task }) => { + logger.info("Hello, world from the init", { ctx, payload, task }); +}); + +// tasks.onSuccess(({ ctx, payload, output }) => { +// logger.info("Hello, world from the success", { ctx, payload }); +// }); + +// tasks.onComplete(({ ctx, payload, output, error }) => { +// logger.info("Hello, world from the success", { ctx, payload }); +// }); + +// tasks.handleError(({ ctx, payload, error, retry, retryAt, retryDelayInMs }) => { +// logger.info("Hello, world from the success", { ctx, payload }); +// }); + +// tasks.onFailure(({ ctx, payload }) => { +// logger.info("Hello, world from the failure", { ctx, payload }); +// }); + +// tasks.onStart(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); +// }); + +// tasks.onWait(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); +// }); + +// tasks.onResume(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); +// }); + export const helloWorldTask = task({ id: "hello-world", run: async (payload: any, { ctx }) => { From 0215a8cfa8fcb4ab05aac55a9d24a33202289f63 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 21 Mar 2025 20:35:02 +0000 Subject: [PATCH 02/40] vibes --- packages/cli-v3/src/build/bundle.ts | 5 +++++ packages/cli-v3/src/entryPoints/dev-run-worker.ts | 12 ++++++++++++ packages/core/src/v3/schemas/build.ts | 2 ++ 3 files changed, 19 insertions(+) diff --git a/packages/cli-v3/src/build/bundle.ts b/packages/cli-v3/src/build/bundle.ts index 0c2dfa5631..5b064ee9d2 100644 --- a/packages/cli-v3/src/build/bundle.ts +++ b/packages/cli-v3/src/build/bundle.ts @@ -47,6 +47,7 @@ export type BundleResult = { runControllerEntryPoint: string | undefined; indexWorkerEntryPoint: string | undefined; indexControllerEntryPoint: string | undefined; + initEntryPoint: string | undefined; stop: (() => Promise) | undefined; }; @@ -229,6 +230,7 @@ export async function getBundleResultFromBuild( let runControllerEntryPoint: string | undefined; let indexWorkerEntryPoint: string | undefined; let indexControllerEntryPoint: string | undefined; + let initEntryPoint: string | undefined; const configEntryPoint = resolvedConfig.configFile ? relative(resolvedConfig.workingDir, resolvedConfig.configFile) @@ -254,6 +256,8 @@ export async function getBundleResultFromBuild( indexControllerEntryPoint = $outputPath; } else if (isIndexWorkerForTarget(outputMeta.entryPoint, target)) { indexWorkerEntryPoint = $outputPath; + } else if (outputMeta.entryPoint.endsWith("init.ts")) { + initEntryPoint = $outputPath; } else { if ( !outputMeta.entryPoint.startsWith("..") && @@ -280,6 +284,7 @@ export async function getBundleResultFromBuild( runControllerEntryPoint, indexWorkerEntryPoint, indexControllerEntryPoint, + initEntryPoint, contentHash: hasher.digest("hex"), }; } diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index fe300b598b..d79f1ee1b1 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -270,6 +270,18 @@ const zodIpc = new ZodIpcConnection({ async () => { const beforeImport = performance.now(); resourceCatalog.setCurrentFileContext(taskManifest.entryPoint, taskManifest.filePath); + + // Load init file if it exists + if (workerManifest.initEntryPoint) { + try { + await import(normalizeImportPath(workerManifest.initEntryPoint)); + log(`Loaded init file from ${workerManifest.initEntryPoint}`); + } catch (err) { + logError(`Failed to load init file`, err); + throw err; + } + } + await import(normalizeImportPath(taskManifest.entryPoint)); resourceCatalog.clearCurrentFileContext(); const durationMs = performance.now() - beforeImport; diff --git a/packages/core/src/v3/schemas/build.ts b/packages/core/src/v3/schemas/build.ts index 0b122af2ed..c3df04eaa7 100644 --- a/packages/core/src/v3/schemas/build.ts +++ b/packages/core/src/v3/schemas/build.ts @@ -38,6 +38,7 @@ export const BuildManifest = z.object({ indexWorkerEntryPoint: z.string(), // Dev & Deploy has a indexWorkerEntryPoint indexControllerEntryPoint: z.string().optional(), // Only deploy has a indexControllerEntryPoint loaderEntryPoint: z.string().optional(), + initEntryPoint: z.string().optional(), // Optional init.ts entry point configPath: z.string(), externals: BuildExternal.array().optional(), build: z.object({ @@ -85,6 +86,7 @@ export const WorkerManifest = z.object({ workerEntryPoint: z.string(), controllerEntryPoint: z.string().optional(), loaderEntryPoint: z.string().optional(), + initEntryPoint: z.string().optional(), // Optional init.ts entry point runtime: BuildRuntime, customConditions: z.array(z.string()).optional(), otelImportHook: z From 2c44e59d078a372b5ee9704dd6048f9fac100b14 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 21 Mar 2025 20:47:01 +0000 Subject: [PATCH 03/40] init.ts at the root of the trigger dir is now automatically loaded --- packages/cli-v3/src/build/bundle.ts | 15 +++++++- .../src/entryPoints/dev-index-worker.ts | 1 + .../src/entryPoints/managed-index-worker.ts | 1 + references/hello-world/src/trigger/example.ts | 34 +------------------ references/hello-world/src/trigger/init.ts | 33 ++++++++++++++++++ 5 files changed, 50 insertions(+), 34 deletions(-) create mode 100644 references/hello-world/src/trigger/init.ts diff --git a/packages/cli-v3/src/build/bundle.ts b/packages/cli-v3/src/build/bundle.ts index 5b064ee9d2..b845d58de0 100644 --- a/packages/cli-v3/src/build/bundle.ts +++ b/packages/cli-v3/src/build/bundle.ts @@ -236,6 +236,18 @@ export async function getBundleResultFromBuild( ? relative(resolvedConfig.workingDir, resolvedConfig.configFile) : "trigger.config.ts"; + // Check if the entry point is an init.ts file at the root of a trigger directory + function isInitEntryPoint(entryPoint: string): boolean { + const normalizedEntryPoint = entryPoint.replace(/\\/g, "/"); // Normalize path separators + const initFileName = "init.ts"; + + // Check if it's directly in one of the trigger directories + return resolvedConfig.dirs.some((dir) => { + const normalizedDir = dir.replace(/\\/g, "/"); + return normalizedEntryPoint === `${normalizedDir}/${initFileName}`; + }); + } + for (const [outputPath, outputMeta] of Object.entries(result.metafile.outputs)) { if (outputPath.endsWith(".mjs")) { const $outputPath = resolve(workingDir, outputPath); @@ -256,7 +268,7 @@ export async function getBundleResultFromBuild( indexControllerEntryPoint = $outputPath; } else if (isIndexWorkerForTarget(outputMeta.entryPoint, target)) { indexWorkerEntryPoint = $outputPath; - } else if (outputMeta.entryPoint.endsWith("init.ts")) { + } else if (isInitEntryPoint(outputMeta.entryPoint)) { initEntryPoint = $outputPath; } else { if ( @@ -362,6 +374,7 @@ export async function createBuildManifestFromBundle({ runControllerEntryPoint: bundle.runControllerEntryPoint ?? getRunControllerForTarget(target), runWorkerEntryPoint: bundle.runWorkerEntryPoint ?? getRunWorkerForTarget(target), loaderEntryPoint: bundle.loaderEntryPoint, + initEntryPoint: bundle.initEntryPoint, configPath: bundle.configPath, customConditions: resolvedConfig.build.conditions ?? [], deploy: { diff --git a/packages/cli-v3/src/entryPoints/dev-index-worker.ts b/packages/cli-v3/src/entryPoints/dev-index-worker.ts index a29f2f8541..86528a93fe 100644 --- a/packages/cli-v3/src/entryPoints/dev-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-index-worker.ts @@ -141,6 +141,7 @@ await sendMessageInCatalog( controllerEntryPoint: buildManifest.runControllerEntryPoint, loaderEntryPoint: buildManifest.loaderEntryPoint, customConditions: buildManifest.customConditions, + initEntryPoint: buildManifest.initEntryPoint, }, importErrors, }, diff --git a/packages/cli-v3/src/entryPoints/managed-index-worker.ts b/packages/cli-v3/src/entryPoints/managed-index-worker.ts index a29f2f8541..86528a93fe 100644 --- a/packages/cli-v3/src/entryPoints/managed-index-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-index-worker.ts @@ -141,6 +141,7 @@ await sendMessageInCatalog( controllerEntryPoint: buildManifest.runControllerEntryPoint, loaderEntryPoint: buildManifest.loaderEntryPoint, customConditions: buildManifest.customConditions, + initEntryPoint: buildManifest.initEntryPoint, }, importErrors, }, diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts index 6740eb2cc5..81c45ec32c 100644 --- a/references/hello-world/src/trigger/example.ts +++ b/references/hello-world/src/trigger/example.ts @@ -1,38 +1,6 @@ -import { batch, logger, task, timeout, wait, tasks } from "@trigger.dev/sdk"; +import { batch, logger, task, timeout, wait } from "@trigger.dev/sdk"; import { setTimeout } from "timers/promises"; -tasks.onInit("logging", ({ ctx, payload, task }) => { - logger.info("Hello, world from the init", { ctx, payload, task }); -}); - -// tasks.onSuccess(({ ctx, payload, output }) => { -// logger.info("Hello, world from the success", { ctx, payload }); -// }); - -// tasks.onComplete(({ ctx, payload, output, error }) => { -// logger.info("Hello, world from the success", { ctx, payload }); -// }); - -// tasks.handleError(({ ctx, payload, error, retry, retryAt, retryDelayInMs }) => { -// logger.info("Hello, world from the success", { ctx, payload }); -// }); - -// tasks.onFailure(({ ctx, payload }) => { -// logger.info("Hello, world from the failure", { ctx, payload }); -// }); - -// tasks.onStart(({ ctx, payload }) => { -// logger.info("Hello, world from the start", { ctx, payload }); -// }); - -// tasks.onWait(({ ctx, payload }) => { -// logger.info("Hello, world from the start", { ctx, payload }); -// }); - -// tasks.onResume(({ ctx, payload }) => { -// logger.info("Hello, world from the start", { ctx, payload }); -// }); - export const helloWorldTask = task({ id: "hello-world", run: async (payload: any, { ctx }) => { diff --git a/references/hello-world/src/trigger/init.ts b/references/hello-world/src/trigger/init.ts new file mode 100644 index 0000000000..fffe5ae89d --- /dev/null +++ b/references/hello-world/src/trigger/init.ts @@ -0,0 +1,33 @@ +import { logger, tasks } from "@trigger.dev/sdk"; + +// tasks.onSuccess(({ ctx, payload, output }) => { +// logger.info("Hello, world from the success", { ctx, payload }); +// }); + +// tasks.onComplete(({ ctx, payload, output, error }) => { +// logger.info("Hello, world from the success", { ctx, payload }); +// }); + +// tasks.handleError(({ ctx, payload, error, retry, retryAt, retryDelayInMs }) => { +// logger.info("Hello, world from the success", { ctx, payload }); +// }); + +// tasks.onFailure(({ ctx, payload }) => { +// logger.info("Hello, world from the failure", { ctx, payload }); +// }); + +// tasks.onStart(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); +// }); + +// tasks.onWait(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); +// }); + +// tasks.onResume(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); +// }); + +tasks.onInit("logging", ({ ctx, payload, task }) => { + logger.info("Hello, world from the init", { ctx, payload, task }); +}); From 17e162310dc56555bed41f6860759b9a3c83e588 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 21 Mar 2025 21:30:55 +0000 Subject: [PATCH 04/40] Improve init lifecycle hook types and fix tabler icons on spans --- .../webapp/app/components/runs/v3/RunIcon.tsx | 5 + packages/core/src/v3/lifecycle-hooks-api.ts | 1 + .../core/src/v3/lifecycleHooks/manager.ts | 23 ++-- packages/core/src/v3/lifecycleHooks/types.ts | 10 +- .../core/src/v3/semanticInternalAttributes.ts | 1 + packages/core/src/v3/workers/taskExecutor.ts | 109 ++++++++++++------ packages/trigger-sdk/src/v3/hooks.ts | 9 +- packages/trigger-sdk/src/v3/tasks.ts | 2 + references/hello-world/src/trigger/example.ts | 11 +- references/hello-world/src/trigger/init.ts | 16 ++- 10 files changed, 131 insertions(+), 56 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx index a557e5cd35..1666212da8 100644 --- a/apps/webapp/app/components/runs/v3/RunIcon.tsx +++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx @@ -28,6 +28,8 @@ type SpanNameIcons = { const spanNameIcons: SpanNameIcons[] = [{ matcher: /^prisma:/, iconName: "brand-prisma" }]; export function RunIcon({ name, className, spanName }: TaskIconProps) { + console.log("spanName", spanName, name); + const spanNameIcon = spanNameIcons.find(({ matcher }) => matcher.test(spanName)); if (spanNameIcon) { @@ -44,6 +46,9 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) { } if (!name) return ; + if (tablerIcons.has(name)) { + return ; + } switch (name) { case "task": diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index 6dc6ee41f2..8790f6e5b6 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -8,6 +8,7 @@ export type { OnInitHookFunction, AnyOnInitHookFunction, RegisteredHookFunction, + TaskInitHookParams, } from "./lifecycleHooks/types.js"; export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index bfbc47b979..5cb759d9dc 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -6,8 +6,8 @@ import { } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { - private initHooks: Map> = new Map(); - private taskInitHooks: Map = new Map(); + private globalInitHooks: Map> = new Map(); + private taskInitHooks: Map> = new Map(); registerGlobalInitHook(hook: RegisterHookFunctionParams): void { // if there is no id, lets generate one based on the contents of the function @@ -15,11 +15,11 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { const registeredHook = { id, - name: hook.id ?? hook.fn.name, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, fn: hook.fn, }; - this.initHooks.set(id, registeredHook); + this.globalInitHooks.set(id, registeredHook); } registerTaskInitHook( @@ -32,18 +32,15 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { fn: hook.fn, }; - this.initHooks.set(registeredHook.id, registeredHook); - this.taskInitHooks.set(taskId, registeredHook.id); + this.taskInitHooks.set(taskId, registeredHook); } getTaskInitHook(taskId: string): AnyOnInitHookFunction | undefined { - const hookId = this.taskInitHooks.get(taskId); - if (!hookId) return undefined; - return this.initHooks.get(hookId)?.fn; + return this.taskInitHooks.get(taskId)?.fn; } getGlobalInitHooks(): RegisteredHookFunction[] { - return Array.from(this.initHooks.values()); + return Array.from(this.globalInitHooks.values()); } } @@ -69,5 +66,9 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { } function generateHookId(hook: RegisterHookFunctionParams): string { - return hook.id ?? hook.fn.name ?? hook.fn.toString(); + return hook.id ?? hook.fn.name + ? hook.fn.name === "" + ? hook.fn.toString() + : hook.fn.name + : hook.fn.toString(); } diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index f900432079..3db619f2f7 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -1,13 +1,17 @@ import { TaskRunContext } from "../schemas/index.js"; -export type OnInitHookFunction = (params: { +export type TaskInitHookParams = { ctx: TaskRunContext; payload: TPayload; task: string; signal?: AbortSignal; -}) => TInitOutput | undefined | void | Promise; +}; + +export type OnInitHookFunction> = ( + params: TaskInitHookParams +) => TInitOutput | undefined | void | Promise; -export type AnyOnInitHookFunction = OnInitHookFunction; +export type AnyOnInitHookFunction = OnInitHookFunction>; export type RegisterHookFunctionParams any> = { id?: string; diff --git a/packages/core/src/v3/semanticInternalAttributes.ts b/packages/core/src/v3/semanticInternalAttributes.ts index 63549bea25..4cfc03e8ec 100644 --- a/packages/core/src/v3/semanticInternalAttributes.ts +++ b/packages/core/src/v3/semanticInternalAttributes.ts @@ -33,6 +33,7 @@ export const SemanticInternalAttributes = { STYLE_ICON: "$style.icon", STYLE_VARIANT: "$style.variant", STYLE_ACCESSORY: "$style.accessory", + COLLAPSED: "$collapsed", METADATA: "$metadata", TRIGGER: "$trigger", PAYLOAD: "$payload", diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 385bda86e5..fed5a6154e 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -3,7 +3,13 @@ import { VERSION } from "../../version.js"; import { ApiError, RateLimitError } from "../apiClient/errors.js"; import { ConsoleInterceptor } from "../consoleInterceptor.js"; import { isInternalError, parseError, sanitizeError, TaskPayloadParsedError } from "../errors.js"; -import { lifecycleHooks, runMetadata, TriggerConfig, waitUntil } from "../index.js"; +import { + flattenAttributes, + lifecycleHooks, + runMetadata, + TriggerConfig, + waitUntil, +} from "../index.js"; import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; import { @@ -288,58 +294,87 @@ export class TaskExecutor { return this._tracer.startActiveSpan( "hooks.init", async (span) => { - return await runTimelineMetrics.measureMetric("trigger.dev/execution", "init", async () => { - // Store global hook results in an array - const globalResults = []; - for (const hook of globalInitHooks) { - const result = await this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - return await hook.fn({ payload, ctx, signal, task: this.task.id }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", + const result = await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "init", + async () => { + // Store global hook results in an array + const globalResults = []; + for (const hook of globalInitHooks) { + const result = await this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + const result = await hook.fn({ payload, ctx, signal, task: this.task.id }); + + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + + return result; + } + + return {}; }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + // Only include object results + if (result && typeof result === "object" && !Array.isArray(result)) { + globalResults.push(result); } - ); - // Only include object results - if (result && typeof result === "object" && !Array.isArray(result)) { - globalResults.push(result); } - } - // Merge all global results into a single object - const mergedGlobalResults = Object.assign({}, ...globalResults); + // Merge all global results into a single object + const mergedGlobalResults = Object.assign({}, ...globalResults); - if (taskInitHook) { - const taskResult = await this._tracer.startActiveSpan( - "task", - async (span) => { - return await taskInitHook({ payload, ctx, signal, task: this.task.id }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", + if (taskInitHook) { + const taskResult = await this._tracer.startActiveSpan( + "task", + async (span) => { + const result = await taskInitHook({ payload, ctx, signal, task: this.task.id }); + + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + + return result; + } + + return {}; }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + + // Only merge if taskResult is an object + if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { + return { ...mergedGlobalResults, ...taskResult }; } - ); - // Only merge if taskResult is an object - if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { - return { ...mergedGlobalResults, ...taskResult }; + // If taskResult isn't an object, return global results + return mergedGlobalResults; } - // If taskResult isn't an object, return global results return mergedGlobalResults; } + ); - return mergedGlobalResults; - }); + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + + return result; + } + + return; }, { attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + [SemanticInternalAttributes.COLLAPSED]: true, }, } ); diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 3e099fa862..5f487255ee 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -1,4 +1,11 @@ -import { lifecycleHooks, type AnyOnInitHookFunction } from "@trigger.dev/core/v3"; +import { + lifecycleHooks, + type AnyOnInitHookFunction, + type TaskInitHookParams, + type OnInitHookFunction, +} from "@trigger.dev/core/v3"; + +export type { AnyOnInitHookFunction, TaskInitHookParams, OnInitHookFunction }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; export function onInit(fn: AnyOnInitHookFunction): void; diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index 11b7e019fd..21b8d9f1b6 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -47,6 +47,8 @@ export type { TaskFromIdentifier, }; +export type * from "./hooks.js"; + /** Creates a task that can be triggered * @param options - Task options * @example diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts index 81c45ec32c..ea065218f0 100644 --- a/references/hello-world/src/trigger/example.ts +++ b/references/hello-world/src/trigger/example.ts @@ -3,8 +3,15 @@ import { setTimeout } from "timers/promises"; export const helloWorldTask = task({ id: "hello-world", - run: async (payload: any, { ctx }) => { - logger.debug("debug: Hello, world!", { payload }); + init: async (payload, { ctx }) => { + logger.info("Hello, world from the init", { ctx, payload }); + + return { + foobar: "baz", + }; + }, + run: async (payload: any, { ctx, init }) => { + logger.debug("debug: Hello, world!", { payload, init }); logger.info("info: Hello, world!", { payload }); logger.log("log: Hello, world!", { payload }); logger.warn("warn: Hello, world!", { payload }); diff --git a/references/hello-world/src/trigger/init.ts b/references/hello-world/src/trigger/init.ts index fffe5ae89d..a4900f4ce9 100644 --- a/references/hello-world/src/trigger/init.ts +++ b/references/hello-world/src/trigger/init.ts @@ -1,4 +1,4 @@ -import { logger, tasks } from "@trigger.dev/sdk"; +import { logger, type TaskInitHookParams, tasks } from "@trigger.dev/sdk"; // tasks.onSuccess(({ ctx, payload, output }) => { // logger.info("Hello, world from the success", { ctx, payload }); @@ -28,6 +28,18 @@ import { logger, tasks } from "@trigger.dev/sdk"; // logger.info("Hello, world from the start", { ctx, payload }); // }); -tasks.onInit("logging", ({ ctx, payload, task }) => { +tasks.onInit(({ ctx, payload, task }) => { logger.info("Hello, world from the init", { ctx, payload, task }); + + return { + foo: "bar", + }; +}); + +tasks.onInit(({ ctx, payload, task }) => { + logger.info("Hello, world from the init 2", { ctx, payload, task }); + + return { + bar: "baz", + }; }); From 1777ff4cfbb4ecdc8b9db101a518277ae26dc72b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Fri, 21 Mar 2025 21:59:47 +0000 Subject: [PATCH 05/40] move onStart to the new lifecycle hook system --- .../cli-v3/src/entryPoints/dev-run-worker.ts | 7 ++ packages/core/src/v3/lifecycle-hooks-api.ts | 3 + .../core/src/v3/lifecycleHooks/adapters.ts | 16 +++- packages/core/src/v3/lifecycleHooks/index.ts | 20 +++++ .../core/src/v3/lifecycleHooks/manager.ts | 54 ++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 20 +++++ packages/core/src/v3/workers/taskExecutor.ts | 88 ++++++++++--------- packages/trigger-sdk/src/v3/hooks.ts | 24 ++++- packages/trigger-sdk/src/v3/shared.ts | 12 +++ packages/trigger-sdk/src/v3/tasks.ts | 5 +- references/hello-world/src/trigger/init.ts | 12 +-- 11 files changed, 208 insertions(+), 53 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index d79f1ee1b1..3af04ab3c2 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -183,6 +183,13 @@ async function bootstrap() { }); } + if (config.onStart) { + lifecycleHooks.registerGlobalStartHook({ + id: "trigger-dev-worker", + fn: lifecycleHooksAdapters.createStartHookAdapter(config.onStart), + }); + } + return { tracer, tracingSDK, diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index 8790f6e5b6..33cc79f347 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -9,6 +9,9 @@ export type { AnyOnInitHookFunction, RegisteredHookFunction, TaskInitHookParams, + TaskStartHookParams, + OnStartHookFunction, + AnyOnStartHookFunction, } from "./lifecycleHooks/types.js"; export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index 0ac3d1fa5b..cf467ced54 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -1,5 +1,5 @@ import { TaskOptions } from "../types/index.js"; -import { AnyOnInitHookFunction } from "./types.js"; +import { AnyOnInitHookFunction, AnyOnStartHookFunction } from "./types.js"; export function createInitHookAdapter( fn: NonNullable["init"]> @@ -14,3 +14,17 @@ export function createInitHookAdapter( return await fn(params.payload as unknown as TPayload, paramsWithoutPayload); }; } + +export function createStartHookAdapter( + fn: NonNullable["onStart"]> +): AnyOnStartHookFunction { + return async (params) => { + const paramsWithoutPayload = { + ...params, + }; + + delete paramsWithoutPayload["payload"]; + + return await fn(params.payload as unknown as TPayload, paramsWithoutPayload); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/index.ts b/packages/core/src/v3/lifecycleHooks/index.ts index 385f8bd6bc..71fd67b123 100644 --- a/packages/core/src/v3/lifecycleHooks/index.ts +++ b/packages/core/src/v3/lifecycleHooks/index.ts @@ -4,6 +4,7 @@ import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js import { NoopLifecycleHooksManager } from "./manager.js"; import { AnyOnInitHookFunction, + AnyOnStartHookFunction, RegisteredHookFunction, RegisterHookFunctionParams, type LifecycleHooksManager, @@ -51,6 +52,25 @@ export class LifecycleHooksAPI { return this.#getManager().getGlobalInitHooks(); } + public registerGlobalStartHook(hook: RegisterHookFunctionParams): void { + this.#getManager().registerGlobalStartHook(hook); + } + + public getTaskStartHook(taskId: string): AnyOnStartHookFunction | undefined { + return this.#getManager().getTaskStartHook(taskId); + } + + public getGlobalStartHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalStartHooks(); + } + + public registerTaskStartHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskStartHook(taskId, hook); + } + #getManager(): LifecycleHooksManager { return getGlobal(API_NAME) ?? NOOP_LIFECYCLE_HOOKS_MANAGER; } diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 5cb759d9dc..65b60e494a 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -1,5 +1,6 @@ import { AnyOnInitHookFunction, + AnyOnStartHookFunction, LifecycleHooksManager, RegisteredHookFunction, RegisterHookFunctionParams, @@ -9,6 +10,40 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private globalInitHooks: Map> = new Map(); private taskInitHooks: Map> = new Map(); + private globalStartHooks: Map> = new Map(); + private taskStartHooks: Map> = new Map(); + + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalStartHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskStartHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskStartHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskStartHook(taskId: string): AnyOnStartHookFunction | undefined { + return this.taskStartHooks.get(taskId)?.fn; + } + + getGlobalStartHooks(): RegisteredHookFunction[] { + return Array.from(this.globalStartHooks.values()); + } + registerGlobalInitHook(hook: RegisterHookFunctionParams): void { // if there is no id, lets generate one based on the contents of the function const id = generateHookId(hook); @@ -63,6 +98,25 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalInitHooks(): RegisteredHookFunction[] { return []; } + + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskStartHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskStartHook(taskId: string): AnyOnStartHookFunction | undefined { + return undefined; + } + + getGlobalStartHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 3db619f2f7..157548c456 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -13,6 +13,19 @@ export type OnInitHookFunction>; +export type TaskStartHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + signal?: AbortSignal; +}; + +export type OnStartHookFunction = ( + params: TaskStartHookParams +) => undefined | void | Promise; + +export type AnyOnStartHookFunction = OnStartHookFunction; + export type RegisterHookFunctionParams any> = { id?: string; fn: THookFunction; @@ -32,4 +45,11 @@ export interface LifecycleHooksManager { ): void; getTaskInitHook(taskId: string): AnyOnInitHookFunction | undefined; getGlobalInitHooks(): RegisteredHookFunction[]; + registerGlobalStartHook(hook: RegisterHookFunctionParams): void; + registerTaskStartHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskStartHook(taskId: string): AnyOnStartHookFunction | undefined; + getGlobalStartHooks(): RegisteredHookFunction[]; } diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index fed5a6154e..799c9ec218 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -513,54 +513,56 @@ export class TaskExecutor { } async #callOnStartFunctions(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { - await this.#callOnStartFunction( - this._importedConfig?.onStart, - "config.onStart", - payload, - ctx, - {}, - signal - ); - - await this.#callOnStartFunction( - this.task.fns.onStart, - "task.onStart", - payload, - ctx, - {}, - signal - ); - } + const globalStartHooks = lifecycleHooks.getGlobalStartHooks(); + const taskStartHook = lifecycleHooks.getTaskStartHook(this.task.id); - async #callOnStartFunction( - onStartFn: TaskMetadataWithFunctions["fns"]["onStart"], - name: string, - payload: unknown, - ctx: TaskRunContext, - initOutput: any, - signal?: AbortSignal - ) { - if (!onStartFn) { + if (globalStartHooks.length === 0 && !taskStartHook) { return; } - try { - await this._tracer.startActiveSpan( - name, - async (span) => { - return await runTimelineMetrics.measureMetric("trigger.dev/execution", name, () => - onStartFn(payload, { ctx, signal }) - ); + return this._tracer.startActiveSpan( + "hooks.start", + async (span) => { + return await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "start", + async () => { + for (const hook of globalStartHooks) { + await this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ payload, ctx, signal, task: this.task.id }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } + + if (taskStartHook) { + await this._tracer.startActiveSpan( + "task", + async (span) => { + await taskStartHook({ payload, ctx, signal, task: this.task.id }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } + } + ); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", - }, - } - ); - } catch { - // Ignore errors from onStart functions - } + } + ); } async #callTaskCleanup( diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 5f487255ee..c1b93e59fb 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -3,9 +3,19 @@ import { type AnyOnInitHookFunction, type TaskInitHookParams, type OnInitHookFunction, + type AnyOnStartHookFunction, + type TaskStartHookParams, + type OnStartHookFunction, } from "@trigger.dev/core/v3"; -export type { AnyOnInitHookFunction, TaskInitHookParams, OnInitHookFunction }; +export type { + AnyOnInitHookFunction, + TaskInitHookParams, + OnInitHookFunction, + AnyOnStartHookFunction, + TaskStartHookParams, + OnStartHookFunction, +}; export function onInit(name: string, fn: AnyOnInitHookFunction): void; export function onInit(fn: AnyOnInitHookFunction): void; @@ -15,3 +25,15 @@ export function onInit(fnOrName: string | AnyOnInitHookFunction, fn?: AnyOnInitH fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function onStart(name: string, fn: AnyOnStartHookFunction): void; +export function onStart(fn: AnyOnStartHookFunction): void; +export function onStart( + fnOrName: string | AnyOnStartHookFunction, + fn?: AnyOnStartHookFunction +): void { + lifecycleHooks.registerGlobalStartHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 1316a16d0e..4f747da7e4 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -193,6 +193,12 @@ export function createTask< }); } + if (params.onStart) { + lifecycleHooks.registerTaskStartHook(params.id, { + fn: lifecycleHooksAdapters.createStartHookAdapter(params.onStart), + }); + } + resourceCatalog.registerTaskMetadata({ id: params.id, description: params.description, @@ -331,6 +337,12 @@ export function createSchemaTask< }); } + if (params.onStart) { + lifecycleHooks.registerTaskStartHook(params.id, { + fn: lifecycleHooksAdapters.createStartHookAdapter(params.onStart), + }); + } + resourceCatalog.registerTaskMetadata({ id: params.id, description: params.description, diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index 21b8d9f1b6..332720a84f 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,4 +1,4 @@ -import { onInit } from "./hooks.js"; +import { onInit, onStart } from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -79,5 +79,6 @@ export const tasks = { batchTrigger, triggerAndWait, batchTriggerAndWait, - onInit, + init: onInit, + onStart, }; diff --git a/references/hello-world/src/trigger/init.ts b/references/hello-world/src/trigger/init.ts index a4900f4ce9..11caa3d712 100644 --- a/references/hello-world/src/trigger/init.ts +++ b/references/hello-world/src/trigger/init.ts @@ -1,4 +1,4 @@ -import { logger, type TaskInitHookParams, tasks } from "@trigger.dev/sdk"; +import { logger, tasks } from "@trigger.dev/sdk"; // tasks.onSuccess(({ ctx, payload, output }) => { // logger.info("Hello, world from the success", { ctx, payload }); @@ -16,9 +16,9 @@ import { logger, type TaskInitHookParams, tasks } from "@trigger.dev/sdk"; // logger.info("Hello, world from the failure", { ctx, payload }); // }); -// tasks.onStart(({ ctx, payload }) => { -// logger.info("Hello, world from the start", { ctx, payload }); -// }); +tasks.onStart(({ ctx, payload }) => { + logger.info("Hello, world from the start", { ctx, payload }); +}); // tasks.onWait(({ ctx, payload }) => { // logger.info("Hello, world from the start", { ctx, payload }); @@ -28,7 +28,7 @@ import { logger, type TaskInitHookParams, tasks } from "@trigger.dev/sdk"; // logger.info("Hello, world from the start", { ctx, payload }); // }); -tasks.onInit(({ ctx, payload, task }) => { +tasks.init(({ ctx, payload, task }) => { logger.info("Hello, world from the init", { ctx, payload, task }); return { @@ -36,7 +36,7 @@ tasks.onInit(({ ctx, payload, task }) => { }; }); -tasks.onInit(({ ctx, payload, task }) => { +tasks.init(({ ctx, payload, task }) => { logger.info("Hello, world from the init 2", { ctx, payload, task }); return { From be676fdb336158e89975750d465ddc7d03e76c16 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 22 Mar 2025 08:05:59 +0000 Subject: [PATCH 06/40] onFailure --- .../core/src/v3/lifecycleHooks/adapters.ts | 21 ++++++- .../core/src/v3/lifecycleHooks/manager.ts | 56 +++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 21 +++++++ packages/trigger-sdk/src/v3/hooks.ts | 14 +++++ packages/trigger-sdk/src/v3/tasks.ts | 3 +- 5 files changed, 113 insertions(+), 2 deletions(-) diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index cf467ced54..17492cf0ce 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -1,5 +1,9 @@ import { TaskOptions } from "../types/index.js"; -import { AnyOnInitHookFunction, AnyOnStartHookFunction } from "./types.js"; +import { + AnyOnInitHookFunction, + AnyOnStartHookFunction, + AnyOnFailureHookFunction, +} from "./types.js"; export function createInitHookAdapter( fn: NonNullable["init"]> @@ -28,3 +32,18 @@ export function createStartHookAdapter( return await fn(params.payload as unknown as TPayload, paramsWithoutPayload); }; } + +export function createFailureHookAdapter( + fn: NonNullable["onFailure"]> +): AnyOnFailureHookFunction { + return async (params) => { + const paramsWithoutPayload = { + ...params, + }; + + delete paramsWithoutPayload["payload"]; + delete paramsWithoutPayload["error"]; + + return await fn(params.payload as unknown as TPayload, params.error, paramsWithoutPayload); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 65b60e494a..6e8d34355c 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -4,6 +4,7 @@ import { LifecycleHooksManager, RegisteredHookFunction, RegisterHookFunctionParams, + AnyOnFailureHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -13,6 +14,11 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private globalStartHooks: Map> = new Map(); private taskStartHooks: Map> = new Map(); + private globalFailureHooks: Map> = + new Map(); + private taskFailureHooks: Map> = + new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -77,6 +83,37 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalInitHooks(): RegisteredHookFunction[] { return Array.from(this.globalInitHooks.values()); } + + registerGlobalFailureHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalFailureHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskFailureHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskFailureHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskFailureHook(taskId: string): AnyOnFailureHookFunction | undefined { + return this.taskFailureHooks.get(taskId)?.fn; + } + + getGlobalFailureHooks(): RegisteredHookFunction[] { + return Array.from(this.globalFailureHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -117,6 +154,25 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalStartHooks(): RegisteredHookFunction[] { return []; } + + registerGlobalFailureHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskFailureHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskFailureHook(taskId: string): AnyOnFailureHookFunction | undefined { + return undefined; + } + + getGlobalFailureHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 157548c456..71f945870e 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -26,6 +26,20 @@ export type OnStartHookFunction = ( export type AnyOnStartHookFunction = OnStartHookFunction; +export type TaskFailureHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + error: unknown; + signal?: AbortSignal; +}; + +export type OnFailureHookFunction = ( + params: TaskFailureHookParams +) => undefined | void | Promise; + +export type AnyOnFailureHookFunction = OnFailureHookFunction; + export type RegisterHookFunctionParams any> = { id?: string; fn: THookFunction; @@ -52,4 +66,11 @@ export interface LifecycleHooksManager { ): void; getTaskStartHook(taskId: string): AnyOnStartHookFunction | undefined; getGlobalStartHooks(): RegisteredHookFunction[]; + registerGlobalFailureHook(hook: RegisterHookFunctionParams): void; + registerTaskFailureHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskFailureHook(taskId: string): AnyOnFailureHookFunction | undefined; + getGlobalFailureHooks(): RegisteredHookFunction[]; } diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index c1b93e59fb..75a03e1673 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -6,6 +6,7 @@ import { type AnyOnStartHookFunction, type TaskStartHookParams, type OnStartHookFunction, + type AnyOnFailureHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -15,6 +16,7 @@ export type { AnyOnStartHookFunction, TaskStartHookParams, OnStartHookFunction, + AnyOnFailureHookFunction, }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; @@ -37,3 +39,15 @@ export function onStart( fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function onFailure(name: string, fn: AnyOnFailureHookFunction): void; +export function onFailure(fn: AnyOnFailureHookFunction): void; +export function onFailure( + fnOrName: string | AnyOnFailureHookFunction, + fn?: AnyOnFailureHookFunction +): void { + lifecycleHooks.registerGlobalFailureHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index 332720a84f..44fc779992 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,4 +1,4 @@ -import { onInit, onStart } from "./hooks.js"; +import { onInit, onStart, onFailure } from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -81,4 +81,5 @@ export const tasks = { batchTriggerAndWait, init: onInit, onStart, + onFailure, }; From 06f6a04056eceb46b1b00423f392a5538cdf58bb Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 22 Mar 2025 08:08:31 +0000 Subject: [PATCH 07/40] onStart --- .../core/src/v3/lifecycleHooks/adapters.ts | 20 +++++++ .../core/src/v3/lifecycleHooks/manager.ts | 56 +++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 21 +++++++ packages/trigger-sdk/src/v3/hooks.ts | 14 +++++ packages/trigger-sdk/src/v3/tasks.ts | 3 +- 5 files changed, 113 insertions(+), 1 deletion(-) diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index 17492cf0ce..1a15d122c6 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -3,6 +3,7 @@ import { AnyOnInitHookFunction, AnyOnStartHookFunction, AnyOnFailureHookFunction, + AnyOnSuccessHookFunction, } from "./types.js"; export function createInitHookAdapter( @@ -47,3 +48,22 @@ export function createFailureHookAdapter( return await fn(params.payload as unknown as TPayload, params.error, paramsWithoutPayload); }; } + +export function createSuccessHookAdapter( + fn: NonNullable["onSuccess"]> +): AnyOnSuccessHookFunction { + return async (params) => { + const paramsWithoutPayload = { + ...params, + }; + + delete paramsWithoutPayload["payload"]; + delete paramsWithoutPayload["output"]; + + return await fn( + params.payload as unknown as TPayload, + params.output as unknown as TOutput, + paramsWithoutPayload + ); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 6e8d34355c..9993422909 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -5,6 +5,7 @@ import { RegisteredHookFunction, RegisterHookFunctionParams, AnyOnFailureHookFunction, + AnyOnSuccessHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -19,6 +20,11 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private taskFailureHooks: Map> = new Map(); + private globalSuccessHooks: Map> = + new Map(); + private taskSuccessHooks: Map> = + new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -114,6 +120,37 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalFailureHooks(): RegisteredHookFunction[] { return Array.from(this.globalFailureHooks.values()); } + + registerGlobalSuccessHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalSuccessHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskSuccessHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskSuccessHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskSuccessHook(taskId: string): AnyOnSuccessHookFunction | undefined { + return this.taskSuccessHooks.get(taskId)?.fn; + } + + getGlobalSuccessHooks(): RegisteredHookFunction[] { + return Array.from(this.globalSuccessHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -173,6 +210,25 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalFailureHooks(): RegisteredHookFunction[] { return []; } + + registerGlobalSuccessHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskSuccessHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskSuccessHook(taskId: string): AnyOnSuccessHookFunction | undefined { + return undefined; + } + + getGlobalSuccessHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 71f945870e..9812cde2be 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -40,6 +40,20 @@ export type OnFailureHookFunction = ( export type AnyOnFailureHookFunction = OnFailureHookFunction; +export type TaskSuccessHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + output: TOutput; + signal?: AbortSignal; +}; + +export type OnSuccessHookFunction = ( + params: TaskSuccessHookParams +) => undefined | void | Promise; + +export type AnyOnSuccessHookFunction = OnSuccessHookFunction; + export type RegisterHookFunctionParams any> = { id?: string; fn: THookFunction; @@ -73,4 +87,11 @@ export interface LifecycleHooksManager { ): void; getTaskFailureHook(taskId: string): AnyOnFailureHookFunction | undefined; getGlobalFailureHooks(): RegisteredHookFunction[]; + registerGlobalSuccessHook(hook: RegisterHookFunctionParams): void; + registerTaskSuccessHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskSuccessHook(taskId: string): AnyOnSuccessHookFunction | undefined; + getGlobalSuccessHooks(): RegisteredHookFunction[]; } diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 75a03e1673..c6f318c784 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -7,6 +7,7 @@ import { type TaskStartHookParams, type OnStartHookFunction, type AnyOnFailureHookFunction, + type AnyOnSuccessHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -17,6 +18,7 @@ export type { TaskStartHookParams, OnStartHookFunction, AnyOnFailureHookFunction, + AnyOnSuccessHookFunction, }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; @@ -51,3 +53,15 @@ export function onFailure( fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function onSuccess(name: string, fn: AnyOnSuccessHookFunction): void; +export function onSuccess(fn: AnyOnSuccessHookFunction): void; +export function onSuccess( + fnOrName: string | AnyOnSuccessHookFunction, + fn?: AnyOnSuccessHookFunction +): void { + lifecycleHooks.registerGlobalSuccessHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index 44fc779992..d337ac134a 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,4 +1,4 @@ -import { onInit, onStart, onFailure } from "./hooks.js"; +import { onInit, onStart, onFailure, onSuccess } from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -82,4 +82,5 @@ export const tasks = { init: onInit, onStart, onFailure, + onSuccess, }; From e816ba438257bc2f49675e46414bb034f99761ac Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 22 Mar 2025 08:12:17 +0000 Subject: [PATCH 08/40] onComplete --- .../core/src/v3/lifecycleHooks/manager.ts | 56 +++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 35 ++++++++++++ packages/trigger-sdk/src/v3/hooks.ts | 16 ++++++ packages/trigger-sdk/src/v3/tasks.ts | 3 +- 4 files changed, 109 insertions(+), 1 deletion(-) diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 9993422909..11dab91823 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -6,6 +6,7 @@ import { RegisterHookFunctionParams, AnyOnFailureHookFunction, AnyOnSuccessHookFunction, + AnyOnCompleteHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -25,6 +26,11 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private taskSuccessHooks: Map> = new Map(); + private globalCompleteHooks: Map> = + new Map(); + private taskCompleteHooks: Map> = + new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -151,6 +157,37 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalSuccessHooks(): RegisteredHookFunction[] { return Array.from(this.globalSuccessHooks.values()); } + + registerGlobalCompleteHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalCompleteHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskCompleteHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskCompleteHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskCompleteHook(taskId: string): AnyOnCompleteHookFunction | undefined { + return this.taskCompleteHooks.get(taskId)?.fn; + } + + getGlobalCompleteHooks(): RegisteredHookFunction[] { + return Array.from(this.globalCompleteHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -229,6 +266,25 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalSuccessHooks(): RegisteredHookFunction[] { return []; } + + registerGlobalCompleteHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskCompleteHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskCompleteHook(taskId: string): AnyOnCompleteHookFunction | undefined { + return undefined; + } + + getGlobalCompleteHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 9812cde2be..bcf7c36dbc 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -54,6 +54,34 @@ export type OnSuccessHookFunction = ( export type AnyOnSuccessHookFunction = OnSuccessHookFunction; +export type TaskCompleteSuccessResult = { + ok: true; + data: TOutput; +}; + +export type TaskCompleteErrorResult = { + ok: false; + error: unknown; +}; + +export type TaskCompleteResult = + | TaskCompleteSuccessResult + | TaskCompleteErrorResult; + +export type TaskCompleteHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + result: TaskCompleteResult; + signal?: AbortSignal; +}; + +export type OnCompleteHookFunction = ( + params: TaskCompleteHookParams +) => undefined | void | Promise; + +export type AnyOnCompleteHookFunction = OnCompleteHookFunction; + export type RegisterHookFunctionParams any> = { id?: string; fn: THookFunction; @@ -94,4 +122,11 @@ export interface LifecycleHooksManager { ): void; getTaskSuccessHook(taskId: string): AnyOnSuccessHookFunction | undefined; getGlobalSuccessHooks(): RegisteredHookFunction[]; + registerGlobalCompleteHook(hook: RegisterHookFunctionParams): void; + registerTaskCompleteHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskCompleteHook(taskId: string): AnyOnCompleteHookFunction | undefined; + getGlobalCompleteHooks(): RegisteredHookFunction[]; } diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index c6f318c784..2ff538089d 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -8,6 +8,8 @@ import { type OnStartHookFunction, type AnyOnFailureHookFunction, type AnyOnSuccessHookFunction, + type AnyOnCompleteHookFunction, + type TaskCompleteResult, } from "@trigger.dev/core/v3"; export type { @@ -19,6 +21,8 @@ export type { OnStartHookFunction, AnyOnFailureHookFunction, AnyOnSuccessHookFunction, + AnyOnCompleteHookFunction, + TaskCompleteResult, }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; @@ -65,3 +69,15 @@ export function onSuccess( fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function onComplete(name: string, fn: AnyOnCompleteHookFunction): void; +export function onComplete(fn: AnyOnCompleteHookFunction): void; +export function onComplete( + fnOrName: string | AnyOnCompleteHookFunction, + fn?: AnyOnCompleteHookFunction +): void { + lifecycleHooks.registerGlobalCompleteHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index d337ac134a..e0ebe10c70 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,4 +1,4 @@ -import { onInit, onStart, onFailure, onSuccess } from "./hooks.js"; +import { onInit, onStart, onFailure, onSuccess, onComplete } from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -83,4 +83,5 @@ export const tasks = { onStart, onFailure, onSuccess, + onComplete, }; From aaf2ed8a200f21e45f74763ff199eaef009f59bf Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 22 Mar 2025 15:47:59 +0000 Subject: [PATCH 09/40] onWait and onResume --- .../core/src/v3/lifecycleHooks/manager.ts | 109 ++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 40 +++++++ packages/trigger-sdk/src/v3/hooks.ts | 25 ++++ packages/trigger-sdk/src/v3/tasks.ts | 4 +- 4 files changed, 177 insertions(+), 1 deletion(-) diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 11dab91823..86b2f1abb0 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -7,6 +7,8 @@ import { AnyOnFailureHookFunction, AnyOnSuccessHookFunction, AnyOnCompleteHookFunction, + AnyOnWaitHookFunction, + AnyOnResumeHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -31,6 +33,13 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private taskCompleteHooks: Map> = new Map(); + private globalWaitHooks: Map> = new Map(); + private taskWaitHooks: Map> = new Map(); + + private globalResumeHooks: Map> = + new Map(); + private taskResumeHooks: Map> = new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -188,6 +197,68 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalCompleteHooks(): RegisteredHookFunction[] { return Array.from(this.globalCompleteHooks.values()); } + + registerGlobalWaitHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalWaitHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskWaitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskWaitHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskWaitHook(taskId: string): AnyOnWaitHookFunction | undefined { + return this.taskWaitHooks.get(taskId)?.fn; + } + + getGlobalWaitHooks(): RegisteredHookFunction[] { + return Array.from(this.globalWaitHooks.values()); + } + + registerGlobalResumeHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalResumeHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskResumeHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskResumeHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskResumeHook(taskId: string): AnyOnResumeHookFunction | undefined { + return this.taskResumeHooks.get(taskId)?.fn; + } + + getGlobalResumeHooks(): RegisteredHookFunction[] { + return Array.from(this.globalResumeHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -285,6 +356,44 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalCompleteHooks(): RegisteredHookFunction[] { return []; } + + registerGlobalWaitHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskWaitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskWaitHook(taskId: string): AnyOnWaitHookFunction | undefined { + return undefined; + } + + getGlobalWaitHooks(): RegisteredHookFunction[] { + return []; + } + + registerGlobalResumeHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskResumeHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskResumeHook(taskId: string): AnyOnResumeHookFunction | undefined { + return undefined; + } + + getGlobalResumeHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index bcf7c36dbc..831e390407 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -26,6 +26,32 @@ export type OnStartHookFunction = ( export type AnyOnStartHookFunction = OnStartHookFunction; +export type TaskWaitHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + signal?: AbortSignal; +}; + +export type OnWaitHookFunction = ( + params: TaskWaitHookParams +) => undefined | void | Promise; + +export type AnyOnWaitHookFunction = OnWaitHookFunction; + +export type TaskResumeHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + signal?: AbortSignal; +}; + +export type OnResumeHookFunction = ( + params: TaskResumeHookParams +) => undefined | void | Promise; + +export type AnyOnResumeHookFunction = OnResumeHookFunction; + export type TaskFailureHookParams = { ctx: TaskRunContext; payload: TPayload; @@ -129,4 +155,18 @@ export interface LifecycleHooksManager { ): void; getTaskCompleteHook(taskId: string): AnyOnCompleteHookFunction | undefined; getGlobalCompleteHooks(): RegisteredHookFunction[]; + registerGlobalWaitHook(hook: RegisterHookFunctionParams): void; + registerTaskWaitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskWaitHook(taskId: string): AnyOnWaitHookFunction | undefined; + getGlobalWaitHooks(): RegisteredHookFunction[]; + registerGlobalResumeHook(hook: RegisterHookFunctionParams): void; + registerTaskResumeHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskResumeHook(taskId: string): AnyOnResumeHookFunction | undefined; + getGlobalResumeHooks(): RegisteredHookFunction[]; } diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 2ff538089d..119566fed6 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -10,6 +10,8 @@ import { type AnyOnSuccessHookFunction, type AnyOnCompleteHookFunction, type TaskCompleteResult, + type AnyOnWaitHookFunction, + type AnyOnResumeHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -23,6 +25,8 @@ export type { AnyOnSuccessHookFunction, AnyOnCompleteHookFunction, TaskCompleteResult, + AnyOnWaitHookFunction, + AnyOnResumeHookFunction, }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; @@ -81,3 +85,24 @@ export function onComplete( fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function onWait(name: string, fn: AnyOnWaitHookFunction): void; +export function onWait(fn: AnyOnWaitHookFunction): void; +export function onWait(fnOrName: string | AnyOnWaitHookFunction, fn?: AnyOnWaitHookFunction): void { + lifecycleHooks.registerGlobalWaitHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} + +export function onResume(name: string, fn: AnyOnResumeHookFunction): void; +export function onResume(fn: AnyOnResumeHookFunction): void; +export function onResume( + fnOrName: string | AnyOnResumeHookFunction, + fn?: AnyOnResumeHookFunction +): void { + lifecycleHooks.registerGlobalResumeHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index e0ebe10c70..c9b1b4be45 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,4 +1,4 @@ -import { onInit, onStart, onFailure, onSuccess, onComplete } from "./hooks.js"; +import { onInit, onStart, onFailure, onSuccess, onComplete, onWait, onResume } from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -84,4 +84,6 @@ export const tasks = { onFailure, onSuccess, onComplete, + onWait, + onResume, }; From e77e8d48d6ea7f25a4c0a7ab1025a89c6b57faf1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 22 Mar 2025 15:50:59 +0000 Subject: [PATCH 10/40] handleError --- .../core/src/v3/lifecycleHooks/manager.ts | 62 +++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 26 ++++++++ packages/trigger-sdk/src/v3/hooks.ts | 14 +++++ packages/trigger-sdk/src/v3/tasks.ts | 1 + 4 files changed, 103 insertions(+) diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 86b2f1abb0..ac5ea8f80a 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -9,6 +9,7 @@ import { AnyOnCompleteHookFunction, AnyOnWaitHookFunction, AnyOnResumeHookFunction, + AnyOnHandleErrorHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -40,6 +41,13 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { new Map(); private taskResumeHooks: Map> = new Map(); + private globalHandleErrorHooks: Map< + string, + RegisteredHookFunction + > = new Map(); + private taskHandleErrorHooks: Map> = + new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -259,6 +267,39 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalResumeHooks(): RegisteredHookFunction[] { return Array.from(this.globalResumeHooks.values()); } + + registerGlobalHandleErrorHook( + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.globalHandleErrorHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskHandleErrorHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskHandleErrorHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskHandleErrorHook(taskId: string): AnyOnHandleErrorHookFunction | undefined { + return this.taskHandleErrorHooks.get(taskId)?.fn; + } + + getGlobalHandleErrorHooks(): RegisteredHookFunction[] { + return Array.from(this.globalHandleErrorHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -394,6 +435,27 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalResumeHooks(): RegisteredHookFunction[] { return []; } + + registerGlobalHandleErrorHook( + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + registerTaskHandleErrorHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskHandleErrorHook(taskId: string): AnyOnHandleErrorHookFunction | undefined { + return undefined; + } + + getGlobalHandleErrorHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 831e390407..3a8d8ce646 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -119,6 +119,23 @@ export type RegisteredHookFunction any> = fn: THookFunction; }; +export type TaskHandleErrorHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + error: unknown; + retry?: RetryOptions; + retryAt?: Date; + retryDelayInMs?: number; + signal?: AbortSignal; +}; + +export type OnHandleErrorHookFunction = ( + params: TaskHandleErrorHookParams +) => HandleErrorResult; + +export type AnyOnHandleErrorHookFunction = OnHandleErrorHookFunction; + export interface LifecycleHooksManager { registerGlobalInitHook(hook: RegisterHookFunctionParams): void; registerTaskInitHook( @@ -169,4 +186,13 @@ export interface LifecycleHooksManager { ): void; getTaskResumeHook(taskId: string): AnyOnResumeHookFunction | undefined; getGlobalResumeHooks(): RegisteredHookFunction[]; + registerGlobalHandleErrorHook( + hook: RegisterHookFunctionParams + ): void; + registerTaskHandleErrorHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskHandleErrorHook(taskId: string): AnyOnHandleErrorHookFunction | undefined; + getGlobalHandleErrorHooks(): RegisteredHookFunction[]; } diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 119566fed6..275192618d 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -12,6 +12,7 @@ import { type TaskCompleteResult, type AnyOnWaitHookFunction, type AnyOnResumeHookFunction, + type AnyOnHandleErrorHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -27,6 +28,7 @@ export type { TaskCompleteResult, AnyOnWaitHookFunction, AnyOnResumeHookFunction, + AnyOnHandleErrorHookFunction, }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; @@ -106,3 +108,15 @@ export function onResume( fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function onHandleError(name: string, fn: AnyOnHandleErrorHookFunction): void; +export function onHandleError(fn: AnyOnHandleErrorHookFunction): void; +export function onHandleError( + fnOrName: string | AnyOnHandleErrorHookFunction, + fn?: AnyOnHandleErrorHookFunction +): void { + lifecycleHooks.registerGlobalHandleErrorHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index c9b1b4be45..9d9ec1717e 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -86,4 +86,5 @@ export const tasks = { onComplete, onWait, onResume, + onHandleError, }; From 597b7baaaa6b752ce265df386c562ca148b51b77 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sat, 22 Mar 2025 15:51:22 +0000 Subject: [PATCH 11/40] adding imports --- packages/core/src/v3/lifecycleHooks/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 3a8d8ce646..faa1f65549 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -1,4 +1,5 @@ -import { TaskRunContext } from "../schemas/index.js"; +import { RetryOptions, TaskRunContext } from "../schemas/index.js"; +import { HandleErrorResult } from "../types/index.js"; export type TaskInitHookParams = { ctx: TaskRunContext; From e34e5200579cd94ab0b43d46b1304191a00f3fda Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Sun, 23 Mar 2025 07:12:01 +0000 Subject: [PATCH 12/40] more hooks --- packages/core/src/v3/lifecycle-hooks-api.ts | 13 ++ .../core/src/v3/lifecycleHooks/adapters.ts | 9 ++ packages/core/src/v3/lifecycleHooks/index.ts | 134 +++++++++++++++++- .../core/src/v3/lifecycleHooks/manager.ts | 43 +++--- packages/core/src/v3/lifecycleHooks/types.ts | 20 ++- packages/core/src/v3/types/tasks.ts | 17 +++ packages/trigger-sdk/src/v3/hooks.ts | 26 +++- packages/trigger-sdk/src/v3/shared.ts | 46 ++++++ packages/trigger-sdk/src/v3/tasks.ts | 16 ++- 9 files changed, 276 insertions(+), 48 deletions(-) diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index 33cc79f347..91cbde9230 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -12,6 +12,19 @@ export type { TaskStartHookParams, OnStartHookFunction, AnyOnStartHookFunction, + TaskFailureHookParams, + AnyOnFailureHookFunction, + TaskSuccessHookParams, + AnyOnSuccessHookFunction, + TaskCompleteHookParams, + AnyOnCompleteHookFunction, + TaskWaitHookParams, + AnyOnWaitHookFunction, + TaskResumeHookParams, + AnyOnResumeHookFunction, + TaskCatchErrorHookParams, + AnyOnCatchErrorHookFunction, + TaskCompleteResult, } from "./lifecycleHooks/types.js"; export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index 1a15d122c6..c786b3a4ed 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -4,6 +4,7 @@ import { AnyOnStartHookFunction, AnyOnFailureHookFunction, AnyOnSuccessHookFunction, + AnyOnCatchErrorHookFunction, } from "./types.js"; export function createInitHookAdapter( @@ -67,3 +68,11 @@ export function createSuccessHookAdapter( ); }; } + +export function createHandleErrorHookAdapter( + fn: NonNullable["handleError"]> +): AnyOnCatchErrorHookFunction { + return async (params) => { + return await fn(params.payload as unknown as TPayload, params.error, params); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/index.ts b/packages/core/src/v3/lifecycleHooks/index.ts index 71fd67b123..5b2ad1ac5b 100644 --- a/packages/core/src/v3/lifecycleHooks/index.ts +++ b/packages/core/src/v3/lifecycleHooks/index.ts @@ -3,8 +3,14 @@ const API_NAME = "lifecycle-hooks"; import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js"; import { NoopLifecycleHooksManager } from "./manager.js"; import { + AnyOnCatchErrorHookFunction, + AnyOnCompleteHookFunction, + AnyOnFailureHookFunction, AnyOnInitHookFunction, + AnyOnResumeHookFunction, AnyOnStartHookFunction, + AnyOnSuccessHookFunction, + AnyOnWaitHookFunction, RegisteredHookFunction, RegisterHookFunctionParams, type LifecycleHooksManager, @@ -52,6 +58,13 @@ export class LifecycleHooksAPI { return this.#getManager().getGlobalInitHooks(); } + public registerTaskStartHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskStartHook(taskId, hook); + } + public registerGlobalStartHook(hook: RegisterHookFunctionParams): void { this.#getManager().registerGlobalStartHook(hook); } @@ -64,11 +77,126 @@ export class LifecycleHooksAPI { return this.#getManager().getGlobalStartHooks(); } - public registerTaskStartHook( + public registerGlobalFailureHook( + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerGlobalFailureHook(hook); + } + + public registerTaskFailureHook( taskId: string, - hook: RegisterHookFunctionParams + hook: RegisterHookFunctionParams ): void { - this.#getManager().registerTaskStartHook(taskId, hook); + this.#getManager().registerTaskFailureHook(taskId, hook); + } + + public getTaskFailureHook(taskId: string): AnyOnFailureHookFunction | undefined { + return this.#getManager().getTaskFailureHook(taskId); + } + + public getGlobalFailureHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalFailureHooks(); + } + + public registerGlobalSuccessHook( + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerGlobalSuccessHook(hook); + } + + public registerTaskSuccessHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskSuccessHook(taskId, hook); + } + + public getTaskSuccessHook(taskId: string): AnyOnSuccessHookFunction | undefined { + return this.#getManager().getTaskSuccessHook(taskId); + } + + public getGlobalSuccessHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalSuccessHooks(); + } + + public registerGlobalCompleteHook( + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerGlobalCompleteHook(hook); + } + + public registerTaskCompleteHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskCompleteHook(taskId, hook); + } + + public getTaskCompleteHook(taskId: string): AnyOnCompleteHookFunction | undefined { + return this.#getManager().getTaskCompleteHook(taskId); + } + + public getGlobalCompleteHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalCompleteHooks(); + } + + public registerGlobalWaitHook(hook: RegisterHookFunctionParams): void { + this.#getManager().registerGlobalWaitHook(hook); + } + + public registerTaskWaitHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskWaitHook(taskId, hook); + } + + public getTaskWaitHook(taskId: string): AnyOnWaitHookFunction | undefined { + return this.#getManager().getTaskWaitHook(taskId); + } + + public getGlobalWaitHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalWaitHooks(); + } + + public registerGlobalResumeHook(hook: RegisterHookFunctionParams): void { + this.#getManager().registerGlobalResumeHook(hook); + } + + public registerTaskResumeHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskResumeHook(taskId, hook); + } + + public getTaskResumeHook(taskId: string): AnyOnResumeHookFunction | undefined { + return this.#getManager().getTaskResumeHook(taskId); + } + + public getGlobalResumeHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalResumeHooks(); + } + + public registerGlobalCatchErrorHook( + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerGlobalCatchErrorHook(hook); + } + + public registerTaskCatchErrorHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskCatchErrorHook(taskId, hook); + } + + public getTaskCatchErrorHook(taskId: string): AnyOnCatchErrorHookFunction | undefined { + return this.#getManager().getTaskCatchErrorHook(taskId); + } + + public getGlobalCatchErrorHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalCatchErrorHooks(); } #getManager(): LifecycleHooksManager { diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index ac5ea8f80a..3e11acc78d 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -9,7 +9,7 @@ import { AnyOnCompleteHookFunction, AnyOnWaitHookFunction, AnyOnResumeHookFunction, - AnyOnHandleErrorHookFunction, + AnyOnCatchErrorHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -41,11 +41,9 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { new Map(); private taskResumeHooks: Map> = new Map(); - private globalHandleErrorHooks: Map< - string, - RegisteredHookFunction - > = new Map(); - private taskHandleErrorHooks: Map> = + private globalCatchErrorHooks: Map> = + new Map(); + private taskCatchErrorHooks: Map> = new Map(); registerGlobalStartHook(hook: RegisterHookFunctionParams): void { @@ -268,37 +266,37 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { return Array.from(this.globalResumeHooks.values()); } - registerGlobalHandleErrorHook( - hook: RegisterHookFunctionParams + registerGlobalCatchErrorHook( + hook: RegisterHookFunctionParams ): void { const id = generateHookId(hook); - this.globalHandleErrorHooks.set(id, { + this.globalCatchErrorHooks.set(id, { id, name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, fn: hook.fn, }); } - registerTaskHandleErrorHook( + registerTaskCatchErrorHook( taskId: string, - hook: RegisterHookFunctionParams + hook: RegisterHookFunctionParams ): void { const id = generateHookId(hook); - this.taskHandleErrorHooks.set(taskId, { + this.taskCatchErrorHooks.set(taskId, { id, name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, fn: hook.fn, }); } - getTaskHandleErrorHook(taskId: string): AnyOnHandleErrorHookFunction | undefined { - return this.taskHandleErrorHooks.get(taskId)?.fn; + getTaskCatchErrorHook(taskId: string): AnyOnCatchErrorHookFunction | undefined { + return this.taskCatchErrorHooks.get(taskId)?.fn; } - getGlobalHandleErrorHooks(): RegisteredHookFunction[] { - return Array.from(this.globalHandleErrorHooks.values()); + getGlobalCatchErrorHooks(): RegisteredHookFunction[] { + return Array.from(this.globalCatchErrorHooks.values()); } } @@ -436,24 +434,19 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { return []; } - registerGlobalHandleErrorHook( - hook: RegisterHookFunctionParams - ): void { + registerGlobalCatchErrorHook(): void { // Noop } - registerTaskHandleErrorHook( - taskId: string, - hook: RegisterHookFunctionParams - ): void { + registerTaskCatchErrorHook(): void { // Noop } - getTaskHandleErrorHook(taskId: string): AnyOnHandleErrorHookFunction | undefined { + getTaskCatchErrorHook(): undefined { return undefined; } - getGlobalHandleErrorHooks(): RegisteredHookFunction[] { + getGlobalCatchErrorHooks(): [] { return []; } } diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index faa1f65549..71d46e1651 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -120,7 +120,7 @@ export type RegisteredHookFunction any> = fn: THookFunction; }; -export type TaskHandleErrorHookParams = { +export type TaskCatchErrorHookParams = { ctx: TaskRunContext; payload: TPayload; task: string; @@ -131,11 +131,11 @@ export type TaskHandleErrorHookParams = { signal?: AbortSignal; }; -export type OnHandleErrorHookFunction = ( - params: TaskHandleErrorHookParams +export type OnCatchErrorHookFunction = ( + params: TaskCatchErrorHookParams ) => HandleErrorResult; -export type AnyOnHandleErrorHookFunction = OnHandleErrorHookFunction; +export type AnyOnCatchErrorHookFunction = OnCatchErrorHookFunction; export interface LifecycleHooksManager { registerGlobalInitHook(hook: RegisterHookFunctionParams): void; @@ -187,13 +187,11 @@ export interface LifecycleHooksManager { ): void; getTaskResumeHook(taskId: string): AnyOnResumeHookFunction | undefined; getGlobalResumeHooks(): RegisteredHookFunction[]; - registerGlobalHandleErrorHook( - hook: RegisterHookFunctionParams - ): void; - registerTaskHandleErrorHook( + registerGlobalCatchErrorHook(hook: RegisterHookFunctionParams): void; + registerTaskCatchErrorHook( taskId: string, - hook: RegisterHookFunctionParams + hook: RegisterHookFunctionParams ): void; - getTaskHandleErrorHook(taskId: string): AnyOnHandleErrorHookFunction | undefined; - getGlobalHandleErrorHooks(): RegisteredHookFunction[]; + getTaskCatchErrorHook(taskId: string): AnyOnCatchErrorHookFunction | undefined; + getGlobalCatchErrorHooks(): RegisteredHookFunction[]; } diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index e8e54771c4..335582c080 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -14,6 +14,12 @@ import { AnySchemaParseFn, inferSchemaIn, inferSchemaOut, Schema } from "./schem import { Prettify } from "./utils.js"; import { inferToolParameters, ToolTaskParameters } from "./tools.js"; import { QueueOptions } from "./queues.js"; +import { + OnCatchErrorHookFunction, + OnCompleteHookFunction, + OnResumeHookFunction, + OnWaitHookFunction, +} from "../lifecycleHooks/types.js"; export type Queue = QueueOptions; export type TaskSchema = Schema; @@ -268,6 +274,8 @@ type CommonTaskOptions< /** * handleError is called when the run function throws an error. It can be used to modify the error or return new retry options. + * + * @deprecated Use catchError instead */ handleError?: ( payload: TPayload, @@ -275,6 +283,15 @@ type CommonTaskOptions< params: HandleErrorFnParams ) => HandleErrorResult; + /** + * catchError is called when the run function throws an error. It can be used to modify the error or return new retry options. + */ + catchError?: OnCatchErrorHookFunction; + + onResume?: OnResumeHookFunction; + onWait?: OnWaitHookFunction; + onComplete?: OnCompleteHookFunction; + /** * middleware allows you to run code "around" the run function. This can be useful for logging, metrics, or other cross-cutting concerns. * diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 275192618d..08534f8a7a 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -12,7 +12,7 @@ import { type TaskCompleteResult, type AnyOnWaitHookFunction, type AnyOnResumeHookFunction, - type AnyOnHandleErrorHookFunction, + type AnyOnCatchErrorHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -28,7 +28,7 @@ export type { TaskCompleteResult, AnyOnWaitHookFunction, AnyOnResumeHookFunction, - AnyOnHandleErrorHookFunction, + AnyOnCatchErrorHookFunction, }; export function onInit(name: string, fn: AnyOnInitHookFunction): void; @@ -109,13 +109,25 @@ export function onResume( }); } -export function onHandleError(name: string, fn: AnyOnHandleErrorHookFunction): void; -export function onHandleError(fn: AnyOnHandleErrorHookFunction): void; +/** @deprecated Use onCatchError instead */ +export function onHandleError(name: string, fn: AnyOnCatchErrorHookFunction): void; +/** @deprecated Use onCatchError instead */ +export function onHandleError(fn: AnyOnCatchErrorHookFunction): void; +/** @deprecated Use onCatchError instead */ export function onHandleError( - fnOrName: string | AnyOnHandleErrorHookFunction, - fn?: AnyOnHandleErrorHookFunction + fnOrName: string | AnyOnCatchErrorHookFunction, + fn?: AnyOnCatchErrorHookFunction ): void { - lifecycleHooks.registerGlobalHandleErrorHook({ + onCatchError(fnOrName as any, fn as any); +} + +export function onCatchError(name: string, fn: AnyOnCatchErrorHookFunction): void; +export function onCatchError(fn: AnyOnCatchErrorHookFunction): void; +export function onCatchError( + fnOrName: string | AnyOnCatchErrorHookFunction, + fn?: AnyOnCatchErrorHookFunction +): void { + lifecycleHooks.registerGlobalCatchErrorHook({ id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, fn: typeof fnOrName === "function" ? fnOrName : fn!, }); diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 4f747da7e4..d2f7bfa384 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -80,6 +80,10 @@ import type { BatchTriggerAndWaitOptions, BatchTriggerTaskV2RequestBody, AnyOnInitHookFunction, + AnyOnCatchErrorHookFunction, + AnyOnCompleteHookFunction, + AnyOnWaitHookFunction, + AnyOnResumeHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -343,6 +347,48 @@ export function createSchemaTask< }); } + if (params.onFailure) { + lifecycleHooks.registerTaskFailureHook(params.id, { + fn: lifecycleHooksAdapters.createFailureHookAdapter(params.onFailure), + }); + } + + if (params.onSuccess) { + lifecycleHooks.registerTaskSuccessHook(params.id, { + fn: lifecycleHooksAdapters.createSuccessHookAdapter(params.onSuccess), + }); + } + + if (params.onComplete) { + lifecycleHooks.registerTaskCompleteHook(params.id, { + fn: params.onComplete as AnyOnCompleteHookFunction, + }); + } + + if (params.onWait) { + lifecycleHooks.registerTaskWaitHook(params.id, { + fn: params.onWait as AnyOnWaitHookFunction, + }); + } + + if (params.onResume) { + lifecycleHooks.registerTaskResumeHook(params.id, { + fn: params.onResume as AnyOnResumeHookFunction, + }); + } + + if (params.catchError) { + lifecycleHooks.registerTaskCatchErrorHook(params.id, { + fn: params.catchError as AnyOnCatchErrorHookFunction, + }); + } + + if (params.handleError) { + lifecycleHooks.registerTaskCatchErrorHook(params.id, { + fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(params.handleError), + }); + } + resourceCatalog.registerTaskMetadata({ id: params.id, description: params.description, diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index 9d9ec1717e..cd05a3dffe 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,4 +1,14 @@ -import { onInit, onStart, onFailure, onSuccess, onComplete, onWait, onResume } from "./hooks.js"; +import { + onInit, + onStart, + onFailure, + onSuccess, + onComplete, + onWait, + onResume, + onHandleError, + onCatchError, +} from "./hooks.js"; import { batchTrigger, batchTriggerAndWait, @@ -86,5 +96,7 @@ export const tasks = { onComplete, onWait, onResume, - onHandleError, + /** @deprecated Use catchError instead */ + handleError: onHandleError, + catchError: onCatchError, }; From dc6e659e94ec0f1bcb2163fc82de3d52e782cbf4 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 15:55:00 +0000 Subject: [PATCH 13/40] new locals API --- .../cli-v3/src/entryPoints/dev-run-worker.ts | 5 +++ .../src/entryPoints/managed-run-worker.ts | 5 +++ packages/core/src/v3/index.ts | 1 + packages/core/src/v3/locals-api.ts | 28 ++++++++++++ packages/core/src/v3/locals/index.ts | 45 +++++++++++++++++++ packages/core/src/v3/locals/manager.ts | 37 +++++++++++++++ packages/core/src/v3/locals/types.ts | 14 ++++++ packages/core/src/v3/types/tasks.ts | 4 ++ packages/core/src/v3/utils/globals.ts | 2 + packages/core/src/v3/workers/index.ts | 1 + packages/trigger-sdk/src/v3/hooks.ts | 15 ------- packages/trigger-sdk/src/v3/index.ts | 1 + packages/trigger-sdk/src/v3/locals.ts | 5 +++ packages/trigger-sdk/src/v3/tasks.ts | 2 - references/hello-world/src/db.ts | 23 ++++++++++ references/hello-world/src/trigger/example.ts | 9 +++- references/hello-world/src/trigger/init.ts | 29 +++++------- 17 files changed, 188 insertions(+), 38 deletions(-) create mode 100644 packages/core/src/v3/locals-api.ts create mode 100644 packages/core/src/v3/locals/index.ts create mode 100644 packages/core/src/v3/locals/manager.ts create mode 100644 packages/core/src/v3/locals/types.ts create mode 100644 packages/trigger-sdk/src/v3/locals.ts create mode 100644 references/hello-world/src/db.ts diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 3af04ab3c2..5952e86512 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -20,6 +20,7 @@ import { runTimelineMetrics, lifecycleHooks, lifecycleHooksAdapters, + localsAPI, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -41,6 +42,7 @@ import { UsageTimeoutManager, StandardRunTimelineMetricsManager, StandardLifecycleHooksManager, + StandardLocalsManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -92,6 +94,9 @@ process.on("uncaughtException", function (error, origin) { const heartbeatIntervalMs = getEnvVar("HEARTBEAT_INTERVAL_MS"); +const standardLocalsManager = new StandardLocalsManager(); +localsAPI.setGlobalLocalsManager(standardLocalsManager); + const standardRunTimelineMetricsManager = new StandardRunTimelineMetricsManager(); runTimelineMetrics.setGlobalManager(standardRunTimelineMetricsManager); standardRunTimelineMetricsManager.seedMetricsFromEnvironment(); diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index f261a9e677..913789e0d1 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -18,6 +18,7 @@ import { waitUntil, apiClientManager, runTimelineMetrics, + localsAPI, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -39,6 +40,7 @@ import { StandardWaitUntilManager, ManagedRuntimeManager, StandardRunTimelineMetricsManager, + StandardLocalsManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -93,6 +95,9 @@ const usageEventUrl = getEnvVar("USAGE_EVENT_URL"); const triggerJWT = getEnvVar("TRIGGER_JWT"); const heartbeatIntervalMs = getEnvVar("HEARTBEAT_INTERVAL_MS"); +const standardLocalsManager = new StandardLocalsManager(); +localsAPI.setGlobalLocalsManager(standardLocalsManager); + const standardRunTimelineMetricsManager = new StandardRunTimelineMetricsManager(); runTimelineMetrics.setGlobalManager(standardRunTimelineMetricsManager); standardRunTimelineMetricsManager.seedMetricsFromEnvironment(); diff --git a/packages/core/src/v3/index.ts b/packages/core/src/v3/index.ts index 74ef493b17..7706842c14 100644 --- a/packages/core/src/v3/index.ts +++ b/packages/core/src/v3/index.ts @@ -16,6 +16,7 @@ export * from "./wait-until-api.js"; export * from "./timeout-api.js"; export * from "./run-timeline-metrics-api.js"; export * from "./lifecycle-hooks-api.js"; +export * from "./locals-api.js"; export * from "./schemas/index.js"; export { SemanticInternalAttributes } from "./semanticInternalAttributes.js"; export * from "./resource-catalog-api.js"; diff --git a/packages/core/src/v3/locals-api.ts b/packages/core/src/v3/locals-api.ts new file mode 100644 index 0000000000..d3c40967ee --- /dev/null +++ b/packages/core/src/v3/locals-api.ts @@ -0,0 +1,28 @@ +// Split module-level variable definition into separate files to allow +// tree-shaking on each api instance. +import { LocalsAPI } from "./locals/index.js"; +import type { LocalsKey } from "./locals/types.js"; +/** Entrypoint for runtime API */ +export const localsAPI = LocalsAPI.getInstance(); + +export const locals = { + create(id: string): LocalsKey { + return localsAPI.createLocal(id); + }, + get(key: LocalsKey): T | undefined { + return localsAPI.getLocal(key); + }, + getOrThrow(key: LocalsKey): T { + const value = localsAPI.getLocal(key); + if (!value) { + throw new Error(`Local with id ${key.id} not found`); + } + return value; + }, + set(key: LocalsKey, value: T): void { + localsAPI.setLocal(key, value); + }, +}; + +export type Locals = typeof locals; +export type { LocalsKey }; diff --git a/packages/core/src/v3/locals/index.ts b/packages/core/src/v3/locals/index.ts new file mode 100644 index 0000000000..def8602384 --- /dev/null +++ b/packages/core/src/v3/locals/index.ts @@ -0,0 +1,45 @@ +const API_NAME = "locals"; + +import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js"; +import { NoopLocalsManager } from "./manager.js"; +import { LocalsKey, type LocalsManager } from "./types.js"; + +const NOOP_LOCALS_MANAGER = new NoopLocalsManager(); + +export class LocalsAPI implements LocalsManager { + private static _instance?: LocalsAPI; + + private constructor() {} + + public static getInstance(): LocalsAPI { + if (!this._instance) { + this._instance = new LocalsAPI(); + } + + return this._instance; + } + + public setGlobalLocalsManager(localsManager: LocalsManager): boolean { + return registerGlobal(API_NAME, localsManager); + } + + public disable() { + unregisterGlobal(API_NAME); + } + + public createLocal(id: string): LocalsKey { + return this.#getManager().createLocal(id); + } + + public getLocal(key: LocalsKey): T | undefined { + return this.#getManager().getLocal(key); + } + + public setLocal(key: LocalsKey, value: T): void { + return this.#getManager().setLocal(key, value); + } + + #getManager(): LocalsManager { + return getGlobal(API_NAME) ?? NOOP_LOCALS_MANAGER; + } +} diff --git a/packages/core/src/v3/locals/manager.ts b/packages/core/src/v3/locals/manager.ts new file mode 100644 index 0000000000..befe219260 --- /dev/null +++ b/packages/core/src/v3/locals/manager.ts @@ -0,0 +1,37 @@ +import { LocalsKey, LocalsManager } from "./types.js"; + +export class NoopLocalsManager implements LocalsManager { + createLocal(id: string): LocalsKey { + return { + __type: Symbol(), + id, + } as unknown as LocalsKey; + } + + getLocal(key: LocalsKey): T | undefined { + return undefined; + } + + setLocal(key: LocalsKey, value: T): void {} +} + +export class StandardLocalsManager implements LocalsManager { + private store: Map = new Map(); + + createLocal(id: string): LocalsKey { + const key = Symbol.for(id); + return { + __type: key, + id, + } as unknown as LocalsKey; + } + + getLocal(key: LocalsKey): T | undefined { + return this.store.get(key.__type) as T | undefined; + } + + setLocal(key: LocalsKey, value: T): void { + this.store.set(key.__type, value); + } +} +0; diff --git a/packages/core/src/v3/locals/types.ts b/packages/core/src/v3/locals/types.ts new file mode 100644 index 0000000000..aab683df09 --- /dev/null +++ b/packages/core/src/v3/locals/types.ts @@ -0,0 +1,14 @@ +declare const __local: unique symbol; +type BrandLocal = { [__local]: T }; + +// Create a type-safe store for your locals +export type LocalsKey = BrandLocal & { + readonly id: string; + readonly __type: unique symbol; +}; + +export interface LocalsManager { + createLocal(id: string): LocalsKey; + getLocal(key: LocalsKey): T | undefined; + setLocal(key: LocalsKey, value: T): void; +} diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index 335582c080..fa32eef406 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -264,11 +264,15 @@ type CommonTaskOptions< /** * init is called before the run function is called. It's useful for setting up any global state. + * + * @deprecated Use locals and middleware instead */ init?: (payload: TPayload, params: InitFnParams) => Promise; /** * cleanup is called after the run function has completed. + * + * @deprecated Use middleware instead */ cleanup?: (payload: TPayload, params: RunFnParams) => Promise; diff --git a/packages/core/src/v3/utils/globals.ts b/packages/core/src/v3/utils/globals.ts index 37811b2ac2..e59539b343 100644 --- a/packages/core/src/v3/utils/globals.ts +++ b/packages/core/src/v3/utils/globals.ts @@ -1,6 +1,7 @@ import { ApiClientConfiguration } from "../apiClientManager/types.js"; import { Clock } from "../clock/clock.js"; import { LifecycleHooksManager } from "../lifecycleHooks/types.js"; +import { LocalsManager } from "../locals/types.js"; import { ResourceCatalog } from "../resource-catalog/catalog.js"; import { RunMetadataManager } from "../runMetadata/types.js"; import type { RuntimeManager } from "../runtime/manager.js"; @@ -64,4 +65,5 @@ type TriggerDotDevGlobalAPI = { ["wait-until"]?: WaitUntilManager; ["run-timeline-metrics"]?: RunTimelineMetricsManager; ["lifecycle-hooks"]?: LifecycleHooksManager; + ["locals"]?: LocalsManager; }; diff --git a/packages/core/src/v3/workers/index.ts b/packages/core/src/v3/workers/index.ts index 2bbcdc7798..7d67a23836 100644 --- a/packages/core/src/v3/workers/index.ts +++ b/packages/core/src/v3/workers/index.ts @@ -21,3 +21,4 @@ export * from "../runEngineWorker/index.js"; export { StandardRunTimelineMetricsManager } from "../runTimelineMetrics/runTimelineMetricsManager.js"; export { WarmStartClient, type WarmStartClientOptions } from "../workers/warmStartClient.js"; export { StandardLifecycleHooksManager } from "../lifecycleHooks/manager.js"; +export { StandardLocalsManager } from "../locals/manager.js"; diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index 08534f8a7a..b4deb339ce 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -1,8 +1,5 @@ import { lifecycleHooks, - type AnyOnInitHookFunction, - type TaskInitHookParams, - type OnInitHookFunction, type AnyOnStartHookFunction, type TaskStartHookParams, type OnStartHookFunction, @@ -16,9 +13,6 @@ import { } from "@trigger.dev/core/v3"; export type { - AnyOnInitHookFunction, - TaskInitHookParams, - OnInitHookFunction, AnyOnStartHookFunction, TaskStartHookParams, OnStartHookFunction, @@ -31,15 +25,6 @@ export type { AnyOnCatchErrorHookFunction, }; -export function onInit(name: string, fn: AnyOnInitHookFunction): void; -export function onInit(fn: AnyOnInitHookFunction): void; -export function onInit(fnOrName: string | AnyOnInitHookFunction, fn?: AnyOnInitHookFunction): void { - lifecycleHooks.registerGlobalInitHook({ - id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, - fn: typeof fnOrName === "function" ? fnOrName : fn!, - }); -} - export function onStart(name: string, fn: AnyOnStartHookFunction): void; export function onStart(fn: AnyOnStartHookFunction): void; export function onStart( diff --git a/packages/trigger-sdk/src/v3/index.ts b/packages/trigger-sdk/src/v3/index.ts index 5f00a4a3e1..4837525734 100644 --- a/packages/trigger-sdk/src/v3/index.ts +++ b/packages/trigger-sdk/src/v3/index.ts @@ -12,6 +12,7 @@ export * from "./tags.js"; export * from "./metadata.js"; export * from "./timeout.js"; export * from "./webhooks.js"; +export * from "./locals.js"; export type { Context }; import type { Context } from "./shared.js"; diff --git a/packages/trigger-sdk/src/v3/locals.ts b/packages/trigger-sdk/src/v3/locals.ts new file mode 100644 index 0000000000..1e6082719b --- /dev/null +++ b/packages/trigger-sdk/src/v3/locals.ts @@ -0,0 +1,5 @@ +import { type Locals, locals, type LocalsKey } from "@trigger.dev/core/v3"; + +export type { Locals, LocalsKey }; + +export { locals }; diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index cd05a3dffe..3a414d239f 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -1,5 +1,4 @@ import { - onInit, onStart, onFailure, onSuccess, @@ -89,7 +88,6 @@ export const tasks = { batchTrigger, triggerAndWait, batchTriggerAndWait, - init: onInit, onStart, onFailure, onSuccess, diff --git a/references/hello-world/src/db.ts b/references/hello-world/src/db.ts new file mode 100644 index 0000000000..43feefacad --- /dev/null +++ b/references/hello-world/src/db.ts @@ -0,0 +1,23 @@ +import { locals } from "@trigger.dev/sdk"; +import { logger, tasks } from "@trigger.dev/sdk"; + +const DbLocal = locals.create<{ connect: () => Promise }>("db"); + +export function getDb() { + return locals.getOrThrow(DbLocal); +} + +export function setDb(db: { connect: () => Promise }) { + locals.set(DbLocal, db); +} + +// tasks.middleware("db", ({ ctx, payload, next, task }) => { +// locals.set(DbLocal, { +// connect: async () => { +// logger.info("Connecting to the database"); +// }, +// }); + +// logger.info("Hello, world from the middleware", { ctx, payload }); +// return next(); +// }); diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts index ea065218f0..b30e1a2018 100644 --- a/references/hello-world/src/trigger/example.ts +++ b/references/hello-world/src/trigger/example.ts @@ -1,16 +1,21 @@ import { batch, logger, task, timeout, wait } from "@trigger.dev/sdk"; import { setTimeout } from "timers/promises"; +import { getDb } from "../db.js"; export const helloWorldTask = task({ id: "hello-world", init: async (payload, { ctx }) => { - logger.info("Hello, world from the init", { ctx, payload }); - return { foobar: "baz", }; }, run: async (payload: any, { ctx, init }) => { + logger.info("Hello, world from the init", { ctx, payload }); + + const db = getDb(); + + await db.connect(); + logger.debug("debug: Hello, world!", { payload, init }); logger.info("info: Hello, world!", { payload }); logger.log("log: Hello, world!", { payload }); diff --git a/references/hello-world/src/trigger/init.ts b/references/hello-world/src/trigger/init.ts index 11caa3d712..5570f92b4f 100644 --- a/references/hello-world/src/trigger/init.ts +++ b/references/hello-world/src/trigger/init.ts @@ -1,4 +1,5 @@ import { logger, tasks } from "@trigger.dev/sdk"; +// import { setDb } from "../db.js"; // tasks.onSuccess(({ ctx, payload, output }) => { // logger.info("Hello, world from the success", { ctx, payload }); @@ -16,9 +17,15 @@ import { logger, tasks } from "@trigger.dev/sdk"; // logger.info("Hello, world from the failure", { ctx, payload }); // }); -tasks.onStart(({ ctx, payload }) => { - logger.info("Hello, world from the start", { ctx, payload }); -}); +// tasks.onStart(({ ctx, payload }) => { +// logger.info("Hello, world from the start", { ctx, payload }); + +// setDb({ +// connect: async () => { +// logger.info("Connecting to the database"); +// }, +// }); +// }); // tasks.onWait(({ ctx, payload }) => { // logger.info("Hello, world from the start", { ctx, payload }); @@ -27,19 +34,3 @@ tasks.onStart(({ ctx, payload }) => { // tasks.onResume(({ ctx, payload }) => { // logger.info("Hello, world from the start", { ctx, payload }); // }); - -tasks.init(({ ctx, payload, task }) => { - logger.info("Hello, world from the init", { ctx, payload, task }); - - return { - foo: "bar", - }; -}); - -tasks.init(({ ctx, payload, task }) => { - logger.info("Hello, world from the init 2", { ctx, payload, task }); - - return { - bar: "baz", - }; -}); From 6001dfa7de512922421758973bc324b3e39dba2f Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 16:01:19 +0000 Subject: [PATCH 14/40] Add middleware types --- packages/core/src/v3/lifecycle-hooks-api.ts | 3 + .../core/src/v3/lifecycleHooks/adapters.ts | 14 +++++ .../core/src/v3/lifecycleHooks/manager.ts | 55 +++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 21 +++++++ 4 files changed, 93 insertions(+) diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index 91cbde9230..b69382ae71 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -25,6 +25,9 @@ export type { TaskCatchErrorHookParams, AnyOnCatchErrorHookFunction, TaskCompleteResult, + TaskMiddlewareHookParams, + AnyOnMiddlewareHookFunction, + OnMiddlewareHookFunction, } from "./lifecycleHooks/types.js"; export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index c786b3a4ed..f7000f2e1b 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -5,6 +5,7 @@ import { AnyOnFailureHookFunction, AnyOnSuccessHookFunction, AnyOnCatchErrorHookFunction, + AnyOnMiddlewareHookFunction, } from "./types.js"; export function createInitHookAdapter( @@ -76,3 +77,16 @@ export function createHandleErrorHookAdapter( return await fn(params.payload as unknown as TPayload, params.error, params); }; } + +export function createMiddlewareHookAdapter( + fn: NonNullable["middleware"]> +): AnyOnMiddlewareHookFunction { + return async (params) => { + const { payload, next, ...paramsWithoutPayloadAndNext } = params; + + return await fn(payload as unknown as TPayload, { + ...paramsWithoutPayloadAndNext, + next, + }); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 3e11acc78d..3cd40d2a86 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -10,6 +10,7 @@ import { AnyOnWaitHookFunction, AnyOnResumeHookFunction, AnyOnCatchErrorHookFunction, + AnyOnMiddlewareHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -46,6 +47,11 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private taskCatchErrorHooks: Map> = new Map(); + private globalMiddlewareHooks: Map> = + new Map(); + private taskMiddlewareHooks: Map> = + new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -298,6 +304,39 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalCatchErrorHooks(): RegisteredHookFunction[] { return Array.from(this.globalCatchErrorHooks.values()); } + + registerGlobalMiddlewareHook( + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.globalMiddlewareHooks.set(id, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + registerTaskMiddlewareHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskMiddlewareHooks.set(taskId, { + id, + name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + fn: hook.fn, + }); + } + + getTaskMiddlewareHook(taskId: string): AnyOnMiddlewareHookFunction | undefined { + return this.taskMiddlewareHooks.get(taskId)?.fn; + } + + getGlobalMiddlewareHooks(): RegisteredHookFunction[] { + return Array.from(this.globalMiddlewareHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -449,6 +488,22 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalCatchErrorHooks(): [] { return []; } + + registerGlobalMiddlewareHook(): void { + // Noop + } + + registerTaskMiddlewareHook(): void { + // Noop + } + + getTaskMiddlewareHook(): undefined { + return undefined; + } + + getGlobalMiddlewareHooks(): [] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 71d46e1651..746dd1f8b0 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -137,6 +137,20 @@ export type OnCatchErrorHookFunction = ( export type AnyOnCatchErrorHookFunction = OnCatchErrorHookFunction; +export type TaskMiddlewareHookParams = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + signal?: AbortSignal; + next: () => Promise; +}; + +export type OnMiddlewareHookFunction = ( + params: TaskMiddlewareHookParams +) => Promise; + +export type AnyOnMiddlewareHookFunction = OnMiddlewareHookFunction; + export interface LifecycleHooksManager { registerGlobalInitHook(hook: RegisterHookFunctionParams): void; registerTaskInitHook( @@ -194,4 +208,11 @@ export interface LifecycleHooksManager { ): void; getTaskCatchErrorHook(taskId: string): AnyOnCatchErrorHookFunction | undefined; getGlobalCatchErrorHooks(): RegisteredHookFunction[]; + registerGlobalMiddlewareHook(hook: RegisterHookFunctionParams): void; + registerTaskMiddlewareHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskMiddlewareHook(taskId: string): AnyOnMiddlewareHookFunction | undefined; + getGlobalMiddlewareHooks(): RegisteredHookFunction[]; } From 2d16d664ef14a9ac88d2bf33fbcc6e0f84ecd4ce Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 16:59:54 +0000 Subject: [PATCH 15/40] Add middleware hooks --- packages/core/src/v3/lifecycleHooks/index.ts | 22 ++++++++++++++++++++ packages/trigger-sdk/src/v3/hooks.ts | 14 +++++++++++++ packages/trigger-sdk/src/v3/shared.ts | 20 ++++++------------ packages/trigger-sdk/src/v3/tasks.ts | 2 ++ references/hello-world/src/trigger/init.ts | 5 +++++ 5 files changed, 49 insertions(+), 14 deletions(-) diff --git a/packages/core/src/v3/lifecycleHooks/index.ts b/packages/core/src/v3/lifecycleHooks/index.ts index 5b2ad1ac5b..721322f639 100644 --- a/packages/core/src/v3/lifecycleHooks/index.ts +++ b/packages/core/src/v3/lifecycleHooks/index.ts @@ -7,6 +7,7 @@ import { AnyOnCompleteHookFunction, AnyOnFailureHookFunction, AnyOnInitHookFunction, + AnyOnMiddlewareHookFunction, AnyOnResumeHookFunction, AnyOnStartHookFunction, AnyOnSuccessHookFunction, @@ -199,6 +200,27 @@ export class LifecycleHooksAPI { return this.#getManager().getGlobalCatchErrorHooks(); } + public registerGlobalMiddlewareHook( + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerGlobalMiddlewareHook(hook); + } + + public registerTaskMiddlewareHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskMiddlewareHook(taskId, hook); + } + + public getTaskMiddlewareHook(taskId: string): AnyOnMiddlewareHookFunction | undefined { + return this.#getManager().getTaskMiddlewareHook(taskId); + } + + public getGlobalMiddlewareHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalMiddlewareHooks(); + } + #getManager(): LifecycleHooksManager { return getGlobal(API_NAME) ?? NOOP_LIFECYCLE_HOOKS_MANAGER; } diff --git a/packages/trigger-sdk/src/v3/hooks.ts b/packages/trigger-sdk/src/v3/hooks.ts index b4deb339ce..d864f4ec8e 100644 --- a/packages/trigger-sdk/src/v3/hooks.ts +++ b/packages/trigger-sdk/src/v3/hooks.ts @@ -10,6 +10,7 @@ import { type AnyOnWaitHookFunction, type AnyOnResumeHookFunction, type AnyOnCatchErrorHookFunction, + type AnyOnMiddlewareHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -23,6 +24,7 @@ export type { AnyOnWaitHookFunction, AnyOnResumeHookFunction, AnyOnCatchErrorHookFunction, + AnyOnMiddlewareHookFunction, }; export function onStart(name: string, fn: AnyOnStartHookFunction): void; @@ -117,3 +119,15 @@ export function onCatchError( fn: typeof fnOrName === "function" ? fnOrName : fn!, }); } + +export function middleware(name: string, fn: AnyOnMiddlewareHookFunction): void; +export function middleware(fn: AnyOnMiddlewareHookFunction): void; +export function middleware( + fnOrName: string | AnyOnMiddlewareHookFunction, + fn?: AnyOnMiddlewareHookFunction +): void { + lifecycleHooks.registerGlobalMiddlewareHook({ + id: typeof fnOrName === "string" ? fnOrName : fnOrName.name ? fnOrName.name : undefined, + fn: typeof fnOrName === "function" ? fnOrName : fn!, + }); +} diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index d2f7bfa384..50bd6f3e23 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -212,13 +212,6 @@ export function createTask< maxDuration: params.maxDuration, fns: { run: params.run, - init: params.init, - cleanup: params.cleanup, - middleware: params.middleware, - handleError: params.handleError, - onSuccess: params.onSuccess, - onFailure: params.onFailure, - onStart: params.onStart, }, }); @@ -389,6 +382,12 @@ export function createSchemaTask< }); } + if (params.middleware) { + lifecycleHooks.registerTaskMiddlewareHook(params.id, { + fn: lifecycleHooksAdapters.createMiddlewareHookAdapter(params.middleware), + }); + } + resourceCatalog.registerTaskMetadata({ id: params.id, description: params.description, @@ -398,13 +397,6 @@ export function createSchemaTask< maxDuration: params.maxDuration, fns: { run: params.run, - init: params.init, - cleanup: params.cleanup, - middleware: params.middleware, - handleError: params.handleError, - onSuccess: params.onSuccess, - onFailure: params.onFailure, - onStart: params.onStart, parsePayload, }, }); diff --git a/packages/trigger-sdk/src/v3/tasks.ts b/packages/trigger-sdk/src/v3/tasks.ts index 3a414d239f..a6089d090e 100644 --- a/packages/trigger-sdk/src/v3/tasks.ts +++ b/packages/trigger-sdk/src/v3/tasks.ts @@ -7,6 +7,7 @@ import { onResume, onHandleError, onCatchError, + middleware, } from "./hooks.js"; import { batchTrigger, @@ -97,4 +98,5 @@ export const tasks = { /** @deprecated Use catchError instead */ handleError: onHandleError, catchError: onCatchError, + middleware, }; diff --git a/references/hello-world/src/trigger/init.ts b/references/hello-world/src/trigger/init.ts index 5570f92b4f..e57bc89838 100644 --- a/references/hello-world/src/trigger/init.ts +++ b/references/hello-world/src/trigger/init.ts @@ -1,6 +1,11 @@ import { logger, tasks } from "@trigger.dev/sdk"; // import { setDb } from "../db.js"; +tasks.middleware("db", ({ ctx, payload, next }) => { + logger.info("Hello, world from the middleware", { ctx, payload }); + return next(); +}); + // tasks.onSuccess(({ ctx, payload, output }) => { // logger.info("Hello, world from the success", { ctx, payload }); // }); From 2e6d0d87837d92fbd1d43f06f571270c2f76c95c Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 17:04:08 +0000 Subject: [PATCH 16/40] share the hook registration code --- packages/trigger-sdk/src/v3/shared.ts | 140 +++++++++++++------------- 1 file changed, 70 insertions(+), 70 deletions(-) diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 50bd6f3e23..68a909fe21 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -191,17 +191,7 @@ export function createTask< }, }; - if (params.init) { - lifecycleHooks.registerTaskInitHook(params.id, { - fn: lifecycleHooksAdapters.createInitHookAdapter(params.init), - }); - } - - if (params.onStart) { - lifecycleHooks.registerTaskStartHook(params.id, { - fn: lifecycleHooksAdapters.createStartHookAdapter(params.onStart), - }); - } + registerTaskLifecycleHooks(params.id, params); resourceCatalog.registerTaskMetadata({ id: params.id, @@ -328,65 +318,7 @@ export function createSchemaTask< }, }; - if (params.init) { - lifecycleHooks.registerTaskInitHook(params.id, { - fn: lifecycleHooksAdapters.createInitHookAdapter(params.init), - }); - } - - if (params.onStart) { - lifecycleHooks.registerTaskStartHook(params.id, { - fn: lifecycleHooksAdapters.createStartHookAdapter(params.onStart), - }); - } - - if (params.onFailure) { - lifecycleHooks.registerTaskFailureHook(params.id, { - fn: lifecycleHooksAdapters.createFailureHookAdapter(params.onFailure), - }); - } - - if (params.onSuccess) { - lifecycleHooks.registerTaskSuccessHook(params.id, { - fn: lifecycleHooksAdapters.createSuccessHookAdapter(params.onSuccess), - }); - } - - if (params.onComplete) { - lifecycleHooks.registerTaskCompleteHook(params.id, { - fn: params.onComplete as AnyOnCompleteHookFunction, - }); - } - - if (params.onWait) { - lifecycleHooks.registerTaskWaitHook(params.id, { - fn: params.onWait as AnyOnWaitHookFunction, - }); - } - - if (params.onResume) { - lifecycleHooks.registerTaskResumeHook(params.id, { - fn: params.onResume as AnyOnResumeHookFunction, - }); - } - - if (params.catchError) { - lifecycleHooks.registerTaskCatchErrorHook(params.id, { - fn: params.catchError as AnyOnCatchErrorHookFunction, - }); - } - - if (params.handleError) { - lifecycleHooks.registerTaskCatchErrorHook(params.id, { - fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(params.handleError), - }); - } - - if (params.middleware) { - lifecycleHooks.registerTaskMiddlewareHook(params.id, { - fn: lifecycleHooksAdapters.createMiddlewareHookAdapter(params.middleware), - }); - } + registerTaskLifecycleHooks(params.id, params); resourceCatalog.registerTaskMetadata({ id: params.id, @@ -1624,3 +1556,71 @@ async function handleTaskRunExecutionResult(taskId: TIdentifier, params: TaskOptions) { + if (params.init) { + lifecycleHooks.registerTaskInitHook(taskId, { + fn: lifecycleHooksAdapters.createInitHookAdapter(params.init), + }); + } + + if (params.onStart) { + lifecycleHooks.registerTaskStartHook(taskId, { + fn: lifecycleHooksAdapters.createStartHookAdapter(params.onStart), + }); + } + + if (params.onFailure) { + lifecycleHooks.registerTaskFailureHook(taskId, { + fn: lifecycleHooksAdapters.createFailureHookAdapter(params.onFailure), + }); + } + + if (params.onSuccess) { + lifecycleHooks.registerTaskSuccessHook(taskId, { + fn: lifecycleHooksAdapters.createSuccessHookAdapter(params.onSuccess), + }); + } + + if (params.onComplete) { + lifecycleHooks.registerTaskCompleteHook(taskId, { + fn: params.onComplete as AnyOnCompleteHookFunction, + }); + } + + if (params.onWait) { + lifecycleHooks.registerTaskWaitHook(taskId, { + fn: params.onWait as AnyOnWaitHookFunction, + }); + } + + if (params.onResume) { + lifecycleHooks.registerTaskResumeHook(taskId, { + fn: params.onResume as AnyOnResumeHookFunction, + }); + } + + if (params.catchError) { + // We don't need to use an adapter here because catchError is the new version of handleError + lifecycleHooks.registerTaskCatchErrorHook(taskId, { + fn: params.catchError as AnyOnCatchErrorHookFunction, + }); + } + + if (params.handleError) { + lifecycleHooks.registerTaskCatchErrorHook(taskId, { + fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(params.handleError), + }); + } + + if (params.middleware) { + lifecycleHooks.registerTaskMiddlewareHook(taskId, { + fn: lifecycleHooksAdapters.createMiddlewareHookAdapter(params.middleware), + }); + } +} From 116788912b13392ee35e2c9d58f88013551cd22c Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 17:42:24 +0000 Subject: [PATCH 17/40] use new onStart --- .../core/src/v3/lifecycleHooks/adapters.ts | 69 ++++++------ packages/core/src/v3/lifecycleHooks/types.ts | 103 +++++++++++------ packages/core/src/v3/workers/taskExecutor.ts | 105 ++++++++++-------- packages/trigger-sdk/src/v3/shared.ts | 1 + 4 files changed, 169 insertions(+), 109 deletions(-) diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index f7000f2e1b..a90cabee35 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -6,6 +6,11 @@ import { AnyOnSuccessHookFunction, AnyOnCatchErrorHookFunction, AnyOnMiddlewareHookFunction, + TaskInitOutput, + TaskSuccessHookParams, + TaskFailureHookParams, + TaskStartHookParams, + TaskCatchErrorHookParams, } from "./types.js"; export function createInitHookAdapter( @@ -22,59 +27,59 @@ export function createInitHookAdapter( }; } -export function createStartHookAdapter( - fn: NonNullable["onStart"]> +export function createStartHookAdapter< + TPayload, + TInitOutput extends TaskInitOutput = TaskInitOutput, +>( + fn: NonNullable["onStart"]> ): AnyOnStartHookFunction { return async (params) => { - const paramsWithoutPayload = { - ...params, - }; - - delete paramsWithoutPayload["payload"]; - - return await fn(params.payload as unknown as TPayload, paramsWithoutPayload); + return await fn( + params.payload as unknown as TPayload, + params as TaskStartHookParams + ); }; } -export function createFailureHookAdapter( - fn: NonNullable["onFailure"]> +export function createFailureHookAdapter< + TPayload, + TInitOutput extends TaskInitOutput = TaskInitOutput, +>( + fn: NonNullable["onFailure"]> ): AnyOnFailureHookFunction { return async (params) => { - const paramsWithoutPayload = { - ...params, - }; - - delete paramsWithoutPayload["payload"]; - delete paramsWithoutPayload["error"]; - - return await fn(params.payload as unknown as TPayload, params.error, paramsWithoutPayload); + return await fn( + params.payload as unknown as TPayload, + params.error, + params as TaskFailureHookParams + ); }; } -export function createSuccessHookAdapter( - fn: NonNullable["onSuccess"]> +export function createSuccessHookAdapter( + fn: NonNullable["onSuccess"]> ): AnyOnSuccessHookFunction { return async (params) => { - const paramsWithoutPayload = { - ...params, - }; - - delete paramsWithoutPayload["payload"]; - delete paramsWithoutPayload["output"]; - return await fn( params.payload as unknown as TPayload, params.output as unknown as TOutput, - paramsWithoutPayload + params as TaskSuccessHookParams ); }; } -export function createHandleErrorHookAdapter( - fn: NonNullable["handleError"]> +export function createHandleErrorHookAdapter< + TPayload, + TInitOutput extends TaskInitOutput = TaskInitOutput, +>( + fn: NonNullable["handleError"]> ): AnyOnCatchErrorHookFunction { return async (params) => { - return await fn(params.payload as unknown as TPayload, params.error, params); + return await fn( + params.payload as unknown as TPayload, + params.error, + params as TaskCatchErrorHookParams + ); }; } diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 746dd1f8b0..5a7044289b 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -1,6 +1,8 @@ import { RetryOptions, TaskRunContext } from "../schemas/index.js"; import { HandleErrorResult } from "../types/index.js"; +export type TaskInitOutput = Record | void | undefined; + export type TaskInitHookParams = { ctx: TaskRunContext; payload: TPayload; @@ -8,78 +10,103 @@ export type TaskInitHookParams = { signal?: AbortSignal; }; -export type OnInitHookFunction> = ( +export type OnInitHookFunction = ( params: TaskInitHookParams ) => TInitOutput | undefined | void | Promise; -export type AnyOnInitHookFunction = OnInitHookFunction>; +export type AnyOnInitHookFunction = OnInitHookFunction; -export type TaskStartHookParams = { +export type TaskStartHookParams< + TPayload = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnStartHookFunction = ( - params: TaskStartHookParams +export type OnStartHookFunction = ( + params: TaskStartHookParams ) => undefined | void | Promise; -export type AnyOnStartHookFunction = OnStartHookFunction; +export type AnyOnStartHookFunction = OnStartHookFunction; -export type TaskWaitHookParams = { +export type TaskWaitHookParams< + TPayload = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnWaitHookFunction = ( - params: TaskWaitHookParams +export type OnWaitHookFunction = ( + params: TaskWaitHookParams ) => undefined | void | Promise; -export type AnyOnWaitHookFunction = OnWaitHookFunction; +export type AnyOnWaitHookFunction = OnWaitHookFunction; -export type TaskResumeHookParams = { +export type TaskResumeHookParams< + TPayload = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnResumeHookFunction = ( - params: TaskResumeHookParams +export type OnResumeHookFunction = ( + params: TaskResumeHookParams ) => undefined | void | Promise; -export type AnyOnResumeHookFunction = OnResumeHookFunction; +export type AnyOnResumeHookFunction = OnResumeHookFunction; -export type TaskFailureHookParams = { +export type TaskFailureHookParams< + TPayload = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; error: unknown; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnFailureHookFunction = ( - params: TaskFailureHookParams +export type OnFailureHookFunction = ( + params: TaskFailureHookParams ) => undefined | void | Promise; -export type AnyOnFailureHookFunction = OnFailureHookFunction; +export type AnyOnFailureHookFunction = OnFailureHookFunction; -export type TaskSuccessHookParams = { +export type TaskSuccessHookParams< + TPayload = unknown, + TOutput = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; output: TOutput; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnSuccessHookFunction = ( - params: TaskSuccessHookParams +export type OnSuccessHookFunction< + TPayload, + TOutput, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = ( + params: TaskSuccessHookParams ) => undefined | void | Promise; -export type AnyOnSuccessHookFunction = OnSuccessHookFunction; +export type AnyOnSuccessHookFunction = OnSuccessHookFunction; export type TaskCompleteSuccessResult = { ok: true; @@ -95,19 +122,28 @@ export type TaskCompleteResult = | TaskCompleteSuccessResult | TaskCompleteErrorResult; -export type TaskCompleteHookParams = { +export type TaskCompleteHookParams< + TPayload = unknown, + TOutput = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; result: TaskCompleteResult; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnCompleteHookFunction = ( - params: TaskCompleteHookParams +export type OnCompleteHookFunction< + TPayload, + TOutput, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = ( + params: TaskCompleteHookParams ) => undefined | void | Promise; -export type AnyOnCompleteHookFunction = OnCompleteHookFunction; +export type AnyOnCompleteHookFunction = OnCompleteHookFunction; export type RegisterHookFunctionParams any> = { id?: string; @@ -120,7 +156,10 @@ export type RegisteredHookFunction any> = fn: THookFunction; }; -export type TaskCatchErrorHookParams = { +export type TaskCatchErrorHookParams< + TPayload = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { ctx: TaskRunContext; payload: TPayload; task: string; @@ -129,13 +168,15 @@ export type TaskCatchErrorHookParams = { retryAt?: Date; retryDelayInMs?: number; signal?: AbortSignal; + init?: TInitOutput; }; -export type OnCatchErrorHookFunction = ( - params: TaskCatchErrorHookParams -) => HandleErrorResult; +export type OnCatchErrorHookFunction< + TPayload, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = (params: TaskCatchErrorHookParams) => HandleErrorResult; -export type AnyOnCatchErrorHookFunction = OnCatchErrorHookFunction; +export type AnyOnCatchErrorHookFunction = OnCatchErrorHookFunction; export type TaskMiddlewareHookParams = { ctx: TaskRunContext; diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 799c9ec218..4d93200840 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -387,57 +387,70 @@ export class TaskExecutor { initOutput: any, signal?: AbortSignal ) { - await this.#callOnSuccessFunction( - this.task.fns.onSuccess, - "task.onSuccess", - payload, - output, - ctx, - initOutput, - signal - ); + const globalSuccessHooks = lifecycleHooks.getGlobalSuccessHooks(); + const taskSuccessHook = lifecycleHooks.getTaskSuccessHook(this.task.id); - await this.#callOnSuccessFunction( - this._importedConfig?.onSuccess, - "config.onSuccess", - payload, - output, - ctx, - initOutput, - signal - ); - } - - async #callOnSuccessFunction( - onSuccessFn: TaskMetadataWithFunctions["fns"]["onSuccess"], - name: string, - payload: unknown, - output: any, - ctx: TaskRunContext, - initOutput: any, - signal?: AbortSignal - ) { - if (!onSuccessFn) { + if (globalSuccessHooks.length === 0 && !taskSuccessHook) { return; } - try { - await this._tracer.startActiveSpan( - name, - async (span) => { - return await runTimelineMetrics.measureMetric("trigger.dev/execution", name, () => - onSuccessFn(payload, output, { ctx, init: initOutput, signal }) - ); + return this._tracer.startActiveSpan( + "hooks.success", + async (span) => { + return await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "success", + async () => { + for (const hook of globalSuccessHooks) { + await this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ + payload, + output, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } + + if (taskSuccessHook) { + await this._tracer.startActiveSpan( + "task", + async (span) => { + await taskSuccessHook({ + payload, + output, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } + } + ); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", - }, - } - ); - } catch { - // Ignore errors from onSuccess functions - } + } + ); } async #callOnFailureFunctions( diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 68a909fe21..63e4d71a50 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -84,6 +84,7 @@ import type { AnyOnCompleteHookFunction, AnyOnWaitHookFunction, AnyOnResumeHookFunction, + AnyOnFailureHookFunction, } from "@trigger.dev/core/v3"; export type { From b974d8297d946dfe79bdaa14c84d5b52df90b15b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 17:43:21 +0000 Subject: [PATCH 18/40] use new onFailure --- packages/core/src/v3/workers/taskExecutor.ts | 113 +++++++++++-------- 1 file changed, 67 insertions(+), 46 deletions(-) diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 4d93200840..a6403ccf20 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -460,57 +460,78 @@ export class TaskExecutor { initOutput: any, signal?: AbortSignal ) { - await this.#callOnFailureFunction( - this.task.fns.onFailure, - "task.onFailure", - payload, - error, - ctx, - initOutput, - signal - ); + const globalFailureHooks = lifecycleHooks.getGlobalFailureHooks(); + const taskFailureHook = lifecycleHooks.getTaskFailureHook(this.task.id); - await this.#callOnFailureFunction( - this._importedConfig?.onFailure, - "config.onFailure", - payload, - error, - ctx, - initOutput, - signal - ); - } - - async #callOnFailureFunction( - onFailureFn: TaskMetadataWithFunctions["fns"]["onFailure"], - name: string, - payload: unknown, - error: unknown, - ctx: TaskRunContext, - initOutput: any, - signal?: AbortSignal - ) { - if (!onFailureFn) { + if (globalFailureHooks.length === 0 && !taskFailureHook) { return; } - try { - return await this._tracer.startActiveSpan( - name, - async (span) => { - return await runTimelineMetrics.measureMetric("trigger.dev/execution", name, () => - onFailureFn(payload, error, { ctx, init: initOutput, signal }) - ); + return this._tracer.startActiveSpan( + "hooks.failure", + async (span) => { + return await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "failure", + async () => { + for (const hook of globalFailureHooks) { + try { + await this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ + payload, + error, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } catch { + // Ignore errors from onFailure functions + } + } + + if (taskFailureHook) { + try { + await this._tracer.startActiveSpan( + "task", + async (span) => { + await taskFailureHook({ + payload, + error, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } catch { + // Ignore errors from onFailure functions + } + } + } + ); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "function", - }, - } - ); - } catch (e) { - // Ignore errors from onFailure functions - } + } + ); } async #parsePayload(payload: unknown) { From 307309f4f7f084098896972d68e2b32282491d05 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 17:46:53 +0000 Subject: [PATCH 19/40] implement onComplete --- packages/core/src/v3/workers/taskExecutor.ts | 118 +++++++++++++++++++ 1 file changed, 118 insertions(+) diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index a6403ccf20..7bb96d1e17 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -33,6 +33,7 @@ import { stringifyIO, } from "../utils/ioSerialization.js"; import { calculateNextRetryDelay } from "../utils/retries.js"; +import { TaskCompleteResult } from "../lifecycleHooks/types.js"; export type TaskExecutorOptions = { tracingSDK: TracingSDK; @@ -154,6 +155,15 @@ export class TaskExecutor { span.setAttributes(attributes); } + // Call onComplete with success result + await this.#callOnCompleteFunctions( + parsedPayload, + { ok: true, data: output }, + ctx, + initOutput, + signal + ); + return { ok: true, id: execution.run.id, @@ -163,6 +173,15 @@ export class TaskExecutor { } catch (outputError) { recordSpanException(span, outputError); + // Call onComplete with error result + await this.#callOnCompleteFunctions( + parsedPayload, + { ok: false, error: outputError }, + ctx, + initOutput, + signal + ); + return { ok: false, id: execution.run.id, @@ -199,6 +218,15 @@ export class TaskExecutor { initOutput, signal ); + + // Call onComplete with error result + await this.#callOnCompleteFunctions( + parsedPayload, + { ok: false, error: handleErrorResult.error ?? runError }, + ctx, + initOutput, + signal + ); } return { @@ -215,6 +243,15 @@ export class TaskExecutor { } catch (handleErrorError) { recordSpanException(span, handleErrorError); + // Call onComplete with error result + await this.#callOnCompleteFunctions( + parsedPayload, + { ok: false, error: handleErrorError }, + ctx, + initOutput, + signal + ); + return { ok: false, id: execution.run.id, @@ -767,4 +804,85 @@ export class TaskExecutor { } ); } + + async #callOnCompleteFunctions( + payload: unknown, + result: TaskCompleteResult, + ctx: TaskRunContext, + initOutput: any, + signal?: AbortSignal + ) { + const globalCompleteHooks = lifecycleHooks.getGlobalCompleteHooks(); + const taskCompleteHook = lifecycleHooks.getTaskCompleteHook(this.task.id); + + if (globalCompleteHooks.length === 0 && !taskCompleteHook) { + return; + } + + return this._tracer.startActiveSpan( + "hooks.complete", + async (span) => { + return await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "complete", + async () => { + for (const hook of globalCompleteHooks) { + try { + await this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ + payload, + result, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } catch { + // Ignore errors from onComplete functions + } + } + + if (taskCompleteHook) { + try { + await this._tracer.startActiveSpan( + "task", + async (span) => { + await taskCompleteHook({ + payload, + result, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } catch { + // Ignore errors from onComplete functions + } + } + } + ); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); + } } From 4a8d6932c85356c2f6fc79e8b33b6a58e79f8029 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 17:49:00 +0000 Subject: [PATCH 20/40] a couple tweaks --- packages/core/src/v3/workers/taskExecutor.ts | 22 ++------------------ 1 file changed, 2 insertions(+), 20 deletions(-) diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 7bb96d1e17..417873c236 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -134,8 +134,6 @@ export class TaskExecutor { const output = await this.#callRun(parsedPayload, ctx, initOutput, signal); - await this.#callOnSuccessFunctions(parsedPayload, output, ctx, initOutput, signal); - try { const stringifiedOutput = await stringifyIO(output); @@ -155,6 +153,8 @@ export class TaskExecutor { span.setAttributes(attributes); } + await this.#callOnSuccessFunctions(parsedPayload, output, ctx, initOutput, signal); + // Call onComplete with success result await this.#callOnCompleteFunctions( parsedPayload, @@ -173,15 +173,6 @@ export class TaskExecutor { } catch (outputError) { recordSpanException(span, outputError); - // Call onComplete with error result - await this.#callOnCompleteFunctions( - parsedPayload, - { ok: false, error: outputError }, - ctx, - initOutput, - signal - ); - return { ok: false, id: execution.run.id, @@ -243,15 +234,6 @@ export class TaskExecutor { } catch (handleErrorError) { recordSpanException(span, handleErrorError); - // Call onComplete with error result - await this.#callOnCompleteFunctions( - parsedPayload, - { ok: false, error: handleErrorError }, - ctx, - initOutput, - signal - ); - return { ok: false, id: execution.run.id, From ef871b0434e90865be33e9d14ca1e17cc1504f33 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 18:11:07 +0000 Subject: [PATCH 21/40] starting test executor --- packages/core/src/v3/workers/taskExecutor.ts | 22 +- packages/core/test/taskExecutor.test.ts | 343 +++++++++++++++++++ 2 files changed, 358 insertions(+), 7 deletions(-) create mode 100644 packages/core/test/taskExecutor.test.ts diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 417873c236..5a23831c51 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -13,6 +13,7 @@ import { import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; import { + RetryOptions, ServerBackgroundWorker, TaskRunContext, TaskRunErrorCodes, @@ -39,7 +40,10 @@ export type TaskExecutorOptions = { tracingSDK: TracingSDK; tracer: TriggerTracer; consoleInterceptor: ConsoleInterceptor; - config: TriggerConfig | undefined; + retries?: { + enabledInDev?: boolean; + default?: RetryOptions; + }; handleErrorFn: HandleErrorFunction | undefined; }; @@ -47,7 +51,12 @@ export class TaskExecutor { private _tracingSDK: TracingSDK; private _tracer: TriggerTracer; private _consoleInterceptor: ConsoleInterceptor; - private _importedConfig: TriggerConfig | undefined; + private _retries: + | { + enabledInDev?: boolean; + default?: RetryOptions; + } + | undefined; private _handleErrorFn: HandleErrorFunction | undefined; constructor( @@ -57,7 +66,7 @@ export class TaskExecutor { this._tracingSDK = options.tracingSDK; this._tracer = options.tracer; this._consoleInterceptor = options.consoleInterceptor; - this._importedConfig = options.config; + this._retries = options.retries; this._handleErrorFn = options.handleErrorFn; } @@ -65,7 +74,6 @@ export class TaskExecutor { execution: TaskRunExecution, worker: ServerBackgroundWorker, traceContext: Record, - usage: UsageMeasurement, signal?: AbortSignal ): Promise<{ result: TaskRunExecutionResult }> { const ctx = TaskRunContext.parse(execution); @@ -665,7 +673,7 @@ export class TaskExecutor { | { status: "skipped"; error?: unknown } // skipped is different than noop, it means that the task was skipped from retrying, instead of just not retrying | { status: "noop"; error?: unknown } > { - const retriesConfig = this._importedConfig?.retries; + const retriesConfig = this._retries; const retry = this.task.retry ?? retriesConfig?.default; @@ -721,8 +729,8 @@ export class TaskExecutor { retryAt: delay ? new Date(Date.now() + delay) : undefined, signal, }) - : this._importedConfig - ? await this._handleErrorFn?.(payload, error, { + : this._handleErrorFn + ? await this._handleErrorFn(payload, error, { ctx, init, retry, diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts new file mode 100644 index 0000000000..411aff2248 --- /dev/null +++ b/packages/core/test/taskExecutor.test.ts @@ -0,0 +1,343 @@ +import { describe, expect, test } from "vitest"; +import { ConsoleInterceptor } from "../src/v3/consoleInterceptor.js"; +import { RunFnParams, ServerBackgroundWorker, TaskRunExecution } from "../src/v3/index.js"; +import { TracingSDK } from "../src/v3/otel/tracingSDK.js"; +import { TriggerTracer } from "../src/v3/tracer.js"; +import { TaskExecutor } from "../src/v3/workers/taskExecutor.js"; +import { StandardLifecycleHooksManager } from "../src/v3/lifecycleHooks/manager.js"; +import { lifecycleHooks } from "../src/v3/index.js"; + +describe("TaskExecutor", () => { + beforeEach(() => { + lifecycleHooks.setGlobalLifecycleHooksManager(new StandardLifecycleHooksManager()); + }); + + afterEach(() => { + lifecycleHooks.disable(); + }); + + test("should call onComplete with success result", async () => { + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + lifecycleHooks.registerTaskInitHook("test-task", { + id: "test-init", + fn: async () => { + return { + bar: "baz", + }; + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + return { + output: "test-output", + init: params.init, + }; + }, + }, + }; + + const tracingSDK = new TracingSDK({ + url: "http://localhost:4318", + }); + + const tracer = new TriggerTracer({ + name: "test-task", + version: "1.0.0", + tracer: tracingSDK.getTracer("test-task"), + logger: tracingSDK.getLogger("test-task"), + }); + + const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); + + const executor = new TaskExecutor(task, { + tracingSDK, + tracer, + consoleInterceptor, + retries: { + enabledInDev: false, + default: { + maxAttempts: 1, + }, + }, + handleErrorFn: undefined, + }); + + const execution: TaskRunExecution = { + task: { + id: "test-task", + filePath: "test-task.ts", + }, + attempt: { + number: 1, + startedAt: new Date(), + id: "test-attempt-id", + status: "success", + backgroundWorkerId: "test-background-worker-id", + backgroundWorkerTaskId: "test-background-worker-task-id", + }, + run: { + id: "test-run-id", + payload: "{}", + payloadType: "json", + metadata: {}, + startedAt: new Date(), + tags: [], + isTest: false, + createdAt: new Date(), + durationMs: 0, + costInCents: 0, + baseCostInCents: 0, + priority: 0, + }, + machine: { + name: "micro", + cpu: 1, + memory: 1, + centsPerMs: 0, + }, + queue: { + name: "test-queue", + id: "test-queue-id", + }, + environment: { + type: "PRODUCTION", + id: "test-environment-id", + slug: "test-environment-slug", + }, + organization: { + id: "test-organization-id", + name: "test-organization-name", + slug: "test-organization-slug", + }, + project: { + id: "test-project-id", + name: "test-project-name", + slug: "test-project-slug", + ref: "test-project-ref", + }, + }; + + const worker: ServerBackgroundWorker = { + id: "test-background-worker-id", + version: "1.0.0", + contentHash: "test-content-hash", + engine: "V2", + }; + + const result = await executor.execute(execution, worker, {}); + + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"output":"test-output","init":{"foo":"bar","bar":"baz"}}}', + outputType: "application/super+json", + }, + }); + }); + + test("should call onSuccess hooks in correct order with proper data", async () => { + const globalSuccessOrder: string[] = []; + const successPayloads: any[] = []; + const successOutputs: any[] = []; + const successInits: any[] = []; + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + // Register two global success hooks + lifecycleHooks.registerGlobalSuccessHook({ + id: "global-success-2", // Register second hook first + fn: async ({ payload, output, init }) => { + console.log("Executing global success hook 2"); + globalSuccessOrder.push("global-2"); + successPayloads.push(payload); + successOutputs.push(output); + successInits.push(init); + }, + }); + + lifecycleHooks.registerGlobalSuccessHook({ + id: "global-success-1", // Register first hook second + fn: async ({ payload, output, init }) => { + console.log("Executing global success hook 1"); + globalSuccessOrder.push("global-1"); + successPayloads.push(payload); + successOutputs.push(output); + successInits.push(init); + }, + }); + + // Register task-specific success hook + lifecycleHooks.registerTaskSuccessHook("test-task", { + id: "task-success", + fn: async ({ payload, output, init }) => { + console.log("Executing task success hook"); + globalSuccessOrder.push("task"); + successPayloads.push(payload); + successOutputs.push(output); + successInits.push(init); + }, + }); + + // Verify hooks are registered + const globalHooks = lifecycleHooks.getGlobalSuccessHooks(); + console.log( + "Registered global hooks:", + globalHooks.map((h) => h.id) + ); + const taskHook = lifecycleHooks.getTaskSuccessHook("test-task"); + console.log("Registered task hook:", taskHook ? "yes" : "no"); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + return { + output: "test-output", + init: params.init, + }; + }, + }, + }; + + const tracingSDK = new TracingSDK({ + url: "http://localhost:4318", + }); + + const tracer = new TriggerTracer({ + name: "test-task", + version: "1.0.0", + tracer: tracingSDK.getTracer("test-task"), + logger: tracingSDK.getLogger("test-task"), + }); + + const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); + + const executor = new TaskExecutor(task, { + tracingSDK, + tracer, + consoleInterceptor, + retries: { + enabledInDev: false, + default: { + maxAttempts: 1, + }, + }, + handleErrorFn: undefined, + }); + + const execution: TaskRunExecution = { + task: { + id: "test-task", + filePath: "test-task.ts", + }, + attempt: { + number: 1, + startedAt: new Date(), + id: "test-attempt-id", + status: "success", + backgroundWorkerId: "test-background-worker-id", + backgroundWorkerTaskId: "test-background-worker-task-id", + }, + run: { + id: "test-run-id", + payload: "{}", + payloadType: "json", + metadata: {}, + startedAt: new Date(), + tags: [], + isTest: false, + createdAt: new Date(), + durationMs: 0, + costInCents: 0, + baseCostInCents: 0, + priority: 0, + }, + machine: { + name: "micro", + cpu: 1, + memory: 1, + centsPerMs: 0, + }, + queue: { + name: "test-queue", + id: "test-queue-id", + }, + environment: { + type: "PRODUCTION", + id: "test-environment-id", + slug: "test-environment-slug", + }, + organization: { + id: "test-organization-id", + name: "test-organization-name", + slug: "test-organization-slug", + }, + project: { + id: "test-project-id", + name: "test-project-name", + slug: "test-project-slug", + ref: "test-project-ref", + }, + }; + + const worker: ServerBackgroundWorker = { + id: "test-background-worker-id", + version: "1.0.0", + contentHash: "test-content-hash", + engine: "V2", + }; + + const result = await executor.execute(execution, worker, {}); + + // Verify hooks were called in correct order - should match registration order + expect(globalSuccessOrder).toEqual(["global-2", "global-1", "task"]); + + // Verify each hook received the correct payload + successPayloads.forEach((payload) => { + expect(payload).toEqual({}); + }); + + // Verify each hook received the correct output + successOutputs.forEach((output) => { + expect(output).toEqual({ + output: "test-output", + init: { foo: "bar" }, + }); + }); + + // Verify each hook received the correct init data + successInits.forEach((init) => { + expect(init).toEqual({ foo: "bar" }); + }); + + // Verify the final result + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"output":"test-output","init":{"foo":"bar"}}}', + outputType: "application/super+json", + }, + }); + }); +}); From 2262a42632f6f736e7c945bed7850521d4daff0d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 21:56:57 +0000 Subject: [PATCH 22/40] more tests and fixes --- .../core/src/v3/lifecycleHooks/manager.ts | 40 ++-- packages/core/src/v3/workers/taskExecutor.ts | 34 ++-- packages/core/test/taskExecutor.test.ts | 188 +++++++++++++++++- 3 files changed, 223 insertions(+), 39 deletions(-) diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 3cd40d2a86..f8e4096b98 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -57,7 +57,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalStartHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -70,7 +70,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskStartHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -89,7 +89,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { const registeredHook = { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }; @@ -122,7 +122,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalFailureHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -135,7 +135,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskFailureHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -153,7 +153,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalSuccessHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -166,7 +166,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskSuccessHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -184,7 +184,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalCompleteHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -197,7 +197,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskCompleteHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -215,7 +215,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalWaitHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -228,7 +228,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskWaitHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -246,7 +246,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalResumeHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -259,7 +259,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskResumeHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -279,7 +279,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalCatchErrorHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -292,7 +292,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskCatchErrorHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -312,7 +312,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.globalMiddlewareHooks.set(id, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -325,7 +325,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { this.taskMiddlewareHooks.set(taskId, { id, - name: hook.id ?? hook.fn.name ? (hook.fn.name === "" ? undefined : hook.fn.name) : undefined, + name: hook.id, fn: hook.fn, }); } @@ -507,9 +507,5 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { } function generateHookId(hook: RegisterHookFunctionParams): string { - return hook.id ?? hook.fn.name - ? hook.fn.name === "" - ? hook.fn.toString() - : hook.fn.name - : hook.fn.toString(); + return hook.id ?? hook.fn.toString(); } diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 5a23831c51..57396acfe4 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -3,13 +3,8 @@ import { VERSION } from "../../version.js"; import { ApiError, RateLimitError } from "../apiClient/errors.js"; import { ConsoleInterceptor } from "../consoleInterceptor.js"; import { isInternalError, parseError, sanitizeError, TaskPayloadParsedError } from "../errors.js"; -import { - flattenAttributes, - lifecycleHooks, - runMetadata, - TriggerConfig, - waitUntil, -} from "../index.js"; +import { flattenAttributes, lifecycleHooks, runMetadata, waitUntil } from "../index.js"; +import { TaskCompleteResult } from "../lifecycleHooks/types.js"; import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; import { @@ -25,7 +20,6 @@ import { SemanticInternalAttributes } from "../semanticInternalAttributes.js"; import { taskContext } from "../task-context-api.js"; import { TriggerTracer } from "../tracer.js"; import { HandleErrorFunction, TaskMetadataWithFunctions } from "../types/index.js"; -import { UsageMeasurement } from "../usage/types.js"; import { conditionallyExportPacket, conditionallyImportPacket, @@ -34,7 +28,6 @@ import { stringifyIO, } from "../utils/ioSerialization.js"; import { calculateNextRetryDelay } from "../utils/retries.js"; -import { TaskCompleteResult } from "../lifecycleHooks/types.js"; export type TaskExecutorOptions = { tracingSDK: TracingSDK; @@ -134,12 +127,12 @@ export class TaskExecutor { try { parsedPayload = await this.#parsePayload(parsedPayload); + initOutput = await this.#callInitFunctions(parsedPayload, ctx, signal); + if (execution.attempt.number === 1) { - await this.#callOnStartFunctions(parsedPayload, ctx, signal); + await this.#callOnStartFunctions(parsedPayload, ctx, initOutput, signal); } - initOutput = await this.#callInitFunctions(parsedPayload, ctx, signal); - const output = await this.#callRun(parsedPayload, ctx, initOutput, signal); try { @@ -573,7 +566,12 @@ export class TaskExecutor { } } - async #callOnStartFunctions(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { + async #callOnStartFunctions( + payload: unknown, + ctx: TaskRunContext, + initOutput: any, + signal?: AbortSignal + ) { const globalStartHooks = lifecycleHooks.getGlobalStartHooks(); const taskStartHook = lifecycleHooks.getTaskStartHook(this.task.id); @@ -592,7 +590,7 @@ export class TaskExecutor { await this._tracer.startActiveSpan( hook.name ?? "global", async (span) => { - await hook.fn({ payload, ctx, signal, task: this.task.id }); + await hook.fn({ payload, ctx, signal, task: this.task.id, init: initOutput }); }, { attributes: { @@ -606,7 +604,13 @@ export class TaskExecutor { await this._tracer.startActiveSpan( "task", async (span) => { - await taskStartHook({ payload, ctx, signal, task: this.task.id }); + await taskStartHook({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); }, { attributes: { diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 411aff2248..952c4b7677 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -89,7 +89,7 @@ describe("TaskExecutor", () => { run: { id: "test-run-id", payload: "{}", - payloadType: "json", + payloadType: "application/json", metadata: {}, startedAt: new Date(), tags: [], @@ -261,7 +261,7 @@ describe("TaskExecutor", () => { run: { id: "test-run-id", payload: "{}", - payloadType: "json", + payloadType: "application/json", metadata: {}, startedAt: new Date(), tags: [], @@ -340,4 +340,188 @@ describe("TaskExecutor", () => { }, }); }); + + test("should call onStart hooks in correct order with proper data", async () => { + const globalStartOrder: string[] = []; + const startPayloads: any[] = []; + const startInits: any[] = []; + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + // Register two global start hooks + lifecycleHooks.registerGlobalStartHook({ + id: "global-start-1", + fn: async ({ payload, ctx, init }) => { + console.log("Executing global start hook 1"); + globalStartOrder.push("global-1"); + startPayloads.push(payload); + startInits.push(init); + }, + }); + + lifecycleHooks.registerGlobalStartHook({ + id: "global-start-2", + fn: async ({ payload, ctx, init }) => { + console.log("Executing global start hook 2"); + globalStartOrder.push("global-2"); + startPayloads.push(payload); + startInits.push(init); + }, + }); + + // Register task-specific start hook + lifecycleHooks.registerTaskStartHook("test-task", { + id: "task-start", + fn: async ({ payload, ctx, init }) => { + console.log("Executing task start hook"); + globalStartOrder.push("task"); + startPayloads.push(payload); + startInits.push(init); + }, + }); + + // Verify hooks are registered + const globalHooks = lifecycleHooks.getGlobalStartHooks(); + console.log( + "Registered global hooks:", + globalHooks.map((h) => h.id) + ); + const taskHook = lifecycleHooks.getTaskStartHook("test-task"); + console.log("Registered task hook:", taskHook ? "yes" : "no"); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + return { + output: "test-output", + init: params.init, + }; + }, + }, + }; + + const tracingSDK = new TracingSDK({ + url: "http://localhost:4318", + }); + + const tracer = new TriggerTracer({ + name: "test-task", + version: "1.0.0", + tracer: tracingSDK.getTracer("test-task"), + logger: tracingSDK.getLogger("test-task"), + }); + + const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); + + const executor = new TaskExecutor(task, { + tracingSDK, + tracer, + consoleInterceptor, + retries: { + enabledInDev: false, + default: { + maxAttempts: 1, + }, + }, + handleErrorFn: undefined, + }); + + const execution: TaskRunExecution = { + task: { + id: "test-task", + filePath: "test-task.ts", + }, + attempt: { + number: 1, + startedAt: new Date(), + id: "test-attempt-id", + status: "success", + backgroundWorkerId: "test-background-worker-id", + backgroundWorkerTaskId: "test-background-worker-task-id", + }, + run: { + id: "test-run-id", + payload: '{"test":"data"}', + payloadType: "application/json", + metadata: {}, + startedAt: new Date(), + tags: [], + isTest: false, + createdAt: new Date(), + durationMs: 0, + costInCents: 0, + baseCostInCents: 0, + priority: 0, + }, + machine: { + name: "micro", + cpu: 1, + memory: 1, + centsPerMs: 0, + }, + queue: { + name: "test-queue", + id: "test-queue-id", + }, + environment: { + type: "PRODUCTION", + id: "test-environment-id", + slug: "test-environment-slug", + }, + organization: { + id: "test-organization-id", + name: "test-organization-name", + slug: "test-organization-slug", + }, + project: { + id: "test-project-id", + name: "test-project-name", + slug: "test-project-slug", + ref: "test-project-ref", + }, + }; + + const worker: ServerBackgroundWorker = { + id: "test-background-worker-id", + version: "1.0.0", + contentHash: "test-content-hash", + engine: "V2", + }; + + const result = await executor.execute(execution, worker, {}); + + // Verify hooks were called in correct order + expect(globalStartOrder).toEqual(["global-1", "global-2", "task"]); + + // Verify each hook received the correct payload + startPayloads.forEach((payload) => { + expect(payload).toEqual({ test: "data" }); + }); + + console.log("startInits", startInits); + + // Verify each hook received the correct init data + startInits.forEach((init) => { + expect(init).toEqual({ foo: "bar" }); + }); + + // Verify the final result + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"output":"test-output","init":{"foo":"bar"}}}', + outputType: "application/super+json", + }, + }); + }); }); From 91225da813c50142b6de4abb2b13376e6f88084f Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 22:00:33 +0000 Subject: [PATCH 23/40] test on failure --- packages/core/test/taskExecutor.test.ts | 195 ++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 952c4b7677..147709fb5e 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -524,4 +524,199 @@ describe("TaskExecutor", () => { }, }); }); + + test("should call onFailure hooks with error when task fails", async () => { + const globalFailureOrder: string[] = []; + const failurePayloads: any[] = []; + const failureErrors: any[] = []; + const failureInits: any[] = []; + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + // Register two global failure hooks + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure-1", + fn: async ({ payload, error, init }) => { + console.log("Executing global failure hook 1"); + globalFailureOrder.push("global-1"); + failurePayloads.push(payload); + failureErrors.push(error); + failureInits.push(init); + }, + }); + + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure-2", + fn: async ({ payload, error, init }) => { + console.log("Executing global failure hook 2"); + globalFailureOrder.push("global-2"); + failurePayloads.push(payload); + failureErrors.push(error); + failureInits.push(init); + }, + }); + + // Register task-specific failure hook + lifecycleHooks.registerTaskFailureHook("test-task", { + id: "task-failure", + fn: async ({ payload, error, init }) => { + console.log("Executing task failure hook"); + globalFailureOrder.push("task"); + failurePayloads.push(payload); + failureErrors.push(error); + failureInits.push(init); + }, + }); + + // Verify hooks are registered + const globalHooks = lifecycleHooks.getGlobalFailureHooks(); + console.log( + "Registered global hooks:", + globalHooks.map((h) => h.id) + ); + const taskHook = lifecycleHooks.getTaskFailureHook("test-task"); + console.log("Registered task hook:", taskHook ? "yes" : "no"); + + const expectedError = new Error("Task failed intentionally"); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + throw expectedError; + }, + }, + }; + + const tracingSDK = new TracingSDK({ + url: "http://localhost:4318", + }); + + const tracer = new TriggerTracer({ + name: "test-task", + version: "1.0.0", + tracer: tracingSDK.getTracer("test-task"), + logger: tracingSDK.getLogger("test-task"), + }); + + const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); + + const executor = new TaskExecutor(task, { + tracingSDK, + tracer, + consoleInterceptor, + retries: { + enabledInDev: false, + default: { + maxAttempts: 1, + }, + }, + handleErrorFn: undefined, + }); + + const execution: TaskRunExecution = { + task: { + id: "test-task", + filePath: "test-task.ts", + }, + attempt: { + number: 1, + startedAt: new Date(), + id: "test-attempt-id", + status: "success", + backgroundWorkerId: "test-background-worker-id", + backgroundWorkerTaskId: "test-background-worker-task-id", + }, + run: { + id: "test-run-id", + payload: '{"test":"data"}', + payloadType: "application/json", + metadata: {}, + startedAt: new Date(), + tags: [], + isTest: false, + createdAt: new Date(), + durationMs: 0, + costInCents: 0, + baseCostInCents: 0, + priority: 0, + }, + machine: { + name: "micro", + cpu: 1, + memory: 1, + centsPerMs: 0, + }, + queue: { + name: "test-queue", + id: "test-queue-id", + }, + environment: { + type: "PRODUCTION", + id: "test-environment-id", + slug: "test-environment-slug", + }, + organization: { + id: "test-organization-id", + name: "test-organization-name", + slug: "test-organization-slug", + }, + project: { + id: "test-project-id", + name: "test-project-name", + slug: "test-project-slug", + ref: "test-project-ref", + }, + }; + + const worker: ServerBackgroundWorker = { + id: "test-background-worker-id", + version: "1.0.0", + contentHash: "test-content-hash", + engine: "V2", + }; + + const result = await executor.execute(execution, worker, {}); + + // Verify hooks were called in correct order + expect(globalFailureOrder).toEqual(["global-1", "global-2", "task"]); + + // Verify each hook received the correct payload + failurePayloads.forEach((payload) => { + expect(payload).toEqual({ test: "data" }); + }); + + // Verify each hook received the correct error + failureErrors.forEach((error) => { + expect(error).toBe(expectedError); + }); + + // Verify each hook received the correct init data + failureInits.forEach((init) => { + expect(init).toEqual({ foo: "bar" }); + }); + + // Verify the final result contains the error + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Task failed intentionally", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); }); From 87824a8ce340cf9f72f9c555e1d071a88b78e209 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 22:06:48 +0000 Subject: [PATCH 24/40] dry up some stuff --- packages/core/test/taskExecutor.test.ts | 459 ++++++------------------ 1 file changed, 102 insertions(+), 357 deletions(-) diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 147709fb5e..fbec009b90 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -1,6 +1,11 @@ import { describe, expect, test } from "vitest"; import { ConsoleInterceptor } from "../src/v3/consoleInterceptor.js"; -import { RunFnParams, ServerBackgroundWorker, TaskRunExecution } from "../src/v3/index.js"; +import { + RunFnParams, + ServerBackgroundWorker, + TaskMetadataWithFunctions, + TaskRunExecution, +} from "../src/v3/index.js"; import { TracingSDK } from "../src/v3/otel/tracingSDK.js"; import { TriggerTracer } from "../src/v3/tracer.js"; import { TaskExecutor } from "../src/v3/workers/taskExecutor.js"; @@ -47,95 +52,7 @@ describe("TaskExecutor", () => { }, }; - const tracingSDK = new TracingSDK({ - url: "http://localhost:4318", - }); - - const tracer = new TriggerTracer({ - name: "test-task", - version: "1.0.0", - tracer: tracingSDK.getTracer("test-task"), - logger: tracingSDK.getLogger("test-task"), - }); - - const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); - - const executor = new TaskExecutor(task, { - tracingSDK, - tracer, - consoleInterceptor, - retries: { - enabledInDev: false, - default: { - maxAttempts: 1, - }, - }, - handleErrorFn: undefined, - }); - - const execution: TaskRunExecution = { - task: { - id: "test-task", - filePath: "test-task.ts", - }, - attempt: { - number: 1, - startedAt: new Date(), - id: "test-attempt-id", - status: "success", - backgroundWorkerId: "test-background-worker-id", - backgroundWorkerTaskId: "test-background-worker-task-id", - }, - run: { - id: "test-run-id", - payload: "{}", - payloadType: "application/json", - metadata: {}, - startedAt: new Date(), - tags: [], - isTest: false, - createdAt: new Date(), - durationMs: 0, - costInCents: 0, - baseCostInCents: 0, - priority: 0, - }, - machine: { - name: "micro", - cpu: 1, - memory: 1, - centsPerMs: 0, - }, - queue: { - name: "test-queue", - id: "test-queue-id", - }, - environment: { - type: "PRODUCTION", - id: "test-environment-id", - slug: "test-environment-slug", - }, - organization: { - id: "test-organization-id", - name: "test-organization-name", - slug: "test-organization-slug", - }, - project: { - id: "test-project-id", - name: "test-project-name", - slug: "test-project-slug", - ref: "test-project-ref", - }, - }; - - const worker: ServerBackgroundWorker = { - id: "test-background-worker-id", - version: "1.0.0", - contentHash: "test-content-hash", - engine: "V2", - }; - - const result = await executor.execute(execution, worker, {}); + const result = await executeTask(task, {}); expect(result).toEqual({ result: { @@ -219,95 +136,7 @@ describe("TaskExecutor", () => { }, }; - const tracingSDK = new TracingSDK({ - url: "http://localhost:4318", - }); - - const tracer = new TriggerTracer({ - name: "test-task", - version: "1.0.0", - tracer: tracingSDK.getTracer("test-task"), - logger: tracingSDK.getLogger("test-task"), - }); - - const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); - - const executor = new TaskExecutor(task, { - tracingSDK, - tracer, - consoleInterceptor, - retries: { - enabledInDev: false, - default: { - maxAttempts: 1, - }, - }, - handleErrorFn: undefined, - }); - - const execution: TaskRunExecution = { - task: { - id: "test-task", - filePath: "test-task.ts", - }, - attempt: { - number: 1, - startedAt: new Date(), - id: "test-attempt-id", - status: "success", - backgroundWorkerId: "test-background-worker-id", - backgroundWorkerTaskId: "test-background-worker-task-id", - }, - run: { - id: "test-run-id", - payload: "{}", - payloadType: "application/json", - metadata: {}, - startedAt: new Date(), - tags: [], - isTest: false, - createdAt: new Date(), - durationMs: 0, - costInCents: 0, - baseCostInCents: 0, - priority: 0, - }, - machine: { - name: "micro", - cpu: 1, - memory: 1, - centsPerMs: 0, - }, - queue: { - name: "test-queue", - id: "test-queue-id", - }, - environment: { - type: "PRODUCTION", - id: "test-environment-id", - slug: "test-environment-slug", - }, - organization: { - id: "test-organization-id", - name: "test-organization-name", - slug: "test-organization-slug", - }, - project: { - id: "test-project-id", - name: "test-project-name", - slug: "test-project-slug", - ref: "test-project-ref", - }, - }; - - const worker: ServerBackgroundWorker = { - id: "test-background-worker-id", - version: "1.0.0", - contentHash: "test-content-hash", - engine: "V2", - }; - - const result = await executor.execute(execution, worker, {}); + const result = await executeTask(task, {}); // Verify hooks were called in correct order - should match registration order expect(globalSuccessOrder).toEqual(["global-2", "global-1", "task"]); @@ -409,95 +238,7 @@ describe("TaskExecutor", () => { }, }; - const tracingSDK = new TracingSDK({ - url: "http://localhost:4318", - }); - - const tracer = new TriggerTracer({ - name: "test-task", - version: "1.0.0", - tracer: tracingSDK.getTracer("test-task"), - logger: tracingSDK.getLogger("test-task"), - }); - - const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); - - const executor = new TaskExecutor(task, { - tracingSDK, - tracer, - consoleInterceptor, - retries: { - enabledInDev: false, - default: { - maxAttempts: 1, - }, - }, - handleErrorFn: undefined, - }); - - const execution: TaskRunExecution = { - task: { - id: "test-task", - filePath: "test-task.ts", - }, - attempt: { - number: 1, - startedAt: new Date(), - id: "test-attempt-id", - status: "success", - backgroundWorkerId: "test-background-worker-id", - backgroundWorkerTaskId: "test-background-worker-task-id", - }, - run: { - id: "test-run-id", - payload: '{"test":"data"}', - payloadType: "application/json", - metadata: {}, - startedAt: new Date(), - tags: [], - isTest: false, - createdAt: new Date(), - durationMs: 0, - costInCents: 0, - baseCostInCents: 0, - priority: 0, - }, - machine: { - name: "micro", - cpu: 1, - memory: 1, - centsPerMs: 0, - }, - queue: { - name: "test-queue", - id: "test-queue-id", - }, - environment: { - type: "PRODUCTION", - id: "test-environment-id", - slug: "test-environment-slug", - }, - organization: { - id: "test-organization-id", - name: "test-organization-name", - slug: "test-organization-slug", - }, - project: { - id: "test-project-id", - name: "test-project-name", - slug: "test-project-slug", - ref: "test-project-ref", - }, - }; - - const worker: ServerBackgroundWorker = { - id: "test-background-worker-id", - version: "1.0.0", - contentHash: "test-content-hash", - engine: "V2", - }; - - const result = await executor.execute(execution, worker, {}); + const result = await executeTask(task, { test: "data" }); // Verify hooks were called in correct order expect(globalStartOrder).toEqual(["global-1", "global-2", "task"]); @@ -596,95 +337,7 @@ describe("TaskExecutor", () => { }, }; - const tracingSDK = new TracingSDK({ - url: "http://localhost:4318", - }); - - const tracer = new TriggerTracer({ - name: "test-task", - version: "1.0.0", - tracer: tracingSDK.getTracer("test-task"), - logger: tracingSDK.getLogger("test-task"), - }); - - const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); - - const executor = new TaskExecutor(task, { - tracingSDK, - tracer, - consoleInterceptor, - retries: { - enabledInDev: false, - default: { - maxAttempts: 1, - }, - }, - handleErrorFn: undefined, - }); - - const execution: TaskRunExecution = { - task: { - id: "test-task", - filePath: "test-task.ts", - }, - attempt: { - number: 1, - startedAt: new Date(), - id: "test-attempt-id", - status: "success", - backgroundWorkerId: "test-background-worker-id", - backgroundWorkerTaskId: "test-background-worker-task-id", - }, - run: { - id: "test-run-id", - payload: '{"test":"data"}', - payloadType: "application/json", - metadata: {}, - startedAt: new Date(), - tags: [], - isTest: false, - createdAt: new Date(), - durationMs: 0, - costInCents: 0, - baseCostInCents: 0, - priority: 0, - }, - machine: { - name: "micro", - cpu: 1, - memory: 1, - centsPerMs: 0, - }, - queue: { - name: "test-queue", - id: "test-queue-id", - }, - environment: { - type: "PRODUCTION", - id: "test-environment-id", - slug: "test-environment-slug", - }, - organization: { - id: "test-organization-id", - name: "test-organization-name", - slug: "test-organization-slug", - }, - project: { - id: "test-project-id", - name: "test-project-name", - slug: "test-project-slug", - ref: "test-project-ref", - }, - }; - - const worker: ServerBackgroundWorker = { - id: "test-background-worker-id", - version: "1.0.0", - contentHash: "test-content-hash", - engine: "V2", - }; - - const result = await executor.execute(execution, worker, {}); + const result = await executeTask(task, { test: "data" }); // Verify hooks were called in correct order expect(globalFailureOrder).toEqual(["global-1", "global-2", "task"]); @@ -720,3 +373,95 @@ describe("TaskExecutor", () => { }); }); }); + +function executeTask(task: TaskMetadataWithFunctions, payload: any) { + const tracingSDK = new TracingSDK({ + url: "http://localhost:4318", + }); + + const tracer = new TriggerTracer({ + name: "test-task", + version: "1.0.0", + tracer: tracingSDK.getTracer("test-task"), + logger: tracingSDK.getLogger("test-task"), + }); + + const consoleInterceptor = new ConsoleInterceptor(tracingSDK.getLogger("test-task"), false); + + const executor = new TaskExecutor(task, { + tracingSDK, + tracer, + consoleInterceptor, + retries: { + enabledInDev: false, + default: { + maxAttempts: 1, + }, + }, + handleErrorFn: undefined, + }); + + const execution: TaskRunExecution = { + task: { + id: "test-task", + filePath: "test-task.ts", + }, + attempt: { + number: 1, + startedAt: new Date(), + id: "test-attempt-id", + status: "success", + backgroundWorkerId: "test-background-worker-id", + backgroundWorkerTaskId: "test-background-worker-task-id", + }, + run: { + id: "test-run-id", + payload: JSON.stringify(payload), + payloadType: "application/json", + metadata: {}, + startedAt: new Date(), + tags: [], + isTest: false, + createdAt: new Date(), + durationMs: 0, + costInCents: 0, + baseCostInCents: 0, + priority: 0, + }, + machine: { + name: "micro", + cpu: 1, + memory: 1, + centsPerMs: 0, + }, + queue: { + name: "test-queue", + id: "test-queue-id", + }, + environment: { + type: "PRODUCTION", + id: "test-environment-id", + slug: "test-environment-slug", + }, + organization: { + id: "test-organization-id", + name: "test-organization-name", + slug: "test-organization-slug", + }, + project: { + id: "test-project-id", + name: "test-project-name", + slug: "test-project-slug", + ref: "test-project-ref", + }, + }; + + const worker: ServerBackgroundWorker = { + id: "test-background-worker-id", + version: "1.0.0", + contentHash: "test-content-hash", + engine: "V2", + }; + + return executor.execute(execution, worker, {}); +} From f84ddcd6ea07d84d783ff4c3a35d13b06a1da48b Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 22:09:11 +0000 Subject: [PATCH 25/40] test oncomplete --- packages/core/test/taskExecutor.test.ts | 199 ++++++++++++++++++++++++ 1 file changed, 199 insertions(+) diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index fbec009b90..975759b740 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -372,6 +372,205 @@ describe("TaskExecutor", () => { }, }); }); + + test("should call onComplete hooks in correct order with proper data", async () => { + const globalCompleteOrder: string[] = []; + const completePayloads: any[] = []; + const completeResults: any[] = []; + const completeInits: any[] = []; + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + // Register two global complete hooks + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete-1", + fn: async ({ payload, result, init }) => { + console.log("Executing global complete hook 1"); + globalCompleteOrder.push("global-1"); + completePayloads.push(payload); + completeResults.push(result); + completeInits.push(init); + }, + }); + + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete-2", + fn: async ({ payload, result, init }) => { + console.log("Executing global complete hook 2"); + globalCompleteOrder.push("global-2"); + completePayloads.push(payload); + completeResults.push(result); + completeInits.push(init); + }, + }); + + // Register task-specific complete hook + lifecycleHooks.registerTaskCompleteHook("test-task", { + id: "task-complete", + fn: async ({ payload, result, init }) => { + console.log("Executing task complete hook"); + globalCompleteOrder.push("task"); + completePayloads.push(payload); + completeResults.push(result); + completeInits.push(init); + }, + }); + + // Verify hooks are registered + const globalHooks = lifecycleHooks.getGlobalCompleteHooks(); + console.log( + "Registered global hooks:", + globalHooks.map((h) => h.id) + ); + const taskHook = lifecycleHooks.getTaskCompleteHook("test-task"); + console.log("Registered task hook:", taskHook ? "yes" : "no"); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + return { + output: "test-output", + init: params.init, + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify hooks were called in correct order + expect(globalCompleteOrder).toEqual(["global-1", "global-2", "task"]); + + // Verify each hook received the correct payload + completePayloads.forEach((payload) => { + expect(payload).toEqual({ test: "data" }); + }); + + // Verify each hook received the correct result + completeResults.forEach((result) => { + expect(result).toEqual({ + ok: true, + data: { + output: "test-output", + init: { foo: "bar" }, + }, + }); + }); + + // Verify each hook received the correct init data + completeInits.forEach((init) => { + expect(init).toEqual({ foo: "bar" }); + }); + + // Verify the final result + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"output":"test-output","init":{"foo":"bar"}}}', + outputType: "application/super+json", + }, + }); + }); + + test("should call onComplete hooks with error when task fails", async () => { + const globalCompleteOrder: string[] = []; + const completePayloads: any[] = []; + const completeResults: any[] = []; + const completeInits: any[] = []; + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + // Register global complete hooks + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async ({ payload, result, init }) => { + console.log("Executing global complete hook"); + globalCompleteOrder.push("global"); + completePayloads.push(payload); + completeResults.push(result); + completeInits.push(init); + }, + }); + + // Register task-specific complete hook + lifecycleHooks.registerTaskCompleteHook("test-task", { + id: "task-complete", + fn: async ({ payload, result, init }) => { + console.log("Executing task complete hook"); + globalCompleteOrder.push("task"); + completePayloads.push(payload); + completeResults.push(result); + completeInits.push(init); + }, + }); + + const expectedError = new Error("Task failed intentionally"); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + throw expectedError; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify hooks were called in correct order + expect(globalCompleteOrder).toEqual(["global", "task"]); + + // Verify each hook received the correct payload + completePayloads.forEach((payload) => { + expect(payload).toEqual({ test: "data" }); + }); + + // Verify each hook received the error result + completeResults.forEach((result) => { + expect(result).toEqual({ + ok: false, + error: expectedError, + }); + }); + + // Verify each hook received the correct init data + completeInits.forEach((init) => { + expect(init).toEqual({ foo: "bar" }); + }); + + // Verify the final result contains the error + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Task failed intentionally", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); }); function executeTask(task: TaskMetadataWithFunctions, payload: any) { From 6ab74c47d67df06c0963b1ff3d5e99e4d063ca51 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Mon, 24 Mar 2025 22:44:43 +0000 Subject: [PATCH 26/40] implement and test handleError (now catchError) --- packages/core/src/v3/workers/taskExecutor.ts | 206 ++++++++++-------- packages/core/test/taskExecutor.test.ts | 210 +++++++++++++++++++ 2 files changed, 331 insertions(+), 85 deletions(-) diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 57396acfe4..f24121ccce 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -4,7 +4,7 @@ import { ApiError, RateLimitError } from "../apiClient/errors.js"; import { ConsoleInterceptor } from "../consoleInterceptor.js"; import { isInternalError, parseError, sanitizeError, TaskPayloadParsedError } from "../errors.js"; import { flattenAttributes, lifecycleHooks, runMetadata, waitUntil } from "../index.js"; -import { TaskCompleteResult } from "../lifecycleHooks/types.js"; +import { TaskCompleteResult, TaskInitOutput } from "../lifecycleHooks/types.js"; import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; import { @@ -19,7 +19,11 @@ import { import { SemanticInternalAttributes } from "../semanticInternalAttributes.js"; import { taskContext } from "../task-context-api.js"; import { TriggerTracer } from "../tracer.js"; -import { HandleErrorFunction, TaskMetadataWithFunctions } from "../types/index.js"; +import { + HandleErrorFunction, + HandleErrorModificationOptions, + TaskMetadataWithFunctions, +} from "../types/index.js"; import { conditionallyExportPacket, conditionallyImportPacket, @@ -670,21 +674,17 @@ export class TaskExecutor { error: unknown, payload: any, ctx: TaskRunContext, - init: unknown, + init: TaskInitOutput, signal?: AbortSignal ): Promise< | { status: "retry"; retry: TaskRunExecutionRetry; error?: unknown } - | { status: "skipped"; error?: unknown } // skipped is different than noop, it means that the task was skipped from retrying, instead of just not retrying + | { status: "skipped"; error?: unknown } | { status: "noop"; error?: unknown } > { const retriesConfig = this._retries; - const retry = this.task.retry ?? retriesConfig?.default; - if (!retry) { - return { status: "noop" }; - } - + // Early exit conditions that prevent retrying if (isInternalError(error) && error.skipRetrying) { return { status: "skipped", error }; } @@ -696,23 +696,28 @@ export class TaskExecutor { return { status: "skipped" }; } - if (execution.run.maxAttempts) { - retry.maxAttempts = Math.max(execution.run.maxAttempts, 1); - } - - let delay = calculateNextRetryDelay(retry, execution.attempt.number); - - if ( - delay && - error instanceof Error && - error.name === "TriggerApiError" && - (error as ApiError).status === 429 - ) { - const rateLimitError = error as RateLimitError; + // Calculate default retry delay if retry config exists + let defaultDelay: number | undefined; + if (retry) { + if (execution.run.maxAttempts) { + retry.maxAttempts = Math.max(execution.run.maxAttempts, 1); + } - delay = rateLimitError.millisecondsUntilReset; + defaultDelay = calculateNextRetryDelay(retry, execution.attempt.number); + + // Handle rate limit errors + if ( + defaultDelay && + error instanceof Error && + error.name === "TriggerApiError" && + (error as ApiError).status === 429 + ) { + const rateLimitError = error as RateLimitError; + defaultDelay = rateLimitError.millisecondsUntilReset; + } } + // Check if retries are enabled in dev environment if ( execution.environment.type === "DEVELOPMENT" && typeof retriesConfig?.enabledInDev === "boolean" && @@ -724,72 +729,53 @@ export class TaskExecutor { return this._tracer.startActiveSpan( "handleError()", async (span) => { - const handleErrorResult = this.task.fns.handleError - ? await this.task.fns.handleError(payload, error, { - ctx, - init, - retry, - retryDelayInMs: delay, - retryAt: delay ? new Date(Date.now() + delay) : undefined, - signal, - }) - : this._handleErrorFn - ? await this._handleErrorFn(payload, error, { - ctx, - init, - retry, - retryDelayInMs: delay, - retryAt: delay ? new Date(Date.now() + delay) : undefined, - signal, - }) - : undefined; - - // If handleErrorResult - if (!handleErrorResult) { - return typeof delay === "undefined" - ? { status: "noop" } - : { status: "retry", retry: { timestamp: Date.now() + delay, delay } }; - } - - if (handleErrorResult.skipRetrying) { - return { status: "skipped", error: handleErrorResult.error }; - } - - if (typeof handleErrorResult.retryAt !== "undefined") { - return { - status: "retry", - retry: { - timestamp: handleErrorResult.retryAt.getTime(), - delay: handleErrorResult.retryAt.getTime() - Date.now(), - }, - error: handleErrorResult.error, - }; - } - - if (typeof handleErrorResult.retryDelayInMs === "number") { - return { - status: "retry", - retry: { - timestamp: Date.now() + handleErrorResult.retryDelayInMs, - delay: handleErrorResult.retryDelayInMs, - }, - error: handleErrorResult.error, - }; + // Try task-specific catch error hook first + const taskCatchErrorHook = lifecycleHooks.getTaskCatchErrorHook(this.task.id); + if (taskCatchErrorHook) { + const result = await taskCatchErrorHook({ + payload, + error, + ctx, + init, + retry, + retryDelayInMs: defaultDelay, + retryAt: defaultDelay ? new Date(Date.now() + defaultDelay) : undefined, + signal, + task: this.task.id, + }); + + if (result) { + return this.#processHandleErrorResult(result, execution.attempt.number, defaultDelay); + } } - if (handleErrorResult.retry && typeof handleErrorResult.retry === "object") { - const delay = calculateNextRetryDelay(handleErrorResult.retry, execution.attempt.number); - - return typeof delay === "undefined" - ? { status: "noop", error: handleErrorResult.error } - : { - status: "retry", - retry: { timestamp: Date.now() + delay, delay }, - error: handleErrorResult.error, - }; + // Try global catch error hooks in order + const globalCatchErrorHooks = lifecycleHooks.getGlobalCatchErrorHooks(); + for (const hook of globalCatchErrorHooks) { + const result = await hook.fn({ + payload, + error, + ctx, + init, + retry, + retryDelayInMs: defaultDelay, + retryAt: defaultDelay ? new Date(Date.now() + defaultDelay) : undefined, + signal, + task: this.task.id, + }); + + if (result) { + return this.#processHandleErrorResult(result, execution.attempt.number, defaultDelay); + } } - return { status: "noop", error: handleErrorResult.error }; + // If no hooks handled the error, use default retry behavior + return typeof defaultDelay === "undefined" + ? { status: "noop" as const } + : { + status: "retry" as const, + retry: { timestamp: Date.now() + defaultDelay, delay: defaultDelay }, + }; }, { attributes: { @@ -799,6 +785,56 @@ export class TaskExecutor { ); } + // Helper method to process handle error results + #processHandleErrorResult( + result: HandleErrorModificationOptions, + attemptNumber: number, + defaultDelay?: number + ): + | { status: "retry"; retry: TaskRunExecutionRetry; error?: unknown } + | { status: "skipped"; error?: unknown } + | { status: "noop"; error?: unknown } { + if (result.skipRetrying) { + return { status: "skipped", error: result.error }; + } + + if (typeof result.retryAt !== "undefined") { + return { + status: "retry", + retry: { + timestamp: result.retryAt.getTime(), + delay: result.retryAt.getTime() - Date.now(), + }, + error: result.error, + }; + } + + if (typeof result.retryDelayInMs === "number") { + return { + status: "retry", + retry: { + timestamp: Date.now() + result.retryDelayInMs, + delay: result.retryDelayInMs, + }, + error: result.error, + }; + } + + if (result.retry && typeof result.retry === "object") { + const delay = calculateNextRetryDelay(result.retry, attemptNumber); + + return typeof delay === "undefined" + ? { status: "noop", error: result.error } + : { + status: "retry", + retry: { timestamp: Date.now() + delay, delay }, + error: result.error, + }; + } + + return { status: "noop", error: result.error }; + } + async #callOnCompleteFunctions( payload: unknown, result: TaskCompleteResult, diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 975759b740..624aeefc36 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -571,6 +571,216 @@ describe("TaskExecutor", () => { }, }); }); + + test("should call catchError hooks in correct order and stop at first handler that returns a result", async () => { + const hookCallOrder: string[] = []; + const expectedError = new Error("Task failed intentionally"); + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + return { + foo: "bar", + }; + }, + }); + + // Register task-specific catch error hook that doesn't handle the error + lifecycleHooks.registerTaskCatchErrorHook("test-task", { + id: "task-catch-error", + fn: async ({ payload, error, init, retry }) => { + console.log("Executing task catch error hook"); + hookCallOrder.push("task"); + // Return undefined to let it fall through to global handlers + return undefined; + }, + }); + + // Register first global catch error hook that doesn't handle the error + lifecycleHooks.registerGlobalCatchErrorHook({ + id: "global-catch-error-1", + fn: async ({ payload, error, init, retry }) => { + console.log("Executing global catch error hook 1"); + hookCallOrder.push("global-1"); + // Return undefined to let it fall through to next handler + return undefined; + }, + }); + + // Register second global catch error hook that handles the error + lifecycleHooks.registerGlobalCatchErrorHook({ + id: "global-catch-error-2", + fn: async ({ payload, error, init, retry }) => { + console.log("Executing global catch error hook 2"); + hookCallOrder.push("global-2"); + // Return a result to handle the error + return { + retry: { + maxAttempts: 3, + minDelay: 1000, + maxDelay: 5000, + factor: 2, + }, + }; + }, + }); + + // Register third global catch error hook that should never be called + lifecycleHooks.registerGlobalCatchErrorHook({ + id: "global-catch-error-3", + fn: async ({ payload, error, init, retry }) => { + console.log("Executing global catch error hook 3"); + hookCallOrder.push("global-3"); + return undefined; + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + throw expectedError; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify hooks were called in correct order and stopped after second global hook + expect(hookCallOrder).toEqual(["task", "global-1", "global-2"]); + // global-3 should not be called since global-2 returned a result + + // Verify the final result contains retry information from the second global hook + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Task failed intentionally", + name: "Error", + stackTrace: expect.any(String), + }, + retry: { + timestamp: expect.any(Number), + delay: expect.any(Number), + }, + skippedRetrying: false, + }, + }); + }); + + test("should skip retrying if catch error hook returns skipRetrying", async () => { + const hookCallOrder: string[] = []; + const expectedError = new Error("Task failed intentionally"); + + // Register task-specific catch error hook that handles the error + lifecycleHooks.registerTaskCatchErrorHook("test-task", { + id: "task-catch-error", + fn: async ({ payload, error, init }) => { + console.log("Executing task catch error hook"); + hookCallOrder.push("task"); + return { + skipRetrying: true, + error: new Error("Modified error in catch hook"), + }; + }, + }); + + // Register global catch error hook that should never be called + lifecycleHooks.registerGlobalCatchErrorHook({ + id: "global-catch-error", + fn: async ({ payload, error, init }) => { + console.log("Executing global catch error hook"); + hookCallOrder.push("global"); + return undefined; + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + throw expectedError; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify only task hook was called + expect(hookCallOrder).toEqual(["task"]); + + // Verify the final result shows skipped retrying and the modified error + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Modified error in catch hook", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: true, + }, + }); + }); + + test("should use specific retry timing if catch error hook provides it", async () => { + const hookCallOrder: string[] = []; + const expectedError = new Error("Task failed intentionally"); + const specificRetryDate = new Date(Date.now() + 30000); // 30 seconds in future + + // Register task-specific catch error hook that specifies retry timing + lifecycleHooks.registerTaskCatchErrorHook("test-task", { + id: "task-catch-error", + fn: async ({ payload, error, init }) => { + console.log("Executing task catch error hook"); + hookCallOrder.push("task"); + return { + retryAt: specificRetryDate, + error: expectedError, + }; + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + throw expectedError; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify only task hook was called + expect(hookCallOrder).toEqual(["task"]); + + // Verify the final result contains the specific retry timing + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Task failed intentionally", + name: "Error", + stackTrace: expect.any(String), + }, + retry: { + timestamp: specificRetryDate.getTime(), + delay: expect.any(Number), + }, + skippedRetrying: false, + }, + }); + + expect((result as any).result.retry.delay).toBeCloseTo(30000, -1); + }); }); function executeTask(task: TaskMetadataWithFunctions, payload: any) { From 7f804f381c0ba10807f81af167b4073ccc228cfe Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 10:58:16 +0000 Subject: [PATCH 27/40] middleware working and tests passing --- .../run-engine/src/engine/errors.ts | 5 +- packages/core/src/v3/errors.ts | 1 + packages/core/src/v3/schemas/common.ts | 1 + packages/core/src/v3/workers/taskExecutor.ts | 321 ++++++++++-------- packages/core/test/taskExecutor.test.ts | 172 ++++++++++ 5 files changed, 358 insertions(+), 142 deletions(-) diff --git a/internal-packages/run-engine/src/engine/errors.ts b/internal-packages/run-engine/src/engine/errors.ts index 81e3b598bd..e1dd34eac4 100644 --- a/internal-packages/run-engine/src/engine/errors.ts +++ b/internal-packages/run-engine/src/engine/errors.ts @@ -13,6 +13,9 @@ export function runStatusFromError(error: TaskRunError): TaskRunStatus { //e.g. a bug switch (error.code) { case "RECURSIVE_WAIT_DEADLOCK": + case "TASK_INPUT_ERROR": + case "TASK_OUTPUT_ERROR": + case "TASK_MIDDLEWARE_ERROR": return "COMPLETED_WITH_ERRORS"; case "TASK_RUN_CANCELLED": return "CANCELED"; @@ -41,8 +44,6 @@ export function runStatusFromError(error: TaskRunError): TaskRunStatus { case "TASK_RUN_STALLED_EXECUTING_WITH_WAITPOINTS": case "TASK_HAS_N0_EXECUTION_SNAPSHOT": case "GRACEFUL_EXIT_TIMEOUT": - case "TASK_INPUT_ERROR": - case "TASK_OUTPUT_ERROR": case "POD_EVICTED": case "POD_UNKNOWN_ERROR": case "TASK_EXECUTION_ABORTED": diff --git a/packages/core/src/v3/errors.ts b/packages/core/src/v3/errors.ts index a8e789f81d..bf9776d1c2 100644 --- a/packages/core/src/v3/errors.ts +++ b/packages/core/src/v3/errors.ts @@ -305,6 +305,7 @@ export function shouldRetryError(error: TaskRunError): boolean { case "HANDLE_ERROR_ERROR": case "TASK_INPUT_ERROR": case "TASK_OUTPUT_ERROR": + case "TASK_MIDDLEWARE_ERROR": case "POD_EVICTED": case "POD_UNKNOWN_ERROR": case "TASK_EXECUTION_ABORTED": diff --git a/packages/core/src/v3/schemas/common.ts b/packages/core/src/v3/schemas/common.ts index 43030847dc..5467603e9d 100644 --- a/packages/core/src/v3/schemas/common.ts +++ b/packages/core/src/v3/schemas/common.ts @@ -165,6 +165,7 @@ export const TaskRunInternalError = z.object({ "TASK_RUN_CANCELLED", "TASK_INPUT_ERROR", "TASK_OUTPUT_ERROR", + "TASK_MIDDLEWARE_ERROR", "HANDLE_ERROR_ERROR", "GRACEFUL_EXIT_TIMEOUT", "TASK_RUN_HEARTBEAT_TIMEOUT", diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index f24121ccce..d9b07a4f60 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -4,7 +4,12 @@ import { ApiError, RateLimitError } from "../apiClient/errors.js"; import { ConsoleInterceptor } from "../consoleInterceptor.js"; import { isInternalError, parseError, sanitizeError, TaskPayloadParsedError } from "../errors.js"; import { flattenAttributes, lifecycleHooks, runMetadata, waitUntil } from "../index.js"; -import { TaskCompleteResult, TaskInitOutput } from "../lifecycleHooks/types.js"; +import { + AnyOnMiddlewareHookFunction, + RegisteredHookFunction, + TaskCompleteResult, + TaskInitOutput, +} from "../lifecycleHooks/types.js"; import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; import { @@ -111,155 +116,146 @@ export class TaskExecutor { } catch (inputError) { recordSpanException(span, inputError); - return { - ok: false, - id: execution.run.id, - error: { - type: "INTERNAL_ERROR", - code: TaskRunErrorCodes.TASK_INPUT_ERROR, - message: - inputError instanceof Error - ? `${inputError.name}: ${inputError.message}` - : typeof inputError === "string" - ? inputError - : undefined, - stackTrace: inputError instanceof Error ? inputError.stack : undefined, - }, - } satisfies TaskRunExecutionResult; + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.TASK_INPUT_ERROR, + inputError + ); } - try { - parsedPayload = await this.#parsePayload(parsedPayload); - - initOutput = await this.#callInitFunctions(parsedPayload, ctx, signal); - - if (execution.attempt.number === 1) { - await this.#callOnStartFunctions(parsedPayload, ctx, initOutput, signal); - } - - const output = await this.#callRun(parsedPayload, ctx, initOutput, signal); + parsedPayload = await this.#parsePayload(parsedPayload); + const executeTask = async (payload: any) => { try { - const stringifiedOutput = await stringifyIO(output); + initOutput = await this.#callInitFunctions(payload, ctx, signal); - const finalOutput = await conditionallyExportPacket( - stringifiedOutput, - `${execution.attempt.id}/output`, - this._tracer - ); + if (execution.attempt.number === 1) { + await this.#callOnStartFunctions(payload, ctx, initOutput, signal); + } - const attributes = await createPacketAttributes( - finalOutput, - SemanticInternalAttributes.OUTPUT, - SemanticInternalAttributes.OUTPUT_TYPE - ); + const output = await this.#callRun(payload, ctx, initOutput, signal); - if (attributes) { - span.setAttributes(attributes); - } + try { + const stringifiedOutput = await stringifyIO(output); - await this.#callOnSuccessFunctions(parsedPayload, output, ctx, initOutput, signal); + const finalOutput = await conditionallyExportPacket( + stringifiedOutput, + `${execution.attempt.id}/output`, + this._tracer + ); - // Call onComplete with success result - await this.#callOnCompleteFunctions( - parsedPayload, - { ok: true, data: output }, - ctx, - initOutput, - signal - ); + const attributes = await createPacketAttributes( + finalOutput, + SemanticInternalAttributes.OUTPUT, + SemanticInternalAttributes.OUTPUT_TYPE + ); - return { - ok: true, - id: execution.run.id, - output: finalOutput.data, - outputType: finalOutput.dataType, - } satisfies TaskRunExecutionResult; - } catch (outputError) { - recordSpanException(span, outputError); - - return { - ok: false, - id: execution.run.id, - error: { - type: "INTERNAL_ERROR", - code: TaskRunErrorCodes.TASK_OUTPUT_ERROR, - message: - outputError instanceof Error - ? outputError.message - : typeof outputError === "string" - ? outputError - : undefined, - }, - } satisfies TaskRunExecutionResult; - } - } catch (runError) { - try { - const handleErrorResult = await this.#handleError( - execution, - runError, - parsedPayload, - ctx, - initOutput, - signal - ); + if (attributes) { + span.setAttributes(attributes); + } - recordSpanException(span, handleErrorResult.error ?? runError); + await this.#callOnSuccessFunctions(payload, output, ctx, initOutput, signal); - if (handleErrorResult.status !== "retry") { - await this.#callOnFailureFunctions( - parsedPayload, - handleErrorResult.error ?? runError, + // Call onComplete with success result + await this.#callOnCompleteFunctions( + payload, + { ok: true, data: output }, ctx, initOutput, signal ); - // Call onComplete with error result - await this.#callOnCompleteFunctions( - parsedPayload, - { ok: false, error: handleErrorResult.error ?? runError }, + return { + ok: true, + id: execution.run.id, + output: finalOutput.data, + outputType: finalOutput.dataType, + } satisfies TaskRunExecutionResult; + } catch (outputError) { + recordSpanException(span, outputError); + + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.TASK_OUTPUT_ERROR, + outputError + ); + } + } catch (runError) { + try { + const handleErrorResult = await this.#handleError( + execution, + runError, + payload, ctx, initOutput, signal ); + + recordSpanException(span, handleErrorResult.error ?? runError); + + if (handleErrorResult.status !== "retry") { + await this.#callOnFailureFunctions( + payload, + handleErrorResult.error ?? runError, + ctx, + initOutput, + signal + ); + + // Call onComplete with error result + await this.#callOnCompleteFunctions( + payload, + { ok: false, error: handleErrorResult.error ?? runError }, + ctx, + initOutput, + signal + ); + } + + return { + id: execution.run.id, + ok: false, + error: sanitizeError( + handleErrorResult.error + ? parseError(handleErrorResult.error) + : parseError(runError) + ), + retry: handleErrorResult.status === "retry" ? handleErrorResult.retry : undefined, + skippedRetrying: handleErrorResult.status === "skipped", + } satisfies TaskRunExecutionResult; + } catch (handleErrorError) { + recordSpanException(span, handleErrorError); + + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.HANDLE_ERROR_ERROR, + handleErrorError + ); } + } finally { + await this.#callTaskCleanup(payload, ctx, initOutput, signal); + await this.#blockForWaitUntil(); - return { - id: execution.run.id, - ok: false, - error: sanitizeError( - handleErrorResult.error - ? parseError(handleErrorResult.error) - : parseError(runError) - ), - retry: handleErrorResult.status === "retry" ? handleErrorResult.retry : undefined, - skippedRetrying: handleErrorResult.status === "skipped", - } satisfies TaskRunExecutionResult; - } catch (handleErrorError) { - recordSpanException(span, handleErrorError); - - return { - ok: false, - id: execution.run.id, - error: { - type: "INTERNAL_ERROR", - code: TaskRunErrorCodes.HANDLE_ERROR_ERROR, - message: - handleErrorError instanceof Error - ? handleErrorError.message - : typeof handleErrorError === "string" - ? handleErrorError - : undefined, - }, - } satisfies TaskRunExecutionResult; + span.setAttributes(runTimelineMetrics.convertMetricsToSpanAttributes()); } - } finally { - await this.#callTaskCleanup(parsedPayload, ctx, initOutput, signal); - await this.#blockForWaitUntil(); + }; - span.setAttributes(runTimelineMetrics.convertMetricsToSpanAttributes()); - } + const globalMiddlewareHooks = lifecycleHooks.getGlobalMiddlewareHooks(); + const taskMiddlewareHook = lifecycleHooks.getTaskMiddlewareHook(this.task.id); + + const middlewareHooks = [ + ...globalMiddlewareHooks, + taskMiddlewareHook ? { id: this.task.id, fn: taskMiddlewareHook } : undefined, + ].filter(Boolean) as RegisteredHookFunction[]; + + return await this.#executeTaskWithMiddlewareHooks( + parsedPayload, + ctx, + execution, + middlewareHooks, + executeTask, + signal + ); }); }, { @@ -283,28 +279,55 @@ export class TaskExecutor { return { result }; } + async #executeTaskWithMiddlewareHooks( + payload: unknown, + ctx: TaskRunContext, + execution: TaskRunExecution, + hooks: RegisteredHookFunction[], + executeTask: (payload: unknown) => Promise, + signal?: AbortSignal + ) { + let output: any; + let executeError: unknown; + + const runner = hooks.reduceRight( + (next, hook) => { + return async () => { + await hook.fn({ payload, ctx, signal, task: this.task.id, next }); + }; + }, + async () => { + try { + output = await executeTask(payload); + } catch (error) { + executeError = error; + } + } + ); + + try { + await runner(); + } catch (error) { + return this.#internalErrorResult(execution, TaskRunErrorCodes.TASK_MIDDLEWARE_ERROR, error); + } + + if (executeError) { + throw executeError; + } + + return output; + } + async #callRun(payload: unknown, ctx: TaskRunContext, init: unknown, signal?: AbortSignal) { const runFn = this.task.fns.run; - const middlewareFn = this.task.fns.middleware; if (!runFn) { throw new Error("Task does not have a run function"); } - if (!middlewareFn) { - return runTimelineMetrics.measureMetric("trigger.dev/execution", "run", () => - runFn(payload, { ctx, init, signal }) - ); - } - - return middlewareFn(payload, { - ctx, - signal, - next: async () => - runTimelineMetrics.measureMetric("trigger.dev/execution", "run", () => - runFn(payload, { ctx, init, signal }) - ), - }); + return runTimelineMetrics.measureMetric("trigger.dev/execution", "run", () => + runFn(payload, { ctx, init, signal }) + ); } async #callInitFunctions(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { @@ -915,4 +938,22 @@ export class TaskExecutor { } ); } + + #internalErrorResult(execution: TaskRunExecution, code: TaskRunErrorCodes, error: unknown) { + return { + ok: false, + id: execution.run.id, + error: { + type: "INTERNAL_ERROR", + code, + message: + error instanceof Error + ? `${error.name}: ${error.message}` + : typeof error === "string" + ? error + : undefined, + stackTrace: error instanceof Error ? error.stack : undefined, + }, + } satisfies TaskRunExecutionResult; + } } diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 624aeefc36..26f397763b 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -781,6 +781,178 @@ describe("TaskExecutor", () => { expect((result as any).result.retry.delay).toBeCloseTo(30000, -1); }); + + test("should execute middleware hooks in correct order around other hooks", async () => { + const executionOrder: string[] = []; + + // Register global init hook + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + executionOrder.push("init"); + return { + foo: "bar", + }; + }, + }); + + // Register global start hook + lifecycleHooks.registerGlobalStartHook({ + id: "global-start", + fn: async ({ payload }) => { + executionOrder.push("start"); + }, + }); + + // Register global success hook + lifecycleHooks.registerGlobalSuccessHook({ + id: "global-success", + fn: async ({ payload, output }) => { + executionOrder.push("success"); + }, + }); + + // Register global complete hook + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async ({ payload, result }) => { + executionOrder.push("complete"); + }, + }); + + // Register task-specific middleware that executes first + lifecycleHooks.registerTaskMiddlewareHook("test-task", { + id: "task-middleware", + fn: async ({ payload, ctx, next }) => { + executionOrder.push("task-middleware-before"); + await next(); + executionOrder.push("task-middleware-after"); + }, + }); + + // Register two global middleware hooks + lifecycleHooks.registerGlobalMiddlewareHook({ + id: "global-middleware-1", + fn: async ({ payload, ctx, next }) => { + executionOrder.push("global-middleware-1-before"); + await next(); + executionOrder.push("global-middleware-1-after"); + }, + }); + + lifecycleHooks.registerGlobalMiddlewareHook({ + id: "global-middleware-2", + fn: async ({ payload, ctx, next }) => { + executionOrder.push("global-middleware-2-before"); + await next(); + executionOrder.push("global-middleware-2-after"); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run"); + return { + output: "test-output", + init: params.init, + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify the execution order: + // 1. Global middlewares (outside to inside) + // 2. Task middleware + // 3. Init hook + // 4. Start hook + // 5. Run function + // 6. Success hook + // 7. Complete hook + // 8. Middlewares in reverse order + expect(executionOrder).toEqual([ + "global-middleware-1-before", + "global-middleware-2-before", + "task-middleware-before", + "init", + "start", + "run", + "success", + "complete", + "task-middleware-after", + "global-middleware-2-after", + "global-middleware-1-after", + ]); + + // Verify the final result + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"output":"test-output","init":{"foo":"bar"}}}', + outputType: "application/super+json", + }, + }); + }); + + test("should handle middleware errors correctly", async () => { + const executionOrder: string[] = []; + const expectedError = new Error("Middleware error"); + + // Register global middleware that throws an error + lifecycleHooks.registerGlobalMiddlewareHook({ + id: "global-middleware", + fn: async ({ payload, ctx, next }) => { + executionOrder.push("middleware-before"); + throw expectedError; + // Should never get here + await next(); + executionOrder.push("middleware-after"); + }, + }); + + // Register failure hook to verify it's called + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure", + fn: async ({ payload, error }) => { + executionOrder.push("failure"); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run"); + return { + output: "test-output", + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify only the middleware-before hook ran + expect(executionOrder).toEqual(["middleware-before"]); + + // Verify the error result + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "INTERNAL_ERROR", + message: "Error: Middleware error", + code: "TASK_MIDDLEWARE_ERROR", + stackTrace: expect.any(String), + }, + }, + }); + }); }); function executeTask(task: TaskMetadataWithFunctions, payload: any) { From 5edbb77dfe12bfbdc02303229b93a05fbc327d21 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 11:05:45 +0000 Subject: [PATCH 28/40] use tryCatch in TaskExecutor --- packages/core/src/v3/workers/taskExecutor.ts | 462 ++++++++++--------- 1 file changed, 241 insertions(+), 221 deletions(-) diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index d9b07a4f60..7297a394e5 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -37,6 +37,7 @@ import { stringifyIO, } from "../utils/ioSerialization.js"; import { calculateNextRetryDelay } from "../utils/retries.js"; +import { tryCatch } from "../tryCatch.js"; export type TaskExecutorOptions = { tracingSDK: TracingSDK; @@ -108,14 +109,15 @@ export class TaskExecutor { let parsedPayload: any; let initOutput: any; - try { - await runTimelineMetrics.measureMetric("trigger.dev/execution", "payload", async () => { + const [inputError, payloadResult] = await tryCatch( + runTimelineMetrics.measureMetric("trigger.dev/execution", "payload", async () => { const payloadPacket = await conditionallyImportPacket(originalPacket, this._tracer); - parsedPayload = await parsePacket(payloadPacket); - }); - } catch (inputError) { - recordSpanException(span, inputError); + return await parsePacket(payloadPacket); + }) + ); + if (inputError) { + recordSpanException(span, inputError); return this.#internalErrorResult( execution, TaskRunErrorCodes.TASK_INPUT_ERROR, @@ -123,121 +125,123 @@ export class TaskExecutor { ); } - parsedPayload = await this.#parsePayload(parsedPayload); + parsedPayload = await this.#parsePayload(payloadResult); const executeTask = async (payload: any) => { - try { - initOutput = await this.#callInitFunctions(payload, ctx, signal); - - if (execution.attempt.number === 1) { - await this.#callOnStartFunctions(payload, ctx, initOutput, signal); - } + const [runError, output] = await tryCatch( + (async () => { + initOutput = await this.#callInitFunctions(payload, ctx, signal); - const output = await this.#callRun(payload, ctx, initOutput, signal); + if (execution.attempt.number === 1) { + await this.#callOnStartFunctions(payload, ctx, initOutput, signal); + } - try { - const stringifiedOutput = await stringifyIO(output); + return await this.#callRun(payload, ctx, initOutput, signal); + })() + ); - const finalOutput = await conditionallyExportPacket( - stringifiedOutput, - `${execution.attempt.id}/output`, - this._tracer - ); + if (runError) { + const [handleErrorError, handleErrorResult] = await tryCatch( + this.#handleError(execution, runError, payload, ctx, initOutput, signal) + ); - const attributes = await createPacketAttributes( - finalOutput, - SemanticInternalAttributes.OUTPUT, - SemanticInternalAttributes.OUTPUT_TYPE + if (handleErrorError) { + recordSpanException(span, handleErrorError); + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.HANDLE_ERROR_ERROR, + handleErrorError ); + } - if (attributes) { - span.setAttributes(attributes); - } - - await this.#callOnSuccessFunctions(payload, output, ctx, initOutput, signal); + recordSpanException(span, handleErrorResult.error ?? runError); - // Call onComplete with success result - await this.#callOnCompleteFunctions( + if (handleErrorResult.status !== "retry") { + await this.#callOnFailureFunctions( payload, - { ok: true, data: output }, + handleErrorResult.error ?? runError, ctx, initOutput, signal ); - return { - ok: true, - id: execution.run.id, - output: finalOutput.data, - outputType: finalOutput.dataType, - } satisfies TaskRunExecutionResult; - } catch (outputError) { - recordSpanException(span, outputError); - - return this.#internalErrorResult( - execution, - TaskRunErrorCodes.TASK_OUTPUT_ERROR, - outputError - ); - } - } catch (runError) { - try { - const handleErrorResult = await this.#handleError( - execution, - runError, + await this.#callOnCompleteFunctions( payload, + { ok: false, error: handleErrorResult.error ?? runError }, ctx, initOutput, signal ); + } - recordSpanException(span, handleErrorResult.error ?? runError); - - if (handleErrorResult.status !== "retry") { - await this.#callOnFailureFunctions( - payload, - handleErrorResult.error ?? runError, - ctx, - initOutput, - signal - ); - - // Call onComplete with error result - await this.#callOnCompleteFunctions( - payload, - { ok: false, error: handleErrorResult.error ?? runError }, - ctx, - initOutput, - signal - ); - } + return { + id: execution.run.id, + ok: false, + error: sanitizeError( + handleErrorResult.error + ? parseError(handleErrorResult.error) + : parseError(runError) + ), + retry: handleErrorResult.status === "retry" ? handleErrorResult.retry : undefined, + skippedRetrying: handleErrorResult.status === "skipped", + } satisfies TaskRunExecutionResult; + } - return { - id: execution.run.id, - ok: false, - error: sanitizeError( - handleErrorResult.error - ? parseError(handleErrorResult.error) - : parseError(runError) - ), - retry: handleErrorResult.status === "retry" ? handleErrorResult.retry : undefined, - skippedRetrying: handleErrorResult.status === "skipped", - } satisfies TaskRunExecutionResult; - } catch (handleErrorError) { - recordSpanException(span, handleErrorError); + const [outputError, stringifiedOutput] = await tryCatch(stringifyIO(output)); - return this.#internalErrorResult( - execution, - TaskRunErrorCodes.HANDLE_ERROR_ERROR, - handleErrorError - ); - } - } finally { - await this.#callTaskCleanup(payload, ctx, initOutput, signal); - await this.#blockForWaitUntil(); + if (outputError) { + recordSpanException(span, outputError); + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.TASK_OUTPUT_ERROR, + outputError + ); + } - span.setAttributes(runTimelineMetrics.convertMetricsToSpanAttributes()); + const [exportError, finalOutput] = await tryCatch( + conditionallyExportPacket( + stringifiedOutput, + `${execution.attempt.id}/output`, + this._tracer + ) + ); + + if (exportError) { + recordSpanException(span, exportError); + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.TASK_OUTPUT_ERROR, + exportError + ); } + + const [attrError, attributes] = await tryCatch( + createPacketAttributes( + finalOutput, + SemanticInternalAttributes.OUTPUT, + SemanticInternalAttributes.OUTPUT_TYPE + ) + ); + + if (!attrError && attributes) { + span.setAttributes(attributes); + } + + await this.#callOnSuccessFunctions(payload, output, ctx, initOutput, signal); + await this.#callOnCompleteFunctions( + payload, + { ok: true, data: output }, + ctx, + initOutput, + signal + ); + + return { + ok: true, + id: execution.run.id, + output: finalOutput.data, + outputType: finalOutput.dataType, + } satisfies TaskRunExecutionResult; }; const globalMiddlewareHooks = lifecycleHooks.getGlobalMiddlewareHooks(); @@ -297,18 +301,22 @@ export class TaskExecutor { }; }, async () => { - try { - output = await executeTask(payload); - } catch (error) { + const [error, result] = await tryCatch(executeTask(payload)); + if (error) { executeError = error; + } else { + output = result; } } ); - try { - await runner(); - } catch (error) { - return this.#internalErrorResult(execution, TaskRunErrorCodes.TASK_MIDDLEWARE_ERROR, error); + const [runnerError] = await tryCatch(runner()); + if (runnerError) { + return this.#internalErrorResult( + execution, + TaskRunErrorCodes.TASK_MIDDLEWARE_ERROR, + runnerError + ); } if (executeError) { @@ -348,27 +356,28 @@ export class TaskExecutor { // Store global hook results in an array const globalResults = []; for (const hook of globalInitHooks) { - const result = await this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - const result = await hook.fn({ payload, ctx, signal, task: this.task.id }); + const [hookError, result] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + const result = await hook.fn({ payload, ctx, signal, task: this.task.id }); - if (result && typeof result === "object" && !Array.isArray(result)) { - span.setAttributes(flattenAttributes(result)); + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + return result; + } - return result; - } - - return {}; - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + return {}; }, - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) ); - // Only include object results - if (result && typeof result === "object" && !Array.isArray(result)) { + + if (!hookError && result && typeof result === "object" && !Array.isArray(result)) { globalResults.push(result); } } @@ -377,28 +386,34 @@ export class TaskExecutor { const mergedGlobalResults = Object.assign({}, ...globalResults); if (taskInitHook) { - const taskResult = await this._tracer.startActiveSpan( - "task", - async (span) => { - const result = await taskInitHook({ payload, ctx, signal, task: this.task.id }); - - if (result && typeof result === "object" && !Array.isArray(result)) { - span.setAttributes(flattenAttributes(result)); + const [hookError, taskResult] = await tryCatch( + this._tracer.startActiveSpan( + "task", + async (span) => { + const result = await taskInitHook({ payload, ctx, signal, task: this.task.id }); - return result; - } + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + return result; + } - return {}; - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + return {}; }, - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) ); // Only merge if taskResult is an object - if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { + if ( + !hookError && + taskResult && + typeof taskResult === "object" && + !Array.isArray(taskResult) + ) { return { ...mergedGlobalResults, ...taskResult }; } @@ -412,7 +427,6 @@ export class TaskExecutor { if (result && typeof result === "object" && !Array.isArray(result)) { span.setAttributes(flattenAttributes(result)); - return result; } @@ -449,45 +463,51 @@ export class TaskExecutor { "success", async () => { for (const hook of globalSuccessHooks) { - await this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ - payload, - output, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ + payload, + output, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); }, - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) ); + // Ignore errors from onSuccess functions } if (taskSuccessHook) { - await this._tracer.startActiveSpan( - "task", - async (span) => { - await taskSuccessHook({ - payload, - output, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "task", + async (span) => { + await taskSuccessHook({ + payload, + output, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); }, - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) ); + // Ignore errors from onSuccess functions } } ); @@ -522,8 +542,8 @@ export class TaskExecutor { "failure", async () => { for (const hook of globalFailureHooks) { - try { - await this._tracer.startActiveSpan( + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( hook.name ?? "global", async (span) => { await hook.fn({ @@ -540,15 +560,14 @@ export class TaskExecutor { [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, } - ); - } catch { - // Ignore errors from onFailure functions - } + ) + ); + // Ignore errors from onFailure functions } if (taskFailureHook) { - try { - await this._tracer.startActiveSpan( + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( "task", async (span) => { await taskFailureHook({ @@ -565,10 +584,9 @@ export class TaskExecutor { [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, } - ); - } catch { - // Ignore errors from onFailure functions - } + ) + ); + // Ignore errors from onFailure functions } } ); @@ -586,11 +604,11 @@ export class TaskExecutor { return payload; } - try { - return await this.task.fns.parsePayload(payload); - } catch (e) { - throw new TaskPayloadParsedError(e); + const [parseError, result] = await tryCatch(this.task.fns.parsePayload(payload)); + if (parseError) { + throw new TaskPayloadParsedError(parseError); } + return result; } async #callOnStartFunctions( @@ -614,36 +632,40 @@ export class TaskExecutor { "start", async () => { for (const hook of globalStartHooks) { - await this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ payload, ctx, signal, task: this.task.id, init: initOutput }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ payload, ctx, signal, task: this.task.id, init: initOutput }); }, - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) ); } if (taskStartHook) { - await this._tracer.startActiveSpan( - "task", - async (span) => { - await taskStartHook({ - payload, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "task", + async (span) => { + await taskStartHook({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); }, - } + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) ); } } @@ -880,8 +902,8 @@ export class TaskExecutor { "complete", async () => { for (const hook of globalCompleteHooks) { - try { - await this._tracer.startActiveSpan( + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( hook.name ?? "global", async (span) => { await hook.fn({ @@ -898,15 +920,14 @@ export class TaskExecutor { [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, } - ); - } catch { - // Ignore errors from onComplete functions - } + ) + ); + // Ignore errors from onComplete functions } if (taskCompleteHook) { - try { - await this._tracer.startActiveSpan( + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( "task", async (span) => { await taskCompleteHook({ @@ -923,10 +944,9 @@ export class TaskExecutor { [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", }, } - ); - } catch { - // Ignore errors from onComplete functions - } + ) + ); + // Ignore errors from onComplete functions } } ); From 6ba2b0476b2014bc14adc16a80743d4dbabe23f5 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 11:18:27 +0000 Subject: [PATCH 29/40] more tests --- .cursor/rules/executing-commands.mdc | 4 +- .../cli-v3/src/entryPoints/dev-run-worker.ts | 10 +- packages/core/src/v3/workers/taskExecutor.ts | 25 +- packages/core/test/taskExecutor.test.ts | 250 ++++++++++++++++++ 4 files changed, 272 insertions(+), 17 deletions(-) diff --git a/.cursor/rules/executing-commands.mdc b/.cursor/rules/executing-commands.mdc index eac17379e4..0d36b44949 100644 --- a/.cursor/rules/executing-commands.mdc +++ b/.cursor/rules/executing-commands.mdc @@ -13,12 +13,12 @@ But often, when running tests, it's better to `cd` into the directory and then r ``` cd apps/webapp -pnpm run test +pnpm run test --run ``` This way you can run for a single file easily: ``` cd internal-packages/run-engine -pnpm run test ./src/engine/tests/ttl.test.ts +pnpm run test ./src/engine/tests/ttl.test.ts --run ``` diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 5952e86512..196d1ba3f1 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -358,7 +358,7 @@ const zodIpc = new ZodIpcConnection({ tracer, tracingSDK, consoleInterceptor, - config, + retries: config.retries, handleErrorFn, }); @@ -406,13 +406,7 @@ const zodIpc = new ZodIpcConnection({ } }); - const { result } = await executor.execute( - execution, - metadata, - traceContext, - measurement, - signal - ); + const { result } = await executor.execute(execution, metadata, traceContext, signal); const usageSample = usage.stop(measurement); diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 7297a394e5..21ec33bc2f 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -377,7 +377,11 @@ export class TaskExecutor { ) ); - if (!hookError && result && typeof result === "object" && !Array.isArray(result)) { + if (hookError) { + throw hookError; + } + + if (result && typeof result === "object" && !Array.isArray(result)) { globalResults.push(result); } } @@ -407,13 +411,12 @@ export class TaskExecutor { ) ); + if (hookError) { + throw hookError; + } + // Only merge if taskResult is an object - if ( - !hookError && - taskResult && - typeof taskResult === "object" && - !Array.isArray(taskResult) - ) { + if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { return { ...mergedGlobalResults, ...taskResult }; } @@ -645,6 +648,10 @@ export class TaskExecutor { } ) ); + + if (hookError) { + throw hookError; + } } if (taskStartHook) { @@ -667,6 +674,10 @@ export class TaskExecutor { } ) ); + + if (hookError) { + throw hookError; + } } } ); diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 26f397763b..ad0b701174 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -953,6 +953,256 @@ describe("TaskExecutor", () => { }, }); }); + + test("should propagate errors from init hooks", async () => { + const executionOrder: string[] = []; + const expectedError = new Error("Init hook error"); + + // Register global init hook that throws an error + lifecycleHooks.registerGlobalInitHook({ + id: "failing-init", + fn: async () => { + executionOrder.push("global-init"); + throw expectedError; + }, + }); + + // Register task init hook that should never be called + lifecycleHooks.registerTaskInitHook("test-task", { + id: "task-init", + fn: async () => { + executionOrder.push("task-init"); + return { + foo: "bar", + }; + }, + }); + + // Register failure hook to verify it's called + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure", + fn: async ({ error }) => { + executionOrder.push("failure"); + expect(error).toBe(expectedError); + }, + }); + + // Register complete hook to verify it's called with error + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async ({ result }) => { + executionOrder.push("complete"); + expect(result).toEqual({ + ok: false, + error: expectedError, + }); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run"); + return { + output: "test-output", + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify only the global init hook ran, and failure/complete hooks were called + expect(executionOrder).toEqual(["global-init", "failure", "complete"]); + + // Verify the error result + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Init hook error", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); + + test("should propagate errors from task init hooks", async () => { + const executionOrder: string[] = []; + const expectedError = new Error("Task init hook error"); + + // Register global init hook that succeeds + lifecycleHooks.registerGlobalInitHook({ + id: "global-init", + fn: async () => { + executionOrder.push("global-init"); + return { + foo: "bar", + }; + }, + }); + + // Register task init hook that throws an error + lifecycleHooks.registerTaskInitHook("test-task", { + id: "task-init", + fn: async () => { + executionOrder.push("task-init"); + throw expectedError; + }, + }); + + // Register failure hook to verify it's called + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure", + fn: async ({ error, init }) => { + executionOrder.push("failure"); + expect(error).toBe(expectedError); + // Verify we got the global init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register complete hook to verify it's called with error + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async ({ result, init }) => { + executionOrder.push("complete"); + expect(result).toEqual({ + ok: false, + error: expectedError, + }); + // Verify we got the global init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run"); + return { + output: "test-output", + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify both init hooks ran, but run wasn't called, and failure/complete hooks were called + expect(executionOrder).toEqual(["global-init", "task-init", "failure", "complete"]); + + // Verify the error result + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Task init hook error", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); + + test("should propagate errors from start hooks", async () => { + const executionOrder: string[] = []; + const expectedError = new Error("Start hook error"); + + // Register global init hook that succeeds + lifecycleHooks.registerGlobalInitHook({ + id: "global-init", + fn: async () => { + executionOrder.push("global-init"); + return { + foo: "bar", + }; + }, + }); + + // Register global start hook that throws an error + lifecycleHooks.registerGlobalStartHook({ + id: "global-start", + fn: async () => { + executionOrder.push("global-start"); + throw expectedError; + }, + }); + + // Register task start hook that should never be called + lifecycleHooks.registerTaskStartHook("test-task", { + id: "task-start", + fn: async () => { + executionOrder.push("task-start"); + }, + }); + + // Register failure hook to verify it's called + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure", + fn: async ({ error, init }) => { + executionOrder.push("failure"); + expect(error).toBe(expectedError); + // Verify we got the init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register complete hook to verify it's called with error + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async ({ result, init }) => { + executionOrder.push("complete"); + expect(result).toEqual({ + ok: false, + error: expectedError, + }); + // Verify we got the init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run"); + return { + output: "test-output", + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify init succeeded, start hook failed, and run wasn't called + expect(executionOrder).toEqual(["global-init", "global-start", "failure", "complete"]); + + // Verify the error result + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Start hook error", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); }); function executeTask(task: TaskMetadataWithFunctions, payload: any) { From d0a5c16c63ae4bc9aaddca0d1f8e248557e6246d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 11:22:11 +0000 Subject: [PATCH 30/40] Add cleanup hook --- packages/core/src/v3/lifecycle-hooks-api.ts | 3 + .../core/src/v3/lifecycleHooks/adapters.ts | 16 ++++++ packages/core/src/v3/lifecycleHooks/index.ts | 22 ++++++++ .../core/src/v3/lifecycleHooks/manager.ts | 56 +++++++++++++++++++ packages/core/src/v3/lifecycleHooks/types.ts | 24 ++++++++ packages/trigger-sdk/src/v3/shared.ts | 6 ++ 6 files changed, 127 insertions(+) diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index b69382ae71..847dc56580 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -28,6 +28,9 @@ export type { TaskMiddlewareHookParams, AnyOnMiddlewareHookFunction, OnMiddlewareHookFunction, + OnCleanupHookFunction, + AnyOnCleanupHookFunction, + TaskCleanupHookParams, } from "./lifecycleHooks/types.js"; export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts index a90cabee35..b3baadec6c 100644 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ b/packages/core/src/v3/lifecycleHooks/adapters.ts @@ -11,6 +11,8 @@ import { TaskFailureHookParams, TaskStartHookParams, TaskCatchErrorHookParams, + TaskCleanupHookParams, + AnyOnCleanupHookFunction, } from "./types.js"; export function createInitHookAdapter( @@ -95,3 +97,17 @@ export function createMiddlewareHookAdapter( }); }; } + +export function createCleanupHookAdapter< + TPayload, + TInitOutput extends TaskInitOutput = TaskInitOutput, +>( + fn: NonNullable["cleanup"]> +): AnyOnCleanupHookFunction { + return async (params) => { + return await fn( + params.payload as unknown as TPayload, + params as TaskCleanupHookParams + ); + }; +} diff --git a/packages/core/src/v3/lifecycleHooks/index.ts b/packages/core/src/v3/lifecycleHooks/index.ts index 721322f639..5607623f14 100644 --- a/packages/core/src/v3/lifecycleHooks/index.ts +++ b/packages/core/src/v3/lifecycleHooks/index.ts @@ -4,6 +4,7 @@ import { getGlobal, registerGlobal, unregisterGlobal } from "../utils/globals.js import { NoopLifecycleHooksManager } from "./manager.js"; import { AnyOnCatchErrorHookFunction, + AnyOnCleanupHookFunction, AnyOnCompleteHookFunction, AnyOnFailureHookFunction, AnyOnInitHookFunction, @@ -221,6 +222,27 @@ export class LifecycleHooksAPI { return this.#getManager().getGlobalMiddlewareHooks(); } + public registerGlobalCleanupHook( + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerGlobalCleanupHook(hook); + } + + public registerTaskCleanupHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + this.#getManager().registerTaskCleanupHook(taskId, hook); + } + + public getTaskCleanupHook(taskId: string): AnyOnCleanupHookFunction | undefined { + return this.#getManager().getTaskCleanupHook(taskId); + } + + public getGlobalCleanupHooks(): RegisteredHookFunction[] { + return this.#getManager().getGlobalCleanupHooks(); + } + #getManager(): LifecycleHooksManager { return getGlobal(API_NAME) ?? NOOP_LIFECYCLE_HOOKS_MANAGER; } diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index f8e4096b98..37794ed938 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -11,6 +11,7 @@ import { AnyOnResumeHookFunction, AnyOnCatchErrorHookFunction, AnyOnMiddlewareHookFunction, + AnyOnCleanupHookFunction, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -52,6 +53,11 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private taskMiddlewareHooks: Map> = new Map(); + private globalCleanupHooks: Map> = + new Map(); + private taskCleanupHooks: Map> = + new Map(); + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -337,6 +343,37 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { getGlobalMiddlewareHooks(): RegisteredHookFunction[] { return Array.from(this.globalMiddlewareHooks.values()); } + + registerGlobalCleanupHook(hook: RegisterHookFunctionParams): void { + const id = generateHookId(hook); + + this.globalCleanupHooks.set(id, { + id, + name: hook.id, + fn: hook.fn, + }); + } + + registerTaskCleanupHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + const id = generateHookId(hook); + + this.taskCleanupHooks.set(taskId, { + id, + name: hook.id, + fn: hook.fn, + }); + } + + getTaskCleanupHook(taskId: string): AnyOnCleanupHookFunction | undefined { + return this.taskCleanupHooks.get(taskId)?.fn; + } + + getGlobalCleanupHooks(): RegisteredHookFunction[] { + return Array.from(this.globalCleanupHooks.values()); + } } export class NoopLifecycleHooksManager implements LifecycleHooksManager { @@ -504,6 +541,25 @@ export class NoopLifecycleHooksManager implements LifecycleHooksManager { getGlobalMiddlewareHooks(): [] { return []; } + + registerGlobalCleanupHook(hook: RegisterHookFunctionParams): void { + // Noop + } + + registerTaskCleanupHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void { + // Noop + } + + getTaskCleanupHook(taskId: string): AnyOnCleanupHookFunction | undefined { + return undefined; + } + + getGlobalCleanupHooks(): RegisteredHookFunction[] { + return []; + } } function generateHookId(hook: RegisterHookFunctionParams): string { diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index 5a7044289b..d0576d650f 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -192,6 +192,23 @@ export type OnMiddlewareHookFunction = ( export type AnyOnMiddlewareHookFunction = OnMiddlewareHookFunction; +export type TaskCleanupHookParams< + TPayload = unknown, + TInitOutput extends TaskInitOutput = TaskInitOutput, +> = { + ctx: TaskRunContext; + payload: TPayload; + task: string; + signal?: AbortSignal; + init?: TInitOutput; +}; + +export type OnCleanupHookFunction = ( + params: TaskCleanupHookParams +) => undefined | void | Promise; + +export type AnyOnCleanupHookFunction = OnCleanupHookFunction; + export interface LifecycleHooksManager { registerGlobalInitHook(hook: RegisterHookFunctionParams): void; registerTaskInitHook( @@ -256,4 +273,11 @@ export interface LifecycleHooksManager { ): void; getTaskMiddlewareHook(taskId: string): AnyOnMiddlewareHookFunction | undefined; getGlobalMiddlewareHooks(): RegisteredHookFunction[]; + registerGlobalCleanupHook(hook: RegisterHookFunctionParams): void; + registerTaskCleanupHook( + taskId: string, + hook: RegisterHookFunctionParams + ): void; + getTaskCleanupHook(taskId: string): AnyOnCleanupHookFunction | undefined; + getGlobalCleanupHooks(): RegisteredHookFunction[]; } diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 63e4d71a50..0f66d599d1 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -1624,4 +1624,10 @@ function registerTaskLifecycleHooks< fn: lifecycleHooksAdapters.createMiddlewareHookAdapter(params.middleware), }); } + + if (params.cleanup) { + lifecycleHooks.registerTaskCleanupHook(taskId, { + fn: lifecycleHooksAdapters.createCleanupHookAdapter(params.cleanup), + }); + } } From 30705fd59bdc1d17f1e14afec2a320db072e81d3 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 11:30:10 +0000 Subject: [PATCH 31/40] implement cleanup --- packages/core/src/v3/workers/taskExecutor.ts | 91 +++++++- packages/core/test/taskExecutor.test.ts | 216 ++++++++++++++++++- 2 files changed, 299 insertions(+), 8 deletions(-) diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 21ec33bc2f..0fe63f0a6d 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -174,6 +174,8 @@ export class TaskExecutor { ); } + await this.#cleanupAndWaitUntil(payload, ctx, initOutput, signal); + return { id: execution.run.id, ok: false, @@ -191,6 +193,8 @@ export class TaskExecutor { if (outputError) { recordSpanException(span, outputError); + await this.#cleanupAndWaitUntil(payload, ctx, initOutput, signal); + return this.#internalErrorResult( execution, TaskRunErrorCodes.TASK_OUTPUT_ERROR, @@ -208,6 +212,8 @@ export class TaskExecutor { if (exportError) { recordSpanException(span, exportError); + await this.#cleanupAndWaitUntil(payload, ctx, initOutput, signal); + return this.#internalErrorResult( execution, TaskRunErrorCodes.TASK_OUTPUT_ERROR, @@ -236,6 +242,8 @@ export class TaskExecutor { signal ); + await this.#cleanupAndWaitUntil(payload, ctx, initOutput, signal); + return { ok: true, id: execution.run.id, @@ -690,21 +698,90 @@ export class TaskExecutor { ); } - async #callTaskCleanup( + async #cleanupAndWaitUntil( payload: unknown, ctx: TaskRunContext, - init: unknown, + initOutput: any, signal?: AbortSignal ) { - const cleanupFn = this.task.fns.cleanup; + await this.#callCleanupFunctions(payload, ctx, initOutput, signal); + await this.#blockForWaitUntil(); + } - if (!cleanupFn) { + async #callCleanupFunctions( + payload: unknown, + ctx: TaskRunContext, + initOutput: any, + signal?: AbortSignal + ) { + const globalCleanupHooks = lifecycleHooks.getGlobalCleanupHooks(); + const taskCleanupHook = lifecycleHooks.getTaskCleanupHook(this.task.id); + + if (globalCleanupHooks.length === 0 && !taskCleanupHook) { return; } - return this._tracer.startActiveSpan("cleanup", async (span) => { - return await cleanupFn(payload, { ctx, init, signal }); - }); + return this._tracer.startActiveSpan( + "hooks.cleanup", + async (span) => { + return await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "cleanup", + async () => { + for (const hook of globalCleanupHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ?? "global", + async (span) => { + await hook.fn({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) + ); + // Ignore errors from cleanup functions + } + + if (taskCleanupHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "task", + async (span) => { + await taskCleanupHook({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ) + ); + // Ignore errors from cleanup functions + } + } + ); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", + }, + } + ); } async #blockForWaitUntil() { diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index ad0b701174..beac1e06a4 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -779,7 +779,8 @@ describe("TaskExecutor", () => { }, }); - expect((result as any).result.retry.delay).toBeCloseTo(30000, -1); + expect((result as any).result.retry.delay).toBeGreaterThan(29900); + expect((result as any).result.retry.delay).toBeLessThan(30100); }); test("should execute middleware hooks in correct order around other hooks", async () => { @@ -1203,6 +1204,219 @@ describe("TaskExecutor", () => { }, }); }); + + test("should call cleanup hooks in correct order after other hooks but before middleware completion", async () => { + const executionOrder: string[] = []; + + // Register global init hook + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + executionOrder.push("init"); + return { + foo: "bar", + }; + }, + }); + + // Register global start hook + lifecycleHooks.registerGlobalStartHook({ + id: "global-start", + fn: async () => { + executionOrder.push("start"); + }, + }); + + // Register global success hook + lifecycleHooks.registerGlobalSuccessHook({ + id: "global-success", + fn: async () => { + executionOrder.push("success"); + }, + }); + + // Register global complete hook + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async () => { + executionOrder.push("complete"); + }, + }); + + // Register global cleanup hooks + lifecycleHooks.registerGlobalCleanupHook({ + id: "global-cleanup-1", + fn: async ({ init }) => { + executionOrder.push("global-cleanup-1"); + // Verify we have access to init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + lifecycleHooks.registerGlobalCleanupHook({ + id: "global-cleanup-2", + fn: async ({ init }) => { + executionOrder.push("global-cleanup-2"); + // Verify we have access to init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register task-specific cleanup hook + lifecycleHooks.registerTaskCleanupHook("test-task", { + id: "task-cleanup", + fn: async ({ init }) => { + executionOrder.push("task-cleanup"); + // Verify we have access to init data + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register middleware to verify cleanup happens before middleware completion + lifecycleHooks.registerGlobalMiddlewareHook({ + id: "global-middleware", + fn: async ({ next }) => { + executionOrder.push("middleware-before"); + await next(); + executionOrder.push("middleware-after"); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run"); + return { + output: "test-output", + init: params.init, + }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify the execution order: + // 1. Middleware starts + // 2. Init hook + // 3. Start hook + // 4. Run function + // 5. Success hook + // 6. Complete hook + // 7. Cleanup hooks + // 8. Middleware completes + expect(executionOrder).toEqual([ + "middleware-before", + "init", + "start", + "run", + "success", + "complete", + "global-cleanup-1", + "global-cleanup-2", + "task-cleanup", + "middleware-after", + ]); + + // Verify the final result + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"output":"test-output","init":{"foo":"bar"}}}', + outputType: "application/super+json", + }, + }); + }); + + test("should call cleanup hooks even when task fails", async () => { + const executionOrder: string[] = []; + const expectedError = new Error("Task failed intentionally"); + + // Register global init hook + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + executionOrder.push("init"); + return { + foo: "bar", + }; + }, + }); + + // Register failure hook + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure", + fn: async () => { + executionOrder.push("failure"); + }, + }); + + // Register complete hook + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async () => { + executionOrder.push("complete"); + }, + }); + + // Register cleanup hooks + lifecycleHooks.registerGlobalCleanupHook({ + id: "global-cleanup", + fn: async ({ init }) => { + executionOrder.push("global-cleanup"); + // Verify we have access to init data even after failure + expect(init).toEqual({ foo: "bar" }); + }, + }); + + lifecycleHooks.registerTaskCleanupHook("test-task", { + id: "task-cleanup", + fn: async ({ init }) => { + executionOrder.push("task-cleanup"); + // Verify we have access to init data even after failure + expect(init).toEqual({ foo: "bar" }); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async () => { + executionOrder.push("run"); + throw expectedError; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify cleanup hooks are called even after failure + expect(executionOrder).toEqual([ + "init", + "run", + "failure", + "complete", + "global-cleanup", + "task-cleanup", + ]); + + // Verify the error result + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "BUILT_IN_ERROR", + message: "Task failed intentionally", + name: "Error", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); }); function executeTask(task: TaskMetadataWithFunctions, payload: any) { From 23ae77afff9a0b2790614de3e6bbbdf4bcbf6b94 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 11:49:21 +0000 Subject: [PATCH 32/40] handle max duration timeout errors better --- .../cli-v3/src/entryPoints/dev-run-worker.ts | 29 ---- packages/core/src/v3/workers/taskExecutor.ts | 34 ++++- packages/core/test/taskExecutor.test.ts | 125 +++++++++++++++--- 3 files changed, 137 insertions(+), 51 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 196d1ba3f1..a720354ae6 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -377,35 +377,6 @@ const zodIpc = new ZodIpcConnection({ ? timeout.abortAfterTimeout(execution.run.maxDuration) : undefined; - signal?.addEventListener("abort", async (e) => { - if (_isRunning) { - _isRunning = false; - _execution = undefined; - - const usageSample = usage.stop(measurement); - - await sender.send("TASK_RUN_COMPLETED", { - execution, - result: { - ok: false, - id: execution.run.id, - error: { - type: "INTERNAL_ERROR", - code: TaskRunErrorCodes.MAX_DURATION_EXCEEDED, - message: - signal.reason instanceof Error - ? signal.reason.message - : String(signal.reason), - }, - usage: { - durationMs: usageSample.cpuTime, - }, - metadata: runMetadataManager.stopAndReturnLastFlush(), - }, - }); - } - }); - const { result } = await executor.execute(execution, metadata, traceContext, signal); const usageSample = usage.stop(measurement); diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 0fe63f0a6d..24c2f0068f 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -2,7 +2,13 @@ import { SpanKind } from "@opentelemetry/api"; import { VERSION } from "../../version.js"; import { ApiError, RateLimitError } from "../apiClient/errors.js"; import { ConsoleInterceptor } from "../consoleInterceptor.js"; -import { isInternalError, parseError, sanitizeError, TaskPayloadParsedError } from "../errors.js"; +import { + InternalError, + isInternalError, + parseError, + sanitizeError, + TaskPayloadParsedError, +} from "../errors.js"; import { flattenAttributes, lifecycleHooks, runMetadata, waitUntil } from "../index.js"; import { AnyOnMiddlewareHookFunction, @@ -341,9 +347,29 @@ export class TaskExecutor { throw new Error("Task does not have a run function"); } - return runTimelineMetrics.measureMetric("trigger.dev/execution", "run", () => - runFn(payload, { ctx, init, signal }) - ); + // Create a promise that rejects when the signal aborts + const abortPromise = signal + ? new Promise((_, reject) => { + signal.addEventListener("abort", () => { + const maxDuration = ctx.run.maxDuration; + reject( + new InternalError({ + code: TaskRunErrorCodes.MAX_DURATION_EXCEEDED, + message: `Task execution exceeded maximum duration of ${maxDuration}ms`, + }) + ); + }); + }) + : undefined; + + return runTimelineMetrics.measureMetric("trigger.dev/execution", "run", async () => { + if (abortPromise) { + // Race between the run function and the abort promise + return await Promise.race([runFn(payload, { ctx, init, signal }), abortPromise]); + } + + return await runFn(payload, { ctx, init, signal }); + }); } async #callInitFunctions(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index beac1e06a4..f8d436a3b0 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -4,6 +4,7 @@ import { RunFnParams, ServerBackgroundWorker, TaskMetadataWithFunctions, + TaskRunErrorCodes, TaskRunExecution, } from "../src/v3/index.js"; import { TracingSDK } from "../src/v3/otel/tracingSDK.js"; @@ -52,7 +53,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, {}); + const result = await executeTask(task, {}, undefined); expect(result).toEqual({ result: { @@ -136,7 +137,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, {}); + const result = await executeTask(task, {}, undefined); // Verify hooks were called in correct order - should match registration order expect(globalSuccessOrder).toEqual(["global-2", "global-1", "task"]); @@ -238,7 +239,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify hooks were called in correct order expect(globalStartOrder).toEqual(["global-1", "global-2", "task"]); @@ -337,7 +338,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify hooks were called in correct order expect(globalFailureOrder).toEqual(["global-1", "global-2", "task"]); @@ -445,7 +446,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify hooks were called in correct order expect(globalCompleteOrder).toEqual(["global-1", "global-2", "task"]); @@ -533,7 +534,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify hooks were called in correct order expect(globalCompleteOrder).toEqual(["global", "task"]); @@ -645,7 +646,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify hooks were called in correct order and stopped after second global hook expect(hookCallOrder).toEqual(["task", "global-1", "global-2"]); @@ -707,7 +708,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify only task hook was called expect(hookCallOrder).toEqual(["task"]); @@ -755,7 +756,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify only task hook was called expect(hookCallOrder).toEqual(["task"]); @@ -863,7 +864,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify the execution order: // 1. Global middlewares (outside to inside) @@ -935,7 +936,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify only the middleware-before hook ran expect(executionOrder).toEqual(["middleware-before"]); @@ -1012,7 +1013,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify only the global init hook ran, and failure/complete hooks were called expect(executionOrder).toEqual(["global-init", "failure", "complete"]); @@ -1094,7 +1095,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify both init hooks ran, but run wasn't called, and failure/complete hooks were called expect(executionOrder).toEqual(["global-init", "task-init", "failure", "complete"]); @@ -1184,7 +1185,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify init succeeded, start hook failed, and run wasn't called expect(executionOrder).toEqual(["global-init", "global-start", "failure", "complete"]); @@ -1295,7 +1296,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify the execution order: // 1. Middleware starts @@ -1390,7 +1391,7 @@ describe("TaskExecutor", () => { }, }; - const result = await executeTask(task, { test: "data" }); + const result = await executeTask(task, { test: "data" }, undefined); // Verify cleanup hooks are called even after failure expect(executionOrder).toEqual([ @@ -1417,9 +1418,96 @@ describe("TaskExecutor", () => { }, }); }); + + test("should handle max duration abort signal and call hooks in correct order", async () => { + const executionOrder: string[] = []; + const maxDurationMs = 1000; + + // Create an abort controller that we'll trigger manually + const controller = new AbortController(); + + // Register global init hook + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + executionOrder.push("init"); + return { + foo: "bar", + }; + }, + }); + + // Register failure hook + lifecycleHooks.registerGlobalFailureHook({ + id: "global-failure", + fn: async ({ error }) => { + executionOrder.push("failure"); + expect((error as Error).message).toBe( + `Task execution exceeded maximum duration of ${maxDurationMs}ms` + ); + }, + }); + + // Register complete hook + lifecycleHooks.registerGlobalCompleteHook({ + id: "global-complete", + fn: async ({ result }) => { + executionOrder.push("complete"); + expect(result.ok).toBe(false); + }, + }); + + // Register cleanup hook + lifecycleHooks.registerGlobalCleanupHook({ + id: "global-cleanup", + fn: async () => { + executionOrder.push("cleanup"); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run-start"); + + // Create a promise that never resolves + await new Promise((resolve) => { + // Trigger abort after a small delay + setTimeout(() => { + controller.abort(); + }, 10); + }); + + // This should never be reached + executionOrder.push("run-end"); + }, + }, + }; + + const result = await executeTask(task, { test: "data" }, controller.signal); + + // Verify hooks were called in correct order + expect(executionOrder).toEqual(["init", "run-start", "failure", "complete", "cleanup"]); + + // Verify the error result + expect(result).toEqual({ + result: { + ok: false, + id: "test-run-id", + error: { + type: "INTERNAL_ERROR", + code: TaskRunErrorCodes.MAX_DURATION_EXCEEDED, + message: "Task execution exceeded maximum duration of 1000ms", + stackTrace: expect.any(String), + }, + skippedRetrying: false, + }, + }); + }); }); -function executeTask(task: TaskMetadataWithFunctions, payload: any) { +function executeTask(task: TaskMetadataWithFunctions, payload: any, signal?: AbortSignal) { const tracingSDK = new TracingSDK({ url: "http://localhost:4318", }); @@ -1472,6 +1560,7 @@ function executeTask(task: TaskMetadataWithFunctions, payload: any) { costInCents: 0, baseCostInCents: 0, priority: 0, + maxDuration: 1000, }, machine: { name: "micro", @@ -1508,5 +1597,5 @@ function executeTask(task: TaskMetadataWithFunctions, payload: any) { engine: "V2", }; - return executor.execute(execution, worker, {}); + return executor.execute(execution, worker, {}, signal); } From ba520fcdd8fa83a13274f3bd7eb3d5405c123d87 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 11:55:54 +0000 Subject: [PATCH 33/40] Make sure and register all the config hooks --- .../cli-v3/src/entryPoints/dev-run-worker.ts | 29 +++++++++++++++---- packages/core/src/v3/types/tasks.ts | 2 +- packages/core/src/v3/workers/taskExecutor.ts | 3 -- packages/core/test/taskExecutor.test.ts | 1 - 4 files changed, 25 insertions(+), 10 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index a720354ae6..2dc0cdc456 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -183,24 +183,44 @@ async function bootstrap() { if (config.init) { lifecycleHooks.registerGlobalInitHook({ - id: "trigger-dev-worker", + id: "config", fn: lifecycleHooksAdapters.createInitHookAdapter(config.init), }); } if (config.onStart) { lifecycleHooks.registerGlobalStartHook({ - id: "trigger-dev-worker", + id: "config", fn: lifecycleHooksAdapters.createStartHookAdapter(config.onStart), }); } + if (config.onSuccess) { + lifecycleHooks.registerGlobalSuccessHook({ + id: "config", + fn: lifecycleHooksAdapters.createSuccessHookAdapter(config.onSuccess), + }); + } + + if (config.onFailure) { + lifecycleHooks.registerGlobalFailureHook({ + id: "config", + fn: lifecycleHooksAdapters.createFailureHookAdapter(config.onFailure), + }); + } + + if (handleError) { + lifecycleHooks.registerGlobalCatchErrorHook({ + id: "config", + fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(handleError), + }); + } + return { tracer, tracingSDK, consoleInterceptor, config, - handleErrorFn: handleError, workerManifest, }; } @@ -242,7 +262,7 @@ const zodIpc = new ZodIpcConnection({ } try { - const { tracer, tracingSDK, consoleInterceptor, config, handleErrorFn, workerManifest } = + const { tracer, tracingSDK, consoleInterceptor, config, workerManifest } = await bootstrap(); _tracingSDK = tracingSDK; @@ -359,7 +379,6 @@ const zodIpc = new ZodIpcConnection({ tracingSDK, consoleInterceptor, retries: config.retries, - handleErrorFn, }); try { diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index fa32eef406..0c425d4277 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -133,7 +133,7 @@ export type HandleErrorResult = export type HandleErrorArgs = { ctx: Context; - init: unknown; + init?: Record; retry?: RetryOptions; retryAt?: Date; retryDelayInMs?: number; diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 24c2f0068f..4dc5cd0005 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -53,7 +53,6 @@ export type TaskExecutorOptions = { enabledInDev?: boolean; default?: RetryOptions; }; - handleErrorFn: HandleErrorFunction | undefined; }; export class TaskExecutor { @@ -66,7 +65,6 @@ export class TaskExecutor { default?: RetryOptions; } | undefined; - private _handleErrorFn: HandleErrorFunction | undefined; constructor( public task: TaskMetadataWithFunctions, @@ -76,7 +74,6 @@ export class TaskExecutor { this._tracer = options.tracer; this._consoleInterceptor = options.consoleInterceptor; this._retries = options.retries; - this._handleErrorFn = options.handleErrorFn; } async execute( diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index f8d436a3b0..83aea93ec3 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -1531,7 +1531,6 @@ function executeTask(task: TaskMetadataWithFunctions, payload: any, signal?: Abo maxAttempts: 1, }, }, - handleErrorFn: undefined, }); const execution: TaskRunExecution = { From df9cad66672b6d14434c6bccd96b4eb6e47a569a Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 12:52:54 +0000 Subject: [PATCH 34/40] Get it all working --- .../src/entryPoints/managed-run-worker.ts | 84 ++++++++++--------- packages/core/src/v3/locals-api.ts | 3 +- packages/core/src/v3/tracer.ts | 2 +- references/hello-world/src/db.ts | 31 ++++--- 4 files changed, 68 insertions(+), 52 deletions(-) diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index 913789e0d1..3b02d3caab 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -18,6 +18,8 @@ import { waitUntil, apiClientManager, runTimelineMetrics, + lifecycleHooks, + lifecycleHooksAdapters, localsAPI, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; @@ -40,6 +42,7 @@ import { StandardWaitUntilManager, ManagedRuntimeManager, StandardRunTimelineMetricsManager, + StandardLifecycleHooksManager, StandardLocalsManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; @@ -98,6 +101,9 @@ const heartbeatIntervalMs = getEnvVar("HEARTBEAT_INTERVAL_MS"); const standardLocalsManager = new StandardLocalsManager(); localsAPI.setGlobalLocalsManager(standardLocalsManager); +const standardLifecycleHooksManager = new StandardLifecycleHooksManager(); +lifecycleHooks.setGlobalLifecycleHooksManager(standardLifecycleHooksManager); + const standardRunTimelineMetricsManager = new StandardRunTimelineMetricsManager(); runTimelineMetrics.setGlobalManager(standardRunTimelineMetricsManager); standardRunTimelineMetricsManager.seedMetricsFromEnvironment(); @@ -185,12 +191,46 @@ async function bootstrap() { logger.setGlobalTaskLogger(otelTaskLogger); + if (config.init) { + lifecycleHooks.registerGlobalInitHook({ + id: "config", + fn: lifecycleHooksAdapters.createInitHookAdapter(config.init), + }); + } + + if (config.onStart) { + lifecycleHooks.registerGlobalStartHook({ + id: "config", + fn: lifecycleHooksAdapters.createStartHookAdapter(config.onStart), + }); + } + + if (config.onSuccess) { + lifecycleHooks.registerGlobalSuccessHook({ + id: "config", + fn: lifecycleHooksAdapters.createSuccessHookAdapter(config.onSuccess), + }); + } + + if (config.onFailure) { + lifecycleHooks.registerGlobalFailureHook({ + id: "config", + fn: lifecycleHooksAdapters.createFailureHookAdapter(config.onFailure), + }); + } + + if (handleError) { + lifecycleHooks.registerGlobalCatchErrorHook({ + id: "config", + fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(handleError), + }); + } + return { tracer, tracingSDK, consoleInterceptor, config, - handleErrorFn: handleError, workerManifest, }; } @@ -232,7 +272,7 @@ const zodIpc = new ZodIpcConnection({ } try { - const { tracer, tracingSDK, consoleInterceptor, config, handleErrorFn, workerManifest } = + const { tracer, tracingSDK, consoleInterceptor, config, workerManifest } = await bootstrap(); _tracingSDK = tracingSDK; @@ -336,8 +376,7 @@ const zodIpc = new ZodIpcConnection({ tracer, tracingSDK, consoleInterceptor, - config, - handleErrorFn, + retries: config.retries, }); try { @@ -355,42 +394,7 @@ const zodIpc = new ZodIpcConnection({ ? timeout.abortAfterTimeout(execution.run.maxDuration) : undefined; - signal?.addEventListener("abort", async (e) => { - if (_isRunning) { - _isRunning = false; - _execution = undefined; - - const usageSample = usage.stop(measurement); - - await sender.send("TASK_RUN_COMPLETED", { - execution, - result: { - ok: false, - id: execution.run.id, - error: { - type: "INTERNAL_ERROR", - code: TaskRunErrorCodes.MAX_DURATION_EXCEEDED, - message: - signal.reason instanceof Error - ? signal.reason.message - : String(signal.reason), - }, - usage: { - durationMs: usageSample.cpuTime, - }, - metadata: runMetadataManager.stopAndReturnLastFlush(), - }, - }); - } - }); - - const { result } = await executor.execute( - execution, - metadata, - traceContext, - measurement, - signal - ); + const { result } = await executor.execute(execution, metadata, traceContext, signal); const usageSample = usage.stop(measurement); diff --git a/packages/core/src/v3/locals-api.ts b/packages/core/src/v3/locals-api.ts index d3c40967ee..a9f86494a6 100644 --- a/packages/core/src/v3/locals-api.ts +++ b/packages/core/src/v3/locals-api.ts @@ -19,8 +19,9 @@ export const locals = { } return value; }, - set(key: LocalsKey, value: T): void { + set(key: LocalsKey, value: T): T { localsAPI.setLocal(key, value); + return value; }, }; diff --git a/packages/core/src/v3/tracer.ts b/packages/core/src/v3/tracer.ts index 085107c017..42bc2f249e 100644 --- a/packages/core/src/v3/tracer.ts +++ b/packages/core/src/v3/tracer.ts @@ -168,7 +168,7 @@ export class TriggerTracer { const attributes = options?.attributes ?? {}; - const span = this.tracer.startSpan(name, options, ctx); + const span = this.tracer.startSpan(name, options, parentContext); this.tracer .startSpan( diff --git a/references/hello-world/src/db.ts b/references/hello-world/src/db.ts index 43feefacad..f55f4f4dbc 100644 --- a/references/hello-world/src/db.ts +++ b/references/hello-world/src/db.ts @@ -1,7 +1,9 @@ import { locals } from "@trigger.dev/sdk"; import { logger, tasks } from "@trigger.dev/sdk"; -const DbLocal = locals.create<{ connect: () => Promise }>("db"); +const DbLocal = locals.create<{ connect: () => Promise; disconnect: () => Promise }>( + "db" +); export function getDb() { return locals.getOrThrow(DbLocal); @@ -11,13 +13,22 @@ export function setDb(db: { connect: () => Promise }) { locals.set(DbLocal, db); } -// tasks.middleware("db", ({ ctx, payload, next, task }) => { -// locals.set(DbLocal, { -// connect: async () => { -// logger.info("Connecting to the database"); -// }, -// }); +tasks.middleware("db", async ({ ctx, payload, next, task }) => { + const db = locals.set(DbLocal, { + connect: async () => { + logger.info("Connecting to the database"); + }, + disconnect: async () => { + logger.info("Disconnecting from the database"); + }, + }); -// logger.info("Hello, world from the middleware", { ctx, payload }); -// return next(); -// }); + await db.connect(); + + logger.info("Hello, world from BEFORE the next call", { ctx, payload }); + await next(); + + logger.info("Hello, world from AFTER the next call", { ctx, payload }); + + await db.disconnect(); +}); From 3ca59852776118ab673e7e425a7fd63e4a04fefb Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 13:53:53 +0000 Subject: [PATCH 35/40] Hooks now all use the new types, and adding some spans --- .../webapp/app/components/runs/v3/RunIcon.tsx | 22 + .../cli-v3/src/entryPoints/dev-run-worker.ts | 30 +- .../src/entryPoints/managed-run-worker.ts | 48 +- packages/core/src/v3/config.ts | 33 +- packages/core/src/v3/lifecycle-hooks-api.ts | 2 - .../core/src/v3/lifecycleHooks/adapters.ts | 113 --- packages/core/src/v3/types/tasks.ts | 50 +- packages/core/src/v3/workers/taskExecutor.ts | 730 +++++++++--------- packages/core/test/taskExecutor.test.ts | 4 - packages/trigger-sdk/src/v3/shared.ts | 55 +- references/hello-world/src/db.ts | 14 + references/hello-world/src/trigger/example.ts | 58 +- 12 files changed, 549 insertions(+), 610 deletions(-) delete mode 100644 packages/core/src/v3/lifecycleHooks/adapters.ts diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx index 1666212da8..6f3a84a07c 100644 --- a/apps/webapp/app/components/runs/v3/RunIcon.tsx +++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx @@ -78,6 +78,28 @@ export function RunIcon({ name, className, spanName }: TaskIconProps) { return ; case "fatal": return ; + case "task-middleware": + return ; + case "task-fn-run": + return ; + case "task-hook-init": + return ; + case "task-hook-onStart": + return ; + case "task-hook-onSuccess": + return ; + case "task-hook-onFailure": + return ; + case "task-hook-onComplete": + return ; + case "task-hook-onWait": + return ; + case "task-hook-onResume": + return ; + case "task-hook-catchError": + return ; + case "task-hook-cleanup": + return ; } return ; diff --git a/packages/cli-v3/src/entryPoints/dev-run-worker.ts b/packages/cli-v3/src/entryPoints/dev-run-worker.ts index 2dc0cdc456..8553e36057 100644 --- a/packages/cli-v3/src/entryPoints/dev-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/dev-run-worker.ts @@ -1,15 +1,23 @@ import type { Tracer } from "@opentelemetry/api"; import type { Logger } from "@opentelemetry/api-logs"; import { + AnyOnCatchErrorHookFunction, + AnyOnFailureHookFunction, + AnyOnInitHookFunction, + AnyOnStartHookFunction, + AnyOnSuccessHookFunction, apiClientManager, clock, ExecutorToWorkerMessageCatalog, type HandleErrorFunction, + lifecycleHooks, + localsAPI, logger, LogLevel, + resourceCatalog, runMetadata, runtime, - resourceCatalog, + runTimelineMetrics, TaskRunErrorCodes, TaskRunExecution, timeout, @@ -17,10 +25,6 @@ import { waitUntil, WorkerManifest, WorkerToExecutorMessageCatalog, - runTimelineMetrics, - lifecycleHooks, - lifecycleHooksAdapters, - localsAPI, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -32,17 +36,17 @@ import { logLevels, ManagedRuntimeManager, OtelTaskLogger, + StandardLifecycleHooksManager, + StandardLocalsManager, StandardMetadataManager, StandardResourceCatalog, + StandardRunTimelineMetricsManager, StandardWaitUntilManager, TaskExecutor, TracingDiagnosticLogLevel, TracingSDK, usage, UsageTimeoutManager, - StandardRunTimelineMetricsManager, - StandardLifecycleHooksManager, - StandardLocalsManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -184,35 +188,35 @@ async function bootstrap() { if (config.init) { lifecycleHooks.registerGlobalInitHook({ id: "config", - fn: lifecycleHooksAdapters.createInitHookAdapter(config.init), + fn: config.init as AnyOnInitHookFunction, }); } if (config.onStart) { lifecycleHooks.registerGlobalStartHook({ id: "config", - fn: lifecycleHooksAdapters.createStartHookAdapter(config.onStart), + fn: config.onStart as AnyOnStartHookFunction, }); } if (config.onSuccess) { lifecycleHooks.registerGlobalSuccessHook({ id: "config", - fn: lifecycleHooksAdapters.createSuccessHookAdapter(config.onSuccess), + fn: config.onSuccess as AnyOnSuccessHookFunction, }); } if (config.onFailure) { lifecycleHooks.registerGlobalFailureHook({ id: "config", - fn: lifecycleHooksAdapters.createFailureHookAdapter(config.onFailure), + fn: config.onFailure as AnyOnFailureHookFunction, }); } if (handleError) { lifecycleHooks.registerGlobalCatchErrorHook({ id: "config", - fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(handleError), + fn: handleError as AnyOnCatchErrorHookFunction, }); } diff --git a/packages/cli-v3/src/entryPoints/managed-run-worker.ts b/packages/cli-v3/src/entryPoints/managed-run-worker.ts index 3b02d3caab..fa4f426bac 100644 --- a/packages/cli-v3/src/entryPoints/managed-run-worker.ts +++ b/packages/cli-v3/src/entryPoints/managed-run-worker.ts @@ -1,26 +1,30 @@ import type { Tracer } from "@opentelemetry/api"; import type { Logger } from "@opentelemetry/api-logs"; import { + AnyOnCatchErrorHookFunction, + AnyOnFailureHookFunction, + AnyOnInitHookFunction, + AnyOnStartHookFunction, + AnyOnSuccessHookFunction, + apiClientManager, clock, + ExecutorToWorkerMessageCatalog, type HandleErrorFunction, + lifecycleHooks, + localsAPI, logger, LogLevel, - runtime, resourceCatalog, + runMetadata, + runtime, + runTimelineMetrics, TaskRunErrorCodes, TaskRunExecution, - WorkerToExecutorMessageCatalog, - TriggerConfig, - WorkerManifest, - ExecutorToWorkerMessageCatalog, timeout, - runMetadata, + TriggerConfig, waitUntil, - apiClientManager, - runTimelineMetrics, - lifecycleHooks, - lifecycleHooksAdapters, - localsAPI, + WorkerManifest, + WorkerToExecutorMessageCatalog, } from "@trigger.dev/core/v3"; import { TriggerTracer } from "@trigger.dev/core/v3/tracer"; import { @@ -30,20 +34,20 @@ import { getEnvVar, getNumberEnvVar, logLevels, + ManagedRuntimeManager, OtelTaskLogger, ProdUsageManager, + StandardLifecycleHooksManager, + StandardLocalsManager, + StandardMetadataManager, StandardResourceCatalog, + StandardRunTimelineMetricsManager, + StandardWaitUntilManager, TaskExecutor, TracingDiagnosticLogLevel, TracingSDK, usage, UsageTimeoutManager, - StandardMetadataManager, - StandardWaitUntilManager, - ManagedRuntimeManager, - StandardRunTimelineMetricsManager, - StandardLifecycleHooksManager, - StandardLocalsManager, } from "@trigger.dev/core/v3/workers"; import { ZodIpcConnection } from "@trigger.dev/core/v3/zodIpc"; import { readFile } from "node:fs/promises"; @@ -194,35 +198,35 @@ async function bootstrap() { if (config.init) { lifecycleHooks.registerGlobalInitHook({ id: "config", - fn: lifecycleHooksAdapters.createInitHookAdapter(config.init), + fn: config.init as AnyOnInitHookFunction, }); } if (config.onStart) { lifecycleHooks.registerGlobalStartHook({ id: "config", - fn: lifecycleHooksAdapters.createStartHookAdapter(config.onStart), + fn: config.onStart as AnyOnStartHookFunction, }); } if (config.onSuccess) { lifecycleHooks.registerGlobalSuccessHook({ id: "config", - fn: lifecycleHooksAdapters.createSuccessHookAdapter(config.onSuccess), + fn: config.onSuccess as AnyOnSuccessHookFunction, }); } if (config.onFailure) { lifecycleHooks.registerGlobalFailureHook({ id: "config", - fn: lifecycleHooksAdapters.createFailureHookAdapter(config.onFailure), + fn: config.onFailure as AnyOnFailureHookFunction, }); } if (handleError) { lifecycleHooks.registerGlobalCatchErrorHook({ id: "config", - fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(handleError), + fn: handleError as AnyOnCatchErrorHookFunction, }); } diff --git a/packages/core/src/v3/config.ts b/packages/core/src/v3/config.ts index 20b49d5d17..9be80decc6 100644 --- a/packages/core/src/v3/config.ts +++ b/packages/core/src/v3/config.ts @@ -1,15 +1,16 @@ import type { Instrumentation } from "@opentelemetry/instrumentation"; import type { SpanExporter } from "@opentelemetry/sdk-trace-base"; import type { BuildExtension } from "./build/extensions.js"; -import type { MachinePresetName } from "./schemas/common.js"; -import type { LogLevel } from "./logger/taskLogger.js"; import type { - FailureFnParams, - InitFnParams, - StartFnParams, - SuccessFnParams, -} from "./types/index.js"; -import type { BuildRuntime, RetryOptions } from "./index.js"; + AnyOnFailureHookFunction, + AnyOnInitHookFunction, + AnyOnStartHookFunction, + AnyOnSuccessHookFunction, + BuildRuntime, + RetryOptions, +} from "./index.js"; +import type { LogLevel } from "./logger/taskLogger.js"; +import type { MachinePresetName } from "./schemas/common.js"; export type CompatibilityFlag = "run_engine_v2"; @@ -215,23 +216,31 @@ export type TriggerConfig = { /** * Run before a task is executed, for all tasks. This is useful for setting up any global state that is needed for all tasks. + * + * @deprecated, please use tasks.init instead */ - init?: (payload: unknown, params: InitFnParams) => any | Promise; + init?: AnyOnInitHookFunction; /** * onSuccess is called after the run function has successfully completed. + * + * @deprecated, please use tasks.onSuccess instead */ - onSuccess?: (payload: unknown, output: unknown, params: SuccessFnParams) => Promise; + onSuccess?: AnyOnSuccessHookFunction; /** * onFailure is called after a task run has failed (meaning the run function threw an error and won't be retried anymore) + * + * @deprecated, please use tasks.onFailure instead */ - onFailure?: (payload: unknown, error: unknown, params: FailureFnParams) => Promise; + onFailure?: AnyOnFailureHookFunction; /** * onStart is called the first time a task is executed in a run (not before every retry) + * + * @deprecated, please use tasks.onStart instead */ - onStart?: (payload: unknown, params: StartFnParams) => Promise; + onStart?: AnyOnStartHookFunction; /** * @deprecated Use a custom build extension to add post install commands diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index 847dc56580..37195cb729 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -32,5 +32,3 @@ export type { AnyOnCleanupHookFunction, TaskCleanupHookParams, } from "./lifecycleHooks/types.js"; - -export * as lifecycleHooksAdapters from "./lifecycleHooks/adapters.js"; diff --git a/packages/core/src/v3/lifecycleHooks/adapters.ts b/packages/core/src/v3/lifecycleHooks/adapters.ts deleted file mode 100644 index b3baadec6c..0000000000 --- a/packages/core/src/v3/lifecycleHooks/adapters.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { TaskOptions } from "../types/index.js"; -import { - AnyOnInitHookFunction, - AnyOnStartHookFunction, - AnyOnFailureHookFunction, - AnyOnSuccessHookFunction, - AnyOnCatchErrorHookFunction, - AnyOnMiddlewareHookFunction, - TaskInitOutput, - TaskSuccessHookParams, - TaskFailureHookParams, - TaskStartHookParams, - TaskCatchErrorHookParams, - TaskCleanupHookParams, - AnyOnCleanupHookFunction, -} from "./types.js"; - -export function createInitHookAdapter( - fn: NonNullable["init"]> -): AnyOnInitHookFunction { - return async (params) => { - const paramsWithoutPayload = { - ...params, - }; - - delete paramsWithoutPayload["payload"]; - - return await fn(params.payload as unknown as TPayload, paramsWithoutPayload); - }; -} - -export function createStartHookAdapter< - TPayload, - TInitOutput extends TaskInitOutput = TaskInitOutput, ->( - fn: NonNullable["onStart"]> -): AnyOnStartHookFunction { - return async (params) => { - return await fn( - params.payload as unknown as TPayload, - params as TaskStartHookParams - ); - }; -} - -export function createFailureHookAdapter< - TPayload, - TInitOutput extends TaskInitOutput = TaskInitOutput, ->( - fn: NonNullable["onFailure"]> -): AnyOnFailureHookFunction { - return async (params) => { - return await fn( - params.payload as unknown as TPayload, - params.error, - params as TaskFailureHookParams - ); - }; -} - -export function createSuccessHookAdapter( - fn: NonNullable["onSuccess"]> -): AnyOnSuccessHookFunction { - return async (params) => { - return await fn( - params.payload as unknown as TPayload, - params.output as unknown as TOutput, - params as TaskSuccessHookParams - ); - }; -} - -export function createHandleErrorHookAdapter< - TPayload, - TInitOutput extends TaskInitOutput = TaskInitOutput, ->( - fn: NonNullable["handleError"]> -): AnyOnCatchErrorHookFunction { - return async (params) => { - return await fn( - params.payload as unknown as TPayload, - params.error, - params as TaskCatchErrorHookParams - ); - }; -} - -export function createMiddlewareHookAdapter( - fn: NonNullable["middleware"]> -): AnyOnMiddlewareHookFunction { - return async (params) => { - const { payload, next, ...paramsWithoutPayloadAndNext } = params; - - return await fn(payload as unknown as TPayload, { - ...paramsWithoutPayloadAndNext, - next, - }); - }; -} - -export function createCleanupHookAdapter< - TPayload, - TInitOutput extends TaskInitOutput = TaskInitOutput, ->( - fn: NonNullable["cleanup"]> -): AnyOnCleanupHookFunction { - return async (params) => { - return await fn( - params.payload as unknown as TPayload, - params as TaskCleanupHookParams - ); - }; -} diff --git a/packages/core/src/v3/types/tasks.ts b/packages/core/src/v3/types/tasks.ts index 0c425d4277..8c1e4d1014 100644 --- a/packages/core/src/v3/types/tasks.ts +++ b/packages/core/src/v3/types/tasks.ts @@ -1,5 +1,18 @@ import { SerializableJson } from "../../schemas/json.js"; import { TriggerApiRequestOptions } from "../apiClient/index.js"; +import { + AnyOnCatchErrorHookFunction, + OnCatchErrorHookFunction, + OnCleanupHookFunction, + OnCompleteHookFunction, + OnFailureHookFunction, + OnInitHookFunction, + OnMiddlewareHookFunction, + OnResumeHookFunction, + OnStartHookFunction, + OnSuccessHookFunction, + OnWaitHookFunction, +} from "../lifecycleHooks/types.js"; import { RunTags } from "../schemas/api.js"; import { MachineCpu, @@ -10,16 +23,10 @@ import { TaskRunContext, } from "../schemas/index.js"; import { IdempotencyKey } from "./idempotencyKeys.js"; +import { QueueOptions } from "./queues.js"; import { AnySchemaParseFn, inferSchemaIn, inferSchemaOut, Schema } from "./schemas.js"; -import { Prettify } from "./utils.js"; import { inferToolParameters, ToolTaskParameters } from "./tools.js"; -import { QueueOptions } from "./queues.js"; -import { - OnCatchErrorHookFunction, - OnCompleteHookFunction, - OnResumeHookFunction, - OnWaitHookFunction, -} from "../lifecycleHooks/types.js"; +import { Prettify } from "./utils.js"; export type Queue = QueueOptions; export type TaskSchema = Schema; @@ -100,6 +107,7 @@ export type InitFnParams = Prettify<{ export type StartFnParams = Prettify<{ ctx: Context; + init?: InitOutput; /** Abort signal that is aborted when a task run exceeds it's maxDuration. Can be used to automatically cancel downstream requests */ signal?: AbortSignal; }>; @@ -267,25 +275,21 @@ type CommonTaskOptions< * * @deprecated Use locals and middleware instead */ - init?: (payload: TPayload, params: InitFnParams) => Promise; + init?: OnInitHookFunction; /** * cleanup is called after the run function has completed. * * @deprecated Use middleware instead */ - cleanup?: (payload: TPayload, params: RunFnParams) => Promise; + cleanup?: OnCleanupHookFunction; /** * handleError is called when the run function throws an error. It can be used to modify the error or return new retry options. * * @deprecated Use catchError instead */ - handleError?: ( - payload: TPayload, - error: unknown, - params: HandleErrorFnParams - ) => HandleErrorResult; + handleError?: OnCatchErrorHookFunction; /** * catchError is called when the run function throws an error. It can be used to modify the error or return new retry options. @@ -313,30 +317,22 @@ type CommonTaskOptions< * }); * ``` */ - middleware?: (payload: TPayload, params: MiddlewareFnParams) => Promise; + middleware?: OnMiddlewareHookFunction; /** * onStart is called the first time a task is executed in a run (not before every retry) */ - onStart?: (payload: TPayload, params: StartFnParams) => Promise; + onStart?: OnStartHookFunction; /** * onSuccess is called after the run function has successfully completed. */ - onSuccess?: ( - payload: TPayload, - output: TOutput, - params: SuccessFnParams - ) => Promise; + onSuccess?: OnSuccessHookFunction; /** * onFailure is called after a task run has failed (meaning the run function threw an error and won't be retried anymore) */ - onFailure?: ( - payload: TPayload, - error: unknown, - params: FailureFnParams - ) => Promise; + onFailure?: OnFailureHookFunction; }; export type TaskOptions< diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 4dc5cd0005..9375eb33a4 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -30,11 +30,8 @@ import { import { SemanticInternalAttributes } from "../semanticInternalAttributes.js"; import { taskContext } from "../task-context-api.js"; import { TriggerTracer } from "../tracer.js"; -import { - HandleErrorFunction, - HandleErrorModificationOptions, - TaskMetadataWithFunctions, -} from "../types/index.js"; +import { tryCatch } from "../tryCatch.js"; +import { HandleErrorModificationOptions, TaskMetadataWithFunctions } from "../types/index.js"; import { conditionallyExportPacket, conditionallyImportPacket, @@ -43,7 +40,6 @@ import { stringifyIO, } from "../utils/ioSerialization.js"; import { calculateNextRetryDelay } from "../utils/retries.js"; -import { tryCatch } from "../tryCatch.js"; export type TaskExecutorOptions = { tracingSDK: TracingSDK; @@ -308,7 +304,17 @@ export class TaskExecutor { const runner = hooks.reduceRight( (next, hook) => { return async () => { - await hook.fn({ payload, ctx, signal, task: this.task.id, next }); + await this._tracer.startActiveSpan( + hook.name ? `middleware/${hook.name}` : "middleware", + async (span) => { + await hook.fn({ payload, ctx, signal, task: this.task.id, next }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-middleware", + }, + } + ); }; }, async () => { @@ -360,12 +366,20 @@ export class TaskExecutor { : undefined; return runTimelineMetrics.measureMetric("trigger.dev/execution", "run", async () => { - if (abortPromise) { - // Race between the run function and the abort promise - return await Promise.race([runFn(payload, { ctx, init, signal }), abortPromise]); - } + return await this._tracer.startActiveSpan( + "run", + async (span) => { + if (abortPromise) { + // Race between the run function and the abort promise + return await Promise.race([runFn(payload, { ctx, init, signal }), abortPromise]); + } - return await runFn(payload, { ctx, init, signal }); + return await runFn(payload, { ctx, init, signal }); + }, + { + attributes: { [SemanticInternalAttributes.STYLE_ICON]: "task-fn-run" }, + } + ); }); } @@ -377,102 +391,92 @@ export class TaskExecutor { return {}; } - return this._tracer.startActiveSpan( - "hooks.init", - async (span) => { - const result = await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "init", - async () => { - // Store global hook results in an array - const globalResults = []; - for (const hook of globalInitHooks) { - const [hookError, result] = await tryCatch( - this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - const result = await hook.fn({ payload, ctx, signal, task: this.task.id }); - - if (result && typeof result === "object" && !Array.isArray(result)) { - span.setAttributes(flattenAttributes(result)); - return result; - } - - return {}; - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); + const result = await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "init", + async () => { + // Store global hook results in an array + const globalResults = []; + for (const hook of globalInitHooks) { + const [hookError, result] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `init/${hook.name}` : "init/global", + async (span) => { + const result = await hook.fn({ payload, ctx, signal, task: this.task.id }); + + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + return result; + } - if (hookError) { - throw hookError; + return {}; + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-init", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - if (result && typeof result === "object" && !Array.isArray(result)) { - globalResults.push(result); - } - } + if (hookError) { + throw hookError; + } - // Merge all global results into a single object - const mergedGlobalResults = Object.assign({}, ...globalResults); - - if (taskInitHook) { - const [hookError, taskResult] = await tryCatch( - this._tracer.startActiveSpan( - "task", - async (span) => { - const result = await taskInitHook({ payload, ctx, signal, task: this.task.id }); - - if (result && typeof result === "object" && !Array.isArray(result)) { - span.setAttributes(flattenAttributes(result)); - return result; - } - - return {}; - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); + if (result && typeof result === "object" && !Array.isArray(result)) { + globalResults.push(result); + } + } - if (hookError) { - throw hookError; - } + // Merge all global results into a single object + const mergedGlobalResults = Object.assign({}, ...globalResults); + + if (taskInitHook) { + const [hookError, taskResult] = await tryCatch( + this._tracer.startActiveSpan( + "init/task", + async (span) => { + const result = await taskInitHook({ payload, ctx, signal, task: this.task.id }); - // Only merge if taskResult is an object - if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { - return { ...mergedGlobalResults, ...taskResult }; + if (result && typeof result === "object" && !Array.isArray(result)) { + span.setAttributes(flattenAttributes(result)); + return result; + } + + return {}; + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-init", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - // If taskResult isn't an object, return global results - return mergedGlobalResults; - } + if (hookError) { + throw hookError; + } - return mergedGlobalResults; + // Only merge if taskResult is an object + if (taskResult && typeof taskResult === "object" && !Array.isArray(taskResult)) { + return { ...mergedGlobalResults, ...taskResult }; } - ); - if (result && typeof result === "object" && !Array.isArray(result)) { - span.setAttributes(flattenAttributes(result)); - return result; + // If taskResult isn't an object, return global results + return mergedGlobalResults; } - return; - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - [SemanticInternalAttributes.COLLAPSED]: true, - }, + return mergedGlobalResults; } ); + + if (result && typeof result === "object" && !Array.isArray(result)) { + return result; + } + + return; } async #callOnSuccessFunctions( @@ -489,69 +493,63 @@ export class TaskExecutor { return; } - return this._tracer.startActiveSpan( - "hooks.success", - async (span) => { - return await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "success", - async () => { - for (const hook of globalSuccessHooks) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ - payload, - output, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from onSuccess functions + return await runTimelineMetrics.measureMetric("trigger.dev/execution", "success", async () => { + for (const hook of globalSuccessHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `onSuccess/${hook.name}` : "onSuccess/global", + async (span) => { + await hook.fn({ + payload, + output, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onSuccess", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - if (taskSuccessHook) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - "task", - async (span) => { - await taskSuccessHook({ - payload, - output, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from onSuccess functions + if (hookError) { + throw hookError; + } + } + + if (taskSuccessHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "onSuccess/task", + async (span) => { + await taskSuccessHook({ + payload, + output, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onSuccess", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } - } + ) ); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, + + if (hookError) { + throw hookError; + } } - ); + }); } async #callOnFailureFunctions( @@ -568,69 +566,63 @@ export class TaskExecutor { return; } - return this._tracer.startActiveSpan( - "hooks.failure", - async (span) => { - return await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "failure", - async () => { - for (const hook of globalFailureHooks) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ - payload, - error, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from onFailure functions + return await runTimelineMetrics.measureMetric("trigger.dev/execution", "failure", async () => { + for (const hook of globalFailureHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `onFailure/${hook.name}` : "onFailure/global", + async (span) => { + await hook.fn({ + payload, + error, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onFailure", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - if (taskFailureHook) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - "task", - async (span) => { - await taskFailureHook({ - payload, - error, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from onFailure functions + if (hookError) { + throw hookError; + } + } + + if (taskFailureHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "onFailure/task", + async (span) => { + await taskFailureHook({ + payload, + error, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onFailure", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } - } + ) ); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, + + if (hookError) { + throw hookError; + } } - ); + }); } async #parsePayload(payload: unknown) { @@ -658,67 +650,55 @@ export class TaskExecutor { return; } - return this._tracer.startActiveSpan( - "hooks.start", - async (span) => { - return await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "start", - async () => { - for (const hook of globalStartHooks) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ payload, ctx, signal, task: this.task.id, init: initOutput }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - - if (hookError) { - throw hookError; - } + return await runTimelineMetrics.measureMetric("trigger.dev/execution", "start", async () => { + for (const hook of globalStartHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `onStart/${hook.name}` : "onStart/global", + async (span) => { + await hook.fn({ payload, ctx, signal, task: this.task.id, init: initOutput }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - if (taskStartHook) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - "task", - async (span) => { - await taskStartHook({ - payload, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); + if (hookError) { + throw hookError; + } + } - if (hookError) { - throw hookError; - } + if (taskStartHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "onStart/task", + async (span) => { + await taskStartHook({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onStart", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } - } + ) ); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, + + if (hookError) { + throw hookError; + } } - ); + }); } async #cleanupAndWaitUntil( @@ -744,67 +724,61 @@ export class TaskExecutor { return; } - return this._tracer.startActiveSpan( - "hooks.cleanup", - async (span) => { - return await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "cleanup", - async () => { - for (const hook of globalCleanupHooks) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ - payload, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from cleanup functions + return await runTimelineMetrics.measureMetric("trigger.dev/execution", "cleanup", async () => { + for (const hook of globalCleanupHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `cleanup/${hook.name}` : "cleanup/global", + async (span) => { + await hook.fn({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-cleanup", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - if (taskCleanupHook) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - "task", - async (span) => { - await taskCleanupHook({ - payload, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from cleanup functions + if (hookError) { + throw hookError; + } + } + + if (taskCleanupHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "cleanup/task", + async (span) => { + await taskCleanupHook({ + payload, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-cleanup", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } - } + ) ); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, + + if (hookError) { + throw hookError; + } } - ); + }); } async #blockForWaitUntil() { @@ -820,6 +794,7 @@ export class TaskExecutor { { attributes: { [SemanticInternalAttributes.STYLE_ICON]: "clock", + [SemanticInternalAttributes.COLLAPSED]: true, }, } ); @@ -883,7 +858,7 @@ export class TaskExecutor { } return this._tracer.startActiveSpan( - "handleError()", + "catchError", async (span) => { // Try task-specific catch error hook first const taskCatchErrorHook = lifecycleHooks.getTaskCatchErrorHook(this.task.id); @@ -935,7 +910,8 @@ export class TaskExecutor { }, { attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "exclamation-circle", + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-catchError", + [SemanticInternalAttributes.COLLAPSED]: true, }, } ); @@ -1005,69 +981,63 @@ export class TaskExecutor { return; } - return this._tracer.startActiveSpan( - "hooks.complete", - async (span) => { - return await runTimelineMetrics.measureMetric( - "trigger.dev/execution", - "complete", - async () => { - for (const hook of globalCompleteHooks) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - hook.name ?? "global", - async (span) => { - await hook.fn({ - payload, - result, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from onComplete functions + return await runTimelineMetrics.measureMetric("trigger.dev/execution", "complete", async () => { + for (const hook of globalCompleteHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `onComplete/${hook.name}` : "onComplete/global", + async (span) => { + await hook.fn({ + payload, + result, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } + ) + ); - if (taskCompleteHook) { - const [hookError] = await tryCatch( - this._tracer.startActiveSpan( - "task", - async (span) => { - await taskCompleteHook({ - payload, - result, - ctx, - signal, - task: this.task.id, - init: initOutput, - }); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, - } - ) - ); - // Ignore errors from onComplete functions + if (hookError) { + throw hookError; + } + } + + if (taskCompleteHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "onComplete/task", + async (span) => { + await taskCompleteHook({ + payload, + result, + ctx, + signal, + task: this.task.id, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onComplete", + [SemanticInternalAttributes.COLLAPSED]: true, + }, } - } + ) ); - }, - { - attributes: { - [SemanticInternalAttributes.STYLE_ICON]: "tabler-function", - }, + + if (hookError) { + throw hookError; + } } - ); + }); } #internalErrorResult(execution: TaskRunExecution, code: TaskRunErrorCodes, error: unknown) { diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 83aea93ec3..7eb43d159c 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -1064,8 +1064,6 @@ describe("TaskExecutor", () => { fn: async ({ error, init }) => { executionOrder.push("failure"); expect(error).toBe(expectedError); - // Verify we got the global init data - expect(init).toEqual({ foo: "bar" }); }, }); @@ -1078,8 +1076,6 @@ describe("TaskExecutor", () => { ok: false, error: expectedError, }); - // Verify we got the global init data - expect(init).toEqual({ foo: "bar" }); }, }); diff --git a/packages/trigger-sdk/src/v3/shared.ts b/packages/trigger-sdk/src/v3/shared.ts index 0f66d599d1..e21cc2f219 100644 --- a/packages/trigger-sdk/src/v3/shared.ts +++ b/packages/trigger-sdk/src/v3/shared.ts @@ -1,4 +1,4 @@ -import { SpanKind, SpanStatusCode } from "@opentelemetry/api"; +import { SpanKind } from "@opentelemetry/api"; import { SerializableJson } from "@trigger.dev/core"; import { accessoryAttributes, @@ -8,47 +8,59 @@ import { convertToolParametersToSchema, createErrorTaskError, defaultRetryOptions, + flattenIdempotencyKey, + getEnvVar, getSchemaParseFn, InitOutput, + lifecycleHooks, makeIdempotencyKey, parsePacket, Queue, QueueOptions, + resourceCatalog, runtime, SemanticInternalAttributes, stringifyIO, SubtaskUnwrapError, - resourceCatalog, taskContext, + TaskFromIdentifier, TaskRunContext, TaskRunExecutionResult, TaskRunPromise, - TaskFromIdentifier, - flattenIdempotencyKey, - getEnvVar, - lifecycleHooks, - lifecycleHooksAdapters, } from "@trigger.dev/core/v3"; import { PollOptions, runs } from "./runs.js"; import { tracer } from "./tracer.js"; import type { + AnyOnCatchErrorHookFunction, + AnyOnCleanupHookFunction, + AnyOnCompleteHookFunction, + AnyOnFailureHookFunction, + AnyOnInitHookFunction, + AnyOnMiddlewareHookFunction, + AnyOnResumeHookFunction, + AnyOnStartHookFunction, + AnyOnSuccessHookFunction, + AnyOnWaitHookFunction, AnyRunHandle, AnyRunTypes, AnyTask, + AnyTaskRunResult, BatchByIdAndWaitItem, - BatchByTaskAndWaitItem, BatchByIdItem, + BatchByIdResult, + BatchByTaskAndWaitItem, BatchByTaskItem, BatchByTaskResult, - BatchByIdResult, BatchItem, BatchResult, BatchRunHandle, BatchRunHandleFromTypes, BatchTasksRunHandleFromTypes, BatchTriggerAndWaitItem, + BatchTriggerAndWaitOptions, BatchTriggerOptions, + BatchTriggerTaskV2RequestBody, InferRunTypes, inferSchemaIn, inferToolParameters, @@ -76,15 +88,6 @@ import type { TriggerAndWaitOptions, TriggerApiRequestOptions, TriggerOptions, - AnyTaskRunResult, - BatchTriggerAndWaitOptions, - BatchTriggerTaskV2RequestBody, - AnyOnInitHookFunction, - AnyOnCatchErrorHookFunction, - AnyOnCompleteHookFunction, - AnyOnWaitHookFunction, - AnyOnResumeHookFunction, - AnyOnFailureHookFunction, } from "@trigger.dev/core/v3"; export type { @@ -101,6 +104,7 @@ export type { SerializableJson, Task, TaskBatchOutputHandle, + TaskFromIdentifier, TaskIdentifier, TaskOptions, TaskOutput, @@ -108,7 +112,6 @@ export type { TaskPayload, TaskRunResult, TriggerOptions, - TaskFromIdentifier, }; export { SubtaskUnwrapError, TaskRunPromise }; @@ -1566,25 +1569,25 @@ function registerTaskLifecycleHooks< >(taskId: TIdentifier, params: TaskOptions) { if (params.init) { lifecycleHooks.registerTaskInitHook(taskId, { - fn: lifecycleHooksAdapters.createInitHookAdapter(params.init), + fn: params.init as AnyOnInitHookFunction, }); } if (params.onStart) { lifecycleHooks.registerTaskStartHook(taskId, { - fn: lifecycleHooksAdapters.createStartHookAdapter(params.onStart), + fn: params.onStart as AnyOnStartHookFunction, }); } if (params.onFailure) { lifecycleHooks.registerTaskFailureHook(taskId, { - fn: lifecycleHooksAdapters.createFailureHookAdapter(params.onFailure), + fn: params.onFailure as AnyOnFailureHookFunction, }); } if (params.onSuccess) { lifecycleHooks.registerTaskSuccessHook(taskId, { - fn: lifecycleHooksAdapters.createSuccessHookAdapter(params.onSuccess), + fn: params.onSuccess as AnyOnSuccessHookFunction, }); } @@ -1615,19 +1618,19 @@ function registerTaskLifecycleHooks< if (params.handleError) { lifecycleHooks.registerTaskCatchErrorHook(taskId, { - fn: lifecycleHooksAdapters.createHandleErrorHookAdapter(params.handleError), + fn: params.handleError as AnyOnCatchErrorHookFunction, }); } if (params.middleware) { lifecycleHooks.registerTaskMiddlewareHook(taskId, { - fn: lifecycleHooksAdapters.createMiddlewareHookAdapter(params.middleware), + fn: params.middleware as AnyOnMiddlewareHookFunction, }); } if (params.cleanup) { lifecycleHooks.registerTaskCleanupHook(taskId, { - fn: lifecycleHooksAdapters.createCleanupHookAdapter(params.cleanup), + fn: params.cleanup as AnyOnCleanupHookFunction, }); } } diff --git a/references/hello-world/src/db.ts b/references/hello-world/src/db.ts index f55f4f4dbc..7c2cd12c15 100644 --- a/references/hello-world/src/db.ts +++ b/references/hello-world/src/db.ts @@ -32,3 +32,17 @@ tasks.middleware("db", async ({ ctx, payload, next, task }) => { await db.disconnect(); }); + +tasks.onWait("db", async ({ ctx, payload, task }) => { + logger.info("Hello, world from ON WAIT", { ctx, payload }); + + const db = getDb(); + await db.disconnect(); +}); + +tasks.onResume("db", async ({ ctx, payload, task }) => { + logger.info("Hello, world from ON RESUME", { ctx, payload }); + + const db = getDb(); + await db.connect(); +}); diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts index b30e1a2018..a1dff610e7 100644 --- a/references/hello-world/src/trigger/example.ts +++ b/references/hello-world/src/trigger/example.ts @@ -4,19 +4,10 @@ import { getDb } from "../db.js"; export const helloWorldTask = task({ id: "hello-world", - init: async (payload, { ctx }) => { - return { - foobar: "baz", - }; - }, - run: async (payload: any, { ctx, init }) => { + run: async (payload: any, { ctx }) => { logger.info("Hello, world from the init", { ctx, payload }); - const db = getDb(); - - await db.connect(); - - logger.debug("debug: Hello, world!", { payload, init }); + logger.debug("debug: Hello, world!", { payload }); logger.info("info: Hello, world!", { payload }); logger.log("log: Hello, world!", { payload }); logger.warn("warn: Hello, world!", { payload }); @@ -157,3 +148,48 @@ const nonExportedTask = task({ logger.info("Hello, world from the non-exported task", { message: payload.message }); }, }); + +export const hooksTask = task({ + id: "hooks", + run: async (payload: { message: string }, { ctx }) => { + logger.info("Hello, world from the hooks task", { message: payload.message }); + + await wait.for({ seconds: 5 }); + + return { + message: "Hello, world!", + }; + }, + init: async () => { + return { + foobar: "baz", + }; + }, + onWait: async ({ payload, ctx, init }) => { + logger.info("Hello, world from the onWait hook", { payload, init }); + }, + onResume: async ({ payload, ctx, init }) => { + logger.info("Hello, world from the onResume hook", { payload, init }); + }, + onStart: async ({ payload, ctx, init }) => { + logger.info("Hello, world from the onStart hook", { payload, init }); + }, + onSuccess: async ({ payload, output, ctx }) => { + logger.info("Hello, world from the onSuccess hook", { payload, output }); + }, + onFailure: async ({ payload, error, ctx }) => { + logger.info("Hello, world from the onFailure hook", { payload, error }); + }, + onComplete: async ({ ctx, payload, result }) => { + logger.info("Hello, world from the onComplete hook", { payload, result }); + }, + handleError: async ({ payload, error, ctx, retry }) => { + logger.info("Hello, world from the handleError hook", { payload, error, retry }); + }, + catchError: async ({ ctx, payload, error, retry }) => { + logger.info("Hello, world from the catchError hook", { payload, error, retry }); + }, + cleanup: async ({ ctx, payload }) => { + logger.info("Hello, world from the cleanup hook", { payload }); + }, +}); From 268c9f6898c6826a235eb1f0e2fdb3764a7258e3 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 14:24:16 +0000 Subject: [PATCH 36/40] implement onWait/onResume --- packages/core/src/v3/lifecycle-hooks-api.ts | 1 + packages/core/src/v3/lifecycleHooks/index.ts | 17 ++ .../core/src/v3/lifecycleHooks/manager.ts | 36 +++++ packages/core/src/v3/lifecycleHooks/types.ts | 27 ++++ .../src/v3/runtime/managedRuntimeManager.ts | 47 ++++++ packages/core/src/v3/workers/taskExecutor.ts | 149 ++++++++++++++++++ packages/core/test/taskExecutor.test.ts | 120 ++++++++++++++ references/hello-world/src/trigger/example.ts | 9 +- 8 files changed, 401 insertions(+), 5 deletions(-) diff --git a/packages/core/src/v3/lifecycle-hooks-api.ts b/packages/core/src/v3/lifecycle-hooks-api.ts index 37195cb729..ec9e87c998 100644 --- a/packages/core/src/v3/lifecycle-hooks-api.ts +++ b/packages/core/src/v3/lifecycle-hooks-api.ts @@ -31,4 +31,5 @@ export type { OnCleanupHookFunction, AnyOnCleanupHookFunction, TaskCleanupHookParams, + TaskWait, } from "./lifecycleHooks/types.js"; diff --git a/packages/core/src/v3/lifecycleHooks/index.ts b/packages/core/src/v3/lifecycleHooks/index.ts index 5607623f14..843ae92ce8 100644 --- a/packages/core/src/v3/lifecycleHooks/index.ts +++ b/packages/core/src/v3/lifecycleHooks/index.ts @@ -15,6 +15,7 @@ import { AnyOnWaitHookFunction, RegisteredHookFunction, RegisterHookFunctionParams, + TaskWait, type LifecycleHooksManager, } from "./types.js"; @@ -243,6 +244,22 @@ export class LifecycleHooksAPI { return this.#getManager().getGlobalCleanupHooks(); } + public callOnWaitHookListeners(wait: TaskWait): Promise { + return this.#getManager().callOnWaitHookListeners(wait); + } + + public callOnResumeHookListeners(wait: TaskWait): Promise { + return this.#getManager().callOnResumeHookListeners(wait); + } + + public registerOnWaitHookListener(listener: (wait: TaskWait) => Promise): void { + this.#getManager().registerOnWaitHookListener(listener); + } + + public registerOnResumeHookListener(listener: (wait: TaskWait) => Promise): void { + this.#getManager().registerOnResumeHookListener(listener); + } + #getManager(): LifecycleHooksManager { return getGlobal(API_NAME) ?? NOOP_LIFECYCLE_HOOKS_MANAGER; } diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 37794ed938..2c1857657b 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -12,6 +12,7 @@ import { AnyOnCatchErrorHookFunction, AnyOnMiddlewareHookFunction, AnyOnCleanupHookFunction, + TaskWait, } from "./types.js"; export class StandardLifecycleHooksManager implements LifecycleHooksManager { @@ -58,6 +59,25 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { private taskCleanupHooks: Map> = new Map(); + private onWaitHookListeners: ((wait: TaskWait) => Promise)[] = []; + private onResumeHookListeners: ((wait: TaskWait) => Promise)[] = []; + + registerOnWaitHookListener(listener: (wait: TaskWait) => Promise): void { + this.onWaitHookListeners.push(listener); + } + + async callOnWaitHookListeners(wait: TaskWait): Promise { + await Promise.all(this.onWaitHookListeners.map((listener) => listener(wait))); + } + + registerOnResumeHookListener(listener: (wait: TaskWait) => Promise): void { + this.onResumeHookListeners.push(listener); + } + + async callOnResumeHookListeners(wait: TaskWait): Promise { + await Promise.all(this.onResumeHookListeners.map((listener) => listener(wait))); + } + registerGlobalStartHook(hook: RegisterHookFunctionParams): void { const id = generateHookId(hook); @@ -377,6 +397,22 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { } export class NoopLifecycleHooksManager implements LifecycleHooksManager { + registerOnWaitHookListener(listener: (wait: TaskWait) => Promise): void { + // Noop + } + + async callOnWaitHookListeners(wait: TaskWait): Promise { + // Noop + } + + registerOnResumeHookListener(listener: (wait: TaskWait) => Promise): void { + // Noop + } + + async callOnResumeHookListeners(wait: TaskWait): Promise { + // Noop + } + registerGlobalInitHook(hook: RegisterHookFunctionParams): void { // Noop } diff --git a/packages/core/src/v3/lifecycleHooks/types.ts b/packages/core/src/v3/lifecycleHooks/types.ts index d0576d650f..5d307c225b 100644 --- a/packages/core/src/v3/lifecycleHooks/types.ts +++ b/packages/core/src/v3/lifecycleHooks/types.ts @@ -33,10 +33,30 @@ export type OnStartHookFunction; +export type TaskWait = + | { + type: "duration"; + date: Date; + } + | { + type: "token"; + token: string; + } + | { + type: "task"; + runId: string; + } + | { + type: "batch"; + batchId: string; + runCount: number; + }; + export type TaskWaitHookParams< TPayload = unknown, TInitOutput extends TaskInitOutput = TaskInitOutput, > = { + wait: TaskWait; ctx: TaskRunContext; payload: TPayload; task: string; @@ -55,6 +75,7 @@ export type TaskResumeHookParams< TInitOutput extends TaskInitOutput = TaskInitOutput, > = { ctx: TaskRunContext; + wait: TaskWait; payload: TPayload; task: string; signal?: AbortSignal; @@ -280,4 +301,10 @@ export interface LifecycleHooksManager { ): void; getTaskCleanupHook(taskId: string): AnyOnCleanupHookFunction | undefined; getGlobalCleanupHooks(): RegisteredHookFunction[]; + + callOnWaitHookListeners(wait: TaskWait): Promise; + registerOnWaitHookListener(listener: (wait: TaskWait) => Promise): void; + + callOnResumeHookListeners(wait: TaskWait): Promise; + registerOnResumeHookListener(listener: (wait: TaskWait) => Promise): void; } diff --git a/packages/core/src/v3/runtime/managedRuntimeManager.ts b/packages/core/src/v3/runtime/managedRuntimeManager.ts index 67ef064498..b876a87084 100644 --- a/packages/core/src/v3/runtime/managedRuntimeManager.ts +++ b/packages/core/src/v3/runtime/managedRuntimeManager.ts @@ -1,3 +1,4 @@ +import { lifecycleHooks } from "../lifecycle-hooks-api.js"; import { BatchTaskRunExecutionResult, CompletedWaitpoint, @@ -44,9 +45,19 @@ export class ManagedRuntimeManager implements RuntimeManager { this.resolversByWaitId.set(params.id, resolve); }); + await lifecycleHooks.callOnWaitHookListeners({ + type: "task", + runId: params.id, + }); + const waitpoint = await promise; const result = this.waitpointToTaskRunExecutionResult(waitpoint); + await lifecycleHooks.callOnResumeHookListeners({ + type: "task", + runId: params.id, + }); + return result; }); } @@ -70,8 +81,20 @@ export class ManagedRuntimeManager implements RuntimeManager { }) ); + await lifecycleHooks.callOnWaitHookListeners({ + type: "batch", + batchId: params.id, + runCount: params.runCount, + }); + const waitpoints = await promise; + await lifecycleHooks.callOnResumeHookListeners({ + type: "batch", + batchId: params.id, + runCount: params.runCount, + }); + return { id: params.id, items: waitpoints.map(this.waitpointToTaskRunExecutionResult), @@ -91,8 +114,32 @@ export class ManagedRuntimeManager implements RuntimeManager { this.resolversByWaitId.set(waitpointFriendlyId, resolve); }); + if (finishDate) { + await lifecycleHooks.callOnWaitHookListeners({ + type: "duration", + date: finishDate, + }); + } else { + await lifecycleHooks.callOnWaitHookListeners({ + type: "token", + token: waitpointFriendlyId, + }); + } + const waitpoint = await promise; + if (finishDate) { + await lifecycleHooks.callOnResumeHookListeners({ + type: "duration", + date: finishDate, + }); + } else { + await lifecycleHooks.callOnResumeHookListeners({ + type: "token", + token: waitpointFriendlyId, + }); + } + return { ok: !waitpoint.outputIsError, output: waitpoint.output, diff --git a/packages/core/src/v3/workers/taskExecutor.ts b/packages/core/src/v3/workers/taskExecutor.ts index 9375eb33a4..355dbec95c 100644 --- a/packages/core/src/v3/workers/taskExecutor.ts +++ b/packages/core/src/v3/workers/taskExecutor.ts @@ -15,6 +15,7 @@ import { RegisteredHookFunction, TaskCompleteResult, TaskInitOutput, + TaskWait, } from "../lifecycleHooks/types.js"; import { recordSpanException, TracingSDK } from "../otel/index.js"; import { runTimelineMetrics } from "../run-timeline-metrics-api.js"; @@ -126,6 +127,14 @@ export class TaskExecutor { parsedPayload = await this.#parsePayload(payloadResult); + lifecycleHooks.registerOnWaitHookListener(async (wait) => { + await this.#callOnWaitFunctions(wait, parsedPayload, ctx, initOutput, signal); + }); + + lifecycleHooks.registerOnResumeHookListener(async (wait) => { + await this.#callOnResumeFunctions(wait, parsedPayload, ctx, initOutput, signal); + }); + const executeTask = async (payload: any) => { const [runError, output] = await tryCatch( (async () => { @@ -383,6 +392,146 @@ export class TaskExecutor { }); } + async #callOnWaitFunctions( + wait: TaskWait, + payload: unknown, + ctx: TaskRunContext, + initOutput: TaskInitOutput, + signal?: AbortSignal + ) { + const globalWaitHooks = lifecycleHooks.getGlobalWaitHooks(); + const taskWaitHook = lifecycleHooks.getTaskWaitHook(this.task.id); + + if (globalWaitHooks.length === 0 && !taskWaitHook) { + return; + } + + const result = await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "onWait", + async () => { + for (const hook of globalWaitHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `onWait/${hook.name}` : "onWait/global", + async (span) => { + await hook.fn({ payload, ctx, signal, task: this.task.id, wait, init: initOutput }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onWait", + [SemanticInternalAttributes.COLLAPSED]: true, + }, + } + ) + ); + + if (hookError) { + throw hookError; + } + } + + if (taskWaitHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "onWait/task", + async (span) => { + await taskWaitHook({ + payload, + ctx, + signal, + task: this.task.id, + wait, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onWait", + [SemanticInternalAttributes.COLLAPSED]: true, + }, + } + ) + ); + + if (hookError) { + throw hookError; + } + } + } + ); + } + + async #callOnResumeFunctions( + wait: TaskWait, + payload: unknown, + ctx: TaskRunContext, + initOutput: TaskInitOutput, + signal?: AbortSignal + ) { + const globalResumeHooks = lifecycleHooks.getGlobalResumeHooks(); + const taskResumeHook = lifecycleHooks.getTaskResumeHook(this.task.id); + + if (globalResumeHooks.length === 0 && !taskResumeHook) { + return; + } + + const result = await runTimelineMetrics.measureMetric( + "trigger.dev/execution", + "onResume", + async () => { + for (const hook of globalResumeHooks) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + hook.name ? `onResume/${hook.name}` : "onResume/global", + async (span) => { + await hook.fn({ payload, ctx, signal, task: this.task.id, wait, init: initOutput }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onResume", + [SemanticInternalAttributes.COLLAPSED]: true, + }, + } + ) + ); + + if (hookError) { + throw hookError; + } + } + + if (taskResumeHook) { + const [hookError] = await tryCatch( + this._tracer.startActiveSpan( + "onResume/task", + async (span) => { + await taskResumeHook({ + payload, + ctx, + signal, + task: this.task.id, + wait, + init: initOutput, + }); + }, + { + attributes: { + [SemanticInternalAttributes.STYLE_ICON]: "task-hook-onResume", + [SemanticInternalAttributes.COLLAPSED]: true, + }, + } + ) + ); + + if (hookError) { + throw hookError; + } + } + } + ); + } + async #callInitFunctions(payload: unknown, ctx: TaskRunContext, signal?: AbortSignal) { const globalInitHooks = lifecycleHooks.getGlobalInitHooks(); const taskInitHook = lifecycleHooks.getTaskInitHook(this.task.id); diff --git a/packages/core/test/taskExecutor.test.ts b/packages/core/test/taskExecutor.test.ts index 7eb43d159c..355471297c 100644 --- a/packages/core/test/taskExecutor.test.ts +++ b/packages/core/test/taskExecutor.test.ts @@ -1501,6 +1501,126 @@ describe("TaskExecutor", () => { }, }); }); + + test("should call onWait and onResume hooks in correct order with proper data", async () => { + const executionOrder: string[] = []; + const waitData = { type: "task", runId: "test-run-id" } as const; + + // Register global init hook to provide init data + lifecycleHooks.registerGlobalInitHook({ + id: "test-init", + fn: async () => { + executionOrder.push("init"); + return { + foo: "bar", + }; + }, + }); + + // Register global wait hooks + lifecycleHooks.registerGlobalWaitHook({ + id: "global-wait-1", + fn: async ({ payload, wait, init }) => { + executionOrder.push("global-wait-1"); + expect(wait).toEqual(waitData); + expect(init).toEqual({ foo: "bar" }); + }, + }); + + lifecycleHooks.registerGlobalWaitHook({ + id: "global-wait-2", + fn: async ({ payload, wait, init }) => { + executionOrder.push("global-wait-2"); + expect(wait).toEqual(waitData); + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register task-specific wait hook + lifecycleHooks.registerTaskWaitHook("test-task", { + id: "task-wait", + fn: async ({ payload, wait, init }) => { + executionOrder.push("task-wait"); + expect(wait).toEqual(waitData); + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register global resume hooks + lifecycleHooks.registerGlobalResumeHook({ + id: "global-resume-1", + fn: async ({ payload, wait, init }) => { + executionOrder.push("global-resume-1"); + expect(wait).toEqual(waitData); + expect(init).toEqual({ foo: "bar" }); + }, + }); + + lifecycleHooks.registerGlobalResumeHook({ + id: "global-resume-2", + fn: async ({ payload, wait, init }) => { + executionOrder.push("global-resume-2"); + expect(wait).toEqual(waitData); + expect(init).toEqual({ foo: "bar" }); + }, + }); + + // Register task-specific resume hook + lifecycleHooks.registerTaskResumeHook("test-task", { + id: "task-resume", + fn: async ({ payload, wait, init }) => { + executionOrder.push("task-resume"); + expect(wait).toEqual(waitData); + expect(init).toEqual({ foo: "bar" }); + }, + }); + + const task = { + id: "test-task", + fns: { + run: async (payload: any, params: RunFnParams) => { + executionOrder.push("run-start"); + + // Simulate a wait + await lifecycleHooks.callOnWaitHookListeners(waitData); + + // Simulate some time passing + await new Promise((resolve) => setTimeout(resolve, 10)); + + // Simulate resuming + await lifecycleHooks.callOnResumeHookListeners(waitData); + + executionOrder.push("run-end"); + return { success: true }; + }, + }, + }; + + const result = await executeTask(task, { test: "data" }); + + // Verify hooks were called in correct order + expect(executionOrder).toEqual([ + "init", + "run-start", + "global-wait-1", + "global-wait-2", + "task-wait", + "global-resume-1", + "global-resume-2", + "task-resume", + "run-end", + ]); + + // Verify the final result + expect(result).toEqual({ + result: { + ok: true, + id: "test-run-id", + output: '{"json":{"success":true}}', + outputType: "application/super+json", + }, + }); + }); }); function executeTask(task: TaskMetadataWithFunctions, payload: any, signal?: AbortSignal) { diff --git a/references/hello-world/src/trigger/example.ts b/references/hello-world/src/trigger/example.ts index a1dff610e7..d1b008f417 100644 --- a/references/hello-world/src/trigger/example.ts +++ b/references/hello-world/src/trigger/example.ts @@ -1,6 +1,5 @@ import { batch, logger, task, timeout, wait } from "@trigger.dev/sdk"; import { setTimeout } from "timers/promises"; -import { getDb } from "../db.js"; export const helloWorldTask = task({ id: "hello-world", @@ -165,11 +164,11 @@ export const hooksTask = task({ foobar: "baz", }; }, - onWait: async ({ payload, ctx, init }) => { - logger.info("Hello, world from the onWait hook", { payload, init }); + onWait: async ({ payload, wait, ctx, init }) => { + logger.info("Hello, world from the onWait hook", { payload, init, wait }); }, - onResume: async ({ payload, ctx, init }) => { - logger.info("Hello, world from the onResume hook", { payload, init }); + onResume: async ({ payload, wait, ctx, init }) => { + logger.info("Hello, world from the onResume hook", { payload, init, wait }); }, onStart: async ({ payload, ctx, init }) => { logger.info("Hello, world from the onStart hook", { payload, init }); From 887c5cac5c7532df51ef7a8859e02d25e4fc83a1 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 15:56:55 +0000 Subject: [PATCH 37/40] Add changeset --- .changeset/weak-jobs-hide.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/weak-jobs-hide.md diff --git a/.changeset/weak-jobs-hide.md b/.changeset/weak-jobs-hide.md new file mode 100644 index 0000000000..0be1f49588 --- /dev/null +++ b/.changeset/weak-jobs-hide.md @@ -0,0 +1,7 @@ +--- +"@trigger.dev/sdk": patch +"trigger.dev": patch +"@trigger.dev/core": patch +--- + +v4: New lifecycle hooks From e745fcb663d2c5a418aaadaf7d905c8b9cc2229d Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 15:58:50 +0000 Subject: [PATCH 38/40] Remove console.log --- apps/webapp/app/components/runs/v3/RunIcon.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/apps/webapp/app/components/runs/v3/RunIcon.tsx b/apps/webapp/app/components/runs/v3/RunIcon.tsx index 6f3a84a07c..7c43fcfae6 100644 --- a/apps/webapp/app/components/runs/v3/RunIcon.tsx +++ b/apps/webapp/app/components/runs/v3/RunIcon.tsx @@ -28,8 +28,6 @@ type SpanNameIcons = { const spanNameIcons: SpanNameIcons[] = [{ matcher: /^prisma:/, iconName: "brand-prisma" }]; export function RunIcon({ name, className, spanName }: TaskIconProps) { - console.log("spanName", spanName, name); - const spanNameIcon = spanNameIcons.find(({ matcher }) => matcher.test(spanName)); if (spanNameIcon) { From 1def8d06c2499f8e071264bcce82aec4297314bb Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 16:32:17 +0000 Subject: [PATCH 39/40] Use allSettled so onWait/onResume errors don't break anything --- packages/core/src/v3/lifecycleHooks/manager.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core/src/v3/lifecycleHooks/manager.ts b/packages/core/src/v3/lifecycleHooks/manager.ts index 2c1857657b..29f4968362 100644 --- a/packages/core/src/v3/lifecycleHooks/manager.ts +++ b/packages/core/src/v3/lifecycleHooks/manager.ts @@ -67,7 +67,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { } async callOnWaitHookListeners(wait: TaskWait): Promise { - await Promise.all(this.onWaitHookListeners.map((listener) => listener(wait))); + await Promise.allSettled(this.onWaitHookListeners.map((listener) => listener(wait))); } registerOnResumeHookListener(listener: (wait: TaskWait) => Promise): void { @@ -75,7 +75,7 @@ export class StandardLifecycleHooksManager implements LifecycleHooksManager { } async callOnResumeHookListeners(wait: TaskWait): Promise { - await Promise.all(this.onResumeHookListeners.map((listener) => listener(wait))); + await Promise.allSettled(this.onResumeHookListeners.map((listener) => listener(wait))); } registerGlobalStartHook(hook: RegisterHookFunctionParams): void { From 6d5d10a45aefd138b33fa568b0f794304a812586 Mon Sep 17 00:00:00 2001 From: Eric Allam Date: Tue, 25 Mar 2025 16:33:12 +0000 Subject: [PATCH 40/40] Support other init file names --- packages/cli-v3/src/build/bundle.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/cli-v3/src/build/bundle.ts b/packages/cli-v3/src/build/bundle.ts index b845d58de0..b326a5f769 100644 --- a/packages/cli-v3/src/build/bundle.ts +++ b/packages/cli-v3/src/build/bundle.ts @@ -239,12 +239,14 @@ export async function getBundleResultFromBuild( // Check if the entry point is an init.ts file at the root of a trigger directory function isInitEntryPoint(entryPoint: string): boolean { const normalizedEntryPoint = entryPoint.replace(/\\/g, "/"); // Normalize path separators - const initFileName = "init.ts"; + const initFileNames = ["init.ts", "init.mts", "init.cts", "init.js", "init.mjs", "init.cjs"]; // Check if it's directly in one of the trigger directories return resolvedConfig.dirs.some((dir) => { const normalizedDir = dir.replace(/\\/g, "/"); - return normalizedEntryPoint === `${normalizedDir}/${initFileName}`; + return initFileNames.some( + (fileName) => normalizedEntryPoint === `${normalizedDir}/${fileName}` + ); }); }