@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.
package/src/index.ts CHANGED
@@ -63,3 +63,99 @@ export type { ServiceClientOptions, ControllerClient } from './service-client.ty
63
63
 
64
64
  // ENV resolver
65
65
  export { resolveEnvOverrides, resolveEnvOverridesSync } from './env-resolver';
66
+
67
+ // WebSocket Gateway
68
+ export { BaseWebSocketGateway } from './ws-base-gateway';
69
+ export {
70
+ WebSocketGateway,
71
+ OnConnect,
72
+ OnDisconnect,
73
+ OnJoinRoom,
74
+ OnLeaveRoom,
75
+ OnMessage,
76
+ Client,
77
+ Socket,
78
+ MessageData,
79
+ RoomName,
80
+ PatternParams,
81
+ WsServer,
82
+ UseWsGuards,
83
+ getGatewayMetadata,
84
+ isWebSocketGateway,
85
+ } from './ws-decorators';
86
+ export {
87
+ WsAuthGuard,
88
+ WsPermissionGuard,
89
+ WsRoomGuard,
90
+ WsAnyPermissionGuard,
91
+ WsServiceGuard,
92
+ WsAllGuards,
93
+ WsAnyGuard,
94
+ WsExecutionContextImpl,
95
+ executeGuards,
96
+ createGuard,
97
+ } from './ws-guards';
98
+ export type {
99
+ WsClientData,
100
+ WsAuthData,
101
+ WsRoom,
102
+ WsHandlerType,
103
+ WsParamType,
104
+ GatewayMetadata,
105
+ WsHandlerMetadata,
106
+ WsParamMetadata,
107
+ WebSocketGatewayOptions,
108
+ WsStorageOptions,
109
+ WebSocketApplicationOptions,
110
+ WsMessage,
111
+ WsHandlerResponse,
112
+ PatternMatch,
113
+ WsExecutionContext,
114
+ WsGuard,
115
+ WsServer as WsServerType,
116
+ } from './ws.types';
117
+ export {
118
+ WsHandlerType as WsHandlerTypeEnum,
119
+ WsParamType as WsParamTypeEnum,
120
+ isWsMessage,
121
+ isWsHandlerResponse,
122
+ isWsClientData,
123
+ isWsRoom,
124
+ } from './ws.types';
125
+ export {
126
+ matchPattern,
127
+ isPatternMatch,
128
+ createPatternMatcher,
129
+ isPattern,
130
+ getPatternParams,
131
+ buildFromPattern,
132
+ } from './ws-pattern-matcher';
133
+ export type { WsStorageAdapter, WsPubSubStorageAdapter, WsStorageEventPayload } from './ws-storage';
134
+ export { WsStorageEvent, isPubSubAdapter } from './ws-storage';
135
+ export { InMemoryWsStorage, createInMemoryWsStorage } from './ws-storage-memory';
136
+ export { RedisWsStorage, createRedisWsStorage } from './ws-storage-redis';
137
+
138
+ // Redis Client (shared)
139
+ export { RedisClient, createRedisClient } from './redis-client';
140
+ export type { RedisClientOptions } from './redis-client';
141
+ export {
142
+ SharedRedisProvider, SharedRedisService, makeSharedRedisLayer, getSharedRedis,
143
+ } from './shared-redis';
144
+ export type { SharedRedisOptions } from './shared-redis';
145
+
146
+ // WebSocket Service Definition and Client
147
+ export {
148
+ createWsServiceDefinition, getWsGatewayNames, getWsEventNames, getWsEndpoint,
149
+ } from './ws-service-definition';
150
+ export type { WsServiceDefinition, WsGatewayDefinition, WsEndpointMetadata } from './ws-service-definition';
151
+ export { createWsClient } from './ws-client';
152
+ export type {
153
+ WsClientOptions,
154
+ WsClient,
155
+ WsGatewayClient,
156
+ WsEventListener,
157
+ WsClientEvent,
158
+ WsClientEventListeners,
159
+ TypedWsClient,
160
+ } from './ws-client.types';
161
+ export { WsConnectionState } from './ws-client.types';
package/src/module.ts CHANGED
@@ -23,6 +23,7 @@ import {
23
23
  registerControllerDependencies,
24
24
  } from './decorators';
25
25
  import { getServiceMetadata, getServiceTag } from './service';
26
+ import { isWebSocketGateway } from './ws-decorators';
26
27
 
27
28
  /**
28
29
  * OneBun Module implementation
@@ -331,14 +332,19 @@ export class OneBunModule implements Module {
331
332
  const controllerConstructor = controllerClass as new (...args: unknown[]) => Controller;
332
333
  const controller = new controllerConstructor(...dependencies);
333
334
 
334
- // Initialize controller with logger and config
335
- controller.initializeController(this.logger, this.config);
335
+ // Initialize controller with logger and config (skip for WebSocket gateways)
336
+ if (!isWebSocketGateway(controllerClass) && typeof controller.initializeController === 'function') {
337
+ controller.initializeController(this.logger, this.config);
338
+ }
336
339
 
337
340
  this.controllerInstances.set(controllerClass, controller);
338
341
 
339
342
  // Inject all services into controller (for legacy compatibility)
340
- for (const [tag, serviceInstance] of this.serviceInstances.entries()) {
341
- controller.setService(tag, serviceInstance);
343
+ // Skip for WebSocket gateways which don't have setService
344
+ if (!isWebSocketGateway(controllerClass) && typeof controller.setService === 'function') {
345
+ for (const [tag, serviceInstance] of this.serviceInstances.entries()) {
346
+ controller.setService(tag, serviceInstance);
347
+ }
342
348
  }
343
349
 
344
350
  if (paramTypes && paramTypes.length > 0) {
@@ -0,0 +1,502 @@
1
+ /**
2
+ * Redis Client Wrapper
3
+ *
4
+ * Unified Redis client for use across OneBun packages (cache, websocket, etc.)
5
+ * Uses Bun's built-in RedisClient (Bun v1.2.9+).
6
+ */
7
+
8
+ // Type for Bun's RedisClient - will be available at runtime
9
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
10
+ type BunRedisClient = any;
11
+
12
+ /**
13
+ * Options for Redis client
14
+ */
15
+ export interface RedisClientOptions {
16
+ /** Redis connection URL (redis://host:port or rediss://host:port for TLS) */
17
+ url: string;
18
+ /** Key prefix for all operations */
19
+ keyPrefix?: string;
20
+ /** Enable automatic reconnection */
21
+ reconnect?: boolean;
22
+ /** Enable TLS */
23
+ tls?: boolean;
24
+ /** Connection timeout in milliseconds */
25
+ connectTimeout?: number;
26
+ /** Command timeout in milliseconds */
27
+ commandTimeout?: number;
28
+ }
29
+
30
+ /**
31
+ * Subscription handler type
32
+ */
33
+ type SubscriptionHandler = (message: string, channel: string) => void;
34
+
35
+ /**
36
+ * Redis client wrapper with unified API
37
+ */
38
+ export class RedisClient {
39
+ private client: BunRedisClient | null = null;
40
+ private subscriptions = new Map<string, SubscriptionHandler[]>();
41
+ private subscriberClient: BunRedisClient | null = null;
42
+ private readonly options: RedisClientOptions;
43
+ private connected = false;
44
+
45
+ constructor(options: RedisClientOptions) {
46
+ this.options = {
47
+ keyPrefix: '',
48
+ reconnect: true,
49
+ ...options,
50
+ };
51
+ }
52
+
53
+ /**
54
+ * Connect to Redis
55
+ */
56
+ async connect(): Promise<void> {
57
+ if (this.connected && this.client) {
58
+ return;
59
+ }
60
+
61
+ try {
62
+ // Get Bun's RedisClient constructor
63
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
64
+ const BunGlobal = (globalThis as any).Bun;
65
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
66
+ const BunDirect = typeof Bun !== 'undefined' ? (Bun as any) : null;
67
+
68
+ // Try different access methods for RedisClient constructor
69
+ // eslint-disable-next-line @typescript-eslint/naming-convention
70
+ const BunRedisClient = BunGlobal?.RedisClient
71
+ || BunDirect?.RedisClient
72
+ || BunGlobal?.Redis;
73
+
74
+ if (!BunRedisClient) {
75
+ throw new Error('Bun.RedisClient is not available. Make sure you are using Bun runtime v1.2.9+');
76
+ }
77
+
78
+ // Create client with URL and options
79
+ this.client = new BunRedisClient(this.options.url, {
80
+ autoReconnect: this.options.reconnect ?? true,
81
+ connectionTimeout: this.options.connectTimeout,
82
+ enableAutoPipelining: true,
83
+ tls: this.options.tls,
84
+ });
85
+
86
+ // Connect to Redis server
87
+ if (this.client.connect) {
88
+ await this.client.connect();
89
+ }
90
+
91
+ this.connected = true;
92
+ } catch (error) {
93
+ this.connected = false;
94
+ throw new Error(`Failed to connect to Redis: ${error}`);
95
+ }
96
+ }
97
+
98
+ /**
99
+ * Disconnect from Redis
100
+ */
101
+ async disconnect(): Promise<void> {
102
+ if (this.subscriberClient) {
103
+ // Unsubscribe from all channels
104
+ for (const channel of this.subscriptions.keys()) {
105
+ try {
106
+ await this.subscriberClient.unsubscribe(channel);
107
+ } catch {
108
+ // Ignore errors during cleanup
109
+ }
110
+ }
111
+ this.subscriptions.clear();
112
+ this.subscriberClient = null;
113
+ }
114
+
115
+ if (this.client) {
116
+ try {
117
+ await this.client.quit();
118
+ } catch {
119
+ // Ignore errors during cleanup
120
+ }
121
+ this.client = null;
122
+ this.connected = false;
123
+ }
124
+ }
125
+
126
+ /**
127
+ * Check if connected
128
+ */
129
+ isConnected(): boolean {
130
+ return this.connected && this.client !== null;
131
+ }
132
+
133
+ /**
134
+ * Get prefixed key
135
+ */
136
+ private prefixKey(key: string): string {
137
+ return this.options.keyPrefix ? `${this.options.keyPrefix}${key}` : key;
138
+ }
139
+
140
+ /**
141
+ * Ensure client is connected
142
+ */
143
+ private ensureConnected(): BunRedisClient {
144
+ if (!this.client || !this.connected) {
145
+ throw new Error('Redis client not connected. Call connect() first.');
146
+ }
147
+
148
+ return this.client;
149
+ }
150
+
151
+ // ============================================================================
152
+ // Basic Operations
153
+ // ============================================================================
154
+
155
+ /**
156
+ * Get a value by key
157
+ */
158
+ async get(key: string): Promise<string | null> {
159
+ const client = this.ensureConnected();
160
+ const result = await client.get(this.prefixKey(key));
161
+
162
+ return result ?? null;
163
+ }
164
+
165
+ /**
166
+ * Set a value with optional TTL
167
+ */
168
+ async set(key: string, value: string, ttlMs?: number): Promise<void> {
169
+ const client = this.ensureConnected();
170
+ const prefixedKey = this.prefixKey(key);
171
+
172
+ if (ttlMs !== undefined && ttlMs > 0) {
173
+ // Use raw SET command with PX option for atomic set with TTL
174
+ await client.send('SET', [prefixedKey, value, 'PX', String(ttlMs)]);
175
+ } else {
176
+ await client.set(prefixedKey, value);
177
+ }
178
+ }
179
+
180
+ /**
181
+ * Delete a key
182
+ */
183
+ async del(key: string): Promise<void> {
184
+ const client = this.ensureConnected();
185
+ await client.del(this.prefixKey(key));
186
+ }
187
+
188
+ /**
189
+ * Check if key exists
190
+ */
191
+ async exists(key: string): Promise<boolean> {
192
+ const client = this.ensureConnected();
193
+ const result = await client.exists(this.prefixKey(key));
194
+
195
+ return result > 0;
196
+ }
197
+
198
+ /**
199
+ * Get keys matching pattern
200
+ */
201
+ async keys(pattern: string): Promise<string[]> {
202
+ const client = this.ensureConnected();
203
+ const prefixedPattern = this.prefixKey(pattern);
204
+ const result = await client.keys(prefixedPattern);
205
+
206
+ // Remove prefix from results
207
+ const prefix = this.options.keyPrefix || '';
208
+
209
+ return result.map((k: string) => k.startsWith(prefix) ? k.substring(prefix.length) : k);
210
+ }
211
+
212
+ /**
213
+ * Set TTL on existing key
214
+ */
215
+ async expire(key: string, ttlMs: number): Promise<boolean> {
216
+ const client = this.ensureConnected();
217
+ const result = await client.pexpire(this.prefixKey(key), ttlMs);
218
+
219
+ return result === 1;
220
+ }
221
+
222
+ /**
223
+ * Get TTL of a key in milliseconds
224
+ */
225
+ async ttl(key: string): Promise<number> {
226
+ const client = this.ensureConnected();
227
+ const result = await client.pttl(this.prefixKey(key));
228
+
229
+ return result;
230
+ }
231
+
232
+ // ============================================================================
233
+ // Batch Operations
234
+ // ============================================================================
235
+
236
+ /**
237
+ * Get multiple values
238
+ */
239
+ async mget(keys: string[]): Promise<(string | null)[]> {
240
+ if (keys.length === 0) {
241
+ return [];
242
+ }
243
+
244
+ const client = this.ensureConnected();
245
+ const prefixedKeys = keys.map((k) => this.prefixKey(k));
246
+ const results = await client.mget(...prefixedKeys);
247
+
248
+ return results.map((r: string | null) => r ?? null);
249
+ }
250
+
251
+ /**
252
+ * Set multiple values with optional TTL
253
+ */
254
+ async mset(entries: Array<{ key: string; value: string; ttlMs?: number }>): Promise<void> {
255
+ if (entries.length === 0) {
256
+ return;
257
+ }
258
+
259
+ const client = this.ensureConnected();
260
+
261
+ // Group entries by TTL
262
+ const noTtl: [string, string][] = [];
263
+ const withTtl: Array<{ key: string; value: string; ttlMs: number }> = [];
264
+
265
+ for (const entry of entries) {
266
+ if (entry.ttlMs !== undefined && entry.ttlMs > 0) {
267
+ withTtl.push({ key: this.prefixKey(entry.key), value: entry.value, ttlMs: entry.ttlMs });
268
+ } else {
269
+ noTtl.push([this.prefixKey(entry.key), entry.value]);
270
+ }
271
+ }
272
+
273
+ // Set entries without TTL using MSET
274
+ if (noTtl.length > 0) {
275
+ const flat = noTtl.flat();
276
+ await client.mset(...flat);
277
+ }
278
+
279
+ // Set entries with TTL individually using raw SET command
280
+ for (const entry of withTtl) {
281
+ await client.send('SET', [entry.key, entry.value, 'PX', String(entry.ttlMs)]);
282
+ }
283
+ }
284
+
285
+ // ============================================================================
286
+ // Hash Operations
287
+ // ============================================================================
288
+
289
+ /**
290
+ * Set a hash field
291
+ */
292
+ async hset(key: string, field: string, value: string): Promise<void> {
293
+ const client = this.ensureConnected();
294
+ await client.hset(this.prefixKey(key), field, value);
295
+ }
296
+
297
+ /**
298
+ * Get a hash field
299
+ */
300
+ async hget(key: string, field: string): Promise<string | null> {
301
+ const client = this.ensureConnected();
302
+ const result = await client.hget(this.prefixKey(key), field);
303
+
304
+ return result ?? null;
305
+ }
306
+
307
+ /**
308
+ * Get all hash fields
309
+ */
310
+ async hgetall(key: string): Promise<Record<string, string>> {
311
+ const client = this.ensureConnected();
312
+ const result = await client.hgetall(this.prefixKey(key));
313
+
314
+ return result || {};
315
+ }
316
+
317
+ /**
318
+ * Delete a hash field
319
+ */
320
+ async hdel(key: string, field: string): Promise<void> {
321
+ const client = this.ensureConnected();
322
+ await client.hdel(this.prefixKey(key), field);
323
+ }
324
+
325
+ /**
326
+ * Set multiple hash fields
327
+ */
328
+ async hmset(key: string, data: Record<string, string>): Promise<void> {
329
+ const client = this.ensureConnected();
330
+ const entries = Object.entries(data).flat();
331
+ if (entries.length > 0) {
332
+ await client.hset(this.prefixKey(key), ...entries);
333
+ }
334
+ }
335
+
336
+ // ============================================================================
337
+ // Set Operations
338
+ // ============================================================================
339
+
340
+ /**
341
+ * Add members to a set
342
+ */
343
+ async sadd(key: string, ...members: string[]): Promise<void> {
344
+ if (members.length === 0) {
345
+ return;
346
+ }
347
+
348
+ const client = this.ensureConnected();
349
+ await client.sadd(this.prefixKey(key), ...members);
350
+ }
351
+
352
+ /**
353
+ * Remove members from a set
354
+ */
355
+ async srem(key: string, ...members: string[]): Promise<void> {
356
+ if (members.length === 0) {
357
+ return;
358
+ }
359
+
360
+ const client = this.ensureConnected();
361
+ await client.srem(this.prefixKey(key), ...members);
362
+ }
363
+
364
+ /**
365
+ * Get all members of a set
366
+ */
367
+ async smembers(key: string): Promise<string[]> {
368
+ const client = this.ensureConnected();
369
+ const result = await client.smembers(this.prefixKey(key));
370
+
371
+ return result || [];
372
+ }
373
+
374
+ /**
375
+ * Check if member exists in set
376
+ */
377
+ async sismember(key: string, member: string): Promise<boolean> {
378
+ const client = this.ensureConnected();
379
+ const result = await client.sismember(this.prefixKey(key), member);
380
+
381
+ return result === 1;
382
+ }
383
+
384
+ /**
385
+ * Get set size
386
+ */
387
+ async scard(key: string): Promise<number> {
388
+ const client = this.ensureConnected();
389
+ const result = await client.scard(this.prefixKey(key));
390
+
391
+ return result || 0;
392
+ }
393
+
394
+ // ============================================================================
395
+ // Pub/Sub Operations
396
+ // ============================================================================
397
+
398
+ /**
399
+ * Publish a message to a channel
400
+ */
401
+ async publish(channel: string, message: string): Promise<void> {
402
+ const client = this.ensureConnected();
403
+ await client.publish(this.prefixKey(channel), message);
404
+ }
405
+
406
+ /**
407
+ * Subscribe to a channel
408
+ */
409
+ async subscribe(channel: string, handler: SubscriptionHandler): Promise<void> {
410
+ const prefixedChannel = this.prefixKey(channel);
411
+
412
+ // Track handler
413
+ const handlers = this.subscriptions.get(prefixedChannel) || [];
414
+ handlers.push(handler);
415
+ this.subscriptions.set(prefixedChannel, handlers);
416
+
417
+ // Create subscriber client if needed
418
+ if (!this.subscriberClient) {
419
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
420
+ const BunGlobal = (globalThis as any).Bun;
421
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
422
+ const BunDirect = typeof Bun !== 'undefined' ? (Bun as any) : null;
423
+
424
+ // eslint-disable-next-line @typescript-eslint/naming-convention
425
+ const BunRedisClient = BunGlobal?.RedisClient
426
+ || BunDirect?.RedisClient
427
+ || BunGlobal?.Redis;
428
+
429
+ if (!BunRedisClient) {
430
+ throw new Error('Bun.RedisClient is not available');
431
+ }
432
+
433
+ this.subscriberClient = new BunRedisClient(this.options.url, {
434
+ autoReconnect: this.options.reconnect ?? true,
435
+ connectionTimeout: this.options.connectTimeout,
436
+ tls: this.options.tls,
437
+ });
438
+
439
+ // Connect subscriber client
440
+ if (this.subscriberClient.connect) {
441
+ await this.subscriberClient.connect();
442
+ }
443
+ }
444
+
445
+ // Subscribe using Bun's subscribe method
446
+ await this.subscriberClient.subscribe(prefixedChannel, (message: string) => {
447
+ const channelHandlers = this.subscriptions.get(prefixedChannel);
448
+ if (channelHandlers) {
449
+ for (const h of channelHandlers) {
450
+ try {
451
+ h(message, channel);
452
+ } catch {
453
+ // Ignore handler errors
454
+ }
455
+ }
456
+ }
457
+ });
458
+ }
459
+
460
+ /**
461
+ * Unsubscribe from a channel
462
+ */
463
+ async unsubscribe(channel: string): Promise<void> {
464
+ const prefixedChannel = this.prefixKey(channel);
465
+ this.subscriptions.delete(prefixedChannel);
466
+
467
+ if (this.subscriberClient) {
468
+ try {
469
+ await this.subscriberClient.unsubscribe(prefixedChannel);
470
+ } catch {
471
+ // Ignore errors
472
+ }
473
+ }
474
+ }
475
+
476
+ // ============================================================================
477
+ // Raw Client Access
478
+ // ============================================================================
479
+
480
+ /**
481
+ * Get the underlying Bun Redis client
482
+ */
483
+ getClient(): BunRedisClient | null {
484
+ return this.client;
485
+ }
486
+
487
+ /**
488
+ * Execute a raw command
489
+ */
490
+ async raw<T = unknown>(command: string, ...args: string[]): Promise<T> {
491
+ const client = this.ensureConnected();
492
+
493
+ return await client[command](...args);
494
+ }
495
+ }
496
+
497
+ /**
498
+ * Create a new Redis client
499
+ */
500
+ export function createRedisClient(options: RedisClientOptions): RedisClient {
501
+ return new RedisClient(options);
502
+ }