Storage and crash recovery
Cana stores browser data durably and exposes enough signals for the application to decide when to back up, clean up or reconcile local data.
Storage assessment
import { createClient, type CanaSchema } from '@jumentix/cana';
const schema: CanaSchema = {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id' },
{ name: 'tasks', keyPath: 'id', indexes: [{ name: 'byCategory', keyPath: 'categoryId' }] }
]
};
const client = createClient({
name: 'tasks-storage-demo',
schema
});
await client.open();
const storage = await client.storageState();
const durability = await client.durabilityAssessment();
console.log({
backend: client.backend,
storage,
durability
});Use the storage and durability signals to decide when the UI should offer export, cleanup or retry options.
Export and import
const exported = await client.exportAll();
const restored = createClient({
name: 'tasks-restored-demo',
schema
});
await restored.open();
await restored.importAll(exported);
console.log({
categories: await restored.table('categories').query(),
tasks: await restored.table('tasks').query()
});Use export/import for user-controlled backup, migration between application versions or diagnostics.
Resolve an uncertain write
const ledgerClient = createClient({
name: 'tasks-ledger-demo',
schema,
operationLedger: true
});
await ledgerClient.open();
const tx = await ledgerClient.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
const now = Date.now();
await scope.table('categories').put({
id: 'support',
name: 'Support',
color: '#0f766e',
createdAt: now,
updatedAt: now
});
await scope.table('tasks').put({
id: 'support-1',
title: 'Verify uncertain writes',
categoryId: 'support',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
});
return 'done';
});
const resolved = await ledgerClient.resolveWrite(tx.correlationId, tx.attemptedAt);
console.log({
outcome: tx.outcome,
resolved
});resolveWrite() is for writes reported as unknown, usually after a worker,
tab or connection dies before the caller receives the result.
Run it here
Storage assessment
Read storageState and durabilityAssessment after writing Task data.
### Storage assessment
```ts
const client = cana.createClient({
name: dbName,
schema: {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
{
name: 'tasks',
keyPath: 'id',
indexes: [
{ name: 'byCategory', keyPath: 'categoryId' },
{ name: 'byCompleted', keyPath: 'completed' },
{ name: 'byUpdatedAt', keyPath: 'updatedAt' }
]
}
]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
id: 'docs-1',
title: 'Durable data',
categoryId: 'docs',
completed: false,
priority: 'high',
createdAt: 1,
updatedAt: 1
});
const storage = await client.storageState();
const durability = await client.durabilityAssessment();
return { backend: client.backend, storage, durability };
```const client = cana.createClient({
name: dbName,
schema: {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
{
name: 'tasks',
keyPath: 'id',
indexes: [
{ name: 'byCategory', keyPath: 'categoryId' },
{ name: 'byCompleted', keyPath: 'completed' },
{ name: 'byUpdatedAt', keyPath: 'updatedAt' }
]
}
]
}
});
await client.open();
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
id: 'docs-1',
title: 'Durable data',
categoryId: 'docs',
completed: false,
priority: 'high',
createdAt: 1,
updatedAt: 1
});
const storage = await client.storageState();
const durability = await client.durabilityAssessment();
return { backend: client.backend, storage, durability };Operation ledger
Resolve a committed Task write with the operation ledger enabled.
### Operation ledger
```ts
const client = cana.createClient({
name: dbName,
schema: {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
{
name: 'tasks',
keyPath: 'id',
indexes: [
{ name: 'byCategory', keyPath: 'categoryId' },
{ name: 'byCompleted', keyPath: 'completed' },
{ name: 'byUpdatedAt', keyPath: 'updatedAt' }
]
}
]
},
operationLedger: true
});
await client.open();
const tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
const now = Date.now();
await scope.table('categories').put({ id: 'docs', name: 'Docs', color: '#0f766e' });
await scope.table('tasks').put({
id: 'docs-1',
title: 'Reconcile uncertain write',
categoryId: 'docs',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
});
return 'wrote';
});
const resolved = await client.resolveWrite(tx.correlationId, tx.attemptedAt);
return { outcome: tx.outcome, resolved, task: await client.table('tasks').get('docs-1') };
```const client = cana.createClient({
name: dbName,
schema: {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
{
name: 'tasks',
keyPath: 'id',
indexes: [
{ name: 'byCategory', keyPath: 'categoryId' },
{ name: 'byCompleted', keyPath: 'completed' },
{ name: 'byUpdatedAt', keyPath: 'updatedAt' }
]
}
]
},
operationLedger: true
});
await client.open();
const tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
const now = Date.now();
await scope.table('categories').put({ id: 'docs', name: 'Docs', color: '#0f766e' });
await scope.table('tasks').put({
id: 'docs-1',
title: 'Reconcile uncertain write',
categoryId: 'docs',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
});
return 'wrote';
});
const resolved = await client.resolveWrite(tx.correlationId, tx.attemptedAt);
return { outcome: tx.outcome, resolved, task: await client.table('tasks').get('docs-1') };Export
Export Category and Task stores as plain data.
### Export
```ts
const client = cana.createClient({
name: dbName,
schema: {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
{
name: 'tasks',
keyPath: 'id',
indexes: [
{ name: 'byCategory', keyPath: 'categoryId' },
{ name: 'byCompleted', keyPath: 'completed' },
{ name: 'byUpdatedAt', keyPath: 'updatedAt' }
]
}
]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
{ id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
{ id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
{
id: 'task-1',
title: 'Write the Cana tutorial',
categoryId: 'work',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
},
{
id: 'task-2',
title: 'Review category filters',
categoryId: 'home',
completed: true,
priority: 'medium',
createdAt: now,
updatedAt: now + 1
}
]);
const dump = await client.exportAll();
return dump;
```const client = cana.createClient({
name: dbName,
schema: {
version: 1,
stores: [
{ name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name', unique: true }] },
{
name: 'tasks',
keyPath: 'id',
indexes: [
{ name: 'byCategory', keyPath: 'categoryId' },
{ name: 'byCompleted', keyPath: 'completed' },
{ name: 'byUpdatedAt', keyPath: 'updatedAt' }
]
}
]
}
});
await client.open();
const now = Date.now();
await client.table('categories').bulkAdd([
{ id: 'work', name: 'Work', color: '#2563eb', createdAt: now, updatedAt: now },
{ id: 'home', name: 'Home', color: '#16a34a', createdAt: now, updatedAt: now }
]);
await client.table('tasks').bulkAdd([
{
id: 'task-1',
title: 'Write the Cana tutorial',
categoryId: 'work',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
},
{
id: 'task-2',
title: 'Review category filters',
categoryId: 'home',
completed: true,
priority: 'medium',
createdAt: now,
updatedAt: now + 1
}
]);
const dump = await client.exportAll();
return dump;Next
Continue to workers and testing.