317 lines
8.0 KiB
Plaintext
317 lines
8.0 KiB
Plaintext
import { ReactiveFlags, createReactiveSystem } from './alien'
|
|
|
|
import type { ReactiveNode } from './alien'
|
|
import type {
|
|
Atom,
|
|
AtomOptions,
|
|
Observer,
|
|
ReadonlyAtom,
|
|
Subscription,
|
|
} from './types'
|
|
|
|
export function toObserver<T>(
|
|
nextHandler?: Observer<T> | ((value: T) => void),
|
|
errorHandler?: (error: any) => void,
|
|
completionHandler?: () => void,
|
|
): Observer<T> {
|
|
const isObserver = typeof nextHandler === 'object'
|
|
const self = isObserver ? nextHandler : undefined
|
|
|
|
return {
|
|
next: (isObserver ? nextHandler.next : nextHandler)?.bind(self),
|
|
error: (isObserver ? nextHandler.error : errorHandler)?.bind(self),
|
|
complete: (isObserver ? nextHandler.complete : completionHandler)?.bind(
|
|
self,
|
|
),
|
|
}
|
|
}
|
|
|
|
interface InternalAtom<T> extends ReactiveNode {
|
|
_snapshot: T
|
|
_update: (getValue?: T | ((snapshot: T) => T)) => boolean
|
|
get: () => T
|
|
subscribe: (observerOrFn: Observer<T> | ((value: T) => void)) => Subscription
|
|
}
|
|
|
|
const queuedEffects: Array<Effect | undefined> = []
|
|
let cycle = 0
|
|
const { link, unlink, propagate, checkDirty, shallowPropagate } =
|
|
createReactiveSystem({
|
|
update(atom: InternalAtom<any>): boolean {
|
|
return atom._update()
|
|
},
|
|
// eslint-disable-next-line no-shadow
|
|
notify(effect: Effect): void {
|
|
queuedEffects[queuedEffectsLength++] = effect
|
|
effect.flags &= ~ReactiveFlags.Watching
|
|
},
|
|
unwatched(atom: InternalAtom<any>): void {
|
|
if (atom.depsTail !== undefined) {
|
|
atom.depsTail = undefined
|
|
atom.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty
|
|
purgeDeps(atom)
|
|
}
|
|
},
|
|
})
|
|
|
|
let notifyIndex = 0
|
|
let queuedEffectsLength = 0
|
|
let activeSub: ReactiveNode | undefined
|
|
let batchDepth = 0
|
|
|
|
export function batch(fn: () => void) {
|
|
try {
|
|
++batchDepth
|
|
fn()
|
|
} finally {
|
|
if (!--batchDepth) {
|
|
flush()
|
|
}
|
|
}
|
|
}
|
|
|
|
function purgeDeps(sub: ReactiveNode) {
|
|
const depsTail = sub.depsTail
|
|
let dep = depsTail !== undefined ? depsTail.nextDep : sub.deps
|
|
while (dep !== undefined) {
|
|
dep = unlink(dep, sub)
|
|
}
|
|
}
|
|
|
|
export function flush(): void {
|
|
if (batchDepth > 0) {
|
|
return
|
|
}
|
|
while (notifyIndex < queuedEffectsLength) {
|
|
// eslint-disable-next-line no-shadow
|
|
const effect = queuedEffects[notifyIndex]!
|
|
queuedEffects[notifyIndex++] = undefined
|
|
effect.notify()
|
|
}
|
|
notifyIndex = 0
|
|
queuedEffectsLength = 0
|
|
}
|
|
|
|
type AsyncAtomState<TData, TError = unknown> =
|
|
| { status: 'pending' }
|
|
| { status: 'done'; data: TData }
|
|
| { status: 'error'; error: TError }
|
|
|
|
export function createAsyncAtom<T>(
|
|
getValue: () => Promise<T>,
|
|
options?: AtomOptions<AsyncAtomState<T>>,
|
|
): ReadonlyAtom<AsyncAtomState<T>> {
|
|
const ref: { current?: InternalAtom<AsyncAtomState<T>> } = {}
|
|
const atom = createAtom<AsyncAtomState<T>>(() => {
|
|
getValue().then(
|
|
(data) => {
|
|
const internalAtom = ref.current!
|
|
if (internalAtom._update({ status: 'done', data })) {
|
|
const subs = internalAtom.subs
|
|
if (subs !== undefined) {
|
|
propagate(subs)
|
|
shallowPropagate(subs)
|
|
flush()
|
|
}
|
|
}
|
|
},
|
|
(error) => {
|
|
const internalAtom = ref.current!
|
|
if (internalAtom._update({ status: 'error', error })) {
|
|
const subs = internalAtom.subs
|
|
if (subs !== undefined) {
|
|
propagate(subs)
|
|
shallowPropagate(subs)
|
|
flush()
|
|
}
|
|
}
|
|
},
|
|
)
|
|
|
|
return { status: 'pending' }
|
|
}, options)
|
|
ref.current = atom as unknown as InternalAtom<AsyncAtomState<T>>
|
|
|
|
return atom
|
|
}
|
|
|
|
export function createAtom<T>(
|
|
getValue: (prev?: NoInfer<T>) => T,
|
|
options?: AtomOptions<T>,
|
|
): ReadonlyAtom<T>
|
|
export function createAtom<T>(
|
|
initialValue: T,
|
|
options?: AtomOptions<T>,
|
|
): Atom<T>
|
|
export function createAtom<T>(
|
|
valueOrFn: T | ((prev?: T) => T),
|
|
options?: AtomOptions<T>,
|
|
): Atom<T> | ReadonlyAtom<T> {
|
|
const isComputed = typeof valueOrFn === 'function'
|
|
const getter = valueOrFn as (prev?: T) => T
|
|
|
|
// Create plain object atom
|
|
const atom: InternalAtom<T> = {
|
|
_snapshot: isComputed ? undefined! : valueOrFn,
|
|
|
|
subs: undefined,
|
|
subsTail: undefined,
|
|
deps: undefined,
|
|
depsTail: undefined,
|
|
flags: isComputed ? ReactiveFlags.None : ReactiveFlags.Mutable,
|
|
|
|
get(): T {
|
|
if (activeSub !== undefined) {
|
|
link(atom, activeSub, cycle)
|
|
}
|
|
return atom._snapshot
|
|
},
|
|
|
|
subscribe(observerOrFn: Observer<T> | ((value: T) => void)) {
|
|
const obs = toObserver(observerOrFn)
|
|
const observed = { current: false }
|
|
const e = effect(() => {
|
|
atom.get()
|
|
if (!observed.current) {
|
|
observed.current = true
|
|
} else {
|
|
obs.next?.(atom._snapshot)
|
|
}
|
|
})
|
|
|
|
return {
|
|
unsubscribe: () => {
|
|
e.stop()
|
|
},
|
|
}
|
|
},
|
|
_update(getValue?: T | ((snapshot: T) => T)): boolean {
|
|
const prevSub = activeSub
|
|
const compare = options?.compare ?? Object.is
|
|
if (isComputed) {
|
|
activeSub = atom
|
|
++cycle
|
|
atom.depsTail = undefined
|
|
} else if (getValue === undefined) {
|
|
// Mutable atoms can be marked dirty by the reactive graph, but they should
|
|
// never be recomputed without an explicit value/updater.
|
|
return false
|
|
}
|
|
if (isComputed) {
|
|
atom.flags = ReactiveFlags.Mutable | ReactiveFlags.RecursedCheck
|
|
}
|
|
try {
|
|
const oldValue = atom._snapshot
|
|
const newValue =
|
|
typeof getValue === 'function'
|
|
? (getValue as (snapshot: T) => T)(oldValue)
|
|
: getValue === undefined && isComputed
|
|
? getter(oldValue)
|
|
: getValue!
|
|
if (oldValue === undefined || !compare(oldValue, newValue)) {
|
|
atom._snapshot = newValue
|
|
return true
|
|
}
|
|
return false
|
|
} finally {
|
|
activeSub = prevSub
|
|
if (isComputed) {
|
|
atom.flags &= ~ReactiveFlags.RecursedCheck
|
|
}
|
|
purgeDeps(atom)
|
|
}
|
|
},
|
|
}
|
|
|
|
if (isComputed) {
|
|
atom.flags = ReactiveFlags.Mutable | ReactiveFlags.Dirty
|
|
atom.get = function (): T {
|
|
const flags = atom.flags
|
|
if (
|
|
flags & ReactiveFlags.Dirty ||
|
|
(flags & ReactiveFlags.Pending && checkDirty(atom.deps!, atom))
|
|
) {
|
|
if (atom._update()) {
|
|
const subs = atom.subs
|
|
if (subs !== undefined) {
|
|
shallowPropagate(subs)
|
|
}
|
|
}
|
|
} else if (flags & ReactiveFlags.Pending) {
|
|
atom.flags = flags & ~ReactiveFlags.Pending
|
|
}
|
|
if (activeSub !== undefined) {
|
|
link(atom, activeSub, cycle)
|
|
}
|
|
return atom._snapshot
|
|
}
|
|
} else {
|
|
;(atom as unknown as Atom<T>).set = function (
|
|
// eslint-disable-next-line no-shadow
|
|
valueOrFn: T | ((prev: T) => T),
|
|
): void {
|
|
if (atom._update(valueOrFn)) {
|
|
const subs = atom.subs
|
|
if (subs !== undefined) {
|
|
propagate(subs)
|
|
shallowPropagate(subs)
|
|
flush()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return atom as unknown as Atom<T> | ReadonlyAtom<T>
|
|
}
|
|
|
|
interface Effect extends ReactiveNode {
|
|
notify: () => void
|
|
stop: () => void
|
|
}
|
|
|
|
function effect<T>(fn: () => T): Effect {
|
|
const run = (): T => {
|
|
const prevSub = activeSub
|
|
activeSub = effectObj
|
|
++cycle
|
|
effectObj.depsTail = undefined
|
|
effectObj.flags = ReactiveFlags.Watching | ReactiveFlags.RecursedCheck
|
|
try {
|
|
return fn()
|
|
} finally {
|
|
activeSub = prevSub
|
|
effectObj.flags &= ~ReactiveFlags.RecursedCheck
|
|
purgeDeps(effectObj)
|
|
}
|
|
}
|
|
const effectObj: Effect = {
|
|
deps: undefined,
|
|
depsTail: undefined,
|
|
subs: undefined,
|
|
subsTail: undefined,
|
|
flags: ReactiveFlags.Watching | ReactiveFlags.RecursedCheck,
|
|
|
|
notify(): void {
|
|
const flags = this.flags
|
|
if (
|
|
flags & ReactiveFlags.Dirty ||
|
|
(flags & ReactiveFlags.Pending && checkDirty(this.deps!, this))
|
|
) {
|
|
run()
|
|
} else {
|
|
this.flags = ReactiveFlags.Watching
|
|
}
|
|
},
|
|
|
|
stop(): void {
|
|
this.flags = ReactiveFlags.None
|
|
this.depsTail = undefined
|
|
purgeDeps(this)
|
|
},
|
|
}
|
|
|
|
run()
|
|
|
|
return effectObj
|
|
}
|