@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.
- package/dist/cjs/src/background/BaseBackgroundedModel.js +49 -25
- package/dist/cjs/src/background/BaseBackgroundedService.js +24 -12
- package/dist/cjs/src/background/BaseScheduledService.js +1 -1
- package/dist/cjs/src/background/index.js +383 -98
- package/dist/cjs/src/error/background/DebouncedJobOutpacesRateLimit.js +64 -0
- package/dist/cjs/src/error/background/DebouncedJobRequiresMinimumDelay.js +52 -0
- package/dist/cjs/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.js +31 -0
- package/dist/cjs/src/error/background/RateLimitedPsychicJob.js +82 -0
- package/dist/cjs/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.js +51 -0
- package/dist/cjs/src/package-exports/errors.js +7 -0
- package/dist/cjs/src/test-utils/WorkerTestUtils.js +78 -13
- package/dist/esm/src/background/BaseBackgroundedModel.js +49 -25
- package/dist/esm/src/background/BaseBackgroundedService.js +24 -12
- package/dist/esm/src/background/BaseScheduledService.js +1 -1
- package/dist/esm/src/background/index.js +383 -98
- package/dist/esm/src/error/background/DebouncedJobOutpacesRateLimit.js +64 -0
- package/dist/esm/src/error/background/DebouncedJobRequiresMinimumDelay.js +52 -0
- package/dist/esm/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.js +31 -0
- package/dist/esm/src/error/background/RateLimitedPsychicJob.js +82 -0
- package/dist/esm/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.js +51 -0
- package/dist/esm/src/package-exports/errors.js +7 -0
- package/dist/esm/src/test-utils/WorkerTestUtils.js +78 -13
- package/dist/types/src/background/BaseBackgroundedModel.d.ts +49 -25
- package/dist/types/src/background/BaseBackgroundedService.d.ts +24 -12
- package/dist/types/src/background/BaseScheduledService.d.ts +2 -2
- package/dist/types/src/background/index.d.ts +109 -5
- package/dist/types/src/error/background/DebouncedJobOutpacesRateLimit.d.ts +31 -0
- package/dist/types/src/error/background/DebouncedJobRequiresMinimumDelay.d.ts +23 -0
- package/dist/types/src/error/background/NamedWorkstreamRateLimitMissingMaxOrDuration.d.ts +16 -0
- package/dist/types/src/error/background/RateLimitedPsychicJob.d.ts +73 -0
- package/dist/types/src/error/background/RateLimitedPsychicJobThrownFromWorkerWithoutLimiter.d.ts +16 -0
- package/dist/types/src/package-exports/errors.d.ts +1 -0
- package/dist/types/src/psychic-app-workers/index.d.ts +102 -53
- package/dist/types/src/test-utils/WorkerTestUtils.d.ts +32 -1
- package/dist/types/src/types/background.d.ts +88 -6
- package/package.json +28 -25
|
@@ -1,10 +1,41 @@
|
|
|
1
|
+
import { Queue } from 'bullmq';
|
|
2
|
+
/**
|
|
3
|
+
* @internal
|
|
4
|
+
*
|
|
5
|
+
* resolves the queue's expected Redis name through `nameToRedisQueueName`,
|
|
6
|
+
* rather than hand-reconstructing a cluster hash tag or parallel-test suffix.
|
|
7
|
+
* `queue.opts.connection` is the same `Redis | Cluster` instance the queue was
|
|
8
|
+
* built with, which is why the cast is safe for every queue this package builds.
|
|
9
|
+
*/
|
|
10
|
+
export declare function queueNamesMatch(queue: Queue, compareQueueName: string): boolean;
|
|
1
11
|
export default class WorkerTestUtils {
|
|
2
12
|
static parallelTestSafeQueueName(queueName: string): string;
|
|
3
13
|
static work(opts?: TestWorkerWorkOffOpts): Promise<void>;
|
|
4
14
|
static workScheduled(opts?: TestWorkerScheduledWorkOffOpts): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* runs a delayed job in place. A `RateLimitedPsychicJob` thrown from a job on
|
|
17
|
+
* a queue whose workers carry no BullMQ `limiter` surfaces as the same
|
|
18
|
+
* misconfiguration error production would fail the job with; on a queue whose
|
|
19
|
+
* workers carry one, it propagates untranslated so the spec can assert on it.
|
|
20
|
+
*/
|
|
21
|
+
private static doScheduledWork;
|
|
5
22
|
static clean(): Promise<void>;
|
|
6
23
|
private static workOne;
|
|
7
|
-
|
|
24
|
+
/**
|
|
25
|
+
* runs one fetched job to completion or failure, mirroring what a real worker
|
|
26
|
+
* would do with a `RateLimitedPsychicJob`, then surfacing it so the spec goes
|
|
27
|
+
* red rather than looping:
|
|
28
|
+
*
|
|
29
|
+
* - on a queue whose workers carry no BullMQ `limiter`, the job is failed with
|
|
30
|
+
* the misconfiguration error production would fail it with (so, with the
|
|
31
|
+
* queue's `attempts` > 1, it lands in `delayed` for its retry, out of
|
|
32
|
+
* `active`, where `clean()` can reach it), and that error is rethrown
|
|
33
|
+
* - on a queue whose workers carry one, the job is moved back to the queue
|
|
34
|
+
* with no attempt counted (BullMQ's own rate-limit path), and the
|
|
35
|
+
* `RateLimitedPsychicJob` is rethrown
|
|
36
|
+
*
|
|
37
|
+
* Every other error only fails the job, as before.
|
|
38
|
+
*/
|
|
8
39
|
private static processJob;
|
|
9
40
|
}
|
|
10
41
|
interface TestWorkerWorkOffOpts {
|
|
@@ -29,14 +29,73 @@ export interface BackgroundJobData {
|
|
|
29
29
|
*/
|
|
30
30
|
globalName?: string;
|
|
31
31
|
}
|
|
32
|
-
|
|
32
|
+
/**
|
|
33
|
+
* the delay to hold a job for and, when a `jobId` is provided, the debounce
|
|
34
|
+
* that delay drives.
|
|
35
|
+
*
|
|
36
|
+
* Calling a delayed background method repeatedly with the same `jobId`
|
|
37
|
+
* collapses those calls into a **single execution**. Each call slides the
|
|
38
|
+
* pending job's fire time further out, and the job runs once, after the last
|
|
39
|
+
* call, when the delay has finally elapsed without another call arriving.
|
|
40
|
+
*
|
|
41
|
+
* ```ts
|
|
42
|
+
* // ten calls in quick succession; the report is generated once,
|
|
43
|
+
* // ten seconds after the last of them
|
|
44
|
+
* await Report.backgroundWith({ delay: { seconds: 10, jobId: `report-${user.id}` } }, 'generate', user.id)
|
|
45
|
+
* ```
|
|
46
|
+
*
|
|
47
|
+
* A delay object must carry at least one of `seconds`, `minutes`, `hours` or
|
|
48
|
+
* `days`. `{}` and `{ jobId: 'my-job' }` are compile errors, since a delay with
|
|
49
|
+
* no duration delays nothing and a `jobId` with no duration debounces
|
|
50
|
+
* nothing.
|
|
51
|
+
*/
|
|
52
|
+
export type DelayedJobOpts = AtLeastOneDelayedJobDuration & {
|
|
33
53
|
/**
|
|
34
|
-
* a
|
|
35
|
-
*
|
|
36
|
-
*
|
|
54
|
+
* a **deduplication key**, which debounces repeated calls: calls carrying the
|
|
55
|
+
* same `jobId` collapse into a single execution, which runs once the delay
|
|
56
|
+
* has elapsed without another call arriving. The window runs from the last
|
|
57
|
+
* call, not the first, so a burst is collapsed however long it lasts.
|
|
58
|
+
*
|
|
59
|
+
* Despite the name, this is not the BullMQ job id of the enqueued job, and
|
|
60
|
+
* `queue.getJob(jobId)` will not resolve the debounced job through it. BullMQ
|
|
61
|
+
* stores it as a separate Redis key; the matching read is
|
|
62
|
+
* `queue.getDeduplicationJobId(jobId)`.
|
|
63
|
+
*
|
|
64
|
+
* A delay carrying a `jobId` must be **at least three seconds**, and a
|
|
65
|
+
* shorter one is refused at enqueue.
|
|
66
|
+
*
|
|
67
|
+
* On a rate-limited workstream the delay must also clear the limiter's
|
|
68
|
+
* spacing: `delay - 1s >= duration / max`, so a workstream limited to one job
|
|
69
|
+
* a minute needs a 61-second delay. A delay that does not is refused at
|
|
70
|
+
* enqueue, naming the queue, both limiter numbers and a delay that would
|
|
71
|
+
* pass.
|
|
37
72
|
*/
|
|
38
73
|
jobId?: string;
|
|
39
74
|
};
|
|
75
|
+
/**
|
|
76
|
+
* `DelayedJobDuration`, but at least one field has to be there. Every field is
|
|
77
|
+
* optional on its own, so `{}` would otherwise compile and delay nothing.
|
|
78
|
+
* TypeScript has no "at least one of these" operator, so this spells out the
|
|
79
|
+
* four ways to satisfy it and joins them with `|`:
|
|
80
|
+
*
|
|
81
|
+
* ```ts
|
|
82
|
+
* | { seconds: number; minutes?: number; hours?: number; days?: number }
|
|
83
|
+
* | { minutes: number; seconds?: number; hours?: number; days?: number }
|
|
84
|
+
* | { hours: number; seconds?: number; minutes?: number; days?: number }
|
|
85
|
+
* | { days: number; seconds?: number; minutes?: number; hours?: number }
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* A value has to match one of those, which means carrying that line's required
|
|
89
|
+
* field. `{ seconds: 30 }` matches the first. `{}` matches none, and neither
|
|
90
|
+
* does `{ jobId: 'my-job' }`.
|
|
91
|
+
*
|
|
92
|
+
* Not exported: it exists only to narrow `DelayedJobOpts`. `DelayedJobDuration`
|
|
93
|
+
* itself stays all-optional because it is also the parameter type of the shared
|
|
94
|
+
* `durationToSeconds` helper.
|
|
95
|
+
*/
|
|
96
|
+
type AtLeastOneDelayedJobDuration = {
|
|
97
|
+
[Field in keyof DelayedJobDuration]-?: Required<Pick<DelayedJobDuration, Field>> & Partial<Omit<DelayedJobDuration, Field>>;
|
|
98
|
+
}[keyof DelayedJobDuration];
|
|
40
99
|
export interface DelayedJobDuration {
|
|
41
100
|
seconds?: number;
|
|
42
101
|
minutes?: number;
|
|
@@ -49,8 +108,15 @@ export interface BackgroundWithOpts {
|
|
|
49
108
|
/**
|
|
50
109
|
* an optional delay to hold off the job for a certain amount of
|
|
51
110
|
* time after it is entered into the queue. Accepts the same options
|
|
52
|
-
* as `backgroundWithDelay`,
|
|
53
|
-
*
|
|
111
|
+
* as `backgroundWithDelay`, and must carry at least one of `seconds`,
|
|
112
|
+
* `minutes`, `hours` or `days`.
|
|
113
|
+
*
|
|
114
|
+
* Adding a `jobId` turns the delay into a **debounce**: repeated calls
|
|
115
|
+
* carrying the same `jobId` collapse into a single execution, which runs
|
|
116
|
+
* once the delay has elapsed without another call arriving. `jobId` is a
|
|
117
|
+
* deduplication key rather than a BullMQ job id.
|
|
118
|
+
*
|
|
119
|
+
* See {@link DelayedJobOpts.jobId}.
|
|
54
120
|
*/
|
|
55
121
|
delay?: DelayedJobOpts;
|
|
56
122
|
/**
|
|
@@ -71,4 +137,20 @@ export interface QueueBackgroundJobConfig<T extends BaseScheduledService | BaseB
|
|
|
71
137
|
}
|
|
72
138
|
export type BackgroundJobConfig<T extends BaseScheduledService | BaseBackgroundedService> = Either<WorkstreamBackgroundJobConfig<T>, QueueBackgroundJobConfig<T>>;
|
|
73
139
|
export type PsychicBackgroundOptions = (PsychicBackgroundSimpleOptions & Partial<Record<Exclude<keyof PsychicBackgroundNativeBullMQOptions, keyof PsychicBackgroundSimpleOptions>, never>>) | (PsychicBackgroundNativeBullMQOptions & Partial<Record<Exclude<keyof PsychicBackgroundSimpleOptions, keyof PsychicBackgroundNativeBullMQOptions>, never>>);
|
|
140
|
+
/**
|
|
141
|
+
* @internal
|
|
142
|
+
*
|
|
143
|
+
* how a queue was configured, as far as the misconfiguration message needs to
|
|
144
|
+
* know in order to name the fix that applies to the worker that threw
|
|
145
|
+
*/
|
|
146
|
+
export interface WorkerQueueDescription {
|
|
147
|
+
/** simple (workstream) configuration or native BullMQ configuration */
|
|
148
|
+
mode: 'simple' | 'native';
|
|
149
|
+
/** the default workstream/queue, as opposed to a named one */
|
|
150
|
+
isDefaultQueue: boolean;
|
|
151
|
+
/** the workstream or queue name as configured (not the formatted Redis queue name) */
|
|
152
|
+
configuredName: string;
|
|
153
|
+
/** a workstream declared under `transitionalWorkstreams` */
|
|
154
|
+
transitional: boolean;
|
|
155
|
+
}
|
|
74
156
|
export {};
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"type": "module",
|
|
3
3
|
"name": "@rvoh/psychic-workers",
|
|
4
4
|
"description": "Background job system for Psychic applications",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.8.0",
|
|
6
6
|
"author": "RVO Health",
|
|
7
7
|
"publishConfig": {
|
|
8
8
|
"access": "public"
|
|
@@ -40,6 +40,9 @@
|
|
|
40
40
|
"files": [
|
|
41
41
|
"dist/**/*"
|
|
42
42
|
],
|
|
43
|
+
"engines": {
|
|
44
|
+
"node": ">=24"
|
|
45
|
+
},
|
|
43
46
|
"scripts": {
|
|
44
47
|
"psy": "NODE_ENV=${NODE_ENV:-test} pnpm psy:ts",
|
|
45
48
|
"psy:js": "node ./dist/test-app/src/cli/index.js",
|
|
@@ -64,46 +67,46 @@
|
|
|
64
67
|
"@eslint/js": "^10.0.1",
|
|
65
68
|
"@koa/cors": "^5.0.0",
|
|
66
69
|
"@koa/etag": "^5.0.2",
|
|
67
|
-
"@koa/router": "^15.
|
|
68
|
-
"@rvoh/dream": "^2.
|
|
69
|
-
"@rvoh/dream-spec-helpers": "^2.
|
|
70
|
-
"@rvoh/psychic": "^3.
|
|
71
|
-
"@rvoh/psychic-spec-helpers": "^3.
|
|
72
|
-
"@types/koa": "^3.0.
|
|
70
|
+
"@koa/router": "^15.7.0",
|
|
71
|
+
"@rvoh/dream": "^2.33.0",
|
|
72
|
+
"@rvoh/dream-spec-helpers": "^2.3.0",
|
|
73
|
+
"@rvoh/psychic": "^3.15.2",
|
|
74
|
+
"@rvoh/psychic-spec-helpers": "^3.4.0",
|
|
75
|
+
"@types/koa": "^3.0.3",
|
|
73
76
|
"@types/koa-bodyparser": "^4.3.13",
|
|
74
77
|
"@types/koa-conditional-get": "^2.0.4",
|
|
75
78
|
"@types/koa-etag": "^3.0.4",
|
|
76
79
|
"@types/koa__cors": "^5.0.1",
|
|
77
80
|
"@types/koa__router": "^12.0.5",
|
|
78
|
-
"@types/node": "^
|
|
81
|
+
"@types/node": "^26.6.3",
|
|
79
82
|
"@types/pg": "^8",
|
|
80
|
-
"@types/supertest": "^
|
|
81
|
-
"@typescript-eslint/parser": "^8.
|
|
83
|
+
"@types/supertest": "^7.2.1",
|
|
84
|
+
"@typescript-eslint/parser": "^8.70.1",
|
|
82
85
|
"bullmq": "^5.64.0",
|
|
83
|
-
"eslint": "^10.0
|
|
86
|
+
"eslint": "^10.11.0",
|
|
84
87
|
"ioredis": "^5.8.2",
|
|
85
|
-
"koa": "^3.1
|
|
88
|
+
"koa": "^3.2.1",
|
|
86
89
|
"koa-bodyparser": "^4.4.1",
|
|
87
90
|
"koa-conditional-get": "^3.0.0",
|
|
88
|
-
"kysely": "^0.
|
|
91
|
+
"kysely": "^0.29.6",
|
|
89
92
|
"kysely-codegen": "~0.20.0",
|
|
90
93
|
"luxon-jest-matchers": "^0.1.14",
|
|
91
|
-
"openapi-typescript": "^7.
|
|
92
|
-
"pg": "^8.
|
|
93
|
-
"prettier": "^3.
|
|
94
|
-
"supertest": "^7.
|
|
94
|
+
"openapi-typescript": "^7.13.0",
|
|
95
|
+
"pg": "^8.23.0",
|
|
96
|
+
"prettier": "^3.9.9",
|
|
97
|
+
"supertest": "^7.3.0",
|
|
95
98
|
"tslib": "^2.7.0",
|
|
96
|
-
"tsx": "^4.
|
|
97
|
-
"typedoc": "^0.28.
|
|
98
|
-
"typescript": "^5.
|
|
99
|
-
"typescript-eslint": "^8.
|
|
100
|
-
"vite": "^
|
|
101
|
-
"vitest": "^
|
|
99
|
+
"tsx": "^4.23.15",
|
|
100
|
+
"typedoc": "^0.28.20",
|
|
101
|
+
"typescript": "^5.9.3",
|
|
102
|
+
"typescript-eslint": "^8.70.1",
|
|
103
|
+
"vite": "^8.3.1",
|
|
104
|
+
"vitest": "^5.0.2"
|
|
102
105
|
},
|
|
103
106
|
"packageManager": "pnpm@11.9.0+sha512.bd682d5d03fe525ef7c9fd6780c6884d1e756ac4c9c9fe00c538782824310dcf90e3ddc4f53835f06dfaebd5085e41855e0bcbb3b60de2ac5bbab89e5036f03b",
|
|
104
107
|
"dependencies": {
|
|
105
|
-
"commander": "^14.0.
|
|
106
|
-
"dotenv": "^17.2
|
|
108
|
+
"commander": "^14.0.3",
|
|
109
|
+
"dotenv": "^17.4.2",
|
|
107
110
|
"pluralize-esm": "^9.0.5"
|
|
108
111
|
}
|
|
109
112
|
}
|