Skip to Content
Jumentix DocsPackages@jumentix/canaUsage guideWorkers and testing

Workers and testing

Workers move persistence work away from the page thread. The boundary is message based, so every request and response must be structured-cloneable plain data.

Worker request flow

Complete same-thread worker-host demo

This playground uses MessageChannel so the docs can run the worker protocol without creating a separate Worker file. A production app would call createWorkerHost() inside the Worker module and keep createRouter() on the page thread.

Worker client flow

Drive Cana through createWorkerHost, createRouter and createWorkerClient using MessageChannel.

const channel = new MessageChannel();
channel.port1.start?.();
channel.port2.start?.();

const broadcasts = [];
const router = cana.createRouter({
  port: channel.port1,
  timeoutMs: 5000,
  onBroadcast(event) {
    broadcasts.push({
      cursor: event.cursor,
      type: event.type,
      store: event.store,
      key: event.key
    });
  }
});

const host = cana.createWorkerHost({
  port: channel.port2,
  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' }
      ]
    }
  ]
},
  originId: 'docs-worker-host',
  retainedEvents: 20,
  operationLedger: true
});

const workerClient = cana.createWorkerClient(router);

try {
  await workerClient.open();
  const now = Date.now();
  await workerClient.put('categories', {
    id: 'work',
    name: 'Work',
    color: '#2563eb',
    createdAt: now,
    updatedAt: now
  });
  await workerClient.add('tasks', {
    id: 'task-worker-1',
    title: 'Persist through the worker boundary',
    categoryId: 'work',
    completed: false,
    priority: 'medium',
    createdAt: now,
    updatedAt: now
  });
  const tasks = await workerClient.query('tasks', {
    index: 'byCategory',
    equals: 'work'
  });
  const count = await workerClient.count('tasks');
  return {
    ping: await workerClient.ping(),
    count,
    tasks,
    broadcasts
  };
} finally {
  await workerClient.close();
  router.dispose();
  await host.dispose();
  channel.port1.close();
  channel.port2.close();
}

Production file layout

src/cana.worker.ts

import { createWorkerHost, type CanaSchema } from '@jumentix/cana';

const schema: CanaSchema = {
  version: 1,
  stores: [
    { name: 'categories', keyPath: 'id', indexes: [{ name: 'byName', keyPath: 'name' }] },
    {
      name: 'tasks',
      keyPath: 'id',
      indexes: [
        { name: 'byCategory', keyPath: 'categoryId' },
        { name: 'byUpdatedAt', keyPath: 'updatedAt' }
      ]
    }
  ]
};

createWorkerHost({
  port: self,
  name: 'tasks-worker-db',
  schema,
  originId: 'tasks-worker',
  retainedEvents: 100,
  operationLedger: true
});

src/cana-client.ts

import {
  createRouter,
  createWorkerClient,
  type CanaChangeEvent
} from '@jumentix/cana';

const worker = new Worker(new URL('./cana.worker.ts', import.meta.url), {
  type: 'module'
});

const events: CanaChangeEvent[] = [];
const router = createRouter({
  port: worker,
  timeoutMs: 15_000,
  onBroadcast(event) {
    events.push(event as CanaChangeEvent);
  }
});

export const canaWorker = createWorkerClient(router);
export const canaWorkerEvents = events;

export async function stopCanaWorker() {
  await canaWorker.close();
  router.dispose();
  worker.terminate();
}

src/tasks.ts

import { canaWorker } from './cana-client';

export async function createTaskInWorker() {
  await canaWorker.open();
  await canaWorker.put('categories', {
    id: 'work',
    name: 'Work',
    color: '#2563eb',
    createdAt: Date.now(),
    updatedAt: Date.now()
  });
  await canaWorker.add('tasks', {
    id: crypto.randomUUID(),
    title: 'Persist through the worker',
    categoryId: 'work',
    completed: false,
    priority: 'medium',
    createdAt: Date.now(),
    updatedAt: Date.now()
  });
  return canaWorker.query('tasks', { index: 'byCategory', equals: 'work' });
}

Testing strategy

LayerWhat to testTooling
UnitSchema builders, record mappers, event reducers.Bun/Jest without a browser.
Browser componentReact/Vue components update from CanaChangeEvent.Testing Library with injected events.
IndexedDB integrationOpen, upgrade, query, transaction, storage assessment, export/import.Cypress or browser automation.
Worker integrationRouter timeout, broadcasts, structured-clone failures.Real Worker in a browser test.
PerformanceQuery ratios, count vs full read, deep pagination.Browser performance suite.

Next

Continue to the API reference.