UNPKG

@convex-dev/workpool

Version:

A Convex component for managing async work.

316 lines 12 kB
import { createFunctionHandle, internalMutationGeneric, } from "convex/server"; import { v } from "convex/values"; import { DEFAULT_LOG_LEVEL } from "../component/logging.js"; import { DEFAULT_MAX_PARALLELISM, vResultValidator, } from "../component/shared.js"; import { safeFunctionName, } from "./utils.js"; export { vResultValidator }; export { retryBehavior as vRetryBehavior } from "../component/shared.js"; export { logLevel as vLogLevel } from "../component/logging.js"; export const vWorkIdValidator = v.string(); export { /** @deprecated Use `vWorkIdValidator` instead. */ vWorkIdValidator as workIdValidator, /** @deprecated Use `vResultValidator` instead. */ vResultValidator as resultValidator, }; /** Equivalent to `vOnCompleteArgs(<your-context-validator>)`. */ export const vOnComplete = vOnCompleteArgs(v.any()); /** @deprecated Use `vOnCompleteArgs()` instead. */ export const vOnCompleteValidator = vOnCompleteArgs; // Attempts will run with delay [0, 250, 500, 1000, 2000] (ms) export const DEFAULT_RETRY_BEHAVIOR = { maxAttempts: 5, initialBackoffMs: 250, base: 2, }; export class Workpool { component; options; /** * Initializes a Workpool. * * Note: if you want different pools, you need to *create different instances* * of Workpool in convex.config.ts. It isn't sufficient to have different * instances of this class. * * @param component - The component to use, like `components.workpool` from * `./_generated/api.ts`. * @param options - The {@link WorkpoolOptions} for the Workpool. */ constructor(component, options) { this.component = component; this.options = options; } /** * Enqueues an action to be run. * * @param ctx - The mutation or action context that can call ctx.runMutation. * @param fn - The action to run, like `internal.example.myAction`. * @param fnArgs - The arguments to pass to the action. * @param options - The options for the action to specify retry behavior, * onComplete handling, and scheduling via `runAt` or `runAfter`. * @returns The ID of the work that was enqueued. */ async enqueueAction(ctx, fn, fnArgs, options) { const retryBehavior = getRetryBehavior(this.options.defaultRetryBehavior, this.options.retryActionsByDefault, options?.retry); return enqueue(this.component, ctx, "action", fn, fnArgs, { retryBehavior, ...this.options, ...options, }); } /** * Enqueues a batch of actions to be run. * Each action will be run independently, and the onComplete handler will * be called for each action. * * @param ctx - The mutation or action ctx that can call ctx.runMutation. * @param fn - The action to run, like `internal.example.myAction`. * @param argsArray - The arguments to pass to the action. * @param options - The options for the actions to specify retry behavior, * onComplete handling, and scheduling via `runAt` or `runAfter`. * @returns The IDs of the work that was enqueued. */ async enqueueActionBatch(ctx, fn, argsArray, options) { const retryBehavior = getRetryBehavior(this.options.defaultRetryBehavior, this.options.retryActionsByDefault, options?.retry); return enqueueBatch(this.component, ctx, "action", fn, argsArray, { retryBehavior, ...this.options, ...options, }); } /** * Enqueues a mutation to be run. * * Note: mutations are not retried by the workpool. Convex automatically * retries them on database conflicts and transient failures. * Because they're deterministic, external retries don't provide any benefit. * * @param ctx - The mutation or action context that can call ctx.runMutation. * @param fn - The mutation to run, like `internal.example.myMutation`. * @param fnArgs - The arguments to pass to the mutation. * @param options - The options for the mutation to specify onComplete handling * and scheduling via `runAt` or `runAfter`. */ async enqueueMutation(ctx, fn, fnArgs, options) { return enqueue(this.component, ctx, "mutation", fn, fnArgs, { ...this.options, ...options, }); } /** * Enqueues a batch of mutations to be run. * Each mutation will be run independently, and the onComplete handler will * be called for each mutation. * * @param ctx - The mutation or action context that can call ctx.runMutation. * @param fn - The mutation to run, like `internal.example.myMutation`. * @param argsArray - The arguments to pass to the mutations. * @param options - The options for the mutations to specify onComplete handling * and scheduling via `runAt` or `runAfter`. */ async enqueueMutationBatch(ctx, fn, argsArray, options) { return enqueueBatch(this.component, ctx, "mutation", fn, argsArray, { ...this.options, ...options, }); } /** * Enqueues a query to be run. * Usually not what you want, but it can be useful during workflows. * The query is run in a mutation and the result is returned to the caller, * so it can conflict if other mutations are writing the value. * * @param ctx - The mutation or action context that can call ctx.runMutation. * @param fn - The query to run, like `internal.example.myQuery`. * @param fnArgs - The arguments to pass to the query. * @param options - The options for the query to specify onComplete handling * and scheduling via `runAt` or `runAfter`. */ async enqueueQuery(ctx, fn, fnArgs, options) { return enqueue(this.component, ctx, "query", fn, fnArgs, { ...this.options, ...options, }); } /** * Enqueues a batch of queries to be run. * Each query will be run independently, and the onComplete handler will * be called for each query. * * @param ctx - The mutation or action context that can call ctx.runMutation. * @param fn - The query to run, like `internal.example.myQuery`. * @param argsArray - The arguments to pass to the queries. * @param options - The options for the queries to specify onComplete handling * and scheduling via `runAt` or `runAfter`. */ async enqueueQueryBatch(ctx, fn, argsArray, options) { return enqueueBatch(this.component, ctx, "query", fn, argsArray, { ...this.options, ...options, }); } /** * Cancels a work item. If it's already started, it will be allowed to finish * but will not be retried. * * @param ctx - The mutation or action context that can call ctx.runMutation. * @param id - The ID of the work to cancel. */ async cancel(ctx, id) { await ctx.runMutation(this.component.lib.cancel, { id, logLevel: this.options.logLevel ?? DEFAULT_LOG_LEVEL, }); } /** * Cancels all pending work items. See {@link cancel}. * * @param ctx - The mutation or action context that can call ctx.runMutation. */ async cancelAll(ctx) { await ctx.runMutation(this.component.lib.cancelAll, { logLevel: this.options.logLevel ?? DEFAULT_LOG_LEVEL, }); } /** * Gets the status of a work item. * * @param ctx - The query context that can call ctx.runQuery. * @param id - The ID of the work to get the status of. * @returns The status of the work item. One of: * - `{ state: "pending", previousAttempts: number }` * - `{ state: "running", previousAttempts: number }` * - `{ state: "finished" }` */ async status(ctx, id) { return ctx.runQuery(this.component.lib.status, { id }); } /** * Gets the status of a batch of work items. * * @param ctx - The query context that can call ctx.runQuery. * @param ids - The IDs of the work to get the status of. * @returns The status of the work items. */ async statusBatch(ctx, ids) { return ctx.runQuery(this.component.lib.statusBatch, { ids }); } /** * Defines a mutation that will be run after a work item completes. * You can pass this to a call to enqueue* like so: * ```ts * export const myOnComplete = workpool.defineOnComplete({ * context: v.literal("myContextValue"), // optional * handler: async (ctx, {workId, context, result}) => { * // ... do something with the result * }, * }); * * // in some other function: * const workId = await workpool.enqueueAction(ctx, internal.foo.bar, { * // ... args to action * }, { * onComplete: internal.foo.myOnComplete, * }); * ``` */ defineOnComplete({ context, handler, }) { return internalMutationGeneric({ args: vOnCompleteArgs(context), handler, }); } } /** * Returns a validator to use for the onComplete mutation. * To be used like: * ```ts * export const myOnComplete = internalMutation({ * args: vOnCompleteArgs(v.string()), * handler: async (ctx, {workId, context, result}) => { * // context has been validated as a string * // ... do something with the result * }, * }); * @param context - The context validator. If not provided, it will be `v.any()`. * @returns The validator for the onComplete mutation. */ export function vOnCompleteArgs(context) { return v.object({ workId: vWorkIdValidator, context: (context ?? v.optional(v.any())), result: vResultValidator, }); } // ensure OnCompleteArgs satisfies SharedOnCompleteArgs const _ = {}; const _2 = {}; // // Helper functions // function getRetryBehavior(defaultRetryBehavior, retryActionsByDefault, retryOverride) { const defaultRetry = defaultRetryBehavior ?? DEFAULT_RETRY_BEHAVIOR; const retryByDefault = retryActionsByDefault ?? false; if (retryOverride === true) { return defaultRetry; } if (retryOverride === false) { return undefined; } return retryOverride ?? (retryByDefault ? defaultRetry : undefined); } async function enqueueArgs(fn, opts) { const [fnHandle, fnName] = typeof fn === "string" && fn.startsWith("function://") ? [fn, opts?.name ?? fn] : [await createFunctionHandle(fn), opts?.name ?? safeFunctionName(fn)]; const onComplete = opts?.onComplete ? { fnHandle: await createFunctionHandle(opts.onComplete), context: opts.context, } : undefined; return { fnHandle, fnName, onComplete, runAt: getRunAt(opts), retryBehavior: opts?.retryBehavior, config: { logLevel: opts?.logLevel ?? DEFAULT_LOG_LEVEL, maxParallelism: opts?.maxParallelism ?? DEFAULT_MAX_PARALLELISM, }, }; } function getRunAt(options) { if (!options) { return Date.now(); } if (options.runAt !== undefined) { return options.runAt; } if (options.runAfter !== undefined) { return Date.now() + options.runAfter; } return Date.now(); } export async function enqueueBatch(component, ctx, fnType, fn, fnArgsArray, options) { const { config, ...defaults } = await enqueueArgs(fn, options); const ids = await ctx.runMutation(component.lib.enqueueBatch, { items: fnArgsArray.map((fnArgs) => ({ ...defaults, fnArgs, fnType, })), config, }); return ids; } export async function enqueue(component, ctx, fnType, fn, fnArgs, options) { const id = await ctx.runMutation(component.lib.enqueue, { ...(await enqueueArgs(fn, options)), fnArgs, fnType, }); return id; } //# sourceMappingURL=index.js.map