Skip to content

Global tx for local and sync#3428

Open
Takeno wants to merge 16 commits into
mainfrom
feat/global-tx
Open

Global tx for local and sync#3428
Takeno wants to merge 16 commits into
mainfrom
feat/global-tx

Conversation

@Takeno
Copy link
Copy Markdown
Contributor

@Takeno Takeno commented Feb 5, 2026

This PR adds atomic transactions for CoValue mutations: multiple mutations can be executed inside a single withTransaction() callback and are then persisted together in one IndexedDB transaction and synced together in one network BatchMessage. Mutations are applied to memory immediately (optimistic); persistence and sync happen once the callback returns, with a single attempt for each. No automatic retry and no rollback of in-memory state.

This is primarily to protect against the client being closed in the middle of a sync/store operation.

It is based on already existing concepts of DBTransactionInterfaceAsync.

DX

account.$jazz.unstable_withTransaction(() => {
  const map = TestMap.create({ name: "test", value: 0 });
  map.$jazz.set("name", "updated");
  map.$jazz.set("value", 42);
});

Critical changes

  • The addition of upsertCoValue based on an existing transaction.
  • The addition of a Batch message, which is sent to every remote peer. The batch message is handled like a simple list of messages. Its role is to ensure messages arrive together; we can't guarantee they are stored successfully.

Limitations

  • If coValues inside withTransaction interact with coValues mutated outside, the indexeddb tx might fail because upsertCoValue looks inside the database and may not find the CoValue due to transaction concurrency.
  • The Batch message's payload sent to the sync server might be bigger than transport limits for a single message.
  • In case of failure on store/sync, the in-memory state is inconsistent. But this is already the current behaviour for every coValue
  • Callbacks must be synchronous, to avoid nested transactions or endless promises.
  • No ACK from sync server for batch messages: since every message in the batch is processed as usual, the server will follow the common flow after a NewContent message is received.

@vercel
Copy link
Copy Markdown

vercel Bot commented Feb 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
clerk-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
file-upload-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
form-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
gcmp-homepage Ready Ready Preview, Comment Feb 18, 2026 11:19am
image-upload-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-chat Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-chat-1 Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-chat-2 Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-filestream Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-image-upload Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-inspector Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-multi-cursors Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-nextjs Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-organization Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-paper-scissors Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-richtext Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-todo Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-vector-search Ready Ready Preview, Comment Feb 18, 2026 11:19am
jazz-version-history Ready Ready Preview, Comment Feb 18, 2026 11:19am
music-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
passkey-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
passphrase-auth-demo Ready Ready Preview, Comment Feb 18, 2026 11:19am
1 Skipped Deployment
Project Deployment Actions Updated (UTC)
jazz-homepage Ignored Ignored Preview Feb 18, 2026 11:19am

Request Review

@Takeno Takeno marked this pull request as ready for review February 11, 2026 10:34
@cursor
Copy link
Copy Markdown

cursor Bot commented Feb 11, 2026

You have run out of free Bugbot PR reviews for this billing cycle. This will reset on February 27.

To receive reviews on all of your PRs, visit the Cursor dashboard to activate Pro and start your 14-day free trial.

Copy link
Copy Markdown
Contributor

@nrainhart nrainhart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new API looks good! I feel like the final implementation should provide stronger guarantees, such as:

  • roll back in-memory changes if an error is thrown during the callback execution
  • store batch transactionally on the server
  • (optionally) waiting for confirmation from the server before resolving the promise?

But we can start trying out this version now.

As a sidenote, I wonder if using withTransaction actually introduces any noticeable performance improvements when doing several operations at the same time. Would it make sense to use it in methods like applyDiff and CoList.push?

Comment thread packages/jazz-tools/src/tools/tests/account.test.ts Outdated
Comment thread packages/cojson/src/tests/localNode.transactions.test.ts Outdated
Comment thread packages/cojson/src/transactionContext.ts Outdated
Comment thread packages/cojson/src/sync.ts Outdated
Comment thread packages/cojson/src/sync.ts Outdated
export class SQLiteTransactionAsync implements DBTransactionInterfaceAsync {
constructor(private readonly tx: SQLiteDatabaseDriverAsync) {}

async upsertCoValue(
Copy link
Copy Markdown
Contributor

@nrainhart nrainhart Feb 16, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't love that we're duplicating upsertCoValue in the DB client and transaction APIs. I think we need to rethink this DB client/transaction split, because in many cases we want to run queries both inside and outside a transaction, and this leads us to either code duplication or creating unnecessary transactions (see also my previous comment) cc @gdorsi

We don't need to fix this in this PR, thought. Maybe let's just add a comment to keep both implementations from drifting apart

Comment thread packages/cojson/src/storage/storageAsync.ts
Comment thread packages/cojson/src/storage/storageAsync.ts Outdated
Comment thread packages/cojson/src/storage/storageAsync.ts Outdated
Comment thread packages/cojson/src/storage/storageAsync.ts Outdated
@Takeno
Copy link
Copy Markdown
Contributor Author

Takeno commented Feb 17, 2026

The new API looks good! I feel like the final implementation should provide stronger guarantees, such as:

* roll back in-memory changes if an error is thrown during the callback execution

* store batch transactionally on the server

* (optionally) waiting for confirmation from the server before resolving the promise?

But we can start trying out this version now.

  • 1 Rollback in-memory changes is something much harder with the current paradigm, since the transactions stored/synced are based on the diffs with in-memory. Currently, normal mutations also have no rollbacks if anything fails.
    Should we rename withTransaction to something more like batchUpdates?

  • 2/3 The main purpose is to avoid partial storage for deeply coupled mutations. Once it is successfully stored locally, we can rely on the background syncing in case the sync didn't go through.
    Would Promise.race be better than Promise.allSettled in this case?

As a sidenote, I wonder if using withTransaction actually introduces any noticeable performance improvements when doing several operations at the same time. Would it make sense to use it in methods like applyDiff and CoList.push?

I observed some performance improvements in browser-perf-tests, creating 50x50 grids, from ~2300ms to ~1900ms.
I would start releasing this under unstable_ prefix, gather feedback, and later opt-in in some internals like applyDiff / push.

@Takeno Takeno requested a review from nrainhart February 17, 2026 15:39
Copy link
Copy Markdown
Contributor

@nrainhart nrainhart left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Leaving a few more comments, but only one's important.

Should we rename withTransaction to something more like batchUpdates

Yeah, this actually may be better to avoid setting incorrect expectations. Wanna discuss this with the rest of the team?

I would start releasing this under unstable_ prefix, gather feedback, and later opt-in in some internals like applyDiff / push.

Sounds good!

Comment on lines 1449 to +1450
this.trySendToPeer(peer, content);
peer.combineOptimisticWith(coValue.id, contentKnownState);
peer.trackToldKnownState(coValue.id);
for (const content of contents) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's a bit confusing that the two content variables here refer to different things. We could rename the param to contentMsg

: Promise.resolve(undefined);

const contentKnownState = knownStateFromContent(content);
const syncPromise = this.syncContent({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there any reason not to have the same test for StorageApiAsync?

Comment thread packages/cojson/src/storage/storageAsync.ts Outdated
Comment on lines +364 to +368
return new Promise((resolve, reject) => {
this.dbClient.transaction((tx) => {
return callback(tx).then(resolve, reject);
});
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ This will create several promises and cojson transactions if a tx is not provided, which could impact performance.

Can we always pass a transaction instead? That way for regular stores we use one tx per store operation

Co-authored-by: Nico Rainhart <nmrainhart@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants