@zudojs/queue 0.1.0 → 1.1.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.
Files changed (149) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +40 -8
  3. package/dist/deadLetter/deadLetter.core.js +1 -1
  4. package/dist/inMemoryQueue/inMemoryQueue.core.d.ts +95 -4
  5. package/dist/inMemoryQueue/inMemoryQueue.core.js +398 -73
  6. package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts +53 -8
  7. package/dist/inMemoryQueue/inMemoryQueue.processing.js +83 -36
  8. package/dist/inMemoryQueue/inMemoryQueue.scheduling.d.ts +17 -1
  9. package/dist/inMemoryQueue/inMemoryQueue.scheduling.js +43 -11
  10. package/dist/index.d.ts +0 -1
  11. package/dist/index.js +0 -2
  12. package/dist/job/job.core.js +20 -12
  13. package/dist/job/job.type.d.ts +3 -2
  14. package/dist/jobOptions/jobOptions.type.d.ts +12 -3
  15. package/dist/jobTypes/index.d.ts +1 -1
  16. package/dist/jobTypes/jobTypes.type.d.ts +9 -0
  17. package/dist/middleware/middleware.core.d.ts +10 -1
  18. package/dist/middleware/middleware.core.js +40 -17
  19. package/dist/processor/index.d.ts +1 -1
  20. package/dist/processor/index.js +1 -1
  21. package/dist/processor/processor.core.js +2 -0
  22. package/dist/processor/processor.type.d.ts +18 -1
  23. package/dist/processor/processor.type.js +15 -0
  24. package/dist/queue/index.d.ts +1 -1
  25. package/dist/queue/queue.core.js +2 -2
  26. package/dist/queue/queue.type.d.ts +108 -2
  27. package/dist/queueEmitter/queueEmitter.core.d.ts +22 -2
  28. package/dist/queueEmitter/queueEmitter.core.js +47 -7
  29. package/dist/queueManager/queueManager.core.js +24 -5
  30. package/dist/queueRegistry/queueRegistry.core.js +15 -0
  31. package/dist/queueRegistry/queueRegistry.type.d.ts +2 -0
  32. package/dist/retryPolicy/retryPolicy.core.d.ts +42 -3
  33. package/dist/retryPolicy/retryPolicy.core.js +82 -13
  34. package/dist/serializer/serializer.core.d.ts +2 -1
  35. package/dist/worker/worker.core.d.ts +4 -0
  36. package/dist/worker/worker.core.js +139 -37
  37. package/dist/worker/worker.type.d.ts +17 -3
  38. package/package.json +25 -14
  39. package/dist/.tsbuildinfo +0 -1
  40. package/dist/deadLetter/deadLetter.core.d.ts.map +0 -1
  41. package/dist/deadLetter/deadLetter.core.js.map +0 -1
  42. package/dist/deadLetter/deadLetter.type.d.ts.map +0 -1
  43. package/dist/deadLetter/deadLetter.type.js.map +0 -1
  44. package/dist/deadLetter/index.d.ts.map +0 -1
  45. package/dist/deadLetter/index.js.map +0 -1
  46. package/dist/inMemoryQueue/inMemoryQueue.core.d.ts.map +0 -1
  47. package/dist/inMemoryQueue/inMemoryQueue.core.js.map +0 -1
  48. package/dist/inMemoryQueue/inMemoryQueue.factory.d.ts.map +0 -1
  49. package/dist/inMemoryQueue/inMemoryQueue.factory.js.map +0 -1
  50. package/dist/inMemoryQueue/inMemoryQueue.processing.d.ts.map +0 -1
  51. package/dist/inMemoryQueue/inMemoryQueue.processing.js.map +0 -1
  52. package/dist/inMemoryQueue/inMemoryQueue.scheduling.d.ts.map +0 -1
  53. package/dist/inMemoryQueue/inMemoryQueue.scheduling.js.map +0 -1
  54. package/dist/inMemoryQueue/index.d.ts.map +0 -1
  55. package/dist/inMemoryQueue/index.js.map +0 -1
  56. package/dist/index.d.ts.map +0 -1
  57. package/dist/index.js.map +0 -1
  58. package/dist/job/index.d.ts.map +0 -1
  59. package/dist/job/index.js.map +0 -1
  60. package/dist/job/job.core.d.ts.map +0 -1
  61. package/dist/job/job.core.js.map +0 -1
  62. package/dist/job/job.type.d.ts.map +0 -1
  63. package/dist/job/job.type.js.map +0 -1
  64. package/dist/jobContext/index.d.ts.map +0 -1
  65. package/dist/jobContext/index.js.map +0 -1
  66. package/dist/jobContext/jobContext.core.d.ts.map +0 -1
  67. package/dist/jobContext/jobContext.core.js.map +0 -1
  68. package/dist/jobContext/jobContext.type.d.ts.map +0 -1
  69. package/dist/jobContext/jobContext.type.js.map +0 -1
  70. package/dist/jobOptions/index.d.ts.map +0 -1
  71. package/dist/jobOptions/index.js.map +0 -1
  72. package/dist/jobOptions/jobOptions.core.d.ts.map +0 -1
  73. package/dist/jobOptions/jobOptions.core.js.map +0 -1
  74. package/dist/jobOptions/jobOptions.type.d.ts.map +0 -1
  75. package/dist/jobOptions/jobOptions.type.js.map +0 -1
  76. package/dist/jobResult/index.d.ts.map +0 -1
  77. package/dist/jobResult/index.js.map +0 -1
  78. package/dist/jobResult/jobResult.core.d.ts.map +0 -1
  79. package/dist/jobResult/jobResult.core.js.map +0 -1
  80. package/dist/jobResult/jobResult.type.d.ts.map +0 -1
  81. package/dist/jobResult/jobResult.type.js.map +0 -1
  82. package/dist/jobTypes/index.d.ts.map +0 -1
  83. package/dist/jobTypes/index.js.map +0 -1
  84. package/dist/jobTypes/jobTypes.type.d.ts.map +0 -1
  85. package/dist/jobTypes/jobTypes.type.js.map +0 -1
  86. package/dist/middleware/index.d.ts.map +0 -1
  87. package/dist/middleware/index.js.map +0 -1
  88. package/dist/middleware/middleware.core.d.ts.map +0 -1
  89. package/dist/middleware/middleware.core.js.map +0 -1
  90. package/dist/middleware/middleware.type.d.ts.map +0 -1
  91. package/dist/middleware/middleware.type.js.map +0 -1
  92. package/dist/processor/index.d.ts.map +0 -1
  93. package/dist/processor/index.js.map +0 -1
  94. package/dist/processor/processor.core.d.ts.map +0 -1
  95. package/dist/processor/processor.core.js.map +0 -1
  96. package/dist/processor/processor.type.d.ts.map +0 -1
  97. package/dist/processor/processor.type.js.map +0 -1
  98. package/dist/queue/index.d.ts.map +0 -1
  99. package/dist/queue/index.js.map +0 -1
  100. package/dist/queue/queue.core.d.ts.map +0 -1
  101. package/dist/queue/queue.core.js.map +0 -1
  102. package/dist/queue/queue.type.d.ts.map +0 -1
  103. package/dist/queue/queue.type.js.map +0 -1
  104. package/dist/queueEmitter/index.d.ts.map +0 -1
  105. package/dist/queueEmitter/index.js.map +0 -1
  106. package/dist/queueEmitter/queueEmitter.core.d.ts.map +0 -1
  107. package/dist/queueEmitter/queueEmitter.core.js.map +0 -1
  108. package/dist/queueEmitter/queueEmitter.type.d.ts.map +0 -1
  109. package/dist/queueEmitter/queueEmitter.type.js.map +0 -1
  110. package/dist/queueEvents/index.d.ts +0 -8
  111. package/dist/queueEvents/index.d.ts.map +0 -1
  112. package/dist/queueEvents/index.js +0 -7
  113. package/dist/queueEvents/index.js.map +0 -1
  114. package/dist/queueEvents/queueEvents.core.d.ts +0 -6
  115. package/dist/queueEvents/queueEvents.core.d.ts.map +0 -1
  116. package/dist/queueEvents/queueEvents.core.js +0 -12
  117. package/dist/queueEvents/queueEvents.core.js.map +0 -1
  118. package/dist/queueEvents/queueEvents.type.d.ts +0 -97
  119. package/dist/queueEvents/queueEvents.type.d.ts.map +0 -1
  120. package/dist/queueEvents/queueEvents.type.js +0 -2
  121. package/dist/queueEvents/queueEvents.type.js.map +0 -1
  122. package/dist/queueManager/index.d.ts.map +0 -1
  123. package/dist/queueManager/index.js.map +0 -1
  124. package/dist/queueManager/queueManager.core.d.ts.map +0 -1
  125. package/dist/queueManager/queueManager.core.js.map +0 -1
  126. package/dist/queueManager/queueManager.type.d.ts.map +0 -1
  127. package/dist/queueManager/queueManager.type.js.map +0 -1
  128. package/dist/queueRegistry/index.d.ts.map +0 -1
  129. package/dist/queueRegistry/index.js.map +0 -1
  130. package/dist/queueRegistry/queueRegistry.core.d.ts.map +0 -1
  131. package/dist/queueRegistry/queueRegistry.core.js.map +0 -1
  132. package/dist/queueRegistry/queueRegistry.type.d.ts.map +0 -1
  133. package/dist/queueRegistry/queueRegistry.type.js.map +0 -1
  134. package/dist/retryPolicy/index.d.ts.map +0 -1
  135. package/dist/retryPolicy/index.js.map +0 -1
  136. package/dist/retryPolicy/retryPolicy.core.d.ts.map +0 -1
  137. package/dist/retryPolicy/retryPolicy.core.js.map +0 -1
  138. package/dist/serializer/index.d.ts.map +0 -1
  139. package/dist/serializer/index.js.map +0 -1
  140. package/dist/serializer/serializer.core.d.ts.map +0 -1
  141. package/dist/serializer/serializer.core.js.map +0 -1
  142. package/dist/serializer/serializer.type.d.ts.map +0 -1
  143. package/dist/serializer/serializer.type.js.map +0 -1
  144. package/dist/worker/index.d.ts.map +0 -1
  145. package/dist/worker/index.js.map +0 -1
  146. package/dist/worker/worker.core.d.ts.map +0 -1
  147. package/dist/worker/worker.core.js.map +0 -1
  148. package/dist/worker/worker.type.d.ts.map +0 -1
  149. package/dist/worker/worker.type.js.map +0 -1
@@ -1,48 +1,138 @@
1
- import { WorkerState } from "../jobTypes/jobTypes.type.js";
1
+ import { JobState as JobStateEnum, WorkerState, } from "../jobTypes/jobTypes.type.js";
2
2
  import { WorkerLifecycleError } from "@zudojs/errors";
3
+ /** How long `stop()` waits for in-flight jobs before forcing a stop. */
4
+ const DEFAULT_DRAIN_TIMEOUT_MS = 30_000;
3
5
  /**
4
6
  * Creates a new Worker.
7
+ *
8
+ * The worker claims each job before running it, so a job is never picked
9
+ * up twice — by this worker on its next poll, or by another worker on the
10
+ * same queue.
5
11
  */
6
12
  export function createWorker(id, queue, options) {
7
13
  let state = WorkerState.CREATED;
8
- let stats = { processed: 0, succeeded: 0, failed: 0, concurrency: 0 };
9
- const concurrency = options?.concurrency ?? 1;
14
+ const stats = { processed: 0, succeeded: 0, failed: 0 };
15
+ const concurrency = Math.max(1, options?.concurrency ?? 1);
10
16
  const pollInterval = options?.pollInterval ?? 100;
17
+ const drainTimeout = options?.drainTimeout ?? DEFAULT_DRAIN_TIMEOUT_MS;
11
18
  let pollTimer = null;
12
19
  let activeJobs = 0;
20
+ let polling = false;
13
21
  let abortController = null;
14
- const poll = async () => {
15
- if (state !== WorkerState.RUNNING || abortController?.signal.aborted) {
16
- return;
17
- }
18
- if (activeJobs >= concurrency) {
19
- pollTimer = setTimeout(poll, pollInterval);
20
- return;
21
- }
22
- const job = await queue.getNextJob();
23
- if (!job) {
24
- pollTimer = setTimeout(poll, pollInterval);
22
+ const onError = options?.onError ??
23
+ ((error) => {
24
+ queueMicrotask(() => {
25
+ console.error(`[@zudojs/queue] Worker "${id}" poll failed.`, error);
26
+ });
27
+ });
28
+ /**
29
+ * Arms the next poll. At most one timer is ever armed: a delayed poll
30
+ * already pending is left alone, while an immediate poll (capacity just
31
+ * freed up, or a job was just dispatched) supersedes it.
32
+ */
33
+ const scheduleNextPoll = (delay) => {
34
+ if (state !== WorkerState.RUNNING) {
25
35
  return;
26
36
  }
27
- const proc = queue.getProcessor(job.name);
28
- if (!proc) {
29
- pollTimer = setTimeout(poll, pollInterval);
30
- return;
37
+ if (pollTimer !== null) {
38
+ if (delay > 0)
39
+ return;
40
+ clearTimeout(pollTimer);
31
41
  }
32
- activeJobs++;
33
- stats.processed++;
42
+ pollTimer = setTimeout(runPoll, delay);
43
+ pollTimer.unref?.();
44
+ };
45
+ /**
46
+ * Wraps `poll` so a rejection can never escape as an unhandled promise
47
+ * rejection — which, under a runtime configured to treat those as
48
+ * fatal, would take down the whole application.
49
+ */
50
+ const runPoll = () => {
51
+ pollTimer = null;
52
+ void poll().catch((error) => {
53
+ onError(error);
54
+ scheduleNextPoll(pollInterval);
55
+ });
56
+ };
57
+ /**
58
+ * Runs one claimed job to completion and frees its concurrency slot.
59
+ *
60
+ * Deliberately not awaited by `poll`: awaiting it there serialised the
61
+ * worker, so `concurrency` was reported by `getStats()` and honoured by
62
+ * nothing.
63
+ */
64
+ const runClaimedJob = async (job) => {
34
65
  try {
35
- await proc(job, {
36
- signal: abortController?.signal ?? new AbortController().signal,
66
+ // Dispatch through the queue rather than invoking the processor
67
+ // directly. The queue owns job state, retry, dead-lettering and
68
+ // middleware; calling the processor here left every job the worker
69
+ // ran stuck in `active` forever.
70
+ await queue.runJob(job, {
71
+ ...(options?.middleware ? { middleware: options.middleware } : {}),
72
+ ...(abortController ? { signal: abortController.signal } : {}),
73
+ ...(options?.timeoutMs !== undefined
74
+ ? { timeoutMs: options.timeoutMs }
75
+ : {}),
37
76
  });
38
- stats.succeeded++;
77
+ const settled = await queue.getJob(job.id);
78
+ if (settled?.state === JobStateEnum.COMPLETED) {
79
+ stats.succeeded++;
80
+ }
81
+ else {
82
+ stats.failed++;
83
+ }
39
84
  }
40
- catch {
85
+ catch (error) {
41
86
  stats.failed++;
87
+ onError(error);
42
88
  }
43
89
  finally {
44
90
  activeJobs--;
45
- pollTimer = setTimeout(poll, 0);
91
+ // A slot just opened: poll again immediately, yielding to the event
92
+ // loop first so a saturated queue cannot starve timers.
93
+ scheduleNextPoll(0);
94
+ }
95
+ };
96
+ const poll = async () => {
97
+ if (polling ||
98
+ state !== WorkerState.RUNNING ||
99
+ abortController?.signal.aborted) {
100
+ return;
101
+ }
102
+ polling = true;
103
+ try {
104
+ if (activeJobs >= concurrency) {
105
+ scheduleNextPoll(pollInterval);
106
+ return;
107
+ }
108
+ const job = await queue.claimNextJob();
109
+ if (!job) {
110
+ scheduleNextPoll(pollInterval);
111
+ return;
112
+ }
113
+ const proc = queue.getProcessor(job.name);
114
+ if (!proc) {
115
+ // Claimed but unrunnable: release it rather than stranding it in
116
+ // `active` where nothing would ever pick it up again.
117
+ await queue.releaseJob(job.id);
118
+ scheduleNextPoll(pollInterval);
119
+ return;
120
+ }
121
+ activeJobs++;
122
+ stats.processed++;
123
+ void runClaimedJob(job);
124
+ // Capacity may remain: look for more work now, not after this job
125
+ // settles.
126
+ scheduleNextPoll(0);
127
+ }
128
+ finally {
129
+ polling = false;
130
+ }
131
+ };
132
+ const clearPollTimer = () => {
133
+ if (pollTimer) {
134
+ clearTimeout(pollTimer);
135
+ pollTimer = null;
46
136
  }
47
137
  };
48
138
  return {
@@ -60,34 +150,46 @@ export function createWorker(id, queue, options) {
60
150
  abortController = new AbortController();
61
151
  try {
62
152
  state = WorkerState.RUNNING;
63
- pollTimer = setTimeout(poll, 0);
153
+ scheduleNextPoll(0);
64
154
  }
65
155
  catch (error) {
66
156
  state = WorkerState.FAILED;
67
157
  throw error;
68
158
  }
69
159
  },
160
+ /**
161
+ * Stops accepting new jobs and waits for in-flight ones to finish.
162
+ *
163
+ * The wait is bounded: past `drainTimeout` the worker force-stops
164
+ * rather than hanging shutdown on a job that never settles.
165
+ */
70
166
  async stop() {
71
- if (state !== WorkerState.RUNNING) {
167
+ if (state !== WorkerState.RUNNING && state !== WorkerState.STARTING) {
168
+ // Still clear any timer armed before the state moved on.
169
+ clearPollTimer();
72
170
  return;
73
171
  }
74
172
  state = WorkerState.DRAINING;
75
- abortController?.abort();
76
- while (activeJobs > 0) {
77
- await new Promise((resolve) => setTimeout(resolve, 50));
173
+ clearPollTimer();
174
+ // Graceful means graceful: in-flight jobs get `drainTimeout` to
175
+ // finish on their own. Aborting them up front — as this once did —
176
+ // made `stop()` indistinguishable from `forceStop()` for any
177
+ // processor that honours its signal, and turned every routine
178
+ // shutdown into a batch of failed jobs.
179
+ const deadline = Date.now() + Math.max(0, drainTimeout);
180
+ while (activeJobs > 0 && Date.now() < deadline) {
181
+ await new Promise((resolve) => setTimeout(resolve, 25));
78
182
  }
79
- if (pollTimer) {
80
- clearTimeout(pollTimer);
81
- pollTimer = null;
183
+ if (activeJobs > 0) {
184
+ onError(new WorkerLifecycleError(`Worker "${id}" still had ${activeJobs} job(s) in flight after ${drainTimeout}ms; forcing stop.`, { workerId: id }));
185
+ abortController?.abort();
82
186
  }
187
+ clearPollTimer();
83
188
  state = WorkerState.STOPPED;
84
189
  },
85
190
  async forceStop() {
86
191
  abortController?.abort();
87
- if (pollTimer) {
88
- clearTimeout(pollTimer);
89
- pollTimer = null;
90
- }
192
+ clearPollTimer();
91
193
  state = WorkerState.STOPPED;
92
194
  },
93
195
  isRunning() {
@@ -9,12 +9,26 @@ export interface WorkerOptions {
9
9
  readonly concurrency?: number;
10
10
  /** Poll interval in milliseconds. */
11
11
  readonly pollInterval?: number;
12
- /** Maximum number of stalled jobs before marking as failed. */
13
- readonly maxStalledCount?: number;
14
- /** Job timeout in milliseconds. */
12
+ /**
13
+ * Default job timeout in milliseconds, applied to jobs that do not
14
+ * carry their own. Stall detection is a queue-level concern; configure
15
+ * it with `QueueOptions.stalledAfter` and `maxStalledCount`.
16
+ */
15
17
  readonly timeoutMs?: number;
16
18
  /** Middleware for job processing. */
17
19
  readonly middleware?: QueueMiddleware[];
20
+ /**
21
+ * How long `stop()` waits for in-flight jobs before forcing a stop, in
22
+ * milliseconds. Defaults to 30000. Without a bound, one stuck job
23
+ * hangs shutdown forever.
24
+ */
25
+ readonly drainTimeout?: number;
26
+ /**
27
+ * Invoked for errors raised outside a job — a failing poll, a job that
28
+ * threw, or a drain that timed out. Defaults to reporting on the
29
+ * console. Poll errors are never left as unhandled rejections.
30
+ */
31
+ readonly onError?: (error: unknown) => void;
18
32
  }
19
33
  /**
20
34
  * Worker lifecycle states.
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/queue",
3
- "version": "0.1.0",
3
+ "version": "1.1.0",
4
4
  "description": "Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.",
5
5
  "license": "MIT",
6
+ "author": {
7
+ "name": "Oluwayemi Oyinlola",
8
+ "url": "https://github.com/oyinlola-tech"
9
+ },
6
10
  "type": "module",
7
11
  "main": "./dist/index.js",
8
12
  "module": "./dist/index.js",
@@ -15,19 +19,15 @@
15
19
  }
16
20
  },
17
21
  "files": [
18
- "dist"
22
+ "dist",
23
+ "!dist/**/*.map",
24
+ "!dist/**/*.tsbuildinfo",
25
+ "!dist/.tsbuildinfo"
19
26
  ],
20
- "scripts": {
21
- "build": "tsc -p tsconfig.json",
22
- "typecheck": "tsc -p tsconfig.json --noEmit",
23
- "clean": "rm -rf dist",
24
- "test": "vitest run",
25
- "test:watch": "vitest"
26
- },
27
27
  "dependencies": {
28
- "@zudojs/errors": "0.1.0",
29
- "@zudojs/constants": "0.1.0",
30
- "@zudojs/serialization": "0.1.0"
28
+ "@zudojs/errors": "1.0.1",
29
+ "@zudojs/constants": "1.0.1",
30
+ "@zudojs/serialization": "1.0.1"
31
31
  },
32
32
  "devDependencies": {
33
33
  "typescript": "7.0.2",
@@ -47,8 +47,19 @@
47
47
  "worker"
48
48
  ],
49
49
  "homepage": "https://github.com/oyinlola-tech/zudo#readme",
50
+ "bugs": {
51
+ "url": "https://github.com/oyinlola-tech/zudo/issues"
52
+ },
50
53
  "repository": {
51
54
  "type": "git",
52
- "url": "https://github.com/oyinlola-tech/zudo"
55
+ "url": "https://github.com/oyinlola-tech/zudo",
56
+ "directory": "packages/queue"
57
+ },
58
+ "scripts": {
59
+ "build": "tsc -p tsconfig.json",
60
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
61
+ "clean": "rm -rf dist",
62
+ "test": "vitest run",
63
+ "test:watch": "vitest"
53
64
  }
54
- }
65
+ }