@michaelstewart/convex-tanstack-db-collection 0.0.1 → 0.0.2

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/README.md CHANGED
@@ -18,8 +18,8 @@ Imagine a Slack-like app with messages inside channels:
18
18
 
19
19
  ```typescript
20
20
  // convex/schema.ts
21
- import { defineSchema, defineTable } from "convex/server"
22
- import { v } from "convex/values"
21
+ import { defineSchema, defineTable } from 'convex/server'
22
+ import { v } from 'convex/values'
23
23
 
24
24
  export default defineSchema({
25
25
  channels: defineTable({
@@ -28,13 +28,13 @@ export default defineSchema({
28
28
  messages: defineTable({
29
29
  // Client-generated UUID to support optimistic inserts
30
30
  id: v.string(),
31
- channelId: v.id("channels"),
31
+ channelId: v.id('channels'),
32
32
  authorId: v.string(),
33
33
  body: v.string(),
34
34
  updatedAt: v.number(),
35
35
  })
36
- .index("by_channel_updatedAt", ["channelId", "updatedAt"])
37
- .index("by_author_updatedAt", ["authorId", "updatedAt"]),
36
+ .index('by_channel_updatedAt', ['channelId', 'updatedAt'])
37
+ .index('by_author_updatedAt', ['authorId', 'updatedAt']),
38
38
  })
39
39
  ```
40
40
 
@@ -50,13 +50,14 @@ const messagesCollection = createCollection(
50
50
  query: api.messages.getMessagesAfter,
51
51
  filters: { filterField: 'channelId', convexArg: 'channelIds' },
52
52
  getKey: (msg) => msg.id,
53
- })
53
+ }),
54
54
  )
55
55
 
56
56
  // In your UI - TanStack DB extracts channelId from the where clause
57
- const { data: messages } = useLiveQuery(q =>
58
- q.from({ msg: messagesCollection })
59
- .where(({ msg }) => msg.channelId.eq(currentChannelId))
57
+ const { data: messages } = useLiveQuery((q) =>
58
+ q
59
+ .from({ msg: messagesCollection })
60
+ .where(({ msg }) => msg.channelId.eq(currentChannelId)),
60
61
  )
61
62
  ```
62
63
 
@@ -67,21 +68,21 @@ import { query } from './_generated/server'
67
68
 
68
69
  export const getMessagesAfter = query({
69
70
  args: {
70
- channelIds: v.optional(v.array(v.id("channels"))),
71
+ channelIds: v.optional(v.array(v.id('channels'))),
71
72
  after: v.optional(v.number()),
72
73
  },
73
74
  handler: async (ctx, { channelIds, after = 0 }) => {
74
75
  if (!channelIds || channelIds.length === 0) return []
75
76
 
76
77
  const results = await Promise.all(
77
- channelIds.map(channelId =>
78
+ channelIds.map((channelId) =>
78
79
  ctx.db
79
- .query("messages")
80
- .withIndex("by_channel_updatedAt", q =>
81
- q.eq("channelId", channelId).gt("updatedAt", after)
80
+ .query('messages')
81
+ .withIndex('by_channel_updatedAt', (q) =>
82
+ q.eq('channelId', channelId).gt('updatedAt', after),
82
83
  )
83
- .collect()
84
- )
84
+ .collect(),
85
+ ),
85
86
  )
86
87
  return results.flat()
87
88
  },
@@ -102,6 +103,7 @@ Convex doesn't have a global transaction log—there's no single writer assignin
102
103
  This adapter uses these two Convex superpowers to construct an **update log** from an index on `updatedAt`. Because OCC guarantees that `updatedAt` is non-decreasing for any given key (it acts as a Lamport timestamp), we can query `after: cursor` to fetch only newer records.
103
104
 
104
105
  The result is efficient cursor-based sync—with two caveats:
106
+
105
107
  1. Index records in the last few seconds of the update log can become visible out of order- solved with [tail overlap](#the-tail-overlap-why-we-need-it)
106
108
  2. [Hard deletes are unsupported](#hard-deletes-not-supported)
107
109
 
@@ -186,19 +188,21 @@ const messagesCollection = createCollection(
186
188
  { filterField: 'authorId', convexArg: 'authorIds' },
187
189
  ],
188
190
  getKey: (msg) => msg.id,
189
- })
191
+ }),
190
192
  )
191
193
 
192
194
  // View messages in a channel
193
- const { data: channelMessages } = useLiveQuery(q =>
194
- q.from({ msg: messagesCollection })
195
- .where(({ msg }) => msg.channelId.eq(channelId))
195
+ const { data: channelMessages } = useLiveQuery((q) =>
196
+ q
197
+ .from({ msg: messagesCollection })
198
+ .where(({ msg }) => msg.channelId.eq(channelId)),
196
199
  )
197
200
 
198
201
  // Or view all messages by an author
199
- const { data: authorMessages } = useLiveQuery(q =>
200
- q.from({ msg: messagesCollection })
201
- .where(({ msg }) => msg.authorId.eq(userId))
202
+ const { data: authorMessages } = useLiveQuery((q) =>
203
+ q
204
+ .from({ msg: messagesCollection })
205
+ .where(({ msg }) => msg.authorId.eq(userId)),
202
206
  )
203
207
  ```
204
208
 
@@ -210,14 +214,16 @@ For small datasets, sync everything:
210
214
  const allMessagesCollection = createCollection(
211
215
  convexCollectionOptions({
212
216
  client: convexClient,
213
- query: api.messages.getAllMessagesAfter, // Query takes only { after }
217
+ query: api.messages.getAllMessagesAfter, // Query takes only { after }
214
218
  getKey: (msg) => msg.id,
215
- })
219
+ }),
216
220
  )
217
221
  ```
218
222
 
219
223
  </details>
220
224
 
225
+ See `examples/convex-tutorial` for a working chat app demonstrating optimistic inserts and cursor-based sync.
226
+
221
227
  <details>
222
228
  <summary><strong>Convex Query Setup (Advanced)</strong></summary>
223
229
 
@@ -232,7 +238,7 @@ import { query } from './_generated/server'
232
238
 
233
239
  export const getMessagesAfter = query({
234
240
  args: {
235
- channelIds: v.optional(v.array(v.id("channels"))),
241
+ channelIds: v.optional(v.array(v.id('channels'))),
236
242
  authorIds: v.optional(v.array(v.string())),
237
243
  after: v.optional(v.number()),
238
244
  },
@@ -240,14 +246,14 @@ export const getMessagesAfter = query({
240
246
  // Query each channel using the compound index
241
247
  if (channelIds && channelIds.length > 0) {
242
248
  const results = await Promise.all(
243
- channelIds.map(channelId =>
249
+ channelIds.map((channelId) =>
244
250
  ctx.db
245
- .query("messages")
246
- .withIndex("by_channel_updatedAt", q =>
247
- q.eq("channelId", channelId).gt("updatedAt", after)
251
+ .query('messages')
252
+ .withIndex('by_channel_updatedAt', (q) =>
253
+ q.eq('channelId', channelId).gt('updatedAt', after),
248
254
  )
249
- .collect()
250
- )
255
+ .collect(),
256
+ ),
251
257
  )
252
258
  return results.flat()
253
259
  }
@@ -255,14 +261,14 @@ export const getMessagesAfter = query({
255
261
  // For author queries, use a different index (or filter)
256
262
  if (authorIds && authorIds.length > 0) {
257
263
  const results = await Promise.all(
258
- authorIds.map(authorId =>
264
+ authorIds.map((authorId) =>
259
265
  ctx.db
260
- .query("messages")
261
- .withIndex("by_author_updatedAt", q =>
262
- q.eq("authorId", authorId).gt("updatedAt", after)
266
+ .query('messages')
267
+ .withIndex('by_author_updatedAt', (q) =>
268
+ q.eq('authorId', authorId).gt('updatedAt', after),
263
269
  )
264
- .collect()
265
- )
270
+ .collect(),
271
+ ),
266
272
  )
267
273
  return results.flat()
268
274
  }
@@ -359,16 +365,17 @@ await ctx.db.delete(id)
359
365
  // Set a status field:
360
366
  await ctx.db.patch(id, {
361
367
  status: 'deleted',
362
- updatedAt: Date.now()
368
+ updatedAt: Date.now(),
363
369
  })
364
370
  ```
365
371
 
366
372
  The sync will receive the updated record with `status: 'deleted'`. Your UI can filter out deleted items:
367
373
 
368
374
  ```typescript
369
- const { data } = useLiveQuery(q =>
370
- q.from({ item: itemsCollection })
371
- .where(({ item }) => item.status.eq('active'))
375
+ const { data } = useLiveQuery((q) =>
376
+ q
377
+ .from({ item: itemsCollection })
378
+ .where(({ item }) => item.status.eq('active')),
372
379
  )
373
380
  ```
374
381
 
@@ -381,5 +388,6 @@ Only `.eq()` and `.in()` operators are supported for filter extraction. Complex
381
388
  **Consider starting with [query-collection](https://tanstack.com/db/latest/docs/collections/query-collection)** if you have few items on screen. It's simpler, uses Convex's built-in `useQuery` under the hood, and is sufficient for many apps.
382
389
 
383
390
  This adapter is for when you need:
391
+
384
392
  - **On-demand sync**: Specifically load data matching your current queries
385
393
  - **Cursor-based efficiency**: Avoid re-fetching unchanged data on every subscription update
@@ -635,7 +635,10 @@ function convexCollectionOptions(config) {
635
635
  if (filterDimensions.length === 0) {
636
636
  return syncManager.requestFilters({});
637
637
  }
638
- const extracted = extractMultipleFilterValues(options, filterDimensions);
638
+ const extracted = extractMultipleFilterValues(
639
+ options,
640
+ filterDimensions
641
+ );
639
642
  if (Object.keys(extracted).length === 0) {
640
643
  return Promise.resolve();
641
644
  }
@@ -646,7 +649,10 @@ function convexCollectionOptions(config) {
646
649
  syncManager.releaseFilters({});
647
650
  return;
648
651
  }
649
- const extracted = extractMultipleFilterValues(options, filterDimensions);
652
+ const extracted = extractMultipleFilterValues(
653
+ options,
654
+ filterDimensions
655
+ );
650
656
  if (Object.keys(extracted).length > 0) {
651
657
  syncManager.releaseFilters(extracted);
652
658
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.cjs","sources":["../../src/serialization.ts","../../src/ConvexSyncManager.ts","../../src/expression-parser.ts","../../src/index.ts"],"sourcesContent":["/**\n * Value serialization utilities for stable hashing and round-tripping.\n *\n * Adapted from TanStack DB's query-db-collection:\n * https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/serialization.ts\n */\n\ninterface TypeMarker {\n __type: string\n value?: unknown\n sign?: number\n}\n\nfunction isTypeMarker(value: unknown): value is TypeMarker {\n return (\n typeof value === `object` &&\n value !== null &&\n `__type` in value &&\n typeof (value as TypeMarker).__type === `string`\n )\n}\n\n/**\n * Serializes a value into a JSON-safe format that preserves special JS types.\n * Handles: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function serializeValue(value: unknown): unknown {\n if (value === undefined) {\n return { __type: `undefined` }\n }\n\n if (typeof value === `number`) {\n if (Number.isNaN(value)) {\n return { __type: `nan` }\n }\n if (value === Number.POSITIVE_INFINITY) {\n return { __type: `infinity`, sign: 1 }\n }\n if (value === Number.NEGATIVE_INFINITY) {\n return { __type: `infinity`, sign: -1 }\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `date`, value: value.toJSON() }\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => serializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n serializeValue(val),\n ])\n )\n }\n\n return value\n}\n\n/**\n * Deserializes a value back from its JSON-safe format.\n * Restores: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function deserializeValue(value: unknown): unknown {\n if (isTypeMarker(value)) {\n switch (value.__type) {\n case `undefined`:\n return undefined\n case `nan`:\n return NaN\n case `infinity`:\n return value.sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY\n case `date`:\n return new Date(value.value as string)\n default:\n return value\n }\n }\n\n if (value === null || typeof value === `string` || typeof value === `number` || typeof value === `boolean`) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => deserializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n deserializeValue(val),\n ])\n )\n }\n\n return value\n}\n\n/**\n * Converts a value to a stable string key for use in Sets/Maps.\n */\nexport function toKey(value: unknown): string {\n return JSON.stringify(serializeValue(value))\n}\n\n/**\n * Restores a value from its string key representation.\n */\nexport function fromKey(key: string): unknown {\n return deserializeValue(JSON.parse(key))\n}\n","import type { FunctionReference } from 'convex/server'\nimport type { ChangeMessageOrDeleteKeyMessage } from '@tanstack/db'\nimport type {\n ConvexClientLike,\n ConvexSyncManagerOptions,\n ExtractedFilters,\n FilterDimension,\n} from './types.js'\nimport { toKey, fromKey } from './serialization.js'\n\nexport type { ConvexSyncManagerOptions }\n\n/**\n * Sync callbacks passed from TanStack DB's sync function\n */\nexport interface SyncCallbacks<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n collection: {\n get: (key: TKey) => T | undefined\n has: (key: TKey) => boolean\n }\n begin: () => void\n write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void\n commit: () => void\n markReady: () => void\n}\n\n/**\n * ConvexSyncManager - Manages real-time synchronization with Convex backend\n *\n * Implements the \"backfill + tail\" pattern:\n * 1. When new filters are requested, backfill with `after: 0` to get full history\n * 2. Maintain a single live subscription for all active filter values with `after: globalCursor - tailOverlapMs`\n * 3. Use LWW (Last-Write-Wins) to handle overlapping data from backfill and subscription\n *\n * Supports 0, 1, or N filter dimensions:\n * - 0 filters: Global sync with just { after }\n * - 1+ filters: Filter-based sync with values extracted from where clauses\n */\nexport class ConvexSyncManager<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n // Configuration\n private client: ConvexClientLike\n private query: FunctionReference<`query`>\n private filterDimensions: FilterDimension[]\n private updatedAtFieldName: string\n private debounceMs: number\n private tailOverlapMs: number\n private resubscribeThreshold: number\n private getKey: (item: T) => TKey\n\n // State - per-dimension tracking (keyed by convexArg)\n private activeDimensions = new Map<string, Set<string>>()\n private refCounts = new Map<string, number>() // composite key -> count\n private pendingFilters: ExtractedFilters = {}\n private globalCursor = 0\n private currentSubscription: (() => void) | null = null\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n private isProcessing = false\n private markedReady = false\n\n // For 0-filter case (global sync)\n private hasRequestedGlobal = false\n private globalRefCount = 0\n\n // Track messages received since last subscription to batch cursor updates\n private messagesSinceSubscription = 0\n\n // Sync callbacks (set when sync() is called)\n private callbacks: SyncCallbacks<T, TKey> | null = null\n\n constructor(options: ConvexSyncManagerOptions<T>) {\n this.client = options.client\n this.query = options.query\n this.filterDimensions = options.filterDimensions\n this.updatedAtFieldName = options.updatedAtFieldName\n this.debounceMs = options.debounceMs\n this.tailOverlapMs = options.tailOverlapMs\n this.resubscribeThreshold = options.resubscribeThreshold\n this.getKey = options.getKey as (item: T) => TKey\n\n // Initialize activeDimensions for each filter dimension\n for (const dim of this.filterDimensions) {\n this.activeDimensions.set(dim.convexArg, new Set())\n }\n }\n\n /**\n * Initialize the sync manager with callbacks from TanStack DB\n */\n setCallbacks(callbacks: SyncCallbacks<T, TKey>): void {\n this.callbacks = callbacks\n }\n\n /**\n * Create a composite key for ref counting multi-filter combinations.\n * Uses serialized values for deterministic keys.\n */\n private createCompositeKey(filters: ExtractedFilters): string {\n // Sort by convexArg for deterministic ordering\n const sorted = Object.keys(filters)\n .sort()\n .reduce(\n (acc, key) => {\n // Serialize values and sort for deterministic ordering\n const values = filters[key]\n if (values) {\n acc[key] = values.map((v) => toKey(v)).sort()\n }\n return acc\n },\n {} as Record<string, string[]>\n )\n return JSON.stringify(sorted)\n }\n\n /**\n * Request filters to be synced (called by loadSubset)\n * Filters are batched via debouncing for efficiency\n */\n requestFilters(filters: ExtractedFilters): Promise<void> {\n // Handle 0-filter case (global sync)\n if (this.filterDimensions.length === 0) {\n this.globalRefCount++\n if (!this.hasRequestedGlobal) {\n this.hasRequestedGlobal = true\n return this.scheduleProcessing()\n }\n return Promise.resolve()\n }\n\n // Increment ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = this.refCounts.get(compositeKey) || 0\n this.refCounts.set(compositeKey, count + 1)\n\n // Track which values are new (not yet active)\n let hasNewValues = false\n for (const [convexArg, values] of Object.entries(filters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (!activeSet) continue\n\n // Find the dimension config for single validation\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n\n // Validate single constraint before adding values\n if (dim?.single) {\n const existingCount = activeSet.size\n const pendingCount = this.pendingFilters[convexArg]?.length ?? 0\n const newValues = values.filter((v) => {\n const serialized = toKey(v)\n const alreadyActive = activeSet.has(serialized)\n const alreadyPending = this.pendingFilters[convexArg]?.some(\n (pv) => toKey(pv) === serialized\n )\n return !alreadyActive && !alreadyPending\n })\n\n if (existingCount + pendingCount + newValues.length > 1) {\n throw new Error(\n `Filter '${dim.filterField}' is configured as single but multiple values were requested. ` +\n `Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. ` +\n `Use single: false if you need to sync multiple values.`\n )\n }\n }\n\n for (const value of values) {\n const serialized = toKey(value)\n if (!activeSet.has(serialized)) {\n // Add to pending filters\n if (!this.pendingFilters[convexArg]) {\n this.pendingFilters[convexArg] = []\n }\n // Check if already pending (by serialized key)\n const alreadyPending = this.pendingFilters[convexArg].some(\n (v) => toKey(v) === serialized\n )\n if (!alreadyPending) {\n this.pendingFilters[convexArg].push(value)\n hasNewValues = true\n }\n }\n }\n }\n\n // If there are new values, schedule processing\n if (hasNewValues) {\n return this.scheduleProcessing()\n }\n\n return Promise.resolve()\n }\n\n /**\n * Release filters when no longer needed (called by unloadSubset)\n */\n releaseFilters(filters: ExtractedFilters): void {\n // Handle 0-filter case\n if (this.filterDimensions.length === 0) {\n this.globalRefCount = Math.max(0, this.globalRefCount - 1)\n if (this.globalRefCount === 0 && this.hasRequestedGlobal) {\n this.hasRequestedGlobal = false\n this.updateSubscription()\n }\n return\n }\n\n // Decrement ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = (this.refCounts.get(compositeKey) || 0) - 1\n\n if (count <= 0) {\n this.refCounts.delete(compositeKey)\n } else {\n this.refCounts.set(compositeKey, count)\n }\n\n // Check if any values are now unreferenced\n this.cleanupUnreferencedValues()\n }\n\n /**\n * Remove values from activeDimensions that are no longer referenced\n * by any composite key in refCounts\n */\n private cleanupUnreferencedValues(): void {\n // Collect all referenced serialized values per dimension\n const referencedValues = new Map<string, Set<string>>()\n for (const dim of this.filterDimensions) {\n referencedValues.set(dim.convexArg, new Set())\n }\n\n // Walk through all composite keys and collect their serialized values\n for (const compositeKey of this.refCounts.keys()) {\n try {\n // Composite keys store values as already-serialized strings\n const filters = JSON.parse(compositeKey) as Record<string, string[]>\n for (const [convexArg, serializedValues] of Object.entries(filters)) {\n const refSet = referencedValues.get(convexArg)\n if (refSet) {\n for (const serialized of serializedValues) {\n refSet.add(serialized)\n }\n }\n }\n } catch {\n // Skip invalid keys\n }\n }\n\n // Remove unreferenced values from activeDimensions\n // (activeDimensions stores serialized keys)\n let needsSubscriptionUpdate = false\n for (const [convexArg, activeSet] of this.activeDimensions) {\n const refSet = referencedValues.get(convexArg)!\n for (const serialized of activeSet) {\n if (!refSet.has(serialized)) {\n activeSet.delete(serialized)\n needsSubscriptionUpdate = true\n }\n }\n }\n\n if (needsSubscriptionUpdate) {\n this.updateSubscription()\n }\n }\n\n /**\n * Schedule debounced processing of pending filters\n */\n private scheduleProcessing(): Promise<void> {\n return new Promise((resolve, reject) => {\n // Clear existing timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n }\n\n // Schedule processing\n this.debounceTimer = setTimeout(async () => {\n try {\n await this.processFilterBatch()\n resolve()\n } catch (error) {\n reject(error)\n }\n }, this.debounceMs)\n })\n }\n\n /**\n * Process the current batch of pending filters\n */\n private async processFilterBatch(): Promise<void> {\n if (this.isProcessing) {\n return\n }\n\n // Check if there's anything to process\n const hasPendingFilters = Object.keys(this.pendingFilters).length > 0\n const needsGlobalSync = this.filterDimensions.length === 0 && this.hasRequestedGlobal\n\n if (!hasPendingFilters && !needsGlobalSync) {\n return\n }\n\n this.isProcessing = true\n\n try {\n if (this.filterDimensions.length === 0) {\n // 0-filter case: global sync\n await this.runGlobalBackfill()\n } else {\n // Collect new filter values that need backfill\n const newFilters = { ...this.pendingFilters }\n this.pendingFilters = {}\n\n // Add to active dimensions (store serialized keys)\n for (const [convexArg, values] of Object.entries(newFilters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (activeSet) {\n for (const value of values) {\n activeSet.add(toKey(value))\n }\n }\n }\n\n // Run backfill for new filter values (fetch full history)\n await this.runBackfill(newFilters)\n }\n\n // Update the live subscription to include all active values\n this.updateSubscription()\n\n // Mark ready after first successful sync\n if (!this.markedReady && this.callbacks) {\n this.callbacks.markReady()\n this.markedReady = true\n }\n } finally {\n this.isProcessing = false\n }\n }\n\n /**\n * Run global backfill for 0-filter case\n */\n private async runGlobalBackfill(): Promise<void> {\n try {\n const args: Record<string, unknown> = { after: 0 }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Global backfill error:', error)\n throw error\n }\n }\n\n /**\n * Run backfill query for new filter values to get their full history\n */\n private async runBackfill(newFilters: ExtractedFilters): Promise<void> {\n if (Object.keys(newFilters).length === 0) return\n\n try {\n // Query with after: 0 to get full history for new filter values\n const args: Record<string, unknown> = {\n ...newFilters,\n after: 0,\n }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Backfill error:', error)\n throw error\n }\n }\n\n /**\n * Build query args from all active dimensions\n */\n private buildQueryArgs(after: number): Record<string, unknown> {\n const args: Record<string, unknown> = { after }\n\n // Deserialize values back to original types for the Convex query\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n const values = [...serializedValues].map((s) => fromKey(s))\n\n // Check if this dimension is configured as single\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n args[convexArg] = dim?.single ? values[0] : values\n }\n\n return args\n }\n\n /**\n * Update the live subscription to cover all active filter values\n */\n private updateSubscription(): void {\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Reset message counter for new subscription\n this.messagesSinceSubscription = 0\n\n // Check if we should subscribe\n if (this.filterDimensions.length === 0) {\n // 0-filter case: subscribe if global sync is active\n if (!this.hasRequestedGlobal) {\n return\n }\n } else {\n // Check if any dimension has active values\n let hasActiveValues = false\n for (const activeSet of this.activeDimensions.values()) {\n if (activeSet.size > 0) {\n hasActiveValues = true\n break\n }\n }\n if (!hasActiveValues) {\n return\n }\n }\n\n // Calculate cursor with overlap to avoid missing updates\n const cursor = Math.max(0, this.globalCursor - this.tailOverlapMs)\n\n // Build subscription args\n const args = this.buildQueryArgs(cursor)\n\n // Runtime detection: ConvexClient has onUpdate, ConvexReactClient has watchQuery\n if ('onUpdate' in this.client) {\n // ConvexClient pattern: client.onUpdate(query, args, callback)\n const subscription = this.client.onUpdate(\n this.query,\n args as any,\n (result: unknown) => {\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n },\n (error: unknown) => {\n console.error(`[ConvexSyncManager] Subscription error:`, error)\n }\n )\n this.currentSubscription = () => subscription.unsubscribe()\n } else {\n // ConvexReactClient pattern: client.watchQuery(query, args).onUpdate(callback)\n const watch = this.client.watchQuery(this.query, args as any)\n this.currentSubscription = watch.onUpdate(() => {\n // Get current value from the watch\n const result = watch.localQueryResult()\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n })\n }\n }\n\n /**\n * Handle incoming data from backfill or subscription\n * Uses LWW (Last-Write-Wins) to resolve conflicts\n */\n private handleIncomingData(items: T[]): void {\n if (!this.callbacks || items.length === 0) return\n\n const { collection, begin, write, commit } = this.callbacks\n\n // Track if we see new items that advance the cursor\n const previousCursor = this.globalCursor\n let newItemCount = 0\n\n begin()\n\n for (const item of items) {\n const key = this.getKey(item)\n const incomingTs = (item as any)[this.updatedAtFieldName] as number | undefined\n\n // Update global cursor to track the latest timestamp we've seen\n if (incomingTs !== undefined && incomingTs > this.globalCursor) {\n this.globalCursor = incomingTs\n }\n\n const existing = collection.get(key)\n\n if (!existing) {\n // New item - insert\n write({ type: `insert`, value: item })\n // Count as new if it's beyond the previous cursor (not from overlap)\n if (incomingTs !== undefined && incomingTs > previousCursor) {\n newItemCount++\n }\n } else {\n // Existing item - check if incoming is fresher (LWW)\n const existingTs = (existing as any)[this.updatedAtFieldName] as number | undefined\n\n if (incomingTs !== undefined && existingTs !== undefined) {\n if (incomingTs > existingTs) {\n // Incoming is fresher - update\n write({ type: `update`, value: item })\n // Count as new if it's beyond the previous cursor\n if (incomingTs > previousCursor) {\n newItemCount++\n }\n }\n // Otherwise skip (stale data from overlap)\n } else if (incomingTs !== undefined) {\n // Existing has no timestamp, incoming does - update\n write({ type: `update`, value: item })\n }\n // If incoming has no timestamp, skip (can't determine freshness)\n }\n }\n\n commit()\n\n // Track only new messages (beyond previous cursor) for cursor advancement\n this.messagesSinceSubscription += newItemCount\n\n // Re-subscribe with advanced cursor after threshold is reached\n if (\n this.resubscribeThreshold > 0 &&\n this.messagesSinceSubscription >= this.resubscribeThreshold &&\n this.currentSubscription !== null\n ) {\n this.updateSubscription()\n }\n }\n\n /**\n * Clean up all resources\n */\n cleanup(): void {\n // Clear debounce timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Clear state\n for (const activeSet of this.activeDimensions.values()) {\n activeSet.clear()\n }\n this.refCounts.clear()\n this.pendingFilters = {}\n this.globalCursor = 0\n this.markedReady = false\n this.hasRequestedGlobal = false\n this.globalRefCount = 0\n this.messagesSinceSubscription = 0\n this.callbacks = null\n }\n\n /**\n * Get debug info about current state\n */\n getDebugInfo(): {\n activeDimensions: Record<string, unknown[]>\n globalCursor: number\n pendingFilters: ExtractedFilters\n hasSubscription: boolean\n markedReady: boolean\n hasRequestedGlobal: boolean\n messagesSinceSubscription: number\n } {\n const activeDimensions: Record<string, unknown[]> = {}\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n activeDimensions[convexArg] = [...serializedValues].map((s) => fromKey(s))\n }\n\n return {\n activeDimensions,\n globalCursor: this.globalCursor,\n pendingFilters: { ...this.pendingFilters },\n hasSubscription: this.currentSubscription !== null,\n markedReady: this.markedReady,\n hasRequestedGlobal: this.hasRequestedGlobal,\n messagesSinceSubscription: this.messagesSinceSubscription,\n }\n }\n}\n","import type { LoadSubsetOptions } from '@tanstack/db'\nimport type { ExtractedFilters, FilterDimension } from './types.js'\nimport { toKey } from './serialization.js'\n\n/**\n * TanStack DB expression types (simplified for our needs)\n * These mirror the IR types from @tanstack/db\n */\ninterface PropRef {\n type: `ref`\n path: Array<string>\n}\n\ninterface Value {\n type: `val`\n value: unknown\n}\n\ninterface Func {\n type: `func`\n name: string\n args: Array<BasicExpression>\n}\n\ntype BasicExpression = PropRef | Value | Func\n\n/**\n * Check if a value is a PropRef expression\n */\nfunction isPropRef(expr: unknown): expr is PropRef {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `ref` &&\n `path` in expr &&\n Array.isArray((expr as PropRef).path)\n )\n}\n\n/**\n * Check if a value is a Value expression\n */\nfunction isValue(expr: unknown): expr is Value {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `val` &&\n `value` in expr\n )\n}\n\n/**\n * Check if a value is a Func expression\n */\nfunction isFunc(expr: unknown): expr is Func {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `func` &&\n `name` in expr &&\n `args` in expr &&\n Array.isArray((expr as Func).args)\n )\n}\n\n/**\n * Check if a PropRef matches our target field.\n * Handles both aliased (e.g., ['msg', 'pageId']) and direct (e.g., ['pageId']) paths.\n */\nfunction propRefMatchesField(propRef: PropRef, fieldName: string): boolean {\n const { path } = propRef\n // Direct field reference: ['pageId']\n if (path.length === 1 && path[0] === fieldName) {\n return true\n }\n // Aliased field reference: ['msg', 'pageId'] or ['m', 'pageId']\n if (path.length === 2 && path[1] === fieldName) {\n return true\n }\n return false\n}\n\n/**\n * Extract filter values from an 'eq' function call.\n * Pattern: eq(ref(filterField), val(x)) or eq(val(x), ref(filterField))\n */\nfunction extractFromEq(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // eq(ref, val)\n if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {\n return [right.value]\n }\n\n // eq(val, ref) - reversed order\n if (isValue(left) && isPropRef(right) && propRefMatchesField(right, filterField)) {\n return [left.value]\n }\n\n return []\n}\n\n/**\n * Extract filter values from an 'in' function call.\n * Pattern: in(ref(filterField), val([a, b, c]))\n */\nfunction extractFromIn(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // in(ref, val)\n if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {\n const val = right.value\n if (Array.isArray(val)) {\n return val\n }\n }\n\n return []\n}\n\n/**\n * Recursively walk an expression tree to find all filter values for the given field.\n * Handles 'eq', 'in', 'and', and 'or' expressions.\n */\nfunction walkExpression(expr: unknown, filterField: string): unknown[] {\n if (!isFunc(expr)) return []\n\n const { name, args } = expr\n const results: unknown[] = []\n\n switch (name) {\n case `eq`:\n results.push(...extractFromEq(expr, filterField))\n break\n\n case `in`:\n results.push(...extractFromIn(expr, filterField))\n break\n\n case `and`:\n case `or`:\n // Recursively process all arguments\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n\n // For other functions, recursively check their arguments\n // (in case of nested expressions)\n default:\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n }\n\n return results\n}\n\n/**\n * Extract filter values from LoadSubsetOptions.\n *\n * Parses the `where` expression to find equality (`.eq()`) and set membership (`.in()`)\n * comparisons for the specified filter field.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterField - The field name to extract values for (e.g., 'pageId')\n * @returns Array of unique filter values found in the expression\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1'))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n *\n * @example\n * // For query: where(m => inArray(m.pageId, ['page-1', 'page-2']))\n * extractFilterValues(options, 'pageId') // returns ['page-1', 'page-2']\n *\n * @example\n * // For query: where(m => and(m.pageId.eq('page-1'), m.status.eq('active')))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n */\nexport function extractFilterValues(\n options: LoadSubsetOptions,\n filterField: string\n): unknown[] {\n const { where } = options\n\n if (!where) {\n return []\n }\n\n // Extract from the where expression\n const values = walkExpression(where, filterField)\n\n // Return unique values using toKey for stable identity comparison\n const seen = new Set<string>()\n const unique: unknown[] = []\n for (const value of values) {\n const key = toKey(value)\n if (!seen.has(key)) {\n seen.add(key)\n unique.push(value)\n }\n }\n\n return unique\n}\n\n/**\n * Check if LoadSubsetOptions contains a filter for the specified field.\n */\nexport function hasFilterField(options: LoadSubsetOptions, filterField: string): boolean {\n return extractFilterValues(options, filterField).length > 0\n}\n\n/**\n * Extract filter values for multiple filter dimensions from LoadSubsetOptions.\n *\n * Parses the `where` expression to find values for each configured filter dimension.\n * Results are keyed by convexArg for direct use in Convex query args.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterDimensions - Array of filter dimensions to extract\n * @returns Object mapping convexArg -> values for dimensions with matches\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1') && m.authorId.eq('user-1'))\n * extractMultipleFilterValues(options, [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ])\n * // returns { pageIds: ['page-1'], authorIds: ['user-1'] }\n */\nexport function extractMultipleFilterValues(\n options: LoadSubsetOptions,\n filterDimensions: FilterDimension[]\n): ExtractedFilters {\n const result: ExtractedFilters = {}\n\n for (const dim of filterDimensions) {\n const values = extractFilterValues(options, dim.filterField)\n if (values.length > 0) {\n result[dim.convexArg] = values\n }\n }\n\n return result\n}\n","import type {\n CollectionConfig,\n LoadSubsetOptions,\n SyncConfig,\n UtilsRecord,\n} from '@tanstack/db'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { FunctionReference, FunctionReturnType } from 'convex/server'\nimport { ConvexSyncManager } from './ConvexSyncManager.js'\nimport { extractMultipleFilterValues } from './expression-parser.js'\nimport type { ConvexCollectionConfig, FilterConfig, FilterDimension } from './types.js'\n\n// Re-export types\nexport type {\n ConvexCollectionConfig,\n ConvexUnsubscribe,\n ExtractedFilters,\n FilterConfig,\n FilterDimension,\n} from './types.js'\nexport { extractFilterValues, extractMultipleFilterValues, hasFilterField } from './expression-parser.js'\nexport { ConvexSyncManager } from './ConvexSyncManager.js'\nexport { serializeValue, deserializeValue, toKey, fromKey } from './serialization.js'\n\n// Default configuration values\nconst DEFAULT_UPDATED_AT_FIELD = `updatedAt`\nconst DEFAULT_DEBOUNCE_MS = 50\nconst DEFAULT_TAIL_OVERLAP_MS = 10000\nconst DEFAULT_RESUBSCRIBE_THRESHOLD = 10\n\n/**\n * Normalize filter configuration to an array of FilterDimension.\n * - undefined = [] (0 filters, global sync)\n * - single object = [object] (1 filter)\n * - array = array as-is (N filters)\n */\nfunction normalizeFilterConfig(filters: FilterConfig): FilterDimension[] {\n if (filters === undefined) return []\n return Array.isArray(filters) ? filters : [filters]\n}\n\n/**\n * Schema output type inference helper\n */\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Infer the item type from a Convex query's return type.\n * Expects the query to return an array of items.\n */\ntype InferQueryItemType<TQuery extends FunctionReference<'query'>> =\n FunctionReturnType<TQuery> extends Array<infer T>\n ? T extends object\n ? T\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Creates collection options for use with TanStack DB's createCollection.\n * This integrates Convex real-time subscriptions with TanStack DB collections\n * using the \"backfill + tail\" synchronization pattern.\n *\n * @example\n * ```typescript\n * import { createCollection } from '@tanstack/react-db'\n * import { convexCollectionOptions } from '@michaelstewart/convex-tanstack-db-collection'\n * import { api } from '@convex/_generated/api'\n *\n * // Single filter dimension\n * const messagesCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.messages.sync,\n * filters: { filterField: 'pageId', convexArg: 'pageIds' },\n * getKey: (msg) => msg._id,\n *\n * onInsert: async ({ transaction }) => {\n * const newMsg = transaction.mutations[0].modified\n * await convexClient.mutation(api.messages.create, newMsg)\n * },\n * })\n * )\n *\n * // Multiple filter dimensions\n * const filteredCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncFiltered,\n * filters: [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ],\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // No filters (global sync)\n * const allItemsCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncAll, // Query takes only { after }\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // In UI:\n * const { data: messages } = useLiveQuery(q =>\n * q.from({ msg: messagesCollection })\n * .where(({ msg }) => msg.pageId.eq('page-123'))\n * )\n * ```\n */\n\n// Overload for when schema is provided\nexport function convexCollectionOptions<\n TSchema extends StandardSchemaV1,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: ConvexCollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils> & {\n schema: TSchema\n }\n): CollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils>\n\n// Overload for when no schema is provided - T is inferred from query's return type\nexport function convexCollectionOptions<\n TQuery extends FunctionReference<'query'>,\n T extends InferQueryItemType<TQuery> = InferQueryItemType<TQuery>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: Omit<ConvexCollectionConfig<T, TKey, never, TUtils>, 'query'> & {\n schema?: never\n query: TQuery\n getKey: (item: T) => TKey\n }\n): CollectionConfig<T, TKey, never, TUtils>\n\n// Implementation - uses concrete types; overloads provide proper type inference\nexport function convexCollectionOptions(\n config: ConvexCollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord>\n): CollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord> {\n const {\n client,\n query,\n filters,\n updatedAtFieldName = DEFAULT_UPDATED_AT_FIELD,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n tailOverlapMs = DEFAULT_TAIL_OVERLAP_MS,\n resubscribeThreshold = DEFAULT_RESUBSCRIBE_THRESHOLD,\n getKey,\n onInsert,\n onUpdate,\n ...baseConfig\n } = config\n\n // Normalize filter configuration\n const filterDimensions = normalizeFilterConfig(filters)\n\n // Create the sync manager\n const syncManager = new ConvexSyncManager<any, any>({\n client,\n query,\n filterDimensions,\n updatedAtFieldName,\n debounceMs,\n tailOverlapMs,\n resubscribeThreshold,\n getKey: getKey as (item: any) => string | number,\n })\n\n // Create the sync configuration\n const syncConfig: SyncConfig<any, any> = {\n sync: (params) => {\n const { collection, begin, write, commit, markReady } = params\n\n // Initialize sync manager with callbacks\n syncManager.setCallbacks({\n collection: {\n get: (key) => collection.get(key),\n has: (key) => collection.has(key),\n },\n begin,\n write,\n commit,\n markReady,\n })\n\n // Return loadSubset, unloadSubset, and cleanup handlers\n return {\n loadSubset: (options: LoadSubsetOptions): Promise<void> => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n return syncManager.requestFilters({})\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(options, filterDimensions)\n\n // Sync if ANY dimension has values (any-filter matching)\n // Convex query arg validators enforce which combinations are valid\n if (Object.keys(extracted).length === 0) {\n // No filter values found - this is expected for queries that filter\n // by other fields (e.g., clientId, parentId). These queries read from\n // the already-synced collection and don't need to trigger a sync.\n return Promise.resolve()\n }\n\n return syncManager.requestFilters(extracted)\n },\n\n unloadSubset: (options: LoadSubsetOptions): void => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n syncManager.releaseFilters({})\n return\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(options, filterDimensions)\n\n if (Object.keys(extracted).length > 0) {\n syncManager.releaseFilters(extracted)\n }\n },\n\n cleanup: () => {\n syncManager.cleanup()\n },\n }\n },\n }\n\n // Return the complete collection config\n return {\n ...baseConfig,\n getKey,\n syncMode: `on-demand`, // Always on-demand since we sync based on query predicates\n sync: syncConfig,\n onInsert,\n onUpdate,\n }\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"index.cjs","sources":["../../src/serialization.ts","../../src/ConvexSyncManager.ts","../../src/expression-parser.ts","../../src/index.ts"],"sourcesContent":["/**\n * Value serialization utilities for stable hashing and round-tripping.\n *\n * Adapted from TanStack DB's query-db-collection:\n * https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/serialization.ts\n */\n\ninterface TypeMarker {\n __type: string\n value?: unknown\n sign?: number\n}\n\nfunction isTypeMarker(value: unknown): value is TypeMarker {\n return (\n typeof value === `object` &&\n value !== null &&\n `__type` in value &&\n typeof (value as TypeMarker).__type === `string`\n )\n}\n\n/**\n * Serializes a value into a JSON-safe format that preserves special JS types.\n * Handles: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function serializeValue(value: unknown): unknown {\n if (value === undefined) {\n return { __type: `undefined` }\n }\n\n if (typeof value === `number`) {\n if (Number.isNaN(value)) {\n return { __type: `nan` }\n }\n if (value === Number.POSITIVE_INFINITY) {\n return { __type: `infinity`, sign: 1 }\n }\n if (value === Number.NEGATIVE_INFINITY) {\n return { __type: `infinity`, sign: -1 }\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `date`, value: value.toJSON() }\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => serializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n serializeValue(val),\n ]),\n )\n }\n\n return value\n}\n\n/**\n * Deserializes a value back from its JSON-safe format.\n * Restores: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function deserializeValue(value: unknown): unknown {\n if (isTypeMarker(value)) {\n switch (value.__type) {\n case `undefined`:\n return undefined\n case `nan`:\n return NaN\n case `infinity`:\n return value.sign === 1\n ? Number.POSITIVE_INFINITY\n : Number.NEGATIVE_INFINITY\n case `date`:\n return new Date(value.value as string)\n default:\n return value\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => deserializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n deserializeValue(val),\n ]),\n )\n }\n\n return value\n}\n\n/**\n * Converts a value to a stable string key for use in Sets/Maps.\n */\nexport function toKey(value: unknown): string {\n return JSON.stringify(serializeValue(value))\n}\n\n/**\n * Restores a value from its string key representation.\n */\nexport function fromKey(key: string): unknown {\n return deserializeValue(JSON.parse(key))\n}\n","import { toKey, fromKey } from './serialization.js'\nimport type { FunctionReference } from 'convex/server'\nimport type { ChangeMessageOrDeleteKeyMessage } from '@tanstack/db'\nimport type {\n ConvexClientLike,\n ConvexSyncManagerOptions,\n ExtractedFilters,\n FilterDimension,\n} from './types.js'\n\nexport type { ConvexSyncManagerOptions }\n\n/**\n * Sync callbacks passed from TanStack DB's sync function\n */\nexport interface SyncCallbacks<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n collection: {\n get: (key: TKey) => T | undefined\n has: (key: TKey) => boolean\n }\n begin: () => void\n write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void\n commit: () => void\n markReady: () => void\n}\n\n/**\n * ConvexSyncManager - Manages real-time synchronization with Convex backend\n *\n * Implements the \"backfill + tail\" pattern:\n * 1. When new filters are requested, backfill with `after: 0` to get full history\n * 2. Maintain a single live subscription for all active filter values with `after: globalCursor - tailOverlapMs`\n * 3. Use LWW (Last-Write-Wins) to handle overlapping data from backfill and subscription\n *\n * Supports 0, 1, or N filter dimensions:\n * - 0 filters: Global sync with just { after }\n * - 1+ filters: Filter-based sync with values extracted from where clauses\n */\nexport class ConvexSyncManager<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n // Configuration\n private client: ConvexClientLike\n private query: FunctionReference<`query`>\n private filterDimensions: FilterDimension[]\n private updatedAtFieldName: string\n private debounceMs: number\n private tailOverlapMs: number\n private resubscribeThreshold: number\n private getKey: (item: T) => TKey\n\n // State - per-dimension tracking (keyed by convexArg)\n private activeDimensions = new Map<string, Set<string>>()\n private refCounts = new Map<string, number>() // composite key -> count\n private pendingFilters: ExtractedFilters = {}\n private globalCursor = 0\n private currentSubscription: (() => void) | null = null\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n private isProcessing = false\n private markedReady = false\n\n // For 0-filter case (global sync)\n private hasRequestedGlobal = false\n private globalRefCount = 0\n\n // Track messages received since last subscription to batch cursor updates\n private messagesSinceSubscription = 0\n\n // Sync callbacks (set when sync() is called)\n private callbacks: SyncCallbacks<T, TKey> | null = null\n\n constructor(options: ConvexSyncManagerOptions<T>) {\n this.client = options.client\n this.query = options.query\n this.filterDimensions = options.filterDimensions\n this.updatedAtFieldName = options.updatedAtFieldName\n this.debounceMs = options.debounceMs\n this.tailOverlapMs = options.tailOverlapMs\n this.resubscribeThreshold = options.resubscribeThreshold\n this.getKey = options.getKey as (item: T) => TKey\n\n // Initialize activeDimensions for each filter dimension\n for (const dim of this.filterDimensions) {\n this.activeDimensions.set(dim.convexArg, new Set())\n }\n }\n\n /**\n * Initialize the sync manager with callbacks from TanStack DB\n */\n setCallbacks(callbacks: SyncCallbacks<T, TKey>): void {\n this.callbacks = callbacks\n }\n\n /**\n * Create a composite key for ref counting multi-filter combinations.\n * Uses serialized values for deterministic keys.\n */\n private createCompositeKey(filters: ExtractedFilters): string {\n // Sort by convexArg for deterministic ordering\n const sorted = Object.keys(filters)\n .sort()\n .reduce(\n (acc, key) => {\n // Serialize values and sort for deterministic ordering\n const values = filters[key]\n if (values) {\n acc[key] = values.map((v) => toKey(v)).sort()\n }\n return acc\n },\n {} as Record<string, string[]>,\n )\n return JSON.stringify(sorted)\n }\n\n /**\n * Request filters to be synced (called by loadSubset)\n * Filters are batched via debouncing for efficiency\n */\n requestFilters(filters: ExtractedFilters): Promise<void> {\n // Handle 0-filter case (global sync)\n if (this.filterDimensions.length === 0) {\n this.globalRefCount++\n if (!this.hasRequestedGlobal) {\n this.hasRequestedGlobal = true\n return this.scheduleProcessing()\n }\n return Promise.resolve()\n }\n\n // Increment ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = this.refCounts.get(compositeKey) || 0\n this.refCounts.set(compositeKey, count + 1)\n\n // Track which values are new (not yet active)\n let hasNewValues = false\n for (const [convexArg, values] of Object.entries(filters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (!activeSet) continue\n\n // Find the dimension config for single validation\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n\n // Validate single constraint before adding values\n if (dim?.single) {\n const existingCount = activeSet.size\n const pendingCount = this.pendingFilters[convexArg]?.length ?? 0\n const newValues = values.filter((v) => {\n const serialized = toKey(v)\n const alreadyActive = activeSet.has(serialized)\n const alreadyPending = this.pendingFilters[convexArg]?.some(\n (pv) => toKey(pv) === serialized,\n )\n return !alreadyActive && !alreadyPending\n })\n\n if (existingCount + pendingCount + newValues.length > 1) {\n throw new Error(\n `Filter '${dim.filterField}' is configured as single but multiple values were requested. ` +\n `Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. ` +\n `Use single: false if you need to sync multiple values.`,\n )\n }\n }\n\n for (const value of values) {\n const serialized = toKey(value)\n if (!activeSet.has(serialized)) {\n // Add to pending filters\n if (!this.pendingFilters[convexArg]) {\n this.pendingFilters[convexArg] = []\n }\n // Check if already pending (by serialized key)\n const alreadyPending = this.pendingFilters[convexArg].some(\n (v) => toKey(v) === serialized,\n )\n if (!alreadyPending) {\n this.pendingFilters[convexArg].push(value)\n hasNewValues = true\n }\n }\n }\n }\n\n // If there are new values, schedule processing\n if (hasNewValues) {\n return this.scheduleProcessing()\n }\n\n return Promise.resolve()\n }\n\n /**\n * Release filters when no longer needed (called by unloadSubset)\n */\n releaseFilters(filters: ExtractedFilters): void {\n // Handle 0-filter case\n if (this.filterDimensions.length === 0) {\n this.globalRefCount = Math.max(0, this.globalRefCount - 1)\n if (this.globalRefCount === 0 && this.hasRequestedGlobal) {\n this.hasRequestedGlobal = false\n this.updateSubscription()\n }\n return\n }\n\n // Decrement ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = (this.refCounts.get(compositeKey) || 0) - 1\n\n if (count <= 0) {\n this.refCounts.delete(compositeKey)\n } else {\n this.refCounts.set(compositeKey, count)\n }\n\n // Check if any values are now unreferenced\n this.cleanupUnreferencedValues()\n }\n\n /**\n * Remove values from activeDimensions that are no longer referenced\n * by any composite key in refCounts\n */\n private cleanupUnreferencedValues(): void {\n // Collect all referenced serialized values per dimension\n const referencedValues = new Map<string, Set<string>>()\n for (const dim of this.filterDimensions) {\n referencedValues.set(dim.convexArg, new Set())\n }\n\n // Walk through all composite keys and collect their serialized values\n for (const compositeKey of this.refCounts.keys()) {\n try {\n // Composite keys store values as already-serialized strings\n const filters = JSON.parse(compositeKey) as Record<string, string[]>\n for (const [convexArg, serializedValues] of Object.entries(filters)) {\n const refSet = referencedValues.get(convexArg)\n if (refSet) {\n for (const serialized of serializedValues) {\n refSet.add(serialized)\n }\n }\n }\n } catch {\n // Skip invalid keys\n }\n }\n\n // Remove unreferenced values from activeDimensions\n // (activeDimensions stores serialized keys)\n let needsSubscriptionUpdate = false\n for (const [convexArg, activeSet] of this.activeDimensions) {\n const refSet = referencedValues.get(convexArg)!\n for (const serialized of activeSet) {\n if (!refSet.has(serialized)) {\n activeSet.delete(serialized)\n needsSubscriptionUpdate = true\n }\n }\n }\n\n if (needsSubscriptionUpdate) {\n this.updateSubscription()\n }\n }\n\n /**\n * Schedule debounced processing of pending filters\n */\n private scheduleProcessing(): Promise<void> {\n return new Promise((resolve, reject) => {\n // Clear existing timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n }\n\n // Schedule processing\n this.debounceTimer = setTimeout(async () => {\n try {\n await this.processFilterBatch()\n resolve()\n } catch (error) {\n reject(error)\n }\n }, this.debounceMs)\n })\n }\n\n /**\n * Process the current batch of pending filters\n */\n private async processFilterBatch(): Promise<void> {\n if (this.isProcessing) {\n return\n }\n\n // Check if there's anything to process\n const hasPendingFilters = Object.keys(this.pendingFilters).length > 0\n const needsGlobalSync =\n this.filterDimensions.length === 0 && this.hasRequestedGlobal\n\n if (!hasPendingFilters && !needsGlobalSync) {\n return\n }\n\n this.isProcessing = true\n\n try {\n if (this.filterDimensions.length === 0) {\n // 0-filter case: global sync\n await this.runGlobalBackfill()\n } else {\n // Collect new filter values that need backfill\n const newFilters = { ...this.pendingFilters }\n this.pendingFilters = {}\n\n // Add to active dimensions (store serialized keys)\n for (const [convexArg, values] of Object.entries(newFilters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (activeSet) {\n for (const value of values) {\n activeSet.add(toKey(value))\n }\n }\n }\n\n // Run backfill for new filter values (fetch full history)\n await this.runBackfill(newFilters)\n }\n\n // Update the live subscription to include all active values\n this.updateSubscription()\n\n // Mark ready after first successful sync\n if (!this.markedReady && this.callbacks) {\n this.callbacks.markReady()\n this.markedReady = true\n }\n } finally {\n this.isProcessing = false\n }\n }\n\n /**\n * Run global backfill for 0-filter case\n */\n private async runGlobalBackfill(): Promise<void> {\n try {\n const args: Record<string, unknown> = { after: 0 }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Global backfill error:', error)\n throw error\n }\n }\n\n /**\n * Run backfill query for new filter values to get their full history\n */\n private async runBackfill(newFilters: ExtractedFilters): Promise<void> {\n if (Object.keys(newFilters).length === 0) return\n\n try {\n // Query with after: 0 to get full history for new filter values\n const args: Record<string, unknown> = {\n ...newFilters,\n after: 0,\n }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Backfill error:', error)\n throw error\n }\n }\n\n /**\n * Build query args from all active dimensions\n */\n private buildQueryArgs(after: number): Record<string, unknown> {\n const args: Record<string, unknown> = { after }\n\n // Deserialize values back to original types for the Convex query\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n const values = [...serializedValues].map((s) => fromKey(s))\n\n // Check if this dimension is configured as single\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n args[convexArg] = dim?.single ? values[0] : values\n }\n\n return args\n }\n\n /**\n * Update the live subscription to cover all active filter values\n */\n private updateSubscription(): void {\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Reset message counter for new subscription\n this.messagesSinceSubscription = 0\n\n // Check if we should subscribe\n if (this.filterDimensions.length === 0) {\n // 0-filter case: subscribe if global sync is active\n if (!this.hasRequestedGlobal) {\n return\n }\n } else {\n // Check if any dimension has active values\n let hasActiveValues = false\n for (const activeSet of this.activeDimensions.values()) {\n if (activeSet.size > 0) {\n hasActiveValues = true\n break\n }\n }\n if (!hasActiveValues) {\n return\n }\n }\n\n // Calculate cursor with overlap to avoid missing updates\n const cursor = Math.max(0, this.globalCursor - this.tailOverlapMs)\n\n // Build subscription args\n const args = this.buildQueryArgs(cursor)\n\n // Runtime detection: ConvexClient has onUpdate, ConvexReactClient has watchQuery\n if ('onUpdate' in this.client) {\n // ConvexClient pattern: client.onUpdate(query, args, callback)\n const subscription = this.client.onUpdate(\n this.query,\n args as any,\n (result: unknown) => {\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n },\n (error: unknown) => {\n console.error(`[ConvexSyncManager] Subscription error:`, error)\n },\n )\n this.currentSubscription = () => subscription.unsubscribe()\n } else {\n // ConvexReactClient pattern: client.watchQuery(query, args).onUpdate(callback)\n const watch = this.client.watchQuery(this.query, args as any)\n this.currentSubscription = watch.onUpdate(() => {\n // Get current value from the watch\n const result = watch.localQueryResult()\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n })\n }\n }\n\n /**\n * Handle incoming data from backfill or subscription\n * Uses LWW (Last-Write-Wins) to resolve conflicts\n */\n private handleIncomingData(items: T[]): void {\n if (!this.callbacks || items.length === 0) return\n\n const { collection, begin, write, commit } = this.callbacks\n\n // Track if we see new items that advance the cursor\n const previousCursor = this.globalCursor\n let newItemCount = 0\n\n begin()\n\n for (const item of items) {\n const key = this.getKey(item)\n const incomingTs = (item as any)[this.updatedAtFieldName] as\n | number\n | undefined\n\n // Update global cursor to track the latest timestamp we've seen\n if (incomingTs !== undefined && incomingTs > this.globalCursor) {\n this.globalCursor = incomingTs\n }\n\n const existing = collection.get(key)\n\n if (!existing) {\n // New item - insert\n write({ type: `insert`, value: item })\n // Count as new if it's beyond the previous cursor (not from overlap)\n if (incomingTs !== undefined && incomingTs > previousCursor) {\n newItemCount++\n }\n } else {\n // Existing item - check if incoming is fresher (LWW)\n const existingTs = (existing as any)[this.updatedAtFieldName] as\n | number\n | undefined\n\n if (incomingTs !== undefined && existingTs !== undefined) {\n if (incomingTs > existingTs) {\n // Incoming is fresher - update\n write({ type: `update`, value: item })\n // Count as new if it's beyond the previous cursor\n if (incomingTs > previousCursor) {\n newItemCount++\n }\n }\n // Otherwise skip (stale data from overlap)\n } else if (incomingTs !== undefined) {\n // Existing has no timestamp, incoming does - update\n write({ type: `update`, value: item })\n }\n // If incoming has no timestamp, skip (can't determine freshness)\n }\n }\n\n commit()\n\n // Track only new messages (beyond previous cursor) for cursor advancement\n this.messagesSinceSubscription += newItemCount\n\n // Re-subscribe with advanced cursor after threshold is reached\n if (\n this.resubscribeThreshold > 0 &&\n this.messagesSinceSubscription >= this.resubscribeThreshold &&\n this.currentSubscription !== null\n ) {\n this.updateSubscription()\n }\n }\n\n /**\n * Clean up all resources\n */\n cleanup(): void {\n // Clear debounce timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Clear state\n for (const activeSet of this.activeDimensions.values()) {\n activeSet.clear()\n }\n this.refCounts.clear()\n this.pendingFilters = {}\n this.globalCursor = 0\n this.markedReady = false\n this.hasRequestedGlobal = false\n this.globalRefCount = 0\n this.messagesSinceSubscription = 0\n this.callbacks = null\n }\n\n /**\n * Get debug info about current state\n */\n getDebugInfo(): {\n activeDimensions: Record<string, unknown[]>\n globalCursor: number\n pendingFilters: ExtractedFilters\n hasSubscription: boolean\n markedReady: boolean\n hasRequestedGlobal: boolean\n messagesSinceSubscription: number\n } {\n const activeDimensions: Record<string, unknown[]> = {}\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n activeDimensions[convexArg] = [...serializedValues].map((s) => fromKey(s))\n }\n\n return {\n activeDimensions,\n globalCursor: this.globalCursor,\n pendingFilters: { ...this.pendingFilters },\n hasSubscription: this.currentSubscription !== null,\n markedReady: this.markedReady,\n hasRequestedGlobal: this.hasRequestedGlobal,\n messagesSinceSubscription: this.messagesSinceSubscription,\n }\n }\n}\n","import { toKey } from './serialization.js'\nimport type { LoadSubsetOptions } from '@tanstack/db'\nimport type { ExtractedFilters, FilterDimension } from './types.js'\n\n/**\n * TanStack DB expression types (simplified for our needs)\n * These mirror the IR types from @tanstack/db\n */\ninterface PropRef {\n type: `ref`\n path: Array<string>\n}\n\ninterface Value {\n type: `val`\n value: unknown\n}\n\ninterface Func {\n type: `func`\n name: string\n args: Array<BasicExpression>\n}\n\ntype BasicExpression = PropRef | Value | Func\n\n/**\n * Check if a value is a PropRef expression\n */\nfunction isPropRef(expr: unknown): expr is PropRef {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `ref` &&\n `path` in expr &&\n Array.isArray((expr as PropRef).path)\n )\n}\n\n/**\n * Check if a value is a Value expression\n */\nfunction isValue(expr: unknown): expr is Value {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `val` &&\n `value` in expr\n )\n}\n\n/**\n * Check if a value is a Func expression\n */\nfunction isFunc(expr: unknown): expr is Func {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `func` &&\n `name` in expr &&\n `args` in expr &&\n Array.isArray((expr as Func).args)\n )\n}\n\n/**\n * Check if a PropRef matches our target field.\n * Handles both aliased (e.g., ['msg', 'pageId']) and direct (e.g., ['pageId']) paths.\n */\nfunction propRefMatchesField(propRef: PropRef, fieldName: string): boolean {\n const { path } = propRef\n // Direct field reference: ['pageId']\n if (path.length === 1 && path[0] === fieldName) {\n return true\n }\n // Aliased field reference: ['msg', 'pageId'] or ['m', 'pageId']\n if (path.length === 2 && path[1] === fieldName) {\n return true\n }\n return false\n}\n\n/**\n * Extract filter values from an 'eq' function call.\n * Pattern: eq(ref(filterField), val(x)) or eq(val(x), ref(filterField))\n */\nfunction extractFromEq(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // eq(ref, val)\n if (\n isPropRef(left) &&\n propRefMatchesField(left, filterField) &&\n isValue(right)\n ) {\n return [right.value]\n }\n\n // eq(val, ref) - reversed order\n if (\n isValue(left) &&\n isPropRef(right) &&\n propRefMatchesField(right, filterField)\n ) {\n return [left.value]\n }\n\n return []\n}\n\n/**\n * Extract filter values from an 'in' function call.\n * Pattern: in(ref(filterField), val([a, b, c]))\n */\nfunction extractFromIn(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // in(ref, val)\n if (\n isPropRef(left) &&\n propRefMatchesField(left, filterField) &&\n isValue(right)\n ) {\n const val = right.value\n if (Array.isArray(val)) {\n return val\n }\n }\n\n return []\n}\n\n/**\n * Recursively walk an expression tree to find all filter values for the given field.\n * Handles 'eq', 'in', 'and', and 'or' expressions.\n */\nfunction walkExpression(expr: unknown, filterField: string): unknown[] {\n if (!isFunc(expr)) return []\n\n const { name, args } = expr\n const results: unknown[] = []\n\n switch (name) {\n case `eq`:\n results.push(...extractFromEq(expr, filterField))\n break\n\n case `in`:\n results.push(...extractFromIn(expr, filterField))\n break\n\n case `and`:\n case `or`:\n // Recursively process all arguments\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n\n // For other functions, recursively check their arguments\n // (in case of nested expressions)\n default:\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n }\n\n return results\n}\n\n/**\n * Extract filter values from LoadSubsetOptions.\n *\n * Parses the `where` expression to find equality (`.eq()`) and set membership (`.in()`)\n * comparisons for the specified filter field.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterField - The field name to extract values for (e.g., 'pageId')\n * @returns Array of unique filter values found in the expression\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1'))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n *\n * @example\n * // For query: where(m => inArray(m.pageId, ['page-1', 'page-2']))\n * extractFilterValues(options, 'pageId') // returns ['page-1', 'page-2']\n *\n * @example\n * // For query: where(m => and(m.pageId.eq('page-1'), m.status.eq('active')))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n */\nexport function extractFilterValues(\n options: LoadSubsetOptions,\n filterField: string,\n): unknown[] {\n const { where } = options\n\n if (!where) {\n return []\n }\n\n // Extract from the where expression\n const values = walkExpression(where, filterField)\n\n // Return unique values using toKey for stable identity comparison\n const seen = new Set<string>()\n const unique: unknown[] = []\n for (const value of values) {\n const key = toKey(value)\n if (!seen.has(key)) {\n seen.add(key)\n unique.push(value)\n }\n }\n\n return unique\n}\n\n/**\n * Check if LoadSubsetOptions contains a filter for the specified field.\n */\nexport function hasFilterField(\n options: LoadSubsetOptions,\n filterField: string,\n): boolean {\n return extractFilterValues(options, filterField).length > 0\n}\n\n/**\n * Extract filter values for multiple filter dimensions from LoadSubsetOptions.\n *\n * Parses the `where` expression to find values for each configured filter dimension.\n * Results are keyed by convexArg for direct use in Convex query args.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterDimensions - Array of filter dimensions to extract\n * @returns Object mapping convexArg -> values for dimensions with matches\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1') && m.authorId.eq('user-1'))\n * extractMultipleFilterValues(options, [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ])\n * // returns { pageIds: ['page-1'], authorIds: ['user-1'] }\n */\nexport function extractMultipleFilterValues(\n options: LoadSubsetOptions,\n filterDimensions: FilterDimension[],\n): ExtractedFilters {\n const result: ExtractedFilters = {}\n\n for (const dim of filterDimensions) {\n const values = extractFilterValues(options, dim.filterField)\n if (values.length > 0) {\n result[dim.convexArg] = values\n }\n }\n\n return result\n}\n","import { ConvexSyncManager } from './ConvexSyncManager.js'\nimport { extractMultipleFilterValues } from './expression-parser.js'\nimport type {\n CollectionConfig,\n LoadSubsetOptions,\n SyncConfig,\n UtilsRecord,\n} from '@tanstack/db'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { FunctionReference, FunctionReturnType } from 'convex/server'\nimport type {\n ConvexCollectionConfig,\n FilterConfig,\n FilterDimension,\n} from './types.js'\n\n// Re-export types\nexport type {\n ConvexCollectionConfig,\n ConvexUnsubscribe,\n ExtractedFilters,\n FilterConfig,\n FilterDimension,\n} from './types.js'\nexport {\n extractFilterValues,\n extractMultipleFilterValues,\n hasFilterField,\n} from './expression-parser.js'\nexport { ConvexSyncManager } from './ConvexSyncManager.js'\nexport {\n serializeValue,\n deserializeValue,\n toKey,\n fromKey,\n} from './serialization.js'\n\n// Default configuration values\nconst DEFAULT_UPDATED_AT_FIELD = `updatedAt`\nconst DEFAULT_DEBOUNCE_MS = 50\nconst DEFAULT_TAIL_OVERLAP_MS = 10000\nconst DEFAULT_RESUBSCRIBE_THRESHOLD = 10\n\n/**\n * Normalize filter configuration to an array of FilterDimension.\n * - undefined = [] (0 filters, global sync)\n * - single object = [object] (1 filter)\n * - array = array as-is (N filters)\n */\nfunction normalizeFilterConfig(filters: FilterConfig): FilterDimension[] {\n if (filters === undefined) return []\n return Array.isArray(filters) ? filters : [filters]\n}\n\n/**\n * Schema output type inference helper\n */\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Infer the item type from a Convex query's return type.\n * Expects the query to return an array of items.\n */\ntype InferQueryItemType<TQuery extends FunctionReference<'query'>> =\n FunctionReturnType<TQuery> extends Array<infer T>\n ? T extends object\n ? T\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Creates collection options for use with TanStack DB's createCollection.\n * This integrates Convex real-time subscriptions with TanStack DB collections\n * using the \"backfill + tail\" synchronization pattern.\n *\n * @example\n * ```typescript\n * import { createCollection } from '@tanstack/react-db'\n * import { convexCollectionOptions } from '@michaelstewart/convex-tanstack-db-collection'\n * import { api } from '@convex/_generated/api'\n *\n * // Single filter dimension\n * const messagesCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.messages.sync,\n * filters: { filterField: 'pageId', convexArg: 'pageIds' },\n * getKey: (msg) => msg._id,\n *\n * onInsert: async ({ transaction }) => {\n * const newMsg = transaction.mutations[0].modified\n * await convexClient.mutation(api.messages.create, newMsg)\n * },\n * })\n * )\n *\n * // Multiple filter dimensions\n * const filteredCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncFiltered,\n * filters: [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ],\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // No filters (global sync)\n * const allItemsCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncAll, // Query takes only { after }\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // In UI:\n * const { data: messages } = useLiveQuery(q =>\n * q.from({ msg: messagesCollection })\n * .where(({ msg }) => msg.pageId.eq('page-123'))\n * )\n * ```\n */\n\n// Overload for when schema is provided\nexport function convexCollectionOptions<\n TSchema extends StandardSchemaV1,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: ConvexCollectionConfig<\n InferSchemaOutput<TSchema>,\n TKey,\n TSchema,\n TUtils\n > & {\n schema: TSchema\n },\n): CollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils>\n\n// Overload for when no schema is provided - T is inferred from query's return type\nexport function convexCollectionOptions<\n TQuery extends FunctionReference<'query'>,\n T extends InferQueryItemType<TQuery> = InferQueryItemType<TQuery>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: Omit<ConvexCollectionConfig<T, TKey, never, TUtils>, 'query'> & {\n schema?: never\n query: TQuery\n getKey: (item: T) => TKey\n },\n): CollectionConfig<T, TKey, never, TUtils>\n\n// Implementation - uses concrete types; overloads provide proper type inference\nexport function convexCollectionOptions(\n config: ConvexCollectionConfig<\n Record<string, unknown>,\n string | number,\n never,\n UtilsRecord\n >,\n): CollectionConfig<\n Record<string, unknown>,\n string | number,\n never,\n UtilsRecord\n> {\n const {\n client,\n query,\n filters,\n updatedAtFieldName = DEFAULT_UPDATED_AT_FIELD,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n tailOverlapMs = DEFAULT_TAIL_OVERLAP_MS,\n resubscribeThreshold = DEFAULT_RESUBSCRIBE_THRESHOLD,\n getKey,\n onInsert,\n onUpdate,\n ...baseConfig\n } = config\n\n // Normalize filter configuration\n const filterDimensions = normalizeFilterConfig(filters)\n\n // Create the sync manager\n const syncManager = new ConvexSyncManager<any, any>({\n client,\n query,\n filterDimensions,\n updatedAtFieldName,\n debounceMs,\n tailOverlapMs,\n resubscribeThreshold,\n getKey: getKey as (item: any) => string | number,\n })\n\n // Create the sync configuration\n const syncConfig: SyncConfig<any, any> = {\n sync: (params) => {\n const { collection, begin, write, commit, markReady } = params\n\n // Initialize sync manager with callbacks\n syncManager.setCallbacks({\n collection: {\n get: (key) => collection.get(key),\n has: (key) => collection.has(key),\n },\n begin,\n write,\n commit,\n markReady,\n })\n\n // Return loadSubset, unloadSubset, and cleanup handlers\n return {\n loadSubset: (options: LoadSubsetOptions): Promise<void> => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n return syncManager.requestFilters({})\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(\n options,\n filterDimensions,\n )\n\n // Sync if ANY dimension has values (any-filter matching)\n // Convex query arg validators enforce which combinations are valid\n if (Object.keys(extracted).length === 0) {\n // No filter values found - this is expected for queries that filter\n // by other fields (e.g., clientId, parentId). These queries read from\n // the already-synced collection and don't need to trigger a sync.\n return Promise.resolve()\n }\n\n return syncManager.requestFilters(extracted)\n },\n\n unloadSubset: (options: LoadSubsetOptions): void => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n syncManager.releaseFilters({})\n return\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(\n options,\n filterDimensions,\n )\n\n if (Object.keys(extracted).length > 0) {\n syncManager.releaseFilters(extracted)\n }\n },\n\n cleanup: () => {\n syncManager.cleanup()\n },\n }\n },\n }\n\n // Return the complete collection config\n return {\n ...baseConfig,\n getKey,\n syncMode: `on-demand`, // Always on-demand since we sync based on query predicates\n sync: syncConfig,\n onInsert,\n onUpdate,\n }\n}\n"],"names":["_a"],"mappings":"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"}
package/dist/esm/index.js CHANGED
@@ -633,7 +633,10 @@ function convexCollectionOptions(config) {
633
633
  if (filterDimensions.length === 0) {
634
634
  return syncManager.requestFilters({});
635
635
  }
636
- const extracted = extractMultipleFilterValues(options, filterDimensions);
636
+ const extracted = extractMultipleFilterValues(
637
+ options,
638
+ filterDimensions
639
+ );
637
640
  if (Object.keys(extracted).length === 0) {
638
641
  return Promise.resolve();
639
642
  }
@@ -644,7 +647,10 @@ function convexCollectionOptions(config) {
644
647
  syncManager.releaseFilters({});
645
648
  return;
646
649
  }
647
- const extracted = extractMultipleFilterValues(options, filterDimensions);
650
+ const extracted = extractMultipleFilterValues(
651
+ options,
652
+ filterDimensions
653
+ );
648
654
  if (Object.keys(extracted).length > 0) {
649
655
  syncManager.releaseFilters(extracted);
650
656
  }
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../../src/serialization.ts","../../src/ConvexSyncManager.ts","../../src/expression-parser.ts","../../src/index.ts"],"sourcesContent":["/**\n * Value serialization utilities for stable hashing and round-tripping.\n *\n * Adapted from TanStack DB's query-db-collection:\n * https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/serialization.ts\n */\n\ninterface TypeMarker {\n __type: string\n value?: unknown\n sign?: number\n}\n\nfunction isTypeMarker(value: unknown): value is TypeMarker {\n return (\n typeof value === `object` &&\n value !== null &&\n `__type` in value &&\n typeof (value as TypeMarker).__type === `string`\n )\n}\n\n/**\n * Serializes a value into a JSON-safe format that preserves special JS types.\n * Handles: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function serializeValue(value: unknown): unknown {\n if (value === undefined) {\n return { __type: `undefined` }\n }\n\n if (typeof value === `number`) {\n if (Number.isNaN(value)) {\n return { __type: `nan` }\n }\n if (value === Number.POSITIVE_INFINITY) {\n return { __type: `infinity`, sign: 1 }\n }\n if (value === Number.NEGATIVE_INFINITY) {\n return { __type: `infinity`, sign: -1 }\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `date`, value: value.toJSON() }\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => serializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n serializeValue(val),\n ])\n )\n }\n\n return value\n}\n\n/**\n * Deserializes a value back from its JSON-safe format.\n * Restores: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function deserializeValue(value: unknown): unknown {\n if (isTypeMarker(value)) {\n switch (value.__type) {\n case `undefined`:\n return undefined\n case `nan`:\n return NaN\n case `infinity`:\n return value.sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY\n case `date`:\n return new Date(value.value as string)\n default:\n return value\n }\n }\n\n if (value === null || typeof value === `string` || typeof value === `number` || typeof value === `boolean`) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => deserializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n deserializeValue(val),\n ])\n )\n }\n\n return value\n}\n\n/**\n * Converts a value to a stable string key for use in Sets/Maps.\n */\nexport function toKey(value: unknown): string {\n return JSON.stringify(serializeValue(value))\n}\n\n/**\n * Restores a value from its string key representation.\n */\nexport function fromKey(key: string): unknown {\n return deserializeValue(JSON.parse(key))\n}\n","import type { FunctionReference } from 'convex/server'\nimport type { ChangeMessageOrDeleteKeyMessage } from '@tanstack/db'\nimport type {\n ConvexClientLike,\n ConvexSyncManagerOptions,\n ExtractedFilters,\n FilterDimension,\n} from './types.js'\nimport { toKey, fromKey } from './serialization.js'\n\nexport type { ConvexSyncManagerOptions }\n\n/**\n * Sync callbacks passed from TanStack DB's sync function\n */\nexport interface SyncCallbacks<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n collection: {\n get: (key: TKey) => T | undefined\n has: (key: TKey) => boolean\n }\n begin: () => void\n write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void\n commit: () => void\n markReady: () => void\n}\n\n/**\n * ConvexSyncManager - Manages real-time synchronization with Convex backend\n *\n * Implements the \"backfill + tail\" pattern:\n * 1. When new filters are requested, backfill with `after: 0` to get full history\n * 2. Maintain a single live subscription for all active filter values with `after: globalCursor - tailOverlapMs`\n * 3. Use LWW (Last-Write-Wins) to handle overlapping data from backfill and subscription\n *\n * Supports 0, 1, or N filter dimensions:\n * - 0 filters: Global sync with just { after }\n * - 1+ filters: Filter-based sync with values extracted from where clauses\n */\nexport class ConvexSyncManager<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n // Configuration\n private client: ConvexClientLike\n private query: FunctionReference<`query`>\n private filterDimensions: FilterDimension[]\n private updatedAtFieldName: string\n private debounceMs: number\n private tailOverlapMs: number\n private resubscribeThreshold: number\n private getKey: (item: T) => TKey\n\n // State - per-dimension tracking (keyed by convexArg)\n private activeDimensions = new Map<string, Set<string>>()\n private refCounts = new Map<string, number>() // composite key -> count\n private pendingFilters: ExtractedFilters = {}\n private globalCursor = 0\n private currentSubscription: (() => void) | null = null\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n private isProcessing = false\n private markedReady = false\n\n // For 0-filter case (global sync)\n private hasRequestedGlobal = false\n private globalRefCount = 0\n\n // Track messages received since last subscription to batch cursor updates\n private messagesSinceSubscription = 0\n\n // Sync callbacks (set when sync() is called)\n private callbacks: SyncCallbacks<T, TKey> | null = null\n\n constructor(options: ConvexSyncManagerOptions<T>) {\n this.client = options.client\n this.query = options.query\n this.filterDimensions = options.filterDimensions\n this.updatedAtFieldName = options.updatedAtFieldName\n this.debounceMs = options.debounceMs\n this.tailOverlapMs = options.tailOverlapMs\n this.resubscribeThreshold = options.resubscribeThreshold\n this.getKey = options.getKey as (item: T) => TKey\n\n // Initialize activeDimensions for each filter dimension\n for (const dim of this.filterDimensions) {\n this.activeDimensions.set(dim.convexArg, new Set())\n }\n }\n\n /**\n * Initialize the sync manager with callbacks from TanStack DB\n */\n setCallbacks(callbacks: SyncCallbacks<T, TKey>): void {\n this.callbacks = callbacks\n }\n\n /**\n * Create a composite key for ref counting multi-filter combinations.\n * Uses serialized values for deterministic keys.\n */\n private createCompositeKey(filters: ExtractedFilters): string {\n // Sort by convexArg for deterministic ordering\n const sorted = Object.keys(filters)\n .sort()\n .reduce(\n (acc, key) => {\n // Serialize values and sort for deterministic ordering\n const values = filters[key]\n if (values) {\n acc[key] = values.map((v) => toKey(v)).sort()\n }\n return acc\n },\n {} as Record<string, string[]>\n )\n return JSON.stringify(sorted)\n }\n\n /**\n * Request filters to be synced (called by loadSubset)\n * Filters are batched via debouncing for efficiency\n */\n requestFilters(filters: ExtractedFilters): Promise<void> {\n // Handle 0-filter case (global sync)\n if (this.filterDimensions.length === 0) {\n this.globalRefCount++\n if (!this.hasRequestedGlobal) {\n this.hasRequestedGlobal = true\n return this.scheduleProcessing()\n }\n return Promise.resolve()\n }\n\n // Increment ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = this.refCounts.get(compositeKey) || 0\n this.refCounts.set(compositeKey, count + 1)\n\n // Track which values are new (not yet active)\n let hasNewValues = false\n for (const [convexArg, values] of Object.entries(filters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (!activeSet) continue\n\n // Find the dimension config for single validation\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n\n // Validate single constraint before adding values\n if (dim?.single) {\n const existingCount = activeSet.size\n const pendingCount = this.pendingFilters[convexArg]?.length ?? 0\n const newValues = values.filter((v) => {\n const serialized = toKey(v)\n const alreadyActive = activeSet.has(serialized)\n const alreadyPending = this.pendingFilters[convexArg]?.some(\n (pv) => toKey(pv) === serialized\n )\n return !alreadyActive && !alreadyPending\n })\n\n if (existingCount + pendingCount + newValues.length > 1) {\n throw new Error(\n `Filter '${dim.filterField}' is configured as single but multiple values were requested. ` +\n `Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. ` +\n `Use single: false if you need to sync multiple values.`\n )\n }\n }\n\n for (const value of values) {\n const serialized = toKey(value)\n if (!activeSet.has(serialized)) {\n // Add to pending filters\n if (!this.pendingFilters[convexArg]) {\n this.pendingFilters[convexArg] = []\n }\n // Check if already pending (by serialized key)\n const alreadyPending = this.pendingFilters[convexArg].some(\n (v) => toKey(v) === serialized\n )\n if (!alreadyPending) {\n this.pendingFilters[convexArg].push(value)\n hasNewValues = true\n }\n }\n }\n }\n\n // If there are new values, schedule processing\n if (hasNewValues) {\n return this.scheduleProcessing()\n }\n\n return Promise.resolve()\n }\n\n /**\n * Release filters when no longer needed (called by unloadSubset)\n */\n releaseFilters(filters: ExtractedFilters): void {\n // Handle 0-filter case\n if (this.filterDimensions.length === 0) {\n this.globalRefCount = Math.max(0, this.globalRefCount - 1)\n if (this.globalRefCount === 0 && this.hasRequestedGlobal) {\n this.hasRequestedGlobal = false\n this.updateSubscription()\n }\n return\n }\n\n // Decrement ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = (this.refCounts.get(compositeKey) || 0) - 1\n\n if (count <= 0) {\n this.refCounts.delete(compositeKey)\n } else {\n this.refCounts.set(compositeKey, count)\n }\n\n // Check if any values are now unreferenced\n this.cleanupUnreferencedValues()\n }\n\n /**\n * Remove values from activeDimensions that are no longer referenced\n * by any composite key in refCounts\n */\n private cleanupUnreferencedValues(): void {\n // Collect all referenced serialized values per dimension\n const referencedValues = new Map<string, Set<string>>()\n for (const dim of this.filterDimensions) {\n referencedValues.set(dim.convexArg, new Set())\n }\n\n // Walk through all composite keys and collect their serialized values\n for (const compositeKey of this.refCounts.keys()) {\n try {\n // Composite keys store values as already-serialized strings\n const filters = JSON.parse(compositeKey) as Record<string, string[]>\n for (const [convexArg, serializedValues] of Object.entries(filters)) {\n const refSet = referencedValues.get(convexArg)\n if (refSet) {\n for (const serialized of serializedValues) {\n refSet.add(serialized)\n }\n }\n }\n } catch {\n // Skip invalid keys\n }\n }\n\n // Remove unreferenced values from activeDimensions\n // (activeDimensions stores serialized keys)\n let needsSubscriptionUpdate = false\n for (const [convexArg, activeSet] of this.activeDimensions) {\n const refSet = referencedValues.get(convexArg)!\n for (const serialized of activeSet) {\n if (!refSet.has(serialized)) {\n activeSet.delete(serialized)\n needsSubscriptionUpdate = true\n }\n }\n }\n\n if (needsSubscriptionUpdate) {\n this.updateSubscription()\n }\n }\n\n /**\n * Schedule debounced processing of pending filters\n */\n private scheduleProcessing(): Promise<void> {\n return new Promise((resolve, reject) => {\n // Clear existing timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n }\n\n // Schedule processing\n this.debounceTimer = setTimeout(async () => {\n try {\n await this.processFilterBatch()\n resolve()\n } catch (error) {\n reject(error)\n }\n }, this.debounceMs)\n })\n }\n\n /**\n * Process the current batch of pending filters\n */\n private async processFilterBatch(): Promise<void> {\n if (this.isProcessing) {\n return\n }\n\n // Check if there's anything to process\n const hasPendingFilters = Object.keys(this.pendingFilters).length > 0\n const needsGlobalSync = this.filterDimensions.length === 0 && this.hasRequestedGlobal\n\n if (!hasPendingFilters && !needsGlobalSync) {\n return\n }\n\n this.isProcessing = true\n\n try {\n if (this.filterDimensions.length === 0) {\n // 0-filter case: global sync\n await this.runGlobalBackfill()\n } else {\n // Collect new filter values that need backfill\n const newFilters = { ...this.pendingFilters }\n this.pendingFilters = {}\n\n // Add to active dimensions (store serialized keys)\n for (const [convexArg, values] of Object.entries(newFilters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (activeSet) {\n for (const value of values) {\n activeSet.add(toKey(value))\n }\n }\n }\n\n // Run backfill for new filter values (fetch full history)\n await this.runBackfill(newFilters)\n }\n\n // Update the live subscription to include all active values\n this.updateSubscription()\n\n // Mark ready after first successful sync\n if (!this.markedReady && this.callbacks) {\n this.callbacks.markReady()\n this.markedReady = true\n }\n } finally {\n this.isProcessing = false\n }\n }\n\n /**\n * Run global backfill for 0-filter case\n */\n private async runGlobalBackfill(): Promise<void> {\n try {\n const args: Record<string, unknown> = { after: 0 }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Global backfill error:', error)\n throw error\n }\n }\n\n /**\n * Run backfill query for new filter values to get their full history\n */\n private async runBackfill(newFilters: ExtractedFilters): Promise<void> {\n if (Object.keys(newFilters).length === 0) return\n\n try {\n // Query with after: 0 to get full history for new filter values\n const args: Record<string, unknown> = {\n ...newFilters,\n after: 0,\n }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Backfill error:', error)\n throw error\n }\n }\n\n /**\n * Build query args from all active dimensions\n */\n private buildQueryArgs(after: number): Record<string, unknown> {\n const args: Record<string, unknown> = { after }\n\n // Deserialize values back to original types for the Convex query\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n const values = [...serializedValues].map((s) => fromKey(s))\n\n // Check if this dimension is configured as single\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n args[convexArg] = dim?.single ? values[0] : values\n }\n\n return args\n }\n\n /**\n * Update the live subscription to cover all active filter values\n */\n private updateSubscription(): void {\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Reset message counter for new subscription\n this.messagesSinceSubscription = 0\n\n // Check if we should subscribe\n if (this.filterDimensions.length === 0) {\n // 0-filter case: subscribe if global sync is active\n if (!this.hasRequestedGlobal) {\n return\n }\n } else {\n // Check if any dimension has active values\n let hasActiveValues = false\n for (const activeSet of this.activeDimensions.values()) {\n if (activeSet.size > 0) {\n hasActiveValues = true\n break\n }\n }\n if (!hasActiveValues) {\n return\n }\n }\n\n // Calculate cursor with overlap to avoid missing updates\n const cursor = Math.max(0, this.globalCursor - this.tailOverlapMs)\n\n // Build subscription args\n const args = this.buildQueryArgs(cursor)\n\n // Runtime detection: ConvexClient has onUpdate, ConvexReactClient has watchQuery\n if ('onUpdate' in this.client) {\n // ConvexClient pattern: client.onUpdate(query, args, callback)\n const subscription = this.client.onUpdate(\n this.query,\n args as any,\n (result: unknown) => {\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n },\n (error: unknown) => {\n console.error(`[ConvexSyncManager] Subscription error:`, error)\n }\n )\n this.currentSubscription = () => subscription.unsubscribe()\n } else {\n // ConvexReactClient pattern: client.watchQuery(query, args).onUpdate(callback)\n const watch = this.client.watchQuery(this.query, args as any)\n this.currentSubscription = watch.onUpdate(() => {\n // Get current value from the watch\n const result = watch.localQueryResult()\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n })\n }\n }\n\n /**\n * Handle incoming data from backfill or subscription\n * Uses LWW (Last-Write-Wins) to resolve conflicts\n */\n private handleIncomingData(items: T[]): void {\n if (!this.callbacks || items.length === 0) return\n\n const { collection, begin, write, commit } = this.callbacks\n\n // Track if we see new items that advance the cursor\n const previousCursor = this.globalCursor\n let newItemCount = 0\n\n begin()\n\n for (const item of items) {\n const key = this.getKey(item)\n const incomingTs = (item as any)[this.updatedAtFieldName] as number | undefined\n\n // Update global cursor to track the latest timestamp we've seen\n if (incomingTs !== undefined && incomingTs > this.globalCursor) {\n this.globalCursor = incomingTs\n }\n\n const existing = collection.get(key)\n\n if (!existing) {\n // New item - insert\n write({ type: `insert`, value: item })\n // Count as new if it's beyond the previous cursor (not from overlap)\n if (incomingTs !== undefined && incomingTs > previousCursor) {\n newItemCount++\n }\n } else {\n // Existing item - check if incoming is fresher (LWW)\n const existingTs = (existing as any)[this.updatedAtFieldName] as number | undefined\n\n if (incomingTs !== undefined && existingTs !== undefined) {\n if (incomingTs > existingTs) {\n // Incoming is fresher - update\n write({ type: `update`, value: item })\n // Count as new if it's beyond the previous cursor\n if (incomingTs > previousCursor) {\n newItemCount++\n }\n }\n // Otherwise skip (stale data from overlap)\n } else if (incomingTs !== undefined) {\n // Existing has no timestamp, incoming does - update\n write({ type: `update`, value: item })\n }\n // If incoming has no timestamp, skip (can't determine freshness)\n }\n }\n\n commit()\n\n // Track only new messages (beyond previous cursor) for cursor advancement\n this.messagesSinceSubscription += newItemCount\n\n // Re-subscribe with advanced cursor after threshold is reached\n if (\n this.resubscribeThreshold > 0 &&\n this.messagesSinceSubscription >= this.resubscribeThreshold &&\n this.currentSubscription !== null\n ) {\n this.updateSubscription()\n }\n }\n\n /**\n * Clean up all resources\n */\n cleanup(): void {\n // Clear debounce timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Clear state\n for (const activeSet of this.activeDimensions.values()) {\n activeSet.clear()\n }\n this.refCounts.clear()\n this.pendingFilters = {}\n this.globalCursor = 0\n this.markedReady = false\n this.hasRequestedGlobal = false\n this.globalRefCount = 0\n this.messagesSinceSubscription = 0\n this.callbacks = null\n }\n\n /**\n * Get debug info about current state\n */\n getDebugInfo(): {\n activeDimensions: Record<string, unknown[]>\n globalCursor: number\n pendingFilters: ExtractedFilters\n hasSubscription: boolean\n markedReady: boolean\n hasRequestedGlobal: boolean\n messagesSinceSubscription: number\n } {\n const activeDimensions: Record<string, unknown[]> = {}\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n activeDimensions[convexArg] = [...serializedValues].map((s) => fromKey(s))\n }\n\n return {\n activeDimensions,\n globalCursor: this.globalCursor,\n pendingFilters: { ...this.pendingFilters },\n hasSubscription: this.currentSubscription !== null,\n markedReady: this.markedReady,\n hasRequestedGlobal: this.hasRequestedGlobal,\n messagesSinceSubscription: this.messagesSinceSubscription,\n }\n }\n}\n","import type { LoadSubsetOptions } from '@tanstack/db'\nimport type { ExtractedFilters, FilterDimension } from './types.js'\nimport { toKey } from './serialization.js'\n\n/**\n * TanStack DB expression types (simplified for our needs)\n * These mirror the IR types from @tanstack/db\n */\ninterface PropRef {\n type: `ref`\n path: Array<string>\n}\n\ninterface Value {\n type: `val`\n value: unknown\n}\n\ninterface Func {\n type: `func`\n name: string\n args: Array<BasicExpression>\n}\n\ntype BasicExpression = PropRef | Value | Func\n\n/**\n * Check if a value is a PropRef expression\n */\nfunction isPropRef(expr: unknown): expr is PropRef {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `ref` &&\n `path` in expr &&\n Array.isArray((expr as PropRef).path)\n )\n}\n\n/**\n * Check if a value is a Value expression\n */\nfunction isValue(expr: unknown): expr is Value {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `val` &&\n `value` in expr\n )\n}\n\n/**\n * Check if a value is a Func expression\n */\nfunction isFunc(expr: unknown): expr is Func {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `func` &&\n `name` in expr &&\n `args` in expr &&\n Array.isArray((expr as Func).args)\n )\n}\n\n/**\n * Check if a PropRef matches our target field.\n * Handles both aliased (e.g., ['msg', 'pageId']) and direct (e.g., ['pageId']) paths.\n */\nfunction propRefMatchesField(propRef: PropRef, fieldName: string): boolean {\n const { path } = propRef\n // Direct field reference: ['pageId']\n if (path.length === 1 && path[0] === fieldName) {\n return true\n }\n // Aliased field reference: ['msg', 'pageId'] or ['m', 'pageId']\n if (path.length === 2 && path[1] === fieldName) {\n return true\n }\n return false\n}\n\n/**\n * Extract filter values from an 'eq' function call.\n * Pattern: eq(ref(filterField), val(x)) or eq(val(x), ref(filterField))\n */\nfunction extractFromEq(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // eq(ref, val)\n if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {\n return [right.value]\n }\n\n // eq(val, ref) - reversed order\n if (isValue(left) && isPropRef(right) && propRefMatchesField(right, filterField)) {\n return [left.value]\n }\n\n return []\n}\n\n/**\n * Extract filter values from an 'in' function call.\n * Pattern: in(ref(filterField), val([a, b, c]))\n */\nfunction extractFromIn(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // in(ref, val)\n if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {\n const val = right.value\n if (Array.isArray(val)) {\n return val\n }\n }\n\n return []\n}\n\n/**\n * Recursively walk an expression tree to find all filter values for the given field.\n * Handles 'eq', 'in', 'and', and 'or' expressions.\n */\nfunction walkExpression(expr: unknown, filterField: string): unknown[] {\n if (!isFunc(expr)) return []\n\n const { name, args } = expr\n const results: unknown[] = []\n\n switch (name) {\n case `eq`:\n results.push(...extractFromEq(expr, filterField))\n break\n\n case `in`:\n results.push(...extractFromIn(expr, filterField))\n break\n\n case `and`:\n case `or`:\n // Recursively process all arguments\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n\n // For other functions, recursively check their arguments\n // (in case of nested expressions)\n default:\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n }\n\n return results\n}\n\n/**\n * Extract filter values from LoadSubsetOptions.\n *\n * Parses the `where` expression to find equality (`.eq()`) and set membership (`.in()`)\n * comparisons for the specified filter field.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterField - The field name to extract values for (e.g., 'pageId')\n * @returns Array of unique filter values found in the expression\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1'))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n *\n * @example\n * // For query: where(m => inArray(m.pageId, ['page-1', 'page-2']))\n * extractFilterValues(options, 'pageId') // returns ['page-1', 'page-2']\n *\n * @example\n * // For query: where(m => and(m.pageId.eq('page-1'), m.status.eq('active')))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n */\nexport function extractFilterValues(\n options: LoadSubsetOptions,\n filterField: string\n): unknown[] {\n const { where } = options\n\n if (!where) {\n return []\n }\n\n // Extract from the where expression\n const values = walkExpression(where, filterField)\n\n // Return unique values using toKey for stable identity comparison\n const seen = new Set<string>()\n const unique: unknown[] = []\n for (const value of values) {\n const key = toKey(value)\n if (!seen.has(key)) {\n seen.add(key)\n unique.push(value)\n }\n }\n\n return unique\n}\n\n/**\n * Check if LoadSubsetOptions contains a filter for the specified field.\n */\nexport function hasFilterField(options: LoadSubsetOptions, filterField: string): boolean {\n return extractFilterValues(options, filterField).length > 0\n}\n\n/**\n * Extract filter values for multiple filter dimensions from LoadSubsetOptions.\n *\n * Parses the `where` expression to find values for each configured filter dimension.\n * Results are keyed by convexArg for direct use in Convex query args.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterDimensions - Array of filter dimensions to extract\n * @returns Object mapping convexArg -> values for dimensions with matches\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1') && m.authorId.eq('user-1'))\n * extractMultipleFilterValues(options, [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ])\n * // returns { pageIds: ['page-1'], authorIds: ['user-1'] }\n */\nexport function extractMultipleFilterValues(\n options: LoadSubsetOptions,\n filterDimensions: FilterDimension[]\n): ExtractedFilters {\n const result: ExtractedFilters = {}\n\n for (const dim of filterDimensions) {\n const values = extractFilterValues(options, dim.filterField)\n if (values.length > 0) {\n result[dim.convexArg] = values\n }\n }\n\n return result\n}\n","import type {\n CollectionConfig,\n LoadSubsetOptions,\n SyncConfig,\n UtilsRecord,\n} from '@tanstack/db'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { FunctionReference, FunctionReturnType } from 'convex/server'\nimport { ConvexSyncManager } from './ConvexSyncManager.js'\nimport { extractMultipleFilterValues } from './expression-parser.js'\nimport type { ConvexCollectionConfig, FilterConfig, FilterDimension } from './types.js'\n\n// Re-export types\nexport type {\n ConvexCollectionConfig,\n ConvexUnsubscribe,\n ExtractedFilters,\n FilterConfig,\n FilterDimension,\n} from './types.js'\nexport { extractFilterValues, extractMultipleFilterValues, hasFilterField } from './expression-parser.js'\nexport { ConvexSyncManager } from './ConvexSyncManager.js'\nexport { serializeValue, deserializeValue, toKey, fromKey } from './serialization.js'\n\n// Default configuration values\nconst DEFAULT_UPDATED_AT_FIELD = `updatedAt`\nconst DEFAULT_DEBOUNCE_MS = 50\nconst DEFAULT_TAIL_OVERLAP_MS = 10000\nconst DEFAULT_RESUBSCRIBE_THRESHOLD = 10\n\n/**\n * Normalize filter configuration to an array of FilterDimension.\n * - undefined = [] (0 filters, global sync)\n * - single object = [object] (1 filter)\n * - array = array as-is (N filters)\n */\nfunction normalizeFilterConfig(filters: FilterConfig): FilterDimension[] {\n if (filters === undefined) return []\n return Array.isArray(filters) ? filters : [filters]\n}\n\n/**\n * Schema output type inference helper\n */\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Infer the item type from a Convex query's return type.\n * Expects the query to return an array of items.\n */\ntype InferQueryItemType<TQuery extends FunctionReference<'query'>> =\n FunctionReturnType<TQuery> extends Array<infer T>\n ? T extends object\n ? T\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Creates collection options for use with TanStack DB's createCollection.\n * This integrates Convex real-time subscriptions with TanStack DB collections\n * using the \"backfill + tail\" synchronization pattern.\n *\n * @example\n * ```typescript\n * import { createCollection } from '@tanstack/react-db'\n * import { convexCollectionOptions } from '@michaelstewart/convex-tanstack-db-collection'\n * import { api } from '@convex/_generated/api'\n *\n * // Single filter dimension\n * const messagesCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.messages.sync,\n * filters: { filterField: 'pageId', convexArg: 'pageIds' },\n * getKey: (msg) => msg._id,\n *\n * onInsert: async ({ transaction }) => {\n * const newMsg = transaction.mutations[0].modified\n * await convexClient.mutation(api.messages.create, newMsg)\n * },\n * })\n * )\n *\n * // Multiple filter dimensions\n * const filteredCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncFiltered,\n * filters: [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ],\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // No filters (global sync)\n * const allItemsCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncAll, // Query takes only { after }\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // In UI:\n * const { data: messages } = useLiveQuery(q =>\n * q.from({ msg: messagesCollection })\n * .where(({ msg }) => msg.pageId.eq('page-123'))\n * )\n * ```\n */\n\n// Overload for when schema is provided\nexport function convexCollectionOptions<\n TSchema extends StandardSchemaV1,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: ConvexCollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils> & {\n schema: TSchema\n }\n): CollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils>\n\n// Overload for when no schema is provided - T is inferred from query's return type\nexport function convexCollectionOptions<\n TQuery extends FunctionReference<'query'>,\n T extends InferQueryItemType<TQuery> = InferQueryItemType<TQuery>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: Omit<ConvexCollectionConfig<T, TKey, never, TUtils>, 'query'> & {\n schema?: never\n query: TQuery\n getKey: (item: T) => TKey\n }\n): CollectionConfig<T, TKey, never, TUtils>\n\n// Implementation - uses concrete types; overloads provide proper type inference\nexport function convexCollectionOptions(\n config: ConvexCollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord>\n): CollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord> {\n const {\n client,\n query,\n filters,\n updatedAtFieldName = DEFAULT_UPDATED_AT_FIELD,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n tailOverlapMs = DEFAULT_TAIL_OVERLAP_MS,\n resubscribeThreshold = DEFAULT_RESUBSCRIBE_THRESHOLD,\n getKey,\n onInsert,\n onUpdate,\n ...baseConfig\n } = config\n\n // Normalize filter configuration\n const filterDimensions = normalizeFilterConfig(filters)\n\n // Create the sync manager\n const syncManager = new ConvexSyncManager<any, any>({\n client,\n query,\n filterDimensions,\n updatedAtFieldName,\n debounceMs,\n tailOverlapMs,\n resubscribeThreshold,\n getKey: getKey as (item: any) => string | number,\n })\n\n // Create the sync configuration\n const syncConfig: SyncConfig<any, any> = {\n sync: (params) => {\n const { collection, begin, write, commit, markReady } = params\n\n // Initialize sync manager with callbacks\n syncManager.setCallbacks({\n collection: {\n get: (key) => collection.get(key),\n has: (key) => collection.has(key),\n },\n begin,\n write,\n commit,\n markReady,\n })\n\n // Return loadSubset, unloadSubset, and cleanup handlers\n return {\n loadSubset: (options: LoadSubsetOptions): Promise<void> => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n return syncManager.requestFilters({})\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(options, filterDimensions)\n\n // Sync if ANY dimension has values (any-filter matching)\n // Convex query arg validators enforce which combinations are valid\n if (Object.keys(extracted).length === 0) {\n // No filter values found - this is expected for queries that filter\n // by other fields (e.g., clientId, parentId). These queries read from\n // the already-synced collection and don't need to trigger a sync.\n return Promise.resolve()\n }\n\n return syncManager.requestFilters(extracted)\n },\n\n unloadSubset: (options: LoadSubsetOptions): void => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n syncManager.releaseFilters({})\n return\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(options, filterDimensions)\n\n if (Object.keys(extracted).length > 0) {\n syncManager.releaseFilters(extracted)\n }\n },\n\n cleanup: () => {\n syncManager.cleanup()\n },\n }\n },\n }\n\n // Return the complete collection config\n return {\n ...baseConfig,\n getKey,\n syncMode: `on-demand`, // Always on-demand since we sync based on query predicates\n sync: syncConfig,\n onInsert,\n onUpdate,\n }\n}\n"],"names":["_a"],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../../src/serialization.ts","../../src/ConvexSyncManager.ts","../../src/expression-parser.ts","../../src/index.ts"],"sourcesContent":["/**\n * Value serialization utilities for stable hashing and round-tripping.\n *\n * Adapted from TanStack DB's query-db-collection:\n * https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/serialization.ts\n */\n\ninterface TypeMarker {\n __type: string\n value?: unknown\n sign?: number\n}\n\nfunction isTypeMarker(value: unknown): value is TypeMarker {\n return (\n typeof value === `object` &&\n value !== null &&\n `__type` in value &&\n typeof (value as TypeMarker).__type === `string`\n )\n}\n\n/**\n * Serializes a value into a JSON-safe format that preserves special JS types.\n * Handles: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function serializeValue(value: unknown): unknown {\n if (value === undefined) {\n return { __type: `undefined` }\n }\n\n if (typeof value === `number`) {\n if (Number.isNaN(value)) {\n return { __type: `nan` }\n }\n if (value === Number.POSITIVE_INFINITY) {\n return { __type: `infinity`, sign: 1 }\n }\n if (value === Number.NEGATIVE_INFINITY) {\n return { __type: `infinity`, sign: -1 }\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `date`, value: value.toJSON() }\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => serializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n serializeValue(val),\n ]),\n )\n }\n\n return value\n}\n\n/**\n * Deserializes a value back from its JSON-safe format.\n * Restores: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function deserializeValue(value: unknown): unknown {\n if (isTypeMarker(value)) {\n switch (value.__type) {\n case `undefined`:\n return undefined\n case `nan`:\n return NaN\n case `infinity`:\n return value.sign === 1\n ? Number.POSITIVE_INFINITY\n : Number.NEGATIVE_INFINITY\n case `date`:\n return new Date(value.value as string)\n default:\n return value\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => deserializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n deserializeValue(val),\n ]),\n )\n }\n\n return value\n}\n\n/**\n * Converts a value to a stable string key for use in Sets/Maps.\n */\nexport function toKey(value: unknown): string {\n return JSON.stringify(serializeValue(value))\n}\n\n/**\n * Restores a value from its string key representation.\n */\nexport function fromKey(key: string): unknown {\n return deserializeValue(JSON.parse(key))\n}\n","import { toKey, fromKey } from './serialization.js'\nimport type { FunctionReference } from 'convex/server'\nimport type { ChangeMessageOrDeleteKeyMessage } from '@tanstack/db'\nimport type {\n ConvexClientLike,\n ConvexSyncManagerOptions,\n ExtractedFilters,\n FilterDimension,\n} from './types.js'\n\nexport type { ConvexSyncManagerOptions }\n\n/**\n * Sync callbacks passed from TanStack DB's sync function\n */\nexport interface SyncCallbacks<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n collection: {\n get: (key: TKey) => T | undefined\n has: (key: TKey) => boolean\n }\n begin: () => void\n write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void\n commit: () => void\n markReady: () => void\n}\n\n/**\n * ConvexSyncManager - Manages real-time synchronization with Convex backend\n *\n * Implements the \"backfill + tail\" pattern:\n * 1. When new filters are requested, backfill with `after: 0` to get full history\n * 2. Maintain a single live subscription for all active filter values with `after: globalCursor - tailOverlapMs`\n * 3. Use LWW (Last-Write-Wins) to handle overlapping data from backfill and subscription\n *\n * Supports 0, 1, or N filter dimensions:\n * - 0 filters: Global sync with just { after }\n * - 1+ filters: Filter-based sync with values extracted from where clauses\n */\nexport class ConvexSyncManager<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n // Configuration\n private client: ConvexClientLike\n private query: FunctionReference<`query`>\n private filterDimensions: FilterDimension[]\n private updatedAtFieldName: string\n private debounceMs: number\n private tailOverlapMs: number\n private resubscribeThreshold: number\n private getKey: (item: T) => TKey\n\n // State - per-dimension tracking (keyed by convexArg)\n private activeDimensions = new Map<string, Set<string>>()\n private refCounts = new Map<string, number>() // composite key -> count\n private pendingFilters: ExtractedFilters = {}\n private globalCursor = 0\n private currentSubscription: (() => void) | null = null\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n private isProcessing = false\n private markedReady = false\n\n // For 0-filter case (global sync)\n private hasRequestedGlobal = false\n private globalRefCount = 0\n\n // Track messages received since last subscription to batch cursor updates\n private messagesSinceSubscription = 0\n\n // Sync callbacks (set when sync() is called)\n private callbacks: SyncCallbacks<T, TKey> | null = null\n\n constructor(options: ConvexSyncManagerOptions<T>) {\n this.client = options.client\n this.query = options.query\n this.filterDimensions = options.filterDimensions\n this.updatedAtFieldName = options.updatedAtFieldName\n this.debounceMs = options.debounceMs\n this.tailOverlapMs = options.tailOverlapMs\n this.resubscribeThreshold = options.resubscribeThreshold\n this.getKey = options.getKey as (item: T) => TKey\n\n // Initialize activeDimensions for each filter dimension\n for (const dim of this.filterDimensions) {\n this.activeDimensions.set(dim.convexArg, new Set())\n }\n }\n\n /**\n * Initialize the sync manager with callbacks from TanStack DB\n */\n setCallbacks(callbacks: SyncCallbacks<T, TKey>): void {\n this.callbacks = callbacks\n }\n\n /**\n * Create a composite key for ref counting multi-filter combinations.\n * Uses serialized values for deterministic keys.\n */\n private createCompositeKey(filters: ExtractedFilters): string {\n // Sort by convexArg for deterministic ordering\n const sorted = Object.keys(filters)\n .sort()\n .reduce(\n (acc, key) => {\n // Serialize values and sort for deterministic ordering\n const values = filters[key]\n if (values) {\n acc[key] = values.map((v) => toKey(v)).sort()\n }\n return acc\n },\n {} as Record<string, string[]>,\n )\n return JSON.stringify(sorted)\n }\n\n /**\n * Request filters to be synced (called by loadSubset)\n * Filters are batched via debouncing for efficiency\n */\n requestFilters(filters: ExtractedFilters): Promise<void> {\n // Handle 0-filter case (global sync)\n if (this.filterDimensions.length === 0) {\n this.globalRefCount++\n if (!this.hasRequestedGlobal) {\n this.hasRequestedGlobal = true\n return this.scheduleProcessing()\n }\n return Promise.resolve()\n }\n\n // Increment ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = this.refCounts.get(compositeKey) || 0\n this.refCounts.set(compositeKey, count + 1)\n\n // Track which values are new (not yet active)\n let hasNewValues = false\n for (const [convexArg, values] of Object.entries(filters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (!activeSet) continue\n\n // Find the dimension config for single validation\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n\n // Validate single constraint before adding values\n if (dim?.single) {\n const existingCount = activeSet.size\n const pendingCount = this.pendingFilters[convexArg]?.length ?? 0\n const newValues = values.filter((v) => {\n const serialized = toKey(v)\n const alreadyActive = activeSet.has(serialized)\n const alreadyPending = this.pendingFilters[convexArg]?.some(\n (pv) => toKey(pv) === serialized,\n )\n return !alreadyActive && !alreadyPending\n })\n\n if (existingCount + pendingCount + newValues.length > 1) {\n throw new Error(\n `Filter '${dim.filterField}' is configured as single but multiple values were requested. ` +\n `Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. ` +\n `Use single: false if you need to sync multiple values.`,\n )\n }\n }\n\n for (const value of values) {\n const serialized = toKey(value)\n if (!activeSet.has(serialized)) {\n // Add to pending filters\n if (!this.pendingFilters[convexArg]) {\n this.pendingFilters[convexArg] = []\n }\n // Check if already pending (by serialized key)\n const alreadyPending = this.pendingFilters[convexArg].some(\n (v) => toKey(v) === serialized,\n )\n if (!alreadyPending) {\n this.pendingFilters[convexArg].push(value)\n hasNewValues = true\n }\n }\n }\n }\n\n // If there are new values, schedule processing\n if (hasNewValues) {\n return this.scheduleProcessing()\n }\n\n return Promise.resolve()\n }\n\n /**\n * Release filters when no longer needed (called by unloadSubset)\n */\n releaseFilters(filters: ExtractedFilters): void {\n // Handle 0-filter case\n if (this.filterDimensions.length === 0) {\n this.globalRefCount = Math.max(0, this.globalRefCount - 1)\n if (this.globalRefCount === 0 && this.hasRequestedGlobal) {\n this.hasRequestedGlobal = false\n this.updateSubscription()\n }\n return\n }\n\n // Decrement ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = (this.refCounts.get(compositeKey) || 0) - 1\n\n if (count <= 0) {\n this.refCounts.delete(compositeKey)\n } else {\n this.refCounts.set(compositeKey, count)\n }\n\n // Check if any values are now unreferenced\n this.cleanupUnreferencedValues()\n }\n\n /**\n * Remove values from activeDimensions that are no longer referenced\n * by any composite key in refCounts\n */\n private cleanupUnreferencedValues(): void {\n // Collect all referenced serialized values per dimension\n const referencedValues = new Map<string, Set<string>>()\n for (const dim of this.filterDimensions) {\n referencedValues.set(dim.convexArg, new Set())\n }\n\n // Walk through all composite keys and collect their serialized values\n for (const compositeKey of this.refCounts.keys()) {\n try {\n // Composite keys store values as already-serialized strings\n const filters = JSON.parse(compositeKey) as Record<string, string[]>\n for (const [convexArg, serializedValues] of Object.entries(filters)) {\n const refSet = referencedValues.get(convexArg)\n if (refSet) {\n for (const serialized of serializedValues) {\n refSet.add(serialized)\n }\n }\n }\n } catch {\n // Skip invalid keys\n }\n }\n\n // Remove unreferenced values from activeDimensions\n // (activeDimensions stores serialized keys)\n let needsSubscriptionUpdate = false\n for (const [convexArg, activeSet] of this.activeDimensions) {\n const refSet = referencedValues.get(convexArg)!\n for (const serialized of activeSet) {\n if (!refSet.has(serialized)) {\n activeSet.delete(serialized)\n needsSubscriptionUpdate = true\n }\n }\n }\n\n if (needsSubscriptionUpdate) {\n this.updateSubscription()\n }\n }\n\n /**\n * Schedule debounced processing of pending filters\n */\n private scheduleProcessing(): Promise<void> {\n return new Promise((resolve, reject) => {\n // Clear existing timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n }\n\n // Schedule processing\n this.debounceTimer = setTimeout(async () => {\n try {\n await this.processFilterBatch()\n resolve()\n } catch (error) {\n reject(error)\n }\n }, this.debounceMs)\n })\n }\n\n /**\n * Process the current batch of pending filters\n */\n private async processFilterBatch(): Promise<void> {\n if (this.isProcessing) {\n return\n }\n\n // Check if there's anything to process\n const hasPendingFilters = Object.keys(this.pendingFilters).length > 0\n const needsGlobalSync =\n this.filterDimensions.length === 0 && this.hasRequestedGlobal\n\n if (!hasPendingFilters && !needsGlobalSync) {\n return\n }\n\n this.isProcessing = true\n\n try {\n if (this.filterDimensions.length === 0) {\n // 0-filter case: global sync\n await this.runGlobalBackfill()\n } else {\n // Collect new filter values that need backfill\n const newFilters = { ...this.pendingFilters }\n this.pendingFilters = {}\n\n // Add to active dimensions (store serialized keys)\n for (const [convexArg, values] of Object.entries(newFilters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (activeSet) {\n for (const value of values) {\n activeSet.add(toKey(value))\n }\n }\n }\n\n // Run backfill for new filter values (fetch full history)\n await this.runBackfill(newFilters)\n }\n\n // Update the live subscription to include all active values\n this.updateSubscription()\n\n // Mark ready after first successful sync\n if (!this.markedReady && this.callbacks) {\n this.callbacks.markReady()\n this.markedReady = true\n }\n } finally {\n this.isProcessing = false\n }\n }\n\n /**\n * Run global backfill for 0-filter case\n */\n private async runGlobalBackfill(): Promise<void> {\n try {\n const args: Record<string, unknown> = { after: 0 }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Global backfill error:', error)\n throw error\n }\n }\n\n /**\n * Run backfill query for new filter values to get their full history\n */\n private async runBackfill(newFilters: ExtractedFilters): Promise<void> {\n if (Object.keys(newFilters).length === 0) return\n\n try {\n // Query with after: 0 to get full history for new filter values\n const args: Record<string, unknown> = {\n ...newFilters,\n after: 0,\n }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Backfill error:', error)\n throw error\n }\n }\n\n /**\n * Build query args from all active dimensions\n */\n private buildQueryArgs(after: number): Record<string, unknown> {\n const args: Record<string, unknown> = { after }\n\n // Deserialize values back to original types for the Convex query\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n const values = [...serializedValues].map((s) => fromKey(s))\n\n // Check if this dimension is configured as single\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n args[convexArg] = dim?.single ? values[0] : values\n }\n\n return args\n }\n\n /**\n * Update the live subscription to cover all active filter values\n */\n private updateSubscription(): void {\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Reset message counter for new subscription\n this.messagesSinceSubscription = 0\n\n // Check if we should subscribe\n if (this.filterDimensions.length === 0) {\n // 0-filter case: subscribe if global sync is active\n if (!this.hasRequestedGlobal) {\n return\n }\n } else {\n // Check if any dimension has active values\n let hasActiveValues = false\n for (const activeSet of this.activeDimensions.values()) {\n if (activeSet.size > 0) {\n hasActiveValues = true\n break\n }\n }\n if (!hasActiveValues) {\n return\n }\n }\n\n // Calculate cursor with overlap to avoid missing updates\n const cursor = Math.max(0, this.globalCursor - this.tailOverlapMs)\n\n // Build subscription args\n const args = this.buildQueryArgs(cursor)\n\n // Runtime detection: ConvexClient has onUpdate, ConvexReactClient has watchQuery\n if ('onUpdate' in this.client) {\n // ConvexClient pattern: client.onUpdate(query, args, callback)\n const subscription = this.client.onUpdate(\n this.query,\n args as any,\n (result: unknown) => {\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n },\n (error: unknown) => {\n console.error(`[ConvexSyncManager] Subscription error:`, error)\n },\n )\n this.currentSubscription = () => subscription.unsubscribe()\n } else {\n // ConvexReactClient pattern: client.watchQuery(query, args).onUpdate(callback)\n const watch = this.client.watchQuery(this.query, args as any)\n this.currentSubscription = watch.onUpdate(() => {\n // Get current value from the watch\n const result = watch.localQueryResult()\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n })\n }\n }\n\n /**\n * Handle incoming data from backfill or subscription\n * Uses LWW (Last-Write-Wins) to resolve conflicts\n */\n private handleIncomingData(items: T[]): void {\n if (!this.callbacks || items.length === 0) return\n\n const { collection, begin, write, commit } = this.callbacks\n\n // Track if we see new items that advance the cursor\n const previousCursor = this.globalCursor\n let newItemCount = 0\n\n begin()\n\n for (const item of items) {\n const key = this.getKey(item)\n const incomingTs = (item as any)[this.updatedAtFieldName] as\n | number\n | undefined\n\n // Update global cursor to track the latest timestamp we've seen\n if (incomingTs !== undefined && incomingTs > this.globalCursor) {\n this.globalCursor = incomingTs\n }\n\n const existing = collection.get(key)\n\n if (!existing) {\n // New item - insert\n write({ type: `insert`, value: item })\n // Count as new if it's beyond the previous cursor (not from overlap)\n if (incomingTs !== undefined && incomingTs > previousCursor) {\n newItemCount++\n }\n } else {\n // Existing item - check if incoming is fresher (LWW)\n const existingTs = (existing as any)[this.updatedAtFieldName] as\n | number\n | undefined\n\n if (incomingTs !== undefined && existingTs !== undefined) {\n if (incomingTs > existingTs) {\n // Incoming is fresher - update\n write({ type: `update`, value: item })\n // Count as new if it's beyond the previous cursor\n if (incomingTs > previousCursor) {\n newItemCount++\n }\n }\n // Otherwise skip (stale data from overlap)\n } else if (incomingTs !== undefined) {\n // Existing has no timestamp, incoming does - update\n write({ type: `update`, value: item })\n }\n // If incoming has no timestamp, skip (can't determine freshness)\n }\n }\n\n commit()\n\n // Track only new messages (beyond previous cursor) for cursor advancement\n this.messagesSinceSubscription += newItemCount\n\n // Re-subscribe with advanced cursor after threshold is reached\n if (\n this.resubscribeThreshold > 0 &&\n this.messagesSinceSubscription >= this.resubscribeThreshold &&\n this.currentSubscription !== null\n ) {\n this.updateSubscription()\n }\n }\n\n /**\n * Clean up all resources\n */\n cleanup(): void {\n // Clear debounce timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Clear state\n for (const activeSet of this.activeDimensions.values()) {\n activeSet.clear()\n }\n this.refCounts.clear()\n this.pendingFilters = {}\n this.globalCursor = 0\n this.markedReady = false\n this.hasRequestedGlobal = false\n this.globalRefCount = 0\n this.messagesSinceSubscription = 0\n this.callbacks = null\n }\n\n /**\n * Get debug info about current state\n */\n getDebugInfo(): {\n activeDimensions: Record<string, unknown[]>\n globalCursor: number\n pendingFilters: ExtractedFilters\n hasSubscription: boolean\n markedReady: boolean\n hasRequestedGlobal: boolean\n messagesSinceSubscription: number\n } {\n const activeDimensions: Record<string, unknown[]> = {}\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n activeDimensions[convexArg] = [...serializedValues].map((s) => fromKey(s))\n }\n\n return {\n activeDimensions,\n globalCursor: this.globalCursor,\n pendingFilters: { ...this.pendingFilters },\n hasSubscription: this.currentSubscription !== null,\n markedReady: this.markedReady,\n hasRequestedGlobal: this.hasRequestedGlobal,\n messagesSinceSubscription: this.messagesSinceSubscription,\n }\n }\n}\n","import { toKey } from './serialization.js'\nimport type { LoadSubsetOptions } from '@tanstack/db'\nimport type { ExtractedFilters, FilterDimension } from './types.js'\n\n/**\n * TanStack DB expression types (simplified for our needs)\n * These mirror the IR types from @tanstack/db\n */\ninterface PropRef {\n type: `ref`\n path: Array<string>\n}\n\ninterface Value {\n type: `val`\n value: unknown\n}\n\ninterface Func {\n type: `func`\n name: string\n args: Array<BasicExpression>\n}\n\ntype BasicExpression = PropRef | Value | Func\n\n/**\n * Check if a value is a PropRef expression\n */\nfunction isPropRef(expr: unknown): expr is PropRef {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `ref` &&\n `path` in expr &&\n Array.isArray((expr as PropRef).path)\n )\n}\n\n/**\n * Check if a value is a Value expression\n */\nfunction isValue(expr: unknown): expr is Value {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `val` &&\n `value` in expr\n )\n}\n\n/**\n * Check if a value is a Func expression\n */\nfunction isFunc(expr: unknown): expr is Func {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `func` &&\n `name` in expr &&\n `args` in expr &&\n Array.isArray((expr as Func).args)\n )\n}\n\n/**\n * Check if a PropRef matches our target field.\n * Handles both aliased (e.g., ['msg', 'pageId']) and direct (e.g., ['pageId']) paths.\n */\nfunction propRefMatchesField(propRef: PropRef, fieldName: string): boolean {\n const { path } = propRef\n // Direct field reference: ['pageId']\n if (path.length === 1 && path[0] === fieldName) {\n return true\n }\n // Aliased field reference: ['msg', 'pageId'] or ['m', 'pageId']\n if (path.length === 2 && path[1] === fieldName) {\n return true\n }\n return false\n}\n\n/**\n * Extract filter values from an 'eq' function call.\n * Pattern: eq(ref(filterField), val(x)) or eq(val(x), ref(filterField))\n */\nfunction extractFromEq(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // eq(ref, val)\n if (\n isPropRef(left) &&\n propRefMatchesField(left, filterField) &&\n isValue(right)\n ) {\n return [right.value]\n }\n\n // eq(val, ref) - reversed order\n if (\n isValue(left) &&\n isPropRef(right) &&\n propRefMatchesField(right, filterField)\n ) {\n return [left.value]\n }\n\n return []\n}\n\n/**\n * Extract filter values from an 'in' function call.\n * Pattern: in(ref(filterField), val([a, b, c]))\n */\nfunction extractFromIn(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // in(ref, val)\n if (\n isPropRef(left) &&\n propRefMatchesField(left, filterField) &&\n isValue(right)\n ) {\n const val = right.value\n if (Array.isArray(val)) {\n return val\n }\n }\n\n return []\n}\n\n/**\n * Recursively walk an expression tree to find all filter values for the given field.\n * Handles 'eq', 'in', 'and', and 'or' expressions.\n */\nfunction walkExpression(expr: unknown, filterField: string): unknown[] {\n if (!isFunc(expr)) return []\n\n const { name, args } = expr\n const results: unknown[] = []\n\n switch (name) {\n case `eq`:\n results.push(...extractFromEq(expr, filterField))\n break\n\n case `in`:\n results.push(...extractFromIn(expr, filterField))\n break\n\n case `and`:\n case `or`:\n // Recursively process all arguments\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n\n // For other functions, recursively check their arguments\n // (in case of nested expressions)\n default:\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n }\n\n return results\n}\n\n/**\n * Extract filter values from LoadSubsetOptions.\n *\n * Parses the `where` expression to find equality (`.eq()`) and set membership (`.in()`)\n * comparisons for the specified filter field.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterField - The field name to extract values for (e.g., 'pageId')\n * @returns Array of unique filter values found in the expression\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1'))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n *\n * @example\n * // For query: where(m => inArray(m.pageId, ['page-1', 'page-2']))\n * extractFilterValues(options, 'pageId') // returns ['page-1', 'page-2']\n *\n * @example\n * // For query: where(m => and(m.pageId.eq('page-1'), m.status.eq('active')))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n */\nexport function extractFilterValues(\n options: LoadSubsetOptions,\n filterField: string,\n): unknown[] {\n const { where } = options\n\n if (!where) {\n return []\n }\n\n // Extract from the where expression\n const values = walkExpression(where, filterField)\n\n // Return unique values using toKey for stable identity comparison\n const seen = new Set<string>()\n const unique: unknown[] = []\n for (const value of values) {\n const key = toKey(value)\n if (!seen.has(key)) {\n seen.add(key)\n unique.push(value)\n }\n }\n\n return unique\n}\n\n/**\n * Check if LoadSubsetOptions contains a filter for the specified field.\n */\nexport function hasFilterField(\n options: LoadSubsetOptions,\n filterField: string,\n): boolean {\n return extractFilterValues(options, filterField).length > 0\n}\n\n/**\n * Extract filter values for multiple filter dimensions from LoadSubsetOptions.\n *\n * Parses the `where` expression to find values for each configured filter dimension.\n * Results are keyed by convexArg for direct use in Convex query args.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterDimensions - Array of filter dimensions to extract\n * @returns Object mapping convexArg -> values for dimensions with matches\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1') && m.authorId.eq('user-1'))\n * extractMultipleFilterValues(options, [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ])\n * // returns { pageIds: ['page-1'], authorIds: ['user-1'] }\n */\nexport function extractMultipleFilterValues(\n options: LoadSubsetOptions,\n filterDimensions: FilterDimension[],\n): ExtractedFilters {\n const result: ExtractedFilters = {}\n\n for (const dim of filterDimensions) {\n const values = extractFilterValues(options, dim.filterField)\n if (values.length > 0) {\n result[dim.convexArg] = values\n }\n }\n\n return result\n}\n","import { ConvexSyncManager } from './ConvexSyncManager.js'\nimport { extractMultipleFilterValues } from './expression-parser.js'\nimport type {\n CollectionConfig,\n LoadSubsetOptions,\n SyncConfig,\n UtilsRecord,\n} from '@tanstack/db'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { FunctionReference, FunctionReturnType } from 'convex/server'\nimport type {\n ConvexCollectionConfig,\n FilterConfig,\n FilterDimension,\n} from './types.js'\n\n// Re-export types\nexport type {\n ConvexCollectionConfig,\n ConvexUnsubscribe,\n ExtractedFilters,\n FilterConfig,\n FilterDimension,\n} from './types.js'\nexport {\n extractFilterValues,\n extractMultipleFilterValues,\n hasFilterField,\n} from './expression-parser.js'\nexport { ConvexSyncManager } from './ConvexSyncManager.js'\nexport {\n serializeValue,\n deserializeValue,\n toKey,\n fromKey,\n} from './serialization.js'\n\n// Default configuration values\nconst DEFAULT_UPDATED_AT_FIELD = `updatedAt`\nconst DEFAULT_DEBOUNCE_MS = 50\nconst DEFAULT_TAIL_OVERLAP_MS = 10000\nconst DEFAULT_RESUBSCRIBE_THRESHOLD = 10\n\n/**\n * Normalize filter configuration to an array of FilterDimension.\n * - undefined = [] (0 filters, global sync)\n * - single object = [object] (1 filter)\n * - array = array as-is (N filters)\n */\nfunction normalizeFilterConfig(filters: FilterConfig): FilterDimension[] {\n if (filters === undefined) return []\n return Array.isArray(filters) ? filters : [filters]\n}\n\n/**\n * Schema output type inference helper\n */\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Infer the item type from a Convex query's return type.\n * Expects the query to return an array of items.\n */\ntype InferQueryItemType<TQuery extends FunctionReference<'query'>> =\n FunctionReturnType<TQuery> extends Array<infer T>\n ? T extends object\n ? T\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Creates collection options for use with TanStack DB's createCollection.\n * This integrates Convex real-time subscriptions with TanStack DB collections\n * using the \"backfill + tail\" synchronization pattern.\n *\n * @example\n * ```typescript\n * import { createCollection } from '@tanstack/react-db'\n * import { convexCollectionOptions } from '@michaelstewart/convex-tanstack-db-collection'\n * import { api } from '@convex/_generated/api'\n *\n * // Single filter dimension\n * const messagesCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.messages.sync,\n * filters: { filterField: 'pageId', convexArg: 'pageIds' },\n * getKey: (msg) => msg._id,\n *\n * onInsert: async ({ transaction }) => {\n * const newMsg = transaction.mutations[0].modified\n * await convexClient.mutation(api.messages.create, newMsg)\n * },\n * })\n * )\n *\n * // Multiple filter dimensions\n * const filteredCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncFiltered,\n * filters: [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ],\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // No filters (global sync)\n * const allItemsCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncAll, // Query takes only { after }\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // In UI:\n * const { data: messages } = useLiveQuery(q =>\n * q.from({ msg: messagesCollection })\n * .where(({ msg }) => msg.pageId.eq('page-123'))\n * )\n * ```\n */\n\n// Overload for when schema is provided\nexport function convexCollectionOptions<\n TSchema extends StandardSchemaV1,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: ConvexCollectionConfig<\n InferSchemaOutput<TSchema>,\n TKey,\n TSchema,\n TUtils\n > & {\n schema: TSchema\n },\n): CollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils>\n\n// Overload for when no schema is provided - T is inferred from query's return type\nexport function convexCollectionOptions<\n TQuery extends FunctionReference<'query'>,\n T extends InferQueryItemType<TQuery> = InferQueryItemType<TQuery>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: Omit<ConvexCollectionConfig<T, TKey, never, TUtils>, 'query'> & {\n schema?: never\n query: TQuery\n getKey: (item: T) => TKey\n },\n): CollectionConfig<T, TKey, never, TUtils>\n\n// Implementation - uses concrete types; overloads provide proper type inference\nexport function convexCollectionOptions(\n config: ConvexCollectionConfig<\n Record<string, unknown>,\n string | number,\n never,\n UtilsRecord\n >,\n): CollectionConfig<\n Record<string, unknown>,\n string | number,\n never,\n UtilsRecord\n> {\n const {\n client,\n query,\n filters,\n updatedAtFieldName = DEFAULT_UPDATED_AT_FIELD,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n tailOverlapMs = DEFAULT_TAIL_OVERLAP_MS,\n resubscribeThreshold = DEFAULT_RESUBSCRIBE_THRESHOLD,\n getKey,\n onInsert,\n onUpdate,\n ...baseConfig\n } = config\n\n // Normalize filter configuration\n const filterDimensions = normalizeFilterConfig(filters)\n\n // Create the sync manager\n const syncManager = new ConvexSyncManager<any, any>({\n client,\n query,\n filterDimensions,\n updatedAtFieldName,\n debounceMs,\n tailOverlapMs,\n resubscribeThreshold,\n getKey: getKey as (item: any) => string | number,\n })\n\n // Create the sync configuration\n const syncConfig: SyncConfig<any, any> = {\n sync: (params) => {\n const { collection, begin, write, commit, markReady } = params\n\n // Initialize sync manager with callbacks\n syncManager.setCallbacks({\n collection: {\n get: (key) => collection.get(key),\n has: (key) => collection.has(key),\n },\n begin,\n write,\n commit,\n markReady,\n })\n\n // Return loadSubset, unloadSubset, and cleanup handlers\n return {\n loadSubset: (options: LoadSubsetOptions): Promise<void> => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n return syncManager.requestFilters({})\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(\n options,\n filterDimensions,\n )\n\n // Sync if ANY dimension has values (any-filter matching)\n // Convex query arg validators enforce which combinations are valid\n if (Object.keys(extracted).length === 0) {\n // No filter values found - this is expected for queries that filter\n // by other fields (e.g., clientId, parentId). These queries read from\n // the already-synced collection and don't need to trigger a sync.\n return Promise.resolve()\n }\n\n return syncManager.requestFilters(extracted)\n },\n\n unloadSubset: (options: LoadSubsetOptions): void => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n syncManager.releaseFilters({})\n return\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(\n options,\n filterDimensions,\n )\n\n if (Object.keys(extracted).length > 0) {\n syncManager.releaseFilters(extracted)\n }\n },\n\n cleanup: () => {\n syncManager.cleanup()\n },\n }\n },\n }\n\n // Return the complete collection config\n return {\n ...baseConfig,\n getKey,\n syncMode: `on-demand`, // Always on-demand since we sync based on query predicates\n sync: syncConfig,\n onInsert,\n onUpdate,\n }\n}\n"],"names":["_a"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@michaelstewart/convex-tanstack-db-collection",
3
- "version": "0.0.1",
3
+ "version": "0.0.2",
4
4
  "description": "Convex collection adapter for TanStack DB with real-time sync",
5
5
  "author": "Michael Stewart",
6
6
  "license": "MIT",
@@ -46,6 +46,8 @@
46
46
  "build": "vite build",
47
47
  "dev": "vite build --watch",
48
48
  "lint": "eslint . --fix",
49
+ "format": "prettier --write .",
50
+ "format:check": "prettier --check .",
49
51
  "check": "tsc --noEmit",
50
52
  "test": "vitest run"
51
53
  },
@@ -55,7 +57,13 @@
55
57
  "convex": "^1.31.2"
56
58
  },
57
59
  "devDependencies": {
60
+ "@eslint/js": "^9.39.2",
61
+ "@types/node": "^25.0.9",
62
+ "eslint": "^9.39.2",
63
+ "eslint-plugin-import-x": "^4.16.1",
64
+ "prettier": "^3.8.0",
58
65
  "typescript": "^5.0.0",
66
+ "typescript-eslint": "^8.53.1",
59
67
  "vite": "^6.0.0",
60
68
  "vite-plugin-dts": "^4.0.0",
61
69
  "vitest": "^3.0.0"
@@ -1,3 +1,4 @@
1
+ import { toKey, fromKey } from './serialization.js'
1
2
  import type { FunctionReference } from 'convex/server'
2
3
  import type { ChangeMessageOrDeleteKeyMessage } from '@tanstack/db'
3
4
  import type {
@@ -6,7 +7,6 @@ import type {
6
7
  ExtractedFilters,
7
8
  FilterDimension,
8
9
  } from './types.js'
9
- import { toKey, fromKey } from './serialization.js'
10
10
 
11
11
  export type { ConvexSyncManagerOptions }
12
12
 
@@ -113,7 +113,7 @@ export class ConvexSyncManager<
113
113
  }
114
114
  return acc
115
115
  },
116
- {} as Record<string, string[]>
116
+ {} as Record<string, string[]>,
117
117
  )
118
118
  return JSON.stringify(sorted)
119
119
  }
@@ -155,7 +155,7 @@ export class ConvexSyncManager<
155
155
  const serialized = toKey(v)
156
156
  const alreadyActive = activeSet.has(serialized)
157
157
  const alreadyPending = this.pendingFilters[convexArg]?.some(
158
- (pv) => toKey(pv) === serialized
158
+ (pv) => toKey(pv) === serialized,
159
159
  )
160
160
  return !alreadyActive && !alreadyPending
161
161
  })
@@ -164,7 +164,7 @@ export class ConvexSyncManager<
164
164
  throw new Error(
165
165
  `Filter '${dim.filterField}' is configured as single but multiple values were requested. ` +
166
166
  `Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. ` +
167
- `Use single: false if you need to sync multiple values.`
167
+ `Use single: false if you need to sync multiple values.`,
168
168
  )
169
169
  }
170
170
  }
@@ -178,7 +178,7 @@ export class ConvexSyncManager<
178
178
  }
179
179
  // Check if already pending (by serialized key)
180
180
  const alreadyPending = this.pendingFilters[convexArg].some(
181
- (v) => toKey(v) === serialized
181
+ (v) => toKey(v) === serialized,
182
182
  )
183
183
  if (!alreadyPending) {
184
184
  this.pendingFilters[convexArg].push(value)
@@ -303,7 +303,8 @@ export class ConvexSyncManager<
303
303
 
304
304
  // Check if there's anything to process
305
305
  const hasPendingFilters = Object.keys(this.pendingFilters).length > 0
306
- const needsGlobalSync = this.filterDimensions.length === 0 && this.hasRequestedGlobal
306
+ const needsGlobalSync =
307
+ this.filterDimensions.length === 0 && this.hasRequestedGlobal
307
308
 
308
309
  if (!hasPendingFilters && !needsGlobalSync) {
309
310
  return
@@ -462,7 +463,7 @@ export class ConvexSyncManager<
462
463
  },
463
464
  (error: unknown) => {
464
465
  console.error(`[ConvexSyncManager] Subscription error:`, error)
465
- }
466
+ },
466
467
  )
467
468
  this.currentSubscription = () => subscription.unsubscribe()
468
469
  } else {
@@ -498,7 +499,9 @@ export class ConvexSyncManager<
498
499
 
499
500
  for (const item of items) {
500
501
  const key = this.getKey(item)
501
- const incomingTs = (item as any)[this.updatedAtFieldName] as number | undefined
502
+ const incomingTs = (item as any)[this.updatedAtFieldName] as
503
+ | number
504
+ | undefined
502
505
 
503
506
  // Update global cursor to track the latest timestamp we've seen
504
507
  if (incomingTs !== undefined && incomingTs > this.globalCursor) {
@@ -516,7 +519,9 @@ export class ConvexSyncManager<
516
519
  }
517
520
  } else {
518
521
  // Existing item - check if incoming is fresher (LWW)
519
- const existingTs = (existing as any)[this.updatedAtFieldName] as number | undefined
522
+ const existingTs = (existing as any)[this.updatedAtFieldName] as
523
+ | number
524
+ | undefined
520
525
 
521
526
  if (incomingTs !== undefined && existingTs !== undefined) {
522
527
  if (incomingTs > existingTs) {
@@ -1,6 +1,6 @@
1
+ import { toKey } from './serialization.js'
1
2
  import type { LoadSubsetOptions } from '@tanstack/db'
2
3
  import type { ExtractedFilters, FilterDimension } from './types.js'
3
- import { toKey } from './serialization.js'
4
4
 
5
5
  /**
6
6
  * TanStack DB expression types (simplified for our needs)
@@ -93,12 +93,20 @@ function extractFromEq(func: Func, filterField: string): unknown[] {
93
93
  const [left, right] = func.args
94
94
 
95
95
  // eq(ref, val)
96
- if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {
96
+ if (
97
+ isPropRef(left) &&
98
+ propRefMatchesField(left, filterField) &&
99
+ isValue(right)
100
+ ) {
97
101
  return [right.value]
98
102
  }
99
103
 
100
104
  // eq(val, ref) - reversed order
101
- if (isValue(left) && isPropRef(right) && propRefMatchesField(right, filterField)) {
105
+ if (
106
+ isValue(left) &&
107
+ isPropRef(right) &&
108
+ propRefMatchesField(right, filterField)
109
+ ) {
102
110
  return [left.value]
103
111
  }
104
112
 
@@ -115,7 +123,11 @@ function extractFromIn(func: Func, filterField: string): unknown[] {
115
123
  const [left, right] = func.args
116
124
 
117
125
  // in(ref, val)
118
- if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {
126
+ if (
127
+ isPropRef(left) &&
128
+ propRefMatchesField(left, filterField) &&
129
+ isValue(right)
130
+ ) {
119
131
  const val = right.value
120
132
  if (Array.isArray(val)) {
121
133
  return val
@@ -188,7 +200,7 @@ function walkExpression(expr: unknown, filterField: string): unknown[] {
188
200
  */
189
201
  export function extractFilterValues(
190
202
  options: LoadSubsetOptions,
191
- filterField: string
203
+ filterField: string,
192
204
  ): unknown[] {
193
205
  const { where } = options
194
206
 
@@ -216,7 +228,10 @@ export function extractFilterValues(
216
228
  /**
217
229
  * Check if LoadSubsetOptions contains a filter for the specified field.
218
230
  */
219
- export function hasFilterField(options: LoadSubsetOptions, filterField: string): boolean {
231
+ export function hasFilterField(
232
+ options: LoadSubsetOptions,
233
+ filterField: string,
234
+ ): boolean {
220
235
  return extractFilterValues(options, filterField).length > 0
221
236
  }
222
237
 
@@ -240,7 +255,7 @@ export function hasFilterField(options: LoadSubsetOptions, filterField: string):
240
255
  */
241
256
  export function extractMultipleFilterValues(
242
257
  options: LoadSubsetOptions,
243
- filterDimensions: FilterDimension[]
258
+ filterDimensions: FilterDimension[],
244
259
  ): ExtractedFilters {
245
260
  const result: ExtractedFilters = {}
246
261
 
package/src/index.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ConvexSyncManager } from './ConvexSyncManager.js'
2
+ import { extractMultipleFilterValues } from './expression-parser.js'
1
3
  import type {
2
4
  CollectionConfig,
3
5
  LoadSubsetOptions,
@@ -6,9 +8,11 @@ import type {
6
8
  } from '@tanstack/db'
7
9
  import type { StandardSchemaV1 } from '@standard-schema/spec'
8
10
  import type { FunctionReference, FunctionReturnType } from 'convex/server'
9
- import { ConvexSyncManager } from './ConvexSyncManager.js'
10
- import { extractMultipleFilterValues } from './expression-parser.js'
11
- import type { ConvexCollectionConfig, FilterConfig, FilterDimension } from './types.js'
11
+ import type {
12
+ ConvexCollectionConfig,
13
+ FilterConfig,
14
+ FilterDimension,
15
+ } from './types.js'
12
16
 
13
17
  // Re-export types
14
18
  export type {
@@ -18,9 +22,18 @@ export type {
18
22
  FilterConfig,
19
23
  FilterDimension,
20
24
  } from './types.js'
21
- export { extractFilterValues, extractMultipleFilterValues, hasFilterField } from './expression-parser.js'
25
+ export {
26
+ extractFilterValues,
27
+ extractMultipleFilterValues,
28
+ hasFilterField,
29
+ } from './expression-parser.js'
22
30
  export { ConvexSyncManager } from './ConvexSyncManager.js'
23
- export { serializeValue, deserializeValue, toKey, fromKey } from './serialization.js'
31
+ export {
32
+ serializeValue,
33
+ deserializeValue,
34
+ toKey,
35
+ fromKey,
36
+ } from './serialization.js'
24
37
 
25
38
  // Default configuration values
26
39
  const DEFAULT_UPDATED_AT_FIELD = `updatedAt`
@@ -121,9 +134,14 @@ export function convexCollectionOptions<
121
134
  TKey extends string | number = string | number,
122
135
  TUtils extends UtilsRecord = UtilsRecord,
123
136
  >(
124
- config: ConvexCollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils> & {
137
+ config: ConvexCollectionConfig<
138
+ InferSchemaOutput<TSchema>,
139
+ TKey,
140
+ TSchema,
141
+ TUtils
142
+ > & {
125
143
  schema: TSchema
126
- }
144
+ },
127
145
  ): CollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils>
128
146
 
129
147
  // Overload for when no schema is provided - T is inferred from query's return type
@@ -137,13 +155,23 @@ export function convexCollectionOptions<
137
155
  schema?: never
138
156
  query: TQuery
139
157
  getKey: (item: T) => TKey
140
- }
158
+ },
141
159
  ): CollectionConfig<T, TKey, never, TUtils>
142
160
 
143
161
  // Implementation - uses concrete types; overloads provide proper type inference
144
162
  export function convexCollectionOptions(
145
- config: ConvexCollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord>
146
- ): CollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord> {
163
+ config: ConvexCollectionConfig<
164
+ Record<string, unknown>,
165
+ string | number,
166
+ never,
167
+ UtilsRecord
168
+ >,
169
+ ): CollectionConfig<
170
+ Record<string, unknown>,
171
+ string | number,
172
+ never,
173
+ UtilsRecord
174
+ > {
147
175
  const {
148
176
  client,
149
177
  query,
@@ -199,7 +227,10 @@ export function convexCollectionOptions(
199
227
  }
200
228
 
201
229
  // Extract filter values from the where expression
202
- const extracted = extractMultipleFilterValues(options, filterDimensions)
230
+ const extracted = extractMultipleFilterValues(
231
+ options,
232
+ filterDimensions,
233
+ )
203
234
 
204
235
  // Sync if ANY dimension has values (any-filter matching)
205
236
  // Convex query arg validators enforce which combinations are valid
@@ -221,7 +252,10 @@ export function convexCollectionOptions(
221
252
  }
222
253
 
223
254
  // Extract filter values from the where expression
224
- const extracted = extractMultipleFilterValues(options, filterDimensions)
255
+ const extracted = extractMultipleFilterValues(
256
+ options,
257
+ filterDimensions,
258
+ )
225
259
 
226
260
  if (Object.keys(extracted).length > 0) {
227
261
  syncManager.releaseFilters(extracted)
@@ -63,7 +63,7 @@ export function serializeValue(value: unknown): unknown {
63
63
  Object.entries(value as Record<string, unknown>).map(([key, val]) => [
64
64
  key,
65
65
  serializeValue(val),
66
- ])
66
+ ]),
67
67
  )
68
68
  }
69
69
 
@@ -82,7 +82,9 @@ export function deserializeValue(value: unknown): unknown {
82
82
  case `nan`:
83
83
  return NaN
84
84
  case `infinity`:
85
- return value.sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY
85
+ return value.sign === 1
86
+ ? Number.POSITIVE_INFINITY
87
+ : Number.NEGATIVE_INFINITY
86
88
  case `date`:
87
89
  return new Date(value.value as string)
88
90
  default:
@@ -90,7 +92,12 @@ export function deserializeValue(value: unknown): unknown {
90
92
  }
91
93
  }
92
94
 
93
- if (value === null || typeof value === `string` || typeof value === `number` || typeof value === `boolean`) {
95
+ if (
96
+ value === null ||
97
+ typeof value === `string` ||
98
+ typeof value === `number` ||
99
+ typeof value === `boolean`
100
+ ) {
94
101
  return value
95
102
  }
96
103
 
@@ -103,7 +110,7 @@ export function deserializeValue(value: unknown): unknown {
103
110
  Object.entries(value as Record<string, unknown>).map(([key, val]) => [
104
111
  key,
105
112
  deserializeValue(val),
106
- ])
113
+ ]),
107
114
  )
108
115
  }
109
116
 
package/src/types.ts CHANGED
@@ -87,7 +87,6 @@ export interface ConvexOnUpdateSubscription<T> {
87
87
  * Base client interface with query method
88
88
  */
89
89
  interface ConvexClientBase {
90
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
91
90
  query(query: FunctionReference<'query'>, args: any): Promise<any>
92
91
  }
93
92
 
@@ -95,10 +94,12 @@ interface ConvexClientBase {
95
94
  * ConvexReactClient pattern: uses watchQuery for subscriptions
96
95
  */
97
96
  interface ConvexReactClientLike extends ConvexClientBase {
98
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
99
- watchQuery(query: FunctionReference<'query'>, args: any): {
97
+ watchQuery(
98
+ query: FunctionReference<'query'>,
99
+ args: any,
100
+ ): {
100
101
  onUpdate(callback: () => void): () => void
101
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
102
+
102
103
  localQueryResult(): any
103
104
  }
104
105
  }
@@ -109,12 +110,12 @@ interface ConvexReactClientLike extends ConvexClientBase {
109
110
  interface ConvexBrowserClientLike extends ConvexClientBase {
110
111
  onUpdate(
111
112
  query: FunctionReference<'query'>,
112
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
+
113
114
  args: any,
114
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
115
+
115
116
  callback: (result: any) => void,
116
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
117
- onError?: (error: any) => void
117
+
118
+ onError?: (error: any) => void,
118
119
  ): {
119
120
  unsubscribe(): void
120
121
  }
@@ -237,7 +238,9 @@ export interface ConvexSyncParams<
237
238
  /**
238
239
  * Options for the ConvexSyncManager
239
240
  */
240
- export interface ConvexSyncManagerOptions<T extends object = Record<string, unknown>> {
241
+ export interface ConvexSyncManagerOptions<
242
+ T extends object = Record<string, unknown>,
243
+ > {
241
244
  client: ConvexClientLike
242
245
  query: FunctionReference<'query'>
243
246
  filterDimensions: FilterDimension[]
@@ -257,4 +260,3 @@ export type ConvexCollectionFullConfig<
257
260
  TSchema extends StandardSchemaV1 = never,
258
261
  TUtils extends UtilsRecord = UtilsRecord,
259
262
  > = CollectionConfig<T, TKey, TSchema, TUtils>
260
-