From a7d181cd4f4fbde782ecd977bcb73c61d0624cf4 Mon Sep 17 00:00:00 2001 From: Chris Garrett Date: Sun, 10 May 2020 08:08:10 -0700 Subject: [PATCH] [FEATURE] Adds `cache` api Adds the cache API specified in [RFC 615](https://github.com/emberjs/rfcs/blob/master/text/0615-autotracking-memoization.md). There were a few minor refactors in addition to this: - Moved `trackedData` into a separate module, since the `tracking` module was getting pretty big. - Renamed `isConst` to `isConstTagged`, since there is a naming conflict with the new `isConst` function. Given `isConstTagged` will be removed soon and is not generally much used outside of the VM, I thought it was better to prioritize the new API getting this name. - Rewrote the `memoizeTracked` implementation on top of the cache APIs. One thing worth noting is that in order to support the API `memoizeTracked` had before, `getValue` also needed to be able to accept and pass on args. This is something we'll wrap to hide when we expose this publicly in Ember/Glimmer for the time being, since that was a capability that was not added in the RFC. --- .../integration-tests/lib/render-test.ts | 4 +- packages/@glimmer/reference/lib/template.ts | 4 +- .../runtime/lib/compiled/opcodes/component.ts | 4 +- .../runtime/lib/compiled/opcodes/content.ts | 4 +- .../runtime/lib/compiled/opcodes/dom.ts | 15 +- .../runtime/lib/compiled/opcodes/vm.ts | 15 +- packages/@glimmer/validator/index.ts | 9 +- .../@glimmer/validator/lib/tracked-data.ts | 42 +++ packages/@glimmer/validator/lib/tracking.ts | 254 ++++++++++-------- packages/@glimmer/validator/lib/validators.ts | 2 +- .../@glimmer/validator/test/tracking-test.ts | 163 ++++++++++- 11 files changed, 379 insertions(+), 137 deletions(-) create mode 100644 packages/@glimmer/validator/lib/tracked-data.ts diff --git a/packages/@glimmer/integration-tests/lib/render-test.ts b/packages/@glimmer/integration-tests/lib/render-test.ts index d08bffd4a2..6e5217afee 100644 --- a/packages/@glimmer/integration-tests/lib/render-test.ts +++ b/packages/@glimmer/integration-tests/lib/render-test.ts @@ -1,6 +1,6 @@ import { Dict, Maybe, Option, RenderResult, Helper } from '@glimmer/interfaces'; import { ASTPluginBuilder } from '@glimmer/syntax'; -import { bump, isConst } from '@glimmer/validator'; +import { bump, isConstTagged } from '@glimmer/validator'; import { clearElement, dict, expect, assign } from '@glimmer/util'; import { SimpleElement, SimpleNode } from '@simple-dom/interface'; import { @@ -393,7 +393,7 @@ export class RenderTest implements IRenderTest { let self = this.delegate.getSelf(this.context); - if (!isConst(self)) { + if (!isConstTagged(self)) { (self as UpdatableRootReference).forceUpdate(this.context); } diff --git a/packages/@glimmer/reference/lib/template.ts b/packages/@glimmer/reference/lib/template.ts index 75f765b8b4..64a356376b 100644 --- a/packages/@glimmer/reference/lib/template.ts +++ b/packages/@glimmer/reference/lib/template.ts @@ -10,7 +10,7 @@ import { updateTag, track, Revision, - isConst, + isConstTagged, isConstTag, valueForTag, validateTag, @@ -150,7 +150,7 @@ export class HelperRootReference extends RootReference { this.didSetupDebugContext = true; } - if (isConst(args)) { + if (isConstTagged(args)) { this.compute(); } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts index 0d35bbb510..64ee34e1e7 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/component.ts @@ -38,7 +38,7 @@ import { ModifierManager, } from '@glimmer/interfaces'; import { VersionedPathReference, VersionedReference } from '@glimmer/reference'; -import { CONSTANT_TAG, isConst, isConstTag, Tag } from '@glimmer/validator'; +import { CONSTANT_TAG, isConstTagged, isConstTag, Tag } from '@glimmer/validator'; import { assert, dict, @@ -538,7 +538,7 @@ function setDeferredAttr( vm.elements().setStaticAttribute(name, value, namespace); } else { let attribute = vm.elements().setDynamicAttribute(name, value.value(), trusting, namespace); - if (!isConst(value)) { + if (!isConstTagged(value)) { vm.updateWith(new UpdateDynamicAttributeOpcode(value, attribute)); } } diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts index 83cc3067a9..214d05b908 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/content.ts @@ -1,5 +1,5 @@ import { Reference } from '@glimmer/reference'; -import { Tag, isConst } from '@glimmer/validator'; +import { Tag, isConstTagged } from '@glimmer/validator'; import { check, CheckString, @@ -77,7 +77,7 @@ APPEND_OPCODES.add(Op.AppendText, vm => { let node = vm.elements().appendDynamicText(value); - if (!isConst(reference)) { + if (!isConstTagged(reference)) { vm.updateWith(new DynamicTextContent(node, reference, value)); } }); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts index 541bed26f1..2e09d53911 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/dom.ts @@ -1,5 +1,12 @@ import { Reference, ReferenceCache, VersionedReference } from '@glimmer/reference'; -import { Revision, Tag, isConst, isConstTag, valueForTag, validateTag } from '@glimmer/validator'; +import { + Revision, + Tag, + isConstTagged, + isConstTag, + valueForTag, + validateTag, +} from '@glimmer/validator'; import { check, CheckString, CheckElement, CheckOption, CheckNode } from '@glimmer/debug'; import { Op, Option, ModifierManager } from '@glimmer/interfaces'; import { $t0 } from '@glimmer/vm'; @@ -43,7 +50,7 @@ APPEND_OPCODES.add(Op.PushRemoteElement, vm => { let insertBefore: Maybe; let guid = guidRef.value() as string; - if (isConst(elementRef)) { + if (isConstTagged(elementRef)) { element = check(elementRef.value(), CheckElement); } else { let cache = new ReferenceCache(elementRef as Reference); @@ -52,7 +59,7 @@ APPEND_OPCODES.add(Op.PushRemoteElement, vm => { } if (insertBeforeRef.value() !== undefined) { - if (isConst(insertBeforeRef)) { + if (isConstTagged(insertBeforeRef)) { insertBefore = check(insertBeforeRef.value(), CheckOption(CheckNode)); } else { let cache = new ReferenceCache(insertBeforeRef as Reference>); @@ -163,7 +170,7 @@ APPEND_OPCODES.add(Op.DynamicAttr, (vm, { op1: _name, op2: trusting, op3: _names let attribute = vm.elements().setDynamicAttribute(name, value, !!trusting, namespace); - if (!isConst(reference)) { + if (!isConstTagged(reference)) { vm.updateWith(new UpdateDynamicAttributeOpcode(reference, attribute)); } }); diff --git a/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts b/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts index 68ae88b3a0..f33dd1d8e3 100644 --- a/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts +++ b/packages/@glimmer/runtime/lib/compiled/opcodes/vm.ts @@ -1,6 +1,13 @@ import { CompilableTemplate, Option, Op } from '@glimmer/interfaces'; import { isModified, ReferenceCache } from '@glimmer/reference'; -import { CONSTANT_TAG, isConst, Revision, Tag, valueForTag, validateTag } from '@glimmer/validator'; +import { + CONSTANT_TAG, + isConstTagged, + Revision, + Tag, + valueForTag, + validateTag, +} from '@glimmer/validator'; import { initializeGuid, assert, isHandle, HandleConstants, decodeHandle } from '@glimmer/util'; import { CheckNumber, @@ -160,7 +167,7 @@ APPEND_OPCODES.add(Op.InvokeYield, vm => { APPEND_OPCODES.add(Op.JumpIf, (vm, { op1: target }) => { let reference = check(vm.stack.pop(), CheckReference); - if (isConst(reference)) { + if (isConstTagged(reference)) { if (reference.value()) { vm.goto(target); } @@ -178,7 +185,7 @@ APPEND_OPCODES.add(Op.JumpIf, (vm, { op1: target }) => { APPEND_OPCODES.add(Op.JumpUnless, (vm, { op1: target }) => { let reference = check(vm.stack.pop(), CheckReference); - if (isConst(reference)) { + if (isConstTagged(reference)) { if (!reference.value()) { vm.goto(target); } @@ -204,7 +211,7 @@ APPEND_OPCODES.add(Op.JumpEq, (vm, { op1: target, op2: comparison }) => { APPEND_OPCODES.add(Op.AssertSame, vm => { let reference = check(vm.stack.peek(), CheckReference); - if (!isConst(reference)) { + if (!isConstTagged(reference)) { vm.updateWith(Assert.initialize(new ReferenceCache(reference))); } }); diff --git a/packages/@glimmer/validator/index.ts b/packages/@glimmer/validator/index.ts index e6bc7c08c4..1434462117 100644 --- a/packages/@glimmer/validator/index.ts +++ b/packages/@glimmer/validator/index.ts @@ -17,7 +17,7 @@ export { EntityTag, EntityTagged, INITIAL, - isConst, + isConstTagged, isConstTag, Revision, Tag, @@ -39,12 +39,17 @@ export { consumeTag, isTracking, track, - trackedData, memo, untrack, isConstMemo, + Cache, + createCache, + isConst, + getValue, } from './lib/tracking'; +export { trackedData } from './lib/tracked-data'; + export { setAutotrackingTransactionEnv, runInAutotrackingTransaction, diff --git a/packages/@glimmer/validator/lib/tracked-data.ts b/packages/@glimmer/validator/lib/tracked-data.ts new file mode 100644 index 0000000000..84fe523ce8 --- /dev/null +++ b/packages/@glimmer/validator/lib/tracked-data.ts @@ -0,0 +1,42 @@ +import { DEBUG } from '@glimmer/env'; +import { tagFor, dirtyTagFor } from './meta'; +import { assertTagNotConsumed } from './debug'; +import { consumeTag } from './tracking'; + +export type Getter = (self: T) => T[K] | undefined; +export type Setter = (self: T, value: T[K]) => void; + +export function trackedData( + key: K, + initializer?: (this: T) => T[K] +): { getter: Getter; setter: Setter } { + let values = new WeakMap(); + let hasInitializer = typeof initializer === 'function'; + + function getter(self: T) { + consumeTag(tagFor(self, key)); + + let value; + + // If the field has never been initialized, we should initialize it + if (hasInitializer && !values.has(self)) { + value = initializer!.call(self); + values.set(self, value); + } else { + value = values.get(self); + } + + return value; + } + + function setter(self: T, value: T[K]): void { + if (DEBUG) { + assertTagNotConsumed!(tagFor(self, key), self, key, true); + } + + dirtyTagFor(self, key); + values.set(self, value); + } + + return { getter, setter }; +} diff --git a/packages/@glimmer/validator/lib/tracking.ts b/packages/@glimmer/validator/lib/tracking.ts index f5a041063b..e3cfa2cc22 100644 --- a/packages/@glimmer/validator/lib/tracking.ts +++ b/packages/@glimmer/validator/lib/tracking.ts @@ -1,7 +1,16 @@ import { DEBUG } from '@glimmer/env'; -import { Tag, combine, CONSTANT_TAG, validateTag, Revision, valueForTag } from './validators'; -import { tagFor, dirtyTagFor } from './meta'; -import { markTagAsConsumed, runInAutotrackingTransaction, assertTagNotConsumed } from './debug'; +import { + Tag, + combine, + CONSTANT_TAG, + validateTag, + Revision, + valueForTag, + isConstTag, +} from './validators'; + +import { markTagAsConsumed, runInAutotrackingTransaction } from './debug'; +import { symbol } from './utils'; type Option = T | null; @@ -72,88 +81,145 @@ export function endTrackFrame(): Tag { return current!.combine(); } +export function isTracking() { + return CURRENT_TRACKER !== null; +} + +export function consumeTag(tag: Tag) { + if (CURRENT_TRACKER !== null) { + CURRENT_TRACKER.add(tag); + } +} + ////////// -const IS_CONST_MAP: WeakMap = new WeakMap(); - -export function memo(cb: () => T, context?: string | false): () => T; -export function memo(cb: (a1: U) => T, context?: string | false): (a1: U) => T; -export function memo( - cb: (a1: U, a2: V, a3: W) => T, - context?: string | false -): (a1: U, a2: V, a3: W) => T; -export function memo( - cb: (a1: U, a2: V, a3: W, a4: X) => T, - context?: string | false -): (a1: U, a2: V, a3: W, a4: X) => T; -export function memo( - cb: (a1: U, a2: V, a3: W, a4: X, a5: Y) => T, - context?: string | false -): (a1: U, a2: V, a3: W, a4: X, a5: Y) => T; -export function memo( - cb: (a1: U, a2: V, a3: W, a4: X, a5: Y, a6: Z) => T, - context?: string | false -): (a1: U, a2: V, a3: W, a4: X, a5: Y, a6: Z) => T; - -export function memo(callback: (...args: unknown[]) => T, debuggingContext?: string | false) { - let lastValue: T | undefined; - let tag: Tag; - let snapshot: Revision; - - let memoized = (...args: unknown[]): T => { - if (!tag || !validateTag(tag, snapshot)) { - beginTrackFrame(); - - try { - if (DEBUG) { - runInAutotrackingTransaction!(() => (lastValue = callback(...args)), debuggingContext); - } else { - lastValue = callback(...args); - } - } finally { - tag = endTrackFrame(); - snapshot = valueForTag(tag); - consumeTag(tag); - - // If the final tag is constant, then we know for sure that this - // memoized function can never change. There are times when this - // information is useful externally (i.e. in the append VM, it tells us - // whether or not to emit opcodes) so we expose it via a metadata weakmap. - if (tag === CONSTANT_TAG) { - IS_CONST_MAP.set(memoized, true); - } else if (DEBUG) { - // In DEBUG, set the value to false explicitly. This way we can throw - // if someone attempts to call `isConst(memoized)` before running - // `memoized()` at least once. - IS_CONST_MAP.set(memoized, false); - } - } - } else { - consumeTag(tag); - } +const CACHE_KEY: unique symbol = symbol('CACHE_KEY'); + +export function memo(callback: () => T, debuggingContext?: string | false) { + let cache = createCache(callback, debuggingContext); + + let memoized = () => getValue(cache); + + (memoized as any)[CACHE_KEY] = cache; + + return memoized; +} + +export function isConstMemo(memoized: Function) { + let cache = (memoized as any)[CACHE_KEY] as Cache | undefined; + + return cache === undefined ? false : isConst(cache); +} - return lastValue!; +////////// + +// public interface +export interface Cache { + [CACHE_KEY]: T; +} + +const FN: unique symbol = symbol('FN'); +const LAST_VALUE: unique symbol = symbol('LAST_VALUE'); +const TAG: unique symbol = symbol('TAG'); +const SNAPSHOT: unique symbol = symbol('SNAPSHOT'); +const DEBUG_LABEL: unique symbol = symbol('DEBUG_LABEL'); + +interface InternalCache { + [FN]: (...args: unknown[]) => T; + [LAST_VALUE]: T | undefined; + [TAG]: Tag | undefined; + [SNAPSHOT]: Revision; + [DEBUG_LABEL]?: string | false; +} + +export function createCache(fn: () => T, debuggingLabel?: string | false): Cache { + if (DEBUG && !(typeof fn === 'function')) { + throw new Error( + `createCache() must be passed a function as its first parameter. Called with: ${String(fn)}` + ); + } + + let cache: InternalCache = { + [FN]: fn, + [LAST_VALUE]: undefined, + [TAG]: undefined, + [SNAPSHOT]: -1, }; if (DEBUG) { - IS_CONST_MAP.set(memoized, undefined); + cache[DEBUG_LABEL] = debuggingLabel; } - return memoized; + return (cache as unknown) as Cache; } -export function isConstMemo(memoized: Function) { - if (DEBUG && IS_CONST_MAP.has(memoized) && IS_CONST_MAP.get(memoized) === undefined) { +export function getValue(cache: Cache): T { + if (!isCache(cache)) { + throw new Error( + `getValue() can only be used on an instance of a cache created with createCache(). Called with: ${String( + cache + )}` + ); + } + + let fn = cache[FN]; + let tag = cache[TAG]; + let snapshot = cache[SNAPSHOT]; + + if (tag === undefined || !validateTag(tag, snapshot)) { + beginTrackFrame(); + + try { + if (DEBUG) { + runInAutotrackingTransaction!(() => (cache[LAST_VALUE] = fn()), cache[DEBUG_LABEL]); + } else { + cache[LAST_VALUE] = fn(); + } + } finally { + tag = endTrackFrame(); + cache[TAG] = tag; + cache[SNAPSHOT] = valueForTag(tag); + consumeTag(tag); + } + } else { + consumeTag(tag); + } + + return cache[LAST_VALUE]!; +} + +export function isConst(cache: Cache) { + if (!isCache(cache)) { + throw new Error( + `isConst() can only be used on an instance of a cache created with createCache(). Called with: ${String( + cache + )}` + ); + } + + if (DEBUG && cache[TAG] === undefined) { throw new Error( - 'Attempted to call `isConstMemo` on a memoized function, but the function has not been run at least once yet. You cannot know if a memoized function is constant or not until it has been run at least once. Call the function, then pass it to `isConstMemo`.' + `isConst() can only be used on a cache once getValue() has been called at least once. Called with cache function:\n\n${String( + cache[FN] + )}` ); } - return IS_CONST_MAP.get(memoized) === true; + return isConstTag(cache[TAG]!); +} + +function isCache(value: Cache | InternalCache): value is InternalCache { + return DEBUG ? FN in value : true; } ////////// +// Legacy tracking APIs + +// track() shouldn't be necessary at all in the VM once the autotracking +// refactors are merged, and we should generally be moving away from it. It may +// be necessary in Ember for a while longer, but I think we'll be able to drop +// it in favor of cache sooner rather than later. export function track(callback: () => void, debuggingContext?: string | false): Tag { beginTrackFrame(); @@ -172,16 +238,10 @@ export function track(callback: () => void, debuggingContext?: string | false): return tag; } -export function consumeTag(tag: Tag) { - if (CURRENT_TRACKER !== null) { - CURRENT_TRACKER.add(tag); - } -} - -export function isTracking() { - return CURRENT_TRACKER !== null; -} - +// untrack() is currently mainly used to handle places that were previously not +// tracked, and that tracking now would cause backtracking rerender assertions. +// I think once we move everyone forward onto modern APIs, we'll probably be +// able to remove it, but I'm not sure yet. export function untrack(callback: () => void) { OPEN_TRACK_FRAMES.push(CURRENT_TRACKER); CURRENT_TRACKER = null; @@ -192,43 +252,3 @@ export function untrack(callback: () => void) { CURRENT_TRACKER = OPEN_TRACK_FRAMES.pop()!; } } - -////////// - -export type Getter = (self: T) => T[K] | undefined; -export type Setter = (self: T, value: T[K]) => void; - -export function trackedData( - key: K, - initializer?: (this: T) => T[K] -): { getter: Getter; setter: Setter } { - let values = new WeakMap(); - let hasInitializer = typeof initializer === 'function'; - - function getter(self: T) { - consumeTag(tagFor(self, key)); - - let value; - - // If the field has never been initialized, we should initialize it - if (hasInitializer && !values.has(self)) { - value = initializer!.call(self); - values.set(self, value); - } else { - value = values.get(self); - } - - return value; - } - - function setter(self: T, value: T[K]): void { - if (DEBUG) { - assertTagNotConsumed!(tagFor(self, key), self, key, true); - } - - dirtyTagFor(self, key); - values.set(self, value); - } - - return { getter, setter }; -} diff --git a/packages/@glimmer/validator/lib/validators.ts b/packages/@glimmer/validator/lib/validators.ts index 82c56961b0..9c032c470c 100644 --- a/packages/@glimmer/validator/lib/validators.ts +++ b/packages/@glimmer/validator/lib/validators.ts @@ -241,7 +241,7 @@ export function createUpdatableTag(): UpdatableTag { export const CONSTANT_TAG = new MonomorphicTagImpl(MonomorphicTagTypes.Constant) as ConstantTag; -export function isConst({ tag }: Tagged): boolean { +export function isConstTagged({ tag }: Tagged): boolean { return tag === CONSTANT_TAG; } diff --git a/packages/@glimmer/validator/test/tracking-test.ts b/packages/@glimmer/validator/test/tracking-test.ts index 4437ed876c..fcfc0a53e5 100644 --- a/packages/@glimmer/validator/test/tracking-test.ts +++ b/packages/@glimmer/validator/test/tracking-test.ts @@ -21,6 +21,9 @@ import { untrack, validateTag, valueForTag, + createCache, + isConst, + getValue, } from '..'; module('@glimmer/validator: tracking', () => { @@ -386,7 +389,165 @@ module('@glimmer/validator: tracking', () => { assert.throws( () => isConstMemo(fn), - /Error: Attempted to call `isConstMemo` on a memoized function/ + /Error: isConst\(\) can only be used on a cache once getValue\(\) has been called at least once/ + ); + }); + } + }); + + module('tracking cache', () => { + test('it memoizes based on tags that are consumed within a track frame', assert => { + let tag1 = createTag(); + let tag2 = createTag(); + let count = 0; + + let cache = createCache(() => { + consumeTag(tag1); + consumeTag(tag2); + + return ++count; + }); + + assert.equal(getValue(cache), 1, 'called correctly the first time'); + assert.equal(getValue(cache), 1, 'memoized result returned second time'); + + dirtyTag(tag1); + assert.equal(getValue(cache), 2, 'cache busted when tag1 dirtied'); + assert.equal(getValue(cache), 2, 'memoized result returned when nothing dirtied'); + + dirtyTag(tag2); + assert.equal(getValue(cache), 3, 'cache busted when tag2 dirtied'); + assert.equal(getValue(cache), 3, 'memoized result returned when nothing dirtied'); + }); + + test('it ignores tags consumed within an untrack frame', assert => { + let tag1 = createTag(); + let tag2 = createTag(); + let count = 0; + + let cache = createCache(() => { + consumeTag(tag1); + + untrack(() => consumeTag(tag2)); + + return ++count; + }); + + assert.equal(getValue(cache), 1, 'called correctly the first time'); + assert.equal(getValue(cache), 1, 'memoized result returned second time'); + + dirtyTag(tag1); + assert.equal(getValue(cache), 2, 'cache busted when tag1 dirtied'); + assert.equal(getValue(cache), 2, 'memoized result returned when nothing dirtied'); + + dirtyTag(tag2); + assert.equal(getValue(cache), 2, 'cache not busted when tag2 dirtied'); + }); + + test('nested memoizations work, and automatically propogate', assert => { + let innerTag = createTag(); + let outerTag = createTag(); + + let innerCount = 0; + let outerCount = 0; + + let innerCache = createCache(() => { + consumeTag(innerTag); + + return ++innerCount; + }); + + let outerCache = createCache(() => { + consumeTag(outerTag); + + return [++outerCount, getValue(innerCache)]; + }); + + assert.deepEqual( + getValue(outerCache), + [1, 1], + 'both functions called correctly the first time' + ); + assert.deepEqual(getValue(outerCache), [1, 1], 'memoized result returned correctly'); + + dirtyTag(outerTag); + + assert.deepEqual( + getValue(outerCache), + [2, 1], + 'outer result updated, inner result still memoized' + ); + assert.deepEqual(getValue(outerCache), [2, 1], 'memoized result returned correctly'); + + dirtyTag(innerTag); + + assert.deepEqual(getValue(outerCache), [3, 2], 'both inner and outer result updated'); + assert.deepEqual(getValue(outerCache), [3, 2], 'memoized result returned correctly'); + }); + + test('isTracking works within a memoized function and untrack frame', assert => { + assert.expect(3); + assert.notOk(isTracking()); + + let cache = createCache(() => { + assert.ok(isTracking()); + + untrack(() => { + assert.notOk(isTracking()); + }); + }); + + getValue(cache); + }); + + test('isConst allows users to check if a memoized function is constant', assert => { + let tag = createTag(); + + let constCache = createCache(() => { + // do nothing; + }); + + let nonConstCache = createCache(() => { + consumeTag(tag); + }); + + getValue(constCache); + getValue(nonConstCache); + + assert.ok(isConst(constCache), 'constant cache returns true'); + assert.notOk(isConst(nonConstCache), 'non-constant cache returns false'); + }); + + if (DEBUG) { + test('createCache throws an error in DEBUG mode if users to use with a non-function', assert => { + assert.throws( + () => createCache(123 as any), + /Error: createCache\(\) must be passed a function as its first parameter. Called with: 123/ + ); + }); + + test('getValue throws an error in DEBUG mode if users to use with a non-cache', assert => { + assert.throws( + () => getValue(123 as any), + /Error: getValue\(\) can only be used on an instance of a cache created with createCache\(\). Called with: 123/ + ); + }); + + test('isConst throws an error in DEBUG mode if users attempt to check a function before it has been called', assert => { + let cache = createCache(() => { + // do nothing; + }); + + assert.throws( + () => isConst(cache), + /Error: isConst\(\) can only be used on a cache once getValue\(\) has been called at least once/ + ); + }); + + test('isConst throws an error in DEBUG mode if users attempt to use with a non-cache', assert => { + assert.throws( + () => isConst(123 as any), + /Error: isConst\(\) can only be used on an instance of a cache created with createCache\(\). Called with: 123/ ); }); }