Skip to content
This repository has been archived by the owner on Jul 21, 2020. It is now read-only.

[WIP] feat(participant): allow method interception #30

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions src/ControlState.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { EventEmitter } from 'eventemitter3';
import { IGroup, IIncomingPacket, IScene } from './interfaces';

/**
* Internally retains data required to acquire a control's kind.
* @private
*/
export class ControlsState extends EventEmitter {
private scenes: {
[sceneID: string]: { [controlID: string]: { kind: string; cost: number } };
} = {};
private groups: { [groupID: string]: string } = {};
private currentGroup = 'default';

/**
* Handles a packet sent from the client.
*/
public handleIncomingPacket({ type, method, params }: IIncomingPacket) {
if (type !== 'method') {
return;
}

if (method === 'onControlCreate' || method === 'onControlUpdate') {
this.cacheScene(params, true);
}

if (method === 'onSceneCreate') {
params.scenes.forEach((scene: IScene) => {
this.cacheScene(scene);
});
}

if (method === 'onSceneDelete') {
delete this.scenes[params.sceneID];
}

if (method === 'onGroupCreate' || method === 'onGroupUpdate') {
params.groups.forEach((group: IGroup) => {
this.cacheGroup(group);
});
}

if (method === 'onGroupDelete') {
delete this.groups[params.groupID];
}

if (method === 'onParticipantJoin' || method === 'onParticipantUpdate') {
this.currentGroup = params.participants[0].groupID;
}
}

/**
* Gets a control's kind by its control ID.
*/
public getControlKind(controlID: string) {
return this.scenes[this.groups[this.currentGroup]][controlID].kind;
}

public getControlCost(controlID: string) {
return this.scenes[this.groups[this.currentGroup]][controlID].cost;
}

/**
* Caches the control kind for a scene.
*/
private cacheScene(scene: IScene, isPartial = false) {
if (!this.scenes[scene.sceneID] || !isPartial) {
this.scenes[scene.sceneID] = {};
}

if (!scene.controls) {
return;
}

scene.controls.forEach(control => {
this.scenes[scene.sceneID][control.controlID] = {
kind: control.kind,
cost: control.cost,
};
});
}

/**
* Caches a group.
*/
private cacheGroup(group: IGroup) {
this.groups[group.groupID] = group.sceneID;
}
}
26 changes: 26 additions & 0 deletions src/intercept.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
export type InterceptorFn = (params: any) => Promise<boolean>;

/**
* Interceptor Manager allows for the storage of method names to interceptor functions.
*
* It is used to gate a method from being transmitted to the Interactive server.
*
* @example interceptor.intercept(params => if(params.potato) { return Promise.resolve(false); })
*/
export class InterceptorManager {
public methods: Map<string, InterceptorFn> = new Map<string, InterceptorFn>();

public add(method: string, interceptor: InterceptorFn) {
this.methods.set(method, interceptor);
}
public has(method: string) {
return this.methods.has(method);
}

public run(method: string, params: any): Promise<boolean> {
if (this.has(method)) {
return this.methods.get(method)!(params);
}
return Promise.resolve(true);
}
}
24 changes: 24 additions & 0 deletions src/interfaces.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
export interface IScene {
sceneID: string;
controls: IControl[] | null;
}

export interface IControl {
controlID: string;
kind: string;
cost: number;
}

export interface IGroup {
groupID: string;
sceneID: string;
}

/**
* Represents raw data received from the interactive server.
*/
export interface IIncomingPacket {
type: string;
method: string;
params: any;
}
170 changes: 45 additions & 125 deletions src/participant.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import { EventEmitter } from 'eventemitter3';
import { stringify } from 'querystring';

import { ControlsState } from './ControlState';
import { InterceptorManager } from './intercept';
import { IIncomingPacket } from './interfaces';
import { IPostable, RPC, RPCError } from './rpc';
import { ErrorCode, ILogEntry, ISettings, IStateDump, IVideoPositionOptions } from './typings';

Expand Down Expand Up @@ -77,108 +80,6 @@ function appendQueryString(url: string, qs: object) {
return `${url}${delimiter}${stringify(qs)}`;
}

/**
* Represents raw data received from the interactive server.
*/
interface IIncomingPacket {
type: string;
method: string;
params: any;
}

interface IScene {
sceneID: string;
controls: IControl[] | null;
}

interface IControl {
controlID: string;
kind: string;
}

interface IGroup {
groupID: string;
sceneID: string;
}

/**
* Internally retains data required to acquire a control's kind.
* @private
*/
class ControlsState extends EventEmitter {
private scenes: { [sceneID: string]: { [controlID: string]: string } } = {};
private groups: { [groupID: string]: string } = {};
private currentGroup = 'default';

/**
* Handles a packet sent from the client.
*/
public handleIncomingPacket({ type, method, params }: IIncomingPacket) {
if (type !== 'method') {
return;
}

if (method === 'onControlCreate' || method === 'onControlUpdate') {
this.cacheScene(params, true);
}

if (method === 'onSceneCreate') {
params.scenes.forEach((scene: IScene) => {
this.cacheScene(scene);
});
}

if (method === 'onSceneDelete') {
delete this.scenes[params.sceneID];
}

if (method === 'onGroupCreate' || method === 'onGroupUpdate') {
params.groups.forEach((group: IGroup) => {
this.cacheGroup(group);
});
}

if (method === 'onGroupDelete') {
delete this.groups[params.groupID];
}

if (method === 'onParticipantJoin' || method === 'onParticipantUpdate') {
this.currentGroup = params.participants[0].groupID;
}
}

/**
* Gets a control's kind by its control ID.
*/
public getControlKind(controlID: string) {
return this.scenes[this.groups[this.currentGroup]][controlID];
}

/**
* Caches the control kind for a scene.
*/
private cacheScene(scene: IScene, isPartial = false) {
if (!this.scenes[scene.sceneID] || !isPartial) {
this.scenes[scene.sceneID] = {};
}

if (!scene.controls) {
return;
}

scene.controls.forEach(control => {
this.scenes[scene.sceneID][control.controlID] = control.kind;
});
}

/**
* Caches a group.
*/
private cacheGroup(group: IGroup) {
this.groups[group.groupID] = group.sceneID;
}
}

/**
* Participant is a bridge between the Interactive service and an iframe that
* shows custom controls. It proxies calls between them and emits events
Expand All @@ -191,8 +92,10 @@ export class Participant extends EventEmitter {
*/
public static readonly protocolVersion = '2.0';

public interceptor = new InterceptorManager();

/**
* Websocket connecte
* Websocket connected
*/
private websocket?: WebSocket;

Expand Down Expand Up @@ -420,8 +323,13 @@ export class Participant extends EventEmitter {
public on(event: string, handler: (...args: any[]) => void): this;
public on(event: string, handler: (...args: any[]) => void): this {
this.exposeRPC(event, (...params: any[]) => {
params.splice(0, 0, event);
this.emit.apply(this, params);
this.interceptor.run(event, params).then(res => {
if (!res) {
return;
}
params.splice(0, 0, event);
this.emit.apply(this, params);
});
});
super.on(event, handler);
return this;
Expand All @@ -438,6 +346,13 @@ export class Participant extends EventEmitter {
}
}

public getControlKind(controlId: string) {
return this.controls.getControlKind(controlId);
}
public getControlCost(controlId: string) {
return this.controls.getControlCost(controlId);
}

/**
* sendInteractive broadcasts the interactive payload down to the controls,
* and emits a `transmit` event.
Expand All @@ -457,6 +372,17 @@ export class Participant extends EventEmitter {
});
}

private giveInputHandler(data: { method: string; params: any }) {
const kind = this.controls.getControlKind(data.params.controlID);
if (!kind) {
return;
}

this.emit('input', {
...data.params,
kind,
});
}
/**
* attachListeners is called once the frame contents load to boot up
* the RPC system.
Expand All @@ -472,26 +398,20 @@ export class Participant extends EventEmitter {
);

this.exposeRPC<{ method: string; params: any }>('sendInteractivePacket', data => {
this.websocket!.send(
JSON.stringify({
...data,
type: 'method',
discard: true,
}),
);

if (data.method !== 'giveInput') {
return;
}

const kind = this.controls.getControlKind(data.params.controlID);
if (!kind) {
return;
}

this.emit('input', {
...data.params,
kind,
this.interceptor.run(data.method, data.params).then(res => {
if (!res) {
return;
}
this.websocket!.send(
JSON.stringify({
...data,
type: 'method',
discard: true,
}),
);
if (data.method === 'giveInput') {
this.giveInputHandler(data);
}
});
});

Expand Down