Skip to Content
Jumentix DocsPackages@jumentix/canaUsage guideStorage and crash recovery

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.

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.

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.

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.