Skip to content

Commit 6f2ae77

Browse files
authored
refactor(core): avoid onCancel hack (#3019)
* wip: refactor: avoid onCancel hack * fix typo * fix useAtomValue * refactor internals * refactor agian to keep the behavior * small refactor * refactor a bit * recover a line * another refactor
1 parent e0289af commit 6f2ae77

File tree

2 files changed

+55
-65
lines changed

2 files changed

+55
-65
lines changed

src/react/useAtomValue.ts

+23-19
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
/// <reference types="react/experimental" />
22

33
import ReactExports, { useDebugValue, useEffect, useReducer } from 'react'
4+
import { INTERNAL_registerAbortHandler as registerAbortHandler } from '../vanilla/internals.ts'
45
import type { Atom, ExtractAtomValue } from '../vanilla.ts'
56
import { useStore } from './Provider.ts'
67

@@ -55,7 +56,10 @@ const continuablePromiseMap = new WeakMap<
5556
Promise<unknown>
5657
>()
5758

58-
const createContinuablePromise = <T>(promise: PromiseLike<T>) => {
59+
const createContinuablePromise = <T>(
60+
promise: PromiseLike<T>,
61+
getValue: () => PromiseLike<T> | T,
62+
) => {
5963
let continuablePromise = continuablePromiseMap.get(promise)
6064
if (!continuablePromise) {
6165
continuablePromise = new Promise<T>((resolve, reject) => {
@@ -70,25 +74,23 @@ const createContinuablePromise = <T>(promise: PromiseLike<T>) => {
7074
reject(e)
7175
}
7276
}
73-
const registerCancelHandler = (p: PromiseLike<T>) => {
74-
if ('onCancel' in p && typeof p.onCancel === 'function') {
75-
p.onCancel((nextValue: PromiseLike<T> | T) => {
76-
if (import.meta.env?.MODE !== 'production' && nextValue === p) {
77-
throw new Error('[Bug] p is not updated even after cancelation')
78-
}
79-
if (isPromiseLike(nextValue)) {
80-
continuablePromiseMap.set(nextValue, continuablePromise!)
81-
curr = nextValue
82-
nextValue.then(onFulfilled(nextValue), onRejected(nextValue))
83-
registerCancelHandler(nextValue)
84-
} else {
85-
resolve(nextValue)
86-
}
87-
})
77+
const onAbort = () => {
78+
try {
79+
const nextValue = getValue()
80+
if (isPromiseLike(nextValue)) {
81+
continuablePromiseMap.set(nextValue, continuablePromise!)
82+
curr = nextValue
83+
nextValue.then(onFulfilled(nextValue), onRejected(nextValue))
84+
registerAbortHandler(nextValue, onAbort)
85+
} else {
86+
resolve(nextValue)
87+
}
88+
} catch (e) {
89+
reject(e)
8890
}
8991
}
9092
promise.then(onFulfilled(promise), onRejected(promise))
91-
registerCancelHandler(promise)
93+
registerAbortHandler(promise, onAbort)
9294
})
9395
continuablePromiseMap.set(promise, continuablePromise)
9496
}
@@ -141,7 +143,9 @@ export function useAtomValue<Value>(atom: Atom<Value>, options?: Options) {
141143
if (typeof delay === 'number') {
142144
const value = store.get(atom)
143145
if (isPromiseLike(value)) {
144-
attachPromiseMeta(createContinuablePromise(value))
146+
attachPromiseMeta(
147+
createContinuablePromise(value, () => store.get(atom)),
148+
)
145149
}
146150
// delay rerendering to wait a promise possibly to resolve
147151
setTimeout(rerender, delay)
@@ -157,7 +161,7 @@ export function useAtomValue<Value>(atom: Atom<Value>, options?: Options) {
157161
// The use of isPromiseLike is to be consistent with `use` type.
158162
// `instanceof Promise` actually works fine in this case.
159163
if (isPromiseLike(value)) {
160-
const promise = createContinuablePromise(value)
164+
const promise = createContinuablePromise(value, () => store.get(atom))
161165
return use(promise)
162166
}
163167
return value as Awaited<Value>

src/vanilla/internals.ts

+32-46
Original file line numberDiff line numberDiff line change
@@ -176,54 +176,42 @@ const returnAtomValue = <Value>(atomState: AtomState<Value>): Value => {
176176
}
177177

178178
//
179-
// Cancelable Promise
180-
// TODO(daishi): revisit this implementation
179+
// Abortable Promise
181180
//
182181

183-
type CancelHandler = (nextValue: unknown) => void
184-
type PromiseState = [cancelHandlers: Set<CancelHandler>, settled: boolean]
185-
186-
const PROMISE_STATE = Symbol()
187-
188-
const getPromiseState = <T>(
189-
promise: PromiseLike<T>,
190-
): PromiseState | undefined => (promise as any)[PROMISE_STATE]
182+
const promiseStateMap: WeakMap<
183+
PromiseLike<unknown>,
184+
[pending: boolean, abortHandlers: Set<() => void>]
185+
> = new WeakMap()
191186

192187
const isPendingPromise = (value: unknown): value is PromiseLike<unknown> =>
193-
isPromiseLike(value) && !getPromiseState(value)?.[1]
188+
isPromiseLike(value) && !!promiseStateMap.get(value as never)?.[0]
194189

195-
const cancelPromise = <T>(
196-
promise: PromiseLike<T>,
197-
nextValue: unknown,
198-
): void => {
199-
const promiseState = getPromiseState(promise)
200-
if (promiseState) {
201-
promiseState[1] = true
202-
promiseState[0].forEach((fn) => fn(nextValue))
203-
} else if (import.meta.env?.MODE !== 'production') {
204-
throw new Error('[Bug] cancelable promise not found')
190+
const abortPromise = <T>(promise: PromiseLike<T>): void => {
191+
const promiseState = promiseStateMap.get(promise)
192+
if (promiseState?.[0]) {
193+
promiseState[0] = false
194+
promiseState[1].forEach((fn) => fn())
205195
}
206196
}
207197

208-
const patchPromiseForCancelability = <T>(promise: PromiseLike<T>): void => {
209-
if (getPromiseState(promise)) {
210-
// already patched
211-
return
212-
}
213-
const promiseState: PromiseState = [new Set(), false]
214-
;(promise as any)[PROMISE_STATE] = promiseState
215-
const settle = () => {
216-
promiseState[1] = true
217-
}
218-
promise.then(settle, settle)
219-
;(promise as { onCancel?: (fn: CancelHandler) => void }).onCancel = (fn) => {
220-
promiseState[0].add(fn)
198+
const registerAbortHandler = <T>(
199+
promise: PromiseLike<T>,
200+
abortHandler: () => void,
201+
): void => {
202+
let promiseState = promiseStateMap.get(promise)
203+
if (!promiseState) {
204+
promiseState = [true, new Set()]
205+
promiseStateMap.set(promise, promiseState)
206+
const settle = () => {
207+
promiseState![0] = false
208+
}
209+
promise.then(settle, settle)
221210
}
211+
promiseState[1].add(abortHandler)
222212
}
223213

224-
const isPromiseLike = (
225-
p: unknown,
226-
): p is PromiseLike<unknown> & { onCancel?: (fn: CancelHandler) => void } =>
214+
const isPromiseLike = (p: unknown): p is PromiseLike<unknown> =>
227215
typeof (p as any)?.then === 'function'
228216

229217
const addPendingPromiseToDependency = (
@@ -253,9 +241,7 @@ const setAtomStateValueOrPromise = (
253241
const atomState = ensureAtomState(atom)
254242
const hasPrevValue = 'v' in atomState
255243
const prevValue = atomState.v
256-
const pendingPromise = isPendingPromise(atomState.v) ? atomState.v : null
257244
if (isPromiseLike(valueOrPromise)) {
258-
patchPromiseForCancelability(valueOrPromise)
259245
for (const a of atomState.d.keys()) {
260246
addPendingPromiseToDependency(atom, valueOrPromise, ensureAtomState(a))
261247
}
@@ -264,8 +250,8 @@ const setAtomStateValueOrPromise = (
264250
delete atomState.e
265251
if (!hasPrevValue || !Object.is(prevValue, atomState.v)) {
266252
++atomState.n
267-
if (pendingPromise) {
268-
cancelPromise(pendingPromise, valueOrPromise)
253+
if (isPromiseLike(prevValue)) {
254+
abortPromise(prevValue)
269255
}
270256
}
271257
}
@@ -662,7 +648,7 @@ const buildStore = (
662648
const valueOrPromise = atomRead(atom, getter, options as never)
663649
setAtomStateValueOrPromise(atom, valueOrPromise, ensureAtomState!)
664650
if (isPromiseLike(valueOrPromise)) {
665-
valueOrPromise.onCancel?.(() => controller?.abort())
651+
registerAbortHandler(valueOrPromise, () => controller?.abort())
666652
valueOrPromise.then(
667653
mountDependenciesIfAsync,
668654
mountDependenciesIfAsync,
@@ -931,12 +917,12 @@ export const INTERNAL_isActuallyWritableAtom: typeof isActuallyWritableAtom =
931917
export const INTERNAL_isAtomStateInitialized: typeof isAtomStateInitialized =
932918
isAtomStateInitialized
933919
export const INTERNAL_returnAtomValue: typeof returnAtomValue = returnAtomValue
934-
export const INTERNAL_getPromiseState: typeof getPromiseState = getPromiseState
920+
export const INTERNAL_promiseStateMap: typeof promiseStateMap = promiseStateMap
935921
export const INTERNAL_isPendingPromise: typeof isPendingPromise =
936922
isPendingPromise
937-
export const INTERNAL_cancelPromise: typeof cancelPromise = cancelPromise
938-
export const INTERNAL_patchPromiseForCancelability: typeof patchPromiseForCancelability =
939-
patchPromiseForCancelability
923+
export const INTERNAL_abortPromise: typeof abortPromise = abortPromise
924+
export const INTERNAL_registerAbortHandler: typeof registerAbortHandler =
925+
registerAbortHandler
940926
export const INTERNAL_isPromiseLike: typeof isPromiseLike = isPromiseLike
941927
export const INTERNAL_addPendingPromiseToDependency: typeof addPendingPromiseToDependency =
942928
addPendingPromiseToDependency

0 commit comments

Comments
 (0)