@tanstack/db 0.4.8 → 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 (134) 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 +59 -17
  14. package/dist/cjs/errors.cjs.map +1 -1
  15. package/dist/cjs/errors.d.cts +44 -8
  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 +9 -4
  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/builder/types.d.cts +1 -1
  24. package/dist/cjs/query/compiler/index.cjs +46 -19
  25. package/dist/cjs/query/compiler/index.cjs.map +1 -1
  26. package/dist/cjs/query/compiler/index.d.cts +35 -9
  27. package/dist/cjs/query/compiler/joins.cjs +91 -66
  28. package/dist/cjs/query/compiler/joins.cjs.map +1 -1
  29. package/dist/cjs/query/compiler/joins.d.cts +6 -3
  30. package/dist/cjs/query/compiler/order-by.cjs +20 -4
  31. package/dist/cjs/query/compiler/order-by.cjs.map +1 -1
  32. package/dist/cjs/query/compiler/order-by.d.cts +3 -1
  33. package/dist/cjs/query/compiler/select.cjs.map +1 -1
  34. package/dist/cjs/query/compiler/types.d.cts +4 -0
  35. package/dist/cjs/query/index.d.cts +1 -0
  36. package/dist/cjs/query/live/collection-config-builder.cjs +306 -46
  37. package/dist/cjs/query/live/collection-config-builder.cjs.map +1 -1
  38. package/dist/cjs/query/live/collection-config-builder.d.cts +97 -9
  39. package/dist/cjs/query/live/collection-registry.cjs +16 -0
  40. package/dist/cjs/query/live/collection-registry.cjs.map +1 -0
  41. package/dist/cjs/query/live/collection-registry.d.cts +26 -0
  42. package/dist/cjs/query/live/collection-subscriber.cjs +86 -58
  43. package/dist/cjs/query/live/collection-subscriber.cjs.map +1 -1
  44. package/dist/cjs/query/live/collection-subscriber.d.cts +5 -7
  45. package/dist/cjs/query/live-query-collection.cjs +11 -5
  46. package/dist/cjs/query/live-query-collection.cjs.map +1 -1
  47. package/dist/cjs/query/live-query-collection.d.cts +12 -5
  48. package/dist/cjs/query/optimizer.cjs +44 -7
  49. package/dist/cjs/query/optimizer.cjs.map +1 -1
  50. package/dist/cjs/query/optimizer.d.cts +4 -4
  51. package/dist/cjs/scheduler.cjs +137 -0
  52. package/dist/cjs/scheduler.cjs.map +1 -0
  53. package/dist/cjs/scheduler.d.cts +56 -0
  54. package/dist/cjs/transactions.cjs +7 -1
  55. package/dist/cjs/transactions.cjs.map +1 -1
  56. package/dist/cjs/types.d.cts +82 -11
  57. package/dist/esm/collection/events.d.ts +18 -7
  58. package/dist/esm/collection/events.js +9 -51
  59. package/dist/esm/collection/events.js.map +1 -1
  60. package/dist/esm/collection/index.d.ts +13 -14
  61. package/dist/esm/collection/index.js +9 -12
  62. package/dist/esm/collection/index.js.map +1 -1
  63. package/dist/esm/collection/subscription.d.ts +16 -3
  64. package/dist/esm/collection/subscription.js +62 -6
  65. package/dist/esm/collection/subscription.js.map +1 -1
  66. package/dist/esm/collection/sync.d.ts +18 -4
  67. package/dist/esm/collection/sync.js +59 -7
  68. package/dist/esm/collection/sync.js.map +1 -1
  69. package/dist/esm/errors.d.ts +44 -8
  70. package/dist/esm/errors.js +60 -18
  71. package/dist/esm/errors.js.map +1 -1
  72. package/dist/esm/event-emitter.d.ts +45 -0
  73. package/dist/esm/event-emitter.js +94 -0
  74. package/dist/esm/event-emitter.js.map +1 -0
  75. package/dist/esm/index.js +10 -5
  76. package/dist/esm/local-only.d.ts +2 -5
  77. package/dist/esm/local-only.js.map +1 -1
  78. package/dist/esm/query/builder/types.d.ts +1 -1
  79. package/dist/esm/query/compiler/index.d.ts +35 -9
  80. package/dist/esm/query/compiler/index.js +46 -19
  81. package/dist/esm/query/compiler/index.js.map +1 -1
  82. package/dist/esm/query/compiler/joins.d.ts +6 -3
  83. package/dist/esm/query/compiler/joins.js +93 -68
  84. package/dist/esm/query/compiler/joins.js.map +1 -1
  85. package/dist/esm/query/compiler/order-by.d.ts +3 -1
  86. package/dist/esm/query/compiler/order-by.js +20 -4
  87. package/dist/esm/query/compiler/order-by.js.map +1 -1
  88. package/dist/esm/query/compiler/select.js.map +1 -1
  89. package/dist/esm/query/compiler/types.d.ts +4 -0
  90. package/dist/esm/query/index.d.ts +1 -0
  91. package/dist/esm/query/live/collection-config-builder.d.ts +97 -9
  92. package/dist/esm/query/live/collection-config-builder.js +306 -46
  93. package/dist/esm/query/live/collection-config-builder.js.map +1 -1
  94. package/dist/esm/query/live/collection-registry.d.ts +26 -0
  95. package/dist/esm/query/live/collection-registry.js +16 -0
  96. package/dist/esm/query/live/collection-registry.js.map +1 -0
  97. package/dist/esm/query/live/collection-subscriber.d.ts +5 -7
  98. package/dist/esm/query/live/collection-subscriber.js +86 -58
  99. package/dist/esm/query/live/collection-subscriber.js.map +1 -1
  100. package/dist/esm/query/live-query-collection.d.ts +12 -5
  101. package/dist/esm/query/live-query-collection.js +11 -5
  102. package/dist/esm/query/live-query-collection.js.map +1 -1
  103. package/dist/esm/query/optimizer.d.ts +4 -4
  104. package/dist/esm/query/optimizer.js +44 -7
  105. package/dist/esm/query/optimizer.js.map +1 -1
  106. package/dist/esm/scheduler.d.ts +56 -0
  107. package/dist/esm/scheduler.js +137 -0
  108. package/dist/esm/scheduler.js.map +1 -0
  109. package/dist/esm/transactions.js +7 -1
  110. package/dist/esm/transactions.js.map +1 -1
  111. package/dist/esm/types.d.ts +82 -11
  112. package/package.json +2 -2
  113. package/src/collection/events.ts +25 -74
  114. package/src/collection/index.ts +15 -19
  115. package/src/collection/subscription.ts +88 -6
  116. package/src/collection/sync.ts +81 -9
  117. package/src/errors.ts +91 -13
  118. package/src/event-emitter.ts +118 -0
  119. package/src/local-only.ts +5 -12
  120. package/src/query/builder/types.ts +1 -1
  121. package/src/query/compiler/index.ts +124 -33
  122. package/src/query/compiler/joins.ts +187 -128
  123. package/src/query/compiler/order-by.ts +30 -2
  124. package/src/query/compiler/select.ts +2 -3
  125. package/src/query/compiler/types.ts +5 -0
  126. package/src/query/index.ts +1 -0
  127. package/src/query/live/collection-config-builder.ts +501 -60
  128. package/src/query/live/collection-registry.ts +47 -0
  129. package/src/query/live/collection-subscriber.ts +137 -105
  130. package/src/query/live-query-collection.ts +47 -18
  131. package/src/query/optimizer.ts +85 -15
  132. package/src/scheduler.ts +198 -0
  133. package/src/transactions.ts +12 -1
  134. package/src/types.ts +93 -11
@@ -0,0 +1,47 @@
1
+ import type { Collection } from "../../collection/index.js"
2
+ import type { CollectionConfigBuilder } from "./collection-config-builder.js"
3
+
4
+ const collectionBuilderRegistry = new WeakMap<
5
+ Collection<any, any, any>,
6
+ CollectionConfigBuilder<any, any>
7
+ >()
8
+
9
+ /**
10
+ * Retrieves the builder attached to a config object via its utils.getBuilder() method.
11
+ *
12
+ * @param config - The collection config object
13
+ * @returns The attached builder, or `undefined` if none exists
14
+ */
15
+ export function getBuilderFromConfig(
16
+ config: object
17
+ ): CollectionConfigBuilder<any, any> | undefined {
18
+ return (config as any).utils?.getBuilder?.()
19
+ }
20
+
21
+ /**
22
+ * Registers a builder for a collection in the global registry.
23
+ * Used to detect when a live query depends on another live query,
24
+ * enabling the scheduler to ensure parent queries run first.
25
+ *
26
+ * @param collection - The collection to register the builder for
27
+ * @param builder - The builder that produces this collection
28
+ */
29
+ export function registerCollectionBuilder(
30
+ collection: Collection<any, any, any>,
31
+ builder: CollectionConfigBuilder<any, any>
32
+ ): void {
33
+ collectionBuilderRegistry.set(collection, builder)
34
+ }
35
+
36
+ /**
37
+ * Retrieves the builder registered for a collection.
38
+ * Used to discover dependencies when a live query subscribes to another live query.
39
+ *
40
+ * @param collection - The collection to look up
41
+ * @returns The registered builder, or `undefined` if none exists
42
+ */
43
+ export function getCollectionBuilder(
44
+ collection: Collection<any, any, any>
45
+ ): CollectionConfigBuilder<any, any> | undefined {
46
+ return collectionBuilderRegistry.get(collection)
47
+ }
@@ -3,15 +3,20 @@ import {
3
3
  convertOrderByToBasicExpression,
4
4
  convertToBasicExpression,
5
5
  } from "../compiler/expressions.js"
6
- import type { FullSyncState } from "./types.js"
6
+ import { WhereClauseConversionError } from "../../errors.js"
7
7
  import type { MultiSetArray, RootStreamBuilder } from "@tanstack/db-ivm"
8
8
  import type { Collection } from "../../collection/index.js"
9
- import type { ChangeMessage, SyncConfig } from "../../types.js"
9
+ import type { ChangeMessage } from "../../types.js"
10
10
  import type { Context, GetResult } from "../builder/types.js"
11
11
  import type { BasicExpression } from "../ir.js"
12
+ import type { OrderByOptimizationInfo } from "../compiler/order-by.js"
12
13
  import type { CollectionConfigBuilder } from "./collection-config-builder.js"
13
14
  import type { CollectionSubscription } from "../../collection/subscription.js"
14
15
 
16
+ const loadMoreCallbackSymbol = Symbol.for(
17
+ `@tanstack/db.collection-config-builder`
18
+ )
19
+
15
20
  export class CollectionSubscriber<
16
21
  TContext extends Context,
17
22
  TResult extends object = GetResult<TContext>,
@@ -19,71 +24,106 @@ export class CollectionSubscriber<
19
24
  // Keep track of the biggest value we've sent so far (needed for orderBy optimization)
20
25
  private biggest: any = undefined
21
26
 
22
- private collectionAlias: string
27
+ // Track deferred promises for subscription loading states
28
+ private subscriptionLoadingPromises = new Map<
29
+ CollectionSubscription,
30
+ { resolve: () => void }
31
+ >()
23
32
 
24
33
  constructor(
34
+ private alias: string,
25
35
  private collectionId: string,
26
36
  private collection: Collection,
27
- private config: Parameters<SyncConfig<TResult>[`sync`]>[0],
28
- private syncState: FullSyncState,
29
37
  private collectionConfigBuilder: CollectionConfigBuilder<TContext, TResult>
30
- ) {
31
- this.collectionAlias = findCollectionAlias(
32
- this.collectionId,
33
- this.collectionConfigBuilder.query
34
- )!
35
- }
38
+ ) {}
36
39
 
37
40
  subscribe(): CollectionSubscription {
38
- const whereClause = this.getWhereClauseFromAlias(this.collectionAlias)
41
+ const whereClause = this.getWhereClauseForAlias()
39
42
 
40
43
  if (whereClause) {
41
- // Convert WHERE clause to BasicExpression format for collection subscription
42
- const whereExpression = convertToBasicExpression(
43
- whereClause,
44
- this.collectionAlias
45
- )
44
+ const whereExpression = convertToBasicExpression(whereClause, this.alias)
46
45
 
47
46
  if (whereExpression) {
48
- // Use index optimization for this collection
49
47
  return this.subscribeToChanges(whereExpression)
50
- } else {
51
- // This should not happen - if we have a whereClause but can't create whereExpression,
52
- // it indicates a bug in our optimization logic
53
- throw new Error(
54
- `Failed to convert WHERE clause to collection filter for collection '${this.collectionId}'. ` +
55
- `This indicates a bug in the query optimization logic.`
56
- )
57
48
  }
58
- } else {
59
- // No WHERE clause for this collection, use regular subscription
60
- return this.subscribeToChanges()
49
+
50
+ throw new WhereClauseConversionError(this.collectionId, this.alias)
61
51
  }
52
+
53
+ return this.subscribeToChanges()
62
54
  }
63
55
 
64
56
  private subscribeToChanges(whereExpression?: BasicExpression<boolean>) {
65
57
  let subscription: CollectionSubscription
66
- if (
67
- Object.hasOwn(
68
- this.collectionConfigBuilder.optimizableOrderByCollections,
69
- this.collectionId
58
+ const orderByInfo = this.getOrderByInfo()
59
+ if (orderByInfo) {
60
+ subscription = this.subscribeToOrderedChanges(
61
+ whereExpression,
62
+ orderByInfo
70
63
  )
71
- ) {
72
- subscription = this.subscribeToOrderedChanges(whereExpression)
73
64
  } else {
74
- // If the collection is lazy then we should not include the initial state
75
- const includeInitialState =
76
- !this.collectionConfigBuilder.lazyCollections.has(this.collectionId)
65
+ // If the source alias is lazy then we should not include the initial state
66
+ const includeInitialState = !this.collectionConfigBuilder.isLazyAlias(
67
+ this.alias
68
+ )
77
69
 
78
70
  subscription = this.subscribeToMatchingChanges(
79
71
  whereExpression,
80
72
  includeInitialState
81
73
  )
82
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
+
83
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()
84
120
  subscription.unsubscribe()
85
121
  }
86
- this.syncState.unsubscribeCallbacks.add(unsubscribe)
122
+ // currentSyncState is always defined when subscribe() is called
123
+ // (called during sync session setup)
124
+ this.collectionConfigBuilder.currentSyncState!.unsubscribeCallbacks.add(
125
+ unsubscribe
126
+ )
87
127
  return subscription
88
128
  }
89
129
 
@@ -91,7 +131,10 @@ export class CollectionSubscriber<
91
131
  changes: Iterable<ChangeMessage<any, string | number>>,
92
132
  callback?: () => boolean
93
133
  ) {
94
- const input = this.syncState.inputs[this.collectionId]!
134
+ // currentSyncState and input are always defined when this method is called
135
+ // (only called from active subscriptions during a sync session)
136
+ const input =
137
+ this.collectionConfigBuilder.currentSyncState!.inputs[this.alias]!
95
138
  const sentChanges = sendChangesToInput(
96
139
  input,
97
140
  changes,
@@ -103,14 +146,12 @@ export class CollectionSubscriber<
103
146
  // otherwise we end up in an infinite loop trying to load more data
104
147
  const dataLoader = sentChanges > 0 ? callback : undefined
105
148
 
106
- // Always call maybeRunGraph to process changes eagerly.
107
- // The graph will run unless the live query is in an error state.
108
- // Status management is handled separately via status:change event listeners.
109
- this.collectionConfigBuilder.maybeRunGraph(
110
- this.config,
111
- this.syncState,
112
- dataLoader
113
- )
149
+ // We need to schedule a graph run even if there's no data to load
150
+ // because we need to mark the collection as ready if it's not already
151
+ // and that's only done in `scheduleGraphRun`
152
+ this.collectionConfigBuilder.scheduleGraphRun(dataLoader, {
153
+ alias: this.alias,
154
+ })
114
155
  }
115
156
 
116
157
  private subscribeToMatchingChanges(
@@ -132,12 +173,11 @@ export class CollectionSubscriber<
132
173
  }
133
174
 
134
175
  private subscribeToOrderedChanges(
135
- whereExpression: BasicExpression<boolean> | undefined
176
+ whereExpression: BasicExpression<boolean> | undefined,
177
+ orderByInfo: OrderByOptimizationInfo
136
178
  ) {
137
179
  const { orderBy, offset, limit, comparator, dataNeeded, index } =
138
- this.collectionConfigBuilder.optimizableOrderByCollections[
139
- this.collectionId
140
- ]!
180
+ orderByInfo
141
181
 
142
182
  const sendChangesInRange = (
143
183
  changes: Iterable<ChangeMessage<any, string | number>>
@@ -147,7 +187,7 @@ export class CollectionSubscriber<
147
187
  // because they can't affect the topK (and if later we need more data, we will dynamically load more data)
148
188
  const splittedChanges = splitUpdates(changes)
149
189
  let filteredChanges = splittedChanges
150
- if (dataNeeded!() === 0) {
190
+ if (dataNeeded && dataNeeded() === 0) {
151
191
  // If the topK is full [..., maxSentValue] then we do not need to send changes > maxSentValue
152
192
  // because they can never make it into the topK.
153
193
  // However, if the topK isn't full yet, we need to also send changes > maxSentValue
@@ -173,7 +213,7 @@ export class CollectionSubscriber<
173
213
  // Normalize the orderBy clauses such that the references are relative to the collection
174
214
  const normalizedOrderBy = convertOrderByToBasicExpression(
175
215
  orderBy,
176
- this.collectionAlias
216
+ this.alias
177
217
  )
178
218
 
179
219
  // Load the first `offset + limit` values from the index
@@ -190,10 +230,7 @@ export class CollectionSubscriber<
190
230
  // after each iteration of the query pipeline
191
231
  // to ensure that the orderBy operator has enough data to work with
192
232
  loadMoreIfNeeded(subscription: CollectionSubscription) {
193
- const orderByInfo =
194
- this.collectionConfigBuilder.optimizableOrderByCollections[
195
- this.collectionId
196
- ]
233
+ const orderByInfo = this.getOrderByInfo()
197
234
 
198
235
  if (!orderByInfo) {
199
236
  // This query has no orderBy operator
@@ -224,24 +261,40 @@ export class CollectionSubscriber<
224
261
  changes: Iterable<ChangeMessage<any, string | number>>,
225
262
  subscription: CollectionSubscription
226
263
  ) {
227
- const { comparator } =
228
- this.collectionConfigBuilder.optimizableOrderByCollections[
229
- this.collectionId
230
- ]!
231
- const trackedChanges = this.trackSentValues(changes, comparator)
264
+ const orderByInfo = this.getOrderByInfo()
265
+ if (!orderByInfo) {
266
+ this.sendChangesToPipeline(changes)
267
+ return
268
+ }
269
+
270
+ const trackedChanges = this.trackSentValues(changes, orderByInfo.comparator)
271
+
272
+ // Cache the loadMoreIfNeeded callback on the subscription using a symbol property.
273
+ // This ensures we pass the same function instance to the scheduler each time,
274
+ // allowing it to deduplicate callbacks when multiple changes arrive during a transaction.
275
+ type SubscriptionWithLoader = CollectionSubscription & {
276
+ [loadMoreCallbackSymbol]?: () => boolean
277
+ }
278
+
279
+ const subscriptionWithLoader = subscription as SubscriptionWithLoader
280
+
281
+ subscriptionWithLoader[loadMoreCallbackSymbol] ??=
282
+ this.loadMoreIfNeeded.bind(this, subscription)
283
+
232
284
  this.sendChangesToPipeline(
233
285
  trackedChanges,
234
- this.loadMoreIfNeeded.bind(this, subscription)
286
+ subscriptionWithLoader[loadMoreCallbackSymbol]
235
287
  )
236
288
  }
237
289
 
238
290
  // Loads the next `n` items from the collection
239
291
  // starting from the biggest item it has sent
240
292
  private loadNextItems(n: number, subscription: CollectionSubscription) {
241
- const { orderBy, valueExtractorForRawRow } =
242
- this.collectionConfigBuilder.optimizableOrderByCollections[
243
- this.collectionId
244
- ]!
293
+ const orderByInfo = this.getOrderByInfo()
294
+ if (!orderByInfo) {
295
+ return
296
+ }
297
+ const { orderBy, valueExtractorForRawRow } = orderByInfo
245
298
  const biggestSentRow = this.biggest
246
299
  const biggestSentValue = biggestSentRow
247
300
  ? valueExtractorForRawRow(biggestSentRow)
@@ -250,7 +303,7 @@ export class CollectionSubscriber<
250
303
  // Normalize the orderBy clauses such that the references are relative to the collection
251
304
  const normalizedOrderBy = convertOrderByToBasicExpression(
252
305
  orderBy,
253
- this.collectionAlias
306
+ this.alias
254
307
  )
255
308
 
256
309
  // Take the `n` items after the biggest sent value
@@ -261,13 +314,22 @@ export class CollectionSubscriber<
261
314
  })
262
315
  }
263
316
 
264
- private getWhereClauseFromAlias(
265
- collectionAlias: string | undefined
266
- ): BasicExpression<boolean> | undefined {
267
- const collectionWhereClausesCache =
268
- this.collectionConfigBuilder.collectionWhereClausesCache
269
- if (collectionAlias && collectionWhereClausesCache) {
270
- return collectionWhereClausesCache.get(collectionAlias)
317
+ private getWhereClauseForAlias(): BasicExpression<boolean> | undefined {
318
+ const sourceWhereClausesCache =
319
+ this.collectionConfigBuilder.sourceWhereClausesCache
320
+ if (!sourceWhereClausesCache) {
321
+ return undefined
322
+ }
323
+ return sourceWhereClausesCache.get(this.alias)
324
+ }
325
+
326
+ private getOrderByInfo(): OrderByOptimizationInfo | undefined {
327
+ const info =
328
+ this.collectionConfigBuilder.optimizableOrderByCollections[
329
+ this.collectionId
330
+ ]
331
+ if (info && info.alias === this.alias) {
332
+ return info
271
333
  }
272
334
  return undefined
273
335
  }
@@ -288,36 +350,6 @@ export class CollectionSubscriber<
288
350
  }
289
351
  }
290
352
 
291
- /**
292
- * Finds the alias for a collection ID in the query
293
- */
294
- function findCollectionAlias(
295
- collectionId: string,
296
- query: any
297
- ): string | undefined {
298
- // Check FROM clause
299
- if (
300
- query.from?.type === `collectionRef` &&
301
- query.from.collection?.id === collectionId
302
- ) {
303
- return query.from.alias
304
- }
305
-
306
- // Check JOIN clauses
307
- if (query.join) {
308
- for (const joinClause of query.join) {
309
- if (
310
- joinClause.from?.type === `collectionRef` &&
311
- joinClause.from.collection?.id === collectionId
312
- ) {
313
- return joinClause.from.alias
314
- }
315
- }
316
- }
317
-
318
- return undefined
319
- }
320
-
321
353
  /**
322
354
  * Helper function to send changes to a D2 input stream
323
355
  */
@@ -1,5 +1,10 @@
1
1
  import { createCollection } from "../collection/index.js"
2
2
  import { CollectionConfigBuilder } from "./live/collection-config-builder.js"
3
+ import {
4
+ getBuilderFromConfig,
5
+ registerCollectionBuilder,
6
+ } from "./live/collection-registry.js"
7
+ import type { LiveQueryCollectionUtils } from "./live/collection-config-builder.js"
3
8
  import type { LiveQueryCollectionConfig } from "./live/types.js"
4
9
  import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
5
10
  import type { Collection } from "../collection/index.js"
@@ -15,16 +20,20 @@ import type { Context, GetResult } from "./builder/types.js"
15
20
  type CollectionConfigForContext<
16
21
  TContext extends Context,
17
22
  TResult extends object,
23
+ TUtils extends UtilsRecord = {},
18
24
  > = TContext extends SingleResult
19
- ? CollectionConfigSingleRowOption<TResult> & SingleResult
20
- : CollectionConfigSingleRowOption<TResult> & NonSingleResult
25
+ ? CollectionConfigSingleRowOption<TResult, string | number, never, TUtils> &
26
+ SingleResult
27
+ : CollectionConfigSingleRowOption<TResult, string | number, never, TUtils> &
28
+ NonSingleResult
21
29
 
22
30
  type CollectionForContext<
23
31
  TContext extends Context,
24
32
  TResult extends object,
33
+ TUtils extends UtilsRecord = {},
25
34
  > = TContext extends SingleResult
26
- ? Collection<TResult> & SingleResult
27
- : Collection<TResult> & NonSingleResult
35
+ ? Collection<TResult, string | number, TUtils> & SingleResult
36
+ : Collection<TResult, string | number, TUtils> & NonSingleResult
28
37
 
29
38
  /**
30
39
  * Creates live query collection options for use with createCollection
@@ -55,7 +64,9 @@ export function liveQueryCollectionOptions<
55
64
  TResult extends object = GetResult<TContext>,
56
65
  >(
57
66
  config: LiveQueryCollectionConfig<TContext, TResult>
58
- ): CollectionConfigForContext<TContext, TResult> {
67
+ ): CollectionConfigForContext<TContext, TResult> & {
68
+ utils: LiveQueryCollectionUtils
69
+ } {
59
70
  const collectionConfigBuilder = new CollectionConfigBuilder<
60
71
  TContext,
61
72
  TResult
@@ -63,7 +74,7 @@ export function liveQueryCollectionOptions<
63
74
  return collectionConfigBuilder.getConfig() as CollectionConfigForContext<
64
75
  TContext,
65
76
  TResult
66
- >
77
+ > & { utils: LiveQueryCollectionUtils }
67
78
  }
68
79
 
69
80
  /**
@@ -106,7 +117,9 @@ export function createLiveQueryCollection<
106
117
  TResult extends object = GetResult<TContext>,
107
118
  >(
108
119
  query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
109
- ): CollectionForContext<TContext, TResult>
120
+ ): CollectionForContext<TContext, TResult> & {
121
+ utils: LiveQueryCollectionUtils
122
+ }
110
123
 
111
124
  // Overload 2: Accept full config object with optional utilities
112
125
  export function createLiveQueryCollection<
@@ -115,7 +128,9 @@ export function createLiveQueryCollection<
115
128
  TUtils extends UtilsRecord = {},
116
129
  >(
117
130
  config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
118
- ): CollectionForContext<TContext, TResult>
131
+ ): CollectionForContext<TContext, TResult> & {
132
+ utils: LiveQueryCollectionUtils & TUtils
133
+ }
119
134
 
120
135
  // Implementation
121
136
  export function createLiveQueryCollection<
@@ -126,7 +141,9 @@ export function createLiveQueryCollection<
126
141
  configOrQuery:
127
142
  | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
128
143
  | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
129
- ): CollectionForContext<TContext, TResult> {
144
+ ): CollectionForContext<TContext, TResult> & {
145
+ utils: LiveQueryCollectionUtils & TUtils
146
+ } {
130
147
  // Determine if the argument is a function (query) or a config object
131
148
  if (typeof configOrQuery === `function`) {
132
149
  // Simple query function case
@@ -139,7 +156,7 @@ export function createLiveQueryCollection<
139
156
  return bridgeToCreateCollection(options) as CollectionForContext<
140
157
  TContext,
141
158
  TResult
142
- >
159
+ > & { utils: LiveQueryCollectionUtils & TUtils }
143
160
  } else {
144
161
  // Config object case
145
162
  const config = configOrQuery as LiveQueryCollectionConfig<
@@ -147,10 +164,16 @@ export function createLiveQueryCollection<
147
164
  TResult
148
165
  > & { utils?: TUtils }
149
166
  const options = liveQueryCollectionOptions<TContext, TResult>(config)
150
- return bridgeToCreateCollection({
151
- ...options,
152
- utils: config.utils,
153
- }) as CollectionForContext<TContext, TResult>
167
+
168
+ // Merge custom utils if provided, preserving the getBuilder() method for dependency tracking
169
+ if (config.utils) {
170
+ options.utils = { ...options.utils, ...config.utils }
171
+ }
172
+
173
+ return bridgeToCreateCollection(options) as CollectionForContext<
174
+ TContext,
175
+ TResult
176
+ > & { utils: LiveQueryCollectionUtils & TUtils }
154
177
  }
155
178
  }
156
179
 
@@ -162,12 +185,18 @@ function bridgeToCreateCollection<
162
185
  TResult extends object,
163
186
  TUtils extends UtilsRecord = {},
164
187
  >(
165
- options: CollectionConfig<TResult> & { utils?: TUtils }
188
+ options: CollectionConfig<TResult> & { utils: TUtils }
166
189
  ): Collection<TResult, string | number, TUtils> {
167
- // This is the only place we need a type assertion, hidden from user API
168
- return createCollection(options as any) as unknown as Collection<
190
+ const collection = createCollection(options as any) as unknown as Collection<
169
191
  TResult,
170
192
  string | number,
171
- TUtils
193
+ LiveQueryCollectionUtils
172
194
  >
195
+
196
+ const builder = getBuilderFromConfig(options)
197
+ if (builder) {
198
+ registerCollectionBuilder(collection, builder)
199
+ }
200
+
201
+ return collection as unknown as Collection<TResult, string | number, TUtils>
173
202
  }