@tanstack/db 0.4.10 → 0.4.12

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 (32) hide show
  1. package/dist/cjs/collection/change-events.cjs +103 -0
  2. package/dist/cjs/collection/change-events.cjs.map +1 -1
  3. package/dist/cjs/collection/change-events.d.cts +14 -2
  4. package/dist/cjs/errors.cjs +0 -16
  5. package/dist/cjs/errors.cjs.map +1 -1
  6. package/dist/cjs/errors.d.cts +0 -6
  7. package/dist/cjs/index.cjs +0 -2
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/local-storage.cjs +24 -8
  10. package/dist/cjs/local-storage.cjs.map +1 -1
  11. package/dist/cjs/local-storage.d.cts +8 -0
  12. package/dist/cjs/query/live/collection-config-builder.cjs +12 -1
  13. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  14. package/dist/cjs/query/live/collection-config-builder.d.cts +14 -0
  15. package/dist/esm/collection/change-events.d.ts +14 -2
  16. package/dist/esm/collection/change-events.js +104 -1
  17. package/dist/esm/collection/change-events.js.map +1 -1
  18. package/dist/esm/errors.d.ts +0 -6
  19. package/dist/esm/errors.js +0 -16
  20. package/dist/esm/errors.js.map +1 -1
  21. package/dist/esm/index.js +1 -3
  22. package/dist/esm/local-storage.d.ts +8 -0
  23. package/dist/esm/local-storage.js +25 -9
  24. package/dist/esm/local-storage.js.map +1 -1
  25. package/dist/esm/query/live/collection-config-builder.d.ts +14 -0
  26. package/dist/esm/query/live/collection-config-builder.js +12 -1
  27. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  28. package/package.json +3 -3
  29. package/src/collection/change-events.ts +181 -7
  30. package/src/errors.ts +0 -16
  31. package/src/local-storage.ts +53 -12
  32. package/src/query/live/collection-config-builder.ts +20 -0
@@ -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 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 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 getBuilder: () => CollectionConfigBuilder<any, any>\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\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\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\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 // 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 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 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 getBuilder: () => this,\n setWindow: this.setWindow.bind(this),\n },\n }\n }\n\n setWindow(options: WindowOptions): true | Promise<void> {\n if (!this.windowFn) {\n throw new SetWindowRequiresOrderByError()\n }\n\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 /**\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 // 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 * 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 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\n if (this.allCollectionsReady()) {\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 * 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 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 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 getBuilder: () => CollectionConfigBuilder<any, any>\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}\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\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 // 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 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 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 getBuilder: () => this,\n setWindow: this.setWindow.bind(this),\n getWindow: this.getWindow.bind(this),\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 // 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 * 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 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\n if (this.allCollectionsReady()) {\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 * 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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"}
package/package.json CHANGED
@@ -1,14 +1,14 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.4.10",
4
+ "version": "0.4.12",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
- "@tanstack/db-ivm": "0.1.11"
7
+ "@tanstack/db-ivm": "0.1.12"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@vitest/coverage-istanbul": "^3.2.4",
11
- "arktype": "^2.1.22",
11
+ "arktype": "^2.1.23",
12
12
  "temporal-polyfill": "^0.3.0"
13
13
  },
14
14
  "exports": {
@@ -3,15 +3,20 @@ import {
3
3
  toExpression,
4
4
  } from "../query/builder/ref-proxy"
5
5
  import { compileSingleRowExpression } from "../query/compiler/evaluators.js"
6
- import { optimizeExpressionWithIndexes } from "../utils/index-optimization.js"
6
+ import {
7
+ findIndexForField,
8
+ optimizeExpressionWithIndexes,
9
+ } from "../utils/index-optimization.js"
10
+ import { ensureIndexForField } from "../indexes/auto-index.js"
11
+ import { makeComparator } from "../utils/comparison.js"
7
12
  import type {
8
13
  ChangeMessage,
9
14
  CurrentStateAsChangesOptions,
10
15
  SubscribeChangesOptions,
11
16
  } from "../types"
12
- import type { Collection } from "./index.js"
17
+ import type { Collection, CollectionImpl } from "./index.js"
13
18
  import type { SingleRowRefProxy } from "../query/builder/ref-proxy"
14
- import type { BasicExpression } from "../query/ir.js"
19
+ import type { BasicExpression, OrderBy } from "../query/ir.js"
15
20
 
16
21
  /**
17
22
  * Interface for a collection-like object that provides the necessary methods
@@ -28,7 +33,7 @@ export interface CollectionLike<
28
33
  /**
29
34
  * Returns the current state of the collection as an array of changes
30
35
  * @param collection - The collection to get changes from
31
- * @param options - Options including optional where filter
36
+ * @param options - Options including optional where filter, orderBy, and limit
32
37
  * @returns An array of changes
33
38
  * @example
34
39
  * // Get all items as changes
@@ -41,7 +46,19 @@ export interface CollectionLike<
41
46
  *
42
47
  * // Get only items using a pre-compiled expression
43
48
  * const activeChanges = currentStateAsChanges(collection, {
44
- * whereExpression: eq(row.status, 'active')
49
+ * where: eq(row.status, 'active')
50
+ * })
51
+ *
52
+ * // Get items ordered by name with limit
53
+ * const topUsers = currentStateAsChanges(collection, {
54
+ * orderBy: [{ expression: row.name, compareOptions: { direction: 'asc' } }],
55
+ * limit: 10
56
+ * })
57
+ *
58
+ * // Get active users ordered by score (highest score first)
59
+ * const topActiveUsers = currentStateAsChanges(collection, {
60
+ * where: eq(row.status, 'active'),
61
+ * orderBy: [{ expression: row.score, compareOptions: { direction: 'desc' } }],
45
62
  * })
46
63
  */
47
64
  export function currentStateAsChanges<
@@ -69,9 +86,48 @@ export function currentStateAsChanges<
69
86
  return result
70
87
  }
71
88
 
72
- // TODO: handle orderBy and limit options
73
- // by calling optimizeOrderedLimit
89
+ // Validate that limit without orderBy doesn't happen
90
+ if (options.limit !== undefined && !options.orderBy) {
91
+ throw new Error(`limit cannot be used without orderBy`)
92
+ }
93
+
94
+ // First check if orderBy is present (optionally with limit)
95
+ if (options.orderBy) {
96
+ // Create where filter function if present
97
+ const whereFilter = options.where
98
+ ? createFilterFunctionFromExpression(options.where)
99
+ : undefined
100
+
101
+ // Get ordered keys using index optimization when possible
102
+ const orderedKeys = getOrderedKeys(
103
+ collection,
104
+ options.orderBy,
105
+ options.limit,
106
+ whereFilter,
107
+ options.optimizedOnly
108
+ )
109
+
110
+ if (orderedKeys === undefined) {
111
+ // `getOrderedKeys` returned undefined because we asked for `optimizedOnly` and there was no index to use
112
+ return
113
+ }
114
+
115
+ // Convert keys to change messages
116
+ const result: Array<ChangeMessage<T>> = []
117
+ for (const key of orderedKeys) {
118
+ const value = collection.get(key)
119
+ if (value !== undefined) {
120
+ result.push({
121
+ type: `insert`,
122
+ key,
123
+ value,
124
+ })
125
+ }
126
+ }
127
+ return result
128
+ }
74
129
 
130
+ // If no orderBy OR orderBy optimization failed, use where clause optimization
75
131
  if (!options.where) {
76
132
  // No filtering, return all items
77
133
  return collectFilteredResults()
@@ -246,3 +302,121 @@ export function createFilteredCallback<T extends object>(
246
302
  }
247
303
  }
248
304
  }
305
+
306
+ /**
307
+ * Gets ordered keys from a collection using index optimization when possible
308
+ * @param collection - The collection to get keys from
309
+ * @param orderBy - The order by clause
310
+ * @param limit - Optional limit on number of keys to return
311
+ * @param whereFilter - Optional filter function to apply while traversing
312
+ * @returns Array of keys in sorted order
313
+ */
314
+ function getOrderedKeys<T extends object, TKey extends string | number>(
315
+ collection: CollectionLike<T, TKey>,
316
+ orderBy: OrderBy,
317
+ limit?: number,
318
+ whereFilter?: (item: T) => boolean,
319
+ optimizedOnly?: boolean
320
+ ): Array<TKey> | undefined {
321
+ // For single-column orderBy on a ref expression, try index optimization
322
+ if (orderBy.length === 1) {
323
+ const clause = orderBy[0]!
324
+ const orderByExpression = clause.expression
325
+
326
+ if (orderByExpression.type === `ref`) {
327
+ const propRef = orderByExpression
328
+ const fieldPath = propRef.path
329
+
330
+ // Ensure index exists for this field
331
+ ensureIndexForField(
332
+ fieldPath[0]!,
333
+ fieldPath,
334
+ collection as CollectionImpl<T, TKey>,
335
+ clause.compareOptions
336
+ )
337
+
338
+ // Find the index
339
+ const index = findIndexForField(
340
+ collection.indexes,
341
+ fieldPath,
342
+ clause.compareOptions
343
+ )
344
+
345
+ if (index && index.supports(`gt`)) {
346
+ // Use index optimization
347
+ const filterFn = (key: TKey): boolean => {
348
+ const value = collection.get(key)
349
+ if (value === undefined) {
350
+ return false
351
+ }
352
+ return whereFilter?.(value) ?? true
353
+ }
354
+
355
+ // Take the keys that match the filter and limit
356
+ // if no limit is provided `index.keyCount` is used,
357
+ // i.e. we will take all keys that match the filter
358
+ return index.take(limit ?? index.keyCount, undefined, filterFn)
359
+ }
360
+ }
361
+ }
362
+
363
+ if (optimizedOnly) {
364
+ return
365
+ }
366
+
367
+ // Fallback: collect all items and sort in memory
368
+ const allItems: Array<{ key: TKey; value: T }> = []
369
+ for (const [key, value] of collection.entries()) {
370
+ if (whereFilter?.(value) ?? true) {
371
+ allItems.push({ key, value })
372
+ }
373
+ }
374
+
375
+ // Sort using makeComparator
376
+ const compare = (a: { key: TKey; value: T }, b: { key: TKey; value: T }) => {
377
+ for (const clause of orderBy) {
378
+ const compareFn = makeComparator(clause.compareOptions)
379
+
380
+ // Extract values for comparison
381
+ const aValue = extractValueFromItem(a.value, clause.expression)
382
+ const bValue = extractValueFromItem(b.value, clause.expression)
383
+
384
+ const result = compareFn(aValue, bValue)
385
+ if (result !== 0) {
386
+ return result
387
+ }
388
+ }
389
+ return 0
390
+ }
391
+
392
+ allItems.sort(compare)
393
+ const sortedKeys = allItems.map((item) => item.key)
394
+
395
+ // Apply limit if provided
396
+ if (limit !== undefined) {
397
+ return sortedKeys.slice(0, limit)
398
+ }
399
+
400
+ // if no limit is provided, we will return all keys
401
+ return sortedKeys
402
+ }
403
+
404
+ /**
405
+ * Helper function to extract a value from an item based on an expression
406
+ */
407
+ function extractValueFromItem(item: any, expression: BasicExpression): any {
408
+ if (expression.type === `ref`) {
409
+ const propRef = expression
410
+ let value = item
411
+ for (const pathPart of propRef.path) {
412
+ value = value?.[pathPart]
413
+ }
414
+ return value
415
+ } else if (expression.type === `val`) {
416
+ return expression.value
417
+ } else {
418
+ // It must be a function
419
+ const evaluator = compileSingleRowExpression(expression)
420
+ return evaluator(item as Record<string, unknown>)
421
+ }
422
+ }
package/src/errors.ts CHANGED
@@ -521,22 +521,6 @@ export class StorageKeyRequiredError extends LocalStorageCollectionError {
521
521
  }
522
522
  }
523
523
 
524
- export class NoStorageAvailableError extends LocalStorageCollectionError {
525
- constructor() {
526
- super(
527
- `[LocalStorageCollection] No storage available. Please provide a storage option or ensure window.localStorage is available.`
528
- )
529
- }
530
- }
531
-
532
- export class NoStorageEventApiError extends LocalStorageCollectionError {
533
- constructor() {
534
- super(
535
- `[LocalStorageCollection] No storage event API available. Please provide a storageEventApi option or ensure window is available.`
536
- )
537
- }
538
- }
539
-
540
524
  export class InvalidStorageDataFormatError extends LocalStorageCollectionError {
541
525
  constructor(storageKey: string, key: string) {
542
526
  super(
@@ -1,8 +1,6 @@
1
1
  import {
2
2
  InvalidStorageDataFormatError,
3
3
  InvalidStorageObjectFormatError,
4
- NoStorageAvailableError,
5
- NoStorageEventApiError,
6
4
  SerializationError,
7
5
  StorageKeyRequiredError,
8
6
  } from "./errors"
@@ -138,12 +136,58 @@ function generateUuid(): string {
138
136
  return crypto.randomUUID()
139
137
  }
140
138
 
139
+ /**
140
+ * Creates an in-memory storage implementation that mimics the StorageApi interface
141
+ * Used as a fallback when localStorage is not available (e.g., server-side rendering)
142
+ * @returns An object implementing the StorageApi interface using an in-memory Map
143
+ */
144
+ function createInMemoryStorage(): StorageApi {
145
+ const storage = new Map<string, string>()
146
+
147
+ return {
148
+ getItem(key: string): string | null {
149
+ return storage.get(key) ?? null
150
+ },
151
+ setItem(key: string, value: string): void {
152
+ storage.set(key, value)
153
+ },
154
+ removeItem(key: string): void {
155
+ storage.delete(key)
156
+ },
157
+ }
158
+ }
159
+
160
+ /**
161
+ * Creates a no-op storage event API for environments without window (e.g., server-side)
162
+ * This provides the required interface but doesn't actually listen to any events
163
+ * since cross-tab synchronization is not possible in server environments
164
+ * @returns An object implementing the StorageEventApi interface with no-op methods
165
+ */
166
+ function createNoOpStorageEventApi(): StorageEventApi {
167
+ return {
168
+ addEventListener: () => {
169
+ // No-op: cannot listen to storage events without window
170
+ },
171
+ removeEventListener: () => {
172
+ // No-op: cannot remove listeners without window
173
+ },
174
+ }
175
+ }
176
+
141
177
  /**
142
178
  * Creates localStorage collection options for use with a standard Collection
143
179
  *
144
180
  * This function creates a collection that persists data to localStorage/sessionStorage
145
181
  * and synchronizes changes across browser tabs using storage events.
146
182
  *
183
+ * **Fallback Behavior:**
184
+ *
185
+ * When localStorage is not available (e.g., in server-side rendering environments),
186
+ * this function automatically falls back to an in-memory storage implementation.
187
+ * This prevents errors during module initialization and allows the collection to
188
+ * work in any environment, though data will not persist across page reloads or
189
+ * be shared across tabs when using the in-memory fallback.
190
+ *
147
191
  * **Using with Manual Transactions:**
148
192
  *
149
193
  * For manual transactions, you must call `utils.acceptMutations()` in your transaction's `mutationFn`
@@ -257,21 +301,18 @@ export function localStorageCollectionOptions(
257
301
  }
258
302
 
259
303
  // Default to window.localStorage if no storage is provided
304
+ // Fall back to in-memory storage if localStorage is not available (e.g., server-side rendering)
260
305
  const storage =
261
306
  config.storage ||
262
- (typeof window !== `undefined` ? window.localStorage : null)
263
-
264
- if (!storage) {
265
- throw new NoStorageAvailableError()
266
- }
307
+ (typeof window !== `undefined` ? window.localStorage : null) ||
308
+ createInMemoryStorage()
267
309
 
268
310
  // Default to window for storage events if not provided
311
+ // Fall back to no-op storage event API if window is not available (e.g., server-side rendering)
269
312
  const storageEventApi =
270
- config.storageEventApi || (typeof window !== `undefined` ? window : null)
271
-
272
- if (!storageEventApi) {
273
- throw new NoStorageEventApiError()
274
- }
313
+ config.storageEventApi ||
314
+ (typeof window !== `undefined` ? window : null) ||
315
+ createNoOpStorageEventApi()
275
316
 
276
317
  // Track the last known state to detect changes
277
318
  const lastKnownData = new Map<string | number, StoredItem<any>>()
@@ -43,6 +43,12 @@ export type LiveQueryCollectionUtils = UtilsRecord & {
43
43
  * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded
44
44
  */
45
45
  setWindow: (options: WindowOptions) => true | Promise<void>
46
+ /**
47
+ * Gets the current window (offset and limit) for an ordered query.
48
+ *
49
+ * @returns The current window settings, or `undefined` if the query is not windowed
50
+ */
51
+ getWindow: () => { offset: number; limit: number } | undefined
46
52
  }
47
53
 
48
54
  type PendingGraphRun = {
@@ -93,6 +99,7 @@ export class CollectionConfigBuilder<
93
99
  public liveQueryCollection?: Collection<TResult, any, any>
94
100
 
95
101
  private windowFn: ((options: WindowOptions) => void) | undefined
102
+ private currentWindow: WindowOptions | undefined
96
103
 
97
104
  private maybeRunGraphFn: (() => void) | undefined
98
105
 
@@ -187,6 +194,7 @@ export class CollectionConfigBuilder<
187
194
  getRunCount: this.getRunCount.bind(this),
188
195
  getBuilder: () => this,
189
196
  setWindow: this.setWindow.bind(this),
197
+ getWindow: this.getWindow.bind(this),
190
198
  },
191
199
  }
192
200
  }
@@ -196,6 +204,7 @@ export class CollectionConfigBuilder<
196
204
  throw new SetWindowRequiresOrderByError()
197
205
  }
198
206
 
207
+ this.currentWindow = options
199
208
  this.windowFn(options)
200
209
  this.maybeRunGraphFn?.()
201
210
 
@@ -219,6 +228,17 @@ export class CollectionConfigBuilder<
219
228
  return true
220
229
  }
221
230
 
231
+ getWindow(): { offset: number; limit: number } | undefined {
232
+ // Only return window if this is a windowed query (has orderBy and windowFn)
233
+ if (!this.windowFn || !this.currentWindow) {
234
+ return undefined
235
+ }
236
+ return {
237
+ offset: this.currentWindow.offset ?? 0,
238
+ limit: this.currentWindow.limit ?? 0,
239
+ }
240
+ }
241
+
222
242
  /**
223
243
  * Resolves a collection alias to its collection ID.
224
244
  *