@skalfa/skalfa-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/bun.lock ADDED
@@ -0,0 +1,99 @@
1
+ {
2
+ "lockfileVersion": 1,
3
+ "workspaces": {
4
+ "": {
5
+ "name": "@skalfa/skalfa-queue",
6
+ "dependencies": {
7
+ "@skalfa/skalfa-api-core": "file:../skalfa-api-core",
8
+ "@skalfa/skalfa-redis": "file:../skalfa-redis",
9
+ },
10
+ "devDependencies": {
11
+ "@types/node": "^26.0.0",
12
+ "typescript": "^6.0.3",
13
+ },
14
+ },
15
+ },
16
+ "packages": {
17
+ "@borewit/text-codec": ["@borewit/text-codec@0.2.2", "", {}, "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ=="],
18
+
19
+ "@ioredis/commands": ["@ioredis/commands@1.10.0", "", {}, "sha512-UmeW7z4LfctwoQ5wkhVzgq8tXkreED2xZGpX+Bg+zA+WJFZCT6c062AfCK/Dfk81xZnnwdhJCUMkitihRaoC2Q=="],
20
+
21
+ "@sinclair/typebox": ["@sinclair/typebox@0.34.49", "", {}, "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A=="],
22
+
23
+ "@skalfa/skalfa-api-core": ["@skalfa/skalfa-api-core@file:../skalfa-api-core", { "dependencies": { "@skalfa/skalfa-orm": "file:../skalfa-orm", "bcrypt": "^6.0.0", "commander": "^12.1.0", "dotenv": "^17.2.2", "elysia": "latest", "nodemailer": "^7.0.9", "validator": "^13.15.15" }, "devDependencies": { "@types/bcrypt": "^6.0.0", "@types/node": "^26.0.0", "@types/nodemailer": "^7.0.2", "@types/validator": "^13.15.3", "bun-types": "latest", "typescript": "^6.0.3" } }],
24
+
25
+ "@skalfa/skalfa-redis": ["@skalfa/skalfa-redis@file:../skalfa-redis", { "dependencies": { "ioredis": "^5.4.1" }, "devDependencies": { "@types/node": "^26.0.0", "typescript": "^6.0.3" } }],
26
+
27
+ "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="],
28
+
29
+ "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="],
30
+
31
+ "@types/bcrypt": ["@types/bcrypt@6.0.0", "", { "dependencies": { "@types/node": "*" } }, "sha512-/oJGukuH3D2+D+3H4JWLaAsJ/ji86dhRidzZ/Od7H/i8g+aCmvkeCc6Ni/f9uxGLSQVCRZkX2/lqEFG2BvWtlQ=="],
32
+
33
+ "@types/node": ["@types/node@26.0.0", "", { "dependencies": { "undici-types": "~8.3.0" } }, "sha512-vf2YFi1iY9lHGwNJMs01biZFbKJkrZR1T6/MlzjhJLPdntOHLhTrDSnSVcdtvjihi4VQNlrFRIxLsDBlQpAipA=="],
34
+
35
+ "@types/nodemailer": ["@types/nodemailer@7.0.12", "", { "dependencies": { "@types/node": "*" } }, "sha512-80vKwiIsVSyFA1rRovH59jNPLBOuc6dRZIHEu40gXTkBkZnQv8vog1xSGEb9j5q/tdMAs5ivvDR2pLTU0hGHXA=="],
36
+
37
+ "@types/validator": ["@types/validator@13.15.10", "", {}, "sha512-T8L6i7wCuyoK8A/ZeLYt1+q0ty3Zb9+qbSSvrIVitzT3YjZqkTZ40IbRsPanlB4h1QB3JVL1SYCdR6ngtFYcuA=="],
38
+
39
+ "bcrypt": ["bcrypt@6.0.0", "", { "dependencies": { "node-addon-api": "^8.3.0", "node-gyp-build": "^4.8.4" } }, "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg=="],
40
+
41
+ "bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
42
+
43
+ "cluster-key-slot": ["cluster-key-slot@1.1.1", "", {}, "sha512-rwHwUfXL40Chm1r08yrhU3qpUvdVlgkKNeyeGPOxnW8/SyVDvgRaed/Uz54AqWNaTCAThlj6QAs3TZcKI0xDEw=="],
44
+
45
+ "commander": ["commander@12.1.0", "", {}, "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA=="],
46
+
47
+ "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="],
48
+
49
+ "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
50
+
51
+ "denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
52
+
53
+ "dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
54
+
55
+ "elysia": ["elysia@1.4.29", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-GwMRGGwSdjfPt+w3LA0fqTuYJtS8uVRJicvoar98/HrO5qdFKDc9CwjIb6Kja+v39lkY+58hr2JvdR9jQzlUuA=="],
56
+
57
+ "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="],
58
+
59
+ "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="],
60
+
61
+ "file-type": ["file-type@22.0.1", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.5", "token-types": "^6.1.2", "uint8array-extras": "^1.5.0" } }, "sha512-ww5Mhre0EE+jmBvOXTmXAbEMuZE7uX4a3+oRCQFNj8w++g3ev913N6tXQz0XTXbueQ5TWQfm6BdaViEHHn8bhA=="],
62
+
63
+ "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
64
+
65
+ "ioredis": ["ioredis@5.11.1", "", { "dependencies": { "@ioredis/commands": "1.10.0", "cluster-key-slot": "1.1.1", "debug": "4.4.3", "denque": "2.1.0", "redis-errors": "1.2.0", "redis-parser": "3.0.0", "standard-as-callback": "2.1.0" } }, "sha512-ehuGcf94bQXhfagULNXrJdfnWO38v070jxSx/qE87Kjzmu2fU7ro5EFAb+OPituLqgfyuQaym5DlrNydW2sJ9A=="],
66
+
67
+ "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="],
68
+
69
+ "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
70
+
71
+ "node-addon-api": ["node-addon-api@8.8.0", "", {}, "sha512-c5Ko1fZJIJmzhFIkhRN76WTq+fC6tWnGy9CXA0fA+XygsWZmEwG8vmbkNqxMyoaa0Tin4djul49NzdVcJJcjeA=="],
72
+
73
+ "node-gyp-build": ["node-gyp-build@4.8.4", "", { "bin": { "node-gyp-build": "bin.js", "node-gyp-build-optional": "optional.js", "node-gyp-build-test": "build-test.js" } }, "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ=="],
74
+
75
+ "nodemailer": ["nodemailer@7.0.13", "", {}, "sha512-PNDFSJdP+KFgdsG3ZzMXCgquO7I6McjY2vlqILjtJd0hy8wEvtugS9xKRF2NWlPNGxvLCXlTNIae4serI7dinw=="],
76
+
77
+ "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="],
78
+
79
+ "redis-errors": ["redis-errors@1.2.0", "", {}, "sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w=="],
80
+
81
+ "redis-parser": ["redis-parser@3.0.0", "", { "dependencies": { "redis-errors": "^1.0.0" } }, "sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A=="],
82
+
83
+ "standard-as-callback": ["standard-as-callback@2.1.0", "", {}, "sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A=="],
84
+
85
+ "strtok3": ["strtok3@10.3.5", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA=="],
86
+
87
+ "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="],
88
+
89
+ "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="],
90
+
91
+ "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="],
92
+
93
+ "undici-types": ["undici-types@8.3.0", "", {}, "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ=="],
94
+
95
+ "validator": ["validator@13.15.35", "", {}, "sha512-TQ5pAGhd5whStmqWvYF4OjQROlmv9SMFVt37qoCBdqRffuuklWYQlCNnEs2ZaIBD1kZRNnikiZOS1eqgkar0iw=="],
96
+
97
+ "@skalfa/skalfa-api-core/@skalfa/skalfa-orm": ["@skalfa/skalfa-orm@file:..\\skalfa-orm", {}],
98
+ }
99
+ }
@@ -0,0 +1,13 @@
1
+ export declare const queue: {
2
+ key: (name: string) => string;
3
+ keyFailed: (name: string) => string;
4
+ add(name: string, jobPayload: any, uniq?: string): Promise<string>;
5
+ addFailed(name: string, job: any, error: any): Promise<void>;
6
+ pop(name: string, direction?: "front" | "back", timeout?: number): Promise<any>;
7
+ worker(name: string, handler: (payload?: Record<string, any>, id?: string) => Promise<void>, opts?: {
8
+ interval?: number;
9
+ concurrency?: number;
10
+ direction?: "front" | "back";
11
+ }): Promise<void>;
12
+ retry(name: string): Promise<number>;
13
+ };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import crypto from 'crypto';
2
+ import { logger } from "@skalfa/skalfa-api-core";
3
+ import { redis } from "@skalfa/skalfa-redis";
4
+ export const queue = {
5
+ // ===========================>
6
+ // ## Queue: make redis key of job
7
+ // ===========================>
8
+ key: (name) => `queue:${name}`,
9
+ // ===========================>
10
+ // ## Queue: make redis key of job failed
11
+ // ===========================>
12
+ keyFailed: (name) => `queue-failed:${name}`,
13
+ // ===========================>
14
+ // ## Queue: add new job
15
+ // ===========================>
16
+ async add(name, jobPayload, uniq) {
17
+ const id = uniq ?? crypto.randomBytes(10).toString("hex");
18
+ const payload = JSON.stringify({ id, payload: jobPayload });
19
+ await redis.rpush(queue.key(name), payload);
20
+ return id;
21
+ },
22
+ // ===========================>
23
+ // ## Queue: add new job failed
24
+ // ===========================>
25
+ async addFailed(name, job, error) {
26
+ const store = {
27
+ id: job.id,
28
+ payload: job.payload,
29
+ error: error?.message || String(error),
30
+ time: new Date().toISOString(),
31
+ };
32
+ await redis.rpush(queue.keyFailed(name), JSON.stringify(store));
33
+ },
34
+ // ===========================>
35
+ // ## Queue: pop job from redis
36
+ // ===========================>
37
+ async pop(name, direction = "front", timeout = 0) {
38
+ const key = queue.key(name);
39
+ const cmd = direction === "front" ? "blpop" : "brpop";
40
+ const result = await redis[cmd](key, timeout);
41
+ if (!result)
42
+ return null;
43
+ try {
44
+ return JSON.parse(result[1]);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ },
50
+ // ===========================>
51
+ // ## Queue: job worker
52
+ // ===========================>
53
+ async worker(name, handler, opts) {
54
+ const interval = opts?.interval ?? 100;
55
+ const concurrency = opts?.concurrency ?? 1;
56
+ const direction = opts?.direction ?? "front";
57
+ const loop = async () => {
58
+ try {
59
+ const tasks = [];
60
+ // ambil beberapa job sekaligus
61
+ for (let i = 0; i < concurrency; i++) {
62
+ const job = await queue.pop(name, direction, 1);
63
+ if (!job)
64
+ break;
65
+ const task = (async () => {
66
+ try {
67
+ await handler(job.payload, job.id);
68
+ logger.queue(`${name} ${job.id} success!`);
69
+ }
70
+ catch (err) {
71
+ await queue.addFailed(name, job, err);
72
+ const em = err instanceof Error ? err.message : String(err);
73
+ logger.queueError(`${name} ${job?.id} error : ${em}`, { error: em, feature: name });
74
+ }
75
+ })();
76
+ tasks.push(task);
77
+ }
78
+ if (tasks.length > 0) {
79
+ await Promise.all(tasks);
80
+ }
81
+ }
82
+ catch (err) {
83
+ const em = err instanceof Error ? err.message : String(err);
84
+ logger.queueError(`${name} error : ${em}`, { error: em, feature: name });
85
+ }
86
+ setTimeout(loop, interval);
87
+ };
88
+ loop();
89
+ },
90
+ // ===========================>
91
+ // ## Queue: retry job failed
92
+ // ===========================>
93
+ async retry(name) {
94
+ const failedKey = queue.keyFailed(name);
95
+ const jobs = await redis.lrange(failedKey, 0, -1);
96
+ if (jobs.length === 0)
97
+ return 0;
98
+ for (const j of jobs) {
99
+ const job = JSON.parse(j);
100
+ try {
101
+ await queue.add(name, job.payload, job.id);
102
+ logger.queue(`${name} ${job?.id} success!`);
103
+ }
104
+ catch (err) {
105
+ const em = err instanceof Error ? err.message : String(err);
106
+ logger.queueError(`${name} ${job?.id} error : ${em}`, { error: em, feature: name });
107
+ }
108
+ }
109
+ await redis.del(failedKey);
110
+ return jobs.length;
111
+ },
112
+ };
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,26 @@
1
+ {
2
+ "name": "@skalfa/skalfa-queue",
3
+ "version": "1.0.0",
4
+ "description": "Queue utility package for Skalfa API framework using Redis.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "scripts": {
8
+ "build": "tsc --ignoreDeprecations 6.0"
9
+ },
10
+ "keywords": [
11
+ "aluna",
12
+ "queue",
13
+ "redis",
14
+ "skalfa"
15
+ ],
16
+ "author": "",
17
+ "license": "UNLICENSED",
18
+ "dependencies": {
19
+ "@skalfa/skalfa-redis": "file:../skalfa-redis",
20
+ "@skalfa/skalfa-api-core": "file:../skalfa-api-core"
21
+ },
22
+ "devDependencies": {
23
+ "@types/node": "^26.0.0",
24
+ "typescript": "^6.0.3"
25
+ }
26
+ }
package/src/index.ts ADDED
@@ -0,0 +1,146 @@
1
+ import crypto from 'crypto'
2
+ import { logger } from "@skalfa/skalfa-api-core"
3
+ import { redis } from "@skalfa/skalfa-redis"
4
+
5
+ export const queue = {
6
+ // ===========================>
7
+ // ## Queue: make redis key of job
8
+ // ===========================>
9
+ key : (name: string) => `queue:${name}`,
10
+
11
+
12
+
13
+ // ===========================>
14
+ // ## Queue: make redis key of job failed
15
+ // ===========================>
16
+ keyFailed : (name: string) => `queue-failed:${name}`,
17
+
18
+
19
+
20
+ // ===========================>
21
+ // ## Queue: add new job
22
+ // ===========================>
23
+ async add(name: string, jobPayload: any, uniq?: string) {
24
+ const id = uniq ?? crypto.randomBytes(10).toString("hex");
25
+ const payload = JSON.stringify({ id, payload: jobPayload });
26
+
27
+ await redis.rpush(queue.key(name), payload);
28
+ return id;
29
+ },
30
+
31
+
32
+
33
+ // ===========================>
34
+ // ## Queue: add new job failed
35
+ // ===========================>
36
+ async addFailed(name: string, job: any, error: any) {
37
+ const store = {
38
+ id : job.id,
39
+ payload : job.payload,
40
+ error : error?.message || String(error),
41
+ time : new Date().toISOString(),
42
+ };
43
+
44
+ await redis.rpush(queue.keyFailed(name), JSON.stringify(store));
45
+ },
46
+
47
+
48
+
49
+ // ===========================>
50
+ // ## Queue: pop job from redis
51
+ // ===========================>
52
+ async pop(name: string, direction: "front" | "back" = "front", timeout = 0) {
53
+ const key = queue.key(name);
54
+ const cmd = direction === "front" ? "blpop" : "brpop";
55
+
56
+ const result = await (redis as any)[cmd](key, timeout);
57
+ if (!result) return null;
58
+
59
+ try {
60
+ return JSON.parse(result[1]);
61
+ } catch {
62
+ return null;
63
+ }
64
+ },
65
+
66
+
67
+
68
+ // ===========================>
69
+ // ## Queue: job worker
70
+ // ===========================>
71
+ async worker(
72
+ name: string,
73
+ handler: (payload?: Record<string, any>, id?: string) => Promise<void>,
74
+ opts?: {
75
+ interval?: number;
76
+ concurrency?: number;
77
+ direction?: "front" | "back";
78
+ }
79
+ ) {
80
+ const interval = opts?.interval ?? 100;
81
+ const concurrency = opts?.concurrency ?? 1;
82
+ const direction = opts?.direction ?? "front";
83
+
84
+ const loop = async () => {
85
+ try {
86
+ const tasks: Promise<void>[] = [];
87
+
88
+ // ambil beberapa job sekaligus
89
+ for (let i = 0; i < concurrency; i++) {
90
+ const job = await queue.pop(name, direction, 1);
91
+ if (!job) break;
92
+
93
+ const task = (async () => {
94
+ try {
95
+ await handler(job.payload, job.id);
96
+ logger.queue(`${name} ${job.id} success!`);
97
+ } catch (err) {
98
+ await queue.addFailed(name, job, err);
99
+ const em = err instanceof Error ? err.message : String(err)
100
+ logger.queueError(`${name} ${job?.id} error : ${em}`, { error: em, feature: name });
101
+ }
102
+ })();
103
+
104
+ tasks.push(task);
105
+ }
106
+
107
+ if (tasks.length > 0) {
108
+ await Promise.all(tasks);
109
+ }
110
+ } catch (err) {
111
+ const em = err instanceof Error ? err.message : String(err)
112
+ logger.queueError(`${name} error : ${em}`, { error: em, feature: name });
113
+ }
114
+
115
+ setTimeout(loop, interval);
116
+ };
117
+
118
+ loop();
119
+ },
120
+
121
+
122
+
123
+ // ===========================>
124
+ // ## Queue: retry job failed
125
+ // ===========================>
126
+ async retry(name: string) {
127
+ const failedKey = queue.keyFailed(name);
128
+ const jobs = await redis.lrange(failedKey, 0, -1);
129
+ if (jobs.length === 0) return 0;
130
+
131
+ for (const j of jobs) {
132
+ const job = JSON.parse(j);
133
+ try {
134
+ await queue.add(name, job.payload, job.id);
135
+ logger.queue(`${name} ${job?.id} success!`);
136
+ } catch (err) {
137
+ const em = err instanceof Error ? err.message : String(err)
138
+ logger.queueError(`${name} ${job?.id} error : ${em}`, { error: em, feature: name });
139
+ }
140
+ }
141
+
142
+ await redis.del(failedKey);
143
+
144
+ return jobs.length;
145
+ },
146
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,18 @@
1
+ {
2
+ "compilerOptions": {
3
+ "ignoreDeprecations": "5.0",
4
+ "target": "ES2021",
5
+ "module": "ES2022",
6
+ "moduleResolution": "node",
7
+ "declaration": true,
8
+ "sourceMap": true,
9
+ "outDir": "./dist",
10
+ "rootDir": "./src",
11
+ "strict": true,
12
+ "esModuleInterop": true,
13
+ "skipLibCheck": true,
14
+ "forceConsistentCasingInFileNames": true,
15
+ "baseUrl": "."
16
+ },
17
+ "include": ["src/**/*"]
18
+ }