@tanstack/db 0.5.16 → 0.5.18
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/changes.cjs +18 -1
- package/dist/cjs/collection/changes.cjs.map +1 -1
- package/dist/cjs/collection/changes.d.cts +1 -1
- package/dist/cjs/collection/index.cjs +8 -5
- package/dist/cjs/collection/index.cjs.map +1 -1
- package/dist/cjs/collection/index.d.cts +9 -6
- package/dist/cjs/errors.cjs +13 -0
- package/dist/cjs/errors.cjs.map +1 -1
- package/dist/cjs/errors.d.cts +3 -0
- package/dist/cjs/index.cjs +1 -0
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.cjs +12 -0
- package/dist/cjs/query/builder/index.cjs.map +1 -1
- package/dist/cjs/query/builder/index.d.cts +2 -1
- package/dist/cjs/query/index.d.cts +1 -1
- package/dist/cjs/query/live/collection-config-builder.cjs +10 -1
- package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
- package/dist/cjs/query/live/collection-subscriber.cjs +41 -32
- package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
- package/dist/cjs/types.d.cts +24 -2
- package/dist/cjs/utils.cjs +9 -0
- package/dist/cjs/utils.cjs.map +1 -1
- package/dist/esm/collection/changes.d.ts +1 -1
- package/dist/esm/collection/changes.js +18 -1
- package/dist/esm/collection/changes.js.map +1 -1
- package/dist/esm/collection/index.d.ts +9 -6
- package/dist/esm/collection/index.js +8 -5
- package/dist/esm/collection/index.js.map +1 -1
- package/dist/esm/errors.d.ts +3 -0
- package/dist/esm/errors.js +13 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +2 -1
- package/dist/esm/query/builder/index.d.ts +2 -1
- package/dist/esm/query/builder/index.js +13 -1
- package/dist/esm/query/builder/index.js.map +1 -1
- package/dist/esm/query/index.d.ts +1 -1
- package/dist/esm/query/live/collection-config-builder.js +10 -1
- package/dist/esm/query/live/collection-config-builder.js.map +1 -1
- package/dist/esm/query/live/collection-subscriber.js +41 -32
- package/dist/esm/query/live/collection-subscriber.js.map +1 -1
- package/dist/esm/types.d.ts +24 -2
- package/dist/esm/utils.js +9 -0
- package/dist/esm/utils.js.map +1 -1
- package/package.json +4 -4
- package/src/collection/changes.ts +29 -2
- package/src/collection/index.ts +9 -6
- package/src/errors.ts +13 -0
- package/src/query/builder/index.ts +27 -0
- package/src/query/index.ts +2 -0
- package/src/query/live/collection-config-builder.ts +23 -2
- package/src/query/live/collection-subscriber.ts +60 -41
- package/src/types.ts +28 -5
- package/src/utils.ts +20 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"collection-subscriber.cjs","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 { ChangeMessage } 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 let subscription: CollectionSubscription\n const orderByInfo = this.getOrderByInfo()\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\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 )\n }\n\n const trackLoadPromise = () => {\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 // It can be that we are not yet subscribed when the first `loadSubset` call happens (i.e. the initial query).\n // So we also check the status here and if it's `loadingSubset` then we track the load promise\n if (subscription.status === `loadingSubset`) {\n trackLoadPromise()\n }\n\n // Subscribe to subscription status changes to propagate loading state\n const statusUnsubscribe = subscription.on(`status:change`, (event) => {\n if (event.status === `loadingSubset`) {\n trackLoadPromise()\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 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 statusUnsubscribe()\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 = false,\n ) {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Only pass includeInitialState when true. When it's false, we leave it\n // undefined so that user subscriptions with explicit `includeInitialState: false`\n // can be distinguished from internal lazy-loading subscriptions.\n // If we pass `false`, changes.ts would call markAllStateAsSeen() which\n // disables filtering - but internal subscriptions still need filtering.\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n ) {\n const { orderBy, offset, limit, index } = orderByInfo\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(splittedChanges, subscription)\n }\n\n // Subscribe to changes and only send changes that are smaller than the biggest value we've sent so far\n // values that are bigger don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n })\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 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":["normalizeExpressionPaths","normalizeOrderByPaths","MultiSet"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"collection-subscriber.cjs","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":["normalizeExpressionPaths","subscription","normalizeOrderByPaths","MultiSet"],"mappings":"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"}
|
package/dist/cjs/types.d.cts
CHANGED
|
@@ -4,6 +4,7 @@ import { StandardSchemaV1 } from '@standard-schema/spec';
|
|
|
4
4
|
import { Transaction } from './transactions.cjs';
|
|
5
5
|
import { BasicExpression, OrderBy } from './query/ir.js';
|
|
6
6
|
import { EventEmitter } from './event-emitter.js';
|
|
7
|
+
import { SingleRowRefProxy } from './query/builder/ref-proxy.js';
|
|
7
8
|
/**
|
|
8
9
|
* Interface for a collection-like object that provides the necessary methods
|
|
9
10
|
* for the change events system to work
|
|
@@ -594,13 +595,34 @@ export type NamespacedAndKeyedStream = IStreamBuilder<KeyedNamespacedRow>;
|
|
|
594
595
|
/**
|
|
595
596
|
* Options for subscribing to collection changes
|
|
596
597
|
*/
|
|
597
|
-
export interface SubscribeChangesOptions {
|
|
598
|
+
export interface SubscribeChangesOptions<T extends object = Record<string, unknown>> {
|
|
598
599
|
/** Whether to include the current state as initial changes */
|
|
599
600
|
includeInitialState?: boolean;
|
|
601
|
+
/**
|
|
602
|
+
* Callback function for filtering changes using a row proxy.
|
|
603
|
+
* The callback receives a proxy object that records property access,
|
|
604
|
+
* allowing you to use query builder functions like `eq`, `gt`, etc.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* import { eq } from "@tanstack/db"
|
|
609
|
+
*
|
|
610
|
+
* collection.subscribeChanges(callback, {
|
|
611
|
+
* where: (row) => eq(row.status, "active")
|
|
612
|
+
* })
|
|
613
|
+
* ```
|
|
614
|
+
*/
|
|
615
|
+
where?: (row: SingleRowRefProxy<T>) => any;
|
|
600
616
|
/** Pre-compiled expression for filtering changes */
|
|
601
617
|
whereExpression?: BasicExpression<boolean>;
|
|
618
|
+
/**
|
|
619
|
+
* Listener for subscription status changes.
|
|
620
|
+
* Registered BEFORE any snapshot is requested, ensuring no status transitions are missed.
|
|
621
|
+
* @internal
|
|
622
|
+
*/
|
|
623
|
+
onStatusChange?: (event: SubscriptionStatusChangeEvent) => void;
|
|
602
624
|
}
|
|
603
|
-
export interface SubscribeChangesSnapshotOptions extends Omit<SubscribeChangesOptions
|
|
625
|
+
export interface SubscribeChangesSnapshotOptions<T extends object = Record<string, unknown>> extends Omit<SubscribeChangesOptions<T>, `includeInitialState`> {
|
|
604
626
|
orderBy?: OrderBy;
|
|
605
627
|
limit?: number;
|
|
606
628
|
}
|
package/dist/cjs/utils.cjs
CHANGED
|
@@ -11,10 +11,12 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
11
11
|
if (!(b instanceof Date)) return false;
|
|
12
12
|
return a.getTime() === b.getTime();
|
|
13
13
|
}
|
|
14
|
+
if (b instanceof Date) return false;
|
|
14
15
|
if (a instanceof RegExp) {
|
|
15
16
|
if (!(b instanceof RegExp)) return false;
|
|
16
17
|
return a.source === b.source && a.flags === b.flags;
|
|
17
18
|
}
|
|
19
|
+
if (b instanceof RegExp) return false;
|
|
18
20
|
if (a instanceof Map) {
|
|
19
21
|
if (!(b instanceof Map)) return false;
|
|
20
22
|
if (a.size !== b.size) return false;
|
|
@@ -29,6 +31,7 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
29
31
|
visited.delete(a);
|
|
30
32
|
return result;
|
|
31
33
|
}
|
|
34
|
+
if (b instanceof Map) return false;
|
|
32
35
|
if (a instanceof Set) {
|
|
33
36
|
if (!(b instanceof Set)) return false;
|
|
34
37
|
if (a.size !== b.size) return false;
|
|
@@ -46,6 +49,7 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
46
49
|
visited.delete(a);
|
|
47
50
|
return result;
|
|
48
51
|
}
|
|
52
|
+
if (b instanceof Set) return false;
|
|
49
53
|
if (ArrayBuffer.isView(a) && ArrayBuffer.isView(b) && !(a instanceof DataView) && !(b instanceof DataView)) {
|
|
50
54
|
const typedA = a;
|
|
51
55
|
const typedB = b;
|
|
@@ -55,6 +59,9 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
55
59
|
}
|
|
56
60
|
return true;
|
|
57
61
|
}
|
|
62
|
+
if (ArrayBuffer.isView(b) && !(b instanceof DataView) && !ArrayBuffer.isView(a)) {
|
|
63
|
+
return false;
|
|
64
|
+
}
|
|
58
65
|
if (isTemporal(a) && isTemporal(b)) {
|
|
59
66
|
const aTag = getStringTag(a);
|
|
60
67
|
const bTag = getStringTag(b);
|
|
@@ -64,6 +71,7 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
64
71
|
}
|
|
65
72
|
return a.toString() === b.toString();
|
|
66
73
|
}
|
|
74
|
+
if (isTemporal(b)) return false;
|
|
67
75
|
if (Array.isArray(a)) {
|
|
68
76
|
if (!Array.isArray(b) || a.length !== b.length) return false;
|
|
69
77
|
if (visited.has(a)) {
|
|
@@ -76,6 +84,7 @@ function deepEqualsInternal(a, b, visited) {
|
|
|
76
84
|
visited.delete(a);
|
|
77
85
|
return result;
|
|
78
86
|
}
|
|
87
|
+
if (Array.isArray(b)) return false;
|
|
79
88
|
if (typeof a === `object`) {
|
|
80
89
|
if (visited.has(a)) {
|
|
81
90
|
return visited.get(a) === b;
|
package/dist/cjs/utils.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Generic utility functions\n */\n\nimport type { CompareOptions } from './query/builder/types'\n\ninterface TypedArray {\n length: number\n [index: number]: number\n}\n\n/**\n * Deep equality function that compares two values recursively\n * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns True if the values are deeply equal, false otherwise\n *\n * @example\n * ```typescript\n * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)\n * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true\n * deepEquals({ a: 1 }, { a: 2 }) // false\n * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true\n * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true\n * ```\n */\nexport function deepEquals(a: any, b: any): boolean {\n return deepEqualsInternal(a, b, new Map())\n}\n\n/**\n * Internal implementation with cycle detection to prevent infinite recursion\n */\nfunction deepEqualsInternal(\n a: any,\n b: any,\n visited: Map<object, object>,\n): boolean {\n // Handle strict equality (primitives, same reference)\n if (a === b) return true\n\n // Handle null/undefined\n if (a == null || b == null) return false\n\n // Handle different types\n if (typeof a !== typeof b) return false\n\n // Handle Date objects\n if (a instanceof Date) {\n if (!(b instanceof Date)) return false\n return a.getTime() === b.getTime()\n }\n\n // Handle RegExp objects\n if (a instanceof RegExp) {\n if (!(b instanceof RegExp)) return false\n return a.source === b.source && a.flags === b.flags\n }\n\n // Handle Map objects - only if both are Maps\n if (a instanceof Map) {\n if (!(b instanceof Map)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const entries = Array.from(a.entries())\n const result = entries.every(([key, val]) => {\n return b.has(key) && deepEqualsInternal(val, b.get(key), visited)\n })\n\n visited.delete(a)\n return result\n }\n\n // Handle Set objects - only if both are Sets\n if (a instanceof Set) {\n if (!(b instanceof Set)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Convert to arrays for comparison\n const aValues = Array.from(a)\n const bValues = Array.from(b)\n\n // Simple comparison for primitive values\n if (aValues.every((val) => typeof val !== `object`)) {\n visited.delete(a)\n return aValues.every((val) => b.has(val))\n }\n\n // For objects in sets, we need to do a more complex comparison\n // This is a simplified approach and may not work for all cases\n const result = aValues.length === bValues.length\n visited.delete(a)\n return result\n }\n\n // Handle TypedArrays\n if (\n ArrayBuffer.isView(a) &&\n ArrayBuffer.isView(b) &&\n !(a instanceof DataView) &&\n !(b instanceof DataView)\n ) {\n const typedA = a as unknown as TypedArray\n const typedB = b as unknown as TypedArray\n if (typedA.length !== typedB.length) return false\n\n for (let i = 0; i < typedA.length; i++) {\n if (typedA[i] !== typedB[i]) return false\n }\n\n return true\n }\n\n // Handle Temporal objects\n // Check if both are Temporal objects of the same type\n if (isTemporal(a) && isTemporal(b)) {\n const aTag = getStringTag(a)\n const bTag = getStringTag(b)\n\n // If they're different Temporal types, they're not equal\n if (aTag !== bTag) return false\n\n // Use Temporal's built-in equals method if available\n if (typeof a.equals === `function`) {\n return a.equals(b)\n }\n\n // Fallback to toString comparison for other types\n return a.toString() === b.toString()\n }\n\n // Handle arrays\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const result = a.every((item, index) =>\n deepEqualsInternal(item, b[index], visited),\n )\n visited.delete(a)\n return result\n }\n\n // Handle objects\n if (typeof a === `object`) {\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Get all keys from both objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n // Check if they have the same number of keys\n if (keysA.length !== keysB.length) {\n visited.delete(a)\n return false\n }\n\n // Check if all keys exist in both objects and their values are equal\n const result = keysA.every(\n (key) => key in b && deepEqualsInternal(a[key], b[key], visited),\n )\n\n visited.delete(a)\n return result\n }\n\n // For primitives that aren't strictly equal\n return false\n}\n\nconst temporalTypes = [\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n]\n\nfunction getStringTag(a: any): any {\n return a[Symbol.toStringTag]\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: any): boolean {\n const tag = getStringTag(a)\n return typeof tag === `string` && temporalTypes.includes(tag)\n}\n\nexport const DEFAULT_COMPARE_OPTIONS: CompareOptions = {\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n}\n"],"names":[],"mappings":";;AA4BO,SAAS,WAAW,GAAQ,GAAiB;AAClD,SAAO,mBAAmB,GAAG,GAAG,oBAAI,KAAK;AAC3C;AAKA,SAAS,mBACP,GACA,GACA,SACS;AAET,MAAI,MAAM,EAAG,QAAO;AAGpB,MAAI,KAAK,QAAQ,KAAK,KAAM,QAAO;AAGnC,MAAI,OAAO,MAAM,OAAO,EAAG,QAAO;AAGlC,MAAI,aAAa,MAAM;AACrB,QAAI,EAAE,aAAa,MAAO,QAAO;AACjC,WAAO,EAAE,cAAc,EAAE,QAAA;AAAA,EAC3B;
|
|
1
|
+
{"version":3,"file":"utils.cjs","sources":["../../src/utils.ts"],"sourcesContent":["/**\n * Generic utility functions\n */\n\nimport type { CompareOptions } from './query/builder/types'\n\ninterface TypedArray {\n length: number\n [index: number]: number\n}\n\n/**\n * Deep equality function that compares two values recursively\n * Handles primitives, objects, arrays, Date, RegExp, Map, Set, TypedArrays, and Temporal objects\n *\n * @param a - First value to compare\n * @param b - Second value to compare\n * @returns True if the values are deeply equal, false otherwise\n *\n * @example\n * ```typescript\n * deepEquals({ a: 1, b: 2 }, { b: 2, a: 1 }) // true (property order doesn't matter)\n * deepEquals([1, { x: 2 }], [1, { x: 2 }]) // true\n * deepEquals({ a: 1 }, { a: 2 }) // false\n * deepEquals(new Date('2023-01-01'), new Date('2023-01-01')) // true\n * deepEquals(new Map([['a', 1]]), new Map([['a', 1]])) // true\n * ```\n */\nexport function deepEquals(a: any, b: any): boolean {\n return deepEqualsInternal(a, b, new Map())\n}\n\n/**\n * Internal implementation with cycle detection to prevent infinite recursion\n */\nfunction deepEqualsInternal(\n a: any,\n b: any,\n visited: Map<object, object>,\n): boolean {\n // Handle strict equality (primitives, same reference)\n if (a === b) return true\n\n // Handle null/undefined\n if (a == null || b == null) return false\n\n // Handle different types\n if (typeof a !== typeof b) return false\n\n // Handle Date objects\n if (a instanceof Date) {\n if (!(b instanceof Date)) return false\n return a.getTime() === b.getTime()\n }\n // Symmetric check: if b is Date but a is not, they're not equal\n if (b instanceof Date) return false\n\n // Handle RegExp objects\n if (a instanceof RegExp) {\n if (!(b instanceof RegExp)) return false\n return a.source === b.source && a.flags === b.flags\n }\n // Symmetric check: if b is RegExp but a is not, they're not equal\n if (b instanceof RegExp) return false\n\n // Handle Map objects - only if both are Maps\n if (a instanceof Map) {\n if (!(b instanceof Map)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const entries = Array.from(a.entries())\n const result = entries.every(([key, val]) => {\n return b.has(key) && deepEqualsInternal(val, b.get(key), visited)\n })\n\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is Map but a is not, they're not equal\n if (b instanceof Map) return false\n\n // Handle Set objects - only if both are Sets\n if (a instanceof Set) {\n if (!(b instanceof Set)) return false\n if (a.size !== b.size) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Convert to arrays for comparison\n const aValues = Array.from(a)\n const bValues = Array.from(b)\n\n // Simple comparison for primitive values\n if (aValues.every((val) => typeof val !== `object`)) {\n visited.delete(a)\n return aValues.every((val) => b.has(val))\n }\n\n // For objects in sets, we need to do a more complex comparison\n // This is a simplified approach and may not work for all cases\n const result = aValues.length === bValues.length\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is Set but a is not, they're not equal\n if (b instanceof Set) return false\n\n // Handle TypedArrays\n if (\n ArrayBuffer.isView(a) &&\n ArrayBuffer.isView(b) &&\n !(a instanceof DataView) &&\n !(b instanceof DataView)\n ) {\n const typedA = a as unknown as TypedArray\n const typedB = b as unknown as TypedArray\n if (typedA.length !== typedB.length) return false\n\n for (let i = 0; i < typedA.length; i++) {\n if (typedA[i] !== typedB[i]) return false\n }\n\n return true\n }\n // Symmetric check: if b is TypedArray but a is not, they're not equal\n if (\n ArrayBuffer.isView(b) &&\n !(b instanceof DataView) &&\n !ArrayBuffer.isView(a)\n ) {\n return false\n }\n\n // Handle Temporal objects\n // Check if both are Temporal objects of the same type\n if (isTemporal(a) && isTemporal(b)) {\n const aTag = getStringTag(a)\n const bTag = getStringTag(b)\n\n // If they're different Temporal types, they're not equal\n if (aTag !== bTag) return false\n\n // Use Temporal's built-in equals method if available\n if (typeof a.equals === `function`) {\n return a.equals(b)\n }\n\n // Fallback to toString comparison for other types\n return a.toString() === b.toString()\n }\n // Symmetric check: if b is Temporal but a is not, they're not equal\n if (isTemporal(b)) return false\n\n // Handle arrays\n if (Array.isArray(a)) {\n if (!Array.isArray(b) || a.length !== b.length) return false\n\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n const result = a.every((item, index) =>\n deepEqualsInternal(item, b[index], visited),\n )\n visited.delete(a)\n return result\n }\n // Symmetric check: if b is array but a is not, they're not equal\n if (Array.isArray(b)) return false\n\n // Handle objects\n if (typeof a === `object`) {\n // Check for circular references\n if (visited.has(a)) {\n return visited.get(a) === b\n }\n visited.set(a, b)\n\n // Get all keys from both objects\n const keysA = Object.keys(a)\n const keysB = Object.keys(b)\n\n // Check if they have the same number of keys\n if (keysA.length !== keysB.length) {\n visited.delete(a)\n return false\n }\n\n // Check if all keys exist in both objects and their values are equal\n const result = keysA.every(\n (key) => key in b && deepEqualsInternal(a[key], b[key], visited),\n )\n\n visited.delete(a)\n return result\n }\n\n // For primitives that aren't strictly equal\n return false\n}\n\nconst temporalTypes = [\n `Temporal.Duration`,\n `Temporal.Instant`,\n `Temporal.PlainDate`,\n `Temporal.PlainDateTime`,\n `Temporal.PlainMonthDay`,\n `Temporal.PlainTime`,\n `Temporal.PlainYearMonth`,\n `Temporal.ZonedDateTime`,\n]\n\nfunction getStringTag(a: any): any {\n return a[Symbol.toStringTag]\n}\n\n/** Checks if the value is a Temporal object by checking for the Temporal brand */\nexport function isTemporal(a: any): boolean {\n const tag = getStringTag(a)\n return typeof tag === `string` && temporalTypes.includes(tag)\n}\n\nexport const DEFAULT_COMPARE_OPTIONS: CompareOptions = {\n direction: `asc`,\n nulls: `first`,\n stringSort: `locale`,\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -36,7 +36,7 @@ export declare class CollectionChangesManager<TOutput extends object = Record<st
|
|
|
36
36
|
/**
|
|
37
37
|
* Subscribe to changes in the collection
|
|
38
38
|
*/
|
|
39
|
-
subscribeChanges(callback: (changes: Array<ChangeMessage<TOutput>>) => void, options?: SubscribeChangesOptions): CollectionSubscription;
|
|
39
|
+
subscribeChanges(callback: (changes: Array<ChangeMessage<TOutput>>) => void, options?: SubscribeChangesOptions<TOutput>): CollectionSubscription;
|
|
40
40
|
/**
|
|
41
41
|
* Increment the active subscribers count and start sync if needed
|
|
42
42
|
*/
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { NegativeActiveSubscribersError } from "../errors.js";
|
|
2
|
+
import { createSingleRowRefProxy, toExpression } from "../query/builder/ref-proxy.js";
|
|
2
3
|
import { CollectionSubscription } from "./subscription.js";
|
|
3
4
|
class CollectionChangesManager {
|
|
4
5
|
/**
|
|
@@ -53,13 +54,29 @@ class CollectionChangesManager {
|
|
|
53
54
|
*/
|
|
54
55
|
subscribeChanges(callback, options = {}) {
|
|
55
56
|
this.addSubscriber();
|
|
57
|
+
if (options.where && options.whereExpression) {
|
|
58
|
+
throw new Error(
|
|
59
|
+
`Cannot specify both 'where' and 'whereExpression' options. Use one or the other.`
|
|
60
|
+
);
|
|
61
|
+
}
|
|
62
|
+
const { where, ...opts } = options;
|
|
63
|
+
let whereExpression = opts.whereExpression;
|
|
64
|
+
if (where) {
|
|
65
|
+
const proxy = createSingleRowRefProxy();
|
|
66
|
+
const result = where(proxy);
|
|
67
|
+
whereExpression = toExpression(result);
|
|
68
|
+
}
|
|
56
69
|
const subscription = new CollectionSubscription(this.collection, callback, {
|
|
57
|
-
...
|
|
70
|
+
...opts,
|
|
71
|
+
whereExpression,
|
|
58
72
|
onUnsubscribe: () => {
|
|
59
73
|
this.removeSubscriber();
|
|
60
74
|
this.changeSubscriptions.delete(subscription);
|
|
61
75
|
}
|
|
62
76
|
});
|
|
77
|
+
if (options.onStatusChange) {
|
|
78
|
+
subscription.on(`status:change`, options.onStatusChange);
|
|
79
|
+
}
|
|
63
80
|
if (options.includeInitialState) {
|
|
64
81
|
subscription.requestSnapshot({ trackLoadSubsetPromise: false });
|
|
65
82
|
} else if (options.includeInitialState === false) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"changes.js","sources":["../../../src/collection/changes.ts"],"sourcesContent":["import { NegativeActiveSubscribersError } from '../errors'\nimport { CollectionSubscription } from './subscription.js'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { ChangeMessage, SubscribeChangesOptions } from '../types'\nimport type { CollectionLifecycleManager } from './lifecycle.js'\nimport type { CollectionSyncManager } from './sync.js'\nimport type { CollectionEventsManager } from './events.js'\nimport type { CollectionImpl } from './index.js'\n\nexport class CollectionChangesManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n\n public activeSubscribersCount = 0\n public changeSubscriptions = new Set<CollectionSubscription>()\n public batchedEvents: Array<ChangeMessage<TOutput, TKey>> = []\n public shouldBatchEvents = false\n\n /**\n * Creates a new CollectionChangesManager instance\n */\n constructor() {}\n\n public setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.sync = deps.sync\n this.events = deps.events\n this.collection = deps.collection\n }\n\n /**\n * Emit an empty ready event to notify subscribers that the collection is ready\n * This bypasses the normal empty array check in emitEvents\n */\n public emitEmptyReadyEvent(): void {\n // Emit empty array directly to all subscribers\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents([])\n }\n }\n\n /**\n * Emit events either immediately or batch them for later emission\n */\n public emitEvents(\n changes: Array<ChangeMessage<TOutput, TKey>>,\n forceEmit = false,\n ): void {\n // Skip batching for user actions (forceEmit=true) to keep UI responsive\n if (this.shouldBatchEvents && !forceEmit) {\n // Add events to the batch\n this.batchedEvents.push(...changes)\n return\n }\n\n // Either we're not batching, or we're forcing emission (user action or ending batch cycle)\n let eventsToEmit = changes\n\n if (forceEmit) {\n // Force emit is used to end a batch (e.g. after a sync commit). Combine any\n // buffered optimistic events with the final changes so subscribers see the\n // whole picture, even if the sync diff is empty.\n if (this.batchedEvents.length > 0) {\n eventsToEmit = [...this.batchedEvents, ...changes]\n }\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n\n if (eventsToEmit.length === 0) {\n return\n }\n\n // Emit to all listeners\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents(eventsToEmit)\n }\n }\n\n /**\n * Subscribe to changes in the collection\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions = {},\n ): CollectionSubscription {\n // Start sync and track subscriber\n this.addSubscriber()\n\n const subscription = new CollectionSubscription(this.collection, callback, {\n ...
|
|
1
|
+
{"version":3,"file":"changes.js","sources":["../../../src/collection/changes.ts"],"sourcesContent":["import { NegativeActiveSubscribersError } from '../errors'\nimport {\n createSingleRowRefProxy,\n toExpression,\n} from '../query/builder/ref-proxy.js'\nimport { CollectionSubscription } from './subscription.js'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { ChangeMessage, SubscribeChangesOptions } from '../types'\nimport type { CollectionLifecycleManager } from './lifecycle.js'\nimport type { CollectionSyncManager } from './sync.js'\nimport type { CollectionEventsManager } from './events.js'\nimport type { CollectionImpl } from './index.js'\n\nexport class CollectionChangesManager<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n private lifecycle!: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n private sync!: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private events!: CollectionEventsManager\n private collection!: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n\n public activeSubscribersCount = 0\n public changeSubscriptions = new Set<CollectionSubscription>()\n public batchedEvents: Array<ChangeMessage<TOutput, TKey>> = []\n public shouldBatchEvents = false\n\n /**\n * Creates a new CollectionChangesManager instance\n */\n constructor() {}\n\n public setDeps(deps: {\n lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n events: CollectionEventsManager\n collection: CollectionImpl<TOutput, TKey, any, TSchema, TInput>\n }) {\n this.lifecycle = deps.lifecycle\n this.sync = deps.sync\n this.events = deps.events\n this.collection = deps.collection\n }\n\n /**\n * Emit an empty ready event to notify subscribers that the collection is ready\n * This bypasses the normal empty array check in emitEvents\n */\n public emitEmptyReadyEvent(): void {\n // Emit empty array directly to all subscribers\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents([])\n }\n }\n\n /**\n * Emit events either immediately or batch them for later emission\n */\n public emitEvents(\n changes: Array<ChangeMessage<TOutput, TKey>>,\n forceEmit = false,\n ): void {\n // Skip batching for user actions (forceEmit=true) to keep UI responsive\n if (this.shouldBatchEvents && !forceEmit) {\n // Add events to the batch\n this.batchedEvents.push(...changes)\n return\n }\n\n // Either we're not batching, or we're forcing emission (user action or ending batch cycle)\n let eventsToEmit = changes\n\n if (forceEmit) {\n // Force emit is used to end a batch (e.g. after a sync commit). Combine any\n // buffered optimistic events with the final changes so subscribers see the\n // whole picture, even if the sync diff is empty.\n if (this.batchedEvents.length > 0) {\n eventsToEmit = [...this.batchedEvents, ...changes]\n }\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n\n if (eventsToEmit.length === 0) {\n return\n }\n\n // Emit to all listeners\n for (const subscription of this.changeSubscriptions) {\n subscription.emitEvents(eventsToEmit)\n }\n }\n\n /**\n * Subscribe to changes in the collection\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions<TOutput> = {},\n ): CollectionSubscription {\n // Start sync and track subscriber\n this.addSubscriber()\n\n // Compile where callback to whereExpression if provided\n if (options.where && options.whereExpression) {\n throw new Error(\n `Cannot specify both 'where' and 'whereExpression' options. Use one or the other.`,\n )\n }\n\n const { where, ...opts } = options\n let whereExpression = opts.whereExpression\n if (where) {\n const proxy = createSingleRowRefProxy<TOutput>()\n const result = where(proxy)\n whereExpression = toExpression(result)\n }\n\n const subscription = new CollectionSubscription(this.collection, callback, {\n ...opts,\n whereExpression,\n onUnsubscribe: () => {\n this.removeSubscriber()\n this.changeSubscriptions.delete(subscription)\n },\n })\n\n // Register status listener BEFORE requesting snapshot to avoid race condition.\n // This ensures the listener catches all status transitions, even if the\n // loadSubset promise resolves synchronously or very quickly.\n if (options.onStatusChange) {\n subscription.on(`status:change`, options.onStatusChange)\n }\n\n if (options.includeInitialState) {\n subscription.requestSnapshot({ trackLoadSubsetPromise: false })\n } else if (options.includeInitialState === false) {\n // When explicitly set to false (not just undefined), mark all state as \"seen\"\n // so that all future changes (including deletes) pass through unfiltered.\n subscription.markAllStateAsSeen()\n }\n\n // Add to batched listeners\n this.changeSubscriptions.add(subscription)\n\n return subscription\n }\n\n /**\n * Increment the active subscribers count and start sync if needed\n */\n private addSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount++\n this.lifecycle.cancelGCTimer()\n\n // Start sync if collection was cleaned up\n if (\n this.lifecycle.status === `cleaned-up` ||\n this.lifecycle.status === `idle`\n ) {\n this.sync.startSync()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount,\n )\n }\n\n /**\n * Decrement the active subscribers count and start GC timer if needed\n */\n private removeSubscriber(): void {\n const previousSubscriberCount = this.activeSubscribersCount\n this.activeSubscribersCount--\n\n if (this.activeSubscribersCount === 0) {\n this.lifecycle.startGCTimer()\n } else if (this.activeSubscribersCount < 0) {\n throw new NegativeActiveSubscribersError()\n }\n\n this.events.emitSubscribersChange(\n this.activeSubscribersCount,\n previousSubscriberCount,\n )\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public cleanup(): void {\n this.batchedEvents = []\n this.shouldBatchEvents = false\n }\n}\n"],"names":[],"mappings":"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"}
|
|
@@ -454,24 +454,27 @@ export declare class CollectionImpl<TOutput extends object = Record<string, unkn
|
|
|
454
454
|
* }, { includeInitialState: true })
|
|
455
455
|
*
|
|
456
456
|
* @example
|
|
457
|
-
* // Subscribe only to changes matching a condition
|
|
457
|
+
* // Subscribe only to changes matching a condition using where callback
|
|
458
|
+
* import { eq } from "@tanstack/db"
|
|
459
|
+
*
|
|
458
460
|
* const subscription = collection.subscribeChanges((changes) => {
|
|
459
461
|
* updateUI(changes)
|
|
460
462
|
* }, {
|
|
461
463
|
* includeInitialState: true,
|
|
462
|
-
* where: (row) => row.status
|
|
464
|
+
* where: (row) => eq(row.status, "active")
|
|
463
465
|
* })
|
|
464
466
|
*
|
|
465
467
|
* @example
|
|
466
|
-
* //
|
|
468
|
+
* // Using multiple conditions with and()
|
|
469
|
+
* import { and, eq, gt } from "@tanstack/db"
|
|
470
|
+
*
|
|
467
471
|
* const subscription = collection.subscribeChanges((changes) => {
|
|
468
472
|
* updateUI(changes)
|
|
469
473
|
* }, {
|
|
470
|
-
*
|
|
471
|
-
* whereExpression: eq(row.status, 'active')
|
|
474
|
+
* where: (row) => and(eq(row.status, "active"), gt(row.priority, 5))
|
|
472
475
|
* })
|
|
473
476
|
*/
|
|
474
|
-
subscribeChanges(callback: (changes: Array<ChangeMessage<TOutput>>) => void, options?: SubscribeChangesOptions): CollectionSubscription;
|
|
477
|
+
subscribeChanges(callback: (changes: Array<ChangeMessage<TOutput>>) => void, options?: SubscribeChangesOptions<TOutput>): CollectionSubscription;
|
|
475
478
|
/**
|
|
476
479
|
* Subscribe to a collection event
|
|
477
480
|
*/
|
|
@@ -373,21 +373,24 @@ class CollectionImpl {
|
|
|
373
373
|
* }, { includeInitialState: true })
|
|
374
374
|
*
|
|
375
375
|
* @example
|
|
376
|
-
* // Subscribe only to changes matching a condition
|
|
376
|
+
* // Subscribe only to changes matching a condition using where callback
|
|
377
|
+
* import { eq } from "@tanstack/db"
|
|
378
|
+
*
|
|
377
379
|
* const subscription = collection.subscribeChanges((changes) => {
|
|
378
380
|
* updateUI(changes)
|
|
379
381
|
* }, {
|
|
380
382
|
* includeInitialState: true,
|
|
381
|
-
* where: (row) => row.status
|
|
383
|
+
* where: (row) => eq(row.status, "active")
|
|
382
384
|
* })
|
|
383
385
|
*
|
|
384
386
|
* @example
|
|
385
|
-
* //
|
|
387
|
+
* // Using multiple conditions with and()
|
|
388
|
+
* import { and, eq, gt } from "@tanstack/db"
|
|
389
|
+
*
|
|
386
390
|
* const subscription = collection.subscribeChanges((changes) => {
|
|
387
391
|
* updateUI(changes)
|
|
388
392
|
* }, {
|
|
389
|
-
*
|
|
390
|
-
* whereExpression: eq(row.status, 'active')
|
|
393
|
+
* where: (row) => and(eq(row.status, "active"), gt(row.priority, 5))
|
|
391
394
|
* })
|
|
392
395
|
*/
|
|
393
396
|
subscribeChanges(callback, options = {}) {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sources":["../../../src/collection/index.ts"],"sourcesContent":["import {\n CollectionRequiresConfigError,\n CollectionRequiresSyncConfigError,\n} from '../errors'\nimport { currentStateAsChanges } from './change-events'\n\nimport { CollectionStateManager } from './state'\nimport { CollectionChangesManager } from './changes'\nimport { CollectionLifecycleManager } from './lifecycle.js'\nimport { CollectionSyncManager } from './sync'\nimport { CollectionIndexesManager } from './indexes'\nimport { CollectionMutationsManager } from './mutations'\nimport { CollectionEventsManager } from './events.js'\nimport type { CollectionSubscription } from './subscription'\nimport type { AllCollectionEvents, CollectionEventHandler } from './events.js'\nimport type { BaseIndex, IndexResolver } from '../indexes/base-index.js'\nimport type { IndexOptions } from '../indexes/index-options.js'\nimport type {\n ChangeMessage,\n CollectionConfig,\n CollectionStatus,\n CurrentStateAsChangesOptions,\n Fn,\n InferSchemaInput,\n InferSchemaOutput,\n InsertConfig,\n NonSingleResult,\n OperationConfig,\n SingleResult,\n StringCollationConfig,\n SubscribeChangesOptions,\n Transaction as TransactionType,\n UtilsRecord,\n WritableDeep,\n} from '../types'\nimport type { SingleRowRefProxy } from '../query/builder/ref-proxy'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { BTreeIndex } from '../indexes/btree-index.js'\nimport type { IndexProxy } from '../indexes/lazy-index.js'\n\n/**\n * Enhanced Collection interface that includes both data type T and utilities TUtils\n * @template T - The type of items in the collection\n * @template TKey - The type of the key for the collection\n * @template TUtils - The utilities record type\n * @template TInsertInput - The type for insert operations (can be different from T for schemas with defaults)\n */\nexport interface Collection<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInsertInput extends object = T,\n> extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {\n readonly utils: TUtils\n readonly singleResult?: true\n}\n\n/**\n * Creates a new Collection instance with the given configuration\n *\n * @template T - The schema type if a schema is provided, otherwise the type of items in the collection\n * @template TKey - The type of the key for the collection\n * @template TUtils - The utilities record type\n * @param options - Collection options with optional utilities\n * @returns A new Collection with utilities exposed both at top level and under .utils\n *\n * @example\n * // Pattern 1: With operation handlers (direct collection calls)\n * const todos = createCollection({\n * id: \"todos\",\n * getKey: (todo) => todo.id,\n * schema,\n * onInsert: async ({ transaction, collection }) => {\n * // Send to API\n * await api.createTodo(transaction.mutations[0].modified)\n * },\n * onUpdate: async ({ transaction, collection }) => {\n * await api.updateTodo(transaction.mutations[0].modified)\n * },\n * onDelete: async ({ transaction, collection }) => {\n * await api.deleteTodo(transaction.mutations[0].key)\n * },\n * sync: { sync: () => {} }\n * })\n *\n * // Direct usage (handlers manage transactions)\n * const tx = todos.insert({ id: \"1\", text: \"Buy milk\", completed: false })\n * await tx.isPersisted.promise\n *\n * @example\n * // Pattern 2: Manual transaction management\n * const todos = createCollection({\n * getKey: (todo) => todo.id,\n * schema: todoSchema,\n * sync: { sync: () => {} }\n * })\n *\n * // Explicit transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Handle all mutations in transaction\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * todos.insert({ id: \"1\", text: \"Buy milk\" })\n * todos.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Using schema for type inference (preferred as it also gives you client side validation)\n * const todoSchema = z.object({\n * id: z.string(),\n * title: z.string(),\n * completed: z.boolean()\n * })\n *\n * const todos = createCollection({\n * schema: todoSchema,\n * getKey: (todo) => todo.id,\n * sync: { sync: () => {} }\n * })\n *\n */\n\n// Overload for when schema is provided and utils is required (not optional)\n// We can't infer the Utils type from the CollectionConfig because it will always be optional\n// So we omit it from that type and instead infer it from the extension `& { utils: TUtils }`\n// such that we have the real, non-optional Utils type\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: Omit<\n CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils>,\n `utils`\n > & {\n schema: T\n utils: TUtils // Required utils\n } & NonSingleResult,\n): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &\n NonSingleResult\n\n// Overload for when schema is provided and utils is optional\n// In this case we can simply infer the Utils type from the CollectionConfig type\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {\n schema: T\n } & NonSingleResult,\n): Collection<\n InferSchemaOutput<T>,\n TKey,\n Exclude<TUtils, undefined>,\n T,\n InferSchemaInput<T>\n> &\n NonSingleResult\n\n// Overload for when schema is provided, singleResult is true, and utils is required\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: Omit<\n CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils>,\n `utils`\n > & {\n schema: T\n utils: TUtils // Required utils\n } & SingleResult,\n): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &\n SingleResult\n\n// Overload for when schema is provided and singleResult is true\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {\n schema: T\n } & SingleResult,\n): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &\n SingleResult\n\n// Overload for when no schema is provided and utils is required\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: Omit<CollectionConfig<T, TKey, never, TUtils>, `utils`> & {\n schema?: never // prohibit schema if an explicit type is provided\n utils: TUtils // Required utils\n } & NonSingleResult,\n): Collection<T, TKey, TUtils, never, T> & NonSingleResult\n\n// Overload for when no schema is provided\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n options: CollectionConfig<T, TKey, never, TUtils> & {\n schema?: never // prohibit schema if an explicit type is provided\n } & NonSingleResult,\n): Collection<T, TKey, TUtils, never, T> & NonSingleResult\n\n// Overload for when no schema is provided, singleResult is true, and utils is required\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n options: Omit<CollectionConfig<T, TKey, never, TUtils>, `utils`> & {\n schema?: never // prohibit schema if an explicit type is provided\n utils: TUtils // Required utils\n } & SingleResult,\n): Collection<T, TKey, TUtils, never, T> & SingleResult\n\n// Overload for when no schema is provided and singleResult is true\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n options: CollectionConfig<T, TKey, never, TUtils> & {\n schema?: never // prohibit schema if an explicit type is provided\n } & SingleResult,\n): Collection<T, TKey, TUtils, never, T> & SingleResult\n\n// Implementation\nexport function createCollection(\n options: CollectionConfig<any, string | number, any, UtilsRecord> & {\n schema?: StandardSchemaV1\n },\n): Collection<any, string | number, UtilsRecord, any, any> {\n const collection = new CollectionImpl<any, string | number, any, any, any>(\n options,\n )\n\n // Attach utils to collection\n if (options.utils) {\n collection.utils = options.utils\n } else {\n collection.utils = {}\n }\n\n return collection\n}\n\nexport class CollectionImpl<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = {},\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n public id: string\n public config: CollectionConfig<TOutput, TKey, TSchema>\n\n // Utilities namespace\n // This is populated by createCollection\n public utils: Record<string, Fn> = {}\n\n // Managers\n private _events: CollectionEventsManager\n private _changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n public _lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n public _sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private _indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private _mutations: CollectionMutationsManager<\n TOutput,\n TKey,\n TUtils,\n TSchema,\n TInput\n >\n // The core state of the collection is \"public\" so that is accessible in tests\n // and for debugging\n public _state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n private comparisonOpts: StringCollationConfig\n\n /**\n * Creates a new Collection instance\n *\n * @param config - Configuration object for the collection\n * @throws Error if sync config is missing\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {\n // eslint-disable-next-line\n if (!config) {\n throw new CollectionRequiresConfigError()\n }\n\n // eslint-disable-next-line\n if (!config.sync) {\n throw new CollectionRequiresSyncConfigError()\n }\n\n if (config.id) {\n this.id = config.id\n } else {\n this.id = crypto.randomUUID()\n }\n\n // Set default values for optional config properties\n this.config = {\n ...config,\n autoIndex: config.autoIndex ?? `eager`,\n }\n\n this._changes = new CollectionChangesManager()\n this._events = new CollectionEventsManager()\n this._indexes = new CollectionIndexesManager()\n this._lifecycle = new CollectionLifecycleManager(config, this.id)\n this._mutations = new CollectionMutationsManager(config, this.id)\n this._state = new CollectionStateManager(config)\n this._sync = new CollectionSyncManager(config, this.id)\n\n this.comparisonOpts = buildCompareOptionsFromConfig(config)\n\n this._changes.setDeps({\n collection: this, // Required for passing to CollectionSubscription\n lifecycle: this._lifecycle,\n sync: this._sync,\n events: this._events,\n })\n this._events.setDeps({\n collection: this, // Required for adding to emitted events\n })\n this._indexes.setDeps({\n state: this._state,\n lifecycle: this._lifecycle,\n })\n this._lifecycle.setDeps({\n changes: this._changes,\n events: this._events,\n indexes: this._indexes,\n state: this._state,\n sync: this._sync,\n })\n this._mutations.setDeps({\n collection: this, // Required for passing to config.onInsert/onUpdate/onDelete and annotating mutations\n lifecycle: this._lifecycle,\n state: this._state,\n })\n this._state.setDeps({\n collection: this, // Required for filtering events to only include this collection\n lifecycle: this._lifecycle,\n changes: this._changes,\n indexes: this._indexes,\n events: this._events,\n })\n this._sync.setDeps({\n collection: this, // Required for passing to config.sync callback\n state: this._state,\n lifecycle: this._lifecycle,\n events: this._events,\n })\n\n // Only start sync immediately if explicitly enabled\n if (config.startSync === true) {\n this._sync.startSync()\n }\n }\n\n /**\n * Gets the current status of the collection\n */\n public get status(): CollectionStatus {\n return this._lifecycle.status\n }\n\n /**\n * Get the number of subscribers to the collection\n */\n public get subscriberCount(): number {\n return this._changes.activeSubscribersCount\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n * @example\n * collection.onFirstReady(() => {\n * console.log('Collection is ready for the first time')\n * // Safe to access collection.state now\n * })\n */\n public onFirstReady(callback: () => void): void {\n return this._lifecycle.onFirstReady(callback)\n }\n\n /**\n * Check if the collection is ready for use\n * Returns true if the collection has been marked as ready by its sync implementation\n * @returns true if the collection is ready, false otherwise\n * @example\n * if (collection.isReady()) {\n * console.log('Collection is ready, data is available')\n * // Safe to access collection.state\n * } else {\n * console.log('Collection is still loading')\n * }\n */\n public isReady(): boolean {\n return this._lifecycle.status === `ready`\n }\n\n /**\n * Check if the collection is currently loading more data\n * @returns true if the collection has pending load more operations, false otherwise\n */\n public get isLoadingSubset(): boolean {\n return this._sync.isLoadingSubset\n }\n\n /**\n * Start sync immediately - internal method for compiled queries\n * This bypasses lazy loading for special cases like live query results\n */\n public startSyncImmediate(): void {\n this._sync.startSync()\n }\n\n /**\n * Preload the collection data by starting sync if not already started\n * Multiple concurrent calls will share the same promise\n */\n public preload(): Promise<void> {\n return this._sync.preload()\n }\n\n /**\n * Get the current value for a key (virtual derived state)\n */\n public get(key: TKey): TOutput | undefined {\n return this._state.get(key)\n }\n\n /**\n * Check if a key exists in the collection (virtual derived state)\n */\n public has(key: TKey): boolean {\n return this._state.has(key)\n }\n\n /**\n * Get the current size of the collection (cached)\n */\n public get size(): number {\n return this._state.size\n }\n\n /**\n * Get all keys (virtual derived state)\n */\n public *keys(): IterableIterator<TKey> {\n yield* this._state.keys()\n }\n\n /**\n * Get all values (virtual derived state)\n */\n public *values(): IterableIterator<TOutput> {\n yield* this._state.values()\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *entries(): IterableIterator<[TKey, TOutput]> {\n yield* this._state.entries()\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *[Symbol.iterator](): IterableIterator<[TKey, TOutput]> {\n yield* this._state[Symbol.iterator]()\n }\n\n /**\n * Execute a callback for each entry in the collection\n */\n public forEach(\n callbackfn: (value: TOutput, key: TKey, index: number) => void,\n ): void {\n return this._state.forEach(callbackfn)\n }\n\n /**\n * Create a new array with the results of calling a function for each entry in the collection\n */\n public map<U>(\n callbackfn: (value: TOutput, key: TKey, index: number) => U,\n ): Array<U> {\n return this._state.map(callbackfn)\n }\n\n public getKeyFromItem(item: TOutput): TKey {\n return this.config.getKey(item)\n }\n\n /**\n * Creates an index on a collection for faster queries.\n * Indexes significantly improve query performance by allowing constant time lookups\n * and logarithmic time range queries instead of full scans.\n *\n * @template TResolver - The type of the index resolver (constructor or async loader)\n * @param indexCallback - Function that extracts the indexed value from each item\n * @param config - Configuration including index type and type-specific options\n * @returns An index proxy that provides access to the index when ready\n *\n * @example\n * // Create a default B+ tree index\n * const ageIndex = collection.createIndex((row) => row.age)\n *\n * // Create a ordered index with custom options\n * const ageIndex = collection.createIndex((row) => row.age, {\n * indexType: BTreeIndex,\n * options: {\n * compareFn: customComparator,\n * compareOptions: { direction: 'asc', nulls: 'first', stringSort: 'lexical' }\n * },\n * name: 'age_btree'\n * })\n *\n * // Create an async-loaded index\n * const textIndex = collection.createIndex((row) => row.content, {\n * indexType: async () => {\n * const { FullTextIndex } = await import('./indexes/fulltext.js')\n * return FullTextIndex\n * },\n * options: { language: 'en' }\n * })\n */\n public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(\n indexCallback: (row: SingleRowRefProxy<TOutput>) => any,\n config: IndexOptions<TResolver> = {},\n ): IndexProxy<TKey> {\n return this._indexes.createIndex(indexCallback, config)\n }\n\n /**\n * Get resolved indexes for query optimization\n */\n get indexes(): Map<number, BaseIndex<TKey>> {\n return this._indexes.indexes\n }\n\n /**\n * Validates the data against the schema\n */\n public validateData(\n data: unknown,\n type: `insert` | `update`,\n key?: TKey,\n ): TOutput | never {\n return this._mutations.validateData(data, type, key)\n }\n\n get compareOptions(): StringCollationConfig {\n // return a copy such that no one can mutate the internal comparison object\n return { ...this.comparisonOpts }\n }\n\n /**\n * Inserts one or more items into the collection\n * @param items - Single item or array of items to insert\n * @param config - Optional configuration including metadata\n * @returns A Transaction object representing the insert operation(s)\n * @throws {SchemaValidationError} If the data fails schema validation\n * @example\n * // Insert a single todo (requires onInsert handler)\n * const tx = collection.insert({ id: \"1\", text: \"Buy milk\", completed: false })\n * await tx.isPersisted.promise\n *\n * @example\n * // Insert multiple todos at once\n * const tx = collection.insert([\n * { id: \"1\", text: \"Buy milk\", completed: false },\n * { id: \"2\", text: \"Walk dog\", completed: true }\n * ])\n * await tx.isPersisted.promise\n *\n * @example\n * // Insert with metadata\n * const tx = collection.insert({ id: \"1\", text: \"Buy groceries\" },\n * { metadata: { source: \"mobile-app\" } }\n * )\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle errors\n * try {\n * const tx = collection.insert({ id: \"1\", text: \"New item\" })\n * await tx.isPersisted.promise\n * console.log('Insert successful')\n * } catch (error) {\n * console.log('Insert failed:', error)\n * }\n */\n insert = (data: TInput | Array<TInput>, config?: InsertConfig) => {\n return this._mutations.insert(data, config)\n }\n\n /**\n * Updates one or more items in the collection using a callback function\n * @param keys - Single key or array of keys to update\n * @param configOrCallback - Either update configuration or update callback\n * @param maybeCallback - Update callback if config was provided\n * @returns A Transaction object representing the update operation(s)\n * @throws {SchemaValidationError} If the updated data fails schema validation\n * @example\n * // Update single item by key\n * const tx = collection.update(\"todo-1\", (draft) => {\n * draft.completed = true\n * })\n * await tx.isPersisted.promise\n *\n * @example\n * // Update multiple items\n * const tx = collection.update([\"todo-1\", \"todo-2\"], (drafts) => {\n * drafts.forEach(draft => { draft.completed = true })\n * })\n * await tx.isPersisted.promise\n *\n * @example\n * // Update with metadata\n * const tx = collection.update(\"todo-1\",\n * { metadata: { reason: \"user update\" } },\n * (draft) => { draft.text = \"Updated text\" }\n * )\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle errors\n * try {\n * const tx = collection.update(\"item-1\", draft => { draft.value = \"new\" })\n * await tx.isPersisted.promise\n * console.log('Update successful')\n * } catch (error) {\n * console.log('Update failed:', error)\n * }\n */\n\n // Overload 1: Update multiple items with a callback\n update(\n key: Array<TKey | unknown>,\n callback: (drafts: Array<WritableDeep<TInput>>) => void,\n ): TransactionType\n\n // Overload 2: Update multiple items with config and a callback\n update(\n keys: Array<TKey | unknown>,\n config: OperationConfig,\n callback: (drafts: Array<WritableDeep<TInput>>) => void,\n ): TransactionType\n\n // Overload 3: Update a single item with a callback\n update(\n id: TKey | unknown,\n callback: (draft: WritableDeep<TInput>) => void,\n ): TransactionType\n\n // Overload 4: Update a single item with config and a callback\n update(\n id: TKey | unknown,\n config: OperationConfig,\n callback: (draft: WritableDeep<TInput>) => void,\n ): TransactionType\n\n update(\n keys: (TKey | unknown) | Array<TKey | unknown>,\n configOrCallback:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void)\n | OperationConfig,\n maybeCallback?:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void),\n ) {\n return this._mutations.update(keys, configOrCallback, maybeCallback)\n }\n\n /**\n * Deletes one or more items from the collection\n * @param keys - Single key or array of keys to delete\n * @param config - Optional configuration including metadata\n * @returns A Transaction object representing the delete operation(s)\n * @example\n * // Delete a single item\n * const tx = collection.delete(\"todo-1\")\n * await tx.isPersisted.promise\n *\n * @example\n * // Delete multiple items\n * const tx = collection.delete([\"todo-1\", \"todo-2\"])\n * await tx.isPersisted.promise\n *\n * @example\n * // Delete with metadata\n * const tx = collection.delete(\"todo-1\", { metadata: { reason: \"completed\" } })\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle errors\n * try {\n * const tx = collection.delete(\"item-1\")\n * await tx.isPersisted.promise\n * console.log('Delete successful')\n * } catch (error) {\n * console.log('Delete failed:', error)\n * }\n */\n delete = (\n keys: Array<TKey> | TKey,\n config?: OperationConfig,\n ): TransactionType<any> => {\n return this._mutations.delete(keys, config)\n }\n\n /**\n * Gets the current state of the collection as a Map\n * @returns Map containing all items in the collection, with keys as identifiers\n * @example\n * const itemsMap = collection.state\n * console.log(`Collection has ${itemsMap.size} items`)\n *\n * for (const [key, item] of itemsMap) {\n * console.log(`${key}: ${item.title}`)\n * }\n *\n * // Check if specific item exists\n * if (itemsMap.has(\"todo-1\")) {\n * console.log(\"Todo 1 exists:\", itemsMap.get(\"todo-1\"))\n * }\n */\n get state() {\n const result = new Map<TKey, TOutput>()\n for (const [key, value] of this.entries()) {\n result.set(key, value)\n }\n return result\n }\n\n /**\n * Gets the current state of the collection as a Map, but only resolves when data is available\n * Waits for the first sync commit to complete before resolving\n *\n * @returns Promise that resolves to a Map containing all items in the collection\n */\n stateWhenReady(): Promise<Map<TKey, TOutput>> {\n // If we already have data or collection is ready, resolve immediately\n if (this.size > 0 || this.isReady()) {\n return Promise.resolve(this.state)\n }\n\n // Use preload to ensure the collection starts loading, then return the state\n return this.preload().then(() => this.state)\n }\n\n /**\n * Gets the current state of the collection as an Array\n *\n * @returns An Array containing all items in the collection\n */\n get toArray() {\n return Array.from(this.values())\n }\n\n /**\n * Gets the current state of the collection as an Array, but only resolves when data is available\n * Waits for the first sync commit to complete before resolving\n *\n * @returns Promise that resolves to an Array containing all items in the collection\n */\n toArrayWhenReady(): Promise<Array<TOutput>> {\n // If we already have data or collection is ready, resolve immediately\n if (this.size > 0 || this.isReady()) {\n return Promise.resolve(this.toArray)\n }\n\n // Use preload to ensure the collection starts loading, then return the array\n return this.preload().then(() => this.toArray)\n }\n\n /**\n * Returns the current state of the collection as an array of changes\n * @param options - Options including optional where filter\n * @returns An array of changes\n * @example\n * // Get all items as changes\n * const allChanges = collection.currentStateAsChanges()\n *\n * // Get only items matching a condition\n * const activeChanges = collection.currentStateAsChanges({\n * where: (row) => row.status === 'active'\n * })\n *\n * // Get only items using a pre-compiled expression\n * const activeChanges = collection.currentStateAsChanges({\n * whereExpression: eq(row.status, 'active')\n * })\n */\n public currentStateAsChanges(\n options: CurrentStateAsChangesOptions = {},\n ): Array<ChangeMessage<TOutput>> | void {\n return currentStateAsChanges(this, options)\n }\n\n /**\n * Subscribe to changes in the collection\n * @param callback - Function called when items change\n * @param options - Subscription options including includeInitialState and where filter\n * @returns Unsubscribe function - Call this to stop listening for changes\n * @example\n * // Basic subscription\n * const subscription = collection.subscribeChanges((changes) => {\n * changes.forEach(change => {\n * console.log(`${change.type}: ${change.key}`, change.value)\n * })\n * })\n *\n * // Later: subscription.unsubscribe()\n *\n * @example\n * // Include current state immediately\n * const subscription = collection.subscribeChanges((changes) => {\n * updateUI(changes)\n * }, { includeInitialState: true })\n *\n * @example\n * // Subscribe only to changes matching a condition\n * const subscription = collection.subscribeChanges((changes) => {\n * updateUI(changes)\n * }, {\n * includeInitialState: true,\n * where: (row) => row.status === 'active'\n * })\n *\n * @example\n * // Subscribe using a pre-compiled expression\n * const subscription = collection.subscribeChanges((changes) => {\n * updateUI(changes)\n * }, {\n * includeInitialState: true,\n * whereExpression: eq(row.status, 'active')\n * })\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions = {},\n ): CollectionSubscription {\n return this._changes.subscribeChanges(callback, options)\n }\n\n /**\n * Subscribe to a collection event\n */\n public on<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>,\n ) {\n return this._events.on(event, callback)\n }\n\n /**\n * Subscribe to a collection event once\n */\n public once<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>,\n ) {\n return this._events.once(event, callback)\n }\n\n /**\n * Unsubscribe from a collection event\n */\n public off<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>,\n ) {\n this._events.off(event, callback)\n }\n\n /**\n * Wait for a collection event\n */\n public waitFor<T extends keyof AllCollectionEvents>(\n event: T,\n timeout?: number,\n ) {\n return this._events.waitFor(event, timeout)\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public async cleanup(): Promise<void> {\n this._lifecycle.cleanup()\n return Promise.resolve()\n }\n}\n\nfunction buildCompareOptionsFromConfig(\n config: CollectionConfig<any, any, any>,\n): StringCollationConfig {\n if (config.defaultStringCollation) {\n const options = config.defaultStringCollation\n return {\n stringSort: options.stringSort ?? `locale`,\n locale: options.stringSort === `locale` ? options.locale : undefined,\n localeOptions:\n options.stringSort === `locale` ? options.localeOptions : undefined,\n }\n } else {\n return {\n stringSort: `locale`,\n }\n }\n}\n"],"names":["config"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../../src/collection/index.ts"],"sourcesContent":["import {\n CollectionRequiresConfigError,\n CollectionRequiresSyncConfigError,\n} from '../errors'\nimport { currentStateAsChanges } from './change-events'\n\nimport { CollectionStateManager } from './state'\nimport { CollectionChangesManager } from './changes'\nimport { CollectionLifecycleManager } from './lifecycle.js'\nimport { CollectionSyncManager } from './sync'\nimport { CollectionIndexesManager } from './indexes'\nimport { CollectionMutationsManager } from './mutations'\nimport { CollectionEventsManager } from './events.js'\nimport type { CollectionSubscription } from './subscription'\nimport type { AllCollectionEvents, CollectionEventHandler } from './events.js'\nimport type { BaseIndex, IndexResolver } from '../indexes/base-index.js'\nimport type { IndexOptions } from '../indexes/index-options.js'\nimport type {\n ChangeMessage,\n CollectionConfig,\n CollectionStatus,\n CurrentStateAsChangesOptions,\n Fn,\n InferSchemaInput,\n InferSchemaOutput,\n InsertConfig,\n NonSingleResult,\n OperationConfig,\n SingleResult,\n StringCollationConfig,\n SubscribeChangesOptions,\n Transaction as TransactionType,\n UtilsRecord,\n WritableDeep,\n} from '../types'\nimport type { SingleRowRefProxy } from '../query/builder/ref-proxy'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { BTreeIndex } from '../indexes/btree-index.js'\nimport type { IndexProxy } from '../indexes/lazy-index.js'\n\n/**\n * Enhanced Collection interface that includes both data type T and utilities TUtils\n * @template T - The type of items in the collection\n * @template TKey - The type of the key for the collection\n * @template TUtils - The utilities record type\n * @template TInsertInput - The type for insert operations (can be different from T for schemas with defaults)\n */\nexport interface Collection<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInsertInput extends object = T,\n> extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {\n readonly utils: TUtils\n readonly singleResult?: true\n}\n\n/**\n * Creates a new Collection instance with the given configuration\n *\n * @template T - The schema type if a schema is provided, otherwise the type of items in the collection\n * @template TKey - The type of the key for the collection\n * @template TUtils - The utilities record type\n * @param options - Collection options with optional utilities\n * @returns A new Collection with utilities exposed both at top level and under .utils\n *\n * @example\n * // Pattern 1: With operation handlers (direct collection calls)\n * const todos = createCollection({\n * id: \"todos\",\n * getKey: (todo) => todo.id,\n * schema,\n * onInsert: async ({ transaction, collection }) => {\n * // Send to API\n * await api.createTodo(transaction.mutations[0].modified)\n * },\n * onUpdate: async ({ transaction, collection }) => {\n * await api.updateTodo(transaction.mutations[0].modified)\n * },\n * onDelete: async ({ transaction, collection }) => {\n * await api.deleteTodo(transaction.mutations[0].key)\n * },\n * sync: { sync: () => {} }\n * })\n *\n * // Direct usage (handlers manage transactions)\n * const tx = todos.insert({ id: \"1\", text: \"Buy milk\", completed: false })\n * await tx.isPersisted.promise\n *\n * @example\n * // Pattern 2: Manual transaction management\n * const todos = createCollection({\n * getKey: (todo) => todo.id,\n * schema: todoSchema,\n * sync: { sync: () => {} }\n * })\n *\n * // Explicit transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Handle all mutations in transaction\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * todos.insert({ id: \"1\", text: \"Buy milk\" })\n * todos.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Using schema for type inference (preferred as it also gives you client side validation)\n * const todoSchema = z.object({\n * id: z.string(),\n * title: z.string(),\n * completed: z.boolean()\n * })\n *\n * const todos = createCollection({\n * schema: todoSchema,\n * getKey: (todo) => todo.id,\n * sync: { sync: () => {} }\n * })\n *\n */\n\n// Overload for when schema is provided and utils is required (not optional)\n// We can't infer the Utils type from the CollectionConfig because it will always be optional\n// So we omit it from that type and instead infer it from the extension `& { utils: TUtils }`\n// such that we have the real, non-optional Utils type\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: Omit<\n CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils>,\n `utils`\n > & {\n schema: T\n utils: TUtils // Required utils\n } & NonSingleResult,\n): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &\n NonSingleResult\n\n// Overload for when schema is provided and utils is optional\n// In this case we can simply infer the Utils type from the CollectionConfig type\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {\n schema: T\n } & NonSingleResult,\n): Collection<\n InferSchemaOutput<T>,\n TKey,\n Exclude<TUtils, undefined>,\n T,\n InferSchemaInput<T>\n> &\n NonSingleResult\n\n// Overload for when schema is provided, singleResult is true, and utils is required\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: Omit<\n CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils>,\n `utils`\n > & {\n schema: T\n utils: TUtils // Required utils\n } & SingleResult,\n): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &\n SingleResult\n\n// Overload for when schema is provided and singleResult is true\nexport function createCollection<\n T extends StandardSchemaV1,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: CollectionConfig<InferSchemaOutput<T>, TKey, T, TUtils> & {\n schema: T\n } & SingleResult,\n): Collection<InferSchemaOutput<T>, TKey, TUtils, T, InferSchemaInput<T>> &\n SingleResult\n\n// Overload for when no schema is provided and utils is required\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number,\n TUtils extends UtilsRecord,\n>(\n options: Omit<CollectionConfig<T, TKey, never, TUtils>, `utils`> & {\n schema?: never // prohibit schema if an explicit type is provided\n utils: TUtils // Required utils\n } & NonSingleResult,\n): Collection<T, TKey, TUtils, never, T> & NonSingleResult\n\n// Overload for when no schema is provided\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n options: CollectionConfig<T, TKey, never, TUtils> & {\n schema?: never // prohibit schema if an explicit type is provided\n } & NonSingleResult,\n): Collection<T, TKey, TUtils, never, T> & NonSingleResult\n\n// Overload for when no schema is provided, singleResult is true, and utils is required\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n options: Omit<CollectionConfig<T, TKey, never, TUtils>, `utils`> & {\n schema?: never // prohibit schema if an explicit type is provided\n utils: TUtils // Required utils\n } & SingleResult,\n): Collection<T, TKey, TUtils, never, T> & SingleResult\n\n// Overload for when no schema is provided and singleResult is true\n// the type T needs to be passed explicitly unless it can be inferred from the getKey function in the config\nexport function createCollection<\n T extends object,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n options: CollectionConfig<T, TKey, never, TUtils> & {\n schema?: never // prohibit schema if an explicit type is provided\n } & SingleResult,\n): Collection<T, TKey, TUtils, never, T> & SingleResult\n\n// Implementation\nexport function createCollection(\n options: CollectionConfig<any, string | number, any, UtilsRecord> & {\n schema?: StandardSchemaV1\n },\n): Collection<any, string | number, UtilsRecord, any, any> {\n const collection = new CollectionImpl<any, string | number, any, any, any>(\n options,\n )\n\n // Attach utils to collection\n if (options.utils) {\n collection.utils = options.utils\n } else {\n collection.utils = {}\n }\n\n return collection\n}\n\nexport class CollectionImpl<\n TOutput extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = {},\n TSchema extends StandardSchemaV1 = StandardSchemaV1,\n TInput extends object = TOutput,\n> {\n public id: string\n public config: CollectionConfig<TOutput, TKey, TSchema>\n\n // Utilities namespace\n // This is populated by createCollection\n public utils: Record<string, Fn> = {}\n\n // Managers\n private _events: CollectionEventsManager\n private _changes: CollectionChangesManager<TOutput, TKey, TSchema, TInput>\n public _lifecycle: CollectionLifecycleManager<TOutput, TKey, TSchema, TInput>\n public _sync: CollectionSyncManager<TOutput, TKey, TSchema, TInput>\n private _indexes: CollectionIndexesManager<TOutput, TKey, TSchema, TInput>\n private _mutations: CollectionMutationsManager<\n TOutput,\n TKey,\n TUtils,\n TSchema,\n TInput\n >\n // The core state of the collection is \"public\" so that is accessible in tests\n // and for debugging\n public _state: CollectionStateManager<TOutput, TKey, TSchema, TInput>\n\n private comparisonOpts: StringCollationConfig\n\n /**\n * Creates a new Collection instance\n *\n * @param config - Configuration object for the collection\n * @throws Error if sync config is missing\n */\n constructor(config: CollectionConfig<TOutput, TKey, TSchema>) {\n // eslint-disable-next-line\n if (!config) {\n throw new CollectionRequiresConfigError()\n }\n\n // eslint-disable-next-line\n if (!config.sync) {\n throw new CollectionRequiresSyncConfigError()\n }\n\n if (config.id) {\n this.id = config.id\n } else {\n this.id = crypto.randomUUID()\n }\n\n // Set default values for optional config properties\n this.config = {\n ...config,\n autoIndex: config.autoIndex ?? `eager`,\n }\n\n this._changes = new CollectionChangesManager()\n this._events = new CollectionEventsManager()\n this._indexes = new CollectionIndexesManager()\n this._lifecycle = new CollectionLifecycleManager(config, this.id)\n this._mutations = new CollectionMutationsManager(config, this.id)\n this._state = new CollectionStateManager(config)\n this._sync = new CollectionSyncManager(config, this.id)\n\n this.comparisonOpts = buildCompareOptionsFromConfig(config)\n\n this._changes.setDeps({\n collection: this, // Required for passing to CollectionSubscription\n lifecycle: this._lifecycle,\n sync: this._sync,\n events: this._events,\n })\n this._events.setDeps({\n collection: this, // Required for adding to emitted events\n })\n this._indexes.setDeps({\n state: this._state,\n lifecycle: this._lifecycle,\n })\n this._lifecycle.setDeps({\n changes: this._changes,\n events: this._events,\n indexes: this._indexes,\n state: this._state,\n sync: this._sync,\n })\n this._mutations.setDeps({\n collection: this, // Required for passing to config.onInsert/onUpdate/onDelete and annotating mutations\n lifecycle: this._lifecycle,\n state: this._state,\n })\n this._state.setDeps({\n collection: this, // Required for filtering events to only include this collection\n lifecycle: this._lifecycle,\n changes: this._changes,\n indexes: this._indexes,\n events: this._events,\n })\n this._sync.setDeps({\n collection: this, // Required for passing to config.sync callback\n state: this._state,\n lifecycle: this._lifecycle,\n events: this._events,\n })\n\n // Only start sync immediately if explicitly enabled\n if (config.startSync === true) {\n this._sync.startSync()\n }\n }\n\n /**\n * Gets the current status of the collection\n */\n public get status(): CollectionStatus {\n return this._lifecycle.status\n }\n\n /**\n * Get the number of subscribers to the collection\n */\n public get subscriberCount(): number {\n return this._changes.activeSubscribersCount\n }\n\n /**\n * Register a callback to be executed when the collection first becomes ready\n * Useful for preloading collections\n * @param callback Function to call when the collection first becomes ready\n * @example\n * collection.onFirstReady(() => {\n * console.log('Collection is ready for the first time')\n * // Safe to access collection.state now\n * })\n */\n public onFirstReady(callback: () => void): void {\n return this._lifecycle.onFirstReady(callback)\n }\n\n /**\n * Check if the collection is ready for use\n * Returns true if the collection has been marked as ready by its sync implementation\n * @returns true if the collection is ready, false otherwise\n * @example\n * if (collection.isReady()) {\n * console.log('Collection is ready, data is available')\n * // Safe to access collection.state\n * } else {\n * console.log('Collection is still loading')\n * }\n */\n public isReady(): boolean {\n return this._lifecycle.status === `ready`\n }\n\n /**\n * Check if the collection is currently loading more data\n * @returns true if the collection has pending load more operations, false otherwise\n */\n public get isLoadingSubset(): boolean {\n return this._sync.isLoadingSubset\n }\n\n /**\n * Start sync immediately - internal method for compiled queries\n * This bypasses lazy loading for special cases like live query results\n */\n public startSyncImmediate(): void {\n this._sync.startSync()\n }\n\n /**\n * Preload the collection data by starting sync if not already started\n * Multiple concurrent calls will share the same promise\n */\n public preload(): Promise<void> {\n return this._sync.preload()\n }\n\n /**\n * Get the current value for a key (virtual derived state)\n */\n public get(key: TKey): TOutput | undefined {\n return this._state.get(key)\n }\n\n /**\n * Check if a key exists in the collection (virtual derived state)\n */\n public has(key: TKey): boolean {\n return this._state.has(key)\n }\n\n /**\n * Get the current size of the collection (cached)\n */\n public get size(): number {\n return this._state.size\n }\n\n /**\n * Get all keys (virtual derived state)\n */\n public *keys(): IterableIterator<TKey> {\n yield* this._state.keys()\n }\n\n /**\n * Get all values (virtual derived state)\n */\n public *values(): IterableIterator<TOutput> {\n yield* this._state.values()\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *entries(): IterableIterator<[TKey, TOutput]> {\n yield* this._state.entries()\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *[Symbol.iterator](): IterableIterator<[TKey, TOutput]> {\n yield* this._state[Symbol.iterator]()\n }\n\n /**\n * Execute a callback for each entry in the collection\n */\n public forEach(\n callbackfn: (value: TOutput, key: TKey, index: number) => void,\n ): void {\n return this._state.forEach(callbackfn)\n }\n\n /**\n * Create a new array with the results of calling a function for each entry in the collection\n */\n public map<U>(\n callbackfn: (value: TOutput, key: TKey, index: number) => U,\n ): Array<U> {\n return this._state.map(callbackfn)\n }\n\n public getKeyFromItem(item: TOutput): TKey {\n return this.config.getKey(item)\n }\n\n /**\n * Creates an index on a collection for faster queries.\n * Indexes significantly improve query performance by allowing constant time lookups\n * and logarithmic time range queries instead of full scans.\n *\n * @template TResolver - The type of the index resolver (constructor or async loader)\n * @param indexCallback - Function that extracts the indexed value from each item\n * @param config - Configuration including index type and type-specific options\n * @returns An index proxy that provides access to the index when ready\n *\n * @example\n * // Create a default B+ tree index\n * const ageIndex = collection.createIndex((row) => row.age)\n *\n * // Create a ordered index with custom options\n * const ageIndex = collection.createIndex((row) => row.age, {\n * indexType: BTreeIndex,\n * options: {\n * compareFn: customComparator,\n * compareOptions: { direction: 'asc', nulls: 'first', stringSort: 'lexical' }\n * },\n * name: 'age_btree'\n * })\n *\n * // Create an async-loaded index\n * const textIndex = collection.createIndex((row) => row.content, {\n * indexType: async () => {\n * const { FullTextIndex } = await import('./indexes/fulltext.js')\n * return FullTextIndex\n * },\n * options: { language: 'en' }\n * })\n */\n public createIndex<TResolver extends IndexResolver<TKey> = typeof BTreeIndex>(\n indexCallback: (row: SingleRowRefProxy<TOutput>) => any,\n config: IndexOptions<TResolver> = {},\n ): IndexProxy<TKey> {\n return this._indexes.createIndex(indexCallback, config)\n }\n\n /**\n * Get resolved indexes for query optimization\n */\n get indexes(): Map<number, BaseIndex<TKey>> {\n return this._indexes.indexes\n }\n\n /**\n * Validates the data against the schema\n */\n public validateData(\n data: unknown,\n type: `insert` | `update`,\n key?: TKey,\n ): TOutput | never {\n return this._mutations.validateData(data, type, key)\n }\n\n get compareOptions(): StringCollationConfig {\n // return a copy such that no one can mutate the internal comparison object\n return { ...this.comparisonOpts }\n }\n\n /**\n * Inserts one or more items into the collection\n * @param items - Single item or array of items to insert\n * @param config - Optional configuration including metadata\n * @returns A Transaction object representing the insert operation(s)\n * @throws {SchemaValidationError} If the data fails schema validation\n * @example\n * // Insert a single todo (requires onInsert handler)\n * const tx = collection.insert({ id: \"1\", text: \"Buy milk\", completed: false })\n * await tx.isPersisted.promise\n *\n * @example\n * // Insert multiple todos at once\n * const tx = collection.insert([\n * { id: \"1\", text: \"Buy milk\", completed: false },\n * { id: \"2\", text: \"Walk dog\", completed: true }\n * ])\n * await tx.isPersisted.promise\n *\n * @example\n * // Insert with metadata\n * const tx = collection.insert({ id: \"1\", text: \"Buy groceries\" },\n * { metadata: { source: \"mobile-app\" } }\n * )\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle errors\n * try {\n * const tx = collection.insert({ id: \"1\", text: \"New item\" })\n * await tx.isPersisted.promise\n * console.log('Insert successful')\n * } catch (error) {\n * console.log('Insert failed:', error)\n * }\n */\n insert = (data: TInput | Array<TInput>, config?: InsertConfig) => {\n return this._mutations.insert(data, config)\n }\n\n /**\n * Updates one or more items in the collection using a callback function\n * @param keys - Single key or array of keys to update\n * @param configOrCallback - Either update configuration or update callback\n * @param maybeCallback - Update callback if config was provided\n * @returns A Transaction object representing the update operation(s)\n * @throws {SchemaValidationError} If the updated data fails schema validation\n * @example\n * // Update single item by key\n * const tx = collection.update(\"todo-1\", (draft) => {\n * draft.completed = true\n * })\n * await tx.isPersisted.promise\n *\n * @example\n * // Update multiple items\n * const tx = collection.update([\"todo-1\", \"todo-2\"], (drafts) => {\n * drafts.forEach(draft => { draft.completed = true })\n * })\n * await tx.isPersisted.promise\n *\n * @example\n * // Update with metadata\n * const tx = collection.update(\"todo-1\",\n * { metadata: { reason: \"user update\" } },\n * (draft) => { draft.text = \"Updated text\" }\n * )\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle errors\n * try {\n * const tx = collection.update(\"item-1\", draft => { draft.value = \"new\" })\n * await tx.isPersisted.promise\n * console.log('Update successful')\n * } catch (error) {\n * console.log('Update failed:', error)\n * }\n */\n\n // Overload 1: Update multiple items with a callback\n update(\n key: Array<TKey | unknown>,\n callback: (drafts: Array<WritableDeep<TInput>>) => void,\n ): TransactionType\n\n // Overload 2: Update multiple items with config and a callback\n update(\n keys: Array<TKey | unknown>,\n config: OperationConfig,\n callback: (drafts: Array<WritableDeep<TInput>>) => void,\n ): TransactionType\n\n // Overload 3: Update a single item with a callback\n update(\n id: TKey | unknown,\n callback: (draft: WritableDeep<TInput>) => void,\n ): TransactionType\n\n // Overload 4: Update a single item with config and a callback\n update(\n id: TKey | unknown,\n config: OperationConfig,\n callback: (draft: WritableDeep<TInput>) => void,\n ): TransactionType\n\n update(\n keys: (TKey | unknown) | Array<TKey | unknown>,\n configOrCallback:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void)\n | OperationConfig,\n maybeCallback?:\n | ((draft: WritableDeep<TInput>) => void)\n | ((drafts: Array<WritableDeep<TInput>>) => void),\n ) {\n return this._mutations.update(keys, configOrCallback, maybeCallback)\n }\n\n /**\n * Deletes one or more items from the collection\n * @param keys - Single key or array of keys to delete\n * @param config - Optional configuration including metadata\n * @returns A Transaction object representing the delete operation(s)\n * @example\n * // Delete a single item\n * const tx = collection.delete(\"todo-1\")\n * await tx.isPersisted.promise\n *\n * @example\n * // Delete multiple items\n * const tx = collection.delete([\"todo-1\", \"todo-2\"])\n * await tx.isPersisted.promise\n *\n * @example\n * // Delete with metadata\n * const tx = collection.delete(\"todo-1\", { metadata: { reason: \"completed\" } })\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle errors\n * try {\n * const tx = collection.delete(\"item-1\")\n * await tx.isPersisted.promise\n * console.log('Delete successful')\n * } catch (error) {\n * console.log('Delete failed:', error)\n * }\n */\n delete = (\n keys: Array<TKey> | TKey,\n config?: OperationConfig,\n ): TransactionType<any> => {\n return this._mutations.delete(keys, config)\n }\n\n /**\n * Gets the current state of the collection as a Map\n * @returns Map containing all items in the collection, with keys as identifiers\n * @example\n * const itemsMap = collection.state\n * console.log(`Collection has ${itemsMap.size} items`)\n *\n * for (const [key, item] of itemsMap) {\n * console.log(`${key}: ${item.title}`)\n * }\n *\n * // Check if specific item exists\n * if (itemsMap.has(\"todo-1\")) {\n * console.log(\"Todo 1 exists:\", itemsMap.get(\"todo-1\"))\n * }\n */\n get state() {\n const result = new Map<TKey, TOutput>()\n for (const [key, value] of this.entries()) {\n result.set(key, value)\n }\n return result\n }\n\n /**\n * Gets the current state of the collection as a Map, but only resolves when data is available\n * Waits for the first sync commit to complete before resolving\n *\n * @returns Promise that resolves to a Map containing all items in the collection\n */\n stateWhenReady(): Promise<Map<TKey, TOutput>> {\n // If we already have data or collection is ready, resolve immediately\n if (this.size > 0 || this.isReady()) {\n return Promise.resolve(this.state)\n }\n\n // Use preload to ensure the collection starts loading, then return the state\n return this.preload().then(() => this.state)\n }\n\n /**\n * Gets the current state of the collection as an Array\n *\n * @returns An Array containing all items in the collection\n */\n get toArray() {\n return Array.from(this.values())\n }\n\n /**\n * Gets the current state of the collection as an Array, but only resolves when data is available\n * Waits for the first sync commit to complete before resolving\n *\n * @returns Promise that resolves to an Array containing all items in the collection\n */\n toArrayWhenReady(): Promise<Array<TOutput>> {\n // If we already have data or collection is ready, resolve immediately\n if (this.size > 0 || this.isReady()) {\n return Promise.resolve(this.toArray)\n }\n\n // Use preload to ensure the collection starts loading, then return the array\n return this.preload().then(() => this.toArray)\n }\n\n /**\n * Returns the current state of the collection as an array of changes\n * @param options - Options including optional where filter\n * @returns An array of changes\n * @example\n * // Get all items as changes\n * const allChanges = collection.currentStateAsChanges()\n *\n * // Get only items matching a condition\n * const activeChanges = collection.currentStateAsChanges({\n * where: (row) => row.status === 'active'\n * })\n *\n * // Get only items using a pre-compiled expression\n * const activeChanges = collection.currentStateAsChanges({\n * whereExpression: eq(row.status, 'active')\n * })\n */\n public currentStateAsChanges(\n options: CurrentStateAsChangesOptions = {},\n ): Array<ChangeMessage<TOutput>> | void {\n return currentStateAsChanges(this, options)\n }\n\n /**\n * Subscribe to changes in the collection\n * @param callback - Function called when items change\n * @param options - Subscription options including includeInitialState and where filter\n * @returns Unsubscribe function - Call this to stop listening for changes\n * @example\n * // Basic subscription\n * const subscription = collection.subscribeChanges((changes) => {\n * changes.forEach(change => {\n * console.log(`${change.type}: ${change.key}`, change.value)\n * })\n * })\n *\n * // Later: subscription.unsubscribe()\n *\n * @example\n * // Include current state immediately\n * const subscription = collection.subscribeChanges((changes) => {\n * updateUI(changes)\n * }, { includeInitialState: true })\n *\n * @example\n * // Subscribe only to changes matching a condition using where callback\n * import { eq } from \"@tanstack/db\"\n *\n * const subscription = collection.subscribeChanges((changes) => {\n * updateUI(changes)\n * }, {\n * includeInitialState: true,\n * where: (row) => eq(row.status, \"active\")\n * })\n *\n * @example\n * // Using multiple conditions with and()\n * import { and, eq, gt } from \"@tanstack/db\"\n *\n * const subscription = collection.subscribeChanges((changes) => {\n * updateUI(changes)\n * }, {\n * where: (row) => and(eq(row.status, \"active\"), gt(row.priority, 5))\n * })\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<TOutput>>) => void,\n options: SubscribeChangesOptions<TOutput> = {},\n ): CollectionSubscription {\n return this._changes.subscribeChanges(callback, options)\n }\n\n /**\n * Subscribe to a collection event\n */\n public on<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>,\n ) {\n return this._events.on(event, callback)\n }\n\n /**\n * Subscribe to a collection event once\n */\n public once<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>,\n ) {\n return this._events.once(event, callback)\n }\n\n /**\n * Unsubscribe from a collection event\n */\n public off<T extends keyof AllCollectionEvents>(\n event: T,\n callback: CollectionEventHandler<T>,\n ) {\n this._events.off(event, callback)\n }\n\n /**\n * Wait for a collection event\n */\n public waitFor<T extends keyof AllCollectionEvents>(\n event: T,\n timeout?: number,\n ) {\n return this._events.waitFor(event, timeout)\n }\n\n /**\n * Clean up the collection by stopping sync and clearing data\n * This can be called manually or automatically by garbage collection\n */\n public async cleanup(): Promise<void> {\n this._lifecycle.cleanup()\n return Promise.resolve()\n }\n}\n\nfunction buildCompareOptionsFromConfig(\n config: CollectionConfig<any, any, any>,\n): StringCollationConfig {\n if (config.defaultStringCollation) {\n const options = config.defaultStringCollation\n return {\n stringSort: options.stringSort ?? `locale`,\n locale: options.stringSort === `locale` ? options.locale : undefined,\n localeOptions:\n options.stringSort === `locale` ? options.localeOptions : undefined,\n }\n } else {\n return {\n stringSort: `locale`,\n }\n }\n}\n"],"names":["config"],"mappings":"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"}
|
package/dist/esm/errors.d.ts
CHANGED
|
@@ -147,6 +147,9 @@ export declare class JoinConditionMustBeEqualityError extends QueryBuilderError
|
|
|
147
147
|
export declare class QueryMustHaveFromClauseError extends QueryBuilderError {
|
|
148
148
|
constructor();
|
|
149
149
|
}
|
|
150
|
+
export declare class InvalidWhereExpressionError extends QueryBuilderError {
|
|
151
|
+
constructor(valueType: string);
|
|
152
|
+
}
|
|
150
153
|
export declare class QueryCompilationError extends TanStackDBError {
|
|
151
154
|
constructor(message: string);
|
|
152
155
|
}
|