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/sdkTwo 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:
| Part | Role |
|---|---|
Consignments | Your logic and your state. Passed as a class, because the Machine constructs it. |
MintblueReader | Watches the project for records and dispatches them into the Machine in order. |
MintblueWriter | Turns each method call into a data output and submits it. |
MemoryRecorder | Keeps snapshots of state after each dispatch, in memory. Optional. |
emitAppEvents | Makes 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.
- The event fired. The handler printed the arguments you passed.
- The state rebuilt itself.
app.stateholds the batch after dispatch, having been empty immediately after the call. - 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
fand the argumentsargs.
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.