skills/idempotency-handling/references/database-based-idempotency.md

124 lines
2.7 KiB
Markdown

# Database-Based Idempotency
## Database-Based Idempotency
```typescript
import { Pool } from "pg";
interface IdempotencyRecord {
key: string;
request_body: any;
response_body?: any;
status: string;
error_message?: string;
created_at: Date;
completed_at?: Date;
}
class DatabaseIdempotency {
constructor(private db: Pool) {
this.createTable();
}
private async createTable(): Promise<void> {
await this.db.query(`
CREATE TABLE IF NOT EXISTS idempotency_keys (
key VARCHAR(255) PRIMARY KEY,
request_body JSONB NOT NULL,
response_body JSONB,
status VARCHAR(50) NOT NULL,
error_message TEXT,
created_at TIMESTAMP DEFAULT NOW(),
completed_at TIMESTAMP,
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_idempotency_expires
ON idempotency_keys (expires_at);
`);
}
async checkIdempotency(
key: string,
requestBody: any,
): Promise<IdempotencyRecord | null> {
const result = await this.db.query(
"SELECT * FROM idempotency_keys WHERE key = $1",
[key],
);
if (result.rows.length === 0) {
return null;
}
const record = result.rows[0];
// Check if request body matches
if (JSON.stringify(record.request_body) !== JSON.stringify(requestBody)) {
throw new Error("Request body mismatch for idempotency key");
}
return record;
}
async startProcessing(key: string, requestBody: any): Promise<boolean> {
try {
const expiresAt = new Date(Date.now() + 86400 * 1000); // 24 hours
await this.db.query(
`
INSERT INTO idempotency_keys (key, request_body, status, expires_at)
VALUES ($1, $2, 'processing', $3)
`,
[key, requestBody, expiresAt],
);
return true;
} catch (error: any) {
if (error.code === "23505") {
// Unique violation
return false;
}
throw error;
}
}
async completeRequest(key: string, responseBody: any): Promise<void> {
await this.db.query(
`
UPDATE idempotency_keys
SET
response_body = $1,
status = 'completed',
completed_at = NOW()
WHERE key = $2
`,
[responseBody, key],
);
}
async failRequest(key: string, errorMessage: string): Promise<void> {
await this.db.query(
`
UPDATE idempotency_keys
SET
error_message = $1,
status = 'failed',
completed_at = NOW()
WHERE key = $2
`,
[errorMessage, key],
);
}
async cleanup(): Promise<number> {
const result = await this.db.query(`
DELETE FROM idempotency_keys
WHERE expires_at < NOW()
`);
return result.rowCount || 0;
}
}
```