@tanstack/query-db-collection 0.2.42 → 0.3.0
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 +61 -26
- package/dist/cjs/query.cjs.map +1 -1
- package/dist/cjs/query.d.cts +17 -7
- package/dist/esm/query.d.ts +17 -7
- package/dist/esm/query.js +61 -26
- package/dist/esm/query.js.map +1 -1
- package/package.json +5 -4
- package/src/query.ts +154 -39
package/dist/cjs/query.cjs
CHANGED
|
@@ -3,6 +3,50 @@ Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
|
3
3
|
const queryCore = require("@tanstack/query-core");
|
|
4
4
|
const errors = require("./errors.cjs");
|
|
5
5
|
const manualSync = require("./manual-sync.cjs");
|
|
6
|
+
class QueryCollectionUtilsImpl {
|
|
7
|
+
constructor(state, refetch, writeUtils) {
|
|
8
|
+
this.state = state;
|
|
9
|
+
this.refetchFn = refetch;
|
|
10
|
+
this.refetch = refetch;
|
|
11
|
+
this.writeInsert = writeUtils.writeInsert;
|
|
12
|
+
this.writeUpdate = writeUtils.writeUpdate;
|
|
13
|
+
this.writeDelete = writeUtils.writeDelete;
|
|
14
|
+
this.writeUpsert = writeUtils.writeUpsert;
|
|
15
|
+
this.writeBatch = writeUtils.writeBatch;
|
|
16
|
+
}
|
|
17
|
+
async clearError() {
|
|
18
|
+
this.state.lastError = void 0;
|
|
19
|
+
this.state.errorCount = 0;
|
|
20
|
+
this.state.lastErrorUpdatedAt = 0;
|
|
21
|
+
await this.refetchFn({ throwOnError: true });
|
|
22
|
+
}
|
|
23
|
+
// Getters for error state
|
|
24
|
+
get lastError() {
|
|
25
|
+
return this.state.lastError;
|
|
26
|
+
}
|
|
27
|
+
get isError() {
|
|
28
|
+
return !!this.state.lastError;
|
|
29
|
+
}
|
|
30
|
+
get errorCount() {
|
|
31
|
+
return this.state.errorCount;
|
|
32
|
+
}
|
|
33
|
+
// Getters for QueryObserver state
|
|
34
|
+
get isFetching() {
|
|
35
|
+
return this.state.queryObserver?.getCurrentResult().isFetching ?? false;
|
|
36
|
+
}
|
|
37
|
+
get isRefetching() {
|
|
38
|
+
return this.state.queryObserver?.getCurrentResult().isRefetching ?? false;
|
|
39
|
+
}
|
|
40
|
+
get isLoading() {
|
|
41
|
+
return this.state.queryObserver?.getCurrentResult().isLoading ?? false;
|
|
42
|
+
}
|
|
43
|
+
get dataUpdatedAt() {
|
|
44
|
+
return this.state.queryObserver?.getCurrentResult().dataUpdatedAt ?? 0;
|
|
45
|
+
}
|
|
46
|
+
get fetchStatus() {
|
|
47
|
+
return this.state.queryObserver?.getCurrentResult().fetchStatus ?? `idle`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
6
50
|
function queryCollectionOptions(config) {
|
|
7
51
|
const {
|
|
8
52
|
queryKey,
|
|
@@ -33,10 +77,12 @@ function queryCollectionOptions(config) {
|
|
|
33
77
|
if (!getKey) {
|
|
34
78
|
throw new errors.GetKeyRequiredError();
|
|
35
79
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
80
|
+
const state = {
|
|
81
|
+
lastError: void 0,
|
|
82
|
+
errorCount: 0,
|
|
83
|
+
lastErrorUpdatedAt: 0,
|
|
84
|
+
queryObserver: void 0
|
|
85
|
+
};
|
|
40
86
|
const internalSync = (params) => {
|
|
41
87
|
const { begin, write, commit, markReady, collection } = params;
|
|
42
88
|
const observerOptions = {
|
|
@@ -53,13 +99,13 @@ function queryCollectionOptions(config) {
|
|
|
53
99
|
...staleTime !== void 0 && { staleTime }
|
|
54
100
|
};
|
|
55
101
|
const localObserver = new queryCore.QueryObserver(queryClient, observerOptions);
|
|
56
|
-
queryObserver = localObserver;
|
|
102
|
+
state.queryObserver = localObserver;
|
|
57
103
|
let isSubscribed = false;
|
|
58
104
|
let actualUnsubscribeFn = null;
|
|
59
105
|
const handleQueryResult = (result) => {
|
|
60
106
|
if (result.isSuccess) {
|
|
61
|
-
lastError = void 0;
|
|
62
|
-
errorCount = 0;
|
|
107
|
+
state.lastError = void 0;
|
|
108
|
+
state.errorCount = 0;
|
|
63
109
|
const rawData = result.data;
|
|
64
110
|
const newItemsArray = select ? select(rawData) : rawData;
|
|
65
111
|
if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
|
|
@@ -104,10 +150,10 @@ function queryCollectionOptions(config) {
|
|
|
104
150
|
commit();
|
|
105
151
|
markReady();
|
|
106
152
|
} else if (result.isError) {
|
|
107
|
-
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
|
|
108
|
-
lastError = result.error;
|
|
109
|
-
errorCount++;
|
|
110
|
-
lastErrorUpdatedAt = result.errorUpdatedAt;
|
|
153
|
+
if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {
|
|
154
|
+
state.lastError = result.error;
|
|
155
|
+
state.errorCount++;
|
|
156
|
+
state.lastErrorUpdatedAt = result.errorUpdatedAt;
|
|
111
157
|
}
|
|
112
158
|
console.error(
|
|
113
159
|
`[QueryCollection] Error observing query ${String(queryKey)}:`,
|
|
@@ -149,10 +195,10 @@ function queryCollectionOptions(config) {
|
|
|
149
195
|
};
|
|
150
196
|
};
|
|
151
197
|
const refetch = async (opts) => {
|
|
152
|
-
if (!queryObserver) {
|
|
198
|
+
if (!state.queryObserver) {
|
|
153
199
|
return;
|
|
154
200
|
}
|
|
155
|
-
return queryObserver.refetch({
|
|
201
|
+
return state.queryObserver.refetch({
|
|
156
202
|
throwOnError: opts?.throwOnError
|
|
157
203
|
});
|
|
158
204
|
};
|
|
@@ -197,6 +243,7 @@ function queryCollectionOptions(config) {
|
|
|
197
243
|
}
|
|
198
244
|
return handlerResult;
|
|
199
245
|
} : void 0;
|
|
246
|
+
const utils = new QueryCollectionUtilsImpl(state, refetch, writeUtils);
|
|
200
247
|
return {
|
|
201
248
|
...baseCollectionConfig,
|
|
202
249
|
getKey,
|
|
@@ -204,19 +251,7 @@ function queryCollectionOptions(config) {
|
|
|
204
251
|
onInsert: wrappedOnInsert,
|
|
205
252
|
onUpdate: wrappedOnUpdate,
|
|
206
253
|
onDelete: wrappedOnDelete,
|
|
207
|
-
utils
|
|
208
|
-
refetch,
|
|
209
|
-
...writeUtils,
|
|
210
|
-
lastError: () => lastError,
|
|
211
|
-
isError: () => !!lastError,
|
|
212
|
-
errorCount: () => errorCount,
|
|
213
|
-
clearError: async () => {
|
|
214
|
-
lastError = void 0;
|
|
215
|
-
errorCount = 0;
|
|
216
|
-
lastErrorUpdatedAt = 0;
|
|
217
|
-
await refetch({ throwOnError: true });
|
|
218
|
-
}
|
|
219
|
-
}
|
|
254
|
+
utils
|
|
220
255
|
};
|
|
221
256
|
}
|
|
222
257
|
exports.queryCollectionOptions = queryCollectionOptions;
|
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 QueryObserverResult,\n} from \"@tanstack/query-core\"\nimport type {\n BaseCollectionConfig,\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Schema input type inference helper (matches electric.ts pattern)\ntype InferSchemaInput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferInput<T> extends object\n ? StandardSchemaV1.InferInput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration options for creating a Query Collection\n * @template T - The explicit type of items stored in the collection\n * @template TQueryFn - The queryFn type\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TSchema - The schema type for validation\n */\nexport interface QueryCollectionConfig<\n T extends object = object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = never,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n> extends BaseCollectionConfig<T, TKey, TSchema> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>\n : TQueryFn\n /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */\n select?: (data: TQueryData) => Array<T>\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`staleTime`]\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n * Returns the QueryObserverResult from TanStack Query\n */\nexport type RefetchFn = (opts?: {\n throwOnError?: boolean\n}) => Promise<QueryObserverResult<any, any> | void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @template TError - The type of errors that can occur during queries\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n TError = unknown,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n /** Get the last error encountered by the query (if any); reset on success */\n lastError: () => TError | undefined\n /** Check if the collection is in an error state */\n isError: () => boolean\n /**\n * Get the number of consecutive sync failures.\n * Incremented only when query fails completely (not per retry attempt); reset on success.\n */\n errorCount: () => number\n /**\n * Clear the error state and trigger a refetch of the query\n * @returns Promise that resolves when the refetch completes successfully\n * @throws Error if the refetch fails\n */\n clearError: () => Promise<void>\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Schema inference (highest priority)\n * 2. QueryFn return type inference (second priority)\n *\n * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n *\n * @example\n * // The select option extracts the items array from a response with metadata\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * select: (data) => data.items, // Extract the array of items\n * queryClient,\n * schema: todoSchema,\n * getKey: (item) => item.id,\n * })\n * )\n */\n// Overload for when schema is provided and select present\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n select: (data: TQueryData) => Array<InferSchemaInput<T>>\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided and select present\nexport function queryCollectionOptions<\n T extends object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n T,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n never,\n TQueryData\n > & {\n schema?: never // prohibit schema\n select: (data: TQueryData) => Array<T>\n }\n): CollectionConfig<T, TKey> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\n// Overload for when schema is provided\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n (\n context: QueryFunctionContext<any>\n ) => Promise<Array<InferSchemaOutput<T>>>,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided\nexport function queryCollectionOptions<\n T extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n T,\n (context: QueryFunctionContext<any>) => Promise<Array<T>>,\n TError,\n TQueryKey,\n TKey\n > & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, TKey> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\nexport function queryCollectionOptions(\n config: QueryCollectionConfig<Record<string, unknown>>\n): CollectionConfig & {\n utils: QueryCollectionUtils\n} {\n const {\n queryKey,\n queryFn,\n select,\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** The last error encountered by the query */\n let lastError: any\n /** The number of consecutive sync failures */\n let errorCount = 0\n /** The timestamp for when the query most recently returned the status as \"error\" */\n let lastErrorUpdatedAt = 0\n /** Reference to the QueryObserver for imperative refetch */\n let queryObserver: QueryObserver<Array<any>, any, Array<any>, Array<any>, any>\n\n const internalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used\n ...(meta !== undefined && { meta }),\n ...(enabled !== undefined && { enabled }),\n ...(refetchInterval !== undefined && { refetchInterval }),\n ...(retry !== undefined && { retry }),\n ...(retryDelay !== undefined && { retryDelay }),\n ...(staleTime !== undefined && { staleTime }),\n }\n\n const localObserver = new QueryObserver<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n >(queryClient, observerOptions)\n\n // Store reference for imperative refetch\n queryObserver = localObserver\n\n let isSubscribed = false\n let actualUnsubscribeFn: (() => void) | null = null\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleQueryResult: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n lastError = undefined\n errorCount = 0\n\n const rawData = result.data\n const newItemsArray = select ? select(rawData) : rawData\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n const errorMessage = select\n ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n\n console.error(errorMessage)\n return\n }\n\n const currentSyncedItems: Map<string | number, any> = new Map(\n collection._state.syncedData.entries()\n )\n const newItemsMap = new Map<string | number, any>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== lastErrorUpdatedAt) {\n lastError = result.error\n errorCount++\n lastErrorUpdatedAt = result.errorUpdatedAt\n }\n\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n }\n\n const subscribeToQuery = () => {\n if (!isSubscribed) {\n actualUnsubscribeFn = localObserver.subscribe(handleQueryResult)\n isSubscribed = true\n }\n }\n\n const unsubscribeFromQuery = () => {\n if (isSubscribed && actualUnsubscribeFn) {\n actualUnsubscribeFn()\n actualUnsubscribeFn = null\n isSubscribed = false\n }\n }\n\n // Always subscribe when sync starts (this could be from preload(), startSync config, or first subscriber)\n // We'll dynamically unsubscribe/resubscribe based on subscriber count to maintain staleTime behavior\n subscribeToQuery()\n\n // Set up event listener for subscriber changes\n const unsubscribeFromCollectionEvents = collection.on(\n `subscribers:change`,\n ({ subscriberCount }) => {\n if (subscriberCount > 0) {\n subscribeToQuery()\n } else if (subscriberCount === 0) {\n unsubscribeFromQuery()\n }\n }\n )\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleQueryResult(localObserver.getCurrentResult())\n\n return async () => {\n unsubscribeFromCollectionEvents()\n unsubscribeFromQuery()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n *\n * Uses queryObserver.refetch() because:\n * - Bypasses `enabled: false` to support manual/imperative refetch patterns (e.g., button-triggered fetch)\n * - Ensures clearError() works even when enabled: false\n * - Always refetches THIS specific collection (exact targeting via observer)\n * - Respects retry, retryDelay, and other observer options\n *\n * This matches TanStack Query's hook behavior where refetch() bypasses enabled: false.\n * See: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries\n *\n * Used by both:\n * - utils.refetch() - for explicit user-triggered refetches\n * - Internal handlers (onInsert/onUpdate/onDelete) - after mutations to get fresh data\n *\n * @returns Promise that resolves when the refetch is complete, with QueryObserverResult\n */\n const refetch: RefetchFn = async (opts) => {\n // Observer is created when sync starts. If never synced, nothing to refetch.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryObserver) {\n return\n }\n // Return the QueryObserverResult for users to inspect\n return queryObserver.refetch({\n throwOnError: opts?.throwOnError,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: any) => string | number\n begin: () => void\n write: (message: Omit<ChangeMessage<any>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: any) => string | number,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<any, string | number, any>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n lastError: () => lastError,\n isError: () => !!lastError,\n errorCount: () => errorCount,\n clearError: async () => {\n lastError = undefined\n errorCount = 0\n lastErrorUpdatedAt = 0\n await refetch({ throwOnError: true })\n },\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n QueryObserverResult,\n} from \"@tanstack/query-core\"\nimport type {\n BaseCollectionConfig,\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Schema input type inference helper (matches electric.ts pattern)\ntype InferSchemaInput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferInput<T> extends object\n ? StandardSchemaV1.InferInput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration options for creating a Query Collection\n * @template T - The explicit type of items stored in the collection\n * @template TQueryFn - The queryFn type\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TSchema - The schema type for validation\n */\nexport interface QueryCollectionConfig<\n T extends object = object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = never,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n> extends BaseCollectionConfig<T, TKey, TSchema> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>\n : TQueryFn\n /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */\n select?: (data: TQueryData) => Array<T>\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`staleTime`]\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n * Returns the QueryObserverResult from TanStack Query\n */\nexport type RefetchFn = (opts?: {\n throwOnError?: boolean\n}) => Promise<QueryObserverResult<any, any> | 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\n // Query Observer State (getters)\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 /** Check if query is currently fetching (initial or background) */\n isFetching: boolean\n /** Check if query is refetching in background (not initial fetch) */\n isRefetching: boolean\n /** Check if query is loading for the first time (no data yet) */\n isLoading: boolean\n /** Get timestamp of last successful data update (in milliseconds) */\n dataUpdatedAt: number\n /** Get current fetch status */\n fetchStatus: `fetching` | `paused` | `idle`\n\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 * Internal state object for tracking query observer and errors\n */\ninterface QueryCollectionState {\n lastError: any\n errorCount: number\n lastErrorUpdatedAt: number\n queryObserver:\n | QueryObserver<Array<any>, any, Array<any>, Array<any>, any>\n | undefined\n}\n\n/**\n * Implementation class for QueryCollectionUtils with explicit dependency injection\n * for better testability and architectural clarity\n */\nclass QueryCollectionUtilsImpl {\n private state: QueryCollectionState\n private refetchFn: RefetchFn\n\n // Write methods\n public refetch: RefetchFn\n public writeInsert: any\n public writeUpdate: any\n public writeDelete: any\n public writeUpsert: any\n public writeBatch: any\n\n constructor(\n state: QueryCollectionState,\n refetch: RefetchFn,\n writeUtils: ReturnType<typeof createWriteUtils>\n ) {\n this.state = state\n this.refetchFn = refetch\n\n // Initialize methods to use passed dependencies\n this.refetch = refetch\n this.writeInsert = writeUtils.writeInsert\n this.writeUpdate = writeUtils.writeUpdate\n this.writeDelete = writeUtils.writeDelete\n this.writeUpsert = writeUtils.writeUpsert\n this.writeBatch = writeUtils.writeBatch\n }\n\n public async clearError() {\n this.state.lastError = undefined\n this.state.errorCount = 0\n this.state.lastErrorUpdatedAt = 0\n await this.refetchFn({ throwOnError: true })\n }\n\n // Getters for error state\n public get lastError() {\n return this.state.lastError\n }\n\n public get isError() {\n return !!this.state.lastError\n }\n\n public get errorCount() {\n return this.state.errorCount\n }\n\n // Getters for QueryObserver state\n public get isFetching() {\n return this.state.queryObserver?.getCurrentResult().isFetching ?? false\n }\n\n public get isRefetching() {\n return this.state.queryObserver?.getCurrentResult().isRefetching ?? false\n }\n\n public get isLoading() {\n return this.state.queryObserver?.getCurrentResult().isLoading ?? false\n }\n\n public get dataUpdatedAt() {\n return this.state.queryObserver?.getCurrentResult().dataUpdatedAt ?? 0\n }\n\n public get fetchStatus() {\n return this.state.queryObserver?.getCurrentResult().fetchStatus ?? `idle`\n }\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Schema inference (highest priority)\n * 2. QueryFn return type inference (second priority)\n *\n * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n *\n * @example\n * // The select option extracts the items array from a response with metadata\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * select: (data) => data.items, // Extract the array of items\n * queryClient,\n * schema: todoSchema,\n * getKey: (item) => item.id,\n * })\n * )\n */\n// Overload for when schema is provided and select present\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n select: (data: TQueryData) => Array<InferSchemaInput<T>>\n }\n): CollectionConfig<\n InferSchemaOutput<T>,\n TKey,\n T,\n QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>\n> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided and select present\nexport function queryCollectionOptions<\n T extends object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n T,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n never,\n TQueryData\n > & {\n schema?: never // prohibit schema\n select: (data: TQueryData) => Array<T>\n }\n): CollectionConfig<\n T,\n TKey,\n never,\n QueryCollectionUtils<T, TKey, T, TError>\n> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\n// Overload for when schema is provided\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n (\n context: QueryFunctionContext<any>\n ) => Promise<Array<InferSchemaOutput<T>>>,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n }\n): CollectionConfig<\n InferSchemaOutput<T>,\n TKey,\n T,\n QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>\n> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided\nexport function queryCollectionOptions<\n T extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n T,\n (context: QueryFunctionContext<any>) => Promise<Array<T>>,\n TError,\n TQueryKey,\n TKey\n > & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<\n T,\n TKey,\n never,\n QueryCollectionUtils<T, TKey, T, TError>\n> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\nexport function queryCollectionOptions(\n config: QueryCollectionConfig<Record<string, unknown>>\n): CollectionConfig<\n Record<string, unknown>,\n string | number,\n never,\n QueryCollectionUtils\n> & {\n utils: QueryCollectionUtils\n} {\n const {\n queryKey,\n queryFn,\n select,\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** State object to hold error tracking and observer reference */\n const state: QueryCollectionState = {\n lastError: undefined as any,\n errorCount: 0,\n lastErrorUpdatedAt: 0,\n queryObserver: undefined,\n }\n\n const internalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used\n ...(meta !== undefined && { meta }),\n ...(enabled !== undefined && { enabled }),\n ...(refetchInterval !== undefined && { refetchInterval }),\n ...(retry !== undefined && { retry }),\n ...(retryDelay !== undefined && { retryDelay }),\n ...(staleTime !== undefined && { staleTime }),\n }\n\n const localObserver = new QueryObserver<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n >(queryClient, observerOptions)\n\n // Store reference for imperative refetch\n state.queryObserver = localObserver\n\n let isSubscribed = false\n let actualUnsubscribeFn: (() => void) | null = null\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleQueryResult: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n state.lastError = undefined\n state.errorCount = 0\n\n const rawData = result.data\n const newItemsArray = select ? select(rawData) : rawData\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n const errorMessage = select\n ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n\n console.error(errorMessage)\n return\n }\n\n const currentSyncedItems: Map<string | number, any> = new Map(\n collection._state.syncedData.entries()\n )\n const newItemsMap = new Map<string | number, any>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {\n state.lastError = result.error\n state.errorCount++\n state.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 subscribeToQuery = () => {\n if (!isSubscribed) {\n actualUnsubscribeFn = localObserver.subscribe(handleQueryResult)\n isSubscribed = true\n }\n }\n\n const unsubscribeFromQuery = () => {\n if (isSubscribed && actualUnsubscribeFn) {\n actualUnsubscribeFn()\n actualUnsubscribeFn = null\n isSubscribed = false\n }\n }\n\n // Always subscribe when sync starts (this could be from preload(), startSync config, or first subscriber)\n // We'll dynamically unsubscribe/resubscribe based on subscriber count to maintain staleTime behavior\n subscribeToQuery()\n\n // Set up event listener for subscriber changes\n const unsubscribeFromCollectionEvents = collection.on(\n `subscribers:change`,\n ({ subscriberCount }) => {\n if (subscriberCount > 0) {\n subscribeToQuery()\n } else if (subscriberCount === 0) {\n unsubscribeFromQuery()\n }\n }\n )\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleQueryResult(localObserver.getCurrentResult())\n\n return async () => {\n unsubscribeFromCollectionEvents()\n unsubscribeFromQuery()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n *\n * Uses queryObserver.refetch() because:\n * - Bypasses `enabled: false` to support manual/imperative refetch patterns (e.g., button-triggered fetch)\n * - Ensures clearError() works even when enabled: false\n * - Always refetches THIS specific collection (exact targeting via observer)\n * - Respects retry, retryDelay, and other observer options\n *\n * This matches TanStack Query's hook behavior where refetch() bypasses enabled: false.\n * See: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries\n *\n * Used by both:\n * - utils.refetch() - for explicit user-triggered refetches\n * - Internal handlers (onInsert/onUpdate/onDelete) - after mutations to get fresh data\n *\n * @returns Promise that resolves when the refetch is complete, with QueryObserverResult\n */\n const refetch: RefetchFn = async (opts) => {\n // Observer is created when sync starts. If never synced, nothing to refetch.\n\n if (!state.queryObserver) {\n return\n }\n // Return the QueryObserverResult for users to inspect\n return state.queryObserver.refetch({\n throwOnError: opts?.throwOnError,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: any) => string | number\n begin: () => void\n write: (message: Omit<ChangeMessage<any>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: any) => string | number,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<any, string | number, any>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n // Create utils instance with state and dependencies passed explicitly\n const utils: any = new QueryCollectionUtilsImpl(state, refetch, writeUtils)\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils,\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
|
package/dist/cjs/query.d.cts
CHANGED
|
@@ -78,14 +78,24 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
78
78
|
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
79
79
|
writeBatch: (callback: () => void) => void;
|
|
80
80
|
/** Get the last error encountered by the query (if any); reset on success */
|
|
81
|
-
lastError:
|
|
81
|
+
lastError: TError | undefined;
|
|
82
82
|
/** Check if the collection is in an error state */
|
|
83
|
-
isError:
|
|
83
|
+
isError: boolean;
|
|
84
84
|
/**
|
|
85
85
|
* Get the number of consecutive sync failures.
|
|
86
86
|
* Incremented only when query fails completely (not per retry attempt); reset on success.
|
|
87
87
|
*/
|
|
88
|
-
errorCount:
|
|
88
|
+
errorCount: number;
|
|
89
|
+
/** Check if query is currently fetching (initial or background) */
|
|
90
|
+
isFetching: boolean;
|
|
91
|
+
/** Check if query is refetching in background (not initial fetch) */
|
|
92
|
+
isRefetching: boolean;
|
|
93
|
+
/** Check if query is loading for the first time (no data yet) */
|
|
94
|
+
isLoading: boolean;
|
|
95
|
+
/** Get timestamp of last successful data update (in milliseconds) */
|
|
96
|
+
dataUpdatedAt: number;
|
|
97
|
+
/** Get current fetch status */
|
|
98
|
+
fetchStatus: `fetching` | `paused` | `idle`;
|
|
89
99
|
/**
|
|
90
100
|
* Clear the error state and trigger a refetch of the query
|
|
91
101
|
* @returns Promise that resolves when the refetch completes successfully
|
|
@@ -181,26 +191,26 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
181
191
|
export declare function queryCollectionOptions<T extends StandardSchemaV1, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TQueryData = Awaited<ReturnType<TQueryFn>>>(config: QueryCollectionConfig<InferSchemaOutput<T>, TQueryFn, TError, TQueryKey, TKey, T> & {
|
|
182
192
|
schema: T;
|
|
183
193
|
select: (data: TQueryData) => Array<InferSchemaInput<T>>;
|
|
184
|
-
}): CollectionConfig<InferSchemaOutput<T>, TKey, T
|
|
194
|
+
}): CollectionConfig<InferSchemaOutput<T>, TKey, T, QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>> & {
|
|
185
195
|
schema: T;
|
|
186
196
|
utils: QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>;
|
|
187
197
|
};
|
|
188
198
|
export declare function queryCollectionOptions<T extends object, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (context: QueryFunctionContext<any>) => Promise<any>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TQueryData = Awaited<ReturnType<TQueryFn>>>(config: QueryCollectionConfig<T, TQueryFn, TError, TQueryKey, TKey, never, TQueryData> & {
|
|
189
199
|
schema?: never;
|
|
190
200
|
select: (data: TQueryData) => Array<T>;
|
|
191
|
-
}): CollectionConfig<T, TKey
|
|
201
|
+
}): CollectionConfig<T, TKey, never, QueryCollectionUtils<T, TKey, T, TError>> & {
|
|
192
202
|
schema?: never;
|
|
193
203
|
utils: QueryCollectionUtils<T, TKey, T, TError>;
|
|
194
204
|
};
|
|
195
205
|
export declare function queryCollectionOptions<T extends StandardSchemaV1, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<InferSchemaOutput<T>, (context: QueryFunctionContext<any>) => Promise<Array<InferSchemaOutput<T>>>, TError, TQueryKey, TKey, T> & {
|
|
196
206
|
schema: T;
|
|
197
|
-
}): CollectionConfig<InferSchemaOutput<T>, TKey, T
|
|
207
|
+
}): CollectionConfig<InferSchemaOutput<T>, TKey, T, QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>> & {
|
|
198
208
|
schema: T;
|
|
199
209
|
utils: QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>;
|
|
200
210
|
};
|
|
201
211
|
export declare function queryCollectionOptions<T extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<T, (context: QueryFunctionContext<any>) => Promise<Array<T>>, TError, TQueryKey, TKey> & {
|
|
202
212
|
schema?: never;
|
|
203
|
-
}): CollectionConfig<T, TKey
|
|
213
|
+
}): CollectionConfig<T, TKey, never, QueryCollectionUtils<T, TKey, T, TError>> & {
|
|
204
214
|
schema?: never;
|
|
205
215
|
utils: QueryCollectionUtils<T, TKey, T, TError>;
|
|
206
216
|
};
|
package/dist/esm/query.d.ts
CHANGED
|
@@ -78,14 +78,24 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
78
78
|
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
79
79
|
writeBatch: (callback: () => void) => void;
|
|
80
80
|
/** Get the last error encountered by the query (if any); reset on success */
|
|
81
|
-
lastError:
|
|
81
|
+
lastError: TError | undefined;
|
|
82
82
|
/** Check if the collection is in an error state */
|
|
83
|
-
isError:
|
|
83
|
+
isError: boolean;
|
|
84
84
|
/**
|
|
85
85
|
* Get the number of consecutive sync failures.
|
|
86
86
|
* Incremented only when query fails completely (not per retry attempt); reset on success.
|
|
87
87
|
*/
|
|
88
|
-
errorCount:
|
|
88
|
+
errorCount: number;
|
|
89
|
+
/** Check if query is currently fetching (initial or background) */
|
|
90
|
+
isFetching: boolean;
|
|
91
|
+
/** Check if query is refetching in background (not initial fetch) */
|
|
92
|
+
isRefetching: boolean;
|
|
93
|
+
/** Check if query is loading for the first time (no data yet) */
|
|
94
|
+
isLoading: boolean;
|
|
95
|
+
/** Get timestamp of last successful data update (in milliseconds) */
|
|
96
|
+
dataUpdatedAt: number;
|
|
97
|
+
/** Get current fetch status */
|
|
98
|
+
fetchStatus: `fetching` | `paused` | `idle`;
|
|
89
99
|
/**
|
|
90
100
|
* Clear the error state and trigger a refetch of the query
|
|
91
101
|
* @returns Promise that resolves when the refetch completes successfully
|
|
@@ -181,26 +191,26 @@ export interface QueryCollectionUtils<TItem extends object = Record<string, unkn
|
|
|
181
191
|
export declare function queryCollectionOptions<T extends StandardSchemaV1, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TQueryData = Awaited<ReturnType<TQueryFn>>>(config: QueryCollectionConfig<InferSchemaOutput<T>, TQueryFn, TError, TQueryKey, TKey, T> & {
|
|
182
192
|
schema: T;
|
|
183
193
|
select: (data: TQueryData) => Array<InferSchemaInput<T>>;
|
|
184
|
-
}): CollectionConfig<InferSchemaOutput<T>, TKey, T
|
|
194
|
+
}): CollectionConfig<InferSchemaOutput<T>, TKey, T, QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>> & {
|
|
185
195
|
schema: T;
|
|
186
196
|
utils: QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>;
|
|
187
197
|
};
|
|
188
198
|
export declare function queryCollectionOptions<T extends object, TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (context: QueryFunctionContext<any>) => Promise<any>, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TQueryData = Awaited<ReturnType<TQueryFn>>>(config: QueryCollectionConfig<T, TQueryFn, TError, TQueryKey, TKey, never, TQueryData> & {
|
|
189
199
|
schema?: never;
|
|
190
200
|
select: (data: TQueryData) => Array<T>;
|
|
191
|
-
}): CollectionConfig<T, TKey
|
|
201
|
+
}): CollectionConfig<T, TKey, never, QueryCollectionUtils<T, TKey, T, TError>> & {
|
|
192
202
|
schema?: never;
|
|
193
203
|
utils: QueryCollectionUtils<T, TKey, T, TError>;
|
|
194
204
|
};
|
|
195
205
|
export declare function queryCollectionOptions<T extends StandardSchemaV1, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<InferSchemaOutput<T>, (context: QueryFunctionContext<any>) => Promise<Array<InferSchemaOutput<T>>>, TError, TQueryKey, TKey, T> & {
|
|
196
206
|
schema: T;
|
|
197
|
-
}): CollectionConfig<InferSchemaOutput<T>, TKey, T
|
|
207
|
+
}): CollectionConfig<InferSchemaOutput<T>, TKey, T, QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>> & {
|
|
198
208
|
schema: T;
|
|
199
209
|
utils: QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>;
|
|
200
210
|
};
|
|
201
211
|
export declare function queryCollectionOptions<T extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number>(config: QueryCollectionConfig<T, (context: QueryFunctionContext<any>) => Promise<Array<T>>, TError, TQueryKey, TKey> & {
|
|
202
212
|
schema?: never;
|
|
203
|
-
}): CollectionConfig<T, TKey
|
|
213
|
+
}): CollectionConfig<T, TKey, never, QueryCollectionUtils<T, TKey, T, TError>> & {
|
|
204
214
|
schema?: never;
|
|
205
215
|
utils: QueryCollectionUtils<T, TKey, T, TError>;
|
|
206
216
|
};
|
package/dist/esm/query.js
CHANGED
|
@@ -1,6 +1,50 @@
|
|
|
1
1
|
import { QueryObserver } from "@tanstack/query-core";
|
|
2
2
|
import { QueryKeyRequiredError, QueryFnRequiredError, QueryClientRequiredError, GetKeyRequiredError } from "./errors.js";
|
|
3
3
|
import { createWriteUtils } from "./manual-sync.js";
|
|
4
|
+
class QueryCollectionUtilsImpl {
|
|
5
|
+
constructor(state, refetch, writeUtils) {
|
|
6
|
+
this.state = state;
|
|
7
|
+
this.refetchFn = refetch;
|
|
8
|
+
this.refetch = refetch;
|
|
9
|
+
this.writeInsert = writeUtils.writeInsert;
|
|
10
|
+
this.writeUpdate = writeUtils.writeUpdate;
|
|
11
|
+
this.writeDelete = writeUtils.writeDelete;
|
|
12
|
+
this.writeUpsert = writeUtils.writeUpsert;
|
|
13
|
+
this.writeBatch = writeUtils.writeBatch;
|
|
14
|
+
}
|
|
15
|
+
async clearError() {
|
|
16
|
+
this.state.lastError = void 0;
|
|
17
|
+
this.state.errorCount = 0;
|
|
18
|
+
this.state.lastErrorUpdatedAt = 0;
|
|
19
|
+
await this.refetchFn({ throwOnError: true });
|
|
20
|
+
}
|
|
21
|
+
// Getters for error state
|
|
22
|
+
get lastError() {
|
|
23
|
+
return this.state.lastError;
|
|
24
|
+
}
|
|
25
|
+
get isError() {
|
|
26
|
+
return !!this.state.lastError;
|
|
27
|
+
}
|
|
28
|
+
get errorCount() {
|
|
29
|
+
return this.state.errorCount;
|
|
30
|
+
}
|
|
31
|
+
// Getters for QueryObserver state
|
|
32
|
+
get isFetching() {
|
|
33
|
+
return this.state.queryObserver?.getCurrentResult().isFetching ?? false;
|
|
34
|
+
}
|
|
35
|
+
get isRefetching() {
|
|
36
|
+
return this.state.queryObserver?.getCurrentResult().isRefetching ?? false;
|
|
37
|
+
}
|
|
38
|
+
get isLoading() {
|
|
39
|
+
return this.state.queryObserver?.getCurrentResult().isLoading ?? false;
|
|
40
|
+
}
|
|
41
|
+
get dataUpdatedAt() {
|
|
42
|
+
return this.state.queryObserver?.getCurrentResult().dataUpdatedAt ?? 0;
|
|
43
|
+
}
|
|
44
|
+
get fetchStatus() {
|
|
45
|
+
return this.state.queryObserver?.getCurrentResult().fetchStatus ?? `idle`;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
4
48
|
function queryCollectionOptions(config) {
|
|
5
49
|
const {
|
|
6
50
|
queryKey,
|
|
@@ -31,10 +75,12 @@ function queryCollectionOptions(config) {
|
|
|
31
75
|
if (!getKey) {
|
|
32
76
|
throw new GetKeyRequiredError();
|
|
33
77
|
}
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
78
|
+
const state = {
|
|
79
|
+
lastError: void 0,
|
|
80
|
+
errorCount: 0,
|
|
81
|
+
lastErrorUpdatedAt: 0,
|
|
82
|
+
queryObserver: void 0
|
|
83
|
+
};
|
|
38
84
|
const internalSync = (params) => {
|
|
39
85
|
const { begin, write, commit, markReady, collection } = params;
|
|
40
86
|
const observerOptions = {
|
|
@@ -51,13 +97,13 @@ function queryCollectionOptions(config) {
|
|
|
51
97
|
...staleTime !== void 0 && { staleTime }
|
|
52
98
|
};
|
|
53
99
|
const localObserver = new QueryObserver(queryClient, observerOptions);
|
|
54
|
-
queryObserver = localObserver;
|
|
100
|
+
state.queryObserver = localObserver;
|
|
55
101
|
let isSubscribed = false;
|
|
56
102
|
let actualUnsubscribeFn = null;
|
|
57
103
|
const handleQueryResult = (result) => {
|
|
58
104
|
if (result.isSuccess) {
|
|
59
|
-
lastError = void 0;
|
|
60
|
-
errorCount = 0;
|
|
105
|
+
state.lastError = void 0;
|
|
106
|
+
state.errorCount = 0;
|
|
61
107
|
const rawData = result.data;
|
|
62
108
|
const newItemsArray = select ? select(rawData) : rawData;
|
|
63
109
|
if (!Array.isArray(newItemsArray) || newItemsArray.some((item) => typeof item !== `object`)) {
|
|
@@ -102,10 +148,10 @@ function queryCollectionOptions(config) {
|
|
|
102
148
|
commit();
|
|
103
149
|
markReady();
|
|
104
150
|
} else if (result.isError) {
|
|
105
|
-
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
|
|
106
|
-
lastError = result.error;
|
|
107
|
-
errorCount++;
|
|
108
|
-
lastErrorUpdatedAt = result.errorUpdatedAt;
|
|
151
|
+
if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {
|
|
152
|
+
state.lastError = result.error;
|
|
153
|
+
state.errorCount++;
|
|
154
|
+
state.lastErrorUpdatedAt = result.errorUpdatedAt;
|
|
109
155
|
}
|
|
110
156
|
console.error(
|
|
111
157
|
`[QueryCollection] Error observing query ${String(queryKey)}:`,
|
|
@@ -147,10 +193,10 @@ function queryCollectionOptions(config) {
|
|
|
147
193
|
};
|
|
148
194
|
};
|
|
149
195
|
const refetch = async (opts) => {
|
|
150
|
-
if (!queryObserver) {
|
|
196
|
+
if (!state.queryObserver) {
|
|
151
197
|
return;
|
|
152
198
|
}
|
|
153
|
-
return queryObserver.refetch({
|
|
199
|
+
return state.queryObserver.refetch({
|
|
154
200
|
throwOnError: opts?.throwOnError
|
|
155
201
|
});
|
|
156
202
|
};
|
|
@@ -195,6 +241,7 @@ function queryCollectionOptions(config) {
|
|
|
195
241
|
}
|
|
196
242
|
return handlerResult;
|
|
197
243
|
} : void 0;
|
|
244
|
+
const utils = new QueryCollectionUtilsImpl(state, refetch, writeUtils);
|
|
198
245
|
return {
|
|
199
246
|
...baseCollectionConfig,
|
|
200
247
|
getKey,
|
|
@@ -202,19 +249,7 @@ function queryCollectionOptions(config) {
|
|
|
202
249
|
onInsert: wrappedOnInsert,
|
|
203
250
|
onUpdate: wrappedOnUpdate,
|
|
204
251
|
onDelete: wrappedOnDelete,
|
|
205
|
-
utils
|
|
206
|
-
refetch,
|
|
207
|
-
...writeUtils,
|
|
208
|
-
lastError: () => lastError,
|
|
209
|
-
isError: () => !!lastError,
|
|
210
|
-
errorCount: () => errorCount,
|
|
211
|
-
clearError: async () => {
|
|
212
|
-
lastError = void 0;
|
|
213
|
-
errorCount = 0;
|
|
214
|
-
lastErrorUpdatedAt = 0;
|
|
215
|
-
await refetch({ throwOnError: true });
|
|
216
|
-
}
|
|
217
|
-
}
|
|
252
|
+
utils
|
|
218
253
|
};
|
|
219
254
|
}
|
|
220
255
|
export {
|
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 QueryObserverResult,\n} from \"@tanstack/query-core\"\nimport type {\n BaseCollectionConfig,\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Schema input type inference helper (matches electric.ts pattern)\ntype InferSchemaInput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferInput<T> extends object\n ? StandardSchemaV1.InferInput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration options for creating a Query Collection\n * @template T - The explicit type of items stored in the collection\n * @template TQueryFn - The queryFn type\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TSchema - The schema type for validation\n */\nexport interface QueryCollectionConfig<\n T extends object = object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = never,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n> extends BaseCollectionConfig<T, TKey, TSchema> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>\n : TQueryFn\n /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */\n select?: (data: TQueryData) => Array<T>\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`staleTime`]\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n * Returns the QueryObserverResult from TanStack Query\n */\nexport type RefetchFn = (opts?: {\n throwOnError?: boolean\n}) => Promise<QueryObserverResult<any, any> | void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n * @template TError - The type of errors that can occur during queries\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n TError = unknown,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n /** Get the last error encountered by the query (if any); reset on success */\n lastError: () => TError | undefined\n /** Check if the collection is in an error state */\n isError: () => boolean\n /**\n * Get the number of consecutive sync failures.\n * Incremented only when query fails completely (not per retry attempt); reset on success.\n */\n errorCount: () => number\n /**\n * Clear the error state and trigger a refetch of the query\n * @returns Promise that resolves when the refetch completes successfully\n * @throws Error if the refetch fails\n */\n clearError: () => Promise<void>\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Schema inference (highest priority)\n * 2. QueryFn return type inference (second priority)\n *\n * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n *\n * @example\n * // The select option extracts the items array from a response with metadata\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * select: (data) => data.items, // Extract the array of items\n * queryClient,\n * schema: todoSchema,\n * getKey: (item) => item.id,\n * })\n * )\n */\n// Overload for when schema is provided and select present\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n select: (data: TQueryData) => Array<InferSchemaInput<T>>\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided and select present\nexport function queryCollectionOptions<\n T extends object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n T,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n never,\n TQueryData\n > & {\n schema?: never // prohibit schema\n select: (data: TQueryData) => Array<T>\n }\n): CollectionConfig<T, TKey> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\n// Overload for when schema is provided\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n (\n context: QueryFunctionContext<any>\n ) => Promise<Array<InferSchemaOutput<T>>>,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n }\n): CollectionConfig<InferSchemaOutput<T>, TKey, T> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided\nexport function queryCollectionOptions<\n T extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n T,\n (context: QueryFunctionContext<any>) => Promise<Array<T>>,\n TError,\n TQueryKey,\n TKey\n > & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<T, TKey> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\nexport function queryCollectionOptions(\n config: QueryCollectionConfig<Record<string, unknown>>\n): CollectionConfig & {\n utils: QueryCollectionUtils\n} {\n const {\n queryKey,\n queryFn,\n select,\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** The last error encountered by the query */\n let lastError: any\n /** The number of consecutive sync failures */\n let errorCount = 0\n /** The timestamp for when the query most recently returned the status as \"error\" */\n let lastErrorUpdatedAt = 0\n /** Reference to the QueryObserver for imperative refetch */\n let queryObserver: QueryObserver<Array<any>, any, Array<any>, Array<any>, any>\n\n const internalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used\n ...(meta !== undefined && { meta }),\n ...(enabled !== undefined && { enabled }),\n ...(refetchInterval !== undefined && { refetchInterval }),\n ...(retry !== undefined && { retry }),\n ...(retryDelay !== undefined && { retryDelay }),\n ...(staleTime !== undefined && { staleTime }),\n }\n\n const localObserver = new QueryObserver<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n >(queryClient, observerOptions)\n\n // Store reference for imperative refetch\n queryObserver = localObserver\n\n let isSubscribed = false\n let actualUnsubscribeFn: (() => void) | null = null\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleQueryResult: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n lastError = undefined\n errorCount = 0\n\n const rawData = result.data\n const newItemsArray = select ? select(rawData) : rawData\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n const errorMessage = select\n ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n\n console.error(errorMessage)\n return\n }\n\n const currentSyncedItems: Map<string | number, any> = new Map(\n collection._state.syncedData.entries()\n )\n const newItemsMap = new Map<string | number, any>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== lastErrorUpdatedAt) {\n lastError = result.error\n errorCount++\n lastErrorUpdatedAt = result.errorUpdatedAt\n }\n\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n }\n\n const subscribeToQuery = () => {\n if (!isSubscribed) {\n actualUnsubscribeFn = localObserver.subscribe(handleQueryResult)\n isSubscribed = true\n }\n }\n\n const unsubscribeFromQuery = () => {\n if (isSubscribed && actualUnsubscribeFn) {\n actualUnsubscribeFn()\n actualUnsubscribeFn = null\n isSubscribed = false\n }\n }\n\n // Always subscribe when sync starts (this could be from preload(), startSync config, or first subscriber)\n // We'll dynamically unsubscribe/resubscribe based on subscriber count to maintain staleTime behavior\n subscribeToQuery()\n\n // Set up event listener for subscriber changes\n const unsubscribeFromCollectionEvents = collection.on(\n `subscribers:change`,\n ({ subscriberCount }) => {\n if (subscriberCount > 0) {\n subscribeToQuery()\n } else if (subscriberCount === 0) {\n unsubscribeFromQuery()\n }\n }\n )\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleQueryResult(localObserver.getCurrentResult())\n\n return async () => {\n unsubscribeFromCollectionEvents()\n unsubscribeFromQuery()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n *\n * Uses queryObserver.refetch() because:\n * - Bypasses `enabled: false` to support manual/imperative refetch patterns (e.g., button-triggered fetch)\n * - Ensures clearError() works even when enabled: false\n * - Always refetches THIS specific collection (exact targeting via observer)\n * - Respects retry, retryDelay, and other observer options\n *\n * This matches TanStack Query's hook behavior where refetch() bypasses enabled: false.\n * See: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries\n *\n * Used by both:\n * - utils.refetch() - for explicit user-triggered refetches\n * - Internal handlers (onInsert/onUpdate/onDelete) - after mutations to get fresh data\n *\n * @returns Promise that resolves when the refetch is complete, with QueryObserverResult\n */\n const refetch: RefetchFn = async (opts) => {\n // Observer is created when sync starts. If never synced, nothing to refetch.\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryObserver) {\n return\n }\n // Return the QueryObserverResult for users to inspect\n return queryObserver.refetch({\n throwOnError: opts?.throwOnError,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: any) => string | number\n begin: () => void\n write: (message: Omit<ChangeMessage<any>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: any) => string | number,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<any, string | number, any>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n lastError: () => lastError,\n isError: () => !!lastError,\n errorCount: () => errorCount,\n clearError: async () => {\n lastError = undefined\n errorCount = 0\n lastErrorUpdatedAt = 0\n await refetch({ throwOnError: true })\n },\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 QueryObserverResult,\n} from \"@tanstack/query-core\"\nimport type {\n BaseCollectionConfig,\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFnParams,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\nimport type { StandardSchemaV1 } from \"@standard-schema/spec\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n// Schema output type inference helper (matches electric.ts pattern)\ntype InferSchemaOutput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferOutput<T> extends object\n ? StandardSchemaV1.InferOutput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n// Schema input type inference helper (matches electric.ts pattern)\ntype InferSchemaInput<T> = T extends StandardSchemaV1\n ? StandardSchemaV1.InferInput<T> extends object\n ? StandardSchemaV1.InferInput<T>\n : Record<string, unknown>\n : Record<string, unknown>\n\n/**\n * Configuration options for creating a Query Collection\n * @template T - The explicit type of items stored in the collection\n * @template TQueryFn - The queryFn type\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @template TSchema - The schema type for validation\n */\nexport interface QueryCollectionConfig<\n T extends object = object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TSchema extends StandardSchemaV1 = never,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n> extends BaseCollectionConfig<T, TKey, TSchema> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: TQueryFn extends (\n context: QueryFunctionContext<TQueryKey>\n ) => Promise<Array<any>>\n ? (context: QueryFunctionContext<TQueryKey>) => Promise<Array<T>>\n : TQueryFn\n /* Function that extracts array items from wrapped API responses (e.g metadata, pagination) */\n select?: (data: TQueryData) => Array<T>\n /** The TanStack Query client instance */\n queryClient: QueryClient\n\n // Query-specific options\n /** Whether the query should automatically run (default: true) */\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<T>,\n TError,\n Array<T>,\n Array<T>,\n TQueryKey\n >[`staleTime`]\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n * Returns the QueryObserverResult from TanStack Query\n */\nexport type RefetchFn = (opts?: {\n throwOnError?: boolean\n}) => Promise<QueryObserverResult<any, any> | 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\n // Query Observer State (getters)\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 /** Check if query is currently fetching (initial or background) */\n isFetching: boolean\n /** Check if query is refetching in background (not initial fetch) */\n isRefetching: boolean\n /** Check if query is loading for the first time (no data yet) */\n isLoading: boolean\n /** Get timestamp of last successful data update (in milliseconds) */\n dataUpdatedAt: number\n /** Get current fetch status */\n fetchStatus: `fetching` | `paused` | `idle`\n\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 * Internal state object for tracking query observer and errors\n */\ninterface QueryCollectionState {\n lastError: any\n errorCount: number\n lastErrorUpdatedAt: number\n queryObserver:\n | QueryObserver<Array<any>, any, Array<any>, Array<any>, any>\n | undefined\n}\n\n/**\n * Implementation class for QueryCollectionUtils with explicit dependency injection\n * for better testability and architectural clarity\n */\nclass QueryCollectionUtilsImpl {\n private state: QueryCollectionState\n private refetchFn: RefetchFn\n\n // Write methods\n public refetch: RefetchFn\n public writeInsert: any\n public writeUpdate: any\n public writeDelete: any\n public writeUpsert: any\n public writeBatch: any\n\n constructor(\n state: QueryCollectionState,\n refetch: RefetchFn,\n writeUtils: ReturnType<typeof createWriteUtils>\n ) {\n this.state = state\n this.refetchFn = refetch\n\n // Initialize methods to use passed dependencies\n this.refetch = refetch\n this.writeInsert = writeUtils.writeInsert\n this.writeUpdate = writeUtils.writeUpdate\n this.writeDelete = writeUtils.writeDelete\n this.writeUpsert = writeUtils.writeUpsert\n this.writeBatch = writeUtils.writeBatch\n }\n\n public async clearError() {\n this.state.lastError = undefined\n this.state.errorCount = 0\n this.state.lastErrorUpdatedAt = 0\n await this.refetchFn({ throwOnError: true })\n }\n\n // Getters for error state\n public get lastError() {\n return this.state.lastError\n }\n\n public get isError() {\n return !!this.state.lastError\n }\n\n public get errorCount() {\n return this.state.errorCount\n }\n\n // Getters for QueryObserver state\n public get isFetching() {\n return this.state.queryObserver?.getCurrentResult().isFetching ?? false\n }\n\n public get isRefetching() {\n return this.state.queryObserver?.getCurrentResult().isRefetching ?? false\n }\n\n public get isLoading() {\n return this.state.queryObserver?.getCurrentResult().isLoading ?? false\n }\n\n public get dataUpdatedAt() {\n return this.state.queryObserver?.getCurrentResult().dataUpdatedAt ?? 0\n }\n\n public get fetchStatus() {\n return this.state.queryObserver?.getCurrentResult().fetchStatus ?? `idle`\n }\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * Supports automatic type inference following the priority order:\n * 1. Schema inference (highest priority)\n * 2. QueryFn return type inference (second priority)\n *\n * @template T - Type of the schema if a schema is provided otherwise it is the type of the values returned by the queryFn\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n * @template TKey - The type of the item keys\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Type inferred from queryFn return type (NEW!)\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => {\n * const response = await fetch('/api/todos')\n * return response.json() as Todo[] // Type automatically inferred!\n * },\n * queryClient,\n * getKey: (item) => item.id, // item is typed as Todo\n * })\n * )\n *\n * @example\n * // Explicit type\n * const todosCollection = createCollection<Todo>(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // Schema inference\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * queryClient,\n * schema: todoSchema, // Type inferred from schema\n * getKey: (item) => item.id,\n * })\n * )\n *\n * @example\n * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n *\n * @example\n * // The select option extracts the items array from a response with metadata\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: async () => fetch('/api/todos').then(r => r.json()),\n * select: (data) => data.items, // Extract the array of items\n * queryClient,\n * schema: todoSchema,\n * getKey: (item) => item.id,\n * })\n * )\n */\n// Overload for when schema is provided and select present\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n select: (data: TQueryData) => Array<InferSchemaInput<T>>\n }\n): CollectionConfig<\n InferSchemaOutput<T>,\n TKey,\n T,\n QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>\n> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided and select present\nexport function queryCollectionOptions<\n T extends object,\n TQueryFn extends (context: QueryFunctionContext<any>) => Promise<any> = (\n context: QueryFunctionContext<any>\n ) => Promise<any>,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TQueryData = Awaited<ReturnType<TQueryFn>>,\n>(\n config: QueryCollectionConfig<\n T,\n TQueryFn,\n TError,\n TQueryKey,\n TKey,\n never,\n TQueryData\n > & {\n schema?: never // prohibit schema\n select: (data: TQueryData) => Array<T>\n }\n): CollectionConfig<\n T,\n TKey,\n never,\n QueryCollectionUtils<T, TKey, T, TError>\n> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\n// Overload for when schema is provided\nexport function queryCollectionOptions<\n T extends StandardSchemaV1,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n InferSchemaOutput<T>,\n (\n context: QueryFunctionContext<any>\n ) => Promise<Array<InferSchemaOutput<T>>>,\n TError,\n TQueryKey,\n TKey,\n T\n > & {\n schema: T\n }\n): CollectionConfig<\n InferSchemaOutput<T>,\n TKey,\n T,\n QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>\n> & {\n schema: T\n utils: QueryCollectionUtils<\n InferSchemaOutput<T>,\n TKey,\n InferSchemaInput<T>,\n TError\n >\n}\n\n// Overload for when no schema is provided\nexport function queryCollectionOptions<\n T extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n>(\n config: QueryCollectionConfig<\n T,\n (context: QueryFunctionContext<any>) => Promise<Array<T>>,\n TError,\n TQueryKey,\n TKey\n > & {\n schema?: never // prohibit schema\n }\n): CollectionConfig<\n T,\n TKey,\n never,\n QueryCollectionUtils<T, TKey, T, TError>\n> & {\n schema?: never // no schema in the result\n utils: QueryCollectionUtils<T, TKey, T, TError>\n}\n\nexport function queryCollectionOptions(\n config: QueryCollectionConfig<Record<string, unknown>>\n): CollectionConfig<\n Record<string, unknown>,\n string | number,\n never,\n QueryCollectionUtils\n> & {\n utils: QueryCollectionUtils\n} {\n const {\n queryKey,\n queryFn,\n select,\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 // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n /** State object to hold error tracking and observer reference */\n const state: QueryCollectionState = {\n lastError: undefined as any,\n errorCount: 0,\n lastErrorUpdatedAt: 0,\n queryObserver: undefined,\n }\n\n const internalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n // Only include options that are explicitly defined to allow QueryClient defaultOptions to be used\n ...(meta !== undefined && { meta }),\n ...(enabled !== undefined && { enabled }),\n ...(refetchInterval !== undefined && { refetchInterval }),\n ...(retry !== undefined && { retry }),\n ...(retryDelay !== undefined && { retryDelay }),\n ...(staleTime !== undefined && { staleTime }),\n }\n\n const localObserver = new QueryObserver<\n Array<any>,\n any,\n Array<any>,\n Array<any>,\n any\n >(queryClient, observerOptions)\n\n // Store reference for imperative refetch\n state.queryObserver = localObserver\n\n let isSubscribed = false\n let actualUnsubscribeFn: (() => void) | null = null\n\n type UpdateHandler = Parameters<typeof localObserver.subscribe>[0]\n const handleQueryResult: UpdateHandler = (result) => {\n if (result.isSuccess) {\n // Clear error state\n state.lastError = undefined\n state.errorCount = 0\n\n const rawData = result.data\n const newItemsArray = select ? select(rawData) : rawData\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n const errorMessage = select\n ? `@tanstack/query-db-collection: select() must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n : `@tanstack/query-db-collection: queryFn must return an array of objects. Got: ${typeof newItemsArray} for queryKey ${JSON.stringify(queryKey)}`\n\n console.error(errorMessage)\n return\n }\n\n const currentSyncedItems: Map<string | number, any> = new Map(\n collection._state.syncedData.entries()\n )\n const newItemsMap = new Map<string | number, any>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {\n state.lastError = result.error\n state.errorCount++\n state.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 subscribeToQuery = () => {\n if (!isSubscribed) {\n actualUnsubscribeFn = localObserver.subscribe(handleQueryResult)\n isSubscribed = true\n }\n }\n\n const unsubscribeFromQuery = () => {\n if (isSubscribed && actualUnsubscribeFn) {\n actualUnsubscribeFn()\n actualUnsubscribeFn = null\n isSubscribed = false\n }\n }\n\n // Always subscribe when sync starts (this could be from preload(), startSync config, or first subscriber)\n // We'll dynamically unsubscribe/resubscribe based on subscriber count to maintain staleTime behavior\n subscribeToQuery()\n\n // Set up event listener for subscriber changes\n const unsubscribeFromCollectionEvents = collection.on(\n `subscribers:change`,\n ({ subscriberCount }) => {\n if (subscriberCount > 0) {\n subscribeToQuery()\n } else if (subscriberCount === 0) {\n unsubscribeFromQuery()\n }\n }\n )\n\n // Ensure we process any existing query data (QueryObserver doesn't invoke its callback automatically with initial\n // state)\n handleQueryResult(localObserver.getCurrentResult())\n\n return async () => {\n unsubscribeFromCollectionEvents()\n unsubscribeFromQuery()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n *\n * Uses queryObserver.refetch() because:\n * - Bypasses `enabled: false` to support manual/imperative refetch patterns (e.g., button-triggered fetch)\n * - Ensures clearError() works even when enabled: false\n * - Always refetches THIS specific collection (exact targeting via observer)\n * - Respects retry, retryDelay, and other observer options\n *\n * This matches TanStack Query's hook behavior where refetch() bypasses enabled: false.\n * See: https://tanstack.com/query/latest/docs/framework/react/guides/disabling-queries\n *\n * Used by both:\n * - utils.refetch() - for explicit user-triggered refetches\n * - Internal handlers (onInsert/onUpdate/onDelete) - after mutations to get fresh data\n *\n * @returns Promise that resolves when the refetch is complete, with QueryObserverResult\n */\n const refetch: RefetchFn = async (opts) => {\n // Observer is created when sync starts. If never synced, nothing to refetch.\n\n if (!state.queryObserver) {\n return\n }\n // Return the QueryObserverResult for users to inspect\n return state.queryObserver.refetch({\n throwOnError: opts?.throwOnError,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: any) => string | number\n begin: () => void\n write: (message: Omit<ChangeMessage<any>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<any>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: any) => string | number,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<any, string | number, any>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<any>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<any>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<any>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n // Create utils instance with state and dependencies passed explicitly\n const utils: any = new QueryCollectionUtilsImpl(state, refetch, writeUtils)\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils,\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/query-db-collection",
|
|
3
3
|
"description": "TanStack Query collection for TanStack DB",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.3.0",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@standard-schema/spec": "^1.0.0"
|
|
7
|
-
"@tanstack/db": "0.4.19"
|
|
6
|
+
"@standard-schema/spec": "^1.0.0"
|
|
8
7
|
},
|
|
9
8
|
"devDependencies": {
|
|
10
9
|
"@tanstack/query-core": "^5.90.5",
|
|
11
|
-
"@vitest/coverage-istanbul": "^3.2.4"
|
|
10
|
+
"@vitest/coverage-istanbul": "^3.2.4",
|
|
11
|
+
"@tanstack/db": "0.4.20"
|
|
12
12
|
},
|
|
13
13
|
"exports": {
|
|
14
14
|
".": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"main": "dist/cjs/index.cjs",
|
|
31
31
|
"module": "dist/esm/index.js",
|
|
32
32
|
"peerDependencies": {
|
|
33
|
+
"@tanstack/db": "*",
|
|
33
34
|
"@tanstack/query-core": "^5.0.0",
|
|
34
35
|
"typescript": ">=4.7"
|
|
35
36
|
},
|
package/src/query.ts
CHANGED
|
@@ -164,15 +164,28 @@ export interface QueryCollectionUtils<
|
|
|
164
164
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void
|
|
165
165
|
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
166
166
|
writeBatch: (callback: () => void) => void
|
|
167
|
+
|
|
168
|
+
// Query Observer State (getters)
|
|
167
169
|
/** Get the last error encountered by the query (if any); reset on success */
|
|
168
|
-
lastError:
|
|
170
|
+
lastError: TError | undefined
|
|
169
171
|
/** Check if the collection is in an error state */
|
|
170
|
-
isError:
|
|
172
|
+
isError: boolean
|
|
171
173
|
/**
|
|
172
174
|
* Get the number of consecutive sync failures.
|
|
173
175
|
* Incremented only when query fails completely (not per retry attempt); reset on success.
|
|
174
176
|
*/
|
|
175
|
-
errorCount:
|
|
177
|
+
errorCount: number
|
|
178
|
+
/** Check if query is currently fetching (initial or background) */
|
|
179
|
+
isFetching: boolean
|
|
180
|
+
/** Check if query is refetching in background (not initial fetch) */
|
|
181
|
+
isRefetching: boolean
|
|
182
|
+
/** Check if query is loading for the first time (no data yet) */
|
|
183
|
+
isLoading: boolean
|
|
184
|
+
/** Get timestamp of last successful data update (in milliseconds) */
|
|
185
|
+
dataUpdatedAt: number
|
|
186
|
+
/** Get current fetch status */
|
|
187
|
+
fetchStatus: `fetching` | `paused` | `idle`
|
|
188
|
+
|
|
176
189
|
/**
|
|
177
190
|
* Clear the error state and trigger a refetch of the query
|
|
178
191
|
* @returns Promise that resolves when the refetch completes successfully
|
|
@@ -181,6 +194,93 @@ export interface QueryCollectionUtils<
|
|
|
181
194
|
clearError: () => Promise<void>
|
|
182
195
|
}
|
|
183
196
|
|
|
197
|
+
/**
|
|
198
|
+
* Internal state object for tracking query observer and errors
|
|
199
|
+
*/
|
|
200
|
+
interface QueryCollectionState {
|
|
201
|
+
lastError: any
|
|
202
|
+
errorCount: number
|
|
203
|
+
lastErrorUpdatedAt: number
|
|
204
|
+
queryObserver:
|
|
205
|
+
| QueryObserver<Array<any>, any, Array<any>, Array<any>, any>
|
|
206
|
+
| undefined
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* Implementation class for QueryCollectionUtils with explicit dependency injection
|
|
211
|
+
* for better testability and architectural clarity
|
|
212
|
+
*/
|
|
213
|
+
class QueryCollectionUtilsImpl {
|
|
214
|
+
private state: QueryCollectionState
|
|
215
|
+
private refetchFn: RefetchFn
|
|
216
|
+
|
|
217
|
+
// Write methods
|
|
218
|
+
public refetch: RefetchFn
|
|
219
|
+
public writeInsert: any
|
|
220
|
+
public writeUpdate: any
|
|
221
|
+
public writeDelete: any
|
|
222
|
+
public writeUpsert: any
|
|
223
|
+
public writeBatch: any
|
|
224
|
+
|
|
225
|
+
constructor(
|
|
226
|
+
state: QueryCollectionState,
|
|
227
|
+
refetch: RefetchFn,
|
|
228
|
+
writeUtils: ReturnType<typeof createWriteUtils>
|
|
229
|
+
) {
|
|
230
|
+
this.state = state
|
|
231
|
+
this.refetchFn = refetch
|
|
232
|
+
|
|
233
|
+
// Initialize methods to use passed dependencies
|
|
234
|
+
this.refetch = refetch
|
|
235
|
+
this.writeInsert = writeUtils.writeInsert
|
|
236
|
+
this.writeUpdate = writeUtils.writeUpdate
|
|
237
|
+
this.writeDelete = writeUtils.writeDelete
|
|
238
|
+
this.writeUpsert = writeUtils.writeUpsert
|
|
239
|
+
this.writeBatch = writeUtils.writeBatch
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
public async clearError() {
|
|
243
|
+
this.state.lastError = undefined
|
|
244
|
+
this.state.errorCount = 0
|
|
245
|
+
this.state.lastErrorUpdatedAt = 0
|
|
246
|
+
await this.refetchFn({ throwOnError: true })
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// Getters for error state
|
|
250
|
+
public get lastError() {
|
|
251
|
+
return this.state.lastError
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
public get isError() {
|
|
255
|
+
return !!this.state.lastError
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
public get errorCount() {
|
|
259
|
+
return this.state.errorCount
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// Getters for QueryObserver state
|
|
263
|
+
public get isFetching() {
|
|
264
|
+
return this.state.queryObserver?.getCurrentResult().isFetching ?? false
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
public get isRefetching() {
|
|
268
|
+
return this.state.queryObserver?.getCurrentResult().isRefetching ?? false
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
public get isLoading() {
|
|
272
|
+
return this.state.queryObserver?.getCurrentResult().isLoading ?? false
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
public get dataUpdatedAt() {
|
|
276
|
+
return this.state.queryObserver?.getCurrentResult().dataUpdatedAt ?? 0
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
public get fetchStatus() {
|
|
280
|
+
return this.state.queryObserver?.getCurrentResult().fetchStatus ?? `idle`
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
184
284
|
/**
|
|
185
285
|
* Creates query collection options for use with a standard Collection.
|
|
186
286
|
* This integrates TanStack Query with TanStack DB for automatic synchronization.
|
|
@@ -286,7 +386,12 @@ export function queryCollectionOptions<
|
|
|
286
386
|
schema: T
|
|
287
387
|
select: (data: TQueryData) => Array<InferSchemaInput<T>>
|
|
288
388
|
}
|
|
289
|
-
): CollectionConfig<
|
|
389
|
+
): CollectionConfig<
|
|
390
|
+
InferSchemaOutput<T>,
|
|
391
|
+
TKey,
|
|
392
|
+
T,
|
|
393
|
+
QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>
|
|
394
|
+
> & {
|
|
290
395
|
schema: T
|
|
291
396
|
utils: QueryCollectionUtils<
|
|
292
397
|
InferSchemaOutput<T>,
|
|
@@ -319,7 +424,12 @@ export function queryCollectionOptions<
|
|
|
319
424
|
schema?: never // prohibit schema
|
|
320
425
|
select: (data: TQueryData) => Array<T>
|
|
321
426
|
}
|
|
322
|
-
): CollectionConfig<
|
|
427
|
+
): CollectionConfig<
|
|
428
|
+
T,
|
|
429
|
+
TKey,
|
|
430
|
+
never,
|
|
431
|
+
QueryCollectionUtils<T, TKey, T, TError>
|
|
432
|
+
> & {
|
|
323
433
|
schema?: never // no schema in the result
|
|
324
434
|
utils: QueryCollectionUtils<T, TKey, T, TError>
|
|
325
435
|
}
|
|
@@ -343,7 +453,12 @@ export function queryCollectionOptions<
|
|
|
343
453
|
> & {
|
|
344
454
|
schema: T
|
|
345
455
|
}
|
|
346
|
-
): CollectionConfig<
|
|
456
|
+
): CollectionConfig<
|
|
457
|
+
InferSchemaOutput<T>,
|
|
458
|
+
TKey,
|
|
459
|
+
T,
|
|
460
|
+
QueryCollectionUtils<InferSchemaOutput<T>, TKey, InferSchemaInput<T>, TError>
|
|
461
|
+
> & {
|
|
347
462
|
schema: T
|
|
348
463
|
utils: QueryCollectionUtils<
|
|
349
464
|
InferSchemaOutput<T>,
|
|
@@ -369,14 +484,24 @@ export function queryCollectionOptions<
|
|
|
369
484
|
> & {
|
|
370
485
|
schema?: never // prohibit schema
|
|
371
486
|
}
|
|
372
|
-
): CollectionConfig<
|
|
487
|
+
): CollectionConfig<
|
|
488
|
+
T,
|
|
489
|
+
TKey,
|
|
490
|
+
never,
|
|
491
|
+
QueryCollectionUtils<T, TKey, T, TError>
|
|
492
|
+
> & {
|
|
373
493
|
schema?: never // no schema in the result
|
|
374
494
|
utils: QueryCollectionUtils<T, TKey, T, TError>
|
|
375
495
|
}
|
|
376
496
|
|
|
377
497
|
export function queryCollectionOptions(
|
|
378
498
|
config: QueryCollectionConfig<Record<string, unknown>>
|
|
379
|
-
): CollectionConfig
|
|
499
|
+
): CollectionConfig<
|
|
500
|
+
Record<string, unknown>,
|
|
501
|
+
string | number,
|
|
502
|
+
never,
|
|
503
|
+
QueryCollectionUtils
|
|
504
|
+
> & {
|
|
380
505
|
utils: QueryCollectionUtils
|
|
381
506
|
} {
|
|
382
507
|
const {
|
|
@@ -418,14 +543,13 @@ export function queryCollectionOptions(
|
|
|
418
543
|
throw new GetKeyRequiredError()
|
|
419
544
|
}
|
|
420
545
|
|
|
421
|
-
/**
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
let queryObserver: QueryObserver<Array<any>, any, Array<any>, Array<any>, any>
|
|
546
|
+
/** State object to hold error tracking and observer reference */
|
|
547
|
+
const state: QueryCollectionState = {
|
|
548
|
+
lastError: undefined as any,
|
|
549
|
+
errorCount: 0,
|
|
550
|
+
lastErrorUpdatedAt: 0,
|
|
551
|
+
queryObserver: undefined,
|
|
552
|
+
}
|
|
429
553
|
|
|
430
554
|
const internalSync: SyncConfig<any>[`sync`] = (params) => {
|
|
431
555
|
const { begin, write, commit, markReady, collection } = params
|
|
@@ -459,7 +583,7 @@ export function queryCollectionOptions(
|
|
|
459
583
|
>(queryClient, observerOptions)
|
|
460
584
|
|
|
461
585
|
// Store reference for imperative refetch
|
|
462
|
-
queryObserver = localObserver
|
|
586
|
+
state.queryObserver = localObserver
|
|
463
587
|
|
|
464
588
|
let isSubscribed = false
|
|
465
589
|
let actualUnsubscribeFn: (() => void) | null = null
|
|
@@ -468,8 +592,8 @@ export function queryCollectionOptions(
|
|
|
468
592
|
const handleQueryResult: UpdateHandler = (result) => {
|
|
469
593
|
if (result.isSuccess) {
|
|
470
594
|
// Clear error state
|
|
471
|
-
lastError = undefined
|
|
472
|
-
errorCount = 0
|
|
595
|
+
state.lastError = undefined
|
|
596
|
+
state.errorCount = 0
|
|
473
597
|
|
|
474
598
|
const rawData = result.data
|
|
475
599
|
const newItemsArray = select ? select(rawData) : rawData
|
|
@@ -543,10 +667,10 @@ export function queryCollectionOptions(
|
|
|
543
667
|
// Mark collection as ready after first successful query result
|
|
544
668
|
markReady()
|
|
545
669
|
} else if (result.isError) {
|
|
546
|
-
if (result.errorUpdatedAt !== lastErrorUpdatedAt) {
|
|
547
|
-
lastError = result.error
|
|
548
|
-
errorCount++
|
|
549
|
-
lastErrorUpdatedAt = result.errorUpdatedAt
|
|
670
|
+
if (result.errorUpdatedAt !== state.lastErrorUpdatedAt) {
|
|
671
|
+
state.lastError = result.error
|
|
672
|
+
state.errorCount++
|
|
673
|
+
state.lastErrorUpdatedAt = result.errorUpdatedAt
|
|
550
674
|
}
|
|
551
675
|
|
|
552
676
|
console.error(
|
|
@@ -622,12 +746,12 @@ export function queryCollectionOptions(
|
|
|
622
746
|
*/
|
|
623
747
|
const refetch: RefetchFn = async (opts) => {
|
|
624
748
|
// Observer is created when sync starts. If never synced, nothing to refetch.
|
|
625
|
-
|
|
626
|
-
if (!queryObserver) {
|
|
749
|
+
|
|
750
|
+
if (!state.queryObserver) {
|
|
627
751
|
return
|
|
628
752
|
}
|
|
629
753
|
// Return the QueryObserverResult for users to inspect
|
|
630
|
-
return queryObserver.refetch({
|
|
754
|
+
return state.queryObserver.refetch({
|
|
631
755
|
throwOnError: opts?.throwOnError,
|
|
632
756
|
})
|
|
633
757
|
}
|
|
@@ -710,6 +834,9 @@ export function queryCollectionOptions(
|
|
|
710
834
|
}
|
|
711
835
|
: undefined
|
|
712
836
|
|
|
837
|
+
// Create utils instance with state and dependencies passed explicitly
|
|
838
|
+
const utils: any = new QueryCollectionUtilsImpl(state, refetch, writeUtils)
|
|
839
|
+
|
|
713
840
|
return {
|
|
714
841
|
...baseCollectionConfig,
|
|
715
842
|
getKey,
|
|
@@ -717,18 +844,6 @@ export function queryCollectionOptions(
|
|
|
717
844
|
onInsert: wrappedOnInsert,
|
|
718
845
|
onUpdate: wrappedOnUpdate,
|
|
719
846
|
onDelete: wrappedOnDelete,
|
|
720
|
-
utils
|
|
721
|
-
refetch,
|
|
722
|
-
...writeUtils,
|
|
723
|
-
lastError: () => lastError,
|
|
724
|
-
isError: () => !!lastError,
|
|
725
|
-
errorCount: () => errorCount,
|
|
726
|
-
clearError: async () => {
|
|
727
|
-
lastError = undefined
|
|
728
|
-
errorCount = 0
|
|
729
|
-
lastErrorUpdatedAt = 0
|
|
730
|
-
await refetch({ throwOnError: true })
|
|
731
|
-
},
|
|
732
|
-
},
|
|
847
|
+
utils,
|
|
733
848
|
}
|
|
734
849
|
}
|