/** * Collection-level index maintenance — the eager/unique rebuild-from-cache * helpers, the full `rebuildIndexes` / `reconcileIndex` repair surface, and the * persisted `_idx//` side-car maintenance fired from the write * path. * * Eager mode keeps an in-memory `CollectionIndexes` + `UniqueConstraintSet`; * lazy mode keeps durable encrypted side-cars whose in-memory mirror is a * `PersistedCollectionIndex`. These functions write/repair both. The unique- * constraint correctness and the side-car drift reconciliation are the parts to * watch: behaviour here is byte-identical to the inline code it replaced. * * Every function takes a small {@link IndexingContext} (the exact `this.*` the * moving methods touched) instead of `this`, mirroring the `record-keys/` * siblings. The eager `cache` Map and the three index/constraint mirror objects * are passed by reference (the SAME instances `Collection` owns, never copied) * so maintenance always mutates the live mirrors the query path reads. The * `persistedIndexesLoaded` flag and `ensure*` hydration stay collection-resident * and are reached via callbacks. * * Internal service — not exported as a `@noy-db/hub/*` subpath. */ import type { NoydbStore, EncryptedEnvelope } from '../../kernel/types.js'; import type { RecordCodec } from '../../kernel/enclave/index.js'; import type { NoydbEventEmitter } from '../../kernel/events.js'; import type { CollectionIndexes } from './eager-indexes.js'; import type { UniqueConstraintSet } from './unique-constraints.js'; import { type PersistedCollectionIndex } from './persisted-indexes.js'; /** Everything the moving index-maintenance methods touched on `this.*`. */ export interface IndexingContext { /** Collection name — the canonical record namespace + error context. */ readonly name: string; /** Vault namespace the records + side-cars live under. */ readonly vault: string; /** The ciphertext store. */ readonly adapter: NoydbStore; /** The record codec — decrypts records + side-car bodies, encrypts side-cars. */ readonly codec: RecordCodec; /** The eager working-set cache (SHARED `Map` reference, never copied). */ readonly cache: Map; /** True in lazy mode — eager rebuild short-circuits, reconcile requires it. */ readonly lazy: boolean; /** Event emitter — surfaces `index:write-partial` on side-car write failure. */ readonly emitter: NoydbEventEmitter; /** The in-memory eager index mirror, or null (SHARED reference). */ readonly indexes: CollectionIndexes | null; /** The in-memory unique-constraint mirror, or null (SHARED reference). */ readonly uniqueConstraints: UniqueConstraintSet | null; /** The lazy-mode persisted-index mirror, or null (SHARED reference). */ readonly persistedIndexes: PersistedCollectionIndex | null; /** Hydrate the eager cache before rebuilding from it. */ ensureHydrated(): Promise; /** Bulk-load the persisted-index mirror from side-cars (lazy mode). */ ensurePersistedIndexesLoaded(): Promise; /** Set the collection-resident `persistedIndexesLoaded` flag. */ setPersistedIndexesLoaded(value: boolean): void; } /** * Rebuild the eager in-memory `CollectionIndexes` from the current cache. * Called after any bulk hydration; incremental put/delete updates go through * `indexes.upsert()`/`.remove()` directly, so this only fires for full reloads. */ export declare function rebuildEagerIndexesFromCache(ctx: IndexingContext): void; /** * Rebuild unique-constraint maps from the current in-memory cache. * Called after any bulk hydration alongside `rebuildEagerIndexesFromCache`. */ export declare function rebuildUniqueConstraintsFromCache(ctx: IndexingContext): void; /** * Rebuild every declared index from scratch — eager refresh from cache, or a * full lazy-mode side-car teardown + rewrite. NOT incremental; for per-field * drift repair use {@link reconcileIndex}. */ export declare function rebuildIndexes(ctx: IndexingContext): Promise; /** * Compare the persisted `_idx//*` side-cars against the canonical * records for a single field, reporting the drift (and optionally repairing * it). Lazy mode only — eager mode throws (the in-memory index cannot drift). */ export declare function reconcileIndex(ctx: IndexingContext, field: string, opts?: { dryRun?: boolean; }): Promise<{ field: string; missing: string[]; stale: string[]; applied: number; }>; /** * Write / update / delete the `_idx//` side-cars for every * declared persistence-index field after a successful main-record `put()`. * Called AFTER `adapter.put()` of the main record succeeds; side-car write * failures surface as `index:write-partial` and do NOT fail the put. */ export declare function maintainPersistedIndexesOnPut(ctx: IndexingContext, id: string, newRecord: T, previousRecord: T | null, version: number): Promise; /** * Tear down `_idx//` side-cars for a deleted record. * Mirror state updates regardless of adapter outcome; adapter failures * surface on `index:write-partial` the same way put does. */ export declare function maintainPersistedIndexesOnDelete(ctx: IndexingContext, id: string, previousRecord: T): Promise; /** * @internal — hard-delete this record's persisted `_idx//` * side-cars for the erasure path. `forget()` crypto-shreds the body but * keeps the collection DEK, under which these side-cars are encrypted — so * without this they leave the indexed field VALUES readable after a "forget". * * Content-free: the side-car id is `encodeIdxId(def.key, id)`, so it needs no * body decode (the body is being shredded). Eager mode has no durable side-car * → no-op. The in-memory mirror is left as-is: it is ephemeral (rebuilt from * the now-deleted side-cars on reopen) and live reads skip the tombstone, so a * stale mirror hit cannot surface the erased record. Returns the count deleted * + the `def.key`s whose delete FAILED (residue that still leaks the value). */ export declare function purgePersistedIndexes(ctx: IndexingContext, id: string): Promise<{ purged: number; residue: string[]; }>; /** * Sync this collection's indexes after a tier move (#709). Persisted * `_idx//` side-cars are always encrypted under the * tier-0 DEK regardless of the record's own tier, and the eager in-memory * `CollectionIndexes` mirror holds plaintext bucket values too — so both * leak an elevated record's indexed field values unless swept the same way * `purgePersistedIndexes` already sweeps them for `forget()` ("forget() * crypto-shreds the body but keeps the collection DEK, under which these * side-cars are encrypted — so without this they leave the indexed field * VALUES readable", `purgePersistedIndexes` above). * * `record === null` — the record just left tier 0: purge its persisted * side-cars (content-free, no decrypt needed) and drop its eager-mirror * entry, read from `ctx.cache` (the caller's pre-write value — the caller * MUST invoke this before evicting/overwriting that cache entry, so it * still reflects the value being replaced). * * `record` given — the record is tier-0 again: (re)build its entries from * that record, using the SAME pre-write `ctx.cache` read as the previous * value so a stale bucket (e.g. a same-tier `putAtTier` value change) is * cleaned up rather than left as a false-positive hit. `version` stamps * the rebuilt side-car's own envelope version. * * `priorEnvelope` (#720) — the RAW envelope the caller read BEFORE its own * overwrite landed (`putAtTier`'s `existing` / `demote`'s pre-move * `envelope`), for the `ctx.cache`-miss lazy case: by this call's time the * adapter already holds the NEW envelope, so a `ctx.adapter.get()` here * would only ever re-observe the write just made — useless for recovering * what a same-tier `putAtTier` may have dropped. Optional: the null-record * branches (elevate / demote-to-intermediate / putAtTier(tier>0)) never * need it — their only prior-dependent step, the eager `indexes.remove` * below, already has what it needs from `ctx.cache` (eager mode is * unaffected by the raw-envelope overwrite; lazy mode has no eager mirror). * * No-ops fast when the collection has neither index kind declared. */ export declare function syncTierIndexes(ctx: IndexingContext, id: string, record: T | null, version: number, priorEnvelope?: EncryptedEnvelope): Promise;