i18nexus-cli
Version:
Command line interface (CLI) for accessing the i18nexus API
170 lines (150 loc) • 4.34 kB
JavaScript
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;