Fix recursive (#136)

This commit is contained in:
Pavel 2022-03-17 07:00:19 +03:00 committed by GitHub
parent f8abe728c0
commit a0a7a9e19b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1055 additions and 1103 deletions

View File

@ -1,211 +0,0 @@
import { Fluence, FluencePeer } from '../../..';
import { Particle } from '../../../internal/Particle';
import { registerHandlersHelper } from '../../util';
import { callMeBack, registerHelloWorld } from './gen1';
import { callFunction } from '../../../internal/compilerSupport/v2';
import { handleTimeout } from '../../../internal/utils';
describe('Compiler support infrastructure tests', () => {
it('Compiled code for function should work', async () => {
// arrange
await Fluence.start();
// act
const res = new Promise((resolve) => {
callMeBack((arg0, arg1, params) => {
resolve({
arg0: arg0,
arg1: arg1,
arg0Tetraplet: params.tetraplets.arg0[0], // completion should work here
arg1Tetraplet: params.tetraplets.arg1[0], // completion should work here
});
});
});
// assert
expect(await res).toMatchObject({
arg0: 'hello, world',
arg1: 42,
arg0Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
arg1Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
});
await Fluence.stop();
});
it('Compiled code for service should work', async () => {
// arrange
await Fluence.start();
// act
const helloPromise = new Promise((resolve) => {
registerHelloWorld('hello_world', {
sayHello: (s, params) => {
const tetraplet = params.tetraplets.s; // completion should work here
resolve(s);
},
getNumber: (params) => {
// ctx.tetraplets should be {}
return 42;
},
});
});
const getNumberPromise = new Promise<string>((resolve, reject) => {
const script = `
(seq
(seq
(call %init_peer_id% ("hello_world" "sayHello") ["hello world!"])
(call %init_peer_id% ("hello_world" "getNumber") [] result)
)
(call %init_peer_id% ("callback" "callback") [result])
)`;
const particle = Particle.createNew(script);
registerHandlersHelper(Fluence.getPeer(), particle, {
callback: {
callback: (args) => {
const [val] = args;
resolve(val);
},
},
});
Fluence.getPeer().internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
expect(await helloPromise).toBe('hello world!');
expect(await getNumberPromise).toBe(42);
await Fluence.stop();
});
it('Compiled code for function should work with another peer', async () => {
// arrange
const peer = new FluencePeer();
await peer.start();
// act
const res = new Promise((resolve) => {
callMeBack(peer, (arg0, arg1, params) => {
resolve({
arg0: arg0,
arg1: arg1,
arg0Tetraplet: params.tetraplets.arg0[0], // completion should work here
arg1Tetraplet: params.tetraplets.arg1[0], // completion should work here
});
});
});
// assert
expect(await res).toMatchObject({
arg0: 'hello, world',
arg1: 42,
arg0Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
arg1Tetraplet: {
function_name: '',
json_path: '',
// peer_pk: '12D3KooWMwDDVRPEn5YGrN5LvVFLjNuBmokaeKfpLUgxsSkqRwwv',
service_id: '',
},
});
await peer.stop();
});
it('Compiled code for service should work another peer', async () => {
// arrange
const anotherPeer = new FluencePeer();
await anotherPeer.start();
// act
const helloPromise = new Promise((resolve) => {
registerHelloWorld(anotherPeer, 'hello_world', {
sayHello: (s, params) => {
const tetraplet = params.tetraplets.s; // completion should work here
resolve(s);
},
getNumber: (params) => {
// ctx.tetraplets should be {}
return 42;
},
});
});
const getNumberPromise = new Promise<string>((resolve, reject) => {
const script = `
(seq
(seq
(call %init_peer_id% ("hello_world" "sayHello") ["hello world!"])
(call %init_peer_id% ("hello_world" "getNumber") [] result)
)
(call %init_peer_id% ("callback" "callback") [result])
)`;
const particle = Particle.createNew(script);
registerHandlersHelper(anotherPeer, particle, {
callback: {
callback: (args) => {
const [val] = args;
resolve(val);
},
},
});
anotherPeer.internals.initiateParticle(particle, handleTimeout(reject));
});
// assert
expect(await helloPromise).toBe('hello world!');
expect(await getNumberPromise).toBe(42);
await anotherPeer.stop();
});
it('Should throw error if particle with incorrect AIR script is initiated', async () => {
// arrange;
const anotherPeer = new FluencePeer();
await anotherPeer.start();
// act
const action = callFunction(
[anotherPeer],
{
functionName: 'dontcare',
argDefs: [],
returnType: { tag: 'void' },
names: {
relay: '-relay-',
getDataSrv: 'getDataSrv',
callbackSrv: 'callbackSrv',
responseSrv: 'callbackSrv',
responseFnName: 'response',
errorHandlingSrv: 'errorHandlingSrv',
errorFnName: 'error',
},
},
'incorrect air script',
);
// assert
await expect(action).rejects.toMatch(/incorrect air script/);
await anotherPeer.stop();
});
});

View File

@ -1,193 +0,0 @@
import { ResultCodes, RequestFlow, RequestFlowBuilder, CallParams } from '../../../internal/compilerSupport/v1';
import { Fluence, FluencePeer } from '../../../index';
/*
-- file to generate functions below from
service HelloWorld("default"):
sayHello(s: string)
getNumber() -> i32
func callMeBack(callback: string, i32 -> ()):
callback("hello, world", 42)
*/
/**
*
* This file is auto-generated. Do not edit manually: changes may be erased.
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* Aqua version: 0.3.1-231
*
*/
function missingFields(obj: any, fields: string[]): string[] {
return fields.filter((f) => !(f in obj));
}
// Services
export interface HelloWorldDef {
getNumber: (callParams: CallParams<null>) => number;
sayHello: (s: string, callParams: CallParams<'s'>) => void;
}
export function registerHelloWorld(service: HelloWorldDef): void;
export function registerHelloWorld(serviceId: string, service: HelloWorldDef): void;
export function registerHelloWorld(peer: FluencePeer, service: HelloWorldDef): void;
export function registerHelloWorld(peer: FluencePeer, serviceId: string, service: HelloWorldDef): void;
export function registerHelloWorld(...args: any) {
let peer: FluencePeer;
let serviceId: any;
let service: any;
if (FluencePeer.isInstance(args[0])) {
peer = args[0];
} else {
peer = Fluence.getPeer();
}
if (typeof args[0] === 'string') {
serviceId = args[0];
} else if (typeof args[1] === 'string') {
serviceId = args[1];
} else {
serviceId = 'default';
}
// Figuring out which overload is the service.
// If the first argument is not Fluence Peer and it is an object, then it can only be the service def
// If the first argument is peer, we are checking further. The second argument might either be
// an object, that it must be the service object
// or a string, which is the service id. In that case the service is the third argument
if (!FluencePeer.isInstance(args[0]) && typeof args[0] === 'object') {
service = args[0];
} else if (typeof args[1] === 'object') {
service = args[1];
} else {
service = args[2];
}
const incorrectServiceDefinitions = missingFields(service, ['getNumber', 'sayHello']);
if (!!incorrectServiceDefinitions.length) {
throw new Error(
'Error registering service HelloWorld: missing functions: ' +
incorrectServiceDefinitions.map((d) => "'" + d + "'").join(', '),
);
}
peer.internals.callServiceHandler.use((req, resp, next) => {
if (req.serviceId !== serviceId) {
next();
return;
}
if (req.fnName === 'getNumber') {
const callParams = {
...req.particleContext,
tetraplets: {},
};
resp.retCode = ResultCodes.success;
resp.result = service.getNumber(callParams);
}
if (req.fnName === 'sayHello') {
const callParams = {
...req.particleContext,
tetraplets: {
s: req.tetraplets[0],
},
};
resp.retCode = ResultCodes.success;
service.sayHello(req.args[0], callParams);
resp.result = {};
}
next();
});
}
// Functions
export function callMeBack(
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function callMeBack(
peer: FluencePeer,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function callMeBack(...args: any) {
let peer: FluencePeer;
let callback: any;
let config: any;
if (FluencePeer.isInstance(args[0])) {
peer = args[0];
callback = args[1];
config = args[2];
} else {
peer = Fluence.getPeer();
callback = args[0];
config = args[1];
}
let request: RequestFlow;
const promise = new Promise<void>((resolve, reject) => {
const r = new RequestFlowBuilder()
.disableInjections()
.withRawScript(
`
(xor
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(xor
(call %init_peer_id% ("callbackSrv" "callback") ["hello, world" 42])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)
`,
)
.configHandler((h) => {
h.on('getDataSrv', '-relay-', () => {
return peer.getStatus().relayPeerId;
});
h.use((req, resp, next) => {
if (req.serviceId === 'callbackSrv' && req.fnName === 'callback') {
const callParams = {
...req.particleContext,
tetraplets: {
arg0: req.tetraplets[0],
arg1: req.tetraplets[1],
},
};
resp.retCode = ResultCodes.success;
callback(req.args[0], req.args[1], callParams);
resp.result = {};
}
next();
});
h.onEvent('callbackSrv', 'response', (args) => {});
h.onEvent('errorHandlingSrv', 'error', (args) => {
const [err] = args;
reject(err);
});
})
.handleScriptError(reject)
.handleTimeout(() => {
reject('Request timed out for callMeBack');
});
if (config && config.ttl) {
r.withTTL(config.ttl);
}
request = r.build();
});
peer.internals.initiateFlow(request!);
return Promise.race([promise, Promise.resolve()]);
}

View File

@ -1,519 +0,0 @@
import { CallParams, registerService, callFunction } from '../../../internal/compilerSupport/v2';
import { FluencePeer } from '../../../index';
/*
-- file to generate functions below from
service ServiceWithDefaultId("defaultId"):
hello(s: string)
service ServiceWithOUTDefaultId:
hello(s: string)
service MoreMembers:
member1()
member2(s1: string)
member3(s1: string, s2: string)
member4(s1: string, s2: string, i: i32) -> i32
member5(s1: string, s2: string, i: i32) -> i32
func f1(callback: string, i32 -> ()):
callback("hello, world", 42)
func f2(num: i32, callback: string, i32 -> ()):
callback("hello, world", 42)
func f3(num: i32, callback: string, i32 -> ()) -> string:
callback("hello, world", 42)
<- "hello world"
func callBackZeroArgs(callback: -> ()):
callback()
*/
/**
*
* This file is auto-generated. Do not edit manually: changes may be erased.
* Generated by Aqua compiler: https://github.com/fluencelabs/aqua/.
* If you find any bugs, please write an issue on GitHub: https://github.com/fluencelabs/aqua/issues
* Aqua version: 0.3.1-231
*
*/
// Services
export interface ServiceWithDefaultIdDef {
hello: (s: string, callParams: CallParams<'s'>) => void;
}
export function registerServiceWithDefaultId(service: ServiceWithDefaultIdDef): void;
export function registerServiceWithDefaultId(serviceId: string, service: ServiceWithDefaultIdDef): void;
export function registerServiceWithDefaultId(peer: FluencePeer, service: ServiceWithDefaultIdDef): void;
export function registerServiceWithDefaultId(
peer: FluencePeer,
serviceId: string,
service: ServiceWithDefaultIdDef,
): void;
export function registerServiceWithDefaultId(...args: any) {
registerService(args, {
defaultServiceId: 'defaultId',
functions: [
{
functionName: 'hello',
argDefs: [
{
name: 's',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
],
});
}
export interface ServiceWithOUTDefaultIdDef {
hello: (s: string, callParams: CallParams<'s'>) => void;
}
export function registerServiceWithOUTDefaultId(serviceId: string, service: ServiceWithOUTDefaultIdDef): void;
export function registerServiceWithOUTDefaultId(
peer: FluencePeer,
serviceId: string,
service: ServiceWithOUTDefaultIdDef,
): void;
export function registerServiceWithOUTDefaultId(...args: any) {
registerService(args, {
defaultServiceId: null,
functions: [
{
functionName: 'hello',
argDefs: [
{
name: 's',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
],
});
}
export interface MoreMembersDef {
member1: (callParams: CallParams<null>) => void;
member2: (s1: string, callParams: CallParams<'s1'>) => void;
member3: (s1: string, s2: string, callParams: CallParams<'s1' | 's2'>) => void;
member4: (s1: string, s2: string, i: number, callParams: CallParams<'s1' | 's2' | 'i'>) => number;
member5: (s1: string, s2: string, i: number, callParams: CallParams<'s1' | 's2' | 'i'>) => number;
}
export function registerMoreMembers(serviceId: string, service: MoreMembersDef): void;
export function registerMoreMembers(peer: FluencePeer, serviceId: string, service: MoreMembersDef): void;
export function registerMoreMembers(...args: any) {
registerService(args, {
defaultServiceId: null,
functions: [
{
functionName: 'member1',
argDefs: [],
returnType: {
tag: 'void',
},
},
{
functionName: 'member2',
argDefs: [
{
name: 's1',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
{
functionName: 'member3',
argDefs: [
{
name: 's1',
argType: {
tag: 'primitive',
},
},
{
name: 's2',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
{
functionName: 'member4',
argDefs: [
{
name: 's1',
argType: {
tag: 'primitive',
},
},
{
name: 's2',
argType: {
tag: 'primitive',
},
},
{
name: 'i',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'primitive',
},
},
{
functionName: 'member5',
argDefs: [
{
name: 's1',
argType: {
tag: 'primitive',
},
},
{
name: 's2',
argType: {
tag: 'primitive',
},
},
{
name: 'i',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'primitive',
},
},
],
});
}
// Functions
export function f1(
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function f1(
peer: FluencePeer,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function f1(...args: any) {
let script = `
(xor
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(xor
(call %init_peer_id% ("callbackSrv" "callback") ["hello, world" 42])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)
`;
return callFunction(
args,
{
functionName: 'f1',
returnType: {
tag: 'void',
},
argDefs: [
{
name: 'callback',
argType: {
tag: 'callback',
callback: {
argDefs: [
{
name: 'arg0',
argType: {
tag: 'primitive',
},
},
{
name: 'arg1',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
},
},
],
names: {
relay: '-relay-',
getDataSrv: 'getDataSrv',
callbackSrv: 'callbackSrv',
responseSrv: 'callbackSrv',
responseFnName: 'response',
errorHandlingSrv: 'errorHandlingSrv',
errorFnName: 'error',
},
},
script,
);
}
export function f2(
num: number,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function f2(
peer: FluencePeer,
num: number,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<void>;
export function f2(...args: any) {
let script = `
(xor
(seq
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(call %init_peer_id% ("getDataSrv" "num") [] num)
)
(xor
(call %init_peer_id% ("callbackSrv" "callback") ["hello, world" 42])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)
`;
return callFunction(
args,
{
functionName: 'f2',
returnType: {
tag: 'void',
},
argDefs: [
{
name: 'num',
argType: {
tag: 'primitive',
},
},
{
name: 'callback',
argType: {
tag: 'callback',
callback: {
argDefs: [
{
name: 'arg0',
argType: {
tag: 'primitive',
},
},
{
name: 'arg1',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
},
},
],
names: {
relay: '-relay-',
getDataSrv: 'getDataSrv',
callbackSrv: 'callbackSrv',
responseSrv: 'callbackSrv',
responseFnName: 'response',
errorHandlingSrv: 'errorHandlingSrv',
errorFnName: 'error',
},
},
script,
);
}
export function f3(
num: number,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<string>;
export function f3(
peer: FluencePeer,
num: number,
callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void,
config?: { ttl?: number },
): Promise<string>;
export function f3(...args: any) {
let script = `
(xor
(seq
(seq
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(call %init_peer_id% ("getDataSrv" "num") [] num)
)
(xor
(call %init_peer_id% ("callbackSrv" "callback") ["hello, world" 42])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
)
)
(xor
(call %init_peer_id% ("callbackSrv" "response") ["hello world"])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 3])
)
`;
return callFunction(
args,
{
functionName: 'f3',
returnType: {
tag: 'primitive',
},
argDefs: [
{
name: 'num',
argType: {
tag: 'primitive',
},
},
{
name: 'callback',
argType: {
tag: 'callback',
callback: {
argDefs: [
{
name: 'arg0',
argType: {
tag: 'primitive',
},
},
{
name: 'arg1',
argType: {
tag: 'primitive',
},
},
],
returnType: {
tag: 'void',
},
},
},
},
],
names: {
relay: '-relay-',
getDataSrv: 'getDataSrv',
callbackSrv: 'callbackSrv',
responseSrv: 'callbackSrv',
responseFnName: 'response',
errorHandlingSrv: 'errorHandlingSrv',
errorFnName: 'error',
},
},
script,
);
}
export function callBackZeroArgs(
callback: (callParams: CallParams<null>) => void,
config?: { ttl?: number },
): Promise<void>;
export function callBackZeroArgs(
peer: FluencePeer,
callback: (callParams: CallParams<null>) => void,
config?: { ttl?: number },
): Promise<void>;
export function callBackZeroArgs(...args: any) {
let script = `
(xor
(seq
(call %init_peer_id% ("getDataSrv" "-relay-") [] -relay-)
(xor
(call %init_peer_id% ("callbackSrv" "callback") [])
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 1])
)
)
(call %init_peer_id% ("errorHandlingSrv" "error") [%last_error% 2])
)
`;
return callFunction(
args,
{
functionName: 'callBackZeroArgs',
returnType: {
tag: 'void',
},
argDefs: [
{
name: 'callback',
argType: {
tag: 'callback',
callback: {
argDefs: [],
returnType: {
tag: 'void',
},
},
},
},
],
names: {
relay: '-relay-',
getDataSrv: 'getDataSrv',
callbackSrv: 'callbackSrv',
responseSrv: 'callbackSrv',
responseFnName: 'response',
errorHandlingSrv: 'errorHandlingSrv',
errorFnName: 'error',
},
},
script,
);
}

View File

@ -158,8 +158,12 @@ describe('Typescript usage suite', () => {
// act
const resMakingPromise = new Promise((resolve) => {
peer2.internals.callServiceHandler.onEvent('test', 'test', (args, _) => {
resolve(args[0]);
peer2.internals.regHandler.common('test', 'test', (req) => {
resolve(req.args[0]);
return {
result: {},
retCode: 0,
};
});
});

View File

@ -1,6 +1,6 @@
import each from 'jest-each';
import { Fluence, FluencePeer } from '../..';
import { forTests } from '../../internal/compilerSupport/v2';
import { Fluence, FluencePeer } from '../../..';
import { forTests } from '../../../internal/compilerSupport/v2';
const peer = new FluencePeer();
const cfg = { ttl: 1000 };

View File

@ -0,0 +1,187 @@
import each from 'jest-each';
import { aqua2ts, ts2aqua } from '../../../internal/compilerSupport/v3impl/conversions';
const i32 = { tag: 'scalar', name: 'i32' } as const;
const opt_i32 = {
tag: 'option',
type: i32,
} as const;
const array_i32 = { tag: 'array', type: i32 };
const array_opt_i32 = { tag: 'array', type: opt_i32 };
const labeledProduct = {
tag: 'labeledProduct',
fields: {
a: i32,
b: opt_i32,
c: array_opt_i32,
},
};
const struct = {
tag: 'struct',
name: 'someStruct',
fields: {
a: i32,
b: opt_i32,
c: array_opt_i32,
},
};
const structs = [
{
aqua: {
a: 1,
b: [2],
c: [[1], [2]],
},
ts: {
a: 1,
b: 2,
c: [1, 2],
},
},
{
aqua: {
a: 1,
b: [],
c: [[], [2]],
},
ts: {
a: 1,
b: null,
c: [null, 2],
},
},
];
const labeledProduct2 = {
tag: 'labeledProduct',
fields: {
x: i32,
y: i32,
},
};
const nestedLabeledProductType = {
tag: 'labeledProduct',
fields: {
a: labeledProduct2,
b: {
tag: 'option',
type: labeledProduct2,
},
c: {
tag: 'array',
type: labeledProduct2,
},
},
};
const nestedStructs = [
{
aqua: {
a: {
x: 1,
y: 2,
},
b: [
{
x: 1,
y: 2,
},
],
c: [
{
x: 1,
y: 2,
},
{
x: 3,
y: 4,
},
],
},
ts: {
a: {
x: 1,
y: 2,
},
b: {
x: 1,
y: 2,
},
c: [
{
x: 1,
y: 2,
},
{
x: 3,
y: 4,
},
],
},
},
{
aqua: {
a: {
x: 1,
y: 2,
},
b: [],
c: [],
},
ts: {
a: {
x: 1,
y: 2,
},
b: null,
c: [],
},
},
];
describe('Conversion from aqua to typescript', () => {
each`
aqua | ts | type
${1} | ${1} | ${i32}
${[]} | ${null} | ${opt_i32}
${[1]} | ${1} | ${opt_i32}
${[1, 2, 3]} | ${[1, 2, 3]} | ${array_i32}
${[]} | ${[]} | ${array_i32}
${[[1]]} | ${[1]} | ${array_opt_i32}
${[[]]} | ${[null]} | ${array_opt_i32}
${[[1], [2]]} | ${[1, 2]} | ${array_opt_i32}
${[[], [2]]} | ${[null, 2]} | ${array_opt_i32}
${structs[0].aqua} | ${structs[0].ts} | ${labeledProduct}
${structs[1].aqua} | ${structs[1].ts} | ${labeledProduct}
${structs[0].aqua} | ${structs[0].ts} | ${struct}
${structs[1].aqua} | ${structs[1].ts} | ${struct}
${nestedStructs[0].aqua} | ${nestedStructs[0].ts} | ${nestedLabeledProductType}
${nestedStructs[1].aqua} | ${nestedStructs[1].ts} | ${nestedLabeledProductType}
`.test(
//
'aqua: $aqua. ts: $ts. type: $type',
async ({ aqua, ts, type }) => {
// arrange
// act
const tsFromAqua = aqua2ts(aqua, type);
const aquaFromTs = ts2aqua(ts, type);
// assert
expect(tsFromAqua).toStrictEqual(ts);
expect(aquaFromTs).toStrictEqual(aqua);
},
);
});

View File

@ -16,14 +16,12 @@
import { Multiaddr } from 'multiaddr';
import { CallServiceData, CallServiceResult, GenericCallServiceHandler, ResultCodes } from './commonTypes';
import { CallServiceHandler as LegacyCallServiceHandler } from './compilerSupport/LegacyCallServiceHandler';
import { PeerIdB58 } from './commonTypes';
import { FluenceConnection } from './FluenceConnection';
import { Particle, ParticleExecutionStage, ParticleQueueItem } from './Particle';
import { KeyPair } from './KeyPair';
import { dataToString, jsonify } from './utils';
import { concatMap, filter, pipe, Subject, tap } from 'rxjs';
import { RequestFlow } from './compilerSupport/v1';
import log from 'loglevel';
import { builtInServices } from './builtins/common';
import { AvmRunner, InterpreterResult, LogLevel } from '@fluencelabs/avm-runner-interface';
@ -212,7 +210,6 @@ export class FluencePeer {
await this._connect();
}
this._legacyCallServiceHandler = new LegacyCallServiceHandler();
registerDefaultServices(this);
this._classServices = {
@ -242,7 +239,6 @@ export class FluencePeer {
await this._disconnect();
await this._avmRunner?.terminate();
this._avmRunner = undefined;
this._legacyCallServiceHandler = null;
this._particleSpecificHandlers.clear();
this._commonHandlers.clear();
@ -311,34 +307,6 @@ export class FluencePeer {
psh.set(serviceFnKey(serviceId, fnName), handler);
},
},
/**
* @deprecated
*/
initiateFlow: (request: RequestFlow): void => {
const particle = request.particle;
this._legacyParticleSpecificHandlers.set(particle.id, {
handler: request.handler,
error: request.error,
timeout: request.timeout,
});
this.internals.initiateParticle(particle, (stage) => {
if (stage.stage === 'interpreterError') {
request?.error(stage.errorMessage);
}
if (stage.stage === 'expired') {
request?.timeout();
}
});
},
/**
* @deprecated
*/
callServiceHandler: this._legacyCallServiceHandler,
};
}
@ -544,24 +512,6 @@ export class FluencePeer {
log.debug('executing call service handler', jsonify(req));
const particleId = req.particleContext.particleId;
// trying particle-specific handler
const lh = this._legacyParticleSpecificHandlers.get(particleId);
let res: CallServiceResult = {
result: undefined,
retCode: undefined,
};
if (lh !== undefined) {
res = lh.handler.execute(req);
}
// if it didn't return any result trying to run the common handler
if (res?.result === undefined) {
res = this._legacyCallServiceHandler.execute(req);
}
// No result from legacy handler.
// Trying to execute async handler
if (res.retCode === undefined) {
const key = serviceFnKey(req.serviceId, req.fnName);
const psh = this._particleSpecificHandlers.get(particleId);
let handler: GenericCallServiceHandler;
@ -578,11 +528,9 @@ export class FluencePeer {
handler = this._commonHandlers.get(key);
}
// if we found a handler, execute it
// otherwise return useful error message to AVM
res = handler
? await handler(req)
: {
// if no handler is found return useful error message to AVM
if (handler === undefined) {
return {
retCode: ResultCodes.error,
result: `No handler has been registered for serviceId='${req.serviceId}' fnName='${
req.fnName
@ -590,6 +538,9 @@ export class FluencePeer {
};
}
// if we found a handler, execute it
const res = await handler(req);
if (res.result === undefined) {
res.result = null;
}
@ -605,23 +556,6 @@ export class FluencePeer {
}
this._particleQueues.clear();
}
/**
* @deprecated
*/
private _legacyParticleSpecificHandlers = new Map<
string,
{
handler: LegacyCallServiceHandler;
timeout?: () => void;
error?: (reason?: any) => void;
}
>();
/**
* @deprecated
*/
private _legacyCallServiceHandler: LegacyCallServiceHandler;
}
function isInterpretationSuccessful(result: InterpreterResult) {

View File

@ -1,87 +0,0 @@
/*
* Copyright 2021 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 { CallServiceHandler } from './LegacyCallServiceHandler';
import { Particle } from '../Particle';
export { FluencePeer } from '../FluencePeer';
export { CallParams, ResultCodes } from '../commonTypes';
/**
* @deprecated This class exists to glue legacy RequestFlowBuilder api with restructured async FluencePeer.
* v2 version of compiler support should be used instead
*/
export interface RequestFlow {
particle: Particle;
handler: CallServiceHandler;
timeout?: () => void;
error?: (reason?: any) => void;
}
/**
* @deprecated This class exists to glue legacy RequestFlowBuilder api with restructured async FluencePeer.
* v2 version of compiler support should be used instead
*/
export class RequestFlowBuilder {
private _ttl?: number;
private _script?: string;
private _configs: any = [];
private _error: (reason?: any) => void = () => {};
private _timeout: () => void = () => {};
build(): RequestFlow {
let h = new CallServiceHandler();
for (let c of this._configs) {
c(h);
}
return {
particle: Particle.createNew(this._script!, this._ttl),
handler: h,
timeout: this._timeout,
error: this._error,
};
}
withTTL(ttl: number): RequestFlowBuilder {
this._ttl = ttl;
return this;
}
handleTimeout(timeout: () => void): RequestFlowBuilder {
this._timeout = timeout;
return this;
}
handleScriptError(reject: (reason?: any) => void): RequestFlowBuilder {
this._error = reject;
return this;
}
withRawScript(script: string): RequestFlowBuilder {
this._script = script;
return this;
}
disableInjections(): RequestFlowBuilder {
return this;
}
configHandler(h: (handler: CallServiceHandler) => void) {
this._configs.push(h);
return this;
}
}

View File

@ -0,0 +1,21 @@
/*
* Copyright 2022 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 { FluencePeer } from '../FluencePeer';
export { CallParams } from '../commonTypes';
export * from './v3impl/interface';
export * from './v3impl/callFunction';
export * from './v3impl/registerService';

View File

@ -0,0 +1,128 @@
import { FnConfig, FunctionCallDef } from './interface';
import { FluencePeer } from '../../FluencePeer';
import { Fluence } from '../../../index';
import { Particle } from '../../Particle';
import {
injectRelayService,
argToServiceDef,
registerParticleScopeService,
responseService,
errorHandlingService,
ServiceDescription,
userHandlerService,
injectValueService,
} from './services';
/**
* Convenience function to support Aqua `func` generation backend
* The compiler only need to generate a call the function and provide the corresponding definitions and the air script
*
* @param rawFnArgs - raw arguments passed by user to the generated function
* @param def - function definition generated by the Aqua compiler
* @param script - air script with function execution logic generated by the Aqua compiler
*/
export function callFunction(rawFnArgs: Array<any>, def: FunctionCallDef, script: string) {
if (def.arrow.domain.tag !== 'labeledProduct') {
throw new Error('Should be impossible');
}
const argumentTypes = Object.entries(def.arrow.domain.fields);
const expectedNumberOfArguments = argumentTypes.length;
const { args, peer, config } = extractArgs(rawFnArgs, expectedNumberOfArguments);
if (args.length !== expectedNumberOfArguments) {
throw new Error('Incorrect number of arguments. Expecting ${def.argDefs.length}');
}
const promise = new Promise((resolve, reject) => {
const particle = Particle.createNew(script, config?.ttl);
for (let i = 0; i < expectedNumberOfArguments; i++) {
const [name, type] = argumentTypes[i];
let service: ServiceDescription;
if (type.tag === 'arrow') {
service = userHandlerService(def.names.callbackSrv, [name, type], args[i]);
} else {
service = injectValueService(def.names.getDataSrv, name, type, args[i]);
}
registerParticleScopeService(peer, particle, service);
}
registerParticleScopeService(peer, particle, responseService(def, resolve));
registerParticleScopeService(peer, particle, injectRelayService(def, peer));
registerParticleScopeService(peer, particle, errorHandlingService(def, reject));
peer.internals.initiateParticle(particle, (stage) => {
// If function is void, then it's completed when one of the two conditions is met:
// 1. The particle is sent to the network (state 'sent')
// 2. All CallRequests are executed, e.g., all variable loading and local function calls are completed (state 'localWorkDone')
if (isReturnTypeVoid(def) && (stage.stage === 'sent' || stage.stage === 'localWorkDone')) {
resolve(undefined);
}
if (stage.stage === 'sendingError') {
reject(`Could not send particle for ${def.functionName}: not connected (particle id: ${particle.id})`);
}
if (stage.stage === 'expired') {
reject(`Request timed out after ${particle.ttl} for ${def.functionName} (particle id: ${particle.id})`);
}
if (stage.stage === 'interpreterError') {
reject(
`Script interpretation failed for ${def.functionName}: ${stage.errorMessage} (particle id: ${particle.id})`,
);
}
});
});
return promise;
}
const isReturnTypeVoid = (def: FunctionCallDef) => {
if (def.arrow.codomain.tag === 'nil') {
return true;
}
return def.arrow.codomain.items.length == 0;
};
/**
* Arguments could be passed in one these configurations:
* [...actualArgs]
* [peer, ...actualArgs]
* [...actualArgs, config]
* [peer, ...actualArgs, config]
*
* This function select the appropriate configuration and returns
* arguments in a structured way of: { peer, config, args }
*/
const extractArgs = (
args: any[],
numberOfExpectedArgs: number,
): {
peer: FluencePeer;
config?: FnConfig;
args: any[];
} => {
let peer: FluencePeer;
let structuredArgs: any[];
let config: any;
if (FluencePeer.isInstance(args[0])) {
peer = args[0];
structuredArgs = args.slice(1, numberOfExpectedArgs + 1);
config = args[numberOfExpectedArgs + 1];
} else {
peer = Fluence.getPeer();
structuredArgs = args.slice(0, numberOfExpectedArgs);
config = args[numberOfExpectedArgs];
}
return {
peer: peer,
config: config,
args: structuredArgs,
};
};

View File

@ -0,0 +1,184 @@
import { jsonify } from '../../utils';
import { match } from 'ts-pattern';
import { ArrowType, ArrowWithoutCallbacks, NonArrowType, UnlabeledProductType } from './interface';
import { CallServiceData } from 'src/internal/commonTypes';
/**
* Convert value from its representation in aqua language to representation in typescript
* @param value - value as represented in aqua
* @param type - definition of the aqua type
* @returns value represented in typescript
*/
export const aqua2ts = (value: any, type: NonArrowType) => {
const res = match(type)
.with({ tag: 'nil' }, () => {
return null;
})
.with({ tag: 'option' }, (opt) => {
if (value.length === 0) {
return null;
} else {
return aqua2ts(value[0], opt.type);
}
})
.with({ tag: 'scalar' }, { tag: 'bottomType' }, { tag: 'topType' }, () => {
return value;
})
.with({ tag: 'array' }, (arr) => {
return value.map((y) => aqua2ts(y, arr.type));
})
.with({ tag: 'struct' }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = aqua2ts(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: 'labeledProduct' }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = aqua2ts(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: 'unlabeledProduct' }, (x) => {
return x.items.map((type, index) => {
return aqua2ts(value[index], type);
});
})
// uncomment to check that every pattern in matched
// .exhaustive();
.otherwise(() => {
throw new Error('Unexpected tag: ' + jsonify(type));
});
return res;
};
/**
* Convert call service arguments list from their aqua representation to representation in typescript
* @param req - call service data
* @param arrow - aqua type definition
* @returns arguments in typescript representation
*/
export const aquaArgs2Ts = (req: CallServiceData, arrow: ArrowWithoutCallbacks) => {
const argTypes = match(arrow.domain)
.with({ tag: 'labeledProduct' }, (x) => {
return Object.values(x.fields);
})
.with({ tag: 'unlabeledProduct' }, (x) => {
return x.items;
})
.with({ tag: 'nil' }, (x) => {
return [];
})
// uncomment to check that every pattern in matched
// .exhaustive()
.otherwise(() => {
throw new Error('Unexpected tag: ' + jsonify(arrow.domain));
});
if (req.args.length !== argTypes.length) {
throw new Error(`incorrect number of arguments, expected: ${argTypes.length}, got: ${req.args.length}`);
}
return req.args.map((arg, index) => {
return aqua2ts(arg, argTypes[index]);
});
};
/**
* Convert value from its typescript representation to representation in aqua
* @param value - the value as represented in typescript
* @param type - definition of the aqua type
* @returns value represented in aqua
*/
export const ts2aqua = (value: any, type: NonArrowType) => {
const res = match(type)
.with({ tag: 'nil' }, () => {
return null;
})
.with({ tag: 'option' }, (opt) => {
if (value === null) {
return [];
} else {
return [ts2aqua(value, opt.type)];
}
})
.with({ tag: 'scalar' }, { tag: 'bottomType' }, { tag: 'topType' }, () => {
return value;
})
.with({ tag: 'array' }, (arr) => {
return value.map((y) => ts2aqua(y, arr.type));
})
.with({ tag: 'struct' }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = ts2aqua(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: 'labeledProduct' }, (x) => {
return Object.entries(x.fields).reduce((agg, [key, type]) => {
const val = ts2aqua(value[key], type);
return { ...agg, [key]: val };
}, {});
})
.with({ tag: 'unlabeledProduct' }, (x) => {
return x.items.map((type, index) => {
return ts2aqua(value[index], type);
});
})
// uncomment to check that every pattern in matched
// .exhaustive()
.otherwise(() => {
throw new Error('Unexpected tag: ' + jsonify(type));
});
return res;
};
/**
* Convert return type of the service from it's typescript representation to representation in aqua
* @param returnValue - the value as represented in typescript
* @param arrowType - the arrow type which describes the service
* @returns - value represented in aqua
*/
export const returnType2Aqua = (returnValue: any, arrowType: ArrowType<NonArrowType>) => {
if (arrowType.codomain.tag === 'nil') {
return {};
}
if (arrowType.codomain.items.length === 0) {
return {};
}
if (arrowType.codomain.items.length === 1) {
return ts2aqua(returnValue, arrowType.codomain.items[0]);
}
return arrowType.codomain.items.map((type, index) => {
return ts2aqua(returnValue[index], type);
});
};
/**
* Converts response value from aqua its representation to representation in typescript
* @param req - call service data
* @param arrow - aqua type definition
* @returns response value in typescript representation
*/
export const responseServiceValue2ts = (req: CallServiceData, arrow: ArrowType<any>) => {
return match(arrow.codomain)
.with({ tag: 'nil' }, () => {
return undefined;
})
.with({ tag: 'unlabeledProduct' }, (x) => {
if (x.items.length === 0) {
return undefined;
}
if (x.items.length === 1) {
return aqua2ts(req.args[0], x.items[0]);
}
return req.args.map((y, index) => aqua2ts(y, x.items[index]));
})
.exhaustive();
};

View File

@ -0,0 +1,238 @@
type SomeNonArrowTypes = ScalarType | OptionType | ArrayType | StructType | TopType | BottomType;
export type NonArrowType = SomeNonArrowTypes | ProductType<SomeNonArrowTypes>;
export type TopType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'topType';
};
export type BottomType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'bottomType';
};
export type OptionType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'option';
/**
* Underlying type of the option
*/
type: NonArrowType;
};
export type NilType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'nil';
};
export type ArrayType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'array';
/**
* Type of array elements
*/
type: NonArrowType;
};
/**
* All possible scalar type names
*/
export type ScalarNames =
| 'u8'
| 'u16'
| 'u32'
| 'u64'
| 'i8'
| 'i16'
| 'i32'
| 'i64'
| 'f32'
| 'f64'
| 'bool'
| 'string';
export type ScalarType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'scalar';
/**
* Name of the scalar type
*/
name: ScalarNames;
};
export type StructType = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'struct';
/**
* Struct name
*/
name: string;
/**
* Struct fields
*/
fields: { [key: string]: NonArrowType };
};
export type LabeledProductType<T> =
| {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'labeledProduct';
/**
* Labelled product fields
*/
fields: { [key: string]: T };
}
| NilType;
export type UnlabeledProductType<T> =
| {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'unlabeledProduct';
/**
* Items in unlabelled product
*/
items: Array<T>;
}
| NilType;
export type ProductType<T> = UnlabeledProductType<T> | LabeledProductType<T>;
/**
* ArrowType is a profunctor pointing its domain to codomain.
* Profunctor means variance: Arrow is contravariant on domain, and variant on codomain.
*/
export type ArrowType<T> = {
/**
* Type descriptor. Used for pattern-matching
*/
tag: 'arrow';
/**
* Where this Arrow is defined
*/
domain: ProductType<T>;
/**
* Where this Arrow points to
*/
codomain: UnlabeledProductType<NonArrowType> | NilType;
};
/**
* Arrow which domain contains only non-arrow types
*/
export type ArrowWithoutCallbacks = ArrowType<NonArrowType>;
/**
* Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows)
*/
export type ArrowWithCallbacks = ArrowType<NonArrowType | ArrowWithoutCallbacks>;
export interface FunctionCallConstants {
/**
* The name of the relay variable
*/
relay: string;
/**
* The name of the serviceId used load variables at the beginning of the script
*/
getDataSrv: string;
/**
* The name of serviceId is used to execute callbacks for the current particle
*/
callbackSrv: string;
/**
* The name of the serviceId which is called to propagate return value to the generated function caller
*/
responseSrv: string;
/**
* The name of the functionName which is called to propagate return value to the generated function caller
*/
responseFnName: string;
/**
* The name of the serviceId which is called to report errors to the generated function caller
*/
errorHandlingSrv: string;
/**
* The name of the functionName which is called to report errors to the generated function caller
*/
errorFnName: string;
}
/**
* Definition of function (`func` instruction) generated by the Aqua compiler
*/
export interface FunctionCallDef {
/**
* The name of the function in Aqua language
*/
functionName: string;
/**
* Underlying arrow which represents function in aqua
*/
arrow: ArrowWithCallbacks;
/**
* Names of the different entities used in generated air script
*/
names: FunctionCallConstants;
}
/**
* Definition of service registration function (`service` instruction) generated by the Aqua compiler
*/
export interface ServiceDef {
/**
* Default service id. If the service has no default id the value should be undefined
*/
defaultServiceId?: string;
/**
* List of functions which the service consists of
*/
functions: LabeledProductType<ArrowWithoutCallbacks>;
}
/**
* Options to configure Aqua function execution
*/
export interface FnConfig {
/**
* Sets the TTL (time to live) for particle responsible for the function execution
* If the option is not set the default TTL from FluencePeer config is used
*/
ttl?: number;
}

View File

@ -0,0 +1,95 @@
import { FluencePeer } from '../../FluencePeer';
import { Fluence } from '../../../index';
import { ServiceDef } from './interface';
import { registerGlobalService, userHandlerService } from './services';
/**
* Convenience function to support Aqua `service` generation backend
* The compiler only need to generate a call the function and provide the corresponding definitions and the air script
*
* @param args - raw arguments passed by user to the generated function
* @param def - service definition generated by the Aqua compiler
*/
export function registerService(args: any[], def: ServiceDef) {
const { peer, service, serviceId } = extractArgs(args, def.defaultServiceId);
if (!peer.getStatus().isInitialized) {
throw new Error(
'Could not register the service because the peer is not initialized. Are you passing the wrong peer to the register function?',
);
}
// Checking for missing keys
const requiredKeys = def.functions.tag === 'nil' ? [] : Object.keys(def.functions.fields);
const incorrectServiceDefinitions = requiredKeys.filter((f) => !(f in service));
if (!!incorrectServiceDefinitions.length) {
throw new Error(
`Error registering service ${serviceId}: missing functions: ` +
incorrectServiceDefinitions.map((d) => "'" + d + "'").join(', '),
);
}
const singleFunctions = def.functions.tag === 'nil' ? [] : Object.entries(def.functions.fields);
for (let singleFunction of singleFunctions) {
let [name, type] = singleFunction;
// The function has type of (arg1, arg2, arg3, ... , callParams) => CallServiceResultType | void
// Account for the fact that user service might be defined as a class - .bind(...)
const userDefinedHandler = service[name].bind(service);
const serviceDescription = userHandlerService(serviceId, singleFunction, userDefinedHandler);
registerGlobalService(peer, serviceDescription);
}
}
/**
* Arguments could be passed in one these configurations:
* [serviceObject]
* [peer, serviceObject]
* [defaultId, serviceObject]
* [peer, defaultId, serviceObject]
*
* Where serviceObject is the raw object with function definitions passed by user
*
* This function select the appropriate configuration and returns
* arguments in a structured way of: { peer, serviceId, service }
*/
const extractArgs = (
args: any[],
defaultServiceId?: string,
): { peer: FluencePeer; serviceId: string; service: any } => {
let peer: FluencePeer;
let serviceId: any;
let service: any;
if (FluencePeer.isInstance(args[0])) {
peer = args[0];
} else {
peer = Fluence.getPeer();
}
if (typeof args[0] === 'string') {
serviceId = args[0];
} else if (typeof args[1] === 'string') {
serviceId = args[1];
} else {
serviceId = defaultServiceId;
}
// Figuring out which overload is the service.
// If the first argument is not Fluence Peer and it is an object, then it can only be the service def
// If the first argument is peer, we are checking further. The second argument might either be
// an object, that it must be the service object
// or a string, which is the service id. In that case the service is the third argument
if (!FluencePeer.isInstance(args[0]) && typeof args[0] === 'object') {
service = args[0];
} else if (typeof args[1] === 'object') {
service = args[1];
} else {
service = args[2];
}
return {
peer: peer,
serviceId: serviceId,
service: service,
};
};

View File

@ -0,0 +1,171 @@
import { SecurityTetraplet } from '@fluencelabs/avm-runner-interface';
import { Particle } from 'src/internal/Particle';
import { match } from 'ts-pattern';
import {
CallParams,
CallServiceData,
CallServiceResult,
GenericCallServiceHandler,
ResultCodes,
} from '../../commonTypes';
import { FluencePeer } from '../../FluencePeer';
import { aquaArgs2Ts, responseServiceValue2ts, returnType2Aqua, ts2aqua } from './conversions';
import { ArrowWithoutCallbacks, FunctionCallConstants, FunctionCallDef, NonArrowType } from './interface';
export interface ServiceDescription {
serviceId: string;
fnName: string;
handler: GenericCallServiceHandler;
}
/**
* Creates a service which injects relay's peer id into aqua space
*/
export const injectRelayService = (def: FunctionCallDef, peer: FluencePeer) => {
return {
serviceId: def.names.getDataSrv,
fnName: def.names.relay,
handler: (req) => {
return {
retCode: ResultCodes.success,
result: peer.getStatus().relayPeerId,
};
},
};
};
/**
* Creates a service which injects plain value into aqua space
*/
export const injectValueService = (serviceId: string, fnName: string, valueType: NonArrowType, value: any) => {
return {
serviceId: serviceId,
fnName: fnName,
handler: (req) => {
return {
retCode: ResultCodes.success,
result: ts2aqua(value, valueType),
};
},
};
};
/**
* Creates a service which is used to return value from aqua function into typescript space
*/
export const responseService = (def: FunctionCallDef, resolveCallback: Function) => {
return {
serviceId: def.names.responseSrv,
fnName: def.names.responseFnName,
handler: (req) => {
const userFunctionReturn = responseServiceValue2ts(req, def.arrow);
setTimeout(() => {
resolveCallback(userFunctionReturn);
}, 0);
return {
retCode: ResultCodes.success,
result: {},
};
},
};
};
/**
* Creates a service which is used to return errors from aqua function into typescript space
*/
export const errorHandlingService = (def: FunctionCallDef, rejectCallback: Function) => {
return {
serviceId: def.names.errorHandlingSrv,
fnName: def.names.errorFnName,
handler: (req) => {
const [err, _] = req.args;
setTimeout(() => {
rejectCallback(err);
}, 0);
return {
retCode: ResultCodes.success,
result: {},
};
},
};
};
/**
* Creates a service for user-defined service function handler
*/
export const userHandlerService = (serviceId: string, arrowType: [string, ArrowWithoutCallbacks], userHandler) => {
const [fnName, type] = arrowType;
return {
serviceId,
fnName,
handler: async (req) => {
const args = [...aquaArgs2Ts(req, type), extractCallParams(req, type)];
const rawResult = await userHandler.apply(null, args);
const result = returnType2Aqua(rawResult, type);
return {
retCode: ResultCodes.success,
result: result,
};
},
};
};
/**
* Converts argument of aqua function to a corresponding service.
* For arguments of non-arrow types the resulting service injects the argument into aqua space.
* For arguments of arrow types the resulting service calls the corresponding function.
*/
export const argToServiceDef = (
arg: any,
argName: string,
argType: NonArrowType | ArrowWithoutCallbacks,
names: FunctionCallConstants,
): ServiceDescription => {
if (argType.tag === 'arrow') {
return userHandlerService(names.callbackSrv, [argName, argType], arg);
} else {
return injectValueService(names.getDataSrv, argName, arg, argType);
}
};
/**
* Extracts call params from from call service data according to aqua type definition
*/
const extractCallParams = (req: CallServiceData, arrow: ArrowWithoutCallbacks): CallParams<any> => {
const names = match(arrow.domain)
.with({ tag: 'nil' }, () => {
return [] as string[];
})
.with({ tag: 'labeledProduct' }, (x) => {
return Object.keys(x.fields);
})
.with({ tag: 'unlabeledProduct' }, (x) => {
return x.items.map((_, index) => 'arg' + index);
})
.exhaustive();
let tetraplets: { [key in string]: SecurityTetraplet[] } = {};
for (let i = 0; i < req.args.length; i++) {
if (names[i]) {
tetraplets[names[i]] = req.tetraplets[i];
}
}
const callParams = {
...req.particleContext,
tetraplets,
};
return callParams;
};
export const registerParticleScopeService = (peer: FluencePeer, particle: Particle, service: ServiceDescription) => {
peer.internals.regHandler.forParticle(particle.id, service.serviceId, service.fnName, service.handler);
};
export const registerGlobalService = (peer: FluencePeer, service: ServiceDescription) => {
peer.internals.regHandler.common(service.serviceId, service.fnName, service.handler);
};