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.
- package/.circleci/config.yml +22 -0
- package/.eslintignore +4 -0
- package/.eslintrc.yaml +104 -0
- package/.github/ISSUE_TEMPLATE/bug_report.md +37 -0
- package/.github/ISSUE_TEMPLATE/feature_request.md +20 -0
- package/.github/pull_request_template.md +21 -0
- package/.ldrelease/config.yml +24 -0
- package/.prettierignore +1 -0
- package/.prettierrc +5 -0
- package/CHANGELOG.md +4 -0
- package/CONTRIBUTING.md +45 -0
- package/babel.config.js +18 -0
- package/docs/typedoc.js +11 -0
- package/jest.config.js +15 -0
- package/package.json +3 -29
- package/scripts/better-audit.sh +76 -0
- package/src/EventEmitter.js +60 -0
- package/src/EventProcessor.js +175 -0
- package/src/EventSender.js +87 -0
- package/src/EventSummarizer.js +84 -0
- package/src/Identity.js +26 -0
- package/src/InitializationState.js +83 -0
- package/src/PersistentFlagStore.js +50 -0
- package/src/PersistentStorage.js +81 -0
- package/src/Requestor.js +111 -0
- package/src/Stream.js +154 -0
- package/src/UserFilter.js +75 -0
- package/src/UserValidator.js +56 -0
- package/src/__tests__/.eslintrc.yaml +7 -0
- package/src/__tests__/EventProcessor-test.js +559 -0
- package/src/__tests__/EventSender-test.js +252 -0
- package/src/__tests__/EventSource-mock.js +61 -0
- package/src/__tests__/EventSummarizer-test.js +103 -0
- package/src/__tests__/LDClient-events-test.js +757 -0
- package/src/__tests__/LDClient-localstorage-test.js +179 -0
- package/src/__tests__/LDClient-streaming-test.js +683 -0
- package/src/__tests__/LDClient-test.js +761 -0
- package/src/__tests__/PersistentFlagStore-test.js +111 -0
- package/src/__tests__/Requestor-test.js +362 -0
- package/src/__tests__/Stream-test.js +299 -0
- package/src/__tests__/UserFilter-test.js +93 -0
- package/src/__tests__/UserValidator-test.js +57 -0
- package/src/__tests__/configuration-test.js +217 -0
- package/src/__tests__/diagnosticEvents-test.js +449 -0
- package/src/__tests__/loggers-test.js +149 -0
- package/src/__tests__/mockHttp.js +122 -0
- package/src/__tests__/promiseCoalescer-test.js +128 -0
- package/src/__tests__/stubPlatform.js +148 -0
- package/src/__tests__/testUtils.js +77 -0
- package/src/__tests__/utils-test.js +148 -0
- package/src/configuration.js +151 -0
- package/src/diagnosticEvents.js +269 -0
- package/src/errors.js +37 -0
- package/src/index.js +772 -0
- package/src/jest.setup.js +1 -0
- package/src/loggers.js +93 -0
- package/src/messages.js +217 -0
- package/src/promiseCoalescer.js +52 -0
- package/src/utils.js +214 -0
- package/test-types.ts +96 -0
- package/tsconfig.json +13 -0
- package/dist/ldclient-common.cjs.js +0 -2
- package/dist/ldclient-common.cjs.js.map +0 -1
- package/dist/ldclient-common.es.js +0 -2
- package/dist/ldclient-common.es.js.map +0 -1
- package/dist/ldclient-common.min.js +0 -2
- package/dist/ldclient-common.min.js.map +0 -1
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
const EventSender = require('./EventSender');
|
|
2
|
+
const EventSummarizer = require('./EventSummarizer');
|
|
3
|
+
const UserFilter = require('./UserFilter');
|
|
4
|
+
const errors = require('./errors');
|
|
5
|
+
const messages = require('./messages');
|
|
6
|
+
const utils = require('./utils');
|
|
7
|
+
|
|
8
|
+
function EventProcessor(
|
|
9
|
+
platform,
|
|
10
|
+
options,
|
|
11
|
+
environmentId,
|
|
12
|
+
diagnosticsAccumulator = null,
|
|
13
|
+
emitter = null,
|
|
14
|
+
sender = null
|
|
15
|
+
) {
|
|
16
|
+
const processor = {};
|
|
17
|
+
const eventSender = sender || EventSender(platform, environmentId, options);
|
|
18
|
+
const mainEventsUrl = options.eventsUrl + '/events/bulk/' + environmentId;
|
|
19
|
+
const summarizer = EventSummarizer();
|
|
20
|
+
const userFilter = UserFilter(options);
|
|
21
|
+
const inlineUsers = options.inlineUsersInEvents;
|
|
22
|
+
const samplingInterval = options.samplingInterval;
|
|
23
|
+
const eventCapacity = options.eventCapacity;
|
|
24
|
+
const flushInterval = options.flushInterval;
|
|
25
|
+
const logger = options.logger;
|
|
26
|
+
let queue = [];
|
|
27
|
+
let lastKnownPastTime = 0;
|
|
28
|
+
let disabled = false;
|
|
29
|
+
let exceededCapacity = false;
|
|
30
|
+
let flushTimer;
|
|
31
|
+
|
|
32
|
+
function shouldSampleEvent() {
|
|
33
|
+
return samplingInterval === 0 || Math.floor(Math.random() * samplingInterval) === 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function shouldDebugEvent(e) {
|
|
37
|
+
if (e.debugEventsUntilDate) {
|
|
38
|
+
// The "last known past time" comes from the last HTTP response we got from the server.
|
|
39
|
+
// In case the client's time is set wrong, at least we know that any expiration date
|
|
40
|
+
// earlier than that point is definitely in the past. If there's any discrepancy, we
|
|
41
|
+
// want to err on the side of cutting off event debugging sooner.
|
|
42
|
+
return e.debugEventsUntilDate > lastKnownPastTime && e.debugEventsUntilDate > new Date().getTime();
|
|
43
|
+
}
|
|
44
|
+
return false;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Transform an event from its internal format to the format we use when sending a payload.
|
|
48
|
+
function makeOutputEvent(e) {
|
|
49
|
+
const ret = utils.extend({}, e);
|
|
50
|
+
if (e.kind === 'alias') {
|
|
51
|
+
// alias events do not require any transformation
|
|
52
|
+
return ret;
|
|
53
|
+
}
|
|
54
|
+
if (inlineUsers || e.kind === 'identify') {
|
|
55
|
+
// identify events always have an inline user
|
|
56
|
+
ret.user = userFilter.filterUser(e.user);
|
|
57
|
+
} else {
|
|
58
|
+
ret.userKey = e.user.key;
|
|
59
|
+
delete ret['user'];
|
|
60
|
+
}
|
|
61
|
+
if (e.kind === 'feature') {
|
|
62
|
+
delete ret['trackEvents'];
|
|
63
|
+
delete ret['debugEventsUntilDate'];
|
|
64
|
+
}
|
|
65
|
+
return ret;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function addToOutbox(event) {
|
|
69
|
+
if (queue.length < eventCapacity) {
|
|
70
|
+
queue.push(event);
|
|
71
|
+
exceededCapacity = false;
|
|
72
|
+
} else {
|
|
73
|
+
if (!exceededCapacity) {
|
|
74
|
+
exceededCapacity = true;
|
|
75
|
+
logger.warn(messages.eventCapacityExceeded());
|
|
76
|
+
}
|
|
77
|
+
if (diagnosticsAccumulator) {
|
|
78
|
+
// For diagnostic events, we track how many times we had to drop an event due to exceeding the capacity.
|
|
79
|
+
diagnosticsAccumulator.incrementDroppedEvents();
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
processor.enqueue = function(event) {
|
|
85
|
+
if (disabled) {
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
let addFullEvent = false;
|
|
89
|
+
let addDebugEvent = false;
|
|
90
|
+
|
|
91
|
+
// Add event to the summary counters if appropriate
|
|
92
|
+
summarizer.summarizeEvent(event);
|
|
93
|
+
|
|
94
|
+
// Decide whether to add the event to the payload. Feature events may be added twice, once for
|
|
95
|
+
// the event (if tracked) and once for debugging.
|
|
96
|
+
if (event.kind === 'feature') {
|
|
97
|
+
if (shouldSampleEvent()) {
|
|
98
|
+
addFullEvent = !!event.trackEvents;
|
|
99
|
+
addDebugEvent = shouldDebugEvent(event);
|
|
100
|
+
}
|
|
101
|
+
} else {
|
|
102
|
+
addFullEvent = shouldSampleEvent();
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (addFullEvent) {
|
|
106
|
+
addToOutbox(makeOutputEvent(event));
|
|
107
|
+
}
|
|
108
|
+
if (addDebugEvent) {
|
|
109
|
+
const debugEvent = utils.extend({}, event, { kind: 'debug' });
|
|
110
|
+
debugEvent.user = userFilter.filterUser(debugEvent.user);
|
|
111
|
+
delete debugEvent['trackEvents'];
|
|
112
|
+
delete debugEvent['debugEventsUntilDate'];
|
|
113
|
+
addToOutbox(debugEvent);
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
processor.flush = function() {
|
|
118
|
+
if (disabled) {
|
|
119
|
+
return Promise.resolve();
|
|
120
|
+
}
|
|
121
|
+
const eventsToSend = queue;
|
|
122
|
+
const summary = summarizer.getSummary();
|
|
123
|
+
summarizer.clearSummary();
|
|
124
|
+
if (summary) {
|
|
125
|
+
summary.kind = 'summary';
|
|
126
|
+
eventsToSend.push(summary);
|
|
127
|
+
}
|
|
128
|
+
if (diagnosticsAccumulator) {
|
|
129
|
+
// For diagnostic events, we record how many events were in the queue at the last flush (since "how
|
|
130
|
+
// many events happened to be in the queue at the moment we decided to send a diagnostic event" would
|
|
131
|
+
// not be a very useful statistic).
|
|
132
|
+
diagnosticsAccumulator.setEventsInLastBatch(eventsToSend.length);
|
|
133
|
+
}
|
|
134
|
+
if (eventsToSend.length === 0) {
|
|
135
|
+
return Promise.resolve();
|
|
136
|
+
}
|
|
137
|
+
queue = [];
|
|
138
|
+
logger.debug(messages.debugPostingEvents(eventsToSend.length));
|
|
139
|
+
return eventSender.sendEvents(eventsToSend, mainEventsUrl).then(responseInfo => {
|
|
140
|
+
if (responseInfo) {
|
|
141
|
+
if (responseInfo.serverTime) {
|
|
142
|
+
lastKnownPastTime = responseInfo.serverTime;
|
|
143
|
+
}
|
|
144
|
+
if (!errors.isHttpErrorRecoverable(responseInfo.status)) {
|
|
145
|
+
disabled = true;
|
|
146
|
+
}
|
|
147
|
+
if (responseInfo.status >= 400) {
|
|
148
|
+
utils.onNextTick(() => {
|
|
149
|
+
emitter.maybeReportError(
|
|
150
|
+
new errors.LDUnexpectedResponseError(
|
|
151
|
+
messages.httpErrorMessage(responseInfo.status, 'event posting', 'some events were dropped')
|
|
152
|
+
)
|
|
153
|
+
);
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
|
|
160
|
+
processor.start = function() {
|
|
161
|
+
const flushTick = () => {
|
|
162
|
+
processor.flush();
|
|
163
|
+
flushTimer = setTimeout(flushTick, flushInterval);
|
|
164
|
+
};
|
|
165
|
+
flushTimer = setTimeout(flushTick, flushInterval);
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
processor.stop = function() {
|
|
169
|
+
clearTimeout(flushTimer);
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
return processor;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
module.exports = EventProcessor;
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
const errors = require('./errors');
|
|
2
|
+
const utils = require('./utils');
|
|
3
|
+
const { v1: uuidv1 } = require('uuid');
|
|
4
|
+
|
|
5
|
+
const MAX_URL_LENGTH = 2000;
|
|
6
|
+
|
|
7
|
+
function EventSender(platform, environmentId, options) {
|
|
8
|
+
const imageUrlPath = '/a/' + environmentId + '.gif';
|
|
9
|
+
const baseHeaders = utils.extend({ 'Content-Type': 'application/json' }, utils.getLDHeaders(platform, options));
|
|
10
|
+
const httpFallbackPing = platform.httpFallbackPing; // this will be set for us if we're in the browser SDK
|
|
11
|
+
const sender = {};
|
|
12
|
+
|
|
13
|
+
function getResponseInfo(result) {
|
|
14
|
+
const ret = { status: result.status };
|
|
15
|
+
const dateStr = result.header('date');
|
|
16
|
+
if (dateStr) {
|
|
17
|
+
const time = Date.parse(dateStr);
|
|
18
|
+
if (time) {
|
|
19
|
+
ret.serverTime = time;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return ret;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
sender.sendChunk = (events, url, isDiagnostic, usePost) => {
|
|
26
|
+
const jsonBody = JSON.stringify(events);
|
|
27
|
+
const payloadId = isDiagnostic ? null : uuidv1();
|
|
28
|
+
|
|
29
|
+
function doPostRequest(canRetry) {
|
|
30
|
+
const headers = isDiagnostic
|
|
31
|
+
? baseHeaders
|
|
32
|
+
: utils.extend({}, baseHeaders, {
|
|
33
|
+
'X-LaunchDarkly-Event-Schema': '3',
|
|
34
|
+
'X-LaunchDarkly-Payload-ID': payloadId,
|
|
35
|
+
});
|
|
36
|
+
return platform
|
|
37
|
+
.httpRequest('POST', url, utils.transformHeaders(headers, options), jsonBody)
|
|
38
|
+
.promise.then(result => {
|
|
39
|
+
if (!result) {
|
|
40
|
+
// This was a response from a fire-and-forget request, so we won't have a status.
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (result.status >= 400 && errors.isHttpErrorRecoverable(result.status) && canRetry) {
|
|
44
|
+
return doPostRequest(false);
|
|
45
|
+
} else {
|
|
46
|
+
return getResponseInfo(result);
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
.catch(() => {
|
|
50
|
+
if (canRetry) {
|
|
51
|
+
return doPostRequest(false);
|
|
52
|
+
}
|
|
53
|
+
return Promise.reject();
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (usePost) {
|
|
58
|
+
return doPostRequest(true).catch(() => {});
|
|
59
|
+
} else {
|
|
60
|
+
httpFallbackPing && httpFallbackPing(url + imageUrlPath + '?d=' + utils.base64URLEncode(jsonBody));
|
|
61
|
+
return Promise.resolve(); // we don't wait for this request to complete, it's just a one-way ping
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
sender.sendEvents = function(events, url, isDiagnostic) {
|
|
66
|
+
if (!platform.httpRequest) {
|
|
67
|
+
return Promise.resolve();
|
|
68
|
+
}
|
|
69
|
+
const canPost = platform.httpAllowsPost();
|
|
70
|
+
let chunks;
|
|
71
|
+
if (canPost) {
|
|
72
|
+
// no need to break up events into chunks if we can send a POST
|
|
73
|
+
chunks = [events];
|
|
74
|
+
} else {
|
|
75
|
+
chunks = utils.chunkUserEventsForUrl(MAX_URL_LENGTH - url.length, events);
|
|
76
|
+
}
|
|
77
|
+
const results = [];
|
|
78
|
+
for (let i = 0; i < chunks.length; i++) {
|
|
79
|
+
results.push(sender.sendChunk(chunks[i], url, isDiagnostic, canPost));
|
|
80
|
+
}
|
|
81
|
+
return Promise.all(results);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
return sender;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
module.exports = EventSender;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
function EventSummarizer() {
|
|
2
|
+
const es = {};
|
|
3
|
+
|
|
4
|
+
let startDate = 0,
|
|
5
|
+
endDate = 0,
|
|
6
|
+
counters = {};
|
|
7
|
+
|
|
8
|
+
es.summarizeEvent = function(event) {
|
|
9
|
+
if (event.kind === 'feature') {
|
|
10
|
+
const counterKey =
|
|
11
|
+
event.key +
|
|
12
|
+
':' +
|
|
13
|
+
(event.variation !== null && event.variation !== undefined ? event.variation : '') +
|
|
14
|
+
':' +
|
|
15
|
+
(event.version !== null && event.version !== undefined ? event.version : '');
|
|
16
|
+
const counterVal = counters[counterKey];
|
|
17
|
+
if (counterVal) {
|
|
18
|
+
counterVal.count = counterVal.count + 1;
|
|
19
|
+
} else {
|
|
20
|
+
counters[counterKey] = {
|
|
21
|
+
count: 1,
|
|
22
|
+
key: event.key,
|
|
23
|
+
variation: event.variation,
|
|
24
|
+
version: event.version,
|
|
25
|
+
value: event.value,
|
|
26
|
+
default: event.default,
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
if (startDate === 0 || event.creationDate < startDate) {
|
|
30
|
+
startDate = event.creationDate;
|
|
31
|
+
}
|
|
32
|
+
if (event.creationDate > endDate) {
|
|
33
|
+
endDate = event.creationDate;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
es.getSummary = function() {
|
|
39
|
+
const flagsOut = {};
|
|
40
|
+
let empty = true;
|
|
41
|
+
for (const i in counters) {
|
|
42
|
+
const c = counters[i];
|
|
43
|
+
let flag = flagsOut[c.key];
|
|
44
|
+
if (!flag) {
|
|
45
|
+
flag = {
|
|
46
|
+
default: c.default,
|
|
47
|
+
counters: [],
|
|
48
|
+
};
|
|
49
|
+
flagsOut[c.key] = flag;
|
|
50
|
+
}
|
|
51
|
+
const counterOut = {
|
|
52
|
+
value: c.value,
|
|
53
|
+
count: c.count,
|
|
54
|
+
};
|
|
55
|
+
if (c.variation !== undefined && c.variation !== null) {
|
|
56
|
+
counterOut.variation = c.variation;
|
|
57
|
+
}
|
|
58
|
+
if (c.version) {
|
|
59
|
+
counterOut.version = c.version;
|
|
60
|
+
} else {
|
|
61
|
+
counterOut.unknown = true;
|
|
62
|
+
}
|
|
63
|
+
flag.counters.push(counterOut);
|
|
64
|
+
empty = false;
|
|
65
|
+
}
|
|
66
|
+
return empty
|
|
67
|
+
? null
|
|
68
|
+
: {
|
|
69
|
+
startDate,
|
|
70
|
+
endDate,
|
|
71
|
+
features: flagsOut,
|
|
72
|
+
};
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
es.clearSummary = function() {
|
|
76
|
+
startDate = 0;
|
|
77
|
+
endDate = 0;
|
|
78
|
+
counters = {};
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
return es;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
module.exports = EventSummarizer;
|
package/src/Identity.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
const utils = require('./utils');
|
|
2
|
+
|
|
3
|
+
function Identity(initialUser, onChange) {
|
|
4
|
+
const ident = {};
|
|
5
|
+
let user;
|
|
6
|
+
|
|
7
|
+
ident.setUser = function(u) {
|
|
8
|
+
const previousUser = user && utils.clone(user);
|
|
9
|
+
user = utils.sanitizeUser(u);
|
|
10
|
+
if (user && onChange) {
|
|
11
|
+
onChange(utils.clone(user), previousUser);
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
ident.getUser = function() {
|
|
16
|
+
return user ? utils.clone(user) : null;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
if (initialUser) {
|
|
20
|
+
ident.setUser(initialUser);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
return ident;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
module.exports = Identity;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// This file provides an abstraction of the client's startup state.
|
|
2
|
+
//
|
|
3
|
+
// Startup can either succeed or fail exactly once; calling signalSuccess() or signalFailure()
|
|
4
|
+
// after that point has no effect.
|
|
5
|
+
//
|
|
6
|
+
// On success, we fire both an "initialized" event and a "ready" event. Both the waitForInitialization()
|
|
7
|
+
// promise and the waitUntilReady() promise are resolved in this case.
|
|
8
|
+
//
|
|
9
|
+
// On failure, we fire both a "failed" event (with the error as a parameter) and a "ready" event.
|
|
10
|
+
// The waitForInitialization() promise is rejected, but the waitUntilReady() promise is resolved.
|
|
11
|
+
//
|
|
12
|
+
// To complicate things, we must *not* create the waitForInitialization() promise unless it is
|
|
13
|
+
// requested, because otherwise failures would cause an *unhandled* rejection which can be a
|
|
14
|
+
// serious problem in some environments. So we use a somewhat roundabout system for tracking the
|
|
15
|
+
// initialization state and lazily creating this promise.
|
|
16
|
+
|
|
17
|
+
const readyEvent = 'ready',
|
|
18
|
+
successEvent = 'initialized',
|
|
19
|
+
failureEvent = 'failed';
|
|
20
|
+
|
|
21
|
+
function InitializationStateTracker(eventEmitter) {
|
|
22
|
+
let succeeded = false,
|
|
23
|
+
failed = false,
|
|
24
|
+
failureValue = null,
|
|
25
|
+
initializationPromise = null;
|
|
26
|
+
|
|
27
|
+
const readyPromise = new Promise(resolve => {
|
|
28
|
+
const onReady = () => {
|
|
29
|
+
eventEmitter.off(readyEvent, onReady); // we can't use "once" because it's not available on some JS platforms
|
|
30
|
+
resolve();
|
|
31
|
+
};
|
|
32
|
+
eventEmitter.on(readyEvent, onReady);
|
|
33
|
+
}).catch(() => {}); // this Promise should never be rejected, but the catch handler is a safety measure
|
|
34
|
+
|
|
35
|
+
return {
|
|
36
|
+
getInitializationPromise: () => {
|
|
37
|
+
if (initializationPromise) {
|
|
38
|
+
return initializationPromise;
|
|
39
|
+
}
|
|
40
|
+
if (succeeded) {
|
|
41
|
+
return Promise.resolve();
|
|
42
|
+
}
|
|
43
|
+
if (failed) {
|
|
44
|
+
return Promise.reject(failureValue);
|
|
45
|
+
}
|
|
46
|
+
initializationPromise = new Promise((resolve, reject) => {
|
|
47
|
+
const onSuccess = () => {
|
|
48
|
+
eventEmitter.off(successEvent, onSuccess);
|
|
49
|
+
resolve();
|
|
50
|
+
};
|
|
51
|
+
const onFailure = err => {
|
|
52
|
+
eventEmitter.off(failureEvent, onFailure);
|
|
53
|
+
reject(err);
|
|
54
|
+
};
|
|
55
|
+
eventEmitter.on(successEvent, onSuccess);
|
|
56
|
+
eventEmitter.on(failureEvent, onFailure);
|
|
57
|
+
});
|
|
58
|
+
return initializationPromise;
|
|
59
|
+
},
|
|
60
|
+
|
|
61
|
+
getReadyPromise: () => readyPromise,
|
|
62
|
+
|
|
63
|
+
signalSuccess: () => {
|
|
64
|
+
if (!succeeded && !failed) {
|
|
65
|
+
succeeded = true;
|
|
66
|
+
eventEmitter.emit(successEvent);
|
|
67
|
+
eventEmitter.emit(readyEvent);
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
|
|
71
|
+
signalFailure: err => {
|
|
72
|
+
if (!succeeded && !failed) {
|
|
73
|
+
failed = true;
|
|
74
|
+
failureValue = err;
|
|
75
|
+
eventEmitter.emit(failureEvent, err);
|
|
76
|
+
eventEmitter.emit(readyEvent);
|
|
77
|
+
}
|
|
78
|
+
eventEmitter.maybeReportError(err); // the "error" event can be emitted more than once, unlike the others
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
module.exports = InitializationStateTracker;
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
const utils = require('./utils');
|
|
2
|
+
|
|
3
|
+
function PersistentFlagStore(storage, environment, hash, ident) {
|
|
4
|
+
const store = {};
|
|
5
|
+
|
|
6
|
+
function getFlagsKey() {
|
|
7
|
+
let key = '';
|
|
8
|
+
const user = ident.getUser();
|
|
9
|
+
if (user) {
|
|
10
|
+
key = hash || utils.btoa(JSON.stringify(user));
|
|
11
|
+
}
|
|
12
|
+
return 'ld:' + environment + ':' + key;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
// Returns a Promise which will be resolved with a parsed JSON value if a stored value was available,
|
|
16
|
+
// or resolved with null if there was no value or if storage was not available.
|
|
17
|
+
store.loadFlags = () =>
|
|
18
|
+
storage.get(getFlagsKey()).then(dataStr => {
|
|
19
|
+
if (dataStr === null || dataStr === undefined) {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
try {
|
|
23
|
+
let data = JSON.parse(dataStr);
|
|
24
|
+
if (data) {
|
|
25
|
+
const schema = data.$schema;
|
|
26
|
+
if (schema === undefined || schema < 1) {
|
|
27
|
+
data = utils.transformValuesToVersionedValues(data);
|
|
28
|
+
} else {
|
|
29
|
+
delete data['$schema'];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return data;
|
|
33
|
+
} catch (ex) {
|
|
34
|
+
return store.clearFlags().then(() => null);
|
|
35
|
+
}
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Resolves with true if successful, or false if storage is unavailable. Never rejects.
|
|
39
|
+
store.saveFlags = flags => {
|
|
40
|
+
const data = utils.extend({}, flags, { $schema: 1 });
|
|
41
|
+
return storage.set(getFlagsKey(), JSON.stringify(data));
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Resolves with true if successful, or false if storage is unavailable. Never rejects.
|
|
45
|
+
store.clearFlags = () => storage.clear(getFlagsKey());
|
|
46
|
+
|
|
47
|
+
return store;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
module.exports = PersistentFlagStore;
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const messages = require('./messages');
|
|
2
|
+
|
|
3
|
+
// The localStorageProvider is provided by the platform object. It should have the following
|
|
4
|
+
// methods, each of which should return a Promise:
|
|
5
|
+
// - get(key): Gets the string value, if any, for the given key
|
|
6
|
+
// - set(key, value): Stores a string value for the given key
|
|
7
|
+
// - remove(key): Removes the given key
|
|
8
|
+
//
|
|
9
|
+
// Storage is just a light wrapper of the localStorageProvider, adding error handling and
|
|
10
|
+
// ensuring that we don't call it if it's unavailable. The get method will simply resolve
|
|
11
|
+
// with an undefined value if there is an error or if there is no localStorageProvider.
|
|
12
|
+
// None of the promises returned by Storage will ever be rejected.
|
|
13
|
+
//
|
|
14
|
+
// It is always possible that the underlying platform storage mechanism might fail or be
|
|
15
|
+
// disabled. If so, it's likely that it will keep failing, so we will only log one warning
|
|
16
|
+
// instead of repetitive warnings.
|
|
17
|
+
function PersistentStorage(localStorageProvider, logger) {
|
|
18
|
+
const storage = {};
|
|
19
|
+
let loggedError = false;
|
|
20
|
+
|
|
21
|
+
const logError = err => {
|
|
22
|
+
if (!loggedError) {
|
|
23
|
+
loggedError = true;
|
|
24
|
+
logger.warn(messages.localStorageUnavailable(err));
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
storage.isEnabled = () => !!localStorageProvider;
|
|
29
|
+
|
|
30
|
+
// Resolves with a value, or undefined if storage is unavailable. Never rejects.
|
|
31
|
+
storage.get = key =>
|
|
32
|
+
new Promise(resolve => {
|
|
33
|
+
if (!localStorageProvider) {
|
|
34
|
+
resolve(undefined);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
localStorageProvider
|
|
38
|
+
.get(key)
|
|
39
|
+
.then(resolve)
|
|
40
|
+
.catch(err => {
|
|
41
|
+
logError(err);
|
|
42
|
+
resolve(undefined);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// Resolves with true if successful, or false if storage is unavailable. Never rejects.
|
|
47
|
+
storage.set = (key, value) =>
|
|
48
|
+
new Promise(resolve => {
|
|
49
|
+
if (!localStorageProvider) {
|
|
50
|
+
resolve(false);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
localStorageProvider
|
|
54
|
+
.set(key, value)
|
|
55
|
+
.then(() => resolve(true))
|
|
56
|
+
.catch(err => {
|
|
57
|
+
logError(err);
|
|
58
|
+
resolve(false);
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
// Resolves with true if successful, or false if storage is unavailable. Never rejects.
|
|
63
|
+
storage.clear = key =>
|
|
64
|
+
new Promise(resolve => {
|
|
65
|
+
if (!localStorageProvider) {
|
|
66
|
+
resolve(false);
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
localStorageProvider
|
|
70
|
+
.clear(key)
|
|
71
|
+
.then(() => resolve(true))
|
|
72
|
+
.catch(err => {
|
|
73
|
+
logError(err);
|
|
74
|
+
resolve(false);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
return storage;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
module.exports = PersistentStorage;
|
package/src/Requestor.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
const utils = require('./utils');
|
|
2
|
+
const errors = require('./errors');
|
|
3
|
+
const messages = require('./messages');
|
|
4
|
+
const promiseCoalescer = require('./promiseCoalescer');
|
|
5
|
+
|
|
6
|
+
const jsonContentType = 'application/json';
|
|
7
|
+
|
|
8
|
+
function getResponseError(result) {
|
|
9
|
+
if (result.status === 404) {
|
|
10
|
+
return new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound());
|
|
11
|
+
} else {
|
|
12
|
+
return new errors.LDFlagFetchError(messages.errorFetchingFlags(result.statusText || String(result.status)));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function Requestor(platform, options, environment) {
|
|
17
|
+
const baseUrl = options.baseUrl;
|
|
18
|
+
const useReport = options.useReport;
|
|
19
|
+
const withReasons = options.evaluationReasons;
|
|
20
|
+
const logger = options.logger;
|
|
21
|
+
|
|
22
|
+
const requestor = {};
|
|
23
|
+
|
|
24
|
+
const activeRequests = {}; // map of URLs to promiseCoalescers
|
|
25
|
+
|
|
26
|
+
function fetchJSON(endpoint, body) {
|
|
27
|
+
if (!platform.httpRequest) {
|
|
28
|
+
return new Promise((resolve, reject) => {
|
|
29
|
+
reject(new errors.LDFlagFetchError(messages.httpUnavailable()));
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const method = body ? 'REPORT' : 'GET';
|
|
34
|
+
const headers = utils.getLDHeaders(platform, options);
|
|
35
|
+
if (body) {
|
|
36
|
+
headers['Content-Type'] = jsonContentType;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
let coalescer = activeRequests[endpoint];
|
|
40
|
+
if (!coalescer) {
|
|
41
|
+
coalescer = promiseCoalescer(() => {
|
|
42
|
+
// this will be called once there are no more active requests for the same endpoint
|
|
43
|
+
delete activeRequests[endpoint];
|
|
44
|
+
});
|
|
45
|
+
activeRequests[endpoint] = coalescer;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const req = platform.httpRequest(method, endpoint, utils.transformHeaders(headers, options), body);
|
|
49
|
+
const p = req.promise.then(
|
|
50
|
+
result => {
|
|
51
|
+
if (result.status === 200) {
|
|
52
|
+
// We're using substring here because using startsWith would require a polyfill in IE.
|
|
53
|
+
if (
|
|
54
|
+
result.header('content-type') &&
|
|
55
|
+
result.header('content-type').substring(0, jsonContentType.length) === jsonContentType
|
|
56
|
+
) {
|
|
57
|
+
return JSON.parse(result.body);
|
|
58
|
+
} else {
|
|
59
|
+
const message = messages.invalidContentType(result.header('content-type') || '');
|
|
60
|
+
return Promise.reject(new errors.LDFlagFetchError(message));
|
|
61
|
+
}
|
|
62
|
+
} else {
|
|
63
|
+
return Promise.reject(getResponseError(result));
|
|
64
|
+
}
|
|
65
|
+
},
|
|
66
|
+
e => Promise.reject(new errors.LDFlagFetchError(messages.networkError(e)))
|
|
67
|
+
);
|
|
68
|
+
coalescer.addPromise(p, () => {
|
|
69
|
+
// this will be called if another request for the same endpoint supersedes this one
|
|
70
|
+
req.cancel && req.cancel();
|
|
71
|
+
});
|
|
72
|
+
return coalescer.resultPromise;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Performs a GET request to an arbitrary path under baseUrl. Returns a Promise which will resolve
|
|
76
|
+
// with the parsed JSON response, or will be rejected if the request failed.
|
|
77
|
+
requestor.fetchJSON = function(path) {
|
|
78
|
+
return fetchJSON(baseUrl + path, null);
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Requests the current state of all flags for the given user from LaunchDarkly. Returns a Promise
|
|
82
|
+
// which will resolve with the parsed JSON response, or will be rejected if the request failed.
|
|
83
|
+
requestor.fetchFlagSettings = function(user, hash) {
|
|
84
|
+
let data;
|
|
85
|
+
let endpoint;
|
|
86
|
+
let query = '';
|
|
87
|
+
let body;
|
|
88
|
+
|
|
89
|
+
if (useReport) {
|
|
90
|
+
endpoint = [baseUrl, '/sdk/evalx/', environment, '/user'].join('');
|
|
91
|
+
body = JSON.stringify(user);
|
|
92
|
+
} else {
|
|
93
|
+
data = utils.base64URLEncode(JSON.stringify(user));
|
|
94
|
+
endpoint = [baseUrl, '/sdk/evalx/', environment, '/users/', data].join('');
|
|
95
|
+
}
|
|
96
|
+
if (hash) {
|
|
97
|
+
query = 'h=' + hash;
|
|
98
|
+
}
|
|
99
|
+
if (withReasons) {
|
|
100
|
+
query = query + (query ? '&' : '') + 'withReasons=true';
|
|
101
|
+
}
|
|
102
|
+
endpoint = endpoint + (query ? '?' : '') + query;
|
|
103
|
+
logger.debug(messages.debugPolling(endpoint));
|
|
104
|
+
|
|
105
|
+
return fetchJSON(endpoint, body);
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
return requestor;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
module.exports = Requestor;
|