import type { IDerivation } from '@theatre/dataverse' ; import { Box } from '@theatre/dataverse' ; import { prism , val } from '@theatre/dataverse' ; import { findIndex } from 'lodash-es' ; import queueMicrotask from 'queue-microtask' ; import { useCallback , useEffect , useLayoutEffect , useMemo , useRef , useState , } from 'react' ; import { unstable_batchedUpdates } from 'react-dom' ; type $IntentionalAny = any ; type VoidFn = ( ) => void ; const logger = { log : console . log } ; function useForceUpdate ( debugLabel ? ) { const [ , setTick ] = useState ( 0 ) ; const update = useCallback ( ( ) => { if ( process . env . NODE_ENV !== 'production' && debugLabel ) logger . log ( debugLabel , 'forceUpdate' , { trace : new Error ( ) } ) ; setTick ( ( tick ) => tick + 1 ) ; } , [ ] ) ; return update ; } export function usePrism < T > ( fn , deps , debugLabel ? ) { const fnAsCallback = useCallback ( fn , deps ) ; const boxRef = useRef < Box < typeof fn > > ( null as $IntentionalAny ) ; if ( ! boxRef . current ) { boxRef . current = new Box ( fnAsCallback ) ; } else { boxRef . current . set ( fnAsCallback ) ; } const derivation = useMemo ( ( ) => prism ( ( ) => { const fn = boxRef . current . derivation . getValue ( ) ; return fn ( ) ; } ) , [ ] ) ; return useDerivation ( derivation , debugLabel ) ; } export const useVal = ( p , debugLabel ? ) => { return usePrism ( ( ) => val ( p ) , [ p ] , debugLabel ) ; } ; /**
 * Each usePrism() call is assigned an `order`. Parents have a smaller
 * order than their children, and so on.
 */ let lastOrder = 0 ; /**
 * A sorted array of derivations that need to be refreshed. The derivations are sorted
 * by their order, which means a parent derivation always gets priority to children
 * and descendents. Ie. we refresh the derivations top to bottom.
 */ const queue = [ ] ; const setOfQueuedItems = new Set < QueueItem > ( ) ; type QueueItem = { order : number ; /**
     * runUpdate() is the equivalent of a forceUpdate() call.
     */ runUpdate : VoidFn ; debugLabel ? : string ; } ; let microtaskIsQueued = false ; const pushToQueue = ( item ) => { _pushToQueue ( item ) ; queueIfNeeded ( ) ; } ; const _pushToQueue = ( item ) => { if ( setOfQueuedItems . has ( item ) ) return ; setOfQueuedItems . add ( item ) ; if ( queue . length === 0 ) { queue . push ( item ) ; } else { const index = findIndex ( queue , ( existingItem ) => existingItem . order >= item . order ) ; if ( index === - 1 ) { queue . push ( item ) ; } else { const right = queue [ index ] ; if ( right . order > item . order ) { queue . splice ( index , 0 , item ) ; } } } } ; const removeFromQueue = ( item ) => { if ( ! setOfQueuedItems . has ( item ) ) return ; setOfQueuedItems . delete ( item ) ; const index = findIndex ( queue , ( o ) => o === item ) ; queue . splice ( index , 1 ) ; } ; function queueIfNeeded ( ) { if ( ! microtaskIsQueued ) { microtaskIsQueued = true ; queueMicrotask ( ( ) => { let i = 0 ; while ( queue . length > 0 ) { i ++ ; if ( i === 4 ) { setTimeout ( queueIfNeeded , 1 ) ; break ; } unstable_batchedUpdates ( ( ) => { for ( const item of queue ) { item . runUpdate ( ) ; } } , 1 ) ; } microtaskIsQueued = false ; } ) ; } } remarks function useDerivation < T > ( der , debugLabel ? ) { const _forceUpdate = useForceUpdate ( debugLabel ) ; const refs = useRef < { queueItem : QueueItem ; unmounted : boolean ; } > ( undefined as $IntentionalAny ) ; if ( ! refs . current ) { lastOrder ++ ; refs . current = { queueItem : { debugLabel , order : lastOrder , runUpdate : ( ) => { if ( ! refs . current . unmounted ) { _forceUpdate ( ) ; } } , } , unmounted : false , } ; } const queueUpdate = useCallback ( ( ) => { pushToQueue ( refs . current . queueItem ) ; } , [ ] ) ; useLayoutEffect ( ( ) => { const untap = der . changesWithoutValues ( ) . tap ( ( ) => { queueUpdate ( ) ; } ) ; if ( lastValueRef . current !== der . getValue ( ) ) { queueUpdate ( ) ; } return untap ; } , [ der ] ) ; useLayoutEffect ( ( ) => { return function onUnmount ( ) { refs . current . unmounted = true ; removeFromQueue ( refs . current . queueItem ) ; } ; } , [ ] ) ; const lastValueRef = useRef < T > ( undefined as $IntentionalAny as T ) ; const queueItem = refs . current . queueItem ; const mustDefer = setOfQueuedItems . has ( queueItem ) && queue [ 0 ] !== refs . current . queueItem ; if ( ! mustDefer ) { removeFromQueue ( queueItem ) ; lastValueRef . current = der . getValue ( ) ; } else { } return lastValueRef . current ; } /**
 * This makes sure the prism derivation remains hot as long as the
 * component calling the hook is alive, but it does not
 * return the value of the derivation, and it does not
 * re-render the component if the value of the derivation changes.
 *
 * Use this hook if you plan to read a derivation in a
 * useEffect() call, without the derivation causing your
 * element to re-render.
 */ export function usePrismWithoutReRender < T > ( fn , deps ) { const derivation = useMemo ( ( ) => prism ( fn ) , deps ) ; return useDerivationWithoutReRender ( derivation ) ; } /**
 * This makes sure the derivation remains hot as long as the
 * component calling the hook is alive, but it does not
 * return the value of the derivation, and it does not
 * re-render the component if the value of the derivation changes.
 */ export function useDerivationWithoutReRender < T > ( der ) { useEffect ( ( ) => { const untap = der . keepHot ( ) ; return ( ) => { untap ( ) ; } ; } , [ der ] ) ; return der ; }