@fluojs/queue 1.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 fluo contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.ko.md ADDED
@@ -0,0 +1,134 @@
1
+ # @fluojs/queue
2
+
3
+ <p><a href="./README.md"><kbd>English</kbd></a> <strong><kbd>한국어</kbd></strong></p>
4
+
5
+ fluo를 위한 Redis 기반 분산 작업 처리 패키지입니다. 데코레이터 기반의 워커 탐색, 자동 작업 직렬화, 그리고 수명 주기 관리 기능을 제공합니다.
6
+
7
+ ## 목차
8
+
9
+ - [설치](#설치)
10
+ - [사용 시점](#사용-시점)
11
+ - [빠른 시작](#빠른-시작)
12
+ - [일반적인 패턴](#일반적인-패턴)
13
+ - [공개 API 개요](#공개-api-개요)
14
+ - [관련 패키지](#관련-패키지)
15
+ - [예제 소스](#예제-소스)
16
+
17
+ ## 설치
18
+
19
+ ```bash
20
+ npm install @fluojs/queue @fluojs/redis
21
+ ```
22
+
23
+ ## 사용 시점
24
+
25
+ - 실행 시간이 길거나 리소스를 많이 사용하는 작업을 백그라운드에서 처리해야 할 때.
26
+ - 이메일 발송, 이미지 처리 등 비용이 큰 작업을 요청-응답 주기와 분리하고 싶을 때.
27
+ - 재시도 로직, 백오프(Backoff), 데드 레터(Dead-letter) 처리가 포함된 분산 큐가 필요할 때.
28
+
29
+ ## 빠른 시작
30
+
31
+ ### 1. 작업(Job) 및 워커(Worker) 정의
32
+
33
+ 작업 클래스를 만들고, 이를 처리할 클래스에 `@QueueWorker` 데코레이터를 붙입니다.
34
+
35
+ ```typescript
36
+ import { QueueWorker } from '@fluojs/queue';
37
+
38
+ export class ProcessOrderJob {
39
+ constructor(public readonly orderId: string) {}
40
+ }
41
+
42
+ @QueueWorker(ProcessOrderJob, { attempts: 3, backoff: { type: 'fixed', delayMs: 5000 } })
43
+ export class OrderWorker {
44
+ async handle(job: ProcessOrderJob) {
45
+ console.log(`주문 처리 중: ${job.orderId}`);
46
+ // 처리 로직 작성
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### 2. 모듈 등록 및 작업 추가
52
+
53
+ `QueueModule`을 등록하고 `QueueLifecycleService`를 주입받아 작업을 큐에 추가합니다.
54
+
55
+ `QueueModule.forRoot(...)`는 큐 등록을 위한 지원되는 루트 엔트리포인트입니다.
56
+
57
+ 큐 등록은 `QueueModule.forRoot(...)`로 구성합니다.
58
+
59
+ ```typescript
60
+ import { Module, Inject } from '@fluojs/core';
61
+ import { QueueModule, QueueLifecycleService } from '@fluojs/queue';
62
+ import { RedisModule } from '@fluojs/redis';
63
+
64
+ @Module({
65
+ imports: [
66
+ RedisModule.forRoot({ host: 'localhost', port: 6379 }),
67
+ QueueModule.forRoot(),
68
+ ],
69
+ providers: [OrderWorker],
70
+ })
71
+ export class AppModule {}
72
+
73
+ export class OrderService {
74
+ @Inject(QueueLifecycleService)
75
+ private readonly queue: QueueLifecycleService;
76
+
77
+ async placeOrder(id: string) {
78
+ await this.queue.enqueue(new ProcessOrderJob(id));
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## 일반적인 패턴
84
+
85
+ ### 이름 있는 Redis 클라이언트
86
+
87
+ `clientName`을 생략하면 애플리케이션의 기본 `@fluojs/redis` 클라이언트를 계속 사용합니다. 큐가 기본 Redis 대신 다른 연결을 사용해야 한다면 `RedisModule.forRootNamed(...)`로 등록한 이름을 `clientName`에 지정하세요.
88
+
89
+ ```typescript
90
+ QueueModule.forRoot({ clientName: 'jobs' })
91
+ ```
92
+
93
+ ### 분산 재시도 (Distributed Retries)
94
+
95
+ 워커 설정에서 최대 시도 횟수와 백오프 전략을 지정하여 일시적인 실패를 자동으로 처리할 수 있습니다.
96
+
97
+ ```typescript
98
+ @QueueWorker(MyJob, {
99
+ attempts: 5,
100
+ backoff: { type: 'exponential', delayMs: 1000 }
101
+ })
102
+ ```
103
+
104
+ ### 데드 레터 처리 (Dead-Letter Handling)
105
+
106
+ 모든 재시도에 실패한 작업은 Redis의 데드 레터 리스트(`fluo:queue:dead-letter:<jobName>`)로 자동 이동되어, 나중에 수동으로 확인하거나 복구할 수 있습니다.
107
+
108
+ `QueueModule.forRoot()`는 기본적으로 작업별 최근 데드 레터 엔트리 `1_000`개만 유지합니다. 무제한 보관이 꼭 필요하면 `defaultDeadLetterMaxEntries: false`로 opt-out 하고, 더 엄격한 운영 예산이 필요하면 더 작은 양의 정수를 지정하세요.
109
+
110
+ 저수준 provider 조합을 루트 barrel API의 일부가 아니라 내부 구현 세부사항으로 취급해야 합니다. 저수준 provider helper는 문서화된 루트 barrel 계약에 포함되지 않습니다.
111
+
112
+ ## 공개 API 개요
113
+
114
+ ### 핵심 구성 요소
115
+ - `QueueModule`: 큐 기능을 위한 기본 모듈입니다.
116
+ - `QueueModule.forRoot(options)`: 애플리케이션 수준 큐 등록을 구성합니다.
117
+ - `QueueLifecycleService`: 작업을 큐에 추가(`enqueue(job)`)하기 위한 기본 서비스입니다.
118
+ - `@QueueWorker(JobClass, options?)`: 특정 작업을 처리할 핸들러를 지정하는 데코레이터입니다.
119
+
120
+
121
+ ### 타입
122
+ - `QueueModuleOptions`: 전역 큐 설정(clientName, 기본 시도 횟수, 동시성, 전송률 제한 등)을 위한 타입입니다.
123
+ - `QueueWorkerOptions`: 개별 작업 설정(시도 횟수, 백오프, 동시성, jobName, 전송률 제한 등)을 위한 타입입니다.
124
+ - `QueueBackoffOptions`: 재시도 백오프 설정(`type`, `delayMs`)을 위한 타입입니다.
125
+
126
+ ## 관련 패키지
127
+
128
+ - `@fluojs/redis`: 작업 데이터 저장을 위한 필수 백엔드 패키지입니다.
129
+ - `@fluojs/cron`: 정해진 시간에 반복 실행되어야 하는 백그라운드 작업을 위한 패키지입니다.
130
+
131
+ ## 예제 소스
132
+
133
+ - `packages/queue/src/module.test.ts`: 워커 탐색 및 작업 추가 테스트 예제.
134
+ - `packages/queue/src/public-surface.test.ts`: 공개 API 계약 검증 예제.
package/README.md ADDED
@@ -0,0 +1,136 @@
1
+ # @fluojs/queue
2
+
3
+ <p><strong><kbd>English</kbd></strong> <a href="./README.ko.md"><kbd>한국어</kbd></a></p>
4
+
5
+ Redis-backed distributed job processing for fluo. It features decorator-based worker discovery, automatic job serialization, and lifecycle-managed execution.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [When to use](#when-to-use)
11
+ - [Quick Start](#quick-start)
12
+ - [Common Patterns](#common-patterns)
13
+ - [Public API](#public-api)
14
+ - [Related Packages](#related-packages)
15
+ - [Example Sources](#example-sources)
16
+
17
+ ## Installation
18
+
19
+ ```bash
20
+ npm install @fluojs/queue @fluojs/redis
21
+ ```
22
+
23
+ ## When to Use
24
+
25
+ - When you need to process long-running or resource-intensive tasks in the background.
26
+ - When you want to decouple expensive operations (e.g., sending emails, image processing) from the request-response cycle.
27
+ - When you need a distributed queue with retry logic, backoff, and dead-letter handling.
28
+
29
+ ## Quick Start
30
+
31
+ ### 1. Define a Job and Worker
32
+
33
+ Create a job class and a worker class decorated with `@QueueWorker`.
34
+
35
+ ```typescript
36
+ import { QueueWorker } from '@fluojs/queue';
37
+
38
+ export class ProcessOrderJob {
39
+ constructor(public readonly orderId: string) {}
40
+ }
41
+
42
+ @QueueWorker(ProcessOrderJob, { attempts: 3, backoff: { type: 'fixed', delayMs: 5000 } })
43
+ export class OrderWorker {
44
+ async handle(job: ProcessOrderJob) {
45
+ console.log(`Processing order: ${job.orderId}`);
46
+ // Your logic here
47
+ }
48
+ }
49
+ ```
50
+
51
+ ### 2. Register and Enqueue
52
+
53
+ Import `QueueModule` and inject `QueueLifecycleService` to enqueue jobs.
54
+
55
+ `QueueModule.forRoot(...)` is the supported root entrypoint for queue registration.
56
+
57
+ Use `QueueModule.forRoot(...)` for application-level queue registration.
58
+
59
+ ```typescript
60
+ import { Module, Inject } from '@fluojs/core';
61
+ import { QueueModule, QueueLifecycleService } from '@fluojs/queue';
62
+ import { RedisModule } from '@fluojs/redis';
63
+
64
+ @Module({
65
+ imports: [
66
+ RedisModule.forRoot({ host: 'localhost', port: 6379 }),
67
+ QueueModule.forRoot(),
68
+ ],
69
+ providers: [OrderWorker],
70
+ })
71
+ export class AppModule {}
72
+
73
+ export class OrderService {
74
+ @Inject(QueueLifecycleService)
75
+ private readonly queue: QueueLifecycleService;
76
+
77
+ async placeOrder(id: string) {
78
+ await this.queue.enqueue(new ProcessOrderJob(id));
79
+ }
80
+ }
81
+ ```
82
+
83
+ ## Common Patterns
84
+
85
+ ### Named Redis Client
86
+
87
+ Leave `clientName` unset to keep using the default `@fluojs/redis` client from your app. If your queues should use a non-default Redis connection, set `clientName` to the name registered with `RedisModule.forRootNamed(...)`.
88
+
89
+ ```typescript
90
+ QueueModule.forRoot({ clientName: 'jobs' })
91
+ ```
92
+
93
+ ### Distributed Retries
94
+
95
+ Workers can be configured with a maximum number of attempts and backoff strategies to handle transient failures automatically.
96
+
97
+ ```typescript
98
+ @QueueWorker(MyJob, {
99
+ attempts: 5,
100
+ backoff: { type: 'exponential', delayMs: 1000 }
101
+ })
102
+ ```
103
+
104
+ ### Dead-Letter Handling
105
+
106
+ Jobs that fail all retry attempts are automatically moved to a dead-letter list in Redis (`fluo:queue:dead-letter:<jobName>`) for manual inspection or recovery.
107
+
108
+ `QueueModule.forRoot()` keeps the most recent `1_000` dead-letter entries per job by default. Set `defaultDeadLetterMaxEntries: false` to opt out, or provide a smaller positive number when operators need a tighter retention budget.
109
+
110
+ Treat low-level provider assembly as an internal implementation detail: low-level provider helpers are not part of the documented root-barrel contract.
111
+
112
+ ## Public API Overview
113
+
114
+ ### Core
115
+ - `QueueModule`: Main entry point for queue registration.
116
+ - `QueueModule.forRoot(options)`: Registers queue support for an application module.
117
+ - `QueueLifecycleService`: Primary service for enqueuing jobs (`enqueue(job)`).
118
+ - `@QueueWorker(JobClass, options?)`: Decorator to mark a class as a job handler.
119
+
120
+
121
+ ### Types
122
+ - `QueueModuleOptions`: Global queue settings (clientName, default attempts, concurrency, rate limiting).
123
+ - `QueueWorkerOptions`: Per-job settings (attempts, backoff, concurrency, jobName, rate limiting).
124
+ - `QueueBackoffOptions`: Retry backoff settings (`type`, `delayMs`).
125
+
126
+ `QueueModuleOptions` also includes dead-letter retention controls such as `defaultDeadLetterMaxEntries`.
127
+
128
+ ## Related Packages
129
+
130
+ - `@fluojs/redis`: Required as the backing store for job persistence.
131
+ - `@fluojs/cron`: For scheduled/recurring background tasks.
132
+
133
+ ## Example Sources
134
+
135
+ - `packages/queue/src/module.test.ts`: Worker discovery and enqueueing tests.
136
+ - `packages/queue/src/public-surface.test.ts`: Public API contract verification.
@@ -0,0 +1,28 @@
1
+ import type { ApplicationLogger } from '@fluojs/runtime';
2
+ import type { NormalizedQueueModuleOptions, QueueWorkerDescriptor } from './types.js';
3
+ export interface QueueDeadLetterJob {
4
+ attemptsMade: number;
5
+ data: unknown;
6
+ finishedOn?: number;
7
+ id?: string;
8
+ opts: {
9
+ attempts?: number;
10
+ };
11
+ }
12
+ export interface QueueRedisDeadLetterClient {
13
+ ltrim(key: string, start: number, stop: number): Promise<unknown>;
14
+ rpush(key: string, value: string): Promise<unknown>;
15
+ }
16
+ export declare class QueueDeadLetterManager {
17
+ private readonly options;
18
+ private readonly logger;
19
+ private readonly getRedisClient;
20
+ private readonly pendingWrites;
21
+ constructor(options: NormalizedQueueModuleOptions, logger: ApplicationLogger, getRedisClient: () => QueueRedisDeadLetterClient);
22
+ get pendingWriteCount(): number;
23
+ trackTerminalFailure(descriptor: QueueWorkerDescriptor, job: QueueDeadLetterJob | undefined, error: Error): void;
24
+ drainPendingWrites(): Promise<void>;
25
+ private appendDeadLetterRecord;
26
+ private isTerminalFailure;
27
+ }
28
+ //# sourceMappingURL=dead-letter-manager.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dead-letter-manager.d.ts","sourceRoot":"","sources":["../src/dead-letter-manager.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AAGzD,OAAO,KAAK,EAAE,4BAA4B,EAAE,qBAAqB,EAAE,MAAM,YAAY,CAAC;AAMtF,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,IAAI,EAAE,OAAO,CAAC;IACd,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE;QACJ,QAAQ,CAAC,EAAE,MAAM,CAAC;KACnB,CAAC;CACH;AAED,MAAM,WAAW,0BAA0B;IACzC,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,KAAK,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CACrD;AAED,qBAAa,sBAAsB;IAI/B,OAAO,CAAC,QAAQ,CAAC,OAAO;IACxB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,cAAc;IALjC,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA4B;gBAGvC,OAAO,EAAE,4BAA4B,EACrC,MAAM,EAAE,iBAAiB,EACzB,cAAc,EAAE,MAAM,0BAA0B;IAGnE,IAAI,iBAAiB,IAAI,MAAM,CAE9B;IAED,oBAAoB,CAAC,UAAU,EAAE,qBAAqB,EAAE,GAAG,EAAE,kBAAkB,GAAG,SAAS,EAAE,KAAK,EAAE,KAAK,GAAG,IAAI;IAY1G,kBAAkB,IAAI,OAAO,CAAC,IAAI,CAAC;YAmB3B,sBAAsB;IA+BpC,OAAO,CAAC,iBAAiB;CAQ1B"}
@@ -0,0 +1,66 @@
1
+ import { cloneWithFallback } from '@fluojs/core/internal';
2
+ import { normalizePositiveInteger, withTimeout } from './helpers.js';
3
+ const DEAD_LETTER_DRAIN_TIMEOUT_MS = 5_000;
4
+ export class QueueDeadLetterManager {
5
+ pendingWrites = new Set();
6
+ constructor(options, logger, getRedisClient) {
7
+ this.options = options;
8
+ this.logger = logger;
9
+ this.getRedisClient = getRedisClient;
10
+ }
11
+ get pendingWriteCount() {
12
+ return this.pendingWrites.size;
13
+ }
14
+ trackTerminalFailure(descriptor, job, error) {
15
+ if (!job || !this.isTerminalFailure(job, descriptor.attempts)) {
16
+ return;
17
+ }
18
+ const pendingWrite = this.appendDeadLetterRecord(descriptor, job, error);
19
+ this.pendingWrites.add(pendingWrite);
20
+ pendingWrite.finally(() => {
21
+ this.pendingWrites.delete(pendingWrite);
22
+ });
23
+ }
24
+ async drainPendingWrites() {
25
+ while (this.pendingWrites.size > 0) {
26
+ await Promise.allSettled(Array.from(this.pendingWrites).map(async write => {
27
+ try {
28
+ await withTimeout(write, DEAD_LETTER_DRAIN_TIMEOUT_MS, () => new Error('dead-letter write timed out'));
29
+ } catch (error) {
30
+ this.pendingWrites.delete(write);
31
+ this.logger.error('Dead-letter write did not complete within shutdown timeout.', error, 'QueueLifecycleService');
32
+ }
33
+ }));
34
+ }
35
+ }
36
+ async appendDeadLetterRecord(descriptor, job, error) {
37
+ try {
38
+ const key = deadLetterKey(descriptor.jobName);
39
+ const deadLetter = {
40
+ attemptsMade: job.attemptsMade,
41
+ errorMessage: error.message,
42
+ failedAt: new Date(job.finishedOn ?? Date.now()).toISOString(),
43
+ jobId: job.id ?? '',
44
+ jobName: descriptor.jobName,
45
+ payload: isQueuePayload(job.data) ? cloneWithFallback(job.data) : job.data
46
+ };
47
+ const redis = this.getRedisClient();
48
+ await redis.rpush(key, JSON.stringify(deadLetter));
49
+ if (this.options.defaultDeadLetterMaxEntries !== false) {
50
+ await redis.ltrim(key, -this.options.defaultDeadLetterMaxEntries, -1);
51
+ }
52
+ } catch (deadLetterError) {
53
+ this.logger.error(`Failed to append dead-letter record for queue job ${descriptor.jobName}.`, deadLetterError, 'QueueLifecycleService');
54
+ }
55
+ }
56
+ isTerminalFailure(job, attemptsFallback) {
57
+ const configuredAttempts = typeof job.opts.attempts === 'number' && Number.isFinite(job.opts.attempts) ? normalizePositiveInteger(job.opts.attempts, attemptsFallback) : attemptsFallback;
58
+ return job.attemptsMade >= configuredAttempts;
59
+ }
60
+ }
61
+ function deadLetterKey(jobName) {
62
+ return `fluo:queue:dead-letter:${jobName}`;
63
+ }
64
+ function isQueuePayload(value) {
65
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
66
+ }
@@ -0,0 +1,28 @@
1
+ import type { QueueJobType, QueueWorkerOptions } from './types.js';
2
+ type ClassDecoratorLike = (value: Function, context: ClassDecoratorContext) => void;
3
+ /**
4
+ * Marks a singleton provider class as the worker for one queue job type.
5
+ *
6
+ * @param jobType Job constructor used for discovery, queue naming, and payload rehydration.
7
+ * @param options Optional execution settings such as retries, concurrency, and backoff.
8
+ * @returns A class decorator that stores worker metadata for bootstrap-time discovery.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { QueueWorker } from '@fluojs/queue';
13
+ *
14
+ * class SendEmailJob {
15
+ * constructor(public readonly email: string) {}
16
+ * }
17
+ *
18
+ * @QueueWorker(SendEmailJob, { attempts: 3, backoff: { type: 'exponential', delayMs: 1_000 } })
19
+ * export class SendEmailWorker {
20
+ * async handle(job: SendEmailJob) {
21
+ * await mailer.send(job.email);
22
+ * }
23
+ * }
24
+ * ```
25
+ */
26
+ export declare function QueueWorker(jobType: QueueJobType, options?: QueueWorkerOptions): ClassDecoratorLike;
27
+ export {};
28
+ //# sourceMappingURL=decorators.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../src/decorators.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,YAAY,EAAuB,kBAAkB,EAAE,MAAM,YAAY,CAAC;AAExF,KAAK,kBAAkB,GAAG,CAAC,KAAK,EAAE,QAAQ,EAAE,OAAO,EAAE,qBAAqB,KAAK,IAAI,CAAC;AAgBpF;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,WAAW,CAAC,OAAO,EAAE,YAAY,EAAE,OAAO,GAAE,kBAAuB,GAAG,kBAAkB,CAWvG"}
@@ -0,0 +1,51 @@
1
+ import { metadataSymbol } from '@fluojs/core/internal';
2
+ import { queueWorkerMetadataSymbol } from './metadata.js';
3
+ function getStandardMetadataBag(metadata) {
4
+ void metadataSymbol;
5
+ return metadata;
6
+ }
7
+ function defineStandardQueueWorkerMetadata(metadata, workerMetadata) {
8
+ const bag = getStandardMetadataBag(metadata);
9
+ bag[queueWorkerMetadataSymbol] = {
10
+ jobType: workerMetadata.jobType,
11
+ options: {
12
+ ...workerMetadata.options
13
+ }
14
+ };
15
+ }
16
+
17
+ /**
18
+ * Marks a singleton provider class as the worker for one queue job type.
19
+ *
20
+ * @param jobType Job constructor used for discovery, queue naming, and payload rehydration.
21
+ * @param options Optional execution settings such as retries, concurrency, and backoff.
22
+ * @returns A class decorator that stores worker metadata for bootstrap-time discovery.
23
+ *
24
+ * @example
25
+ * ```ts
26
+ * import { QueueWorker } from '@fluojs/queue';
27
+ *
28
+ * class SendEmailJob {
29
+ * constructor(public readonly email: string) {}
30
+ * }
31
+ *
32
+ * @QueueWorker(SendEmailJob, { attempts: 3, backoff: { type: 'exponential', delayMs: 1_000 } })
33
+ * export class SendEmailWorker {
34
+ * async handle(job: SendEmailJob) {
35
+ * await mailer.send(job.email);
36
+ * }
37
+ * }
38
+ * ```
39
+ */
40
+ export function QueueWorker(jobType, options = {}) {
41
+ const decorator = (_value, context) => {
42
+ const metadata = {
43
+ jobType,
44
+ options: {
45
+ ...options
46
+ }
47
+ };
48
+ defineStandardQueueWorkerMetadata(context.metadata, metadata);
49
+ };
50
+ return decorator;
51
+ }
@@ -0,0 +1,22 @@
1
+ import { type Token } from '@fluojs/core';
2
+ import type { Provider } from '@fluojs/di';
3
+ import type { CompiledModule } from '@fluojs/runtime';
4
+ import type { QueueRateLimiterOptions } from './types.js';
5
+ export type Scope = 'request' | 'singleton' | 'transient';
6
+ export interface DiscoveryCandidate {
7
+ moduleName: string;
8
+ scope: Scope;
9
+ targetType: Function;
10
+ token: Token;
11
+ }
12
+ export declare function scopeFromProvider(provider: Provider): Scope;
13
+ export declare function isClassProvider(provider: Provider): provider is Extract<Provider, {
14
+ provide: Token;
15
+ useClass: Function;
16
+ }>;
17
+ export declare function collectDiscoveryCandidates(compiledModules: readonly CompiledModule[]): DiscoveryCandidate[];
18
+ export declare function normalizePositiveInteger(value: number | undefined, fallback: number): number;
19
+ export declare function normalizePositiveIntegerOrFalse(value: number | false | undefined, fallback: number | false): number | false;
20
+ export declare function normalizeRateLimiter(rateLimiter: QueueRateLimiterOptions | undefined): QueueRateLimiterOptions | undefined;
21
+ export declare function withTimeout<T>(promise: Promise<T>, timeoutMs: number, timeoutErrorFactory: () => Error): Promise<T>;
22
+ //# sourceMappingURL=helpers.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"helpers.d.ts","sourceRoot":"","sources":["../src/helpers.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,KAAK,EAAE,MAAM,cAAc,CAAC;AAE1C,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AAC3C,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAEtD,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,YAAY,CAAC;AAE1D,MAAM,MAAM,KAAK,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,CAAC;AAE1D,MAAM,WAAW,kBAAkB;IACjC,UAAU,EAAE,MAAM,CAAC;IACnB,KAAK,EAAE,KAAK,CAAC;IACb,UAAU,EAAE,QAAQ,CAAC;IACrB,KAAK,EAAE,KAAK,CAAC;CACd;AAED,wBAAgB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,KAAK,CAU3D;AAED,wBAAgB,eAAe,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,IAAI,OAAO,CAAC,QAAQ,EAAE;IAAE,OAAO,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,QAAQ,CAAA;CAAE,CAAC,CAEzH;AAED,wBAAgB,0BAA0B,CAAC,eAAe,EAAE,SAAS,cAAc,EAAE,GAAG,kBAAkB,EAAE,CAoC3G;AAED,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAY5F;AAED,wBAAgB,+BAA+B,CAC7C,KAAK,EAAE,MAAM,GAAG,KAAK,GAAG,SAAS,EACjC,QAAQ,EAAE,MAAM,GAAG,KAAK,GACvB,MAAM,GAAG,KAAK,CAgBhB;AAED,wBAAgB,oBAAoB,CAAC,WAAW,EAAE,uBAAuB,GAAG,SAAS,GAAG,uBAAuB,GAAG,SAAS,CAS1H;AAED,wBAAsB,WAAW,CAAC,CAAC,EACjC,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,EACjB,mBAAmB,EAAE,MAAM,KAAK,GAC/B,OAAO,CAAC,CAAC,CAAC,CAeZ"}
@@ -0,0 +1,93 @@
1
+ import { getClassDiMetadata } from '@fluojs/core/internal';
2
+ export function scopeFromProvider(provider) {
3
+ if (typeof provider === 'function') {
4
+ return getClassDiMetadata(provider)?.scope ?? 'singleton';
5
+ }
6
+ if ('useClass' in provider) {
7
+ return provider.scope ?? getClassDiMetadata(provider.useClass)?.scope ?? 'singleton';
8
+ }
9
+ return 'scope' in provider ? provider.scope ?? 'singleton' : 'singleton';
10
+ }
11
+ export function isClassProvider(provider) {
12
+ return typeof provider === 'object' && provider !== null && 'useClass' in provider;
13
+ }
14
+ export function collectDiscoveryCandidates(compiledModules) {
15
+ const candidates = [];
16
+ for (const compiledModule of compiledModules) {
17
+ for (const provider of compiledModule.definition.providers ?? []) {
18
+ if (typeof provider === 'function') {
19
+ candidates.push({
20
+ moduleName: compiledModule.type.name,
21
+ scope: scopeFromProvider(provider),
22
+ targetType: provider,
23
+ token: provider
24
+ });
25
+ continue;
26
+ }
27
+ if (isClassProvider(provider)) {
28
+ candidates.push({
29
+ moduleName: compiledModule.type.name,
30
+ scope: scopeFromProvider(provider),
31
+ targetType: provider.useClass,
32
+ token: provider.provide
33
+ });
34
+ }
35
+ }
36
+ for (const controller of compiledModule.definition.controllers ?? []) {
37
+ candidates.push({
38
+ moduleName: compiledModule.type.name,
39
+ scope: scopeFromProvider(controller),
40
+ targetType: controller,
41
+ token: controller
42
+ });
43
+ }
44
+ }
45
+ return candidates;
46
+ }
47
+ export function normalizePositiveInteger(value, fallback) {
48
+ if (value === undefined || !Number.isFinite(value)) {
49
+ return fallback;
50
+ }
51
+ const normalized = Math.trunc(value);
52
+ if (normalized < 1) {
53
+ return fallback;
54
+ }
55
+ return normalized;
56
+ }
57
+ export function normalizePositiveIntegerOrFalse(value, fallback) {
58
+ if (value === false) {
59
+ return false;
60
+ }
61
+ if (value === undefined || !Number.isFinite(value)) {
62
+ return fallback;
63
+ }
64
+ const normalized = Math.trunc(value);
65
+ if (normalized < 1) {
66
+ return fallback;
67
+ }
68
+ return normalized;
69
+ }
70
+ export function normalizeRateLimiter(rateLimiter) {
71
+ if (!rateLimiter) {
72
+ return undefined;
73
+ }
74
+ return {
75
+ duration: normalizePositiveInteger(rateLimiter.duration, 1_000),
76
+ max: normalizePositiveInteger(rateLimiter.max, 1)
77
+ };
78
+ }
79
+ export async function withTimeout(promise, timeoutMs, timeoutErrorFactory) {
80
+ let timeoutId;
81
+ const timeoutPromise = new Promise((_, reject) => {
82
+ timeoutId = setTimeout(() => {
83
+ reject(timeoutErrorFactory());
84
+ }, timeoutMs);
85
+ });
86
+ try {
87
+ return await Promise.race([promise, timeoutPromise]);
88
+ } finally {
89
+ if (timeoutId !== undefined) {
90
+ clearTimeout(timeoutId);
91
+ }
92
+ }
93
+ }
@@ -0,0 +1,7 @@
1
+ export { QueueWorker } from './decorators.js';
2
+ export { QueueModule } from './module.js';
3
+ export { QueueLifecycleService } from './service.js';
4
+ export * from './status.js';
5
+ export { QUEUE } from './tokens.js';
6
+ export type { Queue, QueueBackoffOptions, QueueBackoffType, QueueJobType, QueueModuleOptions, QueueRateLimiterOptions, QueueWorkerOptions, } from './types.js';
7
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAC1C,OAAO,EAAE,qBAAqB,EAAE,MAAM,cAAc,CAAC;AACrD,cAAc,aAAa,CAAC;AAC5B,OAAO,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AACpC,YAAY,EACV,KAAK,EACL,mBAAmB,EACnB,gBAAgB,EAChB,YAAY,EACZ,kBAAkB,EAClB,uBAAuB,EACvB,kBAAkB,GACnB,MAAM,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,5 @@
1
+ export { QueueWorker } from './decorators.js';
2
+ export { QueueModule } from './module.js';
3
+ export { QueueLifecycleService } from './service.js';
4
+ export * from './status.js';
5
+ export { QUEUE } from './tokens.js';
@@ -0,0 +1,5 @@
1
+ import type { QueueWorkerMetadata } from './types.js';
2
+ export declare function defineQueueWorkerMetadata(target: Function, metadata: QueueWorkerMetadata): void;
3
+ export declare function getQueueWorkerMetadata(target: Function): QueueWorkerMetadata | undefined;
4
+ export declare const queueWorkerMetadataSymbol: symbol;
5
+ //# sourceMappingURL=metadata.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"metadata.d.ts","sourceRoot":"","sources":["../src/metadata.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,YAAY,CAAC;AAwBtD,wBAAgB,yBAAyB,CAAC,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,mBAAmB,GAAG,IAAI,CAE/F;AAED,wBAAgB,sBAAsB,CAAC,MAAM,EAAE,QAAQ,GAAG,mBAAmB,GAAG,SAAS,CASxF;AAED,eAAO,MAAM,yBAAyB,QAAiC,CAAC"}
@@ -0,0 +1,30 @@
1
+ import { ensureSymbolMetadataPolyfill, metadataSymbol } from '@fluojs/core/internal';
2
+ void ensureSymbolMetadataPolyfill();
3
+ const standardQueueWorkerMetadataKey = Symbol.for('fluo.queue.standard.worker');
4
+ const queueWorkerMetadataStore = new WeakMap();
5
+ function cloneQueueWorkerMetadata(metadata) {
6
+ return {
7
+ jobType: metadata.jobType,
8
+ options: {
9
+ ...metadata.options
10
+ }
11
+ };
12
+ }
13
+ function getStandardMetadataBag(target) {
14
+ return target[metadataSymbol];
15
+ }
16
+ function getStandardQueueWorkerMetadata(target) {
17
+ return getStandardMetadataBag(target)?.[standardQueueWorkerMetadataKey];
18
+ }
19
+ export function defineQueueWorkerMetadata(target, metadata) {
20
+ queueWorkerMetadataStore.set(target, cloneQueueWorkerMetadata(metadata));
21
+ }
22
+ export function getQueueWorkerMetadata(target) {
23
+ const stored = queueWorkerMetadataStore.get(target);
24
+ const standard = getStandardQueueWorkerMetadata(target);
25
+ if (!stored && !standard) {
26
+ return undefined;
27
+ }
28
+ return cloneQueueWorkerMetadata(stored ?? standard);
29
+ }
30
+ export const queueWorkerMetadataSymbol = standardQueueWorkerMetadataKey;