@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,439 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest'
|
|
2
|
+
import { createActor } from 'xstate'
|
|
3
|
+
import { KvSubscriptionKey, kvConsolidateState } from './kv'
|
|
4
|
+
|
|
5
|
+
describe('KvSubscriptionKey', () => {
|
|
6
|
+
it('should create a key from bucket and key', () => {
|
|
7
|
+
const result = KvSubscriptionKey.key('my-bucket', 'my-key')
|
|
8
|
+
expect(result).toBe('Pair(my-bucket, my-key)')
|
|
9
|
+
})
|
|
10
|
+
|
|
11
|
+
it('should parse a key back', () => {
|
|
12
|
+
const pair = KvSubscriptionKey.fromKey('Pair(bucket1, key1)')
|
|
13
|
+
expect(pair.x).toBe('bucket1')
|
|
14
|
+
expect(pair.y).toBe('key1')
|
|
15
|
+
})
|
|
16
|
+
|
|
17
|
+
it('should support equality check', () => {
|
|
18
|
+
const a = new KvSubscriptionKey('b', 'k')
|
|
19
|
+
const b = new KvSubscriptionKey('b', 'k')
|
|
20
|
+
const c = new KvSubscriptionKey('b', 'other')
|
|
21
|
+
expect(a.equals(b)).toBe(true)
|
|
22
|
+
expect(a.equals(c)).toBe(false)
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
it('should convert to key string', () => {
|
|
26
|
+
const pair = new KvSubscriptionKey('bucket', 'key')
|
|
27
|
+
expect(pair.toKey()).toBe('Pair(bucket, key)')
|
|
28
|
+
})
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
describe('kvConsolidateState', () => {
|
|
32
|
+
it('should throw when connection and kvm are null', async () => {
|
|
33
|
+
let caughtError: Error | undefined
|
|
34
|
+
const origListeners = process.listeners('uncaughtException')
|
|
35
|
+
process.removeAllListeners('uncaughtException')
|
|
36
|
+
process.once('uncaughtException', (err: Error) => {
|
|
37
|
+
caughtError = err
|
|
38
|
+
})
|
|
39
|
+
|
|
40
|
+
const actor = createActor(kvConsolidateState, {
|
|
41
|
+
input: {
|
|
42
|
+
kvm: null,
|
|
43
|
+
connection: null,
|
|
44
|
+
currentState: new Map(),
|
|
45
|
+
targetState: new Map(),
|
|
46
|
+
},
|
|
47
|
+
})
|
|
48
|
+
actor.start()
|
|
49
|
+
|
|
50
|
+
await vi.waitFor(() => {
|
|
51
|
+
expect(caughtError).toBeDefined()
|
|
52
|
+
})
|
|
53
|
+
expect(caughtError!.message).toContain('NATS connection or KVM is not available')
|
|
54
|
+
origListeners.forEach((l) => process.on('uncaughtException', l))
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
it('should throw when kvm is null', async () => {
|
|
58
|
+
let caughtError: Error | undefined
|
|
59
|
+
const origListeners = process.listeners('uncaughtException')
|
|
60
|
+
process.removeAllListeners('uncaughtException')
|
|
61
|
+
process.once('uncaughtException', (err: Error) => {
|
|
62
|
+
caughtError = err
|
|
63
|
+
})
|
|
64
|
+
|
|
65
|
+
const actor = createActor(kvConsolidateState, {
|
|
66
|
+
input: {
|
|
67
|
+
kvm: null,
|
|
68
|
+
connection: {} as any,
|
|
69
|
+
currentState: new Map(),
|
|
70
|
+
targetState: new Map(),
|
|
71
|
+
},
|
|
72
|
+
})
|
|
73
|
+
actor.start()
|
|
74
|
+
|
|
75
|
+
await vi.waitFor(() => {
|
|
76
|
+
expect(caughtError).toBeDefined()
|
|
77
|
+
})
|
|
78
|
+
expect(caughtError!.message).toContain('NATS connection or KVM is not available')
|
|
79
|
+
origListeners.forEach((l) => process.on('uncaughtException', l))
|
|
80
|
+
})
|
|
81
|
+
|
|
82
|
+
it('should unsubscribe from items not in target state', async () => {
|
|
83
|
+
const mockWatcher = { stop: vi.fn() }
|
|
84
|
+
const currentState = new Map([['Pair(bucket, key1)', mockWatcher as any]])
|
|
85
|
+
|
|
86
|
+
const actor = createActor(kvConsolidateState, {
|
|
87
|
+
input: {
|
|
88
|
+
kvm: { open: vi.fn() } as any,
|
|
89
|
+
connection: {} as any,
|
|
90
|
+
currentState,
|
|
91
|
+
targetState: new Map(),
|
|
92
|
+
},
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
96
|
+
actor.subscribe((snap) => {
|
|
97
|
+
if (snap.output) resolve(snap.output)
|
|
98
|
+
})
|
|
99
|
+
})
|
|
100
|
+
actor.start()
|
|
101
|
+
|
|
102
|
+
const result = await outputPromise
|
|
103
|
+
expect(mockWatcher.stop).toHaveBeenCalled()
|
|
104
|
+
expect(result.subscriptions.has('Pair(bucket, key1)')).toBe(false)
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
it('should handle unsubscribe errors gracefully', async () => {
|
|
108
|
+
const mockWatcher = {
|
|
109
|
+
stop: vi.fn(() => {
|
|
110
|
+
throw new Error('stop failed')
|
|
111
|
+
}),
|
|
112
|
+
}
|
|
113
|
+
const currentState = new Map([['Pair(bucket, key1)', mockWatcher as any]])
|
|
114
|
+
|
|
115
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
116
|
+
const actor = createActor(kvConsolidateState, {
|
|
117
|
+
input: {
|
|
118
|
+
kvm: { open: vi.fn() } as any,
|
|
119
|
+
connection: {} as any,
|
|
120
|
+
currentState,
|
|
121
|
+
targetState: new Map(),
|
|
122
|
+
},
|
|
123
|
+
})
|
|
124
|
+
|
|
125
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
126
|
+
actor.subscribe((snap) => {
|
|
127
|
+
if (snap.output) resolve(snap.output)
|
|
128
|
+
})
|
|
129
|
+
})
|
|
130
|
+
actor.start()
|
|
131
|
+
await outputPromise
|
|
132
|
+
consoleSpy.mockRestore()
|
|
133
|
+
|
|
134
|
+
expect(mockWatcher.stop).toHaveBeenCalled()
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
it('should subscribe to new items in target state', async () => {
|
|
138
|
+
const mockWatcher = {
|
|
139
|
+
[Symbol.asyncIterator]: () => ({
|
|
140
|
+
next: () => new Promise(() => {}),
|
|
141
|
+
}),
|
|
142
|
+
}
|
|
143
|
+
const mockKv = {
|
|
144
|
+
watch: vi.fn().mockResolvedValue(mockWatcher),
|
|
145
|
+
}
|
|
146
|
+
const mockKvm = {
|
|
147
|
+
open: vi.fn().mockResolvedValue(mockKv),
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const callback = vi.fn()
|
|
151
|
+
const targetState = new Map([
|
|
152
|
+
[
|
|
153
|
+
'Pair(bucket, key1)',
|
|
154
|
+
{
|
|
155
|
+
bucket: 'bucket',
|
|
156
|
+
key: 'key1',
|
|
157
|
+
callback,
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
])
|
|
161
|
+
|
|
162
|
+
const actor = createActor(kvConsolidateState, {
|
|
163
|
+
input: {
|
|
164
|
+
kvm: mockKvm as any,
|
|
165
|
+
connection: {} as any,
|
|
166
|
+
currentState: new Map(),
|
|
167
|
+
targetState: targetState as any,
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
|
|
171
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
172
|
+
actor.subscribe((snap) => {
|
|
173
|
+
if (snap.output) resolve(snap.output)
|
|
174
|
+
})
|
|
175
|
+
})
|
|
176
|
+
actor.start()
|
|
177
|
+
|
|
178
|
+
const result = await outputPromise
|
|
179
|
+
expect(mockKvm.open).toHaveBeenCalledWith('bucket')
|
|
180
|
+
expect(mockKv.watch).toHaveBeenCalled()
|
|
181
|
+
expect(result.subscriptions.has('Pair(bucket, key1)')).toBe(true)
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it('should keep existing subscriptions that are still in target', async () => {
|
|
185
|
+
const existingWatcher = { stop: vi.fn() } as any
|
|
186
|
+
const currentState = new Map([['Pair(bucket, key1)', existingWatcher]])
|
|
187
|
+
|
|
188
|
+
const callback = vi.fn()
|
|
189
|
+
const targetState = new Map([
|
|
190
|
+
[
|
|
191
|
+
'Pair(bucket, key1)',
|
|
192
|
+
{
|
|
193
|
+
bucket: 'bucket',
|
|
194
|
+
key: 'key1',
|
|
195
|
+
callback,
|
|
196
|
+
},
|
|
197
|
+
],
|
|
198
|
+
])
|
|
199
|
+
|
|
200
|
+
const mockKvm = { open: vi.fn() }
|
|
201
|
+
const actor = createActor(kvConsolidateState, {
|
|
202
|
+
input: {
|
|
203
|
+
kvm: mockKvm as any,
|
|
204
|
+
connection: {} as any,
|
|
205
|
+
currentState,
|
|
206
|
+
targetState: targetState as any,
|
|
207
|
+
},
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
211
|
+
actor.subscribe((snap) => {
|
|
212
|
+
if (snap.output) resolve(snap.output)
|
|
213
|
+
})
|
|
214
|
+
})
|
|
215
|
+
actor.start()
|
|
216
|
+
|
|
217
|
+
const result = await outputPromise
|
|
218
|
+
expect(existingWatcher.stop).not.toHaveBeenCalled()
|
|
219
|
+
expect(mockKvm.open).not.toHaveBeenCalled()
|
|
220
|
+
expect(result.subscriptions.get('Pair(bucket, key1)')).toBe(existingWatcher)
|
|
221
|
+
})
|
|
222
|
+
|
|
223
|
+
it('should invoke callback for watcher entries with JSON values', async () => {
|
|
224
|
+
let entryIndex = 0
|
|
225
|
+
let resolveDelivered: () => void
|
|
226
|
+
const deliveredPromise = new Promise<void>((r) => (resolveDelivered = r))
|
|
227
|
+
const entries = [
|
|
228
|
+
{ operation: 'PUT', string: () => '{"val":1}' },
|
|
229
|
+
{ operation: 'PUT', string: () => '{"val":2}' },
|
|
230
|
+
]
|
|
231
|
+
const mockWatcher = {
|
|
232
|
+
[Symbol.asyncIterator]: () => ({
|
|
233
|
+
next: () => {
|
|
234
|
+
if (entryIndex < entries.length) {
|
|
235
|
+
return Promise.resolve({ value: entries[entryIndex++], done: false })
|
|
236
|
+
}
|
|
237
|
+
resolveDelivered!()
|
|
238
|
+
return new Promise(() => {})
|
|
239
|
+
},
|
|
240
|
+
}),
|
|
241
|
+
}
|
|
242
|
+
const mockKv = { watch: vi.fn().mockResolvedValue(mockWatcher) }
|
|
243
|
+
const mockKvm = { open: vi.fn().mockResolvedValue(mockKv) }
|
|
244
|
+
|
|
245
|
+
const callback = vi.fn()
|
|
246
|
+
const targetState = new Map([['Pair(b, k)', { bucket: 'b', key: 'k', callback }]])
|
|
247
|
+
|
|
248
|
+
const actor = createActor(kvConsolidateState, {
|
|
249
|
+
input: {
|
|
250
|
+
kvm: mockKvm as any,
|
|
251
|
+
connection: {} as any,
|
|
252
|
+
currentState: new Map(),
|
|
253
|
+
targetState: targetState as any,
|
|
254
|
+
},
|
|
255
|
+
})
|
|
256
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
257
|
+
actor.subscribe((snap) => {
|
|
258
|
+
if (snap.output) resolve(snap.output)
|
|
259
|
+
})
|
|
260
|
+
})
|
|
261
|
+
actor.start()
|
|
262
|
+
await outputPromise
|
|
263
|
+
await deliveredPromise
|
|
264
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
265
|
+
|
|
266
|
+
expect(callback).toHaveBeenCalledTimes(2)
|
|
267
|
+
expect(callback).toHaveBeenCalledWith({ bucket: 'b', key: 'k', value: { val: 1 } })
|
|
268
|
+
expect(callback).toHaveBeenCalledWith({ bucket: 'b', key: 'k', value: { val: 2 } })
|
|
269
|
+
})
|
|
270
|
+
|
|
271
|
+
it('should fall back to string when JSON parsing fails in watcher', async () => {
|
|
272
|
+
let delivered = false
|
|
273
|
+
let resolveDelivered: () => void
|
|
274
|
+
const deliveredPromise = new Promise<void>((r) => (resolveDelivered = r))
|
|
275
|
+
const mockWatcher = {
|
|
276
|
+
[Symbol.asyncIterator]: () => ({
|
|
277
|
+
next: () => {
|
|
278
|
+
if (!delivered) {
|
|
279
|
+
delivered = true
|
|
280
|
+
return Promise.resolve({
|
|
281
|
+
value: { operation: 'PUT', string: () => 'not-json' },
|
|
282
|
+
done: false,
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
resolveDelivered!()
|
|
286
|
+
return new Promise(() => {})
|
|
287
|
+
},
|
|
288
|
+
}),
|
|
289
|
+
}
|
|
290
|
+
const mockKv = { watch: vi.fn().mockResolvedValue(mockWatcher) }
|
|
291
|
+
const mockKvm = { open: vi.fn().mockResolvedValue(mockKv) }
|
|
292
|
+
|
|
293
|
+
const callback = vi.fn()
|
|
294
|
+
const targetState = new Map([['Pair(b, k)', { bucket: 'b', key: 'k', callback }]])
|
|
295
|
+
|
|
296
|
+
const actor = createActor(kvConsolidateState, {
|
|
297
|
+
input: {
|
|
298
|
+
kvm: mockKvm as any,
|
|
299
|
+
connection: {} as any,
|
|
300
|
+
currentState: new Map(),
|
|
301
|
+
targetState: targetState as any,
|
|
302
|
+
},
|
|
303
|
+
})
|
|
304
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
305
|
+
actor.subscribe((snap) => {
|
|
306
|
+
if (snap.output) resolve(snap.output)
|
|
307
|
+
})
|
|
308
|
+
})
|
|
309
|
+
actor.start()
|
|
310
|
+
await outputPromise
|
|
311
|
+
await deliveredPromise
|
|
312
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
313
|
+
|
|
314
|
+
expect(callback).toHaveBeenCalledWith({ bucket: 'b', key: 'k', value: 'not-json' })
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('should skip DEL operations in watcher', async () => {
|
|
318
|
+
let delivered = false
|
|
319
|
+
let resolveDelivered: () => void
|
|
320
|
+
const deliveredPromise = new Promise<void>((r) => (resolveDelivered = r))
|
|
321
|
+
const mockWatcher = {
|
|
322
|
+
[Symbol.asyncIterator]: () => ({
|
|
323
|
+
next: () => {
|
|
324
|
+
if (!delivered) {
|
|
325
|
+
delivered = true
|
|
326
|
+
return Promise.resolve({
|
|
327
|
+
value: { operation: 'DEL', string: () => '{}' },
|
|
328
|
+
done: false,
|
|
329
|
+
})
|
|
330
|
+
}
|
|
331
|
+
resolveDelivered!()
|
|
332
|
+
return new Promise(() => {})
|
|
333
|
+
},
|
|
334
|
+
}),
|
|
335
|
+
}
|
|
336
|
+
const mockKv = { watch: vi.fn().mockResolvedValue(mockWatcher) }
|
|
337
|
+
const mockKvm = { open: vi.fn().mockResolvedValue(mockKv) }
|
|
338
|
+
|
|
339
|
+
const callback = vi.fn()
|
|
340
|
+
const targetState = new Map([['Pair(b, k)', { bucket: 'b', key: 'k', callback }]])
|
|
341
|
+
|
|
342
|
+
const actor = createActor(kvConsolidateState, {
|
|
343
|
+
input: {
|
|
344
|
+
kvm: mockKvm as any,
|
|
345
|
+
connection: {} as any,
|
|
346
|
+
currentState: new Map(),
|
|
347
|
+
targetState: targetState as any,
|
|
348
|
+
},
|
|
349
|
+
})
|
|
350
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
351
|
+
actor.subscribe((snap) => {
|
|
352
|
+
if (snap.output) resolve(snap.output)
|
|
353
|
+
})
|
|
354
|
+
})
|
|
355
|
+
actor.start()
|
|
356
|
+
await outputPromise
|
|
357
|
+
await deliveredPromise
|
|
358
|
+
await new Promise((r) => setTimeout(r, 10))
|
|
359
|
+
|
|
360
|
+
expect(callback).not.toHaveBeenCalled()
|
|
361
|
+
})
|
|
362
|
+
|
|
363
|
+
it('should handle watcher loop errors', async () => {
|
|
364
|
+
const mockWatcher = {
|
|
365
|
+
[Symbol.asyncIterator]: () => ({
|
|
366
|
+
next: () => Promise.reject(new Error('watcher broke')),
|
|
367
|
+
}),
|
|
368
|
+
}
|
|
369
|
+
const mockKv = { watch: vi.fn().mockResolvedValue(mockWatcher) }
|
|
370
|
+
const mockKvm = { open: vi.fn().mockResolvedValue(mockKv) }
|
|
371
|
+
|
|
372
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
373
|
+
const callback = vi.fn()
|
|
374
|
+
const targetState = new Map([['Pair(b, k)', { bucket: 'b', key: 'k', callback }]])
|
|
375
|
+
|
|
376
|
+
const actor = createActor(kvConsolidateState, {
|
|
377
|
+
input: {
|
|
378
|
+
kvm: mockKvm as any,
|
|
379
|
+
connection: {} as any,
|
|
380
|
+
currentState: new Map(),
|
|
381
|
+
targetState: targetState as any,
|
|
382
|
+
},
|
|
383
|
+
})
|
|
384
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
385
|
+
actor.subscribe((snap) => {
|
|
386
|
+
if (snap.output) resolve(snap.output)
|
|
387
|
+
})
|
|
388
|
+
})
|
|
389
|
+
actor.start()
|
|
390
|
+
await outputPromise
|
|
391
|
+
|
|
392
|
+
await vi.waitFor(() => {
|
|
393
|
+
expect(consoleSpy).toHaveBeenCalledWith(
|
|
394
|
+
expect.stringContaining('Watcher loop error'),
|
|
395
|
+
expect.any(Error),
|
|
396
|
+
)
|
|
397
|
+
})
|
|
398
|
+
consoleSpy.mockRestore()
|
|
399
|
+
})
|
|
400
|
+
|
|
401
|
+
it('should handle subscribe errors gracefully', async () => {
|
|
402
|
+
const mockKvm = {
|
|
403
|
+
open: vi.fn().mockRejectedValue(new Error('open failed')),
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const callback = vi.fn()
|
|
407
|
+
const targetState = new Map([
|
|
408
|
+
[
|
|
409
|
+
'Pair(bucket, key1)',
|
|
410
|
+
{
|
|
411
|
+
bucket: 'bucket',
|
|
412
|
+
key: 'key1',
|
|
413
|
+
callback,
|
|
414
|
+
},
|
|
415
|
+
],
|
|
416
|
+
])
|
|
417
|
+
|
|
418
|
+
const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
419
|
+
const actor = createActor(kvConsolidateState, {
|
|
420
|
+
input: {
|
|
421
|
+
kvm: mockKvm as any,
|
|
422
|
+
connection: {} as any,
|
|
423
|
+
currentState: new Map(),
|
|
424
|
+
targetState: targetState as any,
|
|
425
|
+
},
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
const outputPromise = new Promise<any>((resolve) => {
|
|
429
|
+
actor.subscribe((snap) => {
|
|
430
|
+
if (snap.output) resolve(snap.output)
|
|
431
|
+
})
|
|
432
|
+
})
|
|
433
|
+
actor.start()
|
|
434
|
+
|
|
435
|
+
const result = await outputPromise
|
|
436
|
+
consoleSpy.mockRestore()
|
|
437
|
+
expect(result.subscriptions.has('Pair(bucket, key1)')).toBe(false)
|
|
438
|
+
})
|
|
439
|
+
})
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { NatsConnection, QueuedIterator } from '@nats-io/nats-core'
|
|
2
|
+
import { Kvm, KvWatchEntry, KvWatchOptions } from '@nats-io/kv'
|
|
3
|
+
import { Pair } from '../utils'
|
|
4
|
+
import { fromPromise } from 'xstate'
|
|
5
|
+
|
|
6
|
+
export class KvSubscriptionKey extends Pair<string, string> {}
|
|
7
|
+
|
|
8
|
+
export type KvSubscriptionConfig = {
|
|
9
|
+
bucket: string
|
|
10
|
+
key: string
|
|
11
|
+
callback: (data: any) => void
|
|
12
|
+
opts?: KvWatchOptions
|
|
13
|
+
replayOnReconnect?: boolean
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export const kvConsolidateState = fromPromise(
|
|
17
|
+
async ({
|
|
18
|
+
input,
|
|
19
|
+
}: {
|
|
20
|
+
input: {
|
|
21
|
+
kvm: Kvm | null
|
|
22
|
+
connection: NatsConnection | null
|
|
23
|
+
currentState: Map<string, QueuedIterator<KvWatchEntry>>
|
|
24
|
+
targetState: Map<string, KvSubscriptionConfig>
|
|
25
|
+
}
|
|
26
|
+
}): Promise<{
|
|
27
|
+
subscriptions: Map<string, QueuedIterator<KvWatchEntry>>
|
|
28
|
+
}> => {
|
|
29
|
+
if (!input.connection || !input.kvm) {
|
|
30
|
+
throw new Error('NATS connection or KVM is not available')
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { currentState, targetState } = input
|
|
34
|
+
const syncedState = new Map(currentState)
|
|
35
|
+
|
|
36
|
+
// Unsubscribe from items that are in currentState but not in targetState
|
|
37
|
+
for (const [kvKey, subscription] of currentState) {
|
|
38
|
+
if (!targetState.has(kvKey)) {
|
|
39
|
+
try {
|
|
40
|
+
syncedState.delete(kvKey)
|
|
41
|
+
subscription.stop()
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error(`Error unsubscribing from subject "${kvKey}"`, error)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// Subscribe to new subjects that are in targetState but not in currentState
|
|
49
|
+
for (const [kvKey, config] of targetState) {
|
|
50
|
+
if (!currentState.has(kvKey)) {
|
|
51
|
+
// the problem is kv key is triggered on successive calls to consolidateState
|
|
52
|
+
// the match is not working?
|
|
53
|
+
try {
|
|
54
|
+
const kv = await input.kvm.open(config.bucket)
|
|
55
|
+
|
|
56
|
+
const watchOptions = config as KvWatchOptions
|
|
57
|
+
const watcher = await kv.watch(watchOptions)
|
|
58
|
+
|
|
59
|
+
syncedState.set(kvKey, watcher)
|
|
60
|
+
;(async () => {
|
|
61
|
+
try {
|
|
62
|
+
for await (const e of watcher) {
|
|
63
|
+
if (e.operation !== 'DEL') {
|
|
64
|
+
let parsedValue
|
|
65
|
+
try {
|
|
66
|
+
parsedValue = JSON.parse(e.string())
|
|
67
|
+
} catch {
|
|
68
|
+
parsedValue = e.string()
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
config.callback({
|
|
72
|
+
bucket: config.bucket,
|
|
73
|
+
key: config.key,
|
|
74
|
+
value: parsedValue,
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error(`KV_SUBSCRIBE (connected): Watcher loop error for ${kvKey}:`, error)
|
|
80
|
+
}
|
|
81
|
+
})()
|
|
82
|
+
} catch (error) {
|
|
83
|
+
console.error(`Error subscribing to subject "${kvKey}"`, error)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
subscriptions: syncedState,
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
)
|