import type { OperationDispatcher, OperationRecord, OperationStore, OperationStoreFilter, OperationStoreStats } from '../types/operations.js'; /** * The Redis commands the operation store needs. * * Structural rather than tied to one client, so `ioredis`, `node-redis`, or a cluster proxy all * satisfy it without this package depending on any of them. The sorted-set commands are needed only * with `index: true`; `ioredis` has them all. */ export interface RedisOperationLikeClient { /** Reads a hash field. */ hget(key: string, field: string): Promise | string | null; /** Sets a hash field. */ hset(key: string, field: string, value: string): Promise | unknown; /** Deletes a hash field. */ hdel(key: string, field: string): Promise | unknown; /** Reads every hash value. */ hvals(key: string): Promise | string[]; /** Optional CAS primitive. When absent the store falls back to a read-compare-write. */ eval?(script: string, numKeys: number, ...args: string[]): Promise | unknown; /** Reads several hash fields in one round trip. Used by the index when present. */ hmget?(key: string, ...fields: string[]): Promise> | Array; /** Adds a sorted-set member. Needed with `index: true`. */ zadd?(key: string, score: number, member: string): Promise | unknown; /** Removes a sorted-set member. Needed with `index: true`. */ zrem?(key: string, member: string): Promise | unknown; /** Members by score, as `ZRANGEBYSCORE key min max LIMIT offset count`. Needed with `index: true`. */ zrangebyscore?(key: string, min: number | string, max: number | string, limit: 'LIMIT', offset: number, count: number): Promise | string[]; } /** Options for the Redis operation store. */ export interface RedisOperationStoreOptions { /** Key prefix. Defaults to `nexus-ai-pro:operations:`. */ prefix?: string; /** Disables the Lua compare-and-set even when the client exposes `eval`. */ useEval?: boolean; /** * Keeps queued work in a sorted set by age, and leased work in one by lease expiry, so * `listQueued()` and `claimExpired()` read the oldest few records instead of every one: a claim * costs the same with 100,000 operations queued as with 1,000. The records stay the source of * truth; an index entry only says where to look, and one that no longer matches its record is * skipped and removed. * * On by default where the client has the commands it needs, and off under 2.x's defaults, * `useDefaults('2.x')`. A worker without the index writes records without index entries, so what it * queued is found once `reindex()` indexes what is already there: call it once no such worker * shares the prefix. Needs * `zadd`, `zrem`, and `zrangebyscore`; with `eval`, a record and its index entries change in one * step. */ index?: boolean; } /** * Redis-backed operation storage, so a submitted operation survives a restart. * * Updates are a compare-and-set on `sequence`. When the client exposes `eval` the comparison and * the write happen in one Lua call, which is genuinely atomic; otherwise the store falls back to a * read-compare-write that narrows the race but cannot close it. Prefer a client with `eval` * whenever more than one worker can touch the same operation. */ export declare class RedisOperationStore implements OperationStore { private readonly client; private readonly prefix; private readonly useEval; /** Whether queued and leased work is kept in sorted sets, as `index: true` asks. */ readonly indexed: boolean; constructor(client: RedisOperationLikeClient, options?: RedisOperationStoreOptions); /** * Stores a new record and indexes its idempotency key. Throws `OperationDuplicateError` when another * record holds the key, checked in the same Lua call as the write when the client has `eval`. * Refuses records carrying raw bytes. */ create(record: OperationRecord): Promise; /** Reads a record. */ read(id: string): Promise | undefined>; /** * Writes a record when its stored sequence still equals `expectedSequence`. Resolves false when * another worker got there first. */ update(record: OperationRecord, expectedSequence: number): Promise; /** Deletes a record and its idempotency index. Resolves true when it existed. */ delete(id: string): Promise; /** * Records whose lease has expired, or running records without one, up to `limit`, for another * worker to take over. With the index, reads only records whose lease has lapsed. */ claimExpired(now: string, limit: number): Promise>>; /** Finds the record that claimed an idempotency key. */ findByIdempotencyKey(key: string): Promise | undefined>; /** Every record. */ list(): Promise>>; /** * Queued records no worker holds, oldest first. With the index, reads them oldest first from the * sorted set and stops at `limit`; without it, reads every record, as `list()` does. */ listQueued(limit: number, filter?: OperationStoreFilter): Promise>>; /** Counts unfinished records by status. Reads every record, as `list()` does. */ stats(now: string, filter?: OperationStoreFilter): Promise; /** * Indexes every record already stored, for turning `index` on over existing data. Safe to run * while workers are busy, and to run again: it only adds entries, then removes those that no * longer match their record, checking each record as it does. Resolves to how many records it read. */ reindex(): Promise; /** * Reads one index in score order up to `max`, a page at a time, keeping records that pass `keep` * until `limit`. Entries whose record is gone or no longer `belongs` are removed afterwards. */ private scanIndex; private readMany; /** Removes index entries that no longer describe their record. Only with `eval`, where the check is atomic. */ private prune; /** Writes a record's index entries without `eval`, after the record itself. */ private place; private recordsKey; private idempotencyKey; private queuedKey; private leasesKey; } /** The part of a BullMQ `Queue` the dispatcher needs. */ export interface BullMQLikeOperationQueue { /** Adds a job. */ add(name: string, data: unknown, options?: Record): Promise<{ id?: string | number; }> | { id?: string | number; }; } /** Options for the BullMQ operation dispatcher. */ export interface BullMQOperationDispatcherOptions { /** Job name used for every dispatched operation. Defaults to `nexus-operation`. */ jobName?: string; /** Merged into the BullMQ job options, for priority, delay, or removal policy. */ jobOptions?: Record; } /** * Hands accepted operations to a BullMQ queue for a worker process to execute. * * Only the operation id and its routing metadata are queued — never the record's result or any * payload — so the job stays small and the store remains the single source of truth. The worker * reads the record by id, which is also what makes a redelivered job safe. */ export declare class BullMQOperationDispatcher implements OperationDispatcher { private readonly queue; private readonly options; constructor(queue: BullMQLikeOperationQueue, options?: BullMQOperationDispatcherOptions); /** * Queues an operation's id and routing metadata, using the operation id as the job id so a * duplicate dispatch is ignored. */ dispatch(record: OperationRecord): Promise; }