UNPKG

@magnusbag/livets-client

Version:

Client-side connector for LiveTS framework - real-time server-rendered web applications

185 lines (182 loc) 7.63 kB
(function () { 'use strict'; /** * LiveTS Client Connector - Ultra-minimal browser runtime for LiveTS applications * Optimized for compact WebSocket messages only */ class LiveTSConnector { constructor() { this.ws = null; this.reconnectAttempts = 0; this.maxReconnectAttempts = 5; this.reconnectDelay = 1000; this.pingInterval = null; this.init(); } init() { this.connect(); this.setupEventDelegation(); } connect() { const wsUrl = this.getWebSocketUrl(); try { this.ws = new WebSocket(wsUrl); this.ws.onopen = () => this.onOpen(); this.ws.onmessage = event => this.onMessage(event); this.ws.onclose = () => this.onClose(); this.ws.onerror = error => this.onError(error); } catch (error) { console.error('Failed to create WebSocket connection:', error); this.scheduleReconnect(); } } onOpen() { console.log('🔗 LiveTS connected'); this.reconnectAttempts = 0; this.startPing(); } onMessage(event) { try { const msg = JSON.parse(event.data); if (msg.t === 'p') { // Ultra-compact format: {t: 'p', c: 'shortId', d: ['op|sel|data', ...]} this.applyCompactPatches(msg.d || []); } } catch (error) { console.error('Failed to parse message:', error); } } onClose() { console.log('🔌 LiveTS disconnected'); this.stopPing(); this.scheduleReconnect(); } onError(error) { console.error('WebSocket error:', error); } scheduleReconnect() { if (this.reconnectAttempts < this.maxReconnectAttempts) { this.reconnectAttempts++; setTimeout(() => this.connect(), this.reconnectDelay * this.reconnectAttempts); } } startPing() { this.pingInterval = window.setInterval(() => { var _a; if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) { // Ultra-compact ping: just "p" (3 bytes vs 19 bytes - 84% reduction) this.ws.send('"p"'); } }, 30000); } stopPing() { if (this.pingInterval) { clearInterval(this.pingInterval); this.pingInterval = null; } } setupEventDelegation() { // Single event listener for all ts-on: events document.addEventListener('click', e => this.handleEvent(e, 'click')); document.addEventListener('input', e => this.handleEvent(e, 'input')); document.addEventListener('change', e => this.handleEvent(e, 'change')); document.addEventListener('submit', e => this.handleEvent(e, 'submit')); } handleEvent(event, type) { const target = event.target; const element = target.closest(`[ts-on\\:${type}]`); if (!element) return; const handler = element.getAttribute(`ts-on:${type}`); const componentElement = element.closest('[data-livets-id]'); if (!handler || !componentElement) return; const componentId = componentElement.dataset.livetsId; if (!componentId) return; event.preventDefault(); this.sendEvent(componentId, handler, this.extractEventData(event, element)); } extractEventData(event, element) { const target = element; return { type: event.type, target: { tagName: element.tagName.toLowerCase(), value: target.value || undefined, checked: target.checked || undefined, dataset: target.dataset || {} } }; } sendEvent(componentId, eventName, payload) { var _a, _b, _c, _d; if (((_a = this.ws) === null || _a === void 0 ? void 0 : _a.readyState) === WebSocket.OPEN) { // Ultra-compact event format: "e|shortId|eventName|value|checked|tagName" // Example: "e|abc123|increment||false|button" (~30 bytes vs ~200 bytes - 85% reduction) const shortId = componentId.substring(0, 8); const value = ((_b = payload === null || payload === void 0 ? void 0 : payload.target) === null || _b === void 0 ? void 0 : _b.value) || ''; const checked = ((_c = payload === null || payload === void 0 ? void 0 : payload.target) === null || _c === void 0 ? void 0 : _c.checked) ? '1' : '0'; const tagName = ((_d = payload === null || payload === void 0 ? void 0 : payload.target) === null || _d === void 0 ? void 0 : _d.tagName) || ''; const compactEvent = `"e|${shortId}|${eventName}|${value}|${checked}|${tagName}"`; this.ws.send(compactEvent); } } applyCompactPatches(compactPatches) { compactPatches.forEach(compact => { try { const parts = compact.split('|'); const op = parts[0]; const selector = `[data-ts-sel="${parts[1]}"]`; const element = document.querySelector(selector); if (!element) return; switch (op) { case 't': // UpdateText element.textContent = parts[2] || ''; break; case 'a': // SetAttribute element.setAttribute(parts[2], parts[3] || ''); break; case 'r': // RemoveAttribute element.removeAttribute(parts[2]); break; case 'h': // ReplaceInnerHtml element.innerHTML = parts[2] || ''; break; case 'e': // ReplaceElement element.outerHTML = parts[2] || ''; break; } } catch (error) { console.error('Failed to apply patch:', compact, error); } }); } getWebSocketUrl() { // Allow server to inject custom WS URL const override = window.LIVETS_WS_URL; if (override) return override; const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:'; return `${protocol}//${window.location.host}/ws`; } } // Auto-initialize when DOM is ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => new LiveTSConnector()); } else { new LiveTSConnector(); } // Export for module systems if (typeof module !== 'undefined' && module.exports) { module.exports = LiveTSConnector; } if (typeof window !== 'undefined') { window.LiveTSConnector = LiveTSConnector; } })();