/ext-4.0.7/src/data/proxy/WebStorage.js
JavaScript | 416 lines | 232 code | 69 blank | 115 comment | 52 complexity | b165a3a224b3ea162bafc3414f79f3b6 MD5 | raw file
1/*
2
3This file is part of Ext JS 4
4
5Copyright (c) 2011 Sencha Inc
6
7Contact: http://www.sencha.com/contact
8
9GNU General Public License Usage
10This 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
12If 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 */
23Ext.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});