@nitra/nats 3.1.3 → 4.0.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 +154 -44
- package/package.json +7 -3
- package/src/cli.js +70 -0
- package/src/consumer.js +55 -35
- package/src/nats.js +19 -6
- package/src/pending-count.js +6 -0
- package/src/publish.js +10 -6
- package/src/stream.js +4 -4
- package/src/worker.js +7 -0
package/README.md
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# @nitra/nats
|
|
2
2
|
|
|
3
3
|
NATS JetStream helper для Node.js.
|
|
4
|
-
Простий API для публікації, обробки та моніторингу повідомлень у черзі з
|
|
4
|
+
Простий API для публікації, обробки та моніторингу повідомлень у черзі з гнучким управлінням consumer-ами через конфігурацію та CLI інструменти.
|
|
5
5
|
|
|
6
6
|
---
|
|
7
7
|
|
|
8
8
|
## Встановлення
|
|
9
9
|
|
|
10
10
|
```sh
|
|
11
|
-
|
|
11
|
+
bun add @nitra/nats
|
|
12
12
|
# або
|
|
13
13
|
npm install @nitra/nats
|
|
14
14
|
```
|
|
@@ -44,18 +44,72 @@ projectName:subjectName
|
|
|
44
44
|
```js
|
|
45
45
|
import { publish } from '@nitra/nats'
|
|
46
46
|
|
|
47
|
-
//
|
|
47
|
+
// Публікація повідомлення
|
|
48
48
|
await publish('project:subject', { id: 1, foo: 'bar' })
|
|
49
|
+
await publish('service:notifications', { message: 'Hello!' })
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
- Повідомлення публікується у subject `dev.project:subject` (або `${NATS_STREAM}.project:subject`)
|
|
53
|
+
- Consumer-и потрібно створювати окремо через `ensureConsumer` або CLI
|
|
54
|
+
|
|
55
|
+
---
|
|
56
|
+
|
|
57
|
+
## Управління Consumer-ами
|
|
58
|
+
|
|
59
|
+
### Програмне створення consumer-а
|
|
60
|
+
|
|
61
|
+
```js
|
|
62
|
+
import { ensureConsumer } from '@nitra/nats'
|
|
63
|
+
|
|
64
|
+
// Створення простого consumer-а
|
|
65
|
+
await ensureConsumer({
|
|
66
|
+
streamName: 'dev',
|
|
67
|
+
durableName: 'project:subject',
|
|
68
|
+
filterSubjects: ['project:subject'],
|
|
69
|
+
deliverPolicy: 'all',
|
|
70
|
+
ackPolicy: 'explicit'
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
// Створення групового consumer-а для кількох subject-ів
|
|
74
|
+
await ensureConsumer({
|
|
75
|
+
streamName: 'dev',
|
|
76
|
+
durableName: 'worker-group',
|
|
77
|
+
filterSubjects: ['project:orders', 'project:payments'],
|
|
78
|
+
deliverPolicy: 'all',
|
|
79
|
+
ackPolicy: 'explicit'
|
|
80
|
+
})
|
|
81
|
+
```
|
|
49
82
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
83
|
+
### CLI для роботи з YAML конфігураціями
|
|
84
|
+
|
|
85
|
+
Створіть YAML файл з конфігурацією consumer-а:
|
|
86
|
+
|
|
87
|
+
```yaml
|
|
88
|
+
# consumer.yaml
|
|
89
|
+
apiVersion: jetstream.nats.io/v1beta2
|
|
90
|
+
kind: Consumer
|
|
91
|
+
metadata:
|
|
92
|
+
name: 'nats:test'
|
|
93
|
+
namespace: dev
|
|
94
|
+
spec:
|
|
95
|
+
streamName: dev
|
|
96
|
+
durableName: 'nats:test'
|
|
97
|
+
filterSubjects:
|
|
98
|
+
- 'nats:subject'
|
|
99
|
+
- 'nats:subject2'
|
|
100
|
+
deliverPolicy: all
|
|
101
|
+
ackPolicy: explicit
|
|
55
102
|
```
|
|
56
103
|
|
|
57
|
-
|
|
58
|
-
|
|
104
|
+
Застосуйте конфігурацію:
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
# З змінними середовища
|
|
108
|
+
NODE_ENV=development NATS_URL=nats://localhost:4222 NATS_STREAM=dev node cli.js consumer.yaml
|
|
109
|
+
|
|
110
|
+
# Або через npx після публікації
|
|
111
|
+
npx @nitra/nats consumer.yaml
|
|
112
|
+
```
|
|
59
113
|
|
|
60
114
|
---
|
|
61
115
|
|
|
@@ -95,68 +149,124 @@ console.log('pending for group:', count2)
|
|
|
95
149
|
|
|
96
150
|
## Як це працює
|
|
97
151
|
|
|
98
|
-
- **publish(subject, data
|
|
152
|
+
- **publish(subject, data):**
|
|
99
153
|
|
|
100
|
-
-
|
|
101
|
-
-
|
|
102
|
-
|
|
154
|
+
- Публікує повідомлення у subject `${stream}.${subject}`
|
|
155
|
+
- Перевіряє формат subject (має бути `project:subject`)
|
|
156
|
+
|
|
157
|
+
- **ensureConsumer(spec):**
|
|
158
|
+
|
|
159
|
+
- Створює consumer якщо не існує
|
|
160
|
+
- Оновлює `filter_subjects` якщо вони змінились
|
|
161
|
+
- Перестворює consumer якщо змінились `deliverPolicy` або `ackPolicy`
|
|
162
|
+
- Автоматично створює stream якщо потрібно
|
|
103
163
|
|
|
104
164
|
- **read(durableName):**
|
|
105
165
|
|
|
106
|
-
- Читає одне повідомлення з черги для durable consumer
|
|
166
|
+
- Читає одне повідомлення з черги для durable consumer
|
|
107
167
|
|
|
108
168
|
- **finish():**
|
|
109
169
|
|
|
110
|
-
- Підтверджує (ack)
|
|
170
|
+
- Підтверджує (ack) повідомлення
|
|
111
171
|
|
|
112
172
|
- **getPendingCount(durableName):**
|
|
113
|
-
- Повертає кількість непрочитаних повідомлень для durable consumer
|
|
173
|
+
- Повертає кількість непрочитаних повідомлень для durable consumer
|
|
114
174
|
|
|
115
175
|
---
|
|
116
176
|
|
|
117
177
|
## Важливо
|
|
118
178
|
|
|
119
|
-
- STREAM у NATS за замовчуванням `dev` (або значення змінної `NATS_STREAM`)
|
|
120
|
-
-
|
|
121
|
-
-
|
|
122
|
-
-
|
|
179
|
+
- STREAM у NATS за замовчуванням `dev` (або значення змінної `NATS_STREAM`)
|
|
180
|
+
- Consumer-и потрібно створювати **явно** через `ensureConsumer` або CLI
|
|
181
|
+
- Subject має відповідати формату `project:subject`
|
|
182
|
+
- Пакет розрахований на single-message workflow (одне повідомлення на читання за раз)
|
|
183
|
+
- `ensureConsumer` розумно оновлює конфігурацію без втрати повідомлень
|
|
184
|
+
- CLI підтримує YAML конфігурації для декларативного управління consumer-ами
|
|
123
185
|
|
|
124
186
|
---
|
|
125
187
|
|
|
126
188
|
## Приклад повного workflow
|
|
127
189
|
|
|
128
190
|
```js
|
|
129
|
-
import { publish, read, finish, getPendingCount } from '@nitra/nats'
|
|
191
|
+
import { publish, ensureConsumer, read, finish, getPendingCount } from '@nitra/nats'
|
|
192
|
+
|
|
193
|
+
// 1. Створення consumer-ів
|
|
194
|
+
await ensureConsumer({
|
|
195
|
+
streamName: 'dev',
|
|
196
|
+
durableName: 'project:subject',
|
|
197
|
+
filterSubjects: ['project:subject'],
|
|
198
|
+
deliverPolicy: 'all',
|
|
199
|
+
ackPolicy: 'explicit'
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
await ensureConsumer({
|
|
203
|
+
streamName: 'dev',
|
|
204
|
+
durableName: 'worker-group',
|
|
205
|
+
filterSubjects: ['project:subject', 'project:orders'],
|
|
206
|
+
deliverPolicy: 'all',
|
|
207
|
+
ackPolicy: 'explicit'
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
// 2. Публікація повідомлень
|
|
211
|
+
await publish('project:subject', { id: 1, action: 'create' })
|
|
212
|
+
await publish('project:orders', { orderId: 123, amount: 100 })
|
|
213
|
+
|
|
214
|
+
// 3. Перевірка pending повідомлень
|
|
215
|
+
const count1 = await getPendingCount('project:subject')
|
|
216
|
+
const count2 = await getPendingCount('worker-group')
|
|
217
|
+
console.log(`pending: ${count1}, worker-group: ${count2}`)
|
|
130
218
|
|
|
131
|
-
//
|
|
132
|
-
await
|
|
219
|
+
// 4. Обробка повідомлень
|
|
220
|
+
const data1 = await read('project:subject')
|
|
221
|
+
console.log('received:', data1)
|
|
222
|
+
await finish()
|
|
133
223
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
])
|
|
224
|
+
const data2 = await read('worker-group')
|
|
225
|
+
console.log('group received:', data2)
|
|
226
|
+
await finish()
|
|
227
|
+
```
|
|
139
228
|
|
|
140
|
-
|
|
141
|
-
const count = await getPendingCount('project:subject')
|
|
142
|
-
console.log('pending:', count)
|
|
229
|
+
---
|
|
143
230
|
|
|
144
|
-
|
|
145
|
-
const count2 = await getPendingCount('worker-group')
|
|
146
|
-
console.log('pending for group:', count2)
|
|
231
|
+
## CLI Інструмент
|
|
147
232
|
|
|
148
|
-
|
|
149
|
-
const { id } = await read('project:subject')
|
|
150
|
-
console.log(`read. id: ${id}`)
|
|
151
|
-
// Підтверджуємо, що повідомлення прочитано (якщо не викликати finish, то повідомлення буде повторно надіслано)
|
|
152
|
-
await finish()
|
|
233
|
+
CLI підтримує роботу з YAML конфігураціями consumer-ів у форматі JetStream Consumer API.
|
|
153
234
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
235
|
+
### Використання
|
|
236
|
+
|
|
237
|
+
```bash
|
|
238
|
+
# Застосування конфігурації consumer-а з YAML файлу
|
|
239
|
+
NODE_ENV=development NATS_URL=nats://localhost:4222 NATS_STREAM=dev node cli.js consumer.yaml
|
|
240
|
+
|
|
241
|
+
# Через npx після публікації пакету
|
|
242
|
+
npx @nitra/nats consumer.yaml
|
|
158
243
|
```
|
|
159
244
|
|
|
245
|
+
### Формат YAML конфігурації
|
|
246
|
+
|
|
247
|
+
```yaml
|
|
248
|
+
apiVersion: jetstream.nats.io/v1beta2
|
|
249
|
+
kind: Consumer
|
|
250
|
+
metadata:
|
|
251
|
+
name: my-consumer
|
|
252
|
+
namespace: dev
|
|
253
|
+
spec:
|
|
254
|
+
streamName: dev
|
|
255
|
+
durableName: my-consumer
|
|
256
|
+
filterSubjects:
|
|
257
|
+
- project:orders
|
|
258
|
+
- project:payments
|
|
259
|
+
deliverPolicy: all
|
|
260
|
+
ackPolicy: explicit
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
CLI автоматично:
|
|
264
|
+
|
|
265
|
+
- Створить consumer якщо не існує
|
|
266
|
+
- Оновить filter_subjects якщо вони змінились
|
|
267
|
+
- Перестворить consumer якщо змінились deliverPolicy або ackPolicy
|
|
268
|
+
- Створить stream якщо потрібно
|
|
269
|
+
|
|
160
270
|
---
|
|
161
271
|
|
|
162
272
|
## Ліцензія
|
package/package.json
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nitra/nats",
|
|
3
3
|
"description": "nats helper",
|
|
4
|
-
"version": "
|
|
4
|
+
"version": "4.0.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"files": [
|
|
7
7
|
"src"
|
|
8
8
|
],
|
|
9
|
+
"bin": {
|
|
10
|
+
"nitra-nats": "./src/cli.js"
|
|
11
|
+
},
|
|
9
12
|
"exports": {
|
|
10
13
|
".": "./src/index.js"
|
|
11
14
|
},
|
|
@@ -21,8 +24,9 @@
|
|
|
21
24
|
"homepage": "https://github.com/nitra/nats",
|
|
22
25
|
"license": "MIT",
|
|
23
26
|
"dependencies": {
|
|
24
|
-
"@nitra/check-env": "^4.
|
|
25
|
-
"@nitra/pino": "^2.
|
|
27
|
+
"@nitra/check-env": "^4.1.0",
|
|
28
|
+
"@nitra/pino": "^2.7.4",
|
|
29
|
+
"js-yaml": "^4.1.1",
|
|
26
30
|
"nats": "^2.29.3"
|
|
27
31
|
},
|
|
28
32
|
"engines": {
|
package/src/cli.js
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import fs from 'fs'
|
|
3
|
+
import path from 'path'
|
|
4
|
+
import { exit } from 'node:process'
|
|
5
|
+
import yaml from 'js-yaml'
|
|
6
|
+
import { ensureConsumer } from './consumer.js'
|
|
7
|
+
import { checkSubjectFormat } from './utils.js'
|
|
8
|
+
|
|
9
|
+
function parseConsumerYaml() {
|
|
10
|
+
try {
|
|
11
|
+
const filePath = path.resolve(process.cwd(), process.argv[2])
|
|
12
|
+
const fileContents = fs.readFileSync(filePath, 'utf8')
|
|
13
|
+
const data = yaml.load(fileContents)
|
|
14
|
+
|
|
15
|
+
return {
|
|
16
|
+
apiVersion: data.apiVersion,
|
|
17
|
+
kind: data.kind,
|
|
18
|
+
metadata: {
|
|
19
|
+
name: data.metadata?.name,
|
|
20
|
+
namespace: data.metadata?.namespace
|
|
21
|
+
},
|
|
22
|
+
spec: {
|
|
23
|
+
streamName: data.spec?.streamName,
|
|
24
|
+
durableName: data.spec?.durableName,
|
|
25
|
+
filterSubjects: data.spec?.filterSubjects,
|
|
26
|
+
deliverPolicy: data.spec?.deliverPolicy,
|
|
27
|
+
ackPolicy: data.spec?.ackPolicy
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
} catch (error) {
|
|
31
|
+
throw new Error(`Помилка парсингу YAML файлу: ${error.message}`)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Валідує структуру парсеного consumer об'єкта
|
|
37
|
+
* @param {Object} consumer - Об'єкт consumer для валідації
|
|
38
|
+
* @returns {boolean} true якщо валідація пройшла успішно
|
|
39
|
+
* @throws {Error} Якщо валідація не пройшла
|
|
40
|
+
*/
|
|
41
|
+
export function validateConsumer(consumer) {
|
|
42
|
+
if (!consumer.apiVersion) throw new Error('Відсутнє поле apiVersion')
|
|
43
|
+
if (!consumer.kind) throw new Error('Відсутнє поле kind')
|
|
44
|
+
if (!consumer.metadata?.name) throw new Error('Відсутнє поле metadata.name')
|
|
45
|
+
|
|
46
|
+
for (const field of ['streamName', 'durableName', 'filterSubjects', 'deliverPolicy', 'ackPolicy']) {
|
|
47
|
+
if (!consumer.spec?.[field]) throw new Error(`Відсутнє поле spec.${field}`)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (!Array.isArray(consumer.spec.filterSubjects)) throw new Error('Поле spec.filterSubjects повинно бути масивом')
|
|
51
|
+
|
|
52
|
+
for (const subject of consumer.spec.filterSubjects) {
|
|
53
|
+
checkSubjectFormat(subject)
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// Приклад використання
|
|
58
|
+
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
59
|
+
try {
|
|
60
|
+
const consumer = parseConsumerYaml()
|
|
61
|
+
|
|
62
|
+
validateConsumer(consumer)
|
|
63
|
+
|
|
64
|
+
await ensureConsumer(consumer.spec)
|
|
65
|
+
exit(0)
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error('Помилка:', error.message)
|
|
68
|
+
exit(1)
|
|
69
|
+
}
|
|
70
|
+
}
|
package/src/consumer.js
CHANGED
|
@@ -1,49 +1,69 @@
|
|
|
1
|
-
import { jsm,
|
|
2
|
-
import {
|
|
1
|
+
import { jsm, stream } from './nats.js'
|
|
2
|
+
import { ensureStream } from './stream.js'
|
|
3
3
|
import { log } from '@nitra/pino'
|
|
4
|
-
// import { ensureStream } from './stream.js'
|
|
5
|
-
|
|
6
|
-
// Set для зберігання існуючих consumer
|
|
7
|
-
const consumerReady = new Set()
|
|
8
4
|
|
|
9
5
|
/**
|
|
10
|
-
*
|
|
11
|
-
* @param {string
|
|
12
|
-
* @param {object[]} consumers - список consumer-ів. [{durableName, filterSubjects}]
|
|
6
|
+
* Створює або оновлює consumer.
|
|
7
|
+
* @param {object{streamName: string, durableName: string, filterSubjects: string[], deliverPolicy: string, ackPolicy: string}} spec - об'єкт consumer-а.
|
|
13
8
|
* @returns {Promise<void>}
|
|
14
9
|
*/
|
|
15
|
-
export async function ensureConsumer(
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
}
|
|
10
|
+
export async function ensureConsumer(spec) {
|
|
11
|
+
// створюємо stream якщо не існує
|
|
12
|
+
const streamName = spec.streamName || stream
|
|
13
|
+
await ensureStream(streamName)
|
|
20
14
|
|
|
21
|
-
|
|
22
|
-
|
|
15
|
+
const consumerArr = (await jsm.consumers?.list(streamName)?.next()) || []
|
|
16
|
+
const consumer = consumerArr.find(ca => ca.config.durable_name === spec.durableName)
|
|
23
17
|
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
// }
|
|
18
|
+
// якщо не існує consumer-а створюємо його. якщо є перевіримо чи відповідає spec(якщо ні, то перестворюємо або оновлюємо)
|
|
19
|
+
if (consumer) {
|
|
20
|
+
log.info(`✅ Consumer «${spec.durableName}» already exists`)
|
|
28
21
|
|
|
29
|
-
|
|
30
|
-
for (const c of consumers) {
|
|
31
|
-
const isExists = consumerArr.some(ca => ca.config.durable_name === c.durableName)
|
|
22
|
+
const { filter_subjects, deliver_policy, ack_policy } = consumer.config
|
|
32
23
|
|
|
33
|
-
// якщо
|
|
34
|
-
if (
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
24
|
+
// якщо deliver_policy або ack_policy не відповідають spec, то перестворюємо consumer
|
|
25
|
+
if (deliver_policy !== spec.deliverPolicy || ack_policy !== spec.ackPolicy) {
|
|
26
|
+
const tempSpec = {
|
|
27
|
+
durableName: `temp_${spec.durableName}`,
|
|
28
|
+
filterSubjects: filter_subjects,
|
|
29
|
+
deliverPolicy: deliver_policy,
|
|
30
|
+
ackPolicy: ack_policy
|
|
31
|
+
}
|
|
32
|
+
// create temp consumer with old filter_subjects
|
|
33
|
+
await createConsumer(streamName, tempSpec)
|
|
34
|
+
// delete old consumer
|
|
35
|
+
await jsm.consumers.delete(streamName, spec.durableName)
|
|
36
|
+
// create new consumer
|
|
37
|
+
await createConsumer(streamName, spec)
|
|
38
|
+
// delete temp consumer
|
|
39
|
+
await jsm.consumers.delete(streamName, tempSpec.durableName)
|
|
43
40
|
|
|
44
|
-
log.
|
|
41
|
+
log.info(`🔥 Consumer «${spec.durableName}» - recreated`)
|
|
45
42
|
}
|
|
43
|
+
// якщо 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}`)
|
|
50
|
+
})
|
|
46
51
|
|
|
47
|
-
|
|
52
|
+
log.info(`🔥 Consumer «${spec.durableName}» - updated`)
|
|
53
|
+
} else {
|
|
54
|
+
log.info(`✅ Consumer «${spec.durableName}» - no changes`)
|
|
55
|
+
}
|
|
56
|
+
} else {
|
|
57
|
+
await createConsumer(streamName, spec)
|
|
58
|
+
log.info(`🔥 Consumer «${spec.durableName}» created`)
|
|
48
59
|
}
|
|
49
60
|
}
|
|
61
|
+
|
|
62
|
+
async function createConsumer(streamName, spec) {
|
|
63
|
+
await jsm.consumers.add(streamName, {
|
|
64
|
+
durable_name: spec.durableName,
|
|
65
|
+
ack_policy: spec.ackPolicy, // якщо не підтвердити повідомлення, воно буде повторно надіслано
|
|
66
|
+
filter_subjects: spec.filterSubjects.map(fs => `${streamName}.${fs}`), // якщо не вказати, то consumer буде слухати всі повідомлення зі stream
|
|
67
|
+
deliver_policy: spec.deliverPolicy // 'all' - всі непрочитані повідомлення
|
|
68
|
+
})
|
|
69
|
+
}
|
package/src/nats.js
CHANGED
|
@@ -1,13 +1,26 @@
|
|
|
1
|
-
import { checkEnv
|
|
1
|
+
import { checkEnv } from '@nitra/check-env'
|
|
2
2
|
import { connect, JSONCodec } from 'nats'
|
|
3
|
-
|
|
4
|
-
checkEnv(['NATS_URL', 'NATS_STREAM'])
|
|
3
|
+
import { env } from 'node:process'
|
|
5
4
|
|
|
6
5
|
// AckPolicy
|
|
7
|
-
export { AckPolicy } from 'nats'
|
|
6
|
+
// export { AckPolicy } from 'nats'
|
|
7
|
+
|
|
8
|
+
let nc
|
|
9
|
+
|
|
10
|
+
// якщо задані демо дані, то використовуємо фейковий NATS
|
|
11
|
+
if (env.NATS_FAKE_DATA) {
|
|
12
|
+
nc = {
|
|
13
|
+
jetstream: () => {},
|
|
14
|
+
jetstreamManager: async () => {
|
|
15
|
+
Promise.resolve()
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
} else {
|
|
19
|
+
checkEnv(['NATS_URL', 'NATS_STREAM'])
|
|
8
20
|
|
|
9
|
-
// Connect to NATS
|
|
10
|
-
|
|
21
|
+
// Connect to NATS
|
|
22
|
+
nc = await connect({ servers: env.NATS_URL })
|
|
23
|
+
}
|
|
11
24
|
|
|
12
25
|
// JSONCodec
|
|
13
26
|
export const jc = JSONCodec()
|
package/src/pending-count.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { jsm, stream } from './nats.js'
|
|
2
2
|
import { log } from '@nitra/pino'
|
|
3
|
+
import { env } from 'node:process'
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
6
|
* Повертає кількість непрочитаних (pending) повідомлень для durable consumer.
|
|
@@ -7,6 +8,11 @@ import { log } from '@nitra/pino'
|
|
|
7
8
|
* @returns {Promise<number>} - кількість непрочитаних повідомлень
|
|
8
9
|
*/
|
|
9
10
|
export async function getPendingCount(consumer) {
|
|
11
|
+
// якщо задані демо дані, то завжди повертаємо 0
|
|
12
|
+
if (env.NATS_FAKE_DATA) {
|
|
13
|
+
return 0
|
|
14
|
+
}
|
|
15
|
+
|
|
10
16
|
try {
|
|
11
17
|
const info = await jsm.consumers.info(stream, consumer)
|
|
12
18
|
return info.num_pending + info.num_ack_pending
|
package/src/publish.js
CHANGED
|
@@ -1,18 +1,22 @@
|
|
|
1
1
|
import { jc, js, stream } from './nats.js'
|
|
2
|
-
import {
|
|
2
|
+
import { checkSubjectFormat } from './utils.js'
|
|
3
3
|
import { log } from '@nitra/pino'
|
|
4
|
+
import { env } from 'node:process'
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Публікує повідомлення у JetStream для вказаного subject.
|
|
7
|
-
* Consumer створюється автоматично, якщо не існує.
|
|
8
8
|
* @param {string} subject - subject у форматі project:subject
|
|
9
9
|
* @param {object} data - дані для публікації
|
|
10
|
-
* @param {object[]} [consumers] - список consumer-ів які потрібно створити. [{durableName, filterSubject}] (необов'язково)
|
|
11
10
|
* @returns {Promise<void>}
|
|
12
11
|
*/
|
|
13
|
-
export async function publish(subject, data
|
|
14
|
-
//
|
|
15
|
-
|
|
12
|
+
export async function publish(subject, data) {
|
|
13
|
+
// якщо задані демо дані, то ігноримо запис
|
|
14
|
+
if (env.NATS_FAKE_DATA) {
|
|
15
|
+
return Promise.resolve()
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// перевіряємо чи subject відповідає формату project:subject
|
|
19
|
+
checkSubjectFormat(subject)
|
|
16
20
|
|
|
17
21
|
log.debug('publish:', data)
|
|
18
22
|
// Publish message
|
package/src/stream.js
CHANGED
|
@@ -5,14 +5,14 @@ import { log } from '@nitra/pino'
|
|
|
5
5
|
* Перевіряє наявність або створює JetStream stream.
|
|
6
6
|
* @returns {Promise<void>}
|
|
7
7
|
*/
|
|
8
|
-
export async function ensureStream() {
|
|
8
|
+
export async function ensureStream(streamName = stream) {
|
|
9
9
|
try {
|
|
10
|
-
await jsm.streams.info(
|
|
10
|
+
await jsm.streams.info(streamName)
|
|
11
11
|
log.debug('✅ Stream already exists')
|
|
12
12
|
} catch {
|
|
13
13
|
await jsm.streams.add({
|
|
14
|
-
name:
|
|
15
|
-
subjects: [`${
|
|
14
|
+
name: streamName,
|
|
15
|
+
subjects: [`${streamName}.>`],
|
|
16
16
|
retention: 'interest', // зберігає повідомлення поки є consumers або поки всі повідомлення не будуть прочитані
|
|
17
17
|
replicas: 1, // кількість реплік у кластері TODO: проговорити кількість реплік
|
|
18
18
|
storage: 'file' //'memory' TODO: проговорити які параметри потрібні для кластера
|
package/src/worker.js
CHANGED
|
@@ -2,6 +2,7 @@ import { js, jc, stream } from './nats.js'
|
|
|
2
2
|
import { state } from './utils.js'
|
|
3
3
|
import { exit } from 'node:process'
|
|
4
4
|
import { log } from '@nitra/pino'
|
|
5
|
+
import { env } from 'node:process'
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Зчитує одне повідомлення з JetStream для вказаного consumer-а.
|
|
@@ -9,6 +10,12 @@ import { log } from '@nitra/pino'
|
|
|
9
10
|
* @returns {Promise<object>} - декодовані дані повідомлення
|
|
10
11
|
*/
|
|
11
12
|
export async function read(consumer) {
|
|
13
|
+
// якщо задані демо дані, то повертаємо їх
|
|
14
|
+
if (env.NATS_FAKE_DATA) {
|
|
15
|
+
console.log('env.NATS_FAKE_DATA:', env.NATS_FAKE_DATA)
|
|
16
|
+
return JSON.parse(env.NATS_FAKE_DATA)
|
|
17
|
+
}
|
|
18
|
+
|
|
12
19
|
const consumerObj = await js.consumers.get(stream, consumer)
|
|
13
20
|
|
|
14
21
|
const iter = await consumerObj.fetch({ max_messages: 1, expires: 1000 })
|