/** * The pure reducer behind the live interactions inbox: frame parsing, the live * overlay accumulated over the paged base, and the two derived projections * (`merge` → the published list, `pendingCount` → the live badge tally). No React, * no I/O — the connection driver owns the SSE loop and feeds frames through here. */ import type { Interaction } from '@tai42/api-client'; /** A live interaction plus the client-maintained `answered` flag. */ export type StreamInteraction = Interaction & { readonly answered: boolean; }; /** * Validate an `interaction.add` frame into an `Interaction`, or null when it is * malformed. This mirrors the api-client `interaction` zod schema exactly — it * accepts what that schema accepts, no more, no less — so the caller surfaces a * malformed frame as an error rather than rendering a blank card, and never * silently coerces one. */ export declare function parseAddFrame(data: string): Interaction | null; /** The interaction id from an `answered` / `removed` frame, or null. */ export declare function parseId(data: string): string | null; /** * The live overlay a connection accumulates over the paged base: the tail's own * adds (kept whether or not the base already lists them, so a fresher add wins), * the answered stamps (wall-clock ms, for the aging sweep), and the tombstones for * ids removed on the tail OR aged out of the answered window (a tombstone stops a * still-stale seed from re-showing a gone card; ids are never reused). `addEpoch` * and `goneEpoch` carry the resync epoch current when an add / a first terminal frame * arrived, so the count applies an add (or subtracts a terminal) only while it is not * yet folded into the refetched `total`; the merge and the aging sweep ignore them. */ export interface LiveOverlay { readonly adds: Map; readonly addEpoch: Map; readonly answeredAt: Map; readonly removed: Set; readonly goneEpoch: Map; } export declare function emptyOverlay(): LiveOverlay; /** Record an `interaction.add`: a live add overwrites any prior copy in place. */ export declare function overlayApplyAdd(overlay: LiveOverlay, interaction: Interaction, epoch: number): void; /** * Record an `interaction.answered`. The answered time and terminal epoch are * stamped on the FIRST answered frame only (a redelivery keeps the original stamp, * so the retention window is not extended and a later terminal never re-dates it * into a fresher resync). A seed-origin card is promoted into the overlay so it * lingers uniformly with a live-add card, since the paged base never carries * answered items and a refetch would otherwise drop it before its window elapses. */ export declare function overlayApplyAnswered(overlay: LiveOverlay, id: string, epoch: number, seed: readonly Interaction[]): void; /** Record an `interaction.removed`: tombstone the id and stamp its first terminal epoch. */ export declare function overlayApplyRemoved(overlay: LiveOverlay, id: string, epoch: number): void; /** * Age answered cards out (the server sends no removal for them): tombstone any past * the retention window, then — as a flood backstop — the oldest beyond the count * cap. A tombstone (not a bare delete) so a still-stale seed cannot resurrect a card * the client has retired; the id keeps its original `goneEpoch` so moving * answered→removed never re-dates it into a fresher resync. */ export declare function sweepAnswered(overlay: LiveOverlay): void; /** Fold the paged base and the live overlay into the published, ordered list. */ export declare function merge(seed: readonly Interaction[], live: LiveOverlay): StreamInteraction[]; /** * The live pending tally: the door's `total` for the seed, plus live adds whose id * is absent from the seed AND stamped at or after `countEpoch` (a new question not * yet folded into `total`), minus one for every terminal id — answered OR removed — * stamped at or after `countEpoch`, deduped per id so an id counts once however many * terminal frames it drew. `countEpoch` is the epoch of the last resync whose refetch * LANDED (a failed refetch never advances it): a delta stamped before it is already * reflected in the fresh `total`, so it is NOT applied again — this is what stops a * resync from re-subtracting a terminal it already dropped from `total`, while a * failed refetch keeps the prior-epoch deltas applying against the prior total. One * residual race remains: a frame arriving while a refetch is in flight may already be * reflected in that refetch's `total`, so the raw tally can transiently dip below zero * until the next resync reconciles. A pending tally below zero is meaningless, so the * published count clamps at 0; the clamp covers only the negative symptom (a transient * positive under-count reconciles on the next resync, not here). */ export declare function pendingCount(seed: readonly Interaction[], total: number, live: LiveOverlay, countEpoch: number): number; //# sourceMappingURL=interactions-stream.d.ts.map