@tanstack/db 0.5.18 → 0.5.20

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.
Files changed (74) hide show
  1. package/dist/cjs/collection/sync.cjs +2 -1
  2. package/dist/cjs/collection/sync.cjs.map +1 -1
  3. package/dist/cjs/proxy.cjs +1 -1
  4. package/dist/cjs/proxy.cjs.map +1 -1
  5. package/dist/cjs/query/builder/index.cjs +5 -4
  6. package/dist/cjs/query/builder/index.cjs.map +1 -1
  7. package/dist/cjs/query/builder/index.d.cts +5 -4
  8. package/dist/cjs/query/builder/ref-proxy.cjs +63 -0
  9. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -1
  10. package/dist/cjs/query/builder/ref-proxy.d.cts +13 -0
  11. package/dist/cjs/query/builder/types.d.cts +26 -1
  12. package/dist/cjs/query/compiler/evaluators.cjs +29 -2
  13. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -1
  14. package/dist/cjs/query/compiler/group-by.cjs +29 -12
  15. package/dist/cjs/query/compiler/group-by.cjs.map +1 -1
  16. package/dist/cjs/query/compiler/group-by.d.cts +18 -2
  17. package/dist/cjs/query/compiler/index.cjs +5 -5
  18. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  19. package/dist/cjs/query/compiler/order-by.cjs +1 -1
  20. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  21. package/dist/cjs/query/compiler/order-by.d.cts +1 -1
  22. package/dist/cjs/query/compiler/select.cjs +1 -1
  23. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  24. package/dist/cjs/query/compiler/select.d.cts +1 -1
  25. package/dist/cjs/query/live/collection-config-builder.cjs +17 -10
  26. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  27. package/dist/cjs/query/live/collection-subscriber.cjs +1 -1
  28. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  29. package/dist/cjs/query/live/internal.cjs +1 -1
  30. package/dist/cjs/query/live/internal.cjs.map +1 -1
  31. package/dist/cjs/query/live/types.d.cts +2 -1
  32. package/dist/esm/collection/sync.js +2 -1
  33. package/dist/esm/collection/sync.js.map +1 -1
  34. package/dist/esm/proxy.js +1 -1
  35. package/dist/esm/proxy.js.map +1 -1
  36. package/dist/esm/query/builder/index.d.ts +5 -4
  37. package/dist/esm/query/builder/index.js +6 -5
  38. package/dist/esm/query/builder/index.js.map +1 -1
  39. package/dist/esm/query/builder/ref-proxy.d.ts +13 -0
  40. package/dist/esm/query/builder/ref-proxy.js +63 -0
  41. package/dist/esm/query/builder/ref-proxy.js.map +1 -1
  42. package/dist/esm/query/builder/types.d.ts +26 -1
  43. package/dist/esm/query/compiler/evaluators.js +29 -2
  44. package/dist/esm/query/compiler/evaluators.js.map +1 -1
  45. package/dist/esm/query/compiler/group-by.d.ts +18 -2
  46. package/dist/esm/query/compiler/group-by.js +30 -13
  47. package/dist/esm/query/compiler/group-by.js.map +1 -1
  48. package/dist/esm/query/compiler/index.js +5 -5
  49. package/dist/esm/query/compiler/index.js.map +1 -1
  50. package/dist/esm/query/compiler/order-by.d.ts +1 -1
  51. package/dist/esm/query/compiler/order-by.js +1 -1
  52. package/dist/esm/query/compiler/order-by.js.map +1 -1
  53. package/dist/esm/query/compiler/select.d.ts +1 -1
  54. package/dist/esm/query/compiler/select.js +1 -1
  55. package/dist/esm/query/compiler/select.js.map +1 -1
  56. package/dist/esm/query/live/collection-config-builder.js +17 -10
  57. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  58. package/dist/esm/query/live/collection-subscriber.js +1 -1
  59. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  60. package/dist/esm/query/live/internal.js +1 -1
  61. package/dist/esm/query/live/internal.js.map +1 -1
  62. package/dist/esm/query/live/types.d.ts +2 -1
  63. package/package.json +2 -2
  64. package/src/collection/sync.ts +2 -2
  65. package/src/query/builder/index.ts +22 -6
  66. package/src/query/builder/ref-proxy.ts +90 -0
  67. package/src/query/builder/types.ts +26 -1
  68. package/src/query/compiler/evaluators.ts +38 -2
  69. package/src/query/compiler/group-by.ts +76 -22
  70. package/src/query/compiler/index.ts +13 -13
  71. package/src/query/compiler/order-by.ts +7 -6
  72. package/src/query/compiler/select.ts +5 -8
  73. package/src/query/live/collection-config-builder.ts +46 -17
  74. package/src/query/live/types.ts +3 -1
@@ -1 +1 @@
1
- {"version":3,"file":"collection-config-builder.js","sources":["../../../../src/query/live/collection-config-builder.ts"],"sourcesContent":["import { D2, output } from '@tanstack/db-ivm'\nimport { compileQuery } from '../compiler/index.js'\nimport { buildQuery, getQueryIR } from '../builder/index.js'\nimport {\n MissingAliasInputsError,\n SetWindowRequiresOrderByError,\n} from '../../errors.js'\nimport { transactionScopedScheduler } from '../../scheduler.js'\nimport { getActiveTransaction } from '../../transactions.js'\nimport { CollectionSubscriber } from './collection-subscriber.js'\nimport { getCollectionBuilder } from './collection-registry.js'\nimport { LIVE_QUERY_INTERNAL } from './internal.js'\nimport type { LiveQueryInternalUtils } from './internal.js'\nimport type { WindowOptions } from '../compiler/index.js'\nimport type { SchedulerContextId } from '../../scheduler.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n CollectionConfigSingleRowOption,\n KeyedStream,\n ResultStream,\n StringCollationConfig,\n SyncConfig,\n UtilsRecord,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression, QueryIR } from '../ir.js'\nimport type { LazyCollectionCallbacks } from '../compiler/joins.js'\nimport type {\n Changes,\n FullSyncState,\n LiveQueryCollectionConfig,\n SyncState,\n} from './types.js'\nimport type { AllCollectionEvents } from '../../collection/events.js'\n\nexport type LiveQueryCollectionUtils = UtilsRecord & {\n getRunCount: () => number\n /**\n * Sets the offset and limit of an ordered query.\n * Is a no-op if the query is not ordered.\n *\n * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded\n */\n setWindow: (options: WindowOptions) => true | Promise<void>\n /**\n * Gets the current window (offset and limit) for an ordered query.\n *\n * @returns The current window settings, or `undefined` if the query is not windowed\n */\n getWindow: () => { offset: number; limit: number } | undefined\n [LIVE_QUERY_INTERNAL]: LiveQueryInternalUtils\n}\n\ntype PendingGraphRun = {\n loadCallbacks: Set<() => boolean>\n}\n\n// Global counter for auto-generated collection IDs\nlet liveQueryCollectionCounter = 0\n\ntype SyncMethods<TResult extends object> = Parameters<\n SyncConfig<TResult>[`sync`]\n>[0]\n\nexport class CollectionConfigBuilder<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n private readonly id: string\n readonly query: QueryIR\n private readonly collections: Record<string, Collection<any, any, any>>\n private readonly collectionByAlias: Record<string, Collection<any, any, any>>\n // Populated during compilation with all aliases (including subquery inner aliases)\n private compiledAliasToCollectionId: Record<string, string> = {}\n\n // WeakMap to store the keys of the results\n // so that we can retrieve them in the getKey function\n private readonly resultKeys = new WeakMap<object, unknown>()\n\n // WeakMap to store the orderBy index for each result\n private readonly orderByIndices = new WeakMap<object, string>()\n\n private readonly compare?: (val1: TResult, val2: TResult) => number\n private readonly compareOptions?: StringCollationConfig\n\n private isGraphRunning = false\n private runCount = 0\n\n // Current sync session state (set when sync starts, cleared when it stops)\n // Public for testing purposes (CollectionConfigBuilder is internal, not public API)\n public currentSyncConfig:\n | Parameters<SyncConfig<TResult>[`sync`]>[0]\n | undefined\n public currentSyncState: FullSyncState | undefined\n\n // Error state tracking\n private isInErrorState = false\n\n // Reference to the live query collection for error state transitions\n public liveQueryCollection?: Collection<TResult, any, any>\n\n private windowFn: ((options: WindowOptions) => void) | undefined\n private currentWindow: WindowOptions | undefined\n\n private maybeRunGraphFn: (() => void) | undefined\n\n private readonly aliasDependencies: Record<\n string,\n Array<CollectionConfigBuilder<any, any>>\n > = {}\n\n private readonly builderDependencies = new Set<\n CollectionConfigBuilder<any, any>\n >()\n\n // Pending graph runs per scheduler context (e.g., per transaction)\n // The builder manages its own state; the scheduler just orchestrates execution order\n // Only stores callbacks - if sync ends, pending jobs gracefully no-op\n private readonly pendingGraphRuns = new Map<\n SchedulerContextId,\n PendingGraphRun\n >()\n\n // Unsubscribe function for scheduler's onClear listener\n // Registered when sync starts, unregistered when sync stops\n // Prevents memory leaks by releasing the scheduler's reference to this builder\n private unsubscribeFromSchedulerClears?: () => void\n\n private graphCache: D2 | undefined\n private inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined\n private pipelineCache: ResultStream | undefined\n public sourceWhereClausesCache:\n | Map<string, BasicExpression<boolean>>\n | undefined\n\n // Map of source alias to subscription\n readonly subscriptions: Record<string, CollectionSubscription> = {}\n // Map of source aliases to functions that load keys for that lazy source\n lazySourcesCallbacks: Record<string, LazyCollectionCallbacks> = {}\n // Set of source aliases that are lazy (don't load initial state)\n readonly lazySources = new Set<string>()\n // Set of collection IDs that include an optimizable ORDER BY clause\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo> = {}\n\n constructor(\n private readonly config: LiveQueryCollectionConfig<TContext, TResult>,\n ) {\n // Generate a unique ID if not provided\n this.id = config.id || `live-query-${++liveQueryCollectionCounter}`\n\n this.query = buildQueryFromConfig(config)\n this.collections = extractCollectionsFromQuery(this.query)\n const collectionAliasesById = extractCollectionAliases(this.query)\n\n // Build a reverse lookup map from alias to collection instance.\n // This enables self-join support where the same collection can be referenced\n // multiple times with different aliases (e.g., { employee: col, manager: col })\n this.collectionByAlias = {}\n for (const [collectionId, aliases] of collectionAliasesById.entries()) {\n const collection = this.collections[collectionId]\n if (!collection) continue\n for (const alias of aliases) {\n this.collectionByAlias[alias] = collection\n }\n }\n\n // Create compare function for ordering if the query has orderBy\n if (this.query.orderBy && this.query.orderBy.length > 0) {\n this.compare = createOrderByComparator<TResult>(this.orderByIndices)\n }\n\n // Use explicitly provided compareOptions if available, otherwise inherit from FROM collection\n this.compareOptions =\n this.config.defaultStringCollation ??\n extractCollectionFromSource(this.query).compareOptions\n\n // Compile the base pipeline once initially\n // This is done to ensure that any errors are thrown immediately and synchronously\n this.compileBasePipeline()\n }\n\n /**\n * Recursively checks if a query or any of its subqueries contains joins\n */\n private hasJoins(query: QueryIR): boolean {\n // Check if this query has joins\n if (query.join && query.join.length > 0) {\n return true\n }\n\n // Recursively check subqueries in the from clause\n if (query.from.type === `queryRef`) {\n if (this.hasJoins(query.from.query)) {\n return true\n }\n }\n\n return false\n }\n\n getConfig(): CollectionConfigSingleRowOption<TResult> & {\n utils: LiveQueryCollectionUtils\n } {\n return {\n id: this.id,\n getKey:\n this.config.getKey ||\n ((item) => this.resultKeys.get(item) as string | number),\n sync: this.getSyncConfig(),\n compare: this.compare,\n defaultStringCollation: this.compareOptions,\n gcTime: this.config.gcTime || 5000, // 5 seconds by default for live queries\n schema: this.config.schema,\n onInsert: this.config.onInsert,\n onUpdate: this.config.onUpdate,\n onDelete: this.config.onDelete,\n startSync: this.config.startSync,\n singleResult: this.query.singleResult,\n utils: {\n getRunCount: this.getRunCount.bind(this),\n setWindow: this.setWindow.bind(this),\n getWindow: this.getWindow.bind(this),\n [LIVE_QUERY_INTERNAL]: {\n getBuilder: () => this,\n hasCustomGetKey: !!this.config.getKey,\n hasJoins: this.hasJoins(this.query),\n },\n },\n }\n }\n\n setWindow(options: WindowOptions): true | Promise<void> {\n if (!this.windowFn) {\n throw new SetWindowRequiresOrderByError()\n }\n\n this.currentWindow = options\n this.windowFn(options)\n this.maybeRunGraphFn?.()\n\n // Check if loading a subset was triggered\n if (this.liveQueryCollection?.isLoadingSubset) {\n // Loading was triggered, return a promise that resolves when it completes\n return new Promise<void>((resolve) => {\n const unsubscribe = this.liveQueryCollection!.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n unsubscribe()\n resolve()\n }\n },\n )\n })\n }\n\n // No loading was triggered\n return true\n }\n\n getWindow(): { offset: number; limit: number } | undefined {\n // Only return window if this is a windowed query (has orderBy and windowFn)\n if (!this.windowFn || !this.currentWindow) {\n return undefined\n }\n return {\n offset: this.currentWindow.offset ?? 0,\n limit: this.currentWindow.limit ?? 0,\n }\n }\n\n /**\n * Resolves a collection alias to its collection ID.\n *\n * Uses a two-tier lookup strategy:\n * 1. First checks compiled aliases (includes subquery inner aliases)\n * 2. Falls back to declared aliases from the query's from/join clauses\n *\n * @param alias - The alias to resolve (e.g., \"employee\", \"manager\")\n * @returns The collection ID that the alias references\n * @throws {Error} If the alias is not found in either lookup\n */\n getCollectionIdForAlias(alias: string): string {\n const compiled = this.compiledAliasToCollectionId[alias]\n if (compiled) {\n return compiled\n }\n const collection = this.collectionByAlias[alias]\n if (collection) {\n return collection.id\n }\n throw new Error(`Unknown source alias \"${alias}\"`)\n }\n\n isLazyAlias(alias: string): boolean {\n return this.lazySources.has(alias)\n }\n\n // The callback function is called after the graph has run.\n // This gives the callback a chance to load more data if needed,\n // that's used to optimize orderBy operators that set a limit,\n // in order to load some more data if we still don't have enough rows after the pipeline has run.\n // That can happen because even though we load N rows, the pipeline might filter some of these rows out\n // causing the orderBy operator to receive less than N rows or even no rows at all.\n // So this callback would notice that it doesn't have enough rows and load some more.\n // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.\n maybeRunGraph(callback?: () => boolean) {\n if (this.isGraphRunning) {\n // no nested runs of the graph\n // which is possible if the `callback`\n // would call `maybeRunGraph` e.g. after it has loaded some more data\n return\n }\n\n // Should only be called when sync is active\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `maybeRunGraph called without active sync session. This should not happen.`,\n )\n }\n\n this.isGraphRunning = true\n\n try {\n const { begin, commit } = this.currentSyncConfig\n const syncState = this.currentSyncState\n\n // Don't run if the live query is in an error state\n if (this.isInErrorState) {\n return\n }\n\n // Always run the graph if subscribed (eager execution)\n if (syncState.subscribedToAllCollections) {\n while (syncState.graph.pendingWork()) {\n syncState.graph.run()\n callback?.()\n }\n\n // On the initial run, we may need to do an empty commit to ensure that\n // the collection is initialized\n if (syncState.messagesCount === 0) {\n begin()\n commit()\n // After initial commit, check if we should mark ready\n // (in case all sources were already ready before we subscribed)\n this.updateLiveQueryStatus(this.currentSyncConfig)\n }\n }\n } finally {\n this.isGraphRunning = false\n }\n }\n\n /**\n * Schedules a graph run with the transaction-scoped scheduler.\n * Ensures each builder runs at most once per transaction, with automatic dependency tracking\n * to run parent queries before child queries. Outside a transaction, runs immediately.\n *\n * Multiple calls during a transaction are coalesced into a single execution.\n * Dependencies are auto-discovered from subscribed live queries, or can be overridden.\n * Load callbacks are combined when entries merge.\n *\n * Uses the current sync session's config and syncState from instance properties.\n *\n * @param callback - Optional callback to load more data if needed (returns true when done)\n * @param options - Optional scheduling configuration\n * @param options.contextId - Transaction ID to group work; defaults to active transaction\n * @param options.jobId - Unique identifier for this job; defaults to this builder instance\n * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies\n * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies\n */\n scheduleGraphRun(\n callback?: () => boolean,\n options?: {\n contextId?: SchedulerContextId\n jobId?: unknown\n alias?: string\n dependencies?: Array<CollectionConfigBuilder<any, any>>\n },\n ) {\n const contextId = options?.contextId ?? getActiveTransaction()?.id\n // Use the builder instance as the job ID for deduplication. This is memory-safe\n // because the scheduler's context Map is deleted after flushing (no long-term retention).\n const jobId = options?.jobId ?? this\n const dependentBuilders = (() => {\n if (options?.dependencies) {\n return options.dependencies\n }\n\n const deps = new Set(this.builderDependencies)\n if (options?.alias) {\n const aliasDeps = this.aliasDependencies[options.alias]\n if (aliasDeps) {\n for (const dep of aliasDeps) {\n deps.add(dep)\n }\n }\n }\n\n deps.delete(this)\n\n return Array.from(deps)\n })()\n\n // Ensure dependent builders are actually scheduled in this context so that\n // dependency edges always point to a real job (or a deduped no-op if already scheduled).\n if (contextId) {\n for (const dep of dependentBuilders) {\n if (typeof dep.scheduleGraphRun === `function`) {\n dep.scheduleGraphRun(undefined, { contextId })\n }\n }\n }\n\n // We intentionally scope deduplication to the builder instance. Each instance\n // owns caches and compiled pipelines, so sharing work across instances that\n // merely reuse the same string id would execute the wrong builder's graph.\n\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `scheduleGraphRun called without active sync session. This should not happen.`,\n )\n }\n\n // Manage our own state - get or create pending callbacks for this context\n let pending = contextId ? this.pendingGraphRuns.get(contextId) : undefined\n if (!pending) {\n pending = {\n loadCallbacks: new Set(),\n }\n if (contextId) {\n this.pendingGraphRuns.set(contextId, pending)\n }\n }\n\n // Add callback if provided (this is what accumulates between schedules)\n if (callback) {\n pending.loadCallbacks.add(callback)\n }\n\n // Schedule execution (scheduler just orchestrates order, we manage state)\n // For immediate execution (no contextId), pass pending directly since it won't be in the map\n const pendingToPass = contextId ? undefined : pending\n transactionScopedScheduler.schedule({\n contextId,\n jobId,\n dependencies: dependentBuilders,\n run: () => this.executeGraphRun(contextId, pendingToPass),\n })\n }\n\n /**\n * Clears pending graph run state for a specific context.\n * Called when the scheduler clears a context (e.g., transaction rollback/abort).\n */\n clearPendingGraphRun(contextId: SchedulerContextId): void {\n this.pendingGraphRuns.delete(contextId)\n }\n\n /**\n * Returns true if this builder has a pending graph run for the given context.\n */\n hasPendingGraphRun(contextId: SchedulerContextId): boolean {\n return this.pendingGraphRuns.has(contextId)\n }\n\n /**\n * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.\n * Clears the pending state BEFORE execution so that any re-schedules during the run\n * create fresh state and don't interfere with the current execution.\n * Uses instance sync state - if sync has ended, gracefully returns without executing.\n *\n * @param contextId - Optional context ID to look up pending state\n * @param pendingParam - For immediate execution (no context), pending state is passed directly\n */\n private executeGraphRun(\n contextId?: SchedulerContextId,\n pendingParam?: PendingGraphRun,\n ): void {\n // Get pending state: either from parameter (no context) or from map (with context)\n // Remove from map BEFORE checking sync state to prevent leaking entries when sync ends\n // before the transaction flushes (e.g., unsubscribe during in-flight transaction)\n const pending =\n pendingParam ??\n (contextId ? this.pendingGraphRuns.get(contextId) : undefined)\n if (contextId) {\n this.pendingGraphRuns.delete(contextId)\n }\n\n // If no pending state, nothing to execute (context was cleared)\n if (!pending) {\n return\n }\n\n // If sync session has ended, don't execute (graph is finalized, subscriptions cleared)\n if (!this.currentSyncConfig || !this.currentSyncState) {\n return\n }\n\n this.incrementRunCount()\n\n const combinedLoader = () => {\n let allDone = true\n let firstError: unknown\n pending.loadCallbacks.forEach((loader) => {\n try {\n allDone = loader() && allDone\n } catch (error) {\n allDone = false\n firstError ??= error\n }\n })\n if (firstError) {\n throw firstError\n }\n // Returning false signals that callers should schedule another pass.\n return allDone\n }\n\n this.maybeRunGraph(combinedLoader)\n }\n\n private getSyncConfig(): SyncConfig<TResult> {\n return {\n rowUpdateMode: `full`,\n sync: this.syncFn.bind(this),\n }\n }\n\n incrementRunCount() {\n this.runCount++\n }\n\n getRunCount() {\n return this.runCount\n }\n\n private syncFn(config: SyncMethods<TResult>) {\n // Store reference to the live query collection for error state transitions\n this.liveQueryCollection = config.collection\n // Store config and syncState as instance properties for the duration of this sync session\n this.currentSyncConfig = config\n\n const syncState: SyncState = {\n messagesCount: 0,\n subscribedToAllCollections: false,\n unsubscribeCallbacks: new Set<() => void>(),\n }\n\n // Extend the pipeline such that it applies the incoming changes to the collection\n const fullSyncState = this.extendPipelineWithChangeProcessing(\n config,\n syncState,\n )\n this.currentSyncState = fullSyncState\n\n // Listen for scheduler context clears to clean up our pending state\n // Re-register on each sync start so the listener is active for the sync session's lifetime\n this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(\n (contextId) => {\n this.clearPendingGraphRun(contextId)\n },\n )\n\n // Listen for loadingSubset changes on the live query collection BEFORE subscribing.\n // This ensures we don't miss the event if subset loading completes synchronously.\n // When isLoadingSubset becomes false, we may need to mark the collection as ready\n // (if all source collections are already ready but we were waiting for subset load to complete)\n const loadingSubsetUnsubscribe = config.collection.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n // Subset loading finished, check if we can now mark ready\n this.updateLiveQueryStatus(config)\n }\n },\n )\n syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe)\n\n const loadSubsetDataCallbacks = this.subscribeToAllCollections(\n config,\n fullSyncState,\n )\n\n this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Initial run with callback to load more data if needed\n this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Return the unsubscribe function\n return () => {\n syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())\n\n // Clear current sync session state\n this.currentSyncConfig = undefined\n this.currentSyncState = undefined\n\n // Clear all pending graph runs to prevent memory leaks from in-flight transactions\n // that may flush after the sync session ends\n this.pendingGraphRuns.clear()\n\n // Reset caches so a fresh graph/pipeline is compiled on next start\n // This avoids reusing a finalized D2 graph across GC restarts\n this.graphCache = undefined\n this.inputsCache = undefined\n this.pipelineCache = undefined\n this.sourceWhereClausesCache = undefined\n\n // Reset lazy source alias state\n this.lazySources.clear()\n this.optimizableOrderByCollections = {}\n this.lazySourcesCallbacks = {}\n\n // Clear subscription references to prevent memory leaks\n // Note: Individual subscriptions are already unsubscribed via unsubscribeCallbacks\n Object.keys(this.subscriptions).forEach(\n (key) => delete this.subscriptions[key],\n )\n this.compiledAliasToCollectionId = {}\n\n // Unregister from scheduler's onClear listener to prevent memory leaks\n // The scheduler's listener Set would otherwise keep a strong reference to this builder\n this.unsubscribeFromSchedulerClears?.()\n this.unsubscribeFromSchedulerClears = undefined\n }\n }\n\n /**\n * Compiles the query pipeline with all declared aliases.\n */\n private compileBasePipeline() {\n this.graphCache = new D2()\n this.inputsCache = Object.fromEntries(\n Object.keys(this.collectionByAlias).map((alias) => [\n alias,\n this.graphCache!.newInput<any>(),\n ]),\n )\n\n const compilation = compileQuery(\n this.query,\n this.inputsCache as Record<string, KeyedStream>,\n this.collections,\n this.subscriptions,\n this.lazySourcesCallbacks,\n this.lazySources,\n this.optimizableOrderByCollections,\n (windowFn: (options: WindowOptions) => void) => {\n this.windowFn = windowFn\n },\n )\n\n this.pipelineCache = compilation.pipeline\n this.sourceWhereClausesCache = compilation.sourceWhereClauses\n this.compiledAliasToCollectionId = compilation.aliasToCollectionId\n\n // Defensive check: verify all compiled aliases have corresponding inputs\n // This should never happen since all aliases come from user declarations,\n // but catch it early if the assumption is violated in the future.\n const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(\n (alias) => !Object.hasOwn(this.inputsCache!, alias),\n )\n if (missingAliases.length > 0) {\n throw new MissingAliasInputsError(missingAliases)\n }\n }\n\n private maybeCompileBasePipeline() {\n if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {\n this.compileBasePipeline()\n }\n return {\n graph: this.graphCache!,\n inputs: this.inputsCache!,\n pipeline: this.pipelineCache!,\n }\n }\n\n private extendPipelineWithChangeProcessing(\n config: SyncMethods<TResult>,\n syncState: SyncState,\n ): FullSyncState {\n const { begin, commit } = config\n const { graph, inputs, pipeline } = this.maybeCompileBasePipeline()\n\n pipeline.pipe(\n output((data) => {\n const messages = data.getInner()\n syncState.messagesCount += messages.length\n\n begin()\n messages\n .reduce(\n accumulateChanges<TResult>,\n new Map<unknown, Changes<TResult>>(),\n )\n .forEach(this.applyChanges.bind(this, config))\n commit()\n }),\n )\n\n graph.finalize()\n\n // Extend the sync state with the graph, inputs, and pipeline\n syncState.graph = graph\n syncState.inputs = inputs\n syncState.pipeline = pipeline\n\n return syncState as FullSyncState\n }\n\n private applyChanges(\n config: SyncMethods<TResult>,\n changes: {\n deletes: number\n inserts: number\n value: TResult\n orderByIndex: string | undefined\n },\n key: unknown,\n ) {\n const { write, collection } = config\n const { deletes, inserts, value, orderByIndex } = changes\n\n // Store the key of the result so that we can retrieve it in the\n // getKey function\n this.resultKeys.set(value, key)\n\n // Store the orderBy index if it exists\n if (orderByIndex !== undefined) {\n this.orderByIndices.set(value, orderByIndex)\n }\n\n // Simple singular insert.\n if (inserts && deletes === 0) {\n write({\n value,\n type: `insert`,\n })\n } else if (\n // Insert & update(s) (updates are a delete & insert)\n inserts > deletes ||\n // Just update(s) but the item is already in the collection (so\n // was inserted previously).\n (inserts === deletes && collection.has(collection.getKeyFromItem(value)))\n ) {\n write({\n value,\n type: `update`,\n })\n // Only delete is left as an option\n } else if (deletes > 0) {\n write({\n value,\n type: `delete`,\n })\n } else {\n throw new Error(\n `Could not apply changes: ${JSON.stringify(changes)}. This should never happen.`,\n )\n }\n }\n\n /**\n * Handle status changes from source collections\n */\n private handleSourceStatusChange(\n config: SyncMethods<TResult>,\n collectionId: string,\n event: AllCollectionEvents[`status:change`],\n ) {\n const { status } = event\n\n // Handle error state - any source collection in error puts live query in error\n if (status === `error`) {\n this.transitionToError(\n `Source collection '${collectionId}' entered error state`,\n )\n return\n }\n\n // Handle manual cleanup - this should not happen due to GC prevention,\n // but could happen if user manually calls cleanup()\n if (status === `cleaned-up`) {\n this.transitionToError(\n `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. ` +\n `Live queries prevent automatic GC, so this was likely a manual cleanup() call.`,\n )\n return\n }\n\n // Update ready status based on all source collections\n this.updateLiveQueryStatus(config)\n }\n\n /**\n * Update the live query status based on source collection statuses\n */\n private updateLiveQueryStatus(config: SyncMethods<TResult>) {\n const { markReady } = config\n\n // Don't update status if already in error\n if (this.isInErrorState) {\n return\n }\n\n // Mark ready when all source collections are ready AND\n // the live query collection is not loading subset data.\n // This prevents marking the live query ready before its data is loaded\n // (fixes issue where useLiveQuery returns isReady=true with empty data)\n if (\n this.allCollectionsReady() &&\n !this.liveQueryCollection?.isLoadingSubset\n ) {\n markReady()\n }\n }\n\n /**\n * Transition the live query to error state\n */\n private transitionToError(message: string) {\n this.isInErrorState = true\n\n // Log error to console for debugging\n console.error(`[Live Query Error] ${message}`)\n\n // Transition live query collection to error state\n this.liveQueryCollection?._lifecycle.setStatus(`error`)\n }\n\n private allCollectionsReady() {\n return Object.values(this.collections).every((collection) =>\n collection.isReady(),\n )\n }\n\n /**\n * Creates per-alias subscriptions enabling self-join support.\n * Each alias gets its own subscription with independent filters, even for the same collection.\n * Example: `{ employee: col, manager: col }` creates two separate subscriptions.\n */\n private subscribeToAllCollections(\n config: SyncMethods<TResult>,\n syncState: FullSyncState,\n ) {\n // Use compiled aliases as the source of truth - these include all aliases from the query\n // including those from subqueries, which may not be in collectionByAlias\n const compiledAliases = Object.entries(this.compiledAliasToCollectionId)\n if (compiledAliases.length === 0) {\n throw new Error(\n `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`,\n )\n }\n\n // Create a separate subscription for each alias, enabling self-joins where the same\n // collection can be used multiple times with different filters and subscriptions\n const loaders = compiledAliases.map(([alias, collectionId]) => {\n // Try collectionByAlias first (for declared aliases), fall back to collections (for subquery aliases)\n const collection =\n this.collectionByAlias[alias] ?? this.collections[collectionId]!\n\n const dependencyBuilder = getCollectionBuilder(collection)\n if (dependencyBuilder && dependencyBuilder !== this) {\n this.aliasDependencies[alias] = [dependencyBuilder]\n this.builderDependencies.add(dependencyBuilder)\n } else {\n this.aliasDependencies[alias] = []\n }\n\n // CollectionSubscriber handles the actual subscription to the source collection\n // and feeds data into the D2 graph inputs for this specific alias\n const collectionSubscriber = new CollectionSubscriber(\n alias,\n collectionId,\n collection,\n this,\n )\n\n // Subscribe to status changes for status flow\n const statusUnsubscribe = collection.on(`status:change`, (event) => {\n this.handleSourceStatusChange(config, collectionId, event)\n })\n syncState.unsubscribeCallbacks.add(statusUnsubscribe)\n\n const subscription = collectionSubscriber.subscribe()\n // Store subscription by alias (not collection ID) to support lazy loading\n // which needs to look up subscriptions by their query alias\n this.subscriptions[alias] = subscription\n\n // Create a callback for loading more data if needed (used by OrderBy optimization)\n const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(\n collectionSubscriber,\n subscription,\n )\n\n return loadMore\n })\n\n // Combine all loaders into a single callback that initiates loading more data\n // from any source that needs it. Returns true once all loaders have been called,\n // but the actual async loading may still be in progress.\n const loadSubsetDataCallbacks = () => {\n loaders.map((loader) => loader())\n return true\n }\n\n // Mark as subscribed so the graph can start running\n // (graph only runs when all collections are subscribed)\n syncState.subscribedToAllCollections = true\n\n // Initial status check after all subscriptions are set up\n this.updateLiveQueryStatus(config)\n\n return loadSubsetDataCallbacks\n }\n}\n\nfunction buildQueryFromConfig<TContext extends Context>(\n config: LiveQueryCollectionConfig<any, any>,\n) {\n // Build the query using the provided query builder function or instance\n if (typeof config.query === `function`) {\n return buildQuery<TContext>(config.query)\n }\n return getQueryIR(config.query)\n}\n\nfunction createOrderByComparator<T extends object>(\n orderByIndices: WeakMap<object, string>,\n) {\n return (val1: T, val2: T): number => {\n // Use the orderBy index stored in the WeakMap\n const index1 = orderByIndices.get(val1)\n const index2 = orderByIndices.get(val2)\n\n // Compare fractional indices lexicographically\n if (index1 && index2) {\n if (index1 < index2) {\n return -1\n } else if (index1 > index2) {\n return 1\n } else {\n return 0\n }\n }\n\n // Fallback to no ordering if indices are missing\n return 0\n }\n}\n\n/**\n * Helper function to extract collections from a compiled query\n * Traverses the query IR to find all collection references\n * Maps collections by their ID (not alias) as expected by the compiler\n */\nfunction extractCollectionsFromQuery(\n query: any,\n): Record<string, Collection<any, any, any>> {\n const collections: Record<string, any> = {}\n\n // Helper function to recursively extract collections from a query or source\n function extractFromSource(source: any) {\n if (source.type === `collectionRef`) {\n collections[source.collection.id] = source.collection\n } else if (source.type === `queryRef`) {\n // Recursively extract from subquery\n extractFromQuery(source.query)\n }\n }\n\n // Helper function to recursively extract collections from a query\n function extractFromQuery(q: any) {\n // Extract from FROM clause\n if (q.from) {\n extractFromSource(q.from)\n }\n\n // Extract from JOIN clauses\n if (q.join && Array.isArray(q.join)) {\n for (const joinClause of q.join) {\n if (joinClause.from) {\n extractFromSource(joinClause.from)\n }\n }\n }\n }\n\n // Start extraction from the root query\n extractFromQuery(query)\n\n return collections\n}\n\n/**\n * Helper function to extract the collection that is referenced in the query's FROM clause.\n * The FROM clause may refer directly to a collection or indirectly to a subquery.\n */\nfunction extractCollectionFromSource(query: any): Collection<any, any, any> {\n const from = query.from\n\n if (from.type === `collectionRef`) {\n return from.collection\n } else if (from.type === `queryRef`) {\n // Recursively extract from subquery\n return extractCollectionFromSource(from.query)\n }\n\n throw new Error(\n `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,\n )\n}\n\n/**\n * Extracts all aliases used for each collection across the entire query tree.\n *\n * Traverses the QueryIR recursively to build a map from collection ID to all aliases\n * that reference that collection. This is essential for self-join support, where the\n * same collection may be referenced multiple times with different aliases.\n *\n * For example, given a query like:\n * ```ts\n * q.from({ employee: employeesCollection })\n * .join({ manager: employeesCollection }, ({ employee, manager }) =>\n * eq(employee.managerId, manager.id)\n * )\n * ```\n *\n * This function would return:\n * ```\n * Map { \"employees\" => Set { \"employee\", \"manager\" } }\n * ```\n *\n * @param query - The query IR to extract aliases from\n * @returns A map from collection ID to the set of all aliases referencing that collection\n */\nfunction extractCollectionAliases(query: QueryIR): Map<string, Set<string>> {\n const aliasesById = new Map<string, Set<string>>()\n\n function recordAlias(source: any) {\n if (!source) return\n\n if (source.type === `collectionRef`) {\n const { id } = source.collection\n const existing = aliasesById.get(id)\n if (existing) {\n existing.add(source.alias)\n } else {\n aliasesById.set(id, new Set([source.alias]))\n }\n } else if (source.type === `queryRef`) {\n traverse(source.query)\n }\n }\n\n function traverse(q?: QueryIR) {\n if (!q) return\n\n recordAlias(q.from)\n\n if (q.join) {\n for (const joinClause of q.join) {\n recordAlias(joinClause.from)\n }\n }\n }\n\n traverse(query)\n\n return aliasesById\n}\n\nfunction accumulateChanges<T>(\n acc: Map<unknown, Changes<T>>,\n [[key, tupleData], multiplicity]: [\n [unknown, [any, string | undefined]],\n number,\n ],\n) {\n // All queries now consistently return [value, orderByIndex] format\n // where orderByIndex is undefined for queries without ORDER BY\n const [value, orderByIndex] = tupleData as [T, string | undefined]\n\n const changes = acc.get(key) || {\n deletes: 0,\n inserts: 0,\n value,\n orderByIndex,\n }\n if (multiplicity < 0) {\n changes.deletes += Math.abs(multiplicity)\n } else if (multiplicity > 0) {\n changes.inserts += multiplicity\n changes.value = value\n changes.orderByIndex = orderByIndex\n }\n acc.set(key, changes)\n return acc\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"collection-config-builder.js","sources":["../../../../src/query/live/collection-config-builder.ts"],"sourcesContent":["import { D2, output } from '@tanstack/db-ivm'\nimport { compileQuery } from '../compiler/index.js'\nimport { buildQuery, getQueryIR } from '../builder/index.js'\nimport {\n MissingAliasInputsError,\n SetWindowRequiresOrderByError,\n} from '../../errors.js'\nimport { transactionScopedScheduler } from '../../scheduler.js'\nimport { getActiveTransaction } from '../../transactions.js'\nimport { CollectionSubscriber } from './collection-subscriber.js'\nimport { getCollectionBuilder } from './collection-registry.js'\nimport { LIVE_QUERY_INTERNAL } from './internal.js'\nimport type { LiveQueryInternalUtils } from './internal.js'\nimport type { WindowOptions } from '../compiler/index.js'\nimport type { SchedulerContextId } from '../../scheduler.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\nimport type { RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n CollectionConfigSingleRowOption,\n KeyedStream,\n ResultStream,\n StringCollationConfig,\n SyncConfig,\n UtilsRecord,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression, QueryIR } from '../ir.js'\nimport type { LazyCollectionCallbacks } from '../compiler/joins.js'\nimport type {\n Changes,\n FullSyncState,\n LiveQueryCollectionConfig,\n SyncState,\n} from './types.js'\nimport type { AllCollectionEvents } from '../../collection/events.js'\n\nexport type LiveQueryCollectionUtils = UtilsRecord & {\n getRunCount: () => number\n /**\n * Sets the offset and limit of an ordered query.\n * Is a no-op if the query is not ordered.\n *\n * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded\n */\n setWindow: (options: WindowOptions) => true | Promise<void>\n /**\n * Gets the current window (offset and limit) for an ordered query.\n *\n * @returns The current window settings, or `undefined` if the query is not windowed\n */\n getWindow: () => { offset: number; limit: number } | undefined\n [LIVE_QUERY_INTERNAL]: LiveQueryInternalUtils\n}\n\ntype PendingGraphRun = {\n loadCallbacks: Set<() => boolean>\n}\n\n// Global counter for auto-generated collection IDs\nlet liveQueryCollectionCounter = 0\n\ntype SyncMethods<TResult extends object> = Parameters<\n SyncConfig<TResult>[`sync`]\n>[0]\n\nexport class CollectionConfigBuilder<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n private readonly id: string\n readonly query: QueryIR\n private readonly collections: Record<string, Collection<any, any, any>>\n private readonly collectionByAlias: Record<string, Collection<any, any, any>>\n // Populated during compilation with all aliases (including subquery inner aliases)\n private compiledAliasToCollectionId: Record<string, string> = {}\n\n // WeakMap to store the keys of the results\n // so that we can retrieve them in the getKey function\n private readonly resultKeys = new WeakMap<object, unknown>()\n\n // WeakMap to store the orderBy index for each result\n private readonly orderByIndices = new WeakMap<object, string>()\n\n private readonly compare?: (val1: TResult, val2: TResult) => number\n private readonly compareOptions?: StringCollationConfig\n\n private isGraphRunning = false\n private runCount = 0\n\n // Current sync session state (set when sync starts, cleared when it stops)\n // Public for testing purposes (CollectionConfigBuilder is internal, not public API)\n public currentSyncConfig:\n | Parameters<SyncConfig<TResult>[`sync`]>[0]\n | undefined\n public currentSyncState: FullSyncState | undefined\n\n // Error state tracking\n private isInErrorState = false\n\n // Reference to the live query collection for error state transitions\n public liveQueryCollection?: Collection<TResult, any, any>\n\n private windowFn: ((options: WindowOptions) => void) | undefined\n private currentWindow: WindowOptions | undefined\n\n private maybeRunGraphFn: (() => void) | undefined\n\n private readonly aliasDependencies: Record<\n string,\n Array<CollectionConfigBuilder<any, any>>\n > = {}\n\n private readonly builderDependencies = new Set<\n CollectionConfigBuilder<any, any>\n >()\n\n // Pending graph runs per scheduler context (e.g., per transaction)\n // The builder manages its own state; the scheduler just orchestrates execution order\n // Only stores callbacks - if sync ends, pending jobs gracefully no-op\n private readonly pendingGraphRuns = new Map<\n SchedulerContextId,\n PendingGraphRun\n >()\n\n // Unsubscribe function for scheduler's onClear listener\n // Registered when sync starts, unregistered when sync stops\n // Prevents memory leaks by releasing the scheduler's reference to this builder\n private unsubscribeFromSchedulerClears?: () => void\n\n private graphCache: D2 | undefined\n private inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined\n private pipelineCache: ResultStream | undefined\n public sourceWhereClausesCache:\n | Map<string, BasicExpression<boolean>>\n | undefined\n\n // Map of source alias to subscription\n readonly subscriptions: Record<string, CollectionSubscription> = {}\n // Map of source aliases to functions that load keys for that lazy source\n lazySourcesCallbacks: Record<string, LazyCollectionCallbacks> = {}\n // Set of source aliases that are lazy (don't load initial state)\n readonly lazySources = new Set<string>()\n // Set of collection IDs that include an optimizable ORDER BY clause\n optimizableOrderByCollections: Record<string, OrderByOptimizationInfo> = {}\n\n constructor(\n private readonly config: LiveQueryCollectionConfig<TContext, TResult>,\n ) {\n // Generate a unique ID if not provided\n this.id = config.id || `live-query-${++liveQueryCollectionCounter}`\n\n this.query = buildQueryFromConfig(config)\n this.collections = extractCollectionsFromQuery(this.query)\n const collectionAliasesById = extractCollectionAliases(this.query)\n\n // Build a reverse lookup map from alias to collection instance.\n // This enables self-join support where the same collection can be referenced\n // multiple times with different aliases (e.g., { employee: col, manager: col })\n this.collectionByAlias = {}\n for (const [collectionId, aliases] of collectionAliasesById.entries()) {\n const collection = this.collections[collectionId]\n if (!collection) continue\n for (const alias of aliases) {\n this.collectionByAlias[alias] = collection\n }\n }\n\n // Create compare function for ordering if the query has orderBy\n if (this.query.orderBy && this.query.orderBy.length > 0) {\n this.compare = createOrderByComparator<TResult>(this.orderByIndices)\n }\n\n // Use explicitly provided compareOptions if available, otherwise inherit from FROM collection\n this.compareOptions =\n this.config.defaultStringCollation ??\n extractCollectionFromSource(this.query).compareOptions\n\n // Compile the base pipeline once initially\n // This is done to ensure that any errors are thrown immediately and synchronously\n this.compileBasePipeline()\n }\n\n /**\n * Recursively checks if a query or any of its subqueries contains joins\n */\n private hasJoins(query: QueryIR): boolean {\n // Check if this query has joins\n if (query.join && query.join.length > 0) {\n return true\n }\n\n // Recursively check subqueries in the from clause\n if (query.from.type === `queryRef`) {\n if (this.hasJoins(query.from.query)) {\n return true\n }\n }\n\n return false\n }\n\n getConfig(): CollectionConfigSingleRowOption<TResult> & {\n utils: LiveQueryCollectionUtils\n } {\n return {\n id: this.id,\n getKey:\n this.config.getKey ||\n ((item) => this.resultKeys.get(item) as string | number),\n sync: this.getSyncConfig(),\n compare: this.compare,\n defaultStringCollation: this.compareOptions,\n gcTime: this.config.gcTime || 5000, // 5 seconds by default for live queries\n schema: this.config.schema,\n onInsert: this.config.onInsert,\n onUpdate: this.config.onUpdate,\n onDelete: this.config.onDelete,\n startSync: this.config.startSync,\n singleResult: this.query.singleResult,\n utils: {\n getRunCount: this.getRunCount.bind(this),\n setWindow: this.setWindow.bind(this),\n getWindow: this.getWindow.bind(this),\n [LIVE_QUERY_INTERNAL]: {\n getBuilder: () => this,\n hasCustomGetKey: !!this.config.getKey,\n hasJoins: this.hasJoins(this.query),\n },\n },\n }\n }\n\n setWindow(options: WindowOptions): true | Promise<void> {\n if (!this.windowFn) {\n throw new SetWindowRequiresOrderByError()\n }\n\n this.currentWindow = options\n this.windowFn(options)\n this.maybeRunGraphFn?.()\n\n // Check if loading a subset was triggered\n if (this.liveQueryCollection?.isLoadingSubset) {\n // Loading was triggered, return a promise that resolves when it completes\n return new Promise<void>((resolve) => {\n const unsubscribe = this.liveQueryCollection!.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n unsubscribe()\n resolve()\n }\n },\n )\n })\n }\n\n // No loading was triggered\n return true\n }\n\n getWindow(): { offset: number; limit: number } | undefined {\n // Only return window if this is a windowed query (has orderBy and windowFn)\n if (!this.windowFn || !this.currentWindow) {\n return undefined\n }\n return {\n offset: this.currentWindow.offset ?? 0,\n limit: this.currentWindow.limit ?? 0,\n }\n }\n\n /**\n * Resolves a collection alias to its collection ID.\n *\n * Uses a two-tier lookup strategy:\n * 1. First checks compiled aliases (includes subquery inner aliases)\n * 2. Falls back to declared aliases from the query's from/join clauses\n *\n * @param alias - The alias to resolve (e.g., \"employee\", \"manager\")\n * @returns The collection ID that the alias references\n * @throws {Error} If the alias is not found in either lookup\n */\n getCollectionIdForAlias(alias: string): string {\n const compiled = this.compiledAliasToCollectionId[alias]\n if (compiled) {\n return compiled\n }\n const collection = this.collectionByAlias[alias]\n if (collection) {\n return collection.id\n }\n throw new Error(`Unknown source alias \"${alias}\"`)\n }\n\n isLazyAlias(alias: string): boolean {\n return this.lazySources.has(alias)\n }\n\n // The callback function is called after the graph has run.\n // This gives the callback a chance to load more data if needed,\n // that's used to optimize orderBy operators that set a limit,\n // in order to load some more data if we still don't have enough rows after the pipeline has run.\n // That can happen because even though we load N rows, the pipeline might filter some of these rows out\n // causing the orderBy operator to receive less than N rows or even no rows at all.\n // So this callback would notice that it doesn't have enough rows and load some more.\n // The callback returns a boolean, when it's true it's done loading data and we can mark the collection as ready.\n maybeRunGraph(callback?: () => boolean) {\n if (this.isGraphRunning) {\n // no nested runs of the graph\n // which is possible if the `callback`\n // would call `maybeRunGraph` e.g. after it has loaded some more data\n return\n }\n\n // Should only be called when sync is active\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `maybeRunGraph called without active sync session. This should not happen.`,\n )\n }\n\n this.isGraphRunning = true\n\n try {\n const { begin, commit } = this.currentSyncConfig\n const syncState = this.currentSyncState\n\n // Don't run if the live query is in an error state\n if (this.isInErrorState) {\n return\n }\n\n // Always run the graph if subscribed (eager execution)\n if (syncState.subscribedToAllCollections) {\n while (syncState.graph.pendingWork()) {\n syncState.graph.run()\n // Flush accumulated changes after each graph step to commit them as one transaction.\n // This ensures intermediate join states (like null on one side) don't cause\n // duplicate key errors when the full join result arrives in the same step.\n syncState.flushPendingChanges?.()\n callback?.()\n }\n\n // On the initial run, we may need to do an empty commit to ensure that\n // the collection is initialized\n if (syncState.messagesCount === 0) {\n begin()\n commit()\n }\n\n // After graph processing completes, check if we should mark ready.\n // This is the canonical place to transition to ready state because:\n // 1. All data has been processed through the graph\n // 2. All source collections have had a chance to send their initial data\n // This prevents marking ready before data is processed (fixes isReady=true with empty data)\n this.updateLiveQueryStatus(this.currentSyncConfig)\n }\n } finally {\n this.isGraphRunning = false\n }\n }\n\n /**\n * Schedules a graph run with the transaction-scoped scheduler.\n * Ensures each builder runs at most once per transaction, with automatic dependency tracking\n * to run parent queries before child queries. Outside a transaction, runs immediately.\n *\n * Multiple calls during a transaction are coalesced into a single execution.\n * Dependencies are auto-discovered from subscribed live queries, or can be overridden.\n * Load callbacks are combined when entries merge.\n *\n * Uses the current sync session's config and syncState from instance properties.\n *\n * @param callback - Optional callback to load more data if needed (returns true when done)\n * @param options - Optional scheduling configuration\n * @param options.contextId - Transaction ID to group work; defaults to active transaction\n * @param options.jobId - Unique identifier for this job; defaults to this builder instance\n * @param options.alias - Source alias that triggered this schedule; adds alias-specific dependencies\n * @param options.dependencies - Explicit dependency list; overrides auto-discovered dependencies\n */\n scheduleGraphRun(\n callback?: () => boolean,\n options?: {\n contextId?: SchedulerContextId\n jobId?: unknown\n alias?: string\n dependencies?: Array<CollectionConfigBuilder<any, any>>\n },\n ) {\n const contextId = options?.contextId ?? getActiveTransaction()?.id\n // Use the builder instance as the job ID for deduplication. This is memory-safe\n // because the scheduler's context Map is deleted after flushing (no long-term retention).\n const jobId = options?.jobId ?? this\n const dependentBuilders = (() => {\n if (options?.dependencies) {\n return options.dependencies\n }\n\n const deps = new Set(this.builderDependencies)\n if (options?.alias) {\n const aliasDeps = this.aliasDependencies[options.alias]\n if (aliasDeps) {\n for (const dep of aliasDeps) {\n deps.add(dep)\n }\n }\n }\n\n deps.delete(this)\n\n return Array.from(deps)\n })()\n\n // Ensure dependent builders are actually scheduled in this context so that\n // dependency edges always point to a real job (or a deduped no-op if already scheduled).\n if (contextId) {\n for (const dep of dependentBuilders) {\n if (typeof dep.scheduleGraphRun === `function`) {\n dep.scheduleGraphRun(undefined, { contextId })\n }\n }\n }\n\n // We intentionally scope deduplication to the builder instance. Each instance\n // owns caches and compiled pipelines, so sharing work across instances that\n // merely reuse the same string id would execute the wrong builder's graph.\n\n if (!this.currentSyncConfig || !this.currentSyncState) {\n throw new Error(\n `scheduleGraphRun called without active sync session. This should not happen.`,\n )\n }\n\n // Manage our own state - get or create pending callbacks for this context\n let pending = contextId ? this.pendingGraphRuns.get(contextId) : undefined\n if (!pending) {\n pending = {\n loadCallbacks: new Set(),\n }\n if (contextId) {\n this.pendingGraphRuns.set(contextId, pending)\n }\n }\n\n // Add callback if provided (this is what accumulates between schedules)\n if (callback) {\n pending.loadCallbacks.add(callback)\n }\n\n // Schedule execution (scheduler just orchestrates order, we manage state)\n // For immediate execution (no contextId), pass pending directly since it won't be in the map\n const pendingToPass = contextId ? undefined : pending\n transactionScopedScheduler.schedule({\n contextId,\n jobId,\n dependencies: dependentBuilders,\n run: () => this.executeGraphRun(contextId, pendingToPass),\n })\n }\n\n /**\n * Clears pending graph run state for a specific context.\n * Called when the scheduler clears a context (e.g., transaction rollback/abort).\n */\n clearPendingGraphRun(contextId: SchedulerContextId): void {\n this.pendingGraphRuns.delete(contextId)\n }\n\n /**\n * Returns true if this builder has a pending graph run for the given context.\n */\n hasPendingGraphRun(contextId: SchedulerContextId): boolean {\n return this.pendingGraphRuns.has(contextId)\n }\n\n /**\n * Executes a pending graph run. Called by the scheduler when dependencies are satisfied.\n * Clears the pending state BEFORE execution so that any re-schedules during the run\n * create fresh state and don't interfere with the current execution.\n * Uses instance sync state - if sync has ended, gracefully returns without executing.\n *\n * @param contextId - Optional context ID to look up pending state\n * @param pendingParam - For immediate execution (no context), pending state is passed directly\n */\n private executeGraphRun(\n contextId?: SchedulerContextId,\n pendingParam?: PendingGraphRun,\n ): void {\n // Get pending state: either from parameter (no context) or from map (with context)\n // Remove from map BEFORE checking sync state to prevent leaking entries when sync ends\n // before the transaction flushes (e.g., unsubscribe during in-flight transaction)\n const pending =\n pendingParam ??\n (contextId ? this.pendingGraphRuns.get(contextId) : undefined)\n if (contextId) {\n this.pendingGraphRuns.delete(contextId)\n }\n\n // If no pending state, nothing to execute (context was cleared)\n if (!pending) {\n return\n }\n\n // If sync session has ended, don't execute (graph is finalized, subscriptions cleared)\n if (!this.currentSyncConfig || !this.currentSyncState) {\n return\n }\n\n this.incrementRunCount()\n\n const combinedLoader = () => {\n let allDone = true\n let firstError: unknown\n pending.loadCallbacks.forEach((loader) => {\n try {\n allDone = loader() && allDone\n } catch (error) {\n allDone = false\n firstError ??= error\n }\n })\n if (firstError) {\n throw firstError\n }\n // Returning false signals that callers should schedule another pass.\n return allDone\n }\n\n this.maybeRunGraph(combinedLoader)\n }\n\n private getSyncConfig(): SyncConfig<TResult> {\n return {\n rowUpdateMode: `full`,\n sync: this.syncFn.bind(this),\n }\n }\n\n incrementRunCount() {\n this.runCount++\n }\n\n getRunCount() {\n return this.runCount\n }\n\n private syncFn(config: SyncMethods<TResult>) {\n // Store reference to the live query collection for error state transitions\n this.liveQueryCollection = config.collection\n // Store config and syncState as instance properties for the duration of this sync session\n this.currentSyncConfig = config\n\n const syncState: SyncState = {\n messagesCount: 0,\n subscribedToAllCollections: false,\n unsubscribeCallbacks: new Set<() => void>(),\n }\n\n // Extend the pipeline such that it applies the incoming changes to the collection\n const fullSyncState = this.extendPipelineWithChangeProcessing(\n config,\n syncState,\n )\n this.currentSyncState = fullSyncState\n\n // Listen for scheduler context clears to clean up our pending state\n // Re-register on each sync start so the listener is active for the sync session's lifetime\n this.unsubscribeFromSchedulerClears = transactionScopedScheduler.onClear(\n (contextId) => {\n this.clearPendingGraphRun(contextId)\n },\n )\n\n // Listen for loadingSubset changes on the live query collection BEFORE subscribing.\n // This ensures we don't miss the event if subset loading completes synchronously.\n // When isLoadingSubset becomes false, we may need to mark the collection as ready\n // (if all source collections are already ready but we were waiting for subset load to complete)\n const loadingSubsetUnsubscribe = config.collection.on(\n `loadingSubset:change`,\n (event) => {\n if (!event.isLoadingSubset) {\n // Subset loading finished, check if we can now mark ready\n this.updateLiveQueryStatus(config)\n }\n },\n )\n syncState.unsubscribeCallbacks.add(loadingSubsetUnsubscribe)\n\n const loadSubsetDataCallbacks = this.subscribeToAllCollections(\n config,\n fullSyncState,\n )\n\n this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Initial run with callback to load more data if needed\n this.scheduleGraphRun(loadSubsetDataCallbacks)\n\n // Return the unsubscribe function\n return () => {\n syncState.unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())\n\n // Clear current sync session state\n this.currentSyncConfig = undefined\n this.currentSyncState = undefined\n\n // Clear all pending graph runs to prevent memory leaks from in-flight transactions\n // that may flush after the sync session ends\n this.pendingGraphRuns.clear()\n\n // Reset caches so a fresh graph/pipeline is compiled on next start\n // This avoids reusing a finalized D2 graph across GC restarts\n this.graphCache = undefined\n this.inputsCache = undefined\n this.pipelineCache = undefined\n this.sourceWhereClausesCache = undefined\n\n // Reset lazy source alias state\n this.lazySources.clear()\n this.optimizableOrderByCollections = {}\n this.lazySourcesCallbacks = {}\n\n // Clear subscription references to prevent memory leaks\n // Note: Individual subscriptions are already unsubscribed via unsubscribeCallbacks\n Object.keys(this.subscriptions).forEach(\n (key) => delete this.subscriptions[key],\n )\n this.compiledAliasToCollectionId = {}\n\n // Unregister from scheduler's onClear listener to prevent memory leaks\n // The scheduler's listener Set would otherwise keep a strong reference to this builder\n this.unsubscribeFromSchedulerClears?.()\n this.unsubscribeFromSchedulerClears = undefined\n }\n }\n\n /**\n * Compiles the query pipeline with all declared aliases.\n */\n private compileBasePipeline() {\n this.graphCache = new D2()\n this.inputsCache = Object.fromEntries(\n Object.keys(this.collectionByAlias).map((alias) => [\n alias,\n this.graphCache!.newInput<any>(),\n ]),\n )\n\n const compilation = compileQuery(\n this.query,\n this.inputsCache as Record<string, KeyedStream>,\n this.collections,\n this.subscriptions,\n this.lazySourcesCallbacks,\n this.lazySources,\n this.optimizableOrderByCollections,\n (windowFn: (options: WindowOptions) => void) => {\n this.windowFn = windowFn\n },\n )\n\n this.pipelineCache = compilation.pipeline\n this.sourceWhereClausesCache = compilation.sourceWhereClauses\n this.compiledAliasToCollectionId = compilation.aliasToCollectionId\n\n // Defensive check: verify all compiled aliases have corresponding inputs\n // This should never happen since all aliases come from user declarations,\n // but catch it early if the assumption is violated in the future.\n const missingAliases = Object.keys(this.compiledAliasToCollectionId).filter(\n (alias) => !Object.hasOwn(this.inputsCache!, alias),\n )\n if (missingAliases.length > 0) {\n throw new MissingAliasInputsError(missingAliases)\n }\n }\n\n private maybeCompileBasePipeline() {\n if (!this.graphCache || !this.inputsCache || !this.pipelineCache) {\n this.compileBasePipeline()\n }\n return {\n graph: this.graphCache!,\n inputs: this.inputsCache!,\n pipeline: this.pipelineCache!,\n }\n }\n\n private extendPipelineWithChangeProcessing(\n config: SyncMethods<TResult>,\n syncState: SyncState,\n ): FullSyncState {\n const { begin, commit } = config\n const { graph, inputs, pipeline } = this.maybeCompileBasePipeline()\n\n // Accumulator for changes across all output callbacks within a single graph run.\n // This allows us to batch all changes from intermediate join states into a single\n // transaction, avoiding duplicate key errors when joins produce multiple outputs\n // for the same key (e.g., first output with null, then output with joined data).\n let pendingChanges: Map<unknown, Changes<TResult>> = new Map()\n\n pipeline.pipe(\n output((data) => {\n const messages = data.getInner()\n syncState.messagesCount += messages.length\n\n // Accumulate changes from this output callback into the pending changes map.\n // Changes for the same key are merged (inserts/deletes are added together).\n messages.reduce(accumulateChanges<TResult>, pendingChanges)\n }),\n )\n\n // Flush pending changes and reset the accumulator.\n // Called at the end of each graph run to commit all accumulated changes.\n syncState.flushPendingChanges = () => {\n if (pendingChanges.size === 0) {\n return\n }\n begin()\n pendingChanges.forEach(this.applyChanges.bind(this, config))\n commit()\n pendingChanges = new Map()\n }\n\n graph.finalize()\n\n // Extend the sync state with the graph, inputs, and pipeline\n syncState.graph = graph\n syncState.inputs = inputs\n syncState.pipeline = pipeline\n\n return syncState as FullSyncState\n }\n\n private applyChanges(\n config: SyncMethods<TResult>,\n changes: {\n deletes: number\n inserts: number\n value: TResult\n orderByIndex: string | undefined\n },\n key: unknown,\n ) {\n const { write, collection } = config\n const { deletes, inserts, value, orderByIndex } = changes\n\n // Store the key of the result so that we can retrieve it in the\n // getKey function\n this.resultKeys.set(value, key)\n\n // Store the orderBy index if it exists\n if (orderByIndex !== undefined) {\n this.orderByIndices.set(value, orderByIndex)\n }\n\n // Simple singular insert.\n if (inserts && deletes === 0) {\n write({\n value,\n type: `insert`,\n })\n } else if (\n // Insert & update(s) (updates are a delete & insert)\n inserts > deletes ||\n // Just update(s) but the item is already in the collection (so\n // was inserted previously).\n (inserts === deletes && collection.has(collection.getKeyFromItem(value)))\n ) {\n write({\n value,\n type: `update`,\n })\n // Only delete is left as an option\n } else if (deletes > 0) {\n write({\n value,\n type: `delete`,\n })\n } else {\n throw new Error(\n `Could not apply changes: ${JSON.stringify(changes)}. This should never happen.`,\n )\n }\n }\n\n /**\n * Handle status changes from source collections\n */\n private handleSourceStatusChange(\n config: SyncMethods<TResult>,\n collectionId: string,\n event: AllCollectionEvents[`status:change`],\n ) {\n const { status } = event\n\n // Handle error state - any source collection in error puts live query in error\n if (status === `error`) {\n this.transitionToError(\n `Source collection '${collectionId}' entered error state`,\n )\n return\n }\n\n // Handle manual cleanup - this should not happen due to GC prevention,\n // but could happen if user manually calls cleanup()\n if (status === `cleaned-up`) {\n this.transitionToError(\n `Source collection '${collectionId}' was manually cleaned up while live query '${this.id}' depends on it. ` +\n `Live queries prevent automatic GC, so this was likely a manual cleanup() call.`,\n )\n return\n }\n\n // Update ready status based on all source collections\n this.updateLiveQueryStatus(config)\n }\n\n /**\n * Update the live query status based on source collection statuses\n */\n private updateLiveQueryStatus(config: SyncMethods<TResult>) {\n const { markReady } = config\n\n // Don't update status if already in error\n if (this.isInErrorState) {\n return\n }\n\n // Mark ready when:\n // 1. All subscriptions are set up (subscribedToAllCollections)\n // 2. All source collections are ready\n // 3. The live query collection is not loading subset data\n // This prevents marking the live query ready before its data is processed\n // (fixes issue where useLiveQuery returns isReady=true with empty data)\n if (\n this.currentSyncState?.subscribedToAllCollections &&\n this.allCollectionsReady() &&\n !this.liveQueryCollection?.isLoadingSubset\n ) {\n markReady()\n }\n }\n\n /**\n * Transition the live query to error state\n */\n private transitionToError(message: string) {\n this.isInErrorState = true\n\n // Log error to console for debugging\n console.error(`[Live Query Error] ${message}`)\n\n // Transition live query collection to error state\n this.liveQueryCollection?._lifecycle.setStatus(`error`)\n }\n\n private allCollectionsReady() {\n return Object.values(this.collections).every((collection) =>\n collection.isReady(),\n )\n }\n\n /**\n * Creates per-alias subscriptions enabling self-join support.\n * Each alias gets its own subscription with independent filters, even for the same collection.\n * Example: `{ employee: col, manager: col }` creates two separate subscriptions.\n */\n private subscribeToAllCollections(\n config: SyncMethods<TResult>,\n syncState: FullSyncState,\n ) {\n // Use compiled aliases as the source of truth - these include all aliases from the query\n // including those from subqueries, which may not be in collectionByAlias\n const compiledAliases = Object.entries(this.compiledAliasToCollectionId)\n if (compiledAliases.length === 0) {\n throw new Error(\n `Compiler returned no alias metadata for query '${this.id}'. This should not happen; please report.`,\n )\n }\n\n // Create a separate subscription for each alias, enabling self-joins where the same\n // collection can be used multiple times with different filters and subscriptions\n const loaders = compiledAliases.map(([alias, collectionId]) => {\n // Try collectionByAlias first (for declared aliases), fall back to collections (for subquery aliases)\n const collection =\n this.collectionByAlias[alias] ?? this.collections[collectionId]!\n\n const dependencyBuilder = getCollectionBuilder(collection)\n if (dependencyBuilder && dependencyBuilder !== this) {\n this.aliasDependencies[alias] = [dependencyBuilder]\n this.builderDependencies.add(dependencyBuilder)\n } else {\n this.aliasDependencies[alias] = []\n }\n\n // CollectionSubscriber handles the actual subscription to the source collection\n // and feeds data into the D2 graph inputs for this specific alias\n const collectionSubscriber = new CollectionSubscriber(\n alias,\n collectionId,\n collection,\n this,\n )\n\n // Subscribe to status changes for status flow\n const statusUnsubscribe = collection.on(`status:change`, (event) => {\n this.handleSourceStatusChange(config, collectionId, event)\n })\n syncState.unsubscribeCallbacks.add(statusUnsubscribe)\n\n const subscription = collectionSubscriber.subscribe()\n // Store subscription by alias (not collection ID) to support lazy loading\n // which needs to look up subscriptions by their query alias\n this.subscriptions[alias] = subscription\n\n // Create a callback for loading more data if needed (used by OrderBy optimization)\n const loadMore = collectionSubscriber.loadMoreIfNeeded.bind(\n collectionSubscriber,\n subscription,\n )\n\n return loadMore\n })\n\n // Combine all loaders into a single callback that initiates loading more data\n // from any source that needs it. Returns true once all loaders have been called,\n // but the actual async loading may still be in progress.\n const loadSubsetDataCallbacks = () => {\n loaders.map((loader) => loader())\n return true\n }\n\n // Mark as subscribed so the graph can start running\n // (graph only runs when all collections are subscribed)\n syncState.subscribedToAllCollections = true\n\n // Note: We intentionally don't call updateLiveQueryStatus() here.\n // The graph hasn't run yet, so marking ready would be premature.\n // The canonical place to mark ready is after the graph processes data\n // in maybeRunGraph(), which ensures data has been processed first.\n\n return loadSubsetDataCallbacks\n }\n}\n\nfunction buildQueryFromConfig<TContext extends Context>(\n config: LiveQueryCollectionConfig<any, any>,\n) {\n // Build the query using the provided query builder function or instance\n if (typeof config.query === `function`) {\n return buildQuery<TContext>(config.query)\n }\n return getQueryIR(config.query)\n}\n\nfunction createOrderByComparator<T extends object>(\n orderByIndices: WeakMap<object, string>,\n) {\n return (val1: T, val2: T): number => {\n // Use the orderBy index stored in the WeakMap\n const index1 = orderByIndices.get(val1)\n const index2 = orderByIndices.get(val2)\n\n // Compare fractional indices lexicographically\n if (index1 && index2) {\n if (index1 < index2) {\n return -1\n } else if (index1 > index2) {\n return 1\n } else {\n return 0\n }\n }\n\n // Fallback to no ordering if indices are missing\n return 0\n }\n}\n\n/**\n * Helper function to extract collections from a compiled query\n * Traverses the query IR to find all collection references\n * Maps collections by their ID (not alias) as expected by the compiler\n */\nfunction extractCollectionsFromQuery(\n query: any,\n): Record<string, Collection<any, any, any>> {\n const collections: Record<string, any> = {}\n\n // Helper function to recursively extract collections from a query or source\n function extractFromSource(source: any) {\n if (source.type === `collectionRef`) {\n collections[source.collection.id] = source.collection\n } else if (source.type === `queryRef`) {\n // Recursively extract from subquery\n extractFromQuery(source.query)\n }\n }\n\n // Helper function to recursively extract collections from a query\n function extractFromQuery(q: any) {\n // Extract from FROM clause\n if (q.from) {\n extractFromSource(q.from)\n }\n\n // Extract from JOIN clauses\n if (q.join && Array.isArray(q.join)) {\n for (const joinClause of q.join) {\n if (joinClause.from) {\n extractFromSource(joinClause.from)\n }\n }\n }\n }\n\n // Start extraction from the root query\n extractFromQuery(query)\n\n return collections\n}\n\n/**\n * Helper function to extract the collection that is referenced in the query's FROM clause.\n * The FROM clause may refer directly to a collection or indirectly to a subquery.\n */\nfunction extractCollectionFromSource(query: any): Collection<any, any, any> {\n const from = query.from\n\n if (from.type === `collectionRef`) {\n return from.collection\n } else if (from.type === `queryRef`) {\n // Recursively extract from subquery\n return extractCollectionFromSource(from.query)\n }\n\n throw new Error(\n `Failed to extract collection. Invalid FROM clause: ${JSON.stringify(query)}`,\n )\n}\n\n/**\n * Extracts all aliases used for each collection across the entire query tree.\n *\n * Traverses the QueryIR recursively to build a map from collection ID to all aliases\n * that reference that collection. This is essential for self-join support, where the\n * same collection may be referenced multiple times with different aliases.\n *\n * For example, given a query like:\n * ```ts\n * q.from({ employee: employeesCollection })\n * .join({ manager: employeesCollection }, ({ employee, manager }) =>\n * eq(employee.managerId, manager.id)\n * )\n * ```\n *\n * This function would return:\n * ```\n * Map { \"employees\" => Set { \"employee\", \"manager\" } }\n * ```\n *\n * @param query - The query IR to extract aliases from\n * @returns A map from collection ID to the set of all aliases referencing that collection\n */\nfunction extractCollectionAliases(query: QueryIR): Map<string, Set<string>> {\n const aliasesById = new Map<string, Set<string>>()\n\n function recordAlias(source: any) {\n if (!source) return\n\n if (source.type === `collectionRef`) {\n const { id } = source.collection\n const existing = aliasesById.get(id)\n if (existing) {\n existing.add(source.alias)\n } else {\n aliasesById.set(id, new Set([source.alias]))\n }\n } else if (source.type === `queryRef`) {\n traverse(source.query)\n }\n }\n\n function traverse(q?: QueryIR) {\n if (!q) return\n\n recordAlias(q.from)\n\n if (q.join) {\n for (const joinClause of q.join) {\n recordAlias(joinClause.from)\n }\n }\n }\n\n traverse(query)\n\n return aliasesById\n}\n\nfunction accumulateChanges<T>(\n acc: Map<unknown, Changes<T>>,\n [[key, tupleData], multiplicity]: [\n [unknown, [any, string | undefined]],\n number,\n ],\n) {\n // All queries now consistently return [value, orderByIndex] format\n // where orderByIndex is undefined for queries without ORDER BY\n const [value, orderByIndex] = tupleData as [T, string | undefined]\n\n const changes = acc.get(key) || {\n deletes: 0,\n inserts: 0,\n value,\n orderByIndex,\n }\n if (multiplicity < 0) {\n changes.deletes += Math.abs(multiplicity)\n } else if (multiplicity > 0) {\n changes.inserts += multiplicity\n // Update value to the latest version for this key\n changes.value = value\n if (orderByIndex !== undefined) {\n changes.orderByIndex = orderByIndex\n }\n }\n acc.set(key, changes)\n return acc\n}\n"],"names":[],"mappings":"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"}
@@ -1,6 +1,6 @@
1
1
  import { MultiSet } from "@tanstack/db-ivm";
2
2
  import { normalizeExpressionPaths, normalizeOrderByPaths } from "../compiler/expressions.js";
3
- const loadMoreCallbackSymbol = Symbol.for(
3
+ const loadMoreCallbackSymbol = /* @__PURE__ */ Symbol.for(
4
4
  `@tanstack/db.collection-config-builder`
5
5
  );
6
6
  class CollectionSubscriber {
@@ -1 +1 @@
1
- {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Track load promises using subscription from the event (avoids circular dependency)\n const trackLoadPromise = (subscription: CollectionSubscription) => {\n // Guard against duplicate transitions\n if (!this.subscriptionLoadingPromises.has(subscription)) {\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Create subscription with onStatusChange - listener is registered before snapshot\n // Note: For non-ordered queries (no limit/offset), we use trackLoadSubsetPromise: false\n // which is the default behavior in subscribeChanges\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changes)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request - onStatusChange listener is already registered\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n // This works for both single-column and multi-column orderBy:\n // - Single-column: index provides exact ordering\n // - Multi-column: index provides ordering on first column, secondary sort in memory\n subscription.setOrderByIndex(index)\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n // so the sync layer can optimize if the backend supports it\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n trackedChanges,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n const minValues =\n extractedValues !== undefined\n ? Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n : undefined\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n offset,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ) {\n for (const change of changes) {\n // Only track inserts/updates for cursor positioning, not deletes\n if (change.type !== `delete`) {\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n }\n\n yield change\n }\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":["subscription"],"mappings":"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"}
1
+ {"version":3,"file":"collection-subscriber.js","sources":["../../../../src/query/live/collection-subscriber.ts"],"sourcesContent":["import { MultiSet } from '@tanstack/db-ivm'\nimport {\n normalizeExpressionPaths,\n normalizeOrderByPaths,\n} from '../compiler/expressions.js'\nimport type { MultiSetArray, RootStreamBuilder } from '@tanstack/db-ivm'\nimport type { Collection } from '../../collection/index.js'\nimport type {\n ChangeMessage,\n SubscriptionStatusChangeEvent,\n} from '../../types.js'\nimport type { Context, GetResult } from '../builder/types.js'\nimport type { BasicExpression } from '../ir.js'\nimport type { OrderByOptimizationInfo } from '../compiler/order-by.js'\nimport type { CollectionConfigBuilder } from './collection-config-builder.js'\nimport type { CollectionSubscription } from '../../collection/subscription.js'\n\nconst loadMoreCallbackSymbol = Symbol.for(\n `@tanstack/db.collection-config-builder`,\n)\n\nexport class CollectionSubscriber<\n TContext extends Context,\n TResult extends object = GetResult<TContext>,\n> {\n // Keep track of the biggest value we've sent so far (needed for orderBy optimization)\n private biggest: any = undefined\n\n // Track deferred promises for subscription loading states\n private subscriptionLoadingPromises = new Map<\n CollectionSubscription,\n { resolve: () => void }\n >()\n\n // Track keys that have been sent to the D2 pipeline to prevent duplicate inserts\n // This is necessary because different code paths (initial load, change events)\n // can potentially send the same item to D2 multiple times.\n private sentToD2Keys = new Set<string | number>()\n\n constructor(\n private alias: string,\n private collectionId: string,\n private collection: Collection,\n private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>,\n ) {}\n\n subscribe(): CollectionSubscription {\n const whereClause = this.getWhereClauseForAlias()\n\n if (whereClause) {\n const whereExpression = normalizeExpressionPaths(whereClause, this.alias)\n return this.subscribeToChanges(whereExpression)\n }\n\n return this.subscribeToChanges()\n }\n\n private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {\n const orderByInfo = this.getOrderByInfo()\n\n // Track load promises using subscription from the event (avoids circular dependency)\n const trackLoadPromise = (subscription: CollectionSubscription) => {\n // Guard against duplicate transitions\n if (!this.subscriptionLoadingPromises.has(subscription)) {\n let resolve: () => void\n const promise = new Promise<void>((res) => {\n resolve = res\n })\n\n this.subscriptionLoadingPromises.set(subscription, {\n resolve: resolve!,\n })\n this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(\n promise,\n )\n }\n }\n\n // Status change handler - passed to subscribeChanges so it's registered\n // BEFORE any snapshot is requested, preventing race conditions\n const onStatusChange = (event: SubscriptionStatusChangeEvent) => {\n const subscription = event.subscription as CollectionSubscription\n if (event.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n } else {\n // status is 'ready'\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n }\n }\n\n // Create subscription with onStatusChange - listener is registered before any async work\n let subscription: CollectionSubscription\n if (orderByInfo) {\n subscription = this.subscribeToOrderedChanges(\n whereExpression,\n orderByInfo,\n onStatusChange,\n )\n } else {\n // If the source alias is lazy then we should not include the initial state\n const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(\n this.alias,\n )\n\n subscription = this.subscribeToMatchingChanges(\n whereExpression,\n includeInitialState,\n onStatusChange,\n )\n }\n\n // Check current status after subscribing - if status is 'loadingSubset', track it.\n // The onStatusChange listener will catch the transition to 'ready'.\n if (subscription.status === `loadingSubset`) {\n trackLoadPromise(subscription)\n }\n\n const unsubscribe = () => {\n // If subscription has a pending promise, resolve it before unsubscribing\n const deferred = this.subscriptionLoadingPromises.get(subscription)\n if (deferred) {\n // Clear the map entry FIRST (before resolving)\n this.subscriptionLoadingPromises.delete(subscription)\n deferred.resolve()\n }\n\n subscription.unsubscribe()\n }\n // currentSyncState is always defined when subscribe() is called\n // (called during sync session setup)\n this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(\n unsubscribe,\n )\n return subscription\n }\n\n private sendChangesToPipeline(\n changes: Iterable<ChangeMessage<any, string | number>>,\n callback?: () => boolean,\n ) {\n // Filter changes to prevent duplicate inserts to D2 pipeline.\n // This ensures D2 multiplicity stays at 1 for visible items, so deletes\n // properly reduce multiplicity to 0 (triggering DELETE output).\n const changesArray = Array.isArray(changes) ? changes : [...changes]\n const filteredChanges: Array<ChangeMessage<any, string | number>> = []\n for (const change of changesArray) {\n if (change.type === `insert`) {\n if (this.sentToD2Keys.has(change.key)) {\n // Skip duplicate insert - already sent to D2\n continue\n }\n this.sentToD2Keys.add(change.key)\n } else if (change.type === `delete`) {\n // Remove from tracking so future re-inserts are allowed\n this.sentToD2Keys.delete(change.key)\n }\n // Updates are handled as delete+insert by splitUpdates, so no special handling needed\n filteredChanges.push(change)\n }\n\n // currentSyncState and input are always defined when this method is called\n // (only called from active subscriptions during a sync session)\n const input =\n this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!\n const sentChanges = sendChangesToInput(\n input,\n filteredChanges,\n this.collection.config.getKey,\n )\n\n // Do not provide the callback that loads more data\n // if there's no more data to load\n // otherwise we end up in an infinite loop trying to load more data\n const dataLoader = sentChanges > 0 ? callback : undefined\n\n // We need to schedule a graph run even if there's no data to load\n // because we need to mark the collection as ready if it's not already\n // and that's only done in `scheduleGraphRun`\n this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {\n alias: this.alias,\n })\n }\n\n private subscribeToMatchingChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n includeInitialState: boolean,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const sendChanges = (\n changes: Array<ChangeMessage<any, string | number>>,\n ) => {\n this.sendChangesToPipeline(changes)\n }\n\n // Create subscription with onStatusChange - listener is registered before snapshot\n // Note: For non-ordered queries (no limit/offset), we use trackLoadSubsetPromise: false\n // which is the default behavior in subscribeChanges\n const subscription = this.collection.subscribeChanges(sendChanges, {\n ...(includeInitialState && { includeInitialState }),\n whereExpression,\n onStatusChange,\n })\n\n return subscription\n }\n\n private subscribeToOrderedChanges(\n whereExpression: BasicExpression<boolean> | undefined,\n orderByInfo: OrderByOptimizationInfo,\n onStatusChange: (event: SubscriptionStatusChangeEvent) => void,\n ): CollectionSubscription {\n const { orderBy, offset, limit, index } = orderByInfo\n\n // Use a holder to forward-reference subscription in the callback\n const subscriptionHolder: { current?: CollectionSubscription } = {}\n\n const sendChangesInRange = (\n changes: Iterable<ChangeMessage<any, string | number>>,\n ) => {\n // Split live updates into a delete of the old value and an insert of the new value\n const splittedChanges = splitUpdates(changes)\n this.sendChangesToPipelineWithTracking(\n splittedChanges,\n subscriptionHolder.current!,\n )\n }\n\n // Subscribe to changes with onStatusChange - listener is registered before any snapshot\n // values bigger than what we've sent don't need to be sent because they can't affect the topK\n const subscription = this.collection.subscribeChanges(sendChangesInRange, {\n whereExpression,\n onStatusChange,\n })\n subscriptionHolder.current = subscription\n\n // Listen for truncate events to reset cursor tracking state and sentToD2Keys\n // This ensures that after a must-refetch/truncate, we don't use stale cursor data\n // and allow re-inserts of previously sent keys\n const truncateUnsubscribe = this.collection.on(`truncate`, () => {\n this.biggest = undefined\n this.sentToD2Keys.clear()\n })\n\n // Clean up truncate listener when subscription is unsubscribed\n subscription.on(`unsubscribed`, () => {\n truncateUnsubscribe()\n })\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Trigger the snapshot request - onStatusChange listener is already registered\n if (index) {\n // We have an index on the first orderBy column - use lazy loading optimization\n // This works for both single-column and multi-column orderBy:\n // - Single-column: index provides exact ordering\n // - Multi-column: index provides ordering on first column, secondary sort in memory\n subscription.setOrderByIndex(index)\n\n // Load the first `offset + limit` values from the index\n // i.e. the K items from the collection that fall into the requested range: [offset, offset + limit[\n subscription.requestLimitedSnapshot({\n limit: offset + limit,\n orderBy: normalizedOrderBy,\n })\n } else {\n // No index available (e.g., non-ref expression): pass orderBy/limit to loadSubset\n // so the sync layer can optimize if the backend supports it\n subscription.requestSnapshot({\n orderBy: normalizedOrderBy,\n limit: offset + limit,\n })\n }\n\n return subscription\n }\n\n // This function is called by maybeRunGraph\n // after each iteration of the query pipeline\n // to ensure that the orderBy operator has enough data to work with\n loadMoreIfNeeded(subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n\n if (!orderByInfo) {\n // This query has no orderBy operator\n // so there's no data to load\n return true\n }\n\n const { dataNeeded } = orderByInfo\n\n if (!dataNeeded) {\n // dataNeeded is not set when there's no index (e.g., non-ref expression).\n // In this case, we've already loaded all data via requestSnapshot\n // and don't need to lazily load more.\n return true\n }\n\n // `dataNeeded` probes the orderBy operator to see if it needs more data\n // if it needs more data, it returns the number of items it needs\n const n = dataNeeded()\n if (n > 0) {\n this.loadNextItems(n, subscription)\n }\n return true\n }\n\n private sendChangesToPipelineWithTracking(\n changes: Iterable<ChangeMessage<any, string | number>>,\n subscription: CollectionSubscription,\n ) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n this.sendChangesToPipeline(changes)\n return\n }\n\n const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)\n\n // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.\n // This ensures we pass the same function instance to the scheduler each time,\n // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.\n type SubscriptionWithLoader = CollectionSubscription & {\n [loadMoreCallbackSymbol]?: () => boolean\n }\n\n const subscriptionWithLoader = subscription as SubscriptionWithLoader\n\n subscriptionWithLoader[loadMoreCallbackSymbol] ??=\n this.loadMoreIfNeeded.bind(this, subscription)\n\n this.sendChangesToPipeline(\n trackedChanges,\n subscriptionWithLoader[loadMoreCallbackSymbol],\n )\n }\n\n // Loads the next `n` items from the collection\n // starting from the biggest item it has sent\n private loadNextItems(n: number, subscription: CollectionSubscription) {\n const orderByInfo = this.getOrderByInfo()\n if (!orderByInfo) {\n return\n }\n const { orderBy, valueExtractorForRawRow, offset } = orderByInfo\n const biggestSentRow = this.biggest\n\n // Extract all orderBy column values from the biggest sent row\n // For single-column: returns single value, for multi-column: returns array\n const extractedValues = biggestSentRow\n ? valueExtractorForRawRow(biggestSentRow)\n : undefined\n\n // Normalize to array format for minValues\n const minValues =\n extractedValues !== undefined\n ? Array.isArray(extractedValues)\n ? extractedValues\n : [extractedValues]\n : undefined\n\n // Normalize the orderBy clauses such that the references are relative to the collection\n const normalizedOrderBy = normalizeOrderByPaths(orderBy, this.alias)\n\n // Take the `n` items after the biggest sent value\n // Pass the current window offset to ensure proper deduplication\n subscription.requestLimitedSnapshot({\n orderBy: normalizedOrderBy,\n limit: n,\n minValues,\n offset,\n })\n }\n\n private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {\n const sourceWhereClausesCache =\n this.collectionConfigBuilder.sourceWhereClausesCache\n if (!sourceWhereClausesCache) {\n return undefined\n }\n return sourceWhereClausesCache.get(this.alias)\n }\n\n private getOrderByInfo(): OrderByOptimizationInfo | undefined {\n const info =\n this.collectionConfigBuilder.optimizableOrderByCollections[\n this.collectionId\n ]\n if (info && info.alias === this.alias) {\n return info\n }\n return undefined\n }\n\n private *trackSentValues(\n changes: Iterable<ChangeMessage<any, string | number>>,\n comparator: (a: any, b: any) => number,\n ) {\n for (const change of changes) {\n // Only track inserts/updates for cursor positioning, not deletes\n if (change.type !== `delete`) {\n if (!this.biggest) {\n this.biggest = change.value\n } else if (comparator(this.biggest, change.value) < 0) {\n this.biggest = change.value\n }\n }\n\n yield change\n }\n }\n}\n\n/**\n * Helper function to send changes to a D2 input stream\n */\nfunction sendChangesToInput(\n input: RootStreamBuilder<unknown>,\n changes: Iterable<ChangeMessage>,\n getKey: (item: ChangeMessage[`value`]) => any,\n): number {\n const multiSetArray: MultiSetArray<unknown> = []\n for (const change of changes) {\n const key = getKey(change.value)\n if (change.type === `insert`) {\n multiSetArray.push([[key, change.value], 1])\n } else if (change.type === `update`) {\n multiSetArray.push([[key, change.previousValue], -1])\n multiSetArray.push([[key, change.value], 1])\n } else {\n // change.type === `delete`\n multiSetArray.push([[key, change.value], -1])\n }\n }\n\n if (multiSetArray.length !== 0) {\n input.sendData(new MultiSet(multiSetArray))\n }\n\n return multiSetArray.length\n}\n\n/** Splits updates into a delete of the old value and an insert of the new value */\nfunction* splitUpdates<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(\n changes: Iterable<ChangeMessage<T, TKey>>,\n): Generator<ChangeMessage<T, TKey>> {\n for (const change of changes) {\n if (change.type === `update`) {\n yield { type: `delete`, key: change.key, value: change.previousValue! }\n yield { type: `insert`, key: change.key, value: change.value }\n } else {\n yield change\n }\n }\n}\n"],"names":["subscription"],"mappings":";;AAiBA,MAAM,yBAAyB,uBAAO;AAAA,EACpC;AACF;AAEO,MAAM,qBAGX;AAAA,EAeA,YACU,OACA,cACA,YACA,yBACR;AAJQ,SAAA,QAAA;AACA,SAAA,eAAA;AACA,SAAA,aAAA;AACA,SAAA,0BAAA;AAjBV,SAAQ,UAAe;AAGvB,SAAQ,kDAAkC,IAAA;AAQ1C,SAAQ,mCAAmB,IAAA;AAAA,EAOxB;AAAA,EAEH,YAAoC;AAClC,UAAM,cAAc,KAAK,uBAAA;AAEzB,QAAI,aAAa;AACf,YAAM,kBAAkB,yBAAyB,aAAa,KAAK,KAAK;AACxE,aAAO,KAAK,mBAAmB,eAAe;AAAA,IAChD;AAEA,WAAO,KAAK,mBAAA;AAAA,EACd;AAAA,EAEQ,mBAAmB,iBAA4C;AACrE,UAAM,cAAc,KAAK,eAAA;AAGzB,UAAM,mBAAmB,CAACA,kBAAyC;AAEjE,UAAI,CAAC,KAAK,4BAA4B,IAAIA,aAAY,GAAG;AACvD,YAAI;AACJ,cAAM,UAAU,IAAI,QAAc,CAAC,QAAQ;AACzC,oBAAU;AAAA,QACZ,CAAC;AAED,aAAK,4BAA4B,IAAIA,eAAc;AAAA,UACjD;AAAA,QAAA,CACD;AACD,aAAK,wBAAwB,oBAAqB,MAAM;AAAA,UACtD;AAAA,QAAA;AAAA,MAEJ;AAAA,IACF;AAIA,UAAM,iBAAiB,CAAC,UAAyC;AAC/D,YAAMA,gBAAe,MAAM;AAC3B,UAAI,MAAM,WAAW,iBAAiB;AACpC,yBAAiBA,aAAY;AAAA,MAC/B,OAAO;AAEL,cAAM,WAAW,KAAK,4BAA4B,IAAIA,aAAY;AAClE,YAAI,UAAU;AAEZ,eAAK,4BAA4B,OAAOA,aAAY;AACpD,mBAAS,QAAA;AAAA,QACX;AAAA,MACF;AAAA,IACF;AAGA,QAAI;AACJ,QAAI,aAAa;AACf,qBAAe,KAAK;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAEJ,OAAO;AAEL,YAAM,sBAAsB,CAAC,KAAK,wBAAwB;AAAA,QACxD,KAAK;AAAA,MAAA;AAGP,qBAAe,KAAK;AAAA,QAClB;AAAA,QACA;AAAA,QACA;AAAA,MAAA;AAAA,IAEJ;AAIA,QAAI,aAAa,WAAW,iBAAiB;AAC3C,uBAAiB,YAAY;AAAA,IAC/B;AAEA,UAAM,cAAc,MAAM;AAExB,YAAM,WAAW,KAAK,4BAA4B,IAAI,YAAY;AAClE,UAAI,UAAU;AAEZ,aAAK,4BAA4B,OAAO,YAAY;AACpD,iBAAS,QAAA;AAAA,MACX;AAEA,mBAAa,YAAA;AAAA,IACf;AAGA,SAAK,wBAAwB,iBAAkB,qBAAqB;AAAA,MAClE;AAAA,IAAA;AAEF,WAAO;AAAA,EACT;AAAA,EAEQ,sBACN,SACA,UACA;AAIA,UAAM,eAAe,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,GAAG,OAAO;AACnE,UAAM,kBAA8D,CAAA;AACpE,eAAW,UAAU,cAAc;AACjC,UAAI,OAAO,SAAS,UAAU;AAC5B,YAAI,KAAK,aAAa,IAAI,OAAO,GAAG,GAAG;AAErC;AAAA,QACF;AACA,aAAK,aAAa,IAAI,OAAO,GAAG;AAAA,MAClC,WAAW,OAAO,SAAS,UAAU;AAEnC,aAAK,aAAa,OAAO,OAAO,GAAG;AAAA,MACrC;AAEA,sBAAgB,KAAK,MAAM;AAAA,IAC7B;AAIA,UAAM,QACJ,KAAK,wBAAwB,iBAAkB,OAAO,KAAK,KAAK;AAClE,UAAM,cAAc;AAAA,MAClB;AAAA,MACA;AAAA,MACA,KAAK,WAAW,OAAO;AAAA,IAAA;AAMzB,UAAM,aAAa,cAAc,IAAI,WAAW;AAKhD,SAAK,wBAAwB,iBAAiB,YAAY;AAAA,MACxD,OAAO,KAAK;AAAA,IAAA,CACb;AAAA,EACH;AAAA,EAEQ,2BACN,iBACA,qBACA,gBACwB;AACxB,UAAM,cAAc,CAClB,YACG;AACH,WAAK,sBAAsB,OAAO;AAAA,IACpC;AAKA,UAAM,eAAe,KAAK,WAAW,iBAAiB,aAAa;AAAA,MACjE,GAAI,uBAAuB,EAAE,oBAAA;AAAA,MAC7B;AAAA,MACA;AAAA,IAAA,CACD;AAED,WAAO;AAAA,EACT;AAAA,EAEQ,0BACN,iBACA,aACA,gBACwB;AACxB,UAAM,EAAE,SAAS,QAAQ,OAAO,UAAU;AAG1C,UAAM,qBAA2D,CAAA;AAEjE,UAAM,qBAAqB,CACzB,YACG;AAEH,YAAM,kBAAkB,aAAa,OAAO;AAC5C,WAAK;AAAA,QACH;AAAA,QACA,mBAAmB;AAAA,MAAA;AAAA,IAEvB;AAIA,UAAM,eAAe,KAAK,WAAW,iBAAiB,oBAAoB;AAAA,MACxE;AAAA,MACA;AAAA,IAAA,CACD;AACD,uBAAmB,UAAU;AAK7B,UAAM,sBAAsB,KAAK,WAAW,GAAG,YAAY,MAAM;AAC/D,WAAK,UAAU;AACf,WAAK,aAAa,MAAA;AAAA,IACpB,CAAC;AAGD,iBAAa,GAAG,gBAAgB,MAAM;AACpC,0BAAA;AAAA,IACF,CAAC;AAGD,UAAM,oBAAoB,sBAAsB,SAAS,KAAK,KAAK;AAGnE,QAAI,OAAO;AAKT,mBAAa,gBAAgB,KAAK;AAIlC,mBAAa,uBAAuB;AAAA,QAClC,OAAO,SAAS;AAAA,QAChB,SAAS;AAAA,MAAA,CACV;AAAA,IACH,OAAO;AAGL,mBAAa,gBAAgB;AAAA,QAC3B,SAAS;AAAA,QACT,OAAO,SAAS;AAAA,MAAA,CACjB;AAAA,IACH;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKA,iBAAiB,cAAsC;AACrD,UAAM,cAAc,KAAK,eAAA;AAEzB,QAAI,CAAC,aAAa;AAGhB,aAAO;AAAA,IACT;AAEA,UAAM,EAAE,eAAe;AAEvB,QAAI,CAAC,YAAY;AAIf,aAAO;AAAA,IACT;AAIA,UAAM,IAAI,WAAA;AACV,QAAI,IAAI,GAAG;AACT,WAAK,cAAc,GAAG,YAAY;AAAA,IACpC;AACA,WAAO;AAAA,EACT;AAAA,EAEQ,kCACN,SACA,cACA;AACA,UAAM,cAAc,KAAK,eAAA;AACzB,QAAI,CAAC,aAAa;AAChB,WAAK,sBAAsB,OAAO;AAClC;AAAA,IACF;AAEA,UAAM,iBAAiB,KAAK,gBAAgB,SAAS,YAAY,UAAU;AAS3E,UAAM,yBAAyB;AAE/B,2BAAuB,sBAAsB,MAC3C,KAAK,iBAAiB,KAAK,MAAM,YAAY;AAE/C,SAAK;AAAA,MACH;AAAA,MACA,uBAAuB,sBAAsB;AAAA,IAAA;AAAA,EAEjD;AAAA;AAAA;AAAA,EAIQ,cAAc,GAAW,cAAsC;AACrE,UAAM,cAAc,KAAK,eAAA;AACzB,QAAI,CAAC,aAAa;AAChB;AAAA,IACF;AACA,UAAM,EAAE,SAAS,yBAAyB,OAAA,IAAW;AACrD,UAAM,iBAAiB,KAAK;AAI5B,UAAM,kBAAkB,iBACpB,wBAAwB,cAAc,IACtC;AAGJ,UAAM,YACJ,oBAAoB,SAChB,MAAM,QAAQ,eAAe,IAC3B,kBACA,CAAC,eAAe,IAClB;AAGN,UAAM,oBAAoB,sBAAsB,SAAS,KAAK,KAAK;AAInE,iBAAa,uBAAuB;AAAA,MAClC,SAAS;AAAA,MACT,OAAO;AAAA,MACP;AAAA,MACA;AAAA,IAAA,CACD;AAAA,EACH;AAAA,EAEQ,yBAA+D;AACrE,UAAM,0BACJ,KAAK,wBAAwB;AAC/B,QAAI,CAAC,yBAAyB;AAC5B,aAAO;AAAA,IACT;AACA,WAAO,wBAAwB,IAAI,KAAK,KAAK;AAAA,EAC/C;AAAA,EAEQ,iBAAsD;AAC5D,UAAM,OACJ,KAAK,wBAAwB,8BAC3B,KAAK,YACP;AACF,QAAI,QAAQ,KAAK,UAAU,KAAK,OAAO;AACrC,aAAO;AAAA,IACT;AACA,WAAO;AAAA,EACT;AAAA,EAEA,CAAS,gBACP,SACA,YACA;AACA,eAAW,UAAU,SAAS;AAE5B,UAAI,OAAO,SAAS,UAAU;AAC5B,YAAI,CAAC,KAAK,SAAS;AACjB,eAAK,UAAU,OAAO;AAAA,QACxB,WAAW,WAAW,KAAK,SAAS,OAAO,KAAK,IAAI,GAAG;AACrD,eAAK,UAAU,OAAO;AAAA,QACxB;AAAA,MACF;AAEA,YAAM;AAAA,IACR;AAAA,EACF;AACF;AAKA,SAAS,mBACP,OACA,SACA,QACQ;AACR,QAAM,gBAAwC,CAAA;AAC9C,aAAW,UAAU,SAAS;AAC5B,UAAM,MAAM,OAAO,OAAO,KAAK;AAC/B,QAAI,OAAO,SAAS,UAAU;AAC5B,oBAAc,KAAK,CAAC,CAAC,KAAK,OAAO,KAAK,GAAG,CAAC,CAAC;AAAA,IAC7C,WAAW,OAAO,SAAS,UAAU;AACnC,oBAAc,KAAK,CAAC,CAAC,KAAK,OAAO,aAAa,GAAG,EAAE,CAAC;AACpD,oBAAc,KAAK,CAAC,CAAC,KAAK,OAAO,KAAK,GAAG,CAAC,CAAC;AAAA,IAC7C,OAAO;AAEL,oBAAc,KAAK,CAAC,CAAC,KAAK,OAAO,KAAK,GAAG,EAAE,CAAC;AAAA,IAC9C;AAAA,EACF;AAEA,MAAI,cAAc,WAAW,GAAG;AAC9B,UAAM,SAAS,IAAI,SAAS,aAAa,CAAC;AAAA,EAC5C;AAEA,SAAO,cAAc;AACvB;AAGA,UAAU,aAIR,SACmC;AACnC,aAAW,UAAU,SAAS;AAC5B,QAAI,OAAO,SAAS,UAAU;AAC5B,YAAM,EAAE,MAAM,UAAU,KAAK,OAAO,KAAK,OAAO,OAAO,cAAA;AACvD,YAAM,EAAE,MAAM,UAAU,KAAK,OAAO,KAAK,OAAO,OAAO,MAAA;AAAA,IACzD,OAAO;AACL,YAAM;AAAA,IACR;AAAA,EACF;AACF;"}
@@ -1,4 +1,4 @@
1
- const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`);
1
+ const LIVE_QUERY_INTERNAL = /* @__PURE__ */ Symbol(`liveQueryInternal`);
2
2
  export {
3
3
  LIVE_QUERY_INTERNAL
4
4
  };
@@ -1 +1 @@
1
- {"version":3,"file":"internal.js","sources":["../../../../src/query/live/internal.ts"],"sourcesContent":["import type { CollectionConfigBuilder } from './collection-config-builder.js'\n\n/**\n * Symbol for accessing internal utilities that should not be part of the public API\n */\nexport const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`)\n\n/**\n * Internal utilities for live queries, accessible via Symbol\n */\nexport type LiveQueryInternalUtils = {\n getBuilder: () => CollectionConfigBuilder<any, any>\n hasCustomGetKey: boolean\n hasJoins: boolean\n}\n"],"names":[],"mappings":"AAKO,MAAM,sBAAsB,OAAO,mBAAmB;"}
1
+ {"version":3,"file":"internal.js","sources":["../../../../src/query/live/internal.ts"],"sourcesContent":["import type { CollectionConfigBuilder } from './collection-config-builder.js'\n\n/**\n * Symbol for accessing internal utilities that should not be part of the public API\n */\nexport const LIVE_QUERY_INTERNAL = Symbol(`liveQueryInternal`)\n\n/**\n * Internal utilities for live queries, accessible via Symbol\n */\nexport type LiveQueryInternalUtils = {\n getBuilder: () => CollectionConfigBuilder<any, any>\n hasCustomGetKey: boolean\n hasJoins: boolean\n}\n"],"names":[],"mappings":"AAKO,MAAM,6CAA6B,mBAAmB;"}
@@ -15,8 +15,9 @@ export type SyncState = {
15
15
  graph?: D2;
16
16
  inputs?: Record<string, RootStreamBuilder<unknown>>;
17
17
  pipeline?: ResultStream;
18
+ flushPendingChanges?: () => void;
18
19
  };
19
- export type FullSyncState = Required<SyncState>;
20
+ export type FullSyncState = Required<Omit<SyncState, `flushPendingChanges`>> & Pick<SyncState, `flushPendingChanges`>;
20
21
  /**
21
22
  * Configuration interface for live query collection options
22
23
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
- "version": "0.5.18",
3
+ "version": "0.5.20",
4
4
  "description": "A reactive client store for building super fast apps on sync",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -39,7 +39,7 @@
39
39
  "dependencies": {
40
40
  "@standard-schema/spec": "^1.1.0",
41
41
  "@tanstack/pacer-lite": "^0.2.0",
42
- "@tanstack/db-ivm": "0.1.14"
42
+ "@tanstack/db-ivm": "0.1.16"
43
43
  },
44
44
  "peerDependencies": {
45
45
  "typescript": ">=4.7"
@@ -134,10 +134,10 @@ export class CollectionSyncManager<
134
134
  !isTruncateTransaction
135
135
  ) {
136
136
  const existingValue = this.state.syncedData.get(key)
137
- if (
137
+ const valuesEqual =
138
138
  existingValue !== undefined &&
139
139
  deepEquals(existingValue, messageWithOptionalKey.value)
140
- ) {
140
+ if (valuesEqual) {
141
141
  // The "insert" is an echo of a value we already have locally.
142
142
  // Treat it as an update so we preserve optimistic intent without
143
143
  // throwing a duplicate-key error during reconciliation.
@@ -17,7 +17,11 @@ import {
17
17
  QueryMustHaveFromClauseError,
18
18
  SubQueryMustHaveFromClauseError,
19
19
  } from '../../errors.js'
20
- import { createRefProxy, toExpression } from './ref-proxy.js'
20
+ import {
21
+ createRefProxy,
22
+ createRefProxyWithSelected,
23
+ toExpression,
24
+ } from './ref-proxy.js'
21
25
  import type { NamespacedRow, SingleResult } from '../../types.js'
22
26
  import type {
23
27
  Aggregate,
@@ -30,6 +34,7 @@ import type {
30
34
  import type {
31
35
  CompareOptions,
32
36
  Context,
37
+ FunctionalHavingRow,
33
38
  GetResult,
34
39
  GroupByCallback,
35
40
  JoinOnCallback,
@@ -408,7 +413,12 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
408
413
  */
409
414
  having(callback: WhereCallback<TContext>): QueryBuilder<TContext> {
410
415
  const aliases = this._getCurrentAliases()
411
- const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
416
+ // Add $selected namespace if SELECT clause exists
417
+ const refProxy = (
418
+ this.query.select
419
+ ? createRefProxyWithSelected(aliases)
420
+ : createRefProxy(aliases)
421
+ ) as RefsForContext<TContext>
412
422
  const expression = callback(refProxy)
413
423
 
414
424
  // Validate that the callback returned a valid expression
@@ -506,7 +516,12 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
506
516
  options: OrderByDirection | OrderByOptions = `asc`,
507
517
  ): QueryBuilder<TContext> {
508
518
  const aliases = this._getCurrentAliases()
509
- const refProxy = createRefProxy(aliases) as RefsForContext<TContext>
519
+ // Add $selected namespace if SELECT clause exists
520
+ const refProxy = (
521
+ this.query.select
522
+ ? createRefProxyWithSelected(aliases)
523
+ : createRefProxy(aliases)
524
+ ) as RefsForContext<TContext>
510
525
  const result = callback(refProxy)
511
526
 
512
527
  const opts: CompareOptions =
@@ -771,7 +786,7 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
771
786
  * Filter grouped rows using a function that operates on each aggregated row
772
787
  * Warning: This cannot be optimized by the query compiler
773
788
  *
774
- * @param callback - A function that receives an aggregated row and returns a boolean
789
+ * @param callback - A function that receives an aggregated row (with $selected when select() was called) and returns a boolean
775
790
  * @returns A QueryBuilder with functional having filter applied
776
791
  *
777
792
  * @example
@@ -780,11 +795,12 @@ export class BaseQueryBuilder<TContext extends Context = Context> {
780
795
  * query
781
796
  * .from({ posts: postsCollection })
782
797
  * .groupBy(({posts}) => posts.userId)
783
- * .fn.having(row => row.count > 5)
798
+ * .select(({posts}) => ({ userId: posts.userId, count: count(posts.id) }))
799
+ * .fn.having(({ $selected }) => $selected.count > 5)
784
800
  * ```
785
801
  */
786
802
  having(
787
- callback: (row: TContext[`schema`]) => any,
803
+ callback: (row: FunctionalHavingRow<TContext>) => any,
788
804
  ): QueryBuilder<TContext> {
789
805
  return new BaseQueryBuilder({
790
806
  ...builder.query,
@@ -175,6 +175,96 @@ export function createRefProxy<T extends Record<string, any>>(
175
175
  return rootProxy
176
176
  }
177
177
 
178
+ /**
179
+ * Creates a ref proxy with $selected namespace for SELECT fields
180
+ *
181
+ * Adds a $selected property that allows accessing SELECT fields via $selected.fieldName syntax.
182
+ * The $selected proxy creates paths like ['$selected', 'fieldName'] which directly reference
183
+ * the $selected property on the namespaced row.
184
+ *
185
+ * @param aliases - Array of table aliases to create proxies for
186
+ * @returns A ref proxy with table aliases and $selected namespace
187
+ */
188
+ export function createRefProxyWithSelected<T extends Record<string, any>>(
189
+ aliases: Array<string>,
190
+ ): RefProxy<T> & T & { $selected: SingleRowRefProxy<any> } {
191
+ const baseProxy = createRefProxy(aliases)
192
+
193
+ // Create a proxy for $selected that prefixes all paths with '$selected'
194
+ const cache = new Map<string, any>()
195
+
196
+ function createSelectedProxy(path: Array<string>): any {
197
+ const pathKey = path.join(`.`)
198
+ if (cache.has(pathKey)) {
199
+ return cache.get(pathKey)
200
+ }
201
+
202
+ const proxy = new Proxy({} as any, {
203
+ get(target, prop, receiver) {
204
+ if (prop === `__refProxy`) return true
205
+ if (prop === `__path`) return [`$selected`, ...path]
206
+ if (prop === `__type`) return undefined
207
+ if (typeof prop === `symbol`) return Reflect.get(target, prop, receiver)
208
+
209
+ const newPath = [...path, String(prop)]
210
+ return createSelectedProxy(newPath)
211
+ },
212
+
213
+ has(target, prop) {
214
+ if (prop === `__refProxy` || prop === `__path` || prop === `__type`)
215
+ return true
216
+ return Reflect.has(target, prop)
217
+ },
218
+
219
+ ownKeys(target) {
220
+ return Reflect.ownKeys(target)
221
+ },
222
+
223
+ getOwnPropertyDescriptor(target, prop) {
224
+ if (prop === `__refProxy` || prop === `__path` || prop === `__type`) {
225
+ return { enumerable: false, configurable: true }
226
+ }
227
+ return Reflect.getOwnPropertyDescriptor(target, prop)
228
+ },
229
+ })
230
+
231
+ cache.set(pathKey, proxy)
232
+ return proxy
233
+ }
234
+
235
+ const wrappedSelectedProxy = createSelectedProxy([])
236
+
237
+ // Wrap the base proxy to also handle $selected access
238
+ return new Proxy(baseProxy, {
239
+ get(target, prop, receiver) {
240
+ if (prop === `$selected`) {
241
+ return wrappedSelectedProxy
242
+ }
243
+ return Reflect.get(target, prop, receiver)
244
+ },
245
+
246
+ has(target, prop) {
247
+ if (prop === `$selected`) return true
248
+ return Reflect.has(target, prop)
249
+ },
250
+
251
+ ownKeys(target) {
252
+ return [...Reflect.ownKeys(target), `$selected`]
253
+ },
254
+
255
+ getOwnPropertyDescriptor(target, prop) {
256
+ if (prop === `$selected`) {
257
+ return {
258
+ enumerable: true,
259
+ configurable: true,
260
+ value: wrappedSelectedProxy,
261
+ }
262
+ }
263
+ return Reflect.getOwnPropertyDescriptor(target, prop)
264
+ },
265
+ }) as RefProxy<T> & T & { $selected: SingleRowRefProxy<any> }
266
+ }
267
+
178
268
  /**
179
269
  * Converts a value to an Expression
180
270
  * If it's a RefProxy, creates a Ref, otherwise creates a Value