@tanstack/db 0.0.12 → 0.0.13

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 (47) hide show
  1. package/dist/cjs/SortedMap.cjs +38 -11
  2. package/dist/cjs/SortedMap.cjs.map +1 -1
  3. package/dist/cjs/SortedMap.d.cts +10 -0
  4. package/dist/cjs/collection.cjs +467 -95
  5. package/dist/cjs/collection.cjs.map +1 -1
  6. package/dist/cjs/collection.d.cts +80 -4
  7. package/dist/cjs/index.cjs +2 -0
  8. package/dist/cjs/index.cjs.map +1 -1
  9. package/dist/cjs/index.d.cts +1 -0
  10. package/dist/cjs/optimistic-action.cjs +21 -0
  11. package/dist/cjs/optimistic-action.cjs.map +1 -0
  12. package/dist/cjs/optimistic-action.d.cts +39 -0
  13. package/dist/cjs/query/compiled-query.cjs +21 -11
  14. package/dist/cjs/query/compiled-query.cjs.map +1 -1
  15. package/dist/cjs/query/query-builder.cjs +2 -2
  16. package/dist/cjs/query/query-builder.cjs.map +1 -1
  17. package/dist/cjs/transactions.cjs +3 -1
  18. package/dist/cjs/transactions.cjs.map +1 -1
  19. package/dist/cjs/types.d.cts +44 -0
  20. package/dist/esm/SortedMap.d.ts +10 -0
  21. package/dist/esm/SortedMap.js +38 -11
  22. package/dist/esm/SortedMap.js.map +1 -1
  23. package/dist/esm/collection.d.ts +80 -4
  24. package/dist/esm/collection.js +467 -95
  25. package/dist/esm/collection.js.map +1 -1
  26. package/dist/esm/index.d.ts +1 -0
  27. package/dist/esm/index.js +2 -0
  28. package/dist/esm/index.js.map +1 -1
  29. package/dist/esm/optimistic-action.d.ts +39 -0
  30. package/dist/esm/optimistic-action.js +21 -0
  31. package/dist/esm/optimistic-action.js.map +1 -0
  32. package/dist/esm/query/compiled-query.js +21 -11
  33. package/dist/esm/query/compiled-query.js.map +1 -1
  34. package/dist/esm/query/query-builder.js +2 -2
  35. package/dist/esm/query/query-builder.js.map +1 -1
  36. package/dist/esm/transactions.js +3 -1
  37. package/dist/esm/transactions.js.map +1 -1
  38. package/dist/esm/types.d.ts +44 -0
  39. package/package.json +1 -1
  40. package/src/SortedMap.ts +46 -13
  41. package/src/collection.ts +624 -119
  42. package/src/index.ts +1 -0
  43. package/src/optimistic-action.ts +65 -0
  44. package/src/query/compiled-query.ts +36 -14
  45. package/src/query/query-builder.ts +2 -2
  46. package/src/transactions.ts +6 -1
  47. package/src/types.ts +47 -0
package/src/index.ts CHANGED
@@ -7,6 +7,7 @@ export * from "./errors"
7
7
  export * from "./utils"
8
8
  export * from "./proxy"
9
9
  export * from "./query/index.js"
10
+ export * from "./optimistic-action"
10
11
 
11
12
  // Re-export some stuff explicitly to ensure the type & value is exported
12
13
  export type { Collection } from "./collection"
@@ -0,0 +1,65 @@
1
+ import { createTransaction } from "./transactions"
2
+ import type { CreateOptimisticActionsOptions, Transaction } from "./types"
3
+
4
+ /**
5
+ * Creates an optimistic action function that applies local optimistic updates immediately
6
+ * before executing the actual mutation on the server.
7
+ *
8
+ * This pattern allows for responsive UI updates while the actual mutation is in progress.
9
+ * The optimistic update is applied via the `onMutate` callback, and the server mutation
10
+ * is executed via the `mutationFn`.
11
+ *
12
+ * @example
13
+ * ```ts
14
+ * const addTodo = createOptimisticAction<string>({
15
+ * onMutate: (text) => {
16
+ * // Instantly applies local optimistic state
17
+ * todoCollection.insert({
18
+ * id: uuid(),
19
+ * text,
20
+ * completed: false
21
+ * })
22
+ * },
23
+ * mutationFn: async (text, params) => {
24
+ * // Persist the todo to your backend
25
+ * const response = await fetch('/api/todos', {
26
+ * method: 'POST',
27
+ * body: JSON.stringify({ text, completed: false }),
28
+ * })
29
+ * return response.json()
30
+ * }
31
+ * })
32
+ *
33
+ * // Usage
34
+ * const transaction = addTodo('New Todo Item')
35
+ * ```
36
+ *
37
+ * @template TVariables - The type of variables that will be passed to the action function
38
+ * @param options - Configuration options for the optimistic action
39
+ * @returns A function that accepts variables of type TVariables and returns a Transaction
40
+ */
41
+ export function createOptimisticAction<TVariables = unknown>(
42
+ options: CreateOptimisticActionsOptions<TVariables>
43
+ ) {
44
+ const { mutationFn, onMutate, ...config } = options
45
+
46
+ return (variables: TVariables): Transaction => {
47
+ // Create transaction with the original config
48
+ const transaction = createTransaction({
49
+ ...config,
50
+ // Wire the mutationFn to use the provided variables
51
+ mutationFn: async (params) => {
52
+ return await mutationFn(variables, params)
53
+ },
54
+ })
55
+
56
+ // Execute the transaction. The mutationFn is called once mutate()
57
+ // is finished.
58
+ transaction.mutate(() => {
59
+ // Call onMutate with variables to apply optimistic updates
60
+ onMutate(variables)
61
+ })
62
+
63
+ return transaction
64
+ }
65
+ }
@@ -1,6 +1,7 @@
1
1
  import { D2, MultiSet, output } from "@electric-sql/d2mini"
2
2
  import { createCollection } from "../collection.js"
3
3
  import { compileQueryPipeline } from "./pipeline-compiler.js"
4
+ import type { StandardSchemaV1 } from "@standard-schema/spec"
4
5
  import type { Collection } from "../collection.js"
5
6
  import type { ChangeMessage, ResolveType, SyncConfig } from "../types.js"
6
7
  import type {
@@ -114,10 +115,36 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
114
115
 
115
116
  this.graph = graph
116
117
  this.inputs = inputs
118
+
119
+ const compare = query.orderBy
120
+ ? (
121
+ val1: ResolveType<
122
+ TResults,
123
+ StandardSchemaV1,
124
+ Record<string, unknown>
125
+ >,
126
+ val2: ResolveType<TResults, StandardSchemaV1, Record<string, unknown>>
127
+ ): number => {
128
+ // The query builder always adds an _orderByIndex property if the results are ordered
129
+ const x = val1 as TResults & { _orderByIndex: number }
130
+ const y = val2 as TResults & { _orderByIndex: number }
131
+ if (x._orderByIndex < y._orderByIndex) {
132
+ return -1
133
+ } else if (x._orderByIndex > y._orderByIndex) {
134
+ return 1
135
+ } else {
136
+ return 0
137
+ }
138
+ }
139
+ : undefined
140
+
117
141
  this.resultCollection = createCollection<TResults>({
118
142
  getKey: (val: unknown) => {
119
143
  return (val as any)._key
120
144
  },
145
+ gcTime: 0,
146
+ startSync: true,
147
+ compare,
121
148
  sync: {
122
149
  sync: sync as unknown as (params: {
123
150
  collection: Collection<
@@ -178,26 +205,21 @@ export class CompiledQuery<TResults extends object = Record<string, unknown>> {
178
205
  throw new Error(`Query is stopped`)
179
206
  }
180
207
 
181
- // Send initial state
182
- Object.entries(this.inputCollections).forEach(([key, collection]) => {
183
- this.sendChangesToInput(
184
- key,
185
- collection.currentStateAsChanges(),
186
- collection.config.getKey
187
- )
188
- })
189
- this.runGraph()
190
-
191
208
  // Subscribe to changes
192
209
  Object.entries(this.inputCollections).forEach(([key, collection]) => {
193
- const unsubscribe = collection.subscribeChanges((changes) => {
194
- this.sendChangesToInput(key, changes, collection.config.getKey)
195
- this.runGraph()
196
- })
210
+ const unsubscribe = collection.subscribeChanges(
211
+ (changes) => {
212
+ this.sendChangesToInput(key, changes, collection.config.getKey)
213
+ this.runGraph()
214
+ },
215
+ { includeInitialState: true }
216
+ )
197
217
 
198
218
  this.unsubscribeCallbacks.push(unsubscribe)
199
219
  })
200
220
 
221
+ this.runGraph()
222
+
201
223
  this.state = `running`
202
224
  return () => {
203
225
  this.stop()
@@ -268,7 +268,7 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
268
268
  // Ensure we have an orderByIndex in the select if we have an orderBy
269
269
  // This is required if select is called after orderBy
270
270
  if (this._query.orderBy) {
271
- validatedSelects.push({ _orderByIndex: { ORDER_INDEX: `numeric` } })
271
+ validatedSelects.push({ _orderByIndex: { ORDER_INDEX: `fractional` } })
272
272
  }
273
273
 
274
274
  const newBuilder = new BaseQueryBuilder<TContext>(
@@ -735,7 +735,7 @@ export class BaseQueryBuilder<TContext extends Context<Schema>> {
735
735
  // This is required if select is called before orderBy
736
736
  newBuilder.query.select = [
737
737
  ...(newBuilder.query.select ?? []),
738
- { _orderByIndex: { ORDER_INDEX: `numeric` } },
738
+ { _orderByIndex: { ORDER_INDEX: `fractional` } },
739
739
  ]
740
740
 
741
741
  return newBuilder as QueryBuilder<TContext>
@@ -156,7 +156,12 @@ export class Transaction<T extends object = Record<string, unknown>> {
156
156
  for (const mutation of this.mutations) {
157
157
  if (!hasCalled.has(mutation.collection.id)) {
158
158
  mutation.collection.onTransactionStateChange()
159
- mutation.collection.commitPendingTransactions()
159
+
160
+ // Only call commitPendingTransactions if there are pending sync transactions
161
+ if (mutation.collection.pendingSyncedTransactions.length > 0) {
162
+ mutation.collection.commitPendingTransactions()
163
+ }
164
+
160
165
  hasCalled.add(mutation.collection.id)
161
166
  }
162
167
  }
package/src/types.ts CHANGED
@@ -111,6 +111,17 @@ export interface TransactionConfig<T extends object = Record<string, unknown>> {
111
111
  metadata?: Record<string, unknown>
112
112
  }
113
113
 
114
+ /**
115
+ * Options for the createOptimisticAction helper
116
+ */
117
+ export interface CreateOptimisticActionsOptions<TVars = unknown>
118
+ extends Omit<TransactionConfig, `mutationFn`> {
119
+ /** Function to apply optimistic updates locally before the mutation completes */
120
+ onMutate: (vars: TVars) => void
121
+ /** Function to execute the mutation on the server */
122
+ mutationFn: (vars: TVars, params: MutationFnParams) => Promise<any>
123
+ }
124
+
114
125
  export type { Transaction }
115
126
 
116
127
  type Value<TExtensions = never> =
@@ -216,6 +227,21 @@ export type DeleteMutationFn<T extends object = Record<string, unknown>> = (
216
227
  params: DeleteMutationFnParams<T>
217
228
  ) => Promise<any>
218
229
 
230
+ /**
231
+ * Collection status values for lifecycle management
232
+ */
233
+ export type CollectionStatus =
234
+ /** Collection is created but sync hasn't started yet (when startSync config is false) */
235
+ | `idle`
236
+ /** Sync has started but hasn't received the first commit yet */
237
+ | `loading`
238
+ /** Collection has received at least one commit and is ready for use */
239
+ | `ready`
240
+ /** An error occurred during sync initialization */
241
+ | `error`
242
+ /** Collection has been cleaned up and resources freed */
243
+ | `cleaned-up`
244
+
219
245
  export interface CollectionConfig<
220
246
  T extends object = Record<string, unknown>,
221
247
  TKey extends string | number = string | number,
@@ -236,6 +262,27 @@ export interface CollectionConfig<
236
262
  * getKey: (item) => item.uuid
237
263
  */
238
264
  getKey: (item: T) => TKey
265
+ /**
266
+ * Time in milliseconds after which the collection will be garbage collected
267
+ * when it has no active subscribers. Defaults to 5 minutes (300000ms).
268
+ */
269
+ gcTime?: number
270
+ /**
271
+ * Whether to start syncing immediately when the collection is created.
272
+ * Defaults to false for lazy loading. Set to true to immediately sync.
273
+ */
274
+ startSync?: boolean
275
+ /**
276
+ * Optional function to compare two items.
277
+ * This is used to order the items in the collection.
278
+ * @param x The first item to compare
279
+ * @param y The second item to compare
280
+ * @returns A number indicating the order of the items
281
+ * @example
282
+ * // For a collection with a 'createdAt' field
283
+ * compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime()
284
+ */
285
+ compare?: (x: T, y: T) => number
239
286
  /**
240
287
  * Optional asynchronous handler function called before an insert operation
241
288
  * @param params Object containing transaction and mutation information