Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1,691 changes: 0 additions & 1,691 deletions jotai-requirements-discussion-with-chatgpt.md

This file was deleted.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@
],
"license": "MIT",
"peerDependencies": {
"jotai": ">=2.16.0",
"jotai": ">=2.20.0",
"react": ">=16.0.0"
},
"devDependencies": {
Expand All @@ -73,8 +73,8 @@
"eslint-plugin-prettier": "^5.2.3",
"happy-dom": "^15.11.7",
"jiti": "^2.4.2",
"jotai": "2.16.0",
"jotai-effect": "2.1.2",
"jotai": "https://pkg.csb.dev/pmndrs/jotai/commit/ac38969c/jotai",
"jotai-effect": "https://pkg.csb.dev/jotaijs/jotai-effect/commit/6828183e/jotai-effect",
"jotai-family": "^1.0.0",
"prettier": "^3.4.2",
"typescript": "^5.7.2",
Expand Down
33 changes: 18 additions & 15 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

165 changes: 84 additions & 81 deletions src/ScopeProvider/scope.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { Atom, WritableAtom } from 'jotai'
import { atom as createAtom } from 'jotai'
import {
INTERNAL_buildStoreRev2 as buildStore,
INTERNAL_getBuildingBlocksRev2 as getBuildingBlocks,
INTERNAL_buildStoreRev3 as buildStore,
INTERNAL_getBuildingBlocksRev3 as getBuildingBlocks,
} from 'jotai/vanilla/internals'
import type {
INTERNAL_AtomState as AtomState,
Expand Down Expand Up @@ -248,53 +248,45 @@ function createPatchedStore(scope: Scope): Store {
const storeGet = storeState[21]
const storeSet = storeState[22]
const storeSub = storeState[23]
// const atomOnInit = storeState[9]
const alreadyPatched: StoreHooks = {}

// storeState[9] = (_, atom) => atomOnInit(scopedStore, atom)
// FIXME: revert to the above
storeState[9] = (_, atom) => {
// backwards compatibility for older versions of jotai
if ((atom as any).INTERNAL_onInit) {
;(atom as any).INTERNAL_onInit(scopedStore)
} else if ((atom as any).unstable_onInit) {
;(atom as any).unstable_onInit(scopedStore)
}
}
storeState[21] = patchStoreFn(storeGet)
storeState[22] = scopedSet
storeState[23] = patchStoreFn(storeSub)
storeState[24] = ([...buildingBlocks]) => {
const patchedBuildingBlocks: BuildingBlocks = [
patchWeakMap(buildingBlocks[0], patchGetAtomState), // atomStateMap
patchWeakMap(buildingBlocks[1], patchGetMounted), // mountedMap
patchWeakMap(buildingBlocks[2]), // invalidatedAtoms
patchSet(buildingBlocks[3]), // changedAtoms
buildingBlocks[4], // mountCallbacks
buildingBlocks[5], // unmountCallbacks
patchStoreHooks(buildingBlocks[6]), // storeHooks
patchStoreFn(buildingBlocks[7]), // atomRead
patchStoreFn(buildingBlocks[8]), // atomWrite
buildingBlocks[9], // atomOnInit
patchStoreFn(buildingBlocks[10]), // atomOnMount
patchStoreFn(
buildingBlocks[11], // ensureAtomState
(fn) => patchEnsureAtomState(patchedBuildingBlocks[0], fn)
),
buildingBlocks[12], // flushCallbacks
buildingBlocks[13], // recomputeInvalidatedAtoms
patchStoreFn(buildingBlocks[14]), // readAtomState
patchStoreFn(buildingBlocks[15]), // invalidateDependents
patchStoreFn(buildingBlocks[16]), // writeAtomState
patchStoreFn(buildingBlocks[17]), // mountDependencies
patchStoreFn(buildingBlocks[18]), // mountAtom
patchStoreFn(buildingBlocks[19]), // unmountAtom
patchStoreFn(buildingBlocks[20]), // setAtomStateValueOrPromise
patchStoreFn(buildingBlocks[21]), // getAtom
patchStoreFn(buildingBlocks[22]), // setAtom
patchStoreFn(buildingBlocks[23]), // subAtom
() => buildingBlocks, // enhanceBuildingBlocks (raw)
...(buildingBlocks.slice(25) as never), // rest of building blocks
let patchedAtomStateMap: AtomStateMap | undefined
let patchedBuildingBlocks: BuildingBlocks | undefined
storeState[24] = function enhanceScopedBuildingBlocks(buildingBlocks) {
patchedAtomStateMap ??= patchWeakMapLike(buildingBlocks[0], patchGetAtomState)
patchedBuildingBlocks ??= [
patchedAtomStateMap, // atomStateMap
patchWeakMapLike(buildingBlocks[1], patchGetMounted), // mountedMap
patchWeakMapLike(buildingBlocks[2]), // invalidatedAtoms
patchSetLike(buildingBlocks[3]), // changedAtoms
buildingBlocks[4], // mountCallbacks
buildingBlocks[5], // unmountCallbacks
patchStoreHooks(buildingBlocks[6]), // storeHooks
patchStoreFn(buildingBlocks[7]), // atomRead
patchStoreFn(buildingBlocks[8]), // atomWrite
patchStoreFn(buildingBlocks[9]), // atomOnInit
patchStoreFn(buildingBlocks[10]), // atomOnMount
patchEnsureAtomState(patchedAtomStateMap, buildingBlocks[11]), // ensureAtomState
(_, ...args) => buildingBlocks[12](storeState, ...args), // flushCallbacks
(_, ...args) => buildingBlocks[13](storeState, ...args), // recomputeInvalidatedAtoms
patchStoreFn(buildingBlocks[14]), // readAtomState
patchStoreFn(buildingBlocks[15]), // invalidateDependents
patchStoreFn(buildingBlocks[16]), // writeAtomState
patchStoreFn(buildingBlocks[17]), // mountDependencies
patchStoreFn(buildingBlocks[18]), // mountAtom
patchStoreFn(buildingBlocks[19]), // unmountAtom
patchStoreFn(buildingBlocks[20]), // setAtomStateValueOrPromise
patchStoreFn(buildingBlocks[21]), // getAtom
patchStoreFn(buildingBlocks[22]), // setAtom
patchStoreFn(buildingBlocks[23]), // subAtom
() => buildingBlocks, // enhanceBuildingBlocks (raw)
buildingBlocks[25], // abortHandlersMap
(_, ...args) => buildingBlocks[26](storeState, ...args), // registerAbortHandler
(_, ...args) => buildingBlocks[27](storeState, ...args), // abortPromise
buildingBlocks[28], // storeEpochHolder
...(buildingBlocks.slice(25) as never), // rest of building blocks
]
return patchedBuildingBlocks
}
Expand All @@ -314,31 +306,21 @@ function createPatchedStore(scope: Scope): Store {
if (!atomState) {
return undefined
}
patchedAtomState = {
...atomState,
d: patchWeakMap(atomState.d, function patchGetDependency(fn) {
return (k) => fn(getAtom(scope, k)[0])
}),
p: patchSet(atomState.p),
get n() {
return atomState.n
},
set n(v) {
atomState.n = v
},
get v() {
return atomState.v
},
set v(v) {
atomState.v = v
let patchedD: AtomState['d'] | undefined
let patchedP: AtomState['p'] | undefined
patchedAtomState = new Proxy(atomState, {
get(_target, prop) {
if (prop === 'd') {
return (patchedD ??= patchWeakMapLike(atomState.d, function patchGetDependency(fn) {
return (k) => fn(getAtom(scope, k)[0])
}) as AtomState['d'])
}
if (prop === 'p') {
return (patchedP ??= patchSetLike(atomState.p) as AtomState['p'])
}
return Reflect.get(atomState, prop) as never
},
get e() {
return atomState.e
},
set e(v) {
atomState.e = v
},
} as AtomState
}) as AtomState
patchedASM.set(atom, patchedAtomState)
return patchedAtomState
} as T
Expand All @@ -357,8 +339,8 @@ function createPatchedStore(scope: Scope): Store {
}
patchedMounted = {
...mounted,
d: patchSet(mounted.d),
t: patchSet(mounted.t),
d: patchSetLike(mounted.d),
t: patchSetLike(mounted.t),
get u() {
return mounted.u
},
Expand All @@ -371,26 +353,29 @@ function createPatchedStore(scope: Scope): Store {
} as T
}

function patchEnsureAtomState(patchedASM: AtomStateMap, fn: EnsureAtomState) {
return function patchedEnsureAtomState(store, atom) {
function patchEnsureAtomState(patchedASM: AtomStateMap, ensureAtomState: EnsureAtomState) {
const patchedEnsureAtomState = patchStoreFn(ensureAtomState)
return function ensureAtomStateWrapper(buildingBlocks, store, atom) {
const patchedAtomState = patchedASM.get(atom)
if (patchedAtomState) {
return patchedAtomState
}
patchedASM.set(atom, fn(store, atom))
const atomState = patchedEnsureAtomState(buildingBlocks, store, atom)
patchedASM.set(atom, atomState)
return patchedASM.get(atom)
} as EnsureAtomState
}

function scopedSet<Value, Args extends any[], Result>(
buildingBlocks: Readonly<BuildingBlocks>,
store: Store,
atom: WritableAtom<Value, Args, Result>,
...args: Args
): Result {
const [scopedAtom, implicitScope] = getAtom(scope, atom)
const restore = prepareWriteAtom(scope, scopedAtom, atom, implicitScope, scope)
try {
return storeSet(store, scopedAtom as typeof atom, ...args)
return storeSet(buildingBlocks, store, scopedAtom as typeof atom, ...args)
} finally {
restore?.()
}
Expand All @@ -404,25 +389,43 @@ function createPatchedStore(scope: Scope): Store {
} as T
}

function patchStoreFn<T extends (...args: any[]) => any>(fn: T, patch?: (fn: T) => T) {
return function scopedStoreFn(store, atom, ...args) {
function patchStoreFn<T extends (...args: any[]) => any>(fn: T) {
return function scopedStoreFn(
_buildingBlocks: Readonly<BuildingBlocks>,
store: Store,
atom: AnyAtom,
...args: any[]
) {
const [scopedAtom] = getAtom(scope, atom)
const f = patch ? patch(fn) : fn
return f(store, scopedAtom, ...args)
return fn(storeState, store, scopedAtom, ...args)
} as T
}

function patchWeakMap<T extends WeakMapLike<AnyAtom, unknown>>(wm: T, patch?: (fn: T['get']) => T['get']): T {
function patchWeakMapLike<T extends WeakMapLike<AnyAtom, unknown>>(wm: T, patch?: (fn: T['get']) => T['get']): T {
const patchedWm: WeakMapLike<AnyAtom, unknown> = {
get: patchAtomFn(wm.get.bind(wm), patch),
set: patchAtomFn(wm.set.bind(wm)),
has: patchAtomFn(wm.has.bind(wm)),
delete: patchAtomFn(wm.delete.bind(wm)),
}
if (typeof (wm as unknown as Map<AnyAtom, unknown>).keys === 'function') {
const map = wm as unknown as Map<AnyAtom, unknown>
const wmExt = patchedWm as WeakMapLike<AnyAtom, unknown> & {
keys: () => IterableIterator<AnyAtom>
[Symbol.iterator]: () => IterableIterator<[AnyAtom, unknown]>
}
wmExt.keys = map.keys.bind(map)
wmExt[Symbol.iterator] = map[Symbol.iterator].bind(map)
Object.defineProperty(patchedWm, 'size', {
enumerable: false,
configurable: true,
get: () => map.size,
})
}
return patchedWm as T
}

function patchSet(s: SetLike<AnyAtom>) {
function patchSetLike(s: SetLike<AnyAtom>) {
return {
get size() {
return s.size
Expand Down
48 changes: 48 additions & 0 deletions tests/effect.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import { createStore } from 'jotai'
import { atomWithReducer } from 'jotai/utils'
import { atomEffect } from 'jotai-effect'
import { createScope } from 'jotai-scope'
// eslint-disable-next-line import/order
import { describe, expect, test, vi } from 'vitest'

describe('atomEffect', () => {
test('should work with atomEffect', () => {
const a = atomWithReducer(0, (v) => v + 1)
a.debugLabel = 'atomA'
const e = atomEffect((_get, set) => {
set(a)
})
e.debugLabel = 'effect'
const s0 = createStore()
const s1 = createScope({ atoms: [a], parentStore: s0, name: 's1' })
s1.sub(e, () => {})
s1.sub(a, () => {})
expect(s0.get(a)).toBe(0)
expect(s1.get(a)).toBe(1)
})

test('should work with atomEffect in a scope', async () => {
const a = atomWithReducer(0, (v) => v + 1)
a.debugLabel = 'atomA'
const b = atomWithReducer(0, (v) => v + 1)
b.debugLabel = 'atomB'
const fn = vi.fn()
const listener = atomEffect((get) => {
fn(get(a))
})
listener.debugLabel = 'listener'
const e = atomEffect((get, set) => {
get(b)
set(a)
})
e.debugLabel = 'effect'
const s0 = createStore()
const s1 = createScope({ atoms: [a, b], parentStore: s0, name: 's1' })
s1.sub(listener, () => {})
s1.sub(e, () => {})
s1.sub(a, () => {})
expect(fn).toHaveBeenLastCalledWith(1)
s1.set(b)
expect(fn).toHaveBeenLastCalledWith(2)
})
})
Loading
Loading