diff --git a/package-lock.json b/package-lock.json index ee3a55b..437f203 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,7 +9,7 @@ "version": "2.9.0", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "2.10.0", + "@splitsoftware/splitio-commons": "3.0.0-rc.0", "bloom-filters": "^3.0.4", "config": "^3.3.9", "express": "^4.17.1", @@ -2394,20 +2394,28 @@ } }, "node_modules/@splitsoftware/splitio-commons": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-2.10.0.tgz", - "integrity": "sha512-+pkDTgcswrvpYIviP5wg9S+XdSQ4HjnjfXuCnPlYN8/aDiXUmzfQxLPyWoz7i5THabxb98QgGyQYRn3fQ8IQnA==", + "version": "3.0.0-rc.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-3.0.0-rc.0.tgz", + "integrity": "sha512-ZtwPsZHZp1yNAkCFzaPH708ntMaaI7CEbIyIdZeGLwINdWyIunAF29bYslqwuqmtaNd2X9El0Z6YbkqtDoKNMw==", "license": "Apache-2.0", "dependencies": { "@types/ioredis": "^4.28.0", "tslib": "^2.3.1" }, "peerDependencies": { - "ioredis": "^4.28.0" + "bloom-filters": "^3.0.0", + "ioredis": "^4.28.0 || ^5.0.0", + "node-fetch": "^2.7.0" }, "peerDependenciesMeta": { + "bloom-filters": { + "optional": true + }, "ioredis": { "optional": true + }, + "node-fetch": { + "optional": true } } }, @@ -2909,6 +2917,7 @@ "resolved": "https://registry.npmjs.org/bloom-filters/-/bloom-filters-3.0.4.tgz", "integrity": "sha512-BdnPWo2OpYhlvuP2fRzJBdioMCkm7Zp0HCf8NJgF5Mbyqy7VQ/CnTiVWMMyq4EZCBHwj0Kq6098gW2/3RsZsrA==", "license": "MIT", + "peer": true, "dependencies": { "@types/seedrandom": "^3.0.8", "base64-arraybuffer": "^1.0.2", @@ -5852,6 +5861,7 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "peer": true, "dependencies": { "whatwg-url": "^5.0.0" }, diff --git a/package.json b/package.json index 6c8d2f0..1b24dc0 100644 --- a/package.json +++ b/package.json @@ -41,7 +41,7 @@ "test": "NODE_ENV=test jest" }, "dependencies": { - "@splitsoftware/splitio-commons": "2.10.0", + "@splitsoftware/splitio-commons": "3.0.0-rc.0", "bloom-filters": "^3.0.4", "config": "^3.3.9", "js-yaml": "^3.14.2", diff --git a/sdk/index.js b/sdk/index.js index 89145fb..cdfc5d6 100644 --- a/sdk/index.js +++ b/sdk/index.js @@ -4,8 +4,8 @@ const { getRolloutPlan } = require('@splitsoftware/splitio-commons/cjs/storages/ const { sdkFactory } = require('@splitsoftware/splitio-commons/cjs/sdkFactory'); const { isConsumerMode } = require('@splitsoftware/splitio-commons/cjs/utils/settingsValidation/mode'); const { settingsFactory } = require('./settings'); -const { platform, SignalListener } = require('./platform'); -const { bloomFilterFactory } = require('./platform/filter/bloomFilter'); +const { platform } = require('@splitsoftware/splitio-commons/cjs/platform/node'); +const { bloomFilterFactory } = require('@splitsoftware/splitio-commons/cjs/utils/filter/bloomFilter'); /** * @@ -21,8 +21,6 @@ function getModules(settings) { platform, - SignalListener, - filterAdapterFactory: bloomFilterFactory, extraProps: (params) => { diff --git a/sdk/platform/filter/bloomFilter.js b/sdk/platform/filter/bloomFilter.js deleted file mode 100644 index 2e650d1..0000000 --- a/sdk/platform/filter/bloomFilter.js +++ /dev/null @@ -1,37 +0,0 @@ -const { BloomFilter } = require('bloom-filters'); - -const EXPECTED_INSERTIONS = 10000000; -const ERROR_RATE = 0.01; -const REFRESH_RATE = 24 * 60 * 60000; // 24HS - -function bloomFilterFactory(expectedInsertions = EXPECTED_INSERTIONS, errorRate = ERROR_RATE, refreshRate = REFRESH_RATE) { - let filter = BloomFilter.create(expectedInsertions, errorRate); - - return { - - refreshRate: refreshRate, - - add(key, value) { - const data = `${key}:${value}`; - if (filter.has(data)) { - return false; - } - filter.add(data); - return true; - }, - - contains(key, value) { - const data = `${key}:${value}`; - return filter.has(data); - }, - - clear() { - filter = BloomFilter.create(expectedInsertions, errorRate); - }, - - }; -} - -module.exports = { - bloomFilterFactory, -}; \ No newline at end of file diff --git a/sdk/platform/getEventSource/eventsource.js b/sdk/platform/getEventSource/eventsource.js deleted file mode 100644 index db3b61c..0000000 --- a/sdk/platform/getEventSource/eventsource.js +++ /dev/null @@ -1,519 +0,0 @@ -/* eslint-disable no-prototype-builtins */ -/* eslint-disable no-restricted-syntax */ -/* -Modified version of "eventsource" v1.1.2 package (https://www.npmjs.com/package/eventsource/v/1.1.2) -that accepts a custom agent. - -The MIT License - -Copyright (c) EventSource GitHub organization - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -*/ -var parse = require('url').parse; -var events = require('events'); -var https = require('https'); -var http = require('http'); -var util = require('util'); - -var httpsOptions = [ - 'pfx', 'key', 'passphrase', 'cert', 'ca', 'ciphers', - 'rejectUnauthorized', 'secureProtocol', 'servername', 'checkServerIdentity' -]; - -var bom = [239, 187, 191]; -var colon = 58; -var space = 32; -var lineFeed = 10; -var carriageReturn = 13; - -function hasBom(buf) { - return bom.every(function (charCode, index) { - return buf[index] === charCode; - }); -} - -/** - * Creates a new EventSource object - * - * @param {String} url the URL to which to connect - * @param {Object} [eventSourceInitDict] extra init params. See README for details. - * @api public - **/ -function EventSource(url, eventSourceInitDict) { - var readyState = EventSource.CONNECTING; - var headers = eventSourceInitDict && eventSourceInitDict.headers; - var hasNewOrigin = false; - Object.defineProperty(this, 'readyState', { - get: function () { - return readyState; - }, - }); - - Object.defineProperty(this, 'url', { - get: function () { - return url; - }, - }); - - var self = this; - self.reconnectInterval = 1000; - self.connectionInProgress = false; - - var reconnectUrl = null; - - function onConnectionClosed(message) { - if (readyState === EventSource.CLOSED) return; - readyState = EventSource.CONNECTING; - _emit('error', new Event('error', { message: message })); - - // The url may have been changed by a temporary redirect. If that's the case, - // revert it now, and flag that we are no longer pointing to a new origin - if (reconnectUrl) { - url = reconnectUrl; - reconnectUrl = null; - hasNewOrigin = false; - } - setTimeout(function () { - if (readyState !== EventSource.CONNECTING || self.connectionInProgress) { - return; - } - self.connectionInProgress = true; - connect(); - }, self.reconnectInterval); - } - - var req; - var lastEventId = ''; - if (headers && headers['Last-Event-ID']) { - lastEventId = headers['Last-Event-ID']; - delete headers['Last-Event-ID']; - } - - var discardTrailingNewline = false; - var data = ''; - var eventName = ''; - - function connect() { - var options = parse(url); - var isSecure = options.protocol === 'https:'; - options.headers = { 'Cache-Control': 'no-cache', 'Accept': 'text/event-stream' }; - if (lastEventId) options.headers['Last-Event-ID'] = lastEventId; - if (headers) { - var reqHeaders = hasNewOrigin ? removeUnsafeHeaders(headers) : headers; - for (var i in reqHeaders) { - var header = reqHeaders[i]; - if (header) { - options.headers[i] = header; - } - } - } - - // Legacy: this should be specified as `eventSourceInitDict.https.rejectUnauthorized`, - // but for now exists as a backwards-compatibility layer - options.rejectUnauthorized = !(eventSourceInitDict && !eventSourceInitDict.rejectUnauthorized); - - if (eventSourceInitDict && eventSourceInitDict.createConnection !== undefined) { - options.createConnection = eventSourceInitDict.createConnection; - } - - // If specify agent, use it. - if (eventSourceInitDict && eventSourceInitDict.agent !== undefined) { - options.agent = eventSourceInitDict.agent; - } - - // If specify http proxy, make the request to sent to the proxy server, - // and include the original url in path and Host headers - var useProxy = eventSourceInitDict && eventSourceInitDict.proxy; - if (useProxy) { - var proxy = parse(eventSourceInitDict.proxy); - isSecure = proxy.protocol === 'https:'; - - options.protocol = isSecure ? 'https:' : 'http:'; - options.path = url; - options.headers.Host = options.host; - options.hostname = proxy.hostname; - options.host = proxy.host; - options.port = proxy.port; - } - - // If https options are specified, merge them into the request options - if (eventSourceInitDict && eventSourceInitDict.https) { - for (var optName in eventSourceInitDict.https) { - if (httpsOptions.indexOf(optName) === -1) { - continue; - } - - var option = eventSourceInitDict.https[optName]; - if (option !== undefined) { - options[optName] = option; - } - } - } - - // Pass this on to the XHR - if (eventSourceInitDict && eventSourceInitDict.withCredentials !== undefined) { - options.withCredentials = eventSourceInitDict.withCredentials; - } - - req = (isSecure ? https : http).request(options, function (res) { - self.connectionInProgress = false; - // Handle HTTP errors - if (res.statusCode === 500 || res.statusCode === 502 || res.statusCode === 503 || res.statusCode === 504) { - _emit('error', new Event('error', { status: res.statusCode, message: res.statusMessage })); - onConnectionClosed(); - return; - } - - // Handle HTTP redirects - if (res.statusCode === 301 || res.statusCode === 302 || res.statusCode === 307) { - var location = res.headers.location; - if (!location) { - // Server sent redirect response without Location header. - _emit('error', new Event('error', { status: res.statusCode, message: res.statusMessage })); - return; - } - var prevOrigin = getOrigin(url); - var nextOrigin = getOrigin(location); - hasNewOrigin = prevOrigin !== nextOrigin; - if (res.statusCode === 307) reconnectUrl = url; - url = location; - process.nextTick(connect); - return; - } - - if (res.statusCode !== 200) { - _emit('error', new Event('error', { status: res.statusCode, message: res.statusMessage })); - return self.close(); - } - - readyState = EventSource.OPEN; - res.on('close', function () { - res.removeAllListeners('close'); - res.removeAllListeners('end'); - onConnectionClosed(); - }); - - res.on('end', function () { - res.removeAllListeners('close'); - res.removeAllListeners('end'); - onConnectionClosed(); - }); - _emit('open', new Event('open')); - - // text/event-stream parser adapted from webkit's - // Source/WebCore/page/EventSource.cpp - var isFirst = true; - var buf; - var startingPos = 0; - var startingFieldLength = -1; - res.on('data', function (chunk) { - buf = buf ? Buffer.concat([buf, chunk]) : chunk; - if (isFirst && hasBom(buf)) { - buf = buf.slice(bom.length); - } - - isFirst = false; - var pos = 0; - var length = buf.length; - - while (pos < length) { - if (discardTrailingNewline) { - if (buf[pos] === lineFeed) { - ++pos; - } - discardTrailingNewline = false; - } - - var lineLength = -1; - var fieldLength = startingFieldLength; - var c; - - for (var i = startingPos; lineLength < 0 && i < length; ++i) { - c = buf[i]; - if (c === colon) { - if (fieldLength < 0) { - fieldLength = i - pos; - } - } else if (c === carriageReturn) { - discardTrailingNewline = true; - lineLength = i - pos; - } else if (c === lineFeed) { - lineLength = i - pos; - } - } - - if (lineLength < 0) { - startingPos = length - pos; - startingFieldLength = fieldLength; - break; - } else { - startingPos = 0; - startingFieldLength = -1; - } - - parseEventStreamLine(buf, pos, fieldLength, lineLength); - - pos += lineLength + 1; - } - - if (pos === length) { - buf = void 0; - } else if (pos > 0) { - buf = buf.slice(pos); - } - }); - }); - - req.on('error', function (err) { - self.connectionInProgress = false; - onConnectionClosed(err.message); - }); - - if (req.setNoDelay) req.setNoDelay(true); - req.end(); - } - - connect(); - - function _emit() { - if (self.listeners(arguments[0]).length > 0) { - self.emit.apply(self, arguments); - } - } - - this._close = function () { - if (readyState === EventSource.CLOSED) return; - readyState = EventSource.CLOSED; - if (req.abort) req.abort(); - if (req.xhr && req.xhr.abort) req.xhr.abort(); - }; - - function parseEventStreamLine(buf, pos, fieldLength, lineLength) { - if (lineLength === 0) { - if (data.length > 0) { - var type = eventName || 'message'; - _emit(type, new MessageEvent(type, { - data: data.slice(0, -1), // remove trailing newline - lastEventId: lastEventId, - origin: getOrigin(url), - })); - data = ''; - } - eventName = void 0; - } else if (fieldLength > 0) { - var noValue = fieldLength < 0; - var step = 0; - var field = buf.slice(pos, pos + (noValue ? lineLength : fieldLength)).toString(); - - if (noValue) { - step = lineLength; - } else if (buf[pos + fieldLength + 1] !== space) { - step = fieldLength + 1; - } else { - step = fieldLength + 2; - } - pos += step; - - var valueLength = lineLength - step; - var value = buf.slice(pos, pos + valueLength).toString(); - - if (field === 'data') { - data += value + '\n'; - } else if (field === 'event') { - eventName = value; - } else if (field === 'id') { - lastEventId = value; - } else if (field === 'retry') { - var retry = parseInt(value, 10); - if (!Number.isNaN(retry)) { - self.reconnectInterval = retry; - } - } - } - } -} - -module.exports = EventSource; - -util.inherits(EventSource, events.EventEmitter); -EventSource.prototype.constructor = EventSource; // make stacktraces readable - -['open', 'error', 'message'].forEach(function (method) { - Object.defineProperty(EventSource.prototype, 'on' + method, { - /** - * Returns the current listener - * - * @return {Mixed} the set function or undefined - * @api private - */ - get: function get() { - var listener = this.listeners(method)[0]; - return listener ? (listener._listener ? listener._listener : listener) : undefined; - }, - - /** - * Start listening for events - * - * @param {Function} listener the listener - * @return {Mixed} the set function or undefined - * @api private - */ - set: function set(listener) { - this.removeAllListeners(method); - this.addEventListener(method, listener); - }, - }); -}); - -/** - * Ready states - */ -Object.defineProperty(EventSource, 'CONNECTING', { enumerable: true, value: 0 }); -Object.defineProperty(EventSource, 'OPEN', { enumerable: true, value: 1 }); -Object.defineProperty(EventSource, 'CLOSED', { enumerable: true, value: 2 }); - -EventSource.prototype.CONNECTING = 0; -EventSource.prototype.OPEN = 1; -EventSource.prototype.CLOSED = 2; - -/** - * Closes the connection, if one is made, and sets the readyState attribute to 2 (closed) - * - * @see https://developer.mozilla.org/en-US/docs/Web/API/EventSource/close - * @api public - */ -EventSource.prototype.close = function () { - this._close(); -}; - -/** - * Emulates the W3C Browser based WebSocket interface using addEventListener. - * - * @param {String} type A string representing the event type to listen out for - * @param {Function} listener callback - * @see https://developer.mozilla.org/en/DOM/element.addEventListener - * @see http://dev.w3.org/html5/websockets/#the-websocket-interface - * @api public - */ -EventSource.prototype.addEventListener = function addEventListener(type, listener) { - if (typeof listener === 'function') { - // store a reference so we can return the original function again - listener._listener = listener; - this.on(type, listener); - } -}; - -/** - * Emulates the W3C Browser based WebSocket interface using dispatchEvent. - * - * @param {Event} event An event to be dispatched - * @see https://developer.mozilla.org/en-US/docs/Web/API/EventTarget/dispatchEvent - * @api public - */ -EventSource.prototype.dispatchEvent = function dispatchEvent(event) { - if (!event.type) { - throw new Error('UNSPECIFIED_EVENT_TYPE_ERR'); - } - // if event is instance of an CustomEvent (or has 'details' property), - // send the detail object as the payload for the event - this.emit(event.type, event.detail); -}; - -/** - * Emulates the W3C Browser based WebSocket interface using removeEventListener. - * - * @param {String} type A string representing the event type to remove - * @param {Function} listener callback - * @see https://developer.mozilla.org/en/DOM/element.removeEventListener - * @see http://dev.w3.org/html5/websockets/#the-websocket-interface - * @api public - */ -EventSource.prototype.removeEventListener = function removeEventListener(type, listener) { - if (typeof listener === 'function') { - listener._listener = undefined; - this.removeListener(type, listener); - } -}; - -/** - * W3C Event - * - * @see http://www.w3.org/TR/DOM-Level-3-Events/#interface-Event - * @api private - */ -function Event(type, optionalProperties) { - Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }); - if (optionalProperties) { - for (var f in optionalProperties) { - if (optionalProperties.hasOwnProperty(f)) { - Object.defineProperty(this, f, { writable: false, value: optionalProperties[f], enumerable: true }); - } - } - } -} - -/** - * W3C MessageEvent - * - * @see http://www.w3.org/TR/webmessaging/#event-definitions - * @api private - */ -function MessageEvent(type, eventInitDict) { - Object.defineProperty(this, 'type', { writable: false, value: type, enumerable: true }); - for (var f in eventInitDict) { - if (eventInitDict.hasOwnProperty(f)) { - Object.defineProperty(this, f, { writable: false, value: eventInitDict[f], enumerable: true }); - } - } -} - -/** - * Returns a new object of headers that does not include any authorization and cookie headers - * - * @param {Object} headers An object of headers ({[headerName]: headerValue}) - * @return {Object} a new object of headers - * @api private - */ -function removeUnsafeHeaders(headers) { - var safe = {}; - for (var key in headers) { - if (/^(cookie|authorization)$/i.test(key)) { - continue; - } - - safe[key] = headers[key]; - } - - return safe; -} - -/** - * Transform an URL to a valid origin value. - * - * @param {String|Object} url URL to transform to it's origin. - * @returns {String} The origin. - * @api private - */ -function getOrigin(url) { - if (typeof url === 'string') url = parse(url); - if (!url.protocol || !url.hostname) return 'null'; - return (url.protocol + '//' + url.host).toLowerCase(); -} diff --git a/sdk/platform/getEventSource/index.js b/sdk/platform/getEventSource/index.js deleted file mode 100644 index cf5525e..0000000 --- a/sdk/platform/getEventSource/index.js +++ /dev/null @@ -1,28 +0,0 @@ -let __isCustom = false; -let __eventSource = undefined; - -// This function is only exposed for testing purposes. -function __setEventSource(eventSource) { - __eventSource = eventSource; - __isCustom = true; -} -function __restore() { - __isCustom = false; -} - -function getEventSource() { - // returns EventSource at `eventsource` package. If not available, return global EventSource or undefined - try { - return __isCustom ? __eventSource : require('./eventsource.js'); - } catch (error) { - console.error(error); - // eslint-disable-next-line no-undef - return typeof EventSource === 'function' ? EventSource : undefined; - } -} - -module.exports = { - getEventSource, - __setEventSource, - __restore, -}; \ No newline at end of file diff --git a/sdk/platform/getFetch/index.js b/sdk/platform/getFetch/index.js deleted file mode 100644 index 8c0176c..0000000 --- a/sdk/platform/getFetch/index.js +++ /dev/null @@ -1,29 +0,0 @@ -let nodeFetch; - -try { - nodeFetch = require('node-fetch'); - - // Handle node-fetch issue https://github.com/node-fetch/node-fetch/issues/1037 - if (typeof nodeFetch !== 'function') nodeFetch = nodeFetch.default; - -} catch (error) { - // Try to access global fetch if `node-fetch` package couldn't be imported (e.g., not in a Node environment) - nodeFetch = typeof fetch === 'function' ? fetch : undefined; -} - -// This function is only exposed for testing purposes. -function __setFetch(fetch) { - nodeFetch = fetch; -} - -/** - * Retrieves 'node-fetch', a Fetch API polyfill for Node.js, with fallback to global 'fetch' if available. - */ -function getFetch() { - return nodeFetch; -} - -module.exports = { - getFetch, - __setFetch, // Exposed for testing purposes only. -}; \ No newline at end of file diff --git a/sdk/platform/getOptions/index.js b/sdk/platform/getOptions/index.js deleted file mode 100644 index 21646e1..0000000 --- a/sdk/platform/getOptions/index.js +++ /dev/null @@ -1,27 +0,0 @@ -// @TODO -// 1- handle multiple protocols automatically -// 2- destroy it once the sdk is destroyed -const https = require('https'); - -const { find } = require('@splitsoftware/splitio-commons/cjs/utils/lang'); - -const agent = new https.Agent({ - keepAlive: true, - keepAliveMsecs: 1500, -}); - -function getOptions(settings) { - // User provided options take precedence - if (settings.sync.requestOptions) return settings.sync.requestOptions; - - // If some URL is not HTTPS, we don't use the agent, to let the SDK connect to HTTP endpoints - if (find(settings.urls, url => !url.startsWith('https:'))) return; - - return { - agent, - }; -} - -module.exports = { - getOptions, -}; \ No newline at end of file diff --git a/sdk/platform/index.js b/sdk/platform/index.js deleted file mode 100644 index 37859eb..0000000 --- a/sdk/platform/index.js +++ /dev/null @@ -1,16 +0,0 @@ -const EventEmitter = require('events'); -const { getFetch } = require('./getFetch'); -const { getEventSource } = require('./getEventSource'); -const { getOptions } = require('./getOptions'); -const { NodeSignalListener } = require('@splitsoftware/splitio-commons/cjs/listeners/node'); -const { now } = require('@splitsoftware/splitio-commons/cjs/utils/timeTracker/now/node'); - -exports.platform = { - getFetch, - getEventSource, - getOptions, - EventEmitter, - now, -}; - -exports.SignalListener = NodeSignalListener; \ No newline at end of file