UNPKG

box-node-sdk

Version:

Official SDK for Box Plaform APIs

277 lines 10.3 kB
"use strict"; /** * @fileoverview Event stream backed by the events API */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; // ------------------------------------------------------------------------------ // Requirements // ------------------------------------------------------------------------------ const bluebird_1 = require("bluebird"); const querystring_1 = __importDefault(require("querystring")); const stream_1 = require("stream"); // ------------------------------------------------------------------------------ // Private // ------------------------------------------------------------------------------ const DEFAULT_OPTIONS = Object.freeze({ deduplicationFilterSize: 5000, retryDelay: 1000, fetchInterval: 1000, }); // ------------------------------------------------------------------------------ // Public // ------------------------------------------------------------------------------ /** * Stream of Box events from a given client and point in time. * @param {BoxClient} client The client to use to get events * @param {string} streamPosition The point in time to start at * @param {Object} [options] Optional parameters * @param {int} [options.retryDelay=1000] Number of ms to wait before retrying after an error * @param {int} [options.deduplicationFilterSize=5000] Number of IDs to track for deduplication * @param {int} [options.fetchInterval=1000] Minimunm number of ms between calls for more events * @constructor * @extends Readable */ class EventStream extends stream_1.Readable { constructor(client, streamPosition, options) { super({ objectMode: true, }); /** * @var {BoxClient} The client for making API calls * @private */ this._client = client; /** * @var {string} The latest stream position * @private */ this._streamPosition = streamPosition; /** * @var {?Object} The information for how to long poll * @private */ this._longPollInfo = undefined; /** * @var {int} The number of long poll requests we've made against one URL so far * @private */ this._longPollRetries = 0; /** * @var {Object.<string, boolean>} Hash of event IDs we've already pushed * @private */ this._dedupHash = {}; /** * Rate limiting promise to ensure that events are not fetched too often, * initially resolved to allow an immediate API call. * @var {Promise} * @private */ this._rateLimiter = bluebird_1.Promise.resolve(); this._options = Object.assign({}, DEFAULT_OPTIONS, options); } /** * Retrieve the url and params for long polling for new updates * @returns {Promise} Promise for testing purposes * @private */ getLongPollInfo() { if (this.destroyed) { return bluebird_1.Promise.resolve(false); } return this._client.events .getLongPollInfo() .then((longPollInfo) => { // On getting new long poll info, reset everything this._longPollInfo = longPollInfo; this._longPollRetries = 0; return this.doLongPoll(); }) .catch((err /* FIXME */) => { this.emit('error', err); // Only retry on resolvable errors if (!err.authExpired) { this.retryPollInfo(); } }); } /** * Long poll for notification of new events. We do this rather than * polling for the events directly in order to minimize the number of API * calls necessary. * @returns {Promise} Promise for testing pruposes * @private */ doLongPoll() { if (this.destroyed) { return bluebird_1.Promise.resolve(false); } // If we're over the max number of retries, reset if (this._longPollRetries > this._longPollInfo.max_retries) { return this.getLongPollInfo(); } var url = this._longPollInfo.url, qsDelim = url.indexOf('?'), query = {}; // Break out the query params, otherwise the request URL gets messed up if (qsDelim > 0) { query = querystring_1.default.parse(url.substr(qsDelim + 1)); url = url.substr(0, qsDelim); } query.stream_position = this._streamPosition; var options = { qs: query, timeout: this._longPollInfo.retry_timeout * 1000, }; this._longPollRetries += 1; return this._client .wrapWithDefaultHandler(this._client.get)(url, options) .then((data /* FIXME */) => { if (this.destroyed) { return false; } if (data.message === 'reconnect') { return this.getLongPollInfo(); } // We don't expect any messages other than reconnect and new_change, so if // we get one just retry the long poll if (data.message !== 'new_change') { return this.doLongPoll(); } return this.fetchEvents(); }) .catch(() => { this.retryPollInfo(); }); } /** * Retries long-polling after a delay. * Does not attempt if stream is already destroyed. * @returns {void} * @private */ retryPollInfo() { if (!this.destroyed) { this._retryTimer = setTimeout(() => this.getLongPollInfo(), this._options.retryDelay); } } /** * Fetch the latest group of events and push them into the stream * @returns {Promise} Promise for testing purposes * @private */ fetchEvents() { if (this.destroyed) { return bluebird_1.Promise.resolve(false); } var eventParams = { stream_position: this._streamPosition, limit: 500, }; // Get new events after the rate limiter expires return this._rateLimiter.then(() => this._client.events .get(eventParams) .then((events /* FIXME */) => { // Reset the rate limiter this._rateLimiter = bluebird_1.Promise.delay(this._options.fetchInterval); // If the response wasn't what we expected, re-poll if (!events.entries || !events.next_stream_position) { return this.doLongPoll(); } this._streamPosition = events.next_stream_position; // De-duplicate the fetched events, since the API often returns // the same events at multiple subsequent stream positions var newEvents = events.entries.filter((event /* FIXME */) => !this._dedupHash[event.event_id]); // If there aren't any non-duplicate events, go back to polling if (newEvents.length === 0) { return this.doLongPoll(); } // Pause the stream to avoid race conditions while pushing in the new events. // Without this, _read() would be called again from inside each push(), // resulting in multiple parallel calls to fetchEvents(). // See https://github.com/nodejs/node/issues/3203 var wasPaused = this.isPaused(); this.pause(); // Push new events into the stream newEvents.forEach((event /* FIXME */) => { this._dedupHash[event.event_id] = true; this.push(event); }); if (!wasPaused) { // This will deliver the events and trigger the next call to _read() once they have been consumed. this.resume(); } // Once the deduplication filter gets too big, clean it up if (Object.keys(this._dedupHash).length >= this._options.deduplicationFilterSize) { this.cleanupDedupFilter(events.entries); } return true; }) .catch((err /* FIXME */) => { this.emit('error', err); this.retryPollInfo(); })); } /** * Clean up the deduplication filter, to prevent it from growing * too big and eating up memory. We look at the latest set of events * returned and assume that any IDs not in that set don't need to be * tracked for deduplication any more. * @param {Object[]} latestEvents The latest events from the API * @returns {void} * @private */ cleanupDedupFilter(latestEvents /* FIXME */) { var dedupIDs = Object.keys(this._dedupHash); dedupIDs.forEach((eventID) => { var isEventCleared = !latestEvents.find((e /* FIXME */) => e.event_id === eventID); if (isEventCleared) { delete this._dedupHash[eventID]; } }); } /** * Implementation of the stream-internal read function. This is called * by the stream whenever it needs more data, and will not be called again * until data is pushed into the stream. * @returns {void} * @private */ _read() { // Start the process of getting new events this.getLongPollInfo(); } /** * Implementation of stream-internal `_destroy` function (v8.0.0 and later). * Called by stream consumers to effectively stop polling via the public * `destroy()`. * @returns {void} * @private */ _destroy() { clearTimeout(this._retryTimer); delete this._retryTimer; } } // backwards-compat for Node.js pre-v8.0.0 /* istanbul ignore if */ if (typeof stream_1.Readable.prototype.destroy !== 'function') { /** * Destroys the stream. Rough polyfill for `Readable#destroy`. * @returns {void} * @public */ EventStream.prototype.destroy = function (error) { if (!this.destroyed) { process.nextTick(() => { this.emit('close'); }); this.destroyed = true; this._destroy(); } return this; }; } module.exports = EventStream; //# sourceMappingURL=event-stream.js.map