@nitra/nats 3.0.6 → 3.0.7

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
@@ -1,7 +1,7 @@
1
1
  # @nitra/nats
2
2
 
3
3
  NATS JetStream helper для Node.js.
4
- Простий API для публікації, обробки та моніторингу повідомлень у черзі з автоматичним створенням stream/consumer для кожного subject.
4
+ Простий API для публікації, обробки та моніторингу повідомлень у черзі з автоматичним створенням consumer для кожного subject або кастомного durable consumer.
5
5
 
6
6
  ---
7
7
 
@@ -31,11 +31,9 @@ npm install @nitra/nats
31
31
  projectName:subjectName
32
32
  ```
33
33
 
34
- ---
35
-
36
34
  **Приклади:**
37
35
 
38
- - `myproject:jobs`
36
+ - `myProject:jobs`
39
37
  - `service:notifications`
40
38
 
41
39
  ---
@@ -45,11 +43,18 @@ projectName:subjectName
45
43
  ```js
46
44
  import { publish } from '@nitra/nats'
47
45
 
46
+ // Мінімальний варіант (durable_name = subject)
48
47
  await publish('project:subject', { id: 1, foo: 'bar' })
48
+
49
+ // Кастомний durable consumer (наприклад, для групової обробки)
50
+ await publish('project:subject', { id: 2 }, [
51
+ { durableName: 'worker-group', filterSubjects: ['project:subject', 'project:subject2'] },
52
+ { durableName: 'analytics', filterSubjects: ['project:subject'] }
53
+ ])
49
54
  ```
50
55
 
51
- - Stream і Consumer будуть створені автоматично, якщо не існують.
52
- - Повідомлення публікується у subject `stream.project:subject`.
56
+ - Consumer буде створений автоматично, якщо не існує.
57
+ - Повідомлення публікується у subject `topic.project:subject`.
53
58
 
54
59
  ---
55
60
 
@@ -58,51 +63,61 @@ await publish('project:subject', { id: 1, foo: 'bar' })
58
63
  ```js
59
64
  import { read, finish } from '@nitra/nats'
60
65
 
66
+ // Читання для стандартного durable consumer (durable_name = subject)
61
67
  const data = await read('project:subject')
62
68
  // ...обробка data...
63
69
  await finish() // підтвердження (ack) повідомлення
70
+
71
+ // Читання для кастомного durable consumer
72
+ const data2 = await read('worker-group')
73
+ await finish()
64
74
  ```
65
75
 
66
76
  - Якщо не викликати `finish()`, повідомлення буде повернуто у чергу (`nak`) при завершенні процесу або помилці.
77
+ - Durable consumer створюється автоматично при першій публікації.
67
78
 
68
79
  ---
69
80
 
70
- ## Кількість непрочитаних повідомлень для subject
81
+ ## Кількість непрочитаних повідомлень для durable consumer
71
82
 
72
83
  ```js
73
84
  import { getPendingCount } from '@nitra/nats'
74
85
 
75
- const count = await getPendingCount('project:subject')
86
+ const count = await getPendingCount('project:subject') // для стандартного durable
76
87
  console.log('pending:', count)
88
+
89
+ const count2 = await getPendingCount('worker-group') // для кастомного durable
90
+ console.log('pending for group:', count2)
77
91
  ```
78
92
 
79
93
  ---
80
94
 
81
95
  ## Як це працює
82
96
 
83
- - **publish(subject, data):**
97
+ - **publish(subject, data, consumers?):**
84
98
 
85
- - Перевіряє/створює stream і consumer для subject (один раз за процес).
86
- - Публікує повідомлення у subject `stream.${subject}`.
99
+ - Перевіряє/створює consumer-ів (один раз за процес).
100
+ - Публікує повідомлення у subject `topic.${subject}`.
101
+ - Якщо передати масив consumers — створює кастомні durable consumer-и з кастомними іменами та фільтрами.
87
102
 
88
- - **read(subject):**
103
+ - **read(durableName):**
89
104
 
90
- - Читає одне повідомлення з черги для subject.
105
+ - Читає одне повідомлення з черги для durable consumer (за замовчуванням durable_name = subject).
91
106
 
92
107
  - **finish():**
93
108
 
94
109
  - Підтверджує (ack) повідомлення.
95
110
 
96
- - **getPendingCount(subject):**
97
- - Повертає кількість непрочитаних повідомлень для consumer `durable_${subject}`.
111
+ - **getPendingCount(durableName):**
112
+ - Повертає кількість непрочитаних повідомлень для durable consumer.
98
113
 
99
114
  ---
100
115
 
101
116
  ## Важливо
102
117
 
103
- - STREAM у NATS завжди один (`stream`), але subject динамічний.
104
- - Для кожного subject створюється окремий durable consumer з іменем `durable_${subject}`.
105
- - Пакет розрахований на single-message workflow (одне повідомлення на читання за раз, публікація - необмежена).
118
+ - STREAM у NATS завжди один (`nitra`), але subject і durable consumer-и динамічні.
119
+ - Для кожного subject або кастомного durable створюється окремий consumer з іменем `durableName`.
120
+ - Пакет розрахований на single-message workflow (одне повідомлення на читання за раз, публікація необмежена).
106
121
  - Для паралельної обробки або кастомних сценаріїв — дивись вихідний код та розширюй під свої задачі.
107
122
 
108
123
  ---
@@ -112,15 +127,31 @@ console.log('pending:', count)
112
127
  ```js
113
128
  import { publish, read, finish, getPendingCount } from '@nitra/nats'
114
129
 
115
- // Публікація
116
- await publish('project:subject', { id: 1, prop: 'prop' })
130
+ // Публікація для стандартного durable (durable_name = subject)
131
+ await publish('project:subject', { id: 1 })
132
+
133
+ // Публікація для кількох consumer-ів
134
+ await publish('project:subject', { id: 2 }, [
135
+ { durableName: 'worker-group', filterSubjects: ['project:subject', 'project:subject2'] },
136
+ { durableName: 'analytics', filterSubjects: ['project:subject'] }
137
+ ])
117
138
 
118
- // Pending для subject
139
+ // Pending для стандартного durable
119
140
  const count = await getPendingCount('project:subject')
120
141
  console.log('pending:', count)
121
142
 
122
- // Читання
123
- const data = await read('project:subject')
143
+ // Pending для кастомного durable
144
+ const count2 = await getPendingCount('worker-group')
145
+ console.log('pending for group:', count2)
146
+
147
+ // Читання для стандартного durable
148
+ const { id } = await read('project:subject')
149
+ console.log(`read. id: ${id}`)
150
+ // Підтверджуємо, що повідомлення прочитано (якщо не викликати finish, то повідомлення буде повторно надіслано)
151
+ await finish()
152
+
153
+ // Читання для кастомного durable
154
+ const data = await read('worker-group')
124
155
  console.log('read:', data)
125
156
  await finish()
126
157
  ```
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nitra/nats",
3
3
  "description": "nats helper",
4
- "version": "3.0.6",
4
+ "version": "3.0.7",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "src"
package/src/consumer.js CHANGED
@@ -1,26 +1,48 @@
1
1
  import { jsm, AckPolicy } from './nats.js'
2
- import { ensureStream } from './stream.js'
2
+ import { checkSubjectFormat } from './utils.js'
3
+ // import { ensureStream } from './stream.js'
4
+
5
+ // Set для зберігання існуючих consumer
6
+ const consumerReady = new Set()
3
7
 
4
8
  /**
5
- * Перевіряє наявність або створює durable consumer для subject.
6
- * Stream створюється автоматично, якщо не існує.
9
+ * Перевіряє наявність або створює durable consumer(а) для subject(ів).
7
10
  * @param {string} subject - subject у форматі project:subject
11
+ * @param {object[]} consumers - список consumer-ів. [{durableName, filterSubjects}]
8
12
  * @returns {Promise<void>}
9
13
  */
10
- export async function ensureConsumer(subject) {
11
- // створюємо stream якщо не існує
12
- await ensureStream()
13
-
14
- try {
15
- await jsm.consumers.info('stream', `durable_${subject}`)
16
- console.debug('✅ Durable consumer already exists')
17
- } catch {
18
- await jsm.consumers.add('stream', {
19
- durable_name: `durable_${subject}`,
20
- ack_policy: AckPolicy.Explicit, // якщо не підтвердити повідомлення, воно буде повторно надіслано
21
- filter_subject: `stream.${subject}`,
22
- deliver_policy: 'all' // 'all' - всі повідомлення, 'last' - останнє повідомлення
23
- })
24
- console.debug(' Durable consumer created')
14
+ export async function ensureConsumer(subject, consumers = [{ durableName: subject, filterSubjects: [subject] }]) {
15
+ if (consumers.every(c => consumerReady.has(c.durableName))) {
16
+ console.debug(`✅ Consumers: ${consumers.map(c => c.durableName)} already exists`)
17
+ return
18
+ }
19
+
20
+ // перевіряємо чи subject відповідає формату project:subject
21
+ checkSubjectFormat(subject)
22
+
23
+ // // створюємо stream якщо не існує
24
+ // if (!consumerReady.size) {
25
+ // await ensureStream()
26
+ // }
27
+
28
+ const consumerArr = (await jsm.consumers?.list('nitra')?.next()) || []
29
+ for (const c of consumers) {
30
+ const isExists = consumerArr.some(ca => ca.config.durable_name === c.durableName)
31
+
32
+ // якщо не існує consumer-а який слухає subject, то створюємо за замовчуванням (durable_name = subject)
33
+ if (isExists) {
34
+ console.debug(`✅ Consumer «${c.durableName}» already exists`)
35
+ } else {
36
+ await jsm.consumers.add('nitra', {
37
+ durable_name: c.durableName,
38
+ ack_policy: AckPolicy.Explicit, // якщо не підтвердити повідомлення, воно буде повторно надіслано
39
+ filter_subjects: c.filterSubjects.map(fs => `topic.${fs}`), // якщо не вказати, то consumer буде слухати всі повідомлення зі stream
40
+ deliver_policy: 'all' // 'all' - всі непрочитані повідомлення
41
+ })
42
+
43
+ console.debug(`🔥 Consumer «${c.durableName}» created`)
44
+ }
45
+
46
+ consumerReady.add(c.durableName)
25
47
  }
26
48
  }
package/src/nats.js CHANGED
@@ -6,13 +6,13 @@ export { AckPolicy } from 'nats'
6
6
  checkEnv(['NATS_SERVER'])
7
7
 
8
8
  // Connect to NATS
9
- export const client = await connect({ servers: env.NATS_SERVER })
9
+ const nc = await connect({ servers: env.NATS_SERVER })
10
10
 
11
11
  // JSONCodec
12
12
  export const jc = JSONCodec()
13
13
 
14
14
  // Створюємо JetStream контекст
15
- export const js = client.jetstream()
15
+ export const js = nc.jetstream()
16
16
 
17
17
  // Менеджер JetStream (для створення стрімів, конфігурацій тощо)
18
- export const jsm = await client.jetstreamManager()
18
+ export const jsm = await nc.jetstreamManager()
@@ -1,19 +1,16 @@
1
1
  import { jsm } from './nats.js'
2
- import { checkSubjectFormat } from './utils.js'
3
2
 
4
3
  /**
5
- * Повертає кількість непрочитаних (pending) повідомлень для durable consumer, пов’язаного з subject.
6
- * @param {string} subject - subject у форматі project:subject
4
+ * Повертає кількість непрочитаних (pending) повідомлень для durable consumer.
5
+ * @param {string} consumer - durable_name для consumer(за замовчуванням durable_name = subject)
7
6
  * @returns {Promise<number>} - кількість непрочитаних повідомлень
8
7
  */
9
- export async function getPendingCount(subject) {
10
- checkSubjectFormat(subject)
11
-
8
+ export async function getPendingCount(consumer) {
12
9
  try {
13
- const info = await jsm.consumers.info('stream', `durable_${subject}`)
14
- return info.num_pending
10
+ const info = await jsm.consumers.info('nitra', consumer)
11
+ return info.num_pending + info.num_ack_pending
15
12
  } catch {
16
- console.error('consumer not found for subject:', subject)
13
+ console.error(`consumer ${consumer} not found`)
17
14
  return 0
18
15
  }
19
16
  }
package/src/publish.js CHANGED
@@ -1,24 +1,19 @@
1
- import { client, jc } from './nats.js'
1
+ import { jc, js } from './nats.js'
2
2
  import { ensureConsumer } from './consumer.js'
3
- import { checkSubjectFormat } from './utils.js'
4
-
5
- const consumerReady = new Set()
6
3
 
7
4
  /**
8
5
  * Публікує повідомлення у JetStream для вказаного subject.
9
- * Stream і Consumer створюються автоматично, якщо не існують.
6
+ * Consumer створюється автоматично, якщо не існує.
10
7
  * @param {string} subject - subject у форматі project:subject
11
8
  * @param {object} data - дані для публікації
9
+ * @param {object[]} consumers - список consumer-ів які потрібно створити. [{durableName, filterSubject}] (необов'язково)
12
10
  * @returns {Promise<void>}
13
11
  */
14
- export async function publish(subject, data) {
15
- // Ensure stream and consumer if not exists
16
- if (!consumerReady.has(subject)) {
17
- checkSubjectFormat(subject)
18
- await ensureConsumer(subject)
19
- consumerReady.add(subject)
20
- }
21
- console.debug('publish', data)
12
+ export async function publish(subject, data, consumers) {
13
+ // Ensure consumer if not exists
14
+ await ensureConsumer(subject, consumers)
15
+
16
+ console.debug('publish:', data)
22
17
  // Publish message
23
- return client.publish(`stream.${subject}`, jc.encode(data))
18
+ return js.publish(`topic.${subject}`, jc.encode(data))
24
19
  }
package/src/stream.js CHANGED
@@ -6,13 +6,13 @@ import { jsm } from './nats.js'
6
6
  */
7
7
  export async function ensureStream() {
8
8
  try {
9
- await jsm.streams.info('stream')
9
+ await jsm.streams.info('nitra')
10
10
  console.debug('✅ Stream already exists')
11
11
  } catch {
12
12
  await jsm.streams.add({
13
- name: 'stream',
14
- subjects: ['stream.>'],
15
- retention: 'workqueue',
13
+ name: 'nitra',
14
+ subjects: ['topic.>'],
15
+ retention: 'interest', // зберігає повідомлення поки є consumers або поки всі повідомлення не будуть прочитані
16
16
  replicas: 1, // кількість реплік у кластері TODO: проговорити кількість реплік
17
17
  storage: 'file' //'memory' TODO: проговорити які параметри потрібні для кластера
18
18
  })
package/src/worker.js CHANGED
@@ -1,23 +1,15 @@
1
1
  import { js, jc } from './nats.js'
2
- import { state, checkSubjectFormat } from './utils.js'
2
+ import { state } from './utils.js'
3
3
 
4
4
  /**
5
- * Зчитує одне повідомлення з JetStream для вказаного subject.
6
- * @param {string} subject - subject у форматі project:subject
5
+ * Зчитує одне повідомлення з JetStream для вказаного consumer-а.
6
+ * @param {string} consumer - durable_name для consumer(за замовчуванням durable_name = subject)
7
7
  * @returns {Promise<object>} - декодовані дані повідомлення
8
8
  */
9
- export async function read(subject) {
10
- checkSubjectFormat(subject)
9
+ export async function read(consumer) {
10
+ const consumerObj = await js.consumers.get('nitra', consumer)
11
11
 
12
- let consumer
13
- try {
14
- consumer = await js.consumers.get('stream', `durable_${subject}`)
15
- } catch {
16
- console.error('consumer not found for subject:', subject)
17
- return {}
18
- }
19
-
20
- const iter = await consumer.fetch({ max_messages: 1 })
12
+ const iter = await consumerObj.fetch({ max_messages: 1, expires: 1000 })
21
13
  for await (const msg of iter) {
22
14
  state.msg = msg
23
15
  console.debug('read msg', jc.decode(msg.data))