@tanstack/db 0.0.14 → 0.0.16

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/dist/cjs/collection.cjs +117 -104
  2. package/dist/cjs/collection.cjs.map +1 -1
  3. package/dist/cjs/collection.d.cts +18 -21
  4. package/dist/cjs/index.cjs +31 -13
  5. package/dist/cjs/index.cjs.map +1 -1
  6. package/dist/cjs/index.d.cts +0 -1
  7. package/dist/cjs/query/builder/functions.cjs +107 -0
  8. package/dist/cjs/query/builder/functions.cjs.map +1 -0
  9. package/dist/cjs/query/builder/functions.d.cts +38 -0
  10. package/dist/cjs/query/builder/index.cjs +499 -0
  11. package/dist/cjs/query/builder/index.cjs.map +1 -0
  12. package/dist/cjs/query/builder/index.d.cts +324 -0
  13. package/dist/cjs/query/builder/ref-proxy.cjs +92 -0
  14. package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
  15. package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
  16. package/dist/cjs/query/builder/types.d.cts +81 -0
  17. package/dist/cjs/query/compiler/evaluators.cjs +261 -0
  18. package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
  19. package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
  20. package/dist/cjs/query/compiler/group-by.cjs +271 -0
  21. package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
  22. package/dist/cjs/query/compiler/group-by.d.cts +7 -0
  23. package/dist/cjs/query/compiler/index.cjs +181 -0
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -0
  25. package/dist/cjs/query/compiler/index.d.cts +15 -0
  26. package/dist/cjs/query/compiler/joins.cjs +116 -0
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -0
  28. package/dist/cjs/query/compiler/joins.d.cts +11 -0
  29. package/dist/cjs/query/compiler/order-by.cjs +89 -0
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
  31. package/dist/cjs/query/compiler/order-by.d.cts +9 -0
  32. package/dist/cjs/query/compiler/select.cjs +57 -0
  33. package/dist/cjs/query/compiler/select.cjs.map +1 -0
  34. package/dist/cjs/query/compiler/select.d.cts +15 -0
  35. package/dist/cjs/query/index.d.cts +5 -5
  36. package/dist/cjs/query/ir.cjs +57 -0
  37. package/dist/cjs/query/ir.cjs.map +1 -0
  38. package/dist/cjs/query/ir.d.cts +81 -0
  39. package/dist/cjs/query/live-query-collection.cjs +224 -0
  40. package/dist/cjs/query/live-query-collection.cjs.map +1 -0
  41. package/dist/cjs/query/live-query-collection.d.cts +124 -0
  42. package/dist/cjs/transactions.cjs +20 -13
  43. package/dist/cjs/transactions.cjs.map +1 -1
  44. package/dist/cjs/transactions.d.cts +10 -1
  45. package/dist/cjs/types.d.cts +13 -0
  46. package/dist/esm/collection.d.ts +18 -21
  47. package/dist/esm/collection.js +118 -105
  48. package/dist/esm/collection.js.map +1 -1
  49. package/dist/esm/index.d.ts +0 -1
  50. package/dist/esm/index.js +30 -12
  51. package/dist/esm/query/builder/functions.d.ts +38 -0
  52. package/dist/esm/query/builder/functions.js +107 -0
  53. package/dist/esm/query/builder/functions.js.map +1 -0
  54. package/dist/esm/query/builder/index.d.ts +324 -0
  55. package/dist/esm/query/builder/index.js +499 -0
  56. package/dist/esm/query/builder/index.js.map +1 -0
  57. package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
  58. package/dist/esm/query/builder/ref-proxy.js +92 -0
  59. package/dist/esm/query/builder/ref-proxy.js.map +1 -0
  60. package/dist/esm/query/builder/types.d.ts +81 -0
  61. package/dist/esm/query/compiler/evaluators.d.ts +11 -0
  62. package/dist/esm/query/compiler/evaluators.js +261 -0
  63. package/dist/esm/query/compiler/evaluators.js.map +1 -0
  64. package/dist/esm/query/compiler/group-by.d.ts +7 -0
  65. package/dist/esm/query/compiler/group-by.js +271 -0
  66. package/dist/esm/query/compiler/group-by.js.map +1 -0
  67. package/dist/esm/query/compiler/index.d.ts +15 -0
  68. package/dist/esm/query/compiler/index.js +181 -0
  69. package/dist/esm/query/compiler/index.js.map +1 -0
  70. package/dist/esm/query/compiler/joins.d.ts +11 -0
  71. package/dist/esm/query/compiler/joins.js +116 -0
  72. package/dist/esm/query/compiler/joins.js.map +1 -0
  73. package/dist/esm/query/compiler/order-by.d.ts +9 -0
  74. package/dist/esm/query/compiler/order-by.js +89 -0
  75. package/dist/esm/query/compiler/order-by.js.map +1 -0
  76. package/dist/esm/query/compiler/select.d.ts +15 -0
  77. package/dist/esm/query/compiler/select.js +57 -0
  78. package/dist/esm/query/compiler/select.js.map +1 -0
  79. package/dist/esm/query/index.d.ts +5 -5
  80. package/dist/esm/query/ir.d.ts +81 -0
  81. package/dist/esm/query/ir.js +57 -0
  82. package/dist/esm/query/ir.js.map +1 -0
  83. package/dist/esm/query/live-query-collection.d.ts +124 -0
  84. package/dist/esm/query/live-query-collection.js +224 -0
  85. package/dist/esm/query/live-query-collection.js.map +1 -0
  86. package/dist/esm/transactions.d.ts +10 -1
  87. package/dist/esm/transactions.js +20 -13
  88. package/dist/esm/transactions.js.map +1 -1
  89. package/dist/esm/types.d.ts +13 -0
  90. package/package.json +3 -4
  91. package/src/collection.ts +152 -129
  92. package/src/index.ts +0 -1
  93. package/src/query/builder/functions.ts +267 -0
  94. package/src/query/builder/index.ts +648 -0
  95. package/src/query/builder/ref-proxy.ts +156 -0
  96. package/src/query/builder/types.ts +282 -0
  97. package/src/query/compiler/evaluators.ts +315 -0
  98. package/src/query/compiler/group-by.ts +428 -0
  99. package/src/query/compiler/index.ts +276 -0
  100. package/src/query/compiler/joins.ts +228 -0
  101. package/src/query/compiler/order-by.ts +139 -0
  102. package/src/query/compiler/select.ts +173 -0
  103. package/src/query/index.ts +54 -5
  104. package/src/query/ir.ts +128 -0
  105. package/src/query/live-query-collection.ts +512 -0
  106. package/src/transactions.ts +27 -16
  107. package/src/types.ts +15 -0
  108. package/dist/cjs/query/compiled-query.cjs +0 -160
  109. package/dist/cjs/query/compiled-query.cjs.map +0 -1
  110. package/dist/cjs/query/compiled-query.d.cts +0 -20
  111. package/dist/cjs/query/evaluators.cjs +0 -161
  112. package/dist/cjs/query/evaluators.cjs.map +0 -1
  113. package/dist/cjs/query/evaluators.d.cts +0 -14
  114. package/dist/cjs/query/extractors.cjs +0 -122
  115. package/dist/cjs/query/extractors.cjs.map +0 -1
  116. package/dist/cjs/query/extractors.d.cts +0 -22
  117. package/dist/cjs/query/functions.cjs +0 -152
  118. package/dist/cjs/query/functions.cjs.map +0 -1
  119. package/dist/cjs/query/functions.d.cts +0 -21
  120. package/dist/cjs/query/group-by.cjs +0 -88
  121. package/dist/cjs/query/group-by.cjs.map +0 -1
  122. package/dist/cjs/query/group-by.d.cts +0 -40
  123. package/dist/cjs/query/joins.cjs +0 -141
  124. package/dist/cjs/query/joins.cjs.map +0 -1
  125. package/dist/cjs/query/joins.d.cts +0 -14
  126. package/dist/cjs/query/order-by.cjs +0 -185
  127. package/dist/cjs/query/order-by.cjs.map +0 -1
  128. package/dist/cjs/query/order-by.d.cts +0 -3
  129. package/dist/cjs/query/pipeline-compiler.cjs +0 -89
  130. package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
  131. package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
  132. package/dist/cjs/query/query-builder.cjs +0 -307
  133. package/dist/cjs/query/query-builder.cjs.map +0 -1
  134. package/dist/cjs/query/query-builder.d.cts +0 -225
  135. package/dist/cjs/query/schema.d.cts +0 -100
  136. package/dist/cjs/query/select.cjs +0 -130
  137. package/dist/cjs/query/select.cjs.map +0 -1
  138. package/dist/cjs/query/select.d.cts +0 -3
  139. package/dist/cjs/query/types.d.cts +0 -189
  140. package/dist/cjs/query/utils.cjs +0 -154
  141. package/dist/cjs/query/utils.cjs.map +0 -1
  142. package/dist/cjs/query/utils.d.cts +0 -37
  143. package/dist/cjs/utils.cjs +0 -17
  144. package/dist/cjs/utils.cjs.map +0 -1
  145. package/dist/cjs/utils.d.cts +0 -3
  146. package/dist/esm/query/compiled-query.d.ts +0 -20
  147. package/dist/esm/query/compiled-query.js +0 -160
  148. package/dist/esm/query/compiled-query.js.map +0 -1
  149. package/dist/esm/query/evaluators.d.ts +0 -14
  150. package/dist/esm/query/evaluators.js +0 -161
  151. package/dist/esm/query/evaluators.js.map +0 -1
  152. package/dist/esm/query/extractors.d.ts +0 -22
  153. package/dist/esm/query/extractors.js +0 -122
  154. package/dist/esm/query/extractors.js.map +0 -1
  155. package/dist/esm/query/functions.d.ts +0 -21
  156. package/dist/esm/query/functions.js +0 -152
  157. package/dist/esm/query/functions.js.map +0 -1
  158. package/dist/esm/query/group-by.d.ts +0 -40
  159. package/dist/esm/query/group-by.js +0 -88
  160. package/dist/esm/query/group-by.js.map +0 -1
  161. package/dist/esm/query/joins.d.ts +0 -14
  162. package/dist/esm/query/joins.js +0 -141
  163. package/dist/esm/query/joins.js.map +0 -1
  164. package/dist/esm/query/order-by.d.ts +0 -3
  165. package/dist/esm/query/order-by.js +0 -185
  166. package/dist/esm/query/order-by.js.map +0 -1
  167. package/dist/esm/query/pipeline-compiler.d.ts +0 -10
  168. package/dist/esm/query/pipeline-compiler.js +0 -89
  169. package/dist/esm/query/pipeline-compiler.js.map +0 -1
  170. package/dist/esm/query/query-builder.d.ts +0 -225
  171. package/dist/esm/query/query-builder.js +0 -307
  172. package/dist/esm/query/query-builder.js.map +0 -1
  173. package/dist/esm/query/schema.d.ts +0 -100
  174. package/dist/esm/query/select.d.ts +0 -3
  175. package/dist/esm/query/select.js +0 -130
  176. package/dist/esm/query/select.js.map +0 -1
  177. package/dist/esm/query/types.d.ts +0 -189
  178. package/dist/esm/query/utils.d.ts +0 -37
  179. package/dist/esm/query/utils.js +0 -154
  180. package/dist/esm/query/utils.js.map +0 -1
  181. package/dist/esm/utils.d.ts +0 -3
  182. package/dist/esm/utils.js +0 -17
  183. package/dist/esm/utils.js.map +0 -1
  184. package/src/query/compiled-query.ts +0 -234
  185. package/src/query/evaluators.ts +0 -250
  186. package/src/query/extractors.ts +0 -214
  187. package/src/query/functions.ts +0 -297
  188. package/src/query/group-by.ts +0 -139
  189. package/src/query/joins.ts +0 -260
  190. package/src/query/order-by.ts +0 -264
  191. package/src/query/pipeline-compiler.ts +0 -149
  192. package/src/query/query-builder.ts +0 -902
  193. package/src/query/schema.ts +0 -268
  194. package/src/query/select.ts +0 -208
  195. package/src/query/types.ts +0 -418
  196. package/src/query/utils.ts +0 -245
  197. package/src/utils.ts +0 -15
@@ -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\nexport function createTransaction<\n TData extends object = Record<string, unknown>,\n>(config: TransactionConfig<TData>): Transaction<TData> {\n if (typeof config.mutationFn === `undefined`) {\n throw `mutationFn is required when creating a transaction`\n }\n\n let transactionId = config.id\n if (!transactionId) {\n transactionId = crypto.randomUUID()\n }\n const newTransaction = new Transaction<TData>({\n ...config,\n id: transactionId,\n })\n\n transactions.push(newTransaction)\n\n return newTransaction\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\nexport class 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 metadata: Record<string, unknown>\n public error?: {\n message: string\n error: Error\n }\n\n constructor(config: TransactionConfig<T>) {\n this.id = config.id!\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.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 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 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 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"],"names":[],"mappings":"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"}
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\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\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 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 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 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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"}
@@ -99,6 +99,14 @@ export interface SyncConfig<T extends object = Record<string, unknown>, TKey ext
99
99
  * @returns Record containing relation information
100
100
  */
101
101
  getSyncMetadata?: () => Record<string, unknown>;
102
+ /**
103
+ * The row update mode used to sync to the collection.
104
+ * @default `partial`
105
+ * @description
106
+ * - `partial`: Updates contain only the changes to the row.
107
+ * - `full`: Updates contain the entire row.
108
+ */
109
+ rowUpdateMode?: `partial` | `full`;
102
110
  }
103
111
  export interface ChangeMessage<T extends object = Record<string, unknown>, TKey extends string | number = string | number> {
104
112
  key: TKey;
@@ -222,6 +230,11 @@ export type InputRow = [unknown, Record<string, unknown>];
222
230
  * This is used as the inputs from a collection to a query
223
231
  */
224
232
  export type KeyedStream = IStreamBuilder<InputRow>;
233
+ /**
234
+ * Result stream type representing the output of compiled queries
235
+ * Always returns [key, [result, orderByIndex]] where orderByIndex is undefined for unordered queries
236
+ */
237
+ export type ResultStream = IStreamBuilder<[unknown, [any, string | undefined]]>;
225
238
  /**
226
239
  * A namespaced row is a row withing a pipeline that had each table wrapped in its alias
227
240
  */
package/package.json CHANGED
@@ -1,11 +1,10 @@
1
1
  {
2
2
  "name": "@tanstack/db",
3
3
  "description": "A reactive client store for building super fast apps on sync",
4
- "version": "0.0.14",
4
+ "version": "0.0.16",
5
5
  "dependencies": {
6
- "@electric-sql/d2mini": "^0.1.2",
7
- "@standard-schema/spec": "^1.0.0",
8
- "@tanstack/store": "^0.7.0"
6
+ "@electric-sql/d2mini": "^0.1.4",
7
+ "@standard-schema/spec": "^1.0.0"
9
8
  },
10
9
  "devDependencies": {
11
10
  "@vitest/coverage-istanbul": "^3.0.9"
package/src/collection.ts CHANGED
@@ -1,7 +1,7 @@
1
- import { Store } from "@tanstack/store"
2
1
  import { withArrayChangeTracking, withChangeTracking } from "./proxy"
3
- import { Transaction, getActiveTransaction } from "./transactions"
2
+ import { createTransaction, getActiveTransaction } from "./transactions"
4
3
  import { SortedMap } from "./SortedMap"
4
+ import type { Transaction } from "./transactions"
5
5
  import type {
6
6
  ChangeListener,
7
7
  ChangeMessage,
@@ -148,8 +148,8 @@ export class CollectionImpl<
148
148
  public syncedMetadata = new Map<TKey, unknown>()
149
149
 
150
150
  // Optimistic state tracking - make public for testing
151
- public derivedUpserts = new Map<TKey, T>()
152
- public derivedDeletes = new Set<TKey>()
151
+ public optimisticUpserts = new Map<TKey, T>()
152
+ public optimisticDeletes = new Set<TKey>()
153
153
 
154
154
  // Cached size for performance
155
155
  private _size = 0
@@ -172,6 +172,10 @@ export class CollectionImpl<
172
172
  // Array to store one-time commit listeners
173
173
  private onFirstCommitCallbacks: Array<() => void> = []
174
174
 
175
+ // Event batching for preventing duplicate emissions during transaction flows
176
+ private batchedEvents: Array<ChangeMessage<T, TKey>> = []
177
+ private shouldBatchEvents = false
178
+
175
179
  // Lifecycle management
176
180
  private _status: CollectionStatus = `idle`
177
181
  private activeSubscribersCount = 0
@@ -277,8 +281,8 @@ export class CollectionImpl<
277
281
  throw new Error(`Collection requires a sync config`)
278
282
  }
279
283
 
280
- this.transactions = new SortedMap<string, Transaction<any>>(
281
- (a, b) => a.createdAt.getTime() - b.createdAt.getTime()
284
+ this.transactions = new SortedMap<string, Transaction<any>>((a, b) =>
285
+ a.compareCreatedAt(b)
282
286
  )
283
287
 
284
288
  this.config = config
@@ -377,12 +381,15 @@ export class CollectionImpl<
377
381
  }
378
382
 
379
383
  pendingTransaction.committed = true
380
- this.commitPendingTransactions()
381
384
 
382
- // Update status to ready after first commit
385
+ // Update status to ready
386
+ // We do this before committing as we want the events from the changes to
387
+ // be from a "ready" state.
383
388
  if (this._status === `loading`) {
384
389
  this.setStatus(`ready`)
385
390
  }
391
+
392
+ this.commitPendingTransactions()
386
393
  },
387
394
  })
388
395
 
@@ -472,14 +479,16 @@ export class CollectionImpl<
472
479
  // Clear data
473
480
  this.syncedData.clear()
474
481
  this.syncedMetadata.clear()
475
- this.derivedUpserts.clear()
476
- this.derivedDeletes.clear()
482
+ this.optimisticUpserts.clear()
483
+ this.optimisticDeletes.clear()
477
484
  this._size = 0
478
485
  this.pendingSyncedTransactions = []
479
486
  this.syncedKeys.clear()
480
487
  this.hasReceivedFirstCommit = false
481
488
  this.onFirstCommitCallbacks = []
482
489
  this.preloadPromise = null
490
+ this.batchedEvents = []
491
+ this.shouldBatchEvents = false
483
492
 
484
493
  // Update status
485
494
  this.setStatus(`cleaned-up`)
@@ -553,12 +562,12 @@ export class CollectionImpl<
553
562
  return
554
563
  }
555
564
 
556
- const previousState = new Map(this.derivedUpserts)
557
- const previousDeletes = new Set(this.derivedDeletes)
565
+ const previousState = new Map(this.optimisticUpserts)
566
+ const previousDeletes = new Set(this.optimisticDeletes)
558
567
 
559
568
  // Clear current optimistic state
560
- this.derivedUpserts.clear()
561
- this.derivedDeletes.clear()
569
+ this.optimisticUpserts.clear()
570
+ this.optimisticDeletes.clear()
562
571
 
563
572
  const activeTransactions: Array<Transaction<any>> = []
564
573
  const completedTransactions: Array<Transaction<any>> = []
@@ -578,12 +587,12 @@ export class CollectionImpl<
578
587
  switch (mutation.type) {
579
588
  case `insert`:
580
589
  case `update`:
581
- this.derivedUpserts.set(mutation.key, mutation.modified as T)
582
- this.derivedDeletes.delete(mutation.key)
590
+ this.optimisticUpserts.set(mutation.key, mutation.modified as T)
591
+ this.optimisticDeletes.delete(mutation.key)
583
592
  break
584
593
  case `delete`:
585
- this.derivedUpserts.delete(mutation.key)
586
- this.derivedDeletes.add(mutation.key)
594
+ this.optimisticUpserts.delete(mutation.key)
595
+ this.optimisticDeletes.add(mutation.key)
587
596
  break
588
597
  }
589
598
  }
@@ -656,10 +665,10 @@ export class CollectionImpl<
656
665
  */
657
666
  private calculateSize(): number {
658
667
  const syncedSize = this.syncedData.size
659
- const deletesFromSynced = Array.from(this.derivedDeletes).filter(
660
- (key) => this.syncedData.has(key) && !this.derivedUpserts.has(key)
668
+ const deletesFromSynced = Array.from(this.optimisticDeletes).filter(
669
+ (key) => this.syncedData.has(key) && !this.optimisticUpserts.has(key)
661
670
  ).length
662
- const upsertsNotInSynced = Array.from(this.derivedUpserts.keys()).filter(
671
+ const upsertsNotInSynced = Array.from(this.optimisticUpserts.keys()).filter(
663
672
  (key) => !this.syncedData.has(key)
664
673
  ).length
665
674
 
@@ -676,9 +685,9 @@ export class CollectionImpl<
676
685
  ): void {
677
686
  const allKeys = new Set([
678
687
  ...previousUpserts.keys(),
679
- ...this.derivedUpserts.keys(),
688
+ ...this.optimisticUpserts.keys(),
680
689
  ...previousDeletes,
681
- ...this.derivedDeletes,
690
+ ...this.optimisticDeletes,
682
691
  ])
683
692
 
684
693
  for (const key of allKeys) {
@@ -726,34 +735,55 @@ export class CollectionImpl<
726
735
  }
727
736
 
728
737
  /**
729
- * Emit multiple events at once to all listeners
738
+ * Emit events either immediately or batch them for later emission
730
739
  */
731
- private emitEvents(changes: Array<ChangeMessage<T, TKey>>): void {
732
- if (changes.length > 0) {
733
- // Emit to general listeners
734
- for (const listener of this.changeListeners) {
735
- listener(changes)
740
+ private emitEvents(
741
+ changes: Array<ChangeMessage<T, TKey>>,
742
+ endBatching = false
743
+ ): void {
744
+ if (this.shouldBatchEvents && !endBatching) {
745
+ // Add events to the batch
746
+ this.batchedEvents.push(...changes)
747
+ return
748
+ }
749
+
750
+ // Either we're not batching, or we're ending the batching cycle
751
+ let eventsToEmit = changes
752
+
753
+ if (endBatching) {
754
+ // End batching: combine any batched events with new events and clean up state
755
+ if (this.batchedEvents.length > 0) {
756
+ eventsToEmit = [...this.batchedEvents, ...changes]
736
757
  }
758
+ this.batchedEvents = []
759
+ this.shouldBatchEvents = false
760
+ }
737
761
 
738
- // Emit to key-specific listeners
739
- if (this.changeKeyListeners.size > 0) {
740
- // Group changes by key, but only for keys that have listeners
741
- const changesByKey = new Map<TKey, Array<ChangeMessage<T, TKey>>>()
742
- for (const change of changes) {
743
- if (this.changeKeyListeners.has(change.key)) {
744
- if (!changesByKey.has(change.key)) {
745
- changesByKey.set(change.key, [])
746
- }
747
- changesByKey.get(change.key)!.push(change)
762
+ if (eventsToEmit.length === 0) return
763
+
764
+ // Emit to all listeners
765
+ for (const listener of this.changeListeners) {
766
+ listener(eventsToEmit)
767
+ }
768
+
769
+ // Emit to key-specific listeners
770
+ if (this.changeKeyListeners.size > 0) {
771
+ // Group changes by key, but only for keys that have listeners
772
+ const changesByKey = new Map<TKey, Array<ChangeMessage<T, TKey>>>()
773
+ for (const change of eventsToEmit) {
774
+ if (this.changeKeyListeners.has(change.key)) {
775
+ if (!changesByKey.has(change.key)) {
776
+ changesByKey.set(change.key, [])
748
777
  }
778
+ changesByKey.get(change.key)!.push(change)
749
779
  }
780
+ }
750
781
 
751
- // Emit batched changes to each key's listeners
752
- for (const [key, keyChanges] of changesByKey) {
753
- const keyListeners = this.changeKeyListeners.get(key)!
754
- for (const listener of keyListeners) {
755
- listener(keyChanges)
756
- }
782
+ // Emit batched changes to each key's listeners
783
+ for (const [key, keyChanges] of changesByKey) {
784
+ const keyListeners = this.changeKeyListeners.get(key)!
785
+ for (const listener of keyListeners) {
786
+ listener(keyChanges)
757
787
  }
758
788
  }
759
789
  }
@@ -764,13 +794,13 @@ export class CollectionImpl<
764
794
  */
765
795
  public get(key: TKey): T | undefined {
766
796
  // Check if optimistically deleted
767
- if (this.derivedDeletes.has(key)) {
797
+ if (this.optimisticDeletes.has(key)) {
768
798
  return undefined
769
799
  }
770
800
 
771
801
  // Check optimistic upserts first
772
- if (this.derivedUpserts.has(key)) {
773
- return this.derivedUpserts.get(key)
802
+ if (this.optimisticUpserts.has(key)) {
803
+ return this.optimisticUpserts.get(key)
774
804
  }
775
805
 
776
806
  // Fall back to synced data
@@ -782,12 +812,12 @@ export class CollectionImpl<
782
812
  */
783
813
  public has(key: TKey): boolean {
784
814
  // Check if optimistically deleted
785
- if (this.derivedDeletes.has(key)) {
815
+ if (this.optimisticDeletes.has(key)) {
786
816
  return false
787
817
  }
788
818
 
789
819
  // Check optimistic upserts first
790
- if (this.derivedUpserts.has(key)) {
820
+ if (this.optimisticUpserts.has(key)) {
791
821
  return true
792
822
  }
793
823
 
@@ -808,14 +838,14 @@ export class CollectionImpl<
808
838
  public *keys(): IterableIterator<TKey> {
809
839
  // Yield keys from synced data, skipping any that are deleted.
810
840
  for (const key of this.syncedData.keys()) {
811
- if (!this.derivedDeletes.has(key)) {
841
+ if (!this.optimisticDeletes.has(key)) {
812
842
  yield key
813
843
  }
814
844
  }
815
845
  // Yield keys from upserts that were not already in synced data.
816
- for (const key of this.derivedUpserts.keys()) {
817
- if (!this.syncedData.has(key) && !this.derivedDeletes.has(key)) {
818
- // The derivedDeletes check is technically redundant if inserts/updates always remove from deletes,
846
+ for (const key of this.optimisticUpserts.keys()) {
847
+ if (!this.syncedData.has(key) && !this.optimisticDeletes.has(key)) {
848
+ // The optimisticDeletes check is technically redundant if inserts/updates always remove from deletes,
819
849
  // but it's safer to keep it.
820
850
  yield key
821
851
  }
@@ -829,10 +859,7 @@ export class CollectionImpl<
829
859
  for (const key of this.keys()) {
830
860
  const value = this.get(key)
831
861
  if (value !== undefined) {
832
- const { _orderByIndex, ...copy } = value as T & {
833
- _orderByIndex?: number | string
834
- }
835
- yield copy as T
862
+ yield value
836
863
  }
837
864
  }
838
865
  }
@@ -844,14 +871,46 @@ export class CollectionImpl<
844
871
  for (const key of this.keys()) {
845
872
  const value = this.get(key)
846
873
  if (value !== undefined) {
847
- const { _orderByIndex, ...copy } = value as T & {
848
- _orderByIndex?: number | string
849
- }
850
- yield [key, copy as T]
874
+ yield [key, value]
851
875
  }
852
876
  }
853
877
  }
854
878
 
879
+ /**
880
+ * Get all entries (virtual derived state)
881
+ */
882
+ public *[Symbol.iterator](): IterableIterator<[TKey, T]> {
883
+ for (const [key, value] of this.entries()) {
884
+ yield [key, value]
885
+ }
886
+ }
887
+
888
+ /**
889
+ * Execute a callback for each entry in the collection
890
+ */
891
+ public forEach(
892
+ callbackfn: (value: T, key: TKey, index: number) => void
893
+ ): void {
894
+ let index = 0
895
+ for (const [key, value] of this.entries()) {
896
+ callbackfn(value, key, index++)
897
+ }
898
+ }
899
+
900
+ /**
901
+ * Create a new array with the results of calling a function for each entry in the collection
902
+ */
903
+ public map<U>(
904
+ callbackfn: (value: T, key: TKey, index: number) => U
905
+ ): Array<U> {
906
+ const result: Array<U> = []
907
+ let index = 0
908
+ for (const [key, value] of this.entries()) {
909
+ result.push(callbackfn(value, key, index++))
910
+ }
911
+ return result
912
+ }
913
+
855
914
  /**
856
915
  * Attempts to commit pending synced transactions if there are no active transactions
857
916
  * This method processes operations from pending transactions and applies them to the synced data
@@ -893,6 +952,7 @@ export class CollectionImpl<
893
952
  }
894
953
 
895
954
  const events: Array<ChangeMessage<T, TKey>> = []
955
+ const rowUpdateMode = this.config.sync.rowUpdateMode || `partial`
896
956
 
897
957
  for (const transaction of this.pendingSyncedTransactions) {
898
958
  for (const operation of transaction.operations) {
@@ -925,12 +985,16 @@ export class CollectionImpl<
925
985
  this.syncedData.set(key, operation.value)
926
986
  break
927
987
  case `update`: {
928
- const updatedValue = Object.assign(
929
- {},
930
- this.syncedData.get(key),
931
- operation.value
932
- )
933
- this.syncedData.set(key, updatedValue)
988
+ if (rowUpdateMode === `partial`) {
989
+ const updatedValue = Object.assign(
990
+ {},
991
+ this.syncedData.get(key),
992
+ operation.value
993
+ )
994
+ this.syncedData.set(key, updatedValue)
995
+ } else {
996
+ this.syncedData.set(key, operation.value)
997
+ }
934
998
  break
935
999
  }
936
1000
  case `delete`:
@@ -941,8 +1005,8 @@ export class CollectionImpl<
941
1005
  }
942
1006
 
943
1007
  // Clear optimistic state since sync operations will now provide the authoritative data
944
- this.derivedUpserts.clear()
945
- this.derivedDeletes.clear()
1008
+ this.optimisticUpserts.clear()
1009
+ this.optimisticDeletes.clear()
946
1010
 
947
1011
  // Reset flag and recompute optimistic state for any remaining active transactions
948
1012
  this.isCommittingSyncTransactions = false
@@ -953,12 +1017,15 @@ export class CollectionImpl<
953
1017
  switch (mutation.type) {
954
1018
  case `insert`:
955
1019
  case `update`:
956
- this.derivedUpserts.set(mutation.key, mutation.modified as T)
957
- this.derivedDeletes.delete(mutation.key)
1020
+ this.optimisticUpserts.set(
1021
+ mutation.key,
1022
+ mutation.modified as T
1023
+ )
1024
+ this.optimisticDeletes.delete(mutation.key)
958
1025
  break
959
1026
  case `delete`:
960
- this.derivedUpserts.delete(mutation.key)
961
- this.derivedDeletes.add(mutation.key)
1027
+ this.optimisticUpserts.delete(mutation.key)
1028
+ this.optimisticDeletes.add(mutation.key)
962
1029
  break
963
1030
  }
964
1031
  }
@@ -1031,8 +1098,8 @@ export class CollectionImpl<
1031
1098
  // Update cached size after synced data changes
1032
1099
  this._size = this.calculateSize()
1033
1100
 
1034
- // Emit all events at once
1035
- this.emitEvents(events)
1101
+ // End batching and emit all events (combines any batched events with sync events)
1102
+ this.emitEvents(events, true)
1036
1103
 
1037
1104
  this.pendingSyncedTransactions = []
1038
1105
 
@@ -1242,7 +1309,7 @@ export class CollectionImpl<
1242
1309
  return ambientTransaction
1243
1310
  } else {
1244
1311
  // Create a new transaction with a mutation function that calls the onInsert handler
1245
- const directOpTransaction = new Transaction<T>({
1312
+ const directOpTransaction = createTransaction<T>({
1246
1313
  mutationFn: async (params) => {
1247
1314
  // Call the onInsert handler with the transaction
1248
1315
  return this.config.onInsert!(params)
@@ -1444,7 +1511,7 @@ export class CollectionImpl<
1444
1511
 
1445
1512
  // If no changes were made, return an empty transaction early
1446
1513
  if (mutations.length === 0) {
1447
- const emptyTransaction = new Transaction({
1514
+ const emptyTransaction = createTransaction({
1448
1515
  mutationFn: async () => {},
1449
1516
  })
1450
1517
  emptyTransaction.commit()
@@ -1464,7 +1531,7 @@ export class CollectionImpl<
1464
1531
  // No need to check for onUpdate handler here as we've already checked at the beginning
1465
1532
 
1466
1533
  // Create a new transaction with a mutation function that calls the onUpdate handler
1467
- const directOpTransaction = new Transaction<T>({
1534
+ const directOpTransaction = createTransaction<T>({
1468
1535
  mutationFn: async (params) => {
1469
1536
  // Call the onUpdate handler with the transaction
1470
1537
  return this.config.onUpdate!(params)
@@ -1559,7 +1626,7 @@ export class CollectionImpl<
1559
1626
  }
1560
1627
 
1561
1628
  // Create a new transaction with a mutation function that calls the onDelete handler
1562
- const directOpTransaction = new Transaction<T>({
1629
+ const directOpTransaction = createTransaction<T>({
1563
1630
  autoCommit: true,
1564
1631
  mutationFn: async (params) => {
1565
1632
  // Call the onDelete handler with the transaction
@@ -1616,19 +1683,7 @@ export class CollectionImpl<
1616
1683
  * @returns An Array containing all items in the collection
1617
1684
  */
1618
1685
  get toArray() {
1619
- const array = Array.from(this.values())
1620
-
1621
- // Currently a query with an orderBy will add a _orderByIndex to the items
1622
- // so for now we need to sort the array by _orderByIndex if it exists
1623
- // TODO: in the future it would be much better is the keys are sorted - this
1624
- // should be done by the query engine.
1625
- if (array[0] && (array[0] as { _orderByIndex?: number })._orderByIndex) {
1626
- return (array as Array<{ _orderByIndex: number }>).sort(
1627
- (a, b) => a._orderByIndex - b._orderByIndex
1628
- ) as Array<T>
1629
- }
1630
-
1631
- return array
1686
+ return Array.from(this.values())
1632
1687
  }
1633
1688
 
1634
1689
  /**
@@ -1767,45 +1822,13 @@ export class CollectionImpl<
1767
1822
  * This method should be called by the Transaction class when state changes
1768
1823
  */
1769
1824
  public onTransactionStateChange(): void {
1825
+ // Check if commitPendingTransactions will be called after this
1826
+ // by checking if there are pending sync transactions (same logic as in transactions.ts)
1827
+ this.shouldBatchEvents = this.pendingSyncedTransactions.length > 0
1828
+
1770
1829
  // CRITICAL: Capture visible state BEFORE clearing optimistic state
1771
1830
  this.capturePreSyncVisibleState()
1772
1831
 
1773
1832
  this.recomputeOptimisticState()
1774
1833
  }
1775
-
1776
- private _storeMap: Store<Map<TKey, T>> | undefined
1777
-
1778
- /**
1779
- * Returns a Tanstack Store Map that is updated when the collection changes
1780
- * This is a temporary solution to enable the existing framework hooks to work
1781
- * with the new internals of Collection until they are rewritten.
1782
- * TODO: Remove this once the framework hooks are rewritten.
1783
- */
1784
- public asStoreMap(): Store<Map<TKey, T>> {
1785
- if (!this._storeMap) {
1786
- this._storeMap = new Store(new Map(this.entries()))
1787
- this.changeListeners.add(() => {
1788
- this._storeMap!.setState(() => new Map(this.entries()))
1789
- })
1790
- }
1791
- return this._storeMap
1792
- }
1793
-
1794
- private _storeArray: Store<Array<T>> | undefined
1795
-
1796
- /**
1797
- * Returns a Tanstack Store Array that is updated when the collection changes
1798
- * This is a temporary solution to enable the existing framework hooks to work
1799
- * with the new internals of Collection until they are rewritten.
1800
- * TODO: Remove this once the framework hooks are rewritten.
1801
- */
1802
- public asStoreArray(): Store<Array<T>> {
1803
- if (!this._storeArray) {
1804
- this._storeArray = new Store(this.toArray)
1805
- this.changeListeners.add(() => {
1806
- this._storeArray!.setState(() => this.toArray)
1807
- })
1808
- }
1809
- return this._storeArray
1810
- }
1811
1834
  }
package/src/index.ts CHANGED
@@ -4,7 +4,6 @@ export * from "./SortedMap"
4
4
  export * from "./transactions"
5
5
  export * from "./types"
6
6
  export * from "./errors"
7
- export * from "./utils"
8
7
  export * from "./proxy"
9
8
  export * from "./query/index.js"
10
9
  export * from "./optimistic-action"