/** * Periodic sync-maintenance loop for the coordinator. * * `@quereus/sync` schedules nothing — the host owns cadence (`docs/sync.md` * § Who drives the sweep). The per-store pass shape (sweep order, per-sweep * error isolation) lives in the library as `runSyncMaintenancePass`; what is * coordinator-specific and therefore lives here is the multi-tenant fan-out: * one pass visits every database currently open in the `StoreManager`. * * Without this loop, tombstones and quarantined changes accumulate for the life * of every hosted database even though `retentionHorizonMs` says they expire. */ import { type SyncMaintenanceTarget } from '@quereus/sync'; /** * Cadence of the coordinator's maintenance loop (1 hour). * * Nothing here is latency-sensitive. The only sweep quoomb-web needs promptly is * `drainHeldChanges` (a reappeared table's held edits gate a visible UI update), * and that one is inert on a relay — no `getTableSchema` oracle to tell it which * held tables are back, so it returns 0. `evictExpiredBasisTables` is likewise * inert without a `dropLocalTable` callback. What actually reclaims here is * `pruneTombstones` / `pruneQuarantine` — both at horizon granularity (default 30 * days) — plus `repairChangeLog`, which needs neither oracle nor callback and so * does real work on a relay too. Hourly expires records well inside the horizon * while costing nothing when there is nothing to reclaim (every sweep is * zero-cost on an empty scan). * A documented constant rather than a `CoordinatorConfig` knob, * matching the other internal timings in this package (store idle timeout, * cleanup interval); promote it to config only if a deployment needs to tune it. */ export declare const COORDINATOR_MAINTENANCE_INTERVAL_MS: number; /** * The slice of `StoreManager` the maintenance pass needs. Structural, so tests * can drive the pass without opening real LevelDB stores; a `StoreManager` is * assignable to it. */ export interface MaintenanceStoreSource { /** Snapshot of the database IDs currently open. */ openDatabaseIds(): string[]; /** Pin an already-open store. Returns undefined if it is not open — never opens one. */ acquireIfOpen(databaseId: string): { syncManager: SyncMaintenanceTarget; } | undefined; /** Drop a pin taken by `acquireIfOpen`. */ releasePin(databaseId: string): void; } /** Reports a failed sweep for one database. `error` is the thrown value verbatim. */ export type StoreMaintenanceLogger = (databaseId: string, step: string, error: unknown) => void; /** * One maintenance pass over every currently-open store. Never rejects: a * failing sweep is isolated by the library pass, and a failing *store* is * isolated here, so one bad tenant cannot starve the rest. * * NOTE: only stores already resident in the StoreManager are swept — a database * that is closed on disk is skipped until it is next legitimately opened (by a * client connecting) and a later tick catches it. An eager scan of every * database directory was rejected: it would open — and, with disk eviction on, * re-download from S3 — databases no client is using, turning a cheap * housekeeping tick into an unbounded I/O storm. The cost is that a database * nobody ever opens again keeps its expired metadata on disk; the fix for that * is deleting the database, not sweeping it. */ export declare function runCoordinatorMaintenancePass(source: MaintenanceStoreSource, log?: StoreMaintenanceLogger): Promise; /** * Timer around {@link runCoordinatorMaintenancePass}. Passes are single-flight: * a tick that fires while the previous pass is still running is a clean no-op, * so a pass slower than the interval never overlaps itself. * * No immediate pass on `start()` — unlike the browser worker, the coordinator * has no stores open at startup (they open lazily as clients connect), so an * eager pass would sweep nothing. */ export declare class CoordinatorMaintenanceLoop { private readonly source; private readonly intervalMs; private readonly log; private timer; /** * The pass currently running, or null when idle. Doubles as the single-flight * flag and as the handle `stop()` awaits. * * NOTE: deliberately not `@quereus/sync`'s `createSyncMaintenanceTicker`, whose * collapsed tick resolves immediately rather than joining the running pass — * right for a fire-and-forget browser timer, wrong here, where `stop()` must * know the sweep has finished before `StoreManager.shutdown()` closes the * stores it is reading. The shared piece — the per-store pass itself — is still * the library's `runSyncMaintenancePass`. */ private inFlight; constructor(source: MaintenanceStoreSource, intervalMs?: number, log?: StoreMaintenanceLogger); /** Arm the loop. Idempotent — re-arming does not stack a second timer. */ start(): void; /** * Disarm the loop and await any pass already in flight, so the caller can * close stores knowing no sweep is still reading them. */ stop(): Promise; /** * Run one pass now, or join the pass already running. Used by the timer and * available for tests / an explicit kick. */ runOnce(): Promise; } //# sourceMappingURL=maintenance.d.ts.map