@octanejs/tanstack-db 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +34 -0
- package/package.json +59 -0
- package/skills/tanstack-db/SKILL.md +432 -0
- package/src/index.ts +13 -0
- package/src/slot.ts +26 -0
- package/src/useLiveInfiniteQuery.ts +358 -0
- package/src/useLiveQuery.ts +473 -0
- package/src/useLiveQueryEffect.ts +74 -0
- package/src/useLiveSuspenseQuery.ts +251 -0
- package/src/usePacedMutations.ts +151 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Dominic Gannaway
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# @octanejs/tanstack-db
|
|
2
|
+
|
|
3
|
+
Octane live-query hooks for [TanStack DB](https://github.com/TanStack/db).
|
|
4
|
+
|
|
5
|
+
Re-exports [`@tanstack/db`](https://tanstack.com/db) unchanged and implements its
|
|
6
|
+
live-query binding surface on Octane hooks:
|
|
7
|
+
|
|
8
|
+
- `useLiveQuery`
|
|
9
|
+
- `useLiveInfiniteQuery`
|
|
10
|
+
- `useLiveSuspenseQuery`
|
|
11
|
+
- `useLiveQueryEffect`
|
|
12
|
+
- `usePacedMutations`
|
|
13
|
+
|
|
14
|
+
Install `octane` alongside this package and configure the Octane compiler in your
|
|
15
|
+
build tool (see [octanejs.dev](https://octanejs.dev/docs/build-tools)).
|
|
16
|
+
|
|
17
|
+
## Compatibility
|
|
18
|
+
|
|
19
|
+
Ports the React live-query hooks of `@tanstack/react-db@0.1.96` onto Octane and
|
|
20
|
+
re-exports the framework-neutral `@tanstack/db@0.7.0` core unchanged.
|
|
21
|
+
`useLiveQuery`/`useLiveSuspenseQuery` run on db's shared `createLiveQueryObserver`
|
|
22
|
+
and `useLiveInfiniteQuery` on the coordinated `createLiveQueryWindowController`.
|
|
23
|
+
|
|
24
|
+
Intentional differences from React:
|
|
25
|
+
|
|
26
|
+
- **Suspense** integrates via Octane's `use(thenable)` rather than throwing a raw
|
|
27
|
+
promise (observable behavior — fallback then data — matches).
|
|
28
|
+
- **`useLiveInfiniteQuery`** rejects a pre-created collection that lacks an
|
|
29
|
+
`orderBy` synchronously during render, so the error reaches the caller.
|
|
30
|
+
- **StrictMode double-invocation** is not applicable (Octane has no development
|
|
31
|
+
double-invoke).
|
|
32
|
+
|
|
33
|
+
See `UPSTREAM.md` for the pin and export crosswalk and `status.json` for the
|
|
34
|
+
tracked binding status.
|
package/package.json
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@octanejs/tanstack-db",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"license": "MIT",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"engines": {
|
|
7
|
+
"node": ">=22.22.2"
|
|
8
|
+
},
|
|
9
|
+
"octane": {
|
|
10
|
+
"hookSlots": {
|
|
11
|
+
"manual": [
|
|
12
|
+
"src"
|
|
13
|
+
]
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"description": "TanStack DB bindings for Octane — reuses @tanstack/db's framework-agnostic core with Octane live-query hooks.",
|
|
17
|
+
"author": "Kyle Mathews",
|
|
18
|
+
"publishConfig": {
|
|
19
|
+
"access": "public"
|
|
20
|
+
},
|
|
21
|
+
"repository": {
|
|
22
|
+
"type": "git",
|
|
23
|
+
"url": "git+https://github.com/octanejs/octane.git",
|
|
24
|
+
"directory": "packages/tanstack-db"
|
|
25
|
+
},
|
|
26
|
+
"homepage": "https://tanstack.com/db",
|
|
27
|
+
"keywords": [
|
|
28
|
+
"optimistic",
|
|
29
|
+
"octane",
|
|
30
|
+
"typescript",
|
|
31
|
+
"tanstack-intent"
|
|
32
|
+
],
|
|
33
|
+
"main": "src/index.ts",
|
|
34
|
+
"module": "src/index.ts",
|
|
35
|
+
"types": "src/index.ts",
|
|
36
|
+
"files": [
|
|
37
|
+
"src",
|
|
38
|
+
"skills",
|
|
39
|
+
"README.md"
|
|
40
|
+
],
|
|
41
|
+
"exports": {
|
|
42
|
+
".": "./src/index.ts"
|
|
43
|
+
},
|
|
44
|
+
"sideEffects": false,
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@tanstack/db": "^0.7.0"
|
|
47
|
+
},
|
|
48
|
+
"peerDependencies": {
|
|
49
|
+
"octane": "0.1.38"
|
|
50
|
+
},
|
|
51
|
+
"devDependencies": {
|
|
52
|
+
"vitest": "^4.1.10",
|
|
53
|
+
"@octanejs/testing-library": "0.1.35",
|
|
54
|
+
"octane": "0.1.38"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"test": "vitest run"
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: tanstack-db
|
|
3
|
+
description: >
|
|
4
|
+
Octane bindings for TanStack DB. useLiveQuery hook with dependency arrays
|
|
5
|
+
(8 overloads: query function, config object, pre-created collection,
|
|
6
|
+
disabled state via returning undefined/null). useLiveSuspenseQuery for
|
|
7
|
+
Octane Suspense with Error Boundaries (data always defined).
|
|
8
|
+
useLiveInfiniteQuery for cursor-based pagination (pageSize, fetchNextPage,
|
|
9
|
+
hasNextPage, isFetchingNextPage). usePacedMutations for debounced Octane
|
|
10
|
+
state updates. Return shape: data, state, collection, status, isLoading,
|
|
11
|
+
isReady, isError. Import from @octanejs/tanstack-db (re-exports all of
|
|
12
|
+
@tanstack/db).
|
|
13
|
+
type: framework
|
|
14
|
+
library: db
|
|
15
|
+
framework: octane
|
|
16
|
+
library_version: '0.6.0'
|
|
17
|
+
requires:
|
|
18
|
+
- db-core
|
|
19
|
+
sources:
|
|
20
|
+
- 'TanStack/db:docs/framework/octane/overview.md'
|
|
21
|
+
- 'TanStack/db:docs/guides/live-queries.md'
|
|
22
|
+
- 'TanStack/db:packages/octane-db/src/useLiveQuery.ts'
|
|
23
|
+
- 'TanStack/db:packages/octane-db/src/useLiveInfiniteQuery.ts'
|
|
24
|
+
- 'TanStack/db:packages/octane-db/src/useLiveQueryEffect.ts'
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
This skill builds on db-core. Read it first for collection setup, query builder, and mutation patterns.
|
|
28
|
+
|
|
29
|
+
# TanStack DB — Octane
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
```tsx
|
|
34
|
+
import { useLiveQuery, eq, not } from '@octanejs/tanstack-db'
|
|
35
|
+
|
|
36
|
+
function TodoList() {
|
|
37
|
+
const { data: todos, isLoading } = useLiveQuery((q) =>
|
|
38
|
+
q
|
|
39
|
+
.from({ todo: todoCollection })
|
|
40
|
+
.where(({ todo }) => not(todo.completed))
|
|
41
|
+
.orderBy(({ todo }) => todo.created_at, 'asc'),
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
if (isLoading) return <div>Loading...</div>
|
|
45
|
+
|
|
46
|
+
return (
|
|
47
|
+
<ul>
|
|
48
|
+
{todos.map((todo) => (
|
|
49
|
+
<li key={todo.id}>{todo.text}</li>
|
|
50
|
+
))}
|
|
51
|
+
</ul>
|
|
52
|
+
)
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
`@octanejs/tanstack-db` re-exports everything from `@tanstack/db`. In Octane projects, import everything from `@octanejs/tanstack-db`.
|
|
57
|
+
|
|
58
|
+
## Hooks
|
|
59
|
+
|
|
60
|
+
### useLiveQuery
|
|
61
|
+
|
|
62
|
+
```tsx
|
|
63
|
+
// Query function with dependency array
|
|
64
|
+
const {
|
|
65
|
+
data,
|
|
66
|
+
state,
|
|
67
|
+
collection,
|
|
68
|
+
status,
|
|
69
|
+
isLoading,
|
|
70
|
+
isReady,
|
|
71
|
+
isError,
|
|
72
|
+
isIdle,
|
|
73
|
+
isCleanedUp,
|
|
74
|
+
} = useLiveQuery(
|
|
75
|
+
(q) =>
|
|
76
|
+
q
|
|
77
|
+
.from({ todo: todoCollection })
|
|
78
|
+
.where(({ todo }) => eq(todo.userId, userId)),
|
|
79
|
+
[userId],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
// Config object
|
|
83
|
+
const { data } = useLiveQuery({
|
|
84
|
+
query: (q) => q.from({ todo: todoCollection }),
|
|
85
|
+
gcTime: 60000,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
// Pre-created collection (from route loader)
|
|
89
|
+
const { data } = useLiveQuery(preloadedCollection)
|
|
90
|
+
|
|
91
|
+
// Conditional query — return undefined/null to disable
|
|
92
|
+
const { data, status } = useLiveQuery(
|
|
93
|
+
(q) => {
|
|
94
|
+
if (!userId) return undefined
|
|
95
|
+
return q
|
|
96
|
+
.from({ todo: todoCollection })
|
|
97
|
+
.where(({ todo }) => eq(todo.userId, userId))
|
|
98
|
+
},
|
|
99
|
+
[userId],
|
|
100
|
+
)
|
|
101
|
+
// When disabled: status='disabled', data=undefined
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### useLiveSuspenseQuery
|
|
105
|
+
|
|
106
|
+
```tsx
|
|
107
|
+
// data is ALWAYS defined — never undefined
|
|
108
|
+
// Must wrap in <Suspense> and <ErrorBoundary>
|
|
109
|
+
function TodoList() {
|
|
110
|
+
const { data: todos } = useLiveSuspenseQuery((q) =>
|
|
111
|
+
q.from({ todo: todoCollection }),
|
|
112
|
+
)
|
|
113
|
+
|
|
114
|
+
return (
|
|
115
|
+
<ul>
|
|
116
|
+
{todos.map((t) => (
|
|
117
|
+
<li key={t.id}>{t.text}</li>
|
|
118
|
+
))}
|
|
119
|
+
</ul>
|
|
120
|
+
)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// With deps — re-suspends when deps change
|
|
124
|
+
const { data } = useLiveSuspenseQuery(
|
|
125
|
+
(q) =>
|
|
126
|
+
q
|
|
127
|
+
.from({ todo: todoCollection })
|
|
128
|
+
.where(({ todo }) => eq(todo.category, category)),
|
|
129
|
+
[category],
|
|
130
|
+
)
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### useLiveInfiniteQuery
|
|
134
|
+
|
|
135
|
+
```tsx
|
|
136
|
+
const { data, fetchNextPage, hasNextPage, isFetchingNextPage } =
|
|
137
|
+
useLiveInfiniteQuery(
|
|
138
|
+
(q) =>
|
|
139
|
+
q
|
|
140
|
+
.from({ posts: postsCollection })
|
|
141
|
+
.where(({ posts }) => eq(posts.category, category))
|
|
142
|
+
.orderBy(({ posts }) => posts.createdAt, 'desc'),
|
|
143
|
+
{ pageSize: 20 },
|
|
144
|
+
[category],
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
// data is the flat array of all loaded pages
|
|
148
|
+
// fetchNextPage() loads the next page
|
|
149
|
+
// hasNextPage is true when more data is available
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
### usePacedMutations
|
|
153
|
+
|
|
154
|
+
```tsx
|
|
155
|
+
import { usePacedMutations, debounceStrategy } from "@octanejs/tanstack-db"
|
|
156
|
+
|
|
157
|
+
const mutate = usePacedMutations({
|
|
158
|
+
onMutate: (value: string) => {
|
|
159
|
+
noteCollection.update(noteId, (draft) => {
|
|
160
|
+
draft.content = value
|
|
161
|
+
})
|
|
162
|
+
},
|
|
163
|
+
mutationFn: async ({ transaction }) => {
|
|
164
|
+
await api.notes.update(noteId, transaction.mutations[0].changes)
|
|
165
|
+
},
|
|
166
|
+
strategy: debounceStrategy({ wait: 500 }),
|
|
167
|
+
})
|
|
168
|
+
|
|
169
|
+
// In handler:
|
|
170
|
+
<textarea onChange={(e) => mutate(e.target.value)} />
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
### useLiveQueryEffect
|
|
174
|
+
|
|
175
|
+
```tsx
|
|
176
|
+
import { useLiveQueryEffect, eq } from '@octanejs/tanstack-db'
|
|
177
|
+
|
|
178
|
+
// Fire side effects when rows enter, exit, or update a query result.
|
|
179
|
+
// No render output — the effect is created on mount, disposed on unmount,
|
|
180
|
+
// and recreated when deps change.
|
|
181
|
+
function ChatComponent() {
|
|
182
|
+
useLiveQueryEffect(
|
|
183
|
+
{
|
|
184
|
+
query: (q) =>
|
|
185
|
+
q.from({ msg: messages }).where(({ msg }) => eq(msg.role, 'user')),
|
|
186
|
+
skipInitial: true,
|
|
187
|
+
onEnter: async (event) => {
|
|
188
|
+
await generateResponse(event.value)
|
|
189
|
+
},
|
|
190
|
+
},
|
|
191
|
+
[],
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
return <div>...</div>
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Includes (Hierarchical Data)
|
|
199
|
+
|
|
200
|
+
When a query uses includes (subqueries in `select`), each child field is a live `Collection` by default. Subscribe to it with `useLiveQuery` in a subcomponent:
|
|
201
|
+
|
|
202
|
+
```tsx
|
|
203
|
+
function ProjectList() {
|
|
204
|
+
const { data: projects } = useLiveQuery((q) =>
|
|
205
|
+
q.from({ p: projectsCollection }).select(({ p }) => ({
|
|
206
|
+
id: p.id,
|
|
207
|
+
name: p.name,
|
|
208
|
+
issues: q
|
|
209
|
+
.from({ i: issuesCollection })
|
|
210
|
+
.where(({ i }) => eq(i.projectId, p.id))
|
|
211
|
+
.select(({ i }) => ({ id: i.id, title: i.title })),
|
|
212
|
+
})),
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
return (
|
|
216
|
+
<ul>
|
|
217
|
+
{projects.map((project) => (
|
|
218
|
+
<li key={project.id}>
|
|
219
|
+
{project.name}
|
|
220
|
+
<IssueList issuesCollection={project.issues} />
|
|
221
|
+
</li>
|
|
222
|
+
))}
|
|
223
|
+
</ul>
|
|
224
|
+
)
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// Child component subscribes to the child Collection
|
|
228
|
+
function IssueList({ issuesCollection }) {
|
|
229
|
+
const { data: issues } = useLiveQuery(issuesCollection)
|
|
230
|
+
return (
|
|
231
|
+
<ul>
|
|
232
|
+
{issues.map((issue) => (
|
|
233
|
+
<li key={issue.id}>{issue.title}</li>
|
|
234
|
+
))}
|
|
235
|
+
</ul>
|
|
236
|
+
)
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Only the affected `IssueList` re-renders when an issue changes — the parent does not.
|
|
241
|
+
|
|
242
|
+
With `toArray()`, child results are plain arrays and the parent re-renders on child changes:
|
|
243
|
+
|
|
244
|
+
```tsx
|
|
245
|
+
import { toArray, eq } from '@octanejs/tanstack-db'
|
|
246
|
+
|
|
247
|
+
const { data: projects } = useLiveQuery((q) =>
|
|
248
|
+
q.from({ p: projectsCollection }).select(({ p }) => ({
|
|
249
|
+
id: p.id,
|
|
250
|
+
name: p.name,
|
|
251
|
+
issues: toArray(
|
|
252
|
+
q
|
|
253
|
+
.from({ i: issuesCollection })
|
|
254
|
+
.where(({ i }) => eq(i.projectId, p.id))
|
|
255
|
+
.select(({ i }) => ({ id: i.id, title: i.title })),
|
|
256
|
+
),
|
|
257
|
+
})),
|
|
258
|
+
)
|
|
259
|
+
// project.issues is Array<{ id, title }> — no subcomponent needed
|
|
260
|
+
```
|
|
261
|
+
|
|
262
|
+
See db-core/live-queries/SKILL.md for full includes rules (correlation conditions, nested includes, aggregates).
|
|
263
|
+
|
|
264
|
+
## Virtual Properties
|
|
265
|
+
|
|
266
|
+
Live query results include computed, read-only virtual properties on every row:
|
|
267
|
+
|
|
268
|
+
- `$synced`: `true` when the row is confirmed by sync; `false` when it is still optimistic.
|
|
269
|
+
- `$origin`: `"local"` if the last confirmed change came from this client, otherwise `"remote"`.
|
|
270
|
+
- `$key`: the row key for the result.
|
|
271
|
+
- `$collectionId`: the source collection ID.
|
|
272
|
+
|
|
273
|
+
These props are added automatically and can be used in `where`, `select`, and `orderBy` clauses. Do not persist them back to storage.
|
|
274
|
+
|
|
275
|
+
```tsx
|
|
276
|
+
const { data } = useLiveQuery(
|
|
277
|
+
(q) =>
|
|
278
|
+
q
|
|
279
|
+
.from({ todo: todoCollection })
|
|
280
|
+
.where(({ todo }) => eq(todo.$synced, false)),
|
|
281
|
+
[],
|
|
282
|
+
)
|
|
283
|
+
// Shows only optimistic (unconfirmed) todos
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Octane-Specific Patterns
|
|
287
|
+
|
|
288
|
+
### Dependency arrays
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
// Include ALL external reactive values
|
|
292
|
+
const { data } = useLiveQuery(
|
|
293
|
+
(q) =>
|
|
294
|
+
q
|
|
295
|
+
.from({ todo: todoCollection })
|
|
296
|
+
.where(({ todo }) =>
|
|
297
|
+
and(eq(todo.userId, userId), eq(todo.status, filter)),
|
|
298
|
+
),
|
|
299
|
+
[userId, filter],
|
|
300
|
+
)
|
|
301
|
+
|
|
302
|
+
// Empty array = static query, never re-runs
|
|
303
|
+
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }), [])
|
|
304
|
+
|
|
305
|
+
// No array = re-runs on every render (usually wrong)
|
|
306
|
+
```
|
|
307
|
+
|
|
308
|
+
### Suspense + Error Boundary
|
|
309
|
+
|
|
310
|
+
```tsx
|
|
311
|
+
<ErrorBoundary fallback={<div>Error</div>}>
|
|
312
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
313
|
+
<TodoList />
|
|
314
|
+
</Suspense>
|
|
315
|
+
</ErrorBoundary>
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Router loader preloading
|
|
319
|
+
|
|
320
|
+
```tsx
|
|
321
|
+
// In route loader:
|
|
322
|
+
await todoCollection.preload()
|
|
323
|
+
|
|
324
|
+
// In component — data available immediately:
|
|
325
|
+
const { data } = useLiveQuery((q) => q.from({ todo: todoCollection }))
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
See meta-framework/SKILL.md for full preloading patterns.
|
|
329
|
+
|
|
330
|
+
## Common Mistakes
|
|
331
|
+
|
|
332
|
+
### CRITICAL Missing external values in dependency array
|
|
333
|
+
|
|
334
|
+
Wrong:
|
|
335
|
+
|
|
336
|
+
```tsx
|
|
337
|
+
const { data } = useLiveQuery((q) =>
|
|
338
|
+
q.from({ todo: todoCollection }).where(({ todo }) => eq(todo.userId, userId)),
|
|
339
|
+
)
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
Correct:
|
|
343
|
+
|
|
344
|
+
```tsx
|
|
345
|
+
const { data } = useLiveQuery(
|
|
346
|
+
(q) =>
|
|
347
|
+
q
|
|
348
|
+
.from({ todo: todoCollection })
|
|
349
|
+
.where(({ todo }) => eq(todo.userId, userId)),
|
|
350
|
+
[userId],
|
|
351
|
+
)
|
|
352
|
+
```
|
|
353
|
+
|
|
354
|
+
When the query uses external state not in the deps array, the query won't re-run when that value changes, showing stale results.
|
|
355
|
+
|
|
356
|
+
Source: docs/framework/octane/overview.md
|
|
357
|
+
|
|
358
|
+
### HIGH useLiveSuspenseQuery without Error Boundary
|
|
359
|
+
|
|
360
|
+
Wrong:
|
|
361
|
+
|
|
362
|
+
```tsx
|
|
363
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
364
|
+
<TodoList /> {/* uses useLiveSuspenseQuery */}
|
|
365
|
+
</Suspense>
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
Correct:
|
|
369
|
+
|
|
370
|
+
```tsx
|
|
371
|
+
<ErrorBoundary fallback={<div>Error</div>}>
|
|
372
|
+
<Suspense fallback={<div>Loading...</div>}>
|
|
373
|
+
<TodoList />
|
|
374
|
+
</Suspense>
|
|
375
|
+
</ErrorBoundary>
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
`useLiveSuspenseQuery` throws errors during rendering. Without an Error Boundary, the entire app crashes.
|
|
379
|
+
|
|
380
|
+
Source: docs/guides/live-queries.md
|
|
381
|
+
|
|
382
|
+
### HIGH "Not a Collection" error from duplicate @tanstack/db
|
|
383
|
+
|
|
384
|
+
If `useLiveQuery` throws `InvalidSourceError: The value provided for alias "todo" is not a Collection`, it usually means two copies of `@tanstack/db` are installed. The collection was created by one copy, but `useLiveQuery` checks `instanceof` against the other.
|
|
385
|
+
|
|
386
|
+
In dev mode, TanStack DB also throws `DuplicateDbInstanceError` if two instances are detected.
|
|
387
|
+
|
|
388
|
+
**Diagnose:**
|
|
389
|
+
|
|
390
|
+
```bash
|
|
391
|
+
pnpm ls @tanstack/db
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
If multiple versions appear, fix with one of:
|
|
395
|
+
|
|
396
|
+
**pnpm overrides** (in root package.json):
|
|
397
|
+
|
|
398
|
+
```json
|
|
399
|
+
{
|
|
400
|
+
"pnpm": {
|
|
401
|
+
"overrides": {
|
|
402
|
+
"@tanstack/db": "^0.6.0"
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
**Vite resolve.alias** (in vite.config.ts):
|
|
409
|
+
|
|
410
|
+
```ts
|
|
411
|
+
import path from 'path'
|
|
412
|
+
|
|
413
|
+
export default defineConfig({
|
|
414
|
+
resolve: {
|
|
415
|
+
alias: {
|
|
416
|
+
'@tanstack/db': path.resolve('./node_modules/@tanstack/db'),
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
})
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
The root cause is typically a dependency that bundles its own copy instead of declaring `@tanstack/db` as a `peerDependency`.
|
|
423
|
+
|
|
424
|
+
### HIGH Tension: Query expressiveness vs. IVM constraints
|
|
425
|
+
|
|
426
|
+
The query builder looks like SQL but has constraints that SQL doesn't — equality joins only, orderBy required for limit/offset, no distinct without select. Agents write SQL-style queries that violate these constraints. See db-core/live-queries/SKILL.md § Common Mistakes for all constraints.
|
|
427
|
+
|
|
428
|
+
See also: db-core/live-queries/SKILL.md — for query builder API and all operators.
|
|
429
|
+
|
|
430
|
+
See also: db-core/mutations-optimistic/SKILL.md — for mutation patterns.
|
|
431
|
+
|
|
432
|
+
See also: meta-framework/SKILL.md — for preloading in route loaders.
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
// Re-export all public APIs
|
|
2
|
+
export * from './useLiveQuery';
|
|
3
|
+
export * from './useLiveSuspenseQuery';
|
|
4
|
+
export * from './usePacedMutations';
|
|
5
|
+
export * from './useLiveInfiniteQuery';
|
|
6
|
+
export * from './useLiveQueryEffect';
|
|
7
|
+
|
|
8
|
+
// Re-export everything from @tanstack/db
|
|
9
|
+
export * from '@tanstack/db';
|
|
10
|
+
|
|
11
|
+
// Re-export some stuff explicitly to ensure the type & value is exported
|
|
12
|
+
export type { Collection } from '@tanstack/db';
|
|
13
|
+
export { createTransaction } from '@tanstack/db';
|
package/src/slot.ts
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const subSlotCache = new Map<symbol, Map<string, symbol>>();
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Derive a stable sub-slot from a wrapper's compiler-injected slot so nested
|
|
5
|
+
* Octane hooks inside a custom hook each get distinct call-site identity.
|
|
6
|
+
*/
|
|
7
|
+
export function subSlot(slot: symbol | undefined, tag: string): symbol | undefined {
|
|
8
|
+
if (slot === undefined) return undefined;
|
|
9
|
+
let byTag = subSlotCache.get(slot);
|
|
10
|
+
if (byTag === undefined) subSlotCache.set(slot, (byTag = new Map()));
|
|
11
|
+
let sym = byTag.get(tag);
|
|
12
|
+
if (sym === undefined) {
|
|
13
|
+
sym = Symbol.for(`${slot.description ?? ``}:${tag}`);
|
|
14
|
+
byTag.set(tag, sym);
|
|
15
|
+
}
|
|
16
|
+
return sym;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Split a compiler-injected trailing slot symbol off hook runtime args. */
|
|
20
|
+
export function splitTrailingSlot<T extends Array<unknown>>(
|
|
21
|
+
args: T,
|
|
22
|
+
): [Array<unknown>, symbol | undefined] {
|
|
23
|
+
const tail = args[args.length - 1];
|
|
24
|
+
const slot = typeof tail === `symbol` ? tail : undefined;
|
|
25
|
+
return [slot !== undefined ? args.slice(0, -1) : args, slot];
|
|
26
|
+
}
|