fluence-js/src/fluenceClient.ts

437 lines
16 KiB
TypeScript
Raw Normal View History

2020-05-14 15:20:39 +03:00
/*
2020-05-14 17:30:17 +03:00
* Copyright 2020 Fluence Labs Limited
2020-05-14 15:20:39 +03:00
*
2020-05-14 17:30:17 +03:00
* 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
2020-05-14 15:20:39 +03:00
*
2020-05-14 17:30:17 +03:00
* http://www.apache.org/licenses/LICENSE-2.0
2020-05-14 15:20:39 +03:00
*
2020-05-14 17:30:17 +03:00
* 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.
2020-05-14 15:20:39 +03:00
*/
2020-07-27 16:39:54 +03:00
import {
Address,
createPeerAddress,
createRelayAddress,
createProviderAddress,
ProtocolType,
addressToString
} from "./address";
import {callToString, FunctionCall, genUUID, makeFunctionCall,} from "./functionCall";
2020-05-14 15:20:39 +03:00
import * as PeerId from "peer-id";
import {LocalServices} from "./localServices";
2020-05-14 15:20:39 +03:00
import Multiaddr from "multiaddr"
import {Subscriptions} from "./subscriptions";
import {FluenceConnection} from "./fluenceConnection";
2020-07-30 15:36:36 +03:00
import {checkInterface, Interface} from "./Interface";
import {Service} from "./service";
import {Blueprint, checkBlueprint} from "./blueprint";
2020-09-15 12:09:13 +03:00
import * as log from 'loglevel';
2020-05-14 15:20:39 +03:00
2020-07-27 16:39:54 +03:00
/**
* @param target receiver
* @param args message in the call
* @param moduleId module name
* @param fname function name
* @param name common field for debug purposes
* @param msgId hash that will be added to replyTo address
*/
interface Call {
target: Address,
args: any,
moduleId?: string,
fname?: string,
msgId?: string,
name?: string
}
2020-05-14 15:20:39 +03:00
export class FluenceClient {
readonly selfPeerId: PeerId;
2020-05-14 15:20:39 +03:00
readonly selfPeerIdStr: string;
2020-07-27 16:39:54 +03:00
private nodePeerIdStr: string;
2020-05-14 15:20:39 +03:00
private connection: FluenceConnection;
private services: LocalServices = new LocalServices();
2020-05-14 15:20:39 +03:00
private subscriptions: Subscriptions = new Subscriptions();
constructor(selfPeerId: PeerId) {
this.selfPeerId = selfPeerId;
this.selfPeerIdStr = selfPeerId.toB58String();
2020-05-14 15:20:39 +03:00
}
/**
* Makes call with response from function. Without reply_to field.
*/
2020-06-12 19:54:09 +03:00
private responseCall(target: Address, args: any): FunctionCall {
return makeFunctionCall(genUUID(), target, this.connection.sender, args, undefined, "response");
2020-05-14 15:20:39 +03:00
}
/**
* Waits a response that match the predicate.
*
* @param predicate will be applied to each incoming call until it matches
2020-07-27 16:39:54 +03:00
* @param ignoreErrors ignore an errors, wait for success response
2020-05-14 15:20:39 +03:00
*/
2020-07-27 16:39:54 +03:00
waitResponse(predicate: (args: any, target: Address, replyTo: Address) => (boolean | undefined), ignoreErrors: boolean): Promise<any> {
return new Promise((resolve, reject) => {
2020-05-14 15:20:39 +03:00
// subscribe for responses, to handle response
// TODO if there's no conn, reject
this.subscribe((args: any, target: Address, replyTo: Address) => {
if (predicate(args, target, replyTo)) {
2020-07-27 16:39:54 +03:00
if (args.reason) {
if (ignoreErrors) {
return false;
} else {
reject(new Error(args.reason));
}
} else {
resolve(args);
}
2020-05-14 15:20:39 +03:00
return true;
}
return false;
});
});
}
2020-06-30 16:34:05 +03:00
private getPredicate(msgId: string): (args: any, target: Address) => (boolean | undefined) {
2020-07-27 16:39:54 +03:00
return (args: any, target: Address) => target.hash && target.hash === msgId;
2020-06-30 16:34:05 +03:00
}
2020-05-14 15:20:39 +03:00
/**
* Send call and forget.
*
*/
2020-07-27 16:39:54 +03:00
async sendCall(call: Call) {
2020-05-14 15:20:39 +03:00
if (this.connection && this.connection.isConnected()) {
await this.connection.sendFunctionCall(call.target, call.args, call.moduleId, call.fname, call.msgId, call.name);
2020-05-14 15:20:39 +03:00
} else {
throw Error("client is not connected")
}
}
/**
2020-07-27 16:39:54 +03:00
* Send call to the provider and wait a response matches predicate.
2020-05-14 15:20:39 +03:00
*
2020-07-27 16:39:54 +03:00
* @param provider published name in dht
2020-05-14 15:20:39 +03:00
* @param args message to the service
2020-07-27 16:39:54 +03:00
* @param moduleId module name
* @param fname function name
2020-07-27 16:39:54 +03:00
* @param name debug info
2020-05-14 15:20:39 +03:00
*/
2020-07-27 16:39:54 +03:00
async callProvider(provider: string, args: any, moduleId?: string, fname?: string, name?: string): Promise<any> {
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
let address = createProviderAddress(provider);
await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, name: name});
return await this.waitResponse(predicate, true);
2020-06-26 16:12:37 +03:00
}
/**
2020-07-27 16:39:54 +03:00
* Send a call to the local service and wait a response matches predicate on a peer the client connected with.
2020-06-26 16:12:37 +03:00
*
* @param moduleId
2020-07-27 16:39:54 +03:00
* @param addr node address
2020-06-26 16:12:37 +03:00
* @param args message to the service
* @param fname function name
2020-07-27 16:39:54 +03:00
* @param name debug info
2020-06-26 16:12:37 +03:00
*/
async callPeer(moduleId: string, args: any, fname?: string, addr?: string, name?: string): Promise<any> {
2020-07-27 16:39:54 +03:00
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
let address;
if (addr) {
address = createPeerAddress(addr);
2020-05-14 15:20:39 +03:00
} else {
2020-07-27 16:39:54 +03:00
address = createPeerAddress(this.nodePeerIdStr);
2020-05-14 15:20:39 +03:00
}
await this.sendCall({target: address, args: args, moduleId: moduleId, fname: fname, msgId: msgId, name: name})
2020-07-27 16:39:54 +03:00
return await this.waitResponse(predicate, false);
2020-05-14 15:20:39 +03:00
}
2020-07-27 16:39:54 +03:00
async callService(peerId: string, serviceId: string, moduleId: string, args: any, fname?: string): Promise<any> {
let target = createPeerAddress(peerId, serviceId);
let msgId = genUUID();
let predicate = this.getPredicate(msgId);
await this.sendCall({target: target, args: args, moduleId: moduleId, fname: fname, msgId: msgId});
return await this.waitResponse(predicate, false);
2020-06-26 16:12:37 +03:00
}
getService(peerId: string, serviceId: string): Service {
return new Service(this, peerId, serviceId);
}
2020-05-14 15:20:39 +03:00
/**
* Handle incoming call.
* If FunctionCall returns - we should send it as a response.
*/
handleCall(): (call: FunctionCall) => FunctionCall | undefined {
let _this = this;
return (call: FunctionCall) => {
2020-09-15 12:09:13 +03:00
log.debug("FunctionCall received:");
2020-05-14 15:20:39 +03:00
// if other side return an error - handle it
// TODO do it in the protocol
/*if (call.arguments.error) {
this.handleError(call);
} else {
}*/
let target = call.target;
// the tail of addresses should be you or your service
let lastProtocol = target.protocols[target.protocols.length - 1];
// call all subscriptions for a new call
_this.subscriptions.applyToSubscriptions(call);
switch (lastProtocol.protocol) {
2020-06-26 16:12:37 +03:00
case ProtocolType.Providers:
2020-05-14 15:20:39 +03:00
return undefined;
case ProtocolType.Client:
if (lastProtocol.value === _this.selfPeerIdStr) {
2020-09-15 12:09:13 +03:00
log.debug(`relay call:`);
log.debug(JSON.stringify(call, undefined, 2));
2020-06-26 16:12:37 +03:00
if (call.module) {
try {
// call of the service, service should handle response sending, error handling, requests to other services
let applied = _this.services.applyToService(call);
// if the request hasn't been applied, there is no such service. Return an error.
if (!applied) {
2020-09-15 12:09:13 +03:00
log.warn(`there is no service ${lastProtocol.value}`);
2020-06-26 16:12:37 +03:00
return this.responseCall(call.reply_to, {
reason: `there is no such service`,
msg: call
});
}
} catch (e) {
// if service throw an error, return it to the sender
return this.responseCall(call.reply_to, {
reason: `error on execution: ${e}`,
msg: call
});
}
}
2020-05-14 15:20:39 +03:00
} else {
2020-09-15 12:09:13 +03:00
log.warn(`this relay call is not for me: ${callToString(call)}`);
2020-06-12 19:54:09 +03:00
return this.responseCall(call.reply_to, {
2020-05-25 19:49:13 +03:00
reason: `this relay call is not for me`,
msg: call
});
2020-05-14 15:20:39 +03:00
}
return undefined;
case ProtocolType.Peer:
if (lastProtocol.value === this.selfPeerIdStr) {
2020-09-15 12:09:13 +03:00
log.debug(`peer call: ${call}`);
2020-05-14 15:20:39 +03:00
} else {
2020-09-15 12:09:13 +03:00
log.warn(`this peer call is not for me: ${callToString(call)}`);
2020-06-12 19:54:09 +03:00
return this.responseCall(call.reply_to, {
2020-05-25 19:49:13 +03:00
reason: `this relay call is not for me`,
msg: call
});
2020-05-14 15:20:39 +03:00
}
return undefined;
}
}
}
/**
2020-07-27 16:39:54 +03:00
* Become a name provider. Other network members could find and call one of the providers of this name by this name.
2020-05-14 15:20:39 +03:00
*/
2020-07-27 16:39:54 +03:00
async provideName(name: string, fn: (req: FunctionCall) => void) {
let replyTo = this.connection.sender;
await this.callPeer("provide", {name: name, address: addressToString(replyTo)})
this.services.addService(name, fn);
}
/**
* Sends a call to create a service on remote node.
*/
2020-09-15 12:09:13 +03:00
async createService(blueprint: string, peerId?: string): Promise<string> {
let resp = await this.callPeer("create", {blueprint_id: blueprint}, undefined, peerId);
2020-07-27 16:39:54 +03:00
if (resp && resp.service_id) {
return resp.service_id
2020-07-27 16:39:54 +03:00
} else {
2020-09-15 12:09:13 +03:00
log.error("Unknown response type on `createService`: ", resp)
2020-07-27 16:39:54 +03:00
throw new Error("Unknown response type on `createService`");
}
}
2020-09-15 12:09:13 +03:00
async addBlueprint(name: string, dependencies: string[], peerId?: 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;
}
2020-07-30 15:36:36 +03:00
async getInterface(serviceId: string, peerId?: string): Promise<Interface> {
2020-07-27 16:39:54 +03:00
let resp;
resp = await this.callPeer("get_interface", {service_id: serviceId}, undefined, peerId)
2020-07-30 15:36:36 +03:00
let i = resp.interface;
if (checkInterface(i)) {
return i;
} else {
throw new Error("Unexpected");
}
2020-07-27 16:39:54 +03:00
}
async getAvailableBlueprints(peerId?: string): Promise<Blueprint[]> {
let resp = await this.callPeer("get_available_blueprints", {}, undefined, peerId);
let blueprints = resp.available_blueprints;
if (blueprints && blueprints instanceof Array) {
return blueprints.map((b: any) => {
if (checkBlueprint(b)) {
return b;
} else {
throw new Error("Unexpected");
}
});
2020-07-27 16:39:54 +03:00
} else {
throw new Error("Unexpected. 'get_active_interfaces' should return an array of interfaces.");
2020-07-27 16:39:54 +03:00
}
}
async getActiveInterfaces(peerId?: string): Promise<Interface[]> {
let resp = await this.callPeer("get_active_interfaces", {}, undefined, peerId);
2020-07-30 15:36:36 +03:00
let interfaces = resp.active_interfaces;
if (interfaces && interfaces instanceof Array) {
return interfaces.map((i: any) => {
if (checkInterface(i)) {
return i;
} else {
throw new Error("Unexpected");
}
});
} else {
throw new Error("Unexpected. 'get_active_interfaces' should return an array pf interfaces.");
2020-07-30 15:36:36 +03:00
}
2020-07-27 16:39:54 +03:00
}
async getAvailableModules(peerId?: string): Promise<string[]> {
let resp = await this.callPeer("get_available_modules", {}, undefined, peerId);
2020-07-27 16:39:54 +03:00
return resp.available_modules;
}
/**
* Add new WASM module to the node.
*
* @param bytes WASM in base64
* @param name WASM identificator
* @param mem_pages_count memory amount for WASM
* @param envs environment variables
* @param mapped_dirs links to directories
* @param preopened_files files and directories that will be used in WASM
* @param peerId the node to add module
*/
async addModule(bytes: string, name: string, mem_pages_count: number, envs: string[], mapped_dirs: any, preopened_files: string[], peerId?: string): Promise<any> {
let config: any = {
logger_enabled: true,
mem_pages_count: mem_pages_count,
name: name,
wasi: {
envs: envs,
mapped_dirs: mapped_dirs,
preopened_files: preopened_files
}
}
let resp = await this.callPeer("add_module", {bytes: bytes, config: config}, undefined, peerId);
2020-05-14 15:20:39 +03:00
2020-07-27 16:39:54 +03:00
return resp.available_modules;
2020-05-14 15:20:39 +03:00
}
// subscribe new hook for every incoming call, to handle in-service responses and other different cases
// the hook will be deleted if it will return `true`
subscribe(predicate: (args: any, target: Address, replyTo: Address) => (boolean | undefined)) {
this.subscriptions.subscribe(predicate)
}
/**
* Sends a call to unregister the service.
2020-05-14 15:20:39 +03:00
*/
async unregisterService(moduleId: string) {
if (this.services.deleteService(moduleId)) {
2020-09-15 12:09:13 +03:00
log.warn("unregister is not implemented yet (service: ${serviceId}")
2020-05-14 15:20:39 +03:00
// TODO unregister in fluence network when it will be supported
// let regMsg = makeRegisterMessage(serviceId, PeerId.createFromB58String(this.nodePeerId));
// await this.sendFunctionCall(regMsg);
}
}
2020-05-28 20:19:26 +03:00
async disconnect(): Promise<void> {
return this.connection.disconnect();
}
2020-05-14 15:20:39 +03:00
/**
* 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> {
multiaddr = Multiaddr(multiaddr);
let nodePeerId = multiaddr.getPeerId();
2020-07-27 16:39:54 +03:00
this.nodePeerIdStr = nodePeerId;
2020-05-14 15:20:39 +03:00
if (!nodePeerId) {
throw Error("'multiaddr' did not contain a valid peer id")
}
let firstConnection: boolean = true;
if (this.connection) {
firstConnection = false;
await this.connection.disconnect();
}
let peerId = PeerId.createFromB58String(nodePeerId);
let relayAddress = await createRelayAddress(nodePeerId, this.selfPeerId, true);
let connection = new FluenceConnection(multiaddr, peerId, this.selfPeerId, relayAddress, this.handleCall());
2020-05-14 15:20:39 +03:00
await connection.connect();
this.connection = connection;
// if the client already had a connection, it will reregister all services after establishing a new connection.
if (!firstConnection) {
for (let service of this.services.getAllServices().keys()) {
2020-07-27 16:39:54 +03:00
await this.connection.provideName(service);
2020-05-14 15:20:39 +03:00
}
}
}
}