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.
Files changed (56) hide show
  1. package/package.json +1 -1
  2. package/plugin/skills/SKILL_STANDARDS.md +743 -0
  3. package/plugin/skills/databases/mongodb/SKILL.md +797 -28
  4. package/plugin/skills/databases/postgresql/SKILL.md +494 -18
  5. package/plugin/skills/databases/prisma/SKILL.md +776 -30
  6. package/plugin/skills/databases/redis/SKILL.md +885 -25
  7. package/plugin/skills/devops/aws/SKILL.md +686 -28
  8. package/plugin/skills/devops/docker/SKILL.md +466 -18
  9. package/plugin/skills/devops/github-actions/SKILL.md +684 -29
  10. package/plugin/skills/devops/kubernetes/SKILL.md +621 -24
  11. package/plugin/skills/frameworks/django/SKILL.md +920 -20
  12. package/plugin/skills/frameworks/express/SKILL.md +1361 -35
  13. package/plugin/skills/frameworks/fastapi/SKILL.md +1260 -33
  14. package/plugin/skills/frameworks/laravel/SKILL.md +1244 -31
  15. package/plugin/skills/frameworks/nestjs/SKILL.md +1005 -26
  16. package/plugin/skills/frameworks/nextjs/SKILL.md +407 -44
  17. package/plugin/skills/frameworks/rails/SKILL.md +594 -28
  18. package/plugin/skills/frameworks/react/SKILL.md +1006 -32
  19. package/plugin/skills/frameworks/spring/SKILL.md +528 -35
  20. package/plugin/skills/frameworks/vue/SKILL.md +1296 -27
  21. package/plugin/skills/frontend/accessibility/SKILL.md +1108 -34
  22. package/plugin/skills/frontend/frontend-design/SKILL.md +1304 -26
  23. package/plugin/skills/frontend/responsive/SKILL.md +847 -21
  24. package/plugin/skills/frontend/shadcn-ui/SKILL.md +976 -38
  25. package/plugin/skills/frontend/tailwindcss/SKILL.md +831 -35
  26. package/plugin/skills/frontend/threejs/SKILL.md +1298 -29
  27. package/plugin/skills/languages/javascript/SKILL.md +935 -31
  28. package/plugin/skills/languages/python/SKILL.md +489 -25
  29. package/plugin/skills/languages/typescript/SKILL.md +379 -30
  30. package/plugin/skills/methodology/brainstorming/SKILL.md +597 -23
  31. package/plugin/skills/methodology/defense-in-depth/SKILL.md +832 -34
  32. package/plugin/skills/methodology/dispatching-parallel-agents/SKILL.md +665 -31
  33. package/plugin/skills/methodology/executing-plans/SKILL.md +556 -24
  34. package/plugin/skills/methodology/finishing-development-branch/SKILL.md +595 -25
  35. package/plugin/skills/methodology/problem-solving/SKILL.md +429 -61
  36. package/plugin/skills/methodology/receiving-code-review/SKILL.md +536 -24
  37. package/plugin/skills/methodology/requesting-code-review/SKILL.md +632 -21
  38. package/plugin/skills/methodology/root-cause-tracing/SKILL.md +641 -30
  39. package/plugin/skills/methodology/sequential-thinking/SKILL.md +262 -3
  40. package/plugin/skills/methodology/systematic-debugging/SKILL.md +571 -32
  41. package/plugin/skills/methodology/test-driven-development/SKILL.md +779 -24
  42. package/plugin/skills/methodology/testing-anti-patterns/SKILL.md +691 -29
  43. package/plugin/skills/methodology/token-optimization/SKILL.md +598 -29
  44. package/plugin/skills/methodology/verification-before-completion/SKILL.md +543 -22
  45. package/plugin/skills/methodology/writing-plans/SKILL.md +590 -18
  46. package/plugin/skills/omega/omega-architecture/SKILL.md +838 -39
  47. package/plugin/skills/omega/omega-coding/SKILL.md +636 -39
  48. package/plugin/skills/omega/omega-sprint/SKILL.md +855 -48
  49. package/plugin/skills/omega/omega-testing/SKILL.md +940 -41
  50. package/plugin/skills/omega/omega-thinking/SKILL.md +703 -50
  51. package/plugin/skills/security/better-auth/SKILL.md +1065 -28
  52. package/plugin/skills/security/oauth/SKILL.md +968 -31
  53. package/plugin/skills/security/owasp/SKILL.md +894 -33
  54. package/plugin/skills/testing/playwright/SKILL.md +764 -38
  55. package/plugin/skills/testing/pytest/SKILL.md +873 -36
  56. package/plugin/skills/testing/vitest/SKILL.md +980 -35
@@ -1,41 +1,901 @@
1
1
  ---
2
2
  name: redis
3
- description: Redis caching. Use for caching, sessions, pub/sub.
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 Skill
15
+ # Redis
7
16
 
8
- ## Patterns
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
- ### Caching
11
- ```javascript
12
- // Set with expiry
13
- await redis.set('user:123', JSON.stringify(user), 'EX', 3600);
19
+ ## Purpose
14
20
 
15
- // Get
16
- const cached = await redis.get('user:123');
17
- if (cached) return JSON.parse(cached);
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
- ### Session
21
- ```javascript
22
- await redis.hset(`session:${id}`, {
23
- userId: user.id,
24
- createdAt: Date.now()
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
- ### Rate Limiting
30
- ```javascript
31
- const key = `rate:${ip}`;
32
- const count = await redis.incr(key);
33
- if (count === 1) await redis.expire(key, 60);
34
- if (count > 100) throw new Error('Rate limited');
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
- - Set TTL on keys
39
- - Use pipelines for batch ops
40
- - Use appropriate data structures
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/)