import { z } from "zod"; import * as v from "valibot"; import { wrap } from "@typeschema/valibot"; export type ParserZodEsque = { _input: TInput; _output: TParsedInput; }; export type ParserMyZodEsque = { parse: (input: any) => TInput; }; export type ParserSuperstructEsque = { create: (input: unknown) => TInput; }; export type ParserCustomValidatorEsque = (input: unknown) => Promise | TInput; export type ParserYupEsque = { validateSync: (input: unknown) => TInput; }; export type ParserScaleEsque = { assert(value: unknown): asserts value is TInput; }; export type ParserWithoutInput = | ParserCustomValidatorEsque | ParserMyZodEsque | ParserScaleEsque | ParserSuperstructEsque | ParserYupEsque; export type ParserWithInputOutput = ParserZodEsque; export type Parser = ParserWithInputOutput | ParserWithoutInput; export type inferParser = TParser extends ParserWithInputOutput< infer $TIn, infer $TOut > ? { in: $TIn; out: $TOut; } : TParser extends ParserWithoutInput ? { in: $InOut; out: $InOut; } : never; export type Simplify = TType extends any[] | Date ? TType : { [K in keyof TType]: TType[K] }; export type TriggerResult = { id: string; }; export type TaskRunResult = | { ok: true; id: string; output: TOutput; } | { ok: false; id: string; error: unknown; }; export type RunMetadata = { run: string; }; export type inferContext = TContextBuilder extends ContextBuilder ? TContext extends UnsetMarker ? unknown : TContextOverrides extends UnsetMarker ? Simplify : Simplify> : never; export type RunFnParams = { /** Metadata about the task, run, attempt, queue, environment, organization, project and batch. */ meta: RunMetadata; /** Context added by task middleware */ ctx: inferContext; payload: TPayload; }; /** * See https://github.com/microsoft/TypeScript/issues/41966#issuecomment-758187996 * Fixes issues with iterating over keys of objects with index signatures. * Without this, iterations over keys of objects with index signatures will lose * type information about the keys and only the index signature will remain. * @internal */ export type WithoutIndexSignature = { [K in keyof TObj as string extends K ? never : number extends K ? never : K]: TObj[K]; }; /** * @internal * Overwrite properties in `TType` with properties in `TWith` * Only overwrites properties when the type to be overwritten * is an object. Otherwise it will just use the type from `TWith`. */ export type Overwrite = TWith extends any ? TType extends object ? { [K in | keyof WithoutIndexSignature | keyof WithoutIndexSignature]: K extends keyof TWith // Exclude index signature from keys ? TWith[K] : K extends keyof TType ? TType[K] : never; } & (string extends keyof TWith // Handle cases with an index signature ? { [key: string]: TWith[string] } : number extends keyof TWith ? { [key: number]: TWith[number] } : // eslint-disable-next-line @typescript-eslint/ban-types {}) : TWith : never; /** @internal */ export const contextMiddlewareMarker = "contextMiddlewareMarker" as "contextMiddlewareMarker" & { __brand: "contextMiddlewareMarker"; }; type ContextMiddlewareMarker = typeof contextMiddlewareMarker; interface ContextMiddlewareResultBase { /** * All middlewares should pass through their `next()`'s output. * Requiring this marker makes sure that can't be forgotten at compile-time. */ readonly marker: ContextMiddlewareMarker; } interface ContextMiddlewareOKResult<_TContextOverride> extends ContextMiddlewareResultBase { ok: true; data: unknown; } interface ContextMiddlewareErrorResult<_TContextOverride> extends ContextMiddlewareResultBase { ok: false; error: Error; // should be our error } /** * @internal */ export type ContextMiddlewareResult<_TContextOverride> = | ContextMiddlewareErrorResult<_TContextOverride> | ContextMiddlewareOKResult<_TContextOverride>; export type ContextMiddlewareFunction = { (opts: { ctx: Simplify>; meta: RunMetadata; next: { (): Promise>; <$ContextOverride>(ctx: $ContextOverride): Promise>; }; }): Promise>; }; export const unsetMarker = Symbol("unsetMarker"); export type UnsetMarker = typeof unsetMarker; export interface ContextBuilder { use<$ContextOverridesOut>( fn: ContextMiddlewareFunction ): ContextBuilder>; } export type AnyContextBuilder = ContextBuilder; export function createContext( initialContext?: TContext ): ContextBuilder { const builder: AnyContextBuilder = { use(middlewareFn) { return {} as AnyContextBuilder; }, }; return builder; } const contextBuilder = createContext({ foo: "bar" }); const context = contextBuilder .use((opts) => { return opts.next({ baz: "whatever", }); }) .use((opts) => { return opts.next({ db: { find: async (id: string) => { return "hello"; }, }, }); }); type ctx = inferContext; const contextBuilder2 = createContext(); type ctx2 = inferContext; const contextBuilder3 = createContext({ bar: "baz" }); type ctx3 = inferContext; const contextBuilder4 = createContext().use((opts) => { return opts.next({ hello: "world", }); }); type ctx4 = inferContext; export type TaskOptions< TOutput, TContext extends AnyContextBuilder, TIdentifier extends string, TParser extends Parser | undefined = undefined, > = { /** An id for your task. This must be unique inside your project and not change between versions. */ id: TIdentifier; schema?: TParser; context?: TContext; /** This gets called when a task is triggered. It's where you put the code you want to execute. * * @param payload - The payload that is passed to your task when it's triggered. This must be JSON serializable. * @param params - Metadata about the run. */ run: (params: Simplify, TContext>>) => Promise; }; export interface Task< TOutput, TIdentifier extends string, TParser extends Parser | undefined = undefined, > { /** * The id of the task. */ id: TIdentifier; /** * Trigger a task with the given payload, and continue without waiting for the result. If you want to wait for the result, use `triggerAndWait`. Returns the id of the triggered task run. * @param payload * @param options * @returns TriggerResult * - `id` - The id of the triggered task run. */ trigger: ( payload: Simplify>, options?: TriggerTaskOptions ) => Promise; /** * Trigger a task with the given payload, and wait for the result. Returns the result of the task run * @param payload * @param options - Options for the task run * @returns TaskRunResult * @example * ``` * const result = await task.triggerAndWait({ foo: "bar" }); * * if (result.ok) { * console.log(result.output); * } else { * console.error(result.error); * } * ``` */ triggerAndWait: ( payload: Simplify>, options?: TriggerTaskOptions ) => Promise>; } export type AnyTask = Task; type inferParserIn = TParser extends Parser ? inferParser["in"] : TDefault; type inferParserOut = TParser extends Parser ? inferParser["out"] : TDefault; export type TaskPayloadIn = TTask extends Task ? inferParserIn : never; export type TaskPayloadOut = TTask extends Task ? inferParserOut : never; export type TaskOutput = TTask extends Task ? TOutput : never; export type TaskIdentifier = TTask extends Task ? TIdentifier : never; export type TaskTypes = TTask extends Task< infer TOutput, infer TIdentifier, infer TParser > ? { id: TIdentifier; payloadIn: inferParserIn; payloadOut: inferParserOut; output: TOutput; } : never; export type TriggerTaskOptions = { idempotencyKey?: string; maxAttempts?: number; startAt?: Date; startAfter?: number; concurrencyKey?: string; }; export type Prettify = { [K in keyof T]: T[K]; } & {}; export function task< TOutput, TContext extends AnyContextBuilder, TIdentifier extends string, TParser extends Parser | undefined = undefined, >( options: TaskOptions ): Task { return createTask(options); } export function createTask< TOutput, TContext extends AnyContextBuilder, TIndentifier extends string, TParser extends Parser | undefined = undefined, >( params: TaskOptions ): Task { const task: Task = { id: params.id, trigger: async (payload, options) => { return { id: "run_1234", }; }, triggerAndWait: async (payload, options) => { const output = await params.run({ meta: { run: "run_1234" }, payload: payload as unknown as inferParserOut, // Actually do the parsing ctx: {} as inferContext, }); return { ok: true, id: "run_1234", output, }; }, }; return task; } export interface TaskLibraryRecord { [key: string]: AnyTask | TaskLibraryRecord; } export interface TaskLibrary { _def: { record: TRecord }; } export type AnyTaskLibrary = TaskLibrary; export type CreateTaskLibraryOptions = { [key: string]: AnyTask | AnyTaskLibrary | CreateTaskLibraryOptions; }; export type DecorateCreateTaskLibraryOptions = { [K in keyof TTaskLibraryOptions]: TTaskLibraryOptions[K] extends infer $Value ? $Value extends AnyTask ? $Value : $Value extends TaskLibrary ? TRecord : $Value extends CreateTaskLibraryOptions ? DecorateCreateTaskLibraryOptions<$Value> : never : never; }; function taskLibrary( input: TInput ): TaskLibrary>; function taskLibrary(input: TInput): TaskLibrary; function taskLibrary(input: TaskLibraryRecord | CreateTaskLibraryOptions) { // TODO: reserved words return { _def: { record: input, }, }; } // ======== client side type DecorateTask = { trigger: (id: TaskIdentifier, payload: TaskPayloadIn) => Promise<{ id: string }>; }; type DecoratedTaskLibraryRecord< TTaskLibrary extends AnyTaskLibrary, TRecord extends TaskLibraryRecord, > = { [TKey in keyof TRecord]: TRecord[TKey] extends infer $Value ? $Value extends TaskLibraryRecord ? DecoratedTaskLibraryRecord : $Value extends AnyTask ? DecorateTask<$Value> : never : never; }; export type inferTaskLibraryClient = DecoratedTaskLibraryRecord; export type CreateTriggerClient = { lib: inferTaskLibraryClient; runs: { retrieve: (id: string) => Promise<{ status: boolean }>; }; }; export type CreateTriggerClientOptions = { secretKey?: string; }; export function createTriggerClient( options?: CreateTriggerClientOptions ): CreateTriggerClient { return {} as CreateTriggerClient; } // trigger/my-tasks.ts const taskOne = task({ id: "task-1", run: async () => { const handle = await taskTwo.trigger({ url: "https://trigger.dev" }); const result = await taskTwo.triggerAndWait({ url: "https://trigger.dev" }); return "foo-bar"; }, }); const taskTwo = task({ id: "task-2", async run(params) { return { hello: "world", payload: params.payload.other, }; }, }); const userTaskOne = task({ id: "user/task-1", context: contextBuilder4, run: async (params) => { return "foo-bar"; }, }); const userTaskTwo = task({ id: "user/task-2", context: contextBuilder3, run: async (params) => { return "foo-bar"; }, }); const zodTaskOne = task({ id: "zod/task-1", context: contextBuilder, schema: z.object({ foo: z.string() }), run: async (params) => {}, }); const zodTaskTwo = task({ id: "zod/task-2", schema: z.object({ foo: z.string(), isAdmin: z.boolean().default(false) }), context: contextBuilder2, run: async (params) => { console.log(params.payload.foo, params.meta.run); }, }); const valibotTaskOne = task({ id: "valibot/task-1", schema: wrap( v.object({ foo: v.string(), }) ), run: async (params) => { await zodTaskOne.trigger({ foo: "bar" }); await zodTaskTwo.trigger({ foo: "bar" }); await valibotTaskTwo.trigger({ foo: "bar" }); }, }); const valibotTaskTwo = task({ id: "valibot/task-2", schema: wrap( v.object({ foo: v.string(), isAdmin: v.optional(v.boolean(), true), }) ), run: async (params) => { await valibotTaskOne.trigger({ foo: "bar" }); }, }); // in trigger/lib.ts const myTaskLibrary = taskLibrary({ myTasks: { taskOne, taskTwo }, }); const userTaskLibrary = taskLibrary({ userTaskOne, userTaskTwo, }); const zodTaskLibrary = taskLibrary({ zodTaskOne, zodTaskTwo, }); const valibotTaskLibrary = taskLibrary({ valibotTaskOne, valibotTaskTwo, }); export const library = taskLibrary({ foo: myTaskLibrary, bar: userTaskLibrary, zod: zodTaskLibrary, valibot: valibotTaskLibrary, }); // Export the library type export type Library = typeof library; // Now on the client const client = createTriggerClient({ secretKey: "tr_dev_1234", }); client.runs.retrieve("run_12343"); // Call regular API client calls // Tasks are now available under lib client.lib.foo.myTasks.taskOne.trigger("task-1", { hello: "world" }); client.lib.bar.userTaskOne.trigger("user/task-1", { userId: "user_123" }); client.lib.bar.userTaskTwo.trigger("user/task-2", { userId: "user_123", isAdmin: true, }); client.lib.bar.userTaskTwo.trigger("user/task-2", { userId: "user_123", isAdmin: false, }); client.lib.zod.zodTaskOne.trigger("zod/task-1", { foo: "bar" }); client.lib.zod.zodTaskTwo.trigger("zod/task-2", { foo: "bar" }); client.lib.zod.zodTaskTwo.trigger("zod/task-2", { foo: "bar", isAdmin: false }); client.lib.valibot.valibotTaskTwo.trigger("valibot/task-2", { foo: "bar" }); client.lib.valibot.valibotTaskTwo.trigger("valibot/task-2", { foo: "bar", isAdmin: true, });