UNPKG

trellis

Version:

Agentic State Engine — event-sourced causal graph with branching, decision traces, and realtime sync for AI-native applications

217 lines (214 loc) 5.91 kB
// src/sync/rate-limiter.ts var RateLimiter = class { options; peerBuckets = /* @__PURE__ */ new Map(); constructor(opts = {}) { this.options = { maxMessages: 100, windowMs: 1e3, ...opts }; } /** * Check if a peer is allowed to send a message. */ check(peerId) { const now = Date.now(); const bucket = this.peerBuckets.get(peerId); if (!bucket || now > bucket.resetTime) { const resetTime = now + this.options.windowMs; this.peerBuckets.set(peerId, { count: 1, resetTime }); return { allowed: true, remaining: this.options.maxMessages - 1, resetTime }; } if (bucket.count >= this.options.maxMessages) { return { allowed: false, remaining: 0, resetTime: bucket.resetTime }; } bucket.count++; return { allowed: true, remaining: this.options.maxMessages - bucket.count, resetTime: bucket.resetTime }; } /** * Reset a peer's rate limit bucket. */ reset(peerId) { this.peerBuckets.delete(peerId); } /** * Clear all peer buckets. */ clear() { this.peerBuckets.clear(); } /** * Get current state for a peer. */ getState(peerId) { const bucket = this.peerBuckets.get(peerId); if (!bucket) return null; const now = Date.now(); if (now > bucket.resetTime) { return { allowed: true, remaining: this.options.maxMessages - 1, resetTime: now + this.options.windowMs }; } return { allowed: bucket.count < this.options.maxMessages, remaining: Math.max(0, this.options.maxMessages - bucket.count), resetTime: bucket.resetTime }; } }; // src/sync/websocket-transport.ts var WebSocketTransport = class { options; ws = null; messageHandler = null; state = { connected: false, reconnectAttempts: 0 }; reconnectTimer = null; rateLimiter; constructor(opts) { this.options = { reconnectDelay: 5e3, maxReconnectAttempts: Infinity, maxMessageSize: 10 * 1024 * 1024, // 10MB default enforceTls: process.env.TRELLIS_SYNC_ENV === "production", ...opts }; this.rateLimiter = new RateLimiter(this.options.rateLimiter); } /** * Connect to the sync server. */ async connect() { if (this.ws?.readyState === WebSocket.OPEN) { return; } if (this.options.enforceTls && this.options.url.startsWith("ws://")) { throw new Error( "TLS enforcement: ws:// not allowed in production environment. Use wss://" ); } return new Promise((resolve, reject) => { try { const wsUrl = new URL(this.options.url); if (this.options.authToken) { wsUrl.searchParams.set("token", this.options.authToken); } this.ws = new WebSocket(wsUrl.toString()); this.ws.onopen = () => { this.state.connected = true; this.state.reconnectAttempts = 0; this.state.lastError = void 0; resolve(); }; this.ws.onmessage = (event) => { const messageSize = event.data.length; if (messageSize > (this.options.maxMessageSize ?? 10 * 1024 * 1024)) { console.error( `Message size (${messageSize} bytes) exceeds limit (${this.options.maxMessageSize} bytes)` ); this.ws?.close(); return; } const rateLimit = this.rateLimiter.check("server"); if (!rateLimit.allowed) { console.error( `Rate limit exceeded for peer: server. Reset at ${new Date(rateLimit.resetTime).toISOString()}` ); return; } if (this.messageHandler) { try { const message = JSON.parse(event.data.toString()); this.messageHandler(message); } catch (err) { console.error("Failed to parse sync message:", err); } } }; this.ws.onerror = (error) => { this.state.lastError = error instanceof Error ? error.message : "Unknown error"; reject(error); }; this.ws.onclose = () => { this.state.connected = false; this.scheduleReconnect(); }; } catch (err) { reject(err); } }); } /** * Disconnect from the sync server. */ async disconnect() { if (this.reconnectTimer) { clearTimeout(this.reconnectTimer); this.reconnectTimer = null; } if (this.ws) { this.ws.close(); this.ws = null; } this.state.connected = false; } /** * Send a message to the sync server. */ async send(_peerId, message) { if (!this.ws || this.ws.readyState !== WebSocket.OPEN) { throw new Error("WebSocket not connected"); } this.ws.send(JSON.stringify(message)); } /** * Register a handler for incoming messages. */ onMessage(handler) { this.messageHandler = handler; } /** * List connected peers. * Note: WebSocket transport connects to a single server, so this returns * the server as a peer if connected. */ peers() { if (!this.state.connected) { return []; } return [ { id: "server", name: "Sync Server", lastSeen: (/* @__PURE__ */ new Date()).toISOString() } ]; } /** * Get current transport state. */ getState() { return { ...this.state }; } /** * Schedule reconnection attempt. */ scheduleReconnect() { if (this.state.reconnectAttempts >= (this.options.maxReconnectAttempts ?? Infinity)) { console.error("Max reconnection attempts reached"); return; } this.reconnectTimer = setTimeout(() => { this.state.reconnectAttempts++; this.connect().catch((err) => { console.error("Reconnection failed:", err); }); }, this.options.reconnectDelay); } }; export { RateLimiter, WebSocketTransport };