UNPKG

@tanstack/db

Version:

A reactive client store for building super fast apps on sync

1 lines 60.3 kB
{"version":3,"file":"collection.cjs","sources":["../../src/collection.ts"],"sourcesContent":["import { Store } from \"@tanstack/store\"\nimport { withArrayChangeTracking, withChangeTracking } from \"./proxy\"\nimport { Transaction, getActiveTransaction } from \"./transactions\"\nimport { SortedMap } from \"./SortedMap\"\nimport type {\n ChangeListener,\n ChangeMessage,\n CollectionConfig,\n Fn,\n InsertConfig,\n OperationConfig,\n OptimisticChangeMessage,\n PendingMutation,\n StandardSchema,\n Transaction as TransactionType,\n UtilsRecord,\n} from \"./types\"\n\n// Store collections in memory\nexport const collectionsStore = new Map<string, CollectionImpl<any, any>>()\n\n// Map to track loading collections\nconst loadingCollectionResolvers = new Map<\n string,\n {\n promise: Promise<CollectionImpl<any, any>>\n resolve: (value: CollectionImpl<any, any>) => void\n }\n>()\n\ninterface PendingSyncedTransaction<T extends object = Record<string, unknown>> {\n committed: boolean\n operations: Array<OptimisticChangeMessage<T>>\n}\n\n/**\n * Enhanced Collection interface that includes both data type T and utilities TUtils\n * @template T - The type of items in the collection\n * @template TUtils - The utilities record type\n */\nexport interface Collection<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = {},\n> extends CollectionImpl<T, TKey> {\n readonly utils: TUtils\n}\n\n/**\n * Creates a new Collection instance with the given configuration\n *\n * @template T - The type of items in the collection\n * @template TKey - The type of the key for the collection\n * @template TUtils - The utilities record type\n * @param options - Collection options with optional utilities\n * @returns A new Collection with utilities exposed both at top level and under .utils\n */\nexport function createCollection<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = {},\n>(\n options: CollectionConfig<T, TKey> & { utils?: TUtils }\n): Collection<T, TKey, TUtils> {\n const collection = new CollectionImpl<T, TKey>(options)\n\n // Copy utils to both top level and .utils namespace\n if (options.utils) {\n collection.utils = { ...options.utils }\n } else {\n collection.utils = {} as TUtils\n }\n\n return collection as Collection<T, TKey, TUtils>\n}\n\n/**\n * Preloads a collection with the given configuration\n * Returns a promise that resolves once the sync tool has done its first commit (initial sync is finished)\n * If the collection has already loaded, it resolves immediately\n *\n * This function is useful in route loaders or similar pre-rendering scenarios where you want\n * to ensure data is available before a route transition completes. It uses the same shared collection\n * instance that will be used by useCollection, ensuring data consistency.\n *\n * @example\n * ```typescript\n * // In a route loader\n * async function loader({ params }) {\n * await preloadCollection({\n * id: `users-${params.userId}`,\n * sync: { ... },\n * });\n *\n * return null;\n * }\n * ```\n *\n * @template T - The type of items in the collection\n * @param config - Configuration for the collection, including id and sync\n * @returns Promise that resolves when the initial sync is finished\n */\nexport function preloadCollection<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n>(config: CollectionConfig<T, TKey>): Promise<CollectionImpl<T, TKey>> {\n if (!config.id) {\n throw new Error(`The id property is required for preloadCollection`)\n }\n\n // If the collection is already fully loaded, return a resolved promise\n if (\n collectionsStore.has(config.id) &&\n !loadingCollectionResolvers.has(config.id)\n ) {\n return Promise.resolve(\n collectionsStore.get(config.id)! as CollectionImpl<T, TKey>\n )\n }\n\n // If the collection is in the process of loading, return its promise\n if (loadingCollectionResolvers.has(config.id)) {\n return loadingCollectionResolvers.get(config.id)!.promise\n }\n\n // Create a new collection instance if it doesn't exist\n if (!collectionsStore.has(config.id)) {\n collectionsStore.set(\n config.id,\n createCollection<T, TKey>({\n id: config.id,\n getKey: config.getKey,\n sync: config.sync,\n schema: config.schema,\n })\n )\n }\n\n const collection = collectionsStore.get(config.id)! as CollectionImpl<T, TKey>\n\n // Create a promise that will resolve after the first commit\n let resolveFirstCommit: (value: CollectionImpl<T, TKey>) => void\n const firstCommitPromise = new Promise<CollectionImpl<T, TKey>>((resolve) => {\n resolveFirstCommit = resolve\n })\n\n // Store the loading promise first\n loadingCollectionResolvers.set(config.id, {\n promise: firstCommitPromise,\n resolve: resolveFirstCommit!,\n })\n\n // Register a one-time listener for the first commit\n collection.onFirstCommit(() => {\n if (!config.id) {\n throw new Error(`The id property is required for preloadCollection`)\n }\n if (loadingCollectionResolvers.has(config.id)) {\n const resolver = loadingCollectionResolvers.get(config.id)!\n loadingCollectionResolvers.delete(config.id)\n resolver.resolve(collection)\n }\n })\n\n return firstCommitPromise\n}\n\n/**\n * Custom error class for schema validation errors\n */\nexport class SchemaValidationError extends Error {\n type: `insert` | `update`\n issues: ReadonlyArray<{\n message: string\n path?: ReadonlyArray<string | number | symbol>\n }>\n\n constructor(\n type: `insert` | `update`,\n issues: ReadonlyArray<{\n message: string\n path?: ReadonlyArray<string | number | symbol>\n }>,\n message?: string\n ) {\n const defaultMessage = `${type === `insert` ? `Insert` : `Update`} validation failed: ${issues\n .map((issue) => issue.message)\n .join(`, `)}`\n\n super(message || defaultMessage)\n this.name = `SchemaValidationError`\n this.type = type\n this.issues = issues\n }\n}\n\nexport class CollectionImpl<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n public transactions: SortedMap<string, Transaction<any>>\n\n // Core state - make public for testing\n public syncedData = new Map<TKey, T>()\n public syncedMetadata = new Map<TKey, unknown>()\n\n // Optimistic state tracking - make public for testing\n public derivedUpserts = new Map<TKey, T>()\n public derivedDeletes = new Set<TKey>()\n\n // Cached size for performance\n private _size = 0\n\n // Event system\n private changeListeners = new Set<ChangeListener<T, TKey>>()\n private changeKeyListeners = new Map<TKey, Set<ChangeListener<T, TKey>>>()\n\n // Utilities namespace\n // This is populated by createCollection\n public utils: Record<string, Fn> = {}\n\n private pendingSyncedTransactions: Array<PendingSyncedTransaction<T>> = []\n private syncedKeys = new Set<TKey>()\n public config: CollectionConfig<T, TKey>\n private hasReceivedFirstCommit = false\n\n // Array to store one-time commit listeners\n private onFirstCommitCallbacks: Array<() => void> = []\n\n /**\n * Register a callback to be executed on the next commit\n * Useful for preloading collections\n * @param callback Function to call after the next commit\n */\n public onFirstCommit(callback: () => void): void {\n this.onFirstCommitCallbacks.push(callback)\n }\n\n public id = ``\n\n /**\n * Creates a new Collection instance\n *\n * @param config - Configuration object for the collection\n * @throws Error if sync config is missing\n */\n constructor(config: CollectionConfig<T, TKey>) {\n // eslint-disable-next-line\n if (!config) {\n throw new Error(`Collection requires a config`)\n }\n if (config.id) {\n this.id = config.id\n } else {\n this.id = crypto.randomUUID()\n }\n\n // eslint-disable-next-line\n if (!config.sync) {\n throw new Error(`Collection requires a sync config`)\n }\n\n this.transactions = new SortedMap<string, Transaction<any>>(\n (a, b) => a.createdAt.getTime() - b.createdAt.getTime()\n )\n\n this.config = config\n\n // Start the sync process\n config.sync.sync({\n collection: this,\n begin: () => {\n this.pendingSyncedTransactions.push({\n committed: false,\n operations: [],\n })\n },\n write: (messageWithoutKey: Omit<ChangeMessage<T>, `key`>) => {\n const pendingTransaction =\n this.pendingSyncedTransactions[\n this.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new Error(`No pending sync transaction to write to`)\n }\n if (pendingTransaction.committed) {\n throw new Error(\n `The pending sync transaction is already committed, you can't still write to it.`\n )\n }\n const key = this.getKeyFromItem(messageWithoutKey.value)\n\n // Check if an item with this key already exists when inserting\n if (messageWithoutKey.type === `insert`) {\n if (\n this.syncedData.has(key) &&\n !pendingTransaction.operations.some(\n (op) => op.key === key && op.type === `delete`\n )\n ) {\n throw new Error(\n `Cannot insert document with key \"${key}\" from sync because it already exists in the collection \"${this.id}\"`\n )\n }\n }\n\n const message: ChangeMessage<T> = {\n ...messageWithoutKey,\n key,\n }\n pendingTransaction.operations.push(message)\n },\n commit: () => {\n const pendingTransaction =\n this.pendingSyncedTransactions[\n this.pendingSyncedTransactions.length - 1\n ]\n if (!pendingTransaction) {\n throw new Error(`No pending sync transaction to commit`)\n }\n if (pendingTransaction.committed) {\n throw new Error(\n `The pending sync transaction is already committed, you can't commit it again.`\n )\n }\n\n pendingTransaction.committed = true\n this.commitPendingTransactions()\n },\n })\n }\n\n /**\n * Recompute optimistic state from active transactions\n */\n private recomputeOptimisticState(): void {\n const previousState = new Map(this.derivedUpserts)\n const previousDeletes = new Set(this.derivedDeletes)\n\n // Clear current optimistic state\n this.derivedUpserts.clear()\n this.derivedDeletes.clear()\n\n // Apply active transactions\n const activeTransactions = Array.from(this.transactions.values())\n for (const transaction of activeTransactions) {\n if (![`completed`, `failed`].includes(transaction.state)) {\n for (const mutation of transaction.mutations) {\n if (mutation.collection === this) {\n switch (mutation.type) {\n case `insert`:\n case `update`:\n this.derivedUpserts.set(mutation.key, mutation.modified as T)\n this.derivedDeletes.delete(mutation.key)\n break\n case `delete`:\n this.derivedUpserts.delete(mutation.key)\n this.derivedDeletes.add(mutation.key)\n break\n }\n }\n }\n }\n }\n\n // Update cached size\n this._size = this.calculateSize()\n\n // Collect events for changes\n const events: Array<ChangeMessage<T, TKey>> = []\n this.collectOptimisticChanges(previousState, previousDeletes, events)\n\n // Emit all events at once\n this.emitEvents(events)\n }\n\n /**\n * Calculate the current size based on synced data and optimistic changes\n */\n private calculateSize(): number {\n const syncedSize = this.syncedData.size\n const deletesFromSynced = Array.from(this.derivedDeletes).filter(\n (key) => this.syncedData.has(key) && !this.derivedUpserts.has(key)\n ).length\n const upsertsNotInSynced = Array.from(this.derivedUpserts.keys()).filter(\n (key) => !this.syncedData.has(key)\n ).length\n\n return syncedSize - deletesFromSynced + upsertsNotInSynced\n }\n\n /**\n * Collect events for optimistic changes\n */\n private collectOptimisticChanges(\n previousUpserts: Map<TKey, T>,\n previousDeletes: Set<TKey>,\n events: Array<ChangeMessage<T, TKey>>\n ): void {\n const allKeys = new Set([\n ...previousUpserts.keys(),\n ...this.derivedUpserts.keys(),\n ...previousDeletes,\n ...this.derivedDeletes,\n ])\n\n for (const key of allKeys) {\n const currentValue = this.get(key)\n const previousValue = this.getPreviousValue(\n key,\n previousUpserts,\n previousDeletes\n )\n\n if (previousValue !== undefined && currentValue === undefined) {\n events.push({ type: `delete`, key, value: previousValue })\n } else if (previousValue === undefined && currentValue !== undefined) {\n events.push({ type: `insert`, key, value: currentValue })\n } else if (\n previousValue !== undefined &&\n currentValue !== undefined &&\n previousValue !== currentValue\n ) {\n events.push({\n type: `update`,\n key,\n value: currentValue,\n previousValue,\n })\n }\n }\n }\n\n /**\n * Get the previous value for a key given previous optimistic state\n */\n private getPreviousValue(\n key: TKey,\n previousUpserts: Map<TKey, T>,\n previousDeletes: Set<TKey>\n ): T | undefined {\n if (previousDeletes.has(key)) {\n return undefined\n }\n if (previousUpserts.has(key)) {\n return previousUpserts.get(key)\n }\n return this.syncedData.get(key)\n }\n\n /**\n * Emit multiple events at once to all listeners\n */\n private emitEvents(changes: Array<ChangeMessage<T, TKey>>): void {\n if (changes.length > 0) {\n // Emit to general listeners\n for (const listener of this.changeListeners) {\n listener(changes)\n }\n\n // Emit to key-specific listeners\n if (this.changeKeyListeners.size > 0) {\n // Group changes by key, but only for keys that have listeners\n const changesByKey = new Map<TKey, Array<ChangeMessage<T, TKey>>>()\n for (const change of changes) {\n if (this.changeKeyListeners.has(change.key)) {\n if (!changesByKey.has(change.key)) {\n changesByKey.set(change.key, [])\n }\n changesByKey.get(change.key)!.push(change)\n }\n }\n\n // Emit batched changes to each key's listeners\n for (const [key, keyChanges] of changesByKey) {\n const keyListeners = this.changeKeyListeners.get(key)!\n for (const listener of keyListeners) {\n listener(keyChanges)\n }\n }\n }\n }\n }\n\n /**\n * Get the current value for a key (virtual derived state)\n */\n public get(key: TKey): T | undefined {\n // Check if optimistically deleted\n if (this.derivedDeletes.has(key)) {\n return undefined\n }\n\n // Check optimistic upserts first\n if (this.derivedUpserts.has(key)) {\n return this.derivedUpserts.get(key)\n }\n\n // Fall back to synced data\n return this.syncedData.get(key)\n }\n\n /**\n * Check if a key exists in the collection (virtual derived state)\n */\n public has(key: TKey): boolean {\n // Check if optimistically deleted\n if (this.derivedDeletes.has(key)) {\n return false\n }\n\n // Check optimistic upserts first\n if (this.derivedUpserts.has(key)) {\n return true\n }\n\n // Fall back to synced data\n return this.syncedData.has(key)\n }\n\n /**\n * Get the current size of the collection (cached)\n */\n public get size(): number {\n return this._size\n }\n\n /**\n * Get all keys (virtual derived state)\n */\n public *keys(): IterableIterator<TKey> {\n // Yield keys from synced data, skipping any that are deleted.\n for (const key of this.syncedData.keys()) {\n if (!this.derivedDeletes.has(key)) {\n yield key\n }\n }\n // Yield keys from upserts that were not already in synced data.\n for (const key of this.derivedUpserts.keys()) {\n if (!this.syncedData.has(key) && !this.derivedDeletes.has(key)) {\n // The derivedDeletes check is technically redundant if inserts/updates always remove from deletes,\n // but it's safer to keep it.\n yield key\n }\n }\n }\n\n /**\n * Get all values (virtual derived state)\n */\n public *values(): IterableIterator<T> {\n for (const key of this.keys()) {\n const value = this.get(key)\n if (value !== undefined) {\n yield value\n }\n }\n }\n\n /**\n * Get all entries (virtual derived state)\n */\n public *entries(): IterableIterator<[TKey, T]> {\n for (const key of this.keys()) {\n const value = this.get(key)\n if (value !== undefined) {\n yield [key, value]\n }\n }\n }\n\n /**\n * Attempts to commit pending synced transactions if there are no active transactions\n * This method processes operations from pending transactions and applies them to the synced data\n */\n commitPendingTransactions = () => {\n if (\n !Array.from(this.transactions.values()).some(\n ({ state }) => state === `persisting`\n )\n ) {\n const changedKeys = new Set<TKey>()\n const events: Array<ChangeMessage<T, TKey>> = []\n\n for (const transaction of this.pendingSyncedTransactions) {\n for (const operation of transaction.operations) {\n const key = operation.key as TKey\n changedKeys.add(key)\n this.syncedKeys.add(key)\n\n // Update metadata\n switch (operation.type) {\n case `insert`:\n this.syncedMetadata.set(key, operation.metadata)\n break\n case `update`:\n this.syncedMetadata.set(\n key,\n Object.assign(\n {},\n this.syncedMetadata.get(key),\n operation.metadata\n )\n )\n break\n case `delete`:\n this.syncedMetadata.delete(key)\n break\n }\n\n // Update synced data and collect events\n const previousValue = this.syncedData.get(key)\n\n switch (operation.type) {\n case `insert`:\n this.syncedData.set(key, operation.value)\n if (\n !this.derivedDeletes.has(key) &&\n !this.derivedUpserts.has(key)\n ) {\n events.push({\n type: `insert`,\n key,\n value: operation.value,\n })\n }\n break\n case `update`: {\n const updatedValue = Object.assign(\n {},\n this.syncedData.get(key),\n operation.value\n )\n this.syncedData.set(key, updatedValue)\n if (\n !this.derivedDeletes.has(key) &&\n !this.derivedUpserts.has(key)\n ) {\n events.push({\n type: `update`,\n key,\n value: updatedValue,\n previousValue,\n })\n }\n break\n }\n case `delete`:\n this.syncedData.delete(key)\n if (\n !this.derivedDeletes.has(key) &&\n !this.derivedUpserts.has(key)\n ) {\n if (previousValue) {\n events.push({\n type: `delete`,\n key,\n value: previousValue,\n })\n }\n }\n break\n }\n }\n }\n\n // Update cached size after synced data changes\n this._size = this.calculateSize()\n\n // Emit all events at once\n this.emitEvents(events)\n\n this.pendingSyncedTransactions = []\n\n // Call any registered one-time commit listeners\n if (!this.hasReceivedFirstCommit) {\n this.hasReceivedFirstCommit = true\n const callbacks = [...this.onFirstCommitCallbacks]\n this.onFirstCommitCallbacks = []\n callbacks.forEach((callback) => callback())\n }\n }\n }\n\n private ensureStandardSchema(schema: unknown): StandardSchema<T> {\n // If the schema already implements the standard-schema interface, return it\n if (schema && typeof schema === `object` && `~standard` in schema) {\n return schema as StandardSchema<T>\n }\n\n throw new Error(\n `Schema must either implement the standard-schema interface or be a Zod schema`\n )\n }\n\n public getKeyFromItem(item: T): TKey {\n return this.config.getKey(item)\n }\n\n public generateGlobalKey(key: any, item: any): string {\n if (typeof key === `undefined`) {\n throw new Error(\n `An object was created without a defined key: ${JSON.stringify(item)}`\n )\n }\n\n return `KEY::${this.id}/${key}`\n }\n\n private validateData(\n data: unknown,\n type: `insert` | `update`,\n key?: TKey\n ): T | never {\n if (!this.config.schema) return data as T\n\n const standardSchema = this.ensureStandardSchema(this.config.schema)\n\n // For updates, we need to merge with the existing data before validation\n if (type === `update` && key) {\n // Get the existing data for this key\n const existingData = this.get(key)\n\n if (\n existingData &&\n data &&\n typeof data === `object` &&\n typeof existingData === `object`\n ) {\n // Merge the update with the existing data\n const mergedData = Object.assign({}, existingData, data)\n\n // Validate the merged data\n const result = standardSchema[`~standard`].validate(mergedData)\n\n // Ensure validation is synchronous\n if (result instanceof Promise) {\n throw new TypeError(`Schema validation must be synchronous`)\n }\n\n // If validation fails, throw a SchemaValidationError with the issues\n if (`issues` in result && result.issues) {\n const typedIssues = result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) => String(p)),\n }))\n throw new SchemaValidationError(type, typedIssues)\n }\n\n // Return the original update data, not the merged data\n // We only used the merged data for validation\n return data as T\n }\n }\n\n // For inserts or updates without existing data, validate the data directly\n const result = standardSchema[`~standard`].validate(data)\n\n // Ensure validation is synchronous\n if (result instanceof Promise) {\n throw new TypeError(`Schema validation must be synchronous`)\n }\n\n // If validation fails, throw a SchemaValidationError with the issues\n if (`issues` in result && result.issues) {\n const typedIssues = result.issues.map((issue) => ({\n message: issue.message,\n path: issue.path?.map((p) => String(p)),\n }))\n throw new SchemaValidationError(type, typedIssues)\n }\n\n return result.value as T\n }\n\n /**\n * Inserts one or more items into the collection\n * @param items - Single item or array of items to insert\n * @param config - Optional configuration including metadata and custom keys\n * @returns A TransactionType object representing the insert operation(s)\n * @throws {SchemaValidationError} If the data fails schema validation\n * @example\n * // Insert a single item\n * insert({ text: \"Buy groceries\", completed: false })\n *\n * // Insert multiple items\n * insert([\n * { text: \"Buy groceries\", completed: false },\n * { text: \"Walk dog\", completed: false }\n * ])\n *\n * // Insert with custom key\n * insert({ text: \"Buy groceries\" }, { key: \"grocery-task\" })\n */\n insert = (data: T | Array<T>, config?: InsertConfig) => {\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onInsert handler early\n if (!ambientTransaction && !this.config.onInsert) {\n throw new Error(\n `Collection.insert called directly (not within an explicit transaction) but no 'onInsert' handler is configured.`\n )\n }\n\n const items = Array.isArray(data) ? data : [data]\n const mutations: Array<PendingMutation<T>> = []\n\n // Create mutations for each item\n items.forEach((item) => {\n // Validate the data against the schema if one exists\n const validatedData = this.validateData(item, `insert`)\n\n // Check if an item with this ID already exists in the collection\n const key = this.getKeyFromItem(item)\n if (this.has(key)) {\n throw `Cannot insert document with ID \"${key}\" because it already exists in the collection`\n }\n const globalKey = this.generateGlobalKey(key, item)\n\n const mutation: PendingMutation<T> = {\n mutationId: crypto.randomUUID(),\n original: {},\n modified: validatedData,\n changes: validatedData,\n globalKey,\n key,\n metadata: config?.metadata as unknown,\n syncMetadata: this.config.sync.getSyncMetadata?.() || {},\n type: `insert`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this,\n }\n\n mutations.push(mutation)\n })\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n this.transactions.set(ambientTransaction.id, ambientTransaction)\n this.recomputeOptimisticState()\n\n return ambientTransaction\n } else {\n // Create a new transaction with a mutation function that calls the onInsert handler\n const directOpTransaction = new Transaction<T>({\n mutationFn: async (params) => {\n // Call the onInsert handler with the transaction\n return this.config.onInsert!(params)\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n directOpTransaction.commit()\n\n // Add the transaction to the collection's transactions store\n this.transactions.set(directOpTransaction.id, directOpTransaction)\n this.recomputeOptimisticState()\n\n return directOpTransaction\n }\n }\n\n /**\n * Updates one or more items in the collection using a callback function\n * @param items - Single item/key or array of items/keys to update\n * @param configOrCallback - Either update configuration or update callback\n * @param maybeCallback - Update callback if config was provided\n * @returns A Transaction object representing the update operation(s)\n * @throws {SchemaValidationError} If the updated data fails schema validation\n * @example\n * // Update a single item\n * update(todo, (draft) => { draft.completed = true })\n *\n * // Update multiple items\n * update([todo1, todo2], (drafts) => {\n * drafts.forEach(draft => { draft.completed = true })\n * })\n *\n * // Update with metadata\n * update(todo, { metadata: { reason: \"user update\" } }, (draft) => { draft.text = \"Updated text\" })\n */\n\n /**\n * Updates one or more items in the collection using a callback function\n * @param ids - Single ID or array of IDs to update\n * @param configOrCallback - Either update configuration or update callback\n * @param maybeCallback - Update callback if config was provided\n * @returns A Transaction object representing the update operation(s)\n * @throws {SchemaValidationError} If the updated data fails schema validation\n * @example\n * // Update a single item\n * update(\"todo-1\", (draft) => { draft.completed = true })\n *\n * // Update multiple items\n * update([\"todo-1\", \"todo-2\"], (drafts) => {\n * drafts.forEach(draft => { draft.completed = true })\n * })\n *\n * // Update with metadata\n * update(\"todo-1\", { metadata: { reason: \"user update\" } }, (draft) => { draft.text = \"Updated text\" })\n */\n // Overload 1: Update multiple items with a callback\n update<TItem extends object = T>(\n key: Array<TKey | unknown>,\n callback: (drafts: Array<TItem>) => void\n ): TransactionType\n\n // Overload 2: Update multiple items with config and a callback\n update<TItem extends object = T>(\n keys: Array<TKey | unknown>,\n config: OperationConfig,\n callback: (drafts: Array<TItem>) => void\n ): TransactionType\n\n // Overload 3: Update a single item with a callback\n update<TItem extends object = T>(\n id: TKey | unknown,\n callback: (draft: TItem) => void\n ): TransactionType\n\n // Overload 4: Update a single item with config and a callback\n update<TItem extends object = T>(\n id: TKey | unknown,\n config: OperationConfig,\n callback: (draft: TItem) => void\n ): TransactionType\n\n update<TItem extends object = T>(\n keys: (TKey | unknown) | Array<TKey | unknown>,\n configOrCallback: ((draft: TItem | Array<TItem>) => void) | OperationConfig,\n maybeCallback?: (draft: TItem | Array<TItem>) => void\n ) {\n if (typeof keys === `undefined`) {\n throw new Error(`The first argument to update is missing`)\n }\n\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onUpdate handler early\n if (!ambientTransaction && !this.config.onUpdate) {\n throw new Error(\n `Collection.update called directly (not within an explicit transaction) but no 'onUpdate' handler is configured.`\n )\n }\n\n const isArray = Array.isArray(keys)\n const keysArray = isArray ? keys : [keys]\n\n if (isArray && keysArray.length === 0) {\n throw new Error(`No keys were passed to update`)\n }\n\n const callback =\n typeof configOrCallback === `function` ? configOrCallback : maybeCallback!\n const config =\n typeof configOrCallback === `function` ? {} : configOrCallback\n\n // Get the current objects or empty objects if they don't exist\n const currentObjects = keysArray.map((key) => {\n const item = this.get(key)\n if (!item) {\n throw new Error(\n `The key \"${key}\" was passed to update but an object for this key was not found in the collection`\n )\n }\n\n return item\n }) as unknown as Array<TItem>\n\n let changesArray\n if (isArray) {\n // Use the proxy to track changes for all objects\n changesArray = withArrayChangeTracking(\n currentObjects,\n callback as (draft: Array<TItem>) => void\n )\n } else {\n const result = withChangeTracking(\n currentObjects[0]!,\n callback as (draft: TItem) => void\n )\n changesArray = [result]\n }\n\n // Create mutations for each object that has changes\n const mutations: Array<PendingMutation<T>> = keysArray\n .map((key, index) => {\n const itemChanges = changesArray[index] // User-provided changes for this specific item\n\n // Skip items with no changes\n if (!itemChanges || Object.keys(itemChanges).length === 0) {\n return null\n }\n\n const originalItem = currentObjects[index] as unknown as T\n // Validate the user-provided changes for this item\n const validatedUpdatePayload = this.validateData(\n itemChanges,\n `update`,\n key\n )\n\n // Construct the full modified item by applying the validated update payload to the original item\n const modifiedItem = Object.assign(\n {},\n originalItem,\n validatedUpdatePayload\n )\n\n // Check if the ID of the item is being changed\n const originalItemId = this.getKeyFromItem(originalItem)\n const modifiedItemId = this.getKeyFromItem(modifiedItem)\n\n if (originalItemId !== modifiedItemId) {\n throw new Error(\n `Updating the key of an item is not allowed. Original key: \"${originalItemId}\", Attempted new key: \"${modifiedItemId}\". Please delete the old item and create a new one if a key change is necessary.`\n )\n }\n\n const globalKey = this.generateGlobalKey(modifiedItemId, modifiedItem)\n\n return {\n mutationId: crypto.randomUUID(),\n original: originalItem as Record<string, unknown>,\n modified: modifiedItem as Record<string, unknown>,\n changes: validatedUpdatePayload as Record<string, unknown>,\n globalKey,\n key,\n metadata: config.metadata as unknown,\n syncMetadata: (this.syncedMetadata.get(key) || {}) as Record<\n string,\n unknown\n >,\n type: `update`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this,\n }\n })\n .filter(Boolean) as Array<PendingMutation<T>>\n\n // If no changes were made, return an empty transaction early\n if (mutations.length === 0) {\n const emptyTransaction = new Transaction({\n mutationFn: async () => {},\n })\n emptyTransaction.commit()\n return emptyTransaction\n }\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n this.transactions.set(ambientTransaction.id, ambientTransaction)\n this.recomputeOptimisticState()\n\n return ambientTransaction\n }\n\n // No need to check for onUpdate handler here as we've already checked at the beginning\n\n // Create a new transaction with a mutation function that calls the onUpdate handler\n const directOpTransaction = new Transaction<T>({\n mutationFn: async (params) => {\n // Call the onUpdate handler with the transaction\n return this.config.onUpdate!(params)\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n directOpTransaction.commit()\n\n // Add the transaction to the collection's transactions store\n\n this.transactions.set(directOpTransaction.id, directOpTransaction)\n this.recomputeOptimisticState()\n\n return directOpTransaction\n }\n\n /**\n * Deletes one or more items from the collection\n * @param ids - Single ID or array of IDs to delete\n * @param config - Optional configuration including metadata\n * @returns A TransactionType object representing the delete operation(s)\n * @example\n * // Delete a single item\n * delete(\"todo-1\")\n *\n * // Delete multiple items\n * delete([\"todo-1\", \"todo-2\"])\n *\n * // Delete with metadata\n * delete(\"todo-1\", { metadata: { reason: \"completed\" } })\n */\n delete = (\n keys: Array<TKey> | TKey,\n config?: OperationConfig\n ): TransactionType<any> => {\n const ambientTransaction = getActiveTransaction()\n\n // If no ambient transaction exists, check for an onDelete handler early\n if (!ambientTransaction && !this.config.onDelete) {\n throw new Error(\n `Collection.delete called directly (not within an explicit transaction) but no 'onDelete' handler is configured.`\n )\n }\n\n if (Array.isArray(keys) && keys.length === 0) {\n throw new Error(`No keys were passed to delete`)\n }\n\n const keysArray = Array.isArray(keys) ? keys : [keys]\n const mutations: Array<PendingMutation<T>> = []\n\n for (const key of keysArray) {\n const globalKey = this.generateGlobalKey(key, this.get(key)!)\n const mutation: PendingMutation<T> = {\n mutationId: crypto.randomUUID(),\n original: this.get(key) || {},\n modified: this.get(key)!,\n changes: this.get(key) || {},\n globalKey,\n key,\n metadata: config?.metadata as unknown,\n syncMetadata: (this.syncedMetadata.get(key) || {}) as Record<\n string,\n unknown\n >,\n type: `delete`,\n createdAt: new Date(),\n updatedAt: new Date(),\n collection: this,\n }\n\n mutations.push(mutation)\n }\n\n // If an ambient transaction exists, use it\n if (ambientTransaction) {\n ambientTransaction.applyMutations(mutations)\n\n this.transactions.set(ambientTransaction.id, ambientTransaction)\n this.recomputeOptimisticState()\n\n return ambientTransaction\n }\n\n // Create a new transaction with a mutation function that calls the onDelete handler\n const directOpTransaction = new Transaction<T>({\n autoCommit: true,\n mutationFn: async (params) => {\n // Call the onDelete handler with the transaction\n return this.config.onDelete!(params)\n },\n })\n\n // Apply mutations to the new transaction\n directOpTransaction.applyMutations(mutations)\n directOpTransaction.commit()\n\n this.transactions.set(directOpTransaction.id, directOpTransaction)\n this.recomputeOptimisticState()\n\n return directOpTransaction\n }\n\n /**\n * Gets the current state of the collection as a Map\n *\n * @returns A Map containing all items in the collection, with keys as identifiers\n */\n get state() {\n const result = new Map<TKey, T>()\n for (const [key, value] of this.entries()) {\n result.set(key, value)\n }\n return result\n }\n\n /**\n * Gets the current state of the collection as a Map, but only resolves when data is available\n * Waits for the first sync commit to complete before resolving\n *\n * @returns Promise that resolves to a Map containing all items in the collection\n */\n stateWhenReady(): Promise<Map<TKey, T>> {\n // If we already have data or there are no loading collections, resolve immediately\n if (this.size > 0 || this.hasReceivedFirstCommit === true) {\n return Promise.resolve(this.state)\n }\n\n // Otherwise, wait for the first commit\n return new Promise<Map<TKey, T>>((resolve) => {\n this.onFirstCommit(() => {\n resolve(this.state)\n })\n })\n }\n\n /**\n * Gets the current state of the collection as an Array\n *\n * @returns An Array containing all items in the collection\n */\n get toArray() {\n const array = Array.from(this.values())\n\n // Currently a query with an orderBy will add a _orderByIndex to the items\n // so for now we need to sort the array by _orderByIndex if it exists\n // TODO: in the future it would be much better is the keys are sorted - this\n // should be done by the query engine.\n if (array[0] && (array[0] as { _orderByIndex?: number })._orderByIndex) {\n return (array as Array<{ _orderByIndex: number }>).sort(\n (a, b) => a._orderByIndex - b._orderByIndex\n ) as Array<T>\n }\n\n return array\n }\n\n /**\n * Gets the current state of the collection as an Array, but only resolves when data is available\n * Waits for the first sync commit to complete before resolving\n *\n * @returns Promise that resolves to an Array containing all items in the collection\n */\n toArrayWhenReady(): Promise<Array<T>> {\n // If we already have data or there are no loading collections, resolve immediately\n if (this.size > 0 || this.hasReceivedFirstCommit === true) {\n return Promise.resolve(this.toArray)\n }\n\n // Otherwise, wait for the first commit\n return new Promise<Array<T>>((resolve) => {\n this.onFirstCommit(() => {\n resolve(this.toArray)\n })\n })\n }\n\n /**\n * Returns the current state of the collection as an array of changes\n * @returns An array of changes\n */\n public currentStateAsChanges(): Array<ChangeMessage<T>> {\n return Array.from(this.entries()).map(([key, value]) => ({\n type: `insert`,\n key,\n value,\n }))\n }\n\n /**\n * Subscribe to changes in the collection\n * @param callback - A function that will be called with the changes in the collection\n * @returns A function that can be called to unsubscribe from the changes\n */\n public subscribeChanges(\n callback: (changes: Array<ChangeMessage<T>>) => void,\n { includeInitialState = false }: { includeInitialState?: boolean } = {}\n ): () => void {\n if (includeInitialState) {\n // First send the current state as changes\n callback(this.currentStateAsChanges())\n }\n\n // Add to batched listeners\n this.changeListeners.add(callback)\n\n return () => {\n this.changeListeners.delete(callback)\n }\n }\n\n /**\n * Subscribe to changes for a specific key\n */\n public subscribeChangesKey(\n key: TKey,\n listener: ChangeListener<T, TKey>,\n { includeInitialState = false }: { includeInitialState?: boolean } = {}\n ): () => void {\n if (!this.changeKeyListeners.has(key)) {\n this.changeKeyListeners.set(key, new Set())\n }\n\n if (includeInitialState) {\n // First send the current state as changes\n listener([\n {\n type: `insert`,\n key,\n value: this.get(key)!,\n },\n ])\n }\n\n this.changeKeyListeners.get(key)!.add(listener)\n\n return () => {\n const listeners = this.changeKeyListeners.get(key)\n if (listeners) {\n listeners.delete(listener)\n if (listeners.size === 0) {\n this.changeKeyListeners.delete(key)\n }\n }\n }\n }\n\n /**\n * Trigger a recomputation when transactions change\n * This method should be called by the Transaction class when state changes\n */\n public onTransactionStateChange(): void {\n this.recomputeOptimisticState()\n }\n\n private _storeMap: Store<Map<TKey, T>> | undefined\n\n /**\n * Returns a Tanstack Store Map that is updated when the collection changes\n * This is a temporary solution to enable the existing framework hooks to work\n * with the new internals of Collection until they are rewritten.\n * TODO: Remove this once the framework hooks are rewritten.\n */\n public asStoreMap(): Store<Map<TKey, T>> {\n if (!this._storeMap) {\n this._storeMap = new Store(new Map(this.entries()))\n this.subscribeChanges(() => {\n this._storeMap!.setState(() => new Map(this.entries()))\n })\n }\n return this._storeMap\n }\n\n private _storeArray: Store<Array<T>> | undefined\n\n /**\n * Returns a Tanstack Store Array that is updated when the collection changes\n * This is a temporary solution to enable the existing framework hooks to work\n * with the new internals of Collection until they are rewritten.\n * TODO: Remove this once the framework hooks are rewritten.\n */\n public asStoreArray(): Store<Array<T>> {\n if (!this._storeArray) {\n this._storeArray = new Store(this.toArray)\n this.subscribeChanges(() => {\n this._storeArray!.setState(() => this.toArray)\n })\n }\n return this._storeArray\n }\n}\n"],"names":["config","getActiveTransaction","Transaction","SortedMap","result","withArrayChangeTracking","withChangeTracking","Store"],"mappings":"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