In-depth

Intro

In this section we will cover the Fluence JS in-depth.

Fluence

@fluencelabs/fluence exports a facade Fluence which provides all the needed functionality for the most uses cases. It defined 4 functions:
    start: Start the default peer.
    stop: Stops the default peer
    getStatus: Gets the status of the default peer. This includes connection
    getPeer: Gets the default Fluence Peer instance (see below)
Under the hood Fluence facade calls the corresponding method on the default instance of FluencePeer. This instance is passed to the Aqua-compiler generated functions by default.

FluencePeer class

The second export @fluencelabs/fluence package is FluencePeer class. It is useful in scenarios when the application need to run several different peer at once. The overall workflow with the FluencePeer is the following:
    1.
    Create an instance of the peer
    2.
    Starting the peer
    3.
    Using the peer in the application
    4.
    Stopping the peer
To create a new peer simple instantiate the FluencePeer class:
1
const peer = new FluencePeer();
Copied!
The constructor simply creates a new object and does not initialize any workflow. The start function starts the Aqua VM, initializes the default call service handlers and (optionally) connect to the Fluence network. The function takes an optional object specifying additional peer configuration. On option you will be using a lot is connectTo. It tells the peer to connect to a relay. For example:
1
await peer.star({
2
connectTo: krasnodar[0],
3
});
Copied!
connects to the first node of the Krasnodar network. You can find the officially maintained list networks in the @fluencelabs/fluence-network-environment package. The full list of supported options is described in the API reference
1
await peer.stop();
Copied!

Using multiple peers in one application

The peer by itself does not do any useful work. You should take advantage of functions generated by the Aqua compiler.
If your application needs several peers, you should create a separate FluencePeer instance for each of them. The generated functions accept the peer as the first argument. For example:
1
import { FluencePeer } from "@fluencelabs/fluence";
2
import {
3
registerSomeService,
4
someCallableFunction,
5
} from "./_aqua/someFunction";
6
7
async function main() {
8
const peer1 = new FluencePeer();
9
const peer2 = new FluencePeer();
10
11
// Don't forget to initialize peers
12
await peer1.start({
13
connectTo: relay,
14
});
15
await peer2.start({
16
connectTo: relay,
17
});
18
19
// ... more application logic
20
21
// Pass the peer as the first argument
22
// ||
23
// \/
24
registerSomeService(peer1, {
25
handler: async (str) => {
26
console.log("Called service on peer 1: " str);
27
},
28
});
29
30
// Pass the peer as the first argument
31
// ||
32
// \/
33
registerSomeService(peer2, {
34
handler: async (str) => {
35
console.log("Called service on peer 2: " str);
36
},
37
});
38
39
// Pass the peer as the first argument
40
// ||
41
// \/
42
await someCallableFunction(peer1, arg1, arg2, arg3);
43
44
45
await peer1.stop();
46
await peer2.stop();
47
}
48
49
// ... more application logic
Copied!
It is possible to combine usage of the default peer with another one. Pay close attention to which peer you are calling the functions against.
1
// Registering handler for the default peerS
2
registerSomeService({
3
handler: async (str) => {
4
console.log("Called against the default peer: " str);
5
},
6
});
7
8
// Pay close attention to this
9
// ||
10
// \/
11
registerSomeService(someOtherPeer, {
12
handler: async (str) => {
13
console.log("Called against the peer named someOtherPeer: " str);
14
},
15
});
Copied!

Understanding the Aqua compiler output

Aqua compiler emits TypeScript or JavaScript which in turn can be called from a js-based environment. The compiler outputs code for the following entities:
    1.
    Exported func declarations are turned into callable async functions
    2.
    Exported service declarations are turned into functions which register callback handler in a typed manner
    3.
    For every exported service the compiler generated it's interface under the name {serviceName}Def

Function definitions

For every exported function definition in aqua the compiler generated two overloads. One accepting the FluencePeer instance as the first argument, and one without it. Otherwise arguments are the same and correspond to the arguments of aqua functions. The last argument is always an optional config object with the following properties:
    ttl: Optional parameter which specify TTL (time to live) of particle with execution logic for the function
The return type is always a promise of the aqua function return type. If the function does not return anything, the return type will be Promise<void>.
Consider the following example:
1
func myFunc(arg0: string, arg1: string):
2
-- implementation
Copied!
The compiler will generate the following overloads:
1
export async function myFunc(
2
arg0: string,
3
arg1: string,
4
config?: { ttl?: number }
5
): Promise<void>;
6
7
export async function callMeBack(
8
peer: FluencePeer,
9
arg0: string,
10
arg1: string,
11
config?: { ttl?: number }
12
): Promise<void>;
Copied!

Service definitions

1
service ServiceName:
2
-- service interface
Copied!
For every exported service declaration the compiler will generate two entities: service interface under the name {serviceName}Def and a function named register{serviceName} with several overloads. First let's describe the most complete one using the following example:
1
export interface ServiceNameDef {
2
//... service function definitions
3
}
4
5
export function registerServiceName(
6
peer: FluencePeer,
7
serviceId: string,
8
service: ServiceNameDef
9
): void;
Copied!
    peer - the Fluence Peer instance where the handler should be registered. The peer can be omitted. In that case the default Fluence Peer will be used instead
    serviceId - the name of the service id. If the service was defined with the default service id in aqua code, this argument can be omitted.
    service - the handler for the service.
Depending on whether or not the services was defined with the default id the number of overloads will be different. In the case it is defined, there would be four overloads:
1
// (1)
2
export function registerServiceName(
3
//
4
service: ServiceNameDef
5
): void;
6
7
// (2)
8
export function registerServiceName(
9
serviceId: string,
10
service: ServiceNameDef
11
): void;
12
13
// (3)
14
export function registerServiceName(
15
peer: FluencePeer,
16
service: ServiceNameDef
17
): void;
18
19
// (4)
20
export function registerServiceName(
21
peer: FluencePeer,
22
serviceId: string,
23
service: ServiceNameDef
24
): void;
Copied!
    1.
    Uses default Fluence Peer and the default id taken from aqua definition
    2.
    Uses default Fluence Peer and specifies the service id explicitly
    3.
    The default id is taken from aqua definition. The peer is specified explicitly
    4.
    Specifying both peer and the service id.
If the default id is not defined in aqua code the overloads will exclude ones without service id:
1
// (1)
2
export function registerServiceName(
3
serviceId: string,
4
service: ServiceNameDef
5
): void;
6
7
// (2)
8
export function registerServiceName(
9
peer: FluencePeer,
10
serviceId: string,
11
service: ServiceNameDef
12
): void;
Copied!
    1.
    Uses default Fluence Peer and specifies the service id explicitly
    2.
    Specifying both peer and the service id.

Service interface

The service interface type follows closely the definition in aqua code. It has the form of the object which keys correspond to the names of service members and the values are functions of the type translated from aqua definition (see Type convertion). For example, for the following aqua definition:
1
service Calc("calc"):
2
add(n: f32)
3
subtract(n: f32)
4
multiply(n: f32)
5
divide(n: f32)
6
reset()
7
getResult() -> f32
Copied!
The typescript interface will be:
1
export interface CalcDef {
2
add: (n: number, callParams: CallParams<"n">) => void | Promise<void>;
3
subtract: (n: number, callParams: CallParams<"n">) => void | Promise<void>;
4
multiply: (n: number, callParams: CallParams<"n">) => void | Promise<void>;
5
divide: (n: number, callParams: CallParams<"n">) => void | Promise<void>;
6
reset: (callParams: CallParams<null>) => void | Promise<void>;
7
getResult: (callParams: CallParams<null>) => number | Promise<number>;
8
}
Copied!
CallParams will be described later in the section

Type conversion

Basic types conversion is pretty much straightforward:
    string is converted to string in typescript
    bool is converted to boolean in typescript
    All number types (u8, u16, u32, u64, s8, s16, s32, s64, f32, f64) are converted to number in typescript
Arrow types translate to functions in typescript which have their arguments translated to typescript types. In addition to arguments defined in aqua, typescript counterparts have an additional argument for call params. For the majority of use cases this parameter is not needed and can be omitted.
The type conversion works the same way for service and func definitions. For example a func with a callback might look like this:
1
func callMeBack(callback: string, i32 -> ()):
2
callback("hello, world", 42)
Copied!
The type for callback argument will be:
1
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void | Promise<void>,
Copied!
For the service definitions arguments are named (see calc example above)

Using asynchronous code in callbacks

Typescript code generated by Aqua compiler has two scenarios where a user should specify a callback function. These are services and callback arguments of function in aqua. If you look at the return type of the generated function you will see a union of callback return type and the promise with this type, e.g string | Promise<string>. Fluence-js supports both sync and async version of callbacks and figures out which one is used under the hood. The callback be made asynchronous like any other function in javascript: either return a Promise or mark it with async keyword to take advantage of async-await pattern.
For example:
1
func withCallback(callback: string -> ()):
2
callback()
3
4
service MyService:
5
callMe(string)
Copied!
Here we are returning a promise
1
registerMyService({
2
callMe: (arg): Promise<void> => {
3
return new Promise((resolve) => {
4
setTimeout(() => {
5
console.log("I'm running 3 seconds after call");
6
resolve();
7
}, 3000);
8
});
9
},
10
});
Copied!
And here we are using async-await pattern
1
await withCallback(async (arg) => {
2
const data = await getStuffFromDatabase(arg);
3
console.log("");
4
});
Copied!

Call params and tetraplets

Each service call is accompanied by additional information specific to Fluence Protocol. Including initPeerId - the peer which initiated the particle execution, particle signature and most importantly security tetraplets. All this data is contained inside the last callParams argument in every generated function definition. These data is passed to the handler on each function call can be used in the application.
Tetraplets have the form of:
1
{
2
argName0: SecurityTetraplet[],
3
argName1: SecurityTetraplet[],
4
// ...
5
}
Copied!
To learn more about tetraplets and application security see Security
To see full specification of CallParams type see API reference
Last modified 4d ago