resolvo-cms
Version:
Headless CMS for Resolvo websites with real-time content management
1,432 lines (1,422 loc) • 115 kB
JavaScript
'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