-
Notifications
You must be signed in to change notification settings - Fork 181
/
Copy pathclient.js
421 lines (359 loc) · 12.5 KB
/
client.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
/**
* Constructs a new cross storage client given the url to a hub. By default,
* an iframe is created within the document body that points to the url. It
* also accepts an options object, which may include a timeout, frameId, and
* promise. The timeout, in milliseconds, is applied to each request and
* defaults to 3000ms. The options object may also include a frameId,
* identifying an existing frame on which to install its listeners. If the
* promise key is supplied the constructor for a Promise, that Promise library
* will be used instead of the default window.Promise.
*
* @example
* var storage = new CrossStorageClient('https://store.example.com/hub.html');
*
* @example
* var storage = new CrossStorageClient('https://store.example.com/hub.html', {
* timeout: 5000,
* frameId: 'storageFrame'
* });
*
* @constructor
*
* @param {string} url The url to a cross storage hub
* @param {object} [opts] An optional object containing additional options,
* including timeout, frameId, and promise
*
* @property {string} _id A UUID v4 id
* @property {function} _promise The Promise object to use
* @property {string} _frameId The id of the iFrame pointing to the hub url
* @property {string} _origin The hub's origin
* @property {object} _requests Mapping of request ids to callbacks
* @property {bool} _connected Whether or not it has connected
* @property {bool} _closed Whether or not the client has closed
* @property {int} _count Number of requests sent
* @property {function} _listener The listener added to the window
* @property {Window} _hub The hub window
*/
function CrossStorageClient(url, opts) {
opts = opts || {};
this._id = CrossStorageClient._generateUUID();
this._promise = opts.promise || Promise;
this._frameId = opts.frameId || 'CrossStorageClient-' + this._id;
this._origin = CrossStorageClient._getOrigin(url);
this._requests = {};
this._connected = false;
this._closed = false;
this._count = 0;
this._timeout = opts.timeout || 3000;
this._listener = null;
this._installListener();
var frame;
if (opts.frameId) {
frame = document.getElementById(opts.frameId);
}
// If using a passed iframe, poll the hub for a ready message
if (frame) {
this._poll();
}
// Create the frame if not found or specified
frame = frame || this._createFrame(url);
this._hub = frame.contentWindow;
}
/**
* The styles to be applied to the generated iFrame. Defines a set of properties
* that hide the element by positioning it outside of the visible area, and
* by modifying its display.
*
* @member {Object}
*/
CrossStorageClient.frameStyle = {
display: 'none',
position: 'absolute',
top: '-999px',
left: '-999px'
};
/**
* Returns the origin of an url, with cross browser support. Accommodates
* the lack of location.origin in IE, as well as the discrepancies in the
* inclusion of the port when using the default port for a protocol, e.g.
* 443 over https. Defaults to the origin of window.location if passed a
* relative path.
*
* @param {string} url The url to a cross storage hub
* @returns {string} The origin of the url
*/
CrossStorageClient._getOrigin = function(url) {
var uri, origin;
uri = document.createElement('a');
uri.href = url;
if (!uri.host) {
uri = window.location;
}
origin = uri.protocol + '//' + uri.host;
origin = origin.replace(/:80$|:443$/, '');
return origin;
};
/**
* UUID v4 generation, taken from: http://stackoverflow.com/questions/
* 105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
*
* @returns {string} A UUID v4 string
*/
CrossStorageClient._generateUUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};
/**
* Returns a promise that is fulfilled when a connection has been established
* with the cross storage hub. Its use is required to avoid sending any
* requests prior to initialization being complete.
*
* @returns {Promise} A promise that is resolved on connect
*/
CrossStorageClient.prototype.onConnect = function() {
var client = this;
if (this._connected) {
return this._promise.resolve();
} else if (this._closed) {
return this._promise.reject(new Error('CrossStorageClient has closed'));
}
// Queue connect requests for client re-use
if (!this._requests.connect) {
this._requests.connect = [];
}
return new this._promise(function(resolve, reject) {
var timeout = setTimeout(function() {
reject(new Error('CrossStorageClient could not connect'));
}, client._timeout);
client._requests.connect.push(function(err) {
clearTimeout(timeout);
if (err) return reject(err);
resolve();
});
});
};
/**
* Sets a key to the specified value, optionally accepting a ttl to passively
* expire the key after a number of milliseconds. Returns a promise that is
* fulfilled on success, or rejected if any errors setting the key occurred,
* or the request timed out.
*
* @param {string} key The key to set
* @param {*} value The value to assign
* @param {int} ttl Time to live in milliseconds
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.set = function(key, value, ttl) {
return this._request('set', {
key: key,
value: value,
ttl: ttl
});
};
/**
* Accepts one or more keys for which to retrieve their values. Returns a
* promise that is settled on hub response or timeout. On success, it is
* fulfilled with the value of the key if only passed a single argument.
* Otherwise it's resolved with an array of values. On failure, it is rejected
* with the corresponding error message.
*
* @param {...string} key The key to retrieve
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.get = function(key) {
var args = Array.prototype.slice.call(arguments);
return this._request('get', {keys: args});
};
/**
* Accepts one or more keys for deletion. Returns a promise that is settled on
* hub response or timeout.
*
* @param {...string} key The key to delete
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.del = function() {
var args = Array.prototype.slice.call(arguments);
return this._request('del', {keys: args});
};
/**
* Returns a promise that, when resolved, indicates that all localStorage
* data has been cleared.
*
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.clear = function() {
return this._request('clear');
};
/**
* Returns a promise that, when resolved, passes an array of all keys
* currently in storage.
*
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype.getKeys = function() {
return this._request('getKeys');
};
/**
* Deletes the iframe and sets the connected state to false. The client can
* no longer be used after being invoked.
*/
CrossStorageClient.prototype.close = function() {
var frame = document.getElementById(this._frameId);
if (frame) {
frame.parentNode.removeChild(frame);
}
// Support IE8 with detachEvent
if (window.removeEventListener) {
window.removeEventListener('message', this._listener, false);
} else {
window.detachEvent('onmessage', this._listener);
}
this._connected = false;
this._closed = true;
};
/**
* Installs the necessary listener for the window message event. When a message
* is received, the client's _connected status is changed to true, and the
* onConnect promise is fulfilled. Given a response message, the callback
* corresponding to its request is invoked. If response.error holds a truthy
* value, the promise associated with the original request is rejected with
* the error. Otherwise the promise is fulfilled and passed response.result.
*
* @private
*/
CrossStorageClient.prototype._installListener = function() {
var client = this;
this._listener = function(message) {
var i, error, response;
if (client._closed) return;
// Ignore messages not from our hub
if (message.origin !== client._origin) return;
// LocalStorage isn't available in the hub
if (message.data === 'cross-storage:unavailable') {
if (!client._closed) client.close();
if (!client._requests.connect) return;
error = new Error('Closing client. Could not access localStorage in hub.');
for (i = 0; i < client._requests.connect.length; i++) {
client._requests.connect[i](error);
}
return;
}
// Handle initial connection
if (message.data.indexOf('cross-storage:') !== -1 && !client._connected) {
client._connected = true;
if (!client._requests.connect) return;
for (i = 0; i < client._requests.connect.length; i++) {
client._requests.connect[i](error);
}
delete client._requests.connect;
}
if (message.data === 'cross-storage:ready') return;
// All other messages
try {
response = JSON.parse(message.data);
} catch(e) {
return;
}
if (!response.id) return;
if (client._requests[response.id]) {
client._requests[response.id](response.error, response.result);
}
};
// Support IE8 with attachEvent
if (window.addEventListener) {
window.addEventListener('message', this._listener, false);
} else {
window.attachEvent('onmessage', this._listener);
}
};
/**
* Invoked when a frame id was passed to the client, rather than allowing
* the client to create its own iframe. Polls the hub for a ready event to
* establish a connected state.
*/
CrossStorageClient.prototype._poll = function() {
var client, interval;
client = this;
interval = setInterval(function() {
if (client._connected) return clearInterval(interval);
if (!client._hub) return;
client._hub.postMessage('cross-storage:poll', client._origin);
}, 1000);
};
/**
* Creates a new iFrame containing the hub. Applies the necessary styles to
* hide the element from view, prior to adding it to the document body.
* Returns the created element.
*
* @private
*
* @param {string} url The url to the hub
* returns {HTMLIFrameElement} The iFrame element itself
*/
CrossStorageClient.prototype._createFrame = function(url) {
var frame, key;
frame = window.document.createElement('iframe');
frame.id = this._frameId;
// Style the iframe
for (key in CrossStorageClient.frameStyle) {
if (CrossStorageClient.frameStyle.hasOwnProperty(key)) {
frame.style[key] = CrossStorageClient.frameStyle[key];
}
}
window.document.body.appendChild(frame);
frame.src = url;
return frame;
};
/**
* Sends a message containing the given method and params to the hub. Stores
* a callback in the _requests object for later invocation on message, or
* deletion on timeout. Returns a promise that is settled in either instance.
*
* @private
*
* @param {string} method The method to invoke
* @param {*} params The arguments to pass
* @returns {Promise} A promise that is settled on hub response or timeout
*/
CrossStorageClient.prototype._request = function(method, params) {
var req, client;
if (this._closed) {
return this._promise.reject(new Error('CrossStorageClient has closed'));
}
client = this;
client._count++;
req = {
id: this._id + ':' + client._count,
method: 'cross-storage:' + method,
params: params
};
return new this._promise(function(resolve, reject) {
var timeout, originalToJSON;
// Timeout if a response isn't received after 4s
timeout = setTimeout(function() {
if (!client._requests[req.id]) return;
delete client._requests[req.id];
reject(new Error('Timeout: could not perform ' + req.method));
}, client._timeout);
// Add request callback
client._requests[req.id] = function(err, result) {
clearTimeout(timeout);
if (err) return reject(new Error(err));
resolve(result);
};
// In case we have a broken Array.prototype.toJSON, e.g. because of
// old versions of prototype
if (Array.prototype.toJSON) {
originalToJSON = Array.prototype.toJSON;
Array.prototype.toJSON = null;
}
// Send serialized message
client._hub.postMessage(JSON.stringify(req), client._origin);
// Restore original toJSON
if (originalToJSON) {
Array.prototype.toJSON = originalToJSON;
}
});
};