import React from 'react' ; import { IActa , TActaValue } from './types' ; import { isObject } from './isObject' ; const actaStoragePrefix = '__acta__' ; const actaStoragePrefixLength = actaStoragePrefix . length ; const actaEventPrefix = '__actaEvent__' ; const actaEventPrefixLength = actaEventPrefix . length ; const isInDOM = typeof window !== 'undefined' ; let tempStateHooksId = 0 ; let tempEventHooksId = 0 ; const Acta = { /**
     * Boolean to know if acta has been initialized before
     */ initialized : false , /**
     * states will hold values, subscribers list and history indexed by keys
     * events will hold subscribers list indexed by events keys
     * actions will hold actions function indexed by actions keys
     * actaIDs will be an array of unique ids
     */ actaIDs : [ ] , events : { } , states : { } , /**
     * The init function will search for any previousely acta states in the local
     * and session storage and will start listening on the changes in the storages
     * to update itself if the storage is updated from another tab
     */ init ( ) { /**
         * Acta is now initialized
         */ this . initialized = true ; /**
         * Attach to the window for global public access
         */ ( window as any ) . Acta = Acta ; /**
         * Listen to the storage to synchronise Acta between tabs
         */ window . addEventListener ( 'storage' , ( event ) => { const key = event . key ; const newValue = event . newValue ; if ( key && key . slice ( 0 , actaStoragePrefixLength ) === actaStoragePrefix ) { if ( newValue !== null && newValue !== '' && newValue !== 'null' ) { this . setState ( { [ key . slice ( actaStoragePrefixLength ) ] : JSON . parse ( newValue ) , } ) ; } else { this . setState ( { [ key . slice ( actaStoragePrefixLength ) ] : null , } ) ; } } else if ( newValue !== null && key ?. slice ( 0 , actaEventPrefixLength ) === actaEventPrefix ) { this . dispatchEvent ( key . slice ( actaEventPrefixLength ) , newValue ? JSON . parse ( newValue ) : newValue , false ) ; localStorage . removeItem ( key ) ; } } , false ) ; /**
         * Load the data from local and session storage
         */ const storage = { ... localStorage , ... sessionStorage , } ; const storageKeys = Object . keys ( storage ) ; for ( const storageKey of storageKeys ) { if ( storageKey . slice ( 0 , actaStoragePrefixLength ) === actaStoragePrefix ) { const value = JSON . parse ( storage [ storageKey ] ) ; if ( value !== undefined && value !== null ) { this . setState ( { [ storageKey . slice ( actaStoragePrefixLength ) ] : value , } ) ; } } } } , param { String } actaStateKey param { TActaValue } defaultValue useActaState ( actaStateKey , defaultValue ? ) { if ( actaStateKey === '' || typeof actaStateKey !== 'string' ) { throw new Error ( `Acta.useActaState params =>
[0]: string,
[1]: optionnal, string | number | object | boolean | null | undefined` ) ; } const internalID = tempStateHooksId ++ ; /**
         * Try to get an initial value in Acta if the state already exists
         * of in the optional defaultValue
         */ const initialValue = this . hasState ( actaStateKey ) ? this . getState ( actaStateKey ) : defaultValue ; /**
         * Init the state hook that wll return the value and
         * trigger a re-render for the component
         */ const [ actaValue , setActaValue ] = React . useState ( initialValue ) ; this . states [ actaStateKey ] = this . states [ actaStateKey ] || { value : defaultValue , defaultValue : defaultValue , subscribtions : { } , } ; /**
         * Add the life cycle hook to subscribe to the state when
         * the component is rendered and unsubscribe when the component
         * is unmounted
         */ React . useEffect ( ( ) => { this . states [ actaStateKey ] . subscribtions [ `__${ internalID }` ] = { callback : ( value ) => setActaValue ( value ) , } ; return ( ) => { delete this . states [ actaStateKey ] . subscribtions [ `__${ internalID }` ] ; } ; } , [ ] ) ; return actaValue ; } , param { String } actaStateKey param { Function | String } callbackOrLocalStateKey param { Object } context param { TActaValue } defaultValue subscribeState ( actaStateKey , callbackOrLocalStateKey , context , defaultValue = undefined ) { if ( actaStateKey === '' || typeof actaStateKey !== 'string' || ( typeof callbackOrLocalStateKey !== 'function' && typeof callbackOrLocalStateKey !== 'string' ) || ! isObject ( context ) ) { throw new Error ( `Acta.subscribeState params =>
[0]: string,
[1]: function or string,
[2]: mounted react component` ) ; } this . ensureActaID ( context ) ; this . states [ actaStateKey ] = this . states [ actaStateKey ] || { value : defaultValue , defaultValue : defaultValue , subscribtions : { } , } ; /**
         * If a subscribtion for this context on this state
         * already exists, stop here
         */ if ( this . states [ actaStateKey ] . subscribtions [ context . actaID as string ] ) { return ; } /**
         * Extend the componentWillUnmount hook on the context
         * with a state unsubscribtion, so when
         * the component unmounts, all its states subscribtions
         * will be destroyed
         */ if ( context . componentWillUnmount ) { const oldComponentWillUnmount = context . componentWillUnmount ; context . componentWillUnmount = ( ) => { this . unsubscribeState ( actaStateKey , context ) ; oldComponentWillUnmount . bind ( context ) ( ) ; } ; } /**
         * If the callback is not a function, it sets the state
         * in the component
         */ let passedCallback ; if ( typeof callbackOrLocalStateKey === 'string' ) { passedCallback = ( value ) => context . setState ( { [ callbackOrLocalStateKey ] : value , } ) ; } else { passedCallback = callbackOrLocalStateKey ; } /**
         * Add the callback and the context to the subscribtion list
         * of the state
         */ this . states [ actaStateKey ] . subscribtions [ context . actaID as string ] = { callback : passedCallback , context , } ; try { if ( this . states [ actaStateKey ] . value !== undefined ) { passedCallback ( JSON . parse ( JSON . stringify ( this . states [ actaStateKey ] . value ) ) ) ; } } catch ( err ) { console . error ( `Error in subscribeState for "${ actaStateKey }":` , err ) ; } } , param { String } stateKey param { Object } context unsubscribeState ( stateKey , context ) { if ( typeof stateKey !== 'string' || ! isObject ( context ) ) { throw new Error ( `Acta.unsubscribeState params =>
[0]: string,
[2]: mounted react component` ) ; } if ( this . states [ stateKey ] ) delete this . states [ stateKey ] . subscribtions [ context . actaID as string ] ; } , param { Object } states param { String } persistenceType setState ( states , persistenceType ) { if ( ! isObject ( states ) || Object . keys ( states ) . length === 0 ) { throw new Error ( 'Acta.setState params => [0]: object with 1+ key' ) ; } /**
         * Loop over the states
         */ for ( const state of Object . entries ( states ) ) { const stateKey = state [ 0 ] ; const value = state [ 1 ] ; if ( ! this . states [ stateKey ] ) { this . states [ stateKey ] = this . states [ stateKey ] || { value : value || undefined , defaultValue : undefined , subscribtions : { } , } ; } this . states [ stateKey ] . value = value ; if ( isInDOM && persistenceType ) { if ( persistenceType === 'localStorage' ) { localStorage . setItem ( `${ actaStoragePrefix }${ stateKey }` , JSON . stringify ( value ) ) ; } else if ( persistenceType === 'sessionStorage' ) { sessionStorage . setItem ( `${ actaStoragePrefix }${ stateKey }` , JSON . stringify ( value ) ) ; } else { throw new Error ( 'Acta.setState params => [1]: "sessionStorage" | "localStorage".' ) ; } } /**
             * Try to dispatch to all subscribers & kill the
             * subscribtion if the subscriber has been destroyed
             */ if ( this . states [ stateKey ] . subscribtions ) { Object . keys ( this . states [ stateKey ] . subscribtions ) . forEach ( ( actaID ) => { try { this . states [ stateKey ] . subscribtions [ actaID ] . callback ( value ) ; } catch ( err ) { } } ) ; } } } , param { String } state deleteState ( stateKey , persistenceType ) { if ( typeof stateKey !== 'string' || stateKey === '' ) { throw new Error ( 'Acta.deleteState params => [0]: string' ) ; } delete this . states [ stateKey ] ; /**
         * If the persistance type is set and we have a window, remove the
         * value from the storage
         */ if ( isInDOM && persistenceType ) { if ( persistenceType === 'sessionStorage' ) { sessionStorage . removeItem ( `${ actaStoragePrefix }${ stateKey }` ) ; } else if ( persistenceType === 'localStorage' ) { localStorage . removeItem ( `${ actaStoragePrefix }${ stateKey }` ) ; } else { throw new Error ( 'Acta.deleteState params => [1]: "sessionStorage" | "localStorage".' ) ; } } } , param { String } stateKey return { * } getState ( stateKey ) { if ( typeof stateKey !== 'string' ) { throw new Error ( 'Acta.deleteState params => [0]: string' ) ; } return ( this . states [ stateKey ] ?. value || this . states [ stateKey ] ?. defaultValue || undefined ) ; } , param { String } stateKey hasState ( stateKey ) { /**
         * Check param
         */ if ( typeof stateKey !== 'string' ) { throw new Error ( 'Acta.hasState params => [0]: string' ) ; } return this . states . hasOwnProperty ( stateKey ) ; } , param { String } eventKey param { Function } callback useActaEvent ( eventKey , callback ) { if ( eventKey === '' || typeof eventKey !== 'string' || typeof callback !== 'function' ) { throw new Error ( `Acta.useActaEvent params =>
[0]: string,
[1]: function` ) ; } const internalID = tempEventHooksId ++ ; this . events [ eventKey ] = this . events [ eventKey ] || { } ; /**
         * Add the life cycle hook to subscribe to the state when
         * the component is rendered and unsubscribe when the component
         * is unmounted
         */ React . useEffect ( ( ) => { this . events [ eventKey ] [ `__${ internalID }` ] = { callback , } ; return ( ) => { delete this . events [ eventKey ] [ `__${ internalID }` ] ; } ; } , [ ] ) ; } , param { String } eventKey param { Function } callback param { React . Component } context subscribeEvent ( eventKey , callback , context ) { if ( eventKey === '' || typeof eventKey !== 'string' || typeof callback !== 'function' || ! isObject ( context ) ) { throw new Error ( `Acta.subscribeEvent params =>
[0]: string,
[1]: function,
[2]: mounted react component` ) ; } this . events [ eventKey ] = this . events [ eventKey ] || { } ; this . ensureActaID ( context ) ; if ( this . events [ eventKey ] [ context . actaID as string ] ) { return false ; } /**
         * Extend the componentWillUnmount hook on the context
         * with a event unsubscribtion, so when
         * the component unmounts, all its events subscribtions
         * will be destroyed
         */ if ( context . componentWillUnmount ) { const oldComponentWillUnmount = context . componentWillUnmount ; context . componentWillUnmount = ( ) => { this . unsubscribeEvent ( eventKey , context ) ; oldComponentWillUnmount . bind ( context ) ( ) ; } ; } /**
         * Add the callback and the context to the event listener list
         * of the event
         */ this . events [ eventKey ] [ context . actaID as string ] = { callback , context , } ; return ; } , param { String } eventKey param { Object } context unsubscribeEvent ( eventKey , context ) { if ( typeof eventKey !== 'string' || ! isObject ( context ) ) { throw new Error ( `Acta.subscribeEvent params =>
[0]: string,
[2]: mounted react component` ) ; } if ( this . events [ eventKey ] ) delete this . events [ eventKey ] [ String ( context . actaID ) ] ; } , param { String } eventKey param { TActaValue } data param { Boolean } isShared dispatchEvent ( eventKey , data , isShared ) { if ( typeof eventKey !== 'string' ) { throw new Error ( 'Acta.dispatchEvent params => [0]: string & must exist in Acta.events' ) ; } /**
         * If this is a shared message, send it to the local storage;
         * it will come back instantly
         */ if ( isShared && isInDOM ) { localStorage . setItem ( actaEventPrefix + eventKey , JSON . stringify ( data ) ) ; } /**
         * Call each subscriber callback
         */ Object . keys ( this . events [ eventKey ] || { } ) . forEach ( ( actaID ) => { try { this . events [ eventKey ] [ actaID ] . callback ( data || null ) ; } catch ( err ) { } } ) ; } , /**
     * ensureActaID is a small utility function to
     * inject a unique id to all subscribers contexts
     */ ensureActaID ( context ) { if ( ! context || context . actaID ) return false ; const newId = `_${ this . actaIDs . length }` ; this . actaIDs . unshift ( newId ) ; context . actaID = newId ; return newId ; } , } ; /**
 * If Acta has not been initialized, init Acta
 */ if ( ! Acta . initialized && isInDOM ) Acta . init ( ) ; export default Acta ;