mintBlue
GuidesMachines

Build your first machine

Wire an application class, a reader, a writer and a recorder into a running Machine, and watch its state rebuild itself from the records it wrote.

Current SDK generation. This page documents the Machine that @mintblue/sdk ships today. Everything on it was checked against version 9.6.0. Generations sets out which document applies to the code you installed.

A Machine takes an ordinary class, records every method call anyone makes on it, and rebuilds the class's state by replaying those records. You write the application; the Machine turns each call into a published record and each published record back into a state change.

Machines explains why that is worth doing. This guide builds one.

Before you start

An SDK access token, a project id and the package, exactly as in the Quickstart.

npm install @mintblue/sdk

Two files: consignments.js for the application logic, and main.js for the wiring.

Step 1: Write the application

An application is a class with a state property and methods that change it. There is no base class to extend. App in @mintblue/sdk is a type, not a runtime value, so a class that tries to extend App fails at run time; in JavaScript you write a plain class, and in TypeScript you declare it implements App.

Every method that changes state takes a context object as its first argument. The Machine supplies it. Methods that only compute do not need it, and private helpers do not take it at all.

class Consignments {
  state = {
    batches: [],
  };

  recordArrival(_ctx, batchId, location) {
    this.state.batches.push({ batchId, location });
  }
}

module.exports = { Consignments };

In TypeScript the same class carries two types from the package:

import type { App, Context } from '@mintblue/sdk';

export class Consignments implements App {
  state = { batches: [] as { batchId: string; location: string }[] };

  recordArrival(_ctx: Context, batchId: string, location: string) {
    this.state.batches.push({ batchId, location });
  }
}

Step 2: Wire the Machine

A Machine takes four things: the application class itself rather than an instance, a reader, a writer, and an optional recorder.

const {
  Machine,
  MemoryRecorder,
  MintblueReader,
  MintblueWriter,
} = require('@mintblue/sdk');
const { Consignments } = require('./consignments.js');

async function main() {
  const sdkToken = process.env.MINTBLUE_SDK_TOKEN;
  const projectId = process.env.MINTBLUE_PROJECT_ID;

  const machine = new Machine(
    Consignments,
    await MintblueReader.create(sdkToken, projectId),
    await MintblueWriter.create(sdkToken, projectId),
    new MemoryRecorder(),
    { emitAppEvents: true },
  );

  const app = machine.getInstance();
}

main();

What each part is for:

PartRole
ConsignmentsYour logic and your state. Passed as a class, because the Machine constructs it.
MintblueReaderWatches the project for records and dispatches them into the Machine in order.
MintblueWriterTurns each method call into a data output and submits it.
MemoryRecorderKeeps snapshots of state after each dispatch, in memory. Optional.
emitAppEventsMakes the Machine emit an event named after each application method, so you can subscribe to recordArrival by name.

Step 3: Understand what getInstance returns

machine.getInstance() returns a proxy of your application, not your application. Calling a method on the proxy does not run your method. It writes the method name and its arguments as a record, through the writer, and returns.

Your method runs later, when the reader dispatches that record back. That is why every call on the proxy is asynchronous, and why the context argument you declared first is not one you pass.

await app.recordArrival('batch-1234', 'Rotterdam');

console.log(app.state);

The state logged immediately after that call is still empty. Nothing has been dispatched yet.

Step 4: Start the reader

machine.start() tells the reader to begin dispatching. As each record arrives, your method runs, the state changes, and the Machine emits an event named after the method.

machine.on('recordArrival', (snapshot) => {
  console.log('arrival recorded', snapshot.action.args);
  console.log(app.state);
});

machine.start();

Now the state fills in:

{
  "batches": [{ "batchId": "batch-1234", "location": "Rotterdam" }]
}

start() opens a live subscription. If you would rather pull on your own schedule, machine.fetch() fetches what has arrived since the last dispatch and returns. Use one or the other, not both in the same process.

Check it worked

Three checks, in increasing strength.

  1. The event fired. The handler printed the arguments you passed.
  2. The state rebuilt itself. app.state holds the batch after dispatch, having been empty immediately after the call.
  3. The record is really published. Open the project in the console, or read it back with the SDK. The writer submits a data output whose value carries the method name f and the arguments args.
const { Mintblue } = require('@mintblue/sdk');

const client = await Mintblue.create({ token: process.env.MINTBLUE_SDK_TOKEN });
const transactions = await client.listTransactions({
  project_id: process.env.MINTBLUE_PROJECT_ID,
  order: 'desc',
  limit: 5,
});

for (const summary of transactions) {
  const full = await client.getTransaction({ txid: summary.txid, parse: true });
  const record = full.outputs.find((output) => output.type === 'data');
  if (record) console.log(record.value);
}

The third check is the one that shows the property the whole design rests on: the state you are looking at was derived from published records, so anyone holding those records and the keys they are entitled to can derive it too.

Next

On this page