import { Effect, Scope, Exit, Predicate } from 'effect'; import { signal } from '../reactivity/signal.js'; import { computed } from '../reactivity/computed.js'; import { watchEffect as reactiveEffect } from '../effects/effect.js'; import { getLayerContext, getLayerService, isLayerRuntimeReady, } from '../layers/context.js'; import { traceHookEffect, traceHookCleanup, traceHookDispose, } from '../layers/tracing/hooks.js'; import { HookLayerNotReadyError } from './errors.js'; import type { HookContext, HookCleanup, HookScope, HookFinalizer, EffectCallback, } from './types.js'; import type { LayerPropsOf, LayerProvidesOf, EffuseLayerRegistry, } from '../layers/types.js'; const createHookScope = (): { scope: HookScope } => { const internalScope = Effect.runSync(Scope.make()); const finalizers: HookFinalizer[] = []; const scope: HookScope = { addFinalizer: (fn: HookFinalizer) => { finalizers.push(fn); }, dispose: async () => { for (const fn of finalizers.reverse()) { await fn(); } Effect.runSync(Scope.close(internalScope, Exit.void)); }, }; return { scope }; }; export const createHookContext = ( config: C, hookName?: string ): { ctx: HookContext; dispose: () => Promise; mountCallbacks: EffectCallback[]; } => { const cleanups: HookCleanup[] = []; const mountCallbacks: EffectCallback[] = []; const { scope } = createHookScope(); const name = hookName ?? 'anonymous'; let effectIndex = 0; const wrappedEffect = (fn: EffectCallback) => { const currentIndex = effectIndex++; reactiveEffect(() => { const start = performance.now(); const result = fn(); const duration = performance.now() - start; traceHookEffect(name, currentIndex, duration); if (Predicate.isFunction(result)) { cleanups.push(() => { traceHookCleanup(`${name}[${String(currentIndex)}]`); result(); }); } }); }; const onMount = (fn: EffectCallback) => { mountCallbacks.push(fn); }; const layer = ( name: K ): LayerPropsOf => { if (!isLayerRuntimeReady()) { throw new HookLayerNotReadyError({ hookContext: 'layer', layerName: name as string, }); } const layerCtx = getLayerContext(name as string); return layerCtx.props as LayerPropsOf; }; const layerProvider = ( name: K ): LayerProvidesOf => { if (!isLayerRuntimeReady()) { throw new HookLayerNotReadyError({ hookContext: 'layerProvider', layerName: name as string, }); } const layerCtx = getLayerContext(name as string); if (!layerCtx.provides) { return {} as LayerProvidesOf; } const providers: Record = {}; for (const key of Object.keys(layerCtx.provides)) { providers[key] = getLayerService(key); } return providers as LayerProvidesOf; }; const use = (hook: () => R): R => hook(); const runAsync = async (fn: () => Promise): Promise => fn(); const dispose = async () => { const start = performance.now(); const cleanupCount = cleanups.length; for (const cleanup of cleanups.reverse()) { cleanup(); } await scope.dispose(); const duration = performance.now() - start; traceHookDispose(name, duration, cleanupCount); }; const ctx: HookContext = { config, signal, computed, watchEffect: wrappedEffect, onMount, scope, layer, layerProvider, use, runAsync, }; return { ctx, dispose, mountCallbacks }; };