/**
* WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage LocalStorage}
* and {@link Ext.data.proxy.SessionStorage SessionStorage} proxies. It uses the new HTML5
* key/value client-side storage objects to save {@link Ext.data.Model model instances} for
* offline use.
* @private
*/
Ext.define('Ext.data.proxy.WebStorage', {
extend: 'Ext.data.proxy.Client',
alternateClassName: 'Ext.data.WebStorageProxy',
requires: [
'Ext.data.identifier.Sequential'
],
config: {
/**
* @cfg {String} id
* The unique ID used as the key in which all record data are stored in the local
* storage object.
*/
id: undefined
},
/**
* @cfg {Object} reader
* Not used by web storage proxy.
* @hide
*/
/**
* @cfg {Object} writer
* Not used by web storage proxy.
* @hide
*/
/**
* Creates the proxy, throws an error if local storage is not supported in the current browser.
* @param {Object} config (optional) Config object.
*/
constructor: function(config) {
this.callParent(arguments);
/**
* @property {Object} cache
* Cached map of records already retrieved by this Proxy. Ensures that the same instance is
* always retrieved.
*/
this.cache = {};
//<debug>
if (this.getStorageObject() === undefined) {
Ext.raise("Local Storage is not supported in this browser, please use another type " +
"of data proxy");
}
//</debug>
//<debug>
if (this.getId() === undefined) {
Ext.raise("No unique id was provided to the local storage proxy. " +
"See Ext.data.proxy.LocalStorage documentation for details");
}
//</debug>
this.initialize();
},
/**
* @method create
* @inheritdoc
*/
create: function(operation) {
var me = this,
records = operation.getRecords(),
length = records.length,
ids = me.getIds(),
id, record, i, identifier;
if (me.isHierarchical === undefined) {
// if the storage object does not yet contain any data, this is the first point
// at which we can determine whether or not this proxy deals with hierarchical data.
// it cannot be determined during initialization because the Model is not decorated
// with NodeInterface until it is used in a TreeStore
me.isHierarchical = !!records[0].isNode;
if (me.isHierarchical) {
me.getStorageObject().setItem(me.getTreeKey(), true);
}
}
for (i = 0; i < length; i++) {
record = records[i];
if (record.phantom) {
identifier = record.identifier;
if (identifier && identifier.isUnique) {
id = record.getId();
}
else {
id = me.getNextId();
}
}
else {
id = record.getId();
}
me.setRecord(record, id);
record.commit();
ids.push(id);
}
me.setIds(ids);
operation.setSuccessful(true);
},
/**
* @method read
* @inheritdoc
*/
read: function(operation) {
var me = this,
allRecords,
records = [],
success = true,
Model = me.getModel(),
validCount = 0,
recordCreator = operation.getRecordCreator(),
filters, sorters, limit, filterLen, valid, record, ids, length, data, id, i, j;
if (me.isHierarchical) {
records = me.getTreeData();
}
else {
ids = me.getIds();
length = ids.length;
id = operation.getId();
// read a single record
if (id) {
data = me.getRecord(id);
if (data !== null) {
record = recordCreator ? recordCreator(data, Model) : new Model(data);
}
if (record) {
records.push(record);
}
else {
success = false;
}
}
else {
sorters = operation.getSorters();
filters = operation.getFilters();
limit = operation.getLimit();
allRecords = [];
// build an array of all records first first so we can sort them before
// applying filters or limit. These are Model instances instead of raw
// data objects so that the sorter and filter Fn can use the Model API
for (i = 0; i < length; i++) {
data = me.getRecord(ids[i]);
record = recordCreator ? recordCreator(data, Model) : new Model(data);
allRecords.push(record);
}
if (sorters) {
Ext.Array.sort(allRecords, Ext.util.Sorter.createComparator(sorters));
}
for (i = operation.getStart() || 0; i < length; i++) {
record = allRecords[i];
valid = true;
if (filters) {
for (j = 0, filterLen = filters.length; j < filterLen; j++) {
valid = filters[j].filter(record);
}
}
if (valid) {
records.push(record);
validCount++;
}
if (limit && validCount === limit) {
break;
}
}
}
}
if (success) {
operation.setResultSet(new Ext.data.ResultSet({
records: records,
total: records.length,
loaded: true
}));
operation.setSuccessful(true);
}
else {
operation.setException('Unable to load records');
}
},
/**
* @method update
* @inheritdoc
*/
update: function(operation) {
var records = operation.getRecords(),
length = records.length,
ids = this.getIds(),
record, id, i;
for (i = 0; i < length; i++) {
record = records[i];
this.setRecord(record);
record.commit();
// we need to update the set of ids here because it's possible that
// a non-phantom record was added to this proxy - in which case the record's
// id would never have been added via the normal 'create' call
id = record.getId();
if (id !== undefined && Ext.Array.indexOf(ids, id) === -1) {
ids.push(id);
}
}
this.setIds(ids);
operation.setSuccessful(true);
},
/**
* @method erase
* @inheritdoc
*/
erase: function(operation) {
var me = this,
records = operation.getRecords(),
ids = me.getIds(),
idLength = ids.length,
newIds = [],
removedHash = {},
i = records.length,
id;
for (; i--;) {
Ext.apply(removedHash, me.removeRecord(records[i]));
}
for (i = 0; i < idLength; i++) {
id = ids[i];
if (!removedHash[id]) {
newIds.push(id);
}
}
me.setIds(newIds);
operation.setSuccessful(true);
},
/**
* @private
* Fetches record data from the Proxy by ID.
* @param {String} id The record's unique ID
* @return {Object} The record data
*/
getRecord: function(id) {
var me = this,
cache = me.cache,
data;
data = !cache[id]
? Ext.decode(me.getStorageObject().getItem(me.getRecordKey(id)))
: cache[id];
if (!data) {
return null;
}
cache[id] = data;
data[me.getModel().prototype.idProperty] = id;
// In order to preserve the cache, we MUST copy it here because
// Models use the incoming raw data as their data object and convert/default values
// into that object
return Ext.merge({}, data);
},
/**
* Saves the given record in the Proxy.
* @param {Ext.data.Model} record The model instance
* @param {String} [id] The id to save the record under (defaults to the value of the
* record's getId() function)
*/
setRecord: function(record, id) {
if (id) {
record.set('id', id, {
commit: true
});
}
else {
id = record.getId();
}
/* eslint-disable-next-line vars-on-top */
var me = this,
rawData = record.getData(),
data = {},
model = me.getModel(),
fields = model.getFields(),
length = fields.length,
i = 0,
field, name, obj, key, value;
for (; i < length; i++) {
field = fields[i];
name = field.name;
if (field.persist) {
value = rawData[name];
if (field.isDateField && field.dateFormat && Ext.isDate(value)) {
value = Ext.Date.format(value, field.dateFormat);
}
else if (field.serialize) {
value = field.serialize(value, record);
}
data[name] = value;
}
}
// no need to store the id in the data, since it is already stored in the record key
delete data[model.prototype.idProperty];
// if the record is a tree node and it's a direct child of the root node, do not store
// the parentId
if (record.isNode && record.get('depth') === 1) {
delete data.parentId;
}
obj = me.getStorageObject();
key = me.getRecordKey(id);
// keep the cache up to date
me.cache[id] = data;
// iPad bug requires that we remove the item before setting it
obj.removeItem(key);
obj.setItem(key, Ext.encode(data));
},
/**
* @private
* Physically removes a given record from the local storage and recursively removes children
* if the record is a tree node. Used internally by {@link #destroy}.
* @param {Ext.data.Model} record The record to remove
* @return {Object} a hash with the ids of the records that were removed as keys and the
* records that were removed as values
*/
removeRecord: function(record) {
var me = this,
id = record.getId(),
records = {},
i, childNodes;
records[id] = record;
me.getStorageObject().removeItem(me.getRecordKey(id));
delete me.cache[id];
if (record.childNodes) {
childNodes = record.childNodes;
for (i = childNodes.length; i--;) {
Ext.apply(records, me.removeRecord(childNodes[i]));
}
}
return records;
},
/**
* @private
* Given the id of a record, returns a unique string based on that id and the id of this proxy.
* This is used when storing data in the local storage object and should prevent naming
* collisions.
* @param {String/Number/Ext.data.Model} id The record id, or a Model instance
* @return {String} The unique key for this record
*/
getRecordKey: function(id) {
if (id.isModel) {
id = id.getId();
}
return Ext.String.format("{0}-{1}", this.getId(), id);
},
/**
* @private
* Returns the unique key used to store the current record counter for this proxy. This is used
* internally when realizing models (creating them when they used to be phantoms), in order to
* give each model instance a unique id.
* @return {String} The counter key
*/
getRecordCounterKey: function() {
return Ext.String.format("{0}-counter", this.getId());
},
/**
* @private
* Returns the unique key used to store the tree indicator. This is used internally to
* determine if the stored data is hierarchical
* @return {String} The counter key
*/
getTreeKey: function() {
return Ext.String.format("{0}-tree", this.getId());
},
/**
* @private
* Returns the array of record IDs stored in this Proxy
* @return {Number[]} The record IDs. Each is cast as a Number
*/
getIds: function() {
var me = this,
ids = (me.getStorageObject().getItem(me.getId()) || "").split(","),
length = ids.length,
isString = this.getIdField().isStringField,
i;
if (length === 1 && ids[0] === "") {
ids = [];
}
else {
for (i = 0; i < length; i++) {
ids[i] = isString ? ids[i] : (Ext.isNumber(+ids[i]) ? +ids[i] : ids[i]);
}
}
return ids;
},
getIdField: function() {
return this.getModel().prototype.idField;
},
/**
* @private
* Saves the array of ids representing the set of all records in the Proxy
* @param {Number[]} ids The ids to set
*/
setIds: function(ids) {
var obj = this.getStorageObject(),
str = ids.join(","),
id = this.getId();
obj.removeItem(id);
if (!Ext.isEmpty(str)) {
obj.setItem(id, str);
}
},
/**
* @private
* Returns the next numerical ID that can be used when realizing a model instance
* (see getRecordCounterKey). Increments the counter.
* @return {Number} The id
*/
getNextId: function() {
var me = this,
obj = me.getStorageObject(),
key = me.getRecordCounterKey(),
isString = me.getIdField().isStringField,
id;
id = me.idGenerator.generate();
obj.setItem(key, id);
if (isString) {
id = id + '';
}
return id;
},
/**
* Gets tree data and transforms it from key value pairs into a hierarchical structure.
* @private
* @return {Ext.data.NodeInterface[]}
*/
getTreeData: function() {
var me = this,
ids = me.getIds(),
length = ids.length,
records = [],
recordHash = {},
root = [],
i = 0,
Model = me.getModel(),
idProperty = Model.prototype.idProperty,
rootLength, record, parent, parentId, children, id;
for (; i < length; i++) {
id = ids[i];
// get the record for each id
record = me.getRecord(id);
// push the record into the records array
records.push(record);
// add the record to the record hash so it can be easily retrieved by id later
recordHash[id] = record;
if (!record.parentId) {
// push records that are at the root level (those with no parent id) into the
// "root" array
root.push(record);
}
}
rootLength = root.length;
// sort the records by parent id for greater efficiency, so that each parent record only
// has to be found once for all of its children
Ext.Array.sort(records, me.sortByParentId);
// append each record to its parent, starting after the root node(s), since root nodes
// do not need to be attached to a parent
for (i = rootLength; i < length; i++) {
record = records[i];
parentId = record.parentId;
if (!parent || parent[idProperty] !== parentId) {
// if this record has a different parent id from the previous record, we need to
// look up the parent by id.
parent = recordHash[parentId];
parent.children = children = [];
}
// push the record onto its parent's children array
children.push(record);
}
for (i = length; i--;) {
record = records[i];
if (!record.children && !record.leaf) {
// set non-leaf nodes with no children to loaded so the proxy won't try to
// dynamically load their contents when they are expanded
record.loaded = true;
}
}
// Create model instances out of all the "root-level" nodes.
for (i = rootLength; i--;) {
record = root[i];
root[i] = new Model(record);
}
return root;
},
/**
* Sorter function for sorting records by parentId
* @private
* @param {Object} node1
* @param {Object} node2
* @return {Number}
*/
sortByParentId: function(node1, node2) {
return (node1.parentId || 0) - (node2.parentId || 0);
},
/**
* @private
* Sets up the Proxy by claiming the key in the storage object that corresponds to the unique
* id of this Proxy. Called automatically by the constructor, this should not need to be called
* again unless {@link #clear} has been called.
*/
initialize: function() {
var me = this,
storageObject = me.getStorageObject(),
lastId = +storageObject.getItem(me.getRecordCounterKey()),
id = me.getId();
storageObject.setItem(id, storageObject.getItem(id) || "");
if (storageObject.getItem(me.getTreeKey())) {
me.isHierarchical = true;
}
me.idGenerator = new Ext.data.identifier.Sequential({
seed: lastId ? lastId + 1 : 1
});
},
/**
* Destroys all records stored in the proxy and removes all keys and values used to support
* the proxy from the storage object.
*/
clear: function() {
var me = this,
obj = me.getStorageObject(),
ids = me.getIds(),
len = ids.length,
i;
// remove all the records
for (i = 0; i < len; i++) {
obj.removeItem(me.getRecordKey(ids[i]));
}
// remove the supporting objects
obj.removeItem(me.getRecordCounterKey());
obj.removeItem(me.getTreeKey());
obj.removeItem(me.getId());
// clear the cache
me.cache = {};
},
/**
* @private
* Abstract function which should return the storage object that data will be saved to.
* This must be implemented in each subclass.
* @return {Object} The storage object
*/
getStorageObject: function() {
//<debug>
Ext.raise("The getStorageObject function has not been defined in your " +
"Ext.data.proxy.WebStorage subclass");
//</debug>
}
});