@michaelstewart/convex-tanstack-db-collection 0.0.1
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/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/cjs/index.cjs +679 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.d.ts +461 -0
- package/dist/esm/index.js +679 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +63 -0
- package/src/ConvexSyncManager.ts +611 -0
- package/src/expression-parser.ts +255 -0
- package/src/index.ts +247 -0
- package/src/serialization.ts +125 -0
- package/src/types.ts +260 -0
package/src/types.ts
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
BaseCollectionConfig,
|
|
3
|
+
ChangeMessageOrDeleteKeyMessage,
|
|
4
|
+
CollectionConfig,
|
|
5
|
+
UtilsRecord,
|
|
6
|
+
} from '@tanstack/db'
|
|
7
|
+
import type { FunctionReference } from 'convex/server'
|
|
8
|
+
import type { StandardSchemaV1 } from '@standard-schema/spec'
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Single filter dimension configuration.
|
|
12
|
+
* Maps a TanStack DB query field to a Convex query argument.
|
|
13
|
+
*/
|
|
14
|
+
export interface FilterDimension {
|
|
15
|
+
/**
|
|
16
|
+
* Field name in TanStack DB queries to extract filter values from.
|
|
17
|
+
* This is the field used in `where` expressions like `m.pageId.eq('p1')`.
|
|
18
|
+
* @example 'pageId'
|
|
19
|
+
*/
|
|
20
|
+
filterField: string
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Argument name to pass to the Convex query for filter values.
|
|
24
|
+
* This should match the array argument in your Convex query.
|
|
25
|
+
* @example 'pageIds' for a Convex query with `args: { pageIds: v.array(v.string()) }`
|
|
26
|
+
*/
|
|
27
|
+
convexArg: string
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* If true, assert that only one value is ever requested for this filter.
|
|
31
|
+
* An error will be thrown if multiple values are requested.
|
|
32
|
+
* When single is true, the value is passed directly (not as an array).
|
|
33
|
+
* @default false
|
|
34
|
+
* @example
|
|
35
|
+
* // Convex query expects: { pageId: v.string() }
|
|
36
|
+
* filters: { filterField: 'pageId', convexArg: 'pageId', single: true }
|
|
37
|
+
*/
|
|
38
|
+
single?: boolean
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Filter configuration supporting 0, 1, or N dimensions.
|
|
43
|
+
* - undefined or [] = sync everything (0 filters)
|
|
44
|
+
* - single object = one filter
|
|
45
|
+
* - array = multiple filters
|
|
46
|
+
*/
|
|
47
|
+
export type FilterConfig = FilterDimension | FilterDimension[] | undefined
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Extracted filter values keyed by convexArg for direct use in query args.
|
|
51
|
+
* Values preserve their original types (strings, numbers, etc.)
|
|
52
|
+
* @example { pageIds: ['p1', 'p2'], authorIds: ['u1'] }
|
|
53
|
+
*/
|
|
54
|
+
export interface ExtractedFilters {
|
|
55
|
+
[convexArg: string]: unknown[]
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Unsubscribe function returned by Convex client.onUpdate
|
|
60
|
+
*/
|
|
61
|
+
export type ConvexUnsubscribe<T> = {
|
|
62
|
+
(): void
|
|
63
|
+
unsubscribe(): void
|
|
64
|
+
getCurrentValue(): T | undefined
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Watch object returned by watchQuery, provides onUpdate subscription.
|
|
69
|
+
* Note: onUpdate callback is called when value changes but doesn't receive the value.
|
|
70
|
+
* Use localQueryResult() to get the current value.
|
|
71
|
+
*/
|
|
72
|
+
export interface ConvexWatch<T> {
|
|
73
|
+
onUpdate: (callback: () => void) => () => void
|
|
74
|
+
localQueryResult: () => T | undefined
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Unsubscribe object returned by ConvexClient.onUpdate
|
|
79
|
+
*/
|
|
80
|
+
export interface ConvexOnUpdateSubscription<T> {
|
|
81
|
+
(): void
|
|
82
|
+
unsubscribe(): void
|
|
83
|
+
getCurrentValue(): T | undefined
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Base client interface with query method
|
|
88
|
+
*/
|
|
89
|
+
interface ConvexClientBase {
|
|
90
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
91
|
+
query(query: FunctionReference<'query'>, args: any): Promise<any>
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* ConvexReactClient pattern: uses watchQuery for subscriptions
|
|
96
|
+
*/
|
|
97
|
+
interface ConvexReactClientLike extends ConvexClientBase {
|
|
98
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
99
|
+
watchQuery(query: FunctionReference<'query'>, args: any): {
|
|
100
|
+
onUpdate(callback: () => void): () => void
|
|
101
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
102
|
+
localQueryResult(): any
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* ConvexClient pattern: uses onUpdate for subscriptions
|
|
108
|
+
*/
|
|
109
|
+
interface ConvexBrowserClientLike extends ConvexClientBase {
|
|
110
|
+
onUpdate(
|
|
111
|
+
query: FunctionReference<'query'>,
|
|
112
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
113
|
+
args: any,
|
|
114
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
115
|
+
callback: (result: any) => void,
|
|
116
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
117
|
+
onError?: (error: any) => void
|
|
118
|
+
): {
|
|
119
|
+
unsubscribe(): void
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* A Convex client that supports query and subscription methods.
|
|
125
|
+
* Compatible with both ConvexClient (browser) and ConvexReactClient (react).
|
|
126
|
+
*
|
|
127
|
+
* ConvexClient uses: client.onUpdate(query, args, callback)
|
|
128
|
+
* ConvexReactClient uses: client.watchQuery(query, args).onUpdate(callback)
|
|
129
|
+
*/
|
|
130
|
+
export type ConvexClientLike = ConvexReactClientLike | ConvexBrowserClientLike
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Configuration for the Convex collection adapter
|
|
134
|
+
*/
|
|
135
|
+
export interface ConvexCollectionConfig<
|
|
136
|
+
T extends object = Record<string, unknown>,
|
|
137
|
+
TKey extends string | number = string | number,
|
|
138
|
+
TSchema extends StandardSchemaV1 = never,
|
|
139
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
140
|
+
> extends Omit<BaseCollectionConfig<T, TKey, TSchema, TUtils>, 'syncMode'> {
|
|
141
|
+
/**
|
|
142
|
+
* The Convex client instance used for queries and subscriptions.
|
|
143
|
+
* Compatible with both ConvexClient and ConvexReactClient.
|
|
144
|
+
*/
|
|
145
|
+
client: ConvexClientLike
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The Convex query function reference for syncing data.
|
|
149
|
+
* This query must accept the configured filter args (arrays of filter values)
|
|
150
|
+
* and an optional `after` timestamp for incremental sync.
|
|
151
|
+
*
|
|
152
|
+
* @example
|
|
153
|
+
* // convex/messages.ts
|
|
154
|
+
* export const sync = userQuery({
|
|
155
|
+
* args: {
|
|
156
|
+
* pageIds: v.array(v.string()),
|
|
157
|
+
* after: v.optional(v.number()),
|
|
158
|
+
* },
|
|
159
|
+
* handler: async (ctx, { pageIds, after = 0 }) => {
|
|
160
|
+
* return await ctx.db
|
|
161
|
+
* .query('messages')
|
|
162
|
+
* .filter(q => pageIds.includes(q.field('pageId')))
|
|
163
|
+
* .filter(q => q.gt(q.field('updatedAt'), after))
|
|
164
|
+
* .collect()
|
|
165
|
+
* },
|
|
166
|
+
* })
|
|
167
|
+
*/
|
|
168
|
+
query: FunctionReference<'query'>
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Filter configuration for syncing data based on query predicates.
|
|
172
|
+
* - undefined or [] = sync everything (0 filters, query takes only { after })
|
|
173
|
+
* - single object = one filter dimension
|
|
174
|
+
* - array = multiple filter dimensions
|
|
175
|
+
*
|
|
176
|
+
* @example
|
|
177
|
+
* // Single filter
|
|
178
|
+
* filters: { filterField: 'pageId', convexArg: 'pageIds' }
|
|
179
|
+
*
|
|
180
|
+
* // Multiple filters
|
|
181
|
+
* filters: [
|
|
182
|
+
* { filterField: 'pageId', convexArg: 'pageIds' },
|
|
183
|
+
* { filterField: 'authorId', convexArg: 'authorIds' },
|
|
184
|
+
* ]
|
|
185
|
+
*/
|
|
186
|
+
filters?: FilterConfig
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* The field name on items that contains the timestamp for LWW conflict resolution.
|
|
190
|
+
* Used to determine which version of an item is newer.
|
|
191
|
+
* @default 'updatedAt'
|
|
192
|
+
*/
|
|
193
|
+
updatedAtFieldName?: string
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Debounce time in milliseconds for batching loadSubset calls.
|
|
197
|
+
* Multiple calls within this window will be batched together.
|
|
198
|
+
* @default 50
|
|
199
|
+
*/
|
|
200
|
+
debounceMs?: number
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Overlap window in milliseconds when rewinding the subscription cursor.
|
|
204
|
+
* This ensures we don't miss updates from transactions that committed out-of-order
|
|
205
|
+
* (commit order doesn't match timestamp generation order across different keys).
|
|
206
|
+
* @default 10000
|
|
207
|
+
*/
|
|
208
|
+
tailOverlapMs?: number
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Number of messages to receive before re-subscribing with an advanced cursor.
|
|
212
|
+
* This reduces Convex function invocations by batching cursor updates.
|
|
213
|
+
* Set to 0 to disable automatic cursor advancement.
|
|
214
|
+
* @default 10
|
|
215
|
+
*/
|
|
216
|
+
resubscribeThreshold?: number
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Internal sync parameters passed to the sync function
|
|
221
|
+
*/
|
|
222
|
+
export interface ConvexSyncParams<
|
|
223
|
+
T extends object = Record<string, unknown>,
|
|
224
|
+
TKey extends string | number = string | number,
|
|
225
|
+
> {
|
|
226
|
+
collection: {
|
|
227
|
+
get: (key: TKey) => T | undefined
|
|
228
|
+
has: (key: TKey) => boolean
|
|
229
|
+
}
|
|
230
|
+
begin: () => void
|
|
231
|
+
write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void
|
|
232
|
+
commit: () => void
|
|
233
|
+
markReady: () => void
|
|
234
|
+
truncate: () => void
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
/**
|
|
238
|
+
* Options for the ConvexSyncManager
|
|
239
|
+
*/
|
|
240
|
+
export interface ConvexSyncManagerOptions<T extends object = Record<string, unknown>> {
|
|
241
|
+
client: ConvexClientLike
|
|
242
|
+
query: FunctionReference<'query'>
|
|
243
|
+
filterDimensions: FilterDimension[]
|
|
244
|
+
updatedAtFieldName: string
|
|
245
|
+
debounceMs: number
|
|
246
|
+
tailOverlapMs: number
|
|
247
|
+
resubscribeThreshold: number
|
|
248
|
+
getKey: (item: T) => string | number
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* The complete collection config after processing by convexCollectionOptions
|
|
253
|
+
*/
|
|
254
|
+
export type ConvexCollectionFullConfig<
|
|
255
|
+
T extends object = Record<string, unknown>,
|
|
256
|
+
TKey extends string | number = string | number,
|
|
257
|
+
TSchema extends StandardSchemaV1 = never,
|
|
258
|
+
TUtils extends UtilsRecord = UtilsRecord,
|
|
259
|
+
> = CollectionConfig<T, TKey, TSchema, TUtils>
|
|
260
|
+
|