@beignet/core 0.0.2 → 0.0.3
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/CHANGELOG.md +16 -0
- package/README.md +55 -6
- package/dist/jobs/index.d.ts +138 -4
- package/dist/jobs/index.d.ts.map +1 -1
- package/dist/jobs/index.js +161 -1
- package/dist/jobs/index.js.map +1 -1
- package/dist/outbox/index.d.ts +5 -0
- package/dist/outbox/index.d.ts.map +1 -1
- package/dist/outbox/index.js +59 -3
- package/dist/outbox/index.js.map +1 -1
- package/dist/providers/instrumentation.d.ts +1 -1
- package/dist/providers/instrumentation.d.ts.map +1 -1
- package/dist/providers/instrumentation.js.map +1 -1
- package/dist/server/hooks/auth.d.ts +50 -65
- package/dist/server/hooks/auth.d.ts.map +1 -1
- package/dist/server/hooks/auth.js +44 -55
- package/dist/server/hooks/auth.js.map +1 -1
- package/dist/server/hooks/index.d.ts +1 -1
- package/dist/server/hooks/index.d.ts.map +1 -1
- package/dist/server/hooks/index.js.map +1 -1
- package/dist/server/http.d.ts +52 -0
- package/dist/server/http.d.ts.map +1 -1
- package/dist/server/http.js +20 -1
- package/dist/server/http.js.map +1 -1
- package/dist/server/index.d.ts +1 -1
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/server.d.ts +54 -13
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +56 -35
- package/dist/server/server.js.map +1 -1
- package/dist/testing/index.d.ts +4 -0
- package/dist/testing/index.d.ts.map +1 -1
- package/dist/testing/index.js +8 -0
- package/dist/testing/index.js.map +1 -1
- package/dist/uploads/client.d.ts +278 -0
- package/dist/uploads/client.d.ts.map +1 -0
- package/dist/uploads/client.js +428 -0
- package/dist/uploads/client.js.map +1 -0
- package/dist/uploads/index.d.ts +361 -0
- package/dist/uploads/index.d.ts.map +1 -0
- package/dist/uploads/index.js +543 -0
- package/dist/uploads/index.js.map +1 -0
- package/package.json +11 -2
- package/src/jobs/index.ts +326 -5
- package/src/outbox/index.ts +83 -3
- package/src/providers/instrumentation.ts +7 -1
- package/src/server/hooks/auth.ts +89 -162
- package/src/server/hooks/index.ts +1 -5
- package/src/server/http.ts +79 -0
- package/src/server/index.ts +1 -0
- package/src/server/server.ts +191 -23
- package/src/testing/index.ts +11 -0
- package/src/uploads/client.ts +861 -0
- package/src/uploads/index.ts +1067 -0
package/src/jobs/index.ts
CHANGED
|
@@ -16,6 +16,48 @@ export type MaybePromise<T> = T | Promise<T>;
|
|
|
16
16
|
export type InferSchemaOutput<T extends StandardSchemaV1> =
|
|
17
17
|
StandardSchemaV1.InferOutput<T>;
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Duration accepted by retry helpers. Numbers are milliseconds.
|
|
21
|
+
*/
|
|
22
|
+
export type JobRetryDuration =
|
|
23
|
+
| number
|
|
24
|
+
| `${number}ms`
|
|
25
|
+
| `${number}s`
|
|
26
|
+
| `${number}m`
|
|
27
|
+
| `${number}h`;
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Retry strategy understood by Beignet job adapters.
|
|
31
|
+
*/
|
|
32
|
+
export type JobRetryStrategy = "none" | "fixed" | "exponential";
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Arguments passed to a retry predicate.
|
|
36
|
+
*/
|
|
37
|
+
export interface JobRetryPredicateArgs {
|
|
38
|
+
/**
|
|
39
|
+
* Error thrown by the previous attempt.
|
|
40
|
+
*/
|
|
41
|
+
error: unknown;
|
|
42
|
+
/**
|
|
43
|
+
* One-based attempt number that just failed.
|
|
44
|
+
*/
|
|
45
|
+
attempt: number;
|
|
46
|
+
/**
|
|
47
|
+
* Maximum attempts allowed for this delivery.
|
|
48
|
+
*/
|
|
49
|
+
maxAttempts: number;
|
|
50
|
+
/**
|
|
51
|
+
* Job name when the retry decision is for a job.
|
|
52
|
+
*/
|
|
53
|
+
jobName?: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Return whether a failed attempt should be retried.
|
|
58
|
+
*/
|
|
59
|
+
export type JobRetryPredicate = (args: JobRetryPredicateArgs) => boolean;
|
|
60
|
+
|
|
19
61
|
/**
|
|
20
62
|
* Job definition created by `defineJob(...)`.
|
|
21
63
|
*/
|
|
@@ -81,12 +123,38 @@ export interface JobHandleArgs<J extends JobDef, Ctx> {
|
|
|
81
123
|
*/
|
|
82
124
|
export interface JobRetryOptions {
|
|
83
125
|
/**
|
|
84
|
-
*
|
|
85
|
-
*
|
|
86
|
-
|
|
87
|
-
|
|
126
|
+
* Retry strategy. Raw objects without a strategy default to exponential
|
|
127
|
+
* backoff so existing `{ attempts }` style definitions stay meaningful.
|
|
128
|
+
*/
|
|
129
|
+
strategy?: JobRetryStrategy;
|
|
130
|
+
/**
|
|
131
|
+
* Maximum total attempts, including the first attempt.
|
|
88
132
|
*/
|
|
89
133
|
attempts?: number;
|
|
134
|
+
/**
|
|
135
|
+
* Delay between attempts for fixed retry policies.
|
|
136
|
+
*/
|
|
137
|
+
delay?: JobRetryDuration;
|
|
138
|
+
/**
|
|
139
|
+
* Initial delay for exponential retry policies.
|
|
140
|
+
*/
|
|
141
|
+
initialDelay?: JobRetryDuration;
|
|
142
|
+
/**
|
|
143
|
+
* Maximum delay for exponential retry policies.
|
|
144
|
+
*/
|
|
145
|
+
maxDelay?: JobRetryDuration;
|
|
146
|
+
/**
|
|
147
|
+
* Exponential multiplier. Defaults to `2`.
|
|
148
|
+
*/
|
|
149
|
+
factor?: number;
|
|
150
|
+
/**
|
|
151
|
+
* Whether adapters that compute delays should add jitter.
|
|
152
|
+
*/
|
|
153
|
+
jitter?: boolean;
|
|
154
|
+
/**
|
|
155
|
+
* Optional app-owned retry classifier.
|
|
156
|
+
*/
|
|
157
|
+
retryIf?: JobRetryPredicate;
|
|
90
158
|
}
|
|
91
159
|
|
|
92
160
|
/**
|
|
@@ -117,6 +185,96 @@ export interface DefineJobOptions<
|
|
|
117
185
|
): MaybePromise<void>;
|
|
118
186
|
}
|
|
119
187
|
|
|
188
|
+
/**
|
|
189
|
+
* Options for a fixed job retry policy.
|
|
190
|
+
*/
|
|
191
|
+
export interface FixedJobRetryOptions {
|
|
192
|
+
/**
|
|
193
|
+
* Maximum total attempts, including the first attempt.
|
|
194
|
+
*/
|
|
195
|
+
attempts: number;
|
|
196
|
+
/**
|
|
197
|
+
* Delay between attempts.
|
|
198
|
+
*/
|
|
199
|
+
delay: JobRetryDuration;
|
|
200
|
+
/**
|
|
201
|
+
* Optional app-owned retry classifier.
|
|
202
|
+
*/
|
|
203
|
+
retryIf?: JobRetryPredicate;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Options for an exponential job retry policy.
|
|
208
|
+
*/
|
|
209
|
+
export interface ExponentialJobRetryOptions {
|
|
210
|
+
/**
|
|
211
|
+
* Maximum total attempts, including the first attempt.
|
|
212
|
+
*/
|
|
213
|
+
attempts: number;
|
|
214
|
+
/**
|
|
215
|
+
* Initial delay. Defaults to `1s`.
|
|
216
|
+
*/
|
|
217
|
+
initialDelay?: JobRetryDuration;
|
|
218
|
+
/**
|
|
219
|
+
* Maximum delay. Defaults to `1m`.
|
|
220
|
+
*/
|
|
221
|
+
maxDelay?: JobRetryDuration;
|
|
222
|
+
/**
|
|
223
|
+
* Exponential multiplier. Defaults to `2`.
|
|
224
|
+
*/
|
|
225
|
+
factor?: number;
|
|
226
|
+
/**
|
|
227
|
+
* Whether computed delays should include jitter.
|
|
228
|
+
*/
|
|
229
|
+
jitter?: boolean;
|
|
230
|
+
/**
|
|
231
|
+
* Optional app-owned retry classifier.
|
|
232
|
+
*/
|
|
233
|
+
retryIf?: JobRetryPredicate;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Retry helper namespace for job definitions.
|
|
238
|
+
*/
|
|
239
|
+
export const retry = {
|
|
240
|
+
/**
|
|
241
|
+
* Disable retries. The first failure is terminal.
|
|
242
|
+
*/
|
|
243
|
+
none(): JobRetryOptions {
|
|
244
|
+
return {
|
|
245
|
+
strategy: "none",
|
|
246
|
+
attempts: 1,
|
|
247
|
+
};
|
|
248
|
+
},
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Retry with the same delay between attempts.
|
|
252
|
+
*/
|
|
253
|
+
fixed(options: FixedJobRetryOptions): JobRetryOptions {
|
|
254
|
+
return validateJobRetryOptions({
|
|
255
|
+
strategy: "fixed",
|
|
256
|
+
attempts: options.attempts,
|
|
257
|
+
delay: options.delay,
|
|
258
|
+
retryIf: options.retryIf,
|
|
259
|
+
});
|
|
260
|
+
},
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Retry with exponential backoff.
|
|
264
|
+
*/
|
|
265
|
+
exponential(options: ExponentialJobRetryOptions): JobRetryOptions {
|
|
266
|
+
return validateJobRetryOptions({
|
|
267
|
+
strategy: "exponential",
|
|
268
|
+
attempts: options.attempts,
|
|
269
|
+
initialDelay: options.initialDelay,
|
|
270
|
+
maxDelay: options.maxDelay,
|
|
271
|
+
factor: options.factor,
|
|
272
|
+
jitter: options.jitter,
|
|
273
|
+
retryIf: options.retryIf,
|
|
274
|
+
});
|
|
275
|
+
},
|
|
276
|
+
} as const;
|
|
277
|
+
|
|
120
278
|
/**
|
|
121
279
|
* Options for the inline job dispatcher.
|
|
122
280
|
*/
|
|
@@ -207,6 +365,165 @@ function formatIssues(issues: readonly StandardSchemaV1.Issue[]): string {
|
|
|
207
365
|
.join("; ");
|
|
208
366
|
}
|
|
209
367
|
|
|
368
|
+
function assertPositiveInteger(name: string, value: number): void {
|
|
369
|
+
if (!Number.isInteger(value) || value <= 0) {
|
|
370
|
+
throw new Error(`${name} must be a positive integer`);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function assertPositiveNumber(name: string, value: number): void {
|
|
375
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
376
|
+
throw new Error(`${name} must be a positive number`);
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
function durationToMs(name: string, value: JobRetryDuration): number {
|
|
381
|
+
if (typeof value === "number") {
|
|
382
|
+
assertPositiveInteger(name, value);
|
|
383
|
+
return value;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const match = /^(\d+)(ms|s|m|h)$/.exec(value);
|
|
387
|
+
if (!match) {
|
|
388
|
+
throw new Error(
|
|
389
|
+
`${name} must be a positive millisecond value or duration string like "500ms", "30s", "5m", or "1h".`,
|
|
390
|
+
);
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
const amount = Number(match[1]);
|
|
394
|
+
assertPositiveInteger(name, amount);
|
|
395
|
+
|
|
396
|
+
switch (match[2]) {
|
|
397
|
+
case "ms":
|
|
398
|
+
return amount;
|
|
399
|
+
case "s":
|
|
400
|
+
return amount * 1000;
|
|
401
|
+
case "m":
|
|
402
|
+
return amount * 60_000;
|
|
403
|
+
case "h":
|
|
404
|
+
return amount * 3_600_000;
|
|
405
|
+
default:
|
|
406
|
+
throw new Error(`${name} has an unsupported duration unit.`);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
function validateJobRetryOptions(options: JobRetryOptions): JobRetryOptions {
|
|
411
|
+
const strategy = options.strategy ?? "exponential";
|
|
412
|
+
|
|
413
|
+
if (!["none", "fixed", "exponential"].includes(strategy)) {
|
|
414
|
+
throw new Error("retry.strategy must be none, fixed, or exponential");
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
const attempts = options.attempts ?? (strategy === "none" ? 1 : undefined);
|
|
418
|
+
if (attempts === undefined) {
|
|
419
|
+
throw new Error("retry.attempts is required");
|
|
420
|
+
}
|
|
421
|
+
assertPositiveInteger("retry.attempts", attempts);
|
|
422
|
+
|
|
423
|
+
if (strategy === "none" && attempts !== 1) {
|
|
424
|
+
throw new Error("retry.none() must use exactly one attempt");
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
if (strategy === "fixed") {
|
|
428
|
+
if (options.delay === undefined) {
|
|
429
|
+
throw new Error("retry.delay is required for fixed retry policies");
|
|
430
|
+
}
|
|
431
|
+
durationToMs("retry.delay", options.delay);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
if (strategy === "exponential") {
|
|
435
|
+
if (options.initialDelay !== undefined) {
|
|
436
|
+
durationToMs("retry.initialDelay", options.initialDelay);
|
|
437
|
+
}
|
|
438
|
+
if (options.maxDelay !== undefined) {
|
|
439
|
+
durationToMs("retry.maxDelay", options.maxDelay);
|
|
440
|
+
}
|
|
441
|
+
if (options.factor !== undefined) {
|
|
442
|
+
assertPositiveNumber("retry.factor", options.factor);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
return {
|
|
447
|
+
...options,
|
|
448
|
+
strategy,
|
|
449
|
+
attempts,
|
|
450
|
+
};
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
/**
|
|
454
|
+
* Return the maximum total attempts configured by a retry policy.
|
|
455
|
+
*/
|
|
456
|
+
export function getJobRetryMaxAttempts(
|
|
457
|
+
options: JobRetryOptions | undefined,
|
|
458
|
+
): number | undefined {
|
|
459
|
+
return options ? validateJobRetryOptions(options).attempts : undefined;
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/**
|
|
463
|
+
* Return whether a failed job attempt should be retried.
|
|
464
|
+
*/
|
|
465
|
+
export function shouldRetryJob(
|
|
466
|
+
options: JobRetryOptions | undefined,
|
|
467
|
+
args: JobRetryPredicateArgs,
|
|
468
|
+
): boolean {
|
|
469
|
+
if (!options) return args.attempt < args.maxAttempts;
|
|
470
|
+
|
|
471
|
+
const retryOptions = validateJobRetryOptions(options);
|
|
472
|
+
const maxAttempts = Math.min(
|
|
473
|
+
args.maxAttempts,
|
|
474
|
+
retryOptions.attempts ?? args.maxAttempts,
|
|
475
|
+
);
|
|
476
|
+
if (retryOptions.strategy === "none") return false;
|
|
477
|
+
if (args.attempt >= maxAttempts) return false;
|
|
478
|
+
|
|
479
|
+
return retryOptions.retryIf?.({ ...args, maxAttempts }) ?? true;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* Compute the next retry delay in milliseconds for a failed job attempt.
|
|
484
|
+
*/
|
|
485
|
+
export function getJobRetryDelayMs(
|
|
486
|
+
options: JobRetryOptions | undefined,
|
|
487
|
+
args: Pick<JobRetryPredicateArgs, "attempt" | "error" | "jobName">,
|
|
488
|
+
): number {
|
|
489
|
+
const retryOptions = options
|
|
490
|
+
? validateJobRetryOptions(options)
|
|
491
|
+
: retry.exponential({ attempts: 3 });
|
|
492
|
+
|
|
493
|
+
let delayMs: number;
|
|
494
|
+
if (retryOptions.strategy === "fixed") {
|
|
495
|
+
delayMs = durationToMs("retry.delay", retryOptions.delay ?? "1s");
|
|
496
|
+
} else if (retryOptions.strategy === "none") {
|
|
497
|
+
delayMs = 0;
|
|
498
|
+
} else {
|
|
499
|
+
const initialDelayMs = durationToMs(
|
|
500
|
+
"retry.initialDelay",
|
|
501
|
+
retryOptions.initialDelay ?? "1s",
|
|
502
|
+
);
|
|
503
|
+
const maxDelayMs = durationToMs(
|
|
504
|
+
"retry.maxDelay",
|
|
505
|
+
retryOptions.maxDelay ?? "1m",
|
|
506
|
+
);
|
|
507
|
+
const factor = retryOptions.factor ?? 2;
|
|
508
|
+
delayMs = Math.min(
|
|
509
|
+
maxDelayMs,
|
|
510
|
+
initialDelayMs * factor ** Math.max(0, args.attempt - 1),
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (retryOptions.jitter && delayMs > 0) {
|
|
515
|
+
delayMs = Math.ceil(delayMs * (0.5 + Math.random()));
|
|
516
|
+
if (retryOptions.strategy === "exponential") {
|
|
517
|
+
delayMs = Math.min(
|
|
518
|
+
delayMs,
|
|
519
|
+
durationToMs("retry.maxDelay", retryOptions.maxDelay ?? "1m"),
|
|
520
|
+
);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
return delayMs;
|
|
525
|
+
}
|
|
526
|
+
|
|
210
527
|
async function parsePayload<Schema extends StandardSchemaV1>(
|
|
211
528
|
schema: Schema,
|
|
212
529
|
input: unknown,
|
|
@@ -253,12 +570,16 @@ export function defineJob<
|
|
|
253
570
|
name: Name,
|
|
254
571
|
options: DefineJobOptions<Name, Payload, Ctx>,
|
|
255
572
|
): JobDef<Name, Payload, Ctx> {
|
|
573
|
+
const retryOptions = options.retry
|
|
574
|
+
? validateJobRetryOptions(options.retry)
|
|
575
|
+
: undefined;
|
|
576
|
+
|
|
256
577
|
return {
|
|
257
578
|
kind: "job",
|
|
258
579
|
name,
|
|
259
580
|
payload: options.payload,
|
|
260
581
|
description: options.description,
|
|
261
|
-
retry:
|
|
582
|
+
retry: retryOptions,
|
|
262
583
|
handle: options.handle as JobDef<Name, Payload, Ctx>["handle"],
|
|
263
584
|
};
|
|
264
585
|
}
|
package/src/outbox/index.ts
CHANGED
|
@@ -10,12 +10,23 @@ import {
|
|
|
10
10
|
type InferEventPayload,
|
|
11
11
|
parseEventPayload,
|
|
12
12
|
} from "../events";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
getJobRetryDelayMs,
|
|
15
|
+
getJobRetryMaxAttempts,
|
|
16
|
+
type InferJobPayload,
|
|
17
|
+
type JobDef,
|
|
18
|
+
parseJobPayload,
|
|
19
|
+
shouldRetryJob,
|
|
20
|
+
} from "../jobs";
|
|
14
21
|
import type { JobDispatcherPort } from "../ports/events";
|
|
15
22
|
import type {
|
|
16
23
|
BufferedDomainEventRecorder,
|
|
17
24
|
DomainEventRecorderPort,
|
|
18
25
|
} from "../ports/unit-of-work";
|
|
26
|
+
import {
|
|
27
|
+
createProviderInstrumentation,
|
|
28
|
+
type ProviderInstrumentationTarget,
|
|
29
|
+
} from "../providers";
|
|
19
30
|
|
|
20
31
|
/**
|
|
21
32
|
* Value or promise of that value.
|
|
@@ -401,6 +412,10 @@ export interface DrainOutboxOptions {
|
|
|
401
412
|
error: unknown;
|
|
402
413
|
now: Date;
|
|
403
414
|
}) => number);
|
|
415
|
+
/**
|
|
416
|
+
* Optional instrumentation target for retry and dead-letter visibility.
|
|
417
|
+
*/
|
|
418
|
+
instrumentation?: ProviderInstrumentationTarget;
|
|
404
419
|
/**
|
|
405
420
|
* Observer called when delivery fails. Observer failures are ignored so the
|
|
406
421
|
* original delivery failure still controls retry/dead-letter behavior.
|
|
@@ -852,7 +867,7 @@ export async function enqueueJob<J extends JobDef>(
|
|
|
852
867
|
name: job.name,
|
|
853
868
|
payload: toOutboxJsonValue(parsed),
|
|
854
869
|
availableAt: options.availableAt,
|
|
855
|
-
maxAttempts: options.maxAttempts,
|
|
870
|
+
maxAttempts: options.maxAttempts ?? getJobRetryMaxAttempts(job.retry),
|
|
856
871
|
});
|
|
857
872
|
}
|
|
858
873
|
|
|
@@ -906,9 +921,38 @@ function resolveRetryDelayMs(
|
|
|
906
921
|
return options.retryDelayMs;
|
|
907
922
|
}
|
|
908
923
|
|
|
924
|
+
if (message.kind === "job") {
|
|
925
|
+
const job = options.registry.jobs.get(message.name);
|
|
926
|
+
if (job?.retry) {
|
|
927
|
+
return getJobRetryDelayMs(job.retry, {
|
|
928
|
+
attempt: message.attempts,
|
|
929
|
+
error,
|
|
930
|
+
jobName: message.name,
|
|
931
|
+
});
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
|
|
909
935
|
return Math.min(60_000, 1000 * 2 ** Math.max(0, message.attempts - 1));
|
|
910
936
|
}
|
|
911
937
|
|
|
938
|
+
function shouldRetryOutboxMessage(
|
|
939
|
+
options: DrainOutboxOptions,
|
|
940
|
+
message: ClaimedOutboxMessage,
|
|
941
|
+
error: unknown,
|
|
942
|
+
): boolean {
|
|
943
|
+
if (message.kind !== "job") {
|
|
944
|
+
return message.attempts < message.maxAttempts;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
const job = options.registry.jobs.get(message.name);
|
|
948
|
+
return shouldRetryJob(job?.retry, {
|
|
949
|
+
attempt: message.attempts,
|
|
950
|
+
error,
|
|
951
|
+
jobName: message.name,
|
|
952
|
+
maxAttempts: message.maxAttempts,
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
|
|
912
956
|
async function deliverOutboxMessage(
|
|
913
957
|
options: DrainOutboxOptions,
|
|
914
958
|
message: ClaimedOutboxMessage,
|
|
@@ -962,6 +1006,13 @@ export async function drainOutbox(
|
|
|
962
1006
|
): Promise<DrainOutboxResult> {
|
|
963
1007
|
const batchSize = options.batchSize ?? 100;
|
|
964
1008
|
assertPositiveInteger("batchSize", batchSize);
|
|
1009
|
+
const instrumentation = createProviderInstrumentation(
|
|
1010
|
+
options.instrumentation,
|
|
1011
|
+
{
|
|
1012
|
+
providerName: "outbox",
|
|
1013
|
+
watcher: "jobs",
|
|
1014
|
+
},
|
|
1015
|
+
);
|
|
965
1016
|
|
|
966
1017
|
const now = options.now ?? new Date();
|
|
967
1018
|
const messages = await options.outbox.claimBatch({
|
|
@@ -992,7 +1043,8 @@ export async function drainOutbox(
|
|
|
992
1043
|
// Preserve the delivery failure path so the message is retried or
|
|
993
1044
|
// dead-lettered even if the observer fails.
|
|
994
1045
|
}
|
|
995
|
-
const
|
|
1046
|
+
const shouldRetry = shouldRetryOutboxMessage(options, message, error);
|
|
1047
|
+
const deadLetter = !shouldRetry;
|
|
996
1048
|
const retryDelayMs = deadLetter
|
|
997
1049
|
? 0
|
|
998
1050
|
: resolveRetryDelayMs(options, message, error, now);
|
|
@@ -1008,8 +1060,36 @@ export async function drainOutbox(
|
|
|
1008
1060
|
});
|
|
1009
1061
|
|
|
1010
1062
|
if (deadLetter) {
|
|
1063
|
+
if (message.kind === "job") {
|
|
1064
|
+
instrumentation.record({
|
|
1065
|
+
type: "job",
|
|
1066
|
+
jobName: message.name,
|
|
1067
|
+
status: "deadLettered",
|
|
1068
|
+
details: {
|
|
1069
|
+
attempt: message.attempts,
|
|
1070
|
+
maxAttempts: message.maxAttempts,
|
|
1071
|
+
messageId: message.id,
|
|
1072
|
+
error: serializeOutboxError(error),
|
|
1073
|
+
},
|
|
1074
|
+
});
|
|
1075
|
+
}
|
|
1011
1076
|
result.deadLettered += 1;
|
|
1012
1077
|
} else {
|
|
1078
|
+
if (message.kind === "job") {
|
|
1079
|
+
instrumentation.record({
|
|
1080
|
+
type: "job",
|
|
1081
|
+
jobName: message.name,
|
|
1082
|
+
status: "retryScheduled",
|
|
1083
|
+
details: {
|
|
1084
|
+
attempt: message.attempts,
|
|
1085
|
+
maxAttempts: message.maxAttempts,
|
|
1086
|
+
messageId: message.id,
|
|
1087
|
+
retryDelayMs,
|
|
1088
|
+
retryAt: new Date(now.getTime() + retryDelayMs).toISOString(),
|
|
1089
|
+
error: serializeOutboxError(error),
|
|
1090
|
+
},
|
|
1091
|
+
});
|
|
1092
|
+
}
|
|
1013
1093
|
result.retried += 1;
|
|
1014
1094
|
}
|
|
1015
1095
|
}
|
|
@@ -159,7 +159,13 @@ export interface JobProviderInstrumentationEvent
|
|
|
159
159
|
/**
|
|
160
160
|
* Job lifecycle status.
|
|
161
161
|
*/
|
|
162
|
-
status:
|
|
162
|
+
status:
|
|
163
|
+
| "scheduled"
|
|
164
|
+
| "started"
|
|
165
|
+
| "completed"
|
|
166
|
+
| "failed"
|
|
167
|
+
| "retryScheduled"
|
|
168
|
+
| "deadLettered";
|
|
163
169
|
}
|
|
164
170
|
|
|
165
171
|
/**
|