Upgrade to ExtJS 4.0.0 - Released 04/26/2011
[extjs.git] / src / data / proxy / WebStorage.js
1 /**
2  * @author Ed Spencer
3  * @class Ext.data.proxy.WebStorage
4  * @extends Ext.data.proxy.Client
5  * 
6  * <p>WebStorageProxy is simply a superclass for the {@link Ext.data.proxy.LocalStorage localStorage} and 
7  * {@link Ext.data.proxy.SessionStorage sessionStorage} proxies. It uses the new HTML5 key/value client-side storage 
8  * objects to save {@link Ext.data.Model model instances} for offline use.</p>
9  * 
10  * @constructor
11  * Creates the proxy, throws an error if local storage is not supported in the current browser
12  * @param {Object} config Optional config object
13  */
14 Ext.define('Ext.data.proxy.WebStorage', {
15     extend: 'Ext.data.proxy.Client',
16     alternateClassName: 'Ext.data.WebStorageProxy',
17     
18     /**
19      * @cfg {String} id The unique ID used as the key in which all record data are stored in the local storage object
20      */
21     id: undefined,
22
23     /**
24      * @ignore
25      */
26     constructor: function(config) {
27         this.callParent(arguments);
28         
29         /**
30          * Cached map of records already retrieved by this Proxy - ensures that the same instance is always retrieved
31          * @property cache
32          * @type Object
33          */
34         this.cache = {};
35
36         //<debug>
37         if (this.getStorageObject() === undefined) {
38             Ext.Error.raise("Local Storage is not supported in this browser, please use another type of data proxy");
39         }
40         //</debug>
41
42         //if an id is not given, try to use the store's id instead
43         this.id = this.id || (this.store ? this.store.storeId : undefined);
44
45         //<debug>
46         if (this.id === undefined) {
47             Ext.Error.raise("No unique id was provided to the local storage proxy. See Ext.data.proxy.LocalStorage documentation for details");
48         }
49         //</debug>
50
51         this.initialize();
52     },
53
54     //inherit docs
55     create: function(operation, callback, scope) {
56         var records = operation.records,
57             length  = records.length,
58             ids     = this.getIds(),
59             id, record, i;
60         
61         operation.setStarted();
62
63         for (i = 0; i < length; i++) {
64             record = records[i];
65
66             if (record.phantom) {
67                 record.phantom = false;
68                 id = this.getNextId();
69             } else {
70                 id = record.getId();
71             }
72
73             this.setRecord(record, id);
74             ids.push(id);
75         }
76
77         this.setIds(ids);
78
79         operation.setCompleted();
80         operation.setSuccessful();
81
82         if (typeof callback == 'function') {
83             callback.call(scope || this, operation);
84         }
85     },
86
87     //inherit docs
88     read: function(operation, callback, scope) {
89         //TODO: respect sorters, filters, start and limit options on the Operation
90
91         var records = [],
92             ids     = this.getIds(),
93             length  = ids.length,
94             i, recordData, record;
95         
96         //read a single record
97         if (operation.id) {
98             record = this.getRecord(operation.id);
99             
100             if (record) {
101                 records.push(record);
102                 operation.setSuccessful();
103             }
104         } else {
105             for (i = 0; i < length; i++) {
106                 records.push(this.getRecord(ids[i]));
107             }
108             operation.setSuccessful();
109         }
110         
111         operation.setCompleted();
112
113         operation.resultSet = Ext.create('Ext.data.ResultSet', {
114             records: records,
115             total  : records.length,
116             loaded : true
117         });
118
119         if (typeof callback == 'function') {
120             callback.call(scope || this, operation);
121         }
122     },
123
124     //inherit docs
125     update: function(operation, callback, scope) {
126         var records = operation.records,
127             length  = records.length,
128             ids     = this.getIds(),
129             record, id, i;
130
131         operation.setStarted();
132
133         for (i = 0; i < length; i++) {
134             record = records[i];
135             this.setRecord(record);
136             
137             //we need to update the set of ids here because it's possible that a non-phantom record was added
138             //to this proxy - in which case the record's id would never have been added via the normal 'create' call
139             id = record.getId();
140             if (id !== undefined && Ext.Array.indexOf(ids, id) == -1) {
141                 ids.push(id);
142             }
143         }
144         this.setIds(ids);
145
146         operation.setCompleted();
147         operation.setSuccessful();
148
149         if (typeof callback == 'function') {
150             callback.call(scope || this, operation);
151         }
152     },
153
154     //inherit
155     destroy: function(operation, callback, scope) {
156         var records = operation.records,
157             length  = records.length,
158             ids     = this.getIds(),
159
160             //newIds is a copy of ids, from which we remove the destroyed records
161             newIds  = [].concat(ids),
162             i;
163
164         for (i = 0; i < length; i++) {
165             Ext.Array.remove(newIds, records[i].getId());
166             this.removeRecord(records[i], false);
167         }
168
169         this.setIds(newIds);
170         
171         operation.setCompleted();
172         operation.setSuccessful();
173
174         if (typeof callback == 'function') {
175             callback.call(scope || this, operation);
176         }
177     },
178
179     /**
180      * @private
181      * Fetches a model instance from the Proxy by ID. Runs each field's decode function (if present) to decode the data
182      * @param {String} id The record's unique ID
183      * @return {Ext.data.Model} The model instance
184      */
185     getRecord: function(id) {
186         if (this.cache[id] === undefined) {
187             var rawData = Ext.decode(this.getStorageObject().getItem(this.getRecordKey(id))),
188                 data    = {},
189                 Model   = this.model,
190                 fields  = Model.prototype.fields.items,
191                 length  = fields.length,
192                 i, field, name, record;
193
194             for (i = 0; i < length; i++) {
195                 field = fields[i];
196                 name  = field.name;
197
198                 if (typeof field.decode == 'function') {
199                     data[name] = field.decode(rawData[name]);
200                 } else {
201                     data[name] = rawData[name];
202                 }
203             }
204
205             record = new Model(data, id);
206             record.phantom = false;
207
208             this.cache[id] = record;
209         }
210         
211         return this.cache[id];
212     },
213
214     /**
215      * Saves the given record in the Proxy. Runs each field's encode function (if present) to encode the data
216      * @param {Ext.data.Model} record The model instance
217      * @param {String} id The id to save the record under (defaults to the value of the record's getId() function)
218      */
219     setRecord: function(record, id) {
220         if (id) {
221             record.setId(id);
222         } else {
223             id = record.getId();
224         }
225
226         var me = this,
227             rawData = record.data,
228             data    = {},
229             model   = me.model,
230             fields  = model.prototype.fields.items,
231             length  = fields.length,
232             i = 0,
233             field, name, obj, key;
234
235         for (; i < length; i++) {
236             field = fields[i];
237             name  = field.name;
238
239             if (typeof field.encode == 'function') {
240                 data[name] = field.encode(rawData[name], record);
241             } else {
242                 data[name] = rawData[name];
243             }
244         }
245
246         obj = me.getStorageObject();
247         key = me.getRecordKey(id);
248         
249         //keep the cache up to date
250         me.cache[id] = record;
251         
252         //iPad bug requires that we remove the item before setting it
253         obj.removeItem(key);
254         obj.setItem(key, Ext.encode(data));
255     },
256
257     /**
258      * @private
259      * Physically removes a given record from the local storage. Used internally by {@link #destroy}, which you should
260      * use instead because it updates the list of currently-stored record ids
261      * @param {String|Number|Ext.data.Model} id The id of the record to remove, or an Ext.data.Model instance
262      */
263     removeRecord: function(id, updateIds) {
264         var me = this,
265             ids;
266             
267         if (id.isModel) {
268             id = id.getId();
269         }
270
271         if (updateIds !== false) {
272             ids = me.getIds();
273             Ext.Array.remove(ids, id);
274             me.setIds(ids);
275         }
276
277         me.getStorageObject().removeItem(me.getRecordKey(id));
278     },
279
280     /**
281      * @private
282      * Given the id of a record, returns a unique string based on that id and the id of this proxy. This is used when
283      * storing data in the local storage object and should prevent naming collisions.
284      * @param {String|Number|Ext.data.Model} id The record id, or a Model instance
285      * @return {String} The unique key for this record
286      */
287     getRecordKey: function(id) {
288         if (id.isModel) {
289             id = id.getId();
290         }
291
292         return Ext.String.format("{0}-{1}", this.id, id);
293     },
294
295     /**
296      * @private
297      * Returns the unique key used to store the current record counter for this proxy. This is used internally when
298      * realizing models (creating them when they used to be phantoms), in order to give each model instance a unique id.
299      * @return {String} The counter key
300      */
301     getRecordCounterKey: function() {
302         return Ext.String.format("{0}-counter", this.id);
303     },
304
305     /**
306      * @private
307      * Returns the array of record IDs stored in this Proxy
308      * @return {Array} The record IDs. Each is cast as a Number
309      */
310     getIds: function() {
311         var ids    = (this.getStorageObject().getItem(this.id) || "").split(","),
312             length = ids.length,
313             i;
314
315         if (length == 1 && ids[0] === "") {
316             ids = [];
317         } else {
318             for (i = 0; i < length; i++) {
319                 ids[i] = parseInt(ids[i], 10);
320             }
321         }
322
323         return ids;
324     },
325
326     /**
327      * @private
328      * Saves the array of ids representing the set of all records in the Proxy
329      * @param {Array} ids The ids to set
330      */
331     setIds: function(ids) {
332         var obj = this.getStorageObject(),
333             str = ids.join(",");
334         
335         obj.removeItem(this.id);
336         
337         if (!Ext.isEmpty(str)) {
338             obj.setItem(this.id, str);
339         }
340     },
341
342     /**
343      * @private
344      * Returns the next numerical ID that can be used when realizing a model instance (see getRecordCounterKey). Increments
345      * the counter.
346      * @return {Number} The id
347      */
348     getNextId: function() {
349         var obj  = this.getStorageObject(),
350             key  = this.getRecordCounterKey(),
351             last = obj.getItem(key),
352             ids, id;
353         
354         if (last === null) {
355             ids = this.getIds();
356             last = ids[ids.length - 1] || 0;
357         }
358         
359         id = parseInt(last, 10) + 1;
360         obj.setItem(key, id);
361         
362         return id;
363     },
364
365     /**
366      * @private
367      * Sets up the Proxy by claiming the key in the storage object that corresponds to the unique id of this Proxy. Called
368      * automatically by the constructor, this should not need to be called again unless {@link #clear} has been called.
369      */
370     initialize: function() {
371         var storageObject = this.getStorageObject();
372         storageObject.setItem(this.id, storageObject.getItem(this.id) || "");
373     },
374
375     /**
376      * Destroys all records stored in the proxy and removes all keys and values used to support the proxy from the storage object
377      */
378     clear: function() {
379         var obj = this.getStorageObject(),
380             ids = this.getIds(),
381             len = ids.length,
382             i;
383
384         //remove all the records
385         for (i = 0; i < len; i++) {
386             this.removeRecord(ids[i]);
387         }
388
389         //remove the supporting objects
390         obj.removeItem(this.getRecordCounterKey());
391         obj.removeItem(this.id);
392     },
393
394     /**
395      * @private
396      * Abstract function which should return the storage object that data will be saved to. This must be implemented
397      * in each subclass.
398      * @return {Object} The storage object
399      */
400     getStorageObject: function() {
401         //<debug>
402         Ext.Error.raise("The getStorageObject function has not been defined in your Ext.data.proxy.WebStorage subclass");
403         //</debug>
404     }
405 });