es-vm
Version:
A visual machine run in ES env like Node/Browser
220 lines (179 loc) • 4.89 kB
JavaScript
'use strict';
const Emitter = require('events');
const {Statement} = require('./statement');
const RPC_LIMIT = 30;
/**
* [E]CMA[S]cript [V]irtual [M]achine
*
* A VM used to run async/sync programe by ECMAScript
* engine. It provide RPC, Statement and Scope
* to help to design a execution tree.
*
* @class {Kernel}
* @extends {Emitter}
*/
class Kernel extends Emitter {
/**
* Initialize the VM.
* @constructor
*/
constructor() {
super();
this.$runtime = null;
this.$operationStack = [];
/*eslint-disable no-console */
this.on('error', err => console.log(`[ESVM]: ${err.message}`));
/*eslint-enable no-console */
this.$haltReject = null;
this.$timeout = [];
this.$onFetchHandler = null;
}
/**
* Get current execution position.
*/
get position() {
const op = this.$operationStack[0];
if (op && op.position) {
return op.position;
}
return null;
}
/**
* Clear the operation stack.
*/
$clearOperationStack() {
this.$operationStack.length = 0;
return this;
}
/**
* Push an operation into operation stack.
*/
pushOperation(operation) {
this.$operationStack.unshift(operation);
this.emit('performing', operation);
return this;
}
/**
* Pop an operation from operation stack.
*/
popOperation() {
return this.$operationStack.shift();
}
/**
* Get a instruction to send to remote and vm will
* be blocked when waiting for the response.
* After get the response, resume the VM.
* @param {Object} invoking Method name & arguments.
* @param {Number} [limit=RPC_LIMIT] Timeout limit.
* @param {function} [errorHandler = defaultErrorHandle] Error handle function.
*/
fetch(invoking, limit = RPC_LIMIT) {
this.emit('fetch', invoking);
// Create the race condition to check if RPC timeout occurs.
const timeoutPromise = new Promise((resolve, reject) => setTimeout(() => {
reject(new Error('Fetching timeout.'));
}, limit));
const RPCPromise = Promise.race([
timeoutPromise,
Promise.resolve(this.$onFetchHandler(invoking))
]).then(ret => this.$run(null, ret), err => this.$run(err));
const externalHaltPromise = new Promise(resolve => {
this.$haltReject = resolve;
});
Promise.race([RPCPromise, externalHaltPromise]).then(() => {
this.$haltReject = null;
});
return 'VM::BLOCKED';
}
setOnFetch(handler) {
if(typeof handler !== 'function') {
throw new Error('[ESVM-DEV]: Need a funtion for vm.setOnFetch.');
}
this.$onFetchHandler = handler;
return this;
}
/**
* Load the statement into VM.
*
* @param {Statement} statement
*/
$loadProgram(statement, context = {}) {
if (!(statement instanceof Statement)) {
throw new Error('[ESVM-DEV]: Invalid statement.');
}
this.$runtime = statement.doExecution(Object.assign({
vm: this
}, context));
return this;
}
/**
* Emit program-start event and run the statement.
*/
$launch() {
if (this.$runtime === null) {
throw new Error('No program in vm. Use vm.$loadProgram before.');
}
this.emit('program-start', this);
return this.$run();
}
/**
* Execute the program step by step. If current signal has
* the interception property with true value, the process will
* suspend. If the current signal is ERROR_HALTING, VM will stop
* running and halt.
*/
$run(outerError = null, ret) {
const CONSTANT_TRUE = true;
const runtime = this.$runtime;
if (runtime === null) {
throw new Error('[LCVM-DEV]: Runtime of VM has been null.');
}
while (CONSTANT_TRUE) {
try {
//TODO other cases to intercepter runtime like breakpoint
const {done, value} =
outerError ? runtime.throw(outerError) : runtime.next(ret);
if (done) {
this.$complete(null, value);
return value;
}
if (value === 'VM::BLOCKED') {
// Use to tell test vm has been blocked successfully.
return 'suspend';
}
outerError = null;
} catch (innerError) {
this.emit('error', innerError, this.position);
this.$complete(innerError);
return null;
}
}
}
$complete(exception = null, ret) {
this.$halt();
this.emit('program-end', exception, ret, this);
}
/**
* Stop executing program and make the VM idle.
*/
$halt() {
this.$clearOperationStack();
this.$haltReject && this.$haltReject();
this.$clearAllTimeout();
this.$runtime = null;
return this;
}
$clearAllTimeout() {
while(this.$timeout.length) {
clearTimeout(this.$timeout.pop());
}
}
$setTimeout(fn, time) {
const id = setTimeout(() => {
fn();
this.$timeout.splice(this.$timeout.indexOf(id), 1);
}, time);
this.$timeout.push(id);
}
}
exports.Kernel = Kernel;