alepha
Version:
Easy-to-use modern TypeScript framework for building many kind of applications.
204 lines (178 loc) • 6.06 kB
text/typescript
import {
$inject,
type Async,
createPrimitive,
KIND,
PipelinePrimitive,
type PipelinePrimitiveOptions,
type Static,
type TSchema,
} from "alepha";
import type { DateTime, DurationLike } from "alepha/datetime";
import {
JobProvider,
type JobTriggerContext,
type PushManyItem,
type PushOptions,
} from "../providers/JobProvider.ts";
/**
* Job primitive for defining scheduled (cron) or queued (push) tasks.
*
* A job must be either **cron-only** (pass `cron`) or **queue-only**
* (pass `schema`), never both. To run scheduled work that processes
* payloads, compose two jobs: a cron that pushes payloads, and a
* queue job that handles them.
*/
export const $job = <T extends TSchema = TSchema>(
options: JobPrimitiveOptions<T>,
): JobPrimitive<T> => {
return createPrimitive(JobPrimitive<T>, options);
};
// -----------------------------------------------------------------------------------------------------------------
export interface JobHandlerArgs<T extends TSchema = TSchema> {
payload: Static<T>;
attempt: number;
now: DateTime;
signal: AbortSignal;
executionId: string;
}
export interface JobRetryOptions {
retries: number;
when?: (error: Error) => boolean;
}
export type JobPriority = "critical" | "high" | "normal" | "low";
export interface JobPrimitiveOptions<T extends TSchema = TSchema>
extends PipelinePrimitiveOptions {
/**
* Optional explicit job name. Defaults to `ClassName.propertyKey`.
* Recommended convention for framework-internal jobs: `api:module:jobName`.
*/
name?: string;
/**
* Human-readable description (shown in the admin UI).
*/
description?: string;
/**
* Payload schema (TypeBox). When set, the job is queue-mode.
* Must not be combined with `cron`.
*/
schema?: T;
/**
* Cron expression for recurring execution. When set, the job is cron-mode.
* Must not be combined with `schema`.
*/
cron?: string;
/**
* Retry policy for queue-mode and direct-mode jobs.
* Cron-mode jobs do not retry — the next tick re-runs.
*
* Retries are picked up by the reconciliation sweep, so retry granularity
* is bounded by `sweepCron` (default 15 minutes). The first retry may run
* earlier than 15 minutes if the sweep tick happens sooner.
*/
retry?: JobRetryOptions;
/**
* **Cron-mode only.** Whether to acquire a distributed lock around the
* cron tick so that only one instance of a multi-replica deployment runs
* the handler per tick.
*
* Has **no effect** on queue-mode and direct-mode jobs — those rely on
* the outbox `claim()` UPDATE-guard to serialize work instead, which is
* always on.
*
* To get cross-instance coordination on Docker / Node deployments,
* register a real `LockProvider` (e.g. `alepha/lock/redis`). The default
* `MemoryLockProvider` is per-process only.
*
* @default true
*/
lock?: boolean;
/**
* Max execution time per attempt. Handler receives an `AbortSignal`.
*/
timeout?: DurationLike;
/**
* Default priority for pushed jobs. Used by the sweep to order
* dispatch when there is a backlog. Real-time queue consumption
* is FIFO.
* @default "normal"
*/
priority?: JobPriority;
/**
* Whether to record successful executions.
*
* - `"error"` (default for queue): only error/cancelled rows kept
* - `"all"`: keep success rows too (bounded by `keepLastSuccess`)
* - `"none"`: fire-and-forget, no row even on error
*
* **Cron jobs default to keeping their last successful run** (`record: "all"`
* with `keep.ok = 1`) so the admin "Last run" is accurate — set
* `record: "error"` to opt out (e.g. for very high-frequency crons).
*
* Note: queue-mode jobs always write a `pending` row at push time (outbox).
* This setting controls whether that row is kept on success.
*/
record?: "error" | "all" | "none";
/**
* Override the global ring-buffer trim for this job.
*
* - `{ ok: 0, error: 0 }` — **keep forever** (no sweep trim). Useful for
* audit-heavy jobs where retention is time-based (handled by a separate
* cron) rather than count-based.
* - `{ ok: 50 }` — keep last 50 successes; fall back to global default for errors.
* - omitted — use global `keepLastSuccess` / `keepLastError` from `jobConfig`.
*/
keep?: {
ok?: number;
error?: number;
};
/**
* Handler function. For cron-mode, `payload` is `undefined`.
*/
handler: (args: JobHandlerArgs<T>) => Async<void>;
}
// -----------------------------------------------------------------------------------------------------------------
export class JobPrimitive<
T extends TSchema = TSchema,
> extends PipelinePrimitive<JobPrimitiveOptions<T>> {
protected readonly jobProvider = $inject(JobProvider);
public get name(): string {
return (
this.options.name ??
`${this.config.service.name}.${this.config.propertyKey}`
);
}
protected onInit() {
const handler = this.handler.run.bind(this.handler);
this.jobProvider.registerJob(this.name, { ...this.options, handler });
}
/**
* Push a single payload to the queue (queue-mode only).
*/
public async push(
payload: Static<T>,
options?: PushOptions,
): Promise<string> {
return this.jobProvider.push(this.name, payload, options);
}
/**
* Push multiple payloads at once (queue-mode only).
* Batched INSERT + batched queue send when supported.
*/
public async pushMany(items: Array<PushManyItem<T>>): Promise<string[]> {
return this.jobProvider.pushMany(this.name, items);
}
/**
* Cancel a pending or running execution.
*/
public async cancel(executionId: string): Promise<void> {
return this.jobProvider.cancel(executionId);
}
/**
* Manually fire a cron-mode job, or trigger a queue-mode job with an explicit payload.
*/
public async trigger(context?: JobTriggerContext<T>): Promise<void> {
return this.jobProvider.trigger(this.name, context);
}
}
$job[KIND] = JobPrimitive;