Fix host imports in fluence-js (#997)

This commit is contained in:
folex 2020-12-08 17:13:24 +03:00 committed by GitHub
parent 84f0b3ba18
commit 8a10957efe
10 changed files with 138 additions and 54 deletions

8
package-lock.json generated
View File

@ -1,6 +1,6 @@
{
"name": "fluence",
"version": "0.7.96",
"version": "0.7.99",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
@ -25,9 +25,9 @@
}
},
"@fluencelabs/aquamarine-stepper": {
"version": "0.0.15",
"resolved": "https://registry.npmjs.org/@fluencelabs/aquamarine-stepper/-/aquamarine-stepper-0.0.15.tgz",
"integrity": "sha512-dCzBlqiR0/EiYzCseIlpE8wbi/jRMLvMsFkTIm0tmhvSfWtBskXLniM9CE4JqyUATAVuBdKJ5HKsufACugGf4A=="
"version": "0.0.16",
"resolved": "https://registry.npmjs.org/@fluencelabs/aquamarine-stepper/-/aquamarine-stepper-0.0.16.tgz",
"integrity": "sha512-yrRMH2ysrxkhOGUe599urPopx6bon44qHppyvE6RKdrE1qVgxYKONWU+BNM+ouzIZ3UW5hoT0gQZ7lmI3HS30g=="
},
"@sinonjs/commons": {
"version": "1.7.2",

View File

@ -1,6 +1,6 @@
{
"name": "fluence",
"version": "0.7.97",
"version": "0.7.100",
"description": "the browser js-libp2p client for the Fluence network",
"main": "./dist/fluence.js",
"typings": "./dist/fluence.d.ts",
@ -10,12 +10,13 @@
"package:build": "NODE_ENV=production webpack && npm run package",
"package": "tsc && rsync -r src/aqua/*.js dist/aqua",
"start": "webpack-dev-server -p",
"build": "webpack"
"build": "webpack --mode production"
},
"repository": "https://github.com/fluencelabs/fluence",
"author": "Fluence Labs",
"license": "Apache 2.0",
"license": "Apache-2.0",
"dependencies": {
"@fluencelabs/aquamarine-stepper": "0.0.15",
"@fluencelabs/aquamarine-stepper": "0.0.16",
"async": "3.2.0",
"base64-js": "1.3.1",
"bs58": "4.0.1",

3
src/aqua/index.d.ts vendored
View File

@ -6,9 +6,10 @@
* @param {string} aqua
* @param {string} prev_data
* @param {string} data
* @param {string} log_level
* @returns {string}
*/
export function invoke(wasm: any, init_user_id: string, aqua: string, prev_data: string, data: string): string;
export function invoke(wasm: any, init_user_id: string, aqua: string, prev_data: string, data: string, log_level: string): string;
export function ast(wasm: any, script: string): string;
export function getStringFromWasm0(wasm: any, arg1: any, arg2: any): string
export function getInt32Memory0(wasm: any): number[]

View File

@ -96,9 +96,10 @@ export function getStringFromWasm0(wasm, ptr, len) {
* @param {string} aqua
* @param {string} prev_data
* @param {string} data
* @param {string} log_level
* @returns {string}
*/
export function invoke(wasm, init_user_id, aqua, prev_data, data) {
export function invoke(wasm, init_user_id, aqua, prev_data, data, log_level) {
try {
var ptr0 = passStringToWasm0(wasm, init_user_id, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
@ -108,7 +109,9 @@ export function invoke(wasm, init_user_id, aqua, prev_data, data) {
var len2 = WASM_VECTOR_LEN;
var ptr3 = passStringToWasm0(wasm, data, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len3 = WASM_VECTOR_LEN;
wasm.invoke(8, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3);
var ptr4 = passStringToWasm0(wasm, log_level, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len4 = WASM_VECTOR_LEN;
wasm.invoke(8, ptr0, len0, ptr1, len1, ptr2, len2, ptr3, len3, ptr4, len4);
var r0 = getInt32Memory0(wasm)[8 / 4 + 0];
var r1 = getInt32Memory0(wasm)[8 / 4 + 1];
return getStringFromWasm0(wasm, r0, r1);

View File

@ -19,10 +19,7 @@ import Multiaddr from "multiaddr"
import {FluenceClient} from "./fluenceClient";
import * as log from "loglevel";
import {LogLevelDesc} from "loglevel";
import {ServiceMultiple} from "./service";
import {registerService} from "./globalState";
import {build} from "./particle";
import {instantiateInterpreter, parseAstClosure} from "./stepper";
import {parseAstClosure} from "./stepper";
log.setLevel('info')
@ -39,6 +36,22 @@ export default class Fluence {
return await PeerId.create({keyType: "Ed25519"});
}
/**
* Create FluenceClient without connecting it to a relay
*
* @param peerId client's peer id. Must contain a private key. See `generatePeerId()`
*/
static async local(peerId?: PeerId): Promise<FluenceClient> {
if (!peerId) {
peerId = await Fluence.generatePeerId()
}
let client = new FluenceClient(peerId);
await client.instantiateInterpreter();
return client;
}
/**
* Connect to Fluence node.
*
@ -46,15 +59,9 @@ export default class Fluence {
* @param peerId your peer id. Should be with a private key. Could be generated by `generatePeerId()` function
*/
static async connect(multiaddr: string | Multiaddr, peerId?: PeerId): Promise<FluenceClient> {
if (!peerId) {
peerId = await Fluence.generatePeerId()
}
let client = new FluenceClient(peerId);
let client = await Fluence.local(peerId);
await client.connect(multiaddr);
return client;
}

View File

@ -59,7 +59,7 @@ export class FluenceClient {
enqueueParticle(particle);
} else {
if (this.interpreter === undefined) {
throw new Error("Undefined. Interpreter is not initialized. User 'Fluence.connect' to create a client.")
throw new Error("Undefined. Interpreter is not initialized. Use 'Fluence.connect' to create a client.")
}
// start particle processing if queue is empty
try {
@ -87,8 +87,8 @@ export class FluenceClient {
log.info("inner interpreter outcome:");
log.info(stepperOutcome);
// do nothing if there is no `next_peer_pks`
if (stepperOutcome.next_peer_pks.length > 0) {
// do nothing if there is no `next_peer_pks` or if client isn't connected to the network
if (stepperOutcome.next_peer_pks.length > 0 && this.connection) {
let newParticle: Particle = {...particle};
newParticle.data = JSON.parse(stepperOutcome.call_path);
@ -138,18 +138,27 @@ export class FluenceClient {
return this.connection.disconnect();
}
/**
* Instantiate WebAssembly with AIR interpreter to execute AIR scripts
*/
async instantiateInterpreter() {
this.interpreter = await instantiateInterpreter(this.selfPeerId);
}
/**
* Establish a connection to the node. If the connection is already established, disconnect and reregister all services in a new connection.
*
* @param multiaddr
*/
async connect(multiaddr: string | Multiaddr): Promise<void> {
async connect(multiaddr: string | Multiaddr) {
multiaddr = Multiaddr(multiaddr);
if (!this.interpreter) {
throw Error("you must call 'instantiateInterpreter' before 'connect'")
}
let nodePeerId = multiaddr.getPeerId();
this.nodePeerIdStr = nodePeerId;
if (!nodePeerId) {
throw Error("'multiaddr' did not contain a valid peer id")
}
@ -160,12 +169,8 @@ export class FluenceClient {
await this.connection.disconnect();
}
let peerId = PeerId.createFromB58String(nodePeerId);
this.interpreter = await instantiateInterpreter(this.selfPeerId);
let connection = new FluenceConnection(multiaddr, peerId, this.selfPeerId, this.handleExternalParticle());
let node = PeerId.createFromB58String(nodePeerId);
let connection = new FluenceConnection(multiaddr, node, this.selfPeerId, this.handleExternalParticle());
await connection.connect();
this.connection = connection;
@ -176,6 +181,10 @@ export class FluenceClient {
return particle.id
}
async executeParticle(particle: Particle) {
await this.handleParticle(particle);
}
nodeIdentityCall(): string {
return `(call "${this.nodePeerIdStr}" ("op" "identity") [] void[])`
}

View File

@ -40,24 +40,36 @@ type LogImport = {
log_utf8_string: (level: any, target: any, offset: any, size: any) => void
}
class HostImportsConfig {
exports: Exports | undefined;
newImportObject: () => ImportObject;
constructor(create: (cfg: HostImportsConfig) => ImportObject) {
this.exports = undefined;
this.newImportObject = () => create(this);
}
setExports(exports: Exports) {
this.exports = exports;
}
}
const interpreter_wasm = toByteArray(wasmBs64)
/// Instantiates WebAssembly runtime with AIR interpreter module
async function interpreterInstance(hostImports: (wasm: Exports) => Imports): Promise<Instance> {
// Uninitialized exports reference that must be initialized after WebAssembly is instantiated
let exports: Exports = undefined;
async function interpreterInstance(cfg: HostImportsConfig): Promise<Instance> {
/// Create host imports that use module exports internally
let imports = hostImports(exports);
let imports = cfg.newImportObject();
/// Instantiate interpreter
let interpreter_module = await WebAssembly.compile(interpreter_wasm);
let instance: Instance = await WebAssembly.instantiate(interpreter_module, imports);
/// Finally initialize exports, so host imports can use them
exports = instance.exports;
/// Set exports, so host imports can use them
cfg.setExports(instance.exports);
/// Trigger interpreter initialization (i.e., call main function)
call_export(exports.main);
call_export(instance.exports.main);
return instance;
}
@ -72,9 +84,10 @@ function call_export(f: ExportValue, ...argArray: any[]): any {
}
}
function log_import(wasm: Exports): LogImport {
function log_import(cfg: HostImportsConfig): LogImport {
return {
log_utf8_string: (level: any, target: any, offset: any, size: any) => {
let wasm = cfg.exports;
try {
let str = getStringFromWasm0(wasm, offset, size)
@ -103,12 +116,13 @@ function log_import(wasm: Exports): LogImport {
}
/// Returns import object that describes host functions called by AIR interpreter
function newImportObject(wasm: Exports, peerId: PeerId): ImportObject {
function newImportObject(cfg: HostImportsConfig, peerId: PeerId): ImportObject {
return {
// __wbg_callserviceimpl_c0ca292e3c8c0c97 this is a function generated by bindgen. Could be changed.
// If so, an error with a new name will be occurred after wasm initialization.
"./aquamarine_client_bg.js": {
__wbg_callserviceimpl_7d3cf77a2722659e: (arg0: any, arg1: any, arg2: any, arg3: any, arg4: any, arg5: any, arg6: any) => {
let wasm = cfg.exports;
try {
let serviceId = getStringFromWasm0(wasm, arg1, arg2)
let fnName = getStringFromWasm0(wasm, arg3, arg4)
@ -126,6 +140,7 @@ function newImportObject(wasm: Exports, peerId: PeerId): ImportObject {
}
},
__wbg_getcurrentpeeridimpl_154ce1848a306ff5: (arg0: any) => {
let wasm = cfg.exports;
var ret = peerId.toB58String();
var ptr0 = passStringToWasm0(wasm, ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
var len0 = WASM_VECTOR_LEN;
@ -133,13 +148,13 @@ function newImportObject(wasm: Exports, peerId: PeerId): ImportObject {
getInt32Memory0(wasm)[arg0 / 4 + 0] = ptr0;
}
},
host: log_import(wasm)
host: log_import(cfg)
};
}
function newLogImport(wasm: Exports): ImportObject {
function newLogImport(cfg: HostImportsConfig): ImportObject {
return {
host: log_import(wasm),
host: log_import(cfg),
"./aquamarine_client_bg.js": {
__wbg_callserviceimpl_7d3cf77a2722659e: _ => {},
__wbg_getcurrentpeeridimpl_154ce1848a306ff5: _ => {}
@ -150,17 +165,36 @@ function newLogImport(wasm: Exports): ImportObject {
/// Instantiates AIR interpreter, and returns its `invoke` function as closure
/// NOTE: an interpreter is also called a stepper from time to time
export async function instantiateInterpreter(peerId: PeerId): Promise<InterpreterInvoke> {
let instance = await interpreterInstance(wasm => newImportObject(wasm, peerId));
let cfg = new HostImportsConfig((cfg) => newImportObject(cfg, peerId))
let instance = await interpreterInstance(cfg);
return (init_user_id: string, script: string, prev_data: string, data: string) => {
return aqua.invoke(instance.exports, init_user_id, script, prev_data, data)
let logLevel = log.getLevel()
let logLevelStr = "info"
if (logLevel === 0) {
logLevelStr = "trace"
} else if (logLevel === 1) {
logLevelStr = "debug"
} else if (logLevel === 2) {
logLevelStr = "info"
} else if (logLevel === 3) {
logLevelStr = "warn"
} else if (logLevel === 4) {
logLevelStr = "error"
} else if (logLevel === 5) {
logLevelStr = "off"
}
return aqua.invoke(instance.exports, init_user_id, script, prev_data, data, logLevelStr)
}
}
/// Instantiate AIR interpreter with host imports containing only logger, but not call_service
/// peerId isn't actually required for AST parsing, but host imports require it, and I don't see any workaround
export async function parseAstClosure(): Promise<(script: string) => string> {
let instance = await interpreterInstance(wasm => newLogImport(wasm));
let cfg = new HostImportsConfig((cfg) => newLogImport(cfg));
let instance = await interpreterInstance(cfg);
return (script: string) => {
return aqua.ast(instance.exports, script)

27
src/test/air.spec.ts Normal file
View File

@ -0,0 +1,27 @@
import 'mocha';
import Fluence from "../fluence";
import {build} from "../particle";
import { ServiceMultiple } from "../service";
import { registerService } from "../globalState";
describe("AIR", () => {
it("call local function", async function () {
let service = new ServiceMultiple("console");
registerService(service);
service.registerFunction('log', (args: any[]) => {
console.log(`log: ${args}`);
return {}
})
let client = await Fluence.local();
let script = `(call %init_peer_id% ("console" "log") ["hello"])`
// Wrap script into particle, so it can be executed by local WASM runtime
let particle = await build(client.selfPeerId, script, new Map())
await client.executeParticle(particle);
})
})

View File

@ -3,7 +3,7 @@ import Fluence from "../fluence";
describe("AIR AST parsing suite", () => {
it("parse simple script and return ast", async function () {
let ast = Fluence.parseAIR(`
let ast = await Fluence.parseAIR(`
(call node ("service" "function") [1 2 3 arg] output)
`);

View File

@ -6,9 +6,9 @@
"./types"
],
"outDir": "./dist/",
"baseUrl": ".",
"sourceMap": true,
"inlineSources": true,
"noImplicitAny": true,
"strictFunctionTypes": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
@ -17,11 +17,13 @@
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"strict": true,
"strictNullChecks": false,
"esModuleInterop": true,
"declarationMap": true,
"baseUrl": "."
"strict": true,
"noImplicitAny": true,
"alwaysStrict": true,
"noImplicitThis": true,
"strictNullChecks": false,
},
"exclude": [
"node_modules",