/** * External dependencies */ import { deepSignal } from 'deepsignal'; import { computed } from '@preact/signals'; import { getScope, setScope, resetScope, setNamespace, resetNamespace, } from './hooks'; const isObject = ( item: unknown ): boolean => !! item && typeof item === 'object' && ! Array.isArray( item ); const deepMerge = ( target: any, source: any ) => { if ( isObject( target ) && isObject( source ) ) { for ( const key in source ) { const getter = Object.getOwnPropertyDescriptor( source, key )?.get; if ( typeof getter === 'function' ) { Object.defineProperty( target, key, { get: getter } ); } else if ( isObject( source[ key ] ) ) { if ( ! target[ key ] ) Object.assign( target, { [ key ]: {} } ); deepMerge( target[ key ], source[ key ] ); } else { Object.assign( target, { [ key ]: source[ key ] } ); } } } }; const parseInitialState = () => { const storeTag = document.querySelector( `script[type="application/json"]#wc-interactivity-initial-state` ); if ( ! storeTag?.textContent ) return {}; try { const initialState = JSON.parse( storeTag.textContent ); if ( isObject( initialState ) ) return initialState; throw Error( 'Parsed state is not an object' ); } catch ( e ) { // eslint-disable-next-line no-console console.log( e ); } return {}; }; export const stores = new Map(); const rawStores = new Map(); const storeLocks = new Map(); const objToProxy = new WeakMap(); const proxyToNs = new WeakMap(); const scopeToGetters = new WeakMap(); const proxify = ( obj: any, ns: string ) => { if ( ! objToProxy.has( obj ) ) { const proxy = new Proxy( obj, handlers ); objToProxy.set( obj, proxy ); proxyToNs.set( proxy, ns ); } return objToProxy.get( obj ); }; const handlers = { get: ( target: any, key: string | symbol, receiver: any ) => { const ns = proxyToNs.get( receiver ); // Check if the property is a getter and we are inside an scope. If that is // the case, we clone the getter to avoid overwriting the scoped // dependencies of the computed each time that getter runs. const getter = Object.getOwnPropertyDescriptor( target, key )?.get; if ( getter ) { const scope = getScope(); if ( scope ) { const getters = scopeToGetters.get( scope ) || scopeToGetters.set( scope, new Map() ).get( scope ); if ( ! getters.has( getter ) ) { getters.set( getter, computed( () => { setNamespace( ns ); setScope( scope ); try { return getter.call( target ); } finally { resetScope(); resetNamespace(); } } ) ); } return getters.get( getter ).value; } } const result = Reflect.get( target, key, receiver ); // Check if the proxy is the store root and no key with that name exist. In // that case, return an empty object for the requested key. if ( typeof result === 'undefined' && receiver === stores.get( ns ) ) { const obj = {}; Reflect.set( target, key, obj, receiver ); return proxify( obj, ns ); } // Check if the property is a generator. If it is, we turn it into an // asynchronous function where we restore the default namespace and scope // each time it awaits/yields. if ( result?.constructor?.name === 'GeneratorFunction' ) { return async ( ...args: unknown[] ) => { const scope = getScope(); const gen: Generator< any > = result( ...args ); let value: any; let it: IteratorResult< any >; while ( true ) { setNamespace( ns ); setScope( scope ); try { it = gen.next( value ); } finally { resetScope(); resetNamespace(); } try { value = await it.value; } catch ( e ) { gen.throw( e ); } if ( it.done ) break; } return value; }; } // Check if the property is a synchronous function. If it is, set the // default namespace. Synchronous functions always run in the proper scope, // which is set by the Directives component. if ( typeof result === 'function' ) { return ( ...args: unknown[] ) => { setNamespace( ns ); try { return result( ...args ); } finally { resetNamespace(); } }; } // Check if the property is an object. If it is, proxyify it. if ( isObject( result ) ) return proxify( result, ns ); return result; }, }; /** * @typedef StoreProps Properties object passed to `store`. * @property {Object} state State to be added to the global store. All the * properties included here become reactive. */ /** * @typedef StoreOptions Options object. */ /** * Extends the Interactivity API global store with the passed properties. * * These props typically consist of `state`, which is reactive, and other * properties like `selectors`, `actions`, `effects`, etc. which can store * callbacks and derived state. These props can then be referenced by any * directive to make the HTML interactive. * * @example * ```js * store({ * state: { * counter: { value: 0 }, * }, * actions: { * counter: { * increment: ({ state }) => { * state.counter.value += 1; * }, * }, * }, * }); * ``` * * The code from the example above allows blocks to subscribe and interact with * the store by using directives in the HTML, e.g.: * * ```html *