Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fall back to memory caching if local storage is inaccessible #592

Merged
merged 1 commit into from
Sep 14, 2016
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions app/components/x-list.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,20 @@ const { service } = inject;
const { readOnly, reads } = computed;

const CHECK_HTML = '✓';
let LOCAL_STORAGE_SUPPORTED;
try {
LOCAL_STORAGE_SUPPORTED = !!window.localStorage;
} catch (e) {
// Security setting in chrome that disables storage for third party
// throws an error when `localStorage` is accessed.
LOCAL_STORAGE_SUPPORTED = false;
}

export default Component.extend({

/**
* @property classNames
* @type {Array}
*/
classNames: ['list'],

/**
Expand Down Expand Up @@ -51,13 +62,16 @@ export default Component.extend({
name: null,

/**
* Service used for local storage. Local storage is
* Service used for storage. Storage is
* needed for caching of widths and visibility of columns.
* The default storage service is local storage however we
* fall back to memory storage if local storage is disabled (For
* example as a security setting in Chrome).
*
* @property localStorage
* @property storage
* @return {Service}
*/
localStorage: service(),
storage: service(`storage/${LOCAL_STORAGE_SUPPORTED ? 'local' : 'memory'}`),

/**
* The key used to cache the current schema. Defaults
Expand Down Expand Up @@ -224,7 +238,7 @@ export default Component.extend({
key: this.get('storageKey'),
tableWidth: this.getTableWidth(),
minWidth: this.get('minWidth'),
localStorage: this.get('localStorage'),
storage: this.get('storage'),
columnSchema: this.get('schema.columns') || []
});
resizableColumns.build();
Expand Down
36 changes: 18 additions & 18 deletions app/libs/resizable-columns.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,17 +21,17 @@ export default class {
* - {String} key Used as key for local storage caching
* - {Number} tableWidth The table's width used for width calculations
* - {Number} minWidth The minimum width a column can reach
* - {Service} localStorage The local storage service that manages caching
* - {Service} storage The local storage service that manages caching
* - {Array} columnSchema Contains the list of columns. Each column object should contain:
* - {String} id The column's unique identifier
* - {String} name The column's name
* - {Boolean} visible The column's default visibility
*/
constructor({ key, tableWidth = 0, minWidth = 10, localStorage, columnSchema }) {
constructor({ key, tableWidth = 0, minWidth = 10, storage, columnSchema }) {
this.tableWidth = tableWidth;
this.minWidth = minWidth;
this.key = key;
this.localStorage = localStorage;
this.storage = storage;
this.columnSchema = columnSchema;
this.setupCache();
}
Expand Down Expand Up @@ -60,9 +60,9 @@ export default class {
* @method setCacheTimestamp
*/
setCacheTimestamp() {
let saved = this.localStorage.getItem(this.getStorageKey()) || {};
let saved = this.storage.getItem(this.getStorageKey()) || {};
saved.updatedAt = Date.now();
this.localStorage.setItem(this.getStorageKey(), saved);
this.storage.setItem(this.getStorageKey(), saved);
}

/**
Expand All @@ -77,13 +77,13 @@ export default class {
* @method clearInvalidCache
*/
clearInvalidCache() {
let saved = this.localStorage.getItem(this.getStorageKey());
let saved = this.storage.getItem(this.getStorageKey());
if (saved && saved.columnVisibility) {
let savedIds = keys(saved.columnVisibility).sort();
let schemaIds = this.columnSchema.mapBy('id').sort();
if (!compareArrays(savedIds, schemaIds)) {
// Clear saved items
this.localStorage.removeItem(this.getStorageKey());
this.storage.removeItem(this.getStorageKey());
}
}
}
Expand All @@ -99,10 +99,10 @@ export default class {
*/
clearExpiredCache() {
let now = Date.now();
this.localStorage.keys().filter(key => key.match(/^x-list/))
this.storage.keys().filter(key => key.match(/^x-list/))
.forEach(key => {
if (now - this.localStorage.getItem(key).updatedAt > THIRTY_DAYS_FROM_NOW) {
this.localStorage.removeItem(key);
if (now - this.storage.getItem(key).updatedAt > THIRTY_DAYS_FROM_NOW) {
this.storage.removeItem(key);
}
});
}
Expand Down Expand Up @@ -158,7 +158,7 @@ export default class {
* @return {Boolean}
*/
isColumnVisible(id) {
let saved = this.localStorage.getItem(this.getStorageKey()) || {};
let saved = this.storage.getItem(this.getStorageKey()) || {};
if (saved.columnVisibility && !isNone(saved.columnVisibility[id])) {
return saved.columnVisibility[id];
}
Expand Down Expand Up @@ -304,12 +304,12 @@ export default class {
* @method saveVisibility
*/
saveVisibility() {
let saved = this.localStorage.getItem(this.getStorageKey()) || {};
let saved = this.storage.getItem(this.getStorageKey()) || {};
saved.columnVisibility = this._columnVisibility.reduce((obj, { id, visible }) => {
obj[id] = visible;
return obj;
}, {});
this.localStorage.setItem(this.getStorageKey(), saved);
this.storage.setItem(this.getStorageKey(), saved);
}

/**
Expand All @@ -319,9 +319,9 @@ export default class {
* @method resetWidths
*/
resetWidths() {
let saved = this.localStorage.getItem(this.getStorageKey()) || {};
let saved = this.storage.getItem(this.getStorageKey()) || {};
delete saved.columnWidths;
this.localStorage.setItem(this.getStorageKey(), saved);
this.storage.setItem(this.getStorageKey(), saved);
this.build();
}

Expand All @@ -340,9 +340,9 @@ export default class {
this._columns.forEach(({ id, width }) => {
columns[id] = width / totalWidth;
});
let saved = this.localStorage.getItem(this.getStorageKey()) || {};
let saved = this.storage.getItem(this.getStorageKey()) || {};
saved.columnWidths = columns;
this.localStorage.setItem(this.getStorageKey(), saved);
this.storage.setItem(this.getStorageKey(), saved);
}

/**
Expand All @@ -363,7 +363,7 @@ export default class {
* @return {Number} The cached percentage
*/
getSavedPercentage(id) {
let saved = this.localStorage.getItem(this.getStorageKey()) || {};
let saved = this.storage.getItem(this.getStorageKey()) || {};
return saved.columnWidths && saved.columnWidths[id];
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,20 @@
* Service that manages local storage. This service is useful because
* it abstracts serialization and parsing of json.
*
* @class LocalStorage
* @class Local
* @extends Service
*/
import Ember from 'ember';
const { Service, isNone } = Ember;
const { parse, stringify } = JSON;

export default Service.extend({
/**
* Reads a stored json string and parses it to
* and object.
*
* @method getItem
* @param {Stirng} key The cache key
* @param {String} key The cache key
* @return {Object} The json value
*/
getItem(key) {
Expand Down
62 changes: 62 additions & 0 deletions app/services/storage/memory.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/**
* Service that manages storage in memory. Usually as a fallback
* for local storage.
*
* @class Memory
* @extends Service
*/
import Ember from 'ember';
const { Service, computed } = Ember;
const { keys } = Object;

export default Service.extend({
/**
* Where data is stored.
*
* @property hash
* @type {Object}
*/
hash: computed(() => ({})),

/**
* Reads a stored item.
*
* @method getItem
* @param {String} key The cache key
* @return {Object} The stored value
*/
getItem(key) {
return this.get('hash')[key];
},

/**
* Stores an item in memory.
*
* @method setItem
* @param {String} key The cache key
* @param {Object} value The item
*/
setItem(key, value) {
this.get('hash')[key] = value;
},

/**
* Deletes an entry from memory storage.
*
* @method removeItem
* @param {String} key The cache key
*/
removeItem(key) {
delete this.get('hash')[key];
},

/**
* Returns the list keys of saved entries in memory.
*
* @method keys
* @return {Array} The array of keys
*/
keys() {
return keys(this.get('hash'));
}
});
10 changes: 5 additions & 5 deletions tests/unit/resizable-columns-test.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ function getOptions() {
key: 'my-key',
tableWidth: 30,
minWidth: 5,
localStorage: {
storage: {
setItem(key, value) {
storage[key] = value;
},
Expand Down Expand Up @@ -106,8 +106,8 @@ test('updates the width correctly', function(assert) {
test('uses the correct cache key', function(assert) {
let resizableColumns = new ResizableColumns(this.options);
resizableColumns.build();
assert.equal(this.options.localStorage.keys().length, 1, "Only uses one key");
assert.equal(this.options.localStorage.keys()[0], 'x-list__my-key', "Uses the correct key");
assert.equal(this.options.storage.keys().length, 1, "Only uses one key");
assert.equal(this.options.storage.keys()[0], 'x-list__my-key', "Uses the correct key");
});

test('shows/hides the correct columns', function(assert) {
Expand Down Expand Up @@ -135,7 +135,7 @@ test('shows/hides the correct columns', function(assert) {

test("resets cache correctly if schema doesn't match cache", function(assert) {
assert.expect(1);
this.options.localStorage.removeItem = (key) => {
this.options.storage.removeItem = (key) => {
assert.equal(key, 'x-list__my-key', "cache was cleared");
};
let resizableColumns = new ResizableColumns(this.options);
Expand All @@ -153,7 +153,7 @@ test("clears expired cache", function(assert) {
let sixtyDaysAgo = 1000 * 60 * 60 * 24 * 30 * 2;
storage['x-list__my-key'] = { updatedAt: Date.now() - sixtyDaysAgo };
assert.expect(1);
this.options.localStorage.removeItem = (key) => {
this.options.storage.removeItem = (key) => {
assert.equal(key, 'x-list__my-key', "cache was cleared");
};
let resizableColumns = new ResizableColumns(this.options);
Expand Down