@holo-js/queue 0.3.28 → 0.4.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.
- package/LICENSE +21 -0
- package/dist/.holo-build-fingerprint +1 -0
- package/dist/{chunk-5MPOVBVE.mjs → chunk-CYZWPHVL.mjs} +231 -8
- package/dist/{config-Bleufve4.d.ts → config-Cztv8leT.d.ts} +21 -1
- package/dist/config.d.ts +1 -1
- package/dist/config.mjs +3 -1
- package/dist/index.d.ts +29 -12
- package/dist/index.mjs +217 -240
- package/package.json +24 -9
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
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
|
1012
|
+
function normalizeHandlerError(error) {
|
|
1087
1013
|
return error instanceof Error ? error : new Error(String(error));
|
|
1088
1014
|
}
|
|
1089
|
-
async function runWorkerHook(
|
|
1015
|
+
async function runWorkerHook(hookName, callback) {
|
|
1090
1016
|
try {
|
|
1091
1017
|
await callback();
|
|
1092
1018
|
} catch (error) {
|
|
1093
|
-
|
|
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
|
|
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
|
-
|
|
1114
|
+
timeoutSeconds,
|
|
1150
1115
|
() => {
|
|
1151
1116
|
executionState.timedOut = true;
|
|
1152
1117
|
}
|
|
1153
1118
|
);
|
|
1154
1119
|
} catch (error) {
|
|
1155
|
-
handlerError =
|
|
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 =
|
|
1128
|
+
const failure = requestedFailure ?? handlerError;
|
|
1129
|
+
const backoff = definition.backoff ?? options.backoff;
|
|
1158
1130
|
let outcome;
|
|
1159
|
-
if (
|
|
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(
|
|
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(
|
|
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
|
|
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(
|
|
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,
|
|
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,
|