mirror of
https://github.com/fluencelabs/fluence-js.git
synced 2025-04-25 09:52:12 +00:00
334 lines
9.6 KiB
TypeScript
334 lines
9.6 KiB
TypeScript
import log from 'loglevel';
|
|
import { CallServiceHandler, CallServiceResultType } from './CallServiceHandler';
|
|
import { DEFAULT_TTL, RequestFlow } from './RequestFlow';
|
|
|
|
export const loadVariablesService = 'load';
|
|
const loadVariablesFn = 'load_variable';
|
|
export const loadRelayFn = 'load_relay';
|
|
const xorHandleService = '__magic';
|
|
const xorHandleFn = 'handle_xor';
|
|
export const relayVariableName = 'init_relay';
|
|
|
|
const wrapWithXor = (script: string): string => {
|
|
return `
|
|
(xor
|
|
${script}
|
|
(xor
|
|
(match ${relayVariableName} ""
|
|
(call %init_peer_id% ("${xorHandleService}" "${xorHandleFn}") [%last_error%])
|
|
)
|
|
(seq
|
|
(call ${relayVariableName} ("op" "identity") [])
|
|
(call %init_peer_id% ("${xorHandleService}" "${xorHandleFn}") [%last_error%])
|
|
)
|
|
)
|
|
)`;
|
|
};
|
|
|
|
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 = [...this.variables, ...fields];
|
|
return this;
|
|
}
|
|
|
|
wrappedWithXor(): ScriptBuilder {
|
|
this.isXorInjected = true;
|
|
return this;
|
|
}
|
|
|
|
withInjectedRelay(): ScriptBuilder {
|
|
this.shouldInjectRelay = true;
|
|
return this;
|
|
}
|
|
|
|
build(): string {
|
|
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;
|
|
}
|
|
}
|
|
|
|
const wrapWithVariableInjectionScript = (script: string, fields: string[]): string => {
|
|
fields.forEach((v) => {
|
|
script = `
|
|
(seq
|
|
(call %init_peer_id% ("${loadVariablesService}" "${loadVariablesFn}") ["${v}"] ${v})
|
|
${script}
|
|
)`;
|
|
});
|
|
|
|
return script;
|
|
};
|
|
|
|
const wrapWithInjectRelayScript = (script: string): string => {
|
|
return `
|
|
(seq
|
|
(call %init_peer_id% ("${loadVariablesService}" "${loadRelayFn}") [] ${relayVariableName})
|
|
${script}
|
|
)`;
|
|
};
|
|
|
|
/**
|
|
* Builder class for configuring and creating Request Flows
|
|
*/
|
|
export class RequestFlowBuilder {
|
|
private shouldInjectVariables: boolean = true;
|
|
private shouldInjectErrorHandling: boolean = true;
|
|
private shouldInjectRelay: boolean = true;
|
|
|
|
private ttl: number = DEFAULT_TTL;
|
|
private variables = new Map<string, CallServiceResultType>();
|
|
private handlerConfigs: Array<(handler: CallServiceHandler, 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.shouldInjectRelay) {
|
|
this.injectRelay();
|
|
}
|
|
if (this.shouldInjectVariables) {
|
|
this.injectVariables();
|
|
}
|
|
if (this.shouldInjectErrorHandling) {
|
|
this.wrapWithXor();
|
|
}
|
|
|
|
const sb = new ScriptBuilder();
|
|
for (let action of this.buildScriptActions) {
|
|
action(sb);
|
|
}
|
|
let script = sb.build();
|
|
|
|
const res = RequestFlow.createLocal(script, this.ttl);
|
|
|
|
for (let h of this.handlerConfigs) {
|
|
h(res.handler, res);
|
|
}
|
|
|
|
if (this.onTimeout) {
|
|
res.onTimeout(this.onTimeout);
|
|
}
|
|
if (this.onError) {
|
|
res.onError(this.onError);
|
|
}
|
|
|
|
return res;
|
|
}
|
|
|
|
/**
|
|
* Removes necessary defaults when building requests by hand without the Aqua language compiler
|
|
* Removed features include: relay and variable injection, error handling with top-level xor wrap
|
|
*/
|
|
disableInjections(): RequestFlowBuilder {
|
|
this.shouldInjectRelay = false;
|
|
this.shouldInjectVariables = false;
|
|
this.shouldInjectErrorHandling = false;
|
|
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, _) => {
|
|
if (this.variables.has(args[0])) {
|
|
return this.variables.get(args[0]);
|
|
}
|
|
|
|
throw new Error(`failed to inject variable: ${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) => {
|
|
if (args[0] === undefined) {
|
|
log.error(
|
|
'Request flow error handler recieved unexpected argument, value of %last_error% is undefined',
|
|
);
|
|
}
|
|
|
|
try {
|
|
request.raiseError(args[0]);
|
|
} 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.buildScriptActions.push((sb) => {
|
|
sb.raw(script);
|
|
});
|
|
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Specify time to live for the request
|
|
*/
|
|
withTTL(ttl?: number): RequestFlowBuilder {
|
|
if (ttl) {
|
|
this.ttl = ttl;
|
|
}
|
|
return this;
|
|
}
|
|
|
|
/**
|
|
* Configure local call handler for the Request Flow
|
|
*/
|
|
configHandler(config: (handler: CallServiceHandler, 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: CallServiceResultType): 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<string, CallServiceResultType> | Record<string, CallServiceResultType>,
|
|
): RequestFlowBuilder {
|
|
if (data instanceof Map) {
|
|
this.variables = new Map([...Array.from(this.variables.entries()), ...Array.from(data.entries())]);
|
|
} else {
|
|
for (let k in data) {
|
|
this.variables.set(k, data[k]);
|
|
}
|
|
}
|
|
|
|
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>(
|
|
callbackServiceId: string = 'callback',
|
|
callbackFnName: string = 'callback',
|
|
): [RequestFlow, Promise<T>] {
|
|
const fetchPromise = new Promise<T>((resolve, reject) => {
|
|
this.handlerConfigs.push((h) => {
|
|
h.onEvent(callbackServiceId, callbackFnName, (args, _) => {
|
|
resolve(args as any);
|
|
});
|
|
});
|
|
|
|
this.handleTimeout(() => {
|
|
reject(`Timed out after ${this.ttl}ms`);
|
|
});
|
|
|
|
this.handleScriptError((e) => {
|
|
reject(e);
|
|
});
|
|
});
|
|
|
|
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>] {
|
|
const promise = new Promise<void>((resolve, reject) => {
|
|
this.handleScriptError(reject);
|
|
});
|
|
|
|
return [this.build(), promise];
|
|
}
|
|
}
|