PDA

View Full Version : [1.1.0] LocalStorageProxy not handling additions correctly



mitchellsimoens
16 Mar 2011, 7:44 AM
Removing records from LocalStorageProxy has been fixed for a couple releases. I have seen a bug but worked around it but figure I would finally report it.

If you load multiple records into the store and sync the store and proxy it works. However, if any of these records has an id that is already in the local storage, it acts wrong.

Say you add two records with ids of 1 and 2. Then you add a third record with an id of 1 the key that holds the ids for the proxy will just append the id. You will end up with 1,2,1. Now if you remove the first record then you will have 2,1 except there isn't going to be a key for what is now the 2nd record with id of 1, there should only be the one with id 2.

Example:


Ext.regModel("CartModel", {
fields: [
{ name: "id", type: "int" }, //worked with type = "string"
{ name: "name", type: "string" }
]
});

var store = new Ext.data.Store({
autoLoad: true,
model: "CartModel",
proxy: {
type : "localstorage",
id : "cart-store"
}
});

store.add({
id: 1,
name: "Product 1"
});
store.add({
id: 2,
name: "Product 2"
});
store.add({
id: 1,
name: "Product 1"
});
store.sync();

setTimeout(function() {
var rec = store.getAt(0);
store.remove(rec);
store.sync();
}, 2000);

After the remove function executes, you will have this:


cart-store-2
{"id":2,"name":"Product 2"}
cart-store
2,1

jmclem
21 Apr 2011, 4:28 AM
Hi,

we are facing this problem (using Sencha Touch 1.1.0). We figured out that ensuring unique ids in WebstorageProxy.setIds() fixed the problem.

You mention that there is a bug fix; where is it available?

Thanks,

Jean-Marie.

For information, we modified the function like this:


setIds: function(ids) {
var uniqueIds = [];
var l = ids.length;
var found=false;

for(var i=0; i<l; i++) {
for(var j=0, ul=uniqueIds.length; j<ul && !found; j++) {
if (ids[i] === uniqueIds[j])
found = true;
}
if ( ! found )
uniqueIds.push(ids[i]);

found = false;
}

var obj = this.getStorageObject(),
str = uniqueIds.join(",");

obj.removeItem(this.id);

if (!Ext.isEmpty(str)) {
obj.setItem(this.id, str);
}
},

mitchellsimoens
21 Apr 2011, 5:20 AM
http://www.sencha.com/forum/showthread.php?127037-1.1.0-Id-in-WebStorageProxy-does-not-need-to-be-int

This override allows ids to be strings and it will keep the field that keeps all the ids to have unique ids meaning before it would have duplicate ids but now it will not.

jmclem
21 Apr 2011, 6:13 AM
Hi,

thanks for your answer; however, the override did not fix it for me, maybe because my id is an integer. I end up with a mix of ints and strings when entering setIds() (st like "1","2",1,2).

So I keep at the moment with my fix. Thanks anyway,

Jean-Marie.

mitchellsimoens
21 Apr 2011, 6:17 AM
Guess I didn't test on int values as this override came in response from someone wanting strings.. guess I could convert all to Strings and then compare but leave them as their original types.

mitchellsimoens
22 Apr 2011, 4:21 PM
Just tested a new override. Tested with both string ids and int ids.


Ext.override(Ext.data.WebStorageProxy, {
eliminateDuplicates : function(arr) { //could go into Array prototype (prob better name)
var i = 0,
x = 0,
len = arr.length,
out = [],
obj = {};

if (arr[0] === '') { delete arr[0]; }

for (; i < len; i++) {
obj[arr[i]] = 0;
}

for (i in obj) {
if (Ext.isDefined(i)) {
out.push(i);
}
}

return out;
},

setIds: function(ids) {
ids = this.eliminateDuplicates(ids);
var obj = this.getStorageObject(),
str = ids.join(",");

obj.removeItem(this.id);

if (!Ext.isEmpty(str)) {
obj.setItem(this.id, str);
}
},

getIds: function() {
var ids = (this.getStorageObject().getItem(this.id) || "").split(","),
length = ids.length,
i;

return ids;
}
});

Bucs
23 Apr 2011, 2:48 AM
Mitchell, I get an error here when using this override over your last one:



getRecord: function(id) {
if (this.cache[id] == undefined) {
var rawData = Ext.decode(this.getStorageObject().getItem(this.getRecordKey(id))),
data = {},
Model = this.model,
fields = Model.prototype.fields.items,
length = fields.length,
i, field, name, record;

for (i = 0; i < length; i++) {
field = fields[i];
name = field.name;

if (typeof field.decode == 'function') {
data[name] = field.decode(rawData[name]);
} else {
data[name] = rawData[name]; <== "Cannot read property 'Id' of null"
}
}


id passed in is null for me which causes the error.

mitchellsimoens
23 Apr 2011, 4:43 AM
id passed in is null for me which causes the error.

Really? I have tested with all int values, tested with int being passed in as data but type in field as string, tested with strings (numbers passed as strings), tested with number spelled out ('one', 'two', etc) and passed with long string with character ('one-instance', 'two-instance', etc). Can you give code that produces that error?

Here is my complete test case:


Ext.override(Ext.data.WebStorageProxy, {
eliminateDuplicates : function(arr) { //could go into Array prototype (prob better name)
var i = 0,
x = 0,
len = arr.length,
out = [],
obj = {};

if (arr[0] === '') { delete arr[0]; }

for (; i < len; i++) {
obj[arr[i]] = 0;
}

for (i in obj) {
if (Ext.isDefined(i)) {
out.push(i);
}
}

return out;
},

setIds: function(ids) {
ids = this.eliminateDuplicates(ids);
var obj = this.getStorageObject(),
str = ids.join(",");

obj.removeItem(this.id);

if (!Ext.isEmpty(str)) {
obj.setItem(this.id, str);
}
},

getIds: function() {
var ids = (this.getStorageObject().getItem(this.id) || "").split(","),
length = ids.length,
i;

return ids;
}
});

Ext.regModel('Test1', {
fields : [
{ name : 'id', type : 'string' },
{ name : 'firstName', type : 'string' },
{ name : 'lastName', type : 'string' }
],
proxy : {
type : 'localstorage',
id : 'test1'
}
});
Ext.regModel('Test2', {
fields : [
{ name : 'id', type : 'int' },
{ name : 'firstName', type : 'string' },
{ name : 'lastName', type : 'string' }
],
proxy : {
type : 'localstorage',
id : 'test2'
}
});

Ext.onReady(function() {
var store1 = new Ext.data.Store({
model : 'Test1',
data : [
{ id : 'one-instance', firstName : 'Mitchell', lastName : 'Simoens' },
{ id : 'two-instance', firstName : 'Doug', lastName : 'Hendricks' },
{ id : 'three-instance', firstName : 'Jack', lastName : 'Ratcliff' },
{ id : 'four-instance', firstName : 'Macy', lastName : 'Abbey' },
{ id : 'five-instance', firstName : 'Jay', lastName : 'Garcia' }
]
});

store1.sync();

var store2 = new Ext.data.Store({
model : 'Test2',
data : [
{ id : 1, firstName : 'Mitchell', lastName : 'Simoens' },
{ id : 2, firstName : 'Doug', lastName : 'Hendricks' },
{ id : 3, firstName : 'Jack', lastName : 'Ratcliff' },
{ id : 4, firstName : 'Macy', lastName : 'Abbey' },
{ id : 5, firstName : 'Jay', lastName : 'Garcia' }
]
});

store2.sync();

var panel = new Ext.Panel({
fullscreen : true,
layout : {
type : 'hbox',
align : 'stretch'
},
defaults : {
flex : 1,
autoHeight : true,
itemSelector : 'div.test-row',
tpl : new Ext.XTemplate(
'<tpl for=".">',
'<div class="test-row">{id} | {firstName} {lastName}</div>',
'</tpl>'
)
},
items : [
{
xtype : 'dataview',
store : store1
},
{
xtype : 'dataview',
store : store2
}
]
});

setTimeout(function() {
console.log('store2');
console.log(store2.getById(2));
console.log('----');
var proxy = store2.getProxy();
console.log(proxy.getRecord(3));
}, 2000);

setTimeout(function() {
console.log('store1');
console.log(store1.getById('two-instance'));
console.log('----');
var proxy = store1.getProxy();
console.log(proxy.getRecord('three-instance'));
}, 1000);
});

Bucs
23 Apr 2011, 4:52 AM
Not sure if this would cause the error or not, but I am using the idProperty of the model to change the default 'id' property.

This code fires upon app startup to set the badge text on my menu bar:


setCartBadge: function () {
var store = new Ext.data.Store({
autoLoad: false,
model: "Product",
proxy: {
type: "localstorage",
id: "cart"
}
});

store.load();

if (store != null) {
// Get cart button
var btn = this.query('#MenuBarButtons')[0].child('#btnCart');
if (btn != null) {
var items = store.data.length;
if (items > 0) {
btn.setBadge(items);
}
else {
btn.setBadge(null); // removes badge
}
}
}
}

mitchellsimoens
23 Apr 2011, 5:08 AM
I spy with my little eye looks like you are setting an id on buttons and their container :-?

So I changed my Models to have idProperty to 'personId' and changed everything and it works for me. Not sure when you got my overrides but I did make a smale change to the eliminateDuplicates function. Here is my new test case:


Ext.override(Ext.data.WebStorageProxy, {
eliminateDuplicates : function(arr) { //could go into Array prototype (prob better name)
var i = 0,
x = 0,
len = arr.length,
out = [],
obj = {};

for (; i < len; i++) {
obj[arr[i]] = 0;
}

for (i in obj) {
if (Ext.isDefined(i)) {
out.push(i);
}
}

return out;
},

setIds: function(ids) {
if (ids[0] === '') { delete ids[0]; }

ids = this.eliminateDuplicates(ids);
var obj = this.getStorageObject(),
str = ids.join(",");

obj.removeItem(this.id);

if (!Ext.isEmpty(str)) {
obj.setItem(this.id, str);
}
},

getIds: function() {
//first time will be right but after that will return duplicates. Why?
var ids = (this.getStorageObject().getItem(this.id) || "").split(","),
length = ids.length,
i;

return ids;
}
});

Ext.regModel('Test1', {
idProperty : 'personId',
fields : [
{ name : 'personId', type : 'string' },
{ name : 'firstName', type : 'string' },
{ name : 'lastName', type : 'string' }
],
proxy : {
type : 'localstorage',
id : 'test1'
}
});
Ext.regModel('Test2', {
idProperty : 'personId',
fields : [
{ name : 'personId', type : 'int' },
{ name : 'firstName', type : 'string' },
{ name : 'lastName', type : 'string' }
],
proxy : {
type : 'localstorage',
id : 'test2'
}
});

Ext.onReady(function() {
var debug = true;
var store1 = new Ext.data.Store({
model : 'Test1',
data : [
{ personId : 'one-instance', firstName : 'Mitchell', lastName : 'Simoens' },
{ personId : 'two-instance', firstName : 'Doug', lastName : 'Hendricks' },
{ personId : 'three-instance', firstName : 'Jack', lastName : 'Ratcliff' },
{ personId : 'four-instance', firstName : 'Macy', lastName : 'Abbey' },
{ personId : 'five-instance', firstName : 'Jay', lastName : 'Garcia' }
]
});

store1.sync();

var store2 = new Ext.data.Store({
model : 'Test2',
data : [
{ personId : 1, firstName : 'Mitchell', lastName : 'Simoens' },
{ personId : 2, firstName : 'Doug', lastName : 'Hendricks' },
{ personId : 3, firstName : 'Jack', lastName : 'Ratcliff' },
{ personId : 4, firstName : 'Macy', lastName : 'Abbey' },
{ personId : 5, firstName : 'Jay', lastName : 'Garcia' }
]
});

store2.sync();

var panel = new Ext.Panel({
fullscreen : true,
layout : {
type : 'hbox',
align : 'stretch'
},
defaults : {
flex : 1,
autoHeight : true,
itemSelector : 'div.test-row',
tpl : new Ext.XTemplate(
'<tpl for=".">',
'<div class="test-row">{personId} | {firstName} {lastName}</div>',
'</tpl>'
)
},
items : [
{
xtype : 'dataview',
store : store1
},
{
xtype : 'dataview',
store : store2
}
]
});

if (debug) {
setTimeout(function() {
console.log('store2');
console.log(store2.getById(2));
console.log('----');
var proxy = store2.getProxy();
console.log(proxy.getRecord(3));
}, 2000);

setTimeout(function() {
console.log('store1');
console.log(store1.getById('two-instance'));
console.log('----');
var proxy = store1.getProxy();
console.log(proxy.getRecord('three-instance'));
}, 1000);
}
});

Everything is working for me so far.

One thing is why aren't you using store.getCount() instead you are using store.data.length? If there is a function to do something, I tend to always use it as you never know if the framework is going to change something if that function does something special to make it work.

Bucs
23 Apr 2011, 5:34 AM
Good call on the store.getCount()...not sure why I wasn't using that :)

Another good point on the ids, however, in this particular case, the menubar control I use is always attached to the viewport control so it will never be used more than once in an app. But point taken...not necessary to use ids.

I will look into why I am getting this error in a bit, probably something I am doing, though the code posted above is the only code that fires to launch the override.

Bucs
23 Apr 2011, 5:39 AM
Oops, btw...I am not using id, but itemId on the menuBar. Those are not unique as far as I know, yet still allow you to easily target a control when querying. You don't like itemId either? I think that beats using item indices or something else to query for specific controls.

Bucs
23 Apr 2011, 6:04 AM
Why does this function return a length property of 1 for the ids object when I have no ids in a localstorage object? I assign the store an id of 'cart', which creates a cart record in localstorage, but if I don't have any items in the localstorage store, shouldn't the length property report a length of 0 and not 1?. That is what causes the id to be passed into the getRecord method as ""...which causes the error mentioned above.

getIds: function () {
//first time will be right but after that will return duplicates. Why?
var ids = (this.getStorageObject().getItem(this.id) || "").split(","),
length = ids.length,
i;

return ids;
}

Bucs
23 Apr 2011, 6:20 AM
The code you posted above has the slimmer getIds function than the one I have...which actually works. I am using string for the ids though, is that why you changed the code, to account for ints?

Working getIds function:



getIds: function () {
var ids = (this.getStorageObject().getItem(this.id) || "").split(","),
length = ids.length,
i;

if (length == 1 && ids[0] == "") {
ids = [];
}

var idArray = [];
for (var i = 0; i < ids.length; i++) {
var id = ids[i];
if (idArray.indexOf(id) < 0) {
idArray.push(id);
}
}

return idArray;
}

jpongin
2 Aug 2011, 1:42 PM
I have a simular but slightly different issue with LocalStorage. Given your example, when I remove a model from the store {id:1000}, then sync the store, store.sync(). That works.

However, when I add the record back into the store, store.add({id:1000}), then sync the store, store.sync(), it doesn't update localStorage.

BUG REPRODUCTION STEPS


Ext.regModel("CartModel", {
fields: [
{ name: "id", type: "int" }, //worked with type = "string"
{ name: "name", type: "string" }
]
});

var store = new Ext.data.Store({
autoLoad: true,
model: "CartModel",
proxy: {
type : "localstorage",
id : "cart-store"
}
});

//this works.
store.add({
id: 1,
name: "Product 1"
});
store.sync();

//this works.
setTimeout(function() {
var rec = store.getAt(0);
store.remove(rec);
store.sync();
}, 2000);

//this doesn't update localstorage.
store.add({
id: 1,
name: "Product 1"
});
store.sync();


The above sequence only works if you do not define or modify the "id" property of the record while removing and adding to and from the store (with localstorageproxy).

Please let me know if you're able to reproduce this.

I've come up with the following workaround. The idea is to replace your store.add(...) logic with this function. It will 1) Remove the record to be added from store.proxy.cache, and 2) Remove the record to be added from store.removed, then 3) add the record to the store.

This function is for stores with localstorage proxies only.


addCorrectRecordToLocalStorageStore: function(store, record, idProperty) {
var id;

if(idProperty == undefined) idProperty = 'id';
id = record.get(idProperty);

if(id) {

//remove record from proxy cache.
delete store.getProxy().cache[id];

//remove record from 'removed' array.
Ext.each(store.removed, function(item, index, allItems) {
if(item.get(idProperty) == id){
allItems.splice(index, 1);
}
});
}

//add js object to store
store.add(record.data);
}