@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/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
+