Skip to content

Commit 6f939c2

Browse files
committed
feat: implement Redis-based storage for rate limiting, mutex, and semaphore utilities
1 parent 8b8cf9f commit 6f939c2

File tree

9 files changed

+546
-80
lines changed

9 files changed

+546
-80
lines changed

apps/website/docs/guide/14-useful-utilities/01-ratelimit.mdx

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -74,26 +74,27 @@ By default, rate limiters store data in memory. If you're running multiple serve
7474

7575
```typescript
7676
import { RateLimiter, RateLimitStorage } from 'commandkit/ratelimit';
77+
import { RedisRateLimitStorage } from '@commandkit/redis';
78+
import { Redis } from 'ioredis';
7779

78-
// Example of how you might use Redis for rate limiting
79-
class RedisRateLimitStorage implements RateLimitStorage {
80-
async get(key: string): Promise<number> {
81-
// Get the current count from Redis
82-
return (await redis.get(key)) || 0;
83-
}
84-
85-
async set(key: string, value: number): Promise<void> {
86-
// Store the count in Redis
87-
await redis.set(key, value);
88-
}
89-
90-
async delete(key: string): Promise<void> {
91-
// Remove the count from Redis
92-
await redis.del(key);
93-
}
94-
}
80+
// Create Redis client
81+
const redis = new Redis();
82+
83+
// Use Redis-based rate limit storage
84+
const limiter = new RateLimiter(10, 60000, new RedisRateLimitStorage(redis));
85+
```
86+
87+
You can also use the convenience function:
9588

96-
const limiter = new RateLimiter(10, 60000, new RedisRateLimitStorage());
89+
```typescript
90+
import { createRateLimiter } from 'commandkit/ratelimit';
91+
import { RedisRateLimitStorage } from '@commandkit/redis';
92+
93+
const limiter = createRateLimiter({
94+
maxRequests: 10,
95+
interval: 60000,
96+
storage: new RedisRateLimitStorage(redis),
97+
});
9798
```
9899

99100
## Default Settings

apps/website/docs/guide/14-useful-utilities/02-mutex.mdx

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -95,30 +95,28 @@ By default, mutexes store lock information in memory. If you're running multiple
9595

9696
```typescript
9797
import { Mutex, MutexStorage } from 'commandkit/mutex';
98+
import { RedisMutexStorage } from '@commandkit/redis';
99+
import { Redis } from 'ioredis';
98100

99-
// Example of how you might use Redis for mutex locks
100-
class RedisMutexStorage implements MutexStorage {
101-
async acquire(
102-
key: string,
103-
timeout?: number,
104-
signal?: AbortSignal,
105-
): Promise<boolean> {
106-
// Try to acquire a lock in Redis
107-
// Return true if successful, false if already locked
108-
}
109-
110-
async release(key: string): Promise<void> {
111-
// Release the lock in Redis
112-
}
113-
114-
async isLocked(key: string): Promise<boolean> {
115-
// Check if a lock exists in Redis
116-
}
117-
}
101+
// Create Redis client
102+
const redis = new Redis();
118103

104+
// Use Redis-based mutex storage
119105
const mutex = new Mutex({
120106
timeout: 30000,
121-
storage: new RedisMutexStorage(),
107+
storage: new RedisMutexStorage(redis),
108+
});
109+
```
110+
111+
You can also use the convenience function:
112+
113+
```typescript
114+
import { createMutex } from 'commandkit/mutex';
115+
import { RedisMutexStorage } from '@commandkit/redis';
116+
117+
const mutex = createMutex({
118+
timeout: 60000,
119+
storage: new RedisMutexStorage(redis),
122120
});
123121
```
124122

apps/website/docs/guide/14-useful-utilities/03-semaphore.mdx

Lines changed: 19 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -113,35 +113,30 @@ By default, semaphores store permit information in memory. If you're running mul
113113

114114
```typescript
115115
import { Semaphore, SemaphoreStorage } from 'commandkit/semaphore';
116+
import { RedisSemaphoreStorage } from '@commandkit/redis';
117+
import { Redis } from 'ioredis';
116118

117-
// Example of how you might use Redis for semaphore permits
118-
class RedisSemaphoreStorage implements SemaphoreStorage {
119-
async acquire(
120-
key: string,
121-
timeout?: number,
122-
signal?: AbortSignal,
123-
): Promise<boolean> {
124-
// Try to acquire a permit in Redis
125-
// Return true if successful, false if no permits available
126-
}
127-
128-
async release(key: string): Promise<void> {
129-
// Release a permit in Redis
130-
}
131-
132-
async getAvailablePermits(key: string): Promise<number> {
133-
// Get the number of available permits from Redis
134-
}
135-
136-
async getTotalPermits(key: string): Promise<number> {
137-
// Get the total number of permits from Redis
138-
}
139-
}
119+
// Create Redis client
120+
const redis = new Redis();
140121

122+
// Use Redis-based semaphore storage
141123
const semaphore = new Semaphore({
142124
permits: 10,
143125
timeout: 30000,
144-
storage: new RedisSemaphoreStorage(),
126+
storage: new RedisSemaphoreStorage(redis),
127+
});
128+
```
129+
130+
You can also use the convenience function:
131+
132+
```typescript
133+
import { createSemaphore } from 'commandkit/semaphore';
134+
import { RedisSemaphoreStorage } from '@commandkit/redis';
135+
136+
const semaphore = createSemaphore({
137+
permits: 5,
138+
timeout: 60000,
139+
storage: new RedisSemaphoreStorage(redis),
145140
});
146141
```
147142

apps/website/docs/guide/14-useful-utilities/index.mdx

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -97,27 +97,32 @@ const result = await queue.add(async () => await task());
9797

9898
### Using External Storage
9999

100-
By default, these utilities store their data in memory. But if you're running multiple instances of your application (like on different servers), you'll want to use external storage like Redis so they can share information.
100+
All utilities support custom storage implementations for distributed environments:
101101

102102
```typescript
103-
// Example of how you might use Redis for rate limiting
104-
class RedisRateLimitStorage implements RateLimitStorage {
105-
async get(key: string): Promise<number> {
106-
// Get the current count from Redis
107-
return (await redis.get(key)) || 0;
108-
}
109-
async set(key: string, value: number): Promise<void> {
110-
// Store the count in Redis
111-
await redis.set(key, value);
112-
}
113-
async delete(key: string): Promise<void> {
114-
// Remove the count from Redis
115-
await redis.del(key);
116-
}
117-
}
103+
// Redis storage implementations are available from @commandkit/redis
104+
import {
105+
RedisRateLimitStorage,
106+
RedisMutexStorage,
107+
RedisSemaphoreStorage,
108+
} from '@commandkit/redis';
109+
import { Redis } from 'ioredis';
110+
111+
const redis = new Redis();
112+
113+
// Rate limiting with Redis
114+
const rateLimiter = createRateLimiter({
115+
storage: new RedisRateLimitStorage(redis),
116+
});
117+
118+
// Mutex with Redis
119+
const mutex = createMutex({
120+
storage: new RedisMutexStorage(redis),
121+
});
118122

119-
const limiter = createRateLimiter({
120-
storage: new RedisRateLimitStorage(),
123+
// Semaphore with Redis
124+
const semaphore = createSemaphore({
125+
storage: new RedisSemaphoreStorage(redis),
121126
});
122127
```
123128

packages/redis/README.md

Lines changed: 123 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,4 +47,127 @@ const redis = new Redis();
4747
const redisProvider = new RedisCacheProvider(redis);
4848

4949
setCacheProvider(redisProvider)
50+
```
51+
52+
## Redis Mutex Storage
53+
54+
This package also provides a Redis-based mutex storage implementation for distributed locking:
55+
56+
```ts
57+
import { createMutex } from 'commandkit/mutex';
58+
import { RedisMutexStorage } from '@commandkit/redis';
59+
import { Redis } from 'ioredis';
60+
61+
// Create Redis client
62+
const redis = new Redis();
63+
64+
// Create Redis-based mutex storage
65+
const redisMutexStorage = new RedisMutexStorage(redis);
66+
67+
// Create mutex with Redis storage
68+
const mutex = createMutex({
69+
timeout: 30000,
70+
storage: redisMutexStorage,
71+
});
72+
73+
// Use the mutex for distributed locking
74+
const result = await mutex.withLock('shared-resource', async () => {
75+
// This code runs with exclusive access across all instances
76+
return await updateSharedResource();
77+
});
78+
```
79+
80+
### Redis Mutex Features
81+
82+
- **Distributed Locking**: Works across multiple application instances
83+
- **Automatic Expiration**: Locks automatically expire to prevent deadlocks
84+
- **Abort Signal Support**: Can be cancelled using AbortSignal
85+
- **Atomic Operations**: Uses Lua scripts for atomic lock operations
86+
- **Lock Extension**: Extend lock timeouts if needed
87+
- **Force Release**: Emergency release of locks (use with caution)
88+
89+
### Advanced Mutex Usage
90+
91+
```ts
92+
import { RedisMutexStorage } from '@commandkit/redis';
93+
94+
const redisStorage = new RedisMutexStorage(redis);
95+
96+
// Get detailed lock information
97+
const lockInfo = await redisStorage.getLockInfo('my-resource');
98+
console.log(`Locked: ${lockInfo.locked}, TTL: ${lockInfo.ttl}ms`);
99+
100+
// Extend a lock
101+
const extended = await redisStorage.extendLock('my-resource', 60000);
102+
if (extended) {
103+
console.log('Lock extended by 60 seconds');
104+
}
105+
106+
// Force release a lock (emergency use only)
107+
await redisStorage.forceRelease('my-resource');
108+
```
109+
110+
## Redis Semaphore Storage
111+
112+
This package also provides a Redis-based semaphore storage implementation for distributed concurrency control:
113+
114+
```ts
115+
import { createSemaphore } from 'commandkit/semaphore';
116+
import { RedisSemaphoreStorage } from '@commandkit/redis';
117+
import { Redis } from 'ioredis';
118+
119+
// Create Redis client
120+
const redis = new Redis();
121+
122+
// Create Redis-based semaphore storage
123+
const redisSemaphoreStorage = new RedisSemaphoreStorage(redis);
124+
125+
// Create semaphore with Redis storage
126+
const semaphore = createSemaphore({
127+
permits: 5,
128+
timeout: 30000,
129+
storage: redisSemaphoreStorage,
130+
});
131+
132+
// Use the semaphore for distributed concurrency control
133+
const result = await semaphore.withPermit('database-connection', async () => {
134+
// This code runs with limited concurrency across all instances
135+
return await executeDatabaseQuery();
136+
});
137+
```
138+
139+
### Redis Semaphore Features
140+
141+
- **Distributed Concurrency Control**: Works across multiple application instances
142+
- **Automatic Initialization**: Semaphores are automatically initialized when first used
143+
- **Abort Signal Support**: Can be cancelled using AbortSignal
144+
- **Atomic Operations**: Uses Lua scripts for atomic permit operations
145+
- **Dynamic Permit Management**: Increase or decrease permits at runtime
146+
- **Semaphore Information**: Get detailed information about permit usage
147+
148+
### Advanced Semaphore Usage
149+
150+
```ts
151+
import { RedisSemaphoreStorage } from '@commandkit/redis';
152+
153+
const redisStorage = new RedisSemaphoreStorage(redis);
154+
155+
// Get detailed semaphore information
156+
const info = await redisStorage.getSemaphoreInfo('database');
157+
console.log(`Total: ${info.total}, Available: ${info.available}, Acquired: ${info.acquired}`);
158+
159+
// Initialize a semaphore with specific permits
160+
await redisStorage.initialize('api-endpoint', 10);
161+
162+
// Increase permits dynamically
163+
await redisStorage.increasePermits('database', 5);
164+
165+
// Decrease permits dynamically
166+
await redisStorage.decreasePermits('api-endpoint', 2);
167+
168+
// Reset semaphore to initial state
169+
await redisStorage.reset('database');
170+
171+
// Clear semaphore completely
172+
await redisStorage.clear('old-semaphore');
50173
```

packages/redis/src/index.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -150,3 +150,7 @@ export class RedisPlugin extends RuntimePlugin<RedisOptions> {
150150
export function redis(options?: RedisOptions) {
151151
return new RedisPlugin(options ?? {});
152152
}
153+
154+
export * from './ratelimit-storage';
155+
export * from './mutex-storage';
156+
export * from './semaphore-storage';

0 commit comments

Comments
 (0)