import React from 'react'
import { isEqualSubsetShallow } from './comparators'
import { UNWRAP_PROXY } from './constants'
import type { StoreInfo } from './interfaces'
import { trackStoresAccess } from './useStore'
const logUpdate =
process.env.NODE_ENV === 'development'
? (fn: any, stores: any[], last: any, next: any) => {
const getStoreLogName = (store: any) => {
const str = store[UNWRAP_PROXY] ?? store
return `${str.constructor.name}${store.props?.id ? `:${store.props.id}` : ''}`
}
const storeNames = stores.map(getStoreLogName).join(', ')
const name = `🌑 ▶️ %c${fn.name} ${storeNames} () ${last} => ${next}`
console.groupCollapsed(name, 'color: tomato;')
console.groupCollapsed('trace >')
console.trace()
console.groupEnd()
console.info(' next', next)
console.groupEnd()
}
: null
export function observe(fn: () => any) {
let prev = getObserverValueAndStoresAccessed(fn)
let disposeValue: Function | null = null
const subscribe = () => {
const stores = [...prev.storeInfos]
return subscribeToStores(stores, () => {
disposeValue?.()
const next = getObserverValueAndStoresAccessed(fn)
if (typeof next.value === 'function') {
disposeValue = next.value
if (process.env.NODE_ENV === 'development') {
logUpdate!(fn, [...next.storeInfos], '(fn)', '(fn)')
}
return
}
if (
isEqualSubsetShallow(prev.storeInfos, next.storeInfos) &&
isEqualSubsetShallow(prev.value, next.value)
) {
return
}
if (process.env.NODE_ENV === 'development') {
logUpdate!(fn, [...next.storeInfos], prev.value, next.value)
}
prev = next
dispose()
dispose = subscribe()
})
}
let dispose = subscribe()
return {
dispose: () => {
dispose()
disposeValue?.()
},
getValue: () => prev.value,
}
}
export function useObserve(fn: () => A): A {
const [state, setState] = React.useState(() => {
return getObserverValueAndStoresAccessed(fn)
})
React.useEffect(() => {
let dispose
const unsub = subscribeToStores([...state.storeInfos], () => {
dispose?.()
const next = getObserverValueAndStoresAccessed(fn)
const nextStoreInfos = [...next.storeInfos]
const prevStoreInfos = [...state.storeInfos]
// return function === return disposable
if (typeof next.value === 'function') {
if (process.env.NODE_ENV === 'development') {
logUpdate!(fn, nextStoreInfos, '(fn)', '(fn)')
}
dispose = next.value
return
}
setState((prev) => {
if (
isEqualSubsetShallow(prevStoreInfos, nextStoreInfos) &&
isEqualSubsetShallow(prev.value, next.value)
) {
return prev
}
if (process.env.NODE_ENV === 'development') {
logUpdate!(fn, nextStoreInfos, prev.value, next.value)
}
return next
})
})
return () => {
unsub()
dispose?.()
}
}, [[...state.storeInfos].map((i) => i.uid).join(',')])
return state.value
}
function getObserverValueAndStoresAccessed(selector: () => A): {
value: A
storeInfos: Set
} {
const storeInfos = new Set()
const dispose = trackStoresAccess((storeInfo) => {
storeInfos.add(storeInfo)
})
const value = selector()
dispose()
return {
value,
storeInfos,
}
}
function subscribeToStores(storeInfos: StoreInfo[], onUpdate: () => any) {
const disposes: Function[] = []
// wrap onUpdate to avoid waterfall calls + avoid tracking during onUpdate
let isUpdating = false
const onUpdateDebouncedWithoutTracking = () => {
if (isUpdating) return
isUpdating = true
queueMicrotask(() => {
try {
for (const storeInfo of storeInfos) {
storeInfo.disableTracking = true
}
onUpdate()
} finally {
isUpdating = false
for (const storeInfo of storeInfos) {
storeInfo.disableTracking = false
}
}
})
}
for (const storeInfo of storeInfos) {
disposes.push(storeInfo.subscribe(onUpdateDebouncedWithoutTracking))
}
return () => {
disposes.forEach((x) => x())
}
}