@onebun/core 0.2.6 → 0.2.7

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.
@@ -0,0 +1,82 @@
1
+ /**
2
+ * Tests for QueueServiceProxy
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ test,
8
+ expect,
9
+ } from 'bun:test';
10
+
11
+
12
+ import { InMemoryQueueAdapter } from './adapters/memory.adapter';
13
+ import { QueueServiceProxy, QUEUE_NOT_ENABLED_ERROR_MESSAGE } from './queue-service-proxy';
14
+ import { QueueService } from './queue.service';
15
+
16
+ describe('QueueServiceProxy', () => {
17
+ test('throws with expected message when delegate is null', async () => {
18
+ const proxy = new QueueServiceProxy();
19
+ expect(() => proxy.getAdapter()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
20
+ expect(() => proxy.getScheduler()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
21
+ await expect(proxy.publish('e', {})).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
22
+ await expect(proxy.publishBatch([{ pattern: 'e', data: {} }])).rejects.toThrow(
23
+ QUEUE_NOT_ENABLED_ERROR_MESSAGE,
24
+ );
25
+ await expect(
26
+ proxy.subscribe('e', async () => {
27
+ /* no-op for throw test */
28
+ }),
29
+ ).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
30
+ await expect(proxy.addScheduledJob('j', { pattern: 'e', schedule: { every: 1000 } })).rejects.toThrow(
31
+ QUEUE_NOT_ENABLED_ERROR_MESSAGE,
32
+ );
33
+ await expect(proxy.removeScheduledJob('j')).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
34
+ await expect(proxy.getScheduledJobs()).rejects.toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
35
+ expect(() => proxy.supports('pattern-subscriptions')).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
36
+ expect(() =>
37
+ proxy.on('onReady', () => {
38
+ /* no-op */
39
+ }),
40
+ ).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
41
+ expect(() =>
42
+ proxy.off('onReady', () => {
43
+ /* no-op */
44
+ }),
45
+ ).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
46
+ });
47
+
48
+ test('delegates to real QueueService after setDelegate', async () => {
49
+ const adapter = new InMemoryQueueAdapter();
50
+ const real = new QueueService({ adapter: 'memory' as const });
51
+ await real.initialize(adapter);
52
+ await real.start();
53
+
54
+ const proxy = new QueueServiceProxy();
55
+ proxy.setDelegate(real);
56
+
57
+ expect(proxy.getAdapter()).toBe(adapter);
58
+ expect(proxy.getScheduler()).toBe(real.getScheduler());
59
+ expect(proxy.supports('pattern-subscriptions')).toBe(true);
60
+
61
+ const ids = await proxy.publish('test.event', { x: 1 });
62
+ expect(ids).toBeDefined();
63
+ expect(typeof ids).toBe('string');
64
+
65
+ const received: unknown[] = [];
66
+ const sub = await proxy.subscribe('test.event', async (msg) => {
67
+ received.push(msg.data);
68
+ });
69
+ expect(sub.isActive).toBe(true);
70
+
71
+ await proxy.publish('test.event', { y: 2 });
72
+ // Allow microtask for handler
73
+ await Promise.resolve();
74
+ await new Promise((r) => setTimeout(r, 10));
75
+ expect(received.length).toBe(1);
76
+ expect(received[0]).toEqual({ y: 2 });
77
+
78
+ await real.stop();
79
+ proxy.setDelegate(null);
80
+ expect(() => proxy.getAdapter()).toThrow(QUEUE_NOT_ENABLED_ERROR_MESSAGE);
81
+ });
82
+ });
@@ -0,0 +1,114 @@
1
+ /**
2
+ * QueueServiceProxy — placeholder for DI when the real QueueService is created after setup().
3
+ * When the queue is enabled, the application sets the real QueueService via setDelegate().
4
+ * When the queue is not enabled, any method call throws a clear error.
5
+ */
6
+
7
+ import type { QueueService } from './queue.service';
8
+ import type { QueueScheduler } from './scheduler';
9
+ import type { QueueAdapter } from './types';
10
+ import type {
11
+ MessageHandler,
12
+ PublishOptions,
13
+ QueueEvents,
14
+ ScheduledJobInfo,
15
+ ScheduledJobOptions,
16
+ SubscribeOptions,
17
+ Subscription,
18
+ QueueFeature,
19
+ } from './types';
20
+
21
+ const QUEUE_NOT_ENABLED_MESSAGE =
22
+ 'Queue is not enabled. Enable it via `queue.enabled: true` in application options or register at least one controller with queue decorators (@Subscribe, @Cron, @Interval, @Timeout).';
23
+
24
+ function throwIfNoDelegate(delegate: QueueService | null): asserts delegate is QueueService {
25
+ if (delegate === null) {
26
+ throw new Error(QUEUE_NOT_ENABLED_MESSAGE);
27
+ }
28
+ }
29
+
30
+ /**
31
+ * Proxy for QueueService used in DI before the real service is created.
32
+ * After initializeQueue(), the application calls setDelegate(realQueueService) when the queue is enabled.
33
+ */
34
+ export class QueueServiceProxy {
35
+ private delegate: QueueService | null = null;
36
+
37
+ setDelegate(service: QueueService | null): void {
38
+ this.delegate = service;
39
+ }
40
+
41
+ getAdapter(): QueueAdapter {
42
+ throwIfNoDelegate(this.delegate);
43
+
44
+ return this.delegate.getAdapter();
45
+ }
46
+
47
+ getScheduler(): QueueScheduler {
48
+ throwIfNoDelegate(this.delegate);
49
+
50
+ return this.delegate.getScheduler();
51
+ }
52
+
53
+ async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
54
+ throwIfNoDelegate(this.delegate);
55
+
56
+ return await this.delegate.publish(pattern, data, options);
57
+ }
58
+
59
+ async publishBatch<T>(
60
+ messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
61
+ ): Promise<string[]> {
62
+ throwIfNoDelegate(this.delegate);
63
+
64
+ return await this.delegate.publishBatch(messages);
65
+ }
66
+
67
+ async subscribe<T>(
68
+ pattern: string,
69
+ handler: MessageHandler<T>,
70
+ options?: SubscribeOptions,
71
+ ): Promise<Subscription> {
72
+ throwIfNoDelegate(this.delegate);
73
+
74
+ return await this.delegate.subscribe(pattern, handler, options);
75
+ }
76
+
77
+ async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
78
+ throwIfNoDelegate(this.delegate);
79
+
80
+ return await this.delegate.addScheduledJob(name, options);
81
+ }
82
+
83
+ async removeScheduledJob(name: string): Promise<boolean> {
84
+ throwIfNoDelegate(this.delegate);
85
+
86
+ return await this.delegate.removeScheduledJob(name);
87
+ }
88
+
89
+ async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
90
+ throwIfNoDelegate(this.delegate);
91
+
92
+ return await this.delegate.getScheduledJobs();
93
+ }
94
+
95
+ supports(feature: QueueFeature): boolean {
96
+ throwIfNoDelegate(this.delegate);
97
+
98
+ return this.delegate.supports(feature);
99
+ }
100
+
101
+ on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
102
+ throwIfNoDelegate(this.delegate);
103
+
104
+ return this.delegate.on(event, handler);
105
+ }
106
+
107
+ off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
108
+ throwIfNoDelegate(this.delegate);
109
+
110
+ return this.delegate.off(event, handler);
111
+ }
112
+ }
113
+
114
+ export const QUEUE_NOT_ENABLED_ERROR_MESSAGE = QUEUE_NOT_ENABLED_MESSAGE;
@@ -395,8 +395,8 @@ export interface QueueAdapterConstructor {
395
395
  * Queue configuration options
396
396
  */
397
397
  export interface QueueConfig {
398
- /** Adapter type or custom adapter class */
399
- adapter: BuiltInAdapterType | QueueAdapterConstructor;
398
+ /** Adapter type (built-in or custom, e.g. 'jetstream') or custom adapter class */
399
+ adapter: QueueAdapterType | QueueAdapterConstructor;
400
400
 
401
401
  /** Adapter-specific options */
402
402
  options?: Record<string, unknown>;
package/src/types.ts CHANGED
@@ -1,9 +1,15 @@
1
1
  import type { BaseMiddleware } from './module/middleware';
2
+ import type { QueueAdapterConstructor } from './queue/types';
2
3
  import type { Type } from 'arktype';
3
- import type { Effect, Layer } from 'effect';
4
+ import type {
5
+ Context,
6
+ Effect,
7
+ Layer,
8
+ } from 'effect';
4
9
 
5
10
  import type { Logger, LoggerOptions } from '@onebun/logger';
6
11
 
12
+
7
13
  /**
8
14
  * HTTP Request type used in OneBun controllers.
9
15
  * Extends standard Web API Request with:
@@ -175,6 +181,11 @@ export interface ModuleInstance {
175
181
  * using this module's DI scope (services + logger + config).
176
182
  */
177
183
  resolveMiddleware?(classes: Function[]): Function[];
184
+
185
+ /**
186
+ * Register a service instance by tag (e.g. before setup() for application-provided services like QueueService proxy).
187
+ */
188
+ registerService?<T>(tag: Context.Tag<unknown, T>, instance: T): void;
178
189
  }
179
190
 
180
191
  /**
@@ -422,6 +433,12 @@ export interface ApplicationOptions {
422
433
  */
423
434
  queue?: QueueApplicationOptions;
424
435
 
436
+ /**
437
+ * Static file serving: serve files from a directory for requests not matched by API routes.
438
+ * Optional path prefix and fallback file (e.g. index.html) for SPA-style routing.
439
+ */
440
+ static?: StaticApplicationOptions;
441
+
425
442
  /**
426
443
  * Documentation configuration (OpenAPI/Swagger)
427
444
  */
@@ -467,8 +484,10 @@ export type QueueAdapterType = 'memory' | 'redis';
467
484
  export interface QueueApplicationOptions {
468
485
  /** Enable/disable queue (default: auto - enabled if handlers exist) */
469
486
  enabled?: boolean;
470
- /** Adapter type or custom adapter instance */
471
- adapter?: QueueAdapterType;
487
+ /** Adapter type, or custom adapter constructor (e.g. for NATS JetStream) */
488
+ adapter?: QueueAdapterType | QueueAdapterConstructor;
489
+ /** Options passed to the custom adapter constructor when adapter is a class */
490
+ options?: unknown;
472
491
  /** Redis-specific options (only used when adapter is 'redis') */
473
492
  redis?: {
474
493
  /** Use shared Redis provider instead of dedicated connection */
@@ -528,6 +547,39 @@ export interface WebSocketApplicationOptions {
528
547
  maxPayload?: number;
529
548
  }
530
549
 
550
+ /**
551
+ * Static file serving configuration for OneBunApplication.
552
+ * Serves files from a directory for requests not matched by API/ws/docs/metrics routes.
553
+ */
554
+ export interface StaticApplicationOptions {
555
+ /**
556
+ * Filesystem path to the directory to serve (static root).
557
+ * Can be absolute or relative to the process cwd.
558
+ */
559
+ root: string;
560
+
561
+ /**
562
+ * URL path prefix under which static files are served.
563
+ * Empty or '/' means "everything not matched by API routes".
564
+ * Example: '/app' — only paths starting with /app are served from static root
565
+ * (the prefix is stripped when resolving the file path).
566
+ */
567
+ pathPrefix?: string;
568
+
569
+ /**
570
+ * Fallback file name (e.g. 'index.html') for SPA-style client-side routing.
571
+ * When the requested file is not found, this file under static root is returned instead.
572
+ */
573
+ fallbackFile?: string;
574
+
575
+ /**
576
+ * TTL in milliseconds for caching file existence checks.
577
+ * Reduces disk I/O for repeated requests. Use 0 to disable caching.
578
+ * @defaultValue 60000
579
+ */
580
+ fileExistenceCacheTtlMs?: number;
581
+ }
582
+
531
583
  /**
532
584
  * Documentation configuration for OneBunApplication
533
585
  * Enables automatic OpenAPI spec generation and Swagger UI