export const createBistate = < State extends Record < string , any > > ( initialState , previousProxy = null ) : State => { if ( ! isArray ( initialState ) && ! isObject ( initialState ) ) { throw new Error ( `Expected initialState to be array or object, but got ${ initialState }` ) ; } const scapegoat = ( isArray ( initialState ) ? [ ] : { } ) as State ; const target = ( isArray ( initialState ) ? [ ] : { } ) as State ; let parent = Symbol ( 'top' ) ; const setParent = ( input ) => { parent = input ; } ; const getParent = ( ) => parent ; const deleteParent = ( ) => { parent = null ; } ; const internal = { setParent , getParent , deleteParent , } ; const handlers = { get : ( target , key ) => { if ( key === BISTATE ) { return internal ; } if ( scapegoat ) { return Reflect . get ( scapegoat , key ) ; } else { return Reflect . get ( target , key ) ; } } , set : ( current , key , value , reciver ) => { if ( hasFreezed ( reciver , key ) ) { throw new Error ( `You can change it, because it has been freezed` ) ; } else if ( scapegoat ) { const result = Reflect . set ( scapegoat , key , value ) ; return result ; } else { throw new Error ( `state is immutable, it's not allowed to set property: ${ key . toString ( ) }` ) ; } } , deleteProperty : ( current , key ) => { throw new Error ( `It's not allowed to delete property: ${ key . toString ( ) }` ) ; } , has : ( target , key ) => { if ( scapegoat ) { return Reflect . has ( scapegoat , key ) ; } else { return Reflect . has ( target , key ) ; } } , ownKeys : target => { if ( scapegoat ) { return Reflect . ownKeys ( scapegoat ) ; } else { return Reflect . ownKeys ( target ) ; } } , getPrototypeOf : target => { if ( scapegoat ) { return Reflect . getPrototypeOf ( scapegoat ) ; } else { return Reflect . getPrototypeOf ( target ) ; } } , setPrototypeOf : ( ) => { throw new Error ( `bistate only supports plain object or array, it's not allowed to setPrototypeOf` ) ; } , getOwnPropertyDescriptor : ( target , prop ) => { if ( scapegoat ) { return Reflect . getOwnPropertyDescriptor ( scapegoat , prop ) ; } else { return Reflect . getOwnPropertyDescriptor ( target , prop ) ; } } , defineProperty : ( _ , property ) => { throw new Error ( `bistate only supports plain object or array, it's not allowed to defineProperty: ${ property . toString ( ) }` ) ; } , } ; const currentProxy = new Proxy ( target , handlers ) ; if ( isArray ( currentProxy ) ) { fillArrayBistate ( currentProxy , initialState , target , scapegoat , previousProxy ) ; } else { fillObjectBistate ( currentProxy , initialState , target , scapegoat , previousProxy ) ; } previousProxy = null ; initialState = null as any ; return currentProxy ; } ; export default function < State extends Record < string , any > > ( initialState ) { return createBistate ( initialState , null ) ; } const BISTATE = Symbol ( 'BISTATE' ) ; export const isBistate = ( input ) => Boolean ( input ?. [ BISTATE ] ) ; export const { isArray } = Array ; export const isObject = ( input ) : input is Record < string , any > => { if ( typeof input !== 'object' || input === null ) { return false ; } let proto = input ; while ( Object . getPrototypeOf ( proto ) !== null ) { proto = Object . getPrototypeOf ( proto ) ; } return Object . getPrototypeOf ( input ) === proto ; } ; export const isThenable = ( input ) : input is PromiseLike < any > => Boolean ( input && typeof input . then === 'function' ) ; const getBistateValue = ( value , currentProxy , previousProxy ) => { let status = '' ; /**
     * if previousProxy exists, it is in reusing phase
     * otherwise is in initializing phase
     */ if ( previousProxy ) { if ( isBistate ( value ) ) { const parent = value [ BISTATE ] . getParent ( ) ; if ( parent === previousProxy ) { status = 'reuse' ; } else { status = 'create' ; } } else if ( isArray ( value ) || isObject ( value ) ) { status = 'create' ; } } else if ( isArray ( value ) || isObject ( value ) ) { status = 'create' ; } if ( status === 'reuse' ) { value = value [ BISTATE ] . compute ( ) ; value [ BISTATE ] . setParent ( currentProxy ) ; } else if ( status === 'create' ) { value = createBistate ( value ) ; value [ BISTATE ] . setParent ( currentProxy ) ; } return value ; } ; const fillObjectBistate = ( currentProxy , initialObject , target , scapegoat , previousProxy ) => { for ( const key in initialObject ) { const value = getBistateValue ( initialObject [ key ] , currentProxy , previousProxy ) ; scapegoat [ key ] = value ; target [ key ] = value ; } } ; const fillArrayBistate = ( currentProxy , initialArray , target , scapegoat , previousProxy ) => { for ( let i = 0 ; i < initialArray . length ; i ++ ) { const item = getBistateValue ( initialArray [ i ] , currentProxy , previousProxy ) ; scapegoat [ i ] = item ; target [ i ] = item ; } } ; export type Path = ( string | symbol | number ) [ ] ; export const isSubArray = ( base , target , equal = ( a , b ) => a === b ) => { for ( let i = 0 ; i < base . length ; i ++ ) { if ( equal ( base [ i ] , target [ 0 ] ) ) { let isEqual = true ; for ( let j = 0 ; j < target . length ; j ++ ) { if ( ! equal ( base [ i + j ] , target [ j ] ) ) { isEqual = false ; break ; } } if ( isEqual ) { return true ; } } } return false ; } ; const createFreezeStore = ( ) => { const map = { } ; const freeze = ( symbol , path ) => { if ( map [ symbol ] ) { map [ symbol ] . push ( path ) ; } else { map [ symbol ] = [ path ] ; } } ; const hasFreezed = ( key , path ) : boolean => { if ( map [ key ] ) { return map [ key ] . some ( ( base ) => isSubArray ( base , path ) || isSubArray ( path , base ) ) ; } else { return false ; } } ; return { freeze , hasFreezed , } ; } ; const store = createFreezeStore ( ) ; export const freeze = < I > ( input , key ) => { const [ path , top ] = getPathAndTop ( input , key ) ; if ( top ) { store . freeze ( top , path ) ; } else { throw new Error ( `input: ${ input } is not bistate` ) ; } } ; export const hasFreezed = < I > ( input , key ) => { const [ path , top ] = getPathAndTop ( input , key ) ; if ( top ) { return store . hasFreezed ( top , path ) ; } else { throw new Error ( `input: ${ input } is not bistate` ) ; } } ; const getPathAndTop = < I > ( current , key ) : readonly [ Path , symbol | null ] => { const path = [ key as any ] ; let top = null ; const getParantpath = ( current , parent ) => { if ( ! parent ) { return ; } if ( typeof parent === 'symbol' ) { top = parent ; return ; } for ( const key in parent ) { if ( parent [ key ] === current ) { path . push ( key ) ; getParantpath ( parent , parent ?. [ BISTATE ] ?. getParent ( ) ) ; } } } ; getParantpath ( current , current ?. [ BISTATE ] ?. getParent ( ) ) ; return [ path , top ] as const ; } ;