UNPKG

happn-3

Version:

pub/sub api as a service using primus and mongo & redis or nedb, can work as cluster, single process or embedded using nedb

278 lines (221 loc) 8.43 kB
const commons = require('happn-commons'); const async = commons.async; const CONSTANTS = commons.constants; const BaseHappnProtocol = require('./happn_base'); class ProtocolHappn1 extends BaseHappnProtocol { constructor(opts) { super(opts); this.__subscriptionMappingsReferenceId = {}; this.__subscriptionMappingsListenerId = {}; } static create(opts) { return new ProtocolHappn1(opts); } initialize() { this.happn.services.session.on('client-disconnect', (sessionId) => { //clear our reference cache delete this.__subscriptionMappingsReferenceId[sessionId]; delete this.__subscriptionMappingsListenerId[sessionId]; }); } transformIn(message) { if (message.raw.encrypted || (message.raw.data && message.raw.data.encrypted)) throw new Error('Encrypted payloads are no longer supported'); message.request = message.raw; //no transform necessary delete message.raw; if (message.request) { if ( message.request.action === 'off' && message.request.options && message.request.options.refCount != null ) { if (this.__subscriptionMappingsReferenceId[message.request.sessionId]) //may be a full disconnect message.request.options.referenceId = this.__subscriptionMappingsReferenceId[message.request.sessionId][ message.request.options.refCount ]; } if ( message.request.action === 'set' && message.request.options && message.request.options.nullValue ) message.request.data = null; //null values dont get passed across the wire } return this.validate(message); } transformSystem(message) { if (message.action === 'disconnect') { var options = message.options ? message.options : {}; if (options.reconnect == null) options.reconnect = true; if (options.reason == null) options.reason = 'server side disconnect'; message.response = { _meta: { type: 'system', }, eventKey: 'server-side-disconnect', data: options.reason, reconnect: options.reconnect, }; } if (message.eventKey === 'security-data-changed') message.__suppress = true; return message; } __initialEmit(message, data, callback) { var session = message.session; var request = message.request; async.eachSeries( data, (itemData, itemDataCallback) => { var requestParts = request.path.split('@'); var publication = { _meta: {}, data: itemData, }; var initialMessage = { request: request, session: session, response: publication, recipients: [ { data: { sessionId: session.id, action: requestParts[0].replace('/', ''), session: { protocol: this.protocolVersion, id: session.id, }, path: requestParts[1], }, }, ], options: { consistency: CONSTANTS.CONSISTENCY.TRANSACTIONAL, }, }; this.happn.services.publisher.processPublish(initialMessage, itemDataCallback); }, (e) => { if (e) return this.happn.services.error.handleSystem( new Error('processInitialEmit failed', e), 'PublisherService', CONSTANTS.ERROR_SEVERITY.MEDIUM, function (e) { callback(e, message); } ); callback(null, message); } ); } __pruneSubscriptionMappings(sessionId, references) { var _this = this; if (!references.forEach) return; if (_this.__subscriptionMappingsListenerId[sessionId]) { //dont do anything if we are off * or path references.forEach(function (reference) { var refCount = _this.__subscriptionMappingsListenerId[sessionId][reference.id]; delete _this.__subscriptionMappingsListenerId[sessionId][reference.id]; if (refCount) delete _this.__subscriptionMappingsReferenceId[sessionId][refCount]; if (Object.keys(_this.__subscriptionMappingsListenerId[sessionId]).length === 0) delete _this.__subscriptionMappingsListenerId[sessionId]; if (Object.keys(_this.__subscriptionMappingsReferenceId[sessionId]).length === 0) delete _this.__subscriptionMappingsReferenceId[sessionId]; }); } } __transformResponse(message, response) { //PROTOCOL 2.0.0 adjustments: var request = message.request; var transformedMeta = response._meta; var transformedData = response.data; if (!request.options) request.options = {}; if (Array.isArray(response)) { if (response.length < 2) return response; transformedMeta = response[response.length - 1]; transformedData = response.slice(0, response.length - 2); } if (transformedMeta.action === 'on' && transformedMeta.status === 'ok') { if (this.__subscriptionMappingsReferenceId[transformedMeta.sessionId] == null) this.__subscriptionMappingsReferenceId[transformedMeta.sessionId] = {}; if (this.__subscriptionMappingsListenerId[transformedMeta.sessionId] == null) this.__subscriptionMappingsListenerId[transformedMeta.sessionId] = {}; this.__subscriptionMappingsReferenceId[transformedMeta.sessionId][request.options.refCount] = transformedData.id; this.__subscriptionMappingsListenerId[transformedMeta.sessionId][transformedData.id] = request.options.refCount; if (request.options.initialEmit && transformedData.length > 0) { this.__initialEmit(message, transformedData, (e) => { if (e) return this.happn.services.error.handleSystem( new Error('initialEmit failed', e), 'ProtocolHappn1', CONSTANTS.ERROR_SEVERITY.MEDIUM ); }); } } if (transformedMeta.action === 'off' && transformedMeta.status === 'ok') this.__pruneSubscriptionMappings(transformedMeta.sessionId, transformedData.removed); return response; } __createResponse(e, message, response, opts) { var _meta = {}; var local = opts ? opts.local : false; if (response == null) response = { data: null, }; else { if (response._meta) _meta = response._meta; if (response.paths) response = response.paths; } _meta.type = 'response'; _meta.status = 'ok'; if (_meta.published == null) _meta.published = false; delete _meta._id; if (message) { _meta.eventId = message.eventId; //we need these passed in case we are encrypting the resulting payload if (['login', 'describe'].indexOf(message.action) === -1) _meta.sessionId = message.sessionId; _meta.action = message.action; } response._meta = _meta; response.protocol = this.protocolVersion; if (e) { response._meta.status = 'error'; response._meta.error = {}; if (e.name == null) response._meta.error.name = e.toString(); else response._meta.error.name = e.name; if (typeof e === 'object') { Object.keys(e).forEach(function (key) { response._meta.error[key] = e[key]; }); if (response._meta.error.message == null && e.message) response._meta.error.message = e.message; //this is a non-iterable property } return response; } if ( message.action === 'on' && message.options && (message.options.initialCallback || message.options.initialEmit) ) response.data = this.__formatReturnItems(response.initialValues, local); if (Array.isArray(response)) { response = this.__formatReturnItems(response, local); if (!local) response.push(_meta); //we encapsulate the meta data in the array, so we can pop it on the other side else response._meta = _meta; // the _meta is preserved as an external property because we arent having to serialize } return response; } success(message) { var response = this.__createResponse(null, message.request, message.response, message.opts); message.response = this.__transformResponse(message, response); return message; } } module.exports = ProtocolHappn1;