/** * MIT License * * Copyright (c) 2025 Chris M. Perez * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in all * copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ import { Effect, pipe } from 'effect'; import type { Scope } from 'effect'; import type { Signal, ReadonlySignal, WatchOptions, EffectOptions, EffectHandle, OnCleanup, } from '../types/index.js'; import type { Component } from '../render/node.js'; import { signal } from '../reactivity/index.js'; import { computed } from '../reactivity/computed.js'; import { watch as standaloneWatch, watchMultiple as standaloneWatchMultiple, } from '../effects/index.js'; import { watchEffect as standaloneEffect } from '../effects/effect.js'; import { createComponentLifecycleSync, type ComponentLifecycle, } from './lifecycle.js'; import { useCallback, useMemo } from './hooks.js'; import { getLayerContext, getLayerComponent, getLayerService, isLayerRuntimeReady, type LayerContext, type TypedLayerContext, } from '../layers/context.js'; import type { EffuseLayerRegistry, EffuseServiceRegistry, EffuseComponentRegistry, LayerPropsOf, LayerProvidesOf, } from '../layers/types.js'; import { LayerRuntimeNotReadyError, RouterNotConfiguredError, } from '../layers/errors.js'; import { StoreGetterNotConfiguredError, mapEffuseErrors } from '../errors.js'; export type ExposedValues = object; // eslint-disable-next-line @typescript-eslint/no-empty-object-type export interface EffuseRegistry {} type RouterType = EffuseRegistry extends { router: infer R } ? R : unknown; export interface ScriptContext

{ readonly props: Readonly

; expose: (values: ExposedValues) => void; signal: typeof signal; computed: (getter: () => T) => ReadonlySignal; store: (name: string) => unknown; router: RouterType; onMount: (callback: () => (() => void) | undefined) => void; onUnmount: (callback: () => void) => void; onBeforeMount: (callback: () => void) => void; onBeforeUnmount: (callback: () => void) => void; watch: ( source: Signal | (() => T), callback: ( newValue: T, oldValue: T | undefined, onCleanup: OnCleanup ) => void, options?: WatchOptions ) => void; watchMultiple: | (() => unknown))[]>( sources: T, callback: ( newValues: { [K in keyof T]: T[K] extends Signal ? V : T[K] extends () => infer V ? V : never; }, oldValues: { [K in keyof T]: T[K] extends Signal ? V | undefined : T[K] extends () => infer V ? V | undefined : never; }, onCleanup: OnCleanup ) => void, options?: WatchOptions ) => void; watchEffect: ( fn: (onCleanup: OnCleanup) => void | Promise, options?: EffectOptions ) => EffectHandle; useCallback: typeof useCallback; useMemo: typeof useMemo; useLayer: ( name: K ) => TypedLayerContext; useStore: { (key: K): EffuseServiceRegistry[K]; (key: string): unknown; }; useService: { (key: K): EffuseServiceRegistry[K]; (key: string): unknown; }; useLayerProps: ( name: K ) => LayerPropsOf | undefined; /** * @deprecated Use `useLayer(name).provides` for all layer providers, * or `useService(key)` for individual services. Will be removed in a future release. */ useLayerProvider: ( name: K ) => LayerProvidesOf | undefined; useComponent: { ( name: K ): EffuseComponentRegistry[K]; (name: string): Component | undefined; }; } export interface ScriptState { exposed: E; lifecycle: ComponentLifecycle; scope: Scope.CloseableScope | null; } let globalStoreGetter: ((name: string) => unknown) | null = null; let globalRouter: unknown = null; export const setGlobalStoreGetter = ( getter: (name: string) => unknown ): void => { globalStoreGetter = getter; }; export const setGlobalRouter = (router: unknown): void => { globalRouter = router; }; export const createScriptContext = ( props: P, storeGetter?: (name: string) => unknown ): { context: ScriptContext

; state: ScriptState } => { const lifecycle = createComponentLifecycleSync(); const state: ScriptState = { exposed: {} as E, lifecycle, scope: lifecycle.scope, }; const getStore = storeGetter ?? globalStoreGetter; const context: ScriptContext

= { props: Object.freeze({ ...props }), expose: (values: ExposedValues): void => { Object.assign(state.exposed, values); }, signal, computed, store: (name: string): unknown => { if (isLayerRuntimeReady()) { const layerService = getLayerService(name); if (layerService !== undefined) { return layerService; } } if (!getStore) { throw new StoreGetterNotConfiguredError({}); } return getStore(name); }, router: (() => { if (!globalRouter) { return new Proxy({} as object, { get: () => { throw new RouterNotConfiguredError(); }, }) as RouterType; } return globalRouter as RouterType; })(), onMount: (callback): void => { lifecycle.onMount(callback); }, onUnmount: (callback): void => { lifecycle.onUnmount(callback); }, onBeforeMount: (callback): void => { lifecycle.onBeforeMount(callback); }, onBeforeUnmount: (callback): void => { lifecycle.onBeforeUnmount(callback); }, watch: ( source: Signal | (() => T), callback: ( newValue: T, oldValue: T | undefined, onCleanup: OnCleanup ) => void, options?: WatchOptions ): void => { const handle = standaloneWatch(source, callback, options); lifecycle.onUnmount(() => handle.stop()); }, watchMultiple: (sources, callback, options): void => { // eslint-disable-next-line @typescript-eslint/no-explicit-any -- bridge ScriptContext types with WatchSource const handle = standaloneWatchMultiple( sources as any, callback as any, options ); lifecycle.onUnmount(() => handle.stop()); }, watchEffect: ( fn: (onCleanup: OnCleanup) => void | Promise, options?: EffectOptions ): EffectHandle => { const handle = standaloneEffect(fn, options); lifecycle.onUnmount(() => handle.stop()); return handle; }, useCallback, useMemo, useLayer: (( name: K ): TypedLayerContext => { if (!isLayerRuntimeReady()) { throw new LayerRuntimeNotReadyError({ layerName: name as string }); } return getLayerContext(name as string) as TypedLayerContext; }) as ScriptContext

['useLayer'], useStore: (key: string): unknown => { if (!isLayerRuntimeReady()) { if (getStore) { return getStore(key); } return undefined; } return getLayerService(key); }, useService: (key: string): unknown => { if (!isLayerRuntimeReady()) { return undefined; } return getLayerService(key); }, useLayerProps: (( name: K ): LayerPropsOf | undefined => { if (!isLayerRuntimeReady()) { return undefined; } const layerContext = getLayerContext(name as string); return layerContext.props as LayerPropsOf | undefined; }) as ScriptContext

['useLayerProps'], useLayerProvider: (( name: K ): LayerProvidesOf | undefined => { if (!isLayerRuntimeReady()) { return undefined; } const layerContext = getLayerContext(name as string); if (!layerContext.provides) { return undefined; } const providers: Record = {}; for (const key of Object.keys(layerContext.provides)) { providers[key] = getLayerService(key); } return providers as LayerProvidesOf; }) as ScriptContext

['useLayerProvider'], useComponent: (name: string): Component | undefined => { if (!isLayerRuntimeReady()) { return undefined; } return getLayerComponent(name) as Component | undefined; }, }; return { context, state }; }; export const runMountCallbacks = ( state: ScriptState ): void => { state.lifecycle.runMount(); }; export const runUnmountCallbacks = ( state: ScriptState ): void => { Effect.runSync(pipe(state.lifecycle.runCleanup(), mapEffuseErrors)); }; export type { LayerContext };