@jr200-labs/xstate-nats 0.6.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/LICENSE +21 -0
- package/README.md +251 -0
- package/dist/actions/connection.d.ts +28 -0
- package/dist/actions/connection.d.ts.map +1 -0
- package/dist/actions/connection.js +102 -0
- package/dist/actions/connection.js.map +1 -0
- package/dist/actions/kv.d.ts +21 -0
- package/dist/actions/kv.d.ts.map +1 -0
- package/dist/actions/kv.js +66 -0
- package/dist/actions/kv.js.map +1 -0
- package/dist/actions/subject.d.ts +39 -0
- package/dist/actions/subject.d.ts.map +1 -0
- package/dist/actions/subject.js +79 -0
- package/dist/actions/subject.js.map +1 -0
- package/dist/actions/types.d.ts +8 -0
- package/dist/actions/types.d.ts.map +1 -0
- package/dist/actions/types.js +2 -0
- package/dist/actions/types.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +6 -0
- package/dist/index.js.map +1 -0
- package/dist/machines/kv.d.ts +190 -0
- package/dist/machines/kv.d.ts.map +1 -0
- package/dist/machines/kv.js +273 -0
- package/dist/machines/kv.js.map +1 -0
- package/dist/machines/root.d.ts +510 -0
- package/dist/machines/root.d.ts.map +1 -0
- package/dist/machines/root.js +245 -0
- package/dist/machines/root.js.map +1 -0
- package/dist/machines/subject.d.ts +95 -0
- package/dist/machines/subject.d.ts.map +1 -0
- package/dist/machines/subject.js +162 -0
- package/dist/machines/subject.js.map +1 -0
- package/dist/utils.d.ts +10 -0
- package/dist/utils.d.ts.map +1 -0
- package/dist/utils.js +27 -0
- package/dist/utils.js.map +1 -0
- package/package.json +55 -0
- package/src/actions/connection.test.ts +324 -0
- package/src/actions/connection.ts +135 -0
- package/src/actions/kv.test.ts +439 -0
- package/src/actions/kv.ts +92 -0
- package/src/actions/subject.test.ts +460 -0
- package/src/actions/subject.ts +127 -0
- package/src/actions/types.ts +7 -0
- package/src/index.ts +20 -0
- package/src/machines/kv.test.ts +720 -0
- package/src/machines/kv.ts +327 -0
- package/src/machines/root.test.ts +329 -0
- package/src/machines/root.ts +286 -0
- package/src/machines/subject.test.ts +272 -0
- package/src/machines/subject.ts +205 -0
- package/src/utils.test.ts +35 -0
- package/src/utils.ts +30 -0
|
@@ -0,0 +1,460 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { subjectConsolidateState, subjectRequest, subjectPublish } from './subject'
|
|
3
|
+
import type { SubjectSubscriptionConfig } from './subject'
|
|
4
|
+
|
|
5
|
+
// Mock the connection module
|
|
6
|
+
vi.mock('./connection', () => ({
|
|
7
|
+
parseNatsResult: vi.fn((msg: any) => {
|
|
8
|
+
if (!msg) return null
|
|
9
|
+
if (msg instanceof Error) return msg
|
|
10
|
+
try {
|
|
11
|
+
return msg.json()
|
|
12
|
+
} catch {
|
|
13
|
+
return msg.string()
|
|
14
|
+
}
|
|
15
|
+
}),
|
|
16
|
+
}))
|
|
17
|
+
|
|
18
|
+
function createMockConnection() {
|
|
19
|
+
return {
|
|
20
|
+
subscribe: vi.fn(),
|
|
21
|
+
request: vi.fn(),
|
|
22
|
+
publish: vi.fn(),
|
|
23
|
+
} as any
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function createMockSubscription(subject: string) {
|
|
27
|
+
return {
|
|
28
|
+
unsubscribe: vi.fn(),
|
|
29
|
+
[Symbol.asyncIterator]: () => ({
|
|
30
|
+
next: () => new Promise(() => {}), // never resolves
|
|
31
|
+
}),
|
|
32
|
+
} as any
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe('subjectConsolidateState', () => {
|
|
36
|
+
it('should throw when connection is null', () => {
|
|
37
|
+
expect(() =>
|
|
38
|
+
subjectConsolidateState({
|
|
39
|
+
input: {
|
|
40
|
+
connection: null,
|
|
41
|
+
currentSubscriptions: new Map(),
|
|
42
|
+
targetSubscriptions: new Map(),
|
|
43
|
+
},
|
|
44
|
+
}),
|
|
45
|
+
).toThrow('NATS connection is not available')
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
it('should subscribe to new subjects in target', () => {
|
|
49
|
+
const connection = createMockConnection()
|
|
50
|
+
const mockSub = createMockSubscription('test.subject')
|
|
51
|
+
connection.subscribe.mockReturnValue(mockSub)
|
|
52
|
+
|
|
53
|
+
const callback = vi.fn()
|
|
54
|
+
const targetSubscriptions = new Map<string, SubjectSubscriptionConfig>([
|
|
55
|
+
['test.subject', { subject: 'test.subject', callback }],
|
|
56
|
+
])
|
|
57
|
+
|
|
58
|
+
const result = subjectConsolidateState({
|
|
59
|
+
input: {
|
|
60
|
+
connection,
|
|
61
|
+
currentSubscriptions: new Map(),
|
|
62
|
+
targetSubscriptions,
|
|
63
|
+
},
|
|
64
|
+
})
|
|
65
|
+
|
|
66
|
+
expect(connection.subscribe).toHaveBeenCalledWith('test.subject', undefined)
|
|
67
|
+
expect(result.subscriptions.has('test.subject')).toBe(true)
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
it('should unsubscribe from subjects not in target', () => {
|
|
71
|
+
const connection = createMockConnection()
|
|
72
|
+
const mockSub = createMockSubscription('old.subject')
|
|
73
|
+
const currentSubscriptions = new Map([['old.subject', mockSub]])
|
|
74
|
+
|
|
75
|
+
const result = subjectConsolidateState({
|
|
76
|
+
input: {
|
|
77
|
+
connection,
|
|
78
|
+
currentSubscriptions,
|
|
79
|
+
targetSubscriptions: new Map(),
|
|
80
|
+
},
|
|
81
|
+
})
|
|
82
|
+
|
|
83
|
+
expect(mockSub.unsubscribe).toHaveBeenCalled()
|
|
84
|
+
expect(result.subscriptions.has('old.subject')).toBe(false)
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
it('should keep existing subscriptions that are still in target', () => {
|
|
88
|
+
const connection = createMockConnection()
|
|
89
|
+
const existingSub = createMockSubscription('keep.subject')
|
|
90
|
+
const callback = vi.fn()
|
|
91
|
+
|
|
92
|
+
const currentSubscriptions = new Map([['keep.subject', existingSub]])
|
|
93
|
+
const targetSubscriptions = new Map<string, SubjectSubscriptionConfig>([
|
|
94
|
+
['keep.subject', { subject: 'keep.subject', callback }],
|
|
95
|
+
])
|
|
96
|
+
|
|
97
|
+
const result = subjectConsolidateState({
|
|
98
|
+
input: {
|
|
99
|
+
connection,
|
|
100
|
+
currentSubscriptions,
|
|
101
|
+
targetSubscriptions,
|
|
102
|
+
},
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
expect(connection.subscribe).not.toHaveBeenCalled()
|
|
106
|
+
expect(existingSub.unsubscribe).not.toHaveBeenCalled()
|
|
107
|
+
expect(result.subscriptions.get('keep.subject')).toBe(existingSub)
|
|
108
|
+
})
|
|
109
|
+
|
|
110
|
+
it('should handle subscribe errors gracefully', () => {
|
|
111
|
+
const connection = createMockConnection()
|
|
112
|
+
connection.subscribe.mockImplementation(() => {
|
|
113
|
+
throw new Error('subscribe failed')
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
const callback = vi.fn()
|
|
117
|
+
const targetSubscriptions = new Map<string, SubjectSubscriptionConfig>([
|
|
118
|
+
['fail.subject', { subject: 'fail.subject', callback }],
|
|
119
|
+
])
|
|
120
|
+
|
|
121
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
122
|
+
const result = subjectConsolidateState({
|
|
123
|
+
input: {
|
|
124
|
+
connection,
|
|
125
|
+
currentSubscriptions: new Map(),
|
|
126
|
+
targetSubscriptions,
|
|
127
|
+
},
|
|
128
|
+
})
|
|
129
|
+
consoleSpy.mockRestore()
|
|
130
|
+
|
|
131
|
+
expect(result.subscriptions.has('fail.subject')).toBe(false)
|
|
132
|
+
})
|
|
133
|
+
|
|
134
|
+
it('should handle unsubscribe errors gracefully', () => {
|
|
135
|
+
const connection = createMockConnection()
|
|
136
|
+
const mockSub = {
|
|
137
|
+
unsubscribe: vi.fn(() => {
|
|
138
|
+
throw new Error('unsub failed')
|
|
139
|
+
}),
|
|
140
|
+
} as any
|
|
141
|
+
|
|
142
|
+
const currentSubscriptions = new Map([['bad.subject', mockSub]])
|
|
143
|
+
|
|
144
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
145
|
+
const result = subjectConsolidateState({
|
|
146
|
+
input: {
|
|
147
|
+
connection,
|
|
148
|
+
currentSubscriptions,
|
|
149
|
+
targetSubscriptions: new Map(),
|
|
150
|
+
},
|
|
151
|
+
})
|
|
152
|
+
consoleSpy.mockRestore()
|
|
153
|
+
|
|
154
|
+
expect(mockSub.unsubscribe).toHaveBeenCalled()
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
it('should invoke callback for each message in the async loop', async () => {
|
|
158
|
+
const connection = createMockConnection()
|
|
159
|
+
const messages = [
|
|
160
|
+
{ json: () => ({ id: 1 }), string: () => '{"id":1}' },
|
|
161
|
+
{ json: () => ({ id: 2 }), string: () => '{"id":2}' },
|
|
162
|
+
]
|
|
163
|
+
let resolveIterator: () => void
|
|
164
|
+
const iteratorDone = new Promise<void>((r) => (resolveIterator = r))
|
|
165
|
+
let msgIndex = 0
|
|
166
|
+
|
|
167
|
+
const mockSub = {
|
|
168
|
+
unsubscribe: vi.fn(),
|
|
169
|
+
[Symbol.asyncIterator]: () => ({
|
|
170
|
+
next: () => {
|
|
171
|
+
if (msgIndex < messages.length) {
|
|
172
|
+
return Promise.resolve({ value: messages[msgIndex++], done: false })
|
|
173
|
+
}
|
|
174
|
+
resolveIterator!()
|
|
175
|
+
return new Promise(() => {}) // hang after messages delivered
|
|
176
|
+
},
|
|
177
|
+
}),
|
|
178
|
+
} as any
|
|
179
|
+
connection.subscribe.mockReturnValue(mockSub)
|
|
180
|
+
|
|
181
|
+
const callback = vi.fn()
|
|
182
|
+
const targetSubscriptions = new Map<string, SubjectSubscriptionConfig>([
|
|
183
|
+
['test.subject', { subject: 'test.subject', callback }],
|
|
184
|
+
])
|
|
185
|
+
|
|
186
|
+
subjectConsolidateState({
|
|
187
|
+
input: {
|
|
188
|
+
connection,
|
|
189
|
+
currentSubscriptions: new Map(),
|
|
190
|
+
targetSubscriptions,
|
|
191
|
+
},
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
await iteratorDone
|
|
195
|
+
// Allow microtasks to flush
|
|
196
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
197
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
198
|
+
expect(callback).toHaveBeenCalledWith({ id: 1 })
|
|
199
|
+
expect(callback).toHaveBeenCalledWith({ id: 2 })
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
it('should handle callback errors in the message loop', async () => {
|
|
203
|
+
const connection = createMockConnection()
|
|
204
|
+
let resolveIterator: () => void
|
|
205
|
+
const iteratorDone = new Promise<void>((r) => (resolveIterator = r))
|
|
206
|
+
let delivered = false
|
|
207
|
+
|
|
208
|
+
const mockSub = {
|
|
209
|
+
unsubscribe: vi.fn(),
|
|
210
|
+
[Symbol.asyncIterator]: () => ({
|
|
211
|
+
next: () => {
|
|
212
|
+
if (!delivered) {
|
|
213
|
+
delivered = true
|
|
214
|
+
return Promise.resolve({
|
|
215
|
+
value: { json: () => ({ id: 1 }), string: () => '{"id":1}' },
|
|
216
|
+
done: false,
|
|
217
|
+
})
|
|
218
|
+
}
|
|
219
|
+
resolveIterator!()
|
|
220
|
+
return new Promise(() => {})
|
|
221
|
+
},
|
|
222
|
+
}),
|
|
223
|
+
} as any
|
|
224
|
+
connection.subscribe.mockReturnValue(mockSub)
|
|
225
|
+
|
|
226
|
+
const callback = vi.fn(() => {
|
|
227
|
+
throw new Error('callback error')
|
|
228
|
+
})
|
|
229
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
230
|
+
|
|
231
|
+
subjectConsolidateState({
|
|
232
|
+
input: {
|
|
233
|
+
connection,
|
|
234
|
+
currentSubscriptions: new Map(),
|
|
235
|
+
targetSubscriptions: new Map([['test.subject', { subject: 'test.subject', callback }]]),
|
|
236
|
+
},
|
|
237
|
+
})
|
|
238
|
+
|
|
239
|
+
await iteratorDone
|
|
240
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
241
|
+
consoleSpy.mockRestore()
|
|
242
|
+
|
|
243
|
+
expect(callback).toHaveBeenCalledTimes(1)
|
|
244
|
+
})
|
|
245
|
+
|
|
246
|
+
it('should handle iterator errors in the message loop', async () => {
|
|
247
|
+
const connection = createMockConnection()
|
|
248
|
+
|
|
249
|
+
const mockSub = {
|
|
250
|
+
unsubscribe: vi.fn(),
|
|
251
|
+
[Symbol.asyncIterator]: () => ({
|
|
252
|
+
next: () => Promise.reject(new Error('iterator broke')),
|
|
253
|
+
}),
|
|
254
|
+
} as any
|
|
255
|
+
connection.subscribe.mockReturnValue(mockSub)
|
|
256
|
+
|
|
257
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
258
|
+
|
|
259
|
+
subjectConsolidateState({
|
|
260
|
+
input: {
|
|
261
|
+
connection,
|
|
262
|
+
currentSubscriptions: new Map(),
|
|
263
|
+
targetSubscriptions: new Map([
|
|
264
|
+
['test.subject', { subject: 'test.subject', callback: vi.fn() }],
|
|
265
|
+
]),
|
|
266
|
+
},
|
|
267
|
+
})
|
|
268
|
+
|
|
269
|
+
await vi.waitFor(() => {
|
|
270
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
271
|
+
expect.stringContaining('Iterator error'),
|
|
272
|
+
expect.any(Error),
|
|
273
|
+
)
|
|
274
|
+
})
|
|
275
|
+
consoleSpy.mockRestore()
|
|
276
|
+
})
|
|
277
|
+
|
|
278
|
+
it('should pass subscription opts to connection.subscribe', () => {
|
|
279
|
+
const connection = createMockConnection()
|
|
280
|
+
const mockSub = createMockSubscription('test.subject')
|
|
281
|
+
connection.subscribe.mockReturnValue(mockSub)
|
|
282
|
+
|
|
283
|
+
const opts = { max: 10 }
|
|
284
|
+
const targetSubscriptions = new Map<string, SubjectSubscriptionConfig>([
|
|
285
|
+
['test.subject', { subject: 'test.subject', callback: vi.fn(), opts }],
|
|
286
|
+
])
|
|
287
|
+
|
|
288
|
+
subjectConsolidateState({
|
|
289
|
+
input: {
|
|
290
|
+
connection,
|
|
291
|
+
currentSubscriptions: new Map(),
|
|
292
|
+
targetSubscriptions,
|
|
293
|
+
},
|
|
294
|
+
})
|
|
295
|
+
|
|
296
|
+
expect(connection.subscribe).toHaveBeenCalledWith('test.subject', opts)
|
|
297
|
+
})
|
|
298
|
+
})
|
|
299
|
+
|
|
300
|
+
describe('subjectRequest', () => {
|
|
301
|
+
it('should throw when connection is null', () => {
|
|
302
|
+
expect(() =>
|
|
303
|
+
subjectRequest({
|
|
304
|
+
input: {
|
|
305
|
+
connection: null,
|
|
306
|
+
subject: 'test',
|
|
307
|
+
payload: {},
|
|
308
|
+
callback: vi.fn(),
|
|
309
|
+
},
|
|
310
|
+
}),
|
|
311
|
+
).toThrow('NATS connection is not available')
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
it('should call connection.request and invoke callback on success', async () => {
|
|
315
|
+
const connection = createMockConnection()
|
|
316
|
+
const mockMsg = { json: () => ({ result: 'ok' }), string: () => '{"result":"ok"}' }
|
|
317
|
+
connection.request.mockResolvedValue(mockMsg)
|
|
318
|
+
|
|
319
|
+
const callback = vi.fn()
|
|
320
|
+
subjectRequest({
|
|
321
|
+
input: {
|
|
322
|
+
connection,
|
|
323
|
+
subject: 'test.request',
|
|
324
|
+
payload: { data: 1 },
|
|
325
|
+
opts: { timeout: 5000 } as any,
|
|
326
|
+
callback,
|
|
327
|
+
},
|
|
328
|
+
})
|
|
329
|
+
|
|
330
|
+
// Wait for promise to resolve
|
|
331
|
+
await vi.waitFor(() => {
|
|
332
|
+
expect(callback).toHaveBeenCalled()
|
|
333
|
+
})
|
|
334
|
+
|
|
335
|
+
expect(connection.request).toHaveBeenCalledWith('test.request', { data: 1 }, { timeout: 5000 })
|
|
336
|
+
})
|
|
337
|
+
|
|
338
|
+
it('should handle request errors', async () => {
|
|
339
|
+
const connection = createMockConnection()
|
|
340
|
+
connection.request.mockRejectedValue(new Error('request failed'))
|
|
341
|
+
|
|
342
|
+
const callback = vi.fn()
|
|
343
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
344
|
+
|
|
345
|
+
subjectRequest({
|
|
346
|
+
input: {
|
|
347
|
+
connection,
|
|
348
|
+
subject: 'test.fail',
|
|
349
|
+
payload: {},
|
|
350
|
+
callback,
|
|
351
|
+
},
|
|
352
|
+
})
|
|
353
|
+
|
|
354
|
+
await vi.waitFor(() => {
|
|
355
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
356
|
+
expect.stringContaining('RequestReply error'),
|
|
357
|
+
expect.any(Error),
|
|
358
|
+
)
|
|
359
|
+
})
|
|
360
|
+
|
|
361
|
+
consoleSpy.mockRestore()
|
|
362
|
+
expect(callback).not.toHaveBeenCalled()
|
|
363
|
+
})
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
describe('subjectPublish', () => {
|
|
367
|
+
it('should throw when connection is null', () => {
|
|
368
|
+
expect(() =>
|
|
369
|
+
subjectPublish({
|
|
370
|
+
input: {
|
|
371
|
+
connection: null,
|
|
372
|
+
subject: 'test',
|
|
373
|
+
payload: {},
|
|
374
|
+
},
|
|
375
|
+
}),
|
|
376
|
+
).toThrow('NATS connection is not available')
|
|
377
|
+
})
|
|
378
|
+
|
|
379
|
+
it('should publish to connection', () => {
|
|
380
|
+
const connection = createMockConnection()
|
|
381
|
+
|
|
382
|
+
subjectPublish({
|
|
383
|
+
input: {
|
|
384
|
+
connection,
|
|
385
|
+
subject: 'test.publish',
|
|
386
|
+
payload: { msg: 'hello' },
|
|
387
|
+
},
|
|
388
|
+
})
|
|
389
|
+
|
|
390
|
+
expect(connection.publish).toHaveBeenCalledWith('test.publish', { msg: 'hello' }, undefined)
|
|
391
|
+
})
|
|
392
|
+
|
|
393
|
+
it('should call onPublishResult with ok on success', () => {
|
|
394
|
+
const connection = createMockConnection()
|
|
395
|
+
const onPublishResult = vi.fn()
|
|
396
|
+
|
|
397
|
+
subjectPublish({
|
|
398
|
+
input: {
|
|
399
|
+
connection,
|
|
400
|
+
subject: 'test.publish',
|
|
401
|
+
payload: 'data',
|
|
402
|
+
onPublishResult,
|
|
403
|
+
},
|
|
404
|
+
})
|
|
405
|
+
|
|
406
|
+
expect(onPublishResult).toHaveBeenCalledWith({ ok: true })
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
it('should pass publish options', () => {
|
|
410
|
+
const connection = createMockConnection()
|
|
411
|
+
const options = { headers: {} } as any
|
|
412
|
+
|
|
413
|
+
subjectPublish({
|
|
414
|
+
input: {
|
|
415
|
+
connection,
|
|
416
|
+
subject: 'test.publish',
|
|
417
|
+
payload: 'data',
|
|
418
|
+
options,
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
expect(connection.publish).toHaveBeenCalledWith('test.publish', 'data', options)
|
|
423
|
+
})
|
|
424
|
+
|
|
425
|
+
it('should call onPublishResult with error on failure', () => {
|
|
426
|
+
const connection = createMockConnection()
|
|
427
|
+
const publishError = new Error('publish failed')
|
|
428
|
+
connection.publish.mockImplementation(() => {
|
|
429
|
+
throw publishError
|
|
430
|
+
})
|
|
431
|
+
const onPublishResult = vi.fn()
|
|
432
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
433
|
+
|
|
434
|
+
subjectPublish({
|
|
435
|
+
input: {
|
|
436
|
+
connection,
|
|
437
|
+
subject: 'test.fail',
|
|
438
|
+
payload: 'data',
|
|
439
|
+
onPublishResult,
|
|
440
|
+
},
|
|
441
|
+
})
|
|
442
|
+
|
|
443
|
+
consoleSpy.mockRestore()
|
|
444
|
+
expect(onPublishResult).toHaveBeenCalledWith({ ok: false, error: publishError })
|
|
445
|
+
})
|
|
446
|
+
|
|
447
|
+
it('should not throw when onPublishResult is not provided and publish succeeds', () => {
|
|
448
|
+
const connection = createMockConnection()
|
|
449
|
+
|
|
450
|
+
expect(() =>
|
|
451
|
+
subjectPublish({
|
|
452
|
+
input: {
|
|
453
|
+
connection,
|
|
454
|
+
subject: 'test.publish',
|
|
455
|
+
payload: 'data',
|
|
456
|
+
},
|
|
457
|
+
}),
|
|
458
|
+
).not.toThrow()
|
|
459
|
+
})
|
|
460
|
+
})
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Msg,
|
|
3
|
+
NatsConnection,
|
|
4
|
+
PublishOptions,
|
|
5
|
+
RequestOptions,
|
|
6
|
+
Subscription,
|
|
7
|
+
SubscriptionOptions,
|
|
8
|
+
} from '@nats-io/nats-core'
|
|
9
|
+
import { parseNatsResult } from './connection'
|
|
10
|
+
|
|
11
|
+
export type SubjectSubscriptionConfig = {
|
|
12
|
+
subject: string
|
|
13
|
+
callback: (data: any) => void
|
|
14
|
+
opts?: SubscriptionOptions
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const subjectConsolidateState = ({
|
|
18
|
+
input,
|
|
19
|
+
}: {
|
|
20
|
+
input: {
|
|
21
|
+
connection: NatsConnection | null
|
|
22
|
+
currentSubscriptions: Map<string, Subscription>
|
|
23
|
+
targetSubscriptions: Map<string, SubjectSubscriptionConfig>
|
|
24
|
+
}
|
|
25
|
+
}) => {
|
|
26
|
+
const { connection, currentSubscriptions, targetSubscriptions } = input
|
|
27
|
+
if (!connection) {
|
|
28
|
+
throw new Error('NATS connection is not available')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const syncedSubscriptions = new Map(currentSubscriptions)
|
|
32
|
+
|
|
33
|
+
// Unsubscribe from subjects that are in currentSubscriptions but not in targetSubscriptions
|
|
34
|
+
for (const [subject, subscription] of currentSubscriptions) {
|
|
35
|
+
if (!targetSubscriptions.has(subject)) {
|
|
36
|
+
try {
|
|
37
|
+
syncedSubscriptions.delete(subject)
|
|
38
|
+
subscription.unsubscribe()
|
|
39
|
+
} catch (error) {
|
|
40
|
+
console.error(`Error unsubscribing from subject "${subject}"`, error)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// Subscribe to new subjects that are in targetSubscriptions but not in currentSubscriptions
|
|
46
|
+
for (const [subject, subscriptionConfig] of targetSubscriptions) {
|
|
47
|
+
if (!currentSubscriptions.has(subject)) {
|
|
48
|
+
try {
|
|
49
|
+
const sub = connection.subscribe(subject, subscriptionConfig.opts)
|
|
50
|
+
|
|
51
|
+
// Set up the message handler
|
|
52
|
+
;(async () => {
|
|
53
|
+
try {
|
|
54
|
+
for await (const msg of sub) {
|
|
55
|
+
try {
|
|
56
|
+
subscriptionConfig?.callback(parseNatsResult(msg))
|
|
57
|
+
} catch (callbackError) {
|
|
58
|
+
console.error(`Callback error for subject "${subject}"`, callbackError)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
} catch (iteratorError) {
|
|
62
|
+
console.error(`Iterator error for subject "${subject}"`, iteratorError)
|
|
63
|
+
}
|
|
64
|
+
})()
|
|
65
|
+
|
|
66
|
+
syncedSubscriptions.set(subject, sub)
|
|
67
|
+
} catch (error) {
|
|
68
|
+
console.error(`Error subscribing to subject "${subject}"`, error)
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
subscriptions: syncedSubscriptions,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export const subjectRequest = ({
|
|
79
|
+
input,
|
|
80
|
+
}: {
|
|
81
|
+
input: {
|
|
82
|
+
connection: NatsConnection | null
|
|
83
|
+
subject: string
|
|
84
|
+
payload: any
|
|
85
|
+
opts?: RequestOptions
|
|
86
|
+
callback: (data: any) => void
|
|
87
|
+
}
|
|
88
|
+
}) => {
|
|
89
|
+
const { connection, subject, payload, opts, callback } = input
|
|
90
|
+
if (!connection) {
|
|
91
|
+
throw new Error('NATS connection is not available')
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
connection
|
|
95
|
+
.request(subject, payload, opts)
|
|
96
|
+
.then((msg: Msg) => {
|
|
97
|
+
callback(parseNatsResult(msg))
|
|
98
|
+
})
|
|
99
|
+
.catch((err) => {
|
|
100
|
+
console.error(`RequestReply error for subject "${subject}"`, err)
|
|
101
|
+
})
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export const subjectPublish = ({
|
|
105
|
+
input,
|
|
106
|
+
}: {
|
|
107
|
+
input: {
|
|
108
|
+
connection: NatsConnection | null
|
|
109
|
+
subject: string
|
|
110
|
+
payload: any
|
|
111
|
+
options?: PublishOptions
|
|
112
|
+
onPublishResult?: (result: { ok: true } | { ok: false; error: Error }) => void
|
|
113
|
+
}
|
|
114
|
+
}) => {
|
|
115
|
+
const { connection, subject, payload, options, onPublishResult } = input
|
|
116
|
+
if (!connection) {
|
|
117
|
+
throw new Error('NATS connection is not available')
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
try {
|
|
121
|
+
connection.publish(subject, payload, options)
|
|
122
|
+
onPublishResult?.({ ok: true })
|
|
123
|
+
} catch (callbackError) {
|
|
124
|
+
console.error(`Publish callback error for subject "${subject}"`, callbackError)
|
|
125
|
+
onPublishResult?.({ ok: false, error: callbackError as Error })
|
|
126
|
+
}
|
|
127
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { natsMachine } from './machines/root'
|
|
2
|
+
export {
|
|
3
|
+
subjectManagerLogic,
|
|
4
|
+
type Context as SubjectContext,
|
|
5
|
+
type ExternalEvents as SubjectEvent,
|
|
6
|
+
} from './machines/subject'
|
|
7
|
+
export {
|
|
8
|
+
kvManagerLogic,
|
|
9
|
+
type Context as KvContext,
|
|
10
|
+
type ExternalEvents as KvEvent,
|
|
11
|
+
} from './machines/kv'
|
|
12
|
+
export { KvSubscriptionKey, type KvSubscriptionConfig } from './actions/kv'
|
|
13
|
+
export { parseNatsResult } from './actions/connection'
|
|
14
|
+
export { type AuthConfig } from './actions/types'
|
|
15
|
+
|
|
16
|
+
export {
|
|
17
|
+
type NatsConnectionConfig,
|
|
18
|
+
type Context as NatsContext,
|
|
19
|
+
type ExternalEvents as NatsEvent,
|
|
20
|
+
} from './machines/root'
|