@zudojs/queue 0.0.1 → 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.
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 +87 -23
  37. package/dist/worker/worker.type.d.ts +17 -3
  38. package/package.json +21 -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,105 @@
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;
13
20
  let abortController = null;
21
+ const onError = options?.onError ??
22
+ ((error) => {
23
+ queueMicrotask(() => {
24
+ console.error(`[@zudojs/queue] Worker "${id}" poll failed.`, error);
25
+ });
26
+ });
27
+ const scheduleNextPoll = (delay) => {
28
+ if (state !== WorkerState.RUNNING) {
29
+ return;
30
+ }
31
+ pollTimer = setTimeout(runPoll, delay);
32
+ pollTimer.unref?.();
33
+ };
34
+ /**
35
+ * Wraps `poll` so a rejection can never escape as an unhandled promise
36
+ * rejection — which, under a runtime configured to treat those as
37
+ * fatal, would take down the whole application.
38
+ */
39
+ const runPoll = () => {
40
+ void poll().catch((error) => {
41
+ onError(error);
42
+ scheduleNextPoll(pollInterval);
43
+ });
44
+ };
14
45
  const poll = async () => {
15
46
  if (state !== WorkerState.RUNNING || abortController?.signal.aborted) {
16
47
  return;
17
48
  }
18
49
  if (activeJobs >= concurrency) {
19
- pollTimer = setTimeout(poll, pollInterval);
50
+ scheduleNextPoll(pollInterval);
20
51
  return;
21
52
  }
22
- const job = await queue.getNextJob();
53
+ const job = await queue.claimNextJob();
23
54
  if (!job) {
24
- pollTimer = setTimeout(poll, pollInterval);
55
+ scheduleNextPoll(pollInterval);
25
56
  return;
26
57
  }
27
58
  const proc = queue.getProcessor(job.name);
28
59
  if (!proc) {
29
- pollTimer = setTimeout(poll, pollInterval);
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);
30
64
  return;
31
65
  }
32
66
  activeJobs++;
33
67
  stats.processed++;
34
68
  try {
35
- await proc(job, {
36
- signal: abortController?.signal ?? new AbortController().signal,
69
+ // Dispatch through the queue rather than invoking the processor
70
+ // directly. The queue owns job state, retry, dead-lettering and
71
+ // middleware; calling the processor here left every job the worker
72
+ // ran stuck in `active` forever.
73
+ await queue.runJob(job, {
74
+ ...(options?.middleware ? { middleware: options.middleware } : {}),
75
+ ...(abortController ? { signal: abortController.signal } : {}),
76
+ ...(options?.timeoutMs !== undefined
77
+ ? { timeoutMs: options.timeoutMs }
78
+ : {}),
37
79
  });
38
- stats.succeeded++;
80
+ const settled = await queue.getJob(job.id);
81
+ if (settled?.state === JobStateEnum.COMPLETED) {
82
+ stats.succeeded++;
83
+ }
84
+ else {
85
+ stats.failed++;
86
+ }
39
87
  }
40
- catch {
88
+ catch (error) {
41
89
  stats.failed++;
90
+ onError(error);
42
91
  }
43
92
  finally {
44
93
  activeJobs--;
45
- pollTimer = setTimeout(poll, 0);
94
+ // Poll again immediately while there is capacity, but yield to the
95
+ // event loop first so a saturated queue cannot starve timers.
96
+ scheduleNextPoll(0);
97
+ }
98
+ };
99
+ const clearPollTimer = () => {
100
+ if (pollTimer) {
101
+ clearTimeout(pollTimer);
102
+ pollTimer = null;
46
103
  }
47
104
  };
48
105
  return {
@@ -60,34 +117,41 @@ export function createWorker(id, queue, options) {
60
117
  abortController = new AbortController();
61
118
  try {
62
119
  state = WorkerState.RUNNING;
63
- pollTimer = setTimeout(poll, 0);
120
+ scheduleNextPoll(0);
64
121
  }
65
122
  catch (error) {
66
123
  state = WorkerState.FAILED;
67
124
  throw error;
68
125
  }
69
126
  },
127
+ /**
128
+ * Stops accepting new jobs and waits for in-flight ones to finish.
129
+ *
130
+ * The wait is bounded: past `drainTimeout` the worker force-stops
131
+ * rather than hanging shutdown on a job that never settles.
132
+ */
70
133
  async stop() {
71
- if (state !== WorkerState.RUNNING) {
134
+ if (state !== WorkerState.RUNNING && state !== WorkerState.STARTING) {
135
+ // Still clear any timer armed before the state moved on.
136
+ clearPollTimer();
72
137
  return;
73
138
  }
74
139
  state = WorkerState.DRAINING;
140
+ clearPollTimer();
75
141
  abortController?.abort();
76
- while (activeJobs > 0) {
77
- await new Promise((resolve) => setTimeout(resolve, 50));
142
+ const deadline = Date.now() + Math.max(0, drainTimeout);
143
+ while (activeJobs > 0 && Date.now() < deadline) {
144
+ await new Promise((resolve) => setTimeout(resolve, 25));
78
145
  }
79
- if (pollTimer) {
80
- clearTimeout(pollTimer);
81
- pollTimer = null;
146
+ if (activeJobs > 0) {
147
+ onError(new WorkerLifecycleError(`Worker "${id}" still had ${activeJobs} job(s) in flight after ${drainTimeout}ms; forcing stop.`, { workerId: id }));
82
148
  }
149
+ clearPollTimer();
83
150
  state = WorkerState.STOPPED;
84
151
  },
85
152
  async forceStop() {
86
153
  abortController?.abort();
87
- if (pollTimer) {
88
- clearTimeout(pollTimer);
89
- pollTimer = null;
90
- }
154
+ clearPollTimer();
91
155
  state = WorkerState.STOPPED;
92
156
  },
93
157
  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,6 +1,6 @@
1
1
  {
2
2
  "name": "@zudojs/queue",
3
- "version": "0.0.1",
3
+ "version": "1.0.0",
4
4
  "description": "Background job and asynchronous task infrastructure with in-memory and adapter-based queue implementations.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -15,19 +15,15 @@
15
15
  }
16
16
  },
17
17
  "files": [
18
- "dist"
18
+ "dist",
19
+ "!dist/**/*.map",
20
+ "!dist/**/*.tsbuildinfo",
21
+ "!dist/.tsbuildinfo"
19
22
  ],
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
23
  "dependencies": {
28
- "@zudojs/errors": "workspace:*",
29
- "@zudojs/constants": "workspace:*",
30
- "@zudojs/serialization": "workspace:*"
24
+ "@zudojs/errors": "1.0.0",
25
+ "@zudojs/constants": "1.0.0",
26
+ "@zudojs/serialization": "1.0.0"
31
27
  },
32
28
  "devDependencies": {
33
29
  "typescript": "7.0.2",
@@ -47,8 +43,19 @@
47
43
  "worker"
48
44
  ],
49
45
  "homepage": "https://github.com/oyinlola-tech/zudo#readme",
46
+ "bugs": {
47
+ "url": "https://github.com/oyinlola-tech/zudo/issues"
48
+ },
50
49
  "repository": {
51
50
  "type": "git",
52
- "url": "https://github.com/oyinlola-tech/zudo"
51
+ "url": "https://github.com/oyinlola-tech/zudo",
52
+ "directory": "packages/queue"
53
+ },
54
+ "scripts": {
55
+ "build": "tsc -p tsconfig.json",
56
+ "typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.test.json --noEmit",
57
+ "clean": "rm -rf dist",
58
+ "test": "vitest run",
59
+ "test:watch": "vitest"
53
60
  }
54
- }
61
+ }