Remove frank (#81)

This commit is contained in:
folex 2020-05-14 15:20:39 +03:00 committed by GitHub
commit 96370bb26f
16 changed files with 10659 additions and 0 deletions

12
.npmignore Normal file
View File

@ -0,0 +1,12 @@
.idea
.gitignore
node_modules
types
src
tsconfig.json
webpack.config.js
*.js.map
bundle

52
README.md Normal file
View File

@ -0,0 +1,52 @@
# Fluence browser client
Browser client for the Fluence network based on the js-libp2p.
## How to build
With `npm` installed building could be done as follows:
```bash
npm install fluence
```
## Example
Shows how to register and call new service in Fluence network.
Generate new peer ids for clients.
```typescript
let peerId1 = await Fluence.generatePeerId();
let peerId2 = await Fluence.generatePeerId();
```
Establish connections to predefined nodes.
```typescript
let client1 = await Fluence.connect("12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", "104.248.25.59", 9003, peerId1);
let client2 = await Fluence.connect("12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", "104.248.25.59", 9002, peerId2);
```
Create a new unique service by the first client that will calculate the sum of two numbers.
```typescript
let serviceId = "sum-calculator-" + genUUID();
await client1.registerService(serviceId, async (req) => {
let message = {msgId: req.arguments.msgId, result: req.arguments.one + req.arguments.two};
await client1.sendCall(req.reply_to, message);
});
```
Send a request by the second client and print a result. The predicate is required to match a request and a response by `msgId`.
```typescript
let msgId = "calculate-it-for-me" + genUUID();
let req = {one: 12, two: 23, msgId: msgId};
let predicate = (args: any) => args.msgId && args.msgId === msgId;
let response = await client2.sendServiceCallWaitResponse(serviceId, req, predicate);
let result = response.result;
console.log(`calculation result is: ${result}`);
```

9343
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

48
package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "fluence",
"version": "0.4.9",
"description": "the browser js-libp2p client for the Fluence network",
"main": "./dist/fluence.js",
"typings": "./dist/fluence.d.ts",
"scripts": {
"test": "mocha -r ts-node/register src/**/*.spec.ts",
"test-ts": "ts-mocha -p tsconfig.json src/**/*.spec.ts",
"package:build": "NODE_ENV=production webpack && tsc",
"compile": "tsc",
"start": "webpack-dev-server",
"build": "webpack"
},
"author": "Fluence Labs",
"license": "MIT",
"dependencies": {
"async": "3.2.0",
"base64-js": "1.3.1",
"bs58": "4.0.1",
"cids": "0.8.0",
"it-length-prefixed": "3.0.1",
"it-pipe": "1.1.0",
"libp2p": "0.27.4",
"libp2p-mplex": "0.9.5",
"libp2p-secio": "0.12.5",
"libp2p-websockets": "0.13.6",
"peer-id": "0.13.11",
"peer-info": "0.17.5"
},
"devDependencies": {
"@types/base64-js": "1.2.5",
"@types/bs58": "4.0.1",
"@types/chai": "4.2.11",
"@types/mocha": "7.0.2",
"assert": "2.0.0",
"chai": "4.2.0",
"clean-webpack-plugin": "3.0.0",
"libp2p-ts": "https://github.com/fluencelabs/libp2p-ts.git",
"mocha": "7.1.1",
"ts-loader": "6.2.2",
"ts-mocha": "^7.0.0",
"typescript": "3.8.3",
"webpack": "4.42.1",
"webpack-cli": "3.3.11",
"webpack-dev-server": "3.10.3"
}
}

150
src/address.ts Normal file
View File

@ -0,0 +1,150 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as PeerId from "peer-id";
import {encode} from "bs58"
export interface Address {
protocols: Protocol[]
}
export interface Protocol {
protocol: ProtocolType,
value?: string
}
export enum ProtocolType {
Service = "service",
Peer = "peer",
Signature = "signature",
Client = "client"
}
const PROTOCOL = "fluence:";
export function addressToString(address: Address): string {
let addressStr = PROTOCOL;
for (let addr of address.protocols) {
addressStr = addressStr + "/" + addr.protocol;
if (addr.value) {
addressStr = addressStr + "/" + addr.value;
}
}
return addressStr;
}
function protocolWithValue(protocol: ProtocolType, protocolIterator: IterableIterator<[number, string]>): Protocol {
let protocolValue = protocolIterator.next().value;
if (!protocolValue || !protocolValue[1]) {
throw Error(`protocol '${protocol}' should be with a value`)
}
return {protocol: protocol, value: protocolValue[1]};
}
export function parseProtocol(protocol: string, protocolIterator: IterableIterator<[number, string]>): Protocol {
protocol = protocol.toLocaleLowerCase();
switch (protocol) {
case ProtocolType.Service:
return protocolWithValue(protocol, protocolIterator);
case ProtocolType.Client:
return protocolWithValue(protocol, protocolIterator);
case ProtocolType.Peer:
return protocolWithValue(protocol, protocolIterator);
case ProtocolType.Signature:
return protocolWithValue(protocol, protocolIterator);
default:
throw Error("cannot parse protocol. Should be 'service|peer|client|signature'");
}
}
export async function createRelayAddress(relay: string, peerId: PeerId, withSig: boolean): Promise<Address> {
let protocols = [
{protocol: ProtocolType.Peer, value: relay},
{protocol: ProtocolType.Client, value: peerId.toB58String()}
];
if (withSig) {
let str = addressToString({protocols: protocols}).replace(PROTOCOL, "");
let signature = await peerId.privKey.sign(Buffer.from(str));
let signatureStr = encode(signature);
protocols.push({protocol: ProtocolType.Signature, value: signatureStr});
}
return {
protocols: protocols
}
}
export function createServiceAddress(service: string): Address {
let protocol = {protocol: ProtocolType.Service, value: service};
return {
protocols: [protocol]
}
}
export function createPeerAddress(peer: string): Address {
let protocol = {protocol: ProtocolType.Peer, value: peer};
return {
protocols: [protocol]
}
}
export function parseAddress(str: string): Address {
str = str.replace("fluence:", "");
// delete leading slashes
str = str.replace(/^\/+/, '');
let parts = str.split("/");
if (parts.length < 1) {
throw Error("address parts should not be empty")
}
let protocols: Protocol[] = [];
let partsEntries: IterableIterator<[number, string]> = parts.entries();
while (true) {
let result = partsEntries.next();
if (result.done) break;
let protocol = parseProtocol(result.value[1], partsEntries);
protocols.push(protocol);
}
return {
protocols: protocols
}
}

55
src/fluence.ts Normal file
View File

@ -0,0 +1,55 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import * as PeerInfo from "peer-info";
import * as PeerId from "peer-id";
import Multiaddr from "multiaddr"
import {FluenceClient} from "./fluence_client";
export default class Fluence {
/**
* Generates new peer id with Ed25519 private key.
*/
static async generatePeerId(): Promise<PeerId> {
return await PeerId.create({keyType: "Ed25519"});
}
/**
* Connect to Fluence node.
*
* @param multiaddr must contain host peer id
* @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> {
let peerInfo = await PeerInfo.create(peerId);
let client = new FluenceClient(peerInfo);
await client.connect(multiaddr);
return client;
}
}

265
src/fluence_client.ts Normal file
View File

@ -0,0 +1,265 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import {Address, createRelayAddress, ProtocolType} from "./address";
import {callToString, FunctionCall, genUUID, makeFunctionCall,} from "./function_call";
import * as PeerId from "peer-id";
import {Services} from "./services";
import Multiaddr from "multiaddr"
import {Subscriptions} from "./subscriptions";
import * as PeerInfo from "peer-info";
import {FluenceConnection} from "./fluence_connection";
export class FluenceClient {
private readonly selfPeerInfo: PeerInfo;
readonly selfPeerIdStr: string;
private connection: FluenceConnection;
private services: Services = new Services();
private subscriptions: Subscriptions = new Subscriptions();
constructor(selfPeerInfo: PeerInfo) {
this.selfPeerInfo = selfPeerInfo;
this.selfPeerIdStr = selfPeerInfo.id.toB58String();
}
/**
* Makes call with response from function. Without reply_to field.
*/
private static responseCall(target: Address, args: any): FunctionCall {
return makeFunctionCall(genUUID(), target, args, undefined, "response");
}
/**
* Waits a response that match the predicate.
*
* @param predicate will be applied to each incoming call until it matches
*/
waitResponse(predicate: (args: any, target: Address, replyTo: Address) => (boolean | undefined)): Promise<any> {
return new Promise((resolve, reject) => {
// 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)) {
resolve(args);
return true;
}
return false;
});
});
}
/**
* Send call and wait a response.
*
* @param target receiver
* @param args message in the call
* @param predicate will be applied to each incoming call until it matches
*/
async sendCallWaitResponse(target: Address, args: any, predicate: (args: any, target: Address, replyTo: Address) => (boolean | undefined)): Promise<any> {
await this.sendCall(target, args, true);
return this.waitResponse(predicate);
}
/**
* Send call and forget.
*
* @param target receiver
* @param args message in the call
* @param reply add a `replyTo` field or not
* @param name common field for debug purposes
*/
async sendCall(target: Address, args: any, reply?: boolean, name?: string) {
if (this.connection && this.connection.isConnected()) {
await this.connection.sendFunctionCall(target, args, reply, name);
} else {
throw Error("client is not connected")
}
}
/**
* Send call to the service.
*
* @param serviceId
* @param args message to the service
* @param name common field for debug purposes
*/
async sendServiceCall(serviceId: string, args: any, name?: string) {
if (this.connection && this.connection.isConnected()) {
await this.connection.sendServiceCall(serviceId, args, name);
} else {
throw Error("client is not connected")
}
}
/**
* Send call to the service and wait a response matches predicate.
*
* @param serviceId
* @param args message to the service
* @param predicate will be applied to each incoming call until it matches
*/
async sendServiceCallWaitResponse(serviceId: string, args: any, predicate: (args: any, target: Address, replyTo: Address) => (boolean | undefined)): Promise<any> {
await this.sendServiceCall(serviceId, args);
return await this.waitResponse(predicate);
}
/**
* Handle incoming call.
* If FunctionCall returns - we should send it as a response.
*/
handleCall(): (call: FunctionCall) => FunctionCall | undefined {
let _this = this;
return (call: FunctionCall) => {
console.log("FunctionCall received:");
// 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) {
case ProtocolType.Service:
try {
// call of the service, service should handle response sending, error handling, requests to other services
let applied = _this.services.applyToService(lastProtocol.value, call);
// if the request hasn't been applied, there is no such service. Return an error.
if (!applied) {
console.log(`there is no service ${lastProtocol.value}`);
return FluenceClient.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 FluenceClient.responseCall(call.reply_to, {reason: `error on execution: ${e}`, msg: call});
}
return undefined;
case ProtocolType.Client:
if (lastProtocol.value === _this.selfPeerIdStr) {
console.log(`relay call: ${call}`);
} else {
console.warn(`this relay call is not for me: ${callToString(call)}`);
return FluenceClient.responseCall(call.reply_to, {reason: `this relay call is not for me`, msg: call});
}
return undefined;
case ProtocolType.Peer:
if (lastProtocol.value === this.selfPeerIdStr) {
console.log(`peer call: ${call}`);
} else {
console.warn(`this peer call is not for me: ${callToString(call)}`);
return FluenceClient.responseCall(call.reply_to, {reason: `this relay call is not for me`, msg: call});
}
return undefined;
}
}
}
/**
* Sends a call to register the service_id.
*/
async registerService(serviceId: string, fn: (req: FunctionCall) => void) {
await this.connection.registerService(serviceId);
this.services.addService(serviceId, fn)
}
// 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_id.
*/
async unregisterService(serviceId: string) {
if (this.services.deleteService(serviceId)) {
console.warn("unregister is not implemented yet (service: ${serviceId}")
// TODO unregister in fluence network when it will be supported
// let regMsg = makeRegisterMessage(serviceId, PeerId.createFromB58String(this.nodePeerId));
// await this.sendFunctionCall(regMsg);
}
}
/**
* 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();
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.selfPeerInfo.id, true);
let connection = new FluenceConnection(multiaddr, peerId, this.selfPeerInfo, relayAddress, this.handleCall());
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()) {
await this.connection.registerService(service);
}
}
}
}

206
src/fluence_connection.ts Normal file
View File

@ -0,0 +1,206 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import {Address} from "./address";
import {
callToString,
FunctionCall,
genUUID,
makeCall,
makeFunctionCall,
makePeerCall,
makeRegisterMessage,
makeRelayCall,
parseFunctionCall
} from "./function_call";
import * as PeerId from "peer-id";
import * as PeerInfo from "peer-info";
import Websockets from "libp2p-websockets";
import Mplex from "libp2p-mplex";
import SECIO from "libp2p-secio";
import Peer from "libp2p";
import {decode, encode} from "it-length-prefixed";
import pipe from "it-pipe";
import Multiaddr from "multiaddr";
export const PROTOCOL_NAME = '/fluence/faas/1.0.0';
enum Status {
Initializing = "Initializing",
Connected = "Connected",
Disconnected = "Disconnected"
}
export class FluenceConnection {
private readonly selfPeerInfo: PeerInfo;
readonly replyToAddress: Address;
private node: LibP2p;
private readonly address: Multiaddr;
private readonly nodePeerId: PeerId;
private readonly selfPeerId: string;
private readonly handleCall: (call: FunctionCall) => FunctionCall | undefined;
constructor(multiaddr: Multiaddr, hostPeerId: PeerId, selfPeerInfo: PeerInfo, replyToAddress: Address, handleCall: (call: FunctionCall) => FunctionCall | undefined) {
this.selfPeerInfo = selfPeerInfo;
this.handleCall = handleCall;
this.selfPeerId = selfPeerInfo.id.toB58String();
this.address = multiaddr;
this.nodePeerId = hostPeerId;
this.replyToAddress = replyToAddress
}
async connect() {
let peerInfo = this.selfPeerInfo;
this.node = await Peer.create({
peerInfo,
config: {},
modules: {
transport: [Websockets],
streamMuxer: [Mplex],
connEncryption: [SECIO],
peerDiscovery: []
},
});
await this.startReceiving();
}
isConnected() {
return this.status === Status.Connected
}
// connection status. If `Disconnected`, it cannot be reconnected
private status: Status = Status.Initializing;
/**
* Sends remote service_id call.
*/
async sendServiceCall(serviceId: string, args: any, name?: string) {
let regMsg = makeCall(serviceId, args, this.replyToAddress, name);
await this.sendCall(regMsg);
}
/**
* Sends custom message to the peer.
*/
async sendPeerCall(peer: string, msg: any, name?: string) {
let regMsg = makePeerCall(PeerId.createFromB58String(peer), msg, this.replyToAddress, name);
await this.sendCall(regMsg);
}
/**
* Sends custom message to the peer through relay.
*/
async sendRelayCall(peer: string, relay: string, msg: any, name?: string) {
let regMsg = await makeRelayCall(PeerId.createFromB58String(peer), PeerId.createFromB58String(relay), msg, this.replyToAddress, name);
await this.sendCall(regMsg);
}
private async startReceiving() {
if (this.status === Status.Initializing) {
await this.node.start();
console.log("dialing to the node with address: " + this.node.peerInfo.id.toB58String());
await this.node.dial(this.address);
let _this = this;
this.node.handle([PROTOCOL_NAME], async ({connection, stream}) => {
pipe(
stream.source,
decode(),
async function (source: AsyncIterable<string>) {
for await (const msg of source) {
try {
console.log(_this.selfPeerId);
let call = parseFunctionCall(msg);
let response = _this.handleCall(call);
// send a response if it exists, do nothing otherwise
if (response) {
await _this.sendCall(response);
}
} catch(e) {
console.log("error on handling a new incoming message: " + e);
}
}
}
)
});
this.status = Status.Connected;
} else {
throw Error(`can't start receiving. Status: ${this.status}`);
}
}
private checkConnectedOrThrow() {
if (this.status !== Status.Connected) {
throw Error(`connection is in ${this.status} state`)
}
}
async disconnect() {
await this.node.stop();
this.status = Status.Disconnected;
}
private async sendCall(call: FunctionCall) {
let callStr = callToString(call);
console.log("send function call: " + callStr);
console.log(call);
// create outgoing substream
const conn = await this.node.dialProtocol(this.address, PROTOCOL_NAME) as {stream: Stream; protocol: string};
pipe(
[callStr],
// at first, make a message varint
encode(),
conn.stream.sink,
);
}
/**
* Send FunctionCall to the connected node.
*/
async sendFunctionCall(target: Address, args: any, reply?: boolean, name?: string) {
this.checkConnectedOrThrow();
let replyTo;
if (reply) replyTo = this.replyToAddress;
let call = makeFunctionCall(genUUID(), target, args, replyTo, name);
await this.sendCall(call);
}
async registerService(serviceId: string) {
let regMsg = await makeRegisterMessage(serviceId, this.nodePeerId, this.selfPeerInfo.id);
await this.sendCall(regMsg);
}
}

134
src/function_call.ts Normal file
View File

@ -0,0 +1,134 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import {
createPeerAddress,
createRelayAddress,
createServiceAddress,
Address, addressToString, parseAddress
} from "./address";
import * as PeerId from "peer-id";
export interface FunctionCall {
uuid: string,
target: Address,
reply_to?: Address,
arguments: any,
name?: string,
action: "FunctionCall"
}
export function callToString(call: FunctionCall) {
let obj: any = {...call};
if (obj.reply_to) {
obj.reply_to = addressToString(obj.reply_to);
}
obj.target = addressToString(obj.target);
return JSON.stringify(obj)
}
export function makeFunctionCall(uuid: string, target: Address, args: object, replyTo?: Address, name?: string): FunctionCall {
return {
uuid: uuid,
target: target,
reply_to: replyTo,
arguments: args,
name: name,
action: "FunctionCall"
}
}
export function parseFunctionCall(str: string): FunctionCall {
let json = JSON.parse(str);
console.log(JSON.stringify(json, undefined, 2));
let replyTo: Address;
if (json.reply_to) replyTo = parseAddress(json.reply_to);
if (!json.uuid) throw Error(`there is no 'uuid' field in json.\n${str}`);
if (!json.target) throw Error(`there is no 'uuid' field in json.\n${str}`);
let target = parseAddress(json.target);
return {
uuid: json.uuid,
target: target,
reply_to: replyTo,
arguments: json.arguments,
name: json.name,
action: "FunctionCall"
}
}
export function genUUID() {
let date = new Date();
return date.toISOString()
}
/**
* Message to peer through relay
*/
export async function makeRelayCall(client: PeerId, relay: PeerId, msg: any, replyTo?: Address, name?: string): Promise<FunctionCall> {
let relayAddress = await createRelayAddress(relay.toB58String(), client, false);
return makeFunctionCall(genUUID(), relayAddress, msg, replyTo, name);
}
/**
* Message to peer
*/
export function makePeerCall(client: PeerId, msg: any, replyTo?: Address, name?: string): FunctionCall {
let peerAddress = createPeerAddress(client.toB58String());
return makeFunctionCall(genUUID(), peerAddress, msg, replyTo, name);
}
/**
* Message to call remote service_id
*/
export function makeCall(functionId: string, args: any, replyTo?: Address, name?: string): FunctionCall {
let target = createServiceAddress(functionId);
return makeFunctionCall(genUUID(), target, args, replyTo, name);
}
/**
* Message to register new service_id.
*/
export async function makeRegisterMessage(serviceId: string, relayPeerId: PeerId, selfPeerId: PeerId): Promise<FunctionCall> {
let target = createServiceAddress("provide");
let replyTo = await createRelayAddress(relayPeerId.toB58String(), selfPeerId, true);
return makeFunctionCall(genUUID(), target, {service_id: serviceId}, replyTo, "provide service_id");
}
export function makeUnregisterMessage(serviceId: string, peerId: PeerId): FunctionCall {
let target = createPeerAddress(peerId.toB58String());
return makeFunctionCall(genUUID(), target, {key: serviceId}, undefined, "unregister");
}

57
src/services.ts Normal file
View File

@ -0,0 +1,57 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import {FunctionCall} from "./function_call";
export class Services {
private services: Map<string, (req: FunctionCall) => void> = new Map();
constructor() {}
addService(serviceId: string, callback: (req: FunctionCall) => void): void {
this.services.set(serviceId, callback);
}
getAllServices(): Map<string, (req: FunctionCall) => void> {
return this.services;
}
deleteService(serviceId: string): boolean {
return this.services.delete(serviceId)
}
// could throw error from service callback
// returns true if the call was applied
applyToService(serviceId: string, call: FunctionCall): boolean {
let service = this.services.get(serviceId);
if (service) {
service(call);
return true;
} else {
return false;
}
}
}

50
src/subscriptions.ts Normal file
View File

@ -0,0 +1,50 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
import {FunctionCall} from "./function_call";
import {Address} from "./address";
export class Subscriptions {
private subscriptions: ((args: any, target: Address, replyTo: Address) => (boolean | undefined))[] = [];
constructor() {}
/**
* Subscriptions will be applied to all peer and relay messages.
* If subscription returns true, delete subscription.
* @param f
*/
subscribe(f: (args: any, target: Address, replyTo: Address) => (boolean | undefined)) {
this.subscriptions.push(f);
}
/**
* Apply call to all subscriptions and delete subscriptions that return `true`.
* @param call
*/
applyToSubscriptions(call: FunctionCall) {
// if subscription return false - delete it from subscriptions
this.subscriptions = this.subscriptions.filter(callback => !callback(call.arguments, call.target, call.reply_to))
}
}

152
src/test/address.spec.ts Normal file
View File

@ -0,0 +1,152 @@
import {
createPeerAddress,
createRelayAddress,
createServiceAddress,
addressToString,
parseAddress
} from "../address";
import {expect} from 'chai';
import 'mocha';
import * as PeerId from "peer-id";
import {callToString, genUUID, makeFunctionCall, parseFunctionCall} from "../function_call";
import Fluence from "../fluence";
describe("Typescript usage suite", () => {
it("should throw an error, if protocol will be without value", () => {
expect(() => parseAddress("/peer/")).to.throw(Error);
});
it("should be able to convert service_id address to and from string", () => {
let addr = createServiceAddress("service_id-1");
let str = addressToString(addr);
let parsed = parseAddress(str);
expect(parsed).to.deep.equal(addr)
});
it("should be able to convert peer address to and from string", () => {
let pid = PeerId.createFromB58String("QmXduoWjhgMdx3rMZXR3fmkHKdUCeori9K1XkKpqeF5DrU");
let addr = createPeerAddress(pid.toB58String());
let str = addressToString(addr);
let parsed = parseAddress(str);
expect(parsed).to.deep.equal(addr)
});
it("should be able to convert relay address to and from string", async () => {
let pid = await PeerId.create();
let relayid = await PeerId.create();
let addr = await createRelayAddress(relayid.toB58String(), pid, true);
let str = addressToString(addr);
let parsed = parseAddress(str);
expect(parsed).to.deep.equal(addr)
});
it("should be able to convert function call to and from string", async () => {
let pid = await PeerId.create();
let relayid = await PeerId.create();
let addr = await createRelayAddress(relayid.toB58String(), pid, true);
let pid2 = await PeerId.create();
let addr2 = createPeerAddress(pid.toB58String());
let functionCall = makeFunctionCall(
"123",
addr2,
{
arg1: "123",
arg2: 3,
arg4: [1, 2, 3]
},
addr,
"2444"
);
let str = callToString(functionCall);
let parsed = parseFunctionCall(str);
expect(parsed).to.deep.equal(functionCall);
let functionCallWithOptional = makeFunctionCall(
"123",
addr,
{
arg1: "123",
arg2: 3,
arg4: [1, 2, 3]
}
);
let str2 = callToString(functionCallWithOptional);
let parsed2 = parseFunctionCall(str2);
expect(parsed2).to.deep.equal(functionCallWithOptional)
});
it("integration test", async function () {
this.timeout(5000);
await testCalculator();
});
});
const delay = (ms: number) => new Promise(res => setTimeout(res, ms));
// Shows how to register and call new service in Fluence network
export async function testCalculator() {
let key1 = await Fluence.generatePeerId();
let key2 = await Fluence.generatePeerId();
// connect to two different nodes
let cl1 = await Fluence.connect("/dns4/104.248.25.59/tcp/9003/ws/p2p/12D3KooWBUJifCTgaxAUrcM9JysqCcS4CS8tiYH5hExbdWCAoNwb", key1);
let cl2 = await Fluence.connect("/ip4/104.248.25.59/tcp/9002/ws/p2p/12D3KooWHk9BjDQBUqnavciRPhAYFvqKBe4ZiPPvde7vDaqgn5er", key2);
// service name that we will register with one connection and call with another
let serviceId = "sum-calculator-" + genUUID();
// register service that will add two numbers and send a response with calculation result
await cl1.registerService(serviceId, async (req) => {
console.log("message received");
console.log(req);
console.log("send response");
let message = {msgId: req.arguments.msgId, result: req.arguments.one + req.arguments.two};
await cl1.sendCall(req.reply_to, message);
});
// msgId is to identify response
let msgId = "calculate-it-for-me";
let req = {one: 12, two: 23, msgId: msgId};
let predicate: (args: any) => boolean | undefined = (args: any) => args.msgId && args.msgId === msgId;
// send call to `sum-calculator` service with two numbers
let response = await cl2.sendServiceCallWaitResponse(serviceId, req, predicate);
let result = response.result;
console.log(`calculation result is: ${result}`);
await cl1.connect("/dns4/relay01.fluence.dev/tcp/19001/wss/p2p/12D3KooWEXNUbCXooUwHrHBbrmjsrpHXoEphPwbjQXEGyzbqKnE9");
await delay(1000);
// send call to `sum-calculator` service with two numbers
await cl2.sendServiceCall(serviceId, req, "calculator request");
let response2 = await cl2.sendServiceCallWaitResponse(serviceId, req, predicate);
let result2 = await response2.result;
console.log(`calculation result AFTER RECONNECT is: ${result2}`);
}

30
tsconfig.json Normal file
View File

@ -0,0 +1,30 @@
{
"compilerOptions": {
"typeRoots": [
"./node_modules/@types",
"./node_modules/libp2p-ts/types",
"./types"
],
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"strictFunctionTypes": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"pretty": true,
"target": "esnext",
"module": "commonjs",
"moduleResolution": "node",
"declaration": true,
"strict": true,
"strictNullChecks": false,
"esModuleInterop": true,
"baseUrl": "."
},
"exclude": [
"node_modules",
"dist",
"bundle"
],
"include": ["src/**/*"]
}

27
types/ipfs-only-hash/index.d.ts vendored Normal file
View File

@ -0,0 +1,27 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
declare module 'ipfs-only-hash' {
export function of(data: Buffer): Promise<string>
}

28
types/it-length-prefixed/index.d.ts vendored Normal file
View File

@ -0,0 +1,28 @@
/*
* MIT License
*
* Copyright (c) 2020 Fluence Labs Limited
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all
* copies or substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
* SOFTWARE.
*/
declare module 'it-length-prefixed' {
export function decode(): any
export function encode(): any
}

50
webpack.config.js Normal file
View File

@ -0,0 +1,50 @@
const path = require('path');
const webpack = require('webpack');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');
const production = (process.env.NODE_ENV === 'production');
const config = {
entry: {
app: ['./src/fluence.ts']
},
module: {
rules: [
{
test: /\.tsx?$/,
loader: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: [ '.tsx', '.ts', '.js']
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'bundle'),
},
node: {
fs: 'empty'
},
plugins: [
new CleanWebpackPlugin(),
]
};
if (production) {
config.mode = 'production';
} else {
config.mode = 'development';
config.devtool = 'inline-source-map';
config.devServer = {
contentBase: './bundle',
hot: false
};
config.plugins = [
...config.plugins,
new webpack.HotModuleReplacementPlugin()
];
}
module.exports = config;