@tanstack/db 0.5.24 → 0.5.25
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection/change-events.cjs +1 -1
- package/dist/cjs/collection/change-events.cjs.map +1 -1
- package/dist/cjs/collection/changes.cjs +6 -1
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/lifecycle.cjs +11 -0
- package/dist/cjs/collection/lifecycle.cjs.map +1 -1
- package/dist/cjs/collection/subscription.cjs +18 -5
- package/dist/cjs/collection/subscription.cjs.map +1 -1
- package/dist/cjs/collection/subscription.d.cts +7 -1
- package/dist/cjs/indexes/base-index.cjs.map +1 -1
- package/dist/cjs/indexes/base-index.d.cts +10 -6
- package/dist/cjs/indexes/btree-index.cjs +64 -24
- package/dist/cjs/indexes/btree-index.cjs.map +1 -1
- package/dist/cjs/indexes/btree-index.d.cts +31 -9
- package/dist/cjs/indexes/reverse-index.cjs +6 -0
- package/dist/cjs/indexes/reverse-index.cjs.map +1 -1
- package/dist/cjs/indexes/reverse-index.d.cts +4 -2
- package/dist/cjs/query/live/collection-config-builder.cjs +4 -1
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +111 -30
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.d.cts +5 -0
- package/dist/cjs/types.d.cts +16 -0
- package/dist/cjs/utils/comparison.cjs +16 -0
- package/dist/cjs/utils/comparison.cjs.map +1 -1
- package/dist/cjs/utils/comparison.d.cts +21 -0
- package/dist/esm/collection/change-events.js +1 -1
- package/dist/esm/collection/change-events.js.map +1 -1
- package/dist/esm/collection/changes.js +6 -1
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/lifecycle.js +11 -0
- package/dist/esm/collection/lifecycle.js.map +1 -1
- package/dist/esm/collection/subscription.d.ts +7 -1
- package/dist/esm/collection/subscription.js +18 -5
- package/dist/esm/collection/subscription.js.map +1 -1
- package/dist/esm/indexes/base-index.d.ts +10 -6
- package/dist/esm/indexes/base-index.js.map +1 -1
- package/dist/esm/indexes/btree-index.d.ts +31 -9
- package/dist/esm/indexes/btree-index.js +65 -25
- package/dist/esm/indexes/btree-index.js.map +1 -1
- package/dist/esm/indexes/reverse-index.d.ts +4 -2
- package/dist/esm/indexes/reverse-index.js +6 -0
- package/dist/esm/indexes/reverse-index.js.map +1 -1
- package/dist/esm/query/live/collection-config-builder.js +4 -1
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.d.ts +5 -0
- package/dist/esm/query/live/collection-subscriber.js +112 -31
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/types.d.ts +16 -0
- package/dist/esm/utils/comparison.d.ts +21 -0
- package/dist/esm/utils/comparison.js +16 -0
- package/dist/esm/utils/comparison.js.map +1 -1
- package/package.json +1 -1
- package/src/collection/change-events.ts +1 -1
- package/src/collection/changes.ts +6 -1
- package/src/collection/lifecycle.ts +14 -0
- package/src/collection/subscription.ts +38 -10
- package/src/indexes/base-index.ts +19 -6
- package/src/indexes/btree-index.ts +101 -30
- package/src/indexes/reverse-index.ts +13 -2
- package/src/query/live/collection-config-builder.ts +4 -5
- package/src/query/live/collection-subscriber.ts +173 -50
- package/src/types.ts +16 -0
- package/src/utils/comparison.ts +34 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Track load promises using subscription from the event (avoids circular dependency)\n const trackLoadPromise = (subscription: CollectionSubscription) => {\n // Guard against duplicate transitions\n if (!this.subscriptionLoadingPromises.has(subscription)) {\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Create subscription with onStatusChange - listener is registered before snapshot\n // Note: For non-ordered queries (no limit/offset), we use trackLoadSubsetPromise: false\n // which is the default behavior in subscribeChanges\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changes)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request - onStatusChange listener is already registered\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n // This works for both single-column and multi-column orderBy:\n // - Single-column: index provides exact ordering\n // - Multi-column: index provides ordering on first column, secondary sort in memory\n subscription.setOrderByIndex(index)\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n // so the sync layer can optimize if the backend supports it\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n trackedChanges,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n const minValues =\n extractedValues !== undefined\n ? Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n : undefined\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n offset,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ) {\n for (const change of changes) {\n // Only track inserts/updates for cursor positioning, not deletes\n if (change.type !== `delete`) {\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n }\n\n yield change\n }\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":["subscription"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet, serializeValue } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track the most recent ordered load request key (cursor + window).\n // This avoids infinite loops from cached data re-writes while still allowing\n // window moves or new keys at the same cursor value to trigger new requests.\n private lastLoadRequestKey: string | undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n // Direct load tracking callback for ordered path (set during subscribeToOrderedChanges,\n // used by loadNextItems for subsequent requestLimitedSnapshot calls)\n private orderedLoadSubsetResult?: (result: Promise<void> | true) => void\n private pendingOrderedLoadPromise: Promise<void> | undefined\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Direct load promise tracking: pipes loadSubset results straight to the\n // live query collection, avoiding the multi-hop deferred promise chain that\n // can break under microtask timing (e.g., queueMicrotask in TanStack Query).\n const trackLoadResult = (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n result,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions.\n // Used as a fallback for status transitions not covered by direct tracking\n // (e.g., truncate-triggered reloads that call trackLoadSubsetPromise directly).\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n this.ensureLoadingPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n trackLoadResult,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n this.ensureLoadingPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Get the query's orderBy and limit to pass to loadSubset.\n // Only include orderBy when it is scoped to this alias and uses simple refs,\n // to avoid leaking cross-collection paths into backend-specific compilers.\n const { orderBy, limit, offset } = this.collectionConfigBuilder.query\n const effectiveLimit =\n limit !== undefined && offset !== undefined ? limit + offset : limit\n const normalizedOrderBy = orderBy\n ? normalizeOrderByPaths(orderBy, this.alias)\n : undefined\n const canPassOrderBy =\n normalizedOrderBy?.every((clause) => {\n const exp = clause.expression\n if (exp.type !== `ref`) {\n return false\n }\n const path = exp.path\n return Array.isArray(path) && path.length === 1\n }) ?? false\n const orderByForSubscription = canPassOrderBy\n ? normalizedOrderBy\n : undefined\n const limitForSubscription = canPassOrderBy ? effectiveLimit : undefined\n\n // Track loading via the loadSubset promise directly.\n // requestSnapshot uses trackLoadSubsetPromise: false (needed for truncate handling),\n // so we use onLoadSubsetResult to get the promise and track it ourselves.\n const onLoadSubsetResult = includeInitialState\n ? (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n result,\n )\n }\n }\n : undefined\n\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n orderBy: orderByForSubscription,\n limit: limitForSubscription,\n onLoadSubsetResult,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n onLoadSubsetResult: (result: Promise<void> | true) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Store the callback so loadNextItems can also use direct tracking.\n // Track in-flight ordered loads to avoid issuing redundant requests while\n // a previous snapshot is still pending.\n const handleLoadSubsetResult = (result: Promise<void> | true) => {\n if (result instanceof Promise) {\n this.pendingOrderedLoadPromise = result\n result.finally(() => {\n if (this.pendingOrderedLoadPromise === result) {\n this.pendingOrderedLoadPromise = undefined\n }\n })\n }\n onLoadSubsetResult(result)\n }\n\n this.orderedLoadSubsetResult = handleLoadSubsetResult\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n\n this.trackSentValues(changesArray, orderByInfo.comparator)\n\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changesArray)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.lastLoadRequestKey = undefined\n this.pendingOrderedLoadPromise = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request — use direct load tracking (trackLoadSubsetPromise: false)\n // to pipe the loadSubset result straight to the live query collection. This bypasses\n // the subscription status → onStatusChange → deferred promise chain which is fragile\n // under microtask timing (e.g., queueMicrotask delays in TanStack Query observers).\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n subscription.setOrderByIndex(index)\n\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: handleLoadSubsetResult,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: handleLoadSubsetResult,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n if (this.pendingOrderedLoadPromise) {\n // Wait for in-flight ordered loads to resolve before issuing another request.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n changes,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n let minValues: Array<unknown> | undefined\n if (extractedValues !== undefined) {\n minValues = Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n }\n\n const loadRequestKey = this.getLoadRequestKey({\n minValues,\n offset,\n limit: n,\n })\n\n // Skip if we already requested a load for this cursor+window.\n // This prevents infinite loops from cached data re-writes while still allowing\n // window moves (offset/limit changes) to trigger new requests.\n if (this.lastLoadRequestKey === loadRequestKey) {\n return\n }\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n // Omit offset so requestLimitedSnapshot can advance the offset based on\n // the number of rows already loaded (supports offset-based backends).\n trackLoadSubsetPromise: false,\n onLoadSubsetResult: this.orderedLoadSubsetResult,\n })\n\n this.lastLoadRequestKey = loadRequestKey\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private trackSentValues(\n changes: Array<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ): void {\n for (const change of changes) {\n if (change.type === `delete`) {\n continue\n }\n\n const isNewKey = !this.sentToD2Keys.has(change.key)\n\n // Only track inserts/updates for cursor positioning, not deletes\n if (!this.biggest) {\n this.biggest = change.value\n this.lastLoadRequestKey = undefined\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n this.lastLoadRequestKey = undefined\n } else if (isNewKey) {\n // New key with same orderBy value - allow another load if needed\n this.lastLoadRequestKey = undefined\n }\n }\n }\n\n private ensureLoadingPromise(subscription: CollectionSubscription) {\n if (this.subscriptionLoadingPromises.has(subscription)) {\n return\n }\n\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n\n private getLoadRequestKey(options: {\n minValues: Array<unknown> | undefined\n offset: number\n limit: number\n }): string {\n return serializeValue({\n minValues: options.minValues ?? null,\n offset: options.offset,\n limit: options.limit,\n })\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":["subscription"],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -628,6 +628,22 @@ export interface SubscribeChangesOptions<T extends object = Record<string, unkno
|
|
|
628
628
|
* @internal
|
|
629
629
|
*/
|
|
630
630
|
onStatusChange?: (event: SubscriptionStatusChangeEvent) => void;
|
|
631
|
+
/**
|
|
632
|
+
* Optional orderBy to include in loadSubset for query-specific cache keys.
|
|
633
|
+
* @internal
|
|
634
|
+
*/
|
|
635
|
+
orderBy?: OrderBy;
|
|
636
|
+
/**
|
|
637
|
+
* Optional limit to include in loadSubset for query-specific cache keys.
|
|
638
|
+
* @internal
|
|
639
|
+
*/
|
|
640
|
+
limit?: number;
|
|
641
|
+
/**
|
|
642
|
+
* Callback that receives the loadSubset result (Promise or true) from requestSnapshot.
|
|
643
|
+
* Allows the caller to directly track the loading promise for isReady status.
|
|
644
|
+
* @internal
|
|
645
|
+
*/
|
|
646
|
+
onLoadSubsetResult?: (result: Promise<void> | true) => void;
|
|
631
647
|
}
|
|
632
648
|
export interface SubscribeChangesSnapshotOptions<T extends object = Record<string, unknown>> extends Omit<SubscribeChangesOptions<T>, `includeInitialState`> {
|
|
633
649
|
orderBy?: OrderBy;
|
|
@@ -13,12 +13,33 @@ export declare const descComparator: (a: unknown, b: unknown, opts: CompareOptio
|
|
|
13
13
|
export declare function makeComparator(opts: CompareOptions): (a: any, b: any) => number;
|
|
14
14
|
/** Default comparator orders values in ascending order with nulls first and locale string comparison. */
|
|
15
15
|
export declare const defaultComparator: (a: any, b: any) => number;
|
|
16
|
+
/**
|
|
17
|
+
* Sentinel value representing undefined in normalized form.
|
|
18
|
+
* This allows distinguishing between "start from beginning" (undefined parameter)
|
|
19
|
+
* and "start from the key undefined" (actual undefined value in the tree).
|
|
20
|
+
*/
|
|
21
|
+
export declare const UNDEFINED_SENTINEL = "__TS_DB_BTREE_UNDEFINED_VALUE__";
|
|
16
22
|
/**
|
|
17
23
|
* Normalize a value for comparison and Map key usage
|
|
18
24
|
* Converts values that can't be directly compared or used as Map keys
|
|
19
25
|
* into comparable primitive representations
|
|
26
|
+
*
|
|
27
|
+
* Note: This does NOT convert undefined to a sentinel. Use normalizeForBTree
|
|
28
|
+
* for BTree index operations that need to distinguish undefined values.
|
|
20
29
|
*/
|
|
21
30
|
export declare function normalizeValue(value: any): any;
|
|
31
|
+
/**
|
|
32
|
+
* Normalize a value for BTree index usage.
|
|
33
|
+
* Extends normalizeValue to also convert undefined to a sentinel value.
|
|
34
|
+
* This is needed because the BTree does not properly support `undefined` as a key
|
|
35
|
+
* (it interprets undefined as "start from beginning" in nextHigherPair/nextLowerPair).
|
|
36
|
+
*/
|
|
37
|
+
export declare function normalizeForBTree(value: any): any;
|
|
38
|
+
/**
|
|
39
|
+
* Converts the `UNDEFINED_SENTINEL` back to `undefined`.
|
|
40
|
+
* Needed such that the sentinel is converted back to `undefined` before comparison.
|
|
41
|
+
*/
|
|
42
|
+
export declare function denormalizeUndefined(value: any): any;
|
|
22
43
|
/**
|
|
23
44
|
* Compare two values for equality, with special handling for Uint8Arrays and Buffers
|
|
24
45
|
*/
|
|
@@ -77,6 +77,7 @@ function areUint8ArraysEqual(a, b) {
|
|
|
77
77
|
return true;
|
|
78
78
|
}
|
|
79
79
|
const UINT8ARRAY_NORMALIZE_THRESHOLD = 128;
|
|
80
|
+
const UNDEFINED_SENTINEL = `__TS_DB_BTREE_UNDEFINED_VALUE__`;
|
|
80
81
|
function normalizeValue(value) {
|
|
81
82
|
if (value instanceof Date) {
|
|
82
83
|
return value.getTime();
|
|
@@ -89,6 +90,18 @@ function normalizeValue(value) {
|
|
|
89
90
|
}
|
|
90
91
|
return value;
|
|
91
92
|
}
|
|
93
|
+
function normalizeForBTree(value) {
|
|
94
|
+
if (value === void 0) {
|
|
95
|
+
return UNDEFINED_SENTINEL;
|
|
96
|
+
}
|
|
97
|
+
return normalizeValue(value);
|
|
98
|
+
}
|
|
99
|
+
function denormalizeUndefined(value) {
|
|
100
|
+
if (value === UNDEFINED_SENTINEL) {
|
|
101
|
+
return void 0;
|
|
102
|
+
}
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
92
105
|
function areValuesEqual(a, b) {
|
|
93
106
|
if (a === b) {
|
|
94
107
|
return true;
|
|
@@ -101,11 +114,14 @@ function areValuesEqual(a, b) {
|
|
|
101
114
|
return false;
|
|
102
115
|
}
|
|
103
116
|
export {
|
|
117
|
+
UNDEFINED_SENTINEL,
|
|
104
118
|
areValuesEqual,
|
|
105
119
|
ascComparator,
|
|
106
120
|
defaultComparator,
|
|
121
|
+
denormalizeUndefined,
|
|
107
122
|
descComparator,
|
|
108
123
|
makeComparator,
|
|
124
|
+
normalizeForBTree,
|
|
109
125
|
normalizeValue
|
|
110
126
|
};
|
|
111
127
|
//# sourceMappingURL=comparison.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"comparison.js","sources":["../../../src/utils/comparison.ts"],"sourcesContent":["import type { CompareOptions } from '../query/builder/types'\n\n// WeakMap to store stable IDs for objects\nconst objectIds = new WeakMap<object, number>()\nlet nextObjectId = 1\n\n/**\n * Get or create a stable ID for an object\n */\nfunction getObjectId(obj: object): number {\n if (objectIds.has(obj)) {\n return objectIds.get(obj)!\n }\n const id = nextObjectId++\n objectIds.set(obj, id)\n return id\n}\n\n/**\n * Universal comparison function for all data types\n * Handles null/undefined, strings, arrays, dates, objects, and primitives\n * Always sorts null/undefined values first\n */\nexport const ascComparator = (a: any, b: any, opts: CompareOptions): number => {\n const { nulls } = opts\n\n // Handle null/undefined\n if (a == null && b == null) return 0\n if (a == null) return nulls === `first` ? -1 : 1\n if (b == null) return nulls === `first` ? 1 : -1\n\n // if a and b are both strings, compare them based on locale\n if (typeof a === `string` && typeof b === `string`) {\n if (opts.stringSort === `locale`) {\n return a.localeCompare(b, opts.locale, opts.localeOptions)\n }\n // For lexical sort we rely on direct comparison for primitive values\n }\n\n // if a and b are both arrays, compare them element by element\n if (Array.isArray(a) && Array.isArray(b)) {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const result = ascComparator(a[i], b[i], opts)\n if (result !== 0) {\n return result\n }\n }\n // All elements are equal up to the minimum length\n return a.length - b.length\n }\n\n // If both are dates, compare them\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime()\n }\n\n // If at least one of the values is an object, use stable IDs for comparison\n const aIsObject = typeof a === `object`\n const bIsObject = typeof b === `object`\n\n if (aIsObject || bIsObject) {\n // If both are objects, compare their stable IDs\n if (aIsObject && bIsObject) {\n const aId = getObjectId(a)\n const bId = getObjectId(b)\n return aId - bId\n }\n\n // If only one is an object, objects come after primitives\n if (aIsObject) return 1\n if (bIsObject) return -1\n }\n\n // For primitive values, use direct comparison\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\n/**\n * Descending comparator function for ordering values\n * Handles null/undefined as largest values (opposite of ascending)\n */\nexport const descComparator = (\n a: unknown,\n b: unknown,\n opts: CompareOptions,\n): number => {\n return ascComparator(b, a, {\n ...opts,\n nulls: opts.nulls === `first` ? `last` : `first`,\n })\n}\n\nexport function makeComparator(\n opts: CompareOptions,\n): (a: any, b: any) => number {\n return (a, b) => {\n if (opts.direction === `asc`) {\n return ascComparator(a, b, opts)\n } else {\n return descComparator(a, b, opts)\n }\n }\n}\n\n/** Default comparator orders values in ascending order with nulls first and locale string comparison. */\nexport const defaultComparator = makeComparator({\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n})\n\n/**\n * Compare two Uint8Arrays for content equality\n */\nfunction areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) {\n return false\n }\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) {\n return false\n }\n }\n return true\n}\n\n/**\n * Threshold for normalizing Uint8Arrays to string representations.\n * Arrays larger than this will use reference equality to avoid memory overhead.\n * 128 bytes is enough for common ID formats (ULIDs are 16 bytes, UUIDs are 16 bytes)\n * while avoiding excessive string allocation for large binary data.\n */\nconst UINT8ARRAY_NORMALIZE_THRESHOLD = 128\n\n/**\n * Normalize a value for comparison and Map key usage\n * Converts values that can't be directly compared or used as Map keys\n * into comparable primitive representations\n */\nexport function normalizeValue(value: any): any {\n if (value instanceof Date) {\n return value.getTime()\n }\n\n // Normalize Uint8Arrays/Buffers to a string representation for Map key usage\n // This enables content-based equality for binary data like ULIDs\n const isUint8Array =\n (typeof Buffer !== `undefined` && value instanceof Buffer) ||\n value instanceof Uint8Array\n\n if (isUint8Array) {\n // Only normalize small arrays to avoid memory overhead for large binary data\n if (value.byteLength <= UINT8ARRAY_NORMALIZE_THRESHOLD) {\n // Convert to a string representation that can be used as a Map key\n // Use a special prefix to avoid collisions with user strings\n return `__u8__${Array.from(value).join(`,`)}`\n }\n // For large arrays, fall back to reference equality\n // Users working with large binary data should use a derived key if needed\n }\n\n return value\n}\n\n/**\n * Compare two values for equality, with special handling for Uint8Arrays and Buffers\n */\nexport function areValuesEqual(a: any, b: any): boolean {\n // Fast path for reference equality\n if (a === b) {\n return true\n }\n\n // Check for Uint8Array/Buffer comparison\n const aIsUint8Array =\n (typeof Buffer !== `undefined` && a instanceof Buffer) ||\n a instanceof Uint8Array\n const bIsUint8Array =\n (typeof Buffer !== `undefined` && b instanceof Buffer) ||\n b instanceof Uint8Array\n\n // If both are Uint8Arrays, compare by content\n if (aIsUint8Array && bIsUint8Array) {\n return areUint8ArraysEqual(a, b)\n }\n\n // Different types or not Uint8Arrays\n return false\n}\n"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"comparison.js","sources":["../../../src/utils/comparison.ts"],"sourcesContent":["import type { CompareOptions } from '../query/builder/types'\n\n// WeakMap to store stable IDs for objects\nconst objectIds = new WeakMap<object, number>()\nlet nextObjectId = 1\n\n/**\n * Get or create a stable ID for an object\n */\nfunction getObjectId(obj: object): number {\n if (objectIds.has(obj)) {\n return objectIds.get(obj)!\n }\n const id = nextObjectId++\n objectIds.set(obj, id)\n return id\n}\n\n/**\n * Universal comparison function for all data types\n * Handles null/undefined, strings, arrays, dates, objects, and primitives\n * Always sorts null/undefined values first\n */\nexport const ascComparator = (a: any, b: any, opts: CompareOptions): number => {\n const { nulls } = opts\n\n // Handle null/undefined\n if (a == null && b == null) return 0\n if (a == null) return nulls === `first` ? -1 : 1\n if (b == null) return nulls === `first` ? 1 : -1\n\n // if a and b are both strings, compare them based on locale\n if (typeof a === `string` && typeof b === `string`) {\n if (opts.stringSort === `locale`) {\n return a.localeCompare(b, opts.locale, opts.localeOptions)\n }\n // For lexical sort we rely on direct comparison for primitive values\n }\n\n // if a and b are both arrays, compare them element by element\n if (Array.isArray(a) && Array.isArray(b)) {\n for (let i = 0; i < Math.min(a.length, b.length); i++) {\n const result = ascComparator(a[i], b[i], opts)\n if (result !== 0) {\n return result\n }\n }\n // All elements are equal up to the minimum length\n return a.length - b.length\n }\n\n // If both are dates, compare them\n if (a instanceof Date && b instanceof Date) {\n return a.getTime() - b.getTime()\n }\n\n // If at least one of the values is an object, use stable IDs for comparison\n const aIsObject = typeof a === `object`\n const bIsObject = typeof b === `object`\n\n if (aIsObject || bIsObject) {\n // If both are objects, compare their stable IDs\n if (aIsObject && bIsObject) {\n const aId = getObjectId(a)\n const bId = getObjectId(b)\n return aId - bId\n }\n\n // If only one is an object, objects come after primitives\n if (aIsObject) return 1\n if (bIsObject) return -1\n }\n\n // For primitive values, use direct comparison\n if (a < b) return -1\n if (a > b) return 1\n return 0\n}\n\n/**\n * Descending comparator function for ordering values\n * Handles null/undefined as largest values (opposite of ascending)\n */\nexport const descComparator = (\n a: unknown,\n b: unknown,\n opts: CompareOptions,\n): number => {\n return ascComparator(b, a, {\n ...opts,\n nulls: opts.nulls === `first` ? `last` : `first`,\n })\n}\n\nexport function makeComparator(\n opts: CompareOptions,\n): (a: any, b: any) => number {\n return (a, b) => {\n if (opts.direction === `asc`) {\n return ascComparator(a, b, opts)\n } else {\n return descComparator(a, b, opts)\n }\n }\n}\n\n/** Default comparator orders values in ascending order with nulls first and locale string comparison. */\nexport const defaultComparator = makeComparator({\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n})\n\n/**\n * Compare two Uint8Arrays for content equality\n */\nfunction areUint8ArraysEqual(a: Uint8Array, b: Uint8Array): boolean {\n if (a.byteLength !== b.byteLength) {\n return false\n }\n for (let i = 0; i < a.byteLength; i++) {\n if (a[i] !== b[i]) {\n return false\n }\n }\n return true\n}\n\n/**\n * Threshold for normalizing Uint8Arrays to string representations.\n * Arrays larger than this will use reference equality to avoid memory overhead.\n * 128 bytes is enough for common ID formats (ULIDs are 16 bytes, UUIDs are 16 bytes)\n * while avoiding excessive string allocation for large binary data.\n */\nconst UINT8ARRAY_NORMALIZE_THRESHOLD = 128\n\n/**\n * Sentinel value representing undefined in normalized form.\n * This allows distinguishing between \"start from beginning\" (undefined parameter)\n * and \"start from the key undefined\" (actual undefined value in the tree).\n */\nexport const UNDEFINED_SENTINEL = `__TS_DB_BTREE_UNDEFINED_VALUE__`\n\n/**\n * Normalize a value for comparison and Map key usage\n * Converts values that can't be directly compared or used as Map keys\n * into comparable primitive representations\n *\n * Note: This does NOT convert undefined to a sentinel. Use normalizeForBTree\n * for BTree index operations that need to distinguish undefined values.\n */\nexport function normalizeValue(value: any): any {\n if (value instanceof Date) {\n return value.getTime()\n }\n\n // Normalize Uint8Arrays/Buffers to a string representation for Map key usage\n // This enables content-based equality for binary data like ULIDs\n const isUint8Array =\n (typeof Buffer !== `undefined` && value instanceof Buffer) ||\n value instanceof Uint8Array\n\n if (isUint8Array) {\n // Only normalize small arrays to avoid memory overhead for large binary data\n if (value.byteLength <= UINT8ARRAY_NORMALIZE_THRESHOLD) {\n // Convert to a string representation that can be used as a Map key\n // Use a special prefix to avoid collisions with user strings\n return `__u8__${Array.from(value).join(`,`)}`\n }\n // For large arrays, fall back to reference equality\n // Users working with large binary data should use a derived key if needed\n }\n\n return value\n}\n\n/**\n * Normalize a value for BTree index usage.\n * Extends normalizeValue to also convert undefined to a sentinel value.\n * This is needed because the BTree does not properly support `undefined` as a key\n * (it interprets undefined as \"start from beginning\" in nextHigherPair/nextLowerPair).\n */\nexport function normalizeForBTree(value: any): any {\n if (value === undefined) {\n return UNDEFINED_SENTINEL\n }\n return normalizeValue(value)\n}\n\n/**\n * Converts the `UNDEFINED_SENTINEL` back to `undefined`.\n * Needed such that the sentinel is converted back to `undefined` before comparison.\n */\nexport function denormalizeUndefined(value: any): any {\n if (value === UNDEFINED_SENTINEL) {\n return undefined\n }\n return value\n}\n\n/**\n * Compare two values for equality, with special handling for Uint8Arrays and Buffers\n */\nexport function areValuesEqual(a: any, b: any): boolean {\n // Fast path for reference equality\n if (a === b) {\n return true\n }\n\n // Check for Uint8Array/Buffer comparison\n const aIsUint8Array =\n (typeof Buffer !== `undefined` && a instanceof Buffer) ||\n a instanceof Uint8Array\n const bIsUint8Array =\n (typeof Buffer !== `undefined` && b instanceof Buffer) ||\n b instanceof Uint8Array\n\n // If both are Uint8Arrays, compare by content\n if (aIsUint8Array && bIsUint8Array) {\n return areUint8ArraysEqual(a, b)\n }\n\n // Different types or not Uint8Arrays\n return false\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -347,7 +347,7 @@ function getOrderedKeys<T extends object, TKey extends string | number>(
|
|
|
347
347
|
// Take the keys that match the filter and limit
|
|
348
348
|
// if no limit is provided `index.keyCount` is used,
|
|
349
349
|
// i.e. we will take all keys that match the filter
|
|
350
|
-
return index.
|
|
350
|
+
return index.takeFromStart(limit ?? index.keyCount, filterFn)
|
|
351
351
|
}
|
|
352
352
|
}
|
|
353
353
|
}
|
|
@@ -135,7 +135,12 @@ export class CollectionChangesManager<
|
|
|
135
135
|
}
|
|
136
136
|
|
|
137
137
|
if (options.includeInitialState) {
|
|
138
|
-
subscription.requestSnapshot({
|
|
138
|
+
subscription.requestSnapshot({
|
|
139
|
+
trackLoadSubsetPromise: false,
|
|
140
|
+
orderBy: options.orderBy,
|
|
141
|
+
limit: options.limit,
|
|
142
|
+
onLoadSubsetResult: options.onLoadSubsetResult,
|
|
143
|
+
})
|
|
139
144
|
} else if (options.includeInitialState === false) {
|
|
140
145
|
// When explicitly set to false (not just undefined), mark all state as "seen"
|
|
141
146
|
// so that all future changes (including deletes) pass through unfiltered.
|
|
@@ -264,7 +264,21 @@ export class CollectionLifecycleManager<
|
|
|
264
264
|
}
|
|
265
265
|
|
|
266
266
|
this.hasBeenReady = false
|
|
267
|
+
|
|
268
|
+
// Call any pending onFirstReady callbacks before clearing them.
|
|
269
|
+
// This ensures preload() promises resolve during cleanup instead of hanging.
|
|
270
|
+
const callbacks = [...this.onFirstReadyCallbacks]
|
|
267
271
|
this.onFirstReadyCallbacks = []
|
|
272
|
+
callbacks.forEach((callback) => {
|
|
273
|
+
try {
|
|
274
|
+
callback()
|
|
275
|
+
} catch (error) {
|
|
276
|
+
console.error(
|
|
277
|
+
`${this.config.id ? `[${this.config.id}] ` : ``}Error in onFirstReady callback during cleanup:`,
|
|
278
|
+
error,
|
|
279
|
+
)
|
|
280
|
+
}
|
|
281
|
+
})
|
|
268
282
|
|
|
269
283
|
// Set status to cleaned-up after everything is cleaned up
|
|
270
284
|
// This fires the status:change event to notify listeners
|
|
@@ -28,6 +28,8 @@ type RequestSnapshotOptions = {
|
|
|
28
28
|
orderBy?: OrderBy
|
|
29
29
|
/** Optional limit to pass to loadSubset for backend optimization */
|
|
30
30
|
limit?: number
|
|
31
|
+
/** Callback that receives the raw loadSubset result for external tracking */
|
|
32
|
+
onLoadSubsetResult?: (result: Promise<void> | true) => void
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
type RequestLimitedSnapshotOptions = {
|
|
@@ -37,6 +39,10 @@ type RequestLimitedSnapshotOptions = {
|
|
|
37
39
|
minValues?: Array<unknown>
|
|
38
40
|
/** Row offset for offset-based pagination (passed to sync layer) */
|
|
39
41
|
offset?: number
|
|
42
|
+
/** Whether to track the loadSubset promise on this subscription (default: true) */
|
|
43
|
+
trackLoadSubsetPromise?: boolean
|
|
44
|
+
/** Callback that receives the raw loadSubset result for external tracking */
|
|
45
|
+
onLoadSubsetResult?: (result: Promise<void> | true) => void
|
|
40
46
|
}
|
|
41
47
|
|
|
42
48
|
type CollectionSubscriptionOptions = {
|
|
@@ -365,6 +371,9 @@ export class CollectionSubscription
|
|
|
365
371
|
}
|
|
366
372
|
const syncResult = this.collection._sync.loadSubset(loadOptions)
|
|
367
373
|
|
|
374
|
+
// Pass the raw loadSubset result to the caller for external tracking
|
|
375
|
+
opts?.onLoadSubsetResult?.(syncResult)
|
|
376
|
+
|
|
368
377
|
// Track this loadSubset call so we can unload it later
|
|
369
378
|
this.loadedSubsets.push(loadOptions)
|
|
370
379
|
|
|
@@ -416,6 +425,8 @@ export class CollectionSubscription
|
|
|
416
425
|
limit,
|
|
417
426
|
minValues,
|
|
418
427
|
offset,
|
|
428
|
+
trackLoadSubsetPromise: shouldTrackLoadSubsetPromise = true,
|
|
429
|
+
onLoadSubsetResult,
|
|
419
430
|
}: RequestLimitedSnapshotOptions) {
|
|
420
431
|
if (!limit) throw new Error(`limit is required`)
|
|
421
432
|
|
|
@@ -425,6 +436,9 @@ export class CollectionSubscription
|
|
|
425
436
|
)
|
|
426
437
|
}
|
|
427
438
|
|
|
439
|
+
// Check if minValues has a first element (regardless of its value)
|
|
440
|
+
// This distinguishes between "no min value provided" vs "min value is undefined"
|
|
441
|
+
const hasMinValue = minValues !== undefined && minValues.length > 0
|
|
428
442
|
// Derive first column value from minValues (used for local index operations)
|
|
429
443
|
const minValue = minValues?.[0]
|
|
430
444
|
// Cast for index operations (index expects string | number)
|
|
@@ -436,8 +450,8 @@ export class CollectionSubscription
|
|
|
436
450
|
? createFilterFunctionFromExpression(where)
|
|
437
451
|
: undefined
|
|
438
452
|
|
|
439
|
-
const filterFn = (key: string | number): boolean => {
|
|
440
|
-
if (this.sentKeys.has(key)) {
|
|
453
|
+
const filterFn = (key: string | number | undefined): boolean => {
|
|
454
|
+
if (key !== undefined && this.sentKeys.has(key)) {
|
|
441
455
|
return false
|
|
442
456
|
}
|
|
443
457
|
|
|
@@ -462,7 +476,7 @@ export class CollectionSubscription
|
|
|
462
476
|
// For multi-column orderBy, we use the first column value for index operations (wide bounds)
|
|
463
477
|
// This may load some duplicates but ensures we never miss any rows.
|
|
464
478
|
let keys: Array<string | number> = []
|
|
465
|
-
if (
|
|
479
|
+
if (hasMinValue) {
|
|
466
480
|
// First, get all items with the same FIRST COLUMN value as minValue
|
|
467
481
|
// This provides wide bounds for the local index
|
|
468
482
|
const { expression } = orderBy[0]!
|
|
@@ -481,15 +495,16 @@ export class CollectionSubscription
|
|
|
481
495
|
// Then get items greater than minValue
|
|
482
496
|
const keysGreaterThanMin = index.take(
|
|
483
497
|
limit - keys.length,
|
|
484
|
-
minValueForIndex
|
|
498
|
+
minValueForIndex!,
|
|
485
499
|
filterFn,
|
|
486
500
|
)
|
|
487
501
|
keys.push(...keysGreaterThanMin)
|
|
488
502
|
} else {
|
|
489
|
-
keys = index.take(limit, minValueForIndex
|
|
503
|
+
keys = index.take(limit, minValueForIndex!, filterFn)
|
|
490
504
|
}
|
|
491
505
|
} else {
|
|
492
|
-
|
|
506
|
+
// No min value provided, start from the beginning
|
|
507
|
+
keys = index.takeFromStart(limit, filterFn)
|
|
493
508
|
}
|
|
494
509
|
|
|
495
510
|
const valuesNeeded = () => Math.max(limit - changes.length, 0)
|
|
@@ -518,7 +533,7 @@ export class CollectionSubscription
|
|
|
518
533
|
insertedKeys.add(key) // Track this key
|
|
519
534
|
}
|
|
520
535
|
|
|
521
|
-
keys = index.take(valuesNeeded(), biggestObservedValue
|
|
536
|
+
keys = index.take(valuesNeeded(), biggestObservedValue!, filterFn)
|
|
522
537
|
}
|
|
523
538
|
|
|
524
539
|
// Track row count for offset-based pagination (before sending to callback)
|
|
@@ -594,9 +609,14 @@ export class CollectionSubscription
|
|
|
594
609
|
}
|
|
595
610
|
const syncResult = this.collection._sync.loadSubset(loadOptions)
|
|
596
611
|
|
|
612
|
+
// Pass the raw loadSubset result to the caller for external tracking
|
|
613
|
+
onLoadSubsetResult?.(syncResult)
|
|
614
|
+
|
|
597
615
|
// Track this loadSubset call
|
|
598
616
|
this.loadedSubsets.push(loadOptions)
|
|
599
|
-
|
|
617
|
+
if (shouldTrackLoadSubsetPromise) {
|
|
618
|
+
this.trackLoadSubsetPromise(syncResult)
|
|
619
|
+
}
|
|
600
620
|
}
|
|
601
621
|
|
|
602
622
|
// TODO: also add similar test but that checks that it can also load it from the collection's loadSubset function
|
|
@@ -667,13 +687,21 @@ export class CollectionSubscription
|
|
|
667
687
|
|
|
668
688
|
for (const change of changes) {
|
|
669
689
|
if (change.type === `delete`) {
|
|
670
|
-
// Remove deleted keys from sentKeys so future re-inserts are allowed
|
|
671
690
|
this.sentKeys.delete(change.key)
|
|
672
691
|
} else {
|
|
673
|
-
// For inserts and updates, track the key as sent
|
|
674
692
|
this.sentKeys.add(change.key)
|
|
675
693
|
}
|
|
676
694
|
}
|
|
695
|
+
|
|
696
|
+
// Keep the limited snapshot offset in sync with keys we've actually sent.
|
|
697
|
+
// This matters when loadSubset resolves asynchronously and requestLimitedSnapshot
|
|
698
|
+
// didn't have local rows to count yet.
|
|
699
|
+
if (this.orderByIndex) {
|
|
700
|
+
this.limitedSnapshotRowCount = Math.max(
|
|
701
|
+
this.limitedSnapshotRowCount,
|
|
702
|
+
this.sentKeys.size,
|
|
703
|
+
)
|
|
704
|
+
}
|
|
677
705
|
}
|
|
678
706
|
|
|
679
707
|
/**
|
|
@@ -26,7 +26,7 @@ export interface IndexStats {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface IndexInterface<
|
|
29
|
-
TKey extends string | number = string | number,
|
|
29
|
+
TKey extends string | number | undefined = string | number | undefined,
|
|
30
30
|
> {
|
|
31
31
|
add: (key: TKey, item: any) => void
|
|
32
32
|
remove: (key: TKey, item: any) => void
|
|
@@ -45,12 +45,17 @@ export interface IndexInterface<
|
|
|
45
45
|
|
|
46
46
|
take: (
|
|
47
47
|
n: number,
|
|
48
|
-
from
|
|
48
|
+
from: TKey,
|
|
49
49
|
filterFn?: (key: TKey) => boolean,
|
|
50
50
|
) => Array<TKey>
|
|
51
|
+
takeFromStart: (n: number, filterFn?: (key: TKey) => boolean) => Array<TKey>
|
|
51
52
|
takeReversed: (
|
|
52
53
|
n: number,
|
|
53
|
-
from
|
|
54
|
+
from: TKey,
|
|
55
|
+
filterFn?: (key: TKey) => boolean,
|
|
56
|
+
) => Array<TKey>
|
|
57
|
+
takeReversedFromEnd: (
|
|
58
|
+
n: number,
|
|
54
59
|
filterFn?: (key: TKey) => boolean,
|
|
55
60
|
) => Array<TKey>
|
|
56
61
|
|
|
@@ -74,7 +79,7 @@ export interface IndexInterface<
|
|
|
74
79
|
* Base abstract class that all index types extend
|
|
75
80
|
*/
|
|
76
81
|
export abstract class BaseIndex<
|
|
77
|
-
TKey extends string | number = string | number,
|
|
82
|
+
TKey extends string | number | undefined = string | number | undefined,
|
|
78
83
|
> implements IndexInterface<TKey> {
|
|
79
84
|
public readonly id: number
|
|
80
85
|
public readonly name?: string
|
|
@@ -108,12 +113,20 @@ export abstract class BaseIndex<
|
|
|
108
113
|
abstract lookup(operation: IndexOperation, value: any): Set<TKey>
|
|
109
114
|
abstract take(
|
|
110
115
|
n: number,
|
|
111
|
-
from
|
|
116
|
+
from: TKey,
|
|
117
|
+
filterFn?: (key: TKey) => boolean,
|
|
118
|
+
): Array<TKey>
|
|
119
|
+
abstract takeFromStart(
|
|
120
|
+
n: number,
|
|
112
121
|
filterFn?: (key: TKey) => boolean,
|
|
113
122
|
): Array<TKey>
|
|
114
123
|
abstract takeReversed(
|
|
115
124
|
n: number,
|
|
116
|
-
from
|
|
125
|
+
from: TKey,
|
|
126
|
+
filterFn?: (key: TKey) => boolean,
|
|
127
|
+
): Array<TKey>
|
|
128
|
+
abstract takeReversedFromEnd(
|
|
129
|
+
n: number,
|
|
117
130
|
filterFn?: (key: TKey) => boolean,
|
|
118
131
|
): Array<TKey>
|
|
119
132
|
abstract get keyCount(): number
|