@onebun/core 0.1.1 → 0.1.2

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,231 @@
1
+ /**
2
+ * Shared Redis Provider
3
+ *
4
+ * Singleton provider for sharing Redis connection across multiple consumers
5
+ * (cache, websocket, etc.)
6
+ */
7
+
8
+ import {
9
+ Context,
10
+ Effect,
11
+ Layer,
12
+ } from 'effect';
13
+
14
+ import { RedisClient, type RedisClientOptions } from './redis-client';
15
+
16
+ /**
17
+ * Options for shared Redis connection
18
+ */
19
+ export interface SharedRedisOptions {
20
+ /** Redis connection URL */
21
+ url: string;
22
+ /** Key prefix for all operations */
23
+ keyPrefix?: string;
24
+ /** Enable automatic reconnection */
25
+ reconnect?: boolean;
26
+ /** Enable TLS */
27
+ tls?: boolean;
28
+ }
29
+
30
+ /**
31
+ * Singleton provider for shared Redis connection
32
+ *
33
+ * @example
34
+ * ```typescript
35
+ * // Configure at app startup
36
+ * SharedRedisProvider.configure({
37
+ * url: 'redis://localhost:6379',
38
+ * keyPrefix: 'myapp:',
39
+ * });
40
+ *
41
+ * // Get shared client
42
+ * const client = await SharedRedisProvider.getClient();
43
+ *
44
+ * // Use in cache
45
+ * const cache = new RedisCache({ useSharedClient: true });
46
+ *
47
+ * // Use in WebSocket storage
48
+ * const wsStorage = new RedisWsStorage(await SharedRedisProvider.getClient());
49
+ * ```
50
+ */
51
+ export class SharedRedisProvider {
52
+ private static instance: RedisClient | null = null;
53
+ private static options: SharedRedisOptions | null = null;
54
+ private static connecting: Promise<RedisClient> | null = null;
55
+
56
+ /**
57
+ * Configure the shared Redis connection
58
+ * Must be called before getClient()
59
+ */
60
+ static configure(options: SharedRedisOptions): void {
61
+ // Note: If already connected, configuration will apply to new connections only
62
+ SharedRedisProvider.options = options;
63
+ }
64
+
65
+ /**
66
+ * Get the shared Redis client (creates connection if needed)
67
+ */
68
+ static async getClient(): Promise<RedisClient> {
69
+ // Return existing instance
70
+ if (SharedRedisProvider.instance?.isConnected()) {
71
+ return SharedRedisProvider.instance;
72
+ }
73
+
74
+ // Wait for existing connection attempt
75
+ if (SharedRedisProvider.connecting) {
76
+ return await SharedRedisProvider.connecting;
77
+ }
78
+
79
+ // Check configuration
80
+ if (!SharedRedisProvider.options) {
81
+ throw new Error(
82
+ 'SharedRedisProvider not configured. Call SharedRedisProvider.configure() first.',
83
+ );
84
+ }
85
+
86
+ // Create new connection
87
+ SharedRedisProvider.connecting = SharedRedisProvider.createConnection();
88
+
89
+ try {
90
+ SharedRedisProvider.instance = await SharedRedisProvider.connecting;
91
+
92
+ return SharedRedisProvider.instance;
93
+ } finally {
94
+ SharedRedisProvider.connecting = null;
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Create a new Redis connection
100
+ */
101
+ private static async createConnection(): Promise<RedisClient> {
102
+ const options = SharedRedisProvider.options!;
103
+ const client = new RedisClient({
104
+ url: options.url,
105
+ keyPrefix: options.keyPrefix,
106
+ reconnect: options.reconnect,
107
+ tls: options.tls,
108
+ });
109
+
110
+ await client.connect();
111
+
112
+ return client;
113
+ }
114
+
115
+ /**
116
+ * Disconnect the shared client
117
+ */
118
+ static async disconnect(): Promise<void> {
119
+ if (SharedRedisProvider.instance) {
120
+ await SharedRedisProvider.instance.disconnect();
121
+ SharedRedisProvider.instance = null;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * Check if shared client is connected
127
+ */
128
+ static isConnected(): boolean {
129
+ return SharedRedisProvider.instance?.isConnected() ?? false;
130
+ }
131
+
132
+ /**
133
+ * Check if shared Redis is configured
134
+ */
135
+ static isConfigured(): boolean {
136
+ return SharedRedisProvider.options !== null;
137
+ }
138
+
139
+ /**
140
+ * Create a standalone Redis client (not shared)
141
+ * Useful for isolated scenarios
142
+ */
143
+ static createClient(options?: Partial<SharedRedisOptions>): RedisClient {
144
+ const baseOptions: Partial<SharedRedisOptions> = SharedRedisProvider.options || {};
145
+ const finalOptions: RedisClientOptions = {
146
+ url: options?.url || baseOptions.url || '',
147
+ keyPrefix: options?.keyPrefix ?? baseOptions.keyPrefix,
148
+ reconnect: options?.reconnect ?? baseOptions.reconnect ?? true,
149
+ tls: options?.tls ?? baseOptions.tls,
150
+ };
151
+
152
+ if (!finalOptions.url) {
153
+ throw new Error('Redis URL is required');
154
+ }
155
+
156
+ return new RedisClient(finalOptions);
157
+ }
158
+
159
+ /**
160
+ * Reset provider state (mainly for testing)
161
+ */
162
+ static async reset(): Promise<void> {
163
+ await SharedRedisProvider.disconnect();
164
+ SharedRedisProvider.options = null;
165
+ }
166
+ }
167
+
168
+ // ============================================================================
169
+ // Effect.js Integration
170
+ // ============================================================================
171
+
172
+ /**
173
+ * Effect.js Tag for shared Redis service
174
+ */
175
+ export class SharedRedisService extends Context.Tag('SharedRedisService')<
176
+ SharedRedisService,
177
+ RedisClient
178
+ >() {}
179
+
180
+ /**
181
+ * Create Effect.js Layer for shared Redis
182
+ *
183
+ * @example
184
+ * ```typescript
185
+ * const redisLayer = makeSharedRedisLayer({
186
+ * url: 'redis://localhost:6379',
187
+ * keyPrefix: 'myapp:',
188
+ * });
189
+ *
190
+ * const program = pipe(
191
+ * SharedRedisService,
192
+ * Effect.flatMap(redis => redis.get('key')),
193
+ * Effect.provide(redisLayer),
194
+ * );
195
+ * ```
196
+ */
197
+ export function makeSharedRedisLayer(
198
+ options: SharedRedisOptions,
199
+ ): Layer.Layer<SharedRedisService> {
200
+ return Layer.scoped(
201
+ SharedRedisService,
202
+ Effect.gen(function* () {
203
+ // Configure shared provider
204
+ SharedRedisProvider.configure(options);
205
+
206
+ // Get client
207
+ const client = yield* Effect.promise(() => SharedRedisProvider.getClient());
208
+
209
+ // Return client with cleanup
210
+ yield* Effect.addFinalizer(() =>
211
+ Effect.promise(async () => {
212
+ // Note: We don't disconnect shared client on scope close
213
+ // because other consumers might still be using it
214
+ }),
215
+ );
216
+
217
+ return client;
218
+ }),
219
+ );
220
+ }
221
+
222
+ /**
223
+ * Get shared Redis client as Effect
224
+ */
225
+ export const getSharedRedis = Effect.gen(function* () {
226
+ if (!SharedRedisProvider.isConfigured()) {
227
+ return yield* Effect.fail(new Error('SharedRedisProvider not configured'));
228
+ }
229
+
230
+ return yield* Effect.promise(() => SharedRedisProvider.getClient());
231
+ });
package/src/types.ts CHANGED
@@ -266,6 +266,56 @@ export interface ApplicationOptions {
266
266
  batchTimeout?: number;
267
267
  };
268
268
  };
269
+
270
+ /**
271
+ * WebSocket configuration
272
+ */
273
+ websocket?: WebSocketApplicationOptions;
274
+
275
+ /**
276
+ * Enable graceful shutdown on SIGTERM/SIGINT
277
+ * When enabled, the application will cleanly shutdown on process signals,
278
+ * including closing shared Redis connections.
279
+ * Set to false to disable automatic signal handling.
280
+ * @defaultValue true
281
+ */
282
+ gracefulShutdown?: boolean;
283
+ }
284
+
285
+ /**
286
+ * WebSocket storage type
287
+ */
288
+ export type WsStorageType = 'memory' | 'redis';
289
+
290
+ /**
291
+ * WebSocket storage options
292
+ */
293
+ export interface WsStorageOptions {
294
+ /** Storage type */
295
+ type: WsStorageType;
296
+ /** Redis-specific options */
297
+ redis?: {
298
+ /** Redis connection URL */
299
+ url: string;
300
+ /** Key prefix for Redis keys */
301
+ prefix?: string;
302
+ };
303
+ }
304
+
305
+ /**
306
+ * WebSocket configuration for OneBunApplication
307
+ */
308
+ export interface WebSocketApplicationOptions {
309
+ /** Enable/disable WebSocket (default: auto - enabled if gateways exist) */
310
+ enabled?: boolean;
311
+ /** Storage options */
312
+ storage?: WsStorageOptions;
313
+ /** Ping interval in milliseconds for heartbeat (socket.io) */
314
+ pingInterval?: number;
315
+ /** Ping timeout in milliseconds (socket.io) */
316
+ pingTimeout?: number;
317
+ /** Maximum payload size in bytes */
318
+ maxPayload?: number;
269
319
  }
270
320
 
271
321
  /**