Skip to content

Instantly share code, notes, and snippets.

@kristianmandrup
Last active August 29, 2015 14:05
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kristianmandrup/5d1108d6b6ccd3366656 to your computer and use it in GitHub Desktop.
Save kristianmandrup/5d1108d6b6ccd3366656 to your computer and use it in GitHub Desktop.
FireBase sync wrapper for arrays and Array.observe
/* FireBase Sync Manager */
var FirebaseSyncManager = {
getSynchronizedArray: function(firebaseRef) {
var list = [];
syncChanges(list, firebaseRef);
wrapLocalCrudOps(list, firebaseRef);
return list;
},
syncChanges: function(list, ref) {
ref.on('child_added', function _add(snap, prevChild) {
var data = snap.val();
data.$id = snap.name(); // assumes data is always an object
var pos = positionAfter(list, prevChild);
list.splice(pos, 0, data);
});
ref.on('child_removed', function _remove(snap) {
var i = positionFor(list, snap.name());
if( i > -1 ) {
list.splice(i, 1);
}
});
ref.on('child_changed', function _change(snap) {
var i = positionFor(list, snap.name());
if( i > -1 ) {
list[i] = snap.val();
list[i].$id = snap.name(); // assumes data is always an object
}
});
ref.on('child_moved', function _move(snap, prevChild) {
var curPos = positionFor(list, snap.name());
if( curPos > -1 ) {
var data = list.splice(curPos, 1)[0];
var newPos = positionAfter(list, prevChild);
list.splice(newPos, 0, data);
}
});
wrapLocalCrudOps: function(list, firebaseRef) {
// we can hack directly on the array to provide some convenience methods
list.$add = function(data) {
return firebaseRef.push(data);
};
list.$remove = function(key) {
firebaseRef.child(key).remove();
};
list.$removeList = function(keys) {
keys.forEach(function(key) {
keys.$remove(key);
});
};
list.$set = function(key, newData) {
// make sure we don't accidentally push our $id prop
if( newData.hasOwnProperty('$id') ) { delete newData.$id; }
firebaseRef.child(key).set(newData);
};
list.$indexOf = function(key) {
return positionFor(list, key); // positionFor in examples above
}
list.$idOf = function(index) {
return idFor(list, index);
}
}
// similar to indexOf, but uses id to find element
function idFor(list, index) {
if (index >= list.length || index < 0) {
throw Error("Index out of bounds", index)
}
return list[index].$id;
}
// similar to indexOf, but uses id to find element
function positionFor(list, key) {
for(var i = 0, len = list.length; i < len; i++) {
if( list[i].$id === key ) {
return i;
}
}
return -1;
}
// using the Firebase API's prevChild behavior, we
// place each element in the list after it's prev
// sibling or, if prevChild is null, at the beginning
function positionAfter(list, prevChild) {
if( prevChild === null ) {
return 0;
}
else {
var i = positionFor(list, prevChild);
if( i === -1 ) {
return list.length;
}
else {
return i+1;
}
}
}
}
var ChangeManager = function (list) {
function anyRemoved(change) {
return change.type == 'splice' && change.removed.length > 0;
}
function anyAdded(change) {
return change.type == 'splice' && change.addedCount > 0;
}
function tryRemove(change) {
if (!anyRemoved(change)) { return false ; }
// one or more items were removed
return remove(change.removed);
}
function remove(items) {
var anyRemoved = false;
var self = this;
items.forEach(function(removedItem) {
if (isObject(removedItem)) {
self.anyRemoved = true;
self.list.$remove(removedItem.$id)
}
});
return anyRemoved;
}
function tryAdd(change) {
if (!anyAdded(change)) { return false; }
// one or more items were added
return add(addedItems(change));
}
function add(items) {
var anyAdded = false;
var self = this;
items.forEach(function(addedItem) {
if (isObject(addedItem)) {
self.anyAdded = true;
self.list.$add(addedItem);
}
});
return anyAdded;
}
function addedItems(change) {
var lastIndex = change.index + change.addedCount + 1;
return this.list.slice(change.index, lastIndex);
}
function isObject(value) {
return typeof value == 'object'
}
function exists(value) {
return value !== undefined && value !== null;
}
this.list = list || [];
this.doChange = function(change) {
// TODO: should be done in parallel
return {
added: tryAdd(change),
removed: tryRemove(change)
};
}
}

To use this with immutable data structures such as https://github.com/facebook/immutable-js

"Almost all of the methods on Array will be found in similar form on Immutable.Vector, those of Map found on Immutable.Map, and those of Set found on Immutable.Set, including sequence operations like forEach and map."

function asImmutable(data) {
  return Immutable.fromJS(data);
}

function dataWrap(data) {
  return asImmutable(data);
}

function valueOf(snap) {
  return dataWrap(snap.val());
}
syncChanges: function(list, ref) {
  ref.on('child_added', function _add(snap, prevChild) {
    var data = valueOf(snap);
    data.$id = snap.name(); // assumes data is always an object
    var pos = positionAfter(list, prevChild);
    list.splice(pos, 0, data);
  });

  ref.on('child_removed', function _remove(snap) {
    var i = positionFor(list, snap.name());
    if( i > -1 ) {
      list.splice(i, 1);
    }
  });

  ref.on('child_changed', function _change(snap) {
    var i = positionFor(list, snap.name());
    if( i > -1 ) {
      list[i] = valueOf(snap);
      list[i].$id = snap.name(); // assumes data is always an object
    }
  });

  ref.on('child_moved', function _move(snap, prevChild) {
    var curPos = positionFor(list, snap.name());
    if( curPos > -1 ) {
      var data = list.splice(curPos, 1)[0];
      var newPos = positionAfter(list, prevChild);
      list.splice(newPos, 0, data);
    }
  });

The initial data must also be made into immutable data structure

getSynchronizedArray: function(firebaseRef) {
  var list = valueOf([]);
  syncChanges(list, firebaseRef);
  // Todo is class name and is f.ex used to find Validator
  wrapLocalCrudOps(list, firebaseRef, new SyncStore('Todo');
  return list;
}

Finally, make the wrapCruds more "intelligent"

Validator: function(options) {
  this.repo = options.repo || {}
  return {
    findFor: function(className) {
      // find validator in repository using className
      return this.repo[className]    
    },
    addFor: function(className, validator) {
      this.repo[className] = validator;
    }
    removeFor: function(className) {
      this.repo.delete(className);
    }
    repo: this.repo
  }
}

SyncStore: function(options) {    
  var options = options || {};

  var validatorFor = function(className) {
    return Validator.findFor(className);
  }

  this.className = options.className || 'Base';
  this.validator = options.validator || validatorFor(this.className);

  this.updateData = function(positionRef, operation, data) {    
    var isValid = function(data) {
      if (!this.validator) { return true; }        
      return this.validator.validate(data);
    }

    if (isValid(data)) {
      console.log(operation, data);
      return positionRef.call(positionRef[operation], data);  
    }    
  }
}

wrapLocalCrudOps: function(list, firebaseRef, syncStore) {
  // we can hack directly on the array to provide some convenience methods    
  list.$add = function(data) {
    return syncStore.updateData(firebaseRef, 'push', data);
  };
  
  list.$remove = function(key) {
   firebaseRef.child(key).remove();
  };

  list.$removeList = function(keys) {
    keys.forEach(function(key) {
      keys.$remove(key);
    });
  };

  list.$set = function(key, newData) {
   // make sure we don't accidentally push our $id prop
   if( newData.hasOwnProperty('$id') ) { delete newData.$id; }
   var positionRef = firebaseRef.child(key)
   return syncStore.updateData(positionRef, 'set', newData);
  };
  ...
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment