UNPKG

alepha

Version:

Easy-to-use modern TypeScript framework for building many kind of applications.

1,249 lines 66.4 kB
import { Alepha, AlephaError, Async, KIND, PipelinePrimitive, PipelinePrimitiveOptions, Static, TNull, TObject, TOptional, TSchema, TUnion } from "alepha"; import { BackgroundTaskProvider } from "alepha/background"; import { LockProvider } from "alepha/lock"; import { CronProvider } from "alepha/scheduler"; import { LogEntry } from "alepha/logger"; import { CryptoProvider } from "alepha/crypto"; import { DateTime, DateTimeProvider, DurationLike } from "alepha/datetime"; import { BuildExtraConfigColumns, SQL } from "drizzle-orm"; import { PgColumnBuilderBase, PgSequenceOptions, PgTableExtraConfigValue, UpdateDeleteAction } from "drizzle-orm/pg-core"; import "drizzle-orm/pg-core/foreign-keys"; import "drizzle-orm/pg-core/session"; import "drizzle-kit/api"; import "drizzle-orm/d1"; import "drizzle-orm/sqlite-core"; //#region ../../src/orm/core/schemas/insertSchema.d.ts /** * Transforms a TObject schema for insert operations. * All default properties at the root level are made optional. * Generated columns are excluded entirely. * * @example * Before: { name: string; age: number(default=0); fullName: generated } * After: { name: string; age?: number; } */ type TObjectInsert<T extends TObject> = TObject<{ [K in keyof T["shape"] as T["shape"][K] extends { [PG_GENERATED]: any; } ? never : K]: T["shape"][K] extends { [PG_DEFAULT]: any; } | { [PG_ORGANIZATION]: any; } ? TOptional<Extract<T["shape"][K], TSchema>> : T["shape"][K]; }>; //#endregion //#region ../../src/orm/core/schemas/updateSchema.d.ts /** * Transforms a TObject schema for update operations. * All optional properties at the root level are made nullable (i.e., `T | null`). * Generated columns are excluded entirely. * * @example * Before: { name?: string; age: number; fullName: generated } * After: { name?: string | null; age: number; } */ type TObjectUpdate<T extends TObject> = TObject<{ [K in keyof T["shape"] as T["shape"][K] extends { [PG_GENERATED]: any; } ? never : K]: T["shape"][K] extends TOptional<infer U extends TSchema> ? TOptional<TUnion<[U, TNull]>> : T["shape"][K]; }>; //#endregion //#region ../../src/orm/core/primitives/$entity.d.ts interface EntityPrimitiveOptions<T extends TObject, Keys = keyof Static<T>> { /** * The database table name that will be created for this entity. * If not provided, name will be inferred from the $repository variable name. */ name: string; /** * TypeBox schema defining the table structure and column types. */ schema: T; /** * Database indexes to create for query optimization. */ indexes?: (Keys | { /** * Single column to index. */ column: Keys; /** * Whether this should be a unique index (enforces uniqueness constraint). */ unique?: boolean; /** * Custom name for the index. If not provided, generates name automatically. */ name?: string; /** * Partial index condition. Only rows matching this SQL expression are indexed. */ where?: SQL; } | { /** * Multiple columns for composite index (order matters for query optimization). */ columns: Keys[]; /** * Whether this should be a unique index (enforces uniqueness constraint). */ unique?: boolean; /** * Custom name for the index. If not provided, generates name automatically. */ name?: string; /** * Partial index condition. Only rows matching this SQL expression are indexed. */ where?: SQL; } | { /** * SQL expressions for expression-based indexes. * * Can include column references and SQL functions like `LOWER()`, `UPPER()`, etc. * Columns and expressions can be mixed together. * * @example * ```ts * // Case-insensitive unique username per realm * indexes: [{ * expressions: (self) => [self.realm, sql`LOWER(${self.username})`], * unique: true, * name: "users_realm_username_lower_idx", * }] * ``` */ expressions: (self: Record<Keys & string, any>) => (SQL | any)[]; /** * Whether this should be a unique index (enforces uniqueness constraint). */ unique?: boolean; /** * Custom name for the index. If not provided, generates name automatically. */ name: string; /** * Partial index condition. Only rows matching this SQL expression are indexed. */ where?: SQL; })[]; /** * Foreign key constraints to maintain referential integrity. */ foreignKeys?: Array<{ /** * Optional name for the foreign key constraint. */ name?: string; /** * Local columns that reference the foreign table. */ columns: Array<keyof Static<T>>; /** * Referenced columns in the foreign table. * Must be EntityColumn references from other entities. */ foreignColumns: Array<() => EntityColumn<any>>; }>; /** * Additional table constraints for data validation. * * Constraints enforce business rules at the database level, providing * an additional layer of data integrity beyond application validation. * * **Constraint Types**: * - **Unique constraints**: Prevent duplicate values across columns * - **Check constraints**: Enforce custom validation rules with SQL expressions * * @example * ```ts * constraints: [ * { * name: "unique_user_email", * columns: ["email"], * unique: true * }, * { * name: "valid_age_range", * columns: ["age"], * check: sql`age >= 0 AND age <= 150` * }, * { * name: "unique_user_username_per_tenant", * columns: ["tenantId", "username"], * unique: true * } * ] * ``` */ constraints?: Array<{ /** * Columns involved in this constraint. */ columns: Array<keyof Static<T>>; /** * Optional name for the constraint. */ name?: string; /** * Whether this is a unique constraint. */ unique?: boolean | {}; /** * SQL expression for check constraint validation. */ check?: SQL; }>; /** * Advanced Drizzle ORM configuration for complex table setups. */ config?: (self: BuildExtraConfigColumns<string, FromSchema<T>, "pg">) => PgTableExtraConfigValue[]; } declare class EntityPrimitive<T extends TObject = TObject> { readonly options: EntityPrimitiveOptions<T>; constructor(options: EntityPrimitiveOptions<T>); alias(alias: string): this; get cols(): EntityColumns<T>; get name(): string; get schema(): T; protected _insertSchema?: TObjectInsert<T>; get insertSchema(): TObjectInsert<T>; protected _updateSchema?: TObjectUpdate<T>; get updateSchema(): TObjectUpdate<T>; } /** * Convert a schema to columns. */ type FromSchema<T extends TObject> = { [key in keyof T["properties"]]: PgColumnBuilderBase; }; type EntityColumn<T extends TObject> = { name: string; entity: EntityPrimitive<T>; }; type EntityColumns<T extends TObject> = { [key in keyof T["properties"]]: EntityColumn<T>; }; //#endregion //#region ../../src/orm/core/constants/PG_SYMBOLS.d.ts declare const PG_DEFAULT: unique symbol; declare const PG_PRIMARY_KEY: unique symbol; declare const PG_CREATED_AT: unique symbol; declare const PG_UPDATED_AT: unique symbol; declare const PG_DELETED_AT: unique symbol; declare const PG_VERSION: unique symbol; declare const PG_IDENTITY: unique symbol; declare const PG_ENUM: unique symbol; declare const PG_REF: unique symbol; declare const PG_GENERATED: unique symbol; declare const PG_ORGANIZATION: unique symbol; /** * @deprecated Use `PG_IDENTITY` instead. */ declare const PG_SERIAL: unique symbol; type PgSymbols = { [PG_DEFAULT]: {}; [PG_PRIMARY_KEY]: {}; [PG_CREATED_AT]: {}; [PG_UPDATED_AT]: {}; [PG_DELETED_AT]: {}; [PG_VERSION]: {}; [PG_IDENTITY]: PgIdentityOptions; [PG_REF]: PgRefOptions; [PG_ENUM]: PgEnumOptions; [PG_GENERATED]: PgGeneratedOptions; [PG_ORGANIZATION]: PgOrganizationOptions; /** * @deprecated Use `PG_IDENTITY` instead. */ [PG_SERIAL]: {}; }; type PgSymbolKeys = keyof PgSymbols; interface PgOrganizationOptions { /** * Fail-closed tenant scoping. When `true`, the repository (a) refuses any * read/write with no resolved tenant instead of falling through to an * unfiltered "see/write everything" query, and (b) drops the * `OR organizationId IS NULL` "global row" escape — a scoped tenant never * sees NULL/global rows. Use for security-sensitive tables. Defaults to * `false` (the historic fail-open semantics). */ strict?: boolean; } type PgIdentityOptions = { mode: "always" | "byDefault"; } & PgSequenceOptions & { name?: string; }; interface PgEnumOptions { name?: string; description?: string; } interface PgGeneratedOptions { /** * SQL expression for the generated column. */ expression: SQL; /** * Storage mode for the generated column. * - `"stored"` — value is computed on write and stored on disk (default for PostgreSQL). * - `"virtual"` — value is computed on read (default for SQLite). */ mode?: "stored" | "virtual"; } interface PgRefOptions { ref: () => { name: string; entity: EntityPrimitive; }; actions?: { onUpdate?: UpdateDeleteAction; onDelete?: UpdateDeleteAction; }; } //#endregion //#region ../../src/orm/core/helpers/pgAttr.d.ts /** * Type representation. */ type PgAttr<T extends TSchema, TAttr extends PgSymbolKeys> = T & { [K in TAttr]: PgSymbols[K]; }; //#endregion //#region ../../src/orm/core/schemas/databaseEnvSchema.d.ts /** * Base database environment schema. * * Defines the `DATABASE_URL` connection string used by all ORM providers * to determine the database driver and connection target. * * Supported URL formats: * - `sqlite://:memory:` or `sqlite://./path/to/db` — SQLite (Node.js or Bun) * - `postgres://user:password@host:port/database` — PostgreSQL (Node.js or Bun) * - `pglite://:memory:` or `pglite://./path` — PGlite (embedded Postgres) * - `d1://BINDING_NAME` — Cloudflare D1 * - `hyperdrive://BINDING_NAME` — Cloudflare Hyperdrive */ declare const databaseEnvSchema: import("zod").ZodObject<{ DATABASE_URL: import("zod").ZodOptional<import("zod").ZodString>; DATABASE_SYNC: import("zod").ZodOptional<import("zod").ZodBoolean>; }, import("zod/v4/core").$strip>; declare module "alepha" { interface Env extends Partial<Static<typeof databaseEnvSchema>> {} } //#endregion //#region ../../src/api/jobs/entities/jobExecutionEntity.d.ts /** * Job execution record. * * Stores durable state for queue-mode jobs (outbox pattern) and error records * for cron-mode jobs. Successful executions are trimmed by the sweep to keep * the last N rows per job (configurable via `jobConfig.keepLastSuccess`). * * Status transitions: * - queue push → pending (or `scheduled` if `delay`/`scheduledAt` was given) * - worker claim → running * - success → ok (or row deleted, depending on `record` and `keepLastSuccess`) * - terminal failure → error * - retryable failure → scheduled (with scheduledAt = now; sweep picks it up) * - delay → scheduled (with scheduledAt = now + delay) * - sweep picks due ones → pending * - cancel → cancelled */ declare const jobExecutionEntity: import("alepha/orm").EntityPrimitive<import("zod").ZodObject<{ id: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_PRIMARY_KEY>, typeof import("alepha/orm").PG_DEFAULT>; createdAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_CREATED_AT>, typeof import("alepha/orm").PG_DEFAULT>; updatedAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_UPDATED_AT>, typeof import("alepha/orm").PG_DEFAULT>; jobName: import("zod").ZodString; key: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; organizationId: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; status: import("alepha/orm").PgAttr<import("zod").ZodEnum<{ cancelled: "cancelled"; error: "error"; ok: "ok"; pending: "pending"; running: "running"; scheduled: "scheduled"; }>, typeof import("alepha/orm").PG_DEFAULT>; priority: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; attempt: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; maxAttempts: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; payload: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>; scheduledAt: import("zod").ZodOptional<import("zod").ZodString>; startedAt: import("zod").ZodOptional<import("zod").ZodString>; completedAt: import("zod").ZodOptional<import("zod").ZodString>; error: import("zod").ZodOptional<import("zod").ZodString>; logs: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{ level: import("zod").ZodEnum<{ DEBUG: "DEBUG"; ERROR: "ERROR"; INFO: "INFO"; SILENT: "SILENT"; TRACE: "TRACE"; WARN: "WARN"; }>; message: import("zod").ZodString; service: import("zod").ZodString; module: import("zod").ZodString; context: import("zod").ZodOptional<import("zod").ZodString>; app: import("zod").ZodOptional<import("zod").ZodString>; data: import("zod").ZodOptional<import("zod").ZodAny>; timestamp: import("zod").ZodNumber; }, import("zod/v4/core").$strip>>>; triggeredBy: import("zod").ZodOptional<import("zod").ZodString>; triggeredByName: import("zod").ZodOptional<import("zod").ZodString>; cancelledBy: import("zod").ZodOptional<import("zod").ZodString>; cancelledByName: import("zod").ZodOptional<import("zod").ZodString>; }, import("zod/v4/core").$strip>>; type JobExecutionEntity = Static<typeof jobExecutionEntity.schema>; type JobStatus = "pending" | "running" | "scheduled" | "ok" | "error" | "cancelled"; //#endregion //#region ../../src/api/jobs/primitives/$job.d.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. */ declare const $job: { <T extends TSchema = import("alepha").ZType>(options: JobPrimitiveOptions<T>): JobPrimitive<T>; [KIND]: typeof JobPrimitive; }; interface JobHandlerArgs<T extends TSchema = TSchema> { payload: Static<T>; attempt: number; now: DateTime; signal: AbortSignal; executionId: string; } interface JobRetryOptions { retries: number; when?: (error: Error) => boolean; } type JobPriority = "critical" | "high" | "normal" | "low"; 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>; } declare class JobPrimitive<T extends TSchema = TSchema> extends PipelinePrimitive<JobPrimitiveOptions<T>> { protected readonly jobProvider: JobProvider; get name(): string; protected onInit(): void; /** * Push a single payload to the queue (queue-mode only). */ push(payload: Static<T>, options?: PushOptions): Promise<string>; /** * Push multiple payloads at once (queue-mode only). * Batched INSERT + batched queue send when supported. */ pushMany(items: Array<PushManyItem<T>>): Promise<string[]>; /** * Cancel a pending or running execution. */ cancel(executionId: string): Promise<void>; /** * Manually fire a cron-mode job, or trigger a queue-mode job with an explicit payload. */ trigger(context?: JobTriggerContext<T>): Promise<void>; } //#endregion //#region ../../src/api/jobs/providers/JobDispatcher.d.ts /** * Abstract dispatcher for queued/direct job executions. * * The default implementation, {@link DirectJobDispatcher}, runs the handler * in-process after the caller's `push()` returns — fast and dependency-free. * * `AlephaApiJobsQueue` substitutes this with `JobQueueProvider`, which * publishes the executionId to `AlephaQueue` so a worker pool can consume * the work asynchronously. * * Substitute via DI: * ```ts * Alepha.create() * .with({ provide: JobDispatcher, use: MyCustomDispatcher }) * .with(AlephaApiJobs); * ``` * * The `kind` getter is read by the `JobProvider.effectiveMode` accessor * and by the admin UI so users can see which dispatcher is currently active. */ declare abstract class JobDispatcher { /** * Identifier for this dispatcher's effective mode. Reported to the admin * UI so operators can see whether `$job` is running in `queue` or * `direct` mode. */ abstract readonly kind: "queue" | "direct"; /** * Hand off a single execution. The caller's `push()` awaits this so the * caller can be sure the dispatch has at least been initiated. Long-running * work must NOT be awaited here (use background scheduling instead) — this * call should return as quickly as possible. */ abstract dispatch(jobName: string, executionId: string): Promise<void>; /** * Optional batch dispatch. The default implementation loops, but * dispatchers backed by a real queue should override this to use the * provider's batch send (e.g. Cloudflare Queues `sendBatch`). */ dispatchMany(items: Array<{ jobName: string; executionId: string; }>): Promise<void>; } //#endregion //#region ../../src/api/jobs/providers/JobProvider.d.ts declare const PRIORITY_MAP: Record<JobPriority, number>; declare const PRIORITY_REVERSE: Record<number, JobPriority>; interface PushOptions { delay?: DurationLike; key?: string; priority?: JobPriority; scheduledAt?: Date; triggeredBy?: string; triggeredByName?: string; /** * Owning tenant for this execution. Persisted on the row so tenant-facing * views (e.g. the notification admin list) can org-scope it. Plumbed through * verbatim — callers in a tenant context pass it explicitly (the resolved * tenant), cron/global pushes leave it undefined. */ organizationId?: string; } interface PushManyItem<T extends TSchema = TSchema> { payload: Static<T>; key?: string; delay?: DurationLike; priority?: JobPriority; scheduledAt?: Date; } interface JobTriggerContext<T extends TSchema = TSchema> { payload?: Static<T>; triggeredBy?: string; triggeredByName?: string; } interface CancelContext { cancelledBy?: string; cancelledByName?: string; } /** * The declared shape of the job (set at registration time). * * **Important** — this `kind` is the *declared* form. The *effective* * runtime mode (cron / queue / direct) is exposed by * `JobProvider.effectiveMode(name)` and the `JobRegistration.type` field on * the admin schema. Don't conflate the two: a `queue` kind can run as * `direct` at runtime when no queue dispatcher is loaded. */ interface JobRuntimeRegistration { name: string; options: JobPrimitiveOptions; kind: "cron" | "queue"; } type JobEffectiveMode = "cron" | "queue" | "direct"; /** * Coordinates cron and push jobs with a durable outbox table and a single * reconciliation sweep. The actual delivery channel (queue / direct) is * abstracted behind {@link JobDispatcher}, substituted by DI: * * - **DirectJobDispatcher** (default, registered by `AlephaApiJobs`) — * runs the handler in-process right after `push()` returns. * - **QueueJobDispatcher** (registered by `AlephaApiJobsQueue`) — sends * the executionId through `AlephaQueue` so a pool of workers can pick * it up. * * Push flow: * push() → INSERT row (pending) → dispatcher.dispatch(jobName, id) * worker → claim → UPDATE running → handler → DELETE/UPDATE on success * → UPDATE error / scheduled (retry) on failure * * Cron flow: * scheduler tick → acquire lock → executeInline (no retry) * → enqueue + dispatch (retry declared) * * Sweep responsibilities (every `sweepCron`): * - re-enqueue pending rows older than `staleThreshold` * - mark crashed running rows as failed and apply retry policy * - move `scheduled` rows with `scheduledAt <= now` to pending + dispatch * * Trim runs on its own cron (`trimCron`, default hourly): * - per-job history trimmed beyond `keepLastSuccess` / `keepLastError` * - decoupled from sweep because trim cost scales with job count, not * retry latency — running it every sweep is wasted work for most apps. */ declare class JobProvider { protected readonly alepha: Alepha; protected readonly dt: DateTimeProvider; protected readonly cronProvider: CronProvider; protected readonly lockProvider: LockProvider; protected readonly crypto: CryptoProvider; protected readonly config: Readonly<{ sweepCron: string; trimCron: string; staleThreshold: number; runTimeout: number; keepLastSuccess: number; keepLastError: number; logMaxEntries: number; drainTimeout: number; }>; /** * Resolved at first use (after the container is fully wired) — picks * the queue dispatcher when `AlephaApiJobsQueue` was loaded, otherwise * the direct dispatcher. Lazy because both dispatchers inject * `JobProvider` themselves; resolving them at field-init time would * create a circular construction. */ protected dispatcherRef?: JobDispatcher; get dispatcher(): JobDispatcher; protected readonly log: import("alepha/logger").Logger; protected readonly executions: import("alepha/orm").Repository<import("zod").ZodObject<{ id: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_PRIMARY_KEY>, typeof import("alepha/orm").PG_DEFAULT>; createdAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_CREATED_AT>, typeof import("alepha/orm").PG_DEFAULT>; updatedAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_UPDATED_AT>, typeof import("alepha/orm").PG_DEFAULT>; jobName: import("zod").ZodString; key: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; organizationId: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; status: import("alepha/orm").PgAttr<import("zod").ZodEnum<{ cancelled: "cancelled"; error: "error"; ok: "ok"; pending: "pending"; running: "running"; scheduled: "scheduled"; }>, typeof import("alepha/orm").PG_DEFAULT>; priority: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; attempt: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; maxAttempts: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; payload: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>; scheduledAt: import("zod").ZodOptional<import("zod").ZodString>; startedAt: import("zod").ZodOptional<import("zod").ZodString>; completedAt: import("zod").ZodOptional<import("zod").ZodString>; error: import("zod").ZodOptional<import("zod").ZodString>; logs: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{ level: import("zod").ZodEnum<{ DEBUG: "DEBUG"; ERROR: "ERROR"; INFO: "INFO"; SILENT: "SILENT"; TRACE: "TRACE"; WARN: "WARN"; }>; message: import("zod").ZodString; service: import("zod").ZodString; module: import("zod").ZodString; context: import("zod").ZodOptional<import("zod").ZodString>; app: import("zod").ZodOptional<import("zod").ZodString>; data: import("zod").ZodOptional<import("zod").ZodAny>; timestamp: import("zod").ZodNumber; }, import("zod/v4/core").$strip>>>; triggeredBy: import("zod").ZodOptional<import("zod").ZodString>; triggeredByName: import("zod").ZodOptional<import("zod").ZodString>; cancelledBy: import("zod").ZodOptional<import("zod").ZodString>; cancelledByName: import("zod").ZodOptional<import("zod").ZodString>; }, import("zod/v4/core").$strip>>; protected readonly jobs: Map<string, JobRuntimeRegistration>; protected readonly inFlight: Set<Promise<void>>; protected readonly abortControllers: Map<string, AbortController>; protected readonly perExecutionLogs: Map<string, { level: "DEBUG" | "ERROR" | "INFO" | "SILENT" | "TRACE" | "WARN"; message: string; service: string; module: string; context?: string | undefined; app?: string | undefined; data?: any; timestamp: number; }[]>; protected stopping: boolean; constructor(); registerJob(name: string, options: JobPrimitiveOptions): void; getRegisteredJobs(): Map<string, JobRuntimeRegistration>; /** * Resolves what *actually* runs at dispatch time. Cron jobs are always * "cron"; non-cron jobs delegate to the active `JobDispatcher` (queue * vs. direct), which is determined by which modules the app loaded. */ effectiveMode(name: string): JobEffectiveMode; protected runCron(name: string): Promise<void>; /** * Cron-mode runner that respects the per-job distributed lock. * Used by both the scheduled tick and manual `trigger()` calls so that an * admin-triggered run on one instance can't race a scheduled run on another. * * **Two paths depending on `retry`:** * * - **No `retry`** — runs the handler inline. No DB row on success; * error row only on failure. The "next tick" is the implicit retry. * - **`retry` declared** — enqueues a synthetic execution row and hands * it to the dispatcher. The handler then runs through the same path * as a queue/direct push (claim, retry-on-fail, sweep recovery). Use * this when a single failed tick must not block work for the whole * `cron` interval (e.g. once-daily jobs). */ protected runCronLocked(registration: JobRuntimeRegistration, ctx: { triggeredBy?: string; triggeredByName?: string; }): Promise<void>; /** * Materialize a cron tick into the outbox so it goes through the normal * retry/sweep path. Used when the user opts into `retry` on a cron job — * a transient failure no longer means "wait for the next cron tick", it * means "the sweep will retry within `sweepCron`". */ protected enqueueCronExecution(registration: JobRuntimeRegistration, ctx: { triggeredBy?: string; triggeredByName?: string; }): Promise<void>; /** * Acquire a per-job NX lock keyed by `cron-job:<name>` so that a single * tick across all replicas runs exactly one execution. Auto-expires after * `2 * timeout` (or 5 minutes if no per-job timeout) so a crashed worker * cannot permanently block the cron from firing. * * **Caveat — same-process double-fire is not prevented.** The lock value * is a per-process holder id, so two concurrent ticks on the same process * (e.g. a scheduled tick overlapping an admin `trigger()` call) will both * see "we own it". This is acceptable for the multi-replica use case the * lock targets; a process that overlaps its own cron handler should set a * smaller `timeout` or use idempotent handler logic. A future fix can add * a per-process Set guard before reaching the LockProvider. */ protected acquireCronLock(registration: JobRuntimeRegistration): Promise<boolean>; /** * Update only when the row is still in one of the expected statuses. * Logs and returns silently when the guard rejects — this happens when a * concurrent operation (most often `cancel()`) has already moved the row * into a terminal state. We must not overwrite that. */ protected guardedUpdate(executionId: string, expectedStatuses: JobStatus[], patch: Parameters<typeof this.executions.updateById>[1], label: string): Promise<void>; protected releaseCronLock(registration: JobRuntimeRegistration): Promise<void>; protected cronLockKey(jobName: string): string; /** * Stable per-process id used as the lock value — survives multiple ticks. * Lazy so that Cloudflare Workers (which forbid random in global scope) * stay happy. */ protected lockHolderIdValue?: string; protected get lockHolderId(): string; /** * Execute a cron handler inline. Records a row only on error (or always, * when `record: 'all'`). No DB writes on the happy path by default. */ protected executeInline(registration: JobRuntimeRegistration, executionId: string, ctx: { payload: unknown; attempt: number; triggeredBy?: string; triggeredByName?: string; }): Promise<void>; protected writeTerminalRow(executionId: string, jobName: string, status: "ok" | "error", fields: { payload: unknown; attempt: number; startedAt: ReturnType<DateTimeProvider["now"]>; error?: Error; context: string; triggeredBy?: string; triggeredByName?: string; }): Promise<void>; push(name: string, payload: unknown, options?: PushOptions): Promise<string>; /** * How long a `running` row may go without a lease renewal before the * sweep assumes the instance running it crashed. Shared by the sweep's * crash detection and the heartbeat cadence so the two can't drift. */ protected crashThresholdMs(registration: JobRuntimeRegistration): number; /** * While a handler runs, keep the row's `updatedAt` fresh so another * instance's sweep can tell a long-running job from a crashed one — the * sweep treats `max(startedAt, updatedAt)` as the lease. Self-stops when * the row leaves `running` (finished, cancelled, swept elsewhere). */ protected startLeaseHeartbeat(executionId: string, registration: JobRuntimeRegistration): ReturnType<typeof setInterval>; /** * Insert a keyed execution row, resolving the dedup pre-check/insert * race: a concurrent same-key push can land between the read and this * insert, so a unique violation on (jobName, key) is settled by * returning the winner's row instead of throwing. */ protected createKeyedExecution(fields: { jobName: string; key: string; payload?: Record<string, unknown>; status: JobStatus; priority: number; maxAttempts: number; scheduledAt?: string; triggeredBy?: string; triggeredByName?: string; organizationId?: string; }): Promise<{ id: string; created: boolean; }>; /** * Ceiling for the optimistic local timer. Past one day the sweep is the * delivery mechanism anyway, and a 32-bit `setTimeout` overflows at * ~24.85 days — an unclamped timer would fire immediately and run the * job weeks early. */ protected readonly maxOptimisticDelayMs: number; /** * Fire a local setTimeout so delayed/retrying rows dispatch as close to * `scheduledAt` as possible, rather than waiting for the next sweep tick. * No-op on stateless runtimes where timers won't survive, and for delays * beyond `maxOptimisticDelayMs` (the sweep handles both). */ protected scheduleOptimisticDispatch(jobName: string, executionId: string, scheduledAt: string): void; pushMany(name: string, items: Array<PushManyItem>): Promise<string[]>; /** * Hand a single execution to the active `JobDispatcher`. Whether that * results in a queue send or in-process execution depends on which * dispatcher is wired (see {@link JobDispatcher}). */ protected dispatch(jobName: string, executionId: string): Promise<void>; /** * Batched variant. Used by `pushMany` so a backing queue can do a single * batch network call (e.g. Cloudflare Queues `sendBatch`). */ protected dispatchMany(items: Array<{ jobName: string; executionId: string; }>): Promise<void>; trigger(name: string, context?: JobTriggerContext): Promise<void>; cancel(executionId: string, context?: CancelContext): Promise<void>; processExecution(jobName: string, executionId: string): Promise<void>; protected processQueueExecution(registration: JobRuntimeRegistration, executionId: string): Promise<void>; /** * Transition pending → running and return the post-update row. * Two round-trips: read current attempt, then guarded UPDATE … RETURNING. * Returns null when the row is gone or already claimed by another worker. * The returned row replaces a separate post-claim findById, so the dispatch * path is 2 queries instead of 3. */ protected claim(executionId: string): Promise<{ id: string; createdAt: string; updatedAt: string; jobName: string; key?: string | null | undefined; organizationId?: string | null | undefined; status: "cancelled" | "error" | "ok" | "pending" | "running" | "scheduled"; priority: number; attempt: number; maxAttempts: number; payload?: Record<string, any> | undefined; scheduledAt?: string | undefined; startedAt?: string | undefined; completedAt?: string | undefined; error?: string | undefined; logs?: { level: "DEBUG" | "ERROR" | "INFO" | "SILENT" | "TRACE" | "WARN"; message: string; service: string; module: string; context?: string | undefined; app?: string | undefined; data?: any; timestamp: number; }[] | undefined; triggeredBy?: string | undefined; triggeredByName?: string | undefined; cancelledBy?: string | undefined; cancelledByName?: string | undefined; } | null>; protected handleFailure(executionId: string, registration: JobRuntimeRegistration, currentAttempt: number, error: Error, contextId: string): Promise<void>; protected snapshotLogs(contextId: string): LogEntry[] | undefined; protected sweep(): Promise<void>; protected dispatchSafe(jobName: string, executionId: string): Promise<void>; /** * Move a row from `scheduled` → `pending` and dispatch it. * Used by the optimistic retry/delay timer. If the sweep has already moved * the row, or another worker has claimed it, the UPDATE guard fails silently. * The `scheduledAt <= now` condition keeps a stray early timer (clock skew, * timer overflow) from promoting a row ahead of schedule. */ protected dispatchScheduled(jobName: string, executionId: string): Promise<void>; protected trimRingBuffers(): Promise<void>; protected trimByStatus(jobName: string, status: "ok" | "error", keep: number): Promise<void>; protected readonly onStart: import("alepha").HookPrimitive<"start">; protected readonly onStop: import("alepha").HookPrimitive<"stop">; protected getRegistration(name: string): JobRuntimeRegistration; } //#endregion //#region ../../src/api/jobs/schemas/jobExecutionQuerySchema.d.ts declare const jobExecutionQuerySchema: import("zod").ZodObject<{ status: import("zod").ZodOptional<import("zod").ZodEnum<{ cancelled: "cancelled"; error: "error"; ok: "ok"; pending: "pending"; running: "running"; scheduled: "scheduled"; }>>; limit: import("zod").ZodOptional<import("zod").ZodDefault<import("zod").ZodInt>>; }, import("zod/v4/core").$strip>; type JobExecutionQuery = Static<typeof jobExecutionQuerySchema>; //#endregion //#region ../../src/api/jobs/schemas/jobExecutionResourceSchema.d.ts /** * Public-facing schema for a job execution row. * * Diverges from the raw entity in two places, both for API ergonomics: * * - `priority` is exposed as the **string enum** (`critical`/`high`/...) * instead of the numeric value used internally for SQL ordering. The * `JobService` is responsible for the int → string transform. * - `can` derives the available admin actions from the row's status. */ declare const jobExecutionResourceSchema: import("zod").ZodObject<{ id: PgAttr<PgAttr<import("zod").ZodString, typeof PG_PRIMARY_KEY>, typeof PG_DEFAULT>; createdAt: PgAttr<PgAttr<import("zod").ZodString, typeof PG_CREATED_AT>, typeof PG_DEFAULT>; updatedAt: PgAttr<PgAttr<import("zod").ZodString, typeof PG_UPDATED_AT>, typeof PG_DEFAULT>; jobName: import("zod").ZodString; key: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; organizationId: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; status: PgAttr<import("zod").ZodEnum<{ cancelled: "cancelled"; error: "error"; ok: "ok"; pending: "pending"; running: "running"; scheduled: "scheduled"; }>, typeof PG_DEFAULT>; attempt: PgAttr<import("zod").ZodInt, typeof PG_DEFAULT>; maxAttempts: PgAttr<import("zod").ZodInt, typeof PG_DEFAULT>; payload: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>; scheduledAt: import("zod").ZodOptional<import("zod").ZodString>; startedAt: import("zod").ZodOptional<import("zod").ZodString>; completedAt: import("zod").ZodOptional<import("zod").ZodString>; error: import("zod").ZodOptional<import("zod").ZodString>; logs: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{ level: import("zod").ZodEnum<{ DEBUG: "DEBUG"; ERROR: "ERROR"; INFO: "INFO"; SILENT: "SILENT"; TRACE: "TRACE"; WARN: "WARN"; }>; message: import("zod").ZodString; service: import("zod").ZodString; module: import("zod").ZodString; context: import("zod").ZodOptional<import("zod").ZodString>; app: import("zod").ZodOptional<import("zod").ZodString>; data: import("zod").ZodOptional<import("zod").ZodAny>; timestamp: import("zod").ZodNumber; }, import("zod/v4/core").$strip>>>; triggeredBy: import("zod").ZodOptional<import("zod").ZodString>; triggeredByName: import("zod").ZodOptional<import("zod").ZodString>; cancelledBy: import("zod").ZodOptional<import("zod").ZodString>; cancelledByName: import("zod").ZodOptional<import("zod").ZodString>; priority: import("zod").ZodEnum<{ critical: "critical"; high: "high"; low: "low"; normal: "normal"; }>; can: import("zod").ZodObject<{ retry: import("zod").ZodBoolean; cancel: import("zod").ZodBoolean; }, import("zod/v4/core").$strip>; }, import("zod/v4/core").$strip>; type JobExecutionResource = Static<typeof jobExecutionResourceSchema>; //#endregion //#region ../../src/api/jobs/schemas/jobRegistrationSchema.d.ts declare const jobRegistrationSchema: import("zod").ZodObject<{ name: import("zod").ZodString; description: import("zod").ZodOptional<import("zod").ZodString>; type: import("zod").ZodEnum<{ cron: "cron"; direct: "direct"; queue: "queue"; }>; priority: import("zod").ZodEnum<{ critical: "critical"; high: "high"; low: "low"; normal: "normal"; }>; cron: import("zod").ZodOptional<import("zod").ZodString>; timeout: import("zod").ZodOptional<import("zod").ZodString>; retry: import("zod").ZodOptional<import("zod").ZodObject<{ retries: import("zod").ZodInt; }, import("zod/v4/core").$strip>>; recent: import("zod").ZodObject<{ ok: import("zod").ZodInt; error: import("zod").ZodInt; lastRun: import("zod").ZodOptional<import("zod").ZodString>; }, import("zod/v4/core").$strip>; }, import("zod/v4/core").$strip>; type JobRegistration = Static<typeof jobRegistrationSchema>; //#endregion //#region ../../src/api/jobs/services/JobService.d.ts /** * Admin surface for the job system. * * Six methods: list jobs, list executions, get execution, * trigger, retry, cancel. Everything else lives in events — any * analytics/observability is an external concern that subscribes * to `job:begin` / `job:success` / `job:error`. */ declare class JobService { protected readonly alepha: Alepha; protected readonly log: import("alepha/logger").Logger; protected readonly jobProvider: JobProvider; protected readonly executions: import("alepha/orm").Repository<import("zod").ZodObject<{ id: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_PRIMARY_KEY>, typeof import("alepha/orm").PG_DEFAULT>; createdAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_CREATED_AT>, typeof import("alepha/orm").PG_DEFAULT>; updatedAt: import("alepha/orm").PgAttr<import("alepha/orm").PgAttr<import("zod").ZodString, typeof import("alepha/orm").PG_UPDATED_AT>, typeof import("alepha/orm").PG_DEFAULT>; jobName: import("zod").ZodString; key: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; organizationId: import("zod").ZodOptional<import("zod").ZodNullable<import("zod").ZodString>>; status: import("alepha/orm").PgAttr<import("zod").ZodEnum<{ cancelled: "cancelled"; error: "error"; ok: "ok"; pending: "pending"; running: "running"; scheduled: "scheduled"; }>, typeof import("alepha/orm").PG_DEFAULT>; priority: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; attempt: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; maxAttempts: import("alepha/orm").PgAttr<import("zod").ZodInt, typeof import("alepha/orm").PG_DEFAULT>; payload: import("zod").ZodOptional<import("zod").ZodRecord<import("zod").ZodString, import("zod").ZodAny>>; scheduledAt: import("zod").ZodOptional<import("zod").ZodString>; startedAt: import("zod").ZodOptional<import("zod").ZodString>; completedAt: import("zod").ZodOptional<import("zod").ZodString>; error: import("zod").ZodOptional<import("zod").ZodString>; logs: import("zod").ZodOptional<import("zod").ZodArray<import("zod").ZodObject<{ level: import("zod").ZodEnum<{ DEBUG: "DEBUG"; ERROR: "ERROR"; INFO: "INFO"; SILENT: "SILENT"; TRACE: "TRACE"; WARN: "WARN"; }>; message: import("zod").ZodString; service: import("zod").ZodString; module: import("zod").ZodString; context: import("zod").ZodOptional<import("zod").ZodString>; app: import("zod").ZodOptional<import("zod").ZodString>; data: import("zod").ZodOptional<import("zod").ZodAny>; timestamp: import("zod").ZodNumber; }, import("zod/v4/core").$strip>>>; triggeredBy: import("zod").ZodOptional<import("zod").ZodString>; triggeredByName: import("zod").ZodOptional<import("zod").ZodString>; cancelledBy: import("zod").ZodOptional<import("zod").ZodString>; cancelledByName: import("zod").ZodOptional<import("zod").ZodString>; }, import("zod/v4/core").$strip>>; protected computeCan(status: string): { retry: boolean; cancel: boolean; }; /** * Convert the int-priority storage column into the public enum string. * The cast through `unknown` skips TypeScript's structural check between * the entity-level row (`priority: number`) and the resource schema * (`priority: enum`); the runtime values are correct. */ protected toResource<T extends { priority: number; status: string; }>(row: T): JobExecutionResource; /** * List every registered job with recent ok/error counts and lastRun. * One aggregate query covers all jobs. */ listJobs(): Promise<JobRegistration[]>; /** * Recent executions for a single job, ORDER BY startedAt DESC. */ getExecutions(jobName: string, query?: JobExecutionQuery): Promise<{ id: string; createdAt: string; updatedAt: string; jobName: string; key?: string | null | undefined; organizationId?: string | null | undefined; status: "cancelled" | "error" | "ok" | "pending" | "running" | "scheduled"; attempt: number; maxAttempts: number; payload?: Record<string, any> | undefined; scheduledAt?: string | undefined; startedAt?: string | undefined; completedAt?: string | undefined; error?: string | undefined; logs?: { level: "DEBUG" | "ERROR" | "INFO" | "SILENT" | "TRACE" | "WARN"; message: string; service: string; module: string; context?: string | undefined; app?: string | undefined; data?: any; timestamp: number; }[] | undefined; triggeredBy?: string | undefined; triggeredByName?: string | undefined; cancelledBy?: string | undefined; cancelledByName?: string | undefined; priority: "critical" | "high" | "low" | "normal"; can: { retry: boolean; cancel: boolean; }; }[]>; /** * Full execution detail (includes captured logs). */ getExecution(id: string): Promise<{ id: string; createdAt: string; updatedAt: string; jobName: string; key?: string | null | undefined; organizationId?: string | null | undefined; status: "cancelled" | "error" | "ok" | "pending" | "running" | "scheduled"; attempt: number; maxAttempts: number; payload?: Record<string, any> | undefined; scheduledAt?: string | undefined; startedAt?: string | undefined; completedAt?: string | undefined; error?: string | undefined; logs?: { level: "DEBUG" | "ERROR" | "INFO" | "SILENT" | "TRACE" | "WARN"; message: string; service: string; module: string; context?: string | undefined; app?: string | undefined; data?: any; timestamp: number; }[] | undefined; triggeredBy?: string | undefined; triggeredByName?: string | undefined; cancelledBy?: string | undefined; cancelledByName?: string | undefined; priority: "critical" | "high" | "low" | "normal"; can: { retry: boolean; cancel: boolean; }; }>; /** * Manual trigger (cron jobs) or push-with-payload (queue jobs). */ triggerJob(name: string, context?: JobTriggerContext): Promise<{ ok: boolean; }>; /** * Retry a dead or cancelled execution by re-pushing with the original payload. */ retryExecution(id: string, context?: { triggeredBy?: string; triggeredByName?: string; }): Promise<{ ok: boolean; }>; cancelExecution(id: string, context?: { cancelledBy?: string; cancelledByName?: string; }): Promise<{ ok: boolean; }>; } //#endregion //#region ../../src/api/jobs/controllers/AdminJobController.d.ts /** * Minimal admin surface for the job system. Six endpoints. */ declare class AdminJobController { protected readonly url: string; protected readonly group: string; protected readonly jobService: JobService; readonly listJobs: import("alepha/server").ActionPrimitiveFn<{ response: import("zod").ZodArray<import("zod").ZodObject<{ name: import("zod").ZodString; description: import("zod").ZodOptional<import("zod").ZodString>; type: import("zod").ZodEnum<{