Commit bf969260 authored by Aurel's avatar Aurel

Merge branch 'master' into erp5toerp5

Conflicts:
	src/jio.storage/replicatestorage.js
	test/jio.storage/replicatestorage.tests.js
parents 9d4d2411 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): ...@@ -41,7 +41,7 @@ each method, please refer to the documentation):
```javascript ```javascript
// create and store new document // create and store new document
jio_instance.put({"title": "some title"}) jio_instance.post({"title": "some title"})
.then(function (new_id) { .then(function (new_id) {
... ...
}); });
......
...@@ -7458,11 +7458,17 @@ return new Parser; ...@@ -7458,11 +7458,17 @@ return new Parser;
}; };
}(window, moment)); }(window, moment));
;/*global window, RSVP, Blob, XMLHttpRequest, QueryFactory, Query, atob, ;/*global window, RSVP, Blob, XMLHttpRequest, QueryFactory, Query, atob,
FileReader, ArrayBuffer, Uint8Array */ FileReader, ArrayBuffer, Uint8Array, navigator */
(function (window, RSVP, Blob, QueryFactory, Query, atob, (function (window, RSVP, Blob, QueryFactory, Query, atob,
FileReader, ArrayBuffer, Uint8Array) { FileReader, ArrayBuffer, Uint8Array, navigator) {
"use strict"; "use strict";
if (window.openDatabase === undefined) {
window.openDatabase = function () {
throw new Error('WebSQL is not supported by ' + navigator.userAgent);
};
}
var util = {}, var util = {},
jIO; jIO;
...@@ -7569,8 +7575,46 @@ return new Parser; ...@@ -7569,8 +7575,46 @@ return new Parser;
} }
util.readBlobAsDataURL = readBlobAsDataURL; 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 // https://gist.github.com/davoclavo/4424731
function dataURItoBlob(dataURI) { function dataURItoBlob(dataURI) {
if (dataURI === 'data:') {
return new Blob();
}
// convert base64 to raw binary data held in a string // convert base64 to raw binary data held in a string
var byteString = atob(dataURI.split(',')[1]), var byteString = atob(dataURI.split(',')[1]),
// separate out the mime component // separate out the mime component
...@@ -7931,7 +7975,7 @@ return new Parser; ...@@ -7931,7 +7975,7 @@ return new Parser;
window.jIO = jIO; window.jIO = jIO;
}(window, RSVP, Blob, QueryFactory, Query, atob, }(window, RSVP, Blob, QueryFactory, Query, atob,
FileReader, ArrayBuffer, Uint8Array)); FileReader, ArrayBuffer, Uint8Array, navigator));
;/* ;/*
* Rusha, a JavaScript implementation of the Secure Hash Algorithm, SHA-1, * 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. * as defined in FIPS PUB 180-1, tuned for high performance with large inputs.
...@@ -8370,7 +8414,7 @@ return new Parser; ...@@ -8370,7 +8414,7 @@ return new Parser;
/*jslint nomen: true*/ /*jslint nomen: true*/
/*global jIO, RSVP, Rusha*/ /*global jIO, RSVP, Rusha*/
(function (jIO, RSVP, Rusha) { (function (jIO, RSVP, Rusha, stringify) {
"use strict"; "use strict";
var rusha = new Rusha(), var rusha = new Rusha(),
...@@ -8397,14 +8441,14 @@ return new Parser; ...@@ -8397,14 +8441,14 @@ return new Parser;
this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage); this._remote_sub_storage = jIO.createJIO(spec.remote_sub_storage);
this._signature_hash = "_replicate_" + generateHash( this._signature_hash = "_replicate_" + generateHash(
JSON.stringify(spec.local_sub_storage) + stringify(spec.local_sub_storage) +
JSON.stringify(spec.remote_sub_storage) + stringify(spec.remote_sub_storage) +
JSON.stringify(this._query_options) stringify(this._query_options)
); );
this._signature_sub_storage = jIO.createJIO({ this._signature_sub_storage = jIO.createJIO({
type: "document", type: "document",
document_id: this._signature_hash, 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; this._use_remote_post = spec.use_remote_post || false;
...@@ -8540,40 +8584,45 @@ return new Parser; ...@@ -8540,40 +8584,45 @@ return new Parser;
}); });
} }
function checkLocalCreation(queue, source, destination, id, options, function propagateDeletion(destination, id) {
getMethod) { return destination.remove(id)
var remote_doc;
queue
.push(function () { .push(function () {
return destination.get(id); return context._signature_sub_storage.remove(id);
}) })
.push(function (doc) { .push(function () {
remote_doc = doc; 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) { }, function (error) {
if ((error instanceof jIO.util.jIOError) && if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) { (error.status_code === 404)) {
// This document was never synced. return [null, null];
// Push it to the remote storage and store sync information
return;
} }
throw error; 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 () { .push(function () {
// This document was never synced. skip_document_dict[id] = null;
// 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);
} }
remote_hash = generateHash(JSON.stringify(remote_doc));
if (local_hash === remote_hash) {
// Same document
return context._signature_sub_storage.put(id, { return context._signature_sub_storage.put(id, {
"hash": local_hash "hash": local_hash
}) })
...@@ -8581,50 +8630,58 @@ return new Parser; ...@@ -8581,50 +8630,58 @@ return new Parser;
skip_document_dict[id] = null; skip_document_dict[id] = null;
}); });
} }
if (options.conflict_ignore === true) {
return; if ((remote_hash === status_hash) || (conflict_force === true)) {
} // Modified only locally. No conflict or force
if (options.conflict_force === true) { if (local_hash === null) {
return propagateModification(source, destination, doc, local_hash, // Deleted locally
id, options); return propagateDeletion(destination, id);
} }
// Already exists on destination return propagateModification(source, destination, doc,
throw new jIO.util.jIOError("Conflict on '" + id + "': " + local_hash, id,
JSON.stringify(doc) + " !== " + {use_post: ((options.use_post) &&
JSON.stringify(remote_doc), (remote_hash === null))});
409);
});
} }
function checkBulkLocalCreation(queue, source, destination, id_list, // Conflict cases
options) { if (conflict_ignore === true) {
queue return;
.push(function () { }
return source.bulk(id_list);
})
.push(function (result_list) {
var i,
sub_queue = new RSVP.Queue();
function getResult(j) { if ((conflict_revert === true) || (local_hash === null)) {
return function (id) { // Automatically resolve conflict or force revert
if (id !== id_list[j].parameter_list[0]) { if (remote_hash === null) {
throw new Error("Does not access expected ID " + id); // 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) { // Minimize conflict if it can be resolved
checkLocalCreation(sub_queue, source, destination, if (remote_hash === null) {
id_list[i].parameter_list[0], // Copy remote modification remotely
options, getResult(i)); 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; var status_hash;
queue queue
.push(function () { .push(function () {
...@@ -8632,93 +8689,57 @@ return new Parser; ...@@ -8632,93 +8689,57 @@ return new Parser;
}) })
.push(function (result) { .push(function (result) {
status_hash = result.hash; status_hash = result.hash;
return destination.get(id) return checkAndPropagate(status_hash, null, null,
.push(function (doc) { source, destination, id,
var remote_hash = generateHash(JSON.stringify(doc)); conflict_force, conflict_revert,
if (remote_hash === status_hash) { conflict_ignore,
return destination.remove(id) options);
.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;
});
}); });
} }
function checkSignatureDifference(queue, source, destination, id, function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_ignore, conflict_force, conflict_revert,
getMethod) { conflict_ignore,
is_creation, is_modification,
getMethod, options) {
queue queue
.push(function () { .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([ return RSVP.all([
getMethod(id), getMethod(id),
context._signature_sub_storage.get(id) context._signature_sub_storage.get(id)
]); ]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkSignatureDifference",
409);
}) })
.push(function (result_list) { .push(function (result_list) {
var doc = result_list[0], var doc = result_list[0],
local_hash = generateHash(JSON.stringify(doc)), local_hash = generateHash(stringify(doc)),
status_hash = result_list[1].hash; status_hash = result_list[1].hash;
if (local_hash !== status_hash) { if (local_hash !== status_hash) {
// Local modifications return checkAndPropagate(status_hash, local_hash, doc,
return destination.get(id) source, destination, id,
.push(function (remote_doc) { conflict_force, conflict_revert,
var remote_hash = generateHash(JSON.stringify(remote_doc)); conflict_ignore,
if (remote_hash !== status_hash) { options);
// 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;
});
} }
}); });
} }
function checkBulkSignatureDifference(queue, source, destination, id_list, function checkBulkSignatureDifference(queue, source, destination, id_list,
conflict_force, conflict_ignore) { document_status_list, options,
conflict_force, conflict_revert,
conflict_ignore) {
queue queue
.push(function () { .push(function () {
return source.bulk(id_list); return source.bulk(id_list);
...@@ -8739,8 +8760,11 @@ return new Parser; ...@@ -8739,8 +8760,11 @@ return new Parser;
for (i = 0; i < result_list.length; i += 1) { for (i = 0; i < result_list.length; i += 1) {
checkSignatureDifference(sub_queue, source, destination, checkSignatureDifference(sub_queue, source, destination,
id_list[i].parameter_list[0], id_list[i].parameter_list[0],
conflict_force, conflict_ignore, conflict_force, conflict_revert,
getResult(i)); conflict_ignore,
document_status_list[i].is_creation,
document_status_list[i].is_modification,
getResult(i), options);
} }
return sub_queue; return sub_queue;
}); });
...@@ -8751,6 +8775,9 @@ return new Parser; ...@@ -8751,6 +8775,9 @@ return new Parser;
if (!options.hasOwnProperty("use_post")) { if (!options.hasOwnProperty("use_post")) {
options.use_post = false; options.use_post = false;
} }
if (!options.hasOwnProperty("use_revert_post")) {
options.use_revert_post = false;
}
return queue return queue
.push(function () { .push(function () {
return RSVP.all([ return RSVP.all([
...@@ -8761,9 +8788,11 @@ return new Parser; ...@@ -8761,9 +8788,11 @@ return new Parser;
.push(function (result_list) { .push(function (result_list) {
var i, var i,
local_dict = {}, local_dict = {},
new_list = [], document_list = [],
change_list = [], document_status_list = [],
signature_dict = {}, signature_dict = {},
is_modification,
is_creation,
key; key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) { for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty( if (!skip_document_dict.hasOwnProperty(
...@@ -8779,55 +8808,53 @@ return new Parser; ...@@ -8779,55 +8808,53 @@ return new Parser;
signature_dict[result_list[1].data.rows[i].id] = i; signature_dict[result_list[1].data.rows[i].id] = i;
} }
} }
if (options.check_creation === true) {
for (key in local_dict) { for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) { 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) { if (options.use_bulk_get === true) {
new_list.push({ document_list.push({
method: "get", method: "get",
parameter_list: [key] parameter_list: [key]
}); });
document_status_list.push({
is_creation: is_creation,
is_modification: is_modification
});
} else { } else {
checkLocalCreation(queue, source, destination, key, checkSignatureDifference(queue, source, destination, key,
options, source.get.bind(source)); 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) { for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) { if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) { if (!local_dict.hasOwnProperty(key)) {
if (options.check_modification === true) { checkLocalDeletion(queue, destination, key, source,
if (options.use_bulk_get === true) {
change_list.push({
method: "get",
parameter_list: [key]
});
} else {
checkSignatureDifference(queue, source, destination, key,
options.conflict_force, options.conflict_force,
options.conflict_revert,
options.conflict_ignore, options.conflict_ignore,
source.get.bind(source)); options);
}
}
} else {
if (options.check_deletion === true) {
checkLocalDeletion(queue, destination, key, source);
} }
} }
} }
} }
if ((options.use_bulk_get === true) && (change_list.length !== 0)) { if ((options.use_bulk_get === true) && (document_list.length !== 0)) {
checkBulkSignatureDifference(queue, source, destination, checkBulkSignatureDifference(queue, source, destination,
change_list, document_list, document_status_list,
options,
options.conflict_force, options.conflict_force,
options.conflict_revert,
options.conflict_ignore); options.conflict_ignore);
} }
}); });
...@@ -8879,10 +8906,10 @@ return new Parser; ...@@ -8879,10 +8906,10 @@ return new Parser;
use_post: context._use_remote_post, use_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL), CONFLICT_KEEP_LOCAL),
conflict_ignore: ((context._conflict_handling === conflict_revert: (context._conflict_handling ===
CONFLICT_CONTINUE) || CONFLICT_KEEP_REMOTE),
(context._conflict_handling === conflict_ignore: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE)), CONFLICT_CONTINUE),
check_modification: context._check_local_modification, check_modification: context._check_local_modification,
check_creation: context._check_local_creation, check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion check_deletion: context._check_local_deletion
...@@ -8907,8 +8934,11 @@ return new Parser; ...@@ -8907,8 +8934,11 @@ return new Parser;
return pushStorage(context._remote_sub_storage, return pushStorage(context._remote_sub_storage,
context._local_sub_storage, { context._local_sub_storage, {
use_bulk_get: use_bulk_get, use_bulk_get: use_bulk_get,
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE), CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling === conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE), CONFLICT_CONTINUE),
check_modification: context._check_remote_modification, check_modification: context._check_remote_modification,
...@@ -8921,7 +8951,7 @@ return new Parser; ...@@ -8921,7 +8951,7 @@ return new Parser;
jIO.addStorage('replicate', ReplicateStorage); jIO.addStorage('replicate', ReplicateStorage);
}(jIO, RSVP, Rusha)); }(jIO, RSVP, Rusha, jIO.util.stringify));
;/* ;/*
* Copyright 2015, Nexedi SA * Copyright 2015, Nexedi SA
* Released under the LGPL license. * Released under the LGPL license.
...@@ -10898,7 +10928,7 @@ return new Parser; ...@@ -10898,7 +10928,7 @@ return new Parser;
sub_query, sub_query,
result_list, result_list,
local_roles, local_roles,
tmp_list = []; sort_list = [];
if (options.query) { if (options.query) {
parsed_query = jIO.QueryFactory.create(options.query); parsed_query = jIO.QueryFactory.create(options.query);
...@@ -10942,9 +10972,8 @@ return new Parser; ...@@ -10942,9 +10972,8 @@ return new Parser;
if (options.sort_on) { if (options.sort_on) {
for (i = 0; i < options.sort_on.length; i += 1) { 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({ return jIO.util.ajax({
...@@ -10955,7 +10984,7 @@ return new Parser; ...@@ -10955,7 +10984,7 @@ return new Parser;
// XXX Force erp5 to return embedded document // XXX Force erp5 to return embedded document
select_list: options.select_list || ["title", "reference"], select_list: options.select_list || ["title", "reference"],
limit: options.limit, limit: options.limit,
sort_on: options.sort_on, sort_on: sort_list,
local_roles: local_roles local_roles: local_roles
}), }),
"xhrFields": { "xhrFields": {
...@@ -11827,9 +11856,11 @@ return new Parser; ...@@ -11827,9 +11856,11 @@ return new Parser;
*/ */
/*jslint nomen: true */ /*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"; "use strict";
// Read only as changing it can lead to data corruption // Read only as changing it can lead to data corruption
...@@ -11888,7 +11919,14 @@ return new Parser; ...@@ -11888,7 +11919,14 @@ return new Parser;
if (request.result) { if (request.result) {
request.result.close(); 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); reject(error);
}
}; };
request.onabort = function () { request.onabort = function () {
...@@ -12249,7 +12287,7 @@ return new Parser; ...@@ -12249,7 +12287,7 @@ return new Parser;
}; };
jIO.addStorage("indexeddb", IndexedDBStorage); jIO.addStorage("indexeddb", IndexedDBStorage);
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange)); }(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError));
;/* ;/*
* Copyright 2015, Nexedi SA * Copyright 2015, Nexedi SA
* Released under the LGPL license. * 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", "name": "jio",
"version": "v3.11.0", "version": "v3.13.0",
"license": "LGPLv3", "license": "LGPLv3",
"author": "Nexedi SA", "author": "Nexedi SA",
"contributors": [ "contributors": [
...@@ -42,6 +42,8 @@ ...@@ -42,6 +42,8 @@
"grunt-contrib-copy": "~0.4.1", "grunt-contrib-copy": "~0.4.1",
"grunt-contrib-uglify": "0.2.x", "grunt-contrib-uglify": "0.2.x",
"grunt-contrib-qunit": "~0.3.0", "grunt-contrib-qunit": "~0.3.0",
"qunitjs": "~1.23.1",
"qunit-tap": "1.5.0",
"grunt-contrib-watch": "~0.5.3", "grunt-contrib-watch": "~0.5.3",
"grunt-jslint": "~1.0.0", "grunt-jslint": "~1.0.0",
"lz-string": "^1.4.4", "lz-string": "^1.4.4",
......
...@@ -124,6 +124,9 @@ ...@@ -124,6 +124,9 @@
i, i,
value, value,
result_list; result_list;
if (obj === undefined) {
return undefined;
}
if (obj.constructor === Object) { if (obj.constructor === Object) {
key_list = Object.keys(obj).sort(); key_list = Object.keys(obj).sort();
result_list = []; result_list = [];
...@@ -150,6 +153,9 @@ ...@@ -150,6 +153,9 @@
// https://gist.github.com/davoclavo/4424731 // https://gist.github.com/davoclavo/4424731
function dataURItoBlob(dataURI) { function dataURItoBlob(dataURI) {
if (dataURI === 'data:') {
return new Blob();
}
// convert base64 to raw binary data held in a string // convert base64 to raw binary data held in a string
var byteString = atob(dataURI.split(',')[1]), var byteString = atob(dataURI.split(',')[1]),
// separate out the mime component // separate out the mime component
......
...@@ -447,7 +447,7 @@ ...@@ -447,7 +447,7 @@
ERP5Storage.prototype.hasCapacity = function (name) { ERP5Storage.prototype.hasCapacity = function (name) {
return ((name === "list") || (name === "query") || return ((name === "list") || (name === "query") ||
(name === "select") || (name === "limit") || (name === "select") || (name === "limit") ||
(name === "sort")); (name === "sort")) || (name === "bulk_get");
}; };
function isSingleLocalRoles(parsed_query) { function isSingleLocalRoles(parsed_query) {
...@@ -496,7 +496,7 @@ ...@@ -496,7 +496,7 @@
sub_query, sub_query,
result_list, result_list,
local_roles, local_roles,
tmp_list = []; sort_list = [];
if (options.query) { if (options.query) {
parsed_query = jIO.QueryFactory.create(options.query); parsed_query = jIO.QueryFactory.create(options.query);
...@@ -540,9 +540,8 @@ ...@@ -540,9 +540,8 @@
if (options.sort_on) { if (options.sort_on) {
for (i = 0; i < options.sort_on.length; i += 1) { 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({ return jIO.util.ajax({
...@@ -553,7 +552,7 @@ ...@@ -553,7 +552,7 @@
// XXX Force erp5 to return embedded document // XXX Force erp5 to return embedded document
select_list: options.select_list || ["title", "reference"], select_list: options.select_list || ["title", "reference"],
limit: options.limit, limit: options.limit,
sort_on: options.sort_on, sort_on: sort_list,
local_roles: local_roles local_roles: local_roles
}), }),
"xhrFields": { "xhrFields": {
......
...@@ -28,9 +28,11 @@ ...@@ -28,9 +28,11 @@
*/ */
/*jslint nomen: true */ /*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"; "use strict";
// Read only as changing it can lead to data corruption // Read only as changing it can lead to data corruption
...@@ -89,7 +91,14 @@ ...@@ -89,7 +91,14 @@
if (request.result) { if (request.result) {
request.result.close(); 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); reject(error);
}
}; };
request.onabort = function () { request.onabort = function () {
...@@ -450,4 +459,4 @@ ...@@ -450,4 +459,4 @@
}; };
jIO.addStorage("indexeddb", IndexedDBStorage); jIO.addStorage("indexeddb", IndexedDBStorage);
}(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange)); }(indexedDB, jIO, RSVP, Blob, Math, IDBKeyRange, IDBOpenDBRequest, DOMError));
...@@ -39,6 +39,11 @@ ...@@ -39,6 +39,11 @@
return rusha.digestFromString(content); return rusha.digestFromString(content);
} }
function generateHashFromArrayBuffer(content) {
// XXX Improve performance by moving calculation to WebWorker
return rusha.digestFromArrayBuffer(content);
}
function ReplicateStorage(spec) { function ReplicateStorage(spec) {
this._query_options = spec.query || {}; this._query_options = spec.query || {};
...@@ -53,10 +58,16 @@ ...@@ -53,10 +58,16 @@
this._signature_sub_storage = jIO.createJIO({ this._signature_sub_storage = jIO.createJIO({
type: "document", type: "document",
document_id: this._signature_hash, 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; this._use_remote_post = spec.use_remote_post || false;
// Number of request we allow browser execution for attachments
this._parallel_operation_attachment_amount =
spec.parallel_operation_attachment_amount || 1;
// Number of request we allow browser execution for documents
this._parallel_operation_amount =
spec.parallel_operation_amount || 1;
this._conflict_handling = spec.conflict_handling || 0; this._conflict_handling = spec.conflict_handling || 0;
// 0: no resolution (ie, throw an Error) // 0: no resolution (ie, throw an Error)
...@@ -99,6 +110,36 @@ ...@@ -99,6 +110,36 @@
if (this._check_remote_deletion === undefined) { if (this._check_remote_deletion === undefined) {
this._check_remote_deletion = true; 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) { ReplicateStorage.prototype.remove = function (id) {
...@@ -125,6 +166,32 @@ ...@@ -125,6 +166,32 @@
return this._local_sub_storage.get.apply(this._local_sub_storage, return this._local_sub_storage.get.apply(this._local_sub_storage,
arguments); 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 () { ReplicateStorage.prototype.hasCapacity = function () {
return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage, return this._local_sub_storage.hasCapacity.apply(this._local_sub_storage,
arguments); arguments);
...@@ -143,6 +210,357 @@ ...@@ -143,6 +210,357 @@
// Do not sync the signature document // Do not sync the signature document
skip_document_dict[context._signature_hash] = null; skip_document_dict[context._signature_hash] = null;
function dispatchQueue(function_used, argument_list, number_queue) {
var result_promise_list = [],
i;
function pushAndExecute(queue) {
queue
.push(function () {
if (argument_list.length > 0) {
var argument_array = argument_list.shift();
argument_array[0] = queue;
function_used.apply(context, argument_array);
pushAndExecute(queue);
}
});
}
for (i = 0; i < number_queue; i += 1) {
result_promise_list.push(new RSVP.Queue());
pushAndExecute(result_promise_list[i]);
}
if (number_queue > 1) {
return RSVP.all(result_promise_list);
}
return result_promise_list[0];
}
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, function propagateModification(source, destination, doc, hash, id,
options) { options) {
var result, var result,
...@@ -158,6 +576,33 @@ ...@@ -158,6 +576,33 @@
post_id = new_id; post_id = new_id;
return source.put(post_id, doc); 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 () { .push(function () {
return source.remove(id); return source.remove(id);
}) })
...@@ -189,40 +634,63 @@ ...@@ -189,40 +634,63 @@
}); });
} }
function checkLocalCreation(queue, source, destination, id, options, function propagateDeletion(destination, id) {
getMethod) { // Do not delete a document if it has an attachment
var remote_doc; // ie, replication should prevent losing user data
queue // Synchronize attachments before, to ensure
// all of them will be deleted too
return repairDocumentAttachment(id)
.push(function () { .push(function () {
return destination.get(id); return destination.allAttachments(id);
}) })
.push(function (doc) { .push(function (attachment_dict) {
remote_doc = doc; if (JSON.stringify(attachment_dict) === "{}") {
return destination.remove(id)
.push(function () {
return context._signature_sub_storage.remove(id);
});
}
}, function (error) { }, function (error) {
if ((error instanceof jIO.util.jIOError) && if ((error instanceof jIO.util.jIOError) &&
(error.status_code === 404)) { (error.status_code === 404)) {
// This document was never synced.
// Push it to the remote storage and store sync information
return; return;
} }
throw error; throw error;
}) })
.push(function () { .push(function () {
// This document was never synced. skip_document_dict[id] = null;
// 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);
} }
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) { 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, { return context._signature_sub_storage.put(id, {
"hash": local_hash "hash": local_hash
}) })
...@@ -230,50 +698,58 @@ ...@@ -230,50 +698,58 @@
skip_document_dict[id] = null; skip_document_dict[id] = null;
}); });
} }
if (options.conflict_ignore === true) {
return; if ((remote_hash === status_hash) || (conflict_force === true)) {
} // Modified only locally. No conflict or force
if (options.conflict_force === true) { if (local_hash === null) {
return propagateModification(source, destination, doc, local_hash, // Deleted locally
id); return propagateDeletion(destination, id);
} }
// Already exists on destination return propagateModification(source, destination, doc,
throw new jIO.util.jIOError("Conflict on '" + id + "': " + local_hash, id,
stringify(doc) + " !== " + {use_post: ((options.use_post) &&
stringify(remote_doc), (remote_hash === null))});
409);
});
} }
function checkBulkLocalCreation(queue, source, destination, id_list, // Conflict cases
options) { if (conflict_ignore === true) {
queue return;
.push(function () { }
return source.bulk(id_list);
})
.push(function (result_list) {
var i,
sub_queue = new RSVP.Queue();
function getResult(j) { if ((conflict_revert === true) || (local_hash === null)) {
return function (id) { // Automatically resolve conflict or force revert
if (id !== id_list[j].parameter_list[0]) { if (remote_hash === null) {
throw new Error("Does not access expected ID " + id); // 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) { // Minimize conflict if it can be resolved
checkLocalCreation(sub_queue, source, destination, if (remote_hash === null) {
id_list[i].parameter_list[0], // Copy remote modification remotely
options, getResult(i)); 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; var status_hash;
queue queue
.push(function () { .push(function () {
...@@ -281,44 +757,37 @@ ...@@ -281,44 +757,37 @@
}) })
.push(function (result) { .push(function (result) {
status_hash = result.hash; status_hash = result.hash;
return destination.get(id) return checkAndPropagate(status_hash, null, null,
.push(function (doc) { source, destination, id,
var remote_hash = generateHash(stringify(doc)); conflict_force, conflict_revert,
if (remote_hash === status_hash) { conflict_ignore,
return destination.remove(id) options);
.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;
});
}); });
} }
function checkSignatureDifference(queue, source, destination, id, function checkSignatureDifference(queue, source, destination, id,
conflict_force, conflict_ignore, conflict_force, conflict_revert,
getMethod) { conflict_ignore,
is_creation, is_modification,
getMethod, options) {
queue queue
.push(function () { .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([ return RSVP.all([
getMethod(id), getMethod(id),
context._signature_sub_storage.get(id) context._signature_sub_storage.get(id)
]); ]);
}
throw new jIO.util.jIOError("Unexpected call of"
+ " checkSignatureDifference",
409);
}) })
.push(function (result_list) { .push(function (result_list) {
var doc = result_list[0], var doc = result_list[0],
...@@ -326,55 +795,26 @@ ...@@ -326,55 +795,26 @@
status_hash = result_list[1].hash; status_hash = result_list[1].hash;
if (local_hash !== status_hash) { if (local_hash !== status_hash) {
// Local modifications return checkAndPropagate(status_hash, local_hash, doc,
return destination.get(id) source, destination, id,
.push(function (remote_doc) { conflict_force, conflict_revert,
var remote_hash = generateHash(stringify(remote_doc)); conflict_ignore,
if (remote_hash !== status_hash) { options);
// 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;
});
} }
}); });
} }
function checkBulkSignatureDifference(queue, source, destination, id_list, function checkBulkSignatureDifference(queue, source, destination, id_list,
conflict_force, conflict_ignore) { document_status_list, options,
conflict_force, conflict_revert,
conflict_ignore) {
queue queue
.push(function () { .push(function () {
return source.bulk(id_list); return source.bulk(id_list);
}) })
.push(function (result_list) { .push(function (result_list) {
var i, var i,
sub_queue = new RSVP.Queue(); argument_list = [];
function getResult(j) { function getResult(j) {
return function (id) { return function (id) {
...@@ -386,20 +826,32 @@ ...@@ -386,20 +826,32 @@
} }
for (i = 0; i < result_list.length; i += 1) { for (i = 0; i < result_list.length; i += 1) {
checkSignatureDifference(sub_queue, source, destination, argument_list[i] = [undefined, source, destination,
id_list[i].parameter_list[0], id_list[i].parameter_list[0],
conflict_force, conflict_ignore, conflict_force, conflict_revert,
getResult(i)); conflict_ignore,
document_status_list[i].is_creation,
document_status_list[i].is_modification,
getResult(i), options];
} }
return sub_queue; return dispatchQueue(
checkSignatureDifference,
argument_list,
options.operation_amount
);
}); });
} }
function pushStorage(source, destination, options) { function pushStorage(source, destination, options) {
var queue = new RSVP.Queue(); var queue = new RSVP.Queue(),
argument_list = [],
argument_list_deletion = [];
if (!options.hasOwnProperty("use_post")) { if (!options.hasOwnProperty("use_post")) {
options.use_post = false; options.use_post = false;
} }
if (!options.hasOwnProperty("use_revert_post")) {
options.use_revert_post = false;
}
return queue return queue
.push(function () { .push(function () {
return RSVP.all([ return RSVP.all([
...@@ -410,9 +862,11 @@ ...@@ -410,9 +862,11 @@
.push(function (result_list) { .push(function (result_list) {
var i, var i,
local_dict = {}, local_dict = {},
new_list = [], document_list = [],
change_list = [], document_status_list = [],
signature_dict = {}, signature_dict = {},
is_modification,
is_creation,
key; key;
for (i = 0; i < result_list[0].data.total_rows; i += 1) { for (i = 0; i < result_list[0].data.total_rows; i += 1) {
if (!skip_document_dict.hasOwnProperty( if (!skip_document_dict.hasOwnProperty(
...@@ -428,60 +882,86 @@ ...@@ -428,60 +882,86 @@
signature_dict[result_list[1].data.rows[i].id] = i; signature_dict[result_list[1].data.rows[i].id] = i;
} }
} }
i = 0;
if (options.check_creation === true) {
for (key in local_dict) { for (key in local_dict) {
if (local_dict.hasOwnProperty(key)) { 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) { if (options.use_bulk_get === true) {
new_list.push({ document_list.push({
method: "get", method: "get",
parameter_list: [key] parameter_list: [key]
}); });
document_status_list.push({
is_creation: is_creation,
is_modification: is_modification
});
} else { } else {
checkLocalCreation(queue, source, destination, key, argument_list[i] = [undefined, source, destination,
options, source.get.bind(source)); key,
} options.conflict_force,
} options.conflict_revert,
options.conflict_ignore,
is_creation, is_modification,
source.get.bind(source),
options];
i += 1;
} }
} }
if ((options.use_bulk_get === true) && (new_list.length !== 0)) {
checkBulkLocalCreation(queue, source, destination, new_list,
options);
} }
} }
queue
.push(function () {
return dispatchQueue(
checkSignatureDifference,
argument_list,
options.operation_amount
);
});
if (options.check_deletion === true) {
i = 0;
for (key in signature_dict) { for (key in signature_dict) {
if (signature_dict.hasOwnProperty(key)) { if (signature_dict.hasOwnProperty(key)) {
if (local_dict.hasOwnProperty(key)) { if (!local_dict.hasOwnProperty(key)) {
if (options.check_modification === true) { argument_list_deletion[i] = [undefined,
if (options.use_bulk_get === true) { destination, key,
change_list.push({ source,
method: "get",
parameter_list: [key]
});
} else {
checkSignatureDifference(queue, source, destination, key,
options.conflict_force, options.conflict_force,
options.conflict_revert,
options.conflict_ignore, options.conflict_ignore,
source.get.bind(source)); options];
} i += 1;
}
} else {
if (options.check_deletion === true) {
checkLocalDeletion(queue, destination, key, source);
} }
} }
} }
queue.push(function () {
return dispatchQueue(
checkLocalDeletion,
argument_list_deletion,
options.operation_amount
);
});
} }
if ((options.use_bulk_get === true) && (change_list.length !== 0)) { if ((options.use_bulk_get === true) && (document_list.length !== 0)) {
checkBulkSignatureDifference(queue, source, destination, checkBulkSignatureDifference(queue, source, destination,
change_list, document_list, document_status_list,
options,
options.conflict_force, options.conflict_force,
options.conflict_revert,
options.conflict_ignore); options.conflict_ignore);
} }
}); });
} }
function repairDocument(queue, id) {
queue.push(function () {
return repairDocumentAttachment(id);
});
}
return new RSVP.Queue() return new RSVP.Queue()
.push(function () { .push(function () {
// Ensure that the document storage is usable // Ensure that the document storage is usable
...@@ -528,13 +1008,14 @@ ...@@ -528,13 +1008,14 @@
use_post: context._use_remote_post, use_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL), CONFLICT_KEEP_LOCAL),
conflict_ignore: ((context._conflict_handling === conflict_revert: (context._conflict_handling ===
CONFLICT_CONTINUE) || CONFLICT_KEEP_REMOTE),
(context._conflict_handling === conflict_ignore: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE)), CONFLICT_CONTINUE),
check_modification: context._check_local_modification, check_modification: context._check_local_modification,
check_creation: context._check_local_creation, check_creation: context._check_local_creation,
check_deletion: context._check_local_deletion check_deletion: context._check_local_deletion,
operation_amount: context._parallel_operation_amount
}); });
} }
}) })
...@@ -543,7 +1024,7 @@ ...@@ -543,7 +1024,7 @@
// Keep it like this until the bulk API is stabilized // Keep it like this until the bulk API is stabilized
var use_bulk_get = false; var use_bulk_get = false;
try { try {
use_bulk_get = context._remote_sub_storage.hasCapacity("bulk"); use_bulk_get = context._remote_sub_storage.hasCapacity("bulk_get");
} catch (error) { } catch (error) {
if (!((error instanceof jIO.util.jIOError) && if (!((error instanceof jIO.util.jIOError) &&
(error.status_code === 501))) { (error.status_code === 501))) {
...@@ -556,13 +1037,45 @@ ...@@ -556,13 +1037,45 @@
return pushStorage(context._remote_sub_storage, return pushStorage(context._remote_sub_storage,
context._local_sub_storage, { context._local_sub_storage, {
use_bulk_get: use_bulk_get, use_bulk_get: use_bulk_get,
use_revert_post: context._use_remote_post,
conflict_force: (context._conflict_handling === conflict_force: (context._conflict_handling ===
CONFLICT_KEEP_REMOTE), CONFLICT_KEEP_REMOTE),
conflict_revert: (context._conflict_handling ===
CONFLICT_KEEP_LOCAL),
conflict_ignore: (context._conflict_handling === conflict_ignore: (context._conflict_handling ===
CONFLICT_CONTINUE), CONFLICT_CONTINUE),
check_modification: context._check_remote_modification, check_modification: context._check_remote_modification,
check_creation: context._check_remote_creation, check_creation: context._check_remote_creation,
check_deletion: context._check_remote_deletion check_deletion: context._check_remote_deletion,
operation_amount: context._parallel_operation_amount
});
}
})
.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,
argument_list = [],
len = result.data.total_rows;
for (i = 0; i < len; i += 1) {
argument_list.push(
[undefined, result.data.rows[i].id]
);
}
return dispatchQueue(
repairDocument,
argument_list,
context._parallel_operation_attachment_amount
);
}); });
} }
}); });
......
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -20,6 +20,7 @@ ...@@ -20,6 +20,7 @@
'"2006-01-02T15:04:05.000Z"'); '"2006-01-02T15:04:05.000Z"');
equal(str({ x: 5, y: 6, z: 7 }), '{"x":5,"y":6,"z":7}'); 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: 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 }, equal(str(Object.create(null, { x: { value: 'x', enumerable: false },
y: { value: 'y', enumerable: true } })), y: { value: 'y', enumerable: true } })),
'{"y":"y"}'); '{"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