UNPKG

signalforge

Version:

Fine-grained reactive state management with automatic dependency tracking - Ultra-optimized, zero dependencies

403 lines (402 loc) 12.4 kB
import { startLatencyMeasurement, endLatencyMeasurement, isProfilerEnabled } from './performanceProfiler'; class DevToolsEventEmitter { constructor() { this.listeners = new Map(); this.sequence = 0; } on(eventType, listener) { const key = eventType; if (!this.listeners.has(key)) { this.listeners.set(key, new Set()); } this.listeners.get(key).add(listener); return () => this.off(eventType, listener); } off(eventType, listener) { const key = eventType; const listenerSet = this.listeners.get(key); if (listenerSet) { listenerSet.delete(listener); if (listenerSet.size === 0) { this.listeners.delete(key); } } } emit(eventType, payload) { const event = { type: eventType, payload, timestamp: Date.now(), sequence: ++this.sequence, }; const specificListeners = this.listeners.get(eventType); if (specificListeners) { for (const listener of specificListeners) { try { listener(event); } catch (error) { console.error(`[DevTools] Error in listener for ${eventType}:`, error); } } } const wildcardListeners = this.listeners.get('*'); if (wildcardListeners) { for (const listener of wildcardListeners) { try { listener(event); } catch (error) { console.error('[DevTools] Error in wildcard listener:', error); } } } } removeAllListeners() { this.listeners.clear(); this.sequence = 0; } getListenerCount(eventType) { if (eventType) { return this.listeners.get(eventType)?.size || 0; } let total = 0; for (const listenerSet of this.listeners.values()) { total += listenerSet.size; } return total; } } export let __DEVTOOLS__ = typeof process !== 'undefined' ? process.env.NODE_ENV !== 'production' : true; const config = { enabled: false, trackPerformance: true, logToConsole: false, maxPerformanceSamples: 1000, slowUpdateThreshold: 16, emitPerformanceWarnings: true, }; const signalRegistry = new Map(); const signalToIdMap = new WeakMap(); const performanceMetrics = []; let signalIdCounter = 0; const eventEmitter = new DevToolsEventEmitter(); export function enableDevTools(options = {}) { __DEVTOOLS__ = true; Object.assign(config, options); config.enabled = true; if (config.logToConsole) { console.log('[SignalForge DevTools] Enabled', config); } } export function disableDevTools() { __DEVTOOLS__ = false; config.enabled = false; if (config.logToConsole) { console.log('[SignalForge DevTools] Disabled'); } } export function isDevToolsEnabled() { return __DEVTOOLS__ && config.enabled; } export function getDevToolsConfig() { return { ...config }; } export function registerSignal(signal, type, initialValue, name) { if (!isDevToolsEnabled()) { return ''; } const id = `${type}_${++signalIdCounter}`; let creationStack; if (typeof Error !== 'undefined') { try { throw new Error(); } catch (e) { creationStack = e.stack ?.split('\n') .slice(2, 6) .join('\n'); } } const metadata = { id, name, type, value: safeSerialize(initialValue), subscriberCount: 0, dependencies: [], subscribers: [], createdAt: Date.now(), updatedAt: Date.now(), updateCount: 0, creationStack, }; signalRegistry.set(id, metadata); signalToIdMap.set(signal, id); eventEmitter.emit('signal-created', { id, type, initialValue: metadata.value, name, creationStack, }); if (config.logToConsole) { console.log(`[SignalForge] Created ${type} "${id}"`, metadata); } return id; } export function unregisterSignal(signal) { if (!isDevToolsEnabled()) { return; } const id = signalToIdMap.get(signal); if (!id) { return; } const metadata = signalRegistry.get(id); if (metadata) { const lifetime = Date.now() - metadata.createdAt; eventEmitter.emit('signal-destroyed', { id, type: metadata.type, finalValue: metadata.value, lifetime, }); } signalRegistry.delete(id); signalToIdMap.delete(signal); if (config.logToConsole) { console.log(`[SignalForge] Destroyed signal "${id}"`); } } export function trackUpdate(signal, updateFn, previousValue) { if (!isDevToolsEnabled()) { updateFn(); return; } const id = signalToIdMap.get(signal); if (!id) { updateFn(); return; } const metadata = signalRegistry.get(id); if (!metadata) { updateFn(); return; } const startTime = performance.now(); let skipped = false; let newValue; if (isProfilerEnabled()) { startLatencyMeasurement(id, metadata.subscribers.length); } try { updateFn(); newValue = (signal._peek ? signal._peek() : signal.get()); skipped = Object.is(previousValue, newValue); } catch (error) { console.error(`[SignalForge] Error updating signal "${id}"`, error); throw error; } finally { const endTime = performance.now(); const duration = endTime - startTime; if (isProfilerEnabled()) { endLatencyMeasurement(id, metadata.type, skipped); } metadata.value = safeSerialize(newValue); metadata.updatedAt = Date.now(); if (!skipped) { metadata.updateCount++; } if (config.trackPerformance) { const metric = { signalId: id, type: metadata.type, timestamp: startTime, duration, skipped, previousValue: safeSerialize(previousValue), newValue: safeSerialize(newValue), }; addPerformanceMetric(metric); if (config.emitPerformanceWarnings && duration > config.slowUpdateThreshold) { eventEmitter.emit('performance-warning', { signalId: id, type: metadata.type, duration, threshold: config.slowUpdateThreshold, message: `Slow update detected: ${duration.toFixed(2)}ms (threshold: ${config.slowUpdateThreshold}ms)`, }); } if (config.logToConsole && duration > config.slowUpdateThreshold) { console.warn(`[SignalForge] Slow update: "${id}" took ${duration.toFixed(2)}ms`, metric); } } eventEmitter.emit('signal-updated', { id, previousValue: safeSerialize(previousValue), newValue: safeSerialize(newValue), duration, skipped, }); } } export function trackDependency(subscriber, dependency) { if (!isDevToolsEnabled()) { return; } const subscriberId = signalToIdMap.get(subscriber); const dependencyId = signalToIdMap.get(dependency); if (!subscriberId || !dependencyId) { return; } const subscriberMeta = signalRegistry.get(subscriberId); const dependencyMeta = signalRegistry.get(dependencyId); if (!subscriberMeta || !dependencyMeta) { return; } if (!subscriberMeta.dependencies.includes(dependencyId)) { subscriberMeta.dependencies.push(dependencyId); } if (!dependencyMeta.subscribers.includes(subscriberId)) { dependencyMeta.subscribers.push(subscriberId); } dependencyMeta.subscriberCount = dependencyMeta.subscribers.length; eventEmitter.emit('dependency-added', { subscriberId, dependencyId, }); } export function untrackDependency(subscriber, dependency) { if (!isDevToolsEnabled()) { return; } const subscriberId = signalToIdMap.get(subscriber); const dependencyId = signalToIdMap.get(dependency); if (!subscriberId || !dependencyId) { return; } const subscriberMeta = signalRegistry.get(subscriberId); const dependencyMeta = signalRegistry.get(dependencyId); if (!subscriberMeta || !dependencyMeta) { return; } subscriberMeta.dependencies = subscriberMeta.dependencies.filter(id => id !== dependencyId); dependencyMeta.subscribers = dependencyMeta.subscribers.filter(id => id !== subscriberId); dependencyMeta.subscriberCount = dependencyMeta.subscribers.length; eventEmitter.emit('dependency-removed', { subscriberId, dependencyId, }); } export function listSignals() { if (!isDevToolsEnabled()) { console.warn('[SignalForge] DevTools is not enabled'); return []; } return Array.from(signalRegistry.values()).sort((a, b) => a.createdAt - b.createdAt); } export function getActivePlugins() { if (!isDevToolsEnabled()) { return []; } try { const pluginManager = require('../core/pluginManager'); if (pluginManager && pluginManager.__getPluginInfoForDevTools) { return pluginManager.__getPluginInfoForDevTools(); } } catch (error) { console.debug('[DevTools] Plugin manager not available'); } return []; } export function getSignal(id) { if (!isDevToolsEnabled()) { console.warn('[SignalForge] DevTools is not enabled'); return undefined; } return signalRegistry.get(id); } export function getDependencies(id) { if (!isDevToolsEnabled()) { console.warn('[SignalForge] DevTools is not enabled'); return []; } const metadata = signalRegistry.get(id); return metadata?.dependencies || []; } export function getSubscribers(id) { if (!isDevToolsEnabled()) { console.warn('[SignalForge] DevTools is not enabled'); return []; } const metadata = signalRegistry.get(id); return metadata?.subscribers || []; } export function onDevToolsEvent(eventType, listener) { return eventEmitter.on(eventType, listener); } export function offDevToolsEvent(eventType, listener) { eventEmitter.off(eventType, listener); } export function getEventListenerCount(eventType) { return eventEmitter.getListenerCount(eventType); } export function clearEventListeners() { eventEmitter.removeAllListeners(); } export function getPerformanceMetrics(limit) { if (!isDevToolsEnabled()) { console.warn('[SignalForge] DevTools is not enabled'); return []; } const metrics = [...performanceMetrics]; return limit ? metrics.slice(-limit) : metrics; } export function clearPerformanceMetrics() { performanceMetrics.length = 0; } function safeSerialize(value) { try { if (value === null || typeof value === 'undefined' || typeof value === 'boolean' || typeof value === 'number' || typeof value === 'string') { return value; } if (typeof value === 'function') { return `[Function: ${value.name || 'anonymous'}]`; } if (value instanceof Date) { return value.toISOString(); } if (Array.isArray(value)) { return value.slice(0, 10).map(safeSerialize); } if (typeof value === 'object') { const keys = Object.keys(value).slice(0, 10); const result = {}; for (const key of keys) { result[key] = safeSerialize(value[key]); } return result; } return String(value); } catch (error) { return '[Unserializable]'; } } function addPerformanceMetric(metric) { performanceMetrics.push(metric); if (performanceMetrics.length > config.maxPerformanceSamples) { performanceMetrics.shift(); } }