69 lines
3.6 KiB
Markdown
69 lines
3.6 KiB
Markdown
# `dead-letter.ts` — `DeadLetterSink` interface
|
|
|
|
<!-- begin:auto-docs -->
|
|
_(no submodules)_
|
|
<!-- end:auto-docs -->
|
|
|
|
## Purpose
|
|
|
|
Defines the `DeadLetterSink` interface and `DeadLetterEntry` shape used by `WebhookService.send` to persist delivery attempts that exhausted every retry. Operators rely on the sink to replay or diagnose failed deliveries — the full `attempts` history (status codes, durations, error messages) survives the failure so a webhook can be reconstructed exactly. The interface ships with a default in-memory implementation (`MemoryDeadLetterSink`) and is the plug point for Mailpit / Slack / S3 / Postgres siblings.
|
|
|
|
## Features
|
|
|
|
- `DeadLetterEntry` — JSON-serialisable; carries the originating endpoint id, tenant id, event name, full payload, every attempt record, first + final attempt timestamps, and a human-readable reason.
|
|
- `ListDeadLettersFilter` — `{ endpointId?, limit? }`. Implementers SHOULD return most-recent first when `limit` is supplied.
|
|
- Two-method interface (`record`, `list`) — small enough for sinks to author trivially, large enough to cover the inspection use case the audit calls for.
|
|
|
|
## Usage: Basic
|
|
|
|
```ts
|
|
import type { DeadLetterSink } from "@suaveplan/api-plugin-webhook/dead-letter/dead-letter";
|
|
|
|
declare const sink: DeadLetterSink;
|
|
const failed = await sink.list({ limit: 50 });
|
|
console.log(failed.length);
|
|
```
|
|
|
|
## Usage: Advanced
|
|
|
|
```ts
|
|
import type {
|
|
DeadLetterEntry,
|
|
DeadLetterSink,
|
|
ListDeadLettersFilter,
|
|
} from "@suaveplan/api-plugin-webhook/dead-letter/dead-letter";
|
|
|
|
class S3DeadLetterSink implements DeadLetterSink {
|
|
constructor(private readonly bucket: string) {}
|
|
async record(entry: DeadLetterEntry): Promise<void> {
|
|
await s3PutObject({
|
|
Bucket: this.bucket,
|
|
Key: `dead-letter/${entry.endpointId}/${entry.id}.json`,
|
|
Body: JSON.stringify(entry),
|
|
ContentType: "application/json",
|
|
});
|
|
}
|
|
async list(filter: ListDeadLettersFilter = {}): Promise<readonly DeadLetterEntry[]> {
|
|
const objects = await s3ListObjects({
|
|
Bucket: this.bucket,
|
|
Prefix: filter.endpointId
|
|
? `dead-letter/${filter.endpointId}/`
|
|
: "dead-letter/",
|
|
MaxKeys: filter.limit ?? 1000,
|
|
});
|
|
return Promise.all(objects.map((o) => s3GetObjectJson<DeadLetterEntry>(o.Key)));
|
|
}
|
|
}
|
|
```
|
|
|
|
## API Reference
|
|
|
|
- `DeadLetterEntry` — readonly record. `id` is sink-generated; `endpointId`/`tenantId`/`event`/`payload`/`attempts`/`firstAttemptAt`/`finalAttemptAt`/`reason` are populated by the service when a delivery exhausts.
|
|
- `ListDeadLettersFilter` — `{ endpointId?: string; limit?: number }`. Both fields optional.
|
|
- `DeadLetterSink.record(entry)` — durably stores the entry. Returns once the write commits; throws on persistence failure (the service translates this to `WEBHOOK_DEAD_LETTER_WRITE_FAILED`).
|
|
- `DeadLetterSink.list(filter?)` — returns recorded entries matching the filter, most-recent first when `limit` is supplied.
|
|
|
|
## Implementation Notes
|
|
|
|
`record` errors are surfaced separately from the delivery failure (the service raises `WEBHOOK_DEAD_LETTER_WRITE_FAILED` with the sink error as `cause` and the original delivery exhaustion preserved on the cause chain). This split lets operators distinguish "the receiver is broken" from "the sink is broken" in their dashboards. The sink interface does not provide a `purge` method on purpose — purging policy belongs to operators (cron jobs, lifecycle policies, manual tooling), not to the recording surface. Sinks SHOULD treat `DeadLetterEntry.payload` as opaque; the plugin's `WebhookPayload.data` field is `unknown` and may carry caller-supplied PII.
|