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
JavaScript
;
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
};