import type { NoydbStore, DirtyEntry, ConflictStrategy, CollectionConflictResolver, PushOptions, PullOptions, PushResult, PullResult, SyncStatus, SyncTargetRole } from '../../kernel/types.js'; import type { NoydbEventEmitter } from '../../kernel/events.js'; import type { SyncPolicy } from '../../kernel/sync-policy.js'; import { SyncScheduler } from '../../kernel/sync-policy.js'; /** #650 Task 4 (#647) — the declared reserved-lookup (`_dict_*`/`_lookup_*`) collection-name * registry a `SyncEngine` enumerates on pull. Explicit, not a blanket underscore-glob — other * `_`-prefixed namespaces keep their `loadAll`-skip semantics untouched. */ export interface ReservedLookupSource { /** Declared reserved-lookup collection names to enumerate on pull. */ collections(): readonly string[]; } /** Sync engine: dirty tracking, push, pull, conflict resolution, scheduling. */ export declare class SyncEngine { private readonly local; private readonly remote; private readonly strategy; private readonly emitter; private readonly vault; readonly role: SyncTargetRole; readonly label: string | undefined; private dirty; private lastPush; private lastPull; private loaded; private autoSyncInterval; private isOnline; /** Sync scheduler. Manages push/pull timing. */ readonly scheduler: SyncScheduler | null; /** Per-collection conflict resolvers registered by Collection instances. */ private readonly conflictResolvers; /** * Expands a collection-name filter to include its satellite pair partner(s) * (spec #591 convergence rule 5b) — wired from vault satellite declaration * via `setPairExpander`. `undefined` when no satellite has ever been * declared for this vault: identity behavior, zero change for non-satellite * users. */ private pairExpander?; /** #598: refreshes Collection in-memory views after a sync-applied local write. Wired by the * vault at open. `action` (#640): `'delete'` for a pulled tombstone/delete-marker, `'put'` * otherwise — classified at `applyRemote`, the one choke point that holds the envelope. */ private cacheInvalidator?; /** Wire the Collection-cache invalidation hook (#598). Same injection pattern as `setPairExpander`. */ setCacheInvalidator(fn: (collection: string, id: string, action: 'put' | 'delete') => Promise): void; /** #638 Task 4: opens/flushes the vault's graph-dispatch touch batch around a whole `pull()`/ * `push()` call, so N `applyRemote`d records feeding the same derivation/rollup/MV target * recompute once. Wired by the vault at open, mirroring `cacheInvalidator`. */ private graphBatchController?; /** Wire the graph-dispatch batch controller (#638 Task 4). Same injection pattern as `setCacheInvalidator`. */ setGraphBatchController(controller: { begin(): void; flush(): Promise; }): void; /** #650 Task 4 (#647): declared reserved-lookup collections `pull()` enumerates explicitly (the * store's `list()` does not skip `_`-prefixed names, unlike `loadAll()`). Wired by the vault at * open, same injection pattern as `setCacheInvalidator`/`setGraphBatchController`. */ private reservedLookup?; /** Wire the reserved-lookup source (#650 Task 4). */ setReservedLookupSource(source: ReservedLookupSource): void; /** #653: expands a (pair-expanded) collection-name filter to include the reserved `_dict_*` * collections those names depend on via their lookup fields — mirrors `pairExpander` so a * partial `push`/`pull({collections:[...]})` never drops the dictionary a named collection's * labels/membership rely on. Wired unconditionally from the vault's `dictKeyFieldRegistry` via * `setReservedDictExpander` (returns `[]` when the vault has no dict-backed collections); only * `undefined` on a standalone engine that was never vault-attached. */ private reservedDictExpander?; /** Wire the reserved-dict expansion function (#653). Same injection pattern as `setPairExpander`. */ setReservedDictExpander(expander: (names: readonly string[]) => readonly string[]): void; constructor(opts: { local: NoydbStore; remote: NoydbStore; vault: string; strategy: ConflictStrategy; emitter: NoydbEventEmitter; syncPolicy?: SyncPolicy; role?: SyncTargetRole; label?: string; }); /** Start the sync scheduler. Called after vault is fully opened. */ startScheduler(): void; /** Stop the sync scheduler. Called on close. */ stopScheduler(): void; /** * Register a per-collection conflict resolver. * Called by Collection when `conflictPolicy` is set. * * Pair-coupled (#591 rule 5b): registering for one satellite-pair member * registers the same resolver for both — the pair converges under one * resolution policy. Expands with whatever the pair-expander knows *at this * call*; resolvers registered before their pair existed are covered by * `remirrorPairResolvers` at pair-registration time. */ registerConflictResolver(collection: string, resolver: CollectionConflictResolver): void; /** * Wire a satellite pair-expansion function (vault registration, #591 Task 11). * The closure re-reads the live `SatelliteRegistry` on every call, so pairs * declared after this is set are still picked up by `push`/`pull` filters. */ setPairExpander(expander: (names: readonly string[]) => readonly string[]): void; /** * Retroactively mirror per-collection conflict resolvers across a newly * registered satellite pair (#591 rule 5b) — covers the NORMAL declaration * order, where the base's `conflictPolicy` resolver was registered before * the satellite was declared (so the call-time expansion in * `registerConflictResolver` saw no pair yet). Tie-break: when BOTH members * already carry (different) resolvers, the FIRST name in `names` wins — * callers pass `[base, satellite]`, so the base's resolver is canonical. * Idempotent; a later explicit registration on either member still * overwrites both (last-wins, via the call-time expansion above). */ remirrorPairResolvers(names: readonly string[]): void; /** Record a local change for later push. */ trackChange(collection: string, id: string, action: 'put' | 'delete', version: number): Promise; /** Remove a dirty entry (satellite fan-out revert cleanup — spec #591). */ removeDirty(collection: string, id: string): Promise; /** Push dirty records to remote adapter. Accepts optional `PushOptions` for partial sync. */ push(options?: PushOptions): Promise; /** Pull remote records to local adapter. Accepts optional `PullOptions` for partial sync. */ pull(options?: PullOptions): Promise; /** Bidirectional sync: pull then push. */ sync(options?: { push?: PushOptions; pull?: PullOptions; }): Promise<{ pull: PullResult; push: PushResult; }>; /** * Push a specific subset of dirty entries (for sync transactions, ). * Entries are matched by collection+id from the dirty log; matched entries * are removed from the dirty log on success. */ pushFiltered(predicate: (entry: DirtyEntry) => boolean): Promise; /** Get current sync status. */ status(): SyncStatus; /** Start auto-sync: listen for online/offline events, optional periodic sync. */ startAutoSync(intervalMs?: number): void; /** Stop auto-sync and scheduler. */ stopAutoSync(): void; private handleOnline; private handleOffline; /** Apply an envelope to the local store and refresh in-memory views (#598). `action` (#640): * classified HERE — the one choke point that still holds the envelope — so the * cacheInvalidator seam (and, downstream, the dispatch wave) can tell a pulled delete from an * ordinary put. `isTombstoneShape` covers a forgotten-elsewhere record arriving as a shred; * `isDeleteMarker` covers an ordinary (#589) delete — both route to the SAME 'delete' action * (sync delete ≠ forget: freshness only, no shred/residue channel on the receiving side). */ private applyRemote; /** Record + emit a tombstone enforcement (#590). */ private reportErasure; /** * Re-assert a local tombstone over a live remote envelope (#590): the shred * wins in both directions, regardless of `_v`. Bumps the tombstone to the * suppressed envelope's `_v` when higher so per-key version counters stay * monotonic on every store; `_by` (the shredding actor) is preserved. */ private reassertTombstone; /** * Resolve a conflict, checking per-collection resolvers first, * then falling back to the db-level `ConflictStrategy`. * * Returns the resolved `Conflict` object (possibly with `resolve` set for * manual mode) and a `handled` discriminant: * - `'local'` — keep the local envelope; push it to remote. * - `'remote'` — keep the remote envelope; update local. * - `'merged'` — a custom merge fn produced a new envelope stored as `conflict.local`. * - `'deferred'` — manual mode, resolve was not called synchronously. */ private handleConflict; /** DB-level ConflictStrategy resolution (legacy, kept for backward compat). */ private legacyResolve; private ensureLoaded; private persistMeta; }