@rvoh/psychic-workers 2.6.0 → 2.8.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.
Files changed (36) hide show
  1. package/dist/cjs/src/background/BaseBackgroundedModel.js +49 -25
  2. package/dist/cjs/src/background/BaseBackgroundedService.js +24 -12
  3. package/dist/cjs/src/background/BaseScheduledService.js +1 -1
  4. package/dist/cjs/src/background/index.js +383 -98
  5. package/dist/cjs/src/error/background/DebouncedJobOutpacesRateLimit.js +64 -0
  6. package/dist/cjs/src/error/background/DebouncedJobRequiresMinimumDelay.js +52 -0
  7. package/dist/cjs/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.js +31 -0
  8. package/dist/cjs/src/error/background/RateLimitedPsychicJob.js +82 -0
  9. package/dist/cjs/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.js +51 -0
  10. package/dist/cjs/src/package-exports/errors.js +7 -0
  11. package/dist/cjs/src/test-utils/WorkerTestUtils.js +78 -13
  12. package/dist/esm/src/background/BaseBackgroundedModel.js +49 -25
  13. package/dist/esm/src/background/BaseBackgroundedService.js +24 -12
  14. package/dist/esm/src/background/BaseScheduledService.js +1 -1
  15. package/dist/esm/src/background/index.js +383 -98
  16. package/dist/esm/src/error/background/DebouncedJobOutpacesRateLimit.js +64 -0
  17. package/dist/esm/src/error/background/DebouncedJobRequiresMinimumDelay.js +52 -0
  18. package/dist/esm/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.js +31 -0
  19. package/dist/esm/src/error/background/RateLimitedPsychicJob.js +82 -0
  20. package/dist/esm/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.js +51 -0
  21. package/dist/esm/src/package-exports/errors.js +7 -0
  22. package/dist/esm/src/test-utils/WorkerTestUtils.js +78 -13
  23. package/dist/types/src/background/BaseBackgroundedModel.d.ts +49 -25
  24. package/dist/types/src/background/BaseBackgroundedService.d.ts +24 -12
  25. package/dist/types/src/background/BaseScheduledService.d.ts +2 -2
  26. package/dist/types/src/background/index.d.ts +109 -5
  27. package/dist/types/src/error/background/DebouncedJobOutpacesRateLimit.d.ts +31 -0
  28. package/dist/types/src/error/background/DebouncedJobRequiresMinimumDelay.d.ts +23 -0
  29. package/dist/types/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.d.ts +16 -0
  30. package/dist/types/src/error/background/RateLimitedPsychicJob.d.ts +73 -0
  31. package/dist/types/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.d.ts +16 -0
  32. package/dist/types/src/package-exports/errors.d.ts +1 -0
  33. package/dist/types/src/psychic-app-workers/index.d.ts +102 -53
  34. package/dist/types/src/test-utils/WorkerTestUtils.d.ts +32 -1
  35. package/dist/types/src/types/background.d.ts +88 -6
  36. package/package.json +28 -25
@@ -11,7 +11,7 @@ export default class BaseBackgroundedService {
11
11
  * @returns {object} config - the background job config
12
12
  * @returns {string} config.priority - 'default' | 'urgent' | 'not_urgent' | 'last'
13
13
  * @returns {string} config.workstream - a workstream name. This would be the name of a workstream, as defined in conf/workers.ts
14
- * @returns {string} config.queueId - the id of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
14
+ * @returns {string} config.queue - the name of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
15
15
  * @returns {string} config.groupId - the groupId of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
16
16
  */
17
17
  static get backgroundJobConfig(): BackgroundJobConfig<BaseBackgroundedService>;
@@ -53,16 +53,22 @@ export default class BaseBackgroundedService {
53
53
  static background<T, MethodName extends PsychicBackgroundedServiceStaticMethods<T & typeof BaseBackgroundedService>, MethodFunc extends T[MethodName & keyof T], MethodArgs extends BackgroundableMethodArgs<MethodFunc>>(this: T, methodName: MethodName, ...args: MethodArgs): Promise<void>;
54
54
  /**
55
55
  * runs the specified method in a background queue, driven by BullMQ,
56
- * sending in the provided args, including a delay in seconds, which
57
- * can be used to hold off the job for a certain amount of time after
58
- * it is entered into the queue.
56
+ * sending in the provided args, along with a delay object — one or more of
57
+ * `{ seconds, minutes, hours, days }` — which holds the job off for that much
58
+ * time after it is entered into the queue.
59
+ *
60
+ * The delay object also accepts an optional `jobId`, which turns the delay
61
+ * into a **debounce**: repeated calls carrying the same `jobId` collapse into
62
+ * a single execution, which runs once the delay has elapsed without another
63
+ * call arriving. `jobId` is a deduplication key rather than a BullMQ job id.
64
+ *
65
+ * See {@link DelayedJobOpts.jobId}.
59
66
  *
60
67
  * ```ts
61
- * await MyBackgroundableClass.backgroundWithDelay('myMethod', 'abc', 123)
68
+ * await MyBackgroundableClass.backgroundWithDelay({ minutes: 5 }, 'myMethod', 'abc', 123)
62
69
  * ```
63
- * though calling background must be awaited, the resolution of the promise
64
- * is an indication that the job was put in the queue, not that it has
65
- * completed.
70
+ * though calling backgroundWithDelay must be awaited, the resolution of the promise
71
+ * is an indication that a run is pending, not that it has completed.
66
72
  *
67
73
  * NOTE: in test environments, psychic will immediately invoke the underlying
68
74
  * method, preventing you from needing to explicitly wait for queues to flush
@@ -70,7 +76,7 @@ export default class BaseBackgroundedService {
70
76
  *
71
77
  * @deprecated use `backgroundWith({ delay }, methodName, ...args)` instead. This method will be removed in a future major version.
72
78
  *
73
- * @param delaySeconds - the amount of time (in seconds) you want to hold off before allowing the job to run
79
+ * @param delay - how long you want to hold off before allowing the job to run, given as at least one of `seconds`, `minutes`, `hours` or `days`, plus an optional `jobId` (a deduplication key) which debounces repeated calls into a single run
74
80
  * @param methodName - the name of the static method you wish to run in the background
75
81
  * @param args - a variadic list of arguments to be sent to your method
76
82
  */
@@ -81,6 +87,13 @@ export default class BaseBackgroundedService {
81
87
  * be used to delay the job and/or override the priority provided by
82
88
  * the `backgroundJobConfig` getter on this service.
83
89
  *
90
+ * Adding a `jobId` to the delay turns it into a **debounce**: repeated calls
91
+ * carrying the same `jobId` collapse into a single execution, which runs once
92
+ * the delay has elapsed without another call arriving. `jobId` is a
93
+ * deduplication key rather than a BullMQ job id.
94
+ *
95
+ * See {@link DelayedJobOpts.jobId}.
96
+ *
84
97
  * ```ts
85
98
  * await MyBackgroundableClass.backgroundWith(
86
99
  * { delay: { seconds: 30, jobId: 'my-unique-job-id' }, priority: 'urgent' },
@@ -90,15 +103,14 @@ export default class BaseBackgroundedService {
90
103
  * )
91
104
  * ```
92
105
  * though calling backgroundWith must be awaited, the resolution of the promise
93
- * is an indication that the job was put in the queue, not that it has
94
- * completed.
106
+ * is an indication that a run is pending, not that it has completed.
95
107
  *
96
108
  * NOTE: in test environments, psychic will immediately invoke the underlying
97
109
  * method, preventing you from needing to explicitly wait for queues to flush
98
110
  * before making assertions.
99
111
  *
100
112
  * @param opts - options for backgrounding this job
101
- * @param opts.delay - (optional) the amount of time you want to hold off before allowing the job to run, and an optional `jobId` which debounces repeated calls within the delay window
113
+ * @param opts.delay - (optional) how long you want to hold off before allowing the job to run, given as at least one of `seconds`, `minutes`, `hours` or `days`, plus an optional `jobId` (a deduplication key) which debounces repeated calls into a single run
102
114
  * @param opts.priority - (optional) a priority which, when provided, overrides the priority provided by `backgroundJobConfig`
103
115
  * @param methodName - the name of the static method you wish to run in the background
104
116
  * @param args - a variadic list of arguments to be sent to your method
@@ -10,7 +10,7 @@ export default class BaseScheduledService {
10
10
  * @returns {object} config - the background job config
11
11
  * @returns {string} config.priority - 'default' | 'urgent' | 'not_urgent' | 'last'
12
12
  * @returns {string} config.workstream - a workstream name. This would be the name of a workstream, as defined in conf/workers.ts
13
- * @returns {string} config.queueId - the id of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
13
+ * @returns {string} config.queue - the name of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
14
14
  * @returns {string} config.groupId - the groupId of the BullMQ queue you wish to connect to. This can only be provided if workstream is not provided.
15
15
  */
16
16
  static get backgroundJobConfig(): BackgroundJobConfig<BaseScheduledService>;
@@ -50,7 +50,7 @@ export default class BaseScheduledService {
50
50
  * @param methodName - the name of the static method you wish to run in the background
51
51
  * @param args - a variadic list of arguments to be sent to your method
52
52
  */
53
- static schedule<T, MethodName extends FunctionPropertyNames<Required<T>>, MethodFunc extends T[MethodName & keyof T], MethodArgs extends MethodFunc extends (...args: any) => any ? Parameters<MethodFunc> : never>(this: T, pattern: string, methodName: MethodName, ...args: MethodArgs): Promise<void>;
53
+ static schedule<T, MethodName extends FunctionPropertyNames<Required<T>>, MethodFunc extends T[MethodName & keyof T], MethodArgs extends (MethodFunc extends (...args: any) => any ? Parameters<MethodFunc> : never)>(this: T, pattern: string, methodName: MethodName, ...args: MethodArgs): Promise<void>;
54
54
  /**
55
55
  * Returns the id that `schedule` registers a job under, which can be passed
56
56
  * to {@link BaseScheduledService.unschedule} to remove that job.
@@ -7,7 +7,13 @@ import { BackgroundJobConfig, BackgroundJobData, BackgroundQueuePriority, JobTyp
7
7
  */
8
8
  export declare class Background {
9
9
  /**
10
- * returns the default queue name for your app
10
+ * returns the **logical** name of your app's default queue, built from your
11
+ * app name (`MyAppBackgroundJobQueue`).
12
+ *
13
+ * This is never the name the queue has in Redis: `connect` rewrites it per
14
+ * connection (brace-stripped, cluster hash-tagged, test-suffixed), and every
15
+ * key BullMQ writes is namespaced by the rewritten name. Do not compose Redis
16
+ * keys from this — read `queue.name` off `background.queues` instead.
11
17
  */
12
18
  static get defaultQueueName(): string;
13
19
  /**
@@ -75,17 +81,61 @@ export declare class Background {
75
81
  */
76
82
  private redisConnections;
77
83
  /**
78
- * Establishes connection to BullMQ via redis
84
+ * @internal
85
+ *
86
+ * For every queue built by `connect`: how it was configured, whether its
87
+ * workers carry a BullMQ `limiter`, and what the workers are built from.
88
+ * Written whether or not workers are activated here, so activation can happen
89
+ * later without re-walking the configuration. Keyed by Queue identity rather
90
+ * than name: a transitional workstream's queue has the same formatted name as
91
+ * its current twin but its own configuration.
92
+ */
93
+ private queueWorkerRecords;
94
+ /**
95
+ * @internal
96
+ *
97
+ * set immediately before {@link buildRecordedWorkers} runs its build loop, so
98
+ * that a second activation builds nothing
99
+ */
100
+ private workersActivated;
101
+ /**
102
+ * Establishes connection to BullMQ via redis: builds the `Queue` objects for
103
+ * the default and named workstreams and, only when `activateWorkers` is true,
104
+ * the `Worker` objects that run jobs off them. Synchronous: it returns
105
+ * nothing, so it is called without `await` even from async code.
106
+ *
107
+ * Connecting and activating are separate. `activateWorkers` defaults to
108
+ * `false`, so connecting gets the producer side only. Queues are built once
109
+ * per instance, but a later activation still builds workers, so a process that
110
+ * connects first and calls `work()` afterwards gets its workers.
111
+ *
112
+ * You rarely call this yourself — Psychic connects on the
113
+ * `server:init:after-routes` hook, on every enqueue path, in CLI codegen and
114
+ * in the `WorkerTestUtils` helpers.
79
115
  */
80
116
  connect({ activateWorkers, }?: {
81
117
  activateWorkers?: boolean;
82
118
  }): void;
83
119
  /**
84
- * Returns all the queues in your application
120
+ * Returns all the queues in your application: the default queue, every named
121
+ * queue, and their transitional twins when transitional workstreams are
122
+ * configured.
123
+ *
124
+ * `connect` is what populates this, and before it has run the getter returns
125
+ * an **empty array**, with no error and no warning — indistinguishable from
126
+ * an application with nothing queued. Each `Queue` here carries the rewritten
127
+ * Redis name rather than the name you configured.
85
128
  */
86
129
  get queues(): Queue[];
87
130
  /**
88
- * Returns all the workers in your application
131
+ * Returns all the workers in your application: every BullMQ `Worker` this
132
+ * process has built.
133
+ *
134
+ * Only a process that activated workers has any — `work()`, or a direct
135
+ * `connect({ activateWorkers: true })`. Anywhere else the getter returns an
136
+ * **empty array**, with no error and no warning, so a worker entrypoint that
137
+ * attaches failure listeners by looping over it before `work()` runs attaches
138
+ * nothing and logs nothing to say so.
89
139
  */
90
140
  get workers(): Worker<any, any, string>[];
91
141
  /**
@@ -141,7 +191,61 @@ export declare class Background {
141
191
  */
142
192
  private nativeBullMQConnect;
143
193
  /**
144
- * starts background workers
194
+ * @internal
195
+ *
196
+ * the sole writer of {@link queueWorkerRecords}. `workerOptions` is the very
197
+ * object the workers are built from (minus the connection), so the record and
198
+ * the workers cannot disagree.
199
+ */
200
+ private recordQueueWorkers;
201
+ /**
202
+ * @internal
203
+ *
204
+ * builds every worker `connect` recorded a queue for, in the order the queues
205
+ * were built, so the default missing-connection throw still pre-empts the
206
+ * named one. The activation flag is set before the loop, not after it, because
207
+ * `_workers` is append-only and a build that throws partway must not be
208
+ * retryable into a second copy of the workers it already constructed.
209
+ */
210
+ private buildRecordedWorkers;
211
+ /**
212
+ * @internal
213
+ *
214
+ * throws {@link NamedWorkstreamRateLimitMissingMaxOrDuration} for the first
215
+ * workstream whose `rateLimit` lacks a positive integer `max` or `duration`.
216
+ * The type requires both but admits any number, and open-source BullMQ does
217
+ * not validate a `limiter`: a fractional `duration` floors to 0ms and rate
218
+ * limits nothing, and one past Redis's integer range fails every job fetch.
219
+ */
220
+ private assertNamedWorkstreamRateLimits;
221
+ /**
222
+ * @internal
223
+ *
224
+ * when `err` is a {@link RateLimitedPsychicJob} thrown on a queue whose workers
225
+ * carry no BullMQ `limiter`, returns the misconfiguration error to fail the job
226
+ * with instead; otherwise undefined. Declared as `Error` so the concrete,
227
+ * unexported class stays out of the emitted declarations. Four callers, one
228
+ * across a module boundary in `WorkerTestUtils`.
229
+ */
230
+ misconfiguredRateLimitSignal(err: unknown, queue: Queue): Error | undefined;
231
+ /**
232
+ * @internal
233
+ *
234
+ * the processor every worker on `queue` runs: {@link doWork}, with a
235
+ * {@link RateLimitedPsychicJob} translated into BullMQ's own rate-limit
236
+ * signal — the queue's limiter key is set, pausing every worker on the queue,
237
+ * then `RateLimitError` moves the job back without counting an attempt or
238
+ * emitting `failed`. Thrown where the workers carry no `limiter`, it is
239
+ * instead a misconfiguration: see {@link misconfiguredRateLimitSignal}.
240
+ */
241
+ private processorFor;
242
+ /**
243
+ * starts background workers: connects if this process has not connected yet,
244
+ * then builds the `Worker` objects for every queue `connect` recorded, and
245
+ * installs the process-level fatal-error and signal handlers that shut them
246
+ * down. An earlier producer-only `connect()` no longer leaves `work()` with
247
+ * nothing to do. A process with no `defaultWorkerConnection` configured throws
248
+ * here rather than returning quietly.
145
249
  */
146
250
  work(): void;
147
251
  /**
@@ -0,0 +1,31 @@
1
+ import { WorkerQueueDescription } from '../../types/background.js';
2
+ /**
3
+ * Not exported from the package: a delayed background job carried a `jobId`
4
+ * behind a delay short enough that it can enqueue work faster than its own
5
+ * queue's rate limit allows that work to start. Thrown from `_addToQueue`,
6
+ * above the test-mode short circuit, so a consumer's test environment raises it
7
+ * exactly as production does.
8
+ */
9
+ export default class DebouncedJobOutpacesRateLimit extends Error {
10
+ private jobId;
11
+ private delayMs;
12
+ private keyLifetimeMs;
13
+ private limiter;
14
+ private queue;
15
+ constructor(jobId: string, delayMs: number, keyLifetimeMs: number, limiter: {
16
+ max: number;
17
+ duration: number;
18
+ }, queue: WorkerQueueDescription);
19
+ get message(): string;
20
+ private get msPerJob();
21
+ /**
22
+ * the shortest delay that would pass: enough that the key outlives the
23
+ * limiter's spacing. Rounded up to a whole second, since the delay is given
24
+ * in whole-ish duration fields and a value that still failed by a fraction
25
+ * of a second would be a hostile suggestion.
26
+ */
27
+ private get minimumDelayMs();
28
+ private seconds;
29
+ private get kind();
30
+ private get where();
31
+ }
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Not exported from the package, and deliberately so: every branch of the guard
3
+ * that throws it is a call-site mistake — a `jobId` behind a delay under the
4
+ * floor, an empty `jobId`, or a delay that is zero, negative, `Infinity`, `NaN`
5
+ * or past `Number.MAX_SAFE_INTEGER` — so an application can never usefully
6
+ * catch it. The fix is always an edit to the call.
7
+ *
8
+ * `durationToSeconds` catches none of these: it validates nothing, and its
9
+ * falsy-field sum silently drops a `NaN` field rather than propagating it
10
+ * (`{ hours: 1, seconds: NaN }` is `3600`), so `{ seconds: NaN }` reaches this
11
+ * check as `0`.
12
+ *
13
+ * The check runs before the test-mode short circuit in `_addToQueue`, so a
14
+ * consumer's default test environment raises this exactly as production does.
15
+ */
16
+ export default class DebouncedJobRequiresMinimumDelay extends Error {
17
+ private jobId;
18
+ private delaySeconds;
19
+ private minimumDelayMs;
20
+ constructor(jobId: string, delaySeconds: number | undefined, minimumDelayMs: number);
21
+ get message(): string;
22
+ private get emptyJobIdMessage();
23
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Not exported from the package: a named workstream's `rateLimit` reached
3
+ * `connect()` without a positive integer `max` or `duration`. Open-source
4
+ * BullMQ forwards the worker `limiter` it becomes unvalidated: a missing or
5
+ * non-numeric field fails every job fetch on that workstream with a Lua error
6
+ * that never names `rateLimit`, a fractional `duration` floors to 0ms and rate
7
+ * limits nothing, and one past Redis's integer range fails every fetch.
8
+ */
9
+ export default class NamedWorkstreamRateLimitMissingMaxOrDuration extends Error {
10
+ private workstreamName;
11
+ private transitional;
12
+ private field;
13
+ private received;
14
+ constructor(workstreamName: string, transitional: boolean, field: 'max' | 'duration', received: unknown);
15
+ get message(): string;
16
+ }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * Throw from inside a backgrounded (or scheduled) method to signal that the
3
+ * outside service it talks to rate-limited the request (an HTTP 429, say) and
4
+ * asked it to stop sending for a while. `pauseQueueForSeconds` is how long that
5
+ * while is, in whole seconds. Importable from `@rvoh/psychic-workers/errors`.
6
+ *
7
+ * ```ts
8
+ * // conf/workers.ts — one named workstream per rate-limited service
9
+ * namedWorkstreams: [{ name: 'slack', rateLimit: { max: 1, duration: 1000 } }]
10
+ *
11
+ * // app/services/SlackNotifier.ts
12
+ * import { RateLimitedPsychicJob } from '@rvoh/psychic-workers/errors'
13
+ *
14
+ * export default class SlackNotifier extends ApplicationBackgroundedService {
15
+ * public static override get backgroundJobConfig() {
16
+ * return { workstream: 'slack' }
17
+ * }
18
+ *
19
+ * public static async post(channel: string, text: string) {
20
+ * const response = await fetch('https://slack.com/api/chat.postMessage', { ... })
21
+ *
22
+ * if (response.status === 429) {
23
+ * // Slack's Retry-After header is in seconds, and so is this field
24
+ * const seconds = Number(response.headers.get('retry-after') ?? 1)
25
+ * throw new RateLimitedPsychicJob({ pauseQueueForSeconds: seconds })
26
+ * }
27
+ * }
28
+ * }
29
+ * ```
30
+ *
31
+ * The worker running the job must carry a BullMQ `limiter` — a named
32
+ * workstream with `rateLimit`, a native `namedQueueWorkers` entry with
33
+ * `limiter`, `nativeBullMQ.defaultWorkerOptions.limiter`, or a global
34
+ * `defaultBullMQWorkerOptions.limiter`. Given one, psychic-workers translates
35
+ * the signal into BullMQ's own rate-limit mechanism: the queue is paused for
36
+ * that many seconds and the job goes back onto the queue with `attemptsMade`
37
+ * unchanged and no backoff, so the burst is not multiplied by the retry
38
+ * schedule. Note that this package writes a BullMQ priority on every job it
39
+ * enqueues, so a re-queued job reports `prioritized` rather than `waiting` —
40
+ * assertions on `getWaitingCount()` do not hold.
41
+ *
42
+ * The pause replaces the queue's current limiter window and counter — the
43
+ * configured `rateLimit` window and any earlier pause alike — so once
44
+ * `pauseQueueForSeconds` elapses up to `max` jobs may start at once, and a
45
+ * `pauseQueueForSeconds` shorter than the window's remaining time shortens the
46
+ * configured limit. Concurrent throws overwrite each other: last writer wins,
47
+ * not longest.
48
+ *
49
+ * `attemptsMade` never grows, but each re-fetch counts in BullMQ's
50
+ * `attemptsStarted`, and the worker option `maxStartedAttempts`
51
+ * (`defaultBullMQWorkerOptions: { maxStartedAttempts: 10 }`, say) is the only
52
+ * bound on that cycle: without it it is unbounded. Set it on any workstream
53
+ * whose jobs throw this signal.
54
+ *
55
+ * Thrown from a job whose worker carries no `limiter`, the signal is a
56
+ * misconfiguration: the job fails with an ordinary error whose message names
57
+ * the fix. The same check runs under test invocation and `WorkerTestUtils`, so
58
+ * a misplaced job goes red in specs.
59
+ *
60
+ * `pauseQueueForSeconds` must be a positive, finite number of safe magnitude;
61
+ * the constructor throws a `RangeError` otherwise, at the throw site, so a
62
+ * nonsense value fails there rather than at the pause. A fractional number of
63
+ * seconds is legal and is rounded **up** — the field is a lower bound on the
64
+ * pause, so overshooting it by under a second cannot break the promise, while
65
+ * rounding down could.
66
+ */
67
+ export default class RateLimitedPsychicJob extends Error {
68
+ readonly pauseQueueForSeconds: number;
69
+ constructor({ pauseQueueForSeconds }: {
70
+ pauseQueueForSeconds: number;
71
+ });
72
+ get message(): string;
73
+ }
@@ -0,0 +1,16 @@
1
+ import { WorkerQueueDescription } from '../../types/background.js';
2
+ import RateLimitedPsychicJob from './RateLimitedPsychicJob.js';
3
+ /**
4
+ * Not exported from the package: a `RateLimitedPsychicJob` was thrown from a
5
+ * job whose worker carries no BullMQ `limiter`, so the queue cannot be paused.
6
+ * The job runner fails the job with this in the signal's place, so the failure
7
+ * reaches the application's failed-job monitoring.
8
+ */
9
+ export default class RateLimitedPsychicJobThrownFromWorkerWithoutLimiter extends Error {
10
+ private signal;
11
+ private queue;
12
+ constructor(signal: RateLimitedPsychicJob, queue: WorkerQueueDescription);
13
+ get message(): string;
14
+ private get where();
15
+ private get fix();
16
+ }
@@ -1,3 +1,4 @@
1
1
  export { default as NoClassForSpecifiedGlobalName } from '../error/background/NoClassForSpecifiedGlobalName.js';
2
2
  export { default as NoQueueForSpecifiedQueueName } from '../error/background/NoQueueForSpecifiedQueueName.js';
3
3
  export { default as NoQueueForSpecifiedWorkstream } from '../error/background/NoQueueForSpecifiedWorkstream.js';
4
+ export { default as RateLimitedPsychicJob } from '../error/background/RateLimitedPsychicJob.js';
@@ -64,7 +64,25 @@ export interface BullMQNativeWorkerOptions extends WorkerOptions {
64
64
  concurrency?: number;
65
65
  priority?: number;
66
66
  };
67
+ /**
68
+ * How many jobs each worker built from this configuration runs at once
69
+ * (https://docs.bullmq.io/guide/workers/concurrency).
70
+ *
71
+ * Native BullMQ mode writes none, so BullMQ's default of **1** applies unless
72
+ * this or `defaultBullMQWorkerOptions.concurrency` sets it. See
73
+ * `PsychicBackgroundWorkstreamOptions.concurrency`.
74
+ */
67
75
  concurrency?: number;
76
+ /**
77
+ * the number of workers to create with this configuration, in this process.
78
+ *
79
+ * Read **only** for `nativeBullMQ.namedQueueWorkers`, where it defaults to 1
80
+ * (a named queue with no entry in `namedQueueWorkers` gets no workers at
81
+ * all). It is *not* read for `nativeBullMQ.defaultWorkerOptions`: the default
82
+ * queue's worker count comes from `nativeBullMQ.defaultWorkerCount`. See
83
+ * `PsychicBackgroundWorkstreamOptions.workerCount` for the
84
+ * `workerCount × concurrency` product.
85
+ */
68
86
  workerCount?: number;
69
87
  }
70
88
  export interface PsychicBackgroundNativeBullMQOptions extends PsychicBackgroundSharedOptions {
@@ -82,26 +100,24 @@ export interface PsychicBackgroundNativeBullMQOptions extends PsychicBackgroundS
82
100
  */
83
101
  namedQueueOptions?: Record<string, QueueOptionsWithConnectionInstance>;
84
102
  /**
85
- * Native BullMQ options to provide to configure the default workers
86
- * for psychic. By default, Psychic leverages a single-queue system, with
87
- * many workers running off the queue. Each worker receives the
88
- * same worker configuration, so this configuration is really
89
- * only used to supply the number of default workers that you want.
103
+ * Native BullMQ options for the default workers, spread over
104
+ * `defaultBullMQWorkerOptions` — where `concurrency`, `limiter` and every
105
+ * other BullMQ worker option for them comes from. A `workerCount` set here
106
+ * is never read; the count comes from `defaultWorkerCount` below.
90
107
  */
91
108
  defaultWorkerOptions?: BullMQNativeWorkerOptions;
92
109
  /**
93
- * The number of default workers to run against the default Psychic
94
- * background queues.
95
- *
96
- * By default, Psychic leverages a single-queue system, with
97
- * many workers running off a single queue. This number determines
98
- * the number of those default workers to provide.
110
+ * The number of default workers to run in this process, and the only source
111
+ * of that count in native mode. Defaults to 1, and takes effect only in a
112
+ * process that activates workers. See
113
+ * `PsychicBackgroundWorkstreamOptions.workerCount`.
99
114
  */
100
115
  defaultWorkerCount?: number;
101
116
  /**
102
- * namedQueueWorkers are necessary to work off namedQueues
103
- * With BullMQ Pro, namedQueueWorkers can be rate limited (useful
104
- * for interacting with external APIs)
117
+ * namedQueueWorkers are necessary to work off namedQueues.
118
+ * A named queue's workers can be rate limited with BullMQ's `limiter`
119
+ * option (https://docs.bullmq.io/guide/rate-limiting; useful for
120
+ * interacting with external APIs) on open-source BullMQ and BullMQ Pro alike
105
121
  */
106
122
  namedQueueWorkers?: Record<string, BullMQNativeWorkerOptions>;
107
123
  };
@@ -138,24 +154,34 @@ export interface PsychicBackgroundSimpleOptions extends PsychicBackgroundSharedO
138
154
  */
139
155
  defaultWorkerConnection: RedisOrRedisClusterConnection | undefined;
140
156
  /**
141
- * Every Psychic application that leverages simple background jobs will have a default
142
- * workstream. Set workerCount to set the number of workers that will work through the
143
- * default queue
157
+ * The default workstream, whose queue carries every job not backgrounded to a
158
+ * named workstream. Both fields mean exactly what they mean on a named
159
+ * workstream — see `PsychicBackgroundWorkstreamOptions.workerCount` and
160
+ * `.concurrency`.
144
161
  */
145
162
  defaultWorkstream?: {
163
+ /** The number of workers working through the default queue, in this process. Defaults to 1. */
146
164
  workerCount?: number;
165
+ /**
166
+ * How many jobs each default worker runs at once
167
+ * (https://docs.bullmq.io/guide/workers/concurrency). Left unset, psychic
168
+ * falls back to 10, as everywhere in simple mode. See
169
+ * `PsychicBackgroundWorkstreamOptions.concurrency`.
170
+ */
147
171
  concurrency?: number;
148
172
  };
149
173
  /**
150
174
  * When running background jobs on BullMQ, each named workstream corresponds
151
- * to a specific queue and workers created for a named workstream are given
152
- * a groupId corresponding to the workstream name
153
- *
154
- * named workstreams are useful for dispersing queues among nodes in a Redis cluster
155
- * and for running queues on different Redis instances
175
+ * to a specific queue, and jobs backgrounded to a named workstream are added
176
+ * with a group id equal to the workstream name (a BullMQ Pro concept, ignored
177
+ * by open-source BullMQ)
156
178
  *
157
- * With BullMQ Pro, named workstreams can be rate limited (useful
158
- * for interacting with external APIs)
179
+ * named workstreams are useful for dispersing queues among nodes in a Redis cluster,
180
+ * for running queues on different Redis instances, and for rate limiting, on
181
+ * open-source BullMQ and BullMQ Pro alike (see
182
+ * `PsychicBackgroundWorkstreamOptions.rateLimit`). A rate limit targets one external
183
+ * limit, so give each rate-limited service its own named workstream — and where a
184
+ * service meters its endpoints separately, one workstream per limit
159
185
  */
160
186
  namedWorkstreams?: PsychicBackgroundWorkstreamOptions[];
161
187
  /**
@@ -171,43 +197,66 @@ export interface PsychicBackgroundWorkstreamOptions {
171
197
  */
172
198
  name: string;
173
199
  /**
174
- * The number of workers you want to run on this configuration
200
+ * The number of workers you want to run on this configuration, in **this
201
+ * process**. Defaults to 1. These are BullMQ `Worker` objects sharing the
202
+ * activating process's event loop, not forked processes, so raising this buys
203
+ * concurrent waiting rather than concurrent CPU.
204
+ *
205
+ * `workerCount × concurrency` is the ceiling on this workstream's jobs in
206
+ * flight in one process, and that product is what to size against a
207
+ * downstream limit such as a connection pool or an external API's quota.
175
208
  */
176
209
  workerCount?: number;
177
- concurrency?: number;
178
- /**
179
- * See https://docs.bullmq.io/bullmq-pro/groups/rate-limiting for documentation
180
- * on rate limiting in BullMQ Pro (requires paid BullMQ Pro license)
181
- */
182
- rateLimit?: {
183
- max?: number;
184
- duration?: number;
185
- };
186
- /**
187
- * Optional redis connection. If not provided, the default background redis connection will be used.
188
- * See https://docs.bullmq.io/guide/going-to-production for the different settings to use between
189
- * queue and worker connections.
190
- */
191
- queueConnection?: RedisOrRedisClusterConnection;
192
- workerConnection?: RedisOrRedisClusterConnection;
193
- }
194
- export interface PsychicBackgroundWorkstreamOptions {
195
- /**
196
- * This will be the name of the queue (and the group if using BullMQ Pro)
197
- */
198
- name: string;
199
210
  /**
200
- * The number of workers you want to run on this configuration
211
+ * How many jobs each of this workstream's workers runs at once
212
+ * (https://docs.bullmq.io/guide/workers/concurrency).
213
+ *
214
+ * **In simple mode this defaults to 10.** Psychic always writes a
215
+ * `concurrency` onto a simple-mode worker, falling back to 10 when the
216
+ * workstream does not set one — which also means it overrides any
217
+ * `concurrency` placed in `defaultBullMQWorkerOptions`. Native BullMQ mode
218
+ * writes none, so BullMQ's own default of 1 applies there. Moving a queue
219
+ * between the two modes therefore changes how many jobs it runs at once by a
220
+ * factor of ten, with nothing in either configuration saying so.
221
+ *
222
+ * See `workerCount` above for the `workerCount × concurrency` product.
201
223
  */
202
- workerCount?: number;
203
224
  concurrency?: number;
204
225
  /**
205
- * See https://docs.bullmq.io/bullmq-pro/groups/rate-limiting for documentation
206
- * on rate limiting in BullMQ Pro (requires paid BullMQ Pro license)
226
+ * Rate-limit this workstream: at most `max` jobs start in any `duration`
227
+ * milliseconds. Works on open-source BullMQ and on BullMQ Pro.
228
+ *
229
+ * The limit is per queue, not per worker: every one of this workstream's
230
+ * workers, in every process running it, shares one counter kept in Redis, so
231
+ * `{ max: 1, duration: 1000 }` means one job per second for the workstream as
232
+ * a whole. `concurrency` caps how many jobs each worker runs at once;
233
+ * `rateLimit` caps how many may start per window across the whole workstream.
234
+ * For this workstream's workers it takes precedence over a global
235
+ * `defaultBullMQWorkerOptions.limiter`.
236
+ *
237
+ * A job on this workstream that is told to slow down (an HTTP 429, say) can
238
+ * throw `RateLimitedPsychicJob` from `@rvoh/psychic-workers/errors` to pause
239
+ * the whole workstream without burning a retry attempt; see that class for
240
+ * the pause semantics and for the `maxStartedAttempts` bound.
241
+ *
242
+ * A debounced job on this workstream must carry a delay long enough that it
243
+ * cannot enqueue faster than this limit starts jobs, or it is refused at
244
+ * enqueue: see `DelayedJobOpts.jobId`.
245
+ *
246
+ * `max` and `duration` are both required positive integers (`duration` in
247
+ * milliseconds): open-source BullMQ floors a fractional `duration` to 0ms
248
+ * (rate limiting nothing) and fails every job fetch on one past Redis's
249
+ * integer range. A `rateLimit` that reaches `connect()` without both fails
250
+ * `connect()` naming the workstream and the field, before any queue or worker
251
+ * is built.
252
+ *
253
+ * Implemented as BullMQ's worker `limiter` option, and on BullMQ Pro also as
254
+ * the workstream group's rate limit (unverified against an installed Pro
255
+ * build).
207
256
  */
208
257
  rateLimit?: {
209
- max?: number;
210
- duration?: number;
258
+ max: number;
259
+ duration: number;
211
260
  };
212
261
  /**
213
262
  * Optional redis connection. If not provided, the default background redis connection will be used.