ravenflow 0.3.1 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -1,673 +1,214 @@
1
- import { z } from 'zod';
2
- import { Job, Queue, QueueEvents, ConnectionOptions, Worker, WorkerOptions } from 'bullmq';
1
+ import { Job, ConnectionOptions } from 'bullmq';
3
2
  import pino from 'pino';
4
- import { IndexSpecification, CreateIndexesOptions, Document, Collection, Db, AnyBulkWriteOperation } from 'mongodb';
5
3
 
6
- interface BaseJobConfig {
7
- queue?: string;
8
- cron?: string;
9
- attempts?: number;
10
- backoff?: {
11
- type: "fixed" | "exponential";
12
- delay: number;
13
- };
4
+ declare const CONTINUATION: unique symbol;
5
+ interface Continuation<TPayload> {
6
+ readonly [CONTINUATION]: true;
7
+ readonly params: TPayload;
8
+ readonly delayMs?: number;
9
+ readonly deduplicationId?: string;
14
10
  }
15
- declare const zRateLimitConfig: z.ZodUnion<readonly [z.ZodObject<{
16
- perSecond: z.ZodNumber;
17
- }, z.core.$strip>, z.ZodObject<{
18
- perMinute: z.ZodNumber;
19
- }, z.core.$strip>]>;
20
- type RateLimitConfig = z.infer<typeof zRateLimitConfig>;
21
- interface MemoryEtlStepConfig {
22
- rateLimit?: RateLimitConfig;
23
- }
24
- interface StepConfig extends MemoryEtlStepConfig {
25
- attempts?: number;
26
- backoff?: {
27
- type: "fixed" | "exponential";
28
- delay: number;
29
- };
30
- queue?: string;
31
- }
32
- interface EtlExtractResult<TRaw, TCursor> {
33
- data: TRaw[];
34
- cursor: TCursor | null;
35
- hasMore: boolean;
36
- }
37
- declare const zJobStatus: z.ZodEnum<{
38
- pending: "pending";
39
- in_progress: "in_progress";
40
- completed: "completed";
41
- failed: "failed";
42
- stalled: "stalled";
43
- }>;
44
- type JobStatus = z.infer<typeof zJobStatus>;
45
-
46
- interface SimpleJobConfig<TData, TReturn> extends BaseJobConfig {
47
- type: "simple";
48
- handler: (job: Job, params: TData) => Promise<TReturn>;
49
- }
50
- declare function defineSimpleJob<TData, TReturn>(config: Omit<SimpleJobConfig<TData, TReturn>, "type">): SimpleJobConfig<TData, TReturn>;
51
-
52
- interface MemoryEtlJobConfig<TRaw, TProcessed, TCursor = number> extends BaseJobConfig {
53
- type: "memory-etl";
54
- extract: (cursor: TCursor | null) => Promise<EtlExtractResult<TRaw, TCursor>>;
55
- transform: (data: TRaw) => Promise<TProcessed>;
56
- load: (data: TProcessed[]) => Promise<void>;
57
- steps?: {
58
- extract?: MemoryEtlStepConfig;
59
- transform?: MemoryEtlStepConfig;
60
- load?: MemoryEtlStepConfig;
61
- };
11
+ interface ContinueOptions {
12
+ delayMs?: number;
13
+ deduplicationId?: string;
62
14
  }
63
- declare function defineMemoryEtlJob<TRaw, TProcessed, TCursor = number>(config: Omit<MemoryEtlJobConfig<TRaw, TProcessed, TCursor>, "type">): MemoryEtlJobConfig<TRaw, TProcessed, TCursor>;
64
-
65
- interface DistributedEtlJobConfig<TRaw, TProcessed, TCursor = number> extends BaseJobConfig {
66
- type: "distributed-etl";
67
- extract: (cursor: TCursor | null) => Promise<EtlExtractResult<TRaw, TCursor>>;
68
- transform: (data: TRaw) => Promise<TProcessed>;
69
- load: (data: TProcessed[]) => Promise<void>;
70
- steps?: {
71
- extract?: StepConfig;
72
- transform?: StepConfig;
73
- load?: StepConfig;
74
- };
75
- }
76
- declare function defineDistributedEtlJob<TRaw, TProcessed, TCursor = number>(config: Omit<DistributedEtlJobConfig<TRaw, TProcessed, TCursor>, "type">): DistributedEtlJobConfig<TRaw, TProcessed, TCursor>;
77
-
78
- interface StatefulEtlJobConfig<TRaw, TProcessed, TCursor = string> extends BaseJobConfig {
79
- type: "stateful-etl";
80
- extract: (cursor: TCursor | null) => Promise<EtlExtractResult<TRaw, TCursor>>;
81
- transform: (data: TRaw) => Promise<TProcessed>;
82
- load: (data: TProcessed[]) => Promise<void>;
83
- /**
84
- * When true, only one run of this job can be active at a time.
85
- * If a run is already pending or in_progress (with a live heartbeat), the new trigger is skipped.
86
- * Defaults to false — each trigger creates an independent run.
87
- */
88
- singletonRun?: boolean;
89
- stallTimeoutMs?: number;
90
- maxStallRetries?: number;
91
- steps?: {
92
- extract?: StepConfig;
93
- transform?: StepConfig;
94
- load?: StepConfig;
95
- };
96
- }
97
- declare function defineStatefulEtlJob<TRaw, TProcessed, TCursor = string>(config: Omit<StatefulEtlJobConfig<TRaw, TProcessed, TCursor>, "type">): StatefulEtlJobConfig<TRaw, TProcessed, TCursor>;
98
-
99
- type TokenBucketInterval = "second" | "minute" | "hour" | "day" | "week";
100
- interface TokenBucketJobConfig extends BaseJobConfig {
101
- type: "token-bucket";
102
- cron: string;
103
- rate?: {
104
- amount: number;
105
- interval: TokenBucketInterval;
106
- };
107
- getBudget?: () => Promise<number> | number;
108
- maxCarryOver?: number;
109
- consumeTimeout?: number;
110
- consume: (tokens: number) => Promise<number | void> | number | void;
111
- }
112
- declare function defineTokenBucketJob(config: Omit<TokenBucketJobConfig, "type">): TokenBucketJobConfig;
113
-
114
- type JobConfig = SimpleJobConfig<any, any> | MemoryEtlJobConfig<any, any, any> | DistributedEtlJobConfig<any, any, any> | StatefulEtlJobConfig<any, any, any> | TokenBucketJobConfig;
115
-
116
- interface RetentionConfig {
117
- /** Number of completed jobs to keep per queue. Set to 0 to remove immediately. */
118
- completed?: number;
119
- /** Number of failed jobs to keep per queue. Set to 0 to remove immediately. */
120
- failed?: number;
121
- }
122
- interface RavenflowConfig<TJobs extends string = string> {
123
- defaultQueue: string;
124
- jobs: Record<TJobs, JobConfig>;
125
- }
126
- type JobNames<T extends RavenflowConfig> = T extends RavenflowConfig<infer TJobs> ? TJobs : never;
127
- declare function defineConfig<const T extends RavenflowConfig>(config: T): T;
128
-
129
- interface EtlResult {
130
- extractedCount: number;
131
- transformedCount: number;
132
- loadedCount: number;
133
- }
134
- type JobProxy<T extends JobConfig> = T extends SimpleJobConfig<infer TData, infer TReturn> ? {
135
- send: (params: TData) => Promise<Job<TData, TReturn>>;
136
- sendAndWait: (params: TData) => Promise<TReturn>;
137
- } : T extends MemoryEtlJobConfig<any, any, any> ? {
138
- send: () => Promise<void>;
139
- sendAndWait: () => Promise<EtlResult>;
140
- } : T extends DistributedEtlJobConfig<any, any> ? {
141
- send: () => Promise<void>;
142
- sendAndWait: () => Promise<EtlResult>;
143
- } : T extends StatefulEtlJobConfig<any, any, any> ? {
144
- send: () => Promise<void>;
145
- sendAndWait: () => Promise<void>;
146
- } : T extends TokenBucketJobConfig ? {
147
- send: () => Promise<void>;
148
- sendAndWait: () => Promise<void>;
149
- } : never;
150
- type JobsProxy<TJobs extends Record<string, JobConfig>> = {
151
- [K in keyof TJobs]: JobProxy<TJobs[K]>;
152
- };
153
15
 
154
16
  type Logger = pino.Logger;
155
17
  interface LoggerOptions {
156
18
  level?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
157
19
  pretty?: boolean;
158
20
  }
159
- declare function initLogger(options?: LoggerOptions): Logger;
160
- declare function getLogger(): Logger;
161
-
162
- type QueueStore = Record<string, {
163
- queue: Queue;
164
- events: QueueEvents;
165
- }>;
166
21
 
167
- declare class StallWatcher {
168
- private readonly workerQueue;
169
- private readonly statefulEtlConfigs;
170
- private readonly userQueues;
171
- constructor(jobConfigs: Record<string, JobConfig>, userQueues: Record<string, {
172
- queue: Queue;
173
- }>, defaultQueue: string, workerQueue: Queue);
174
- canProcess(jobName: string): boolean;
175
- start(): Promise<void>;
176
- stop(): Promise<void>;
177
- process(job: Job): Promise<void>;
178
- }
22
+ interface JobContext<TParams> {
23
+ params: TParams;
24
+ deduplicationId?: string;
25
+ continueWith(params: TParams, options?: ContinueOptions): Continuation<TParams>;
26
+ job: Job;
27
+ logger: Logger;
28
+ signal: AbortSignal;
29
+ }
30
+
31
+ interface BackoffPolicy {
32
+ type: "fixed" | "exponential";
33
+ delayMs: number;
34
+ jitter?: number;
35
+ }
36
+ type DeduplicatePolicy<TPayload> = true | ((params: TPayload) => string);
37
+ type PacePolicy = {
38
+ perSecond: number;
39
+ perMinute?: never;
40
+ perHour?: never;
41
+ } | {
42
+ perMinute: number;
43
+ perSecond?: never;
44
+ perHour?: never;
45
+ } | {
46
+ perHour: number;
47
+ perSecond?: never;
48
+ perMinute?: never;
49
+ };
179
50
 
180
- interface IndexDescriptor {
181
- keys: IndexSpecification;
182
- options?: CreateIndexesOptions;
183
- }
184
- type BucketGranularity = "hour" | "day";
185
- interface IncrementStatsInput {
186
- success: boolean;
187
- durationMs?: number;
188
- }
189
- interface JobStatsDoc {
190
- jobType: string;
191
- bucketStart: Date;
192
- granularity: BucketGranularity;
193
- count: number;
194
- success: number;
195
- failed: number;
196
- totalDurationMs: number;
197
- createdAt: Date;
198
- updatedAt: Date;
199
- }
200
- interface JobErrorDoc {
201
- signature: string;
202
- jobType: string;
203
- message: string;
204
- stack: string;
205
- count: number;
206
- firstSeenAt: Date;
207
- lastSeenAt: Date;
208
- createdAt: Date;
209
- updatedAt: Date;
210
- /** Normalized `Error.cause` chain beyond the top-level error. OMITTED when empty. */
211
- causeChain?: Array<{
212
- message: string;
213
- name: string;
214
- }>;
215
- }
216
- interface RecordOccurrenceInput {
217
- jobType: string;
218
- error: unknown;
219
- }
220
- interface TopErrorsOptions {
221
- limit: number;
222
- jobType?: string;
223
- /**
224
- * When set, restricts results to errors whose `lastSeenAt >= since`.
225
- * Dashboard health views pass the range start (e.g. 7 days ago, UTC midnight)
226
- * so "top errors" matches the displayed range; omitting returns all errors
227
- * back to the collection's 90-day TTL boundary.
228
- */
229
- since?: Date;
230
- }
231
- interface JobFailureDoc {
232
- jobId: string;
233
- jobType: string;
234
- input: string;
235
- errorMessage: string;
236
- errorStack: string;
237
- logs: string;
238
- failedAt: Date;
239
- createdAt: Date;
240
- /** Normalized `Error.cause` chain beyond the top-level error. OMITTED when empty. */
241
- causeChain?: Array<{
242
- message: string;
243
- name: string;
244
- }>;
245
- }
246
- interface RecordFailureInput {
247
- jobId: string;
248
- jobType: string;
249
- input: unknown;
250
- error: unknown;
251
- failedAt: Date;
252
- logs?: string;
51
+ type JobTrigger = CronTrigger | SpoolTriggerDeclaration;
52
+ interface CronTrigger {
53
+ cron: string;
54
+ spool?: never;
253
55
  }
254
- interface ListFailuresOptions {
255
- jobType?: string;
256
- fromDate?: Date | null;
257
- toDate?: Date | null;
258
- skip?: number;
259
- limit?: number;
56
+ interface SpoolTriggerDeclaration {
57
+ spool: SpoolTrigger;
58
+ cron?: never;
260
59
  }
261
- declare const JOB_FAILURES_INPUT_MAX_CHARS = 100000;
262
- declare const JOB_FAILURES_MESSAGE_MAX_CHARS = 2000;
263
- declare const JOB_FAILURES_STACK_MAX_CHARS = 8000;
264
- declare const JOB_FAILURES_LOGS_MAX_CHARS = 500000;
265
- interface WorkerQueueEntry {
60
+ interface SpoolTrigger {
266
61
  name: string;
267
- concurrency: number;
268
- }
269
- interface WorkerDoc {
270
- workerId: string;
271
- hostname: string;
272
- pid: number;
273
- queues: WorkerQueueEntry[];
274
- startedAt: Date;
275
- lastHeartbeatAt: Date;
276
- createdAt: Date;
277
- updatedAt: Date;
278
- }
279
- interface RegisterWorkerInput {
280
- workerId: string;
281
- hostname: string;
282
- pid: number;
283
- queues: WorkerQueueEntry[];
284
- startedAt: Date;
285
- }
286
- interface UpsertWorkerQueueInput {
287
- workerId: string;
288
- queue: WorkerQueueEntry;
289
- }
290
- /**
291
- * Read shape for the `job_configs` collection — what `JobConfigsRepo.getConfig`
292
- * returns (with persistence timestamps stripped, or synthesized for absent records).
293
- *
294
- * Distinct from `JobConfig` (in `src/types/job/unions.ts`), which is the discriminated
295
- * union of job-definition types (simple / memory-etl / distributed-etl / etc.) used
296
- * at app-registration time. `JobConfigEntry` is the runtime persistence shape for
297
- * dashboard-managed per-job-type settings (enabled flag + free-form metadata).
298
- */
299
- interface JobConfigEntry {
300
- jobType: string;
301
- enabled: boolean;
302
- metadata: Record<string, unknown>;
62
+ tickMs?: number;
63
+ canPull?: () => boolean | Promise<boolean>;
64
+ expireUnclaimedAfterMs?: number;
303
65
  }
304
- /**
305
- * Persisted shape for the `job_configs` collection — the read shape plus
306
- * MongoDB-managed timestamps. Returned by `JobConfigsRepo.listAll` (every row
307
- * there is, by definition, persisted, so the timestamps are always real).
308
- */
309
- interface JobConfigDoc extends JobConfigEntry {
310
- createdAt: Date;
311
- updatedAt: Date;
66
+ interface DeclaredTrigger {
67
+ cron?: string;
68
+ spool?: SpoolTrigger;
312
69
  }
313
70
 
314
- declare abstract class Repository<T extends Document> {
315
- readonly collection: Collection<T>;
316
- constructor(db: Db, rawCollectionName: string);
317
- protected abstract indexes(): IndexDescriptor[];
318
- init(): Promise<void>;
71
+ type TriggerFor<TPayload> = SatisfiableByNothing<TPayload> extends true ? JobTrigger : SpoolTriggerDeclaration | CronRefused;
72
+ type CronTriggerFor<TPayload> = SatisfiableByNothing<TPayload> extends true ? CronTrigger : CronRefused;
73
+ type SatisfiableByNothing<TPayload> = AcceptsUndefined<TPayload> extends true ? true : AcceptsEmptyObject<TPayload>;
74
+ type AcceptsUndefined<TPayload> = undefined extends TPayload ? true : false;
75
+ type AcceptsEmptyObject<TPayload> = {} extends TPayload ? true : false;
76
+ interface CronRefused {
77
+ cron?: CronNeedsAPayloadNothingHasToProvide;
78
+ spool?: never;
319
79
  }
80
+ type CronNeedsAPayloadNothingHasToProvide = "a cron trigger has nothing to pass this job: make every field of its payload optional, or declare the payload as void";
320
81
 
321
- declare class WorkersRepo extends Repository<WorkerDoc> {
322
- constructor(db: Db);
323
- protected indexes(): IndexDescriptor[];
324
- registerWorker(input: RegisterWorkerInput): Promise<void>;
325
- updateHeartbeat(workerId: string): Promise<void>;
326
- /**
327
- * Appends or replaces a queue entry on the worker's record.
328
- *
329
- * Single-writer invariant: this method is NOT atomic under concurrent writers to the
330
- * SAME `workerId`. WorkerRegistry is a process-local singleton, and `workerId` is
331
- * unique per process by contract (default: `<hostname>:<pid>:<uuid>`). Sharing a
332
- * stable `workerId` across processes violates AC #6 and will race last-writer-wins.
333
- */
334
- addQueue(input: UpsertWorkerQueueInput): Promise<void>;
335
- deregisterWorker(workerId: string): Promise<void>;
336
- findAlive(maxHeartbeatAgeMs: number): Promise<WorkerDoc[]>;
337
- cleanupStale(maxHeartbeatAgeMs: number): Promise<number>;
338
- private validateRegisterInput;
339
- private validateQueueEntry;
340
- private validateMaxHeartbeatAgeMs;
341
- }
82
+ interface JobDefinition<TPayload, TResult> {
83
+ queue?: string;
84
+ attempts?: number;
85
+ backoff?: BackoffPolicy;
86
+ trigger?: TriggerFor<TPayload>;
87
+ deduplicate?: DeduplicatePolicy<TPayload>;
88
+ pace?: PacePolicy;
89
+ handler: JobHandler<TPayload, TResult>;
90
+ }
91
+ type JobHandler<TPayload, TResult> = (ctx: JobContext<TPayload>) => Promise<TResult | Continuation<TPayload>>;
92
+ type SpoolFedJob<TPayload, TResult> = JobDefinition<TPayload, TResult> & {
93
+ trigger: SpoolTriggerDeclaration;
94
+ pace: PacePolicy;
95
+ };
96
+ type ClockFedJob<TPayload, TResult> = JobDefinition<TPayload, TResult> & {
97
+ trigger: CronTrigger;
98
+ };
99
+ declare function defineJob<TPayload, TResult>(definition: JobDefinitionWith<SpoolTriggerDeclaration, TPayload, TResult> & {
100
+ pace: PacePolicy;
101
+ }): SpoolFedJob<TPayload, TResult>;
102
+ declare function defineJob<TPayload, TResult>(definition: JobDefinitionWith<CronTriggerFor<TPayload>, TPayload, TResult>): ClockFedJob<TPayload, TResult>;
103
+ declare function defineJob<TPayload, TResult>(definition: UnspooledJobDefinition<TPayload, TResult>): JobDefinition<TPayload, TResult>;
104
+ type JobDefinitionWith<TTrigger, TPayload, TResult> = Omit<JobDefinition<TPayload, TResult>, "trigger"> & {
105
+ trigger: TTrigger;
106
+ };
107
+ type UnspooledJobDefinition<TPayload, TResult> = Omit<JobDefinition<TPayload, TResult>, "trigger"> & {
108
+ trigger?: CronTriggerFor<TPayload>;
109
+ };
342
110
 
343
- declare class JobStatsRepo extends Repository<JobStatsDoc> {
344
- constructor(db: Db);
345
- protected indexes(): IndexDescriptor[];
346
- /**
347
- * Builds the bulk write operations for a stats increment WITHOUT executing them.
348
- * Returns TWO ops — one for the hour bucket, one for the day bucket — matching
349
- * `incrementStats`'s direct-call shape bit-for-bit. Intended to be pushed through
350
- * a `BatchWriter` by the event-hooks layer (Story 1.7). Throws synchronously on
351
- * invalid `durationMs`; otherwise no side effects.
352
- *
353
- * NOTE: `now` is captured at builder invocation time, NOT at BatchWriter flush
354
- * time — `createdAt`, `updatedAt`, and `bucketStart` all reflect the event's
355
- * wall-clock, not the flush wall-clock. Aggregations are therefore correctly
356
- * attributed to the hour/day the event occurred, even if the batch flush is
357
- * delayed by the buffer timer.
358
- */
359
- toIncrementStatsOps(jobType: string, input: IncrementStatsInput): AnyBulkWriteOperation<JobStatsDoc>[];
360
- incrementStats(jobType: string, input: IncrementStatsInput): Promise<void>;
361
- getStats(jobType: string, fromDate: Date, toDate: Date, granularity: BucketGranularity): Promise<JobStatsDoc[]>;
362
- /**
363
- * Fetches every stats document in the given range + granularity, across ALL
364
- * jobTypes. Intended for dashboard views that need a cross-jobType overview
365
- * (e.g., "7-day health per job type") without a prior enumeration step.
366
- *
367
- * Result order: `bucketStart` ascending, then `jobType` ascending. Caller is
368
- * responsible for grouping by jobType client-side if desired.
369
- *
370
- * Applies the same `MAX_GET_STATS_RESULTS` cap as `getStats` — a single query
371
- * across all jobTypes × buckets × granularity is bounded, but the cap keeps
372
- * the method safe under accidental wide-range queries.
373
- */
374
- getStatsAllJobTypes(fromDate: Date, toDate: Date, granularity: BucketGranularity): Promise<JobStatsDoc[]>;
375
- private upsertBucket;
376
- private buildBucketOp;
377
- private buildBucketPayload;
378
- private validateDurationMs;
111
+ interface AnyJob {
112
+ queue?: string;
113
+ attempts?: number;
114
+ backoff?: BackoffPolicy;
115
+ trigger?: DeclaredTrigger;
116
+ deduplicate?: DeduplicatePolicy<never>;
117
+ pace?: PacePolicy;
118
+ handler: (ctx: JobContext<never>) => Promise<unknown>;
379
119
  }
380
120
 
381
- declare class JobErrorsRepo extends Repository<JobErrorDoc> {
382
- constructor(db: Db);
383
- protected indexes(): IndexDescriptor[];
384
- /**
385
- * Records one occurrence of an error, deduplicated by its stable signature.
386
- *
387
- * Invariant: `jobType` is set **only on insert** (`$setOnInsert`). Once a signature
388
- * is first observed under jobType A, subsequent occurrences under jobType B do NOT
389
- * overwrite the stored value. The signature is the identity of the error; a signature
390
- * appearing across multiple job types indicates shared infrastructure failure (e.g.
391
- * ECONNRESET from the shared DB) and is worth surfacing on the first-observed job.
392
- */
393
- recordOccurrence(input: RecordOccurrenceInput): Promise<void>;
394
- /**
395
- * Builds the bulk write operation for `recordOccurrence` WITHOUT executing it.
396
- * Returns a single upsert op matching the direct-call shape bit-for-bit.
397
- *
398
- * NOTE: `now` is captured at builder invocation time, NOT at BatchWriter flush
399
- * time — firstSeenAt / lastSeenAt reflect the event's wall-clock, not the
400
- * flush wall-clock. This preserves the semantic that these timestamps track
401
- * the error's occurrence, not the observability pipeline's delay.
402
- */
403
- toRecordOccurrenceOp(input: RecordOccurrenceInput): AnyBulkWriteOperation<JobErrorDoc>;
404
- private buildRecordOccurrencePayload;
405
- topErrors(options: TopErrorsOptions): Promise<JobErrorDoc[]>;
121
+ interface RavenflowConfig<TJobs extends string = string> {
122
+ defaultQueue: string;
123
+ jobs: Record<TJobs, AnyJob>;
124
+ logger?: LoggerOptions;
125
+ queuesRetention?: QueuesRetentionConfig;
126
+ jobResultsRetentionHours?: number;
127
+ aggregatedResultsRetentionHours?: number;
406
128
  }
407
-
408
- declare class JobFailuresRepo extends Repository<JobFailureDoc> {
409
- constructor(db: Db);
410
- protected indexes(): IndexDescriptor[];
411
- recordFailure(input: RecordFailureInput): Promise<void>;
412
- /**
413
- * Builds the bulk write operation for `recordFailure` WITHOUT executing it.
414
- * Returns a single insertOne op matching the direct-call document shape
415
- * bit-for-bit. Intended for the event-hooks layer (Story 1.7) to push through
416
- * a `BatchWriter`. Throws synchronously on invalid input using the same error
417
- * messages as `recordFailure`.
418
- */
419
- toRecordFailureOp(input: RecordFailureInput): AnyBulkWriteOperation<JobFailureDoc>;
420
- private buildFailureDoc;
421
- private validateFailureInput;
422
- listFailures(options: ListFailuresOptions): Promise<{
423
- items: JobFailureDoc[];
424
- total: number;
425
- }>;
426
- getFailure(jobId: string): Promise<JobFailureDoc | null>;
427
- private serializeInputValue;
428
- private capLogs;
129
+ interface QueuesRetentionConfig {
130
+ completedCount?: number;
131
+ failedCount?: number;
429
132
  }
133
+ declare function defineConfig<const T extends RavenflowConfig>(config: T): T;
430
134
 
431
- declare class JobConfigsRepo extends Repository<JobConfigDoc> {
432
- constructor(db: Db);
433
- protected indexes(): IndexDescriptor[];
434
- getConfig(jobType: string): Promise<JobConfigEntry>;
435
- setEnabled(jobType: string, enabled: boolean): Promise<void>;
436
- listAll(): Promise<JobConfigDoc[]>;
437
- isEnabled(jobType: string): Promise<boolean>;
438
- /**
439
- * Idempotent bulk upsert of every registered jobType + its metadata
440
- * projection (Story 5.1 D5.1-a). Called once per `RavenflowApp` instance
441
- * inside `initializeMonitoring()`.
442
- *
443
- * Semantics:
444
- * - `$setOnInsert { enabled: true, createdAt }` — the enabled flag is
445
- * set to the default ONLY on initial insert. An operator-toggled
446
- * `enabled: false` is NEVER overwritten by a subsequent boot.
447
- * - `$set { metadata }` — metadata is always refreshed from code config
448
- * (code is source of truth for non-user-writable fields).
449
- * - `$currentDate { updatedAt }` — timestamp bumps every call.
450
- *
451
- * Metadata projection (narrow + stable — see D5.1-h):
452
- * - `type`: jobConfig.type (always present in the discriminated union).
453
- * - `queue`: jobConfig.queue ?? null (null = inherits defaultQueue).
454
- * - `cron`: jobConfig.cron if present (token-bucket only today), else null.
455
- *
456
- * Additional type-specific fields (token-bucket's `rate`, distributed-etl's
457
- * `steps`) are NOT extracted — BSON can't serialize the function references
458
- * they contain, and the dashboard surface consumes only `type`/`queue`/`cron`
459
- * in V1. Future stories can widen this projection without a schema change.
460
- *
461
- * Failure mode: `bulkWrite` is the only I/O. Empty input short-circuits
462
- * because the driver rejects empty ops arrays.
463
- */
464
- ensureEntries(jobs: Record<string, JobConfig>): Promise<void>;
465
- private validateJobType;
135
+ interface SpoolPurgeReport {
136
+ dropped: number;
137
+ inFlight: number;
466
138
  }
467
139
 
468
- interface WorkerRegistryOptions {
469
- heartbeatIntervalMs?: number;
470
- /** @internal Injected for testability; real callers should rely on the default. */
471
- hostnameProvider?: () => string;
472
- /** @internal Injected for testability; real callers should rely on the default. */
473
- pidProvider?: () => number;
474
- /** @internal Injected for testability; real callers should rely on the default. */
475
- workerIdProvider?: () => string;
476
- /** @internal Injected for testability; real callers should rely on the default. */
477
- clock?: () => Date;
140
+ type JobsProxy<TJobs extends Record<string, AnyJob>> = {
141
+ [K in keyof TJobs]: JobProxy<TJobs[K]>;
142
+ };
143
+ type JobProxy<T extends AnyJob> = T extends JobDefinition<infer TPayload, infer TResult> ? SendOrIngest<T, TPayload, TResult> & {
144
+ controls: ControlsOf<T>;
145
+ } : never;
146
+ type SendOrIngest<T extends AnyJob, TPayload, TResult> = T extends {
147
+ trigger: SpoolTriggerDeclaration;
148
+ } ? SpoolIngest<TPayload> : JobSend<TPayload, TResult>;
149
+ interface JobSend<TPayload, TResult> {
150
+ send(params: TPayload, options?: SendOptions): Promise<Job<TPayload, TResult>>;
151
+ }
152
+ interface SendOptions {
153
+ delayMs?: number;
154
+ priority?: number;
155
+ deduplicationId?: string;
156
+ }
157
+ interface SpoolIngest<TItem> {
158
+ push(item: TItem): Promise<string>;
159
+ pushMany(items: TItem[]): Promise<string[]>;
160
+ }
161
+ type ControlsOf<T extends AnyJob> = T extends {
162
+ trigger: SpoolTriggerDeclaration;
163
+ } ? SpoolFedControls : T extends {
164
+ trigger: CronTrigger;
165
+ } ? ClockFedControls : ExecutionControls;
166
+ interface ExecutionControls {
167
+ pauseExecution(): Promise<void>;
168
+ resumeExecution(): Promise<void>;
169
+ }
170
+ interface TriggerControls {
171
+ pauseTrigger(): Promise<void>;
172
+ resumeTrigger(): Promise<void>;
173
+ }
174
+ interface IngestControls {
175
+ pauseIngest(): Promise<void>;
176
+ resumeIngest(): Promise<void>;
177
+ }
178
+ interface ClockFedControls extends ExecutionControls, TriggerControls {
179
+ }
180
+ interface SpoolFedControls extends ExecutionControls, TriggerControls, IngestControls {
181
+ purgeBacklog(): Promise<SpoolPurgeReport>;
182
+ }
183
+
184
+ interface RavenflowClient<TConfig extends RavenflowConfig = RavenflowConfig> {
185
+ readonly jobs: JobsProxy<TConfig["jobs"]>;
186
+ close(): Promise<void>;
478
187
  }
479
- declare class WorkerRegistry {
480
- private readonly repo;
481
- private readonly heartbeatIntervalMs;
482
- private readonly hostname;
483
- private readonly pid;
484
- private readonly _workerId;
485
- private readonly startedAt;
486
- private readonly clock;
487
- private timer;
488
- private started;
489
- private registered;
490
- constructor(repo: WorkersRepo, options?: WorkerRegistryOptions);
491
- get workerId(): string;
492
- registerQueue(queue: WorkerQueueEntry): Promise<void>;
493
- start(): Promise<void>;
494
- stop(): Promise<void>;
495
- private doHeartbeat;
496
- private static validateHeartbeatInterval;
497
- private static validateQueueEntry;
188
+ interface RavenflowClientOptions {
189
+ redis: ConnectionOptions;
190
+ mongo: string;
498
191
  }
499
192
 
500
- interface BatchWriterConfig {
501
- /** Max ops buffered before a size-triggered flush. Must be in [1000, 5000]. */
502
- maxBufferSize: number;
503
- /** Interval between time-triggered flushes in ms. Default 30000. Also used as `maxTimeMS` on bulkWrite. */
504
- flushIntervalMs?: number;
505
- }
193
+ declare function createRavenflowClient<TConfig extends RavenflowConfig>(config: TConfig, options: RavenflowClientOptions): RavenflowClient<TConfig>;
506
194
 
507
- declare class BatchWriter<T extends Document = Document> {
508
- private readonly collection;
509
- private readonly maxBufferSize;
510
- private readonly hardCapSize;
511
- private readonly flushIntervalMs;
512
- private buffer;
513
- private timer;
514
- private flushing;
515
- private drained;
516
- constructor(collection: Collection<T>, config: BatchWriterConfig);
517
- push(operation: AnyBulkWriteOperation<T>): void;
518
- drain(): Promise<void>;
519
- private flush;
520
- private doFlush;
521
- private enforceHardCap;
522
- private startTimer;
523
- private stopTimer;
524
- private resetTimer;
525
- private validateConfig;
195
+ interface RavenflowWorker {
196
+ start(): Promise<void>;
197
+ close(): Promise<void>;
526
198
  }
527
199
 
528
- interface MongoConnectionObject {
529
- host: string;
530
- port?: number;
531
- username?: string;
532
- password?: string;
533
- database?: string;
534
- }
535
- interface RavenflowAppOptions {
200
+ interface RavenflowWorkerOptions {
536
201
  redis: ConnectionOptions;
537
- mongo?: string | MongoConnectionObject;
538
- retention?: RetentionConfig;
539
- logger?: LoggerOptions;
540
- monitoring?: {
541
- enabled?: boolean;
542
- heartbeatIntervalMs?: number;
543
- batchWriter?: {
544
- maxBufferSize?: number;
545
- flushIntervalMs?: number;
546
- };
547
- };
548
- }
549
- declare class RavenflowApp<TConfig extends RavenflowConfig = RavenflowConfig> {
550
- protected redisConnection: ConnectionOptions;
551
- protected defaultConcurrency: number;
552
- protected config: TConfig;
553
- protected mongoUri: string | null;
554
- jobs: JobsProxy<TConfig["jobs"]>;
555
- protected jobConfigs: Record<JobNames<TConfig>, JobConfig>;
556
- protected queues: QueueStore;
557
- protected workers: Worker[];
558
- protected stallWatcher: StallWatcher | null;
559
- protected workersRepo: WorkersRepo | null;
560
- protected workerRegistry: WorkerRegistry | null;
561
- protected jobStatsRepo: JobStatsRepo | null;
562
- protected jobErrorsRepo: JobErrorsRepo | null;
563
- protected jobFailuresRepo: JobFailuresRepo | null;
564
- protected jobConfigsRepo: JobConfigsRepo | null;
565
- protected jobStatsBatchWriter: BatchWriter<JobStatsDoc> | null;
566
- protected jobErrorsBatchWriter: BatchWriter<JobErrorDoc> | null;
567
- protected jobFailuresBatchWriter: BatchWriter<JobFailureDoc> | null;
568
- protected monitoringInitPromise: Promise<void> | null;
569
- protected readonly monitoringOptions: {
570
- enabled: boolean;
571
- heartbeatIntervalMs: number | undefined;
572
- batchWriter: BatchWriterConfig;
573
- };
574
- constructor(config: TConfig, options: RavenflowAppOptions);
575
- startWorker(queueName: string, options?: {
576
- concurrency?: number;
577
- }): Promise<void>;
578
- protected initializeMonitoring(queueName: string, concurrency: number): Promise<void>;
579
- close(): Promise<void>;
202
+ mongo: string;
203
+ queue: string;
204
+ concurrency?: number;
205
+ withdrawal?: WithdrawalOptions;
580
206
  }
581
-
582
- declare function errorSignature(err: unknown): string;
583
-
584
- declare function captureJobLogs(queue: Queue, jobId: string, maxChars: number): Promise<string>;
585
-
586
- interface EventHooksOptions {
587
- queue: Queue;
588
- jobConfigs: Record<string, JobConfig>;
589
- jobStatsRepo: JobStatsRepo;
590
- jobErrorsRepo: JobErrorsRepo;
591
- jobFailuresRepo: JobFailuresRepo;
592
- jobStatsBatchWriter: BatchWriter<JobStatsDoc>;
593
- jobErrorsBatchWriter: BatchWriter<JobErrorDoc>;
594
- jobFailuresBatchWriter: BatchWriter<JobFailureDoc>;
595
- logsMaxChars?: number;
207
+ interface WithdrawalOptions {
208
+ afterMs?: number;
209
+ onWithdraw?: (reason: string) => void | Promise<void>;
596
210
  }
597
- declare function attachEventHooks(worker: Worker, options: EventHooksOptions): void;
598
-
599
- /**
600
- * Inlined minute-count constants. Mirrors BullMQ's `MetricsTime` enum
601
- * (bullmq@5.63.1) but severs the import edge so consumers of this constant
602
- * don't pull BullMQ into their bundle. Values are pure math (minutes in
603
- * the named window) and therefore stable across BullMQ versions. Lives
604
- * here instead of `index.ts` to avoid an import cycle
605
- * (`index.ts` → `./app/index` → `./app/app` → `worker-manager`).
606
- */
607
- declare const METRICS_TIME: {
608
- readonly ONE_MINUTE: 1;
609
- readonly FIVE_MINUTES: 5;
610
- readonly FIFTEEN_MINUTES: 15;
611
- readonly THIRTY_MINUTES: 30;
612
- readonly ONE_HOUR: 60;
613
- readonly ONE_DAY: 1440;
614
- readonly ONE_WEEK: 10080;
615
- readonly TWO_WEEKS: 20160;
616
- readonly ONE_MONTH: 80640;
617
- };
618
- /**
619
- * Default BullMQ metrics configuration applied to every ravenflow-managed
620
- * Worker. Enables the per-minute data-point collection read by the
621
- * ravenflow-dashboard Jobs-per-Second indicator (Epic 4 Story 4.2).
622
- *
623
- * Without this default, BullMQ's `collectMetrics` Lua function is a no-op on
624
- * every `moveToFinished` — `queue.getMetrics('completed')` would return
625
- * `{ meta: { count: 0 }, data: [] }` indefinitely for Ravenflow-managed
626
- * queues (silent false-negative under real throughput).
627
- *
628
- * `ONE_WEEK = 10_080` minutes ≈ 7 days of minute-resolution history at a
629
- * Redis footprint of ~10 KB/queue/week. Callers may override via the
630
- * `workerOpts.metrics` parameter on `createWorker` (e.g. to disable
631
- * collection or pick a shorter window).
632
- *
633
- * Frozen so the exported reference cannot be mutated by a caller (shared
634
- * default would otherwise bleed globally across `createWorker` calls).
635
- */
636
- declare const DEFAULT_WORKER_METRICS: NonNullable<WorkerOptions["metrics"]>;
637
-
638
- /**
639
- * Delay (ms) applied to jobs whose jobType is `enabled: false`. After this
640
- * window BullMQ returns the job to the `waiting` set and a worker picks it
641
- * up again; the gate re-evaluates. When the jobType re-enables the next
642
- * attempt passes the check and dispatches normally.
643
- *
644
- * 60 seconds strikes the balance between "pause takes effect quickly" and
645
- * "don't hammer Redis / Mongo with redefer churn" (D5.1-b).
646
- */
647
- declare const DISABLED_JOB_DEFER_DELAY_MS = 60000;
648
- /**
649
- * Worker-side defer gate for disabled jobTypes (Story 5.1 D5.1-b).
650
- *
651
- * When the jobType's `enabled` flag is `false` in `job_configs`, this
652
- * helper moves the job to `delayed` with a +60s timestamp, increments
653
- * the cumulative `disabledDeferCount` on `job.progress`, and throws
654
- * `DelayedError` — the BullMQ-canonical signal for "I moved this job to
655
- * delayed, don't mark completed/failed" (verified against
656
- * node_modules/bullmq/dist/esm/classes/errors/delayed-error.d.ts:9
657
- * bullmq@5.63.1).
658
- *
659
- * When the jobType is enabled (or no row exists — default is `true`),
660
- * this is a no-op and the caller proceeds to normal dispatch.
661
- *
662
- * The `token` argument is the processor lambda's 2nd BullMQ argument
663
- * (verified against node_modules/bullmq/dist/esm/classes/job.d.ts:444
664
- * bullmq@5.63.1 — moveToDelayed(timestamp, token?) — passing undefined
665
- * is accepted but may fail under lock contention with renewLock).
666
- */
667
- declare function deferIfDisabled(job: Job, jobConfigs: Record<string, JobConfig>, jobConfigsRepo: JobConfigsRepo, token: string | undefined): Promise<void>;
668
-
669
- declare function computeBucketStart(ts: Date, granularity: BucketGranularity): Date;
670
211
 
671
- declare function createRavenflowApp<TConfig extends RavenflowConfig>(config: TConfig, options: RavenflowAppOptions): RavenflowApp<TConfig>;
212
+ declare function createRavenflowWorker(config: RavenflowConfig, options: RavenflowWorkerOptions): RavenflowWorker;
672
213
 
673
- export { BatchWriter, type BatchWriterConfig, type BucketGranularity, DEFAULT_WORKER_METRICS, DISABLED_JOB_DEFER_DELAY_MS, type DistributedEtlJobConfig, type EtlExtractResult, type EtlResult, type EventHooksOptions, type IncrementStatsInput, type IndexDescriptor, JOB_FAILURES_INPUT_MAX_CHARS, JOB_FAILURES_LOGS_MAX_CHARS, JOB_FAILURES_MESSAGE_MAX_CHARS, JOB_FAILURES_STACK_MAX_CHARS, type JobConfig, type JobConfigDoc, type JobConfigEntry, JobConfigsRepo, type JobErrorDoc, JobErrorsRepo, type JobFailureDoc, JobFailuresRepo, type JobStatsDoc, JobStatsRepo, type JobStatus, type ListFailuresOptions, type Logger, type LoggerOptions, METRICS_TIME, type MemoryEtlJobConfig, type MemoryEtlStepConfig, type MongoConnectionObject, type RateLimitConfig, RavenflowApp, type RavenflowAppOptions, type RavenflowConfig, type RecordFailureInput, type RecordOccurrenceInput, type RegisterWorkerInput, Repository, type RetentionConfig, type SimpleJobConfig, type StatefulEtlJobConfig, type StepConfig, type TokenBucketInterval, type TokenBucketJobConfig, type TopErrorsOptions, type UpsertWorkerQueueInput, type WorkerDoc, type WorkerQueueEntry, WorkerRegistry, type WorkerRegistryOptions, WorkersRepo, attachEventHooks, captureJobLogs, computeBucketStart, createRavenflowApp, deferIfDisabled, defineConfig, defineDistributedEtlJob, defineMemoryEtlJob, defineSimpleJob, defineStatefulEtlJob, defineTokenBucketJob, errorSignature, getLogger, initLogger };
214
+ export { type JobDefinition, type RavenflowClient, type RavenflowConfig, type RavenflowWorker, type RavenflowWorkerOptions, createRavenflowClient, createRavenflowWorker, defineConfig, defineJob };