@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.
- package/dist/cjs/collection.cjs +117 -104
- package/dist/cjs/collection.cjs.map +1 -1
- package/dist/cjs/collection.d.cts +19 -22
- package/dist/cjs/index.cjs +35 -13
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/cjs/index.d.cts +0 -1
- package/dist/cjs/query/builder/functions.cjs +107 -0
- package/dist/cjs/query/builder/functions.cjs.map +1 -0
- package/dist/cjs/query/builder/functions.d.cts +38 -0
- package/dist/cjs/query/builder/index.cjs +499 -0
- package/dist/cjs/query/builder/index.cjs.map +1 -0
- package/dist/cjs/query/builder/index.d.cts +324 -0
- package/dist/cjs/query/builder/ref-proxy.cjs +96 -0
- package/dist/cjs/query/builder/ref-proxy.cjs.map +1 -0
- package/dist/cjs/query/builder/ref-proxy.d.cts +28 -0
- package/dist/cjs/query/builder/types.d.cts +80 -0
- package/dist/cjs/query/compiler/evaluators.cjs +261 -0
- package/dist/cjs/query/compiler/evaluators.cjs.map +1 -0
- package/dist/cjs/query/compiler/evaluators.d.cts +11 -0
- package/dist/cjs/query/compiler/group-by.cjs +271 -0
- package/dist/cjs/query/compiler/group-by.cjs.map +1 -0
- package/dist/cjs/query/compiler/group-by.d.cts +7 -0
- package/dist/cjs/query/compiler/index.cjs +181 -0
- package/dist/cjs/query/compiler/index.cjs.map +1 -0
- package/dist/cjs/query/compiler/index.d.cts +15 -0
- package/dist/cjs/query/compiler/joins.cjs +116 -0
- package/dist/cjs/query/compiler/joins.cjs.map +1 -0
- package/dist/cjs/query/compiler/joins.d.cts +11 -0
- package/dist/cjs/query/compiler/order-by.cjs +89 -0
- package/dist/cjs/query/compiler/order-by.cjs.map +1 -0
- package/dist/cjs/query/compiler/order-by.d.cts +9 -0
- package/dist/cjs/query/compiler/select.cjs +57 -0
- package/dist/cjs/query/compiler/select.cjs.map +1 -0
- package/dist/cjs/query/compiler/select.d.cts +15 -0
- package/dist/cjs/query/index.d.cts +6 -5
- package/dist/cjs/query/ir.cjs +57 -0
- package/dist/cjs/query/ir.cjs.map +1 -0
- package/dist/cjs/query/ir.d.cts +81 -0
- package/dist/cjs/query/live-query-collection.cjs +224 -0
- package/dist/cjs/query/live-query-collection.cjs.map +1 -0
- package/dist/cjs/query/live-query-collection.d.cts +124 -0
- package/dist/cjs/transactions.cjs +20 -13
- package/dist/cjs/transactions.cjs.map +1 -1
- package/dist/cjs/transactions.d.cts +13 -4
- package/dist/cjs/types.d.cts +14 -1
- package/dist/esm/collection.d.ts +19 -22
- package/dist/esm/collection.js +118 -105
- package/dist/esm/collection.js.map +1 -1
- package/dist/esm/index.d.ts +0 -1
- package/dist/esm/index.js +34 -12
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/query/builder/functions.d.ts +38 -0
- package/dist/esm/query/builder/functions.js +107 -0
- package/dist/esm/query/builder/functions.js.map +1 -0
- package/dist/esm/query/builder/index.d.ts +324 -0
- package/dist/esm/query/builder/index.js +499 -0
- package/dist/esm/query/builder/index.js.map +1 -0
- package/dist/esm/query/builder/ref-proxy.d.ts +28 -0
- package/dist/esm/query/builder/ref-proxy.js +96 -0
- package/dist/esm/query/builder/ref-proxy.js.map +1 -0
- package/dist/esm/query/builder/types.d.ts +80 -0
- package/dist/esm/query/compiler/evaluators.d.ts +11 -0
- package/dist/esm/query/compiler/evaluators.js +261 -0
- package/dist/esm/query/compiler/evaluators.js.map +1 -0
- package/dist/esm/query/compiler/group-by.d.ts +7 -0
- package/dist/esm/query/compiler/group-by.js +271 -0
- package/dist/esm/query/compiler/group-by.js.map +1 -0
- package/dist/esm/query/compiler/index.d.ts +15 -0
- package/dist/esm/query/compiler/index.js +181 -0
- package/dist/esm/query/compiler/index.js.map +1 -0
- package/dist/esm/query/compiler/joins.d.ts +11 -0
- package/dist/esm/query/compiler/joins.js +116 -0
- package/dist/esm/query/compiler/joins.js.map +1 -0
- package/dist/esm/query/compiler/order-by.d.ts +9 -0
- package/dist/esm/query/compiler/order-by.js +89 -0
- package/dist/esm/query/compiler/order-by.js.map +1 -0
- package/dist/esm/query/compiler/select.d.ts +15 -0
- package/dist/esm/query/compiler/select.js +57 -0
- package/dist/esm/query/compiler/select.js.map +1 -0
- package/dist/esm/query/index.d.ts +6 -5
- package/dist/esm/query/ir.d.ts +81 -0
- package/dist/esm/query/ir.js +57 -0
- package/dist/esm/query/ir.js.map +1 -0
- package/dist/esm/query/live-query-collection.d.ts +124 -0
- package/dist/esm/query/live-query-collection.js +224 -0
- package/dist/esm/query/live-query-collection.js.map +1 -0
- package/dist/esm/transactions.d.ts +13 -4
- package/dist/esm/transactions.js +20 -13
- package/dist/esm/transactions.js.map +1 -1
- package/dist/esm/types.d.ts +14 -1
- package/package.json +3 -4
- package/src/collection.ts +152 -129
- package/src/index.ts +0 -1
- package/src/query/builder/functions.ts +267 -0
- package/src/query/builder/index.ts +648 -0
- package/src/query/builder/ref-proxy.ts +156 -0
- package/src/query/builder/types.ts +278 -0
- package/src/query/compiler/evaluators.ts +315 -0
- package/src/query/compiler/group-by.ts +428 -0
- package/src/query/compiler/index.ts +276 -0
- package/src/query/compiler/joins.ts +228 -0
- package/src/query/compiler/order-by.ts +139 -0
- package/src/query/compiler/select.ts +173 -0
- package/src/query/index.ts +64 -5
- package/src/query/ir.ts +128 -0
- package/src/query/live-query-collection.ts +509 -0
- package/src/transactions.ts +34 -19
- package/src/types.ts +16 -1
- package/dist/cjs/query/compiled-query.cjs +0 -160
- package/dist/cjs/query/compiled-query.cjs.map +0 -1
- package/dist/cjs/query/compiled-query.d.cts +0 -20
- package/dist/cjs/query/evaluators.cjs +0 -161
- package/dist/cjs/query/evaluators.cjs.map +0 -1
- package/dist/cjs/query/evaluators.d.cts +0 -14
- package/dist/cjs/query/extractors.cjs +0 -122
- package/dist/cjs/query/extractors.cjs.map +0 -1
- package/dist/cjs/query/extractors.d.cts +0 -22
- package/dist/cjs/query/functions.cjs +0 -152
- package/dist/cjs/query/functions.cjs.map +0 -1
- package/dist/cjs/query/functions.d.cts +0 -21
- package/dist/cjs/query/group-by.cjs +0 -88
- package/dist/cjs/query/group-by.cjs.map +0 -1
- package/dist/cjs/query/group-by.d.cts +0 -40
- package/dist/cjs/query/joins.cjs +0 -141
- package/dist/cjs/query/joins.cjs.map +0 -1
- package/dist/cjs/query/joins.d.cts +0 -14
- package/dist/cjs/query/order-by.cjs +0 -185
- package/dist/cjs/query/order-by.cjs.map +0 -1
- package/dist/cjs/query/order-by.d.cts +0 -3
- package/dist/cjs/query/pipeline-compiler.cjs +0 -89
- package/dist/cjs/query/pipeline-compiler.cjs.map +0 -1
- package/dist/cjs/query/pipeline-compiler.d.cts +0 -10
- package/dist/cjs/query/query-builder.cjs +0 -307
- package/dist/cjs/query/query-builder.cjs.map +0 -1
- package/dist/cjs/query/query-builder.d.cts +0 -225
- package/dist/cjs/query/schema.d.cts +0 -100
- package/dist/cjs/query/select.cjs +0 -130
- package/dist/cjs/query/select.cjs.map +0 -1
- package/dist/cjs/query/select.d.cts +0 -3
- package/dist/cjs/query/types.d.cts +0 -189
- package/dist/cjs/query/utils.cjs +0 -154
- package/dist/cjs/query/utils.cjs.map +0 -1
- package/dist/cjs/query/utils.d.cts +0 -37
- package/dist/cjs/utils.cjs +0 -17
- package/dist/cjs/utils.cjs.map +0 -1
- package/dist/cjs/utils.d.cts +0 -3
- package/dist/esm/query/compiled-query.d.ts +0 -20
- package/dist/esm/query/compiled-query.js +0 -160
- package/dist/esm/query/compiled-query.js.map +0 -1
- package/dist/esm/query/evaluators.d.ts +0 -14
- package/dist/esm/query/evaluators.js +0 -161
- package/dist/esm/query/evaluators.js.map +0 -1
- package/dist/esm/query/extractors.d.ts +0 -22
- package/dist/esm/query/extractors.js +0 -122
- package/dist/esm/query/extractors.js.map +0 -1
- package/dist/esm/query/functions.d.ts +0 -21
- package/dist/esm/query/functions.js +0 -152
- package/dist/esm/query/functions.js.map +0 -1
- package/dist/esm/query/group-by.d.ts +0 -40
- package/dist/esm/query/group-by.js +0 -88
- package/dist/esm/query/group-by.js.map +0 -1
- package/dist/esm/query/joins.d.ts +0 -14
- package/dist/esm/query/joins.js +0 -141
- package/dist/esm/query/joins.js.map +0 -1
- package/dist/esm/query/order-by.d.ts +0 -3
- package/dist/esm/query/order-by.js +0 -185
- package/dist/esm/query/order-by.js.map +0 -1
- package/dist/esm/query/pipeline-compiler.d.ts +0 -10
- package/dist/esm/query/pipeline-compiler.js +0 -89
- package/dist/esm/query/pipeline-compiler.js.map +0 -1
- package/dist/esm/query/query-builder.d.ts +0 -225
- package/dist/esm/query/query-builder.js +0 -307
- package/dist/esm/query/query-builder.js.map +0 -1
- package/dist/esm/query/schema.d.ts +0 -100
- package/dist/esm/query/select.d.ts +0 -3
- package/dist/esm/query/select.js +0 -130
- package/dist/esm/query/select.js.map +0 -1
- package/dist/esm/query/types.d.ts +0 -189
- package/dist/esm/query/utils.d.ts +0 -37
- package/dist/esm/query/utils.js +0 -154
- package/dist/esm/query/utils.js.map +0 -1
- package/dist/esm/utils.d.ts +0 -3
- package/dist/esm/utils.js +0 -17
- package/dist/esm/utils.js.map +0 -1
- package/src/query/compiled-query.ts +0 -234
- package/src/query/evaluators.ts +0 -250
- package/src/query/extractors.ts +0 -214
- package/src/query/functions.ts +0 -297
- package/src/query/group-by.ts +0 -139
- package/src/query/joins.ts +0 -260
- package/src/query/order-by.ts +0 -264
- package/src/query/pipeline-compiler.ts +0 -149
- package/src/query/query-builder.ts +0 -902
- package/src/query/schema.ts +0 -268
- package/src/query/select.ts +0 -208
- package/src/query/types.ts +0 -418
- package/src/query/utils.ts +0 -245
- package/src/utils.ts +0 -15
package/src/query/ir.ts
ADDED
|
@@ -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
|
+
}
|