Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 3 additions & 0 deletions examples/react/offline-transactions/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"start": "node .output/server/index.mjs"
},
"dependencies": {
"@journeyapps/wa-sqlite": "^1.4.1",
"@tanstack/db": "workspace:*",
"@tanstack/db-browser-wa-sqlite-persisted-collection": "workspace:*",
"@tanstack/offline-transactions": "^1.0.21",
"@tanstack/query-db-collection": "^1.0.27",
"@tanstack/react-db": "^0.1.74",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
import React, { useState } from 'react'
import { useLiveQuery } from '@tanstack/react-db'
import type { Collection } from '@tanstack/db'
import type { PersistedTodo } from '~/db/persisted-todos'

interface PersistedTodoDemoProps {
collection: Collection<PersistedTodo, string>
}

export function PersistedTodoDemo({ collection }: PersistedTodoDemoProps) {
const [newTodoText, setNewTodoText] = useState(``)
const [error, setError] = useState<string | null>(null)

const { data: todoList = [] } = useLiveQuery((q) =>
q.from({ todo: collection }).orderBy(({ todo }) => todo.createdAt, `desc`),
)

const handleAddTodo = () => {
if (!newTodoText.trim()) return

try {
setError(null)
const now = new Date().toISOString()
collection.insert({
id: crypto.randomUUID(),
text: newTodoText.trim(),
completed: false,
createdAt: now,
updatedAt: now,
})
setNewTodoText(``)
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to add todo`)
}
}

const handleToggleTodo = (id: string) => {
try {
setError(null)
collection.update(id, (draft) => {
draft.completed = !draft.completed
draft.updatedAt = new Date().toISOString()
})
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to toggle todo`)
}
}

const handleDeleteTodo = (id: string) => {
try {
setError(null)
collection.delete(id)
} catch (err) {
setError(err instanceof Error ? err.message : `Failed to delete todo`)
}
}

const handleKeyPress = (e: React.KeyboardEvent) => {
if (e.key === `Enter`) {
handleAddTodo()
}
}

return (
<div className="max-w-2xl mx-auto p-6">
<div className="bg-white rounded-lg shadow-lg p-6">
<div className="flex items-center gap-3 mb-4">
<span className="text-2xl">🗃️</span>
<div>
<h2 className="text-2xl font-bold text-gray-900">
wa-sqlite OPFS Persistence Demo
</h2>
<p className="text-gray-600">
Collection data is persisted to SQLite via OPFS. Data survives
page reloads without any server sync.
</p>
</div>
</div>

{/* Persistence indicator */}
<div className="flex flex-wrap gap-4 mb-6 text-sm">
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-emerald-100 text-emerald-800">
<div className="w-2 h-2 rounded-full bg-emerald-500" />
SQLite OPFS Persistence Active
</div>
<div className="flex items-center gap-2 px-3 py-1 rounded-full bg-gray-100 text-gray-600">
{todoList.length} todo{todoList.length !== 1 ? `s` : ``}
</div>
</div>

{/* Error display */}
{error && (
<div className="mb-4 p-3 bg-red-100 border border-red-300 rounded-md">
<p className="text-red-700 text-sm">{error}</p>
</div>
)}

{/* Add new todo */}
<div className="flex gap-2 mb-6">
<input
type="text"
value={newTodoText}
onChange={(e) => setNewTodoText(e.target.value)}
onKeyPress={handleKeyPress}
placeholder="Add a new todo..."
className="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
<button
onClick={handleAddTodo}
disabled={!newTodoText.trim()}
className="px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
>
Add
</button>
</div>

{/* Todo list */}
<div className="space-y-2">
{todoList.length === 0 ? (
<div className="text-center py-8 text-gray-500">
No todos yet. Add one above to get started!
<br />
<span className="text-xs">
Try adding todos, then refresh the page to see them persist
</span>
</div>
) : (
todoList.map((todo) => (
<div
key={todo.id}
className="flex items-center gap-3 p-3 border border-gray-200 rounded-md hover:bg-gray-50"
>
<button
onClick={() => handleToggleTodo(todo.id)}
className={`w-5 h-5 rounded border-2 flex items-center justify-center ${
todo.completed
? `bg-green-500 border-green-500 text-white`
: `border-gray-300 hover:border-green-400`
}`}
>
{todo.completed && <span className="text-xs">✓</span>}
</button>
<span
className={`flex-1 ${
todo.completed
? `line-through text-gray-500`
: `text-gray-900`
}`}
>
{todo.text}
</span>
<span className="text-xs text-gray-400">
{new Date(todo.createdAt).toLocaleDateString()}
</span>
<button
onClick={() => handleDeleteTodo(todo.id)}
className="px-2 py-1 text-red-600 hover:bg-red-50 rounded text-sm"
>
Delete
</button>
</div>
))
)}
</div>

{/* Instructions */}
<div className="mt-6 p-4 bg-gray-50 rounded-md">
<h3 className="font-medium text-gray-900 mb-2">Try this:</h3>
<ol className="text-sm text-gray-600 space-y-1">
<li>1. Add some todos</li>
<li>2. Refresh the page (Ctrl+R / Cmd+R)</li>
<li>
3. Your todos are still here - persisted in SQLite via OPFS!
</li>
<li>4. This uses wa-sqlite with OPFSCoopSyncVFS in a Web Worker</li>
</ol>
</div>
</div>
</div>
)
}
53 changes: 53 additions & 0 deletions examples/react/offline-transactions/src/db/persisted-todos.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { createCollection } from '@tanstack/react-db'
import {
BrowserCollectionCoordinator,
createBrowserWASQLitePersistence,
openBrowserWASQLiteOPFSDatabase,
persistedCollectionOptions,
} from '@tanstack/db-browser-wa-sqlite-persisted-collection'
import type { Collection } from '@tanstack/db'

export type PersistedTodo = {
id: string
text: string
completed: boolean
createdAt: string
updatedAt: string
}

export type PersistedTodosHandle = {
collection: Collection<PersistedTodo, string>
close: () => Promise<void>
}

export async function createPersistedTodoCollection(): Promise<PersistedTodosHandle> {
const database = await openBrowserWASQLiteOPFSDatabase({
databaseName: `tanstack-db-demo-v2.sqlite`,
})

const coordinator = new BrowserCollectionCoordinator({
dbName: `tanstack-db-demo`,
})

const persistence = createBrowserWASQLitePersistence({
database,
coordinator,
})

const collection = createCollection<PersistedTodo, string>(
persistedCollectionOptions<PersistedTodo, string>({
id: `persisted-todos`,
getKey: (todo) => todo.id,
persistence,
schemaVersion: 1,
}),
)

return {
collection,
close: async () => {
coordinator.dispose()
await database.close?.()
},
}
}
24 changes: 21 additions & 3 deletions examples/react/offline-transactions/src/routeTree.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,16 @@
// Additionally, you should also exclude this file from your linter and/or formatter to prevent it from being checked or modified.

import { Route as rootRouteImport } from './routes/__root'
import { Route as WaSqliteRouteImport } from './routes/wa-sqlite'
import { Route as LocalstorageRouteImport } from './routes/localstorage'
import { Route as IndexeddbRouteImport } from './routes/indexeddb'
import { Route as IndexRouteImport } from './routes/index'

const WaSqliteRoute = WaSqliteRouteImport.update({
id: '/wa-sqlite',
path: '/wa-sqlite',
getParentRoute: () => rootRouteImport,
} as any)
const LocalstorageRoute = LocalstorageRouteImport.update({
id: '/localstorage',
path: '/localstorage',
Expand All @@ -33,34 +39,45 @@ export interface FileRoutesByFullPath {
'/': typeof IndexRoute
'/indexeddb': typeof IndexeddbRoute
'/localstorage': typeof LocalstorageRoute
'/wa-sqlite': typeof WaSqliteRoute
}
export interface FileRoutesByTo {
'/': typeof IndexRoute
'/indexeddb': typeof IndexeddbRoute
'/localstorage': typeof LocalstorageRoute
'/wa-sqlite': typeof WaSqliteRoute
}
export interface FileRoutesById {
__root__: typeof rootRouteImport
'/': typeof IndexRoute
'/indexeddb': typeof IndexeddbRoute
'/localstorage': typeof LocalstorageRoute
'/wa-sqlite': typeof WaSqliteRoute
}
export interface FileRouteTypes {
fileRoutesByFullPath: FileRoutesByFullPath
fullPaths: '/' | '/indexeddb' | '/localstorage'
fullPaths: '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite'
fileRoutesByTo: FileRoutesByTo
to: '/' | '/indexeddb' | '/localstorage'
id: '__root__' | '/' | '/indexeddb' | '/localstorage'
to: '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite'
id: '__root__' | '/' | '/indexeddb' | '/localstorage' | '/wa-sqlite'
fileRoutesById: FileRoutesById
}
export interface RootRouteChildren {
IndexRoute: typeof IndexRoute
IndexeddbRoute: typeof IndexeddbRoute
LocalstorageRoute: typeof LocalstorageRoute
WaSqliteRoute: typeof WaSqliteRoute
}

declare module '@tanstack/react-router' {
interface FileRoutesByPath {
'/wa-sqlite': {
id: '/wa-sqlite'
path: '/wa-sqlite'
fullPath: '/wa-sqlite'
preLoaderRoute: typeof WaSqliteRouteImport
parentRoute: typeof rootRouteImport
}
'/localstorage': {
id: '/localstorage'
path: '/localstorage'
Expand Down Expand Up @@ -89,6 +106,7 @@ const rootRouteChildren: RootRouteChildren = {
IndexRoute: IndexRoute,
IndexeddbRoute: IndexeddbRoute,
LocalstorageRoute: LocalstorageRoute,
WaSqliteRoute: WaSqliteRoute,
}
export const routeTree = rootRouteImport
._addFileChildren(rootRouteChildren)
Expand Down
4 changes: 4 additions & 0 deletions examples/react/offline-transactions/src/router.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export function createRouter() {
return router
}

export async function getRouter() {
return createRouter()
}

declare module '@tanstack/react-router' {
interface Register {
router: ReturnType<typeof createRouter>
Expand Down
9 changes: 9 additions & 0 deletions examples/react/offline-transactions/src/routes/__root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,15 @@ function RootDocument({ children }: { children: React.ReactNode }) {
>
💾 localStorage
</Link>
<Link
to="/wa-sqlite"
activeProps={{
className: `font-bold text-blue-600`,
}}
className="text-gray-600 hover:text-gray-900"
>
🗃️ wa-sqlite
</Link>
</div>
</div>
</div>
Expand Down
37 changes: 37 additions & 0 deletions examples/react/offline-transactions/src/routes/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,43 @@ function Home() {
</Link>
</div>

<div className="mb-8">
<Link to="/wa-sqlite">
<div className="bg-white rounded-lg shadow-lg p-6 hover:shadow-xl transition-shadow cursor-pointer group border-2 border-emerald-200">
<div className="flex items-center mb-4">
<span className="text-3xl mr-3">🗃️</span>
<div>
<h2 className="text-2xl font-bold text-gray-900 group-hover:text-emerald-600">
wa-sqlite OPFS Persistence
</h2>
<span className="text-xs px-2 py-0.5 bg-emerald-100 text-emerald-700 rounded-full">
NEW
</span>
</div>
</div>
<p className="text-gray-600 mb-4">
Collection-level persistence using wa-sqlite with OPFS. Data is
stored in a real SQLite database in the browser via a Web
Worker. Survives page reloads without server sync.
</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-emerald-100 text-emerald-800 text-xs rounded">
SQLite in Browser
</span>
<span className="px-2 py-1 bg-blue-100 text-blue-800 text-xs rounded">
OPFS Storage
</span>
<span className="px-2 py-1 bg-purple-100 text-purple-800 text-xs rounded">
Web Worker
</span>
<span className="px-2 py-1 bg-orange-100 text-orange-800 text-xs rounded">
Local-only
</span>
</div>
</div>
</Link>
</div>

<div className="bg-white rounded-lg shadow-lg p-8">
<h2 className="text-2xl font-bold text-gray-900 mb-6">
Features Demonstrated
Expand Down
Loading
Loading