From 79775f77b9cb1da64f7138a487b29790f9051138 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Thu, 21 Jan 2021 17:54:01 +0200 Subject: [PATCH 1/2] allow custom implementation of equals function to be provided when creating an atom --- packages/focal/src/atom/base.ts | 6 +++--- packages/focal/src/atom/index.ts | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 5 deletions(-) diff --git a/packages/focal/src/atom/base.ts b/packages/focal/src/atom/base.ts index 2c40990..756fad3 100644 --- a/packages/focal/src/atom/base.ts +++ b/packages/focal/src/atom/base.ts @@ -297,7 +297,7 @@ export abstract class AbstractAtom } export class JsonAtom extends AbstractAtom { - constructor(initialValue: T) { + constructor(initialValue: T, private _eq: (x: T, y: T) => boolean = structEq) { super(initialValue) } @@ -309,14 +309,14 @@ export class JsonAtom extends AbstractAtom { const prevValue = this.getValue() const next = updateFn(prevValue) - if (!structEq(prevValue, next)) + if (!this._eq(prevValue, next)) this.next(next) } set(x: T) { const prevValue = this.getValue() - if (!structEq(prevValue, x)) + if (!this._eq(prevValue, x)) this.next(x) } } diff --git a/packages/focal/src/atom/index.ts b/packages/focal/src/atom/index.ts index 740ec82..64e7cde 100644 --- a/packages/focal/src/atom/index.ts +++ b/packages/focal/src/atom/index.ts @@ -23,10 +23,24 @@ export namespace Atom { * @export * @template T type of atom values * @param initialValue initial value for this atom + * @param eq - optional custom equality check function, + * used to compare prev and next atom values to emit value only when it has changed. + * + * !!!BEWARE!!!: make sure that your custom equality check function is optimal + * (e.g. checks for value reference equality first), + * as it will be called EVERY time an atom value is changed + * + * (when setting(modifying) an atom value, a new value is only emitted by atom if it has changed, + * i.e. it does not equal to the previous value) + * + * If not specified, a default private implementation of structure equality is used. + * + * As an alternative, a value may have an 'equals' method that will be recognized + * by default structure equality implementation * @returns fresh atom */ - export function create(initialValue: T): Atom { - return new JsonAtom(initialValue) + export function create(initialValue: T, eq?: (x: T, y: T) => boolean): Atom { + return new JsonAtom(initialValue, eq) } // tslint:disable no-unused-vars From 688ef41db3c7636ba03cb748ed6ee9ebdac78335 Mon Sep 17 00:00:00 2001 From: Oleksandr Date: Thu, 21 Jan 2021 19:08:43 +0200 Subject: [PATCH 2/2] add tests for custom equality --- packages/focal/test/atom.test.ts | 79 +++++++++++++++++++++++++++----- 1 file changed, 67 insertions(+), 12 deletions(-) diff --git a/packages/focal/test/atom.test.ts b/packages/focal/test/atom.test.ts index b9147de..8453d3d 100644 --- a/packages/focal/test/atom.test.ts +++ b/packages/focal/test/atom.test.ts @@ -500,42 +500,36 @@ describe('atom', () => { let called1 = 0 const a1 = source.view(x => { - console.log('a1', called1, x) called1++ return x + 1 }) let called2 = 0 const a2 = a1.view(x => { - console.log('a2', called2, x) called2++ return -x }) let called3 = 0 const a3 = a2.view(x => { - console.log('a3', called3, x) called3++ return x * 2 }) let called4 = 0 const a4 = a3.view(x => { - console.log('a4', called4, x) called4++ return `Hi ${x}` }) let called5 = 0 const a5 = a3.view(x => { - console.log('a5', called5, x) called5++ return `Ho ${x}` }) let called6 = 0 const a6 = a3.view(x => { - console.log('a6', called6, x) called6++ return `HU ${x}` }) @@ -583,42 +577,36 @@ describe('atom', () => { let called1 = 0 const a1 = source.view(x => { - console.log('a1', called1, x) called1++ return x + 1 }) let called2 = 0 const a2 = a1.view(x => { - console.log('a2', called2, x) called2++ return -x }) let called3 = 0 const a3 = a2.view(x => { - console.log('a3', called3, x) called3++ return x * 2 }) let called4 = 0 const a4 = a3.view(x => { - console.log('a4', called4, x) called4++ return `Hi ${x}` }) let called5 = 0 const a5 = a3.view(x => { - console.log('a5', called5, x) called5++ return `Ho ${x}` }) let called6 = 0 const a6 = a3.view(x => { - console.log('a6', called6, x) called6++ return `HU ${x}` }) @@ -879,4 +867,71 @@ describe('atom', () => { ).toEqual(['N', 'C']) }) }) + + describe('value equality', () => { + interface TestValue { + a: number, + b: number + } + + const eqByA = (a: TestValue, b: TestValue): boolean => { + console.log('equals', a.a === b.a) + return a.a === b.a + } + + it('custom equals is called on modify', () => { + const eqMock = jest.fn(eqByA) + const a = Atom.create({ a: 1, b: 2 }, eqMock) + + a.modify(v => ({ ...v, b: 3 })) + + expect(eqMock.mock.calls.length).toBe(1) + }) + + it('custom equals used on modify', () => { + const a = Atom.create({ a: 1, b: 2 }, eqByA) + const observations: TestValue[] = [] + const cb = (x: TestValue) => observations.push(x) + const subscription = a.subscribe(cb) + + a.modify(v => ({ ...v, b: 3 })) + a.modify(v => ({ ...v, a: 2 })) + + expect(observations.length).toBe(2) // including initial value + expect(observations).toEqual([{ a: 1, b: 2 }, { a: 2, b: 2 }]) + + subscription.unsubscribe() + }) + + it('custom equals used on set', () => { + const a = Atom.create({ a: 1, b: 2 }, eqByA) + const observations: TestValue[] = [] + const cb = (x: TestValue) => observations.push(x) + const subscription = a.subscribe(cb) + + a.set({ a: 1, b: 3 }) + a.set({ a: 2, b: 2 }) + + expect(observations.length).toBe(2) // including initial value + expect(observations).toEqual([{ a: 1, b: 2 }, { a: 2, b: 2 }]) + + subscription.unsubscribe() + }) + + it('custom equals used in child atom', () => { + const a = Atom.create({ a: 1, b: 2 }, eqByA) + const b = a.view(v => v.b) + const bObservations: number[] = [] + const cb = (x: number) => bObservations.push(x) + const subscription = b.subscribe(cb) + + a.set({ a: 1, b: 3 }) + a.set({ a: 2, b: 4 }) + + expect(bObservations.length).toBe(2) // including initial value + expect(bObservations).toEqual([2, 4]) + + subscription.unsubscribe() + }) + }) })