/** * FeltDB State-First Collections API * * Provides a high-level, state-oriented collection abstraction * that makes the database disappear from the programming model. * * Collections now use reactive dependency graphs instead of polling * for efficient, real-time state propagation. */ import type { JsDb } from './feltdb.js'; import { type FreshnessCapability, type Revision } from './freshness.js'; import type { IndexConfig, CollectionQueryPlan } from './index-types.js'; export type Predicate = (item: T) => boolean; export type Subscriber = (items: T[]) => void; export interface CollectionFindOptions { orderBy?: Array<{ field: string; direction: 'asc' | 'desc'; }>; limit?: number; cursor?: string; } /** * How a refresh decided whether the local cache could be reused. * * Reporting only, and deliberately explicit about *why* the runtime was * queried, so a benchmark or a caller can tell a validated cache hit from a * fallback that happens to be fast. */ export interface FreshnessCheck { /** `revision` when a revision decided it; `refresh` when the runtime was asked for the data. */ validation: 'revision' | 'refresh'; /** True only when the runtime was not queried for the collection at all. */ servedFromCache: boolean; /** The revision the cache now corresponds to, when there is one. */ revision?: Revision; /** Why a revision was not used. */ reason?: string; } /** * Result of an atomic version-checked update operation. * Represents either a successful commit or a version conflict. */ export interface UpdateIfVersionResult { /** Whether the update succeeded */ updated: boolean; /** The updated item with new __version (only if updated=true) */ item?: T & { __version: number; }; /** Current version if update failed due to version mismatch */ currentVersion?: number; /** Structured backend conflict code, when available. */ conflictCode?: string; } /** * A live collection that automatically updates when underlying data changes. * Represents application state, not a one-time query result. * * Uses reactive dependency graph instead of polling for efficient updates. */ export declare class Collection { private db; private name; private collectionId; private predicate; private cache; private subscribers; private parent; private parentId; private isInitialized; private unsubscribeFunctions; private graphUnsubscribe; private runtimeUnsubscribe; private indexBackend; /** Declarations include storage-boundary compound indexes not used by the legacy cache index. */ private declaredIndexes; private indexStore; private indexesLoaded; /** Incremented whenever `cache` is replaced, so the index can tell it is stale. */ private cacheGeneration; /** The cache generation the index entries currently reflect, or -1 for none. */ private indexedGeneration; private lastPlan; /** Cache records addressed by id, valid for `indexedGeneration`. */ private recordsById; /** Raw payload the cache was parsed from, used to detect that nothing changed. */ private lastRawSnapshot; /** * The runtime revision the cache was built from, when the runtime offers one. * * Captured *before* the query that produced the cache, never after. A write * landing between the two then makes this older than the data it labels, * which costs one unnecessary refresh. Capturing it after would make it * newer than the data, and the cache would look current forever. */ private cachedRevision; /** How the last refresh decided the cache was usable. Reporting only. */ private lastFreshness; constructor(db: JsDb, name: string, predicate?: Predicate, parent?: Collection, loadIndexes?: boolean); /** * Load indexes that were persisted from a previous session. */ private loadPersistedIndexes; /** * Get all records in this collection. * Returns cached results (live-updated). */ all(): Promise; /** List records through the canonical application collection API. */ list(): Promise; /** * Find records whose fields match the supplied query. * * When a hash index covers one of the queried fields, the index supplies the * candidate records and the remaining fields are checked on those candidates * only. Otherwise every record is examined. * * The result is identical either way. The index is a projection of the same * cached array a scan would walk, rebuilt whenever that array is replaced, * so an indexed lookup cannot return a record a scan would miss or resolve * an id the cache no longer holds. * * `lastQueryPlan()` reports which path ran. */ find(query?: Partial, options?: CollectionFindOptions): Promise; /** Query records by field equality through the canonical application API. */ query(query?: Partial): Promise; /** * How the most recent `find` was answered. * * Exposed so an application — and the benchmark — can confirm that the index * was actually consulted rather than inferring it from timing. */ lastQueryPlan(): CollectionQueryPlan | null; /** A hash index covering one of the queried fields, if there is one. */ private indexFor; /** * Candidate records from an index, or null when the index cannot be trusted. * * Returning null makes the caller fall back to a scan, which is always * correct. That is the safe direction: a wrong answer is far worse than a * slow one. */ private indexCandidates; /** * Rebuild the index from the current cache when the cache has moved on. * * The index is only ever a projection of `this.cache`. Deriving it from the * same array a scan walks is what makes the two paths agree by construction, * rather than by hoping every write path remembered to update the index. */ private ensureIndexReflectsCache; /** * Create an index on this collection for faster queries. * @example * tasks.createIndex({ name: 'status_idx', type: 'hash', field: 'status' }); * tasks.createIndex({ name: 'created_idx', type: 'sorted', field: 'createdAt' }); */ createIndex(config: IndexConfig): void; /** * List all indexes on this collection. */ listIndexes(): IndexConfig[]; /** * Rebuild all indexes for this collection. * Used to repair corrupted indexes or after data migration. */ rebuildIndexes(): Promise; /** * Check if indexes are stale and need rebuilding. */ validateIndexes(): Promise; /** * Get a single record by ID. */ get(id: string | number): Promise; /** * Find records matching a predicate. * Returns a derived collection that automatically updates. */ where(predicate: Predicate): Collection; /** * Insert a new record into this collection. * Automatically initializes __version to 1 for durable atomic transitions. */ insert(data: Partial, id?: string | number): Promise; /** Put a record through the canonical application collection API. */ put(data: Partial, id?: string | number): Promise; /** * Update a record in this collection. */ update(id: string | number, changes: Partial): Promise; /** * Atomically update a record only if its version matches the expected version. * * Provides Compare-And-Set semantics for durable state transitions. * The version check and update happen atomically at the backend boundary, * ensuring exactly one writer succeeds when multiple writers race. * * A runtime without a dedicated compare-and-set carries the same fence * through its atomic transaction, so this means the same thing on the file * runtime as it does against a managed authority. * * @param id Record identifier * @param expectedVersion The version you observed when you read this record * @param updates Fields to update (does not include __version; version is auto-incremented) * @returns UpdateIfVersionResult with either the updated item or conflict info * * @example * // Read the current state * const handoff = await handoffs.get(handoffId); * * // Try to accept it atomically * const result = await handoffs.updateIfVersion( * handoffId, * handoff.__version, * { status: "accepted", acceptedAt: new Date().toISOString() } * ); * * if (result.updated) { * console.log('Accepted at version', result.item.__version); * } else { * console.log('Conflict - another writer won at version', result.currentVersion); * } */ updateIfVersion(id: string | number, expectedVersion: number, updates: Partial, expectedEpoch?: number, expectedLeaseId?: string, rejectIfDiverged?: boolean, knownCurrent?: T): Promise>; /** * Delete a record from this collection. */ delete(id: string | number): Promise; /** * Count records in this collection. */ count(): Promise; /** * Check if a record exists. */ exists(id: string | number): Promise; /** * Split a storage key back into the collection and record it addresses. * * The transaction contract is expressed in collections and ids rather than * in storage keys, so a helper that holds a key has to name the record the * same way the authority does. */ private addressOf; /** * The runtime's atomic transaction, used as the conditional-create surface. * * Returns the same shape a runtime's own `putIfAbsent` returns, so the caller * above cannot tell which route it took. A refusal that is a lost race is * resolved by reading the record the winner wrote; anything else -- a * missing capability, authentication, schema, transport, a server error -- * is raised. */ private putIfAbsentThroughTransaction; /** * The runtime's atomic transaction, used as the version-fenced update surface. * * Returns the same shape a runtime's own `cas` returns. The authority owns * the next version, so the proposed value carries none. */ private updateIfVersionThroughTransaction; /** * Atomically insert a record only if the key does not already exist. * * Process-safe on the file runtime and across clients against an authority: * the condition is evaluated by the runtime inside its own commit boundary, * never by this method. A runtime without a dedicated conditional create * uses its atomic transaction, so this means the same thing everywhere. * * The created record starts at `__version: 1`, so it can be fenced by * `updateIfVersion` immediately. * * @returns {inserted: true, value: newRecord} if this call created the record * @returns {inserted: false, value: existingRecord} if record already existed */ putIfAbsent(id: string | number, data: Partial): Promise<{ inserted: boolean; value: T; }>; /** * Subscribe to changes in this collection using reactive dependency graph. * Returns an unsubscribe function. */ subscribe(subscriber: Subscriber, _pollInterval?: number): () => void; /** Release runtime subscriptions owned by this live collection. */ close(): void; /** * What this collection's runtime can tell it about staleness. * * This is a *validation mechanism*, not a guarantee that the cache is fresh. * `validation: 'revision'` means a cheap staleness check exists; it says * nothing about whether this collection's cache currently passes it. * * @example * const capability = await people.freshness(); * // 'revision' -> a cheap staleness check exists * // 'refresh' -> re-read is the only honest answer, and `reason` says why */ freshness(): Promise; /** * How the last refresh decided the cache was usable. * * `servedFromCache` is true only when the runtime was never asked for this * collection's data. Reporting only; it exists so a benchmark can tell a * validated cache hit from a fallback that merely happened to be fast. */ lastFreshnessCheck(): FreshnessCheck | null; /** * Read the runtime's current revision, or explain why there isn't one. * * The capability is re-checked on every call rather than remembered, because * a deployment can change underneath a live collection: switching on * replication revokes revision authority, and a collection holding a cached * capability would keep trusting a number that stopped meaning anything. * Any refusal — no capability, a topology change, a transport failure — * lands here as a reason, and the caller falls back to a full refresh. */ private currentRevision; /** * Refresh data from the database and notify subscribers. */ refresh(): Promise; } /** * Relationship helper for loading related data. */ export declare class Relationship { private parentDb; private childDb; private childCollection; private foreignKey; constructor(parentDb: JsDb, childDb: JsDb, childCollection: string, foreignKey: (child: Child) => string | number); /** * Load related children for a parent item. */ load(parentId: string | number): Promise; } //# sourceMappingURL=collection.d.ts.map