From 1dc9c3a59137b1cb9a38669938ba865154100c64 Mon Sep 17 00:00:00 2001 From: Kaarle Ritvanen Date: Thu, 6 Feb 2014 22:27:20 +0200 Subject: web client: transaction module --- web/transaction.js | 416 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 416 insertions(+) create mode 100644 web/transaction.js (limited to 'web/transaction.js') diff --git a/web/transaction.js b/web/transaction.js new file mode 100644 index 0000000..9a9368b --- /dev/null +++ b/web/transaction.js @@ -0,0 +1,416 @@ +/* + * Copyright (c) 2012-2014 Kaarle Ritvanen + * See LICENSE file for license details + */ + +define( + ["acf2/path", "acf2/type", "jquery", "underscore", "jquery-blockui"], + function(pth, type, $, _) { + return function(token, saveRequired) { + var txn, changed, invalid; + + function reset() { + txn = null; + changed = {}; + invalid = {}; + } + reset(); + + function isValid() { return !(_.size(invalid)); } + + function request(url, options) { + options = options || {}; + options.headers = {"X-ACF-Auth-Token": token}; + if (txn) options.headers["X-ACF-Transaction-ID"] = txn; + if (options.data != undefined) + options.data = JSON.stringify(options.data); + return $.ajax(url, options); + } + + function abort() { + request("/transaction", {type: "DELETE"}); + reset(); + } + + function objRequest(path, options) { + return request("/config" + path, options); + } + + function exclusive(task) { + $.blockUI(); + + var def = $.Deferred(); + function resolve(txnValid) { def.resolve(txnValid); } + function reject() { def.reject(); } + + var tasks = _.filter( + _.pluck(_.values(invalid), 1), function(d) { + return d && d.state() == "pending"; + } + ); + + if (tasks.length) + tasks[0].always(function() { + exclusive(task).done(resolve).fail(reject); + }); + else task().always($.unblockUI).done(resolve).fail(reject); + + return def; + } + + function query(path) { + var def = $.Deferred(); + + objRequest(path).done(function(data) { + if (type.isTreeNode(data.meta) && !_.size(data.data)) + data.data = type.isList(data.meta) ? [] : {}; + + function index(name) { + return _.isArray(data.data) ? name - 1 : name; + } + + data.get = function(name, valid) { + var p = pth.join(path, name); + if (!valid && p in invalid) return invalid[p][0]; + + if (data.meta.type == "set") + return _.contains(data.data, name) ? name : null; + + return data.data[index(name)]; + }; + + data.metaRequest = function(name) { + return request("/meta" + pth.join(path, name)); + }; + + data.match = function(filter) { + if (!filter) return true; + return _.every(_.map(filter, function(values, key) { + return _.contains(values, data.data[key]); + })); + }; + + data.status = function(name) { + var p = name ? pth.join(path, name) : path; + function scan(objs) { + return _.size(_.filter( + _.keys(objs), function(obj) { + return pth.isSubordinate(obj, p); + } + )); + } + + if (scan(invalid)) return "invalid"; + if (scan(changed)) return "changed"; + return null; + }; + + data.validate = function() { + var valid = true; + if (data.meta.required) { + valid = _.size(data.data); + if (valid) delete invalid[path]; + else invalid[path] = [path]; + } + return valid && isValid(); + }; + + data.set = function(name, newValue) { + var def = $.Deferred(); + function reject(xhr) { def.reject(xhr); } + + var mpath = pth.join(path, name); + + var value = data.get(name, true); + if (value == undefined) value = null; + + var tn = _.isObject(newValue); + var npv = tn ? mpath : newValue; + + function ignore(path) { + _.each(_.keys(invalid), function(p) { + if (pth.isSubordinate(p, path)) + delete invalid[p]; + }); + } + + function resolve() { + if (mpath in invalid && + invalid[mpath][1] == def) { + + var del = invalid[mpath][0] == null; + + delete invalid[mpath]; + if (del) ignore(mpath); + } + + def.resolve(isValid()); + } + + function validate() { + var del = newValue == null; + var set = data.meta.type == "set"; + + var options; + if (!del) + options = { + type: set ? "POST" : "PUT", data: newValue + }; + else if (data.get(name, true) != null) + options = {type: "DELETE"}; + + if (!options) { + if (data.meta.type == "model" && + _.findWhere( + data.meta.fields, {name: name} + ).required) + def.reject("Required value not set"); + else resolve(); + return; + } + + objRequest( + set && !del ? path : mpath, options + ).done(function() { + if (!(mpath in changed)) + changed[mpath] = value; + if (!tn && newValue == changed[mpath]) + delete changed[mpath]; + + if (npv == null) + _.each( + _.keys(changed), + function(p) { + if (pth.isSubordinate( + p, mpath, true + )) + delete changed[p]; + } + ); + + if (data.meta.type == "list" && del) + data.data.splice(name - 1, 1); + else if (!set) data.data[index(name)] = npv; + else if (del) + data.data.splice( + data.data.indexOf(name), 1 + ); + else data.data.push(name); + + data.validate(); + + if (data.meta.type == "model") + _.each( + data.meta.fields, function(field) { + if (field.condition && + field.condition[name] && + !_.contains( + field.condition[name], + newValue + )) + ignore( + pth.join(path, field.name) + ); + } + ); + + if (tn && !set) + query(mpath).done(function(data) { + + if (mpath in invalid) { + if (data.meta.type == "model") + _.each( + data.meta.fields, + function(field) { + var mmpath = pth.join( + mpath, + field.name + ); + if (field.required && + data.match( + field.condition + ) && + !(mmpath in invalid) && + (type.isCollection( + field + ) || data.get( + field.name + ) == null)) { + invalid[mmpath] = [ + null + ]; + } + }); + + else if ( + type.isCollection(data.meta) && + data.meta.required + ) + invalid[mpath] = [mpath]; + } + + resolve(); + }).fail(reject); + + else resolve(); + + }).fail(reject); + } + + var prevTask; + if (mpath in invalid) prevTask = invalid[mpath][1]; + + invalid[mpath] = [npv, def]; + + if (prevTask) prevTask.always(validate); + else validate(); + + return def; + }; + + data.add = function(name) { + return data.set(name, name); + }; + + function adjustListIndex(oldIndex, newIndex) { + var opath = pth.join(path, oldIndex); + var npath = pth.join(path, newIndex); + _.each( + [changed, invalid], + function(map) { + _.each( + _.keys(map), + function(p) { + if (pth.isSubordinate( + p, opath + )) { + map[npath + p.substring( + opath.length + )] = map[p]; + delete map[p]; + } + }); + } + ); + } + + function adjustListIndices(start, end) { + var offset = start < end ? 1 : -1; + for (var i = start; i != end; i += offset) + adjustListIndex(i + offset, i); + } + + function _delete(name) { + var def = $.Deferred(); + var length = data.data.length; + + data.set(name, null).done(function(txnValid) { + if (type.isTreeNode(data.meta) && + data.meta.type != "set") { + + delete changed[pth.join(path, name)]; + changed[path] = path; + + if (data.meta.type == "list") + adjustListIndices(name, length); + } + def.resolve(txnValid); + + }).fail(function() { def.reject(); }); + + return def; + } + + data.delete = function(name) { + return exclusive(function() { return _delete(name); }); + }; + + data.move = function(oldIndex, newIndex) { + if (oldIndex == newIndex) + return $.Deferred().resolve(isValid()); + + var value = data.get(oldIndex); + var length = data.data.length; + + return exclusive(function() { + var def = $.Deferred(); + function reject() { def.reject(); } + + if (oldIndex > newIndex) oldIndex++; + else newIndex++; + + objRequest(path, {type: "POST", data: { + index: newIndex, + data: type.isTreeNode(data.meta.members) ? + pth.join(path, oldIndex) : value + }}).done(function() { + + data.data.splice(newIndex - 1, 0, value); + + adjustListIndices(length + 1, newIndex); + adjustListIndex(oldIndex, newIndex); + + data.delete(oldIndex) + .done(function(txnValid) { + def.resolve(txnValid); + }) + .fail(reject); + + }).fail(reject); + + return def; + }); + }; + + data.invoke = function(name) { + return objRequest(pth.join(path, name), {type: "POST"}); + }; + + def.resolve(data); + }).fail(function() { def.reject(); }); + + return def; + } + + return { + start: function() { + var def = $.Deferred(); + if (txn && isValid() && !(_.size(changed))) abort(); + + if (txn) def.resolve(); + + else request("/transaction", {type: "POST"}) + .done(function(data, status, xhr) { + txn = xhr.getResponseHeader("X-ACF-Transaction-ID"); + def.resolve(); + }) + .fail(function() { def.reject(); }); + + return def; + }, + + commit: function() { + var def = $.Deferred(); + function reject(xhr) { def.reject(xhr); } + request("/transaction", {type: "PUT"}).done(function() { + reset(); + if (saveRequired) + request("/save", {type: "POST"}).done(function() { + def.resolve(); + }).fail(reject); + else def.resolve(); + }).fail(reject); + return def; + }, + + abort: abort, + + query: query, + + logout: function() { + return request("/login", {type: "DELETE"}); + } + }; + } + } +); -- cgit v1.2.3