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