@nitra/nats 4.2.2 → 4.3.0

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nitra/nats",
3
- "version": "4.2.2",
3
+ "version": "4.3.0",
4
4
  "description": "nats helper",
5
5
  "keywords": [
6
6
  "nats",
@@ -27,8 +27,8 @@
27
27
  "@nats-io/jetstream": "^3.3.0",
28
28
  "@nats-io/nats-core": "^3.3.0",
29
29
  "@nats-io/transport-node": "^3.3.0",
30
- "@nitra/check-env": "^4.1.0",
31
- "@nitra/pino": "^2.10.0",
30
+ "@nitra/check-env": "^4.1.1",
31
+ "@nitra/pino": "^2.12.0",
32
32
  "js-yaml": "^4.1.1"
33
33
  },
34
34
  "engines": {
package/src/consumer.js CHANGED
@@ -1,6 +1,6 @@
1
- import { jsm } from './nats.js'
1
+ import { millis, nanos } from '@nats-io/nats-core'
2
2
  import { log } from '@nitra/pino'
3
- import { nanos, millis } from '@nats-io/nats-core'
3
+ import { jsm } from './nats.js'
4
4
 
5
5
  /**
6
6
  * Створює або оновлює consumer.
@@ -10,6 +10,7 @@ import { nanos, millis } from '@nats-io/nats-core'
10
10
  * @param {string[]} spec.filterSubjects - масив subject-ів для фільтрації
11
11
  * @param {string} spec.deliverPolicy - політика доставки
12
12
  * @param {string} spec.ackPolicy - політика підтвердження
13
+ * @param {string} spec.ackWait - час очікування підтвердження в мілісекундах
13
14
  * @returns {Promise<void>}
14
15
  */
15
16
  export async function ensureConsumer(spec) {
@@ -52,7 +53,7 @@ export async function ensureConsumer(spec) {
52
53
  // якщо filter_subjects не відповідають spec, то оновлюємо consumer
53
54
  else if (
54
55
  compareArrays(spec.filterSubjects, filter_subjects) &&
55
- (spec.ackWait === undefined || spec.ackWait === millis(ack_wait ?? 0))
56
+ (spec.ackWait === undefined || Number(spec.ackWait) === millis(ack_wait ?? 0))
56
57
  ) {
57
58
  log.info(`✅ Consumer «${spec.durableName}» - no changes`)
58
59
  } else {
@@ -79,7 +80,7 @@ function prepareSpec(spec) {
79
80
  deliver_policy: spec.deliverPolicy // 'all' - всі непрочитані повідомлення
80
81
  }
81
82
  if (spec.ackWait) {
82
- preparedSpec.ack_wait = nanos(spec.ackWait)
83
+ preparedSpec.ack_wait = nanos(Number(spec.ackWait))
83
84
  }
84
85
  return preparedSpec
85
86
  }
package/src/index.js CHANGED
@@ -1,3 +1,4 @@
1
1
  export { publish } from './publish.js'
2
- export { read, finish } from './worker.js'
2
+ export { read, finish } from './read.js'
3
+ export { readM, finishM, nakM } from './read-m.js'
3
4
  export { getPendingCount } from './pending-count.js'
package/src/nats.js CHANGED
@@ -3,9 +3,6 @@ import { connect } from '@nats-io/transport-node'
3
3
  import { jetstream, jetstreamManager } from '@nats-io/jetstream'
4
4
  import { env } from 'node:process'
5
5
 
6
- // AckPolicy
7
- // export { AckPolicy } from '@nats-io/jetstream'
8
-
9
6
  let nc
10
7
 
11
8
  // Експортуємо JetStream контекст
@@ -13,6 +10,23 @@ export let js
13
10
  // Експортуємо менеджер JetStream
14
11
  export let jsm
15
12
 
13
+ /**
14
+ * Припиняє приймати нові повідомлення для існуючих підписок (дренить підписки).
15
+ * Дає дочитати/доробити те, що вже “в дорозі”/в черзі (залежить від типу підписки й вашого хендлера).
16
+ * Флашить (дочікується відправки) вже ініційовані публікації.
17
+ * Після цього закриває з’єднання.
18
+ * Для CLI-скриптів це потрібно, інакше процес буде "висіти" через відкритий сокет.
19
+ * @returns {Promise<void>}
20
+ */
21
+ export async function closeNats() {
22
+ // якщо фейковий режим або ще не підключались — нічого закривати
23
+ if (!nc) return
24
+
25
+ await nc.drain()
26
+
27
+ return nc.closed()
28
+ }
29
+
16
30
  // якщо задані демо дані, то використовуємо фейковий NATS
17
31
  if (env.NATS_FAKE_DATA) {
18
32
  // Створюємо фейкові об'єкти для js та jsm
@@ -2,6 +2,7 @@ import { jsm } from './nats.js'
2
2
  import { log } from '@nitra/pino'
3
3
  import { env } from 'node:process'
4
4
  import { stream } from './stream.js'
5
+ import { ClosedConnectionError } from '@nats-io/nats-core'
5
6
 
6
7
  /**
7
8
  * Повертає кількість непрочитаних (pending) повідомлень для durable consumer.
@@ -17,7 +18,13 @@ export async function getPendingCount(consumer) {
17
18
  try {
18
19
  const info = await jsm.consumers.info(stream, consumer)
19
20
  return info.num_pending + info.num_ack_pending
20
- } catch {
21
+ } catch (error) {
22
+ // Перевірити що це ClosedConnectionError
23
+ if (error instanceof ClosedConnectionError) {
24
+ log.error('getPendingCount: nats connection already closed')
25
+ return null
26
+ }
27
+
21
28
  log.error(`consumer ${consumer} not found`)
22
29
  return 0
23
30
  }
package/src/read-m.js ADDED
@@ -0,0 +1,99 @@
1
+ import { log } from '@nitra/pino'
2
+ import { env, exit } from 'node:process'
3
+ import { js, closeNats } from './nats.js'
4
+ import { stream } from './stream.js'
5
+
6
+ // Кешуємо результат перевірки фейкових даних
7
+ let FAKE_DATA
8
+ if (env.NATS_FAKE_DATA) {
9
+ try {
10
+ FAKE_DATA = JSON.parse(env.NATS_FAKE_DATA)
11
+ } catch (error) {
12
+ log.error('Failed to parse fake data', { error, data: env.NATS_FAKE_DATA })
13
+ throw new Error(`Invalid JSON in fake data: ${error.message}`)
14
+ }
15
+ }
16
+
17
+ // state variables
18
+ let msgs = []
19
+
20
+ /**
21
+ * Зчитує N повідомлень з JetStream для вказаного consumer-а.
22
+ * @param {string} consumer - durable_name для consumer(за замовчуванням durable_name = subject)
23
+ * @param {number} count - кількість повідомлень, яку треба зчитати
24
+ * @returns {Promise<object[]>} - масив декодованих даних повідомлень
25
+ */
26
+ export async function readM(consumer, count = 1) {
27
+ if (!Number.isFinite(count) || count <= 0) {
28
+ throw new Error(`Invalid count: ${count}. Must be a positive number.`)
29
+ }
30
+
31
+ // скидаємо попередній стан (якщо readM викликають повторно без finishM/nakM)
32
+ msgs = []
33
+
34
+ // якщо задані демо дані, то повертаємо їх
35
+ if (FAKE_DATA) {
36
+ log.info('env.NATS_FAKE_DATA:', env.NATS_FAKE_DATA)
37
+
38
+ if (Array.isArray(FAKE_DATA)) return FAKE_DATA.slice(0, count)
39
+
40
+ // якщо передали не масив — повторюємо один і той самий payload N разів
41
+ return Array.from({ length: count }, () => FAKE_DATA)
42
+ }
43
+
44
+ const consumerObj = await js.consumers.get(stream, consumer)
45
+
46
+ // час очікування (в мс), протягом якого fetch() буде чекати повідомлення перед тим, як завершити ітератор без повідомлень.
47
+ const iter = await consumerObj.fetch({ max_messages: count, expires: 2000 })
48
+
49
+ const decodedMessages = []
50
+ for await (const msg of iter) {
51
+ msgs.push(msg)
52
+
53
+ let decoded
54
+ try {
55
+ decoded = JSON.parse(msg.data)
56
+ } catch (error) {
57
+ log.error('Failed to parse message data', { error, data: msg.data })
58
+ msg.ack() // НЕ повертаємо повідомлення назад в чергу при помилці парсингу, бо зациклиться
59
+ throw new Error(`Invalid JSON in message: ${error.message}`)
60
+ }
61
+
62
+ decodedMessages.push(decoded)
63
+ }
64
+
65
+ if (!decodedMessages.length) {
66
+ log.info(`${consumer} - no msg...`)
67
+ exit(0) // якщо не було жодного повідомлення
68
+ }
69
+
70
+ if (decodedMessages.length < count) {
71
+ log.info(`${consumer} - read ${decodedMessages.length}/${count} msg(s)`)
72
+ }
73
+
74
+ return decodedMessages
75
+ }
76
+
77
+ /**
78
+ * Підтверджує (ack) всі прочитані повідомлення.
79
+ * Якщо не викликати, повідомлення будуть повернуті у чергу (nak) після таймауту.
80
+ * @returns {Promise<void>}
81
+ */
82
+ export const finishM = async () => {
83
+ // oxlint-disable-next-line require-await
84
+ for (const msg of msgs) msg?.ack()
85
+ msgs = []
86
+ return closeNats()
87
+ }
88
+
89
+ /**
90
+ * Повертає всі прочитані повідомлення назад в чергу (nak).
91
+ * @returns {Promise<void>}
92
+ */
93
+ export const nakM = async () => {
94
+ // oxlint-disable-next-line require-await
95
+ for (const msg of msgs) msg?.nak()
96
+ msgs = []
97
+
98
+ return closeNats()
99
+ }
@@ -1,6 +1,6 @@
1
1
  import { log } from '@nitra/pino'
2
2
  import { env, exit } from 'node:process'
3
- import { js } from './nats.js'
3
+ import { js, closeNats } from './nats.js'
4
4
  import { stream } from './stream.js'
5
5
 
6
6
  // Кешуємо результат перевірки фейкових даних
@@ -57,7 +57,8 @@ export async function read(consumer) {
57
57
 
58
58
  const consumerObj = await js.consumers.get(stream, consumer)
59
59
 
60
- const iter = await consumerObj.fetch({ max_messages: 1, expires: 1000 })
60
+ // час очікування мс), протягом якого fetch() буде чекати повідомлення перед тим, як завершити ітератор без повідомлень.
61
+ const iter = await consumerObj.fetch({ max_messages: 1, expires: 2000 })
61
62
  for await (const msgI of iter) {
62
63
  msg = msgI
63
64
 
@@ -80,6 +81,7 @@ export async function read(consumer) {
80
81
  }
81
82
 
82
83
  log.info(`${consumer} - no msg...`)
84
+ await closeNats()
83
85
  exit(0) // якщо не було жодного повідомлення
84
86
  }
85
87
 
@@ -89,8 +91,8 @@ export async function read(consumer) {
89
91
  * @returns {Promise<void>}
90
92
  */
91
93
  export const finish = async () => {
92
- // oxlint-disable-next-line require-await
93
- return msg?.ack()
94
+ await msg?.ack()
95
+ return closeNats()
94
96
  }
95
97
 
96
98
  /**
@@ -98,6 +100,6 @@ export const finish = async () => {
98
100
  * @returns {Promise<void>}
99
101
  */
100
102
  export const nak = async () => {
101
- // oxlint-disable-next-line require-await
102
- return msg?.nak()
103
+ await msg?.nak()
104
+ return closeNats()
103
105
  }