-
Notifications
You must be signed in to change notification settings - Fork 132
Description
NOTE - The below was generated by Opus 4.5. I was having an issue switching a couple of collections from localStorage to localOnly while using a manual transaction. They initially persist but after commit they get removed from the collection.
I asked Opus 4.5 to diagnose what was going on, and it dug into tanstack db, created a patch, and the patch actually worked. I asked it to describe the problem, reproduction and the patch below.
I hope this is helpful, really enjoying tanstack, but I don't know enough to turn this into a proper PR, just a lowly lowly Rails dev ;)
I hope this is helpful!
Side Note: There's actually more than one collection in the transaction, but it fails regardless of the number of collections.
Bug: localOnlyCollectionOptions acceptMutations fails to match mutations by collection ID
Summary
When using localOnlyCollectionOptions with manual transactions that call utils.acceptMutations(), mutations are not persisted and disappear when the transaction completes. The same code works correctly with localStorageCollectionOptions.
Environment
@tanstack/db: 0.5.10@tanstack/react-db: 0.1.54
Reproduction
// Collection definition
export const workoutsCollection = createCollection(
localOnlyCollectionOptions({
id: "training.workouts",
getKey: (item: Workout) => item.id,
schema: WorkoutSchema,
onInsert: performSync,
onUpdate: performSync,
onDelete: performSync,
})
)
// Transaction usage
const tx = createTransaction({
mutationFn: async ({ transaction }) => {
workoutsCollection.utils.acceptMutations(transaction)
await performSync({ transaction })
},
})
tx.mutate(() => {
workoutsCollection.insert(workout)
})Expected Behavior
After the transaction completes, the workout should exist in the collection.
Actual Behavior
The workout exists during optimistic state but disappears when the transaction completes:
[createWorkout] acceptMutations done: workoutExists: true ✓
[createWorkout] tx.isPersisted resolved: workoutExists: false ✗
Switching to localStorageCollectionOptions (with the same code pattern) works correctly.
Root Cause
The issue is in acceptMutations within localOnlyCollectionOptions. It filters mutations to find those belonging to the collection:
Current code (local-only.ts line ~249):
const acceptMutations = (transaction) => {
const collectionMutations = transaction.mutations.filter(
(m) => m.collection === syncResult.collection
);
// ...
};The problem is that syncResult.collection is always null when acceptMutations is called. Here's why:
function createLocalOnlySync(initialData) {
let collection = null; // Starts as null
const sync = {
sync: (params) => {
collection = params.collection; // Set later when sync initializes
},
};
return {
sync,
confirmOperationsSync,
collection, // ← Captured as null at object creation time!
};
}When createLocalOnlySync() returns, collection in the returned object is captured as null. Even though the sync() function later updates the local variable, the returned object's property remains null.
As a result, m.collection === syncResult.collection always compares against null, never matches, and confirmOperationsSync is never called. The mutations stay in optimistic state only, and when the transaction completes, they're cleared.
Why localStorageCollectionOptions Works
The localStorageCollectionOptions implementation has a fallback:
// local-storage.ts
const collectionMutations = transaction.mutations.filter((m) => {
// Try to match by collection reference first
if (sync.collection && m.collection === sync.collection) {
return true;
}
// Fall back to matching by collection ID
return m.collection.id === collectionId; // ← This fallback is missing in localOnly!
});This ID-based fallback ensures mutations are matched even when the collection reference isn't available.
Suggested Fix
Add the same fallback to localOnlyCollectionOptions:
// local-only.ts
const acceptMutations = (transaction) => {
const collectionMutations = transaction.mutations.filter((m) => {
// Try to match by collection reference first
if (syncResult.collection && m.collection === syncResult.collection) {
return true;
}
// Fall back to matching by collection ID
return m.collection.id === config.id;
});
if (collectionMutations.length === 0) {
return;
}
syncResult.confirmOperationsSync(collectionMutations);
};This fix needs to be applied to both:
src/local-only.ts(TypeScript source)dist/esm/local-only.js(compiled output)
Workaround
Until this is fixed, users can apply the patch using patch-package or bun patch.