Skip to content

Commit bea99b0

Browse files
committed
Persistence
1 parent c9aabad commit bea99b0

22 files changed

+1048
-8
lines changed

CONVENTIONS.md

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,27 @@ export const Store = () => {
8686

8787
useProvideStore(STORE_ID, store);
8888

89+
// Persistence (if enabled)
90+
{{#if persist}}
91+
{{#if persistLocalStorage}}
92+
useCreatePersister(
93+
store,
94+
(store) => createLocalPersister(store, STORE_ID),
95+
[],
96+
async (persister) => {
97+
await persister.startAutoLoad();
98+
await persister.startAutoSave();
99+
},
100+
);
101+
{{/if}}
102+
// Similar patterns for persistSqlite and persistPglite
103+
{{/if}}
104+
105+
// Synchronization (if enabled)
106+
{{#if sync}}
107+
// ... synchronizer setup
108+
{{/if}}
109+
89110
return null;
90111
};
91112
```
@@ -133,6 +154,20 @@ export const store = createMergeableStore().setTable('todos', {
133154
/* ... */
134155
});
135156

157+
// Persistence (if enabled)
158+
{{#if persist}}
159+
{{#if persistLocalStorage}}
160+
const persister = createLocalPersister(store, 'todos');
161+
persister.startAutoLoad().then(() => persister.startAutoSave());
162+
{{/if}}
163+
// Similar patterns for persistSqlite and persistPglite
164+
{{/if}}
165+
166+
// Synchronization (if enabled)
167+
{{#if sync}}
168+
// ... synchronizer setup
169+
{{/if}}
170+
136171
export type TodosStore = typeof store; // or GameStore
137172
```
138173

@@ -457,6 +492,126 @@ App
457492

458493
---
459494

495+
## Persistence Patterns
496+
497+
### Order of Initialization
498+
499+
Persistence should be configured **before** synchronization to avoid race conditions.
500+
501+
**React Pattern:**
502+
503+
```typescript
504+
export const Store = () => {
505+
const store = useCreateStore(() => createMergeableStore());
506+
507+
useProvideStore(STORE_ID, store);
508+
509+
// 1. Configure persistence FIRST
510+
useCreatePersister(
511+
store,
512+
(store) => createLocalPersister(store, STORE_ID),
513+
[],
514+
async (persister) => {
515+
await persister.startAutoLoad();
516+
await persister.startAutoSave();
517+
},
518+
);
519+
520+
// 2. Configure synchronization SECOND
521+
useCreateSynchronizer(store, async (store) => {
522+
const synchronizer = await createWsSynchronizer(/* ... */);
523+
await synchronizer.startSync();
524+
return synchronizer;
525+
});
526+
527+
return null;
528+
};
529+
```
530+
531+
**Vanilla Pattern:**
532+
533+
```typescript
534+
export const store = createMergeableStore();
535+
536+
// 1. Configure persistence FIRST
537+
const persister = createLocalPersister(store, 'todos');
538+
persister.startAutoLoad().then(() => persister.startAutoSave());
539+
540+
// 2. Configure synchronization SECOND
541+
createWsSynchronizer(store, new ReconnectingWebSocket(SERVER)).then(
542+
async (synchronizer) => {
543+
await synchronizer.startSync();
544+
},
545+
);
546+
```
547+
548+
### Persister Types
549+
550+
**Local Storage (Browser):**
551+
552+
```typescript
553+
import {createLocalPersister} from 'tinybase/persisters/persister-browser';
554+
555+
const persister = createLocalPersister(store, STORE_ID);
556+
await persister.startAutoLoad();
557+
await persister.startAutoSave();
558+
```
559+
560+
**SQLite (WebAssembly):**
561+
562+
```typescript
563+
import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';
564+
import sqlite3InitModule from '@sqlite.org/sqlite-wasm';
565+
566+
const sqlite3 = await sqlite3InitModule();
567+
const db = new sqlite3.oo1.DB(':local:' + STORE_ID, 'c');
568+
const persister = createSqliteWasmPersister(store, sqlite3, db, STORE_ID);
569+
await persister.startAutoLoad();
570+
await persister.startAutoSave();
571+
```
572+
573+
**PGLite (PostgreSQL in Browser):**
574+
575+
```typescript
576+
import {createPglitePersister} from 'tinybase/persisters/persister-pglite';
577+
import {PGlite} from '@electric-sql/pglite';
578+
579+
const pgLite = await PGlite.create('idb://' + STORE_ID);
580+
const persister = createPglitePersister(store, pgLite, STORE_ID);
581+
await persister.startAutoLoad();
582+
await persister.startAutoSave();
583+
```
584+
585+
### Multi-Store Persistence
586+
587+
In apps with multiple stores (chat, drawing), **both** stores get persistence:
588+
589+
```typescript
590+
// settingsStore - local preferences
591+
const settingsPersister = createLocalPersister(settingsStore, 'settings');
592+
settingsPersister.startAutoLoad().then(() => settingsPersister.startAutoSave());
593+
594+
// chatStore - collaborative data (also gets persistence AND sync)
595+
const chatPersister = createLocalPersister(chatStore, 'chat');
596+
chatPersister.startAutoLoad().then(() => chatPersister.startAutoSave());
597+
```
598+
599+
### Schema Support
600+
601+
When using schemas, import from the schema-aware paths:
602+
603+
```typescript
604+
{{#if schemas}}
605+
import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';
606+
import {useCreatePersister} from 'tinybase/ui-react/with-schemas';
607+
{{else}}
608+
import {createLocalPersister} from 'tinybase/persisters/persister-browser';
609+
import {useCreatePersister} from 'tinybase/ui-react';
610+
{{/if}}
611+
```
612+
613+
---
614+
460615
## Key Principles
461616

462617
1. **Consistency**: React and vanilla versions should follow parallel patterns

README.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ This will prompt you with questions to configure your new TinyBase app:
2323
- **App type** - Todo app, Chat app, Drawing app, or Tic-tac-toe game
2424
- **Store schemas** - TypeScript type safety for stores (TypeScript only)
2525
- **Synchronization** - None, remote demo server, local Node server, or local Durable Objects server
26+
- **Persistence** - None, Local Storage, SQLite, or PGLite
2627
- **Prettier** - Include Prettier for code formatting
2728
- **ESLint** - Include ESLint for code linting
2829

@@ -184,6 +185,50 @@ Choose from four synchronization options:
184185
- Runs locally with Wrangler
185186
- Designed for production deployment
186187

188+
### Persistence
189+
190+
Choose how to persist store data on the client:
191+
192+
**None**:
193+
194+
- No client-side persistence
195+
- Data lost on page refresh
196+
- Simplest option for demos
197+
- Still works with synchronization
198+
199+
**Local Storage** (default):
200+
201+
- Browser localStorage persistence
202+
- Data persists across sessions
203+
- Simple and widely supported
204+
- Automatic load/save with `createLocalPersister`
205+
- Best for most use cases
206+
207+
**SQLite**:
208+
209+
- Browser-based SQLite via WebAssembly
210+
- Structured database storage
211+
- Uses `@sqlite.org/sqlite-wasm` package
212+
- IndexedDB-backed (`:local:` prefix)
213+
- Good for complex data structures
214+
- Slightly larger bundle size
215+
216+
**PGLite**:
217+
218+
- PostgreSQL in the browser
219+
- Full PostgreSQL compatibility
220+
- Uses `@electric-sql/pglite` package
221+
- IndexedDB-backed (`idb://` prefix)
222+
- Advanced SQL features
223+
- Larger bundle size
224+
225+
**Important Notes:**
226+
227+
- Persistence is configured **before** synchronization to avoid race conditions
228+
- Works with all sync types (or no sync)
229+
- In multi-store apps, all stores get persistence
230+
- Data is automatically loaded on app start and saved on changes
231+
187232
### Prettier & ESLint
188233

189234
**Prettier**:

cspell.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
"jamesgpearce",
66
"networkidle",
77
"Pearce",
8+
"pglite",
89
"PIDS",
910
"tinybase",
1011
"tinycreate",

screenshots/chat.png

105 Bytes
Loading

screenshots/drawing.png

165 Bytes
Loading

screenshots/game.png

-4 Bytes
Loading

src/cli.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,18 @@ const config = {
8787
],
8888
initial: 1,
8989
},
90+
{
91+
type: 'select' as const,
92+
name: 'persistenceType',
93+
message: 'Persistence:',
94+
choices: [
95+
{title: 'None', value: 'none'},
96+
{title: 'Local Storage', value: 'local-storage'},
97+
{title: 'SQLite', value: 'sqlite'},
98+
{title: 'PGlite', value: 'pglite'},
99+
],
100+
initial: 1,
101+
},
90102
{
91103
type: 'confirm' as const,
92104
name: 'prettier',
@@ -120,6 +132,7 @@ const config = {
120132
eslint,
121133
schemas,
122134
syncType,
135+
persistenceType,
123136
installAndRun,
124137
} = answers;
125138
const typescript = language === 'typescript';
@@ -132,6 +145,11 @@ const config = {
132145
normalizedSyncType === 'node' || normalizedSyncType === 'durable-objects';
133146
const serverType =
134147
normalizedSyncType === 'durable-objects' ? 'durable-objects' : 'node';
148+
const normalizedPersistenceType = persistenceType || 'local-storage';
149+
const persist = normalizedPersistenceType !== 'none';
150+
const persistLocalStorage = normalizedPersistenceType === 'local-storage';
151+
const persistSqlite = normalizedPersistenceType === 'sqlite';
152+
const persistPglite = normalizedPersistenceType === 'pglite';
135153

136154
return {
137155
projectName,
@@ -145,6 +163,11 @@ const config = {
145163
sync,
146164
server,
147165
serverType,
166+
persistenceType: normalizedPersistenceType,
167+
persist,
168+
persistLocalStorage,
169+
persistSqlite,
170+
persistPglite,
148171
installAndRun: installAndRun === true || installAndRun === 'true',
149172
typescript,
150173
javascript,

templates/client/package.json.hbs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,12 @@
5959
{{#if sync}}
6060
"reconnecting-websocket": "^4.4.0"
6161
{{/if}}
62+
{{#if persistSqlite}}
63+
"@sqlite.org/sqlite-wasm": "^3.50.4-build1"
64+
{{/if}}
65+
{{#if persistPglite}}
66+
"@electric-sql/pglite": "^0.3.15"
67+
{{/if}}
6268
{{/list}}
6369
}
6470
}

templates/client/src/chat/ChatStore.tsx.hbs

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ export const STORE_ID = 'chat';
2323

2424
type Schemas = [typeof TABLES_SCHEMA, NoValuesSchema];
2525

26-
const {useCreateMergeableStore, useProvideStore, useAddRowCallback, useRow, useSortedRowIds, useStore} = UiReact as UiReact.WithSchemas<Schemas>;
26+
const {useAddRowCallback, useCreateMergeableStore, {{#if persist}}useCreatePersister, {{/if}}useProvideStore, useRow, useSortedRowIds, useStore} = UiReact as UiReact.WithSchemas<Schemas>;
2727
{{/if}}
2828

2929
export type MessageRow = {{#if schemas}}Row<typeof TABLES_SCHEMA, 'messages'>{{else}}{username: string; text: string; timestamp: number}{{/if}};
@@ -39,6 +39,72 @@ createMergeableStore(){{#if schemas}}
3939

4040
useProvideStore(STORE_ID, store);
4141

42+
{{#if persist}}
43+
{{#if persistLocalStorage}}
44+
{{#if schemas}}
45+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser/with-schemas';"}}
46+
{{else}}
47+
{{addImport "import {createLocalPersister} from 'tinybase/persisters/persister-browser';"}}
48+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
49+
{{/if}}
50+
51+
useCreatePersister(
52+
store,
53+
(store) => createLocalPersister(store, STORE_ID),
54+
[],
55+
async (persister) => {
56+
await persister.startAutoLoad();
57+
await persister.startAutoSave();
58+
},
59+
);
60+
{{/if}}
61+
{{#if persistSqlite}}
62+
{{#if schemas}}
63+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm/with-schemas';"}}
64+
{{else}}
65+
{{addImport "import {createSqliteWasmPersister} from 'tinybase/persisters/persister-sqlite-wasm';"}}
66+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
67+
{{/if}}
68+
{{addImport "import sqlite3InitModule from '@sqlite.org/sqlite-wasm';"}}
69+
70+
useCreatePersister(
71+
store,
72+
async (store) => {
73+
const sqlite3 = await sqlite3InitModule();
74+
const db = new sqlite3.oo1.DB(':local:' + STORE_ID, 'c');
75+
return createSqliteWasmPersister(store, sqlite3, db, STORE_ID);
76+
},
77+
[],
78+
async (persister) => {
79+
await persister.startAutoLoad();
80+
await persister.startAutoSave();
81+
},
82+
);
83+
{{/if}}
84+
{{#if persistPglite}}
85+
{{#if schemas}}
86+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite/with-schemas';"}}
87+
{{else}}
88+
{{addImport "import {createPglitePersister} from 'tinybase/persisters/persister-pglite';"}}
89+
{{addImport "import {useCreatePersister} from 'tinybase/ui-react';"}}
90+
{{/if}}
91+
{{addImport "import {PGlite} from '@electric-sql/pglite';"}}
92+
93+
useCreatePersister(
94+
store,
95+
async (store) => {
96+
const pgLite = await PGlite.create('idb://' + STORE_ID);
97+
return createPglitePersister(store, pgLite, STORE_ID);
98+
},
99+
[],
100+
async (persister) => {
101+
await persister.startAutoLoad();
102+
await persister.startAutoSave();
103+
},
104+
);
105+
{{/if}}
106+
{{/if}}
107+
42108
{{#if sync}}
43109
{{includeFile template="client/src/shared/config.ts.hbs" output="client/src/config.ts"}}
44110
{{addImport "import {SERVER} from './config';"}}

0 commit comments

Comments
 (0)