Files
fluence-js/src/fluenceClient.ts

421 lines
14 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-12-23 17:24:22 +03:00
import { build, Particle } from './particle';
import { StepperOutcome } from './stepperOutcome';
import * as PeerId from 'peer-id';
import Multiaddr from 'multiaddr';
import { FluenceConnection } from './fluenceConnection';
import { Subscriptions } from './subscriptions';
import { enqueueParticle, getCurrentParticleId, popParticle, setCurrentParticleId } from './globalState';
import { instantiateInterpreter, InterpreterInvoke } from './stepper';
import log from 'loglevel';
import { waitService } from './helpers/waitService';
import { ModuleConfig } from './moduleConfig';
const bs58 = require('bs58');
const INFO_LOG_LEVEL = 2;
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-09-28 17:01:49 +03:00
2020-07-27 16:39:54 +03:00
private nodePeerIdStr: string;
2020-09-28 17:01:49 +03:00
private subscriptions = new Subscriptions();
private interpreter: InterpreterInvoke = undefined;
2020-05-14 15:20:39 +03:00
2020-09-21 16:42:53 +03:00
connection: FluenceConnection;
2020-05-14 15:20:39 +03:00
constructor(selfPeerId: PeerId) {
this.selfPeerId = selfPeerId;
this.selfPeerIdStr = selfPeerId.toB58String();
2020-05-14 15:20:39 +03:00
}
/**
* Pass a particle to a interpreter and send a result to other services.
2020-05-14 15:20:39 +03:00
*/
private async handleParticle(particle: Particle): Promise<void> {
2020-10-12 14:07:28 +03:00
// if a current particle is processing, add new particle to the queue
2020-11-04 00:03:22 +03:00
if (getCurrentParticleId() !== undefined && getCurrentParticleId() !== particle.id) {
enqueueParticle(particle);
2020-10-12 14:07:28 +03:00
} else {
if (this.interpreter === undefined) {
2020-12-23 17:24:22 +03:00
throw new Error("Undefined. Interpreter is not initialized. Use 'Fluence.connect' to create a client.");
2020-10-12 14:07:28 +03:00
}
// start particle processing if queue is empty
try {
2020-12-23 17:24:22 +03:00
setCurrentParticleId(particle.id);
// check if a particle is relevant
let now = Date.now();
let actualTtl = particle.timestamp + particle.ttl - now;
if (actualTtl <= 0) {
2020-12-23 17:24:22 +03:00
log.info(`Particle expired. Now: ${now}, ttl: ${particle.ttl}, ts: ${particle.timestamp}`);
} else {
// if there is no subscription yet, previous data is empty
2020-12-24 19:11:10 +03:00
let prevData: Uint8Array = Buffer.from([]);
let prevParticle = this.subscriptions.get(particle.id);
if (prevParticle) {
prevData = prevParticle.data;
// update a particle in a subscription
2020-12-23 17:24:22 +03:00
this.subscriptions.update(particle);
} else {
// set a particle with actual ttl
2020-12-23 17:24:22 +03:00
this.subscriptions.subscribe(particle, actualTtl);
}
2020-12-23 17:24:22 +03:00
let stepperOutcomeStr = this.interpreter(
particle.init_peer_id,
particle.script,
2020-12-24 19:11:10 +03:00
prevData,
particle.data,
2020-12-23 17:24:22 +03:00
);
let stepperOutcome: StepperOutcome = JSON.parse(stepperOutcomeStr);
2020-10-12 14:07:28 +03:00
if (log.getLevel() <= INFO_LOG_LEVEL) {
2020-12-23 17:24:22 +03:00
log.info('inner interpreter outcome:');
let so = { ...stepperOutcome };
try {
2020-12-23 17:24:22 +03:00
so.data = JSON.parse(Buffer.from(so.data).toString('utf8'));
log.info(so);
} catch (e) {
2020-12-23 17:24:22 +03:00
log.info('cannot parse StepperOutcome data as JSON: ', e);
}
}
2020-10-12 14:07:28 +03:00
// update data after aquamarine execution
2020-12-23 17:24:22 +03:00
let newParticle: Particle = { ...particle };
newParticle.data = stepperOutcome.data;
2020-12-23 17:24:22 +03:00
this.subscriptions.update(newParticle);
2020-12-08 17:13:24 +03:00
// 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) {
await this.connection.sendParticle(newParticle).catch((reason) => {
2020-12-23 17:24:22 +03:00
console.error(`Error on sending particle with id ${particle.id}: ${reason}`);
});
}
2020-10-12 14:07:28 +03:00
}
} finally {
// get last particle from the queue
let nextParticle = popParticle();
// start the processing of a new particle if it exists
if (nextParticle) {
// update current particle
setCurrentParticleId(nextParticle.id);
2020-12-23 17:24:22 +03:00
await this.handleParticle(nextParticle);
2020-10-12 14:07:28 +03:00
} else {
// wait for a new call (do nothing) if there is no new particle in a queue
setCurrentParticleId(undefined);
}
}
}
}
2020-05-14 15:20:39 +03:00
/**
2020-10-12 14:07:28 +03:00
* Handle incoming particle from a relay.
2020-05-14 15:20:39 +03:00
*/
private handleExternalParticle(): (particle: Particle) => Promise<void> {
2020-05-14 15:20:39 +03:00
let _this = this;
return async (particle: Particle) => {
2020-12-24 19:11:10 +03:00
let data: any = particle.data;
2020-12-23 17:24:22 +03:00
let error: any = data['protocol!error'];
if (error !== undefined) {
2020-12-23 17:24:22 +03:00
log.error('error in external particle: ');
log.error(error);
2020-10-12 14:07:28 +03:00
} else {
2020-12-23 17:24:22 +03:00
log.info('handle external particle: ');
log.info(particle);
await _this.handleParticle(particle);
2020-10-05 17:17:04 +03:00
}
2020-12-23 17:24:22 +03:00
};
2020-05-14 15:20:39 +03:00
}
2020-05-28 20:19:26 +03:00
async disconnect(): Promise<void> {
return this.connection.disconnect();
}
2020-12-08 17:13:24 +03:00
/**
* Instantiate WebAssembly with AIR interpreter to execute AIR scripts
*/
async instantiateInterpreter() {
this.interpreter = await instantiateInterpreter(this.selfPeerId);
}
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
*/
2020-12-08 17:13:24 +03:00
async connect(multiaddr: string | Multiaddr) {
2020-05-14 15:20:39 +03:00
multiaddr = Multiaddr(multiaddr);
2020-12-08 17:13:24 +03:00
if (!this.interpreter) {
2020-12-23 17:24:22 +03:00
throw Error("you must call 'instantiateInterpreter' before 'connect'");
2020-12-08 17:13:24 +03:00
}
2020-05-14 15:20:39 +03:00
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) {
2020-12-23 17:24:22 +03:00
throw Error("'multiaddr' did not contain a valid peer id");
2020-05-14 15:20:39 +03:00
}
let firstConnection: boolean = true;
if (this.connection) {
firstConnection = false;
await this.connection.disconnect();
}
2020-12-08 17:13:24 +03:00
let node = PeerId.createFromB58String(nodePeerId);
let connection = new FluenceConnection(multiaddr, node, this.selfPeerId, this.handleExternalParticle());
2020-05-14 15:20:39 +03:00
await connection.connect();
this.connection = connection;
2020-09-28 17:01:49 +03:00
}
2020-05-14 15:20:39 +03:00
async sendParticle(particle: Particle): Promise<string> {
await this.handleParticle(particle);
2020-12-23 17:24:22 +03:00
return particle.id;
2020-05-14 15:20:39 +03:00
}
2020-12-08 17:13:24 +03:00
async executeParticle(particle: Particle) {
await this.handleParticle(particle);
}
nodeIdentityCall(): string {
2020-12-23 17:24:22 +03:00
return `(call "${this.nodePeerIdStr}" ("op" "identity") [] void[])`;
}
2020-12-23 17:24:22 +03:00
async requestResponse<T>(
name: string,
call: (nodeId: string) => string,
returnValue: string,
data: Map<string, any>,
handleResponse: (args: any[]) => T,
nodeId?: string,
ttl?: number,
): Promise<T> {
if (!ttl) {
2020-12-23 17:24:22 +03:00
ttl = 10000;
}
if (!nodeId) {
2020-12-23 17:24:22 +03:00
nodeId = this.nodePeerIdStr;
}
2020-12-23 17:24:22 +03:00
let serviceCall = call(nodeId);
2020-12-23 17:24:22 +03:00
let namedPromise = waitService(name, handleResponse, ttl);
2020-11-04 00:03:22 +03:00
let script = `(seq
${this.nodeIdentityCall()}
2020-11-04 00:03:22 +03:00
(seq
(seq
${serviceCall}
${this.nodeIdentityCall()}
2020-11-04 00:03:22 +03:00
)
(call "${this.selfPeerIdStr}" ("${namedPromise.name}" "") [${returnValue}] void[])
)
)
2020-12-23 17:24:22 +03:00
`;
2020-12-23 17:24:22 +03:00
let particle = await build(this.selfPeerId, script, data, ttl);
await this.sendParticle(particle);
2020-12-23 17:24:22 +03:00
return namedPromise.promise;
}
/**
* Send a script to add module to a relay. Waiting for a response from a relay.
*/
2020-12-23 17:24:22 +03:00
async addModule(
name: string,
moduleBase64: string,
config?: ModuleConfig,
nodeId?: string,
ttl?: number,
): Promise<void> {
2020-12-04 17:08:35 +03:00
if (!config) {
config = {
name: name,
mem_pages_count: 100,
logger_enabled: true,
wasi: {
envs: {},
2020-12-23 17:24:22 +03:00
preopened_files: ['/tmp'],
2020-12-04 17:08:35 +03:00
mapped_dirs: {},
2020-12-23 17:24:22 +03:00
},
};
}
2020-12-23 17:24:22 +03:00
let data = new Map();
data.set('module_bytes', moduleBase64);
data.set('module_config', config);
2020-12-23 17:24:22 +03:00
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_module") [module_bytes module_config] void[])`;
2020-12-23 17:24:22 +03:00
return this.requestResponse('addModule', call, '', data, () => {}, nodeId, ttl);
}
/**
* Send a script to add module to a relay. Waiting for a response from a relay.
*/
2020-12-23 17:24:22 +03:00
async addBlueprint(
name: string,
dependencies: string[],
blueprintId?: string,
nodeId?: string,
ttl?: number,
): Promise<string> {
let returnValue = 'blueprint_id';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "add_blueprint") [blueprint] ${returnValue})`;
let data = new Map();
data.set('blueprint', { name: name, dependencies: dependencies, id: blueprintId });
return this.requestResponse(
'addBlueprint',
call,
returnValue,
data,
(args: any[]) => args[0] as string,
nodeId,
ttl,
);
}
/**
* Send a script to create a service to a relay. Waiting for a response from a relay.
*/
async createService(blueprintId: string, nodeId?: string, ttl?: number): Promise<string> {
2020-12-23 17:24:22 +03:00
let returnValue = 'service_id';
let call = (nodeId: string) => `(call "${nodeId}" ("srv" "create") [blueprint_id] ${returnValue})`;
let data = new Map();
data.set('blueprint_id', blueprintId);
return this.requestResponse(
'createService',
call,
returnValue,
data,
(args: any[]) => args[0] as string,
nodeId,
ttl,
);
}
/**
* Get all available modules hosted on a connected relay.
*/
async getAvailableModules(nodeId?: string, ttl?: number): Promise<string[]> {
2020-12-23 17:24:22 +03:00
let returnValue = 'modules';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "get_modules") [] ${returnValue})`;
return this.requestResponse(
'getAvailableModules',
call,
returnValue,
new Map(),
(args: any[]) => args[0] as string[],
nodeId,
ttl,
);
}
/**
* Get all available blueprints hosted on a connected relay.
*/
async getBlueprints(nodeId: string, ttl?: number): Promise<string[]> {
2020-12-23 17:24:22 +03:00
let returnValue = 'blueprints';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "get_blueprints") [] ${returnValue})`;
return this.requestResponse(
'getBlueprints',
call,
returnValue,
new Map(),
(args: any[]) => args[0] as string[],
nodeId,
ttl,
);
}
/**
* Add a provider to DHT network to neighborhood around a key.
*/
2020-12-23 17:24:22 +03:00
async addProvider(
key: Buffer,
providerPeer: string,
providerServiceId?: string,
nodeId?: string,
ttl?: number,
): Promise<void> {
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "add_provider") [key provider] void[])`;
2020-12-23 17:24:22 +03:00
key = bs58.encode(key);
let provider = {
peer: providerPeer,
2020-12-23 17:24:22 +03:00
service_id: providerServiceId,
};
2020-12-23 17:24:22 +03:00
let data = new Map();
data.set('key', key);
data.set('provider', provider);
2020-11-04 00:03:22 +03:00
2020-12-23 17:24:22 +03:00
return this.requestResponse('addProvider', call, '', data, () => {}, nodeId, ttl);
}
/**
* Get a provider from DHT network from neighborhood around a key..
*/
async getProviders(key: Buffer, nodeId?: string, ttl?: number): Promise<any> {
2020-12-23 17:24:22 +03:00
key = bs58.encode(key);
2020-12-23 17:24:22 +03:00
let returnValue = 'providers';
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "get_providers") [key] providers[])`;
2020-11-04 00:03:22 +03:00
2020-12-23 17:24:22 +03:00
let data = new Map();
data.set('key', key);
2020-12-23 17:24:22 +03:00
return this.requestResponse('getProviders', call, returnValue, data, (args) => args[0], nodeId, ttl);
}
/**
* Get relays neighborhood
*/
async neighborhood(node: string, ttl?: number): Promise<string[]> {
2020-12-23 17:24:22 +03:00
let returnValue = 'neighborhood';
let call = (nodeId: string) => `(call "${nodeId}" ("dht" "neighborhood") [node] ${returnValue})`;
2020-11-04 00:03:22 +03:00
2020-12-23 17:24:22 +03:00
let data = new Map();
data.set('node', node);
2020-12-23 17:24:22 +03:00
return this.requestResponse('neighborhood', call, returnValue, data, (args) => args[0] as string[], node, ttl);
}
/**
* Call relays 'identity' method. It should return passed 'fields'
*/
2020-11-04 00:03:22 +03:00
async relayIdentity(fields: string[], data: Map<string, any>, nodeId?: string, ttl?: number): Promise<any> {
2020-12-23 17:24:22 +03:00
let returnValue = 'id';
let call = (nodeId: string) => `(call "${nodeId}" ("op" "identity") [${fields.join(' ')}] ${returnValue})`;
2020-12-23 17:24:22 +03:00
return this.requestResponse('getIdentity', call, returnValue, data, (args: any[]) => args[0], nodeId, ttl);
}
2020-05-14 15:20:39 +03:00
}