@bbc/sofie-server-core-integration
Version:
Library for connecting to Core
368 lines • 14 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.CoreConnection = void 0;
const tslib_1 = require("tslib");
const events_1 = require("events");
const underscore_1 = tslib_1.__importDefault(require("underscore"));
const peripheralDeviceAPI_1 = require("@bbc/sofie-shared-lib/dist/peripheralDevice/peripheralDeviceAPI");
const methodsAPI_1 = require("@bbc/sofie-shared-lib/dist/peripheralDevice/methodsAPI");
const ddpConnector_js_1 = require("./ddpConnector.js");
const timeSync_js_1 = require("./timeSync.js");
const watchDog_js_1 = require("./watchDog.js");
const methods_js_1 = require("./methods.js");
const CoreConnectionChild_js_1 = require("./CoreConnectionChild.js");
const ping_js_1 = require("./ping.js");
const subscriptions_js_1 = require("./subscriptions.js");
const PkgInfo = require('../../package.json');
class CoreConnection extends events_1.EventEmitter {
_ddp;
_methodQueue;
_subscriptions;
_children = [];
_coreOptions;
_timeSync = null;
_watchDog;
_watchDogPingResponse = '';
_connected = false;
_sentConnectionId = '';
_pinger;
_destroyed = false;
_peripheralDeviceApi;
_peripheralDeviceApiLowPriority;
constructor(coreOptions) {
super();
this._coreOptions = coreOptions;
/** We continuously ping Core to let Core know that we're alive */
this._pinger = new ping_js_1.CorePinger((err) => this._emitError(err), async () => this.coreMethods.ping());
if (this._coreOptions.watchDog) {
this._watchDog = new watchDog_js_1.WatchDog();
this._watchDog.on('message', (msg) => this._emitError('msg ' + msg));
this._watchDog.startWatching();
}
this._peripheralDeviceApi = (0, methods_js_1.makeMethods)(this, methodsAPI_1.PeripheralDeviceAPIMethods);
this._peripheralDeviceApiLowPriority = (0, methods_js_1.makeMethodsLowPrio)(this, methodsAPI_1.PeripheralDeviceAPIMethods);
}
async init(ddpOptions0) {
this._destroyed = false;
this.on('connected', () => {
if (this._subscriptions) {
this._subscriptions.renewAutoSubscriptions();
}
});
const ddpOptions = ddpOptions0 || {
host: '127.0.0.1',
port: 3000,
};
// TODO: The following line is ignored - autoReconnect ends up as false - which is what the tests want. Why?
if (!underscore_1.default.has(ddpOptions, 'autoReconnect'))
ddpOptions.autoReconnect = true;
if (!underscore_1.default.has(ddpOptions, 'autoReconnectTimer'))
ddpOptions.autoReconnectTimer = 1000;
this._ddp = new ddpConnector_js_1.DDPConnector(ddpOptions);
this._methodQueue = new methods_js_1.ConnectionMethodsQueue(this._ddp, this._coreOptions);
this._subscriptions = new subscriptions_js_1.SubscriptionsHelper(this._emitError.bind(this), this._ddp, this._coreOptions.deviceToken);
this._ddp.on('error', (err) => {
this._emitError('ddpError: ' + (underscore_1.default.isObject(err) && err.message) || err.toString());
});
this._ddp.on('failed', (err) => {
this.emit('failed', err);
});
this._ddp.on('connected', () => {
// this.emit('connected')
if (this._watchDog)
this._watchDog.addCheck(async () => this._watchDogCheck());
});
this._ddp.on('disconnected', () => {
// this.emit('disconnected')
if (this._watchDog)
this._watchDog.removeCheck(async () => this._watchDogCheck());
});
this._ddp.on('message', () => {
if (this._watchDog)
this._watchDog.receivedData();
});
await this._ddp.createClient();
await this._ddp.connect();
this._setConnected(this._ddp.connected); // ensure that connection status is synced
// set up the connectionChanged event handler after we've connected, so that it doesn't trigger on the await this._ddp.connect()
this._ddp.on('connectionChanged', (connected) => {
this._setConnected(connected);
this._maybeSendInit().catch((err) => {
this._emitError('_maybesendInit ' + JSON.stringify(err));
});
});
const deviceId = await this._sendInit();
this._timeSync = new timeSync_js_1.TimeSync({
serverDelayTime: 0,
}, async () => {
const stat = await this.coreMethods.getTimeDiff();
return stat.currentTime;
});
await this._timeSync.init();
this._pinger.triggerPing();
return deviceId;
}
async destroy() {
this._destroyed = true;
if (this._ddp) {
this._ddp.close();
this._ddp.removeAllListeners();
}
this.removeAllListeners();
if (this._watchDog)
this._watchDog.stopWatching();
this._pinger.destroy();
if (this._timeSync) {
this._timeSync.stop();
this._timeSync = null;
}
await Promise.all(this._children.map(async (child) => child.destroy()));
this._children = [];
}
async createChild(coreOptions) {
const child = new CoreConnectionChild_js_1.CoreConnectionChild(coreOptions);
this._children.push(child);
// Set the max listeners to a higher value, so that we don't get warnings:
this.setMaxListeners(
// Base count, can be increased if needed:
10 +
// Each child adds 2 listeners:
this._children.length * 2);
await child.init(this, this._coreOptions);
return child;
}
removeChild(childToRemove) {
let removeIndex = -1;
this._children.forEach((c, i) => {
if (c === childToRemove)
removeIndex = i;
});
if (removeIndex !== -1) {
this._children.splice(removeIndex, 1);
}
}
onConnectionChanged(cb) {
this.on('connectionChanged', cb);
}
onConnected(cb) {
this.on('connected', cb);
}
onDisconnected(cb) {
this.on('disconnected', cb);
}
onError(cb) {
this.on('error', cb);
}
onFailed(cb) {
this.on('failed', cb);
}
get ddp() {
if (!this._ddp) {
throw new Error('Not connected to Core');
}
else {
return this._ddp;
}
}
get connected() {
return this._connected;
}
get deviceId() {
return this._coreOptions.deviceId;
}
get coreMethods() {
return this._peripheralDeviceApi;
}
get coreMethodsLowPriority() {
return this._peripheralDeviceApiLowPriority;
}
async setStatus(status) {
return this.coreMethods.setStatus(status);
}
/**
* This should not be used directly, use the `coreMethods` wrapper instead.
* Call a meteor method
* @param methodName The name of the method to call
* @param attrs Parameters to the method
* @returns Resopnse, if any
*/
async callMethodRaw(methodName, attrs) {
if (this._destroyed) {
throw new Error('callMethod: CoreConnection has been destroyed');
}
if (!this._methodQueue)
throw new Error('Connection is not ready to call methods');
return this._methodQueue.callMethodRaw(methodName, attrs);
}
async callMethodLowPrioRaw(methodName, attrs) {
if (!this._methodQueue)
throw new Error('Connection is not ready to call methods');
return this._methodQueue.callMethodLowPrioRaw(methodName, attrs);
}
async unInitialize() {
return this.coreMethods.unInitialize();
}
async getPeripheralDevice() {
return this.coreMethods.getPeripheralDevice();
}
getCollection(collectionName) {
if (!this.ddp.ddpClient) {
throw new Error('getCollection: DDP client not initialized');
}
const collections = this.ddp.ddpClient.collections;
const c = {
find(selector) {
const collection = (collections[String(collectionName)] || {});
if (underscore_1.default.isUndefined(selector)) {
return underscore_1.default.values(collection);
}
else if (underscore_1.default.isFunction(selector)) {
return underscore_1.default.filter(underscore_1.default.values(collection), selector);
}
else if (underscore_1.default.isObject(selector)) {
return underscore_1.default.where(underscore_1.default.values(collection), selector);
}
else {
return [collection[selector]];
}
},
findOne(docId) {
const collection = (collections[String(collectionName)] || {});
return collection[docId];
},
};
return c;
}
// /**
// * Subscribe to a DDP publication
// * Upon reconnecting to Sofie, this publication will be terminated
// */
// async subscribeOnce(publicationName: string, ...params: Array<any>): Promise<SubscriptionId> {
// if (!this._subscriptions) throw new Error('Connection is not ready to handle subscriptions')
// return this._subscriptions.subscribeOnce(publicationName, ...params)
// }
/**
* Subscribe to a DDP publication
* Upon reconnecting to Sofie, this publication will be restarted
*/
async autoSubscribe(publicationName, ...params) {
if (!this._subscriptions)
throw new Error('Connection is not ready to handle subscriptions');
return this._subscriptions.autoSubscribe(publicationName, ...params);
}
/**
* Unsubscribe from subscroption to a DDP publication
*/
unsubscribe(subscriptionId) {
if (!this._subscriptions)
throw new Error('Connection is not ready to handle subscriptions');
this._subscriptions.unsubscribe(subscriptionId);
}
/**
* Unsubscribe from all subscriptions to DDP publications
*/
unsubscribeAll() {
if (!this._subscriptions)
throw new Error('Connection is not ready to handle subscriptions');
this._subscriptions.unsubscribeAll();
}
observe(collectionName) {
if (!this.ddp.ddpClient) {
throw new Error('observe: DDP client not initialised');
}
return this.ddp.ddpClient.observe(String(collectionName));
}
getCurrentTime() {
return this._timeSync?.currentTime() || 0;
}
hasSyncedTime() {
return this._timeSync?.isGood() || false;
}
syncTimeQuality() {
return this._timeSync?.quality || null;
}
setPingResponse(message) {
this._watchDogPingResponse = message;
}
_emitError(e) {
if (!this._destroyed) {
this.emit('error', e);
}
else {
console.log('destroyed error', e);
}
}
_setConnected(connected) {
const prevConnected = this._connected;
this._connected = connected;
if (prevConnected !== connected) {
if (connected)
this.emit('connected');
else
this.emit('disconnected');
this.emit('connectionChanged', connected);
}
this._pinger.setConnectedAndTriggerPing(connected);
}
async _maybeSendInit() {
// If the connectionId has changed, we should report that to Core:
if (this.ddp && this.ddp.connectionId !== this._sentConnectionId) {
return this._sendInit();
}
else {
return Promise.resolve();
}
}
async _sendInit() {
if (!this.ddp || !this.ddp.connectionId)
throw Error('Not connected to Core');
const options = {
category: this._coreOptions.deviceCategory,
type: this._coreOptions.deviceType,
subType: peripheralDeviceAPI_1.PERIPHERAL_SUBTYPE_PROCESS,
name: this._coreOptions.deviceName,
connectionId: this.ddp.connectionId,
parentDeviceId: undefined,
versions: this._coreOptions.versions,
configManifest: this._coreOptions.configManifest,
documentationUrl: this._coreOptions.documentationUrl,
};
if (!options.versions)
options.versions = {};
options.versions['@sofie-automation/server-core-integration'] = PkgInfo.version;
this._sentConnectionId = options.connectionId;
return this.coreMethods.initialize(options);
}
async _watchDogCheck() {
/*
Randomize a message and send it to Core.
Core should then reply with triggering executeFunction with the "pingResponse" method.
*/
const message = 'watchdogPing_' + Math.round(Math.random() * 100000);
this.coreMethods.pingWithCommand(message).catch((e) => this._emitError('watchdogPing' + e));
return new Promise((resolve, reject) => {
let i = 0;
const checkPingReply = () => {
if (this._watchDogPingResponse === message) {
// if we've got a good watchdog response, we can delay the pinging:
this._pinger.triggerDelayPing();
resolve();
}
else {
i++;
if (i > 50) {
reject(new Error('Watchdog ping timeout'));
}
else {
setTimeout(checkPingReply, 300);
}
}
};
checkPingReply();
}).then(() => {
return;
});
}
}
exports.CoreConnection = CoreConnection;
//# sourceMappingURL=coreConnection.js.map