@tanstack/query-db-collection 0.2.16 → 0.2.17

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.
@@ -1 +1 @@
1
- {"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\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// QueryFn return type inference helper\ntype InferQueryFnOutput<TQueryFn> = TQueryFn extends (\n context: QueryFunctionContext<any>\n) => Promise<Array<infer TItem>>\n ? TItem extends object\n ? TItem\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Type resolution system with priority order (matches electric.ts pattern)\ntype ResolveType<\n TExplicit extends object | unknown = unknown,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn = unknown,\n> = unknown extends TExplicit\n ? [TSchema] extends [never]\n ? InferQueryFnOutput<TQueryFn>\n : InferSchemaOutput<TSchema>\n : TExplicit\n\n/**\n * Configuration options for creating a Query Collection\n * @template TExplicit - The explicit type of items stored in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? TQueryFn\n : (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>\n\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`]\n /** Schema for validating items */\n schema?: TSchema\n sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`]\n startSync?: CollectionConfig<\n ResolveType<TExplicit, TSchema, TQueryFn>\n >[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @template TError - The type of errors that can occur during queries\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n TError = unknown,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n /** Get the last error encountered by the query (if any); reset on success */\n lastError: () => TError | undefined\n /** Check if the collection is in an error state */\n isError: () => boolean\n /**\n * Get the number of consecutive sync failures.\n * Incremented only when query fails completely (not per retry attempt); reset on success.\n */\n errorCount: () => number\n /**\n * Clear the error state and trigger a refetch of the query\n * @returns Promise that resolves when the refetch completes successfully\n * @throws Error if the refetch fails\n */\n clearError: () => Promise<void>\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Explicit type (highest priority)\n * 2. Schema inference (second priority)\n * 3. QueryFn return type inference (third priority)\n * 4. Fallback to Record<string, unknown>\n *\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type (highest priority)\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference (second priority)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>,\n>(\n config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>\n): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {\n utils: QueryCollectionUtils<\n ResolveType<TExplicit, TSchema, TQueryFn>,\n TKey,\n TInsertInput,\n TError\n >\n} {\n type TItem = ResolveType<TExplicit, TSchema, TQueryFn>\n\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** The last error encountered by the query */\n let lastError: TError | undefined\n /** The number of consecutive sync failures */\n let errorCount = 0\n /** The timestamp for when the query most recently returned the status as \"error\" */\n let lastErrorUpdatedAt = 0\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleUpdate: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n lastError = undefined\n errorCount = 0\n\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== lastErrorUpdatedAt) {\n lastError = result.error\n errorCount++\n lastErrorUpdatedAt = result.errorUpdatedAt\n }\n\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n }\n\n const actualUnsubscribeFn = localObserver.subscribe(handleUpdate)\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleUpdate(localObserver.getCurrentResult())\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = (opts) => {\n return queryClient.refetchQueries(\n {\n queryKey: queryKey,\n },\n {\n throwOnError: opts?.throwOnError,\n }\n )\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n lastError: () => lastError,\n isError: () => !!lastError,\n errorCount: () => errorCount,\n clearError: () => {\n lastError = undefined\n errorCount = 0\n lastErrorUpdatedAt = 0\n return refetch({ throwOnError: true })\n },\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
1
+ {"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n BaseCollectionConfig,\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\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// Schema input type inference helper (matches electric.ts pattern)\ntype InferSchemaInput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferInput<T> extends object\n ? StandardSchemaV1.InferInput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration options for creating a Query Collection\n * @template T - The explicit type of items stored in the collection\n * @template TQueryFn - The queryFn type\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TSchema - The schema type for validation\n */\nexport interface QueryCollectionConfig<\n T extends object = object,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<T, TKey, TSchema> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? TQueryFn\n : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>\n\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`staleTime`]\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @template TError - The type of errors that can occur during queries\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n TError = unknown,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n /** Get the last error encountered by the query (if any); reset on success */\n lastError: () => TError | undefined\n /** Check if the collection is in an error state */\n isError: () => boolean\n /**\n * Get the number of consecutive sync failures.\n * Incremented only when query fails completely (not per retry attempt); reset on success.\n */\n errorCount: () => number\n /**\n * Clear the error state and trigger a refetch of the query\n * @returns Promise that resolves when the refetch completes successfully\n * @throws Error if the refetch fails\n */\n clearError: () => Promise<void>\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Schema inference (highest priority)\n * 2. QueryFn return type inference (second priority)\n *\n * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\n\n// Overload for when schema is provided\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n (\n context: QueryFunctionContext<any>\n ) => Promise<Array<InferSchemaOutput<T>>>,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided\nexport function queryCollectionOptions<\n T extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n T,\n (context: QueryFunctionContext<any>) => Promise<Array<T>>,\n TError,\n TQueryKey,\n TKey\n > & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, TKey> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\nexport function queryCollectionOptions(\n config: QueryCollectionConfig<Record<string, unknown>>\n): CollectionConfig & {\n utils: QueryCollectionUtils\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** The last error encountered by the query */\n let lastError: any\n /** The number of consecutive sync failures */\n let errorCount = 0\n /** The timestamp for when the query most recently returned the status as \"error\" */\n let lastErrorUpdatedAt = 0\n\n const internalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n >(queryClient, observerOptions)\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleUpdate: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n lastError = undefined\n errorCount = 0\n\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, any>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== lastErrorUpdatedAt) {\n lastError = result.error\n errorCount++\n lastErrorUpdatedAt = result.errorUpdatedAt\n }\n\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n }\n\n const actualUnsubscribeFn = localObserver.subscribe(handleUpdate)\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleUpdate(localObserver.getCurrentResult())\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = (opts) => {\n return queryClient.refetchQueries(\n {\n queryKey: queryKey,\n },\n {\n throwOnError: opts?.throwOnError,\n }\n )\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: any) => string | number\n begin: () => void\n write: (message: Omit<ChangeMessage<any>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: any) => string | number,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<any, string | number, any>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n lastError: () => lastError,\n isError: () => !!lastError,\n errorCount: () => errorCount,\n clearError: () => {\n lastError = undefined\n errorCount = 0\n lastErrorUpdatedAt = 0\n return refetch({ throwOnError: true })\n },\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
@@ -1,177 +1,31 @@
1
1
  import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
2
- import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
2
+ import { BaseCollectionConfig, CollectionConfig, UtilsRecord } from '@tanstack/db';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  export type { SyncOperation } from './manual-sync.cjs';
5
5
  type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
6
- type InferQueryFnOutput<TQueryFn> = TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<infer TItem>> ? TItem extends object ? TItem : Record<string, unknown> : Record<string, unknown>;
7
- type ResolveType<TExplicit extends object | unknown = unknown, TSchema extends StandardSchemaV1 = never, TQueryFn = unknown> = unknown extends TExplicit ? [TSchema] extends [never] ? InferQueryFnOutput<TQueryFn> : InferSchemaOutput<TSchema> : TExplicit;
6
+ type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown>;
8
7
  /**
9
8
  * Configuration options for creating a Query Collection
10
- * @template TExplicit - The explicit type of items stored in the collection (highest priority)
11
- * @template TSchema - The schema type for validation and type inference (second priority)
12
- * @template TQueryFn - The queryFn type for inferring return type (third priority)
9
+ * @template T - The explicit type of items stored in the collection
10
+ * @template TQueryFn - The queryFn type
13
11
  * @template TError - The type of errors that can occur during queries
14
12
  * @template TQueryKey - The type of the query key
13
+ * @template TKey - The type of the item keys
14
+ * @template TSchema - The schema type for validation
15
15
  */
16
- export interface QueryCollectionConfig<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
16
+ export interface QueryCollectionConfig<T extends object = object, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, TKey, TSchema> {
17
17
  /** The query key used by TanStack Query to identify this query */
18
18
  queryKey: TQueryKey;
19
19
  /** Function that fetches data from the server. Must return the complete collection state */
20
- queryFn: TQueryFn extends (context: QueryFunctionContext<TQueryKey>) => Promise<Array<any>> ? TQueryFn : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>;
20
+ queryFn: TQueryFn extends (context: QueryFunctionContext<TQueryKey>) => Promise<Array<any>> ? TQueryFn : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>;
21
21
  /** The TanStack Query client instance */
22
22
  queryClient: QueryClient;
23
23
  /** Whether the query should automatically run (default: true) */
24
24
  enabled?: boolean;
25
- refetchInterval?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`refetchInterval`];
26
- retry?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retry`];
27
- retryDelay?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retryDelay`];
28
- staleTime?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`staleTime`];
29
- /** Unique identifier for the collection */
30
- id?: string;
31
- /** Function to extract the unique key from an item */
32
- getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`];
33
- /** Schema for validating items */
34
- schema?: TSchema;
35
- sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`];
36
- startSync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`startSync`];
37
- /**
38
- * Optional asynchronous handler function called before an insert operation
39
- * @param params Object containing transaction and collection information
40
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
41
- * @example
42
- * // Basic query collection insert handler
43
- * onInsert: async ({ transaction }) => {
44
- * const newItem = transaction.mutations[0].modified
45
- * await api.createTodo(newItem)
46
- * // Automatically refetches query after insert
47
- * }
48
- *
49
- * @example
50
- * // Insert handler with refetch control
51
- * onInsert: async ({ transaction }) => {
52
- * const newItem = transaction.mutations[0].modified
53
- * await api.createTodo(newItem)
54
- * return { refetch: false } // Skip automatic refetch
55
- * }
56
- *
57
- * @example
58
- * // Insert handler with multiple items
59
- * onInsert: async ({ transaction }) => {
60
- * const items = transaction.mutations.map(m => m.modified)
61
- * await api.createTodos(items)
62
- * // Will refetch query to get updated data
63
- * }
64
- *
65
- * @example
66
- * // Insert handler with error handling
67
- * onInsert: async ({ transaction }) => {
68
- * try {
69
- * const newItem = transaction.mutations[0].modified
70
- * await api.createTodo(newItem)
71
- * } catch (error) {
72
- * console.error('Insert failed:', error)
73
- * throw error // Transaction will rollback optimistic changes
74
- * }
75
- * }
76
- */
77
- onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
78
- /**
79
- * Optional asynchronous handler function called before an update operation
80
- * @param params Object containing transaction and collection information
81
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
82
- * @example
83
- * // Basic query collection update handler
84
- * onUpdate: async ({ transaction }) => {
85
- * const mutation = transaction.mutations[0]
86
- * await api.updateTodo(mutation.original.id, mutation.changes)
87
- * // Automatically refetches query after update
88
- * }
89
- *
90
- * @example
91
- * // Update handler with multiple items
92
- * onUpdate: async ({ transaction }) => {
93
- * const updates = transaction.mutations.map(m => ({
94
- * id: m.key,
95
- * changes: m.changes
96
- * }))
97
- * await api.updateTodos(updates)
98
- * // Will refetch query to get updated data
99
- * }
100
- *
101
- * @example
102
- * // Update handler with manual refetch
103
- * onUpdate: async ({ transaction, collection }) => {
104
- * const mutation = transaction.mutations[0]
105
- * await api.updateTodo(mutation.original.id, mutation.changes)
106
- *
107
- * // Manually trigger refetch
108
- * await collection.utils.refetch()
109
- *
110
- * return { refetch: false } // Skip automatic refetch
111
- * }
112
- *
113
- * @example
114
- * // Update handler with related collection refetch
115
- * onUpdate: async ({ transaction, collection }) => {
116
- * const mutation = transaction.mutations[0]
117
- * await api.updateTodo(mutation.original.id, mutation.changes)
118
- *
119
- * // Refetch related collections when this item changes
120
- * await Promise.all([
121
- * collection.utils.refetch(), // Refetch this collection
122
- * usersCollection.utils.refetch(), // Refetch users
123
- * tagsCollection.utils.refetch() // Refetch tags
124
- * ])
125
- *
126
- * return { refetch: false } // Skip automatic refetch since we handled it manually
127
- * }
128
- */
129
- onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
130
- /**
131
- * Optional asynchronous handler function called before a delete operation
132
- * @param params Object containing transaction and collection information
133
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
134
- * @example
135
- * // Basic query collection delete handler
136
- * onDelete: async ({ transaction }) => {
137
- * const mutation = transaction.mutations[0]
138
- * await api.deleteTodo(mutation.original.id)
139
- * // Automatically refetches query after delete
140
- * }
141
- *
142
- * @example
143
- * // Delete handler with refetch control
144
- * onDelete: async ({ transaction }) => {
145
- * const mutation = transaction.mutations[0]
146
- * await api.deleteTodo(mutation.original.id)
147
- * return { refetch: false } // Skip automatic refetch
148
- * }
149
- *
150
- * @example
151
- * // Delete handler with multiple items
152
- * onDelete: async ({ transaction }) => {
153
- * const keysToDelete = transaction.mutations.map(m => m.key)
154
- * await api.deleteTodos(keysToDelete)
155
- * // Will refetch query to get updated data
156
- * }
157
- *
158
- * @example
159
- * // Delete handler with related collection refetch
160
- * onDelete: async ({ transaction, collection }) => {
161
- * const mutation = transaction.mutations[0]
162
- * await api.deleteTodo(mutation.original.id)
163
- *
164
- * // Refetch related collections when this item is deleted
165
- * await Promise.all([
166
- * collection.utils.refetch(), // Refetch this collection
167
- * usersCollection.utils.refetch(), // Refetch users
168
- * projectsCollection.utils.refetch() // Refetch projects
169
- * ])
170
- *
171
- * return { refetch: false } // Skip automatic refetch since we handled it manually
172
- * }
173
- */
174
- onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
25
+ refetchInterval?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`refetchInterval`];
26
+ retry?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`retry`];
27
+ retryDelay?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`retryDelay`];
28
+ staleTime?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`staleTime`];
175
29
  /**
176
30
  * Metadata to pass to the query.
177
31
  * Available in queryFn via context.meta
@@ -242,18 +96,13 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
242
96
  * This integrates TanStack Query with TanStack DB for automatic synchronization.
243
97
  *
244
98
  * Supports automatic type inference following the priority order:
245
- * 1. Explicit type (highest priority)
246
- * 2. Schema inference (second priority)
247
- * 3. QueryFn return type inference (third priority)
248
- * 4. Fallback to Record<string, unknown>
99
+ * 1. Schema inference (highest priority)
100
+ * 2. QueryFn return type inference (second priority)
249
101
  *
250
- * @template TExplicit - The explicit type of items in the collection (highest priority)
251
- * @template TSchema - The schema type for validation and type inference (second priority)
252
- * @template TQueryFn - The queryFn type for inferring return type (third priority)
102
+ * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn
253
103
  * @template TError - The type of errors that can occur during queries
254
104
  * @template TQueryKey - The type of the query key
255
105
  * @template TKey - The type of the item keys
256
- * @template TInsertInput - The type accepted for insert operations
257
106
  * @param config - Configuration options for the Query collection
258
107
  * @returns Collection options with utilities for direct writes and manual operations
259
108
  *
@@ -272,7 +121,7 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
272
121
  * )
273
122
  *
274
123
  * @example
275
- * // Explicit type (highest priority)
124
+ * // Explicit type
276
125
  * const todosCollection = createCollection<Todo>(
277
126
  * queryCollectionOptions({
278
127
  * queryKey: ['todos'],
@@ -283,7 +132,7 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
283
132
  * )
284
133
  *
285
134
  * @example
286
- * // Schema inference (second priority)
135
+ * // Schema inference
287
136
  * const todosCollection = createCollection(
288
137
  * queryCollectionOptions({
289
138
  * queryKey: ['todos'],
@@ -314,6 +163,15 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
314
163
  * })
315
164
  * )
316
165
  */
317
- export declare function queryCollectionOptions<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>>(config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {
318
- utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput, TError>;
166
+ export declare function queryCollectionOptions<T extends StandardSchemaV1, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<InferSchemaOutput<T>, (context: QueryFunctionContext<any>) => Promise<Array<InferSchemaOutput<T>>>, TError, TQueryKey, TKey, T> & {
167
+ schema: T;
168
+ }): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
169
+ schema: T;
170
+ utils: QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>;
171
+ };
172
+ export declare function queryCollectionOptions<T extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<T, (context: QueryFunctionContext<any>) => Promise<Array<T>>, TError, TQueryKey, TKey> & {
173
+ schema?: never;
174
+ }): CollectionConfig<T, TKey> & {
175
+ schema?: never;
176
+ utils: QueryCollectionUtils<T, TKey, T, TError>;
319
177
  };
@@ -1,177 +1,31 @@
1
1
  import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
2
- import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
2
+ import { BaseCollectionConfig, CollectionConfig, UtilsRecord } from '@tanstack/db';
3
3
  import { StandardSchemaV1 } from '@standard-schema/spec';
4
4
  export type { SyncOperation } from './manual-sync.js';
5
5
  type InferSchemaOutput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferOutput<T> extends object ? StandardSchemaV1.InferOutput<T> : Record<string, unknown> : Record<string, unknown>;
6
- type InferQueryFnOutput<TQueryFn> = TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<infer TItem>> ? TItem extends object ? TItem : Record<string, unknown> : Record<string, unknown>;
7
- type ResolveType<TExplicit extends object | unknown = unknown, TSchema extends StandardSchemaV1 = never, TQueryFn = unknown> = unknown extends TExplicit ? [TSchema] extends [never] ? InferQueryFnOutput<TQueryFn> : InferSchemaOutput<TSchema> : TExplicit;
6
+ type InferSchemaInput<T> = T extends StandardSchemaV1 ? StandardSchemaV1.InferInput<T> extends object ? StandardSchemaV1.InferInput<T> : Record<string, unknown> : Record<string, unknown>;
8
7
  /**
9
8
  * Configuration options for creating a Query Collection
10
- * @template TExplicit - The explicit type of items stored in the collection (highest priority)
11
- * @template TSchema - The schema type for validation and type inference (second priority)
12
- * @template TQueryFn - The queryFn type for inferring return type (third priority)
9
+ * @template T - The explicit type of items stored in the collection
10
+ * @template TQueryFn - The queryFn type
13
11
  * @template TError - The type of errors that can occur during queries
14
12
  * @template TQueryKey - The type of the query key
13
+ * @template TKey - The type of the item keys
14
+ * @template TSchema - The schema type for validation
15
15
  */
16
- export interface QueryCollectionConfig<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
16
+ export interface QueryCollectionConfig<T extends object = object, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TSchema extends StandardSchemaV1 = never> extends BaseCollectionConfig<T, TKey, TSchema> {
17
17
  /** The query key used by TanStack Query to identify this query */
18
18
  queryKey: TQueryKey;
19
19
  /** Function that fetches data from the server. Must return the complete collection state */
20
- queryFn: TQueryFn extends (context: QueryFunctionContext<TQueryKey>) => Promise<Array<any>> ? TQueryFn : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>;
20
+ queryFn: TQueryFn extends (context: QueryFunctionContext<TQueryKey>) => Promise<Array<any>> ? TQueryFn : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>;
21
21
  /** The TanStack Query client instance */
22
22
  queryClient: QueryClient;
23
23
  /** Whether the query should automatically run (default: true) */
24
24
  enabled?: boolean;
25
- refetchInterval?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`refetchInterval`];
26
- retry?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retry`];
27
- retryDelay?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`retryDelay`];
28
- staleTime?: QueryObserverOptions<Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TError, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, Array<ResolveType<TExplicit, TSchema, TQueryFn>>, TQueryKey>[`staleTime`];
29
- /** Unique identifier for the collection */
30
- id?: string;
31
- /** Function to extract the unique key from an item */
32
- getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`];
33
- /** Schema for validating items */
34
- schema?: TSchema;
35
- sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`];
36
- startSync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`startSync`];
37
- /**
38
- * Optional asynchronous handler function called before an insert operation
39
- * @param params Object containing transaction and collection information
40
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
41
- * @example
42
- * // Basic query collection insert handler
43
- * onInsert: async ({ transaction }) => {
44
- * const newItem = transaction.mutations[0].modified
45
- * await api.createTodo(newItem)
46
- * // Automatically refetches query after insert
47
- * }
48
- *
49
- * @example
50
- * // Insert handler with refetch control
51
- * onInsert: async ({ transaction }) => {
52
- * const newItem = transaction.mutations[0].modified
53
- * await api.createTodo(newItem)
54
- * return { refetch: false } // Skip automatic refetch
55
- * }
56
- *
57
- * @example
58
- * // Insert handler with multiple items
59
- * onInsert: async ({ transaction }) => {
60
- * const items = transaction.mutations.map(m => m.modified)
61
- * await api.createTodos(items)
62
- * // Will refetch query to get updated data
63
- * }
64
- *
65
- * @example
66
- * // Insert handler with error handling
67
- * onInsert: async ({ transaction }) => {
68
- * try {
69
- * const newItem = transaction.mutations[0].modified
70
- * await api.createTodo(newItem)
71
- * } catch (error) {
72
- * console.error('Insert failed:', error)
73
- * throw error // Transaction will rollback optimistic changes
74
- * }
75
- * }
76
- */
77
- onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
78
- /**
79
- * Optional asynchronous handler function called before an update operation
80
- * @param params Object containing transaction and collection information
81
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
82
- * @example
83
- * // Basic query collection update handler
84
- * onUpdate: async ({ transaction }) => {
85
- * const mutation = transaction.mutations[0]
86
- * await api.updateTodo(mutation.original.id, mutation.changes)
87
- * // Automatically refetches query after update
88
- * }
89
- *
90
- * @example
91
- * // Update handler with multiple items
92
- * onUpdate: async ({ transaction }) => {
93
- * const updates = transaction.mutations.map(m => ({
94
- * id: m.key,
95
- * changes: m.changes
96
- * }))
97
- * await api.updateTodos(updates)
98
- * // Will refetch query to get updated data
99
- * }
100
- *
101
- * @example
102
- * // Update handler with manual refetch
103
- * onUpdate: async ({ transaction, collection }) => {
104
- * const mutation = transaction.mutations[0]
105
- * await api.updateTodo(mutation.original.id, mutation.changes)
106
- *
107
- * // Manually trigger refetch
108
- * await collection.utils.refetch()
109
- *
110
- * return { refetch: false } // Skip automatic refetch
111
- * }
112
- *
113
- * @example
114
- * // Update handler with related collection refetch
115
- * onUpdate: async ({ transaction, collection }) => {
116
- * const mutation = transaction.mutations[0]
117
- * await api.updateTodo(mutation.original.id, mutation.changes)
118
- *
119
- * // Refetch related collections when this item changes
120
- * await Promise.all([
121
- * collection.utils.refetch(), // Refetch this collection
122
- * usersCollection.utils.refetch(), // Refetch users
123
- * tagsCollection.utils.refetch() // Refetch tags
124
- * ])
125
- *
126
- * return { refetch: false } // Skip automatic refetch since we handled it manually
127
- * }
128
- */
129
- onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
130
- /**
131
- * Optional asynchronous handler function called before a delete operation
132
- * @param params Object containing transaction and collection information
133
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
134
- * @example
135
- * // Basic query collection delete handler
136
- * onDelete: async ({ transaction }) => {
137
- * const mutation = transaction.mutations[0]
138
- * await api.deleteTodo(mutation.original.id)
139
- * // Automatically refetches query after delete
140
- * }
141
- *
142
- * @example
143
- * // Delete handler with refetch control
144
- * onDelete: async ({ transaction }) => {
145
- * const mutation = transaction.mutations[0]
146
- * await api.deleteTodo(mutation.original.id)
147
- * return { refetch: false } // Skip automatic refetch
148
- * }
149
- *
150
- * @example
151
- * // Delete handler with multiple items
152
- * onDelete: async ({ transaction }) => {
153
- * const keysToDelete = transaction.mutations.map(m => m.key)
154
- * await api.deleteTodos(keysToDelete)
155
- * // Will refetch query to get updated data
156
- * }
157
- *
158
- * @example
159
- * // Delete handler with related collection refetch
160
- * onDelete: async ({ transaction, collection }) => {
161
- * const mutation = transaction.mutations[0]
162
- * await api.deleteTodo(mutation.original.id)
163
- *
164
- * // Refetch related collections when this item is deleted
165
- * await Promise.all([
166
- * collection.utils.refetch(), // Refetch this collection
167
- * usersCollection.utils.refetch(), // Refetch users
168
- * projectsCollection.utils.refetch() // Refetch projects
169
- * ])
170
- *
171
- * return { refetch: false } // Skip automatic refetch since we handled it manually
172
- * }
173
- */
174
- onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>;
25
+ refetchInterval?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`refetchInterval`];
26
+ retry?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`retry`];
27
+ retryDelay?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`retryDelay`];
28
+ staleTime?: QueryObserverOptions<Array<T>, TError, Array<T>, Array<T>, TQueryKey>[`staleTime`];
175
29
  /**
176
30
  * Metadata to pass to the query.
177
31
  * Available in queryFn via context.meta
@@ -242,18 +96,13 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
242
96
  * This integrates TanStack Query with TanStack DB for automatic synchronization.
243
97
  *
244
98
  * Supports automatic type inference following the priority order:
245
- * 1. Explicit type (highest priority)
246
- * 2. Schema inference (second priority)
247
- * 3. QueryFn return type inference (third priority)
248
- * 4. Fallback to Record<string, unknown>
99
+ * 1. Schema inference (highest priority)
100
+ * 2. QueryFn return type inference (second priority)
249
101
  *
250
- * @template TExplicit - The explicit type of items in the collection (highest priority)
251
- * @template TSchema - The schema type for validation and type inference (second priority)
252
- * @template TQueryFn - The queryFn type for inferring return type (third priority)
102
+ * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn
253
103
  * @template TError - The type of errors that can occur during queries
254
104
  * @template TQueryKey - The type of the query key
255
105
  * @template TKey - The type of the item keys
256
- * @template TInsertInput - The type accepted for insert operations
257
106
  * @param config - Configuration options for the Query collection
258
107
  * @returns Collection options with utilities for direct writes and manual operations
259
108
  *
@@ -272,7 +121,7 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
272
121
  * )
273
122
  *
274
123
  * @example
275
- * // Explicit type (highest priority)
124
+ * // Explicit type
276
125
  * const todosCollection = createCollection<Todo>(
277
126
  * queryCollectionOptions({
278
127
  * queryKey: ['todos'],
@@ -283,7 +132,7 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
283
132
  * )
284
133
  *
285
134
  * @example
286
- * // Schema inference (second priority)
135
+ * // Schema inference
287
136
  * const todosCollection = createCollection(
288
137
  * queryCollectionOptions({
289
138
  * queryKey: ['todos'],
@@ -314,6 +163,15 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
314
163
  * })
315
164
  * )
316
165
  */
317
- export declare function queryCollectionOptions<TExplicit extends object = object, TSchema extends StandardSchemaV1 = never, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<Array<any>> = (context: QueryFunctionContext<any>) => Promise<Array<any>>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>>(config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {
318
- utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput, TError>;
166
+ export declare function queryCollectionOptions<T extends StandardSchemaV1, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<InferSchemaOutput<T>, (context: QueryFunctionContext<any>) => Promise<Array<InferSchemaOutput<T>>>, TError, TQueryKey, TKey, T> & {
167
+ schema: T;
168
+ }): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
169
+ schema: T;
170
+ utils: QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>;
171
+ };
172
+ export declare function queryCollectionOptions<T extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<T, (context: QueryFunctionContext<any>) => Promise<Array<T>>, TError, TQueryKey, TKey> & {
173
+ schema?: never;
174
+ }): CollectionConfig<T, TKey> & {
175
+ schema?: never;
176
+ utils: QueryCollectionUtils<T, TKey, T, TError>;
319
177
  };
@@ -1 +1 @@
1
- {"version":3,"file":"query.js","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\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// QueryFn return type inference helper\ntype InferQueryFnOutput<TQueryFn> = TQueryFn extends (\n context: QueryFunctionContext<any>\n) => Promise<Array<infer TItem>>\n ? TItem extends object\n ? TItem\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Type resolution system with priority order (matches electric.ts pattern)\ntype ResolveType<\n TExplicit extends object | unknown = unknown,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn = unknown,\n> = unknown extends TExplicit\n ? [TSchema] extends [never]\n ? InferQueryFnOutput<TQueryFn>\n : InferSchemaOutput<TSchema>\n : TExplicit\n\n/**\n * Configuration options for creating a Query Collection\n * @template TExplicit - The explicit type of items stored in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? TQueryFn\n : (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>\n\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TError,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n Array<ResolveType<TExplicit, TSchema, TQueryFn>>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`]\n /** Schema for validating items */\n schema?: TSchema\n sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`]\n startSync?: CollectionConfig<\n ResolveType<TExplicit, TSchema, TQueryFn>\n >[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @template TError - The type of errors that can occur during queries\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n TError = unknown,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n /** Get the last error encountered by the query (if any); reset on success */\n lastError: () => TError | undefined\n /** Check if the collection is in an error state */\n isError: () => boolean\n /**\n * Get the number of consecutive sync failures.\n * Incremented only when query fails completely (not per retry attempt); reset on success.\n */\n errorCount: () => number\n /**\n * Clear the error state and trigger a refetch of the query\n * @returns Promise that resolves when the refetch completes successfully\n * @throws Error if the refetch fails\n */\n clearError: () => Promise<void>\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Explicit type (highest priority)\n * 2. Schema inference (second priority)\n * 3. QueryFn return type inference (third priority)\n * 4. Fallback to Record<string, unknown>\n *\n * @template TExplicit - The explicit type of items in the collection (highest priority)\n * @template TSchema - The schema type for validation and type inference (second priority)\n * @template TQueryFn - The queryFn type for inferring return type (third priority)\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type (highest priority)\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference (second priority)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TExplicit extends object = object,\n TSchema extends StandardSchemaV1 = never,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>,\n>(\n config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>\n): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {\n utils: QueryCollectionUtils<\n ResolveType<TExplicit, TSchema, TQueryFn>,\n TKey,\n TInsertInput,\n TError\n >\n} {\n type TItem = ResolveType<TExplicit, TSchema, TQueryFn>\n\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** The last error encountered by the query */\n let lastError: TError | undefined\n /** The number of consecutive sync failures */\n let errorCount = 0\n /** The timestamp for when the query most recently returned the status as \"error\" */\n let lastErrorUpdatedAt = 0\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleUpdate: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n lastError = undefined\n errorCount = 0\n\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== lastErrorUpdatedAt) {\n lastError = result.error\n errorCount++\n lastErrorUpdatedAt = result.errorUpdatedAt\n }\n\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n }\n\n const actualUnsubscribeFn = localObserver.subscribe(handleUpdate)\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleUpdate(localObserver.getCurrentResult())\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = (opts) => {\n return queryClient.refetchQueries(\n {\n queryKey: queryKey,\n },\n {\n throwOnError: opts?.throwOnError,\n }\n )\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n lastError: () => lastError,\n isError: () => !!lastError,\n errorCount: () => errorCount,\n clearError: () => {\n lastError = undefined\n errorCount = 0\n lastErrorUpdatedAt = 0\n return refetch({ throwOnError: true })\n },\n },\n }\n}\n"],"names":[],"mappings":";;;AAyaO,SAAS,uBAad,QAQA;AAGA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,IACD;AAKJ,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,sBAAA;AAAA,EACZ;AAEA,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,qBAAA;AAAA,EACZ;AAGA,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,yBAAA;AAAA,EACZ;AAEA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,oBAAA;AAAA,EACZ;AAGA,MAAI;AAEJ,MAAI,aAAa;AAEjB,MAAI,qBAAqB;AAEzB,QAAM,eAA0C,CAAC,WAAW;AAC1D,UAAM,EAAE,OAAO,OAAO,QAAQ,WAAW,eAAe;AAExD,UAAM,kBAMF;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,qBAAqB;AAAA,IAAA;AAGvB,UAAM,gBAAgB,IAAI,cAMxB,aAAa,eAAe;AAG9B,UAAM,eAA8B,CAAC,WAAW;AAC9C,UAAI,OAAO,WAAW;AAEpB,oBAAY;AACZ,qBAAa;AAEb,cAAM,gBAAgB,OAAO;AAE7B,YACE,CAAC,MAAM,QAAQ,aAAa,KAC5B,cAAc,KAAK,CAAC,SAAS,OAAO,SAAS,QAAQ,GACrD;AACA,kBAAQ;AAAA,YACN;AAAA,YACA;AAAA,UAAA;AAEF;AAAA,QACF;AAEA,cAAM,qBAAqB,IAAI,IAAI,WAAW,UAAU;AACxD,cAAM,kCAAkB,IAAA;AACxB,sBAAc,QAAQ,CAAC,SAAS;AAC9B,gBAAM,MAAM,OAAO,IAAI;AACvB,sBAAY,IAAI,KAAK,IAAI;AAAA,QAC3B,CAAC;AAED,cAAA;AAGA,cAAM,eAAe,CACnB,MACA,SACY;AAEZ,gBAAM,QAAQ,OAAO,KAAK,IAAI;AAC9B,gBAAM,QAAQ,OAAO,KAAK,IAAI;AAG9B,cAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAG1C,iBAAO,MAAM,MAAM,CAAC,QAAQ;AAE1B,gBAAI,OAAO,KAAK,GAAG,MAAM,WAAY,QAAO;AAC5C,mBAAO,KAAK,GAAG,MAAM,KAAK,GAAG;AAAA,UAC/B,CAAC;AAAA,QACH;AAEA,2BAAmB,QAAQ,CAAC,SAAS,QAAQ;AAC3C,gBAAM,UAAU,YAAY,IAAI,GAAG;AACnC,cAAI,CAAC,SAAS;AACZ,kBAAM,EAAE,MAAM,UAAU,OAAO,SAAS;AAAA,UAC1C,WACE,CAAC;AAAA,YACC;AAAA,YACA;AAAA,UAAA,GAEF;AAEA,kBAAM,EAAE,MAAM,UAAU,OAAO,SAAS;AAAA,UAC1C;AAAA,QACF,CAAC;AAED,oBAAY,QAAQ,CAAC,SAAS,QAAQ;AACpC,cAAI,CAAC,mBAAmB,IAAI,GAAG,GAAG;AAChC,kBAAM,EAAE,MAAM,UAAU,OAAO,SAAS;AAAA,UAC1C;AAAA,QACF,CAAC;AAED,eAAA;AAGA,kBAAA;AAAA,MACF,WAAW,OAAO,SAAS;AACzB,YAAI,OAAO,mBAAmB,oBAAoB;AAChD,sBAAY,OAAO;AACnB;AACA,+BAAqB,OAAO;AAAA,QAC9B;AAEA,gBAAQ;AAAA,UACN,2CAA2C,OAAO,QAAQ,CAAC;AAAA,UAC3D,OAAO;AAAA,QAAA;AAIT,kBAAA;AAAA,MACF;AAAA,IACF;AAEA,UAAM,sBAAsB,cAAc,UAAU,YAAY;AAIhE,iBAAa,cAAc,kBAAkB;AAE7C,WAAO,YAAY;AACjB,0BAAA;AACA,YAAM,YAAY,cAAc,EAAE,UAAU;AAC5C,kBAAY,cAAc,EAAE,UAAU;AAAA,IACxC;AAAA,EACF;AAMA,QAAM,UAAqB,CAAC,SAAS;AACnC,WAAO,YAAY;AAAA,MACjB;AAAA,QACE;AAAA,MAAA;AAAA,MAEF;AAAA,QACE,cAAc,6BAAM;AAAA,MAAA;AAAA,IACtB;AAAA,EAEJ;AAGA,MAAI,eAQO;AAGX,QAAM,uBAAkD,CAAC,WAAW;AAClE,UAAM,EAAE,OAAO,OAAO,QAAQ,eAAe;AAG7C,mBAAe;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAIF,WAAO,aAAa,MAAM;AAAA,EAC5B;AAGA,QAAM,aAAa;AAAA,IACjB,MAAM;AAAA,EAAA;AAIR,QAAM,kBAAkB,WACpB,OAAO,WAA0C;AAC/C,UAAM,gBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAClD,UAAM,gBACH,cAAwC,YAAY;AAEvD,QAAI,eAAe;AACjB,YAAM,QAAA;AAAA,IACR;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,WACpB,OAAO,WAA0C;AAC/C,UAAM,gBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAClD,UAAM,gBACH,cAAwC,YAAY;AAEvD,QAAI,eAAe;AACjB,YAAM,QAAA;AAAA,IACR;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,WACpB,OAAO,WAA0C;AAC/C,UAAM,gBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAClD,UAAM,gBACH,cAAwC,YAAY;AAEvD,QAAI,eAAe;AACjB,YAAM,QAAA;AAAA,IACR;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,MAAM,EAAE,MAAM,qBAAA;AAAA,IACd,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,OAAO;AAAA,MACL;AAAA,MACA,GAAG;AAAA,MACH,WAAW,MAAM;AAAA,MACjB,SAAS,MAAM,CAAC,CAAC;AAAA,MACjB,YAAY,MAAM;AAAA,MAClB,YAAY,MAAM;AAChB,oBAAY;AACZ,qBAAa;AACb,6BAAqB;AACrB,eAAO,QAAQ,EAAE,cAAc,MAAM;AAAA,MACvC;AAAA,IAAA;AAAA,EACF;AAEJ;"}
1
+ {"version":3,"file":"query.js","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n BaseCollectionConfig,\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\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// Schema input type inference helper (matches electric.ts pattern)\ntype InferSchemaInput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferInput<T> extends object\n ? StandardSchemaV1.InferInput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration options for creating a Query Collection\n * @template T - The explicit type of items stored in the collection\n * @template TQueryFn - The queryFn type\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TSchema - The schema type for validation\n */\nexport interface QueryCollectionConfig<\n T extends object = object,\n TQueryFn extends (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>> = (\n context: QueryFunctionContext<any>\n ) => Promise<Array<any>>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = never,\n> extends BaseCollectionConfig<T, TKey, TSchema> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? TQueryFn\n : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>\n\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`staleTime`]\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @template TError - The type of errors that can occur during queries\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n TError = unknown,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n /** Get the last error encountered by the query (if any); reset on success */\n lastError: () => TError | undefined\n /** Check if the collection is in an error state */\n isError: () => boolean\n /**\n * Get the number of consecutive sync failures.\n * Incremented only when query fails completely (not per retry attempt); reset on success.\n */\n errorCount: () => number\n /**\n * Clear the error state and trigger a refetch of the query\n * @returns Promise that resolves when the refetch completes successfully\n * @throws Error if the refetch fails\n */\n clearError: () => Promise<void>\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Schema inference (highest priority)\n * 2. QueryFn return type inference (second priority)\n *\n * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\n\n// Overload for when schema is provided\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n (\n context: QueryFunctionContext<any>\n ) => Promise<Array<InferSchemaOutput<T>>>,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided\nexport function queryCollectionOptions<\n T extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n T,\n (context: QueryFunctionContext<any>) => Promise<Array<T>>,\n TError,\n TQueryKey,\n TKey\n > & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, TKey> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\nexport function queryCollectionOptions(\n config: QueryCollectionConfig<Record<string, unknown>>\n): CollectionConfig & {\n utils: QueryCollectionUtils\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** The last error encountered by the query */\n let lastError: any\n /** The number of consecutive sync failures */\n let errorCount = 0\n /** The timestamp for when the query most recently returned the status as \"error\" */\n let lastErrorUpdatedAt = 0\n\n const internalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n >(queryClient, observerOptions)\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleUpdate: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n lastError = undefined\n errorCount = 0\n\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, any>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== lastErrorUpdatedAt) {\n lastError = result.error\n errorCount++\n lastErrorUpdatedAt = result.errorUpdatedAt\n }\n\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n }\n\n const actualUnsubscribeFn = localObserver.subscribe(handleUpdate)\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleUpdate(localObserver.getCurrentResult())\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = (opts) => {\n return queryClient.refetchQueries(\n {\n queryKey: queryKey,\n },\n {\n throwOnError: opts?.throwOnError,\n }\n )\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: any) => string | number\n begin: () => void\n write: (message: Omit<ChangeMessage<any>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: any) => string | number,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<any, string | number, any>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n lastError: () => lastError,\n isError: () => !!lastError,\n errorCount: () => errorCount,\n clearError: () => {\n lastError = undefined\n errorCount = 0\n lastErrorUpdatedAt = 0\n return refetch({ throwOnError: true })\n },\n },\n }\n}\n"],"names":[],"mappings":"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"}
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "@tanstack/query-db-collection",
3
3
  "description": "TanStack Query collection for TanStack DB",
4
- "version": "0.2.16",
4
+ "version": "0.2.17",
5
5
  "dependencies": {
6
6
  "@standard-schema/spec": "^1.0.0",
7
- "@tanstack/db": "0.2.4"
7
+ "@tanstack/db": "0.2.5"
8
8
  },
9
9
  "devDependencies": {
10
10
  "@tanstack/query-core": "^5.87.4",
package/src/query.ts CHANGED
@@ -13,14 +13,12 @@ import type {
13
13
  QueryObserverOptions,
14
14
  } from "@tanstack/query-core"
15
15
  import type {
16
+ BaseCollectionConfig,
16
17
  ChangeMessage,
17
18
  CollectionConfig,
18
- DeleteMutationFn,
19
19
  DeleteMutationFnParams,
20
- InsertMutationFn,
21
20
  InsertMutationFnParams,
22
21
  SyncConfig,
23
- UpdateMutationFn,
24
22
  UpdateMutationFnParams,
25
23
  UtilsRecord,
26
24
  } from "@tanstack/db"
@@ -36,37 +34,24 @@ type InferSchemaOutput<T> = T extends StandardSchemaV1
36
34
  : Record<string, unknown>
37
35
  : Record<string, unknown>
38
36
 
39
- // QueryFn return type inference helper
40
- type InferQueryFnOutput<TQueryFn> = TQueryFn extends (
41
- context: QueryFunctionContext<any>
42
- ) => Promise<Array<infer TItem>>
43
- ? TItem extends object
44
- ? TItem
37
+ // Schema input type inference helper (matches electric.ts pattern)
38
+ type InferSchemaInput<T> = T extends StandardSchemaV1
39
+ ? StandardSchemaV1.InferInput<T> extends object
40
+ ? StandardSchemaV1.InferInput<T>
45
41
  : Record<string, unknown>
46
42
  : Record<string, unknown>
47
43
 
48
- // Type resolution system with priority order (matches electric.ts pattern)
49
- type ResolveType<
50
- TExplicit extends object | unknown = unknown,
51
- TSchema extends StandardSchemaV1 = never,
52
- TQueryFn = unknown,
53
- > = unknown extends TExplicit
54
- ? [TSchema] extends [never]
55
- ? InferQueryFnOutput<TQueryFn>
56
- : InferSchemaOutput<TSchema>
57
- : TExplicit
58
-
59
44
  /**
60
45
  * Configuration options for creating a Query Collection
61
- * @template TExplicit - The explicit type of items stored in the collection (highest priority)
62
- * @template TSchema - The schema type for validation and type inference (second priority)
63
- * @template TQueryFn - The queryFn type for inferring return type (third priority)
46
+ * @template T - The explicit type of items stored in the collection
47
+ * @template TQueryFn - The queryFn type
64
48
  * @template TError - The type of errors that can occur during queries
65
49
  * @template TQueryKey - The type of the query key
50
+ * @template TKey - The type of the item keys
51
+ * @template TSchema - The schema type for validation
66
52
  */
67
53
  export interface QueryCollectionConfig<
68
- TExplicit extends object = object,
69
- TSchema extends StandardSchemaV1 = never,
54
+ T extends object = object,
70
55
  TQueryFn extends (
71
56
  context: QueryFunctionContext<any>
72
57
  ) => Promise<Array<any>> = (
@@ -74,7 +59,9 @@ export interface QueryCollectionConfig<
74
59
  ) => Promise<Array<any>>,
75
60
  TError = unknown,
76
61
  TQueryKey extends QueryKey = QueryKey,
77
- > {
62
+ TKey extends string | number = string | number,
63
+ TSchema extends StandardSchemaV1 = never,
64
+ > extends BaseCollectionConfig<T, TKey, TSchema> {
78
65
  /** The query key used by TanStack Query to identify this query */
79
66
  queryKey: TQueryKey
80
67
  /** Function that fetches data from the server. Must return the complete collection state */
@@ -82,9 +69,7 @@ export interface QueryCollectionConfig<
82
69
  context: QueryFunctionContext<TQueryKey>
83
70
  ) => Promise<Array<any>>
84
71
  ? TQueryFn
85
- : (
86
- context: QueryFunctionContext<TQueryKey>
87
- ) => Promise<Array<ResolveType<TExplicit, TSchema, TQueryFn>>>
72
+ : (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>
88
73
 
89
74
  /** The TanStack Query client instance */
90
75
  queryClient: QueryClient
@@ -93,188 +78,34 @@ export interface QueryCollectionConfig<
93
78
  /** Whether the query should automatically run (default: true) */
94
79
  enabled?: boolean
95
80
  refetchInterval?: QueryObserverOptions<
96
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
81
+ Array<T>,
97
82
  TError,
98
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
99
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
83
+ Array<T>,
84
+ Array<T>,
100
85
  TQueryKey
101
86
  >[`refetchInterval`]
102
87
  retry?: QueryObserverOptions<
103
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
88
+ Array<T>,
104
89
  TError,
105
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
106
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
90
+ Array<T>,
91
+ Array<T>,
107
92
  TQueryKey
108
93
  >[`retry`]
109
94
  retryDelay?: QueryObserverOptions<
110
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
95
+ Array<T>,
111
96
  TError,
112
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
113
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
97
+ Array<T>,
98
+ Array<T>,
114
99
  TQueryKey
115
100
  >[`retryDelay`]
116
101
  staleTime?: QueryObserverOptions<
117
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
102
+ Array<T>,
118
103
  TError,
119
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
120
- Array<ResolveType<TExplicit, TSchema, TQueryFn>>,
104
+ Array<T>,
105
+ Array<T>,
121
106
  TQueryKey
122
107
  >[`staleTime`]
123
108
 
124
- // Standard Collection configuration properties
125
- /** Unique identifier for the collection */
126
- id?: string
127
- /** Function to extract the unique key from an item */
128
- getKey: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`getKey`]
129
- /** Schema for validating items */
130
- schema?: TSchema
131
- sync?: CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>>[`sync`]
132
- startSync?: CollectionConfig<
133
- ResolveType<TExplicit, TSchema, TQueryFn>
134
- >[`startSync`]
135
-
136
- // Direct persistence handlers
137
- /**
138
- * Optional asynchronous handler function called before an insert operation
139
- * @param params Object containing transaction and collection information
140
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
141
- * @example
142
- * // Basic query collection insert handler
143
- * onInsert: async ({ transaction }) => {
144
- * const newItem = transaction.mutations[0].modified
145
- * await api.createTodo(newItem)
146
- * // Automatically refetches query after insert
147
- * }
148
- *
149
- * @example
150
- * // Insert handler with refetch control
151
- * onInsert: async ({ transaction }) => {
152
- * const newItem = transaction.mutations[0].modified
153
- * await api.createTodo(newItem)
154
- * return { refetch: false } // Skip automatic refetch
155
- * }
156
- *
157
- * @example
158
- * // Insert handler with multiple items
159
- * onInsert: async ({ transaction }) => {
160
- * const items = transaction.mutations.map(m => m.modified)
161
- * await api.createTodos(items)
162
- * // Will refetch query to get updated data
163
- * }
164
- *
165
- * @example
166
- * // Insert handler with error handling
167
- * onInsert: async ({ transaction }) => {
168
- * try {
169
- * const newItem = transaction.mutations[0].modified
170
- * await api.createTodo(newItem)
171
- * } catch (error) {
172
- * console.error('Insert failed:', error)
173
- * throw error // Transaction will rollback optimistic changes
174
- * }
175
- * }
176
- */
177
- onInsert?: InsertMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>
178
-
179
- /**
180
- * Optional asynchronous handler function called before an update operation
181
- * @param params Object containing transaction and collection information
182
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
183
- * @example
184
- * // Basic query collection update handler
185
- * onUpdate: async ({ transaction }) => {
186
- * const mutation = transaction.mutations[0]
187
- * await api.updateTodo(mutation.original.id, mutation.changes)
188
- * // Automatically refetches query after update
189
- * }
190
- *
191
- * @example
192
- * // Update handler with multiple items
193
- * onUpdate: async ({ transaction }) => {
194
- * const updates = transaction.mutations.map(m => ({
195
- * id: m.key,
196
- * changes: m.changes
197
- * }))
198
- * await api.updateTodos(updates)
199
- * // Will refetch query to get updated data
200
- * }
201
- *
202
- * @example
203
- * // Update handler with manual refetch
204
- * onUpdate: async ({ transaction, collection }) => {
205
- * const mutation = transaction.mutations[0]
206
- * await api.updateTodo(mutation.original.id, mutation.changes)
207
- *
208
- * // Manually trigger refetch
209
- * await collection.utils.refetch()
210
- *
211
- * return { refetch: false } // Skip automatic refetch
212
- * }
213
- *
214
- * @example
215
- * // Update handler with related collection refetch
216
- * onUpdate: async ({ transaction, collection }) => {
217
- * const mutation = transaction.mutations[0]
218
- * await api.updateTodo(mutation.original.id, mutation.changes)
219
- *
220
- * // Refetch related collections when this item changes
221
- * await Promise.all([
222
- * collection.utils.refetch(), // Refetch this collection
223
- * usersCollection.utils.refetch(), // Refetch users
224
- * tagsCollection.utils.refetch() // Refetch tags
225
- * ])
226
- *
227
- * return { refetch: false } // Skip automatic refetch since we handled it manually
228
- * }
229
- */
230
- onUpdate?: UpdateMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>
231
-
232
- /**
233
- * Optional asynchronous handler function called before a delete operation
234
- * @param params Object containing transaction and collection information
235
- * @returns Promise resolving to void or { refetch?: boolean } to control refetching
236
- * @example
237
- * // Basic query collection delete handler
238
- * onDelete: async ({ transaction }) => {
239
- * const mutation = transaction.mutations[0]
240
- * await api.deleteTodo(mutation.original.id)
241
- * // Automatically refetches query after delete
242
- * }
243
- *
244
- * @example
245
- * // Delete handler with refetch control
246
- * onDelete: async ({ transaction }) => {
247
- * const mutation = transaction.mutations[0]
248
- * await api.deleteTodo(mutation.original.id)
249
- * return { refetch: false } // Skip automatic refetch
250
- * }
251
- *
252
- * @example
253
- * // Delete handler with multiple items
254
- * onDelete: async ({ transaction }) => {
255
- * const keysToDelete = transaction.mutations.map(m => m.key)
256
- * await api.deleteTodos(keysToDelete)
257
- * // Will refetch query to get updated data
258
- * }
259
- *
260
- * @example
261
- * // Delete handler with related collection refetch
262
- * onDelete: async ({ transaction, collection }) => {
263
- * const mutation = transaction.mutations[0]
264
- * await api.deleteTodo(mutation.original.id)
265
- *
266
- * // Refetch related collections when this item is deleted
267
- * await Promise.all([
268
- * collection.utils.refetch(), // Refetch this collection
269
- * usersCollection.utils.refetch(), // Refetch users
270
- * projectsCollection.utils.refetch() // Refetch projects
271
- * ])
272
- *
273
- * return { refetch: false } // Skip automatic refetch since we handled it manually
274
- * }
275
- */
276
- onDelete?: DeleteMutationFn<ResolveType<TExplicit, TSchema, TQueryFn>>
277
-
278
109
  /**
279
110
  * Metadata to pass to the query.
280
111
  * Available in queryFn via context.meta
@@ -351,18 +182,13 @@ export interface QueryCollectionUtils<
351
182
  * This integrates TanStack Query with TanStack DB for automatic synchronization.
352
183
  *
353
184
  * Supports automatic type inference following the priority order:
354
- * 1. Explicit type (highest priority)
355
- * 2. Schema inference (second priority)
356
- * 3. QueryFn return type inference (third priority)
357
- * 4. Fallback to Record<string, unknown>
185
+ * 1. Schema inference (highest priority)
186
+ * 2. QueryFn return type inference (second priority)
358
187
  *
359
- * @template TExplicit - The explicit type of items in the collection (highest priority)
360
- * @template TSchema - The schema type for validation and type inference (second priority)
361
- * @template TQueryFn - The queryFn type for inferring return type (third priority)
188
+ * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn
362
189
  * @template TError - The type of errors that can occur during queries
363
190
  * @template TQueryKey - The type of the query key
364
191
  * @template TKey - The type of the item keys
365
- * @template TInsertInput - The type accepted for insert operations
366
192
  * @param config - Configuration options for the Query collection
367
193
  * @returns Collection options with utilities for direct writes and manual operations
368
194
  *
@@ -381,7 +207,7 @@ export interface QueryCollectionUtils<
381
207
  * )
382
208
  *
383
209
  * @example
384
- * // Explicit type (highest priority)
210
+ * // Explicit type
385
211
  * const todosCollection = createCollection<Todo>(
386
212
  * queryCollectionOptions({
387
213
  * queryKey: ['todos'],
@@ -392,7 +218,7 @@ export interface QueryCollectionUtils<
392
218
  * )
393
219
  *
394
220
  * @example
395
- * // Schema inference (second priority)
221
+ * // Schema inference
396
222
  * const todosCollection = createCollection(
397
223
  * queryCollectionOptions({
398
224
  * queryKey: ['todos'],
@@ -423,30 +249,62 @@ export interface QueryCollectionUtils<
423
249
  * })
424
250
  * )
425
251
  */
252
+
253
+ // Overload for when schema is provided
426
254
  export function queryCollectionOptions<
427
- TExplicit extends object = object,
428
- TSchema extends StandardSchemaV1 = never,
429
- TQueryFn extends (
430
- context: QueryFunctionContext<any>
431
- ) => Promise<Array<any>> = (
432
- context: QueryFunctionContext<any>
433
- ) => Promise<Array<any>>,
255
+ T extends StandardSchemaV1,
434
256
  TError = unknown,
435
257
  TQueryKey extends QueryKey = QueryKey,
436
258
  TKey extends string | number = string | number,
437
- TInsertInput extends object = ResolveType<TExplicit, TSchema, TQueryFn>,
438
259
  >(
439
- config: QueryCollectionConfig<TExplicit, TSchema, TQueryFn, TError, TQueryKey>
440
- ): CollectionConfig<ResolveType<TExplicit, TSchema, TQueryFn>> & {
260
+ config: QueryCollectionConfig<
261
+ InferSchemaOutput<T>,
262
+ (
263
+ context: QueryFunctionContext<any>
264
+ ) => Promise<Array<InferSchemaOutput<T>>>,
265
+ TError,
266
+ TQueryKey,
267
+ TKey,
268
+ T
269
+ > & {
270
+ schema: T
271
+ }
272
+ ): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {
273
+ schema: T
441
274
  utils: QueryCollectionUtils<
442
- ResolveType<TExplicit, TSchema, TQueryFn>,
275
+ InferSchemaOutput<T>,
443
276
  TKey,
444
- TInsertInput,
277
+ InferSchemaInput<T>,
445
278
  TError
446
279
  >
447
- } {
448
- type TItem = ResolveType<TExplicit, TSchema, TQueryFn>
280
+ }
449
281
 
282
+ // Overload for when no schema is provided
283
+ export function queryCollectionOptions<
284
+ T extends object,
285
+ TError = unknown,
286
+ TQueryKey extends QueryKey = QueryKey,
287
+ TKey extends string | number = string | number,
288
+ >(
289
+ config: QueryCollectionConfig<
290
+ T,
291
+ (context: QueryFunctionContext<any>) => Promise<Array<T>>,
292
+ TError,
293
+ TQueryKey,
294
+ TKey
295
+ > & {
296
+ schema?: never // prohibit schema
297
+ }
298
+ ): CollectionConfig<T, TKey> & {
299
+ schema?: never // no schema in the result
300
+ utils: QueryCollectionUtils<T, TKey, T, TError>
301
+ }
302
+
303
+ export function queryCollectionOptions(
304
+ config: QueryCollectionConfig<Record<string, unknown>>
305
+ ): CollectionConfig & {
306
+ utils: QueryCollectionUtils
307
+ } {
450
308
  const {
451
309
  queryKey,
452
310
  queryFn,
@@ -485,21 +343,21 @@ export function queryCollectionOptions<
485
343
  }
486
344
 
487
345
  /** The last error encountered by the query */
488
- let lastError: TError | undefined
346
+ let lastError: any
489
347
  /** The number of consecutive sync failures */
490
348
  let errorCount = 0
491
349
  /** The timestamp for when the query most recently returned the status as "error" */
492
350
  let lastErrorUpdatedAt = 0
493
351
 
494
- const internalSync: SyncConfig<TItem>[`sync`] = (params) => {
352
+ const internalSync: SyncConfig<any>[`sync`] = (params) => {
495
353
  const { begin, write, commit, markReady, collection } = params
496
354
 
497
355
  const observerOptions: QueryObserverOptions<
498
- Array<TItem>,
499
- TError,
500
- Array<TItem>,
501
- Array<TItem>,
502
- TQueryKey
356
+ Array<any>,
357
+ any,
358
+ Array<any>,
359
+ Array<any>,
360
+ any
503
361
  > = {
504
362
  queryKey: queryKey,
505
363
  queryFn: queryFn,
@@ -514,11 +372,11 @@ export function queryCollectionOptions<
514
372
  }
515
373
 
516
374
  const localObserver = new QueryObserver<
517
- Array<TItem>,
518
- TError,
519
- Array<TItem>,
520
- Array<TItem>,
521
- TQueryKey
375
+ Array<any>,
376
+ any,
377
+ Array<any>,
378
+ Array<any>,
379
+ any
522
380
  >(queryClient, observerOptions)
523
381
 
524
382
  type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]
@@ -542,7 +400,7 @@ export function queryCollectionOptions<
542
400
  }
543
401
 
544
402
  const currentSyncedItems = new Map(collection.syncedData)
545
- const newItemsMap = new Map<string | number, TItem>()
403
+ const newItemsMap = new Map<string | number, any>()
546
404
  newItemsArray.forEach((item) => {
547
405
  const key = getKey(item)
548
406
  newItemsMap.set(key, item)
@@ -645,14 +503,14 @@ export function queryCollectionOptions<
645
503
  collection: any
646
504
  queryClient: QueryClient
647
505
  queryKey: Array<unknown>
648
- getKey: (item: TItem) => TKey
506
+ getKey: (item: any) => string | number
649
507
  begin: () => void
650
- write: (message: Omit<ChangeMessage<TItem>, `key`>) => void
508
+ write: (message: Omit<ChangeMessage<any>, `key`>) => void
651
509
  commit: () => void
652
510
  } | null = null
653
511
 
654
512
  // Enhanced internalSync that captures write functions for manual use
655
- const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {
513
+ const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {
656
514
  const { begin, write, commit, collection } = params
657
515
 
658
516
  // Store references for manual write operations
@@ -660,7 +518,7 @@ export function queryCollectionOptions<
660
518
  collection,
661
519
  queryClient,
662
520
  queryKey: queryKey as unknown as Array<unknown>,
663
- getKey: getKey as (item: TItem) => TKey,
521
+ getKey: getKey as (item: any) => string | number,
664
522
  begin,
665
523
  write,
666
524
  commit,
@@ -671,13 +529,13 @@ export function queryCollectionOptions<
671
529
  }
672
530
 
673
531
  // Create write utils using the manual-sync module
674
- const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(
532
+ const writeUtils = createWriteUtils<any, string | number, any>(
675
533
  () => writeContext
676
534
  )
677
535
 
678
536
  // Create wrapper handlers for direct persistence operations that handle refetching
679
537
  const wrappedOnInsert = onInsert
680
- ? async (params: InsertMutationFnParams<TItem>) => {
538
+ ? async (params: InsertMutationFnParams<any>) => {
681
539
  const handlerResult = (await onInsert(params)) ?? {}
682
540
  const shouldRefetch =
683
541
  (handlerResult as { refetch?: boolean }).refetch !== false
@@ -691,7 +549,7 @@ export function queryCollectionOptions<
691
549
  : undefined
692
550
 
693
551
  const wrappedOnUpdate = onUpdate
694
- ? async (params: UpdateMutationFnParams<TItem>) => {
552
+ ? async (params: UpdateMutationFnParams<any>) => {
695
553
  const handlerResult = (await onUpdate(params)) ?? {}
696
554
  const shouldRefetch =
697
555
  (handlerResult as { refetch?: boolean }).refetch !== false
@@ -705,7 +563,7 @@ export function queryCollectionOptions<
705
563
  : undefined
706
564
 
707
565
  const wrappedOnDelete = onDelete
708
- ? async (params: DeleteMutationFnParams<TItem>) => {
566
+ ? async (params: DeleteMutationFnParams<any>) => {
709
567
  const handlerResult = (await onDelete(params)) ?? {}
710
568
  const shouldRefetch =
711
569
  (handlerResult as { refetch?: boolean }).refetch !== false