Runtime
To understand the internals, we need to understand what the sequence of events are. What happens when you press ‘Build’?
Compile
Your code, is sent to the web-worker running in background. It uses the typescript compiler and our transformers to convert your code into something that can be played and paused.
Feel free to explore this in our source code!
Glue
The runtime doesn’t get bundled and compiled with your code. It exists separately, the compiled code simply calls to it. And to be able to do that, the compiled code is glued together with the runtime.
Checkout this code in our repo for more clarity.
The Instruction Set
Our compiler converts your code into something that can emit our instructions, or to be precise, can yield
an instruction.
Sounds fancy, except our instructions are really simple:
export enum MethodRuntimeCommands {
LOAD = 'load',
LOG = 'log',
UNLOAD = 'unload',
HALT = 'halt',
NO_OP = 'no_op',
AWAIT_FLOW = 'await_flow',
}
State machines
Here’s a more precise statement:
Our compiler converts your code into state machines, where each instruction represents a state.
And in turn, the runtime manages all these state machines and their transitions.
Take this code for example:
class Main {
/**
* Is reponsible for many important things.
*/
hello() {
const result = this.world('Hello');
std.log(result);
}
/**
* Does all the heavy lifting!
*/
world(arg: string) {
return `${arg} World!`
}
}
Here’s a simplistic view of what it looks like after transformation:
Tick
We understand that our code gets boiled down to simple state machines,
and the runtime is responsible for managing state transitions.
But what does ‘managing’ mean?
The runtime maintains a number internally, called currentTick
. It also has a function called tick()
whose job is to get all the available state machines,
and make them transition, together.
When the combined transition of all the state machines is complete, currentTick
is incremented. Here’s the code if you want to check it out.
Time stands still
Unless a given state machine is transitioned, it’s just stuck in time, stuck in that state, preserving all its context. And the only way to change that is if you press the Play or the Next button.
Which means the only unit of time that matters is the tick
.
A funtion was loaded at tick=1
, logged something at tick=2
and returned a number at tick=3
.
For how long did this function run? 3 ticks.
Doesn’t matter if the user spent 10 seconds reading the log and 10 more seconds just keeping the playground paused, external time doesn’t affect the runtime.
Conclusion
We have established that tick
is the only unit of time in metz, and it moves forward only when you ask it to.
You can have flows, that begin after certain ticks are passed, or flows that run every other tick.
You can also sleep for certain number of ticks.
This gives you the ability to simulate scenarios that might be hard to do in real life. Especially the ones, that involve time.
Like what would happen if a service got hit with two requests for the same thing, at the same time.
The challenge lies in making this state possible. Without doing that, how can we even start to think about it?
Metz makes this a breeze, you are free to manipulate time however way you want. Halt a flow for a few ticks so that when it resumes, it co-incides with some other flow. Or start a flow after certain ticks. Anything you want.
That’s what made this example possible:
In the first story we see that the poller and webhook align perfectly to create a blind spot. Both start working on the same payment, not knowing that the other is doing the same. This leads to our system capturing the same payment twice!
Click here to open in a tab
Things might be squished here!
After this much lore dumping, it’s natural to question, what was it all about? So before we go further, take a break, watch some KRAZAM, this doc will still be here.
And when you are back, we will re-visit flows in Flows-102. A lot the learnings from this section will come handy!