From c2dffd4e8bc299a77417ebb48895c6a7ee01a2c2 Mon Sep 17 00:00:00 2001 From: David Maskasky Date: Thu, 26 Sep 2024 14:01:57 -0700 Subject: [PATCH] trial: jotai-core scope impl --- .../01_basic_spec.test.tsx | 4 +- .../02_removeScope.test.tsx | 4 +- .../03_nested.test.tsx | 4 +- .../04_derived.test.tsx | 4 +- .../05_derived_self.test.tsx | 4 +- .../06_implicit_parent.test.tsx | 4 +- .../07_writable.test.tsx | 4 +- .../08_family.test.tsx | 4 +- .../09_mount.test.tsx | 4 +- __tests__/{ => ScopeProvider2}/utils.ts | 2 +- .../ScopeProvider3/01_basic_spec.test.tsx | 958 ++++++++++++++++++ .../ScopeProvider3/02_removeScope.test.tsx | 129 +++ __tests__/ScopeProvider3/03_nested.test.tsx | 233 +++++ __tests__/ScopeProvider3/04_derived.test.tsx | 499 +++++++++ .../ScopeProvider3/05_derived_self.test.tsx | 53 + .../06_implicit_parent.test.tsx | 104 ++ __tests__/ScopeProvider3/07_writable.test.tsx | 146 +++ __tests__/ScopeProvider3/08_family.test.tsx | 257 +++++ __tests__/ScopeProvider3/09_mount.test.tsx | 73 ++ __tests__/ScopeProvider3/utils.ts | 48 + .../derive/trials/getAtomState-caller.ts | 2 +- .../derive/understanding/atomState.test.ts | 2 +- __tests__/derive/understanding/derive.test.ts | 120 +-- .../derive/understanding/deriveStack.test.ts | 2 +- src/ScopeProvider3/ScopeProvider.tsx | 59 ++ src/ScopeProvider3/scope.ts | 75 ++ src/ScopeProvider3/types.ts | 59 ++ 27 files changed, 2765 insertions(+), 92 deletions(-) rename __tests__/{ScopeProvider => ScopeProvider2}/01_basic_spec.test.tsx (99%) rename __tests__/{ScopeProvider => ScopeProvider2}/02_removeScope.test.tsx (97%) rename __tests__/{ScopeProvider => ScopeProvider2}/03_nested.test.tsx (97%) rename __tests__/{ScopeProvider => ScopeProvider2}/04_derived.test.tsx (99%) rename __tests__/{ScopeProvider => ScopeProvider2}/05_derived_self.test.tsx (93%) rename __tests__/{ScopeProvider => ScopeProvider2}/06_implicit_parent.test.tsx (96%) rename __tests__/{ScopeProvider => ScopeProvider2}/07_writable.test.tsx (97%) rename __tests__/{ScopeProvider => ScopeProvider2}/08_family.test.tsx (98%) rename __tests__/{ScopeProvider => ScopeProvider2}/09_mount.test.tsx (95%) rename __tests__/{ => ScopeProvider2}/utils.ts (96%) create mode 100644 __tests__/ScopeProvider3/01_basic_spec.test.tsx create mode 100644 __tests__/ScopeProvider3/02_removeScope.test.tsx create mode 100644 __tests__/ScopeProvider3/03_nested.test.tsx create mode 100644 __tests__/ScopeProvider3/04_derived.test.tsx create mode 100644 __tests__/ScopeProvider3/05_derived_self.test.tsx create mode 100644 __tests__/ScopeProvider3/06_implicit_parent.test.tsx create mode 100644 __tests__/ScopeProvider3/07_writable.test.tsx create mode 100644 __tests__/ScopeProvider3/08_family.test.tsx create mode 100644 __tests__/ScopeProvider3/09_mount.test.tsx create mode 100644 __tests__/ScopeProvider3/utils.ts create mode 100644 src/ScopeProvider3/ScopeProvider.tsx create mode 100644 src/ScopeProvider3/scope.ts create mode 100644 src/ScopeProvider3/types.ts diff --git a/__tests__/ScopeProvider/01_basic_spec.test.tsx b/__tests__/ScopeProvider2/01_basic_spec.test.tsx similarity index 99% rename from __tests__/ScopeProvider/01_basic_spec.test.tsx rename to __tests__/ScopeProvider2/01_basic_spec.test.tsx index 82cc009..c68bb0a 100644 --- a/__tests__/ScopeProvider/01_basic_spec.test.tsx +++ b/__tests__/ScopeProvider2/01_basic_spec.test.tsx @@ -8,8 +8,8 @@ import { type SetStateAction, } from 'jotai' import { atomWithReducer } from 'jotai/vanilla/utils' -import { ScopeProvider } from '../../src/index' -import { clickButton, getTextContents } from '../utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' +import { clickButton, getTextContents } from './utils' describe('Counter', () => { /* diff --git a/__tests__/ScopeProvider/02_removeScope.test.tsx b/__tests__/ScopeProvider2/02_removeScope.test.tsx similarity index 97% rename from __tests__/ScopeProvider/02_removeScope.test.tsx rename to __tests__/ScopeProvider2/02_removeScope.test.tsx index 0f7c8f1..236fac4 100644 --- a/__tests__/ScopeProvider/02_removeScope.test.tsx +++ b/__tests__/ScopeProvider2/02_removeScope.test.tsx @@ -2,8 +2,8 @@ import { render } from '@testing-library/react' import type { PropsWithChildren } from 'react' import { atom, useAtom, useAtomValue } from 'jotai' import { atomWithReducer } from 'jotai/vanilla/utils' -import { ScopeProvider } from '../../src/index' -import { clickButton, getTextContents } from '../utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' +import { clickButton, getTextContents } from './utils' const baseAtom1 = atomWithReducer(0, (v) => v + 1) const baseAtom2 = atomWithReducer(0, (v) => v + 1) diff --git a/__tests__/ScopeProvider/03_nested.test.tsx b/__tests__/ScopeProvider2/03_nested.test.tsx similarity index 97% rename from __tests__/ScopeProvider/03_nested.test.tsx rename to __tests__/ScopeProvider2/03_nested.test.tsx index 36e8562..6b69c8a 100644 --- a/__tests__/ScopeProvider/03_nested.test.tsx +++ b/__tests__/ScopeProvider2/03_nested.test.tsx @@ -1,9 +1,9 @@ import { render } from '@testing-library/react' import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' import { atomWithReducer } from 'jotai/vanilla/utils' -import { clickButton, getTextContents } from '../utils' +import { clickButton, getTextContents } from './utils' -import { ScopeProvider } from '../../src/index' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' const baseAtom1 = atomWithReducer(0, (v) => v + 1) const baseAtom2 = atomWithReducer(0, (v) => v + 1) diff --git a/__tests__/ScopeProvider/04_derived.test.tsx b/__tests__/ScopeProvider2/04_derived.test.tsx similarity index 99% rename from __tests__/ScopeProvider/04_derived.test.tsx rename to __tests__/ScopeProvider2/04_derived.test.tsx index a728bc2..462e137 100644 --- a/__tests__/ScopeProvider/04_derived.test.tsx +++ b/__tests__/ScopeProvider2/04_derived.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react' import { atom, useAtom } from 'jotai' -import { clickButton, getTextContents } from '../utils' -import { ScopeProvider } from '../../src/index' +import { clickButton, getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' const atomValueSelectors = [ '.case1.base', diff --git a/__tests__/ScopeProvider/05_derived_self.test.tsx b/__tests__/ScopeProvider2/05_derived_self.test.tsx similarity index 93% rename from __tests__/ScopeProvider/05_derived_self.test.tsx rename to __tests__/ScopeProvider2/05_derived_self.test.tsx index 0f8e190..350c47a 100644 --- a/__tests__/ScopeProvider/05_derived_self.test.tsx +++ b/__tests__/ScopeProvider2/05_derived_self.test.tsx @@ -1,8 +1,8 @@ import { render } from '@testing-library/react' import { atom, useAtom } from 'jotai' import { useHydrateAtoms } from 'jotai/utils' -import { getTextContents } from '../utils' -import { ScopeProvider } from '../../src/index' +import { getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' const baseAtom = atom(0) const derivedAtom1 = atom( diff --git a/__tests__/ScopeProvider/06_implicit_parent.test.tsx b/__tests__/ScopeProvider2/06_implicit_parent.test.tsx similarity index 96% rename from __tests__/ScopeProvider/06_implicit_parent.test.tsx rename to __tests__/ScopeProvider2/06_implicit_parent.test.tsx index 95b1a10..c5b546e 100644 --- a/__tests__/ScopeProvider/06_implicit_parent.test.tsx +++ b/__tests__/ScopeProvider2/06_implicit_parent.test.tsx @@ -2,8 +2,8 @@ import type { FC } from 'react' import { render } from '@testing-library/react' import { atom, useAtom, useAtomValue } from 'jotai' import { atomWithReducer } from 'jotai/vanilla/utils' -import { clickButton, getTextContents } from '../utils' -import { ScopeProvider } from '../../src/index' +import { clickButton, getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' function renderWithOrder(level1: 'BD' | 'DB', level2: 'BD' | 'DB') { const baseAtom = atomWithReducer(0, (v) => v + 1) diff --git a/__tests__/ScopeProvider/07_writable.test.tsx b/__tests__/ScopeProvider2/07_writable.test.tsx similarity index 97% rename from __tests__/ScopeProvider/07_writable.test.tsx rename to __tests__/ScopeProvider2/07_writable.test.tsx index b1e4c9f..77c0b8c 100644 --- a/__tests__/ScopeProvider/07_writable.test.tsx +++ b/__tests__/ScopeProvider2/07_writable.test.tsx @@ -1,7 +1,7 @@ import { render } from '@testing-library/react' import { type WritableAtom, type PrimitiveAtom, atom, useAtom } from 'jotai' -import { clickButton, getTextContents } from '../utils' -import { ScopeProvider } from '../../src/index' +import { clickButton, getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' let baseAtom: PrimitiveAtom diff --git a/__tests__/ScopeProvider/08_family.test.tsx b/__tests__/ScopeProvider2/08_family.test.tsx similarity index 98% rename from __tests__/ScopeProvider/08_family.test.tsx rename to __tests__/ScopeProvider2/08_family.test.tsx index 67b5bcd..7c7a556 100644 --- a/__tests__/ScopeProvider/08_family.test.tsx +++ b/__tests__/ScopeProvider2/08_family.test.tsx @@ -1,8 +1,8 @@ import { render, act } from '@testing-library/react' import { useAtom, atom, useSetAtom } from 'jotai' import { atomFamily, atomWithReducer } from 'jotai/utils' -import { ScopeProvider } from '../../src/index' -import { clickButton, getTextContents } from '../utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' +import { clickButton, getTextContents } from './utils' describe('AtomFamily with ScopeProvider', () => { /* diff --git a/__tests__/ScopeProvider/09_mount.test.tsx b/__tests__/ScopeProvider2/09_mount.test.tsx similarity index 95% rename from __tests__/ScopeProvider/09_mount.test.tsx rename to __tests__/ScopeProvider2/09_mount.test.tsx index 61a3cc8..45bf843 100644 --- a/__tests__/ScopeProvider/09_mount.test.tsx +++ b/__tests__/ScopeProvider2/09_mount.test.tsx @@ -1,8 +1,8 @@ import { useState } from 'react' import { render, act } from '@testing-library/react' import { atom, useAtomValue } from 'jotai' -import { ScopeProvider } from '../../src/index' -import { clickButton } from '../utils' +import { ScopeProvider } from 'src/ScopeProvider2/ScopeProvider' +import { clickButton } from './utils' describe('ScopeProvider', () => { it('mounts and unmounts successfully', () => { diff --git a/__tests__/utils.ts b/__tests__/ScopeProvider2/utils.ts similarity index 96% rename from __tests__/utils.ts rename to __tests__/ScopeProvider2/utils.ts index 7f72863..9f219e8 100644 --- a/__tests__/utils.ts +++ b/__tests__/ScopeProvider2/utils.ts @@ -1,5 +1,5 @@ import { fireEvent } from '@testing-library/react' -import type { Store } from 'src/ScopeProvider2/types' +import type { Store } from 'src/ScopeProvider3/types' function getElements(container: HTMLElement, querySelectors: string[]): Element[] { return querySelectors.map((querySelector) => { diff --git a/__tests__/ScopeProvider3/01_basic_spec.test.tsx b/__tests__/ScopeProvider3/01_basic_spec.test.tsx new file mode 100644 index 0000000..ef1e34d --- /dev/null +++ b/__tests__/ScopeProvider3/01_basic_spec.test.tsx @@ -0,0 +1,958 @@ +import { render } from '@testing-library/react' +import { + useAtom, + useSetAtom, + useAtomValue, + atom, + type WritableAtom, + type SetStateAction, +} from 'jotai' +import { atomWithReducer } from 'jotai/vanilla/utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' +import { clickButton, getTextContents } from './utils' + +describe('Counter', () => { + /* + base + S0[]: base0 + S1[]: base0 + */ + test('01. ScopeProvider does not provide isolation for unscoped primitive atoms', () => { + const baseAtom = atom(0) + baseAtom.debugLabel = 'base' + function Counter({ level }: { level: string }) { + const [base, increaseBase] = useAtom(baseAtom) + return ( +
+ base:{base} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setBase' + const increaseScopedBase = '.level1.setBase' + const atomValueSelectors = ['.level0.base', '.level1.base'] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level0 base + '0', // level1 base + ]) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '1', // level1 base + ]) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', // level0 base + '2', // level1 base + ]) + }) + + /* + base, Derived(base) + S0[]: base0 Derived0(base0) + S1[]: base0 Derived0(base0) + */ + test('02. unscoped derived atoms are unaffected in ScopeProvider', () => { + const baseAtom = atom(0) + const derivedAtom = atom( + (get) => get(baseAtom), + (_get, set, value: SetStateAction) => set(baseAtom, value), + ) + baseAtom.debugLabel = 'base' + function Counter({ level }: { level: string }) { + const [derived, setDerived] = useAtom(derivedAtom) + const increaseDerived = () => setDerived((c) => c + 1) + return ( +
+ base:{derived} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setDerived' + const increaseScopedBase = '.level1.setDerived' + const atomValueSelectors = ['.level0.derived', '.level1.derived'] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level 0 derived + '0', // level 1 derived + ]) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level 0 derived + '1', // level 1 derived + ]) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', // level 0 derived + '2', // level 1 derived + ]) + }) + + /* + base + S0[base]: base0 + S1[base]: base1 + */ + test('03. ScopeProvider provides isolation for scoped primitive atoms', () => { + const baseAtom = atom(0) + baseAtom.debugLabel = 'base' + function Counter({ level }: { level: string }) { + const [base, increaseBase] = useAtom(baseAtom) + return ( +
+ base:{base} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setBase' + const increaseScopedBase = '.level1.setBase' + const atomValueSelectors = ['.level0.base', '.level1.base'] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level0 base + '0', // level1 base + ]) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '0', // level1 base + ]) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '1', // level1 base + ]) + }) + + /* + base, derived(base) + S0[base]: derived0(base0) + S1[base]: derived0(base1) + */ + test('04. unscoped derived can read and write to scoped primitive atoms', () => { + const baseAtom = atom(0) + baseAtom.debugLabel = 'base' + const derivedAtom = atom( + (get) => get(baseAtom), + (get, set) => set(baseAtom, get(baseAtom) + 1), + ) + derivedAtom.debugLabel = 'derived' + + function Counter({ level }: { level: string }) { + const [derived, increaseFromDerived] = useAtom(derivedAtom) + const value = useAtomValue(baseAtom) + return ( +
+ base:{derived} + value:{value} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setBase' + const increaseScopedBase = '.level1.setBase' + const atomValueSelectors = ['.level0.base', '.level0.value', '.level1.base', '.level1.value'] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level0 base + '0', // level0 value + '0', // level1 base + '0', // level1 value + ]) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '1', // level0 value + '0', // level1 base + '0', // level1 value + ]) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '1', // level0 value + '1', // level1 base + '1', // level1 value + ]) + }) + + /* + base, notScoped, derived(base + notScoped) + S0[base]: derived0(base0 + notScoped0) + S1[base]: derived0(base1 + notScoped0) + */ + test('05. unscoped derived can read both scoped and unscoped atoms', () => { + const baseAtom = atomWithReducer(0, (v) => v + 1) + baseAtom.debugLabel = 'base' + const notScopedAtom = atomWithReducer(0, (v) => v + 1) + notScopedAtom.debugLabel = 'notScoped' + const derivedAtom = atom((get) => ({ + base: get(baseAtom), + notScoped: get(notScopedAtom), + })) + derivedAtom.debugLabel = 'derived' + + function Counter({ level }: { level: string }) { + const increaseBase = useSetAtom(baseAtom) + const derived = useAtomValue(derivedAtom) + return ( +
+ base:{derived.base} + not scoped: + {derived.notScoped} + +
+ ) + } + + function IncreaseUnscoped() { + const increaseNotScoped = useSetAtom(notScopedAtom) + return ( + + ) + } + + function App() { + return ( +
+

Unscoped

+ + +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setBase' + const increaseScopedBase = '.level1.setBase' + const increaseNotScoped = '.increaseNotScoped' + const atomValueSelectors = ['.level0.base', '.level1.base', '.level1.notScoped'] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level0 base + '0', // level1 base + '0', // level1 notScoped + ]) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '0', // level1 base + '0', // level1 notScoped + ]) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '1', // level1 base + '0', // level1 notScoped + ]) + + clickButton(container, increaseNotScoped) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 base + '1', // level1 base + '1', // level1 notScoped + ]) + }) + + /* + base, derived(base), + S0[derived]: derived0(base0) + S1[derived]: derived1(base1) + */ + test('06. dependencies of scoped derived are implicitly scoped', () => { + const baseAtom = atomWithReducer(0, (v) => v + 1) + baseAtom.debugLabel = 'base' + + const derivedAtom = atom( + (get) => get(baseAtom), + (_get, set) => set(baseAtom), + ) + derivedAtom.debugLabel = 'derived' + + function Counter({ level }: { level: string }) { + const increaseBase = useSetAtom(baseAtom) + const [derived, setDerived] = useAtom(derivedAtom) + return ( +
+ base:{derived} + + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setBase' + const increaseScopedBase = '.level1.setBase' + const increaseScopedDerived = '.level1.setDerived' + const atomValueSelectors = ['.level0.base', '.level1.base'] + + expect(getTextContents(container, atomValueSelectors)).toEqual(['0', '0']) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual(['1', '0']) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual(['2', '0']) + + clickButton(container, increaseScopedDerived) + expect(getTextContents(container, atomValueSelectors)).toEqual(['2', '1']) + }) + + /* + base, derivedA(base), derivedB(base) + S0[derivedA, derivedB]: derivedA0(base0), derivedB0(base0) + S1[derivedA, derivedB]: derivedA1(base1), derivedB1(base1) + */ + test('07. scoped derived atoms can share implicitly scoped dependencies', () => { + const baseAtom = atomWithReducer(0, (v) => v + 1) + baseAtom.debugLabel = 'base' + const derivedAtomA = atom( + (get) => get(baseAtom), + (_get, set) => set(baseAtom), + ) + derivedAtomA.debugLabel = 'derivedAtomA' + const derivedAtomB = atom( + (get) => get(baseAtom), + (_get, set) => set(baseAtom), + ) + derivedAtomB.debugLabel = 'derivedAtomB' + + function Counter({ level }: { level: string }) { + const setBase = useSetAtom(baseAtom) + const [derivedA, setDerivedA] = useAtom(derivedAtomA) + const [derivedB, setDerivedB] = useAtom(derivedAtomB) + return ( +
+ base:{derivedA} + derivedA: + {derivedA} + derivedB: + {derivedB} + + + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + +
+ ) + } + const { container } = render() + const increaseLevel0Base = '.level0.setBase' + const increaseLevel1Base = '.level1.setBase' + const increaseLevel1DerivedA = '.level1.setDerivedA' + const increaseLevel1DerivedB = '.level1.setDerivedB' + const atomValueSelectors = ['.level0.derivedA', '.level1.derivedA', '.level1.derivedB'] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level0 derivedA + '0', // level1 derivedA + '0', // level1 derivedB + ]) + + clickButton(container, increaseLevel0Base) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level0 derivedA + '0', // level1 derivedA + '0', // level1 derivedB + ]) + + clickButton(container, increaseLevel1Base) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', // level0 derivedA + '0', // level1 derivedA + '0', // level1 derivedB + ]) + + clickButton(container, increaseLevel1DerivedA) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', // level0 derivedA + '1', // level1 derivedA + '1', // level1 derivedB + ]) + + clickButton(container, increaseLevel1DerivedB) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', // level0 derivedA + '2', // level1 derivedA + '2', // level1 derivedB + ]) + }) + + /* + base, derivedA(base), derivedB(base) + S0[base]: base0 + S1[base]: base1 + S2[base]: base2 + S3[base]: base3 + */ + test('08. nested scopes provide isolation for primitive atoms at every level', () => { + const baseAtom = atomWithReducer(0, (v) => v + 1) + + function Counter({ level }: { level: string }) { + const [base, increaseBase] = useAtom(baseAtom) + return ( +
+ base:{base} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + + + + +
+ ) + } + const { container } = render() + const increaseUnscopedBase = '.level0.setBase' + const increaseScopedBase = '.level1.setBase' + const increaseDoubleScopedBase = '.level2.setBase' + const atomValueSelectors = ['.level0.base', '.level1.base', '.level2.base'] + + expect(getTextContents(container, atomValueSelectors)).toEqual(['0', '0', '0']) + + clickButton(container, increaseUnscopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual(['1', '0', '0']) + + clickButton(container, increaseScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual(['1', '1', '0']) + + clickButton(container, increaseDoubleScopedBase) + expect(getTextContents(container, atomValueSelectors)).toEqual(['1', '1', '1']) + }) + + /* + baseA, baseB, baseC, derived(baseA + baseB + baseC) + S0[ ]: derived(baseA0 + baseB0 + baseC0) + S1[baseB]: derived(baseA0 + baseB1 + baseC0) + S2[baseC]: derived(baseA0 + baseB1 + baseC2) + */ + test('09. unscoped derived atoms in nested scoped can read and write to scoped primitive atoms at every level', async () => { + function clickButtonGetResults(buttonSelector: string) { + const baseAAtom = atom(0) + baseAAtom.debugLabel = 'baseA' + const baseBAtom = atom(0) + baseBAtom.debugLabel = 'baseB' + const baseCAtom = atom(0) + baseCAtom.debugLabel = 'baseC' + const derivedAtom = atom( + (get) => ({ + baseA: get(baseAAtom), + baseB: get(baseBAtom), + baseC: get(baseCAtom), + }), + (get, set) => { + set(baseAAtom, get(baseAAtom) + 1) + set(baseBAtom, get(baseBAtom) + 1) + set(baseCAtom, get(baseCAtom) + 1) + }, + ) + derivedAtom.debugLabel = 'derived' + + function Counter({ + level, + baseAtom, + }: { + level: string + baseAtom: WritableAtom], void> + }) { + const setBase = useSetAtom(baseAtom) + const [{ baseA, baseB, baseC }, increaseAll] = useAtom(derivedAtom) + const valueA = useAtomValue(baseAAtom) + const valueB = useAtomValue(baseBAtom) + const valueC = useAtomValue(baseCAtom) + return ( +
+ baseA:{baseA} + baseB:{baseB} + baseC:{baseC} + valueA:{valueA} + valueB:{valueB} + valueC:{valueC} + + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + + + + +
+ ) + } + const { container } = render() + expectAllZeroes(container) + clickButton(container, buttonSelector) + return getTextContents(container, atomValueSelectors).join('') + } + function expectAllZeroes(container: HTMLElement) { + expect(getTextContents(container, atomValueSelectors).join('')).toEqual( + [ + // level0 + '0', // baseA0 + '0', // baseB0 + '0', // baseC0 + '0', // valueA0 + '0', // valueB0 + '0', // valueC0 + // level1 + '0', // baseA0 + '0', // baseB1 + '0', // baseC0 + '0', // valueA0 + '0', // valueB1 + '0', // valueC0 + // level2 + '0', // baseA0 + '0', // baseB1 + '0', // baseC2 + '0', // valueA0 + '0', // valueB1 + '0', // valueC2 + ].join(''), + ) + } + const atomValueSelectors = [ + '.level0.baseA', + '.level0.baseB', + '.level0.baseC', + '.level0.valueA', + '.level0.valueB', + '.level0.valueC', + '.level1.baseA', + '.level1.baseB', + '.level1.baseC', + '.level1.valueA', + '.level1.valueB', + '.level1.valueC', + '.level2.baseA', + '.level2.baseB', + '.level2.baseC', + '.level2.valueA', + '.level2.valueB', + '.level2.valueC', + ] + const increaseLevel0BaseA = '.level0.increaseBase' + const increaseLevel1BaseB = '.level1.increaseBase' + const increaseLevel2BaseC = '.level2.increaseBase' + const increaseLevel0All = '.level0.increaseAll' + const increaseLevel1All = '.level1.increaseAll' + const increaseLevel2All = '.level2.increaseAll' + + expect(clickButtonGetResults(increaseLevel0BaseA)).toEqual( + [ + // level0 + '1', // baseA0 + '0', // baseB0 + '0', // baseC0 + '1', // valueA0 + '0', // valueB0 + '0', // valueC0 + // level1 + '1', // baseA0 + '0', // baseB1 + '0', // baseC0 + '1', // valueA0 + '0', // valueB1 + '0', // valueC0 + // level2 + '1', // baseA0 + '0', // baseB1 + '0', // baseC2 + '1', // valueA0 + '0', // valueB1 + '0', // valueC2 + ].join(''), + ) + + expect(clickButtonGetResults(increaseLevel1BaseB)).toEqual( + [ + // level0 + '0', // baseA0 + '0', // baseB0 + '0', // baseC0 + '0', // valueA0 + '0', // valueB0 + '0', // valueC0 + // level1 + '0', // baseA0 + '1', // baseB1 + '0', // baseC0 + '0', // valueA0 + '1', // valueB1 + '0', // valueC0 + // level2 + '0', // baseA0 + '1', // baseB1 + '0', // baseC2 + '0', // valueA0 + '1', // valueB1 + '0', // valueC2 + ].join(''), + ) + + expect(clickButtonGetResults(increaseLevel2BaseC)).toEqual( + [ + // level0 + '0', // baseA0 + '0', // baseB0 + '0', // baseC0 + '0', // valueA0 + '0', // valueB0 + '0', // valueC0 + // level1 + '0', // baseA0 + '0', // baseB1 + '0', // baseC0 + '0', // valueA0 + '0', // valueB1 + '0', // valueC0 + // level2 + '0', // baseA0 + '0', // baseB1 + '1', // baseC2 + '0', // valueA0 + '0', // valueB1 + '1', // valueC2 + ].join(''), + ) + + expect(clickButtonGetResults(increaseLevel0All)).toEqual( + [ + // level0 + '1', // baseA0 + '1', // baseB0 + '1', // baseC0 + '1', // valueA0 + '1', // valueB0 + '1', // valueC0 + // level1 + '1', // baseA0 + '0', // baseB1 + '1', // baseC0 + '1', // valueA0 + '0', // valueB1 + '1', // valueC0 + // level2 + '1', // baseA0 + '0', // baseB1 + '0', // baseC2 + '1', // valueA0 + '0', // valueB1 + '0', // valueC2 + ].join(''), + ) + + expect(clickButtonGetResults(increaseLevel1All)).toEqual( + [ + // level0 + '1', // baseA0 + '0', // baseB0 + '1', // baseC0 + '1', // valueA0 + '0', // valueB0 + '1', // valueC0 + // level1 + '1', // baseA0 + '1', // baseB1 + '1', // baseC0 + '1', // valueA0 + '1', // valueB1 + '1', // valueC0 + // level2 + '1', // baseA0 + '1', // baseB1 + '0', // baseC2 + '1', // valueA0 + '1', // valueB1 + '0', // valueC2 + ].join(''), + ) + + expect(clickButtonGetResults(increaseLevel2All)).toEqual( + [ + // level0 + '1', // baseA0 + '0', // baseB0 + '0', // baseC0 + '1', // valueA0 + '0', // valueB0 + '0', // valueC0 + // level1 + '1', // baseA0 + '1', // baseB1 + '0', // baseC0 + '1', // valueA0 + '1', // valueB1 + '0', // valueC0 + // level2 + '1', // baseA0 + '1', // baseB1 + '1', // baseC2 + '1', // valueA0 + '1', // valueB1 + '1', // valueC2 + ].join(''), + ) + }) + + /* + baseA, baseB, derived(baseA + baseB) + S1[baseB, derived]: derived1(baseA1 + baseB1) + S2[baseB]: derived1(baseA1 + baseB2) + */ + test('10. inherited scoped derived atoms can read and write to scoped primitive atoms at every nested level', () => { + const baseAAtom = atomWithReducer(0, (v) => v + 1) + baseAAtom.debugLabel = 'baseA' + + const baseBAtom = atomWithReducer(0, (v) => v + 1) + baseBAtom.debugLabel = 'baseB' + + const derivedAtom = atom( + (get) => ({ + baseA: get(baseAAtom), + baseB: get(baseBAtom), + }), + (_get, set) => { + set(baseAAtom) + set(baseBAtom) + }, + ) + derivedAtom.debugLabel = 'derived' + + function Counter({ level }: { level: string }) { + const [{ baseA, baseB }, increaseAll] = useAtom(derivedAtom) + return ( +
+ baseA:{baseA} + baseB:{baseB} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+

Scoped Provider

+ + + + + + +
+ ) + } + const { container } = render() + + const increaseLevel1All = '.level1.increaseAll' + const increaseLevel2All = '.level2.increaseAll' + const atomValueSelectors = ['.level1.baseA', '.level1.baseB', '.level2.baseA', '.level2.baseB'] + + /* + baseA, baseB, derived(baseA + baseB) + S1[baseB, derived]: derived1(baseA1 + baseB1) + S2[baseB]: derived1(baseA1 + baseB2) + */ + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', // level1 baseA1 + '0', // level1 baseB1 + '0', // level2 baseA1 + '0', // level2 baseB2 + ]) + + /* + baseA, baseB, derived(baseA + baseB) + S1[baseB, derived]: derived1(baseA1 + baseB1) + S2[baseB]: derived1(baseA1 + baseB2) + */ + clickButton(container, increaseLevel1All) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', // level1 baseA1 + '1', // level1 baseB1 + '1', // level2 baseA1 + '0', // level2 baseB2 + ]) + + /* + baseA, baseB, derived(baseA + baseB) + S1[baseB, derived]: derived1(baseA1 + baseB1) + S2[baseB]: derived1(baseA1 + baseB2) + */ + clickButton(container, increaseLevel2All) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', // level1 baseA1 + '1', // level1 baseB1 + '2', // level2 baseA1 + '1', // level2 baseB2 + ]) + }) +}) diff --git a/__tests__/ScopeProvider3/02_removeScope.test.tsx b/__tests__/ScopeProvider3/02_removeScope.test.tsx new file mode 100644 index 0000000..8ba1512 --- /dev/null +++ b/__tests__/ScopeProvider3/02_removeScope.test.tsx @@ -0,0 +1,129 @@ +import { render } from '@testing-library/react' +import type { PropsWithChildren } from 'react' +import { atom, useAtom, useAtomValue } from 'jotai' +import { atomWithReducer } from 'jotai/vanilla/utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' +import { clickButton, getTextContents } from './utils' + +const baseAtom1 = atomWithReducer(0, (v) => v + 1) +const baseAtom2 = atomWithReducer(0, (v) => v + 1) +const shouldHaveScopeAtom = atom(true) + +function Counter({ counterClass }: { counterClass: string }) { + const [base1, increaseBase1] = useAtom(baseAtom1) + const [base2, increaseBase2] = useAtom(baseAtom2) + return ( + <> +
+ base1: {base1} + +
+
+ base2: {base2} + +
+ + ) +} + +function Wrapper({ children }: PropsWithChildren) { + const shouldHaveScope = useAtomValue(shouldHaveScopeAtom) + return shouldHaveScope ? {children} : children +} + +function ScopeButton() { + const [shouldHaveScope, setShouldHaveScope] = useAtom(shouldHaveScopeAtom) + return ( + + ) +} + +function App() { + return ( +
+

Unscoped

+ +

Scoped Provider

+ + + + +
+ ) +} + +describe('Counter', () => { + test('atom get correct value when ScopeProvider is added/removed', () => { + const { container } = render() + const increaseUnscopedBase1 = '.unscoped.setBase1' + const increaseUnscopedBase2 = '.unscoped.setBase2' + const increaseScopedBase1 = '.scoped.setBase1' + const increaseScopedBase2 = '.scoped.setBase2' + const toggleScope = '#toggleScope' + + const atomValueSelectors = [ + '.unscoped.base1', + '.unscoped.base2', + '.scoped.base1', + '.scoped.base2', + ] + + expect(getTextContents(container, atomValueSelectors)).toEqual(['0', '0', '0', '0']) + + clickButton(container, increaseUnscopedBase1) + expect(getTextContents(container, atomValueSelectors)).toEqual(['1', '0', '1', '0']) + + clickButton(container, increaseUnscopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual(['1', '1', '1', '0']) + + clickButton(container, increaseScopedBase1) + expect(getTextContents(container, atomValueSelectors)).toEqual(['2', '1', '2', '0']) + + clickButton(container, increaseScopedBase2) + clickButton(container, increaseScopedBase2) + clickButton(container, increaseScopedBase2) + + expect(getTextContents(container, atomValueSelectors)).toEqual(['2', '1', '2', '3']) + + clickButton(container, toggleScope) + expect(getTextContents(container, atomValueSelectors)).toEqual(['2', '1', '2', '1']) + + clickButton(container, increaseUnscopedBase1) + expect(getTextContents(container, atomValueSelectors)).toEqual(['3', '1', '3', '1']) + + clickButton(container, increaseUnscopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual(['3', '2', '3', '2']) + + clickButton(container, increaseScopedBase1) + expect(getTextContents(container, atomValueSelectors)).toEqual(['4', '2', '4', '2']) + + clickButton(container, increaseScopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual(['4', '3', '4', '3']) + + clickButton(container, toggleScope) + expect(getTextContents(container, atomValueSelectors)).toEqual(['4', '3', '4', '0']) + + clickButton(container, increaseScopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual(['4', '3', '4', '1']) + + clickButton(container, increaseScopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual(['4', '3', '4', '2']) + + clickButton(container, increaseScopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual(['4', '3', '4', '3']) + }) +}) diff --git a/__tests__/ScopeProvider3/03_nested.test.tsx b/__tests__/ScopeProvider3/03_nested.test.tsx new file mode 100644 index 0000000..9110c6b --- /dev/null +++ b/__tests__/ScopeProvider3/03_nested.test.tsx @@ -0,0 +1,233 @@ +import { render } from '@testing-library/react' +import { atom, useAtom, useAtomValue, useSetAtom } from 'jotai' +import { atomWithReducer } from 'jotai/vanilla/utils' +import { clickButton, getTextContents } from './utils' + +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' + +const baseAtom1 = atomWithReducer(0, (v) => v + 1) +const baseAtom2 = atomWithReducer(0, (v) => v + 1) +const baseAtom = atom(0) + +const writeProxyAtom = atom('unused', (get, set) => { + set(baseAtom, get(baseAtom) + 1) + set(baseAtom1) + set(baseAtom2) +}) + +function Counter({ counterClass }: { counterClass: string }) { + const [base1, increaseBase1] = useAtom(baseAtom1) + const [base2, increaseBase2] = useAtom(baseAtom2) + const base = useAtomValue(baseAtom) + const increaseAll = useSetAtom(writeProxyAtom) + return ( + <> +
+ base1: {base1} + +
+
+ base2: {base2} + +
+
+ base: {base} +
+ + + ) +} + +function App() { + return ( +
+

Unscoped

+ +

Layer 1: Scope base 1

+

base 2 and base should be globally shared

+ + +

Layer 2: Scope base 2

+

base 1 should be shared between layer 1 and layer 2, base should be globally shared

+ + + +
+
+ ) +} + +describe('Counter', () => { + /* + baseA, baseB, baseC + S1[baseA]: baseA1 baseB0 baseC0 + S2[baseB]: baseA1 baseB2 baseC0 + */ + test('nested primitive atoms are correctly scoped', () => { + const { container } = render() + const increaseUnscopedBase1 = '.unscoped.setBase1' + const increaseUnscopedBase2 = '.unscoped.setBase2' + const increaseAllUnscoped = '.unscoped.setAll' + const increaseLayer1Base1 = '.layer1.setBase1' + const increaseLayer1Base2 = '.layer1.setBase2' + const increaseAllLayer1 = '.layer1.setAll' + const increaseLayer2Base1 = '.layer2.setBase1' + const increaseLayer2Base2 = '.layer2.setBase2' + const increaseAllLayer2 = '.layer2.setAll' + + const atomValueSelectors = [ + '.unscoped.base1', + '.unscoped.base2', + '.unscoped.base', + '.layer1.base1', + '.layer1.base2', + '.layer1.base', + '.layer2.base1', + '.layer2.base2', + '.layer2.base', + ] + + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + ]) + + clickButton(container, increaseUnscopedBase1) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + '0', + ]) + + clickButton(container, increaseUnscopedBase2) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '1', + '1', + '0', + '0', + '1', + '0', + '0', + '0', + '0', + ]) + + clickButton(container, increaseAllUnscoped) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '2', + '1', + '0', + '2', + '1', + '0', + '0', + '1', + ]) + + clickButton(container, increaseLayer1Base1) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '2', + '1', + '1', + '2', + '1', + '1', + '0', + '1', + ]) + + clickButton(container, increaseLayer1Base2) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '3', + '1', + '1', + '3', + '1', + '1', + '0', + '1', + ]) + + clickButton(container, increaseAllLayer1) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '4', + '2', + '2', + '4', + '2', + '2', + '0', + '2', + ]) + + clickButton(container, increaseLayer2Base1) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '4', + '2', + '3', + '4', + '2', + '3', + '0', + '2', + ]) + + clickButton(container, increaseLayer2Base2) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '4', + '2', + '3', + '4', + '2', + '3', + '1', + '2', + ]) + + clickButton(container, increaseAllLayer2) + expect(getTextContents(container, atomValueSelectors)).toEqual([ + '2', + '4', + '3', + '4', + '4', + '3', + '4', + '2', + '3', + ]) + }) +}) diff --git a/__tests__/ScopeProvider3/04_derived.test.tsx b/__tests__/ScopeProvider3/04_derived.test.tsx new file mode 100644 index 0000000..fa25fd9 --- /dev/null +++ b/__tests__/ScopeProvider3/04_derived.test.tsx @@ -0,0 +1,499 @@ +import { render } from '@testing-library/react' +import { atom, useAtom } from 'jotai' +import { clickButton, getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' + +const atomValueSelectors = [ + '.case1.base', + '.case1.derivedA', + '.case1.derivedB', + '.case2.base', + '.case2.derivedA', + '.case2.derivedB', + '.layer1.base', + '.layer1.derivedA', + '.layer1.derivedB', + '.layer2.base', + '.layer2.derivedA', + '.layer2.derivedB', +] + +function clickButtonGetResults(buttonSelector: string) { + const baseAtom = atom(0) + const derivedAtomA = atom( + (get) => get(baseAtom), + (get, set) => { + set(baseAtom, get(baseAtom) + 1) + }, + ) + + const derivedAtomB = atom( + (get) => get(baseAtom), + (get, set) => { + set(baseAtom, get(baseAtom) + 1) + }, + ) + + function Counter({ counterClass }: { counterClass: string }) { + const [base, setBase] = useAtom(baseAtom) + const [derivedA, setDerivedA] = useAtom(derivedAtomA) + const [derivedB, setDerivedB] = useAtom(derivedAtomB) + return ( + <> +
+ base:{base} + +
+
+ derivedA: + {derivedA} + +
+
+ derivedB: + {derivedB} + +
+ + ) + } + + function App() { + return ( +
+

Only base is scoped

+

derivedA and derivedB should also be scoped

+ + + +

Both derivedA an derivedB are scoped

+

base should be global, derivedA and derivedB are shared

+ + + +

Layer1: Only derivedA is scoped

+

base and derivedB should be global

+ + +

Layer2: Base and derivedB are scoped

+

derivedA should use layer2's atom, base and derivedB are layer 2 scoped

+ + + +
+
+ ) + } + + const { container } = render() + expectAllZeroes(container) + clickButton(container, buttonSelector) + return getTextContents(container, atomValueSelectors) +} + +function expectAllZeroes(container: HTMLElement) { + expect(getTextContents(container, atomValueSelectors)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) +} + +describe('Counter', () => { + test("parent scope's derived atom is prior to nested scope's scoped base", () => { + const increaseCase1Base = '.case1.setBase' + const increaseCase1DerivedA = '.case1.setDerivedA' + const increaseCase1DerivedB = '.case1.setDerivedB' + const increaseCase2Base = '.case2.setBase' + const increaseCase2DerivedA = '.case2.setDerivedA' + const increaseCase2DerivedB = '.case2.setDerivedB' + const increaseLayer1Base = '.layer1.setBase' + const increaseLayer1DerivedA = '.layer1.setDerivedA' + const increaseLayer1DerivedB = '.layer1.setDerivedB' + const increaseLayer2Base = '.layer2.setBase' + const increaseLayer2DerivedA = '.layer2.setDerivedA' + const increaseLayer2DerivedB = '.layer2.setDerivedB' + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseCase1Base)).toEqual([ + // case 1 + '1', // base + '1', // derivedA + '1', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseCase1DerivedA)).toEqual([ + // case 1 + '1', // base + '1', // derivedA + '1', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseCase1DerivedB)).toEqual([ + // case 1 + '1', // base + '1', // derivedA + '1', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseCase2Base)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '1', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '1', // base + '0', // derivedA + '1', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseCase2DerivedA)).toEqual([ + // case 1: case 1 + '0', // base actual: 0, + '0', // derivedA actual: 0, + '0', // derivedB actual: 0, + + // case 2 + '0', // base actual: 1, + '1', // derivedA actual: 1, + '1', // derivedB actual: 1, + + // layer 1 + '0', // base actual: 1, + '0', // derivedA actual: 0, + '0', // derivedB actual: 1, + + // layer 2 + '0', // base actual: 0, + '0', // derivedA actual: 0, + '0', // derivedB actual: 0 + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseCase2DerivedB)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '0', // base + '1', // derivedA + '1', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseLayer1Base)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '1', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '1', // base + '0', // derivedA + '1', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseLayer1DerivedA)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '1', // derivedA + '0', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseLayer1DerivedB)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '1', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '1', // base + '0', // derivedA + '1', // derivedB + + // layer 2 + '0', // base + '0', // derivedA + '0', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseLayer2Base)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '1', // base + '1', // derivedA + '1', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseLayer2DerivedA)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '1', // base + '1', // derivedA + '1', // derivedB + ]) + + /* + base, derivedA(base), derivedB(base) + case1[base]: base1, derivedA0(base1), derivedB0(base1) + case2[derivedA, derivedB]: base0, derivedA1(base1), derivedB1(base1) + layer1[derivedA]: base0, derivedA1(base1), derivedB0(base0) + layer2[base, derivedB]: base2, derivedA1(base2), derivedB2(base2) + */ + expect(clickButtonGetResults(increaseLayer2DerivedB)).toEqual([ + // case 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // case 2 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 1 + '0', // base + '0', // derivedA + '0', // derivedB + + // layer 2 + '1', // base + '1', // derivedA + '1', // derivedB + ]) + }) +}) diff --git a/__tests__/ScopeProvider3/05_derived_self.test.tsx b/__tests__/ScopeProvider3/05_derived_self.test.tsx new file mode 100644 index 0000000..77a319f --- /dev/null +++ b/__tests__/ScopeProvider3/05_derived_self.test.tsx @@ -0,0 +1,53 @@ +import { render } from '@testing-library/react' +import { atom, useAtom } from 'jotai' +import { useHydrateAtoms } from 'jotai/utils' +import { getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' + +const baseAtom = atom(0) +const derivedAtom1 = atom( + (get) => get(baseAtom), + (get): number => { + return get(derivedAtom1) + }, +) + +function Component({ className, initialValue = 0 }: { className: string; initialValue?: number }) { + useHydrateAtoms([[baseAtom, initialValue]]) + const [atom1ReadValue, setAtom1Value] = useAtom(derivedAtom1) + const atom1WriteValue = setAtom1Value() + return ( +
+ {atom1ReadValue} + {atom1WriteValue} +
+ ) +} + +function App() { + return ( + <> +

base component

+

derived1 should read itself from global scope

+ + +

scoped component

+

derived1 should read itself from scoped scope

+ +
+ + ) +} + +describe('Self', () => { + /* + baseA, derivedB(baseA, derivedB) + S1[baseA]: baseA1, derivedB0(baseA1, derivedB0) + */ + test('derived dep scope is preserved in self reference', () => { + const { container } = render() + expect(getTextContents(container, ['.unscoped .read', '.unscoped .write'])).toEqual(['0', '0']) + + expect(getTextContents(container, ['.scoped .read', '.scoped .write'])).toEqual(['1', '1']) + }) +}) diff --git a/__tests__/ScopeProvider3/06_implicit_parent.test.tsx b/__tests__/ScopeProvider3/06_implicit_parent.test.tsx new file mode 100644 index 0000000..948fc43 --- /dev/null +++ b/__tests__/ScopeProvider3/06_implicit_parent.test.tsx @@ -0,0 +1,104 @@ +import type { FC } from 'react' +import { render } from '@testing-library/react' +import { atom, useAtom, useAtomValue } from 'jotai' +import { atomWithReducer } from 'jotai/vanilla/utils' +import { clickButton, getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' + +function renderWithOrder(level1: 'BD' | 'DB', level2: 'BD' | 'DB') { + const baseAtom = atomWithReducer(0, (v) => v + 1) + baseAtom.debugLabel = 'baseAtom' + baseAtom.toString = function toString() { + return this.debugLabel ?? 'Unknown Atom' + } + + const derivedAtom = atom((get) => get(baseAtom)) + derivedAtom.debugLabel = 'derivedAtom' + derivedAtom.toString = function toString() { + return this.debugLabel ?? 'Unknown Atom' + } + + function BaseThenDerived({ level }: { level: string }) { + const [base, increaseBase] = useAtom(baseAtom) + const derived = useAtomValue(derivedAtom) + return ( + <> +
+ base: {base} + +
+
+ derived:{derived} +
+ + ) + } + + function DerivedThenBase({ level }: { level: string }) { + const derived = useAtomValue(derivedAtom) + const [base, increaseBase] = useAtom(baseAtom) + return ( + <> +
+ base:{base} + +
+
+ derived:{derived} +
+ + ) + } + function App(props: { + Level1Counter: FC<{ level: string }> + Level2Counter: FC<{ level: string }> + }) { + const { Level1Counter, Level2Counter } = props + return ( +
+

Layer 1: Scope derived

+

base should be globally shared

+ + +

Layer 2: Scope base

+

base should be globally shared

+ + + +
+
+ ) + } + function getCounter(order: 'BD' | 'DB') { + return order === 'BD' ? BaseThenDerived : DerivedThenBase + } + return render() +} + +/* + b, D(b) + S1[D]: b0, D1(b1) + S2[ ]: b0, D1(b1) +*/ +describe('Implicit parent does not affect unscoped', () => { + const cases = [ + ['BD', 'BD'], + ['BD', 'DB'], + ['DB', 'BD'], + ['DB', 'DB'], + ] as const + test.each(cases)('level 1: %p and level 2: %p', (level1, level2) => { + const { container } = renderWithOrder(level1, level2) + const increaseLayer2Base = '.layer2.setBase' + const selectors = ['.layer1.base', '.layer1.derived', '.layer2.base', '.layer2.derived'] + + expect(getTextContents(container, selectors).join('')).toEqual('0000') + + clickButton(container, increaseLayer2Base) + expect(getTextContents(container, selectors).join('')).toEqual('1010') + }) +}) diff --git a/__tests__/ScopeProvider3/07_writable.test.tsx b/__tests__/ScopeProvider3/07_writable.test.tsx new file mode 100644 index 0000000..9107aee --- /dev/null +++ b/__tests__/ScopeProvider3/07_writable.test.tsx @@ -0,0 +1,146 @@ +import { render } from '@testing-library/react' +import { type WritableAtom, type PrimitiveAtom, atom, useAtom } from 'jotai' +import { clickButton, getTextContents } from './utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' + +let baseAtom: PrimitiveAtom + +type WritableNumberAtom = WritableAtom + +const writableAtom: WritableNumberAtom = atom(0, (get, set, value = 0) => { + set(writableAtom, get(writableAtom) + get(baseAtom) + value) +}) + +const thisWritableAtom: WritableNumberAtom = atom( + 0, + function write(this: WritableNumberAtom, get, set, value = 0) { + set(this, get(this) + get(baseAtom) + value) + }, +) + +function renderTest(targetAtom: WritableNumberAtom) { + baseAtom = atom(0) + function Component({ level }: { level: string }) { + const [value, increaseWritable] = useAtom(targetAtom) + const [baseValue, increaseBase] = useAtom(baseAtom) + return ( +
+
{value}
+
{baseValue}
+ + +
+ ) + } + + function App() { + return ( + <> +

unscoped

+ + +

scoped

+

+ writable atom should update its value in both scoped and unscoped and read scoped atom +

+ +
+ + ) + } + return render() +} + +/* +writable=w(,w + s), base=b +S0[ ]: b0, w0(,w0 + b0) +S1[b]: b1, w0(,w0 + b1) +*/ +describe('Self', () => { + test.each(['writableAtom', 'thisWritableAtom'])( + '%p updates its value in both scoped and unscoped and read scoped atom', + (atomKey) => { + const target = atomKey === 'writableAtom' ? writableAtom : thisWritableAtom + const { container } = renderTest(target) + + const increaseLevel0BaseAtom = '.level0 .writeBase' + const increaseLevel0Writable = '.level0 .write' + const increaseLevel1BaseAtom = '.level1 .writeBase' + const increaseLevel1Writable = '.level1 .write' + + const selectors = ['.level0 .readBase', '.level0 .read', '.level1 .readBase', '.level1 .read'] + + // all initial values are zero + expect(getTextContents(container, selectors)).toEqual([ + '0', // level0 readBase + '0', // level0 read + '0', // level1 readBase + '0', // level1 read + ]) + + // level0 base atom updates its value to 1 + clickButton(container, increaseLevel0BaseAtom) + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 readBase + '0', // level0 read + '0', // level1 readBase + '0', // level1 read + ]) + + // level0 writable atom increases its value, level1 writable atom shares the same value + clickButton(container, increaseLevel0Writable) + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 readBase + '1', // level0 read + '0', // level1 readBase + '1', // level1 read + ]) + + // level1 writable atom increases its value, + // but since level1 base atom is zero, + // level0 and level1 writable atoms value should not change + clickButton(container, increaseLevel1Writable) + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 readBase + '1', // level0 read + '0', // level1 readBase + '1', // level1 read + ]) + + // level1 base atom updates its value to 10 + clickButton(container, increaseLevel1BaseAtom) + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 readBase + '1', // level0 read + '10', // level1 readBase + '1', // level1 read + ]) + + // level0 writable atom increases its value using level0 base atom + clickButton(container, increaseLevel0Writable) + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 readBase + '2', // level0 read + '10', // level1 readBase + '2', // level1 read + ]) + + // level1 writable atom increases its value using level1 base atom + clickButton(container, increaseLevel1Writable) + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 readBase + '12', // level0 read + '10', // level1 readBase + '12', // level1 read + ]) + }, + ) +}) diff --git a/__tests__/ScopeProvider3/08_family.test.tsx b/__tests__/ScopeProvider3/08_family.test.tsx new file mode 100644 index 0000000..89315e7 --- /dev/null +++ b/__tests__/ScopeProvider3/08_family.test.tsx @@ -0,0 +1,257 @@ +import { render, act } from '@testing-library/react' +import { useAtom, atom, useSetAtom } from 'jotai' +import { atomFamily, atomWithReducer } from 'jotai/utils' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' +import { clickButton, getTextContents } from './utils' + +describe('AtomFamily with ScopeProvider', () => { + /* + a = aFamily('a'), b = aFamily('b') + S0[]: a0 b0 + S1[aFamily]: a1 b1 + */ + test('01. Scoped atom families provide isolated state', () => { + const aFamily = atomFamily(() => atom(0)) + const aAtom = aFamily('a') + aAtom.debugLabel = 'aAtom' + const bAtom = aFamily('b') + bAtom.debugLabel = 'bAtom' + function Counter({ level, param }: { level: string; param: string }) { + const [value, setValue] = useAtom(aFamily(param)) + return ( +
+ {param}:{value} + +
+ ) + } + + function App() { + return ( +
+

Unscoped

+ + +

Scoped Provider

+ + + + +
+ ) + } + + const { container } = render() + const selectors = ['.level0.a', '.level0.b', '.level1.a', '.level1.b'] + + expect(getTextContents(container, selectors)).toEqual([ + '0', // level0 a + '0', // level0 b + '0', // level1 a + '0', // level1 b + ]) + + clickButton(container, '.level0.set-a') + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 a + '0', // level0 b + '0', // level1 a + '0', // level1 b + ]) + + clickButton(container, '.level1.set-a') + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 a + '0', // level0 b + '1', // level1 a + '0', // level1 b + ]) + + clickButton(container, '.level1.set-b') + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 a + '0', // level0 b + '1', // level1 a + '1', // level1 b + ]) + }) + + /* + aFamily('a'), aFamily.remove('a') + S0[aFamily('a')]: a0 -> removed + S1[aFamily('a')]: a1 + */ + // TODO: refactor atomFamily to support descoping removing atoms + test.skip('02. Removing atom from atomFamily does not affect scoped state', () => { + const aFamily = atomFamily(() => atom(0)) + const atomA = aFamily('a') + atomA.debugLabel = 'atomA' + const rerenderAtom = atomWithReducer(0, (s) => s + 1) + rerenderAtom.debugLabel = 'rerenderAtom' + function Counter({ level, param }: { level: string; param: string }) { + const [value, setValue] = useAtom(atomA) + useAtom(rerenderAtom) + return ( +
+ {param}:{value} + +
+ ) + } + + function App() { + const rerender = useSetAtom(rerenderAtom) + return ( +
+

Unscoped

+ + +

Scoped Provider

+ + + +
+ ) + } + + const { container } = render() + const selectors = ['.level0.a', '.level1.a'] + + expect(getTextContents(container, selectors)).toEqual([ + '0', // level0 a + '0', // level1 a + ]) + + clickButton(container, '.level0.set-a') + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 a + '0', // level1 a + ]) + + act(() => { + clickButton(container, '.remove-atom') + }) + + expect(getTextContents(container, ['.level0.a', '.level1.a'])).toEqual([ + '1', // level0 a + '1', // level1 a // atomA is now unscoped + ]) + + clickButton(container, '.level1.set-a') + expect(getTextContents(container, ['.level0.a', '.level1.a'])).toEqual([ + '2', // level0 a + '2', // level1 a + ]) + }) + + /* + aFamily.setShouldRemove((createdAt, param) => param === 'b') + S0[aFamily('a'), aFamily('b')]: a0 removed + S1[aFamily('a'), aFamily('b')]: a1 b1 + */ + // TODO: refactor atomFamily to support descoping removing atoms + test.skip('03. Scoped atom families respect custom removal conditions', () => { + const aFamily = atomFamily(() => atom(0)) + const atomA = aFamily('a') + atomA.debugLabel = 'atomA' + const atomB = aFamily('b') + atomB.debugLabel = 'atomB' + const rerenderAtom = atomWithReducer(0, (s) => s + 1) + rerenderAtom.debugLabel = 'rerenderAtom' + + function Counter({ level, param }: { level: string; param: string }) { + const [value, setValue] = useAtom(aFamily(param)) + useAtom(rerenderAtom) + return ( +
+ {param}:{value} + +
+ ) + } + + function App() { + const rerender = useSetAtom(rerenderAtom) + return ( +
+ +

Unscoped

+ + +

Scoped Provider

+ + + + +
+ ) + } + + const { container } = render() + const removeBButton = '.remove-b' + const selectors = ['.level0.a', '.level0.b', '.level1.a', '.level1.b'] + + expect(getTextContents(container, selectors)).toEqual([ + '0', // level0 a + '0', // level0 b + '0', // level1 a + '0', // level1 b + ]) + + clickButton(container, '.level0.set-a') + clickButton(container, '.level0.set-b') + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 a + '1', // level0 b + '0', // level1 a // a is scoped + '0', // level1 b // b is scoped + ]) + + act(() => { + clickButton(container, removeBButton) + }) + + expect(getTextContents(container, selectors)).toEqual([ + '1', // level0 a + '1', // level0 b + '0', // level1 a // a is still scoped + '1', // level1 b // b is no longer scoped + ]) + }) +}) diff --git a/__tests__/ScopeProvider3/09_mount.test.tsx b/__tests__/ScopeProvider3/09_mount.test.tsx new file mode 100644 index 0000000..c47354e --- /dev/null +++ b/__tests__/ScopeProvider3/09_mount.test.tsx @@ -0,0 +1,73 @@ +import { useState } from 'react' +import { render, act } from '@testing-library/react' +import { atom, useAtomValue } from 'jotai' +import { ScopeProvider } from 'src/ScopeProvider3/ScopeProvider' +import { clickButton } from './utils' + +describe('ScopeProvider', () => { + it('mounts and unmounts successfully', () => { + const baseAtom = atom(0) + function Component() { + const base = useAtomValue(baseAtom) + return
{base}
+ } + function App() { + const [isMounted, setIsMounted] = useState(false) + return ( + <> +
+ +
+ {isMounted && ( + + + + )} + + ) + } + const { unmount, container } = render() + const mountButton = '.mount' + const base = '.base' + + act(() => clickButton(container, mountButton)) + expect(container.querySelector(base)).not.toBe(null) + act(() => clickButton(container, mountButton)) + expect(container.querySelector(base)).toBe(null) + act(() => clickButton(container, mountButton)) + unmount() + expect(container.querySelector(base)).toBe(null) + }) +}) + +it('computed atom mounts once for the unscoped and once for the scoped', () => { + const baseAtom = atom(0) + const deriveAtom = atom( + (get) => get(baseAtom), + () => {}, + ) + const onUnmount = jest.fn() + const onMount = jest.fn(() => onUnmount) + deriveAtom.onMount = onMount + function Component() { + return useAtomValue(deriveAtom) + } + function App() { + return ( + <> + + + + + + + + ) + } + const { unmount } = render() + expect(onMount).toHaveBeenCalledTimes(2) + unmount() + expect(onUnmount).toHaveBeenCalledTimes(2) +}) diff --git a/__tests__/ScopeProvider3/utils.ts b/__tests__/ScopeProvider3/utils.ts new file mode 100644 index 0000000..9f219e8 --- /dev/null +++ b/__tests__/ScopeProvider3/utils.ts @@ -0,0 +1,48 @@ +import { fireEvent } from '@testing-library/react' +import type { Store } from 'src/ScopeProvider3/types' + +function getElements(container: HTMLElement, querySelectors: string[]): Element[] { + return querySelectors.map((querySelector) => { + const element = container.querySelector(querySelector) + if (!element) { + throw new Error(`Element not found: ${querySelector}`) + } + return element + }) +} + +export function getTextContents(container: HTMLElement, selectors: string[]): string[] { + return getElements(container, selectors).map((element) => element.textContent!) +} + +export function clickButton(container: HTMLElement, querySelector: string) { + const button = container.querySelector(querySelector) + if (!button) { + throw new Error(`Button not found: ${querySelector}`) + } + fireEvent.click(button) +} + +export type PrdStore = Exclude +export type DevStoreRev4 = Omit, keyof PrdStore> + +function isDevStore(store: Store): store is PrdStore & DevStoreRev4 { + return ( + 'dev4_get_internal_weak_map' in store && + 'dev4_get_mounted_atoms' in store && + 'dev4_restore_atoms' in store + ) +} + +export function assertIsDevStore(store: Store): asserts store is PrdStore & DevStoreRev4 { + if (!isDevStore(store)) { + throw new Error('Store is not a dev store') + } +} + +export function delay(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +export type WithJestMock any> = T & + jest.Mock, Parameters> diff --git a/__tests__/derive/trials/getAtomState-caller.ts b/__tests__/derive/trials/getAtomState-caller.ts index 1c1ef46..799c8f4 100644 --- a/__tests__/derive/trials/getAtomState-caller.ts +++ b/__tests__/derive/trials/getAtomState-caller.ts @@ -1,6 +1,6 @@ import type { AtomState, AnyAtom, AnyWritableAtom, Store } from 'src/ScopeProvider2/types' import { atom, createStore, SetStateAction, type Getter, type Setter } from 'jotai' -import { assertIsDevStore, WithJestMock } from '../../utils' +import { assertIsDevStore, WithJestMock } from '../../ScopeProvider2/utils' type AtomStateWithExtras = AtomState & { label?: string; caller?: AnyAtom } type GetAtomStateExtended = ( diff --git a/__tests__/derive/understanding/atomState.test.ts b/__tests__/derive/understanding/atomState.test.ts index 240bd92..7c694cf 100644 --- a/__tests__/derive/understanding/atomState.test.ts +++ b/__tests__/derive/understanding/atomState.test.ts @@ -1,6 +1,6 @@ import type { AtomState } from 'src/ScopeProvider2/types' import { atom, createStore } from 'jotai' -import { assertIsDevStore } from '../../utils' +import { assertIsDevStore } from '../../ScopeProvider2/utils' const store = createStore() assertIsDevStore(store) diff --git a/__tests__/derive/understanding/derive.test.ts b/__tests__/derive/understanding/derive.test.ts index 844c775..07d2dcf 100644 --- a/__tests__/derive/understanding/derive.test.ts +++ b/__tests__/derive/understanding/derive.test.ts @@ -1,7 +1,7 @@ import type { AtomState, AnyAtom, AnyWritableAtom } from 'src/ScopeProvider2/types' import { atom, type Getter, type Setter, type SetStateAction } from 'jotai' import { createStore } from '../../protoStore' -import { assertIsDevStore, PrdStore, WithJestMock } from '../../utils' +import { assertIsDevStore, PrdStore, WithJestMock } from '../../ScopeProvider2/utils' type Store = ReturnType type AtomStateWithLabel = AtomState & { label?: string } @@ -106,7 +106,7 @@ describe('calls GAS and accessor traps on', () => { atomAState = stateMap.get(atomA)! atomAState.label = atomA.debugLabel! expect(getAtomState).nthCalledWith(1, atomA) - expect(getAtomState).nthCalledWith(2, atomA, atomAState) + expect(getAtomState).nthCalledWith(2, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomA])) @@ -128,7 +128,7 @@ describe('calls GAS and accessor traps on', () => { atomBState = stateMap.get(atomB)! atomBState.label = atomB.debugLabel! expect(getAtomState).nthCalledWith(1, atomB) - expect(getAtomState).nthCalledWith(2, atomA, atomBState) + expect(getAtomState).nthCalledWith(2, atomA, atomB) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomB])) @@ -140,7 +140,7 @@ describe('calls GAS and accessor traps on', () => { store.get(atomB) expect(getAtomState).toHaveBeenCalledTimes(2) expect(getAtomState).nthCalledWith(1, atomB) - expect(getAtomState).nthCalledWith(2, atomA, atomBState) + expect(getAtomState).nthCalledWith(2, atomA, atomB) expect(atomReadTrap).toHaveBeenCalledTimes(0) // atomRead is cached }) @@ -152,8 +152,8 @@ describe('calls GAS and accessor traps on', () => { atomDState = stateMap.get(atomD)! atomDState.label = atomD.debugLabel! expect(getAtomState).nthCalledWith(1, atomD) - expect(getAtomState).nthCalledWith(2, atomA, atomDState) - expect(getAtomState).nthCalledWith(3, atomA, atomDState) + expect(getAtomState).nthCalledWith(2, atomA, atomD) + expect(getAtomState).nthCalledWith(3, atomA, atomD) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomD])) @@ -167,9 +167,8 @@ describe('calls GAS and accessor traps on', () => { it('store.set(primitiveAtom, value)', () => { store.set(atomA, 1) - expect(getAtomState).toHaveBeenCalledTimes(2) - expect(getAtomState).nthCalledWith(1, atomA) - expect(getAtomState).nthCalledWith(2, atomA, atomAState) + expect(getAtomState).toHaveBeenCalledTimes(1) + expect(getAtomState).nthCalledWith(1, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) expect(atomWriteTrap).toHaveBeenCalledTimes(1) @@ -181,10 +180,9 @@ describe('calls GAS and accessor traps on', () => { it('store.set(primitiveAtom, (currValue) => nextValue)', () => { store.set(atomA, increment) - expect(getAtomState).toHaveBeenCalledTimes(3) - expect(getAtomState).nthCalledWith(1, atomA) - expect(getAtomState).nthCalledWith(2, atomA, atomAState) - expect(getAtomState).nthCalledWith(3, atomA, atomAState) + expect(getAtomState).toHaveBeenCalledTimes(2) + expect(getAtomState).nthCalledWith(1, atomA, atomA) + expect(getAtomState).nthCalledWith(2, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) expect(atomWriteTrap).toHaveBeenCalledTimes(1) expect(atomWriteTrap).toHaveBeenNthCalledWith(1, ...nthWriteParams(1, [atomA, , , increment])) @@ -196,13 +194,12 @@ describe('calls GAS and accessor traps on', () => { it('store.set(writableAtom, value)', () => { store.set(atomC, 3) - expect(getAtomState).toHaveBeenCalledTimes(3) + expect(getAtomState).toHaveBeenCalledTimes(2) store.get(atomC) atomCState = stateMap.get(atomC)! atomCState.label = atomC.debugLabel! - expect(getAtomState).nthCalledWith(1, atomC) - expect(getAtomState).nthCalledWith(2, atomA, atomCState) - expect(getAtomState).nthCalledWith(3, atomA, atomAState) + expect(getAtomState).nthCalledWith(1, atomA, atomC) + expect(getAtomState).nthCalledWith(2, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomC])) @@ -221,11 +218,10 @@ describe('calls GAS and accessor traps on', () => { it('store.set(writableAtom, currValue => nextValue)', () => { store.set(atomC, increment) - expect(getAtomState).toHaveBeenCalledTimes(4) - expect(getAtomState).nthCalledWith(1, atomC) - expect(getAtomState).nthCalledWith(2, atomA, atomCState) - expect(getAtomState).nthCalledWith(3, atomA, atomAState) - expect(getAtomState).nthCalledWith(4, atomA, atomAState) + expect(getAtomState).toHaveBeenCalledTimes(3) + expect(getAtomState).nthCalledWith(1, atomA, atomC) + expect(getAtomState).nthCalledWith(2, atomA, atomA) + expect(getAtomState).nthCalledWith(3, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) expect(atomWriteTrap).toHaveBeenCalledTimes(2) @@ -256,9 +252,9 @@ describe('calls GAS and accessor traps on', () => { const unsubB = store.sub(atomB, () => {}) expect(getAtomState).toHaveBeenCalledTimes(4) expect(getAtomState).nthCalledWith(1, atomB) - expect(getAtomState).nthCalledWith(2, atomA, atomBState) - expect(getAtomState).nthCalledWith(3, atomA, atomBState) - expect(getAtomState).nthCalledWith(4, atomA, atomBState) + expect(getAtomState).nthCalledWith(2, atomA, atomB) + expect(getAtomState).nthCalledWith(3, atomA, atomB) + expect(getAtomState).nthCalledWith(4, atomA, atomB) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomB])) @@ -267,11 +263,11 @@ describe('calls GAS and accessor traps on', () => { unsubB() expect(getAtomState).toHaveBeenCalledTimes(6) expect(getAtomState).nthCalledWith(1, atomB) - expect(getAtomState).nthCalledWith(2, atomA, atomBState) - expect(getAtomState).nthCalledWith(3, atomA, atomBState) - expect(getAtomState).nthCalledWith(4, atomA, atomBState) - expect(getAtomState).nthCalledWith(5, atomA, atomBState) - expect(getAtomState).nthCalledWith(6, atomB, atomAState) + expect(getAtomState).nthCalledWith(2, atomA, atomB) + expect(getAtomState).nthCalledWith(3, atomA, atomB) + expect(getAtomState).nthCalledWith(4, atomA, atomB) + expect(getAtomState).nthCalledWith(5, atomA, atomB) + expect(getAtomState).nthCalledWith(6, atomB, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomB])) expect(nthAtomReadGetter(1)).toHaveBeenCalledTimes(1) @@ -305,15 +301,15 @@ describe('calls GAS and accessor traps on', () => { expect(getAtomState).toHaveBeenCalledTimes(5) expect(getAtomState).nthCalledWith(1, atomA) expect(getAtomState).nthCalledWith(2, atomB) - expect(getAtomState).nthCalledWith(3, atomA, atomBState) - expect(getAtomState).nthCalledWith(4, atomA, atomBState) - expect(getAtomState).nthCalledWith(5, atomB, atomAState) + expect(getAtomState).nthCalledWith(3, atomA, atomB) + expect(getAtomState).nthCalledWith(4, atomA, atomB) + expect(getAtomState).nthCalledWith(5, atomB, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) jest.clearAllMocks() unsubB() expect(getAtomState).toHaveBeenCalledTimes(2) - expect(getAtomState).nthCalledWith(1, atomA, atomBState) - expect(getAtomState).nthCalledWith(2, atomB, atomAState) + expect(getAtomState).nthCalledWith(1, atomA, atomB) + expect(getAtomState).nthCalledWith(2, atomB, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) }) @@ -322,10 +318,10 @@ describe('calls GAS and accessor traps on', () => { unsubB() expect(getAtomState).toHaveBeenCalledTimes(5) expect(getAtomState).nthCalledWith(1, atomB) - expect(getAtomState).nthCalledWith(2, atomA, atomBState) - expect(getAtomState).nthCalledWith(3, atomA, atomBState) - expect(getAtomState).nthCalledWith(4, atomA, atomBState) - expect(getAtomState).nthCalledWith(5, atomB, atomAState) + expect(getAtomState).nthCalledWith(2, atomA, atomB) + expect(getAtomState).nthCalledWith(3, atomA, atomB) + expect(getAtomState).nthCalledWith(4, atomA, atomB) + expect(getAtomState).nthCalledWith(5, atomB, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) }) @@ -336,9 +332,9 @@ describe('calls GAS and accessor traps on', () => { expect(getAtomState).toHaveBeenCalledTimes(5) expect(getAtomState).nthCalledWith(1, atomA) expect(getAtomState).nthCalledWith(2, atomB) - expect(getAtomState).nthCalledWith(3, atomA, atomBState) - expect(getAtomState).nthCalledWith(4, atomA, atomBState) - expect(getAtomState).nthCalledWith(5, atomA, atomBState) + expect(getAtomState).nthCalledWith(3, atomA, atomB) + expect(getAtomState).nthCalledWith(4, atomA, atomB) + expect(getAtomState).nthCalledWith(5, atomA, atomB) expect(atomReadTrap).toHaveBeenCalledTimes(0) jest.clearAllMocks() unsubA() @@ -372,7 +368,7 @@ describe('calls GAS and accessor traps on', () => { unsubA = store.sub(atomA, () => {}) expect(getAtomState).toHaveBeenCalledTimes(2) expect(getAtomState).nthCalledWith(1, atomA) - expect(getAtomState).nthCalledWith(2, atomA, atomAState) + expect(getAtomState).nthCalledWith(2, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) expect(atomWriteTrap).toHaveBeenCalledTimes(1) expect(atomWriteTrap).toHaveBeenNthCalledWith(1, ...nthWriteParams(1, [atomA, , , -1])) @@ -396,7 +392,7 @@ describe('calls GAS and accessor traps on', () => { jest.clearAllMocks() unsubA() expect(getAtomState).toHaveBeenCalledTimes(1) - expect(getAtomState).nthCalledWith(1, atomA, atomAState) + expect(getAtomState).nthCalledWith(1, atomA, atomA) expect(atomReadTrap).toHaveBeenCalledTimes(0) expect(atomWriteTrap).toHaveBeenCalledTimes(1) expect(atomWriteTrap).toHaveBeenNthCalledWith(1, ...nthWriteParams(1, [atomA, , , -1])) @@ -408,11 +404,10 @@ describe('calls GAS and accessor traps on', () => { it('setSelf', async () => { store.get(atomE) await 'microtask' - expect(getAtomState).toHaveBeenCalledTimes(2) + expect(getAtomState).toHaveBeenCalledTimes(1) atomState = stateMap.get(atomE)! atomState.label = atomE.debugLabel! expect(getAtomState).nthCalledWith(1, atomE) - expect(getAtomState).nthCalledWith(2, atomE) expect(atomReadTrap).toHaveBeenCalledTimes(1) expect(atomReadTrap).toHaveBeenNthCalledWith(1, ...nthReadParams(1, [atomE])) @@ -424,18 +419,14 @@ describe('calls GAS and accessor traps on', () => { const atomG = atom( (get) => get(atomF), (get, set, value: number) => { - get(atomF) set(atomF, value) - get(atomF) }, ) atomG.debugLabel = 'atomG' const atomH = atom( (get) => get(atomG), (get, set, value: number) => { - get(atomG) set(atomG, value) - get(atomG) }, ) atomH.debugLabel = 'atomH' @@ -453,9 +444,9 @@ describe('calls GAS and accessor traps on', () => { expect(getAtomState).toHaveBeenCalledTimes(7) expect(getAtomState).nthCalledWith(1, atomH) - expect(getAtomState).nthCalledWith(2, atomG, atomHState) - expect(getAtomState).nthCalledWith(3, atomF, atomGState) - expect(getAtomState).nthCalledWith(4, atomF, atomFState) + expect(getAtomState).nthCalledWith(2, atomG, atomH) + expect(getAtomState).nthCalledWith(3, atomF, atomG) + expect(getAtomState).nthCalledWith(4, atomF, atomF) expect(getAtomState).nthCalledWith(5, atomF) expect(getAtomState).nthCalledWith(6, atomG) expect(getAtomState).nthCalledWith(7, atomH) @@ -473,30 +464,19 @@ describe('calls GAS and accessor traps on', () => { jest.clearAllMocks() store.set(atomH, 0) - expect(getAtomState).toHaveBeenCalledTimes(10) - expect(getAtomState).nthCalledWith(1, atomH) - expect(getAtomState).nthCalledWith(2, atomG, atomHState) - expect(getAtomState).nthCalledWith(3, atomF, atomGState) - expect(getAtomState).nthCalledWith(4, atomG, atomHState) - expect(getAtomState).nthCalledWith(5, atomF, atomGState) - expect(getAtomState).nthCalledWith(6, atomF, atomGState) - expect(getAtomState).nthCalledWith(7, atomF, atomFState) - expect(getAtomState).nthCalledWith(8, atomF, atomGState) - expect(getAtomState).nthCalledWith(9, atomG, atomHState) - expect(getAtomState).nthCalledWith(10, atomF, atomGState) + expect(getAtomState).toHaveBeenCalledTimes(3) + expect(getAtomState).nthCalledWith(1, atomG, atomH) + expect(getAtomState).nthCalledWith(2, atomF, atomG) + expect(getAtomState).nthCalledWith(3, atomF, atomF) expect(atomReadTrap).toHaveBeenCalledTimes(0) expect(atomWriteTrap).toHaveBeenCalledTimes(3) expect(atomWriteTrap).toHaveBeenNthCalledWith(1, ...nthWriteParams(1, [atomH, , , 0])) - expect(nthAtomWriteGetter(1)).toHaveBeenCalledTimes(2) - expect(nthAtomWriteGetter(1)).toHaveBeenNthCalledWith(1, atomG) - expect(nthAtomWriteGetter(1)).toHaveBeenNthCalledWith(2, atomG) + expect(nthAtomWriteGetter(1)).toHaveBeenCalledTimes(0) expect(nthAtomWriteSetter(1)).toHaveBeenCalledTimes(1) expect(nthAtomWriteSetter(1)).toHaveBeenCalledWith(atomG, 0) expect(atomWriteTrap).toHaveBeenNthCalledWith(2, ...nthWriteParams(2, [atomG, , , 0])) - expect(nthAtomWriteGetter(2)).toHaveBeenCalledTimes(2) - expect(nthAtomWriteGetter(2)).toHaveBeenNthCalledWith(1, atomF) - expect(nthAtomWriteGetter(2)).toHaveBeenNthCalledWith(2, atomF) + expect(nthAtomWriteGetter(2)).toHaveBeenCalledTimes(0) expect(nthAtomWriteSetter(2)).toHaveBeenCalledTimes(1) expect(nthAtomWriteSetter(2)).toHaveBeenCalledWith(atomF, 0) expect(atomWriteTrap).toHaveBeenNthCalledWith(3, ...nthWriteParams(3, [atomF, , , 0])) diff --git a/__tests__/derive/understanding/deriveStack.test.ts b/__tests__/derive/understanding/deriveStack.test.ts index 8aa8ddd..cc1038e 100644 --- a/__tests__/derive/understanding/deriveStack.test.ts +++ b/__tests__/derive/understanding/deriveStack.test.ts @@ -1,6 +1,6 @@ import type { AtomState, Store } from 'src/ScopeProvider2/types' import { atom, createStore, type Getter, type Setter } from 'jotai' -import { assertIsDevStore, WithJestMock } from '../../utils' +import { assertIsDevStore, WithJestMock } from '../../ScopeProvider2/utils' type AtomStateWithLabel = AtomState & { label?: string } type DeriveCallack = Parameters[0] diff --git a/src/ScopeProvider3/ScopeProvider.tsx b/src/ScopeProvider3/ScopeProvider.tsx new file mode 100644 index 0000000..5a0aa66 --- /dev/null +++ b/src/ScopeProvider3/ScopeProvider.tsx @@ -0,0 +1,59 @@ +import { type ReactNode, useState } from 'react' +import { Provider, useStore } from 'jotai/react' +import { type Atom } from 'jotai/vanilla' +import { AnyAtom, AnyAtomFamily, Store } from './types' +import { createScope } from './scope' + +type BaseScopeProviderProps = { + atoms?: Iterable + atomFamilies?: Iterable + debugName?: string + store?: Store + children: ReactNode +} + +export function ScopeProvider( + props: { atoms: Iterable> } & BaseScopeProviderProps, +): JSX.Element + +export function ScopeProvider( + props: { atomFamilies: Iterable } & BaseScopeProviderProps, +): JSX.Element + +export function ScopeProvider(props: BaseScopeProviderProps) { + const { atoms, atomFamilies, children, debugName, ...options } = props + const baseStore = useStore(options) + const atomSet = new Set(atoms) + const atomFamilySet = new Set(atomFamilies) + + function initialize() { + return { + scope: createScope(atomSet, atomFamilySet, baseStore, debugName), + hasChanged(current: { + baseStore: Store + atomSet: Set> + atomFamilySet: Set + }) { + return ( + current.baseStore !== baseStore || + !isEqualSet(atomSet, current.atomSet) || + !isEqualSet(atomFamilySet, current.atomFamilySet) + ) + }, + } + } + + const [{ hasChanged, scope }, setState] = useState(initialize) + if (hasChanged({ baseStore, atomSet, atomFamilySet })) { + scope.cleanup() + setState(initialize) + } + return {children} +} + +/** + * @returns true if the two sets are equal + */ +export function isEqualSet(a: Set, b: Set) { + return a === b || (a.size === b.size && Array.from(a).every((v) => b.has(v))) +} diff --git a/src/ScopeProvider3/scope.ts b/src/ScopeProvider3/scope.ts new file mode 100644 index 0000000..df94894 --- /dev/null +++ b/src/ScopeProvider3/scope.ts @@ -0,0 +1,75 @@ +import type { AnyAtom, AnyAtomFamily, AtomState, NamedStore, Store } from './types' + +/** + * @returns a derived store that intercepts get and set calls to apply the scope + */ +export function createScope( + atoms: Set, + atomFamilies: Set, + baseStore: Store, + debugName?: string, +) { + // ================================================================================== + + /** set of explicitly scoped atoms */ + const explicit = new WeakSet(atoms) + + /** set of cleanup functions */ + const cleanupSet = new Set<() => void>() + + function cleanup() { + for (const c of cleanupSet) { + c() + } + cleanupSet.clear() + } + + for (const atomFamily of atomFamilies) { + for (const param of atomFamily.getParams()) { + const anAtom = atomFamily(param) + explicit.add(anAtom) + } + cleanupSet.add( + atomFamily.unstable_listen(({ type, atom: anAtom }) => { + if (type === 'CREATE') { + explicit.add(anAtom) + } else if (!atoms.has(anAtom)) { + explicit.delete(anAtom) + } + }), + ) + } + + const store: NamedStore = baseStore.unstable_derive( + (baseGetAtomState, readAtomTrap, writeAtomTrap) => { + /** map of scoped atoms to their atomState states */ + const scopedAtomStateMap = new WeakMap>() + + /** set of scoped atom states */ + const scopedAtomStateSet = new WeakSet>() + + return [ + function getAtomState(atom, originAtomState) { + if (scopedAtomStateSet.has(originAtomState!) || explicit.has(atom)) { + let atomState = scopedAtomStateMap.get(atom) + if (!atomState) { + atomState = { d: new Map(), p: new Set(), n: 0 } + scopedAtomStateMap.set(atom, atomState) + scopedAtomStateSet.add(atomState) + } + return atomState + } + // inherit atom state + return baseGetAtomState(atom, originAtomState)! + }, + readAtomTrap, + writeAtomTrap, + ] + }, + ) + if (debugName && process.env.NODE_ENV !== 'production') { + store.name = `store:${debugName}` + } + + return { store, cleanup } +} diff --git a/src/ScopeProvider3/types.ts b/src/ScopeProvider3/types.ts new file mode 100644 index 0000000..a255daf --- /dev/null +++ b/src/ScopeProvider3/types.ts @@ -0,0 +1,59 @@ +import type { getDefaultStore, WritableAtom, Atom } from 'jotai' +import type { AtomFamily } from 'jotai/vanilla/utils/atomFamily' + +export type Store = ReturnType + +export type NamedStore = Store & { name?: string } + +export type AnyAtom = Atom | WritableAtom + +export type AnyAtomFamily = AtomFamily + +/* =================== Stolen from jotai/store.ts ================== */ +type AnyValue = unknown +type AnyError = unknown +type OnUnmount = () => void + +/** + * Mutable atom state, + * tracked for both mounted and unmounted atoms in a store. + */ +export type AtomState = { + /** + * Map of atoms that the atom depends on. + * The map value is the epoch number of the dependency. + */ + d: Map + /** + * Set of atoms with pending promise that depend on the atom. + * + * This may cause memory leaks, but it's for the capability to continue promises + */ + p: Set + /** The epoch number of the atom. */ + n: number + /** Object to store mounted state of the atom. */ + /** + * State tracked for mounted atoms. An atom is considered "mounted" if it has a + * subscriber, or is a transitive dependency of another atom that has a + * subscriber. + * + * The mounted state of an atom is freed once it is no longer mounted. + * + * only available if the atom is mounted + */ + m?: { + /** Set of listeners to notify when the atom value changes. */ + l: Set<() => void> + /** Set of mounted atoms that the atom depends on. */ + d: Set + /** Set of mounted atoms that depends on the atom. */ + t: Set + /** Function to run when the atom is unmounted. */ + u?: OnUnmount + } + /** Atom value */ + v?: Value + /** Atom error */ + e?: AnyError +}