Skip to content

transaction.isPersisted.promise never settles for the second consecutive insert (0.8.x) #1767

Description

@elylucas
  • I've validated the bug against the latest version of DB packages

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

  1. 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 })),
        ),
      }))
  2. Insert a row into contentNodesCollection via an optimistic action, and
    await the transaction's isPersisted.promise. This one resolves.

  3. Without reloading, insert a second row the same way and await its
    isPersisted.promise.

  4. 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.

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