diff --git a/src/FluenceClient.ts b/src/FluenceClient.ts index 495c07d9..45a7c04e 100644 --- a/src/FluenceClient.ts +++ b/src/FluenceClient.ts @@ -123,6 +123,7 @@ export const checkConnection = async (client: FluenceClient, ttl?: number): Prom const callbackService = '_callback'; const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( `(seq (call init_relay ("op" "identity") [msg] result) diff --git a/src/__test__/integration/client.spec.ts b/src/__test__/integration/client.spec.ts index 6a7a87c3..5182eb45 100644 --- a/src/__test__/integration/client.spec.ts +++ b/src/__test__/integration/client.spec.ts @@ -19,6 +19,7 @@ describe('Typescript usage suite', () => { // act const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( `(seq (call init_relay ("op" "identity") ["hello world!"] result) @@ -76,7 +77,9 @@ describe('Typescript usage suite', () => { data.set('c', 'some c'); data.set('d', 'some d'); - await client1.initiateFlow(new RequestFlowBuilder().withRawScript(script).withVariables(data).build()); + await client1.initiateFlow( + new RequestFlowBuilder().withDefaults().withRawScript(script).withVariables(data).build(), + ); let res = await resMakingPromise; expect(res).toEqual(['some a', 'some b', 'some c', 'some d']); @@ -186,6 +189,7 @@ describe('Typescript usage suite', () => { it('xor handling should work with connected client', async function () { // arrange const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( ` (seq @@ -210,6 +214,7 @@ describe('Typescript usage suite', () => { it('xor handling should work with local client', async function () { // arrange const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( ` (call %init_peer_id% ("service" "fails") []) diff --git a/src/__test__/unit/air.spec.ts b/src/__test__/unit/air.spec.ts index 75663fe9..22a14779 100644 --- a/src/__test__/unit/air.spec.ts +++ b/src/__test__/unit/air.spec.ts @@ -19,6 +19,7 @@ describe('== AIR suite', () => { // prettier-ignore const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript(script) .buildAsFetch(serviceId, fnName); @@ -59,6 +60,7 @@ describe('== AIR suite', () => { const script = `(incorrect)`; // prettier-ignore const [request, error] = new RequestFlowBuilder() + .withDefaults() .withRawScript(script) .buildWithErrorHandling(); @@ -75,6 +77,7 @@ describe('== AIR suite', () => { const script = `(null)`; // prettier-ignore const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withTTL(1) .withRawScript(script) .buildAsFetch(); @@ -96,6 +99,7 @@ describe('== AIR suite', () => { // prettier-ignore const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript(script) .withVariable('arg1', 'hello') .buildAsFetch(serviceId, fnName); @@ -138,7 +142,7 @@ describe('== AIR suite', () => { (call %init_peer_id% ("${makeDataServiceId}" "${makeDataFnName}") [] result) (call %init_peer_id% ("${getDataServiceId}" "${getDataFnName}") [result.$.field]) )`; - await client.initiateFlow(new RequestFlowBuilder().withRawScript(script).build()); + await client.initiateFlow(new RequestFlowBuilder().withDefaults().withRawScript(script).build()); // assert const tetraplet = res.tetraplets[0][0]; @@ -187,7 +191,7 @@ describe('== AIR suite', () => { (call %init_peer_id% ("${serviceId2}" "${fnName2}") ["${arg2}"] result2)) (call %init_peer_id% ("${serviceId3}" "${fnName3}") [result1 result2])) `; - await client.initiateFlow(new RequestFlowBuilder().withRawScript(script).build()); + await client.initiateFlow(new RequestFlowBuilder().withDefaults().withRawScript(script).build()); // assert expect(res1).toEqual(arg1); diff --git a/src/api.ts b/src/api.ts index 6e48c5d6..20b56163 100644 --- a/src/api.ts +++ b/src/api.ts @@ -44,6 +44,7 @@ export const sendParticle = async ( onError?: (err) => void, ): Promise => { const [req, errorPromise] = new RequestFlowBuilder() + .withDefaults() .withRawScript(particle.script) .withVariables(particle.data) .withTTL(particle.ttl) @@ -147,6 +148,7 @@ export const sendParticleAsFetch = async ( callbackServiceId: string = '_callback', ): Promise => { const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript(particle.script) .withVariables(particle.data) .withTTL(particle.ttl) diff --git a/src/internal/RequestFlowBuilder.ts b/src/internal/RequestFlowBuilder.ts index 8d729c93..ec05f891 100644 --- a/src/internal/RequestFlowBuilder.ts +++ b/src/internal/RequestFlowBuilder.ts @@ -1,4 +1,3 @@ -import { of } from 'ipfs-only-hash'; import log from 'loglevel'; import { AquaCallHandler } from './AquaHandler'; import { DEFAULT_TTL, RequestFlow } from './RequestFlow'; @@ -28,14 +27,47 @@ const wrapWithXor = (script: string): string => { class ScriptBuilder { private script: string; + private isXorInjected: boolean; + private shouldInjectRelay: boolean; + private variables?: string[]; + + constructor() { + this.isXorInjected = false; + this.shouldInjectRelay = false; + } raw(script: string): ScriptBuilder { this.script = script; return this; } + withInjectedVariables(fields: string[]): ScriptBuilder { + this.variables = fields; + return this; + } + + wrappedWithXor(): ScriptBuilder { + this.isXorInjected = true; + return this; + } + + withInjectedRelay(): ScriptBuilder { + this.shouldInjectRelay = true; + return this; + } + build(): string { - return this.script; + let script = this.script; + if (this.withInjectedVariables && this.withInjectedVariables.length > 0) { + script = wrapWithVariableInjectionScript(script, this.variables); + } + if (this.isXorInjected) { + script = wrapWithXor(script); + } + if (this.shouldInjectRelay) { + script = wrapWithInjectRelayScript(script); + } + return script; } } @@ -54,55 +86,36 @@ const wrapWithVariableInjectionScript = (script: string, fields: string[]): stri const wrapWithInjectRelayScript = (script: string): string => { return ` (seq - (seq - (call %init_peer_id% ("${loadVariablesService}" "${loadRelayFn}") [] ${relayVariableName}) - (call %init_peer_id% ("op" "identity") [%init_peer_id%] init_peer_id) - ) + (call %init_peer_id% ("${loadVariablesService}" "${loadRelayFn}") [] ${relayVariableName}) ${script} )`; }; +/** + * Builder class for configuring and creating Request Flows + */ export class RequestFlowBuilder { private ttl: number = DEFAULT_TTL; private variables = new Map(); - private handlerConfigs: Array<(handler: AquaCallHandler) => void> = []; - private buildScript: (sb: ScriptBuilder) => void; + private handlerConfigs: Array<(handler: AquaCallHandler, request: RequestFlow) => void> = []; + private buildScriptActions: Array<(sb: ScriptBuilder) => void> = []; private onTimeout: () => void; private onError: (error: any) => void; + /** + * Builds the Request flow with current configuration + */ build() { - if (!this.buildScript) { - throw new Error(); + const sb = new ScriptBuilder(); + for (let action of this.buildScriptActions) { + action(sb); } - - const b = new ScriptBuilder(); - this.buildScript(b); - let script = b.build(); - script = wrapWithVariableInjectionScript(script, Array.from(this.variables.keys())); - script = wrapWithXor(script); - script = wrapWithInjectRelayScript(script); + let script = sb.build(); const res = RequestFlow.createLocal(script, this.ttl); - res.handler.on(loadVariablesService, loadVariablesFn, (args, _) => { - return this.variables.get(args[0]) || {}; - }); - res.handler.onEvent(xorHandleService, xorHandleFn, (args) => { - let msg; - try { - msg = JSON.parse(args[0]); - } catch (e) { - msg = e; - } - - try { - res.raiseError(msg); - } catch (e) { - log.error('Error handling script executed with error', e); - } - }); for (let h of this.handlerConfigs) { - h(res.handler); + h(res.handler, res); } if (this.onTimeout) { @@ -115,18 +128,96 @@ export class RequestFlowBuilder { return res; } - withScript(action: (sb: ScriptBuilder) => void): RequestFlowBuilder { - this.buildScript = action; + /** + * Provides necessary defaults when building requests by hand without the Aquamarine language compiler + * Includes: relay and variable injection, error handling with top-level xor wrap + */ + withDefaults(): RequestFlowBuilder { + this.injectRelay(); + this.injectVariables(); + this.wrapWithXor(); + return this; } + /** + * Injects `init_relay` variable into the script + */ + injectRelay(): RequestFlowBuilder { + this.configureScript((sb) => { + sb.withInjectedRelay(); + }); + + return this; + } + + /** + * Registers services for variable injection. Required for variables registration to work + */ + injectVariables(): RequestFlowBuilder { + this.configureScript((sb) => { + sb.withInjectedVariables(Array.from(this.variables.keys())); + }); + + this.configHandler((h) => { + h.on(loadVariablesService, loadVariablesFn, (args, _) => { + return this.variables.get(args[0]) || {}; + }); + }); + + return this; + } + + /** + * Wraps the script with top-level error handling with xor instruction. Will raise error in the Request Flow in xor catches any error + */ + wrapWithXor(): RequestFlowBuilder { + this.configureScript((sb) => { + sb.wrappedWithXor(); + }); + + this.configHandler((h, request) => { + h.onEvent(xorHandleService, xorHandleFn, (args) => { + let msg; + try { + msg = JSON.parse(args[0]); + } catch (e) { + msg = e; + } + + try { + request.raiseError(msg); + } catch (e) { + log.error('Error handling script executed with error', e); + } + }); + }); + + return this; + } + + /** + * Use ScriptBuilder provided by action in argument to configure script of the Request Flow + */ + configureScript(action: (sb: ScriptBuilder) => void): RequestFlowBuilder { + this.buildScriptActions.push(action); + return this; + } + + /** + * Use raw text as script for the Request Flow + */ withRawScript(script: string): RequestFlowBuilder { - this.buildScript = (sb) => { + this.buildScriptActions.push((sb) => { sb.raw(script); - }; + }); + return this; } + /** + * Specify time to live for the request + */ withTTL(ttl?: number): RequestFlowBuilder { if (ttl) { this.ttl = ttl; @@ -134,26 +225,42 @@ export class RequestFlowBuilder { return this; } - configHandler(config: (handler: AquaCallHandler) => void): RequestFlowBuilder { + /** + * Configure local call handler for the Request Flow + */ + configHandler(config: (handler: AquaCallHandler, request: RequestFlow) => void): RequestFlowBuilder { this.handlerConfigs.push(config); return this; } + /** + * Specifies handler for the particle timeout event + */ handleTimeout(handler: () => void): RequestFlowBuilder { this.onTimeout = handler; return this; } + /** + * Specifies handler for any script errors + */ handleScriptError(handler: (error) => void): RequestFlowBuilder { this.onError = handler; return this; } + /** + * Adds a variable to the list of injected variables + */ withVariable(name: string, value: any): RequestFlowBuilder { this.variables.set(name, value); return this; } + /** + * Adds a multiple variable to the list of injected variables. + * Variables can be specified in form of either object or a map where keys correspond to variable names + */ withVariables(data: Map | Record): RequestFlowBuilder { if (data instanceof Map) { this.variables = new Map([...Array.from(this.variables.entries()), ...Array.from(data.entries())]); @@ -166,6 +273,11 @@ export class RequestFlowBuilder { return this; } + /** + * Builds the Request flow with current configuration with a fetch-single-result semantics + * returns a tuple of [RequestFlow, promise] where promise is a fetch-like promise resolved when + * the execution hits callback service and rejected when particle times out or any error happens + */ buildAsFetch( callbackServiceId: string = 'callback', callbackFnName: string = 'callback', @@ -189,6 +301,10 @@ export class RequestFlowBuilder { return [this.build(), fetchPromise]; } + /** + * Builds the Request flow with current configuration with error handling + * returns a tuple of [RequestFlow, promise]. The promise is never resolved and rejected in case of any error in the script + */ buildWithErrorHandling(): [RequestFlow, Promise] { const promise = new Promise((resolve, reject) => { this.handleScriptError(reject); diff --git a/src/internal/builtins.ts b/src/internal/builtins.ts index 0eea0985..6226e2f2 100644 --- a/src/internal/builtins.ts +++ b/src/internal/builtins.ts @@ -55,6 +55,7 @@ const requestResponse = async ( `; const [request, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript(script) .withVariables(data) .withTTL(ttl) @@ -72,6 +73,7 @@ const requestResponse = async ( export const getModules = async (client: FluenceClient, ttl?: number): Promise => { let callbackFn = 'getModules'; const [req, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( ` (seq @@ -100,6 +102,7 @@ export const getModules = async (client: FluenceClient, ttl?: number): Promise => { let callbackFn = 'getInterfaces'; const [req, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( ` (seq @@ -166,6 +169,7 @@ export const uploadModule = async ( data.set('myPeerId', client.selfPeerId); const [req, promise] = new RequestFlowBuilder() + .withDefaults() .withRawScript( ` (seq @@ -259,7 +263,11 @@ export const createService = async ( * @param {[number]} ttl - Optional ttl for the particle which does the job * @returns { Array } - List of available blueprints */ -export const getBlueprints = async (client: FluenceClient, nodeId?: string, ttl?: number): Promise<[{dependencies, id: string, name: string}]> => { +export const getBlueprints = async ( + client: FluenceClient, + nodeId?: string, + ttl?: number, +): Promise<[{ dependencies; id: string; name: string }]> => { let returnValue = 'blueprints'; let call = (nodeId: string) => `(call "${nodeId}" ("dist" "list_blueprints") [] ${returnValue})`;