Describe the bug
When two inserts happen one after another on the same collection, the second
one's transaction.isPersisted.promise never settles. It neither resolves nor
rejects — it just hangs forever.
Both writes are fine. Both rows reach the server and are saved. Only the
promise is stuck.
We log around the await. First insert:
[probe] awaiting isPersisted createScene g1eudlenw7dh9q7nkaa9c703
[probe] isPersisted RESOLVED createScene g1eudlenw7dh9q7nkaa9c703
[probe] navigating to /studio/$projectId/manuscript/$contentNodeId
Second insert, immediately after:
[probe] awaiting isPersisted createScene mna5k3arqjl2cv4b9ewwjv6p
...and nothing follows. No resolve, no reject, no error in the console. The
server received both calls and the database has both rows.
Why it matters
We use that promise to know a write is durable before navigating the user to
the row they just created:
await tx.isPersisted.promise
navigate(newRow)
Because the promise never settles, the navigation never happens. The user
creates something and is left sitting on the previous item, with no error
shown. Any code that awaits durability before doing something will hang the
same way.
To Reproduce
-
Set up a collection using persistedQueryCollectionOptions, read by a live
query that has a leftJoin and a correlated toArray subquery:
q.from({ nodes: contentNodesCollection })
.where(({ nodes }) => eq(nodes.id, contentNodeId))
.leftJoin({ user: usersCollection }, ({ nodes, user }) =>
eq(nodes.updatedBy, user.id),
)
.select(({ nodes, user }) => ({
...nodes,
updatedByUser: user,
tags: toArray(
q.from({ nodeTag: contentNodeTagsCollection })
.where(({ nodeTag }) => eq(nodeTag.contentNodeId, nodes.id))
.innerJoin({ tag: tagsCollection }, ({ nodeTag, tag }) =>
eq(nodeTag.tagId, tag.id),
)
.select(({ tag }) => ({ id: tag.id, name: tag.name, color: tag.color })),
),
}))
-
Insert a row into contentNodesCollection via an optimistic action, and
await the transaction's isPersisted.promise. This one resolves.
-
Without reloading, insert a second row the same way and await its
isPersisted.promise.
-
The second promise never settles. Both rows are saved on the server.
Expected behavior
The second insert's isPersisted.promise resolves, the same as the first.
Screenshots
n/a
Desktop:
- OS: macOS 26.5.2
- Browser: Chromium 148.0.7778.280 (Electron 42.9.2)
- Version:
@tanstack/db 0.8.4, @tanstack/react-db 0.3.4,
@tanstack/query-db-collection 1.2.9,
@tanstack/offline-transactions 1.0.50
Smartphone:
n/a — not tested on mobile.
Additional context
Versions we tried:
| Versions |
Result |
db 0.6.13, react-db 0.1.91 |
fine |
db 0.7.2, react-db 0.2.1 |
fine |
db 0.8.3, react-db 0.3.3 |
broken, different symptom (below) |
db 0.8.4, react-db 0.3.4 |
broken as described above |
On 0.8.3 the same scenario failed differently: instead of hanging, the
promise rejected, with
Error: Query contributors with the same row key are not congruent
at ReduceOperator.<anonymous>
at ReduceOperator.run
at D2.step
at D2.run
at CollectionConfigBuilder.maybeRunGraph
at CollectionConfigBuilder.executeGraphRun
at run
at Scheduler.flush
at withPublicationContext
at CollectionChangesManager.publishEvents
0.8.4 no longer throws that — we assume #1761 addressed it — but the promise
now hangs instead. We can't tell from outside whether these are the same
underlying problem, so both are recorded here.
How we narrowed it: we hit this while upgrading several unrelated packages at
once, and went commit by commit. Everything passed until db 0.8.3 landed,
and passed again on 0.7.2 with nothing else changed. Our end-to-end suite
catches it reliably (2 of 4 tests in one file), and it reproduces by hand.
Rest of the stack: React 19, TanStack Start 1.168.49, Node 22.18.
One caveat on our own testing: an earlier round of manual probing was done in
a browser profile left dirty by unrelated offline testing of ours, which
produced a confusing third symptom. The behaviour reported above was
re-confirmed after clearing IndexedDB and local storage, so it isn't that.
We don't have a small standalone reproduction — this is from a real app.
Happy to build one if that would help.
Describe the bug
When two inserts happen one after another on the same collection, the second
one's
transaction.isPersisted.promisenever settles. It neither resolves norrejects — it just hangs forever.
Both writes are fine. Both rows reach the server and are saved. Only the
promise is stuck.
We log around the await. First insert:
Second insert, immediately after:
...and nothing follows. No resolve, no reject, no error in the console. The
server received both calls and the database has both rows.
Why it matters
We use that promise to know a write is durable before navigating the user to
the row they just created:
Because the promise never settles, the navigation never happens. The user
creates something and is left sitting on the previous item, with no error
shown. Any code that awaits durability before doing something will hang the
same way.
To Reproduce
Set up a collection using
persistedQueryCollectionOptions, read by a livequery that has a
leftJoinand a correlatedtoArraysubquery:Insert a row into
contentNodesCollectionvia an optimistic action, andawait the transaction's
isPersisted.promise. This one resolves.Without reloading, insert a second row the same way and await its
isPersisted.promise.The second promise never settles. Both rows are saved on the server.
Expected behavior
The second insert's
isPersisted.promiseresolves, the same as the first.Screenshots
n/a
Desktop:
@tanstack/db0.8.4,@tanstack/react-db0.3.4,@tanstack/query-db-collection1.2.9,@tanstack/offline-transactions1.0.50Smartphone:
n/a — not tested on mobile.
Additional context
Versions we tried:
db0.6.13,react-db0.1.91db0.7.2,react-db0.2.1db0.8.3,react-db0.3.3db0.8.4,react-db0.3.4On 0.8.3 the same scenario failed differently: instead of hanging, the
promise rejected, with
0.8.4 no longer throws that — we assume #1761 addressed it — but the promise
now hangs instead. We can't tell from outside whether these are the same
underlying problem, so both are recorded here.
How we narrowed it: we hit this while upgrading several unrelated packages at
once, and went commit by commit. Everything passed until
db0.8.3 landed,and passed again on 0.7.2 with nothing else changed. Our end-to-end suite
catches it reliably (2 of 4 tests in one file), and it reproduces by hand.
Rest of the stack: React 19, TanStack Start 1.168.49, Node 22.18.
One caveat on our own testing: an earlier round of manual probing was done in
a browser profile left dirty by unrelated offline testing of ours, which
produced a confusing third symptom. The behaviour reported above was
re-confirmed after clearing IndexedDB and local storage, so it isn't that.
We don't have a small standalone reproduction — this is from a real app.
Happy to build one if that would help.