Transações e eventos de mudança
Transações são a fronteira de commit. Eventos de mudança são emitidos somente depois que a transação IndexedDB completa, então subscribers nunca reagem a dados que depois sofrem rollback.
Transação multi-store
type Category = {
id: string;
name: string;
color: string;
createdAt: number;
updatedAt: number;
};
type Task = {
id: string;
title: string;
categoryId: string;
completed: boolean;
priority: 'low' | 'medium' | 'high';
createdAt: number;
updatedAt: number;
};
const result = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
const now = Date.now();
await scope.table<Category>('categories').put({
id: 'ops',
name: 'Operations',
color: '#f97316',
createdAt: now,
updatedAt: now
});
await scope.table<Task>('tasks').put({
id: 'ops-1',
title: 'Create the operations category',
categoryId: 'ops',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
});
return { categoryId: 'ops', taskId: 'ops-1' };
});
console.log({
outcome: result.outcome,
result: result.result,
events: result.events
});Não espere rede, timers ou trabalho de UI dentro do corpo da transação. A
transação IndexedDB faz auto-commit quando o event loop cede sem uma requisição
IndexedDB pendente. O Cana reporta isso como TransactionInactive.
Ouvir eventos commitados
import type { CanaChangeEvent } from '@jumentix/cana';
const events: Array<Pick<CanaChangeEvent, 'cursor' | 'type' | 'store' | 'key'>> = [];
const stop = client.subscribe((event) => {
events.push({
cursor: event.cursor,
type: event.type,
store: event.store,
key: event.key
});
});
await client.table<Task>('tasks').update('ops-1', {
completed: true,
updatedAt: Date.now()
});
stop();
console.log(events);Replay depois que a UI ficou ausente
import { isCanaErrorCode, type CanaChangeEvent } from '@jumentix/cana';
let lastCursor = Number(localStorage.getItem('tasks:lastCursor') ?? 0);
function applyEventToUiStore(event: CanaChangeEvent) {
lastCursor = event.cursor;
localStorage.setItem('tasks:lastCursor', String(lastCursor));
console.log(`${event.cursor}: ${event.type} ${event.store}/${String(event.key)}`);
}
try {
const stop = client.subscribe(applyEventToUiStore, { sinceCursor: lastCursor });
window.addEventListener('beforeunload', () => stop(), { once: true });
} catch (error) {
if (isCanaErrorCode(error, 'NotFound')) {
const categories = await client.table<Category>('categories').query({ index: 'byName' });
const tasks = await client.table<Task>('tasks').query({ index: 'byUpdatedAt' });
console.log({ reloadRequired: true, categories, tasks });
} else {
throw error;
}
}Replay é limitado por retainedEvents. Quando o cursor é antigo demais, o Cana
se recusa a fingir que o replay está completo. Recarregue a tabela e assine
novamente.
Execute aqui
Transacoes
Crie uma Category e sua primeira Task em um unico commit.
### Transacoes
```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 tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
const now = Date.now();
await scope.table('categories').put({
id: 'ops',
name: 'Operations',
color: '#f97316',
createdAt: now,
updatedAt: now
});
await scope.table('tasks').put({
id: 'ops-1',
title: 'Created with the category',
categoryId: 'ops',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
});
return 'ok';
});
return {
outcome: tx.outcome,
result: tx.result,
categories: await client.table('categories').query(),
tasks: await client.table('tasks').query()
};
```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 tx = await client.transaction('readwrite', ['categories', 'tasks'], async (scope) => {
const now = Date.now();
await scope.table('categories').put({
id: 'ops',
name: 'Operations',
color: '#f97316',
createdAt: now,
updatedAt: now
});
await scope.table('tasks').put({
id: 'ops-1',
title: 'Created with the category',
categoryId: 'ops',
completed: false,
priority: 'high',
createdAt: now,
updatedAt: now
});
return 'ok';
});
return {
outcome: tx.outcome,
result: tx.result,
categories: await client.table('categories').query(),
tasks: await client.table('tasks').query()
};Eventos de mudanca
Assine e colete eventos confirmados de Task.
### Eventos de mudanca
```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 seen = [];
const stop = client.subscribe((event) => {
seen.push({ cursor: event.cursor, type: event.type, store: event.store, key: event.key });
});
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
id: 'docs-1',
title: 'Listen to Cana events',
categoryId: 'docs',
completed: false,
priority: 'medium',
createdAt: 1,
updatedAt: 1
});
await client.table('tasks').update('docs-1', { completed: true, updatedAt: 2 });
stop();
return { events: seen };
```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 seen = [];
const stop = client.subscribe((event) => {
seen.push({ cursor: event.cursor, type: event.type, store: event.store, key: event.key });
});
await client.table('categories').add({ id: 'docs', name: 'Docs', color: '#0f766e' });
await client.table('tasks').add({
id: 'docs-1',
title: 'Listen to Cana events',
categoryId: 'docs',
completed: false,
priority: 'medium',
createdAt: 1,
updatedAt: 1
});
await client.table('tasks').update('docs-1', { completed: true, updatedAt: 2 });
stop();
return { events: seen };Próximo
Continue em hooks e erros.