Skip to content
This repository has been archived by the owner on Aug 14, 2021. It is now read-only.

Commit

Permalink
feat: added event system (CORE-5263) (#28)
Browse files Browse the repository at this point in the history
* wip: added EventsManager

* fix: ambiguous exports

* refactor: refactoring trace types

* feat: added all interface methods

* refactor: refactored to use common handler signature

* style: linting

* style: renaming classes

* test: added tests for EventManager

* test: unit tests for maximum coverage

* style: linting errors

Co-authored-by: Zhiheng Lu <[email protected]>
  • Loading branch information
zhihil and Zhiheng Lu authored Feb 20, 2021
1 parent 801528b commit 0a1cc44
Show file tree
Hide file tree
Showing 33 changed files with 641 additions and 333 deletions.
2 changes: 0 additions & 2 deletions index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,4 @@ export * from '@/lib/Client';
export * from '@/lib/types';
export * from '@/lib/Utils';

export { TraceType } from '@voiceflow/general-types';

export default RuntimeClientFactory;
47 changes: 47 additions & 0 deletions lib/Client/adapters/addAudioSrc.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { GeneralTrace as DBGeneralTrace, SpeakTrace, TraceType } from '@voiceflow/general-types';
import { SpeakType } from '@voiceflow/general-types/build/nodes/speak';
import htmlParse from 'html-parse-stringify';

import { DBResponseContext } from './types';

/**
* WORK-AROUND function to deal with bug where enabling `tts` will cause the Audio Step's audio
* file url to not generate. Better solution is to decouple the TTS and audio handlers in our
* backend and remove `parseAudioStepSrc` and `adaptResponseContext`
*/
export const parseAudioStepSrc = (trace: DBGeneralTrace): DBGeneralTrace => {
if (trace.type !== TraceType.SPEAK) {
return trace;
}

const node = htmlParse.parse(trace.payload.message)[0];

if (!node || node.name !== 'audio') {
return {
...trace,
payload: {
...trace.payload,
type: SpeakType.MESSAGE,
},
} as SpeakTrace;
}

const audioSrc = node.attrs.src;

return {
...trace,
payload: {
...trace.payload,
type: SpeakType.AUDIO,
src: audioSrc,
},
};
};

/**
* WORK-AROUND function, see `parseAudioStepSrc` above
*/
export const adaptResponseContext = (context: DBResponseContext): DBResponseContext => ({
...context,
trace: context.trace.map(parseAudioStepSrc),
});
21 changes: 21 additions & 0 deletions lib/Client/adapters/extractAudioStep.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { GeneralTrace as DBGeneralTrace, TraceType as DBTraceType } from '@voiceflow/general-types';
import { SpeakType } from '@voiceflow/general-types/build/nodes/speak';

import { GeneralTrace, ResponseContext, TraceType } from '@/lib/types';

export const extractAudioStep = (context: Omit<ResponseContext, 'trace'> & { trace: DBGeneralTrace[] }) => ({
...context,
trace: context.trace.map((trace) => {
if (trace.type !== DBTraceType.SPEAK) {
return (trace as unknown) as GeneralTrace;
}
const { type, ...payload } = trace.payload;

return ({
type: type === SpeakType.MESSAGE ? TraceType.SPEAK : TraceType.AUDIO,
payload,
} as unknown) as GeneralTrace;
}),
});

export default extractAudioStep;
49 changes: 2 additions & 47 deletions lib/Client/adapters/index.ts
Original file line number Diff line number Diff line change
@@ -1,47 +1,2 @@
import { GeneralTrace, SpeakTrace, TraceType } from '@voiceflow/general-types';
import { SpeakType } from '@voiceflow/general-types/build/nodes/speak';
import htmlParse from 'html-parse-stringify';

import { ResponseContext } from '../../types';

/**
* WORK-AROUND function to deal with bug where enabling `tts` will cause the Audio Step's audio
* file url to not generate. Better solution is to decouple the TTS and audio handlers in our
* backend and remove `parseAudioStepSrc` and `adaptResponseContext`
*/
export const parseAudioStepSrc = (trace: GeneralTrace): GeneralTrace => {
if (trace.type !== TraceType.SPEAK) {
return trace;
}

const node = htmlParse.parse(trace.payload.message)[0];

if (!node || node.name !== 'audio') {
return {
...trace,
payload: {
...trace.payload,
type: SpeakType.MESSAGE,
},
} as SpeakTrace;
}

const audioSrc = node.attrs.src;

return {
...trace,
payload: {
...trace.payload,
type: SpeakType.AUDIO,
src: audioSrc,
},
};
};

/**
* WORK-AROUND function, see `parseAudioStepSrc` above
*/
export const adaptResponseContext = (context: ResponseContext) => ({
...context,
trace: context.trace.map(parseAudioStepSrc),
});
export { adaptResponseContext } from './addAudioSrc';
export { extractAudioStep } from './extractAudioStep';
7 changes: 7 additions & 0 deletions lib/Client/adapters/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import { GeneralTrace as DBGeneralTrace } from '@voiceflow/general-types';

import { ResponseContext } from '@/lib/types';

export type DBResponseContext = Omit<ResponseContext, 'trace'> & {
trace: DBGeneralTrace[];
};
5 changes: 3 additions & 2 deletions lib/Client/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import _cloneDeep from 'lodash/cloneDeep';

import { RequestContext, ResponseContext } from '@/lib/types';

import { adaptResponseContext } from './adapters';
import { adaptResponseContext, extractAudioStep } from './adapters';

export type ClientConfig<S> = { variables?: Partial<S>; endpoint: string; versionID: string };

Expand Down Expand Up @@ -42,7 +42,8 @@ export class Client<S extends Record<string, any> = Record<string, any>> {
return this.axios
.post(`/interact/${this.versionID}`, body)
.then((response) => response.data)
.then((context) => adaptResponseContext(context));
.then((context) => adaptResponseContext(context))
.then((context) => extractAudioStep(context));
}

getVersionID() {
Expand Down
2 changes: 1 addition & 1 deletion lib/Common/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { TraceType } from '@voiceflow/general-types';
import { TraceType } from '../types';

export const validTraceTypes = new Set(Object.keys(TraceType));

Expand Down
4 changes: 1 addition & 3 deletions lib/Context/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { TraceType } from '@voiceflow/general-types';

import DataFilterer from '@/lib/DataFilterer';
import { Choice, DataConfig, ResponseContext } from '@/lib/types';
import { Choice, DataConfig, ResponseContext, TraceType } from '@/lib/types';

import VariableManager from '../Variables';

Expand Down
4 changes: 1 addition & 3 deletions lib/DataFilterer/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
import { GeneralTrace, TraceType } from '@voiceflow/general-types';

import { VFTypeError } from '@/lib/Common';
import { DataConfig } from '@/lib/types';
import { DataConfig, GeneralTrace, TraceType } from '@/lib/types';

import { isValidTraceType, stripSSMLFromSpeak } from './utils';

Expand Down
3 changes: 1 addition & 2 deletions lib/DataFilterer/utils.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { GeneralTrace, TraceType } from '@voiceflow/general-types';

import { validTraceTypes } from '@/lib/Common';
import { GeneralTrace, TraceType } from '@/lib/types';

export const SSML_TAG_REGEX = /<\/?[^>]+(>|$)/g;

Expand Down
40 changes: 40 additions & 0 deletions lib/Events/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import { GeneralTrace, TraceMap, TraceType } from '@/lib/types';

import Context from '../Context';

export type TraceEventHandler<T extends TraceType, V extends Record<string, any>> = (object: TraceMap[T], context: Context<V>) => void;

export type GeneralTraceEventHandler<V extends Record<string, any>> = (object: GeneralTrace, context: Context<V>) => void;

type _Map<T extends Record<string, any>, K extends TraceType = TraceType> = Map<K, Array<TraceEventHandler<K, T>>>;

export class EventManager<V extends Record<string, any>> {
private specHandlers: _Map<V>;

private genHandlers: GeneralTraceEventHandler<V>[];

constructor() {
this.specHandlers = new Map();
const traceTypeVals = Object.keys(TraceType).map((type) => type.toLowerCase()) as TraceType[];
traceTypeVals.forEach((traceType) => this.specHandlers.set(traceType, []));

this.genHandlers = [];
}

on<T extends TraceType>(event: T, handler: TraceEventHandler<T, V>) {
const handlerList = this.specHandlers.get(event)! as TraceEventHandler<T, V>[];
handlerList.push(handler);
}

onAny(handler: GeneralTraceEventHandler<V>) {
this.genHandlers.push(handler);
}

handle<T extends TraceType>(trace: TraceMap[T], context: Context<V>) {
this.specHandlers.get(trace.type)!.forEach((handler: TraceEventHandler<T, V>) => handler(trace, context));

this.genHandlers.forEach((handler: GeneralTraceEventHandler<V>) => handler(trace, context));
}
}

export default EventManager;
71 changes: 63 additions & 8 deletions lib/RuntimeClient/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,43 @@ import { GeneralRequest, RequestType } from '@voiceflow/general-types';
import { State } from '@voiceflow/runtime';

import Client from '@/lib/Client';
import { VFClientError } from '@/lib/Common';
import { VFClientError, VFTypeError } from '@/lib/Common';
import Context from '@/lib/Context';
import { DataConfig, ResponseContext } from '@/lib/types';
import EventManager, { GeneralTraceEventHandler, TraceEventHandler } from '@/lib/Events';
import { DataConfig, ResponseContext, TRACE_EVENT, TraceType } from '@/lib/types';

import { isValidTraceType } from '../DataFilterer/utils';
import { makeRequestBody, resetContext } from './utils';

export class RuntimeClient<S extends Record<string, any> = Record<string, any>> {
private client: Client<S>;
type OnMethodHandlerArgMap<V> = {
[K in TraceType]: TraceEventHandler<K, V>;
} & {
trace: GeneralTraceEventHandler<V>;
};

export class RuntimeClient<V extends Record<string, any> = Record<string, any>> {
private client: Client<V>;

private dataConfig: DataConfig;

private context: Context<S>;
private context: Context<V>;

private events: EventManager<V>;

constructor(state: State, { client, dataConfig = {} }: { client: Client<S>; dataConfig?: DataConfig }) {
constructor(state: State, { client, dataConfig = {} }: { client: Client<V>; dataConfig?: DataConfig }) {
this.client = client;
this.dataConfig = dataConfig;
this.events = new EventManager();

this.context = new Context({ request: null, state, trace: [] }, this.dataConfig);
}

async start(): Promise<Context<S>> {
async start(): Promise<Context<V>> {
this.context = new Context(resetContext(this.context.toJSON()), this.dataConfig);
return this.sendRequest(null);
}

async sendText(userInput: string): Promise<Context<S>> {
async sendText(userInput: string): Promise<Context<V>> {
if (!userInput?.trim?.()) {
return this.sendRequest(null);
}
Expand All @@ -44,9 +55,53 @@ export class RuntimeClient<S extends Record<string, any> = Record<string, any>>
this.context!.getResponse().forEach(this.dataConfig.traceProcessor);
}

this.context!.getTrace().forEach((trace) => this.events.handle(trace, this.context!));

return this.context;
}

on<T extends TraceType | TRACE_EVENT>(event: T, handler: OnMethodHandlerArgMap<V>[T]) {
if (event === TRACE_EVENT) {
return this.events.onAny(handler as GeneralTraceEventHandler<V>);
}
if (isValidTraceType(event)) {
return this.events.on(event as any, handler as any);
}
throw new VFTypeError(`event "${event}" is not valid`);
}

onSpeak(handler: TraceEventHandler<TraceType.SPEAK, V>) {
this.events.on(TraceType.SPEAK, handler);
}

onAudio(handler: TraceEventHandler<TraceType.AUDIO, V>) {
this.events.on(TraceType.AUDIO, handler);
}

onBlock(handler: TraceEventHandler<TraceType.BLOCK, V>) {
this.events.on(TraceType.BLOCK, handler);
}

onDebug(handler: TraceEventHandler<TraceType.DEBUG, V>) {
this.events.on(TraceType.DEBUG, handler);
}

onEnd(handler: TraceEventHandler<TraceType.END, V>) {
this.events.on(TraceType.END, handler);
}

onFlow(handler: TraceEventHandler<TraceType.FLOW, V>) {
this.events.on(TraceType.FLOW, handler);
}

onVisual(handler: TraceEventHandler<TraceType.VISUAL, V>) {
this.events.on(TraceType.VISUAL, handler);
}

onChoice(handler: TraceEventHandler<TraceType.CHOICE, V>) {
this.events.on(TraceType.CHOICE, handler);
}

setContext(contextJSON: ResponseContext) {
this.context = new Context(contextJSON, this.dataConfig);
}
Expand Down
9 changes: 9 additions & 0 deletions lib/Utils/makeTraceProcessor/audio.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { AudioTrace, AudioTraceHandler } from '@/lib/types';

export const invokeAudioHandler = (trace: AudioTrace, handler: AudioTraceHandler) => {
const {
payload: { message, src },
} = trace;
return handler(message, src);
};
export default invokeAudioHandler;
5 changes: 2 additions & 3 deletions lib/Utils/makeTraceProcessor/block.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { BlockTrace } from '@voiceflow/general-types';

export type BlockTraceHandler = (blockID: BlockTrace['payload']['blockID']) => any;
import { BlockTrace, BlockTraceHandler } from '@/lib/types';

export const invokeBlockHandler = (trace: BlockTrace, handler: BlockTraceHandler) => handler(trace.payload.blockID);
export default invokeBlockHandler;
5 changes: 2 additions & 3 deletions lib/Utils/makeTraceProcessor/choice.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ChoiceTrace } from '@voiceflow/general-types';

export type ChoiceTraceHandler = (choices: ChoiceTrace['payload']['choices']) => any;
import { ChoiceTrace, ChoiceTraceHandler } from '@/lib/types';

export const invokeChoiceHandler = (trace: ChoiceTrace, handler: ChoiceTraceHandler) => handler(trace.payload.choices);
export default invokeChoiceHandler;
5 changes: 2 additions & 3 deletions lib/Utils/makeTraceProcessor/debug.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { DebugTrace } from '@voiceflow/general-types';

export type DebugTraceHandler = (message: DebugTrace['payload']['message']) => any;
import { DebugTrace, DebugTraceHandler } from '@/lib/types';

export const invokeDebugHandler = (trace: DebugTrace, handler: DebugTraceHandler) => handler(trace.payload.message);
export default invokeDebugHandler;
7 changes: 3 additions & 4 deletions lib/Utils/makeTraceProcessor/end.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ExitTrace } from '@voiceflow/general-types';
import { EndTrace, EndTraceHandler } from '@/lib/types';

export type EndTraceHandler = () => any;

export const invokeEndHandler = (_: ExitTrace, handler: EndTraceHandler) => handler();
export const invokeEndHandler = (_: EndTrace, handler: EndTraceHandler) => handler();
export default invokeEndHandler;
5 changes: 2 additions & 3 deletions lib/Utils/makeTraceProcessor/flow.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { FlowTrace } from '@voiceflow/general-types';

export type FlowTraceHandler = (diagramID: FlowTrace['payload']['diagramID']) => any;
import { FlowTrace, FlowTraceHandler } from '@/lib/types';

export const invokeFlowHandler = (trace: FlowTrace, handler: FlowTraceHandler) => handler(trace.payload.diagramID);
export default invokeFlowHandler;
Loading

0 comments on commit 0a1cc44

Please sign in to comment.