UNPKG

conveyor-client

Version:

A client for communicating with the Conveyor event sourcing engine via the HTTP/Webhook interface'

420 lines (347 loc) 13.8 kB
'use strict'; var _slicedToArray = function () { function sliceIterator(arr, i) { var _arr = []; var _n = true; var _d = false; var _e = undefined; try { for (var _i = arr[Symbol.iterator](), _s; !(_n = (_s = _i.next()).done); _n = true) { _arr.push(_s.value); if (i && _arr.length === i) break; } } catch (err) { _d = true; _e = err; } finally { try { if (!_n && _i["return"]) _i["return"](); } finally { if (_d) throw _e; } } return _arr; } return function (arr, i) { if (Array.isArray(arr)) { return arr; } else if (Symbol.iterator in Object(arr)) { return sliceIterator(arr, i); } else { throw new TypeError("Invalid attempt to destructure non-iterable instance"); } }; }(); let httpGet = (() => { var _ref = _asyncToGenerator(function* (uri) { return yield request({ uri, method: 'GET', json: true, headers: { 'Accept': 'application/json' } }); }); return function httpGet(_x) { return _ref.apply(this, arguments); }; })(); let httpPost = (() => { var _ref2 = _asyncToGenerator(function* (uri, body = null, opts = {}) { return yield request(reqForMethod('POST', uri, body, opts)); }); return function httpPost(_x2) { return _ref2.apply(this, arguments); }; })(); let httpDelete = (() => { var _ref3 = _asyncToGenerator(function* (uri, body = null, opts = {}) { return yield request(reqForMethod('DELETE', uri, body, opts)); }); return function httpDelete(_x3) { return _ref3.apply(this, arguments); }; })(); let getFeed = (() => { var _ref4 = _asyncToGenerator(function* (conn, feed) { return yield httpGet(feedUrl(conn.baseUrl, feed)); }); return function getFeed(_x4, _x5) { return _ref4.apply(this, arguments); }; })(); let getEvents = (() => { var _ref5 = _asyncToGenerator(function* (conn, feed) { return yield httpGet(eventsUrl(conn.baseUrl, feed)); }); return function getEvents(_x6, _x7) { return _ref5.apply(this, arguments); }; })(); let createFeed = (() => { var _ref6 = _asyncToGenerator(function* (conn, feed) { return yield httpPost(feedUrl(conn.baseUrl, feed)); }); return function createFeed(_x8, _x9) { return _ref6.apply(this, arguments); }; })(); let emitToSubscribers = (() => { var _ref7 = _asyncToGenerator(function* (conn, event) { const path = maybeStripSlash(event.feed).split('/'); const subscribers = conn.subscribers.collect(path); yield Promise.all(subscribers.map(function (callback) { return callback(event); })); }); return function emitToSubscribers(_x10, _x11) { return _ref7.apply(this, arguments); }; })(); let getWebhookSubscriptions = (() => { var _ref8 = _asyncToGenerator(function* (conn, url) { try { return yield httpGet(subscriberUrl(conn.baseUrl, url)); } catch (e) { return []; } }); return function getWebhookSubscriptions(_x12, _x13) { return _ref8.apply(this, arguments); }; })(); let registerSubscriptionWebhook = (() => { var _ref9 = _asyncToGenerator(function* (conn, feed) { return yield httpPost(subscriptionsUrl(conn.baseUrl, feed), { url: conn.webhookUrl }); }); return function registerSubscriptionWebhook(_x14, _x15) { return _ref9.apply(this, arguments); }; })(); let cancelSubscriptionWebhook = (() => { var _ref10 = _asyncToGenerator(function* (conn, feed) { return yield httpDelete(subscriptionsUrl(conn.baseUrl, feed), { url: conn.webhookUrl }); }); return function cancelSubscriptionWebhook(_x16, _x17) { return _ref10.apply(this, arguments); }; })(); let cancelAllSubscriptionWebhook = (() => { var _ref11 = _asyncToGenerator(function* (conn) { return yield httpDelete(subscriptionsUrl(conn.baseUrl), { url: conn.webhookUrl }); }); return function cancelAllSubscriptionWebhook(_x18) { return _ref11.apply(this, arguments); }; })(); let subscribe = (() => { var _ref12 = _asyncToGenerator(function* (conn, feed, callback) { const path = maybeStripSlash(feed).split('/'); if (conn.webhookUrl && !conn.subscribers.contains(path)) { yield registerSubscriptionWebhook(conn, feed); } conn.subscribers.addElement(path, callback); }); return function subscribe(_x19, _x20, _x21) { return _ref12.apply(this, arguments); }; })(); let unsubscribe = (() => { var _ref13 = _asyncToGenerator(function* (conn, feed, callback) { const path = maybeStripSlash(feed).split('/'); conn.subscribers.removeElement(path, callback); // Remove webhook subscription when last subscriber for that feed has been unsubscribed if (conn.webhookUrl) { const nodeForPath = conn.subscribers.walk(path); if (!nodeForPath || // All subscribers for this feed and all sub-feeds have been removed nodeForPath.elements.length === 0 // All subscribers for this feed have been removed, but subscribers still exist for sub-feeds ) { yield cancelSubscriptionWebhook(conn, feed); } } }); return function unsubscribe(_x22, _x23, _x24) { return _ref13.apply(this, arguments); }; })(); /** * Register a subscription registration implementation * * @param conn connection * @param impl {Function} Factory function that takes the following functions * as positional arguments: * - emitToSubscribers(event): Callback to call when a message is received * - cancelAllSubscriptionWebhook(): Callback to call on clean-up to re-register clean-up * and returns a WebHook postback URL */ let registerSubscriptionImpl = (() => { var _ref14 = _asyncToGenerator(function* (conn, impl) { conn.webhookUrl = impl(emitToSubscribers.bind(null, conn), cancelAllSubscriptionWebhook.bind(null, conn)); const registeredFeeds = new Set(); var _iteratorNormalCompletion = true; var _didIteratorError = false; var _iteratorError = undefined; try { for (var _iterator = conn.subscribers.entriesIter()[Symbol.iterator](), _step; !(_iteratorNormalCompletion = (_step = _iterator.next()).done); _iteratorNormalCompletion = true) { let subscriber = _step.value; var _subscriber = _slicedToArray(subscriber, 2); const path = _subscriber[0], callback = _subscriber[1]; const feed = path.join('/'); if (!registeredFeeds.has(feed)) { registeredFeeds.add(feed); yield registerSubscriptionWebhook(conn, feed); } } } catch (err) { _didIteratorError = true; _iteratorError = err; } finally { try { if (!_iteratorNormalCompletion && _iterator.return) { _iterator.return(); } } finally { if (_didIteratorError) { throw _iteratorError; } } } }); return function registerSubscriptionImpl(_x25, _x26) { return _ref14.apply(this, arguments); }; })(); let emitEvent = (() => { var _ref15 = _asyncToGenerator(function* (conn, feed, event) { const reqBody = { data: event }; if (conn.txId) { reqBody['tx-id'] = conn.txId; } // TODO: Support feed auto-creation via a query parameter inside Conveyor // then remove the following call to createFeed yield createFeed(conn, feed); return yield httpPost(eventsUrl(conn.baseUrl, feed), reqBody); }); return function emitEvent(_x27, _x28, _x29) { return _ref15.apply(this, arguments); }; })(); let withTransaction = (() => { var _ref16 = _asyncToGenerator(function* (conn, fn) { const txResp = yield httpPost(transactionsUrl(conn.baseUrl), null, { resolveWithFullResponse: true }); if (txResp.statusCode > 299) { throw new Error({ message: 'Could not create transaction', data: txResp.body }); } const txId = parseIdFromLocation(txResp.headers.location, true); let action = 'commit'; try { yield fn(Object.assign({ txId }, conn)); } catch (e) { console.warn('Error in transaction. Rolling back.', e); action = 'rollback'; } yield httpPost(transactionActionsUrl(conn.baseUrl, txId), { action }); }); return function withTransaction(_x30, _x31) { return _ref16.apply(this, arguments); }; })(); /** * Emit a batch of events as a single transaction * * @param {Connection} conn Conveyor connection * @param {array} events Array of objects with `feed` and `event` keys */ let emitEvents = (() => { var _ref17 = _asyncToGenerator(function* (conn, events) { yield withTransaction(conn, (() => { var _ref18 = _asyncToGenerator(function* (tx) { var _iteratorNormalCompletion2 = true; var _didIteratorError2 = false; var _iteratorError2 = undefined; try { for (var _iterator2 = chunk(events, conn.maxEventsPerBatch)[Symbol.iterator](), _step2; !(_iteratorNormalCompletion2 = (_step2 = _iterator2.next()).done); _iteratorNormalCompletion2 = true) { let batch = _step2.value; yield Promise.all(batch.map((() => { var _ref19 = _asyncToGenerator(function* ({ feed, event }) { if (typeof feed === 'undefined' || typeof event === 'undefined') { throw new Error('Each element in a emitEvents array must have `feed` and `event` properties'); } yield emitEvent(tx, feed, event); }); return function (_x35) { return _ref19.apply(this, arguments); }; })())); } } catch (err) { _didIteratorError2 = true; _iteratorError2 = err; } finally { try { if (!_iteratorNormalCompletion2 && _iterator2.return) { _iterator2.return(); } } finally { if (_didIteratorError2) { throw _iteratorError2; } } } }); return function (_x34) { return _ref18.apply(this, arguments); }; })()); }); return function emitEvents(_x32, _x33) { return _ref17.apply(this, arguments); }; })(); function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; } const request = require('request-promise'); const Trie = require('ds-trie'); const DEFAULT_MAX_EVENTS_PER_BATCH = 20; function* chunk(xs, batchSize) { const clone = array.slice(0); while (clone.length > 0) { yield clone.splice(0, size); } } function maybeStripSlash(feed) { return feed.charAt(0) === '/' ? feed.substring(1) : feed; } function feedUrl(baseUrl, feed) { return `${baseUrl}/feeds/${maybeStripSlash(feed)}`; } function subscriberUrl(baseUrl, subscriberUrl) { return `${baseUrl}/subscribers/${encodeURIComponent(subscriberUrl)}`; } function subscriptionsUrl(baseUrl, feed = null) { return feed ? `${baseUrl}/subscriptions/${maybeStripSlash(feed)}` : `${baseUrl}/subscriptions`; } function eventsUrl(baseUrl, feed) { return `${baseUrl}/events/${maybeStripSlash(feed)}`; } function transactionsUrl(baseUrl) { return `${baseUrl}/transactions`; } function transactionActionsUrl(baseUrl, txId) { return `${baseUrl}/transactions/${txId}/actions`; } function parseIdFromLocation(loc, asInt = false) { const lastSegmentStart = loc.lastIndexOf('/') + 1; const id = loc.substring(lastSegmentStart); return asInt ? parseInt(id) : id; } function reqForMethod(method, uri, body, opts) { const req = Object.assign({ uri, method, headers: { 'Accept': 'application/json' } }, opts); if (body) { req.body = JSON.stringify(body); req.headers['Content-Type'] = 'application/json'; } return req; } function connection(host, port, isSecure) { const proto = 'http' + (isSecure ? 's' : ''); return { host, port, isSecure, baseUrl: `${proto}://${host}:${port}`, subscribers: new Trie(), webhookUrl: null, maxEventsPerBatch: DEFAULT_MAX_EVENTS_PER_BATCH }; } module.exports = { connection, createFeed, getFeed, subscribe, unsubscribe, registerSubscriptionImpl, getEvents, emitEvent, emitEvents, withTransaction };