ravenflow 0.3.0 → 0.4.0

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,363 @@
1
- import { z } from 'zod';
2
- import { Job, Queue, QueueEvents, ConnectionOptions, Worker, WorkerOptions } from 'bullmq';
1
+ import { Job, ConnectionOptions, Queue } from 'bullmq';
3
2
  import pino from 'pino';
4
- import { IndexSpecification, CreateIndexesOptions, Document, Collection, Db, AnyBulkWriteOperation } from 'mongodb';
3
+ import { Db } from 'mongodb';
5
4
 
6
- interface BaseJobConfig {
7
- queue?: string;
8
- cron?: string;
9
- attempts?: number;
10
- backoff?: {
11
- type: "fixed" | "exponential";
12
- delay: number;
13
- };
14
- }
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;
5
+ declare const CONTINUATION: unique symbol;
6
+ interface Continuation<TPayload> {
7
+ readonly [CONTINUATION]: true;
8
+ readonly params: TPayload;
9
+ readonly delayMs?: number;
10
+ readonly deduplicationId?: string;
31
11
  }
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>;
12
+ interface ContinueOptions {
13
+ delayMs?: number;
14
+ deduplicationId?: string;
49
15
  }
50
- declare function defineSimpleJob<TData, TReturn>(config: Omit<SimpleJobConfig<TData, TReturn>, "type">): SimpleJobConfig<TData, TReturn>;
51
16
 
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
- };
62
- }
63
- declare function defineMemoryEtlJob<TRaw, TProcessed, TCursor = number>(config: Omit<MemoryEtlJobConfig<TRaw, TProcessed, TCursor>, "type">): MemoryEtlJobConfig<TRaw, TProcessed, TCursor>;
17
+ type Logger = pino.Logger;
18
+ interface LoggerOptions {
19
+ level?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
20
+ pretty?: boolean;
21
+ }
64
22
 
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>;
23
+ interface JobContext<TParams> {
24
+ params: TParams;
25
+ deduplicationId?: string;
26
+ continueWith(params: TParams, options?: ContinueOptions): Continuation<TParams>;
27
+ job: Job;
28
+ logger: Logger;
29
+ signal: AbortSignal;
30
+ }
77
31
 
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>;
32
+ interface BackoffPolicy {
33
+ type: "fixed" | "exponential";
34
+ delayMs: number;
35
+ jitter?: number;
36
+ }
37
+ type DeduplicatePolicy<TPayload> = true | ((params: TPayload) => string);
38
+ type PacePolicy = {
39
+ perSecond: number;
40
+ perMinute?: never;
41
+ perHour?: never;
42
+ } | {
43
+ perMinute: number;
44
+ perSecond?: never;
45
+ perHour?: never;
46
+ } | {
47
+ perHour: number;
48
+ perSecond?: never;
49
+ perMinute?: never;
50
+ };
98
51
 
99
- type TokenBucketInterval = "second" | "minute" | "hour" | "day" | "week";
100
- interface TokenBucketJobConfig extends BaseJobConfig {
101
- type: "token-bucket";
52
+ type JobTrigger = CronTrigger | SpoolTriggerDeclaration;
53
+ interface CronTrigger {
102
54
  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;
55
+ spool?: never;
56
+ }
57
+ interface SpoolTriggerDeclaration {
58
+ spool: SpoolTrigger;
59
+ cron?: never;
60
+ }
61
+ interface SpoolTrigger {
62
+ name: string;
63
+ tickMs?: number;
64
+ canPull?: () => boolean | Promise<boolean>;
65
+ expireUnclaimedAfterMs?: number;
66
+ }
67
+ interface DeclaredTrigger {
68
+ cron?: string;
69
+ spool?: SpoolTrigger;
70
+ }
113
71
 
114
- type JobConfig = SimpleJobConfig<any, any> | MemoryEtlJobConfig<any, any, any> | DistributedEtlJobConfig<any, any, any> | StatefulEtlJobConfig<any, any, any> | TokenBucketJobConfig;
72
+ type TriggerFor<TPayload> = SatisfiableByNothing<TPayload> extends true ? JobTrigger : SpoolTriggerDeclaration | CronRefused;
73
+ type CronTriggerFor<TPayload> = SatisfiableByNothing<TPayload> extends true ? CronTrigger : CronRefused;
74
+ type SatisfiableByNothing<TPayload> = AcceptsUndefined<TPayload> extends true ? true : AcceptsEmptyObject<TPayload>;
75
+ type AcceptsUndefined<TPayload> = undefined extends TPayload ? true : false;
76
+ type AcceptsEmptyObject<TPayload> = {} extends TPayload ? true : false;
77
+ interface CronRefused {
78
+ cron?: CronNeedsAPayloadNothingHasToProvide;
79
+ spool?: never;
80
+ }
81
+ type CronNeedsAPayloadNothingHasToProvide = "a cron trigger has nothing to pass this job: make every field of its payload optional, or declare the payload as void";
115
82
 
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;
83
+ interface JobDefinition<TPayload, TResult> {
84
+ queue?: string;
85
+ attempts?: number;
86
+ backoff?: BackoffPolicy;
87
+ trigger?: TriggerFor<TPayload>;
88
+ deduplicate?: DeduplicatePolicy<TPayload>;
89
+ pace?: PacePolicy;
90
+ handler: JobHandler<TPayload, TResult>;
91
+ }
92
+ type JobHandler<TPayload, TResult> = (ctx: JobContext<TPayload>) => Promise<TResult | Continuation<TPayload>>;
93
+ type SpoolFedJob<TPayload, TResult> = JobDefinition<TPayload, TResult> & {
94
+ trigger: SpoolTriggerDeclaration;
95
+ pace: PacePolicy;
96
+ };
97
+ type ClockFedJob<TPayload, TResult> = JobDefinition<TPayload, TResult> & {
98
+ trigger: CronTrigger;
99
+ };
100
+ declare function defineJob<TPayload, TResult>(definition: JobDefinitionWith<SpoolTriggerDeclaration, TPayload, TResult> & {
101
+ pace: PacePolicy;
102
+ }): SpoolFedJob<TPayload, TResult>;
103
+ declare function defineJob<TPayload, TResult>(definition: JobDefinitionWith<CronTriggerFor<TPayload>, TPayload, TResult>): ClockFedJob<TPayload, TResult>;
104
+ declare function defineJob<TPayload, TResult>(definition: UnspooledJobDefinition<TPayload, TResult>): JobDefinition<TPayload, TResult>;
105
+ type JobDefinitionWith<TTrigger, TPayload, TResult> = Omit<JobDefinition<TPayload, TResult>, "trigger"> & {
106
+ trigger: TTrigger;
107
+ };
108
+ type UnspooledJobDefinition<TPayload, TResult> = Omit<JobDefinition<TPayload, TResult>, "trigger"> & {
109
+ trigger?: CronTriggerFor<TPayload>;
110
+ };
111
+
112
+ interface AnyJob {
113
+ queue?: string;
114
+ attempts?: number;
115
+ backoff?: BackoffPolicy;
116
+ trigger?: DeclaredTrigger;
117
+ deduplicate?: DeduplicatePolicy<never>;
118
+ pace?: PacePolicy;
119
+ handler: (ctx: JobContext<never>) => Promise<unknown>;
121
120
  }
121
+
122
122
  interface RavenflowConfig<TJobs extends string = string> {
123
123
  defaultQueue: string;
124
- jobs: Record<TJobs, JobConfig>;
124
+ jobs: Record<TJobs, AnyJob>;
125
+ logger?: LoggerOptions;
126
+ queuesRetention?: QueuesRetentionConfig;
127
+ jobResultsRetentionHours?: number;
128
+ aggregatedResultsRetentionHours?: number;
129
+ }
130
+ interface QueuesRetentionConfig {
131
+ completedCount?: number;
132
+ failedCount?: number;
125
133
  }
126
- type JobNames<T extends RavenflowConfig> = T extends RavenflowConfig<infer TJobs> ? TJobs : never;
127
134
  declare function defineConfig<const T extends RavenflowConfig>(config: T): T;
128
135
 
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
-
154
- type Logger = pino.Logger;
155
- interface LoggerOptions {
156
- level?: "trace" | "debug" | "info" | "warn" | "error" | "fatal";
157
- pretty?: boolean;
136
+ declare class MongoConnection {
137
+ private readonly uri;
138
+ private client;
139
+ private database;
140
+ private connecting;
141
+ constructor(uri: string);
142
+ connect(): Promise<Db>;
143
+ close(): Promise<void>;
144
+ private openTheDatabase;
145
+ private forgetTheAttempt;
158
146
  }
159
- declare function initLogger(options?: LoggerOptions): Logger;
160
- declare function getLogger(): Logger;
161
147
 
162
- type QueueStore = Record<string, {
163
- queue: Queue;
164
- events: QueueEvents;
165
- }>;
148
+ type ControlKey = `job:${string}`;
166
149
 
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>;
150
+ interface ControlPublisher {
151
+ publish(key: ControlKey): Promise<void>;
152
+ }
153
+ declare class ControlChannel implements ControlPublisher {
154
+ private readonly connection;
155
+ private readonly logger;
156
+ private publisher;
157
+ private subscriber;
158
+ constructor(connection: ConnectionOptions, logger: Logger);
159
+ publish(key: ControlKey): Promise<void>;
160
+ subscribe(onInvalidate: (key: ControlKey) => void): Promise<void>;
161
+ close(): Promise<void>;
162
+ private deliver;
163
+ private listenOn;
164
+ private open;
178
165
  }
179
166
 
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;
253
- }
254
- interface ListFailuresOptions {
255
- jobType?: string;
256
- fromDate?: Date | null;
257
- toDate?: Date | null;
258
- skip?: number;
259
- limit?: number;
260
- }
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 {
266
- 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;
167
+ declare const LEVERS: readonly ["executionPaused", "triggerPaused", "ingestPaused"];
168
+ type Lever = (typeof LEVERS)[number];
169
+ type JobControls = Record<Lever, boolean>;
170
+
171
+ interface ControlReads {
172
+ read(jobName: string): Promise<JobControls>;
278
173
  }
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>;
303
- }
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;
174
+ interface JobControlPlane extends ControlReads {
175
+ write(jobName: string, patch: Partial<JobControls>): Promise<void>;
312
176
  }
313
177
 
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>;
178
+ interface LeverEntry {
179
+ key: ControlKey;
180
+ lever: Lever;
181
+ value: string;
182
+ }
183
+ interface StoredLever {
184
+ jobName: string;
185
+ lever: Lever;
186
+ value: string;
187
+ }
188
+ declare class ControlHash {
189
+ private readonly options;
190
+ private readonly logger;
191
+ private connection;
192
+ constructor(options: ConnectionOptions, logger: Logger);
193
+ readLevers(key: ControlKey): Promise<Map<Lever, string>>;
194
+ readEveryLever(): Promise<StoredLever[]>;
195
+ write(entry: LeverEntry): Promise<void>;
196
+ writeIfAbsent(entry: LeverEntry): Promise<boolean>;
197
+ close(): Promise<void>;
198
+ private get redis();
319
199
  }
320
200
 
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;
201
+ interface MirroredControls extends Partial<JobControls> {
202
+ _id: string;
203
+ updatedAt: Date;
341
204
  }
342
-
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;
205
+ interface ControlMirror {
206
+ mirrorJobControls(jobName: string, patch: Partial<JobControls>): Promise<void>;
207
+ listAll(): Promise<MirroredControls[]>;
379
208
  }
380
209
 
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[]>;
406
- }
210
+ type LiveControls = Map<string, JobControls>;
407
211
 
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;
212
+ interface RehydrationReport {
213
+ restored: number;
214
+ alreadyLive: number;
215
+ truncated: boolean;
429
216
  }
430
217
 
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;
218
+ interface ControlStoreParts {
219
+ hash: ControlHash;
220
+ publisher: ControlPublisher;
221
+ mirror: ControlMirror;
222
+ logger: Logger;
223
+ }
224
+ declare class ControlStore implements JobControlPlane {
225
+ private readonly parts;
226
+ constructor(parts: ControlStoreParts);
227
+ read(jobName: string): Promise<JobControls>;
228
+ write(jobName: string, patch: Partial<JobControls>): Promise<void>;
229
+ readAll(): Promise<LiveControls>;
230
+ rehydrate(): Promise<RehydrationReport>;
231
+ close(): Promise<void>;
232
+ private isPulled;
233
+ private writeLevers;
234
+ private announce;
235
+ private writeMirror;
466
236
  }
467
237
 
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;
478
- }
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;
238
+ declare class ControlPlane implements JobControlPlane {
239
+ private readonly parts;
240
+ private readonly cache;
241
+ constructor(parts: {
242
+ store: ControlStore;
243
+ channel: ControlChannel;
244
+ logger: Logger;
245
+ });
246
+ read(jobName: string): Promise<JobControls>;
247
+ write(jobName: string, patch: Partial<JobControls>): Promise<void>;
248
+ rehydrate(): Promise<void>;
249
+ close(): Promise<void>;
250
+ private closeQuietly;
498
251
  }
499
252
 
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;
253
+ type QueuesByName = Record<string, Queue>;
254
+
255
+ interface SpoolPurgeReport {
256
+ dropped: number;
257
+ inFlight: number;
505
258
  }
506
259
 
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;
260
+ type JobsProxy<TJobs extends Record<string, AnyJob>> = {
261
+ [K in keyof TJobs]: JobProxy<TJobs[K]>;
262
+ };
263
+ type JobProxy<T extends AnyJob> = T extends JobDefinition<infer TPayload, infer TResult> ? SendOrIngest<T, TPayload, TResult> & {
264
+ controls: ControlsOf<T>;
265
+ } : never;
266
+ type SendOrIngest<T extends AnyJob, TPayload, TResult> = T extends {
267
+ trigger: SpoolTriggerDeclaration;
268
+ } ? SpoolIngest<TPayload> : JobSend<TPayload, TResult>;
269
+ interface JobSend<TPayload, TResult> {
270
+ send(params: TPayload, options?: SendOptions): Promise<Job<TPayload, TResult>>;
271
+ }
272
+ interface SendOptions {
273
+ delayMs?: number;
274
+ priority?: number;
275
+ deduplicationId?: string;
276
+ }
277
+ interface SpoolIngest<TItem> {
278
+ push(item: TItem): Promise<string>;
279
+ pushMany(items: TItem[]): Promise<string[]>;
280
+ }
281
+ type ControlsOf<T extends AnyJob> = T extends {
282
+ trigger: SpoolTriggerDeclaration;
283
+ } ? SpoolFedControls : T extends {
284
+ trigger: CronTrigger;
285
+ } ? ClockFedControls : ExecutionControls;
286
+ interface ExecutionControls {
287
+ pauseExecution(): Promise<void>;
288
+ resumeExecution(): Promise<void>;
289
+ }
290
+ interface TriggerControls {
291
+ pauseTrigger(): Promise<void>;
292
+ resumeTrigger(): Promise<void>;
293
+ }
294
+ interface IngestControls {
295
+ pauseIngest(): Promise<void>;
296
+ resumeIngest(): Promise<void>;
297
+ }
298
+ interface ClockFedControls extends ExecutionControls, TriggerControls {
299
+ }
300
+ interface SpoolFedControls extends ExecutionControls, TriggerControls, IngestControls {
301
+ purgeBacklog(): Promise<SpoolPurgeReport>;
526
302
  }
527
303
 
528
- interface MongoConnectionObject {
529
- host: string;
530
- port?: number;
531
- username?: string;
532
- password?: string;
533
- database?: string;
534
- }
535
- interface RavenflowAppOptions {
536
- 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>;
304
+ declare class RavenflowClient<TConfig extends RavenflowConfig = RavenflowConfig> {
305
+ readonly jobs: JobsProxy<TConfig["jobs"]>;
306
+ protected readonly redisConnectionOptions: ConnectionOptions;
307
+ protected readonly mongoUri: string;
308
+ protected readonly mongo: MongoConnection;
309
+ protected readonly config: TConfig;
310
+ protected readonly controlPlane: ControlPlane;
311
+ protected readonly queues: QueuesByName;
312
+ protected readonly logger: Logger;
313
+ constructor(config: TConfig, options: {
314
+ redis: ConnectionOptions;
315
+ mongo: string;
316
+ });
579
317
  close(): Promise<void>;
580
318
  }
581
319
 
582
- declare function errorSignature(err: unknown): string;
583
-
584
- declare function captureJobLogs(queue: Queue, jobId: string, maxChars: number): Promise<string>;
320
+ declare function createRavenflowClient<TConfig extends RavenflowConfig>(config: TConfig, options: ConstructorParameters<typeof RavenflowClient>[1]): RavenflowClient<TConfig>;
585
321
 
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;
596
- }
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>;
322
+ interface RavenflowWorkerOptions {
323
+ redis: ConnectionOptions;
324
+ mongo: string;
325
+ queue: string;
326
+ concurrency?: number;
327
+ withdrawal?: WithdrawalOptions;
328
+ }
329
+ interface WithdrawalOptions {
330
+ afterMs?: number;
331
+ onWithdraw?: (reason: string) => void | Promise<void>;
332
+ }
668
333
 
669
- declare function computeBucketStart(ts: Date, granularity: BucketGranularity): Date;
334
+ declare class RavenflowWorker {
335
+ private readonly config;
336
+ private readonly options;
337
+ private readonly jobs;
338
+ private readonly logger;
339
+ private serving;
340
+ private starting;
341
+ private closing;
342
+ constructor(config: RavenflowConfig, options: RavenflowWorkerOptions);
343
+ start(): Promise<void>;
344
+ close(): Promise<void>;
345
+ private serveOrStop;
346
+ private stopServing;
347
+ private refuseASecondStart;
348
+ private openServing;
349
+ private buildControlPlaneReader;
350
+ private buildJobResultsRecorder;
351
+ private buildJobResultsAggregator;
352
+ private buildSpoolFeeder;
353
+ private buildMongoWatch;
354
+ private beginAWithdrawal;
355
+ private serve;
356
+ private openRecorder;
357
+ private sayJobResultsAreNotRecorded;
358
+ private takeJobs;
359
+ }
670
360
 
671
- declare function createRavenflowApp<TConfig extends RavenflowConfig>(config: TConfig, options: RavenflowAppOptions): RavenflowApp<TConfig>;
361
+ declare function createRavenflowWorker(config: RavenflowConfig, options: RavenflowWorkerOptions): RavenflowWorker;
672
362
 
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 };
363
+ export { type JobDefinition, RavenflowClient, type RavenflowConfig, RavenflowWorker, type RavenflowWorkerOptions, createRavenflowClient, createRavenflowWorker, defineConfig, defineJob };