@objectstack/service-queue 4.0.3 → 4.0.5

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 ADDED
@@ -0,0 +1,453 @@
1
+ # @objectstack/service-queue
2
+
3
+ Queue Service for ObjectStack — implements `IQueueService` with in-memory and BullMQ adapters.
4
+
5
+ ## Features
6
+
7
+ - **Multiple Adapters**: In-memory (development) and BullMQ/Redis (production)
8
+ - **Job Queues**: Organize work into named queues with priorities
9
+ - **Worker Pools**: Process jobs concurrently with configurable workers
10
+ - **Retry Logic**: Automatic retry with exponential backoff
11
+ - **Job Scheduling**: Delay job execution or schedule for future
12
+ - **Progress Tracking**: Track job progress and completion
13
+ - **Job Events**: Listen to job lifecycle events (active, completed, failed)
14
+ - **Rate Limiting**: Control job processing rate
15
+
16
+ ## Installation
17
+
18
+ ```bash
19
+ pnpm add @objectstack/service-queue
20
+ ```
21
+
22
+ For BullMQ adapter (production):
23
+ ```bash
24
+ pnpm add bullmq ioredis
25
+ ```
26
+
27
+ ## Basic Usage
28
+
29
+ ```typescript
30
+ import { defineStack } from '@objectstack/spec';
31
+ import { ServiceQueue } from '@objectstack/service-queue';
32
+
33
+ const stack = defineStack({
34
+ services: [
35
+ ServiceQueue.configure({
36
+ adapter: 'memory', // or 'bullmq'
37
+ defaultQueue: 'default',
38
+ }),
39
+ ],
40
+ });
41
+ ```
42
+
43
+ ## Configuration
44
+
45
+ ### In-Memory Adapter (Development)
46
+
47
+ ```typescript
48
+ ServiceQueue.configure({
49
+ adapter: 'memory',
50
+ concurrency: 5, // Max concurrent jobs
51
+ });
52
+ ```
53
+
54
+ ### BullMQ Adapter (Production)
55
+
56
+ ```typescript
57
+ ServiceQueue.configure({
58
+ adapter: 'bullmq',
59
+ redis: {
60
+ host: 'localhost',
61
+ port: 6379,
62
+ password: process.env.REDIS_PASSWORD,
63
+ },
64
+ queues: {
65
+ default: { concurrency: 10 },
66
+ email: { concurrency: 5, rateLimit: { max: 100, duration: 60000 } },
67
+ reports: { concurrency: 2 },
68
+ },
69
+ });
70
+ ```
71
+
72
+ ## Service API
73
+
74
+ ```typescript
75
+ // Get queue service
76
+ const queue = kernel.getService<IQueueService>('queue');
77
+ ```
78
+
79
+ ### Adding Jobs
80
+
81
+ ```typescript
82
+ // Add a simple job
83
+ await queue.add('email', 'send_welcome', {
84
+ to: 'user@example.com',
85
+ template: 'welcome',
86
+ });
87
+
88
+ // Add job with options
89
+ await queue.add('reports', 'generate_monthly', {
90
+ month: '2024-01',
91
+ format: 'pdf',
92
+ }, {
93
+ priority: 1, // Higher number = higher priority
94
+ attempts: 3, // Retry up to 3 times
95
+ backoff: {
96
+ type: 'exponential',
97
+ delay: 1000,
98
+ },
99
+ });
100
+
101
+ // Add delayed job (runs in 1 hour)
102
+ await queue.add('notifications', 'reminder', {
103
+ userId: '123',
104
+ message: 'Don't forget!',
105
+ }, {
106
+ delay: 3600000, // 1 hour in milliseconds
107
+ });
108
+
109
+ // Schedule job for specific time
110
+ await queue.add('cleanup', 'old_files', {}, {
111
+ timestamp: new Date('2024-12-31T23:59:59Z').getTime(),
112
+ });
113
+ ```
114
+
115
+ ### Processing Jobs
116
+
117
+ ```typescript
118
+ // Register a job processor
119
+ queue.process('email', async (job) => {
120
+ console.log('Processing email job:', job.data);
121
+
122
+ // Access job data
123
+ const { to, template } = job.data;
124
+
125
+ // Update progress
126
+ await job.updateProgress(25);
127
+
128
+ // Send email
129
+ await sendEmail(to, template);
130
+
131
+ await job.updateProgress(100);
132
+
133
+ // Return result
134
+ return { sent: true, messageId: 'msg_123' };
135
+ });
136
+
137
+ // Process with concurrency
138
+ queue.process('reports', 5, async (job) => {
139
+ // Up to 5 reports generated concurrently
140
+ return await generateReport(job.data);
141
+ });
142
+
143
+ // Process with named handler
144
+ queue.process('default', 'calculate_metrics', async (job) => {
145
+ return await calculateMetrics(job.data);
146
+ });
147
+ ```
148
+
149
+ ### Job Management
150
+
151
+ ```typescript
152
+ // Get job by ID
153
+ const job = await queue.getJob('email', 'job_abc123');
154
+
155
+ // Get job status
156
+ const status = await job.getState();
157
+ // 'waiting' | 'active' | 'completed' | 'failed' | 'delayed'
158
+
159
+ // Remove job
160
+ await queue.removeJob('email', 'job_abc123');
161
+
162
+ // Retry failed job
163
+ await queue.retryJob('email', 'job_abc123');
164
+
165
+ // Get job result
166
+ const result = await job.returnvalue;
167
+ ```
168
+
169
+ ### Queue Operations
170
+
171
+ ```typescript
172
+ // Pause queue (stop processing new jobs)
173
+ await queue.pause('email');
174
+
175
+ // Resume queue
176
+ await queue.resume('email');
177
+
178
+ // Clear all jobs in queue
179
+ await queue.clear('email');
180
+
181
+ // Get queue statistics
182
+ const stats = await queue.getStats('email');
183
+ // {
184
+ // waiting: 45,
185
+ // active: 5,
186
+ // completed: 1250,
187
+ // failed: 12,
188
+ // delayed: 3
189
+ // }
190
+ ```
191
+
192
+ ## Advanced Features
193
+
194
+ ### Job Events
195
+
196
+ ```typescript
197
+ // Listen to job lifecycle events
198
+ queue.on('email', 'completed', async (job, result) => {
199
+ console.log(`Email sent: ${result.messageId}`);
200
+ });
201
+
202
+ queue.on('email', 'failed', async (job, error) => {
203
+ console.error(`Email failed: ${error.message}`);
204
+ // Send alert to admin
205
+ });
206
+
207
+ queue.on('email', 'progress', async (job, progress) => {
208
+ console.log(`Email progress: ${progress}%`);
209
+ });
210
+
211
+ queue.on('email', 'active', async (job) => {
212
+ console.log(`Email job started: ${job.id}`);
213
+ });
214
+ ```
215
+
216
+ ### Bulk Operations
217
+
218
+ ```typescript
219
+ // Add multiple jobs at once
220
+ await queue.addBulk('email', [
221
+ { name: 'send_welcome', data: { to: 'user1@example.com' } },
222
+ { name: 'send_welcome', data: { to: 'user2@example.com' } },
223
+ { name: 'send_welcome', data: { to: 'user3@example.com' } },
224
+ ]);
225
+
226
+ // Get multiple jobs
227
+ const jobs = await queue.getJobs('email', ['waiting', 'active']);
228
+ ```
229
+
230
+ ### Job Patterns
231
+
232
+ #### Worker Pattern
233
+
234
+ ```typescript
235
+ // Dedicated worker process
236
+ queue.process('heavy_processing', async (job) => {
237
+ // CPU-intensive work
238
+ const result = await processLargeDataset(job.data);
239
+ return result;
240
+ });
241
+ ```
242
+
243
+ #### Fan-Out Pattern
244
+
245
+ ```typescript
246
+ // Split work across multiple jobs
247
+ await queue.add('orchestrator', 'process_batch', { batchId: '123' });
248
+
249
+ queue.process('orchestrator', async (job) => {
250
+ const items = await loadBatchItems(job.data.batchId);
251
+
252
+ // Create sub-jobs for each item
253
+ for (const item of items) {
254
+ await queue.add('worker', 'process_item', { item });
255
+ }
256
+ });
257
+
258
+ queue.process('worker', async (job) => {
259
+ return await processItem(job.data.item);
260
+ });
261
+ ```
262
+
263
+ #### Priority Queues
264
+
265
+ ```typescript
266
+ // High priority
267
+ await queue.add('tasks', 'urgent', data, { priority: 10 });
268
+
269
+ // Normal priority
270
+ await queue.add('tasks', 'normal', data, { priority: 5 });
271
+
272
+ // Low priority
273
+ await queue.add('tasks', 'background', data, { priority: 1 });
274
+ ```
275
+
276
+ ### Rate Limiting
277
+
278
+ ```typescript
279
+ // Limit queue to 100 jobs per minute
280
+ ServiceQueue.configure({
281
+ adapter: 'bullmq',
282
+ queues: {
283
+ api_calls: {
284
+ concurrency: 5,
285
+ rateLimit: {
286
+ max: 100,
287
+ duration: 60000, // 1 minute
288
+ },
289
+ },
290
+ },
291
+ });
292
+ ```
293
+
294
+ ### Repeatable Jobs
295
+
296
+ ```typescript
297
+ // Add cron-based repeatable job
298
+ await queue.addRepeatable('cleanup', 'old_sessions', {}, {
299
+ cron: '0 2 * * *', // Daily at 2 AM
300
+ });
301
+
302
+ // Add interval-based repeatable job
303
+ await queue.addRepeatable('sync', 'data', {}, {
304
+ every: 300000, // Every 5 minutes
305
+ });
306
+
307
+ // Remove repeatable job
308
+ await queue.removeRepeatable('cleanup', 'old_sessions');
309
+ ```
310
+
311
+ ## Common Use Cases
312
+
313
+ ### Email Queue
314
+
315
+ ```typescript
316
+ queue.process('email', async (job) => {
317
+ const { to, subject, body, template } = job.data;
318
+
319
+ try {
320
+ const result = await emailProvider.send({
321
+ to,
322
+ subject,
323
+ html: renderTemplate(template, job.data),
324
+ });
325
+
326
+ return { messageId: result.id, sentAt: new Date() };
327
+ } catch (error) {
328
+ // Throw error to trigger retry
329
+ throw new Error(`Failed to send email: ${error.message}`);
330
+ }
331
+ });
332
+
333
+ // Add email job
334
+ await queue.add('email', 'welcome', {
335
+ to: 'newuser@example.com',
336
+ template: 'welcome',
337
+ name: 'John Doe',
338
+ }, {
339
+ attempts: 3,
340
+ backoff: { type: 'exponential', delay: 5000 },
341
+ });
342
+ ```
343
+
344
+ ### Report Generation
345
+
346
+ ```typescript
347
+ queue.process('reports', async (job) => {
348
+ const { reportType, userId, dateRange } = job.data;
349
+
350
+ await job.updateProgress(10);
351
+
352
+ // Fetch data
353
+ const data = await fetchReportData(reportType, dateRange);
354
+
355
+ await job.updateProgress(50);
356
+
357
+ // Generate report
358
+ const report = await generatePDF(data);
359
+
360
+ await job.updateProgress(90);
361
+
362
+ // Upload to storage
363
+ const url = await uploadReport(report);
364
+
365
+ await job.updateProgress(100);
366
+
367
+ // Notify user
368
+ await notifyUser(userId, { reportUrl: url });
369
+
370
+ return { url, size: report.length };
371
+ });
372
+ ```
373
+
374
+ ### Webhook Processing
375
+
376
+ ```typescript
377
+ queue.process('webhooks', async (job) => {
378
+ const { url, payload, headers } = job.data;
379
+
380
+ const response = await fetch(url, {
381
+ method: 'POST',
382
+ headers,
383
+ body: JSON.stringify(payload),
384
+ });
385
+
386
+ if (!response.ok) {
387
+ throw new Error(`Webhook failed: ${response.status}`);
388
+ }
389
+
390
+ return { status: response.status, responseTime: Date.now() - job.timestamp };
391
+ });
392
+ ```
393
+
394
+ ## REST API Endpoints
395
+
396
+ ```
397
+ POST /api/v1/queues/:queue/jobs # Add job
398
+ GET /api/v1/queues/:queue/jobs/:id # Get job
399
+ DELETE /api/v1/queues/:queue/jobs/:id # Remove job
400
+ POST /api/v1/queues/:queue/jobs/:id/retry # Retry failed job
401
+ GET /api/v1/queues/:queue/stats # Get queue stats
402
+ POST /api/v1/queues/:queue/pause # Pause queue
403
+ POST /api/v1/queues/:queue/resume # Resume queue
404
+ DELETE /api/v1/queues/:queue # Clear queue
405
+ ```
406
+
407
+ ## Best Practices
408
+
409
+ 1. **Idempotent Jobs**: Design jobs to be safely retried
410
+ 2. **Error Handling**: Always handle errors and throw to trigger retry
411
+ 3. **Progress Updates**: Update progress for long-running jobs
412
+ 4. **Resource Limits**: Set appropriate concurrency limits
413
+ 5. **Job Data**: Keep job data small (< 1MB)
414
+ 6. **Monitoring**: Track queue metrics and job failure rates
415
+ 7. **Cleanup**: Remove completed jobs periodically
416
+
417
+ ## Performance Considerations
418
+
419
+ - **Concurrency**: Tune based on system resources and external API limits
420
+ - **Rate Limiting**: Prevent overwhelming external services
421
+ - **Job Size**: Keep job payloads small for faster serialization
422
+ - **Redis Connection**: Use connection pooling for BullMQ
423
+ - **Queue Organization**: Use separate queues for different job types
424
+
425
+ ## Contract Implementation
426
+
427
+ Implements `IQueueService` from `@objectstack/spec/contracts`:
428
+
429
+ ```typescript
430
+ interface IQueueService {
431
+ add(queue: string, name: string, data: any, options?: JobOptions): Promise<Job>;
432
+ addBulk(queue: string, jobs: JobDefinition[]): Promise<Job[]>;
433
+ process(queue: string, handler: JobHandler): void;
434
+ getJob(queue: string, jobId: string): Promise<Job | null>;
435
+ removeJob(queue: string, jobId: string): Promise<void>;
436
+ retryJob(queue: string, jobId: string): Promise<void>;
437
+ getStats(queue: string): Promise<QueueStats>;
438
+ pause(queue: string): Promise<void>;
439
+ resume(queue: string): Promise<void>;
440
+ clear(queue: string): Promise<void>;
441
+ on(queue: string, event: JobEvent, handler: EventHandler): void;
442
+ }
443
+ ```
444
+
445
+ ## License
446
+
447
+ Apache-2.0
448
+
449
+ ## See Also
450
+
451
+ - [BullMQ Documentation](https://docs.bullmq.io/)
452
+ - [@objectstack/spec/contracts](../../spec/src/contracts/)
453
+ - [Queue Patterns Guide](/content/docs/guides/queues/)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@objectstack/service-queue",
3
- "version": "4.0.3",
3
+ "version": "4.0.5",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Queue Service for ObjectStack — implements IQueueService with in-memory and BullMQ adapters",
6
6
  "type": "module",
@@ -14,13 +14,38 @@
14
14
  }
15
15
  },
16
16
  "dependencies": {
17
- "@objectstack/core": "4.0.3",
18
- "@objectstack/spec": "4.0.3"
17
+ "@objectstack/core": "4.0.5",
18
+ "@objectstack/spec": "4.0.5"
19
19
  },
20
20
  "devDependencies": {
21
- "@types/node": "^25.6.0",
22
- "typescript": "^6.0.2",
23
- "vitest": "^4.1.4"
21
+ "@types/node": "^25.6.2",
22
+ "typescript": "^6.0.3",
23
+ "vitest": "^4.1.5"
24
+ },
25
+ "keywords": [
26
+ "objectstack",
27
+ "service",
28
+ "queue",
29
+ "jobs",
30
+ "background"
31
+ ],
32
+ "author": "ObjectStack",
33
+ "repository": {
34
+ "type": "git",
35
+ "url": "https://github.com/objectstack-ai/framework.git",
36
+ "directory": "packages/services/service-queue"
37
+ },
38
+ "homepage": "https://objectstack.ai/docs",
39
+ "bugs": "https://github.com/objectstack-ai/framework/issues",
40
+ "publishConfig": {
41
+ "access": "public"
42
+ },
43
+ "files": [
44
+ "dist",
45
+ "README.md"
46
+ ],
47
+ "engines": {
48
+ "node": ">=18.0.0"
24
49
  },
25
50
  "scripts": {
26
51
  "build": "tsup --config ../../../tsup.config.ts",
@@ -1,22 +0,0 @@
1
-
2
- > @objectstack/service-queue@4.0.3 build /home/runner/work/framework/framework/packages/services/service-queue
3
- > tsup --config ../../../tsup.config.ts
4
-
5
- CLI Building entry: src/index.ts
6
- CLI Using tsconfig: tsconfig.json
7
- CLI tsup v8.5.1
8
- CLI Using tsup config: /home/runner/work/framework/framework/tsup.config.ts
9
- CLI Target: es2020
10
- CLI Cleaning output folder
11
- ESM Build start
12
- CJS Build start
13
- ESM dist/index.js 2.78 KB
14
- ESM dist/index.js.map 8.33 KB
15
- ESM ⚡️ Build success in 49ms
16
- CJS dist/index.cjs 3.89 KB
17
- CJS dist/index.cjs.map 8.90 KB
18
- CJS ⚡️ Build success in 59ms
19
- DTS Build start
20
- DTS ⚡️ Build success in 6379ms
21
- DTS dist/index.d.ts 3.63 KB
22
- DTS dist/index.d.cts 3.63 KB
package/CHANGELOG.md DELETED
@@ -1,169 +0,0 @@
1
- # @objectstack/service-queue
2
-
3
- ## 4.0.3
4
-
5
- ### Patch Changes
6
-
7
- - @objectstack/spec@4.0.3
8
- - @objectstack/core@4.0.3
9
-
10
- ## 4.0.2
11
-
12
- ### Patch Changes
13
-
14
- - Updated dependencies [5f659e9]
15
- - @objectstack/spec@4.0.2
16
- - @objectstack/core@4.0.2
17
-
18
- ## 4.0.0
19
-
20
- ### Patch Changes
21
-
22
- - Updated dependencies [f08ffc3]
23
- - Updated dependencies [e0b0a78]
24
- - @objectstack/spec@4.0.0
25
- - @objectstack/core@4.0.0
26
-
27
- ## 3.3.1
28
-
29
- ### Patch Changes
30
-
31
- - @objectstack/spec@3.3.1
32
- - @objectstack/core@3.3.1
33
-
34
- ## 3.3.0
35
-
36
- ### Patch Changes
37
-
38
- - @objectstack/spec@3.3.0
39
- - @objectstack/core@3.3.0
40
-
41
- ## 3.2.9
42
-
43
- ### Patch Changes
44
-
45
- - @objectstack/spec@3.2.9
46
- - @objectstack/core@3.2.9
47
-
48
- ## 3.2.8
49
-
50
- ### Patch Changes
51
-
52
- - @objectstack/spec@3.2.8
53
- - @objectstack/core@3.2.8
54
-
55
- ## 3.2.7
56
-
57
- ### Patch Changes
58
-
59
- - @objectstack/spec@3.2.7
60
- - @objectstack/core@3.2.7
61
-
62
- ## 3.2.6
63
-
64
- ### Patch Changes
65
-
66
- - @objectstack/spec@3.2.6
67
- - @objectstack/core@3.2.6
68
-
69
- ## 3.2.5
70
-
71
- ### Patch Changes
72
-
73
- - @objectstack/spec@3.2.5
74
- - @objectstack/core@3.2.5
75
-
76
- ## 3.2.4
77
-
78
- ### Patch Changes
79
-
80
- - @objectstack/spec@3.2.4
81
- - @objectstack/core@3.2.4
82
-
83
- ## 3.2.3
84
-
85
- ### Patch Changes
86
-
87
- - @objectstack/spec@3.2.3
88
- - @objectstack/core@3.2.3
89
-
90
- ## 3.2.2
91
-
92
- ### Patch Changes
93
-
94
- - Updated dependencies [46defbb]
95
- - @objectstack/spec@3.2.2
96
- - @objectstack/core@3.2.2
97
-
98
- ## 3.2.1
99
-
100
- ### Patch Changes
101
-
102
- - Updated dependencies [850b546]
103
- - @objectstack/spec@3.2.1
104
- - @objectstack/core@3.2.1
105
-
106
- ## 3.2.0
107
-
108
- ### Patch Changes
109
-
110
- - Updated dependencies [5901c29]
111
- - @objectstack/spec@3.2.0
112
- - @objectstack/core@3.2.0
113
-
114
- ## 3.1.1
115
-
116
- ### Patch Changes
117
-
118
- - Updated dependencies [953d667]
119
- - @objectstack/spec@3.1.1
120
- - @objectstack/core@3.1.1
121
-
122
- ## 3.1.0
123
-
124
- ### Patch Changes
125
-
126
- - Updated dependencies [0088830]
127
- - @objectstack/spec@3.1.0
128
- - @objectstack/core@3.1.0
129
-
130
- ## 3.0.11
131
-
132
- ### Patch Changes
133
-
134
- - Updated dependencies [92d9d99]
135
- - @objectstack/spec@3.0.11
136
- - @objectstack/core@3.0.11
137
-
138
- ## 3.0.10
139
-
140
- ### Patch Changes
141
-
142
- - Updated dependencies [d1e5d31]
143
- - @objectstack/spec@3.0.10
144
- - @objectstack/core@3.0.10
145
-
146
- ## 3.0.9
147
-
148
- ### Patch Changes
149
-
150
- - Updated dependencies [15e0df6]
151
- - @objectstack/spec@3.0.9
152
- - @objectstack/core@3.0.9
153
-
154
- ## 3.0.8
155
-
156
- ### Patch Changes
157
-
158
- - Updated dependencies [5a968a2]
159
- - @objectstack/spec@3.0.8
160
- - @objectstack/core@3.0.8
161
-
162
- ## 3.0.7
163
-
164
- ### Patch Changes
165
-
166
- - Updated dependencies [0119bd7]
167
- - Updated dependencies [5426bdf]
168
- - @objectstack/spec@3.0.7
169
- - @objectstack/core@3.0.7
@@ -1,58 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { IQueueService, QueuePublishOptions, QueueHandler } from '@objectstack/spec/contracts';
4
-
5
- /**
6
- * Configuration for the BullMQ queue adapter.
7
- */
8
- export interface BullMQQueueAdapterOptions {
9
- /** Redis connection URL (e.g. 'redis://localhost:6379') */
10
- redisUrl: string;
11
- /** Default job options */
12
- defaultJobOptions?: {
13
- /** Number of retry attempts */
14
- attempts?: number;
15
- /** Backoff strategy */
16
- backoff?: { type: 'fixed' | 'exponential'; delay: number };
17
- };
18
- }
19
-
20
- /**
21
- * BullMQ queue adapter skeleton implementing IQueueService.
22
- *
23
- * This is a placeholder for future BullMQ integration.
24
- * Concrete implementation will use the `bullmq` package.
25
- *
26
- * @example
27
- * ```ts
28
- * const queue = new BullMQQueueAdapter({ redisUrl: 'redis://localhost:6379' });
29
- * await queue.publish('orders', { orderId: 123 });
30
- * ```
31
- */
32
- export class BullMQQueueAdapter implements IQueueService {
33
- private readonly redisUrl: string;
34
-
35
- constructor(options: BullMQQueueAdapterOptions) {
36
- this.redisUrl = options.redisUrl;
37
- }
38
-
39
- async publish<T = unknown>(_queue: string, _data: T, _options?: QueuePublishOptions): Promise<string> {
40
- throw new Error(`BullMQQueueAdapter not yet implemented (url: ${this.redisUrl})`);
41
- }
42
-
43
- async subscribe<T = unknown>(_queue: string, _handler: QueueHandler<T>): Promise<void> {
44
- throw new Error('BullMQQueueAdapter not yet implemented');
45
- }
46
-
47
- async unsubscribe(_queue: string): Promise<void> {
48
- throw new Error('BullMQQueueAdapter not yet implemented');
49
- }
50
-
51
- async getQueueSize(_queue: string): Promise<number> {
52
- throw new Error('BullMQQueueAdapter not yet implemented');
53
- }
54
-
55
- async purge(_queue: string): Promise<void> {
56
- throw new Error('BullMQQueueAdapter not yet implemented');
57
- }
58
- }
package/src/index.ts DELETED
@@ -1,8 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- export { QueueServicePlugin } from './queue-service-plugin.js';
4
- export type { QueueServicePluginOptions } from './queue-service-plugin.js';
5
- export { MemoryQueueAdapter } from './memory-queue-adapter.js';
6
- export type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';
7
- export { BullMQQueueAdapter } from './bullmq-queue-adapter.js';
8
- export type { BullMQQueueAdapterOptions } from './bullmq-queue-adapter.js';
@@ -1,87 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import { describe, it, expect } from 'vitest';
4
- import { MemoryQueueAdapter } from './memory-queue-adapter';
5
- import type { IQueueService, QueueMessage } from '@objectstack/spec/contracts';
6
-
7
- describe('MemoryQueueAdapter', () => {
8
- it('should implement IQueueService contract', () => {
9
- const queue: IQueueService = new MemoryQueueAdapter();
10
- expect(typeof queue.publish).toBe('function');
11
- expect(typeof queue.subscribe).toBe('function');
12
- expect(typeof queue.unsubscribe).toBe('function');
13
- expect(typeof queue.getQueueSize).toBe('function');
14
- expect(typeof queue.purge).toBe('function');
15
- });
16
-
17
- it('should publish and deliver to subscriber', async () => {
18
- const queue = new MemoryQueueAdapter();
19
- const received: QueueMessage[] = [];
20
-
21
- await queue.subscribe('orders', async (msg) => {
22
- received.push(msg);
23
- });
24
-
25
- const id = await queue.publish('orders', { orderId: 123 });
26
- expect(id).toBe('msg-1');
27
- expect(received).toHaveLength(1);
28
- expect(received[0].data).toEqual({ orderId: 123 });
29
- expect(received[0].attempts).toBe(1);
30
- });
31
-
32
- it('should support multiple subscribers', async () => {
33
- const queue = new MemoryQueueAdapter();
34
- const log1: unknown[] = [];
35
- const log2: unknown[] = [];
36
-
37
- await queue.subscribe('events', async (msg) => { log1.push(msg.data); });
38
- await queue.subscribe('events', async (msg) => { log2.push(msg.data); });
39
-
40
- await queue.publish('events', 'hello');
41
- expect(log1).toEqual(['hello']);
42
- expect(log2).toEqual(['hello']);
43
- });
44
-
45
- it('should unsubscribe from a queue', async () => {
46
- const queue = new MemoryQueueAdapter();
47
- const received: unknown[] = [];
48
-
49
- await queue.subscribe('q1', async (msg) => { received.push(msg.data); });
50
- await queue.unsubscribe('q1');
51
- await queue.publish('q1', 'data');
52
- expect(received).toHaveLength(0);
53
- });
54
-
55
- it('should retain dead letters when no subscribers', async () => {
56
- const queue = new MemoryQueueAdapter();
57
- const id = await queue.publish('orphan', { lost: true });
58
- expect(id).toBe('msg-1');
59
- });
60
-
61
- it('should return queue size of 0 for in-memory queue', async () => {
62
- const queue = new MemoryQueueAdapter();
63
- expect(await queue.getQueueSize('test')).toBe(0);
64
- });
65
-
66
- it('should purge a queue by removing handlers', async () => {
67
- const queue = new MemoryQueueAdapter();
68
- const received: unknown[] = [];
69
-
70
- await queue.subscribe('q1', async (msg) => { received.push(msg.data); });
71
- await queue.purge('q1');
72
- await queue.publish('q1', 'data');
73
- expect(received).toHaveLength(0);
74
- });
75
-
76
- it('should increment message counter across publishes', async () => {
77
- const queue = new MemoryQueueAdapter();
78
- const ids: string[] = [];
79
-
80
- await queue.subscribe('q', async () => {});
81
- ids.push(await queue.publish('q', 'a'));
82
- ids.push(await queue.publish('q', 'b'));
83
- ids.push(await queue.publish('q', 'c'));
84
-
85
- expect(ids).toEqual(['msg-1', 'msg-2', 'msg-3']);
86
- });
87
- });
@@ -1,82 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { IQueueService, QueuePublishOptions, QueueMessage, QueueHandler } from '@objectstack/spec/contracts';
4
-
5
- /**
6
- * Configuration options for MemoryQueueAdapter.
7
- */
8
- export interface MemoryQueueAdapterOptions {
9
- /** Maximum number of messages retained per queue (0 = unlimited) */
10
- maxQueueSize?: number;
11
- }
12
-
13
- /**
14
- * In-memory queue adapter implementing IQueueService.
15
- *
16
- * Provides synchronous in-process pub/sub delivery.
17
- * Suitable for single-process environments, development, and testing.
18
- */
19
- export class MemoryQueueAdapter implements IQueueService {
20
- private readonly handlers = new Map<string, QueueHandler[]>();
21
- private readonly deadLetters: QueueMessage[] = [];
22
- private msgCounter = 0;
23
- private readonly maxQueueSize: number;
24
-
25
- constructor(options: MemoryQueueAdapterOptions = {}) {
26
- this.maxQueueSize = options.maxQueueSize ?? 0;
27
- }
28
-
29
- async publish<T = unknown>(queue: string, data: T, options?: QueuePublishOptions): Promise<string> {
30
- const id = `msg-${++this.msgCounter}`;
31
- const msg: QueueMessage<T> = {
32
- id,
33
- data,
34
- attempts: 0,
35
- timestamp: Date.now(),
36
- };
37
-
38
- const fns = this.handlers.get(queue) ?? [];
39
- if (fns.length === 0) {
40
- // No subscribers — retain as dead letter if within limits
41
- if (this.maxQueueSize === 0 || this.deadLetters.length < this.maxQueueSize) {
42
- this.deadLetters.push(msg);
43
- }
44
- return id;
45
- }
46
-
47
- const maxRetries = options?.retries ?? 0;
48
- for (const handler of fns) {
49
- let attempt = 0;
50
- let success = false;
51
- while (!success && attempt <= maxRetries) {
52
- try {
53
- msg.attempts = attempt + 1;
54
- await handler(msg as QueueMessage);
55
- success = true;
56
- } catch {
57
- attempt++;
58
- }
59
- }
60
- }
61
-
62
- return id;
63
- }
64
-
65
- async subscribe<T = unknown>(queue: string, handler: QueueHandler<T>): Promise<void> {
66
- const existing = this.handlers.get(queue) ?? [];
67
- this.handlers.set(queue, [...existing, handler as QueueHandler]);
68
- }
69
-
70
- async unsubscribe(queue: string): Promise<void> {
71
- this.handlers.delete(queue);
72
- }
73
-
74
- async getQueueSize(_queue: string): Promise<number> {
75
- // In-memory: no persistent queue depth tracking
76
- return 0;
77
- }
78
-
79
- async purge(queue: string): Promise<void> {
80
- this.handlers.delete(queue);
81
- }
82
- }
@@ -1,62 +0,0 @@
1
- // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license.
2
-
3
- import type { Plugin, PluginContext } from '@objectstack/core';
4
- import { MemoryQueueAdapter } from './memory-queue-adapter.js';
5
- import type { MemoryQueueAdapterOptions } from './memory-queue-adapter.js';
6
-
7
- /**
8
- * Configuration options for the QueueServicePlugin.
9
- */
10
- export interface QueueServicePluginOptions {
11
- /** Queue adapter type (default: 'memory') */
12
- adapter?: 'memory' | 'bullmq';
13
- /** Options for the memory queue adapter */
14
- memory?: MemoryQueueAdapterOptions;
15
- /** Redis connection URL (used when adapter is 'bullmq') */
16
- redisUrl?: string;
17
- }
18
-
19
- /**
20
- * QueueServicePlugin — Production IQueueService implementation.
21
- *
22
- * Registers a queue service with the kernel during the init phase.
23
- * Supports in-memory and BullMQ adapters.
24
- *
25
- * @example
26
- * ```ts
27
- * import { ObjectKernel } from '@objectstack/core';
28
- * import { QueueServicePlugin } from '@objectstack/service-queue';
29
- *
30
- * const kernel = new ObjectKernel();
31
- * kernel.use(new QueueServicePlugin({ adapter: 'memory' }));
32
- * await kernel.bootstrap();
33
- *
34
- * const queue = kernel.getService('queue');
35
- * await queue.publish('orders', { orderId: 123 });
36
- * ```
37
- */
38
- export class QueueServicePlugin implements Plugin {
39
- name = 'com.objectstack.service.queue';
40
- version = '1.0.0';
41
- type = 'standard';
42
-
43
- private readonly options: QueueServicePluginOptions;
44
-
45
- constructor(options: QueueServicePluginOptions = {}) {
46
- this.options = { adapter: 'memory', ...options };
47
- }
48
-
49
- async init(ctx: PluginContext): Promise<void> {
50
- const adapter = this.options.adapter;
51
- if (adapter === 'bullmq') {
52
- throw new Error(
53
- 'BullMQ queue adapter is not yet implemented. ' +
54
- 'Use adapter: "memory" or provide a custom IQueueService via ctx.registerService("queue", impl).'
55
- );
56
- }
57
-
58
- const queue = new MemoryQueueAdapter(this.options.memory);
59
- ctx.registerService('queue', queue);
60
- ctx.logger.info('QueueServicePlugin: registered memory queue adapter');
61
- }
62
- }
package/tsconfig.json DELETED
@@ -1,17 +0,0 @@
1
- {
2
- "extends": "../../../tsconfig.json",
3
- "compilerOptions": {
4
- "outDir": "dist",
5
- "rootDir": "src",
6
- "types": [
7
- "node"
8
- ]
9
- },
10
- "include": [
11
- "src"
12
- ],
13
- "exclude": [
14
- "node_modules",
15
- "dist"
16
- ]
17
- }