UNPKG

signalforge

Version:

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

450 lines (415 loc) 14.8 kB
/** * JSI Bridge for SignalForge - TypeScript Wrapper * * This module provides a seamless bridge between JavaScript and the native JSI implementation. * It automatically detects if the native module is available and falls back to the pure JS * implementation if not. * * Architecture: * - When native JSI bindings are loaded, uses direct C++ calls for maximum performance * - Falls back gracefully to JavaScript implementation on web or when native unavailable * - Provides identical API regardless of implementation (transparent to consumers) * - Zero-cost abstraction: no overhead when using native implementation * * Compatible with: * - React Native with Hermes engine * - React Native with JSC (JavaScriptCore) engine * - Falls back on Web/Node.js environments */ // Note: Store import removed - fallback implementation would need full Store API // For now, native implementation is primary target // ============================================================================ // Type Definitions // ============================================================================ /** * Signal reference returned by create operations * Contains the unique identifier used for all subsequent operations */ export interface SignalRef { id: string; } /** * Hermes internal API declaration for engine detection */ declare const HermesInternal: any | undefined; /** * Native JSI function signatures * These are injected into global scope by the C++ JSI bindings */ declare global { var __signalForgeCreateSignal: ((initialValue: any) => string) | undefined; var __signalForgeGetSignal: ((signalId: string) => any) | undefined; var __signalForgeSetSignal: ((signalId: string, value: any) => void) | undefined; var __signalForgeHasSignal: ((signalId: string) => boolean) | undefined; var __signalForgeDeleteSignal: ((signalId: string) => void) | undefined; var __signalForgeGetVersion: ((signalId: string) => number) | undefined; var __signalForgeBatchUpdate: ((updates: [string, any][]) => void) | undefined; } // ============================================================================ // Native Module Detection // ============================================================================ /** * Check if native JSI bindings are available * The C++ module installs these functions on the global object when loaded * * Detection strategy: * 1. Check for presence of all required JSI functions * 2. Verify they are callable functions (not undefined or null) * 3. Return true only if ALL functions are available * * This runs once at module load time for zero runtime overhead */ const isNativeAvailable = (): boolean => { return ( typeof global !== 'undefined' && typeof global.__signalForgeCreateSignal === 'function' && typeof global.__signalForgeGetSignal === 'function' && typeof global.__signalForgeSetSignal === 'function' && typeof global.__signalForgeHasSignal === 'function' && typeof global.__signalForgeDeleteSignal === 'function' && typeof global.__signalForgeGetVersion === 'function' && typeof global.__signalForgeBatchUpdate === 'function' ); }; /** * Cache the detection result before attempting installation */ const NATIVE_PRESENT_AT_LOAD = isNativeAvailable(); // Attempt to install the native bindings on React Native before falling back try { if (!NATIVE_PRESENT_AT_LOAD) { // Lazy import to avoid bundling React Native in non-RN builds const { installJSIBindings } = require('./setup'); installJSIBindings(); } } catch (error) { // Non-RN environments (web/Node) will skip installation gracefully } const NATIVE_READY = isNativeAvailable(); // ============================================================================ // Fallback JavaScript Store // ============================================================================ /** * JavaScript fallback store instance * Used when native JSI bindings are not available * Would need to implement a compatible Store interface * * For production, this would be imported from the core store module * For now, we focus on the native implementation as primary target */ interface FallbackStore { createSignal<T>(value: T): { __id: string }; getSignal<T>(id: string): T; setSignal<T>(id: string, value: T): void; } let jsStore: FallbackStore | null = null; /** * Get or create the JavaScript fallback store * Lazy initialization to avoid unnecessary allocation when native is used */ const getJsStore = (): FallbackStore => { if (!jsStore) { // Placeholder implementation - in production, import from '../core/store' throw new Error('JavaScript fallback store not initialized. Native JSI bindings required.'); } return jsStore; }; // ============================================================================ // JSI Bridge API // ============================================================================ /** * Create a new signal with an initial value * * Native path: * - Calls C++ JSI function directly via global.__signalForgeCreateSignal * - Value is converted from JS to C++ SignalValue in native code * - Signal is stored in C++ memory (shared_ptr managed) * - Returns unique signal ID generated by C++ atomic counter * * Fallback path: * - Uses pure JavaScript Store implementation * - Signal stored in JavaScript heap * * @param initialValue - The initial value for the signal (any JSON-serializable type) * @returns SignalRef containing unique signal ID */ export const createSignal = <T = any>(initialValue: T): SignalRef => { if (NATIVE_READY) { // Direct JSI call - no overhead, direct C++ execution const id = global.__signalForgeCreateSignal!(initialValue); return { id }; } // Fallback to JavaScript implementation const store = getJsStore(); const signal = store.createSignal(initialValue); // Extract internal ID from JavaScript signal return { id: (signal as any).__id || String(Math.random()) }; }; /** * Get the current value of a signal * * Native path: * - Calls C++ JSI function with signal ID * - C++ looks up signal in thread-safe unordered_map * - Acquires mutex, reads value, releases mutex * - Converts C++ SignalValue to JS value via toJSI() * * Fallback path: * - Retrieves value from JavaScript Store * * @param signalRef - Reference to the signal * @returns Current value of the signal * @throws Error if signal doesn't exist */ export const getSignal = <T = any>(signalRef: SignalRef): T => { if (NATIVE_READY) { // Direct C++ memory access - returns value immediately return global.__signalForgeGetSignal!(signalRef.id) as T; } // Fallback: retrieve from JS Store const store = getJsStore(); // In production, you'd need to maintain a map from ID to signal reference throw new Error('JavaScript fallback for getSignal not fully implemented'); }; /** * Update a signal's value * * Native path: * - Calls C++ JSI function with signal ID and new value * - C++ converts JS value to SignalValue * - Looks up signal in store (mutex protected) * - Calls signal->setValue() which: * * Acquires signal's mutex * * Updates the value * * Atomically increments version counter (lock-free) * * Releases mutex * * Notifies subscribers * * This enables React Native components to detect changes efficiently * by comparing version numbers without locking * * Fallback path: * - Updates value in JavaScript Store * * @param signalRef - Reference to the signal * @param value - New value to set * @throws Error if signal doesn't exist */ export const setSignal = <T = any>(signalRef: SignalRef, value: T): void => { if (NATIVE_READY) { // Direct C++ call - updates C++ memory and triggers atomic version bump global.__signalForgeSetSignal!(signalRef.id, value); return; } // Fallback: update in JS Store const store = getJsStore(); throw new Error('JavaScript fallback for setSignal not fully implemented'); }; /** * Check if a signal exists in the store * * Native path: * - Quick hash table lookup in C++ unordered_map * - Returns boolean immediately * * @param signalRef - Reference to the signal * @returns true if signal exists, false otherwise */ export const hasSignal = (signalRef: SignalRef): boolean => { if (NATIVE_READY) { return global.__signalForgeHasSignal!(signalRef.id); } // Fallback return false; }; /** * Delete a signal from the store * * Native path: * - Removes signal from C++ unordered_map * - shared_ptr reference count decrements * - If no other references exist, Signal is automatically destroyed * - C++ destructor handles cleanup (mutex, version counter, subscribers) * * This provides automatic memory management with zero leaks * * @param signalRef - Reference to the signal to delete */ export const deleteSignal = (signalRef: SignalRef): void => { if (NATIVE_READY) { global.__signalForgeDeleteSignal!(signalRef.id); return; } // Fallback: remove from JS Store // (Would need implementation in Store class) }; /** * Get the current version number of a signal * * Version tracking enables efficient change detection in React: * - Each setValue() increments the version atomically * - React components can compare versions to detect changes * - No need to deep-compare values * - Lock-free read (atomic operation) for maximum performance * * Native path: * - Atomic load from C++ std::atomic<uint64_t> * - memory_order_acquire ensures visibility of all previous writes * - No mutex locking required * * This is how React Native components efficiently re-render: * 1. Store version in component state * 2. On each render, read current version * 3. If version changed, re-read value * 4. Update local state and version * * @param signalRef - Reference to the signal * @returns Current version number (increments on each update) */ export const getSignalVersion = (signalRef: SignalRef): number => { if (NATIVE_READY) { // Lock-free atomic read - fastest possible change detection return global.__signalForgeGetVersion!(signalRef.id); } // Fallback: would need version tracking in JS Store return 0; }; /** * Batch update multiple signals in one operation * * More efficient than individual updates when changing many signals: * - Reduces number of JSI boundary crossings * - C++ can optimize lock acquisition * - All updates happen atomically from JS perspective * * Native path: * - Single JSI call passes entire array to C++ * - C++ validates all signals exist before updating any * - Updates all signals in sequence * - Each signal's version still increments individually * - Subscribers notified after all updates complete * * @param updates - Array of [signalRef, value] tuples */ export const batchUpdate = (updates: [SignalRef, any][]): void => { if (NATIVE_READY) { // Convert SignalRef[] to string[] for C++ consumption const nativeUpdates: [string, any][] = updates.map(([ref, value]) => [ ref.id, value, ]); global.__signalForgeBatchUpdate!(nativeUpdates); return; } // Fallback: update each signal individually for (const [ref, value] of updates) { setSignal(ref, value); } }; /** * Check if the native JSI implementation is being used * * Useful for: * - Debugging and diagnostics * - Conditional logic based on native availability * - Performance monitoring and optimization decisions * * @returns true if using native C++ implementation, false if using JS fallback */ export const isUsingNative = (): boolean => { return NATIVE_READY; }; /** * Get information about the current implementation * * @returns Object containing implementation details */ export const getImplementationInfo = () => { return { native: NATIVE_READY, engine: NATIVE_READY ? (typeof HermesInternal !== 'undefined' ? 'Hermes' : 'JSC') : 'JavaScript', features: { directMemoryAccess: NATIVE_READY, atomicOperations: NATIVE_READY, threadSafe: NATIVE_READY, sharedPtrManagement: NATIVE_READY, }, }; }; // ============================================================================ // Exports // ============================================================================ export default { createSignal, getSignal, setSignal, hasSignal, deleteSignal, getSignalVersion, batchUpdate, isUsingNative, getImplementationInfo, }; /** * Usage Example: * * ```typescript * import jsiBridge from './native/jsiBridge'; * * // Check if native is available * console.log('Using native:', jsiBridge.isUsingNative()); * * // Create a signal (uses C++ if available) * const counterRef = jsiBridge.createSignal(0); * * // Update signal (atomic version increment in C++) * jsiBridge.setSignal(counterRef, 1); * * // Read value (direct C++ memory access) * const value = jsiBridge.getSignal(counterRef); * * // Check version for efficient change detection * const version = jsiBridge.getSignalVersion(counterRef); * * // Batch update multiple signals * jsiBridge.batchUpdate([ * [counterRef, 10], * [otherRef, 'new value'], * ]); * * // Clean up when done * jsiBridge.deleteSignal(counterRef); * ``` * * React Native Hook Example: * * ```typescript * function useNativeSignal<T>(signalRef: SignalRef): [T, (value: T) => void] { * const [version, setVersion] = useState(() => jsiBridge.getSignalVersion(signalRef)); * const [value, setValue] = useState<T>(() => jsiBridge.getSignal(signalRef)); * * useEffect(() => { * // Poll for changes (or use native subscription in production) * const interval = setInterval(() => { * const newVersion = jsiBridge.getSignalVersion(signalRef); * if (newVersion !== version) { * setVersion(newVersion); * setValue(jsiBridge.getSignal(signalRef)); * } * }, 16); // Check every frame * * return () => clearInterval(interval); * }, [signalRef, version]); * * const updateSignal = useCallback((newValue: T) => { * jsiBridge.setSignal(signalRef, newValue); * setVersion(jsiBridge.getSignalVersion(signalRef)); * setValue(newValue); * }, [signalRef]); * * return [value, updateSignal]; * } * ``` */