@tanstack/db
Version:
A reactive client store for building super fast apps on sync
1 lines • 12 kB
Source Map (JSON)
{"version":3,"file":"change-events.cjs","sources":["../../src/change-events.ts"],"sourcesContent":["import {\n createSingleRowRefProxy,\n toExpression,\n} from \"./query/builder/ref-proxy\"\nimport { compileSingleRowExpression } from \"./query/compiler/evaluators.js\"\nimport { optimizeExpressionWithIndexes } from \"./utils/index-optimization.js\"\nimport type {\n ChangeMessage,\n CurrentStateAsChangesOptions,\n SubscribeChangesOptions,\n} from \"./types\"\nimport type { Collection } from \"./collection\"\nimport type { SingleRowRefProxy } from \"./query/builder/ref-proxy\"\nimport type { BasicExpression } from \"./query/ir.js\"\n\n/**\n * Interface for a collection-like object that provides the necessary methods\n * for the change events system to work\n */\nexport interface CollectionLike<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> extends Pick<Collection<T, TKey>, `get` | `has` | `entries` | `indexes`> {}\n\n/**\n * Returns the current state of the collection as an array of changes\n * @param collection - The collection to get changes from\n * @param options - Options including optional where filter\n * @returns An array of changes\n * @example\n * // Get all items as changes\n * const allChanges = currentStateAsChanges(collection)\n *\n * // Get only items matching a condition\n * const activeChanges = currentStateAsChanges(collection, {\n * where: (row) => row.status === 'active'\n * })\n *\n * // Get only items using a pre-compiled expression\n * const activeChanges = currentStateAsChanges(collection, {\n * whereExpression: eq(row.status, 'active')\n * })\n */\nexport function currentStateAsChanges<\n T extends object,\n TKey extends string | number,\n>(\n collection: CollectionLike<T, TKey>,\n options: CurrentStateAsChangesOptions<T> = {}\n): Array<ChangeMessage<T>> {\n // Helper function to collect filtered results\n const collectFilteredResults = (\n filterFn?: (value: T) => boolean\n ): Array<ChangeMessage<T>> => {\n const result: Array<ChangeMessage<T>> = []\n for (const [key, value] of collection.entries()) {\n // If no filter function is provided, include all items\n if (filterFn?.(value) ?? true) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n }\n\n if (!options.where && !options.whereExpression) {\n // No filtering, return all items\n return collectFilteredResults()\n }\n\n // There's a where clause, let's see if we can use an index\n try {\n let expression: BasicExpression<boolean>\n\n if (options.whereExpression) {\n // Use the pre-compiled expression directly\n expression = options.whereExpression\n } else if (options.where) {\n // Create the single-row refProxy for the callback\n const singleRowRefProxy = createSingleRowRefProxy<T>()\n\n // Execute the callback to get the expression\n const whereExpression = options.where(singleRowRefProxy)\n\n // Convert the result to a BasicExpression\n expression = toExpression(whereExpression)\n } else {\n // This should never happen due to the check above, but TypeScript needs it\n return []\n }\n\n // Try to optimize the query using indexes\n const optimizationResult = optimizeExpressionWithIndexes(\n expression,\n collection.indexes\n )\n\n if (optimizationResult.canOptimize) {\n // Use index optimization\n const result: Array<ChangeMessage<T>> = []\n for (const key of optimizationResult.matchingKeys) {\n const value = collection.get(key)\n if (value !== undefined) {\n result.push({\n type: `insert`,\n key,\n value,\n })\n }\n }\n return result\n } else {\n // No index found or complex expression, fall back to full scan with filter\n const filterFn = options.where\n ? createFilterFunction(options.where)\n : createFilterFunctionFromExpression(expression)\n\n return collectFilteredResults(filterFn)\n }\n } catch (error) {\n // If anything goes wrong with the where clause, fall back to full scan\n console.warn(\n `Error processing where clause, falling back to full scan:`,\n error\n )\n\n const filterFn = options.where\n ? createFilterFunction(options.where)\n : createFilterFunctionFromExpression(options.whereExpression!)\n\n return collectFilteredResults(filterFn)\n }\n}\n\n/**\n * Creates a filter function from a where callback\n * @param whereCallback - The callback function that defines the filter condition\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunction<T extends object>(\n whereCallback: (row: SingleRowRefProxy<T>) => any\n): (item: T) => boolean {\n return (item: T): boolean => {\n try {\n // First try the RefProxy approach for query builder functions\n const singleRowRefProxy = createSingleRowRefProxy<T>()\n const whereExpression = whereCallback(singleRowRefProxy)\n const expression = toExpression(whereExpression)\n const evaluator = compileSingleRowExpression(expression)\n const result = evaluator(item as Record<string, unknown>)\n // WHERE clauses should always evaluate to boolean predicates (Kevin's feedback)\n return result\n } catch {\n // If RefProxy approach fails (e.g., arithmetic operations), fall back to direct evaluation\n try {\n // Create a simple proxy that returns actual values for arithmetic operations\n const simpleProxy = new Proxy(item as any, {\n get(target, prop) {\n return target[prop]\n },\n }) as SingleRowRefProxy<T>\n\n const result = whereCallback(simpleProxy)\n return result\n } catch {\n // If both approaches fail, exclude the item\n return false\n }\n }\n }\n}\n\n/**\n * Creates a filter function from a pre-compiled expression\n * @param expression - The pre-compiled expression to evaluate\n * @returns A function that takes an item and returns true if it matches the filter\n */\nexport function createFilterFunctionFromExpression<T extends object>(\n expression: BasicExpression<boolean>\n): (item: T) => boolean {\n return (item: T): boolean => {\n try {\n const evaluator = compileSingleRowExpression(expression)\n const result = evaluator(item as Record<string, unknown>)\n return Boolean(result)\n } catch {\n // If evaluation fails, exclude the item\n return false\n }\n }\n}\n\n/**\n * Creates a filtered callback that only calls the original callback with changes that match the where clause\n * @param originalCallback - The original callback to filter\n * @param options - The subscription options containing the where clause\n * @returns A filtered callback function\n */\nexport function createFilteredCallback<T extends object>(\n originalCallback: (changes: Array<ChangeMessage<T>>) => void,\n options: SubscribeChangesOptions<T>\n): (changes: Array<ChangeMessage<T>>) => void {\n const filterFn = options.whereExpression\n ? createFilterFunctionFromExpression(options.whereExpression)\n : createFilterFunction(options.where!)\n\n return (changes: Array<ChangeMessage<T>>) => {\n const filteredChanges: Array<ChangeMessage<T>> = []\n\n for (const change of changes) {\n if (change.type === `insert`) {\n // For inserts, check if the new value matches the filter\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n } else if (change.type === `update`) {\n // For updates, we need to check both old and new values\n const newValueMatches = filterFn(change.value)\n const oldValueMatches = change.previousValue\n ? filterFn(change.previousValue)\n : false\n\n if (newValueMatches && oldValueMatches) {\n // Both old and new match: emit update\n filteredChanges.push(change)\n } else if (newValueMatches && !oldValueMatches) {\n // New matches but old didn't: emit insert\n filteredChanges.push({\n ...change,\n type: `insert`,\n })\n } else if (!newValueMatches && oldValueMatches) {\n // Old matched but new doesn't: emit delete\n filteredChanges.push({\n ...change,\n type: `delete`,\n value: change.previousValue!, // Use the previous value for the delete\n })\n }\n // If neither matches, don't emit anything\n } else {\n // For deletes, include if the previous value would have matched\n // (so subscribers know something they were tracking was deleted)\n if (filterFn(change.value)) {\n filteredChanges.push(change)\n }\n }\n }\n\n // Always call the original callback if we have filtered changes OR\n // if the original changes array was empty (which indicates a ready signal)\n if (filteredChanges.length > 0 || changes.length === 0) {\n originalCallback(filteredChanges)\n }\n }\n}\n"],"names":["createSingleRowRefProxy","toExpression","optimizeExpressionWithIndexes","compileSingleRowExpression"],"mappings":"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"}