@nitra/nats 3.0.6 → 3.0.8

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
 
@@ -20,6 +20,7 @@ npm install @nitra/nats
20
20
  Пакет використовує такі змінні середовища:
21
21
 
22
22
  - `NATS_SERVER` — адреса сервера NATS (наприклад, `nats://localhost:4222`)
23
+ - `NATS_STREAM` — назва stream (за замовчуванням `dev`)
23
24
 
24
25
  ---
25
26
 
@@ -31,11 +32,9 @@ npm install @nitra/nats
31
32
  projectName:subjectName
32
33
  ```
33
34
 
34
- ---
35
-
36
35
  **Приклади:**
37
36
 
38
- - `myproject:jobs`
37
+ - `myProject:jobs`
39
38
  - `service:notifications`
40
39
 
41
40
  ---
@@ -45,11 +44,18 @@ projectName:subjectName
45
44
  ```js
46
45
  import { publish } from '@nitra/nats'
47
46
 
47
+ // Мінімальний варіант (durable_name = subject)
48
48
  await publish('project:subject', { id: 1, foo: 'bar' })
49
+
50
+ // Кастомний durable consumer (наприклад, для групової обробки)
51
+ await publish('project:subject', { id: 2 }, [
52
+ { durableName: 'worker-group', filterSubjects: ['project:subject', 'project:subject2'] },
53
+ { durableName: 'analytics', filterSubjects: ['project:subject'] }
54
+ ])
49
55
  ```
50
56
 
51
- - Stream і Consumer будуть створені автоматично, якщо не існують.
52
- - Повідомлення публікується у subject `stream.project:subject`.
57
+ - Consumer буде створений автоматично, якщо не існує.
58
+ - Повідомлення публікується у subject `dev.project:subject` (або `${NATS_STREAM}.project:subject`).
53
59
 
54
60
  ---
55
61
 
@@ -58,51 +64,61 @@ await publish('project:subject', { id: 1, foo: 'bar' })
58
64
  ```js
59
65
  import { read, finish } from '@nitra/nats'
60
66
 
67
+ // Читання для стандартного durable consumer (durable_name = subject)
61
68
  const data = await read('project:subject')
62
69
  // ...обробка data...
63
70
  await finish() // підтвердження (ack) повідомлення
71
+
72
+ // Читання для кастомного durable consumer
73
+ const data2 = await read('worker-group')
74
+ await finish()
64
75
  ```
65
76
 
66
77
  - Якщо не викликати `finish()`, повідомлення буде повернуто у чергу (`nak`) при завершенні процесу або помилці.
78
+ - Durable consumer створюється автоматично при першій публікації.
67
79
 
68
80
  ---
69
81
 
70
- ## Кількість непрочитаних повідомлень для subject
82
+ ## Кількість непрочитаних повідомлень для durable consumer
71
83
 
72
84
  ```js
73
85
  import { getPendingCount } from '@nitra/nats'
74
86
 
75
- const count = await getPendingCount('project:subject')
87
+ const count = await getPendingCount('project:subject') // для стандартного durable
76
88
  console.log('pending:', count)
89
+
90
+ const count2 = await getPendingCount('worker-group') // для кастомного durable
91
+ console.log('pending for group:', count2)
77
92
  ```
78
93
 
79
94
  ---
80
95
 
81
96
  ## Як це працює
82
97
 
83
- - **publish(subject, data):**
98
+ - **publish(subject, data, consumers?):**
84
99
 
85
- - Перевіряє/створює stream і consumer для subject (один раз за процес).
86
- - Публікує повідомлення у subject `stream.${subject}`.
100
+ - Перевіряє/створює consumer-ів (один раз за процес).
101
+ - Публікує повідомлення у subject `${stream}.${subject}`.
102
+ - Якщо передати масив consumers — створює кастомні durable consumer-и з кастомними іменами та фільтрами.
87
103
 
88
- - **read(subject):**
104
+ - **read(durableName):**
89
105
 
90
- - Читає одне повідомлення з черги для subject.
106
+ - Читає одне повідомлення з черги для durable consumer (за замовчуванням durable_name = subject).
91
107
 
92
108
  - **finish():**
93
109
 
94
110
  - Підтверджує (ack) повідомлення.
95
111
 
96
- - **getPendingCount(subject):**
97
- - Повертає кількість непрочитаних повідомлень для consumer `durable_${subject}`.
112
+ - **getPendingCount(durableName):**
113
+ - Повертає кількість непрочитаних повідомлень для durable consumer.
98
114
 
99
115
  ---
100
116
 
101
117
  ## Важливо
102
118
 
103
- - STREAM у NATS завжди один (`stream`), але subject динамічний.
104
- - Для кожного subject створюється окремий durable consumer з іменем `durable_${subject}`.
105
- - Пакет розрахований на single-message workflow (одне повідомлення на читання за раз, публікація - необмежена).
119
+ - STREAM у NATS за замовчуванням `dev` (або значення змінної `NATS_STREAM`), але subject і durable consumer-и динамічні.
120
+ - Для кожного subject або кастомного durable створюється окремий consumer з іменем `durableName`.
121
+ - Пакет розрахований на single-message workflow (одне повідомлення на читання за раз, публікація необмежена).
106
122
  - Для паралельної обробки або кастомних сценаріїв — дивись вихідний код та розширюй під свої задачі.
107
123
 
108
124
  ---
@@ -112,15 +128,31 @@ console.log('pending:', count)
112
128
  ```js
113
129
  import { publish, read, finish, getPendingCount } from '@nitra/nats'
114
130
 
115
- // Публікація
116
- await publish('project:subject', { id: 1, prop: 'prop' })
131
+ // Публікація для стандартного durable (durable_name = subject)
132
+ await publish('project:subject', { id: 1 })
133
+
134
+ // Публікація для кількох consumer-ів
135
+ await publish('project:subject', { id: 2 }, [
136
+ { durableName: 'worker-group', filterSubjects: ['project:subject', 'project:subject2'] },
137
+ { durableName: 'analytics', filterSubjects: ['project:subject'] }
138
+ ])
117
139
 
118
- // Pending для subject
140
+ // Pending для стандартного durable
119
141
  const count = await getPendingCount('project:subject')
120
142
  console.log('pending:', count)
121
143
 
122
- // Читання
123
- const data = await read('project:subject')
144
+ // Pending для кастомного durable
145
+ const count2 = await getPendingCount('worker-group')
146
+ console.log('pending for group:', count2)
147
+
148
+ // Читання для стандартного durable
149
+ const { id } = await read('project:subject')
150
+ console.log(`read. id: ${id}`)
151
+ // Підтверджуємо, що повідомлення прочитано (якщо не викликати finish, то повідомлення буде повторно надіслано)
152
+ await finish()
153
+
154
+ // Читання для кастомного durable
155
+ const data = await read('worker-group')
124
156
  console.log('read:', data)
125
157
  await finish()
126
158
  ```
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.8",
5
5
  "type": "module",
6
6
  "files": [
7
7
  "src"
package/src/consumer.js CHANGED
@@ -1,26 +1,48 @@
1
- import { jsm, AckPolicy } from './nats.js'
2
- import { ensureStream } from './stream.js'
1
+ import { jsm, AckPolicy, stream } from './nats.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(stream)?.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(stream, {
37
+ durable_name: c.durableName,
38
+ ack_policy: AckPolicy.Explicit, // якщо не підтвердити повідомлення, воно буде повторно надіслано
39
+ filter_subjects: c.filterSubjects.map(fs => `${stream}.${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,16 @@ 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()
19
+
20
+ // Stream name
21
+ export const stream = process.env.NATS_STREAM || 'dev'
@@ -1,19 +1,16 @@
1
- import { jsm } from './nats.js'
2
- import { checkSubjectFormat } from './utils.js'
1
+ import { jsm, stream } from './nats.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(stream, 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, stream } 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(`${stream}.${subject}`, jc.encode(data))
24
19
  }
package/src/stream.js CHANGED
@@ -1,4 +1,4 @@
1
- import { jsm } from './nats.js'
1
+ import { jsm, stream } from './nats.js'
2
2
 
3
3
  /**
4
4
  * Перевіряє наявність або створює JetStream stream.
@@ -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(stream)
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: stream,
14
+ subjects: [`${stream}.>`],
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
- import { js, jc } from './nats.js'
2
- import { state, checkSubjectFormat } from './utils.js'
1
+ import { js, jc, stream } from './nats.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(stream, 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))