Commit c86d1ae7 authored by Aurel's avatar Aurel

Merge branch 'master' of https://lab.nexedi.com/nexedi/jio into nodejs

Conflicts:
	src/jio.storage/replicatestorage.js
	test/jio.storage/replicatestorage.tests.js
parents caf85ce9 d83a1306
# purpose of placing this file is to allow this folder to
# accessible for anonymous users inside testnode's apache
# frontend.
# Without this file by automatic unit testing will not work
Require all granted
......@@ -41,7 +41,7 @@ each method, please refer to the documentation):
```javascript
// create and store new document
jio_instance.put({"title": "some title"})
jio_instance.post({"title": "some title"})
.then(function (new_id) {
...
});
......
......@@ -7458,11 +7458,17 @@ return new Parser;
};
}(window, moment));
;/*global window, RSVP, Blob, XMLHttpRequest, QueryFactory, Query, atob,
FileReader, ArrayBuffer, Uint8Array */
FileReader, ArrayBuffer, Uint8Array, navigator */
(function (window, RSVP, Blob, QueryFactory, Query, atob,
FileReader, ArrayBuffer, Uint8Array) {
FileReader, ArrayBuffer, Uint8Array, navigator) {
"use strict";
if (window.openDatabase === undefined) {
window.openDatabase = function () {
throw new Error('WebSQL is not supported by ' + navigator.userAgent);
};
}
var util = {},
jIO;
......@@ -7569,8 +7575,46 @@ return new Parser;
}
util.readBlobAsDataURL = readBlobAsDataURL;
function stringify(obj) {
// Implement a stable JSON.stringify
// Object's keys are alphabetically ordered
var key,
key_list,
i,
value,
result_list;
if (obj === undefined) {
return undefined;
}
if (obj.constructor === Object) {
key_list = Object.keys(obj).sort();
result_list = [];
for (i = 0; i < key_list.length; i += 1) {
key = key_list[i];
value = stringify(obj[key]);
if (value !== undefined) {
result_list.push(stringify(key) + ':' + value);
}
}
return '{' + result_list.join(',') + '}';
}
if (obj.constructor === Array) {
result_list = [];
for (i = 0; i < obj.length; i += 1) {
result_list.push(stringify(obj[i]));
}
return '[' + result_list.join(',') + ']';
}
return JSON.stringify(obj);
}
util.stringify = stringify;
// https://gist.github.com/davoclavo/4424731
function dataURItoBlob(dataURI) {
if (dataURI === 'data:') {
return new Blob();
}
// convert base64 to raw binary data held in a string
var byteString = atob(dataURI.split(',')[1]),
// separate out the mime component
......@@ -7931,7 +7975,7 @@ return new Parser;
window.jIO = jIO;
}(window, RSVP, Blob, QueryFactory, Query, atob,
FileReader, ArrayBuffer, Uint8Array));
FileReader, ArrayBuffer, Uint8Array, navigator));
;/*
* Rusha, a JavaScript implementation of the Secure Hash Algorithm, SHA-1,
* as defined in FIPS PUB 180-1, tuned for high performance with large inputs.
......@@ -8370,7 +8414,7 @@ return new Parser;
/*jslint nomen: true*/
/*global jIO, RSVP, Rusha*/
(function (jIO, RSVP, Rusha) {
(function (jIO, RSVP, Rusha, stringify) {
"use strict";
var rusha = new Rusha(),
......@@ -8397,14 +8441,14 @@ return new Parser;
this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage);
this._signature_hash = "_replicate_" + generateHash(
JSON.stringify(spec.local_sub_storage) +
JSON.stringify(spec.remote_sub_storage) +
JSON.stringify(this._query_options)
stringify(spec.local_sub_storage) +
stringify(spec.remote_sub_storage) +
stringify(this._query_options)
);
this._signature_sub_storage = jIO.createJIO({
type: "document",
document_id: this._signature_hash,
sub_storage: spec.local_sub_storage
sub_storage: spec.signature_storage || spec.local_sub_storage
});
this._use_remote_post = spec.use_remote_post || false;
......@@ -8540,40 +8584,45 @@ return new Parser;
});
}
function checkLocalCreation(queue, source, destination, id, options,
getMethod) {
var remote_doc;
queue
function propagateDeletion(destination, id) {
return destination.remove(id)
.push(function () {
return destination.get(id);
return context._signature_sub_storage.remove(id);
})
.push(function (doc) {
remote_doc = doc;
.push(function () {
skip_document_dict[id] = null;
});
}
function checkAndPropagate(status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options) {
return destination.get(id)
.push(function (remote_doc) {
return [remote_doc, generateHash(stringify(remote_doc))];
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// This document was never synced.
// Push it to the remote storage and store sync information
return;
return [null, null];
}
throw error;
})
.push(function (remote_list) {
var remote_doc = remote_list[0],
remote_hash = remote_list[1];
if (local_hash === remote_hash) {
// Same modifications on both side
if (local_hash === null) {
// Deleted on both side, drop signature
return context._signature_sub_storage.remove(id)
.push(function () {
// This document was never synced.
// Push it to the remote storage and store sync information
return getMethod(id);
})
.push(function (doc) {
var local_hash = generateHash(JSON.stringify(doc)),
remote_hash;
if (remote_doc === undefined) {
return propagateModification(source, destination, doc, local_hash,
id, options);
skip_document_dict[id] = null;
});
}
remote_hash = generateHash(JSON.stringify(remote_doc));
if (local_hash === remote_hash) {
// Same document
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
......@@ -8581,50 +8630,58 @@ return new Parser;
skip_document_dict[id] = null;
});
}
if (options.conflict_ignore === true) {
return;
}
if (options.conflict_force === true) {
return propagateModification(source, destination, doc, local_hash,
id, options);
if ((remote_hash === status_hash) || (conflict_force === true)) {
// Modified only locally. No conflict or force
if (local_hash === null) {
// Deleted locally
return propagateDeletion(destination, id);
}
// Already exists on destination
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
JSON.stringify(doc) + " !== " +
JSON.stringify(remote_doc),
409);
});
return propagateModification(source, destination, doc,
local_hash, id,
{use_post: ((options.use_post) &&
(remote_hash === null))});
}
function checkBulkLocalCreation(queue, source, destination, id_list,
options) {
queue
.push(function () {
return source.bulk(id_list);
})
.push(function (result_list) {
var i,
sub_queue = new RSVP.Queue();
// Conflict cases
if (conflict_ignore === true) {
return;
}
function getResult(j) {
return function (id) {
if (id !== id_list[j].parameter_list[0]) {
throw new Error("Does not access expected ID " + id);
if ((conflict_revert === true) || (local_hash === null)) {
// Automatically resolve conflict or force revert
if (remote_hash === null) {
// Deleted remotely
return propagateDeletion(source, id);
}
return result_list[j];
};
return propagateModification(
destination,
source,
remote_doc,
remote_hash,
id,
{use_post: ((options.use_revert_post) &&
(local_hash === null))}
);
}
for (i = 0; i < result_list.length; i += 1) {
checkLocalCreation(sub_queue, source, destination,
id_list[i].parameter_list[0],
options, getResult(i));
// Minimize conflict if it can be resolved
if (remote_hash === null) {
// Copy remote modification remotely
return propagateModification(source, destination, doc,
local_hash, id,
{use_post: options.use_post});
}
return sub_queue;
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
stringify(doc || '') + " !== " +
stringify(remote_doc || ''),
409);
});
}
function checkLocalDeletion(queue, destination, id, source) {
function checkLocalDeletion(queue, destination, id, source,
conflict_force, conflict_revert,
conflict_ignore, options) {
var status_hash;
queue
.push(function () {
......@@ -8632,93 +8689,57 @@ return new Parser;
})
.push(function (result) {
status_hash = result.hash;
return destination.get(id)
.push(function (doc) {
var remote_hash = generateHash(JSON.stringify(doc));
if (remote_hash === status_hash) {
return destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
})
.push(function () {
skip_document_dict[id] = null;
});
}
// Modifications on remote side
// Push them locally
return propagateModification(destination, source, doc,
remote_hash, id);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._signature_sub_storage.remove(id)
.push(function () {
skip_document_dict[id] = null;
});
}
throw error;
});
return checkAndPropagate(status_hash, null, null,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
});
}
function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_ignore,
getMethod) {
conflict_force, conflict_revert,
conflict_ignore,
is_creation, is_modification,
getMethod, options) {
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([
getMethod(id),
{hash: null}
]);
}
if (is_modification === true) {
return RSVP.all([
getMethod(id),
context._signature_sub_storage.get(id)
]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkSignatureDifference",
409);
})
.push(function (result_list) {
var doc = result_list[0],
local_hash = generateHash(JSON.stringify(doc)),
local_hash = generateHash(stringify(doc)),
status_hash = result_list[1].hash;
if (local_hash !== status_hash) {
// Local modifications
return destination.get(id)
.push(function (remote_doc) {
var remote_hash = generateHash(JSON.stringify(remote_doc));
if (remote_hash !== status_hash) {
// Modifications on both sides
if (local_hash === remote_hash) {
// Same modifications on both side \o/
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
.push(function () {
skip_document_dict[id] = null;
});
}
if (conflict_ignore === true) {
return;
}
if (conflict_force !== true) {
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
JSON.stringify(doc) + " !== " +
JSON.stringify(remote_doc),
409);
}
}
return propagateModification(source, destination, doc,
local_hash, id);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// Document has been deleted remotely
return propagateModification(source, destination, doc,
local_hash, id);
}
throw error;
});
return checkAndPropagate(status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
}
});
}
function checkBulkSignatureDifference(queue, source, destination, id_list,
conflict_force, conflict_ignore) {
document_status_list, options,
conflict_force, conflict_revert,
conflict_ignore) {
queue
.push(function () {
return source.bulk(id_list);
......@@ -8739,8 +8760,11 @@ return new Parser;
for (i = 0; i < result_list.length; i += 1) {
checkSignatureDifference(sub_queue, source, destination,
id_list[i].parameter_list[0],
conflict_force, conflict_ignore,
getResult(i));
conflict_force, conflict_revert,
conflict_ignore,
document_status_list[i].is_creation,
document_status_list[i].is_modification,
getResult(i), options);
}
return sub_queue;
});
......@@ -8751,6 +8775,9 @@ return new Parser;
if (!options.hasOwnProperty("use_post")) {
options.use_post = false;
}
if (!options.hasOwnProperty("use_revert_post")) {
options.use_revert_post = false;
}
return queue
.push(function () {
return RSVP.all([
......@@ -8761,9 +8788,11 @@ return new Parser;
.push(function (result_list) {
var i,
local_dict = {},
new_list = [],
change_list = [],
document_list = [],
document_status_list = [],
signature_dict = {},
is_modification,
is_creation,
key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
......@@ -8779,55 +8808,53 @@ return new Parser;
signature_dict[result_list[1].data.rows[i].id] = i;
}
}
if (options.check_creation === true) {
for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) {
if (!signature_dict.hasOwnProperty(key)) {
is_modification = signature_dict.hasOwnProperty(key)
&& options.check_modification;
is_creation = !signature_dict.hasOwnProperty(key)
&& options.check_creation;
if (is_modification === true || is_creation === true) {
if (options.use_bulk_get === true) {
new_list.push({
document_list.push({
method: "get",
parameter_list: [key]
});
document_status_list.push({
is_creation: is_creation,
is_modification: is_modification
});
} else {
checkLocalCreation(queue, source, destination, key,
options, source.get.bind(source));
}
}
checkSignatureDifference(queue, source, destination, key,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
is_creation, is_modification,
source.get.bind(source),
options);
}
}
if ((options.use_bulk_get === true) && (new_list.length !== 0)) {
checkBulkLocalCreation(queue, source, destination, new_list,
options);
}
}
if (options.check_deletion === true) {
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) {
if (options.check_modification === true) {
if (options.use_bulk_get === true) {
change_list.push({
method: "get",
parameter_list: [key]
});
} else {
checkSignatureDifference(queue, source, destination, key,
if (!local_dict.hasOwnProperty(key)) {
checkLocalDeletion(queue, destination, key, source,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
source.get.bind(source));
}
}
} else {
if (options.check_deletion === true) {
checkLocalDeletion(queue, destination, key, source);
options);
}
}
}
}
if ((options.use_bulk_get === true) && (change_list.length !== 0)) {
if ((options.use_bulk_get === true) && (document_list.length !== 0)) {
checkBulkSignatureDifference(queue, source, destination,
change_list,
document_list, document_status_list,
options,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore);
}
});
......@@ -8879,10 +8906,10 @@ return new Parser;
use_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: ((context._conflict_handling ===
CONFLICT_CONTINUE) ||
(context._conflict_handling ===
CONFLICT_KEEP_REMOTE)),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_local_modification,
check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion
......@@ -8907,8 +8934,11 @@ return new Parser;
return pushStorage(context._remote_sub_storage,
context._local_sub_storage, {
use_bulk_get: use_bulk_get,
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_remote_modification,
......@@ -8921,7 +8951,7 @@ return new Parser;
jIO.addStorage('replicate', ReplicateStorage);
}(jIO, RSVP, Rusha));
}(jIO, RSVP, Rusha, jIO.util.stringify));
;/*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
......@@ -10898,7 +10928,7 @@ return new Parser;
sub_query,
result_list,
local_roles,
tmp_list = [];
sort_list = [];
if (options.query) {
parsed_query = jIO.QueryFactory.create(options.query);
......@@ -10942,9 +10972,8 @@ return new Parser;
if (options.sort_on) {
for (i = 0; i < options.sort_on.length; i += 1) {
tmp_list.push(JSON.stringify(options.sort_on[i]));
sort_list.push(JSON.stringify(options.sort_on[i]));
}
options.sort_on = tmp_list;
}
return jIO.util.ajax({
......@@ -10955,7 +10984,7 @@ return new Parser;
// XXX Force erp5 to return embedded document
select_list: options.select_list || ["title", "reference"],
limit: options.limit,
sort_on: options.sort_on,
sort_on: sort_list,
local_roles: local_roles
}),
"xhrFields": {
......@@ -11827,9 +11856,11 @@ return new Parser;
*/
/*jslint nomen: true */
/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange*/
/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest,
DOMError, Event*/
(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange) {
(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest,
DOMError) {
"use strict";
// Read only as changing it can lead to data corruption
......@@ -11888,7 +11919,14 @@ return new Parser;
if (request.result) {
request.result.close();
}
if ((error !== undefined) &&
(error.target instanceof IDBOpenDBRequest) &&
(error.target.error instanceof DOMError)) {
reject("Connection to: " + db_name + " failed: " +
error.target.error.message);
} else {
reject(error);
}
};
request.onabort = function () {
......@@ -12249,7 +12287,7 @@ return new Parser;
};
jIO.addStorage("indexeddb", IndexedDBStorage);
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange));
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError));
;/*
* Copyright 2015, Nexedi SA
* Released under the LGPL license.
......
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
This source diff could not be displayed because it is too large. You can view the blob instead.
{
"name": "jio",
"version": "v3.11.0",
"version": "v3.13.0",
"license": "LGPLv3",
"author": "Nexedi SA",
"contributors": [
......@@ -42,6 +42,8 @@
"grunt-contrib-copy": "~0.4.1",
"grunt-contrib-uglify": "0.2.x",
"grunt-contrib-qunit": "~0.3.0",
"qunitjs": "~1.23.1",
"qunit-tap": "1.5.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-jslint": "~1.0.0",
"lz-string": "^1.4.4",
......
......@@ -124,6 +124,9 @@
i,
value,
result_list;
if (obj === undefined) {
return undefined;
}
if (obj.constructor === Object) {
key_list = Object.keys(obj).sort();
result_list = [];
......@@ -150,6 +153,9 @@
// https://gist.github.com/davoclavo/4424731
function dataURItoBlob(dataURI) {
if (dataURI === 'data:') {
return new Blob();
}
// convert base64 to raw binary data held in a string
var byteString = atob(dataURI.split(',')[1]),
// separate out the mime component
......
......@@ -447,7 +447,7 @@
ERP5Storage.prototype.hasCapacity = function (name) {
return ((name === "list") || (name === "query") ||
(name === "select") || (name === "limit") ||
(name === "sort"));
(name === "sort")) || (name === "bulk_get");
};
function isSingleLocalRoles(parsed_query) {
......@@ -496,7 +496,7 @@
sub_query,
result_list,
local_roles,
tmp_list = [];
sort_list = [];
if (options.query) {
parsed_query = jIO.QueryFactory.create(options.query);
......@@ -540,9 +540,8 @@
if (options.sort_on) {
for (i = 0; i < options.sort_on.length; i += 1) {
tmp_list.push(JSON.stringify(options.sort_on[i]));
sort_list.push(JSON.stringify(options.sort_on[i]));
}
options.sort_on = tmp_list;
}
return jIO.util.ajax({
......@@ -553,7 +552,7 @@
// XXX Force erp5 to return embedded document
select_list: options.select_list || ["title", "reference"],
limit: options.limit,
sort_on: options.sort_on,
sort_on: sort_list,
local_roles: local_roles
}),
"xhrFields": {
......
......@@ -28,9 +28,11 @@
*/
/*jslint nomen: true */
/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange*/
/*global indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest,
DOMError, Event*/
(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange) {
(function (indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest,
DOMError) {
"use strict";
// Read only as changing it can lead to data corruption
......@@ -89,7 +91,14 @@
if (request.result) {
request.result.close();
}
if ((error !== undefined) &&
(error.target instanceof IDBOpenDBRequest) &&
(error.target.error instanceof DOMError)) {
reject("Connection to: " + db_name + " failed: " +
error.target.error.message);
} else {
reject(error);
}
};
request.onabort = function () {
......@@ -450,4 +459,4 @@
};
jIO.addStorage("indexeddb", IndexedDBStorage);
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange));
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError));
......@@ -39,6 +39,11 @@
return rusha.digestFromString(content);
}
function generateHashFromArrayBuffer(content) {
// XXX Improve performance by moving calculation to WebWorker
return rusha.digestFromArrayBuffer(content);
}
function ReplicateStorage(spec) {
this._query_options = spec.query || {};
......@@ -53,7 +58,7 @@
this._signature_sub_storage = jIO.createJIO({
type: "document",
document_id: this._signature_hash,
sub_storage: spec.local_sub_storage
sub_storage: spec.signature_storage || spec.local_sub_storage
});
this._use_remote_post = spec.use_remote_post || false;
......@@ -99,6 +104,36 @@
if (this._check_remote_deletion === undefined) {
this._check_remote_deletion = true;
}
this._check_local_attachment_modification =
spec.check_local_attachment_modification;
if (this._check_local_attachment_modification === undefined) {
this._check_local_attachment_modification = false;
}
this._check_local_attachment_creation =
spec.check_local_attachment_creation;
if (this._check_local_attachment_creation === undefined) {
this._check_local_attachment_creation = false;
}
this._check_local_attachment_deletion =
spec.check_local_attachment_deletion;
if (this._check_local_attachment_deletion === undefined) {
this._check_local_attachment_deletion = false;
}
this._check_remote_attachment_modification =
spec.check_remote_attachment_modification;
if (this._check_remote_attachment_modification === undefined) {
this._check_remote_attachment_modification = false;
}
this._check_remote_attachment_creation =
spec.check_remote_attachment_creation;
if (this._check_remote_attachment_creation === undefined) {
this._check_remote_attachment_creation = false;
}
this._check_remote_attachment_deletion =
spec.check_remote_attachment_deletion;
if (this._check_remote_attachment_deletion === undefined) {
this._check_remote_attachment_deletion = false;
}
}
ReplicateStorage.prototype.remove = function (id) {
......@@ -125,6 +160,32 @@
return this._local_sub_storage.get.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.getAttachment = function () {
return this._local_sub_storage.getAttachment.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.allAttachments = function () {
return this._local_sub_storage.allAttachments.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.putAttachment = function (id) {
if (id === this._signature_hash) {
throw new jIO.util.jIOError(this._signature_hash + " is frozen",
403);
}
return this._local_sub_storage.putAttachment.apply(this._local_sub_storage,
arguments);
};
ReplicateStorage.prototype.removeAttachment = function (id) {
if (id === this._signature_hash) {
throw new jIO.util.jIOError(this._signature_hash + " is frozen",
403);
}
return this._local_sub_storage.removeAttachment.apply(
this._local_sub_storage,
arguments
);
};
ReplicateStorage.prototype.hasCapacity = function () {
return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage,
arguments);
......@@ -143,6 +204,333 @@
// Do not sync the signature document
skip_document_dict[context._signature_hash] = null;
function propagateAttachmentDeletion(skip_attachment_dict,
destination,
id, name) {
return destination.removeAttachment(id, name)
.push(function () {
return context._signature_sub_storage.removeAttachment(id, name);
})
.push(function () {
skip_attachment_dict[name] = null;
});
}
function propagateAttachmentModification(skip_attachment_dict,
destination,
blob, hash, id, name) {
return destination.putAttachment(id, name, blob)
.push(function () {
return context._signature_sub_storage.putAttachment(id, name,
JSON.stringify({
hash: hash
}));
})
.push(function () {
skip_attachment_dict[name] = null;
});
}
function checkAndPropagateAttachment(skip_attachment_dict,
status_hash, local_hash, blob,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore) {
var remote_blob;
return destination.getAttachment(id, name)
.push(function (result) {
remote_blob = result;
return jIO.util.readBlobAsArrayBuffer(remote_blob);
})
.push(function (evt) {
return generateHashFromArrayBuffer(
evt.target.result
);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
remote_blob = null;
return null;
}
throw error;
})
.push(function (remote_hash) {
if (local_hash === remote_hash) {
// Same modifications on both side
if (local_hash === null) {
// Deleted on both side, drop signature
return context._signature_sub_storage.removeAttachment(id, name)
.push(function () {
skip_attachment_dict[id] = null;
});
}
return context._signature_sub_storage.putAttachment(id, name,
JSON.stringify({
hash: local_hash
}))
.push(function () {
skip_document_dict[id] = null;
});
}
if ((remote_hash === status_hash) || (conflict_force === true)) {
// Modified only locally. No conflict or force
if (local_hash === null) {
// Deleted locally
return propagateAttachmentDeletion(skip_attachment_dict,
destination,
id, name);
}
return propagateAttachmentModification(skip_attachment_dict,
destination, blob,
local_hash, id, name);
}
// Conflict cases
if (conflict_ignore === true) {
return;
}
if ((conflict_revert === true) || (local_hash === null)) {
// Automatically resolve conflict or force revert
if (remote_hash === null) {
// Deleted remotely
return propagateAttachmentDeletion(skip_attachment_dict,
source, id, name);
}
return propagateAttachmentModification(
skip_attachment_dict,
source,
remote_blob,
remote_hash,
id,
name
);
}
// Minimize conflict if it can be resolved
if (remote_hash === null) {
// Copy remote modification remotely
return propagateAttachmentModification(skip_attachment_dict,
destination, blob,
local_hash, id, name);
}
throw new jIO.util.jIOError("Conflict on '" + id +
"' with attachment '" +
name + "'",
409);
});
}
function checkAttachmentSignatureDifference(skip_attachment_dict,
queue, source,
destination, id, name,
conflict_force,
conflict_revert,
conflict_ignore,
is_creation, is_modification) {
var blob,
status_hash;
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([
source.getAttachment(id, name),
{hash: null}
]);
}
if (is_modification === true) {
return RSVP.all([
source.getAttachment(id, name),
context._signature_sub_storage.getAttachment(
id,
name,
{format: 'json'}
)
]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkAttachmentSignatureDifference",
409);
})
.push(function (result_list) {
blob = result_list[0];
status_hash = result_list[1].hash;
return jIO.util.readBlobAsArrayBuffer(blob);
})
.push(function (evt) {
var array_buffer = evt.target.result,
local_hash = generateHashFromArrayBuffer(array_buffer);
if (local_hash !== status_hash) {
return checkAndPropagateAttachment(skip_attachment_dict,
status_hash, local_hash, blob,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore);
}
});
}
function checkAttachmentLocalDeletion(skip_attachment_dict,
queue, destination, id, name, source,
conflict_force, conflict_revert,
conflict_ignore) {
var status_hash;
queue
.push(function () {
return context._signature_sub_storage.getAttachment(id, name,
{format: 'json'});
})
.push(function (result) {
status_hash = result.hash;
return checkAndPropagateAttachment(skip_attachment_dict,
status_hash, null, null,
source, destination, id, name,
conflict_force, conflict_revert,
conflict_ignore);
});
}
function pushDocumentAttachment(skip_attachment_dict, id, source,
destination, options) {
var queue = new RSVP.Queue();
return queue
.push(function () {
return RSVP.all([
source.allAttachments(id)
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
}),
context._signature_sub_storage.allAttachments(id)
.push(undefined, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return {};
}
throw error;
})
]);
})
.push(function (result_list) {
var local_dict = {},
signature_dict = {},
is_modification,
is_creation,
key;
for (key in result_list[0]) {
if (result_list[0].hasOwnProperty(key)) {
if (!skip_attachment_dict.hasOwnProperty(key)) {
local_dict[key] = null;
}
}
}
for (key in result_list[1]) {
if (result_list[1].hasOwnProperty(key)) {
if (!skip_attachment_dict.hasOwnProperty(key)) {
signature_dict[key] = null;
}
}
}
for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) {
is_modification = signature_dict.hasOwnProperty(key)
&& options.check_modification;
is_creation = !signature_dict.hasOwnProperty(key)
&& options.check_creation;
if (is_modification === true || is_creation === true) {
checkAttachmentSignatureDifference(skip_attachment_dict,
queue, source,
destination, id, key,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
is_creation,
is_modification);
}
}
}
if (options.check_deletion === true) {
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (!local_dict.hasOwnProperty(key)) {
checkAttachmentLocalDeletion(skip_attachment_dict,
queue, destination, id, key,
source,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore);
}
}
}
}
});
}
function repairDocumentAttachment(id) {
var skip_attachment_dict = {};
return new RSVP.Queue()
.push(function () {
if (context._check_local_attachment_modification ||
context._check_local_attachment_creation ||
context._check_local_attachment_deletion) {
return pushDocumentAttachment(
skip_attachment_dict,
id,
context._local_sub_storage,
context._remote_sub_storage,
{
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification:
context._check_local_attachment_modification,
check_creation: context._check_local_attachment_creation,
check_deletion: context._check_local_attachment_deletion
}
);
}
})
.push(function () {
if (context._check_remote_attachment_modification ||
context._check_remote_attachment_creation ||
context._check_remote_attachment_deletion) {
return pushDocumentAttachment(
skip_attachment_dict,
id,
context._remote_sub_storage,
context._local_sub_storage,
{
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification:
context._check_remote_attachment_modification,
check_creation: context._check_remote_attachment_creation,
check_deletion: context._check_remote_attachment_deletion
}
);
}
});
}
function propagateModification(source, destination, doc, hash, id,
options) {
var result,
......@@ -158,6 +546,33 @@
post_id = new_id;
return source.put(post_id, doc);
})
.push(function () {
// Copy all attachments
// This is not related to attachment replication
// It's just about not losing user data
return source.allAttachments(id);
})
.push(function (attachment_dict) {
var key,
copy_queue = new RSVP.Queue();
function copyAttachment(name) {
copy_queue
.push(function () {
return source.getAttachment(id, name);
})
.push(function (blob) {
return source.putAttachment(post_id, name, blob);
});
}
for (key in attachment_dict) {
if (attachment_dict.hasOwnProperty(key)) {
copyAttachment(key);
}
}
return copy_queue;
})
.push(function () {
return source.remove(id);
})
......@@ -189,40 +604,63 @@
});
}
function checkLocalCreation(queue, source, destination, id, options,
getMethod) {
var remote_doc;
queue
function propagateDeletion(destination, id) {
// Do not delete a document if it has an attachment
// ie, replication should prevent losing user data
// Synchronize attachments before, to ensure
// all of them will be deleted too
return repairDocumentAttachment(id)
.push(function () {
return destination.get(id);
return destination.allAttachments(id);
})
.push(function (doc) {
remote_doc = doc;
.push(function (attachment_dict) {
if (JSON.stringify(attachment_dict) === "{}") {
return destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
});
}
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// This document was never synced.
// Push it to the remote storage and store sync information
return;
}
throw error;
})
.push(function () {
// This document was never synced.
// Push it to the remote storage and store sync information
return getMethod(id);
})
.push(function (doc) {
var local_hash = generateHash(stringify(doc)),
remote_hash;
if (remote_doc === undefined) {
return propagateModification(source, destination, doc, local_hash,
id, options);
skip_document_dict[id] = null;
});
}
remote_hash = generateHash(stringify(remote_doc));
function checkAndPropagate(status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options) {
return destination.get(id)
.push(function (remote_doc) {
return [remote_doc, generateHash(stringify(remote_doc))];
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return [null, null];
}
throw error;
})
.push(function (remote_list) {
var remote_doc = remote_list[0],
remote_hash = remote_list[1];
if (local_hash === remote_hash) {
// Same document
// Same modifications on both side
if (local_hash === null) {
// Deleted on both side, drop signature
return context._signature_sub_storage.remove(id)
.push(function () {
skip_document_dict[id] = null;
});
}
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
......@@ -230,50 +668,58 @@
skip_document_dict[id] = null;
});
}
if (options.conflict_ignore === true) {
return;
}
if (options.conflict_force === true) {
return propagateModification(source, destination, doc, local_hash,
id);
if ((remote_hash === status_hash) || (conflict_force === true)) {
// Modified only locally. No conflict or force
if (local_hash === null) {
// Deleted locally
return propagateDeletion(destination, id);
}
// Already exists on destination
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
stringify(doc) + " !== " +
stringify(remote_doc),
409);
});
return propagateModification(source, destination, doc,
local_hash, id,
{use_post: ((options.use_post) &&
(remote_hash === null))});
}
function checkBulkLocalCreation(queue, source, destination, id_list,
options) {
queue
.push(function () {
return source.bulk(id_list);
})
.push(function (result_list) {
var i,
sub_queue = new RSVP.Queue();
function getResult(j) {
return function (id) {
if (id !== id_list[j].parameter_list[0]) {
throw new Error("Does not access expected ID " + id);
// Conflict cases
if (conflict_ignore === true) {
return;
}
return result_list[j];
};
if ((conflict_revert === true) || (local_hash === null)) {
// Automatically resolve conflict or force revert
if (remote_hash === null) {
// Deleted remotely
return propagateDeletion(source, id);
}
return propagateModification(
destination,
source,
remote_doc,
remote_hash,
id,
{use_post: ((options.use_revert_post) &&
(local_hash === null))}
);
}
for (i = 0; i < result_list.length; i += 1) {
checkLocalCreation(sub_queue, source, destination,
id_list[i].parameter_list[0],
options, getResult(i));
// Minimize conflict if it can be resolved
if (remote_hash === null) {
// Copy remote modification remotely
return propagateModification(source, destination, doc,
local_hash, id,
{use_post: options.use_post});
}
return sub_queue;
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
stringify(doc || '') + " !== " +
stringify(remote_doc || ''),
409);
});
}
function checkLocalDeletion(queue, destination, id, source) {
function checkLocalDeletion(queue, destination, id, source,
conflict_force, conflict_revert,
conflict_ignore, options) {
var status_hash;
queue
.push(function () {
......@@ -281,44 +727,37 @@
})
.push(function (result) {
status_hash = result.hash;
return destination.get(id)
.push(function (doc) {
var remote_hash = generateHash(stringify(doc));
if (remote_hash === status_hash) {
return destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
})
.push(function () {
skip_document_dict[id] = null;
});
}
// Modifications on remote side
// Push them locally
return propagateModification(destination, source, doc,
remote_hash, id);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
return context._signature_sub_storage.remove(id)
.push(function () {
skip_document_dict[id] = null;
});
}
throw error;
});
return checkAndPropagate(status_hash, null, null,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
});
}
function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_ignore,
getMethod) {
conflict_force, conflict_revert,
conflict_ignore,
is_creation, is_modification,
getMethod, options) {
queue
.push(function () {
// Optimisation to save a get call to signature storage
if (is_creation === true) {
return RSVP.all([
getMethod(id),
{hash: null}
]);
}
if (is_modification === true) {
return RSVP.all([
getMethod(id),
context._signature_sub_storage.get(id)
]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkSignatureDifference",
409);
})
.push(function (result_list) {
var doc = result_list[0],
......@@ -326,48 +765,19 @@
status_hash = result_list[1].hash;
if (local_hash !== status_hash) {
// Local modifications
return destination.get(id)
.push(function (remote_doc) {
var remote_hash = generateHash(stringify(remote_doc));
if (remote_hash !== status_hash) {
// Modifications on both sides
if (local_hash === remote_hash) {
// Same modifications on both side \o/
return context._signature_sub_storage.put(id, {
"hash": local_hash
})
.push(function () {
skip_document_dict[id] = null;
});
}
if (conflict_ignore === true) {
return;
}
if (conflict_force !== true) {
throw new jIO.util.jIOError("Conflict on '" + id + "': " +
stringify(doc) + " !== " +
stringify(remote_doc),
409);
}
}
return propagateModification(source, destination, doc,
local_hash, id);
}, function (error) {
if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) {
// Document has been deleted remotely
return propagateModification(source, destination, doc,
local_hash, id);
}
throw error;
});
return checkAndPropagate(status_hash, local_hash, doc,
source, destination, id,
conflict_force, conflict_revert,
conflict_ignore,
options);
}
});
}
function checkBulkSignatureDifference(queue, source, destination, id_list,
conflict_force, conflict_ignore) {
document_status_list, options,
conflict_force, conflict_revert,
conflict_ignore) {
queue
.push(function () {
return source.bulk(id_list);
......@@ -388,8 +798,11 @@
for (i = 0; i < result_list.length; i += 1) {
checkSignatureDifference(sub_queue, source, destination,
id_list[i].parameter_list[0],
conflict_force, conflict_ignore,
getResult(i));
conflict_force, conflict_revert,
conflict_ignore,
document_status_list[i].is_creation,
document_status_list[i].is_modification,
getResult(i), options);
}
return sub_queue;
});
......@@ -400,6 +813,9 @@
if (!options.hasOwnProperty("use_post")) {
options.use_post = false;
}
if (!options.hasOwnProperty("use_revert_post")) {
options.use_revert_post = false;
}
return queue
.push(function () {
return RSVP.all([
......@@ -410,9 +826,11 @@
.push(function (result_list) {
var i,
local_dict = {},
new_list = [],
change_list = [],
document_list = [],
document_status_list = [],
signature_dict = {},
is_modification,
is_creation,
key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty(
......@@ -428,55 +846,53 @@
signature_dict[result_list[1].data.rows[i].id] = i;
}
}
if (options.check_creation === true) {
for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) {
if (!signature_dict.hasOwnProperty(key)) {
is_modification = signature_dict.hasOwnProperty(key)
&& options.check_modification;
is_creation = !signature_dict.hasOwnProperty(key)
&& options.check_creation;
if (is_modification === true || is_creation === true) {
if (options.use_bulk_get === true) {
new_list.push({
document_list.push({
method: "get",
parameter_list: [key]
});
document_status_list.push({
is_creation: is_creation,
is_modification: is_modification
});
} else {
checkLocalCreation(queue, source, destination, key,
options, source.get.bind(source));
}
}
checkSignatureDifference(queue, source, destination, key,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
is_creation, is_modification,
source.get.bind(source),
options);
}
}
if ((options.use_bulk_get === true) && (new_list.length !== 0)) {
checkBulkLocalCreation(queue, source, destination, new_list,
options);
}
}
if (options.check_deletion === true) {
for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) {
if (options.check_modification === true) {
if (options.use_bulk_get === true) {
change_list.push({
method: "get",
parameter_list: [key]
});
} else {
checkSignatureDifference(queue, source, destination, key,
if (!local_dict.hasOwnProperty(key)) {
checkLocalDeletion(queue, destination, key, source,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore,
source.get.bind(source));
}
}
} else {
if (options.check_deletion === true) {
checkLocalDeletion(queue, destination, key, source);
options);
}
}
}
}
if ((options.use_bulk_get === true) && (change_list.length !== 0)) {
if ((options.use_bulk_get === true) && (document_list.length !== 0)) {
checkBulkSignatureDifference(queue, source, destination,
change_list,
document_list, document_status_list,
options,
options.conflict_force,
options.conflict_revert,
options.conflict_ignore);
}
});
......@@ -528,10 +944,10 @@
use_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: ((context._conflict_handling ===
CONFLICT_CONTINUE) ||
(context._conflict_handling ===
CONFLICT_KEEP_REMOTE)),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_local_modification,
check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion
......@@ -543,7 +959,7 @@
// Keep it like this until the bulk API is stabilized
var use_bulk_get = false;
try {
use_bulk_get = context._remote_sub_storage.hasCapacity("bulk");
use_bulk_get = context._remote_sub_storage.hasCapacity("bulk_get");
} catch (error) {
if (!((error instanceof jIO.util.jIOError) &&
(error.status_code === 501))) {
......@@ -556,8 +972,11 @@
return pushStorage(context._remote_sub_storage,
context._local_sub_storage, {
use_bulk_get: use_bulk_get,
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE),
check_modification: context._check_remote_modification,
......@@ -565,6 +984,34 @@
check_deletion: context._check_remote_deletion
});
}
})
.push(function () {
if (context._check_local_attachment_modification ||
context._check_local_attachment_creation ||
context._check_local_attachment_deletion ||
context._check_remote_attachment_modification ||
context._check_remote_attachment_creation ||
context._check_remote_attachment_deletion) {
// Attachments are synchronized if and only if their parent document
// has been also marked as synchronized.
return context._signature_sub_storage.allDocs()
.push(function (result) {
var i,
repair_document_queue = new RSVP.Queue();
function repairDocument(id) {
repair_document_queue
.push(function () {
return repairDocumentAttachment(id);
});
}
for (i = 0; i < result.data.total_rows; i += 1) {
repairDocument(result.data.rows[i].id);
}
return repair_document_queue;
});
}
});
};
......
This source diff could not be displayed because it is too large. You can view the blob instead.
......@@ -20,6 +20,7 @@
'"2006-01-02T15:04:05.000Z"');
equal(str({ x: 5, y: 6, z: 7 }), '{"x":5,"y":6,"z":7}');
equal(str({ z: 7, y: 6, x: 5 }), '{"x":5,"y":6,"z":7}');
equal(str({ z: "", y: undefined, x: 5 }), '{"x":5,"z":""}');
equal(str(Object.create(null, { x: { value: 'x', enumerable: false },
y: { value: 'y', enumerable: true } })),
'{"y":"y"}');
......
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>jIO in WebWorker Tests</title>
<link rel="stylesheet" href="../node_modules/qunitjs/qunit/qunit.css" type="text/css" media="screen"/>
</head>
<body>
<div id="qunit"></div>
<script src="../node_modules/qunitjs/qunit/qunit.js" type="text/javascript"></script>
<script>
QUnit.config.autorun = false;
</script>
<script src="webworker.js"></script>
</body>
</html>
/*global QUnit, Worker, console*/
(function (QUnit) {
"use strict";
var worker = new Worker("webworker.tests.js");
//queue = new RSVP.Queue();
worker.onerror = function (event) {
console.log(event);
};
worker.onmessage = function (event) {
var parsed_data = JSON.parse(event.data),
//callbacks = QUnit.config[parsed_data.method],
callbacks = QUnit.config.callbacks[parsed_data.method],
i,
start;
if (parsed_data.method) {
if (parsed_data.method === 'tap') {
start = parsed_data.data.slice(0, 6);
if (start === "not ok") {
console.log(parsed_data.data);
}
} else {
for (i = 0; i < callbacks.length; i += 1) {
callbacks[i](parsed_data.data);
}
if (parsed_data.method === 'done') {
worker.terminate();
}
}
}
};
}(QUnit));
/*global self, QUnit, qunitTap, importScripts*/
var global = self,
window = self;
(function (self) {
"use strict";
self.DOMParser = {};
self.DOMError = {};
self.sessionStorage = {};
self.localStorage = {};
self.openDatabase = {};
importScripts(
"../node_modules/rsvp/dist/rsvp-2.0.4.js",
"../dist/jio-latest.js"
);
self.exports = self;
importScripts("../node_modules/qunitjs/qunit/qunit.js");
self.exports = undefined;
//QUnit.config.autorun = false;
//QUnit.config.testTimeout = 5000;
importScripts("../node_modules/sinon/pkg/sinon.js");
importScripts("../node_modules/qunit-tap/lib/qunit-tap.js");
qunitTap(QUnit, function (data) {
self.postMessage(JSON.stringify({
method: 'tap',
data: data
}));
});
function createCallback(logType) {
QUnit[logType](function (data) {
self.postMessage(JSON.stringify({
method: logType,
data: data
}));
});
}
var i,
logs = [
"begin",
"testStart",
"testDone",
"log",
"moduleStart",
"moduleDone",
"done"
];
for (i = 0; i < logs.length; i += 1) {
createCallback(logs[i]);
}
// queries/key.tests.js needs it
//self.module = QUnit.module;
//self.test = QUnit.test;
//self.stop = QUnit.stop;
//self.start = QUnit.start;
//self.ok = QUnit.ok;
//self.expect = QUnit.expect;
//self.deepEqual = QUnit.deepEqual;
//self.equal = QUnit.equal;
importScripts(
//"jio/util.js",
//"queries/key.tests.js",
//"queries/key-schema.tests.js",
//"queries/tests.js",
//"queries/key-typechecks.tests.js",
//"queries/jiodate.tests.js",
//"queries/key-jiodate.tests.js",
//"jio.storage/querystorage.tests.js",
//"jio.storage/uuidstorage.tests.js",
//"jio.storage/replicatestorage.tests.js",
//"jio.storage/unionstorage.tests.js",
//"jio.storage/shastorage.tests.js",
//"jio.storage/cryptstorage.tests.js",
//"jio.storage/zipstorage.tests.js",
"jio.storage/indexeddbstorage.tests.js"
);
QUnit.load();
}(self));
\ No newline at end of file
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment