@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 +50 -42
- package/dist/cjs/index.cjs +8 -2
- package/dist/cjs/index.cjs.map +1 -1
- package/dist/esm/index.js +8 -2
- package/dist/esm/index.js.map +1 -1
- package/package.json +9 -1
- package/src/ConvexSyncManager.ts +14 -9
- package/src/expression-parser.ts +22 -7
- package/src/index.ts +46 -12
- package/src/serialization.ts +11 -4
- package/src/types.ts +12 -10
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
|
|
22
|
-
import { v } from
|
|
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(
|
|
31
|
+
channelId: v.id('channels'),
|
|
32
32
|
authorId: v.string(),
|
|
33
33
|
body: v.string(),
|
|
34
34
|
updatedAt: v.number(),
|
|
35
35
|
})
|
|
36
|
-
.index(
|
|
37
|
-
.index(
|
|
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
|
|
59
|
-
|
|
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(
|
|
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(
|
|
80
|
-
.withIndex(
|
|
81
|
-
q.eq(
|
|
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
|
|
195
|
-
|
|
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
|
|
201
|
-
|
|
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,
|
|
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(
|
|
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(
|
|
246
|
-
.withIndex(
|
|
247
|
-
q.eq(
|
|
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(
|
|
261
|
-
.withIndex(
|
|
262
|
-
q.eq(
|
|
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
|
|
371
|
-
|
|
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
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -635,7 +635,10 @@ function convexCollectionOptions(config) {
|
|
|
635
635
|
if (filterDimensions.length === 0) {
|
|
636
636
|
return syncManager.requestFilters({});
|
|
637
637
|
}
|
|
638
|
-
const extracted = extractMultipleFilterValues(
|
|
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(
|
|
652
|
+
const extracted = extractMultipleFilterValues(
|
|
653
|
+
options,
|
|
654
|
+
filterDimensions
|
|
655
|
+
);
|
|
650
656
|
if (Object.keys(extracted).length > 0) {
|
|
651
657
|
syncManager.releaseFilters(extracted);
|
|
652
658
|
}
|
package/dist/cjs/index.cjs.map
CHANGED
|
@@ -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(
|
|
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(
|
|
650
|
+
const extracted = extractMultipleFilterValues(
|
|
651
|
+
options,
|
|
652
|
+
filterDimensions
|
|
653
|
+
);
|
|
648
654
|
if (Object.keys(extracted).length > 0) {
|
|
649
655
|
syncManager.releaseFilters(extracted);
|
|
650
656
|
}
|
package/dist/esm/index.js.map
CHANGED
|
@@ -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":"AAaA,SAAS,aAAa,OAAqC;AACzD,SACE,OAAO,UAAU,YACjB,UAAU,QACV,YAAY,SACZ,OAAQ,MAAqB,WAAW;AAE5C;AAMO,SAAS,eAAe,OAAyB;AACtD,MAAI,UAAU,QAAW;AACvB,WAAO,EAAE,QAAQ,YAAA;AAAA,EACnB;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,QAAI,OAAO,MAAM,KAAK,GAAG;AACvB,aAAO,EAAE,QAAQ,MAAA;AAAA,IACnB;AACA,QAAI,UAAU,OAAO,mBAAmB;AACtC,aAAO,EAAE,QAAQ,YAAY,MAAM,EAAA;AAAA,IACrC;AACA,QAAI,UAAU,OAAO,mBAAmB;AACtC,aAAO,EAAE,QAAQ,YAAY,MAAM,GAAA;AAAA,IACrC;AAAA,EACF;AAEA,MACE,UAAU,QACV,OAAO,UAAU,YACjB,OAAO,UAAU,YACjB,OAAO,UAAU,WACjB;AACA,WAAO;AAAA,EACT;AAEA,MAAI,iBAAiB,MAAM;AACzB,WAAO,EAAE,QAAQ,QAAQ,OAAO,MAAM,SAAO;AAAA,EAC/C;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,SAAS,eAAe,IAAI,CAAC;AAAA,EACjD;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO;AAAA,MACZ,OAAO,QAAQ,KAAgC,EAAE,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAAA,QACnE;AAAA,QACA,eAAe,GAAG;AAAA,MAAA,CACnB;AAAA,IAAA;AAAA,EAEL;AAEA,SAAO;AACT;AAMO,SAAS,iBAAiB,OAAyB;AACxD,MAAI,aAAa,KAAK,GAAG;AACvB,YAAQ,MAAM,QAAA;AAAA,MACZ,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO;AAAA,MACT,KAAK;AACH,eAAO,MAAM,SAAS,IAAI,OAAO,oBAAoB,OAAO;AAAA,MAC9D,KAAK;AACH,eAAO,IAAI,KAAK,MAAM,KAAe;AAAA,MACvC;AACE,eAAO;AAAA,IAAA;AAAA,EAEb;AAEA,MAAI,UAAU,QAAQ,OAAO,UAAU,YAAY,OAAO,UAAU,YAAY,OAAO,UAAU,WAAW;AAC1G,WAAO;AAAA,EACT;AAEA,MAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,WAAO,MAAM,IAAI,CAAC,SAAS,iBAAiB,IAAI,CAAC;AAAA,EACnD;AAEA,MAAI,OAAO,UAAU,UAAU;AAC7B,WAAO,OAAO;AAAA,MACZ,OAAO,QAAQ,KAAgC,EAAE,IAAI,CAAC,CAAC,KAAK,GAAG,MAAM;AAAA,QACnE;AAAA,QACA,iBAAiB,GAAG;AAAA,MAAA,CACrB;AAAA,IAAA;AAAA,EAEL;AAEA,SAAO;AACT;AAKO,SAAS,MAAM,OAAwB;AAC5C,SAAO,KAAK,UAAU,eAAe,KAAK,CAAC;AAC7C;AAKO,SAAS,QAAQ,KAAsB;AAC5C,SAAO,iBAAiB,KAAK,MAAM,GAAG,CAAC;AACzC;ACnFO,MAAM,kBAGX;AAAA,EA+BA,YAAY,SAAsC;AAnBlD,SAAQ,uCAAuB,IAAA;AAC/B,SAAQ,gCAAgB,IAAA;AACxB,SAAQ,iBAAmC,CAAA;AAC3C,SAAQ,eAAe;AACvB,SAAQ,sBAA2C;AACnD,SAAQ,gBAAsD;AAC9D,SAAQ,eAAe;AACvB,SAAQ,cAAc;AAGtB,SAAQ,qBAAqB;AAC7B,SAAQ,iBAAiB;AAGzB,SAAQ,4BAA4B;AAGpC,SAAQ,YAA2C;AAGjD,SAAK,SAAS,QAAQ;AACtB,SAAK,QAAQ,QAAQ;AACrB,SAAK,mBAAmB,QAAQ;AAChC,SAAK,qBAAqB,QAAQ;AAClC,SAAK,aAAa,QAAQ;AAC1B,SAAK,gBAAgB,QAAQ;AAC7B,SAAK,uBAAuB,QAAQ;AACpC,SAAK,SAAS,QAAQ;AAGtB,eAAW,OAAO,KAAK,kBAAkB;AACvC,WAAK,iBAAiB,IAAI,IAAI,WAAW,oBAAI,KAAK;AAAA,IACpD;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,aAAa,WAAyC;AACpD,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBAAmB,SAAmC;AAE5D,UAAM,SAAS,OAAO,KAAK,OAAO,EAC/B,OACA;AAAA,MACC,CAAC,KAAK,QAAQ;AAEZ,cAAM,SAAS,QAAQ,GAAG;AAC1B,YAAI,QAAQ;AACV,cAAI,GAAG,IAAI,OAAO,IAAI,CAAC,MAAM,MAAM,CAAC,CAAC,EAAE,KAAA;AAAA,QACzC;AACA,eAAO;AAAA,MACT;AAAA,MACA,CAAA;AAAA,IAAC;AAEL,WAAO,KAAK,UAAU,MAAM;AAAA,EAC9B;AAAA;AAAA;AAAA;AAAA;AAAA,EAMA,eAAe,SAA0C;AD/G3D;ACiHI,QAAI,KAAK,iBAAiB,WAAW,GAAG;AACtC,WAAK;AACL,UAAI,CAAC,KAAK,oBAAoB;AAC5B,aAAK,qBAAqB;AAC1B,eAAO,KAAK,mBAAA;AAAA,MACd;AACA,aAAO,QAAQ,QAAA;AAAA,IACjB;AAGA,UAAM,eAAe,KAAK,mBAAmB,OAAO;AACpD,UAAM,QAAQ,KAAK,UAAU,IAAI,YAAY,KAAK;AAClD,SAAK,UAAU,IAAI,cAAc,QAAQ,CAAC;AAG1C,QAAI,eAAe;AACnB,eAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,OAAO,GAAG;AACzD,YAAM,YAAY,KAAK,iBAAiB,IAAI,SAAS;AACrD,UAAI,CAAC,UAAW;AAGhB,YAAM,MAAM,KAAK,iBAAiB,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS;AAGvE,UAAI,2BAAK,QAAQ;AACf,cAAM,gBAAgB,UAAU;AAChC,cAAM,iBAAe,UAAK,eAAe,SAAS,MAA7B,mBAAgC,WAAU;AAC/D,cAAM,YAAY,OAAO,OAAO,CAAC,MAAM;AD5I/C,cAAAA;AC6IU,gBAAM,aAAa,MAAM,CAAC;AAC1B,gBAAM,gBAAgB,UAAU,IAAI,UAAU;AAC9C,gBAAM,kBAAiBA,MAAA,KAAK,eAAe,SAAS,MAA7B,gBAAAA,IAAgC;AAAA,YACrD,CAAC,OAAO,MAAM,EAAE,MAAM;AAAA;AAExB,iBAAO,CAAC,iBAAiB,CAAC;AAAA,QAC5B,CAAC;AAED,YAAI,gBAAgB,eAAe,UAAU,SAAS,GAAG;AACvD,gBAAM,IAAI;AAAA,YACR,WAAW,IAAI,WAAW,yEACb,aAAa,cAAc,YAAY,UAAU,UAAU,MAAM;AAAA,UAAA;AAAA,QAGlF;AAAA,MACF;AAEA,iBAAW,SAAS,QAAQ;AAC1B,cAAM,aAAa,MAAM,KAAK;AAC9B,YAAI,CAAC,UAAU,IAAI,UAAU,GAAG;AAE9B,cAAI,CAAC,KAAK,eAAe,SAAS,GAAG;AACnC,iBAAK,eAAe,SAAS,IAAI,CAAA;AAAA,UACnC;AAEA,gBAAM,iBAAiB,KAAK,eAAe,SAAS,EAAE;AAAA,YACpD,CAAC,MAAM,MAAM,CAAC,MAAM;AAAA,UAAA;AAEtB,cAAI,CAAC,gBAAgB;AACnB,iBAAK,eAAe,SAAS,EAAE,KAAK,KAAK;AACzC,2BAAe;AAAA,UACjB;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAGA,QAAI,cAAc;AAChB,aAAO,KAAK,mBAAA;AAAA,IACd;AAEA,WAAO,QAAQ,QAAA;AAAA,EACjB;AAAA;AAAA;AAAA;AAAA,EAKA,eAAe,SAAiC;AAE9C,QAAI,KAAK,iBAAiB,WAAW,GAAG;AACtC,WAAK,iBAAiB,KAAK,IAAI,GAAG,KAAK,iBAAiB,CAAC;AACzD,UAAI,KAAK,mBAAmB,KAAK,KAAK,oBAAoB;AACxD,aAAK,qBAAqB;AAC1B,aAAK,mBAAA;AAAA,MACP;AACA;AAAA,IACF;AAGA,UAAM,eAAe,KAAK,mBAAmB,OAAO;AACpD,UAAM,SAAS,KAAK,UAAU,IAAI,YAAY,KAAK,KAAK;AAExD,QAAI,SAAS,GAAG;AACd,WAAK,UAAU,OAAO,YAAY;AAAA,IACpC,OAAO;AACL,WAAK,UAAU,IAAI,cAAc,KAAK;AAAA,IACxC;AAGA,SAAK,0BAAA;AAAA,EACP;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,4BAAkC;AAExC,UAAM,uCAAuB,IAAA;AAC7B,eAAW,OAAO,KAAK,kBAAkB;AACvC,uBAAiB,IAAI,IAAI,WAAW,oBAAI,KAAK;AAAA,IAC/C;AAGA,eAAW,gBAAgB,KAAK,UAAU,KAAA,GAAQ;AAChD,UAAI;AAEF,cAAM,UAAU,KAAK,MAAM,YAAY;AACvC,mBAAW,CAAC,WAAW,gBAAgB,KAAK,OAAO,QAAQ,OAAO,GAAG;AACnE,gBAAM,SAAS,iBAAiB,IAAI,SAAS;AAC7C,cAAI,QAAQ;AACV,uBAAW,cAAc,kBAAkB;AACzC,qBAAO,IAAI,UAAU;AAAA,YACvB;AAAA,UACF;AAAA,QACF;AAAA,MACF,QAAQ;AAAA,MAER;AAAA,IACF;AAIA,QAAI,0BAA0B;AAC9B,eAAW,CAAC,WAAW,SAAS,KAAK,KAAK,kBAAkB;AAC1D,YAAM,SAAS,iBAAiB,IAAI,SAAS;AAC7C,iBAAW,cAAc,WAAW;AAClC,YAAI,CAAC,OAAO,IAAI,UAAU,GAAG;AAC3B,oBAAU,OAAO,UAAU;AAC3B,oCAA0B;AAAA,QAC5B;AAAA,MACF;AAAA,IACF;AAEA,QAAI,yBAAyB;AAC3B,WAAK,mBAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAAoC;AAC1C,WAAO,IAAI,QAAQ,CAAC,SAAS,WAAW;AAEtC,UAAI,KAAK,eAAe;AACtB,qBAAa,KAAK,aAAa;AAAA,MACjC;AAGA,WAAK,gBAAgB,WAAW,YAAY;AAC1C,YAAI;AACF,gBAAM,KAAK,mBAAA;AACX,kBAAA;AAAA,QACF,SAAS,OAAO;AACd,iBAAO,KAAK;AAAA,QACd;AAAA,MACF,GAAG,KAAK,UAAU;AAAA,IACpB,CAAC;AAAA,EACH;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,qBAAoC;AAChD,QAAI,KAAK,cAAc;AACrB;AAAA,IACF;AAGA,UAAM,oBAAoB,OAAO,KAAK,KAAK,cAAc,EAAE,SAAS;AACpE,UAAM,kBAAkB,KAAK,iBAAiB,WAAW,KAAK,KAAK;AAEnE,QAAI,CAAC,qBAAqB,CAAC,iBAAiB;AAC1C;AAAA,IACF;AAEA,SAAK,eAAe;AAEpB,QAAI;AACF,UAAI,KAAK,iBAAiB,WAAW,GAAG;AAEtC,cAAM,KAAK,kBAAA;AAAA,MACb,OAAO;AAEL,cAAM,aAAa,EAAE,GAAG,KAAK,eAAA;AAC7B,aAAK,iBAAiB,CAAA;AAGtB,mBAAW,CAAC,WAAW,MAAM,KAAK,OAAO,QAAQ,UAAU,GAAG;AAC5D,gBAAM,YAAY,KAAK,iBAAiB,IAAI,SAAS;AACrD,cAAI,WAAW;AACb,uBAAW,SAAS,QAAQ;AAC1B,wBAAU,IAAI,MAAM,KAAK,CAAC;AAAA,YAC5B;AAAA,UACF;AAAA,QACF;AAGA,cAAM,KAAK,YAAY,UAAU;AAAA,MACnC;AAGA,WAAK,mBAAA;AAGL,UAAI,CAAC,KAAK,eAAe,KAAK,WAAW;AACvC,aAAK,UAAU,UAAA;AACf,aAAK,cAAc;AAAA,MACrB;AAAA,IACF,UAAA;AACE,WAAK,eAAe;AAAA,IACtB;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,oBAAmC;AAC/C,QAAI;AACF,YAAM,OAAgC,EAAE,OAAO,EAAA;AAE/C,YAAM,QAAQ,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,IAAW;AAE7D,UAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,aAAK,mBAAmB,KAAY;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,8CAA8C,KAAK;AACjE,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,MAAc,YAAY,YAA6C;AACrE,QAAI,OAAO,KAAK,UAAU,EAAE,WAAW,EAAG;AAE1C,QAAI;AAEF,YAAM,OAAgC;AAAA,QACpC,GAAG;AAAA,QACH,OAAO;AAAA,MAAA;AAGT,YAAM,QAAQ,MAAM,KAAK,OAAO,MAAM,KAAK,OAAO,IAAW;AAE7D,UAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,aAAK,mBAAmB,KAAY;AAAA,MACtC;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,uCAAuC,KAAK;AAC1D,YAAM;AAAA,IACR;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKQ,eAAe,OAAwC;AAC7D,UAAM,OAAgC,EAAE,MAAA;AAGxC,eAAW,CAAC,WAAW,gBAAgB,KAAK,KAAK,kBAAkB;AACjE,YAAM,SAAS,CAAC,GAAG,gBAAgB,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,CAAC;AAG1D,YAAM,MAAM,KAAK,iBAAiB,KAAK,CAAC,MAAM,EAAE,cAAc,SAAS;AACvE,WAAK,SAAS,KAAI,2BAAK,UAAS,OAAO,CAAC,IAAI;AAAA,IAC9C;AAEA,WAAO;AAAA,EACT;AAAA;AAAA;AAAA;AAAA,EAKQ,qBAA2B;AAEjC,QAAI,KAAK,qBAAqB;AAC5B,WAAK,oBAAA;AACL,WAAK,sBAAsB;AAAA,IAC7B;AAGA,SAAK,4BAA4B;AAGjC,QAAI,KAAK,iBAAiB,WAAW,GAAG;AAEtC,UAAI,CAAC,KAAK,oBAAoB;AAC5B;AAAA,MACF;AAAA,IACF,OAAO;AAEL,UAAI,kBAAkB;AACtB,iBAAW,aAAa,KAAK,iBAAiB,OAAA,GAAU;AACtD,YAAI,UAAU,OAAO,GAAG;AACtB,4BAAkB;AAClB;AAAA,QACF;AAAA,MACF;AACA,UAAI,CAAC,iBAAiB;AACpB;AAAA,MACF;AAAA,IACF;AAGA,UAAM,SAAS,KAAK,IAAI,GAAG,KAAK,eAAe,KAAK,aAAa;AAGjE,UAAM,OAAO,KAAK,eAAe,MAAM;AAGvC,QAAI,cAAc,KAAK,QAAQ;AAE7B,YAAM,eAAe,KAAK,OAAO;AAAA,QAC/B,KAAK;AAAA,QACL;AAAA,QACA,CAAC,WAAoB;AACnB,cAAI,WAAW,QAAW;AACxB,kBAAM,QAAQ;AACd,gBAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,mBAAK,mBAAmB,KAAK;AAAA,YAC/B;AAAA,UACF;AAAA,QACF;AAAA,QACA,CAAC,UAAmB;AAClB,kBAAQ,MAAM,2CAA2C,KAAK;AAAA,QAChE;AAAA,MAAA;AAEF,WAAK,sBAAsB,MAAM,aAAa,YAAA;AAAA,IAChD,OAAO;AAEL,YAAM,QAAQ,KAAK,OAAO,WAAW,KAAK,OAAO,IAAW;AAC5D,WAAK,sBAAsB,MAAM,SAAS,MAAM;AAE9C,cAAM,SAAS,MAAM,iBAAA;AACrB,YAAI,WAAW,QAAW;AACxB,gBAAM,QAAQ;AACd,cAAI,MAAM,QAAQ,KAAK,GAAG;AACxB,iBAAK,mBAAmB,KAAK;AAAA,UAC/B;AAAA,QACF;AAAA,MACF,CAAC;AAAA,IACH;AAAA,EACF;AAAA;AAAA;AAAA;AAAA;AAAA,EAMQ,mBAAmB,OAAkB;AAC3C,QAAI,CAAC,KAAK,aAAa,MAAM,WAAW,EAAG;AAE3C,UAAM,EAAE,YAAY,OAAO,OAAO,OAAA,IAAW,KAAK;AAGlD,UAAM,iBAAiB,KAAK;AAC5B,QAAI,eAAe;AAEnB,UAAA;AAEA,eAAW,QAAQ,OAAO;AACxB,YAAM,MAAM,KAAK,OAAO,IAAI;AAC5B,YAAM,aAAc,KAAa,KAAK,kBAAkB;AAGxD,UAAI,eAAe,UAAa,aAAa,KAAK,cAAc;AAC9D,aAAK,eAAe;AAAA,MACtB;AAEA,YAAM,WAAW,WAAW,IAAI,GAAG;AAEnC,UAAI,CAAC,UAAU;AAEb,cAAM,EAAE,MAAM,UAAU,OAAO,MAAM;AAErC,YAAI,eAAe,UAAa,aAAa,gBAAgB;AAC3D;AAAA,QACF;AAAA,MACF,OAAO;AAEL,cAAM,aAAc,SAAiB,KAAK,kBAAkB;AAE5D,YAAI,eAAe,UAAa,eAAe,QAAW;AACxD,cAAI,aAAa,YAAY;AAE3B,kBAAM,EAAE,MAAM,UAAU,OAAO,MAAM;AAErC,gBAAI,aAAa,gBAAgB;AAC/B;AAAA,YACF;AAAA,UACF;AAAA,QAEF,WAAW,eAAe,QAAW;AAEnC,gBAAM,EAAE,MAAM,UAAU,OAAO,MAAM;AAAA,QACvC;AAAA,MAEF;AAAA,IACF;AAEA,WAAA;AAGA,SAAK,6BAA6B;AAGlC,QACE,KAAK,uBAAuB,KAC5B,KAAK,6BAA6B,KAAK,wBACvC,KAAK,wBAAwB,MAC7B;AACA,WAAK,mBAAA;AAAA,IACP;AAAA,EACF;AAAA;AAAA;AAAA;AAAA,EAKA,UAAgB;AAEd,QAAI,KAAK,eAAe;AACtB,mBAAa,KAAK,aAAa;AAC/B,WAAK,gBAAgB;AAAA,IACvB;AAGA,QAAI,KAAK,qBAAqB;AAC5B,WAAK,oBAAA;AACL,WAAK,sBAAsB;AAAA,IAC7B;AAGA,eAAW,aAAa,KAAK,iBAAiB,OAAA,GAAU;AACtD,gBAAU,MAAA;AAAA,IACZ;AACA,SAAK,UAAU,MAAA;AACf,SAAK,iBAAiB,CAAA;AACtB,SAAK,eAAe;AACpB,SAAK,cAAc;AACnB,SAAK,qBAAqB;AAC1B,SAAK,iBAAiB;AACtB,SAAK,4BAA4B;AACjC,SAAK,YAAY;AAAA,EACnB;AAAA;AAAA;AAAA;AAAA,EAKA,eAQE;AACA,UAAM,mBAA8C,CAAA;AACpD,eAAW,CAAC,WAAW,gBAAgB,KAAK,KAAK,kBAAkB;AACjE,uBAAiB,SAAS,IAAI,CAAC,GAAG,gBAAgB,EAAE,IAAI,CAAC,MAAM,QAAQ,CAAC,CAAC;AAAA,IAC3E;AAEA,WAAO;AAAA,MACL;AAAA,MACA,cAAc,KAAK;AAAA,MACnB,gBAAgB,EAAE,GAAG,KAAK,eAAA;AAAA,MAC1B,iBAAiB,KAAK,wBAAwB;AAAA,MAC9C,aAAa,KAAK;AAAA,MAClB,oBAAoB,KAAK;AAAA,MACzB,2BAA2B,KAAK;AAAA,IAAA;AAAA,EAEpC;AACF;ACrkBA,SAAS,UAAU,MAAgC;AACjD,SACE,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,KAAK,SAAS,SACd,UAAU,QACV,MAAM,QAAS,KAAiB,IAAI;AAExC;AAKA,SAAS,QAAQ,MAA8B;AAC7C,SACE,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,KAAK,SAAS,SACd,WAAW;AAEf;AAKA,SAAS,OAAO,MAA6B;AAC3C,SACE,OAAO,SAAS,YAChB,SAAS,QACT,UAAU,QACV,KAAK,SAAS,UACd,UAAU,QACV,UAAU,QACV,MAAM,QAAS,KAAc,IAAI;AAErC;AAMA,SAAS,oBAAoB,SAAkB,WAA4B;AACzE,QAAM,EAAE,SAAS;AAEjB,MAAI,KAAK,WAAW,KAAK,KAAK,CAAC,MAAM,WAAW;AAC9C,WAAO;AAAA,EACT;AAEA,MAAI,KAAK,WAAW,KAAK,KAAK,CAAC,MAAM,WAAW;AAC9C,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAMA,SAAS,cAAc,MAAY,aAAgC;AACjE,MAAI,KAAK,KAAK,WAAW,UAAU,CAAA;AAEnC,QAAM,CAAC,MAAM,KAAK,IAAI,KAAK;AAG3B,MAAI,UAAU,IAAI,KAAK,oBAAoB,MAAM,WAAW,KAAK,QAAQ,KAAK,GAAG;AAC/E,WAAO,CAAC,MAAM,KAAK;AAAA,EACrB;AAGA,MAAI,QAAQ,IAAI,KAAK,UAAU,KAAK,KAAK,oBAAoB,OAAO,WAAW,GAAG;AAChF,WAAO,CAAC,KAAK,KAAK;AAAA,EACpB;AAEA,SAAO,CAAA;AACT;AAMA,SAAS,cAAc,MAAY,aAAgC;AACjE,MAAI,KAAK,KAAK,WAAW,UAAU,CAAA;AAEnC,QAAM,CAAC,MAAM,KAAK,IAAI,KAAK;AAG3B,MAAI,UAAU,IAAI,KAAK,oBAAoB,MAAM,WAAW,KAAK,QAAQ,KAAK,GAAG;AAC/E,UAAM,MAAM,MAAM;AAClB,QAAI,MAAM,QAAQ,GAAG,GAAG;AACtB,aAAO;AAAA,IACT;AAAA,EACF;AAEA,SAAO,CAAA;AACT;AAMA,SAAS,eAAe,MAAe,aAAgC;AACrE,MAAI,CAAC,OAAO,IAAI,UAAU,CAAA;AAE1B,QAAM,EAAE,MAAM,KAAA,IAAS;AACvB,QAAM,UAAqB,CAAA;AAE3B,UAAQ,MAAA;AAAA,IACN,KAAK;AACH,cAAQ,KAAK,GAAG,cAAc,MAAM,WAAW,CAAC;AAChD;AAAA,IAEF,KAAK;AACH,cAAQ,KAAK,GAAG,cAAc,MAAM,WAAW,CAAC;AAChD;AAAA,IAEF,KAAK;AAAA,IACL,KAAK;AAEH,iBAAW,OAAO,MAAM;AACtB,gBAAQ,KAAK,GAAG,eAAe,KAAK,WAAW,CAAC;AAAA,MAClD;AACA;AAAA;AAAA;AAAA,IAIF;AACE,iBAAW,OAAO,MAAM;AACtB,gBAAQ,KAAK,GAAG,eAAe,KAAK,WAAW,CAAC;AAAA,MAClD;AACA;AAAA,EAAA;AAGJ,SAAO;AACT;AAwBO,SAAS,oBACd,SACA,aACW;AACX,QAAM,EAAE,UAAU;AAElB,MAAI,CAAC,OAAO;AACV,WAAO,CAAA;AAAA,EACT;AAGA,QAAM,SAAS,eAAe,OAAO,WAAW;AAGhD,QAAM,2BAAW,IAAA;AACjB,QAAM,SAAoB,CAAA;AAC1B,aAAW,SAAS,QAAQ;AAC1B,UAAM,MAAM,MAAM,KAAK;AACvB,QAAI,CAAC,KAAK,IAAI,GAAG,GAAG;AAClB,WAAK,IAAI,GAAG;AACZ,aAAO,KAAK,KAAK;AAAA,IACnB;AAAA,EACF;AAEA,SAAO;AACT;AAKO,SAAS,eAAe,SAA4B,aAA8B;AACvF,SAAO,oBAAoB,SAAS,WAAW,EAAE,SAAS;AAC5D;AAoBO,SAAS,4BACd,SACA,kBACkB;AAClB,QAAM,SAA2B,CAAA;AAEjC,aAAW,OAAO,kBAAkB;AAClC,UAAM,SAAS,oBAAoB,SAAS,IAAI,WAAW;AAC3D,QAAI,OAAO,SAAS,GAAG;AACrB,aAAO,IAAI,SAAS,IAAI;AAAA,IAC1B;AAAA,EACF;AAEA,SAAO;AACT;ACrOA,MAAM,2BAA2B;AACjC,MAAM,sBAAsB;AAC5B,MAAM,0BAA0B;AAChC,MAAM,gCAAgC;AAQtC,SAAS,sBAAsB,SAA0C;AACvE,MAAI,YAAY,OAAW,QAAO,CAAA;AAClC,SAAO,MAAM,QAAQ,OAAO,IAAI,UAAU,CAAC,OAAO;AACpD;AAwGO,SAAS,wBACd,QACgF;AAChF,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA,qBAAqB;AAAA,IACrB,aAAa;AAAA,IACb,gBAAgB;AAAA,IAChB,uBAAuB;AAAA,IACvB;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,IACD;AAGJ,QAAM,mBAAmB,sBAAsB,OAAO;AAGtD,QAAM,cAAc,IAAI,kBAA4B;AAAA,IAClD;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,EAAA,CACD;AAGD,QAAM,aAAmC;AAAA,IACvC,MAAM,CAAC,WAAW;AAChB,YAAM,EAAE,YAAY,OAAO,OAAO,QAAQ,cAAc;AAGxD,kBAAY,aAAa;AAAA,QACvB,YAAY;AAAA,UACV,KAAK,CAAC,QAAQ,WAAW,IAAI,GAAG;AAAA,UAChC,KAAK,CAAC,QAAQ,WAAW,IAAI,GAAG;AAAA,QAAA;AAAA,QAElC;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MAAA,CACD;AAGD,aAAO;AAAA,QACL,YAAY,CAAC,YAA8C;AAEzD,cAAI,iBAAiB,WAAW,GAAG;AACjC,mBAAO,YAAY,eAAe,EAAE;AAAA,UACtC;AAGA,gBAAM,YAAY,4BAA4B,SAAS,gBAAgB;AAIvE,cAAI,OAAO,KAAK,SAAS,EAAE,WAAW,GAAG;AAIvC,mBAAO,QAAQ,QAAA;AAAA,UACjB;AAEA,iBAAO,YAAY,eAAe,SAAS;AAAA,QAC7C;AAAA,QAEA,cAAc,CAAC,YAAqC;AAElD,cAAI,iBAAiB,WAAW,GAAG;AACjC,wBAAY,eAAe,EAAE;AAC7B;AAAA,UACF;AAGA,gBAAM,YAAY,4BAA4B,SAAS,gBAAgB;AAEvE,cAAI,OAAO,KAAK,SAAS,EAAE,SAAS,GAAG;AACrC,wBAAY,eAAe,SAAS;AAAA,UACtC;AAAA,QACF;AAAA,QAEA,SAAS,MAAM;AACb,sBAAY,QAAA;AAAA,QACd;AAAA,MAAA;AAAA,IAEJ;AAAA,EAAA;AAIF,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,UAAU;AAAA;AAAA,IACV,MAAM;AAAA,IACN;AAAA,IACA;AAAA,EAAA;AAEJ;"}
|
|
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.
|
|
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"
|
package/src/ConvexSyncManager.ts
CHANGED
|
@@ -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 =
|
|
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
|
|
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
|
|
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) {
|
package/src/expression-parser.ts
CHANGED
|
@@ -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 (
|
|
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 (
|
|
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 (
|
|
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(
|
|
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 {
|
|
10
|
-
|
|
11
|
-
|
|
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 {
|
|
25
|
+
export {
|
|
26
|
+
extractFilterValues,
|
|
27
|
+
extractMultipleFilterValues,
|
|
28
|
+
hasFilterField,
|
|
29
|
+
} from './expression-parser.js'
|
|
22
30
|
export { ConvexSyncManager } from './ConvexSyncManager.js'
|
|
23
|
-
export {
|
|
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<
|
|
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<
|
|
146
|
-
|
|
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(
|
|
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(
|
|
255
|
+
const extracted = extractMultipleFilterValues(
|
|
256
|
+
options,
|
|
257
|
+
filterDimensions,
|
|
258
|
+
)
|
|
225
259
|
|
|
226
260
|
if (Object.keys(extracted).length > 0) {
|
|
227
261
|
syncManager.releaseFilters(extracted)
|
package/src/serialization.ts
CHANGED
|
@@ -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
|
|
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 (
|
|
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
|
-
|
|
99
|
-
|
|
97
|
+
watchQuery(
|
|
98
|
+
query: FunctionReference<'query'>,
|
|
99
|
+
args: any,
|
|
100
|
+
): {
|
|
100
101
|
onUpdate(callback: () => void): () => void
|
|
101
|
-
|
|
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
|
-
|
|
113
|
+
|
|
113
114
|
args: any,
|
|
114
|
-
|
|
115
|
+
|
|
115
116
|
callback: (result: any) => void,
|
|
116
|
-
|
|
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<
|
|
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
|
-
|