@tanstack/store
Version:
Framework agnostic type-safe store w/ reactive framework adapters
299 lines (272 loc) • 7.85 kB
text/typescript
import { ReactiveFlags, createReactiveSystem, getBatchDepth } 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
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 (getBatchDepth() > 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
activeSub = atom
++cycle
atom.depsTail = undefined
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
}