i18nexus-cli 3.8.0 → 3.8.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/index.js CHANGED
@@ -49,7 +49,8 @@ program
49
49
  )
50
50
  .option(
51
51
  '--compact',
52
- 'Output JSON without extra whitespace (default is pretty-printed)'
52
+ 'Output JSON without extra whitespace (default is pretty-printed)',
53
+ false
53
54
  )
54
55
  .action(options => {
55
56
  pull({
@@ -275,7 +276,8 @@ program
275
276
  )
276
277
  .option(
277
278
  '--compact',
278
- 'Output JSON without extra whitespace (default is pretty-printed)'
279
+ 'Output JSON without extra whitespace (default is pretty-printed)',
280
+ false
279
281
  )
280
282
  .action(options => {
281
283
  listen({
@@ -5,7 +5,8 @@ const pull = require('../commands/pull');
5
5
  const baseUrl = require('../baseUrl');
6
6
  const { URL } = require('url');
7
7
 
8
- const listen = async ({ apiKey, path, compact = false }) => {
8
+ const listen = async ({ apiKey, path, compact }) => {
9
+ // Initial sync
9
10
  await pull(
10
11
  {
11
12
  apiKey,
@@ -15,69 +16,149 @@ const listen = async ({ apiKey, path, compact = false }) => {
15
16
  confirmed: false,
16
17
  compact
17
18
  },
18
- {
19
- logging: false,
20
- successLog: 'Latest strings downloaded'
21
- }
19
+ { logging: false, successLog: 'Latest strings downloaded' }
22
20
  );
23
21
 
24
22
  const wsUrl = new URL(`${baseUrl.replace(/^http/, 'ws')}/cable`);
25
23
  wsUrl.searchParams.set('api_key', apiKey);
26
24
 
27
- const ws = new WebSocket(wsUrl.toString(), {
28
- headers: {
29
- Origin: 'http://cli.i18nexus.com'
30
- }
31
- });
25
+ let ws;
26
+ let pingInterval; // timer sending pings
27
+ let watchdogInterval; // timer detecting sleep/drift
28
+ let lastPong = Date.now();
29
+ let reconnectAttempts = 0;
30
+ const PING_EVERY_MS = 30_000; // send ping every 30s
31
+ const STALE_AFTER_MS = 90_000; // if >90s since last pong => stale
32
+ const WATCHDOG_EVERY_MS = 10_000; // check for sleep every 10s
33
+
34
+ const backoff = () => {
35
+ const base = Math.min(60_000, 1000 * Math.pow(2, reconnectAttempts)); // 1s -> 60s
36
+ const jitter = Math.floor(Math.random() * 1000);
37
+ return base + jitter;
38
+ };
39
+
40
+ const clearTimers = () => {
41
+ if (pingInterval) clearInterval(pingInterval);
42
+ if (watchdogInterval) clearInterval(watchdogInterval);
43
+ pingInterval = undefined;
44
+ watchdogInterval = undefined;
45
+ };
46
+
47
+ const startHeartbeat = () => {
48
+ // respond to server pings automatically (ws does this), we send pings to keep NAT/CF alive
49
+ ws.on('pong', () => {
50
+ lastPong = Date.now();
51
+ });
52
+
53
+ pingInterval = setInterval(() => {
54
+ if (ws && ws.readyState === WebSocket.OPEN) {
55
+ try {
56
+ ws.ping();
57
+ } catch (_) {}
58
+ }
59
+ // consider stale?
60
+ if (Date.now() - lastPong > STALE_AFTER_MS) {
61
+ try {
62
+ ws.terminate();
63
+ } catch (_) {}
64
+ }
65
+ }, PING_EVERY_MS);
32
66
 
33
- ws.on('open', () => {
34
- console.log(colors.green('Listening for i18nexus string updates...'));
67
+ // detect sleep/wake (large timer drift)
68
+ let lastTick = Date.now();
69
+ watchdogInterval = setInterval(() => {
70
+ const now = Date.now();
71
+ if (now - lastTick > PING_EVERY_MS * 3) {
72
+ // slept for a while
73
+ try {
74
+ ws.terminate();
75
+ } catch (_) {}
76
+ }
77
+ lastTick = now;
78
+ }, WATCHDOG_EVERY_MS);
79
+ };
35
80
 
81
+ const subscribe = () => {
36
82
  ws.send(
37
83
  JSON.stringify({
38
84
  command: 'subscribe',
39
85
  identifier: JSON.stringify({ channel: 'CliListenChannel' })
40
86
  })
41
87
  );
42
- });
43
-
44
- ws.on('message', async message => {
45
- try {
46
- const data = JSON.parse(message);
47
- const payload = data.message;
48
-
49
- if (payload?.event === 'strings.changed') {
50
- await pull(
51
- {
52
- apiKey,
53
- version: 'latest',
54
- path,
55
- clean: false,
56
- confirmed: false,
57
- compact
58
- },
59
- {
60
- logging: false,
61
- successLog: ' ✔ Translations updated'
62
- }
63
- );
88
+ };
89
+
90
+ const connect = () => {
91
+ ws = new WebSocket(wsUrl.toString(), {
92
+ headers: { Origin: 'http://cli.i18nexus.com' }
93
+ });
94
+
95
+ ws.on('open', () => {
96
+ reconnectAttempts = 0;
97
+ lastPong = Date.now();
98
+ console.log(colors.green('Listening for i18nexus string updates...'));
99
+ subscribe();
100
+ startHeartbeat();
101
+
102
+ // Optional: TCP keepalive at the socket layer
103
+ if (ws._socket && ws._socket.setKeepAlive) {
104
+ ws._socket.setKeepAlive(true, 60_000);
64
105
  }
65
- } catch (e) {
66
- console.error(colors.red('i18nexus Sync Failed'));
67
- }
68
- });
106
+ });
69
107
 
70
- ws.on('close', () => {
71
- process.exit(1);
72
- });
108
+ ws.on('message', async message => {
109
+ try {
110
+ const data = JSON.parse(message);
111
+ const payload = data.message;
112
+ if (payload?.event === 'strings.changed') {
113
+ await pull(
114
+ {
115
+ apiKey,
116
+ version: 'latest',
117
+ path,
118
+ clean: false,
119
+ confirmed: false,
120
+ compact
121
+ },
122
+ { logging: false, successLog: ' ✔ Translations updated' }
123
+ );
124
+ }
125
+ } catch (e) {
126
+ console.error(colors.red('i18nexus Sync Failed'));
127
+ }
128
+ });
129
+
130
+ const scheduleReconnect = why => {
131
+ clearTimers();
132
+ if (ws) {
133
+ ws.removeAllListeners();
134
+ }
135
+ reconnectAttempts += 1;
136
+ const delay = backoff();
137
+ console.log(
138
+ colors.yellow(
139
+ `Attempting i18nexus reconnect in ${Math.round(delay / 1000)}s...`
140
+ )
141
+ );
142
+ setTimeout(connect, delay);
143
+ };
73
144
 
74
- ws.on('error', err => {
75
- console.error(colors.red('i18nexus Connection Error'));
76
- process.exit(1);
77
- });
145
+ ws.on('close', () => scheduleReconnect('close'));
146
+ ws.on('error', () => scheduleReconnect('error'));
147
+ };
148
+
149
+ connect();
78
150
 
79
151
  const cleanUp = () => {
80
- ws.close();
152
+ clearTimers();
153
+ if (
154
+ ws &&
155
+ (ws.readyState === WebSocket.OPEN ||
156
+ ws.readyState === WebSocket.CONNECTING)
157
+ ) {
158
+ try {
159
+ ws.close();
160
+ } catch (_) {}
161
+ }
81
162
  process.exit(0);
82
163
  };
83
164
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "i18nexus-cli",
3
- "version": "3.8.0",
3
+ "version": "3.8.2",
4
4
  "description": "Command line interface (CLI) for accessing the i18nexus API",
5
5
  "main": "index.js",
6
6
  "bin": {