diff --git a/src/lib/redis/dead-letter-queue.ts b/src/lib/redis/dead-letter-queue.ts new file mode 100644 index 00000000..1d45e979 --- /dev/null +++ b/src/lib/redis/dead-letter-queue.ts @@ -0,0 +1,32 @@ +/** + * Dead letter queue for failed events (frontend hook-point). + * + * Minimal DLQ over the existing Redis client. Retry/manual-requeue logic + * lives in the backend jobqueue. + */ + +import { getRedisClient } from "../redis/client" + +const DLQ_KEY = "moistello:dlq" + +export interface DeadLetterEvent { + id: string + type: string + attempts: number + error: string +} + +export class DeadLetterQueue { + async push(event: DeadLetterEvent): Promise { + await getRedisClient().rpush(DLQ_KEY, JSON.stringify(event)) + } + + async list(): Promise { + const raw = await getRedisClient().lrange(DLQ_KEY, 0, -1) + return raw.map((item) => JSON.parse(item) as DeadLetterEvent) + } + + async retry(id: string): Promise { + await getRedisClient().lrem(DLQ_KEY, 0, JSON.stringify({ id })) + } +} diff --git a/src/lib/stellar/error-classifier.ts b/src/lib/stellar/error-classifier.ts new file mode 100644 index 00000000..1a3c5739 --- /dev/null +++ b/src/lib/stellar/error-classifier.ts @@ -0,0 +1,33 @@ +/** + * Soroban error classification (frontend hook-point). + * + * Maps Soroban/Stellar error responses to typed domain errors. The + * authoritative classifier (Go) lives in pkg/stellar/errors.go in the + * backend; this mirrors the same codes for client-side handling. + */ + +export class SorobanError extends Error { + code: string + isRetryable: boolean + + constructor(code: string, message: string, isRetryable: boolean) { + super(message) + this.code = code + this.isRetryable = isRetryable + } +} + +export function classifySorobanError( + statusCode: number, + body: string, +): SorobanError { + const message = body.trim() + switch (statusCode) { + case 429: + return new SorobanError("TX_RATE_LIMITED", message, true) + case 400: + return new SorobanError("TX_BAD_REQUEST", message, false) + default: + return new SorobanError("TX_UNKNOWN", message, false) + } +}