Skip to content

Instantly share code, notes, and snippets.

@dcporter
Last active August 29, 2015 13:57
Show Gist options
  • Save dcporter/9514029 to your computer and use it in GitHub Desktop.
Save dcporter/9514029 to your computer and use it in GitHub Desktop.
A data source for SproutCore which persists data to local storage instead of to a server.
// ==========================================================================
// Project: LocalStorageDataSource For SproutCore
// Copyright: 2014 Dave Porter & contributors
// License: Licensed under MIT license
// ==========================================================================
window.DCP = window.DCP || {};
/** @class
A data source which persists records to local storage.
Records are stored by record type name and ID. Record type name is serialized with
MyApp.MyRecord.toString, or you may specify a `localStorageKey` on your record. You
should do this if a model class's toString is likely to change (e.g. if the definition
changes from 'MyApp.MyRecord' to 'MA.MyRecord'). If this value changes, users may lose
access to existing data.
Note that this data source is not for use with data sources which persist to a server.
Syncing between local storage and a server is your responsibility.
*/
DCP.LocalStorageDataSource = SC.DataSource.extend({
/**
The app domain string used to isolate this storage from other apps on the same domain. It
is recommended that you override this. Once deployed, you should not change it, as doing so
will prevent users from accessing any data stored under the previous appDomain.
(Note that "isolated data" is functionally, but not securely, isolated.)
@type String
@default 'LocalStorageDataSource'
*/
appDomain: 'LocalStorageDataSource',
/**
You can use the domain string to isolate the data of different users from each other. Note
that if a user's userDomain changes, they will be unable to access any of their data stored
under the previous userDomain.
(Note that "isolated data" is functionally, but not securely, isolated.)
@type String
@default 'default'
*/
userDomain: 'default',
/**
The local storage object used to access and store data. This will be created for you by
on initialization.
@type SC.UserDefaults
*/
localStorage: null,
/**
By default, this data source supports local, parameter-less queries by loading all of the
specified record type from local storage. This supports the queries generated by standard
calls to SC.Store#find(recordType).
You can easily support your own queries by overriding this method with:
fetch: function(store, query) {
// FAST PATH: If the superclass method handles the query, return immediately.
if (sc_super()) return YES;
// Otherwise, handle other queries.
...
}
*/
fetch: function(store, query) {
// GATEKEEP:
if (query.get('isRemote')) return NO;
if (query.parameters) return NO;
// invokedNext so that changes to the query's status will register to observers.
this.invokeNext(function() {
this._fetchAll(store, query);
});
return YES;
},
/** @private */
retrieveRecord: function(store, storeKey) {
var recordType = store.recordTypeFor(storeKey),
id = store.idFor(storeKey),
ls = this.get('localStorage'),
idKey = this._idKeyForIdAndRecordType(id, recordType),
ids = this._idsForRecordType(recordType);
// 404 not found. TODO: This could be more useful.
if (!ids || !ids.contains(id)) {
store.dataSourceDidError(storeKey, 404);
return YES;
}
var hash = ls.readDefault(idKey);
store.loadRecord(recordType, hash, id);
return YES;
},
/** @private */
updateRecords: function(store, storeKeys) {
this.invokeNext(function() {
var ls = this.get('localStorage'),
len = storeKeys.length,
i, key, id, idKey, ids, recordType, recordTypeKeyPrefix, hash, handled;
for (i = 0; i < len; i++) {
key = storeKeys[i];
recordType = store.recordTypeFor(key);
ids = this._idsForRecordType(recordType);
id = store.idFor(key);
if (ids.contains(id)) {
idKey = this._idKeyForIdAndRecordType(id, recordType);
hash = store.readDataHash(key);
ls.writeDefault(idKey, hash);
store.dataSourceDidComplete(key);
}
}
});
return YES;
},
/** @private */
updateRecord: function(store, storeKey) {
return this.updateRecords(store, [storeKey]);
},
/** @private */
createRecords: function(store, storeKeys) {
this.invokeNext(function() {
var ls = this.get('localStorage'),
len = storeKeys.length,
i, key, id, idKey, ids, recordType, hash, handled;
for (i = 0; i < len; i++) {
key = storeKeys[i];
recordType = store.recordTypeFor(key);
id = store.idFor(key);
idKey = this._idKeyForIdAndRecordType(id, recordType);
ids = this._idsForRecordType(recordType);
hash = store.readDataHash(key);
ls.writeDefault(idKey, hash);
if (!ids.contains(id)) {
ids.pushObject(id);
this._idsForRecordType(recordType, ids);
}
store.dataSourceDidComplete(key);
}
});
return YES;
},
/** @private */
createRecord: function(store, storeKey) {
return this.createRecords(store, [storeKey], [params]);
},
/** @private */
destroyRecords: function(store, storeKeys) {
this.invokeNext(function() {
var ls = this.get('localStorage'),
len = storeKeys.length,
i, key, id, idKey, ids, recordType, hash, handled;
for (i = 0; i < len; i++) {
key = storeKeys[i];
recordType = store.recordTypeFor(key);
id = store.idFor(key);
hash = store.readDataHash(key);
ids = this._idsForRecordType(recordType);
idKey = this._idKeyForIdAndRecordType(id, recordType);
if (ids.contains(id)) {
ls.resetDefault(idKey);
ids.removeObject(id);
this._idsForRecordType(recordType, ids);
store.dataSourceDidDestroy(key);
}
}
});
return YES;
},
/** @private */
destroyRecord: function(store, storeKey) {
return this.destroyRecords(store, ['storeKeys'], ['params']);
},
// --------------------------------------
// Internal Support
//
/** @private */
init: function() {
sc_super();
// Create our private local storage accessor.
var ls = SC.UserDefaults.create({
appDomain: this.get('appDomain') || 'LocalStorageDataSource',
userDomain: this.get('userDomain') || 'default'
});
this.set('localStorage', ls);
},
// Called by fetch to fetch all records of a particular .
_fetchAll: function(store, query) {
var ls = this.get('localStorage'),
recordType = query.get('recordType'),
ids;
// Get the IDs.
ids = this._idsForRecordType(recordType);
// Load each as-yet-unloaded record from the list of IDs.
var hashes = [],
idsForHashes = [],
len = ids.get('length'),
i, id, storeKey, hash;
for (i = 0; i < len; i++) {
id = ids[i];
storeKey = recordType.storeKeyFor(id);
if (store.peekStatus(storeKey) === SC.Record.EMPTY) {
hash = ls.readDefault(this._idKeyForIdAndRecordType(id, recordType));
if (hash) {
hashes.push(hash);
idsForHashes.push(id);
}
}
}
store.loadRecords(recordType, hashes, idsForHashes);
store.dataSourceDidFetchQuery(query);
},
_prefixForRecordType: function(recordType) {
return recordType.localStorageKey || recordType.prototype.localStorageKey || recordType.toString();
},
_idKeyForIdAndRecordType: function(id, recordType) {
return '%@_id_%@'.fmt(this._prefixForRecordType(recordType), id);
},
// read/write pseudo-property
_idsForRecordType: function(recordType, ids) {
var ls = this.get('localStorage'),
idsListKey = '%@_ids'.fmt(this._prefixForRecordType(recordType));
if (ids === undefined) {
return ls.readDefault(idsListKey) || [];
} else {
return ls.writeDefault(idsListKey, ids);
}
}
});
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment