@klastra/ksync
Version:
Real-time sync engine for modern applications with 300k+ ops/sec performance
1,009 lines • 37.9 kB
JavaScript
import { EventEmitter } from 'events';
import { KSyncError } from './types.js';
import { MemoryStorage } from './storage/memory.js';
import { IndexedDBStorage } from './storage/indexeddb.js';
import { generateId } from './utils.js';
const log = (debug, category, ...args) => {
if (debug === true || (typeof debug === 'object' && debug[category])) {
console.log(`[kSync:${category}]`, ...args);
}
};
export class KSync extends EventEmitter {
config;
storage;
syncClient;
events = [];
materializedState = null;
version = 0;
isConnected = false;
isOnline = typeof navigator !== 'undefined' && navigator.onLine !== undefined ? navigator.onLine : true;
messageQueue = [];
presenceMap = new Map();
streamStates = new Map();
// Performance optimization state
eventBatchTimeout;
pendingEvents = [];
materializationCache = new Map();
lastMaterializationHash = '';
performanceMetrics = {
eventsProcessed: 0,
averageProcessingTime: 0,
materializationCount: 0,
averageMaterializationTime: 0
};
// FIXED: Track timers/intervals for proper cleanup
performanceInterval;
onlineListener;
offlineListener;
constructor(config = {}) {
super();
this.config = this.resolveConfig(config);
this.initializeStorage();
// Note: initializeSync is now called lazily when needed to avoid require() in constructor
this.setupOfflineHandling();
this.setupPerformanceMonitoring();
log(this.config.debug, 'events', 'KSync initialized with config:', this.config);
}
// === PUBLIC API ===
/**
* Send an event to the system
* @param type Event type - use namespaced names like 'chat.message' for organization
* @param data Event data - will be validated if schema is defined
* @param options Additional options for this specific event
*/
async send(type, data, options) {
const startTime = performance.now();
const event = {
id: generateId(),
type,
data,
timestamp: Date.now(),
version: ++this.version,
userId: this.config.userId,
...(options?.metadata && { metadata: options.metadata }),
...(options?.ttl && { ttl: options.ttl }),
...(options?.priority && { priority: options.priority })
};
log(this.config.debug, 'events', 'Sending event:', { type, eventId: event.id });
// Add to local events immediately (optimistic update)
await this.addEvent(event);
// Handle sync based on configuration
if (this.config.sync.enabled) {
await this.ensureSyncInitialized();
log(this.config.debug, 'sync', `Sync check: syncClient=${!!this.syncClient}, connected=${this.isConnected}, online=${this.isOnline}`);
if (this.syncClient && this.isConnected && this.isOnline) {
try {
await this.sendToServer(event);
log(this.config.debug, 'sync', 'Event synced successfully:', event.id);
if (options?.requireSync) {
// Event has been successfully synced
return;
}
}
catch (error) {
log(this.config.debug, 'sync', 'Sync failed, queuing event:', error);
this.queueEvent(event);
}
}
else {
// Queue for later sync
this.queueEvent(event);
log(this.config.debug, 'offline', 'Event queued for offline sync:', event.id);
}
}
// Always track performance metrics regardless of sync status
this.updatePerformanceMetrics('eventProcessing', performance.now() - startTime);
}
/**
* Listen to events of a specific type
* @param type Event type to listen for
* @param listener Callback function that receives the event data
* @returns Unsubscribe function
*/
on(type, listener) {
super.on(type, listener);
return this;
}
/**
* Listen to an event once
* @param type Event type to listen for
* @param listener Callback function
* @returns This instance for chaining
*/
once(type, listener) {
super.once(type, listener);
return this;
}
/**
* Remove event listener
* @param type Event type
* @param listener Listener function to remove
* @returns This instance for chaining
*/
off(type, listener) {
super.off(type, listener);
return this;
}
/**
* Get current materialized state
* @param forceMaterialize Force re-materialization even if cached
* @returns Current state or null if no materializer is configured
*/
getState(forceMaterialize = false) {
if (!this.config.state.materializer) {
log(this.config.debug, 'events', 'No materializer configured, returning raw events');
return { events: this.events };
}
if (forceMaterialize || this.needsRematerialization()) {
this.materialize();
}
return this.materializedState;
}
/**
* Get all events (backward compatibility)
*/
getEvents() {
return [...this.events];
}
/**
* Initialize the instance (backward compatibility)
*/
async initialize(config, syncClient) {
if (syncClient) {
this.syncClient = syncClient;
}
if (this.config.sync.enabled && this.config.serverUrl) {
await this.connect();
}
}
/**
* Define a schema (backward compatibility - no-op for now)
*/
defineSchema(name, schema) {
log(this.config.debug, 'events', `Schema defined: ${name}`);
// No-op for backward compatibility
}
/**
* Define a materializer (backward compatibility)
*/
defineMaterializer(name, materializer) {
if (!this.config.state.materializer) {
// If no materializer set, use this one
this.config.state.materializer = materializer;
}
log(this.config.debug, 'events', `Materializer defined: ${name}`);
}
/**
* Close/cleanup (backward compatibility)
*/
async close() {
await this.disconnect();
}
/**
* Update presence (backward compatibility)
*/
async updatePresence(data) {
await this.setPresence(data);
}
/**
* Listen to presence updates (backward compatibility)
*/
onPresence(listener) {
this.on('presence-update', listener);
}
/**
* Get system status and metrics
* @returns Comprehensive status information
*/
getStatus() {
return {
// Connection status
connected: this.isConnected,
online: this.isOnline,
serverUrl: this.config.serverUrl,
// Data status
events: this.events.length,
queued: this.messageQueue.length,
version: this.version,
// User context
room: this.config.room,
userId: this.config.userId,
// Performance metrics
performance: { ...this.performanceMetrics },
// Feature status
features: {
syncEnabled: this.config.sync.enabled,
offlineEnabled: this.config.offline.enabled,
presenceEnabled: this.config.features.presence,
streamingEnabled: this.config.features.streaming
}
};
}
/**
* Manually trigger sync (useful for manual sync mode or troubleshooting)
* @param options Sync options
*/
async sync(options) {
if (!this.config.sync.enabled) {
throw new KSyncError('Sync is disabled', 'SYNC_DISABLED');
}
if (!this.isOnline) {
throw new KSyncError('Cannot sync while offline', 'OFFLINE');
}
if (!this.isConnected || options?.force) {
await this.connect();
}
const startTime = performance.now();
const result = await this.sendQueuedEvents();
const duration = performance.now() - startTime;
log(this.config.debug, 'sync', `Sync completed in ${duration.toFixed(2)}ms:`, result);
return result;
}
/**
* Join a room (for multi-room applications)
* @param room Room name to join
* @param options Room join options
*/
async joinRoom(room, options) {
const previousRoom = this.config.room;
this.config.room = room;
if (this.syncClient && this.isConnected) {
if (options?.leaveCurrentRoom && previousRoom !== room) {
await this.syncClient.send({
type: 'leave',
room: previousRoom,
data: { userId: this.config.userId }
});
}
await this.syncClient.send({
type: 'join',
room: room,
data: {
userId: this.config.userId,
syncHistory: options?.syncHistory !== false
}
});
}
log(this.config.debug, 'sync', `Joined room: ${room}`);
this.emit('room-changed', { from: previousRoom, to: room });
}
/**
* Set user presence information
* @param info Presence information to set
*/
async setPresence(info) {
if (!this.config.features.presence) {
throw new KSyncError('Presence feature is disabled', 'FEATURE_DISABLED');
}
const presence = {
userId: this.config.userId,
status: 'online',
lastSeen: Date.now(),
...info
};
this.presenceMap.set(this.config.userId, presence);
await this.send('presence-update', presence);
log(this.config.debug, 'events', 'Presence updated:', presence);
}
/**
* Get presence information for all users
* @param filter Optional filter function
* @returns Array of presence information
*/
getPresence(filter) {
const allPresence = Array.from(this.presenceMap.values());
return filter ? allPresence.filter(filter) : allPresence;
}
/**
* Start a data stream (useful for AI responses, live updates, etc.)
* @param streamId Unique stream identifier
* @param options Stream configuration
*/
async startStream(streamId, options) {
if (!this.config.features.streaming) {
throw new KSyncError('Streaming feature is disabled', 'FEATURE_DISABLED');
}
this.streamStates.set(streamId, {
active: true,
startTime: Date.now(),
...options
});
await this.send('stream-start', { streamId, ...options });
this.emit('stream-start', { streamId, ...options });
// Auto-end stream if configured
if (options?.autoEnd) {
setTimeout(() => {
if (this.streamStates.has(streamId)) {
this.endStream(streamId);
}
}, options.autoEnd);
}
log(this.config.debug, 'events', 'Stream started:', streamId);
}
/**
* Send data chunk to a stream
* @param streamId Stream identifier
* @param chunk Data chunk to send
*/
async streamChunk(streamId, chunk) {
if (!this.streamStates.has(streamId)) {
throw new KSyncError('Stream not found', 'STREAM_NOT_FOUND');
}
const streamData = { streamId, data: chunk.data, metadata: chunk.metadata, complete: chunk.complete };
await this.send('stream-chunk', streamData);
this.emit('stream-chunk', streamData);
}
/**
* End a stream
* @param streamId Stream identifier
* @param data Optional final data
*/
async endStream(streamId, data) {
this.streamStates.delete(streamId);
const streamData = { streamId, data, endTime: Date.now() };
await this.send('stream-end', streamData);
this.emit('stream-end', streamData);
log(this.config.debug, 'events', 'Stream ended:', streamId);
}
/**
* Get all active streams
* @returns Array of active stream IDs and their metadata
*/
getActiveStreams() {
return Array.from(this.streamStates.entries()).map(([streamId, metadata]) => ({
streamId,
metadata
}));
}
/**
* Connect to the sync server (usually automatic)
* @param options Connection options
*/
async connect(options) {
if (!this.config.sync.enabled || !this.config.serverUrl) {
throw new KSyncError('Sync is not configured', 'SYNC_NOT_CONFIGURED');
}
if (this.isConnected && !options?.force) {
return;
}
await this.ensureSyncInitialized();
if (!this.syncClient) {
throw new KSyncError('Failed to initialize sync client', 'SYNC_INIT_FAILED');
}
const startTime = performance.now();
await this.syncClient.connect();
const duration = performance.now() - startTime;
log(this.config.debug, 'sync', `Connected in ${duration.toFixed(2)}ms`);
// Auto-join room
if (this.config.room) {
await this.joinRoom(this.config.room);
}
// Auto-authenticate
if (this.config.auth.token || this.config.auth.provider) {
await this.authenticate();
}
// Send queued events
await this.sendQueuedEvents();
}
/**
* Disconnect from the sync server
*/
async disconnect() {
// FIXED: Proper cleanup to prevent tests hanging
// Clean up sync client
if (this.syncClient) {
await this.syncClient.disconnect();
}
this.isConnected = false;
// Clean up timers and intervals
if (this.eventBatchTimeout) {
clearTimeout(this.eventBatchTimeout);
this.eventBatchTimeout = undefined;
}
if (this.performanceInterval) {
clearInterval(this.performanceInterval);
this.performanceInterval = undefined;
}
// Clean up event listeners
if (typeof window !== 'undefined') {
if (this.onlineListener) {
window.removeEventListener('online', this.onlineListener);
this.onlineListener = undefined;
}
if (this.offlineListener) {
window.removeEventListener('offline', this.offlineListener);
this.offlineListener = undefined;
}
}
// Clean up all event listeners from EventEmitter
this.removeAllListeners();
log(this.config.debug, 'sync', 'Disconnected from server and cleaned up resources');
this.emit('disconnected');
}
/**
* Clear all events and reset state
* @param options Clear options
*/
async clear(options) {
const opts = {
events: true,
state: true,
queue: true,
storage: false,
...options
};
if (opts.events) {
this.events = [];
this.version = 0;
}
if (opts.state) {
this.materializedState = null;
this.materializationCache.clear();
}
if (opts.queue) {
this.messageQueue = [];
}
if (opts.storage) {
await this.storage.clear();
}
log(this.config.debug, 'events', 'Cleared data:', opts);
this.emit('cleared', opts);
}
// === PRIVATE METHODS ===
resolveConfig(config) {
const userId = config.userId || config.clientId || this.generateUserId();
const serverUrl = config.serverUrl || '';
const hasServer = Boolean(serverUrl);
return {
serverUrl,
room: config.room || 'default',
userId,
clientId: userId, // Backward compatibility
auth: {
token: config.auth?.token,
provider: config.auth?.provider,
type: config.auth?.type || 'bearer'
},
storage: {
type: config.storage?.type || 'memory',
instance: config.storage?.instance,
options: {
persistEvents: config.storage?.options?.persistEvents ?? true,
maxEvents: config.storage?.options?.maxEvents || 10000,
compression: config.storage?.options?.compression || false
}
},
sync: {
// Enable sync by default if serverUrl is provided
enabled: config.sync?.enabled ?? hasServer,
client: config.sync?.client,
// Default to realtime mode for better UX
mode: config.sync?.mode || 'realtime',
options: {
autoReconnect: config.sync?.options?.autoReconnect ?? true,
maxReconnectAttempts: config.sync?.options?.maxReconnectAttempts || 10,
reconnectDelay: config.sync?.options?.reconnectDelay || 1000,
heartbeatInterval: config.sync?.options?.heartbeatInterval || 30000
}
},
performance: {
batchSize: config.performance?.batchSize || 50, // Smaller batches for better responsiveness
batchDelay: config.performance?.batchDelay || 5, // Faster batching
materializationCaching: config.performance?.materializationCaching ?? true,
compressionThreshold: config.performance?.compressionThreshold || 1024
},
offline: {
enabled: config.offline?.enabled ?? true,
queueSize: config.offline?.queueSize || 1000,
persistence: config.offline?.persistence ?? true,
syncOnReconnect: config.offline?.syncOnReconnect ?? true
},
state: {
materializer: config.state?.materializer,
enableCaching: config.state?.enableCaching ?? true,
// Enable auto-materialization by default for better UX
autoMaterialize: config.state?.autoMaterialize ?? true
},
debug: typeof config.debug === 'boolean'
? {
events: config.debug,
sync: config.debug,
performance: config.debug,
storage: config.debug
}
: {
events: config.debug?.events || false,
sync: config.debug?.sync || false,
performance: config.debug?.performance || false,
storage: config.debug?.storage || false
},
features: {
presence: config.features?.presence ?? true,
streaming: config.features?.streaming ?? true,
encryption: config.features?.encryption || false
}
};
}
initializeStorage() {
if (this.config.storage.instance) {
this.storage = this.config.storage.instance;
return;
}
switch (this.config.storage.type) {
case 'memory':
this.storage = new MemoryStorage();
break;
case 'indexeddb':
this.storage = new IndexedDBStorage();
break;
case 'file':
// TODO: Implement file storage
this.storage = new MemoryStorage();
log(this.config.debug, 'storage', 'File storage not implemented, using memory');
break;
default:
this.storage = new MemoryStorage();
}
log(this.config.debug, 'storage', `Initialized ${this.config.storage.type} storage`);
}
async ensureSyncInitialized() {
if (!this.config.sync.enabled || this.syncClient)
return;
// Initialize WebSocket client if serverUrl is provided and no custom client
if (this.config.serverUrl && !this.config.sync.client) {
const { WebSocketSyncClient } = await import('./sync/websocket-client.js');
this.syncClient = new WebSocketSyncClient(this.config.serverUrl, this.config.sync.options.maxReconnectAttempts, this.config.sync.options.reconnectDelay, this.config.debug.sync);
this.setupSyncHandlers();
}
else if (this.config.sync.client) {
this.syncClient = this.config.sync.client;
this.setupSyncHandlers();
}
log(this.config.debug, 'sync', 'Sync client initialized');
}
setupOfflineHandling() {
if (!this.config.offline.enabled || typeof window === 'undefined')
return;
// FIXED: Track event listeners for proper cleanup
this.onlineListener = () => {
this.isOnline = true;
log(this.config.debug, 'offline', 'Back online');
if (this.config.offline.syncOnReconnect && this.config.sync.enabled) {
this.connect().then(() => this.sync()).catch(error => {
log(this.config.debug, 'sync', 'Reconnect sync failed:', error);
});
}
this.emit('online');
};
this.offlineListener = () => {
this.isOnline = false;
log(this.config.debug, 'offline', 'Gone offline');
this.emit('offline');
};
window.addEventListener('online', this.onlineListener);
window.addEventListener('offline', this.offlineListener);
}
setupPerformanceMonitoring() {
if (!this.config.debug.performance)
return;
// FIXED: Track interval for proper cleanup
this.performanceInterval = setInterval(() => {
// Only log if there's actual activity
if (this.performanceMetrics.eventsProcessed > 0 || this.performanceMetrics.materializationCount > 0) {
log(this.config.debug, 'performance', 'Metrics:', this.performanceMetrics);
}
}, 60000);
}
async addEvent(event) {
// Add to pending batch
this.pendingEvents.push(event);
// Trigger batch processing
if (this.pendingEvents.length >= this.config.performance.batchSize) {
await this.flushPendingEvents();
}
else {
this.debounceBatchFlush();
}
}
debounceBatchFlush() {
if (this.eventBatchTimeout) {
clearTimeout(this.eventBatchTimeout);
}
this.eventBatchTimeout = setTimeout(() => {
this.flushPendingEvents();
}, this.config.performance.batchDelay);
}
async flushPendingEvents() {
if (this.pendingEvents.length === 0)
return;
const eventsToProcess = [...this.pendingEvents];
this.pendingEvents = [];
// Add to main events array
this.events.push(...eventsToProcess);
// Trim events if over limit
if (this.events.length > this.config.storage.options.maxEvents) {
const trimCount = this.events.length - this.config.storage.options.maxEvents;
this.events = this.events.slice(trimCount);
log(this.config.debug, 'storage', `Trimmed ${trimCount} old events`);
}
// Store in persistent storage
if (this.config.storage.options.persistEvents) {
try {
await this.storage.saveEvents(eventsToProcess);
log(this.config.debug, 'storage', `Saved ${eventsToProcess.length} events`);
}
catch (error) {
log(this.config.debug, 'storage', 'Failed to save events:', error);
}
}
// Emit events
eventsToProcess.forEach(event => {
this.emit(event.type, event.data, event);
this.emit('event', event);
});
// Auto-materialize if enabled
if (this.config.state.autoMaterialize && this.config.state.materializer) {
this.materialize();
}
// Update performance metrics
this.updatePerformanceMetrics('eventsProcessed', eventsToProcess.length);
// Invalidate materialization cache
this.lastMaterializationHash = '';
}
materialize() {
if (!this.config.state.materializer)
return;
const startTime = performance.now();
// Check cache first
if (this.config.performance.materializationCaching) {
const eventsHash = this.getEventsHash();
if (this.materializationCache.has(eventsHash)) {
this.materializedState = this.materializationCache.get(eventsHash);
this.lastMaterializationHash = eventsHash;
return;
}
}
// Materialize state
this.materializedState = this.config.state.materializer(this.events);
const duration = performance.now() - startTime;
// Cache result
if (this.config.performance.materializationCaching) {
const eventsHash = this.getEventsHash();
this.materializationCache.set(eventsHash, this.materializedState);
this.lastMaterializationHash = eventsHash;
// Limit cache size
if (this.materializationCache.size > 10) {
const firstKey = this.materializationCache.keys().next().value;
if (firstKey !== undefined) {
this.materializationCache.delete(firstKey);
}
}
}
this.updatePerformanceMetrics('materialization', duration);
log(this.config.debug, 'performance', `Materialized ${this.events.length} events in ${duration.toFixed(2)}ms`);
}
needsRematerialization() {
if (!this.config.performance.materializationCaching)
return true;
return this.lastMaterializationHash !== this.getEventsHash();
}
getEventsHash() {
return `${this.events.length}-${this.version}`;
}
queueEvent(event) {
this.messageQueue.push(event);
// Limit queue size
if (this.messageQueue.length > this.config.offline.queueSize) {
this.messageQueue = this.messageQueue.slice(-this.config.offline.queueSize);
log(this.config.debug, 'offline', `Queue size limited to ${this.config.offline.queueSize} events`);
}
}
async sendQueuedEvents() {
if (!this.syncClient || !this.isConnected || this.messageQueue.length === 0) {
return { synced: 0, errors: 0 };
}
const events = [...this.messageQueue];
this.messageQueue = [];
let synced = 0;
let errors = 0;
// Send in batches for better performance
const batches = [];
for (let i = 0; i < events.length; i += this.config.performance.batchSize) {
batches.push(events.slice(i, i + this.config.performance.batchSize));
}
for (const batch of batches) {
try {
await this.syncClient.send({
type: 'event-batch',
data: batch
});
synced += batch.length;
log(this.config.debug, 'sync', `Synced batch of ${batch.length} events`);
}
catch (error) {
errors += batch.length;
log(this.config.debug, 'sync', 'Batch sync failed:', error);
// Put events back at front of queue
this.messageQueue.unshift(...batch);
break;
}
}
return { synced, errors };
}
async sendToServer(event) {
if (!this.syncClient) {
throw new KSyncError('Not connected', 'NOT_CONNECTED');
}
if (!this.isConnected) {
log(this.config.debug, 'sync', 'WARNING: Sending event but not connected, queueing');
this.queueEvent(event);
return;
}
log(this.config.debug, 'sync', `Sending event to server: ${event.type} in room ${this.config.room}`);
await this.syncClient.send({
type: 'event',
room: this.config.room,
data: event
});
log(this.config.debug, 'sync', `Event sent to server successfully: ${event.type}`);
}
async authenticate() {
if (!this.syncClient || !this.isConnected)
return;
let token = this.config.auth.token;
if (this.config.auth.provider) {
token = await this.config.auth.provider();
}
if (token) {
await this.syncClient.send({
type: 'auth',
data: {
token,
type: this.config.auth.type,
userId: this.config.userId
}
});
log(this.config.debug, 'sync', 'Authenticated successfully');
}
}
setupSyncHandlers() {
if (!this.syncClient)
return;
this.syncClient.onMessage((message) => {
this.handleSyncMessage(message);
});
this.syncClient.onConnect(() => {
this.isConnected = true;
log(this.config.debug, 'sync', 'Connected to server');
this.emit('connected');
});
this.syncClient.onDisconnect(() => {
this.isConnected = false;
log(this.config.debug, 'sync', 'Disconnected from server');
this.emit('disconnected');
});
}
async handleSyncMessage(message) {
switch (message.type) {
case 'event':
if (message.data) {
// Check if this is our own event (prevent duplication)
const isOwnEvent = message.data._meta?.userId === this.config.userId;
if (!isOwnEvent) {
await this.addEvent(message.data);
}
// Always emit the event for listening, but don't double-store our own events
this.emit(message.data.type, message.data.data, message.data);
}
break;
case 'event-batch':
if (Array.isArray(message.data)) {
for (const event of message.data) {
const isOwnEvent = event._meta?.userId === this.config.userId;
if (!isOwnEvent) {
await this.addEvent(event);
}
// Always emit for listening
this.emit(event.type, event.data, event);
}
}
break;
case 'sync':
case 'sync-response':
if (Array.isArray(message.events)) {
for (const event of message.events) {
// For sync responses, check if we already have this event
const existingEvent = this.events.find(e => e.id === event.id);
if (!existingEvent) {
await this.addEvent(event);
}
}
}
break;
case 'presence-update':
if (message.data && this.config.features.presence) {
this.presenceMap.set(message.data.userId, message.data);
this.emit('presence-update', message.data);
}
break;
case 'stream-start':
case 'stream-chunk':
case 'stream-end':
if (message.data && this.config.features.streaming) {
this.emit(message.type, message.data);
}
break;
default:
log(this.config.debug, 'sync', 'Unknown message type:', message.type);
}
}
updatePerformanceMetrics(type, value) {
switch (type) {
case 'eventProcessing':
this.performanceMetrics.eventsProcessed++;
this.performanceMetrics.averageProcessingTime =
this.performanceMetrics.averageProcessingTime === 0
? value
: (this.performanceMetrics.averageProcessingTime + value) / 2;
break;
case 'materialization':
this.performanceMetrics.materializationCount++;
this.performanceMetrics.averageMaterializationTime =
this.performanceMetrics.averageMaterializationTime === 0
? value
: (this.performanceMetrics.averageMaterializationTime + value) / 2;
break;
case 'eventsProcessed':
this.performanceMetrics.eventsProcessed += value;
break;
}
// Log for debugging in test environment
if (this.config.debug.performance) {
log(this.config.debug, 'performance', `Metrics updated - ${type}: ${value}`, this.performanceMetrics);
}
}
generateUserId() {
return `user-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
}
}
// === CONVENIENCE FACTORY FUNCTIONS ===
/**
* Create a KSync instance with sensible defaults
* @param config Configuration options
* @returns KSync instance
*/
export function createKSync(config) {
return new KSync(config);
}
/**
* Create a KSync instance optimized for chat applications
* @param room Chat room name
* @param config Additional configuration
* @returns KSync instance configured for chat
*/
export function createChat(room, config) {
return new KSync({
room,
// FIXED: Don't force server URL or sync - let user configure
serverUrl: config?.serverUrl,
features: {
presence: true,
streaming: false,
...config?.features
},
sync: {
// FIXED: Only enable sync if serverUrl is provided
enabled: config?.sync?.enabled ?? Boolean(config?.serverUrl),
mode: 'realtime',
options: {
autoReconnect: true,
maxReconnectAttempts: 10,
reconnectDelay: 1000,
...config?.sync?.options
},
...config?.sync
},
state: {
autoMaterialize: true,
materializer: (events) => {
const messages = events.filter(e => e.type === 'message').map(e => e.data);
return { messages, messageCount: messages.length };
},
...config?.state
},
debug: config?.debug ?? false,
...config
});
}
/**
* Create a KSync instance optimized for todo/task applications
* @param config Configuration options
* @returns KSync instance configured for todos
*/
export function createTodos(config) {
return new KSync({
room: 'todos',
// FIXED: Only enable sync if serverUrl is provided
serverUrl: config?.serverUrl,
offline: {
enabled: true,
queueSize: 5000,
persistence: true,
...config?.offline
},
storage: {
type: 'indexeddb',
options: {
persistEvents: true,
maxEvents: 50000,
...config?.storage?.options
},
...config?.storage
},
sync: {
enabled: config?.sync?.enabled ?? Boolean(config?.serverUrl),
...config?.sync
},
debug: config?.debug ?? false, // FIXED: Don't force debug
...config
});
}
/**
* Create a KSync instance optimized for multiplayer games
* @param gameId Game identifier
* @param config Additional configuration
* @returns KSync instance configured for gaming
*/
export function createGame(gameId, config) {
return new KSync({
room: `game-${gameId}`,
// FIXED: Only enable sync if serverUrl is provided
serverUrl: config?.serverUrl,
performance: {
batchSize: 50,
batchDelay: 5,
...config?.performance
},
features: {
presence: true,
streaming: true,
...config?.features
},
sync: {
enabled: config?.sync?.enabled ?? Boolean(config?.serverUrl),
...config?.sync
},
debug: config?.debug ?? false, // FIXED: Don't force debug
...config
});
}
/**
* Create a KSync instance optimized for AI applications with streaming
* @param conversationId Conversation identifier
* @param config Additional configuration
* @returns KSync instance configured for AI
*/
export function createAI(conversationId, config) {
return new KSync({
room: conversationId || 'ai-chat',
// FIXED: Only enable sync if serverUrl is provided
serverUrl: config?.serverUrl,
features: {
streaming: true,
presence: false,
...config?.features
},
performance: {
batchSize: 10,
batchDelay: 1,
...config?.performance
},
sync: {
enabled: config?.sync?.enabled ?? Boolean(config?.serverUrl),
...config?.sync
},
debug: config?.debug ?? false, // FIXED: Don't force debug
...config
});
}
//# sourceMappingURL=core.js.map