UNPKG

@bbc/sofie-server-core-integration

Version:
368 lines 14 kB
"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