@zudojs/queue 1.0.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.
@@ -17,6 +17,7 @@ export function createWorker(id, queue, options) {
17
17
  const drainTimeout = options?.drainTimeout ?? DEFAULT_DRAIN_TIMEOUT_MS;
18
18
  let pollTimer = null;
19
19
  let activeJobs = 0;
20
+ let polling = false;
20
21
  let abortController = null;
21
22
  const onError = options?.onError ??
22
23
  ((error) => {
@@ -24,10 +25,20 @@ export function createWorker(id, queue, options) {
24
25
  console.error(`[@zudojs/queue] Worker "${id}" poll failed.`, error);
25
26
  });
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
+ */
27
33
  const scheduleNextPoll = (delay) => {
28
34
  if (state !== WorkerState.RUNNING) {
29
35
  return;
30
36
  }
37
+ if (pollTimer !== null) {
38
+ if (delay > 0)
39
+ return;
40
+ clearTimeout(pollTimer);
41
+ }
31
42
  pollTimer = setTimeout(runPoll, delay);
32
43
  pollTimer.unref?.();
33
44
  };
@@ -37,34 +48,20 @@ export function createWorker(id, queue, options) {
37
48
  * fatal, would take down the whole application.
38
49
  */
39
50
  const runPoll = () => {
51
+ pollTimer = null;
40
52
  void poll().catch((error) => {
41
53
  onError(error);
42
54
  scheduleNextPoll(pollInterval);
43
55
  });
44
56
  };
45
- const poll = async () => {
46
- if (state !== WorkerState.RUNNING || abortController?.signal.aborted) {
47
- return;
48
- }
49
- if (activeJobs >= concurrency) {
50
- scheduleNextPoll(pollInterval);
51
- return;
52
- }
53
- const job = await queue.claimNextJob();
54
- if (!job) {
55
- scheduleNextPoll(pollInterval);
56
- return;
57
- }
58
- const proc = queue.getProcessor(job.name);
59
- if (!proc) {
60
- // Claimed but unrunnable: release it rather than stranding it in
61
- // `active` where nothing would ever pick it up again.
62
- await queue.releaseJob(job.id);
63
- scheduleNextPoll(pollInterval);
64
- return;
65
- }
66
- activeJobs++;
67
- stats.processed++;
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) => {
68
65
  try {
69
66
  // Dispatch through the queue rather than invoking the processor
70
67
  // directly. The queue owns job state, retry, dead-lettering and
@@ -91,11 +88,47 @@ export function createWorker(id, queue, options) {
91
88
  }
92
89
  finally {
93
90
  activeJobs--;
94
- // Poll again immediately while there is capacity, but yield to the
95
- // event loop first so a saturated queue cannot starve timers.
91
+ // A slot just opened: poll again immediately, yielding to the event
92
+ // loop first so a saturated queue cannot starve timers.
96
93
  scheduleNextPoll(0);
97
94
  }
98
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
+ };
99
132
  const clearPollTimer = () => {
100
133
  if (pollTimer) {
101
134
  clearTimeout(pollTimer);
@@ -138,13 +171,18 @@ export function createWorker(id, queue, options) {
138
171
  }
139
172
  state = WorkerState.DRAINING;
140
173
  clearPollTimer();
141
- abortController?.abort();
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.
142
179
  const deadline = Date.now() + Math.max(0, drainTimeout);
143
180
  while (activeJobs > 0 && Date.now() < deadline) {
144
181
  await new Promise((resolve) => setTimeout(resolve, 25));
145
182
  }
146
183
  if (activeJobs > 0) {
147
184
  onError(new WorkerLifecycleError(`Worker "${id}" still had ${activeJobs} job(s) in flight after ${drainTimeout}ms; forcing stop.`, { workerId: id }));
185
+ abortController?.abort();
148
186
  }
149
187
  clearPollTimer();
150
188
  state = WorkerState.STOPPED;
package/package.json CHANGED
@@ -1,8 +1,12 @@
1
1
  {
2
2
  "name": "@zudojs/queue",
3
- "version": "1.0.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",
@@ -21,9 +25,9 @@
21
25
  "!dist/.tsbuildinfo"
22
26
  ],
23
27
  "dependencies": {
24
- "@zudojs/errors": "1.0.0",
25
- "@zudojs/constants": "1.0.0",
26
- "@zudojs/serialization": "1.0.0"
28
+ "@zudojs/errors": "1.0.1",
29
+ "@zudojs/constants": "1.0.1",
30
+ "@zudojs/serialization": "1.0.1"
27
31
  },
28
32
  "devDependencies": {
29
33
  "typescript": "7.0.2",