import { EventEmitter } from "node:events"; import { type DbExec } from "../db/client.js"; import { type RecurringSweepContext } from "../jobs/sweep-hooks.js"; import { type SyncEventsPruneOptions, type SyncEventsPruneResult } from "./sync-events-prune.js"; export interface ChangeEvent { version: number; cursorId?: string; source: string; type: string; key?: string; owner?: string; orgId?: string; resourceType?: string; resourceId?: string; visibility?: "public"; [k: string]: unknown; } export interface TransactionalChange { persist(transaction: DbExec): Promise; isPersisted(): boolean; publish(): ChangeEvent; } export declare const SYNC_EVENTS_PRUNE_SWEEP_ID = "sync-events-prune"; export declare const SYNC_EVENTS_PRUNE_STATE_CREATE_SQL = "\n CREATE TABLE IF NOT EXISTS sync_events_prune_state (\n id INT PRIMARY KEY,\n cursor_version BIGINT NOT NULL DEFAULT 0,\n lease_owner TEXT,\n lease_expires_at BIGINT,\n last_run_at BIGINT,\n last_checked_at BIGINT,\n last_success_at BIGINT,\n last_deleted BIGINT NOT NULL DEFAULT 0,\n total_deleted BIGINT NOT NULL DEFAULT 0,\n backlog BOOLEAN NOT NULL DEFAULT FALSE,\n consecutive_failures INT NOT NULL DEFAULT 0,\n last_error TEXT,\n last_error_at BIGINT\n )\n"; export declare const DURABLE_LEGACY_DB_CHECK_INTERVAL_MS = 30000; export declare const POLL_CHANGE_EVENT = "poll-change"; type SyncCursor = { version: number; id: string; }; export type ChangeVisibility = "visible" | "hidden" | "pending"; export type ChangeReadResult = { version: number; events: ChangeEvent[]; cursor?: string; cursorLimited?: boolean; /** Set when the read stopped at an event whose access check is unresolved. */ accessPending?: boolean; }; export type AccessResolver = (resourceType: string, resourceId: string, ctx: { userEmail: string; orgId: string | undefined; }) => Promise; export interface AppSyncStateOptions { getDb?: () => DbExec; resolveAccess?: AccessResolver; deterministicEventIds?: boolean; dbAssignedVersions?: boolean; accessAllowTtlMs?: number; } export declare class AppSyncState { private readonly getDb; private readonly resolveAccessFn; private readonly deterministicEventIds; private readonly dbAssignedVersions; private recordChain; private dbVersionFallbacks; private warnedListenerThrow; private version; private latestCursor; private readonly buffer; private readonly pollEmitter; private syncEventsInitPromise; private syncEventsInitFailures; private lastDurablePrune; private pruneInFlight; private durableWriteFailures; private allocatorReseedFailures; private versionSeeded; private lastDbCheck; private checkPromise; private lastAppStateTs; private lastSettingsTs; private lastExtensionsTs; private lastExtensionsUpdatedAt; private lastExtensionMarkerTs; private lastActionMarkerTs; private replayActionMarkerOnce; private lastScreenRefreshTs; private lastScreenRefreshSessionId; private screenRefreshInitialized; private screenRefreshHasMore; private localEmittersWired; private readonly accessCache; private readonly accessInFlight; private readonly accessInvalidationEpoch; private readonly accessAllowTtlMs; constructor(options?: AppSyncStateOptions); private accessCacheTtl; private durableEventId; getVersion(): number; getPollBaseline(useDurableEvents: boolean): Promise<{ version: number; cursor: string; }>; getPollEmitter(): EventEmitter; wireLocalEmitters(): void; ensureSyncEventsTable(): Promise; /** Single-flight per process; cross-instance exclusion is the state-row lease. */ pruneDurableEvents(client: DbExec, options?: SyncEventsPruneOptions): Promise; private prunePiggyback; /** Recurring-sweep driver: a failed prune must fail the tick, not pass as idle. */ pruneDurableEventsForSweep(context: RecurringSweepContext): Promise; private reportDurableWriteFailure; private reportAllocatorReseedFailure; persistSyncEvent(event: ChangeEvent, dedupeKey?: string, presetId?: string): Promise; private persistWithDbAssignedVersion; private recoverCommittedVersion; private alignVersionAllocator; readMaxSyncEventVersion(): Promise; readMinSyncEventVersion(): Promise; invalidateCollabAccessCache(resourceType: string, resourceId: string): void; private setAccessCache; private scheduleAccessCheck; __resetAccessCacheForTests(): void; /** * Decide whether a poll/SSE change event should be delivered to a user. * * SYNC-CACHE VARIANT — WHY THIS IS SYNCHRONOUS: * This function is called on hot, synchronous paths: the SSE emitter callback * `push(change)` in poll-events.ts (fires per event) and the * `getChangesSinceForUser` loop in this file. Making it async would be * invasive. Instead, for the access-aware branch we consult an in-memory * cache and, on a miss, fire a NON-BLOCKING background access check and * return `false` for the current event. Callers that must not lose the event * use `getChangeVisibilityForUser` and wait for the pending check (the SSE * handler and `getCombinedChangesSinceForUser` do); this boolean form alone * would drop the first event for a fresh (user, resource) pair, and a live * stream does not poll to recover it. * * Security: a cache MISS returns `false`, so we NEVER deliver to a user before * their access has been affirmatively confirmed by the resolver — the same * authority that gates the HTTP routes. Errors fail closed (cached deny). * Organization membership alone does not grant access to a resource-scoped event. */ canSeeChangeForUser(event: Pick, userEmail: string, orgId: string | undefined): boolean; private waitForAccessChecks; /** * Like `getChangeVisibilityForUser`, but a "pending" verdict waits (bounded) * for this event's own access check and answers again, so a caller that * cannot poll later does not lose the event. Still "pending" after the wait * means the check did not finish. */ resolveChangeVisibilityForUser(event: Pick, userEmail: string, orgId: string | undefined, timeoutMs: number): Promise; getChangeVisibilityForUser(event: Pick, userEmail: string, orgId: string | undefined): ChangeVisibility; recordChange(event: { source: string; type: string; key?: string; [k: string]: unknown; }, opts?: { dedupeKey?: string; }): void; prepareTransactionalChange(event: { source: string; type: string; key?: string; [k: string]: unknown; }): Promise; private commitEntry; private recordWithDbVersion; private commitEntryForChain; private recordExtensionChanges; private recordActionChanges; getChangesSince(since: number): { version: number; events: ChangeEvent[]; }; getChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, cursor?: SyncCursor): ChangeReadResult; getDurableChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, cursor?: SyncCursor): Promise; getCombinedChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined, useDurableEvents: boolean, cursor?: SyncCursor): Promise; private readCombinedChangesSinceForUser; seedVersionFromDb(): Promise; checkExternalDbChanges(options: { durableEvents: boolean; }): Promise; private doCheckExternalDbChanges; } declare function hostedRealtimeTransportEnabled(): boolean; export declare const __hostedRealtimeTransportEnabledForTests: typeof hostedRealtimeTransportEnabled; export declare function getDefaultAppSyncState(): AppSyncState; export declare const syncEventsStore: import("../db/store-registry.js").Store; export declare const syncEventsPruneStateStore: import("../db/store-registry.js").Store; export declare function getVersion(): number; export declare function getCurrentPollBaseline(state?: AppSyncState): Promise<{ version: number; cursor: string; }>; export declare function getPollEmitter(): EventEmitter; export declare function invalidateCollabAccessCache(resourceType: string, resourceId: string): void; export declare function __resetCollabAccessCacheForTests(): void; export declare function canSeeChangeForUser(event: Pick, userEmail: string, orgId: string | undefined): boolean; export declare function recordChange(event: { source: string; type: string; key?: string; [k: string]: unknown; }): void; export declare function prepareTransactionalChange(event: { source: string; type: string; key?: string; [k: string]: unknown; }): Promise; export declare function getChangesSince(since: number): { version: number; events: ChangeEvent[]; }; export declare function getChangesSinceForUser(since: number, userEmail: string, orgId: string | undefined): ChangeReadResult; export declare function registerSyncEventsPruneSweep(state: AppSyncState): () => void; export declare function createPollHandler(state?: AppSyncState): import("h3").EventHandlerWithFetch>; export {};