@magnusbag/livets-client
Version:
Client-side connector for LiveTS framework - real-time server-rendered web applications
185 lines (182 loc) • 7.63 kB
JavaScript
(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;
}
})();