@tanstack/query-db-collection 1.0.17 → 1.0.18

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.
@@ -51,7 +51,7 @@ function validateOperations(operations, ctx) {
51
51
  function performWriteOperations(operations, ctx) {
52
52
  const normalized = normalizeOperations(operations, ctx);
53
53
  validateOperations(normalized, ctx);
54
- ctx.begin();
54
+ ctx.begin({ immediate: true });
55
55
  for (const op of normalized) {
56
56
  switch (op.type) {
57
57
  case `insert`: {
@@ -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// 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 * Optional function to update the query cache with the latest synced data.\n * Handles both direct array caches and wrapped response formats (when `select` is used).\n * If not provided, falls back to directly setting the cache with the raw array.\n */\n updateCacheData?: (items: Array<TRow>) => 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 // NOTE: These validations check the synced store only, not the combined view (synced + optimistic)\n // This allows write operations to work correctly even when items are optimistically modified\n if (op.type === `update`) {\n if (!ctx.collection._state.syncedData.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection._state.syncedData.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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n // Check synced store only, not the combined view\n const existsInSyncedStore = ctx.collection._state.syncedData.has(op.key)\n const resolved = ctx.collection.validateData(\n op.data,\n existsInSyncedStore ? `update` : `insert`,\n op.key,\n )\n if (existsInSyncedStore) {\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 = Array.from(ctx.collection._state.syncedData.values())\n if (ctx.updateCacheData) {\n ctx.updateCacheData(updatedData)\n } else {\n // Fallback: directly set the cache with raw array (for non-Query Collection consumers)\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n }\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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"}
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 /**\n * Begin a new sync transaction.\n * @param options.immediate - When true, the transaction will be processed immediately\n * even if there are persisting user transactions. Used by manual write operations.\n */\n begin: (options?: { immediate?: boolean }) => void\n write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n commit: () => void\n /**\n * Optional function to update the query cache with the latest synced data.\n * Handles both direct array caches and wrapped response formats (when `select` is used).\n * If not provided, falls back to directly setting the cache with the raw array.\n */\n updateCacheData?: (items: Array<TRow>) => 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 // NOTE: These validations check the synced store only, not the combined view (synced + optimistic)\n // This allows write operations to work correctly even when items are optimistically modified\n if (op.type === `update`) {\n if (!ctx.collection._state.syncedData.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection._state.syncedData.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 // Use immediate: true to ensure syncedData is updated synchronously,\n // even when called from within a mutationFn with an active persisting transaction\n ctx.begin({ immediate: true })\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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n // Check synced store only, not the combined view\n const existsInSyncedStore = ctx.collection._state.syncedData.has(op.key)\n const resolved = ctx.collection.validateData(\n op.data,\n existsInSyncedStore ? `update` : `insert`,\n op.key,\n )\n if (existsInSyncedStore) {\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 = Array.from(ctx.collection._state.syncedData.values())\n if (ctx.updateCacheData) {\n ctx.updateCacheData(updatedData)\n } else {\n // Fallback: directly set the cache with raw array (for non-Query Collection consumers)\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n }\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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"}
@@ -18,7 +18,14 @@ export interface SyncContext<TRow extends object, TKey extends string | number =
18
18
  queryClient: QueryClient;
19
19
  queryKey: Array<unknown>;
20
20
  getKey: (item: TRow) => TKey;
21
- begin: () => void;
21
+ /**
22
+ * Begin a new sync transaction.
23
+ * @param options.immediate - When true, the transaction will be processed immediately
24
+ * even if there are persisting user transactions. Used by manual write operations.
25
+ */
26
+ begin: (options?: {
27
+ immediate?: boolean;
28
+ }) => void;
22
29
  write: (message: Omit<ChangeMessage<TRow>, `key`>) => void;
23
30
  commit: () => void;
24
31
  /**
@@ -18,7 +18,14 @@ export interface SyncContext<TRow extends object, TKey extends string | number =
18
18
  queryClient: QueryClient;
19
19
  queryKey: Array<unknown>;
20
20
  getKey: (item: TRow) => TKey;
21
- begin: () => void;
21
+ /**
22
+ * Begin a new sync transaction.
23
+ * @param options.immediate - When true, the transaction will be processed immediately
24
+ * even if there are persisting user transactions. Used by manual write operations.
25
+ */
26
+ begin: (options?: {
27
+ immediate?: boolean;
28
+ }) => void;
22
29
  write: (message: Omit<ChangeMessage<TRow>, `key`>) => void;
23
30
  commit: () => void;
24
31
  /**
@@ -49,7 +49,7 @@ function validateOperations(operations, ctx) {
49
49
  function performWriteOperations(operations, ctx) {
50
50
  const normalized = normalizeOperations(operations, ctx);
51
51
  validateOperations(normalized, ctx);
52
- ctx.begin();
52
+ ctx.begin({ immediate: true });
53
53
  for (const op of normalized) {
54
54
  switch (op.type) {
55
55
  case `insert`: {
@@ -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// 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 * Optional function to update the query cache with the latest synced data.\n * Handles both direct array caches and wrapped response formats (when `select` is used).\n * If not provided, falls back to directly setting the cache with the raw array.\n */\n updateCacheData?: (items: Array<TRow>) => 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 // NOTE: These validations check the synced store only, not the combined view (synced + optimistic)\n // This allows write operations to work correctly even when items are optimistically modified\n if (op.type === `update`) {\n if (!ctx.collection._state.syncedData.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection._state.syncedData.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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n // Check synced store only, not the combined view\n const existsInSyncedStore = ctx.collection._state.syncedData.has(op.key)\n const resolved = ctx.collection.validateData(\n op.data,\n existsInSyncedStore ? `update` : `insert`,\n op.key,\n )\n if (existsInSyncedStore) {\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 = Array.from(ctx.collection._state.syncedData.values())\n if (ctx.updateCacheData) {\n ctx.updateCacheData(updatedData)\n } else {\n // Fallback: directly set the cache with raw array (for non-Query Collection consumers)\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n }\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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"}
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 /**\n * Begin a new sync transaction.\n * @param options.immediate - When true, the transaction will be processed immediately\n * even if there are persisting user transactions. Used by manual write operations.\n */\n begin: (options?: { immediate?: boolean }) => void\n write: (message: Omit<ChangeMessage<TRow>, `key`>) => void\n commit: () => void\n /**\n * Optional function to update the query cache with the latest synced data.\n * Handles both direct array caches and wrapped response formats (when `select` is used).\n * If not provided, falls back to directly setting the cache with the raw array.\n */\n updateCacheData?: (items: Array<TRow>) => 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 // NOTE: These validations check the synced store only, not the combined view (synced + optimistic)\n // This allows write operations to work correctly even when items are optimistically modified\n if (op.type === `update`) {\n if (!ctx.collection._state.syncedData.has(op.key)) {\n throw new UpdateOperationItemNotFoundError(op.key)\n }\n } else if (op.type === `delete`) {\n if (!ctx.collection._state.syncedData.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 // Use immediate: true to ensure syncedData is updated synchronously,\n // even when called from within a mutationFn with an active persisting transaction\n ctx.begin({ immediate: true })\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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.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 // Get from synced store only, not the combined view\n const currentItem = ctx.collection._state.syncedData.get(op.key)!\n ctx.write({\n type: `delete`,\n value: currentItem,\n })\n break\n }\n case `upsert`: {\n // Check synced store only, not the combined view\n const existsInSyncedStore = ctx.collection._state.syncedData.has(op.key)\n const resolved = ctx.collection.validateData(\n op.data,\n existsInSyncedStore ? `update` : `insert`,\n op.key,\n )\n if (existsInSyncedStore) {\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 = Array.from(ctx.collection._state.syncedData.values())\n if (ctx.updateCacheData) {\n ctx.updateCacheData(updatedData)\n } else {\n // Fallback: directly set the cache with raw array (for non-Query Collection consumers)\n ctx.queryClient.setQueryData(ctx.queryKey, updatedData)\n }\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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/query-db-collection",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "TanStack Query collection for TanStack DB",
5
5
  "author": "Kyle Mathews",
6
6
  "license": "MIT",
@@ -40,7 +40,7 @@
40
40
  ],
41
41
  "dependencies": {
42
42
  "@standard-schema/spec": "^1.1.0",
43
- "@tanstack/db": "0.5.20"
43
+ "@tanstack/db": "0.5.21"
44
44
  },
45
45
  "peerDependencies": {
46
46
  "@tanstack/query-core": "^5.0.0",
@@ -35,7 +35,12 @@ export interface SyncContext<
35
35
  queryClient: QueryClient
36
36
  queryKey: Array<unknown>
37
37
  getKey: (item: TRow) => TKey
38
- begin: () => void
38
+ /**
39
+ * Begin a new sync transaction.
40
+ * @param options.immediate - When true, the transaction will be processed immediately
41
+ * even if there are persisting user transactions. Used by manual write operations.
42
+ */
43
+ begin: (options?: { immediate?: boolean }) => void
39
44
  write: (message: Omit<ChangeMessage<TRow>, `key`>) => void
40
45
  commit: () => void
41
46
  /**
@@ -144,7 +149,9 @@ export function performWriteOperations<
144
149
  const normalized = normalizeOperations(operations, ctx)
145
150
  validateOperations(normalized, ctx)
146
151
 
147
- ctx.begin()
152
+ // Use immediate: true to ensure syncedData is updated synchronously,
153
+ // even when called from within a mutationFn with an active persisting transaction
154
+ ctx.begin({ immediate: true })
148
155
 
149
156
  for (const op of normalized) {
150
157
  switch (op.type) {