diff --git a/addons/api/addon/services/storage.js b/addons/api/addon/services/storage.js new file mode 100644 index 000000000..2be7034d2 --- /dev/null +++ b/addons/api/addon/services/storage.js @@ -0,0 +1,129 @@ +import Service from '@ember/service'; +import { getOwner } from '@ember/application'; +import { get } from '@ember/object'; + +/** + * A dead simple memory storage class used by `StorageService` when it + * is configued for memory-only. + */ +class MemoryStorage { + /** + * Internal storage object. + * @type {object} + * @private + */ + #storage = {}; + + /** + * Gets a value from storage. + * @param {string} key + * @return {any} + */ + getItem(key) { + return this.#storage[key]; + } + + /** + * Sets a value to storage. + * @param {string} key + */ + setItem(key, value) { + this.#storage[key] = value; + } + + /** + * Removes a value from storage. + * @param {string} key + */ + removeItem(key) { + delete this.#storage[key]; + } +} + +/** + * The storage service provides a simple way to persist data in the browser. + * Most of the time localStorage is used. Appplications may specify in-memory + * storage via the config, for example: + * + * ENV.storage = { + * memory: true + * }; + * + * @example + * import Route from '@ember/routing/route'; + * import { inject as service } from '@ember/service'; + * export default class ApplicationRoute extends Route { + * @service storage; + * afterModel() { + * this.storage.setItem('key', 'value'); + * } + * } + */ +export default class StorageService extends Service { + /** + * @type {MemoryStorage} + * @private + */ + #memoryStorage = new MemoryStorage(); + + /** + * Gets the underlying storage mechanism. + * @return {Storage} + */ + get storage() { + // Get the application config to determine if memory storage is enabled + const config = getOwner(this).resolveRegistration('config:environment'); + const inMemory = get(config, 'storage.memory'); + // Get the global context object (window) via service lookup, + // because just reaching for a global is icky + const globalContext = getOwner(this).lookup('service:-document') + .documentElement.parentNode.defaultView; + // If memory storage enabled, return the memory storage instance, + // otherwise return localStorage. + return inMemory ? this.#memoryStorage : globalContext.localStorage; + } + + /** + * Returns the storage namespace, which is the applications `modulePrefix`. + * @return {string} + */ + get namespace() { + const config = getOwner(this).resolveRegistration('config:environment'); + return config.modulePrefix; + } + + /** + * Returns a fully qualified key from the passed string. A fully qualified + * key includes the namespace. + * @param {string} key + * @return {string} fully qualified key + */ + qualifiedKey(key) { + return `${this.namespace}:${key}`; + } + + /** + * Gets a value from storage. + * @param {string} key + * @return {any} + */ + getItem(key) { + return this.storage.getItem(this.qualifiedKey(key)); + } + + /** + * Sets a value to storage. + * @param {string} key + */ + setItem(key, value) { + this.storage.setItem(this.qualifiedKey(key), value); + } + + /** + * Removes a value from storage. + * @param {string} key + */ + removeItem(key) { + this.storage.removeItem(this.qualifiedKey(key)); + } +} diff --git a/addons/api/app/services/storage.js b/addons/api/app/services/storage.js new file mode 100644 index 000000000..51cbec2c0 --- /dev/null +++ b/addons/api/app/services/storage.js @@ -0,0 +1 @@ +export { default } from 'api/services/storage'; diff --git a/addons/api/tests/dummy/app/routes/application.js b/addons/api/tests/dummy/app/routes/application.js index 62946d66d..a9e919f63 100644 --- a/addons/api/tests/dummy/app/routes/application.js +++ b/addons/api/tests/dummy/app/routes/application.js @@ -3,8 +3,9 @@ import { inject as service } from '@ember/service'; export default class ApplicationRoute extends Route { @service scope; + @service storage; afterModel() { - //console.log(this.scope); + //console.log(this.storage); } } diff --git a/addons/api/tests/dummy/config/environment.js b/addons/api/tests/dummy/config/environment.js index ba4e8cb76..1b05a0196 100644 --- a/addons/api/tests/dummy/config/environment.js +++ b/addons/api/tests/dummy/config/environment.js @@ -41,6 +41,10 @@ module.exports = function (environment) { ENV.APP.rootElement = '#ember-testing'; ENV.APP.autoboot = false; + + ENV.storage = { + memory: true, + }; } if (environment === 'production') { diff --git a/addons/api/tests/unit/services/storage-test.js b/addons/api/tests/unit/services/storage-test.js new file mode 100644 index 000000000..61702c765 --- /dev/null +++ b/addons/api/tests/unit/services/storage-test.js @@ -0,0 +1,38 @@ +import { module, test } from 'qunit'; +import { setupTest } from 'ember-qunit'; + +module('Unit | Service | storage', function (hooks) { + setupTest(hooks); + + test('it can persist values', function (assert) { + assert.expect(1); + const service = this.owner.lookup('service:storage'); + service.setItem('foo', 'bar'); + assert.equal(service.getItem('foo'), 'bar'); + }); + + test('it can remove persisted values', function (assert) { + assert.expect(2); + const service = this.owner.lookup('service:storage'); + service.setItem('foo', 'bar'); + assert.equal(service.getItem('foo'), 'bar'); + service.removeItem('foo'); + assert.notOk(service.getItem('foo')); + }); + + // NOTE: the following tests are order-dependent, so may be invalidated + // if we switch to random test ordering + + test("it's in-memory storage is implicitly refreshed for every test case", function (assert) { + assert.expect(1); + const service = this.owner.lookup('service:storage'); + service.setItem('foo', 'bar'); + assert.equal(service.getItem('foo'), 'bar'); + }); + + test("it's in-memory storage is implicitly refreshed for every test case", function (assert) { + assert.expect(1); + const service = this.owner.lookup('service:storage'); + assert.notOk(service.getItem('foo')); + }); +});