Skip to content

Commit

Permalink
Add replication example
Browse files Browse the repository at this point in the history
  • Loading branch information
ritch committed Jan 28, 2014
1 parent 1a13a8d commit 2582c3f
Show file tree
Hide file tree
Showing 4 changed files with 425 additions and 22 deletions.
138 changes: 138 additions & 0 deletions example/replication/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
var loopback = require('../../');
var app = loopback();
var db = app.dataSource('db', {connector: loopback.Memory});
var Color = app.model('color', {dataSource: 'db', options: {trackChanges: true}});
var Color2 = app.model('color2', {dataSource: 'db', options: {trackChanges: true}});
var target = Color2;
var source = Color;
var SPEED = process.env.SPEED || 100;
var conflicts;

var steps = [

createSomeInitialSourceData,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data'),
list.bind(this, target, 'current TARGET data'),

updateSomeTargetData,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data '),
list.bind(this, target, 'current TARGET data (includes conflicting update)'),

updateSomeSourceDataCausingAConflict,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data (now has a conflict)'),
list.bind(this, target, 'current TARGET data (includes conflicting update)'),

resolveAllConflicts,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data (conflict resolved)'),
list.bind(this, target, 'current TARGET data (conflict resolved)'),

createMoreSourceData,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data'),
list.bind(this, target, 'current TARGET data'),

createEvenMoreSourceData,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data'),
list.bind(this, target, 'current TARGET data'),

deleteAllSourceData,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data (empty)'),
list.bind(this, target, 'current TARGET data (empty)'),

createSomeNewSourceData,

replicateSourceToTarget,
list.bind(this, source, 'current SOURCE data'),
list.bind(this, target, 'current TARGET data')
];

run(steps);

function createSomeInitialSourceData() {
Color.create([
{name: 'red'},
{name: 'blue'},
{name: 'green'}
]);
}

function replicateSourceToTarget() {
Color.replicate(0, Color2, {}, function(err, replicationConflicts) {
conflicts = replicationConflicts;
});
}

function resolveAllConflicts() {
if(conflicts.length) {
conflicts.forEach(function(conflict) {
conflict.resolve();
});
}
}

function updateSomeTargetData() {
Color2.findById(1, function(err, color) {
color.name = 'conflict';
color.save();
});
}

function createMoreSourceData() {
Color.create({name: 'orange'});
}

function createEvenMoreSourceData() {
Color.create({name: 'black'});
}

function updateSomeSourceDataCausingAConflict() {
Color.findById(1, function(err, color) {
color.name = 'red!!!!';
color.save();
});
}

function deleteAllSourceData() {
Color.destroyAll();
}

function createSomeNewSourceData() {
Color.create([
{name: 'violet'},
{name: 'amber'},
{name: 'olive'}
]);
}

function list(model, msg) {
console.log(msg);
model.find(function(err, items) {
items.forEach(function(item) {
console.log(' -', item.name);
});
console.log();
});
}

function run(steps) {
setInterval(function() {
var step = steps.shift();
if(step) {
console.log(step.name);
step();
}
}, SPEED);
}
126 changes: 119 additions & 7 deletions lib/models/change.js
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ var properties = {
*/

var options = {

trackChanges: false
};

/**
Expand Down Expand Up @@ -55,6 +55,12 @@ Change.CREATE = 'create';
Change.DELETE = 'delete';
Change.UNKNOWN = 'unknown';

/*!
* Conflict Class
*/

Change.Conflict = Conflict;

/*!
* Setup the extended model.
*/
Expand Down Expand Up @@ -149,13 +155,34 @@ Change.findOrCreate = function(modelName, modelId, callback) {

Change.prototype.rectify = function(cb) {
var change = this;
this.prev = this.rev;
// get the current revision
this.currentRevision(function(err, rev) {
if(err) return Change.handleError(err, cb);
change.rev = rev;
var tasks = [
updateRevision,
updateCheckpoint
];

if(this.rev) this.prev = this.rev;

async.parallel(tasks, function(err) {
if(err) return cb(err);
change.save(cb);
});

function updateRevision(cb) {
// get the current revision
change.currentRevision(function(err, rev) {
if(err) return Change.handleError(err, cb);
change.rev = rev;
cb();
});
}

function updateCheckpoint(cb) {
change.constructor.getCheckpointModel().current(function(err, checkpoint) {
if(err) return Change.handleError(err);
change.checkpoint = ++checkpoint;
cb();
});
}
}

/**
Expand Down Expand Up @@ -233,7 +260,7 @@ Change.prototype.type = function() {

Change.prototype.getModelCtor = function() {
// todo - not sure if this works with multiple data sources
return this.constructor.modelBuilder.models[this.modelName];
return loopback.getModel(this.modelName);
}

/**
Expand Down Expand Up @@ -306,7 +333,10 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
if(err) return callback(err);
var deltas = [];
var conflicts = [];
var localModelIds = [];

localChanges.forEach(function(localChange) {
localModelIds.push(localChange.modelId);
var remoteChange = remoteChangeIndex[localChange.modelId];
if(!localChange.equals(remoteChange)) {
if(remoteChange.isBasedOn(localChange)) {
Expand All @@ -317,9 +347,91 @@ Change.diff = function(modelName, since, remoteChanges, callback) {
}
});

modelIds.forEach(function(id) {
if(localModelIds.indexOf(id) === -1) {
deltas.push(remoteChangeIndex[id]);
}
});

callback(null, {
deltas: deltas,
conflicts: conflicts
});
});
}

/**
* Correct all change list entries.
* @param {Function} callback
*/

Change.rectifyAll = function(cb) {
// this should be optimized
this.find(function(err, changes) {
if(err) return cb(err);
changes.forEach(function(change) {
change.rectify();
});
});
}

/**
* Get the checkpoint model.
* @return {Checkpoint}
*/

Change.getCheckpointModel = function() {
var checkpointModel = this.Checkpoint;
if(checkpointModel) return checkpointModel;
this.checkpoint = checkpointModel = require('./checkpoint').extend('checkpoint');
checkpointModel.attachTo(this.dataSource);
return checkpointModel;
}


/**
* When two changes conflict a conflict is created.
*
* **Note: call `conflict.fetch()` to get the `target` and `source` models.
*
* @param {Change} sourceChange The change object for the source model
* @param {Change} targetChange The conflicting model's change object
* @property {Model} source The source model instance
* @property {Model} target The target model instance
*/

function Conflict(sourceChange, targetChange) {
this.sourceChange = sourceChange;
this.targetChange = targetChange;
}

Conflict.prototype.fetch = function(cb) {
var conflict = this;
var tasks = [
getSourceModel,
getTargetModel
];

async.parallel(tasks, cb);

function getSourceModel(change, cb) {
conflict.sourceModel.getModel(function(err, model) {
if(err) return cb(err);
conflict.source = model;
cb();
});
}

function getTargetModel(cb) {
conflict.targetModel.getModel(function(err, model) {
if(err) return cb(err);
conflict.target = model;
cb();
});
}
}

Conflict.prototype.resolve = function(cb) {
this.sourceChange.prev = this.targetChange.rev;
this.sourceChange.save(cb);
}
3 changes: 2 additions & 1 deletion lib/models/checkpoint.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,8 +48,9 @@ Checkpoint.current = function(cb) {
this.find({
limit: 1,
sort: 'id DESC'
}, function(err, checkpoint) {
}, function(err, checkpoints) {
if(err) return cb(err);
var checkpoint = checkpoints[0] || {id: 0};
cb(null, checkpoint.id);
});
}
Expand Down
Loading

0 comments on commit 2582c3f

Please sign in to comment.