@michaelstewart/convex-tanstack-db-collection 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/cjs/index.cjs +679 -0
- package/dist/cjs/index.cjs.map +1 -0
- package/dist/esm/index.d.ts +461 -0
- package/dist/esm/index.js +679 -0
- package/dist/esm/index.js.map +1 -0
- package/package.json +63 -0
- package/src/ConvexSyncManager.ts +611 -0
- package/src/expression-parser.ts +255 -0
- package/src/index.ts +247 -0
- package/src/serialization.ts +125 -0
- package/src/types.ts +260 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sources":["../../src/serialization.ts","../../src/ConvexSyncManager.ts","../../src/expression-parser.ts","../../src/index.ts"],"sourcesContent":["/**\n * Value serialization utilities for stable hashing and round-tripping.\n *\n * Adapted from TanStack DB's query-db-collection:\n * https://github.com/TanStack/db/blob/main/packages/query-db-collection/src/serialization.ts\n */\n\ninterface TypeMarker {\n __type: string\n value?: unknown\n sign?: number\n}\n\nfunction isTypeMarker(value: unknown): value is TypeMarker {\n return (\n typeof value === `object` &&\n value !== null &&\n `__type` in value &&\n typeof (value as TypeMarker).__type === `string`\n )\n}\n\n/**\n * Serializes a value into a JSON-safe format that preserves special JS types.\n * Handles: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function serializeValue(value: unknown): unknown {\n if (value === undefined) {\n return { __type: `undefined` }\n }\n\n if (typeof value === `number`) {\n if (Number.isNaN(value)) {\n return { __type: `nan` }\n }\n if (value === Number.POSITIVE_INFINITY) {\n return { __type: `infinity`, sign: 1 }\n }\n if (value === Number.NEGATIVE_INFINITY) {\n return { __type: `infinity`, sign: -1 }\n }\n }\n\n if (\n value === null ||\n typeof value === `string` ||\n typeof value === `number` ||\n typeof value === `boolean`\n ) {\n return value\n }\n\n if (value instanceof Date) {\n return { __type: `date`, value: value.toJSON() }\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => serializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n serializeValue(val),\n ])\n )\n }\n\n return value\n}\n\n/**\n * Deserializes a value back from its JSON-safe format.\n * Restores: undefined, NaN, Infinity, -Infinity, Date, arrays, and objects.\n */\nexport function deserializeValue(value: unknown): unknown {\n if (isTypeMarker(value)) {\n switch (value.__type) {\n case `undefined`:\n return undefined\n case `nan`:\n return NaN\n case `infinity`:\n return value.sign === 1 ? Number.POSITIVE_INFINITY : Number.NEGATIVE_INFINITY\n case `date`:\n return new Date(value.value as string)\n default:\n return value\n }\n }\n\n if (value === null || typeof value === `string` || typeof value === `number` || typeof value === `boolean`) {\n return value\n }\n\n if (Array.isArray(value)) {\n return value.map((item) => deserializeValue(item))\n }\n\n if (typeof value === `object`) {\n return Object.fromEntries(\n Object.entries(value as Record<string, unknown>).map(([key, val]) => [\n key,\n deserializeValue(val),\n ])\n )\n }\n\n return value\n}\n\n/**\n * Converts a value to a stable string key for use in Sets/Maps.\n */\nexport function toKey(value: unknown): string {\n return JSON.stringify(serializeValue(value))\n}\n\n/**\n * Restores a value from its string key representation.\n */\nexport function fromKey(key: string): unknown {\n return deserializeValue(JSON.parse(key))\n}\n","import type { FunctionReference } from 'convex/server'\nimport type { ChangeMessageOrDeleteKeyMessage } from '@tanstack/db'\nimport type {\n ConvexClientLike,\n ConvexSyncManagerOptions,\n ExtractedFilters,\n FilterDimension,\n} from './types.js'\nimport { toKey, fromKey } from './serialization.js'\n\nexport type { ConvexSyncManagerOptions }\n\n/**\n * Sync callbacks passed from TanStack DB's sync function\n */\nexport interface SyncCallbacks<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n collection: {\n get: (key: TKey) => T | undefined\n has: (key: TKey) => boolean\n }\n begin: () => void\n write: (message: ChangeMessageOrDeleteKeyMessage<T, TKey>) => void\n commit: () => void\n markReady: () => void\n}\n\n/**\n * ConvexSyncManager - Manages real-time synchronization with Convex backend\n *\n * Implements the \"backfill + tail\" pattern:\n * 1. When new filters are requested, backfill with `after: 0` to get full history\n * 2. Maintain a single live subscription for all active filter values with `after: globalCursor - tailOverlapMs`\n * 3. Use LWW (Last-Write-Wins) to handle overlapping data from backfill and subscription\n *\n * Supports 0, 1, or N filter dimensions:\n * - 0 filters: Global sync with just { after }\n * - 1+ filters: Filter-based sync with values extracted from where clauses\n */\nexport class ConvexSyncManager<\n T extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n> {\n // Configuration\n private client: ConvexClientLike\n private query: FunctionReference<`query`>\n private filterDimensions: FilterDimension[]\n private updatedAtFieldName: string\n private debounceMs: number\n private tailOverlapMs: number\n private resubscribeThreshold: number\n private getKey: (item: T) => TKey\n\n // State - per-dimension tracking (keyed by convexArg)\n private activeDimensions = new Map<string, Set<string>>()\n private refCounts = new Map<string, number>() // composite key -> count\n private pendingFilters: ExtractedFilters = {}\n private globalCursor = 0\n private currentSubscription: (() => void) | null = null\n private debounceTimer: ReturnType<typeof setTimeout> | null = null\n private isProcessing = false\n private markedReady = false\n\n // For 0-filter case (global sync)\n private hasRequestedGlobal = false\n private globalRefCount = 0\n\n // Track messages received since last subscription to batch cursor updates\n private messagesSinceSubscription = 0\n\n // Sync callbacks (set when sync() is called)\n private callbacks: SyncCallbacks<T, TKey> | null = null\n\n constructor(options: ConvexSyncManagerOptions<T>) {\n this.client = options.client\n this.query = options.query\n this.filterDimensions = options.filterDimensions\n this.updatedAtFieldName = options.updatedAtFieldName\n this.debounceMs = options.debounceMs\n this.tailOverlapMs = options.tailOverlapMs\n this.resubscribeThreshold = options.resubscribeThreshold\n this.getKey = options.getKey as (item: T) => TKey\n\n // Initialize activeDimensions for each filter dimension\n for (const dim of this.filterDimensions) {\n this.activeDimensions.set(dim.convexArg, new Set())\n }\n }\n\n /**\n * Initialize the sync manager with callbacks from TanStack DB\n */\n setCallbacks(callbacks: SyncCallbacks<T, TKey>): void {\n this.callbacks = callbacks\n }\n\n /**\n * Create a composite key for ref counting multi-filter combinations.\n * Uses serialized values for deterministic keys.\n */\n private createCompositeKey(filters: ExtractedFilters): string {\n // Sort by convexArg for deterministic ordering\n const sorted = Object.keys(filters)\n .sort()\n .reduce(\n (acc, key) => {\n // Serialize values and sort for deterministic ordering\n const values = filters[key]\n if (values) {\n acc[key] = values.map((v) => toKey(v)).sort()\n }\n return acc\n },\n {} as Record<string, string[]>\n )\n return JSON.stringify(sorted)\n }\n\n /**\n * Request filters to be synced (called by loadSubset)\n * Filters are batched via debouncing for efficiency\n */\n requestFilters(filters: ExtractedFilters): Promise<void> {\n // Handle 0-filter case (global sync)\n if (this.filterDimensions.length === 0) {\n this.globalRefCount++\n if (!this.hasRequestedGlobal) {\n this.hasRequestedGlobal = true\n return this.scheduleProcessing()\n }\n return Promise.resolve()\n }\n\n // Increment ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = this.refCounts.get(compositeKey) || 0\n this.refCounts.set(compositeKey, count + 1)\n\n // Track which values are new (not yet active)\n let hasNewValues = false\n for (const [convexArg, values] of Object.entries(filters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (!activeSet) continue\n\n // Find the dimension config for single validation\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n\n // Validate single constraint before adding values\n if (dim?.single) {\n const existingCount = activeSet.size\n const pendingCount = this.pendingFilters[convexArg]?.length ?? 0\n const newValues = values.filter((v) => {\n const serialized = toKey(v)\n const alreadyActive = activeSet.has(serialized)\n const alreadyPending = this.pendingFilters[convexArg]?.some(\n (pv) => toKey(pv) === serialized\n )\n return !alreadyActive && !alreadyPending\n })\n\n if (existingCount + pendingCount + newValues.length > 1) {\n throw new Error(\n `Filter '${dim.filterField}' is configured as single but multiple values were requested. ` +\n `Active: ${existingCount}, Pending: ${pendingCount}, New: ${newValues.length}. ` +\n `Use single: false if you need to sync multiple values.`\n )\n }\n }\n\n for (const value of values) {\n const serialized = toKey(value)\n if (!activeSet.has(serialized)) {\n // Add to pending filters\n if (!this.pendingFilters[convexArg]) {\n this.pendingFilters[convexArg] = []\n }\n // Check if already pending (by serialized key)\n const alreadyPending = this.pendingFilters[convexArg].some(\n (v) => toKey(v) === serialized\n )\n if (!alreadyPending) {\n this.pendingFilters[convexArg].push(value)\n hasNewValues = true\n }\n }\n }\n }\n\n // If there are new values, schedule processing\n if (hasNewValues) {\n return this.scheduleProcessing()\n }\n\n return Promise.resolve()\n }\n\n /**\n * Release filters when no longer needed (called by unloadSubset)\n */\n releaseFilters(filters: ExtractedFilters): void {\n // Handle 0-filter case\n if (this.filterDimensions.length === 0) {\n this.globalRefCount = Math.max(0, this.globalRefCount - 1)\n if (this.globalRefCount === 0 && this.hasRequestedGlobal) {\n this.hasRequestedGlobal = false\n this.updateSubscription()\n }\n return\n }\n\n // Decrement ref count for this filter combination\n const compositeKey = this.createCompositeKey(filters)\n const count = (this.refCounts.get(compositeKey) || 0) - 1\n\n if (count <= 0) {\n this.refCounts.delete(compositeKey)\n } else {\n this.refCounts.set(compositeKey, count)\n }\n\n // Check if any values are now unreferenced\n this.cleanupUnreferencedValues()\n }\n\n /**\n * Remove values from activeDimensions that are no longer referenced\n * by any composite key in refCounts\n */\n private cleanupUnreferencedValues(): void {\n // Collect all referenced serialized values per dimension\n const referencedValues = new Map<string, Set<string>>()\n for (const dim of this.filterDimensions) {\n referencedValues.set(dim.convexArg, new Set())\n }\n\n // Walk through all composite keys and collect their serialized values\n for (const compositeKey of this.refCounts.keys()) {\n try {\n // Composite keys store values as already-serialized strings\n const filters = JSON.parse(compositeKey) as Record<string, string[]>\n for (const [convexArg, serializedValues] of Object.entries(filters)) {\n const refSet = referencedValues.get(convexArg)\n if (refSet) {\n for (const serialized of serializedValues) {\n refSet.add(serialized)\n }\n }\n }\n } catch {\n // Skip invalid keys\n }\n }\n\n // Remove unreferenced values from activeDimensions\n // (activeDimensions stores serialized keys)\n let needsSubscriptionUpdate = false\n for (const [convexArg, activeSet] of this.activeDimensions) {\n const refSet = referencedValues.get(convexArg)!\n for (const serialized of activeSet) {\n if (!refSet.has(serialized)) {\n activeSet.delete(serialized)\n needsSubscriptionUpdate = true\n }\n }\n }\n\n if (needsSubscriptionUpdate) {\n this.updateSubscription()\n }\n }\n\n /**\n * Schedule debounced processing of pending filters\n */\n private scheduleProcessing(): Promise<void> {\n return new Promise((resolve, reject) => {\n // Clear existing timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n }\n\n // Schedule processing\n this.debounceTimer = setTimeout(async () => {\n try {\n await this.processFilterBatch()\n resolve()\n } catch (error) {\n reject(error)\n }\n }, this.debounceMs)\n })\n }\n\n /**\n * Process the current batch of pending filters\n */\n private async processFilterBatch(): Promise<void> {\n if (this.isProcessing) {\n return\n }\n\n // Check if there's anything to process\n const hasPendingFilters = Object.keys(this.pendingFilters).length > 0\n const needsGlobalSync = this.filterDimensions.length === 0 && this.hasRequestedGlobal\n\n if (!hasPendingFilters && !needsGlobalSync) {\n return\n }\n\n this.isProcessing = true\n\n try {\n if (this.filterDimensions.length === 0) {\n // 0-filter case: global sync\n await this.runGlobalBackfill()\n } else {\n // Collect new filter values that need backfill\n const newFilters = { ...this.pendingFilters }\n this.pendingFilters = {}\n\n // Add to active dimensions (store serialized keys)\n for (const [convexArg, values] of Object.entries(newFilters)) {\n const activeSet = this.activeDimensions.get(convexArg)\n if (activeSet) {\n for (const value of values) {\n activeSet.add(toKey(value))\n }\n }\n }\n\n // Run backfill for new filter values (fetch full history)\n await this.runBackfill(newFilters)\n }\n\n // Update the live subscription to include all active values\n this.updateSubscription()\n\n // Mark ready after first successful sync\n if (!this.markedReady && this.callbacks) {\n this.callbacks.markReady()\n this.markedReady = true\n }\n } finally {\n this.isProcessing = false\n }\n }\n\n /**\n * Run global backfill for 0-filter case\n */\n private async runGlobalBackfill(): Promise<void> {\n try {\n const args: Record<string, unknown> = { after: 0 }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Global backfill error:', error)\n throw error\n }\n }\n\n /**\n * Run backfill query for new filter values to get their full history\n */\n private async runBackfill(newFilters: ExtractedFilters): Promise<void> {\n if (Object.keys(newFilters).length === 0) return\n\n try {\n // Query with after: 0 to get full history for new filter values\n const args: Record<string, unknown> = {\n ...newFilters,\n after: 0,\n }\n\n const items = await this.client.query(this.query, args as any)\n\n if (Array.isArray(items)) {\n this.handleIncomingData(items as T[])\n }\n } catch (error) {\n console.error('[ConvexSyncManager] Backfill error:', error)\n throw error\n }\n }\n\n /**\n * Build query args from all active dimensions\n */\n private buildQueryArgs(after: number): Record<string, unknown> {\n const args: Record<string, unknown> = { after }\n\n // Deserialize values back to original types for the Convex query\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n const values = [...serializedValues].map((s) => fromKey(s))\n\n // Check if this dimension is configured as single\n const dim = this.filterDimensions.find((d) => d.convexArg === convexArg)\n args[convexArg] = dim?.single ? values[0] : values\n }\n\n return args\n }\n\n /**\n * Update the live subscription to cover all active filter values\n */\n private updateSubscription(): void {\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Reset message counter for new subscription\n this.messagesSinceSubscription = 0\n\n // Check if we should subscribe\n if (this.filterDimensions.length === 0) {\n // 0-filter case: subscribe if global sync is active\n if (!this.hasRequestedGlobal) {\n return\n }\n } else {\n // Check if any dimension has active values\n let hasActiveValues = false\n for (const activeSet of this.activeDimensions.values()) {\n if (activeSet.size > 0) {\n hasActiveValues = true\n break\n }\n }\n if (!hasActiveValues) {\n return\n }\n }\n\n // Calculate cursor with overlap to avoid missing updates\n const cursor = Math.max(0, this.globalCursor - this.tailOverlapMs)\n\n // Build subscription args\n const args = this.buildQueryArgs(cursor)\n\n // Runtime detection: ConvexClient has onUpdate, ConvexReactClient has watchQuery\n if ('onUpdate' in this.client) {\n // ConvexClient pattern: client.onUpdate(query, args, callback)\n const subscription = this.client.onUpdate(\n this.query,\n args as any,\n (result: unknown) => {\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n },\n (error: unknown) => {\n console.error(`[ConvexSyncManager] Subscription error:`, error)\n }\n )\n this.currentSubscription = () => subscription.unsubscribe()\n } else {\n // ConvexReactClient pattern: client.watchQuery(query, args).onUpdate(callback)\n const watch = this.client.watchQuery(this.query, args as any)\n this.currentSubscription = watch.onUpdate(() => {\n // Get current value from the watch\n const result = watch.localQueryResult()\n if (result !== undefined) {\n const items = result as T[]\n if (Array.isArray(items)) {\n this.handleIncomingData(items)\n }\n }\n })\n }\n }\n\n /**\n * Handle incoming data from backfill or subscription\n * Uses LWW (Last-Write-Wins) to resolve conflicts\n */\n private handleIncomingData(items: T[]): void {\n if (!this.callbacks || items.length === 0) return\n\n const { collection, begin, write, commit } = this.callbacks\n\n // Track if we see new items that advance the cursor\n const previousCursor = this.globalCursor\n let newItemCount = 0\n\n begin()\n\n for (const item of items) {\n const key = this.getKey(item)\n const incomingTs = (item as any)[this.updatedAtFieldName] as number | undefined\n\n // Update global cursor to track the latest timestamp we've seen\n if (incomingTs !== undefined && incomingTs > this.globalCursor) {\n this.globalCursor = incomingTs\n }\n\n const existing = collection.get(key)\n\n if (!existing) {\n // New item - insert\n write({ type: `insert`, value: item })\n // Count as new if it's beyond the previous cursor (not from overlap)\n if (incomingTs !== undefined && incomingTs > previousCursor) {\n newItemCount++\n }\n } else {\n // Existing item - check if incoming is fresher (LWW)\n const existingTs = (existing as any)[this.updatedAtFieldName] as number | undefined\n\n if (incomingTs !== undefined && existingTs !== undefined) {\n if (incomingTs > existingTs) {\n // Incoming is fresher - update\n write({ type: `update`, value: item })\n // Count as new if it's beyond the previous cursor\n if (incomingTs > previousCursor) {\n newItemCount++\n }\n }\n // Otherwise skip (stale data from overlap)\n } else if (incomingTs !== undefined) {\n // Existing has no timestamp, incoming does - update\n write({ type: `update`, value: item })\n }\n // If incoming has no timestamp, skip (can't determine freshness)\n }\n }\n\n commit()\n\n // Track only new messages (beyond previous cursor) for cursor advancement\n this.messagesSinceSubscription += newItemCount\n\n // Re-subscribe with advanced cursor after threshold is reached\n if (\n this.resubscribeThreshold > 0 &&\n this.messagesSinceSubscription >= this.resubscribeThreshold &&\n this.currentSubscription !== null\n ) {\n this.updateSubscription()\n }\n }\n\n /**\n * Clean up all resources\n */\n cleanup(): void {\n // Clear debounce timer\n if (this.debounceTimer) {\n clearTimeout(this.debounceTimer)\n this.debounceTimer = null\n }\n\n // Unsubscribe from current subscription\n if (this.currentSubscription) {\n this.currentSubscription()\n this.currentSubscription = null\n }\n\n // Clear state\n for (const activeSet of this.activeDimensions.values()) {\n activeSet.clear()\n }\n this.refCounts.clear()\n this.pendingFilters = {}\n this.globalCursor = 0\n this.markedReady = false\n this.hasRequestedGlobal = false\n this.globalRefCount = 0\n this.messagesSinceSubscription = 0\n this.callbacks = null\n }\n\n /**\n * Get debug info about current state\n */\n getDebugInfo(): {\n activeDimensions: Record<string, unknown[]>\n globalCursor: number\n pendingFilters: ExtractedFilters\n hasSubscription: boolean\n markedReady: boolean\n hasRequestedGlobal: boolean\n messagesSinceSubscription: number\n } {\n const activeDimensions: Record<string, unknown[]> = {}\n for (const [convexArg, serializedValues] of this.activeDimensions) {\n activeDimensions[convexArg] = [...serializedValues].map((s) => fromKey(s))\n }\n\n return {\n activeDimensions,\n globalCursor: this.globalCursor,\n pendingFilters: { ...this.pendingFilters },\n hasSubscription: this.currentSubscription !== null,\n markedReady: this.markedReady,\n hasRequestedGlobal: this.hasRequestedGlobal,\n messagesSinceSubscription: this.messagesSinceSubscription,\n }\n }\n}\n","import type { LoadSubsetOptions } from '@tanstack/db'\nimport type { ExtractedFilters, FilterDimension } from './types.js'\nimport { toKey } from './serialization.js'\n\n/**\n * TanStack DB expression types (simplified for our needs)\n * These mirror the IR types from @tanstack/db\n */\ninterface PropRef {\n type: `ref`\n path: Array<string>\n}\n\ninterface Value {\n type: `val`\n value: unknown\n}\n\ninterface Func {\n type: `func`\n name: string\n args: Array<BasicExpression>\n}\n\ntype BasicExpression = PropRef | Value | Func\n\n/**\n * Check if a value is a PropRef expression\n */\nfunction isPropRef(expr: unknown): expr is PropRef {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `ref` &&\n `path` in expr &&\n Array.isArray((expr as PropRef).path)\n )\n}\n\n/**\n * Check if a value is a Value expression\n */\nfunction isValue(expr: unknown): expr is Value {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `val` &&\n `value` in expr\n )\n}\n\n/**\n * Check if a value is a Func expression\n */\nfunction isFunc(expr: unknown): expr is Func {\n return (\n typeof expr === `object` &&\n expr !== null &&\n `type` in expr &&\n expr.type === `func` &&\n `name` in expr &&\n `args` in expr &&\n Array.isArray((expr as Func).args)\n )\n}\n\n/**\n * Check if a PropRef matches our target field.\n * Handles both aliased (e.g., ['msg', 'pageId']) and direct (e.g., ['pageId']) paths.\n */\nfunction propRefMatchesField(propRef: PropRef, fieldName: string): boolean {\n const { path } = propRef\n // Direct field reference: ['pageId']\n if (path.length === 1 && path[0] === fieldName) {\n return true\n }\n // Aliased field reference: ['msg', 'pageId'] or ['m', 'pageId']\n if (path.length === 2 && path[1] === fieldName) {\n return true\n }\n return false\n}\n\n/**\n * Extract filter values from an 'eq' function call.\n * Pattern: eq(ref(filterField), val(x)) or eq(val(x), ref(filterField))\n */\nfunction extractFromEq(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // eq(ref, val)\n if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {\n return [right.value]\n }\n\n // eq(val, ref) - reversed order\n if (isValue(left) && isPropRef(right) && propRefMatchesField(right, filterField)) {\n return [left.value]\n }\n\n return []\n}\n\n/**\n * Extract filter values from an 'in' function call.\n * Pattern: in(ref(filterField), val([a, b, c]))\n */\nfunction extractFromIn(func: Func, filterField: string): unknown[] {\n if (func.args.length !== 2) return []\n\n const [left, right] = func.args\n\n // in(ref, val)\n if (isPropRef(left) && propRefMatchesField(left, filterField) && isValue(right)) {\n const val = right.value\n if (Array.isArray(val)) {\n return val\n }\n }\n\n return []\n}\n\n/**\n * Recursively walk an expression tree to find all filter values for the given field.\n * Handles 'eq', 'in', 'and', and 'or' expressions.\n */\nfunction walkExpression(expr: unknown, filterField: string): unknown[] {\n if (!isFunc(expr)) return []\n\n const { name, args } = expr\n const results: unknown[] = []\n\n switch (name) {\n case `eq`:\n results.push(...extractFromEq(expr, filterField))\n break\n\n case `in`:\n results.push(...extractFromIn(expr, filterField))\n break\n\n case `and`:\n case `or`:\n // Recursively process all arguments\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n\n // For other functions, recursively check their arguments\n // (in case of nested expressions)\n default:\n for (const arg of args) {\n results.push(...walkExpression(arg, filterField))\n }\n break\n }\n\n return results\n}\n\n/**\n * Extract filter values from LoadSubsetOptions.\n *\n * Parses the `where` expression to find equality (`.eq()`) and set membership (`.in()`)\n * comparisons for the specified filter field.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterField - The field name to extract values for (e.g., 'pageId')\n * @returns Array of unique filter values found in the expression\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1'))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n *\n * @example\n * // For query: where(m => inArray(m.pageId, ['page-1', 'page-2']))\n * extractFilterValues(options, 'pageId') // returns ['page-1', 'page-2']\n *\n * @example\n * // For query: where(m => and(m.pageId.eq('page-1'), m.status.eq('active')))\n * extractFilterValues(options, 'pageId') // returns ['page-1']\n */\nexport function extractFilterValues(\n options: LoadSubsetOptions,\n filterField: string\n): unknown[] {\n const { where } = options\n\n if (!where) {\n return []\n }\n\n // Extract from the where expression\n const values = walkExpression(where, filterField)\n\n // Return unique values using toKey for stable identity comparison\n const seen = new Set<string>()\n const unique: unknown[] = []\n for (const value of values) {\n const key = toKey(value)\n if (!seen.has(key)) {\n seen.add(key)\n unique.push(value)\n }\n }\n\n return unique\n}\n\n/**\n * Check if LoadSubsetOptions contains a filter for the specified field.\n */\nexport function hasFilterField(options: LoadSubsetOptions, filterField: string): boolean {\n return extractFilterValues(options, filterField).length > 0\n}\n\n/**\n * Extract filter values for multiple filter dimensions from LoadSubsetOptions.\n *\n * Parses the `where` expression to find values for each configured filter dimension.\n * Results are keyed by convexArg for direct use in Convex query args.\n *\n * @param options - The LoadSubsetOptions from a live query\n * @param filterDimensions - Array of filter dimensions to extract\n * @returns Object mapping convexArg -> values for dimensions with matches\n *\n * @example\n * // For query: where(m => m.pageId.eq('page-1') && m.authorId.eq('user-1'))\n * extractMultipleFilterValues(options, [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ])\n * // returns { pageIds: ['page-1'], authorIds: ['user-1'] }\n */\nexport function extractMultipleFilterValues(\n options: LoadSubsetOptions,\n filterDimensions: FilterDimension[]\n): ExtractedFilters {\n const result: ExtractedFilters = {}\n\n for (const dim of filterDimensions) {\n const values = extractFilterValues(options, dim.filterField)\n if (values.length > 0) {\n result[dim.convexArg] = values\n }\n }\n\n return result\n}\n","import type {\n CollectionConfig,\n LoadSubsetOptions,\n SyncConfig,\n UtilsRecord,\n} from '@tanstack/db'\nimport type { StandardSchemaV1 } from '@standard-schema/spec'\nimport type { FunctionReference, FunctionReturnType } from 'convex/server'\nimport { ConvexSyncManager } from './ConvexSyncManager.js'\nimport { extractMultipleFilterValues } from './expression-parser.js'\nimport type { ConvexCollectionConfig, FilterConfig, FilterDimension } from './types.js'\n\n// Re-export types\nexport type {\n ConvexCollectionConfig,\n ConvexUnsubscribe,\n ExtractedFilters,\n FilterConfig,\n FilterDimension,\n} from './types.js'\nexport { extractFilterValues, extractMultipleFilterValues, hasFilterField } from './expression-parser.js'\nexport { ConvexSyncManager } from './ConvexSyncManager.js'\nexport { serializeValue, deserializeValue, toKey, fromKey } from './serialization.js'\n\n// Default configuration values\nconst DEFAULT_UPDATED_AT_FIELD = `updatedAt`\nconst DEFAULT_DEBOUNCE_MS = 50\nconst DEFAULT_TAIL_OVERLAP_MS = 10000\nconst DEFAULT_RESUBSCRIBE_THRESHOLD = 10\n\n/**\n * Normalize filter configuration to an array of FilterDimension.\n * - undefined = [] (0 filters, global sync)\n * - single object = [object] (1 filter)\n * - array = array as-is (N filters)\n */\nfunction normalizeFilterConfig(filters: FilterConfig): FilterDimension[] {\n if (filters === undefined) return []\n return Array.isArray(filters) ? filters : [filters]\n}\n\n/**\n * Schema output type inference helper\n */\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Infer the item type from a Convex query's return type.\n * Expects the query to return an array of items.\n */\ntype InferQueryItemType<TQuery extends FunctionReference<'query'>> =\n FunctionReturnType<TQuery> extends Array<infer T>\n ? T extends object\n ? T\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Creates collection options for use with TanStack DB's createCollection.\n * This integrates Convex real-time subscriptions with TanStack DB collections\n * using the \"backfill + tail\" synchronization pattern.\n *\n * @example\n * ```typescript\n * import { createCollection } from '@tanstack/react-db'\n * import { convexCollectionOptions } from '@michaelstewart/convex-tanstack-db-collection'\n * import { api } from '@convex/_generated/api'\n *\n * // Single filter dimension\n * const messagesCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.messages.sync,\n * filters: { filterField: 'pageId', convexArg: 'pageIds' },\n * getKey: (msg) => msg._id,\n *\n * onInsert: async ({ transaction }) => {\n * const newMsg = transaction.mutations[0].modified\n * await convexClient.mutation(api.messages.create, newMsg)\n * },\n * })\n * )\n *\n * // Multiple filter dimensions\n * const filteredCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncFiltered,\n * filters: [\n * { filterField: 'pageId', convexArg: 'pageIds' },\n * { filterField: 'authorId', convexArg: 'authorIds' },\n * ],\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // No filters (global sync)\n * const allItemsCollection = createCollection(\n * convexCollectionOptions({\n * client: convexClient,\n * query: api.items.syncAll, // Query takes only { after }\n * getKey: (item) => item._id,\n * })\n * )\n *\n * // In UI:\n * const { data: messages } = useLiveQuery(q =>\n * q.from({ msg: messagesCollection })\n * .where(({ msg }) => msg.pageId.eq('page-123'))\n * )\n * ```\n */\n\n// Overload for when schema is provided\nexport function convexCollectionOptions<\n TSchema extends StandardSchemaV1,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: ConvexCollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils> & {\n schema: TSchema\n }\n): CollectionConfig<InferSchemaOutput<TSchema>, TKey, TSchema, TUtils>\n\n// Overload for when no schema is provided - T is inferred from query's return type\nexport function convexCollectionOptions<\n TQuery extends FunctionReference<'query'>,\n T extends InferQueryItemType<TQuery> = InferQueryItemType<TQuery>,\n TKey extends string | number = string | number,\n TUtils extends UtilsRecord = UtilsRecord,\n>(\n config: Omit<ConvexCollectionConfig<T, TKey, never, TUtils>, 'query'> & {\n schema?: never\n query: TQuery\n getKey: (item: T) => TKey\n }\n): CollectionConfig<T, TKey, never, TUtils>\n\n// Implementation - uses concrete types; overloads provide proper type inference\nexport function convexCollectionOptions(\n config: ConvexCollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord>\n): CollectionConfig<Record<string, unknown>, string | number, never, UtilsRecord> {\n const {\n client,\n query,\n filters,\n updatedAtFieldName = DEFAULT_UPDATED_AT_FIELD,\n debounceMs = DEFAULT_DEBOUNCE_MS,\n tailOverlapMs = DEFAULT_TAIL_OVERLAP_MS,\n resubscribeThreshold = DEFAULT_RESUBSCRIBE_THRESHOLD,\n getKey,\n onInsert,\n onUpdate,\n ...baseConfig\n } = config\n\n // Normalize filter configuration\n const filterDimensions = normalizeFilterConfig(filters)\n\n // Create the sync manager\n const syncManager = new ConvexSyncManager<any, any>({\n client,\n query,\n filterDimensions,\n updatedAtFieldName,\n debounceMs,\n tailOverlapMs,\n resubscribeThreshold,\n getKey: getKey as (item: any) => string | number,\n })\n\n // Create the sync configuration\n const syncConfig: SyncConfig<any, any> = {\n sync: (params) => {\n const { collection, begin, write, commit, markReady } = params\n\n // Initialize sync manager with callbacks\n syncManager.setCallbacks({\n collection: {\n get: (key) => collection.get(key),\n has: (key) => collection.has(key),\n },\n begin,\n write,\n commit,\n markReady,\n })\n\n // Return loadSubset, unloadSubset, and cleanup handlers\n return {\n loadSubset: (options: LoadSubsetOptions): Promise<void> => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n return syncManager.requestFilters({})\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(options, filterDimensions)\n\n // Sync if ANY dimension has values (any-filter matching)\n // Convex query arg validators enforce which combinations are valid\n if (Object.keys(extracted).length === 0) {\n // No filter values found - this is expected for queries that filter\n // by other fields (e.g., clientId, parentId). These queries read from\n // the already-synced collection and don't need to trigger a sync.\n return Promise.resolve()\n }\n\n return syncManager.requestFilters(extracted)\n },\n\n unloadSubset: (options: LoadSubsetOptions): void => {\n // 0-filter case: global sync\n if (filterDimensions.length === 0) {\n syncManager.releaseFilters({})\n return\n }\n\n // Extract filter values from the where expression\n const extracted = extractMultipleFilterValues(options, filterDimensions)\n\n if (Object.keys(extracted).length > 0) {\n syncManager.releaseFilters(extracted)\n }\n },\n\n cleanup: () => {\n syncManager.cleanup()\n },\n }\n },\n }\n\n // Return the complete collection config\n return {\n ...baseConfig,\n getKey,\n syncMode: `on-demand`, // Always on-demand since we sync based on query predicates\n sync: syncConfig,\n onInsert,\n onUpdate,\n }\n}\n"],"names":["_a"],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@michaelstewart/convex-tanstack-db-collection",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "Convex collection adapter for TanStack DB with real-time sync",
|
|
5
|
+
"author": "Michael Stewart",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"type": "module",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "git+https://github.com/michaelstewart/convex-tanstack-db-collection.git"
|
|
11
|
+
},
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://github.com/michaelstewart/convex-tanstack-db-collection/issues"
|
|
14
|
+
},
|
|
15
|
+
"homepage": "https://github.com/michaelstewart/convex-tanstack-db-collection#readme",
|
|
16
|
+
"keywords": [
|
|
17
|
+
"convex",
|
|
18
|
+
"tanstack",
|
|
19
|
+
"tanstack-db",
|
|
20
|
+
"real-time",
|
|
21
|
+
"sync",
|
|
22
|
+
"collection"
|
|
23
|
+
],
|
|
24
|
+
"main": "dist/cjs/index.cjs",
|
|
25
|
+
"module": "dist/esm/index.js",
|
|
26
|
+
"types": "dist/esm/index.d.ts",
|
|
27
|
+
"exports": {
|
|
28
|
+
".": {
|
|
29
|
+
"import": {
|
|
30
|
+
"types": "./dist/esm/index.d.ts",
|
|
31
|
+
"default": "./dist/esm/index.js"
|
|
32
|
+
},
|
|
33
|
+
"require": {
|
|
34
|
+
"types": "./dist/cjs/index.d.cts",
|
|
35
|
+
"default": "./dist/cjs/index.cjs"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"./package.json": "./package.json"
|
|
39
|
+
},
|
|
40
|
+
"sideEffects": false,
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"src"
|
|
44
|
+
],
|
|
45
|
+
"scripts": {
|
|
46
|
+
"build": "vite build",
|
|
47
|
+
"dev": "vite build --watch",
|
|
48
|
+
"lint": "eslint . --fix",
|
|
49
|
+
"check": "tsc --noEmit",
|
|
50
|
+
"test": "vitest run"
|
|
51
|
+
},
|
|
52
|
+
"dependencies": {
|
|
53
|
+
"@standard-schema/spec": "^1.1.0",
|
|
54
|
+
"@tanstack/db": "^0.5.18",
|
|
55
|
+
"convex": "^1.31.2"
|
|
56
|
+
},
|
|
57
|
+
"devDependencies": {
|
|
58
|
+
"typescript": "^5.0.0",
|
|
59
|
+
"vite": "^6.0.0",
|
|
60
|
+
"vite-plugin-dts": "^4.0.0",
|
|
61
|
+
"vitest": "^3.0.0"
|
|
62
|
+
}
|
|
63
|
+
}
|