mixpanel-browser 2.56.0 → 2.58.0

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.
@@ -1,5 +1,7 @@
1
1
  import { SharedLock } from './shared-lock';
2
2
  import { cheap_guid, console_with_prefix, localStorageSupported, JSONParse, JSONStringify, _ } from './utils'; // eslint-disable-line camelcase
3
+ import { LocalStorageWrapper } from './storage/local-storage';
4
+ import { Promise } from './promise-polyfill';
3
5
 
4
6
  var logger = console_with_prefix('batch');
5
7
 
@@ -19,19 +21,38 @@ var logger = console_with_prefix('batch');
19
21
  * to data loss in some situations).
20
22
  * @constructor
21
23
  */
22
- var RequestQueue = function(storageKey, options) {
24
+ var RequestQueue = function (storageKey, options) {
23
25
  options = options || {};
24
26
  this.storageKey = storageKey;
25
27
  this.usePersistence = options.usePersistence;
26
28
  if (this.usePersistence) {
27
- this.storage = options.storage || window.localStorage;
28
- this.lock = new SharedLock(storageKey, {storage: this.storage});
29
+ this.queueStorage = options.queueStorage || new LocalStorageWrapper();
30
+ this.lock = new SharedLock(storageKey, { storage: options.sharedLockStorage || window.localStorage });
31
+ this.queueStorage.init();
29
32
  }
30
33
  this.reportError = options.errorReporter || _.bind(logger.error, logger);
31
34
 
32
35
  this.pid = options.pid || null; // pass pid to test out storage lock contention scenarios
33
36
 
34
37
  this.memQueue = [];
38
+ this.initialized = false;
39
+ };
40
+
41
+ RequestQueue.prototype.ensureInit = function () {
42
+ if (this.initialized) {
43
+ return Promise.resolve();
44
+ }
45
+
46
+ return this.queueStorage
47
+ .init()
48
+ .then(_.bind(function () {
49
+ this.initialized = true;
50
+ }, this))
51
+ .catch(_.bind(function (err) {
52
+ this.reportError('Error initializing queue persistence. Disabling persistence', err);
53
+ this.initialized = true;
54
+ this.usePersistence = false;
55
+ }, this));
35
56
  };
36
57
 
37
58
  /**
@@ -46,7 +67,7 @@ var RequestQueue = function(storageKey, options) {
46
67
  * failure of the enqueue operation; it is asynchronous because the localStorage
47
68
  * lock is asynchronous.
48
69
  */
49
- RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
70
+ RequestQueue.prototype.enqueue = function (item, flushInterval) {
50
71
  var queueEntry = {
51
72
  'id': cheap_guid(),
52
73
  'flushAfter': new Date().getTime() + flushInterval * 2,
@@ -55,33 +76,37 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
55
76
 
56
77
  if (!this.usePersistence) {
57
78
  this.memQueue.push(queueEntry);
58
- if (cb) {
59
- cb(true);
60
- }
79
+ return Promise.resolve(true);
61
80
  } else {
62
- this.lock.withLock(_.bind(function lockAcquired() {
63
- var succeeded;
64
- try {
65
- var storedQueue = this.readFromStorage();
66
- storedQueue.push(queueEntry);
67
- succeeded = this.saveToStorage(storedQueue);
68
- if (succeeded) {
81
+
82
+ var enqueueItem = _.bind(function () {
83
+ return this.ensureInit()
84
+ .then(_.bind(function () {
85
+ return this.readFromStorage();
86
+ }, this))
87
+ .then(_.bind(function (storedQueue) {
88
+ storedQueue.push(queueEntry);
89
+ return this.saveToStorage(storedQueue);
90
+ }, this))
91
+ .then(_.bind(function (succeeded) {
69
92
  // only add to in-memory queue when storage succeeds
70
- this.memQueue.push(queueEntry);
71
- }
72
- } catch(err) {
73
- this.reportError('Error enqueueing item', item);
74
- succeeded = false;
75
- }
76
- if (cb) {
77
- cb(succeeded);
78
- }
79
- }, this), _.bind(function lockFailure(err) {
80
- this.reportError('Error acquiring storage lock', err);
81
- if (cb) {
82
- cb(false);
83
- }
84
- }, this), this.pid);
93
+ if (succeeded) {
94
+ this.memQueue.push(queueEntry);
95
+ }
96
+ return succeeded;
97
+ }, this))
98
+ .catch(_.bind(function (err) {
99
+ this.reportError('Error enqueueing item', err, item);
100
+ return false;
101
+ }, this));
102
+ }, this);
103
+
104
+ return this.lock
105
+ .withLock(enqueueItem, this.pid)
106
+ .catch(_.bind(function (err) {
107
+ this.reportError('Error acquiring storage lock', err);
108
+ return false;
109
+ }, this));
85
110
  }
86
111
  };
87
112
 
@@ -91,31 +116,41 @@ RequestQueue.prototype.enqueue = function(item, flushInterval, cb) {
91
116
  * in the persisted queue (items where the 'flushAfter' time has
92
117
  * already passed).
93
118
  */
94
- RequestQueue.prototype.fillBatch = function(batchSize) {
119
+ RequestQueue.prototype.fillBatch = function (batchSize) {
95
120
  var batch = this.memQueue.slice(0, batchSize);
96
121
  if (this.usePersistence && batch.length < batchSize) {
97
122
  // don't need lock just to read events; localStorage is thread-safe
98
123
  // and the worst that could happen is a duplicate send of some
99
124
  // orphaned events, which will be deduplicated on the server side
100
- var storedQueue = this.readFromStorage();
101
- if (storedQueue.length) {
102
- // item IDs already in batch; don't duplicate out of storage
103
- var idsInBatch = {}; // poor man's Set
104
- _.each(batch, function(item) { idsInBatch[item['id']] = true; });
125
+ return this.ensureInit()
126
+ .then(_.bind(function () {
127
+ return this.readFromStorage();
128
+ }, this))
129
+ .then(_.bind(function (storedQueue) {
130
+ if (storedQueue.length) {
131
+ // item IDs already in batch; don't duplicate out of storage
132
+ var idsInBatch = {}; // poor man's Set
133
+ _.each(batch, function (item) {
134
+ idsInBatch[item['id']] = true;
135
+ });
105
136
 
106
- for (var i = 0; i < storedQueue.length; i++) {
107
- var item = storedQueue[i];
108
- if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
109
- item.orphaned = true;
110
- batch.push(item);
111
- if (batch.length >= batchSize) {
112
- break;
137
+ for (var i = 0; i < storedQueue.length; i++) {
138
+ var item = storedQueue[i];
139
+ if (new Date().getTime() > item['flushAfter'] && !idsInBatch[item['id']]) {
140
+ item.orphaned = true;
141
+ batch.push(item);
142
+ if (batch.length >= batchSize) {
143
+ break;
144
+ }
145
+ }
113
146
  }
114
147
  }
115
- }
116
- }
148
+
149
+ return batch;
150
+ }, this));
151
+ } else {
152
+ return Promise.resolve(batch);
117
153
  }
118
- return batch;
119
154
  };
120
155
 
121
156
  /**
@@ -123,9 +158,9 @@ RequestQueue.prototype.fillBatch = function(batchSize) {
123
158
  * also remove any item without a valid id (e.g., malformed
124
159
  * storage entries).
125
160
  */
126
- var filterOutIDsAndInvalid = function(items, idSet) {
161
+ var filterOutIDsAndInvalid = function (items, idSet) {
127
162
  var filteredItems = [];
128
- _.each(items, function(item) {
163
+ _.each(items, function (item) {
129
164
  if (item['id'] && !idSet[item['id']]) {
130
165
  filteredItems.push(item);
131
166
  }
@@ -137,78 +172,80 @@ var filterOutIDsAndInvalid = function(items, idSet) {
137
172
  * Remove items with matching IDs from both in-memory queue
138
173
  * and persisted queue
139
174
  */
140
- RequestQueue.prototype.removeItemsByID = function(ids, cb) {
175
+ RequestQueue.prototype.removeItemsByID = function (ids) {
141
176
  var idSet = {}; // poor man's Set
142
- _.each(ids, function(id) { idSet[id] = true; });
177
+ _.each(ids, function (id) {
178
+ idSet[id] = true;
179
+ });
143
180
 
144
181
  this.memQueue = filterOutIDsAndInvalid(this.memQueue, idSet);
145
182
  if (!this.usePersistence) {
146
- if (cb) {
147
- cb(true);
148
- }
183
+ return Promise.resolve(true);
149
184
  } else {
150
- var removeFromStorage = _.bind(function() {
151
- var succeeded;
152
- try {
153
- var storedQueue = this.readFromStorage();
154
- storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
155
- succeeded = this.saveToStorage(storedQueue);
156
-
157
- // an extra check: did storage report success but somehow
158
- // the items are still there?
159
- if (succeeded) {
160
- storedQueue = this.readFromStorage();
185
+ var removeFromStorage = _.bind(function () {
186
+ return this.ensureInit()
187
+ .then(_.bind(function () {
188
+ return this.readFromStorage();
189
+ }, this))
190
+ .then(_.bind(function (storedQueue) {
191
+ storedQueue = filterOutIDsAndInvalid(storedQueue, idSet);
192
+ return this.saveToStorage(storedQueue);
193
+ }, this))
194
+ .then(_.bind(function () {
195
+ return this.readFromStorage();
196
+ }, this))
197
+ .then(_.bind(function (storedQueue) {
198
+ // an extra check: did storage report success but somehow
199
+ // the items are still there?
161
200
  for (var i = 0; i < storedQueue.length; i++) {
162
201
  var item = storedQueue[i];
163
202
  if (item['id'] && !!idSet[item['id']]) {
164
- this.reportError('Item not removed from storage');
165
- return false;
203
+ throw new Error('Item not removed from storage');
166
204
  }
167
205
  }
168
- }
169
- } catch(err) {
170
- this.reportError('Error removing items', ids);
171
- succeeded = false;
172
- }
173
- return succeeded;
206
+ return true;
207
+ }, this))
208
+ .catch(_.bind(function (err) {
209
+ this.reportError('Error removing items', err, ids);
210
+ return false;
211
+ }, this));
174
212
  }, this);
175
213
 
176
- this.lock.withLock(function lockAcquired() {
177
- var succeeded = removeFromStorage();
178
- if (cb) {
179
- cb(succeeded);
180
- }
181
- }, _.bind(function lockFailure(err) {
182
- var succeeded = false;
183
- this.reportError('Error acquiring storage lock', err);
184
- if (!localStorageSupported(this.storage, true)) {
185
- // Looks like localStorage writes have stopped working sometime after
186
- // initialization (probably full), and so nobody can acquire locks
187
- // anymore. Consider it temporarily safe to remove items without the
188
- // lock, since nobody's writing successfully anyway.
189
- succeeded = removeFromStorage();
190
- if (!succeeded) {
191
- // OK, we couldn't even write out the smaller queue. Try clearing it
192
- // entirely.
193
- try {
194
- this.storage.removeItem(this.storageKey);
195
- } catch(err) {
196
- this.reportError('Error clearing queue', err);
197
- }
214
+ return this.lock
215
+ .withLock(removeFromStorage, this.pid)
216
+ .catch(_.bind(function (err) {
217
+ this.reportError('Error acquiring storage lock', err);
218
+ if (!localStorageSupported(this.queueStorage.storage, true)) {
219
+ // Looks like localStorage writes have stopped working sometime after
220
+ // initialization (probably full), and so nobody can acquire locks
221
+ // anymore. Consider it temporarily safe to remove items without the
222
+ // lock, since nobody's writing successfully anyway.
223
+ return removeFromStorage()
224
+ .then(_.bind(function (success) {
225
+ if (!success) {
226
+ // OK, we couldn't even write out the smaller queue. Try clearing it
227
+ // entirely.
228
+ return this.queueStorage.removeItem(this.storageKey).then(function () {
229
+ return success;
230
+ });
231
+ }
232
+ return success;
233
+ }, this))
234
+ .catch(_.bind(function (err) {
235
+ this.reportError('Error clearing queue', err);
236
+ return false;
237
+ }, this));
238
+ } else {
239
+ return false;
198
240
  }
199
- }
200
- if (cb) {
201
- cb(succeeded);
202
- }
203
- }, this), this.pid);
241
+ }, this));
204
242
  }
205
-
206
243
  };
207
244
 
208
245
  // internal helper for RequestQueue.updatePayloads
209
- var updatePayloads = function(existingItems, itemsToUpdate) {
246
+ var updatePayloads = function (existingItems, itemsToUpdate) {
210
247
  var newItems = [];
211
- _.each(existingItems, function(item) {
248
+ _.each(existingItems, function (item) {
212
249
  var id = item['id'];
213
250
  if (id in itemsToUpdate) {
214
251
  var newPayload = itemsToUpdate[id];
@@ -228,79 +265,95 @@ var updatePayloads = function(existingItems, itemsToUpdate) {
228
265
  * Update payloads of given items in both in-memory queue and
229
266
  * persisted queue. Items set to null are removed from queues.
230
267
  */
231
- RequestQueue.prototype.updatePayloads = function(itemsToUpdate, cb) {
268
+ RequestQueue.prototype.updatePayloads = function (itemsToUpdate) {
232
269
  this.memQueue = updatePayloads(this.memQueue, itemsToUpdate);
233
270
  if (!this.usePersistence) {
234
- if (cb) {
235
- cb(true);
236
- }
271
+ return Promise.resolve(true);
237
272
  } else {
238
- this.lock.withLock(_.bind(function lockAcquired() {
239
- var succeeded;
240
- try {
241
- var storedQueue = this.readFromStorage();
242
- storedQueue = updatePayloads(storedQueue, itemsToUpdate);
243
- succeeded = this.saveToStorage(storedQueue);
244
- } catch(err) {
245
- this.reportError('Error updating items', itemsToUpdate);
246
- succeeded = false;
247
- }
248
- if (cb) {
249
- cb(succeeded);
250
- }
251
- }, this), _.bind(function lockFailure(err) {
252
- this.reportError('Error acquiring storage lock', err);
253
- if (cb) {
254
- cb(false);
255
- }
256
- }, this), this.pid);
273
+ return this.lock
274
+ .withLock(_.bind(function lockAcquired() {
275
+ return this.ensureInit()
276
+ .then(_.bind(function () {
277
+ return this.readFromStorage();
278
+ }, this))
279
+ .then(_.bind(function (storedQueue) {
280
+ storedQueue = updatePayloads(storedQueue, itemsToUpdate);
281
+ return this.saveToStorage(storedQueue);
282
+ }, this))
283
+ .catch(_.bind(function (err) {
284
+ this.reportError('Error updating items', itemsToUpdate, err);
285
+ return false;
286
+ }, this));
287
+ }, this), this.pid)
288
+ .catch(_.bind(function (err) {
289
+ this.reportError('Error acquiring storage lock', err);
290
+ return false;
291
+ }, this));
257
292
  }
258
-
259
293
  };
260
294
 
261
295
  /**
262
296
  * Read and parse items array from localStorage entry, handling
263
297
  * malformed/missing data if necessary.
264
298
  */
265
- RequestQueue.prototype.readFromStorage = function() {
266
- var storageEntry;
267
- try {
268
- storageEntry = this.storage.getItem(this.storageKey);
269
- if (storageEntry) {
270
- storageEntry = JSONParse(storageEntry);
271
- if (!_.isArray(storageEntry)) {
272
- this.reportError('Invalid storage entry:', storageEntry);
273
- storageEntry = null;
299
+ RequestQueue.prototype.readFromStorage = function () {
300
+ return this.ensureInit()
301
+ .then(_.bind(function () {
302
+ return this.queueStorage.getItem(this.storageKey);
303
+ }, this))
304
+ .then(_.bind(function (storageEntry) {
305
+ if (storageEntry) {
306
+ storageEntry = JSONParse(storageEntry);
307
+ if (!_.isArray(storageEntry)) {
308
+ this.reportError('Invalid storage entry:', storageEntry);
309
+ storageEntry = null;
310
+ }
274
311
  }
275
- }
276
- } catch (err) {
277
- this.reportError('Error retrieving queue', err);
278
- storageEntry = null;
279
- }
280
- return storageEntry || [];
312
+ return storageEntry || [];
313
+ }, this))
314
+ .catch(_.bind(function (err) {
315
+ this.reportError('Error retrieving queue', err);
316
+ return [];
317
+ }, this));
281
318
  };
282
319
 
283
320
  /**
284
321
  * Serialize the given items array to localStorage.
285
322
  */
286
- RequestQueue.prototype.saveToStorage = function(queue) {
323
+ RequestQueue.prototype.saveToStorage = function (queue) {
287
324
  try {
288
- this.storage.setItem(this.storageKey, JSONStringify(queue));
289
- return true;
325
+ var serialized = JSONStringify(queue);
290
326
  } catch (err) {
291
- this.reportError('Error saving queue', err);
292
- return false;
327
+ this.reportError('Error serializing queue', err);
328
+ return Promise.resolve(false);
293
329
  }
330
+
331
+ return this.ensureInit()
332
+ .then(_.bind(function () {
333
+ return this.queueStorage.setItem(this.storageKey, serialized);
334
+ }, this))
335
+ .then(function () {
336
+ return true;
337
+ })
338
+ .catch(_.bind(function (err) {
339
+ this.reportError('Error saving queue', err);
340
+ return false;
341
+ }, this));
294
342
  };
295
343
 
296
344
  /**
297
345
  * Clear out queues (memory and localStorage).
298
346
  */
299
- RequestQueue.prototype.clear = function() {
347
+ RequestQueue.prototype.clear = function () {
300
348
  this.memQueue = [];
301
349
 
302
350
  if (this.usePersistence) {
303
- this.storage.removeItem(this.storageKey);
351
+ return this.ensureInit()
352
+ .then(_.bind(function () {
353
+ return this.queueStorage.removeItem(this.storageKey);
354
+ }, this));
355
+ } else {
356
+ return Promise.resolve();
304
357
  }
305
358
  };
306
359