@tanstack/db 0.0.22 → 0.0.23
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/collection.cjs +14 -6
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +10 -9
- package/dist/cjs/local-storage.cjs +1 -1
- package/dist/cjs/local-storage.cjs.map +1 -1
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +5 -5
- package/dist/cjs/types.d.cts +35 -10
- package/dist/esm/collection.d.ts +10 -9
- package/dist/esm/collection.js +14 -6
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/local-storage.js +1 -1
- package/dist/esm/local-storage.js.map +1 -1
- package/dist/esm/transactions.d.ts +5 -5
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +35 -10
- package/package.json +1 -1
- package/src/collection.ts +62 -21
- package/src/local-storage.ts +2 -2
- package/src/transactions.ts +8 -12
- package/src/types.ts +69 -14
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n OperationType,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<\n TData extends object = Record<string, unknown>,\n>(config: TransactionConfig<TData>): Transaction<TData> {\n const newTransaction = new Transaction<TData>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<\n T extends object = Record<string, unknown>,\n TOperation extends OperationType = OperationType,\n> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T, TOperation>>\n public isPersisted: Deferred<Transaction<T, TOperation>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T, TOperation>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":";AAWA,MAAM,eAAwC,CAAA;AAC9C,IAAI,mBAA4C,CAAA;AAEhD,IAAI,iBAAiB;AAsDd,SAAS,kBAEd,QAAsD;AACtD,QAAM,iBAAiB,IAAI,YAAmB,MAAM;AACpD,eAAa,KAAK,cAAc;AAChC,SAAO;AACT;AAaO,SAAS,uBAAgD;AAC9D,MAAI,iBAAiB,SAAS,GAAG;AAC/B,WAAO,iBAAiB,MAAM,EAAE,EAAE,CAAC;AAAA,EACrC,OAAO;AACL,WAAO;AAAA,EACT;AACF;AAEA,SAAS,oBAAoB,IAAsB;AACjD,mBAAiB,KAAK,EAAE;AAC1B;AAEA,SAAS,sBAAsB,IAAsB;AACnD,qBAAmB,iBAAiB,OAAO,CAAC,MAAM,EAAE,OAAO,GAAG,EAAE;AAClE;AAEA,SAAS,sBAAsB,IAAsB;AACnD,QAAM,QAAQ,aAAa,UAAU,CAAC,MAAM,EAAE,OAAO,GAAG,EAAE;AAC1D,MAAI,UAAU,IAAI;AAChB,iBAAa,OAAO,OAAO,CAAC;AAAA,EAC9B;AACF;AAEA,MAAM,YAGJ;AAAA,EAeA,YAAY,QAA8B;AACxC,QAAI,OAAO,OAAO,eAAe,aAAa;AAC5C,YAAM;AAAA,IACR;AACA,SAAK,KAAK,OAAO,MAAM,OAAO,WAAA;AAC9B,SAAK,aAAa,OAAO;AACzB,SAAK,QAAQ;AACb,SAAK,YAAY,CAAA;AACjB,SAAK,cAAc,eAAA;AACnB,SAAK,aAAa,OAAO,cAAc;AACvC,SAAK,gCAAgB,KAAA;AACrB,SAAK,iBAAiB;AACtB,SAAK,WAAW,OAAO,YAAY,CAAA;AAAA,EACrC;AAAA,EAEA,SAAS,UAA4B;AACnC,SAAK,QAAQ;AAEb,QAAI,aAAa,eAAe,aAAa,UAAU;AACrD,4BAAsB,IAAI;AAAA,IAC5B;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCA,OAAO,UAAsC;AAC3C,QAAI,KAAK,UAAU,WAAW;AAC5B,YAAM;AAAA,IACR;AAEA,wBAAoB,IAAI;AACxB,QAAI;AACF,eAAA;AAAA,IACF,UAAA;AACE,4BAAsB,IAAI;AAAA,IAC5B;AAEA,QAAI,KAAK,YAAY;AACnB,WAAK,OAAA;AAAA,IACP;AAEA,WAAO;AAAA,EACT;AAAA,EAEA,eAAe,WAA8C;AAC3D,eAAW,eAAe,WAAW;AACnC,YAAM,gBAAgB,KAAK,UAAU;AAAA,QACnC,CAAC,MAAM,EAAE,cAAc,YAAY;AAAA,MAAA;AAGrC,UAAI,iBAAiB,GAAG;AAEtB,aAAK,UAAU,aAAa,IAAI;AAAA,MAClC,OAAO;AAEL,aAAK,UAAU,KAAK,WAAW;AAAA,MACjC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAwCA,SAAS,QAA4D;;AACnE,UAAM,uBAAsB,iCAAQ,wBAAuB;AAC3D,QAAI,KAAK,UAAU,aAAa;AAC9B,YAAM;AAAA,IACR;AAEA,SAAK,SAAS,QAAQ;AAItB,QAAI,CAAC,qBAAqB;AACxB,YAAM,kCAAkB,IAAA;AACxB,WAAK,UAAU,QAAQ,CAAC,MAAM,YAAY,IAAI,EAAE,SAAS,CAAC;AAC1D,iBAAW,KAAK,cAAc;AAC5B,UAAE,UAAU,aACV,EAAE,UAAU,KAAK,CAAC,MAAM,YAAY,IAAI,EAAE,SAAS,CAAC,KACpD,EAAE,SAAS,EAAE,qBAAqB,MAAM;AAAA,MAC5C;AAAA,IACF;AAGA,SAAK,YAAY,QAAO,UAAK,UAAL,mBAAY,KAAK;AACzC,SAAK,gBAAA;AAEL,WAAO;AAAA,EACT;AAAA;AAAA,EAGA,kBAAwB;AACtB,UAAM,gCAAgB,IAAA;AACtB,eAAW,YAAY,KAAK,WAAW;AACrC,UAAI,CAAC,UAAU,IAAI,SAAS,WAAW,EAAE,GAAG;AAC1C,iBAAS,WAAW,yBAAA;AAGpB,YAAI,SAAS,WAAW,0BAA0B,SAAS,GAAG;AAC5D,mBAAS,WAAW,0BAAA;AAAA,QACtB;AAEA,kBAAU,IAAI,SAAS,WAAW,EAAE;AAAA,MACtC;AAAA,IACF;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAyCA,MAAM,SAAkC;AACtC,QAAI,KAAK,UAAU,WAAW;AAC5B,YAAM;AAAA,IACR;AAEA,SAAK,SAAS,YAAY;AAE1B,QAAI,KAAK,UAAU,WAAW,GAAG;AAC/B,WAAK,SAAS,WAAW;AAEzB,aAAO;AAAA,IACT;AAGA,QAAI;AAIF,YAAM,KAAK,WAAW;AAAA,QACpB,aAAa;AAAA,MAAA,CACd;AAED,WAAK,SAAS,WAAW;AACzB,WAAK,gBAAA;AAEL,WAAK,YAAY,QAAQ,IAAI;AAAA,IAC/B,SAAS,OAAO;AAEd,WAAK,QAAQ;AAAA,QACX,SAAS,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK;AAAA,QAC9D,OAAO,iBAAiB,QAAQ,QAAQ,IAAI,MAAM,OAAO,KAAK,CAAC;AAAA,MAAA;AAIjE,aAAO,KAAK,SAAA;AAAA,IACd;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAQA,iBAAiB,OAAiC;AAChD,UAAM,sBACJ,KAAK,UAAU,YAAY,MAAM,UAAU,QAAA;AAC7C,QAAI,wBAAwB,GAAG;AAC7B,aAAO;AAAA,IACT;AACA,WAAO,KAAK,iBAAiB,MAAM;AAAA,EACrC;AACF;"}
|
|
1
|
+
{"version":3,"file":"transactions.js","sources":["../../src/transactions.ts"],"sourcesContent":["import { createDeferred } from \"./deferred\"\nimport type { Deferred } from \"./deferred\"\nimport type {\n MutationFn,\n PendingMutation,\n TransactionConfig,\n TransactionState,\n TransactionWithMutations,\n} from \"./types\"\n\nconst transactions: Array<Transaction<any>> = []\nlet transactionStack: Array<Transaction<any>> = []\n\nlet sequenceNumber = 0\n\n/**\n * Creates a new transaction for grouping multiple collection operations\n * @param config - Transaction configuration with mutation function\n * @returns A new Transaction instance\n * @example\n * // Basic transaction usage\n * const tx = createTransaction({\n * mutationFn: async ({ transaction }) => {\n * // Send all mutations to API\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle transaction errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"New item\" })\n * })\n *\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async () => {\n * // API call\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later\n * await tx.commit()\n */\nexport function createTransaction<T extends object = Record<string, unknown>>(\n config: TransactionConfig<T>\n): Transaction<T> {\n const newTransaction = new Transaction<T>(config)\n transactions.push(newTransaction)\n return newTransaction\n}\n\n/**\n * Gets the currently active ambient transaction, if any\n * Used internally by collection operations to join existing transactions\n * @returns The active transaction or undefined if none is active\n * @example\n * // Check if operations will join an ambient transaction\n * const ambientTx = getActiveTransaction()\n * if (ambientTx) {\n * console.log('Operations will join transaction:', ambientTx.id)\n * }\n */\nexport function getActiveTransaction(): Transaction | undefined {\n if (transactionStack.length > 0) {\n return transactionStack.slice(-1)[0]\n } else {\n return undefined\n }\n}\n\nfunction registerTransaction(tx: Transaction<any>) {\n transactionStack.push(tx)\n}\n\nfunction unregisterTransaction(tx: Transaction<any>) {\n transactionStack = transactionStack.filter((t) => t.id !== tx.id)\n}\n\nfunction removeFromPendingList(tx: Transaction<any>) {\n const index = transactions.findIndex((t) => t.id === tx.id)\n if (index !== -1) {\n transactions.splice(index, 1)\n }\n}\n\nclass Transaction<T extends object = Record<string, unknown>> {\n public id: string\n public state: TransactionState\n public mutationFn: MutationFn<T>\n public mutations: Array<PendingMutation<T>>\n public isPersisted: Deferred<Transaction<T>>\n public autoCommit: boolean\n public createdAt: Date\n public sequenceNumber: number\n public metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n this.id = config.id ?? crypto.randomUUID()\n this.mutationFn = config.mutationFn\n this.state = `pending`\n this.mutations = []\n this.isPersisted = createDeferred<Transaction<T>>()\n this.autoCommit = config.autoCommit ?? true\n this.createdAt = new Date()\n this.sequenceNumber = sequenceNumber++\n this.metadata = config.metadata ?? {}\n }\n\n setState(newState: TransactionState) {\n this.state = newState\n\n if (newState === `completed` || newState === `failed`) {\n removeFromPendingList(this)\n }\n }\n\n /**\n * Execute collection operations within this transaction\n * @param callback - Function containing collection operations to group together\n * @returns This transaction for chaining\n * @example\n * // Group multiple operations\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * collection.update(\"2\", draft => { draft.completed = true })\n * collection.delete(\"3\")\n * })\n *\n * await tx.isPersisted.promise\n *\n * @example\n * // Handle mutate errors\n * try {\n * tx.mutate(() => {\n * collection.insert({ id: \"invalid\" }) // This might throw\n * })\n * } catch (error) {\n * console.log('Mutation failed:', error)\n * }\n *\n * @example\n * // Manual commit control\n * const tx = createTransaction({ autoCommit: false, mutationFn: async () => {} })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * // Commit later when ready\n * await tx.commit()\n */\n mutate(callback: () => void): Transaction<T> {\n if (this.state !== `pending`) {\n throw `You can no longer call .mutate() as the transaction is no longer pending`\n }\n\n registerTransaction(this)\n try {\n callback()\n } finally {\n unregisterTransaction(this)\n }\n\n if (this.autoCommit) {\n this.commit()\n }\n\n return this\n }\n\n applyMutations(mutations: Array<PendingMutation<any>>): void {\n for (const newMutation of mutations) {\n const existingIndex = this.mutations.findIndex(\n (m) => m.globalKey === newMutation.globalKey\n )\n\n if (existingIndex >= 0) {\n // Replace existing mutation\n this.mutations[existingIndex] = newMutation\n } else {\n // Insert new mutation\n this.mutations.push(newMutation)\n }\n }\n }\n\n /**\n * Rollback the transaction and any conflicting transactions\n * @param config - Configuration for rollback behavior\n * @returns This transaction for chaining\n * @example\n * // Manual rollback\n * const tx = createTransaction({ mutationFn: async () => {\n * // Send to API\n * }})\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * // Rollback if needed\n * if (shouldCancel) {\n * tx.rollback()\n * }\n *\n * @example\n * // Handle rollback cascade (automatic)\n * const tx1 = createTransaction({ mutationFn: async () => {} })\n * const tx2 = createTransaction({ mutationFn: async () => {} })\n *\n * tx1.mutate(() => collection.update(\"1\", draft => { draft.value = \"A\" }))\n * tx2.mutate(() => collection.update(\"1\", draft => { draft.value = \"B\" })) // Same item\n *\n * tx1.rollback() // This will also rollback tx2 due to conflict\n *\n * @example\n * // Handle rollback in error scenarios\n * try {\n * await tx.isPersisted.promise\n * } catch (error) {\n * console.log('Transaction was rolled back:', error)\n * // Transaction automatically rolled back on mutation function failure\n * }\n */\n rollback(config?: { isSecondaryRollback?: boolean }): Transaction<T> {\n const isSecondaryRollback = config?.isSecondaryRollback ?? false\n if (this.state === `completed`) {\n throw `You can no longer call .rollback() as the transaction is already completed`\n }\n\n this.setState(`failed`)\n\n // See if there's any other transactions w/ mutations on the same ids\n // and roll them back as well.\n if (!isSecondaryRollback) {\n const mutationIds = new Set()\n this.mutations.forEach((m) => mutationIds.add(m.globalKey))\n for (const t of transactions) {\n t.state === `pending` &&\n t.mutations.some((m) => mutationIds.has(m.globalKey)) &&\n t.rollback({ isSecondaryRollback: true })\n }\n }\n\n // Reject the promise\n this.isPersisted.reject(this.error?.error)\n this.touchCollection()\n\n return this\n }\n\n // Tell collection that something has changed with the transaction\n touchCollection(): void {\n const hasCalled = new Set()\n for (const mutation of this.mutations) {\n if (!hasCalled.has(mutation.collection.id)) {\n mutation.collection.onTransactionStateChange()\n\n // Only call commitPendingTransactions if there are pending sync transactions\n if (mutation.collection.pendingSyncedTransactions.length > 0) {\n mutation.collection.commitPendingTransactions()\n }\n\n hasCalled.add(mutation.collection.id)\n }\n }\n }\n\n /**\n * Commit the transaction and execute the mutation function\n * @returns Promise that resolves to this transaction when complete\n * @example\n * // Manual commit (when autoCommit is false)\n * const tx = createTransaction({\n * autoCommit: false,\n * mutationFn: async ({ transaction }) => {\n * await api.saveChanges(transaction.mutations)\n * }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Buy milk\" })\n * })\n *\n * await tx.commit() // Manually commit\n *\n * @example\n * // Handle commit errors\n * try {\n * const tx = createTransaction({\n * mutationFn: async () => { throw new Error(\"API failed\") }\n * })\n *\n * tx.mutate(() => {\n * collection.insert({ id: \"1\", text: \"Item\" })\n * })\n *\n * await tx.commit()\n * } catch (error) {\n * console.log('Commit failed, transaction rolled back:', error)\n * }\n *\n * @example\n * // Check transaction state after commit\n * await tx.commit()\n * console.log(tx.state) // \"completed\" or \"failed\"\n */\n async commit(): Promise<Transaction<T>> {\n if (this.state !== `pending`) {\n throw `You can no longer call .commit() as the transaction is no longer pending`\n }\n\n this.setState(`persisting`)\n\n if (this.mutations.length === 0) {\n this.setState(`completed`)\n\n return this\n }\n\n // Run mutationFn\n try {\n // At this point we know there's at least one mutation\n // We've already verified mutations is non-empty, so this cast is safe\n // Use a direct type assertion instead of object spreading to preserve the original type\n await this.mutationFn({\n transaction: this as unknown as TransactionWithMutations<T>,\n })\n\n this.setState(`completed`)\n this.touchCollection()\n\n this.isPersisted.resolve(this)\n } catch (error) {\n // Update transaction with error information\n this.error = {\n message: error instanceof Error ? error.message : String(error),\n error: error instanceof Error ? error : new Error(String(error)),\n }\n\n // rollback the transaction\n return this.rollback()\n }\n\n return this\n }\n\n /**\n * Compare two transactions by their createdAt time and sequence number in order\n * to sort them in the order they were created.\n * @param other - The other transaction to compare to\n * @returns -1 if this transaction was created before the other, 1 if it was created after, 0 if they were created at the same time\n */\n compareCreatedAt(other: Transaction<any>): number {\n const createdAtComparison =\n this.createdAt.getTime() - other.createdAt.getTime()\n if (createdAtComparison !== 0) {\n return createdAtComparison\n }\n return this.sequenceNumber - other.sequenceNumber\n }\n}\n\nexport type { Transaction }\n"],"names":[],"mappings":"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"}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -8,6 +8,24 @@ import { Transaction } from './transactions.js';
|
|
|
8
8
|
* @internal This is used by the type resolution system
|
|
9
9
|
*/
|
|
10
10
|
export type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
|
|
11
|
+
/**
|
|
12
|
+
* Helper type to extract the input type from a standard schema
|
|
13
|
+
*
|
|
14
|
+
* @internal This is used for collection insert type inference
|
|
15
|
+
*/
|
|
16
|
+
export type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown>;
|
|
17
|
+
/**
|
|
18
|
+
* Helper type to determine the insert input type
|
|
19
|
+
* This takes the raw generics (TExplicit, TSchema, TFallback) instead of the resolved T.
|
|
20
|
+
*
|
|
21
|
+
* Priority:
|
|
22
|
+
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
23
|
+
* 2. Schema input type (if schema provided)
|
|
24
|
+
* 3. Fallback type TFallback
|
|
25
|
+
*
|
|
26
|
+
* @internal This is used for collection insert type inference
|
|
27
|
+
*/
|
|
28
|
+
export type ResolveInsertInput<TExplicit = unknown, TSchema extends StandardSchemaV1 = never, TFallback extends object = Record<string, unknown>> = unknown extends TExplicit ? [TSchema] extends [never] ? TFallback : InferSchemaInput<TSchema> : TExplicit extends object ? TExplicit : Record<string, unknown>;
|
|
11
29
|
/**
|
|
12
30
|
* Helper type to determine the final type based on priority:
|
|
13
31
|
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
@@ -28,25 +46,32 @@ export type Fn = (...args: Array<any>) => any;
|
|
|
28
46
|
* A record of utility functions that can be attached to a collection
|
|
29
47
|
*/
|
|
30
48
|
export type UtilsRecord = Record<string, Fn>;
|
|
49
|
+
/**
|
|
50
|
+
*
|
|
51
|
+
* @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity.
|
|
52
|
+
*
|
|
53
|
+
* @see https://github.com/TanStack/db/pull/209#issuecomment-3053001206
|
|
54
|
+
*/
|
|
55
|
+
export type ResolveTransactionChanges<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = TOperation extends `delete` ? T : Partial<T>;
|
|
31
56
|
/**
|
|
32
57
|
* Represents a pending mutation within a transaction
|
|
33
58
|
* Contains information about the original and modified data, as well as metadata
|
|
34
59
|
*/
|
|
35
|
-
export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> {
|
|
60
|
+
export interface PendingMutation<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType, TCollection extends Collection<T, any, any, any, any> = Collection<T, any, any, any, any>> {
|
|
36
61
|
mutationId: string;
|
|
37
62
|
original: TOperation extends `insert` ? {} : T;
|
|
38
63
|
modified: T;
|
|
39
|
-
changes:
|
|
64
|
+
changes: ResolveTransactionChanges<T, TOperation>;
|
|
40
65
|
globalKey: string;
|
|
41
66
|
key: any;
|
|
42
|
-
type:
|
|
67
|
+
type: TOperation;
|
|
43
68
|
metadata: unknown;
|
|
44
69
|
syncMetadata: Record<string, unknown>;
|
|
45
70
|
/** Whether this mutation should be applied optimistically (defaults to true) */
|
|
46
71
|
optimistic: boolean;
|
|
47
72
|
createdAt: Date;
|
|
48
73
|
updatedAt: Date;
|
|
49
|
-
collection:
|
|
74
|
+
collection: TCollection;
|
|
50
75
|
}
|
|
51
76
|
/**
|
|
52
77
|
* Configuration options for creating a new transaction
|
|
@@ -63,7 +88,7 @@ export type NonEmptyArray<T> = [T, ...Array<T>];
|
|
|
63
88
|
* Utility type for a Transaction with at least one mutation
|
|
64
89
|
* This is used internally by the Transaction.commit method
|
|
65
90
|
*/
|
|
66
|
-
export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T
|
|
91
|
+
export type TransactionWithMutations<T extends object = Record<string, unknown>, TOperation extends OperationType = OperationType> = Transaction<T> & {
|
|
67
92
|
mutations: NonEmptyArray<PendingMutation<T, TOperation>>;
|
|
68
93
|
};
|
|
69
94
|
export interface TransactionConfig<T extends object = Record<string, unknown>> {
|
|
@@ -77,11 +102,11 @@ export interface TransactionConfig<T extends object = Record<string, unknown>> {
|
|
|
77
102
|
/**
|
|
78
103
|
* Options for the createOptimisticAction helper
|
|
79
104
|
*/
|
|
80
|
-
export interface CreateOptimisticActionsOptions<TVars = unknown
|
|
105
|
+
export interface CreateOptimisticActionsOptions<TVars = unknown, T extends object = Record<string, unknown>> extends Omit<TransactionConfig<T>, `mutationFn`> {
|
|
81
106
|
/** Function to apply optimistic updates locally before the mutation completes */
|
|
82
107
|
onMutate: (vars: TVars) => void;
|
|
83
108
|
/** Function to execute the mutation on the server */
|
|
84
|
-
mutationFn: (vars: TVars, params: MutationFnParams) => Promise<any>;
|
|
109
|
+
mutationFn: (vars: TVars, params: MutationFnParams<T>) => Promise<any>;
|
|
85
110
|
}
|
|
86
111
|
export type { Transaction };
|
|
87
112
|
type Value<TExtensions = never> = string | number | boolean | bigint | null | TExtensions | Array<Value<TExtensions>> | {
|
|
@@ -91,7 +116,7 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>;
|
|
|
91
116
|
export type OperationType = `insert` | `update` | `delete`;
|
|
92
117
|
export interface SyncConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
|
|
93
118
|
sync: (params: {
|
|
94
|
-
collection: Collection<T, TKey>;
|
|
119
|
+
collection: Collection<T, TKey, any, any, any>;
|
|
95
120
|
begin: () => void;
|
|
96
121
|
write: (message: Omit<ChangeMessage<T>, `key`>) => void;
|
|
97
122
|
commit: () => void;
|
|
@@ -189,7 +214,7 @@ export type CollectionStatus =
|
|
|
189
214
|
| `error`
|
|
190
215
|
/** Collection has been cleaned up and resources freed */
|
|
191
216
|
| `cleaned-up`;
|
|
192
|
-
export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1> {
|
|
217
|
+
export interface CollectionConfig<T extends object = Record<string, unknown>, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = StandardSchemaV1, TInsertInput extends object = T> {
|
|
193
218
|
id?: string;
|
|
194
219
|
sync: SyncConfig<T, TKey>;
|
|
195
220
|
schema?: TSchema;
|
|
@@ -265,7 +290,7 @@ export interface CollectionConfig<T extends object = Record<string, unknown>, TK
|
|
|
265
290
|
* })
|
|
266
291
|
* }
|
|
267
292
|
*/
|
|
268
|
-
onInsert?: InsertMutationFn<
|
|
293
|
+
onInsert?: InsertMutationFn<TInsertInput, TKey>;
|
|
269
294
|
/**
|
|
270
295
|
* Optional asynchronous handler function called before an update operation
|
|
271
296
|
* @param params Object containing transaction and collection information
|
package/package.json
CHANGED
package/src/collection.ts
CHANGED
|
@@ -12,15 +12,17 @@ import type {
|
|
|
12
12
|
OperationConfig,
|
|
13
13
|
OptimisticChangeMessage,
|
|
14
14
|
PendingMutation,
|
|
15
|
+
ResolveInsertInput,
|
|
15
16
|
ResolveType,
|
|
16
17
|
StandardSchema,
|
|
17
18
|
Transaction as TransactionType,
|
|
19
|
+
TransactionWithMutations,
|
|
18
20
|
UtilsRecord,
|
|
19
21
|
} from "./types"
|
|
20
22
|
import type { StandardSchemaV1 } from "@standard-schema/spec"
|
|
21
23
|
|
|
22
24
|
// Store collections in memory
|
|
23
|
-
export const collectionsStore = new Map<string, CollectionImpl<any, any>>()
|
|
25
|
+
export const collectionsStore = new Map<string, CollectionImpl<any, any, any>>()
|
|
24
26
|
|
|
25
27
|
interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
26
28
|
committed: boolean
|
|
@@ -32,12 +34,15 @@ interface PendingSyncedTransaction<T extends object = Record<string, unknown>> {
|
|
|
32
34
|
* @template T - The type of items in the collection
|
|
33
35
|
* @template TKey - The type of the key for the collection
|
|
34
36
|
* @template TUtils - The utilities record type
|
|
37
|
+
* @template TInsertInput - The type for insert operations (can be different from T for schemas with defaults)
|
|
35
38
|
*/
|
|
36
39
|
export interface Collection<
|
|
37
40
|
T extends object = Record<string, unknown>,
|
|
38
41
|
TKey extends string | number = string | number,
|
|
39
42
|
TUtils extends UtilsRecord = {},
|
|
40
|
-
|
|
43
|
+
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
44
|
+
TInsertInput extends object = T,
|
|
45
|
+
> extends CollectionImpl<T, TKey, TUtils, TSchema, TInsertInput> {
|
|
41
46
|
readonly utils: TUtils
|
|
42
47
|
}
|
|
43
48
|
|
|
@@ -124,12 +129,22 @@ export function createCollection<
|
|
|
124
129
|
options: CollectionConfig<
|
|
125
130
|
ResolveType<TExplicit, TSchema, TFallback>,
|
|
126
131
|
TKey,
|
|
127
|
-
TSchema
|
|
132
|
+
TSchema,
|
|
133
|
+
ResolveInsertInput<TExplicit, TSchema, TFallback>
|
|
128
134
|
> & { utils?: TUtils }
|
|
129
|
-
): Collection<
|
|
135
|
+
): Collection<
|
|
136
|
+
ResolveType<TExplicit, TSchema, TFallback>,
|
|
137
|
+
TKey,
|
|
138
|
+
TUtils,
|
|
139
|
+
TSchema,
|
|
140
|
+
ResolveInsertInput<TExplicit, TSchema, TFallback>
|
|
141
|
+
> {
|
|
130
142
|
const collection = new CollectionImpl<
|
|
131
143
|
ResolveType<TExplicit, TSchema, TFallback>,
|
|
132
|
-
TKey
|
|
144
|
+
TKey,
|
|
145
|
+
TUtils,
|
|
146
|
+
TSchema,
|
|
147
|
+
ResolveInsertInput<TExplicit, TSchema, TFallback>
|
|
133
148
|
>(options)
|
|
134
149
|
|
|
135
150
|
// Copy utils to both top level and .utils namespace
|
|
@@ -142,7 +157,9 @@ export function createCollection<
|
|
|
142
157
|
return collection as Collection<
|
|
143
158
|
ResolveType<TExplicit, TSchema, TFallback>,
|
|
144
159
|
TKey,
|
|
145
|
-
TUtils
|
|
160
|
+
TUtils,
|
|
161
|
+
TSchema,
|
|
162
|
+
ResolveInsertInput<TExplicit, TSchema, TFallback>
|
|
146
163
|
>
|
|
147
164
|
}
|
|
148
165
|
|
|
@@ -179,8 +196,10 @@ export class CollectionImpl<
|
|
|
179
196
|
T extends object = Record<string, unknown>,
|
|
180
197
|
TKey extends string | number = string | number,
|
|
181
198
|
TUtils extends UtilsRecord = {},
|
|
199
|
+
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
200
|
+
TInsertInput extends object = T,
|
|
182
201
|
> {
|
|
183
|
-
public config: CollectionConfig<T, TKey,
|
|
202
|
+
public config: CollectionConfig<T, TKey, TSchema, TInsertInput>
|
|
184
203
|
|
|
185
204
|
// Core state - make public for testing
|
|
186
205
|
public transactions: SortedMap<string, Transaction<any>>
|
|
@@ -312,7 +331,7 @@ export class CollectionImpl<
|
|
|
312
331
|
* @param config - Configuration object for the collection
|
|
313
332
|
* @throws Error if sync config is missing
|
|
314
333
|
*/
|
|
315
|
-
constructor(config: CollectionConfig<T, TKey,
|
|
334
|
+
constructor(config: CollectionConfig<T, TKey, TSchema, TInsertInput>) {
|
|
316
335
|
// eslint-disable-next-line
|
|
317
336
|
if (!config) {
|
|
318
337
|
throw new Error(`Collection requires a config`)
|
|
@@ -1322,9 +1341,11 @@ export class CollectionImpl<
|
|
|
1322
1341
|
* console.log('Insert failed:', error)
|
|
1323
1342
|
* }
|
|
1324
1343
|
*/
|
|
1325
|
-
insert = (
|
|
1344
|
+
insert = (
|
|
1345
|
+
data: TInsertInput | Array<TInsertInput>,
|
|
1346
|
+
config?: InsertConfig
|
|
1347
|
+
) => {
|
|
1326
1348
|
this.validateCollectionUsable(`insert`)
|
|
1327
|
-
|
|
1328
1349
|
const ambientTransaction = getActiveTransaction()
|
|
1329
1350
|
|
|
1330
1351
|
// If no ambient transaction exists, check for an onInsert handler early
|
|
@@ -1335,7 +1356,7 @@ export class CollectionImpl<
|
|
|
1335
1356
|
}
|
|
1336
1357
|
|
|
1337
1358
|
const items = Array.isArray(data) ? data : [data]
|
|
1338
|
-
const mutations: Array<PendingMutation<T
|
|
1359
|
+
const mutations: Array<PendingMutation<T>> = []
|
|
1339
1360
|
|
|
1340
1361
|
// Create mutations for each item
|
|
1341
1362
|
items.forEach((item) => {
|
|
@@ -1343,7 +1364,7 @@ export class CollectionImpl<
|
|
|
1343
1364
|
const validatedData = this.validateData(item, `insert`)
|
|
1344
1365
|
|
|
1345
1366
|
// Check if an item with this ID already exists in the collection
|
|
1346
|
-
const key = this.getKeyFromItem(
|
|
1367
|
+
const key = this.getKeyFromItem(validatedData)
|
|
1347
1368
|
if (this.has(key)) {
|
|
1348
1369
|
throw `Cannot insert document with ID "${key}" because it already exists in the collection`
|
|
1349
1370
|
}
|
|
@@ -1353,7 +1374,15 @@ export class CollectionImpl<
|
|
|
1353
1374
|
mutationId: crypto.randomUUID(),
|
|
1354
1375
|
original: {},
|
|
1355
1376
|
modified: validatedData,
|
|
1356
|
-
|
|
1377
|
+
// Pick the values from validatedData based on what's passed in - this is for cases
|
|
1378
|
+
// where a schema has default values. The validated data has the extra default
|
|
1379
|
+
// values but for changes, we just want to show the data that was actually passed in.
|
|
1380
|
+
changes: Object.fromEntries(
|
|
1381
|
+
Object.keys(item).map((k) => [
|
|
1382
|
+
k,
|
|
1383
|
+
validatedData[k as keyof typeof validatedData],
|
|
1384
|
+
])
|
|
1385
|
+
) as TInsertInput,
|
|
1357
1386
|
globalKey,
|
|
1358
1387
|
key,
|
|
1359
1388
|
metadata: config?.metadata as unknown,
|
|
@@ -1381,8 +1410,12 @@ export class CollectionImpl<
|
|
|
1381
1410
|
const directOpTransaction = createTransaction<T>({
|
|
1382
1411
|
mutationFn: async (params) => {
|
|
1383
1412
|
// Call the onInsert handler with the transaction and collection
|
|
1384
|
-
return this.config.onInsert!({
|
|
1385
|
-
|
|
1413
|
+
return await this.config.onInsert!({
|
|
1414
|
+
transaction:
|
|
1415
|
+
params.transaction as unknown as TransactionWithMutations<
|
|
1416
|
+
TInsertInput,
|
|
1417
|
+
`insert`
|
|
1418
|
+
>,
|
|
1386
1419
|
collection: this as unknown as Collection<T, TKey, TUtils>,
|
|
1387
1420
|
})
|
|
1388
1421
|
},
|
|
@@ -1526,7 +1559,7 @@ export class CollectionImpl<
|
|
|
1526
1559
|
}
|
|
1527
1560
|
|
|
1528
1561
|
// Create mutations for each object that has changes
|
|
1529
|
-
const mutations: Array<PendingMutation<T, `update
|
|
1562
|
+
const mutations: Array<PendingMutation<T, `update`, this>> = keysArray
|
|
1530
1563
|
.map((key, index) => {
|
|
1531
1564
|
const itemChanges = changesArray[index] // User-provided changes for this specific item
|
|
1532
1565
|
|
|
@@ -1581,7 +1614,7 @@ export class CollectionImpl<
|
|
|
1581
1614
|
collection: this,
|
|
1582
1615
|
}
|
|
1583
1616
|
})
|
|
1584
|
-
.filter(Boolean) as Array<PendingMutation<T, `update
|
|
1617
|
+
.filter(Boolean) as Array<PendingMutation<T, `update`, this>>
|
|
1585
1618
|
|
|
1586
1619
|
// If no changes were made, return an empty transaction early
|
|
1587
1620
|
if (mutations.length === 0) {
|
|
@@ -1609,7 +1642,11 @@ export class CollectionImpl<
|
|
|
1609
1642
|
mutationFn: async (params) => {
|
|
1610
1643
|
// Call the onUpdate handler with the transaction and collection
|
|
1611
1644
|
return this.config.onUpdate!({
|
|
1612
|
-
|
|
1645
|
+
transaction:
|
|
1646
|
+
params.transaction as unknown as TransactionWithMutations<
|
|
1647
|
+
T,
|
|
1648
|
+
`update`
|
|
1649
|
+
>,
|
|
1613
1650
|
collection: this as unknown as Collection<T, TKey, TUtils>,
|
|
1614
1651
|
})
|
|
1615
1652
|
},
|
|
@@ -1677,7 +1714,7 @@ export class CollectionImpl<
|
|
|
1677
1714
|
}
|
|
1678
1715
|
|
|
1679
1716
|
const keysArray = Array.isArray(keys) ? keys : [keys]
|
|
1680
|
-
const mutations: Array<PendingMutation<T, `delete
|
|
1717
|
+
const mutations: Array<PendingMutation<T, `delete`, this>> = []
|
|
1681
1718
|
|
|
1682
1719
|
for (const key of keysArray) {
|
|
1683
1720
|
if (!this.has(key)) {
|
|
@@ -1686,7 +1723,7 @@ export class CollectionImpl<
|
|
|
1686
1723
|
)
|
|
1687
1724
|
}
|
|
1688
1725
|
const globalKey = this.generateGlobalKey(key, this.get(key)!)
|
|
1689
|
-
const mutation: PendingMutation<T, `delete
|
|
1726
|
+
const mutation: PendingMutation<T, `delete`, this> = {
|
|
1690
1727
|
mutationId: crypto.randomUUID(),
|
|
1691
1728
|
original: this.get(key)!,
|
|
1692
1729
|
modified: this.get(key)!,
|
|
@@ -1724,7 +1761,11 @@ export class CollectionImpl<
|
|
|
1724
1761
|
mutationFn: async (params) => {
|
|
1725
1762
|
// Call the onDelete handler with the transaction and collection
|
|
1726
1763
|
return this.config.onDelete!({
|
|
1727
|
-
|
|
1764
|
+
transaction:
|
|
1765
|
+
params.transaction as unknown as TransactionWithMutations<
|
|
1766
|
+
T,
|
|
1767
|
+
`delete`
|
|
1768
|
+
>,
|
|
1728
1769
|
collection: this as unknown as Collection<T, TKey, TUtils>,
|
|
1729
1770
|
})
|
|
1730
1771
|
},
|
package/src/local-storage.ts
CHANGED
|
@@ -393,7 +393,7 @@ export function localStorageCollectionOptions<
|
|
|
393
393
|
// Remove items
|
|
394
394
|
params.transaction.mutations.forEach((mutation) => {
|
|
395
395
|
// For delete operations, mutation.original contains the full object
|
|
396
|
-
const key = config.getKey(mutation.original)
|
|
396
|
+
const key = config.getKey(mutation.original as ResolvedType)
|
|
397
397
|
currentData.delete(key)
|
|
398
398
|
})
|
|
399
399
|
|
|
@@ -506,7 +506,7 @@ function createLocalStorageSync<T extends object>(
|
|
|
506
506
|
storageKey: string,
|
|
507
507
|
storage: StorageApi,
|
|
508
508
|
storageEventApi: StorageEventApi,
|
|
509
|
-
|
|
509
|
+
_getKey: (item: T) => string | number,
|
|
510
510
|
lastKnownData: Map<string | number, StoredItem<T>>
|
|
511
511
|
): SyncConfig<T> & { manualTrigger?: () => void } {
|
|
512
512
|
let syncParams: Parameters<SyncConfig<T>[`sync`]>[0] | null = null
|
package/src/transactions.ts
CHANGED
|
@@ -2,7 +2,6 @@ import { createDeferred } from "./deferred"
|
|
|
2
2
|
import type { Deferred } from "./deferred"
|
|
3
3
|
import type {
|
|
4
4
|
MutationFn,
|
|
5
|
-
OperationType,
|
|
6
5
|
PendingMutation,
|
|
7
6
|
TransactionConfig,
|
|
8
7
|
TransactionState,
|
|
@@ -66,10 +65,10 @@ let sequenceNumber = 0
|
|
|
66
65
|
* // Commit later
|
|
67
66
|
* await tx.commit()
|
|
68
67
|
*/
|
|
69
|
-
export function createTransaction<
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
const newTransaction = new Transaction<
|
|
68
|
+
export function createTransaction<T extends object = Record<string, unknown>>(
|
|
69
|
+
config: TransactionConfig<T>
|
|
70
|
+
): Transaction<T> {
|
|
71
|
+
const newTransaction = new Transaction<T>(config)
|
|
73
72
|
transactions.push(newTransaction)
|
|
74
73
|
return newTransaction
|
|
75
74
|
}
|
|
@@ -108,15 +107,12 @@ function removeFromPendingList(tx: Transaction<any>) {
|
|
|
108
107
|
}
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
class Transaction<
|
|
112
|
-
T extends object = Record<string, unknown>,
|
|
113
|
-
TOperation extends OperationType = OperationType,
|
|
114
|
-
> {
|
|
110
|
+
class Transaction<T extends object = Record<string, unknown>> {
|
|
115
111
|
public id: string
|
|
116
112
|
public state: TransactionState
|
|
117
113
|
public mutationFn: MutationFn<T>
|
|
118
|
-
public mutations: Array<PendingMutation<T
|
|
119
|
-
public isPersisted: Deferred<Transaction<T
|
|
114
|
+
public mutations: Array<PendingMutation<T>>
|
|
115
|
+
public isPersisted: Deferred<Transaction<T>>
|
|
120
116
|
public autoCommit: boolean
|
|
121
117
|
public createdAt: Date
|
|
122
118
|
public sequenceNumber: number
|
|
@@ -134,7 +130,7 @@ class Transaction<
|
|
|
134
130
|
this.mutationFn = config.mutationFn
|
|
135
131
|
this.state = `pending`
|
|
136
132
|
this.mutations = []
|
|
137
|
-
this.isPersisted = createDeferred<Transaction<T
|
|
133
|
+
this.isPersisted = createDeferred<Transaction<T>>()
|
|
138
134
|
this.autoCommit = config.autoCommit ?? true
|
|
139
135
|
this.createdAt = new Date()
|
|
140
136
|
this.sequenceNumber = sequenceNumber++
|
package/src/types.ts
CHANGED
|
@@ -14,6 +14,40 @@ export type InferSchemaOutput<T> = T extends StandardSchemaV1
|
|
|
14
14
|
: Record<string, unknown>
|
|
15
15
|
: Record<string, unknown>
|
|
16
16
|
|
|
17
|
+
/**
|
|
18
|
+
* Helper type to extract the input type from a standard schema
|
|
19
|
+
*
|
|
20
|
+
* @internal This is used for collection insert type inference
|
|
21
|
+
*/
|
|
22
|
+
export type InferSchemaInput<T> = T extends StandardSchemaV1
|
|
23
|
+
? StandardSchemaV1.InferInput<T> extends object
|
|
24
|
+
? StandardSchemaV1.InferInput<T>
|
|
25
|
+
: Record<string, unknown>
|
|
26
|
+
: Record<string, unknown>
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Helper type to determine the insert input type
|
|
30
|
+
* This takes the raw generics (TExplicit, TSchema, TFallback) instead of the resolved T.
|
|
31
|
+
*
|
|
32
|
+
* Priority:
|
|
33
|
+
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
34
|
+
* 2. Schema input type (if schema provided)
|
|
35
|
+
* 3. Fallback type TFallback
|
|
36
|
+
*
|
|
37
|
+
* @internal This is used for collection insert type inference
|
|
38
|
+
*/
|
|
39
|
+
export type ResolveInsertInput<
|
|
40
|
+
TExplicit = unknown,
|
|
41
|
+
TSchema extends StandardSchemaV1 = never,
|
|
42
|
+
TFallback extends object = Record<string, unknown>,
|
|
43
|
+
> = unknown extends TExplicit
|
|
44
|
+
? [TSchema] extends [never]
|
|
45
|
+
? TFallback
|
|
46
|
+
: InferSchemaInput<TSchema>
|
|
47
|
+
: TExplicit extends object
|
|
48
|
+
? TExplicit
|
|
49
|
+
: Record<string, unknown>
|
|
50
|
+
|
|
17
51
|
/**
|
|
18
52
|
* Helper type to determine the final type based on priority:
|
|
19
53
|
* 1. Explicit generic TExplicit (if not 'unknown')
|
|
@@ -48,6 +82,17 @@ export type Fn = (...args: Array<any>) => any
|
|
|
48
82
|
*/
|
|
49
83
|
export type UtilsRecord = Record<string, Fn>
|
|
50
84
|
|
|
85
|
+
/**
|
|
86
|
+
*
|
|
87
|
+
* @remarks `update` and `insert` are both represented as `Partial<T>`, but changes for `insert` could me made more precise by inferring the schema input type. In practice, this has almost 0 real world impact so it's not worth the added type complexity.
|
|
88
|
+
*
|
|
89
|
+
* @see https://github.com/TanStack/db/pull/209#issuecomment-3053001206
|
|
90
|
+
*/
|
|
91
|
+
export type ResolveTransactionChanges<
|
|
92
|
+
T extends object = Record<string, unknown>,
|
|
93
|
+
TOperation extends OperationType = OperationType,
|
|
94
|
+
> = TOperation extends `delete` ? T : Partial<T>
|
|
95
|
+
|
|
51
96
|
/**
|
|
52
97
|
* Represents a pending mutation within a transaction
|
|
53
98
|
* Contains information about the original and modified data, as well as metadata
|
|
@@ -55,25 +100,32 @@ export type UtilsRecord = Record<string, Fn>
|
|
|
55
100
|
export interface PendingMutation<
|
|
56
101
|
T extends object = Record<string, unknown>,
|
|
57
102
|
TOperation extends OperationType = OperationType,
|
|
103
|
+
TCollection extends Collection<T, any, any, any, any> = Collection<
|
|
104
|
+
T,
|
|
105
|
+
any,
|
|
106
|
+
any,
|
|
107
|
+
any,
|
|
108
|
+
any
|
|
109
|
+
>,
|
|
58
110
|
> {
|
|
59
111
|
mutationId: string
|
|
112
|
+
// The state of the object before the mutation.
|
|
60
113
|
original: TOperation extends `insert` ? {} : T
|
|
114
|
+
// The result state of the object after all mutations.
|
|
61
115
|
modified: T
|
|
62
|
-
changes
|
|
63
|
-
|
|
64
|
-
: TOperation extends `delete`
|
|
65
|
-
? T
|
|
66
|
-
: Partial<T>
|
|
116
|
+
// Only the actual changes to the object by the mutation.
|
|
117
|
+
changes: ResolveTransactionChanges<T, TOperation>
|
|
67
118
|
globalKey: string
|
|
119
|
+
|
|
68
120
|
key: any
|
|
69
|
-
type:
|
|
121
|
+
type: TOperation
|
|
70
122
|
metadata: unknown
|
|
71
123
|
syncMetadata: Record<string, unknown>
|
|
72
124
|
/** Whether this mutation should be applied optimistically (defaults to true) */
|
|
73
125
|
optimistic: boolean
|
|
74
126
|
createdAt: Date
|
|
75
127
|
updatedAt: Date
|
|
76
|
-
collection:
|
|
128
|
+
collection: TCollection
|
|
77
129
|
}
|
|
78
130
|
|
|
79
131
|
/**
|
|
@@ -99,7 +151,7 @@ export type NonEmptyArray<T> = [T, ...Array<T>]
|
|
|
99
151
|
export type TransactionWithMutations<
|
|
100
152
|
T extends object = Record<string, unknown>,
|
|
101
153
|
TOperation extends OperationType = OperationType,
|
|
102
|
-
> = Transaction<T
|
|
154
|
+
> = Transaction<T> & {
|
|
103
155
|
mutations: NonEmptyArray<PendingMutation<T, TOperation>>
|
|
104
156
|
}
|
|
105
157
|
|
|
@@ -116,12 +168,14 @@ export interface TransactionConfig<T extends object = Record<string, unknown>> {
|
|
|
116
168
|
/**
|
|
117
169
|
* Options for the createOptimisticAction helper
|
|
118
170
|
*/
|
|
119
|
-
export interface CreateOptimisticActionsOptions<
|
|
120
|
-
|
|
171
|
+
export interface CreateOptimisticActionsOptions<
|
|
172
|
+
TVars = unknown,
|
|
173
|
+
T extends object = Record<string, unknown>,
|
|
174
|
+
> extends Omit<TransactionConfig<T>, `mutationFn`> {
|
|
121
175
|
/** Function to apply optimistic updates locally before the mutation completes */
|
|
122
176
|
onMutate: (vars: TVars) => void
|
|
123
177
|
/** Function to execute the mutation on the server */
|
|
124
|
-
mutationFn: (vars: TVars, params: MutationFnParams) => Promise<any>
|
|
178
|
+
mutationFn: (vars: TVars, params: MutationFnParams<T>) => Promise<any>
|
|
125
179
|
}
|
|
126
180
|
|
|
127
181
|
export type { Transaction }
|
|
@@ -145,7 +199,7 @@ export interface SyncConfig<
|
|
|
145
199
|
TKey extends string | number = string | number,
|
|
146
200
|
> {
|
|
147
201
|
sync: (params: {
|
|
148
|
-
collection: Collection<T, TKey>
|
|
202
|
+
collection: Collection<T, TKey, any, any, any>
|
|
149
203
|
begin: () => void
|
|
150
204
|
write: (message: Omit<ChangeMessage<T>, `key`>) => void
|
|
151
205
|
commit: () => void
|
|
@@ -232,7 +286,6 @@ export type InsertMutationFnParams<
|
|
|
232
286
|
transaction: TransactionWithMutations<T, `insert`>
|
|
233
287
|
collection: Collection<T, TKey, TUtils>
|
|
234
288
|
}
|
|
235
|
-
|
|
236
289
|
export type DeleteMutationFnParams<
|
|
237
290
|
T extends object = Record<string, unknown>,
|
|
238
291
|
TKey extends string | number = string | number,
|
|
@@ -293,6 +346,7 @@ export interface CollectionConfig<
|
|
|
293
346
|
T extends object = Record<string, unknown>,
|
|
294
347
|
TKey extends string | number = string | number,
|
|
295
348
|
TSchema extends StandardSchemaV1 = StandardSchemaV1,
|
|
349
|
+
TInsertInput extends object = T,
|
|
296
350
|
> {
|
|
297
351
|
// If an id isn't passed in, a UUID will be
|
|
298
352
|
// generated for it.
|
|
@@ -371,7 +425,8 @@ export interface CollectionConfig<
|
|
|
371
425
|
* })
|
|
372
426
|
* }
|
|
373
427
|
*/
|
|
374
|
-
onInsert?: InsertMutationFn<
|
|
428
|
+
onInsert?: InsertMutationFn<TInsertInput, TKey>
|
|
429
|
+
|
|
375
430
|
/**
|
|
376
431
|
* Optional asynchronous handler function called before an update operation
|
|
377
432
|
* @param params Object containing transaction and collection information
|