omgkit 2.1.0 → 2.2.0
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/package.json +1 -1
- package/plugin/skills/SKILL_STANDARDS.md +743 -0
- package/plugin/skills/databases/mongodb/SKILL.md +797 -28
- package/plugin/skills/databases/postgresql/SKILL.md +494 -18
- package/plugin/skills/databases/prisma/SKILL.md +776 -30
- package/plugin/skills/databases/redis/SKILL.md +885 -25
- package/plugin/skills/devops/aws/SKILL.md +686 -28
- package/plugin/skills/devops/docker/SKILL.md +466 -18
- package/plugin/skills/devops/github-actions/SKILL.md +684 -29
- package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
- package/plugin/skills/frameworks/django/SKILL.md +920 -20
- package/plugin/skills/frameworks/express/SKILL.md +1361 -35
- package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
- package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
- package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
- package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
- package/plugin/skills/frameworks/rails/SKILL.md +594 -28
- package/plugin/skills/frameworks/react/SKILL.md +1006 -32
- package/plugin/skills/frameworks/spring/SKILL.md +528 -35
- package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
- package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
- package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
- package/plugin/skills/frontend/responsive/SKILL.md +847 -21
- package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
- package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
- package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
- package/plugin/skills/languages/javascript/SKILL.md +935 -31
- package/plugin/skills/languages/python/SKILL.md +489 -25
- package/plugin/skills/languages/typescript/SKILL.md +379 -30
- package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
- package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
- package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
- package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
- package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
- package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
- package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
- package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
- package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
- package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
- package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
- package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
- package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
- package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
- package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
- package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
- package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
- package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
- package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
- package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
- package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
- package/plugin/skills/security/better-auth/SKILL.md +1065 -28
- package/plugin/skills/security/oauth/SKILL.md +968 -31
- package/plugin/skills/security/owasp/SKILL.md +894 -33
- package/plugin/skills/testing/playwright/SKILL.md +764 -38
- package/plugin/skills/testing/pytest/SKILL.md +873 -36
- package/plugin/skills/testing/vitest/SKILL.md +980 -35
|
@@ -1,41 +1,901 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: redis
|
|
3
|
-
description: Redis caching
|
|
3
|
+
description: Redis caching and data structures with pub/sub, streams, sessions, and rate limiting patterns
|
|
4
|
+
category: databases
|
|
5
|
+
triggers:
|
|
6
|
+
- redis
|
|
7
|
+
- caching
|
|
8
|
+
- cache
|
|
9
|
+
- pub sub
|
|
10
|
+
- session storage
|
|
11
|
+
- rate limiting
|
|
12
|
+
- ioredis
|
|
4
13
|
---
|
|
5
14
|
|
|
6
|
-
# Redis
|
|
15
|
+
# Redis
|
|
7
16
|
|
|
8
|
-
|
|
17
|
+
Enterprise-grade **Redis development** following industry best practices. This skill covers caching strategies, data structures, pub/sub messaging, streams, sessions, rate limiting, and production-ready patterns used by top engineering teams.
|
|
9
18
|
|
|
10
|
-
|
|
11
|
-
```javascript
|
|
12
|
-
// Set with expiry
|
|
13
|
-
await redis.set('user:123', JSON.stringify(user), 'EX', 3600);
|
|
19
|
+
## Purpose
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
Build high-performance applications with Redis:
|
|
22
|
+
|
|
23
|
+
- Implement efficient caching strategies
|
|
24
|
+
- Use appropriate data structures
|
|
25
|
+
- Build real-time features with pub/sub
|
|
26
|
+
- Process events with Redis Streams
|
|
27
|
+
- Manage user sessions
|
|
28
|
+
- Implement rate limiting
|
|
29
|
+
- Deploy for high availability
|
|
30
|
+
|
|
31
|
+
## Features
|
|
32
|
+
|
|
33
|
+
### 1. Redis Client Setup
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
// src/lib/redis.ts
|
|
37
|
+
import Redis from 'ioredis';
|
|
38
|
+
|
|
39
|
+
const redisConfig = {
|
|
40
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
41
|
+
port: parseInt(process.env.REDIS_PORT || '6379'),
|
|
42
|
+
password: process.env.REDIS_PASSWORD,
|
|
43
|
+
db: parseInt(process.env.REDIS_DB || '0'),
|
|
44
|
+
maxRetriesPerRequest: 3,
|
|
45
|
+
retryStrategy: (times: number) => {
|
|
46
|
+
const delay = Math.min(times * 50, 2000);
|
|
47
|
+
return delay;
|
|
48
|
+
},
|
|
49
|
+
reconnectOnError: (err: Error) => {
|
|
50
|
+
const targetErrors = ['READONLY', 'ECONNRESET'];
|
|
51
|
+
return targetErrors.some(e => err.message.includes(e));
|
|
52
|
+
},
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// Singleton instance
|
|
56
|
+
let redis: Redis | null = null;
|
|
57
|
+
|
|
58
|
+
export function getRedis(): Redis {
|
|
59
|
+
if (!redis) {
|
|
60
|
+
redis = new Redis(redisConfig);
|
|
61
|
+
|
|
62
|
+
redis.on('connect', () => {
|
|
63
|
+
console.log('Redis connected');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
redis.on('error', (err) => {
|
|
67
|
+
console.error('Redis error:', err);
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
redis.on('close', () => {
|
|
71
|
+
console.log('Redis connection closed');
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
return redis;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export async function closeRedis(): Promise<void> {
|
|
79
|
+
if (redis) {
|
|
80
|
+
await redis.quit();
|
|
81
|
+
redis = null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Health check
|
|
86
|
+
export async function checkRedisHealth(): Promise<boolean> {
|
|
87
|
+
try {
|
|
88
|
+
const client = getRedis();
|
|
89
|
+
const result = await client.ping();
|
|
90
|
+
return result === 'PONG';
|
|
91
|
+
} catch {
|
|
92
|
+
return false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### 2. Caching Service
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
// src/services/cache.service.ts
|
|
101
|
+
import Redis from 'ioredis';
|
|
102
|
+
import { getRedis } from '../lib/redis';
|
|
103
|
+
|
|
104
|
+
interface CacheOptions {
|
|
105
|
+
ttl?: number; // seconds
|
|
106
|
+
prefix?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export class CacheService {
|
|
110
|
+
private redis: Redis;
|
|
111
|
+
private defaultTTL: number;
|
|
112
|
+
private prefix: string;
|
|
113
|
+
|
|
114
|
+
constructor(options: CacheOptions = {}) {
|
|
115
|
+
this.redis = getRedis();
|
|
116
|
+
this.defaultTTL = options.ttl || 3600; // 1 hour
|
|
117
|
+
this.prefix = options.prefix || 'cache:';
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
private getKey(key: string): string {
|
|
121
|
+
return `${this.prefix}${key}`;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Basic get/set
|
|
125
|
+
async get<T>(key: string): Promise<T | null> {
|
|
126
|
+
const data = await this.redis.get(this.getKey(key));
|
|
127
|
+
if (!data) return null;
|
|
128
|
+
return JSON.parse(data) as T;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
|
|
132
|
+
const serialized = JSON.stringify(value);
|
|
133
|
+
const expiry = ttl || this.defaultTTL;
|
|
134
|
+
await this.redis.setex(this.getKey(key), expiry, serialized);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
async delete(key: string): Promise<void> {
|
|
138
|
+
await this.redis.del(this.getKey(key));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Get or set pattern
|
|
142
|
+
async getOrSet<T>(
|
|
143
|
+
key: string,
|
|
144
|
+
factory: () => Promise<T>,
|
|
145
|
+
ttl?: number
|
|
146
|
+
): Promise<T> {
|
|
147
|
+
const cached = await this.get<T>(key);
|
|
148
|
+
if (cached !== null) {
|
|
149
|
+
return cached;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const value = await factory();
|
|
153
|
+
await this.set(key, value, ttl);
|
|
154
|
+
return value;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// Cache invalidation patterns
|
|
158
|
+
async invalidatePattern(pattern: string): Promise<number> {
|
|
159
|
+
const keys = await this.redis.keys(`${this.prefix}${pattern}`);
|
|
160
|
+
if (keys.length === 0) return 0;
|
|
161
|
+
return this.redis.del(...keys);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
async invalidateByTags(tags: string[]): Promise<void> {
|
|
165
|
+
const pipeline = this.redis.pipeline();
|
|
166
|
+
|
|
167
|
+
for (const tag of tags) {
|
|
168
|
+
const tagKey = `tag:${tag}`;
|
|
169
|
+
const members = await this.redis.smembers(tagKey);
|
|
170
|
+
|
|
171
|
+
for (const key of members) {
|
|
172
|
+
pipeline.del(key);
|
|
173
|
+
}
|
|
174
|
+
pipeline.del(tagKey);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
await pipeline.exec();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Set with tags for invalidation
|
|
181
|
+
async setWithTags<T>(
|
|
182
|
+
key: string,
|
|
183
|
+
value: T,
|
|
184
|
+
tags: string[],
|
|
185
|
+
ttl?: number
|
|
186
|
+
): Promise<void> {
|
|
187
|
+
const fullKey = this.getKey(key);
|
|
188
|
+
const pipeline = this.redis.pipeline();
|
|
189
|
+
|
|
190
|
+
pipeline.setex(fullKey, ttl || this.defaultTTL, JSON.stringify(value));
|
|
191
|
+
|
|
192
|
+
for (const tag of tags) {
|
|
193
|
+
pipeline.sadd(`tag:${tag}`, fullKey);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
await pipeline.exec();
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Bulk operations
|
|
200
|
+
async mget<T>(keys: string[]): Promise<(T | null)[]> {
|
|
201
|
+
const fullKeys = keys.map(k => this.getKey(k));
|
|
202
|
+
const results = await this.redis.mget(...fullKeys);
|
|
203
|
+
return results.map(r => (r ? JSON.parse(r) : null));
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async mset<T>(entries: Array<{ key: string; value: T; ttl?: number }>): Promise<void> {
|
|
207
|
+
const pipeline = this.redis.pipeline();
|
|
208
|
+
|
|
209
|
+
for (const entry of entries) {
|
|
210
|
+
const fullKey = this.getKey(entry.key);
|
|
211
|
+
const serialized = JSON.stringify(entry.value);
|
|
212
|
+
pipeline.setex(fullKey, entry.ttl || this.defaultTTL, serialized);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
await pipeline.exec();
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
### 3. Session Management
|
|
221
|
+
|
|
222
|
+
```typescript
|
|
223
|
+
// src/services/session.service.ts
|
|
224
|
+
import Redis from 'ioredis';
|
|
225
|
+
import { getRedis } from '../lib/redis';
|
|
226
|
+
import { randomUUID } from 'crypto';
|
|
227
|
+
|
|
228
|
+
interface Session {
|
|
229
|
+
id: string;
|
|
230
|
+
userId: string;
|
|
231
|
+
data: Record<string, unknown>;
|
|
232
|
+
createdAt: number;
|
|
233
|
+
expiresAt: number;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export class SessionService {
|
|
237
|
+
private redis: Redis;
|
|
238
|
+
private prefix = 'session:';
|
|
239
|
+
private userSessionsPrefix = 'user_sessions:';
|
|
240
|
+
private defaultTTL = 86400 * 7; // 7 days
|
|
241
|
+
|
|
242
|
+
constructor() {
|
|
243
|
+
this.redis = getRedis();
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
async create(userId: string, data: Record<string, unknown> = {}): Promise<Session> {
|
|
247
|
+
const sessionId = randomUUID();
|
|
248
|
+
const now = Date.now();
|
|
249
|
+
|
|
250
|
+
const session: Session = {
|
|
251
|
+
id: sessionId,
|
|
252
|
+
userId,
|
|
253
|
+
data,
|
|
254
|
+
createdAt: now,
|
|
255
|
+
expiresAt: now + this.defaultTTL * 1000,
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const pipeline = this.redis.pipeline();
|
|
259
|
+
|
|
260
|
+
// Store session
|
|
261
|
+
pipeline.hset(`${this.prefix}${sessionId}`, {
|
|
262
|
+
userId,
|
|
263
|
+
data: JSON.stringify(data),
|
|
264
|
+
createdAt: now.toString(),
|
|
265
|
+
expiresAt: session.expiresAt.toString(),
|
|
266
|
+
});
|
|
267
|
+
pipeline.expire(`${this.prefix}${sessionId}`, this.defaultTTL);
|
|
268
|
+
|
|
269
|
+
// Track user sessions
|
|
270
|
+
pipeline.sadd(`${this.userSessionsPrefix}${userId}`, sessionId);
|
|
271
|
+
pipeline.expire(`${this.userSessionsPrefix}${userId}`, this.defaultTTL);
|
|
272
|
+
|
|
273
|
+
await pipeline.exec();
|
|
274
|
+
|
|
275
|
+
return session;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
async get(sessionId: string): Promise<Session | null> {
|
|
279
|
+
const data = await this.redis.hgetall(`${this.prefix}${sessionId}`);
|
|
280
|
+
|
|
281
|
+
if (!data || !data.userId) {
|
|
282
|
+
return null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return {
|
|
286
|
+
id: sessionId,
|
|
287
|
+
userId: data.userId,
|
|
288
|
+
data: JSON.parse(data.data || '{}'),
|
|
289
|
+
createdAt: parseInt(data.createdAt),
|
|
290
|
+
expiresAt: parseInt(data.expiresAt),
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
async update(sessionId: string, data: Record<string, unknown>): Promise<void> {
|
|
295
|
+
const exists = await this.redis.exists(`${this.prefix}${sessionId}`);
|
|
296
|
+
if (!exists) {
|
|
297
|
+
throw new Error('Session not found');
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
await this.redis.hset(`${this.prefix}${sessionId}`, 'data', JSON.stringify(data));
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
async refresh(sessionId: string): Promise<void> {
|
|
304
|
+
const session = await this.get(sessionId);
|
|
305
|
+
if (!session) {
|
|
306
|
+
throw new Error('Session not found');
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const newExpiresAt = Date.now() + this.defaultTTL * 1000;
|
|
310
|
+
|
|
311
|
+
const pipeline = this.redis.pipeline();
|
|
312
|
+
pipeline.hset(`${this.prefix}${sessionId}`, 'expiresAt', newExpiresAt.toString());
|
|
313
|
+
pipeline.expire(`${this.prefix}${sessionId}`, this.defaultTTL);
|
|
314
|
+
await pipeline.exec();
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
async destroy(sessionId: string): Promise<void> {
|
|
318
|
+
const session = await this.get(sessionId);
|
|
319
|
+
if (!session) return;
|
|
320
|
+
|
|
321
|
+
const pipeline = this.redis.pipeline();
|
|
322
|
+
pipeline.del(`${this.prefix}${sessionId}`);
|
|
323
|
+
pipeline.srem(`${this.userSessionsPrefix}${session.userId}`, sessionId);
|
|
324
|
+
await pipeline.exec();
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
async destroyAllUserSessions(userId: string): Promise<number> {
|
|
328
|
+
const sessionIds = await this.redis.smembers(`${this.userSessionsPrefix}${userId}`);
|
|
329
|
+
|
|
330
|
+
if (sessionIds.length === 0) return 0;
|
|
331
|
+
|
|
332
|
+
const pipeline = this.redis.pipeline();
|
|
333
|
+
for (const sessionId of sessionIds) {
|
|
334
|
+
pipeline.del(`${this.prefix}${sessionId}`);
|
|
335
|
+
}
|
|
336
|
+
pipeline.del(`${this.userSessionsPrefix}${userId}`);
|
|
337
|
+
|
|
338
|
+
await pipeline.exec();
|
|
339
|
+
return sessionIds.length;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
async getUserSessions(userId: string): Promise<Session[]> {
|
|
343
|
+
const sessionIds = await this.redis.smembers(`${this.userSessionsPrefix}${userId}`);
|
|
344
|
+
|
|
345
|
+
const sessions: Session[] = [];
|
|
346
|
+
for (const sessionId of sessionIds) {
|
|
347
|
+
const session = await this.get(sessionId);
|
|
348
|
+
if (session) {
|
|
349
|
+
sessions.push(session);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return sessions;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
```
|
|
357
|
+
|
|
358
|
+
### 4. Rate Limiting
|
|
359
|
+
|
|
360
|
+
```typescript
|
|
361
|
+
// src/services/rate-limiter.service.ts
|
|
362
|
+
import Redis from 'ioredis';
|
|
363
|
+
import { getRedis } from '../lib/redis';
|
|
364
|
+
|
|
365
|
+
interface RateLimitResult {
|
|
366
|
+
allowed: boolean;
|
|
367
|
+
remaining: number;
|
|
368
|
+
resetAt: number;
|
|
369
|
+
retryAfter?: number;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
interface RateLimitConfig {
|
|
373
|
+
points: number; // Number of requests
|
|
374
|
+
duration: number; // Time window in seconds
|
|
375
|
+
blockDuration?: number; // Block duration when exceeded
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
export class RateLimiter {
|
|
379
|
+
private redis: Redis;
|
|
380
|
+
private prefix = 'ratelimit:';
|
|
381
|
+
|
|
382
|
+
constructor() {
|
|
383
|
+
this.redis = getRedis();
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// Fixed window rate limiting
|
|
387
|
+
async checkFixedWindow(
|
|
388
|
+
key: string,
|
|
389
|
+
config: RateLimitConfig
|
|
390
|
+
): Promise<RateLimitResult> {
|
|
391
|
+
const fullKey = `${this.prefix}fixed:${key}`;
|
|
392
|
+
const now = Math.floor(Date.now() / 1000);
|
|
393
|
+
const windowStart = now - (now % config.duration);
|
|
394
|
+
const windowKey = `${fullKey}:${windowStart}`;
|
|
395
|
+
|
|
396
|
+
const current = await this.redis.incr(windowKey);
|
|
397
|
+
|
|
398
|
+
if (current === 1) {
|
|
399
|
+
await this.redis.expire(windowKey, config.duration);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
const remaining = Math.max(0, config.points - current);
|
|
403
|
+
const resetAt = (windowStart + config.duration) * 1000;
|
|
404
|
+
|
|
405
|
+
if (current > config.points) {
|
|
406
|
+
return {
|
|
407
|
+
allowed: false,
|
|
408
|
+
remaining: 0,
|
|
409
|
+
resetAt,
|
|
410
|
+
retryAfter: resetAt - Date.now(),
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
return { allowed: true, remaining, resetAt };
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Sliding window rate limiting
|
|
418
|
+
async checkSlidingWindow(
|
|
419
|
+
key: string,
|
|
420
|
+
config: RateLimitConfig
|
|
421
|
+
): Promise<RateLimitResult> {
|
|
422
|
+
const fullKey = `${this.prefix}sliding:${key}`;
|
|
423
|
+
const now = Date.now();
|
|
424
|
+
const windowStart = now - config.duration * 1000;
|
|
425
|
+
|
|
426
|
+
// Use sorted set for sliding window
|
|
427
|
+
const pipeline = this.redis.pipeline();
|
|
428
|
+
pipeline.zremrangebyscore(fullKey, '-inf', windowStart);
|
|
429
|
+
pipeline.zadd(fullKey, now, `${now}:${Math.random()}`);
|
|
430
|
+
pipeline.zcard(fullKey);
|
|
431
|
+
pipeline.expire(fullKey, config.duration);
|
|
432
|
+
|
|
433
|
+
const results = await pipeline.exec();
|
|
434
|
+
const count = results?.[2]?.[1] as number;
|
|
435
|
+
|
|
436
|
+
const remaining = Math.max(0, config.points - count);
|
|
437
|
+
const resetAt = now + config.duration * 1000;
|
|
438
|
+
|
|
439
|
+
if (count > config.points) {
|
|
440
|
+
// Remove the request we just added
|
|
441
|
+
await this.redis.zremrangebyscore(fullKey, now, now);
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
allowed: false,
|
|
445
|
+
remaining: 0,
|
|
446
|
+
resetAt,
|
|
447
|
+
retryAfter: config.duration * 1000,
|
|
448
|
+
};
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
return { allowed: true, remaining, resetAt };
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
// Token bucket rate limiting
|
|
455
|
+
async checkTokenBucket(
|
|
456
|
+
key: string,
|
|
457
|
+
config: { capacity: number; refillRate: number }
|
|
458
|
+
): Promise<RateLimitResult> {
|
|
459
|
+
const fullKey = `${this.prefix}bucket:${key}`;
|
|
460
|
+
const now = Date.now();
|
|
461
|
+
|
|
462
|
+
const luaScript = `
|
|
463
|
+
local key = KEYS[1]
|
|
464
|
+
local capacity = tonumber(ARGV[1])
|
|
465
|
+
local refillRate = tonumber(ARGV[2])
|
|
466
|
+
local now = tonumber(ARGV[3])
|
|
467
|
+
|
|
468
|
+
local bucket = redis.call('HMGET', key, 'tokens', 'lastRefill')
|
|
469
|
+
local tokens = tonumber(bucket[1]) or capacity
|
|
470
|
+
local lastRefill = tonumber(bucket[2]) or now
|
|
471
|
+
|
|
472
|
+
-- Calculate refill
|
|
473
|
+
local elapsed = (now - lastRefill) / 1000
|
|
474
|
+
local refill = elapsed * refillRate
|
|
475
|
+
tokens = math.min(capacity, tokens + refill)
|
|
476
|
+
|
|
477
|
+
-- Try to consume a token
|
|
478
|
+
local allowed = 0
|
|
479
|
+
if tokens >= 1 then
|
|
480
|
+
tokens = tokens - 1
|
|
481
|
+
allowed = 1
|
|
482
|
+
end
|
|
483
|
+
|
|
484
|
+
redis.call('HMSET', key, 'tokens', tokens, 'lastRefill', now)
|
|
485
|
+
redis.call('EXPIRE', key, 3600)
|
|
486
|
+
|
|
487
|
+
return {allowed, tokens}
|
|
488
|
+
`;
|
|
489
|
+
|
|
490
|
+
const result = await this.redis.eval(
|
|
491
|
+
luaScript,
|
|
492
|
+
1,
|
|
493
|
+
fullKey,
|
|
494
|
+
config.capacity,
|
|
495
|
+
config.refillRate,
|
|
496
|
+
now
|
|
497
|
+
) as [number, number];
|
|
498
|
+
|
|
499
|
+
return {
|
|
500
|
+
allowed: result[0] === 1,
|
|
501
|
+
remaining: Math.floor(result[1]),
|
|
502
|
+
resetAt: now + Math.ceil((config.capacity - result[1]) / config.refillRate) * 1000,
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
// Express middleware
|
|
508
|
+
export function createRateLimitMiddleware(config: RateLimitConfig) {
|
|
509
|
+
const limiter = new RateLimiter();
|
|
510
|
+
|
|
511
|
+
return async (req: any, res: any, next: any) => {
|
|
512
|
+
const key = req.ip || req.connection.remoteAddress;
|
|
513
|
+
const result = await limiter.checkSlidingWindow(key, config);
|
|
514
|
+
|
|
515
|
+
res.setHeader('X-RateLimit-Limit', config.points);
|
|
516
|
+
res.setHeader('X-RateLimit-Remaining', result.remaining);
|
|
517
|
+
res.setHeader('X-RateLimit-Reset', result.resetAt);
|
|
518
|
+
|
|
519
|
+
if (!result.allowed) {
|
|
520
|
+
res.setHeader('Retry-After', Math.ceil((result.retryAfter || 0) / 1000));
|
|
521
|
+
return res.status(429).json({
|
|
522
|
+
error: 'Too many requests',
|
|
523
|
+
retryAfter: result.retryAfter,
|
|
524
|
+
});
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
next();
|
|
528
|
+
};
|
|
529
|
+
}
|
|
18
530
|
```
|
|
19
531
|
|
|
20
|
-
###
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
532
|
+
### 5. Pub/Sub Messaging
|
|
533
|
+
|
|
534
|
+
```typescript
|
|
535
|
+
// src/services/pubsub.service.ts
|
|
536
|
+
import Redis from 'ioredis';
|
|
537
|
+
import { getRedis } from '../lib/redis';
|
|
538
|
+
|
|
539
|
+
type MessageHandler = (message: unknown, channel: string) => void | Promise<void>;
|
|
540
|
+
|
|
541
|
+
export class PubSubService {
|
|
542
|
+
private publisher: Redis;
|
|
543
|
+
private subscriber: Redis;
|
|
544
|
+
private handlers: Map<string, Set<MessageHandler>> = new Map();
|
|
545
|
+
|
|
546
|
+
constructor() {
|
|
547
|
+
this.publisher = getRedis();
|
|
548
|
+
this.subscriber = getRedis().duplicate();
|
|
549
|
+
|
|
550
|
+
this.subscriber.on('message', (channel, message) => {
|
|
551
|
+
this.handleMessage(channel, message);
|
|
552
|
+
});
|
|
553
|
+
|
|
554
|
+
this.subscriber.on('pmessage', (pattern, channel, message) => {
|
|
555
|
+
this.handleMessage(pattern, message, channel);
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
private async handleMessage(channel: string, message: string, actualChannel?: string): Promise<void> {
|
|
560
|
+
const handlers = this.handlers.get(channel);
|
|
561
|
+
if (!handlers) return;
|
|
562
|
+
|
|
563
|
+
const parsed = JSON.parse(message);
|
|
564
|
+
const targetChannel = actualChannel || channel;
|
|
565
|
+
|
|
566
|
+
for (const handler of handlers) {
|
|
567
|
+
try {
|
|
568
|
+
await handler(parsed, targetChannel);
|
|
569
|
+
} catch (error) {
|
|
570
|
+
console.error(`Error handling message on ${targetChannel}:`, error);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// Subscribe to channel
|
|
576
|
+
async subscribe(channel: string, handler: MessageHandler): Promise<() => void> {
|
|
577
|
+
if (!this.handlers.has(channel)) {
|
|
578
|
+
this.handlers.set(channel, new Set());
|
|
579
|
+
await this.subscriber.subscribe(channel);
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
this.handlers.get(channel)!.add(handler);
|
|
583
|
+
|
|
584
|
+
return () => {
|
|
585
|
+
const handlers = this.handlers.get(channel);
|
|
586
|
+
if (handlers) {
|
|
587
|
+
handlers.delete(handler);
|
|
588
|
+
if (handlers.size === 0) {
|
|
589
|
+
this.handlers.delete(channel);
|
|
590
|
+
this.subscriber.unsubscribe(channel);
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
};
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
// Subscribe to pattern
|
|
597
|
+
async psubscribe(pattern: string, handler: MessageHandler): Promise<() => void> {
|
|
598
|
+
if (!this.handlers.has(pattern)) {
|
|
599
|
+
this.handlers.set(pattern, new Set());
|
|
600
|
+
await this.subscriber.psubscribe(pattern);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
this.handlers.get(pattern)!.add(handler);
|
|
604
|
+
|
|
605
|
+
return () => {
|
|
606
|
+
const handlers = this.handlers.get(pattern);
|
|
607
|
+
if (handlers) {
|
|
608
|
+
handlers.delete(handler);
|
|
609
|
+
if (handlers.size === 0) {
|
|
610
|
+
this.handlers.delete(pattern);
|
|
611
|
+
this.subscriber.punsubscribe(pattern);
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// Publish message
|
|
618
|
+
async publish<T>(channel: string, message: T): Promise<number> {
|
|
619
|
+
return this.publisher.publish(channel, JSON.stringify(message));
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
// Cleanup
|
|
623
|
+
async close(): Promise<void> {
|
|
624
|
+
await this.subscriber.quit();
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// Usage example
|
|
629
|
+
const pubsub = new PubSubService();
|
|
630
|
+
|
|
631
|
+
// Subscribe to user events
|
|
632
|
+
const unsubscribe = await pubsub.subscribe('user:events', (message, channel) => {
|
|
633
|
+
console.log('User event:', message);
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
// Publish event
|
|
637
|
+
await pubsub.publish('user:events', {
|
|
638
|
+
type: 'user.created',
|
|
639
|
+
userId: '123',
|
|
640
|
+
timestamp: Date.now(),
|
|
25
641
|
});
|
|
26
|
-
await redis.expire(`session:${id}`, 86400);
|
|
27
642
|
```
|
|
28
643
|
|
|
29
|
-
###
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
644
|
+
### 6. Redis Streams
|
|
645
|
+
|
|
646
|
+
```typescript
|
|
647
|
+
// src/services/stream.service.ts
|
|
648
|
+
import Redis from 'ioredis';
|
|
649
|
+
import { getRedis } from '../lib/redis';
|
|
650
|
+
|
|
651
|
+
interface StreamMessage {
|
|
652
|
+
id: string;
|
|
653
|
+
fields: Record<string, string>;
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
export class StreamService {
|
|
657
|
+
private redis: Redis;
|
|
658
|
+
|
|
659
|
+
constructor() {
|
|
660
|
+
this.redis = getRedis();
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Add message to stream
|
|
664
|
+
async add(
|
|
665
|
+
stream: string,
|
|
666
|
+
data: Record<string, string | number>,
|
|
667
|
+
maxLen?: number
|
|
668
|
+
): Promise<string> {
|
|
669
|
+
const fields = Object.entries(data).flat().map(String);
|
|
670
|
+
|
|
671
|
+
if (maxLen) {
|
|
672
|
+
return this.redis.xadd(stream, 'MAXLEN', '~', maxLen, '*', ...fields);
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return this.redis.xadd(stream, '*', ...fields);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
// Read messages
|
|
679
|
+
async read(
|
|
680
|
+
stream: string,
|
|
681
|
+
options: { count?: number; block?: number; lastId?: string } = {}
|
|
682
|
+
): Promise<StreamMessage[]> {
|
|
683
|
+
const { count = 10, block, lastId = '$' } = options;
|
|
684
|
+
|
|
685
|
+
const args: (string | number)[] = ['COUNT', count];
|
|
686
|
+
if (block !== undefined) {
|
|
687
|
+
args.push('BLOCK', block);
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
const result = await this.redis.xread(...args, 'STREAMS', stream, lastId);
|
|
691
|
+
|
|
692
|
+
if (!result) return [];
|
|
693
|
+
|
|
694
|
+
return result[0][1].map(([id, fields]: [string, string[]]) => ({
|
|
695
|
+
id,
|
|
696
|
+
fields: this.parseFields(fields),
|
|
697
|
+
}));
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Consumer group operations
|
|
701
|
+
async createConsumerGroup(
|
|
702
|
+
stream: string,
|
|
703
|
+
group: string,
|
|
704
|
+
startId: string = '$'
|
|
705
|
+
): Promise<void> {
|
|
706
|
+
try {
|
|
707
|
+
await this.redis.xgroup('CREATE', stream, group, startId, 'MKSTREAM');
|
|
708
|
+
} catch (error: any) {
|
|
709
|
+
if (!error.message.includes('BUSYGROUP')) {
|
|
710
|
+
throw error;
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
async readGroup(
|
|
716
|
+
stream: string,
|
|
717
|
+
group: string,
|
|
718
|
+
consumer: string,
|
|
719
|
+
options: { count?: number; block?: number } = {}
|
|
720
|
+
): Promise<StreamMessage[]> {
|
|
721
|
+
const { count = 10, block = 5000 } = options;
|
|
722
|
+
|
|
723
|
+
const result = await this.redis.xreadgroup(
|
|
724
|
+
'GROUP', group, consumer,
|
|
725
|
+
'COUNT', count,
|
|
726
|
+
'BLOCK', block,
|
|
727
|
+
'STREAMS', stream, '>'
|
|
728
|
+
);
|
|
729
|
+
|
|
730
|
+
if (!result) return [];
|
|
731
|
+
|
|
732
|
+
return result[0][1].map(([id, fields]: [string, string[]]) => ({
|
|
733
|
+
id,
|
|
734
|
+
fields: this.parseFields(fields),
|
|
735
|
+
}));
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async acknowledge(stream: string, group: string, ...ids: string[]): Promise<number> {
|
|
739
|
+
return this.redis.xack(stream, group, ...ids);
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
async getPendingMessages(
|
|
743
|
+
stream: string,
|
|
744
|
+
group: string,
|
|
745
|
+
consumer?: string
|
|
746
|
+
): Promise<any> {
|
|
747
|
+
if (consumer) {
|
|
748
|
+
return this.redis.xpending(stream, group, '-', '+', 100, consumer);
|
|
749
|
+
}
|
|
750
|
+
return this.redis.xpending(stream, group);
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
private parseFields(fields: string[]): Record<string, string> {
|
|
754
|
+
const result: Record<string, string> = {};
|
|
755
|
+
for (let i = 0; i < fields.length; i += 2) {
|
|
756
|
+
result[fields[i]] = fields[i + 1];
|
|
757
|
+
}
|
|
758
|
+
return result;
|
|
759
|
+
}
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
// Stream consumer worker
|
|
763
|
+
async function startStreamWorker(
|
|
764
|
+
stream: string,
|
|
765
|
+
group: string,
|
|
766
|
+
consumer: string,
|
|
767
|
+
handler: (message: StreamMessage) => Promise<void>
|
|
768
|
+
) {
|
|
769
|
+
const streamService = new StreamService();
|
|
770
|
+
await streamService.createConsumerGroup(stream, group);
|
|
771
|
+
|
|
772
|
+
while (true) {
|
|
773
|
+
try {
|
|
774
|
+
const messages = await streamService.readGroup(stream, group, consumer);
|
|
775
|
+
|
|
776
|
+
for (const message of messages) {
|
|
777
|
+
try {
|
|
778
|
+
await handler(message);
|
|
779
|
+
await streamService.acknowledge(stream, group, message.id);
|
|
780
|
+
} catch (error) {
|
|
781
|
+
console.error('Error processing message:', error);
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
} catch (error) {
|
|
785
|
+
console.error('Stream read error:', error);
|
|
786
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
```
|
|
791
|
+
|
|
792
|
+
## Use Cases
|
|
793
|
+
|
|
794
|
+
### Distributed Locking
|
|
795
|
+
|
|
796
|
+
```typescript
|
|
797
|
+
async function acquireLock(
|
|
798
|
+
key: string,
|
|
799
|
+
ttl: number = 10000
|
|
800
|
+
): Promise<string | null> {
|
|
801
|
+
const redis = getRedis();
|
|
802
|
+
const lockId = randomUUID();
|
|
803
|
+
|
|
804
|
+
const acquired = await redis.set(
|
|
805
|
+
`lock:${key}`,
|
|
806
|
+
lockId,
|
|
807
|
+
'PX', ttl,
|
|
808
|
+
'NX'
|
|
809
|
+
);
|
|
810
|
+
|
|
811
|
+
return acquired === 'OK' ? lockId : null;
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
async function releaseLock(key: string, lockId: string): Promise<boolean> {
|
|
815
|
+
const redis = getRedis();
|
|
816
|
+
const script = `
|
|
817
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
818
|
+
return redis.call("del", KEYS[1])
|
|
819
|
+
else
|
|
820
|
+
return 0
|
|
821
|
+
end
|
|
822
|
+
`;
|
|
823
|
+
|
|
824
|
+
const result = await redis.eval(script, 1, `lock:${key}`, lockId);
|
|
825
|
+
return result === 1;
|
|
826
|
+
}
|
|
827
|
+
```
|
|
828
|
+
|
|
829
|
+
### Leaderboard
|
|
830
|
+
|
|
831
|
+
```typescript
|
|
832
|
+
class LeaderboardService {
|
|
833
|
+
private redis: Redis;
|
|
834
|
+
private key: string;
|
|
835
|
+
|
|
836
|
+
constructor(name: string) {
|
|
837
|
+
this.redis = getRedis();
|
|
838
|
+
this.key = `leaderboard:${name}`;
|
|
839
|
+
}
|
|
840
|
+
|
|
841
|
+
async addScore(userId: string, score: number): Promise<void> {
|
|
842
|
+
await this.redis.zadd(this.key, score, userId);
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
async getTopN(n: number): Promise<Array<{ userId: string; score: number; rank: number }>> {
|
|
846
|
+
const results = await this.redis.zrevrange(this.key, 0, n - 1, 'WITHSCORES');
|
|
847
|
+
const entries = [];
|
|
848
|
+
|
|
849
|
+
for (let i = 0; i < results.length; i += 2) {
|
|
850
|
+
entries.push({
|
|
851
|
+
userId: results[i],
|
|
852
|
+
score: parseFloat(results[i + 1]),
|
|
853
|
+
rank: i / 2 + 1,
|
|
854
|
+
});
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
return entries;
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
async getUserRank(userId: string): Promise<number | null> {
|
|
861
|
+
const rank = await this.redis.zrevrank(this.key, userId);
|
|
862
|
+
return rank !== null ? rank + 1 : null;
|
|
863
|
+
}
|
|
864
|
+
}
|
|
35
865
|
```
|
|
36
866
|
|
|
37
867
|
## Best Practices
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
868
|
+
|
|
869
|
+
### Do's
|
|
870
|
+
|
|
871
|
+
- Use connection pooling
|
|
872
|
+
- Set appropriate TTLs on all keys
|
|
873
|
+
- Use pipelines for batch operations
|
|
874
|
+
- Implement proper error handling
|
|
875
|
+
- Use Lua scripts for atomic operations
|
|
41
876
|
- Monitor memory usage
|
|
877
|
+
- Set up Redis Sentinel or Cluster for HA
|
|
878
|
+
- Use key prefixes for namespacing
|
|
879
|
+
- Implement circuit breakers
|
|
880
|
+
- Use appropriate data structures
|
|
881
|
+
|
|
882
|
+
### Don'ts
|
|
883
|
+
|
|
884
|
+
- Don't store large objects (>100KB)
|
|
885
|
+
- Don't use KEYS in production
|
|
886
|
+
- Don't skip key expiration
|
|
887
|
+
- Don't ignore memory limits
|
|
888
|
+
- Don't use single Redis for critical apps
|
|
889
|
+
- Don't block on long operations
|
|
890
|
+
- Don't store sensitive data unencrypted
|
|
891
|
+
- Don't ignore connection errors
|
|
892
|
+
- Don't use Redis as primary database
|
|
893
|
+
- Don't skip monitoring
|
|
894
|
+
|
|
895
|
+
## References
|
|
896
|
+
|
|
897
|
+
- [Redis Documentation](https://redis.io/documentation)
|
|
898
|
+
- [ioredis Documentation](https://github.com/luin/ioredis)
|
|
899
|
+
- [Redis Best Practices](https://redis.io/docs/management/optimization/)
|
|
900
|
+
- [Redis University](https://university.redis.com/)
|
|
901
|
+
- [Redis Patterns](https://redis.io/docs/manual/patterns/)
|