Writes that stand or fall as one

Storage<Post> is the port whose every gesture is one statement, and one statement is atomic in every engine. Together is the port whose unit is a block: what the callback did happens entirely, or not at all.

export default class TransferHandler {
  constructor(private together: Together<[Account, Ledger]>) {}

  async move(from: string, to: string, amount: number) {
    return this.together.run(async ([accounts, ledger]) => {
      const debited = await accounts.findById(from);
      await accounts.update(from, { balance: debited.balance - amount });
      await ledger.create({ from, to, amount });
    });
  }
}

Resolved by type, like RepositoryOf<E>, Facade<H> and Emit<F> — the type names the subject, the container holds the realization. Nothing to register, and nothing ambient: no decorator makes an ordinary storage.create() transactional behind your back, so the block is visible where you read it.

Inside, you are in the ordinary vocabulary. accounts and ledger are the same guarded entity ports that repositories forward, and every judge still runs: a value the entity refuses is refused here too.

Two realizations, and the boot says which

The declaration names its members. Where those members live is a sources: decision, and it decides what the frame can promise:

Account+LedgerTogether — transaction, source 'db'
Account+LedgerTogether — compensated: account in 'db', ledger in 'accounting' — no isolation

One engine — the members are rebuilt over a real transaction. The engine gives the unwind and the isolation, and it costs nothing.

Several engines — a transaction reaches one engine, so there is none. The frame keeps the before-image of every write and replays the inverses in reverse order. The port has a fixed set of gestures, which is what makes an inverse derivable rather than declared:

writeits inverse
create(x)delete(x.id) — the row is handed back, so it costs nothing
update(id, patch)update(id, before) — one read first
delete(id)create(row) — one read first
upsertAll(page)restore what was there, delete what was not — one read for the page

This is why it is not a saga: a saga asks its author for an undo per step, because its steps are arbitrary code. These are not.

All-or-nothing holds either way. Isolation does not. Under a transaction, nobody outside can see the block half-done. Compensated, they can — between two writes a reader sees the half, and the unwind puts it back afterwards.The boot states which realization it built rather than letting you assume the stronger one.
A fact announced inside a frame is not covered.Emit<T> resolves outside the block, so an announcement leaves the moment it is made — including from a frame that then unwinds. Subscribers will have acted on something that did not happen. Announce afterrun() returns, where the block has already stood.

Covering what a collaborator writes

A member list of entities covers what your block writes. A second list names providers — a service, a repository, a mirror — rebuilt inside the frame so that what they write is covered too:

constructor(private together: Together<[RateCard, Ledger], [RateMirror]>) {}

await this.together.run(async ([rates, ledger], [mirror]) => {
  await mirror.refresh();          // its pages are under the same unwind
  await ledger.create({});
});

RateMirror writes through Storage<RateCard>, so it receives the framed storage through its ordinary constructor. Not one line of Mirror knows a frame exists.

Two lists rather than one, because in a signature an entity and a class are both written as a name and their types do not separate them. They are two different facts anyway: what the unwind covers, and what is rebuilt to make that true.

What a frame refuses

At boot, so you never discover it at the first call:

a member whose frond is declared in remotes:it registers no storage here — nothing to record, nothing to undo
a name that resolves to nothinga typo, whose only other symptom is a frame quietly one member short
a provider writing an entity absent from the first listits writes would escape the unwind

Two more at the call:

A frame opened inside another. On one engine the second transaction waits for the first, so the call hangs — measured, five seconds and no answer; split across engines the same code returns. Two frames that must both hold are one frame, and its member list is the union.

A fact announced from inside a frame. Announcing is dispatch: every subscriber has been handed it and the carrier has already put it on the wire, while the writes can still be taken back. Announce after run() returns, which is when it is true. Deferring to the commit was the other option and it was refused — it would make Emit<T> behave differently depending on where it is called, and the window it opens between commit and announcement has no remedy short of an outbox.

And in a compensated frame only: upsert on an entity that declares a unique constraint besides its key. The inverse of an upsert is derivable exactly when the conflict is the key; MySQL's ON DUPLICATE KEY UPDATE fires on any unique constraint, so the question is asked of your declaration and never of the engine.

Where the gradient stops

Declaring Together<[Account, Ledger]> says these two may not be separated by a change of topology. That is a real cost, and it is the point: it is the one place where moving a boundary is refused out loud instead of quietly weakening a guarantee.

When two things genuinely live apart and must both end up written, that is not a frame — it is a fact: commit one half, announce, and let the subscriber commit the other. You then own the compensation, and the framework does not pretend otherwise.

Next: Repositories.

Built with Fougere — this site runs on the framework it documents.