From ca352334dd51ea7f3119dbb89d2f0ea49f75e351 Mon Sep 17 00:00:00 2001 From: David Maskasky Date: Tue, 4 Feb 2025 18:52:17 -0800 Subject: [PATCH] expose building blocks (#2974) * expose building blocks * elminate store args * move top-level functions to createBuildingBlocks and add some organization * tweak createBuildingBlocks * update store and tests invocation of INTERNAL_createBuildingBlocks * fix lint and decomposition * refactor(internals): simplify building blocks --------- Co-authored-by: daishi --- .github/workflows/test-multiple-builds.yml | 2 +- src/vanilla/internals.ts | 275 ++++++++++----------- src/vanilla/store.ts | 19 +- tests/vanilla/derive.test.tsx | 11 +- tests/vanilla/effect.test.ts | 4 +- tests/vanilla/store.test.tsx | 11 +- 6 files changed, 142 insertions(+), 180 deletions(-) diff --git a/.github/workflows/test-multiple-builds.yml b/.github/workflows/test-multiple-builds.yml index f0766c980b..8110e34d8d 100644 --- a/.github/workflows/test-multiple-builds.yml +++ b/.github/workflows/test-multiple-builds.yml @@ -43,7 +43,7 @@ jobs: sed -i~ "s/resolve('\.\/src\(.*\)\.ts')/resolve('\.\/dist\1.js')/" vitest.config.mts sed -i~ "s/import { useResetAtom } from 'jotai\/react\/utils'/const { useResetAtom } = require('..\/..\/..\/dist\/react\/utils.js')/" tests/react/utils/useResetAtom.test.tsx sed -i~ "s/import { RESET, atomWithReducer, atomWithReset } from 'jotai\/vanilla\/utils'/const { RESET, atomWithReducer, atomWithReset } = require('..\/..\/..\/dist\/vanilla\/utils.js')/" tests/react/utils/useResetAtom.test.tsx - perl -i~ -0777 -pe "s/import {[^}]+} from 'jotai\/vanilla\/internals'/const { INTERNAL_buildStore, INTERNAL_initializeStoreHooks, INTERNAL_getBuildingBlocksRev1: INTERNAL_getBuildingBlocks, INTERNAL_createBuildingBlocksRev1: INTERNAL_createBuildingBlocks } = require('..\/..\/dist\/vanilla\/internals.js')/g" tests/vanilla/store.test.tsx tests/vanilla/derive.test.tsx tests/vanilla/effect.test.ts + perl -i~ -0777 -pe "s/import {[^}]+} from 'jotai\/vanilla\/internals'/const { INTERNAL_buildStore, INTERNAL_initializeStoreHooks, INTERNAL_getBuildingBlocksRev1: INTERNAL_getBuildingBlocks } = require('..\/..\/dist\/vanilla\/internals.js')/g" tests/vanilla/store.test.tsx tests/vanilla/derive.test.tsx tests/vanilla/effect.test.ts - name: Patch for ESM if: ${{ matrix.build == 'esm' }} run: | diff --git a/src/vanilla/internals.ts b/src/vanilla/internals.ts index 8d83a67395..2e6727b933 100644 --- a/src/vanilla/internals.ts +++ b/src/vanilla/internals.ts @@ -62,9 +62,19 @@ type AtomStateMap = { set(atom: AnyAtom, atomState: AtomState): void } +type Store = { + get: (atom: Atom) => Value + set: ( + atom: WritableAtom, + ...args: Args + ) => Result + sub: (atom: AnyAtom, listener: () => void) => () => void +} + export type INTERNAL_Mounted = Mounted export type INTERNAL_AtomState = AtomState export type INTERNAL_AtomStateMap = AtomStateMap +export type INTERNAL_Store = Store // // Some util functions @@ -165,21 +175,98 @@ const addPendingPromiseToDependency = ( } // -// Some building-block functions +// Store hooks +// + +type StoreHook = { + (): void + add(callback: () => void): () => void +} + +type StoreHookForAtoms = { + (atom: AnyAtom): void + add(atom: AnyAtom, callback: () => void): () => void + add(atom: undefined, callback: (atom: AnyAtom) => void): () => void +} + +type StoreHooks = { + /** + * Listener to notify when the atom value is changed. + * This is an experimental API. + */ + readonly c?: StoreHookForAtoms + /** + * Listener to notify when the atom is mounted. + * This is an experimental API. + */ + readonly m?: StoreHookForAtoms + /** + * Listener to notify when the atom is unmounted. + * This is an experimental API. + */ + readonly u?: StoreHookForAtoms + /** + * Listener to notify when callbacks are being flushed. + * This is an experimental API. + */ + readonly f?: StoreHook +} + +const createStoreHook = (): StoreHook => { + const callbacks = new Set<() => void>() + const notify = () => { + callbacks.forEach((fn) => fn()) + } + notify.add = (fn: () => void) => { + callbacks.add(fn) + return () => { + callbacks.delete(fn) + } + } + return notify +} + +const createStoreHookForAtoms = (): StoreHookForAtoms => { + const all: object = {} + const callbacks = new WeakMap< + AnyAtom | typeof all, + Set<(atom?: AnyAtom) => void> + >() + const notify = (atom: AnyAtom) => { + callbacks.get(all)?.forEach((fn) => fn(atom)) + callbacks.get(atom)?.forEach((fn) => fn()) + } + notify.add = (atom: AnyAtom | undefined, fn: (atom?: AnyAtom) => void) => { + const key = atom || all + const fns = ( + callbacks.has(key) ? callbacks : callbacks.set(key, new Set()) + ).get(key)! + fns.add(fn) + return () => { + fns?.delete(fn) + if (!fns.size) { + callbacks.delete(key) + } + } + } + return notify as StoreHookForAtoms +} + +const initializeStoreHooks = (storeHooks: StoreHooks): Required => { + type SH = { -readonly [P in keyof StoreHooks]: StoreHooks[P] } + ;(storeHooks as SH).c ||= createStoreHookForAtoms() + ;(storeHooks as SH).m ||= createStoreHookForAtoms() + ;(storeHooks as SH).u ||= createStoreHookForAtoms() + ;(storeHooks as SH).f ||= createStoreHook() + return storeHooks as Required +} + +// +// Main functions // type BuildingBlocks = readonly [ - // main functions for buildStore - flushCallbacks: () => void, - recomputeInvalidatedAtoms: () => void, - readAtomState: (atom: Atom) => AtomState, - writeAtomState: ( - atom: WritableAtom, - ...args: Args - ) => Result, - mountAtom: (atom: Atom) => Mounted, - unmountAtom: (atom: Atom) => Mounted | undefined, - // other things for ecosystem + // store state atomStateMap: AtomStateMap, mountedAtoms: WeakMap, invalidatedAtoms: WeakMap, @@ -187,6 +274,7 @@ type BuildingBlocks = readonly [ mountCallbacks: Set<() => void>, unmountCallbacks: Set<() => void>, storeHooks: StoreHooks, + // store intercepters atomRead: ( atom: Atom, ...params: Parameters['read']> @@ -200,15 +288,29 @@ type BuildingBlocks = readonly [ atom: WritableAtom, setAtom: (...args: Args) => Result, ) => OnUnmount | void, + // functions ensureAtomState: (atom: Atom) => AtomState, + flushCallbacks: () => void, + recomputeInvalidatedAtoms: () => void, setAtomStateValueOrPromise: (atom: AnyAtom, valueOrPromise: unknown) => void, + readAtomState: (atom: Atom) => AtomState, getMountedOrPendingDependents: (atom: AnyAtom) => Set, invalidateDependents: (atom: AnyAtom) => void, + writeAtomState: ( + atom: WritableAtom, + ...args: Args + ) => Result, mountDependencies: (atom: AnyAtom) => void, + mountAtom: (atom: Atom) => Mounted, + unmountAtom: (atom: Atom) => Mounted | undefined, ] -const createBuildingBlocks = ( - getStore: () => Store, +const BUILDING_BLOCKS: unique symbol = Symbol() // no description intentionally + +const getBuildingBlocks = (store: unknown): BuildingBlocks => + (store as any)[BUILDING_BLOCKS] + +const buildStore = ( atomStateMap: AtomStateMap = new WeakMap(), mountedAtoms: WeakMap = new WeakMap(), invalidatedAtoms: WeakMap = new WeakMap(), @@ -232,7 +334,7 @@ const createBuildingBlocks = ( atom: WritableAtom, setAtom: (...args: Args) => Result, ) => OnUnmount | void = (atom, setAtom) => atom.onMount?.(setAtom), -): BuildingBlocks => { +): Store => { const ensureAtomState = (atom: Atom): AtomState => { if (import.meta.env?.MODE !== 'production' && !atom) { throw new Error('Atom is undefined or null') @@ -241,7 +343,7 @@ const createBuildingBlocks = ( if (!atomState) { atomState = { d: new Map(), p: new Set(), n: 0 } atomStateMap.set(atom, atomState) - atomOnInit?.(atom, getStore()) + atomOnInit?.(atom, store) } return atomState as AtomState } @@ -680,15 +782,8 @@ const createBuildingBlocks = ( return mounted } - return [ - // main functions for buildStore - flushCallbacks, - recomputeInvalidatedAtoms, - readAtomState, - writeAtomState, - mountAtom, - unmountAtom, - // other things for ecosystem + const buildingBlocks: BuildingBlocks = [ + // store state atomStateMap, mountedAtoms, invalidatedAtoms, @@ -696,134 +791,24 @@ const createBuildingBlocks = ( mountCallbacks, unmountCallbacks, storeHooks, + // store intercepters atomRead, atomWrite, atomOnInit, atomOnMount, + // functions ensureAtomState, - setAtomStateValueOrPromise, - getMountedOrPendingDependents, - invalidateDependents, - mountDependencies, - ] as const -} - -// -// Store hooks -// - -type StoreHook = { - (): void - add(callback: () => void): () => void -} - -type StoreHookForAtoms = { - (atom: AnyAtom): void - add(atom: AnyAtom, callback: () => void): () => void - add(atom: undefined, callback: (atom: AnyAtom) => void): () => void -} - -type StoreHooks = { - /** - * Listener to notify when the atom value is changed. - * This is an experimental API. - */ - readonly c?: StoreHookForAtoms - /** - * Listener to notify when the atom is mounted. - * This is an experimental API. - */ - readonly m?: StoreHookForAtoms - /** - * Listener to notify when the atom is unmounted. - * This is an experimental API. - */ - readonly u?: StoreHookForAtoms - /** - * Listener to notify when callbacks are being flushed. - * This is an experimental API. - */ - readonly f?: StoreHook -} - -const createStoreHook = (): StoreHook => { - const callbacks = new Set<() => void>() - const notify = () => { - callbacks.forEach((fn) => fn()) - } - notify.add = (fn: () => void) => { - callbacks.add(fn) - return () => { - callbacks.delete(fn) - } - } - return notify -} - -const createStoreHookForAtoms = (): StoreHookForAtoms => { - const all: object = {} - const callbacks = new WeakMap< - AnyAtom | typeof all, - Set<(atom?: AnyAtom) => void> - >() - const notify = (atom: AnyAtom) => { - callbacks.get(all)?.forEach((fn) => fn(atom)) - callbacks.get(atom)?.forEach((fn) => fn()) - } - notify.add = (atom: AnyAtom | undefined, fn: (atom?: AnyAtom) => void) => { - const key = atom || all - const fns = ( - callbacks.has(key) ? callbacks : callbacks.set(key, new Set()) - ).get(key)! - fns.add(fn) - return () => { - fns?.delete(fn) - if (!fns.size) { - callbacks.delete(key) - } - } - } - return notify as StoreHookForAtoms -} - -const initializeStoreHooks = (storeHooks: StoreHooks): Required => { - type SH = { -readonly [P in keyof StoreHooks]: StoreHooks[P] } - ;(storeHooks as SH).c ||= createStoreHookForAtoms() - ;(storeHooks as SH).m ||= createStoreHookForAtoms() - ;(storeHooks as SH).u ||= createStoreHookForAtoms() - ;(storeHooks as SH).f ||= createStoreHook() - return storeHooks as Required -} - -// -// Main functions -// - -// Do not export this type. -type Store = { - get: (atom: Atom) => Value - set: ( - atom: WritableAtom, - ...args: Args - ) => Result - sub: (atom: AnyAtom, listener: () => void) => () => void - [BUILDING_BLOCKS]: BuildingBlocks -} - -const BUILDING_BLOCKS: unique symbol = Symbol() // no description intentionally - -const getBuildingBlocks = (store: unknown): BuildingBlocks => - (store as Store)[BUILDING_BLOCKS] - -const buildStore = (buildingBlocks: BuildingBlocks): Store => { - const [ flushCallbacks, recomputeInvalidatedAtoms, + setAtomStateValueOrPromise, readAtomState, + getMountedOrPendingDependents, + invalidateDependents, writeAtomState, + mountDependencies, mountAtom, unmountAtom, - ] = buildingBlocks + ] const readAtom = (atom: Atom): Value => returnAtomValue(readAtomState(atom)) @@ -852,13 +837,13 @@ const buildStore = (buildingBlocks: BuildingBlocks): Store => { } } - const store: Omit = { + const store: Store = { get: readAtom, set: writeAtom, sub: subscribeAtom, } Object.defineProperty(store, BUILDING_BLOCKS, { value: buildingBlocks }) - return store as Store + return store } // @@ -866,8 +851,6 @@ const buildStore = (buildingBlocks: BuildingBlocks): Store => { // export const INTERNAL_buildStore: typeof buildStore = buildStore -export const INTERNAL_createBuildingBlocksRev1: typeof createBuildingBlocks = - createBuildingBlocks export const INTERNAL_getBuildingBlocksRev1: typeof getBuildingBlocks = getBuildingBlocks export const INTERNAL_initializeStoreHooks: typeof initializeStoreHooks = diff --git a/src/vanilla/store.ts b/src/vanilla/store.ts index 6ac43cf4f1..906393cf4f 100644 --- a/src/vanilla/store.ts +++ b/src/vanilla/store.ts @@ -1,20 +1,12 @@ import type { Atom, WritableAtom } from './atom.ts' import { INTERNAL_buildStore, - INTERNAL_createBuildingBlocksRev1 as INTERNAL_createBuildingBlocks, INTERNAL_initializeStoreHooks, } from './internals.ts' -import type { INTERNAL_AtomState } from './internals.ts' +import type { INTERNAL_AtomState, INTERNAL_Store } from './internals.ts' // TODO: rename this to `Store` in the near future -export type INTERNAL_PrdStore = { - get: (atom: Atom) => Value - set: ( - atom: WritableAtom, - ...args: Args - ) => Result - sub: (atom: Atom, listener: () => void) => () => void -} +export type INTERNAL_PrdStore = INTERNAL_Store // For debugging purpose only // This will be removed in the near future @@ -33,8 +25,7 @@ const createDevStoreRev4 = (): INTERNAL_PrdStore & INTERNAL_DevStoreRev4 => { const storeHooks = INTERNAL_initializeStoreHooks({}) const atomStateMap = new WeakMap() const mountedAtoms = new WeakMap() - const buildingBlocks = INTERNAL_createBuildingBlocks( - () => store, + const store = INTERNAL_buildStore( atomStateMap, mountedAtoms, undefined, @@ -50,7 +41,6 @@ const createDevStoreRev4 = (): INTERNAL_PrdStore & INTERNAL_DevStoreRev4 => { return atom.write(get, set, ...args) }, ) - const store = INTERNAL_buildStore(buildingBlocks) const debugMountedAtoms = new Set>() storeHooks.m.add(undefined, (atom) => { debugMountedAtoms.add(atom) @@ -98,8 +88,7 @@ export const createStore = (): PrdOrDevStore => { if (import.meta.env?.MODE !== 'production') { return createDevStoreRev4() } - const buildingBlocks = INTERNAL_createBuildingBlocks(() => store) - const store = INTERNAL_buildStore(buildingBlocks) + const store = INTERNAL_buildStore() return store } diff --git a/tests/vanilla/derive.test.tsx b/tests/vanilla/derive.test.tsx index c038af97cc..be6c4d17db 100644 --- a/tests/vanilla/derive.test.tsx +++ b/tests/vanilla/derive.test.tsx @@ -3,23 +3,18 @@ import { atom, createStore } from 'jotai/vanilla' import type { Atom } from 'jotai/vanilla' import { INTERNAL_buildStore, - INTERNAL_createBuildingBlocksRev1 as INTERNAL_createBuildingBlocks, INTERNAL_getBuildingBlocksRev1 as INTERNAL_getBuildingBlocks, } from 'jotai/vanilla/internals' -type AtomStateMapType = ReturnType[6] +type AtomStateMapType = ReturnType[0] const deriveStore = ( store: ReturnType, enhanceAtomStateMap: (atomStateMap: AtomStateMapType) => AtomStateMapType, ): ReturnType => { const buildingBlocks = INTERNAL_getBuildingBlocks(store) - const atomStateMap = buildingBlocks[6] - const newBuildingBlocks = INTERNAL_createBuildingBlocks( - () => derivedStore, - enhanceAtomStateMap(atomStateMap), - ) - const derivedStore = INTERNAL_buildStore(newBuildingBlocks) + const atomStateMap = buildingBlocks[0] + const derivedStore = INTERNAL_buildStore(enhanceAtomStateMap(atomStateMap)) return derivedStore } diff --git a/tests/vanilla/effect.test.ts b/tests/vanilla/effect.test.ts index a0fb3f81f9..edbd36e5d0 100644 --- a/tests/vanilla/effect.test.ts +++ b/tests/vanilla/effect.test.ts @@ -59,7 +59,7 @@ function syncEffect(effect: Effect): Atom { } } const buildingBlocks = INTERNAL_getBuildingBlocks(store) - const storeHooks = INTERNAL_initializeStoreHooks(buildingBlocks[12]) + const storeHooks = INTERNAL_initializeStoreHooks(buildingBlocks[6]) const syncEffectChannel = ensureSyncEffectChannel(store) storeHooks.m.add(internalAtom, () => { // mount @@ -88,7 +88,7 @@ function ensureSyncEffectChannel(store: any) { if (!store[syncEffectChannelSymbol]) { store[syncEffectChannelSymbol] = new Set<() => void>() const buildingBlocks = INTERNAL_getBuildingBlocks(store) - const storeHooks = INTERNAL_initializeStoreHooks(buildingBlocks[12]) + const storeHooks = INTERNAL_initializeStoreHooks(buildingBlocks[6]) storeHooks.f.add(() => { const syncEffectChannel = store[syncEffectChannelSymbol] as Set< () => void diff --git a/tests/vanilla/store.test.tsx b/tests/vanilla/store.test.tsx index 5f584e6e67..f64ec2aa59 100644 --- a/tests/vanilla/store.test.tsx +++ b/tests/vanilla/store.test.tsx @@ -4,23 +4,18 @@ import { atom, createStore } from 'jotai/vanilla' import type { Atom, Getter, PrimitiveAtom } from 'jotai/vanilla' import { INTERNAL_buildStore, - INTERNAL_createBuildingBlocksRev1 as INTERNAL_createBuildingBlocks, INTERNAL_getBuildingBlocksRev1 as INTERNAL_getBuildingBlocks, } from 'jotai/vanilla/internals' -type AtomStateMapType = ReturnType[6] +type AtomStateMapType = ReturnType[0] const deriveStore = ( store: ReturnType, enhanceAtomStateMap: (atomStateMap: AtomStateMapType) => AtomStateMapType, ): ReturnType => { const buildingBlocks = INTERNAL_getBuildingBlocks(store) - const atomStateMap = buildingBlocks[6] - const newBuildingBlocks = INTERNAL_createBuildingBlocks( - () => derivedStore, - enhanceAtomStateMap(atomStateMap), - ) - const derivedStore = INTERNAL_buildStore(newBuildingBlocks) + const atomStateMap = buildingBlocks[0] + const derivedStore = INTERNAL_buildStore(enhanceAtomStateMap(atomStateMap)) return derivedStore }