@holo-js/queue 0.3.28 → 0.4.1

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/dist/index.mjs CHANGED
@@ -7,182 +7,16 @@ import {
7
7
  DEFAULT_QUEUE_NAME,
8
8
  DEFAULT_QUEUE_RETRY_AFTER,
9
9
  DEFAULT_QUEUE_SLEEP,
10
+ QUEUE_TIMER_MAX_SECONDS,
11
+ assertQueueTimeoutBelowRetryAfter,
12
+ defineJob,
10
13
  holoQueueDefaults,
14
+ isQueueJobDefinition,
11
15
  normalizeQueueConfig,
12
- queueInternals
13
- } from "./chunk-5MPOVBVE.mjs";
14
-
15
- // src/contracts.ts
16
- function getQueueJobDispatcher() {
17
- return globalThis.__holoQueueJobDispatcher__;
18
- }
19
- function setQueueJobDispatcher(dispatcher) {
20
- const runtime = globalThis;
21
- runtime.__holoQueueJobDispatcher__ = dispatcher;
22
- }
23
- function getQueueJobSyncDispatcher() {
24
- return globalThis.__holoQueueJobSyncDispatcher__;
25
- }
26
- function setQueueJobSyncDispatcher(dispatcher) {
27
- const runtime = globalThis;
28
- runtime.__holoQueueJobSyncDispatcher__ = dispatcher;
29
- }
30
- function getQueueJobDefinitionNames() {
31
- const runtime = globalThis;
32
- runtime.__holoQueueJobDefinitionNames__ ??= /* @__PURE__ */ new WeakMap();
33
- return runtime.__holoQueueJobDefinitionNames__;
34
- }
35
- function getQueueJobDefinitionsByName() {
36
- const runtime = globalThis;
37
- runtime.__holoQueueJobDefinitionsByName__ ??= /* @__PURE__ */ new Map();
38
- return runtime.__holoQueueJobDefinitionsByName__;
39
- }
40
- function setQueueJobDefinitionName(definition, name) {
41
- getQueueJobDefinitionNames().set(definition, name);
42
- const definitions = getQueueJobDefinitionsByName().get(name) ?? /* @__PURE__ */ new Set();
43
- definitions.add(definition);
44
- getQueueJobDefinitionsByName().set(name, definitions);
45
- }
46
- function deleteQueueJobDefinitionName(name) {
47
- const definitions = getQueueJobDefinitionsByName().get(name);
48
- for (const definition of definitions ?? []) {
49
- if (getQueueJobDefinitionNames().get(definition) === name) {
50
- getQueueJobDefinitionNames().delete(definition);
51
- }
52
- }
53
- getQueueJobDefinitionsByName().delete(name);
54
- }
55
- function clearQueueJobDefinitionNames() {
56
- for (const name of getQueueJobDefinitionsByName().keys()) {
57
- deleteQueueJobDefinitionName(name);
58
- }
59
- }
60
- function resolveQueueJobDefinitionName(definition) {
61
- const name = getQueueJobDefinitionNames().get(definition);
62
- if (name) {
63
- return name;
64
- }
65
- throw new Error("[Holo Queue] Job definitions cannot dispatch before the job is registered.");
66
- }
67
- function dispatchDefinedQueueJob(definition, payload) {
68
- const dispatcher = getQueueJobDispatcher();
69
- if (!dispatcher) {
70
- throw new Error("[Holo Queue] Job definitions cannot dispatch before the queue runtime is loaded.");
71
- }
72
- return dispatcher(resolveQueueJobDefinitionName(definition), payload);
73
- }
74
- function dispatchDefinedQueueJobSync(definition, payload) {
75
- const dispatcher = getQueueJobSyncDispatcher();
76
- if (!dispatcher) {
77
- throw new Error("[Holo Queue] Job definitions cannot dispatch before the queue runtime is loaded.");
78
- }
79
- return dispatcher(resolveQueueJobDefinitionName(definition), payload);
80
- }
81
- function normalizeOptionalString(value, label) {
82
- if (typeof value === "undefined") {
83
- return void 0;
84
- }
85
- const normalized = value.trim();
86
- if (!normalized) {
87
- throw new Error(`[Holo Queue] ${label} must be a non-empty string when provided.`);
88
- }
89
- return normalized;
90
- }
91
- function normalizeOptionalInteger(value, label, options = {}) {
92
- if (typeof value === "undefined") {
93
- return void 0;
94
- }
95
- if (!Number.isInteger(value)) {
96
- throw new Error(`[Holo Queue] ${label} must be an integer when provided.`);
97
- }
98
- if (typeof options.minimum === "number" && value < options.minimum) {
99
- throw new Error(`[Holo Queue] ${label} must be greater than or equal to ${options.minimum}.`);
100
- }
101
- return value;
102
- }
103
- function normalizeBackoff(value) {
104
- if (typeof value === "undefined") {
105
- return void 0;
106
- }
107
- if (typeof value === "number") {
108
- return normalizeOptionalInteger(value, "Job backoff", { minimum: 0 });
109
- }
110
- if (!Array.isArray(value)) {
111
- throw new Error("[Holo Queue] Job backoff must be a number or an array of integers.");
112
- }
113
- const normalized = value.map((entry, index) => {
114
- if (!Number.isInteger(entry)) {
115
- throw new Error(`[Holo Queue] Job backoff entry at index ${index} must be an integer.`);
116
- }
117
- if (entry < 0) {
118
- throw new Error(`[Holo Queue] Job backoff entry at index ${index} must be greater than or equal to 0.`);
119
- }
120
- return entry;
121
- });
122
- return Object.freeze(normalized);
123
- }
124
- function normalizeOptionalHook(value, label) {
125
- if (typeof value === "undefined") {
126
- return void 0;
127
- }
128
- if (typeof value !== "function") {
129
- throw new Error(`[Holo Queue] ${label} must be a function when provided.`);
130
- }
131
- return value;
132
- }
133
- function isQueueJobDefinition(value) {
134
- return value !== null && typeof value === "object" && "handle" in value && typeof value.handle === "function";
135
- }
136
- function normalizeQueueJobDefinition(job) {
137
- if (!isQueueJobDefinition(job)) {
138
- throw new Error('[Holo Queue] Jobs must define a "handle" function.');
139
- }
140
- return {
141
- ...job,
142
- ...typeof job.connection === "undefined" ? {} : { connection: normalizeOptionalString(job.connection, "Job connection") },
143
- ...typeof job.queue === "undefined" ? {} : { queue: normalizeOptionalString(job.queue, "Job queue") },
144
- ...typeof job.tries === "undefined" ? {} : { tries: normalizeOptionalInteger(job.tries, "Job tries", { minimum: 1 }) },
145
- ...typeof job.timeout === "undefined" ? {} : { timeout: normalizeOptionalInteger(job.timeout, "Job timeout", { minimum: 0 }) },
146
- ...typeof job.backoff === "undefined" ? {} : { backoff: normalizeBackoff(job.backoff) },
147
- ...typeof job.onCompleted === "undefined" ? {} : { onCompleted: normalizeOptionalHook(job.onCompleted, "Job onCompleted hook") },
148
- ...typeof job.onFailed === "undefined" ? {} : { onFailed: normalizeOptionalHook(job.onFailed, "Job onFailed hook") }
149
- };
150
- }
151
- function defineJob(job) {
152
- const normalized = normalizeQueueJobDefinition(job);
153
- const registeredName = getQueueJobDefinitionNames().get(job);
154
- if (registeredName) {
155
- setQueueJobDefinitionName(normalized, registeredName);
156
- }
157
- Object.defineProperty(normalized, "dispatch", {
158
- value(payload) {
159
- return dispatchDefinedQueueJob(normalized, payload);
160
- },
161
- enumerable: false
162
- });
163
- Object.defineProperty(normalized, "dispatchSync", {
164
- value(payload) {
165
- return dispatchDefinedQueueJobSync(normalized, payload);
166
- },
167
- enumerable: false
168
- });
169
- return Object.freeze(normalized);
170
- }
171
- var queueJobInternals = {
172
- clearQueueJobDefinitionNames,
173
- deleteQueueJobDefinitionName,
174
- dispatchDefinedQueueJob,
175
- dispatchDefinedQueueJobSync,
176
16
  normalizeQueueJobDefinition,
177
- normalizeBackoff,
178
- normalizeOptionalHook,
179
- normalizeOptionalInteger,
180
- normalizeOptionalString,
181
- resolveQueueJobDefinitionName,
182
- setQueueJobDefinitionName,
183
- setQueueJobDispatcher,
184
- setQueueJobSyncDispatcher
185
- };
17
+ queueInternals,
18
+ queueJobInternals
19
+ } from "./chunk-CYZWPHVL.mjs";
186
20
 
187
21
  // src/registry.ts
188
22
  function toPosixPath(value) {
@@ -212,6 +46,10 @@ function resolveRegistrationName(definition, options = {}) {
212
46
  }
213
47
  throw new Error("[Holo Queue] Registered jobs require an explicit name or a sourcePath-derived name.");
214
48
  }
49
+ var registrationTimeoutCheck;
50
+ function setQueueJobRegistrationTimeoutCheck(check) {
51
+ registrationTimeoutCheck = check;
52
+ }
215
53
  function registerQueueJob(definition, options = {}) {
216
54
  if (!isQueueJobDefinition(definition)) {
217
55
  throw new Error('[Holo Queue] Jobs must define a "handle" function.');
@@ -222,6 +60,7 @@ function registerQueueJob(definition, options = {}) {
222
60
  if (registry.has(name) && options.replaceExisting !== true) {
223
61
  throw new Error(`[Holo Queue] Queue job "${name}" is already registered.`);
224
62
  }
63
+ registrationTimeoutCheck?.({ name, definition: normalizedDefinition });
225
64
  const entry = Object.freeze({
226
65
  name,
227
66
  ...options.sourcePath ? { sourcePath: options.sourcePath } : {},
@@ -262,6 +101,7 @@ var SyncQueueDriver = class {
262
101
  name;
263
102
  driver = "sync";
264
103
  mode = "sync";
104
+ acceptsJobId = true;
265
105
  context;
266
106
  constructor(connection, context) {
267
107
  this.name = connection.name;
@@ -292,6 +132,7 @@ var syncQueueDriverInternals = {
292
132
  };
293
133
 
294
134
  // src/runtime.ts
135
+ import { reportError } from "@holo-js/kernel/error-reporting";
295
136
  import { randomUUID } from "crypto";
296
137
 
297
138
  // src/plugins.ts
@@ -546,8 +387,7 @@ async function executeRegisteredQueueJobCompletedHook(envelope, result, contextO
546
387
  createJobContext(envelope, contextOverrides)
547
388
  );
548
389
  } catch (error) {
549
- const resolvedError = error instanceof Error ? error : new Error(String(error));
550
- console.warn(`[Holo Queue] onCompleted hook failed for job "${envelope.name}": ${resolvedError.message}`);
390
+ reportError(error, { capability: "queue", operation: "job.onCompleted" });
551
391
  }
552
392
  }
553
393
  async function executeRegisteredQueueJobFailedHook(envelope, error, contextOverrides = {}) {
@@ -562,8 +402,7 @@ async function executeRegisteredQueueJobFailedHook(envelope, error, contextOverr
562
402
  createJobContext(envelope, contextOverrides)
563
403
  );
564
404
  } catch (hookError) {
565
- const resolvedError = hookError instanceof Error ? hookError : new Error(String(hookError));
566
- console.warn(`[Holo Queue] onFailed hook failed for job "${envelope.name}": ${resolvedError.message}`);
405
+ reportError(hookError, { capability: "queue", operation: "job.onFailed" });
567
406
  }
568
407
  }
569
408
  async function dispatchThroughDriver(driver, envelope) {
@@ -646,7 +485,7 @@ function resolveSyncExecutionDriver() {
646
485
  state.drivers.set(INLINE_SYNC_DRIVER_KEY, driver);
647
486
  return driver;
648
487
  }
649
- function createJobEnvelope(jobName, payload, options) {
488
+ function createJobEnvelope(jobName, payload, options, { validateTimeout = true } = {}) {
650
489
  const runtime = getQueueRuntimeState();
651
490
  const registered = requireRegisteredQueueJob(jobName);
652
491
  const connection = resolveConnectionConfig(
@@ -654,6 +493,10 @@ function createJobEnvelope(jobName, payload, options) {
654
493
  options.connection ?? registered.definition.connection
655
494
  );
656
495
  const queue = normalizeQueueName(options.queue ?? registered.definition.queue ?? connection.queue ?? DEFAULT_QUEUE_NAME);
496
+ const tries = queueJobInternals.normalizeOptionalInteger(options.tries, "Queue dispatch tries", { minimum: 1 }) ?? registered.definition.tries ?? 1;
497
+ if (validateTimeout) {
498
+ assertQueueTimeoutBelowRetryAfter(`Queue job "${registered.name}"`, registered.definition.timeout, connection);
499
+ }
657
500
  return Object.freeze({
658
501
  id: randomUUID(),
659
502
  name: registered.name,
@@ -661,18 +504,36 @@ function createJobEnvelope(jobName, payload, options) {
661
504
  queue,
662
505
  payload,
663
506
  attempts: 0,
664
- maxAttempts: registered.definition.tries ?? 1,
507
+ maxAttempts: tries,
665
508
  ...typeof options.delay === "undefined" ? {} : { availableAt: normalizeDelay(options.delay) },
666
509
  createdAt: Date.now()
667
510
  });
668
511
  }
669
- function createDispatchEnvelope(jobName, payload, options = {}) {
512
+ function createDispatchEnvelope(jobName, payload, options = {}, { runsInline = false } = {}) {
670
513
  validateQueuePayload(payload);
671
- return createJobEnvelope(jobName, payload, options);
514
+ const snapshot = JSON.parse(JSON.stringify(payload));
515
+ return createJobEnvelope(jobName, snapshot, options, { validateTimeout: !runsInline });
516
+ }
517
+ var databaseSpecifier = "@holo-js/db";
518
+ async function loadDatabaseModule() {
519
+ try {
520
+ return await import(databaseSpecifier);
521
+ } catch (error) {
522
+ if (error && typeof error === "object" && "code" in error && (error.code === "ERR_MODULE_NOT_FOUND" || error.code === "MODULE_NOT_FOUND")) {
523
+ return null;
524
+ }
525
+ throw error;
526
+ }
527
+ }
528
+ async function resolveOpenDatabaseTransaction() {
529
+ const database = await loadDatabaseModule();
530
+ const active = database?.connectionAsyncContext.getActive()?.connection;
531
+ if (!active || active.getScope().kind === "root") {
532
+ return void 0;
533
+ }
534
+ return active;
672
535
  }
673
- async function dispatchRecord(envelope) {
674
- const driver = resolveConnectionDriver(envelope.connection);
675
- const result = await dispatchThroughDriver(driver, envelope);
536
+ function toDispatchResult(envelope, result) {
676
537
  return {
677
538
  jobId: result.jobId,
678
539
  connection: envelope.connection,
@@ -680,6 +541,47 @@ async function dispatchRecord(envelope) {
680
541
  synchronous: result.synchronous
681
542
  };
682
543
  }
544
+ async function dispatchRecord(envelope, options, handlers) {
545
+ try {
546
+ const driver = resolveConnectionDriver(envelope.connection);
547
+ const connection = resolveConnectionConfig(getQueueRuntimeState().config, envelope.connection);
548
+ const afterCommit = options.afterCommit ?? connection.afterCommit;
549
+ if (afterCommit && driver.acceptsJobId !== true) {
550
+ throw new Error(
551
+ `[Holo Queue] Queue connection "${envelope.connection}" uses driver "${driver.driver}", which does not accept a caller-supplied job ID, so afterCommit needs a driver that accepts the job ID.`
552
+ );
553
+ }
554
+ const transaction = afterCommit ? await resolveOpenDatabaseTransaction() : void 0;
555
+ if (transaction) {
556
+ transaction.afterCommit(async () => {
557
+ try {
558
+ const result = await dispatchThroughDriver(driver, envelope);
559
+ if (result.jobId !== envelope.id) {
560
+ throw new Error(
561
+ `[Holo Queue] Queue connection "${envelope.connection}" driver "${driver.driver}" returned job ID "${result.jobId}" for job "${envelope.id}", but it declares that it accepts the job ID.`
562
+ );
563
+ }
564
+ await handlers.completed(toDispatchResult(envelope, result));
565
+ } catch (error) {
566
+ await handlers.failed(error);
567
+ throw error;
568
+ }
569
+ });
570
+ return {
571
+ jobId: envelope.id,
572
+ connection: envelope.connection,
573
+ queue: envelope.queue,
574
+ synchronous: driver.mode === "sync"
575
+ };
576
+ }
577
+ const dispatched = toDispatchResult(envelope, await dispatchThroughDriver(driver, envelope));
578
+ await handlers.completed(dispatched);
579
+ return dispatched;
580
+ } catch (error) {
581
+ await handlers.failed(error);
582
+ throw error;
583
+ }
584
+ }
683
585
  var PendingQueueDispatch = class _PendingQueueDispatch {
684
586
  jobName;
685
587
  payload;
@@ -713,6 +615,12 @@ var PendingQueueDispatch = class _PendingQueueDispatch {
713
615
  delay: value
714
616
  }, this.completedHooks, this.failedHooks);
715
617
  }
618
+ afterCommit() {
619
+ return new _PendingQueueDispatch(this.jobName, this.payload, {
620
+ ...this.options,
621
+ afterCommit: true
622
+ }, this.completedHooks, this.failedHooks);
623
+ }
716
624
  onComplete(callback) {
717
625
  return new _PendingQueueDispatch(
718
626
  this.jobName,
@@ -745,32 +653,38 @@ var PendingQueueDispatch = class _PendingQueueDispatch {
745
653
  }
746
654
  execute() {
747
655
  this.executionPromise ??= (async () => {
656
+ let envelope;
748
657
  try {
749
- const envelope = createDispatchEnvelope(this.jobName, this.payload, this.options);
750
- const result = await dispatchRecord(envelope);
751
- for (const hook of this.completedHooks) {
752
- try {
753
- await hook(result);
754
- } catch (error) {
755
- const resolvedError = error instanceof Error ? error : new Error(String(error));
756
- console.warn(`[Holo Queue] onComplete hook failed during dispatch of "${this.jobName}": ${resolvedError.message}`);
757
- }
758
- }
759
- return result;
658
+ envelope = createDispatchEnvelope(this.jobName, this.payload, this.options);
760
659
  } catch (error) {
761
- for (const hook of this.failedHooks) {
762
- try {
763
- await hook(error);
764
- } catch (hookError) {
765
- const resolvedError = hookError instanceof Error ? hookError : new Error(String(hookError));
766
- console.warn(`[Holo Queue] onFailed hook failed during dispatch of "${this.jobName}": ${resolvedError.message}`);
767
- }
768
- }
660
+ await this.runFailedHooks(error);
769
661
  throw error;
770
662
  }
663
+ return dispatchRecord(envelope, { afterCommit: this.options.afterCommit }, {
664
+ completed: (result) => this.runCompletedHooks(result),
665
+ failed: (error) => this.runFailedHooks(error)
666
+ });
771
667
  })();
772
668
  return this.executionPromise;
773
669
  }
670
+ async runCompletedHooks(result) {
671
+ for (const hook of this.completedHooks) {
672
+ try {
673
+ await hook(result);
674
+ } catch (error) {
675
+ reportError(error, { capability: "queue", operation: "dispatch.onComplete" });
676
+ }
677
+ }
678
+ }
679
+ async runFailedHooks(error) {
680
+ for (const hook of this.failedHooks) {
681
+ try {
682
+ await hook(error);
683
+ } catch (hookError) {
684
+ reportError(hookError, { capability: "queue", operation: "dispatch.onFailed" });
685
+ }
686
+ }
687
+ }
774
688
  };
775
689
  function createQueueConnection(name) {
776
690
  const resolvedName = name ? normalizeConnectionName(name) : getQueueRuntimeState().config.default;
@@ -791,11 +705,27 @@ function createQueueConnection(name) {
791
705
  dispatchSync: dispatchSyncViaConnection
792
706
  };
793
707
  }
708
+ function assertJobTimeoutsBelowRetryAfter(config, jobs) {
709
+ for (const job of jobs) {
710
+ const connectionName = job.definition.connection ? normalizeConnectionName(job.definition.connection) : config.default;
711
+ const connection = config.connections[connectionName];
712
+ if (!connection) {
713
+ continue;
714
+ }
715
+ assertQueueTimeoutBelowRetryAfter(`Queue job "${job.name}"`, job.definition.timeout, connection);
716
+ }
717
+ }
718
+ function assertQueueJobTimeouts(jobs) {
719
+ assertJobTimeoutsBelowRetryAfter(getQueueRuntimeState().config, jobs);
720
+ }
721
+ setQueueJobRegistrationTimeoutCheck((job) => assertJobTimeoutsBelowRetryAfter(getQueueRuntimeState().config, [job]));
794
722
  function configureQueueRuntime(options = {}) {
795
723
  const state = getQueueRuntimeState();
796
724
  let shouldResetDrivers = false;
797
725
  if (options.config) {
798
- state.config = isNormalizedQueueConfig(options.config) ? options.config : normalizeQueueConfig(options.config, options.redisConfig);
726
+ const config = isNormalizedQueueConfig(options.config) ? options.config : normalizeQueueConfig(options.config, options.redisConfig);
727
+ assertJobTimeoutsBelowRetryAfter(config, listRegisteredQueueJobs().filter((job) => !job.sourcePath));
728
+ state.config = config;
799
729
  shouldResetDrivers = true;
800
730
  }
801
731
  if (options.driverFactories) {
@@ -843,7 +773,7 @@ function useQueueConnection(name) {
843
773
  }
844
774
  function dispatchSyncInternal(jobName, payload, options = {}) {
845
775
  return (async () => {
846
- const envelope = createDispatchEnvelope(jobName, payload, options);
776
+ const envelope = createDispatchEnvelope(jobName, payload, options, { runsInline: true });
847
777
  const result = await dispatchThroughDriver(
848
778
  resolveSyncExecutionDriver(),
849
779
  envelope
@@ -1006,6 +936,7 @@ var queueFailedInternals = {
1006
936
  };
1007
937
 
1008
938
  // src/worker.ts
939
+ import { reportError as reportError2 } from "@holo-js/kernel/error-reporting";
1009
940
  import { randomUUID as randomUUID2 } from "crypto";
1010
941
  var QueueWorkerUnsupportedDriverError = class extends Error {
1011
942
  constructor(connectionName, driverName) {
@@ -1028,13 +959,6 @@ function requireAsyncDriver(driver, connectionName) {
1028
959
  }
1029
960
  return driver;
1030
961
  }
1031
- function requireRegisteredDefinition(jobName) {
1032
- const registered = getRegisteredQueueJob(jobName);
1033
- if (!registered) {
1034
- throw new Error(`[Holo Queue] Queue job "${jobName}" is not registered.`);
1035
- }
1036
- return registered.definition;
1037
- }
1038
962
  function normalizeQueueNames(queueNames, fallbackQueueName) {
1039
963
  if (!queueNames || queueNames.length === 0) {
1040
964
  return Object.freeze([fallbackQueueName ?? DEFAULT_QUEUE_NAME]);
@@ -1045,13 +969,16 @@ function normalizeQueueNames(queueNames, fallbackQueueName) {
1045
969
  }
1046
970
  return Object.freeze(normalized);
1047
971
  }
1048
- function normalizeNonNegativeInteger(value, label) {
972
+ function normalizeNonNegativeInteger(value, label, options = {}) {
1049
973
  if (typeof value === "undefined") {
1050
974
  return void 0;
1051
975
  }
1052
976
  if (!Number.isInteger(value) || value < 0) {
1053
977
  throw new Error(`[Holo Queue] ${label} must be a non-negative integer.`);
1054
978
  }
979
+ if (typeof options.maximum === "number" && value > options.maximum) {
980
+ throw new Error(`[Holo Queue] ${label} must be less than or equal to ${options.maximum}.`);
981
+ }
1055
982
  return value;
1056
983
  }
1057
984
  function normalizePositiveInteger(value, label) {
@@ -1073,8 +1000,7 @@ function createWorkerEvent(envelope, maxAttempts) {
1073
1000
  maxAttempts
1074
1001
  };
1075
1002
  }
1076
- function resolveRetryDelaySeconds(definition, attempt) {
1077
- const { backoff } = definition;
1003
+ function resolveRetryDelaySeconds(backoff, attempt) {
1078
1004
  if (typeof backoff === "undefined") {
1079
1005
  return void 0;
1080
1006
  }
@@ -1083,15 +1009,14 @@ function resolveRetryDelaySeconds(definition, attempt) {
1083
1009
  }
1084
1010
  return backoff[Math.min(Math.max(attempt - 1, 0), backoff.length - 1)];
1085
1011
  }
1086
- function normalizeHookError(error) {
1012
+ function normalizeHandlerError(error) {
1087
1013
  return error instanceof Error ? error : new Error(String(error));
1088
1014
  }
1089
- async function runWorkerHook(jobName, hookName, callback) {
1015
+ async function runWorkerHook(hookName, callback) {
1090
1016
  try {
1091
1017
  await callback();
1092
1018
  } catch (error) {
1093
- const resolvedError = normalizeHookError(error);
1094
- console.warn(`[Holo Queue] ${hookName} hook failed for job "${jobName}": ${resolvedError.message}`);
1019
+ reportError2(error, { capability: "queue", operation: `worker.${hookName}` });
1095
1020
  }
1096
1021
  }
1097
1022
  async function runWithTimeout(callback, jobName, timeoutSeconds, onTimeout) {
@@ -1112,11 +1037,51 @@ async function runWithTimeout(callback, jobName, timeoutSeconds, onTimeout) {
1112
1037
  })
1113
1038
  ]);
1114
1039
  }
1040
+ async function failReservedJob(driver, reserved, event, options, failure) {
1041
+ const { error, runJobFailedHook } = failure;
1042
+ await queueRuntimeInternals.getQueueRuntimeState().failedJobStore?.persistFailedJob(reserved, error);
1043
+ await driver.delete(reserved);
1044
+ if (runJobFailedHook) {
1045
+ await queueRuntimeInternals.executeRegisteredQueueJobFailedHook(reserved.envelope, error, { maxAttempts: event.maxAttempts });
1046
+ }
1047
+ if (options.onJobFailed) {
1048
+ await runWorkerHook("onJobFailed", async () => {
1049
+ await options.onJobFailed?.({ ...event, error });
1050
+ });
1051
+ }
1052
+ return { kind: "failed", error };
1053
+ }
1115
1054
  async function processReservedJob(driver, reserved, options) {
1116
1055
  const envelope = reserved.envelope;
1117
- const definition = requireRegisteredDefinition(envelope.name);
1056
+ const registered = getRegisteredQueueJob(envelope.name);
1118
1057
  const maxAttempts = options.tries ?? envelope.maxAttempts;
1119
1058
  const event = createWorkerEvent(envelope, maxAttempts);
1059
+ if (!registered) {
1060
+ return await failReservedJob(driver, reserved, event, options, {
1061
+ error: new Error(`[Holo Queue] Queue job "${envelope.name}" is not registered.`),
1062
+ runJobFailedHook: false
1063
+ });
1064
+ }
1065
+ if (event.attempt > maxAttempts) {
1066
+ return await failReservedJob(driver, reserved, event, options, {
1067
+ error: new Error(`[Holo Queue] Queue job "${envelope.name}" has been attempted too many times.`),
1068
+ runJobFailedHook: true
1069
+ });
1070
+ }
1071
+ const definition = registered.definition;
1072
+ const timeoutSeconds = options.timeout ?? definition.timeout;
1073
+ try {
1074
+ assertQueueTimeoutBelowRetryAfter(
1075
+ `Queue job "${envelope.name}"`,
1076
+ timeoutSeconds,
1077
+ queueRuntimeInternals.resolveConnectionConfig(queueRuntimeInternals.getQueueRuntimeState().config, envelope.connection)
1078
+ );
1079
+ } catch (error) {
1080
+ return await failReservedJob(driver, reserved, event, options, {
1081
+ error: normalizeHandlerError(error),
1082
+ runJobFailedHook: true
1083
+ });
1084
+ }
1120
1085
  const executionState = {
1121
1086
  timedOut: false
1122
1087
  };
@@ -1146,19 +1111,24 @@ async function processReservedJob(driver, reserved, options) {
1146
1111
  }
1147
1112
  }),
1148
1113
  envelope.name,
1149
- options.timeout ?? definition.timeout,
1114
+ timeoutSeconds,
1150
1115
  () => {
1151
1116
  executionState.timedOut = true;
1152
1117
  }
1153
1118
  );
1154
1119
  } catch (error) {
1155
- handlerError = normalizeHookError(error);
1120
+ handlerError = normalizeHandlerError(error);
1121
+ }
1122
+ if (executionState.timedOut && handlerError) {
1123
+ if (definition.failOnTimeout === true) {
1124
+ await failReservedJob(driver, reserved, event, options, { error: handlerError, runJobFailedHook: true });
1125
+ }
1126
+ throw handlerError;
1156
1127
  }
1157
- const failure = handlerError instanceof QueueWorkerTimeoutError ? handlerError : requestedFailure ?? handlerError;
1128
+ const failure = requestedFailure ?? handlerError;
1129
+ const backoff = definition.backoff ?? options.backoff;
1158
1130
  let outcome;
1159
- if (handlerError instanceof QueueWorkerTimeoutError) {
1160
- outcome = { kind: "failed", error: handlerError };
1161
- } else if (action === "release") {
1131
+ if (action === "release") {
1162
1132
  outcome = {
1163
1133
  kind: "released",
1164
1134
  ...typeof requestedReleaseDelay === "number" ? { delaySeconds: requestedReleaseDelay } : {},
@@ -1167,7 +1137,7 @@ async function processReservedJob(driver, reserved, options) {
1167
1137
  } else if (failure) {
1168
1138
  outcome = action === "fail" || event.attempt >= maxAttempts ? { kind: "failed", error: failure } : {
1169
1139
  kind: "released",
1170
- delaySeconds: resolveRetryDelaySeconds(definition, event.attempt),
1140
+ delaySeconds: resolveRetryDelaySeconds(backoff, event.attempt),
1171
1141
  error: failure
1172
1142
  };
1173
1143
  } else {
@@ -1176,7 +1146,7 @@ async function processReservedJob(driver, reserved, options) {
1176
1146
  if (outcome.kind === "released") {
1177
1147
  await driver.release(reserved, typeof outcome.delaySeconds === "number" ? { delaySeconds: outcome.delaySeconds } : void 0);
1178
1148
  if (options.onJobReleased) {
1179
- await runWorkerHook(envelope.name, "onJobReleased", async () => {
1149
+ await runWorkerHook("onJobReleased", async () => {
1180
1150
  await options.onJobReleased?.({
1181
1151
  ...event,
1182
1152
  ...typeof outcome.delaySeconds === "number" ? { delaySeconds: outcome.delaySeconds } : {},
@@ -1185,24 +1155,24 @@ async function processReservedJob(driver, reserved, options) {
1185
1155
  });
1186
1156
  }
1187
1157
  } else if (outcome.kind === "failed") {
1188
- await queueRuntimeInternals.getQueueRuntimeState().failedJobStore?.persistFailedJob(reserved, outcome.error);
1189
- await driver.delete(reserved);
1190
- await queueRuntimeInternals.executeRegisteredQueueJobFailedHook(envelope, outcome.error, { maxAttempts });
1191
- if (options.onJobFailed) {
1192
- await runWorkerHook(envelope.name, "onJobFailed", async () => {
1193
- await options.onJobFailed?.({ ...event, error: outcome.error });
1194
- });
1195
- }
1158
+ await failReservedJob(driver, reserved, event, options, { error: outcome.error, runJobFailedHook: true });
1196
1159
  } else {
1197
1160
  await driver.acknowledge(reserved);
1198
1161
  if (options.onJobProcessed) {
1199
- await runWorkerHook(envelope.name, "onJobProcessed", async () => {
1162
+ await runWorkerHook("onJobProcessed", async () => {
1200
1163
  await options.onJobProcessed?.(event);
1201
1164
  });
1202
1165
  }
1203
1166
  }
1204
1167
  return outcome;
1205
1168
  }
1169
+ function abandonReservation(driver, reserved) {
1170
+ try {
1171
+ driver.abandon?.(reserved);
1172
+ } catch (error) {
1173
+ reportError2(error, { capability: "queue", operation: "worker.abandonReservation" });
1174
+ }
1175
+ }
1206
1176
  function resolveWorkerStopReason(options, state) {
1207
1177
  const now = Date.now();
1208
1178
  if (typeof options.maxJobs === "number" && state.processed >= options.maxJobs) {
@@ -1220,7 +1190,10 @@ async function runQueueWorker(options = {}) {
1220
1190
  connectionName
1221
1191
  );
1222
1192
  const queueNames = normalizeQueueNames(options.queueNames, connection.queue);
1223
- const sleepSeconds = normalizeNonNegativeInteger(options.sleep, "Queue worker sleep") ?? ("sleep" in connection && typeof connection.sleep === "number" ? connection.sleep : 1);
1193
+ const sleepSeconds = normalizeNonNegativeInteger(options.sleep, "Queue worker sleep", { maximum: QUEUE_TIMER_MAX_SECONDS }) ?? ("sleep" in connection && typeof connection.sleep === "number" ? connection.sleep : 1);
1194
+ const timeout = normalizeNonNegativeInteger(options.timeout, "Queue worker timeout", { maximum: QUEUE_TIMER_MAX_SECONDS });
1195
+ assertQueueTimeoutBelowRetryAfter("Queue worker", timeout, connection);
1196
+ const backoff = queueJobInternals.normalizeBackoff(options.backoff);
1224
1197
  const maxJobs = normalizePositiveInteger(options.maxJobs, "Queue worker maxJobs");
1225
1198
  const maxTime = normalizePositiveInteger(options.maxTime, "Queue worker maxTime");
1226
1199
  const workerId = options.workerId?.trim() || randomUUID2();
@@ -1236,6 +1209,7 @@ async function runQueueWorker(options = {}) {
1236
1209
  maxJobs,
1237
1210
  maxTime
1238
1211
  };
1212
+ const jobOptions = { ...options, timeout, backoff };
1239
1213
  while (true) {
1240
1214
  if (await options.shouldStop?.()) {
1241
1215
  return {
@@ -1286,7 +1260,10 @@ async function runQueueWorker(options = {}) {
1286
1260
  }
1287
1261
  continue;
1288
1262
  }
1289
- const outcome = await processReservedJob(driver, reserved, options);
1263
+ const outcome = await processReservedJob(driver, reserved, jobOptions).catch((error) => {
1264
+ abandonReservation(driver, reserved);
1265
+ throw error;
1266
+ });
1290
1267
  if (outcome.kind === "processed") {
1291
1268
  processed += 1;
1292
1269
  } else if (outcome.kind === "released") {
@@ -1319,7 +1296,6 @@ var queueWorkerInternals = {
1319
1296
  normalizeQueueNames,
1320
1297
  processReservedJob,
1321
1298
  requireAsyncDriver,
1322
- requireRegisteredDefinition,
1323
1299
  resolveRetryDelaySeconds,
1324
1300
  resolveWorkerStopReason,
1325
1301
  runWithTimeout,
@@ -1348,6 +1324,7 @@ export {
1348
1324
  QueueReleaseUnsupportedError,
1349
1325
  QueueWorkerTimeoutError,
1350
1326
  QueueWorkerUnsupportedDriverError,
1327
+ assertQueueJobTimeouts,
1351
1328
  clearQueueConnection,
1352
1329
  configureQueueRuntime,
1353
1330
  defineJob,