Skip to content

Commit e22631a

Browse files
kevin-dpclaude
andcommitted
feat(examples): add wa-sqlite OPFS persistence demo to offline-transactions
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent d351c67 commit e22631a

8 files changed

Lines changed: 470 additions & 6 deletions

File tree

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import React, { useState } from 'react'
2+
import { useLiveQuery } from '@tanstack/react-db'
3+
import type { Collection } from '@tanstack/db'
4+
import type { PersistedTodo } from '~/db/persisted-todos'
5+
6+
interface PersistedTodoDemoProps {
7+
collection: Collection<PersistedTodo, string>
8+
}
9+
10+
export function PersistedTodoDemo({ collection }: PersistedTodoDemoProps) {
11+
const [newTodoText, setNewTodoText] = useState(``)
12+
const [error, setError] = useState<string | null>(null)
13+
14+
const { data: todoList = [] } = useLiveQuery((q) =>
15+
q.from({ todo: collection }).orderBy(({ todo }) => todo.createdAt, `desc`),
16+
)
17+
18+
const handleAddTodo = () => {
19+
if (!newTodoText.trim()) return
20+
21+
try {
22+
setError(null)
23+
const now = new Date().toISOString()
24+
collection.insert({
25+
id: crypto.randomUUID(),
26+
text: newTodoText.trim(),
27+
completed: false,
28+
createdAt: now,
29+
updatedAt: now,
30+
})
31+
setNewTodoText(``)
32+
} catch (err) {
33+
setError(err instanceof Error ? err.message : `Failed to add todo`)
34+
}
35+
}
36+
37+
const handleToggleTodo = (id: string) => {
38+
try {
39+
setError(null)
40+
collection.update(id, (draft) => {
41+
draft.completed = !draft.completed
42+
draft.updatedAt = new Date().toISOString()
43+
})
44+
} catch (err) {
45+
setError(err instanceof Error ? err.message : `Failed to toggle todo`)
46+
}
47+
}
48+
49+
const handleDeleteTodo = (id: string) => {
50+
try {
51+
setError(null)
52+
collection.delete(id)
53+
} catch (err) {
54+
setError(err instanceof Error ? err.message : `Failed to delete todo`)
55+
}
56+
}
57+
58+
const handleKeyPress = (e: React.KeyboardEvent) => {
59+
if (e.key === `Enter`) {
60+
handleAddTodo()
61+
}
62+
}
63+
64+
return (
65+
<div className="max-w-2xl mx-auto p-6">
66+
<div className="bg-white rounded-lg shadow-lg p-6">
67+
<div className="flex items-center gap-3 mb-4">
68+
<span className="text-2xl">🗃️</span>
69+
<div>
70+
<h2 className="text-2xl font-bold text-gray-900">
71+
wa-sqlite OPFS Persistence Demo
72+
</h2>
73+
<p className="text-gray-600">
74+
Collection data is persisted to SQLite via OPFS. Data survives
75+
page reloads without any server sync.
76+
</p>
77+
</div>
78+
</div>
79+
80+
{/* Persistence indicator */}
81+
<div className="flex flex-wrap gap-4 mb-6 text-sm">
82+
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-100 text-emerald-800">
83+
<div className="w-2 h-2 rounded-full bg-emerald-500" />
84+
SQLite OPFS Persistence Active
85+
</div>
86+
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100 text-gray-600">
87+
{todoList.length} todo{todoList.length !== 1 ? `s` : ``}
88+
</div>
89+
</div>
90+
91+
{/* Error display */}
92+
{error && (
93+
<div className="mb-4 p-3 bg-red-100 border border-red-300 rounded-md">
94+
<p className="text-red-700 text-sm">{error}</p>
95+
</div>
96+
)}
97+
98+
{/* Add new todo */}
99+
<div className="flex gap-2 mb-6">
100+
<input
101+
type="text"
102+
value={newTodoText}
103+
onChange={(e) => setNewTodoText(e.target.value)}
104+
onKeyPress={handleKeyPress}
105+
placeholder="Add a new todo..."
106+
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
107+
/>
108+
<button
109+
onClick={handleAddTodo}
110+
disabled={!newTodoText.trim()}
111+
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
112+
>
113+
Add
114+
</button>
115+
</div>
116+
117+
{/* Todo list */}
118+
<div className="space-y-2">
119+
{todoList.length === 0 ? (
120+
<div className="text-center py-8 text-gray-500">
121+
No todos yet. Add one above to get started!
122+
<br />
123+
<span className="text-xs">
124+
Try adding todos, then refresh the page to see them persist
125+
</span>
126+
</div>
127+
) : (
128+
todoList.map((todo) => (
129+
<div
130+
key={todo.id}
131+
className="flex items-center gap-3 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
132+
>
133+
<button
134+
onClick={() => handleToggleTodo(todo.id)}
135+
className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
136+
todo.completed
137+
? `bg-green-500 border-green-500 text-white`
138+
: `border-gray-300 hover:border-green-400`
139+
}`}
140+
>
141+
{todo.completed && <span className="text-xs"></span>}
142+
</button>
143+
<span
144+
className={`flex-1 ${
145+
todo.completed
146+
? `line-through text-gray-500`
147+
: `text-gray-900`
148+
}`}
149+
>
150+
{todo.text}
151+
</span>
152+
<span className="text-xs text-gray-400">
153+
{new Date(todo.createdAt).toLocaleDateString()}
154+
</span>
155+
<button
156+
onClick={() => handleDeleteTodo(todo.id)}
157+
className="px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm"
158+
>
159+
Delete
160+
</button>
161+
</div>
162+
))
163+
)}
164+
</div>
165+
166+
{/* Instructions */}
167+
<div className="mt-6 p-4 bg-gray-50 rounded-md">
168+
<h3 className="font-medium text-gray-900 mb-2">Try this:</h3>
169+
<ol className="text-sm text-gray-600 space-y-1">
170+
<li>1. Add some todos</li>
171+
<li>2. Refresh the page (Ctrl+R / Cmd+R)</li>
172+
<li>
173+
3. Your todos are still here - persisted in SQLite via OPFS!
174+
</li>
175+
<li>4. This uses wa-sqlite with OPFSCoopSyncVFS in a Web Worker</li>
176+
</ol>
177+
</div>
178+
</div>
179+
</div>
180+
)
181+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { createCollection } from '@tanstack/react-db'
2+
import {
3+
BrowserCollectionCoordinator,
4+
createBrowserWASQLitePersistence,
5+
openBrowserWASQLiteOPFSDatabase,
6+
persistedCollectionOptions,
7+
} from '@tanstack/db-browser-wa-sqlite-persisted-collection'
8+
import type { Collection } from '@tanstack/db'
9+
10+
export type PersistedTodo = {
11+
id: string
12+
text: string
13+
completed: boolean
14+
createdAt: string
15+
updatedAt: string
16+
}
17+
18+
export type PersistedTodosHandle = {
19+
collection: Collection<PersistedTodo, string>
20+
close: () => Promise<void>
21+
}
22+
23+
export async function createPersistedTodoCollection(): Promise<PersistedTodosHandle> {
24+
const database = await openBrowserWASQLiteOPFSDatabase({
25+
databaseName: `tanstack-db-demo-v2.sqlite`,
26+
})
27+
28+
const coordinator = new BrowserCollectionCoordinator({
29+
dbName: `tanstack-db-demo`,
30+
})
31+
32+
const persistence = createBrowserWASQLitePersistence({
33+
database,
34+
coordinator,
35+
})
36+
37+
const collection = createCollection<PersistedTodo, string>(
38+
persistedCollectionOptions<PersistedTodo, string>({
39+
id: `persisted-todos`,
40+
getKey: (todo) => todo.id,
41+
persistence,
42+
schemaVersion: 1,
43+
}),
44+
)
45+
46+
return {
47+
collection,
48+
close: async () => {
49+
coordinator.dispose()
50+
await database.close?.()
51+
},
52+
}
53+
}

examples/react/offline-transactions/src/routeTree.gen.ts

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,16 @@
99
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.
1010

1111
import { Route as rootRouteImport } from './routes/__root'
12+
import { Route as WaSqliteRouteImport } from './routes/wa-sqlite'
1213
import { Route as LocalstorageRouteImport } from './routes/localstorage'
1314
import { Route as IndexeddbRouteImport } from './routes/indexeddb'
1415
import { Route as IndexRouteImport } from './routes/index'
1516

17+
const WaSqliteRoute = WaSqliteRouteImport.update({
18+
id: '/wa-sqlite',
19+
path: '/wa-sqlite',
20+
getParentRoute: () => rootRouteImport,
21+
} as any)
1622
const LocalstorageRoute = LocalstorageRouteImport.update({
1723
id: '/localstorage',
1824
path: '/localstorage',
@@ -33,34 +39,45 @@ export interface FileRoutesByFullPath {
3339
'/': typeof IndexRoute
3440
'/indexeddb': typeof IndexeddbRoute
3541
'/localstorage': typeof LocalstorageRoute
42+
'/wa-sqlite': typeof WaSqliteRoute
3643
}
3744
export interface FileRoutesByTo {
3845
'/': typeof IndexRoute
3946
'/indexeddb': typeof IndexeddbRoute
4047
'/localstorage': typeof LocalstorageRoute
48+
'/wa-sqlite': typeof WaSqliteRoute
4149
}
4250
export interface FileRoutesById {
4351
__root__: typeof rootRouteImport
4452
'/': typeof IndexRoute
4553
'/indexeddb': typeof IndexeddbRoute
4654
'/localstorage': typeof LocalstorageRoute
55+
'/wa-sqlite': typeof WaSqliteRoute
4756
}
4857
export interface FileRouteTypes {
4958
fileRoutesByFullPath: FileRoutesByFullPath
50-
fullPaths: '/' | '/indexeddb' | '/localstorage'
59+
fullPaths: '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite'
5160
fileRoutesByTo: FileRoutesByTo
52-
to: '/' | '/indexeddb' | '/localstorage'
53-
id: '__root__' | '/' | '/indexeddb' | '/localstorage'
61+
to: '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite'
62+
id: '__root__' | '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite'
5463
fileRoutesById: FileRoutesById
5564
}
5665
export interface RootRouteChildren {
5766
IndexRoute: typeof IndexRoute
5867
IndexeddbRoute: typeof IndexeddbRoute
5968
LocalstorageRoute: typeof LocalstorageRoute
69+
WaSqliteRoute: typeof WaSqliteRoute
6070
}
6171

6272
declare module '@tanstack/react-router' {
6373
interface FileRoutesByPath {
74+
'/wa-sqlite': {
75+
id: '/wa-sqlite'
76+
path: '/wa-sqlite'
77+
fullPath: '/wa-sqlite'
78+
preLoaderRoute: typeof WaSqliteRouteImport
79+
parentRoute: typeof rootRouteImport
80+
}
6481
'/localstorage': {
6582
id: '/localstorage'
6683
path: '/localstorage'
@@ -89,6 +106,7 @@ const rootRouteChildren: RootRouteChildren = {
89106
IndexRoute: IndexRoute,
90107
IndexeddbRoute: IndexeddbRoute,
91108
LocalstorageRoute: LocalstorageRoute,
109+
WaSqliteRoute: WaSqliteRoute,
92110
}
93111
export const routeTree = rootRouteImport
94112
._addFileChildren(rootRouteChildren)

examples/react/offline-transactions/src/router.tsx

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,10 @@ export function createRouter() {
1515
return router
1616
}
1717

18+
export async function getRouter() {
19+
return createRouter()
20+
}
21+
1822
declare module '@tanstack/react-router' {
1923
interface Register {
2024
router: ReturnType<typeof createRouter>

examples/react/offline-transactions/src/routes/__root.tsx

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,15 @@ function RootDocument({ children }: { children: React.ReactNode }) {
109109
>
110110
💾 localStorage
111111
</Link>
112+
<Link
113+
to="/wa-sqlite"
114+
activeProps={{
115+
className: `font-bold text-blue-600`,
116+
}}
117+
className="text-gray-600 hover:text-gray-900"
118+
>
119+
🗃️ wa-sqlite
120+
</Link>
112121
</div>
113122
</div>
114123
</div>

examples/react/offline-transactions/src/routes/index.tsx

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,43 @@ function Home() {
7373
</Link>
7474
</div>
7575

76+
<div className="mb-8">
77+
<Link to="/wa-sqlite">
78+
<div className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition-shadow cursor-pointer group border-2 border-emerald-200">
79+
<div className="flex items-center mb-4">
80+
<span className="text-3xl mr-3">🗃️</span>
81+
<div>
82+
<h2 className="text-2xl font-bold text-gray-900 group-hover:text-emerald-600">
83+
wa-sqlite OPFS Persistence
84+
</h2>
85+
<span className="text-xs px-2 py-0.5 bg-emerald-100 text-emerald-700 rounded-full">
86+
NEW
87+
</span>
88+
</div>
89+
</div>
90+
<p className="text-gray-600 mb-4">
91+
Collection-level persistence using wa-sqlite with OPFS. Data is
92+
stored in a real SQLite database in the browser via a Web
93+
Worker. Survives page reloads without server sync.
94+
</p>
95+
<div className="flex flex-wrap gap-2">
96+
<span className="px-2 py-1 bg-emerald-100 text-emerald-800 text-xs rounded">
97+
SQLite in Browser
98+
</span>
99+
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
100+
OPFS Storage
101+
</span>
102+
<span className="px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded">
103+
Web Worker
104+
</span>
105+
<span className="px-2 py-1 bg-orange-100 text-orange-800 text-xs rounded">
106+
Local-only
107+
</span>
108+
</div>
109+
</div>
110+
</Link>
111+
</div>
112+
76113
<div className="bg-white rounded-lg shadow-lg p-8">
77114
<h2 className="text-2xl font-bold text-gray-900 mb-6">
78115
Features Demonstrated

0 commit comments

Comments
 (0)