Strata · guide

Stamps, and refusing what is too new

What a stamp says, what happens at each distance from the code, and what Strata does with a document from a later release.

Install it
pnpm add @niscorp/strata @niscorp/prism zod
Step 1 of 6

A format with two changes behind it

A sequence that owns a format of document is called a grammar. It declares documents, the kinds it owns, and its migrations are document steps: at names the kind, and transform is a Prism config.

The config is handed one document as $.document and answers with the document to put in its place. The first moves one seat into a list. The second renames a key.

A format with two changes behind it
import { evaluate } from '@niscorp/prism';
import { StrataError, createUpgrader, defineSequence } from '@niscorp/strata';

const seatsFromSeat = {
  $merge: [
    { $omit: { from: { $ref: '$.document' }, keys: ['seat'] } },
    { seats: [{ $ref: '$.document.seat' }] },
  ],
};
const memberFromHolder = { $renameKeys: { from: { $ref: '$.document' }, map: { holder: 'member' } } };

const tickets = defineSequence({
  id: 'theatre.tickets',
  documents: { ticket: {} },
  migrations: [
    {
      description: 'seat is now seats, a list',
      steps: [{ kind: 'document', at: 'theatre.tickets/ticket', transform: seatsFromSeat }],
    },
    {
      description: 'holder is now member',
      steps: [{ kind: 'document', at: 'theatre.tickets/ticket', transform: memberFromHolder }],
    },
  ],
});
const upgrader = await createUpgrader([tickets], { transform: evaluate });
Step 2 of 6

The stamp to store

upgrader.stamp is what a document written now carries: each grammar, and how many migrations it has. Store it beside the document, in a column of its own.

The stamp to store
console.log('written now:', upgrader.stamp);
Step 3 of 6

Behind the code

A ticket stored before either change has a stamp of 0, and both migrations run over it. One stored between the two has a stamp of 1, and only the second runs.

Behind the code
const kind = 'theatre.tickets/ticket';
const oldest = upgrader.upgrade({ holder: 'Grace', seat: 'C4' }, { kind, stamp: { 'theatre.tickets': 0 } });
console.log(oldest.applied, oldest.document);

const older = upgrader.upgrade({ holder: 'Ada', seats: ['A1'] }, { kind, stamp: { 'theatre.tickets': 1 } });
console.log(older.applied, older.document);
Step 4 of 6

No stamp at all

A document with no stamp is read as the start of every grammar. That is how rows from before you used Strata come in: everything runs over them.

No stamp at all
const unstamped = upgrader.upgrade({ holder: 'Linus', seat: 'D2' }, { kind, stamp: null });
console.log(unstamped.applied.length, 'applied to a ticket with no stamp');
Step 5 of 6

Ahead of the code

A stamp of 3 was written by a release with a third migration, which this code does not have. Strata cannot know what that migration did, so it refuses with TOO_NEW.

This is what a server sees when it is rolled back, or when an add-on built on a newer version sends it a document. The fix is to run the newer code.

Ahead of the code
try {
  upgrader.upgrade({ member: 'Mia', seats: ['B7'] }, { kind, stamp: { 'theatre.tickets': 3 } });
} catch (error) {
  if (!(error instanceof StrataError)) throw error;
  console.log(error.code, error.details);
}
Step 6 of 6

Is this one behind

behind answers whether a stamp has anything to run, without running it. It is how to count the rows that are still old.

Is this one behind
console.log('behind at 1:', upgrader.behind({ 'theatre.tickets': 1 }));
console.log('behind at 2:', upgrader.behind({ 'theatre.tickets': 2 }));
Then

Run it.

The stamp a new ticket carries, what ran at each distance and what came out, the refusal with the two numbers it is about, and which stamps are behind.

Run it
npx tsx stamps.mts
It prints
written now: { 'theatre.tickets': 2 }
[ 'theatre.tickets/1', 'theatre.tickets/2' ] { member: 'Grace', seats: [ 'C4' ] }
[ 'theatre.tickets/2' ] { member: 'Ada', seats: [ 'A1' ] }
2 applied to a ticket with no stamp
TOO_NEW [ 'theatre.tickets: the document is at 3, this code knows 2' ]
behind at 1: true
behind at 2: false