diff --git a/packages/@ember/-internals/metal/lib/storage.ts b/packages/@ember/-internals/metal/lib/storage.ts new file mode 100644 index 00000000000..5b9eede60f8 --- /dev/null +++ b/packages/@ember/-internals/metal/lib/storage.ts @@ -0,0 +1,77 @@ +import type { UpdatableTag } from '@glimmer/interfaces'; + +import { consumeTag } from './tracking'; +import { createUpdatableTag, DIRTY_TAG } from './validators'; +import { assert } from '@ember/debug'; + +const SET = Symbol.for('TrackedStorage.set'); +const READ = Symbol.for('TrackedStorage.read'); + +function tripleEq(a: Value, b: Value): boolean { + return a === b; +} + +class Storage { + #tag: UpdatableTag; + #value: Value; + #lastValue: Value; + #isEqual: (a: Value, b: Value) => boolean; + + get #current() { + consumeTag(this.#tag); + return this.#value; + } + set #current(value) { + if (this.#isEqual(this.#value, this.#lastValue)) { + return; + } + + this.#value = this.#lastValue = value; + + DIRTY_TAG(this.#tag); + } + + constructor(initialValue: Value, isEqual?: (a: Value, b: Value) => boolean) { + this.#tag = createUpdatableTag(); + this.#value = this.#lastValue = initialValue; + this.#isEqual = isEqual ?? tripleEq; + } + + [READ]() { + return this.#current; + } + + [SET](value: Value) { + this.#current = value; + } +} + +export function createStorage( + initialValue: Value, + isEqual?: (oldValue: Value, newValue: Value) => boolean +): Storage { + assert( + 'the second parameter to `createStorage` must be an equality function or undefined', + isEqual === undefined || typeof isEqual === 'function' + ); + + return new Storage(initialValue, isEqual); +} + +export function getValue(storage: Storage): Value { + assert( + 'getValue must be passed a tracked store created with `createStorage`.', + storage instanceof Storage + ); + + return storage[READ](); +} + +export function setValue(storage: Storage, value: Value): void { + assert( + 'setValue must be passed a tracked store created with `createStorage`.', + storage instanceof Storage + ); + + storage[SET](value); +} diff --git a/packages/@ember/-internals/metal/tests/tracked/storage_test.js b/packages/@ember/-internals/metal/tests/tracked/storage_test.js new file mode 100644 index 00000000000..c34c8a94ebd --- /dev/null +++ b/packages/@ember/-internals/metal/tests/tracked/storage_test.js @@ -0,0 +1,85 @@ +import { createCache, getValue as getCacheValue } from '../../lib/cache'; + +import { createStorage, getValue, setValue } from '../../lib/storage'; + +import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; + +moduleFor( + '@ember/-internals/metal/storage', + class extends AbstractTestCase { + ['@test it works'](assert) { + let count = 0; + + let storage = createStorage(); + + let cache = createCache(() => { + getValue(storage); + return ++count; + }); + + assert.equal(getValue(storage), undefined, 'does not have a value initially'); + assert.equal(getCacheValue(cache), 1, 'cache runs the first time'); + assert.equal(getCacheValue(cache), 1, 'cache does not the second time'); + + setValue(storage, 123); + + assert.equal(getValue(storage), 123, 'value is set correctly'); + assert.equal(getCacheValue(cache), 2, 'cache ran after storage was set'); + + setValue(storage, 123); + + assert.equal(getValue(storage), 123, 'value remains the same'); + assert.equal(getCacheValue(cache), 2, 'cache not ran after storage was set to same value'); + } + + ['@test it can set an initial value'](assert) { + let count = 0; + + let storage = createStorage(123); + + let cache = createCache(() => { + getValue(storage); + return ++count; + }); + + assert.equal(getValue(storage), 123, 'has a initial value'); + assert.equal(getCacheValue(cache), 1, 'cache runs the first time'); + assert.equal(getCacheValue(cache), 1, 'cache does not the second time'); + + setValue(storage, 123); + + assert.equal(getValue(storage), 123, 'value is not updated'); + assert.equal(getCacheValue(cache), 1, 'cache not ran after storage was set to same value'); + + setValue(storage, 456); + + assert.equal(getValue(storage), 456, 'value updated'); + assert.equal(getCacheValue(cache), 2, 'cache ran after storage was set to different value'); + } + + ['@test it can set an equality function'](assert) { + let count = 0; + + let storage = createStorage(123, () => false); + + let cache = createCache(() => { + getValue(storage); + return ++count; + }); + + assert.equal(getValue(storage), 123, 'has a initial value'); + assert.equal(getCacheValue(cache), 1, 'cache runs the first time'); + assert.equal(getCacheValue(cache), 1, 'cache does not the second time'); + + setValue(storage, 123); + + assert.equal(getValue(storage), 123, 'value is not updated'); + assert.equal(getCacheValue(cache), 2, 'cache runs again'); + + setValue(storage, 456); + + assert.equal(getValue(storage), 456, 'value updated'); + assert.equal(getCacheValue(cache), 3, 'cache ran after storage was set to different value'); + } + } +); diff --git a/packages/@glimmer/tracking/primitives/storage.ts b/packages/@glimmer/tracking/primitives/storage.ts new file mode 100644 index 00000000000..a3ac3b4847d --- /dev/null +++ b/packages/@glimmer/tracking/primitives/storage.ts @@ -0,0 +1,10 @@ +export { createStorage, getValue, setValue } from '@ember/-internals/metal/lib/storage'; + +/** + * NOTE: '@ember/-internals/metal' already exports a getValue function + * from @glimmer/tracking/primitives/cache.ts, + * so we can't use the pattern of re-export everything from + * @ember/-internals/metal + * + * At somepoint we need to untangle all that actually move things to their appropriate packages. + */ diff --git a/packages/@glimmer/tracking/tests/storage_test.ts b/packages/@glimmer/tracking/tests/storage_test.ts new file mode 100644 index 00000000000..58f40014b9f --- /dev/null +++ b/packages/@glimmer/tracking/tests/storage_test.ts @@ -0,0 +1,34 @@ +import { moduleFor, AbstractTestCase } from 'internal-test-helpers'; + +import { createStorage, getValue, setValue } from '@glimmer/tracking/primitives/storage'; +import { createCache, getValue as getCacheValue } from '@glimmer/tracking/primitives/cache'; + +moduleFor( + '@glimmer/tracking/primitives/storage', + class extends AbstractTestCase { + ['@test it works'](assert: QUnit['assert']) { + let count = 0; + + let storage = createStorage(); + + let cache = createCache(() => { + getValue(storage); + return ++count; + }); + + assert.equal(getValue(storage), undefined, 'does not have a value initially'); + assert.equal(getCacheValue(cache), 1, 'cache runs the first time'); + assert.equal(getCacheValue(cache), 1, 'cache does not the second time'); + + setValue(storage, 123); + + assert.equal(getValue(storage), 123, 'value is set correctly'); + assert.equal(getCacheValue(cache), 2, 'cache ran after storage was set'); + + setValue(storage, 123); + + assert.equal(getValue(storage), 123, 'value remains the same'); + assert.equal(getCacheValue(cache), 2, 'cache not ran after storage was set to same value'); + } + } +);