UNPKG

@sofie-automation/server-core-integration

Version:
353 lines • 13.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.CoreConnection = void 0; const events_1 = require("events"); const _ = require("underscore"); const peripheralDeviceAPI_1 = require("@sofie-automation/shared-lib/dist/peripheralDevice/peripheralDeviceAPI"); const methodsAPI_1 = require("@sofie-automation/shared-lib/dist/peripheralDevice/methodsAPI"); const ddpConnector_1 = require("./ddpConnector"); const timeSync_1 = require("./timeSync"); const watchDog_1 = require("./watchDog"); const methods_1 = require("./methods"); const CoreConnectionChild_1 = require("./CoreConnectionChild"); const ping_1 = require("./ping"); const subscriptions_1 = require("./subscriptions"); // eslint-disable-next-line @typescript-eslint/no-var-requires const PkgInfo = require('../../package.json'); class CoreConnection extends events_1.EventEmitter { constructor(coreOptions) { super(); this._children = []; this._timeSync = null; this._watchDogPingResponse = ''; this._connected = false; this._sentConnectionId = ''; this._destroyed = false; this._coreOptions = coreOptions; /** We continuously ping Core to let Core know that we're alive */ this._pinger = new ping_1.CorePinger((err) => this._emitError(err), async () => this.coreMethods.ping()); if (this._coreOptions.watchDog) { this._watchDog = new watchDog_1.WatchDog(); this._watchDog.on('message', (msg) => this._emitError('msg ' + msg)); this._watchDog.startWatching(); } this._peripheralDeviceApi = (0, methods_1.makeMethods)(this, methodsAPI_1.PeripheralDeviceAPIMethods); this._peripheralDeviceApiLowPriority = (0, methods_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 (!_.has(ddpOptions, 'autoReconnect')) ddpOptions.autoReconnect = true; if (!_.has(ddpOptions, 'autoReconnectTimer')) ddpOptions.autoReconnectTimer = 1000; this._ddp = new ddpConnector_1.DDPConnector(ddpOptions); this._methodQueue = new methods_1.ConnectionMethodsQueue(this._ddp, this._coreOptions); this._subscriptions = new subscriptions_1.SubscriptionsHelper(this._emitError.bind(this), this._ddp, this._coreOptions.deviceToken); this._ddp.on('error', (err) => { this._emitError('ddpError: ' + (_.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_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_1.CoreConnectionChild(coreOptions); 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 '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 (_.isUndefined(selector)) { return _.values(collection); } else if (_.isFunction(selector)) { return _.filter(_.values(collection), selector); } else if (_.isObject(selector)) { return _.where(_.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(); } else { setTimeout(checkPingReply, 300); } } }; checkPingReply(); }).then(() => { return; }); } } exports.CoreConnection = CoreConnection; //# sourceMappingURL=coreConnection.js.map