@nest-native/jobs 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +115 -0
- package/dist/dialects/mysql/index.d.ts +2 -0
- package/dist/dialects/mysql/index.js +21 -0
- package/dist/dialects/mysql/index.js.map +1 -0
- package/dist/dialects/mysql/job-store.d.ts +35 -0
- package/dist/dialects/mysql/job-store.js +150 -0
- package/dist/dialects/mysql/job-store.js.map +1 -0
- package/dist/dialects/mysql/schema.d.ts +255 -0
- package/dist/dialects/mysql/schema.js +47 -0
- package/dist/dialects/mysql/schema.js.map +1 -0
- package/dist/dialects/postgres/index.d.ts +2 -0
- package/dist/dialects/postgres/index.js +21 -0
- package/dist/dialects/postgres/index.js.map +1 -0
- package/dist/dialects/postgres/job-store.d.ts +32 -0
- package/dist/dialects/postgres/job-store.js +145 -0
- package/dist/dialects/postgres/job-store.js.map +1 -0
- package/dist/dialects/postgres/schema.d.ts +262 -0
- package/dist/dialects/postgres/schema.js +37 -0
- package/dist/dialects/postgres/schema.js.map +1 -0
- package/dist/dialects/sqlite/index.d.ts +2 -0
- package/dist/dialects/sqlite/index.js +21 -0
- package/dist/dialects/sqlite/index.js.map +1 -0
- package/dist/dialects/sqlite/job-store.d.ts +32 -0
- package/dist/dialects/sqlite/job-store.js +155 -0
- package/dist/dialects/sqlite/job-store.js.map +1 -0
- package/dist/dialects/sqlite/schema.d.ts +279 -0
- package/dist/dialects/sqlite/schema.js +37 -0
- package/dist/dialects/sqlite/schema.js.map +1 -0
- package/dist/enqueue-input.d.ts +12 -0
- package/dist/enqueue-input.js +26 -0
- package/dist/enqueue-input.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.js +33 -0
- package/dist/errors.js.map +1 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +30 -0
- package/dist/index.js.map +1 -0
- package/dist/interfaces.d.ts +125 -0
- package/dist/interfaces.js +10 -0
- package/dist/interfaces.js.map +1 -0
- package/dist/job-handler.decorator.d.ts +31 -0
- package/dist/job-handler.decorator.js +18 -0
- package/dist/job-handler.decorator.js.map +1 -0
- package/dist/jobs-claimer.service.d.ts +37 -0
- package/dist/jobs-claimer.service.js +121 -0
- package/dist/jobs-claimer.service.js.map +1 -0
- package/dist/jobs-handler.explorer.d.ts +24 -0
- package/dist/jobs-handler.explorer.js +67 -0
- package/dist/jobs-handler.explorer.js.map +1 -0
- package/dist/jobs-worker.d.ts +21 -0
- package/dist/jobs-worker.js +40 -0
- package/dist/jobs-worker.js.map +1 -0
- package/dist/jobs.module.d.ts +19 -0
- package/dist/jobs.module.js +49 -0
- package/dist/jobs.module.js.map +1 -0
- package/dist/jobs.service.d.ts +25 -0
- package/dist/jobs.service.js +55 -0
- package/dist/jobs.service.js.map +1 -0
- package/dist/testing/harness.d.ts +61 -0
- package/dist/testing/harness.js +86 -0
- package/dist/testing/harness.js.map +1 -0
- package/dist/testing/index.d.ts +1 -0
- package/dist/testing/index.js +19 -0
- package/dist/testing/index.js.map +1 -0
- package/dist/tokens.d.ts +10 -0
- package/dist/tokens.js +15 -0
- package/dist/tokens.js.map +1 -0
- package/package.json +107 -0
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import type { ModuleMetadata } from '@nestjs/common';
|
|
2
|
+
export declare const JOB_STATUSES: readonly ["pending", "processing", "completed", "failed"];
|
|
3
|
+
export type JobStatus = (typeof JOB_STATUSES)[number];
|
|
4
|
+
/**
|
|
5
|
+
* The dialect-agnostic shape of a job row as the engine reasons about it. All
|
|
6
|
+
* three dialect stores map their Drizzle rows to this shape, so the claimer
|
|
7
|
+
* never sees a dialect-specific type. Timestamps are ISO-8601 strings on every
|
|
8
|
+
* dialect (they compare lexicographically, which the claimer's due-time query
|
|
9
|
+
* relies on).
|
|
10
|
+
*/
|
|
11
|
+
export interface JobRow {
|
|
12
|
+
id: string;
|
|
13
|
+
name: string;
|
|
14
|
+
payload: Record<string, unknown>;
|
|
15
|
+
status: JobStatus;
|
|
16
|
+
attempts: number;
|
|
17
|
+
maxAttempts: number;
|
|
18
|
+
/**
|
|
19
|
+
* The active-dedup key. Uniqueness is enforced by a FULL unique index on
|
|
20
|
+
* `(name, unique_key)` on every dialect, and terminal transitions
|
|
21
|
+
* (`completed`/`failed`) clear it to `NULL` — so "unique among ACTIVE jobs"
|
|
22
|
+
* holds everywhere without partial indexes. `NULL` keys never collide.
|
|
23
|
+
*/
|
|
24
|
+
uniqueKey: string | null;
|
|
25
|
+
/** Claim priority — higher runs first among due jobs. Default 0. */
|
|
26
|
+
priority: number;
|
|
27
|
+
availableAt: string;
|
|
28
|
+
claimedAt: string | null;
|
|
29
|
+
claimedBy: string | null;
|
|
30
|
+
processedAt: string | null;
|
|
31
|
+
lastError: string | null;
|
|
32
|
+
createdAt: string;
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* What a caller supplies to enqueue a job.
|
|
36
|
+
*
|
|
37
|
+
* `runAt` and `delayMs` are **mutually exclusive** — supplying both throws.
|
|
38
|
+
* Omitting both makes the job due immediately.
|
|
39
|
+
*
|
|
40
|
+
* `TPayload` keeps the payload **structural**: a value typed as a plain
|
|
41
|
+
* interface (which has no index signature, so it is not assignable to
|
|
42
|
+
* `Record<string, unknown>`) is accepted as-is — no `as unknown as
|
|
43
|
+
* Record<string, unknown>` at every call site. The stored row shape stays
|
|
44
|
+
* `Record<string, unknown>` (see {@link JobRow}); the dialect stores perform
|
|
45
|
+
* that widening internally, exactly once.
|
|
46
|
+
*/
|
|
47
|
+
export interface EnqueueJobInput<TPayload extends object = Record<string, unknown>> {
|
|
48
|
+
name: string;
|
|
49
|
+
payload: TPayload;
|
|
50
|
+
/** Absolute due time. Mutually exclusive with `delayMs`. */
|
|
51
|
+
runAt?: Date;
|
|
52
|
+
/** Relative due time in milliseconds from now. Mutually exclusive with `runAt`. */
|
|
53
|
+
delayMs?: number;
|
|
54
|
+
maxAttempts?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Dedup among ACTIVE jobs with the same `name`: while a `(name, uniqueKey)`
|
|
57
|
+
* job is pending/processing, enqueueing the same pair is a no-op that
|
|
58
|
+
* returns the EXISTING row. Terminal jobs release the key.
|
|
59
|
+
*/
|
|
60
|
+
uniqueKey?: string;
|
|
61
|
+
/** Higher runs first among due jobs (default 0). */
|
|
62
|
+
priority?: number;
|
|
63
|
+
}
|
|
64
|
+
/** Fully-resolved runner configuration (defaults applied). */
|
|
65
|
+
export interface ResolvedRunnerConfig {
|
|
66
|
+
workerInstanceId: string;
|
|
67
|
+
stuckTimeoutMs: number;
|
|
68
|
+
batchSize: number;
|
|
69
|
+
baseBackoffMs: number;
|
|
70
|
+
maxBackoffMs: number;
|
|
71
|
+
}
|
|
72
|
+
export type RunnerConfig = Partial<ResolvedRunnerConfig>;
|
|
73
|
+
/** Execution context the claimer passes to a handler alongside the payload. */
|
|
74
|
+
export interface JobContext {
|
|
75
|
+
/** The job row id — the natural idempotency key for handler side effects. */
|
|
76
|
+
jobId: string;
|
|
77
|
+
/** The 1-based attempt number of THIS execution (first run → 1). */
|
|
78
|
+
attempt: number;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* The transactional persistence seam for the queue. Each implementation is
|
|
82
|
+
* dialect-specific and owns its Drizzle table + the sync/async query execution;
|
|
83
|
+
* `db` is passed per call (the tx-scoped instance for {@link enqueue}, the base
|
|
84
|
+
* instance for the rest) and is intentionally opaque (`unknown`) to the engine.
|
|
85
|
+
*
|
|
86
|
+
* `enqueue` returns the store's native shape — the **sqlite** store returns a
|
|
87
|
+
* synchronous `JobRow` (so it composes inside a synchronous `@Transactional`
|
|
88
|
+
* body); the **postgres** and **mysql** stores return a `Promise`. On a
|
|
89
|
+
* `(name, uniqueKey)` unique violation it returns the EXISTING active row
|
|
90
|
+
* (dedup no-op). It accepts `EnqueueJobInput<object>` so any structurally-typed
|
|
91
|
+
* payload flows through; the store widens the payload internally.
|
|
92
|
+
*
|
|
93
|
+
* `claimBatch` opens its own transaction, claims due `pending` jobs plus
|
|
94
|
+
* `processing` jobs stuck past `stuckTimeoutMs`, ordered by `priority DESC,
|
|
95
|
+
* available_at ASC`, and marks them `processing`.
|
|
96
|
+
*/
|
|
97
|
+
export interface JobStore {
|
|
98
|
+
enqueue(db: unknown, input: EnqueueJobInput<object>): JobRow | Promise<JobRow>;
|
|
99
|
+
claimBatch(db: unknown, cfg: ResolvedRunnerConfig): Promise<JobRow[]>;
|
|
100
|
+
/** Terminal: sets `completed` and clears `uniqueKey` (releases the key). */
|
|
101
|
+
markCompleted(db: unknown, id: string): Promise<void>;
|
|
102
|
+
/** Re-arms the job: `pending`, attempts+1, due in `delayMs`. Keeps `uniqueKey`. */
|
|
103
|
+
retry(db: unknown, id: string, delayMs: number, lastError?: string): Promise<void>;
|
|
104
|
+
/** Terminal: sets `failed`, attempts+1, and clears `uniqueKey`. */
|
|
105
|
+
markFailed(db: unknown, id: string, reason: string): Promise<void>;
|
|
106
|
+
}
|
|
107
|
+
/** Options for {@link JobsModule.forRoot}. */
|
|
108
|
+
export interface JobsModuleOptions {
|
|
109
|
+
/**
|
|
110
|
+
* Token of the base (non-transactional) Drizzle instance — the same instance
|
|
111
|
+
* the `@nestjs-cls/transactional` Drizzle adapter is configured with. The
|
|
112
|
+
* claimer uses it to open its own claim transaction.
|
|
113
|
+
*/
|
|
114
|
+
drizzleInstanceToken: symbol | string;
|
|
115
|
+
/** The dialect-specific job store. */
|
|
116
|
+
store: JobStore;
|
|
117
|
+
/**
|
|
118
|
+
* Modules that provide (and export) the `drizzleInstanceToken`. Required when
|
|
119
|
+
* that token is not registered by a global module — `JobsModule` imports
|
|
120
|
+
* these so it can resolve the Drizzle instance.
|
|
121
|
+
*/
|
|
122
|
+
imports?: ModuleMetadata['imports'];
|
|
123
|
+
/** Register the module globally (default: true). */
|
|
124
|
+
isGlobal?: boolean;
|
|
125
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"interfaces.js","sourceRoot":"","sources":["../interfaces.ts"],"names":[],"mappings":";;;AAEa,QAAA,YAAY,GAAG;IAC1B,SAAS;IACT,YAAY;IACZ,WAAW;IACX,QAAQ;CACA,CAAC"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type CustomDecorator } from '@nestjs/common';
|
|
2
|
+
import type { JobContext } from './interfaces';
|
|
3
|
+
/** Metadata key carrying the job name a `@JobHandler` class handles. */
|
|
4
|
+
export declare const JOB_HANDLER_NAME: unique symbol;
|
|
5
|
+
/**
|
|
6
|
+
* The contract a `@JobHandler(name)` class fulfils. Handlers run in the
|
|
7
|
+
* claimer's poll loop, OUTSIDE any business transaction, and delivery is
|
|
8
|
+
* at-least-once — make them idempotent or key side effects on `ctx.jobId`.
|
|
9
|
+
* Throw {@link RetryableError} / {@link PermanentError} to steer retries; any
|
|
10
|
+
* other throw retries with backoff until `maxAttempts`, then fails.
|
|
11
|
+
*
|
|
12
|
+
* The interface and the decorator below share the name deliberately (one lives
|
|
13
|
+
* in the type space, the other in the value space), so a handler reads:
|
|
14
|
+
*
|
|
15
|
+
* ```ts
|
|
16
|
+
* @JobHandler('email.welcome')
|
|
17
|
+
* @Injectable()
|
|
18
|
+
* export class WelcomeEmailHandler implements JobHandler { ... }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
export interface JobHandler {
|
|
22
|
+
handle(payload: Record<string, unknown>, ctx: JobContext): void | Promise<void>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Marks a provider class as the handler for jobs enqueued under `name`.
|
|
26
|
+
*
|
|
27
|
+
* Register the class as a provider in any module; {@link JobsHandlerExplorer}
|
|
28
|
+
* discovers it at application bootstrap. Exactly one handler per name —
|
|
29
|
+
* duplicates throw at startup.
|
|
30
|
+
*/
|
|
31
|
+
export declare function JobHandler(name: string): CustomDecorator<symbol>;
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.JOB_HANDLER_NAME = void 0;
|
|
4
|
+
exports.JobHandler = JobHandler;
|
|
5
|
+
const common_1 = require("@nestjs/common");
|
|
6
|
+
/** Metadata key carrying the job name a `@JobHandler` class handles. */
|
|
7
|
+
exports.JOB_HANDLER_NAME = Symbol.for('@nest-native/jobs:job-handler-name');
|
|
8
|
+
/**
|
|
9
|
+
* Marks a provider class as the handler for jobs enqueued under `name`.
|
|
10
|
+
*
|
|
11
|
+
* Register the class as a provider in any module; {@link JobsHandlerExplorer}
|
|
12
|
+
* discovers it at application bootstrap. Exactly one handler per name —
|
|
13
|
+
* duplicates throw at startup.
|
|
14
|
+
*/
|
|
15
|
+
function JobHandler(name) {
|
|
16
|
+
return (0, common_1.SetMetadata)(exports.JOB_HANDLER_NAME, name);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=job-handler.decorator.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"job-handler.decorator.js","sourceRoot":"","sources":["../job-handler.decorator.ts"],"names":[],"mappings":";;;AAiCA,gCAEC;AAnCD,2CAAmE;AAGnE,wEAAwE;AAC3D,QAAA,gBAAgB,GAAG,MAAM,CAAC,GAAG,CAAC,oCAAoC,CAAC,CAAC;AAsBjF;;;;;;GAMG;AACH,SAAgB,UAAU,CAAC,IAAY;IACrC,OAAO,IAAA,oBAAW,EAAC,wBAAgB,EAAE,IAAI,CAAC,CAAC;AAC7C,CAAC"}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import type { JobStore, ResolvedRunnerConfig, RunnerConfig } from './interfaces';
|
|
2
|
+
import { JobsHandlerExplorer } from './jobs-handler.explorer';
|
|
3
|
+
export declare const DEFAULT_RUNNER_CONFIG: ResolvedRunnerConfig;
|
|
4
|
+
export interface TickReport {
|
|
5
|
+
claimed: number;
|
|
6
|
+
completed: number;
|
|
7
|
+
retried: number;
|
|
8
|
+
failed: number;
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Executes committed jobs. `tick()` claims a batch of due jobs (the store opens
|
|
12
|
+
* its own transaction; priority first, oldest due first, reclaiming jobs stuck
|
|
13
|
+
* in `processing`), dispatches each to the `@JobHandler` registered for its
|
|
14
|
+
* name, and records the outcome:
|
|
15
|
+
*
|
|
16
|
+
* - handler returns → `markCompleted` (key released)
|
|
17
|
+
* - {@link PermanentError} → `markFailed` immediately (no point retrying)
|
|
18
|
+
* - {@link RetryableError} → `retry`, honouring its `delayMs` if given
|
|
19
|
+
* - any other throw → `retry` with jittered exponential backoff
|
|
20
|
+
* until `maxAttempts`, then `markFailed`
|
|
21
|
+
* - no handler registered → `PermanentError` → `markFailed`
|
|
22
|
+
*
|
|
23
|
+
* Runs in a background worker (see `runWorkerLoop`) — never inside a business
|
|
24
|
+
* transaction — so it freely awaits the store and the handlers.
|
|
25
|
+
*/
|
|
26
|
+
export declare class JobsClaimer {
|
|
27
|
+
private readonly db;
|
|
28
|
+
private readonly store;
|
|
29
|
+
private readonly explorer;
|
|
30
|
+
private readonly logger;
|
|
31
|
+
constructor(db: unknown, store: JobStore, explorer: JobsHandlerExplorer);
|
|
32
|
+
tick(overrides?: RunnerConfig): Promise<TickReport>;
|
|
33
|
+
private processOne;
|
|
34
|
+
private onHandlerError;
|
|
35
|
+
private fail;
|
|
36
|
+
private backoff;
|
|
37
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
var __param = (this && this.__param) || function (paramIndex, decorator) {
|
|
12
|
+
return function (target, key) { decorator(target, key, paramIndex); }
|
|
13
|
+
};
|
|
14
|
+
var JobsClaimer_1;
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.JobsClaimer = exports.DEFAULT_RUNNER_CONFIG = void 0;
|
|
17
|
+
const node_os_1 = require("node:os");
|
|
18
|
+
const common_1 = require("@nestjs/common");
|
|
19
|
+
const errors_1 = require("./errors");
|
|
20
|
+
const jobs_handler_explorer_1 = require("./jobs-handler.explorer");
|
|
21
|
+
const tokens_1 = require("./tokens");
|
|
22
|
+
exports.DEFAULT_RUNNER_CONFIG = {
|
|
23
|
+
workerInstanceId: `${(0, node_os_1.hostname)()}-${process.pid}`,
|
|
24
|
+
stuckTimeoutMs: 60_000,
|
|
25
|
+
batchSize: 32,
|
|
26
|
+
baseBackoffMs: 1_000,
|
|
27
|
+
maxBackoffMs: 60_000,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* Executes committed jobs. `tick()` claims a batch of due jobs (the store opens
|
|
31
|
+
* its own transaction; priority first, oldest due first, reclaiming jobs stuck
|
|
32
|
+
* in `processing`), dispatches each to the `@JobHandler` registered for its
|
|
33
|
+
* name, and records the outcome:
|
|
34
|
+
*
|
|
35
|
+
* - handler returns → `markCompleted` (key released)
|
|
36
|
+
* - {@link PermanentError} → `markFailed` immediately (no point retrying)
|
|
37
|
+
* - {@link RetryableError} → `retry`, honouring its `delayMs` if given
|
|
38
|
+
* - any other throw → `retry` with jittered exponential backoff
|
|
39
|
+
* until `maxAttempts`, then `markFailed`
|
|
40
|
+
* - no handler registered → `PermanentError` → `markFailed`
|
|
41
|
+
*
|
|
42
|
+
* Runs in a background worker (see `runWorkerLoop`) — never inside a business
|
|
43
|
+
* transaction — so it freely awaits the store and the handlers.
|
|
44
|
+
*/
|
|
45
|
+
let JobsClaimer = JobsClaimer_1 = class JobsClaimer {
|
|
46
|
+
constructor(db, store, explorer) {
|
|
47
|
+
this.db = db;
|
|
48
|
+
this.store = store;
|
|
49
|
+
this.explorer = explorer;
|
|
50
|
+
this.logger = new common_1.Logger(JobsClaimer_1.name);
|
|
51
|
+
}
|
|
52
|
+
async tick(overrides = {}) {
|
|
53
|
+
const cfg = { ...exports.DEFAULT_RUNNER_CONFIG, ...overrides };
|
|
54
|
+
const claimed = await this.store.claimBatch(this.db, cfg);
|
|
55
|
+
const report = {
|
|
56
|
+
claimed: claimed.length,
|
|
57
|
+
completed: 0,
|
|
58
|
+
retried: 0,
|
|
59
|
+
failed: 0,
|
|
60
|
+
};
|
|
61
|
+
for (const job of claimed) {
|
|
62
|
+
const outcome = await this.processOne(job, cfg);
|
|
63
|
+
report[outcome] += 1;
|
|
64
|
+
}
|
|
65
|
+
return report;
|
|
66
|
+
}
|
|
67
|
+
async processOne(job, cfg) {
|
|
68
|
+
try {
|
|
69
|
+
const handler = this.explorer.get(job.name);
|
|
70
|
+
if (!handler) {
|
|
71
|
+
throw new errors_1.PermanentError(`No @JobHandler registered for job "${job.name}"`);
|
|
72
|
+
}
|
|
73
|
+
await handler.handle(job.payload, {
|
|
74
|
+
jobId: job.id,
|
|
75
|
+
attempt: job.attempts + 1,
|
|
76
|
+
});
|
|
77
|
+
await this.store.markCompleted(this.db, job.id);
|
|
78
|
+
return 'completed';
|
|
79
|
+
}
|
|
80
|
+
catch (error) {
|
|
81
|
+
return this.onHandlerError(job, cfg, error);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async onHandlerError(job, cfg, error) {
|
|
85
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
86
|
+
// Permanent: retrying can never succeed — fail now instead of burning attempts.
|
|
87
|
+
if (error instanceof errors_1.PermanentError) {
|
|
88
|
+
return this.fail(job, message);
|
|
89
|
+
}
|
|
90
|
+
// Retryable: schedule another attempt, honouring a handler-supplied delay.
|
|
91
|
+
if (error instanceof errors_1.RetryableError) {
|
|
92
|
+
const delay = error.delayMs ?? this.backoff(job.attempts, cfg);
|
|
93
|
+
await this.store.retry(this.db, job.id, delay, message);
|
|
94
|
+
return 'retried';
|
|
95
|
+
}
|
|
96
|
+
// Anything else: retry with backoff until maxAttempts, then fail.
|
|
97
|
+
if (job.attempts + 1 >= job.maxAttempts) {
|
|
98
|
+
return this.fail(job, message);
|
|
99
|
+
}
|
|
100
|
+
await this.store.retry(this.db, job.id, this.backoff(job.attempts, cfg), message);
|
|
101
|
+
return 'retried';
|
|
102
|
+
}
|
|
103
|
+
async fail(job, reason) {
|
|
104
|
+
this.logger.warn(`job ${job.id} ("${job.name}") failed: ${reason}`);
|
|
105
|
+
await this.store.markFailed(this.db, job.id, reason);
|
|
106
|
+
return 'failed';
|
|
107
|
+
}
|
|
108
|
+
backoff(attempts, cfg) {
|
|
109
|
+
const base = cfg.baseBackoffMs * 2 ** attempts;
|
|
110
|
+
const capped = Math.min(base, cfg.maxBackoffMs);
|
|
111
|
+
return capped + Math.floor(Math.random() * cfg.baseBackoffMs);
|
|
112
|
+
}
|
|
113
|
+
};
|
|
114
|
+
exports.JobsClaimer = JobsClaimer;
|
|
115
|
+
exports.JobsClaimer = JobsClaimer = JobsClaimer_1 = __decorate([
|
|
116
|
+
(0, common_1.Injectable)(),
|
|
117
|
+
__param(0, (0, common_1.Inject)(tokens_1.JOBS_DRIZZLE)),
|
|
118
|
+
__param(1, (0, common_1.Inject)(tokens_1.JOBS_STORE)),
|
|
119
|
+
__metadata("design:paramtypes", [Object, Object, jobs_handler_explorer_1.JobsHandlerExplorer])
|
|
120
|
+
], JobsClaimer);
|
|
121
|
+
//# sourceMappingURL=jobs-claimer.service.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs-claimer.service.js","sourceRoot":"","sources":["../jobs-claimer.service.ts"],"names":[],"mappings":";;;;;;;;;;;;;;;;AAAA,qCAAmC;AACnC,2CAA4D;AAC5D,qCAA0D;AAO1D,mEAA8D;AAC9D,qCAAoD;AAEvC,QAAA,qBAAqB,GAAyB;IACzD,gBAAgB,EAAE,GAAG,IAAA,kBAAQ,GAAE,IAAI,OAAO,CAAC,GAAG,EAAE;IAChD,cAAc,EAAE,MAAM;IACtB,SAAS,EAAE,EAAE;IACb,aAAa,EAAE,KAAK;IACpB,YAAY,EAAE,MAAM;CACrB,CAAC;AAWF;;;;;;;;;;;;;;;GAeG;AAEI,IAAM,WAAW,mBAAjB,MAAM,WAAW;IAGtB,YACwB,EAA4B,EAC9B,KAAgC,EACnC,QAA6B;QAFP,OAAE,GAAF,EAAE,CAAS;QACb,UAAK,GAAL,KAAK,CAAU;QACnC,aAAQ,GAAR,QAAQ,CAAqB;QAL/B,WAAM,GAAG,IAAI,eAAM,CAAC,aAAW,CAAC,IAAI,CAAC,CAAC;IAMpD,CAAC;IAEJ,KAAK,CAAC,IAAI,CAAC,YAA0B,EAAE;QACrC,MAAM,GAAG,GAAG,EAAE,GAAG,6BAAqB,EAAE,GAAG,SAAS,EAAE,CAAC;QACvD,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;QAC1D,MAAM,MAAM,GAAe;YACzB,OAAO,EAAE,OAAO,CAAC,MAAM;YACvB,SAAS,EAAE,CAAC;YACZ,OAAO,EAAE,CAAC;YACV,MAAM,EAAE,CAAC;SACV,CAAC;QACF,KAAK,MAAM,GAAG,IAAI,OAAO,EAAE,CAAC;YAC1B,MAAM,OAAO,GAAG,MAAM,IAAI,CAAC,UAAU,CAAC,GAAG,EAAE,GAAG,CAAC,CAAC;YAChD,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC,CAAC;QACvB,CAAC;QACD,OAAO,MAAM,CAAC;IAChB,CAAC;IAEO,KAAK,CAAC,UAAU,CACtB,GAAW,EACX,GAAyB;QAEzB,IAAI,CAAC;YACH,MAAM,OAAO,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;YAC5C,IAAI,CAAC,OAAO,EAAE,CAAC;gBACb,MAAM,IAAI,uBAAc,CACtB,sCAAsC,GAAG,CAAC,IAAI,GAAG,CAClD,CAAC;YACJ,CAAC;YACD,MAAM,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE;gBAChC,KAAK,EAAE,GAAG,CAAC,EAAE;gBACb,OAAO,EAAE,GAAG,CAAC,QAAQ,GAAG,CAAC;aAC1B,CAAC,CAAC;YACH,MAAM,IAAI,CAAC,KAAK,CAAC,aAAa,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,CAAC,CAAC;YAChD,OAAO,WAAW,CAAC;QACrB,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,IAAI,CAAC,cAAc,CAAC,GAAG,EAAE,GAAG,EAAE,KAAK,CAAC,CAAC;QAC9C,CAAC;IACH,CAAC;IAEO,KAAK,CAAC,cAAc,CAC1B,GAAW,EACX,GAAyB,EACzB,KAAc;QAEd,MAAM,OAAO,GAAG,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC;QACvE,gFAAgF;QAChF,IAAI,KAAK,YAAY,uBAAc,EAAE,CAAC;YACpC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACjC,CAAC;QACD,2EAA2E;QAC3E,IAAI,KAAK,YAAY,uBAAc,EAAE,CAAC;YACpC,MAAM,KAAK,GAAG,KAAK,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC/D,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,KAAK,EAAE,OAAO,CAAC,CAAC;YACxD,OAAO,SAAS,CAAC;QACnB,CAAC;QACD,kEAAkE;QAClE,IAAI,GAAG,CAAC,QAAQ,GAAG,CAAC,IAAI,GAAG,CAAC,WAAW,EAAE,CAAC;YACxC,OAAO,IAAI,CAAC,IAAI,CAAC,GAAG,EAAE,OAAO,CAAC,CAAC;QACjC,CAAC;QACD,MAAM,IAAI,CAAC,KAAK,CAAC,KAAK,CACpB,IAAI,CAAC,EAAE,EACP,GAAG,CAAC,EAAE,EACN,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,QAAQ,EAAE,GAAG,CAAC,EAC/B,OAAO,CACR,CAAC;QACF,OAAO,SAAS,CAAC;IACnB,CAAC;IAEO,KAAK,CAAC,IAAI,CAAC,GAAW,EAAE,MAAc;QAC5C,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,GAAG,CAAC,EAAE,MAAM,GAAG,CAAC,IAAI,cAAc,MAAM,EAAE,CAAC,CAAC;QACpE,MAAM,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC,IAAI,CAAC,EAAE,EAAE,GAAG,CAAC,EAAE,EAAE,MAAM,CAAC,CAAC;QACrD,OAAO,QAAQ,CAAC;IAClB,CAAC;IAEO,OAAO,CAAC,QAAgB,EAAE,GAAyB;QACzD,MAAM,IAAI,GAAG,GAAG,CAAC,aAAa,GAAG,CAAC,IAAI,QAAQ,CAAC;QAC/C,MAAM,MAAM,GAAG,IAAI,CAAC,GAAG,CAAC,IAAI,EAAE,GAAG,CAAC,YAAY,CAAC,CAAC;QAChD,OAAO,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,EAAE,GAAG,GAAG,CAAC,aAAa,CAAC,CAAC;IAChE,CAAC;CACF,CAAA;AAvFY,kCAAW;sBAAX,WAAW;IADvB,IAAA,mBAAU,GAAE;IAKR,WAAA,IAAA,eAAM,EAAC,qBAAY,CAAC,CAAA;IACpB,WAAA,IAAA,eAAM,EAAC,mBAAU,CAAC,CAAA;qDACQ,2CAAmB;GANrC,WAAW,CAuFvB"}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { type OnApplicationBootstrap } from '@nestjs/common';
|
|
2
|
+
import { DiscoveryService, Reflector } from '@nestjs/core';
|
|
3
|
+
import { type JobHandler } from './job-handler.decorator';
|
|
4
|
+
/**
|
|
5
|
+
* Discovers `@JobHandler(name)` provider classes in the Nest container and
|
|
6
|
+
* builds the name → handler-instance registry the claimer dispatches through.
|
|
7
|
+
*
|
|
8
|
+
* Discovery happens once, at application bootstrap (so every module has
|
|
9
|
+
* finished instantiating its providers). Exactly one handler per job name:
|
|
10
|
+
* a second class claiming an already-registered name throws, failing the
|
|
11
|
+
* application at startup instead of silently shadowing a handler at runtime.
|
|
12
|
+
*/
|
|
13
|
+
export declare class JobsHandlerExplorer implements OnApplicationBootstrap {
|
|
14
|
+
private readonly discovery;
|
|
15
|
+
private readonly reflector;
|
|
16
|
+
private readonly registry;
|
|
17
|
+
constructor(discovery: DiscoveryService, reflector: Reflector);
|
|
18
|
+
onApplicationBootstrap(): void;
|
|
19
|
+
/** The handler registered for `name`, if any. */
|
|
20
|
+
get(name: string): JobHandler | undefined;
|
|
21
|
+
/** Every registered job name (useful for diagnostics and tests). */
|
|
22
|
+
names(): readonly string[];
|
|
23
|
+
private register;
|
|
24
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
var __metadata = (this && this.__metadata) || function (k, v) {
|
|
9
|
+
if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
|
|
10
|
+
};
|
|
11
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
|
+
exports.JobsHandlerExplorer = void 0;
|
|
13
|
+
const common_1 = require("@nestjs/common");
|
|
14
|
+
const core_1 = require("@nestjs/core");
|
|
15
|
+
const job_handler_decorator_1 = require("./job-handler.decorator");
|
|
16
|
+
/**
|
|
17
|
+
* Discovers `@JobHandler(name)` provider classes in the Nest container and
|
|
18
|
+
* builds the name → handler-instance registry the claimer dispatches through.
|
|
19
|
+
*
|
|
20
|
+
* Discovery happens once, at application bootstrap (so every module has
|
|
21
|
+
* finished instantiating its providers). Exactly one handler per job name:
|
|
22
|
+
* a second class claiming an already-registered name throws, failing the
|
|
23
|
+
* application at startup instead of silently shadowing a handler at runtime.
|
|
24
|
+
*/
|
|
25
|
+
let JobsHandlerExplorer = class JobsHandlerExplorer {
|
|
26
|
+
constructor(discovery, reflector) {
|
|
27
|
+
this.discovery = discovery;
|
|
28
|
+
this.reflector = reflector;
|
|
29
|
+
this.registry = new Map();
|
|
30
|
+
}
|
|
31
|
+
onApplicationBootstrap() {
|
|
32
|
+
for (const wrapper of this.discovery.getProviders()) {
|
|
33
|
+
this.register(wrapper);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
/** The handler registered for `name`, if any. */
|
|
37
|
+
get(name) {
|
|
38
|
+
return this.registry.get(name);
|
|
39
|
+
}
|
|
40
|
+
/** Every registered job name (useful for diagnostics and tests). */
|
|
41
|
+
names() {
|
|
42
|
+
return [...this.registry.keys()];
|
|
43
|
+
}
|
|
44
|
+
register(wrapper) {
|
|
45
|
+
const { instance, metatype } = wrapper;
|
|
46
|
+
if (!instance || !metatype) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const name = this.reflector.get(job_handler_decorator_1.JOB_HANDLER_NAME, metatype);
|
|
50
|
+
if (name === undefined) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
const existing = this.registry.get(name);
|
|
54
|
+
if (existing && existing !== instance) {
|
|
55
|
+
throw new Error(`Duplicate @JobHandler("${name}"): ${metatype.name} conflicts ` +
|
|
56
|
+
'with an already-registered handler. Job names must map to exactly one handler.');
|
|
57
|
+
}
|
|
58
|
+
this.registry.set(name, instance);
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
exports.JobsHandlerExplorer = JobsHandlerExplorer;
|
|
62
|
+
exports.JobsHandlerExplorer = JobsHandlerExplorer = __decorate([
|
|
63
|
+
(0, common_1.Injectable)(),
|
|
64
|
+
__metadata("design:paramtypes", [core_1.DiscoveryService,
|
|
65
|
+
core_1.Reflector])
|
|
66
|
+
], JobsHandlerExplorer);
|
|
67
|
+
//# sourceMappingURL=jobs-handler.explorer.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs-handler.explorer.js","sourceRoot":"","sources":["../jobs-handler.explorer.ts"],"names":[],"mappings":";;;;;;;;;;;;AAAA,2CAAoF;AACpF,uCAA2D;AAC3D,mEAA4E;AAW5E;;;;;;;;GAQG;AAEI,IAAM,mBAAmB,GAAzB,MAAM,mBAAmB;IAG9B,YACmB,SAA2B,EAC3B,SAAoB;QADpB,cAAS,GAAT,SAAS,CAAkB;QAC3B,cAAS,GAAT,SAAS,CAAW;QAJtB,aAAQ,GAAG,IAAI,GAAG,EAAsB,CAAC;IAKvD,CAAC;IAEJ,sBAAsB;QACpB,KAAK,MAAM,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,YAAY,EAA2B,EAAE,CAAC;YAC7E,IAAI,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC;QACzB,CAAC;IACH,CAAC;IAED,iDAAiD;IACjD,GAAG,CAAC,IAAY;QACd,OAAO,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;IACjC,CAAC;IAED,oEAAoE;IACpE,KAAK;QACH,OAAO,CAAC,GAAG,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC;IACnC,CAAC;IAEO,QAAQ,CAAC,OAA4B;QAC3C,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,GAAG,OAAO,CAAC;QACvC,IAAI,CAAC,QAAQ,IAAI,CAAC,QAAQ,EAAE,CAAC;YAC3B,OAAO;QACT,CAAC;QACD,MAAM,IAAI,GAAG,IAAI,CAAC,SAAS,CAAC,GAAG,CAC7B,wCAAgB,EAChB,QAAgB,CACjB,CAAC;QACF,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QACD,MAAM,QAAQ,GAAG,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACzC,IAAI,QAAQ,IAAI,QAAQ,KAAK,QAAQ,EAAE,CAAC;YACtC,MAAM,IAAI,KAAK,CACb,0BAA0B,IAAI,OAAQ,QAAiB,CAAC,IAAI,aAAa;gBACvE,gFAAgF,CACnF,CAAC;QACJ,CAAC;QACD,IAAI,CAAC,QAAQ,CAAC,GAAG,CAAC,IAAI,EAAE,QAAsB,CAAC,CAAC;IAClD,CAAC;CACF,CAAA;AA7CY,kDAAmB;8BAAnB,mBAAmB;IAD/B,IAAA,mBAAU,GAAE;qCAKmB,uBAAgB;QAChB,gBAAS;GAL5B,mBAAmB,CA6C/B"}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { RunnerConfig } from './interfaces';
|
|
2
|
+
import type { JobsClaimer, TickReport } from './jobs-claimer.service';
|
|
3
|
+
export interface WorkerLoopOptions {
|
|
4
|
+
/** Delay between ticks when the last tick claimed nothing (default 2000ms). */
|
|
5
|
+
pollIntervalMs?: number;
|
|
6
|
+
/** Runner overrides applied to every tick. */
|
|
7
|
+
runner?: RunnerConfig;
|
|
8
|
+
/** Abort to stop the loop. */
|
|
9
|
+
signal?: AbortSignal;
|
|
10
|
+
/** Called after each successful tick. */
|
|
11
|
+
onTick?: (report: TickReport) => void;
|
|
12
|
+
/** Called when a tick throws — the loop reports and continues. */
|
|
13
|
+
onError?: (error: unknown) => void;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Runs `claimer.tick()` in a loop until `signal` aborts. When a tick claims a
|
|
17
|
+
* batch it loops immediately to drain the backlog; when it claims nothing it
|
|
18
|
+
* waits `pollIntervalMs`. A throwing tick is reported via `onError` and the loop
|
|
19
|
+
* continues after the same wait.
|
|
20
|
+
*/
|
|
21
|
+
export declare function runWorkerLoop(claimer: JobsClaimer, options?: WorkerLoopOptions): Promise<void>;
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.runWorkerLoop = runWorkerLoop;
|
|
4
|
+
/**
|
|
5
|
+
* Runs `claimer.tick()` in a loop until `signal` aborts. When a tick claims a
|
|
6
|
+
* batch it loops immediately to drain the backlog; when it claims nothing it
|
|
7
|
+
* waits `pollIntervalMs`. A throwing tick is reported via `onError` and the loop
|
|
8
|
+
* continues after the same wait.
|
|
9
|
+
*/
|
|
10
|
+
async function runWorkerLoop(claimer, options = {}) {
|
|
11
|
+
const pollIntervalMs = options.pollIntervalMs ?? 2_000;
|
|
12
|
+
const { signal } = options;
|
|
13
|
+
while (!signal?.aborted) {
|
|
14
|
+
try {
|
|
15
|
+
const report = await claimer.tick(options.runner);
|
|
16
|
+
options.onTick?.(report);
|
|
17
|
+
if (report.claimed === 0) {
|
|
18
|
+
await sleep(pollIntervalMs, signal);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
options.onError?.(error);
|
|
23
|
+
await sleep(pollIntervalMs, signal);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
function sleep(ms, signal) {
|
|
28
|
+
return new Promise((resolve) => {
|
|
29
|
+
if (signal?.aborted) {
|
|
30
|
+
resolve();
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const timer = setTimeout(resolve, ms);
|
|
34
|
+
signal?.addEventListener('abort', () => {
|
|
35
|
+
clearTimeout(timer);
|
|
36
|
+
resolve();
|
|
37
|
+
}, { once: true });
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=jobs-worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs-worker.js","sourceRoot":"","sources":["../jobs-worker.ts"],"names":[],"mappings":";;AAsBA,sCAkBC;AAxBD;;;;;GAKG;AACI,KAAK,UAAU,aAAa,CACjC,OAAoB,EACpB,UAA6B,EAAE;IAE/B,MAAM,cAAc,GAAG,OAAO,CAAC,cAAc,IAAI,KAAK,CAAC;IACvD,MAAM,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC;IAC3B,OAAO,CAAC,MAAM,EAAE,OAAO,EAAE,CAAC;QACxB,IAAI,CAAC;YACH,MAAM,MAAM,GAAG,MAAM,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;YAClD,OAAO,CAAC,MAAM,EAAE,CAAC,MAAM,CAAC,CAAC;YACzB,IAAI,MAAM,CAAC,OAAO,KAAK,CAAC,EAAE,CAAC;gBACzB,MAAM,KAAK,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;YACtC,CAAC;QACH,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,OAAO,CAAC,OAAO,EAAE,CAAC,KAAK,CAAC,CAAC;YACzB,MAAM,KAAK,CAAC,cAAc,EAAE,MAAM,CAAC,CAAC;QACtC,CAAC;IACH,CAAC;AACH,CAAC;AAED,SAAS,KAAK,CAAC,EAAU,EAAE,MAAoB;IAC7C,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,IAAI,MAAM,EAAE,OAAO,EAAE,CAAC;YACpB,OAAO,EAAE,CAAC;YACV,OAAO;QACT,CAAC;QACD,MAAM,KAAK,GAAG,UAAU,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC;QACtC,MAAM,EAAE,gBAAgB,CACtB,OAAO,EACP,GAAG,EAAE;YACH,YAAY,CAAC,KAAK,CAAC,CAAC;YACpB,OAAO,EAAE,CAAC;QACZ,CAAC,EACD,EAAE,IAAI,EAAE,IAAI,EAAE,CACf,CAAC;IACJ,CAAC,CAAC,CAAC;AACL,CAAC"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type DynamicModule, type InjectionToken, type ModuleMetadata, type OptionalFactoryDependency } from '@nestjs/common';
|
|
2
|
+
import type { JobsModuleOptions, JobStore } from './interfaces';
|
|
3
|
+
/**
|
|
4
|
+
* Async configuration. The Drizzle token is static (a DI token is known at
|
|
5
|
+
* module-definition time); the store is built by a factory so it can inject
|
|
6
|
+
* runtime providers (e.g. configuration).
|
|
7
|
+
*/
|
|
8
|
+
export interface JobsModuleAsyncOptions {
|
|
9
|
+
isGlobal?: boolean;
|
|
10
|
+
/** Token of the base (non-transactional) Drizzle instance. */
|
|
11
|
+
drizzleInstanceToken: symbol | string;
|
|
12
|
+
imports?: ModuleMetadata['imports'];
|
|
13
|
+
inject?: (InjectionToken | OptionalFactoryDependency)[];
|
|
14
|
+
useStore: (...args: any[]) => JobStore | Promise<JobStore>;
|
|
15
|
+
}
|
|
16
|
+
export declare class JobsModule {
|
|
17
|
+
static forRoot(options: JobsModuleOptions): DynamicModule;
|
|
18
|
+
static forRootAsync(options: JobsModuleAsyncOptions): DynamicModule;
|
|
19
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
|
|
3
|
+
var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
|
|
4
|
+
if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
|
|
5
|
+
else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
|
|
6
|
+
return c > 3 && r && Object.defineProperty(target, key, r), r;
|
|
7
|
+
};
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
|
+
exports.JobsModule = void 0;
|
|
10
|
+
const common_1 = require("@nestjs/common");
|
|
11
|
+
const core_1 = require("@nestjs/core");
|
|
12
|
+
const jobs_claimer_service_1 = require("./jobs-claimer.service");
|
|
13
|
+
const jobs_handler_explorer_1 = require("./jobs-handler.explorer");
|
|
14
|
+
const jobs_service_1 = require("./jobs.service");
|
|
15
|
+
const tokens_1 = require("./tokens");
|
|
16
|
+
let JobsModule = class JobsModule {
|
|
17
|
+
static forRoot(options) {
|
|
18
|
+
return assemble(options.isGlobal ?? true, options.imports ?? [], [
|
|
19
|
+
{ provide: tokens_1.JOBS_OPTIONS, useValue: options },
|
|
20
|
+
{ provide: tokens_1.JOBS_STORE, useValue: options.store },
|
|
21
|
+
{ provide: tokens_1.JOBS_DRIZZLE, useExisting: options.drizzleInstanceToken },
|
|
22
|
+
]);
|
|
23
|
+
}
|
|
24
|
+
static forRootAsync(options) {
|
|
25
|
+
return assemble(options.isGlobal ?? true, options.imports ?? [], [
|
|
26
|
+
{
|
|
27
|
+
provide: tokens_1.JOBS_STORE,
|
|
28
|
+
useFactory: options.useStore,
|
|
29
|
+
inject: options.inject ?? [],
|
|
30
|
+
},
|
|
31
|
+
{ provide: tokens_1.JOBS_DRIZZLE, useExisting: options.drizzleInstanceToken },
|
|
32
|
+
]);
|
|
33
|
+
}
|
|
34
|
+
};
|
|
35
|
+
exports.JobsModule = JobsModule;
|
|
36
|
+
exports.JobsModule = JobsModule = __decorate([
|
|
37
|
+
(0, common_1.Module)({})
|
|
38
|
+
], JobsModule);
|
|
39
|
+
function assemble(global, imports, base) {
|
|
40
|
+
return {
|
|
41
|
+
module: JobsModule,
|
|
42
|
+
global,
|
|
43
|
+
// DiscoveryModule powers the @JobHandler scan at bootstrap.
|
|
44
|
+
imports: [core_1.DiscoveryModule, ...imports],
|
|
45
|
+
providers: [...base, jobs_service_1.JobsService, jobs_claimer_service_1.JobsClaimer, jobs_handler_explorer_1.JobsHandlerExplorer],
|
|
46
|
+
exports: [jobs_service_1.JobsService, jobs_claimer_service_1.JobsClaimer, jobs_handler_explorer_1.JobsHandlerExplorer],
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
//# sourceMappingURL=jobs.module.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"jobs.module.js","sourceRoot":"","sources":["../jobs.module.ts"],"names":[],"mappings":";;;;;;;;;AAAA,2CAOwB;AACxB,uCAA+C;AAE/C,iEAAqD;AACrD,mEAA8D;AAC9D,iDAA6C;AAC7C,qCAAkE;AAoB3D,IAAM,UAAU,GAAhB,MAAM,UAAU;IACrB,MAAM,CAAC,OAAO,CAAC,OAA0B;QACvC,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;YAC/D,EAAE,OAAO,EAAE,qBAAY,EAAE,QAAQ,EAAE,OAAO,EAAE;YAC5C,EAAE,OAAO,EAAE,mBAAU,EAAE,QAAQ,EAAE,OAAO,CAAC,KAAK,EAAE;YAChD,EAAE,OAAO,EAAE,qBAAY,EAAE,WAAW,EAAE,OAAO,CAAC,oBAAoB,EAAE;SACrE,CAAC,CAAC;IACL,CAAC;IAED,MAAM,CAAC,YAAY,CAAC,OAA+B;QACjD,OAAO,QAAQ,CAAC,OAAO,CAAC,QAAQ,IAAI,IAAI,EAAE,OAAO,CAAC,OAAO,IAAI,EAAE,EAAE;YAC/D;gBACE,OAAO,EAAE,mBAAU;gBACnB,UAAU,EAAE,OAAO,CAAC,QAAQ;gBAC5B,MAAM,EAAE,OAAO,CAAC,MAAM,IAAI,EAAE;aAC7B;YACD,EAAE,OAAO,EAAE,qBAAY,EAAE,WAAW,EAAE,OAAO,CAAC,oBAAoB,EAAE;SACrE,CAAC,CAAC;IACL,CAAC;CACF,CAAA;AAnBY,gCAAU;qBAAV,UAAU;IADtB,IAAA,eAAM,EAAC,EAAE,CAAC;GACE,UAAU,CAmBtB;AAED,SAAS,QAAQ,CACf,MAAe,EACf,OAA+C,EAC/C,IAAgB;IAEhB,OAAO;QACL,MAAM,EAAE,UAAU;QAClB,MAAM;QACN,4DAA4D;QAC5D,OAAO,EAAE,CAAC,sBAAe,EAAE,GAAG,OAAO,CAAC;QACtC,SAAS,EAAE,CAAC,GAAG,IAAI,EAAE,0BAAW,EAAE,kCAAW,EAAE,2CAAmB,CAAC;QACnE,OAAO,EAAE,CAAC,0BAAW,EAAE,kCAAW,EAAE,2CAAmB,CAAC;KACzD,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { EnqueueJobInput, JobStore } from './interfaces';
|
|
2
|
+
/**
|
|
3
|
+
* Enqueues jobs **inside the caller's business transaction** — the job row
|
|
4
|
+
* commits atomically with your writes. Inject it into a `@Transactional()`
|
|
5
|
+
* service and call `enqueue` alongside your business writes; if the
|
|
6
|
+
* transaction rolls back, the job was never enqueued.
|
|
7
|
+
*
|
|
8
|
+
* `enqueue` returns the store's native shape: the **sqlite** store returns a
|
|
9
|
+
* synchronous `JobRow` (call it without `await` inside a synchronous
|
|
10
|
+
* `@Transactional` body); the **postgres** and **mysql** stores return a
|
|
11
|
+
* `Promise` (await it). Type the service as `JobsService<typeof yourStore>` to get the
|
|
12
|
+
* exact shape. On a `(name, uniqueKey)` collision with an ACTIVE job it
|
|
13
|
+
* returns that existing row — a dedup no-op.
|
|
14
|
+
*
|
|
15
|
+
* Requires the host app to configure `@nestjs-cls/transactional` with the
|
|
16
|
+
* Drizzle adapter (`enableTransactionProxy: true`) — `@InjectTransaction()`
|
|
17
|
+
* resolves the transaction-scoped Drizzle instance from it (and falls back to
|
|
18
|
+
* the base instance outside any transaction).
|
|
19
|
+
*/
|
|
20
|
+
export declare class JobsService<TStore extends JobStore = JobStore> {
|
|
21
|
+
private readonly db;
|
|
22
|
+
private readonly store;
|
|
23
|
+
constructor(db: unknown, store: TStore);
|
|
24
|
+
enqueue<TPayload extends object>(input: EnqueueJobInput<TPayload>): ReturnType<TStore['enqueue']>;
|
|
25
|
+
}
|