| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| require('es6-shim'); |
| |
| var verror = require('vanadium').verror; |
| |
| var $ = require('../src/util/jquery'); |
| var defineClass = require('../src/util/define-class'); |
| |
| //All periods are expressed in milliseconds. |
| var SYNC_LOOP_PERIOD = 50; |
| var WATCH_LOOP_PERIOD = 50; |
| |
| var syncgroups = {}; |
| |
| function update(a, b) { |
| $.each(a, function(k, v) { |
| if (k !== 'value' && k !== 'version') { |
| var bv = b[k]; |
| if (bv) { |
| update(v, bv); |
| } else { |
| b[k] = $.extend(true, {}, v); |
| } |
| } |
| }); |
| |
| if (a.version > b.version) { |
| b.value = a.value; |
| b.version = a.version; |
| } |
| } |
| |
| function sync(a, b, prefixes) { |
| $.each(prefixes, function() { |
| var suba = recursiveGet(a, this); |
| var subb = recursiveGet(b, this); |
| |
| if (suba && subb) { |
| update(suba, subb); |
| update(subb, suba); |
| } else if (!suba) { |
| recursiveCopy(a, this, subb); |
| } else if (!subb) { |
| recursiveCopy(b, this, suba); |
| } |
| }); |
| } |
| |
| function syncLoop() { |
| $.each(syncgroups, function() { |
| var prev; |
| this.forEach(function(sb) { |
| if (prev) { |
| sync(prev, sb, this.prefixes); |
| } |
| |
| prev = sb; |
| }, this); |
| }); |
| |
| setTimeout(syncLoop, SYNC_LOOP_PERIOD); |
| } |
| process.nextTick(syncLoop); |
| |
| function advanceVersion(node) { |
| if (node.version === undefined) { |
| node.version = 0; |
| } else { |
| node.version++; |
| } |
| } |
| |
| function recursiveCreate(node, key) { //it's recursive in spirit |
| $.each(key, function() { |
| var child = node[this]; |
| if (!child) { |
| child = node[this] = {}; |
| } |
| node = child; |
| }); |
| |
| return node; |
| } |
| |
| function recursiveSet(node, key, value) { |
| node = recursiveCreate(node, key); |
| |
| node.value = value; |
| advanceVersion(node); |
| } |
| |
| function recursiveCopy(node, key, content) { |
| $.extend(true, recursiveCreate(node, key), content); |
| } |
| |
| function recursiveGet(node, key) { |
| $.each(key, function() { |
| if (!node) { |
| return false; |
| } |
| node = node[this]; |
| }); |
| return node; |
| } |
| |
| function recursiveDelete(node, key) { |
| if (key) { |
| node = recursiveGet(node, key); |
| } |
| |
| if (node) { |
| delete node.value; |
| advanceVersion(node); |
| $.each(node, function(key, value) { |
| if (key !== 'version') { |
| recursiveDelete(value); |
| } |
| }); |
| } |
| } |
| |
| function extractData(repo) { |
| var data; |
| $.each(repo, function(k, v) { |
| if (k === 'value') { |
| if (typeof data === 'object') { |
| if (v !== undefined) { |
| data._ = v; |
| } |
| } else { |
| data = v; |
| } |
| } else if (k !== 'version') { |
| var value = extractData(v); |
| if (value !== undefined) { |
| if (data === undefined) { |
| data = {}; |
| } else if (typeof data !== 'object') { |
| data = { _: data }; |
| } |
| data[k] = value; |
| } |
| } |
| }); |
| |
| return data; |
| } |
| |
| var MockSyncbaseWrapper = defineClass({ |
| statics: { |
| /** |
| * SLA for a write to a mocked Syncbase instance to be reflected by synced |
| * instances. This is actually based on the size of the SyncGroups with the |
| * current mock implementation--roughly n * SYNC_LOOP_SLA--but let's express |
| * it as a constant for simplicity. |
| */ |
| SYNC_SLA: 250 //ms |
| }, |
| |
| publics: { |
| batch: function(fn) { |
| var ops = { |
| put: this.put, |
| delete: this.delete |
| }; |
| |
| fn.call(ops, ops); |
| return Promise.resolve(); |
| }, |
| |
| put: function(k, v) { |
| recursiveSet(this.repo, k, v); |
| return Promise.resolve(); |
| }, |
| |
| delete: function(k) { |
| recursiveDelete(this.repo, k); |
| return Promise.resolve(); |
| }, |
| |
| getData: function() { |
| return extractData(this.repo) || {}; |
| }, |
| |
| syncGroup: function(sgAdmin, name) { |
| var repo = this.repo; |
| var sgp; |
| |
| function sgFactory(spec) { |
| return function() { |
| var sg = new Set(); |
| sg.prefixes = spec; |
| return sg; |
| }; |
| } |
| |
| function errNoExist() { |
| return new verror.NoExistError(null, 'SyncGroup does not exist.'); |
| } |
| |
| function getSg() { |
| return syncgroups[sgAdmin + '$' + name]; |
| } |
| |
| function joinSg(factory) { |
| var sgKey = sgAdmin + '$' + name; |
| var sg = syncgroups[sgKey]; |
| if (!sg) { |
| sg = syncgroups[sgKey] = factory(); |
| } |
| sg.add(repo); |
| |
| return Promise.resolve(sgp); |
| } |
| |
| sgp = { |
| buildSpec: function(prefixes) { |
| return prefixes; |
| }, |
| |
| changeSpec: function(){ |
| return getSg()? Promise.resolve() : Promise.reject(errNoExist()); |
| }, |
| |
| join: function() { |
| return joinSg(function() { |
| throw errNoExist(); |
| }); |
| }, |
| |
| createOrJoin: function(spec) { |
| return joinSg(sgFactory(spec)); |
| }, |
| |
| joinOrCreate: function(spec) { |
| return joinSg(sgFactory(spec)); |
| } |
| }; |
| |
| return sgp; |
| }, |
| |
| refresh: function() { |
| this.onUpdate(this.getData()); |
| } |
| }, |
| |
| events: { |
| onError: 'memory', |
| onUpdate: 'memory' |
| }, |
| |
| init: function() { |
| var self = this; |
| |
| this.repo = {}; |
| |
| function watchLoop() { |
| self.refresh(); |
| setTimeout(watchLoop, WATCH_LOOP_PERIOD); |
| } |
| process.nextTick(watchLoop); |
| } |
| }); |
| |
| module.exports = MockSyncbaseWrapper; |