Skip to content

Commit e513fe3

Browse files
authored
Merge branch 'main' into test/migrate-to-vitest-fake-timer
2 parents dbf8b5e + 4afc31c commit e513fe3

File tree

4 files changed

+101
-83
lines changed

4 files changed

+101
-83
lines changed

src/vanilla/internals.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,8 @@ type StoreHookForAtoms = {
330330

331331
/** StoreHooks are an experimental API. */
332332
type StoreHooks = {
333+
/** Listener to notify when the atom is initialized. */
334+
readonly i?: StoreHookForAtoms
333335
/** Listener to notify when the atom is read. */
334336
readonly r?: StoreHookForAtoms
335337
/** Listener to notify when the atom value is changed. */
@@ -380,6 +382,7 @@ const createStoreHookForAtoms = (): StoreHookForAtoms => {
380382

381383
function initializeStoreHooks(storeHooks: StoreHooks): Required<StoreHooks> {
382384
type SH = { -readonly [P in keyof StoreHooks]: StoreHooks[P] }
385+
;(storeHooks as SH).i ||= createStoreHookForAtoms()
383386
;(storeHooks as SH).r ||= createStoreHookForAtoms()
384387
;(storeHooks as SH).c ||= createStoreHookForAtoms()
385388
;(storeHooks as SH).m ||= createStoreHookForAtoms()
@@ -401,6 +404,7 @@ const atomOnMount: AtomOnMount = (_store, atom, setAtom) =>
401404
const ensureAtomState: EnsureAtomState = (store, atom) => {
402405
const buildingBlocks = getInternalBuildingBlocks(store)
403406
const atomStateMap = buildingBlocks[0]
407+
const storeHooks = buildingBlocks[6]
404408
const atomOnInit = buildingBlocks[9]
405409
if (import.meta.env?.MODE !== 'production' && !atom) {
406410
throw new Error('Atom is undefined or null')
@@ -409,6 +413,7 @@ const ensureAtomState: EnsureAtomState = (store, atom) => {
409413
if (!atomState) {
410414
atomState = { d: new Map(), p: new Set(), n: 0 }
411415
atomStateMap.set(atom, atomState)
416+
storeHooks.i?.(atom)
412417
atomOnInit?.(store, atom)
413418
}
414419
return atomState as never

src/vanilla/utils/loadable.ts

Lines changed: 40 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -21,51 +21,52 @@ export function loadable<Value>(anAtom: Atom<Value>): Atom<Loadable<Value>> {
2121
PromiseLike<Awaited<Value>>,
2222
Loadable<Value>
2323
>()
24-
const refreshAtom = atom(0)
24+
const refreshAtom = atom([() => {}, 0] as [() => void, number])
25+
refreshAtom.unstable_onInit = (store) => {
26+
store.set(refreshAtom, ([, c]) => [
27+
() => store.set(refreshAtom, ([f, c]) => [f, c + 1]),
28+
c,
29+
])
30+
}
2531

2632
if (import.meta.env?.MODE !== 'production') {
2733
refreshAtom.debugPrivate = true
2834
}
2935

30-
const derivedAtom = atom(
31-
(get, { setSelf }) => {
32-
get(refreshAtom)
33-
let value: Value
34-
try {
35-
value = get(anAtom)
36-
} catch (error) {
37-
return { state: 'hasError', error } as Loadable<Value>
38-
}
39-
if (!isPromiseLike<Value>(value)) {
40-
return { state: 'hasData', data: value } as Loadable<Value>
41-
}
42-
const promise = value
43-
const cached1 = loadableCache.get(promise)
44-
if (cached1) {
45-
return cached1
46-
}
47-
promise.then(
48-
(data) => {
49-
loadableCache.set(promise, { state: 'hasData', data })
50-
setSelf()
51-
},
52-
(error) => {
53-
loadableCache.set(promise, { state: 'hasError', error })
54-
setSelf()
55-
},
56-
)
36+
const derivedAtom = atom((get) => {
37+
const [triggerRefresh] = get(refreshAtom)
38+
let value: Value
39+
try {
40+
value = get(anAtom)
41+
} catch (error) {
42+
return { state: 'hasError', error } as Loadable<Value>
43+
}
44+
if (!isPromiseLike<Value>(value)) {
45+
return { state: 'hasData', data: value } as Loadable<Value>
46+
}
47+
const promise = value
48+
const cached1 = loadableCache.get(promise)
49+
if (cached1) {
50+
return cached1
51+
}
52+
promise.then(
53+
(data) => {
54+
loadableCache.set(promise, { state: 'hasData', data })
55+
triggerRefresh()
56+
},
57+
(error) => {
58+
loadableCache.set(promise, { state: 'hasError', error })
59+
triggerRefresh()
60+
},
61+
)
5762

58-
const cached2 = loadableCache.get(promise)
59-
if (cached2) {
60-
return cached2
61-
}
62-
loadableCache.set(promise, LOADING as Loadable<Value>)
63-
return LOADING as Loadable<Value>
64-
},
65-
(_get, set) => {
66-
set(refreshAtom, (c) => c + 1)
67-
},
68-
)
63+
const cached2 = loadableCache.get(promise)
64+
if (cached2) {
65+
return cached2
66+
}
67+
loadableCache.set(promise, LOADING as Loadable<Value>)
68+
return LOADING as Loadable<Value>
69+
})
6970

7071
if (import.meta.env?.MODE !== 'production') {
7172
derivedAtom.debugPrivate = true

src/vanilla/utils/unwrap.ts

Lines changed: 45 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -47,57 +47,58 @@ export function unwrap<Value, Args extends unknown[], Result, PendingValue>(
4747
PromiseLike<unknown>,
4848
Awaited<Value>
4949
>()
50-
const refreshAtom = atom(0)
50+
const refreshAtom = atom([() => {}, 0] as [() => void, number])
51+
refreshAtom.unstable_onInit = (store) => {
52+
store.set(refreshAtom, ([, c]) => [
53+
() => store.set(refreshAtom, ([f, c]) => [f, c + 1]),
54+
c,
55+
])
56+
}
5157

5258
if (import.meta.env?.MODE !== 'production') {
5359
refreshAtom.debugPrivate = true
5460
}
5561

56-
const promiseAndValueAtom: WritableAtom<PromiseAndValue, [], void> & {
62+
const promiseAndValueAtom: Atom<PromiseAndValue> & {
5763
init?: undefined
58-
} = atom(
59-
(get, { setSelf }) => {
60-
get(refreshAtom)
61-
let prev: PromiseAndValue | undefined
62-
try {
63-
prev = get(promiseAndValueAtom) as PromiseAndValue | undefined
64-
} catch {
65-
// ignore previous errors to avoid getting stuck in error state
66-
}
67-
const promise = get(anAtom)
68-
if (!isPromiseLike(promise)) {
69-
return { v: promise as Awaited<Value> }
70-
}
71-
if (promise !== prev?.p) {
72-
promise.then(
73-
(v) => {
74-
promiseResultCache.set(promise, v as Awaited<Value>)
75-
setSelf()
76-
},
77-
(e) => {
78-
promiseErrorCache.set(promise, e)
79-
setSelf()
80-
},
81-
)
82-
}
83-
if (promiseErrorCache.has(promise)) {
84-
throw promiseErrorCache.get(promise)
85-
}
86-
if (promiseResultCache.has(promise)) {
87-
return {
88-
p: promise,
89-
v: promiseResultCache.get(promise) as Awaited<Value>,
90-
}
64+
} = atom((get) => {
65+
const [triggerRefresh] = get(refreshAtom)
66+
let prev: PromiseAndValue | undefined
67+
try {
68+
prev = get(promiseAndValueAtom) as PromiseAndValue | undefined
69+
} catch {
70+
// ignore previous errors to avoid getting stuck in error state
71+
}
72+
const promise = get(anAtom)
73+
if (!isPromiseLike(promise)) {
74+
return { v: promise as Awaited<Value> }
75+
}
76+
if (promise !== prev?.p) {
77+
promise.then(
78+
(v) => {
79+
promiseResultCache.set(promise, v as Awaited<Value>)
80+
triggerRefresh()
81+
},
82+
(e) => {
83+
promiseErrorCache.set(promise, e)
84+
triggerRefresh()
85+
},
86+
)
87+
}
88+
if (promiseErrorCache.has(promise)) {
89+
throw promiseErrorCache.get(promise)
90+
}
91+
if (promiseResultCache.has(promise)) {
92+
return {
93+
p: promise,
94+
v: promiseResultCache.get(promise) as Awaited<Value>,
9195
}
92-
if (prev && 'v' in prev) {
93-
return { p: promise, f: fallback(prev.v), v: prev.v }
94-
}
95-
return { p: promise, f: fallback() }
96-
},
97-
(_get, set) => {
98-
set(refreshAtom, (c) => c + 1)
99-
},
100-
)
96+
}
97+
if (prev && 'v' in prev) {
98+
return { p: promise, f: fallback(prev.v), v: prev.v }
99+
}
100+
return { p: promise, f: fallback() }
101+
})
101102
// HACK to read PromiseAndValue atom before initialization
102103
promiseAndValueAtom.init = undefined
103104

tests/vanilla/internals.test.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ describe('store hooks', () => {
106106
return { store, storeHooks }
107107
}
108108

109+
describe('init hook (i)', () => {
110+
it('should call init hook when atom state is initialized', () => {
111+
const { store, storeHooks } = createStoreWithHooks()
112+
const baseAtom = atom(0)
113+
const initCallback = vi.fn()
114+
storeHooks.i.add(baseAtom, initCallback)
115+
store.get(baseAtom)
116+
expect(initCallback).toHaveBeenCalledTimes(1)
117+
})
118+
})
119+
109120
describe('read hook (r)', () => {
110121
it('should call read hook when atom is read', () => {
111122
const { store, storeHooks } = createStoreWithHooks()

0 commit comments

Comments
 (0)