feat(@embark/core): Auto generate EmbarkJS events

For every provider registered and set with EmbarkJS, auto generate events to inform other modules of the state of the provider.

Any provider that belongs to EmbarkJS (currently Blockchain, Names, Messages, and Storage) will have both a `runcode:<provider name>:providerRegistered` and a `runcode:<provider name>:providerSet` event created automatically when the `CodeRunner` is instantiated.

Once the `registerProvider` code is run through the VM, ie `EmbarkJS.Blockchain.registerProvider(…)`, the corresponding event will be fired, ie `runcode:blockchain:providerRegistered`.

Likewise, once the `setProvider` code is run through the VM, ie `EmbarkJS.Blockchain.setProvider(…)`, the corresponding event will be fired, ie `runcode:blockchain:providerSet`.

Additional updates/fixes with this PR:
* Move `CodeRunner` to TypeScript
* Fix console errors with ENS and Storage due to a recent PR that waits for `code-generator:ready`. The solution here was to ensure that `code-generator:ready` is requested, so that premature events can be handled.
This commit is contained in:
emizzle 2019-02-21 19:16:39 +11:00 committed by Eric Mastro
parent 58ea3d9c30
commit d378ccf150
10 changed files with 221 additions and 156 deletions

View File

@ -8,24 +8,26 @@ export interface Events {
setCommandHandler(name: string, callback: (options: any, cb: () => void) => void): void;
}
export interface Config {
contractsFiles: any[];
embarkConfig: {
contracts: string[] | string;
config: {
contracts: string;
};
versions: {
solc: string;
}
};
reloadConfig(): void;
}
export interface Embark {
events: Events;
registerAPICall: any;
registerConsoleCommand: any;
logger: Logger;
fs: any;
config: {
contractsFiles: any[];
embarkConfig: {
contracts: string[] | string;
config: {
contracts: string;
};
versions: {
solc: string;
}
};
reloadConfig(): void;
};
config: Config;
registerActionForEvent(name: string, action: (callback: () => void) => void): void;
}

View File

@ -1,4 +1,4 @@
export interface Logger {
info(text: string): void;
error(text: string): void;
error(text: string, ...args: Array<string|Error>): void;
}

View File

@ -1,131 +0,0 @@
const VM = require('./vm');
const fs = require('../../core/fs');
const EmbarkJS = require('embarkjs');
const Web3 = require('web3');
class CodeRunner {
constructor(embark, options) {
this.ready = false;
this.blockchainConnected = false;
this.config = embark.config;
this.plugins = embark.plugins;
this.logger = embark.logger;
this.events = embark.events;
this.ipc = options.ipc;
this.vm = new VM({
sandbox: {
EmbarkJS,
Web3
},
require: {
mock: {
fs: {
access: fs.access,
diagramPath: fs.diagramPath,
dappPath: fs.dappPath,
embarkPath: fs.embarkPath,
existsSync: fs.existsSync,
ipcPath: fs.ipcPath,
pkgPath: fs.pkgPath,
readFile: fs.readFile,
readFileSync: fs.readFileSync,
readJSONSync: fs.readJSONSync,
readdir: fs.readdir,
readdirSync: fs.readdirSync,
stat: fs.stat,
statSync: fs.statSync,
tmpDir: fs.tmpDir
}
}
}
}, this.logger);
this.embark = embark;
this.commands = [];
this.registerEvents();
this.registerCommands();
this.events.emit('runcode:ready');
this.ready = true;
}
registerEvents() {
this.events.on("runcode:register", this.registerVar.bind(this));
this.events.on("runcode:init-console-code:updated", (code, cb) => {
this.evalCode(code, (err, _result) => {
if(err) {
this.logger.error("Error running init console code: ", err.message || err);
}
else if(code.includes("EmbarkJS.Blockchain.setProvider")) {
this.events.emit('runcode:blockchain:connected');
this.blockchainConnected = true;
}
cb();
});
});
this.events.on("runcode:embarkjs-code:updated", (code, cb) => {
this.evalCode(code, (err, _result) => {
if(err) {
this.logger.error("Error running embarkjs code: ", err.message || err);
}
cb();
});
});
}
registerCommands() {
this.events.setCommandHandler('runcode:getContext', (cb) => {
cb(this.vm.options.sandbox);
});
this.events.setCommandHandler('runcode:eval', this.evalCode.bind(this));
this.events.setCommandHandler('runcode:ready', (cb) => {
if (this.ready) {
return cb();
}
this.events.once("runcode:ready", cb);
});
this.events.setCommandHandler('runcode:blockchain:connected', (cb) => {
if (this.blockchainConnected) {
return cb();
}
this.events.once("runcode:blockchain:connected", cb);
});
this.events.setCommandHandler('runcode:embarkjs:reset', this.resetEmbarkJS.bind(this));
}
resetEmbarkJS(cb) {
this.events.request("code-generator:embarkjs:provider-code", (code) => {
this.evalCode(code, (err) => {
if (err) {
return cb(err);
}
this.events.request("code-generator:embarkjs:init-provider-code", (providerCode) => {
this.evalCode(providerCode, (err, _result) => {
cb(err);
}, true);
});
}, true);
});
}
registerVar(varName, code, cb = () => {}) {
this.vm.registerVar(varName, code, cb);
}
evalCode(code, cb, tolerateError = false) {
cb = cb || function () {};
if (!code) return cb(null, '');
this.vm.doEval(code, tolerateError, (err, result) => {
if (err) {
return cb(err);
}
cb(null, result);
});
}
}
module.exports = CodeRunner;

View File

@ -0,0 +1,179 @@
import VM from "./vm";
const fs = require("../../core/fs");
import { Callback, Embark, Events, Logger } from "embark";
import Web3 from "web3";
const EmbarkJS = require("embarkjs");
export enum ProviderEventType {
ProviderRegistered = "providerRegistered",
ProviderSet = "providerSet",
}
class CodeRunner {
private ready: boolean = false;
private blockchainConnected: boolean = false;
private logger: Logger;
private events: Events;
private vm: VM;
private providerStates: { [key: string]: boolean } = {};
constructor(embark: Embark, _options: any) {
this.logger = embark.logger;
this.events = embark.events;
this.vm = new VM({
require: {
mock: {
fs: {
access: fs.access,
dappPath: fs.dappPath,
diagramPath: fs.diagramPath,
embarkPath: fs.embarkPath,
existsSync: fs.existsSync,
ipcPath: fs.ipcPath,
pkgPath: fs.pkgPath,
readFile: fs.readFile,
readFileSync: fs.readFileSync,
readJSONSync: fs.readJSONSync,
readdir: fs.readdir,
readdirSync: fs.readdirSync,
stat: fs.stat,
statSync: fs.statSync,
tmpDir: fs.tmpDir,
},
},
},
sandbox: {
EmbarkJS,
Web3,
},
}, this.logger);
this.registerEvents();
this.registerCommands();
this.events.emit("runcode:ready");
this.ready = true;
}
private generateListener(provider: string, eventType: ProviderEventType) {
const providerStateName = `${provider}:${eventType}`;
const eventName = `runcode:${providerStateName}`;
this.providerStates[`${provider}:${eventType}`] = false;
this.events.setCommandHandler(eventName, (cb) => {
if (this.providerStates[providerStateName]) {
return cb();
}
this.events.once(eventName, cb);
});
}
private fireEmbarkJSEvents(code: string) {
const regexRegister = /EmbarkJS\.(.*)\.registerProvider/gm;
const regexSet = /EmbarkJS\.(.*)\.setProvider/gm;
let matches = regexRegister.exec(code);
if (matches) {
let [, provider] = matches;
provider = provider.toLowerCase();
this.providerStates[`${provider}:${ProviderEventType.ProviderRegistered}`] = true;
this.events.emit(`runcode:${provider}:${ProviderEventType.ProviderRegistered}`);
}
matches = regexSet.exec(code);
if (matches) {
let [, provider] = matches;
provider = provider.toLowerCase();
this.providerStates[`${provider}:${ProviderEventType.ProviderSet}`] = true;
this.events.emit(`runcode:${provider}:${ProviderEventType.ProviderSet}`);
}
}
private registerEvents() {
this.events.on("runcode:register", this.registerVar.bind(this));
this.events.on("runcode:init-console-code:updated", (code: string, cb: Callback<null>) => {
this.evalCode(code, (err, _result) => {
if (err) {
this.logger.error("Error running init console code: ", err.message || err);
}
this.fireEmbarkJSEvents(code);
cb();
});
});
this.events.on("runcode:embarkjs-code:updated", (code: string, cb: Callback<any>) => {
this.evalCode(code, (err, _result) => {
if (err) {
this.logger.error("Error running embarkjs code: ", err.message || err);
}
this.fireEmbarkJSEvents(code);
cb();
});
});
}
private registerCommands() {
this.events.setCommandHandler("runcode:getContext", (cb) => {
cb(this.vm.options.sandbox);
});
this.events.setCommandHandler("runcode:eval", this.evalCode.bind(this));
this.events.setCommandHandler("runcode:ready", (cb) => {
if (this.ready) {
return cb();
}
this.events.once("runcode:ready", cb);
});
this.events.setCommandHandler("runcode:embarkjs:reset", this.resetEmbarkJS.bind(this));
// register listeners for when EmbarkJS runs registerProvider through the console.
// For example, when `EmbarkJS.Storage.registerProvider(...)` is run through the console,
// emit the `runcode:storage:providerRegistered` event, and fire any requests attached to it
Object.keys(EmbarkJS)
.filter((propName) => EmbarkJS[propName].hasOwnProperty("registerProvider"))
.forEach((providerName) => {
this.generateListener(providerName.toLowerCase(), ProviderEventType.ProviderRegistered);
});
// register listeners for when EmbarkJS runs setProvider through the console.
// For example, when `EmbarkJS.Storage.setProvider(...)` is run through the console,
// emit the `runcode:storage:providerSet` event, and fire any requests attached to it
Object.keys(EmbarkJS)
.filter((propName) => EmbarkJS[propName].hasOwnProperty("setProvider"))
.forEach((providerName) => {
this.generateListener(providerName.toLowerCase(), ProviderEventType.ProviderSet);
});
}
private resetEmbarkJS(cb: Callback<null>) {
this.events.request("code-generator:embarkjs:provider-code", (code: string) => {
this.evalCode(code, (err) => {
if (err) {
return cb(err);
}
this.events.request("code-generator:embarkjs:init-provider-code", (providerCode: string) => {
this.evalCode(providerCode, (errInitProvider, _result) => {
cb(errInitProvider);
}, true);
});
}, true);
});
}
private registerVar(varName: string, code: any, cb = () => { }) {
this.vm.registerVar(varName, code, cb);
}
private evalCode(code: string, cb: Callback < any >, tolerateError = false) {
cb = cb || (() => { });
if (!code) {
return cb(null, "");
}
this.vm.doEval(code, tolerateError, (err, result) => {
if (err) {
return cb(err);
}
cb(null, result);
});
}
}
module.exports = CodeRunner;

View File

@ -31,7 +31,7 @@ class VM {
* Currently, all of the allowed external requires appear in the EmbarkJS scripts. If
* the requires change in any of the EmbarkJS scripts, they will need to be updated here.
*/
private options: NodeVMOptions = {
private _options: NodeVMOptions = {
require: {
builtin: ["path", "rxjs", "util"],
external: [
@ -54,11 +54,15 @@ class VM {
* @param {Logger} logger Logger.
*/
constructor(options: NodeVMOptions, private logger: Logger) {
this.options = recursiveMerge(this.options, options);
this._options = recursiveMerge(this._options, options);
this.setupNodeVm(() => { });
}
public get options(): NodeVMOptions {
return this._options;
}
/**
* Transforms a snippet of code such that the last expression is returned,
* so long as it is not an assignment.
@ -128,7 +132,7 @@ class VM {
}
this.updateState(() => {
this.options.sandbox[varName] = code;
this._options.sandbox[varName] = code;
this.setupNodeVm(cb);
});
}
@ -137,10 +141,10 @@ class VM {
if (!this.vm) { return cb(); }
// update sandbox state from VM
each(Object.keys(this.options.sandbox), (sandboxVar: string, next: Callback<null>) => {
each(Object.keys(this._options.sandbox), (sandboxVar: string, next: Callback<null>) => {
this.doEval(sandboxVar, false, (err?: Error | null, result?: any) => {
if (!err) {
this.options.sandbox[sandboxVar] = result;
this._options.sandbox[sandboxVar] = result;
}
next(err);
});
@ -153,9 +157,9 @@ class VM {
* by calling @function {registerVar}.
*/
private setupNodeVm(cb: Callback<null>) {
this.vm = new NodeVM(this.options);
this.vm = new NodeVM(this._options);
cb();
}
}
module.exports = VM;
export default VM;

View File

@ -17,6 +17,7 @@ const Templates = {
class CodeGenerator {
constructor(embark, options) {
this.ready = false;
this.blockchainConfig = embark.config.blockchainConfig || {};
this.embarkConfig = embark.config.embarkConfig;
this.fs = embark.fs;
@ -34,6 +35,7 @@ class CodeGenerator {
this.events = embark.events;
this.listenToCommands();
this.ready = true;
this.events.emit('code-generator:ready');
}
@ -87,6 +89,13 @@ class CodeGenerator {
this.events.setCommandHandler("code-generator:embarkjs:build", (cb) => {
this.buildEmbarkJS(cb);
});
this.events.setCommandHandler('code-generator:ready', (cb) => {
if (this.ready) {
return cb();
}
this.events.once('code-generator:ready', cb);
});
}
generateContracts(contractsList, useEmbarkJS, isDeployment, useLoader) {

View File

@ -216,7 +216,7 @@ class Console {
if (this.isEmbarkConsole) {
return next();
}
this.events.request("runcode:blockchain:connected", next);
this.events.request("runcode:blockchain:providerSet", next);
},
(next: any) => {
if (this.isEmbarkConsole) {

View File

@ -372,7 +372,7 @@ class ENS {
return this.logger.error(err.message || err);
}
this.events.once('code-generator:ready', () => {
this.events.request('code-generator:ready', () => {
this.events.request('code-generator:symlink:generate', location, 'eth-ens-namehash', (err, symlinkDest) => {
if (err) {
this.logger.error(__('Error creating a symlink to eth-ens-namehash'));

View File

@ -34,7 +34,9 @@ class Storage {
};
this.embark.addProviderInit('storage', code, shouldInit);
this.embark.addConsoleProviderInit('storage', code, shouldInit);
this.embark.events.request("runcode:storage:providerRegistered", () => {
this.embark.addConsoleProviderInit('storage', code, shouldInit);
});
}
}

View File

@ -1,6 +1,6 @@
/*globals describe, it*/
const TestLogger = require('../lib/utils/test_logger');
const VM = require('../lib/modules/codeRunner/vm');
const VM = require('../lib/modules/codeRunner/vm').default;
const {expect} = require('chai');
describe('embark.vm', function () {