omgkit 2.2.0 → 2.3.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.
Files changed (55) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/databases/mongodb/SKILL.md +60 -776
  3. package/plugin/skills/databases/prisma/SKILL.md +53 -744
  4. package/plugin/skills/databases/redis/SKILL.md +53 -860
  5. package/plugin/skills/devops/aws/SKILL.md +68 -672
  6. package/plugin/skills/devops/github-actions/SKILL.md +54 -657
  7. package/plugin/skills/devops/kubernetes/SKILL.md +67 -602
  8. package/plugin/skills/devops/performance-profiling/SKILL.md +59 -863
  9. package/plugin/skills/frameworks/django/SKILL.md +87 -853
  10. package/plugin/skills/frameworks/express/SKILL.md +95 -1301
  11. package/plugin/skills/frameworks/fastapi/SKILL.md +90 -1198
  12. package/plugin/skills/frameworks/laravel/SKILL.md +87 -1187
  13. package/plugin/skills/frameworks/nestjs/SKILL.md +106 -973
  14. package/plugin/skills/frameworks/react/SKILL.md +94 -962
  15. package/plugin/skills/frameworks/vue/SKILL.md +95 -1242
  16. package/plugin/skills/frontend/accessibility/SKILL.md +91 -1056
  17. package/plugin/skills/frontend/frontend-design/SKILL.md +69 -1262
  18. package/plugin/skills/frontend/responsive/SKILL.md +76 -799
  19. package/plugin/skills/frontend/shadcn-ui/SKILL.md +73 -921
  20. package/plugin/skills/frontend/tailwindcss/SKILL.md +60 -788
  21. package/plugin/skills/frontend/threejs/SKILL.md +72 -1266
  22. package/plugin/skills/languages/javascript/SKILL.md +106 -849
  23. package/plugin/skills/methodology/brainstorming/SKILL.md +70 -576
  24. package/plugin/skills/methodology/defense-in-depth/SKILL.md +79 -831
  25. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +81 -654
  26. package/plugin/skills/methodology/executing-plans/SKILL.md +86 -529
  27. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +95 -586
  28. package/plugin/skills/methodology/problem-solving/SKILL.md +67 -681
  29. package/plugin/skills/methodology/receiving-code-review/SKILL.md +70 -533
  30. package/plugin/skills/methodology/requesting-code-review/SKILL.md +70 -610
  31. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +70 -646
  32. package/plugin/skills/methodology/sequential-thinking/SKILL.md +70 -478
  33. package/plugin/skills/methodology/systematic-debugging/SKILL.md +66 -559
  34. package/plugin/skills/methodology/test-driven-development/SKILL.md +91 -752
  35. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +78 -687
  36. package/plugin/skills/methodology/token-optimization/SKILL.md +72 -602
  37. package/plugin/skills/methodology/verification-before-completion/SKILL.md +108 -529
  38. package/plugin/skills/methodology/writing-plans/SKILL.md +79 -566
  39. package/plugin/skills/omega/omega-architecture/SKILL.md +91 -752
  40. package/plugin/skills/omega/omega-coding/SKILL.md +161 -552
  41. package/plugin/skills/omega/omega-sprint/SKILL.md +132 -777
  42. package/plugin/skills/omega/omega-testing/SKILL.md +157 -845
  43. package/plugin/skills/omega/omega-thinking/SKILL.md +165 -606
  44. package/plugin/skills/security/better-auth/SKILL.md +46 -1034
  45. package/plugin/skills/security/oauth/SKILL.md +80 -934
  46. package/plugin/skills/security/owasp/SKILL.md +78 -862
  47. package/plugin/skills/testing/playwright/SKILL.md +77 -700
  48. package/plugin/skills/testing/pytest/SKILL.md +73 -811
  49. package/plugin/skills/testing/vitest/SKILL.md +60 -920
  50. package/plugin/skills/tools/document-processing/SKILL.md +111 -838
  51. package/plugin/skills/tools/image-processing/SKILL.md +126 -659
  52. package/plugin/skills/tools/mcp-development/SKILL.md +85 -758
  53. package/plugin/skills/tools/media-processing/SKILL.md +118 -735
  54. package/plugin/stdrules/SKILL_STANDARDS.md +490 -0
  55. package/plugin/skills/SKILL_STANDARDS.md +0 -743
@@ -1,901 +1,94 @@
1
1
  ---
2
- name: redis
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
2
+ name: Developing with Redis
3
+ description: The agent implements Redis caching, data structures, and real-time messaging patterns. Use when implementing caching layers, session storage, rate limiting, pub/sub messaging, or distributed data structures.
13
4
  ---
14
5
 
15
- # Redis
6
+ # Developing with Redis
16
7
 
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.
18
-
19
- ## Purpose
20
-
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
8
+ ## Quick Start
34
9
 
35
10
  ```typescript
36
- // src/lib/redis.ts
37
11
  import Redis from 'ioredis';
38
12
 
39
- const redisConfig = {
13
+ const redis = new Redis({
40
14
  host: process.env.REDIS_HOST || 'localhost',
41
15
  port: parseInt(process.env.REDIS_PORT || '6379'),
42
- password: process.env.REDIS_PASSWORD,
43
- db: parseInt(process.env.REDIS_DB || '0'),
44
16
  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
- }
17
+ });
352
18
 
353
- return sessions;
354
- }
355
- }
19
+ // Basic caching
20
+ await redis.setex('user:123', 3600, JSON.stringify(userData));
21
+ const cached = await redis.get('user:123');
356
22
  ```
357
23
 
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);
24
+ ## Features
518
25
 
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
- }
26
+ | Feature | Description | Guide |
27
+ |---------|-------------|-------|
28
+ | Caching | High-speed key-value storage with TTL | Use `setex` for auto-expiration, `get` for retrieval |
29
+ | Session Storage | Distributed session management | Store sessions with user ID index for multi-device |
30
+ | Rate Limiting | Request throttling with sliding windows | Use sorted sets or token bucket algorithms |
31
+ | Pub/Sub | Real-time messaging between services | Separate subscriber connections from publishers |
32
+ | Streams | Event sourcing and message queues | Consumer groups for reliable message processing |
33
+ | Data Structures | Lists, sets, sorted sets, hashes | Choose structure based on access patterns |
526
34
 
527
- next();
528
- };
529
- }
530
- ```
35
+ ## Common Patterns
531
36
 
532
- ### 5. Pub/Sub Messaging
37
+ ### Cache-Aside Pattern
533
38
 
534
39
  ```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;
40
+ async function getOrSet<T>(key: string, factory: () => Promise<T>, ttl = 3600): Promise<T> {
41
+ const cached = await redis.get(key);
42
+ if (cached) return JSON.parse(cached);
565
43
 
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
- }
44
+ const value = await factory();
45
+ await redis.setex(key, ttl, JSON.stringify(value));
46
+ return value;
626
47
  }
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(),
641
- });
642
48
  ```
643
49
 
644
- ### 6. Redis Streams
50
+ ### Sliding Window Rate Limiter
645
51
 
646
52
  ```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
- }
53
+ async function checkRateLimit(key: string, limit: number, windowSec: number): Promise<boolean> {
54
+ const now = Date.now();
55
+ const windowStart = now - windowSec * 1000;
689
56
 
690
- const result = await this.redis.xread(...args, 'STREAMS', stream, lastId);
57
+ const pipeline = redis.pipeline();
58
+ pipeline.zremrangebyscore(key, '-inf', windowStart);
59
+ pipeline.zadd(key, now, `${now}:${Math.random()}`);
60
+ pipeline.zcard(key);
61
+ pipeline.expire(key, windowSec);
691
62
 
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
- }
63
+ const results = await pipeline.exec();
64
+ const count = results?.[2]?.[1] as number;
65
+ return count <= limit;
789
66
  }
790
67
  ```
791
68
 
792
- ## Use Cases
793
-
794
- ### Distributed Locking
69
+ ### Distributed Lock
795
70
 
796
71
  ```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
-
72
+ async function acquireLock(key: string, ttlMs = 10000): Promise<string | null> {
73
+ const lockId = crypto.randomUUID();
74
+ const acquired = await redis.set(`lock:${key}`, lockId, 'PX', ttlMs, 'NX');
811
75
  return acquired === 'OK' ? lockId : null;
812
76
  }
813
77
 
814
78
  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
- }
79
+ const script = `if redis.call("get",KEYS[1])==ARGV[1] then return redis.call("del",KEYS[1]) else return 0 end`;
80
+ return (await redis.eval(script, 1, `lock:${key}`, lockId)) === 1;
864
81
  }
865
82
  ```
866
83
 
867
84
  ## Best Practices
868
85
 
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
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/)
86
+ | Do | Avoid |
87
+ |----|-------|
88
+ | Set TTL on all cache keys | Storing objects larger than 100KB |
89
+ | Use pipelines for batch operations | Using `KEYS` command in production |
90
+ | Implement connection pooling | Ignoring memory limits and eviction |
91
+ | Use Lua scripts for atomic operations | Using Redis as primary database |
92
+ | Add key prefixes for namespacing | Blocking on long-running operations |
93
+ | Monitor memory with `INFO memory` | Storing sensitive data unencrypted |
94
+ | Set up Redis Sentinel for HA | Skipping connection error handling |