@tanstack/db
Version:
A reactive client store for building super fast apps on sync
1 lines • 58.3 kB
Source Map (JSON)
{"version":3,"file":"subscription.cjs","sources":["../../../src/collection/subscription.ts"],"sourcesContent":["import { ensureIndexForExpression } from '../indexes/auto-index.js'\nimport { and, eq, gte, lt } from '../query/builder/functions.js'\nimport { PropRef, Value } from '../query/ir.js'\nimport { EventEmitter } from '../event-emitter.js'\nimport { compileExpression } from '../query/compiler/evaluators.js'\nimport { buildCursor } from '../utils/cursor.js'\nimport { deepEquals } from '../utils.js'\nimport {\n createFilterFunctionFromExpression,\n createFilteredCallback,\n} from './change-events.js'\nimport type { BasicExpression, OrderBy } from '../query/ir.js'\nimport type { IndexInterface } from '../indexes/base-index.js'\nimport type {\n ChangeMessage,\n LoadSubsetOptions,\n Subscription,\n SubscriptionEvents,\n SubscriptionLoadSubsetErrorEvent,\n SubscriptionStatus,\n SubscriptionUnsubscribedEvent,\n} from '../types.js'\nimport type { CollectionImpl } from './index.js'\n\ntype RequestSnapshotOptions = {\n where?: BasicExpression<boolean>\n signal?: AbortSignal\n optimizedOnly?: boolean\n trackLoadSubsetPromise?: boolean\n /** Optional orderBy to pass to loadSubset for backend optimization */\n orderBy?: OrderBy\n /** Optional limit to pass to loadSubset for backend optimization */\n limit?: number\n /** Callback that receives the raw loadSubset result for external tracking */\n onLoadSubsetResult?: (result: Promise<void> | true) => void\n /** Called when the local snapshot must fall back from an index to a scan. */\n onUnoptimized?: () => void\n}\n\ntype RequestLimitedSnapshotOptions = {\n orderBy: OrderBy\n limit: number\n /** All column values for cursor (first value used for local index, all values for sync layer) */\n minValues?: Array<unknown>\n /** Row offset for offset-based pagination (passed to sync layer) */\n offset?: number\n /** Whether to track the loadSubset promise on this subscription (default: true) */\n trackLoadSubsetPromise?: boolean\n /** Callback that receives the raw loadSubset result for external tracking */\n onLoadSubsetResult?: (result: Promise<void> | true) => void\n}\n\ntype CollectionSubscriptionOptions = {\n includeInitialState?: boolean\n /** Pre-compiled expression for filtering changes */\n whereExpression?: BasicExpression<boolean>\n /** Callback to call when the subscription is unsubscribed */\n onUnsubscribe?: (event: SubscriptionUnsubscribedEvent) => void\n /** Callback for subset-load failures scoped to this subscription. */\n onLoadSubsetError?: (event: SubscriptionLoadSubsetErrorEvent) => void\n}\n\ntype TruncatePublicationState = {\n loadedInitialState: boolean\n snapshotSent: boolean\n sentKeys: Set<string | number>\n publishedRows: Map<string | number, object>\n limitedSnapshotRowCount: number\n lastSentKey: string | number | undefined\n}\n\ntype SubsetAcquisition = {\n options: LoadSubsetOptions\n abortController?: AbortController\n removeRequestAbortListener?: () => void\n}\n\ntype SubsetDemand = SubsetAcquisition & {\n requestOptions: LoadSubsetOptions\n}\n\ntype TruncateReplayAttempt = {\n pending: Set<{ promise: Promise<void> }>\n failed: boolean\n setupComplete: boolean\n}\n\ntype TruncateReplaySession = {\n publicationState: TruncatePublicationState\n buffer: Array<Array<ChangeMessage<any, any>>>\n attempts: Set<TruncateReplayAttempt>\n currentAttempt: TruncateReplayAttempt\n}\n\nexport class CollectionSubscription\n extends EventEmitter<SubscriptionEvents>\n implements Subscription\n{\n private loadedInitialState = false\n\n // Flag to skip filtering in filterAndFlipChanges.\n // This is separate from loadedInitialState because we want to allow\n // requestSnapshot to still work even when filtering is skipped.\n private skipFiltering = false\n\n // Flag to indicate that we have sent at least 1 snapshot.\n // While `snapshotSent` is false we filter out all changes from subscription to the collection.\n private snapshotSent = false\n\n /**\n * Track all loadSubset calls made by this subscription so we can unload them on cleanup.\n * We store the exact LoadSubsetOptions we passed to loadSubset to ensure symmetric unload.\n */\n private subsetDemands: Array<SubsetDemand> = []\n private readonly requestedSubsetWhere = new WeakMap<\n LoadSubsetOptions,\n BasicExpression<boolean>\n >()\n\n // Keep track of the keys we've sent (needed for join and orderBy optimizations)\n private sentKeys = new Set<string | number>()\n private publishedRows = new Map<string | number, object>()\n private stalePublishedRows = new Map<string | number, object>()\n\n // Track the count of rows sent via requestLimitedSnapshot for offset-based pagination\n private limitedSnapshotRowCount = 0\n\n // Track the last key sent via requestLimitedSnapshot for cursor-based pagination\n private lastSentKey: string | number | undefined\n\n private filteredCallback: (changes: Array<ChangeMessage<any, any>>) => boolean\n\n private orderByIndex: IndexInterface<string | number> | undefined\n\n // Status tracking\n private _status: SubscriptionStatus = `ready`\n private _lastError: unknown | undefined\n private pendingLoadSubsetPromises: Set<Promise<void>> = new Set()\n\n // Cleanup function for truncate event listener\n private truncateCleanup: (() => void) | undefined\n\n // One replay session owns the publication baseline, overlapping attempts,\n // and buffered changes until every attempt settles.\n private truncateReplaySession: TruncateReplaySession | undefined\n\n public get status(): SubscriptionStatus {\n return this._status\n }\n\n public get lastError(): unknown | undefined {\n return this._lastError\n }\n\n constructor(\n private collection: CollectionImpl<any, any, any, any, any>,\n private callback: (changes: Array<ChangeMessage<any, any>>) => void,\n private options: CollectionSubscriptionOptions,\n ) {\n super()\n if (options.onUnsubscribe) {\n this.on(`unsubscribed`, options.onUnsubscribe)\n }\n if (options.onLoadSubsetError) {\n this.on(`loadSubset:error`, options.onLoadSubsetError)\n }\n\n // Auto-index for where expressions if enabled\n if (options.whereExpression) {\n ensureIndexForExpression(options.whereExpression, this.collection)\n }\n\n const callbackWithSentKeysTracking = (\n changes: Array<ChangeMessage<any, any>>,\n ) => {\n this.trackPublishedRows(changes)\n this.trackSentKeys(changes)\n callback(changes)\n }\n\n this.callback = callbackWithSentKeysTracking\n\n // Create a filtered callback if where clause is provided\n this.filteredCallback = options.whereExpression\n ? createFilteredCallback(this.callback, options)\n : (changes) => {\n this.callback(changes)\n return true\n }\n\n // Listen for truncate events to re-request data after must-refetch\n // When a truncate happens (e.g., from a 409 must-refetch), all collection data is cleared.\n // We need to re-request all previously loaded subsets to repopulate the data.\n this.truncateCleanup = this.collection.on(`truncate`, () => {\n this.handleTruncate()\n })\n }\n\n /**\n * Handle collection truncate event by resetting state and re-requesting subsets.\n * This is called when the sync layer receives a must-refetch and clears all data.\n *\n * To prevent a flash of missing content, we buffer all changes (deletes from truncate\n * and inserts from refetch) until all loadSubset calls succeed, then emit them together.\n * A failed replay keeps the last published snapshot, resumes ordinary deltas,\n * and retains subset ownership so a later truncate can retry the replay.\n */\n private handleTruncate() {\n const demandsToReload = [...this.subsetDemands]\n\n // Only buffer if there's an actual loadSubset handler that can do async work.\n // Without a loadSubset handler, there's nothing to re-request and no reason to buffer.\n // This prevents unnecessary buffering in eager sync mode or when loadSubset isn't implemented.\n const hasLoadSubsetHandler = this.collection._sync.syncLoadSubsetFn !== null\n\n // If there are no subsets to reload OR no loadSubset handler, just reset state\n if (demandsToReload.length === 0 || !hasLoadSubsetHandler) {\n this.snapshotSent = false\n this.loadedInitialState = false\n this.limitedSnapshotRowCount = 0\n this.lastSentKey = undefined\n return\n }\n\n const attempt: TruncateReplayAttempt = {\n pending: new Set(),\n failed: false,\n setupComplete: false,\n }\n let session = this.truncateReplaySession\n if (!session) {\n session = {\n publicationState: {\n loadedInitialState: this.loadedInitialState,\n snapshotSent: this.snapshotSent,\n sentKeys: new Set(this.sentKeys),\n publishedRows: new Map(this.publishedRows),\n limitedSnapshotRowCount: this.limitedSnapshotRowCount,\n lastSentKey: this.lastSentKey,\n },\n buffer: [],\n attempts: new Set(),\n currentAttempt: attempt,\n }\n this.truncateReplaySession = session\n }\n session.attempts.add(attempt)\n session.currentAttempt = attempt\n\n // A newer replay replaces every prior acquisition for these demands. Abort\n // the old work before it can install rows into the new generation.\n for (const demand of demandsToReload) {\n demand.abortController?.abort()\n }\n\n // Start buffering before the truncate commit publishes its deletes. Every\n // overlapping attempt shares this one publication baseline and buffer.\n // Retained rows from an earlier failed replay stay marked until this\n // attempt either replaces them or proves they are absent.\n\n // Reset snapshot/pagination tracking state for the replacement snapshot.\n this.snapshotSent = false\n this.loadedInitialState = false\n this.limitedSnapshotRowCount = 0\n this.lastSentKey = undefined\n\n // Defer the requests so the truncate commit's deletes enter the session\n // buffer before a synchronous adapter can publish replacement rows.\n queueMicrotask(() => {\n if (this.truncateReplaySession !== session) return\n\n for (const demand of demandsToReload) {\n if (!this.subsetDemands.includes(demand)) continue\n\n const isCurrentAttempt = () =>\n this.truncateReplaySession === session &&\n session.currentAttempt === attempt\n const nextAcquisition = this.createSubsetAcquisition(demand)\n let syncResult: Promise<void> | true\n try {\n syncResult = this.loadSubset(\n nextAcquisition.options,\n isCurrentAttempt,\n )\n } catch {\n nextAcquisition.abortController.abort()\n nextAcquisition.removeRequestAbortListener?.()\n attempt.failed = true\n continue\n }\n\n this.observeLoadSubsetResult(\n syncResult,\n nextAcquisition.options,\n true,\n () => isCurrentAttempt() && !nextAcquisition.options.signal?.aborted,\n )\n\n if (syncResult instanceof Promise) {\n // A transport promise may be shared by several deduplicated logical\n // demands. Track each demand separately so one settlement observer\n // cannot complete the attempt before the others apply their result.\n const pending = { promise: syncResult }\n attempt.pending.add(pending)\n void syncResult.then(\n () => this.settleTruncateReplay(session, attempt, pending),\n () => {\n // A released demand no longer participates in the current\n // replacement. Its cooperative AbortError must not discard the\n // successful rows from demands that are still active.\n if (\n this.subsetDemands.includes(demand) &&\n !nextAcquisition.options.signal?.aborted\n ) {\n attempt.failed = true\n }\n this.settleTruncateReplay(session, attempt, pending)\n },\n )\n }\n\n try {\n this.replaceSubsetAcquisition(demand, nextAcquisition)\n } catch (error) {\n // The old lease is still owned because its release failed. Abort and\n // release the new acquisition, but keep observing its work so rows\n // from a non-cooperative adapter cannot escape the replay buffer.\n nextAcquisition.abortController.abort()\n nextAcquisition.removeRequestAbortListener?.()\n try {\n this.collection._sync.unloadSubset(nextAcquisition.options)\n } catch {\n // Preserve the first ownership error. The demand still retains the\n // old acquisition so normal cleanup can retry that release.\n }\n this.recordLoadSubsetError(demand.options, error, true)\n attempt.failed = true\n }\n }\n\n attempt.setupComplete = true\n this.checkTruncateReplayComplete(session)\n })\n }\n\n private settleTruncateReplay(\n session: TruncateReplaySession,\n attempt: TruncateReplayAttempt,\n pending: { promise: Promise<void> },\n ): void {\n if (this.truncateReplaySession !== session) return\n attempt.pending.delete(pending)\n this.checkTruncateReplayComplete(session)\n }\n\n /** Publish only after every overlapping replay attempt has settled. */\n private checkTruncateReplayComplete(session: TruncateReplaySession): void {\n if (this.truncateReplaySession !== session) return\n for (const attempt of session.attempts) {\n if (!attempt.setupComplete || attempt.pending.size > 0) return\n }\n\n if (session.currentAttempt.failed) {\n this.abandonTruncateReplay(session)\n } else {\n this.flushTruncateReplay(session)\n }\n }\n\n /**\n * Discard an incomplete current replay and restore the last publication.\n * Rows in that publication remain stale until a later source delta or replay\n * reconciles them with the source collection.\n */\n private abandonTruncateReplay(session: TruncateReplaySession): void {\n if (this.truncateReplaySession !== session) return\n const publicationState = session.publicationState\n this.loadedInitialState = publicationState.loadedInitialState\n this.snapshotSent = publicationState.snapshotSent\n this.sentKeys = new Set(publicationState.sentKeys)\n this.publishedRows = new Map(publicationState.publishedRows)\n this.stalePublishedRows = new Map(publicationState.publishedRows)\n this.limitedSnapshotRowCount = publicationState.limitedSnapshotRowCount\n this.lastSentKey = publicationState.lastSentKey\n this.truncateReplaySession = undefined\n }\n\n /** Publish the complete buffered replacement as one subscriber batch. */\n private flushTruncateReplay(session: TruncateReplaySession): void {\n if (this.truncateReplaySession !== session) return\n this.truncateReplaySession = undefined\n\n const retainedDeletes = [...this.stalePublishedRows].map(\n ([key, value]): ChangeMessage<any, any> => ({\n type: `delete`,\n key,\n value,\n }),\n )\n this.stalePublishedRows.clear()\n\n const merged = [...session.buffer.flat(), ...retainedDeletes]\n const activeDemandFilters = this.subsetDemands.map((demand) =>\n demand.requestOptions.where\n ? createFilterFunctionFromExpression(demand.requestOptions.where)\n : undefined,\n )\n const replacement = this.createPublicationDiff(\n session.publicationState.publishedRows,\n merged,\n (value) => activeDemandFilters.some((filter) => filter?.(value) ?? true),\n )\n if (replacement.length > 0) this.filteredCallback(replacement)\n // Buffering records every source key before active-demand filtering. Reset\n // the dedupe set to what the subscriber actually received so a later\n // request can publish a row that belonged only to a released demand.\n this.sentKeys = new Set(this.publishedRows.keys())\n if (this.orderByIndex) {\n this.limitedSnapshotRowCount = this.sentKeys.size\n const orderedSentKeys = this.orderByIndex.takeFromStart(\n this.sentKeys.size,\n (key) => this.sentKeys.has(key),\n )\n this.lastSentKey = orderedSentKeys.at(-1)\n }\n }\n\n /** Reduce a replay's raw delete/insert stream to one exact semantic delta. */\n private createPublicationDiff(\n baseline: ReadonlyMap<string | number, object>,\n changes: ReadonlyArray<ChangeMessage<any, any>>,\n isCoveredByActiveDemand: (value: object) => boolean,\n ): Array<ChangeMessage<any, any>> {\n const finalRows = new Map(baseline)\n for (const change of changes) {\n if (change.type === `delete`) finalRows.delete(change.key)\n else finalRows.set(change.key, change.value)\n }\n for (const [key, value] of finalRows) {\n if (!isCoveredByActiveDemand(value)) finalRows.delete(key)\n }\n\n const replacement: Array<ChangeMessage<any, any>> = []\n for (const [key, previousValue] of baseline) {\n const value = finalRows.get(key)\n if (value === undefined) {\n replacement.push({\n type: `delete`,\n key,\n value: previousValue,\n })\n } else if (!deepEquals(value, previousValue)) {\n replacement.push({\n type: `update`,\n key,\n value,\n previousValue,\n })\n }\n }\n for (const [key, value] of finalRows) {\n if (!baseline.has(key)) replacement.push({ type: `insert`, key, value })\n }\n return replacement\n }\n\n private get isBufferingForTruncate(): boolean {\n return this.truncateReplaySession !== undefined\n }\n\n setOrderByIndex(index: IndexInterface<any>) {\n this.orderByIndex = index\n }\n\n /**\n * Check if an orderBy index has been set for this subscription\n */\n hasOrderByIndex(): boolean {\n return this.orderByIndex !== undefined\n }\n\n /**\n * Set subscription status and emit events if changed\n */\n private setStatus(newStatus: SubscriptionStatus) {\n if (this._status === newStatus) {\n return // No change\n }\n\n const previousStatus = this._status\n this._status = newStatus\n\n // Emit status:change event\n this.emitInner(`status:change`, {\n type: `status:change`,\n subscription: this,\n previousStatus,\n status: newStatus,\n })\n\n // Emit specific status event\n const eventKey: `status:${SubscriptionStatus}` = `status:${newStatus}`\n this.emitInner(eventKey, {\n type: eventKey,\n subscription: this,\n previousStatus,\n status: newStatus,\n } as SubscriptionEvents[typeof eventKey])\n }\n\n /** Observe an asynchronous subset load and restore status on settlement. */\n private observeLoadSubsetResult(\n syncResult: Promise<void> | true,\n options: LoadSubsetOptions,\n trackStatus: boolean,\n shouldReportError: () => boolean = () => true,\n ) {\n if (!(syncResult instanceof Promise)) return\n\n if (trackStatus) {\n this.pendingLoadSubsetPromises.add(syncResult)\n this.setStatus(`loadingSubset`)\n }\n\n const finish = () => {\n if (trackStatus) {\n this.pendingLoadSubsetPromises.delete(syncResult)\n if (this.pendingLoadSubsetPromises.size === 0) {\n this.setStatus(`ready`)\n }\n }\n }\n\n void syncResult.then(finish, (error: unknown) => {\n if (shouldReportError()) this.recordLoadSubsetError(options, error)\n finish()\n })\n }\n\n private loadSubset(\n options: LoadSubsetOptions,\n shouldReportError: () => boolean = () => true,\n ): Promise<void> | true {\n try {\n return this.collection._sync.loadSubset(options)\n } catch (error) {\n if (shouldReportError()) this.recordLoadSubsetError(options, error)\n throw error\n }\n }\n\n /** Create a fresh, abortable adapter acquisition for a replay generation. */\n private createSubsetAcquisition(\n demand: SubsetDemand,\n ): SubsetAcquisition & { abortController: AbortController } {\n const abortController = new AbortController()\n const requestSignal = demand.requestOptions.signal\n let removeRequestAbortListener: (() => void) | undefined\n\n if (requestSignal?.aborted) {\n abortController.abort(requestSignal.reason)\n } else if (requestSignal) {\n const abort = () => abortController.abort(requestSignal.reason)\n requestSignal.addEventListener(`abort`, abort, { once: true })\n removeRequestAbortListener = () =>\n requestSignal.removeEventListener(`abort`, abort)\n }\n\n return {\n options: {\n ...demand.requestOptions,\n signal: abortController.signal,\n },\n abortController,\n removeRequestAbortListener,\n }\n }\n\n /** Replace the adapter lease held for one logical subset demand. */\n private replaceSubsetAcquisition(\n demand: SubsetDemand,\n next: SubsetAcquisition & { abortController: AbortController },\n ): void {\n const previousOptions = demand.options\n const removePreviousAbortListener = demand.removeRequestAbortListener\n this.collection._sync.unloadSubset(previousOptions)\n removePreviousAbortListener?.()\n demand.options = next.options\n demand.abortController = next.abortController\n demand.removeRequestAbortListener = next.removeRequestAbortListener\n }\n\n /** Abort and release one current adapter acquisition. */\n private releaseSubsetDemand(demand: SubsetDemand): void {\n demand.abortController?.abort()\n try {\n this.collection._sync.unloadSubset(demand.options)\n } finally {\n demand.removeRequestAbortListener?.()\n }\n }\n\n /** Start and retain the first acquisition for one logical subset demand. */\n private startSubsetDemand(requestOptions: LoadSubsetOptions): {\n demand: SubsetDemand\n result: Promise<void> | true\n } {\n const demand: SubsetDemand = {\n requestOptions,\n options: requestOptions,\n }\n const acquisition = this.createSubsetAcquisition(demand)\n try {\n const result = this.loadSubset(acquisition.options)\n demand.options = acquisition.options\n demand.abortController = acquisition.abortController\n demand.removeRequestAbortListener = acquisition.removeRequestAbortListener\n this.subsetDemands.push(demand)\n return { demand, result }\n } catch (error) {\n acquisition.abortController.abort()\n acquisition.removeRequestAbortListener?.()\n throw error\n }\n }\n\n private recordLoadSubsetError(\n options: LoadSubsetOptions,\n error: unknown,\n reportAborted = false,\n ): void {\n // Aborted subset requests are obsolete demand, not load failures. The\n // request may reject after its route has already been released.\n if (options.signal?.aborted && !reportAborted) return\n\n this._lastError = error\n this.emitInner(`loadSubset:error`, {\n type: `loadSubset:error`,\n subscription: this,\n options,\n error,\n })\n }\n\n hasLoadedInitialState() {\n return this.loadedInitialState\n }\n\n hasSentAtLeastOneSnapshot() {\n return this.snapshotSent\n }\n\n emitEvents(changes: Array<ChangeMessage<any, any>>): boolean {\n const newChanges = this.filterAndFlipChanges(changes)\n\n // Reconciliation can reduce a source delta to no visible change. Do not\n // wake subscribers for an empty semantic batch.\n if (changes.length > 0 && newChanges.length === 0) return false\n\n if (this.isBufferingForTruncate) {\n // Buffer the changes instead of emitting immediately\n // This prevents a flash of missing content during truncate/refetch\n if (newChanges.length > 0) {\n this.truncateReplaySession!.buffer.push(newChanges)\n }\n return false\n } else {\n return this.filteredCallback(newChanges)\n }\n }\n\n /**\n * Sends the snapshot to the callback.\n * Returns a boolean indicating if it succeeded.\n * It can only fail if there is no index to fulfill the request\n * and the optimizedOnly option is set to true,\n * or, the entire state was already loaded.\n */\n requestSnapshot(opts?: RequestSnapshotOptions): boolean {\n if (this.loadedInitialState) {\n // Subscription was deoptimized so we already sent the entire initial state\n return false\n }\n\n const stateOpts: RequestSnapshotOptions = {\n where: this.options.whereExpression,\n optimizedOnly: opts?.optimizedOnly ?? false,\n }\n\n if (opts) {\n if (`where` in opts) {\n const snapshotWhereExp = opts.where\n if (stateOpts.where) {\n // Combine the two where expressions\n const subWhereExp = stateOpts.where\n const combinedWhereExp = and(subWhereExp, snapshotWhereExp)\n stateOpts.where = combinedWhereExp\n } else {\n stateOpts.where = snapshotWhereExp\n }\n }\n } else {\n // No options provided so it's loading the entire initial state\n this.loadedInitialState = true\n }\n\n // Request the sync layer to load more data\n // don't await it, we will load the data into the collection when it comes in\n const loadOptions: LoadSubsetOptions = {\n where: stateOpts.where,\n signal: opts?.signal,\n subscription: this,\n // Include orderBy and limit if provided so sync layer can optimize the query\n orderBy: opts?.orderBy,\n limit: opts?.limit,\n }\n\n const { demand, result: syncResult } = this.startSubsetDemand(loadOptions)\n if (opts?.where) this.requestedSubsetWhere.set(loadOptions, opts.where)\n\n // Pass the raw loadSubset result to the caller for external tracking\n opts?.onLoadSubsetResult?.(syncResult)\n\n this.observeLoadSubsetResult(\n syncResult,\n demand.options,\n opts?.trackLoadSubsetPromise ?? true,\n )\n\n // Also load data immediately from the collection\n let snapshot: Array<ChangeMessage<any, any>> | void\n if (opts?.onUnoptimized) {\n snapshot = this.collection.currentStateAsChanges({\n ...stateOpts,\n optimizedOnly: true,\n })\n if (snapshot === undefined) {\n opts.onUnoptimized()\n snapshot = this.collection.currentStateAsChanges({\n ...stateOpts,\n optimizedOnly: false,\n })\n }\n } else {\n snapshot = this.collection.currentStateAsChanges(stateOpts)\n }\n\n if (snapshot === undefined) {\n // Couldn't load from indexes\n return false\n }\n\n // Only send changes that have not been sent yet\n const filteredSnapshot = snapshot.filter(\n (change) => !this.sentKeys.has(change.key),\n )\n\n // Add keys to sentKeys BEFORE calling callback to prevent race condition.\n // If a change event arrives while the callback is executing, it will see\n // the keys already in sentKeys and filter out duplicates correctly.\n for (const change of filteredSnapshot) {\n this.sentKeys.add(change.key)\n }\n\n this.snapshotSent = true\n this.callback(filteredSnapshot)\n return true\n }\n\n /** Release one exact subset request while keeping the subscription alive. */\n releaseSnapshot(where: BasicExpression<boolean>): void {\n const index = this.subsetDemands.findIndex(\n (demand) =>\n demand.requestOptions.where === where ||\n this.requestedSubsetWhere.get(demand.requestOptions) === where,\n )\n if (index === -1) return\n\n const [demand] = this.subsetDemands.splice(index, 1)\n if (demand) this.releaseSubsetDemand(demand)\n }\n\n /**\n * Sends a snapshot that fulfills the `where` clause and all rows are bigger or equal to the cursor.\n * Requires a range index to be set with `setOrderByIndex` prior to calling this method.\n * It uses that range index to load the items in the order of the index.\n *\n * For multi-column orderBy:\n * - Uses first value from `minValues` for LOCAL index operations (wide bounds, ensures no missed rows)\n * - Uses all `minValues` to build a precise composite cursor for SYNC layer loadSubset\n *\n * 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.\n * This is needed to ensure that it does not accidentally skip duplicate values when the limit falls in the middle of some duplicated values.\n * Note 2: it does not send keys that have already been sent before.\n */\n requestLimitedSnapshot({\n orderBy,\n limit,\n minValues,\n offset,\n trackLoadSubsetPromise: shouldTrackLoadSubsetPromise = true,\n onLoadSubsetResult,\n }: RequestLimitedSnapshotOptions) {\n if (!limit) throw new Error(`limit is required`)\n\n if (!this.orderByIndex) {\n throw new Error(\n `Ordered snapshot was requested but no index was found. You have to call setOrderByIndex before requesting an ordered snapshot.`,\n )\n }\n\n // Check if minValues has a first element (regardless of its value)\n // This distinguishes between \"no min value provided\" vs \"min value is undefined\"\n const hasMinValue = minValues !== undefined && minValues.length > 0\n // Derive first column value from minValues (used for local index operations)\n const minValue = minValues?.[0]\n // Cast for index operations (index expects string | number)\n const minValueForIndex = minValue as string | number | undefined\n\n const index = this.orderByIndex\n const where = this.options.whereExpression\n const whereFilterFn = where\n ? createFilterFunctionFromExpression(where)\n : undefined\n\n const filterFn = (key: string | number | undefined): boolean => {\n if (key !== undefined && this.sentKeys.has(key)) {\n return false\n }\n\n const value = this.collection.get(key)\n if (value === undefined) {\n return false\n }\n\n return whereFilterFn?.(value) ?? true\n }\n\n let biggestObservedValue = minValueForIndex\n const changes: Array<ChangeMessage<any, string | number>> = []\n\n // If we have a minValue we need to handle the case\n // where there might be duplicate values equal to minValue that we need to include\n // because we can have data like this: [1, 2, 3, 3, 3, 4, 5]\n // so if minValue is 3 then the previous snapshot may not have included all 3s\n // e.g. if it was offset 0 and limit 3 it would only have loaded the first 3\n // so we load all rows equal to minValue first, to be sure we don't skip any duplicate values\n //\n // For multi-column orderBy, we use the first column value for index operations (wide bounds)\n // This may load some duplicates but ensures we never miss any rows.\n let keys: Array<string | number> = []\n if (hasMinValue) {\n // First, get all items with the same FIRST COLUMN value as minValue\n // This provides wide bounds for the local index\n const { expression } = orderBy[0]!\n const allRowsWithMinValue = this.collection.currentStateAsChanges({\n where: eq(expression, new Value(minValueForIndex)),\n })\n\n if (allRowsWithMinValue) {\n const keysWithMinValue = allRowsWithMinValue\n .map((change) => change.key)\n .filter((key) => !this.sentKeys.has(key) && filterFn(key))\n\n // Add items with the minValue first\n keys.push(...keysWithMinValue)\n\n // Then get items greater than minValue\n const keysGreaterThanMin = index.take(\n limit - keys.length,\n minValueForIndex!,\n filterFn,\n )\n keys.push(...keysGreaterThanMin)\n } else {\n keys = index.take(limit, minValueForIndex!, filterFn)\n }\n } else {\n // No min value provided, start from the beginning\n keys = index.takeFromStart(limit, filterFn)\n }\n\n const valuesNeeded = () => Math.max(limit - changes.length, 0)\n const collectionExhausted = () => keys.length === 0\n\n // Create a value extractor for the orderBy field to properly track the biggest indexed value\n const orderByExpression = orderBy[0]!.expression\n const valueExtractor =\n orderByExpression.type === `ref`\n ? compileExpression(new PropRef(orderByExpression.path), true)\n : null\n\n while (valuesNeeded() > 0 && !collectionExhausted()) {\n const insertedKeys = new Set<string | number>() // Track keys we add to `changes` in this iteration\n\n for (const key of keys) {\n const value = this.collection.get(key)!\n changes.push({\n type: `insert`,\n key,\n value,\n })\n // Extract the indexed value (e.g., salary) from the row, not the full row\n // This is needed for index.take() to work correctly with the BTree comparator\n biggestObservedValue = valueExtractor ? valueExtractor(value) : value\n insertedKeys.add(key) // Track this key\n }\n\n keys = index.take(valuesNeeded(), biggestObservedValue!, filterFn)\n }\n\n // Track row count for offset-based pagination (before sending to callback)\n // Use the current count as the offset for this load\n const currentOffset = this.limitedSnapshotRowCount\n\n // Add keys to sentKeys BEFORE calling callback to prevent race condition.\n // If a change event arrives while the callback is executing, it will see\n // the keys already in sentKeys and filter out duplicates correctly.\n for (const change of changes) {\n this.sentKeys.add(change.key)\n }\n\n this.callback(changes)\n\n // Update the row count and last key after sending (for next call's offset/cursor)\n this.limitedSnapshotRowCount = Math.max(\n this.limitedSnapshotRowCount,\n currentOffset + changes.length,\n )\n if (changes.length > 0) {\n this.lastSentKey = changes[changes.length - 1]!.key\n }\n\n // Build cursor expressions for sync layer loadSubset\n // The cursor expressions are separate from the main where clause\n // so the sync layer can choose cursor-based or offset-based pagination\n let cursorExpressions:\n | {\n whereFrom: BasicExpression<boolean>\n whereCurrent: BasicExpression<boolean>\n lastKey?: string | number\n }\n | undefined\n\n if (minValues !== undefined && minValues.length > 0) {\n const whereFromCursor = buildCursor(orderBy, minValues)\n\n if (whereFromCursor) {\n const { expression } = orderBy[0]!\n const cursorMinValue = minValues[0]\n\n // Build the whereCurrent expression for the first orderBy column\n // For Date values, we need to handle precision differences between JS (ms) and backends (μs)\n // A JS Date represents a 1ms range, so we query for all values within that range\n let whereCurrentCursor: BasicExpression<boolean>\n if (cursorMinValue instanceof Date) {\n const cursorMinValuePlus1ms = new Date(cursorMinValue.getTime() + 1)\n whereCurrentCursor = and(\n gte(expression, new Value(cursorMinValue)),\n lt(expression, new Value(cursorMinValuePlus1ms)),\n )\n } else {\n whereCurrentCursor = eq(expression, new Value(cursorMinValue))\n }\n\n cursorExpressions = {\n whereFrom: whereFromCursor,\n whereCurrent: whereCurrentCursor,\n lastKey: this.lastSentKey,\n }\n }\n }\n\n // Request the sync layer to load more data\n // don't await it, we will load the data into the collection when it comes in\n // Note: `where` does NOT include cursor expressions - they are passed separately\n // The sync layer can choose to use cursor-based or offset-based pagination\n const loadOptions: LoadSubsetOptions = {\n where, // Main filter only, no cursor\n limit,\n orderBy,\n cursor: cursorExpressions, // Cursor expressions passed separately\n offset: offset ?? currentOffset, // Use provided offset, or auto-tracked offset\n subscription: this,\n }\n\n const { demand, result: syncResult } = this.startSubsetDemand(loadOptions)\n\n // Pass the raw loadSubset result to the caller for external tracking\n onLoadSubsetResult?.(syncResult)\n this.observeLoadSubsetResult(\n syncResult,\n demand.options,\n shouldTrackLoadSubsetPromise,\n )\n }\n\n // TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function\n // and that that also works properly (i.e. does not skip duplicate values)\n\n /**\n * Filters and flips changes for keys that have not been sent yet.\n * Deletes are filtered out for keys that have not been sent yet.\n * Updates are flipped into inserts for keys that have not been sent yet.\n * Duplicate inserts are filtered out to prevent D2 multiplicity > 1.\n */\n private filterAndFlipChanges(changes: Array<ChangeMessage<any, any>>) {\n changes = this.reconcileStalePublishedChanges(changes)\n\n if (this.loadedInitialState || this.skipFiltering) {\n // We loaded the entire initial state or filtering is explicitly skipped\n // so no need to filter or flip changes\n return changes\n }\n\n // When buffering for truncate, we need all changes (including deletes) to pass through.\n // This is important because:\n // 1. If loadedInitialState was previously true, sentKeys will be empty\n // (trackSentKeys early-returns when loadedInitialState is true)\n // 2. The truncate deletes are for keys that WERE sent to the subscriber\n // 3. We're collecting all changes atomically, so filtering doesn't make sense\n const skipDeleteFilter = this.isBufferingForTruncate\n\n const newChanges = []\n for (const change of changes) {\n let newChange = change\n const keyInSentKeys = this.sentKeys.has(change.key)\n\n if (!keyInSentKeys) {\n if (change.type === `update`) {\n newChange = { ...change, type: `insert`, previousValue: undefined }\n this.sentKeys.add(change.key)\n } else if (change.type === `delete`) {\n // Filter out deletes for keys that have not been sent,\n // UNLESS we're buffering for truncate (where all deletes should pass through)\n if (!skipDeleteFilter) {\n continue\n }\n } else {\n this.sentKeys.add(change.key)\n }\n } else {\n // Key was already sent - handle based on change type\n if (change.type === `insert`) {\n // Filter out duplicate inserts - the key was already inserted.\n // This prevents D2 multiplicity from going above 1, which would\n // cause deletes to not properly remove items (multiplicity would\n // go from 2 to 1 instead of 1 to 0).\n continue\n } else if (change.type === `delete`) {\n // Remove from sentKeys so future inserts for this key are allowed\n // (e.g., after truncate + reinsert)\n this.sentKeys.delete(change.key)\n }\n }\n newChanges.push(newChange)\n }\n return newChanges\n }\n\n /**\n * After a failed replay, the source collection is empty but subscribers still\n * hold the last good publication. Reconcile the first later source delta for\n * each retained key against that publication instead of treating it as a\n * duplicate insert.\n */\n private reconcileStalePublishedChanges(\n changes: Array<ChangeMessage<any, any>>,\n ): Array<ChangeMessage<any, any>> {\n if (this.stalePublishedRows.size === 0) return changes\n\n const reconciled: Array<ChangeMessage<any, any>> = []\n for (const change of changes) {\n const previous = this.stalePublishedRows.get(change.key)\n if (previous === undefined) {\n reconciled.push(change)\n continue\n }\n\n this.stalePublishedRows.delete(change.key)\n if (change.type === `delete`) {\n reconciled.push({\n ...change,\n value: previous,\n previousValue: undefined,\n })\n } else if (!deepEquals(previous, change.value)) {\n reconciled.push({\n ...change,\n type: `update`,\n previousValue: previous,\n })\n }\n }\n return reconciled\n }\n\n private trackPublishedRows(\n changes: Array<ChangeMessage<any, string | number>>,\n ): void {\n for (const change of changes) {\n if (change.type === `delete`) {\n this.publishedRows.delete(change.key)\n } else {\n this.publishedRows.set(change.key, change.value)\n }\n }\n }\n\n private trackSentKeys(changes: Array<ChangeMessage<any, string | number>>) {\n if (this.loadedInitialState || this.skipFiltering) {\n // No need to track sent keys if we loaded the entire state or filtering is skipped.\n // Since filtering won't be applied, all keys are effectively \"observed\".\n return\n }\n\n for (const change of changes) {\n if (change.type === `delete`) {\n this.sentKeys.delete(change.key)\n } else {\n this.sentKeys.add(change.key)\n }\n }\n\n // Keep the limited snapshot offset in sync with keys we've actually sent.\n // This matters when loadSubset resolves asynchronously and requestLimitedSnapshot\n // didn't have local rows to count yet.\n if (this.orderByIndex) {\n this.limitedSnapshotRowCount = Math.max(\n this.limitedSnapshotRowCount,\n this.sentKeys.size,\n )\n }\n }\n\n /**\n * Mark that the subscription should not filter any changes.\n * This is used when includeInitialState is explicitly set to false,\n * meaning the caller doesn't want initial state but does want ALL future changes.\n */\n markAllStateAsSeen() {\n this.skipFiltering = true\n }\n\n unsubscribe() {\n let firstCleanupError: unknown\n\n // Clean up truncate event listener\n try {\n this.truncateCleanup?.()\n } catch (error) {\n firstCleanupError = error\n }\n this.truncateCleanup = undefined\n\n // Stop any buffered replay from publishing after unsubscription.\n this.truncateReplaySession = undefined\n this.stalePublishedRows.clear()\n\n // Release the current adapter acquisition for each logical subset demand.\n for (const demand of this.subsetDemands) {\n try {\n this.releaseSubsetDemand(demand)\n } catch (error) {\n firstCleanupError ??= error\n }\n }\n this.subsetDemands = []\n\n try {\n this.emitInner(`unsubscribed`, {\n type: `unsubscribed`,\n subscription: this,\n })\n } catch (error) {\n firstCleanupError ??= error\n } finally {\n // Clear all event listeners to prevent memory leaks\n this.clearListeners()\n }\n\n if (firstCleanupError !== undefined) throw firstCleanupError\n }\n}\n"],"names":["EventEmitter","ensureIndexForExpression","createFilteredCallback","createFilterFunctionFromExpression","deepEquals","and","demand","eq","Value","compileExpression","PropRef","buildCursor","gte","lt"],"mappings":"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"}