@tanstack/query-db-collection 0.2.15 → 0.2.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/query.cjs +29 -5
- package/dist/cjs/query.cjs.map +1 -1
- package/dist/cjs/query.d.cts +21 -3
- package/dist/esm/query.d.ts +21 -3
- package/dist/esm/query.js +29 -5
- package/dist/esm/query.js.map +1 -1
- package/package.json +2 -2
- package/src/query.ts +55 -6
package/dist/cjs/query.cjs
CHANGED
|
@@ -32,6 +32,9 @@ function queryCollectionOptions(config) {
|
|
|
32
32
|
if (!getKey) {
|
|
33
33
|
throw new errors.GetKeyRequiredError();
|
|
34
34
|
}
|
|
35
|
+
let lastError;
|
|
36
|
+
let errorCount = 0;
|
|
37
|
+
let lastErrorUpdatedAt = 0;
|
|
35
38
|
const internalSync = (params) => {
|
|
36
39
|
const { begin, write, commit, markReady, collection } = params;
|
|
37
40
|
const observerOptions = {
|
|
@@ -49,6 +52,8 @@ function queryCollectionOptions(config) {
|
|
|
49
52
|
const localObserver = new queryCore.QueryObserver(queryClient, observerOptions);
|
|
50
53
|
const handleUpdate = (result) => {
|
|
51
54
|
if (result.isSuccess) {
|
|
55
|
+
lastError = void 0;
|
|
56
|
+
errorCount = 0;
|
|
52
57
|
const newItemsArray = result.data;
|
|
53
58
|
if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
|
|
54
59
|
console.error(
|
|
@@ -92,6 +97,11 @@ function queryCollectionOptions(config) {
|
|
|
92
97
|
commit();
|
|
93
98
|
markReady();
|
|
94
99
|
} else if (result.isError) {
|
|
100
|
+
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
|
|
101
|
+
lastError = result.error;
|
|
102
|
+
errorCount++;
|
|
103
|
+
lastErrorUpdatedAt = result.errorUpdatedAt;
|
|
104
|
+
}
|
|
95
105
|
console.error(
|
|
96
106
|
`[QueryCollection] Error observing query ${String(queryKey)}:`,
|
|
97
107
|
result.error
|
|
@@ -107,10 +117,15 @@ function queryCollectionOptions(config) {
|
|
|
107
117
|
queryClient.removeQueries({ queryKey });
|
|
108
118
|
};
|
|
109
119
|
};
|
|
110
|
-
const refetch =
|
|
111
|
-
return queryClient.refetchQueries(
|
|
112
|
-
|
|
113
|
-
|
|
120
|
+
const refetch = (opts) => {
|
|
121
|
+
return queryClient.refetchQueries(
|
|
122
|
+
{
|
|
123
|
+
queryKey
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
throwOnError: opts == null ? void 0 : opts.throwOnError
|
|
127
|
+
}
|
|
128
|
+
);
|
|
114
129
|
};
|
|
115
130
|
let writeContext = null;
|
|
116
131
|
const enhancedInternalSync = (params) => {
|
|
@@ -162,7 +177,16 @@ function queryCollectionOptions(config) {
|
|
|
162
177
|
onDelete: wrappedOnDelete,
|
|
163
178
|
utils: {
|
|
164
179
|
refetch,
|
|
165
|
-
...writeUtils
|
|
180
|
+
...writeUtils,
|
|
181
|
+
lastError: () => lastError,
|
|
182
|
+
isError: () => !!lastError,
|
|
183
|
+
errorCount: () => errorCount,
|
|
184
|
+
clearError: () => {
|
|
185
|
+
lastError = void 0;
|
|
186
|
+
errorCount = 0;
|
|
187
|
+
lastErrorUpdatedAt = 0;
|
|
188
|
+
return refetch({ throwOnError: true });
|
|
189
|
+
}
|
|
166
190
|
}
|
|
167
191
|
};
|
|
168
192
|
}
|
package/dist/cjs/query.cjs.map
CHANGED
|
@@ -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 = () => 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 */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\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}\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 >\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 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 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 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 = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\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 },\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 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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"}
|
package/dist/cjs/query.d.cts
CHANGED
|
@@ -197,15 +197,18 @@ export interface QueryCollectionConfig<TExplicit extends object = object, TSchem
|
|
|
197
197
|
/**
|
|
198
198
|
* Type for the refetch utility function
|
|
199
199
|
*/
|
|
200
|
-
export type RefetchFn = (
|
|
200
|
+
export type RefetchFn = (opts?: {
|
|
201
|
+
throwOnError?: boolean;
|
|
202
|
+
}) => Promise<void>;
|
|
201
203
|
/**
|
|
202
204
|
* Utility methods available on Query Collections for direct writes and manual operations.
|
|
203
205
|
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
|
|
204
206
|
* @template TItem - The type of items stored in the collection
|
|
205
207
|
* @template TKey - The type of the item keys
|
|
206
208
|
* @template TInsertInput - The type accepted for insert operations
|
|
209
|
+
* @template TError - The type of errors that can occur during queries
|
|
207
210
|
*/
|
|
208
|
-
export interface QueryCollectionUtils<TItem extends object = Record<string, unknown>, TKey extends string | number = string | number, TInsertInput extends object = TItem> extends UtilsRecord {
|
|
211
|
+
export interface QueryCollectionUtils<TItem extends object = Record<string, unknown>, TKey extends string | number = string | number, TInsertInput extends object = TItem, TError = unknown> extends UtilsRecord {
|
|
209
212
|
/** Manually trigger a refetch of the query */
|
|
210
213
|
refetch: RefetchFn;
|
|
211
214
|
/** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */
|
|
@@ -218,6 +221,21 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
218
221
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void;
|
|
219
222
|
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
220
223
|
writeBatch: (callback: () => void) => void;
|
|
224
|
+
/** Get the last error encountered by the query (if any); reset on success */
|
|
225
|
+
lastError: () => TError | undefined;
|
|
226
|
+
/** Check if the collection is in an error state */
|
|
227
|
+
isError: () => boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Get the number of consecutive sync failures.
|
|
230
|
+
* Incremented only when query fails completely (not per retry attempt); reset on success.
|
|
231
|
+
*/
|
|
232
|
+
errorCount: () => number;
|
|
233
|
+
/**
|
|
234
|
+
* Clear the error state and trigger a refetch of the query
|
|
235
|
+
* @returns Promise that resolves when the refetch completes successfully
|
|
236
|
+
* @throws Error if the refetch fails
|
|
237
|
+
*/
|
|
238
|
+
clearError: () => Promise<void>;
|
|
221
239
|
}
|
|
222
240
|
/**
|
|
223
241
|
* Creates query collection options for use with a standard Collection.
|
|
@@ -297,5 +315,5 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
297
315
|
* )
|
|
298
316
|
*/
|
|
299
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>> & {
|
|
300
|
-
utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput>;
|
|
318
|
+
utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput, TError>;
|
|
301
319
|
};
|
package/dist/esm/query.d.ts
CHANGED
|
@@ -197,15 +197,18 @@ export interface QueryCollectionConfig<TExplicit extends object = object, TSchem
|
|
|
197
197
|
/**
|
|
198
198
|
* Type for the refetch utility function
|
|
199
199
|
*/
|
|
200
|
-
export type RefetchFn = (
|
|
200
|
+
export type RefetchFn = (opts?: {
|
|
201
|
+
throwOnError?: boolean;
|
|
202
|
+
}) => Promise<void>;
|
|
201
203
|
/**
|
|
202
204
|
* Utility methods available on Query Collections for direct writes and manual operations.
|
|
203
205
|
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
|
|
204
206
|
* @template TItem - The type of items stored in the collection
|
|
205
207
|
* @template TKey - The type of the item keys
|
|
206
208
|
* @template TInsertInput - The type accepted for insert operations
|
|
209
|
+
* @template TError - The type of errors that can occur during queries
|
|
207
210
|
*/
|
|
208
|
-
export interface QueryCollectionUtils<TItem extends object = Record<string, unknown>, TKey extends string | number = string | number, TInsertInput extends object = TItem> extends UtilsRecord {
|
|
211
|
+
export interface QueryCollectionUtils<TItem extends object = Record<string, unknown>, TKey extends string | number = string | number, TInsertInput extends object = TItem, TError = unknown> extends UtilsRecord {
|
|
209
212
|
/** Manually trigger a refetch of the query */
|
|
210
213
|
refetch: RefetchFn;
|
|
211
214
|
/** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */
|
|
@@ -218,6 +221,21 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
218
221
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void;
|
|
219
222
|
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
220
223
|
writeBatch: (callback: () => void) => void;
|
|
224
|
+
/** Get the last error encountered by the query (if any); reset on success */
|
|
225
|
+
lastError: () => TError | undefined;
|
|
226
|
+
/** Check if the collection is in an error state */
|
|
227
|
+
isError: () => boolean;
|
|
228
|
+
/**
|
|
229
|
+
* Get the number of consecutive sync failures.
|
|
230
|
+
* Incremented only when query fails completely (not per retry attempt); reset on success.
|
|
231
|
+
*/
|
|
232
|
+
errorCount: () => number;
|
|
233
|
+
/**
|
|
234
|
+
* Clear the error state and trigger a refetch of the query
|
|
235
|
+
* @returns Promise that resolves when the refetch completes successfully
|
|
236
|
+
* @throws Error if the refetch fails
|
|
237
|
+
*/
|
|
238
|
+
clearError: () => Promise<void>;
|
|
221
239
|
}
|
|
222
240
|
/**
|
|
223
241
|
* Creates query collection options for use with a standard Collection.
|
|
@@ -297,5 +315,5 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
297
315
|
* )
|
|
298
316
|
*/
|
|
299
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>> & {
|
|
300
|
-
utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput>;
|
|
318
|
+
utils: QueryCollectionUtils<ResolveType<TExplicit, TSchema, TQueryFn>, TKey, TInsertInput, TError>;
|
|
301
319
|
};
|
package/dist/esm/query.js
CHANGED
|
@@ -30,6 +30,9 @@ function queryCollectionOptions(config) {
|
|
|
30
30
|
if (!getKey) {
|
|
31
31
|
throw new GetKeyRequiredError();
|
|
32
32
|
}
|
|
33
|
+
let lastError;
|
|
34
|
+
let errorCount = 0;
|
|
35
|
+
let lastErrorUpdatedAt = 0;
|
|
33
36
|
const internalSync = (params) => {
|
|
34
37
|
const { begin, write, commit, markReady, collection } = params;
|
|
35
38
|
const observerOptions = {
|
|
@@ -47,6 +50,8 @@ function queryCollectionOptions(config) {
|
|
|
47
50
|
const localObserver = new QueryObserver(queryClient, observerOptions);
|
|
48
51
|
const handleUpdate = (result) => {
|
|
49
52
|
if (result.isSuccess) {
|
|
53
|
+
lastError = void 0;
|
|
54
|
+
errorCount = 0;
|
|
50
55
|
const newItemsArray = result.data;
|
|
51
56
|
if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
|
|
52
57
|
console.error(
|
|
@@ -90,6 +95,11 @@ function queryCollectionOptions(config) {
|
|
|
90
95
|
commit();
|
|
91
96
|
markReady();
|
|
92
97
|
} else if (result.isError) {
|
|
98
|
+
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
|
|
99
|
+
lastError = result.error;
|
|
100
|
+
errorCount++;
|
|
101
|
+
lastErrorUpdatedAt = result.errorUpdatedAt;
|
|
102
|
+
}
|
|
93
103
|
console.error(
|
|
94
104
|
`[QueryCollection] Error observing query ${String(queryKey)}:`,
|
|
95
105
|
result.error
|
|
@@ -105,10 +115,15 @@ function queryCollectionOptions(config) {
|
|
|
105
115
|
queryClient.removeQueries({ queryKey });
|
|
106
116
|
};
|
|
107
117
|
};
|
|
108
|
-
const refetch =
|
|
109
|
-
return queryClient.refetchQueries(
|
|
110
|
-
|
|
111
|
-
|
|
118
|
+
const refetch = (opts) => {
|
|
119
|
+
return queryClient.refetchQueries(
|
|
120
|
+
{
|
|
121
|
+
queryKey
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
throwOnError: opts == null ? void 0 : opts.throwOnError
|
|
125
|
+
}
|
|
126
|
+
);
|
|
112
127
|
};
|
|
113
128
|
let writeContext = null;
|
|
114
129
|
const enhancedInternalSync = (params) => {
|
|
@@ -160,7 +175,16 @@ function queryCollectionOptions(config) {
|
|
|
160
175
|
onDelete: wrappedOnDelete,
|
|
161
176
|
utils: {
|
|
162
177
|
refetch,
|
|
163
|
-
...writeUtils
|
|
178
|
+
...writeUtils,
|
|
179
|
+
lastError: () => lastError,
|
|
180
|
+
isError: () => !!lastError,
|
|
181
|
+
errorCount: () => errorCount,
|
|
182
|
+
clearError: () => {
|
|
183
|
+
lastError = void 0;
|
|
184
|
+
errorCount = 0;
|
|
185
|
+
lastErrorUpdatedAt = 0;
|
|
186
|
+
return refetch({ throwOnError: true });
|
|
187
|
+
}
|
|
164
188
|
}
|
|
165
189
|
};
|
|
166
190
|
}
|
package/dist/esm/query.js.map
CHANGED
|
@@ -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 = () => 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 */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\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}\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 >\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 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 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 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 = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\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 },\n }\n}\n"],"names":[],"mappings":"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"}
|
|
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":"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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.
|
|
4
|
+
"version": "0.2.16",
|
|
5
5
|
"dependencies": {
|
|
6
6
|
"@standard-schema/spec": "^1.0.0",
|
|
7
|
-
"@tanstack/db": "0.2.
|
|
7
|
+
"@tanstack/db": "0.2.4"
|
|
8
8
|
},
|
|
9
9
|
"devDependencies": {
|
|
10
10
|
"@tanstack/query-core": "^5.87.4",
|
package/src/query.ts
CHANGED
|
@@ -301,7 +301,7 @@ export interface QueryCollectionConfig<
|
|
|
301
301
|
/**
|
|
302
302
|
* Type for the refetch utility function
|
|
303
303
|
*/
|
|
304
|
-
export type RefetchFn = () => Promise<void>
|
|
304
|
+
export type RefetchFn = (opts?: { throwOnError?: boolean }) => Promise<void>
|
|
305
305
|
|
|
306
306
|
/**
|
|
307
307
|
* Utility methods available on Query Collections for direct writes and manual operations.
|
|
@@ -309,11 +309,13 @@ export type RefetchFn = () => Promise<void>
|
|
|
309
309
|
* @template TItem - The type of items stored in the collection
|
|
310
310
|
* @template TKey - The type of the item keys
|
|
311
311
|
* @template TInsertInput - The type accepted for insert operations
|
|
312
|
+
* @template TError - The type of errors that can occur during queries
|
|
312
313
|
*/
|
|
313
314
|
export interface QueryCollectionUtils<
|
|
314
315
|
TItem extends object = Record<string, unknown>,
|
|
315
316
|
TKey extends string | number = string | number,
|
|
316
317
|
TInsertInput extends object = TItem,
|
|
318
|
+
TError = unknown,
|
|
317
319
|
> extends UtilsRecord {
|
|
318
320
|
/** Manually trigger a refetch of the query */
|
|
319
321
|
refetch: RefetchFn
|
|
@@ -327,6 +329,21 @@ export interface QueryCollectionUtils<
|
|
|
327
329
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void
|
|
328
330
|
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
329
331
|
writeBatch: (callback: () => void) => void
|
|
332
|
+
/** Get the last error encountered by the query (if any); reset on success */
|
|
333
|
+
lastError: () => TError | undefined
|
|
334
|
+
/** Check if the collection is in an error state */
|
|
335
|
+
isError: () => boolean
|
|
336
|
+
/**
|
|
337
|
+
* Get the number of consecutive sync failures.
|
|
338
|
+
* Incremented only when query fails completely (not per retry attempt); reset on success.
|
|
339
|
+
*/
|
|
340
|
+
errorCount: () => number
|
|
341
|
+
/**
|
|
342
|
+
* Clear the error state and trigger a refetch of the query
|
|
343
|
+
* @returns Promise that resolves when the refetch completes successfully
|
|
344
|
+
* @throws Error if the refetch fails
|
|
345
|
+
*/
|
|
346
|
+
clearError: () => Promise<void>
|
|
330
347
|
}
|
|
331
348
|
|
|
332
349
|
/**
|
|
@@ -424,7 +441,8 @@ export function queryCollectionOptions<
|
|
|
424
441
|
utils: QueryCollectionUtils<
|
|
425
442
|
ResolveType<TExplicit, TSchema, TQueryFn>,
|
|
426
443
|
TKey,
|
|
427
|
-
TInsertInput
|
|
444
|
+
TInsertInput,
|
|
445
|
+
TError
|
|
428
446
|
>
|
|
429
447
|
} {
|
|
430
448
|
type TItem = ResolveType<TExplicit, TSchema, TQueryFn>
|
|
@@ -466,6 +484,13 @@ export function queryCollectionOptions<
|
|
|
466
484
|
throw new GetKeyRequiredError()
|
|
467
485
|
}
|
|
468
486
|
|
|
487
|
+
/** The last error encountered by the query */
|
|
488
|
+
let lastError: TError | undefined
|
|
489
|
+
/** The number of consecutive sync failures */
|
|
490
|
+
let errorCount = 0
|
|
491
|
+
/** The timestamp for when the query most recently returned the status as "error" */
|
|
492
|
+
let lastErrorUpdatedAt = 0
|
|
493
|
+
|
|
469
494
|
const internalSync: SyncConfig<TItem>[`sync`] = (params) => {
|
|
470
495
|
const { begin, write, commit, markReady, collection } = params
|
|
471
496
|
|
|
@@ -499,6 +524,10 @@ export function queryCollectionOptions<
|
|
|
499
524
|
type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]
|
|
500
525
|
const handleUpdate: UpdateHandler = (result) => {
|
|
501
526
|
if (result.isSuccess) {
|
|
527
|
+
// Clear error state
|
|
528
|
+
lastError = undefined
|
|
529
|
+
errorCount = 0
|
|
530
|
+
|
|
502
531
|
const newItemsArray = result.data
|
|
503
532
|
|
|
504
533
|
if (
|
|
@@ -567,6 +596,12 @@ export function queryCollectionOptions<
|
|
|
567
596
|
// Mark collection as ready after first successful query result
|
|
568
597
|
markReady()
|
|
569
598
|
} else if (result.isError) {
|
|
599
|
+
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
|
|
600
|
+
lastError = result.error
|
|
601
|
+
errorCount++
|
|
602
|
+
lastErrorUpdatedAt = result.errorUpdatedAt
|
|
603
|
+
}
|
|
604
|
+
|
|
570
605
|
console.error(
|
|
571
606
|
`[QueryCollection] Error observing query ${String(queryKey)}:`,
|
|
572
607
|
result.error
|
|
@@ -594,10 +629,15 @@ export function queryCollectionOptions<
|
|
|
594
629
|
* Refetch the query data
|
|
595
630
|
* @returns Promise that resolves when the refetch is complete
|
|
596
631
|
*/
|
|
597
|
-
const refetch: RefetchFn =
|
|
598
|
-
return queryClient.refetchQueries(
|
|
599
|
-
|
|
600
|
-
|
|
632
|
+
const refetch: RefetchFn = (opts) => {
|
|
633
|
+
return queryClient.refetchQueries(
|
|
634
|
+
{
|
|
635
|
+
queryKey: queryKey,
|
|
636
|
+
},
|
|
637
|
+
{
|
|
638
|
+
throwOnError: opts?.throwOnError,
|
|
639
|
+
}
|
|
640
|
+
)
|
|
601
641
|
}
|
|
602
642
|
|
|
603
643
|
// Create write context for manual write operations
|
|
@@ -688,6 +728,15 @@ export function queryCollectionOptions<
|
|
|
688
728
|
utils: {
|
|
689
729
|
refetch,
|
|
690
730
|
...writeUtils,
|
|
731
|
+
lastError: () => lastError,
|
|
732
|
+
isError: () => !!lastError,
|
|
733
|
+
errorCount: () => errorCount,
|
|
734
|
+
clearError: () => {
|
|
735
|
+
lastError = undefined
|
|
736
|
+
errorCount = 0
|
|
737
|
+
lastErrorUpdatedAt = 0
|
|
738
|
+
return refetch({ throwOnError: true })
|
|
739
|
+
},
|
|
691
740
|
},
|
|
692
741
|
}
|
|
693
742
|
}
|