UNPKG

@klastra/ksync

Version:

Real-time sync engine for modern applications with 300k+ ops/sec performance

514 lines (500 loc) 15.7 kB
import React, { useEffect, useState, useCallback, useMemo, createContext, useContext } from 'react'; import { KSync } from '../core.js'; // TODO: Fix these imports after fixing the modules // import { DrizzleKSyncAdapter, QueryResult, WhereClause, OrderClause } from '../drizzle/index.js'; // import { KSyncMultistore } from '../multistore/index.js'; // Context for provider pattern const KSyncContext = createContext(null); // Hook for easy one-line sync setup export function useKSync(config) { // Create stable config reference const stableConfig = useMemo(() => ({ room: config?.room || 'default', serverUrl: config?.serverUrl || 'ws://localhost:8080/ws', userId: config?.userId || `user-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`, debug: config?.debug || false, sync: { enabled: true, mode: 'realtime', options: { autoReconnect: true, maxReconnectAttempts: 10, reconnectDelay: 1000, } }, storage: { type: 'memory', options: { persistEvents: false, // Disable for simplicity in dev maxEvents: 1000, } }, offline: { enabled: true, syncOnReconnect: true, }, state: { autoMaterialize: true, materializer: (events) => { // Simple state materialization - merge all events by type const state = {}; events.forEach(event => { if (!state[event.type]) { state[event.type] = []; } state[event.type].push(event.data); }); return state; } } }), [config?.room, config?.serverUrl, config?.userId, config?.debug]); // Create KSync instance with stable config const ksync = useMemo(() => { return new KSync(stableConfig); }, [stableConfig]); // State management const [isConnected, setIsConnected] = useState(false); const [state, setState] = useState({}); const [isOnline, setIsOnline] = useState(true); // Initialize and connect useEffect(() => { let mounted = true; const initializeKSync = async () => { try { await ksync.initialize(); // Auto-connect if serverUrl provided if (stableConfig.serverUrl) { await ksync.connect(); } if (mounted) { setIsConnected(ksync.getStatus().connected); setState(ksync.getState()); } } catch (error) { console.error('kSync initialization failed:', error); } }; // Connection event handlers const handleConnect = () => { if (mounted) { setIsConnected(true); setIsOnline(true); } }; const handleDisconnect = () => { if (mounted) { setIsConnected(false); } }; const handleEvent = () => { if (mounted) { setState(ksync.getState()); } }; const handleOnline = () => { if (mounted) { setIsOnline(true); } }; const handleOffline = () => { if (mounted) { setIsOnline(false); } }; // Setup event listeners ksync.on('connected', handleConnect); ksync.on('disconnected', handleDisconnect); ksync.on('event', handleEvent); ksync.on('online', handleOnline); ksync.on('offline', handleOffline); // Initialize initializeKSync(); // Cleanup return () => { mounted = false; ksync.off('connected', handleConnect); ksync.off('disconnected', handleDisconnect); ksync.off('event', handleEvent); ksync.off('online', handleOnline); ksync.off('offline', handleOffline); ksync.close(); }; }, [ksync, stableConfig.serverUrl]); // Send function const send = useCallback(async (type, data) => { try { await ksync.send(type, data); } catch (error) { console.error('Failed to send event:', error); throw error; } }, [ksync]); return { ksync, send, state, isConnected, isOnline, }; } // Hook for listening to specific events export function useKSyncEvent(ksync, eventType, dependencies = []) { const [events, setEvents] = useState([]); useEffect(() => { const listener = (event) => { setEvents(prev => [...prev, event.data]); }; ksync.on(eventType, listener); // Get existing events const existingEvents = ksync.getEvents() .filter(event => event.type === eventType) .map(event => event.data); setEvents(existingEvents); return () => { ksync.off(eventType, listener); }; }, [ksync, eventType, ...dependencies]); return events; } // Hook for sending events with loading state export function useKSyncSend(ksync) { const [isSending, setIsSending] = useState(false); const [error, setError] = useState(null); const send = useCallback(async (eventType, data) => { setIsSending(true); setError(null); try { await ksync.send(eventType, data); } catch (err) { setError(err); throw err; } finally { setIsSending(false); } }, [ksync]); return { send, isSending, error }; } // Hook for materialized state export function useKSyncState(ksync, materializerName, dependencies = []) { const [state, setState] = useState(); useEffect(() => { // Listen for any event that might change state const listener = () => { const newState = ksync.getState(); setState(newState); }; ksync.on('event', listener); // Get initial state setState(ksync.getState()); return () => { ksync.off('event', listener); }; }, [ksync, materializerName, ...dependencies]); return state; } // Hook for Drizzle queries - TODO: Re-enable when Drizzle module is fixed /* export function useKSyncQuery<T = any>( adapter: DrizzleKSyncAdapter, tableName: string, queryBuilder?: (table: any) => Promise<QueryResult<T>>, dependencies: any[] = [] ): { data: T[]; isLoading: boolean; error: Error | null; refetch: () => Promise<void>; } { const [data, setData] = useState<T[]>([]); const [isLoading, setIsLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const executeQuery = useCallback(async () => { setIsLoading(true); setError(null); try { const table = adapter.table(tableName); const result = queryBuilder ? await queryBuilder(table) : await table.findMany({}); setData(Array.isArray(result) ? result : result.data || []); } catch (err) { setError(err as Error); } finally { setIsLoading(false); } }, [adapter, tableName, queryBuilder, ...dependencies]); useEffect(() => { executeQuery(); }, [executeQuery]); const refetch = useCallback(async () => { await executeQuery(); }, [executeQuery]); return { data, isLoading, error, refetch }; } */ // Hook for real-time Drizzle queries with auto-refresh - TODO: Re-enable when Drizzle module is fixed /* export function useKSyncLiveQuery<T = any>( ksync: KSync, adapter: DrizzleKSyncAdapter, tableName: string, queryBuilder?: (table: any) => Promise<QueryResult<T>>, dependencies: any[] = [] ): { data: T[]; isLoading: boolean; error: Error | null; refetch: () => Promise<void>; } { const { data, isLoading, error, refetch } = useKSyncQuery( adapter, tableName, queryBuilder, dependencies ); // Auto-refresh on relevant events useEffect(() => { const listener = (event: KSyncEvent) => { // Check if event affects this table if (event.type.startsWith(`${tableName}:`) || event.type === tableName) { refetch(); } }; ksync.on('*', listener); return () => { ksync.off('*', listener); }; }, [ksync, tableName, refetch]); return { data, isLoading, error, refetch }; } */ // Multistore hook - TODO: Re-enable when Multistore module is fixed /* export function useMultistore(multistore: KSyncMultistore) { const [isInitialized, setIsInitialized] = useState(false); useEffect(() => { const initialize = async () => { if (!isInitialized) { await multistore.initialize(); setIsInitialized(true); } }; initialize(); return () => { multistore.close(); }; }, [multistore, isInitialized]); return { multistore, isInitialized }; } */ // Hook for presence awareness export function useKSyncPresence(ksync, initialData) { const [presence, setPresence] = useState({}); const [myPresence, setMyPresence] = useState(initialData || {}); useEffect(() => { const listener = (newPresence) => { setPresence(newPresence); }; ksync.onPresence(listener); return () => { // Remove presence listener (implementation would need this method) }; }, [ksync]); const updatePresence = useCallback((data) => { ksync.updatePresence(data); setMyPresence(prev => ({ ...prev, ...data })); }, [ksync]); return { presence, myPresence, updatePresence }; } // Hook for streaming (AI applications) export function useKSyncStream(ksync, streamId) { const [content, setContent] = useState(''); const [isComplete, setIsComplete] = useState(false); const [isStreaming, setIsStreaming] = useState(false); const startStream = useCallback(async (options) => { setContent(''); setIsComplete(false); setIsStreaming(true); const id = await ksync.startStream({ streamId, onChunk: (chunk) => { setContent(prev => prev + chunk); }, onComplete: (finalContent) => { setContent(finalContent); setIsComplete(true); setIsStreaming(false); }, ...options }); return id; }, [ksync, streamId]); const sendChunk = useCallback(async (id, chunk) => { await ksync.streamChunk(id, { data: chunk }); }, [ksync]); return { content, isComplete, isStreaming, startStream, sendChunk }; } // Hook for optimistic updates export function useKSyncOptimistic(ksync, eventType, optimisticUpdater) { const [data, setData] = useState([]); const [pendingUpdates, setPendingUpdates] = useState(new Map()); // Listen for confirmed events useEffect(() => { const listener = (event) => { if (event.type === eventType) { // Remove from pending if it was optimistic setPendingUpdates(prev => { const newMap = new Map(prev); newMap.delete(event.id); return newMap; }); // Update confirmed data setData(prev => { // Check if this was already added optimistically const existingIndex = prev.findIndex((item) => item.id === event.data.id); if (existingIndex >= 0) { // Update existing item const newData = [...prev]; newData[existingIndex] = event.data; return newData; } else { // Add new item return optimisticUpdater(prev, event.data); } }); } }; ksync.on(eventType, listener); return () => { ksync.off(eventType, listener); }; }, [ksync, eventType, optimisticUpdater]); const sendOptimistic = useCallback(async (data, tempId) => { const id = tempId || `temp-${Date.now()}-${Math.random()}`; // Add optimistic update setPendingUpdates(prev => new Map(prev).set(id, data)); setData(prev => optimisticUpdater(prev, { ...data, id, __optimistic: true })); try { await ksync.send(eventType, { ...data, id }); } catch (error) { // Revert optimistic update on error setPendingUpdates(prev => { const newMap = new Map(prev); newMap.delete(id); return newMap; }); setData(prev => prev.filter((item) => item.id !== id)); throw error; } }, [ksync, eventType, optimisticUpdater]); return { data: useMemo(() => { // Merge confirmed data with pending optimistic updates return data; }, [data]), pendingCount: pendingUpdates.size, sendOptimistic }; } // Hook for connection status export function useKSyncConnection(ksync) { const [isConnected, setIsConnected] = useState(false); const [isConnecting, setIsConnecting] = useState(false); const [error, setError] = useState(null); useEffect(() => { const handleConnect = () => { setIsConnected(true); setIsConnecting(false); setError(null); }; const handleDisconnect = () => { setIsConnected(false); setIsConnecting(false); }; const handleError = (error) => { setError(error); setIsConnecting(false); }; ksync.on('connected', handleConnect); ksync.on('disconnected', handleDisconnect); ksync.on('error', handleError); // Check initial connection status const status = ksync.getStatus(); setIsConnected(status.connected); return () => { ksync.off('connected', handleConnect); ksync.off('disconnected', handleDisconnect); ksync.off('error', handleError); }; }, [ksync]); const connect = useCallback(async () => { setIsConnecting(true); setError(null); try { await ksync.connect(); } catch (err) { setError(err); setIsConnecting(false); } }, [ksync]); const disconnect = useCallback(async () => { try { await ksync.disconnect(); } catch (err) { setError(err); } }, [ksync]); return { isConnected, isConnecting, error, connect, disconnect }; } export function KSyncProvider({ ksync, children }) { return React.createElement(KSyncContext.Provider, { value: ksync }, children); } export function useKSyncContext() { const ksync = useContext(KSyncContext); if (!ksync) { throw new Error('useKSyncContext must be used within a KSyncProvider'); } return ksync; } //# sourceMappingURL=index.js.map