semola 0.5.0 → 0.5.2

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.
Files changed (273) hide show
  1. package/README.md +75 -20
  2. package/dist/api/core/index.cjs +206 -0
  3. package/dist/api/core/index.d.cts +21 -0
  4. package/dist/api/core/index.d.cts.map +1 -0
  5. package/dist/api/core/index.d.mts +21 -0
  6. package/dist/api/core/index.d.mts.map +1 -0
  7. package/dist/api/core/index.mjs +208 -0
  8. package/dist/api/core/index.mjs.map +1 -0
  9. package/dist/api/core/types.d.cts +107 -0
  10. package/dist/api/core/types.d.cts.map +1 -0
  11. package/dist/api/core/types.d.mts +107 -0
  12. package/dist/api/core/types.d.mts.map +1 -0
  13. package/dist/api/middleware/index.cjs +8 -0
  14. package/dist/api/middleware/index.d.cts +11 -0
  15. package/dist/api/middleware/index.d.cts.map +1 -0
  16. package/dist/api/middleware/index.d.mts +11 -0
  17. package/dist/api/middleware/index.d.mts.map +1 -0
  18. package/dist/api/middleware/index.mjs +10 -0
  19. package/dist/api/middleware/index.mjs.map +1 -0
  20. package/dist/api/middleware/types.d.cts +16 -0
  21. package/dist/api/middleware/types.d.cts.map +1 -0
  22. package/dist/api/middleware/types.d.mts +16 -0
  23. package/dist/api/middleware/types.d.mts.map +1 -0
  24. package/dist/api/openapi/index.cjs +254 -0
  25. package/dist/api/openapi/index.mjs +256 -0
  26. package/dist/api/openapi/index.mjs.map +1 -0
  27. package/dist/api/openapi/types.d.cts +60 -0
  28. package/dist/api/openapi/types.d.cts.map +1 -0
  29. package/dist/api/openapi/types.d.mts +60 -0
  30. package/dist/api/openapi/types.d.mts.map +1 -0
  31. package/dist/api/validation/index.cjs +64 -0
  32. package/dist/api/validation/index.mjs +61 -0
  33. package/dist/api/validation/index.mjs.map +1 -0
  34. package/dist/cache/types.d.cts +17 -0
  35. package/dist/cache/types.d.cts.map +1 -0
  36. package/dist/cache/types.d.mts +17 -0
  37. package/dist/cache/types.d.mts.map +1 -0
  38. package/dist/cron/scanner.cjs +237 -0
  39. package/dist/cron/scanner.mjs +238 -0
  40. package/dist/cron/scanner.mjs.map +1 -0
  41. package/dist/cron/types.d.cts +11 -0
  42. package/dist/cron/types.d.cts.map +1 -0
  43. package/dist/cron/types.d.mts +11 -0
  44. package/dist/cron/types.d.mts.map +1 -0
  45. package/dist/errors/types.d.cts +5 -0
  46. package/dist/errors/types.d.cts.map +1 -0
  47. package/dist/errors/types.d.mts +5 -0
  48. package/dist/errors/types.d.mts.map +1 -0
  49. package/dist/i18n/types.d.cts +13 -0
  50. package/dist/i18n/types.d.cts.map +1 -0
  51. package/dist/i18n/types.d.mts +13 -0
  52. package/dist/i18n/types.d.mts.map +1 -0
  53. package/dist/lib/api/index.cjs +5 -0
  54. package/dist/lib/api/index.d.cts +5 -0
  55. package/dist/lib/api/index.d.mts +5 -0
  56. package/dist/lib/api/index.mjs +3 -0
  57. package/dist/lib/cache/index.cjs +74 -0
  58. package/dist/lib/cache/index.d.cts +36 -0
  59. package/dist/lib/cache/index.d.cts.map +1 -0
  60. package/dist/lib/cache/index.d.mts +36 -0
  61. package/dist/lib/cache/index.d.mts.map +1 -0
  62. package/dist/lib/cache/index.mjs +75 -0
  63. package/dist/lib/cache/index.mjs.map +1 -0
  64. package/dist/lib/cron/index.cjs +276 -0
  65. package/dist/lib/cron/index.d.cts +39 -0
  66. package/dist/lib/cron/index.d.cts.map +1 -0
  67. package/dist/lib/cron/index.d.mts +39 -0
  68. package/dist/lib/cron/index.d.mts.map +1 -0
  69. package/dist/lib/cron/index.mjs +277 -0
  70. package/dist/lib/cron/index.mjs.map +1 -0
  71. package/dist/lib/errors/index.cjs +30 -0
  72. package/dist/lib/errors/index.d.cts +13 -0
  73. package/dist/lib/errors/index.d.cts.map +1 -0
  74. package/dist/lib/errors/index.d.mts +13 -0
  75. package/dist/lib/errors/index.d.mts.map +1 -0
  76. package/dist/lib/errors/index.mjs +28 -0
  77. package/dist/lib/errors/index.mjs.map +1 -0
  78. package/dist/lib/i18n/index.cjs +37 -0
  79. package/dist/lib/i18n/index.d.cts +20 -0
  80. package/dist/lib/i18n/index.d.cts.map +1 -0
  81. package/dist/lib/i18n/index.d.mts +20 -0
  82. package/dist/lib/i18n/index.d.mts.map +1 -0
  83. package/dist/lib/i18n/index.mjs +38 -0
  84. package/dist/lib/i18n/index.mjs.map +1 -0
  85. package/dist/lib/policy/index.cjs +99 -0
  86. package/dist/lib/policy/index.d.cts +21 -0
  87. package/dist/lib/policy/index.d.cts.map +1 -0
  88. package/dist/lib/policy/index.d.mts +21 -0
  89. package/dist/lib/policy/index.d.mts.map +1 -0
  90. package/dist/lib/policy/index.mjs +81 -0
  91. package/dist/lib/policy/index.mjs.map +1 -0
  92. package/dist/lib/prompts/index.cjs +409 -0
  93. package/dist/lib/prompts/index.d.cts +31 -0
  94. package/dist/lib/prompts/index.d.cts.map +1 -0
  95. package/dist/lib/prompts/index.d.mts +31 -0
  96. package/dist/lib/prompts/index.d.mts.map +1 -0
  97. package/dist/lib/prompts/index.mjs +405 -0
  98. package/dist/lib/prompts/index.mjs.map +1 -0
  99. package/dist/lib/pubsub/index.cjs +48 -0
  100. package/dist/lib/pubsub/index.d.cts +27 -0
  101. package/dist/lib/pubsub/index.d.cts.map +1 -0
  102. package/dist/lib/pubsub/index.d.mts +27 -0
  103. package/dist/lib/pubsub/index.d.mts.map +1 -0
  104. package/dist/lib/pubsub/index.mjs +49 -0
  105. package/dist/lib/pubsub/index.mjs.map +1 -0
  106. package/dist/lib/queue/index.cjs +182 -0
  107. package/dist/lib/queue/index.d.cts +32 -0
  108. package/dist/lib/queue/index.d.cts.map +1 -0
  109. package/dist/lib/queue/index.d.mts +32 -0
  110. package/dist/lib/queue/index.d.mts.map +1 -0
  111. package/dist/lib/queue/index.mjs +183 -0
  112. package/dist/lib/queue/index.mjs.map +1 -0
  113. package/dist/node_modules/@standard-schema/spec/dist/index.d.cts +80 -0
  114. package/dist/node_modules/@standard-schema/spec/dist/index.d.cts.map +1 -0
  115. package/dist/node_modules/@standard-schema/spec/dist/index.d.mts +80 -0
  116. package/dist/node_modules/@standard-schema/spec/dist/index.d.mts.map +1 -0
  117. package/dist/policy/helpers.cjs +206 -0
  118. package/dist/policy/helpers.d.cts +50 -0
  119. package/dist/policy/helpers.d.cts.map +1 -0
  120. package/dist/policy/helpers.d.mts +50 -0
  121. package/dist/policy/helpers.d.mts.map +1 -0
  122. package/dist/policy/helpers.mjs +190 -0
  123. package/dist/policy/helpers.mjs.map +1 -0
  124. package/dist/policy/types.d.cts +16 -0
  125. package/dist/policy/types.d.cts.map +1 -0
  126. package/dist/policy/types.d.mts +16 -0
  127. package/dist/policy/types.d.mts.map +1 -0
  128. package/dist/prompts/core/keys.cjs +165 -0
  129. package/dist/prompts/core/keys.mjs +167 -0
  130. package/dist/prompts/core/keys.mjs.map +1 -0
  131. package/dist/prompts/core/runtime.cjs +104 -0
  132. package/dist/prompts/core/runtime.mjs +106 -0
  133. package/dist/prompts/core/runtime.mjs.map +1 -0
  134. package/dist/prompts/core/session.cjs +98 -0
  135. package/dist/prompts/core/session.mjs +100 -0
  136. package/dist/prompts/core/session.mjs.map +1 -0
  137. package/dist/prompts/core/types.d.cts +21 -0
  138. package/dist/prompts/core/types.d.cts.map +1 -0
  139. package/dist/prompts/core/types.d.mts +21 -0
  140. package/dist/prompts/core/types.d.mts.map +1 -0
  141. package/dist/prompts/types.d.cts +52 -0
  142. package/dist/prompts/types.d.cts.map +1 -0
  143. package/dist/prompts/types.d.mts +52 -0
  144. package/dist/prompts/types.d.mts.map +1 -0
  145. package/dist/pubsub/types.d.cts +10 -0
  146. package/dist/pubsub/types.d.cts.map +1 -0
  147. package/dist/pubsub/types.d.mts +10 -0
  148. package/dist/pubsub/types.d.mts.map +1 -0
  149. package/dist/queue/types.d.cts +47 -0
  150. package/dist/queue/types.d.cts.map +1 -0
  151. package/dist/queue/types.d.mts +47 -0
  152. package/dist/queue/types.d.mts.map +1 -0
  153. package/package.json +86 -16
  154. package/dist/lib/api/core/index.d.ts +0 -15
  155. package/dist/lib/api/core/index.d.ts.map +0 -1
  156. package/dist/lib/api/core/index.js +0 -166
  157. package/dist/lib/api/core/index.js.map +0 -1
  158. package/dist/lib/api/core/index.test.d.ts +0 -2
  159. package/dist/lib/api/core/index.test.d.ts.map +0 -1
  160. package/dist/lib/api/core/index.test.js +0 -219
  161. package/dist/lib/api/core/index.test.js.map +0 -1
  162. package/dist/lib/api/core/types.d.ts +0 -102
  163. package/dist/lib/api/core/types.d.ts.map +0 -1
  164. package/dist/lib/api/core/types.js +0 -2
  165. package/dist/lib/api/core/types.js.map +0 -1
  166. package/dist/lib/api/index.d.ts +0 -5
  167. package/dist/lib/api/index.d.ts.map +0 -1
  168. package/dist/lib/api/index.js +0 -3
  169. package/dist/lib/api/index.js.map +0 -1
  170. package/dist/lib/api/middleware/index.d.ts +0 -7
  171. package/dist/lib/api/middleware/index.d.ts.map +0 -1
  172. package/dist/lib/api/middleware/index.js +0 -7
  173. package/dist/lib/api/middleware/index.js.map +0 -1
  174. package/dist/lib/api/middleware/index.test.d.ts +0 -2
  175. package/dist/lib/api/middleware/index.test.d.ts.map +0 -1
  176. package/dist/lib/api/middleware/index.test.js +0 -67
  177. package/dist/lib/api/middleware/index.test.js.map +0 -1
  178. package/dist/lib/api/middleware/types.d.ts +0 -12
  179. package/dist/lib/api/middleware/types.d.ts.map +0 -1
  180. package/dist/lib/api/middleware/types.js +0 -2
  181. package/dist/lib/api/middleware/types.js.map +0 -1
  182. package/dist/lib/api/openapi/index.d.ts +0 -31
  183. package/dist/lib/api/openapi/index.d.ts.map +0 -1
  184. package/dist/lib/api/openapi/index.js +0 -328
  185. package/dist/lib/api/openapi/index.js.map +0 -1
  186. package/dist/lib/api/openapi/index.test.d.ts +0 -2
  187. package/dist/lib/api/openapi/index.test.d.ts.map +0 -1
  188. package/dist/lib/api/openapi/index.test.js +0 -359
  189. package/dist/lib/api/openapi/index.test.js.map +0 -1
  190. package/dist/lib/api/openapi/types.d.ts +0 -57
  191. package/dist/lib/api/openapi/types.d.ts.map +0 -1
  192. package/dist/lib/api/openapi/types.js +0 -5
  193. package/dist/lib/api/openapi/types.js.map +0 -1
  194. package/dist/lib/api/validation/index.d.ts +0 -33
  195. package/dist/lib/api/validation/index.d.ts.map +0 -1
  196. package/dist/lib/api/validation/index.js +0 -81
  197. package/dist/lib/api/validation/index.js.map +0 -1
  198. package/dist/lib/api/validation/index.test.d.ts +0 -2
  199. package/dist/lib/api/validation/index.test.d.ts.map +0 -1
  200. package/dist/lib/api/validation/index.test.js +0 -135
  201. package/dist/lib/api/validation/index.test.js.map +0 -1
  202. package/dist/lib/cache/index.d.ts +0 -25
  203. package/dist/lib/cache/index.d.ts.map +0 -1
  204. package/dist/lib/cache/index.js +0 -62
  205. package/dist/lib/cache/index.js.map +0 -1
  206. package/dist/lib/cache/index.test.d.ts +0 -2
  207. package/dist/lib/cache/index.test.d.ts.map +0 -1
  208. package/dist/lib/cache/index.test.js +0 -314
  209. package/dist/lib/cache/index.test.js.map +0 -1
  210. package/dist/lib/cache/types.d.ts +0 -5
  211. package/dist/lib/cache/types.d.ts.map +0 -1
  212. package/dist/lib/cache/types.js +0 -2
  213. package/dist/lib/cache/types.js.map +0 -1
  214. package/dist/lib/errors/index.d.ts +0 -9
  215. package/dist/lib/errors/index.d.ts.map +0 -1
  216. package/dist/lib/errors/index.js +0 -25
  217. package/dist/lib/errors/index.js.map +0 -1
  218. package/dist/lib/errors/index.test.d.ts +0 -2
  219. package/dist/lib/errors/index.test.d.ts.map +0 -1
  220. package/dist/lib/errors/index.test.js +0 -197
  221. package/dist/lib/errors/index.test.js.map +0 -1
  222. package/dist/lib/errors/types.d.ts +0 -2
  223. package/dist/lib/errors/types.d.ts.map +0 -1
  224. package/dist/lib/errors/types.js +0 -2
  225. package/dist/lib/errors/types.js.map +0 -1
  226. package/dist/lib/i18n/index.d.ts +0 -18
  227. package/dist/lib/i18n/index.d.ts.map +0 -1
  228. package/dist/lib/i18n/index.js +0 -38
  229. package/dist/lib/i18n/index.js.map +0 -1
  230. package/dist/lib/i18n/index.test.d.ts +0 -2
  231. package/dist/lib/i18n/index.test.d.ts.map +0 -1
  232. package/dist/lib/i18n/index.test.js +0 -402
  233. package/dist/lib/i18n/index.test.js.map +0 -1
  234. package/dist/lib/i18n/types.d.ts +0 -15
  235. package/dist/lib/i18n/types.d.ts.map +0 -1
  236. package/dist/lib/i18n/types.js +0 -2
  237. package/dist/lib/i18n/types.js.map +0 -1
  238. package/dist/lib/policy/index.d.ts +0 -9
  239. package/dist/lib/policy/index.d.ts.map +0 -1
  240. package/dist/lib/policy/index.js +0 -51
  241. package/dist/lib/policy/index.js.map +0 -1
  242. package/dist/lib/policy/index.test.d.ts +0 -2
  243. package/dist/lib/policy/index.test.d.ts.map +0 -1
  244. package/dist/lib/policy/index.test.js +0 -328
  245. package/dist/lib/policy/index.test.js.map +0 -1
  246. package/dist/lib/policy/types.d.ts +0 -27
  247. package/dist/lib/policy/types.d.ts.map +0 -1
  248. package/dist/lib/policy/types.js +0 -2
  249. package/dist/lib/policy/types.js.map +0 -1
  250. package/dist/lib/pubsub/index.d.ts +0 -23
  251. package/dist/lib/pubsub/index.d.ts.map +0 -1
  252. package/dist/lib/pubsub/index.js +0 -55
  253. package/dist/lib/pubsub/index.js.map +0 -1
  254. package/dist/lib/pubsub/index.test.d.ts +0 -2
  255. package/dist/lib/pubsub/index.test.d.ts.map +0 -1
  256. package/dist/lib/pubsub/index.test.js +0 -550
  257. package/dist/lib/pubsub/index.test.js.map +0 -1
  258. package/dist/lib/pubsub/types.d.ts +0 -7
  259. package/dist/lib/pubsub/types.d.ts.map +0 -1
  260. package/dist/lib/pubsub/types.js +0 -2
  261. package/dist/lib/pubsub/types.js.map +0 -1
  262. package/dist/lib/queue/index.d.ts +0 -28
  263. package/dist/lib/queue/index.d.ts.map +0 -1
  264. package/dist/lib/queue/index.js +0 -211
  265. package/dist/lib/queue/index.js.map +0 -1
  266. package/dist/lib/queue/index.test.d.ts +0 -2
  267. package/dist/lib/queue/index.test.d.ts.map +0 -1
  268. package/dist/lib/queue/index.test.js +0 -740
  269. package/dist/lib/queue/index.test.js.map +0 -1
  270. package/dist/lib/queue/types.d.ts +0 -52
  271. package/dist/lib/queue/types.d.ts.map +0 -1
  272. package/dist/lib/queue/types.js +0 -2
  273. package/dist/lib/queue/types.js.map +0 -1
@@ -1,740 +0,0 @@
1
- import { describe, expect, test } from "bun:test";
2
- import { Queue } from "./index.js";
3
- class MockRedisClient {
4
- lists = new Map();
5
- shouldFail = false;
6
- setShouldFail(value) {
7
- this.shouldFail = value;
8
- }
9
- async lpush(key, value) {
10
- if (this.shouldFail) {
11
- throw new Error("Redis connection error");
12
- }
13
- if (!this.lists.has(key)) {
14
- this.lists.set(key, []);
15
- }
16
- this.lists.get(key)?.unshift(value);
17
- return this.lists.get(key)?.length ?? 0;
18
- }
19
- async rpop(key) {
20
- if (this.shouldFail) {
21
- throw new Error("Redis connection error");
22
- }
23
- const list = this.lists.get(key);
24
- if (!list || list.length === 0) {
25
- return null;
26
- }
27
- return list.pop();
28
- }
29
- clear() {
30
- this.lists.clear();
31
- }
32
- getList(key) {
33
- return this.lists.get(key) ?? [];
34
- }
35
- }
36
- const createMockRedis = () => {
37
- return new MockRedisClient();
38
- };
39
- const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
40
- describe("Queue", () => {
41
- describe("enqueue", () => {
42
- test.concurrent("should enqueue a job successfully", async () => {
43
- const redis = createMockRedis();
44
- const handler = () => { };
45
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
46
- const [error, jobId] = await queue.enqueue({ message: "hello" });
47
- expect(error).toBeNull();
48
- expect(jobId).toBeDefined();
49
- expect(typeof jobId).toBe("string");
50
- await queue.stop();
51
- });
52
- test.concurrent("should handle Redis connection errors", async () => {
53
- const redis = createMockRedis();
54
- const handler = () => { };
55
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
56
- redis.setShouldFail(true);
57
- const [error, jobId] = await queue.enqueue({ message: "hello" });
58
- expect(error).toEqual({
59
- type: "QueueError",
60
- message: "Unable to enqueue job",
61
- });
62
- expect(jobId).toBeNull();
63
- await queue.stop();
64
- });
65
- test.concurrent("should handle serialization errors", async () => {
66
- const redis = createMockRedis();
67
- const handler = () => { };
68
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
69
- const circular = { a: 1 };
70
- circular.self = circular;
71
- const [error, jobId] = await queue.enqueue(circular);
72
- expect(error).toEqual({
73
- type: "QueueError",
74
- message: "Unable to serialize job data",
75
- });
76
- expect(jobId).toBeNull();
77
- await queue.stop();
78
- });
79
- });
80
- describe("processing", () => {
81
- test.concurrent("should verify job is re-enqueued on failure", async () => {
82
- const redis = createMockRedis();
83
- let callCount = 0;
84
- const handler = () => {
85
- callCount++;
86
- if (callCount === 1) {
87
- throw new Error("First attempt fails");
88
- }
89
- };
90
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
91
- await queue.enqueue({ message: "test" });
92
- await sleep(2500);
93
- expect(callCount).toBeGreaterThan(1);
94
- await queue.stop();
95
- });
96
- test.concurrent("should process jobs in FIFO order", async () => {
97
- const redis = createMockRedis();
98
- const processed = [];
99
- const handler = (data) => {
100
- processed.push(data.message);
101
- };
102
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
103
- await queue.enqueue({ message: "first" });
104
- await queue.enqueue({ message: "second" });
105
- await queue.enqueue({ message: "third" });
106
- await sleep(500);
107
- expect(processed).toEqual(["first", "second", "third"]);
108
- await queue.stop();
109
- });
110
- test.concurrent("should call onSuccess callback on successful processing", async () => {
111
- const redis = createMockRedis();
112
- const handler = () => { };
113
- const successJobs = [];
114
- const onSuccess = (job) => {
115
- successJobs.push(job.id);
116
- };
117
- const queue = new Queue({
118
- name: "test",
119
- redis,
120
- retries: 3,
121
- handler,
122
- onSuccess,
123
- });
124
- const [, jobId] = await queue.enqueue({ message: "hello" });
125
- await sleep(200);
126
- expect(jobId).not.toBeNull();
127
- if (jobId) {
128
- expect(successJobs).toContain(jobId);
129
- }
130
- await queue.stop();
131
- });
132
- test.concurrent("should call onRetry callback when job fails but will be retried", async () => {
133
- const redis = createMockRedis();
134
- let attempts = 0;
135
- const handler = () => {
136
- attempts++;
137
- if (attempts < 2) {
138
- throw new Error("First attempt fails");
139
- }
140
- };
141
- const retryContexts = [];
142
- const onRetry = (context) => {
143
- retryContexts.push({
144
- id: context.job.id,
145
- attempts: context.job.attempts,
146
- error: context.error,
147
- nextRetryDelayMs: context.nextRetryDelayMs,
148
- retriesRemaining: context.retriesRemaining,
149
- });
150
- };
151
- const queue = new Queue({
152
- name: "test",
153
- redis,
154
- retries: 2,
155
- handler,
156
- onRetry,
157
- });
158
- const [, jobId] = await queue.enqueue({ message: "hello" });
159
- await sleep(2500);
160
- expect(jobId).not.toBeNull();
161
- expect(retryContexts.length).toBe(1);
162
- if (jobId) {
163
- expect(retryContexts[0]?.id).toBe(jobId);
164
- expect(retryContexts[0]?.error).toBe("First attempt fails");
165
- expect(retryContexts[0]?.attempts).toBe(1);
166
- expect(retryContexts[0]?.nextRetryDelayMs).toBe(1000);
167
- expect(retryContexts[0]?.retriesRemaining).toBe(1);
168
- }
169
- await queue.stop();
170
- });
171
- test.concurrent("should call onRetry multiple times for multiple failures", async () => {
172
- const redis = createMockRedis();
173
- let attempts = 0;
174
- const handler = () => {
175
- attempts++;
176
- throw new Error(`Attempt ${attempts} failed`);
177
- };
178
- const retryContexts = [];
179
- const onRetry = (context) => {
180
- retryContexts.push({
181
- attempts: context.job.attempts,
182
- error: context.error,
183
- nextRetryDelayMs: context.nextRetryDelayMs,
184
- });
185
- };
186
- const queue = new Queue({
187
- name: "test",
188
- redis,
189
- retries: 2,
190
- handler,
191
- onRetry,
192
- });
193
- await queue.enqueue({ message: "hello" });
194
- await sleep(4000);
195
- // Should have called onRetry 2 times (attempts 1, 2)
196
- expect(retryContexts.length).toBe(2);
197
- expect(retryContexts[0]?.attempts).toBe(1);
198
- expect(retryContexts[0]?.nextRetryDelayMs).toBe(1000);
199
- expect(retryContexts[1]?.attempts).toBe(2);
200
- expect(retryContexts[1]?.nextRetryDelayMs).toBe(2000);
201
- await queue.stop();
202
- });
203
- test.concurrent("should not call onRetry when job succeeds on first try", async () => {
204
- const redis = createMockRedis();
205
- const handler = () => { };
206
- const retryJobs = [];
207
- const onRetry = (context) => {
208
- retryJobs.push(context.job.id);
209
- };
210
- const successJobs = [];
211
- const onSuccess = (job) => {
212
- successJobs.push(job.id);
213
- };
214
- const queue = new Queue({
215
- name: "test",
216
- redis,
217
- retries: 3,
218
- handler,
219
- onRetry,
220
- onSuccess,
221
- });
222
- const [, jobId] = await queue.enqueue({ message: "hello" });
223
- await sleep(200);
224
- expect(jobId).not.toBeNull();
225
- expect(retryJobs.length).toBe(0);
226
- if (jobId) {
227
- expect(successJobs).toContain(jobId);
228
- }
229
- await queue.stop();
230
- });
231
- test.concurrent("should call onRetry then onError when retries exhausted", async () => {
232
- const redis = createMockRedis();
233
- const handler = () => {
234
- throw new Error("Always fails");
235
- };
236
- const retryJobs = [];
237
- const errorJobs = [];
238
- const onRetry = (context) => {
239
- retryJobs.push(context.job.id);
240
- };
241
- const onError = (context) => {
242
- errorJobs.push(context.job.id);
243
- };
244
- const queue = new Queue({
245
- name: "test",
246
- redis,
247
- retries: 2,
248
- handler,
249
- onRetry,
250
- onError,
251
- });
252
- const [, jobId] = await queue.enqueue({ message: "hello" });
253
- await sleep(4000);
254
- expect(jobId).not.toBeNull();
255
- expect(retryJobs.length).toBe(2);
256
- expect(errorJobs.length).toBe(1);
257
- if (jobId) {
258
- expect(retryJobs).toContain(jobId);
259
- expect(errorJobs).toContain(jobId);
260
- }
261
- await queue.stop();
262
- });
263
- test.concurrent("should retry failed jobs with exponential backoff", async () => {
264
- const redis = createMockRedis();
265
- let attempts = 0;
266
- const handler = () => {
267
- attempts++;
268
- if (attempts < 2) {
269
- throw new Error("Processing failed");
270
- }
271
- };
272
- const queue = new Queue({ name: "test", redis, retries: 2, handler });
273
- await queue.enqueue({ message: "hello" });
274
- await sleep(3000);
275
- expect(attempts).toBe(2);
276
- await queue.stop();
277
- });
278
- test.concurrent("should respect maxRetries limit", async () => {
279
- const redis = createMockRedis();
280
- let attempts = 0;
281
- const handler = () => {
282
- attempts++;
283
- throw new Error("Always fails");
284
- };
285
- const errorJobs = [];
286
- const onError = (context) => {
287
- errorJobs.push(context.job.id);
288
- };
289
- const queue = new Queue({
290
- name: "test",
291
- redis,
292
- retries: 2,
293
- handler,
294
- onError,
295
- });
296
- const [, jobId] = await queue.enqueue({ message: "hello" });
297
- await sleep(4000);
298
- expect(attempts).toBe(3);
299
- expect(jobId).not.toBeNull();
300
- if (jobId) {
301
- expect(errorJobs).toContain(jobId);
302
- }
303
- await queue.stop();
304
- });
305
- test.concurrent("should call onError when retries are exhausted", async () => {
306
- const redis = createMockRedis();
307
- const handler = () => {
308
- throw new Error("Processing failed");
309
- };
310
- const errorContexts = [];
311
- const onError = (context) => {
312
- errorContexts.push({
313
- id: context.job.id,
314
- lastError: context.lastError,
315
- totalDurationMs: context.totalDurationMs,
316
- totalAttempts: context.totalAttempts,
317
- errorHistory: context.errorHistory,
318
- });
319
- };
320
- const queue = new Queue({
321
- name: "test",
322
- redis,
323
- retries: 1,
324
- handler,
325
- onError,
326
- });
327
- const [, jobId] = await queue.enqueue({ message: "hello" });
328
- await sleep(3000);
329
- expect(errorContexts.length).toBe(1);
330
- expect(jobId).not.toBeNull();
331
- if (jobId) {
332
- expect(errorContexts[0]?.id).toBe(jobId);
333
- expect(errorContexts[0]?.lastError).toBe("Processing failed");
334
- expect(errorContexts[0]?.totalAttempts).toBe(2);
335
- expect(errorContexts[0]?.totalDurationMs).toBeGreaterThan(0);
336
- expect(errorContexts[0]?.errorHistory).toBeDefined();
337
- expect(errorContexts[0]?.errorHistory.length).toBeGreaterThan(0);
338
- }
339
- await queue.stop();
340
- });
341
- test.concurrent("should handle handler errors", async () => {
342
- const redis = createMockRedis();
343
- const handler = () => {
344
- throw new Error("Handler error");
345
- };
346
- const errorJobs = [];
347
- const onError = (context) => {
348
- errorJobs.push(context.job.id);
349
- };
350
- const queue = new Queue({
351
- name: "test",
352
- redis,
353
- retries: 1,
354
- handler,
355
- onError,
356
- });
357
- const [, jobId] = await queue.enqueue({ message: "hello" });
358
- await sleep(3000);
359
- expect(jobId).not.toBeNull();
360
- if (jobId) {
361
- expect(errorJobs).toContain(jobId);
362
- }
363
- await queue.stop();
364
- });
365
- });
366
- describe("lifecycle", () => {
367
- test.concurrent("should auto-start processing in constructor", async () => {
368
- const redis = createMockRedis();
369
- const processed = [];
370
- const handler = (data) => {
371
- processed.push(data.message);
372
- };
373
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
374
- await queue.enqueue({ message: "hello" });
375
- await sleep(200);
376
- expect(processed).toContain("hello");
377
- await queue.stop();
378
- });
379
- test.concurrent("should stop processing after stop() is called", async () => {
380
- const redis = createMockRedis();
381
- const processed = [];
382
- const handler = (data) => {
383
- processed.push(data.message);
384
- };
385
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
386
- await queue.enqueue({ message: "first" });
387
- await sleep(200);
388
- await queue.stop();
389
- await queue.enqueue({ message: "second" });
390
- await sleep(200);
391
- expect(processed).toContain("first");
392
- expect(processed).not.toContain("second");
393
- });
394
- });
395
- describe("edge cases", () => {
396
- test.concurrent("should handle empty queue gracefully", async () => {
397
- const redis = createMockRedis();
398
- const handler = () => { };
399
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
400
- await sleep(200);
401
- await queue.stop();
402
- });
403
- test.concurrent("should handle async handlers", async () => {
404
- const redis = createMockRedis();
405
- const processed = [];
406
- const handler = async (data) => {
407
- await sleep(10);
408
- processed.push(data.message);
409
- };
410
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
411
- await queue.enqueue({ message: "async" });
412
- await sleep(200);
413
- expect(processed).toContain("async");
414
- await queue.stop();
415
- });
416
- test.concurrent("should handle multiple queues with different names", async () => {
417
- const redis = createMockRedis();
418
- const processed1 = [];
419
- const processed2 = [];
420
- const handler1 = (data) => {
421
- processed1.push(data.message);
422
- };
423
- const handler2 = (data) => {
424
- processed2.push(data.message);
425
- };
426
- const queue1 = new Queue({
427
- name: "queue1",
428
- redis,
429
- retries: 3,
430
- handler: handler1,
431
- });
432
- const queue2 = new Queue({
433
- name: "queue2",
434
- redis,
435
- retries: 3,
436
- handler: handler2,
437
- });
438
- await queue1.enqueue({ message: "q1" });
439
- await queue2.enqueue({ message: "q2" });
440
- await sleep(200);
441
- expect(processed1).toContain("q1");
442
- expect(processed1).not.toContain("q2");
443
- expect(processed2).toContain("q2");
444
- expect(processed2).not.toContain("q1");
445
- await queue1.stop();
446
- await queue2.stop();
447
- });
448
- test.concurrent("should handle deserialization errors gracefully", async () => {
449
- const redis = createMockRedis();
450
- const handler = () => { };
451
- const queue = new Queue({ name: "test", redis, retries: 3, handler });
452
- await redis.lpush("queue:test:jobs", "invalid json {");
453
- await sleep(200);
454
- await queue.stop();
455
- });
456
- test.concurrent("should move malformed job to dead-letter queue on parse error", async () => {
457
- const redis = createMockRedis();
458
- const parseErrors = [];
459
- const queue = new Queue({
460
- name: "test",
461
- redis,
462
- retries: 3,
463
- handler: () => {
464
- throw new Error("Handler should not be called");
465
- },
466
- onParseError: async (context) => {
467
- parseErrors.push(context);
468
- },
469
- pollInterval: 10,
470
- });
471
- const malformedData = '{"id":"test","incomplete';
472
- await redis.lpush("queue:test:jobs", malformedData);
473
- await sleep(100);
474
- await queue.stop();
475
- const deadLetterQueue = redis.getList("queue:test:dead-letter");
476
- expect(deadLetterQueue.length).toBe(1);
477
- const firstEntry = deadLetterQueue[0];
478
- expect(firstEntry).toBeDefined();
479
- if (firstEntry) {
480
- const deadLetterEntry = JSON.parse(firstEntry);
481
- expect(deadLetterEntry.jobData).toBe(malformedData);
482
- expect(deadLetterEntry.parseError).toBeDefined();
483
- expect(deadLetterEntry.timestamp).toBeGreaterThan(0);
484
- }
485
- expect(parseErrors.length).toBe(1);
486
- const firstError = parseErrors[0];
487
- expect(firstError).toBeDefined();
488
- if (firstError) {
489
- expect(firstError.rawJobData).toBe(malformedData);
490
- expect(firstError.parseError).toBeDefined();
491
- expect(firstError.timestamp).toBeGreaterThan(0);
492
- }
493
- });
494
- test.concurrent("should preserve multiple malformed jobs in dead-letter queue", async () => {
495
- const redis = createMockRedis();
496
- const queue = new Queue({
497
- name: "test",
498
- redis,
499
- retries: 3,
500
- handler: () => { },
501
- pollInterval: 10,
502
- });
503
- const malformedPayloads = [
504
- "invalid json",
505
- '{"incomplete": "object"',
506
- '["unclosed", "array"',
507
- ];
508
- for (const payload of malformedPayloads) {
509
- await redis.lpush("queue:test:jobs", payload);
510
- }
511
- await sleep(150);
512
- await queue.stop();
513
- const deadLetterQueue = redis.getList("queue:test:dead-letter");
514
- expect(deadLetterQueue.length).toBe(malformedPayloads.length);
515
- deadLetterQueue.forEach((entry) => {
516
- const parsed = JSON.parse(entry);
517
- expect(parsed.jobData).toBeDefined();
518
- expect(parsed.parseError).toBeDefined();
519
- expect(parsed.timestamp).toBeGreaterThan(0);
520
- });
521
- });
522
- test.concurrent("should call onParseError with parse error details and raw jobData", async () => {
523
- const redis = createMockRedis();
524
- const parseErrors = [];
525
- const queue = new Queue({
526
- name: "test",
527
- redis,
528
- retries: 3,
529
- handler: () => { },
530
- onParseError: async (context) => {
531
- parseErrors.push(context);
532
- },
533
- pollInterval: 10,
534
- });
535
- const malformedData = '{"bad": json}';
536
- await redis.lpush("queue:test:jobs", malformedData);
537
- await sleep(100);
538
- await queue.stop();
539
- expect(parseErrors.length).toBe(1);
540
- const firstError = parseErrors[0];
541
- expect(firstError).toBeDefined();
542
- if (firstError) {
543
- expect(firstError.rawJobData).toBe(malformedData);
544
- expect(firstError.parseError).toBeDefined();
545
- expect(firstError.timestamp).toBeGreaterThan(0);
546
- }
547
- });
548
- test.concurrent("should continue processing valid jobs after malformed jobs", async () => {
549
- const redis = createMockRedis();
550
- const processed = [];
551
- const parseErrors = [];
552
- const queue = new Queue({
553
- name: "test",
554
- redis,
555
- retries: 3,
556
- handler: async (data) => {
557
- processed.push(data);
558
- },
559
- onParseError: async (context) => {
560
- parseErrors.push(context);
561
- },
562
- pollInterval: 10,
563
- });
564
- await redis.lpush("queue:test:jobs", '{"bad": json}');
565
- await queue.enqueue({ message: "valid-1" });
566
- await redis.lpush("queue:test:jobs", "invalid json");
567
- await queue.enqueue({ message: "valid-2" });
568
- await sleep(200);
569
- await queue.stop();
570
- expect(processed.length).toBe(2);
571
- expect(processed.some((p) => p.message === "valid-1")).toBe(true);
572
- expect(processed.some((p) => p.message === "valid-2")).toBe(true);
573
- expect(parseErrors.length).toBe(2);
574
- const deadLetterQueue = redis.getList("queue:test:dead-letter");
575
- expect(deadLetterQueue.length).toBe(2);
576
- });
577
- test.concurrent("should handle parse errors even without onError callback", async () => {
578
- const redis = createMockRedis();
579
- const queue = new Queue({
580
- name: "test",
581
- redis,
582
- retries: 3,
583
- handler: () => { },
584
- pollInterval: 10,
585
- });
586
- const malformedData = '{"invalid": json}';
587
- await redis.lpush("queue:test:jobs", malformedData);
588
- await sleep(100);
589
- await queue.stop();
590
- const deadLetterQueue = redis.getList("queue:test:dead-letter");
591
- expect(deadLetterQueue.length).toBe(1);
592
- const firstEntry = deadLetterQueue[0];
593
- expect(firstEntry).toBeDefined();
594
- if (firstEntry) {
595
- const deadLetterEntry = JSON.parse(firstEntry);
596
- expect(deadLetterEntry.jobData).toBe(malformedData);
597
- }
598
- });
599
- test.concurrent("should handle completely invalid JSON syntax", async () => {
600
- const redis = createMockRedis();
601
- const parseErrors = [];
602
- const queue = new Queue({
603
- name: "test",
604
- redis,
605
- retries: 3,
606
- handler: () => { },
607
- onParseError: async (context) => {
608
- parseErrors.push(context);
609
- },
610
- pollInterval: 10,
611
- });
612
- const invalidJson = "not valid json at all";
613
- await redis.lpush("queue:test:jobs", invalidJson);
614
- await sleep(100);
615
- await queue.stop();
616
- const deadLetterQueue = redis.getList("queue:test:dead-letter");
617
- expect(deadLetterQueue.length).toBe(1);
618
- const firstEntry = deadLetterQueue[0];
619
- expect(firstEntry).toBeDefined();
620
- if (firstEntry) {
621
- const deadLetterEntry = JSON.parse(firstEntry);
622
- expect(deadLetterEntry.jobData).toBe(invalidJson);
623
- }
624
- expect(parseErrors.length).toBe(1);
625
- const firstParseError = parseErrors[0];
626
- expect(firstParseError).toBeDefined();
627
- if (firstParseError) {
628
- expect(firstParseError.rawJobData).toBe(invalidJson);
629
- expect(firstParseError.parseError).toBeDefined();
630
- expect(firstParseError.timestamp).toBeGreaterThan(0);
631
- }
632
- });
633
- test.concurrent("should handle large job data", async () => {
634
- const redis = createMockRedis();
635
- const processed = [];
636
- const handler = (data) => {
637
- processed.push(data);
638
- };
639
- const queue = new Queue({
640
- name: "test",
641
- redis,
642
- retries: 3,
643
- handler,
644
- });
645
- const largeData = {
646
- items: Array.from({ length: 1000 }, (_, i) => ({
647
- id: i,
648
- name: `Item ${i}`,
649
- })),
650
- };
651
- await queue.enqueue(largeData);
652
- await sleep(200);
653
- expect(processed[0]).toEqual(largeData);
654
- await queue.stop();
655
- });
656
- test.concurrent("should handle job timeout", async () => {
657
- const redis = createMockRedis();
658
- let attempts = 0;
659
- const signalStates = [];
660
- const handler = async (_data, signal) => {
661
- attempts++;
662
- // Capture initial signal state
663
- signalStates.push(signal?.aborted ?? false);
664
- // Sleep longer than timeout
665
- await sleep(500);
666
- // Capture signal state after timeout should have fired
667
- signalStates.push(signal?.aborted ?? false);
668
- };
669
- const errorJobs = [];
670
- const onError = (context) => {
671
- errorJobs.push(context.job.id);
672
- };
673
- const queue = new Queue({
674
- name: "test",
675
- redis,
676
- retries: 1,
677
- handler,
678
- timeout: 100,
679
- onError,
680
- });
681
- const [, jobId] = await queue.enqueue({ message: "hello" });
682
- await sleep(3000);
683
- // Should have retried once due to timeout
684
- expect(attempts).toBeGreaterThanOrEqual(2);
685
- expect(jobId).not.toBeNull();
686
- if (jobId) {
687
- expect(errorJobs).toContain(jobId);
688
- }
689
- // Verify signal was received and got aborted after timeout
690
- expect(signalStates.length).toBeGreaterThan(0);
691
- expect(signalStates[0]).toBe(false); // Signal not aborted initially
692
- // At least one signal should be aborted after timeout fires
693
- expect(signalStates.some((state) => state === true)).toBe(true);
694
- await queue.stop();
695
- });
696
- test.concurrent("should respect concurrency limits", async () => {
697
- const redis = createMockRedis();
698
- let maxConcurrent = 0;
699
- let currentConcurrent = 0;
700
- const handler = async () => {
701
- currentConcurrent++;
702
- maxConcurrent = Math.max(maxConcurrent, currentConcurrent);
703
- await sleep(50);
704
- currentConcurrent--;
705
- };
706
- const queue = new Queue({
707
- name: "test",
708
- redis,
709
- retries: 1,
710
- handler,
711
- concurrency: 2,
712
- });
713
- // Enqueue multiple jobs
714
- await queue.enqueue({ message: "1" });
715
- await queue.enqueue({ message: "2" });
716
- await queue.enqueue({ message: "3" });
717
- await queue.enqueue({ message: "4" });
718
- await sleep(500);
719
- // Should not exceed concurrency limit
720
- expect(maxConcurrent).toBeLessThanOrEqual(2);
721
- await queue.stop();
722
- });
723
- test.concurrent("should handle graceful shutdown with pending jobs", async () => {
724
- const redis = createMockRedis();
725
- const processed = [];
726
- const handler = async (data) => {
727
- await sleep(50);
728
- processed.push(data.message);
729
- };
730
- const queue = new Queue({ name: "test", redis, retries: 1, handler });
731
- await queue.enqueue({ message: "job1" });
732
- await queue.enqueue({ message: "job2" });
733
- await sleep(100); // Give it time to start processing
734
- await queue.stop(); // Should wait for in-flight jobs
735
- // Should have completed at least job1
736
- expect(processed.length).toBeGreaterThan(0);
737
- });
738
- });
739
- });
740
- //# sourceMappingURL=index.test.js.map