Skip to content

Transactions and events

A primary event is one that carries an origin: { origin: "ui" } for a user action (later, "macro"). The bus opens a transaction on a primary event and keeps it open for every event that cascades from it, including async handlers.

While a transaction is open:

  • store writes are buffered — handlers see them via slice.get(), but no component re-renders yet;
  • on success everything is published in one refresh;
  • on failure everything is dropped, so the UI never shows a partial state.

Always pass { origin: "ui" } when you emit from your own UI:

onClick: () => ctx.emit("notes/document.select", { docId }, { origin: "ui" }),

Commands, menus, toolbars and shell buttons already emit with a "ui" origin.

Subscribe to the boundaries to snapshot and restore your own model:

ctx.onTransaction({
onBegin: (info) => { /* snapshot your model */ },
onPrepareCommit: async (info) => { /* phase 1: persist, throw to veto */ },
onCommit: (info) => { /* writes are published: record undo, etc. */ },
onRollback: (info, error) => { /* writes were dropped: restore */ },
});
primary event
-> onBegin may be async: the event is dispatched once all resolve
-> handlers cascades and async handlers are awaited
-> onPrepareCommit may be async (database, file…); throw/reject to veto
-> onCommit | onRollback synchronous; store published or dropped

onPrepareCommit is phase 1: flush your I/O there. If any participant rejects, the whole transaction rolls back and every participant gets onRollback, including those that already prepared — undo your I/O there. Store writes are dropped by the kernel, but a model kept outside the store (Notes keeps text and tasks in memory) must be restored by the plugin.

Transactions never nest. A primary event emitted while a transaction is open (a user click during an async save) is queued and runs in its own, fresh transaction once the current one has settled. Emitting { origin: "ui" } from a handler is therefore discouraged: it will not join the current work. kernel.transactions.waitSettled() waits for the queue too.

Notes demo: Notes → Demo: make the next change fail to save, then edit a note — the fake database refuses the change after 400 ms and the note reverts.

A handler can also fail the current transaction explicitly with tx.fail(error) from its context, or simply throw.

The kernel emits transaction/started, transaction/committed and transaction/failed; the Events panel shows the transaction id per event.

The kernel has no undo stack. A transaction is the natural atom for one, so a plugin records history in onCommit. The Notes plugin keeps one stack pair per document (title, text, tasks and precedences as they were before each committed transaction, see packages/notes-core/history.ts) and contributes its own notes-tsx.undo / notes-tsx.redo commands with mod+z / mod+shift+z, placed in its menu and in the editor toolbar. Undo and redo run in a transaction too, so onBegin ignores its own notes-tsx/document.undo and …redo events instead of recording a new restore point.

Documentation under CC BY 4.0 · © SCFlow contributors ·Back to supplychainflow.org