@tanstack/query-db-collection 0.1.3 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cjs/manual-sync.cjs +72 -6
- package/dist/cjs/manual-sync.cjs.map +1 -1
- package/dist/cjs/manual-sync.d.cts +1 -1
- package/dist/cjs/query.cjs.map +1 -1
- package/dist/cjs/query.d.cts +59 -8
- package/dist/esm/manual-sync.d.ts +1 -1
- package/dist/esm/manual-sync.js +72 -6
- package/dist/esm/manual-sync.js.map +1 -1
- package/dist/esm/query.d.ts +59 -8
- package/dist/esm/query.js.map +1 -1
- package/package.json +2 -2
- package/src/manual-sync.ts +108 -6
- package/src/query.ts +59 -10
package/dist/cjs/manual-sync.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
3
3
|
const errors = require("./errors.cjs");
|
|
4
|
+
const activeBatchContexts = /* @__PURE__ */ new WeakMap();
|
|
4
5
|
function normalizeOperations(ops, ctx) {
|
|
5
6
|
const operations = Array.isArray(ops) ? ops : [ops];
|
|
6
7
|
const normalized = [];
|
|
@@ -121,24 +122,89 @@ function createWriteUtils(getContext) {
|
|
|
121
122
|
}
|
|
122
123
|
return {
|
|
123
124
|
writeInsert(data) {
|
|
125
|
+
const operation = {
|
|
126
|
+
type: `insert`,
|
|
127
|
+
data
|
|
128
|
+
};
|
|
124
129
|
const ctx = ensureContext();
|
|
125
|
-
|
|
130
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
131
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
132
|
+
batchContext.operations.push(operation);
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
performWriteOperations(operation, ctx);
|
|
126
136
|
},
|
|
127
137
|
writeUpdate(data) {
|
|
138
|
+
const operation = {
|
|
139
|
+
type: `update`,
|
|
140
|
+
data
|
|
141
|
+
};
|
|
128
142
|
const ctx = ensureContext();
|
|
129
|
-
|
|
143
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
144
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
145
|
+
batchContext.operations.push(operation);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
performWriteOperations(operation, ctx);
|
|
130
149
|
},
|
|
131
150
|
writeDelete(key) {
|
|
151
|
+
const operation = {
|
|
152
|
+
type: `delete`,
|
|
153
|
+
key
|
|
154
|
+
};
|
|
132
155
|
const ctx = ensureContext();
|
|
133
|
-
|
|
156
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
157
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
158
|
+
batchContext.operations.push(operation);
|
|
159
|
+
return;
|
|
160
|
+
}
|
|
161
|
+
performWriteOperations(operation, ctx);
|
|
134
162
|
},
|
|
135
163
|
writeUpsert(data) {
|
|
164
|
+
const operation = {
|
|
165
|
+
type: `upsert`,
|
|
166
|
+
data
|
|
167
|
+
};
|
|
136
168
|
const ctx = ensureContext();
|
|
137
|
-
|
|
169
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
170
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
171
|
+
batchContext.operations.push(operation);
|
|
172
|
+
return;
|
|
173
|
+
}
|
|
174
|
+
performWriteOperations(operation, ctx);
|
|
138
175
|
},
|
|
139
|
-
writeBatch(
|
|
176
|
+
writeBatch(callback) {
|
|
140
177
|
const ctx = ensureContext();
|
|
141
|
-
|
|
178
|
+
const existingBatch = activeBatchContexts.get(ctx);
|
|
179
|
+
if (existingBatch == null ? void 0 : existingBatch.isActive) {
|
|
180
|
+
throw new Error(
|
|
181
|
+
`Cannot nest writeBatch calls. Complete the current batch before starting a new one.`
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
const batchContext = {
|
|
185
|
+
operations: [],
|
|
186
|
+
isActive: true
|
|
187
|
+
};
|
|
188
|
+
activeBatchContexts.set(ctx, batchContext);
|
|
189
|
+
try {
|
|
190
|
+
const result = callback();
|
|
191
|
+
if (
|
|
192
|
+
// @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
194
|
+
result && typeof result === `object` && `then` in result && // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async
|
|
195
|
+
typeof result.then === `function`
|
|
196
|
+
) {
|
|
197
|
+
throw new Error(
|
|
198
|
+
`writeBatch does not support async callbacks. The callback must be synchronous.`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
if (batchContext.operations.length > 0) {
|
|
202
|
+
performWriteOperations(batchContext.operations, ctx);
|
|
203
|
+
}
|
|
204
|
+
} finally {
|
|
205
|
+
batchContext.isActive = false;
|
|
206
|
+
activeBatchContexts.delete(ctx);
|
|
207
|
+
}
|
|
142
208
|
}
|
|
143
209
|
};
|
|
144
210
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manual-sync.cjs","sources":["../../src/manual-sync.ts"],"sourcesContent":["import {\n DeleteOperationItemNotFoundError,\n DuplicateKeyInBatchError,\n SyncNotInitializedError,\n UpdateOperationItemNotFoundError,\n} from \"./errors\"\nimport type { QueryClient } from \"@tanstack/query-core\"\nimport type { ChangeMessage, Collection } from \"@tanstack/db\"\n\n// Types for sync operations\nexport type SyncOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n> =\n | { type: `insert`; data: TInsertInput | Array<TInsertInput> }\n | { type: `update`; data: Partial<TRow> | Array<Partial<TRow>> }\n | { type: `delete`; key: TKey | Array<TKey> }\n | { type: `upsert`; data: Partial<TRow> | Array<Partial<TRow>> }\n\nexport interface SyncContext<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n collection: Collection<TRow>\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TRow) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n commit: () => void\n}\n\ninterface NormalizedOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n type: `insert` | `update` | `delete` | `upsert`\n key: TKey\n data?: TRow | Partial<TRow>\n}\n\n// Normalize operations into a consistent format\nfunction normalizeOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n ops:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): Array<NormalizedOperation<TRow, TKey>> {\n const operations = Array.isArray(ops) ? ops : [ops]\n const normalized: Array<NormalizedOperation<TRow, TKey>> = []\n\n for (const op of operations) {\n if (op.type === `delete`) {\n const keys = Array.isArray(op.key) ? op.key : [op.key]\n for (const key of keys) {\n normalized.push({ type: `delete`, key })\n }\n } else {\n const items = Array.isArray(op.data) ? op.data : [op.data]\n for (const item of items) {\n let key: TKey\n if (op.type === `update`) {\n // For updates, we need to get the key from the partial data\n key = ctx.getKey(item as TRow)\n } else {\n // For insert/upsert, validate and resolve the full item first\n const resolved = ctx.collection.validateData(\n item,\n op.type === `upsert` ? `insert` : op.type\n )\n key = ctx.getKey(resolved)\n }\n normalized.push({ type: op.type, key, data: item })\n }\n }\n }\n\n return normalized\n}\n\n// Validate operations before executing\nfunction validateOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n>(\n operations: Array<NormalizedOperation<TRow, TKey>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const seenKeys = new Set<TKey>()\n\n for (const op of operations) {\n // Check for duplicate keys within the batch\n if (seenKeys.has(op.key)) {\n throw new DuplicateKeyInBatchError(op.key)\n }\n seenKeys.add(op.key)\n\n // Validate operation-specific requirements\n if (op.type === `update`) {\n if (!ctx.collection.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection.has(op.key)) {\n throw new DeleteOperationItemNotFoundError(op.key)\n }\n }\n }\n}\n\n// Execute a batch of operations\nexport function performWriteOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n operations:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const normalized = normalizeOperations(operations, ctx)\n validateOperations(normalized, ctx)\n\n ctx.begin()\n\n for (const op of normalized) {\n switch (op.type) {\n case `insert`: {\n const resolved = ctx.collection.validateData(op.data, `insert`)\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n break\n }\n case `update`: {\n const currentItem = ctx.collection.get(op.key)!\n const updatedItem = {\n ...currentItem,\n ...op.data,\n }\n const resolved = ctx.collection.validateData(\n updatedItem,\n `update`,\n op.key\n )\n ctx.write({\n type: `update`,\n value: resolved,\n })\n break\n }\n case `delete`: {\n const currentItem = ctx.collection.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n const resolved = ctx.collection.validateData(\n op.data,\n ctx.collection.has(op.key) ? `update` : `insert`,\n op.key\n )\n if (ctx.collection.has(op.key)) {\n ctx.write({\n type: `update`,\n value: resolved,\n })\n } else {\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n }\n break\n }\n }\n }\n\n ctx.commit()\n\n // Update query cache after successful commit\n const updatedData = ctx.collection.toArray\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n}\n\n// Factory function to create write utils\nexport function createWriteUtils<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(getContext: () => SyncContext<TRow, TKey> | null) {\n function ensureContext(): SyncContext<TRow, TKey> {\n const context = getContext()\n if (!context) {\n throw new SyncNotInitializedError()\n }\n return context\n }\n\n return {\n writeInsert(data: TInsertInput | Array<TInsertInput>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `insert`, data }, ctx)\n },\n\n writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `update`, data }, ctx)\n },\n\n writeDelete(key: TKey | Array<TKey>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `delete`, key }, ctx)\n },\n\n writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `upsert`, data }, ctx)\n },\n\n writeBatch(operations: Array<SyncOperation<TRow, TKey, TInsertInput>>) {\n const ctx = ensureContext()\n performWriteOperations(operations, ctx)\n },\n }\n}\n"],"names":["DuplicateKeyInBatchError","UpdateOperationItemNotFoundError","DeleteOperationItemNotFoundError","SyncNotInitializedError"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"manual-sync.cjs","sources":["../../src/manual-sync.ts"],"sourcesContent":["import {\n DeleteOperationItemNotFoundError,\n DuplicateKeyInBatchError,\n SyncNotInitializedError,\n UpdateOperationItemNotFoundError,\n} from \"./errors\"\nimport type { QueryClient } from \"@tanstack/query-core\"\nimport type { ChangeMessage, Collection } from \"@tanstack/db\"\n\n// Track active batch operations per context to prevent cross-collection contamination\nconst activeBatchContexts = new WeakMap<\n SyncContext<any, any>,\n {\n operations: Array<SyncOperation<any, any, any>>\n isActive: boolean\n }\n>()\n\n// Types for sync operations\nexport type SyncOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n> =\n | { type: `insert`; data: TInsertInput | Array<TInsertInput> }\n | { type: `update`; data: Partial<TRow> | Array<Partial<TRow>> }\n | { type: `delete`; key: TKey | Array<TKey> }\n | { type: `upsert`; data: Partial<TRow> | Array<Partial<TRow>> }\n\nexport interface SyncContext<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n collection: Collection<TRow>\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TRow) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n commit: () => void\n}\n\ninterface NormalizedOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n type: `insert` | `update` | `delete` | `upsert`\n key: TKey\n data?: TRow | Partial<TRow>\n}\n\n// Normalize operations into a consistent format\nfunction normalizeOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n ops:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): Array<NormalizedOperation<TRow, TKey>> {\n const operations = Array.isArray(ops) ? ops : [ops]\n const normalized: Array<NormalizedOperation<TRow, TKey>> = []\n\n for (const op of operations) {\n if (op.type === `delete`) {\n const keys = Array.isArray(op.key) ? op.key : [op.key]\n for (const key of keys) {\n normalized.push({ type: `delete`, key })\n }\n } else {\n const items = Array.isArray(op.data) ? op.data : [op.data]\n for (const item of items) {\n let key: TKey\n if (op.type === `update`) {\n // For updates, we need to get the key from the partial data\n key = ctx.getKey(item as TRow)\n } else {\n // For insert/upsert, validate and resolve the full item first\n const resolved = ctx.collection.validateData(\n item,\n op.type === `upsert` ? `insert` : op.type\n )\n key = ctx.getKey(resolved)\n }\n normalized.push({ type: op.type, key, data: item })\n }\n }\n }\n\n return normalized\n}\n\n// Validate operations before executing\nfunction validateOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n>(\n operations: Array<NormalizedOperation<TRow, TKey>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const seenKeys = new Set<TKey>()\n\n for (const op of operations) {\n // Check for duplicate keys within the batch\n if (seenKeys.has(op.key)) {\n throw new DuplicateKeyInBatchError(op.key)\n }\n seenKeys.add(op.key)\n\n // Validate operation-specific requirements\n if (op.type === `update`) {\n if (!ctx.collection.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection.has(op.key)) {\n throw new DeleteOperationItemNotFoundError(op.key)\n }\n }\n }\n}\n\n// Execute a batch of operations\nexport function performWriteOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n operations:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const normalized = normalizeOperations(operations, ctx)\n validateOperations(normalized, ctx)\n\n ctx.begin()\n\n for (const op of normalized) {\n switch (op.type) {\n case `insert`: {\n const resolved = ctx.collection.validateData(op.data, `insert`)\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n break\n }\n case `update`: {\n const currentItem = ctx.collection.get(op.key)!\n const updatedItem = {\n ...currentItem,\n ...op.data,\n }\n const resolved = ctx.collection.validateData(\n updatedItem,\n `update`,\n op.key\n )\n ctx.write({\n type: `update`,\n value: resolved,\n })\n break\n }\n case `delete`: {\n const currentItem = ctx.collection.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n const resolved = ctx.collection.validateData(\n op.data,\n ctx.collection.has(op.key) ? `update` : `insert`,\n op.key\n )\n if (ctx.collection.has(op.key)) {\n ctx.write({\n type: `update`,\n value: resolved,\n })\n } else {\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n }\n break\n }\n }\n }\n\n ctx.commit()\n\n // Update query cache after successful commit\n const updatedData = ctx.collection.toArray\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n}\n\n// Factory function to create write utils\nexport function createWriteUtils<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(getContext: () => SyncContext<TRow, TKey> | null) {\n function ensureContext(): SyncContext<TRow, TKey> {\n const context = getContext()\n if (!context) {\n throw new SyncNotInitializedError()\n }\n return context\n }\n\n return {\n writeInsert(data: TInsertInput | Array<TInsertInput>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `insert`,\n data,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n // If we're in a batch, just add to the batch operations\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n // Otherwise, perform the operation immediately\n performWriteOperations(operation, ctx)\n },\n\n writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `update`,\n data,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n performWriteOperations(operation, ctx)\n },\n\n writeDelete(key: TKey | Array<TKey>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `delete`,\n key,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n performWriteOperations(operation, ctx)\n },\n\n writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `upsert`,\n data,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n performWriteOperations(operation, ctx)\n },\n\n writeBatch(callback: () => void) {\n const ctx = ensureContext()\n\n // Check if we're already in a batch (nested batch)\n const existingBatch = activeBatchContexts.get(ctx)\n if (existingBatch?.isActive) {\n throw new Error(\n `Cannot nest writeBatch calls. Complete the current batch before starting a new one.`\n )\n }\n\n // Set up the batch context for this specific collection\n const batchContext = {\n operations: [] as Array<SyncOperation<TRow, TKey, TInsertInput>>,\n isActive: true,\n }\n activeBatchContexts.set(ctx, batchContext)\n\n try {\n // Execute the callback - any write operations will be collected\n const result = callback()\n\n // Check if callback returns a promise (async function)\n if (\n // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n result &&\n typeof result === `object` &&\n `then` in result &&\n // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async\n typeof result.then === `function`\n ) {\n throw new Error(\n `writeBatch does not support async callbacks. The callback must be synchronous.`\n )\n }\n\n // Perform all collected operations\n if (batchContext.operations.length > 0) {\n performWriteOperations(batchContext.operations, ctx)\n }\n } finally {\n // Always clear the batch context\n batchContext.isActive = false\n activeBatchContexts.delete(ctx)\n }\n },\n }\n}\n"],"names":["DuplicateKeyInBatchError","UpdateOperationItemNotFoundError","DeleteOperationItemNotFoundError","SyncNotInitializedError"],"mappings":"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"}
|
|
@@ -28,5 +28,5 @@ export declare function createWriteUtils<TRow extends object, TKey extends strin
|
|
|
28
28
|
writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>): void;
|
|
29
29
|
writeDelete(key: TKey | Array<TKey>): void;
|
|
30
30
|
writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>): void;
|
|
31
|
-
writeBatch(
|
|
31
|
+
writeBatch(callback: () => void): void;
|
|
32
32
|
};
|
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 { SyncOperation } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n queryKey: TQueryKey\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\n queryClient: QueryClient\n\n // Query-specific options\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n id?: string\n getKey: CollectionConfig<TItem>[`getKey`]\n schema?: CollectionConfig<TItem>[`schema`]\n sync?: CollectionConfig<TItem>[`sync`]\n startSync?: CollectionConfig<TItem>[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<TItem>\n // TODO type returning { refetch: boolean }\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Query collection utilities type\n */\n/**\n * Write operation types for batch operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n refetch: RefetchFn\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n writeDelete: (keys: TKey | Array<TKey>) => void\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n writeBatch: (\n operations: Array<SyncOperation<TItem, TKey, TInsertInput>>\n ) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & {\n utils: QueryCollectionUtils<TItem, TKey, TInsertInput>\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"query.cjs","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n/**\n * Configuration options for creating a Query Collection\n * @template TItem - The type of items stored in the collection\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\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<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<TItem>[`getKey`]\n /** Schema for validating items */\n schema?: CollectionConfig<TItem>[`schema`]\n sync?: CollectionConfig<TItem>[`sync`]\n startSync?: CollectionConfig<TItem>[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<TItem>\n // TODO type returning { refetch: boolean }\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Basic usage\n * const todosCollection = createCollection(\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 * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & {\n utils: QueryCollectionUtils<TItem, TKey, TInsertInput>\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":["QueryKeyRequiredError","QueryFnRequiredError","QueryClientRequiredError","GetKeyRequiredError","QueryObserver","createWriteUtils"],"mappings":"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"}
|
package/dist/cjs/query.d.cts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
import { SyncOperation } from './manual-sync.cjs';
|
|
2
1
|
import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
|
|
3
2
|
import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
|
|
4
3
|
export type { SyncOperation } from './manual-sync.cjs';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for creating a Query Collection
|
|
6
|
+
* @template TItem - The type of items stored in the collection
|
|
7
|
+
* @template TError - The type of errors that can occur during queries
|
|
8
|
+
* @template TQueryKey - The type of the query key
|
|
9
|
+
*/
|
|
5
10
|
export interface QueryCollectionConfig<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
|
|
11
|
+
/** The query key used by TanStack Query to identify this query */
|
|
6
12
|
queryKey: TQueryKey;
|
|
13
|
+
/** Function that fetches data from the server. Must return the complete collection state */
|
|
7
14
|
queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>;
|
|
15
|
+
/** The TanStack Query client instance */
|
|
8
16
|
queryClient: QueryClient;
|
|
17
|
+
/** Whether the query should automatically run (default: true) */
|
|
9
18
|
enabled?: boolean;
|
|
10
19
|
refetchInterval?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`refetchInterval`];
|
|
11
20
|
retry?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retry`];
|
|
12
21
|
retryDelay?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retryDelay`];
|
|
13
22
|
staleTime?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`staleTime`];
|
|
23
|
+
/** Unique identifier for the collection */
|
|
14
24
|
id?: string;
|
|
25
|
+
/** Function to extract the unique key from an item */
|
|
15
26
|
getKey: CollectionConfig<TItem>[`getKey`];
|
|
27
|
+
/** Schema for validating items */
|
|
16
28
|
schema?: CollectionConfig<TItem>[`schema`];
|
|
17
29
|
sync?: CollectionConfig<TItem>[`sync`];
|
|
18
30
|
startSync?: CollectionConfig<TItem>[`startSync`];
|
|
@@ -181,24 +193,63 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
|
|
|
181
193
|
*/
|
|
182
194
|
export type RefetchFn = () => Promise<void>;
|
|
183
195
|
/**
|
|
184
|
-
* Query
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
196
|
+
* Utility methods available on Query Collections for direct writes and manual operations.
|
|
197
|
+
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
|
|
198
|
+
* @template TItem - The type of items stored in the collection
|
|
199
|
+
* @template TKey - The type of the item keys
|
|
200
|
+
* @template TInsertInput - The type accepted for insert operations
|
|
188
201
|
*/
|
|
189
202
|
export interface QueryCollectionUtils<TItem extends object = Record<string, unknown>, TKey extends string | number = string | number, TInsertInput extends object = TItem> extends UtilsRecord {
|
|
203
|
+
/** Manually trigger a refetch of the query */
|
|
190
204
|
refetch: RefetchFn;
|
|
205
|
+
/** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */
|
|
191
206
|
writeInsert: (data: TInsertInput | Array<TInsertInput>) => void;
|
|
207
|
+
/** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */
|
|
192
208
|
writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void;
|
|
209
|
+
/** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */
|
|
193
210
|
writeDelete: (keys: TKey | Array<TKey>) => void;
|
|
211
|
+
/** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */
|
|
194
212
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void;
|
|
195
|
-
|
|
213
|
+
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
214
|
+
writeBatch: (callback: () => void) => void;
|
|
196
215
|
}
|
|
197
216
|
/**
|
|
198
|
-
* Creates query collection options for use with a standard Collection
|
|
217
|
+
* Creates query collection options for use with a standard Collection.
|
|
218
|
+
* This integrates TanStack Query with TanStack DB for automatic synchronization.
|
|
199
219
|
*
|
|
200
220
|
* @param config - Configuration options for the Query collection
|
|
201
|
-
* @returns Collection options with utilities
|
|
221
|
+
* @returns Collection options with utilities for direct writes and manual operations
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // Basic usage
|
|
225
|
+
* const todosCollection = createCollection(
|
|
226
|
+
* queryCollectionOptions({
|
|
227
|
+
* queryKey: ['todos'],
|
|
228
|
+
* queryFn: async () => fetch('/api/todos').then(r => r.json()),
|
|
229
|
+
* queryClient,
|
|
230
|
+
* getKey: (item) => item.id,
|
|
231
|
+
* })
|
|
232
|
+
* )
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* // With persistence handlers
|
|
236
|
+
* const todosCollection = createCollection(
|
|
237
|
+
* queryCollectionOptions({
|
|
238
|
+
* queryKey: ['todos'],
|
|
239
|
+
* queryFn: fetchTodos,
|
|
240
|
+
* queryClient,
|
|
241
|
+
* getKey: (item) => item.id,
|
|
242
|
+
* onInsert: async ({ transaction }) => {
|
|
243
|
+
* await api.createTodos(transaction.mutations.map(m => m.modified))
|
|
244
|
+
* },
|
|
245
|
+
* onUpdate: async ({ transaction }) => {
|
|
246
|
+
* await api.updateTodos(transaction.mutations)
|
|
247
|
+
* },
|
|
248
|
+
* onDelete: async ({ transaction }) => {
|
|
249
|
+
* await api.deleteTodos(transaction.mutations.map(m => m.key))
|
|
250
|
+
* }
|
|
251
|
+
* })
|
|
252
|
+
* )
|
|
202
253
|
*/
|
|
203
254
|
export declare function queryCollectionOptions<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = TItem>(config: QueryCollectionConfig<TItem, TError, TQueryKey>): CollectionConfig<TItem> & {
|
|
204
255
|
utils: QueryCollectionUtils<TItem, TKey, TInsertInput>;
|
|
@@ -28,5 +28,5 @@ export declare function createWriteUtils<TRow extends object, TKey extends strin
|
|
|
28
28
|
writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>): void;
|
|
29
29
|
writeDelete(key: TKey | Array<TKey>): void;
|
|
30
30
|
writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>): void;
|
|
31
|
-
writeBatch(
|
|
31
|
+
writeBatch(callback: () => void): void;
|
|
32
32
|
};
|
package/dist/esm/manual-sync.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { SyncNotInitializedError, DuplicateKeyInBatchError, UpdateOperationItemNotFoundError, DeleteOperationItemNotFoundError } from "./errors.js";
|
|
2
|
+
const activeBatchContexts = /* @__PURE__ */ new WeakMap();
|
|
2
3
|
function normalizeOperations(ops, ctx) {
|
|
3
4
|
const operations = Array.isArray(ops) ? ops : [ops];
|
|
4
5
|
const normalized = [];
|
|
@@ -119,24 +120,89 @@ function createWriteUtils(getContext) {
|
|
|
119
120
|
}
|
|
120
121
|
return {
|
|
121
122
|
writeInsert(data) {
|
|
123
|
+
const operation = {
|
|
124
|
+
type: `insert`,
|
|
125
|
+
data
|
|
126
|
+
};
|
|
122
127
|
const ctx = ensureContext();
|
|
123
|
-
|
|
128
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
129
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
130
|
+
batchContext.operations.push(operation);
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
performWriteOperations(operation, ctx);
|
|
124
134
|
},
|
|
125
135
|
writeUpdate(data) {
|
|
136
|
+
const operation = {
|
|
137
|
+
type: `update`,
|
|
138
|
+
data
|
|
139
|
+
};
|
|
126
140
|
const ctx = ensureContext();
|
|
127
|
-
|
|
141
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
142
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
143
|
+
batchContext.operations.push(operation);
|
|
144
|
+
return;
|
|
145
|
+
}
|
|
146
|
+
performWriteOperations(operation, ctx);
|
|
128
147
|
},
|
|
129
148
|
writeDelete(key) {
|
|
149
|
+
const operation = {
|
|
150
|
+
type: `delete`,
|
|
151
|
+
key
|
|
152
|
+
};
|
|
130
153
|
const ctx = ensureContext();
|
|
131
|
-
|
|
154
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
155
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
156
|
+
batchContext.operations.push(operation);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
performWriteOperations(operation, ctx);
|
|
132
160
|
},
|
|
133
161
|
writeUpsert(data) {
|
|
162
|
+
const operation = {
|
|
163
|
+
type: `upsert`,
|
|
164
|
+
data
|
|
165
|
+
};
|
|
134
166
|
const ctx = ensureContext();
|
|
135
|
-
|
|
167
|
+
const batchContext = activeBatchContexts.get(ctx);
|
|
168
|
+
if (batchContext == null ? void 0 : batchContext.isActive) {
|
|
169
|
+
batchContext.operations.push(operation);
|
|
170
|
+
return;
|
|
171
|
+
}
|
|
172
|
+
performWriteOperations(operation, ctx);
|
|
136
173
|
},
|
|
137
|
-
writeBatch(
|
|
174
|
+
writeBatch(callback) {
|
|
138
175
|
const ctx = ensureContext();
|
|
139
|
-
|
|
176
|
+
const existingBatch = activeBatchContexts.get(ctx);
|
|
177
|
+
if (existingBatch == null ? void 0 : existingBatch.isActive) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`Cannot nest writeBatch calls. Complete the current batch before starting a new one.`
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const batchContext = {
|
|
183
|
+
operations: [],
|
|
184
|
+
isActive: true
|
|
185
|
+
};
|
|
186
|
+
activeBatchContexts.set(ctx, batchContext);
|
|
187
|
+
try {
|
|
188
|
+
const result = callback();
|
|
189
|
+
if (
|
|
190
|
+
// @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async
|
|
191
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
192
|
+
result && typeof result === `object` && `then` in result && // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async
|
|
193
|
+
typeof result.then === `function`
|
|
194
|
+
) {
|
|
195
|
+
throw new Error(
|
|
196
|
+
`writeBatch does not support async callbacks. The callback must be synchronous.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
if (batchContext.operations.length > 0) {
|
|
200
|
+
performWriteOperations(batchContext.operations, ctx);
|
|
201
|
+
}
|
|
202
|
+
} finally {
|
|
203
|
+
batchContext.isActive = false;
|
|
204
|
+
activeBatchContexts.delete(ctx);
|
|
205
|
+
}
|
|
140
206
|
}
|
|
141
207
|
};
|
|
142
208
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"manual-sync.js","sources":["../../src/manual-sync.ts"],"sourcesContent":["import {\n DeleteOperationItemNotFoundError,\n DuplicateKeyInBatchError,\n SyncNotInitializedError,\n UpdateOperationItemNotFoundError,\n} from \"./errors\"\nimport type { QueryClient } from \"@tanstack/query-core\"\nimport type { ChangeMessage, Collection } from \"@tanstack/db\"\n\n// Types for sync operations\nexport type SyncOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n> =\n | { type: `insert`; data: TInsertInput | Array<TInsertInput> }\n | { type: `update`; data: Partial<TRow> | Array<Partial<TRow>> }\n | { type: `delete`; key: TKey | Array<TKey> }\n | { type: `upsert`; data: Partial<TRow> | Array<Partial<TRow>> }\n\nexport interface SyncContext<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n collection: Collection<TRow>\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TRow) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n commit: () => void\n}\n\ninterface NormalizedOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n type: `insert` | `update` | `delete` | `upsert`\n key: TKey\n data?: TRow | Partial<TRow>\n}\n\n// Normalize operations into a consistent format\nfunction normalizeOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n ops:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): Array<NormalizedOperation<TRow, TKey>> {\n const operations = Array.isArray(ops) ? ops : [ops]\n const normalized: Array<NormalizedOperation<TRow, TKey>> = []\n\n for (const op of operations) {\n if (op.type === `delete`) {\n const keys = Array.isArray(op.key) ? op.key : [op.key]\n for (const key of keys) {\n normalized.push({ type: `delete`, key })\n }\n } else {\n const items = Array.isArray(op.data) ? op.data : [op.data]\n for (const item of items) {\n let key: TKey\n if (op.type === `update`) {\n // For updates, we need to get the key from the partial data\n key = ctx.getKey(item as TRow)\n } else {\n // For insert/upsert, validate and resolve the full item first\n const resolved = ctx.collection.validateData(\n item,\n op.type === `upsert` ? `insert` : op.type\n )\n key = ctx.getKey(resolved)\n }\n normalized.push({ type: op.type, key, data: item })\n }\n }\n }\n\n return normalized\n}\n\n// Validate operations before executing\nfunction validateOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n>(\n operations: Array<NormalizedOperation<TRow, TKey>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const seenKeys = new Set<TKey>()\n\n for (const op of operations) {\n // Check for duplicate keys within the batch\n if (seenKeys.has(op.key)) {\n throw new DuplicateKeyInBatchError(op.key)\n }\n seenKeys.add(op.key)\n\n // Validate operation-specific requirements\n if (op.type === `update`) {\n if (!ctx.collection.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection.has(op.key)) {\n throw new DeleteOperationItemNotFoundError(op.key)\n }\n }\n }\n}\n\n// Execute a batch of operations\nexport function performWriteOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n operations:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const normalized = normalizeOperations(operations, ctx)\n validateOperations(normalized, ctx)\n\n ctx.begin()\n\n for (const op of normalized) {\n switch (op.type) {\n case `insert`: {\n const resolved = ctx.collection.validateData(op.data, `insert`)\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n break\n }\n case `update`: {\n const currentItem = ctx.collection.get(op.key)!\n const updatedItem = {\n ...currentItem,\n ...op.data,\n }\n const resolved = ctx.collection.validateData(\n updatedItem,\n `update`,\n op.key\n )\n ctx.write({\n type: `update`,\n value: resolved,\n })\n break\n }\n case `delete`: {\n const currentItem = ctx.collection.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n const resolved = ctx.collection.validateData(\n op.data,\n ctx.collection.has(op.key) ? `update` : `insert`,\n op.key\n )\n if (ctx.collection.has(op.key)) {\n ctx.write({\n type: `update`,\n value: resolved,\n })\n } else {\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n }\n break\n }\n }\n }\n\n ctx.commit()\n\n // Update query cache after successful commit\n const updatedData = ctx.collection.toArray\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n}\n\n// Factory function to create write utils\nexport function createWriteUtils<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(getContext: () => SyncContext<TRow, TKey> | null) {\n function ensureContext(): SyncContext<TRow, TKey> {\n const context = getContext()\n if (!context) {\n throw new SyncNotInitializedError()\n }\n return context\n }\n\n return {\n writeInsert(data: TInsertInput | Array<TInsertInput>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `insert`, data }, ctx)\n },\n\n writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `update`, data }, ctx)\n },\n\n writeDelete(key: TKey | Array<TKey>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `delete`, key }, ctx)\n },\n\n writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {\n const ctx = ensureContext()\n performWriteOperations({ type: `upsert`, data }, ctx)\n },\n\n writeBatch(operations: Array<SyncOperation<TRow, TKey, TInsertInput>>) {\n const ctx = ensureContext()\n performWriteOperations(operations, ctx)\n },\n }\n}\n"],"names":[],"mappings":";AA2CA,SAAS,oBAKP,KAGA,KACwC;AACxC,QAAM,aAAa,MAAM,QAAQ,GAAG,IAAI,MAAM,CAAC,GAAG;AAClD,QAAM,aAAqD,CAAA;AAE3D,aAAW,MAAM,YAAY;AAC3B,QAAI,GAAG,SAAS,UAAU;AACxB,YAAM,OAAO,MAAM,QAAQ,GAAG,GAAG,IAAI,GAAG,MAAM,CAAC,GAAG,GAAG;AACrD,iBAAW,OAAO,MAAM;AACtB,mBAAW,KAAK,EAAE,MAAM,UAAU,KAAK;AAAA,MACzC;AAAA,IACF,OAAO;AACL,YAAM,QAAQ,MAAM,QAAQ,GAAG,IAAI,IAAI,GAAG,OAAO,CAAC,GAAG,IAAI;AACzD,iBAAW,QAAQ,OAAO;AACxB,YAAI;AACJ,YAAI,GAAG,SAAS,UAAU;AAExB,gBAAM,IAAI,OAAO,IAAY;AAAA,QAC/B,OAAO;AAEL,gBAAM,WAAW,IAAI,WAAW;AAAA,YAC9B;AAAA,YACA,GAAG,SAAS,WAAW,WAAW,GAAG;AAAA,UAAA;AAEvC,gBAAM,IAAI,OAAO,QAAQ;AAAA,QAC3B;AACA,mBAAW,KAAK,EAAE,MAAM,GAAG,MAAM,KAAK,MAAM,MAAM;AAAA,MACpD;AAAA,IACF;AAAA,EACF;AAEA,SAAO;AACT;AAGA,SAAS,mBAIP,YACA,KACM;AACN,QAAM,+BAAe,IAAA;AAErB,aAAW,MAAM,YAAY;AAE3B,QAAI,SAAS,IAAI,GAAG,GAAG,GAAG;AACxB,YAAM,IAAI,yBAAyB,GAAG,GAAG;AAAA,IAC3C;AACA,aAAS,IAAI,GAAG,GAAG;AAGnB,QAAI,GAAG,SAAS,UAAU;AACxB,UAAI,CAAC,IAAI,WAAW,IAAI,GAAG,GAAG,GAAG;AAC/B,cAAM,IAAI,iCAAiC,GAAG,GAAG;AAAA,MACnD;AAAA,IACF,WAAW,GAAG,SAAS,UAAU;AAC/B,UAAI,CAAC,IAAI,WAAW,IAAI,GAAG,GAAG,GAAG;AAC/B,cAAM,IAAI,iCAAiC,GAAG,GAAG;AAAA,MACnD;AAAA,IACF;AAAA,EACF;AACF;AAGO,SAAS,uBAKd,YAGA,KACM;AACN,QAAM,aAAa,oBAAoB,YAAY,GAAG;AACtD,qBAAmB,YAAY,GAAG;AAElC,MAAI,MAAA;AAEJ,aAAW,MAAM,YAAY;AAC3B,YAAQ,GAAG,MAAA;AAAA,MACT,KAAK,UAAU;AACb,cAAM,WAAW,IAAI,WAAW,aAAa,GAAG,MAAM,QAAQ;AAC9D,YAAI,MAAM;AAAA,UACR,MAAM;AAAA,UACN,OAAO;AAAA,QAAA,CACR;AACD;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,cAAc,IAAI,WAAW,IAAI,GAAG,GAAG;AAC7C,cAAM,cAAc;AAAA,UAClB,GAAG;AAAA,UACH,GAAG,GAAG;AAAA,QAAA;AAER,cAAM,WAAW,IAAI,WAAW;AAAA,UAC9B;AAAA,UACA;AAAA,UACA,GAAG;AAAA,QAAA;AAEL,YAAI,MAAM;AAAA,UACR,MAAM;AAAA,UACN,OAAO;AAAA,QAAA,CACR;AACD;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,cAAc,IAAI,WAAW,IAAI,GAAG,GAAG;AAC7C,YAAI,MAAM;AAAA,UACR,MAAM;AAAA,UACN,OAAO;AAAA,QAAA,CACR;AACD;AAAA,MACF;AAAA,MACA,KAAK,UAAU;AACb,cAAM,WAAW,IAAI,WAAW;AAAA,UAC9B,GAAG;AAAA,UACH,IAAI,WAAW,IAAI,GAAG,GAAG,IAAI,WAAW;AAAA,UACxC,GAAG;AAAA,QAAA;AAEL,YAAI,IAAI,WAAW,IAAI,GAAG,GAAG,GAAG;AAC9B,cAAI,MAAM;AAAA,YACR,MAAM;AAAA,YACN,OAAO;AAAA,UAAA,CACR;AAAA,QACH,OAAO;AACL,cAAI,MAAM;AAAA,YACR,MAAM;AAAA,YACN,OAAO;AAAA,UAAA,CACR;AAAA,QACH;AACA;AAAA,MACF;AAAA,IAAA;AAAA,EAEJ;AAEA,MAAI,OAAA;AAGJ,QAAM,cAAc,IAAI,WAAW;AACnC,MAAI,YAAY,aAAa,IAAI,UAAU,WAAW;AACxD;AAGO,SAAS,iBAId,YAAkD;AAClD,WAAS,gBAAyC;AAChD,UAAM,UAAU,WAAA;AAChB,QAAI,CAAC,SAAS;AACZ,YAAM,IAAI,wBAAA;AAAA,IACZ;AACA,WAAO;AAAA,EACT;AAEA,SAAO;AAAA,IACL,YAAY,MAA0C;AACpD,YAAM,MAAM,cAAA;AACZ,6BAAuB,EAAE,MAAM,UAAU,KAAA,GAAQ,GAAG;AAAA,IACtD;AAAA,IAEA,YAAY,MAA4C;AACtD,YAAM,MAAM,cAAA;AACZ,6BAAuB,EAAE,MAAM,UAAU,KAAA,GAAQ,GAAG;AAAA,IACtD;AAAA,IAEA,YAAY,KAAyB;AACnC,YAAM,MAAM,cAAA;AACZ,6BAAuB,EAAE,MAAM,UAAU,IAAA,GAAO,GAAG;AAAA,IACrD;AAAA,IAEA,YAAY,MAA4C;AACtD,YAAM,MAAM,cAAA;AACZ,6BAAuB,EAAE,MAAM,UAAU,KAAA,GAAQ,GAAG;AAAA,IACtD;AAAA,IAEA,WAAW,YAA4D;AACrE,YAAM,MAAM,cAAA;AACZ,6BAAuB,YAAY,GAAG;AAAA,IACxC;AAAA,EAAA;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"manual-sync.js","sources":["../../src/manual-sync.ts"],"sourcesContent":["import {\n DeleteOperationItemNotFoundError,\n DuplicateKeyInBatchError,\n SyncNotInitializedError,\n UpdateOperationItemNotFoundError,\n} from \"./errors\"\nimport type { QueryClient } from \"@tanstack/query-core\"\nimport type { ChangeMessage, Collection } from \"@tanstack/db\"\n\n// Track active batch operations per context to prevent cross-collection contamination\nconst activeBatchContexts = new WeakMap<\n SyncContext<any, any>,\n {\n operations: Array<SyncOperation<any, any, any>>\n isActive: boolean\n }\n>()\n\n// Types for sync operations\nexport type SyncOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n> =\n | { type: `insert`; data: TInsertInput | Array<TInsertInput> }\n | { type: `update`; data: Partial<TRow> | Array<Partial<TRow>> }\n | { type: `delete`; key: TKey | Array<TKey> }\n | { type: `upsert`; data: Partial<TRow> | Array<Partial<TRow>> }\n\nexport interface SyncContext<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n collection: Collection<TRow>\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TRow) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n commit: () => void\n}\n\ninterface NormalizedOperation<\n TRow extends object,\n TKey extends string | number = string | number,\n> {\n type: `insert` | `update` | `delete` | `upsert`\n key: TKey\n data?: TRow | Partial<TRow>\n}\n\n// Normalize operations into a consistent format\nfunction normalizeOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n ops:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): Array<NormalizedOperation<TRow, TKey>> {\n const operations = Array.isArray(ops) ? ops : [ops]\n const normalized: Array<NormalizedOperation<TRow, TKey>> = []\n\n for (const op of operations) {\n if (op.type === `delete`) {\n const keys = Array.isArray(op.key) ? op.key : [op.key]\n for (const key of keys) {\n normalized.push({ type: `delete`, key })\n }\n } else {\n const items = Array.isArray(op.data) ? op.data : [op.data]\n for (const item of items) {\n let key: TKey\n if (op.type === `update`) {\n // For updates, we need to get the key from the partial data\n key = ctx.getKey(item as TRow)\n } else {\n // For insert/upsert, validate and resolve the full item first\n const resolved = ctx.collection.validateData(\n item,\n op.type === `upsert` ? `insert` : op.type\n )\n key = ctx.getKey(resolved)\n }\n normalized.push({ type: op.type, key, data: item })\n }\n }\n }\n\n return normalized\n}\n\n// Validate operations before executing\nfunction validateOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n>(\n operations: Array<NormalizedOperation<TRow, TKey>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const seenKeys = new Set<TKey>()\n\n for (const op of operations) {\n // Check for duplicate keys within the batch\n if (seenKeys.has(op.key)) {\n throw new DuplicateKeyInBatchError(op.key)\n }\n seenKeys.add(op.key)\n\n // Validate operation-specific requirements\n if (op.type === `update`) {\n if (!ctx.collection.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection.has(op.key)) {\n throw new DeleteOperationItemNotFoundError(op.key)\n }\n }\n }\n}\n\n// Execute a batch of operations\nexport function performWriteOperations<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(\n operations:\n | SyncOperation<TRow, TKey, TInsertInput>\n | Array<SyncOperation<TRow, TKey, TInsertInput>>,\n ctx: SyncContext<TRow, TKey>\n): void {\n const normalized = normalizeOperations(operations, ctx)\n validateOperations(normalized, ctx)\n\n ctx.begin()\n\n for (const op of normalized) {\n switch (op.type) {\n case `insert`: {\n const resolved = ctx.collection.validateData(op.data, `insert`)\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n break\n }\n case `update`: {\n const currentItem = ctx.collection.get(op.key)!\n const updatedItem = {\n ...currentItem,\n ...op.data,\n }\n const resolved = ctx.collection.validateData(\n updatedItem,\n `update`,\n op.key\n )\n ctx.write({\n type: `update`,\n value: resolved,\n })\n break\n }\n case `delete`: {\n const currentItem = ctx.collection.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n const resolved = ctx.collection.validateData(\n op.data,\n ctx.collection.has(op.key) ? `update` : `insert`,\n op.key\n )\n if (ctx.collection.has(op.key)) {\n ctx.write({\n type: `update`,\n value: resolved,\n })\n } else {\n ctx.write({\n type: `insert`,\n value: resolved,\n })\n }\n break\n }\n }\n }\n\n ctx.commit()\n\n // Update query cache after successful commit\n const updatedData = ctx.collection.toArray\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n}\n\n// Factory function to create write utils\nexport function createWriteUtils<\n TRow extends object,\n TKey extends string | number = string | number,\n TInsertInput extends object = TRow,\n>(getContext: () => SyncContext<TRow, TKey> | null) {\n function ensureContext(): SyncContext<TRow, TKey> {\n const context = getContext()\n if (!context) {\n throw new SyncNotInitializedError()\n }\n return context\n }\n\n return {\n writeInsert(data: TInsertInput | Array<TInsertInput>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `insert`,\n data,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n // If we're in a batch, just add to the batch operations\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n // Otherwise, perform the operation immediately\n performWriteOperations(operation, ctx)\n },\n\n writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `update`,\n data,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n performWriteOperations(operation, ctx)\n },\n\n writeDelete(key: TKey | Array<TKey>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `delete`,\n key,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n performWriteOperations(operation, ctx)\n },\n\n writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {\n const operation: SyncOperation<TRow, TKey, TInsertInput> = {\n type: `upsert`,\n data,\n }\n\n const ctx = ensureContext()\n const batchContext = activeBatchContexts.get(ctx)\n\n if (batchContext?.isActive) {\n batchContext.operations.push(operation)\n return\n }\n\n performWriteOperations(operation, ctx)\n },\n\n writeBatch(callback: () => void) {\n const ctx = ensureContext()\n\n // Check if we're already in a batch (nested batch)\n const existingBatch = activeBatchContexts.get(ctx)\n if (existingBatch?.isActive) {\n throw new Error(\n `Cannot nest writeBatch calls. Complete the current batch before starting a new one.`\n )\n }\n\n // Set up the batch context for this specific collection\n const batchContext = {\n operations: [] as Array<SyncOperation<TRow, TKey, TInsertInput>>,\n isActive: true,\n }\n activeBatchContexts.set(ctx, batchContext)\n\n try {\n // Execute the callback - any write operations will be collected\n const result = callback()\n\n // Check if callback returns a promise (async function)\n if (\n // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n result &&\n typeof result === `object` &&\n `then` in result &&\n // @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async\n typeof result.then === `function`\n ) {\n throw new Error(\n `writeBatch does not support async callbacks. The callback must be synchronous.`\n )\n }\n\n // Perform all collected operations\n if (batchContext.operations.length > 0) {\n performWriteOperations(batchContext.operations, ctx)\n }\n } finally {\n // Always clear the batch context\n batchContext.isActive = false\n activeBatchContexts.delete(ctx)\n }\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
package/dist/esm/query.d.ts
CHANGED
|
@@ -1,18 +1,30 @@
|
|
|
1
|
-
import { SyncOperation } from './manual-sync.js';
|
|
2
1
|
import { QueryClient, QueryFunctionContext, QueryKey, QueryObserverOptions } from '@tanstack/query-core';
|
|
3
2
|
import { CollectionConfig, DeleteMutationFn, InsertMutationFn, UpdateMutationFn, UtilsRecord } from '@tanstack/db';
|
|
4
3
|
export type { SyncOperation } from './manual-sync.js';
|
|
4
|
+
/**
|
|
5
|
+
* Configuration options for creating a Query Collection
|
|
6
|
+
* @template TItem - The type of items stored in the collection
|
|
7
|
+
* @template TError - The type of errors that can occur during queries
|
|
8
|
+
* @template TQueryKey - The type of the query key
|
|
9
|
+
*/
|
|
5
10
|
export interface QueryCollectionConfig<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey> {
|
|
11
|
+
/** The query key used by TanStack Query to identify this query */
|
|
6
12
|
queryKey: TQueryKey;
|
|
13
|
+
/** Function that fetches data from the server. Must return the complete collection state */
|
|
7
14
|
queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>;
|
|
15
|
+
/** The TanStack Query client instance */
|
|
8
16
|
queryClient: QueryClient;
|
|
17
|
+
/** Whether the query should automatically run (default: true) */
|
|
9
18
|
enabled?: boolean;
|
|
10
19
|
refetchInterval?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`refetchInterval`];
|
|
11
20
|
retry?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retry`];
|
|
12
21
|
retryDelay?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`retryDelay`];
|
|
13
22
|
staleTime?: QueryObserverOptions<Array<TItem>, TError, Array<TItem>, Array<TItem>, TQueryKey>[`staleTime`];
|
|
23
|
+
/** Unique identifier for the collection */
|
|
14
24
|
id?: string;
|
|
25
|
+
/** Function to extract the unique key from an item */
|
|
15
26
|
getKey: CollectionConfig<TItem>[`getKey`];
|
|
27
|
+
/** Schema for validating items */
|
|
16
28
|
schema?: CollectionConfig<TItem>[`schema`];
|
|
17
29
|
sync?: CollectionConfig<TItem>[`sync`];
|
|
18
30
|
startSync?: CollectionConfig<TItem>[`startSync`];
|
|
@@ -181,24 +193,63 @@ export interface QueryCollectionConfig<TItem extends object, TError = unknown, T
|
|
|
181
193
|
*/
|
|
182
194
|
export type RefetchFn = () => Promise<void>;
|
|
183
195
|
/**
|
|
184
|
-
* Query
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
*
|
|
196
|
+
* Utility methods available on Query Collections for direct writes and manual operations.
|
|
197
|
+
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
|
|
198
|
+
* @template TItem - The type of items stored in the collection
|
|
199
|
+
* @template TKey - The type of the item keys
|
|
200
|
+
* @template TInsertInput - The type accepted for insert operations
|
|
188
201
|
*/
|
|
189
202
|
export interface QueryCollectionUtils<TItem extends object = Record<string, unknown>, TKey extends string | number = string | number, TInsertInput extends object = TItem> extends UtilsRecord {
|
|
203
|
+
/** Manually trigger a refetch of the query */
|
|
190
204
|
refetch: RefetchFn;
|
|
205
|
+
/** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */
|
|
191
206
|
writeInsert: (data: TInsertInput | Array<TInsertInput>) => void;
|
|
207
|
+
/** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */
|
|
192
208
|
writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void;
|
|
209
|
+
/** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */
|
|
193
210
|
writeDelete: (keys: TKey | Array<TKey>) => void;
|
|
211
|
+
/** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */
|
|
194
212
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void;
|
|
195
|
-
|
|
213
|
+
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
214
|
+
writeBatch: (callback: () => void) => void;
|
|
196
215
|
}
|
|
197
216
|
/**
|
|
198
|
-
* Creates query collection options for use with a standard Collection
|
|
217
|
+
* Creates query collection options for use with a standard Collection.
|
|
218
|
+
* This integrates TanStack Query with TanStack DB for automatic synchronization.
|
|
199
219
|
*
|
|
200
220
|
* @param config - Configuration options for the Query collection
|
|
201
|
-
* @returns Collection options with utilities
|
|
221
|
+
* @returns Collection options with utilities for direct writes and manual operations
|
|
222
|
+
*
|
|
223
|
+
* @example
|
|
224
|
+
* // Basic usage
|
|
225
|
+
* const todosCollection = createCollection(
|
|
226
|
+
* queryCollectionOptions({
|
|
227
|
+
* queryKey: ['todos'],
|
|
228
|
+
* queryFn: async () => fetch('/api/todos').then(r => r.json()),
|
|
229
|
+
* queryClient,
|
|
230
|
+
* getKey: (item) => item.id,
|
|
231
|
+
* })
|
|
232
|
+
* )
|
|
233
|
+
*
|
|
234
|
+
* @example
|
|
235
|
+
* // With persistence handlers
|
|
236
|
+
* const todosCollection = createCollection(
|
|
237
|
+
* queryCollectionOptions({
|
|
238
|
+
* queryKey: ['todos'],
|
|
239
|
+
* queryFn: fetchTodos,
|
|
240
|
+
* queryClient,
|
|
241
|
+
* getKey: (item) => item.id,
|
|
242
|
+
* onInsert: async ({ transaction }) => {
|
|
243
|
+
* await api.createTodos(transaction.mutations.map(m => m.modified))
|
|
244
|
+
* },
|
|
245
|
+
* onUpdate: async ({ transaction }) => {
|
|
246
|
+
* await api.updateTodos(transaction.mutations)
|
|
247
|
+
* },
|
|
248
|
+
* onDelete: async ({ transaction }) => {
|
|
249
|
+
* await api.deleteTodos(transaction.mutations.map(m => m.key))
|
|
250
|
+
* }
|
|
251
|
+
* })
|
|
252
|
+
* )
|
|
202
253
|
*/
|
|
203
254
|
export declare function queryCollectionOptions<TItem extends object, TError = unknown, TQueryKey extends QueryKey = QueryKey, TKey extends string | number = string | number, TInsertInput extends object = TItem>(config: QueryCollectionConfig<TItem, TError, TQueryKey>): CollectionConfig<TItem> & {
|
|
204
255
|
utils: QueryCollectionUtils<TItem, TKey, TInsertInput>;
|
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 { SyncOperation } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n queryKey: TQueryKey\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\n queryClient: QueryClient\n\n // Query-specific options\n enabled?: boolean\n refetchInterval?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n id?: string\n getKey: CollectionConfig<TItem>[`getKey`]\n schema?: CollectionConfig<TItem>[`schema`]\n sync?: CollectionConfig<TItem>[`sync`]\n startSync?: CollectionConfig<TItem>[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<TItem>\n // TODO type returning { refetch: boolean }\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Query collection utilities type\n */\n/**\n * Write operation types for batch operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n refetch: RefetchFn\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n writeDelete: (keys: TKey | Array<TKey>) => void\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n writeBatch: (\n operations: Array<SyncOperation<TItem, TKey, TInsertInput>>\n ) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & {\n utils: QueryCollectionUtils<TItem, TKey, TInsertInput>\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":[],"mappings":";;;AAoRO,SAAS,uBAOd,QAGA;AACA,QAAM;AAAA,IACJ;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA;AAAA,IACA,GAAG;AAAA,EAAA,IACD;AAKJ,MAAI,CAAC,UAAU;AACb,UAAM,IAAI,sBAAA;AAAA,EACZ;AAEA,MAAI,CAAC,SAAS;AACZ,UAAM,IAAI,qBAAA;AAAA,EACZ;AAGA,MAAI,CAAC,aAAa;AAChB,UAAM,IAAI,yBAAA;AAAA,EACZ;AAEA,MAAI,CAAC,QAAQ;AACX,UAAM,IAAI,oBAAA;AAAA,EACZ;AAEA,QAAM,eAA0C,CAAC,WAAW;AAC1D,UAAM,EAAE,OAAO,OAAO,QAAQ,WAAW,eAAe;AAExD,UAAM,kBAMF;AAAA,MACF;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA,mBAAmB;AAAA,MACnB,qBAAqB;AAAA,IAAA;AAGvB,UAAM,gBAAgB,IAAI,cAMxB,aAAa,eAAe;AAE9B,UAAM,sBAAsB,cAAc,UAAU,CAAC,WAAW;AAC9D,UAAI,OAAO,WAAW;AACpB,cAAM,gBAAgB,OAAO;AAE7B,YACE,CAAC,MAAM,QAAQ,aAAa,KAC5B,cAAc,KAAK,CAAC,SAAS,OAAO,SAAS,QAAQ,GACrD;AACA,kBAAQ;AAAA,YACN;AAAA,YACA;AAAA,UAAA;AAEF;AAAA,QACF;AAEA,cAAM,qBAAqB,IAAI,IAAI,WAAW,UAAU;AACxD,cAAM,kCAAkB,IAAA;AACxB,sBAAc,QAAQ,CAAC,SAAS;AAC9B,gBAAM,MAAM,OAAO,IAAI;AACvB,sBAAY,IAAI,KAAK,IAAI;AAAA,QAC3B,CAAC;AAED,cAAA;AAGA,cAAM,eAAe,CACnB,MACA,SACY;AAEZ,gBAAM,QAAQ,OAAO,KAAK,IAAI;AAC9B,gBAAM,QAAQ,OAAO,KAAK,IAAI;AAG9B,cAAI,MAAM,WAAW,MAAM,OAAQ,QAAO;AAG1C,iBAAO,MAAM,MAAM,CAAC,QAAQ;AAE1B,gBAAI,OAAO,KAAK,GAAG,MAAM,WAAY,QAAO;AAC5C,gBAAI,OAAO,KAAK,GAAG,MAAM,YAAY,KAAK,GAAG,MAAM,MAAM;AAIvD,qBAAO,KAAK,GAAG,MAAM,KAAK,GAAG;AAAA,YAC/B;AACA,mBAAO,KAAK,GAAG,MAAM,KAAK,GAAG;AAAA,UAC/B,CAAC;AAAA,QACH;AAEA,2BAAmB,QAAQ,CAAC,SAAS,QAAQ;AAC3C,gBAAM,UAAU,YAAY,IAAI,GAAG;AACnC,cAAI,CAAC,SAAS;AACZ,kBAAM,EAAE,MAAM,UAAU,OAAO,SAAS;AAAA,UAC1C,WACE,CAAC;AAAA,YACC;AAAA,YACA;AAAA,UAAA,GAEF;AAEA,kBAAM,EAAE,MAAM,UAAU,OAAO,SAAS;AAAA,UAC1C;AAAA,QACF,CAAC;AAED,oBAAY,QAAQ,CAAC,SAAS,QAAQ;AACpC,cAAI,CAAC,mBAAmB,IAAI,GAAG,GAAG;AAChC,kBAAM,EAAE,MAAM,UAAU,OAAO,SAAS;AAAA,UAC1C;AAAA,QACF,CAAC;AAED,eAAA;AAGA,kBAAA;AAAA,MACF,WAAW,OAAO,SAAS;AACzB,gBAAQ;AAAA,UACN,2CAA2C,OAAO,QAAQ,CAAC;AAAA,UAC3D,OAAO;AAAA,QAAA;AAIT,kBAAA;AAAA,MACF;AAAA,IACF,CAAC;AAED,WAAO,YAAY;AACjB,0BAAA;AACA,YAAM,YAAY,cAAc,EAAE,UAAU;AAC5C,kBAAY,cAAc,EAAE,UAAU;AAAA,IACxC;AAAA,EACF;AAMA,QAAM,UAAqB,YAA2B;AACpD,WAAO,YAAY,eAAe;AAAA,MAChC;AAAA,IAAA,CACD;AAAA,EACH;AAGA,MAAI,eAQO;AAGX,QAAM,uBAAkD,CAAC,WAAW;AAClE,UAAM,EAAE,OAAO,OAAO,QAAQ,eAAe;AAG7C,mBAAe;AAAA,MACb;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAIF,WAAO,aAAa,MAAM;AAAA,EAC5B;AAGA,QAAM,aAAa;AAAA,IACjB,MAAM;AAAA,EAAA;AAIR,QAAM,kBAAkB,WACpB,OAAO,WAA0C;AAC/C,UAAM,gBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAClD,UAAM,gBACH,cAAwC,YAAY;AAEvD,QAAI,eAAe;AACjB,YAAM,QAAA;AAAA,IACR;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,WACpB,OAAO,WAA0C;AAC/C,UAAM,gBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAClD,UAAM,gBACH,cAAwC,YAAY;AAEvD,QAAI,eAAe;AACjB,YAAM,QAAA;AAAA,IACR;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,QAAM,kBAAkB,WACpB,OAAO,WAA0C;AAC/C,UAAM,gBAAiB,MAAM,SAAS,MAAM,KAAM,CAAA;AAClD,UAAM,gBACH,cAAwC,YAAY;AAEvD,QAAI,eAAe;AACjB,YAAM,QAAA;AAAA,IACR;AAEA,WAAO;AAAA,EACT,IACA;AAEJ,SAAO;AAAA,IACL,GAAG;AAAA,IACH;AAAA,IACA,MAAM,EAAE,MAAM,qBAAA;AAAA,IACd,UAAU;AAAA,IACV,UAAU;AAAA,IACV,UAAU;AAAA,IACV,OAAO;AAAA,MACL;AAAA,MACA,GAAG;AAAA,IAAA;AAAA,EACL;AAEJ;"}
|
|
1
|
+
{"version":3,"file":"query.js","sources":["../../src/query.ts"],"sourcesContent":["import { QueryObserver } from \"@tanstack/query-core\"\nimport {\n GetKeyRequiredError,\n QueryClientRequiredError,\n QueryFnRequiredError,\n QueryKeyRequiredError,\n} from \"./errors\"\nimport { createWriteUtils } from \"./manual-sync\"\nimport type {\n QueryClient,\n QueryFunctionContext,\n QueryKey,\n QueryObserverOptions,\n} from \"@tanstack/query-core\"\nimport type {\n ChangeMessage,\n CollectionConfig,\n DeleteMutationFn,\n DeleteMutationFnParams,\n InsertMutationFn,\n InsertMutationFnParams,\n SyncConfig,\n UpdateMutationFn,\n UpdateMutationFnParams,\n UtilsRecord,\n} from \"@tanstack/db\"\n\n// Re-export for external use\nexport type { SyncOperation } from \"./manual-sync\"\n\n/**\n * Configuration options for creating a Query Collection\n * @template TItem - The type of items stored in the collection\n * @template TError - The type of errors that can occur during queries\n * @template TQueryKey - The type of the query key\n */\nexport interface QueryCollectionConfig<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n> {\n /** The query key used by TanStack Query to identify this query */\n queryKey: TQueryKey\n /** Function that fetches data from the server. Must return the complete collection state */\n queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>\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<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`refetchInterval`]\n retry?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retry`]\n retryDelay?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`retryDelay`]\n staleTime?: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >[`staleTime`]\n\n // Standard Collection configuration properties\n /** Unique identifier for the collection */\n id?: string\n /** Function to extract the unique key from an item */\n getKey: CollectionConfig<TItem>[`getKey`]\n /** Schema for validating items */\n schema?: CollectionConfig<TItem>[`schema`]\n sync?: CollectionConfig<TItem>[`sync`]\n startSync?: CollectionConfig<TItem>[`startSync`]\n\n // Direct persistence handlers\n /**\n * Optional asynchronous handler function called before an insert operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection insert handler\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * // Automatically refetches query after insert\n * }\n *\n * @example\n * // Insert handler with refetch control\n * onInsert: async ({ transaction }) => {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Insert handler with multiple items\n * onInsert: async ({ transaction }) => {\n * const items = transaction.mutations.map(m => m.modified)\n * await api.createTodos(items)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Insert handler with error handling\n * onInsert: async ({ transaction }) => {\n * try {\n * const newItem = transaction.mutations[0].modified\n * await api.createTodo(newItem)\n * } catch (error) {\n * console.error('Insert failed:', error)\n * throw error // Transaction will rollback optimistic changes\n * }\n * }\n */\n onInsert?: InsertMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before an update operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection update handler\n * onUpdate: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n * // Automatically refetches query after update\n * }\n *\n * @example\n * // Update handler with multiple items\n * onUpdate: async ({ transaction }) => {\n * const updates = transaction.mutations.map(m => ({\n * id: m.key,\n * changes: m.changes\n * }))\n * await api.updateTodos(updates)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Update handler with manual refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Manually trigger refetch\n * await collection.utils.refetch()\n *\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Update handler with related collection refetch\n * onUpdate: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.updateTodo(mutation.original.id, mutation.changes)\n *\n * // Refetch related collections when this item changes\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * tagsCollection.utils.refetch() // Refetch tags\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onUpdate?: UpdateMutationFn<TItem>\n\n /**\n * Optional asynchronous handler function called before a delete operation\n * @param params Object containing transaction and collection information\n * @returns Promise resolving to void or { refetch?: boolean } to control refetching\n * @example\n * // Basic query collection delete handler\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * // Automatically refetches query after delete\n * }\n *\n * @example\n * // Delete handler with refetch control\n * onDelete: async ({ transaction }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n * return { refetch: false } // Skip automatic refetch\n * }\n *\n * @example\n * // Delete handler with multiple items\n * onDelete: async ({ transaction }) => {\n * const keysToDelete = transaction.mutations.map(m => m.key)\n * await api.deleteTodos(keysToDelete)\n * // Will refetch query to get updated data\n * }\n *\n * @example\n * // Delete handler with related collection refetch\n * onDelete: async ({ transaction, collection }) => {\n * const mutation = transaction.mutations[0]\n * await api.deleteTodo(mutation.original.id)\n *\n * // Refetch related collections when this item is deleted\n * await Promise.all([\n * collection.utils.refetch(), // Refetch this collection\n * usersCollection.utils.refetch(), // Refetch users\n * projectsCollection.utils.refetch() // Refetch projects\n * ])\n *\n * return { refetch: false } // Skip automatic refetch since we handled it manually\n * }\n */\n onDelete?: DeleteMutationFn<TItem>\n // TODO type returning { refetch: boolean }\n\n /**\n * Metadata to pass to the query.\n * Available in queryFn via context.meta\n *\n * @example\n * // Using meta for error context\n * queryFn: async (context) => {\n * try {\n * return await api.getTodos(userId)\n * } catch (error) {\n * // Use meta for better error messages\n * throw new Error(\n * context.meta?.errorMessage || 'Failed to load todos'\n * )\n * }\n * },\n * meta: {\n * errorMessage: `Failed to load todos for user ${userId}`\n * }\n */\n meta?: Record<string, unknown>\n}\n\n/**\n * Type for the refetch utility function\n */\nexport type RefetchFn = () => Promise<void>\n\n/**\n * Utility methods available on Query Collections for direct writes and manual operations.\n * Direct writes bypass the normal query/mutation flow and write directly to the synced data store.\n * @template TItem - The type of items stored in the collection\n * @template TKey - The type of the item keys\n * @template TInsertInput - The type accepted for insert operations\n */\nexport interface QueryCollectionUtils<\n TItem extends object = Record<string, unknown>,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n> extends UtilsRecord {\n /** Manually trigger a refetch of the query */\n refetch: RefetchFn\n /** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */\n writeInsert: (data: TInsertInput | Array<TInsertInput>) => void\n /** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */\n writeDelete: (keys: TKey | Array<TKey>) => void\n /** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */\n writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void\n /** Execute multiple write operations as a single atomic batch to the synced data store */\n writeBatch: (callback: () => void) => void\n}\n\n/**\n * Creates query collection options for use with a standard Collection.\n * This integrates TanStack Query with TanStack DB for automatic synchronization.\n *\n * @param config - Configuration options for the Query collection\n * @returns Collection options with utilities for direct writes and manual operations\n *\n * @example\n * // Basic usage\n * const todosCollection = createCollection(\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 * // With persistence handlers\n * const todosCollection = createCollection(\n * queryCollectionOptions({\n * queryKey: ['todos'],\n * queryFn: fetchTodos,\n * queryClient,\n * getKey: (item) => item.id,\n * onInsert: async ({ transaction }) => {\n * await api.createTodos(transaction.mutations.map(m => m.modified))\n * },\n * onUpdate: async ({ transaction }) => {\n * await api.updateTodos(transaction.mutations)\n * },\n * onDelete: async ({ transaction }) => {\n * await api.deleteTodos(transaction.mutations.map(m => m.key))\n * }\n * })\n * )\n */\nexport function queryCollectionOptions<\n TItem extends object,\n TError = unknown,\n TQueryKey extends QueryKey = QueryKey,\n TKey extends string | number = string | number,\n TInsertInput extends object = TItem,\n>(\n config: QueryCollectionConfig<TItem, TError, TQueryKey>\n): CollectionConfig<TItem> & {\n utils: QueryCollectionUtils<TItem, TKey, TInsertInput>\n} {\n const {\n queryKey,\n queryFn,\n queryClient,\n enabled,\n refetchInterval,\n retry,\n retryDelay,\n staleTime,\n getKey,\n onInsert,\n onUpdate,\n onDelete,\n meta,\n ...baseCollectionConfig\n } = config\n\n // Validate required parameters\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryKey) {\n throw new QueryKeyRequiredError()\n }\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryFn) {\n throw new QueryFnRequiredError()\n }\n\n // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition\n if (!queryClient) {\n throw new QueryClientRequiredError()\n }\n\n if (!getKey) {\n throw new GetKeyRequiredError()\n }\n\n const internalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, markReady, collection } = params\n\n const observerOptions: QueryObserverOptions<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n > = {\n queryKey: queryKey,\n queryFn: queryFn,\n meta: meta,\n enabled: enabled,\n refetchInterval: refetchInterval,\n retry: retry,\n retryDelay: retryDelay,\n staleTime: staleTime,\n structuralSharing: true,\n notifyOnChangeProps: `all`,\n }\n\n const localObserver = new QueryObserver<\n Array<TItem>,\n TError,\n Array<TItem>,\n Array<TItem>,\n TQueryKey\n >(queryClient, observerOptions)\n\n const actualUnsubscribeFn = localObserver.subscribe((result) => {\n if (result.isSuccess) {\n const newItemsArray = result.data\n\n if (\n !Array.isArray(newItemsArray) ||\n newItemsArray.some((item) => typeof item !== `object`)\n ) {\n console.error(\n `[QueryCollection] queryFn did not return an array of objects. Skipping update.`,\n newItemsArray\n )\n return\n }\n\n const currentSyncedItems = new Map(collection.syncedData)\n const newItemsMap = new Map<string | number, TItem>()\n newItemsArray.forEach((item) => {\n const key = getKey(item)\n newItemsMap.set(key, item)\n })\n\n begin()\n\n // Helper function for shallow equality check of objects\n const shallowEqual = (\n obj1: Record<string, any>,\n obj2: Record<string, any>\n ): boolean => {\n // Get all keys from both objects\n const keys1 = Object.keys(obj1)\n const keys2 = Object.keys(obj2)\n\n // If number of keys is different, objects are not equal\n if (keys1.length !== keys2.length) return false\n\n // Check if all keys in obj1 have the same values in obj2\n return keys1.every((key) => {\n // Skip comparing functions and complex objects deeply\n if (typeof obj1[key] === `function`) return true\n if (typeof obj1[key] === `object` && obj1[key] !== null) {\n // For nested objects, just compare references\n // A more robust solution might do recursive shallow comparison\n // or let users provide a custom equality function\n return obj1[key] === obj2[key]\n }\n return obj1[key] === obj2[key]\n })\n }\n\n currentSyncedItems.forEach((oldItem, key) => {\n const newItem = newItemsMap.get(key)\n if (!newItem) {\n write({ type: `delete`, value: oldItem })\n } else if (\n !shallowEqual(\n oldItem as Record<string, any>,\n newItem as Record<string, any>\n )\n ) {\n // Only update if there are actual differences in the properties\n write({ type: `update`, value: newItem })\n }\n })\n\n newItemsMap.forEach((newItem, key) => {\n if (!currentSyncedItems.has(key)) {\n write({ type: `insert`, value: newItem })\n }\n })\n\n commit()\n\n // Mark collection as ready after first successful query result\n markReady()\n } else if (result.isError) {\n console.error(\n `[QueryCollection] Error observing query ${String(queryKey)}:`,\n result.error\n )\n\n // Mark collection as ready even on error to avoid blocking apps\n markReady()\n }\n })\n\n return async () => {\n actualUnsubscribeFn()\n await queryClient.cancelQueries({ queryKey })\n queryClient.removeQueries({ queryKey })\n }\n }\n\n /**\n * Refetch the query data\n * @returns Promise that resolves when the refetch is complete\n */\n const refetch: RefetchFn = async (): Promise<void> => {\n return queryClient.refetchQueries({\n queryKey: queryKey,\n })\n }\n\n // Create write context for manual write operations\n let writeContext: {\n collection: any\n queryClient: QueryClient\n queryKey: Array<unknown>\n getKey: (item: TItem) => TKey\n begin: () => void\n write: (message: Omit<ChangeMessage<TItem>, `key`>) => void\n commit: () => void\n } | null = null\n\n // Enhanced internalSync that captures write functions for manual use\n const enhancedInternalSync: SyncConfig<TItem>[`sync`] = (params) => {\n const { begin, write, commit, collection } = params\n\n // Store references for manual write operations\n writeContext = {\n collection,\n queryClient,\n queryKey: queryKey as unknown as Array<unknown>,\n getKey: getKey as (item: TItem) => TKey,\n begin,\n write,\n commit,\n }\n\n // Call the original internalSync logic\n return internalSync(params)\n }\n\n // Create write utils using the manual-sync module\n const writeUtils = createWriteUtils<TItem, TKey, TInsertInput>(\n () => writeContext\n )\n\n // Create wrapper handlers for direct persistence operations that handle refetching\n const wrappedOnInsert = onInsert\n ? async (params: InsertMutationFnParams<TItem>) => {\n const handlerResult = (await onInsert(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnUpdate = onUpdate\n ? async (params: UpdateMutationFnParams<TItem>) => {\n const handlerResult = (await onUpdate(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n const wrappedOnDelete = onDelete\n ? async (params: DeleteMutationFnParams<TItem>) => {\n const handlerResult = (await onDelete(params)) ?? {}\n const shouldRefetch =\n (handlerResult as { refetch?: boolean }).refetch !== false\n\n if (shouldRefetch) {\n await refetch()\n }\n\n return handlerResult\n }\n : undefined\n\n return {\n ...baseCollectionConfig,\n getKey,\n sync: { sync: enhancedInternalSync },\n onInsert: wrappedOnInsert,\n onUpdate: wrappedOnUpdate,\n onDelete: wrappedOnDelete,\n utils: {\n refetch,\n ...writeUtils,\n },\n }\n}\n"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/query-db-collection",
|
|
3
3
|
"description": "TanStack Query collection for TanStack DB",
|
|
4
|
-
"version": "0.1
|
|
4
|
+
"version": "0.2.1",
|
|
5
5
|
"dependencies": {
|
|
6
|
-
"@tanstack/db": "0.1.
|
|
6
|
+
"@tanstack/db": "0.1.2"
|
|
7
7
|
},
|
|
8
8
|
"devDependencies": {
|
|
9
9
|
"@tanstack/query-core": "^5.0.5",
|
package/src/manual-sync.ts
CHANGED
|
@@ -7,6 +7,15 @@ import {
|
|
|
7
7
|
import type { QueryClient } from "@tanstack/query-core"
|
|
8
8
|
import type { ChangeMessage, Collection } from "@tanstack/db"
|
|
9
9
|
|
|
10
|
+
// Track active batch operations per context to prevent cross-collection contamination
|
|
11
|
+
const activeBatchContexts = new WeakMap<
|
|
12
|
+
SyncContext<any, any>,
|
|
13
|
+
{
|
|
14
|
+
operations: Array<SyncOperation<any, any, any>>
|
|
15
|
+
isActive: boolean
|
|
16
|
+
}
|
|
17
|
+
>()
|
|
18
|
+
|
|
10
19
|
// Types for sync operations
|
|
11
20
|
export type SyncOperation<
|
|
12
21
|
TRow extends object,
|
|
@@ -209,28 +218,121 @@ export function createWriteUtils<
|
|
|
209
218
|
|
|
210
219
|
return {
|
|
211
220
|
writeInsert(data: TInsertInput | Array<TInsertInput>) {
|
|
221
|
+
const operation: SyncOperation<TRow, TKey, TInsertInput> = {
|
|
222
|
+
type: `insert`,
|
|
223
|
+
data,
|
|
224
|
+
}
|
|
225
|
+
|
|
212
226
|
const ctx = ensureContext()
|
|
213
|
-
|
|
227
|
+
const batchContext = activeBatchContexts.get(ctx)
|
|
228
|
+
|
|
229
|
+
// If we're in a batch, just add to the batch operations
|
|
230
|
+
if (batchContext?.isActive) {
|
|
231
|
+
batchContext.operations.push(operation)
|
|
232
|
+
return
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// Otherwise, perform the operation immediately
|
|
236
|
+
performWriteOperations(operation, ctx)
|
|
214
237
|
},
|
|
215
238
|
|
|
216
239
|
writeUpdate(data: Partial<TRow> | Array<Partial<TRow>>) {
|
|
240
|
+
const operation: SyncOperation<TRow, TKey, TInsertInput> = {
|
|
241
|
+
type: `update`,
|
|
242
|
+
data,
|
|
243
|
+
}
|
|
244
|
+
|
|
217
245
|
const ctx = ensureContext()
|
|
218
|
-
|
|
246
|
+
const batchContext = activeBatchContexts.get(ctx)
|
|
247
|
+
|
|
248
|
+
if (batchContext?.isActive) {
|
|
249
|
+
batchContext.operations.push(operation)
|
|
250
|
+
return
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
performWriteOperations(operation, ctx)
|
|
219
254
|
},
|
|
220
255
|
|
|
221
256
|
writeDelete(key: TKey | Array<TKey>) {
|
|
257
|
+
const operation: SyncOperation<TRow, TKey, TInsertInput> = {
|
|
258
|
+
type: `delete`,
|
|
259
|
+
key,
|
|
260
|
+
}
|
|
261
|
+
|
|
222
262
|
const ctx = ensureContext()
|
|
223
|
-
|
|
263
|
+
const batchContext = activeBatchContexts.get(ctx)
|
|
264
|
+
|
|
265
|
+
if (batchContext?.isActive) {
|
|
266
|
+
batchContext.operations.push(operation)
|
|
267
|
+
return
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
performWriteOperations(operation, ctx)
|
|
224
271
|
},
|
|
225
272
|
|
|
226
273
|
writeUpsert(data: Partial<TRow> | Array<Partial<TRow>>) {
|
|
274
|
+
const operation: SyncOperation<TRow, TKey, TInsertInput> = {
|
|
275
|
+
type: `upsert`,
|
|
276
|
+
data,
|
|
277
|
+
}
|
|
278
|
+
|
|
227
279
|
const ctx = ensureContext()
|
|
228
|
-
|
|
280
|
+
const batchContext = activeBatchContexts.get(ctx)
|
|
281
|
+
|
|
282
|
+
if (batchContext?.isActive) {
|
|
283
|
+
batchContext.operations.push(operation)
|
|
284
|
+
return
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
performWriteOperations(operation, ctx)
|
|
229
288
|
},
|
|
230
289
|
|
|
231
|
-
writeBatch(
|
|
290
|
+
writeBatch(callback: () => void) {
|
|
232
291
|
const ctx = ensureContext()
|
|
233
|
-
|
|
292
|
+
|
|
293
|
+
// Check if we're already in a batch (nested batch)
|
|
294
|
+
const existingBatch = activeBatchContexts.get(ctx)
|
|
295
|
+
if (existingBatch?.isActive) {
|
|
296
|
+
throw new Error(
|
|
297
|
+
`Cannot nest writeBatch calls. Complete the current batch before starting a new one.`
|
|
298
|
+
)
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
// Set up the batch context for this specific collection
|
|
302
|
+
const batchContext = {
|
|
303
|
+
operations: [] as Array<SyncOperation<TRow, TKey, TInsertInput>>,
|
|
304
|
+
isActive: true,
|
|
305
|
+
}
|
|
306
|
+
activeBatchContexts.set(ctx, batchContext)
|
|
307
|
+
|
|
308
|
+
try {
|
|
309
|
+
// Execute the callback - any write operations will be collected
|
|
310
|
+
const result = callback()
|
|
311
|
+
|
|
312
|
+
// Check if callback returns a promise (async function)
|
|
313
|
+
if (
|
|
314
|
+
// @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async
|
|
315
|
+
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
|
|
316
|
+
result &&
|
|
317
|
+
typeof result === `object` &&
|
|
318
|
+
`then` in result &&
|
|
319
|
+
// @ts-expect-error - Runtime check for async callback, callback is typed as () => void but user might pass async
|
|
320
|
+
typeof result.then === `function`
|
|
321
|
+
) {
|
|
322
|
+
throw new Error(
|
|
323
|
+
`writeBatch does not support async callbacks. The callback must be synchronous.`
|
|
324
|
+
)
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
// Perform all collected operations
|
|
328
|
+
if (batchContext.operations.length > 0) {
|
|
329
|
+
performWriteOperations(batchContext.operations, ctx)
|
|
330
|
+
}
|
|
331
|
+
} finally {
|
|
332
|
+
// Always clear the batch context
|
|
333
|
+
batchContext.isActive = false
|
|
334
|
+
activeBatchContexts.delete(ctx)
|
|
335
|
+
}
|
|
234
336
|
},
|
|
235
337
|
}
|
|
236
338
|
}
|
package/src/query.ts
CHANGED
|
@@ -6,7 +6,6 @@ import {
|
|
|
6
6
|
QueryKeyRequiredError,
|
|
7
7
|
} from "./errors"
|
|
8
8
|
import { createWriteUtils } from "./manual-sync"
|
|
9
|
-
import type { SyncOperation } from "./manual-sync"
|
|
10
9
|
import type {
|
|
11
10
|
QueryClient,
|
|
12
11
|
QueryFunctionContext,
|
|
@@ -29,16 +28,26 @@ import type {
|
|
|
29
28
|
// Re-export for external use
|
|
30
29
|
export type { SyncOperation } from "./manual-sync"
|
|
31
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Configuration options for creating a Query Collection
|
|
33
|
+
* @template TItem - The type of items stored in the collection
|
|
34
|
+
* @template TError - The type of errors that can occur during queries
|
|
35
|
+
* @template TQueryKey - The type of the query key
|
|
36
|
+
*/
|
|
32
37
|
export interface QueryCollectionConfig<
|
|
33
38
|
TItem extends object,
|
|
34
39
|
TError = unknown,
|
|
35
40
|
TQueryKey extends QueryKey = QueryKey,
|
|
36
41
|
> {
|
|
42
|
+
/** The query key used by TanStack Query to identify this query */
|
|
37
43
|
queryKey: TQueryKey
|
|
44
|
+
/** Function that fetches data from the server. Must return the complete collection state */
|
|
38
45
|
queryFn: (context: QueryFunctionContext<TQueryKey>) => Promise<Array<TItem>>
|
|
46
|
+
/** The TanStack Query client instance */
|
|
39
47
|
queryClient: QueryClient
|
|
40
48
|
|
|
41
49
|
// Query-specific options
|
|
50
|
+
/** Whether the query should automatically run (default: true) */
|
|
42
51
|
enabled?: boolean
|
|
43
52
|
refetchInterval?: QueryObserverOptions<
|
|
44
53
|
Array<TItem>,
|
|
@@ -70,8 +79,11 @@ export interface QueryCollectionConfig<
|
|
|
70
79
|
>[`staleTime`]
|
|
71
80
|
|
|
72
81
|
// Standard Collection configuration properties
|
|
82
|
+
/** Unique identifier for the collection */
|
|
73
83
|
id?: string
|
|
84
|
+
/** Function to extract the unique key from an item */
|
|
74
85
|
getKey: CollectionConfig<TItem>[`getKey`]
|
|
86
|
+
/** Schema for validating items */
|
|
75
87
|
schema?: CollectionConfig<TItem>[`schema`]
|
|
76
88
|
sync?: CollectionConfig<TItem>[`sync`]
|
|
77
89
|
startSync?: CollectionConfig<TItem>[`startSync`]
|
|
@@ -248,31 +260,68 @@ export interface QueryCollectionConfig<
|
|
|
248
260
|
export type RefetchFn = () => Promise<void>
|
|
249
261
|
|
|
250
262
|
/**
|
|
251
|
-
* Query
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
*
|
|
263
|
+
* Utility methods available on Query Collections for direct writes and manual operations.
|
|
264
|
+
* Direct writes bypass the normal query/mutation flow and write directly to the synced data store.
|
|
265
|
+
* @template TItem - The type of items stored in the collection
|
|
266
|
+
* @template TKey - The type of the item keys
|
|
267
|
+
* @template TInsertInput - The type accepted for insert operations
|
|
255
268
|
*/
|
|
256
269
|
export interface QueryCollectionUtils<
|
|
257
270
|
TItem extends object = Record<string, unknown>,
|
|
258
271
|
TKey extends string | number = string | number,
|
|
259
272
|
TInsertInput extends object = TItem,
|
|
260
273
|
> extends UtilsRecord {
|
|
274
|
+
/** Manually trigger a refetch of the query */
|
|
261
275
|
refetch: RefetchFn
|
|
276
|
+
/** Insert one or more items directly into the synced data store without triggering a query refetch or optimistic update */
|
|
262
277
|
writeInsert: (data: TInsertInput | Array<TInsertInput>) => void
|
|
278
|
+
/** Update one or more items directly in the synced data store without triggering a query refetch or optimistic update */
|
|
263
279
|
writeUpdate: (updates: Partial<TItem> | Array<Partial<TItem>>) => void
|
|
280
|
+
/** Delete one or more items directly from the synced data store without triggering a query refetch or optimistic update */
|
|
264
281
|
writeDelete: (keys: TKey | Array<TKey>) => void
|
|
282
|
+
/** Insert or update one or more items directly in the synced data store without triggering a query refetch or optimistic update */
|
|
265
283
|
writeUpsert: (data: Partial<TItem> | Array<Partial<TItem>>) => void
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
) => void
|
|
284
|
+
/** Execute multiple write operations as a single atomic batch to the synced data store */
|
|
285
|
+
writeBatch: (callback: () => void) => void
|
|
269
286
|
}
|
|
270
287
|
|
|
271
288
|
/**
|
|
272
|
-
* Creates query collection options for use with a standard Collection
|
|
289
|
+
* Creates query collection options for use with a standard Collection.
|
|
290
|
+
* This integrates TanStack Query with TanStack DB for automatic synchronization.
|
|
273
291
|
*
|
|
274
292
|
* @param config - Configuration options for the Query collection
|
|
275
|
-
* @returns Collection options with utilities
|
|
293
|
+
* @returns Collection options with utilities for direct writes and manual operations
|
|
294
|
+
*
|
|
295
|
+
* @example
|
|
296
|
+
* // Basic usage
|
|
297
|
+
* const todosCollection = createCollection(
|
|
298
|
+
* queryCollectionOptions({
|
|
299
|
+
* queryKey: ['todos'],
|
|
300
|
+
* queryFn: async () => fetch('/api/todos').then(r => r.json()),
|
|
301
|
+
* queryClient,
|
|
302
|
+
* getKey: (item) => item.id,
|
|
303
|
+
* })
|
|
304
|
+
* )
|
|
305
|
+
*
|
|
306
|
+
* @example
|
|
307
|
+
* // With persistence handlers
|
|
308
|
+
* const todosCollection = createCollection(
|
|
309
|
+
* queryCollectionOptions({
|
|
310
|
+
* queryKey: ['todos'],
|
|
311
|
+
* queryFn: fetchTodos,
|
|
312
|
+
* queryClient,
|
|
313
|
+
* getKey: (item) => item.id,
|
|
314
|
+
* onInsert: async ({ transaction }) => {
|
|
315
|
+
* await api.createTodos(transaction.mutations.map(m => m.modified))
|
|
316
|
+
* },
|
|
317
|
+
* onUpdate: async ({ transaction }) => {
|
|
318
|
+
* await api.updateTodos(transaction.mutations)
|
|
319
|
+
* },
|
|
320
|
+
* onDelete: async ({ transaction }) => {
|
|
321
|
+
* await api.deleteTodos(transaction.mutations.map(m => m.key))
|
|
322
|
+
* }
|
|
323
|
+
* })
|
|
324
|
+
* )
|
|
276
325
|
*/
|
|
277
326
|
export function queryCollectionOptions<
|
|
278
327
|
TItem extends object,
|