UNPKG

resolvo-cms

Version:

Headless CMS for Resolvo websites with real-time content management

1,432 lines (1,422 loc) 115 kB
'use strict'; Object.defineProperty(exports, '__esModule', { value: true }); var axios = require('axios'); var socket_ioClient = require('socket.io-client'); var zod = require('zod'); var require$$0 = require('react'); class ContentCache { constructor(cleanupIntervalMs = 60000) { this.cleanupIntervalMs = cleanupIntervalMs; this.cache = new Map(); this.cleanupInterval = null; this.defaultTTL = 5 * 60 * 1000; // 5 minutes this.maxSize = 1000; // Prevent memory leaks this.startCleanupInterval(); } get(key) { const item = this.cache.get(key); if (!item) return null; if (this.isExpired(item)) { this.cache.delete(key); return null; } return item.data; } set(key, data, ttl = this.defaultTTL) { // Implement LRU-like behavior when cache is full if (this.cache.size >= this.maxSize && !this.cache.has(key)) { this.evictOldest(); } this.cache.set(key, { data, timestamp: Date.now(), ttl }); } delete(key) { return this.cache.delete(key); } clear() { this.cache.clear(); } has(key) { const item = this.cache.get(key); if (!item) return false; if (this.isExpired(item)) { this.cache.delete(key); return false; } return true; } size() { return this.cache.size; } isExpired(item) { return Date.now() - item.timestamp > item.ttl; } evictOldest() { let oldestKey = null; let oldestTime = Date.now(); for (const [key, item] of this.cache.entries()) { if (item.timestamp < oldestTime) { oldestTime = item.timestamp; oldestKey = key; } } if (oldestKey) { this.cache.delete(oldestKey); } } startCleanupInterval() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); } this.cleanupInterval = setInterval(() => { this.cleanup(); }, this.cleanupIntervalMs); } cleanup() { for (const [key, item] of this.cache.entries()) { if (this.isExpired(item)) { this.cache.delete(key); } } } destroy() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = null; } this.cache.clear(); } } class SchemaCache extends ContentCache { constructor() { super(...arguments); this.fieldCache = new Map(); } getField(schemaId, fieldName) { const schemaFields = this.fieldCache.get(schemaId); if (!schemaFields) return null; return schemaFields.get(fieldName) || null; } setField(schemaId, fieldName, data) { if (!this.fieldCache.has(schemaId)) { this.fieldCache.set(schemaId, new Map()); } this.fieldCache.get(schemaId).set(fieldName, data); } clearSchema(schemaId) { this.fieldCache.delete(schemaId); } } class LRUCache { constructor(capacity = 100) { this.cache = new Map(); this.capacity = capacity; } get(key) { const item = this.cache.get(key); if (!item) return null; // Update timestamp for LRU this.cache.delete(key); this.cache.set(key, { value: item.value, timestamp: Date.now() }); return item.value; } set(key, value) { if (this.cache.has(key)) { this.cache.delete(key); } else if (this.cache.size >= this.capacity) { // Remove oldest item const oldestKey = this.cache.keys().next().value; if (oldestKey) { this.cache.delete(oldestKey); } } this.cache.set(key, { value, timestamp: Date.now() }); } delete(key) { return this.cache.delete(key); } clear() { this.cache.clear(); } size() { return this.cache.size; } } class ValidationError extends Error { constructor(message, field, value, rule) { super(message); this.field = field; this.value = value; this.rule = rule; this.name = 'ValidationError'; } } function validateField(field, value) { const errors = []; if (!field.validation) { return { isValid: true, errors: [] }; } for (const rule of field.validation) { const error = validateRule(field, value, rule); if (error) { errors.push(error); } } return { isValid: errors.length === 0, errors }; } function validateRule(field, value, rule) { switch (rule.type) { case 'required': if (value === null || value === undefined || value === '') { return new ValidationError(rule.message || `${field.label} is required`, field.name, value, rule); } break; case 'min': if (field.type === 'number' && typeof value === 'number') { if (value < rule.value) { return new ValidationError(rule.message || `${field.label} must be at least ${rule.value}`, field.name, value, rule); } } else if (typeof value === 'string') { if (value.length < rule.value) { return new ValidationError(rule.message || `${field.label} must be at least ${rule.value} characters`, field.name, value, rule); } } break; case 'max': if (field.type === 'number' && typeof value === 'number') { if (value > rule.value) { return new ValidationError(rule.message || `${field.label} must be at most ${rule.value}`, field.name, value, rule); } } else if (typeof value === 'string') { if (value.length > rule.value) { return new ValidationError(rule.message || `${field.label} must be at most ${rule.value} characters`, field.name, value, rule); } } break; case 'pattern': if (typeof value === 'string' && rule.value) { const regex = new RegExp(rule.value); if (!regex.test(value)) { return new ValidationError(rule.message || `${field.label} format is invalid`, field.name, value, rule); } } break; case 'email': if (typeof value === 'string' && value) { const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; if (!emailRegex.test(value)) { return new ValidationError(rule.message || `${field.label} must be a valid email address`, field.name, value, rule); } } break; case 'url': if (typeof value === 'string' && value) { try { new URL(value); } catch { return new ValidationError(rule.message || `${field.label} must be a valid URL`, field.name, value, rule); } } break; } return null; } function validateContent(schema, data) { const errors = []; // Validate that all required fields are present for (const field of schema.fields) { const value = data[field.name]; // Check if field is required but missing const isRequired = field.validation?.some(rule => rule.type === 'required'); if (isRequired && (value === undefined || value === null)) { errors.push(new ValidationError(`${field.label || field.name} is required`, field.name, value, { type: 'required', message: `${field.label || field.name} is required` })); continue; } // Skip validation for undefined/null values unless required if (value === undefined || value === null) { continue; } const fieldValidation = validateField(field, value); if (!fieldValidation.isValid) { errors.push(...fieldValidation.errors); } } return { isValid: errors.length === 0, errors }; } function createZodSchema(fields) { const schemaObject = {}; for (const field of fields) { let fieldSchema; switch (field.type) { case 'text': case 'textarea': case 'url': fieldSchema = zod.z.string(); break; case 'number': fieldSchema = zod.z.number(); break; case 'boolean': fieldSchema = zod.z.boolean(); break; case 'date': fieldSchema = zod.z.string().datetime(); break; case 'datetime': fieldSchema = zod.z.string().datetime(); break; case 'color': fieldSchema = zod.z.string().regex(/^#[0-9A-F]{6}$/i); break; case 'array': fieldSchema = zod.z.array(zod.z.any()); break; case 'object': fieldSchema = zod.z.record(zod.z.any()); break; default: fieldSchema = zod.z.any(); } // Apply validation rules if (field.validation) { for (const rule of field.validation) { switch (rule.type) { case 'required': fieldSchema = fieldSchema; break; case 'min': if (field.type === 'number') { fieldSchema = fieldSchema.min(rule.value); } else { fieldSchema = fieldSchema.min(rule.value); } break; case 'max': if (field.type === 'number') { fieldSchema = fieldSchema.max(rule.value); } else { fieldSchema = fieldSchema.max(rule.value); } break; case 'email': fieldSchema = fieldSchema.email(); break; case 'url': fieldSchema = fieldSchema.url(); break; } } } else if (!field.required) { fieldSchema = fieldSchema.optional(); } schemaObject[field.name] = fieldSchema; } return zod.z.object(schemaObject); } function serializeContent(data, fields) { const serialized = {}; for (const field of fields) { const value = data[field.name]; serialized[field.name] = serializeField(value, field.type); } return serialized; } function deserializeContent(data, fields) { const deserialized = {}; for (const field of fields) { const value = data[field.name]; deserialized[field.name] = deserializeField(value, field.type); } return deserialized; } function serializeField(value, type) { if (value === null || value === undefined) { return value; } switch (type) { case 'date': case 'datetime': return value instanceof Date ? value.toISOString() : value; case 'number': return typeof value === 'string' ? parseFloat(value) : value; case 'boolean': if (typeof value === 'string') { return value.toLowerCase() === 'true' || value === '1'; } return Boolean(value); case 'array': return Array.isArray(value) ? value : [value]; case 'object': return typeof value === 'object' ? value : JSON.parse(value); case 'image': case 'file': return typeof value === 'string' ? value : value?.url || value; default: return value; } } function deserializeField(value, type) { if (value === null || value === undefined) { return value; } switch (type) { case 'date': case 'datetime': return new Date(value); case 'number': return typeof value === 'number' ? value : parseFloat(value); case 'boolean': return Boolean(value); case 'array': return Array.isArray(value) ? value : [value]; case 'object': return typeof value === 'object' ? value : JSON.parse(value); default: return value; } } function formatFieldValue(value, type) { if (value === null || value === undefined) { return ''; } switch (type) { case 'date': case 'datetime': return value instanceof Date ? value.toLocaleDateString() : value; case 'boolean': return value ? 'Yes' : 'No'; case 'array': return Array.isArray(value) ? value.join(', ') : String(value); case 'object': return typeof value === 'object' ? JSON.stringify(value) : String(value); case 'image': case 'file': return typeof value === 'string' ? value : value?.url || 'No file'; default: return String(value); } } function parseFieldValue(value, type) { if (!value) { return null; } switch (type) { case 'number': return parseFloat(value); case 'boolean': return value.toLowerCase() === 'true' || value === '1'; case 'array': return value.split(',').map(v => v.trim()); case 'object': try { return JSON.parse(value); } catch { return value; } default: return value; } } function generateFieldId() { return Math.random().toString(36).substr(2, 9); } function sanitizeFieldName(name) { return name .toLowerCase() .replace(/[^a-z0-9]/g, '_') .replace(/_+/g, '_') .replace(/^_|_$/g, ''); } const API_ENDPOINTS = { SCHEMAS: '/cms/schemas', CONTENT: '/cms/content', PUBLIC_CONTENT: '/cms/public/content', AUTH: '/cms/auth', VALIDATE_TOKEN: '/cms/validate-token', }; const DEFAULT_CONFIG = { apiUrl: 'http://localhost:3000/api', projectId: 1, timeout: 10000, retries: 3, reconnectAttempts: 5, reconnectDelay: 1000, }; const FIELD_TYPES = { TEXT: 'text', TEXTAREA: 'textarea', NUMBER: 'number', BOOLEAN: 'boolean', IMAGE: 'image', FILE: 'file', SELECT: 'select', ARRAY: 'array', OBJECT: 'object', RICH_TEXT: 'rich-text', DATE: 'date', DATETIME: 'datetime', COLOR: 'color', URL: 'url', }; const VALIDATION_TYPES = { REQUIRED: 'required', MIN: 'min', MAX: 'max', PATTERN: 'pattern', EMAIL: 'email', URL: 'url', CUSTOM: 'custom', }; const REALTIME_EVENTS = { CONTENT_UPDATED: 'content:updated', CONTENT_CREATED: 'content:created', CONTENT_DELETED: 'content:deleted', SCHEMA_UPDATED: 'schema:updated', }; const ERROR_CODES = { UNAUTHORIZED: 'UNAUTHORIZED', FORBIDDEN: 'FORBIDDEN', NOT_FOUND: 'NOT_FOUND', VALIDATION_ERROR: 'VALIDATION_ERROR', NETWORK_ERROR: 'NETWORK_ERROR', TIMEOUT_ERROR: 'TIMEOUT_ERROR', }; class ResolvoCMSClient { constructor(config) { this.socket = null; this.subscriptions = new Map(); this.reconnectAttempts = 0; this.reconnectTimeout = null; this.isConnecting = false; this.requestQueue = []; this.isProcessingQueue = false; // Event handling this.eventListeners = new Map(); this.config = { ...DEFAULT_CONFIG, ...config }; this.maxReconnectAttempts = this.config.reconnectAttempts ?? DEFAULT_CONFIG.reconnectAttempts; this.cache = new ContentCache(); this.httpClient = axios.create({ baseURL: this.config.apiUrl, timeout: this.config.timeout, headers: { 'Content-Type': 'application/json', }, }); this.setupInterceptors(); } setupInterceptors() { // Request interceptor this.httpClient.interceptors.request.use((config) => { // Add authentication headers if (this.config.websiteToken) { config.headers['X-Website-Token'] = this.config.websiteToken; } if (this.config.authToken) { config.headers['Authorization'] = `Bearer ${this.config.authToken}`; } return config; }, (error) => Promise.reject(error)); // Response interceptor this.httpClient.interceptors.response.use((response) => response, async (error) => { if (error.response?.status === 401) { // Handle authentication errors this.handleAuthError(); } return Promise.reject(error); }); } handleAuthError() { // Clear cache and disconnect socket this.cache.clear(); this.disconnect(); // Emit auth error event this.emit('auth:error', { message: 'Authentication failed' }); } async retryRequest(requestFn, maxRetries = 3, delay = 1000) { let lastError; for (let attempt = 0; attempt <= maxRetries; attempt++) { try { return await requestFn(); } catch (error) { lastError = error; if (attempt === maxRetries) { break; } // Don't retry on authentication errors if (error.response?.status === 401) { break; } // Exponential backoff const backoffDelay = delay * Math.pow(2, attempt); await new Promise(resolve => setTimeout(resolve, backoffDelay)); } } throw lastError; } async queueRequest(requestFn) { return new Promise((resolve, reject) => { this.requestQueue.push(async () => { try { const result = await requestFn(); resolve(result); } catch (error) { reject(error); } }); this.processQueue(); }); } async processQueue() { if (this.isProcessingQueue || this.requestQueue.length === 0) { return; } this.isProcessingQueue = true; while (this.requestQueue.length > 0) { const request = this.requestQueue.shift(); if (request) { await request(); } } this.isProcessingQueue = false; } // Schema Management async createSchema(schema) { return this.retryRequest(async () => { const response = await this.httpClient.post(API_ENDPOINTS.SCHEMAS, schema); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to create schema'); } // Clear schema cache this.cache.delete(`schema:${result.data.id}`); return result.data; }); } async getSchema(schemaId) { // Check cache first const cached = this.cache.get(`schema:${schemaId}`); if (cached) return cached; return this.retryRequest(async () => { const response = await this.httpClient.get(`${API_ENDPOINTS.SCHEMAS}/${schemaId}`); const result = response.data; if (!result.success) { throw new Error(result.message || 'Schema not found'); } // Cache the result this.cache.set(`schema:${schemaId}`, result.data); return result.data; }); } async updateSchema(schemaId, updates) { const response = await this.httpClient.put(`${API_ENDPOINTS.SCHEMAS}/${schemaId}`, updates); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to update schema'); } // Clear cache and emit realtime event this.cache.delete(`schema:${schemaId}`); this.emitRealtimeEvent(REALTIME_EVENTS.SCHEMA_UPDATED, result.data); return result.data; } async deleteSchema(schemaId) { const response = await this.httpClient.delete(`${API_ENDPOINTS.SCHEMAS}/${schemaId}`); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to delete schema'); } // Clear cache this.cache.delete(`schema:${schemaId}`); } async listSchemas(query) { const response = await this.httpClient.get(API_ENDPOINTS.SCHEMAS, { params: query }); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to fetch schemas'); } return result.data; } // Content Management async createContent(content) { // Get schema for validation const schema = await this.getSchema(content.schemaId); // Validate content const validation = validateContent(schema, content.data); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.map(e => e.message).join(', ')}`); } // Serialize content const serializedData = serializeContent(content.data, schema.fields); const response = await this.httpClient.post(API_ENDPOINTS.CONTENT, { ...content, data: serializedData }); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to create content'); } // Emit realtime event this.emitRealtimeEvent(REALTIME_EVENTS.CONTENT_CREATED, result.data); return result.data; } async getContent(contentId) { // Check cache first const cached = this.cache.get(`content:${contentId}`); if (cached) return cached; const response = await this.httpClient.get(`${API_ENDPOINTS.CONTENT}/${contentId}`); const result = response.data; if (!result.success) { throw new Error(result.message || 'Content not found'); } // Cache the result this.cache.set(`content:${contentId}`, result.data); return result.data; } async getContentBySchema(schemaId, options) { const query = { schemaId, projectId: this.config.projectId, ...options }; const response = await this.httpClient.get(API_ENDPOINTS.CONTENT, { params: query }); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to fetch content'); } return result.data; } async updateContent(contentId, updates) { // Get current content and schema for validation const currentContent = await this.getContent(contentId); const schema = await this.getSchema(currentContent.schemaId); // Merge updates with current data const updatedData = { ...currentContent.data, ...updates.data }; // Validate content const validation = validateContent(schema, updatedData); if (!validation.isValid) { throw new Error(`Validation failed: ${validation.errors.map(e => e.message).join(', ')}`); } // Serialize content const serializedData = serializeContent(updatedData, schema.fields); const response = await this.httpClient.put(`${API_ENDPOINTS.CONTENT}/${contentId}`, { ...updates, data: serializedData }); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to update content'); } // Clear cache and emit realtime event this.cache.delete(`content:${contentId}`); this.emitRealtimeEvent(REALTIME_EVENTS.CONTENT_UPDATED, result.data); return result.data; } async deleteContent(contentId) { const response = await this.httpClient.delete(`${API_ENDPOINTS.CONTENT}/${contentId}`); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to delete content'); } // Clear cache and emit realtime event this.cache.delete(`content:${contentId}`); this.emitRealtimeEvent(REALTIME_EVENTS.CONTENT_DELETED, { id: contentId }); } async publishContent(contentId) { const response = await this.httpClient.post(`${API_ENDPOINTS.CONTENT}/${contentId}/publish`); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to publish content'); } // Clear cache and emit realtime event this.cache.delete(`content:${contentId}`); this.emitRealtimeEvent(REALTIME_EVENTS.CONTENT_UPDATED, result.data); return result.data; } // Public API (for websites) async getPublicContent(schemaId) { const response = await this.httpClient.get(`${API_ENDPOINTS.PUBLIC_CONTENT}/${this.config.projectId}`, { params: { schemaId, isPublished: true }, headers: { 'X-Website-Token': this.config.websiteToken } }); const result = response.data; if (!result.success) { throw new Error(result.message || 'Failed to fetch public content'); } return result.data; } // Real-time Features connect(wsConfig) { if (this.socket?.connected || this.isConnecting) { return; } this.isConnecting = true; const wsUrl = wsConfig?.url || this.config.apiUrl.replace('http', 'ws'); this.socket = socket_ioClient.io(wsUrl, { auth: { token: this.config.authToken || this.config.websiteToken }, reconnectionAttempts: this.maxReconnectAttempts, reconnectionDelay: this.config.reconnectDelay, reconnectionDelayMax: 5000, timeout: 20000, forceNew: true }); this.socket.on('connect', () => { this.reconnectAttempts = 0; this.isConnecting = false; this.emit('connected'); // Resubscribe to all subscriptions this.resubscribeAll(); }); this.socket.on('disconnect', (reason) => { this.isConnecting = false; this.emit('disconnected', { reason }); if (reason === 'io server disconnect') { // Server disconnected us, don't reconnect return; } }); this.socket.on('connect_error', (error) => { this.isConnecting = false; this.reconnectAttempts++; this.emit('connection_error', error); }); // Handle real-time events Object.values(REALTIME_EVENTS).forEach(event => { this.socket.on(event, (data) => { this.handleRealtimeEvent(event, data); }); }); } disconnect() { if (this.socket) { this.socket.disconnect(); this.socket = null; } } subscribe(schemaId, callback) { if (!this.subscriptions.has(schemaId)) { this.subscriptions.set(schemaId, []); } this.subscriptions.get(schemaId).push(callback); // Subscribe to real-time updates if (this.socket?.connected) { this.socket.emit('subscribe:content', { schemaId }); } } unsubscribe(schemaId, callback) { if (!callback) { this.subscriptions.delete(schemaId); } else { const callbacks = this.subscriptions.get(schemaId); if (callbacks) { const index = callbacks.indexOf(callback); if (index > -1) { callbacks.splice(index, 1); } if (callbacks.length === 0) { this.subscriptions.delete(schemaId); } } } } handleRealtimeEvent(event, data) { const message = { type: event, data, timestamp: new Date() }; // Notify subscribers if (data.schemaId && this.subscriptions.has(data.schemaId)) { const callbacks = this.subscriptions.get(data.schemaId); callbacks.forEach(callback => callback(message)); } // Emit general event this.emit(event, message); } emitRealtimeEvent(event, data) { if (this.socket?.connected) { this.socket.emit(event, data); } } resubscribeAll() { // Resubscribe to all active subscriptions for (const [schemaId] of this.subscriptions) { if (this.socket?.connected) { this.socket.emit('subscribe:content', { schemaId }); } } } on(event, callback) { if (!this.eventListeners.has(event)) { this.eventListeners.set(event, []); } this.eventListeners.get(event).push(callback); } off(event, callback) { const listeners = this.eventListeners.get(event); if (listeners) { const index = listeners.indexOf(callback); if (index > -1) { listeners.splice(index, 1); } } } emit(event, data) { const listeners = this.eventListeners.get(event); if (listeners) { listeners.forEach(callback => callback(data)); } } // Utility methods clearCache() { this.cache.clear(); } getCacheStats() { return { size: this.cache.size() }; } isConnected() { return this.socket?.connected || false; } } function useContent(client, options = {}) { const { schemaId, contentId, isPublished = true, autoConnect = true, realtime = true } = options; const [content, setContent] = require$$0.useState(null); const [contentList, setContentList] = require$$0.useState([]); const [loading, setLoading] = require$$0.useState(true); const [error, setError] = require$$0.useState(null); const [isInitialized, setIsInitialized] = require$$0.useState(false); require$$0.useRef(client); require$$0.useRef(options); const abortControllerRef = require$$0.useRef(null); // Initialize and fetch content require$$0.useEffect(() => { if (!client || isInitialized) return; // Cancel previous request if still pending if (abortControllerRef.current) { abortControllerRef.current.abort(); } abortControllerRef.current = new AbortController(); const initialize = async () => { try { setLoading(true); setError(null); if (autoConnect && realtime) { client.connect(); } if (contentId) { const contentData = await client.getContent(contentId); if (!abortControllerRef.current?.signal.aborted) { setContent(contentData); } } else if (schemaId) { const contentData = await client.getContentBySchema(schemaId, { isPublished }); if (!abortControllerRef.current?.signal.aborted) { setContentList(contentData); } } if (!abortControllerRef.current?.signal.aborted) { setIsInitialized(true); } } catch (err) { if (!abortControllerRef.current?.signal.aborted) { setError(err instanceof Error ? err : new Error('Failed to fetch content')); } } finally { if (!abortControllerRef.current?.signal.aborted) { setLoading(false); } } }; initialize(); // Cleanup function return () => { if (abortControllerRef.current) { abortControllerRef.current.abort(); } }; }, [client, contentId, schemaId, isPublished, autoConnect, realtime, isInitialized]); // Real-time updates require$$0.useEffect(() => { if (!client || !realtime || !schemaId) return; const handleContentUpdate = (message) => { if (message.type === 'content:updated' || message.type === 'content:created') { const updatedContent = message.data; if (contentId && updatedContent.id === contentId) { setContent(updatedContent); } if (schemaId && updatedContent.schemaId === schemaId) { setContentList(prev => { const index = prev.findIndex(c => c.id === updatedContent.id); if (index >= 0) { const newList = [...prev]; newList[index] = updatedContent; return newList; } else { return [...prev, updatedContent]; } }); } } else if (message.type === 'content:deleted') { const deletedId = message.data.id; if (contentId && deletedId === contentId) { setContent(null); } setContentList(prev => prev.filter(c => c.id !== deletedId)); } }; client.subscribe(schemaId, handleContentUpdate); return () => { client.unsubscribe(schemaId, handleContentUpdate); }; }, [client, schemaId, contentId, realtime]); // Content operations const createContent = require$$0.useCallback(async (data) => { try { setError(null); const newContent = await client.createContent(data); if (schemaId && newContent.schemaId === schemaId) { setContentList(prev => [...prev, newContent]); } return newContent; } catch (err) { const error = err instanceof Error ? err : new Error('Failed to create content'); setError(error); throw error; } }, [client, schemaId]); const updateContent = require$$0.useCallback(async (contentId, updates) => { try { setError(null); const updatedContent = await client.updateContent(contentId, updates); if (content && content.id === contentId) { setContent(updatedContent); } setContentList(prev => prev.map(c => c.id === contentId ? updatedContent : c)); return updatedContent; } catch (err) { const error = err instanceof Error ? err : new Error('Failed to update content'); setError(error); throw error; } }, [client, content]); const deleteContent = require$$0.useCallback(async (contentId) => { try { setError(null); await client.deleteContent(contentId); if (content && content.id === contentId) { setContent(null); } setContentList(prev => prev.filter(c => c.id !== contentId)); } catch (err) { const error = err instanceof Error ? err : new Error('Failed to delete content'); setError(error); throw error; } }, [client, content]); const publishContent = require$$0.useCallback(async (contentId) => { try { setError(null); const publishedContent = await client.publishContent(contentId); if (content && content.id === contentId) { setContent(publishedContent); } setContentList(prev => prev.map(c => c.id === contentId ? publishedContent : c)); return publishedContent; } catch (err) { const error = err instanceof Error ? err : new Error('Failed to publish content'); setError(error); throw error; } }, [client, content]); const refresh = require$$0.useCallback(async () => { try { setLoading(true); setError(null); if (contentId) { const contentData = await client.getContent(contentId); setContent(contentData); } else if (schemaId) { const contentData = await client.getContentBySchema(schemaId, { isPublished }); setContentList(contentData); } } catch (err) { setError(err instanceof Error ? err : new Error('Failed to refresh content')); } finally { setLoading(false); } }, [client, contentId, schemaId, isPublished]); return { content, contentList, loading, error, createContent, updateContent, deleteContent, publishContent, refresh }; } // Optimistic updates hook function useOptimisticContent(client, options = {}) { const [optimisticData, setOptimisticData] = require$$0.useState(null); const { content, updateContent, ...rest } = useContent(client, options); const optimisticUpdate = require$$0.useCallback(async (contentId, updates) => { if (!content) { throw new Error('No content to update'); } // Set optimistic data immediately const optimisticContent = { ...content, data: { ...content.data, ...updates.data }}; setOptimisticData(optimisticContent.data); try { // Perform actual update const result = await updateContent(contentId, updates); setOptimisticData(null); return result; } catch (error) { // Revert on error setOptimisticData(null); throw error; } }, [content, updateContent]); return { ...rest, content: optimisticData ? { ...content, data: optimisticData } : content, updateContent: optimisticUpdate }; } function useSchema(client, options = {}) { const { schemaId, projectId, isActive = true, autoConnect = true, realtime = true } = options; const [schema, setSchema] = require$$0.useState(null); const [schemas, setSchemas] = require$$0.useState([]); const [loading, setLoading] = require$$0.useState(true); const [error, setError] = require$$0.useState(null); const [isInitialized, setIsInitialized] = require$$0.useState(false); // Initialize and fetch schemas require$$0.useEffect(() => { if (!client || isInitialized) return; const initialize = async () => { try { setLoading(true); setError(null); if (autoConnect && realtime) { client.connect(); } if (schemaId) { const schemaData = await client.getSchema(schemaId); setSchema(schemaData); } else { const schemasData = await client.listSchemas({ projectId: projectId || client['config'].projectId, isActive }); setSchemas(schemasData); } setIsInitialized(true); } catch (err) { setError(err instanceof Error ? err : new Error('Failed to fetch schemas')); } finally { setLoading(false); } }; initialize(); }, [client, schemaId, projectId, isActive, autoConnect, realtime, isInitialized]); // Real-time updates require$$0.useEffect(() => { if (!client || !realtime) return; const handleSchemaUpdate = (message) => { if (message.type === 'schema:updated') { const updatedSchema = message.data; if (schemaId && updatedSchema.id === schemaId) { setSchema(updatedSchema); } setSchemas(prev => { const index = prev.findIndex(s => s.id === updatedSchema.id); if (index >= 0) { const newList = [...prev]; newList[index] = updatedSchema; return newList; } return prev; }); } }; client.on('schema:updated', handleSchemaUpdate); return () => { client.off('schema:updated', handleSchemaUpdate); }; }, [client, schemaId, realtime]); // Schema operations const createSchema = require$$0.useCallback(async (data) => { try { setError(null); const newSchema = await client.createSchema(data); setSchemas(prev => [...prev, newSchema]); return newSchema; } catch (err) { const error = err instanceof Error ? err : new Error('Failed to create schema'); setError(error); throw error; } }, [client]); const updateSchema = require$$0.useCallback(async (schemaId, updates) => { try { setError(null); const updatedSchema = await client.updateSchema(schemaId, updates); if (schema && schema.id === schemaId) { setSchema(updatedSchema); } setSchemas(prev => prev.map(s => s.id === schemaId ? updatedSchema : s)); return updatedSchema; } catch (err) { const error = err instanceof Error ? err : new Error('Failed to update schema'); setError(error); throw error; } }, [client, schema]); const deleteSchema = require$$0.useCallback(async (schemaId) => { try { setError(null); await client.deleteSchema(schemaId); if (schema && schema.id === schemaId) { setSchema(null); } setSchemas(prev => prev.filter(s => s.id !== schemaId)); } catch (err) { const error = err instanceof Error ? err : new Error('Failed to delete schema'); setError(error); throw error; } }, [client, schema]); const refresh = require$$0.useCallback(async () => { try { setLoading(true); setError(null); if (schemaId) { const schemaData = await client.getSchema(schemaId); setSchema(schemaData); } else { const schemasData = await client.listSchemas({ projectId: projectId || client['config'].projectId, isActive }); setSchemas(schemasData); } } catch (err) { setError(err instanceof Error ? err : new Error('Failed to refresh schemas')); } finally { setLoading(false); } }, [client, schemaId, projectId, isActive]); return { schema, schemas, loading, error, createSchema, updateSchema, deleteSchema, refresh }; } function useRealtime(client, options = {}) { const { autoConnect = true, reconnectAttempts = 5, reconnectDelay = 1000 } = options; const [isConnected, setIsConnected] = require$$0.useState(false); const [isConnecting, setIsConnecting] = require$$0.useState(false); const [error, setError] = require$$0.useState(null); const [reconnectCount, setReconnectCount] = require$$0.useState(0); const reconnectTimeoutRef = require$$0.useRef(null); // Auto-connect on mount require$$0.useEffect(() => { if (autoConnect && client) { connect(); } return () => { if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } }; }, [autoConnect, client]); // Connection event handlers require$$0.useEffect(() => { if (!client) return; const handleConnect = () => { setIsConnected(true); setIsConnecting(false); setError(null); setReconnectCount(0); }; const handleDisconnect = () => { setIsConnected(false); setIsConnecting(false); }; const handleConnectError = (err) => { setIsConnecting(false); setError(err); // Auto-reconnect logic if (reconnectCount < reconnectAttempts) { setReconnectCount(prev => prev + 1); reconnectTimeoutRef.current = setTimeout(() => { connect(); }, reconnectDelay); } }; const handleReconnect = (attempt) => { setIsConnecting(true); setError(null); }; const handleReconnectAttempt = (attempt) => { setIsConnecting(true); }; const handleReconnectError = () => { setIsConnecting(false); setError(new Error('Failed to reconnect')); }; // Set up event listeners client.on('connected', handleConnect); client.on('disconnected', handleDisconnect); client.on('connection_error', handleConnectError); client.on('reconnect', handleReconnect); client.on('reconnect_attempt', handleReconnectAttempt); client.on('reconnect_error', handleReconnectError); return () => { client.off('connected', handleConnect); client.off('disconnected', handleDisconnect); client.off('connection_error', handleConnectError); client.off('reconnect', handleReconnect); client.off('reconnect_attempt', handleReconnectAttempt); client.off('reconnect_error', handleReconnectError); }; }, [client, reconnectCount, reconnectAttempts, reconnectDelay]); const connect = require$$0.useCallback(() => { if (!client || isConnected || isConnecting) return; setIsConnecting(true); setError(null); client.connect(); }, [client, isConnected, isConnecting]); const disconnect = require$$0.useCallback(() => { if (!client) return; if (reconnectTimeoutRef.current) { clearTimeout(reconnectTimeoutRef.current); } setIsConnecting(false); setReconnectCount(0); client.disconnect(); }, [client]); const subscribe = require$$0.useCallback((schemaId, callback) => { if (!client) return; client.subscribe(schemaId, callback); }, [client]); const unsubscribe = require$$0.useCallback((schemaId, callback) => { if (!client) return; client.unsubscribe(schemaId, callback); }, [client]); const sendMessage = require$$0.useCallback((event, data) => { if (!client || !isConnected) return; // This would need to be implemented in the client if needed // For now, we'll just emit events through the client client.on(event, data); }, [client, isConnected]); return { isConnected, isConnecting, error, connect, disconnect, subscribe, unsubscribe, sendMessage }; } // Hook for subscribing to specific content updates function useContentSubscription(client, schemaId, options = {}) { const [messages, setMessages] = require$$0.useState([]); const [lastMessage, setLastMessage] = require$$0.useState(null); const { isConnected, subscribe, unsubscribe } = useRealtime(client); require$$0.useEffect(() => { if (!isConnected || !schemaId) return; const handleMessage = (message) => { setMessages(prev => [...prev, message]); setLastMessage(message); }; subscribe(schemaId, handleMessage); return () => { unsubscribe(schemaId, handleMessage); }; }, [isConnected, schemaId, subscribe, unsubscribe]); const clearMessages = require$$0.useCallback(() => { setMessages([]); setLastMessage(null); }, []); return { messages, lastMessage, isConnected, clearMessages }; } // Hook for optimistic updates with real-time sync function useOptimisticRealtime(client, schemaId, initialData) { const [optimisticData, setOptimisticData] = require$$0.useState(initialData