Skip to content

Commit 6b6f186

Browse files
prepare 3.2.3 release (#17)
1 parent bd9cd93 commit 6b6f186

File tree

5 files changed

+221
-122
lines changed

5 files changed

+221
-122
lines changed

src/EventEmitter.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,12 @@ export default function EventEmitter(logger) {
2727
if (!events[event]) {
2828
return;
2929
}
30-
for (let i = 0; i < events[event].length; i++) {
31-
events[event][i].handler.apply(events[event][i].context, Array.prototype.slice.call(arguments, 1));
30+
// Copy the list of handlers before iterating, in case any handler adds or removes another handler.
31+
// Any such changes should not affect what we do here-- we want to notify every handler that existed
32+
// at the moment that the event was fired.
33+
const copiedHandlers = events[event].slice(0);
34+
for (let i = 0; i < copiedHandlers.length; i++) {
35+
copiedHandlers[i].handler.apply(copiedHandlers[i].context, Array.prototype.slice.call(arguments, 1));
3236
}
3337
};
3438

src/InitializationState.js

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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;

src/__tests__/LDClient-test.js

Lines changed: 89 additions & 89 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import * as LDClient from '../index';
2-
import * as errors from '../errors';
32
import * as messages from '../messages';
43
import * as utils from '../utils';
54

@@ -43,7 +42,7 @@ describe('LDClient', () => {
4342
});
4443

4544
describe('initialization', () => {
46-
it('should trigger the ready event', async () => {
45+
it('triggers "ready" event', async () => {
4746
await withServers(async baseConfig => {
4847
await withClient(user, baseConfig, async client => {
4948
const gotReady = eventSink(client, 'ready');
@@ -54,7 +53,7 @@ describe('LDClient', () => {
5453
});
5554
});
5655

57-
it('should trigger the initialized event', async () => {
56+
it('triggers "initialized" event', async () => {
5857
await withServers(async baseConfig => {
5958
await withClient(user, baseConfig, async client => {
6059
const gotInited = eventSink(client, 'initialized');
@@ -63,49 +62,18 @@ describe('LDClient', () => {
6362
});
6463
});
6564

66-
it('should emit an error when initialize is called without an environment key', async () => {
67-
const client = platform.testing.makeClient('', user);
68-
const gotError = eventSink(client, 'error');
69-
70-
const err = await gotError.take();
71-
expect(err.message).toEqual(messages.environmentNotSpecified());
72-
});
73-
74-
it('should emit an error when an invalid environment key is specified', async () => {
75-
await withServers(async (baseConfig, pollServer) => {
76-
pollServer.byDefault(respond(404));
77-
await withClient(user, baseConfig, async client => {
78-
const gotError = eventSink(client, 'error');
79-
80-
await expect(client.waitForInitialization()).rejects.toThrow();
81-
82-
const err = await gotError.take();
83-
expect(err).toEqual(new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound()));
84-
});
85-
});
86-
});
87-
88-
it('should emit a failure event when an invalid environment key is specified', async () => {
89-
await withServers(async (baseConfig, pollServer) => {
90-
pollServer.byDefault(respond(404));
65+
it('resolves waitForInitialization promise', async () => {
66+
await withServers(async baseConfig => {
9167
await withClient(user, baseConfig, async client => {
92-
const gotFailed = eventSink(client, 'failed');
93-
94-
await expect(client.waitForInitialization()).rejects.toThrow();
95-
96-
const err = await gotFailed.take();
97-
expect(err).toEqual(new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound()));
68+
await client.waitForInitialization();
9869
});
9970
});
10071
});
10172

102-
it('returns default values when an invalid environment key is specified', async () => {
103-
await withServers(async (baseConfig, pollServer) => {
104-
pollServer.byDefault(respond(404));
73+
it('resolves waitUntilReady promise', async () => {
74+
await withServers(async baseConfig => {
10575
await withClient(user, baseConfig, async client => {
106-
await expect(client.waitForInitialization()).rejects.toThrow();
107-
108-
expect(client.variation('flag-key', 1)).toEqual(1);
76+
await client.waitUntilReady();
10977
});
11078
});
11179
});
@@ -145,19 +113,6 @@ describe('LDClient', () => {
145113
expect(result).toEqual(1);
146114
});
147115

148-
it('should emit an error event if there was an error fetching flags', async () => {
149-
await withServers(async (baseConfig, pollServer) => {
150-
pollServer.byDefault(respond(503));
151-
await withClient(user, baseConfig, async client => {
152-
const gotError = eventSink(client, 'error');
153-
154-
await expect(client.waitForInitialization()).rejects.toThrow();
155-
const err = await gotError.take();
156-
expect(err).toEqual(new errors.LDFlagFetchError(messages.errorFetchingFlags(503)));
157-
});
158-
});
159-
});
160-
161116
async function verifyCustomHeader(sendLDHeaders, shouldGetHeaders) {
162117
await withServers(async (baseConfig, pollServer) => {
163118
await withClient(user, { ...baseConfig, sendLDHeaders }, async client => {
@@ -231,6 +186,87 @@ describe('LDClient', () => {
231186
});
232187
});
233188

189+
describe('failed initialization', () => {
190+
function doErrorTests(expectedMessage, doWithClientAsyncFn) {
191+
async function runTest(asyncTest) {
192+
try {
193+
await doWithClientAsyncFn(asyncTest);
194+
} finally {
195+
// sleep briefly so any unhandled promise rejections will show up in this test, instead of
196+
// in a later test
197+
await sleepAsync(2);
198+
}
199+
}
200+
201+
it('rejects waitForInitialization promise', async () => {
202+
await runTest(async client => {
203+
await expect(client.waitForInitialization()).rejects.toThrow();
204+
});
205+
});
206+
207+
it('resolves waitUntilReady promise', async () => {
208+
await runTest(async client => {
209+
await client.waitUntilReady();
210+
});
211+
});
212+
213+
it('emits "error" event', async () => {
214+
await runTest(async client => {
215+
const gotError = eventSink(client, 'error');
216+
const err = await gotError.take();
217+
expect(err.message).toEqual(expectedMessage);
218+
});
219+
});
220+
221+
it('emits "failed" event', async () => {
222+
await runTest(async client => {
223+
const gotFailed = eventSink(client, 'failed');
224+
const err = await gotFailed.take();
225+
expect(err.message).toEqual(expectedMessage);
226+
});
227+
});
228+
229+
it('emits "ready" event', async () => {
230+
await runTest(async client => {
231+
const gotReady = eventSink(client, 'ready');
232+
await gotReady.take();
233+
});
234+
});
235+
236+
it('returns default values', async () => {
237+
await runTest(async client => {
238+
await client.waitUntilReady();
239+
expect(client.variation('flag-key', 1)).toEqual(1);
240+
});
241+
});
242+
}
243+
244+
describe('environment key not specified', () => {
245+
doErrorTests(
246+
messages.environmentNotSpecified(),
247+
async callback => await withCloseable(platform.testing.makeClient('', user), callback)
248+
);
249+
});
250+
251+
describe('invalid environment key (404 error)', () => {
252+
doErrorTests(messages.environmentNotFound(), async callback => {
253+
await withServers(async (baseConfig, pollServer) => {
254+
pollServer.byDefault(respond(404));
255+
await withClient(user, baseConfig, callback);
256+
});
257+
});
258+
});
259+
260+
describe('HTTP error other than 404 on initial poll', () => {
261+
doErrorTests(messages.errorFetchingFlags(503), async callback => {
262+
await withServers(async (baseConfig, pollServer) => {
263+
pollServer.byDefault(respond(503));
264+
await withClient(user, baseConfig, callback);
265+
});
266+
});
267+
});
268+
});
269+
234270
describe('initialization with bootstrap object', () => {
235271
it('should not fetch flag settings', async () => {
236272
await withServers(async (baseConfig, pollServer) => {
@@ -271,42 +307,6 @@ describe('LDClient', () => {
271307
});
272308
});
273309

274-
describe('waitUntilReady', () => {
275-
it('should resolve waitUntilReady promise when ready', async () => {
276-
await withServers(async baseConfig => {
277-
await withClient(user, baseConfig, async client => {
278-
const gotReady = eventSink(client, 'ready');
279-
280-
await gotReady.take();
281-
await client.waitUntilReady();
282-
});
283-
});
284-
});
285-
});
286-
287-
describe('waitForInitialization', () => {
288-
it('resolves promise on successful init', async () => {
289-
await withServers(async baseConfig => {
290-
await withClient(user, baseConfig, async client => {
291-
const gotReady = eventSink(client, 'ready');
292-
293-
await gotReady.take();
294-
await client.waitForInitialization();
295-
});
296-
});
297-
});
298-
299-
it('rejects promise if flags request fails', async () => {
300-
await withServers(async (baseConfig, pollServer) => {
301-
pollServer.byDefault(respond(404));
302-
await withClient(user, baseConfig, async client => {
303-
const err = new errors.LDInvalidEnvironmentIdError(messages.environmentNotFound());
304-
await expect(client.waitForInitialization()).rejects.toThrow(err);
305-
});
306-
});
307-
});
308-
});
309-
310310
describe('variation', () => {
311311
it('returns value for an existing flag - from bootstrap', async () => {
312312
const config = {

0 commit comments

Comments
 (0)