@onebun/core 0.2.14 → 0.2.16
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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@onebun/core",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.16",
|
|
4
4
|
"description": "Core package for OneBun framework - decorators, DI, modules, controllers",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -45,11 +45,11 @@
|
|
|
45
45
|
"dependencies": {
|
|
46
46
|
"effect": "^3.13.10",
|
|
47
47
|
"arktype": "^2.0.0",
|
|
48
|
-
"@onebun/logger": "^0.2.
|
|
48
|
+
"@onebun/logger": "^0.2.2",
|
|
49
49
|
"@onebun/envs": "^0.2.2",
|
|
50
50
|
"@onebun/metrics": "^0.2.2",
|
|
51
51
|
"@onebun/requests": "^0.2.1",
|
|
52
|
-
"@onebun/trace": "^0.2.
|
|
52
|
+
"@onebun/trace": "^0.2.2"
|
|
53
53
|
},
|
|
54
54
|
"devDependencies": {
|
|
55
55
|
"bun-types": "^1.3.8",
|
|
@@ -20,6 +20,7 @@ import {
|
|
|
20
20
|
LoggerService,
|
|
21
21
|
makeLogger,
|
|
22
22
|
makeLoggerFromOptions,
|
|
23
|
+
shutdownLogger,
|
|
23
24
|
type SyncLogger,
|
|
24
25
|
} from '@onebun/logger';
|
|
25
26
|
import {
|
|
@@ -295,9 +296,26 @@ export class OneBunApplication<QA extends import('../queue/types').QueueAdapterC
|
|
|
295
296
|
|
|
296
297
|
// Use provided logger layer, or create from options, or use default
|
|
297
298
|
// Priority: loggerLayer > loggerOptions > env variables > NODE_ENV defaults
|
|
299
|
+
// Auto-populate OTLP resource attributes from tracing config if available
|
|
300
|
+
const loggerOptions = this.options.loggerOptions
|
|
301
|
+
? {
|
|
302
|
+
...this.options.loggerOptions,
|
|
303
|
+
otlpResourceAttributes: this.options.loggerOptions.otlpResourceAttributes ?? (
|
|
304
|
+
this.options.loggerOptions.otlpEndpoint && this.options.tracing
|
|
305
|
+
? {
|
|
306
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
307
|
+
'service.name': this.options.tracing.serviceName ?? 'onebun-service',
|
|
308
|
+
// eslint-disable-next-line @typescript-eslint/naming-convention
|
|
309
|
+
'service.version': this.options.tracing.serviceVersion ?? '1.0.0',
|
|
310
|
+
}
|
|
311
|
+
: undefined
|
|
312
|
+
),
|
|
313
|
+
}
|
|
314
|
+
: undefined;
|
|
315
|
+
|
|
298
316
|
this.loggerLayer = this.options.loggerLayer
|
|
299
|
-
?? (
|
|
300
|
-
? makeLoggerFromOptions(
|
|
317
|
+
?? (loggerOptions
|
|
318
|
+
? makeLoggerFromOptions(loggerOptions)
|
|
301
319
|
: makeLogger());
|
|
302
320
|
|
|
303
321
|
// Initialize logger with application class name as context
|
|
@@ -1744,6 +1762,12 @@ export class OneBunApplication<QA extends import('../queue/types').QueueAdapterC
|
|
|
1744
1762
|
this.logger.debug('HTTP server stopped');
|
|
1745
1763
|
}
|
|
1746
1764
|
|
|
1765
|
+
// Shutdown trace service — flush pending spans before module destroy
|
|
1766
|
+
if (this.traceService?.shutdown) {
|
|
1767
|
+
this.logger.debug('Shutting down trace service');
|
|
1768
|
+
await this.traceService.shutdown();
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1747
1771
|
// Call onModuleDestroy lifecycle hook
|
|
1748
1772
|
if (this.rootModule?.callOnModuleDestroy) {
|
|
1749
1773
|
this.logger.debug('Calling onModuleDestroy hooks');
|
|
@@ -1763,6 +1787,9 @@ export class OneBunApplication<QA extends import('../queue/types').QueueAdapterC
|
|
|
1763
1787
|
}
|
|
1764
1788
|
|
|
1765
1789
|
this.logger.info('OneBun application stopped');
|
|
1790
|
+
|
|
1791
|
+
// Shutdown logger transport LAST — flush OTLP log batches after final log message
|
|
1792
|
+
await shutdownLogger();
|
|
1766
1793
|
}
|
|
1767
1794
|
|
|
1768
1795
|
/**
|
|
@@ -189,6 +189,48 @@ describe('queue-decorators', () => {
|
|
|
189
189
|
expect(handlers[0].propertyKey).toBe('handleReady');
|
|
190
190
|
});
|
|
191
191
|
|
|
192
|
+
it('should support multiple @OnQueueReady handlers in one class', () => {
|
|
193
|
+
class TestService {
|
|
194
|
+
@OnQueueReady()
|
|
195
|
+
handleReady1() {}
|
|
196
|
+
|
|
197
|
+
@OnQueueReady()
|
|
198
|
+
handleReady2() {}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const handlers = getLifecycleHandlers(TestService, 'ON_READY');
|
|
202
|
+
expect(handlers.length).toBe(2);
|
|
203
|
+
expect(handlers[0].propertyKey).toBe('handleReady1');
|
|
204
|
+
expect(handlers[1].propertyKey).toBe('handleReady2');
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should return empty array when no @OnQueueReady handlers', () => {
|
|
208
|
+
class TestService {
|
|
209
|
+
// eslint-disable-next-line @typescript-eslint/no-empty-function
|
|
210
|
+
handle() {}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
const handlers = getLifecycleHandlers(TestService, 'ON_READY');
|
|
214
|
+
expect(handlers.length).toBe(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should not mix @OnQueueReady with other lifecycle metadata', () => {
|
|
218
|
+
class TestService {
|
|
219
|
+
@OnQueueReady()
|
|
220
|
+
handleReady() {}
|
|
221
|
+
|
|
222
|
+
@OnQueueError()
|
|
223
|
+
handleError(_error: Error) {}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const readyHandlers = getLifecycleHandlers(TestService, 'ON_READY');
|
|
227
|
+
const errorHandlers = getLifecycleHandlers(TestService, 'ON_ERROR');
|
|
228
|
+
expect(readyHandlers.length).toBe(1);
|
|
229
|
+
expect(readyHandlers[0].propertyKey).toBe('handleReady');
|
|
230
|
+
expect(errorHandlers.length).toBe(1);
|
|
231
|
+
expect(errorHandlers[0].propertyKey).toBe('handleError');
|
|
232
|
+
});
|
|
233
|
+
|
|
192
234
|
it('should register @OnQueueError handler', () => {
|
|
193
235
|
class TestService {
|
|
194
236
|
@OnQueueError()
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import type { Message } from './types';
|
|
14
14
|
|
|
15
15
|
import { InMemoryQueueAdapter } from './adapters/memory.adapter';
|
|
16
|
+
import { OnQueueReady, Subscribe } from './decorators';
|
|
16
17
|
import { QueueService } from './queue.service';
|
|
17
18
|
|
|
18
19
|
describe('QueueService', () => {
|
|
@@ -341,6 +342,161 @@ describe('QueueService', () => {
|
|
|
341
342
|
});
|
|
342
343
|
});
|
|
343
344
|
|
|
345
|
+
describe('registerService lifecycle', () => {
|
|
346
|
+
test('should call @OnQueueReady handler when queue starts', async () => {
|
|
347
|
+
let readyCalled = false;
|
|
348
|
+
|
|
349
|
+
class TestService {
|
|
350
|
+
@OnQueueReady()
|
|
351
|
+
handleReady() {
|
|
352
|
+
readyCalled = true;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const instance = new TestService();
|
|
357
|
+
await service.registerService(instance, TestService);
|
|
358
|
+
expect(readyCalled).toBe(false);
|
|
359
|
+
|
|
360
|
+
await service.start();
|
|
361
|
+
expect(readyCalled).toBe(true);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
test('should allow publishing from @OnQueueReady handler', async () => {
|
|
365
|
+
const received: Message[] = [];
|
|
366
|
+
|
|
367
|
+
class TestService {
|
|
368
|
+
@OnQueueReady()
|
|
369
|
+
handleReady() {
|
|
370
|
+
service.publish('init.ready', { status: 'ok' });
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
@Subscribe('init.ready')
|
|
374
|
+
handleInit(_message: Message) {}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
const instance = new TestService();
|
|
378
|
+
|
|
379
|
+
// Subscribe before start so the handler is in place
|
|
380
|
+
await service.subscribe('init.ready', async (message) => {
|
|
381
|
+
received.push(message);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
await service.registerService(instance, TestService);
|
|
385
|
+
await service.start();
|
|
386
|
+
|
|
387
|
+
expect(received.length).toBe(1);
|
|
388
|
+
expect(received[0].data).toEqual({ status: 'ok' });
|
|
389
|
+
});
|
|
390
|
+
|
|
391
|
+
test('should call multiple @OnQueueReady handlers in order', async () => {
|
|
392
|
+
const callOrder: string[] = [];
|
|
393
|
+
|
|
394
|
+
class TestService {
|
|
395
|
+
@OnQueueReady()
|
|
396
|
+
handleReady1() {
|
|
397
|
+
callOrder.push('first');
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
@OnQueueReady()
|
|
401
|
+
handleReady2() {
|
|
402
|
+
callOrder.push('second');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const instance = new TestService();
|
|
407
|
+
await service.registerService(instance, TestService);
|
|
408
|
+
await service.start();
|
|
409
|
+
|
|
410
|
+
expect(callOrder).toEqual(['first', 'second']);
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
test('should call @OnQueueReady with correct this binding', async () => {
|
|
414
|
+
let capturedValue: string | null = null as string | null;
|
|
415
|
+
|
|
416
|
+
class TestService {
|
|
417
|
+
readonly name = 'test-service';
|
|
418
|
+
|
|
419
|
+
@OnQueueReady()
|
|
420
|
+
handleReady() {
|
|
421
|
+
capturedValue = this.name;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const instance = new TestService();
|
|
426
|
+
await service.registerService(instance, TestService);
|
|
427
|
+
await service.start();
|
|
428
|
+
|
|
429
|
+
expect(capturedValue).toBe('test-service');
|
|
430
|
+
});
|
|
431
|
+
|
|
432
|
+
test('should call @OnQueueReady again after stop() and start()', async () => {
|
|
433
|
+
let readyCount = 0;
|
|
434
|
+
|
|
435
|
+
class TestService {
|
|
436
|
+
@OnQueueReady()
|
|
437
|
+
handleReady() {
|
|
438
|
+
readyCount++;
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const instance = new TestService();
|
|
443
|
+
await service.registerService(instance, TestService);
|
|
444
|
+
|
|
445
|
+
await service.start();
|
|
446
|
+
expect(readyCount).toBe(1);
|
|
447
|
+
|
|
448
|
+
await service.stop();
|
|
449
|
+
await service.start();
|
|
450
|
+
expect(readyCount).toBe(2);
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('should call @OnQueueReady on adapter reconnect', async () => {
|
|
454
|
+
let readyCount = 0;
|
|
455
|
+
|
|
456
|
+
class TestService {
|
|
457
|
+
@OnQueueReady()
|
|
458
|
+
handleReady() {
|
|
459
|
+
readyCount++;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
const instance = new TestService();
|
|
464
|
+
await service.registerService(instance, TestService);
|
|
465
|
+
|
|
466
|
+
await service.start();
|
|
467
|
+
expect(readyCount).toBe(1);
|
|
468
|
+
|
|
469
|
+
// Simulate reconnect without stopping the service
|
|
470
|
+
await adapter.disconnect();
|
|
471
|
+
await adapter.connect();
|
|
472
|
+
expect(readyCount).toBe(2);
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
test('should not call @OnQueueReady on adapter reconnect when service is stopped', async () => {
|
|
476
|
+
let readyCount = 0;
|
|
477
|
+
|
|
478
|
+
class TestService {
|
|
479
|
+
@OnQueueReady()
|
|
480
|
+
handleReady() {
|
|
481
|
+
readyCount++;
|
|
482
|
+
}
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
const instance = new TestService();
|
|
486
|
+
await service.registerService(instance, TestService);
|
|
487
|
+
|
|
488
|
+
await service.start();
|
|
489
|
+
expect(readyCount).toBe(1);
|
|
490
|
+
|
|
491
|
+
await service.stop();
|
|
492
|
+
|
|
493
|
+
// Reconnect while service is stopped — handler should NOT fire
|
|
494
|
+
await adapter.connect();
|
|
495
|
+
expect(readyCount).toBe(1);
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
});
|
|
499
|
+
|
|
344
500
|
describe('error handling', () => {
|
|
345
501
|
test('should handle subscription errors gracefully', async () => {
|
|
346
502
|
await service.start();
|
|
@@ -56,6 +56,8 @@ export class QueueService {
|
|
|
56
56
|
private subscriptions: Subscription[] = [];
|
|
57
57
|
private started = false;
|
|
58
58
|
private config: QueueConfig;
|
|
59
|
+
private onReadyHandlers: Array<() => void> = [];
|
|
60
|
+
private adapterOnReadyRegistered = false;
|
|
59
61
|
|
|
60
62
|
constructor(config: QueueConfig) {
|
|
61
63
|
this.config = config;
|
|
@@ -86,11 +88,28 @@ export class QueueService {
|
|
|
86
88
|
await this.adapter.connect();
|
|
87
89
|
}
|
|
88
90
|
|
|
91
|
+
// Register adapter-level onReady listener once for reconnection scenarios
|
|
92
|
+
if (!this.adapterOnReadyRegistered) {
|
|
93
|
+
this.adapter.on('onReady', () => {
|
|
94
|
+
if (this.started) {
|
|
95
|
+
for (const handler of this.onReadyHandlers) {
|
|
96
|
+
handler();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
});
|
|
100
|
+
this.adapterOnReadyRegistered = true;
|
|
101
|
+
}
|
|
102
|
+
|
|
89
103
|
if (this.scheduler) {
|
|
90
104
|
this.scheduler.start();
|
|
91
105
|
}
|
|
92
106
|
|
|
93
107
|
this.started = true;
|
|
108
|
+
|
|
109
|
+
// Call @OnQueueReady handlers — queue is fully ready at this point
|
|
110
|
+
for (const handler of this.onReadyHandlers) {
|
|
111
|
+
handler();
|
|
112
|
+
}
|
|
94
113
|
}
|
|
95
114
|
|
|
96
115
|
/**
|
|
@@ -359,7 +378,7 @@ export class QueueService {
|
|
|
359
378
|
const onReadyHandlers = getMetadata(QUEUE_METADATA.ON_READY, serviceClass) || [];
|
|
360
379
|
for (const handler of onReadyHandlers) {
|
|
361
380
|
const method = serviceInstance[handler.propertyKey].bind(serviceInstance);
|
|
362
|
-
this.
|
|
381
|
+
this.onReadyHandlers.push(method);
|
|
363
382
|
}
|
|
364
383
|
|
|
365
384
|
const onErrorHandlers = getMetadata(QUEUE_METADATA.ON_ERROR, serviceClass) || [];
|