/** * `SyncularAdmin` is the operator-facing read surface over the server core. * It is a read-only, partition-scoped, JSON-able query layer over * `ServerStorage`, the optional segment/blob store stats, and an in-memory * event ring. It delivers the 80% operator value (who's connected, what's * flowing, horizon health, the event tail) as a handful of queries in the * server package — no separate UI package, no framework, and no wire-protocol * surface. This host surface is mirrored in the server README. * * Nothing here is on the sync hot path. Every method is a plain read; the * additive optional storage/store methods it depends on are documented as * such and this module fails loud (a thrown `Error`) when a backend lacks * one, so a host wiring an unsupported store learns immediately rather than * getting a silently-empty console. */ import type { BlobStore, BlobStoreStats } from './blob-store.js'; import { type SyncServerConfig } from './context.js'; import type { SyncularServerEvent } from './events.js'; import type { RingBufferEvents, RingEventQuery } from './events-ring.js'; import type { LeaseRecord, LeaseStore } from './lease-store.js'; import { type RetentionPolicy } from './prune.js'; import { type ServerSchema } from './schema.js'; import type { SegmentStore, SegmentStoreStats } from './segment-store.js'; import type { CommitMetadata, ReactionStatus, ScopeCommitActivity, ServerStorage, StoredReaction } from './storage.js'; /** A connected/known client as the console sees it (§4.5, §8.1). */ export interface AdminClient { readonly clientId: string; readonly actorId: string; readonly cursor: number; /** * Commits the client has not pulled yet: `maxCommitSeq − max(cursor, 0)`. * The first number an operator wants for "why is this client stale". */ readonly lag: number; readonly updatedAtMs: number; readonly subscriptions: readonly { readonly id: string; readonly table: string; readonly scopes: Record; }[]; /** True when the cursor record was touched within the active window. */ readonly active: boolean; } /** One client's drill-down: record + lease + its slice of the event tail. */ export interface AdminClientDetail { readonly clientId: string; readonly exists: boolean; /** Present iff `exists` — the same shape `listClients` returns. */ readonly client?: AdminClient; /** The client's §7.3 lease, when a lease store is wired and one exists. */ readonly lease?: LeaseRecord; /** Recent ring events carrying this clientId (newest first). */ readonly events: readonly SyncularServerEvent[]; } /** Ring-derived request/push aggregates over a trailing window. */ export interface AdminMetrics { readonly partition: string; readonly windowMs: number; /** Wall-clock the aggregation ran at (window end). */ readonly atMs: number; readonly requests: { readonly count: number; readonly perMinute: number; readonly errorCount: number; /** errors ÷ requests, 0 when the window is empty. */ readonly errorRate: number; readonly p50Ms: number; readonly p95Ms: number; }; readonly pushes: { readonly applied: number; readonly rejected: number; readonly conflicted: number; }; /** * Request counts split into `counts.length` equal buckets, oldest first — * the console's sparkline. `errors` marks the error share per bucket. */ readonly buckets: { readonly widthMs: number; readonly counts: readonly number[]; readonly errors: readonly number[]; }; } /** One partition's row in the fleet view (`listPartitions` + horizon math). */ export interface AdminPartitionOverview { readonly partition: string; readonly maxCommitSeq: number; readonly horizonSeq: number; readonly retainedCommits: number; readonly knownClients: number; /** Clients whose cursor record was touched within the active window. */ readonly activeClients: number; readonly recommendation: 'up-to-date' | 'prune-recommended'; } export interface AdminRowInspection { readonly table: string; readonly rowId: string; readonly exists: boolean; readonly serverVersion?: number; readonly scopes?: Record; /** blobIds this row currently references (§5.9.4), when the store tracks them. */ readonly referencedBlobIds?: readonly string[]; } export interface AdminHorizonStatus { readonly partition: string; readonly maxCommitSeq: number; readonly horizonSeq: number; /** Commits still below the retained log's tail — the pruneable backlog. */ readonly retainedCommits: number; /** min(cursor) over active clients, or null when none are active. */ readonly activeCursorFloor: number | null; /** The horizon a prune pass would advance to right now (§4.6). */ readonly recommendedHorizonSeq: number; readonly recommendation: 'up-to-date' | 'prune-recommended'; } export interface AdminListCommitsOptions { readonly afterSeq?: number; readonly limit?: number; readonly table?: string; } export interface AdminScopeActivityOptions { readonly limit?: number; } export interface AdminListReactionsOptions { readonly statuses?: readonly ReactionStatus[]; readonly types?: readonly string[]; readonly limit?: number; } export interface AdminStats { readonly segments?: SegmentStoreStats; readonly blobs?: BlobStoreStats; } export interface SyncularAdminOptions { readonly storage: ServerStorage; /** * The server schema. When present, row reads (`inspectRow`) ensure the * relational row tables exist first — needed when the admin runs against * a storage instance that has not served a sync request yet. */ readonly schema?: ServerSchema; /** The event ring feeding the event tail. Absent ⇒ `events()` is empty. */ readonly ring?: RingBufferEvents; readonly segments?: SegmentStore; readonly blobs?: BlobStore; /** The §7.3 lease store — feeds the client drill-down's lease read. */ readonly leases?: LeaseStore; /** Retention policy for horizon recommendation (defaults to §4.6). */ readonly retention?: Partial; /** Epoch-ms clock (defaults to `Date.now`) — active-window math. */ readonly clock?: () => number; } /** The read-only console query surface. Construct one per host process. */ export declare class SyncularAdmin { #private; constructor(options: SyncularAdminOptions); /** * Build an admin over a `SyncServerConfig`, reusing its storage / segment * / blob store / clock. Pass the ring separately (it is an events sink, * composed into the config's `events` by the host — see `composeEvents`). */ static fromConfig(config: SyncServerConfig, extra?: { ring?: RingBufferEvents; retention?: Partial; }): SyncularAdmin; /** The known clients for a partition (cursor, lag, subscriptions). */ listClients(partition: string): Promise; /** * One client's drill-down: its record (with lag), its §7.3 lease when a * lease store is wired, and its recent slice of the event tail. Answers * "why is this client stale" in a single read. */ clientDetail(partition: string, clientId: string, options?: { readonly eventLimit?: number; }): Promise; /** Commit-log metadata (no payloads), newest first. */ listCommits(partition: string, options?: AdminListCommitsOptions): Promise; /** Pending, leased, completed, and dead-lettered durable reactions. */ listReactions(partition: string, options?: AdminListReactionsOptions): Promise; /** * Inspect a single row: current server_version, stored scopes, and the * blobIds it references (when the store tracks references). Payload bytes * are deliberately NOT decoded — the console shows metadata, not content. */ inspectRow(partition: string, table: string, rowId: string): Promise; /** * Recent commits touching one scope key (`variable:value`, e.g. * `project:p1`) — routed through the change-scope index, never a scan. */ scopeActivity(partition: string, scopeKey: { variable: string; value: string; }, options?: AdminScopeActivityOptions): Promise; /** * Horizon health for a partition: current horizon, retained-commit * backlog, active cursor floor, and the horizon a prune pass would reach * now (§4.6) plus a coarse recommendation. */ horizonStatus(partition: string): Promise; /** Segment + blob store counters where the stores expose them. */ stats(partition: string): Promise; /** Segment store counters alone (undefined when unsupported/unset). */ segmentStats(): Promise; /** Blob store counters for a partition (undefined when unsupported/unset). */ blobStats(partition: string): Promise; /** True when this admin has an event ring wired (the event tail is live). */ get hasEventStream(): boolean; /** The event tail from the ring buffer (newest first). Empty when unwired. */ events(query?: RingEventQuery): SyncularServerEvent[]; /** * Subscribe to events as they land in the ring (the SSE tail). Returns * the unsubscribe function, or `undefined` when no ring is wired — a * host can branch on that the same way `hasEventStream` reports it. */ subscribeEvents(listener: (event: SyncularServerEvent) => void): (() => void) | undefined; /** * Request/push health over a trailing window, derived entirely from the * ring (zero new server state): rates, error share, duration percentiles, * and per-bucket counts for the console's sparkline. Only events carrying * this `partition` count. Empty (all zeros) when no ring is wired. */ metrics(partition: string, options?: { readonly windowMs?: number; readonly buckets?: number; }): AdminMetrics; /** Every authenticated partition in the storage-backed registry. */ listPartitions(): Promise; /** * The fleet view: one row per known partition — retained backlog, client * counts, prune recommendation. A cross-partition read (every other * method is partition-scoped); host authorization should account for it. */ partitionsOverview(): Promise; }