@tanstack/db 0.0.13 → 0.0.15

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