@c9up/bay 0.1.3
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/LICENSE +21 -0
- package/README.md +34 -0
- package/dist/BayProvider.d.ts +60 -0
- package/dist/BayProvider.d.ts.map +1 -0
- package/dist/BayProvider.js +50 -0
- package/dist/BayProvider.js.map +1 -0
- package/dist/QueueManager.d.ts +57 -0
- package/dist/QueueManager.d.ts.map +1 -0
- package/dist/QueueManager.js +121 -0
- package/dist/QueueManager.js.map +1 -0
- package/dist/drivers/MemoryDriver.d.ts +18 -0
- package/dist/drivers/MemoryDriver.d.ts.map +1 -0
- package/dist/drivers/MemoryDriver.js +39 -0
- package/dist/drivers/MemoryDriver.js.map +1 -0
- package/dist/drivers/RedisDriver.d.ts +39 -0
- package/dist/drivers/RedisDriver.d.ts.map +1 -0
- package/dist/drivers/RedisDriver.js +163 -0
- package/dist/drivers/RedisDriver.js.map +1 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +11 -0
- package/dist/index.js.map +1 -0
- package/dist/services/main.d.ts +22 -0
- package/dist/services/main.d.ts.map +1 -0
- package/dist/services/main.js +35 -0
- package/dist/services/main.js.map +1 -0
- package/dist/testing/FakeQueue.d.ts +44 -0
- package/dist/testing/FakeQueue.d.ts.map +1 -0
- package/dist/testing/FakeQueue.js +131 -0
- package/dist/testing/FakeQueue.js.map +1 -0
- package/package.json +63 -0
- package/src/BayProvider.ts +85 -0
- package/src/QueueManager.ts +165 -0
- package/src/drivers/MemoryDriver.ts +49 -0
- package/src/drivers/RedisDriver.ts +202 -0
- package/src/index.ts +13 -0
- package/src/services/main.ts +43 -0
- package/src/testing/FakeQueue.ts +172 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 C9up
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @c9up/bay
|
|
2
|
+
|
|
3
|
+
> Pluggable job-queue contract for the Ream framework, with memory + Redis drivers.
|
|
4
|
+
|
|
5
|
+
Part of **[Ream](https://github.com/C9up/ream)** — a Rust-powered, AdonisJS-compatible Node.js framework. Independent, publishable package.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pnpm add @c9up/bay
|
|
11
|
+
ream configure @c9up/bay
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## Usage
|
|
15
|
+
|
|
16
|
+
Register the provider in your app, then configure it under `config/bay.ts`:
|
|
17
|
+
|
|
18
|
+
```ts
|
|
19
|
+
// reamrc.ts
|
|
20
|
+
providers: [
|
|
21
|
+
() => import('@c9up/bay/provider'),
|
|
22
|
+
]
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Entry points
|
|
26
|
+
|
|
27
|
+
- `@c9up/bay` — main API
|
|
28
|
+
- `@c9up/bay/provider` — Ream IoC provider
|
|
29
|
+
- `@c9up/bay/services/main` — container service accessor
|
|
30
|
+
- `@c9up/bay/testing` — test fakes & helpers
|
|
31
|
+
|
|
32
|
+
## License
|
|
33
|
+
|
|
34
|
+
MIT
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Slim, duck-typed host context — bay stays publishable without
|
|
3
|
+
* importing `@c9up/ream`. Any framework that exposes a Container with
|
|
4
|
+
* `singleton(token, factory)` + `resolve(token)` and a config store
|
|
5
|
+
* with `get(key)` satisfies the contract.
|
|
6
|
+
*/
|
|
7
|
+
interface BayContainer {
|
|
8
|
+
singleton(token: unknown, factory: () => unknown): void;
|
|
9
|
+
resolve<T = unknown>(token: unknown): T;
|
|
10
|
+
}
|
|
11
|
+
interface BayConfigStore {
|
|
12
|
+
get<T = unknown>(key: string): T | undefined;
|
|
13
|
+
}
|
|
14
|
+
export interface BayAppContext {
|
|
15
|
+
container: BayContainer;
|
|
16
|
+
config: BayConfigStore;
|
|
17
|
+
}
|
|
18
|
+
export interface BayProviderConfig {
|
|
19
|
+
/**
|
|
20
|
+
* Driver to bind by default. Recognized strings: `"memory"`,
|
|
21
|
+
* `"redis"`. For any other case (custom driver, pre-built
|
|
22
|
+
* instance), wire `QueueManager` directly in your app's startup
|
|
23
|
+
* and skip the provider — the `services/main` singleton accepts
|
|
24
|
+
* `setQueue(myQueue)` from outside.
|
|
25
|
+
*
|
|
26
|
+
* Default `"memory"`.
|
|
27
|
+
*/
|
|
28
|
+
driver?: "memory" | "redis";
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* BayProvider — registers a default in-memory `QueueManager` in the
|
|
32
|
+
* host container so apps that don't need custom driver wiring can
|
|
33
|
+
* `import queue from '@c9up/bay/services/main'` and dispatch
|
|
34
|
+
* straight away. Job handlers are still registered manually via
|
|
35
|
+
* `queue.register(name, handler)` — that's intrinsic to the queue
|
|
36
|
+
* design (handlers are app-defined, not config-driven).
|
|
37
|
+
*
|
|
38
|
+
* Apps with non-trivial wiring (Redis driver, custom queue config)
|
|
39
|
+
* can ignore this provider and bind their own `QueueManager` instance
|
|
40
|
+
* in the container; the `services/main` proxy resolves whatever is
|
|
41
|
+
* registered.
|
|
42
|
+
*
|
|
43
|
+
* // reamrc.ts
|
|
44
|
+
* providers: [() => import('@c9up/bay/provider')]
|
|
45
|
+
*
|
|
46
|
+
* // start/queue.ts
|
|
47
|
+
* import queue from '@c9up/bay/services/main'
|
|
48
|
+
*
|
|
49
|
+
* queue.register('send-email', new SendEmailJob())
|
|
50
|
+
* await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
51
|
+
*/
|
|
52
|
+
export default class BayProvider {
|
|
53
|
+
protected app: BayAppContext;
|
|
54
|
+
constructor(app: BayAppContext);
|
|
55
|
+
register(): void;
|
|
56
|
+
boot(): Promise<void>;
|
|
57
|
+
shutdown(): Promise<void>;
|
|
58
|
+
}
|
|
59
|
+
export {};
|
|
60
|
+
//# sourceMappingURL=BayProvider.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BayProvider.d.ts","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAIA;;;;;GAKG;AACH,UAAU,YAAY;IACrB,SAAS,CAAC,KAAK,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,OAAO,GAAG,IAAI,CAAC;IACxD,OAAO,CAAC,CAAC,GAAG,OAAO,EAAE,KAAK,EAAE,OAAO,GAAG,CAAC,CAAC;CACxC;AACD,UAAU,cAAc;IACvB,GAAG,CAAC,CAAC,GAAG,OAAO,EAAE,GAAG,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS,CAAC;CAC7C;AACD,MAAM,WAAW,aAAa;IAC7B,SAAS,EAAE,YAAY,CAAC;IACxB,MAAM,EAAE,cAAc,CAAC;CACvB;AAED,MAAM,WAAW,iBAAiB;IACjC;;;;;;;;OAQG;IACH,MAAM,CAAC,EAAE,QAAQ,GAAG,OAAO,CAAC;CAC5B;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACnB,SAAS,CAAC,GAAG,EAAE,aAAa;gBAAlB,GAAG,EAAE,aAAa;IAExC,QAAQ,IAAI,IAAI;IAiBV,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAMrB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;CAC/B"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { MemoryDriver } from "./drivers/MemoryDriver.js";
|
|
2
|
+
import { QueueManager } from "./QueueManager.js";
|
|
3
|
+
import { setQueue } from "./services/main.js";
|
|
4
|
+
/**
|
|
5
|
+
* BayProvider — registers a default in-memory `QueueManager` in the
|
|
6
|
+
* host container so apps that don't need custom driver wiring can
|
|
7
|
+
* `import queue from '@c9up/bay/services/main'` and dispatch
|
|
8
|
+
* straight away. Job handlers are still registered manually via
|
|
9
|
+
* `queue.register(name, handler)` — that's intrinsic to the queue
|
|
10
|
+
* design (handlers are app-defined, not config-driven).
|
|
11
|
+
*
|
|
12
|
+
* Apps with non-trivial wiring (Redis driver, custom queue config)
|
|
13
|
+
* can ignore this provider and bind their own `QueueManager` instance
|
|
14
|
+
* in the container; the `services/main` proxy resolves whatever is
|
|
15
|
+
* registered.
|
|
16
|
+
*
|
|
17
|
+
* // reamrc.ts
|
|
18
|
+
* providers: [() => import('@c9up/bay/provider')]
|
|
19
|
+
*
|
|
20
|
+
* // start/queue.ts
|
|
21
|
+
* import queue from '@c9up/bay/services/main'
|
|
22
|
+
*
|
|
23
|
+
* queue.register('send-email', new SendEmailJob())
|
|
24
|
+
* await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
25
|
+
*/
|
|
26
|
+
export default class BayProvider {
|
|
27
|
+
app;
|
|
28
|
+
constructor(app) {
|
|
29
|
+
this.app = app;
|
|
30
|
+
}
|
|
31
|
+
register() {
|
|
32
|
+
this.app.container.singleton(QueueManager, () => {
|
|
33
|
+
const config = this.app.config.get("queue");
|
|
34
|
+
const driverName = config?.driver ?? "memory";
|
|
35
|
+
if (driverName !== "memory") {
|
|
36
|
+
throw new Error(`[bay] Unsupported driver '${driverName}' for default provider — ` +
|
|
37
|
+
"wire QueueManager yourself in start/queue.ts for non-memory drivers.");
|
|
38
|
+
}
|
|
39
|
+
return new QueueManager(new MemoryDriver());
|
|
40
|
+
});
|
|
41
|
+
this.app.container.singleton("queue", () => this.app.container.resolve(QueueManager));
|
|
42
|
+
}
|
|
43
|
+
async boot() {
|
|
44
|
+
// Populate the `@c9up/bay/services/main` singleton so apps can
|
|
45
|
+
// `import queue from '@c9up/bay/services/main'` from anywhere.
|
|
46
|
+
setQueue(this.app.container.resolve(QueueManager));
|
|
47
|
+
}
|
|
48
|
+
async shutdown() { }
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=BayProvider.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BayProvider.js","sourceRoot":"","sources":["../src/BayProvider.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AACzD,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAiC9C;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,CAAC,OAAO,OAAO,WAAW;IACT;IAAtB,YAAsB,GAAkB;QAAlB,QAAG,GAAH,GAAG,CAAe;IAAG,CAAC;IAE5C,QAAQ;QACP,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,YAAY,EAAE,GAAG,EAAE;YAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAoB,OAAO,CAAC,CAAC;YAC/D,MAAM,UAAU,GAAG,MAAM,EAAE,MAAM,IAAI,QAAQ,CAAC;YAC9C,IAAI,UAAU,KAAK,QAAQ,EAAE,CAAC;gBAC7B,MAAM,IAAI,KAAK,CACd,6BAA6B,UAAU,2BAA2B;oBACjE,sEAAsE,CACvE,CAAC;YACH,CAAC;YACD,OAAO,IAAI,YAAY,CAAC,IAAI,YAAY,EAAE,CAAC,CAAC;QAC7C,CAAC,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,SAAS,CAAC,OAAO,EAAE,GAAG,EAAE,CAC1C,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CACtD,CAAC;IACH,CAAC;IAED,KAAK,CAAC,IAAI;QACT,+DAA+D;QAC/D,+DAA+D;QAC/D,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC,OAAO,CAAe,YAAY,CAAC,CAAC,CAAC;IAClE,CAAC;IAED,KAAK,CAAC,QAAQ,KAAmB,CAAC;CAClC"}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueManager — dispatch and process background jobs.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* queue.register('send-email', new SendEmailHandler())
|
|
6
|
+
* await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
7
|
+
* queue.work()
|
|
8
|
+
*/
|
|
9
|
+
export interface Job {
|
|
10
|
+
id: string;
|
|
11
|
+
name: string;
|
|
12
|
+
payload: unknown;
|
|
13
|
+
attempts: number;
|
|
14
|
+
maxAttempts: number;
|
|
15
|
+
status: "pending" | "processing" | "completed" | "failed";
|
|
16
|
+
error?: string;
|
|
17
|
+
createdAt: number;
|
|
18
|
+
processedAt?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface JobHandler {
|
|
21
|
+
handle(payload: unknown): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
export interface QueueDriver {
|
|
24
|
+
push(job: Job): Promise<void>;
|
|
25
|
+
pop(): Promise<Job | null>;
|
|
26
|
+
fail(job: Job, error: string): Promise<void>;
|
|
27
|
+
complete(job: Job): Promise<void>;
|
|
28
|
+
retry(job: Job): Promise<void>;
|
|
29
|
+
failed(): Promise<Job[]>;
|
|
30
|
+
size(): Promise<number>;
|
|
31
|
+
}
|
|
32
|
+
export declare class QueueManager {
|
|
33
|
+
private driver;
|
|
34
|
+
private handlers;
|
|
35
|
+
private running;
|
|
36
|
+
private inflightPromise;
|
|
37
|
+
constructor(driver: QueueDriver);
|
|
38
|
+
/** Register a job handler. */
|
|
39
|
+
register(name: string, handler: JobHandler | (new () => JobHandler)): void;
|
|
40
|
+
/** Dispatch a job to the queue. */
|
|
41
|
+
dispatch(name: string, payload: unknown, options?: {
|
|
42
|
+
maxAttempts?: number;
|
|
43
|
+
}): Promise<string>;
|
|
44
|
+
/** Process the next job in the queue. */
|
|
45
|
+
processOne(): Promise<boolean>;
|
|
46
|
+
/** Start processing jobs continuously. */
|
|
47
|
+
work(pollIntervalMs?: number): Promise<void>;
|
|
48
|
+
/** Await the currently in-flight processOne, if any. */
|
|
49
|
+
drain(): Promise<void>;
|
|
50
|
+
/** Stop the worker. */
|
|
51
|
+
stop(): Promise<void>;
|
|
52
|
+
/** Get failed jobs. */
|
|
53
|
+
failedJobs(): Promise<Job[]>;
|
|
54
|
+
/** Get queue size. */
|
|
55
|
+
size(): Promise<number>;
|
|
56
|
+
}
|
|
57
|
+
//# sourceMappingURL=QueueManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueManager.d.ts","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAEH,MAAM,WAAW,GAAG;IACnB,EAAE,EAAE,MAAM,CAAC;IACX,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;IACjB,WAAW,EAAE,MAAM,CAAC;IACpB,MAAM,EAAE,SAAS,GAAG,YAAY,GAAG,WAAW,GAAG,QAAQ,CAAC;IAC1D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,UAAU;IAC1B,MAAM,CAAC,OAAO,EAAE,OAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CACxC;AAED,MAAM,WAAW,WAAW;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC9B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC,CAAC;IAC3B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7C,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;CACxB;AAED,qBAAa,YAAY;IACxB,OAAO,CAAC,MAAM,CAAc;IAC5B,OAAO,CAAC,QAAQ,CACL;IACX,OAAO,CAAC,OAAO,CAAS;IACxB,OAAO,CAAC,eAAe,CAAiC;gBAE5C,MAAM,EAAE,WAAW;IAI/B,8BAA8B;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,UAAU,GAAG,CAAC,UAAU,UAAU,CAAC,GAAG,IAAI;IAI1E,mCAAmC;IAC7B,QAAQ,CACb,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,OAAO,EAChB,OAAO,CAAC,EAAE;QAAE,WAAW,CAAC,EAAE,MAAM,CAAA;KAAE,GAChC,OAAO,CAAC,MAAM,CAAC;IAkBlB,yCAAyC;IACnC,UAAU,IAAI,OAAO,CAAC,OAAO,CAAC;IAwCpC,0CAA0C;IACpC,IAAI,CAAC,cAAc,SAAO,GAAG,OAAO,CAAC,IAAI,CAAC;IA0BhD,wDAAwD;IAClD,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;IAM5B,uBAAuB;IACjB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAK3B,uBAAuB;IACjB,UAAU,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIlC,sBAAsB;IAChB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG7B"}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* QueueManager — dispatch and process background jobs.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* queue.register('send-email', new SendEmailHandler())
|
|
6
|
+
* await queue.dispatch('send-email', { to: 'user@example.com' })
|
|
7
|
+
* queue.work()
|
|
8
|
+
*/
|
|
9
|
+
export class QueueManager {
|
|
10
|
+
driver;
|
|
11
|
+
handlers = new Map();
|
|
12
|
+
running = false;
|
|
13
|
+
inflightPromise = null;
|
|
14
|
+
constructor(driver) {
|
|
15
|
+
this.driver = driver;
|
|
16
|
+
}
|
|
17
|
+
/** Register a job handler. */
|
|
18
|
+
register(name, handler) {
|
|
19
|
+
this.handlers.set(name, handler);
|
|
20
|
+
}
|
|
21
|
+
/** Dispatch a job to the queue. */
|
|
22
|
+
async dispatch(name, payload, options) {
|
|
23
|
+
if (options?.maxAttempts !== undefined && options.maxAttempts < 1) {
|
|
24
|
+
throw new Error("maxAttempts must be >= 1");
|
|
25
|
+
}
|
|
26
|
+
const id = `job_${crypto.randomUUID()}`;
|
|
27
|
+
const job = {
|
|
28
|
+
id,
|
|
29
|
+
name,
|
|
30
|
+
payload,
|
|
31
|
+
attempts: 0,
|
|
32
|
+
maxAttempts: options?.maxAttempts ?? 3,
|
|
33
|
+
status: "pending",
|
|
34
|
+
createdAt: Date.now(),
|
|
35
|
+
};
|
|
36
|
+
await this.driver.push(job);
|
|
37
|
+
return id;
|
|
38
|
+
}
|
|
39
|
+
/** Process the next job in the queue. */
|
|
40
|
+
async processOne() {
|
|
41
|
+
const job = await this.driver.pop();
|
|
42
|
+
if (!job)
|
|
43
|
+
return false;
|
|
44
|
+
const handlerOrClass = this.handlers.get(job.name);
|
|
45
|
+
if (!handlerOrClass) {
|
|
46
|
+
process.stderr.write(`QueueManager: no handler registered for job '${job.name}'\n`);
|
|
47
|
+
await this.driver.fail(job, `No handler registered for job: ${job.name}`);
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
const handler = typeof handlerOrClass === "function"
|
|
51
|
+
? new handlerOrClass()
|
|
52
|
+
: handlerOrClass;
|
|
53
|
+
job.attempts++;
|
|
54
|
+
job.status = "processing";
|
|
55
|
+
job.processedAt = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
await handler.handle(job.payload);
|
|
58
|
+
job.status = "completed";
|
|
59
|
+
await this.driver.complete(job);
|
|
60
|
+
}
|
|
61
|
+
catch (err) {
|
|
62
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
63
|
+
if (job.attempts < job.maxAttempts) {
|
|
64
|
+
job.status = "pending";
|
|
65
|
+
await this.driver.retry(job);
|
|
66
|
+
}
|
|
67
|
+
else {
|
|
68
|
+
job.status = "failed";
|
|
69
|
+
job.error = errorMsg;
|
|
70
|
+
await this.driver.fail(job, errorMsg);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return true;
|
|
74
|
+
}
|
|
75
|
+
/** Start processing jobs continuously. */
|
|
76
|
+
async work(pollIntervalMs = 1000) {
|
|
77
|
+
if (pollIntervalMs <= 0) {
|
|
78
|
+
throw new Error("pollIntervalMs must be positive");
|
|
79
|
+
}
|
|
80
|
+
if (this.running) {
|
|
81
|
+
throw new Error("QueueManager is already running");
|
|
82
|
+
}
|
|
83
|
+
this.running = true;
|
|
84
|
+
while (this.running) {
|
|
85
|
+
try {
|
|
86
|
+
this.inflightPromise = this.processOne();
|
|
87
|
+
const processed = await this.inflightPromise;
|
|
88
|
+
if (!processed) {
|
|
89
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
catch (err) {
|
|
93
|
+
process.stderr.write(`QueueManager processOne error: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
94
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
95
|
+
}
|
|
96
|
+
finally {
|
|
97
|
+
this.inflightPromise = null;
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
/** Await the currently in-flight processOne, if any. */
|
|
102
|
+
async drain() {
|
|
103
|
+
if (this.inflightPromise) {
|
|
104
|
+
await this.inflightPromise.catch(() => { });
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
/** Stop the worker. */
|
|
108
|
+
async stop() {
|
|
109
|
+
this.running = false;
|
|
110
|
+
await this.drain();
|
|
111
|
+
}
|
|
112
|
+
/** Get failed jobs. */
|
|
113
|
+
async failedJobs() {
|
|
114
|
+
return this.driver.failed();
|
|
115
|
+
}
|
|
116
|
+
/** Get queue size. */
|
|
117
|
+
async size() {
|
|
118
|
+
return this.driver.size();
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=QueueManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueManager.js","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory queue driver — in-process queue for development.
|
|
3
|
+
*/
|
|
4
|
+
import type { Job, QueueDriver } from "../QueueManager.js";
|
|
5
|
+
export declare class MemoryDriver implements QueueDriver {
|
|
6
|
+
#private;
|
|
7
|
+
constructor(options?: {
|
|
8
|
+
maxFailedJobs?: number;
|
|
9
|
+
});
|
|
10
|
+
push(job: Job): Promise<void>;
|
|
11
|
+
pop(): Promise<Job | null>;
|
|
12
|
+
fail(job: Job, error: string): Promise<void>;
|
|
13
|
+
complete(_job: Job): Promise<void>;
|
|
14
|
+
retry(job: Job): Promise<void>;
|
|
15
|
+
failed(): Promise<Job[]>;
|
|
16
|
+
size(): Promise<number>;
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=MemoryDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/MemoryDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,qBAAa,YAAa,YAAW,WAAW;;gBAKnC,OAAO,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE;IAI1C,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAI1B,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAS5C,QAAQ,CAAC,IAAI,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlC,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAK9B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAIxB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG7B"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Memory queue driver — in-process queue for development.
|
|
3
|
+
*/
|
|
4
|
+
export class MemoryDriver {
|
|
5
|
+
#pending = [];
|
|
6
|
+
#failedJobs = [];
|
|
7
|
+
#maxFailedJobs;
|
|
8
|
+
constructor(options) {
|
|
9
|
+
this.#maxFailedJobs = options?.maxFailedJobs ?? 1000;
|
|
10
|
+
}
|
|
11
|
+
async push(job) {
|
|
12
|
+
this.#pending.push(job);
|
|
13
|
+
}
|
|
14
|
+
async pop() {
|
|
15
|
+
return this.#pending.shift() ?? null;
|
|
16
|
+
}
|
|
17
|
+
async fail(job, error) {
|
|
18
|
+
job.error = error;
|
|
19
|
+
job.status = "failed";
|
|
20
|
+
this.#failedJobs.push(job);
|
|
21
|
+
if (this.#failedJobs.length > this.#maxFailedJobs) {
|
|
22
|
+
this.#failedJobs.splice(0, this.#failedJobs.length - this.#maxFailedJobs);
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
async complete(_job) {
|
|
26
|
+
// Nothing to do for memory driver
|
|
27
|
+
}
|
|
28
|
+
async retry(job) {
|
|
29
|
+
job.status = "pending";
|
|
30
|
+
this.#pending.push(job);
|
|
31
|
+
}
|
|
32
|
+
async failed() {
|
|
33
|
+
return [...this.#failedJobs];
|
|
34
|
+
}
|
|
35
|
+
async size() {
|
|
36
|
+
return this.#pending.length;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=MemoryDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryDriver.js","sourceRoot":"","sources":["../../src/drivers/MemoryDriver.ts"],"names":[],"mappings":"AAAA;;GAEG;AAIH,MAAM,OAAO,YAAY;IACxB,QAAQ,GAAU,EAAE,CAAC;IACrB,WAAW,GAAU,EAAE,CAAC;IACxB,cAAc,CAAS;IAEvB,YAAY,OAAoC;QAC/C,IAAI,CAAC,cAAc,GAAG,OAAO,EAAE,aAAa,IAAI,IAAI,CAAC;IACtD,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAQ;QAClB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,GAAG;QACR,OAAO,IAAI,CAAC,QAAQ,CAAC,KAAK,EAAE,IAAI,IAAI,CAAC;IACtC,CAAC;IAED,KAAK,CAAC,IAAI,CAAC,GAAQ,EAAE,KAAa;QACjC,GAAG,CAAC,KAAK,GAAG,KAAK,CAAC;QAClB,GAAG,CAAC,MAAM,GAAG,QAAQ,CAAC;QACtB,IAAI,CAAC,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC3B,IAAI,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,cAAc,EAAE,CAAC;YACnD,IAAI,CAAC,WAAW,CAAC,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,WAAW,CAAC,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,CAAC;QAC3E,CAAC;IACF,CAAC;IAED,KAAK,CAAC,QAAQ,CAAC,IAAS;QACvB,kCAAkC;IACnC,CAAC;IAED,KAAK,CAAC,KAAK,CAAC,GAAQ;QACnB,GAAG,CAAC,MAAM,GAAG,SAAS,CAAC;QACvB,IAAI,CAAC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACzB,CAAC;IAED,KAAK,CAAC,MAAM;QACX,OAAO,CAAC,GAAG,IAAI,CAAC,WAAW,CAAC,CAAC;IAC9B,CAAC;IAED,KAAK,CAAC,IAAI;QACT,OAAO,IAAI,CAAC,QAAQ,CAAC,MAAM,CAAC;IAC7B,CAAC;CACD"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis queue driver — FIFO job queue with visibility timeout.
|
|
3
|
+
*
|
|
4
|
+
* Uses LMOVE (Redis 6.2+) for at-least-once delivery:
|
|
5
|
+
* - pop() moves the job from pending → processing (atomic)
|
|
6
|
+
* - complete() removes from processing
|
|
7
|
+
* - If a worker crashes, the job stays in processing
|
|
8
|
+
* - recoverStale() moves expired processing jobs back to pending
|
|
9
|
+
*
|
|
10
|
+
* Compatible with ioredis and node-redis clients.
|
|
11
|
+
*/
|
|
12
|
+
import type { Job, QueueDriver } from "../QueueManager.js";
|
|
13
|
+
export interface RedisClient {
|
|
14
|
+
rpush(key: string, ...values: string[]): Promise<number>;
|
|
15
|
+
lpop(key: string): Promise<string | null>;
|
|
16
|
+
lmove?(source: string, destination: string, from: "LEFT" | "RIGHT", to: "LEFT" | "RIGHT"): Promise<string | null>;
|
|
17
|
+
lrem(key: string, count: number, element: string): Promise<number>;
|
|
18
|
+
llen(key: string): Promise<number>;
|
|
19
|
+
lrange(key: string, start: number, stop: number): Promise<string[]>;
|
|
20
|
+
del(key: string): Promise<number>;
|
|
21
|
+
set(key: string, value: string, ...args: string[]): Promise<string | null>;
|
|
22
|
+
get(key: string): Promise<string | null>;
|
|
23
|
+
}
|
|
24
|
+
export declare class RedisDriver implements QueueDriver {
|
|
25
|
+
#private;
|
|
26
|
+
constructor(client: RedisClient, options?: {
|
|
27
|
+
prefix?: string;
|
|
28
|
+
visibilityTimeoutMs?: number;
|
|
29
|
+
});
|
|
30
|
+
push(job: Job): Promise<void>;
|
|
31
|
+
pop(): Promise<Job | null>;
|
|
32
|
+
complete(job: Job): Promise<void>;
|
|
33
|
+
fail(job: Job, error: string): Promise<void>;
|
|
34
|
+
retry(job: Job): Promise<void>;
|
|
35
|
+
recoverStale(): Promise<number>;
|
|
36
|
+
failed(): Promise<Job[]>;
|
|
37
|
+
size(): Promise<number>;
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=RedisDriver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisDriver.d.ts","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,OAAO,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,oBAAoB,CAAC;AAE3D,MAAM,WAAW,WAAW;IAC3B,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzD,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1C,KAAK,CAAC,CACL,MAAM,EAAE,MAAM,EACd,WAAW,EAAE,MAAM,EACnB,IAAI,EAAE,MAAM,GAAG,OAAO,EACtB,EAAE,EAAE,MAAM,GAAG,OAAO,GAClB,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC1B,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnE,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACnC,MAAM,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;IACpE,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,GAAG,IAAI,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3E,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;CACzC;AAcD,qBAAa,WAAY,YAAW,WAAW;;gBAM7C,MAAM,EAAE,WAAW,EACnB,OAAO,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,MAAM,CAAC;QAAC,mBAAmB,CAAC,EAAE,MAAM,CAAA;KAAE;IAYtD,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAI7B,GAAG,IAAI,OAAO,CAAC,GAAG,GAAG,IAAI,CAAC;IAsC1B,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAKjC,IAAI,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAQ5C,KAAK,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC,IAAI,CAAC;IAO9B,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IA4D/B,MAAM,IAAI,OAAO,CAAC,GAAG,EAAE,CAAC;IAcxB,IAAI,IAAI,OAAO,CAAC,MAAM,CAAC;CAG7B"}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redis queue driver — FIFO job queue with visibility timeout.
|
|
3
|
+
*
|
|
4
|
+
* Uses LMOVE (Redis 6.2+) for at-least-once delivery:
|
|
5
|
+
* - pop() moves the job from pending → processing (atomic)
|
|
6
|
+
* - complete() removes from processing
|
|
7
|
+
* - If a worker crashes, the job stays in processing
|
|
8
|
+
* - recoverStale() moves expired processing jobs back to pending
|
|
9
|
+
*
|
|
10
|
+
* Compatible with ioredis and node-redis clients.
|
|
11
|
+
*/
|
|
12
|
+
function isValidJob(obj) {
|
|
13
|
+
if (typeof obj !== "object" || obj === null)
|
|
14
|
+
return false;
|
|
15
|
+
const j = obj;
|
|
16
|
+
return (typeof j.id === "string" &&
|
|
17
|
+
typeof j.name === "string" &&
|
|
18
|
+
typeof j.attempts === "number" &&
|
|
19
|
+
typeof j.maxAttempts === "number" &&
|
|
20
|
+
typeof j.status === "string");
|
|
21
|
+
}
|
|
22
|
+
export class RedisDriver {
|
|
23
|
+
#client;
|
|
24
|
+
#prefix;
|
|
25
|
+
#visibilityTimeout;
|
|
26
|
+
constructor(client, options) {
|
|
27
|
+
this.#client = client;
|
|
28
|
+
this.#prefix = options?.prefix ?? "queue:";
|
|
29
|
+
this.#visibilityTimeout = options?.visibilityTimeoutMs ?? 30_000;
|
|
30
|
+
}
|
|
31
|
+
#pendingKey = () => `${this.#prefix}pending`;
|
|
32
|
+
#processingKey = () => `${this.#prefix}processing`;
|
|
33
|
+
#failedKey = () => `${this.#prefix}failed`;
|
|
34
|
+
#leaseKey = (jobId) => `${this.#prefix}lease:${jobId}`;
|
|
35
|
+
async push(job) {
|
|
36
|
+
await this.#client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
37
|
+
}
|
|
38
|
+
async pop() {
|
|
39
|
+
let raw = null;
|
|
40
|
+
if (this.#client.lmove) {
|
|
41
|
+
raw = await this.#client.lmove(this.#pendingKey(), this.#processingKey(), "LEFT", "RIGHT");
|
|
42
|
+
}
|
|
43
|
+
else {
|
|
44
|
+
raw = await this.#client.lpop(this.#pendingKey());
|
|
45
|
+
if (raw)
|
|
46
|
+
await this.#client.rpush(this.#processingKey(), raw);
|
|
47
|
+
}
|
|
48
|
+
if (!raw)
|
|
49
|
+
return null;
|
|
50
|
+
try {
|
|
51
|
+
const parsed = JSON.parse(raw);
|
|
52
|
+
if (!isValidJob(parsed)) {
|
|
53
|
+
// Malformed payload — purge from `processing` so it can't sit
|
|
54
|
+
// there indefinitely as a poison pill. recoverStale() also
|
|
55
|
+
// catches survivors but pop()'s own move is the primary path.
|
|
56
|
+
await this.#client.lrem(this.#processingKey(), 1, raw);
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
await this.#client.set(this.#leaseKey(parsed.id), raw, "PX", String(this.#visibilityTimeout));
|
|
60
|
+
return parsed;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
await this.#client.lrem(this.#processingKey(), 1, raw);
|
|
64
|
+
return null;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
async complete(job) {
|
|
68
|
+
await this.#removeFromProcessing(job);
|
|
69
|
+
await this.#client.del(this.#leaseKey(job.id));
|
|
70
|
+
}
|
|
71
|
+
async fail(job, error) {
|
|
72
|
+
await this.#removeFromProcessing(job);
|
|
73
|
+
await this.#client.del(this.#leaseKey(job.id));
|
|
74
|
+
job.error = error;
|
|
75
|
+
job.status = "failed";
|
|
76
|
+
await this.#client.rpush(this.#failedKey(), JSON.stringify(job));
|
|
77
|
+
}
|
|
78
|
+
async retry(job) {
|
|
79
|
+
await this.#removeFromProcessing(job);
|
|
80
|
+
await this.#client.del(this.#leaseKey(job.id));
|
|
81
|
+
job.status = "pending";
|
|
82
|
+
await this.#client.rpush(this.#pendingKey(), JSON.stringify(job));
|
|
83
|
+
}
|
|
84
|
+
async recoverStale() {
|
|
85
|
+
const processing = await this.#client.lrange(this.#processingKey(), 0, -1);
|
|
86
|
+
let recovered = 0;
|
|
87
|
+
for (const raw of processing) {
|
|
88
|
+
let parsed;
|
|
89
|
+
try {
|
|
90
|
+
parsed = JSON.parse(raw);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
// Malformed JSON would otherwise sit in processing forever —
|
|
94
|
+
// LREM purges it so the queue makes progress.
|
|
95
|
+
await this.#client.lrem(this.#processingKey(), 1, raw);
|
|
96
|
+
continue;
|
|
97
|
+
}
|
|
98
|
+
if (!isValidJob(parsed)) {
|
|
99
|
+
await this.#client.lrem(this.#processingKey(), 1, raw);
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const lease = await this.#client.get(this.#leaseKey(parsed.id));
|
|
103
|
+
if (lease === null) {
|
|
104
|
+
await this.#client.lrem(this.#processingKey(), 1, raw);
|
|
105
|
+
parsed.status = "pending";
|
|
106
|
+
await this.#client.rpush(this.#pendingKey(), JSON.stringify(parsed));
|
|
107
|
+
recovered++;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
return recovered;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Remove the entry for `job` from the processing list. The string in
|
|
114
|
+
* Redis is whatever pop() pushed, but QueueManager mutates `job` after
|
|
115
|
+
* pop returns (attempts++, status="processing", processedAt, then
|
|
116
|
+
* completed/failed/pending). LREM-ing on `JSON.stringify(job)` would
|
|
117
|
+
* therefore miss every real-world entry. Use the lease — set to the
|
|
118
|
+
* exact raw string at pop() time — and fall back to a list scan when
|
|
119
|
+
* the lease has expired (e.g. recoverStale already handled it).
|
|
120
|
+
*/
|
|
121
|
+
async #removeFromProcessing(job) {
|
|
122
|
+
const stored = await this.#client.get(this.#leaseKey(job.id));
|
|
123
|
+
if (stored !== null) {
|
|
124
|
+
const removed = await this.#client.lrem(this.#processingKey(), 1, stored);
|
|
125
|
+
if (removed > 0)
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
// Lease missing or already-LREM'd entry not found — best-effort scan
|
|
129
|
+
// matches by job id and removes the actual stored representation.
|
|
130
|
+
const items = await this.#client.lrange(this.#processingKey(), 0, -1);
|
|
131
|
+
for (const item of items) {
|
|
132
|
+
let parsed;
|
|
133
|
+
try {
|
|
134
|
+
parsed = JSON.parse(item);
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
139
|
+
if (isValidJob(parsed) && parsed.id === job.id) {
|
|
140
|
+
await this.#client.lrem(this.#processingKey(), 1, item);
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async failed() {
|
|
146
|
+
const raws = await this.#client.lrange(this.#failedKey(), 0, -1);
|
|
147
|
+
return raws
|
|
148
|
+
.map((r) => {
|
|
149
|
+
try {
|
|
150
|
+
const parsed = JSON.parse(r);
|
|
151
|
+
return isValidJob(parsed) ? parsed : null;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
})
|
|
157
|
+
.filter((j) => j !== null);
|
|
158
|
+
}
|
|
159
|
+
async size() {
|
|
160
|
+
return this.#client.llen(this.#pendingKey());
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
//# sourceMappingURL=RedisDriver.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"RedisDriver.js","sourceRoot":"","sources":["../../src/drivers/RedisDriver.ts"],"names":[],"mappings":"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"}
|