UNPKG

i18nexus-cli

Version:

Command line interface (CLI) for accessing the i18nexus API

170 lines (150 loc) 4.34 kB
#!/usr/bin/env node const WebSocket = require('ws'); const colors = require('colors'); const pull = require('../commands/pull'); const baseUrl = require('../baseUrl'); const { URL } = require('url'); const listen = async ({ apiKey, path, compact }) => { // Initial sync await pull( { apiKey, version: 'latest', path, clean: false, confirmed: false, compact }, { logging: false, successLog: 'Latest strings downloaded' } ); const wsUrl = new URL(`${baseUrl.replace(/^http/, 'ws')}/cable`); wsUrl.searchParams.set('api_key', apiKey); let ws; let pingInterval; // timer sending pings let watchdogInterval; // timer detecting sleep/drift let lastPong = Date.now(); let reconnectAttempts = 0; const PING_EVERY_MS = 30_000; // send ping every 30s const STALE_AFTER_MS = 90_000; // if >90s since last pong => stale const WATCHDOG_EVERY_MS = 10_000; // check for sleep every 10s const backoff = () => { const base = Math.min(60_000, 1000 * Math.pow(2, reconnectAttempts)); // 1s -> 60s const jitter = Math.floor(Math.random() * 1000); return base + jitter; }; const clearTimers = () => { if (pingInterval) clearInterval(pingInterval); if (watchdogInterval) clearInterval(watchdogInterval); pingInterval = undefined; watchdogInterval = undefined; }; const startHeartbeat = () => { // respond to server pings automatically (ws does this), we send pings to keep NAT/CF alive ws.on('pong', () => { lastPong = Date.now(); }); pingInterval = setInterval(() => { if (ws && ws.readyState === WebSocket.OPEN) { try { ws.ping(); } catch (_) {} } // consider stale? if (Date.now() - lastPong > STALE_AFTER_MS) { try { ws.terminate(); } catch (_) {} } }, PING_EVERY_MS); // detect sleep/wake (large timer drift) let lastTick = Date.now(); watchdogInterval = setInterval(() => { const now = Date.now(); if (now - lastTick > PING_EVERY_MS * 3) { // slept for a while try { ws.terminate(); } catch (_) {} } lastTick = now; }, WATCHDOG_EVERY_MS); }; const subscribe = () => { ws.send( JSON.stringify({ command: 'subscribe', identifier: JSON.stringify({ channel: 'CliListenChannel' }) }) ); }; const connect = () => { ws = new WebSocket(wsUrl.toString(), { headers: { Origin: 'http://cli.i18nexus.com' } }); ws.on('open', () => { reconnectAttempts = 0; lastPong = Date.now(); console.log(colors.green('Listening for i18nexus string updates...')); subscribe(); startHeartbeat(); // Optional: TCP keepalive at the socket layer if (ws._socket && ws._socket.setKeepAlive) { ws._socket.setKeepAlive(true, 60_000); } }); ws.on('message', async message => { try { const data = JSON.parse(message); const payload = data.message; if (payload?.event === 'strings.changed') { await pull( { apiKey, version: 'latest', path, clean: false, confirmed: false, compact }, { logging: false, successLog: ' ✔ Translations updated' } ); } } catch (e) { console.error(colors.red('i18nexus Sync Failed')); } }); const scheduleReconnect = why => { clearTimers(); if (ws) { ws.removeAllListeners(); } reconnectAttempts += 1; const delay = backoff(); console.log( colors.yellow( `Attempting i18nexus reconnect in ${Math.round(delay / 1000)}s...` ) ); setTimeout(connect, delay); }; ws.on('close', () => scheduleReconnect('close')); ws.on('error', () => scheduleReconnect('error')); }; connect(); const cleanUp = () => { clearTimers(); if ( ws && (ws.readyState === WebSocket.OPEN || ws.readyState === WebSocket.CONNECTING) ) { try { ws.close(); } catch (_) {} } process.exit(0); }; process.on('SIGINT', cleanUp); process.on('SIGTERM', cleanUp); }; module.exports = listen;