Extract defaults (#34)

* Extracted default request flow builder behavior out of the constructor

* Remove init_peer_id variable, which stands in the way when building apps with aquamarine
This commit is contained in:
Pavel 2021-03-29 23:52:46 +03:00 committed by GitHub
parent 0ff10a25de
commit 5355eeb152
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 180 additions and 44 deletions

View File

@ -123,6 +123,7 @@ export const checkConnection = async (client: FluenceClient, ttl?: number): Prom
const callbackService = '_callback'; const callbackService = '_callback';
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
`(seq `(seq
(call init_relay ("op" "identity") [msg] result) (call init_relay ("op" "identity") [msg] result)

View File

@ -19,6 +19,7 @@ describe('Typescript usage suite', () => {
// act // act
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
`(seq `(seq
(call init_relay ("op" "identity") ["hello world!"] result) (call init_relay ("op" "identity") ["hello world!"] result)
@ -76,7 +77,9 @@ describe('Typescript usage suite', () => {
data.set('c', 'some c'); data.set('c', 'some c');
data.set('d', 'some d'); 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; let res = await resMakingPromise;
expect(res).toEqual(['some a', 'some b', 'some c', 'some d']); 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 () { it('xor handling should work with connected client', async function () {
// arrange // arrange
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
` `
(seq (seq
@ -210,6 +214,7 @@ describe('Typescript usage suite', () => {
it('xor handling should work with local client', async function () { it('xor handling should work with local client', async function () {
// arrange // arrange
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
` `
(call %init_peer_id% ("service" "fails") []) (call %init_peer_id% ("service" "fails") [])

View File

@ -19,6 +19,7 @@ describe('== AIR suite', () => {
// prettier-ignore // prettier-ignore
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript(script) .withRawScript(script)
.buildAsFetch<string[]>(serviceId, fnName); .buildAsFetch<string[]>(serviceId, fnName);
@ -59,6 +60,7 @@ describe('== AIR suite', () => {
const script = `(incorrect)`; const script = `(incorrect)`;
// prettier-ignore // prettier-ignore
const [request, error] = new RequestFlowBuilder() const [request, error] = new RequestFlowBuilder()
.withDefaults()
.withRawScript(script) .withRawScript(script)
.buildWithErrorHandling(); .buildWithErrorHandling();
@ -75,6 +77,7 @@ describe('== AIR suite', () => {
const script = `(null)`; const script = `(null)`;
// prettier-ignore // prettier-ignore
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withTTL(1) .withTTL(1)
.withRawScript(script) .withRawScript(script)
.buildAsFetch(); .buildAsFetch();
@ -96,6 +99,7 @@ describe('== AIR suite', () => {
// prettier-ignore // prettier-ignore
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript(script) .withRawScript(script)
.withVariable('arg1', 'hello') .withVariable('arg1', 'hello')
.buildAsFetch<string[]>(serviceId, fnName); .buildAsFetch<string[]>(serviceId, fnName);
@ -138,7 +142,7 @@ describe('== AIR suite', () => {
(call %init_peer_id% ("${makeDataServiceId}" "${makeDataFnName}") [] result) (call %init_peer_id% ("${makeDataServiceId}" "${makeDataFnName}") [] result)
(call %init_peer_id% ("${getDataServiceId}" "${getDataFnName}") [result.$.field]) (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 // assert
const tetraplet = res.tetraplets[0][0]; 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% ("${serviceId2}" "${fnName2}") ["${arg2}"] result2))
(call %init_peer_id% ("${serviceId3}" "${fnName3}") [result1 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 // assert
expect(res1).toEqual(arg1); expect(res1).toEqual(arg1);

View File

@ -44,6 +44,7 @@ export const sendParticle = async (
onError?: (err) => void, onError?: (err) => void,
): Promise<string> => { ): Promise<string> => {
const [req, errorPromise] = new RequestFlowBuilder() const [req, errorPromise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript(particle.script) .withRawScript(particle.script)
.withVariables(particle.data) .withVariables(particle.data)
.withTTL(particle.ttl) .withTTL(particle.ttl)
@ -147,6 +148,7 @@ export const sendParticleAsFetch = async <T>(
callbackServiceId: string = '_callback', callbackServiceId: string = '_callback',
): Promise<T> => { ): Promise<T> => {
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript(particle.script) .withRawScript(particle.script)
.withVariables(particle.data) .withVariables(particle.data)
.withTTL(particle.ttl) .withTTL(particle.ttl)

View File

@ -1,4 +1,3 @@
import { of } from 'ipfs-only-hash';
import log from 'loglevel'; import log from 'loglevel';
import { AquaCallHandler } from './AquaHandler'; import { AquaCallHandler } from './AquaHandler';
import { DEFAULT_TTL, RequestFlow } from './RequestFlow'; import { DEFAULT_TTL, RequestFlow } from './RequestFlow';
@ -28,14 +27,47 @@ const wrapWithXor = (script: string): string => {
class ScriptBuilder { class ScriptBuilder {
private script: string; private script: string;
private isXorInjected: boolean;
private shouldInjectRelay: boolean;
private variables?: string[];
constructor() {
this.isXorInjected = false;
this.shouldInjectRelay = false;
}
raw(script: string): ScriptBuilder { raw(script: string): ScriptBuilder {
this.script = script; this.script = script;
return this; 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 { 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 => { const wrapWithInjectRelayScript = (script: string): string => {
return ` return `
(seq (seq
(seq
(call %init_peer_id% ("${loadVariablesService}" "${loadRelayFn}") [] ${relayVariableName}) (call %init_peer_id% ("${loadVariablesService}" "${loadRelayFn}") [] ${relayVariableName})
(call %init_peer_id% ("op" "identity") [%init_peer_id%] init_peer_id)
)
${script} ${script}
)`; )`;
}; };
/**
* Builder class for configuring and creating Request Flows
*/
export class RequestFlowBuilder { export class RequestFlowBuilder {
private ttl: number = DEFAULT_TTL; private ttl: number = DEFAULT_TTL;
private variables = new Map<string, any>(); private variables = new Map<string, any>();
private handlerConfigs: Array<(handler: AquaCallHandler) => void> = []; private handlerConfigs: Array<(handler: AquaCallHandler, request: RequestFlow) => void> = [];
private buildScript: (sb: ScriptBuilder) => void; private buildScriptActions: Array<(sb: ScriptBuilder) => void> = [];
private onTimeout: () => void; private onTimeout: () => void;
private onError: (error: any) => void; private onError: (error: any) => void;
/**
* Builds the Request flow with current configuration
*/
build() { build() {
if (!this.buildScript) { const sb = new ScriptBuilder();
throw new Error(); for (let action of this.buildScriptActions) {
action(sb);
} }
let script = sb.build();
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);
const res = RequestFlow.createLocal(script, this.ttl); 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) { for (let h of this.handlerConfigs) {
h(res.handler); h(res.handler, res);
} }
if (this.onTimeout) { if (this.onTimeout) {
@ -115,18 +128,96 @@ export class RequestFlowBuilder {
return res; 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; 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 { withRawScript(script: string): RequestFlowBuilder {
this.buildScript = (sb) => { this.buildScriptActions.push((sb) => {
sb.raw(script); sb.raw(script);
}; });
return this; return this;
} }
/**
* Specify time to live for the request
*/
withTTL(ttl?: number): RequestFlowBuilder { withTTL(ttl?: number): RequestFlowBuilder {
if (ttl) { if (ttl) {
this.ttl = ttl; this.ttl = ttl;
@ -134,26 +225,42 @@ export class RequestFlowBuilder {
return this; 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); this.handlerConfigs.push(config);
return this; return this;
} }
/**
* Specifies handler for the particle timeout event
*/
handleTimeout(handler: () => void): RequestFlowBuilder { handleTimeout(handler: () => void): RequestFlowBuilder {
this.onTimeout = handler; this.onTimeout = handler;
return this; return this;
} }
/**
* Specifies handler for any script errors
*/
handleScriptError(handler: (error) => void): RequestFlowBuilder { handleScriptError(handler: (error) => void): RequestFlowBuilder {
this.onError = handler; this.onError = handler;
return this; return this;
} }
/**
* Adds a variable to the list of injected variables
*/
withVariable(name: string, value: any): RequestFlowBuilder { withVariable(name: string, value: any): RequestFlowBuilder {
this.variables.set(name, value); this.variables.set(name, value);
return this; 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<string, any> | Record<string, any>): RequestFlowBuilder { withVariables(data: Map<string, any> | Record<string, any>): RequestFlowBuilder {
if (data instanceof Map) { if (data instanceof Map) {
this.variables = new Map([...Array.from(this.variables.entries()), ...Array.from(data.entries())]); this.variables = new Map([...Array.from(this.variables.entries()), ...Array.from(data.entries())]);
@ -166,6 +273,11 @@ export class RequestFlowBuilder {
return this; 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<T>( buildAsFetch<T>(
callbackServiceId: string = 'callback', callbackServiceId: string = 'callback',
callbackFnName: string = 'callback', callbackFnName: string = 'callback',
@ -189,6 +301,10 @@ export class RequestFlowBuilder {
return [this.build(), fetchPromise]; 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<void>] { buildWithErrorHandling(): [RequestFlow, Promise<void>] {
const promise = new Promise<void>((resolve, reject) => { const promise = new Promise<void>((resolve, reject) => {
this.handleScriptError(reject); this.handleScriptError(reject);

View File

@ -55,6 +55,7 @@ const requestResponse = async <T>(
`; `;
const [request, promise] = new RequestFlowBuilder() const [request, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript(script) .withRawScript(script)
.withVariables(data) .withVariables(data)
.withTTL(ttl) .withTTL(ttl)
@ -72,6 +73,7 @@ const requestResponse = async <T>(
export const getModules = async (client: FluenceClient, ttl?: number): Promise<string[]> => { export const getModules = async (client: FluenceClient, ttl?: number): Promise<string[]> => {
let callbackFn = 'getModules'; let callbackFn = 'getModules';
const [req, promise] = new RequestFlowBuilder() const [req, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
` `
(seq (seq
@ -100,6 +102,7 @@ export const getModules = async (client: FluenceClient, ttl?: number): Promise<s
export const getInterfaces = async (client: FluenceClient, ttl?: number): Promise<string[]> => { export const getInterfaces = async (client: FluenceClient, ttl?: number): Promise<string[]> => {
let callbackFn = 'getInterfaces'; let callbackFn = 'getInterfaces';
const [req, promise] = new RequestFlowBuilder() const [req, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
` `
(seq (seq
@ -166,6 +169,7 @@ export const uploadModule = async (
data.set('myPeerId', client.selfPeerId); data.set('myPeerId', client.selfPeerId);
const [req, promise] = new RequestFlowBuilder() const [req, promise] = new RequestFlowBuilder()
.withDefaults()
.withRawScript( .withRawScript(
` `
(seq (seq
@ -259,7 +263,11 @@ export const createService = async (
* @param {[number]} ttl - Optional ttl for the particle which does the job * @param {[number]} ttl - Optional ttl for the particle which does the job
* @returns { Array<object> } - List of available blueprints * @returns { Array<object> } - 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 returnValue = 'blueprints';
let call = (nodeId: string) => `(call "${nodeId}" ("dist" "list_blueprints") [] ${returnValue})`; let call = (nodeId: string) => `(call "${nodeId}" ("dist" "list_blueprints") [] ${returnValue})`;