mixpanel-browser 2.56.0 → 2.57.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.
@@ -3,11 +3,9 @@
3
3
 
4
4
  var Config = {
5
5
  DEBUG: false,
6
- LIB_VERSION: '2.56.0'
6
+ LIB_VERSION: '2.57.1'
7
7
  };
8
8
 
9
- /* eslint camelcase: "off", eqeqeq: "off" */
10
-
11
9
  // since es6 imports are static and we run unit tests from the console, window won't be defined when importing this file
12
10
  var win;
13
11
  if (typeof(window) === 'undefined') {
@@ -27,6 +25,370 @@
27
25
  win = window;
28
26
  }
29
27
 
28
+ var setImmediate = win['setImmediate'];
29
+ var builtInProp, cycle, schedulingQueue,
30
+ ToString = Object.prototype.toString,
31
+ timer = (typeof setImmediate !== 'undefined') ?
32
+ function timer(fn) { return setImmediate(fn); } :
33
+ setTimeout;
34
+
35
+ // dammit, IE8.
36
+ try {
37
+ Object.defineProperty({},'x',{});
38
+ builtInProp = function builtInProp(obj,name,val,config) {
39
+ return Object.defineProperty(obj,name,{
40
+ value: val,
41
+ writable: true,
42
+ configurable: config !== false
43
+ });
44
+ };
45
+ }
46
+ catch (err) {
47
+ builtInProp = function builtInProp(obj,name,val) {
48
+ obj[name] = val;
49
+ return obj;
50
+ };
51
+ }
52
+
53
+ // Note: using a queue instead of array for efficiency
54
+ schedulingQueue = (function Queue() {
55
+ var first, last, item;
56
+
57
+ function Item(fn,self) {
58
+ this.fn = fn;
59
+ this.self = self;
60
+ this.next = void 0;
61
+ }
62
+
63
+ return {
64
+ add: function add(fn,self) {
65
+ item = new Item(fn,self);
66
+ if (last) {
67
+ last.next = item;
68
+ }
69
+ else {
70
+ first = item;
71
+ }
72
+ last = item;
73
+ item = void 0;
74
+ },
75
+ drain: function drain() {
76
+ var f = first;
77
+ first = last = cycle = void 0;
78
+
79
+ while (f) {
80
+ f.fn.call(f.self);
81
+ f = f.next;
82
+ }
83
+ }
84
+ };
85
+ })();
86
+
87
+ function schedule(fn,self) {
88
+ schedulingQueue.add(fn,self);
89
+ if (!cycle) {
90
+ cycle = timer(schedulingQueue.drain);
91
+ }
92
+ }
93
+
94
+ // promise duck typing
95
+ function isThenable(o) {
96
+ var _then, oType = typeof o;
97
+
98
+ if (o !== null && (oType === 'object' || oType === 'function')) {
99
+ _then = o.then;
100
+ }
101
+ return typeof _then === 'function' ? _then : false;
102
+ }
103
+
104
+ function notify() {
105
+ for (var i=0; i<this.chain.length; i++) {
106
+ notifyIsolated(
107
+ this,
108
+ (this.state === 1) ? this.chain[i].success : this.chain[i].failure,
109
+ this.chain[i]
110
+ );
111
+ }
112
+ this.chain.length = 0;
113
+ }
114
+
115
+ // NOTE: This is a separate function to isolate
116
+ // the `try..catch` so that other code can be
117
+ // optimized better
118
+ function notifyIsolated(self,cb,chain) {
119
+ var ret, _then;
120
+ try {
121
+ if (cb === false) {
122
+ chain.reject(self.msg);
123
+ }
124
+ else {
125
+ if (cb === true) {
126
+ ret = self.msg;
127
+ }
128
+ else {
129
+ ret = cb.call(void 0,self.msg);
130
+ }
131
+
132
+ if (ret === chain.promise) {
133
+ chain.reject(TypeError('Promise-chain cycle'));
134
+ }
135
+ // eslint-disable-next-line no-cond-assign
136
+ else if (_then = isThenable(ret)) {
137
+ _then.call(ret,chain.resolve,chain.reject);
138
+ }
139
+ else {
140
+ chain.resolve(ret);
141
+ }
142
+ }
143
+ }
144
+ catch (err) {
145
+ chain.reject(err);
146
+ }
147
+ }
148
+
149
+ function resolve(msg) {
150
+ var _then, self = this;
151
+
152
+ // already triggered?
153
+ if (self.triggered) { return; }
154
+
155
+ self.triggered = true;
156
+
157
+ // unwrap
158
+ if (self.def) {
159
+ self = self.def;
160
+ }
161
+
162
+ try {
163
+ // eslint-disable-next-line no-cond-assign
164
+ if (_then = isThenable(msg)) {
165
+ schedule(function(){
166
+ var defWrapper = new MakeDefWrapper(self);
167
+ try {
168
+ _then.call(msg,
169
+ function $resolve$(){ resolve.apply(defWrapper,arguments); },
170
+ function $reject$(){ reject.apply(defWrapper,arguments); }
171
+ );
172
+ }
173
+ catch (err) {
174
+ reject.call(defWrapper,err);
175
+ }
176
+ });
177
+ }
178
+ else {
179
+ self.msg = msg;
180
+ self.state = 1;
181
+ if (self.chain.length > 0) {
182
+ schedule(notify,self);
183
+ }
184
+ }
185
+ }
186
+ catch (err) {
187
+ reject.call(new MakeDefWrapper(self),err);
188
+ }
189
+ }
190
+
191
+ function reject(msg) {
192
+ var self = this;
193
+
194
+ // already triggered?
195
+ if (self.triggered) { return; }
196
+
197
+ self.triggered = true;
198
+
199
+ // unwrap
200
+ if (self.def) {
201
+ self = self.def;
202
+ }
203
+
204
+ self.msg = msg;
205
+ self.state = 2;
206
+ if (self.chain.length > 0) {
207
+ schedule(notify,self);
208
+ }
209
+ }
210
+
211
+ function iteratePromises(Constructor,arr,resolver,rejecter) {
212
+ for (var idx=0; idx<arr.length; idx++) {
213
+ (function IIFE(idx){
214
+ Constructor.resolve(arr[idx])
215
+ .then(
216
+ function $resolver$(msg){
217
+ resolver(idx,msg);
218
+ },
219
+ rejecter
220
+ );
221
+ })(idx);
222
+ }
223
+ }
224
+
225
+ function MakeDefWrapper(self) {
226
+ this.def = self;
227
+ this.triggered = false;
228
+ }
229
+
230
+ function MakeDef(self) {
231
+ this.promise = self;
232
+ this.state = 0;
233
+ this.triggered = false;
234
+ this.chain = [];
235
+ this.msg = void 0;
236
+ }
237
+
238
+ function NpoPromise(executor) {
239
+ if (typeof executor !== 'function') {
240
+ throw TypeError('Not a function');
241
+ }
242
+
243
+ if (this['__NPO__'] !== 0) {
244
+ throw TypeError('Not a promise');
245
+ }
246
+
247
+ // instance shadowing the inherited "brand"
248
+ // to signal an already "initialized" promise
249
+ this['__NPO__'] = 1;
250
+
251
+ var def = new MakeDef(this);
252
+
253
+ this['then'] = function then(success,failure) {
254
+ var o = {
255
+ success: typeof success === 'function' ? success : true,
256
+ failure: typeof failure === 'function' ? failure : false
257
+ };
258
+ // Note: `then(..)` itself can be borrowed to be used against
259
+ // a different promise constructor for making the chained promise,
260
+ // by substituting a different `this` binding.
261
+ o.promise = new this.constructor(function extractChain(resolve,reject) {
262
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
263
+ throw TypeError('Not a function');
264
+ }
265
+
266
+ o.resolve = resolve;
267
+ o.reject = reject;
268
+ });
269
+ def.chain.push(o);
270
+
271
+ if (def.state !== 0) {
272
+ schedule(notify,def);
273
+ }
274
+
275
+ return o.promise;
276
+ };
277
+ this['catch'] = function $catch$(failure) {
278
+ return this.then(void 0,failure);
279
+ };
280
+
281
+ try {
282
+ executor.call(
283
+ void 0,
284
+ function publicResolve(msg){
285
+ resolve.call(def,msg);
286
+ },
287
+ function publicReject(msg) {
288
+ reject.call(def,msg);
289
+ }
290
+ );
291
+ }
292
+ catch (err) {
293
+ reject.call(def,err);
294
+ }
295
+ }
296
+
297
+ var PromisePrototype = builtInProp({},'constructor',NpoPromise,
298
+ /*configurable=*/false
299
+ );
300
+
301
+ // Note: Android 4 cannot use `Object.defineProperty(..)` here
302
+ NpoPromise.prototype = PromisePrototype;
303
+
304
+ // built-in "brand" to signal an "uninitialized" promise
305
+ builtInProp(PromisePrototype,'__NPO__',0,
306
+ /*configurable=*/false
307
+ );
308
+
309
+ builtInProp(NpoPromise,'resolve',function Promise$resolve(msg) {
310
+ var Constructor = this;
311
+
312
+ // spec mandated checks
313
+ // note: best "isPromise" check that's practical for now
314
+ if (msg && typeof msg === 'object' && msg['__NPO__'] === 1) {
315
+ return msg;
316
+ }
317
+
318
+ return new Constructor(function executor(resolve,reject){
319
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
320
+ throw TypeError('Not a function');
321
+ }
322
+
323
+ resolve(msg);
324
+ });
325
+ });
326
+
327
+ builtInProp(NpoPromise,'reject',function Promise$reject(msg) {
328
+ return new this(function executor(resolve,reject){
329
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
330
+ throw TypeError('Not a function');
331
+ }
332
+
333
+ reject(msg);
334
+ });
335
+ });
336
+
337
+ builtInProp(NpoPromise,'all',function Promise$all(arr) {
338
+ var Constructor = this;
339
+
340
+ // spec mandated checks
341
+ if (ToString.call(arr) !== '[object Array]') {
342
+ return Constructor.reject(TypeError('Not an array'));
343
+ }
344
+ if (arr.length === 0) {
345
+ return Constructor.resolve([]);
346
+ }
347
+
348
+ return new Constructor(function executor(resolve,reject){
349
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
350
+ throw TypeError('Not a function');
351
+ }
352
+
353
+ var len = arr.length, msgs = Array(len), count = 0;
354
+
355
+ iteratePromises(Constructor,arr,function resolver(idx,msg) {
356
+ msgs[idx] = msg;
357
+ if (++count === len) {
358
+ resolve(msgs);
359
+ }
360
+ },reject);
361
+ });
362
+ });
363
+
364
+ builtInProp(NpoPromise,'race',function Promise$race(arr) {
365
+ var Constructor = this;
366
+
367
+ // spec mandated checks
368
+ if (ToString.call(arr) !== '[object Array]') {
369
+ return Constructor.reject(TypeError('Not an array'));
370
+ }
371
+
372
+ return new Constructor(function executor(resolve,reject){
373
+ if (typeof resolve !== 'function' || typeof reject !== 'function') {
374
+ throw TypeError('Not a function');
375
+ }
376
+
377
+ iteratePromises(Constructor,arr,function resolver(idx,msg){
378
+ resolve(msg);
379
+ },reject);
380
+ });
381
+ });
382
+
383
+ var PromisePolyfill;
384
+ if (typeof Promise !== 'undefined' && Promise.toString().indexOf('[native code]') !== -1) {
385
+ PromisePolyfill = Promise;
386
+ } else {
387
+ PromisePolyfill = NpoPromise;
388
+ }
389
+
390
+ /* eslint camelcase: "off", eqeqeq: "off" */
391
+
30
392
  // Maximum allowed session recording length
31
393
  var MAX_RECORDING_MS = 24 * 60 * 60 * 1000; // 24 hours
32
394
 
@@ -1086,7 +1448,7 @@
1086
1448
 
1087
1449
  var supported = true;
1088
1450
  try {
1089
- storage = storage || window.localStorage;
1451
+ storage = storage || win.localStorage;
1090
1452
  var key = '__mplss_' + cheap_guid(8),
1091
1453
  val = 'xyz';
1092
1454
  storage.setItem(key, val);
@@ -1118,7 +1480,7 @@
1118
1480
 
1119
1481
  get: function(name) {
1120
1482
  try {
1121
- return window.localStorage.getItem(name);
1483
+ return win.localStorage.getItem(name);
1122
1484
  } catch (err) {
1123
1485
  _.localStorage.error(err);
1124
1486
  }
@@ -1136,7 +1498,7 @@
1136
1498
 
1137
1499
  set: function(name, value) {
1138
1500
  try {
1139
- window.localStorage.setItem(name, value);
1501
+ win.localStorage.setItem(name, value);
1140
1502
  } catch (err) {
1141
1503
  _.localStorage.error(err);
1142
1504
  }
@@ -1144,7 +1506,7 @@
1144
1506
 
1145
1507
  remove: function(name) {
1146
1508
  try {
1147
- window.localStorage.removeItem(name);
1509
+ win.localStorage.removeItem(name);
1148
1510
  } catch (err) {
1149
1511
  _.localStorage.error(err);
1150
1512
  }
@@ -1183,7 +1545,7 @@
1183
1545
 
1184
1546
  function makeHandler(element, new_handler, old_handlers) {
1185
1547
  var handler = function(event) {
1186
- event = event || fixEvent(window.event);
1548
+ event = event || fixEvent(win.event);
1187
1549
 
1188
1550
  // this basically happens in firefox whenever another script
1189
1551
  // overwrites the onload callback and doesn't pass the event
@@ -1738,6 +2100,7 @@
1738
2100
  _['info']['browser'] = _.info.browser;
1739
2101
  _['info']['browserVersion'] = _.info.browserVersion;
1740
2102
  _['info']['properties'] = _.info.properties;
2103
+ _['NPO'] = NpoPromise;
1741
2104
 
1742
2105
  /* eslint camelcase: "off" */
1743
2106
 
@@ -1919,121 +2282,175 @@
1919
2282
  this.storage = options.storage || window.localStorage;
1920
2283
  this.pollIntervalMS = options.pollIntervalMS || 100;
1921
2284
  this.timeoutMS = options.timeoutMS || 2000;
2285
+
2286
+ // dependency-inject promise implementation for testing purposes
2287
+ this.promiseImpl = options.promiseImpl || PromisePolyfill;
1922
2288
  };
1923
2289
 
1924
2290
  // pass in a specific pid to test contention scenarios; otherwise
1925
2291
  // it is chosen randomly for each acquisition attempt
1926
- SharedLock.prototype.withLock = function(lockedCB, errorCB, pid) {
1927
- if (!pid && typeof errorCB !== 'function') {
1928
- pid = errorCB;
1929
- errorCB = null;
1930
- }
1931
-
1932
- var i = pid || (new Date().getTime() + '|' + Math.random());
1933
- var startTime = new Date().getTime();
2292
+ SharedLock.prototype.withLock = function(lockedCB, pid) {
2293
+ var Promise = this.promiseImpl;
2294
+ return new Promise(_.bind(function (resolve, reject) {
2295
+ var i = pid || (new Date().getTime() + '|' + Math.random());
2296
+ var startTime = new Date().getTime();
1934
2297
 
1935
- var key = this.storageKey;
1936
- var pollIntervalMS = this.pollIntervalMS;
1937
- var timeoutMS = this.timeoutMS;
1938
- var storage = this.storage;
2298
+ var key = this.storageKey;
2299
+ var pollIntervalMS = this.pollIntervalMS;
2300
+ var timeoutMS = this.timeoutMS;
2301
+ var storage = this.storage;
1939
2302
 
1940
- var keyX = key + ':X';
1941
- var keyY = key + ':Y';
1942
- var keyZ = key + ':Z';
2303
+ var keyX = key + ':X';
2304
+ var keyY = key + ':Y';
2305
+ var keyZ = key + ':Z';
1943
2306
 
1944
- var reportError = function(err) {
1945
- errorCB && errorCB(err);
1946
- };
2307
+ var delay = function(cb) {
2308
+ if (new Date().getTime() - startTime > timeoutMS) {
2309
+ logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
2310
+ storage.removeItem(keyZ);
2311
+ storage.removeItem(keyY);
2312
+ loop();
2313
+ return;
2314
+ }
2315
+ setTimeout(function() {
2316
+ try {
2317
+ cb();
2318
+ } catch(err) {
2319
+ reject(err);
2320
+ }
2321
+ }, pollIntervalMS * (Math.random() + 0.1));
2322
+ };
1947
2323
 
1948
- var delay = function(cb) {
1949
- if (new Date().getTime() - startTime > timeoutMS) {
1950
- logger$2.error('Timeout waiting for mutex on ' + key + '; clearing lock. [' + i + ']');
1951
- storage.removeItem(keyZ);
1952
- storage.removeItem(keyY);
1953
- loop();
1954
- return;
1955
- }
1956
- setTimeout(function() {
1957
- try {
2324
+ var waitFor = function(predicate, cb) {
2325
+ if (predicate()) {
1958
2326
  cb();
1959
- } catch(err) {
1960
- reportError(err);
2327
+ } else {
2328
+ delay(function() {
2329
+ waitFor(predicate, cb);
2330
+ });
1961
2331
  }
1962
- }, pollIntervalMS * (Math.random() + 0.1));
1963
- };
1964
-
1965
- var waitFor = function(predicate, cb) {
1966
- if (predicate()) {
1967
- cb();
1968
- } else {
1969
- delay(function() {
1970
- waitFor(predicate, cb);
1971
- });
1972
- }
1973
- };
2332
+ };
1974
2333
 
1975
- var getSetY = function() {
1976
- var valY = storage.getItem(keyY);
1977
- if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
1978
- return false;
1979
- } else {
1980
- storage.setItem(keyY, i);
1981
- if (storage.getItem(keyY) === i) {
1982
- return true;
2334
+ var getSetY = function() {
2335
+ var valY = storage.getItem(keyY);
2336
+ if (valY && valY !== i) { // if Y == i then this process already has the lock (useful for test cases)
2337
+ return false;
1983
2338
  } else {
1984
- if (!localStorageSupported(storage, true)) {
1985
- throw new Error('localStorage support dropped while acquiring lock');
2339
+ storage.setItem(keyY, i);
2340
+ if (storage.getItem(keyY) === i) {
2341
+ return true;
2342
+ } else {
2343
+ if (!localStorageSupported(storage, true)) {
2344
+ reject(new Error('localStorage support dropped while acquiring lock'));
2345
+ }
2346
+ return false;
1986
2347
  }
1987
- return false;
1988
2348
  }
1989
- }
1990
- };
1991
-
1992
- var loop = function() {
1993
- storage.setItem(keyX, i);
2349
+ };
1994
2350
 
1995
- waitFor(getSetY, function() {
1996
- if (storage.getItem(keyX) === i) {
1997
- criticalSection();
1998
- return;
1999
- }
2351
+ var loop = function() {
2352
+ storage.setItem(keyX, i);
2000
2353
 
2001
- delay(function() {
2002
- if (storage.getItem(keyY) !== i) {
2003
- loop();
2354
+ waitFor(getSetY, function() {
2355
+ if (storage.getItem(keyX) === i) {
2356
+ criticalSection();
2004
2357
  return;
2005
2358
  }
2006
- waitFor(function() {
2007
- return !storage.getItem(keyZ);
2008
- }, criticalSection);
2359
+
2360
+ delay(function() {
2361
+ if (storage.getItem(keyY) !== i) {
2362
+ loop();
2363
+ return;
2364
+ }
2365
+ waitFor(function() {
2366
+ return !storage.getItem(keyZ);
2367
+ }, criticalSection);
2368
+ });
2009
2369
  });
2010
- });
2011
- };
2370
+ };
2371
+
2372
+ var criticalSection = function() {
2373
+ storage.setItem(keyZ, '1');
2374
+ var removeLock = function () {
2375
+ storage.removeItem(keyZ);
2376
+ if (storage.getItem(keyY) === i) {
2377
+ storage.removeItem(keyY);
2378
+ }
2379
+ if (storage.getItem(keyX) === i) {
2380
+ storage.removeItem(keyX);
2381
+ }
2382
+ };
2383
+
2384
+ lockedCB()
2385
+ .then(function (ret) {
2386
+ removeLock();
2387
+ resolve(ret);
2388
+ })
2389
+ .catch(function (err) {
2390
+ removeLock();
2391
+ reject(err);
2392
+ });
2393
+ };
2012
2394
 
2013
- var criticalSection = function() {
2014
- storage.setItem(keyZ, '1');
2015
2395
  try {
2016
- lockedCB();
2017
- } finally {
2018
- storage.removeItem(keyZ);
2019
- if (storage.getItem(keyY) === i) {
2020
- storage.removeItem(keyY);
2021
- }
2022
- if (storage.getItem(keyX) === i) {
2023
- storage.removeItem(keyX);
2396
+ if (localStorageSupported(storage, true)) {
2397
+ loop();
2398
+ } else {
2399
+ throw new Error('localStorage support check failed');
2024
2400
  }
2401
+ } catch(err) {
2402
+ reject(err);
2025
2403
  }
2026
- };
2404
+ }, this));
2405
+ };
2027
2406
 
2028
- try {
2029
- if (localStorageSupported(storage, true)) {
2030
- loop();
2031
- } else {
2032
- throw new Error('localStorage support check failed');
2407
+ /**
2408
+ * @typedef {import('./wrapper').StorageWrapper}
2409
+ */
2410
+
2411
+ /**
2412
+ * @type {StorageWrapper}
2413
+ */
2414
+ var LocalStorageWrapper = function (storageOverride) {
2415
+ this.storage = storageOverride || localStorage;
2416
+ };
2417
+
2418
+ LocalStorageWrapper.prototype.init = function () {
2419
+ return PromisePolyfill.resolve();
2420
+ };
2421
+
2422
+ LocalStorageWrapper.prototype.setItem = function (key, value) {
2423
+ return new PromisePolyfill(_.bind(function (resolve, reject) {
2424
+ try {
2425
+ this.storage.setItem(key, value);
2426
+ } catch (e) {
2427
+ reject(e);
2033
2428
  }
2034
- } catch(err) {
2035
- reportError(err);
2036
- }
2429
+ resolve();
2430
+ }, this));
2431
+ };
2432
+
2433
+ LocalStorageWrapper.prototype.getItem = function (key) {
2434
+ return new PromisePolyfill(_.bind(function (resolve, reject) {
2435
+ var item;
2436
+ try {
2437
+ item = this.storage.getItem(key);
2438
+ } catch (e) {
2439
+ reject(e);
2440
+ }
2441
+ resolve(item);
2442
+ }, this));
2443
+ };
2444
+
2445
+ LocalStorageWrapper.prototype.removeItem = function (key) {
2446
+ return new PromisePolyfill(_.bind(function (resolve, reject) {
2447
+ try {
2448
+ this.storage.removeItem(key);
2449
+ } catch (e) {
2450
+ reject(e);
2451
+ }
2452
+ resolve();
2453
+ }, this));
2037
2454
  };
2038
2455
 
2039
2456
  var logger$1 = console_with_prefix('batch');
@@ -2054,19 +2471,38 @@
2054
2471
  * to data loss in some situations).
2055
2472
  * @constructor
2056
2473
  */
2057
- var RequestQueue = function(storageKey, options) {
2474
+ var RequestQueue = function (storageKey, options) {
2058
2475
  options = options || {};
2059
2476
  this.storageKey = storageKey;
2060
2477
  this.usePersistence = options.usePersistence;
2061
2478
  if (this.usePersistence) {
2062
- this.storage = options.storage || window.localStorage;
2063
- this.lock = new SharedLock(storageKey, {storage: this.storage});
2479
+ this.queueStorage = options.queueStorage || new LocalStorageWrapper();
2480
+ this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
2481
+ this.queueStorage.init();
2064
2482
  }
2065
2483
  this.reportError = options.errorReporter || _.bind(logger$1.error, logger$1);
2066
2484
 
2067
2485
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
2068
2486
 
2069
2487
  this.memQueue = [];
2488
+ this.initialized = false;
2489
+ };
2490
+
2491
+ RequestQueue.prototype.ensureInit = function () {
2492
+ if (this.initialized) {
2493
+ return PromisePolyfill.resolve();
2494
+ }
2495
+
2496
+ return this.queueStorage
2497
+ .init()
2498
+ .then(_.bind(function () {
2499
+ this.initialized = true;
2500
+ }, this))
2501
+ .catch(_.bind(function (err) {
2502
+ this.reportError('Error initializing queue persistence. Disabling persistence', err);
2503
+ this.initialized = true;
2504
+ this.usePersistence = false;
2505
+ }, this));
2070
2506
  };
2071
2507
 
2072
2508
  /**
@@ -2081,7 +2517,7 @@
2081
2517
  * failure of the enqueue operation; it is asynchronous because the localStorage
2082
2518
  * lock is asynchronous.
2083
2519
  */
2084
- RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
2520
+ RequestQueue.prototype.enqueue = function (item, flushInterval) {
2085
2521
  var queueEntry = {
2086
2522
  'id': cheap_guid(),
2087
2523
  'flushAfter': new Date().getTime() + flushInterval * 2,
@@ -2090,33 +2526,37 @@
2090
2526
 
2091
2527
  if (!this.usePersistence) {
2092
2528
  this.memQueue.push(queueEntry);
2093
- if (cb) {
2094
- cb(true);
2095
- }
2529
+ return PromisePolyfill.resolve(true);
2096
2530
  } else {
2097
- this.lock.withLock(_.bind(function lockAcquired() {
2098
- var succeeded;
2099
- try {
2100
- var storedQueue = this.readFromStorage();
2101
- storedQueue.push(queueEntry);
2102
- succeeded = this.saveToStorage(storedQueue);
2103
- if (succeeded) {
2531
+
2532
+ var enqueueItem = _.bind(function () {
2533
+ return this.ensureInit()
2534
+ .then(_.bind(function () {
2535
+ return this.readFromStorage();
2536
+ }, this))
2537
+ .then(_.bind(function (storedQueue) {
2538
+ storedQueue.push(queueEntry);
2539
+ return this.saveToStorage(storedQueue);
2540
+ }, this))
2541
+ .then(_.bind(function (succeeded) {
2104
2542
  // only add to in-memory queue when storage succeeds
2105
- this.memQueue.push(queueEntry);
2106
- }
2107
- } catch(err) {
2108
- this.reportError('Error enqueueing item', item);
2109
- succeeded = false;
2110
- }
2111
- if (cb) {
2112
- cb(succeeded);
2113
- }
2114
- }, this), _.bind(function lockFailure(err) {
2115
- this.reportError('Error acquiring storage lock', err);
2116
- if (cb) {
2117
- cb(false);
2118
- }
2119
- }, this), this.pid);
2543
+ if (succeeded) {
2544
+ this.memQueue.push(queueEntry);
2545
+ }
2546
+ return succeeded;
2547
+ }, this))
2548
+ .catch(_.bind(function (err) {
2549
+ this.reportError('Error enqueueing item', err, item);
2550
+ return false;
2551
+ }, this));
2552
+ }, this);
2553
+
2554
+ return this.lock
2555
+ .withLock(enqueueItem, this.pid)
2556
+ .catch(_.bind(function (err) {
2557
+ this.reportError('Error acquiring storage lock', err);
2558
+ return false;
2559
+ }, this));
2120
2560
  }
2121
2561
  };
2122
2562
 
@@ -2126,31 +2566,41 @@
2126
2566
  * in the persisted queue (items where the 'flushAfter' time has
2127
2567
  * already passed).
2128
2568
  */
2129
- RequestQueue.prototype.fillBatch = function(batchSize) {
2569
+ RequestQueue.prototype.fillBatch = function (batchSize) {
2130
2570
  var batch = this.memQueue.slice(0, batchSize);
2131
2571
  if (this.usePersistence && batch.length < batchSize) {
2132
2572
  // don't need lock just to read events; localStorage is thread-safe
2133
2573
  // and the worst that could happen is a duplicate send of some
2134
2574
  // orphaned events, which will be deduplicated on the server side
2135
- var storedQueue = this.readFromStorage();
2136
- if (storedQueue.length) {
2137
- // item IDs already in batch; don't duplicate out of storage
2138
- var idsInBatch = {}; // poor man's Set
2139
- _.each(batch, function(item) { idsInBatch[item['id']] = true; });
2140
-
2141
- for (var i = 0; i < storedQueue.length; i++) {
2142
- var item = storedQueue[i];
2143
- if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
2144
- item.orphaned = true;
2145
- batch.push(item);
2146
- if (batch.length >= batchSize) {
2147
- break;
2575
+ return this.ensureInit()
2576
+ .then(_.bind(function () {
2577
+ return this.readFromStorage();
2578
+ }, this))
2579
+ .then(_.bind(function (storedQueue) {
2580
+ if (storedQueue.length) {
2581
+ // item IDs already in batch; don't duplicate out of storage
2582
+ var idsInBatch = {}; // poor man's Set
2583
+ _.each(batch, function (item) {
2584
+ idsInBatch[item['id']] = true;
2585
+ });
2586
+
2587
+ for (var i = 0; i < storedQueue.length; i++) {
2588
+ var item = storedQueue[i];
2589
+ if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
2590
+ item.orphaned = true;
2591
+ batch.push(item);
2592
+ if (batch.length >= batchSize) {
2593
+ break;
2594
+ }
2595
+ }
2148
2596
  }
2149
2597
  }
2150
- }
2151
- }
2598
+
2599
+ return batch;
2600
+ }, this));
2601
+ } else {
2602
+ return PromisePolyfill.resolve(batch);
2152
2603
  }
2153
- return batch;
2154
2604
  };
2155
2605
 
2156
2606
  /**
@@ -2158,9 +2608,9 @@
2158
2608
  * also remove any item without a valid id (e.g., malformed
2159
2609
  * storage entries).
2160
2610
  */
2161
- var filterOutIDsAndInvalid = function(items, idSet) {
2611
+ var filterOutIDsAndInvalid = function (items, idSet) {
2162
2612
  var filteredItems = [];
2163
- _.each(items, function(item) {
2613
+ _.each(items, function (item) {
2164
2614
  if (item['id'] && !idSet[item['id']]) {
2165
2615
  filteredItems.push(item);
2166
2616
  }
@@ -2172,78 +2622,80 @@
2172
2622
  * Remove items with matching IDs from both in-memory queue
2173
2623
  * and persisted queue
2174
2624
  */
2175
- RequestQueue.prototype.removeItemsByID = function(ids, cb) {
2625
+ RequestQueue.prototype.removeItemsByID = function (ids) {
2176
2626
  var idSet = {}; // poor man's Set
2177
- _.each(ids, function(id) { idSet[id] = true; });
2627
+ _.each(ids, function (id) {
2628
+ idSet[id] = true;
2629
+ });
2178
2630
 
2179
2631
  this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
2180
2632
  if (!this.usePersistence) {
2181
- if (cb) {
2182
- cb(true);
2183
- }
2633
+ return PromisePolyfill.resolve(true);
2184
2634
  } else {
2185
- var removeFromStorage = _.bind(function() {
2186
- var succeeded;
2187
- try {
2188
- var storedQueue = this.readFromStorage();
2189
- storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
2190
- succeeded = this.saveToStorage(storedQueue);
2191
-
2192
- // an extra check: did storage report success but somehow
2193
- // the items are still there?
2194
- if (succeeded) {
2195
- storedQueue = this.readFromStorage();
2635
+ var removeFromStorage = _.bind(function () {
2636
+ return this.ensureInit()
2637
+ .then(_.bind(function () {
2638
+ return this.readFromStorage();
2639
+ }, this))
2640
+ .then(_.bind(function (storedQueue) {
2641
+ storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
2642
+ return this.saveToStorage(storedQueue);
2643
+ }, this))
2644
+ .then(_.bind(function () {
2645
+ return this.readFromStorage();
2646
+ }, this))
2647
+ .then(_.bind(function (storedQueue) {
2648
+ // an extra check: did storage report success but somehow
2649
+ // the items are still there?
2196
2650
  for (var i = 0; i < storedQueue.length; i++) {
2197
2651
  var item = storedQueue[i];
2198
2652
  if (item['id'] && !!idSet[item['id']]) {
2199
- this.reportError('Item not removed from storage');
2200
- return false;
2653
+ throw new Error('Item not removed from storage');
2201
2654
  }
2202
2655
  }
2203
- }
2204
- } catch(err) {
2205
- this.reportError('Error removing items', ids);
2206
- succeeded = false;
2207
- }
2208
- return succeeded;
2656
+ return true;
2657
+ }, this))
2658
+ .catch(_.bind(function (err) {
2659
+ this.reportError('Error removing items', err, ids);
2660
+ return false;
2661
+ }, this));
2209
2662
  }, this);
2210
2663
 
2211
- this.lock.withLock(function lockAcquired() {
2212
- var succeeded = removeFromStorage();
2213
- if (cb) {
2214
- cb(succeeded);
2215
- }
2216
- }, _.bind(function lockFailure(err) {
2217
- var succeeded = false;
2218
- this.reportError('Error acquiring storage lock', err);
2219
- if (!localStorageSupported(this.storage, true)) {
2220
- // Looks like localStorage writes have stopped working sometime after
2221
- // initialization (probably full), and so nobody can acquire locks
2222
- // anymore. Consider it temporarily safe to remove items without the
2223
- // lock, since nobody's writing successfully anyway.
2224
- succeeded = removeFromStorage();
2225
- if (!succeeded) {
2226
- // OK, we couldn't even write out the smaller queue. Try clearing it
2227
- // entirely.
2228
- try {
2229
- this.storage.removeItem(this.storageKey);
2230
- } catch(err) {
2231
- this.reportError('Error clearing queue', err);
2232
- }
2664
+ return this.lock
2665
+ .withLock(removeFromStorage, this.pid)
2666
+ .catch(_.bind(function (err) {
2667
+ this.reportError('Error acquiring storage lock', err);
2668
+ if (!localStorageSupported(this.queueStorage.storage, true)) {
2669
+ // Looks like localStorage writes have stopped working sometime after
2670
+ // initialization (probably full), and so nobody can acquire locks
2671
+ // anymore. Consider it temporarily safe to remove items without the
2672
+ // lock, since nobody's writing successfully anyway.
2673
+ return removeFromStorage()
2674
+ .then(_.bind(function (success) {
2675
+ if (!success) {
2676
+ // OK, we couldn't even write out the smaller queue. Try clearing it
2677
+ // entirely.
2678
+ return this.queueStorage.removeItem(this.storageKey).then(function () {
2679
+ return success;
2680
+ });
2681
+ }
2682
+ return success;
2683
+ }, this))
2684
+ .catch(_.bind(function (err) {
2685
+ this.reportError('Error clearing queue', err);
2686
+ return false;
2687
+ }, this));
2688
+ } else {
2689
+ return false;
2233
2690
  }
2234
- }
2235
- if (cb) {
2236
- cb(succeeded);
2237
- }
2238
- }, this), this.pid);
2691
+ }, this));
2239
2692
  }
2240
-
2241
2693
  };
2242
2694
 
2243
2695
  // internal helper for RequestQueue.updatePayloads
2244
- var updatePayloads = function(existingItems, itemsToUpdate) {
2696
+ var updatePayloads = function (existingItems, itemsToUpdate) {
2245
2697
  var newItems = [];
2246
- _.each(existingItems, function(item) {
2698
+ _.each(existingItems, function (item) {
2247
2699
  var id = item['id'];
2248
2700
  if (id in itemsToUpdate) {
2249
2701
  var newPayload = itemsToUpdate[id];
@@ -2263,79 +2715,95 @@
2263
2715
  * Update payloads of given items in both in-memory queue and
2264
2716
  * persisted queue. Items set to null are removed from queues.
2265
2717
  */
2266
- RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
2718
+ RequestQueue.prototype.updatePayloads = function (itemsToUpdate) {
2267
2719
  this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
2268
2720
  if (!this.usePersistence) {
2269
- if (cb) {
2270
- cb(true);
2271
- }
2721
+ return PromisePolyfill.resolve(true);
2272
2722
  } else {
2273
- this.lock.withLock(_.bind(function lockAcquired() {
2274
- var succeeded;
2275
- try {
2276
- var storedQueue = this.readFromStorage();
2277
- storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2278
- succeeded = this.saveToStorage(storedQueue);
2279
- } catch(err) {
2280
- this.reportError('Error updating items', itemsToUpdate);
2281
- succeeded = false;
2282
- }
2283
- if (cb) {
2284
- cb(succeeded);
2285
- }
2286
- }, this), _.bind(function lockFailure(err) {
2287
- this.reportError('Error acquiring storage lock', err);
2288
- if (cb) {
2289
- cb(false);
2290
- }
2291
- }, this), this.pid);
2723
+ return this.lock
2724
+ .withLock(_.bind(function lockAcquired() {
2725
+ return this.ensureInit()
2726
+ .then(_.bind(function () {
2727
+ return this.readFromStorage();
2728
+ }, this))
2729
+ .then(_.bind(function (storedQueue) {
2730
+ storedQueue = updatePayloads(storedQueue, itemsToUpdate);
2731
+ return this.saveToStorage(storedQueue);
2732
+ }, this))
2733
+ .catch(_.bind(function (err) {
2734
+ this.reportError('Error updating items', itemsToUpdate, err);
2735
+ return false;
2736
+ }, this));
2737
+ }, this), this.pid)
2738
+ .catch(_.bind(function (err) {
2739
+ this.reportError('Error acquiring storage lock', err);
2740
+ return false;
2741
+ }, this));
2292
2742
  }
2293
-
2294
2743
  };
2295
2744
 
2296
2745
  /**
2297
2746
  * Read and parse items array from localStorage entry, handling
2298
2747
  * malformed/missing data if necessary.
2299
2748
  */
2300
- RequestQueue.prototype.readFromStorage = function() {
2301
- var storageEntry;
2302
- try {
2303
- storageEntry = this.storage.getItem(this.storageKey);
2304
- if (storageEntry) {
2305
- storageEntry = JSONParse(storageEntry);
2306
- if (!_.isArray(storageEntry)) {
2307
- this.reportError('Invalid storage entry:', storageEntry);
2308
- storageEntry = null;
2749
+ RequestQueue.prototype.readFromStorage = function () {
2750
+ return this.ensureInit()
2751
+ .then(_.bind(function () {
2752
+ return this.queueStorage.getItem(this.storageKey);
2753
+ }, this))
2754
+ .then(_.bind(function (storageEntry) {
2755
+ if (storageEntry) {
2756
+ storageEntry = JSONParse(storageEntry);
2757
+ if (!_.isArray(storageEntry)) {
2758
+ this.reportError('Invalid storage entry:', storageEntry);
2759
+ storageEntry = null;
2760
+ }
2309
2761
  }
2310
- }
2311
- } catch (err) {
2312
- this.reportError('Error retrieving queue', err);
2313
- storageEntry = null;
2314
- }
2315
- return storageEntry || [];
2762
+ return storageEntry || [];
2763
+ }, this))
2764
+ .catch(_.bind(function (err) {
2765
+ this.reportError('Error retrieving queue', err);
2766
+ return [];
2767
+ }, this));
2316
2768
  };
2317
2769
 
2318
2770
  /**
2319
2771
  * Serialize the given items array to localStorage.
2320
2772
  */
2321
- RequestQueue.prototype.saveToStorage = function(queue) {
2773
+ RequestQueue.prototype.saveToStorage = function (queue) {
2322
2774
  try {
2323
- this.storage.setItem(this.storageKey, JSONStringify(queue));
2324
- return true;
2775
+ var serialized = JSONStringify(queue);
2325
2776
  } catch (err) {
2326
- this.reportError('Error saving queue', err);
2327
- return false;
2777
+ this.reportError('Error serializing queue', err);
2778
+ return PromisePolyfill.resolve(false);
2328
2779
  }
2780
+
2781
+ return this.ensureInit()
2782
+ .then(_.bind(function () {
2783
+ return this.queueStorage.setItem(this.storageKey, serialized);
2784
+ }, this))
2785
+ .then(function () {
2786
+ return true;
2787
+ })
2788
+ .catch(_.bind(function (err) {
2789
+ this.reportError('Error saving queue', err);
2790
+ return false;
2791
+ }, this));
2329
2792
  };
2330
2793
 
2331
2794
  /**
2332
2795
  * Clear out queues (memory and localStorage).
2333
2796
  */
2334
- RequestQueue.prototype.clear = function() {
2797
+ RequestQueue.prototype.clear = function () {
2335
2798
  this.memQueue = [];
2336
2799
 
2337
2800
  if (this.usePersistence) {
2338
- this.storage.removeItem(this.storageKey);
2801
+ return this.ensureInit()
2802
+ .then(_.bind(function () {
2803
+ return this.queueStorage.removeItem(this.storageKey);
2804
+ }, this));
2805
+ } else {
2806
+ return PromisePolyfill.resolve();
2339
2807
  }
2340
2808
  };
2341
2809
 
@@ -2354,7 +2822,8 @@
2354
2822
  this.errorReporter = options.errorReporter;
2355
2823
  this.queue = new RequestQueue(storageKey, {
2356
2824
  errorReporter: _.bind(this.reportError, this),
2357
- storage: options.storage,
2825
+ queueStorage: options.queueStorage,
2826
+ sharedLockStorage: options.sharedLockStorage,
2358
2827
  usePersistence: options.usePersistence
2359
2828
  });
2360
2829
 
@@ -2382,8 +2851,8 @@
2382
2851
  /**
2383
2852
  * Add one item to queue.
2384
2853
  */
2385
- RequestBatcher.prototype.enqueue = function(item, cb) {
2386
- this.queue.enqueue(item, this.flushInterval, cb);
2854
+ RequestBatcher.prototype.enqueue = function(item) {
2855
+ return this.queue.enqueue(item, this.flushInterval);
2387
2856
  };
2388
2857
 
2389
2858
  /**
@@ -2393,7 +2862,7 @@
2393
2862
  RequestBatcher.prototype.start = function() {
2394
2863
  this.stopped = false;
2395
2864
  this.consecutiveRemovalFailures = 0;
2396
- this.flush();
2865
+ return this.flush();
2397
2866
  };
2398
2867
 
2399
2868
  /**
@@ -2411,7 +2880,7 @@
2411
2880
  * Clear out queue.
2412
2881
  */
2413
2882
  RequestBatcher.prototype.clear = function() {
2414
- this.queue.clear();
2883
+ return this.queue.clear();
2415
2884
  };
2416
2885
 
2417
2886
  /**
@@ -2442,6 +2911,17 @@
2442
2911
  }
2443
2912
  };
2444
2913
 
2914
+ /**
2915
+ * Send a request using the sendRequest callback, but promisified.
2916
+ * TODO: sendRequest should be promisified in the first place.
2917
+ */
2918
+ RequestBatcher.prototype.sendRequestPromise = function(data, options) {
2919
+ return new PromisePolyfill(_.bind(function(resolve) {
2920
+ this.sendRequest(data, options, resolve);
2921
+ }, this));
2922
+ };
2923
+
2924
+
2445
2925
  /**
2446
2926
  * Flush one batch to network. Depending on success/failure modes, it will either
2447
2927
  * remove the batch from the queue or leave it in for retry, and schedule the next
@@ -2453,183 +2933,191 @@
2453
2933
  * sendBeacon offers no callbacks or status indications)
2454
2934
  */
2455
2935
  RequestBatcher.prototype.flush = function(options) {
2456
- try {
2936
+ if (this.requestInProgress) {
2937
+ logger.log('Flush: Request already in progress');
2938
+ return PromisePolyfill.resolve();
2939
+ }
2457
2940
 
2458
- if (this.requestInProgress) {
2459
- logger.log('Flush: Request already in progress');
2460
- return;
2461
- }
2941
+ this.requestInProgress = true;
2462
2942
 
2463
- options = options || {};
2464
- var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2465
- var startTime = new Date().getTime();
2466
- var currentBatchSize = this.batchSize;
2467
- var batch = this.queue.fillBatch(currentBatchSize);
2468
- // if there's more items in the queue than the batch size, attempt
2469
- // to flush again after the current batch is done.
2470
- var attemptSecondaryFlush = batch.length === currentBatchSize;
2471
- var dataForRequest = [];
2472
- var transformedItems = {};
2473
- _.each(batch, function(item) {
2474
- var payload = item['payload'];
2475
- if (this.beforeSendHook && !item.orphaned) {
2476
- payload = this.beforeSendHook(payload);
2477
- }
2478
- if (payload) {
2479
- // mp_sent_by_lib_version prop captures which lib version actually
2480
- // sends each event (regardless of which version originally queued
2481
- // it for sending)
2482
- if (payload['event'] && payload['properties']) {
2483
- payload['properties'] = _.extend(
2484
- {},
2485
- payload['properties'],
2486
- {'mp_sent_by_lib_version': Config.LIB_VERSION}
2487
- );
2943
+ options = options || {};
2944
+ var timeoutMS = this.libConfig['batch_request_timeout_ms'];
2945
+ var startTime = new Date().getTime();
2946
+ var currentBatchSize = this.batchSize;
2947
+
2948
+ return this.queue.fillBatch(currentBatchSize)
2949
+ .then(_.bind(function(batch) {
2950
+
2951
+ // if there's more items in the queue than the batch size, attempt
2952
+ // to flush again after the current batch is done.
2953
+ var attemptSecondaryFlush = batch.length === currentBatchSize;
2954
+ var dataForRequest = [];
2955
+ var transformedItems = {};
2956
+ _.each(batch, function(item) {
2957
+ var payload = item['payload'];
2958
+ if (this.beforeSendHook && !item.orphaned) {
2959
+ payload = this.beforeSendHook(payload);
2488
2960
  }
2489
- var addPayload = true;
2490
- var itemId = item['id'];
2491
- if (itemId) {
2492
- if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
2493
- this.reportError('[dupe] item ID sent too many times, not sending', {
2494
- item: item,
2495
- batchSize: batch.length,
2496
- timesSent: this.itemIdsSentSuccessfully[itemId]
2497
- });
2498
- addPayload = false;
2961
+ if (payload) {
2962
+ // mp_sent_by_lib_version prop captures which lib version actually
2963
+ // sends each event (regardless of which version originally queued
2964
+ // it for sending)
2965
+ if (payload['event'] && payload['properties']) {
2966
+ payload['properties'] = _.extend(
2967
+ {},
2968
+ payload['properties'],
2969
+ {'mp_sent_by_lib_version': Config.LIB_VERSION}
2970
+ );
2971
+ }
2972
+ var addPayload = true;
2973
+ var itemId = item['id'];
2974
+ if (itemId) {
2975
+ if ((this.itemIdsSentSuccessfully[itemId] || 0) > 5) {
2976
+ this.reportError('[dupe] item ID sent too many times, not sending', {
2977
+ item: item,
2978
+ batchSize: batch.length,
2979
+ timesSent: this.itemIdsSentSuccessfully[itemId]
2980
+ });
2981
+ addPayload = false;
2982
+ }
2983
+ } else {
2984
+ this.reportError('[dupe] found item with no ID', {item: item});
2499
2985
  }
2500
- } else {
2501
- this.reportError('[dupe] found item with no ID', {item: item});
2502
- }
2503
2986
 
2504
- if (addPayload) {
2505
- dataForRequest.push(payload);
2987
+ if (addPayload) {
2988
+ dataForRequest.push(payload);
2989
+ }
2506
2990
  }
2507
- }
2508
- transformedItems[item['id']] = payload;
2509
- }, this);
2510
- if (dataForRequest.length < 1) {
2511
- this.resetFlush();
2512
- return; // nothing to do
2513
- }
2514
-
2515
- this.requestInProgress = true;
2516
-
2517
- var batchSendCallback = _.bind(function(res) {
2518
- this.requestInProgress = false;
2991
+ transformedItems[item['id']] = payload;
2992
+ }, this);
2519
2993
 
2520
- try {
2994
+ if (dataForRequest.length < 1) {
2995
+ this.requestInProgress = false;
2996
+ this.resetFlush();
2997
+ return PromisePolyfill.resolve(); // nothing to do
2998
+ }
2521
2999
 
2522
- // handle API response in a try-catch to make sure we can reset the
2523
- // flush operation if something goes wrong
2524
-
2525
- var removeItemsFromQueue = false;
2526
- if (options.unloading) {
2527
- // update persisted data to include hook transformations
2528
- this.queue.updatePayloads(transformedItems);
2529
- } else if (
2530
- _.isObject(res) &&
2531
- res.error === 'timeout' &&
2532
- new Date().getTime() - startTime >= timeoutMS
2533
- ) {
2534
- this.reportError('Network timeout; retrying');
2535
- this.flush();
2536
- } else if (
2537
- _.isObject(res) &&
2538
- (
2539
- res.httpStatusCode >= 500
2540
- || res.httpStatusCode === 429
2541
- || (res.httpStatusCode <= 0 && !isOnline())
2542
- || res.error === 'timeout'
3000
+ var removeItemsFromQueue = _.bind(function () {
3001
+ return this.queue
3002
+ .removeItemsByID(
3003
+ _.map(batch, function (item) {
3004
+ return item['id'];
3005
+ })
2543
3006
  )
2544
- ) {
2545
- // network or API error, or 429 Too Many Requests, retry
2546
- var retryMS = this.flushInterval * 2;
2547
- if (res.retryAfter) {
2548
- retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
2549
- }
2550
- retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
2551
- this.reportError('Error; retry in ' + retryMS + ' ms');
2552
- this.scheduleFlush(retryMS);
2553
- } else if (_.isObject(res) && res.httpStatusCode === 413) {
2554
- // 413 Payload Too Large
2555
- if (batch.length > 1) {
2556
- var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
2557
- this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
2558
- this.reportError('413 response; reducing batch size to ' + this.batchSize);
2559
- this.resetFlush();
2560
- } else {
2561
- this.reportError('Single-event request too large; dropping', batch);
2562
- this.resetBatchSize();
2563
- removeItemsFromQueue = true;
2564
- }
2565
- } else {
2566
- // successful network request+response; remove each item in batch from queue
2567
- // (even if it was e.g. a 400, in which case retrying won't help)
2568
- removeItemsFromQueue = true;
2569
- }
2570
-
2571
- if (removeItemsFromQueue) {
2572
- this.queue.removeItemsByID(
2573
- _.map(batch, function(item) { return item['id']; }),
2574
- _.bind(function(succeeded) {
2575
- if (succeeded) {
2576
- this.consecutiveRemovalFailures = 0;
2577
- if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
2578
- this.resetFlush(); // schedule next batch with a delay
2579
- } else {
2580
- this.flush(); // handle next batch if the queue isn't empty
3007
+ .then(_.bind(function (succeeded) {
3008
+ // client-side dedupe
3009
+ _.each(batch, _.bind(function(item) {
3010
+ var itemId = item['id'];
3011
+ if (itemId) {
3012
+ this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
3013
+ this.itemIdsSentSuccessfully[itemId]++;
3014
+ if (this.itemIdsSentSuccessfully[itemId] > 5) {
3015
+ this.reportError('[dupe] item ID sent too many times', {
3016
+ item: item,
3017
+ batchSize: batch.length,
3018
+ timesSent: this.itemIdsSentSuccessfully[itemId]
3019
+ });
2581
3020
  }
2582
3021
  } else {
2583
- this.reportError('Failed to remove items from queue');
2584
- if (++this.consecutiveRemovalFailures > 5) {
2585
- this.reportError('Too many queue failures; disabling batching system.');
2586
- this.stopAllBatching();
2587
- } else {
2588
- this.resetFlush();
2589
- }
3022
+ this.reportError('[dupe] found item with no ID while removing', {item: item});
2590
3023
  }
2591
- }, this)
2592
- );
3024
+ }, this));
2593
3025
 
2594
- // client-side dedupe
2595
- _.each(batch, _.bind(function(item) {
2596
- var itemId = item['id'];
2597
- if (itemId) {
2598
- this.itemIdsSentSuccessfully[itemId] = this.itemIdsSentSuccessfully[itemId] || 0;
2599
- this.itemIdsSentSuccessfully[itemId]++;
2600
- if (this.itemIdsSentSuccessfully[itemId] > 5) {
2601
- this.reportError('[dupe] item ID sent too many times', {
2602
- item: item,
2603
- batchSize: batch.length,
2604
- timesSent: this.itemIdsSentSuccessfully[itemId]
2605
- });
3026
+ if (succeeded) {
3027
+ this.consecutiveRemovalFailures = 0;
3028
+ if (this.flushOnlyOnInterval && !attemptSecondaryFlush) {
3029
+ this.resetFlush(); // schedule next batch with a delay
3030
+ return PromisePolyfill.resolve();
3031
+ } else {
3032
+ return this.flush(); // handle next batch if the queue isn't empty
2606
3033
  }
2607
3034
  } else {
2608
- this.reportError('[dupe] found item with no ID while removing', {item: item});
3035
+ if (++this.consecutiveRemovalFailures > 5) {
3036
+ this.reportError('Too many queue failures; disabling batching system.');
3037
+ this.stopAllBatching();
3038
+ } else {
3039
+ this.resetFlush();
3040
+ }
3041
+ return PromisePolyfill.resolve();
2609
3042
  }
2610
3043
  }, this));
2611
- }
3044
+ }, this);
2612
3045
 
2613
- } catch(err) {
2614
- this.reportError('Error handling API response', err);
2615
- this.resetFlush();
3046
+ var batchSendCallback = _.bind(function(res) {
3047
+ this.requestInProgress = false;
3048
+
3049
+ try {
3050
+
3051
+ // handle API response in a try-catch to make sure we can reset the
3052
+ // flush operation if something goes wrong
3053
+
3054
+ if (options.unloading) {
3055
+ // update persisted data to include hook transformations
3056
+ return this.queue.updatePayloads(transformedItems);
3057
+ } else if (
3058
+ _.isObject(res) &&
3059
+ res.error === 'timeout' &&
3060
+ new Date().getTime() - startTime >= timeoutMS
3061
+ ) {
3062
+ this.reportError('Network timeout; retrying');
3063
+ return this.flush();
3064
+ } else if (
3065
+ _.isObject(res) &&
3066
+ (
3067
+ res.httpStatusCode >= 500
3068
+ || res.httpStatusCode === 429
3069
+ || (res.httpStatusCode <= 0 && !isOnline())
3070
+ || res.error === 'timeout'
3071
+ )
3072
+ ) {
3073
+ // network or API error, or 429 Too Many Requests, retry
3074
+ var retryMS = this.flushInterval * 2;
3075
+ if (res.retryAfter) {
3076
+ retryMS = (parseInt(res.retryAfter, 10) * 1000) || retryMS;
3077
+ }
3078
+ retryMS = Math.min(MAX_RETRY_INTERVAL_MS, retryMS);
3079
+ this.reportError('Error; retry in ' + retryMS + ' ms');
3080
+ this.scheduleFlush(retryMS);
3081
+ return PromisePolyfill.resolve();
3082
+ } else if (_.isObject(res) && res.httpStatusCode === 413) {
3083
+ // 413 Payload Too Large
3084
+ if (batch.length > 1) {
3085
+ var halvedBatchSize = Math.max(1, Math.floor(currentBatchSize / 2));
3086
+ this.batchSize = Math.min(this.batchSize, halvedBatchSize, batch.length - 1);
3087
+ this.reportError('413 response; reducing batch size to ' + this.batchSize);
3088
+ this.resetFlush();
3089
+ return PromisePolyfill.resolve();
3090
+ } else {
3091
+ this.reportError('Single-event request too large; dropping', batch);
3092
+ this.resetBatchSize();
3093
+ return removeItemsFromQueue();
3094
+ }
3095
+ } else {
3096
+ // successful network request+response; remove each item in batch from queue
3097
+ // (even if it was e.g. a 400, in which case retrying won't help)
3098
+ return removeItemsFromQueue();
3099
+ }
3100
+ } catch(err) {
3101
+ this.reportError('Error handling API response', err);
3102
+ this.resetFlush();
3103
+ }
3104
+ }, this);
3105
+ var requestOptions = {
3106
+ method: 'POST',
3107
+ verbose: true,
3108
+ ignore_json_errors: true, // eslint-disable-line camelcase
3109
+ timeout_ms: timeoutMS // eslint-disable-line camelcase
3110
+ };
3111
+ if (options.unloading) {
3112
+ requestOptions.transport = 'sendBeacon';
2616
3113
  }
2617
- }, this);
2618
- var requestOptions = {
2619
- method: 'POST',
2620
- verbose: true,
2621
- ignore_json_errors: true, // eslint-disable-line camelcase
2622
- timeout_ms: timeoutMS // eslint-disable-line camelcase
2623
- };
2624
- if (options.unloading) {
2625
- requestOptions.transport = 'sendBeacon';
2626
- }
2627
- logger.log('MIXPANEL REQUEST:', dataForRequest);
2628
- this.sendRequest(dataForRequest, requestOptions, batchSendCallback);
2629
- } catch(err) {
2630
- this.reportError('Error flushing request queue', err);
2631
- this.resetFlush();
2632
- }
3114
+ logger.log('MIXPANEL REQUEST:', dataForRequest);
3115
+ return this.sendRequestPromise(dataForRequest, requestOptions).then(batchSendCallback);
3116
+ }, this))
3117
+ .catch(_.bind(function(err) {
3118
+ this.reportError('Error flushing request queue', err);
3119
+ this.resetFlush();
3120
+ }, this));
2633
3121
  };
2634
3122
 
2635
3123
  /**
@@ -5035,7 +5523,7 @@
5035
5523
  }, this);
5036
5524
 
5037
5525
  if (this._batch_requests && !should_send_immediately) {
5038
- batcher.enqueue(truncated_data, function(succeeded) {
5526
+ batcher.enqueue(truncated_data).then(function(succeeded) {
5039
5527
  if (succeeded) {
5040
5528
  callback(1, truncated_data);
5041
5529
  } else {