/** * The app resource change feed: a durable note that one of an app's * resources changed, kept once for every consumer that asked to hear about * that resource type. * * Producers call one SQL function, `agent_native_app_resource_changed`, in the * writer's own transaction, so nothing is recorded unless the write commits. * Today the producers are row triggers that core generates for a registered * table. They catch every writer: actions, sync jobs, raw SQL, and deletes. * If core later gains a data layer that every write goes through, it calls the * same function and the triggers retire; consumers don't change. * * Each (consumer, resource) pair is one row. Repeated changes update that row * rather than adding rows, so autosave doesn't grow the feed. Every change * takes a new value from one sequence, taken after the row lock, so for any * one resource a larger `seq` always means a later committed change. Consumers * use it to reject stale work and to delete only what they processed. * * A change that fails is retried with backoff, and after its last allowed * attempt it is marked failed but still retried every few minutes. It stays * pending the whole time, so a consumer that reports freshness never treats * it as done. * * Consumers never poll. They process changes only where the database is * already awake: before a read that needs fresh data, right after a write, or * inside the framework's recurring sweep. See docs/search-architecture.md. */ import { type DbExec } from "../db/client.js"; export declare const RESOURCE_CHANGES_TABLE = "app_resource_changes"; export declare const RESOURCE_CHANGE_CONSUMERS_TABLE = "app_resource_change_consumers"; export declare const RESOURCE_CHANGE_SEQUENCE = "app_resource_change_seq"; export declare const RESOURCE_CHANGED_FUNCTION = "agent_native_app_resource_changed"; /** After this many attempts a change is marked failed. */ export declare const RESOURCE_CHANGE_MAX_ATTEMPTS = 5; export declare const resourceChangesStore: import("../db/store-registry.js").Store; /** * Creates the feed's tables, sequence, and producer function. Listed in * `server/release-schema.ts`; trigger installation calls it too, so the * function a trigger calls always exists before the trigger does. */ export declare function ensureResourceChangeTables(injectedClient?: DbExec): Promise; /** Where a registered resource lives, for generating its triggers. */ export interface ResourceChangeSource { app: string; resourceType: string; table: string; idColumn: string; } export declare function assertResourceKey(value: string, label: string): string; /** * Names of the function and triggers generated for one source. Functions are * schema-wide and the readable part is shortened to fit Postgres's 63 * characters, so a hash of the app, table, and type keeps every source's * names distinct. Installing a source replaces whatever has its names, so * the hash is wide enough that two sources never share them. */ export declare function resourceChangeTriggerNames(source: ResourceChangeSource): { function: string; insertDeleteTrigger: string; updateTrigger: string; truncateTrigger: string; }; /** * SQL that makes every committed insert, update, delete, and truncate on the * source table call the producer function. An update that changes nothing is * skipped. An update that changes the id reports both ids. A truncate * reports every row it removes as deleted. * * "Changes nothing" compares the rows' stored bytes (`*<>`), not their * values: a `json` or `point` column has no equality operator, and comparing * such rows by value would make every update on the table fail. * * Triggers are replaced in place, never dropped and recreated: a write * committed between a drop and a create would never be recorded. */ export declare function resourceChangeTriggerSql(source: ResourceChangeSource): string[]; /** A hash of the capture SQL for a source, which changes whenever it does. */ export declare function resourceChangeCaptureFingerprint(source: ResourceChangeSource): string; /** * Installs change capture for a source and subscribes a consumer to it. * Apps call this from a named migration, so the triggers ship with the app's * schema rather than being created on a request path. * * Creating a trigger waits for every open transaction on the table, and * queues the table's writes behind it while it waits, so each waits at most * a few seconds. Returns false when one gave up; the caller retries later. */ export declare function installResourceChangeCapture(exec: DbExec, source: ResourceChangeSource, consumer: string): Promise; export declare function subscribeResourceChangeConsumer(exec: DbExec, source: Pick, consumer: string): Promise; /** * True when every generated trigger exists on the source table and fires. * A disabled trigger (`ALTER TABLE ... DISABLE TRIGGER`) records nothing, so * it counts as missing. */ export declare function resourceChangeCaptureInstalled(exec: DbExec, source: ResourceChangeSource): Promise; export interface ResourceChangeFeed { consumer: string; app: string; resourceType: string; } export interface ClaimedResourceChange { resourceId: string; seq: string; attempts: number; } /** * A condition a consumer attaches to its claim, complete, and fail * statements. It is checked inside each statement, so once it stops holding, * those statements change nothing. Search uses it so a process running an * older index version can't consume changes queued for a newer one. */ export interface ResourceChangeFence { sql: string; args: unknown[]; } /** * Leases up to `limit` ready changes, oldest first. Rows another claimant is * locking right now are skipped, and the lease expires on its own if this * process dies, so there is nothing to clean up. */ export declare function claimResourceChanges(exec: DbExec, feed: ResourceChangeFeed, limit: number, fence?: ResourceChangeFence): Promise; export declare function compareSeq(a: string, b: string): number; /** * Deletes processed changes, but only rows whose seq is still the one that * was claimed. A change recorded meanwhile stays for the next pass. */ export declare function completeResourceChanges(exec: DbExec, feed: ResourceChangeFeed, changes: readonly ClaimedResourceChange[], fence?: ResourceChangeFence): Promise; /** * Backs off failed changes, doubling the wait up to five minutes. After the * last allowed attempt a change is also marked failed, which a consumer can * report, but it keeps being retried: a failure that was only temporary * clears itself, and a later write to the resource starts it fresh. */ export declare function failResourceChanges(exec: DbExec, feed: ResourceChangeFeed, changes: readonly ClaimedResourceChange[], fence?: ResourceChangeFence): Promise; /** * Two boolean columns for a caller's own SELECT, so checking the feed costs * no extra round trip: `pending` while any change is waiting, leased, or * backing off, and `failing` while any has used up its attempts. */ export declare function resourceChangeBacklogColumns(feed: ResourceChangeFeed): { sql: string; args: unknown[]; }; /** True while any change at or below `seq` is still pending for this feed. */ export declare function hasPendingResourceChanges(exec: DbExec, feed: ResourceChangeFeed, options?: { atOrBelowSeq?: string; }): Promise; /** * Records a change for every row of the source table, for this consumer * only, and returns a seq at or above every one it assigned. Consumers use * it to rebuild from scratch. * * A change already queued is replaced by a fresh one, even if another * process has it leased or it has failed: that work was done for the old * consumer state, so it must not complete what the rebuild queued. */ export declare function enqueueAllResourceChanges(exec: DbExec, source: ResourceChangeSource, consumer: string, reason: string): Promise; type DrainHook = () => Promise; /** * Registers work to run right after a request that changed data, while the * database is known to be awake. Returns an unregister function. */ export declare function registerAfterWriteDrain(id: string, drain: DrainHook): () => void; /** * Runs registered after-write drains without delaying the caller. On * serverless the request's `waitUntil` keeps the function alive for them. */ export declare function runAfterWriteDrains(waitUntil?: (promise: Promise) => void): void; export {};