UNPKG

@zenithcore/core

Version:

Core functionality for ZenithKernel framework

951 lines (814 loc) 33 kB
/** * Signals-based Reactivity System for Zenith Framework * * Provides fine-grained reactivity using signals that integrate with the ECS system * for direct DOM updates without VDOM reconciliation overhead. */ import { ECSManager } from './ECSManager'; // Global tracking state let currentComputation: Computation | null = null; let currentBatch: Set<Signal<any>> | null = null; let batchDepth = 0; let scheduledUpdate: number | null = null; // Can be number for RAF, or a truthy sentinel for microtask // Infinite loop prevention const MAX_EFFECT_EXECUTIONS_PER_COMPUTATION = 1000; const EXECUTION_TIME_WINDOW = 100; // ms // Signal ID counter for unique identification let signalIdCounter = 0; // Debug mode for development let debugMode = false; const debugLog = (message: string, ...args: any[]) => { if (debugMode) console.log(`[Signals] ${message}`, ...args); }; // Type utilities for better TypeScript support export type MaybeSignal<T> = T | Signal<T>; export type UnwrapSignal<T> = T extends Signal<infer U> ? U : T; export type SignalValue<T> = T extends Signal<infer U> ? U : never; export type Signalize<T> = { [K in keyof T]: Signal<T[K]> }; // Enhanced error handling export class SignalError extends Error { constructor(message: string, public readonly signalId?: number, public readonly signalName?: string) { super(message); this.name = 'SignalError'; } } // --- ECS Integration Specifics --- /** * Data structure for storing signal information as an ECS component. */ export interface SignalComponentData<T = any> { signalId: number; value: T; type: 'signal'; name?: string; created: number; lastUpdated?: number; updateCount?: number; } /** * Placeholder class to act as the ComponentType for ECS interactions. */ export class SignalECSComponent { constructor(public data: SignalComponentData<any>) {} } // --- End ECS Integration Specifics --- const scheduleRafUpdate = () => { if (scheduledUpdate === null) { scheduledUpdate = requestAnimationFrame(() => { scheduledUpdate = null; flushUpdates(); }); } }; const scheduleMicrotaskUpdate = () => { if (scheduledUpdate === null) { scheduledUpdate = 1; Promise.resolve().then(() => { if (scheduledUpdate === 1) { scheduledUpdate = null; flushUpdates(); } }); } }; const flushUpdates = () => { if (!currentBatch) return; const uniqueComputationsToRun = new Set<Computation>(); const batchToProcess = Array.from(currentBatch); currentBatch = null; for (const signal of batchToProcess) { for (const comp of (signal as any)._subscribers as Set<Computation>) { if (!comp.isDisposed) { uniqueComputationsToRun.add(comp); } } } debugLog(`Flushing updates for ${uniqueComputationsToRun.size} computations from ${batchToProcess.length} signals.`); for (const comp of uniqueComputationsToRun) { if (!comp.isDisposed) { comp.execute(); } } }; export interface SignalOptions<T> { equals?: (a: T, b: T) => boolean; name?: string; ecsEntity?: number; ecsManager?: ECSManager; debug?: boolean; scheduler?: 'sync' | 'async' | 'raf'; errorHandler?: (error: Error, signal: Signal<T>) => void; } export interface AsyncSignalOptions<T> extends Omit<SignalOptions<T | undefined>, 'equals'> { equals?: (a: T | undefined, b: T | undefined) => boolean; initialState?: 'loading' | 'idle'; timeout?: number; retryCount?: number; retryDelay?: number; } export interface ComputedOptions<T> extends SignalOptions<T> { defer?: boolean; } export interface EffectOptions { name?: string; defer?: boolean; errorHandler?: (error: Error) => void; } export class Signal<T> { protected _value: T; private _id: number; private _name?: string; protected _equals: (a: T, b: T) => boolean; protected _subscribers = new Set<Computation>(); private _ecsEntity?: number; private _ecsManager?: ECSManager; private _debug: boolean; protected _scheduler: 'sync' | 'async' | 'raf'; // Changed to protected protected _errorHandler?: (error: Error, signal: Signal<T>) => void; protected _disposed = false; // Changed to protected private _accessCount = 0; private _updateCount = 0; private _lastAccess?: number; private _lastUpdate?: number; private _createdAt: number; constructor(initialValue: T, options: SignalOptions<T> = {}) { this._value = initialValue; this._id = ++signalIdCounter; this._name = options.name; this._equals = options.equals || Object.is; this._ecsEntity = options.ecsEntity; this._ecsManager = options.ecsManager; this._debug = options.debug || debugMode; this._scheduler = options.scheduler || 'sync'; this._errorHandler = options.errorHandler; this._createdAt = Date.now(); debugLog(`Created signal ${this._id}`, { name: this._name, value: initialValue, scheduler: this._scheduler }); if (typeof this._ecsEntity === 'number' && this._ecsManager && typeof this._ecsManager.addComponent === 'function') { try { const componentData: SignalComponentData<T> = { signalId: this._id, value: initialValue, type: 'signal', name: this._name || `signal_${this._id}`, created: this._createdAt }; const signalComponentInstance = new SignalECSComponent(componentData); this._ecsManager.addComponent(this._ecsEntity, SignalECSComponent, signalComponentInstance); debugLog(`Signal ${this.id} registered with ECS entity ${this._ecsEntity}`); } catch (error: any) { this._handleError(new SignalError(`Failed to register with ECS: ${error.message}`, this._id, this._name)); } } } get value(): T { if (this._disposed) { throw new SignalError(`Cannot access disposed signal ${this._name || this._id}`, this._id, this._name); } this._accessCount++; this._lastAccess = Date.now(); if (currentComputation && !currentComputation.isDisposed) { this._subscribers.add(currentComputation); currentComputation.dependencies.add(this); debugLog(`Tracked dependency: signal ${this._id} (${this._name}) -> computation ${currentComputation.name || 'anonymous'}`); } return this._value; } set value(newValue: T) { if (this._disposed) { throw new SignalError(`Cannot update disposed signal ${this._name || this._id}`, this._id, this._name); } if (!this._equals(this._value, newValue)) { const oldValue = this._value; this._value = newValue; this._updateCount++; this._lastUpdate = Date.now(); debugLog(`Signal ${this._id} (${this._name}) updated`, { oldValue, newValue, subscribers: this._subscribers.size }); this._notifySubscribers(); if (typeof this._ecsEntity === 'number' && this._ecsManager && typeof this._ecsManager.addComponent === 'function') { try { const componentData: SignalComponentData<T> = { signalId: this._id, value: newValue, lastUpdated: this._lastUpdate, updateCount: this._updateCount, type: 'signal', name: this._name || `signal_${this._id}`, created: this._createdAt }; const signalComponentInstance = new SignalECSComponent(componentData); this._ecsManager.addComponent(this._ecsEntity, SignalECSComponent, signalComponentInstance); debugLog(`Signal ${this.id} updated ECS entity ${this._ecsEntity}`); } catch (error: any) { this._handleError(new SignalError(`Failed to update ECS: ${error.message}`, this._id, this._name)); } } } } protected _notifySubscribers(): void { if (this._scheduler === 'sync' && batchDepth === 0) { this._flushNotifications(); return; } if (!currentBatch) { currentBatch = new Set(); } currentBatch.add(this); if (batchDepth === 0) { if (this._scheduler === 'raf') { scheduleRafUpdate(); } else { // 'async' or implicit async due to batching scheduleMicrotaskUpdate(); } } } protected _flushNotifications(): void { if(this._subscribers.size === 0) return; debugLog(`Flushing notifications for signal ${this.id} (${this.name}) to ${this._subscribers.size} subscribers`); const subscribers = Array.from(this._subscribers); for (const computation of subscribers) { if (!computation.isDisposed) { try { computation.execute(); } catch (error: any) { this._handleError(new SignalError(`Computation error during flush for signal ${this.id}: ${error.message}`, this._id, this._name)); } } } } peek(): T { return this._value; } get id(): number { return this._id; } get name(): string | undefined { return this._name; } get subscriberCount(): number { return this._subscribers.size; } get isDisposed(): boolean { return this._disposed; } get accessCount(): number { return this._accessCount; } get updateCount(): number { return this._updateCount; } get lastAccess(): number | undefined { return this._lastAccess; } get lastUpdate(): number | undefined { return this._lastUpdate; } dispose(): void { if (this._disposed) return; debugLog(`Disposing signal ${this._id}`, { name: this._name, subscribers: this._subscribers.size }); // First mark as disposed to prevent any new updates this._disposed = true; // Immediately notify subscribers to trigger cleanup const subscribersToNotify = Array.from(this._subscribers); this._subscribers.clear(); // Execute subscriber cleanup synchronously for (const comp of subscribersToNotify) { // First remove from dependencies to prevent re-entrancy comp.dependencies.delete(this); // Then execute the computation to trigger cleanup if (!comp.isDisposed) { comp.execute(); } } // Clean up ECS integration if (typeof this._ecsEntity === 'number' && this._ecsManager && typeof this._ecsManager.removeComponent === 'function') { try { this._ecsManager.removeComponent(this._ecsEntity, SignalECSComponent); debugLog(`Signal ${this.id} removed from ECS entity ${this._ecsEntity}`); } catch (error: any) { this._handleError(new SignalError(`Failed to cleanup ECS: ${error.message}`, this._id, this._name)); } } } private _handleError(error: SignalError): void { if (this._errorHandler) { this._errorHandler(error, this); } else { console.error(error.message, error.signalId ? `(Signal ID: ${error.signalId})` : '', error.signalName ? `(Name: ${error.signalName})` : ''); } } map<U>(mapper: (value: T) => U, options?: Omit<ComputedOptions<U>, 'defer' | 'name' | 'debug' | 'scheduler' | 'errorHandler'>): ComputedSignal<U> { return new ComputedSignal(() => mapper(this.value), { name: this._name ? `${this._name}.map` : undefined, debug: this._debug, scheduler: this._scheduler, errorHandler: this._errorHandler ? (err: Error, sig: Signal<U>) => this._errorHandler!(err, this as any) : undefined, ...options, }); } filter(predicate: (value: T) => boolean, options?: Omit<ComputedOptions<T|undefined>, 'defer' | 'name' | 'debug' | 'scheduler' | 'errorHandler'>): ComputedSignal<T | undefined> { return new ComputedSignal(() => predicate(this.value) ? this.value : undefined, { name: this._name ? `${this._name}.filter` : undefined, debug: this._debug, scheduler: this._scheduler, errorHandler: this._errorHandler ? (err: Error, sig: Signal<T|undefined>) => this._errorHandler!(err, this as any) : undefined, ...options, }); } } export class Computation { public dependencies = new Set<Signal<any>>(); private _fn: () => void | (() => void); private _cleanup?: () => void; private _name?: string; private _disposed = false; private _executing = false; private _executionCount = 0; private _lastExecution?: number; private _errorHandler?: (error: Error) => void; public _isEffect = true; private _recentExecutions: number[] = []; // Track recent execution times private _pendingExecution = false; // Track if execution is pending constructor(fn: () => void | (() => void), options: EffectOptions = {}, isEffect:boolean = true) { this._fn = fn; this._name = options.name; this._errorHandler = options.errorHandler; this._isEffect = isEffect; debugLog(`Created computation ${this._name || 'anonymous'}`, {isEffect}); if (!options.defer) { this.execute(); } } execute(): void { if (this._disposed) { debugLog(`Attempted to execute disposed computation ${this._name || 'anonymous'}`); return; } // For effects, mark pending execution instead of blocking it if (this._executing && this._isEffect) { this._pendingExecution = true; debugLog(`Computation ${this._name || 'anonymous'} (effect) already executing, marking for re-execution.`); return; } // Prevent infinite loops by tracking executions per computation if (this._isEffect) { const now = Date.now(); // Clean old executions outside the time window this._recentExecutions = this._recentExecutions.filter(time => now - time < EXECUTION_TIME_WINDOW); // Check if we're executing too frequently if (this._recentExecutions.length >= MAX_EFFECT_EXECUTIONS_PER_COMPUTATION) { debugLog(`Effect execution limit reached (${MAX_EFFECT_EXECUTIONS_PER_COMPUTATION} in ${EXECUTION_TIME_WINDOW}ms), preventing infinite loop for ${this._name || 'anonymous'}`); return; } // Record this execution this._recentExecutions.push(now); } this._executing = true; this._executionCount++; this._lastExecution = Date.now(); debugLog(`Executing computation ${this._name || 'anonymous'}`, { count: this._executionCount, recentExecutions: this._recentExecutions.length }); // Before re-running, remove this computation from its old dependencies' subscriber lists for (const signal of this.dependencies) { (signal as any)._subscribers.delete(this); } this.dependencies.clear(); // Clear its own dependency list for the new run if (this._cleanup) { try { this._cleanup(); } catch (error: any) { this._handleError(error); } this._cleanup = undefined; } const prevComputation = currentComputation; currentComputation = this; try { const result = this._fn(); // Running the function will re-populate this.dependencies if (typeof result === 'function') { this._cleanup = result; } } catch (error: any) { this._handleError(error); } finally { currentComputation = prevComputation; this._executing = false; // Check if we need to run again due to changes during execution if (this._pendingExecution && !this._disposed) { this._pendingExecution = false; debugLog(`Computation ${this._name || 'anonymous'} has pending execution, running again.`); this.execute(); // Recursive call, but with protection against infinite loops } } } dispose(): void { if (this._disposed) return; this._disposed = true; debugLog(`Disposing computation ${this._name || 'anonymous'}`); for (const signal of this.dependencies) { (signal as any)._subscribers.delete(this); } this.dependencies.clear(); if (this._cleanup) { try { this._cleanup(); } catch(e:any) { this._handleError(e); } this._cleanup = undefined; } } get name(): string | undefined { return this._name; } get isDisposed(): boolean { return this._disposed; } get isExecuting(): boolean { return this._executing; } get executionCount(): number { return this._executionCount; } get lastExecution(): number | undefined { return this._lastExecution; } get dependencyCount(): number { return this.dependencies.size; } private _handleError(error: Error): void { if (this._errorHandler) { this._errorHandler(error); } else { console.error(`Computation error in ${this._name || 'anonymous'}:`, error); } } } export class AsyncSignal<T> extends Signal<T | undefined> { private _loadingSignal: Signal<boolean>; private _errorSignal: Signal<Error | null>; private _timeout?: number; private _retryCount: number; private _retryDelay: number; private _currentRetry = 0; private _loadFn: () => Promise<T>; private _currentLoadPromise: Promise<void> | null = null; constructor( initialValue: T | undefined, loadFn: () => Promise<T>, options: AsyncSignalOptions<T> = {} ) { const baseOptions: SignalOptions<T | undefined> = { name: options.name, ecsEntity: options.ecsEntity, ecsManager: options.ecsManager, debug: options.debug, scheduler: options.scheduler || 'sync', errorHandler: options.errorHandler, equals: options.equals }; super(initialValue, baseOptions); this._loadFn = loadFn; this._timeout = options.timeout; this._retryCount = options.retryCount || 0; this._retryDelay = options.retryDelay || 1000; const isLoadingInitially = options.initialState === 'loading'; this._loadingSignal = signal(isLoadingInitially, { name: options.name ? `${options.name}.$loading` : undefined, scheduler: 'sync' }); this._errorSignal = signal<Error | null>(null, { name: options.name ? `${options.name}.$error` : undefined, scheduler: 'sync' }); if (isLoadingInitially) { // If reload() returns a promise, we might want to handle it, but constructor can't be async. // The reload call here will kick off the process. this.reload().catch(err => { // If reload itself throws (unlikely with current structure, but good practice) // or if _attemptLoad throws and isn't caught internally before this point. // This usually means an issue in _attemptLoad's immediate execution path. debugLog(`AsyncSignal ${this.id} (${this.name}) initial reload() call resulted in error: `, err); if(this._errorSignal.peek() === null) { // If _attemptLoad didn't set an error this._errorSignal.value = err instanceof Error ? err : new SignalError(String(err), this.id, this.name); } if(this._loadingSignal.peek()) { this._loadingSignal.value = false; } }); } } get loading(): boolean { return this._loadingSignal.value; } get error(): Error | null { return this._errorSignal.value; } get isSuccess(): boolean { // Check peek() to avoid triggering a read dependency on this computed property itself. return !this._loadingSignal.peek() && !this._errorSignal.peek() && this.peek() !== undefined; } async reload(): Promise<void> { if (this._loadingSignal.peek() && this._currentLoadPromise) { debugLog(`AsyncSignal ${this.id} (${this.name}) reload called while already loading.`); return this._currentLoadPromise; } this._loadingSignal.value = true; this._errorSignal.value = null; this._currentRetry = 0; this._currentLoadPromise = this._attemptLoad(); return this._currentLoadPromise; } private async _attemptLoad(): Promise<void> { let result: T | undefined = undefined; let caughtError: Error | null = null; debugLog(`AsyncSignal ${this.id} (${this.name}) attempting load (attempt ${this._currentRetry + 1}).`); try { let loadPromise = this._loadFn(); if (this._timeout) { const timeoutPromise = new Promise<never>((_, reject) => { setTimeout(() => reject(new SignalError(`Timeout after ${this._timeout}ms`, this.id, this.name)), this._timeout); }); loadPromise = Promise.race([loadPromise, timeoutPromise]); } result = await loadPromise; } catch (error: any) { caughtError = error instanceof Error ? error : new SignalError(String(error), this.id, this.name); } // This function updates the signal's state. It should be robust. const updateSignalStates = () => { if (this.isDisposed) { debugLog(`AsyncSignal ${this.id} (${this.name}) disposed during load/retry. Aborting state update.`); return; } if(caughtError) { if (this._currentRetry < this._retryCount) { this._currentRetry++; debugLog(`AsyncSignal ${this.id} (${this.name}) retrying load... (${this._currentRetry}/${this._retryCount})`, { error: caughtError }); // Keep loading=true, error=null for the retry attempt. // No change to this.value yet. this._loadingSignal.value = true; // Ensure loading is true for retry this._errorSignal.value = null; // Clear previous attempt's specific error setTimeout(() => { if(!this.isDisposed) this._attemptLoad(); }, this._retryDelay * Math.pow(2, this._currentRetry - 1)); // We are still effectively loading because a retry is scheduled. // So, _loadingSignal should remain true. } else { debugLog(`AsyncSignal ${this.id} (${this.name}) load failed after all retries.`, { error: caughtError }); this._errorSignal.value = caughtError; this._loadingSignal.value = false; // Loading finished (failed) } } else { // Success debugLog(`AsyncSignal ${this.id} (${this.name}) loaded successfully.`, { value: result }); this.value = result; // This will set _value and notify subscribers this._errorSignal.value = null; this._loadingSignal.value = false; // Loading finished (success) } }; // Apply state updates. If scheduler is sync and not in a batch, updates happen immediately. // Otherwise, they are batched. if (this._scheduler === 'sync' && batchDepth === 0) { updateSignalStates(); } else { batch(updateSignalStates); } // Clear the current load promise only if the load attempt is truly finished // (i.e., success, or all retries exhausted and it failed). if (!caughtError || this._currentRetry >= this._retryCount) { this._currentLoadPromise = null; } } dispose(): void { // TODO: Consider cancelling any ongoing _currentLoadPromise if possible (e.g. AbortController) this._loadingSignal.dispose(); this._errorSignal.dispose(); super.dispose(); } } export class ComputedSignal<T> extends Signal<T> { private _computation: Computation; private readonly _fn: () => T; constructor(fn: () => T, options: ComputedOptions<T> = {}) { const { defer, errorHandler, ...baseSignalOptions } = options; super(undefined as T, baseSignalOptions); this._fn = fn; let adaptedComputationErrorHandler: ((error: Error) => void) | undefined = undefined; if (errorHandler) { adaptedComputationErrorHandler = (err: Error) => errorHandler(err, this); } else if (this._errorHandler) { adaptedComputationErrorHandler = (err: Error) => this._errorHandler!(err, this); } this._computation = new Computation(() => { const newValue = this._fn(); // Update if value changed OR if it's the first successful computation and _value is placeholder // The this._computation.executionCount === 1 check helps ensure the initial value is set correctly, // especially if the computed value might be undefined but valid. if ((this._computation.executionCount === 1 && this._value === undefined && newValue !== undefined) || !this._equals(this._value, newValue)) { this._value = newValue; this._notifySubscribers(); } }, { defer: true, // Internal computation always starts deferred from its own perspective. // The ComputedSignal's `defer` option controls if we execute it immediately after setup. name: options.name || (this._fn && this._fn.name ? `computed(${this._fn.name})` : 'computed'), errorHandler: adaptedComputationErrorHandler }, false); // isEffect = false for pure computed if (!defer) { // If ComputedSignal itself is not deferred by user option this._computation.execute(); } } get value(): T { if (this._disposed) { throw new SignalError(`Cannot access disposed computed signal ${this.name || this.id}`); } // If the computation has never run (its executionCount is 0, implies it was deferred or initial execute didn't set a value) // and it's not disposed, execute it now. This handles deferred computations on first access. // A computed value is also re-evaluated if its dependencies change, which is handled by the effect system // making this._computation.execute() be called by a dependency. if (this._computation.executionCount === 0 && !this._computation.isDisposed) { debugLog(`Computed signal ${this.id} (${this.name}) accessed, running initial/deferred computation.`); this._computation.execute(); } return super.value; } set value(_: T) { throw new SignalError('Cannot set value of computed signal', this.id, this.name); } dispose(): void { this._computation.dispose(); super.dispose(); } } export function signal<T>(initialValue: T, options?: SignalOptions<T>): Signal<T> { return new Signal(initialValue, options); } export function computed<T>(fn: () => T, options?: ComputedOptions<T>): ComputedSignal<T> { return new ComputedSignal(fn, options); } export function asyncSignal<T>( loadFn: () => Promise<T>, options?: AsyncSignalOptions<T> ): AsyncSignal<T> { return new AsyncSignal(undefined, loadFn, {initialState: 'idle', ...options}); } export function asyncSignalWithInitial<T>( initialValue: T, loadFn: () => Promise<T>, options?: AsyncSignalOptions<T> ): AsyncSignal<T> { return new AsyncSignal(initialValue, loadFn, {initialState: 'idle', ...options}); } export function effect(fn: () => void | (() => void), options?: EffectOptions): Computation { return new Computation(fn, options, true); } export function resource<T>( loadFn: () => Promise<T>, options?: AsyncSignalOptions<T> ): [Signal<T | undefined>, { readonly loading: Signal<boolean>; readonly error: Signal<Error | null>; reload: () => Promise<void> }] { const asyncSig = new AsyncSignal<T>(undefined, loadFn, { ...options, initialState: 'loading' }); const valueSignal = computed<T | undefined>(() => asyncSig.value, { name: options?.name ? `${options.name}.$value` : undefined, scheduler: options?.scheduler || 'sync' }); const loadingSignal = computed(() => asyncSig.loading, { name: options?.name ? `${options.name}.$loadingState` : undefined, scheduler: 'sync' }); const errorSignal = computed<Error | null>(() => asyncSig.error, { name: options?.name ? `${options.name}.$errorState` : undefined, scheduler: 'sync' }); return [ valueSignal, { loading: loadingSignal, error: errorSignal, reload: () => asyncSig.reload() } ]; } export function batch<T>(fn: () => T): T { if (batchDepth > 0) { debugLog("Nested batch call"); return fn(); } batchDepth++; debugLog(`Batch started (depth: ${batchDepth})`); const isOuterMostBatch = currentBatch === null; if (isOuterMostBatch) { currentBatch = new Set(); } let result: T; try { result = fn(); if (batchDepth === 1) { debugLog(`Top-level batch finished, flushing updates...`); flushUpdates(); } } finally { batchDepth--; debugLog(`Batch ended (depth: ${batchDepth})`); if (batchDepth === 0) { if (isOuterMostBatch && currentBatch !== null) { currentBatch = null; debugLog("Global batch context explicitly cleared post-outermost batch."); } } } return result; } export function untrack<T>(fn: () => T): T { const prevComputation = currentComputation; currentComputation = null; try { return fn(); } finally { currentComputation = prevComputation; } } export function ecsSignal<T>( initialValue: T, ecsEntity: number, ecsManager: ECSManager, options?: Omit<SignalOptions<T>, 'ecsEntity' | 'ecsManager'> ): Signal<T> { return new Signal(initialValue, { ...options, ecsEntity, ecsManager }); } export function signalObject<T extends Record<string, any>>( obj: T, options?: SignalOptions<any> ): { [K in keyof T]: Signal<T[K]> } { const result = {} as { [K in keyof T]: Signal<T[K]> }; for (const [key, value] of Object.entries(obj)) { result[key as keyof T] = signal(value, { ...options, name: options?.name ? `${options.name}.${key}` : key }); } return result; } export function createStore<T extends Record<string, any>>( initialState: T, options?: SignalOptions<any> ): T & { [K in keyof T as `$${string & K}`]: Signal<T[K]> } { const signals = signalObject(initialState, options); const store = {} as any; for (const [key, signalInstance] of Object.entries(signals)) { Object.defineProperty(store, key, { get: () => signalInstance.value, set: (value) => { signalInstance.value = value; }, enumerable: true, configurable: true }); store[`$${key}`] = signalInstance; } return store; } export function getCurrentComputation(): Computation | null { return currentComputation; } export function isInBatch(): boolean { return batchDepth > 0; } export function setDebugMode(enabled: boolean): void { debugMode = enabled; debugLog(`Debug mode ${enabled ? 'enabled' : 'disabled'}`); } export function getSignalRegistry(): Map<number, Signal<any>> { console.warn("getSignalRegistry: Global signal registry not implemented in this version."); return new Map(); } export function resolve<T>(value: MaybeSignal<T>): T { return value instanceof Signal ? value.value : value; } export function isSignal<T>(value: any): value is Signal<T> { return value instanceof Signal; } export function derived<T, U>( source: Signal<T>, mapper: (value: T) => U, options?: ComputedOptions<U> ): ComputedSignal<U> { return computed(() => mapper(source.value), { ...options, name: options?.name || (source.name ? `${source.name}.derived` : undefined) }); } export function derivedWithSetter<T, U>( source: Signal<T>, getter: (value: T) => U, setter: (newValue: U, oldSourceValue: T) => T, options?: SignalOptions<U> ): Signal<U> { const derivedSignal = signal(getter(source.peek()), options); let internalUpdate = false; effect(() => { if (internalUpdate) return; const newDerivedValue = getter(source.value); internalUpdate = true; derivedSignal.value = newDerivedValue; internalUpdate = false; }); effect(() => { if (internalUpdate) return; const newSourceValue = setter(derivedSignal.value, source.peek()); internalUpdate = true; source.value = newSourceValue; internalUpdate = false; }); return derivedSignal; } export function combine<T extends readonly unknown[]>( signals: { [K in keyof T]: Signal<T[K]> }, options?: ComputedOptions<[...{ [K in keyof T]: UnwrapSignal<typeof signals[K]> }]> ): ComputedSignal<[...{ [K in keyof T]: UnwrapSignal<typeof signals[K]> }]> { type ResultTuple = [...{ [K in keyof T]: UnwrapSignal<typeof signals[K]> }]; return computed(() => signals.map(s => s.value) as ResultTuple, options); } export function fromPromise<T>( promise: Promise<T>, options?: AsyncSignalOptions<T> ): AsyncSignal<T> { const asyncOptions: AsyncSignalOptions<T> = { initialState: 'loading', scheduler: 'sync', // Default to sync for fromPromise to make initial loading state more predictable ...options, }; return new AsyncSignal(undefined, () => promise, asyncOptions); } export function fromEvent<E, T = E>( emitter: { addEventListener(event: string, handler: (data: E) => void): void; removeEventListener?(event: string, handler: (data: E) => void): void; }, event: string, initialValue: T, mapper?: (eventData: E) => T, options?: SignalOptions<T> ): Signal<T> { const sig = signal(initialValue, options); const handler = (data: E) => { sig.value = mapper ? mapper(data) : data as unknown as T; }; emitter.addEventListener(event, handler); // TODO: Implement listener removal on signal disposal. return sig; }