diff --git a/packages/injectable/react/index.d.ts b/packages/injectable/react/index.d.ts index 47464623..7e66a8a3 100644 --- a/packages/injectable/react/index.d.ts +++ b/packages/injectable/react/index.d.ts @@ -1,9 +1,5 @@ /// -import type { - DiContainer, - DiContainerForInjection, -} from '@lensapp/injectable'; -import { IComputedValue } from 'mobx'; +import type { DiContainer, DiContainerForInjection } from '@lensapp/injectable'; interface DiContainerProviderProps { di: DiContainer | DiContainerForInjection; @@ -54,20 +50,4 @@ export interface WithInjectables { export const withInjectables: WithInjectables; -export type IAsyncComputed = { - value: IComputedValue; - pending: IComputedValue; - invalidate: () => void; -}; - -type AsyncComputedParams = { - getValueFromObservedPromise: () => Promise; - valueWhenPending?: T; - betweenUpdates?: 'show-pending-value' | 'show-latest-value'; -}; - -export function asyncComputed( - configuration: AsyncComputedParams, -): IAsyncComputed; - export function registerInjectableReact(di: DiContainer): void; diff --git a/packages/injectable/react/index.js b/packages/injectable/react/index.js index a0aa3ed3..fce3e9b4 100644 --- a/packages/injectable/react/index.js +++ b/packages/injectable/react/index.js @@ -2,12 +2,6 @@ import withInjectables, { DiContextProvider, } from './src/withInjectables/withInjectables'; -import asyncComputed from './src/asyncComputed/asyncComputed'; import registerInjectableReact from './src/registerInjectableReact/registerInjectableReact'; -export { - withInjectables, - DiContextProvider, - asyncComputed, - registerInjectableReact, -}; +export { withInjectables, DiContextProvider, registerInjectableReact }; diff --git a/packages/injectable/react/src/asyncComputed/asyncComputed.js b/packages/injectable/react/src/asyncComputed/asyncComputed.js deleted file mode 100644 index bc973ded..00000000 --- a/packages/injectable/react/src/asyncComputed/asyncComputed.js +++ /dev/null @@ -1,93 +0,0 @@ -import { noop } from 'lodash/fp'; -import { computed, createAtom, observable, runInAction, untracked } from 'mobx'; - -const neutralizeObsoletePromiseSymbol = Symbol.for( - 'neutralize-obsolete-promise', -); - -export default ({ - getValueFromObservedPromise, - valueWhenPending, - betweenUpdates = 'show-pending-value', -}) => { - const invalidateAtom = createAtom('invalidate'); - - const pendingBox = observable.box(false); - - let neutralizeObsoletePromise = noop; - - const syncValueBox = observable.box(valueWhenPending, { - name: 'sync-value-box-for-async-computed', - deep: false, - }); - - const computedPromise = computed( - () => { - if (untracked(() => pendingBox.get()) === true) { - neutralizeObsoletePromise(); - } - - invalidateAtom.reportObserved(); - - runInAction(() => { - pendingBox.set(true); - if (betweenUpdates === 'show-pending-value') { - syncValueBox.set(valueWhenPending); - } - }); - - return Promise.race([ - getValueFromObservedPromise(), - - new Promise(resolve => { - neutralizeObsoletePromise = () => - resolve(neutralizeObsoletePromiseSymbol); - }), - ]); - }, - { - name: 'computed-promise-for-async-computed', - }, - ); - - const originalComputed = computed( - () => { - computedPromise.get().then(syncValue => { - if (syncValue !== neutralizeObsoletePromiseSymbol) { - runInAction(() => { - pendingBox.set(false); - syncValueBox.set(syncValue); - }); - } - }); - - return syncValueBox.get(); - }, - - { - name: 'computed-promise-result-for-async-computed', - keepAlive: true, - }, - ); - - return { - value: originalComputed, - - invalidate: () => { - runInAction(() => { - invalidateAtom.reportChanged(); - pendingBox.set(true); - - if (betweenUpdates === 'show-pending-value') { - syncValueBox.set(valueWhenPending); - } - }); - }, - - pending: computed(() => { - originalComputed.get(); - - return pendingBox.get(); - }), - }; -}; diff --git a/packages/injectable/react/src/asyncComputed/asyncComputed.test.js b/packages/injectable/react/src/asyncComputed/asyncComputed.test.js deleted file mode 100644 index 7382d0a8..00000000 --- a/packages/injectable/react/src/asyncComputed/asyncComputed.test.js +++ /dev/null @@ -1,829 +0,0 @@ -import asyncComputed from './asyncComputed'; -import asyncFn from '@async-fn/jest'; -import { isObservableProp, observable, observe, runInAction } from 'mobx'; - -describe('asyncComputed', () => { - describe('given callback to observe something that returns a promise, and a specific value for when pending', () => { - let someMock; - let someAsyncComputed; - let someObservable; - - beforeEach(() => { - someMock = asyncFn(); - - someObservable = observable.box('some-initial-value'); - - someAsyncComputed = asyncComputed({ - getValueFromObservedPromise: () => { - const someObservedValue = someObservable.get(); - - return someMock(someObservedValue); - }, - - valueWhenPending: 'some-pending-value', - }); - }); - - it('given invalidated before observation, when observed, does not throw', () => { - someAsyncComputed.invalidate(); - - expect(() => { - observe(someAsyncComputed.value, () => {}); - }).not.toThrow(); - }); - - describe('when only status is observed but not value', () => { - beforeEach(() => { - observe(someAsyncComputed.pending, () => {}); - }); - - it('when status is observed, computes', () => { - expect(someMock).toHaveBeenCalled(); - }); - - describe('when observed promise resolves', () => { - beforeEach(async () => { - await someMock.resolve('some-promise-result'); - }); - - it('is no longer pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - - it('when value is observed, observed value is the result of promise', () => { - let observedValue; - - observe( - someAsyncComputed.value, - - change => { - observedValue = change.newValue; - }, - true, - ); - - expect(observedValue).toBe('some-promise-result'); - }); - }); - }); - - it('when status is observed multiple times, computes only once', () => { - observe(someAsyncComputed.pending, () => {}); - observe(someAsyncComputed.pending, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - describe('given value is observed', () => { - let observedValue; - - beforeEach(() => { - observe( - someAsyncComputed.value, - - change => { - observedValue = change.newValue; - }, - true, - ); - }); - - it('computes', () => { - expect(someMock).toHaveBeenCalledWith('some-initial-value'); - }); - - describe('when observed promise has not resolved yet', () => { - it('observed value is pending value', async () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('but another change is observed', () => { - beforeEach(() => { - someMock.mockClear(); - - runInAction(() => { - someObservable.set('some-other-changed-value'); - }); - }); - - it('computes', () => { - expect(someMock).toHaveBeenCalledWith('some-other-changed-value'); - }); - - it('observed value is pending value', async () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('when the obsolete promise resolves', () => { - beforeEach(async () => { - await someMock.resolveSpecific( - ['some-initial-value'], - 'some-obsolete-promise-result', - ); - }); - - it('still observes value as pending value', async () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('still observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('when the latest promise resolves', () => { - beforeEach(async () => { - await someMock.resolveSpecific( - ['some-other-changed-value'], - 'some-latest-promise-result', - ); - }); - - it('observed value is result of latest promise', async () => { - expect(observedValue).toBe('some-latest-promise-result'); - }); - - it('observes as not pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - }); - }); - }); - }); - - it('when promise resolves as non observable object, computed value is also non observable', async () => { - await someMock.resolve({ some: 'non-observable-object' }); - - expect(isObservableProp(observedValue, 'some')).toBe(false); - }); - - describe('when observed promise resolves', () => { - beforeEach(async () => { - await someMock.resolve('some-promise-result'); - }); - - it('observed value is the result of promise', () => { - expect(observedValue).toBe('some-promise-result'); - }); - - it('is no longer pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - - it('when observed again, still does not recompute', () => { - someMock.mockClear(); - - observe(someAsyncComputed.value, () => {}); - - expect(someMock).not.toHaveBeenCalled(); - }); - - describe('when a change is observed', () => { - beforeEach(() => { - someMock.mockClear(); - - runInAction(() => { - someObservable.set('some-changed-value'); - }); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - it('observed value is pending value', () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('recomputes', () => { - expect(someMock).toHaveBeenCalledWith('some-changed-value'); - }); - }); - - describe('when invalidated', () => { - beforeEach(() => { - someMock.mockClear(); - - someAsyncComputed.invalidate(); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - it('observed value is pending value', () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('recomputes', () => { - expect(someMock).toHaveBeenCalledWith('some-initial-value'); - }); - }); - }); - }); - - it('given observed and unobserved, when observed again, does not recompute', () => { - const unobserve = observe(someAsyncComputed.value, () => {}); - - unobserve(); - - observe(someAsyncComputed.value, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - it('given callback, when observed multiple times, does not recompute', () => { - observe(someAsyncComputed.value, () => {}); - observe(someAsyncComputed.value, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - it('when not observed, does not call callback', () => { - expect(someMock).not.toHaveBeenCalled(); - }); - }); - - describe('given callback to observe something that returns a promise, and no specific value for when pending', () => { - let someMock; - let someAsyncComputed; - let someObservable; - - beforeEach(() => { - someMock = asyncFn(); - - someObservable = observable.box('some-initial-value'); - - someAsyncComputed = asyncComputed({ - getValueFromObservedPromise: () => { - const someObservedValue = someObservable.get(); - - return someMock(someObservedValue); - }, - - // Notice: no pending value. - // valueWhenPending: 'some-pending-value', - }); - }); - - it('given invalidated before observation, when observed, does not throw', () => { - someAsyncComputed.invalidate(); - - expect(() => { - observe(someAsyncComputed.value, () => {}); - }).not.toThrow(); - }); - - describe('when only status is observed but not value', () => { - beforeEach(() => { - observe(someAsyncComputed.pending, () => {}); - }); - - it('when status is observed, computes', () => { - expect(someMock).toHaveBeenCalled(); - }); - - describe('when observed promise resolves', () => { - beforeEach(async () => { - await someMock.resolve('some-promise-result'); - }); - - it('is no longer pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - - it('when value is observed, observed value is the result of promise', () => { - let observedValue; - - observe( - someAsyncComputed.value, - - change => { - observedValue = change.newValue; - }, - true, - ); - - expect(observedValue).toBe('some-promise-result'); - }); - }); - }); - - it('when status is observed multiple times, computes only once', () => { - observe(someAsyncComputed.pending, () => {}); - observe(someAsyncComputed.pending, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - describe('given value is observed', () => { - let observedValue; - - beforeEach(() => { - observe( - someAsyncComputed.value, - - change => { - observedValue = change.newValue; - }, - true, - ); - }); - - it('computes', () => { - expect(someMock).toHaveBeenCalledWith('some-initial-value'); - }); - - describe('when observed promise has not resolved yet', () => { - it('observed value is undefined', async () => { - expect(observedValue).toBe(undefined); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('but another change is observed', () => { - beforeEach(() => { - someMock.mockClear(); - - runInAction(() => { - someObservable.set('some-other-changed-value'); - }); - }); - - it('computes', () => { - expect(someMock).toHaveBeenCalledWith('some-other-changed-value'); - }); - - it('observed value is pending value', async () => { - expect(observedValue).toBe(undefined); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('when the obsolete promise resolves', () => { - beforeEach(async () => { - await someMock.resolveSpecific( - ['some-initial-value'], - 'some-obsolete-promise-result', - ); - }); - - it('still observes value as pending value', async () => { - expect(observedValue).toBe(undefined); - }); - - it('still observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('when the latest promise resolves', () => { - beforeEach(async () => { - await someMock.resolveSpecific( - ['some-other-changed-value'], - 'some-latest-promise-result', - ); - }); - - it('observed value is result of latest promise', async () => { - expect(observedValue).toBe('some-latest-promise-result'); - }); - - it('observes as not pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - }); - }); - }); - }); - - it('when promise resolves as non observable object, computed value is also non observable', async () => { - await someMock.resolve({ some: 'non-observable-object' }); - - expect(isObservableProp(observedValue, 'some')).toBe(false); - }); - - describe('when observed promise resolves', () => { - beforeEach(async () => { - await someMock.resolve('some-promise-result'); - }); - - it('observed value is the result of promise', () => { - expect(observedValue).toBe('some-promise-result'); - }); - - it('is no longer pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - - it('when observed again, still does not recompute', () => { - someMock.mockClear(); - - observe(someAsyncComputed.value, () => {}); - - expect(someMock).not.toHaveBeenCalled(); - }); - - describe('when a change is observed', () => { - beforeEach(() => { - someMock.mockClear(); - - runInAction(() => { - someObservable.set('some-changed-value'); - }); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - it('observed value is pending value', () => { - expect(observedValue).toBe(undefined); - }); - - it('recomputes', () => { - expect(someMock).toHaveBeenCalledWith('some-changed-value'); - }); - }); - - describe('when invalidated', () => { - beforeEach(() => { - someMock.mockClear(); - - someAsyncComputed.invalidate(); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - it('observed value is pending value', () => { - expect(observedValue).toBe(undefined); - }); - - it('recomputes', () => { - expect(someMock).toHaveBeenCalledWith('some-initial-value'); - }); - }); - }); - }); - - it('given observed and unobserved, when observed again, does not recompute', () => { - const unobserve = observe(someAsyncComputed.value, () => {}); - - unobserve(); - - observe(someAsyncComputed.value, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - it('given callback, when observed multiple times, does not recompute', () => { - observe(someAsyncComputed.value, () => {}); - observe(someAsyncComputed.value, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - it('when not observed, does not call callback', () => { - expect(someMock).not.toHaveBeenCalled(); - }); - }); - - describe('given callback to observe something that returns a promise, and showing latest value when between observed updates', () => { - let someMock; - let someAsyncComputed; - let someObservable; - - beforeEach(() => { - someMock = asyncFn(); - - someObservable = observable.box('some-initial-value'); - - someAsyncComputed = asyncComputed({ - getValueFromObservedPromise: () => { - const someObservedValue = someObservable.get(); - - return someMock(someObservedValue); - }, - - valueWhenPending: 'some-pending-value', - - betweenUpdates: 'show-latest-value', - }); - }); - - it('given invalidated before observation, when observed, does not throw', () => { - someAsyncComputed.invalidate(); - - expect(() => { - observe(someAsyncComputed.value, () => {}); - }).not.toThrow(); - }); - - describe('when only status is observed but not value', () => { - beforeEach(() => { - observe(someAsyncComputed.pending, () => {}); - }); - - it('when status is observed, computes', () => { - expect(someMock).toHaveBeenCalled(); - }); - - describe('when observed promise resolves', () => { - beforeEach(async () => { - await someMock.resolve('some-promise-result'); - }); - - it('is no longer pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - - it('when value is observed, observed value is the result of promise', () => { - let observedValue; - - observe( - someAsyncComputed.value, - - change => { - observedValue = change.newValue; - }, - true, - ); - - expect(observedValue).toBe('some-promise-result'); - }); - }); - }); - - it('when status is observed multiple times, computes only once', () => { - observe(someAsyncComputed.pending, () => {}); - observe(someAsyncComputed.pending, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - describe('given value is observed', () => { - let observedValue; - let changeOfValueIsObserved; - - beforeEach(() => { - changeOfValueIsObserved = false; - - observe( - someAsyncComputed.value, - - change => { - observedValue = change.newValue; - changeOfValueIsObserved = true; - }, - true, - ); - }); - - it('computes', () => { - expect(someMock).toHaveBeenCalledWith('some-initial-value'); - }); - - describe('when observed promise has not resolved yet', () => { - it('observed value is undefined', async () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('but another change is observed', () => { - beforeEach(() => { - someMock.mockClear(); - - runInAction(() => { - someObservable.set('some-other-changed-value'); - }); - }); - - it('computes', () => { - expect(someMock).toHaveBeenCalledWith('some-other-changed-value'); - }); - - it('observed value is still the pending value', async () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('when the obsolete promise resolves', () => { - beforeEach(async () => { - await someMock.resolveSpecific( - ['some-initial-value'], - 'some-obsolete-promise-result', - ); - }); - - it('still observes value as pending value', async () => { - expect(observedValue).toBe('some-pending-value'); - }); - - it('still observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - describe('when the latest promise resolves', () => { - beforeEach(async () => { - await someMock.resolveSpecific( - ['some-other-changed-value'], - 'some-latest-promise-result', - ); - }); - - it('observed value is result of latest promise', async () => { - expect(observedValue).toBe('some-latest-promise-result'); - }); - - it('observes as not pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - }); - }); - }); - }); - - it('when promise resolves as non observable object, computed value is also non observable', async () => { - await someMock.resolve({ some: 'non-observable-object' }); - - expect(isObservableProp(observedValue, 'some')).toBe(false); - }); - - describe('when observed promise resolves', () => { - beforeEach(async () => { - await someMock.resolve('some-promise-result'); - }); - - it('observed value is the result of promise', () => { - expect(observedValue).toBe('some-promise-result'); - }); - - it('is no longer pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(false); - }); - - it('when observed again, still does not recompute', () => { - someMock.mockClear(); - - observe(someAsyncComputed.value, () => {}); - - expect(someMock).not.toHaveBeenCalled(); - }); - - describe('when a change is observed', () => { - beforeEach(() => { - someMock.mockClear(); - changeOfValueIsObserved = false; - - runInAction(() => { - someObservable.set('some-changed-value'); - }); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - it('no change of value is observed yet', () => { - expect(changeOfValueIsObserved).toBe(false); - }); - - it('observed value remains the latest value', () => { - expect(observedValue).toBe('some-promise-result'); - }); - - it('recomputes', () => { - expect(someMock).toHaveBeenCalledWith('some-changed-value'); - }); - }); - - describe('when invalidated', () => { - beforeEach(() => { - someMock.mockClear(); - - changeOfValueIsObserved = false; - someAsyncComputed.invalidate(); - }); - - it('observes as pending', () => { - const pendingStatus = getPendingStatus(someAsyncComputed); - - expect(pendingStatus).toBe(true); - }); - - it('no change of value is observed yet', () => { - expect(changeOfValueIsObserved).toBe(false); - }); - - it('observed value yet remains the latest value', () => { - expect(observedValue).toBe('some-promise-result'); - }); - - it('recomputes', () => { - expect(someMock).toHaveBeenCalledWith('some-initial-value'); - }); - }); - }); - }); - - it('given observed and unobserved, when observed again, does not recompute', () => { - const unobserve = observe(someAsyncComputed.value, () => {}); - - unobserve(); - - observe(someAsyncComputed.value, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - it('given callback, when observed multiple times, does not recompute', () => { - observe(someAsyncComputed.value, () => {}); - observe(someAsyncComputed.value, () => {}); - - expect(someMock).toHaveBeenCalledTimes(1); - }); - - it('when not observed, does not call callback', () => { - expect(someMock).not.toHaveBeenCalled(); - }); - }); -}); - -const getPendingStatus = someAsyncComputed => { - let observedPending; - - observe( - someAsyncComputed.pending, - - change => { - observedPending = change.newValue; - }, - true, - ); - - return observedPending; -};