Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
66 commits
Select commit Hold shift + click to select a range
036e9fb
test(conformance): add the eager live-source adapter conformance oracle
Oct 29, 2026
6e19f76
test(rxdb): run RxDB on the eager live-source conformance oracle
Oct 29, 2026
844b97b
test(conformance): generate batches committed during the initial fetch
Oct 29, 2026
f15cd73
fix(rxdb): apply a buffered insert over the row the initial read holds
Oct 29, 2026
f82985a
test(conformance): generate field removal and check update events
Oct 29, 2026
7aa1946
fix(rxdb): replace the synced row on a document update
Oct 29, 2026
3812216
test(db): name replay owners for the adapter conformance properties
Oct 29, 2026
e528597
fix(db): publish a synced update that changes only -0 to 0
Oct 29, 2026
443bd61
fix(db): treat -0 and 0 as distinct in writes and change detection
Oct 30, 2026
9856fa5
test(conformance): generate complete deliveries with two rows for one…
Oct 30, 2026
9f0841a
test(db): generate failed-replay histories in the publication oracle
Oct 30, 2026
7958b8e
test(db): hold back the failed-replay publication generator
Oct 30, 2026
dad3408
test(conformance): allow asynchronous providers and a propagation step
Oct 30, 2026
3cbd0ff
fix(powersync): release the change watcher when the first load fails
Oct 30, 2026
2830454
test(conformance): generate typed fields and run them through PowerSync
Oct 30, 2026
39294b6
test(conformance): client-created failures, change-carried txids, ret…
Oct 30, 2026
8158548
fix(electric): report extended outages, scope txid evidence to a run,…
Oct 30, 2026
8da7afa
test(conformance): outages anywhere, matched failure values, waits re…
Oct 30, 2026
e67c4ba
fix(electric): count only shape-log outages, keep retired evidence un…
Oct 30, 2026
9401176
test(conformance): empty responses restart the outage count; missing …
Nov 2, 2026
2dd5f0e
fix(electric): fail the run when a give-up preceded the first snapsho…
Nov 2, 2026
f4a03a4
fix(electric): an external abort after ready is the source's error
Nov 2, 2026
b2ecbcf
test(conformance): delivery errors and reported rejected deliveries
Nov 2, 2026
6aa2671
fix(convex): release the query subscription when the first load fails
Nov 2, 2026
c25a5bb
test(conformance): declare reach for a restart after a failed run
Nov 2, 2026
759dded
fix(convex): a cached query error does not fail a new subscription
Nov 2, 2026
ab648b9
chore: add the db-adapter-conformance package to the lockfile
Nov 2, 2026
6d31ddf
docs(conformance): document the driver fields added since Electric
Nov 2, 2026
1f2f44a
docs(conformance): plan the duplicated-test audit and defer publishing
Nov 2, 2026
f06ee8f
Merge origin/main into feat/adapter-conformance-laws
Nov 2, 2026
877f1a1
fix(db): keep -0 distinct in transaction merges and ordered Effects
Nov 2, 2026
aaebae8
fix(convex): awaitSync waits through a cached error and never re-repo…
Nov 2, 2026
370e029
test: declare zero-sign publication reach; fix the Neon config pointer
Nov 2, 2026
9c0f059
fix(db): -0 inside typed arrays, and ordered Effects compare class in…
Nov 2, 2026
7fba045
Apply code simplification review
Nov 2, 2026
e975e94
Merge origin/main into feat/adapter-conformance-laws
Nov 2, 2026
f3b761f
ci: apply automated fixes
autofix-ci[bot] Nov 2, 2026
3b53029
Fix CI failure: assign the conformance package a test group
Nov 2, 2026
2c78d9c
Fix CI failure: build an outage FetchError's headers without iteratin…
Nov 2, 2026
94a85bb
fix(convex): keep a fresh server error that arrives before the cached…
Nov 2, 2026
564a3b5
fix(electric): reject a wait made after an abort stopped the sync run
Nov 2, 2026
4ea8830
fix(db): a sync insert that differs only by a zero's sign is a duplicate
Nov 2, 2026
755af34
fix(db-adapter-conformance): remove the check's patches when a driver…
Nov 2, 2026
ec576f8
fix(electric): name the status and request in an outage's error message
Nov 2, 2026
dac45c2
fix(electric): export a retired run's txids only once the current run…
Nov 2, 2026
a23b039
docs(db-adapter-conformance): refresh the evidence block and TrailBas…
Nov 2, 2026
8aa144b
test(powersync): release the diff trigger when the first load's accep…
Nov 2, 2026
51e0a8e
ci: apply automated fixes
autofix-ci[bot] Nov 2, 2026
683d4f4
docs(electric): count a failed catch-up prefetch toward the outage th…
Nov 2, 2026
3f851bf
Fix CI failure: reach every descriptor error-handoff history in the r…
Nov 2, 2026
ba7c6b6
fix(db-ivm): an ordered query delivers an update that changes only a …
Nov 2, 2026
e0eca76
fix(electric): count a fetchClient that throws before it returns as a…
Nov 2, 2026
a00cdcd
fix(db-ivm): apply the signed-zero rule to object headers in equalRow…
Nov 2, 2026
58c5bd1
fix(db-adapter-conformance): check LIFE-5 at a prompt restart's own cut
Nov 2, 2026
ee012e1
test: assert declared reach only when its generated campaign ran
Nov 2, 2026
b7cff8f
test(electric): scale the SDK campaign timeout with the run multiplier
Nov 3, 2026
d8b2940
test(db-adapter-conformance): isolate the prompt-restart cut and chec…
Nov 3, 2026
4e0ef4c
test(db-ivm): state that row-value headers treat NaN as equal to NaN
Nov 3, 2026
0f5f759
test(electric): generate live attempts as any order of 503s and synch…
Nov 3, 2026
126b7a7
fix(convex): keep a cached error from failing a restart behind a clie…
Nov 3, 2026
4e665f5
fix(db-adapter-conformance): dispose a fixture whose seed fails, and …
Nov 3, 2026
3ae9018
refactor(electric): record an external abort's failure only where REA…
Nov 3, 2026
0ac99be
test(db): check the retention reach inside each generated campaign
Nov 3, 2026
88f5ea2
docs: record the Convex wrapper fallback and the Electric live-attemp…
Nov 3, 2026
195f3ec
refactor(db): one comparison mode for deepEqualsInternal; typed array…
Nov 3, 2026
77ef9a5
refactor(db-ivm): compare a row's versions without a separate wrapper…
Nov 3, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/convex-failed-load-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/convex-db-collection': patch
---

Release the Convex query subscription when a Collection's first load fails, or when a later acceptance failure stops the sync run, instead of keeping it until cleanup. A new subscription no longer fails from an error the Convex client cached for its query before it subscribed, such as a restart right after a failed run; it waits for the server's result, also when the server's answer arrives before the client replays the cached error. `awaitSync` during that wait no longer fails the run from the cached error. While the latest result is a failure, `awaitSync` rejects with the reported failure instead of reporting it again.
5 changes: 5 additions & 0 deletions .changeset/electric-retry-health-and-evidence.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/electric-db-collection': patch
---

Report an extended Electric outage in `collection.health`. After the Collection is ready, five failed requests to the shape log in a row, retried by the client or by an `onError` that returns a retry, show as the source node's `failureReason` with the fifth attempt's error, and the next applied up-to-date clears it. Shorter outages, and retried subset snapshot requests, leave health unchanged. A restarted or reloaded Collection resolves `awaitTxId` for a txid the previous sync run delivered only once it is ready again, instead of at once, and the sync metadata it exports for hydration carries those txids only from then on. When the stream fails, pending `awaitTxId` and `awaitMatch` calls reject instead of waiting for their timeout, and so do calls made after the failure, also when an abort stopped the stream. An `onError` handler that throws now gives up the stream with its own error, without an unhandled rejection. A give-up while the first snapshot's acceptance is pending, under a persistence wrapper, now fails the Collection once that acceptance makes it ready, instead of leaving a stopped stream with healthy status. Aborting `shapeOptions.signal` after the Collection is ready now shows as the source node's `error`, since the Collection no longer syncs.
5 changes: 5 additions & 0 deletions .changeset/powersync-failed-load-release.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/powersync-db-collection': patch
---

Release the change watcher and any diff trigger when an eager Collection's first load fails, instead of keeping them until cleanup.
5 changes: 5 additions & 0 deletions .changeset/rxdb-buffered-insert-replay.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/rxdb-db-collection': patch
---

Fix initial sync failing with "Cannot insert document ... from sync because it already exists" when a document is inserted and then updated while the initial fetch is running. The buffered insert event is now applied over the row that the initial read already returned.
5 changes: 5 additions & 0 deletions .changeset/rxdb-full-row-updates.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
'@tanstack/rxdb-db-collection': patch
---

Remove a field from a synced row when an RxDB document update removes it. RxDB change events carry the whole document, so the adapter now replaces the row instead of merging the update into it.
6 changes: 6 additions & 0 deletions .changeset/sync-negative-zero-change.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@tanstack/db': patch
'@tanstack/db-ivm': patch
---

Treat `-0` and `0` as distinct row values. A synced update, a replay that replaces a subscription's rows, and an optimistic update that change a field only from `-0` to `0` (or back) now publish a change; before, the Collection could store the new value while subscribers kept the old one. A draft write of `0` over `-0` is now recorded as a change instead of being dropped. A transaction that deletes a row and reinserts it with only a zero's sign changed now sends that update instead of dropping it, and an ordered live query or Effect (`orderBy`, with or without `limit`) now delivers an update that changes only a zero's sign; before, it kept the old value. Join, distinct, and grouping keys still treat `-0` and `0` as equal. A sync insert of a held key whose row differs only by a zero's sign is now a duplicate key, not an equal re-insert. The same holds inside typed arrays. An ordered Effect also delivers an update whose value is a class instance, which can change in private state its enumerable fields do not show.
1 change: 1 addition & 0 deletions .prettierignore
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,4 @@ pnpm-lock.yaml
**/routeTree.gen.ts
packages/convex-db-collection/e2e/backend/.convex/
packages/convex-db-collection/e2e/backend/convex/_generated/
packages/db-adapter-conformance/tests/neon/vendor/
4 changes: 2 additions & 2 deletions docs/collections/convex-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -322,9 +322,9 @@ convexCollectionOptions({
})
```

An initial subscription or projection error rejects `preload()` with the original error and ends that sync run. Call `collection.reload()` to try again. Later recoverable errors preserve existing rows, set `collection.health.failureReason`, and call `onError`. A valid query result clears that failure reason. `collection.utils.getSyncError()` returns the latest error, but this getter is not reactive. A rejected source acceptance stops the run and sets its health error; it also requires `reload()`.
An initial subscription or projection error rejects `preload()` with the original error, ends that sync run, and releases its query subscription. Call `collection.reload()` to try again. Later recoverable errors preserve existing rows, set `collection.health.failureReason`, and call `onError`. A valid query result clears that failure reason. `collection.utils.getSyncError()` returns the latest error, but this getter is not reactive. A rejected source acceptance stops the run and sets its health error; it also requires `reload()`.

Collection cleanup rejects while live queries or Effects read it. Unmount those consumers first, or use `collection.cleanup({ dependents: true })`. Cleanup unsubscribes and invalidates callbacks from that sync run. It does not close the shared Convex client. After a restart, the first result replaces retained data, including when the result is empty. Close the client separately when your application no longer needs it.
Collection cleanup rejects while live queries or Effects read it. Unmount those consumers first, or use `collection.cleanup({ dependents: true })`. Cleanup unsubscribes and invalidates callbacks from that sync run. It does not close the shared Convex client. After a restart, the first result replaces retained data, including when the result is empty. The Convex client serves a query's cached result to a new subscription at once, so a restart soon after cleanup can start from the previous run's rows until the server's next result. A cached error does not fail the restarted run: it waits for the server's result instead, so a query that has recovered is not reported failed again. The same holds for a Collection that subscribes to a query another subscription on the same client has in error. Close the client separately when your application no longer needs it.

## Scope

Expand Down
42 changes: 33 additions & 9 deletions docs/collections/electric-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -341,11 +341,16 @@ await todosCollection.utils.awaitMatch(

### Cleanup and resume safety

Transaction evidence and pending `awaitTxId`/`awaitMatch` calls belong to one
collection lifecycle. Explicit cleanup and automatic garbage collection reject
pending waits with `StreamAbortedError`; callbacks from the retired stream cannot
settle waits in a restarted collection. Observe these promises even when the
component or collection may be disposed before they settle.
Pending `awaitTxId`/`awaitMatch` calls belong to one collection lifecycle.
Explicit cleanup and automatic garbage collection reject pending waits with
`StreamAbortedError`; callbacks from the retired stream cannot settle waits in
a restarted collection. A txid the retired stream delivered still counts after
a restart or reload, but only once the restarted collection is ready again,
because its rows are read after that txid; until then `awaitTxId` waits. When
the stream fails or gives up, pending waits reject with the failure, and a
later `awaitTxId` or `awaitMatch` rejects unless its evidence was already
seen. Observe these promises even when the component or collection may be
disposed before they settle.

An on-demand Collection with managed SQLite persistence can also replace its
Electric provider session within the same Collection sync run when its cached
Expand Down Expand Up @@ -436,10 +441,29 @@ source node shows an `error` in `collection.health` and in each live query's
`health` tree. This does not silently retry the failed stream;
`collection.reload()` refetches.

While the Electric stream retries a failed request, the Collection reports
nothing: it stays `loading` before its first up-to-date, and keeps its health tree
unchanged afterward. When the stream gives up, it calls `fail()`: a failed first load
before readiness, or the source node's `error` after it. Complete updates from an explicit `replica: 'full'` stream remain valid.
While the Electric stream retries a failed request, the Collection stays
`loading` before its first up-to-date. After it, a short outage leaves the
health tree unchanged, so a blip does not flap it. Once five requests to the
shape log in a row have failed, whether the client retried them or your
`onError` returned a retry, the source node shows the outage as its
`failureReason`, with the error of the fifth attempt; the next applied
up-to-date clears it. Any response the client accepts, such as a
`must-refetch`, starts the count again. With the default backoff, five failed
attempts take from under a second to about 15 seconds, because each retry
waits a random part of a doubling delay. A source that keeps failing five
times and then succeeding once shows and clears the outage each time. A
subset snapshot request that the client retries does not count: its failure
belongs to the live query that requested it. While the stream catches up on
a log that spans several responses, such as after a `must-refetch`, the
client also prefetches the next responses, and each failed prefetch attempt
counts too, so the outage can show sooner than five rounds of retries. When the stream gives up, it
calls `fail()` with the error, or with the error your `onError` threw: a
failed first load before readiness, or the source node's `error` after it. A give-up while a persistence wrapper is
still accepting the first snapshot fails the Collection once that acceptance
makes it ready. Aborting `shapeOptions.signal` stops the stream for
good: before readiness the first load fails, and after it the source node
shows an `error`, because the Collection no longer syncs, whether or not the
abort was deliberate. Complete updates from an explicit `replica: 'full'` stream remain valid.
On-demand streams can observe updates outside their loaded subsets; unknown
partial rows are ignored, while transaction acknowledgement evidence is retained.
Outside scoped recovery, complete rows published by persistence reloads or
Expand Down
2 changes: 1 addition & 1 deletion docs/contributing/glossary.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@ until cleanup ends the run.
| loading error | The `fail()` that ends a sync run's first load in status `error` (`isLoadingError`). It is final for its sync run: `markReady()` afterward does nothing, and only a new sync run can reach `ready`. | Sync error or a retry. |
| crash | Moving a Collection to status `crashed` (`isCrashed`) from any active status, because it can no longer serve reads or admit writes. Core uses it for IndexedDB closure, terminal local persistence failures, a throwing sync function, and internal invariant failures. It cannot be recovered in place: nothing restarts it and `reload()` rejects. Only cleanup leaves it, and cleanup discards all of the Collection's state, so a later start is a fresh start. After a crash, the sync adapter's status calls (`markReady()`, `fail()`, `reportError()`, `reportHealthy()`) do nothing. Sync data that the run commits still applies, because closure keeps accepted sync obligations such as the confirmation of a write admitted before it. | Loading error, sync error, or cleanup. |
| sync error | A problem after Collection readiness on a source's health node, from the sync adapter: a failure reason (`reportError()`, still trying; `reportHealthy()` clears it, and `reportHealthy(error)` removes one failure) or an error (`fail()`, stopped; it stays for the sync run, every later report throws, and a second `fail()` does nothing). A live query's `isSyncError` is `true` when it is `ready` and its tree has any problem. It never changes status or rows. Cleanup resets it. | Loading error or subscription status. |
| failure reason | A node's `failureReason`: the problem while the sync adapter keeps trying, after TanStack Query's `failureReason`. Its frame is the sync run: it lists each `reportError()` since the slot was last clear, once per error value (a value already listed keeps its failure's place and `at`), `reportHealthy(error)` removes that value's failure, `reportHealthy()` ends it, and a reload's new run that reports starts a new one. The slot clears when its list empties. | An error or a retry count. |
| failure reason | A node's `failureReason`: the problem while the sync adapter keeps trying, after TanStack Query's `failureReason`. When to report is the adapter's choice of threshold: a connection-aware source can report at once, while Electric, which syncs over HTTP, reports only after five failed attempts in a row. Its frame is the sync run: it lists each `reportError()` since the slot was last clear, once per error value (a value already listed keeps its failure's place and `at`), `reportHealthy(error)` removes that value's failure, `reportHealthy()` ends it, and a reload's new run that reports starts a new one. The slot clears when its list empties. | An error or a retry count. |
| problem | A health node's `failureReason` or `error` (`HealthProblem`). Its `message`, `cause`, and `at` are its newest failure's, and `errors` lists every failure of its frame still open, oldest first, each `{ name, message, code?, at, cause }`. A failure that clears leaves the list; one error value is one failure, which keeps the place of its first failure. Adding or removing a failure changes the problem, so its node is a new object. | A failure, which is one entry of a problem. |
| failure | One reported value in a problem's `errors`: one `reportError()`, the `fail()` of a sync run, a crash, or one failed request. | A problem. |
| request error | A failed subset load (a rejection, or a synchronous throw, which is a rejection) for demand that a live query or Effect started after its initial-query readiness, shown as the `error` on that query's own node. The source Collection's node does not change. The node's problem lists every request failure still open. The public name stays general on purpose: an application sees a failed request for more data, not the subset-load mechanism. It clears when a later subset load for that demand fulfills, including one that the query's own reload issues, or when the demand retires. A load that core aborted decides nothing. A truncate's or a source reload's replay decides every request error of the demand it replayed: its success clears them all, and its failure replaces them with the replay's failures, which stay until a replay succeeds for the demand that remains (a replay failure belongs to the logical demand that failed, ARCHITECTURE law 9) or the demand retires. | Sync error or request data. |
Expand Down
Loading
Loading