launchdarkly-js-sdk-common 4.0.0 → 4.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. package/.circleci/config.yml +22 -0
  2. package/.eslintignore +4 -0
  3. package/.eslintrc.yaml +104 -0
  4. package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
  5. package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
  6. package/.github/pull_request_template.md +21 -0
  7. package/.ldrelease/config.yml +24 -0
  8. package/.prettierignore +1 -0
  9. package/.prettierrc +5 -0
  10. package/CHANGELOG.md +4 -0
  11. package/CONTRIBUTING.md +45 -0
  12. package/babel.config.js +18 -0
  13. package/docs/typedoc.js +11 -0
  14. package/jest.config.js +15 -0
  15. package/package.json +3 -29
  16. package/scripts/better-audit.sh +76 -0
  17. package/src/EventEmitter.js +60 -0
  18. package/src/EventProcessor.js +175 -0
  19. package/src/EventSender.js +87 -0
  20. package/src/EventSummarizer.js +84 -0
  21. package/src/Identity.js +26 -0
  22. package/src/InitializationState.js +83 -0
  23. package/src/PersistentFlagStore.js +50 -0
  24. package/src/PersistentStorage.js +81 -0
  25. package/src/Requestor.js +111 -0
  26. package/src/Stream.js +154 -0
  27. package/src/UserFilter.js +75 -0
  28. package/src/UserValidator.js +56 -0
  29. package/src/__tests__/.eslintrc.yaml +7 -0
  30. package/src/__tests__/EventProcessor-test.js +559 -0
  31. package/src/__tests__/EventSender-test.js +252 -0
  32. package/src/__tests__/EventSource-mock.js +61 -0
  33. package/src/__tests__/EventSummarizer-test.js +103 -0
  34. package/src/__tests__/LDClient-events-test.js +757 -0
  35. package/src/__tests__/LDClient-localstorage-test.js +179 -0
  36. package/src/__tests__/LDClient-streaming-test.js +683 -0
  37. package/src/__tests__/LDClient-test.js +761 -0
  38. package/src/__tests__/PersistentFlagStore-test.js +111 -0
  39. package/src/__tests__/Requestor-test.js +362 -0
  40. package/src/__tests__/Stream-test.js +299 -0
  41. package/src/__tests__/UserFilter-test.js +93 -0
  42. package/src/__tests__/UserValidator-test.js +57 -0
  43. package/src/__tests__/configuration-test.js +217 -0
  44. package/src/__tests__/diagnosticEvents-test.js +449 -0
  45. package/src/__tests__/loggers-test.js +149 -0
  46. package/src/__tests__/mockHttp.js +122 -0
  47. package/src/__tests__/promiseCoalescer-test.js +128 -0
  48. package/src/__tests__/stubPlatform.js +148 -0
  49. package/src/__tests__/testUtils.js +77 -0
  50. package/src/__tests__/utils-test.js +148 -0
  51. package/src/configuration.js +151 -0
  52. package/src/diagnosticEvents.js +269 -0
  53. package/src/errors.js +37 -0
  54. package/src/index.js +772 -0
  55. package/src/jest.setup.js +1 -0
  56. package/src/loggers.js +93 -0
  57. package/src/messages.js +217 -0
  58. package/src/promiseCoalescer.js +52 -0
  59. package/src/utils.js +214 -0
  60. package/test-types.ts +96 -0
  61. package/tsconfig.json +13 -0
  62. package/dist/ldclient-common.cjs.js +0 -2
  63. package/dist/ldclient-common.cjs.js.map +0 -1
  64. package/dist/ldclient-common.es.js +0 -2
  65. package/dist/ldclient-common.es.js.map +0 -1
  66. package/dist/ldclient-common.min.js +0 -2
  67. package/dist/ldclient-common.min.js.map +0 -1
package/src/Stream.js ADDED
@@ -0,0 +1,154 @@
1
+ const messages = require('./messages');
2
+ const { base64URLEncode, getLDHeaders, transformHeaders, objectHasOwnProperty } = require('./utils');
3
+
4
+ // The underlying event source implementation is abstracted via the platform object, which should
5
+ // have these three properties:
6
+ // eventSourceFactory(): a function that takes a URL and optional config object and returns an object
7
+ // with the same methods as the regular HTML5 EventSource object. The properties in the config
8
+ // object are those supported by the launchdarkly-eventsource package; browser EventSource
9
+ // implementations don't have any config options.
10
+ // eventSourceIsActive(): a function that takes an EventSource-compatible object and returns true if
11
+ // it is in an active state (connected or connecting).
12
+ // eventSourceAllowsReport: true if REPORT is supported.
13
+
14
+ // The read timeout for the stream is a fixed value that is set to be slightly longer than the expected
15
+ // interval between heartbeats from the LaunchDarkly streaming server. If this amount of time elapses
16
+ // with no new data, the connection will be cycled.
17
+ const streamReadTimeoutMillis = 5 * 60 * 1000; // 5 minutes
18
+
19
+ function Stream(platform, config, environment, diagnosticsAccumulator) {
20
+ const baseUrl = config.streamUrl;
21
+ const logger = config.logger;
22
+ const stream = {};
23
+ const evalUrlPrefix = baseUrl + '/eval/' + environment;
24
+ const useReport = config.useReport;
25
+ const withReasons = config.evaluationReasons;
26
+ const streamReconnectDelay = config.streamReconnectDelay;
27
+ const headers = getLDHeaders(platform, config);
28
+ let firstConnectionErrorLogged = false;
29
+ let es = null;
30
+ let reconnectTimeoutReference = null;
31
+ let connectionAttemptStartTime;
32
+ let user = null;
33
+ let hash = null;
34
+ let handlers = null;
35
+
36
+ stream.connect = function(newUser, newHash, newHandlers) {
37
+ user = newUser;
38
+ hash = newHash;
39
+ handlers = {};
40
+ for (const key in newHandlers || {}) {
41
+ handlers[key] = function(e) {
42
+ // Reset the state for logging the first connection error so that the first
43
+ // connection error following a successful connection will once again be logged.
44
+ // We will decorate *all* handlers to do this to keep this abstraction agnostic
45
+ // for different stream implementations.
46
+ firstConnectionErrorLogged = false;
47
+ logConnectionResult(true);
48
+ newHandlers[key] && newHandlers[key](e);
49
+ };
50
+ }
51
+ tryConnect();
52
+ };
53
+
54
+ stream.disconnect = function() {
55
+ clearTimeout(reconnectTimeoutReference);
56
+ reconnectTimeoutReference = null;
57
+ closeConnection();
58
+ };
59
+
60
+ stream.isConnected = function() {
61
+ return !!(es && platform.eventSourceIsActive && platform.eventSourceIsActive(es));
62
+ };
63
+
64
+ function handleError(err) {
65
+ if (!firstConnectionErrorLogged) {
66
+ logger.warn(messages.streamError(err, streamReconnectDelay));
67
+ firstConnectionErrorLogged = true;
68
+ }
69
+ logConnectionResult(false);
70
+ closeConnection();
71
+ tryConnect(streamReconnectDelay);
72
+ }
73
+
74
+ function tryConnect(delay) {
75
+ if (!reconnectTimeoutReference) {
76
+ if (delay) {
77
+ reconnectTimeoutReference = setTimeout(openConnection, delay);
78
+ } else {
79
+ openConnection();
80
+ }
81
+ }
82
+ }
83
+
84
+ function openConnection() {
85
+ reconnectTimeoutReference = null;
86
+ let url;
87
+ let query = '';
88
+ const options = { headers, readTimeoutMillis: streamReadTimeoutMillis };
89
+ if (platform.eventSourceFactory) {
90
+ if (hash !== null && hash !== undefined) {
91
+ query = 'h=' + hash;
92
+ }
93
+ if (useReport) {
94
+ if (platform.eventSourceAllowsReport) {
95
+ url = evalUrlPrefix;
96
+ options.method = 'REPORT';
97
+ options.headers['Content-Type'] = 'application/json';
98
+ options.body = JSON.stringify(user);
99
+ } else {
100
+ // if we can't do REPORT, fall back to the old ping-based stream
101
+ url = baseUrl + '/ping/' + environment;
102
+ query = '';
103
+ }
104
+ } else {
105
+ url = evalUrlPrefix + '/' + base64URLEncode(JSON.stringify(user));
106
+ }
107
+ options.headers = transformHeaders(options.headers, config);
108
+ if (withReasons) {
109
+ query = query + (query ? '&' : '') + 'withReasons=true';
110
+ }
111
+ url = url + (query ? '?' : '') + query;
112
+
113
+ closeConnection();
114
+ logger.info(messages.streamConnecting(url));
115
+ logConnectionStarted();
116
+
117
+ es = platform.eventSourceFactory(url, options);
118
+ for (const key in handlers) {
119
+ if (objectHasOwnProperty(handlers, key)) {
120
+ es.addEventListener(key, handlers[key]);
121
+ }
122
+ }
123
+
124
+ es.onerror = handleError;
125
+ }
126
+ }
127
+
128
+ function closeConnection() {
129
+ if (es) {
130
+ logger.info(messages.streamClosing());
131
+ es.close();
132
+ es = null;
133
+ }
134
+ }
135
+
136
+ function logConnectionStarted() {
137
+ connectionAttemptStartTime = new Date().getTime();
138
+ }
139
+
140
+ function logConnectionResult(success) {
141
+ if (connectionAttemptStartTime && diagnosticsAccumulator) {
142
+ diagnosticsAccumulator.recordStreamInit(
143
+ connectionAttemptStartTime,
144
+ !success,
145
+ new Date().getTime() - connectionAttemptStartTime
146
+ );
147
+ }
148
+ connectionAttemptStartTime = null;
149
+ }
150
+
151
+ return stream;
152
+ }
153
+
154
+ module.exports = Stream;
@@ -0,0 +1,75 @@
1
+ const utils = require('./utils');
2
+
3
+ /**
4
+ * The UserFilter object transforms user objects into objects suitable to be sent as JSON to
5
+ * the server, hiding any private user attributes.
6
+ *
7
+ * @param {Object} the LaunchDarkly client configuration object
8
+ **/
9
+ function UserFilter(config) {
10
+ const filter = {};
11
+ const allAttributesPrivate = config.allAttributesPrivate;
12
+ const privateAttributeNames = config.privateAttributeNames || [];
13
+ const ignoreAttrs = { key: true, custom: true, anonymous: true };
14
+ const allowedTopLevelAttrs = {
15
+ key: true,
16
+ secondary: true,
17
+ ip: true,
18
+ country: true,
19
+ email: true,
20
+ firstName: true,
21
+ lastName: true,
22
+ avatar: true,
23
+ name: true,
24
+ anonymous: true,
25
+ custom: true,
26
+ };
27
+
28
+ filter.filterUser = function(user) {
29
+ if (!user) {
30
+ return null;
31
+ }
32
+ const userPrivateAttrs = user.privateAttributeNames || [];
33
+
34
+ const isPrivateAttr = function(name) {
35
+ return (
36
+ !ignoreAttrs[name] &&
37
+ (allAttributesPrivate || userPrivateAttrs.indexOf(name) !== -1 || privateAttributeNames.indexOf(name) !== -1)
38
+ );
39
+ };
40
+ const filterAttrs = function(props, isAttributeAllowed) {
41
+ return Object.keys(props).reduce(
42
+ (acc, name) => {
43
+ const ret = acc;
44
+ if (isAttributeAllowed(name)) {
45
+ if (isPrivateAttr(name)) {
46
+ // add to hidden list
47
+ ret[1][name] = true;
48
+ } else {
49
+ ret[0][name] = props[name];
50
+ }
51
+ }
52
+ return ret;
53
+ },
54
+ [{}, {}]
55
+ );
56
+ };
57
+ const result = filterAttrs(user, key => allowedTopLevelAttrs[key]);
58
+ const filteredProps = result[0];
59
+ let removedAttrs = result[1];
60
+ if (user.custom) {
61
+ const customResult = filterAttrs(user.custom, () => true);
62
+ filteredProps.custom = customResult[0];
63
+ removedAttrs = utils.extend({}, removedAttrs, customResult[1]);
64
+ }
65
+ const removedAttrNames = Object.keys(removedAttrs);
66
+ if (removedAttrNames.length) {
67
+ removedAttrNames.sort();
68
+ filteredProps.privateAttrs = removedAttrNames;
69
+ }
70
+ return filteredProps;
71
+ };
72
+ return filter;
73
+ }
74
+
75
+ module.exports = UserFilter;
@@ -0,0 +1,56 @@
1
+ const { v1: uuidv1 } = require('uuid');
2
+
3
+ const errors = require('./errors');
4
+ const messages = require('./messages');
5
+ const utils = require('./utils');
6
+
7
+ // Transforms the user object if necessary to make sure it has a valid key.
8
+ // 1. If a key is present, but is not a string, change it to a string.
9
+ // 2. If no key is present, and "anonymous" is true, use a UUID as a key. This is cached in local
10
+ // storage if possible.
11
+ // 3. If there is no key (or no user object), return an error.
12
+
13
+ const ldUserIdKey = 'ld:$anonUserId';
14
+
15
+ function UserValidator(persistentStorage) {
16
+ function getCachedUserId() {
17
+ return persistentStorage.get(ldUserIdKey);
18
+ }
19
+
20
+ function setCachedUserId(id) {
21
+ return persistentStorage.set(ldUserIdKey, id);
22
+ }
23
+
24
+ const ret = {};
25
+
26
+ // Validates the user, returning a Promise that resolves to the validated user, or rejects if there is an error.
27
+ ret.validateUser = user => {
28
+ if (!user) {
29
+ return Promise.reject(new errors.LDInvalidUserError(messages.userNotSpecified()));
30
+ }
31
+
32
+ const userOut = utils.clone(user);
33
+ if (userOut.key !== null && userOut.key !== undefined) {
34
+ userOut.key = userOut.key.toString();
35
+ return Promise.resolve(userOut);
36
+ }
37
+ if (userOut.anonymous) {
38
+ return getCachedUserId().then(cachedId => {
39
+ if (cachedId) {
40
+ userOut.key = cachedId;
41
+ return userOut;
42
+ } else {
43
+ const id = uuidv1();
44
+ userOut.key = id;
45
+ return setCachedUserId(id).then(() => userOut);
46
+ }
47
+ });
48
+ } else {
49
+ return Promise.reject(new errors.LDInvalidUserError(messages.invalidUser()));
50
+ }
51
+ };
52
+
53
+ return ret;
54
+ }
55
+
56
+ module.exports = UserValidator;
@@ -0,0 +1,7 @@
1
+ ---
2
+ globals:
3
+ VERSION: true
4
+ sinon: true
5
+ expect: true
6
+ requestor: true
7
+ client: true