UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

348 lines (347 loc) • 15.8 kB
import { SortedMap } from '../SortedMap.cjs'; import { DuplicateKeySyncError } from "../errors.cjs"; import { VirtualOrigin, VirtualRowProps, WithVirtualProps } from "../virtual-props.cjs"; import { Transaction } from '../transactions.cjs'; import { StandardSchemaV1 } from '@standard-schema/spec'; import { ChangeMessage, CollectionConfig, OperationType, OptimisticChangeMessage, PendingMutation } from '../types.cjs'; import { CollectionImpl } from "./index.cjs"; import { CollectionLifecycleManager } from './lifecycle.cjs'; import { CollectionChangesManager } from './changes.cjs'; import { CollectionIndexesManager } from './indexes.cjs'; import { CollectionEventsManager } from './events.cjs'; import { Deferred } from '../deferred.cjs'; type PendingSyncOperation<T extends object, TKey extends string | number> = OptimisticChangeMessage<T, TKey> & { /** Preserve adapter intent when queue changes require reclassification. */ originalSyncType?: `insert`; }; type PendingInsertDisposition = `insert` | `update` | `duplicate`; interface PendingSyncedTransaction<T extends object = Record<string, unknown>, TKey extends string | number = string | number> { committed: boolean; applicationStarted: boolean; layoutChanged: boolean; operations: Array<PendingSyncOperation<T, TKey>>; truncate?: boolean; truncateMarkReady?: boolean; rowMetadataWrites: Map<TKey, PendingMetadataWrite>; /** The last explicit write per key, after `position` operations. */ explicitRowMetadataWrites: Map<TKey, { position: number; write: PendingMetadataWrite; }>; collectionMetadataWrites: Map<string, PendingMetadataWrite>; /** Resolves after application and rejects if canceled before application. */ applied: Deferred<void>; /** * Keys whose writes keep their hydration-seed status and fix no adapter * authority: a DbClient seed chunk's keys, or a wrapping sync's storage * read's seed rows. */ preserveHydrationSeedKeys?: ReadonlySet<TKey>; /** * Set on a truncate that keeps `preserveHydrationSeedKeys`: a schema reset * on demand truncates the source's state of every other key (decision 20). */ sourceTruncate?: boolean; /** Builds the error for an open transaction whose insert a replay invalidates. */ duplicateKeyError?: (key: TKey) => DuplicateKeySyncError; /** * Set when a replay invalidates an open transaction, which happens when a * later transaction begun inside it commits first. Its commit rejects. */ invalidationError?: Error; } /** The parts of a wrapping sync's staged transaction that seed authority reads. */ export type StagedSyncTransaction<TKey extends string | number> = { truncate?: boolean; operations: ReadonlyArray<{ key: unknown; }>; rowMetadataWrites: ReadonlyMap<TKey, unknown>; preserveHydrationSeedKeys?: ReadonlySet<TKey>; sourceTruncate?: boolean; }; /** Whether a sync transaction truncates the source's state of this key. */ export declare function truncatesSourceKey<TKey extends string | number>(transaction: StagedSyncTransaction<TKey>, key: TKey): boolean; export type PendingMetadataWrite = { type: `set`; value: unknown; } | { type: `delete`; }; /** The row metadata a sync operation writes unless the adapter set it explicitly. */ export declare function automaticRowMetadataWrite(operation: Pick<OptimisticChangeMessage<object>, `type` | `metadata`>): PendingMetadataWrite | undefined; export declare class CollectionStateManager<TOutput extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInput extends object = TOutput> { config: CollectionConfig<TOutput, TKey, TSchema, any>; collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>; lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>; changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>; indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>; private _events; transactions: SortedMap<string, Transaction<any>>; pendingSyncedTransactions: Array<PendingSyncedTransaction<TOutput, TKey>>; private pendingSyncedProjection; syncedData: SortedMap<TKey, TOutput>; syncedMetadata: Map<TKey, unknown>; syncedCollectionMetadata: Map<string, unknown>; hydrationSeedKeys: Set<TKey>; hydratedKeys: Set<TKey>; appliedAdapterKeys?: Map<TKey, boolean>; hasAppliedAdapterTruncate: boolean; /** Held seeds that the applied adapter truncate kept (decision 20). */ adapterTruncateKeptKeys: Set<TKey>; optimisticUpserts: Map<TKey, TOutput>; optimisticDeletes: Set<TKey>; private isDeferredSyncKey; getStagedSyncTransactions: () => Array<StagedSyncTransaction<TKey>>; heldOptimisticRows: Map<TKey, { owner: Transaction<any>; row?: TOutput; }>; /** * Tracks Collection attribution for applied source rows. A same-key local * mutation can make an independent source write appear 'local'; this map * does not identify the source client. Optimistic rows are separately 'local'. * Used for the $origin virtual property. */ rowOrigins: Map<TKey, VirtualOrigin>; /** * Tracks keys of pending or persisting local mutations for source-write attribution. * A same-key source write can receive 'local' even when a peer supplied it. */ pendingLocalChanges: Set<TKey>; pendingLocalOrigins: Set<TKey>; private virtualPropsCache; size: number; preSyncVisibleState: Map<TKey, TOutput | undefined>; preSyncVirtualState: Map<TKey, VirtualRowProps<TKey>>; recentlySyncedKeys: Set<TKey>; hasReceivedFirstCommit: boolean; isCommittingSyncTransactions: boolean; private isDrainingSyncTransactions; private syncRunGeneration; isLocalOnly: boolean; /** * Set by a local-only Collection for operation types without a user handler. * Their direct mutations can be written as synced rows at once. */ localOnlyDirectWrite: { types: ReadonlySet<OperationType>; write: (mutations: Array<PendingMutation<TOutput>>) => void; } | undefined; /** * Creates a new CollectionState manager */ constructor(config: CollectionConfig<TOutput, TKey, TSchema, any>); setDeps(deps: { collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>; lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>; changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>; indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>; events: CollectionEventsManager; }): void; /** * Snapshot virtual properties from the current or supplied prior state. * A row with an optimistic write is local and pending; otherwise it keeps * the source row's attribution. Local-only rows are always local without pending writes. * * This is local mutation status, not backend confirmation: `$hasPendingWrites: false` means the * row is not currently affected by an optimistic transaction in this * collection's visible state. */ private getVirtualPropsSnapshotForState; private snapshotRowOriginsForKeys; private enrichWithVirtualPropsSnapshot; private clearOriginTrackingState; /** * Enriches a row with virtual properties using the "add-if-missing" pattern. * If the row already has virtual properties (from an upstream collection), * they are preserved. Otherwise, new values are computed. */ enrichWithVirtualProps(row: TOutput, key: TKey): WithVirtualProps<TOutput, TKey>; /** * Visible entries whose stored row passes `prefilter`, enriched with virtual * properties. Rows that fail are never enriched. */ entriesPassing(prefilter: (row: object) => boolean): IterableIterator<[ TKey, WithVirtualProps<TOutput, TKey> ]>; /** * Creates a change message with virtual properties. * Uses the "add-if-missing" pattern so that pass-through from upstream * collections works correctly. */ enrichChangeMessage(change: ChangeMessage<TOutput, TKey>): ChangeMessage<WithVirtualProps<TOutput, TKey>, TKey>; /** * Get the current value for a key enriched with virtual properties. */ getWithVirtualProps(key: TKey): WithVirtualProps<TOutput, TKey> | undefined; /** * Read a key from the current optimistic layer, or a supplied prior layer. * Both layers fall back to the current synced rows. */ get(key: TKey, optimisticUpserts?: Map<TKey, TOutput>, optimisticDeletes?: Set<TKey>): TOutput | undefined; /** * Check if a key exists in the collection (virtual derived state) */ has(key: TKey): boolean; /** * Get all keys (virtual derived state) */ keys(): IterableIterator<TKey>; /** * Get all values (virtual derived state) */ values(): IterableIterator<TOutput>; /** * Get all entries (virtual derived state) */ entries(): IterableIterator<[ TKey, TOutput ]>; /** * Get all entries (virtual derived state) */ [Symbol.iterator](): IterableIterator<[ TKey, TOutput ]>; /** * Execute a callback for each entry in the collection */ forEach(callbackfn: (value: TOutput, key: TKey, index: number) => void): void; /** * Create a new array with the results of calling a function for each entry in the collection */ map<U>(callbackfn: (value: TOutput, key: TKey, index: number) => U): Array<U>; /** * Lets a wrapping sync report which keys have committed sync transactions * it has not handed to this Collection yet. */ setDeferredSyncKeys(isDeferred: (key: TKey) => boolean, getStaged?: () => Array<StagedSyncTransaction<TKey>>): void; /** * Recompute optimistic state from active transactions */ recomputeOptimisticState(triggeredByUserAction?: boolean): void; /** * Tracks `transaction`. Returns `false` when this Collection already tracks * it. A different unsettled transaction with the same id is a contract * violation: ids are unique. */ trackTransaction(transaction: Transaction<any>): boolean; /** * Overlay held settled rows, then still-active transactions. Given which * keys have a committed sync transaction that has not applied, first end * every other hold and attribution; a deferred one can be dropped without * applying. */ private overlayHeldAndActive; /** * Overlay still-active optimistic mutations on the current layers and * record their keys as pending local changes for $origin tracking. * * A settled transaction leaves `transactions` here, by its own entry. A * recompute calls this after it records held rows, and a sync commit calls * it after it skips those recomputes. */ private overlayActiveTransactions; /** * Calculate the current size based on synced data and optimistic changes */ private calculateSize; /** * Collect events for optimistic changes */ private collectOptimisticChanges; /** Build once per output flush; queued membership excludes optimistic edits. */ createSyncedKeyLookup(): (key: TKey) => boolean; enableHydrationAuthorityTracking(): void; private getProjectedSyncedKeyState; /** * The synced row once every accepted sync transaction applies. A queued * transaction is accepted before it is visible, so a direct write must * read this rather than `syncedData`. */ getAcceptedSyncedRow(key: TKey): TOutput | undefined; /** * Accept a committed transaction in commit order: after every accepted * transaction and ahead of every open one. A transaction begun inside an * open one is independent, so when it commits first it also applies, and * the source reads it, beneath that open transaction. A seed, such as a * DbClient hydration chunk, is accepted the same way, so an open source * transaction's `commit()` still targets its own writes. */ acceptSyncTransaction(transaction: PendingSyncedTransaction<TOutput, TKey>): void; /** A retired sync run's open transaction can never commit. */ discardOpenSyncTransactions(): void; /** Whether the newest sync transaction is still open for writes. */ hasOpenSyncTransaction(): boolean; /** Whether an accepted sync transaction is not yet visible. */ hasHeldSyncTransaction(): boolean; /** * Every synced row once every accepted sync transaction applies: the * baseline a diffing adapter writes against, also during a reload. Inside * an open snapshot or truncate, only that transaction's writes. */ acceptedSyncedEntries(): IterableIterator<[ TKey, TOutput ]>; private classifyProjectedInsert; /** * A partial update merges over the row before it. An insert names the * whole row, even as an equal re-insert, and replaces it. */ private mergesOverRow; /** Classify an adapter insert against retained and queued source state. */ classifyPendingSyncedInsert(key: TKey, value: TOutput): PendingInsertDisposition; private applyPendingSyncOperation; /** * Extend the queued projection after admitting one sync operation. A key * its transaction keeps as a seed's is no source write, as in the rebuild. */ stagePendingSyncOperation(operation: PendingSyncOperation<TOutput, TKey>, preserved?: boolean): void; private rebuildAutomaticRowMetadataWrites; /** * Rebuild the queued projection after truncate, application, or an * accepted transaction changes queue history. An accepted transaction keeps * the operation kinds it was accepted with, even when it commits ahead of * an open transaction it began inside: its insert names the whole row. An * open transaction's inserts follow the rows before it, so it can become * invalid when a later transaction begun inside it commits first; it then * holds an invalidation error that its commit returns. */ private rebuildPendingSyncedProjection; /** Rebuild after truncate, application, or an accepted seed. */ refreshPendingSyncedProjection(): void; /** * Attempts to commit pending synced transactions if there are no active transactions * This method processes operations from pending transactions and applies them to the synced data */ commitPendingTransactions: () => void; hasPersistingTransaction(): boolean; private commitNextPendingTransactionBatch; /** * Abandons the open last sync transaction before acceptance. Only * `commit(signal)` with an aborted signal reaches this, so no other * transaction can depend on the one canceled. */ cancelPendingSyncedTransaction(transaction: PendingSyncedTransaction<TOutput, TKey>, reason?: Error): void; /** * Capture visible state for keys that will be affected by pending sync operations * This must be called BEFORE onTransactionStateChange clears optimistic state */ capturePreSyncVisibleState(): void; /** * Trigger a recomputation when transactions change * This method should be called by the Transaction class when state changes */ onTransactionStateChange(): void; /** * Clean up the collection by stopping sync and clearing data * This can be called manually or automatically by garbage collection */ cleanup(): void; } export {};