import { EventEmitter } from 'events';
import log from '../log';
import normalizePath from './normalizePath';
import * as rpc from './rpc';
import { Backend } from '../backend';
import version from '../version';
import { Config, Event, Status } from './components';
import Feedback from './feedback';
import createXapiProxy from './proxy';
import { Path, XapiError, XapiOptions, XapiResponse } from './types';
export interface Requests {
[idx: string]: {
resolve(result: any): void;
reject(result: XapiError): void;
};
}
export declare interface XAPI {
on(event: 'error', listener: (error: Error) => void): this;
on(event: 'ready', listener: (xapi: XAPI) => void): this;
on(event: string, listener: () => void): this;
}
/**
* User-facing API towards the XAPI. Requires a backend for communicating
* with an XAPI instance. It should be possible to write backends for all kinds
* of transports (TSH over SSH, Websockets, HTTP, plain sockets, etc.)
*
* ### Initialization
*
* ```typescript
* const xapi = new XAPI(backend);
* ```
*
* ### Invoke command
*
* ```typescript
* xapi
* .command('Dial', { Number: 'johndoe@example.com' })
* .then(onSuccess, onFailure);
*
* // Alternate notation
* xapi
* .Command.Dial({ Number: 'johndoe@example.com' })
* .then(onSuccess, onFailure);
* ```
*
* ### Fetch a configuration
*
* ```typescript
* xapi
* .config.get('Audio DefaultVolume')
* .then((volume) => console.log(`default volume is: ${volume}`));
*
* // Alternate notation
* xapi.Audio.DefaultVolume
* .get()
* .then((volume) => console.log(`default volume is: ${volume}`));
* ```
*
* ### Set a configuration
*
* ```typescript
* xapi.config.set('Audio DefaultVolume', 100);
*
* // Alternate notation
* xapi.Audio.DefaultVolume.set(100);
* ```
*
* ### Fetch a status
*
* ```typescript
* xapi
* .status.get('Audio Volume')
* .then((volume) => { console.log(`volume is: ${volume}`); });
*
* // Alternate notation
* xapi.Status.Audio.Volume
* .get()
* .then((volume) => { console.log(`volume is: ${volume}`); });
* ```
*
* ### Listen to an event
*
* ```typescript
* xapi.event.on('Message Send Text', (text) => {
* console.log(`Received message text: ${text}`);
* });
*
* // Alternate notation
* xapi.Event.Message.Send.Text.on((text) => {
* console.log(`Received message text: ${text}`);
* });
* ```
*/
export class XAPI extends EventEmitter {
public version: string = version;
/**
* Interface to XAPI feedback registration.
*/
public feedback: Feedback;
/**
* Interface to XAPI configurations.
*/
public config = new Config(this);
/**
* Interface to XAPI events.
*/
public event = new Event(this);
/**
* Interface to XAPI statuses.
*/
public status = new Status(this);
/**
* Proxy for XAPI Status.
*/
public Command: any;
/**
* Proxy for XAPI Status.
*/
public Config: any;
/**
* Proxy for XAPI Status.
*/
public Status: any;
/**
* Proxy for XAPI Status.
*/
public Event: any;
/**
* @param backend Backend connected to an XAPI instance.
* @param options XAPI object options.
*/
private requestId = 1;
private requests: Requests = {};
constructor(
private readonly backend: Backend,
options: XapiOptions = {}) {
super();
this.feedback = new Feedback(this, options.feedbackInterceptor);
this.Command = createXapiProxy(this, this.command);
this.Config = createXapiProxy(this, this.config);
this.Event = createXapiProxy(this, this.event);
this.Status = createXapiProxy(this, this.status);
// Restrict object mutation
if (!options.hasOwnProperty('seal') || options.seal) {
Object.defineProperties(this, {
Command: { writable: false },
Config: { writable: false },
Event: { writable: false },
Status: { writable: false },
config: { writable: false },
event: { writable: false },
feedback: { writable: false },
status: { writable: false },
});
Object.seal(this);
}
backend
.on('close', () => {
this.emit('close');
})
.on('error', (error) => {
this.emit('error', error);
})
.on('ready', () => {
this.emit('ready', this);
})
.on('data', this.handleResponse.bind(this));
}
/**
* Close the XAPI connection.
*/
public close(): XAPI {
this.backend.close();
return this;
}
/**
* Executes the command specified by the given path.
*
* ```typescript
* // Space delimited
* xapi.command('Presentation Start');
*
* // Slash delimited
* xapi.command('Presentation/Start');
*
* // Array path
* xapi.command(['Presentation', 'Start']);
*
* // With parameters
* xapi.command('Presentation Start', { PresentationSource: 1 });
*
* // Multi-line
* xapi.command('UserInterface Extensions Set', { ConfigId: 'example' }, `
*
* 1.1
*
* Lightbulb
* Statusbar
*
* Foo
*
* Bar
*
* widget_3
* ToggleButton
*
*
*
*
*
* `);
* ```
*
* @param path Path to command node.
* @param params Object containing named command arguments.
* @param body Multi-line body for commands requiring it.
* @return Resolved with the command response when ready.
*/
public command(path: Path, params?: object | string, body?: string): Promise {
const apiPath = normalizePath(path).join('/');
const method = `xCommand/${apiPath}`;
let executeParams;
if (typeof params === 'string' && typeof body === 'undefined') {
executeParams = { body: params };
} else if ((typeof params === 'object' || !params) && typeof body === 'string') {
executeParams = Object.assign({ body }, params);
} else {
executeParams = params;
}
return this.execute(method, executeParams);
}
/**
* Interface to XAPI documents.
*
* @param path Path to xDocument.
* @return xDocument as specified by path.
*/
public doc(path: Path) {
return this.execute('xDoc', {
Path: normalizePath(path),
Type: 'Schema',
});
}
/**
* Execute the given JSON-RPC request on the backend.
*
* ```typescript
* xapi.execute('xFeedback/Subscribe', {
* Query: ['Status', 'Audio'],
* });
* ```
*
* @param method Name of RPC method to invoke.
* @param params Parameters to add to the request.
* @typeparam T Return type.
* @return Resolved with the command response.
*/
public execute(method: string, params: any): Promise {
return new Promise((resolve, reject) => {
const id = this.nextRequestId();
const request = rpc.createRequest(id, method, params);
this.backend.execute(request);
this.requests[id] = { resolve, reject };
});
}
private handleResponse(response: XapiResponse) {
const { id, method } = response;
if (method === 'xFeedback/Event') {
log.debug('feedback:', response);
this.feedback.dispatch(response.params);
} else {
if ({}.hasOwnProperty.call(response, 'result')) {
log.debug('result:', response);
const { resolve } = this.requests[id];
resolve(response.result);
} else {
log.debug('error:', response);
const { reject } = this.requests[id];
reject(response.error);
}
delete this.requests[id];
}
}
private nextRequestId() {
const requestId = this.requestId;
this.requestId += 1;
return requestId.toString();
}
}
export default XAPI;