diff --git a/CHANGES.txt b/CHANGES.txt index 8a9cf92c6..fda8ab2dd 100644 --- a/CHANGES.txt +++ b/CHANGES.txt @@ -1,3 +1,9 @@ +10.26.1 (June 14, 2024) + - Updated the internal imports of 'os' and 'ioredis' modules for NodeJS, to use EcmaScript 'import' rather than CommonJS 'require' for the ES modules build. This avoids runtime errors on some scenarios when bundling the SDK into a .mjs file or importing it from a .mjs file. + - Updated @splitsoftware/splitio-commons package to version 1.16.0 that includes some vulnerability and bug fixes. + - Bugfixing - Restored some input validation error logs that were removed in version 10.24.0. The logs inform the user when the `getTreatment(s)` methods are called with an invalid value as feature flag name or flag set name. + - Bugfixing - Fixed localhost mode to emit SDK_UPDATE when mocked feature flags are updated in the `features` object map of the config object (Related to issue https://github.com/splitio/javascript-browser-client/issues/119). + 10.26.0 (May 6, 2024) - Updated @splitsoftware/splitio-commons package to version 1.14.0 that includes minor updates: - Added support for targeting rules based on semantic versions (https://semver.org/). @@ -80,7 +86,7 @@ - Bugfixing - Moved js-yaml dependency from @splitsoftware/splitio-commons to avoid resolution to an incompatible version on certain npm versions when installing third-party dependencies that also define js-yaml as transitive dependency (Related to issue https://github.com/splitio/javascript-client/issues/662). 10.20.0 (June 29, 2022) - - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config sync.enabled . Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used. + - Added a new config option to control the tasks that listen or poll for updates on feature flags and segments, via the new config `sync.enabled`. Running online Split will always pull the most recent updates upon initialization, this only affects updates fetching on a running instance. Useful when a consistent session experience is a must or to save resources when updates are not being used. - Updated telemetry logic to track the anonymous config for user consent flag set to declined or unknown. - Updated submitters logic, to avoid duplicating the post of impressions to Split cloud when the SDK is destroyed while its periodic post of impressions is running. diff --git a/package-lock.json b/package-lock.json index d794787d8..016340127 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,15 +1,15 @@ { "name": "@splitsoftware/splitio", - "version": "10.26.0", + "version": "10.26.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "@splitsoftware/splitio", - "version": "10.26.0", + "version": "10.26.1", "license": "Apache-2.0", "dependencies": { - "@splitsoftware/splitio-commons": "1.14.0", + "@splitsoftware/splitio-commons": "1.16.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -55,9 +55,6 @@ "engines": { "node": ">=6", "npm": ">=3" - }, - "optionalDependencies": { - "eventsource": "^1.1.2" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -875,9 +872,9 @@ "dev": true }, "node_modules/@splitsoftware/splitio-commons": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.14.0.tgz", - "integrity": "sha512-ANP0NRPAMehi4bUQsb19kP5W5NVuCYUKRsDC5Nl78xHIu6cskAej1rXkjsocLnWerz2rO0H9kMjRKZj9lVsvKA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.16.0.tgz", + "integrity": "sha512-k16cCWJOWut/NB5W1d9hQEYPxFrZXO66manp+8d6RjZYH4r+Q6lu82NYjDcfh5E93H9v+TVKcQLAmpVofbjcvg==", "dependencies": { "tslib": "^2.3.1" }, @@ -1653,12 +1650,12 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -3108,15 +3105,6 @@ "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==", "dev": true }, - "node_modules/eventsource": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz", - "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==", - "optional": true, - "engines": { - "node": ">=0.12.0" - } - }, "node_modules/evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -3257,9 +3245,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "dependencies": { "to-regex-range": "^5.0.1" @@ -8447,9 +8435,9 @@ "dev": true }, "@splitsoftware/splitio-commons": { - "version": "1.14.0", - "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.14.0.tgz", - "integrity": "sha512-ANP0NRPAMehi4bUQsb19kP5W5NVuCYUKRsDC5Nl78xHIu6cskAej1rXkjsocLnWerz2rO0H9kMjRKZj9lVsvKA==", + "version": "1.16.0", + "resolved": "https://registry.npmjs.org/@splitsoftware/splitio-commons/-/splitio-commons-1.16.0.tgz", + "integrity": "sha512-k16cCWJOWut/NB5W1d9hQEYPxFrZXO66manp+8d6RjZYH4r+Q6lu82NYjDcfh5E93H9v+TVKcQLAmpVofbjcvg==", "requires": { "tslib": "^2.3.1" } @@ -9111,12 +9099,12 @@ } }, "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, "requires": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" } }, "brorand": { @@ -10260,12 +10248,6 @@ "integrity": "sha512-inRWzRY7nG+aXZxBzEqYKB3HPgwflZRopAjDCHv0whhRx+MTUr1ei0ICZUypdyE0HRm4L2d5VEcIqLD6yl+BFA==", "dev": true }, - "eventsource": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.1.2.tgz", - "integrity": "sha512-xAH3zWhgO2/3KIniEKYPr8plNSzlGINOUqYj0m0u7AB81iRw8b/3E73W6AuU+6klLbaSFmZnaETQ2lXPfAydrA==", - "optional": true - }, "evp_bytestokey": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", @@ -10374,9 +10356,9 @@ } }, "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, "requires": { "to-regex-range": "^5.0.1" diff --git a/package.json b/package.json index b301c0345..a8ce5e003 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@splitsoftware/splitio", - "version": "10.26.0", + "version": "10.26.1", "description": "Split SDK", "files": [ "README.md", @@ -40,7 +40,7 @@ "node": ">=6" }, "dependencies": { - "@splitsoftware/splitio-commons": "1.14.0", + "@splitsoftware/splitio-commons": "1.16.0", "@types/google.analytics": "0.0.40", "@types/ioredis": "^4.28.0", "bloom-filters": "^3.0.0", @@ -50,9 +50,6 @@ "tslib": "^2.3.1", "unfetch": "^4.2.0" }, - "optionalDependencies": { - "eventsource": "^1.1.2" - }, "devDependencies": { "@types/node-fetch": "^2.6.10", "@types/seedrandom": "^3.0.2", diff --git a/src/__tests__/browserSuites/ready-from-cache.spec.js b/src/__tests__/browserSuites/ready-from-cache.spec.js index 828f16122..70eb2cda9 100644 --- a/src/__tests__/browserSuites/ready-from-cache.spec.js +++ b/src/__tests__/browserSuites/ready-from-cache.spec.js @@ -588,7 +588,7 @@ export default function (fetchMock, assert) { fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.1&since=25&names=p2__split&prefixes=p1', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: 25, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); - const expectedHash = getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&names=p2__split&prefixes=p1' } } }); + const expectedHash = getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&names=p2__split&prefixes=p1' }, flagSpecVersion: '1.1' } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_6.SPLITIO.splits.till', 25); localStorage.setItem('readyFromCache_6.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); @@ -639,7 +639,7 @@ export default function (fetchMock, assert) { fetchMock.getOnce(testUrls.sdk + '/splitChanges?s=1.1&since=-1&prefixes=p1,p2', { status: 200, body: { splits: [splitDeclarations.p1__split, splitDeclarations.p2__split], since: -1, till: 1457552620999 } }, { delay: 10 }); // short delay to let emit SDK_READY_FROM_CACHE fetchMock.getOnce(testUrls.sdk + '/mySegments/nicolas%40split.io', { status: 200, body: { mySegments: [] } }); - const expectedHash = getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&prefixes=p1,p2' } } }); + const expectedHash = getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&prefixes=p1,p2' }, flagSpecVersion: '1.1' } }); localStorage.setItem('some_user_item', 'user_item'); localStorage.setItem('readyFromCache_7.SPLITIO.splits.till', 25); localStorage.setItem('readyFromCache_7.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); @@ -763,7 +763,7 @@ export default function (fetchMock, assert) { localStorage.setItem('readyFromCache_9.SPLITIO.splits.till', 25); localStorage.setItem('readyFromCache_9.SPLITIO.split.p1__split', JSON.stringify(splitDeclarations.p1__split)); localStorage.setItem('readyFromCache_9.SPLITIO.split.p2__split', JSON.stringify(splitDeclarations.p2__split)); - localStorage.setItem('readyFromCache_9.SPLITIO.hash', getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&names=p2__split&prefixes=p1' } } })); + localStorage.setItem('readyFromCache_9.SPLITIO.hash', getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&names=p2__split&prefixes=p1' }, flagSpecVersion: '1.1' } })); const splitio = SplitFactory({ ...baseConfig, @@ -788,7 +788,7 @@ export default function (fetchMock, assert) { t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.splits.till'), '1457552620999', 'splits.till must correspond to the till of the last successfully fetched Splits'); t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.split.p2__split'), JSON.stringify(splitDeclarations.p2__split), 'feature flag declarations must be cached'); t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.split.p3__split'), JSON.stringify(splitDeclarations.p3__split), 'feature flag declarations must be cached'); - t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.hash'), getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&names=no%20exist%20trim,no_exist,p3__split&prefixes=no%20exist%20trim,p2' } } }), 'Storage hash must correspond to the split filter query and SDK key'); + t.equal(localStorage.getItem('readyFromCache_9.SPLITIO.hash'), getStorageHash({ ...baseConfig, sync: { __splitFiltersValidation: { queryString: '&names=no%20exist%20trim,no_exist,p3__split&prefixes=no%20exist%20trim,p2' }, flagSpecVersion: '1.1' } }), 'Storage hash must correspond to the split filter query and SDK key'); t.end(); }); }); diff --git a/src/__tests__/consumer/node_redis.spec.js b/src/__tests__/consumer/node_redis.spec.js index cf9239ad6..74d579fb5 100644 --- a/src/__tests__/consumer/node_redis.spec.js +++ b/src/__tests__/consumer/node_redis.spec.js @@ -1,7 +1,7 @@ /* eslint-disable no-console */ import osFunction from 'os'; -import ipFunction from '../../utils/ip'; +import * as ipFunction from '../../utils/ip'; import tape from 'tape'; import sinon from 'sinon'; import RedisServer from 'redis-server'; diff --git a/src/__tests__/errorCatching/browser.spec.js b/src/__tests__/errorCatching/browser.spec.js index a35cdf5d8..c5719ed3b 100644 --- a/src/__tests__/errorCatching/browser.spec.js +++ b/src/__tests__/errorCatching/browser.spec.js @@ -21,7 +21,7 @@ const settings = settingsFactory({ // prepare localstorage to emit SDK_READY_FROM_CACHE localStorage.clear(); localStorage.setItem('SPLITIO.splits.till', 25); -localStorage.setItem('SPLITIO.hash', getStorageHash({ core: { authorizationKey: '' }, sync: { __splitFiltersValidation: { queryString: null } } })); +localStorage.setItem('SPLITIO.hash', getStorageHash({ core: { authorizationKey: '' }, sync: { __splitFiltersValidation: { queryString: null }, flagSpecVersion: '1.1' } })); fetchMock.get(url(settings, '/splitChanges?s=1.1&since=25'), function () { return new Promise((res) => { setTimeout(() => res({ status: 200, body: splitChangesMock1 }), 1000); }); diff --git a/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js b/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js index 0e3de4098..674873ad4 100644 --- a/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js +++ b/src/__tests__/nodeSuites/ip-addresses-setting.debug.spec.js @@ -1,5 +1,5 @@ import osFunction from 'os'; -import ipFunction from '../../utils/ip'; +import * as ipFunction from '../../utils/ip'; import { SplitFactory } from '../../'; import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; diff --git a/src/__tests__/nodeSuites/ip-addresses-setting.spec.js b/src/__tests__/nodeSuites/ip-addresses-setting.spec.js index cde08e2ec..2fbf92f12 100644 --- a/src/__tests__/nodeSuites/ip-addresses-setting.spec.js +++ b/src/__tests__/nodeSuites/ip-addresses-setting.spec.js @@ -1,5 +1,5 @@ import osFunction from 'os'; -import ipFunction from '../../utils/ip'; +import * as ipFunction from '../../utils/ip'; import { SplitFactory } from '../../'; import { settingsFactory } from '../../settings'; import splitChangesMock1 from '../mocks/splitchanges.since.-1.json'; diff --git a/src/__tests__/nodeSuites/push-initialization-nopush.spec.js b/src/__tests__/nodeSuites/push-initialization-nopush.spec.js index 6fc773caa..b79db1f1f 100644 --- a/src/__tests__/nodeSuites/push-initialization-nopush.spec.js +++ b/src/__tests__/nodeSuites/push-initialization-nopush.spec.js @@ -53,7 +53,7 @@ function testInitializationFail(fetchMock, assert, fallbackToPolling) { fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=1457552620999'), function () { assert.true(ready, 'client ready'); const lapse = Date.now() - start; - assert.true(nearlyEqual(lapse, 0), 'polling (first fetch)'); + assert.true(nearlyEqual(lapse, 0, 100), 'polling (first fetch)'); return { status: 200, body: splitChangesMock2 }; }); } @@ -61,7 +61,7 @@ function testInitializationFail(fetchMock, assert, fallbackToPolling) { fetchMock.getOnce(url(settings, '/splitChanges?s=1.1&since=1457552620999'), function () { assert.true(ready, 'client ready'); const lapse = Date.now() - start; - assert.true(nearlyEqual(lapse, settings.scheduler.featuresRefreshRate), 'polling (second fetch)'); + assert.true(nearlyEqual(lapse, settings.scheduler.featuresRefreshRate, 100), 'polling (second fetch)'); client.destroy().then(() => { assert.end(); }); diff --git a/src/__tests__/nodeSuites/push-synchronization.spec.js b/src/__tests__/nodeSuites/push-synchronization.spec.js index e45f3370a..51c17724d 100644 --- a/src/__tests__/nodeSuites/push-synchronization.spec.js +++ b/src/__tests__/nodeSuites/push-synchronization.spec.js @@ -58,7 +58,7 @@ const MILLIS_IFFU_UPDATE_EVENT_WITH_NEW_SEGMENTS = 700; const MILLIS_IFFU_UPDATE_EVENT_WITH_WRONG_COMPRESS = 800; const MILLIS_IFFU_UPDATE_EVENT_WITH_OLD_CHANGENUMBER = 900; const MILLIS_IFFU_UPDATE_EVENT_WITH_ZERO_PCN = 1000; -const MILLIS_IFFU_UPDATE_EVENT_WITH_MISSING_PCN= 1100; +const MILLIS_IFFU_UPDATE_EVENT_WITH_MISSING_PCN = 1100; const MILLIS_IFFU_UPDATE_EVENT_WITH_ARCHIVED = 1200; const MILLIS_DESTROY = 1300; @@ -89,13 +89,11 @@ export function testSynchronization(fetchMock, assert) { setMockListener(function (eventSourceInstance) { const expectedSSEurl = `${url(settings, '/sse')}?channels=NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_segments,NzM2MDI5Mzc0_NDEzMjQ1MzA0Nw%3D%3D_splits,%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_pri,%5B%3Foccupancy%3Dmetrics.publishers%5Dcontrol_sec&accessToken=${authPushEnabled.token}&v=1.1&heartbeats=true`; assert.equals(eventSourceInstance.url, expectedSSEurl, 'EventSource URL is the expected'); - assert.deepEqual(eventSourceInstance.__eventSourceInitDict, { - headers: { - SplitSDKClientKey: 'h-1>', - SplitSDKVersion: settings.version, - SplitSDKMachineIP: settings.runtime.ip, - SplitSDKMachineName: settings.runtime.hostname - } + assert.deepEqual(eventSourceInstance.__eventSourceInitDict.headers, { + SplitSDKClientKey: 'h-1>', + SplitSDKVersion: settings.version, + SplitSDKMachineIP: settings.runtime.ip, + SplitSDKMachineName: settings.runtime.hostname }, 'EventSource headers are the expected'); setTimeout(() => { @@ -285,7 +283,7 @@ export function testSynchronization(fetchMock, assert) { // fetch segments due to IFFU SPLIT_UPDATE event with new segments fetchMock.getOnce(url(settings, '/segmentChanges/maur-2?since=-1'), function () { - return { status: 200, body: { since: 1457552650000, till: 1457552650000, name: 'maur-2', added: ['admin','mauro','nico'], removed: [] } }; + return { status: 200, body: { since: 1457552650000, till: 1457552650000, name: 'maur-2', added: ['admin', 'mauro', 'nico'], removed: [] } }; }); // fetch feature flags due to IFFU SPLIT_UPDATE event with wrong compress code diff --git a/src/__tests__/offline/browser.spec.js b/src/__tests__/offline/browser.spec.js index 570536435..3abc25ea7 100644 --- a/src/__tests__/offline/browser.spec.js +++ b/src/__tests__/offline/browser.spec.js @@ -88,12 +88,14 @@ tape('Browser offline mode', function (assert) { }); // Multiple factories must handle their own `features` mock, even if instantiated with the same config. - const factories = [ - SplitFactory(config), - SplitFactory({ ...config }), - SplitFactory({ ...config, features: { ...config.features }, storage: { type: 'INVALID TYPE' } }), - SplitFactory({ ...config, storage: { type: 'LOCALSTORAGE' } }) + const configs = [ + { ...config, features: { ...config.features }, storage: { type: 'INVALID TYPE' } }, + { ...config, storage: { type: 'LOCALSTORAGE' } }, + { ...config }, + config, ]; + const factories = configs.map(config => SplitFactory(config)); + let readyCount = 0, updateCount = 0, readyFromCacheCount = 0; for (let i = 0; i < factories.length; i++) { @@ -105,7 +107,7 @@ tape('Browser offline mode', function (assert) { readyCount++; }); client.on(client.Event.SDK_UPDATE, () => { - assert.deepEqual(manager.names(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']); + assert.deepEqual(manager.names().sort(), ['testing_split', 'testing_split_2', 'testing_split_3', 'testing_split_with_config']); assert.equal(client.getTreatment('testing_split_with_config'), 'nope'); updateCount++; }); @@ -211,7 +213,7 @@ tape('Browser offline mode', function (assert) { }); setTimeout(() => { - // Update the features + // Update features reference in settings factory.settings.features = { testing_split: 'on', testing_split_2: 'off', @@ -221,10 +223,23 @@ tape('Browser offline mode', function (assert) { config: null } }; - // Update the features in all factories except the last one - for (let i = 0; i < factories.length - 1; i++) { + + // Update features properties in config + configs[0].features['testing_split'] = 'on'; + configs[0].features['testing_split_2'] = 'off'; + configs[0].features['testing_split_3'] = 'custom_treatment'; + configs[0].features['testing_split_with_config'] = { + treatment: 'nope', + config: null + }; + + // Update the features in all remaining factories except the last one + for (let i = 1; i < factories.length - 1; i++) { factories[i].settings.features = factory.settings.features; } + + // Assigning a new object to the features property in the config doesn't trigger an update + configs[configs.length - 1].features = { ...factory.settings.features }; }, 1000); setTimeout(() => { factory.settings.features = originalFeaturesMap; }, 200); diff --git a/src/platform/getEventSource/__tests__/node.spec.js b/src/platform/getEventSource/__tests__/node.spec.js index e6bb1c5bc..27736468b 100644 --- a/src/platform/getEventSource/__tests__/node.spec.js +++ b/src/platform/getEventSource/__tests__/node.spec.js @@ -2,7 +2,7 @@ import tape from 'tape-catch'; import { getEventSource } from '../node'; tape('getEventSource returns eventsource module in Node', assert => { - assert.equal(getEventSource(), require('eventsource')); + assert.equal(getEventSource(), require('../eventsource')); assert.end(); }); diff --git a/src/platform/getEventSource/eventsource.js b/src/platform/getEventSource/eventsource.js new file mode 100644 index 000000000..15e750f47 --- /dev/null +++ b/src/platform/getEventSource/eventsource.js @@ -0,0 +1,519 @@ +/* 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 organisation + +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/src/platform/getEventSource/node.js b/src/platform/getEventSource/node.js index e6049faa4..4ef41d0af 100644 --- a/src/platform/getEventSource/node.js +++ b/src/platform/getEventSource/node.js @@ -13,7 +13,7 @@ export function __restore() { export function getEventSource() { // returns EventSource at `eventsource` package. If not available, return global EventSource or undefined try { - return __isCustom ? __eventSource : require('eventsource'); + return __isCustom ? __eventSource : require('./eventsource'); } catch (error) { return typeof EventSource === 'function' ? EventSource : undefined; } diff --git a/src/platform/getFetch/__tests__/node.spec.js b/src/platform/getFetch/__tests__/node.spec.js index e557f292c..144d8880c 100644 --- a/src/platform/getFetch/__tests__/node.spec.js +++ b/src/platform/getFetch/__tests__/node.spec.js @@ -1,33 +1,8 @@ import tape from 'tape-catch'; -import sinon from 'sinon'; -import { getFetch, __setFetch } from '../node'; +import { getFetch } from '../node'; -tape('getFetch returns a wrapped node-fetch module in Node', assert => { - assert.equal(typeof getFetch(), 'function'); - - assert.end(); -}); - -tape('getFetch passes an agent object to HTTPs requests', assert => { - const fetchMock = sinon.stub(); - __setFetch(fetchMock); - - const fetch = getFetch(); - - fetch('http://test.com'); - assert.true(fetchMock.calledWithExactly('http://test.com', { agent: undefined })); - - fetch('https-https://', { option: 'value' }); - assert.true(fetchMock.calledWithExactly('https-https://', { option: 'value', agent: undefined })); - - fetch('https://test.com'); - assert.true(fetchMock.calledWithExactly('https://test.com', { agent: sinon.match.object })); - - fetch('https://test.com', { option: 'value' }); - assert.true(fetchMock.calledWithExactly('https://test.com', { option: 'value', agent: sinon.match.object })); - - // Restore - __setFetch(require('node-fetch')); +tape('getFetch returns node-fetch module in Node', assert => { + assert.equal(getFetch(), require('node-fetch')); assert.end(); }); diff --git a/src/platform/getFetch/node.js b/src/platform/getFetch/node.js index a6d5ccc22..77a75e363 100644 --- a/src/platform/getFetch/node.js +++ b/src/platform/getFetch/node.js @@ -1,14 +1,3 @@ -/* eslint-disable compat/compat */ -import https from 'https'; - -// @TODO -// 1- handle multiple protocols automatically -// 2- destroy it once the sdk is destroyed -const agent = new https.Agent({ - keepAlive: true, - keepAliveMsecs: 1500 -}); - let nodeFetch; try { @@ -29,12 +18,7 @@ export function __setFetch(fetch) { /** * Retrieves 'node-fetch', a Fetch API polyfill for NodeJS, with fallback to global 'fetch' if available. - * It passes an https agent with keepAlive enabled if URL is https. */ export function getFetch() { - if (nodeFetch) { - return (url, options) => { - return nodeFetch(url, Object.assign({ agent: url.startsWith('https:') ? agent : undefined }, options)); - }; - } + return nodeFetch; } diff --git a/src/platform/getOptions/__tests__/node.spec.js b/src/platform/getOptions/__tests__/node.spec.js new file mode 100644 index 000000000..be409110b --- /dev/null +++ b/src/platform/getOptions/__tests__/node.spec.js @@ -0,0 +1,17 @@ +import tape from 'tape-catch'; +import { settingsFactory } from '../../../settings/node'; +import { getOptions } from '../node'; + +tape('getOptions returns an object with a custom agent if all urls are https', assert => { + const settings = settingsFactory({}); + assert.true(typeof getOptions(settings).agent === 'object'); + + assert.end(); +}); + +tape('getOptions returns undefined if some url is not https', assert => { + const settings = settingsFactory({ urls: { sdk: 'http://sdk.split.io' } }); + assert.equal(getOptions(settings), undefined); + + assert.end(); +}); diff --git a/src/platform/getOptions/node.js b/src/platform/getOptions/node.js new file mode 100644 index 000000000..5a6b9eca7 --- /dev/null +++ b/src/platform/getOptions/node.js @@ -0,0 +1,20 @@ +// @TODO +// 1- handle multiple protocols automatically +// 2- destroy it once the sdk is destroyed +import https from 'https'; + +import { find } from '@splitsoftware/splitio-commons/src/utils/lang'; + +const agent = new https.Agent({ + keepAlive: true, + keepAliveMsecs: 1500 +}); + +export function getOptions(settings) { + // 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 + }; +} diff --git a/src/platform/node.js b/src/platform/node.js index 0a87e3f6e..8572e70f1 100644 --- a/src/platform/node.js +++ b/src/platform/node.js @@ -1,12 +1,14 @@ import EventEmitter from 'events'; import { getFetch } from '../platform/getFetch/node'; import { getEventSource } from '../platform/getEventSource/node'; +import { getOptions } from '../platform/getOptions/node'; import { NodeSignalListener } from '@splitsoftware/splitio-commons/src/listeners/node'; import { now } from '@splitsoftware/splitio-commons/src/utils/timeTracker/now/node'; export const platform = { getFetch, getEventSource, + getOptions, EventEmitter, now }; diff --git a/src/settings/__tests__/node.spec.js b/src/settings/__tests__/node.spec.js index b876fcad4..54418b874 100644 --- a/src/settings/__tests__/node.spec.js +++ b/src/settings/__tests__/node.spec.js @@ -1,7 +1,7 @@ import tape from 'tape-catch'; import sinon from 'sinon'; import osFunction from 'os'; -import ipFunction from '../../utils/ip'; +import * as ipFunction from '../../utils/ip'; import { settingsFactory } from '../node'; import { CONSUMER_MODE, NA } from '@splitsoftware/splitio-commons/src/utils/constants'; diff --git a/src/settings/defaults/version.js b/src/settings/defaults/version.js index 768a07529..b1f5fb078 100644 --- a/src/settings/defaults/version.js +++ b/src/settings/defaults/version.js @@ -1 +1 @@ -export const packageVersion = '10.26.0'; +export const packageVersion = '10.26.1'; diff --git a/src/settings/runtime/node.js b/src/settings/runtime/node.js index 2c936219c..c835dd016 100644 --- a/src/settings/runtime/node.js +++ b/src/settings/runtime/node.js @@ -1,5 +1,5 @@ import osFunction from 'os'; -import ipFunction from '../../utils/ip'; +import { address } from '../../utils/ip'; import { UNKNOWN, NA, CONSUMER_MODE } from '@splitsoftware/splitio-commons/src/utils/constants'; @@ -8,7 +8,7 @@ export function validateRuntime(settings) { const isConsumerMode = settings.mode === CONSUMER_MODE; // If the values are not available, default to false (for standalone) or "unknown" (for consumer mode, to be used on Redis keys) - let ip = ipFunction.address() || (isConsumerMode ? UNKNOWN : false); + let ip = address() || (isConsumerMode ? UNKNOWN : false); let hostname = osFunction.hostname() || (isConsumerMode ? UNKNOWN : false); if (!isIPAddressesEnabled) { // If IPAddresses setting is not enabled, set as false (for standalone) or "NA" (for consumer mode, to be used on Redis keys) diff --git a/src/utils/ip.js b/src/utils/ip.js index 3374f29f1..39aa9758c 100644 --- a/src/utils/ip.js +++ b/src/utils/ip.js @@ -12,10 +12,7 @@ The above copyright notice and this permission notice shall be included in all c 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. */ -'use strict'; - -var ip = exports; -var os = require('os'); +import os from 'os'; function _resolveFamily(family) { return typeof family === 'number' ? 'ipv' + family : family.toLowerCase(); @@ -79,7 +76,7 @@ function loopback(family) { // * 'private': the first private ip address of family. // * undefined: First address with `ipv4` or loopback address `127.0.0.1`. // -ip.address = function (name, family) { +export function address(name, family) { var interfaces = os.networkInterfaces(); var all; @@ -123,4 +120,4 @@ ip.address = function (name, family) { }).filter(Boolean); return !all.length ? loopback(family) : all[0]; -}; +}