@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 +99 -0
- package/dist/index.d.ts +13 -0
- package/dist/index.js +113 -0
- package/dist/index.js.map +1 -0
- package/package.json +26 -0
- package/src/index.ts +146 -0
- package/tsconfig.json +18 -0
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
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -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
|
+
}
|