@onebun/cache 0.1.1 → 0.1.3
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 +49 -7
- package/package.json +3 -3
- package/src/index.ts +3 -6
- package/src/redis-cache.ts +125 -89
- package/src/types.ts +13 -0
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Caching module for OneBun framework - in-memory and Redis support",
|
|
5
5
|
"license": "LGPL-3.0",
|
|
6
6
|
"author": "RemRyahirev",
|
|
@@ -37,8 +37,8 @@
|
|
|
37
37
|
"dev": "bun run --watch src/index.ts"
|
|
38
38
|
},
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@onebun/core": "workspace
|
|
41
|
-
"@onebun/envs": "workspace
|
|
40
|
+
"@onebun/core": "workspace:^",
|
|
41
|
+
"@onebun/envs": "workspace:^",
|
|
42
42
|
"effect": "^3.13.10"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
package/src/index.ts
CHANGED
|
@@ -37,12 +37,9 @@ export {
|
|
|
37
37
|
RedisCache,
|
|
38
38
|
} from './redis-cache';
|
|
39
39
|
|
|
40
|
-
// Redis
|
|
41
|
-
export type {
|
|
42
|
-
|
|
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';
|
package/src/redis-cache.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Redis cache implementation using
|
|
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
|
|
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
|
|
41
|
+
* @param optionsOrClient - Redis cache configuration options or existing RedisClient
|
|
31
42
|
*/
|
|
32
|
-
constructor(
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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
|
-
|
|
104
|
+
redisUrl += `:${password}@`;
|
|
52
105
|
}
|
|
53
|
-
|
|
54
|
-
url += `${host}:${port}`;
|
|
55
|
-
|
|
106
|
+
redisUrl += `${host}:${port}`;
|
|
56
107
|
if (database) {
|
|
57
|
-
|
|
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
|
|
137
|
-
|
|
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
|
|
182
|
+
const existed = await this.client.exists(fullKey);
|
|
183
|
+
await this.client.del(fullKey);
|
|
159
184
|
|
|
160
|
-
return
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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.
|
|
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
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
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
|
|
381
|
+
* @param optionsOrClient - Redis cache configuration options or existing RedisClient
|
|
346
382
|
* @returns RedisCache instance
|
|
347
383
|
*/
|
|
348
|
-
export function createRedisCache(
|
|
349
|
-
return new RedisCache(
|
|
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
|
/**
|