@nitra/nats 4.0.3 → 4.2.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/README.md CHANGED
@@ -150,23 +150,19 @@ console.log('pending for group:', count2)
150
150
  ## Як це працює
151
151
 
152
152
  - **publish(subject, data):**
153
-
154
153
  - Публікує повідомлення у subject `${stream}.${subject}`
155
154
  - Перевіряє формат subject (має бути `project:subject`)
156
155
 
157
156
  - **ensureConsumer(spec):**
158
-
159
157
  - Створює consumer якщо не існує
160
158
  - Оновлює `filter_subjects` якщо вони змінились
161
159
  - Перестворює consumer якщо змінились `deliverPolicy` або `ackPolicy`
162
160
  - Автоматично створює stream якщо потрібно
163
161
 
164
162
  - **read(durableName):**
165
-
166
163
  - Читає одне повідомлення з черги для durable consumer
167
164
 
168
165
  - **finish():**
169
-
170
166
  - Підтверджує (ack) повідомлення
171
167
 
172
168
  - **getPendingCount(durableName):**
@@ -236,7 +232,7 @@ CLI підтримує роботу з YAML конфігураціями consume
236
232
 
237
233
  ```bash
238
234
  # Застосування конфігурації consumer-а з YAML файлу
239
- NODE_ENV=development NATS_URL=nats://localhost:4222 NATS_STREAM=dev node cli.js consumer.yaml
235
+ NATS_URL=nats://localhost:4222 node cli.js consumer.yaml
240
236
 
241
237
  # Через npx після публікації пакету
242
238
  npx @nitra/nats consumer.yaml
package/package.json CHANGED
@@ -1,33 +1,35 @@
1
1
  {
2
2
  "name": "@nitra/nats",
3
+ "version": "4.2.0",
3
4
  "description": "nats helper",
4
- "version": "4.0.3",
5
- "type": "module",
6
- "files": [
7
- "src"
5
+ "keywords": [
6
+ "nats",
7
+ "nitra"
8
8
  ],
9
+ "homepage": "https://github.com/nitra/nats",
10
+ "bugs": "https://github.com/nitra/nats/issues",
11
+ "license": "MIT",
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/nitra/nats.git"
15
+ },
9
16
  "bin": {
10
17
  "nitra-nats": "src/cli.js"
11
18
  },
19
+ "files": [
20
+ "src"
21
+ ],
22
+ "type": "module",
12
23
  "exports": {
13
24
  ".": "./src/index.js"
14
25
  },
15
- "keywords": [
16
- "nitra",
17
- "nats"
18
- ],
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/nitra/nats.git"
22
- },
23
- "bugs": "https://github.com/nitra/nats/issues",
24
- "homepage": "https://github.com/nitra/nats",
25
- "license": "MIT",
26
26
  "dependencies": {
27
+ "@nats-io/jetstream": "^3.3.0",
28
+ "@nats-io/nats-core": "^3.3.0",
29
+ "@nats-io/transport-node": "^3.3.0",
27
30
  "@nitra/check-env": "^4.1.0",
28
- "@nitra/pino": "^2.8.1",
29
- "js-yaml": "^4.1.1",
30
- "nats": "^2.29.3"
31
+ "@nitra/pino": "^2.10.0",
32
+ "js-yaml": "^4.1.1"
31
33
  },
32
34
  "engines": {
33
35
  "node": ">=22.0.0"
package/src/cli.js CHANGED
@@ -39,7 +39,6 @@ function parseConsumerYaml() {
39
39
  /**
40
40
  * Валідує структуру парсеного consumer об'єкта
41
41
  * @param {object} consumer - Об'єкт consumer для валідації
42
- * @returns {boolean} true якщо валідація пройшла успішно
43
42
  * @throws {Error} Якщо валідація не пройшла
44
43
  */
45
44
  export function validateConsumer(consumer) {
@@ -54,7 +53,7 @@ export function validateConsumer(consumer) {
54
53
  if (!Array.isArray(consumer.spec.filterSubjects)) throw new Error('Поле spec.filterSubjects повинно бути масивом')
55
54
 
56
55
  for (const subject of consumer.spec.filterSubjects) {
57
- checkSubjectFormat(subject)
56
+ checkSubjectFormat(consumer.spec.streamName, `${consumer.spec.streamName}.${subject}`)
58
57
  }
59
58
  }
60
59
 
package/src/consumer.js CHANGED
@@ -1,20 +1,27 @@
1
- import { jsm, stream } from './nats.js'
2
- import { ensureStream } from './stream.js'
1
+ import { jsm } from './nats.js'
3
2
  import { log } from '@nitra/pino'
4
3
 
5
4
  /**
6
5
  * Створює або оновлює consumer.
7
- * @param {object{streamName: string, durableName: string, filterSubjects: string[], deliverPolicy: string, ackPolicy: string}} spec - об'єкт consumer-а.
6
+ * @param {object} spec - об'єкт consumer
7
+ * @param {string} spec.streamName - назва stream
8
+ * @param {string} spec.durableName - назва consumer-а
9
+ * @param {string[]} spec.filterSubjects - масив subject-ів для фільтрації
10
+ * @param {string} spec.deliverPolicy - політика доставки
11
+ * @param {string} spec.ackPolicy - політика підтвердження
8
12
  * @returns {Promise<void>}
9
13
  */
10
14
  export async function ensureConsumer(spec) {
11
15
  // створюємо stream якщо не існує
12
- const streamName = spec.streamName || stream
13
- await ensureStream(streamName)
16
+ await ensureStream(spec.streamName)
14
17
 
15
- const consumerArr = (await jsm.consumers?.list(streamName)?.next()) || []
18
+ const consumerArr = (await jsm.consumers?.list(spec.streamName)?.next()) || []
16
19
  const consumer = consumerArr.find(ca => ca.config.durable_name === spec.durableName)
17
20
 
21
+ // Додаємо стрім до subject
22
+ // якщо не вказати, то consumer буде слухати всі повідомлення зі stream
23
+ spec.filterSubjects = spec.filterSubjects.map(fs => `${spec.streamName}.${fs}`)
24
+
18
25
  // якщо не існує consumer-а створюємо його. якщо є перевіримо чи відповідає spec(якщо ні, то перестворюємо або оновлюємо)
19
26
  if (consumer) {
20
27
  log.info(`✅ Consumer «${spec.durableName}» already exists`)
@@ -24,46 +31,96 @@ export async function ensureConsumer(spec) {
24
31
  // якщо deliver_policy або ack_policy не відповідають spec, то перестворюємо consumer
25
32
  if (deliver_policy !== spec.deliverPolicy || ack_policy !== spec.ackPolicy) {
26
33
  const tempSpec = {
34
+ streamName: spec.streamName,
27
35
  durableName: `temp_${spec.durableName}`,
28
- filterSubjects: filter_subjects,
36
+ filterSubjects: spec.filterSubjects,
29
37
  deliverPolicy: deliver_policy,
30
38
  ackPolicy: ack_policy
31
39
  }
32
- // create temp consumer with old filter_subjects
33
- await createConsumer(streamName, tempSpec)
40
+ // Цей “тимчасовий consumer потрібен як страховка на час перестворення,
41
+ // щоб не втратити повідомлення у стрімі з retention: 'interest'
42
+ await createConsumer(tempSpec)
34
43
  // delete old consumer
35
- await jsm.consumers.delete(streamName, spec.durableName)
44
+ await jsm.consumers.delete(spec.streamName, spec.durableName)
36
45
  // create new consumer
37
- await createConsumer(streamName, spec)
46
+ await createConsumer(spec)
38
47
  // delete temp consumer
39
- await jsm.consumers.delete(streamName, tempSpec.durableName)
48
+ await jsm.consumers.delete(tempSpec.streamName, tempSpec.durableName)
40
49
 
41
50
  log.info(`🔥 Consumer «${spec.durableName}» - recreated`)
42
51
  }
43
52
  // якщо filter_subjects не відповідають spec, то оновлюємо consumer
44
- else if (
45
- spec.filterSubjects.some(fs => !filter_subjects.includes(`${streamName}.${fs}`)) ||
46
- filter_subjects.some(fs => !spec.filterSubjects.includes(fs.split('.').pop()))
47
- ) {
48
- await jsm.consumers.update(streamName, spec.durableName, {
49
- filter_subjects: spec.filterSubjects.map(fs => `${streamName}.${fs}`)
53
+ else if (compareArrays(spec.filterSubjects, filter_subjects)) {
54
+ log.info(`✅ Consumer «${spec.durableName}» - no changes`)
55
+ } else {
56
+ await jsm.consumers.update(spec.streamName, spec.durableName, {
57
+ filter_subjects: spec.filterSubjects
50
58
  })
51
59
 
52
- log.info(`🔥 Consumer «${spec.durableName}» - updated`)
53
- } else {
54
- log.info(`✅ Consumer «${spec.durableName}» - no changes`)
60
+ log.info(`🔥 Consumer «${spec.durableName}» filter_subjects - updated`)
55
61
  }
56
62
  } else {
57
- await createConsumer(streamName, spec)
63
+ await createConsumer(spec)
58
64
  log.info(`🔥 Consumer «${spec.durableName}» created`)
59
65
  }
60
66
  }
61
67
 
62
- async function createConsumer(streamName, spec) {
63
- await jsm.consumers.add(streamName, {
68
+ /**
69
+ * Створює consumer для stream
70
+ * @param {object} spec - специфікація consumer-а
71
+ * @param {string} spec.streamName - назва stream
72
+ * @param {string} spec.durableName - назва consumer-а
73
+ * @param {string[]} spec.filterSubjects - масив subject-ів для фільтрації
74
+ * @param {string} spec.deliverPolicy - політика доставки
75
+ * @param {string} spec.ackPolicy - політика підтвердження
76
+ * @returns {Promise<void>}
77
+ */
78
+ async function createConsumer(spec) {
79
+ await jsm.consumers.add(spec.streamName, {
64
80
  durable_name: spec.durableName,
65
81
  ack_policy: spec.ackPolicy, // якщо не підтвердити повідомлення, воно буде повторно надіслано
66
- filter_subjects: spec.filterSubjects.map(fs => `${streamName}.${fs}`), // якщо не вказати, то consumer буде слухати всі повідомлення зі stream
82
+ filter_subjects: spec.filterSubjects,
67
83
  deliver_policy: spec.deliverPolicy // 'all' - всі непрочитані повідомлення
68
84
  })
69
85
  }
86
+
87
+ /**
88
+ * Перевіряє наявність або створює JetStream stream.
89
+ * @param {string} streamName - назва stream
90
+ * @returns {Promise<void>}
91
+ */
92
+ async function ensureStream(streamName) {
93
+ try {
94
+ const info = await jsm.streams.info(streamName)
95
+ // Перевіряємо, чи stream підтримує rollup
96
+
97
+ if (!info.config.allow_rollup_hdrs) {
98
+ log.warn(
99
+ `⚠️ Stream «${streamName}» не має увімкненого allow_rollup_hdrs. Для використання Nats-Rollup заголовка потрібно видалити stream і створити його заново з allow_rollup_hdrs: true`
100
+ )
101
+ }
102
+ log.info('✅ Stream already exists')
103
+ } catch {
104
+ await jsm.streams.add({
105
+ name: streamName,
106
+ subjects: [`${streamName}.>`],
107
+ retention: 'interest', // зберігає повідомлення поки є consumers або поки всі повідомлення не будуть прочитані
108
+ num_replicas: 1, // означає, що stream не реплікується — підходить для локальної розробки. Для продакшену в кластері зазвичай використовують 3 або 5.
109
+ storage: 'file', // 'memory'
110
+ allow_rollup_hdrs: true // дозволяє використання заголовка Nats-Rollup для автоматичного видалення старих повідомлень
111
+ })
112
+ log.info('✅ Stream created')
113
+ }
114
+ }
115
+
116
+ /**
117
+ * Порівнює два масиви
118
+ * @param {string[]} a - перший масив
119
+ * @param {string[]} b - другий масив
120
+ * @returns {boolean} - чи масиви рівні, true - рівні, false - не рівні
121
+ */
122
+ const compareArrays = (a, b) => {
123
+ if (a.length !== b.length) return false
124
+ const setB = new Set(b)
125
+ return a.every(v => setB.has(v))
126
+ }
package/src/index.js CHANGED
@@ -1,4 +1,3 @@
1
1
  export { publish } from './publish.js'
2
- export { read } from './worker.js'
3
- export { finish } from './finish.js'
2
+ export { read, finish } from './worker.js'
4
3
  export { getPendingCount } from './pending-count.js'
package/src/nats.js CHANGED
@@ -1,35 +1,74 @@
1
1
  import { checkEnv } from '@nitra/check-env'
2
- import { connect, JSONCodec } from 'nats'
2
+ import { connect } from '@nats-io/transport-node'
3
+ import { jetstream, jetstreamManager } from '@nats-io/jetstream'
3
4
  import { env } from 'node:process'
4
5
 
5
6
  // AckPolicy
6
- // export { AckPolicy } from 'nats'
7
+ // export { AckPolicy } from '@nats-io/jetstream'
7
8
 
8
9
  let nc
9
10
 
11
+ // Експортуємо JetStream контекст
12
+ export let js
13
+ // Експортуємо менеджер JetStream
14
+ export let jsm
15
+
10
16
  // якщо задані демо дані, то використовуємо фейковий NATS
11
17
  if (env.NATS_FAKE_DATA) {
12
- nc = {
13
- jetstream: () => {},
14
- jetstreamManager: async () => {
15
- {}
18
+ // Створюємо фейкові об'єкти для js та jsm
19
+ js = {
20
+ publish: () => Promise.resolve(),
21
+ consumers: {
22
+ get: () => {
23
+ return {
24
+ fetch: () => {
25
+ return {
26
+ [Symbol.asyncIterator]: async function* () {
27
+ yield
28
+ }
29
+ }
30
+ }
31
+ }
32
+ }
33
+ }
34
+ }
35
+ jsm = {
36
+ streams: {
37
+ info: () => {
38
+ return {
39
+ config: {
40
+ allow_rollup_hdrs: true
41
+ }
42
+ }
43
+ },
44
+ add: () => Promise.resolve()
45
+ },
46
+ consumers: {
47
+ info: () => {
48
+ return {
49
+ num_pending: 0,
50
+ num_ack_pending: 0
51
+ }
52
+ },
53
+ list: () => {
54
+ return {
55
+ next: () => []
56
+ }
57
+ },
58
+ delete: () => Promise.resolve(),
59
+ update: () => Promise.resolve(),
60
+ add: () => Promise.resolve()
16
61
  }
17
62
  }
18
63
  } else {
19
- checkEnv(['NATS_URL', 'NATS_STREAM'])
64
+ checkEnv(['NATS_URL'])
20
65
 
21
66
  // Connect to NATS
22
67
  nc = await connect({ servers: env.NATS_URL })
23
- }
24
68
 
25
- // JSONCodec
26
- export const jc = JSONCodec()
69
+ // Створюємо JetStream контекст
70
+ js = jetstream(nc)
27
71
 
28
- // Створюємо JetStream контекст
29
- export const js = nc.jetstream()
30
-
31
- // Менеджер JetStream (для створення стрімів, конфігурацій тощо)
32
- export const jsm = await nc.jetstreamManager()
33
-
34
- // Stream name
35
- export const stream = env.NATS_STREAM
72
+ // Менеджер JetStream (для створення стрімів, конфігурацій тощо)
73
+ jsm = await jetstreamManager(nc)
74
+ }
@@ -1,6 +1,7 @@
1
- import { jsm, stream } from './nats.js'
1
+ import { jsm } from './nats.js'
2
2
  import { log } from '@nitra/pino'
3
3
  import { env } from 'node:process'
4
+ import { stream } from './stream.js'
4
5
 
5
6
  /**
6
7
  * Повертає кількість непрочитаних (pending) повідомлень для durable consumer.
package/src/publish.js CHANGED
@@ -1,25 +1,57 @@
1
1
  import { log } from '@nitra/pino'
2
2
  import { env } from 'node:process'
3
- import { jc, js, stream } from './nats.js'
3
+ import { js } from './nats.js'
4
4
  import { checkSubjectFormat } from './utils.js'
5
+ import { headers } from '@nats-io/nats-core'
6
+ import { createHash } from 'node:crypto'
7
+ import { stream } from './stream.js'
5
8
 
6
9
  /**
7
10
  * Публікує повідомлення у JetStream для вказаного subject.
8
11
  * @param {string} subject - subject у форматі project:subject
9
12
  * @param {object} data - дані для публікації
13
+ * @param {object} options - опції публікації
10
14
  * @returns {Promise<void>}
11
15
  */
12
16
  // oxlint-disable-next-line require-await
13
- export async function publish(subject, data) {
17
+ export async function publish(subject, data, options = {}) {
14
18
  // якщо задані демо дані, то ігноримо запис
15
19
  if (env.NATS_FAKE_DATA) {
16
20
  return
17
21
  }
18
22
 
19
- // перевіряємо чи subject відповідає формату project:subject
20
- checkSubjectFormat(subject)
23
+ // перевіряємо чи subject відповідає формату stream.project:subject
24
+ checkSubjectFormat(stream, `${stream}.${subject}`)
21
25
 
22
26
  log.debug('publish:', data)
27
+ const payload = JSON.stringify(data)
28
+ let suffix = ''
29
+
30
+ // якщо не задано опції, то використовуємо default опції
31
+ if (Object.keys(options).length === 0) {
32
+ // headers always have their names turned into a canonical mime header key
33
+ // header names can be any printable ASCII character with the exception of `:`.
34
+ // header values can be any ASCII character except `\r` or `\n`.
35
+ // see https://www.ietf.org/rfc/rfc822.txt
36
+ const h = headers()
37
+ h.set('Nats-Rollup', 'sub')
38
+ options.headers = h
39
+
40
+ suffix = `.${hash(payload)}`
41
+ }
42
+ // log.debug('options:', options)
43
+ // log.debug('suffix:', suffix)
44
+
23
45
  // Publish message
24
- return js.publish(`${stream}.${subject}`, jc.encode(data))
46
+ return js.publish(`${stream}.${subject}${suffix}`, payload, options || {})
47
+ }
48
+
49
+ /**
50
+ * Створює MD5 хеш з тексту та повертає його в форматі base64url.
51
+ * Використовується для дедуплікації повідомлень через суфікс у subject.
52
+ * @param {string} text - текст для хешування
53
+ * @returns {string} - хеш у форматі base64url
54
+ */
55
+ function hash(text) {
56
+ return createHash('md5').update(text, 'utf8').digest('base64url')
25
57
  }
package/src/stream.js CHANGED
@@ -1,23 +1,8 @@
1
- import { log } from '@nitra/pino'
2
- import { jsm, stream } from './nats.js'
1
+ import { checkEnv } from '@nitra/check-env'
2
+ import { env } from 'node:process'
3
3
 
4
- /**
5
- * Перевіряє наявність або створює JetStream stream.
6
- * @param streamName
7
- * @returns {Promise<void>}
8
- */
9
- export async function ensureStream(streamName = stream) {
10
- try {
11
- await jsm.streams.info(streamName)
12
- log.debug('✅ Stream already exists')
13
- } catch {
14
- await jsm.streams.add({
15
- name: streamName,
16
- subjects: [`${streamName}.>`],
17
- retention: 'interest', // зберігає повідомлення поки є consumers або поки всі повідомлення не будуть прочитані
18
- replicas: 1, // кількість реплік у кластері TODO: проговорити кількість реплік
19
- storage: 'file' //'memory' TODO: проговорити які параметри потрібні для кластера
20
- })
21
- log.debug('✅ Stream created')
22
- }
4
+ // якщо не задані демо дані, то перевіряємо чи задана назва stream
5
+ if (!env.NATS_FAKE_DATA) {
6
+ checkEnv(['NATS_STREAM'])
23
7
  }
8
+ export const stream = env.NATS_STREAM
package/src/utils.js CHANGED
@@ -1,15 +1,22 @@
1
1
  /**
2
- * Перевіряє чи subject має правильний формат: projectName:subjectName
2
+ * Перевіряє чи subject має правильний формат: stream.project.subject
3
+ * @param {string} stream - назва stream
3
4
  * @param {string} subject - subject для перевірки
4
5
  * @throws {Error} - якщо subject не відповідає формату
5
6
  */
6
- export const checkSubjectFormat = subject => {
7
- const arr = subject?.split(':')
7
+ export const checkSubjectFormat = (stream, subject) => {
8
+ const arr = subject.split('.')
8
9
 
9
- if (arr?.length !== 2 || !arr[0] || !arr[1]) {
10
- throw new Error('subject must be in the format: projectName:subjectName')
10
+ if (arr?.length < 2 || !arr[0] || !arr[1]) {
11
+ throw new Error('subject must be in the format: stream.project:subject')
12
+ }
13
+
14
+ const theme = arr[1].split(':')
15
+ if (theme?.length < 2) {
16
+ throw new Error('subject must be in the format: stream.project:subject')
11
17
  }
12
- }
13
18
 
14
- // state variables
15
- export const state = { msg: null, isFinished: false }
19
+ if (arr[0] !== stream) {
20
+ throw new Error(`${arr[0]} - stream must be - ${stream}`)
21
+ }
22
+ }
package/src/worker.js CHANGED
@@ -1,7 +1,47 @@
1
1
  import { log } from '@nitra/pino'
2
2
  import { env, exit } from 'node:process'
3
- import { jc, js, stream } from './nats.js'
4
- import { state } from './utils.js'
3
+ import { js } 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
+ // Флаг для відстеження чи вже зареєстровані обробники подій
18
+ // let handlersRegistered = false
19
+
20
+ /**
21
+ * Обробник завершення процесу - відправляє повідомлення назад в чергу якщо воно не було підтверджене
22
+ */
23
+ // const handleExit = () => {
24
+ // if (!state.isFinished && state.msg) {
25
+ // state.msg.nak()
26
+ // }
27
+ // }
28
+
29
+ /**
30
+ * Реєструє обробники подій для автоматичного NAK при завершенні процесу
31
+ */
32
+ // function registerExitHandlers() {
33
+ // if (handlersRegistered) return
34
+ // handlersRegistered = true
35
+
36
+ // // Реєструємо хук на завершення процесу. якщо не підтвердили повідомлення або помилка - відправляємо повідомлення назад в чергу
37
+ // for (const signal of ['exit', 'SIGINT', 'uncaughtException', 'unhandledRejection']) {
38
+ // process.once(signal, handleExit)
39
+ // }
40
+ // }
41
+
42
+ // state variables
43
+ // const state = { msg: null, isFinished: false }
44
+ let msg = null
5
45
 
6
46
  /**
7
47
  * Зчитує одне повідомлення з JetStream для вказаного consumer-а.
@@ -10,26 +50,54 @@ import { state } from './utils.js'
10
50
  */
11
51
  export async function read(consumer) {
12
52
  // якщо задані демо дані, то повертаємо їх
13
- if (env.NATS_FAKE_DATA) {
14
- console.log('env.NATS_FAKE_DATA:', env.NATS_FAKE_DATA)
15
- return JSON.parse(env.NATS_FAKE_DATA)
53
+ if (FAKE_DATA) {
54
+ log.info('env.NATS_FAKE_DATA:', env.NATS_FAKE_DATA)
55
+ return FAKE_DATA
16
56
  }
17
57
 
18
58
  const consumerObj = await js.consumers.get(stream, consumer)
19
59
 
20
60
  const iter = await consumerObj.fetch({ max_messages: 1, expires: 1000 })
21
- for await (const msg of iter) {
22
- state.msg = msg
23
- log.debug('read msg', jc.decode(msg.data))
61
+ for await (const msgI of iter) {
62
+ msg = msgI
24
63
 
25
- // Реєструємо хук на завершення процесу. якщо не підтвердили повідомлення або помилка - відправляємо повідомлення назад в чергу
26
- for (const signal of ['exit', 'SIGINT', 'uncaughtException', 'unhandledRejection']) {
27
- process.once(signal, () => !state.isFinished && state.msg && state.msg.nak())
64
+ let decoded
65
+ try {
66
+ decoded = JSON.parse(msg.data)
67
+ } catch (error) {
68
+ log.error('Failed to parse message data', { error, data: msg.data })
69
+ msg.ack() // НЕ повертаємо повідомлення назад в чергу при помилці парсингу, бо зациклиться
70
+ throw new Error(`Invalid JSON in message: ${error.message}`)
28
71
  }
29
72
 
30
- return jc.decode(msg.data)
73
+ log.debug('read msg', decoded)
74
+
75
+ // Реєструємо обробники подій один раз
76
+ // registerExitHandlers()
77
+
78
+ // Оброблюємо тільки перше повідомлення
79
+ return decoded
31
80
  }
32
81
 
33
82
  log.info(`${consumer} - no msg...`)
34
83
  exit(0) // якщо не було жодного повідомлення
35
84
  }
85
+
86
+ /**
87
+ * Підтверджує (ack) останнє прочитане повідомлення.
88
+ * Якщо не викликати, повідомлення буде повернуто у чергу (nak).
89
+ * @returns {Promise<void>}
90
+ */
91
+ export const finish = async () => {
92
+ // oxlint-disable-next-line require-await
93
+ return msg?.ack()
94
+ }
95
+
96
+ /**
97
+ * Повертає повідомлення назад в чергу (nak).
98
+ * @returns {Promise<void>}
99
+ */
100
+ export const nak = async () => {
101
+ // oxlint-disable-next-line require-await
102
+ return msg?.nak()
103
+ }
package/src/finish.js DELETED
@@ -1,13 +0,0 @@
1
- import { state } from './utils.js'
2
-
3
- /**
4
- * Підтверджує (ack) останнє прочитане повідомлення.
5
- * Якщо не викликати, повідомлення буде повернуто у чергу (nak).
6
- * @returns {Promise<void>}
7
- */
8
- export const finish = async () => {
9
- if (state.msg) {
10
- await state.msg.ack()
11
- state.isFinished = true
12
- }
13
- }