@tanstack/db
Version:
A reactive client store for building super fast apps on sync
336 lines (335 loc) • 15.7 kB
TypeScript
import { SortedMap } from '../SortedMap.js';
import { DuplicateKeySyncError } from '../errors.js';
import { VirtualOrigin, VirtualRowProps, WithVirtualProps } from '../virtual-props.js';
import { Transaction } from '../transactions.js';
import { StandardSchemaV1 } from '@standard-schema/spec';
import { ChangeMessage, CollectionConfig, OperationType, OptimisticChangeMessage, PendingMutation } from '../types.js';
import { CollectionImpl } from './index.js';
import { CollectionLifecycleManager } from './lifecycle.js';
import { CollectionChangesManager } from './changes.js';
import { CollectionIndexesManager } from './indexes.js';
import { CollectionEventsManager } from './events.js';
import { Deferred } from '../deferred.js';
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 {};