diff --git a/src/vanilla/store.ts b/src/vanilla/store.ts index 6d500efe9b..b382bbbb13 100644 --- a/src/vanilla/store.ts +++ b/src/vanilla/store.ts @@ -142,19 +142,6 @@ type DevStoreRev2 = { dev_get_mounted: (a: AnyAtom) => Mounted | undefined dev_restore_atoms: (values: Iterable) => void } -type DevListenerRev3 = ( - action: { type: 'set'; atom: AnyAtom } | { type: 'unsub' }, -) => void -type DevStoreRev3 = { - dev3_subscribe_store: (l: DevListenerRev3) => () => void - dev3_get_mounted_atoms: () => Iterable - dev3_get_atom_state: ( - a: AnyAtom, - ) => { readonly v: AnyValue } | { readonly e: AnyError } | undefined - // deps are atoms that specified atom depends on (not including self) - dev3_get_atom_deps: (a: AnyAtom) => Iterable | undefined - dev3_restore_atoms: (values: Iterable) => void -} type PrdStore = { get: (atom: Atom) => Value @@ -164,12 +151,9 @@ type PrdStore = { ) => Result sub: (atom: AnyAtom, listener: () => void) => () => void } -type Store = - | (PrdStore & Partial) - | (PrdStore & DevStoreRev2 & DevStoreRev3) +type Store = PrdStore & Partial export type INTERNAL_DevStoreRev2 = DevStoreRev2 -export type INTERNAL_DevStoreRev3 = DevStoreRev3 export type INTERNAL_PrdStore = PrdStore /** @@ -196,10 +180,10 @@ export const createStore = (): Store => { AnyAtom, [prevAtomState: AtomState | undefined, dependents: Dependents] >() - let devListenersRev3: Set + let devListenersRev2: Set let mountedAtoms: MountedAtoms if (import.meta.env?.MODE !== 'production') { - devListenersRev3 = new Set() + devListenersRev2 = new Set() mountedAtoms = new Set() } @@ -602,7 +586,7 @@ export const createStore = (): Store => { if (!isSync) { const flushed = flushPending(pendingStack.pop()!) if (import.meta.env?.MODE !== 'production') { - devListenersRev3.forEach((l) => + devListenersRev2.forEach((l) => l({ type: 'async-write', flushed: flushed! }), ) } @@ -621,7 +605,7 @@ export const createStore = (): Store => { const result = writeAtomState(atom, ...args) const flushed = flushPending(pendingStack.pop()!) if (import.meta.env?.MODE !== 'production') { - devListenersRev3.forEach((l) => l({ type: 'write', flushed: flushed! })) + devListenersRev2.forEach((l) => l({ type: 'write', flushed: flushed! })) } return result } @@ -806,7 +790,7 @@ export const createStore = (): Store => { const listeners = mounted.l listeners.add(listener) if (import.meta.env?.MODE !== 'production') { - devListenersRev3.forEach((l) => + devListenersRev2.forEach((l) => l({ type: 'sub', flushed: flushed as Set }), ) } @@ -815,7 +799,7 @@ export const createStore = (): Store => { tryUnmountAtom(atom, mounted) if (import.meta.env?.MODE !== 'production') { // devtools uses this to detect if it _can_ unmount or not - devListenersRev3.forEach((l) => l({ type: 'unsub' })) + devListenersRev2.forEach((l) => l({ type: 'unsub' })) } } } @@ -827,9 +811,9 @@ export const createStore = (): Store => { sub: subscribeAtom, // store dev methods (these are tentative and subject to change without notice) dev_subscribe_store: (l) => { - devListenersRev3.add(l) + devListenersRev2.add(l) return () => { - devListenersRev3.delete(l) + devListenersRev2.delete(l) } }, dev_get_mounted_atoms: () => mountedAtoms.values(), @@ -844,55 +828,7 @@ export const createStore = (): Store => { } } const flushed = flushPending(pendingStack.pop()!) - devListenersRev3.forEach((l) => - l({ type: 'restore', flushed: flushed! }), - ) - }, - dev3_subscribe_store: (l) => { - const l2: DevListenerRev2 = (action) => { - if (action.type === 'unsub') { - l(action) - } else if (action.type !== 'sub' && 'flushed' in action) { - for (const a of action.flushed) { - l({ type: 'set', atom: a }) - } - } - } - devListenersRev3.add(l2) - return () => { - devListenersRev3.delete(l2) - } - }, - dev3_get_mounted_atoms: () => mountedAtoms.values(), - dev3_get_atom_state: (a) => { - const aState = atomStateMap.get(a) - if (aState && 'v' in aState) { - return { v: aState.v } - } - if (aState && 'e' in aState) { - return { e: aState.e } - } - return undefined - }, - dev3_get_atom_deps: (a) => { - const aState = atomStateMap.get(a) - if (!aState) { - return undefined - } - const deps = new Set(aState.d.keys()) - deps.delete(a) - return deps - }, - dev3_restore_atoms: (values) => { - pendingStack.push(new Set()) - for (const [atom, valueOrPromise] of values) { - if (hasInitialValue(atom)) { - setAtomValueOrPromise(atom, valueOrPromise) - recomputeDependents(atom) - } - } - const flushed = flushPending(pendingStack.pop()!) - devListenersRev3.forEach((l) => + devListenersRev2.forEach((l) => l({ type: 'restore', flushed: flushed! }), ) }, diff --git a/src/vanilla/store2.ts b/src/vanilla/store2.ts index 3d3cf67081..2f3c12e5cd 100644 --- a/src/vanilla/store2.ts +++ b/src/vanilla/store2.ts @@ -240,38 +240,12 @@ const setAtomStateValueOrPromise = ( } // for debugging purpose only -type OldAtomState = { d: Map } & ( - | { e: AnyError } - | { v: AnyValue } -) -type OldMounted = { l: Set<() => void>; t: Set; u?: OnUnmount } -type DevListenerRev2 = ( - action: - | { type: 'write'; flushed: Set } - | { type: 'async-write'; flushed: Set } - | { type: 'sub'; flushed: Set } - | { type: 'unsub' } - | { type: 'restore'; flushed: Set }, -) => void -type DevStoreRev2 = { - dev_subscribe_store: (l: DevListenerRev2, rev: 2) => () => void - dev_get_mounted_atoms: () => IterableIterator - dev_get_atom_state: (a: AnyAtom) => OldAtomState | undefined - dev_get_mounted: (a: AnyAtom) => OldMounted | undefined - dev_restore_atoms: (values: Iterable) => void -} -type DevListenerRev3 = ( - action: { type: 'set'; atom: AnyAtom } | { type: 'unsub' }, -) => void -type DevStoreRev3 = { - dev3_subscribe_store: (l: DevListenerRev3) => () => void - dev3_get_mounted_atoms: () => Iterable - dev3_get_atom_state: ( - a: AnyAtom, - ) => { readonly v: AnyValue } | { readonly e: AnyError } | undefined - // deps are atoms that specified atom depends on (not including self) - dev3_get_atom_deps: (a: AnyAtom) => Iterable | undefined - dev3_restore_atoms: (values: Iterable) => void +type DevStoreRev4 = { + dev4_get_internal_weak_map: () => WeakMap + dev4_override_method: ( + key: K, + fn: PrdStore[K], + ) => void } type PrdStore = { @@ -282,9 +256,10 @@ type PrdStore = { ) => Result sub: (atom: AnyAtom, listener: () => void) => () => void } -type Store = - | (PrdStore & Partial) - | (PrdStore & DevStoreRev2 & DevStoreRev3) +type Store = PrdStore | (PrdStore & DevStoreRev4) + +export type INTERNAL_DevStoreRev4 = DevStoreRev4 +export type INTERNAL_PrdStore = PrdStore export const createStore = (): Store => { const atomStateMap = new WeakMap() @@ -298,15 +273,6 @@ export const createStore = (): Store => { return atomState } - let devListenersRev2: Set - let devListenersRev3: Set - let mountedAtoms: Set - if (import.meta.env?.MODE !== 'production') { - devListenersRev2 = new Set() - devListenersRev3 = new Set() - mountedAtoms = new Set() - } - const clearDependencies = (atom: Atom) => { const atomState = getAtomState(atom) for (const a of atomState.d.keys()) { @@ -330,12 +296,7 @@ export const createStore = (): Store => { if (!isSync && atomState.m) { const pendingPair = createPendingPair() mountDependencies(pendingPair, atomState) - const flushed = flushPending(pendingPair) - if (import.meta.env?.MODE !== 'production' && flushed) { - flushed.forEach((a) => { - devListenersRev3.forEach((l) => l({ type: 'set', atom: a })) - }) - } + flushPending(pendingPair) } } @@ -424,12 +385,7 @@ export const createStore = (): Store => { if (atomState.m) { const pendingPair = createPendingPair() mountDependencies(pendingPair, atomState) - const flushed = flushPending(pendingPair) - if (import.meta.env?.MODE !== 'production' && flushed) { - flushed.forEach((a) => { - devListenersRev3.forEach((l) => l({ type: 'set', atom: a })) - }) - } + flushPending(pendingPair) } }, ) @@ -540,13 +496,7 @@ export const createStore = (): Store => { } else { r = writeAtomState(pendingPair, a as AnyWritableAtom, ...args) as R } - const flushed = flushPending(pendingPair, true) - if (import.meta.env?.MODE !== 'production' && flushed) { - flushed.forEach((a) => { - devListenersRev3.forEach((l) => l({ type: 'set', atom: a })) - }) - devListenersRev2.forEach((l) => l({ type: 'async-write', flushed })) - } + flushPending(pendingPair, true) return r as R } const result = atom.write(getter, setter, ...args) @@ -559,13 +509,7 @@ export const createStore = (): Store => { ): Result => { const pendingPair = createPendingPair() const result = writeAtomState(pendingPair, atom, ...args) - const flushed = flushPending(pendingPair) - if (import.meta.env?.MODE !== 'production' && flushed) { - flushed.forEach((a) => { - devListenersRev3.forEach((l) => l({ type: 'set', atom: a })) - }) - devListenersRev2.forEach((l) => l({ type: 'write', flushed })) - } + flushPending(pendingPair) return result } @@ -600,9 +544,6 @@ export const createStore = (): Store => { } // mount self atomState.m = { l: new Set(), d: new Set(atomState.d.keys()) } - if (import.meta.env?.MODE !== 'production') { - mountedAtoms.add(atom) - } if (isActuallyWritableAtom(atom) && atom.onMount) { const mounted = atomState.m const { onMount } = atom @@ -632,9 +573,6 @@ export const createStore = (): Store => { addPending(pendingPair, onUnmount) } delete atomState.m - if (import.meta.env?.MODE !== 'production') { - mountedAtoms.delete(atom) - } // unmount dependencies for (const a of atomState.d.keys()) { unmountAtom(pendingPair, a) @@ -649,114 +587,31 @@ export const createStore = (): Store => { } const subscribeAtom = (atom: AnyAtom, listener: () => void) => { - let prevMounted: Mounted | undefined - if (import.meta.env?.MODE !== 'production') { - prevMounted = atomStateMap.get(atom)?.m - } const pendingPair = createPendingPair() const mounted = mountAtom(pendingPair, atom) - const flushed = flushPending(pendingPair) + flushPending(pendingPair) const listeners = mounted.l listeners.add(listener) - if (import.meta.env?.MODE !== 'production' && flushed) { - flushed.forEach((a) => { - devListenersRev3.forEach((l) => l({ type: 'set', atom: a })) - }) - if (!prevMounted) { - flushed.add(atom) // HACK to include self - } - devListenersRev2.forEach((l) => l({ type: 'sub', flushed })) - } return () => { listeners.delete(listener) const pendingPair = createPendingPair() unmountAtom(pendingPair, atom) flushPending(pendingPair) - if (import.meta.env?.MODE !== 'production') { - // devtools uses this to detect if it _can_ unmount or not - devListenersRev2.forEach((l) => l({ type: 'unsub' })) - devListenersRev3.forEach((l) => l({ type: 'unsub' })) - } } } if (import.meta.env?.MODE !== 'production') { - return { + const store: Store = { get: readAtom, set: writeAtom, sub: subscribeAtom, // store dev methods (these are tentative and subject to change without notice) - dev_subscribe_store: (l) => { - devListenersRev2.add(l) - return () => { - devListenersRev2.delete(l) - } - }, - dev_get_mounted_atoms: () => mountedAtoms.values(), - dev_get_atom_state: (a: AnyAtom) => { - const getOldAtomState = (a: AnyAtom): OldAtomState | undefined => { - const aState = atomStateMap.get(a) - return ( - aState && - aState.s && { - d: new Map( - Array.from(aState.d.keys()).flatMap((a) => { - const s = getOldAtomState(a) - return s ? [[a, s]] : [] - }), - ), - ...aState.s, - } - ) - } - return getOldAtomState(a) - }, - dev_get_mounted: (a: AnyAtom) => { - const aState = atomStateMap.get(a) - return ( - aState && - aState.m && { - l: aState.m.l, - t: new Set([...aState.t, a]), // HACK to include self - ...(aState.m.u ? { u: aState.m.u } : {}), - } - ) - }, - dev_restore_atoms: (values: Iterable) => { - const pendingPair = createPendingPair() - for (const [atom, value] of values) { - setAtomStateValueOrPromise(getAtomState(atom), value) - recomputeDependents(pendingPair, atom) - } - const flushed = flushPending(pendingPair) - devListenersRev2.forEach((l) => - l({ type: 'restore', flushed: flushed! }), - ) - }, - dev3_subscribe_store: (l) => { - devListenersRev3.add(l) - return () => { - devListenersRev3.delete(l) - } - }, - dev3_get_mounted_atoms: () => mountedAtoms.values(), - dev3_get_atom_state: (a) => atomStateMap.get(a)?.s, - dev3_get_atom_deps: (a) => { - const aState = atomStateMap.get(a) - return aState ? new Set(aState.d.keys()) : undefined - }, - dev3_restore_atoms: (values) => { - const pendingPair = createPendingPair() - for (const [atom, value] of values) { - setAtomStateValueOrPromise(getAtomState(atom), value) - recomputeDependents(pendingPair, atom) - } - const flushed = flushPending(pendingPair) - flushed?.forEach((a) => { - devListenersRev3.forEach((l) => l({ type: 'set', atom: a })) - }) + dev4_get_internal_weak_map: () => atomStateMap, + dev4_override_method: (key, fn) => { + ;(store as any)[key] = fn }, } + return store } return { get: readAtom, diff --git a/tests/vanilla/storedev.test.tsx b/tests/vanilla/storedev.test.tsx index a950ea417a..6a0d6ee0ca 100644 --- a/tests/vanilla/storedev.test.tsx +++ b/tests/vanilla/storedev.test.tsx @@ -1,7 +1,10 @@ import { describe, expect, it, vi } from 'vitest' import { atom, createStore } from 'jotai/vanilla' -describe('[DEV-ONLY] dev-only methods rev2', () => { +const IS_DEV_STORE = 'dev_subscribe_store' in createStore() +const IS_DEV_STORE2 = 'dev4_get_internal_weak_map' in createStore() + +describe.skipIf(!IS_DEV_STORE)('[DEV-ONLY] dev-only methods rev2', () => { it('should return the values of all mounted atoms', () => { const store = createStore() const countAtom = atom(0) @@ -121,145 +124,16 @@ describe('[DEV-ONLY] dev-only methods rev2', () => { }) }) -describe('[DEV-ONLY] dev-only methods rev3', () => { - it('should return the values of all mounted atoms', () => { - const store = createStore() - if (!('dev3_get_mounted_atoms' in store)) { +describe.skipIf(!IS_DEV_STORE2)('[DEV-ONLY] dev-only methods rev4', () => { + it('should get atom value', () => { + const store = createStore() as any + if (!('dev4_get_internal_weak_map' in store)) { throw new Error('dev methods are not available') } const countAtom = atom(0) countAtom.debugLabel = 'countAtom' - const derivedAtom = atom((get) => get(countAtom) * 0) - const unsub = store.sub(derivedAtom, vi.fn()) - store.set(countAtom, 1) - const result = store.dev3_get_mounted_atoms() || [] - expect( - Array.from(result).sort( - (a, b) => Object.keys(a).length - Object.keys(b).length, - ), - ).toStrictEqual([ - { toString: expect.any(Function), read: expect.any(Function) }, - { - toString: expect.any(Function), - init: 0, - read: expect.any(Function), - write: expect.any(Function), - debugLabel: 'countAtom', - }, - ]) - unsub() - }) - - it('should get atom state of a given atom', () => { - const store = createStore() - if (!('dev3_get_atom_state' in store)) { - throw new Error('dev methods are not available') - } - const countAtom = atom(0) - const unsub = store.sub(countAtom, vi.fn()) - store.set(countAtom, 1) - const result = store.dev3_get_atom_state(countAtom) - expect(result).toHaveProperty('v', 1) - unsub() - }) - - it('should get atom deps', () => { - const store = createStore() - if (!('dev3_get_atom_deps' in store)) { - throw new Error('dev methods are not available') - } - const countAtom = atom(0) - const cb = vi.fn() - const unsub = store.sub(countAtom, cb) store.set(countAtom, 1) - const result = store.dev3_get_atom_deps(countAtom) - expect(result && Array.from(result)).toStrictEqual([]) - unsub() - }) - - it('should get atom deps 2', () => { - const store = createStore() - if (!('dev3_get_atom_deps' in store)) { - throw new Error('dev methods are not available') - } - const countAtom = atom(0) - const doubleAtom = atom((get) => get(countAtom) * 2) - const cb = vi.fn() - const unsub = store.sub(doubleAtom, cb) - store.set(countAtom, 1) - const result = store.dev3_get_atom_deps(doubleAtom) - expect(result && Array.from(result)).toStrictEqual([countAtom]) - unsub() - }) - - it('should restore atoms and its dependencies correctly', () => { - const store = createStore() - if (!('dev3_restore_atoms' in store)) { - throw new Error('dev methods are not available') - } - const countAtom = atom(0) - const derivedAtom = atom((get) => get(countAtom) * 2) - store.set(countAtom, 1) - store.dev3_restore_atoms([[countAtom, 2]]) - expect(store.get(countAtom)).toBe(2) - expect(store.get(derivedAtom)).toBe(4) - }) - - describe('dev3_subscribe_store', () => { - it('should call the callback when state changes', () => { - const store = createStore() - if (!('dev3_subscribe_store' in store)) { - throw new Error('dev methods are not available') - } - const callback = vi.fn() - const unsub = store.dev3_subscribe_store(callback) - const countAtom = atom(0) - const unsubAtom = store.sub(countAtom, vi.fn()) - store.set(countAtom, 1) - expect(callback).toHaveBeenNthCalledWith(1, { - type: 'set', - atom: countAtom, - }) - expect(callback).toHaveBeenCalledTimes(1) - unsub() - unsubAtom() - }) - - it('should call unsub only when atom is unsubscribed', () => { - const store = createStore() - if (!('dev3_subscribe_store' in store)) { - throw new Error('dev methods are not available') - } - const callback = vi.fn() - const unsub = store.dev3_subscribe_store(callback) - const countAtom = atom(0) - const unsubAtom = store.sub(countAtom, vi.fn()) - const unsubAtomSecond = store.sub(countAtom, vi.fn()) - unsubAtom() - expect(callback).toHaveBeenNthCalledWith(1, { type: 'unsub' }) - expect(callback).toHaveBeenCalledTimes(1) - unsub() - unsubAtomSecond() - }) - }) - - it('should unmount tree dependencies with store.get', async () => { - const store = createStore() - if (!('dev3_subscribe_store' in store)) { - throw new Error('dev methods are not available') - } - const countAtom = atom(0) - const derivedAtom = atom((get) => get(countAtom) * 2) - const anotherDerivedAtom = atom((get) => get(countAtom) * 3) - const callback = vi.fn() - const unsubStore = store.dev3_subscribe_store(() => { - // Comment this line to make the test pass - store.get(derivedAtom) - }) - const unsub = store.sub(anotherDerivedAtom, callback) - unsub() - unsubStore() - const result = Array.from(store.dev3_get_mounted_atoms() ?? []) - expect(result).toEqual([]) + const weakMap = store.dev4_get_internal_weak_map() + expect(weakMap.get(countAtom)?.s).toEqual({ v: 1 }) }) })