n8n-nodes-chat2crm 0.1.16 → 0.1.17

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.
@@ -121,6 +121,10 @@ class Chat2CrmTrigger {
121
121
  }
122
122
  // Храним последние прочитанные ID для каждого stream
123
123
  const lastReadIds = new Map();
124
+ // ИСПРАВЛЕНИЕ: Добавляем флаг для предотвращения параллельных вызовов
125
+ let isReading = false;
126
+ // ИСПРАВЛЕНИЕ: Добавляем Set для отслеживания уже обработанных messageId
127
+ const processedMessageIds = new Set();
124
128
  // Собираем информацию о найденных streams для вывода
125
129
  const foundStreams = [];
126
130
  // Инициализируем lastReadIds для каждого stream и собираем информацию
@@ -242,87 +246,97 @@ class Chat2CrmTrigger {
242
246
  let isClosing = false;
243
247
  // Функция для чтения сообщений
244
248
  const readMessages = async () => {
245
- // Проверяем, не закрывается ли триггер
246
- if (isClosing) {
249
+ // ИСПРАВЛЕНИЕ: Предотвращаем параллельные вызовы
250
+ if (isReading || isClosing) {
247
251
  return;
248
252
  }
249
- for (const [db, dbStreams] of streamsByDb.entries()) {
250
- // Проверяем снова перед каждой итерацией
251
- if (isClosing) {
252
- break;
253
- }
254
- const redis = redisConnections.get(db);
255
- // Читаем каждый stream отдельно через XREAD
256
- // XREAD читает напрямую из stream, независимо от consumer groups
257
- for (const stream of dbStreams) {
253
+ isReading = true;
254
+ try {
255
+ for (const [db, dbStreams] of streamsByDb.entries()) {
258
256
  if (isClosing) {
259
257
  break;
260
258
  }
261
- try {
262
- const lastId = lastReadIds.get(stream) || '$';
263
- // Используем более короткий block time для возможности быстрого закрытия
264
- const blockTime = isClosing ? 0 : Math.min(block, MAX_BLOCK_TIME_FOR_RESPONSIVENESS_MS);
265
- // Используем XREAD чтобы читать все сообщения напрямую из stream
266
- // Это позволяет читать сообщения даже если они уже обработаны другими consumer'ами
267
- const messages = await redis.xread('COUNT', count, 'BLOCK', blockTime, 'STREAMS', stream, lastId);
268
- // Проверяем снова после блокирующей операции
259
+ const redis = redisConnections.get(db);
260
+ for (const stream of dbStreams) {
269
261
  if (isClosing) {
270
- return;
262
+ break;
271
263
  }
272
- if (messages && Array.isArray(messages) && messages.length > 0) {
273
- // XREAD возвращает массив с одним элементом [streamName, messages]
274
- const streamData = messages[0];
275
- const [streamName, streamMessages] = streamData;
276
- if (Array.isArray(streamMessages) && streamMessages.length > 0) {
277
- // Обновляем lastReadId на последний прочитанный ID
278
- const lastMessageId = streamMessages[streamMessages.length - 1][0];
279
- lastReadIds.set(streamName, lastMessageId);
280
- for (const [messageId, fields] of streamMessages) {
281
- if (isClosing) {
282
- break;
283
- }
284
- const messageData = {};
285
- // Parse fields
286
- for (let j = 0; j < fields.length; j += 2) {
287
- messageData[fields[j]] = fields[j + 1];
288
- }
289
- // Parse commands if it's a string
290
- if (messageData.commands && typeof messageData.commands === 'string') {
291
- try {
292
- messageData.commands = JSON.parse(messageData.commands);
264
+ try {
265
+ const lastId = lastReadIds.get(stream) || '$';
266
+ const blockTime = isClosing ? 0 : Math.min(block, MAX_BLOCK_TIME_FOR_RESPONSIVENESS_MS);
267
+ const messages = await redis.xread('COUNT', count, 'BLOCK', blockTime, 'STREAMS', stream, lastId);
268
+ if (isClosing) {
269
+ return;
270
+ }
271
+ if (messages && Array.isArray(messages) && messages.length > 0) {
272
+ const streamData = messages[0];
273
+ const [streamName, streamMessages] = streamData;
274
+ if (Array.isArray(streamMessages) && streamMessages.length > 0) {
275
+ // ИСПРАВЛЕНИЕ: Фильтруем уже обработанные сообщения
276
+ const newMessages = streamMessages.filter(([messageId]) => {
277
+ const messageKey = `${streamName}:${messageId}`;
278
+ if (processedMessageIds.has(messageKey)) {
279
+ return false; // Пропускаем уже обработанное сообщение
280
+ }
281
+ processedMessageIds.add(messageKey);
282
+ return true;
283
+ });
284
+ // ИСПРАВЛЕНИЕ: Обновляем lastReadId сразу после чтения, ДО обработки
285
+ const lastMessageId = streamMessages[streamMessages.length - 1][0];
286
+ lastReadIds.set(streamName, lastMessageId);
287
+ // ИСПРАВЛЕНИЕ: Обрабатываем только новые сообщения
288
+ for (const [messageId, fields] of newMessages) {
289
+ if (isClosing) {
290
+ break;
293
291
  }
294
- catch (e) {
295
- // Ignore parse errors
292
+ const messageData = {};
293
+ for (let j = 0; j < fields.length; j += 2) {
294
+ messageData[fields[j]] = fields[j + 1];
296
295
  }
296
+ if (messageData.commands && typeof messageData.commands === 'string') {
297
+ try {
298
+ messageData.commands = JSON.parse(messageData.commands);
299
+ }
300
+ catch (e) {
301
+ // Ignore parse errors
302
+ }
303
+ }
304
+ const executionData = {
305
+ json: {
306
+ messageId,
307
+ stream: streamName,
308
+ data: messageData,
309
+ commandList: messageData,
310
+ },
311
+ };
312
+ this.emit([[executionData]]);
313
+ }
314
+ // ИСПРАВЛЕНИЕ: Очищаем старые записи из Set для предотвращения утечки памяти
315
+ // Оставляем только последние 1000 обработанных ID
316
+ if (processedMessageIds.size > 1000) {
317
+ const entries = Array.from(processedMessageIds);
318
+ const toRemove = entries.slice(0, entries.length - 1000);
319
+ toRemove.forEach(id => processedMessageIds.delete(id));
297
320
  }
298
- // Триггерим workflow с данными сообщения
299
- const executionData = {
300
- json: {
301
- messageId,
302
- stream: streamName,
303
- data: messageData,
304
- commandList: messageData,
305
- },
306
- };
307
- this.emit([[executionData]]);
308
321
  }
309
322
  }
310
323
  }
311
- }
312
- catch (error) {
313
- // Игнорируем ошибки при закрытии
314
- if (isClosing && (error.message?.includes('Connection is closed') || error.message?.includes('disconnect'))) {
315
- return;
316
- }
317
- // Игнорируем ошибки для несуществующих streams
318
- if (error.message?.includes('Invalid stream ID') || error.message?.includes('no such key')) {
319
- continue;
324
+ catch (error) {
325
+ if (isClosing && (error.message?.includes('Connection is closed') || error.message?.includes('disconnect'))) {
326
+ return;
327
+ }
328
+ if (error.message?.includes('Invalid stream ID') || error.message?.includes('no such key')) {
329
+ continue;
330
+ }
331
+ console.error(`[Chat2Crm Trigger] Error reading stream ${stream} from Redis DB ${db}:`, error);
320
332
  }
321
- // Логируем только критические ошибки
322
- console.error(`[Chat2Crm Trigger] Error reading stream ${stream} from Redis DB ${db}:`, error);
323
333
  }
324
334
  }
325
335
  }
336
+ finally {
337
+ // ИСПРАВЛЕНИЕ: Всегда сбрасываем флаг, даже при ошибке
338
+ isReading = false;
339
+ }
326
340
  };
327
341
  // Начинаем polling с более коротким интервалом
328
342
  let interval = null;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "n8n-nodes-chat2crm",
3
- "version": "0.1.16",
3
+ "version": "0.1.17",
4
4
  "description": "n8n node for Chat2Crm Redis integration",
5
5
  "keywords": [
6
6
  "n8n-community-node-package",