@xacos/queue 1.0.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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/QueueManager.d.ts +8 -0
- package/dist/QueueManager.d.ts.map +1 -0
- package/dist/QueueManager.js +29 -0
- package/dist/QueueManager.js.map +1 -0
- package/dist/XJob.d.ts +41 -0
- package/dist/XJob.d.ts.map +1 -0
- package/dist/XJob.js +39 -0
- package/dist/XJob.js.map +1 -0
- package/dist/dispatch.d.ts +10 -0
- package/dist/dispatch.d.ts.map +1 -0
- package/dist/dispatch.js +12 -0
- package/dist/dispatch.js.map +1 -0
- package/dist/dispatch.test.d.ts +2 -0
- package/dist/dispatch.test.d.ts.map +1 -0
- package/dist/dispatch.test.js +16 -0
- package/dist/dispatch.test.js.map +1 -0
- package/dist/drivers/BullMQQueue.d.ts +23 -0
- package/dist/drivers/BullMQQueue.d.ts.map +1 -0
- package/dist/drivers/BullMQQueue.js +67 -0
- package/dist/drivers/BullMQQueue.js.map +1 -0
- package/dist/drivers/MemoryQueue.d.ts +15 -0
- package/dist/drivers/MemoryQueue.d.ts.map +1 -0
- package/dist/drivers/MemoryQueue.js +53 -0
- package/dist/drivers/MemoryQueue.js.map +1 -0
- package/dist/drivers/MemoryQueue.test.d.ts +2 -0
- package/dist/drivers/MemoryQueue.test.d.ts.map +1 -0
- package/dist/drivers/MemoryQueue.test.js +28 -0
- package/dist/drivers/MemoryQueue.test.js.map +1 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/package.json +68 -0
- package/src/QueueManager.ts +38 -0
- package/src/XJob.ts +47 -0
- package/src/dispatch.test.ts +18 -0
- package/src/dispatch.ts +14 -0
- package/src/drivers/BullMQQueue.ts +88 -0
- package/src/drivers/MemoryQueue.test.ts +32 -0
- package/src/drivers/MemoryQueue.ts +65 -0
- package/src/index.ts +5 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 XAOCS Team
|
|
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,17 @@
|
|
|
1
|
+
# @xacos/queue
|
|
2
|
+
|
|
3
|
+
> Part of the [XAOCS Framework](https://xaocs.dev) — full-stack TypeScript, built for AI-native development.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun add @xacos/queue
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Docs
|
|
12
|
+
|
|
13
|
+
Full documentation at [xaocs.dev/docs/queue](https://xaocs.dev/docs/queue).
|
|
14
|
+
|
|
15
|
+
## License
|
|
16
|
+
|
|
17
|
+
MIT © XAOCS Team
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface QueueDriver {
|
|
2
|
+
push(job: import('./XJob').XJob): Promise<void> | void;
|
|
3
|
+
}
|
|
4
|
+
export declare function resolveQueueDriver(): QueueDriver;
|
|
5
|
+
export declare class QueueManager {
|
|
6
|
+
static resolve(): QueueDriver;
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=QueueManager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueManager.d.ts","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,WAAW;IAC1B,IAAI,CAAC,GAAG,EAAE,OAAO,QAAQ,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;CACxD;AAID,wBAAgB,kBAAkB,IAAI,WAAW,CAsBhD;AAED,qBAAa,YAAY;IACvB,MAAM,CAAC,OAAO,IAAI,WAAW;CAG9B"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { MemoryQueue } from './drivers/MemoryQueue';
|
|
2
|
+
let _instance = null;
|
|
3
|
+
export function resolveQueueDriver() {
|
|
4
|
+
if (_instance)
|
|
5
|
+
return _instance;
|
|
6
|
+
const driver = process.env['QUEUE_DRIVER'] ?? 'memory';
|
|
7
|
+
switch (driver) {
|
|
8
|
+
case 'redis': {
|
|
9
|
+
const { BullMQQueue } = require('./drivers/BullMQQueue');
|
|
10
|
+
_instance = new BullMQQueue({
|
|
11
|
+
host: process.env['REDIS_HOST'] ?? 'localhost',
|
|
12
|
+
port: Number(process.env['REDIS_PORT'] ?? 6379),
|
|
13
|
+
password: process.env['REDIS_PASSWORD'],
|
|
14
|
+
});
|
|
15
|
+
break;
|
|
16
|
+
}
|
|
17
|
+
case 'memory':
|
|
18
|
+
default:
|
|
19
|
+
_instance = new MemoryQueue();
|
|
20
|
+
break;
|
|
21
|
+
}
|
|
22
|
+
return _instance;
|
|
23
|
+
}
|
|
24
|
+
export class QueueManager {
|
|
25
|
+
static resolve() {
|
|
26
|
+
return resolveQueueDriver();
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
//# sourceMappingURL=QueueManager.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"QueueManager.js","sourceRoot":"","sources":["../src/QueueManager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AAMpD,IAAI,SAAS,GAAuB,IAAI,CAAC;AAEzC,MAAM,UAAU,kBAAkB;IAChC,IAAI,SAAS;QAAE,OAAO,SAAS,CAAC;IAEhC,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,IAAI,QAAQ,CAAC;IAEvD,QAAQ,MAAM,EAAE,CAAC;QACf,KAAK,OAAO,CAAC,CAAC,CAAC;YACb,MAAM,EAAE,WAAW,EAAE,GAAG,OAAO,CAAC,uBAAuB,CAA2C,CAAC;YACnG,SAAS,GAAG,IAAI,WAAW,CAAC;gBAC1B,IAAI,EAAE,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,WAAW;gBAC9C,IAAI,EAAE,MAAM,CAAC,OAAO,CAAC,GAAG,CAAC,YAAY,CAAC,IAAI,IAAI,CAAC;gBAC/C,QAAQ,EAAE,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC;aACxC,CAAC,CAAC;YACH,MAAM;QACR,CAAC;QACD,KAAK,QAAQ,CAAC;QACd;YACE,SAAS,GAAG,IAAI,WAAW,EAAE,CAAC;YAC9B,MAAM;IACV,CAAC;IAED,OAAO,SAAS,CAAC;AACnB,CAAC;AAED,MAAM,OAAO,YAAY;IACvB,MAAM,CAAC,OAAO;QACZ,OAAO,kBAAkB,EAAE,CAAC;IAC9B,CAAC;CACF"}
|
package/dist/XJob.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all XAOCS background jobs.
|
|
3
|
+
* Extend this class and implement `handle()`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* export class SendWelcomeEmailJob extends XJob {
|
|
7
|
+
* constructor(public userId: number) { super(); }
|
|
8
|
+
*
|
|
9
|
+
* async handle(): Promise<void> {
|
|
10
|
+
* const user = await User.findOrFail(this.userId);
|
|
11
|
+
* await mail.send({ to: user.email, subject: 'Welcome!' });
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export declare abstract class XJob {
|
|
16
|
+
/**
|
|
17
|
+
* The queue this job should be placed on.
|
|
18
|
+
* Defaults to 'default'.
|
|
19
|
+
*/
|
|
20
|
+
static queue: string;
|
|
21
|
+
/**
|
|
22
|
+
* Number of times to retry on failure.
|
|
23
|
+
* Defaults to 3.
|
|
24
|
+
*/
|
|
25
|
+
static retries: number;
|
|
26
|
+
/**
|
|
27
|
+
* Delay in milliseconds before first attempt.
|
|
28
|
+
* Defaults to 0 (immediate).
|
|
29
|
+
*/
|
|
30
|
+
static delay: number;
|
|
31
|
+
/**
|
|
32
|
+
* Implement your job logic here.
|
|
33
|
+
*/
|
|
34
|
+
abstract handle(): Promise<void>;
|
|
35
|
+
/**
|
|
36
|
+
* Called if all retries are exhausted.
|
|
37
|
+
* Override to implement custom failure handling.
|
|
38
|
+
*/
|
|
39
|
+
failed(error: Error): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
//# sourceMappingURL=XJob.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XJob.d.ts","sourceRoot":"","sources":["../src/XJob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,8BAAsB,IAAI;IACxB;;;OAGG;IACH,MAAM,CAAC,KAAK,SAAa;IAEzB;;;OAGG;IACH,MAAM,CAAC,OAAO,SAAK;IAEnB;;;OAGG;IACH,MAAM,CAAC,KAAK,SAAK;IAEjB;;OAEG;IACH,QAAQ,CAAC,MAAM,IAAI,OAAO,CAAC,IAAI,CAAC;IAEhC;;;OAGG;IACG,MAAM,CAAC,KAAK,EAAE,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC;CAG1C"}
|
package/dist/XJob.js
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all XAOCS background jobs.
|
|
3
|
+
* Extend this class and implement `handle()`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* export class SendWelcomeEmailJob extends XJob {
|
|
7
|
+
* constructor(public userId: number) { super(); }
|
|
8
|
+
*
|
|
9
|
+
* async handle(): Promise<void> {
|
|
10
|
+
* const user = await User.findOrFail(this.userId);
|
|
11
|
+
* await mail.send({ to: user.email, subject: 'Welcome!' });
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export class XJob {
|
|
16
|
+
/**
|
|
17
|
+
* The queue this job should be placed on.
|
|
18
|
+
* Defaults to 'default'.
|
|
19
|
+
*/
|
|
20
|
+
static queue = 'default';
|
|
21
|
+
/**
|
|
22
|
+
* Number of times to retry on failure.
|
|
23
|
+
* Defaults to 3.
|
|
24
|
+
*/
|
|
25
|
+
static retries = 3;
|
|
26
|
+
/**
|
|
27
|
+
* Delay in milliseconds before first attempt.
|
|
28
|
+
* Defaults to 0 (immediate).
|
|
29
|
+
*/
|
|
30
|
+
static delay = 0;
|
|
31
|
+
/**
|
|
32
|
+
* Called if all retries are exhausted.
|
|
33
|
+
* Override to implement custom failure handling.
|
|
34
|
+
*/
|
|
35
|
+
async failed(error) {
|
|
36
|
+
console.error(`[XAOCS Queue] Job failed: ${this.constructor.name}`, error.message);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
//# sourceMappingURL=XJob.js.map
|
package/dist/XJob.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"XJob.js","sourceRoot":"","sources":["../src/XJob.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,MAAM,OAAgB,IAAI;IACxB;;;OAGG;IACH,MAAM,CAAC,KAAK,GAAG,SAAS,CAAC;IAEzB;;;OAGG;IACH,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC;IAEnB;;;OAGG;IACH,MAAM,CAAC,KAAK,GAAG,CAAC,CAAC;IAOjB;;;OAGG;IACH,KAAK,CAAC,MAAM,CAAC,KAAY;QACvB,OAAO,CAAC,KAAK,CAAC,6BAA6B,IAAI,CAAC,WAAW,CAAC,IAAI,EAAE,EAAE,KAAK,CAAC,OAAO,CAAC,CAAC;IACrF,CAAC"}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { XJob } from './XJob';
|
|
2
|
+
/**
|
|
3
|
+
* Dispatch a job to the queue.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { dispatch } from '@xacos/queue';
|
|
7
|
+
* await dispatch(new SendWelcomeEmailJob(user.id));
|
|
8
|
+
*/
|
|
9
|
+
export declare function dispatch(job: XJob): Promise<void>;
|
|
10
|
+
//# sourceMappingURL=dispatch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAEnC;;;;;;GAMG;AACH,wBAAsB,QAAQ,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAEvD"}
|
package/dist/dispatch.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { QueueManager } from './QueueManager';
|
|
2
|
+
/**
|
|
3
|
+
* Dispatch a job to the queue.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* import { dispatch } from '@xacos/queue';
|
|
7
|
+
* await dispatch(new SendWelcomeEmailJob(user.id));
|
|
8
|
+
*/
|
|
9
|
+
export async function dispatch(job) {
|
|
10
|
+
await QueueManager.resolve().push(job);
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=dispatch.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.js","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,YAAY,EAAE,MAAM,gBAAgB,CAAC;AAG9C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,QAAQ,CAAC,GAAS;IACtC,MAAM,YAAY,CAAC,OAAO,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;AACzC,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.test.d.ts","sourceRoot":"","sources":["../src/dispatch.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { dispatch } from './dispatch';
|
|
3
|
+
import { XJob } from './XJob';
|
|
4
|
+
let handled = false;
|
|
5
|
+
class SimpleJob extends XJob {
|
|
6
|
+
async handle() { handled = true; }
|
|
7
|
+
}
|
|
8
|
+
describe('dispatch', () => {
|
|
9
|
+
it('dispatches a job and executes it', async () => {
|
|
10
|
+
process.env['QUEUE_DRIVER'] = 'memory';
|
|
11
|
+
await dispatch(new SimpleJob());
|
|
12
|
+
await new Promise(r => setTimeout(r, 10));
|
|
13
|
+
expect(handled).toBe(true);
|
|
14
|
+
});
|
|
15
|
+
});
|
|
16
|
+
//# sourceMappingURL=dispatch.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"dispatch.test.js","sourceRoot":"","sources":["../src/dispatch.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAE9B,IAAI,OAAO,GAAG,KAAK,CAAC;AACpB,MAAM,SAAU,SAAQ,IAAI;IAC1B,KAAK,CAAC,MAAM,KAAK,OAAO,GAAG,IAAI,CAAC,CAAC,CAAC;CACnC;AAED,QAAQ,CAAC,UAAU,EAAE,GAAG,EAAE;IACxB,EAAE,CAAC,kCAAkC,EAAE,KAAK,IAAI,EAAE;QAChD,OAAO,CAAC,GAAG,CAAC,cAAc,CAAC,GAAG,QAAQ,CAAC;QACvC,MAAM,QAAQ,CAAC,IAAI,SAAS,EAAE,CAAC,CAAC;QAChC,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Worker } from 'bullmq';
|
|
2
|
+
import type { XJob } from '../XJob';
|
|
3
|
+
export interface BullMQConfig {
|
|
4
|
+
host?: string | undefined;
|
|
5
|
+
port?: number | undefined;
|
|
6
|
+
password?: string | undefined;
|
|
7
|
+
}
|
|
8
|
+
export declare class BullMQQueue {
|
|
9
|
+
private queues;
|
|
10
|
+
private workers;
|
|
11
|
+
private config;
|
|
12
|
+
constructor(config?: BullMQConfig);
|
|
13
|
+
private getConnection;
|
|
14
|
+
private getOrCreateQueue;
|
|
15
|
+
push(job: XJob): Promise<void>;
|
|
16
|
+
/**
|
|
17
|
+
* Start a worker for the given queue name.
|
|
18
|
+
* Workers process jobs in separate processes in production.
|
|
19
|
+
*/
|
|
20
|
+
startWorker(queueName: string, jobClasses: Array<new (...args: unknown[]) => XJob>): Worker;
|
|
21
|
+
close(): Promise<void>;
|
|
22
|
+
}
|
|
23
|
+
//# sourceMappingURL=BullMQQueue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BullMQQueue.d.ts","sourceRoot":"","sources":["../../src/drivers/BullMQQueue.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAuB,MAAM,QAAQ,CAAC;AAC5D,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAEpC,MAAM,WAAW,YAAY;IAC3B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC1B,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CAC/B;AAGD,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAA4B;IAC1C,OAAO,CAAC,OAAO,CAA6B;IAC5C,OAAO,CAAC,MAAM,CAAe;gBAEjB,MAAM,GAAE,YAAiB;IAIrC,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,gBAAgB;IAalB,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;IAcpC;;;OAGG;IACH,WAAW,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,KAAK,CAAC,KAAK,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC,GAAG,MAAM;IAwBrF,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC;CAI7B"}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { Queue, Worker } from 'bullmq';
|
|
2
|
+
export class BullMQQueue {
|
|
3
|
+
queues = new Map();
|
|
4
|
+
workers = new Map();
|
|
5
|
+
config;
|
|
6
|
+
constructor(config = {}) {
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
getConnection() {
|
|
10
|
+
return {
|
|
11
|
+
host: this.config.host ?? 'localhost',
|
|
12
|
+
port: this.config.port ?? 6379,
|
|
13
|
+
password: this.config.password,
|
|
14
|
+
};
|
|
15
|
+
}
|
|
16
|
+
getOrCreateQueue(name) {
|
|
17
|
+
if (!this.queues.has(name)) {
|
|
18
|
+
this.queues.set(name, new Queue(name, {
|
|
19
|
+
connection: this.getConnection(),
|
|
20
|
+
defaultJobOptions: {
|
|
21
|
+
attempts: 3,
|
|
22
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
23
|
+
},
|
|
24
|
+
}));
|
|
25
|
+
}
|
|
26
|
+
return this.queues.get(name);
|
|
27
|
+
}
|
|
28
|
+
async push(job) {
|
|
29
|
+
const JobClass = job.constructor;
|
|
30
|
+
const queueName = JobClass.queue;
|
|
31
|
+
const queue = this.getOrCreateQueue(queueName);
|
|
32
|
+
const jobName = JobClass.name;
|
|
33
|
+
const payload = JSON.stringify(job);
|
|
34
|
+
await queue.add(jobName, { jobName, payload }, {
|
|
35
|
+
delay: JobClass.delay,
|
|
36
|
+
attempts: JobClass.retries + 1,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Start a worker for the given queue name.
|
|
41
|
+
* Workers process jobs in separate processes in production.
|
|
42
|
+
*/
|
|
43
|
+
startWorker(queueName, jobClasses) {
|
|
44
|
+
// Build a registry: jobName → constructor
|
|
45
|
+
const registry = new Map();
|
|
46
|
+
for (const JobClass of jobClasses) {
|
|
47
|
+
registry.set(JobClass.name, JobClass);
|
|
48
|
+
}
|
|
49
|
+
const worker = new Worker(queueName, async (bullJob) => {
|
|
50
|
+
const { jobName, payload } = bullJob.data;
|
|
51
|
+
const JobClass = registry.get(jobName);
|
|
52
|
+
if (!JobClass)
|
|
53
|
+
throw new Error(`[XAOCS Queue] Unknown job: ${jobName}`);
|
|
54
|
+
const instance = Object.assign(new JobClass(), JSON.parse(payload));
|
|
55
|
+
await instance.handle();
|
|
56
|
+
}, { connection: this.getConnection() });
|
|
57
|
+
this.workers.set(queueName, worker);
|
|
58
|
+
return worker;
|
|
59
|
+
}
|
|
60
|
+
async close() {
|
|
61
|
+
for (const worker of this.workers.values())
|
|
62
|
+
await worker.close();
|
|
63
|
+
for (const queue of this.queues.values())
|
|
64
|
+
await queue.close();
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
//# sourceMappingURL=BullMQQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"BullMQQueue.js","sourceRoot":"","sources":["../../src/drivers/BullMQQueue.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { XJob } from '../XJob';
|
|
2
|
+
export declare class MemoryQueue {
|
|
3
|
+
private queues;
|
|
4
|
+
private running;
|
|
5
|
+
/**
|
|
6
|
+
* Add a job to the queue.
|
|
7
|
+
*/
|
|
8
|
+
push(job: XJob): void;
|
|
9
|
+
private processNext;
|
|
10
|
+
/**
|
|
11
|
+
* Get the number of pending jobs across all queues.
|
|
12
|
+
*/
|
|
13
|
+
pending(): number;
|
|
14
|
+
}
|
|
15
|
+
//# sourceMappingURL=MemoryQueue.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryQueue.d.ts","sourceRoot":"","sources":["../../src/drivers/MemoryQueue.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAQpC,qBAAa,WAAW;IACtB,OAAO,CAAC,MAAM,CAAkC;IAChD,OAAO,CAAC,OAAO,CAAS;IAExB;;OAEG;IACH,IAAI,CAAC,GAAG,EAAE,IAAI,GAAG,IAAI;YAgBP,WAAW;IAsBzB;;OAEG;IACH,OAAO,IAAI,MAAM;CAOlB"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export class MemoryQueue {
|
|
2
|
+
queues = new Map();
|
|
3
|
+
running = false;
|
|
4
|
+
/**
|
|
5
|
+
* Add a job to the queue.
|
|
6
|
+
*/
|
|
7
|
+
push(job) {
|
|
8
|
+
const queueName = job.constructor.queue;
|
|
9
|
+
if (!this.queues.has(queueName)) {
|
|
10
|
+
this.queues.set(queueName, []);
|
|
11
|
+
}
|
|
12
|
+
this.queues.get(queueName).push({
|
|
13
|
+
job,
|
|
14
|
+
retries: 0,
|
|
15
|
+
maxRetries: job.constructor.retries,
|
|
16
|
+
});
|
|
17
|
+
if (!this.running) {
|
|
18
|
+
void this.processNext();
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
async processNext() {
|
|
22
|
+
this.running = true;
|
|
23
|
+
for (const [, queue] of this.queues) {
|
|
24
|
+
while (queue.length > 0) {
|
|
25
|
+
const item = queue.shift();
|
|
26
|
+
try {
|
|
27
|
+
await item.job.handle();
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
if (item.retries < item.maxRetries) {
|
|
31
|
+
item.retries++;
|
|
32
|
+
queue.push(item); // re-queue with incremented retries
|
|
33
|
+
}
|
|
34
|
+
else {
|
|
35
|
+
await item.job.failed(error);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
this.running = false;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Get the number of pending jobs across all queues.
|
|
44
|
+
*/
|
|
45
|
+
pending() {
|
|
46
|
+
let total = 0;
|
|
47
|
+
for (const queue of this.queues.values()) {
|
|
48
|
+
total += queue.length;
|
|
49
|
+
}
|
|
50
|
+
return total;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=MemoryQueue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryQueue.js","sourceRoot":"","sources":["../../src/drivers/MemoryQueue.ts"],"names":[],"mappings":"AAQA,MAAM,OAAO,WAAW;IACd,MAAM,GAAG,IAAI,GAAG,EAAuB,CAAC;IACxC,OAAO,GAAG,KAAK,CAAC;IAExB;;OAEG;IACH,IAAI,CAAC,GAAS;QACZ,MAAM,SAAS,GAAI,GAAG,CAAC,WAA2B,CAAC,KAAK,CAAC;QACzD,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,CAAC;YAChC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;QACjC,CAAC;QACD,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,SAAS,CAAE,CAAC,IAAI,CAAC;YAC/B,GAAG;YACH,OAAO,EAAE,CAAC;YACV,UAAU,EAAG,GAAG,CAAC,WAA2B,CAAC,OAAO;SACrD,CAAC,CAAC;QAEH,IAAI,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC;YAClB,KAAK,IAAI,CAAC,WAAW,EAAE,CAAC;QAC1B,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,WAAW;QACvB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QAEpB,KAAK,MAAM,CAAC,EAAE,KAAK,CAAC,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;YACpC,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBACxB,MAAM,IAAI,GAAG,KAAK,CAAC,KAAK,EAAG,CAAC;gBAC5B,IAAI,CAAC;oBACH,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,EAAE,CAAC;gBAC1B,CAAC;gBAAC,OAAO,KAAK,EAAE,CAAC;oBACf,IAAI,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC,UAAU,EAAE,CAAC;wBACnC,IAAI,CAAC,OAAO,EAAE,CAAC;wBACf,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,oCAAoC;oBACxD,CAAC;yBAAM,CAAC;wBACN,MAAM,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,KAAc,CAAC,CAAC;oBACxC,CAAC;gBACH,CAAC;YACH,CAAC;QACH,CAAC;QAED,IAAI,CAAC,OAAO,GAAG,KAAK,CAAC;IACvB,CAAC;IAED;;OAEG;IACH,OAAO;QACL,IAAI,KAAK,GAAG,CAAC,CAAC;QACd,KAAK,MAAM,KAAK,IAAI,IAAI,CAAC,MAAM,CAAC,MAAM,EAAE,EAAE,CAAC;YACzC,KAAK,IAAI,KAAK,CAAC,MAAM,CAAC;QACxB,CAAC;QACD,OAAO,KAAK,CAAC;IACf,CAAC;CACF"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryQueue.test.d.ts","sourceRoot":"","sources":["../../src/drivers/MemoryQueue.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { MemoryQueue } from './MemoryQueue';
|
|
3
|
+
import { XJob } from '../XJob';
|
|
4
|
+
class TestJob extends XJob {
|
|
5
|
+
static executed = false;
|
|
6
|
+
async handle() { TestJob.executed = true; }
|
|
7
|
+
}
|
|
8
|
+
class FailingJob extends XJob {
|
|
9
|
+
static retries = 0;
|
|
10
|
+
static failedCalled = false;
|
|
11
|
+
async handle() { throw new Error('intentional fail'); }
|
|
12
|
+
async failed(_err) { FailingJob.failedCalled = true; }
|
|
13
|
+
}
|
|
14
|
+
describe('MemoryQueue', () => {
|
|
15
|
+
it('executes a job', async () => {
|
|
16
|
+
const queue = new MemoryQueue();
|
|
17
|
+
queue.push(new TestJob());
|
|
18
|
+
await new Promise(r => setTimeout(r, 10)); // let queue process
|
|
19
|
+
expect(TestJob.executed).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
it('calls failed() after exhausting retries', async () => {
|
|
22
|
+
const queue = new MemoryQueue();
|
|
23
|
+
queue.push(new FailingJob());
|
|
24
|
+
await new Promise(r => setTimeout(r, 10));
|
|
25
|
+
expect(FailingJob.failedCalled).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
//# sourceMappingURL=MemoryQueue.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"MemoryQueue.test.js","sourceRoot":"","sources":["../../src/drivers/MemoryQueue.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,UAAU,CAAC;AAChD,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAC5C,OAAO,EAAE,IAAI,EAAE,MAAM,SAAS,CAAC;AAE/B,MAAM,OAAQ,SAAQ,IAAI;IACxB,MAAM,CAAC,QAAQ,GAAG,KAAK,CAAC;IACxB,KAAK,CAAC,MAAM,KAAK,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC,CAAC;;AAG7C,MAAM,UAAW,SAAQ,IAAI;IAC3B,MAAM,CAAC,OAAO,GAAG,CAAC,CAAC;IACnB,MAAM,CAAC,YAAY,GAAG,KAAK,CAAC;IAC5B,KAAK,CAAC,MAAM,KAAK,MAAM,IAAI,KAAK,CAAC,kBAAkB,CAAC,CAAC,CAAC,CAAC;IACvD,KAAK,CAAC,MAAM,CAAC,IAAW,IAAI,UAAU,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC,CAAC;;AAG/D,QAAQ,CAAC,aAAa,EAAE,GAAG,EAAE;IAC3B,EAAE,CAAC,gBAAgB,EAAE,KAAK,IAAI,EAAE;QAC9B,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,IAAI,OAAO,EAAE,CAAC,CAAC;QAC1B,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,oBAAoB;QAC/D,MAAM,CAAC,OAAO,CAAC,QAAQ,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IACtC,CAAC,CAAC,CAAC;IAEH,EAAE,CAAC,yCAAyC,EAAE,KAAK,IAAI,EAAE;QACvD,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC;QAChC,KAAK,CAAC,IAAI,CAAC,IAAI,UAAU,EAAE,CAAC,CAAC;QAC7B,MAAM,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,UAAU,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC;QAC1C,MAAM,CAAC,UAAU,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;IAC7C,CAAC,CAAC,CAAC;AACL,CAAC,CAAC,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,QAAQ,CAAC;AAC9B,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACtC,OAAO,EAAE,WAAW,EAAE,MAAM,uBAAuB,CAAC;AACpD,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,MAAM,gBAAgB,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@xacos/queue",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
}
|
|
12
|
+
},
|
|
13
|
+
"dependencies": {
|
|
14
|
+
"@xacos/config": "workspace:*",
|
|
15
|
+
"@xacos/shared": "workspace:*",
|
|
16
|
+
"bullmq": "^5.76.8"
|
|
17
|
+
},
|
|
18
|
+
"devDependencies": {
|
|
19
|
+
"typescript": "^5.x",
|
|
20
|
+
"bun-types": "^1.3.12"
|
|
21
|
+
},
|
|
22
|
+
"scripts": {
|
|
23
|
+
"build": "tsc",
|
|
24
|
+
"dev": "tsc --watch",
|
|
25
|
+
"test": "bun test src",
|
|
26
|
+
"type-check": "tsc --noEmit"
|
|
27
|
+
},
|
|
28
|
+
"license": "MIT",
|
|
29
|
+
"author": "XAOCS Team",
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "https://github.com/zoherr/xaocs.git",
|
|
33
|
+
"directory": "packages/queue"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://xaocs.dev",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/zoherr/xaocs/issues"
|
|
38
|
+
},
|
|
39
|
+
"keywords": [
|
|
40
|
+
"xaocs",
|
|
41
|
+
"xacos",
|
|
42
|
+
"framework",
|
|
43
|
+
"typescript",
|
|
44
|
+
"fullstack",
|
|
45
|
+
"fastify",
|
|
46
|
+
"react",
|
|
47
|
+
"queue",
|
|
48
|
+
"bullmq",
|
|
49
|
+
"jobs",
|
|
50
|
+
"workers",
|
|
51
|
+
"redis"
|
|
52
|
+
],
|
|
53
|
+
"files": [
|
|
54
|
+
"dist",
|
|
55
|
+
"src",
|
|
56
|
+
"README.md",
|
|
57
|
+
"LICENSE"
|
|
58
|
+
],
|
|
59
|
+
"publishConfig": {
|
|
60
|
+
"access": "public",
|
|
61
|
+
"registry": "https://registry.npmjs.org",
|
|
62
|
+
"provenance": false
|
|
63
|
+
},
|
|
64
|
+
"engines": {
|
|
65
|
+
"bun": ">=1.0.0",
|
|
66
|
+
"node": ">=18.0.0"
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { MemoryQueue } from './drivers/MemoryQueue';
|
|
2
|
+
|
|
3
|
+
export interface QueueDriver {
|
|
4
|
+
push(job: import('./XJob').XJob): Promise<void> | void;
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
let _instance: QueueDriver | null = null;
|
|
8
|
+
|
|
9
|
+
export function resolveQueueDriver(): QueueDriver {
|
|
10
|
+
if (_instance) return _instance;
|
|
11
|
+
|
|
12
|
+
const driver = process.env['QUEUE_DRIVER'] ?? 'memory';
|
|
13
|
+
|
|
14
|
+
switch (driver) {
|
|
15
|
+
case 'redis': {
|
|
16
|
+
const { BullMQQueue } = require('./drivers/BullMQQueue') as typeof import('./drivers/BullMQQueue');
|
|
17
|
+
_instance = new BullMQQueue({
|
|
18
|
+
host: process.env['REDIS_HOST'] ?? 'localhost',
|
|
19
|
+
port: Number(process.env['REDIS_PORT'] ?? 6379),
|
|
20
|
+
password: process.env['REDIS_PASSWORD'],
|
|
21
|
+
});
|
|
22
|
+
break;
|
|
23
|
+
}
|
|
24
|
+
case 'memory':
|
|
25
|
+
default:
|
|
26
|
+
_instance = new MemoryQueue();
|
|
27
|
+
break;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return _instance;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export class QueueManager {
|
|
34
|
+
static resolve(): QueueDriver {
|
|
35
|
+
return resolveQueueDriver();
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
package/src/XJob.ts
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base class for all XAOCS background jobs.
|
|
3
|
+
* Extend this class and implement `handle()`.
|
|
4
|
+
*
|
|
5
|
+
* @example
|
|
6
|
+
* export class SendWelcomeEmailJob extends XJob {
|
|
7
|
+
* constructor(public userId: number) { super(); }
|
|
8
|
+
*
|
|
9
|
+
* async handle(): Promise<void> {
|
|
10
|
+
* const user = await User.findOrFail(this.userId);
|
|
11
|
+
* await mail.send({ to: user.email, subject: 'Welcome!' });
|
|
12
|
+
* }
|
|
13
|
+
* }
|
|
14
|
+
*/
|
|
15
|
+
export abstract class XJob {
|
|
16
|
+
/**
|
|
17
|
+
* The queue this job should be placed on.
|
|
18
|
+
* Defaults to 'default'.
|
|
19
|
+
*/
|
|
20
|
+
static queue = 'default';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Number of times to retry on failure.
|
|
24
|
+
* Defaults to 3.
|
|
25
|
+
*/
|
|
26
|
+
static retries = 3;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Delay in milliseconds before first attempt.
|
|
30
|
+
* Defaults to 0 (immediate).
|
|
31
|
+
*/
|
|
32
|
+
static delay = 0;
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Implement your job logic here.
|
|
36
|
+
*/
|
|
37
|
+
abstract handle(): Promise<void>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Called if all retries are exhausted.
|
|
41
|
+
* Override to implement custom failure handling.
|
|
42
|
+
*/
|
|
43
|
+
async failed(error: Error): Promise<void> {
|
|
44
|
+
console.error(`[XAOCS Queue] Job failed: ${this.constructor.name}`, error.message);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { dispatch } from './dispatch';
|
|
3
|
+
import { XJob } from './XJob';
|
|
4
|
+
|
|
5
|
+
let handled = false;
|
|
6
|
+
class SimpleJob extends XJob {
|
|
7
|
+
async handle() { handled = true; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
describe('dispatch', () => {
|
|
11
|
+
it('dispatches a job and executes it', async () => {
|
|
12
|
+
process.env['QUEUE_DRIVER'] = 'memory';
|
|
13
|
+
await dispatch(new SimpleJob());
|
|
14
|
+
await new Promise(r => setTimeout(r, 10));
|
|
15
|
+
expect(handled).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
package/src/dispatch.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { QueueManager } from './QueueManager';
|
|
2
|
+
import type { XJob } from './XJob';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Dispatch a job to the queue.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* import { dispatch } from '@xacos/queue';
|
|
9
|
+
* await dispatch(new SendWelcomeEmailJob(user.id));
|
|
10
|
+
*/
|
|
11
|
+
export async function dispatch(job: XJob): Promise<void> {
|
|
12
|
+
await QueueManager.resolve().push(job);
|
|
13
|
+
}
|
|
14
|
+
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { Queue, Worker, type Job as BullJob } from 'bullmq';
|
|
2
|
+
import type { XJob } from '../XJob';
|
|
3
|
+
|
|
4
|
+
export interface BullMQConfig {
|
|
5
|
+
host?: string | undefined;
|
|
6
|
+
port?: number | undefined;
|
|
7
|
+
password?: string | undefined;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
export class BullMQQueue {
|
|
12
|
+
private queues = new Map<string, Queue>();
|
|
13
|
+
private workers = new Map<string, Worker>();
|
|
14
|
+
private config: BullMQConfig;
|
|
15
|
+
|
|
16
|
+
constructor(config: BullMQConfig = {}) {
|
|
17
|
+
this.config = config;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
private getConnection() {
|
|
21
|
+
return {
|
|
22
|
+
host: this.config.host ?? 'localhost',
|
|
23
|
+
port: this.config.port ?? 6379,
|
|
24
|
+
password: this.config.password,
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private getOrCreateQueue(name: string): Queue {
|
|
29
|
+
if (!this.queues.has(name)) {
|
|
30
|
+
this.queues.set(name, new Queue(name, {
|
|
31
|
+
connection: this.getConnection(),
|
|
32
|
+
defaultJobOptions: {
|
|
33
|
+
attempts: 3,
|
|
34
|
+
backoff: { type: 'exponential', delay: 1000 },
|
|
35
|
+
},
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
return this.queues.get(name)!;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async push(job: XJob): Promise<void> {
|
|
42
|
+
const JobClass = job.constructor as typeof XJob;
|
|
43
|
+
const queueName = JobClass.queue;
|
|
44
|
+
const queue = this.getOrCreateQueue(queueName);
|
|
45
|
+
|
|
46
|
+
const jobName = JobClass.name;
|
|
47
|
+
const payload = JSON.stringify(job);
|
|
48
|
+
|
|
49
|
+
await queue.add(jobName, { jobName, payload }, {
|
|
50
|
+
delay: JobClass.delay,
|
|
51
|
+
attempts: JobClass.retries + 1,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Start a worker for the given queue name.
|
|
57
|
+
* Workers process jobs in separate processes in production.
|
|
58
|
+
*/
|
|
59
|
+
startWorker(queueName: string, jobClasses: Array<new (...args: unknown[]) => XJob>): Worker {
|
|
60
|
+
// Build a registry: jobName → constructor
|
|
61
|
+
const registry = new Map<string, new (...args: unknown[]) => XJob>();
|
|
62
|
+
for (const JobClass of jobClasses) {
|
|
63
|
+
registry.set(JobClass.name, JobClass);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const worker = new Worker(
|
|
67
|
+
queueName,
|
|
68
|
+
async (bullJob: BullJob) => {
|
|
69
|
+
const { jobName, payload } = bullJob.data as { jobName: string; payload: string };
|
|
70
|
+
const JobClass = registry.get(jobName);
|
|
71
|
+
if (!JobClass) throw new Error(`[XAOCS Queue] Unknown job: ${jobName}`);
|
|
72
|
+
|
|
73
|
+
const instance = Object.assign(new JobClass(), JSON.parse(payload));
|
|
74
|
+
await instance.handle();
|
|
75
|
+
},
|
|
76
|
+
{ connection: this.getConnection() }
|
|
77
|
+
);
|
|
78
|
+
|
|
79
|
+
this.workers.set(queueName, worker);
|
|
80
|
+
return worker;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async close(): Promise<void> {
|
|
84
|
+
for (const worker of this.workers.values()) await worker.close();
|
|
85
|
+
for (const queue of this.queues.values()) await queue.close();
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { describe, it, expect } from 'bun:test';
|
|
2
|
+
import { MemoryQueue } from './MemoryQueue';
|
|
3
|
+
import { XJob } from '../XJob';
|
|
4
|
+
|
|
5
|
+
class TestJob extends XJob {
|
|
6
|
+
static executed = false;
|
|
7
|
+
async handle() { TestJob.executed = true; }
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
class FailingJob extends XJob {
|
|
11
|
+
static retries = 0;
|
|
12
|
+
static failedCalled = false;
|
|
13
|
+
async handle() { throw new Error('intentional fail'); }
|
|
14
|
+
async failed(_err: Error) { FailingJob.failedCalled = true; }
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
describe('MemoryQueue', () => {
|
|
18
|
+
it('executes a job', async () => {
|
|
19
|
+
const queue = new MemoryQueue();
|
|
20
|
+
queue.push(new TestJob());
|
|
21
|
+
await new Promise(r => setTimeout(r, 10)); // let queue process
|
|
22
|
+
expect(TestJob.executed).toBe(true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it('calls failed() after exhausting retries', async () => {
|
|
26
|
+
const queue = new MemoryQueue();
|
|
27
|
+
queue.push(new FailingJob());
|
|
28
|
+
await new Promise(r => setTimeout(r, 10));
|
|
29
|
+
expect(FailingJob.failedCalled).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
});
|
|
32
|
+
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { XJob } from '../XJob';
|
|
2
|
+
|
|
3
|
+
interface QueuedJob {
|
|
4
|
+
job: XJob;
|
|
5
|
+
retries: number;
|
|
6
|
+
maxRetries: number;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export class MemoryQueue {
|
|
10
|
+
private queues = new Map<string, QueuedJob[]>();
|
|
11
|
+
private running = false;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Add a job to the queue.
|
|
15
|
+
*/
|
|
16
|
+
push(job: XJob): void {
|
|
17
|
+
const queueName = (job.constructor as typeof XJob).queue;
|
|
18
|
+
if (!this.queues.has(queueName)) {
|
|
19
|
+
this.queues.set(queueName, []);
|
|
20
|
+
}
|
|
21
|
+
this.queues.get(queueName)!.push({
|
|
22
|
+
job,
|
|
23
|
+
retries: 0,
|
|
24
|
+
maxRetries: (job.constructor as typeof XJob).retries,
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
if (!this.running) {
|
|
28
|
+
void this.processNext();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
private async processNext(): Promise<void> {
|
|
33
|
+
this.running = true;
|
|
34
|
+
|
|
35
|
+
for (const [, queue] of this.queues) {
|
|
36
|
+
while (queue.length > 0) {
|
|
37
|
+
const item = queue.shift()!;
|
|
38
|
+
try {
|
|
39
|
+
await item.job.handle();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
if (item.retries < item.maxRetries) {
|
|
42
|
+
item.retries++;
|
|
43
|
+
queue.push(item); // re-queue with incremented retries
|
|
44
|
+
} else {
|
|
45
|
+
await item.job.failed(error as Error);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
this.running = false;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Get the number of pending jobs across all queues.
|
|
56
|
+
*/
|
|
57
|
+
pending(): number {
|
|
58
|
+
let total = 0;
|
|
59
|
+
for (const queue of this.queues.values()) {
|
|
60
|
+
total += queue.length;
|
|
61
|
+
}
|
|
62
|
+
return total;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|