Skip to content

Bug: Transactions do not work when using localOnlyCollection #446

@seanfronteras

Description

@seanfronteras

Example:

// transaction-test.js
// Run with: node transaction-test.js

import { createCollection, createTransaction } from '@tanstack/db';
import { localOnlyCollectionOptions } from '@tanstack/db';

console.log('🧪 Starting TanStack DB LocalOnly Transaction Test\n');

const testCollection = createCollection(
  localOnlyCollectionOptions({
    id: 'test-collection',
    getKey: (item) => item.id,
    initialData: [
      { id: 'initial1', name: 'Initial Item 1', value: 100 },
      { id: 'initial2', name: 'Initial Item 2', value: 200 },
    ],
    onInsert: async ({ transaction }) => {
      const inserted = transaction.mutations[0].modified;
      console.log('  📦 onInsert triggered:', inserted.name);
    },
    onUpdate: async ({ transaction }) => {
      const { original, modified } = transaction.mutations[0];
      console.log(
        '  📦 onUpdate triggered:',
        original.name,
        '->',
        modified.name
      );
    },
    onDelete: async ({ transaction }) => {
      const deleted = transaction.mutations[0].original;
      console.log('  📦 onDelete triggered:', deleted.name);
    },
  })
);

console.log('1️⃣ Initial collection state:');
console.log('   Array:', testCollection.toArray);
console.log('   Count:', testCollection.size, '\n');

// Test 1: Direct operations (should work fine)
console.log('2️⃣ Testing direct operations:');
testCollection.insert({ id: 'direct1', name: 'Direct Insert', value: 300 });
console.log('   After direct insert - Count:', testCollection.size);
console.log('   Inserted item:', testCollection.get('direct1'));

const firstItem = testCollection.toArray[0];
if (firstItem) {
  testCollection.update(firstItem.id, (draft) => {
    draft.name = 'Updated Direct';
    draft.value = 999;
  });
  console.log('   After direct update - Count:', testCollection.size);
  console.log('   Updated item:', testCollection.get(firstItem.id));
}
console.log();

// Test 2: Manual transaction
console.log('3️⃣ Testing manual transaction:');
console.log('   Before transaction - Count:', testCollection.size);
console.log('   Array:', testCollection.toArray);

const transaction = createTransaction({
  autoCommit: false,
  mutationFn: async ({ transaction }) => {
    console.log(
      '   🎯 Transaction mutationFn called with',
      transaction.mutations.length,
      'mutations'
    );
  },
});

console.log('   Creating transaction mutate block...');
transaction.mutate(() => {
  console.log('   🔄 Inside mutate block');
  console.log('Inserting item tx1');
  testCollection.insert({
    id: 'tx1',
    name: 'Transaction Insert 1',
    value: 400,
  });
  console.log('Inserting item tx2');
  testCollection.insert({
    id: 'tx2',
    name: 'Transaction Insert 2',
    value: 500,
  });

  console.log('Updating item initial2');
  const itemToUpdate = testCollection.get('initial2');
  if (itemToUpdate) {
    testCollection.update('initial2', (draft) => {
      draft.name = 'Updated via Transaction';
      draft.value = 777;
    });
  }
});

console.log('   Before commit - Count:', testCollection.size);
console.log(
  '   Items:',
  testCollection.toArray.map((i) => ({ id: i.id, name: i.name }))
);

console.log('   Committing transaction...');
try {
  await transaction.commit();
  console.log('   ✅ Transaction committed successfully');
} catch (error) {
  console.log('   ❌ Transaction commit failed:', error.message);
}

console.log('   After commit - Count:', testCollection.size);
console.log(
  '   Final items:',
  testCollection.toArray.map((i) => ({
    id: i.id,
    name: i.name,
    value: i.value,
  }))
);

Output:

🧪 Starting TanStack DB LocalOnly Transaction Test

1️⃣ Initial collection state:
   Array: [
  { id: 'initial1', name: 'Initial Item 1', value: 100 },
  { id: 'initial2', name: 'Initial Item 2', value: 200 }
]
   Count: 2 

2️⃣ Testing direct operations:
  📦 onInsert triggered: Direct Insert
   After direct insert - Count: 3
   Inserted item: { id: 'direct1', name: 'Direct Insert', value: 300 }
  📦 onUpdate triggered: Initial Item 1 -> Updated Direct
   After direct update - Count: 3
   Updated item: { id: 'initial1', name: 'Updated Direct', value: 999 }

3️⃣ Testing manual transaction:
   Before transaction - Count: 3
   Array: [
  { id: 'initial1', name: 'Updated Direct', value: 999 },
  { id: 'initial2', name: 'Initial Item 2', value: 200 },
  { id: 'direct1', name: 'Direct Insert', value: 300 }
]
   Creating transaction mutate block...
   🔄 Inside mutate block
Inserting item tx1
Inserting item tx2
Updating item initial2
   Before commit - Count: 5
   Items: [
  { id: 'initial1', name: 'Updated Direct' },
  { id: 'initial2', name: 'Updated via Transaction' },
  { id: 'direct1', name: 'Direct Insert' },
  { id: 'tx1', name: 'Transaction Insert 1' },
  { id: 'tx2', name: 'Transaction Insert 2' }
]
   Committing transaction...
   🎯 Transaction mutationFn called with 3 mutations
   ✅ Transaction committed successfully
   After commit - Count: 3
   Final items: [
  { id: 'initial1', name: 'Updated Direct', value: 999 },
  { id: 'initial2', name: 'Initial Item 2', value: 200 },
  { id: 'direct1', name: 'Direct Insert', value: 300 }
]

Expected behavior: Committing a transaction applies the changes as a batch update.
Actual behavior: Committing a transaction erases the changes.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions