@tanstack/db 0.4.9 → 0.4.10

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 (100) hide show
  1. package/dist/cjs/collection/events.cjs +9 -51
  2. package/dist/cjs/collection/events.cjs.map +1 -1
  3. package/dist/cjs/collection/events.d.cts +18 -7
  4. package/dist/cjs/collection/index.cjs +9 -12
  5. package/dist/cjs/collection/index.cjs.map +1 -1
  6. package/dist/cjs/collection/index.d.cts +13 -14
  7. package/dist/cjs/collection/subscription.cjs +62 -6
  8. package/dist/cjs/collection/subscription.cjs.map +1 -1
  9. package/dist/cjs/collection/subscription.d.cts +16 -3
  10. package/dist/cjs/collection/sync.cjs +58 -6
  11. package/dist/cjs/collection/sync.cjs.map +1 -1
  12. package/dist/cjs/collection/sync.d.cts +18 -4
  13. package/dist/cjs/errors.cjs +8 -0
  14. package/dist/cjs/errors.cjs.map +1 -1
  15. package/dist/cjs/errors.d.cts +6 -0
  16. package/dist/cjs/event-emitter.cjs +94 -0
  17. package/dist/cjs/event-emitter.cjs.map +1 -0
  18. package/dist/cjs/event-emitter.d.cts +45 -0
  19. package/dist/cjs/index.cjs +1 -0
  20. package/dist/cjs/index.cjs.map +1 -1
  21. package/dist/cjs/local-only.cjs.map +1 -1
  22. package/dist/cjs/local-only.d.cts +2 -5
  23. package/dist/cjs/query/compiler/index.cjs +6 -2
  24. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  25. package/dist/cjs/query/compiler/index.d.cts +3 -2
  26. package/dist/cjs/query/compiler/joins.cjs +6 -3
  27. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  28. package/dist/cjs/query/compiler/joins.d.cts +2 -2
  29. package/dist/cjs/query/compiler/order-by.cjs +18 -4
  30. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  31. package/dist/cjs/query/compiler/order-by.d.cts +2 -1
  32. package/dist/cjs/query/compiler/types.d.cts +4 -0
  33. package/dist/cjs/query/index.d.cts +1 -0
  34. package/dist/cjs/query/live/collection-config-builder.cjs +32 -6
  35. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  36. package/dist/cjs/query/live/collection-config-builder.d.cts +13 -1
  37. package/dist/cjs/query/live/collection-subscriber.cjs +29 -0
  38. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  39. package/dist/cjs/query/live/collection-subscriber.d.cts +1 -0
  40. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  41. package/dist/cjs/query/live-query-collection.d.cts +2 -2
  42. package/dist/cjs/types.d.cts +82 -11
  43. package/dist/esm/collection/events.d.ts +18 -7
  44. package/dist/esm/collection/events.js +9 -51
  45. package/dist/esm/collection/events.js.map +1 -1
  46. package/dist/esm/collection/index.d.ts +13 -14
  47. package/dist/esm/collection/index.js +9 -12
  48. package/dist/esm/collection/index.js.map +1 -1
  49. package/dist/esm/collection/subscription.d.ts +16 -3
  50. package/dist/esm/collection/subscription.js +62 -6
  51. package/dist/esm/collection/subscription.js.map +1 -1
  52. package/dist/esm/collection/sync.d.ts +18 -4
  53. package/dist/esm/collection/sync.js +59 -7
  54. package/dist/esm/collection/sync.js.map +1 -1
  55. package/dist/esm/errors.d.ts +6 -0
  56. package/dist/esm/errors.js +8 -0
  57. package/dist/esm/errors.js.map +1 -1
  58. package/dist/esm/event-emitter.d.ts +45 -0
  59. package/dist/esm/event-emitter.js +94 -0
  60. package/dist/esm/event-emitter.js.map +1 -0
  61. package/dist/esm/index.js +2 -1
  62. package/dist/esm/local-only.d.ts +2 -5
  63. package/dist/esm/local-only.js.map +1 -1
  64. package/dist/esm/query/compiler/index.d.ts +3 -2
  65. package/dist/esm/query/compiler/index.js +6 -2
  66. package/dist/esm/query/compiler/index.js.map +1 -1
  67. package/dist/esm/query/compiler/joins.d.ts +2 -2
  68. package/dist/esm/query/compiler/joins.js +6 -3
  69. package/dist/esm/query/compiler/joins.js.map +1 -1
  70. package/dist/esm/query/compiler/order-by.d.ts +2 -1
  71. package/dist/esm/query/compiler/order-by.js +18 -4
  72. package/dist/esm/query/compiler/order-by.js.map +1 -1
  73. package/dist/esm/query/compiler/types.d.ts +4 -0
  74. package/dist/esm/query/index.d.ts +1 -0
  75. package/dist/esm/query/live/collection-config-builder.d.ts +13 -1
  76. package/dist/esm/query/live/collection-config-builder.js +33 -7
  77. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  78. package/dist/esm/query/live/collection-subscriber.d.ts +1 -0
  79. package/dist/esm/query/live/collection-subscriber.js +29 -0
  80. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  81. package/dist/esm/query/live-query-collection.d.ts +2 -2
  82. package/dist/esm/query/live-query-collection.js.map +1 -1
  83. package/dist/esm/types.d.ts +82 -11
  84. package/package.json +2 -2
  85. package/src/collection/events.ts +25 -74
  86. package/src/collection/index.ts +15 -19
  87. package/src/collection/subscription.ts +88 -6
  88. package/src/collection/sync.ts +81 -9
  89. package/src/errors.ts +12 -0
  90. package/src/event-emitter.ts +118 -0
  91. package/src/local-only.ts +5 -12
  92. package/src/query/compiler/index.ts +9 -1
  93. package/src/query/compiler/joins.ts +7 -1
  94. package/src/query/compiler/order-by.ts +23 -2
  95. package/src/query/compiler/types.ts +5 -0
  96. package/src/query/index.ts +1 -0
  97. package/src/query/live/collection-config-builder.ts +56 -7
  98. package/src/query/live/collection-subscriber.ts +50 -0
  99. package/src/query/live-query-collection.ts +8 -4
  100. package/src/types.ts +93 -11
package/src/local-only.ts CHANGED
@@ -1,15 +1,12 @@
1
1
  import type {
2
2
  BaseCollectionConfig,
3
3
  CollectionConfig,
4
- DeleteMutationFn,
5
4
  DeleteMutationFnParams,
6
5
  InferSchemaOutput,
7
- InsertMutationFn,
8
6
  InsertMutationFnParams,
9
7
  OperationType,
10
8
  PendingMutation,
11
9
  SyncConfig,
12
- UpdateMutationFn,
13
10
  UpdateMutationFnParams,
14
11
  UtilsRecord,
15
12
  } from "./types"
@@ -67,13 +64,7 @@ type LocalOnlyCollectionOptionsResult<
67
64
  T extends object,
68
65
  TKey extends string | number,
69
66
  TSchema extends StandardSchemaV1 | never = never,
70
- > = Omit<
71
- CollectionConfig<T, TKey, TSchema>,
72
- `onInsert` | `onUpdate` | `onDelete`
73
- > & {
74
- onInsert?: InsertMutationFn<T, TKey, LocalOnlyCollectionUtils>
75
- onUpdate?: UpdateMutationFn<T, TKey, LocalOnlyCollectionUtils>
76
- onDelete?: DeleteMutationFn<T, TKey, LocalOnlyCollectionUtils>
67
+ > = CollectionConfig<T, TKey, TSchema> & {
77
68
  utils: LocalOnlyCollectionUtils
78
69
  }
79
70
 
@@ -191,7 +182,7 @@ export function localOnlyCollectionOptions<
191
182
  const { initialData, onInsert, onUpdate, onDelete, ...restConfig } = config
192
183
 
193
184
  // Create the sync configuration with transaction confirmation capability
194
- const syncResult = createLocalOnlySync(initialData)
185
+ const syncResult = createLocalOnlySync<T, TKey>(initialData)
195
186
 
196
187
  /**
197
188
  * Create wrapper handlers that call user handlers first, then confirm transactions
@@ -279,9 +270,11 @@ export function localOnlyCollectionOptions<
279
270
  onDelete: wrappedOnDelete,
280
271
  utils: {
281
272
  acceptMutations,
282
- } as LocalOnlyCollectionUtils,
273
+ },
283
274
  startSync: true,
284
275
  gcTime: 0,
276
+ } as LocalOnlyCollectionOptionsResult<T, TKey, TSchema> & {
277
+ schema?: StandardSchemaV1
285
278
  }
286
279
  }
287
280
 
@@ -28,7 +28,9 @@ import type {
28
28
  NamespacedAndKeyedStream,
29
29
  ResultStream,
30
30
  } from "../../types.js"
31
- import type { QueryCache, QueryMapping } from "./types.js"
31
+ import type { QueryCache, QueryMapping, WindowOptions } from "./types.js"
32
+
33
+ export type { WindowOptions } from "./types.js"
32
34
 
33
35
  /**
34
36
  * Result of query compilation including both the pipeline and source-specific WHERE clauses
@@ -87,6 +89,7 @@ export function compileQuery(
87
89
  callbacks: Record<string, LazyCollectionCallbacks>,
88
90
  lazySources: Set<string>,
89
91
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
92
+ setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
90
93
  cache: QueryCache = new WeakMap(),
91
94
  queryMapping: QueryMapping = new WeakMap()
92
95
  ): CompilationResult {
@@ -134,6 +137,7 @@ export function compileQuery(
134
137
  callbacks,
135
138
  lazySources,
136
139
  optimizableOrderByCollections,
140
+ setWindowFn,
137
141
  cache,
138
142
  queryMapping,
139
143
  aliasToCollectionId,
@@ -169,6 +173,7 @@ export function compileQuery(
169
173
  callbacks,
170
174
  lazySources,
171
175
  optimizableOrderByCollections,
176
+ setWindowFn,
172
177
  rawQuery,
173
178
  compileQuery,
174
179
  aliasToCollectionId,
@@ -311,6 +316,7 @@ export function compileQuery(
311
316
  query.select || {},
312
317
  collections[mainCollectionId]!,
313
318
  optimizableOrderByCollections,
319
+ setWindowFn,
314
320
  query.limit,
315
321
  query.offset
316
322
  )
@@ -381,6 +387,7 @@ function processFrom(
381
387
  callbacks: Record<string, LazyCollectionCallbacks>,
382
388
  lazySources: Set<string>,
383
389
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
390
+ setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
384
391
  cache: QueryCache,
385
392
  queryMapping: QueryMapping,
386
393
  aliasToCollectionId: Record<string, string>,
@@ -412,6 +419,7 @@ function processFrom(
412
419
  callbacks,
413
420
  lazySources,
414
421
  optimizableOrderByCollections,
422
+ setWindowFn,
415
423
  cache,
416
424
  queryMapping
417
425
  )
@@ -31,7 +31,7 @@ import type {
31
31
  NamespacedAndKeyedStream,
32
32
  NamespacedRow,
33
33
  } from "../../types.js"
34
- import type { QueryCache, QueryMapping } from "./types.js"
34
+ import type { QueryCache, QueryMapping, WindowOptions } from "./types.js"
35
35
  import type { CollectionSubscription } from "../../collection/subscription.js"
36
36
 
37
37
  /** Function type for loading specific keys into a lazy collection */
@@ -61,6 +61,7 @@ export function processJoins(
61
61
  callbacks: Record<string, LazyCollectionCallbacks>,
62
62
  lazySources: Set<string>,
63
63
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
64
+ setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
64
65
  rawQuery: QueryIR,
65
66
  onCompileSubquery: CompileQueryFn,
66
67
  aliasToCollectionId: Record<string, string>,
@@ -83,6 +84,7 @@ export function processJoins(
83
84
  callbacks,
84
85
  lazySources,
85
86
  optimizableOrderByCollections,
87
+ setWindowFn,
86
88
  rawQuery,
87
89
  onCompileSubquery,
88
90
  aliasToCollectionId,
@@ -111,6 +113,7 @@ function processJoin(
111
113
  callbacks: Record<string, LazyCollectionCallbacks>,
112
114
  lazySources: Set<string>,
113
115
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
116
+ setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
114
117
  rawQuery: QueryIR,
115
118
  onCompileSubquery: CompileQueryFn,
116
119
  aliasToCollectionId: Record<string, string>,
@@ -131,6 +134,7 @@ function processJoin(
131
134
  callbacks,
132
135
  lazySources,
133
136
  optimizableOrderByCollections,
137
+ setWindowFn,
134
138
  cache,
135
139
  queryMapping,
136
140
  onCompileSubquery,
@@ -421,6 +425,7 @@ function processJoinSource(
421
425
  callbacks: Record<string, LazyCollectionCallbacks>,
422
426
  lazySources: Set<string>,
423
427
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
428
+ setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
424
429
  cache: QueryCache,
425
430
  queryMapping: QueryMapping,
426
431
  onCompileSubquery: CompileQueryFn,
@@ -453,6 +458,7 @@ function processJoinSource(
453
458
  callbacks,
454
459
  lazySources,
455
460
  optimizableOrderByCollections,
461
+ setWindowFn,
456
462
  cache,
457
463
  queryMapping
458
464
  )
@@ -5,6 +5,7 @@ import { ensureIndexForField } from "../../indexes/auto-index.js"
5
5
  import { findIndexForField } from "../../utils/index-optimization.js"
6
6
  import { compileExpression } from "./evaluators.js"
7
7
  import { replaceAggregatesByRefs } from "./group-by.js"
8
+ import type { WindowOptions } from "./types.js"
8
9
  import type { CompiledSingleRowExpression } from "./evaluators.js"
9
10
  import type { OrderBy, OrderByClause, QueryIR, Select } from "../ir.js"
10
11
  import type { NamespacedAndKeyedStream, NamespacedRow } from "../../types.js"
@@ -38,6 +39,7 @@ export function processOrderBy(
38
39
  selectClause: Select,
39
40
  collection: Collection,
40
41
  optimizableOrderByCollections: Record<string, OrderByOptimizationInfo>,
42
+ setWindowFn: (windowFn: (options: WindowOptions) => void) => void,
41
43
  limit?: number,
42
44
  offset?: number
43
45
  ): IStreamBuilder<KeyValue<unknown, [NamespacedRow, string]>> {
@@ -107,6 +109,8 @@ export function processOrderBy(
107
109
 
108
110
  let setSizeCallback: ((getSize: () => number) => void) | undefined
109
111
 
112
+ let orderByOptimizationInfo: OrderByOptimizationInfo | undefined
113
+
110
114
  // Optimize the orderBy operator to lazily load elements
111
115
  // by using the range index of the collection.
112
116
  // Only for orderBy clause on a single column for now (no composite ordering)
@@ -161,7 +165,7 @@ export function processOrderBy(
161
165
  ? String(orderByExpression.path[0])
162
166
  : rawQuery.from.alias
163
167
 
164
- const orderByOptimizationInfo = {
168
+ orderByOptimizationInfo = {
165
169
  alias: orderByAlias,
166
170
  offset: offset ?? 0,
167
171
  limit,
@@ -179,7 +183,7 @@ export function processOrderBy(
179
183
  ...optimizableOrderByCollections[followRefCollection.id]!,
180
184
  dataNeeded: () => {
181
185
  const size = getSize()
182
- return Math.max(0, limit - size)
186
+ return Math.max(0, orderByOptimizationInfo!.limit - size)
183
187
  },
184
188
  }
185
189
  }
@@ -194,6 +198,23 @@ export function processOrderBy(
194
198
  offset,
195
199
  comparator: compare,
196
200
  setSizeCallback,
201
+ setWindowFn: (
202
+ windowFn: (options: { offset?: number; limit?: number }) => void
203
+ ) => {
204
+ setWindowFn(
205
+ // We wrap the move function such that we update the orderByOptimizationInfo
206
+ // because that is used by the `dataNeeded` callback to determine if we need to load more data
207
+ (options) => {
208
+ windowFn(options)
209
+ if (orderByOptimizationInfo) {
210
+ orderByOptimizationInfo.offset =
211
+ options.offset ?? orderByOptimizationInfo.offset
212
+ orderByOptimizationInfo.limit =
213
+ options.limit ?? orderByOptimizationInfo.limit
214
+ }
215
+ }
216
+ )
217
+ },
197
218
  })
198
219
  // orderByWithFractionalIndex returns [key, [value, index]] - we keep this format
199
220
  )
@@ -10,3 +10,8 @@ export type QueryCache = WeakMap<QueryIR, CompilationResult>
10
10
  * Mapping from optimized queries back to their original queries for caching
11
11
  */
12
12
  export type QueryMapping = WeakMap<QueryIR, QueryIR>
13
+
14
+ export type WindowOptions = {
15
+ offset?: number
16
+ limit?: number
17
+ }
@@ -56,3 +56,4 @@ export {
56
56
  } from "./live-query-collection.js"
57
57
 
58
58
  export { type LiveQueryCollectionConfig } from "./live/types.js"
59
+ export { type LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
@@ -1,11 +1,15 @@
1
1
  import { D2, output } from "@tanstack/db-ivm"
2
2
  import { compileQuery } from "../compiler/index.js"
3
3
  import { buildQuery, getQueryIR } from "../builder/index.js"
4
- import { MissingAliasInputsError } from "../../errors.js"
4
+ import {
5
+ MissingAliasInputsError,
6
+ SetWindowRequiresOrderByError,
7
+ } from "../../errors.js"
5
8
  import { transactionScopedScheduler } from "../../scheduler.js"
6
9
  import { getActiveTransaction } from "../../transactions.js"
7
10
  import { CollectionSubscriber } from "./collection-subscriber.js"
8
11
  import { getCollectionBuilder } from "./collection-registry.js"
12
+ import type { WindowOptions } from "../compiler/index.js"
9
13
  import type { SchedulerContextId } from "../../scheduler.js"
10
14
  import type { CollectionSubscription } from "../../collection/subscription.js"
11
15
  import type { RootStreamBuilder } from "@tanstack/db-ivm"
@@ -32,6 +36,13 @@ import type { AllCollectionEvents } from "../../collection/events.js"
32
36
  export type LiveQueryCollectionUtils = UtilsRecord & {
33
37
  getRunCount: () => number
34
38
  getBuilder: () => CollectionConfigBuilder<any, any>
39
+ /**
40
+ * Sets the offset and limit of an ordered query.
41
+ * Is a no-op if the query is not ordered.
42
+ *
43
+ * @returns `true` if no subset loading was triggered, or `Promise<void>` that resolves when the subset has been loaded
44
+ */
45
+ setWindow: (options: WindowOptions) => true | Promise<void>
35
46
  }
36
47
 
37
48
  type PendingGraphRun = {
@@ -79,7 +90,11 @@ export class CollectionConfigBuilder<
79
90
  private isInErrorState = false
80
91
 
81
92
  // Reference to the live query collection for error state transitions
82
- private liveQueryCollection?: Collection<TResult, any, any>
93
+ public liveQueryCollection?: Collection<TResult, any, any>
94
+
95
+ private windowFn: ((options: WindowOptions) => void) | undefined
96
+
97
+ private maybeRunGraphFn: (() => void) | undefined
83
98
 
84
99
  private readonly aliasDependencies: Record<
85
100
  string,
@@ -171,10 +186,39 @@ export class CollectionConfigBuilder<
171
186
  utils: {
172
187
  getRunCount: this.getRunCount.bind(this),
173
188
  getBuilder: () => this,
189
+ setWindow: this.setWindow.bind(this),
174
190
  },
175
191
  }
176
192
  }
177
193
 
194
+ setWindow(options: WindowOptions): true | Promise<void> {
195
+ if (!this.windowFn) {
196
+ throw new SetWindowRequiresOrderByError()
197
+ }
198
+
199
+ this.windowFn(options)
200
+ this.maybeRunGraphFn?.()
201
+
202
+ // Check if loading a subset was triggered
203
+ if (this.liveQueryCollection?.isLoadingSubset) {
204
+ // Loading was triggered, return a promise that resolves when it completes
205
+ return new Promise<void>((resolve) => {
206
+ const unsubscribe = this.liveQueryCollection!.on(
207
+ `loadingSubset:change`,
208
+ (event) => {
209
+ if (!event.isLoadingSubset) {
210
+ unsubscribe()
211
+ resolve()
212
+ }
213
+ }
214
+ )
215
+ })
216
+ }
217
+
218
+ // No loading was triggered
219
+ return true
220
+ }
221
+
178
222
  /**
179
223
  * Resolves a collection alias to its collection ID.
180
224
  *
@@ -452,13 +496,15 @@ export class CollectionConfigBuilder<
452
496
  }
453
497
  )
454
498
 
455
- const loadMoreDataCallbacks = this.subscribeToAllCollections(
499
+ const loadSubsetDataCallbacks = this.subscribeToAllCollections(
456
500
  config,
457
501
  fullSyncState
458
502
  )
459
503
 
504
+ this.maybeRunGraphFn = () => this.scheduleGraphRun(loadSubsetDataCallbacks)
505
+
460
506
  // Initial run with callback to load more data if needed
461
- this.scheduleGraphRun(loadMoreDataCallbacks)
507
+ this.scheduleGraphRun(loadSubsetDataCallbacks)
462
508
 
463
509
  // Return the unsubscribe function
464
510
  return () => {
@@ -517,7 +563,10 @@ export class CollectionConfigBuilder<
517
563
  this.subscriptions,
518
564
  this.lazySourcesCallbacks,
519
565
  this.lazySources,
520
- this.optimizableOrderByCollections
566
+ this.optimizableOrderByCollections,
567
+ (windowFn: (options: WindowOptions) => void) => {
568
+ this.windowFn = windowFn
569
+ }
521
570
  )
522
571
 
523
572
  this.pipelineCache = compilation.pipeline
@@ -764,7 +813,7 @@ export class CollectionConfigBuilder<
764
813
  // Combine all loaders into a single callback that initiates loading more data
765
814
  // from any source that needs it. Returns true once all loaders have been called,
766
815
  // but the actual async loading may still be in progress.
767
- const loadMoreDataCallback = () => {
816
+ const loadSubsetDataCallbacks = () => {
768
817
  loaders.map((loader) => loader())
769
818
  return true
770
819
  }
@@ -776,7 +825,7 @@ export class CollectionConfigBuilder<
776
825
  // Initial status check after all subscriptions are set up
777
826
  this.updateLiveQueryStatus(config)
778
827
 
779
- return loadMoreDataCallback
828
+ return loadSubsetDataCallbacks
780
829
  }
781
830
  }
782
831
 
@@ -24,6 +24,12 @@ export class CollectionSubscriber<
24
24
  // Keep track of the biggest value we've sent so far (needed for orderBy optimization)
25
25
  private biggest: any = undefined
26
26
 
27
+ // Track deferred promises for subscription loading states
28
+ private subscriptionLoadingPromises = new Map<
29
+ CollectionSubscription,
30
+ { resolve: () => void }
31
+ >()
32
+
27
33
  constructor(
28
34
  private alias: string,
29
35
  private collectionId: string,
@@ -66,7 +72,51 @@ export class CollectionSubscriber<
66
72
  includeInitialState
67
73
  )
68
74
  }
75
+
76
+ // Subscribe to subscription status changes to propagate loading state
77
+ const statusUnsubscribe = subscription.on(`status:change`, (event) => {
78
+ // TODO: For now we are setting this loading state whenever the subscription
79
+ // status changes to 'loadingSubset'. But we have discussed it only happening
80
+ // when the the live query has it's offset/limit changed, and that triggers the
81
+ // subscription to request a snapshot. This will require more work to implement,
82
+ // and builds on https://github.com/TanStack/db/pull/663 which this PR
83
+ // does not yet depend on.
84
+ if (event.status === `loadingSubset`) {
85
+ // Guard against duplicate transitions
86
+ if (!this.subscriptionLoadingPromises.has(subscription)) {
87
+ let resolve: () => void
88
+ const promise = new Promise<void>((res) => {
89
+ resolve = res
90
+ })
91
+
92
+ this.subscriptionLoadingPromises.set(subscription, {
93
+ resolve: resolve!,
94
+ })
95
+ this.collectionConfigBuilder.liveQueryCollection!._sync.trackLoadPromise(
96
+ promise
97
+ )
98
+ }
99
+ } else {
100
+ // status is 'ready'
101
+ const deferred = this.subscriptionLoadingPromises.get(subscription)
102
+ if (deferred) {
103
+ // Clear the map entry FIRST (before resolving)
104
+ this.subscriptionLoadingPromises.delete(subscription)
105
+ deferred.resolve()
106
+ }
107
+ }
108
+ })
109
+
69
110
  const unsubscribe = () => {
111
+ // If subscription has a pending promise, resolve it before unsubscribing
112
+ const deferred = this.subscriptionLoadingPromises.get(subscription)
113
+ if (deferred) {
114
+ // Clear the map entry FIRST (before resolving)
115
+ this.subscriptionLoadingPromises.delete(subscription)
116
+ deferred.resolve()
117
+ }
118
+
119
+ statusUnsubscribe()
70
120
  subscription.unsubscribe()
71
121
  }
72
122
  // currentSyncState is always defined when subscribe() is called
@@ -20,16 +20,20 @@ import type { Context, GetResult } from "./builder/types.js"
20
20
  type CollectionConfigForContext<
21
21
  TContext extends Context,
22
22
  TResult extends object,
23
+ TUtils extends UtilsRecord = {},
23
24
  > = TContext extends SingleResult
24
- ? CollectionConfigSingleRowOption<TResult> & SingleResult
25
- : CollectionConfigSingleRowOption<TResult> & NonSingleResult
25
+ ? CollectionConfigSingleRowOption<TResult, string | number, never, TUtils> &
26
+ SingleResult
27
+ : CollectionConfigSingleRowOption<TResult, string | number, never, TUtils> &
28
+ NonSingleResult
26
29
 
27
30
  type CollectionForContext<
28
31
  TContext extends Context,
29
32
  TResult extends object,
33
+ TUtils extends UtilsRecord = {},
30
34
  > = TContext extends SingleResult
31
- ? Collection<TResult> & SingleResult
32
- : Collection<TResult> & NonSingleResult
35
+ ? Collection<TResult, string | number, TUtils> & SingleResult
36
+ : Collection<TResult, string | number, TUtils> & NonSingleResult
33
37
 
34
38
  /**
35
39
  * Creates live query collection options for use with createCollection
package/src/types.ts CHANGED
@@ -3,6 +3,7 @@ import type { Collection } from "./collection/index.js"
3
3
  import type { StandardSchemaV1 } from "@standard-schema/spec"
4
4
  import type { Transaction } from "./transactions"
5
5
  import type { BasicExpression, OrderBy } from "./query/ir.js"
6
+ import type { EventEmitter } from "./event-emitter.js"
6
7
 
7
8
  /**
8
9
  * Helper type to extract the output type from a standard schema
@@ -150,17 +151,83 @@ export type Row<TExtensions = never> = Record<string, Value<TExtensions>>
150
151
 
151
152
  export type OperationType = `insert` | `update` | `delete`
152
153
 
153
- export type OnLoadMoreOptions = {
154
+ /**
155
+ * Subscription status values
156
+ */
157
+ export type SubscriptionStatus = `ready` | `loadingSubset`
158
+
159
+ /**
160
+ * Event emitted when subscription status changes
161
+ */
162
+ export interface SubscriptionStatusChangeEvent {
163
+ type: `status:change`
164
+ subscription: Subscription
165
+ previousStatus: SubscriptionStatus
166
+ status: SubscriptionStatus
167
+ }
168
+
169
+ /**
170
+ * Event emitted when subscription status changes to a specific status
171
+ */
172
+ export interface SubscriptionStatusEvent<T extends SubscriptionStatus> {
173
+ type: `status:${T}`
174
+ subscription: Subscription
175
+ previousStatus: SubscriptionStatus
176
+ status: T
177
+ }
178
+
179
+ /**
180
+ * Event emitted when subscription is unsubscribed
181
+ */
182
+ export interface SubscriptionUnsubscribedEvent {
183
+ type: `unsubscribed`
184
+ subscription: Subscription
185
+ }
186
+
187
+ /**
188
+ * All subscription events
189
+ */
190
+ export type SubscriptionEvents = {
191
+ "status:change": SubscriptionStatusChangeEvent
192
+ "status:ready": SubscriptionStatusEvent<`ready`>
193
+ "status:loadingSubset": SubscriptionStatusEvent<`loadingSubset`>
194
+ unsubscribed: SubscriptionUnsubscribedEvent
195
+ }
196
+
197
+ /**
198
+ * Public interface for a collection subscription
199
+ * Used by sync implementations to track subscription lifecycle
200
+ */
201
+ export interface Subscription extends EventEmitter<SubscriptionEvents> {
202
+ /** Current status of the subscription */
203
+ readonly status: SubscriptionStatus
204
+ }
205
+
206
+ export type LoadSubsetOptions = {
207
+ /** The where expression to filter the data */
154
208
  where?: BasicExpression<boolean>
209
+ /** The order by clause to sort the data */
155
210
  orderBy?: OrderBy
211
+ /** The limit of the data to load */
156
212
  limit?: number
213
+ /**
214
+ * The subscription that triggered the load.
215
+ * Advanced sync implementations can use this for:
216
+ * - LRU caching keyed by subscription
217
+ * - Reference counting to track active subscriptions
218
+ * - Subscribing to subscription events (e.g., finalization/unsubscribe)
219
+ * @optional Available when called from CollectionSubscription, may be undefined for direct calls
220
+ */
221
+ subscription?: Subscription
157
222
  }
158
223
 
224
+ export type LoadSubsetFn = (options: LoadSubsetOptions) => true | Promise<void>
225
+
159
226
  export type CleanupFn = () => void
160
227
 
161
228
  export type SyncConfigRes = {
162
229
  cleanup?: CleanupFn
163
- onLoadMore?: (options: OnLoadMoreOptions) => void | Promise<void>
230
+ loadSubset?: LoadSubsetFn
164
231
  }
165
232
  export interface SyncConfig<
166
233
  T extends object = Record<string, unknown>,
@@ -242,7 +309,7 @@ export interface InsertConfig {
242
309
  export type UpdateMutationFnParams<
243
310
  T extends object = Record<string, unknown>,
244
311
  TKey extends string | number = string | number,
245
- TUtils extends UtilsRecord = Record<string, Fn>,
312
+ TUtils extends UtilsRecord = UtilsRecord,
246
313
  > = {
247
314
  transaction: TransactionWithMutations<T, `update`>
248
315
  collection: Collection<T, TKey, TUtils>
@@ -251,7 +318,7 @@ export type UpdateMutationFnParams<
251
318
  export type InsertMutationFnParams<
252
319
  T extends object = Record<string, unknown>,
253
320
  TKey extends string | number = string | number,
254
- TUtils extends UtilsRecord = Record<string, Fn>,
321
+ TUtils extends UtilsRecord = UtilsRecord,
255
322
  > = {
256
323
  transaction: TransactionWithMutations<T, `insert`>
257
324
  collection: Collection<T, TKey, TUtils>
@@ -259,7 +326,7 @@ export type InsertMutationFnParams<
259
326
  export type DeleteMutationFnParams<
260
327
  T extends object = Record<string, unknown>,
261
328
  TKey extends string | number = string | number,
262
- TUtils extends UtilsRecord = Record<string, Fn>,
329
+ TUtils extends UtilsRecord = UtilsRecord,
263
330
  > = {
264
331
  transaction: TransactionWithMutations<T, `delete`>
265
332
  collection: Collection<T, TKey, TUtils>
@@ -268,21 +335,21 @@ export type DeleteMutationFnParams<
268
335
  export type InsertMutationFn<
269
336
  T extends object = Record<string, unknown>,
270
337
  TKey extends string | number = string | number,
271
- TUtils extends UtilsRecord = Record<string, Fn>,
338
+ TUtils extends UtilsRecord = UtilsRecord,
272
339
  TReturn = any,
273
340
  > = (params: InsertMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>
274
341
 
275
342
  export type UpdateMutationFn<
276
343
  T extends object = Record<string, unknown>,
277
344
  TKey extends string | number = string | number,
278
- TUtils extends UtilsRecord = Record<string, Fn>,
345
+ TUtils extends UtilsRecord = UtilsRecord,
279
346
  TReturn = any,
280
347
  > = (params: UpdateMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>
281
348
 
282
349
  export type DeleteMutationFn<
283
350
  T extends object = Record<string, unknown>,
284
351
  TKey extends string | number = string | number,
285
- TUtils extends UtilsRecord = Record<string, Fn>,
352
+ TUtils extends UtilsRecord = UtilsRecord,
286
353
  TReturn = any,
287
354
  > = (params: DeleteMutationFnParams<T, TKey, TUtils>) => Promise<TReturn>
288
355
 
@@ -313,6 +380,8 @@ export type CollectionStatus =
313
380
  /** Collection has been cleaned up and resources freed */
314
381
  | `cleaned-up`
315
382
 
383
+ export type SyncMode = `eager` | `on-demand`
384
+
316
385
  export interface BaseCollectionConfig<
317
386
  T extends object = Record<string, unknown>,
318
387
  TKey extends string | number = string | number,
@@ -321,7 +390,7 @@ export interface BaseCollectionConfig<
321
390
  // then it would conflict with the overloads of createCollection which
322
391
  // requires either T to be provided or a schema to be provided but not both!
323
392
  TSchema extends StandardSchemaV1 = never,
324
- TUtils extends UtilsRecord = Record<string, Fn>,
393
+ TUtils extends UtilsRecord = UtilsRecord,
325
394
  TReturn = any,
326
395
  > {
327
396
  // If an id isn't passed in, a UUID will be
@@ -374,6 +443,15 @@ export interface BaseCollectionConfig<
374
443
  * compare: (x, y) => x.createdAt.getTime() - y.createdAt.getTime()
375
444
  */
376
445
  compare?: (x: T, y: T) => number
446
+ /**
447
+ * The mode of sync to use for the collection.
448
+ * @default `eager`
449
+ * @description
450
+ * - `eager`: syncs all data immediately on preload
451
+ * - `on-demand`: syncs data in incremental snapshots when the collection is queried
452
+ * The exact implementation of the sync mode is up to the sync implementation.
453
+ */
454
+ syncMode?: SyncMode
377
455
  /**
378
456
  * Optional asynchronous handler function called before an insert operation
379
457
  * @param params Object containing transaction and collection information
@@ -503,13 +581,16 @@ export interface BaseCollectionConfig<
503
581
  * }
504
582
  */
505
583
  onDelete?: DeleteMutationFn<T, TKey, TUtils, TReturn>
584
+
585
+ utils?: TUtils
506
586
  }
507
587
 
508
588
  export interface CollectionConfig<
509
589
  T extends object = Record<string, unknown>,
510
590
  TKey extends string | number = string | number,
511
591
  TSchema extends StandardSchemaV1 = never,
512
- > extends BaseCollectionConfig<T, TKey, TSchema> {
592
+ TUtils extends UtilsRecord = UtilsRecord,
593
+ > extends BaseCollectionConfig<T, TKey, TSchema, TUtils> {
513
594
  sync: SyncConfig<T, TKey>
514
595
  }
515
596
 
@@ -533,7 +614,8 @@ export type CollectionConfigSingleRowOption<
533
614
  T extends object = Record<string, unknown>,
534
615
  TKey extends string | number = string | number,
535
616
  TSchema extends StandardSchemaV1 = never,
536
- > = CollectionConfig<T, TKey, TSchema> & MaybeSingleResult
617
+ TUtils extends UtilsRecord = {},
618
+ > = CollectionConfig<T, TKey, TSchema, TUtils> & MaybeSingleResult
537
619
 
538
620
  export type ChangesPayload<T extends object = Record<string, unknown>> = Array<
539
621
  ChangeMessage<T>