@onebun/cache 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/README.md CHANGED
@@ -261,6 +261,43 @@ await cache.close();
261
261
  - ✅ TLS support
262
262
  - ✅ Fully typed responses
263
263
 
264
+ ### Shared Redis Connection
265
+
266
+ For applications using both cache and WebSocket (or other Redis-based features), you can share a single Redis connection using `SharedRedisProvider` from `@onebun/core`:
267
+
268
+ ```typescript
269
+ import { SharedRedisProvider } from '@onebun/core';
270
+ import { createRedisCache, RedisCache } from '@onebun/cache';
271
+
272
+ // Configure shared Redis connection at app startup
273
+ SharedRedisProvider.configure({
274
+ url: 'redis://localhost:6379',
275
+ keyPrefix: 'myapp:',
276
+ });
277
+
278
+ // Option 1: Use shared client via options
279
+ const cache = createRedisCache({
280
+ useSharedClient: true,
281
+ defaultTtl: 60000,
282
+ });
283
+ await cache.connect(); // Will use SharedRedisProvider
284
+
285
+ // Option 2: Pass existing RedisClient directly
286
+ const sharedClient = await SharedRedisProvider.getClient();
287
+ const cache = new RedisCache(sharedClient);
288
+
289
+ // Check if using shared client
290
+ console.log(cache.isUsingSharedClient()); // true
291
+ ```
292
+
293
+ **Benefits of shared connection:**
294
+ - ✅ Single connection pool for cache and WebSocket
295
+ - ✅ Reduced memory footprint
296
+ - ✅ Consistent key prefixing
297
+ - ✅ Centralized connection management
298
+
299
+ **Note:** When using shared client, the cache will NOT disconnect the client when `close()` is called, as other parts of your application may still be using it.
300
+
264
301
  ### TTL Examples
265
302
 
266
303
  ```typescript
@@ -434,6 +471,12 @@ interface RedisCacheOptions extends CacheOptions {
434
471
 
435
472
  // Key prefix for all cache keys (default: 'onebun:cache:')
436
473
  keyPrefix?: string;
474
+
475
+ // Use shared Redis client from SharedRedisProvider (default: false)
476
+ useSharedClient?: boolean;
477
+
478
+ // Redis URL (alternative to host/port/password)
479
+ url?: string;
437
480
  }
438
481
  ```
439
482
 
@@ -463,15 +506,14 @@ interface CacheSetOptions {
463
506
 
464
507
  ## Implementation Details
465
508
 
466
- ### Redis Client Types
467
-
468
- This package includes minimal type definitions for Bun's `RedisClient` (in `bun-redis-types.ts`) until official types are available in `bun-types`. These types provide:
509
+ ### Redis Client Integration
469
510
 
470
- - Type safety for Redis operations
471
- - IntelliSense support in IDEs
472
- - Compatibility with TypeScript strict mode
511
+ This package uses `RedisClient` from `@onebun/core`, which wraps Bun's native Redis implementation with:
473
512
 
474
- The types will be automatically replaced once official types are published in future versions of `bun-types`.
513
+ - Unified API for cache and WebSocket features
514
+ - Automatic key prefixing
515
+ - Connection management with auto-reconnect
516
+ - Pub/Sub support for distributed features
475
517
 
476
518
  ### Auto-Pipelining
477
519
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/cache",
3
- "version": "0.1.1",
3
+ "version": "0.1.2",
4
4
  "description": "Caching module for OneBun framework - in-memory and Redis support",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
package/src/index.ts CHANGED
@@ -37,12 +37,9 @@ export {
37
37
  RedisCache,
38
38
  } from './redis-cache';
39
39
 
40
- // Redis type definitions (until official types are available)
41
- export type {
42
- RedisClient,
43
- RedisClientOptions,
44
- } from './bun-redis-types';
45
- export { hasRedisClient } from './bun-redis-types';
40
+ // Re-export Redis client from @onebun/core for convenience
41
+ export type { RedisClient, RedisClientOptions } from '@onebun/core';
42
+ export { SharedRedisProvider, createRedisClient } from '@onebun/core';
46
43
 
47
44
  // NestJS-like module and service for use with @Module decorator (recommended for new applications)
48
45
  export { CacheModule } from './cache.module';
@@ -1,11 +1,13 @@
1
1
  /**
2
- * Redis cache implementation using Bun's native RedisClient
2
+ * Redis cache implementation using @onebun/core RedisClient
3
+ *
4
+ * Supports two modes:
5
+ * 1. Standalone - creates its own Redis connection
6
+ * 2. Shared - uses SharedRedisProvider from @onebun/core
3
7
  *
4
- * Requires Bun v1.2.9 or later
5
8
  * @see https://bun.com/docs/api/redis
6
9
  */
7
10
 
8
- import type { RedisClient } from './bun-redis-types';
9
11
  import type {
10
12
  CacheService,
11
13
  CacheSetOptions,
@@ -13,10 +15,17 @@ import type {
13
15
  RedisCacheOptions,
14
16
  } from './types';
15
17
 
18
+ import type { RedisClientOptions } from '@onebun/core';
19
+ import {
20
+ RedisClient,
21
+ SharedRedisProvider,
22
+ createRedisClient,
23
+ } from '@onebun/core';
24
+
16
25
  import { DEFAULT_REDIS_CACHE_OPTIONS } from './types';
17
26
 
18
27
  /**
19
- * Redis-based cache implementation using Bun's native RedisClient
28
+ * Redis-based cache implementation using @onebun/core RedisClient
20
29
  * Implements CacheService interface with Redis as the backing store
21
30
  */
22
31
  export class RedisCache implements CacheService {
@@ -24,68 +33,88 @@ export class RedisCache implements CacheService {
24
33
  private readonly options: Required<RedisCacheOptions>;
25
34
  private hits = 0;
26
35
  private misses = 0;
36
+ private ownsClient = false;
37
+ private useShared = false;
27
38
 
28
39
  /**
29
40
  * Create a new Redis cache instance
30
- * @param options - Redis cache configuration options
41
+ * @param optionsOrClient - Redis cache configuration options or existing RedisClient
31
42
  */
32
- constructor(options: RedisCacheOptions = {}) {
33
- this.options = {
34
- ...DEFAULT_REDIS_CACHE_OPTIONS,
35
- ...options,
36
- } as Required<RedisCacheOptions>;
43
+ constructor(optionsOrClient: RedisCacheOptions | RedisClient = {}) {
44
+ if (optionsOrClient instanceof RedisClient) {
45
+ // Use provided client
46
+ this.client = optionsOrClient;
47
+ this.ownsClient = false;
48
+ this.options = {
49
+ ...DEFAULT_REDIS_CACHE_OPTIONS,
50
+ keyPrefix: '', // Client already has prefix configured
51
+ } as Required<RedisCacheOptions>;
52
+ } else {
53
+ // Configure from options
54
+ this.options = {
55
+ ...DEFAULT_REDIS_CACHE_OPTIONS,
56
+ ...optionsOrClient,
57
+ } as Required<RedisCacheOptions>;
58
+ this.useShared = optionsOrClient.useSharedClient ?? false;
59
+ }
37
60
  }
38
61
 
39
62
  /**
40
63
  * Connect to Redis
41
64
  */
42
65
  async connect(): Promise<void> {
66
+ // Skip if already have a client
67
+ if (this.client?.isConnected()) {
68
+ return;
69
+ }
70
+
43
71
  try {
44
- // Build Redis URL
45
- const {
46
- host, port, password, database,
47
- } = this.options;
48
- let url = 'redis://';
72
+ if (this.useShared) {
73
+ // Use shared client from core
74
+ this.client = await SharedRedisProvider.getClient();
75
+ this.ownsClient = false;
76
+ } else if (!this.client) {
77
+ // Create new client
78
+ const clientOptions = this.buildClientOptions();
79
+ this.client = createRedisClient(clientOptions);
80
+ this.ownsClient = true;
81
+ await this.client.connect();
82
+ } else if (!this.client.isConnected()) {
83
+ // Reconnect existing client (passed via constructor)
84
+ await this.client.connect();
85
+ }
86
+ } catch (error) {
87
+ throw new Error(`Failed to connect to Redis: ${error}`);
88
+ }
89
+ }
49
90
 
91
+ /**
92
+ * Build client options from cache options
93
+ */
94
+ private buildClientOptions(): RedisClientOptions {
95
+ const {
96
+ host, port, password, database, url, keyPrefix, connectTimeout,
97
+ } = this.options;
98
+
99
+ // Use URL if provided, otherwise build from components
100
+ let redisUrl = url;
101
+ if (!redisUrl) {
102
+ redisUrl = 'redis://';
50
103
  if (password) {
51
- url += `:${password}@`;
104
+ redisUrl += `:${password}@`;
52
105
  }
53
-
54
- url += `${host}:${port}`;
55
-
106
+ redisUrl += `${host}:${port}`;
56
107
  if (database) {
57
- url += `/${database}`;
108
+ redisUrl += `/${database}`;
58
109
  }
59
-
60
- // Create Bun's native RedisClient
61
- // Type assertion needed until official types are available in bun-types
62
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
63
- const BunGlobal = (globalThis as any).Bun;
64
-
65
- // Try alternative access methods if BunGlobal doesn't have RedisClient
66
- // Bun is available at runtime but may not be in TypeScript types
67
- // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/naming-convention
68
- const BunDirect = typeof Bun !== 'undefined' ? (Bun as any) : null;
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('RedisClient is not available. Make sure you are using Bun runtime.');
76
- }
77
-
78
- this.client = new BunRedisClient(url, {
79
- connectionTimeout: this.options.connectTimeout,
80
- enableAutoPipelining: true,
81
- autoReconnect: true,
82
- }) as RedisClient;
83
-
84
- // Connect to Redis
85
- await this.client.connect();
86
- } catch (error) {
87
- throw new Error(`Failed to connect to Redis: ${error}`);
88
110
  }
111
+
112
+ return {
113
+ url: redisUrl,
114
+ keyPrefix,
115
+ connectTimeout,
116
+ reconnect: true,
117
+ };
89
118
  }
90
119
 
91
120
  /**
@@ -133,13 +162,8 @@ export class RedisCache implements CacheService {
133
162
  const serialized = JSON.stringify(value);
134
163
  const ttl = options.ttl ?? this.options.defaultTtl;
135
164
 
136
- // Set value with TTL atomically using PX (milliseconds)
137
- if (ttl && ttl > 0) {
138
- // Use send command to set value with TTL atomically
139
- await this.client.send('SET', [fullKey, serialized, 'PX', String(ttl)]);
140
- } else {
141
- await this.client.set(fullKey, serialized);
142
- }
165
+ // Set value with TTL
166
+ await this.client.set(fullKey, serialized, ttl);
143
167
  } catch (error) {
144
168
  throw new Error(`Redis cache set error for key ${key}: ${error}`);
145
169
  }
@@ -155,9 +179,10 @@ export class RedisCache implements CacheService {
155
179
 
156
180
  try {
157
181
  const fullKey = this.getFullKey(key);
158
- const result = await this.client.del(fullKey);
182
+ const existed = await this.client.exists(fullKey);
183
+ await this.client.del(fullKey);
159
184
 
160
- return result > 0;
185
+ return existed;
161
186
  } catch {
162
187
  return false;
163
188
  }
@@ -173,10 +198,8 @@ export class RedisCache implements CacheService {
173
198
 
174
199
  try {
175
200
  const fullKey = this.getFullKey(key);
176
- // Bun's exists returns boolean instead of number
177
- const result = await this.client.exists(fullKey);
178
201
 
179
- return Boolean(result);
202
+ return await this.client.exists(fullKey);
180
203
  } catch {
181
204
  return false;
182
205
  }
@@ -193,12 +216,12 @@ export class RedisCache implements CacheService {
193
216
 
194
217
  try {
195
218
  const pattern = `${this.options.keyPrefix}*`;
196
- // Use SCAN instead of KEYS for better performance in production
197
- // For now, using send() method for KEYS command
198
- const keys = await this.client.send('KEYS', [pattern]);
219
+ const keys = await this.client.keys(pattern);
199
220
 
200
221
  if (keys && Array.isArray(keys) && keys.length > 0) {
201
- await this.client.del(...keys);
222
+ for (const key of keys) {
223
+ await this.client.del(key);
224
+ }
202
225
  }
203
226
 
204
227
  this.resetStats();
@@ -217,13 +240,7 @@ export class RedisCache implements CacheService {
217
240
 
218
241
  try {
219
242
  const fullKeys = keys.map((key) => this.getFullKey(key));
220
- const values = await this.client.send('MGET', fullKeys);
221
-
222
- if (!Array.isArray(values)) {
223
- this.misses += keys.length;
224
-
225
- return new Array(keys.length).fill(undefined);
226
- }
243
+ const values = await this.client.mget(fullKeys);
227
244
 
228
245
  return values.map((value) => {
229
246
  if (value === null || value === undefined) {
@@ -234,7 +251,7 @@ export class RedisCache implements CacheService {
234
251
 
235
252
  this.hits++;
236
253
  try {
237
- return JSON.parse(value as string) as T;
254
+ return JSON.parse(value) as T;
238
255
  } catch {
239
256
  this.misses++;
240
257
 
@@ -260,11 +277,13 @@ export class RedisCache implements CacheService {
260
277
  }
261
278
 
262
279
  try {
263
- // Bun's RedisClient has auto-pipelining enabled by default
264
- // We can execute commands sequentially and they will be pipelined automatically
265
- for (const { key, value, options } of entries) {
266
- await this.set(key, value, options);
267
- }
280
+ const msetEntries = entries.map(({ key, value, options }) => ({
281
+ key: this.getFullKey(key),
282
+ value: JSON.stringify(value),
283
+ ttlMs: options?.ttl ?? this.options.defaultTtl,
284
+ }));
285
+
286
+ await this.client.mset(msetEntries);
268
287
  } catch (error) {
269
288
  throw new Error(`Redis cache mset error: ${error}`);
270
289
  }
@@ -280,7 +299,7 @@ export class RedisCache implements CacheService {
280
299
 
281
300
  try {
282
301
  const pattern = `${this.options.keyPrefix}*`;
283
- const keys = await this.client.send('KEYS', [pattern]);
302
+ const keys = await this.client.keys(pattern);
284
303
  const totalRequests = this.hits + this.misses;
285
304
  const hitRate = totalRequests > 0 ? this.hits / totalRequests : 0;
286
305
 
@@ -302,25 +321,35 @@ export class RedisCache implements CacheService {
302
321
 
303
322
  /**
304
323
  * Close cache connection and cleanup resources
324
+ * Note: Only disconnects if this instance owns the client
305
325
  */
306
326
  async close(): Promise<void> {
307
327
  if (!this.client) {
308
328
  return;
309
329
  }
310
330
 
311
- try {
312
- this.client.close();
313
- this.client = null;
314
- } catch {
315
- // Force disconnect if graceful close fails
316
- this.client = null;
331
+ // Only disconnect if we own the client (not shared)
332
+ if (this.ownsClient) {
333
+ try {
334
+ await this.client.disconnect();
335
+ } catch {
336
+ // Ignore errors during cleanup
337
+ }
317
338
  }
339
+
340
+ this.client = null;
318
341
  }
319
342
 
320
343
  /**
321
344
  * Get full key with prefix
345
+ * Note: When using shared client, prefix is already applied
322
346
  */
323
347
  private getFullKey(key: string): string {
348
+ // If client is shared or passed in, don't add prefix (client has its own)
349
+ if (!this.ownsClient || this.useShared) {
350
+ return key;
351
+ }
352
+
324
353
  return `${this.options.keyPrefix}${key}`;
325
354
  }
326
355
 
@@ -338,13 +367,20 @@ export class RedisCache implements CacheService {
338
367
  getClient(): RedisClient | null {
339
368
  return this.client;
340
369
  }
370
+
371
+ /**
372
+ * Check if using shared client
373
+ */
374
+ isUsingSharedClient(): boolean {
375
+ return this.useShared || !this.ownsClient;
376
+ }
341
377
  }
342
378
 
343
379
  /**
344
380
  * Create a new Redis cache instance
345
- * @param options - Redis cache configuration options
381
+ * @param optionsOrClient - Redis cache configuration options or existing RedisClient
346
382
  * @returns RedisCache instance
347
383
  */
348
- export function createRedisCache(options: RedisCacheOptions = {}): RedisCache {
349
- return new RedisCache(options);
350
- }
384
+ export function createRedisCache(optionsOrClient: RedisCacheOptions | RedisClient = {}): RedisCache {
385
+ return new RedisCache(optionsOrClient);
386
+ }
package/src/types.ts CHANGED
@@ -196,6 +196,19 @@ export interface RedisCacheOptions extends CacheOptions {
196
196
  * @defaultValue 'onebun:cache:'
197
197
  */
198
198
  keyPrefix?: string;
199
+
200
+ /**
201
+ * Use shared Redis client from @onebun/core SharedRedisProvider
202
+ * When true, expects SharedRedisProvider to be configured
203
+ * @defaultValue false
204
+ */
205
+ useSharedClient?: boolean;
206
+
207
+ /**
208
+ * Redis connection URL (alternative to host/port/password)
209
+ * When provided, takes precedence over host/port/password
210
+ */
211
+ url?: string;
199
212
  }
200
213
 
201
214
  /**