Skip to content

Commit

Permalink
Refactor server to use a single on method
Browse files Browse the repository at this point in the history
I also added a babel config since babel was doing a bit too much.
  • Loading branch information
meyer committed Jun 29, 2020
1 parent 05888fa commit e061397
Show file tree
Hide file tree
Showing 15 changed files with 371 additions and 428 deletions.
10 changes: 10 additions & 0 deletions babel.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @ts-check

/** @type {import('@babel/core').ConfigFunction} */
module.exports = (api) => {
api.cache.forever();

return {
presets: [['@babel/preset-env', { targets: { node: '8' } }]],
};
};
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@
"license-validate": "node-license-validator -p -d --allow-licenses MIT BSD BSD-3-Clause BSD-2-Clause ISC Apache Apache-2.0 WTFPL Unlicense --allow-packages cycle"
},
"devDependencies": {
"@babel/preset-env": "^7.10.3",
"@types/jest": "^26.0.3",
"@types/npm-packlist": "^1.1.1",
"@types/pino": "^6.3.0",
Expand Down
276 changes: 100 additions & 176 deletions src/HyperDeckServer.ts
Original file line number Diff line number Diff line change
@@ -1,53 +1,26 @@
import { HyperDeckSocket } from './HyperDeckSocket';
import type { ReceivedCommandCallback } from './HyperDeckSocket';
import { DeserializedCommand, SynchronousCode, ErrorCode, NotifyType } from './types';
import * as ResponseInterface from './types/ResponseInterface';
import * as DeserializedCommands from './types/DeserializedCommands';
import { formatClipsGetResponse } from './formatClipsGetResponse';
import { SynchronousCode, ErrorCode, NotifyType, CommandHandler, CommandResponse } from './types';
import { createServer, Server } from 'net';
import pino from 'pino';
import { CommandName, paramsByCommandName } from './api';
import { formatClipsGetResponse } from './formatClipsGetResponse';
import { invariant } from './invariant';

type Handler<C extends DeserializedCommand, R extends any> = (command: C) => Promise<R>;

class UnimplementedError extends Error {}

const noop = async () => {
throw new UnimplementedError();
const internalCommands = {
remote: true,
notify: true,
watchdog: true,
ping: true,
};

type SupportedCommands = Exclude<CommandName, keyof typeof internalCommands>;

export class HyperDeckServer {
private logger: pino.Logger;
private sockets: { [id: string]: HyperDeckSocket } = {};
private server: Server;

onDeviceInfo: Handler<DeserializedCommand, ResponseInterface.DeviceInfo> = noop;
onDiskList: Handler<DeserializedCommand, ResponseInterface.DiskList> = noop;
onPreview: Handler<DeserializedCommands.PreviewCommand, void> = noop;
onPlay: Handler<DeserializedCommands.PlayCommand, void> = noop;
onPlayrangeSet: Handler<DeserializedCommands.PlayrangeSetCommand, void> = noop;
onPlayrangeClear: Handler<DeserializedCommand, void> = noop;
onRecord: Handler<DeserializedCommands.RecordCommand, void> = noop;
onStop: Handler<DeserializedCommand, void> = noop;
onClipsCount: Handler<DeserializedCommand, ResponseInterface.ClipsCount> = noop;
onClipsGet: Handler<DeserializedCommands.ClipsGetCommand, ResponseInterface.ClipsGet> = noop;
onClipsAdd: Handler<DeserializedCommands.ClipsAddCommand, void> = noop;
onClipsClear: Handler<DeserializedCommand, void> = noop;
onTransportInfo: Handler<DeserializedCommand, ResponseInterface.TransportInfo> = noop;
onSlotInfo: Handler<DeserializedCommands.SlotInfoCommand, ResponseInterface.SlotInfo> = noop;
onSlotSelect: Handler<DeserializedCommands.SlotSelectCommand, void> = noop;
onGoTo: Handler<DeserializedCommands.GoToCommand, void> = noop;
onJog: Handler<DeserializedCommands.JogCommand, void> = noop;
onShuttle: Handler<DeserializedCommands.ShuttleCommand, void> = noop;
onConfiguration: Handler<
DeserializedCommands.ConfigurationCommand,
ResponseInterface.Configuration
> = noop;
onUptime: Handler<DeserializedCommand, ResponseInterface.Uptime> = noop;
onFormat: Handler<DeserializedCommands.FormatCommand, ResponseInterface.Format> = noop;
onIdentify: Handler<DeserializedCommands.IdentifyCommand, void> = noop;
onWatchdog: Handler<DeserializedCommands.WatchdogCommand, void> = noop;

constructor(ip?: string, logger = pino()) {
this.logger = logger.child({ name: 'HyperDeck Emulator' });

Expand All @@ -57,9 +30,7 @@ export class HyperDeckServer {

const socketLogger = this.logger.child({ name: 'HyperDeck socket ' + socketId });

this.sockets[socketId] = new HyperDeckSocket(socket, socketLogger, (cmd) =>
this.receivedCommand(cmd)
);
this.sockets[socketId] = new HyperDeckSocket(socket, socketLogger, this.receivedCommand);

this.sockets[socketId].on('disconnected', () => {
socketLogger.info('disconnected');
Expand Down Expand Up @@ -92,162 +63,115 @@ export class HyperDeckServer {
}
}

private receivedCommand: ReceivedCommandCallback = async (cmd) => {
// TODO(meyer) more sophisticated debouncing
await new Promise((resolve) => setTimeout(() => resolve(), 200));

this.logger.info({ cmd }, '<-- ' + cmd.name);
try {
if (cmd.name === 'device info') {
const res = await this.onDeviceInfo(cmd);
return { code: SynchronousCode.DeviceInfo, params: res };
}

if (cmd.name === 'disk list') {
const res = await this.onDiskList(cmd);
return { code: SynchronousCode.DiskList, params: res };
}
private commandHandlers: { [K in CommandName]?: CommandHandler<K> } = {};

if (cmd.name === 'preview') {
await this.onPreview(cmd);
return SynchronousCode.OK;
}
public on = <T extends SupportedCommands>(key: T, handler: CommandHandler<T>): void => {
invariant(paramsByCommandName.hasOwnProperty(key), 'Invalid key: `%s`', key);

if (cmd.name === 'play') {
await this.onPlay(cmd);
return SynchronousCode.OK;
}
invariant(
!this.commandHandlers.hasOwnProperty(key),
'Handler already registered for `%s`',
key
);

if (cmd.name === 'playrange set') {
await this.onPlayrangeSet(cmd);
return SynchronousCode.OK;
}

if (cmd.name === 'playrange clear') {
await this.onPlayrangeClear(cmd);
return SynchronousCode.OK;
}

if (cmd.name === 'record') {
await this.onRecord(cmd);
return SynchronousCode.OK;
}

if (cmd.name === 'stop') {
await this.onStop(cmd);
return SynchronousCode.OK;
}

if (cmd.name === 'clips count') {
const res = await this.onClipsCount(cmd);
return { code: SynchronousCode.ClipsCount, params: res };
}

if (cmd.name === 'clips get') {
const res = await this.onClipsGet(cmd).then(formatClipsGetResponse);
return { code: SynchronousCode.ClipsInfo, params: res };
}

if (cmd.name === 'clips add') {
await this.onClipsAdd(cmd);
return SynchronousCode.OK;
}

if (cmd.name === 'clips clear') {
await this.onClipsClear(cmd);
return SynchronousCode.OK;
}
this.commandHandlers[key] = handler as any;
};

if (cmd.name === 'transport info') {
const res = await this.onTransportInfo(cmd);
return { code: SynchronousCode.TransportInfo, params: res };
}
private receivedCommand: ReceivedCommandCallback = async (cmd) => {
// TODO(meyer) more sophisticated debouncing
await new Promise((resolve) => setTimeout(() => resolve(), 200));

if (cmd.name === 'slot info') {
const res = await this.onSlotInfo(cmd);
return { code: SynchronousCode.SlotInfo, params: res };
}
this.logger.info({ cmd }, 'receivedCommand %s', cmd.name);

if (cmd.name === 'slot select') {
await this.onSlotSelect(cmd);
return SynchronousCode.OK;
}
if (cmd.name === 'remote') {
return {
code: SynchronousCode.Remote,
params: {
enabled: true,
override: false,
},
};
}

if (cmd.name === 'notify') {
// implemented in socket.ts
return SynchronousCode.OK;
}
// implemented in socket.ts
if (cmd.name === 'notify' || cmd.name === 'watchdog' || cmd.name === 'ping') {
return SynchronousCode.OK;
}

if (cmd.name === 'go to') {
await this.onGoTo(cmd);
return SynchronousCode.OK;
}
const handler = this.commandHandlers[cmd.name];
if (!handler) {
this.logger.error({ cmd }, 'unimplemented');
return ErrorCode.Unsupported;
}

if (cmd.name === 'jog') {
await this.onJog(cmd);
return SynchronousCode.OK;
}
const response = await handler(cmd);

const result: CommandResponse = {
name: cmd.name,
response,
} as any;

if (
result.name === 'clips add' ||
result.name === 'clips clear' ||
result.name === 'goto' ||
result.name === 'identify' ||
result.name === 'jog' ||
result.name === 'play' ||
result.name === 'playrange clear' ||
result.name === 'playrange set' ||
result.name === 'preview' ||
result.name === 'record' ||
result.name === 'shuttle' ||
result.name === 'slot select' ||
result.name === 'stop'
) {
return SynchronousCode.OK;
}

if (cmd.name === 'shuttle') {
await this.onShuttle(cmd);
return SynchronousCode.OK;
}
if (result.name === 'device info') {
return { code: SynchronousCode.DeviceInfo, params: result.response };
}

if (cmd.name === 'remote') {
return {
code: SynchronousCode.Remote,
params: {
enabled: true,
override: false,
},
};
}
if (result.name === 'disk list') {
return { code: SynchronousCode.DiskList, params: result.response };
}

if (cmd.name === 'configuration') {
const res = await this.onConfiguration(cmd);
if (res) {
return { code: SynchronousCode.Configuration, params: res };
}
return SynchronousCode.OK;
}
if (result.name === 'clips count') {
return { code: SynchronousCode.ClipsCount, params: result.response };
}

if (cmd.name === 'uptime') {
const res = await this.onUptime(cmd);
return { code: SynchronousCode.Uptime, params: res };
}
if (result.name === 'clips get') {
return { code: SynchronousCode.ClipsInfo, params: formatClipsGetResponse(result.response) };
}

if (cmd.name === 'format') {
const res = await this.onFormat(cmd);
if (res) {
return { code: SynchronousCode.FormatReady, params: res };
}
return SynchronousCode.OK;
}
if (result.name === 'transport info') {
return { code: SynchronousCode.TransportInfo, params: result.response };
}

if (cmd.name === 'identify') {
await this.onIdentify(cmd);
return SynchronousCode.OK;
}
if (result.name === 'slot info') {
return { code: SynchronousCode.SlotInfo, params: result.response };
}

if (cmd.name === 'watchdog') {
// implemented in socket.ts
return SynchronousCode.OK;
if (result.name === 'configuration') {
if (result) {
return { code: SynchronousCode.Configuration, params: result.response };
}
return SynchronousCode.OK;
}

if (cmd.name === 'ping') {
// implemented in socket.ts
return SynchronousCode.OK;
}
if (result.name === 'uptime') {
return { code: SynchronousCode.Uptime, params: result.response };
}

invariant(false, 'Unhandled command name: `%s`', cmd.name);
} catch (err) {
if (err instanceof UnimplementedError) {
this.logger.error({ cmd }, 'unimplemented');
return ErrorCode.Unsupported;
if (result.name === 'format') {
if (result) {
return { code: SynchronousCode.FormatReady, params: result.response };
}

this.logger.error({ cmd, err: err.message }, 'unhandled command name');
return ErrorCode.InternalError;
return SynchronousCode.OK;
}

this.logger.error({ cmd, res: result }, 'Unsupported command');
return ErrorCode.Unsupported;
};
}
Loading

0 comments on commit e061397

Please sign in to comment.