@convex-dev/workpool
Version:
A Convex component for managing async work.
316 lines • 12 kB
JavaScript
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