Skip to content

Commit 54ddc7d

Browse files
committed
Sync from main repo
1 parent 866781c commit 54ddc7d

File tree

3 files changed

+311
-102
lines changed

3 files changed

+311
-102
lines changed

src/middleware/protection.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,5 @@
1-
import { Redis } from "@upstash/redis";
21
import { Context, Next } from "hono";
3-
4-
// Initialize Redis client
5-
const redis = new Redis({
6-
url: process.env.UPSTASH_REDIS_REST_URL!,
7-
token: process.env.UPSTASH_REDIS_REST_TOKEN!,
8-
});
2+
import { redis } from "../storage.js";
93

104
// Rate limit configuration
115
const RATE_LIMIT = {
@@ -20,7 +14,6 @@ const memCache = new Map<string, { count: number; expires: number }>();
2014
/**
2115
* Rate limiting middleware for public endpoints
2216
* Uses Redis to track request counts across multiple instances
23-
* Optimized to use Redis pipeline for better performance
2417
*/
2518
export async function rateLimiter(
2619
c: Context,
@@ -41,7 +34,6 @@ export async function rateLimiter(
4134
let requests: number;
4235
let ttl: number;
4336

44-
// Check memory cache first to avoid Redis calls for frequent requests
4537
const now = Date.now();
4638
const cached = memCache.get(key);
4739

src/redis-mock.ts

Lines changed: 307 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,307 @@
1+
/**
2+
* Redis Mock Implementation
3+
*
4+
* This file provides a mock implementation of Redis for development and testing.
5+
* It implements the core Redis methods needed by the RSS service.
6+
*/
7+
8+
// In-memory storage for mock Redis
9+
const inMemoryStorage: Record<string, any> = {};
10+
11+
/**
12+
* Redis Mock Client
13+
* Implements the Redis methods used by the RSS service
14+
*/
15+
export class RedisMock {
16+
/**
17+
* Get a value from the mock Redis store
18+
*/
19+
async get(key: string): Promise<string | null> {
20+
console.log(`[MOCK REDIS] get: ${key}`);
21+
const value = inMemoryStorage[key];
22+
23+
if (value === undefined) {
24+
console.log(`[MOCK REDIS] Key not found: ${key}, returning null`);
25+
return null;
26+
}
27+
28+
console.log(`[MOCK REDIS] Retrieved value for key: ${key}`);
29+
return value;
30+
}
31+
32+
/**
33+
* Set a value in the mock Redis store
34+
*/
35+
async set(key: string, value: string): Promise<string> {
36+
console.log(
37+
`[MOCK REDIS] set: ${key}, ${value.substring(0, 50)}${value.length > 50 ? "..." : ""}`,
38+
);
39+
inMemoryStorage[key] = value;
40+
return "OK";
41+
}
42+
43+
/**
44+
* Check if a key exists in the mock Redis store
45+
*/
46+
async exists(key: string): Promise<number> {
47+
console.log(`[MOCK REDIS] exists: ${key}`);
48+
const exists = inMemoryStorage[key] !== undefined;
49+
console.log(`[MOCK REDIS] Key ${key} exists: ${exists}`);
50+
return exists ? 1 : 0;
51+
}
52+
53+
/**
54+
* Delete a key from the mock Redis store
55+
*/
56+
async del(key: string): Promise<number> {
57+
console.log(`[MOCK REDIS] del: ${key}`);
58+
59+
if (inMemoryStorage[key] === undefined) {
60+
return 0;
61+
}
62+
63+
delete inMemoryStorage[key];
64+
return 1;
65+
}
66+
67+
/**
68+
* Get a range of values from a list
69+
*/
70+
async lrange(key: string, start: number, end: number): Promise<string[]> {
71+
console.log(`[MOCK REDIS] lrange: ${key}, ${start}, ${end}`);
72+
73+
if (!inMemoryStorage[key] || !Array.isArray(inMemoryStorage[key])) {
74+
console.log(
75+
`[MOCK REDIS] Key not found or not an array: ${key}, returning empty array`,
76+
);
77+
return [];
78+
}
79+
80+
const result = inMemoryStorage[key].slice(
81+
start,
82+
end === -1 ? undefined : end + 1,
83+
);
84+
85+
console.log(`[MOCK REDIS] lrange result: ${result.length} items`);
86+
return result;
87+
}
88+
89+
/**
90+
* Push a value to the beginning of a list
91+
*/
92+
async lpush(key: string, value: string): Promise<number> {
93+
console.log(
94+
`[MOCK REDIS] lpush: ${key}, ${value.substring(0, 50)}${value.length > 50 ? "..." : ""}`,
95+
);
96+
97+
if (!inMemoryStorage[key]) {
98+
inMemoryStorage[key] = [];
99+
} else if (!Array.isArray(inMemoryStorage[key])) {
100+
// Convert to array if it's not already
101+
inMemoryStorage[key] = [inMemoryStorage[key]];
102+
}
103+
104+
inMemoryStorage[key].unshift(value);
105+
console.log(
106+
`[MOCK REDIS] New length after lpush: ${inMemoryStorage[key].length}`,
107+
);
108+
return inMemoryStorage[key].length;
109+
}
110+
111+
/**
112+
* Trim a list to a specified range
113+
*/
114+
async ltrim(key: string, start: number, end: number): Promise<string> {
115+
console.log(`[MOCK REDIS] ltrim: ${key}, ${start}, ${end}`);
116+
117+
if (!inMemoryStorage[key] || !Array.isArray(inMemoryStorage[key])) {
118+
return "OK";
119+
}
120+
121+
// Handle NaN or invalid end values
122+
let endIndex = end;
123+
if (isNaN(endIndex) || endIndex < 0) {
124+
console.log(
125+
`[MOCK REDIS] Invalid end index: ${end}, using -1 (keep all items)`,
126+
);
127+
endIndex = -1;
128+
}
129+
130+
inMemoryStorage[key] = inMemoryStorage[key].slice(
131+
start,
132+
endIndex === -1 ? undefined : endIndex + 1,
133+
);
134+
135+
console.log(
136+
`[MOCK REDIS] New length after ltrim: ${inMemoryStorage[key].length}`,
137+
);
138+
return "OK";
139+
}
140+
141+
/**
142+
* Increment a value
143+
* For rate limiting
144+
*/
145+
async incr(key: string): Promise<number> {
146+
console.log(`[MOCK REDIS] incr: ${key}`);
147+
148+
let value = inMemoryStorage[key];
149+
150+
if (value === undefined) {
151+
// Key doesn't exist, initialize to 1
152+
inMemoryStorage[key] = "1";
153+
return 1;
154+
}
155+
156+
if (typeof value === "string") {
157+
// Try to parse as number
158+
const num = parseInt(value, 10);
159+
if (!isNaN(num)) {
160+
inMemoryStorage[key] = (num + 1).toString();
161+
return num + 1;
162+
}
163+
}
164+
165+
// If we can't parse as number, start from 1
166+
inMemoryStorage[key] = "1";
167+
return 1;
168+
}
169+
170+
/**
171+
* Set an expiry time on a key
172+
* For rate limiting
173+
*/
174+
async expire(key: string, seconds: number): Promise<number> {
175+
console.log(`[MOCK REDIS] expire: ${key}, ${seconds} seconds`);
176+
177+
if (inMemoryStorage[key] === undefined) {
178+
return 0;
179+
}
180+
181+
// Store expiry time in a separate key
182+
const expiryKey = `${key}:expiry`;
183+
const expiryTime = Date.now() + seconds * 1000;
184+
inMemoryStorage[expiryKey] = expiryTime.toString();
185+
186+
// Set up automatic cleanup (this is a simplification)
187+
setTimeout(() => {
188+
if (inMemoryStorage[key] !== undefined) {
189+
console.log(`[MOCK REDIS] Auto-expiring key: ${key}`);
190+
delete inMemoryStorage[key];
191+
delete inMemoryStorage[expiryKey];
192+
}
193+
}, seconds * 1000);
194+
195+
return 1;
196+
}
197+
198+
/**
199+
* Get the time-to-live for a key
200+
* For rate limiting
201+
*/
202+
async ttl(key: string): Promise<number> {
203+
console.log(`[MOCK REDIS] ttl: ${key}`);
204+
205+
const expiryKey = `${key}:expiry`;
206+
const expiryTime = inMemoryStorage[expiryKey];
207+
208+
if (expiryTime === undefined) {
209+
return -2; // Key does not exist or has no expiry
210+
}
211+
212+
const ttl = Math.ceil((parseInt(expiryTime, 10) - Date.now()) / 1000);
213+
return ttl > 0 ? ttl : -1; // -1 means the key exists but has no expiry
214+
}
215+
216+
/**
217+
* Ping the Redis server
218+
* For health checks
219+
*/
220+
async ping(): Promise<string> {
221+
console.log(`[MOCK REDIS] ping`);
222+
return "PONG";
223+
}
224+
225+
/**
226+
* Create a pipeline for batched operations
227+
* For rate limiting optimization
228+
*/
229+
pipeline(): RedisMockPipeline {
230+
console.log(`[MOCK REDIS] Creating pipeline`);
231+
return new RedisMockPipeline(this);
232+
}
233+
234+
/**
235+
* Helper method to inspect the current state (not part of Redis API)
236+
*/
237+
getStorageState(): Record<string, any> {
238+
return Object.keys(inMemoryStorage).reduce(
239+
(acc, key) => {
240+
acc[key] = inMemoryStorage[key];
241+
return acc;
242+
},
243+
{} as Record<string, any>,
244+
);
245+
}
246+
}
247+
248+
/**
249+
* Redis Mock Pipeline
250+
* Implements the Redis pipeline interface for batched operations
251+
*/
252+
class RedisMockPipeline {
253+
private commands: { method: string; args: any[] }[] = [];
254+
private redis: RedisMock;
255+
256+
constructor(redis: RedisMock) {
257+
this.redis = redis;
258+
}
259+
260+
/**
261+
* Add an incr command to the pipeline
262+
*/
263+
incr(key: string): this {
264+
this.commands.push({ method: "incr", args: [key] });
265+
return this;
266+
}
267+
268+
/**
269+
* Add a ttl command to the pipeline
270+
*/
271+
ttl(key: string): this {
272+
this.commands.push({ method: "ttl", args: [key] });
273+
return this;
274+
}
275+
276+
/**
277+
* Add an expire command to the pipeline
278+
*/
279+
expire(key: string, seconds: number): this {
280+
this.commands.push({ method: "expire", args: [key, seconds] });
281+
return this;
282+
}
283+
284+
/**
285+
* Execute all commands in the pipeline
286+
*/
287+
async exec(): Promise<any[]> {
288+
console.log(
289+
`[MOCK REDIS] Executing pipeline with ${this.commands.length} commands`,
290+
);
291+
292+
const results: any[] = [];
293+
294+
for (const command of this.commands) {
295+
try {
296+
// @ts-ignore - We know these methods exist on the redis mock
297+
const result = await this.redis[command.method](...command.args);
298+
results.push(result);
299+
} catch (error) {
300+
console.error(`[MOCK REDIS] Pipeline command error:`, error);
301+
results.push(null);
302+
}
303+
}
304+
305+
return results;
306+
}
307+
}

0 commit comments

Comments
 (0)