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
242 lines (196 loc) • 7.06 kB
JavaScript
const commons = require('happn-commons');
const async = commons.async,
CONSTANTS = commons.constants;
function Publication(message, options, happn) {
this.happn = happn;
this.message = message;
this.options = message.request.options || {};
this.result = {
successful: 0,
failed: 0,
skipped: 0,
};
if (!options) options = {};
if (!options.acknowledgeTimeout) options.acknowledgeTimeout = 60000;
this.publication_options = options;
if (this.options.consistency == null)
this.options.consistency = CONSTANTS.CONSISTENCY.TRANSACTIONAL; //by default
if (this.options.consistency === CONSTANTS.CONSISTENCY.ACKNOWLEDGED) {
this.unacknowledged_recipients = [];
this.options.publishResults = true;
this.result.acknowledged = 0;
}
this.id = message.session.id + '-' + message.request.eventId;
this.origin = message.session;
if (message.recipients) this.recipients = message.recipients;
//we are targeting specific recipients for this message
else this.recipients = this.happn.services.subscription.getFilteredRecipients(message);
this.result.queued = this.recipients.length;
this.duplicate_channels = {};
this.__configurePayload(message, message.response.data, 'payload');
}
Publication.create = function (message, options, happn) {
return new Publication(message, options, happn);
};
Publication.prototype.__reservedMetaKeys = [
'created',
'modified',
'path',
'action',
'type',
'published',
'status',
'eventId',
'sessionId',
];
Publication.prototype.__configurePayload = function (message, data, payloadType) {
var payload = {
data: data,
_meta: {
action: '/' + message.request.action.toUpperCase() + '@' + message.request.path,
type: 'data',
sessionId: message.session.id,
consistency: this.options.consistency,
publicationId: this.id,
path: message.request.path,
created: message.response._meta.created,
modified: message.response._meta.modified,
},
protocol: message.protocol,
__outbound: true,
};
if (this.origin && this.origin.user)
payload._meta.eventOrigin = { username: this.origin.user.username, id: this.origin.id };
if (message.request.options && typeof message.request.options.meta === 'object') {
var _this = this;
Object.keys(message.request.options.meta).forEach(function (key) {
if (_this.__reservedMetaKeys.indexOf(key) >= 0) return;
payload._meta[key] = message.request.options.meta[key];
});
}
this[payloadType] = payload;
};
Publication.prototype.acknowledge = function (sessionId) {
this.unacknowledged_recipients.every((recipientSessionId, recipientSessionIdIndex) => {
if (recipientSessionId === sessionId) {
this.unacknowledged_recipients.splice(recipientSessionIdIndex, 1); //remove it
this.result.acknowledged++;
return false;
}
return true;
});
if (this.unacknowledged_recipients.length === 0) {
this.__acknowledgeStatus = 1;
if (this.__onAcknowledgeComplete) this.__onAcknowledgeComplete();
}
};
Publication.prototype.__waitForAck = function (callback) {
this.recipients.forEach((recipient) => {
this.unacknowledged_recipients.push(recipient.data.session.id);
});
this.__onAcknowledgeComplete = function (e) {
clearTimeout(this.__unacknowledgedTimedout);
if (e) {
this.message.response._meta.published = false;
this.message.response._meta.publishError = e.toString();
} else this.message.response._meta.published = true;
this.message.response._meta.publishResults = this.result;
callback(e, this.result);
};
this.__unacknowledgedTimedout = setTimeout(() => {
this.__onAcknowledgeComplete(new Error('unacknowledged publication'));
}, this.publication_options.acknowledgeTimeout);
this.__checkAcknowledgeStatus(); //race condition, as we may have acknowledgements already
};
Publication.prototype.__checkAcknowledgeStatus = function () {
if (this.__acknowledgeStatus === 1) this.__onAcknowledgeComplete();
};
Publication.prototype.getRecipientMessage = function (recipient) {
var recipientMessage,
channel = '/' + recipient.data.action + '@' + recipient.data.path;
if (this.duplicate_channels[recipient.data.session.id + channel]) return null;
if (recipient.data.options && recipient.data.options.merge) {
//lazy load mergePayload
if (!this.mergePayload)
this.__configurePayload(this.message, this.message.request.data, 'mergePayload');
recipientMessage = this.mergePayload;
} else recipientMessage = this.payload;
recipientMessage._meta = Object.assign({}, recipientMessage._meta);
recipientMessage._meta.channel = channel;
if (recipient.data.options && recipient.data.options.depth > 0)
recipientMessage._meta.depth = recipient.data.options.depth;
this.duplicate_channels[recipient.data.session.id + channel] = true;
return {
request: {
publication: recipientMessage,
options: recipient.data.options,
action: 'emit',
},
session: recipient.data.session,
};
};
Publication.prototype.publish = function (callback) {
const done = () => {
if (this.options.consistency === CONSTANTS.CONSISTENCY.ACKNOWLEDGED)
return this.__waitForAck(callback);
this.message.response._meta.published = true;
if (this.options.publishResults) this.message.response._meta.publishResults = this.result;
callback(null, this.result);
};
setImmediate(() => {
if (!this.recipients.length) return done();
async.eachLimit(
this.recipients,
100,
(recipient, recipientCB) => {
if (this.options.noCluster && recipient.data.session.info.clusterName) {
this.skipped++;
return recipientCB();
}
var recipientMessage = this.getRecipientMessage(recipient);
if (!recipientMessage) {
this.skipped++;
return recipientCB();
}
this.happn.services.protocol.processMessageOut(recipientMessage, (e) => {
if (e) {
this.result.failed++;
this.result.lastError = e;
} else this.result.successful++;
recipientCB();
});
},
(e) => {
if (e || this.result.failed > 0) {
this.message.response._meta.publishResults = this.result;
return callback(e || this.result.lastError);
}
done();
}
);
});
};
Publication.prototype.resultsMessage = function (e) {
var message = {
request: {
publication: {
id: this.id,
action: 'publication-ack',
result: this.result,
status: 'ok',
_meta: {
type: 'ack',
eventId: this.message.request.eventId,
},
},
},
session: this.origin,
};
if (e) {
//need to take out Error: - or we get a double up
message.request.publication.error = e.toString().replace('Error: ', '');
message.request.publication.status = 'error';
}
return message;
};
module.exports = Publication;