Client blueprint, service incapsulation (#940)

This commit is contained in:
Dima 2020-08-20 20:28:32 +03:00 committed by GitHub
parent cc493ea8d4
commit 2277a3584c
12 changed files with 182 additions and 81 deletions

31
package-lock.json generated
View File

@ -1,6 +1,6 @@
{ {
"name": "fluence", "name": "fluence",
"version": "0.5.3", "version": "0.7.3",
"lockfileVersion": 1, "lockfileVersion": 1,
"requires": true, "requires": true,
"dependencies": { "dependencies": {
@ -134,6 +134,12 @@
"source-map": "^0.6.1" "source-map": "^0.6.1"
} }
}, },
"@types/uuid": {
"version": "8.3.0",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==",
"dev": true
},
"@types/webpack": { "@types/webpack": {
"version": "4.41.12", "version": "4.41.12",
"resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.12.tgz", "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.12.tgz",
@ -5957,6 +5963,14 @@
"faye-websocket": "^0.10.0", "faye-websocket": "^0.10.0",
"uuid": "^3.4.0", "uuid": "^3.4.0",
"websocket-driver": "0.6.5" "websocket-driver": "0.6.5"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"sockjs-client": { "sockjs-client": {
@ -6769,10 +6783,9 @@
"dev": true "dev": true
}, },
"uuid": { "uuid": {
"version": "3.4.0", "version": "8.3.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ=="
"dev": true
}, },
"v8-compile-cache": { "v8-compile-cache": {
"version": "2.0.3", "version": "2.0.3",
@ -7873,6 +7886,14 @@
"requires": { "requires": {
"ansi-colors": "^3.0.0", "ansi-colors": "^3.0.0",
"uuid": "^3.3.2" "uuid": "^3.3.2"
},
"dependencies": {
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==",
"dev": true
}
} }
}, },
"webpack-sources": { "webpack-sources": {

View File

@ -1,6 +1,6 @@
{ {
"name": "fluence", "name": "fluence",
"version": "0.7.3", "version": "0.7.6",
"description": "the browser js-libp2p client for the Fluence network", "description": "the browser js-libp2p client for the Fluence network",
"main": "./dist/fluence.js", "main": "./dist/fluence.js",
"typings": "./dist/fluence.d.ts", "typings": "./dist/fluence.d.ts",
@ -26,11 +26,13 @@
"libp2p-secio": "0.12.5", "libp2p-secio": "0.12.5",
"libp2p-websockets": "0.13.6", "libp2p-websockets": "0.13.6",
"peer-id": "0.13.12", "peer-id": "0.13.12",
"peer-info": "0.17.5" "peer-info": "0.17.5",
"uuid": "8.3.0"
}, },
"devDependencies": { "devDependencies": {
"@types/base64-js": "1.2.5", "@types/base64-js": "1.2.5",
"@types/bs58": "4.0.1", "@types/bs58": "4.0.1",
"@types/uuid": "8.3.0",
"@types/chai": "4.2.11", "@types/chai": "4.2.11",
"@types/mocha": "7.0.2", "@types/mocha": "7.0.2",
"assert": "2.0.0", "assert": "2.0.0",

35
src/blueprint.ts Normal file
View File

@ -0,0 +1,35 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export interface Blueprint {
dependencies: string[],
id: string,
name: string
}
export function checkBlueprint(b: any): b is Blueprint {
if (!b.id) throw new Error(`There is no 'id' field in Blueprint struct: ${JSON.stringify(b)}`)
if (b.dependencies) {
b.dependencies.forEach((dep: any) => {
if ((typeof dep) !== 'string') {
throw Error(`'dependencies' should be an array of strings: ${JSON.stringify(b)}`)
}
});
}
return true;
}

View File

@ -17,7 +17,7 @@
import * as PeerInfo from "peer-info"; import * as PeerInfo from "peer-info";
import * as PeerId from "peer-id"; import * as PeerId from "peer-id";
import Multiaddr from "multiaddr" import Multiaddr from "multiaddr"
import {FluenceClient} from "./fluence_client"; import {FluenceClient} from "./fluenceClient";
export default class Fluence { export default class Fluence {

View File

@ -22,21 +22,22 @@ import {
ProtocolType, ProtocolType,
addressToString addressToString
} from "./address"; } from "./address";
import {callToString, FunctionCall, genUUID, makeFunctionCall,} from "./function_call"; import {callToString, FunctionCall, genUUID, makeFunctionCall,} from "./functionCall";
import * as PeerId from "peer-id"; import * as PeerId from "peer-id";
import {Services} from "./services"; import {LocalServices} from "./localServices";
import Multiaddr from "multiaddr" import Multiaddr from "multiaddr"
import {Subscriptions} from "./subscriptions"; import {Subscriptions} from "./subscriptions";
import * as PeerInfo from "peer-info"; import * as PeerInfo from "peer-info";
import {FluenceConnection} from "./fluence_connection"; import {FluenceConnection} from "./fluenceConnection";
import {checkInterface, Interface} from "./Interface"; import {checkInterface, Interface} from "./Interface";
import {Service} from "./service";
import {Blueprint, checkBlueprint} from "./blueprint";
/** /**
* @param target receiver * @param target receiver
* @param args message in the call * @param args message in the call
* @param moduleId module name * @param moduleId module name
* @param fname function name * @param fname function name
* @param context list of modules to use with the request
* @param name common field for debug purposes * @param name common field for debug purposes
* @param msgId hash that will be added to replyTo address * @param msgId hash that will be added to replyTo address
*/ */
@ -46,7 +47,6 @@ interface Call {
moduleId?: string, moduleId?: string,
fname?: string, fname?: string,
msgId?: string, msgId?: string,
context?: string[],
name?: string name?: string
} }
@ -57,7 +57,7 @@ export class FluenceClient {
private connection: FluenceConnection; private connection: FluenceConnection;
private services: Services = new Services(); private services: LocalServices = new LocalServices();
private subscriptions: Subscriptions = new Subscriptions(); private subscriptions: Subscriptions = new Subscriptions();
@ -105,30 +105,13 @@ export class FluenceClient {
return (args: any, target: Address) => target.hash && target.hash === msgId; return (args: any, target: Address) => target.hash && target.hash === msgId;
} }
/**
* Send call and wait a response.
*
* @param target receiver
* @param args message in the call
* @param moduleId module name
* @param fname functin name
*/
async sendCallWaitResponse(target: Address, args: any, moduleId?: string, fname?: string): Promise<any> {
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
await this.sendCall({target: target, args: args, moduleId: moduleId, fname: fname, msgId: msgId});
return this.waitResponse(predicate, false);
}
/** /**
* Send call and forget. * Send call and forget.
* *
*/ */
async sendCall(call: Call) { async sendCall(call: Call) {
if (this.connection && this.connection.isConnected()) { if (this.connection && this.connection.isConnected()) {
await this.connection.sendFunctionCall(call.target, call.args, call.moduleId, call.fname, call.msgId, call.context, call.name); await this.connection.sendFunctionCall(call.target, call.args, call.moduleId, call.fname, call.msgId, call.name);
} else { } else {
throw Error("client is not connected") throw Error("client is not connected")
} }
@ -158,10 +141,9 @@ export class FluenceClient {
* @param addr node address * @param addr node address
* @param args message to the service * @param args message to the service
* @param fname function name * @param fname function name
* @param context
* @param name debug info * @param name debug info
*/ */
async callPeer(moduleId: string, args: any, fname?: string, addr?: string, context?: string[], name?: string): Promise<any> { async callPeer(moduleId: string, args: any, fname?: string, addr?: string, name?: string): Promise<any> {
let msgId = genUUID(); let msgId = genUUID();
let predicate = this.getPredicate(msgId); let predicate = this.getPredicate(msgId);
@ -172,7 +154,7 @@ export class FluenceClient {
address = createPeerAddress(this.nodePeerIdStr); address = createPeerAddress(this.nodePeerIdStr);
} }
await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, context: context, name: name}) await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, name: name})
return await this.waitResponse(predicate, false); return await this.waitResponse(predicate, false);
} }
@ -186,6 +168,10 @@ export class FluenceClient {
return await this.waitResponse(predicate, false); return await this.waitResponse(predicate, false);
} }
getService(peerId: string, serviceId: string): Service {
return new Service(this, peerId, serviceId);
}
/** /**
* Handle incoming call. * Handle incoming call.
* If FunctionCall returns - we should send it as a response. * If FunctionCall returns - we should send it as a response.
@ -280,17 +266,32 @@ export class FluenceClient {
/** /**
* Sends a call to create a service on remote node. * Sends a call to create a service on remote node.
*/ */
async createService(peerId: string, context: string[]): Promise<string> { async createService(peerId: string, blueprint: string): Promise<string> {
let resp = await this.callPeer("create", {}, undefined, peerId, context); let resp = await this.callPeer("create", {blueprint_id: blueprint}, undefined, peerId);
if (resp.result && resp.result.service_id) { if (resp && resp.service_id) {
return resp.result.service_id return resp.service_id
} else { } else {
console.error("Unknown response type on `createService`: ", resp) console.error("Unknown response type on `createService`: ", resp)
throw new Error("Unknown response type on `createService`"); throw new Error("Unknown response type on `createService`");
} }
} }
async addBlueprint(peerId: string, name: string, dependencies: string[]): Promise<string> {
let id = genUUID();
let blueprint = {
name: name,
id: id,
dependencies: dependencies
};
let msg_id = genUUID();
await this.callPeer("add_blueprint", {msg_id, blueprint}, undefined, peerId);
return id;
}
async getInterface(serviceId: string, peerId?: string): Promise<Interface> { async getInterface(serviceId: string, peerId?: string): Promise<Interface> {
let resp; let resp;
resp = await this.callPeer("get_interface", {service_id: serviceId}, undefined, peerId) resp = await this.callPeer("get_interface", {service_id: serviceId}, undefined, peerId)
@ -303,13 +304,27 @@ export class FluenceClient {
} }
} }
async getActiveInterfaces(peerId?: string): Promise<Interface[]> { async getAvailableBlueprints(peerId?: string): Promise<Blueprint[]> {
let resp; let resp = await this.callPeer("get_available_blueprints", {}, undefined);
if (peerId) {
resp = await this.sendCallWaitResponse(createPeerAddress(peerId), {}, "get_active_interfaces"); let blueprints = resp.available_blueprints;
if (blueprints && blueprints instanceof Array) {
return blueprints.map((b: any) => {
if (checkBlueprint(b)) {
return b;
} else { } else {
resp = await this.callPeer("get_active_interfaces", {}, undefined, peerId); throw new Error("Unexpected");
} }
});
} else {
throw new Error("Unexpected. 'get_active_interfaces' should return an array of interfaces.");
}
}
async getActiveInterfaces(peerId?: string): Promise<Interface[]> {
let resp = await this.callPeer("get_active_interfaces", {}, undefined);
let interfaces = resp.active_interfaces; let interfaces = resp.active_interfaces;
if (interfaces && interfaces instanceof Array) { if (interfaces && interfaces instanceof Array) {
return interfaces.map((i: any) => { return interfaces.map((i: any) => {
@ -320,18 +335,12 @@ export class FluenceClient {
} }
}); });
} else { } else {
throw new Error("Unexpected"); throw new Error("Unexpected. 'get_active_interfaces' should return an array pf interfaces.");
} }
} }
async getAvailableModules(peerId?: string): Promise<string[]> { async getAvailableModules(peerId?: string): Promise<string[]> {
let resp; let resp = await this.callPeer("get_available_modules", {}, undefined, peerId);
if (peerId) {
resp = await this.sendCallWaitResponse(createPeerAddress(peerId), {}, "get_available_modules");
} else {
resp = await this.callPeer("get_available_modules", {}, undefined, peerId);
}
return resp.available_modules; return resp.available_modules;
} }
@ -357,12 +366,7 @@ export class FluenceClient {
preopened_files: preopened_files preopened_files: preopened_files
} }
} }
let resp; let resp = await this.callPeer("add_module", {bytes: bytes, config: config}, undefined, peerId);
if (peerId) {
resp = await this.sendCallWaitResponse(createPeerAddress(peerId), {bytes: bytes, config: config}, "add_module");
} else {
resp = await this.callPeer("add_module", {bytes: bytes, config: config}, undefined, peerId);
}
return resp.available_modules; return resp.available_modules;
} }

View File

@ -22,7 +22,7 @@ import {
makeFunctionCall, makeFunctionCall,
makeProvideMessage, makeProvideMessage,
parseFunctionCall parseFunctionCall
} from "./function_call"; } from "./functionCall";
import * as PeerId from "peer-id"; import * as PeerId from "peer-id";
import * as PeerInfo from "peer-info"; import * as PeerInfo from "peer-info";
import Websockets from "libp2p-websockets"; import Websockets from "libp2p-websockets";
@ -162,13 +162,13 @@ export class FluenceConnection {
/** /**
* Send FunctionCall to the connected node. * Send FunctionCall to the connected node.
*/ */
async sendFunctionCall(target: Address, args: any, moduleId?: string, fname?: string, msgId?: string, context?: string[], name?: string) { async sendFunctionCall(target: Address, args: any, moduleId?: string, fname?: string, msgId?: string, name?: string) {
this.checkConnectedOrThrow(); this.checkConnectedOrThrow();
let replyTo; let replyTo;
if (msgId) replyTo = this.makeReplyTo(msgId); if (msgId) replyTo = this.makeReplyTo(msgId);
let call = makeFunctionCall(genUUID(), target, this.sender, args, moduleId, fname, replyTo, context, name); let call = makeFunctionCall(genUUID(), target, this.sender, args, moduleId, fname, replyTo, name);
await this.sendCall(call); await this.sendCall(call);
} }

View File

@ -15,11 +15,9 @@
*/ */
import { import {
createPeerAddress,
createRelayAddress,
Address, addressToString, parseAddress Address, addressToString, parseAddress
} from "./address"; } from "./address";
import * as PeerId from "peer-id"; import { v4 as uuidv4 } from 'uuid';
export interface FunctionCall { export interface FunctionCall {
uuid: string, uuid: string,
@ -29,7 +27,6 @@ export interface FunctionCall {
"module"?: string, "module"?: string,
fname?: string, fname?: string,
arguments: any, arguments: any,
context?: string[],
name?: string, name?: string,
action: "FunctionCall" action: "FunctionCall"
} }
@ -47,7 +44,7 @@ export function callToString(call: FunctionCall) {
return JSON.stringify(obj) return JSON.stringify(obj)
} }
export function makeFunctionCall(uuid: string, target: Address, sender: Address, args: object, moduleId?: string, fname?: string, replyTo?: Address, context?: string[], name?: string): FunctionCall { export function makeFunctionCall(uuid: string, target: Address, sender: Address, args: object, moduleId?: string, fname?: string, replyTo?: Address, name?: string): FunctionCall {
return { return {
uuid: uuid, uuid: uuid,
@ -57,7 +54,6 @@ export function makeFunctionCall(uuid: string, target: Address, sender: Address,
"module": moduleId, "module": moduleId,
fname: fname, fname: fname,
arguments: args, arguments: args,
context: context,
name: name, name: name,
action: "FunctionCall" action: "FunctionCall"
} }
@ -83,7 +79,6 @@ export function parseFunctionCall(str: string): FunctionCall {
reply_to: replyTo, reply_to: replyTo,
sender: sender, sender: sender,
arguments: json.arguments, arguments: json.arguments,
context: json.context,
"module": json.module, "module": json.module,
fname: json.fname, fname: json.fname,
name: json.name, name: json.name,
@ -92,15 +87,14 @@ export function parseFunctionCall(str: string): FunctionCall {
} }
export function genUUID() { export function genUUID() {
let date = new Date(); return uuidv4();
return date.toISOString()
} }
/** /**
* Message to provide new name. * Message to provide new name.
*/ */
export async function makeProvideMessage(name: string, target: Address, sender: Address): Promise<FunctionCall> { export async function makeProvideMessage(name: string, target: Address, sender: Address): Promise<FunctionCall> {
return makeFunctionCall(genUUID(), target, sender, {name: name, address: addressToString(sender)}, "provide", undefined, sender, undefined, "provide service_id"); return makeFunctionCall(genUUID(), target, sender, {name: name, address: addressToString(sender)}, "provide", undefined, sender, "provide service_id");
} }
// TODO uncomment when this will be implemented in Fluence network // TODO uncomment when this will be implemented in Fluence network

View File

@ -14,9 +14,9 @@
* limitations under the License. * limitations under the License.
*/ */
import {FunctionCall} from "./function_call"; import {FunctionCall} from "./functionCall";
export class Services { export class LocalServices {
private services: Map<string, (req: FunctionCall) => void> = new Map(); private services: Map<string, (req: FunctionCall) => void> = new Map();

39
src/service.ts Normal file
View File

@ -0,0 +1,39 @@
/*
* Copyright 2020 Fluence Labs Limited
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import {FluenceClient} from "./fluenceClient";
export class Service {
private readonly client: FluenceClient;
private readonly serviceId: string;
private readonly peerId: string;
constructor(client: FluenceClient, peerId: string, serviceId: string) {
this.client = client;
this.serviceId = serviceId;
this.peerId = peerId;
}
/**
*
* @param moduleId wich module in service to call
* @param args parameters to call service
* @param fname function name if existed
*/
async call(moduleId: string, args: any, fname?: string): Promise<any> {
return this.client.callService(this.peerId, this.serviceId, moduleId, args, fname);
}
}

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import {FunctionCall} from "./function_call"; import {FunctionCall} from "./functionCall";
import {Address} from "./address"; import {Address} from "./address";
export class Subscriptions { export class Subscriptions {

View File

@ -10,7 +10,7 @@ import {expect} from 'chai';
import 'mocha'; import 'mocha';
import {encode} from "bs58" import {encode} from "bs58"
import * as PeerId from "peer-id"; import * as PeerId from "peer-id";
import {callToString, genUUID, makeFunctionCall, parseFunctionCall} from "../function_call"; import {callToString, genUUID, makeFunctionCall, parseFunctionCall} from "../functionCall";
import Fluence from "../fluence"; import Fluence from "../fluence";
import {certificateFromString, certificateToString, issue} from "../trust/certificate"; import {certificateFromString, certificateToString, issue} from "../trust/certificate";
import {TrustGraph} from "../trust/trust_graph"; import {TrustGraph} from "../trust/trust_graph";
@ -70,7 +70,6 @@ describe("Typescript usage suite", () => {
"mm", "mm",
"fff", "fff",
addr, addr,
undefined,
"2444" "2444"
); );
@ -197,10 +196,16 @@ export async function testUploadWasm() {
let peerId1 = "12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM" let peerId1 = "12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM"
let serviceId = await cl1.createService(peerId1, [moduleName]); let blueprintId = await cl1.addBlueprint(peerId1, "some test blueprint", [moduleName])
let blueprints = await cl1.getAvailableBlueprints(peerId1)
console.log(blueprints);
let serviceId = await cl1.createService(peerId1, blueprintId);
let service = cl1.getService(peerId1, serviceId);
let argName = genUUID(); let argName = genUUID();
let resp = await cl1.callService(peerId1, serviceId, moduleName, {name: argName}, "greeting") let resp = await service.call(moduleName, {name: argName}, "greeting")
expect(resp.result).to.be.equal(`Hi, ${argName}`) expect(resp.result).to.be.equal(`Hi, ${argName}`)
} }
@ -215,9 +220,10 @@ export async function testServicesAndInterfaces() {
let peerId1 = "12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM" let peerId1 = "12D3KooWPnLxnY71JDxvB3zbjKu9k1BCYNthGZw6iGrLYsR1RnWM"
let serviceId = await cl2.createService(peerId1, ["ipfs_node.wasm"]); let blueprintId = await cl1.addBlueprint(peerId1, "some test blueprint", ["ipfs_node"])
let serviceId = await cl2.createService(peerId1, blueprintId);
let resp = await cl2.callService(peerId1, serviceId, "ipfs_node.wasm", {}, "get_address") let resp = await cl2.callService(peerId1, serviceId, "ipfs_node", {}, "get_address")
console.log(resp) console.log(resp)
let interfaces = await cl1.getActiveInterfaces(); let interfaces = await cl1.getActiveInterfaces();

View File

@ -14,7 +14,7 @@
* limitations under the License. * limitations under the License.
*/ */
import {FluenceClient} from "../fluence_client"; import {FluenceClient} from "../fluenceClient";
import {Certificate, certificateFromString, certificateToString} from "./certificate"; import {Certificate, certificateFromString, certificateToString} from "./certificate";
// The client to interact with the Fluence trust graph API // The client to interact with the Fluence trust graph API