@ohos-ports/cross-storage 1.0.0-beta.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.
package/lib/client.js ADDED
@@ -0,0 +1,629 @@
1
+ ;(function(root) {
2
+ /**
3
+ * Constructs a new cross storage client given the url to a hub. By default,
4
+ * an iframe is created within the document body that points to the url. It
5
+ * also accepts an options object, which may include a timeout, frameId, and
6
+ * promise. The timeout, in milliseconds, is applied to each request and
7
+ * defaults to 5000ms. The options object may also include a frameId,
8
+ * identifying an existing frame on which to install its listeners. If the
9
+ * promise key is supplied the constructor for a Promise, that Promise library
10
+ * will be used instead of the default window.Promise.
11
+ *
12
+ * @example
13
+ * var storage = new CrossStorageClient('https://store.example.com/hub.html');
14
+ *
15
+ * @example
16
+ * var storage = new CrossStorageClient('https://store.example.com/hub.html', {
17
+ * timeout: 5000,
18
+ * frameId: 'storageFrame'
19
+ * });
20
+ *
21
+ * @constructor
22
+ *
23
+ * @param {string} url The url to a cross storage hub
24
+ * @param {object} [opts] An optional object containing additional options,
25
+ * including timeout, frameId, and promise
26
+ *
27
+ * @property {string} _id A UUID v4 id
28
+ * @property {function} _promise The Promise object to use
29
+ * @property {string} _frameId The id of the iFrame pointing to the hub url
30
+ * @property {string} _origin The hub's origin
31
+ * @property {object} _requests Mapping of request ids to callbacks
32
+ * @property {bool} _connected Whether or not it has connected
33
+ * @property {bool} _closed Whether or not the client has closed
34
+ * @property {int} _count Number of requests sent
35
+ * @property {function} _listener The listener added to the window
36
+ * @property {Window} _hub The hub window
37
+ */
38
+ function CrossStorageClient(url, opts) {
39
+ opts = opts || {};
40
+
41
+ this._id = CrossStorageClient._generateUUID();
42
+ this._promise = opts.promise || Promise;
43
+ this._frameId = opts.frameId || 'CrossStorageClient-' + this._id;
44
+ this._origin = CrossStorageClient._getOrigin(url);
45
+ this._requests = {};
46
+ this._connected = false;
47
+ this._closed = false;
48
+ this._count = 0;
49
+ this._timeout = opts.timeout || 5000;
50
+ this._listener = null;
51
+
52
+ // OHOS/Node.js in-process mode: skip iframe, use hub directly
53
+ if (CrossStorageClient._isNodeEnvironment()) {
54
+ this._initInProcessHub(url, opts);
55
+ return;
56
+ }
57
+
58
+ this._installListener();
59
+
60
+ var frame;
61
+ if (opts.frameId) {
62
+ frame = document.getElementById(opts.frameId);
63
+ }
64
+
65
+ // If using a passed iframe, poll the hub for a ready message
66
+ if (frame) {
67
+ this._poll();
68
+ }
69
+
70
+ // Create the frame if not found or specified
71
+ frame = frame || this._createFrame(url);
72
+ this._hub = frame.contentWindow;
73
+ }
74
+
75
+ /**
76
+ * The styles to be applied to the generated iFrame. Defines a set of properties
77
+ * that hide the element by positioning it outside of the visible area, and
78
+ * by modifying its display.
79
+ *
80
+ * @member {Object}
81
+ */
82
+ CrossStorageClient.frameStyle = {
83
+ display: 'none',
84
+ position: 'absolute',
85
+ top: '-999px',
86
+ left: '-999px'
87
+ };
88
+
89
+ /**
90
+ * Detects whether we are running in a Node.js/OHOS environment (no browser).
91
+ *
92
+ * @returns {boolean} True if running in Node.js/OHOS
93
+ * @private
94
+ */
95
+ CrossStorageClient._isNodeEnvironment = function() {
96
+ return typeof window === 'undefined' || typeof document === 'undefined';
97
+ };
98
+
99
+ /**
100
+ * In-process localStorage implementation for Node.js/OHOS environments.
101
+ * Uses a simple Map to store key-value pairs.
102
+ *
103
+ * @returns {object} A localStorage-compatible object
104
+ * @private
105
+ */
106
+ CrossStorageClient._createLocalStorage = function() {
107
+ var store = {};
108
+ return {
109
+ getItem: function(key) {
110
+ return Object.prototype.hasOwnProperty.call(store, key) ? store[key] : null;
111
+ },
112
+ setItem: function(key, value) {
113
+ store[key] = String(value);
114
+ },
115
+ removeItem: function(key) {
116
+ delete store[key];
117
+ },
118
+ clear: function() {
119
+ store = {};
120
+ },
121
+ key: function(index) {
122
+ var keys = Object.keys(store);
123
+ return index < keys.length ? keys[index] : null;
124
+ },
125
+ get length() {
126
+ return Object.keys(store).length;
127
+ }
128
+ };
129
+ };
130
+
131
+ /**
132
+ * Initializes an in-process hub for Node.js/OHOS environments.
133
+ * Instead of creating an iframe and using postMessage, this directly
134
+ * instantiates the CrossStorageHub with a mock localStorage and
135
+ * wires the client to call hub methods directly.
136
+ *
137
+ * @param {string} url The url to a cross storage hub (used for origin)
138
+ * @param {object} opts Options object
139
+ * @private
140
+ */
141
+ CrossStorageClient.prototype._initInProcessHub = function(url, opts) {
142
+ var client = this;
143
+
144
+ // Create in-process localStorage
145
+ var localStorage = CrossStorageClient._createLocalStorage();
146
+
147
+ // Try to load CrossStorageHub
148
+ var CrossStorageHub;
149
+ try {
150
+ CrossStorageHub = require('./hub.js');
151
+ } catch (e) {
152
+ // If hub.js can't be loaded, create a minimal in-process hub
153
+ CrossStorageHub = null;
154
+ }
155
+
156
+ if (CrossStorageHub) {
157
+ // Initialize the hub with permissive permissions and our localStorage
158
+ // We patch the hub's localStorage reference
159
+ CrossStorageHub._storage = localStorage;
160
+ CrossStorageHub.init([
161
+ {origin: /.*/, allow: ['get', 'set', 'del', 'clear', 'getKeys']}
162
+ ]);
163
+ this._inProcessHub = CrossStorageHub;
164
+ } else {
165
+ // Create a minimal in-process hub implementation
166
+ this._inProcessHub = {
167
+ _storage: localStorage,
168
+ _set: function(params) {
169
+ localStorage.setItem(params.key, params.value);
170
+ },
171
+ _get: function(params) {
172
+ var result = [];
173
+ for (var i = 0; i < params.keys.length; i++) {
174
+ result.push(localStorage.getItem(params.keys[i]));
175
+ }
176
+ return result.length > 1 ? result : result[0];
177
+ },
178
+ _del: function(params) {
179
+ for (var i = 0; i < params.keys.length; i++) {
180
+ localStorage.removeItem(params.keys[i]);
181
+ }
182
+ },
183
+ _clear: function() {
184
+ localStorage.clear();
185
+ },
186
+ _getKeys: function() {
187
+ var keys = [];
188
+ for (var i = 0; i < localStorage.length; i++) {
189
+ keys.push(localStorage.key(i));
190
+ }
191
+ return keys;
192
+ }
193
+ };
194
+ }
195
+
196
+ // Create a mock hub window for compatibility
197
+ this._hub = {
198
+ postMessage: function() {} // no-op in in-process mode
199
+ };
200
+
201
+ // Mark as connected immediately since hub is in-process
202
+ this._connected = true;
203
+ };
204
+
205
+ /**
206
+ * Returns the origin of an url, with cross browser support. Accommodates
207
+ * the lack of location.origin in IE, as well as the discrepancies in the
208
+ * inclusion of the port when using the default port for a protocol, e.g.
209
+ * 443 over https. Defaults to the origin of window.location if passed a
210
+ * relative path.
211
+ *
212
+ * @param {string} url The url to a cross storage hub
213
+ * @returns {string} The origin of the url
214
+ */
215
+ CrossStorageClient._getOrigin = function(url) {
216
+ var uri, protocol, origin;
217
+
218
+ // Node.js/OHOS path: use URL parsing without document.createElement
219
+ if (CrossStorageClient._isNodeEnvironment()) {
220
+ try {
221
+ var parsed = new URL(url, 'http://localhost');
222
+ origin = parsed.protocol + '//' + parsed.host;
223
+ origin = origin.replace(/:80$|:443$/, '');
224
+ return origin;
225
+ } catch (e) {
226
+ return 'http://localhost';
227
+ }
228
+ }
229
+
230
+ uri = document.createElement('a');
231
+ uri.href = url;
232
+
233
+ if (!uri.host) {
234
+ uri = window.location;
235
+ }
236
+
237
+ if (!uri.protocol || uri.protocol === ':') {
238
+ protocol = window.location.protocol;
239
+ } else {
240
+ protocol = uri.protocol;
241
+ }
242
+
243
+ origin = protocol + '//' + uri.host;
244
+ origin = origin.replace(/:80$|:443$/, '');
245
+
246
+ return origin;
247
+ };
248
+
249
+ /**
250
+ * UUID v4 generation, taken from: http://stackoverflow.com/questions/
251
+ * 105034/how-to-create-a-guid-uuid-in-javascript/2117523#2117523
252
+ *
253
+ * @returns {string} A UUID v4 string
254
+ */
255
+ CrossStorageClient._generateUUID = function() {
256
+ return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
257
+ var r = Math.random() * 16|0, v = c == 'x' ? r : (r&0x3|0x8);
258
+
259
+ return v.toString(16);
260
+ });
261
+ };
262
+
263
+ /**
264
+ * Returns a promise that is fulfilled when a connection has been established
265
+ * with the cross storage hub. Its use is required to avoid sending any
266
+ * requests prior to initialization being complete.
267
+ *
268
+ * @returns {Promise} A promise that is resolved on connect
269
+ */
270
+ CrossStorageClient.prototype.onConnect = function() {
271
+ var client = this;
272
+
273
+ // In-process mode: already connected
274
+ if (this._inProcessHub) {
275
+ if (this._connected) {
276
+ return this._promise.resolve();
277
+ } else if (this._closed) {
278
+ return this._promise.reject(new Error('CrossStorageClient has closed'));
279
+ }
280
+ }
281
+
282
+ if (this._connected) {
283
+ return this._promise.resolve();
284
+ } else if (this._closed) {
285
+ return this._promise.reject(new Error('CrossStorageClient has closed'));
286
+ }
287
+
288
+ // Queue connect requests for client re-use
289
+ if (!this._requests.connect) {
290
+ this._requests.connect = [];
291
+ }
292
+
293
+ return new this._promise(function(resolve, reject) {
294
+ var timeout = setTimeout(function() {
295
+ reject(new Error('CrossStorageClient could not connect'));
296
+ }, client._timeout);
297
+
298
+ client._requests.connect.push(function(err) {
299
+ clearTimeout(timeout);
300
+ if (err) return reject(err);
301
+
302
+ resolve();
303
+ });
304
+ });
305
+ };
306
+
307
+ /**
308
+ * Sets a key to the specified value. Returns a promise that is fulfilled on
309
+ * success, or rejected if any errors setting the key occurred, or the request
310
+ * timed out.
311
+ *
312
+ * @param {string} key The key to set
313
+ * @param {*} value The value to assign
314
+ * @returns {Promise} A promise that is settled on hub response or timeout
315
+ */
316
+ CrossStorageClient.prototype.set = function(key, value) {
317
+ return this._request('set', {
318
+ key: key,
319
+ value: value
320
+ });
321
+ };
322
+
323
+ /**
324
+ * Accepts one or more keys for which to retrieve their values. Returns a
325
+ * promise that is settled on hub response or timeout. On success, it is
326
+ * fulfilled with the value of the key if only passed a single argument.
327
+ * Otherwise it's resolved with an array of values. On failure, it is rejected
328
+ * with the corresponding error message.
329
+ *
330
+ * @param {...string} key The key to retrieve
331
+ * @returns {Promise} A promise that is settled on hub response or timeout
332
+ */
333
+ CrossStorageClient.prototype.get = function(key) {
334
+ var args = Array.prototype.slice.call(arguments);
335
+
336
+ return this._request('get', {keys: args});
337
+ };
338
+
339
+ /**
340
+ * Accepts one or more keys for deletion. Returns a promise that is settled on
341
+ * hub response or timeout.
342
+ *
343
+ * @param {...string} key The key to delete
344
+ * @returns {Promise} A promise that is settled on hub response or timeout
345
+ */
346
+ CrossStorageClient.prototype.del = function() {
347
+ var args = Array.prototype.slice.call(arguments);
348
+
349
+ return this._request('del', {keys: args});
350
+ };
351
+
352
+ /**
353
+ * Returns a promise that, when resolved, indicates that all localStorage
354
+ * data has been cleared.
355
+ *
356
+ * @returns {Promise} A promise that is settled on hub response or timeout
357
+ */
358
+ CrossStorageClient.prototype.clear = function() {
359
+ return this._request('clear');
360
+ };
361
+
362
+ /**
363
+ * Returns a promise that, when resolved, passes an array of all keys
364
+ * currently in storage.
365
+ *
366
+ * @returns {Promise} A promise that is settled on hub response or timeout
367
+ */
368
+ CrossStorageClient.prototype.getKeys = function() {
369
+ return this._request('getKeys');
370
+ };
371
+
372
+ /**
373
+ * Deletes the iframe and sets the connected state to false. The client can
374
+ * no longer be used after being invoked.
375
+ */
376
+ CrossStorageClient.prototype.close = function() {
377
+ // In-process mode: no iframe to remove
378
+ if (this._inProcessHub) {
379
+ this._connected = false;
380
+ this._closed = true;
381
+ return;
382
+ }
383
+
384
+ var frame = document.getElementById(this._frameId);
385
+ if (frame) {
386
+ frame.parentNode.removeChild(frame);
387
+ }
388
+
389
+ // Support IE8 with detachEvent
390
+ if (window.removeEventListener) {
391
+ window.removeEventListener('message', this._listener, false);
392
+ } else {
393
+ window.detachEvent('onmessage', this._listener);
394
+ }
395
+
396
+ this._connected = false;
397
+ this._closed = true;
398
+ };
399
+
400
+ /**
401
+ * Installs the necessary listener for the window message event. When a message
402
+ * is received, the client's _connected status is changed to true, and the
403
+ * onConnect promise is fulfilled. Given a response message, the callback
404
+ * corresponding to its request is invoked. If response.error holds a truthy
405
+ * value, the promise associated with the original request is rejected with
406
+ * the error. Otherwise the promise is fulfilled and passed response.result.
407
+ *
408
+ * @private
409
+ */
410
+ CrossStorageClient.prototype._installListener = function() {
411
+ // In-process mode: no message listener needed
412
+ if (this._inProcessHub) {
413
+ return;
414
+ }
415
+
416
+ var client = this;
417
+
418
+ this._listener = function(message) {
419
+ var i, origin, error, response;
420
+
421
+ // Ignore invalid messages or those after the client has closed
422
+ if (client._closed || !message.data || typeof message.data !== 'string') {
423
+ return;
424
+ }
425
+
426
+ // postMessage returns the string "null" as the origin for "file://"
427
+ origin = (message.origin === 'null') ? 'file://' : message.origin;
428
+
429
+ // Ignore messages not from the correct origin
430
+ if (origin !== client._origin) return;
431
+
432
+ // LocalStorage isn't available in the hub
433
+ if (message.data === 'cross-storage:unavailable') {
434
+ if (!client._closed) client.close();
435
+ if (!client._requests.connect) return;
436
+
437
+ error = new Error('Closing client. Could not access localStorage in hub.');
438
+ for (i = 0; i < client._requests.connect.length; i++) {
439
+ client._requests.connect[i](error);
440
+ }
441
+
442
+ return;
443
+ }
444
+
445
+ // Handle initial connection
446
+ if (message.data.indexOf('cross-storage:') !== -1 && !client._connected) {
447
+ client._connected = true;
448
+ if (!client._requests.connect) return;
449
+
450
+ for (i = 0; i < client._requests.connect.length; i++) {
451
+ client._requests.connect[i](error);
452
+ }
453
+ delete client._requests.connect;
454
+ }
455
+
456
+ if (message.data === 'cross-storage:ready') return;
457
+
458
+ // All other messages
459
+ try {
460
+ response = JSON.parse(message.data);
461
+ } catch(e) {
462
+ return;
463
+ }
464
+
465
+ if (!response.id) return;
466
+
467
+ if (client._requests[response.id]) {
468
+ client._requests[response.id](response.error, response.result);
469
+ }
470
+ };
471
+
472
+ // Support IE8 with attachEvent
473
+ if (window.addEventListener) {
474
+ window.addEventListener('message', this._listener, false);
475
+ } else {
476
+ window.attachEvent('onmessage', this._listener);
477
+ }
478
+ };
479
+
480
+ /**
481
+ * Invoked when a frame id was passed to the client, rather than allowing
482
+ * the client to create its own iframe. Polls the hub for a ready event to
483
+ * establish a connected state.
484
+ */
485
+ CrossStorageClient.prototype._poll = function() {
486
+ var client, interval, targetOrigin;
487
+
488
+ client = this;
489
+
490
+ // postMessage requires that the target origin be set to "*" for "file://"
491
+ targetOrigin = (client._origin === 'file://') ? '*' : client._origin;
492
+
493
+ interval = setInterval(function() {
494
+ if (client._connected) return clearInterval(interval);
495
+ if (!client._hub) return;
496
+
497
+ client._hub.postMessage('cross-storage:poll', targetOrigin);
498
+ }, 1000);
499
+ };
500
+
501
+ /**
502
+ * Creates a new iFrame containing the hub. Applies the necessary styles to
503
+ * hide the element from view, prior to adding it to the document body.
504
+ * Returns the created element.
505
+ *
506
+ * @private
507
+ *
508
+ * @param {string} url The url to the hub
509
+ * returns {HTMLIFrameElement} The iFrame element itself
510
+ */
511
+ CrossStorageClient.prototype._createFrame = function(url) {
512
+ var frame, key;
513
+
514
+ frame = window.document.createElement('iframe');
515
+ frame.id = this._frameId;
516
+
517
+ // Style the iframe
518
+ for (key in CrossStorageClient.frameStyle) {
519
+ if (CrossStorageClient.frameStyle.hasOwnProperty(key)) {
520
+ frame.style[key] = CrossStorageClient.frameStyle[key];
521
+ }
522
+ }
523
+
524
+ window.document.body.appendChild(frame);
525
+ frame.src = url;
526
+
527
+ return frame;
528
+ };
529
+
530
+ /**
531
+ * Sends a message containing the given method and params to the hub. Stores
532
+ * a callback in the _requests object for later invocation on message, or
533
+ * deletion on timeout. Returns a promise that is settled in either instance.
534
+ *
535
+ * In in-process mode (Node.js/OHOS), this directly calls the hub methods
536
+ * instead of using postMessage.
537
+ *
538
+ * @private
539
+ *
540
+ * @param {string} method The method to invoke
541
+ * @param {*} params The arguments to pass
542
+ * @returns {Promise} A promise that is settled on hub response or timeout
543
+ */
544
+ CrossStorageClient.prototype._request = function(method, params) {
545
+ var req, client;
546
+
547
+ if (this._closed) {
548
+ return this._promise.reject(new Error('CrossStorageClient has closed'));
549
+ }
550
+
551
+ // In-process mode: directly call hub methods
552
+ if (this._inProcessHub) {
553
+ client = this;
554
+ var hubMethod = method;
555
+ var hubFn = this._inProcessHub['_' + hubMethod];
556
+
557
+ return new this._promise(function(resolve, reject) {
558
+ try {
559
+ var result = hubFn.call(client._inProcessHub, params || {});
560
+ resolve(result);
561
+ } catch (err) {
562
+ reject(new Error(err.message));
563
+ }
564
+ });
565
+ }
566
+
567
+ client = this;
568
+ client._count++;
569
+
570
+ req = {
571
+ id: this._id + ':' + client._count,
572
+ method: 'cross-storage:' + method,
573
+ params: params
574
+ };
575
+
576
+ return new this._promise(function(resolve, reject) {
577
+ var timeout, originalToJSON, targetOrigin;
578
+
579
+ // Timeout if a response isn't received after 4s
580
+ timeout = setTimeout(function() {
581
+ if (!client._requests[req.id]) return;
582
+
583
+ delete client._requests[req.id];
584
+ reject(new Error('Timeout: could not perform ' + req.method));
585
+ }, client._timeout);
586
+
587
+ // Add request callback
588
+ client._requests[req.id] = function(err, result) {
589
+ clearTimeout(timeout);
590
+ delete client._requests[req.id];
591
+ if (err) return reject(new Error(err));
592
+ resolve(result);
593
+ };
594
+
595
+ // In case we have a broken Array.prototype.toJSON, e.g. because of
596
+ // old versions of prototype
597
+ if (Array.prototype.toJSON) {
598
+ originalToJSON = Array.prototype.toJSON;
599
+ Array.prototype.toJSON = null;
600
+ }
601
+
602
+ // postMessage requires that the target origin be set to "*" for "file://"
603
+ targetOrigin = (client._origin === 'file://') ? '*' : client._origin;
604
+
605
+ // Send serialized message
606
+ client._hub.postMessage(JSON.stringify(req), targetOrigin);
607
+
608
+ // Restore original toJSON
609
+ if (originalToJSON) {
610
+ Array.prototype.toJSON = originalToJSON;
611
+ }
612
+ });
613
+ };
614
+
615
+ /**
616
+ * Export for various environments.
617
+ */
618
+ if (typeof module !== 'undefined' && module.exports) {
619
+ module.exports = CrossStorageClient;
620
+ } else if (typeof exports !== 'undefined') {
621
+ exports.CrossStorageClient = CrossStorageClient;
622
+ } else if (typeof define === 'function' && define.amd) {
623
+ define([], function() {
624
+ return CrossStorageClient;
625
+ });
626
+ } else {
627
+ root.CrossStorageClient = CrossStorageClient;
628
+ }
629
+ }(typeof global !== 'undefined' ? global : this));