Des écritures qui tiennent ou tombent ensemble

Storage<Post> est le port dont chaque geste est une instruction, et une instruction est atomique sur tous les moteurs. Together est le port dont l'unité est un bloc : ce que la fonction a fait a lieu entièrement, ou pas du tout.

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 debite = await accounts.findById(from);
      await accounts.update(from, { balance: debite.balance - amount });
      await ledger.create({ from, to, amount });
    });
  }
}

Résolu par type, comme RepositoryOf<E>, Facade<H> et Emit<F> — le type nomme le sujet, le conteneur tient la réalisation. Rien à enregistrer, et rien d'ambiant : aucun décorateur ne rend un storage.create() ordinaire transactionnel dans votre dos, donc le bloc est visible là où on le lit.

À l'intérieur, on est dans le vocabulaire habituel. accounts et ledger sont les mêmes ports d'entité gardés que les repositories transmettent, et tous les juges tournent encore : une valeur que l'entité refuse est refusée là aussi.

Deux réalisations, et le démarrage dit laquelle

La déclaration nomme ses membres. Où ces membres vivent est une décision de sources:, et c'est elle qui décide de ce que le cadre peut promettre :

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

Un seul moteur — les membres sont reconstruits sur une vraie transaction. Le moteur donne le retour arrière et l'isolation, et ça ne coûte rien.

Plusieurs moteurs — une transaction atteint un moteur, donc il n'y en a pas. Le cadre garde l'image d'avant de chaque écriture et rejoue les inverses en ordre inverse. Le port a un jeu de gestes fixe, et c'est ce qui rend l'inverse dérivable plutôt que déclaré :

écritureson inverse
create(x)delete(x.id) — la ligne est rendue, donc ça ne coûte rien
update(id, patch)update(id, avant) — une lecture d'abord
delete(id)create(ligne) — une lecture d'abord
upsertAll(page)restaurer ce qui était là, supprimer le reste — une lecture pour la page

C'est pour ça que ce n'est pas une saga : une saga demande à son auteur un undo par étape, parce que ses étapes sont du code quelconque. Ici, non.

Le tout-ou-rien tient des deux côtés. L'isolation, non. Sous transaction, personne à l'extérieur ne peut voir le bloc à moitié fait. En compensé, si — entre deux écritures un lecteur voit la moitié, et le retour arrière la remet ensuite.Le démarrage énonce la réalisation qu'il a construite plutôt que de vous laisser supposer la plus forte.
Un fact annoncé dans un frame n'est pas couvert.Emit<T> se résout hors du bloc, donc une annonce part au moment où elle est faite — y compris depuis un frame qui se déroule ensuite. Les abonnés auront agi sur quelque chose qui n'a pas eu lieu. Annoncez après le retour de run(), quand le bloc a déjà tenu.

Couvrir ce qu'écrit un collaborateur

Une liste d'entités couvre ce que votre bloc écrit. Une seconde liste nomme des fournisseurs — un service, un repository, un miroir — reconstruits dans le cadre pour que ce qu'eux écrivent soit couvert aussi :

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

await this.together.run(async ([rates, ledger], [mirror]) => {
  await mirror.refresh();          // ses pages sont sous le même retour arrière
  await ledger.create({});
});

RateMirror écrit par Storage<RateCard>, donc il reçoit le storage du cadre par son constructeur ordinaire. Pas une ligne de Mirror ne sait qu'un cadre existe.

Deux listes plutôt qu'une, parce que dans une signature une entité et une classe s'écrivent toutes deux comme un nom et que leurs types ne les séparent pas. Ce sont deux faits différents de toute façon : ce que le retour arrière couvre, et ce qui est reconstruit pour que ce soit vrai.

Ce qu'un cadre refuse

Au démarrage, pour que vous ne le découvriez jamais au premier appel :

un membre dont le frond est déclaré dans remotes:il n'enregistre aucun stockage ici — rien à consigner, rien à défaire
un nom qui ne désigne rienune faute de frappe, dont le seul autre symptôme est un cadre discrètement amputé d'un membre
un fournisseur qui écrit une entité absente de la première listeses écritures échapperaient au retour arrière

Deux autres à l'appel :

Un cadre ouvert dans un autre. Sur un seul moteur, la seconde transaction attend la première et l'appel se bloque — mesuré, cinq secondes sans réponse ; répartis sur deux moteurs, le même code répond. Deux cadres qui doivent tous deux tenir n'en font qu'un, et sa liste de membres est l'union.

Un fait annoncé depuis l'intérieur. Annoncer, c'est distribuer : chaque abonné l'a reçu et le transporteur l'a déjà mis sur le fil, alors que les écritures peuvent encore être reprises. Annoncer après le retour de run(), qui est le moment où c'est vrai. Différer jusqu'au commit était l'autre option, écartée : Emit<T> se comporterait autrement selon l'endroit d'où on l'appelle, et la fenêtre ouverte entre le commit et l'annonce n'a d'autre remède qu'une table d'attente.

Et dans un cadre compensé seulement : upsert sur une entité qui déclare une contrainte unique en plus de sa clé. L'inverse d'un upsert est dérivable exactement quand le conflit porte sur la clé ; le ON DUPLICATE KEY UPDATE de MySQL se déclenche sur n'importe quelle contrainte unique, donc la question se pose à votre déclaration et jamais au moteur.

Là où le gradient s'arrête

Déclarer Together<[Account, Ledger]>, c'est dire que ces deux-là ne peuvent pas être séparées par un changement de topologie. C'est un vrai coût, et c'est l'intérêt : c'est le seul endroit où déplacer une frontière est refusé à voix haute au lieu d'affaiblir une garantie en silence.

Quand deux choses vivent réellement à part et doivent toutes deux finir écrites, ce n'est pas un cadre — c'est un fait : valider une moitié, annoncer, et laisser l'abonné valider l'autre. La compensation vous appartient alors, et le framework ne prétend pas le contraire.

Suite : Repositories.

Construit avec Fougere — ce site tourne sur le framework qu'il documente.