diff --git a/packages/@glimmer/validator/lib/tracked-value.ts b/packages/@glimmer/validator/lib/tracked-value.ts index b08c964c6c7..3e2a619d0b8 100644 --- a/packages/@glimmer/validator/lib/tracked-value.ts +++ b/packages/@glimmer/validator/lib/tracked-value.ts @@ -28,6 +28,20 @@ export class TrackedValue implements Reactive { readonly #options: ReactiveOptions; readonly #tag: UpdatableTag; + /** + * `get`, `set`, `update` and `freeze` are bound to the instance, + * so that they can be detached from it. + * + * Each one is made the first time that it is read, + * so an instance that only uses `value` makes no function. + * + * Each one can be assigned, for example by a test that replaces it. + */ + #get: (() => Value) | undefined; + #set: ((value: Value) => boolean) | undefined; + #update: ((updater: (value: Value) => Value) => void) | undefined; + #freeze: (() => void) | undefined; + constructor(value: Value, options: ReactiveOptions) { this.#value = value; this.#options = options; @@ -48,15 +62,21 @@ export class TrackedValue implements Reactive { } set value(value: Value) { - this.set(value); + let set = this.#set; + + if (set === undefined) { + this.#write(value); + } else { + set(value); + } } /** * Function short-hand for reading `value`. */ - get = (): Value => { - return this.value; - }; + get get(): () => Value { + return (this.#get ??= () => this.value); + } /** * Function short-hand for assigning `value`. @@ -64,7 +84,30 @@ export class TrackedValue implements Reactive { * Returns `true` if the value changed (and consumers were notified), * `false` if the new value was equal to the current one. */ - set = (value: Value): boolean => { + get set(): (value: Value) => boolean { + return (this.#set ??= (value) => this.#write(value)); + } + + /** + * Update the value based on the current value, without consuming it. + */ + get update(): (updater: (value: Value) => Value) => void { + return (this.#update ??= (updater) => { + this.#write(updater(this.#value)); + }); + } + + /** + * Prevents further updates, making the TrackedValue behave as a + * ReadOnlyReactive. + */ + get freeze(): () => void { + return (this.#freeze ??= () => { + this.#isFrozen = true; + }); + } + + #write(value: Value): boolean { if (this.#isFrozen) { throw new Error( `Cannot update a frozen TrackedValue${ @@ -82,30 +125,24 @@ export class TrackedValue implements Reactive { DIRTY_TAG(this.#tag); return true; - }; - - /** - * Update the value based on the current value, without consuming it. - */ - update = (updater: (value: Value) => Value): void => { - this.set(updater(this.#value)); - }; - - /** - * Prevents further updates, making the TrackedValue behave as a - * ReadOnlyReactive. - */ - freeze = (): void => { - this.#isFrozen = true; - }; + } } +const DEFAULT_OPTIONS: ReactiveOptions = Object.freeze({ + equals: Object.is, + description: undefined, +}); + export function trackedValue( value: Value, options?: { equals?: (a: Value, b: Value) => boolean; description?: string } ): TrackedValue { + if (options === undefined) { + return new TrackedValue(value, DEFAULT_OPTIONS); + } + return new TrackedValue(value, { - equals: options?.equals ?? Object.is, - description: options?.description, + equals: options.equals ?? Object.is, + description: options.description, }); } diff --git a/packages/@glimmer/validator/test/tracked-value-test.ts b/packages/@glimmer/validator/test/tracked-value-test.ts index abcd6c6b63e..b7962546ab7 100644 --- a/packages/@glimmer/validator/test/tracked-value-test.ts +++ b/packages/@glimmer/validator/test/tracked-value-test.ts @@ -101,6 +101,25 @@ module('@glimmer/validator: trackedValue()', () => { assert.true(validateTag(tag, snapshot), 'update() did not entangle with the value'); }); + test('each method is the same function on each read', (assert) => { + const x = trackedValue(0); + + assert.strictEqual(x.get, x.get); + assert.strictEqual(x.set, x.set); + assert.strictEqual(x.update, x.update); + assert.strictEqual(x.freeze, x.freeze); + }); + + test('a detached freeze() prevents updates', (assert) => { + const x = trackedValue(0); + const { freeze, set } = x; + + freeze(); + + assert.throws(() => set(1), /frozen/u); + assert.throws(() => (x.value = 1), /frozen/u); + }); + test('methods can be detached from the instance', (assert) => { const x = trackedValue(0); const { get, set, update } = x;