@nitra/nats 3.0.5 → 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 +54 -23
- package/package.json +1 -1
- package/src/consumer.js +40 -18
- package/src/nats.js +3 -3
- package/src/pending-count.js +6 -9
- package/src/publish.js +9 -14
- package/src/stream.js +5 -6
- package/src/worker.js +5 -14
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @nitra/nats
|
|
2
2
|
|
|
3
3
|
NATS JetStream helper для Node.js.
|
|
4
|
-
Простий API для публікації, обробки та моніторингу повідомлень у черзі з автоматичним створенням
|
|
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
|
-
- `
|
|
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
|
-
-
|
|
52
|
-
- Повідомлення публікується у 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
|
-
## Кількість непрочитаних повідомлень для
|
|
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
|
-
- Перевіряє/створює
|
|
86
|
-
- Публікує повідомлення у subject `
|
|
99
|
+
- Перевіряє/створює consumer-ів (один раз за процес).
|
|
100
|
+
- Публікує повідомлення у subject `topic.${subject}`.
|
|
101
|
+
- Якщо передати масив consumers — створює кастомні durable consumer-и з кастомними іменами та фільтрами.
|
|
87
102
|
|
|
88
|
-
- **read(
|
|
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(
|
|
97
|
-
- Повертає кількість непрочитаних повідомлень для consumer
|
|
111
|
+
- **getPendingCount(durableName):**
|
|
112
|
+
- Повертає кількість непрочитаних повідомлень для durable consumer.
|
|
98
113
|
|
|
99
114
|
---
|
|
100
115
|
|
|
101
116
|
## Важливо
|
|
102
117
|
|
|
103
|
-
- STREAM у NATS завжди один (`
|
|
104
|
-
- Для кожного 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
|
|
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 для
|
|
139
|
+
// Pending для стандартного durable
|
|
119
140
|
const count = await getPendingCount('project:subject')
|
|
120
141
|
console.log('pending:', count)
|
|
121
142
|
|
|
122
|
-
//
|
|
123
|
-
const
|
|
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
package/src/consumer.js
CHANGED
|
@@ -1,26 +1,48 @@
|
|
|
1
1
|
import { jsm, AckPolicy } from './nats.js'
|
|
2
|
-
import {
|
|
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
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
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
|
-
|
|
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 =
|
|
15
|
+
export const js = nc.jetstream()
|
|
16
16
|
|
|
17
17
|
// Менеджер JetStream (для створення стрімів, конфігурацій тощо)
|
|
18
|
-
export const jsm = await
|
|
18
|
+
export const jsm = await nc.jetstreamManager()
|
package/src/pending-count.js
CHANGED
|
@@ -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
|
|
6
|
-
* @param {string}
|
|
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(
|
|
10
|
-
checkSubjectFormat(subject)
|
|
11
|
-
|
|
8
|
+
export async function getPendingCount(consumer) {
|
|
12
9
|
try {
|
|
13
|
-
const info = await jsm.consumers.info('
|
|
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(
|
|
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 {
|
|
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
|
-
*
|
|
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
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
18
|
+
return js.publish(`topic.${subject}`, jc.encode(data))
|
|
24
19
|
}
|
package/src/stream.js
CHANGED
|
@@ -2,18 +2,17 @@ import { jsm } from './nats.js'
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Перевіряє наявність або створює JetStream stream.
|
|
5
|
-
* @param {string} subject - subject у форматі project:subject (для валідації)
|
|
6
5
|
* @returns {Promise<void>}
|
|
7
6
|
*/
|
|
8
|
-
export async function ensureStream(
|
|
7
|
+
export async function ensureStream() {
|
|
9
8
|
try {
|
|
10
|
-
await jsm.streams.info('
|
|
9
|
+
await jsm.streams.info('nitra')
|
|
11
10
|
console.debug('✅ Stream already exists')
|
|
12
11
|
} catch {
|
|
13
12
|
await jsm.streams.add({
|
|
14
|
-
name: '
|
|
15
|
-
subjects: ['
|
|
16
|
-
retention: '
|
|
13
|
+
name: 'nitra',
|
|
14
|
+
subjects: ['topic.>'],
|
|
15
|
+
retention: 'interest', // зберігає повідомлення поки є consumers або поки всі повідомлення не будуть прочитані
|
|
17
16
|
replicas: 1, // кількість реплік у кластері TODO: проговорити кількість реплік
|
|
18
17
|
storage: 'file' //'memory' TODO: проговорити які параметри потрібні для кластера
|
|
19
18
|
})
|
package/src/worker.js
CHANGED
|
@@ -1,24 +1,15 @@
|
|
|
1
1
|
import { js, jc } from './nats.js'
|
|
2
2
|
import { state } from './utils.js'
|
|
3
|
-
import { checkSubjectFormat } from './utils.js'
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
|
-
* Зчитує одне повідомлення з JetStream для вказаного
|
|
7
|
-
* @param {string}
|
|
5
|
+
* Зчитує одне повідомлення з JetStream для вказаного consumer-а.
|
|
6
|
+
* @param {string} consumer - durable_name для consumer(за замовчуванням durable_name = subject)
|
|
8
7
|
* @returns {Promise<object>} - декодовані дані повідомлення
|
|
9
8
|
*/
|
|
10
|
-
export async function read(
|
|
11
|
-
|
|
9
|
+
export async function read(consumer) {
|
|
10
|
+
const consumerObj = await js.consumers.get('nitra', consumer)
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
try {
|
|
15
|
-
consumer = await js.consumers.get('stream', `durable_${subject}`)
|
|
16
|
-
} catch (e) {
|
|
17
|
-
console.error('consumer not found for subject:', subject)
|
|
18
|
-
return {}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const iter = await consumer.fetch({ max_messages: 1 })
|
|
12
|
+
const iter = await consumerObj.fetch({ max_messages: 1, expires: 1000 })
|
|
22
13
|
for await (const msg of iter) {
|
|
23
14
|
state.msg = msg
|
|
24
15
|
console.debug('read msg', jc.decode(msg.data))
|