import { createHash } from 'crypto';
export interface IdempotencyRecord {
key: string;
fingerprint: string;
outcome: any;
created_at: string;
expires_at: string;
}
export interface IdempotencyStore {
get(key: string, fingerprint: string): Promise<IdempotencyRecord | null>;
set(key: string, fingerprint: string, outcome: any, ttlMs: number): Promise<void>;
delete(key: string): Promise<void>;
}
export class MemoryIdempotencyStore implements IdempotencyStore {
private store = new Map<string, IdempotencyRecord>();
async get(key: string, fingerprint: string): Promise<IdempotencyRecord | null> {
const recordKey = `${key}:${fingerprint}`;
const record = this.store.get(recordKey);
if (!record) {
return null;
}
if (new Date(record.expires_at) < new Date()) {
this.store.delete(recordKey);
return null;
}
return record;
}
async set(key: string, fingerprint: string, outcome: any, ttlMs: number): Promise<void> {
const recordKey = `${key}:${fingerprint}`;
const now = new Date();
const expiresAt = new Date(now.getTime() + ttlMs);
const record: IdempotencyRecord = {
key,
fingerprint,
outcome,
created_at: now.toISOString(),
expires_at: expiresAt.toISOString()
};
this.store.set(recordKey, record);
}
async delete(key: string): Promise<void> {
for (const [recordKey] of this.store) {
if (recordKey.startsWith(`${key}:`)) {
this.store.delete(recordKey);
}
}
}
}
export class RedisIdempotencyStore implements IdempotencyStore {
constructor(private redis: any) {}
async get(key: string, fingerprint: string): Promise<IdempotencyRecord | null> {
const recordKey = `idempotency:${key}:${fingerprint}`;
const record = await this.redis.get(recordKey);
if (!record) {
return null;
}
return JSON.parse(record);
}
async set(key: string, fingerprint: string, outcome: any, ttlMs: number): Promise<void> {
const recordKey = `idempotency:${key}:${fingerprint}`;
const now = new Date();
const expiresAt = new Date(now.getTime() + ttlMs);
const record: IdempotencyRecord = {
key,
fingerprint,
outcome,
created_at: now.toISOString(),
expires_at: expiresAt.toISOString()
};
await this.redis.setex(recordKey, Math.ceil(ttlMs / 1000), JSON.stringify(record));
}
async delete(key: string): Promise<void> {
const pattern = `idempotency:${key}:*`;
const keys = await this.redis.keys(pattern);
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
}
export function generateFingerprint(request: any): string {
const normalized = JSON.stringify(request, Object.keys(request).sort());
return createHash('sha256').update(normalized).digest('hex');
}
export class IdempotencyManager {
constructor(private store: IdempotencyStore) {}
async checkIdempotency<T>(
key: string,
request: any,
ttlMs: number = 24 * 60 * 60 * 1000
): Promise<{ isIdempotent: boolean; cachedResult?: T }> {
const fingerprint = generateFingerprint(request);
const record = await this.store.get(key, fingerprint);
if (record) {
return {
isIdempotent: true,
cachedResult: record.outcome
};
}
return { isIdempotent: false };
}
async storeResult(
key: string,
request: any,
result: any,
ttlMs: number = 24 * 60 * 60 * 1000
): Promise<void> {
const fingerprint = generateFingerprint(request);
await this.store.set(key, fingerprint, result, ttlMs);
}
async clearKey(key: string): Promise<void> {
await this.store.delete(key);
}
}
export function createIdempotencyStore(redis?: any): IdempotencyStore {
if (redis) {
return new RedisIdempotencyStore(redis);
}
return new MemoryIdempotencyStore();
}