import { Subject } from "rxjs" ; import React , { useContext , useState } from "react" ; import { Provider } from "polar-shared/src/util/Providers" ; import { Equals } from "./Equals" ; import { typedMemo } from "../../hooks/ReactHooks" ; export function pick < T , K extends keyof T > ( value , keys ) { const result = { } ; for ( const key of keys ) { result [ key ] = value [ key ] ; } return result ; } /**
 * Hook that allows us to just pick specific keys from the store.
 */ function usePick < T , K extends keyof T > ( useStoreHook , keys ) { const store = useStoreHook ( ) ; const [ value , ] = React . useState < Pick < T , K > > ( pick ( store , keys ) ) ; return value ; } interface InternalObservableStore < V > { /**
     * The underlying rxjs observable for sending off updates to components.
     */ readonly subject : Subject < V > ; /**
     * The current value, used for the the initial render of each component
     * and to update it each time so that on useObservableStore we can
     * return the current value.
     */ current : V ; } export interface ObservableStore < V > { /**
     * The current value, used for the the initial render of each component.
     */ readonly current : V ; } /**
 * Use the subject itself so that we can perform operations on it that don't
 * cause react to render.
 */ export function useObservableStoreSubject < V , R > ( context ) { const { subject } = useContext ( context ) as InternalObservableStore < V > ; return subject ; } export type StoreProvider < V > = ( ) => V ; export type SetStore < V > = ( value : V ) => void ; export type Store < V > = [ V , SetStore < V > ] ; interface IUseObservableStoreOpts < V , K extends keyof V > extends IUseStoreHookOpts < V , K > { readonly enableShallowEquals : boolean ; } type Dict = { [ key ] : any ; } ; export type UseStoreReducerFilter < R > = ( prev : R , next : R ) => boolean ; interface IUseObservableStoreReducerOpts < R > { readonly enableShallowEquals ? : boolean ; readonly debug ? : boolean ; readonly filter ? : UseStoreReducerFilter < R > ; } export function useObservableStoreReducer < V , R > ( context , reducer , opts = { } ) { const internalObservableStore = useContext ( context ) as InternalObservableStore < V > ; const [ value , setValue ] = useState < R > ( reducer ( internalObservableStore . current ) ) ; const valueRef = React . useRef ( value ) ; React . useEffect ( ( ) => { const subscription = internalObservableStore . subject . subscribe ( ( nextStore ) => { function doUpdateValue ( newValue ) { setValue ( newValue ) ; valueRef . current = newValue ; } const currValue = valueRef . current ; function isEqual ( a , b ) { if ( opts . enableShallowEquals ) { return Equals . shallow ( a , b ) ; } return Equals . deep ( a , b ) ; } const nextValue = reducer ( nextStore ) ; if ( ! isEqual ( currValue , nextValue ) ) { if ( opts . filter && ! opts . filter ( currValue , nextValue ) ) { valueRef . current = nextValue ; return ; } return doUpdateValue ( nextValue ) ; } else { } } ) ; return ( ) => { subscription . unsubscribe ( ) ; } ; } , [ internalObservableStore . subject , opts , reducer ] ) ; return value ; } export function useObservableStore < V , K extends keyof V > ( context , keys , opts ) { const internalObservableStore = useContext ( context ) as InternalObservableStore < V > ; const [ value , setValue ] = useState < V > ( internalObservableStore . current ) ; const valueRef = React . useRef ( value ) ; const { filter , enableShallowEquals } = opts ; const handleUpdate = React . useCallback ( ( nextValue ) => { try { function doUpdateValue ( newValue ) { valueRef . current = newValue ; setValue ( newValue ) ; } const currValue = valueRef . current ; if ( nextValue === currValue ) { return ; } function isEqual ( a , b ) { if ( enableShallowEquals ) { return Equals . shallow ( a , b ) ; } return Equals . deep ( a , b ) ; } if ( keys ) { const nextValuePicked = pick ( nextValue , keys ) ; const currValuePicked = pick ( currValue , keys ) ; if ( ! isEqual ( currValuePicked , nextValuePicked ) ) { if ( filter && ! filter ( nextValuePicked ) ) { return ; } return doUpdateValue ( nextValue ) ; } else { } } else { const currValue = valueRef . current ; if ( ! isEqual ( currValue , nextValue ) ) { return doUpdateValue ( nextValue ) ; } } } catch ( e ) { console . error ( "Unable to handle subscription: " , e ) ; } } , [ enableShallowEquals , filter , keys ] ) ; React . useEffect ( ( ) => { handleUpdate ( internalObservableStore . current ) ; const subscription = internalObservableStore . subject . subscribe ( ( nextValue ) => { handleUpdate ( nextValue ) ; } ) ; return ( ) => { subscription . unsubscribe ( ) ; } ; } , [ handleUpdate , internalObservableStore ] ) ; return value ; } export type InternalStoreContext < V > = [ React . Context < ObservableStore < V > > ] ; export type StoreContext < V > = [ React . Context < ObservableStore < V > > , ObservableStore < V > ] ; function createInternalObservableStore < V > ( initialValue ) { const subject = new Subject < V > ( ) ; subject . next ( initialValue ) ; const store = { subject , current : initialValue } ; return store ; } export interface ObservableStoreProps < V > { readonly children : JSX . Element | Provider < JSX . Element > ; readonly store ? : V ; } interface ObservableStorePropsWithoutStore < V > { readonly children : JSX . Element | Provider < JSX . Element > ; } export type ObservableStoreProviderComponent < V > = React . FunctionComponent < ObservableStoreProps < V > > ; /**
 * Hook to listen to store changes. Use undefined to not filter for properties
 * but we don't recommend it.
 */ export type UseStoreHook < V , K extends keyof V > = ( keys : ReadonlyArray < K > | undefined , opts ? : IUseStoreHookOpts < V , K > ) => Pick < V , K > ; export type UseContextHook < V > = ( ) => V ; /**
 * Tag interface just for documentation right now.
 */ export interface StoreMutator { } const DEFAULT_USE_STORE_MAPPER = < V , K extends keyof V , N > ( store ) => store ; export type UseStoreMapper < V , K extends keyof V , N > = ( store : Pick < V , K > ) => N ; const DEFAULT_USE_STORE_FILTER = < V , K extends keyof V > ( ) => true ; /**
 * Given a store, only render the hook it the value passes the filter.  This is
 * an option for when we want to avoid triggering too many sub-component
 * re-renders by filtering before RXJS is pushed.
 */ export type UseStoreFilter < V , K extends keyof V > = ( store : Pick < V , K > ) => boolean ; export interface IUseStoreHookOpts < V , K extends keyof V > { readonly debug ? : boolean ; readonly filter : UseStoreFilter < V , K > ; } export type ObservableStoreTuple < V , M extends StoreMutator , C > = [ ObservableStoreProviderComponent < V > , < K extends keyof V > ( keys : ReadonlyArray < K > | undefined , opts ? : IUseStoreHookOpts < V , K > ) => Pick < V , K > , UseContextHook < C > , UseContextHook < M > , < R extends any > ( reducer : ( value : V ) => R , opts ? : IUseObservableStoreReducerOpts < R > ) => R , UseContextHook < SetStore < V > > , UseContextHook < StoreProvider < V > > ] ; /**
 * Create the callbacks. Called for every useCallbacks function so that we can
 * call hooks when the callbacks are created. This allows us to use other hooks
 * in our created callbacks.
 */ export type CallbacksFactory < V , M , C > = ( storeProvider : Provider < V > , setStore : SetStore < V > , mutator : M ) => C ; export type MutatorFactory < V , M > = ( storeProvider : Provider < V > , setStore : SetStore < V > ) => M ; export interface ObservableStoreOpts < V , M , C > { /**
     * The initial value for the store including defaults for every value.
     */ readonly initialValue : V ; readonly mutatorFactory : MutatorFactory < V , M > ; /**
     * Used to create high level callbacks that can be injected into your
     * components with a useXCallbacks hook to mutate your store.  The callback
     * is a singleton and never updated so component memo works.
     */ readonly callbacksFactory : CallbacksFactory < V , M , C > ; /**
     * Same functionality as callbacksFactory but used with mocks so that you
     * can work with components using the mock without complex initialization.
     *
     * Used with more production apps so other developers don't need to worry
     * about configuration of our component system.
     *
     * Usually these mocks should just print to the console or perform some
     * action to note that they were called and with what arguments.
     */ readonly mockCallbacksFactory ? : CallbacksFactory < V , M , C > ; /**
     * Do a shallow equals by default..
     */ readonly enableShallowEquals ? : boolean ; } type ComponentCallbacksFactory < C > = ( ) => C ; type InitialContextValues < V , M , C > = [ M , ComponentCallbacksFactory < C > , SetStore < V > , StoreProvider < V > ] ; /**
 * Create the initial values of the components we're working with (store
 * and callbacks)
 */ function createInitialContextValues < V , M , C > ( opts , internalObservableStore ) { const { mutatorFactory , callbacksFactory } = opts ; const setStore = ( value ) => { internalObservableStore . current = value ; internalObservableStore . subject . next ( value ) ; } ; const storeProvider = ( ) => internalObservableStore . current ; const mutator = mutatorFactory ( storeProvider , setStore ) ; const componentCallbacksFactory = ( ) => callbacksFactory ( storeProvider , setStore , mutator ) ; return [ mutator , componentCallbacksFactory , setStore , storeProvider ] ; } export function createObservableStore < V , M , C > ( opts ) { const internalObservableStore = createInternalObservableStore ( opts . initialValue ) ; const [ mutator , componentCallbacksFactory , setStore , storeProvider ] = createInitialContextValues ( opts , internalObservableStore ) ; const storeContext = React . createContext < InternalObservableStore < V > > ( internalObservableStore ) ; const callbacksContext = React . createContext < ComponentCallbacksFactory < C > > ( componentCallbacksFactory ) ; const mutatorContext = React . createContext < M > ( mutator ) ; const setStoreContext = React . createContext < SetStore < V > > ( setStore ) ; const storeProviderContext = React . createContext < StoreProvider < V > > ( storeProvider ) ; const useStoreHook = < K extends keyof V > ( keys , storeHookOpts = { filter : DEFAULT_USE_STORE_FILTER } ) => { return useObservableStore ( storeContext , keys , { enableShallowEquals : opts . enableShallowEquals || false , filter : storeHookOpts . filter } ) ; } ; const useStoreReducerHook = < R extends any > ( reducer , opts = { } ) => { return useObservableStoreReducer ( storeContext , reducer , opts ) ; } ; const useMutatorHook = ( ) => { return React . useContext ( mutatorContext ) ; } ; const useCallbacksHook = ( ) => { return React . useContext ( callbacksContext ) ( ) ; } ; const useSetStore = ( ) => { return React . useContext ( setStoreContext ) ; } ; const useStoreProvider = ( ) => { return React . useContext ( storeProviderContext ) ; } ; interface MountedChildProps { readonly children : JSX . Element | Provider < JSX . Element > ; } const MountedChild = React . memo ( function MountedChild ( props ) { const [ mounted , setMounted ] = React . useState ( false ) ; React . useEffect ( ( ) => setMounted ( true ) , [ ] ) ; return ( < > { mounted && props . children } < />); } ) ; const ObservableProviderComponent = typedMemo ( ( props ) => { const internalObservableStore = React . useMemo ( ( ) => createInternalObservableStore ( props . store || opts . initialValue ) , [ props . store ] ) ; const [ mutator , componentCallbacksFactory , setStore , storeProvider ] = React . useMemo ( ( ) => createInitialContextValues ( opts , internalObservableStore ) , [ internalObservableStore ] ) ; return ( < storeContext . Provider value = { internalObservableStore } > < callbacksContext . Provider value = { componentCallbacksFactory } > < mutatorContext . Provider value = { mutator } > < setStoreContext . Provider value = { setStore } > < storeProviderContext . Provider value = { storeProvider } > < MountedChild { ... props } / > < /storeProviderContext.Provider> < /setStoreContext.Provider> < /mutatorContext.Provider> < /callbacksContext.Provider> < /storeContext.Provider>); } ) ; return [ ObservableProviderComponent , useStoreHook , useCallbacksHook , useMutatorHook , useStoreReducerHook , useSetStore , useStoreProvider ] ; }