@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 +1 -5
- package/package.json +20 -18
- package/src/cli.js +1 -2
- package/src/consumer.js +82 -25
- package/src/index.js +1 -2
- package/src/nats.js +57 -18
- package/src/pending-count.js +2 -1
- package/src/publish.js +37 -5
- package/src/stream.js +6 -21
- package/src/utils.js +15 -8
- package/src/worker.js +80 -12
- package/src/finish.js +0 -13
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
|
-
|
|
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
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
|
|
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:
|
|
36
|
+
filterSubjects: spec.filterSubjects,
|
|
29
37
|
deliverPolicy: deliver_policy,
|
|
30
38
|
ackPolicy: ack_policy
|
|
31
39
|
}
|
|
32
|
-
//
|
|
33
|
-
|
|
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(
|
|
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
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
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(
|
|
63
|
+
await createConsumer(spec)
|
|
58
64
|
log.info(`🔥 Consumer «${spec.durableName}» created`)
|
|
59
65
|
}
|
|
60
66
|
}
|
|
61
67
|
|
|
62
|
-
|
|
63
|
-
|
|
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
|
|
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
package/src/nats.js
CHANGED
|
@@ -1,35 +1,74 @@
|
|
|
1
1
|
import { checkEnv } from '@nitra/check-env'
|
|
2
|
-
import { connect
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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'
|
|
64
|
+
checkEnv(['NATS_URL'])
|
|
20
65
|
|
|
21
66
|
// Connect to NATS
|
|
22
67
|
nc = await connect({ servers: env.NATS_URL })
|
|
23
|
-
}
|
|
24
68
|
|
|
25
|
-
//
|
|
26
|
-
|
|
69
|
+
// Створюємо JetStream контекст
|
|
70
|
+
js = jetstream(nc)
|
|
27
71
|
|
|
28
|
-
//
|
|
29
|
-
|
|
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
|
+
}
|
package/src/pending-count.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { jsm
|
|
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 {
|
|
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}`,
|
|
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 {
|
|
2
|
-
import {
|
|
1
|
+
import { checkEnv } from '@nitra/check-env'
|
|
2
|
+
import { env } from 'node:process'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
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 має правильний формат:
|
|
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
|
|
7
|
+
export const checkSubjectFormat = (stream, subject) => {
|
|
8
|
+
const arr = subject.split('.')
|
|
8
9
|
|
|
9
|
-
if (arr?.length
|
|
10
|
-
throw new Error('subject must be in the format:
|
|
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
|
-
|
|
15
|
-
|
|
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 {
|
|
4
|
-
import {
|
|
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 (
|
|
14
|
-
|
|
15
|
-
return
|
|
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
|
|
22
|
-
|
|
23
|
-
log.debug('read msg', jc.decode(msg.data))
|
|
61
|
+
for await (const msgI of iter) {
|
|
62
|
+
msg = msgI
|
|
24
63
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
}
|