UNPKG

es-vm

Version:

A visual machine run in ES env like Node/Browser

220 lines (179 loc) 4.89 kB
'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;