Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
bfc1e10
feat: prepare first 1.0 release candidate
Oct 15, 2026
72d1fae
feat: automate 1.0 RC patch and stable releases
Oct 15, 2026
169c151
ci: apply automated fixes
autofix-ci[bot] Oct 15, 2026
af3b9d7
fix: migrate remaining RC API consumers
Oct 15, 2026
ff6b2bd
merge: include automated PR fixes
Oct 15, 2026
13eb849
fix: update todo example RC usage
Oct 15, 2026
704ca0d
fix: preserve pending stable changesets on RC exit
Oct 15, 2026
abd5455
ci: check release versioning on PRs
Oct 15, 2026
b83dab9
docs: add 1.0 RC migration guide
Oct 15, 2026
b761339
docs: clarify accidental 1.0 releases
Oct 15, 2026
244776f
docs: cover recent breaking changes in RC migration guide
Oct 15, 2026
0f61c3d
Merge origin/main into RC preparation branch
Oct 16, 2026
b74f619
fix: address first RC review findings
Oct 16, 2026
8940336
Merge remote-tracking branch 'origin/main' into codex/rc1-deprecation…
Oct 16, 2026
509b378
fix: order offline transaction peer dependencies
Oct 16, 2026
fa5a48a
test: migrate merged ordered-query oracles to RC APIs
Oct 16, 2026
6218332
Merge origin/main before explicit Query Collection refetch
Oct 22, 2026
70a2627
Remove automatic Query Collection mutation refetch
Oct 22, 2026
e7d8446
ci: apply automated fixes
autofix-ci[bot] Oct 22, 2026
07ee3a7
docs: update RC migration guide for recent API changes
Oct 28, 2026
93959fa
docs: state removal of live-query isError explicitly
Oct 28, 2026
413b511
chore: include Preact and Convex in the first RC
Oct 28, 2026
7c53f27
docs: cover latest persistence and hydration migrations
Oct 28, 2026
751f9a7
Merge origin/main and address RC review findings
Oct 28, 2026
0a60ad8
docs: record RC review evidence and decisions
Oct 28, 2026
b05e405
ci: apply automated fixes
autofix-ci[bot] Oct 28, 2026
ade3920
Merge origin/main into first RC preparation
Oct 28, 2026
674509c
docs: record verification of latest main merge
Oct 28, 2026
17b0e7d
Merge commit 'b05e4052c' into codex/rc1-deprecation-cleanup
Oct 28, 2026
e46d67b
Merge commit 'bc77952e5' into codex/rc1-deprecation-cleanup
Oct 28, 2026
6645480
docs: note toolchain and CI formatting integration
Oct 28, 2026
68228aa
fix: close RC metadata and cached query review gaps
Oct 28, 2026
858b174
docs: record second RC review evidence and dispositions
Oct 28, 2026
5fa6eca
Merge origin/main into RC preparation branch
Oct 29, 2026
6e6034d
test: strengthen RC oracles and repair bounded release failures
Oct 29, 2026
180b920
test: make RC receiving fixtures portable across CI hosts
Oct 29, 2026
119da03
test: make required RC histories and schema fixtures reliable
Oct 29, 2026
d9911f1
ci: apply automated fixes
autofix-ci[bot] Oct 29, 2026
86896a3
test: guarantee source-health reach and named replay registration
Oct 29, 2026
a2db8e9
docs: update transaction settlement comments
Oct 29, 2026
b4b923f
test: remove stale RC API references
Oct 29, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
39 changes: 39 additions & 0 deletions .changeset/first-rc.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
---
'@tanstack/angular-db': major
'@tanstack/browser-db-sqlite-persistence': major
'@tanstack/capacitor-db-sqlite-persistence': major
'@tanstack/cloudflare-durable-objects-db-sqlite-persistence': major
'@tanstack/convex-db-collection': major
'@tanstack/db': major
'@tanstack/db-ivm': major
'@tanstack/db-sqlite-persistence-core': major
'@tanstack/electric-db-collection': major
'@tanstack/electron-db-sqlite-persistence': major
'@tanstack/expo-db-sqlite-persistence': major
'@tanstack/node-db-sqlite-persistence': major
'@tanstack/offline-transactions': minor
'@tanstack/powersync-db-collection': major
'@tanstack/preact-db': major
'@tanstack/query-db-collection': minor
'@tanstack/react-db': major
'@tanstack/react-native-db-sqlite-persistence': major
'@tanstack/react-router-with-db': major
'@tanstack/rxdb-db-collection': major
'@tanstack/solid-db': major
'@tanstack/svelte-db': major
'@tanstack/tauri-db-sqlite-persistence': major
'@tanstack/trailbase-db-collection': major
'@tanstack/vue-db': major
---

Release the first 1.0 candidate. Remove deprecated pre-1.0 APIs and give
published row metadata precedence over colliding selected fields. Structured
source spreads and direct source-row returns keep source attribution; new
functional objects and other projections receive output Collection metadata.
Keep the two packages accidentally released as stable 1.x on their stable
release lines. Both stable minor releases require `@tanstack/db@^1.0.0-rc.0`
for the RC health and restoration settlement APIs; upgrade DB with them.

Remove Query Collection’s automatic refetch after mutation handlers. Handlers
return `Promise<void>` and must await `collection.utils.refetch()` or write
confirmed data with the `write*` utilities; `{ refetch: false }` is removed.
2 changes: 2 additions & 0 deletions .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- name: Check lint
run: pnpm run lint:check
- name: Check release versioning
run: pnpm run test:release
- name: Typecheck doc and skill examples
run: pnpm run test:doc-snippets
- name: Check oracle lint without inline directives
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ jobs:
uses: tanstack/config/.github/setup@e4b48f16568324f76f467aa4c2aac2f05db632c3
- name: Check lint
run: pnpm run lint:check
- name: Check release versioning
run: pnpm run test:release

test-db:
name: Test (${{ matrix.group }})
Expand Down
17 changes: 17 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,14 @@ If you want to run an example without installing dependencies for the whole repo

This repo uses [Changesets](https://github.com/changesets/changesets) to automate releases. If your PR should release a new package version (patch, minor, or major), please run `pnpm changeset` and commit the generated file. If your PR affects docs, examples, styles, etc., you probably don't need to generate a changeset.

During the 1.0 release candidate period, use the same Changeset types. For a
package at `1.0.0-rc.1`, a patch produces `1.0.0-rc.1.1`, and another patch
produces `1.0.0-rc.1.2`. A minor or major change advances the candidate number:
`1.0.0-rc.1.2` becomes `1.0.0-rc.2`. Packages already on stable 1.x versions,
including `@tanstack/query-db-collection` and
`@tanstack/offline-transactions`, continue to use normal semantic versioning
and publish under npm's `latest` tag.

## Pull requests

Maintainers merge pull requests by squashing all commits and editing the commit message if necessary using the GitHub user interface.
Expand All @@ -167,3 +175,12 @@ Use an appropriate commit type. Be especially careful with breaking changes.
## Releases

For each new commit added to `main`, a GitHub Workflow is triggered which runs the [Changesets Action](https://github.com/changesets/action). This generates a preview PR showing the impact of all changesets. When this PR is merged, the package will be published to npm.

The first 1.0 candidate Changeset enters Changesets prerelease mode through
`pnpm run changeset:version`. Leave `.changeset/pre.json` in prerelease mode
for subsequent candidate releases; the normal release workflow handles each
patch or minor Changeset. When the candidates are ready for stable 1.0, run
`pnpm changeset pre exit` and commit the resulting `.changeset/pre.json` change.
The next Version Packages PR promotes the candidate packages to `1.0.0`.
Already stable packages keep their versions unless a pending Changeset
explicitly releases them. Merge that PR to publish 1.0.
2 changes: 1 addition & 1 deletion docs/collections/convex-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ const tasks = createCollection(
const transaction = tasks.update(taskId, (task) => {
task.completed = true
})
await transaction.isPersisted.promise
await transaction.when('settled')
```

DB applies optimistic writes immediately. Successful Convex mutation promises wait for the client's queries to catch up. Before a handler settles, the adapter reads the latest cached result and waits for DB to accept its sync writes. It does not wait for visibility held by that same optimistic transaction. If a wrapping persistence layer rejects source acceptance, that sync run stays failed. Call `collection.reload()` before retrying replay. After readiness, a provider query error can recover when a later valid result arrives. A failed first load requires `reload()`. A rejected handler rolls back its optimistic writes.
Expand Down
2 changes: 1 addition & 1 deletion docs/collections/electric-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -468,7 +468,7 @@ const matchFn = (message) => {

### Common Issue: awaitTxId Stalls or Times Out

A frequent issue developers encounter is that `awaitTxId` (or the transaction's `isPersisted.promise`) stalls indefinitely, eventually timing out with no error messages. The data persists correctly to the database, but the optimistic mutation never resolves.
A frequent issue developers encounter is that `awaitTxId` (or the transaction's `when('settled')`) stalls indefinitely, eventually timing out with no error messages. The data persists correctly to the database, but the optimistic mutation never resolves.

**Root Cause:** This happens when the transaction ID (txid) returned from your API doesn't match the actual transaction ID of the mutation in Postgres. This mismatch occurs when you query `pg_current_xact_id()` **outside** the same transaction that performs the mutation.

Expand Down
10 changes: 5 additions & 5 deletions docs/collections/indexed-db-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,18 +86,18 @@ const insert = todos.insert({
text: 'Write a draft',
completed: false,
})
await insert.isPersisted.promise
await insert.when('settled')

const update = todos.update(id, (draft) => {
draft.completed = true
})
await update.isPersisted.promise
await update.when('settled')

const remove = todos.delete(id)
await remove.isPersisted.promise
await remove.when('settled')
```

Await `isPersisted.promise` to observe persistence success or failure. The returned transaction itself is not a Promise.
Await `when('settled')` to observe persistence success or failure. The returned transaction itself is not a Promise.

If you provide `onInsert`, `onUpdate`, or `onDelete`, the handler runs before persistence. A rejected handler leaves durable rows unchanged and rolls back its optimistic changes. Each accepted Collection batch writes rows and metadata in one IndexedDB transaction. A failed write aborts the batch.

Expand Down Expand Up @@ -212,7 +212,7 @@ For a transaction involving several Collections, call each Collection's acceptan

Automatic writes in one Collection persist in mutation order, even when their
handlers finish out of order. Later writes wait for earlier handlers and
persistence to settle before reporting `isPersisted`. Rejection contributes no
persistence to settle before reporting `when('settled')`. Rejection contributes no
durable write and allows the next mutation to proceed. Do not await a later
automatic write from an earlier handler in the same Collection: each would wait
for the other. This rule does not order separate Collections, manual acceptance,
Expand Down
4 changes: 2 additions & 2 deletions docs/collections/local-storage-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ that contains any of these:
- a `Map`, `Set`, `WeakMap`, `WeakSet`, `RegExp`, `Promise`, `ArrayBuffer`,
`SharedArrayBuffer`, or typed array

The rejected mutation's `isPersisted` promise rejects with
The rejected mutation's `when('settled')` promise rejects with
`SerializationError`. Nothing is written to storage and the optimistic row
rolls back. An automatic write's `onInsert` or `onUpdate` handler does not run;
a manual `acceptMutations` call rejects with the same error.
Expand All @@ -138,7 +138,7 @@ localStorage before the mutation method returns. A later handler failure rejects
its transaction promptly, even if an earlier handler is still pending.

A handler can start another mutation on the same Collection, then return without
awaiting that mutation's `isPersisted` promise. Do not await that promise or a
awaiting that mutation's `when('settled')` promise. Do not await that promise or a
later `acceptMutations()` call inside the earlier handler. Those writes wait for
the earlier handler to return, so awaiting them would leave both pending.

Expand Down
10 changes: 5 additions & 5 deletions docs/collections/powersync-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -452,21 +452,21 @@ await documents.insert(
timestamp: Date.now(),
},
}
).isPersisted.promise
).when('settled')

// Update with metadata
await documents.update(
docId,
String(docId),
{ metadata: { reason: "typo-fix", editor: "user-456" } },
(doc) => {
doc.name = "Report Q4 (Updated)"
}
).isPersisted.promise
).when('settled')

// Delete with metadata
await documents.delete(docId, {
metadata: { deletedBy: "user-789", reason: "duplicate" },
}).isPersisted.promise
}).when('settled')
```
#### Accessing Metadata During Upload
Expand Down Expand Up @@ -568,7 +568,7 @@ batchTx.mutate(() => {
await batchTx.commit()

// Wait for the changes to be persisted
await batchTx.isPersisted.promise
await batchTx.when('settled')
```

This approach allows you to:
Expand Down
98 changes: 34 additions & 64 deletions docs/collections/query-collection.md
Original file line number Diff line number Diff line change
Expand Up @@ -556,71 +556,49 @@ const productsCollection = createCollection(

## Persistence Handlers

You can define handlers that are called when mutations occur. These handlers persist changes to your backend:
Handlers persist changes to your backend and explicitly synchronize the
confirmed state before returning. They return `Promise<void>`:

```typescript
const todosCollection = createCollection(
queryCollectionOptions({
queryCollectionOptions<{ id: string; text: string; completed: boolean }>({
queryKey: ["todos"],
queryFn: fetchTodos,
queryClient,
getKey: (item) => item.id,

onInsert: async ({ transaction }) => {
const newItems = transaction.mutations.map((m) => m.modified)
await api.createTodos(newItems)
// Auto-refetch happens after handler completes (pre-1.0 behavior)
onInsert: async ({ transaction, collection }) => {
await api.createTodos(transaction.mutations.map((m) => m.modified))
await collection.utils.refetch()
},

onUpdate: async ({ transaction }) => {
const updates = transaction.mutations.map((m) => ({
onUpdate: async ({ transaction, collection }) => {
await api.updateTodos(transaction.mutations.map((m) => ({
id: m.key,
changes: m.changes,
}))
await api.updateTodos(updates)
})))
await collection.utils.refetch()
},

onDelete: async ({ transaction }) => {
const ids = transaction.mutations.map((m) => m.key)
await api.deleteTodos(ids)
onDelete: async ({ transaction, collection }) => {
await api.deleteTodos(transaction.mutations.map((m) => m.key))
await collection.utils.refetch()
},
})
)
```

> **Note**: QueryCollection currently auto-refetches after handlers complete. See [Controlling Refetch Behavior](#controlling-refetch-behavior) for details on this transitional behavior.
### Controlling Refetch Behavior

> **⚠️ Transitional API**: QueryCollection currently auto-refetches after handlers complete. This behavior is deprecated and will be removed in v1.0. See the migration notes below.
#### Current Behavior (Pre-1.0)

By default, QueryCollection automatically refetches after each handler completes. To **skip** auto-refetch, return `{ refetch: false }`:
Mutation handlers do not automatically refetch. Call and await
`collection.utils.refetch()` to read back server state, or await the direct-write
utilities to apply the confirmed server response without a request. If a handler
does neither, its optimistic changes revert to the previous synced rows when the
transaction settles. A successful server write alone does not update those rows.

```typescript
onInsert: async ({ transaction }) => {
await api.createTodos(transaction.mutations.map((m) => m.modified))

// Skip auto-refetch - use this when server doesn't modify the data
return { refetch: false }
}
```
The `{ refetch: false }` return value has been removed. Handlers return
`Promise<void>` and do not control synchronization through their return value.
Query's configured polling, focus, reconnect, and invalidation triggers still
operate independently of mutation handlers.

If you don't return `{ refetch: false }`, auto-refetch happens automatically.

#### v1.0 Behavior (Future)

In v1.0, auto-refetch will be **removed**. Handlers will need to explicitly call `collection.utils.refetch()` when refetching is needed:

```typescript
onInsert: async ({ transaction, collection }) => {
await api.createTodos(transaction.mutations.map((m) => m.modified))

// Explicitly trigger refetch when you need server state
await collection.utils.refetch()
}
```
#### Explicit Refetch Inside a Handler

When no user mutation is persisting and no mutation handler is active, the
`refetch()` promise resolves after every accepted result from that call has been
Expand All @@ -636,7 +614,7 @@ the Query fetch boundary. This rule applies whether the mutation or the refetch
starts first. It includes calls through the handler parameter, captured or
external Collection references, manual transaction mutation functions, and
`createOptimisticAction` mutation functions. Code can await the mutation's
`isPersisted.promise` when it needs the complete transaction and its queued
`when('settled')` when it needs the complete transaction and its queued
Collection application. Utilities that refetch inherit the same phase
boundary.

Expand All @@ -655,24 +633,18 @@ boundary, `throwOnError` applies only to the Query fetch because application
happens after that call settles. A later application failure is recorded by the
Collection's error utilities.

To skip refetch in v1.0, simply don't call `refetch()`:
#### Applying a Server Response Without Refetching

```typescript
onInsert: async ({ transaction }) => {
await api.createTodos(transaction.mutations.map((m) => m.modified))
Use direct-write utilities when the server returns the confirmed rows or another
source delivers them. Await the write before the handler returns:

// No refetch call = no refetch (v1.0 behavior)
```typescript
onInsert: async ({ transaction, collection }) => {
const rows = await api.createTodos(transaction.mutations.map((m) => m.modified))
await collection.utils.writeUpsert(rows)
}
```

#### When to Skip Refetch

Skip refetching when:

- You're confident the server state exactly matches what you sent (no server-side processing)
- You're handling state updates through other mechanisms (like WebSockets or direct writes)
- You want to optimize for fewer network requests

## Utility Methods

The collection provides these utility methods via `collection.utils`:
Expand Down Expand Up @@ -845,7 +817,7 @@ When the server returns computed fields (like server-generated IDs or timestamps

```typescript
const todosCollection = createCollection(
queryCollectionOptions({
queryCollectionOptions<{ id: string; text: string; completed: boolean }>({
queryKey: ["todos"],
queryFn: fetchTodos,
queryClient,
Expand All @@ -867,7 +839,6 @@ const todosCollection = createCollection(

// No need to refetch - we've already synced the server response via direct writes
// (optimistic state is automatically replaced when handler completes)
return { refetch: false } // Transitional pre-1.0 opt-out
},

onUpdate: async ({ transaction, collection }) => {
Expand All @@ -885,13 +856,12 @@ const todosCollection = createCollection(
})

// No refetch needed since we used direct writes
return { refetch: false } // Transitional pre-1.0 opt-out
},
})
)

// Usage is just like a regular collection
todosCollection.insert({ text: "Buy milk", completed: false })
todosCollection.insert({ id: crypto.randomUUID(), text: "Buy milk", completed: false })
```

### Server pagination with live queries
Expand Down Expand Up @@ -1285,7 +1255,7 @@ has a subset owner or not. It refetches that Query when the subset is acquired,
then applies the new server result. The earlier direct write still starts no
request by itself.

To use direct writes with persistence handlers, return `{ refetch: false }` to avoid the handler's separate automatic refetch during the pre-1.0 transition. An on-demand `queryFn` still needs to return the complete current result for its pushed-down predicate, order, limit, and offset when it does run. For robust real-time synchronization, use a collection backed by a sync engine that owns snapshot and change-stream ordering.
Await direct writes inside persistence handlers to apply confirmed data before settlement. Handlers do not refetch automatically. An on-demand `queryFn` still needs to return the complete current result for its pushed-down predicate, order, limit, and offset when it does run. For robust real-time synchronization, use a collection backed by a sync engine that owns snapshot and change-stream ordering.

## Complete Direct Write API Reference

Expand Down
4 changes: 4 additions & 0 deletions docs/config.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
{
"label": "Guides",
"children": [
{
"label": "Migrate to the 1.0 RC",
"to": "guides/migrate-to-1-0-rc"
},
{
"label": "Live Queries",
"to": "guides/live-queries"
Expand Down
Loading
Loading