Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP

Loading…

Model Sync Local Storage - sync layer for Model and ModelList #190

Closed
wants to merge 7 commits into from

4 participants

Clarence Leung Eric Ferraiuolo Juan Ignacio Dopazo Andrew Wooldridge
Clarence Leung
Collaborator

Looks like I'm probably too late for this release, but here goes:

This provides Y.ModelSync.Local, an extension which provides a sync implementation through locally stored
key value pairs, either through the HTML localStorage API or falling back onto an in-memory cache, that can be mixed into a Model or ModelList subclass.

A group of Models/ModelLists is serialized in localStorage by either its class name, or a specified 'root' that is provided.

Example Usage

var User = Y.Base.create('user', Y.Model, [Y.ModelSync.Local], {
    root: 'users'
});

var Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.Local], {
    model: User,
    root  : 'users'
});

var myUser = new User({id: '123'});

// Will load the data from the in-memory object
// or HTML5 localStorage
myUser.load(); 

// Will update the in-memory object 
// and HTML5 localStorage
myUser.set('name', 'Eric F').save();

// Will delete the item in the in-memory object 
// and HTML5 localStorage
myUser.destroy({remove: true});

// Will create a new item in the in-memory object 
// and localStorage under key 'users' with a generated GUID
var newUser = new User({name: 'Clarence L'});
newUser.save(); 

To-do

Has API docs, high test coverage, and is only missing user documentation. However, there is an example writen for TodoMVC that can be found here:

https://github.com/clarle/todomvc/tree/yui-3.6.0/architecture-examples/yuilibrary

Eric Ferraiuolo ericf was assigned
Eric Ferraiuolo
Owner

(Keeping a record of what we decided in IRC)

@clarle Looks good on first glance! But let's keep iterating on this in Gallery and put it through its paces some more. We'll plan on merging this in for 3.7.0pr1.

src/app/build-model-sync-local.properties
@@ -0,0 +1,8 @@
+builddir=../../../builder/componentbuild
+srcdir=../..
+
+#global.build.component=${srcdir}/build/app
+
+component=model-sync-local
+component.jsfiles=model-extensions/model-sync-local.js
+component.requires=model, model-list, json-stringify
Eric Ferraiuolo Owner
ericf added a note

We don't have to explicitly require model-list. I left it out of ModelSync.REST's requirements too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/meta/app.json
@@ -63,6 +64,14 @@
]
},
+ "model-sync-local": {
+ "requires": [
+ "model",
+ "model-list",
Eric Ferraiuolo Owner
ericf added a note

Remove model-list from here too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/js/model-extensions/model-sync-local.js
((4 lines not shown))
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+@module app
+@submodule model-sync-local
+@since 3.6.0
+**/
+
+/**
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+A group of Models/ModelLists is serialized in localStorage by either its
+class name, or a specified 'root' that is provided.
+
+ var User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
Eric Ferraiuolo Owner
ericf added a note

s/REST/Local/

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
src/app/js/model-extensions/model-sync-local.js
((8 lines not shown))
+@since 3.6.0
+**/
+
+/**
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+A group of Models/ModelLists is serialized in localStorage by either its
+class name, or a specified 'root' that is provided.
+
+ var User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
+ root: 'user'
+ });
+
+ var Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
Eric Ferraiuolo Owner
ericf added a note

s/REST/Local/

Clarence Leung Collaborator
clarle added a note

Oops, that's a little embarassing...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Juan Ignacio Dopazo
Collaborator

Looks great!

Clarence Leung
Collaborator

@juandopazo and anyone else interested in testing it out, ModelSync.Local is available in the Gallery here:

http://yuilibrary.com/gallery/show/model-sync-local

Juan Ignacio Dopazo
Collaborator

I'm totally using this for a new project :)

Juan Ignacio Dopazo juandopazo commented on the diff
src/app/js/model-extensions/model-sync-local.js
((78 lines not shown))
+ **/
+ storage: null,
+
+ // -- Lifecycle Methods -----------------------------------------------------
+ initializer: function (config) {
+ var store;
+
+ config || (config = {});
+
+ this.root = config.root || this.constructor.NAME;
+
+ try {
+ this.storage = Y.config.win.localStorage;
+ store = this.storage.getItem(this.root);
+ } catch (e) {
+ Y.log("Could not access localStorage.", "warn");
Juan Ignacio Dopazo Collaborator

How do you propose to use the extension? Like this?

var exts = [];
if (Y.config.win.localStorage) {
  exts.push(Y.ModelSync.Local);
}
var Foo = Y.Base.create('foo', Y.Model, exts)?
Clarence Leung Collaborator
clarle added a note

It should fall back to an in-memory store if Y.config.win.localStorage is not available.

Juan Ignacio Dopazo Collaborator

Does it make sense to use Y.CacheOffline?

Clarence Leung Collaborator
clarle added a note

@ericf and I discussed this before, and we decided that Y.CacheOffline had too much overhead and not all of the parts in it would be needed for ModelSync.Local, so just a quick re-implementation of the core portion of it would be fine. Any thoughts on your side?

Juan Ignacio Dopazo Collaborator

Nope, I was thinking exactly the same. Let me know if I you need help with the implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Eric Ferraiuolo
Owner

A quick update:

I am going to need to wait until after this sprint to pull this in (code freeze is this Friday, 9/7).

That said, I've been thinking about using localStorage in conjunction with ModelSync.REST, which could enable a couple of things:

  1. Offline support: This seem great for mobile devices which may have a spotty connection. This is probably a huge rabbit hole, since master-master replication is a PITA and app-specific.

  2. Multi-tab support: One thing I really hate is having an app open in two tabs and each one displaying a different state. By passing all changes through localStorage, the sync layer can listen to the storage event and to get updates which are happening in other tabs/windows of that browser.

Juan Ignacio Dopazo
Collaborator

\o/ Thanks for tackling my problems!

Clarence Leung
Collaborator

Yeah, I understand, it's for the better. I've made some changes to the Gallery version of LocalStorage that I haven't synced up to this PR yet, my apologies.

I still need to do more testing on it myself in the meantime and write user documentation for it. There might be a fairly major API change for this module (I forgot about the use case where people can have multiple ModelLists that share the same Model), so that's one thing I need to work on.

Andrew Wooldridge
Collaborator

@clarle would it make sense to close this one out in favor of your new one #385 ?

Clarence Leung
Collaborator

@triptych Yep, sounds good to me.

Clarence Leung clarle closed this
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
This page is out of date. Refresh to see the latest.
264 build/model-sync-local/model-sync-local-debug.js
View
@@ -0,0 +1,264 @@
+YUI.add('model-sync-local', function(Y) {
+
+/*
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+@module app
+@submodule model-sync-local
+@since 3.6.0
+**/
+
+/**
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+A group of Models/ModelLists is serialized in localStorage by either its
+class name, or a specified 'root' that is provided.
+
+ var User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
+ root: 'user'
+ });
+
+ var Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
+ model: User,
+ root : 'user'
+ });
+
+@class ModelSync.Local
+@extensionfor Model
+@extensionfor ModelList
+@since 3.6.0
+**/
+function LocalSync() {}
+
+/**
+Properties that shouldn't be turned into ad-hoc attributes when passed to a
+Model or ModelList constructor.
+
+@property _NON_ATTRS_CFG
+@type Array
+@default ['root'']
+@static
+@protected
+@since 3.6.0
+**/
+LocalSync._NON_ATTRS_CFG = ['root'];
+
+/**
+Object of key/value pairs to fall back on when localStorage is not available.
+
+@property _data
+@type Object
+@private
+**/
+LocalSync._data = {};
+
+LocalSync.prototype = {
+
+ // -- Public Methods -------------------------------------------------------
+
+ /**
+ Root used as the key inside of localStorage and/or the in-memory store.
+
+ @property root
+ @type String
+ @default ""
+ @since 3.6.0
+ **/
+ root: '',
+
+ /**
+ Shortcut for access to localStorage.
+
+ @property storage
+ @type Storage
+ @default null
+ @since 3.6.0
+ **/
+ storage: null,
+
+ // -- Lifecycle Methods -----------------------------------------------------
+ initializer: function (config) {
+ var store;
+
+ config || (config = {});
+
+ this.root = config.root || this.constructor.NAME;
+
+ try {
+ this.storage = Y.config.win.localStorage;
+ store = this.storage.getItem(this.root);
+ } catch (e) {
+ Y.log("Could not access localStorage.", "warn");
+ }
+
+ // Pull in existing data from localStorage, if possible
+ LocalSync._data[this.root] = (store && Y.JSON.parse(store)) || {};
+ },
+
+ // -- Public Methods -----------------------------------------------------------
+
+ /**
+ Creates a synchronization layer with the localStorage API, if available.
+ Otherwise, falls back to a in-memory data store.
+
+ This method is called internally by load(), save(), and destroy().
+
+ @method sync
+ @param {String} action Sync action to perform. May be one of the following:
+
+ * **create**: Store a newly-created model for the first time.
+ * **read** : Load an existing model.
+ * **update**: Update an existing model.
+ * **delete**: Delete an existing model.
+
+ @param {Object} [options] Sync options
+ @param {callback} [callback] Called when the sync operation finishes.
+ @param {Error|null} callback.err If an error occurred, this parameter will
+ contain the error. If the sync operation succeeded, _err_ will be
+ falsy.
+ @param {Any} [callback.response] The response from our sync. This value will
+ be passed to the parse() method, which is expected to parse it and
+ return an attribute hash.
+ **/
+ sync: function (action, options, callback) {
+ options || (options = {});
+ var response;
+
+ switch (action) {
+ case 'read':
+ if (this._isYUIModelList) {
+ response = this._index(options);
+ } else {
+ response = this._show(options);
+ }
+ break;
+ case 'create':
+ response = this._create(options);
+ break;
+ case 'update':
+ response = this._update(options);
+ break;
+ case 'delete':
+ response = this._destroy(options);
+ break;
+ }
+
+ if (response) {
+ callback(null, response);
+ } else {
+ callback('Data not found');
+ }
+ },
+
+ // -- Protected Methods ----------------------------------------------------
+
+ /**
+ Sync method correlating to the "read" operation, for a Model List
+
+ @method _index
+ @return {Object[]} Array of objects found for that root key
+ @protected
+ @since 3.6.0
+ **/
+ _index: function (options) {
+ return Y.Object.values(LocalSync._data[this.root]);
+ },
+
+ /**
+ Sync method correlating to the "read" operation, for a Model
+
+ @method _show
+ @return {Object} Object found for that root key and model ID
+ @protected
+ @since 3.6.0
+ **/
+ _show: function (options) {
+ return LocalSync._data[this.root][this.get('id')];
+ },
+
+ /**
+ Sync method correlating to the "create" operation
+
+ @method _show
+ @return {Object} The new object created.
+ @protected
+ @since 3.6.0
+ **/
+ _create: function (options) {
+ var hash = this.toJSON();
+ hash.id = this._generateID(this.root);
+ LocalSync._data[this.root][hash.id] = hash;
+
+ this._save();
+ return hash;
+ },
+
+ /**
+ Sync method correlating to the "update" operation. Merges
+
+ @method _update
+ @return {Object} The updated object.
+ @protected
+ @since 3.6.0
+ **/
+ _update: function (options) {
+ var hash = this.toJSON();
+ LocalSync._data[this.root][this.get('id')] = hash;
+
+ this._save();
+ return hash;
+ },
+
+ /**
+ Sync method correlating to the "delete" operation. Deletes the data
+ from the in-memory object, and saves into localStorage if available.
+
+ @method _destroy
+ @return {Object} The deleted object.
+ @protected
+ @since 3.6.0
+ **/
+ _destroy: function (options) {
+ delete LocalSync._data[this.root][this.get('id')];
+ this._save();
+ return this.toJSON();
+ },
+
+ /**
+ Saves the current in-memory store into a localStorage key/value pair
+ if localStorage is available; otherwise, does nothing.
+
+ @method _save
+ @protected
+ @since 3.6.0
+ **/
+ _save: function () {
+ this.storage && this.storage.setItem(
+ this.root,
+ Y.JSON.stringify(LocalSync._data[this.root])
+ );
+ },
+
+ /**
+ Generate a random GUID for our Models. This can be overriden if you have
+ another method of generating different IDs.
+
+ @method _generateID
+ @protected
+ @param {String} pre Optional GUID prefix
+ **/
+ _generateID: function (pre) {
+ return Y.guid(pre + '_');
+ }
+};
+
+// -- Namespace ---------------------------------------------------------------
+
+Y.namespace('ModelSync').Local = LocalSync;
+
+
+}, '@VERSION@' ,{requires:['model', 'model-list', 'json-stringify']});
1  build/model-sync-local/model-sync-local-min.js
View
@@ -0,0 +1 @@
+YUI.add("model-sync-local",function(b){function a(){}a._NON_ATTRS_CFG=["root"];a._data={};a.prototype={root:"",storage:null,initializer:function(d){var c;d||(d={});this.root=d.root||this.constructor.NAME;try{this.storage=b.config.win.localStorage;c=this.storage.getItem(this.root);}catch(f){}a._data[this.root]=(c&&b.JSON.parse(c))||{};},sync:function(e,d,f){d||(d={});var c;switch(e){case"read":if(this._isYUIModelList){c=this._index(d);}else{c=this._show(d);}break;case"create":c=this._create(d);break;case"update":c=this._update(d);break;case"delete":c=this._destroy(d);break;}if(c){f(null,c);}else{f("Data not found");}},_index:function(c){return b.Object.values(a._data[this.root]);},_show:function(c){return a._data[this.root][this.get("id")];},_create:function(c){var d=this.toJSON();d.id=this._generateID(this.root);a._data[this.root][d.id]=d;this._save();return d;},_update:function(c){var d=this.toJSON();a._data[this.root][this.get("id")]=d;this._save();return d;},_destroy:function(c){delete a._data[this.root][this.get("id")];this._save();return this.toJSON();},_save:function(){this.storage&&this.storage.setItem(this.root,b.JSON.stringify(a._data[this.root]));},_generateID:function(c){return b.guid(c+"_");}};b.namespace("ModelSync").Local=a;},"@VERSION@",{requires:["model","model-list","json-stringify"]});
263 build/model-sync-local/model-sync-local.js
View
@@ -0,0 +1,263 @@
+YUI.add('model-sync-local', function(Y) {
+
+/*
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+@module app
+@submodule model-sync-local
+@since 3.6.0
+**/
+
+/**
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+A group of Models/ModelLists is serialized in localStorage by either its
+class name, or a specified 'root' that is provided.
+
+ var User = Y.Base.create('user', Y.Model, [Y.ModelSync.REST], {
+ root: 'user'
+ });
+
+ var Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.REST], {
+ model: User,
+ root : 'user'
+ });
+
+@class ModelSync.Local
+@extensionfor Model
+@extensionfor ModelList
+@since 3.6.0
+**/
+function LocalSync() {}
+
+/**
+Properties that shouldn't be turned into ad-hoc attributes when passed to a
+Model or ModelList constructor.
+
+@property _NON_ATTRS_CFG
+@type Array
+@default ['root'']
+@static
+@protected
+@since 3.6.0
+**/
+LocalSync._NON_ATTRS_CFG = ['root'];
+
+/**
+Object of key/value pairs to fall back on when localStorage is not available.
+
+@property _data
+@type Object
+@private
+**/
+LocalSync._data = {};
+
+LocalSync.prototype = {
+
+ // -- Public Methods -------------------------------------------------------
+
+ /**
+ Root used as the key inside of localStorage and/or the in-memory store.
+
+ @property root
+ @type String
+ @default ""
+ @since 3.6.0
+ **/
+ root: '',
+
+ /**
+ Shortcut for access to localStorage.
+
+ @property storage
+ @type Storage
+ @default null
+ @since 3.6.0
+ **/
+ storage: null,
+
+ // -- Lifecycle Methods -----------------------------------------------------
+ initializer: function (config) {
+ var store;
+
+ config || (config = {});
+
+ this.root = config.root || this.constructor.NAME;
+
+ try {
+ this.storage = Y.config.win.localStorage;
+ store = this.storage.getItem(this.root);
+ } catch (e) {
+ }
+
+ // Pull in existing data from localStorage, if possible
+ LocalSync._data[this.root] = (store && Y.JSON.parse(store)) || {};
+ },
+
+ // -- Public Methods -----------------------------------------------------------
+
+ /**
+ Creates a synchronization layer with the localStorage API, if available.
+ Otherwise, falls back to a in-memory data store.
+
+ This method is called internally by load(), save(), and destroy().
+
+ @method sync
+ @param {String} action Sync action to perform. May be one of the following:
+
+ * **create**: Store a newly-created model for the first time.
+ * **read** : Load an existing model.
+ * **update**: Update an existing model.
+ * **delete**: Delete an existing model.
+
+ @param {Object} [options] Sync options
+ @param {callback} [callback] Called when the sync operation finishes.
+ @param {Error|null} callback.err If an error occurred, this parameter will
+ contain the error. If the sync operation succeeded, _err_ will be
+ falsy.
+ @param {Any} [callback.response] The response from our sync. This value will
+ be passed to the parse() method, which is expected to parse it and
+ return an attribute hash.
+ **/
+ sync: function (action, options, callback) {
+ options || (options = {});
+ var response;
+
+ switch (action) {
+ case 'read':
+ if (this._isYUIModelList) {
+ response = this._index(options);
+ } else {
+ response = this._show(options);
+ }
+ break;
+ case 'create':
+ response = this._create(options);
+ break;
+ case 'update':
+ response = this._update(options);
+ break;
+ case 'delete':
+ response = this._destroy(options);
+ break;
+ }
+
+ if (response) {
+ callback(null, response);
+ } else {
+ callback('Data not found');
+ }
+ },
+
+ // -- Protected Methods ----------------------------------------------------
+
+ /**
+ Sync method correlating to the "read" operation, for a Model List
+
+ @method _index
+ @return {Object[]} Array of objects found for that root key
+ @protected
+ @since 3.6.0
+ **/
+ _index: function (options) {
+ return Y.Object.values(LocalSync._data[this.root]);
+ },
+
+ /**
+ Sync method correlating to the "read" operation, for a Model
+
+ @method _show
+ @return {Object} Object found for that root key and model ID
+ @protected
+ @since 3.6.0
+ **/
+ _show: function (options) {
+ return LocalSync._data[this.root][this.get('id')];
+ },
+
+ /**
+ Sync method correlating to the "create" operation
+
+ @method _show
+ @return {Object} The new object created.
+ @protected
+ @since 3.6.0
+ **/
+ _create: function (options) {
+ var hash = this.toJSON();
+ hash.id = this._generateID(this.root);
+ LocalSync._data[this.root][hash.id] = hash;
+
+ this._save();
+ return hash;
+ },
+
+ /**
+ Sync method correlating to the "update" operation. Merges
+
+ @method _update
+ @return {Object} The updated object.
+ @protected
+ @since 3.6.0
+ **/
+ _update: function (options) {
+ var hash = this.toJSON();
+ LocalSync._data[this.root][this.get('id')] = hash;
+
+ this._save();
+ return hash;
+ },
+
+ /**
+ Sync method correlating to the "delete" operation. Deletes the data
+ from the in-memory object, and saves into localStorage if available.
+
+ @method _destroy
+ @return {Object} The deleted object.
+ @protected
+ @since 3.6.0
+ **/
+ _destroy: function (options) {
+ delete LocalSync._data[this.root][this.get('id')];
+ this._save();
+ return this.toJSON();
+ },
+
+ /**
+ Saves the current in-memory store into a localStorage key/value pair
+ if localStorage is available; otherwise, does nothing.
+
+ @method _save
+ @protected
+ @since 3.6.0
+ **/
+ _save: function () {
+ this.storage && this.storage.setItem(
+ this.root,
+ Y.JSON.stringify(LocalSync._data[this.root])
+ );
+ },
+
+ /**
+ Generate a random GUID for our Models. This can be overriden if you have
+ another method of generating different IDs.
+
+ @method _generateID
+ @protected
+ @param {String} pre Optional GUID prefix
+ **/
+ _generateID: function (pre) {
+ return Y.guid(pre + '_');
+ }
+};
+
+// -- Namespace ---------------------------------------------------------------
+
+Y.namespace('ModelSync').Local = LocalSync;
+
+
+}, '@VERSION@' ,{requires:['model', 'model-list', 'json-stringify']});
8 src/app/build-model-sync-local.properties
View
@@ -0,0 +1,8 @@
+builddir=../../../builder/componentbuild
+srcdir=../..
+
+#global.build.component=${srcdir}/build/app
+
+component=model-sync-local
+component.jsfiles=model-extensions/model-sync-local.js
+component.requires=model, json-stringify
7 src/app/build-model-sync-local.xml
View
@@ -0,0 +1,7 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project name="YUI" default="local">
+ <property environment="env" />
+ <property file="build-model-sync-local.properties" />
+ <import file="${builddir}/3.x/bootstrap.xml"
+ description="Default Build Properties and Targets" />
+</project>
259 src/app/js/model-extensions/model-sync-local.js
View
@@ -0,0 +1,259 @@
+/*
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+@module app
+@submodule model-sync-local
+@since 3.6.0
+**/
+
+/**
+An extension which provides a sync implementation through locally stored
+key value pairs, either through the HTML localStorage API or falling back
+onto an in-memory cache, that can be mixed into a Model or ModelList subclass.
+
+A group of Models/ModelLists is serialized in localStorage by either its
+class name, or a specified 'root' that is provided.
+
+ var User = Y.Base.create('user', Y.Model, [Y.ModelSync.Local], {
+ root: 'user'
+ });
+
+ var Users = Y.Base.create('users', Y.ModelList, [Y.ModelSync.Local], {
+ model: User,
+ root : 'user'
+ });
+
+@class ModelSync.Local
+@extensionfor Model
+@extensionfor ModelList
+@since 3.6.0
+**/
+function LocalSync() {}
+
+/**
+Properties that shouldn't be turned into ad-hoc attributes when passed to a
+Model or ModelList constructor.
+
+@property _NON_ATTRS_CFG
+@type Array
+@default ['root'']
+@static
+@protected
+@since 3.6.0
+**/
+LocalSync._NON_ATTRS_CFG = ['root'];
+
+/**
+Object of key/value pairs to fall back on when localStorage is not available.
+
+@property _data
+@type Object
+@private
+**/
+LocalSync._data = {};
+
+LocalSync.prototype = {
+
+ // -- Public Methods -------------------------------------------------------
+
+ /**
+ Root used as the key inside of localStorage and/or the in-memory store.
+
+ @property root
+ @type String
+ @default ""
+ @since 3.6.0
+ **/
+ root: '',
+
+ /**
+ Shortcut for access to localStorage.
+
+ @property storage
+ @type Storage
+ @default null
+ @since 3.6.0
+ **/
+ storage: null,
+
+ // -- Lifecycle Methods -----------------------------------------------------
+ initializer: function (config) {
+ var store;
+
+ config || (config = {});
+
+ this.root = config.root || this.constructor.NAME;
+
+ try {
+ this.storage = Y.config.win.localStorage;
+ store = this.storage.getItem(this.root);
+ } catch (e) {
+ Y.log("Could not access localStorage.", "warn");
Juan Ignacio Dopazo Collaborator

How do you propose to use the extension? Like this?

var exts = [];
if (Y.config.win.localStorage) {
  exts.push(Y.ModelSync.Local);
}
var Foo = Y.Base.create('foo', Y.Model, exts)?
Clarence Leung Collaborator
clarle added a note

It should fall back to an in-memory store if Y.config.win.localStorage is not available.

Juan Ignacio Dopazo Collaborator

Does it make sense to use Y.CacheOffline?

Clarence Leung Collaborator
clarle added a note

@ericf and I discussed this before, and we decided that Y.CacheOffline had too much overhead and not all of the parts in it would be needed for ModelSync.Local, so just a quick re-implementation of the core portion of it would be fine. Any thoughts on your side?

Juan Ignacio Dopazo Collaborator

Nope, I was thinking exactly the same. Let me know if I you need help with the implementation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
+ }
+
+ // Pull in existing data from localStorage, if possible
+ LocalSync._data[this.root] = (store && Y.JSON.parse(store)) || {};
+ },
+
+ // -- Public Methods -----------------------------------------------------------
+
+ /**
+ Creates a synchronization layer with the localStorage API, if available.
+ Otherwise, falls back to a in-memory data store.
+
+ This method is called internally by load(), save(), and destroy().
+
+ @method sync
+ @param {String} action Sync action to perform. May be one of the following:
+
+ * **create**: Store a newly-created model for the first time.
+ * **read** : Load an existing model.
+ * **update**: Update an existing model.
+ * **delete**: Delete an existing model.
+
+ @param {Object} [options] Sync options
+ @param {callback} [callback] Called when the sync operation finishes.
+ @param {Error|null} callback.err If an error occurred, this parameter will
+ contain the error. If the sync operation succeeded, _err_ will be
+ falsy.
+ @param {Any} [callback.response] The response from our sync. This value will
+ be passed to the parse() method, which is expected to parse it and
+ return an attribute hash.
+ **/
+ sync: function (action, options, callback) {
+ options || (options = {});
+ var response;
+
+ switch (action) {
+ case 'read':
+ if (this._isYUIModelList) {
+ response = this._index(options);
+ } else {
+ response = this._show(options);
+ }
+ break;
+ case 'create':
+ response = this._create(options);
+ break;
+ case 'update':
+ response = this._update(options);
+ break;
+ case 'delete':
+ response = this._destroy(options);
+ break;
+ }
+
+ if (response) {
+ callback(null, response);
+ } else {
+ callback('Data not found');
+ }
+ },
+
+ // -- Protected Methods ----------------------------------------------------
+
+ /**
+ Sync method correlating to the "read" operation, for a Model List
+
+ @method _index
+ @return {Object[]} Array of objects found for that root key
+ @protected
+ @since 3.6.0
+ **/
+ _index: function (options) {
+ return Y.Object.values(LocalSync._data[this.root]);
+ },
+
+ /**
+ Sync method correlating to the "read" operation, for a Model
+
+ @method _show
+ @return {Object} Object found for that root key and model ID
+ @protected
+ @since 3.6.0
+ **/
+ _show: function (options) {
+ return LocalSync._data[this.root][this.get('id')];
+ },
+
+ /**
+ Sync method correlating to the "create" operation
+
+ @method _show
+ @return {Object} The new object created.
+ @protected
+ @since 3.6.0
+ **/
+ _create: function (options) {
+ var hash = this.toJSON();
+ hash.id = this._generateID(this.root);
+ LocalSync._data[this.root][hash.id] = hash;
+
+ this._save();
+ return hash;
+ },
+
+ /**
+ Sync method correlating to the "update" operation. Merges
+
+ @method _update
+ @return {Object} The updated object.
+ @protected
+ @since 3.6.0
+ **/
+ _update: function (options) {
+ var hash = this.toJSON();
+ LocalSync._data[this.root][this.get('id')] = hash;
+
+ this._save();
+ return hash;
+ },
+
+ /**
+ Sync method correlating to the "delete" operation. Deletes the data
+ from the in-memory object, and saves into localStorage if available.
+
+ @method _destroy
+ @return {Object} The deleted object.
+ @protected
+ @since 3.6.0
+ **/
+ _destroy: function (options) {
+ delete LocalSync._data[this.root][this.get('id')];
+ this._save();
+ return this.toJSON();
+ },
+
+ /**
+ Saves the current in-memory store into a localStorage key/value pair
+ if localStorage is available; otherwise, does nothing.
+
+ @method _save
+ @protected
+ @since 3.6.0
+ **/
+ _save: function () {
+ this.storage && this.storage.setItem(
+ this.root,
+ Y.JSON.stringify(LocalSync._data[this.root])
+ );
+ },
+
+ /**
+ Generate a random GUID for our Models. This can be overriden if you have
+ another method of generating different IDs.
+
+ @method _generateID
+ @protected
+ @param {String} pre Optional GUID prefix
+ **/
+ _generateID: function (pre) {
+ return Y.guid(pre + '_');
+ }
+};
+
+// -- Namespace ---------------------------------------------------------------
+
+Y.namespace('ModelSync').Local = LocalSync;
8 src/app/meta/app.json
View
@@ -7,6 +7,7 @@
"model",
"model-list",
"model-sync-rest",
+ "model-sync-local",
"router",
"view",
"view-node-map"
@@ -63,6 +64,13 @@
]
},
+ "model-sync-local": {
+ "requires": [
+ "model",
+ "json-stringify"
+ ]
+ },
+
"model-sync-rest": {
"requires": [
"model",
6 src/app/tests/app.html
View
@@ -26,6 +26,7 @@
'lazy-model-list-test',
'model-test',
'model-list-test',
+ 'model-sync-local-test',
'model-sync-rest-test',
'router-test',
'view-test',
@@ -49,6 +50,11 @@
requires: ['model-list', 'test']
},
+ 'model-sync-local-test': {
+ fullpath: 'model-sync-local-test.js',
+ requires: ['model', 'model-list', 'model-sync-local', 'test']
+ },
+
'model-sync-rest-test': {
fullpath: 'model-sync-rest-test.js',
requires: ['model', 'model-list', 'model-sync-rest', 'test']
170 src/app/tests/model-sync-local-test.js
View
@@ -0,0 +1,170 @@
+YUI.add('model-sync-local-test', function (Y) {
+
+var ArrayAssert = Y.ArrayAssert,
+ Assert = Y.Assert,
+ ObjectAssert = Y.ObjectAssert,
+
+ suite,
+ modelSyncLocalSuite;
+
+// -- Global Suite -------------------------------------------------------------
+suite = Y.AppTestSuite || (Y.AppTestSuite = new Y.Test.Suite('App Framework'));
+
+// -- ModelSync.Local Suite ----------------------------------------------------
+modelSyncLocalSuite = new Y.Test.Suite('ModelSync.Local');
+
+// -- ModelSync.Local: Lifecycle -----------------------------------------------
+modelSyncLocalSuite.add(new Y.Test.Case({
+ name: 'Lifecycle',
+
+ setUp: function () {
+ Y.TestModel = Y.Base.create('customModel', Y.Model, [Y.ModelSync.Local]);
+
+ Y.TestModelList = Y.Base.create('testModelList', Y.ModelList, [Y.ModelSync.Local], {
+ model: Y.TestModel
+ })
+ },
+
+ tearDown: function () {
+ delete Y.TestModel;
+ delete Y.TestModelList;
+ },
+
+ 'initializer should set the `root` property on the instance': function () {
+ var model = new Y.TestModel({root: 'model'}),
+ modelList = new Y.TestModelList({root: 'list'});
+
+ Assert.areSame('model', model.root);
+ Assert.areSame('list', modelList.root);
+ },
+
+ '`root` property should be the class name by default': function () {
+ var model = new Y.TestModel(),
+ modelList = new Y.TestModelList();
+
+ Assert.areSame('customModel', model.root);
+ Assert.areSame('testModelList', modelList.root);
+ },
+
+ '`localStorage` should be set to the `storage` property': function () {
+ var model = new Y.TestModel(),
+ modelList = new Y.TestModelList();
+ test = 'test',
+ hasStorage = function(context) {
+ try {
+ context.storage.setItem(test, test);
+ context.storage.removeItem(test);
+ return true;
+ } catch(e) {
+ return false;
+ }
+ };
+
+ Assert.isTrue(hasStorage(model), 'Model storage not properly set');
+ Assert.isTrue(hasStorage(modelList), 'List storage not properly set');
+ },
+
+ '`data` property should be filled with any existing `localStorage` data': function () {
+ var testStore;
+ try {
+ testStore = Y.config.win.localStorage;
+ testStore.setItem('users', '{"users-1":{"id":"users-1","name":"clarle"},"users-2":{"id":"users-2","name":"eric"}}');
+ } catch (e) {
+ Y.log("Could not access localStorage.", "warn");
+ }
+
+ var model = new Y.TestModel({root: 'users', id: 'users-1'}),
+ modelList = new Y.TestModelList({ root: 'users'}),
+ data = Y.ModelSync.Local._data;
+
+ Assert.areSame('clarle', data['users']['users-1']['name']);
+ }
+}));
+
+// -- ModelSync.Local: Sync ----------------------------------------------------
+modelSyncLocalSuite.add(new Y.Test.Case({
+ name: 'Sync',
+
+ setUp: function () {
+ try {
+ testStore = Y.config.win.localStorage;
+ testStore.setItem('users', '{"users-1":{"id":"users-1","name":"clarle"},"users-2":{"id":"users-2","name":"eric"}}');
+
+ } catch (e) {
+ Y.ModelSync._data = {"users": {"users-1":{"id":"users-1","name":"clarle"},"users-2":{"id":"users-2","name":"eric"}}};
+ }
+
+ Y.TestModel = Y.Base.create('users', Y.Model, [Y.ModelSync.Local]);
+
+ Y.TestModelList = Y.Base.create('users', Y.ModelList, [Y.ModelSync.Local], {
+ model: Y.TestModel
+ })
+ },
+
+ tearDown: function () {
+ delete Y.TestModel;
+ delete Y.TestModelList;
+ try {
+ Y.config.win.storage.clear();
+ } catch (e) {
+ Y.log("Could not access localStorage.", "warn");
+ }
+ },
+
+ 'load() of Model should get the stored local object': function () {
+ var model = new Y.TestModel({id: 'users-1'});
+ model.load();
+ Assert.areSame('clarle', model.get('name'));
+ },
+
+ 'load() of ModelList should get all stored local objects': function () {
+ var modelList = new Y.TestModelList();
+ modelList.load();
+ Assert.areSame(2, modelList.size());
+ Assert.areSame('users-1', modelList.item(0).get('id'));
+ Assert.areSame('clarle', modelList.item(0).get('name'));
+ },
+
+ 'save() of a new Model should create a new object with an ID': function () {
+ var model = new Y.TestModel({name: 'dav'});
+
+ Assert.isUndefined(model.get('id'), 'Initial model ID should be undefined.');
+ model.save();
+ Assert.isNotNull(model.get('id'), 'Model ID should not be null');
+ Assert.areSame('dav', model.get('name'), 'Model should have correct name');
+ },
+
+ 'save() of an existing Model should update the object': function () {
+ var model = new Y.TestModel({id: 'users-2'});
+ model.load();
+ Assert.areSame('eric', model.get('name'), 'Model should have correct name');
+ model.set('name', 'satyen');
+ model.save();
+ Assert.areSame('satyen', model.get('name'), 'Model should have updated name');
+ },
+
+ 'destroy({remove: true}) of an existing Model should delete the object': function () {
+ var model = new Y.TestModel({id: 'users-1'}),
+ data;
+
+ model.load();
+ Assert.areSame('clarle', model.get('name'), 'Model should have correct name');
+ data = Y.ModelSync.Local._data;
+ Assert.isUndefined(data['users-1'], 'Data should be deleted');
+ },
+
+ 'Failed sync() calls should pass an error message to the callback': function () {
+ var model = new Y.TestModel({id: 'users-3'});
+
+ model.sync('read', {}, function (err, res) {
+ Assert.areSame('Data not found', err);
+ });
+ }
+}));
+
+
+suite.add(modelSyncLocalSuite);
+
+}, '@VERSION@', {
+ requires: ['model-sync-local', 'model', 'model-list', 'test']
+});
Something went wrong with that request. Please try again.