n8n-nodes-chat2crm 0.1.19 → 0.1.21

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.
@@ -260,25 +260,60 @@ class Chat2CrmTrigger {
260
260
  // Запускаем один последовательный polling loop (без setInterval), чтобы не было наложения вызовов
261
261
  let pollingPromise = null;
262
262
  const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
263
+ const KEEPALIVE_INTERVAL_MS = 30_000;
264
+ let lastKeepaliveAt = Date.now();
265
+ const keepalive = async () => {
266
+ const now = Date.now();
267
+ if (now - lastKeepaliveAt < KEEPALIVE_INTERVAL_MS)
268
+ return;
269
+ lastKeepaliveAt = now;
270
+ for (const [db, redis] of redisConnections.entries()) {
271
+ if (isClosing)
272
+ return;
273
+ try {
274
+ await redis.ping();
275
+ if (isDebug) {
276
+ console.log(`[Chat2Crm Trigger] Keepalive PING OK (DB ${db})`);
277
+ }
278
+ }
279
+ catch (error) {
280
+ // Не роняем триггер из-за keepalive — это best-effort. Ошибка даст сигнал, что SSH/Redis начинает умирать.
281
+ console.error(`[Chat2Crm Trigger] Keepalive PING failed (DB ${db}):`, error?.message ?? error);
282
+ }
283
+ }
284
+ };
263
285
  const runPollingLoop = async () => {
264
- while (!isClosing) {
265
- await readMessages();
266
- // Если block=0, добавляем небольшой интервал чтобы не прожигать CPU
286
+ try {
287
+ while (!isClosing) {
288
+ await readMessages();
289
+ await keepalive();
290
+ // Если block=0, добавляем небольшой интервал чтобы не прожигать CPU
291
+ if (!isClosing) {
292
+ await sleep(pollInterval);
293
+ }
294
+ }
295
+ }
296
+ catch (error) {
297
+ // Важно: не даём polling loop'у "повесить" деактивацию воркфлоу
267
298
  if (!isClosing) {
268
- await sleep(pollInterval);
299
+ console.error('[Chat2Crm Trigger] Polling loop crashed:', error);
269
300
  }
270
301
  }
271
302
  };
272
303
  pollingPromise = runPollingLoop();
304
+ const withTimeout = async (promise, timeoutMs) => {
305
+ return await Promise.race([
306
+ promise,
307
+ new Promise((_, reject) => setTimeout(() => reject(new Error('close timeout')), timeoutMs)),
308
+ ]);
309
+ };
273
310
  // Возвращаем объект с функцией closeFunction для cleanup
274
311
  return {
275
312
  closeFunction: async () => {
276
313
  console.log(`[Chat2Crm Trigger] Closing trigger, cleaning up resources...`);
277
314
  isClosing = true; // Устанавливаем флаг закрытия
278
315
  try {
279
- // Ждем завершения loop'а, чтобы не было гонок с отключением соединений
280
- await pollingPromise?.catch(() => undefined);
281
- // Используем disconnect() вместо quit() для более быстрого закрытия
316
+ // ВАЖНО: СНАЧАЛА рвём соединения, чтобы прервать возможный зависший XREAD (особенно через SSH)
282
317
  for (const [db, redis] of redisConnections.entries()) {
283
318
  try {
284
319
  redis.disconnect();
@@ -288,6 +323,15 @@ class Chat2CrmTrigger {
288
323
  }
289
324
  }
290
325
  redisConnections.clear();
326
+ // Не даём unpublish/deactivate висеть бесконечно: ждём loop ограниченное время
327
+ if (pollingPromise) {
328
+ try {
329
+ await withTimeout(pollingPromise.catch(() => undefined), 1000);
330
+ }
331
+ catch {
332
+ // Игнорируем таймаут, главное — быстро закрыть триггер
333
+ }
334
+ }
291
335
  console.log(`[Chat2Crm Trigger] Cleanup completed`);
292
336
  }
293
337
  catch (error) {
@@ -117,6 +117,13 @@ async function createRedisConnection(credentials, db = 0) {
117
117
  console.log(`[Redis Connection] Creating tunnel to Redis at ${remoteRedisHost}:${remoteRedisPort} on remote server`);
118
118
  // Создаем локальный TCP сервер для проксирования через SSH туннель
119
119
  const localServer = net.createServer((localSocket) => {
120
+ // TCP keepalive помогает держать соединение живым при длительном простое (важно для SSH/прокси/фаерволов)
121
+ try {
122
+ localSocket.setKeepAlive(true, 60_000); // 60 секунд
123
+ }
124
+ catch (e) {
125
+ // best-effort
126
+ }
120
127
  // Создаем SSH туннель для каждого соединения
121
128
  sshClient.forwardOut('127.0.0.1', 0, remoteRedisHost, remoteRedisPort, (err, stream) => {
122
129
  if (err) {
@@ -177,6 +184,8 @@ async function createRedisConnection(credentials, db = 0) {
177
184
  enableReadyCheck: true,
178
185
  maxRetriesPerRequest: 3,
179
186
  connectTimeout: 10000,
187
+ // TCP keepalive для предотвращения "тихих" разрывов соединения при простое (особенно через SSH)
188
+ keepAlive: 30_000,
180
189
  retryStrategy: (times) => {
181
190
  const delay = Math.min(times * 50, 2000);
182
191
  return delay;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-chat2crm",
3
- "version": "0.1.19",
3
+ "version": "0.1.21",
4
4
  "description": "n8n node for Chat2Crm Redis integration",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",