@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
@@ -0,0 +1,128 @@
1
+ /*
2
+ This is the intermediate representation of the query.
3
+ */
4
+
5
+ import type { CollectionImpl } from "../collection"
6
+ import type { NamespacedRow } from "../types"
7
+
8
+ export interface QueryIR {
9
+ from: From
10
+ select?: Select
11
+ join?: Join
12
+ where?: Array<Where>
13
+ groupBy?: GroupBy
14
+ having?: Array<Having>
15
+ orderBy?: OrderBy
16
+ limit?: Limit
17
+ offset?: Offset
18
+
19
+ // Functional variants
20
+ fnSelect?: (row: NamespacedRow) => any
21
+ fnWhere?: Array<(row: NamespacedRow) => any>
22
+ fnHaving?: Array<(row: NamespacedRow) => any>
23
+ }
24
+
25
+ export type From = CollectionRef | QueryRef
26
+
27
+ export type Select = {
28
+ [alias: string]: BasicExpression | Aggregate
29
+ }
30
+
31
+ export type Join = Array<JoinClause>
32
+
33
+ export interface JoinClause {
34
+ from: CollectionRef | QueryRef
35
+ type: `left` | `right` | `inner` | `outer` | `full` | `cross`
36
+ left: BasicExpression
37
+ right: BasicExpression
38
+ }
39
+
40
+ export type Where = BasicExpression<boolean>
41
+
42
+ export type GroupBy = Array<BasicExpression>
43
+
44
+ export type Having = Where
45
+
46
+ export type OrderBy = Array<OrderByClause>
47
+
48
+ export type OrderByClause = {
49
+ expression: BasicExpression
50
+ direction: OrderByDirection
51
+ }
52
+
53
+ export type OrderByDirection = `asc` | `desc`
54
+
55
+ export type Limit = number
56
+
57
+ export type Offset = number
58
+
59
+ /* Expressions */
60
+
61
+ abstract class BaseExpression<T = any> {
62
+ public abstract type: string
63
+ /** @internal - Type brand for TypeScript inference */
64
+ declare readonly __returnType: T
65
+ }
66
+
67
+ export class CollectionRef extends BaseExpression {
68
+ public type = `collectionRef` as const
69
+ constructor(
70
+ public collection: CollectionImpl,
71
+ public alias: string
72
+ ) {
73
+ super()
74
+ }
75
+ }
76
+
77
+ export class QueryRef extends BaseExpression {
78
+ public type = `queryRef` as const
79
+ constructor(
80
+ public query: QueryIR,
81
+ public alias: string
82
+ ) {
83
+ super()
84
+ }
85
+ }
86
+
87
+ export class PropRef<T = any> extends BaseExpression<T> {
88
+ public type = `ref` as const
89
+ constructor(
90
+ public path: Array<string> // path to the property in the collection, with the alias as the first element
91
+ ) {
92
+ super()
93
+ }
94
+ }
95
+
96
+ export class Value<T = any> extends BaseExpression<T> {
97
+ public type = `val` as const
98
+ constructor(
99
+ public value: T // any js value
100
+ ) {
101
+ super()
102
+ }
103
+ }
104
+
105
+ export class Func<T = any> extends BaseExpression<T> {
106
+ public type = `func` as const
107
+ constructor(
108
+ public name: string, // such as eq, gt, lt, upper, lower, etc.
109
+ public args: Array<BasicExpression>
110
+ ) {
111
+ super()
112
+ }
113
+ }
114
+
115
+ // This is the basic expression type that is used in the majority of expression
116
+ // builder callbacks (select, where, groupBy, having, orderBy, etc.)
117
+ // it doesn't include aggregate functions as those are only used in the select clause
118
+ export type BasicExpression<T = any> = PropRef<T> | Value<T> | Func<T>
119
+
120
+ export class Aggregate<T = any> extends BaseExpression<T> {
121
+ public type = `agg` as const
122
+ constructor(
123
+ public name: string, // such as count, avg, sum, min, max, etc.
124
+ public args: Array<BasicExpression>
125
+ ) {
126
+ super()
127
+ }
128
+ }
@@ -0,0 +1,512 @@
1
+ import { D2, MultiSet, output } from "@electric-sql/d2mini"
2
+ import { createCollection } from "../collection.js"
3
+ import { compileQuery } from "./compiler/index.js"
4
+ import { buildQuery, getQueryIR } from "./builder/index.js"
5
+ import type { InitialQueryBuilder, QueryBuilder } from "./builder/index.js"
6
+ import type { Collection } from "../collection.js"
7
+ import type {
8
+ ChangeMessage,
9
+ CollectionConfig,
10
+ KeyedStream,
11
+ ResultStream,
12
+ SyncConfig,
13
+ UtilsRecord,
14
+ } from "../types.js"
15
+ import type { Context, GetResult } from "./builder/types.js"
16
+ import type { MultiSetArray, RootStreamBuilder } from "@electric-sql/d2mini"
17
+
18
+ // Global counter for auto-generated collection IDs
19
+ let liveQueryCollectionCounter = 0
20
+
21
+ /**
22
+ * Configuration interface for live query collection options
23
+ *
24
+ * @example
25
+ * ```typescript
26
+ * const config: LiveQueryCollectionConfig<any, any> = {
27
+ * // id is optional - will auto-generate "live-query-1", "live-query-2", etc.
28
+ * query: (q) => q
29
+ * .from({ comment: commentsCollection })
30
+ * .join(
31
+ * { user: usersCollection },
32
+ * ({ comment, user }) => eq(comment.user_id, user.id)
33
+ * )
34
+ * .where(({ comment }) => eq(comment.active, true))
35
+ * .select(({ comment, user }) => ({
36
+ * id: comment.id,
37
+ * content: comment.content,
38
+ * authorName: user.name,
39
+ * })),
40
+ * // getKey is optional - defaults to using stream key
41
+ * getKey: (item) => item.id,
42
+ * }
43
+ * ```
44
+ */
45
+ export interface LiveQueryCollectionConfig<
46
+ TContext extends Context,
47
+ TResult extends object = GetResult<TContext> & object,
48
+ > {
49
+ /**
50
+ * Unique identifier for the collection
51
+ * If not provided, defaults to `live-query-${number}` with auto-incrementing number
52
+ */
53
+ id?: string
54
+
55
+ /**
56
+ * Query builder function that defines the live query
57
+ */
58
+ query:
59
+ | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
60
+ | QueryBuilder<TContext>
61
+
62
+ /**
63
+ * Function to extract the key from result items
64
+ * If not provided, defaults to using the key from the D2 stream
65
+ */
66
+ getKey?: (item: TResult) => string | number
67
+
68
+ /**
69
+ * Optional schema for validation
70
+ */
71
+ schema?: CollectionConfig<TResult>[`schema`]
72
+
73
+ /**
74
+ * Optional mutation handlers
75
+ */
76
+ onInsert?: CollectionConfig<TResult>[`onInsert`]
77
+ onUpdate?: CollectionConfig<TResult>[`onUpdate`]
78
+ onDelete?: CollectionConfig<TResult>[`onDelete`]
79
+
80
+ /**
81
+ * Start sync / the query immediately
82
+ */
83
+ startSync?: boolean
84
+
85
+ /**
86
+ * GC time for the collection
87
+ */
88
+ gcTime?: number
89
+ }
90
+
91
+ /**
92
+ * Creates live query collection options for use with createCollection
93
+ *
94
+ * @example
95
+ * ```typescript
96
+ * const options = liveQueryCollectionOptions({
97
+ * // id is optional - will auto-generate if not provided
98
+ * query: (q) => q
99
+ * .from({ post: postsCollection })
100
+ * .where(({ post }) => eq(post.published, true))
101
+ * .select(({ post }) => ({
102
+ * id: post.id,
103
+ * title: post.title,
104
+ * content: post.content,
105
+ * })),
106
+ * // getKey is optional - will use stream key if not provided
107
+ * })
108
+ *
109
+ * const collection = createCollection(options)
110
+ * ```
111
+ *
112
+ * @param config - Configuration options for the live query collection
113
+ * @returns Collection options that can be passed to createCollection
114
+ */
115
+ export function liveQueryCollectionOptions<
116
+ TContext extends Context,
117
+ TResult extends object = GetResult<TContext>,
118
+ >(
119
+ config: LiveQueryCollectionConfig<TContext, TResult>
120
+ ): CollectionConfig<TResult> {
121
+ // Generate a unique ID if not provided
122
+ const id = config.id || `live-query-${++liveQueryCollectionCounter}`
123
+
124
+ // Build the query using the provided query builder function or instance
125
+ const query =
126
+ typeof config.query === `function`
127
+ ? buildQuery<TContext>(config.query)
128
+ : getQueryIR(config.query)
129
+
130
+ // WeakMap to store the keys of the results so that we can retreve them in the
131
+ // getKey function
132
+ const resultKeys = new WeakMap<object, unknown>()
133
+
134
+ // WeakMap to store the orderBy index for each result
135
+ const orderByIndices = new WeakMap<object, string>()
136
+
137
+ // Create compare function for ordering if the query has orderBy
138
+ const compare =
139
+ query.orderBy && query.orderBy.length > 0
140
+ ? (val1: TResult, val2: TResult): number => {
141
+ // Use the orderBy index stored in the WeakMap
142
+ const index1 = orderByIndices.get(val1)
143
+ const index2 = orderByIndices.get(val2)
144
+
145
+ // Compare fractional indices lexicographically
146
+ if (index1 && index2) {
147
+ if (index1 < index2) {
148
+ return -1
149
+ } else if (index1 > index2) {
150
+ return 1
151
+ } else {
152
+ return 0
153
+ }
154
+ }
155
+
156
+ // Fallback to no ordering if indices are missing
157
+ return 0
158
+ }
159
+ : undefined
160
+
161
+ const collections = extractCollectionsFromQuery(query)
162
+
163
+ const allCollectionsReady = () => {
164
+ return Object.values(collections).every(
165
+ (collection) => collection.status === `ready`
166
+ )
167
+ }
168
+
169
+ let graphCache: D2 | undefined
170
+ let inputsCache: Record<string, RootStreamBuilder<unknown>> | undefined
171
+ let pipelineCache: ResultStream | undefined
172
+
173
+ const compileBasePipeline = () => {
174
+ graphCache = new D2()
175
+ inputsCache = Object.fromEntries(
176
+ Object.entries(collections).map(([key]) => [
177
+ key,
178
+ graphCache!.newInput<any>(),
179
+ ])
180
+ )
181
+ pipelineCache = compileQuery(
182
+ query,
183
+ inputsCache as Record<string, KeyedStream>
184
+ )
185
+ }
186
+
187
+ const maybeCompileBasePipeline = () => {
188
+ if (!graphCache || !inputsCache || !pipelineCache) {
189
+ compileBasePipeline()
190
+ }
191
+ return {
192
+ graph: graphCache!,
193
+ inputs: inputsCache!,
194
+ pipeline: pipelineCache!,
195
+ }
196
+ }
197
+
198
+ // Compile the base pipeline once initially
199
+ // This is done to ensure that any errors are thrown immediately and synchronously
200
+ compileBasePipeline()
201
+
202
+ // Create the sync configuration
203
+ const sync: SyncConfig<TResult> = {
204
+ rowUpdateMode: `full`,
205
+ sync: ({ begin, write, commit, collection: theCollection }) => {
206
+ const { graph, inputs, pipeline } = maybeCompileBasePipeline()
207
+ let messagesCount = 0
208
+ pipeline.pipe(
209
+ output((data) => {
210
+ const messages = data.getInner()
211
+ messagesCount += messages.length
212
+
213
+ begin()
214
+ messages
215
+ .reduce((acc, [[key, tupleData], multiplicity]) => {
216
+ // All queries now consistently return [value, orderByIndex] format
217
+ // where orderByIndex is undefined for queries without ORDER BY
218
+ const [value, orderByIndex] = tupleData as [
219
+ TResult,
220
+ string | undefined,
221
+ ]
222
+
223
+ const changes = acc.get(key) || {
224
+ deletes: 0,
225
+ inserts: 0,
226
+ value,
227
+ orderByIndex,
228
+ }
229
+ if (multiplicity < 0) {
230
+ changes.deletes += Math.abs(multiplicity)
231
+ } else if (multiplicity > 0) {
232
+ changes.inserts += multiplicity
233
+ changes.value = value
234
+ changes.orderByIndex = orderByIndex
235
+ }
236
+ acc.set(key, changes)
237
+ return acc
238
+ }, new Map<unknown, { deletes: number; inserts: number; value: TResult; orderByIndex: string | undefined }>())
239
+ .forEach((changes, rawKey) => {
240
+ const { deletes, inserts, value, orderByIndex } = changes
241
+
242
+ // Store the key of the result so that we can retrieve it in the
243
+ // getKey function
244
+ resultKeys.set(value, rawKey)
245
+
246
+ // Store the orderBy index if it exists
247
+ if (orderByIndex !== undefined) {
248
+ orderByIndices.set(value, orderByIndex)
249
+ }
250
+
251
+ // Simple singular insert.
252
+ if (inserts && deletes === 0) {
253
+ write({
254
+ value,
255
+ type: `insert`,
256
+ })
257
+ } else if (
258
+ // Insert & update(s) (updates are a delete & insert)
259
+ inserts > deletes ||
260
+ // Just update(s) but the item is already in the collection (so
261
+ // was inserted previously).
262
+ (inserts === deletes &&
263
+ theCollection.has(rawKey as string | number))
264
+ ) {
265
+ write({
266
+ value,
267
+ type: `update`,
268
+ })
269
+ // Only delete is left as an option
270
+ } else if (deletes > 0) {
271
+ write({
272
+ value,
273
+ type: `delete`,
274
+ })
275
+ } else {
276
+ throw new Error(
277
+ `This should never happen ${JSON.stringify(changes)}`
278
+ )
279
+ }
280
+ })
281
+ commit()
282
+ })
283
+ )
284
+
285
+ graph.finalize()
286
+
287
+ const maybeRunGraph = () => {
288
+ // We only run the graph if all the collections are ready
289
+ if (allCollectionsReady()) {
290
+ graph.run()
291
+ // On the initial run, we may need to do an empty commit to ensure that
292
+ // the collection is initialized
293
+ if (messagesCount === 0) {
294
+ begin()
295
+ commit()
296
+ }
297
+ }
298
+ }
299
+
300
+ // Unsubscribe callbacks
301
+ const unsubscribeCallbacks = new Set<() => void>()
302
+
303
+ // Set up data flow from input collections to the compiled query
304
+ Object.entries(collections).forEach(([collectionId, collection]) => {
305
+ const input = inputs[collectionId]!
306
+
307
+ // Subscribe to changes
308
+ const unsubscribe = collection.subscribeChanges(
309
+ (changes: Array<ChangeMessage>) => {
310
+ sendChangesToInput(input, changes, collection.config.getKey)
311
+ maybeRunGraph()
312
+ },
313
+ { includeInitialState: true }
314
+ )
315
+ unsubscribeCallbacks.add(unsubscribe)
316
+ })
317
+
318
+ // Initial run
319
+ maybeRunGraph()
320
+
321
+ // Return the unsubscribe function
322
+ return () => {
323
+ unsubscribeCallbacks.forEach((unsubscribe) => unsubscribe())
324
+ }
325
+ },
326
+ }
327
+
328
+ // Return collection configuration
329
+ return {
330
+ id,
331
+ getKey:
332
+ config.getKey || ((item) => resultKeys.get(item) as string | number),
333
+ sync,
334
+ compare,
335
+ gcTime: config.gcTime || 5000, // 5 seconds by default for live queries
336
+ schema: config.schema,
337
+ onInsert: config.onInsert,
338
+ onUpdate: config.onUpdate,
339
+ onDelete: config.onDelete,
340
+ startSync: config.startSync,
341
+ }
342
+ }
343
+
344
+ /**
345
+ * Creates a live query collection directly
346
+ *
347
+ * @example
348
+ * ```typescript
349
+ * // Minimal usage - just pass a query function
350
+ * const activeUsers = createLiveQueryCollection(
351
+ * (q) => q
352
+ * .from({ user: usersCollection })
353
+ * .where(({ user }) => eq(user.active, true))
354
+ * .select(({ user }) => ({ id: user.id, name: user.name }))
355
+ * )
356
+ *
357
+ * // Full configuration with custom options
358
+ * const searchResults = createLiveQueryCollection({
359
+ * id: "search-results", // Custom ID (auto-generated if omitted)
360
+ * query: (q) => q
361
+ * .from({ post: postsCollection })
362
+ * .where(({ post }) => like(post.title, `%${searchTerm}%`))
363
+ * .select(({ post }) => ({
364
+ * id: post.id,
365
+ * title: post.title,
366
+ * excerpt: post.excerpt,
367
+ * })),
368
+ * getKey: (item) => item.id, // Custom key function (uses stream key if omitted)
369
+ * utils: {
370
+ * updateSearchTerm: (newTerm: string) => {
371
+ * // Custom utility functions
372
+ * }
373
+ * }
374
+ * })
375
+ * ```
376
+ */
377
+
378
+ // Overload 1: Accept just the query function
379
+ export function createLiveQueryCollection<
380
+ TContext extends Context,
381
+ TResult extends object = GetResult<TContext>,
382
+ >(
383
+ query: (q: InitialQueryBuilder) => QueryBuilder<TContext>
384
+ ): Collection<TResult, string | number, {}>
385
+
386
+ // Overload 2: Accept full config object with optional utilities
387
+ export function createLiveQueryCollection<
388
+ TContext extends Context,
389
+ TResult extends object = GetResult<TContext>,
390
+ TUtils extends UtilsRecord = {},
391
+ >(
392
+ config: LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils }
393
+ ): Collection<TResult, string | number, TUtils>
394
+
395
+ // Implementation
396
+ export function createLiveQueryCollection<
397
+ TContext extends Context,
398
+ TResult extends object = GetResult<TContext>,
399
+ TUtils extends UtilsRecord = {},
400
+ >(
401
+ configOrQuery:
402
+ | (LiveQueryCollectionConfig<TContext, TResult> & { utils?: TUtils })
403
+ | ((q: InitialQueryBuilder) => QueryBuilder<TContext>)
404
+ ): Collection<TResult, string | number, TUtils> {
405
+ // Determine if the argument is a function (query) or a config object
406
+ if (typeof configOrQuery === `function`) {
407
+ // Simple query function case
408
+ const config: LiveQueryCollectionConfig<TContext, TResult> = {
409
+ query: configOrQuery as (
410
+ q: InitialQueryBuilder
411
+ ) => QueryBuilder<TContext>,
412
+ }
413
+ const options = liveQueryCollectionOptions<TContext, TResult>(config)
414
+ return bridgeToCreateCollection(options)
415
+ } else {
416
+ // Config object case
417
+ const config = configOrQuery as LiveQueryCollectionConfig<
418
+ TContext,
419
+ TResult
420
+ > & { utils?: TUtils }
421
+ const options = liveQueryCollectionOptions<TContext, TResult>(config)
422
+ return bridgeToCreateCollection({
423
+ ...options,
424
+ utils: config.utils,
425
+ })
426
+ }
427
+ }
428
+
429
+ /**
430
+ * Bridge function that handles the type compatibility between query2's TResult
431
+ * and core collection's ResolveType without exposing ugly type assertions to users
432
+ */
433
+ function bridgeToCreateCollection<
434
+ TResult extends object,
435
+ TUtils extends UtilsRecord = {},
436
+ >(
437
+ options: CollectionConfig<TResult> & { utils?: TUtils }
438
+ ): Collection<TResult, string | number, TUtils> {
439
+ // This is the only place we need a type assertion, hidden from user API
440
+ return createCollection(options as any) as unknown as Collection<
441
+ TResult,
442
+ string | number,
443
+ TUtils
444
+ >
445
+ }
446
+
447
+ /**
448
+ * Helper function to send changes to a D2 input stream
449
+ */
450
+ function sendChangesToInput(
451
+ input: RootStreamBuilder<unknown>,
452
+ changes: Array<ChangeMessage>,
453
+ getKey: (item: ChangeMessage[`value`]) => any
454
+ ) {
455
+ const multiSetArray: MultiSetArray<unknown> = []
456
+ for (const change of changes) {
457
+ const key = getKey(change.value)
458
+ if (change.type === `insert`) {
459
+ multiSetArray.push([[key, change.value], 1])
460
+ } else if (change.type === `update`) {
461
+ multiSetArray.push([[key, change.previousValue], -1])
462
+ multiSetArray.push([[key, change.value], 1])
463
+ } else {
464
+ // change.type === `delete`
465
+ multiSetArray.push([[key, change.value], -1])
466
+ }
467
+ }
468
+ input.sendData(new MultiSet(multiSetArray))
469
+ }
470
+
471
+ /**
472
+ * Helper function to extract collections from a compiled query
473
+ * Traverses the query IR to find all collection references
474
+ * Maps collections by their ID (not alias) as expected by the compiler
475
+ */
476
+ function extractCollectionsFromQuery(
477
+ query: any
478
+ ): Record<string, Collection<any, any, any>> {
479
+ const collections: Record<string, any> = {}
480
+
481
+ // Helper function to recursively extract collections from a query or source
482
+ function extractFromSource(source: any) {
483
+ if (source.type === `collectionRef`) {
484
+ collections[source.collection.id] = source.collection
485
+ } else if (source.type === `queryRef`) {
486
+ // Recursively extract from subquery
487
+ extractFromQuery(source.query)
488
+ }
489
+ }
490
+
491
+ // Helper function to recursively extract collections from a query
492
+ function extractFromQuery(q: any) {
493
+ // Extract from FROM clause
494
+ if (q.from) {
495
+ extractFromSource(q.from)
496
+ }
497
+
498
+ // Extract from JOIN clauses
499
+ if (q.join && Array.isArray(q.join)) {
500
+ for (const joinClause of q.join) {
501
+ if (joinClause.from) {
502
+ extractFromSource(joinClause.from)
503
+ }
504
+ }
505
+ }
506
+ }
507
+
508
+ // Start extraction from the root query
509
+ extractFromQuery(query)
510
+
511
+ return collections
512
+ }