@beignet/core
Version:
Core framework primitives for Beignet
760 lines • 24.3 kB
TypeScript
import type { StandardSchemaV1 } from "@standard-schema/spec";
import type { LeaseMetadata, LocksPort } from "../locks/index.js";
import { type TraceCarrier, type TracingPort } from "../tracing/index.js";
/**
* Any Standard Schema compatible validator.
*/
export type StandardSchema = StandardSchemaV1<unknown, unknown>;
/**
* Value or promise of that value.
*/
export type MaybePromise<T> = T | Promise<T>;
type NoInferJob<T> = [T][T extends unknown ? 0 : never];
/**
* Infer the parsed output type from a Standard Schema.
*/
export type InferSchemaOutput<T extends StandardSchemaV1> = StandardSchemaV1.InferOutput<T>;
/**
* Duration accepted by job helpers. Numbers are milliseconds.
*/
export type JobDuration = number | `${number}ms` | `${number}s` | `${number}m` | `${number}h`;
/**
* Duration accepted by retry helpers. Numbers are milliseconds.
*/
export type JobRetryDuration = JobDuration;
/**
* Duration accepted by job timeout helpers. Numbers are milliseconds.
*/
export type JobTimeoutDuration = JobDuration;
/**
* Duration accepted by job execution lease helpers. Numbers are milliseconds.
*/
export type JobExecutionLeaseDuration = JobDuration;
/**
* Retry strategy understood by Beignet job adapters.
*/
export type JobRetryStrategy = "none" | "fixed" | "exponential";
/**
* Arguments passed to a retry predicate.
*/
export interface JobRetryPredicateArgs {
/**
* Error thrown by the previous attempt.
*/
error: unknown;
/**
* One-based attempt number that just failed.
*/
attempt: number;
/**
* Maximum attempts allowed for this delivery.
*/
maxAttempts: number;
/**
* Job name when the retry decision is for a job.
*/
jobName?: string;
}
/**
* Return whether a failed attempt should be retried.
*/
export type JobRetryPredicate = (args: JobRetryPredicateArgs) => boolean;
/**
* Dispatch-time uniqueness metadata for a job.
*
* Unique jobs are suppressed while the resolved key's lease is active.
* The lease is intentionally left to expire after a successful dispatch.
*/
export interface JobUniqueOptions {
/**
* Logical uniqueness key within this job name.
*/
key: string;
/**
* How long duplicate dispatches should be suppressed.
*/
ttl: JobDuration;
}
/**
* Arguments passed to a per-payload unique job resolver.
*/
export interface JobUniqueResolverArgs<Payload extends StandardSchema> {
/**
* Stable job name.
*/
jobName: string;
/**
* Parsed job payload.
*/
payload: StandardSchemaV1.InferOutput<Payload>;
}
/**
* Per-payload resolver for dispatch-time uniqueness.
*/
export type JobUniqueResolver<Payload extends StandardSchema> = {
bivarianceHack(args: JobUniqueResolverArgs<Payload>): MaybePromise<JobUniqueOptions | null | undefined>;
}["bivarianceHack"];
/**
* Unique job declaration value.
*/
export type JobUniqueConfig<Payload extends StandardSchema = StandardSchema> = JobUniqueOptions | JobUniqueResolver<Payload>;
/**
* Resolved uniqueness metadata used by dispatchers.
*/
export interface ResolvedJobUniqueOptions {
/**
* Logical uniqueness key returned by the job declaration.
*/
key: string;
/**
* Concrete lock key used by `LocksPort`.
*/
lockKey: string;
/**
* Lease TTL in milliseconds.
*/
ttlMs: number;
}
/**
* Job definition created by `defineJob(...)`.
*/
export interface JobDef<Name extends string = string, Payload extends StandardSchema = StandardSchema, Ctx = unknown> {
/**
* Discriminator for job definitions.
*/
readonly kind: "job";
/**
* Stable job name used by dispatchers and provider adapters.
*/
readonly name: Name;
/**
* Standard Schema payload validator.
*/
readonly payload: Payload;
/**
* Optional human-readable description for docs and tooling.
*/
readonly description?: string;
/**
* Retry metadata for durable job providers.
*/
readonly retry?: JobRetryOptions;
/**
* Optional dispatch-time uniqueness guard.
*/
readonly unique?: JobUniqueConfig<Payload>;
/**
* Optional maximum execution time for each job handler attempt.
*/
readonly timeout?: JobTimeoutDuration;
/**
* Optional hooks that wrap each handler attempt for this job.
*/
readonly hooks?: readonly JobHook<JobDef<Name, Payload, Ctx>, Ctx>[];
/**
* Handle a parsed job payload.
*/
handle(args: JobHandleArgs<JobDef<Name, Payload, Ctx>, Ctx>): MaybePromise<void>;
}
/**
* Infer the parsed payload type for a job definition.
*/
export type InferJobPayload<J extends JobDef> = J["payload"] extends StandardSchemaV1<unknown, infer Output> ? Output : never;
/**
* Arguments passed to a job handler.
*/
export interface JobHandleArgs<J extends JobDef, Ctx> {
/**
* Job definition being handled.
*/
job: J;
/**
* Parsed job payload.
*/
payload: InferJobPayload<J>;
/** Handler context. */
ctx: Ctx;
/**
* Abort signal that fires when the job's declared timeout expires.
*
* Beignet dispatchers and worker helpers provide this signal. It is optional
* in the type so direct handler tests can stay lightweight.
*
* JavaScript cannot forcibly stop arbitrary async work, so handlers that
* call cancellable APIs should pass this signal through.
*/
signal?: AbortSignal;
}
/**
* Arguments passed to job execution hooks.
*/
export interface JobHookArgs<J extends JobDef, Ctx> extends JobHandleArgs<J, Ctx> {
/**
* Abort signal provided by the Beignet execution runner.
*/
signal: AbortSignal;
/**
* One-based execution attempt when the runner can report it.
*/
attempt?: number;
/**
* Maximum execution attempts when the runner can report it.
*/
maxAttempts?: number;
}
/**
* Hook that wraps one job handler attempt.
*
* Hooks run only through Beignet dispatchers and worker helpers. Direct calls
* to `job.handle(...)` bypass hooks, which keeps lightweight unit tests
* possible.
*/
export type JobHook<J extends JobDef = JobDef, Ctx = unknown> = {
bivarianceHack(args: JobHookArgs<J, Ctx>, next: () => Promise<void>): MaybePromise<void>;
}["bivarianceHack"];
/**
* Arguments passed to an execution lease key resolver.
*/
export type JobExecutionLeaseResolverArgs<J extends JobDef, Ctx> = JobHookArgs<J, Ctx>;
/**
* Resolver for the logical execution lease key within a job name.
*/
export type JobExecutionLeaseKeyResolver<J extends JobDef, Ctx> = {
bivarianceHack(args: JobExecutionLeaseResolverArgs<J, Ctx>): MaybePromise<string>;
}["bivarianceHack"];
/**
* Resolver for the locks port used by an execution lease hook.
*/
export type JobExecutionLeaseLocksResolver<J extends JobDef, Ctx> = LocksPort | {
bivarianceHack(args: JobExecutionLeaseResolverArgs<J, Ctx>): MaybePromise<LocksPort>;
}["bivarianceHack"];
/**
* Resolver for optional diagnostics metadata attached to lease acquisition.
*/
export type JobExecutionLeaseMetadataResolver<J extends JobDef, Ctx> = LeaseMetadata | {
bivarianceHack(args: JobExecutionLeaseResolverArgs<J, Ctx>): MaybePromise<LeaseMetadata | undefined>;
}["bivarianceHack"];
/**
* Arguments passed when an execution lease cannot be acquired.
*/
export interface JobExecutionLeaseUnavailableArgs<J extends JobDef, Ctx> extends JobHookArgs<J, Ctx> {
/**
* Logical lease key returned by the hook configuration.
*/
key: string;
/**
* Concrete lock key passed to `LocksPort`.
*/
lockKey: string;
/**
* Acquisition failure reason returned by `LocksPort`.
*/
reason: "unavailable" | "timeout";
}
/**
* Behavior when an execution lease cannot be acquired.
*
* `"skip"` treats the attempt as successful without running the handler.
* `"throw"` fails the attempt with `JobExecutionLeaseUnavailableError` so the
* job retry policy can classify it. A function can log or throw custom errors;
* returning from it skips the handler.
*/
export type JobExecutionLeaseUnavailableBehavior<J extends JobDef, Ctx> = "skip" | "throw" | {
bivarianceHack(args: JobExecutionLeaseUnavailableArgs<J, Ctx>): MaybePromise<void>;
}["bivarianceHack"];
/**
* Options for `createJobExecutionLeaseHook(...)`.
*/
export interface JobExecutionLeaseHookOptions<J extends JobDef = JobDef, Ctx = unknown> {
/**
* Locks port or resolver used to acquire the execution lease.
*/
locks: JobExecutionLeaseLocksResolver<J, Ctx>;
/**
* Logical lease key within this job name.
*/
key: string | JobExecutionLeaseKeyResolver<J, Ctx>;
/**
* Lease time-to-live. This is the real safety boundary in serverless runtimes
* where an invocation may terminate before `finally` runs.
*/
ttl: JobExecutionLeaseDuration;
/**
* How long to wait for an existing lease before applying `onUnavailable`.
*
* Defaults to no wait.
*/
wait?: JobExecutionLeaseDuration;
/**
* Delay between acquisition attempts while waiting.
*/
retryDelay?: JobExecutionLeaseDuration;
/**
* Prefix used to build the concrete lock key.
*
* Defaults to `"jobs:lease"`.
*/
keyPrefix?: string;
/**
* Optional diagnostics metadata attached to the lease acquire call.
*/
metadata?: JobExecutionLeaseMetadataResolver<J, Ctx>;
/**
* Behavior when the lease cannot be acquired.
*
* Defaults to `"skip"` so overlapping executions do not create retry storms.
*/
onUnavailable?: JobExecutionLeaseUnavailableBehavior<J, Ctx>;
}
/**
* Retry metadata that durable job providers can map to their own retry model.
*/
export interface JobRetryOptions {
/**
* Retry strategy. Raw objects without a strategy default to exponential
* backoff so existing `{ attempts }` style definitions stay meaningful.
*/
strategy?: JobRetryStrategy;
/**
* Maximum total attempts, including the first attempt.
*/
attempts?: number;
/**
* Delay between attempts for fixed retry policies.
*/
delay?: JobRetryDuration;
/**
* Initial delay for exponential retry policies.
*/
initialDelay?: JobRetryDuration;
/**
* Maximum delay for exponential retry policies.
*/
maxDelay?: JobRetryDuration;
/**
* Exponential multiplier. Defaults to `2`.
*/
factor?: number;
/**
* Whether adapters that compute delays should add jitter.
*/
jitter?: boolean;
/**
* Optional app-owned retry classifier.
*/
retryIf?: JobRetryPredicate;
}
/**
* Options for declaring a typed job.
*/
export interface DefineJobOptions<Name extends string, Payload extends StandardSchema, Ctx> {
/**
* Standard Schema payload validator.
*/
payload: Payload;
/**
* Optional human-readable description for docs and tooling.
*/
description?: string;
/**
* Retry metadata for durable job providers.
*/
retry?: JobRetryOptions;
/**
* Optional dispatch-time uniqueness guard.
*/
unique?: JobUniqueConfig<Payload>;
/**
* Optional maximum execution time for each job handler attempt.
*/
timeout?: JobTimeoutDuration;
/**
* Optional hooks that wrap each handler attempt for this job.
*/
hooks?: readonly JobHook<JobDef<NoInferJob<Name>, NoInferJob<Payload>, Ctx>, Ctx>[];
/**
* Handle a parsed job payload.
*/
handle(args: JobHandleArgs<JobDef<Name, Payload, Ctx>, Ctx>): MaybePromise<void>;
}
/**
* Options for a fixed job retry policy.
*/
export interface FixedJobRetryOptions {
/**
* Maximum total attempts, including the first attempt.
*/
attempts: number;
/**
* Delay between attempts.
*/
delay: JobRetryDuration;
/**
* Optional app-owned retry classifier.
*/
retryIf?: JobRetryPredicate;
}
/**
* Options for an exponential job retry policy.
*/
export interface ExponentialJobRetryOptions {
/**
* Maximum total attempts, including the first attempt.
*/
attempts: number;
/**
* Initial delay. Defaults to `1s`.
*/
initialDelay?: JobRetryDuration;
/**
* Maximum delay. Defaults to `1m`.
*/
maxDelay?: JobRetryDuration;
/**
* Exponential multiplier. Defaults to `2`.
*/
factor?: number;
/**
* Whether computed delays should include jitter.
*/
jitter?: boolean;
/**
* Optional app-owned retry classifier.
*/
retryIf?: JobRetryPredicate;
}
/**
* Retry helper namespace for job definitions.
*/
export declare const retry: {
/**
* Disable retries. The first failure is terminal.
*/
readonly none: () => JobRetryOptions;
/**
* Retry with the same delay between attempts.
*/
readonly fixed: (options: FixedJobRetryOptions) => JobRetryOptions;
/**
* Retry with exponential backoff.
*/
readonly exponential: (options: ExponentialJobRetryOptions) => JobRetryOptions;
};
/**
* Options for the inline job dispatcher.
*/
export interface InlineJobDispatcherOptions<Ctx> {
/**
* Static job context or factory evaluated for each dispatched job.
*/
ctx?: Ctx | (() => MaybePromise<Ctx>);
/**
* Called when a dispatched inline job fails all attempts allowed by its
* retry policy. When omitted, the final error is rethrown to the caller.
*/
onError?: (error: unknown, job: JobDef<string, StandardSchema, Ctx>) => void;
/**
* Sleep implementation used between retry attempts. Defaults to a real
* `setTimeout` delay; inject a fake in tests to keep retries instant.
*/
sleep?: (ms: number) => Promise<void>;
/**
* Honor the job's declared retry policy inline. Defaults to `true`. Set
* `false` when another layer owns execution retries for every dispatch
* through this dispatcher.
*/
retry?: boolean;
/**
* Hooks that wrap every job attempt executed by this dispatcher. Runner hooks
* wrap job-local hooks.
*/
hooks?: readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[];
}
/** Metadata propagated when a job is dispatched. */
export interface JobDispatchOptions {
/** Versioned trace context captured by the job producer. */
trace?: TraceCarrier;
}
/** Parsed payload and propagation metadata from a job transport envelope. */
export interface ParsedJobTransportEnvelope {
payload: unknown;
trace?: TraceCarrier;
}
/**
* Wrap a job payload with transport metadata when a trace is present.
* Payloads without metadata retain their legacy wire shape.
*/
export declare function createJobTransportEnvelope(payload: unknown, options?: JobDispatchOptions): unknown;
/**
* Decode a Beignet job transport envelope while accepting legacy raw payloads.
* Unknown or malformed trace metadata is ignored without dropping the payload.
*/
export declare function parseJobTransportEnvelope(value: unknown): ParsedJobTransportEnvelope;
/**
* Well-known symbol under which the inline dispatcher exposes a
* single-attempt dispatch. Delivery systems that own execution retries
* themselves — the outbox drain — call it instead of `dispatch(...)` so a
* job's retry policy runs in exactly one layer. Registered with `Symbol.for`
* so multiple core copies in one process agree on the key.
*/
export declare const SINGLE_ATTEMPT_DISPATCH: unique symbol;
/**
* Metadata passed to a single-attempt dispatch when another delivery layer
* owns retry scheduling.
*/
export interface SingleAttemptJobDispatchOptions extends JobDispatchOptions {
/**
* One-based delivery attempt.
*/
attempt?: number;
/**
* Maximum delivery attempts.
*/
maxAttempts?: number;
}
/**
* Shape of the single-attempt dispatch exposed under
* `SINGLE_ATTEMPT_DISPATCH`.
*/
export type SingleAttemptJobDispatch = <J extends JobDef>(job: J, payload: InferJobPayload<J>, options?: SingleAttemptJobDispatchOptions) => Promise<void>;
/**
* Job dispatcher shape accepted by job wrappers.
*/
export interface JobDispatcher {
dispatch<J extends JobDef>(job: J, payload: InferJobPayload<J>, options?: JobDispatchOptions): MaybePromise<void>;
}
/**
* Arguments passed when a unique job dispatch is suppressed.
*/
export interface UniqueJobDuplicateArgs<J extends JobDef = JobDef> {
/**
* Job definition whose dispatch was suppressed.
*/
job: J;
/**
* Parsed job payload.
*/
payload: InferJobPayload<J>;
/**
* Logical uniqueness key returned by the job declaration.
*/
key: string;
/**
* Concrete lock key used by `LocksPort`.
*/
lockKey: string;
/**
* Lease TTL in milliseconds.
*/
ttlMs: number;
/**
* Lock acquisition reason returned by `LocksPort`.
*/
reason: "unavailable" | "timeout";
}
/**
* Options for `createUniqueJobDispatcher(...)`.
*/
export interface UniqueJobDispatcherOptions {
/**
* Dispatcher to call when a job is not unique or the unique lease is acquired.
*/
jobs: JobDispatcher;
/**
* Lease-backed lock port used as the uniqueness guard.
*/
locks: LocksPort;
/**
* Prefix for concrete lock keys.
*
* Defaults to `"jobs:unique"`.
*/
keyPrefix?: string;
/**
* Called when a duplicate dispatch is suppressed.
*/
onDuplicate?: (args: UniqueJobDuplicateArgs) => MaybePromise<void>;
}
/**
* Local/test job dispatcher that executes job handlers inline.
*/
export interface InlineJobDispatcher<Ctx = unknown> {
/**
* Validate a payload and run the job handler inline, honoring the job's
* declared retry policy before failing.
*/
dispatch<J extends JobDef<string, StandardSchema, Ctx>>(job: J, payload: InferJobPayload<J>, options?: JobDispatchOptions): Promise<void>;
}
/**
* Context-bound job helper factory.
*/
export interface Jobs<Ctx> {
/**
* Define a job with the bound context type.
*/
defineJob<Name extends string, Payload extends StandardSchema>(name: Name, options: DefineJobOptions<Name, Payload, Ctx>): JobDef<Name, Payload, Ctx>;
}
/**
* Error thrown when job payload validation fails.
*/
export declare class JobValidationError extends Error {
/**
* Raw Standard Schema validation issues.
*/
readonly issues: readonly StandardSchemaV1.Issue[];
constructor(args: {
name: string;
issues: readonly StandardSchemaV1.Issue[];
});
}
/**
* Error thrown when a job handler exceeds its declared timeout.
*/
export declare class JobTimeoutError extends Error {
/**
* Stable job name that timed out.
*/
readonly jobName: string;
/**
* Timeout in milliseconds.
*/
readonly timeoutMs: number;
constructor(args: {
jobName: string;
timeoutMs: number;
});
}
/**
* Error thrown when an execution lease hook is configured to fail on an
* unavailable lease.
*/
export declare class JobExecutionLeaseUnavailableError extends Error {
/**
* Stable job name whose execution lease was unavailable.
*/
readonly jobName: string;
/**
* Logical lease key returned by the hook configuration.
*/
readonly key: string;
/**
* Concrete lock key passed to `LocksPort`.
*/
readonly lockKey: string;
/**
* Acquisition failure reason returned by `LocksPort`.
*/
readonly reason: "unavailable" | "timeout";
constructor(args: {
jobName: string;
key: string;
lockKey: string;
reason: "unavailable" | "timeout";
});
}
/**
* Return the maximum total attempts configured by a retry policy.
*/
export declare function getJobRetryMaxAttempts(options: JobRetryOptions | undefined): number | undefined;
/**
* Return whether a failed job attempt should be retried.
*/
export declare function shouldRetryJob(options: JobRetryOptions | undefined, args: JobRetryPredicateArgs): boolean;
/**
* Compute the next retry delay in milliseconds for a failed job attempt.
*/
export declare function getJobRetryDelayMs(options: JobRetryOptions | undefined, args: Pick<JobRetryPredicateArgs, "attempt" | "error" | "jobName">): number;
/**
* Return the execution timeout in milliseconds configured by a job.
*/
export declare function getJobTimeoutMs(job: Pick<JobDef, "timeout">): number | undefined;
/**
* Create a job hook that prevents overlapping handler attempts for the same
* logical execution key.
*
* The hook uses one bounded `LocksPort.acquire(...)` call and never starts
* renewal loops, so it can run in serverless entrypoints as long as `locks`
* points at shared storage. `ttl` is the real safety boundary when a runtime
* terminates before best-effort release runs.
*/
export declare function createJobExecutionLeaseHook<J extends JobDef = JobDef, Ctx = unknown>(options: JobExecutionLeaseHookOptions<J, Ctx>): JobHook<J, Ctx>;
/**
* Validate and parse a job payload with the job's Standard Schema.
*/
export declare function parseJobPayload<J extends JobDef>(job: J, payload: unknown): Promise<InferJobPayload<J>>;
/**
* Options for running one parsed job handler attempt.
*/
export interface RunJobHandlerOptions<J extends JobDef<string, StandardSchema, Ctx>, Ctx> {
/**
* Job definition to execute.
*/
job: J;
/**
* Parsed job payload.
*/
payload: InferJobPayload<J>;
/** Handler context or factory resolved inside the job span. */
ctx: Ctx | (() => MaybePromise<Ctx>);
/** Runtime tracing port used before a lazy context factory runs. */
tracing?: TracingPort;
/** Trace context captured by the dispatching process. */
trace?: TraceCarrier;
/**
* Runner-level hooks. These wrap job-local hooks.
*/
hooks?: readonly JobHook<JobDef<string, StandardSchema, Ctx>, Ctx>[];
/**
* One-based execution attempt when the runner can report it.
*/
attempt?: number;
/**
* Maximum execution attempts when the runner can report it.
*/
maxAttempts?: number;
}
/**
* Run a parsed job handler once, enforcing hooks and the job's declared
* timeout.
*/
export declare function runJobHandler<J extends JobDef<string, StandardSchema, Ctx>, Ctx>(args: RunJobHandlerOptions<J, Ctx>): Promise<void>;
/**
* Resolve a job's dispatch-time uniqueness metadata for a parsed payload.
*/
export declare function resolveJobUnique<J extends JobDef>(job: J, payload: InferJobPayload<J>, options?: {
keyPrefix?: string;
}): Promise<ResolvedJobUniqueOptions | undefined>;
/**
* Create a local/test dispatcher that runs job handlers inline.
*
* Dispatch honors the job's declared retry policy: failed attempts retry with
* the policy's delays until the policy is exhausted. Payload validation
* failures never retry. Jobs without a retry policy run exactly once.
*/
export declare function createInlineJobDispatcher<Ctx>(options?: InlineJobDispatcherOptions<Ctx>): InlineJobDispatcher<Ctx>;
/**
* Wrap any job dispatcher with dispatch-time unique job suppression.
*
* When a job has no `unique` declaration, dispatch passes through unchanged.
* When it does, the wrapper validates the payload, resolves the unique key,
* acquires the matching lease, and calls the underlying dispatcher only when
* the lease is acquired. Successful dispatches intentionally keep the lease
* until its TTL expires; failed dispatches release it so callers can retry.
*/
export declare function createUniqueJobDispatcher(options: UniqueJobDispatcherOptions): JobDispatcher;
/**
* Create job helper methods bound to an application context type.
*
* Call it once in `lib/jobs.ts`:
*
* ```ts
* export const { defineJob } = createJobs<AppContext>();
* ```
*
* Retry options describe the job's retry policy. Inline dispatchers run the
* policy in-process with real delays; durable providers map the policy onto
* their own runtime and reject options they cannot honor.
*/
export declare function createJobs<Ctx>(): Jobs<Ctx>;
export {};
//# sourceMappingURL=index.d.ts.map