From ce4b08d5f771350b06efd83680b0d3fe5babdaa8 Mon Sep 17 00:00:00 2001 From: Daishi Kato Date: Thu, 12 Aug 2021 20:54:30 +0900 Subject: [PATCH] refactor: new store.ts based on old vanilla.ts (#650) * refactor: new store.ts based on old vanilla.ts * fix benchmark tools --- .size-snapshot.json | 18 +- benchmarks/simple-read.ts | 10 +- benchmarks/simple-write.ts | 10 +- benchmarks/subscribe-write.ts | 24 +- src/core/Provider.ts | 46 +- src/core/contexts.ts | 72 ++- src/core/store.ts | 653 ++++++++++++++++++++++++++ src/core/useAtom.ts | 20 +- src/core/vanilla.ts | 665 --------------------------- src/devtools/useAtomsSnapshot.ts | 31 +- src/devtools/useGotoAtomsSnapshot.ts | 14 +- src/utils/useHydrateAtoms.ts | 5 +- src/utils/useResetAtom.ts | 7 +- src/utils/useUpdateAtom.ts | 7 +- 14 files changed, 770 insertions(+), 812 deletions(-) create mode 100644 src/core/store.ts delete mode 100644 src/core/vanilla.ts diff --git a/.size-snapshot.json b/.size-snapshot.json index 86297c5675..ebdd29a8de 100644 --- a/.size-snapshot.json +++ b/.size-snapshot.json @@ -1,8 +1,8 @@ { "index.js": { - "bundled": 21700, - "minified": 10368, - "gzipped": 3421, + "bundled": 22369, + "minified": 8941, + "gzipped": 3292, "treeshaked": { "rollup": { "code": 14, @@ -14,9 +14,9 @@ } }, "utils.js": { - "bundled": 15909, - "minified": 7527, - "gzipped": 2770, + "bundled": 15954, + "minified": 7566, + "gzipped": 2795, "treeshaked": { "rollup": { "code": 28, @@ -28,9 +28,9 @@ } }, "devtools.js": { - "bundled": 19182, - "minified": 9522, - "gzipped": 3266, + "bundled": 20710, + "minified": 8460, + "gzipped": 3193, "treeshaked": { "rollup": { "code": 28, diff --git a/benchmarks/simple-read.ts b/benchmarks/simple-read.ts index 0fb5ae01f7..b749967ec1 100755 --- a/benchmarks/simple-read.ts +++ b/benchmarks/simple-read.ts @@ -3,7 +3,7 @@ import { add, complete, cycle, save, suite } from 'benny' import { atom } from '../src/core/atom' import type { PrimitiveAtom } from '../src/core/atom' -import { createState, readAtom } from '../src/core/vanilla' +import { READ_ATOM, createStore } from '../src/core/store' const createStateWithAtoms = (n: number) => { let targetAtom: PrimitiveAtom | undefined @@ -15,11 +15,11 @@ const createStateWithAtoms = (n: number) => { } initialValues.set(a, i) } - const state = createState(initialValues) + const store = createStore(initialValues) if (!targetAtom) { throw new Error() } - return [state, targetAtom] as const + return [store, targetAtom] as const } const main = async () => { @@ -27,8 +27,8 @@ const main = async () => { await suite( `simple-read-${n}`, add(`atoms=${10 ** n}`, () => { - const [state, targetAtom] = createStateWithAtoms(10 ** n) - return () => readAtom(state, targetAtom) + const [store, targetAtom] = createStateWithAtoms(10 ** n) + return () => store[READ_ATOM](targetAtom) }), cycle(), complete(), diff --git a/benchmarks/simple-write.ts b/benchmarks/simple-write.ts index a991358a9b..81d60a153b 100755 --- a/benchmarks/simple-write.ts +++ b/benchmarks/simple-write.ts @@ -3,7 +3,7 @@ import { add, complete, cycle, save, suite } from 'benny' import { atom } from '../src/core/atom' import type { PrimitiveAtom } from '../src/core/atom' -import { createState, writeAtom } from '../src/core/vanilla' +import { WRITE_ATOM, createStore } from '../src/core/store' const createStateWithAtoms = (n: number) => { let targetAtom: PrimitiveAtom | undefined @@ -15,11 +15,11 @@ const createStateWithAtoms = (n: number) => { } initialValues.set(a, i) } - const state = createState(initialValues) + const store = createStore(initialValues) if (!targetAtom) { throw new Error() } - return [state, targetAtom] as const + return [store, targetAtom] as const } const main = async () => { @@ -27,8 +27,8 @@ const main = async () => { await suite( `simple-write-${n}`, add(`atoms=${10 ** n}`, () => { - const [state, targetAtom] = createStateWithAtoms(10 ** n) - return () => writeAtom(state, targetAtom, (c) => c + 1) + const [store, targetAtom] = createStateWithAtoms(10 ** n) + return () => store[WRITE_ATOM](targetAtom, (c) => c + 1) }), cycle(), complete(), diff --git a/benchmarks/subscribe-write.ts b/benchmarks/subscribe-write.ts index ccd87af288..8fc216f2b2 100755 --- a/benchmarks/subscribe-write.ts +++ b/benchmarks/subscribe-write.ts @@ -4,11 +4,11 @@ import { add, complete, cycle, save, suite } from 'benny' import { atom } from '../src/core/atom' import type { PrimitiveAtom } from '../src/core/atom' import { - createState, - readAtom, - subscribeAtom, - writeAtom, -} from '../src/core/vanilla' + READ_ATOM, + SUBSCRIBE_ATOM, + WRITE_ATOM, + createStore, +} from '../src/core/store' const cleanupFns = new Set<() => void>() const cleanup = () => { @@ -18,22 +18,22 @@ const cleanup = () => { const createStateWithAtoms = (n: number) => { let targetAtom: PrimitiveAtom | undefined - const state = createState() + const store = createStore() for (let i = 0; i < n; ++i) { const a = atom(i) if (!targetAtom) { targetAtom = a } - readAtom(state, a) - const unsub = subscribeAtom(state, a, () => { - readAtom(state, a) + store[READ_ATOM](a) + const unsub = store[SUBSCRIBE_ATOM](a, () => { + store[READ_ATOM](a) }) cleanupFns.add(unsub) } if (!targetAtom) { throw new Error() } - return [state, targetAtom] as const + return [store, targetAtom] as const } const main = async () => { @@ -42,8 +42,8 @@ const main = async () => { `subscribe-write-${n}`, add(`atoms=${10 ** n}`, () => { cleanup() - const [state, targetAtom] = createStateWithAtoms(10 ** n) - return () => writeAtom(state, targetAtom, (c) => c + 1) + const [store, targetAtom] = createStateWithAtoms(10 ** n) + return () => store[WRITE_ATOM](targetAtom, (c) => c + 1) }), cycle(), complete(), diff --git a/src/core/Provider.ts b/src/core/Provider.ts index fd6d8e3e43..e7552607f4 100644 --- a/src/core/Provider.ts +++ b/src/core/Provider.ts @@ -1,14 +1,16 @@ -import { createElement, useDebugValue, useRef } from 'react' +import { createElement, useCallback, useDebugValue, useRef } from 'react' import type { PropsWithChildren } from 'react' import type { Atom, Scope } from './atom' import { + ScopeContainer, createScopeContainer, getScopeContext, isDevScopeContainer, } from './contexts' import type { ScopeContainerForDevelopment } from './contexts' +import { DEV_GET_ATOM_STATE, DEV_GET_MOUNTED } from './store' +import type { AtomState, Store } from './store' import { useMutableSource } from './useMutableSource' -import type { AtomState, State } from './vanilla' export const Provider = ({ initialValues, @@ -18,10 +20,8 @@ export const Provider = ({ initialValues?: Iterable, unknown]> scope?: Scope }>) => { - const scopeContainerRef = useRef | null>(null) - if (scopeContainerRef.current === null) { + const scopeContainerRef = useRef() + if (!scopeContainerRef.current) { // lazy initialization scopeContainerRef.current = createScopeContainer(initialValues) } @@ -51,15 +51,15 @@ export const Provider = ({ const atomToPrintable = (atom: Atom) => atom.debugLabel || atom.toString() -const stateToPrintable = ([state, atoms]: [State, Atom[]]) => +const stateToPrintable = ([store, atoms]: [Store, Atom[]]) => Object.fromEntries( atoms.flatMap((atom) => { - const mounted = state.m.get(atom) + const mounted = store[DEV_GET_MOUNTED]?.(atom) if (!mounted) { return [] } const dependents = mounted.d - const atomState = state.a.get(atom) || ({} as AtomState) + const atomState = store[DEV_GET_ATOM_STATE]?.(atom) || ({} as AtomState) return [ [ atomToPrintable(atom), @@ -72,30 +72,14 @@ const stateToPrintable = ([state, atoms]: [State, Atom[]]) => }) ) -export const getDebugStateAndAtoms = ({ - atoms, - state, -}: { - atoms: Atom[] - state: State -}) => [state, atoms] as const - -export const subscribeDebugScopeContainer = ( - { listeners }: { listeners: Set<() => void> }, - callback: () => void -) => { - listeners.add(callback) - return () => listeners.delete(callback) -} - // We keep a reference to the atoms in Provider's registeredAtoms in dev mode, // so atoms aren't garbage collected by the WeakMap of mounted atoms const useDebugState = (scopeContainer: ScopeContainerForDevelopment) => { - const debugMutableSource = scopeContainer[4] - const [state, atoms] = useMutableSource( - debugMutableSource, - getDebugStateAndAtoms, - subscribeDebugScopeContainer + const [store, , devMutableSource, devSubscribe] = scopeContainer + const atoms = useMutableSource( + devMutableSource, + useCallback((devContainer) => devContainer.atoms, []), + devSubscribe ) - useDebugValue([state, atoms], stateToPrintable) + useDebugValue([store, atoms], stateToPrintable) } diff --git a/src/core/contexts.ts b/src/core/contexts.ts index 6807e7c131..cada3fc217 100644 --- a/src/core/contexts.ts +++ b/src/core/contexts.ts @@ -1,64 +1,50 @@ import { createContext } from 'react' import type { Context } from 'react' -import type { Atom, Scope, WritableAtom } from './atom' +import type { Atom, Scope } from './atom' +import { GET_VERSION, createStore } from './store' import { createMutableSource } from './useMutableSource' -import { createState, flushPending, restoreAtoms, writeAtom } from './vanilla' const createScopeContainerForProduction = ( initialValues?: Iterable, unknown]> ) => { - const state = createState(initialValues) - const stateMutableSource = createMutableSource(state, () => state.v) - const commitCallback = () => flushPending(state) - const updateAtom = ( - atom: WritableAtom, - update: Update - ) => writeAtom(state, atom, update) - const restore = (values: Iterable, unknown]>) => - restoreAtoms(state, values) - return [stateMutableSource, updateAtom, commitCallback, restore] as const + const store = createStore(initialValues) + const mutableSource = createMutableSource(store, store[GET_VERSION]) + return [store, mutableSource] as const } const createScopeContainerForDevelopment = ( initialValues?: Iterable, unknown]> ) => { + let devVersion = 0 + const devListeners = new Set<() => void>() + const devContainer = { + atoms: Array.from(initialValues ?? []).map(([a]) => a), + } const stateListener = (updatedAtom: Atom, isNewAtom: boolean) => { - ++debugContainer.version + ++devVersion if (isNewAtom) { // FIXME memory leak // we should probably remove unmounted atoms eventually - debugContainer.atoms = [...debugContainer.atoms, updatedAtom] + devContainer.atoms = [...devContainer.atoms, updatedAtom] } Promise.resolve().then(() => { - debugContainer.listeners.forEach((listener) => listener()) + devListeners.forEach((listener) => listener()) }) } - const state = createState(initialValues, stateListener) - const stateMutableSource = createMutableSource(state, () => state.v) - const commitCallback = () => flushPending(state) - const updateAtom = ( - atom: WritableAtom, - update: Update - ) => writeAtom(state, atom, update) - const debugContainer = { - version: 0, - atoms: Array.from(initialValues ?? []).map(([a]) => a), - state, - listeners: new Set<() => void>(), + const store = createStore(initialValues, stateListener) + const mutableSource = createMutableSource(store, store[GET_VERSION]) + const devMutableSource = createMutableSource(devContainer, () => devVersion) + const devSubscribe = (_: unknown, callback: () => void) => { + devListeners.add(callback) + return () => devListeners.delete(callback) } - const debugMutableSource = createMutableSource( - debugContainer, - () => debugContainer.version - ) - const restore = (values: Iterable, unknown]>) => - restoreAtoms(state, values) - return [ - stateMutableSource, - updateAtom, - commitCallback, - restore, - debugMutableSource, - ] as const + return [store, mutableSource, devMutableSource, devSubscribe] as const +} + +export const isDevScopeContainer = ( + scopeContainer: ScopeContainer +): scopeContainer is ScopeContainerForDevelopment => { + return scopeContainer.length > 2 } type ScopeContainerForProduction = ReturnType< @@ -90,9 +76,3 @@ export const getScopeContext = (scope?: Scope) => { } return ScopeContextMap.get(scope) as ScopeContext } - -export const isDevScopeContainer = ( - store: ScopeContainer -): store is ScopeContainerForDevelopment => { - return store.length > 4 -} diff --git a/src/core/store.ts b/src/core/store.ts new file mode 100644 index 0000000000..d885a4b63e --- /dev/null +++ b/src/core/store.ts @@ -0,0 +1,653 @@ +import type { Atom, WritableAtom } from './atom' + +type AnyAtom = Atom +type AnyWritableAtom = WritableAtom +type OnUnmount = () => void +type NonPromise = T extends Promise ? V : T +type WriteGetter = Parameters['write']>[0] +type Setter = Parameters['write']>[1] + +const hasInitialValue = >( + atom: T +): atom is T & (T extends Atom ? { init: Value } : never) => + 'init' in atom + +const IS_EQUAL_PROMISE = Symbol() +const INTERRUPT_PROMISE = Symbol() +type InterruptablePromise = Promise & { + [IS_EQUAL_PROMISE]: (p: Promise) => boolean + [INTERRUPT_PROMISE]: () => void +} + +const isInterruptablePromise = ( + promise: Promise +): promise is InterruptablePromise => + !!(promise as InterruptablePromise)[INTERRUPT_PROMISE] + +const createInterruptablePromise = ( + promise: Promise +): InterruptablePromise => { + let interrupt: (() => void) | undefined + const interruptablePromise = new Promise((resolve, reject) => { + interrupt = resolve + promise.then(resolve, reject) + }) as InterruptablePromise + interruptablePromise[IS_EQUAL_PROMISE] = (p: Promise) => + p === interruptablePromise || p === promise + interruptablePromise[INTERRUPT_PROMISE] = interrupt as () => void + return interruptablePromise +} + +type Revision = number +type InvalidatedRevision = number +type ReadDependencies = Map + +// immutable atom state +export type AtomState = { + e?: Error // read error + p?: InterruptablePromise // read promise + c?: () => void // cancel read promise + w?: Promise // write promise + v?: NonPromise + r: Revision + i?: InvalidatedRevision + d: ReadDependencies +} + +type Listeners = Set<() => void> +type Dependents = Set +type Mounted = { + l: Listeners + d: Dependents + u: OnUnmount | void +} + +// for debugging purpose only +type StateListener = (updatedAtom: AnyAtom, isNewAtom: boolean) => void + +// store methods +export const GET_VERSION = 'v' +export const READ_ATOM = 'r' +export const WRITE_ATOM = 'w' +export const FLUSH_PENDING = 'f' +export const SUBSCRIBE_ATOM = 's' +export const RESTORE_ATOMS = 'h' +export const DEV_GET_ATOM_STATE = 'a' +export const DEV_GET_MOUNTED = 'm' + +export const createStore = ( + initialValues?: Iterable, + stateListener?: StateListener +) => { + let version = 0 + const atomStateMap = new WeakMap() + const mountedMap = new WeakMap() + const pendingMap = new Map() + + if (initialValues) { + for (const [atom, value] of initialValues) { + const atomState: AtomState = { v: value, r: 0, d: new Map() } + if ( + typeof process === 'object' && + process.env.NODE_ENV !== 'production' + ) { + Object.freeze(atomState) + if (!hasInitialValue(atom)) { + console.warn( + 'Found initial value for derived atom which can cause unexpected behavior', + atom + ) + } + } + atomStateMap.set(atom, atomState) + } + } + + const getAtomState = (atom: Atom) => + atomStateMap.get(atom) as AtomState | undefined + + const wipAtomState = ( + atom: Atom, + dependencies?: Set + ): [AtomState, ReadDependencies] => { + const atomState = getAtomState(atom) + const nextAtomState = { + r: 0, + ...atomState, + d: dependencies + ? new Map( + Array.from(dependencies).map((a) => [a, getAtomState(a)?.r ?? 0]) + ) + : atomState?.d || new Map(), + } + return [nextAtomState, atomState?.d || new Map()] + } + + const setAtomValue = ( + atom: Atom, + value: NonPromise, + dependencies?: Set, + promise?: Promise + ): void => { + const [atomState, prevDependencies] = wipAtomState(atom, dependencies) + if (promise && !atomState.p?.[IS_EQUAL_PROMISE](promise)) { + // newer async read is running, not updating + return + } + atomState.c?.() // cancel read promise + delete atomState.e // read error + delete atomState.p // read promise + delete atomState.c // cancel read promise + delete atomState.i // invalidated revision + if (!('v' in atomState) || !Object.is(atomState.v, value)) { + atomState.v = value + ++atomState.r // increment revision + } + commitAtomState(atom, atomState, dependencies && prevDependencies) + } + + const setAtomReadError = ( + atom: Atom, + error: Error, + dependencies?: Set, + promise?: Promise + ): void => { + const [atomState, prevDependencies] = wipAtomState(atom, dependencies) + if (promise && !atomState.p?.[IS_EQUAL_PROMISE](promise)) { + // newer async read is running, not updating + return + } + atomState.c?.() // cancel read promise + delete atomState.p // read promise + delete atomState.c // cancel read promise + delete atomState.i // invalidated revision + atomState.e = error // read error + commitAtomState(atom, atomState, prevDependencies) + } + + const setAtomReadPromise = ( + atom: Atom, + promise: Promise, + dependencies?: Set + ): void => { + const [atomState, prevDependencies] = wipAtomState(atom, dependencies) + if (atomState.p?.[IS_EQUAL_PROMISE](promise)) { + // the same promise, not updating + return + } + atomState.c?.() // cancel read promise + if (isInterruptablePromise(promise)) { + atomState.p = promise // read promise + delete atomState.c // this promise is from another atom state, shouldn't be canceled here + } else { + const interruptablePromise = createInterruptablePromise(promise) + atomState.p = interruptablePromise // read promise + atomState.c = interruptablePromise[INTERRUPT_PROMISE] + } + commitAtomState(atom, atomState, prevDependencies) + } + + const setAtomInvalidated = (atom: Atom): void => { + const [atomState] = wipAtomState(atom) + atomState.i = atomState.r // invalidated revision + commitAtomState(atom, atomState) + } + + const setAtomWritePromise = ( + atom: Atom, + promise?: Promise + ): void => { + const [atomState] = wipAtomState(atom) + if (promise) { + atomState.w = promise + } else { + delete atomState.w // write promise + } + commitAtomState(atom, atomState) + } + + const scheduleReadAtomState = ( + atom: Atom, + promise: Promise + ): void => { + promise.finally(() => { + readAtomState(atom, true) + }) + } + + const readAtomState = ( + atom: Atom, + force?: boolean + ): AtomState => { + if (!force) { + const atomState = getAtomState(atom) + if (atomState) { + atomState.d.forEach((_, a) => { + if (a !== atom) { + const aState = getAtomState(a) + if ( + aState && + !aState.e && // no read error + !aState.p && // no read promise + aState.r === aState.i // revision is invalidated + ) { + readAtomState(a, true) + } + } + }) + if ( + Array.from(atomState.d.entries()).every(([a, r]) => { + const aState = getAtomState(a) + return ( + aState && + !aState.e && // no read error + !aState.p && // no read promise + aState.r !== aState.i && // revision is not invalidated + aState.r === r // revision is equal to the last one + ) + }) + ) { + return atomState + } + } + } + let error: Error | undefined + let promise: Promise | undefined + let value: NonPromise | undefined + const dependencies = new Set() + try { + const promiseOrValue = atom.read((a: AnyAtom) => { + dependencies.add(a) + if (a !== atom) { + const aState = readAtomState(a) + if (aState.e) { + throw aState.e // read error + } + if (aState.p) { + throw aState.p // read promise + } + return aState.v // value + } + // a === atom + const aState = getAtomState(a) + if (aState) { + if (aState.e) { + throw aState.e // read error + } + if (aState.p) { + throw aState.p // read promise + } + return aState.v // value + } + if (hasInitialValue(a)) { + return a.init + } + throw new Error('no atom init') + }) + if (promiseOrValue instanceof Promise) { + promise = promiseOrValue + .then((value) => { + setAtomValue( + atom, + value as NonPromise, + dependencies, + promise as Promise + ) + flushPending() + }) + .catch((e) => { + if (e instanceof Promise) { + scheduleReadAtomState(atom, e) + return e + } + setAtomReadError( + atom, + e instanceof Error ? e : new Error(e), + dependencies, + promise as Promise + ) + flushPending() + }) + } else { + value = promiseOrValue as NonPromise + } + } catch (errorOrPromise) { + if (errorOrPromise instanceof Promise) { + promise = errorOrPromise + } else if (errorOrPromise instanceof Error) { + error = errorOrPromise + } else { + error = new Error(errorOrPromise) + } + } + if (error) { + setAtomReadError(atom, error, dependencies) + } else if (promise) { + setAtomReadPromise(atom, promise, dependencies) + } else { + setAtomValue(atom, value as NonPromise, dependencies) + } + return getAtomState(atom) as AtomState + } + + const readAtom = (readingAtom: Atom): AtomState => { + const atomState = readAtomState(readingAtom) + return atomState + } + + const addAtom = (addingAtom: AnyAtom): Mounted => { + let mounted = mountedMap.get(addingAtom) + if (!mounted) { + mounted = mountAtom(addingAtom) + } + flushPending() + return mounted + } + + // FIXME doesn't work with mutally dependent atoms + const canUnmountAtom = (atom: AnyAtom, mounted: Mounted) => + !mounted.l.size && + (!mounted.d.size || (mounted.d.size === 1 && mounted.d.has(atom))) + + const delAtom = (deletingAtom: AnyAtom): void => { + const mounted = mountedMap.get(deletingAtom) + if (mounted && canUnmountAtom(deletingAtom, mounted)) { + unmountAtom(deletingAtom) + } + flushPending() + } + + const invalidateDependents = (atom: Atom): void => { + const mounted = mountedMap.get(atom) + mounted?.d.forEach((dependent) => { + if (dependent === atom) { + return + } + setAtomInvalidated(dependent) + invalidateDependents(dependent) + }) + } + + const writeAtomState = ( + atom: WritableAtom, + update: Update + ): void => { + const writePromise = getAtomState(atom)?.w + if (writePromise) { + writePromise.then(() => { + writeAtomState(atom, update) + flushPending() + }) + return + } + const writeGetter: WriteGetter = ( + a: AnyAtom, + unstable_promise: boolean = false + ) => { + const aState = readAtomState(a) + if (aState.e) { + throw aState.e // read error + } + if (aState.p) { + if ( + typeof process === 'object' && + process.env.NODE_ENV !== 'production' + ) { + if (unstable_promise) { + console.info( + 'promise option in getter is an experimental feature.', + a + ) + } else { + console.warn( + 'Reading pending atom state in write operation. We throw a promise for now.', + a + ) + } + } + if (unstable_promise) { + return aState.p.then(() => writeGetter(a, unstable_promise)) + } + throw aState.p // read promise + } + if ('v' in aState) { + return aState.v // value + } + if ( + typeof process === 'object' && + process.env.NODE_ENV !== 'production' + ) { + console.warn( + '[Bug] no value found while reading atom in write operation. This is probably a bug.', + a + ) + } + throw new Error('no value found') + } + const promiseOrVoid = atom.write( + writeGetter, + ((a: AnyWritableAtom, v: unknown) => { + if (a === atom) { + if (!hasInitialValue(a)) { + // NOTE technically possible but restricted as it may cause bugs + throw new Error('no atom init') + } + if (v instanceof Promise) { + const promise = v + .then((resolvedValue) => { + setAtomValue(a, resolvedValue) + invalidateDependents(a) + flushPending() + }) + .catch((e) => { + setAtomReadError(atom, e instanceof Error ? e : new Error(e)) + flushPending() + }) + setAtomReadPromise(atom, promise) + } else { + setAtomValue(a, v) + } + invalidateDependents(a) + } else { + writeAtomState(a, v) + } + flushPending() + }) as Setter, + update + ) + if (promiseOrVoid instanceof Promise) { + const promise = promiseOrVoid.finally(() => { + setAtomWritePromise(atom) + flushPending() + }) + setAtomWritePromise(atom, promise) + } + // TODO write error is not handled + } + + const writeAtom = ( + writingAtom: WritableAtom, + update: Update + ): void => { + writeAtomState(writingAtom, update) + flushPending() + } + + const isActuallyWritableAtom = (atom: AnyAtom): atom is AnyWritableAtom => + !!(atom as AnyWritableAtom).write + + const mountAtom = ( + atom: Atom, + initialDependent?: AnyAtom + ): Mounted => { + const atomState = readAtomState(atom) + // mount read dependencies beforehand + atomState.d.forEach((_, a) => { + if (a !== atom) { + const aMounted = mountedMap.get(a) + if (aMounted) { + aMounted.d.add(atom) // add dependent + } else { + mountAtom(a, atom) + } + } + }) + // mount self + const mounted: Mounted = { + d: new Set(initialDependent && [initialDependent]), + l: new Set(), + u: undefined, + } + mountedMap.set(atom, mounted) + if (isActuallyWritableAtom(atom) && atom.onMount) { + const setAtom = (update: unknown) => writeAtom(atom, update) + mounted.u = atom.onMount(setAtom) + } + return mounted + } + + const unmountAtom = (atom: Atom): void => { + // unmount self + const onUnmount = mountedMap.get(atom)?.u + if (onUnmount) { + onUnmount() + } + mountedMap.delete(atom) + // unmount read dependencies afterward + const atomState = getAtomState(atom) + if (atomState) { + atomState.d.forEach((_, a) => { + if (a !== atom) { + const mounted = mountedMap.get(a) + if (mounted) { + mounted.d.delete(atom) + if (canUnmountAtom(a, mounted)) { + unmountAtom(a) + } + } + } + }) + } else if ( + typeof process === 'object' && + process.env.NODE_ENV !== 'production' + ) { + console.warn('[Bug] could not find atom state to unmount', atom) + } + } + + const mountDependencies = ( + atom: Atom, + atomState: AtomState, + prevDependencies: ReadDependencies + ): void => { + const dependencies = new Set(atomState.d.keys()) + prevDependencies.forEach((_, a) => { + if (dependencies.has(a)) { + // not changed + dependencies.delete(a) + return + } + const mounted = mountedMap.get(a) + if (mounted) { + mounted.d.delete(atom) + if (canUnmountAtom(a, mounted)) { + unmountAtom(a) + } + } + }) + dependencies.forEach((a) => { + const mounted = mountedMap.get(a) + if (mounted) { + const dependents = mounted.d + dependents.add(atom) + } else { + mountAtom(a, atom) + } + }) + } + + const commitAtomState = ( + atom: Atom, + atomState: AtomState, + prevDependencies?: ReadDependencies + ): void => { + if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { + Object.freeze(atomState) + } + const isNewAtom = !atomStateMap.has(atom) + atomStateMap.set(atom, atomState) + if (stateListener) { + stateListener(atom, isNewAtom) + } + ++version + if (!pendingMap.has(atom)) { + pendingMap.set(atom, prevDependencies) + } + } + + const flushPending = (): void => { + const pending = Array.from(pendingMap) + pendingMap.clear() + pending.forEach(([atom, prevDependencies]) => { + const atomState = getAtomState(atom) + if (atomState) { + if (prevDependencies) { + mountDependencies(atom, atomState, prevDependencies) + } + } else if ( + typeof process === 'object' && + process.env.NODE_ENV !== 'production' + ) { + console.warn('[Bug] atom state not found in flush', atom) + } + const mounted = mountedMap.get(atom) + mounted?.l.forEach((listener) => listener()) + }) + } + + const subscribeAtom = (atom: AnyAtom, callback: () => void) => { + const mounted = addAtom(atom) + const listeners = mounted.l + listeners.add(callback) + return () => { + listeners.delete(callback) + delAtom(atom) + } + } + + const restoreAtoms = ( + values: Iterable + ): void => { + for (const [atom, value] of values) { + if (hasInitialValue(atom)) { + setAtomValue(atom, value) + invalidateDependents(atom) + } + } + flushPending() + } + + if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { + return { + [GET_VERSION]: () => version, + [READ_ATOM]: readAtom, + [WRITE_ATOM]: writeAtom, + [FLUSH_PENDING]: flushPending, + [SUBSCRIBE_ATOM]: subscribeAtom, + [RESTORE_ATOMS]: restoreAtoms, + [DEV_GET_ATOM_STATE]: (a: AnyAtom) => atomStateMap.get(a), + [DEV_GET_MOUNTED]: (a: AnyAtom) => mountedMap.get(a), + } + } else { + return { + [GET_VERSION]: () => version, + [READ_ATOM]: readAtom, + [WRITE_ATOM]: writeAtom, + [FLUSH_PENDING]: flushPending, + [SUBSCRIBE_ATOM]: subscribeAtom, + [RESTORE_ATOMS]: restoreAtoms, + } + } +} + +export type Store = ReturnType diff --git a/src/core/useAtom.ts b/src/core/useAtom.ts index c5a43dd482..6f02a0c853 100644 --- a/src/core/useAtom.ts +++ b/src/core/useAtom.ts @@ -1,9 +1,9 @@ import { useCallback, useContext, useDebugValue, useEffect } from 'react' import type { Atom, Scope, SetAtom, WritableAtom } from './atom' import { getScopeContext } from './contexts' +import { FLUSH_PENDING, READ_ATOM, SUBSCRIBE_ATOM, WRITE_ATOM } from './store' +import type { Store } from './store' import { useMutableSource } from './useMutableSource' -import { readAtom, subscribeAtom } from './vanilla' -import type { State } from './vanilla' const isWritable = ( atom: Atom | WritableAtom @@ -42,8 +42,8 @@ export function useAtom( scope?: Scope ) { const getAtomValue = useCallback( - (state: State) => { - const atomState = readAtom(state, atom) + (store: Store) => { + const atomState = store[READ_ATOM](atom) if (atomState.e) { throw atomState.e // read error } @@ -62,8 +62,8 @@ export function useAtom( ) const subscribe = useCallback( - (state: State, callback: () => void) => - subscribeAtom(state, atom, callback), + (store: Store, callback: () => void) => + store[SUBSCRIBE_ATOM](atom, callback), [atom] ) @@ -75,21 +75,21 @@ export function useAtom( } const ScopeContext = getScopeContext(scope) - const [mutableSource, updateAtom, commitCallback] = useContext(ScopeContext) + const [store, mutableSource] = useContext(ScopeContext) const value = useMutableSource(mutableSource, getAtomValue, subscribe) useEffect(() => { - commitCallback() + store[FLUSH_PENDING]() }) const setAtom = useCallback( (update: Update) => { if (isWritable(atom)) { - updateAtom(atom, update) + store[WRITE_ATOM](atom, update) } else { throw new Error('not writable atom') } }, - [updateAtom, atom] + [store, atom] ) useDebugValue(value) diff --git a/src/core/vanilla.ts b/src/core/vanilla.ts deleted file mode 100644 index d01988e257..0000000000 --- a/src/core/vanilla.ts +++ /dev/null @@ -1,665 +0,0 @@ -import type { Atom, WritableAtom } from './atom' - -type AnyAtom = Atom -type AnyWritableAtom = WritableAtom -type OnUnmount = () => void -type NonPromise = T extends Promise ? V : T -type WriteGetter = Parameters['write']>[0] -type Setter = Parameters['write']>[1] - -const hasInitialValue = >( - atom: T -): atom is T & (T extends Atom ? { init: Value } : never) => - 'init' in atom - -const IS_EQUAL_PROMISE = Symbol() -const INTERRUPT_PROMISE = Symbol() -type InterruptablePromise = Promise & { - [IS_EQUAL_PROMISE]: (p: Promise) => boolean - [INTERRUPT_PROMISE]: () => void -} - -const isInterruptablePromise = ( - promise: Promise -): promise is InterruptablePromise => - !!(promise as InterruptablePromise)[INTERRUPT_PROMISE] - -const createInterruptablePromise = ( - promise: Promise -): InterruptablePromise => { - let interrupt: (() => void) | undefined - const interruptablePromise = new Promise((resolve, reject) => { - interrupt = resolve - promise.then(resolve, reject) - }) as InterruptablePromise - interruptablePromise[IS_EQUAL_PROMISE] = (p: Promise) => - p === interruptablePromise || p === promise - interruptablePromise[INTERRUPT_PROMISE] = interrupt as () => void - return interruptablePromise -} - -type Revision = number -type InvalidatedRevision = number -type ReadDependencies = Map - -// immutable atom state -export type AtomState = { - e?: Error // read error - p?: InterruptablePromise // read promise - c?: () => void // cancel read promise - w?: Promise // write promise - v?: NonPromise - r: Revision - i?: InvalidatedRevision - d: ReadDependencies -} - -type AtomStateMap = WeakMap - -type Listeners = Set<() => void> -type Dependents = Set -type Mounted = { - l: Listeners - d: Dependents - u: OnUnmount | void -} - -type MountedMap = WeakMap - -// for debugging purpose only -type StateListener = (updatedAtom: AnyAtom, isNewAtom: boolean) => void - -type StateVersion = number - -type PendingMap = Map - -// mutable state -export type State = { - l?: StateListener - v: StateVersion - a: AtomStateMap - m: MountedMap - p: PendingMap -} - -export const createState = ( - initialValues?: Iterable, - stateListener?: StateListener -): State => { - const state: State = { - l: stateListener, - v: 0, - a: new WeakMap(), - m: new WeakMap(), - p: new Map(), - } - if (initialValues) { - for (const [atom, value] of initialValues) { - const atomState: AtomState = { v: value, r: 0, d: new Map() } - if ( - typeof process === 'object' && - process.env.NODE_ENV !== 'production' - ) { - Object.freeze(atomState) - if (!hasInitialValue(atom)) { - console.warn( - 'Found initial value for derived atom which can cause unexpected behavior', - atom - ) - } - } - state.a.set(atom, atomState) - } - } - return state -} - -const getAtomState = (state: State, atom: Atom) => - state.a.get(atom) as AtomState | undefined - -const wipAtomState = ( - state: State, - atom: Atom, - dependencies?: Set -): [AtomState, ReadDependencies] => { - const atomState = getAtomState(state, atom) - const nextAtomState = { - r: 0, - ...atomState, - d: dependencies - ? new Map( - Array.from(dependencies).map((a) => [ - a, - getAtomState(state, a)?.r ?? 0, - ]) - ) - : atomState?.d || new Map(), - } - return [nextAtomState, atomState?.d || new Map()] -} - -const setAtomValue = ( - state: State, - atom: Atom, - value: NonPromise, - dependencies?: Set, - promise?: Promise -): void => { - const [atomState, prevDependencies] = wipAtomState(state, atom, dependencies) - if (promise && !atomState.p?.[IS_EQUAL_PROMISE](promise)) { - // newer async read is running, not updating - return - } - atomState.c?.() // cancel read promise - delete atomState.e // read error - delete atomState.p // read promise - delete atomState.c // cancel read promise - delete atomState.i // invalidated revision - if (!('v' in atomState) || !Object.is(atomState.v, value)) { - atomState.v = value - ++atomState.r // increment revision - } - commitAtomState(state, atom, atomState, dependencies && prevDependencies) -} - -const setAtomReadError = ( - state: State, - atom: Atom, - error: Error, - dependencies?: Set, - promise?: Promise -): void => { - const [atomState, prevDependencies] = wipAtomState(state, atom, dependencies) - if (promise && !atomState.p?.[IS_EQUAL_PROMISE](promise)) { - // newer async read is running, not updating - return - } - atomState.c?.() // cancel read promise - delete atomState.p // read promise - delete atomState.c // cancel read promise - delete atomState.i // invalidated revision - atomState.e = error // read error - commitAtomState(state, atom, atomState, prevDependencies) -} - -const setAtomReadPromise = ( - state: State, - atom: Atom, - promise: Promise, - dependencies?: Set -): void => { - const [atomState, prevDependencies] = wipAtomState(state, atom, dependencies) - if (atomState.p?.[IS_EQUAL_PROMISE](promise)) { - // the same promise, not updating - return - } - atomState.c?.() // cancel read promise - if (isInterruptablePromise(promise)) { - atomState.p = promise // read promise - delete atomState.c // this promise is from another atom state, shouldn't be canceled here - } else { - const interruptablePromise = createInterruptablePromise(promise) - atomState.p = interruptablePromise // read promise - atomState.c = interruptablePromise[INTERRUPT_PROMISE] - } - commitAtomState(state, atom, atomState, prevDependencies) -} - -const setAtomInvalidated = (state: State, atom: Atom): void => { - const [atomState] = wipAtomState(state, atom) - atomState.i = atomState.r // invalidated revision - commitAtomState(state, atom, atomState) -} - -const setAtomWritePromise = ( - state: State, - atom: Atom, - promise?: Promise -): void => { - const [atomState] = wipAtomState(state, atom) - if (promise) { - atomState.w = promise - } else { - delete atomState.w // write promise - } - commitAtomState(state, atom, atomState) -} - -const scheduleReadAtomState = ( - state: State, - atom: Atom, - promise: Promise -): void => { - promise.finally(() => { - readAtomState(state, atom, true) - }) -} - -const readAtomState = ( - state: State, - atom: Atom, - force?: boolean -): AtomState => { - if (!force) { - const atomState = getAtomState(state, atom) - if (atomState) { - atomState.d.forEach((_, a) => { - if (a !== atom) { - const aState = getAtomState(state, a) - if ( - aState && - !aState.e && // no read error - !aState.p && // no read promise - aState.r === aState.i // revision is invalidated - ) { - readAtomState(state, a, true) - } - } - }) - if ( - Array.from(atomState.d.entries()).every(([a, r]) => { - const aState = getAtomState(state, a) - return ( - aState && - !aState.e && // no read error - !aState.p && // no read promise - aState.r !== aState.i && // revision is not invalidated - aState.r === r // revision is equal to the last one - ) - }) - ) { - return atomState - } - } - } - let error: Error | undefined - let promise: Promise | undefined - let value: NonPromise | undefined - const dependencies = new Set() - try { - const promiseOrValue = atom.read((a: AnyAtom) => { - dependencies.add(a) - if (a !== atom) { - const aState = readAtomState(state, a) - if (aState.e) { - throw aState.e // read error - } - if (aState.p) { - throw aState.p // read promise - } - return aState.v // value - } - // a === atom - const aState = getAtomState(state, a) - if (aState) { - if (aState.e) { - throw aState.e // read error - } - if (aState.p) { - throw aState.p // read promise - } - return aState.v // value - } - if (hasInitialValue(a)) { - return a.init - } - throw new Error('no atom init') - }) - if (promiseOrValue instanceof Promise) { - promise = promiseOrValue - .then((value) => { - setAtomValue( - state, - atom, - value as NonPromise, - dependencies, - promise as Promise - ) - flushPending(state) - }) - .catch((e) => { - if (e instanceof Promise) { - scheduleReadAtomState(state, atom, e) - return e - } - setAtomReadError( - state, - atom, - e instanceof Error ? e : new Error(e), - dependencies, - promise as Promise - ) - flushPending(state) - }) - } else { - value = promiseOrValue as NonPromise - } - } catch (errorOrPromise) { - if (errorOrPromise instanceof Promise) { - promise = errorOrPromise - } else if (errorOrPromise instanceof Error) { - error = errorOrPromise - } else { - error = new Error(errorOrPromise) - } - } - if (error) { - setAtomReadError(state, atom, error, dependencies) - } else if (promise) { - setAtomReadPromise(state, atom, promise, dependencies) - } else { - setAtomValue(state, atom, value as NonPromise, dependencies) - } - return getAtomState(state, atom) as AtomState -} - -export const readAtom = ( - state: State, - readingAtom: Atom -): AtomState => { - const atomState = readAtomState(state, readingAtom) - return atomState -} - -const addAtom = (state: State, addingAtom: AnyAtom): Mounted => { - let mounted = state.m.get(addingAtom) - if (!mounted) { - mounted = mountAtom(state, addingAtom) - } - flushPending(state) - return mounted -} - -// FIXME doesn't work with mutally dependent atoms -const canUnmountAtom = (atom: AnyAtom, mounted: Mounted) => - !mounted.l.size && - (!mounted.d.size || (mounted.d.size === 1 && mounted.d.has(atom))) - -const delAtom = (state: State, deletingAtom: AnyAtom): void => { - const mounted = state.m.get(deletingAtom) - if (mounted && canUnmountAtom(deletingAtom, mounted)) { - unmountAtom(state, deletingAtom) - } - flushPending(state) -} - -const invalidateDependents = (state: State, atom: Atom): void => { - const mounted = state.m.get(atom) - mounted?.d.forEach((dependent) => { - if (dependent === atom) { - return - } - setAtomInvalidated(state, dependent) - invalidateDependents(state, dependent) - }) -} - -const writeAtomState = ( - state: State, - atom: WritableAtom, - update: Update -): void => { - const writePromise = getAtomState(state, atom)?.w - if (writePromise) { - writePromise.then(() => { - writeAtomState(state, atom, update) - flushPending(state) - }) - return - } - const writeGetter: WriteGetter = ( - a: AnyAtom, - unstable_promise: boolean = false - ) => { - const aState = readAtomState(state, a) - if (aState.e) { - throw aState.e // read error - } - if (aState.p) { - if ( - typeof process === 'object' && - process.env.NODE_ENV !== 'production' - ) { - if (unstable_promise) { - console.info( - 'promise option in getter is an experimental feature.', - a - ) - } else { - console.warn( - 'Reading pending atom state in write operation. We throw a promise for now.', - a - ) - } - } - if (unstable_promise) { - return aState.p.then(() => writeGetter(a, unstable_promise)) - } - throw aState.p // read promise - } - if ('v' in aState) { - return aState.v // value - } - if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { - console.warn( - '[Bug] no value found while reading atom in write operation. This is probably a bug.', - a - ) - } - throw new Error('no value found') - } - const promiseOrVoid = atom.write( - writeGetter, - ((a: AnyWritableAtom, v: unknown) => { - if (a === atom) { - if (!hasInitialValue(a)) { - // NOTE technically possible but restricted as it may cause bugs - throw new Error('no atom init') - } - if (v instanceof Promise) { - const promise = v - .then((resolvedValue) => { - setAtomValue(state, a, resolvedValue) - invalidateDependents(state, a) - flushPending(state) - }) - .catch((e) => { - setAtomReadError( - state, - atom, - e instanceof Error ? e : new Error(e) - ) - flushPending(state) - }) - setAtomReadPromise(state, atom, promise) - } else { - setAtomValue(state, a, v) - } - invalidateDependents(state, a) - } else { - writeAtomState(state, a, v) - } - flushPending(state) - }) as Setter, - update - ) - if (promiseOrVoid instanceof Promise) { - const promise = promiseOrVoid.finally(() => { - setAtomWritePromise(state, atom) - flushPending(state) - }) - setAtomWritePromise(state, atom, promise) - } - // TODO write error is not handled -} - -export const writeAtom = ( - state: State, - writingAtom: WritableAtom, - update: Update -): void => { - writeAtomState(state, writingAtom, update) - flushPending(state) -} - -const isActuallyWritableAtom = (atom: AnyAtom): atom is AnyWritableAtom => - !!(atom as AnyWritableAtom).write - -const mountAtom = ( - state: State, - atom: Atom, - initialDependent?: AnyAtom -): Mounted => { - const atomState = readAtomState(state, atom) - // mount read dependencies beforehand - atomState.d.forEach((_, a) => { - if (a !== atom) { - const aMounted = state.m.get(a) - if (aMounted) { - aMounted.d.add(atom) // add dependent - } else { - mountAtom(state, a, atom) - } - } - }) - // mount self - const mounted: Mounted = { - d: new Set(initialDependent && [initialDependent]), - l: new Set(), - u: undefined, - } - state.m.set(atom, mounted) - if (isActuallyWritableAtom(atom) && atom.onMount) { - const setAtom = (update: unknown) => writeAtom(state, atom, update) - mounted.u = atom.onMount(setAtom) - } - return mounted -} - -const unmountAtom = (state: State, atom: Atom): void => { - // unmount self - const onUnmount = state.m.get(atom)?.u - if (onUnmount) { - onUnmount() - } - state.m.delete(atom) - // unmount read dependencies afterward - const atomState = getAtomState(state, atom) - if (atomState) { - atomState.d.forEach((_, a) => { - if (a !== atom) { - const mounted = state.m.get(a) - if (mounted) { - mounted.d.delete(atom) - if (canUnmountAtom(a, mounted)) { - unmountAtom(state, a) - } - } - } - }) - } else if ( - typeof process === 'object' && - process.env.NODE_ENV !== 'production' - ) { - console.warn('[Bug] could not find atom state to unmount', atom) - } -} - -const mountDependencies = ( - state: State, - atom: Atom, - atomState: AtomState, - prevDependencies: ReadDependencies -): void => { - const dependencies = new Set(atomState.d.keys()) - prevDependencies.forEach((_, a) => { - if (dependencies.has(a)) { - // not changed - dependencies.delete(a) - return - } - const mounted = state.m.get(a) - if (mounted) { - mounted.d.delete(atom) - if (canUnmountAtom(a, mounted)) { - unmountAtom(state, a) - } - } - }) - dependencies.forEach((a) => { - const mounted = state.m.get(a) - if (mounted) { - const dependents = mounted.d - dependents.add(atom) - } else { - mountAtom(state, a, atom) - } - }) -} - -const commitAtomState = ( - state: State, - atom: Atom, - atomState: AtomState, - prevDependencies?: ReadDependencies -): void => { - if (typeof process === 'object' && process.env.NODE_ENV !== 'production') { - Object.freeze(atomState) - } - const isNewAtom = !state.a.has(atom) - state.a.set(atom, atomState) - if (state.l) { - state.l(atom, isNewAtom) - } - ++state.v - if (!state.p.has(atom)) { - state.p.set(atom, prevDependencies) - } -} - -export const flushPending = (state: State): void => { - const pending = Array.from(state.p) - state.p.clear() - pending.forEach(([atom, prevDependencies]) => { - const atomState = getAtomState(state, atom) - if (atomState) { - if (prevDependencies) { - mountDependencies(state, atom, atomState, prevDependencies) - } - } else if ( - typeof process === 'object' && - process.env.NODE_ENV !== 'production' - ) { - console.warn('[Bug] atom state not found in flush', atom) - } - const mounted = state.m.get(atom) - mounted?.l.forEach((listener) => listener()) - }) -} - -export const subscribeAtom = ( - state: State, - atom: AnyAtom, - callback: () => void -) => { - const mounted = addAtom(state, atom) - const listeners = mounted.l - listeners.add(callback) - return () => { - listeners.delete(callback) - delAtom(state, atom) - } -} - -export const restoreAtoms = ( - state: State, - values: Iterable -): void => { - for (const [atom, value] of values) { - if (hasInitialValue(atom)) { - setAtomValue(state, atom, value) - invalidateDependents(state, atom) - } - } - flushPending(state) -} diff --git a/src/devtools/useAtomsSnapshot.ts b/src/devtools/useAtomsSnapshot.ts index 4ed69b05a2..af26d5c31b 100644 --- a/src/devtools/useAtomsSnapshot.ts +++ b/src/devtools/useAtomsSnapshot.ts @@ -1,36 +1,37 @@ -import { useContext } from 'react' +import { useCallback, useContext } from 'react' import { SECRET_INTERNAL_getScopeContext as getScopeContext, SECRET_INTERNAL_useMutableSource as useMutableSource, } from 'jotai' import type { Atom, Scope } from '../core/atom' -import { - getDebugStateAndAtoms, - subscribeDebugScopeContainer, -} from '../core/Provider' -import type { AtomState } from '../core/vanilla' -// NOTE importing from '../core/Provider' is across bundles and actually copying code +// NOTE importing from '../core/contexts' is across bundles and actually copying code +import { isDevScopeContainer } from '../core/contexts' +import { DEV_GET_ATOM_STATE, DEV_GET_MOUNTED } from '../core/store' +import type { AtomState } from '../core/store' type AtomsSnapshot = Map, unknown> export function useAtomsSnapshot(scope?: Scope): AtomsSnapshot { const ScopeContext = getScopeContext(scope) - const debugMutableSource = useContext(ScopeContext)[4] + const scopeContainer = useContext(ScopeContext) - if (debugMutableSource === undefined) { + if (!isDevScopeContainer(scopeContainer)) { throw Error('useAtomsSnapshot can only be used in dev mode.') } - const [state, atoms] = useMutableSource( - debugMutableSource, - getDebugStateAndAtoms, - subscribeDebugScopeContainer + const [store, , devMutableSource, devSubscribe] = scopeContainer + + const atoms = useMutableSource( + devMutableSource, + // FIXME HACK creating new reference to force re-render + useCallback((devContainer) => [...devContainer.atoms], []), + devSubscribe ) const atomToAtomValueTuples = atoms - .filter((atom) => !!state.m.get(atom)) + .filter((atom) => !!store[DEV_GET_MOUNTED]?.(atom)) .map<[Atom, unknown]>((atom) => { - const atomState = state.a.get(atom) ?? ({} as AtomState) + const atomState = store[DEV_GET_ATOM_STATE]?.(atom) ?? ({} as AtomState) return [atom, atomState.e || atomState.p || atomState.w || atomState.v] }) return new Map(atomToAtomValueTuples) diff --git a/src/devtools/useGotoAtomsSnapshot.ts b/src/devtools/useGotoAtomsSnapshot.ts index c5ff377294..c9801ab514 100644 --- a/src/devtools/useGotoAtomsSnapshot.ts +++ b/src/devtools/useGotoAtomsSnapshot.ts @@ -1,8 +1,9 @@ import { useCallback, useContext } from 'react' import { SECRET_INTERNAL_getScopeContext as getScopeContext } from 'jotai' -import type { Scope } from '../core/atom' -import { isDevScopeContainer } from '../core/contexts' +import type { Atom, Scope } from '../core/atom' // NOTE importing from '../core/contexts' is across bundles and actually copying code +import { isDevScopeContainer } from '../core/contexts' +import { RESTORE_ATOMS } from '../core/store' export function useGotoAtomsSnapshot(scope?: Scope) { const ScopeContext = getScopeContext(scope) @@ -11,11 +12,12 @@ export function useGotoAtomsSnapshot(scope?: Scope) { if (!isDevScopeContainer(scopeContainer)) { throw new Error('useGotoAtomsSnapshot can only be used in dev mode.') } - const restore = scopeContainer[3] + + const store = scopeContainer[0] return useCallback( - (values: Parameters[0]) => { - restore(values) + (values: Iterable, unknown]>) => { + store[RESTORE_ATOMS](values) }, - [restore] + [store] ) } diff --git a/src/utils/useHydrateAtoms.ts b/src/utils/useHydrateAtoms.ts index 58aa2b7c26..ff58ed4b31 100644 --- a/src/utils/useHydrateAtoms.ts +++ b/src/utils/useHydrateAtoms.ts @@ -2,6 +2,7 @@ import { useContext } from 'react' import { SECRET_INTERNAL_getScopeContext as getScopeContext } from 'jotai' import type { Atom, Scope } from '../core/atom' import type { ScopeContainer } from '../core/contexts' +import { RESTORE_ATOMS } from '../core/store' const hydratedMap: WeakMap< ScopeContainer, @@ -14,7 +15,7 @@ export function useHydrateAtoms( ) { const ScopeContext = getScopeContext(scope) const scopeContainer = useContext(ScopeContext) - const restoreAtoms = scopeContainer[3] + const store = scopeContainer[0] const hydratedSet = getHydratedSet(scopeContainer) const tuplesToRestore = [] @@ -26,7 +27,7 @@ export function useHydrateAtoms( } } if (tuplesToRestore.length) { - restoreAtoms(tuplesToRestore) + store[RESTORE_ATOMS](tuplesToRestore) } } diff --git a/src/utils/useResetAtom.ts b/src/utils/useResetAtom.ts index cb01dbaac9..3b372981e0 100644 --- a/src/utils/useResetAtom.ts +++ b/src/utils/useResetAtom.ts @@ -2,6 +2,7 @@ import { useCallback, useContext } from 'react' import { SECRET_INTERNAL_getScopeContext as getScopeContext } from 'jotai' import type { WritableAtom } from 'jotai' import type { Scope } from '../core/atom' +import { WRITE_ATOM } from '../core/store' import { RESET } from './constants' export function useResetAtom( @@ -9,10 +10,10 @@ export function useResetAtom( scope?: Scope ) { const ScopeContext = getScopeContext(scope) - const [, updateAtom] = useContext(ScopeContext) + const store = useContext(ScopeContext)[0] const setAtom = useCallback( - () => updateAtom(anAtom, RESET), - [updateAtom, anAtom] + () => store[WRITE_ATOM](anAtom, RESET), + [store, anAtom] ) return setAtom } diff --git a/src/utils/useUpdateAtom.ts b/src/utils/useUpdateAtom.ts index ff17de6ab8..c417be3c20 100644 --- a/src/utils/useUpdateAtom.ts +++ b/src/utils/useUpdateAtom.ts @@ -2,16 +2,17 @@ import { useCallback, useContext } from 'react' import { SECRET_INTERNAL_getScopeContext as getScopeContext } from 'jotai' import type { WritableAtom } from 'jotai' import type { Scope, SetAtom } from '../core/atom' +import { WRITE_ATOM } from '../core/store' export function useUpdateAtom( anAtom: WritableAtom, scope?: Scope ) { const ScopeContext = getScopeContext(scope) - const [, updateAtom] = useContext(ScopeContext) + const store = useContext(ScopeContext)[0] const setAtom = useCallback( - (update: Update) => updateAtom(anAtom, update), - [updateAtom, anAtom] + (update: Update) => store[WRITE_ATOM](anAtom, update), + [store, anAtom] ) return setAtom as SetAtom }