import { type DomainEvent, type DrainedEvent } from '@substrat-run/contracts'; /** * Decoding a stored `_substrat_outbox` row into the envelope work is done with (#1636). * * The READ side of the outbox is tolerant (`readHistory` and the walks, via `rowDecoder`): * a row that does not decode comes back beside its neighbours, saying why. This is the * other side — the rows that are EXECUTED. A consumer's handler, an executor's handler and * the Tier-2 drain each act on the event, and a half-decoded event is not one any of them * may act on: a consumer built from stand-ins would record effects nobody asked for, and a * lake row built from them would be exact history that is not. So this decode is strict and * throws, and what changed is WHERE the throw lands. It used to sit above each loop's * per-event containment, so one bad row halted every event behind it on every pass: * * - consumer delivery never delivered anything of that type after it; * - executor dispatch failed the executor's whole pending list; * - the Tier-2 drain stalled for the scope, because a row that never ships is never * stamped, so the next pass read it first again. * * Each caller now contains it per event — a dead letter for a delivery, a skip for the * drain — and the event behind it moves. * * One decoder, where there were two copies of the same `domainEvent.parse` in the two * adapters: both now decode through here, so a row cannot deliver on one and dead-letter * on the other. */ /** The `_substrat_outbox` columns the envelope is built from, as either adapter hands them back. */ export interface OutboxEnvelopeRow { id: string; type: string; schema_version: number; occurred_at: string; tenant_id: string; scope_id: string; actor: string; entity_type: string; entity_id: string; pii_class: string; subject_id: string | null; authorization: string | null; impersonation: string | null; operation: string | null; payload: string | null; } /** …plus the columns the Tier-2 drain lifts beside the envelope (#1242, #1237). */ export interface OutboxDrainRow extends OutboxEnvelopeRow { version?: string | null; caused_by?: string | null; invocation_id?: string | null; } /** * The stored row → the `DomainEvent` a consumer or executor is handed, or a throw naming * EVERY column that did not decode. * * A row the kernel wrote decodes exactly as it always did — the same candidate into the same * `domainEvent.parse`. The throw's message never quotes the stored text (see `rowDecoder`): * it is written into a dead letter, which is stored beside the event and survives an erasure * of its payload. */ export declare function domainEventOf(row: OutboxEnvelopeRow): DomainEvent; /** * The stored row → the `DrainedEvent` the Tier-2 drain ships, or a throw (`domainEventOf`'s). * * The WHOLE event is parsed by the published `drainedEvent` schema — the envelope and the * columns lifted beside it (`operation`, `version`, `caused_by`, `invocation_id`), which the * envelope does not carry. They used to be copied onto an already-validated envelope as * stored, into a value merely TYPED as a `DrainedEvent` (#1641 review): a restored row with * `version: ''` or a `caused_by` that is not an event id reached the lake, append-only, and * was stamped as having left. Parsed here, it throws, and `readUndrainedOutbox` skips it and * says so. A row the kernel wrote passes unchanged: each lifted column is written from a * validated value or as NULL. */ export declare function drainedEventOf(row: OutboxDrainRow): DrainedEvent; /** * What a Tier-2 read passed over (#1636): rows that would not decode, so were neither * returned nor — since nothing ships them — ever stamped. * * `eventIds` is capped at {@link UNDRAINED_SKIPPED_IDS}; `count` is exact. Ids only, never * a reason: this travels to the control plane's sweep report and its log, and a decode * message is one step from the stored text it describes. */ export interface UndrainedSkipped { count: number; eventIds: string[]; } /** * `readUndrainedEvents`' answer: the events, oldest first — an ARRAY, because every caller * already reads one — with what the read skipped riding beside them as an optional * property. Absent when every row it read decoded, so a clean read is the array it always * was. * * A property on an array does not survive `JSON.stringify` or an RPC hop, so each hop that * carries this says so explicitly (the DO's `undrainedEventsRead`, the vertical's * `/internal/undrained-events?withSkipped=1`); it is only ever attached in-process. */ export type UndrainedEvents = DrainedEvent[] & { skipped?: UndrainedSkipped; }; /** The same answer, as a plain object — the shape that crosses an RPC or HTTP hop. */ export interface UndrainedRead { events: DrainedEvent[]; skipped?: UndrainedSkipped; } /** How many rows past `limit` one read may look through, as a multiple of `limit`. */ export declare const UNDRAINED_SCAN_FACTOR = 10; /** How many skipped ids a read names; the count past it stays exact. */ export declare const UNDRAINED_SKIPPED_IDS = 50; /** * The Tier-2 drain's read over one scope's outbox (#1334), CONTAINED per row (#1636) — the * one implementation both adapters run, so they cannot disagree about what is skipped. * * `page(offset, count)` is the adapter's * `SELECT * … WHERE drained_at IS NULL ORDER BY id LIMIT count OFFSET offset`. Offset * rather than an id cursor on purpose: the rows being stepped over are exactly the ones * whose `id` may be the column that is broken. * * A row that does not decode is SKIPPED: not returned, so no sink ever receives an event * built from stand-ins — the lake is append-only, and a wrong row there cannot be taken * back — and not stamped, so `drained_at` stays an exact record of what left. The read * keeps going past it, so the healthy rows behind it still ship. The cost is stated rather * than hidden: a skipped row never reaches the lake, and every pass reads it again. It * stays visible in Tier 1, where `readHistory` returns it with a `decodeError`. * * Bounded: at most `limit × UNDRAINED_SCAN_FACTOR` rows are looked at. A scope whose next * that-many undrained rows ALL fail to decode ships nothing, pass after pass — at that * point the spine is broken wholesale, which is a restore to repair rather than a row to * step over, and the skipped count in the sweep report is what says so. */ export declare function readUndrainedOutbox(page: (offset: number, count: number) => OutboxDrainRow[], limit: number): UndrainedRead; /** An `UndrainedRead` → the in-process array shape, the skip attached only when there is one. */ export declare function undrainedEventsOf(read: UndrainedRead): UndrainedEvents; //# sourceMappingURL=outbox-event.d.ts.map