import { type JsonFileStore } from "@listmonk-ops/common"; import { type TransactionalIdempotencyStore } from "@listmonk-ops/operations"; import { z } from "zod"; export declare const SEQUENCE_STORE_VERSION = 1; export declare const DEFAULT_SEQUENCE_LEASE_MS = 90000; export declare const DEFAULT_SEQUENCE_WORKER_STALE_MS = 90000; export declare const DEFAULT_SEQUENCE_WORKER_RETENTION_MS: number; export declare const SEQUENCE_STEP_TYPES: readonly ["send", "wait", "wait_until", "condition", "stop"]; export declare const sequenceStepSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{ id: z.ZodString; type: z.ZodLiteral<"send">; templateId: z.ZodNumber; consentListIds: z.ZodOptional>; fromEmail: z.ZodOptional; data: z.ZodOptional>; contentType: z.ZodOptional>; messenger: z.ZodOptional; subject: z.ZodOptional; altBody: z.ZodOptional; }, z.core.$strip>, z.ZodObject<{ id: z.ZodString; type: z.ZodLiteral<"wait">; durationSeconds: z.ZodNumber; }, z.core.$strip>, z.ZodObject<{ id: z.ZodString; type: z.ZodLiteral<"wait_until">; at: z.ZodISODateTime; }, z.core.$strip>, z.ZodObject<{ id: z.ZodString; type: z.ZodLiteral<"condition">; path: z.ZodString; operator: z.ZodEnum<{ equals: "equals"; exists: "exists"; not_equals: "not_equals"; }>; value: z.ZodOptional; onTrue: z.ZodString; onFalse: z.ZodString; }, z.core.$strip>, z.ZodObject<{ id: z.ZodString; type: z.ZodLiteral<"stop">; }, z.core.$strip>], "type">; export type SequenceStep = z.output; export type SequenceDefinitionStatus = "active" | "paused"; export type SequenceEnrollmentStatus = "pending" | "running" | "waiting" | "paused" | "completed" | "failed" | "ambiguous" | "cancelled"; export type SequenceWorkerStatus = "running" | "stopped" | "failed"; export type SequenceRevision = Readonly<{ revision: number; steps: readonly SequenceStep[]; createdAt: string; }>; export type SequenceDefinition = Readonly<{ id: string; name: string; description?: string | undefined; status: SequenceDefinitionStatus; currentRevision: number; revisions: readonly SequenceRevision[]; createdAt: string; updatedAt: string; }>; export type SequenceUpdateResult = Readonly<{ definition: SequenceDefinition; updated: boolean; }>; export type SequenceEnrollment = Readonly<{ id: string; sequenceId: string; revision: number; subscriberId: number; context: Readonly>; status: SequenceEnrollmentStatus; retryCount: number; currentStepId: string; /** The requested initial activation time; nextRunAt moves as steps advance. */ startAt?: string | undefined; nextRunAt: string; leaseToken?: string | undefined; leaseExpiresAt?: string | undefined; lastError?: string | undefined; lastTransitionAt: string; createdAt: string; updatedAt: string; }>; export type SequenceWorker = Readonly<{ id: string; status: SequenceWorkerStatus; startedAt: string; heartbeatAt: string; stoppedAt?: string | undefined; lastError?: string | undefined; lastTick?: SequenceTickSummary | undefined; }>; export type SequenceTickSummary = Readonly<{ claimed: number; /** Echoed ids of the exact enrollments this tick claimed. */ claimedIds: readonly string[]; /** Echoed claim positions: enrollment id plus its originally claimed step. */ claimedSteps: readonly Readonly<{ id: string; stepId: string; }>[]; advanced: number; waiting: number; completed: number; failed: number; ambiguous: number; cancelled: number; completedAt: string; }>; export type SequenceRuntimeHealth = Readonly<{ store: "file" | "postgres"; schemaVersion: number; healthy: boolean; checkedAt: string; definitions: Readonly<{ total: number; active: number; paused: number; }>; enrollments: Readonly & { due: number; leased: number; oldestDueAt?: string | undefined; }>; workers: Readonly<{ running: number; stale: number; stopped: number; failed: number; lastHeartbeatAt?: string | undefined; }>; }>; export type CreateSequenceDefinitionInput = Readonly<{ id?: string; name: string; description?: string; steps: readonly SequenceStep[]; }>; export type UpdateSequenceDefinitionInput = Readonly<{ name?: string; description?: string; steps: readonly SequenceStep[]; }>; export type CreateSequenceEnrollmentInput = Readonly<{ id?: string; sequenceId: string; subscriberId: number; context?: Readonly>; startAt?: string; }>; export type SequenceEnrollmentListOptions = Readonly<{ sequenceId?: string; status?: SequenceEnrollmentStatus; subscriberId?: number; limit?: number; }>; export type SequenceReconcileResult = Readonly<{ /** Echoed ids of the enrollments the scan considered. */ scannedIds: readonly string[]; scanned: number; recovered: number; unchanged: number; dryRun: boolean; }>; export type ClaimedSequenceEnrollment = Readonly<{ enrollment: SequenceEnrollment; definition: SequenceDefinition; revision: SequenceRevision; }>; export interface SequenceRepository { readonly kind: "file" | "postgres"; /** * Shared transactional idempotency store when the repository can provide * one. Postgres repositories expose a database-backed implementation so * every worker observes the same send claims. */ readonly idempotencyStore?: TransactionalIdempotencyStore; listDefinitions(): Promise; getDefinition(id: string): Promise; createDefinition(definition: SequenceDefinition): Promise; updateDefinition(id: string, input: UpdateSequenceDefinitionInput, now: Date): Promise; deleteDefinition(id: string): Promise; setDefinitionStatus(id: string, status: SequenceDefinitionStatus, now: Date): Promise; listEnrollments(options?: SequenceEnrollmentListOptions): Promise; getEnrollment(id: string): Promise; /** * Atomically read the enrollment generation for a (sequence, * subscriber) pair under the same serialization boundary as guarded * creates: the exact total and the newest record cannot interleave * with a concurrent guarded create. */ readEnrollmentGeneration(options: { sequenceId: string; subscriberId: number; }): Promise<{ total: number; newest?: SequenceEnrollment; }>; createEnrollment(enrollment: SequenceEnrollment, options?: Readonly<{ /** * Generation guard: create only while exactly this many * enrollments already exist for the (sequence, subscriber) * pair, verified inside the store transaction so a concurrent * guarded retry cannot double-create after a terminal * lifecycle. */ expectedPriorEnrollments?: number; }>): Promise; claimDue(options: Readonly<{ limit: number; now: Date; leaseMs: number; }>): Promise; /** * Claim exactly the requested enrollment ids when they are still * claimable (same eligibility rule as claimDue). Entries that exist but * are not claimable — already advanced, completed, ambiguous, or leased * by a live worker — are skipped so an echoed-set retry converges * instead of doing new work; unknown ids are rejected. */ claimSpecific(options: Readonly<{ claims: readonly Readonly<{ id: string; stepId: string; }>[]; now: Date; leaseMs: number; }>): Promise; completeClaim(enrollment: SequenceEnrollment, next: Omit): Promise; resolveAmbiguous(enrollment: SequenceEnrollment, next: Omit): Promise; reconcile(options: Readonly<{ now: Date; limit: number; dryRun: boolean; /** Recovery binding: restrict the expired-lease scan to exactly these enrollments. */ enrollmentIds?: readonly string[]; }>): Promise; getRuntimeHealth(options: Readonly<{ now: Date; workerStaleMs: number; }>): Promise; upsertWorker(worker: SequenceWorker): Promise; close?(): Promise; } type SequenceStore = Readonly<{ version: typeof SEQUENCE_STORE_VERSION; definitions: readonly SequenceDefinition[]; enrollments: readonly SequenceEnrollment[]; workers: readonly SequenceWorker[]; }>; export declare const sequenceEnrollmentStatusSchema: z.ZodEnum<{ ambiguous: "ambiguous"; cancelled: "cancelled"; completed: "completed"; failed: "failed"; paused: "paused"; pending: "pending"; running: "running"; waiting: "waiting"; }>; export declare class SequenceNotFoundError extends Error { constructor(kind: "definition" | "enrollment", id: string); } export declare class SequenceConflictError extends Error { constructor(message: string); } export declare function validateSequenceSteps(steps: readonly SequenceStep[]): readonly SequenceStep[]; export declare function parseSequenceDefinition(value: unknown): SequenceDefinition; /** Parses storage created before strict sender validation without weakening new writes. */ export declare function parsePersistedSequenceDefinition(value: unknown): SequenceDefinition; export declare function parseSequenceEnrollment(value: unknown): SequenceEnrollment; /** Overrides follow the shared home-anchored path rule, like other stores. */ export declare function getSequenceStorePath(): string; export declare function createSequenceStore(path?: string): JsonFileStore; export declare function createSequenceDefinition(input: CreateSequenceDefinitionInput, now?: Date): SequenceDefinition; export declare function createSequenceEnrollment(definition: SequenceDefinition, input: CreateSequenceEnrollmentInput, now?: Date): SequenceEnrollment; export declare function canonicalJsonValue(value: unknown): unknown; export declare function canonicalStepsJson(steps: readonly SequenceStep[]): string; export declare function createFileSequenceRepository(path?: string): SequenceRepository; export declare function buildSequenceRuntimeHealth(kind: "file" | "postgres", definitions: readonly SequenceDefinition[], enrollments: readonly SequenceEnrollment[], workers: readonly SequenceWorker[], options: Readonly<{ now: Date; workerStaleMs: number; }>): SequenceRuntimeHealth; export {};