Skip to content

Commit fd6eeeb

Browse files
authored
Merge pull request #38 from launchdarkly/ag/ch15259/add-flush-method
[ch15259] Add flush method
2 parents eafeae7 + 39dd6eb commit fd6eeeb

File tree

6 files changed

+192
-47
lines changed

6 files changed

+192
-47
lines changed

src/EventProcessor.js

Lines changed: 55 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,43 @@
11
var utils = require('./utils');
22

3+
var MAX_URL_LENGTH = 2000;
4+
var hasCors = 'withCredentials' in new XMLHttpRequest();
5+
6+
function sendEvents(eventsUrl, events, sync) {
7+
var src = eventsUrl + '?d=' + utils.base64URLEncode(JSON.stringify(events));
8+
9+
var send = function(onDone) {
10+
// Detect browser support for CORS
11+
if (hasCors) {
12+
/* supports cross-domain requests */
13+
var xhr = new XMLHttpRequest();
14+
xhr.open('GET', src, !sync);
15+
16+
if (!sync) {
17+
xhr.addEventListener('load', onDone);
18+
}
19+
20+
xhr.send();
21+
} else {
22+
var img = new Image();
23+
24+
if (!sync) {
25+
img.addEventListener('load', onDone);
26+
}
27+
28+
img.src = src;
29+
}
30+
}
31+
32+
if (sync) {
33+
send();
34+
} else {
35+
return new Promise(function(resolve) {
36+
send(resolve);
37+
});
38+
}
39+
}
40+
341
function EventProcessor(eventsUrl, eventSerializer) {
442
var processor = {};
543
var queue = [];
@@ -8,63 +46,35 @@ function EventProcessor(eventsUrl, eventSerializer) {
846
processor.enqueue = function(event) {
947
queue.push(event);
1048
};
11-
49+
1250
processor.flush = function(user, sync) {
13-
var maxLength = 2000 - eventsUrl.length;
14-
var data = [];
15-
51+
var finalSync = sync === undefined ? false : sync;
52+
var serializedQueue = eventSerializer.serialize_events(queue);
53+
var chunks;
54+
var results = [];
55+
1656
if (!user) {
1757
if (initialFlush) {
1858
console && console.warn && console.warn('Be sure to call `identify` in the LaunchDarkly client: http://docs.launchdarkly.com/docs/running-an-ab-test#include-the-client-side-snippet');
1959
}
20-
return false;
60+
return Promise.resolve();
2161
}
2262

2363
initialFlush = false;
24-
while (maxLength > 0 && queue.length > 0) {
25-
var event = queue.pop();
26-
event.user = user;
27-
maxLength = maxLength - utils.base64URLEncode(JSON.stringify(event)).length;
28-
// If we are over the max size, put this one back on the queue
29-
// to try in the next round, unless this event alone is larger
30-
// than the limit, in which case, screw it, and try it anyway.
31-
if (maxLength < 0 && data.length > 0) {
32-
queue.push(event);
33-
} else {
34-
data.push(event);
35-
}
64+
65+
if (serializedQueue.length === 0) {
66+
return Promise.resolve();
3667
}
68+
69+
chunks = utils.chunkUserEventsForUrl(MAX_URL_LENGTH - eventsUrl.length, serializedQueue);
3770

38-
if (data.length > 0) {
39-
data = eventSerializer.serialize_events(data);
40-
var src = eventsUrl + '?d=' + utils.base64URLEncode(JSON.stringify(data));
41-
//Detect browser support for CORS
42-
if ('withCredentials' in new XMLHttpRequest()) {
43-
/* supports cross-domain requests */
44-
var xhr = new XMLHttpRequest();
45-
xhr.open('GET', src, !sync);
46-
xhr.send();
47-
} else {
48-
var img = new Image();
49-
img.src = src;
50-
}
71+
for (var i=0 ; i < chunks.length ; i++) {
72+
results.push(sendEvents(eventsUrl, chunks[i], finalSync));
5173
}
5274

53-
// if the queue is not empty, call settimeout to flush it again
54-
// with a 0 timeout (stack-less recursion)
55-
// Or, just recursively call flush_queue with the remaining elements
56-
// if we're doing this on unload
57-
if (queue.length > 0) {
58-
if (sync) {
59-
processor.flush(user, sync);
60-
}
61-
else {
62-
setTimeout(function() {
63-
processor.flush(user);
64-
}, 0);
65-
}
66-
}
67-
return false;
75+
queue = [];
76+
77+
return sync ? Promise.resolve() : Promise.all(results);
6878
};
6979

7080
return processor;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
var EventSerializer = require('../EventSerializer');
2+
var EventProcessor = require('../EventProcessor');
3+
4+
describe('EventProcessor', function() {
5+
var sandbox;
6+
var xhr;
7+
var requests = [];
8+
var serializer = EventSerializer({});
9+
10+
beforeEach(function() {
11+
sandbox = sinon.sandbox.create();
12+
requests = [];
13+
xhr = sinon.useFakeXMLHttpRequest();
14+
xhr.onCreate = function(xhr) {
15+
requests.push(xhr);
16+
};
17+
})
18+
19+
afterEach(function() {
20+
sandbox.restore();
21+
xhr.restore();
22+
})
23+
24+
it('should warn about missing user on initial flush', function() {
25+
var warnSpy = sandbox.spy(console, 'warn');
26+
var processor = EventProcessor('/fake-url', serializer);
27+
processor.flush(null);
28+
warnSpy.restore();
29+
expect(warnSpy.called).to.be.true;
30+
})
31+
32+
it('should flush asynchronously', function() {
33+
var processor = EventProcessor('/fake-url', serializer);
34+
var user = {key: 'foo'};
35+
var event = {kind: 'identify', key: user.key};
36+
var result;
37+
38+
processor.enqueue(event);
39+
processor.enqueue(event);
40+
processor.enqueue(event);
41+
processor.enqueue(event);
42+
43+
result = processor.flush(user);
44+
requests[0].respond();
45+
46+
expect(requests.length).to.equal(1);
47+
expect(requests[0].async).to.be.true;
48+
});
49+
50+
it('should flush synchronously', function() {
51+
var processor = EventProcessor('/fake-url', serializer);
52+
var user = {key: 'foo'};
53+
var event = {kind: 'identify', key: user.key};
54+
var result;
55+
56+
processor.enqueue(event);
57+
processor.enqueue(event);
58+
processor.enqueue(event);
59+
processor.enqueue(event);
60+
61+
result = processor.flush(user, true);
62+
requests[0].respond();
63+
64+
expect(requests.length).to.equal(1);
65+
expect(requests[0].async).to.be.false;
66+
});
67+
})

src/__tests__/utils-test.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
var assert = require('assert');
2-
var wrapPromiseCallback = require('../utils.js').wrapPromiseCallback;
2+
var utils = require('../utils.js');
3+
var wrapPromiseCallback = utils.wrapPromiseCallback;
4+
var chunkUserEventsForUrl = utils.chunkUserEventsForUrl;
35

46
describe('utils', function() {
57
describe('wrapPromiseCallback', function() {
@@ -49,4 +51,15 @@ describe('utils', function() {
4951
});
5052
});
5153
});
54+
55+
describe('chunkUserEventsForUrl', function() {
56+
it('should properly chunk the list of events', function() {
57+
var user = {key: 'foo'};
58+
var event = {kind: 'identify', key: user.key};
59+
var eventLength = utils.base64URLEncode(JSON.stringify(event)).length;
60+
var events = [event,event,event,event,event];
61+
var chunks = chunkUserEventsForUrl(eventLength * 2, events);
62+
expect(chunks).to.eql([[event, event],[event,event],[event]]);
63+
})
64+
})
5265
});

src/index.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,14 @@ declare module 'ldclient-js' {
206206
*/
207207
identify: (user: LDUser, hash?: string, onDone?: () => void) => Promise<void>;
208208

209+
/**
210+
* Flushes pending events asynchronously.
211+
*
212+
* @param onDone
213+
* A callback to invoke after the events were flushed.
214+
*/
215+
flush: (onDone?: Function) => Promise<void>;
216+
209217
/**
210218
* Retrieves a flag's value.
211219
*

src/index.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,7 @@ function initialize(env, user, options) {
7373
key: goal.key,
7474
data: null,
7575
url: window.location.href,
76+
user: ident.getUser(),
7677
creationDate: (new Date()).getTime()
7778
};
7879

@@ -99,6 +100,12 @@ function initialize(env, user, options) {
99100
}).bind(this)), onDone);
100101
}
101102

103+
function flush(onDone) {
104+
return utils.wrapPromiseCallback(new Promise(function(resolve) {
105+
return sendEvents ? resolve(events.flush(ident.getUser())) : resolve();
106+
}.bind(this), onDone));
107+
}
108+
102109
function variation(key, defaultValue) {
103110
var value;
104111

@@ -407,6 +414,7 @@ function initialize(env, user, options) {
407414
track: track,
408415
on: on,
409416
off: off,
417+
flush: flush,
410418
allFlags: allFlags
411419
};
412420

src/utils.js

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,12 +91,51 @@ function wrapPromiseCallback(promise, callback) {
9191
);
9292
}
9393

94+
/**
95+
* Returns an array of event groups each of which can be safely URL-encoded
96+
* without hitting the safe maximum URL length of certain browsers.
97+
*
98+
* @param {number} maxLength maximum URL length targeted
99+
* @param {Array[Object}]} events queue of events to divide
100+
* @returns Array[Array[Object]]
101+
*/
102+
function chunkUserEventsForUrl(maxLength, events) {
103+
var allEvents = events.slice(0);
104+
var remainingSpace = maxLength;
105+
var allChunks = [];
106+
var chunk;
107+
108+
while (allEvents.length > 0) {
109+
chunk = [];
110+
111+
while (remainingSpace > 0) {
112+
var event = allEvents.pop();
113+
if (!event) { break; }
114+
remainingSpace = remainingSpace - base64URLEncode(JSON.stringify(event)).length;
115+
// If we are over the max size, put this one back on the queue
116+
// to try in the next round, unless this event alone is larger
117+
// than the limit, in which case, screw it, and try it anyway.
118+
if (remainingSpace < 0 && chunk.length > 0) {
119+
allEvents.push(event);
120+
} else {
121+
chunk.push(event);
122+
}
123+
}
124+
125+
remainingSpace = maxLength;
126+
allChunks.push(chunk);
127+
}
128+
129+
return allChunks;
130+
}
131+
94132
module.exports = {
95133
btoa: btoa,
96134
base64URLEncode: base64URLEncode,
97135
clone: clone,
98136
modifications: modifications,
99137
merge: merge,
100138
onNextTick: onNextTick,
101-
wrapPromiseCallback: wrapPromiseCallback
139+
wrapPromiseCallback: wrapPromiseCallback,
140+
chunkUserEventsForUrl: chunkUserEventsForUrl
102141
};

0 commit comments

Comments
 (0)