From a0a7a9e19be511841671c0652b4f3d10caf2a8e5 Mon Sep 17 00:00:00 2001 From: Pavel Date: Thu, 17 Mar 2022 07:00:19 +0300 Subject: [PATCH] Fix recursive (#136) --- .../integration/compiler/compiler.spec.ts | 211 ------- src/__test__/integration/compiler/gen1.ts | 193 ------- src/__test__/integration/compiler/gen2.ts | 519 ------------------ src/__test__/integration/peer.spec.ts | 8 +- .../v2.spec.ts} | 4 +- src/__test__/unit/compiler/v3.spec.ts | 187 +++++++ src/internal/FluencePeer.ts | 112 +--- src/internal/compilerSupport/v1.ts | 87 --- src/internal/compilerSupport/v3.ts | 21 + .../compilerSupport/v3impl/callFunction.ts | 128 +++++ .../compilerSupport/v3impl/conversions.ts | 184 +++++++ .../compilerSupport/v3impl/interface.ts | 238 ++++++++ .../compilerSupport/v3impl/registerService.ts | 95 ++++ .../compilerSupport/v3impl/services.ts | 171 ++++++ 14 files changed, 1055 insertions(+), 1103 deletions(-) delete mode 100644 src/__test__/integration/compiler/compiler.spec.ts delete mode 100644 src/__test__/integration/compiler/gen1.ts delete mode 100644 src/__test__/integration/compiler/gen2.ts rename src/__test__/unit/{compilerSupport.spec.ts => compiler/v2.spec.ts} (93%) create mode 100644 src/__test__/unit/compiler/v3.spec.ts delete mode 100644 src/internal/compilerSupport/v1.ts create mode 100644 src/internal/compilerSupport/v3.ts create mode 100644 src/internal/compilerSupport/v3impl/callFunction.ts create mode 100644 src/internal/compilerSupport/v3impl/conversions.ts create mode 100644 src/internal/compilerSupport/v3impl/interface.ts create mode 100644 src/internal/compilerSupport/v3impl/registerService.ts create mode 100644 src/internal/compilerSupport/v3impl/services.ts diff --git a/src/__test__/integration/compiler/compiler.spec.ts b/src/__test__/integration/compiler/compiler.spec.ts deleted file mode 100644 index dcc3f98c..00000000 --- a/src/__test__/integration/compiler/compiler.spec.ts +++ /dev/null @@ -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((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((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(); - }); -}); diff --git a/src/__test__/integration/compiler/gen1.ts b/src/__test__/integration/compiler/gen1.ts deleted file mode 100644 index 29677bb5..00000000 --- a/src/__test__/integration/compiler/gen1.ts +++ /dev/null @@ -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) => 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; -export function callMeBack( - peer: FluencePeer, - callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void, - config?: { ttl?: number }, -): Promise; -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((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()]); -} diff --git a/src/__test__/integration/compiler/gen2.ts b/src/__test__/integration/compiler/gen2.ts deleted file mode 100644 index 8e3f11aa..00000000 --- a/src/__test__/integration/compiler/gen2.ts +++ /dev/null @@ -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) => 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; -export function f1( - peer: FluencePeer, - callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void, - config?: { ttl?: number }, -): Promise; -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; -export function f2( - peer: FluencePeer, - num: number, - callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void, - config?: { ttl?: number }, -): Promise; -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; -export function f3( - peer: FluencePeer, - num: number, - callback: (arg0: string, arg1: number, callParams: CallParams<'arg0' | 'arg1'>) => void, - config?: { ttl?: number }, -): Promise; -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) => void, - config?: { ttl?: number }, -): Promise; -export function callBackZeroArgs( - peer: FluencePeer, - callback: (callParams: CallParams) => void, - config?: { ttl?: number }, -): Promise; -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, - ); -} diff --git a/src/__test__/integration/peer.spec.ts b/src/__test__/integration/peer.spec.ts index 77f292e8..6c09e84a 100644 --- a/src/__test__/integration/peer.spec.ts +++ b/src/__test__/integration/peer.spec.ts @@ -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, + }; }); }); diff --git a/src/__test__/unit/compilerSupport.spec.ts b/src/__test__/unit/compiler/v2.spec.ts similarity index 93% rename from src/__test__/unit/compilerSupport.spec.ts rename to src/__test__/unit/compiler/v2.spec.ts index a529ffb5..92155cb7 100644 --- a/src/__test__/unit/compilerSupport.spec.ts +++ b/src/__test__/unit/compiler/v2.spec.ts @@ -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 }; diff --git a/src/__test__/unit/compiler/v3.spec.ts b/src/__test__/unit/compiler/v3.spec.ts new file mode 100644 index 00000000..cba09ff2 --- /dev/null +++ b/src/__test__/unit/compiler/v3.spec.ts @@ -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); + }, + ); +}); diff --git a/src/internal/FluencePeer.ts b/src/internal/FluencePeer.ts index 683b7bcd..814828e2 100644 --- a/src/internal/FluencePeer.ts +++ b/src/internal/FluencePeer.ts @@ -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,52 +512,35 @@ 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); + const key = serviceFnKey(req.serviceId, req.fnName); + const psh = this._particleSpecificHandlers.get(particleId); + let handler: GenericCallServiceHandler; + + // we should prioritize handler for this particle if there is one + // if particle-specific handlers exist for this particle try getting handler there + if (psh !== undefined) { + handler = psh.get(key); } - // if it didn't return any result trying to run the common handler - if (res?.result === undefined) { - res = this._legacyCallServiceHandler.execute(req); + // then try to find a common handler for all particles with this service-fn key + // if there is no particle-specific handler, get one from common map + if (handler === undefined) { + handler = this._commonHandlers.get(key); } - // 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; - - // we should prioritize handler for this particle if there is one - // if particle-specific handlers exist for this particle try getting handler there - if (psh !== undefined) { - handler = psh.get(key); - } - - // then try to find a common handler for all particles with this service-fn key - // if there is no particle-specific handler, get one from common map - if (handler === undefined) { - handler = this._commonHandlers.get(key); - } - - // if we found a handler, execute it - // otherwise return useful error message to AVM - res = handler - ? await handler(req) - : { - retCode: ResultCodes.error, - result: `No handler has been registered for serviceId='${req.serviceId}' fnName='${ - req.fnName - }' args='${jsonify(req.args)}'`, - }; + // 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 + }' args='${jsonify(req.args)}'`, + }; } + // 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) { diff --git a/src/internal/compilerSupport/v1.ts b/src/internal/compilerSupport/v1.ts deleted file mode 100644 index 4a81f766..00000000 --- a/src/internal/compilerSupport/v1.ts +++ /dev/null @@ -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; - } -} diff --git a/src/internal/compilerSupport/v3.ts b/src/internal/compilerSupport/v3.ts new file mode 100644 index 00000000..e743584f --- /dev/null +++ b/src/internal/compilerSupport/v3.ts @@ -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'; diff --git a/src/internal/compilerSupport/v3impl/callFunction.ts b/src/internal/compilerSupport/v3impl/callFunction.ts new file mode 100644 index 00000000..dcaf161a --- /dev/null +++ b/src/internal/compilerSupport/v3impl/callFunction.ts @@ -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, 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, + }; +}; diff --git a/src/internal/compilerSupport/v3impl/conversions.ts b/src/internal/compilerSupport/v3impl/conversions.ts new file mode 100644 index 00000000..3f989bff --- /dev/null +++ b/src/internal/compilerSupport/v3impl/conversions.ts @@ -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) => { + 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) => { + 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(); +}; diff --git a/src/internal/compilerSupport/v3impl/interface.ts b/src/internal/compilerSupport/v3impl/interface.ts new file mode 100644 index 00000000..ce14fe71 --- /dev/null +++ b/src/internal/compilerSupport/v3impl/interface.ts @@ -0,0 +1,238 @@ +type SomeNonArrowTypes = ScalarType | OptionType | ArrayType | StructType | TopType | BottomType; + +export type NonArrowType = SomeNonArrowTypes | ProductType; + +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 = + | { + /** + * Type descriptor. Used for pattern-matching + */ + tag: 'labeledProduct'; + + /** + * Labelled product fields + */ + fields: { [key: string]: T }; + } + | NilType; + +export type UnlabeledProductType = + | { + /** + * Type descriptor. Used for pattern-matching + */ + tag: 'unlabeledProduct'; + + /** + * Items in unlabelled product + */ + items: Array; + } + | NilType; + +export type ProductType = UnlabeledProductType | LabeledProductType; + +/** + * ArrowType is a profunctor pointing its domain to codomain. + * Profunctor means variance: Arrow is contravariant on domain, and variant on codomain. + */ +export type ArrowType = { + /** + * Type descriptor. Used for pattern-matching + */ + tag: 'arrow'; + + /** + * Where this Arrow is defined + */ + domain: ProductType; + + /** + * Where this Arrow points to + */ + codomain: UnlabeledProductType | NilType; +}; + +/** + * Arrow which domain contains only non-arrow types + */ +export type ArrowWithoutCallbacks = ArrowType; + +/** + * Arrow which domain does can contain both non-arrow types and arrows (which themselves cannot contain arrows) + */ +export type ArrowWithCallbacks = ArrowType; + +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; +} + +/** + * 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; +} diff --git a/src/internal/compilerSupport/v3impl/registerService.ts b/src/internal/compilerSupport/v3impl/registerService.ts new file mode 100644 index 00000000..39deb88f --- /dev/null +++ b/src/internal/compilerSupport/v3impl/registerService.ts @@ -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, + }; +}; diff --git a/src/internal/compilerSupport/v3impl/services.ts b/src/internal/compilerSupport/v3impl/services.ts new file mode 100644 index 00000000..a1baabd0 --- /dev/null +++ b/src/internal/compilerSupport/v3impl/services.ts @@ -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 => { + 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); +};