UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

210 lines (209 loc) 10.4 kB
import { EventEmitter } from '../event-emitter.js'; import { BasicExpression, OrderBy } from '../query/ir.js'; import { IndexReader } from '../indexes/base-index.js'; import { ChangeMessage, LoadSubsetOptions, LoadSubsetRequestResult, Subscription, SubscriptionEvents, SubscriptionLoadSubsetErrorEvent, SubscriptionStatus, SubscriptionUnsubscribedEvent } from '../types.js'; import { CollectionImpl } from './index.js'; type RequestSnapshotOptions = { where?: BasicExpression<boolean>; signal?: AbortSignal; optimizedOnly?: boolean; trackLoadSubsetPromise?: boolean; /** Optional orderBy to pass to loadSubset for backend optimization */ orderBy?: OrderBy; /** Optional limit to pass to loadSubset for backend optimization */ limit?: number; /** Callback that receives the normalized loadSubset result for internal tracking */ onLoadSubsetResult?: SubsetResultObserver; /** Called when the local snapshot must fall back from an index to a scan. */ onUnoptimized?: () => void; }; type RequestLimitedSnapshotOptions = { orderBy: OrderBy; limit: number; /** A single cursor value; composite cursor inputs are rejected. */ minValues?: Array<unknown>; /** Row offset for offset-based pagination (passed to sync layer) */ offset?: number; /** Whether to track the loadSubset promise on this subscription (default: true) */ trackLoadSubsetPromise?: boolean; /** Callback that receives the normalized loadSubset result for internal tracking */ onLoadSubsetResult?: SubsetResultObserver; }; export type ReleaseLoadSubset = (primaryFailure?: { error: unknown; }) => void; type SubsetResultObserver = (result: LoadSubsetRequestResult, options: LoadSubsetOptions, release: ReleaseLoadSubset) => void; type CollectionSubscriptionOptions = { includeInitialState?: boolean; /** Pre-compiled expression for filtering changes */ whereExpression?: BasicExpression<boolean>; /** Callback to call when the subscription is unsubscribed */ onUnsubscribe?: (event: SubscriptionUnsubscribedEvent) => void; /** Callback for subset-load failures scoped to this subscription. */ onLoadSubsetError?: (event: SubscriptionLoadSubsetErrorEvent) => void; truncateReplayPublication?: TruncateReplayPublicationControl; }; type TruncateReplayPublicationControl = Readonly<{ start: () => void; succeed: () => void; }>; export declare class CollectionSubscription extends EventEmitter<SubscriptionEvents> implements Subscription { private collection; private callback; private options; private loadedInitialState; private skipFiltering; private snapshotSent; /** * Track all loadSubset calls made by this subscription so we can unload them on cleanup. * We store the exact LoadSubsetOptions we passed to loadSubset to ensure symmetric unload. */ private subsetDemands; private primaryFailureDeliveryDepth; private readonly requestedSubsetWhere; private sentKeys; private publishedRows; private stalePublishedRows; private limitedSnapshotRowCount; private lastSentKey; private filteredCallback; private orderByIndex; private _status; private statusRevision; private _lastError; private pendingLoadSubsetParticipants; private truncateCleanup; private collectionCleanup; private collectionRestartCleanup; private truncateReplaySession; private readonly loadSubsetPromiseErrors; private truncateReplacementPending; private unsubscribed; get status(): SubscriptionStatus; get lastError(): unknown | undefined; constructor(collection: CollectionImpl<any, any, any, any, any>, callback: (changes: Array<ChangeMessage<any, any>>) => void, options: CollectionSubscriptionOptions); /** Detach logical demand from work owned by a discarded sync session. */ private handleCollectionCleanup; /** Acquire detached demand after startup or initial-error recovery. */ private restartDetachedDemands; /** * Handle collection truncate event by resetting state and re-requesting subsets. * This is called when the sync layer receives a must-refetch and clears all data. * * To prevent a flash of missing content, we buffer all changes (deletes from truncate * and inserts from refetch) until all loadSubset calls succeed, then emit them together. * A failed replay keeps the last published snapshot private until a later * authoritative replay succeeds. */ private handleTruncate; /** Make tentative replay ownership visible before adapter code can reenter. */ private startTruncateReplayDemand; private settleTruncateReplay; /** Keep every acquisition begun during recovery inside its publication barrier. */ private trackTruncateReplayParticipant; /** Stop obsolete logical demand from pinning a replay barrier. */ private removeTruncateReplayParticipant; /** Publish only after every overlapping replay attempt has settled. */ private checkTruncateReplayComplete; /** * Keep an incomplete replay private. The source no longer proves a complete * state, so only a later successful truncate replay may reopen publication. */ private abandonTruncateReplay; /** Publish the buffered replacement as one batch, or release the delegate. */ private flushTruncateReplay; private restorePublishedSnapshotTracking; /** Fold changes into the private replacement; false when they publish now. */ private bufferPrivately; private createStateDiff; private get isBufferingForTruncate(); private setReadyIfIdle; private isLoadSubsetSessionCurrent; private retireStaleTruncateReplay; /** Drop the replay without publishing; an unfinished wait rejects as aborted. */ private discardTruncateReplay; private resetSnapshotTracking; /** One replay session; only direct subscribers buffer a private replacement. */ private createTruncateReplaySession; /** Start one attempt's demands, then release the setup hold on publication. */ private startTruncateReplayAttempt; get hasPendingTruncateReplacement(): boolean; get pendingTruncateReplacement(): Promise<void> | undefined; get hasFailedTruncateReplacement(): boolean; setOrderByIndex(index: IndexReader<any>): void; /** * Set subscription status and emit events if changed */ private setStatus; /** Observe an asynchronous subset load and restore status on settlement. */ private observeLoadSubsetResult; /** Give every logical observer of one transport rejection the same Error. */ private normalizeLoadSubsetPromiseError; private stopDemandStatusParticipants; private loadSubset; /** Create a fresh, abortable adapter acquisition for a replay generation. */ private createSubsetAcquisition; /** Retire an acquisition before user code; failed cleanup is not retryable. */ private releaseAcquisition; /** Start and retain the first acquisition for one logical subset demand. */ private startSubsetDemand; /** Re-check ownership after adapter and event callbacks that may reenter. */ private isDemandActive; private recordLoadSubsetError; emitEvents(changes: Array<ChangeMessage<any, any>>): boolean; /** Keep direct snapshot reads private while an authoritative replay is open. */ private publishSnapshot; /** * Sends the snapshot to the callback. * Returns a boolean indicating if it succeeded. * It can only fail if there is no index to fulfill the request * and the optimizedOnly option is set to true, * or, the entire state was already loaded or the request was cancelled. */ requestSnapshot(opts?: RequestSnapshotOptions): boolean; /** Release one exact subset request while keeping the subscription alive. */ releaseSnapshot(where: BasicExpression<boolean>): void; private releaseDemand; private releaseDemandAt; /** A replay with no remaining logical demand cannot establish more rows. */ private retireEmptyReplay; /** Read the applied rows in an ordered acquisition without starting demand. */ readOrderedSnapshot(options: LoadSubsetOptions): Array<ChangeMessage<Record<string, unknown>, string | number>>; /** * Sends a snapshot that fulfills the `where` clause and all rows are bigger or equal to the cursor. * Requires a range index to be set with `setOrderByIndex` prior to calling this method. * It uses that range index to load the items in the order of the index. * * Cursor requests support one order term and one minValue. Multi-column * queries use the ordered loader's prefix-and-tie fallback instead. * * Note 1: it may load more rows than the provided LIMIT because it loads all values equal to the first cursor value + limit values greater. * This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values. * Note 2: it does not send keys that have already been sent before. */ requestLimitedSnapshot({ orderBy, limit, minValues, offset, trackLoadSubsetPromise: shouldTrackLoadSubsetPromise, onLoadSubsetResult, }: RequestLimitedSnapshotOptions): void; /** * Filters and flips changes for keys that have not been sent yet. * Deletes are filtered out for keys that have not been sent yet. * Updates are flipped into inserts for keys that have not been sent yet. * Duplicate inserts are filtered out to prevent D2 multiplicity > 1. */ private filterAndFlipChanges; /** * After a failed replay, the source collection is empty but subscribers still * hold the last good publication. Reconcile the first later source delta for * each retained key against that publication instead of treating it as a * duplicate insert. */ private reconcileStalePublishedChanges; private trackPublishedRows; private trackSentKeys; /** * Mark that the subscription should not filter any changes. * This is used when includeInitialState is explicitly set to false, * meaning the caller doesn't want initial state but does want ALL future changes. */ markAllStateAsSeen(): void; unsubscribe(): void; } export {};