Skip to content

Commit 6f3ebd3

Browse files
committed
Merge branch 'ref-refactor' into select-refactor
2 parents 254df36 + 39570bc commit 6f3ebd3

73 files changed

Lines changed: 2104 additions & 291 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.changeset/gold-flowers-rest.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@tanstack/query-db-collection": patch
3+
---
4+
5+
fix: race condition creating a collection from a query that has already loaded
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
---
2+
title: Electric Collection
3+
---
4+
5+
# Electric Collection
6+
7+
Electric collections provide seamless integration between TanStack DB and ElectricSQL, enabling real-time data synchronization with your Postgres database through Electric's sync engine.
8+
9+
## Overview
10+
11+
The `@tanstack/electric-db-collection` package allows you to create collections that:
12+
- Automatically sync data from Postgres via Electric shapes
13+
- Support optimistic updates with transaction matching and automatic rollback on errors
14+
- Handle persistence through customizable mutation handlers
15+
16+
## Installation
17+
18+
```bash
19+
npm install @tanstack/electric-db-collection @tanstack/react-db
20+
```
21+
22+
## Basic Usage
23+
24+
```typescript
25+
import { createCollection } from '@tanstack/react-db'
26+
import { electricCollectionOptions } from '@tanstack/electric-db-collection'
27+
28+
const todosCollection = createCollection(
29+
electricCollectionOptions({
30+
shapeOptions: {
31+
url: '/api/todos',
32+
},
33+
getKey: (item) => item.id,
34+
})
35+
)
36+
```
37+
38+
## Configuration Options
39+
40+
The `electricCollectionOptions` function accepts the following options:
41+
42+
### Required Options
43+
44+
- `shapeOptions`: Configuration for the ElectricSQL ShapeStream
45+
- `url`: The URL of your proxy to Electric
46+
47+
- `getKey`: Function to extract the unique key from an item
48+
49+
### Optional
50+
51+
- `id`: Unique identifier for the collection
52+
- `schema`: Schema for validating items. Any Standard Schema compatible schema
53+
- `sync`: Custom sync configuration
54+
55+
### Persistence Handlers
56+
57+
- `onInsert`: Handler called before insert operations
58+
- `onUpdate`: Handler called before update operations
59+
- `onDelete`: Handler called before delete operations
60+
61+
## Persistence Handlers
62+
63+
Handlers can be defined to run on mutations. They are useful to send mutations to the backend and confirming them once Electric delivers the corresponding transactions. Until confirmation, TanStack DB blocks sync data for the collection to prevent race conditions. To avoid any delays, it’s important to use a matching strategy.
64+
65+
The most reliable strategy is for the backend to include the transaction ID (txid) in its response, allowing the client to match each mutation with Electric’s transaction identifiers for precise confirmation. If no strategy is provided, client mutations are automatically confirmed after three seconds.
66+
67+
```typescript
68+
const todosCollection = createCollection(
69+
electricCollectionOptions({
70+
id: 'todos',
71+
schema: todoSchema,
72+
getKey: (item) => item.id,
73+
shapeOptions: {
74+
url: '/api/todos',
75+
params: { table: 'todos' },
76+
},
77+
78+
onInsert: async ({ transaction }) => {
79+
const newItem = transaction.mutations[0].modified
80+
const response = await api.todos.create(newItem)
81+
82+
return { txid: response.txid }
83+
},
84+
85+
// you can also implement onUpdate and onDelete handlers
86+
})
87+
)
88+
```
89+
90+
On the backend, you can extract the `txid` for a transaction by querying Postgres directly.
91+
92+
```ts
93+
async function generateTxId(tx) {
94+
// The ::xid cast strips off the epoch, giving you the raw 32-bit value
95+
// that matches what PostgreSQL sends in logical replication streams
96+
// (and then exposed through Electric which we'll match against
97+
// in the client).
98+
const result = await tx.execute(
99+
sql`SELECT pg_current_xact_id()::xid::text as txid`
100+
)
101+
const txid = result.rows[0]?.txid
102+
103+
if (txid === undefined) {
104+
throw new Error(`Failed to get transaction ID`)
105+
}
106+
107+
return parseInt(txid as string, 10)
108+
}
109+
```
110+
111+
### Electric Proxy Example
112+
113+
Electric is typically deployed behind a proxy server that handles shape configuration, authentication and authorization. This provides better security and allows you to control what data users can access without exposing Electric to the client.
114+
115+
116+
Here is an example proxy implementation using TanStack Starter:
117+
118+
```js
119+
import { createServerFileRoute } from "@tanstack/react-start/server"
120+
import { ELECTRIC_PROTOCOL_QUERY_PARAMS } from "@electric-sql/client"
121+
122+
// Electric URL
123+
const baseUrl = 'http://.../v1/shape'
124+
125+
const serve = async ({ request }: { request: Request }) => {
126+
// ...check user authorization
127+
const url = new URL(request.url)
128+
const originUrl = new URL(baseUrl)
129+
130+
// passthrough parameters from electric client
131+
url.searchParams.forEach((value, key) => {
132+
if (ELECTRIC_PROTOCOL_QUERY_PARAMS.includes(key)) {
133+
originUrl.searchParams.set(key, value)
134+
}
135+
})
136+
137+
// set shape parameters
138+
// full spec: https://github.com/electric-sql/electric/blob/main/website/electric-api.yaml
139+
originUrl.searchParams.set("table", "todos")
140+
// Where clause to filter rows in the table (optional).
141+
// originUrl.searchParams.set("where", "completed = true")
142+
143+
// Select the columns to sync (optional)
144+
// originUrl.searchParams.set("columns", "id,text,completed")
145+
146+
const response = await fetch(originUrl)
147+
const headers = new Headers(response.headers)
148+
headers.delete("content-encoding")
149+
headers.delete("content-length")
150+
151+
return new Response(response.body, {
152+
status: response.status,
153+
statusText: response.statusText,
154+
headers,
155+
})
156+
}
157+
158+
export const ServerRoute = createServerFileRoute("/api/todos").methods({
159+
GET: serve,
160+
})
161+
```
162+
163+
## Optimistic Updates with Explicit Transactions
164+
165+
For more advanced use cases, you can create custom actions that can do multiple mutations across collections transactionally. In this case, you need to explicitly await for the transaction ID using `utils.awaitTxId()`.
166+
167+
```typescript
168+
const addTodoAction = createOptimisticAction({
169+
onMutate: ({ text }) => {
170+
// optimistically insert with a temporary ID
171+
const tempId = crypto.randomUUID()
172+
todosCollection.insert({
173+
id: tempId,
174+
text,
175+
completed: false,
176+
created_at: new Date(),
177+
})
178+
179+
// ... mutate other collections
180+
},
181+
182+
mutationFn: async ({ text }) => {
183+
const response = await api.todos.create({
184+
data: { text, completed: false }
185+
})
186+
187+
await todosCollection.utils.awaitTxId(response.txid)
188+
}
189+
})
190+
```
191+
192+
## Utility Methods
193+
194+
The collection provides these utility methods via `collection.utils`:
195+
196+
- `awaitTxId(txid, timeout?)`: Manually wait for a specific transaction ID to be synchronized
197+
198+
```typescript
199+
todosCollection.utils.awaitTxId(12345)
200+
```
201+
202+
This is useful when you need to ensure a mutation has been synchronized before proceeding with other operations.

docs/config.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@
8484
{
8585
"label": "Query Collection",
8686
"to": "collections/query-collection"
87+
},
88+
{
89+
"label": "Electric Collection",
90+
"to": "collections/electric-collection"
8791
}
8892
]
8993
},

docs/guides/error-handling.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,7 @@ const todoCollection = createCollection({
101101

102102
// Usage - optimistic update will be rolled back if the mutation fails
103103
try {
104-
const tx = await todoCollection.insert({
104+
const tx = todoCollection.insert({
105105
id: "1",
106106
text: "New todo",
107107
completed: false,
@@ -523,6 +523,6 @@ const TodoApp = () => {
523523

524524
## See Also
525525

526-
- [Mutations Guide](./overview.md#mutations) - Learn about optimistic updates and rollbacks
527-
- [API Reference](./overview.md#api-reference) - Detailed API documentation
526+
- [API Reference](../../overview.md#api-reference) - Detailed API documentation
527+
- [Mutations Guide](../../overview.md#making-optimistic-mutations) - Learn about optimistic updates and rollbacks
528528
- [TanStack Query Error Handling](https://tanstack.com/query/latest/docs/react/guides/error-handling) - Query-specific error handling

docs/guides/live-queries.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -136,7 +136,7 @@ function UserList() {
136136
}
137137
```
138138

139-
For more details on framework integration, see the [React](/docs/framework/react/adapter) and [Vue](/docs/framework/vue/adapter) adapter documentation.
139+
For more details on framework integration, see the [React](../../framework/react/adapter) and [Vue](../../framework/vue/adapter) adapter documentation.
140140

141141
## From Clause
142142

docs/overview.md

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ Live queries support joins across collections. This allows you to:
8282

8383
Every query returns another collection which can _also_ be queried.
8484

85-
For more details on live queries, see the [Live Queries](live-queries.md) documentation.
85+
For more details on live queries, see the [Live Queries](../guides/live-queries.md) documentation.
8686

8787
### Making optimistic mutations
8888

@@ -424,9 +424,9 @@ This also works with joins to derive collections from multiple source collection
424424

425425
#### Collection
426426

427-
There is a `Collection` interface in [`../packages/db/src/collection.ts`](../packages/db/src/collection.ts). You can use this to implement your own collection types.
427+
There is a `Collection` interface in [`../packages/db/src/collection.ts`](https://github.com/TanStack/db/blob/main/packages/db/src/collection.ts). You can use this to implement your own collection types.
428428

429-
See the existing implementations in [`../packages/db`](../packages/db), [`../packages/query-db-collection`](../packages/query-db-collection), [`../packages/electric-db-collection`](../packages/electric-db-collection) and [`../packages/trailbase-db-collection`](../packages/trailbase-db-collection) for reference.
429+
See the existing implementations in [`../packages/db`](https://github.com/TanStack/db/tree/main/packages/db), [`../packages/query-db-collection`](https://github.com/TanStack/db/tree/main/packages/query-db-collection), [`../packages/electric-db-collection`](https://github.com/TanStack/db/tree/main/packages/electric-db-collection) and [`../packages/trailbase-db-collection`](https://github.com/TanStack/db/tree/main/packages/trailbase-db-collection) for reference.
430430

431431
### Live queries
432432

@@ -504,7 +504,7 @@ Note also that:
504504
1. the query results [are themselves a collection](#derived-collections)
505505
2. the `useLiveQuery` automatically starts and stops live query subscriptions when you mount and unmount your components; if you're creating queries manually, you need to manually manage the subscription lifecycle yourself
506506

507-
See the [Live Queries](live-queries.md) documentation for more details.
507+
See the [Live Queries](../guides/live-queries.md) documentation for more details.
508508

509509
### Transactional mutators
510510

@@ -517,7 +517,11 @@ Transactional mutators allow you to batch and stage local changes across collect
517517

518518
Mutators are created with a `mutationFn`. You can define a single, generic `mutationFn` for your whole app. Or you can define collection or mutation specific functions.
519519

520-
The `mutationFn` is responsible for handling the local changes and processing them, usually to send them to a server or database to be stored, e.g.:
520+
The `mutationFn` is responsible for handling the local changes and processing them, usually to send them to a server or database to be stored.
521+
522+
**Important:** Inside your `mutationFn`, you must ensure that your server writes have synced back before you return, as the optimistic state is dropped when you return from the mutation function. You generally use collection-specific helpers to do this, such as Query's `utils.refetch()`, direct write APIs, or Electric's `utils.awaitTxId()`.
523+
524+
For example:
521525

522526
```tsx
523527
import type { MutationFn } from "@tanstack/react-db"
@@ -556,13 +560,19 @@ const addTodo = createOptimisticAction<string>({
556560
completed: false,
557561
})
558562
},
559-
mutationFn: async (text) => {
563+
mutationFn: async (text, params) => {
560564
// Persist the todo to your backend
561565
const response = await fetch("/api/todos", {
562566
method: "POST",
563567
body: JSON.stringify({ text, completed: false }),
564568
})
565-
return response.json()
569+
const result = await response.json()
570+
571+
// IMPORTANT: Ensure server writes have synced back before returning
572+
// This ensures the optimistic state can be safely discarded
573+
await todoCollection.utils.refetch()
574+
575+
return result
566576
},
567577
})
568578

docs/quick-start.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,6 @@ You now understand the basics of TanStack DB! The collection loads and persists
178178

179179
Explore the docs to learn more about:
180180

181-
- **[Installation](./installation.md)** - All framework and collection packages
182-
- **[Overview](./overview.md)** - Complete feature overview and examples
183-
- **[Live Queries](./live-queries.md)** - Advanced querying, joins, and aggregations
181+
- **[Installation](../installation.md)** - All framework and collection packages
182+
- **[Overview](../overview.md)** - Complete feature overview and examples
183+
- **[Live Queries](../guides/live-queries.md)** - Advanced querying, joins, and aggregations

packages/db/CHANGELOG.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,51 @@
11
# @tanstack/db
22

3+
## 0.1.11
4+
5+
### Patch Changes
6+
7+
- fix: improve InvalidSourceError message clarity ([#488](https://github.com/TanStack/db/pull/488))
8+
9+
The InvalidSourceError now provides a clear, actionable error message that:
10+
- Explicitly states the problem is passing a non-Collection/non-subquery to a live query
11+
- Includes the alias name to help identify which source is problematic
12+
- Provides guidance on what should be passed instead (Collection instances or QueryBuilder subqueries)
13+
14+
This replaces the generic "Invalid source" message with helpful debugging information.
15+
16+
## 0.1.10
17+
18+
### Patch Changes
19+
20+
- Fixed an optimization bug where orderBy clauses using a single-column array were not recognized as optimizable. Queries that order by a single column are now correctly optimized even when specified as an array. ([#477](https://github.com/TanStack/db/pull/477))
21+
22+
- fix an bug where a live query that used joins could become stuck empty when its remounted/resubscribed ([#484](https://github.com/TanStack/db/pull/484))
23+
24+
- fixed a bug where a pending sync transaction could be applied early when an optimistic mutation was resolved or rolled back ([#482](https://github.com/TanStack/db/pull/482))
25+
26+
- Add support for queries to order results based on aggregated values ([#481](https://github.com/TanStack/db/pull/481))
27+
28+
## 0.1.9
29+
30+
### Patch Changes
31+
32+
- Fix handling of Temporal objects in proxy's deepClone and deepEqual functions ([#434](https://github.com/TanStack/db/pull/434))
33+
- Temporal objects (like Temporal.ZonedDateTime) are now properly preserved during cloning instead of being converted to empty objects
34+
- Added detection for all Temporal API object types via Symbol.toStringTag
35+
- Temporal objects are returned directly from deepClone since they're immutable
36+
- Added proper equality checking for Temporal objects using their built-in equals() method
37+
- Prevents unnecessary proxy creation for immutable Temporal objects
38+
39+
## 0.1.8
40+
41+
### Patch Changes
42+
43+
- Fix bug that caused initial query results to have too few rows when query has orderBy, limit, and where clauses. ([#461](https://github.com/TanStack/db/pull/461))
44+
45+
- fix disabling of gc by setting `gcTime: 0` on the collection options ([#463](https://github.com/TanStack/db/pull/463))
46+
47+
- docs: electric-collection reference page ([#429](https://github.com/TanStack/db/pull/429))
48+
349
## 0.1.7
450

551
### Patch Changes

packages/db/package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,15 @@
11
{
22
"name": "@tanstack/db",
33
"description": "A reactive client store for building super fast apps on sync",
4-
"version": "0.1.7",
4+
"version": "0.1.11",
55
"dependencies": {
66
"@standard-schema/spec": "^1.0.0",
77
"@tanstack/db-ivm": "workspace:*"
88
},
99
"devDependencies": {
1010
"@vitest/coverage-istanbul": "^3.0.9",
11-
"arktype": "^2.1.20"
11+
"arktype": "^2.1.20",
12+
"temporal-polyfill": "^0.3.0"
1213
},
1314
"exports": {
1415
".": {

0 commit comments

Comments
 (0)