@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,327 @@
|
|
|
1
|
+
import { NatsConnection, QueuedIterator } from '@nats-io/nats-core'
|
|
2
|
+
import { jetstream } from '@nats-io/jetstream'
|
|
3
|
+
import { KvEntry, Kvm, KvOptions, KvStatus, KvWatchEntry } from '@nats-io/kv'
|
|
4
|
+
import { assign, sendParent, setup } from 'xstate'
|
|
5
|
+
import { KvSubscriptionKey, KvSubscriptionConfig, kvConsolidateState } from '../actions/kv'
|
|
6
|
+
|
|
7
|
+
// internal events and events from nats connection
|
|
8
|
+
type InternalEvents = { type: 'ERROR'; error: Error }
|
|
9
|
+
|
|
10
|
+
// events which can be sent to the machine from the user
|
|
11
|
+
export type ExternalEvents =
|
|
12
|
+
| { type: 'KV.CONNECT'; connection: NatsConnection }
|
|
13
|
+
| { type: 'KV.CONNECTED' }
|
|
14
|
+
| { type: 'KV.DISCONNECTED' }
|
|
15
|
+
| {
|
|
16
|
+
type: 'KV.BUCKET_LIST'
|
|
17
|
+
bucket?: string
|
|
18
|
+
onResult: (result: KvStatus[] | string[] | { error: Error }) => void
|
|
19
|
+
}
|
|
20
|
+
| {
|
|
21
|
+
type: 'KV.BUCKET_CREATE'
|
|
22
|
+
bucket: string
|
|
23
|
+
onResult: (result: { ok: true } | { ok: false } | { error: Error }) => void
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: 'KV.BUCKET_DELETE'
|
|
27
|
+
bucket: string
|
|
28
|
+
onResult: (result: { ok: true } | { ok: false } | { error: Error }) => void
|
|
29
|
+
}
|
|
30
|
+
| {
|
|
31
|
+
type: 'KV.GET'
|
|
32
|
+
bucket: string
|
|
33
|
+
key: string
|
|
34
|
+
onResult: (result: KvEntry | null | { error: Error }) => void
|
|
35
|
+
}
|
|
36
|
+
| {
|
|
37
|
+
type: 'KV.PUT'
|
|
38
|
+
bucket: string
|
|
39
|
+
key: string
|
|
40
|
+
value: any
|
|
41
|
+
onResult: (result: { ok: true } | { error: Error }) => void
|
|
42
|
+
}
|
|
43
|
+
| {
|
|
44
|
+
type: 'KV.DELETE'
|
|
45
|
+
bucket: string
|
|
46
|
+
key: string
|
|
47
|
+
onResult: (result: { ok: true } | { error: Error }) => void
|
|
48
|
+
}
|
|
49
|
+
| { type: 'KV.SUBSCRIBE'; config: KvSubscriptionConfig }
|
|
50
|
+
| { type: 'KV.UNSUBSCRIBE'; bucket: string; key: string }
|
|
51
|
+
| { type: 'KV.UNSUBSCRIBE_ALL' }
|
|
52
|
+
|
|
53
|
+
export type Events = InternalEvents | ExternalEvents
|
|
54
|
+
|
|
55
|
+
export interface Context {
|
|
56
|
+
uid: string
|
|
57
|
+
cachedConnection: NatsConnection | null
|
|
58
|
+
cachedKvm: Kvm | null
|
|
59
|
+
kvmOpts?: KvOptions
|
|
60
|
+
subscriptions: Map<string, QueuedIterator<KvWatchEntry>>
|
|
61
|
+
subscriptionConfigs: Map<string, KvSubscriptionConfig>
|
|
62
|
+
syncRequired: number
|
|
63
|
+
error?: Error
|
|
64
|
+
}
|
|
65
|
+
export const kvManagerLogic = setup({
|
|
66
|
+
types: {
|
|
67
|
+
context: {} as Context,
|
|
68
|
+
events: {} as Events,
|
|
69
|
+
},
|
|
70
|
+
actors: {
|
|
71
|
+
kvConsolidateState: kvConsolidateState,
|
|
72
|
+
},
|
|
73
|
+
guards: {
|
|
74
|
+
hasPendingSync: ({ context }) => {
|
|
75
|
+
return context.syncRequired > 0
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
}).createMachine({
|
|
79
|
+
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOlgFcAjAKzEwBcB9XCAGzAGIBlAVQCEAUgFEAwgBVGASQByksZICCAGUlchAbQAMAXUSgADgHtYuerkP49IAB6IATJs0kAnAFYAzAEY7rgDQgAT3tXTxJNAA53KOiYrwBfOP80LDxCUgoaOiYCU1x0VlwTfCgOa1h6dHowEnQAMyqAJ2RXRyIOZJwCYjIqWgZmfFz8woIoLV0kECMTMwsrWwQANk0AFhJPNy8ffyCEHzsSV0jY2M8EpIxOtJ7M-swLQgZIbn5hcUZePi4RACVJPg0Ois01yc0mC08jnCJAA7O4Vu4YX5AvYVjDDnZ4YjXOcQB1Ut0Mn0mPd8I8qhAXoJRBIeNJPt8-gDxsDjKDLODEEiDu5FjDPOEkTtEJ4Yc5cfiuulellGKTyc9Pm8JCIlEIFD8PvxGf9ARMDGzZhzQAs4a4SHY7ItXIttiiEN5oTjcfhDBA4FZJWlWTNzMabIgALSLYUIYMSy4E6W3bJsMA+9nzRArOyhy3QiInU4RlJSm7EgZDApFKAJo1JhArTzmzTV23I3bpsLHLNRM6JPGRvNE2XyrKQMt+iswpEkRaChv2eGHHNXQkyu4WWCGAoQSpgRjldeDsEmkWY0IrFYebyTvZomcdr3zmOMMANBqGBo7-0LFaLC0CoX26KHFuthIEiAA */
|
|
80
|
+
initial: 'kv_idle',
|
|
81
|
+
context: {
|
|
82
|
+
uid: new Date().toISOString(),
|
|
83
|
+
cachedConnection: null,
|
|
84
|
+
cachedKvm: null,
|
|
85
|
+
kvmOpts: undefined,
|
|
86
|
+
subscriptions: new Map<string, QueuedIterator<KvWatchEntry>>(),
|
|
87
|
+
subscriptionConfigs: new Map<string, KvSubscriptionConfig>(),
|
|
88
|
+
syncRequired: 0,
|
|
89
|
+
},
|
|
90
|
+
on: {
|
|
91
|
+
'KV.SUBSCRIBE': {
|
|
92
|
+
actions: [
|
|
93
|
+
assign({
|
|
94
|
+
subscriptionConfigs: ({ context, event }) => {
|
|
95
|
+
const { config } = event
|
|
96
|
+
const newConfigs = new Map(context.subscriptionConfigs)
|
|
97
|
+
const newKvKey = KvSubscriptionKey.key(config.bucket, config.key)
|
|
98
|
+
newConfigs.set(newKvKey, config)
|
|
99
|
+
return newConfigs
|
|
100
|
+
},
|
|
101
|
+
syncRequired: ({ context }) => context.syncRequired + 1,
|
|
102
|
+
}),
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
'KV.UNSUBSCRIBE': {
|
|
106
|
+
actions: assign(({ context, event }) => {
|
|
107
|
+
const newConfigs = new Map(context.subscriptionConfigs)
|
|
108
|
+
const newKvKey = KvSubscriptionKey.key(event.bucket, event.key)
|
|
109
|
+
newConfigs.delete(newKvKey)
|
|
110
|
+
return {
|
|
111
|
+
subscriptionConfigs: newConfigs,
|
|
112
|
+
syncRequired: context.syncRequired + 1,
|
|
113
|
+
}
|
|
114
|
+
}),
|
|
115
|
+
},
|
|
116
|
+
'KV.UNSUBSCRIBE_ALL': {
|
|
117
|
+
actions: assign({
|
|
118
|
+
subscriptionConfigs: new Map(),
|
|
119
|
+
syncRequired: ({ context }) => context.syncRequired + 1,
|
|
120
|
+
}),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
states: {
|
|
124
|
+
kv_idle: {
|
|
125
|
+
on: {
|
|
126
|
+
'KV.CONNECT': {
|
|
127
|
+
target: 'kv_check_sync',
|
|
128
|
+
actions: [
|
|
129
|
+
assign({
|
|
130
|
+
cachedConnection: ({ event }) => event.connection,
|
|
131
|
+
cachedKvm: ({ event }) => new Kvm(event.connection),
|
|
132
|
+
}),
|
|
133
|
+
],
|
|
134
|
+
},
|
|
135
|
+
},
|
|
136
|
+
},
|
|
137
|
+
kv_disconnecting: {
|
|
138
|
+
target: 'kv_idle',
|
|
139
|
+
entry: [
|
|
140
|
+
// dont close the connection here, it will be closed by the nats connection machine
|
|
141
|
+
assign({
|
|
142
|
+
cachedConnection: null,
|
|
143
|
+
cachedKvm: null,
|
|
144
|
+
subscriptions: new Map<string, QueuedIterator<KvWatchEntry>>(),
|
|
145
|
+
}),
|
|
146
|
+
sendParent({ type: 'KV.DISCONNECTED' }),
|
|
147
|
+
],
|
|
148
|
+
},
|
|
149
|
+
kv_connected: {
|
|
150
|
+
entry: [sendParent({ type: 'KV.CONNECTED' })],
|
|
151
|
+
always: {
|
|
152
|
+
target: 'kv_syncing',
|
|
153
|
+
guard: 'hasPendingSync',
|
|
154
|
+
},
|
|
155
|
+
on: {
|
|
156
|
+
'KV.BUCKET_LIST': {
|
|
157
|
+
actions: async ({ context, event }) => {
|
|
158
|
+
try {
|
|
159
|
+
if (!context.cachedKvm) {
|
|
160
|
+
event.onResult({ error: new Error('KVM not initialized') })
|
|
161
|
+
return
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const results = []
|
|
165
|
+
if (event.bucket) {
|
|
166
|
+
const bucket = await context.cachedKvm.open(event.bucket)
|
|
167
|
+
for await (const key of await bucket.keys()) {
|
|
168
|
+
results.push(key)
|
|
169
|
+
}
|
|
170
|
+
event.onResult(results)
|
|
171
|
+
} else {
|
|
172
|
+
for await (const status of await context.cachedKvm.list()) {
|
|
173
|
+
results.push(status)
|
|
174
|
+
}
|
|
175
|
+
event.onResult(results)
|
|
176
|
+
}
|
|
177
|
+
} catch (error) {
|
|
178
|
+
event.onResult({ error: error as Error })
|
|
179
|
+
}
|
|
180
|
+
},
|
|
181
|
+
},
|
|
182
|
+
'KV.BUCKET_CREATE': {
|
|
183
|
+
actions: async ({ context, event }) => {
|
|
184
|
+
try {
|
|
185
|
+
if (!context.cachedKvm) throw new Error('KVM not initialized')
|
|
186
|
+
|
|
187
|
+
for await (const status of context.cachedKvm.list()) {
|
|
188
|
+
if (status.bucket === event.bucket) {
|
|
189
|
+
event.onResult?.({ ok: false })
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
await context.cachedKvm.create(event.bucket)
|
|
195
|
+
event.onResult?.({ ok: true })
|
|
196
|
+
} catch (error) {
|
|
197
|
+
event.onResult?.({ error: error as Error })
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
},
|
|
201
|
+
'KV.BUCKET_DELETE': {
|
|
202
|
+
actions: async ({ event }) => {
|
|
203
|
+
try {
|
|
204
|
+
try {
|
|
205
|
+
// workaround: theres no delete bucket method on the kvm
|
|
206
|
+
const connection = (event as any).connection as NatsConnection
|
|
207
|
+
const js = jetstream(connection!)
|
|
208
|
+
const jsm = await js.jetstreamManager()
|
|
209
|
+
const res = await jsm.streams.delete(`KV_${event.bucket}`)
|
|
210
|
+
event.onResult({ ok: res })
|
|
211
|
+
} catch (streamError) {
|
|
212
|
+
// Stream deletion might fail, but that's okay
|
|
213
|
+
event.onResult({ ok: false })
|
|
214
|
+
}
|
|
215
|
+
} catch (error) {
|
|
216
|
+
event.onResult({ error: error as Error })
|
|
217
|
+
}
|
|
218
|
+
},
|
|
219
|
+
},
|
|
220
|
+
'KV.GET': {
|
|
221
|
+
actions: async ({ context, event }) => {
|
|
222
|
+
try {
|
|
223
|
+
const kv = await context.cachedKvm?.open(event.bucket)
|
|
224
|
+
if (!kv) {
|
|
225
|
+
event.onResult({ error: new Error(`Bucket '${event.bucket}' not found`) })
|
|
226
|
+
return
|
|
227
|
+
}
|
|
228
|
+
const entry = await kv.get(event.key)
|
|
229
|
+
event.onResult(entry)
|
|
230
|
+
} catch (error) {
|
|
231
|
+
event.onResult({ error: error as Error })
|
|
232
|
+
}
|
|
233
|
+
},
|
|
234
|
+
},
|
|
235
|
+
'KV.PUT': {
|
|
236
|
+
actions: async ({ context, event }) => {
|
|
237
|
+
try {
|
|
238
|
+
const kv = await context.cachedKvm?.open(event.bucket)
|
|
239
|
+
if (!kv) {
|
|
240
|
+
event.onResult({ error: new Error(`Bucket '${event.bucket}' not found`) })
|
|
241
|
+
return
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
await kv.put(event.key, event.value)
|
|
245
|
+
event.onResult({ ok: true })
|
|
246
|
+
} catch (error) {
|
|
247
|
+
event.onResult({ error: error as Error })
|
|
248
|
+
}
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
'KV.DELETE': {
|
|
252
|
+
actions: async ({ context, event }) => {
|
|
253
|
+
try {
|
|
254
|
+
const kv = await context.cachedKvm?.open(event.bucket)
|
|
255
|
+
if (!kv) {
|
|
256
|
+
event.onResult({ error: new Error(`Bucket '${event.bucket}' not found`) })
|
|
257
|
+
return
|
|
258
|
+
}
|
|
259
|
+
await kv.delete(event.key)
|
|
260
|
+
event.onResult({ ok: true })
|
|
261
|
+
} catch (error) {
|
|
262
|
+
event.onResult({ error: error as Error })
|
|
263
|
+
}
|
|
264
|
+
},
|
|
265
|
+
},
|
|
266
|
+
},
|
|
267
|
+
},
|
|
268
|
+
kv_check_sync: {
|
|
269
|
+
always: [
|
|
270
|
+
{
|
|
271
|
+
target: 'kv_syncing',
|
|
272
|
+
guard: 'hasPendingSync',
|
|
273
|
+
},
|
|
274
|
+
{
|
|
275
|
+
target: 'kv_connected',
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
},
|
|
279
|
+
kv_syncing: {
|
|
280
|
+
entry: [
|
|
281
|
+
({ context }) => {
|
|
282
|
+
// either going to be 0 or 1 (if there were multiple syncs pending)
|
|
283
|
+
context.syncRequired = Math.min(context.syncRequired - 1, 1)
|
|
284
|
+
},
|
|
285
|
+
],
|
|
286
|
+
invoke: {
|
|
287
|
+
id: 'single-instance-sync',
|
|
288
|
+
src: 'kvConsolidateState',
|
|
289
|
+
input: ({ context }: { context: Context }) => ({
|
|
290
|
+
kvm: context.cachedKvm!,
|
|
291
|
+
connection: context.cachedConnection!,
|
|
292
|
+
currentState: context.subscriptions,
|
|
293
|
+
targetState: context.subscriptionConfigs,
|
|
294
|
+
}),
|
|
295
|
+
onDone: {
|
|
296
|
+
target: 'kv_connected',
|
|
297
|
+
actions: [
|
|
298
|
+
() => {
|
|
299
|
+
// console.log('kvConsolidateState onDone')
|
|
300
|
+
},
|
|
301
|
+
assign(({ event }) => ({
|
|
302
|
+
subscriptions: event.output.subscriptions,
|
|
303
|
+
})),
|
|
304
|
+
],
|
|
305
|
+
},
|
|
306
|
+
onError: {
|
|
307
|
+
target: 'kv_error',
|
|
308
|
+
actions: [
|
|
309
|
+
assign({
|
|
310
|
+
error: ({ event }) => {
|
|
311
|
+
console.error('kvConsolidateState onError', event.error)
|
|
312
|
+
return event.error as Error
|
|
313
|
+
},
|
|
314
|
+
}),
|
|
315
|
+
],
|
|
316
|
+
},
|
|
317
|
+
},
|
|
318
|
+
},
|
|
319
|
+
kv_error: {
|
|
320
|
+
on: {
|
|
321
|
+
'KV.CONNECT': {
|
|
322
|
+
target: 'kv_check_sync',
|
|
323
|
+
},
|
|
324
|
+
},
|
|
325
|
+
},
|
|
326
|
+
},
|
|
327
|
+
})
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { createActor, fromPromise, createMachine, sendParent } from 'xstate'
|
|
3
|
+
import { natsMachine } from './root'
|
|
4
|
+
|
|
5
|
+
vi.mock('@nats-io/nats-core', () => ({
|
|
6
|
+
wsconnect: vi.fn(),
|
|
7
|
+
credsAuthenticator: vi.fn(),
|
|
8
|
+
}))
|
|
9
|
+
|
|
10
|
+
vi.mock('@nats-io/kv', () => ({
|
|
11
|
+
Kvm: vi.fn().mockImplementation(() => ({})),
|
|
12
|
+
}))
|
|
13
|
+
|
|
14
|
+
const mockSubjectMachine = createMachine({
|
|
15
|
+
initial: 'idle',
|
|
16
|
+
states: {
|
|
17
|
+
idle: {
|
|
18
|
+
on: {
|
|
19
|
+
'SUBJECT.CONNECT': { target: 'connected' },
|
|
20
|
+
'SUBJECT.DISCONNECTED': { target: 'idle' },
|
|
21
|
+
},
|
|
22
|
+
},
|
|
23
|
+
connected: {
|
|
24
|
+
entry: sendParent({ type: 'SUBJECT.CONNECTED' }),
|
|
25
|
+
on: {
|
|
26
|
+
'SUBJECT.*': {},
|
|
27
|
+
'SUBJECT.DISCONNECTED': { target: 'idle' },
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
const mockKvMachine = createMachine({
|
|
34
|
+
initial: 'idle',
|
|
35
|
+
states: {
|
|
36
|
+
idle: {
|
|
37
|
+
on: {
|
|
38
|
+
'KV.CONNECT': { target: 'connected' },
|
|
39
|
+
'KV.DISCONNECTED': { target: 'idle' },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
connected: {
|
|
43
|
+
entry: sendParent({ type: 'KV.CONNECTED' }),
|
|
44
|
+
on: {
|
|
45
|
+
'KV.*': {},
|
|
46
|
+
'KV.DISCONNECTED': { target: 'idle' },
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
})
|
|
51
|
+
|
|
52
|
+
const mockConnection = {
|
|
53
|
+
drain: vi.fn().mockResolvedValue(undefined),
|
|
54
|
+
close: vi.fn().mockResolvedValue(undefined),
|
|
55
|
+
getServer: vi.fn().mockReturnValue('ws://localhost:4222'),
|
|
56
|
+
status: () => ({
|
|
57
|
+
[Symbol.asyncIterator]: () => ({
|
|
58
|
+
next: () => new Promise(() => {}),
|
|
59
|
+
}),
|
|
60
|
+
}),
|
|
61
|
+
} as any
|
|
62
|
+
|
|
63
|
+
function createTestMachine() {
|
|
64
|
+
return natsMachine.provide({
|
|
65
|
+
actors: {
|
|
66
|
+
connectToNats: fromPromise(async () => mockConnection),
|
|
67
|
+
disconnectNats: fromPromise(async () => {}),
|
|
68
|
+
subject: mockSubjectMachine,
|
|
69
|
+
kv: mockKvMachine,
|
|
70
|
+
},
|
|
71
|
+
})
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function configureAndConnect(actor: any) {
|
|
75
|
+
actor.send({
|
|
76
|
+
type: 'CONFIGURE',
|
|
77
|
+
config: { opts: { servers: ['ws://localhost:4222'] }, maxRetries: 3 },
|
|
78
|
+
})
|
|
79
|
+
actor.send({ type: 'CONNECT' })
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
describe('natsMachine', () => {
|
|
83
|
+
beforeEach(() => {
|
|
84
|
+
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
85
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
it('should start in not_configured state', () => {
|
|
89
|
+
const actor = createActor(createTestMachine())
|
|
90
|
+
actor.start()
|
|
91
|
+
expect(actor.getSnapshot().value).toBe('not_configured')
|
|
92
|
+
actor.stop()
|
|
93
|
+
})
|
|
94
|
+
|
|
95
|
+
it('should transition to configured on CONFIGURE', () => {
|
|
96
|
+
const actor = createActor(createTestMachine())
|
|
97
|
+
actor.start()
|
|
98
|
+
|
|
99
|
+
actor.send({
|
|
100
|
+
type: 'CONFIGURE',
|
|
101
|
+
config: { opts: { servers: ['ws://localhost:4222'] }, maxRetries: 3 },
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
expect(actor.getSnapshot().value).toBe('configured')
|
|
105
|
+
expect(actor.getSnapshot().context.natsConfig).toEqual({
|
|
106
|
+
opts: { servers: ['ws://localhost:4222'] },
|
|
107
|
+
maxRetries: 3,
|
|
108
|
+
})
|
|
109
|
+
actor.stop()
|
|
110
|
+
})
|
|
111
|
+
|
|
112
|
+
it('should reach connected state after CONNECT', async () => {
|
|
113
|
+
const actor = createActor(createTestMachine())
|
|
114
|
+
actor.start()
|
|
115
|
+
configureAndConnect(actor)
|
|
116
|
+
|
|
117
|
+
await vi.waitFor(() => {
|
|
118
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
119
|
+
})
|
|
120
|
+
expect(actor.getSnapshot().context.connection).toBe(mockConnection)
|
|
121
|
+
expect(actor.getSnapshot().context.subjectManagerReady).toBe(true)
|
|
122
|
+
expect(actor.getSnapshot().context.kvManagerReady).toBe(true)
|
|
123
|
+
actor.stop()
|
|
124
|
+
})
|
|
125
|
+
|
|
126
|
+
it('should transition to closed on DISCONNECT from connected', async () => {
|
|
127
|
+
const actor = createActor(createTestMachine())
|
|
128
|
+
actor.start()
|
|
129
|
+
configureAndConnect(actor)
|
|
130
|
+
|
|
131
|
+
await vi.waitFor(() => {
|
|
132
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
133
|
+
})
|
|
134
|
+
actor.send({ type: 'DISCONNECT' })
|
|
135
|
+
|
|
136
|
+
await vi.waitFor(() => {
|
|
137
|
+
expect(actor.getSnapshot().value).toBe('closed')
|
|
138
|
+
})
|
|
139
|
+
actor.stop()
|
|
140
|
+
})
|
|
141
|
+
|
|
142
|
+
it('should transition to error when connection fails', async () => {
|
|
143
|
+
const failError = new Error('connection failed')
|
|
144
|
+
const failMachine = natsMachine.provide({
|
|
145
|
+
actors: {
|
|
146
|
+
connectToNats: fromPromise(async () => {
|
|
147
|
+
throw failError
|
|
148
|
+
}),
|
|
149
|
+
disconnectNats: fromPromise(async () => {}),
|
|
150
|
+
subject: mockSubjectMachine,
|
|
151
|
+
kv: mockKvMachine,
|
|
152
|
+
},
|
|
153
|
+
})
|
|
154
|
+
|
|
155
|
+
const actor = createActor(failMachine)
|
|
156
|
+
actor.start()
|
|
157
|
+
configureAndConnect(actor)
|
|
158
|
+
|
|
159
|
+
await vi.waitFor(() => {
|
|
160
|
+
expect(actor.getSnapshot().value).toBe('error')
|
|
161
|
+
})
|
|
162
|
+
expect(actor.getSnapshot().context.error).toBe(failError)
|
|
163
|
+
expect(actor.getSnapshot().context.retries).toBe(1)
|
|
164
|
+
actor.stop()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should reset from error state', async () => {
|
|
168
|
+
const failMachine = natsMachine.provide({
|
|
169
|
+
actors: {
|
|
170
|
+
connectToNats: fromPromise(async () => {
|
|
171
|
+
throw new Error('fail')
|
|
172
|
+
}),
|
|
173
|
+
disconnectNats: fromPromise(async () => {}),
|
|
174
|
+
subject: mockSubjectMachine,
|
|
175
|
+
kv: mockKvMachine,
|
|
176
|
+
},
|
|
177
|
+
})
|
|
178
|
+
|
|
179
|
+
const actor = createActor(failMachine)
|
|
180
|
+
actor.start()
|
|
181
|
+
configureAndConnect(actor)
|
|
182
|
+
|
|
183
|
+
await vi.waitFor(() => {
|
|
184
|
+
expect(actor.getSnapshot().value).toBe('error')
|
|
185
|
+
})
|
|
186
|
+
|
|
187
|
+
actor.send({ type: 'RESET' })
|
|
188
|
+
expect(actor.getSnapshot().value).toBe('not_configured')
|
|
189
|
+
expect(actor.getSnapshot().context.natsConfig).toBeUndefined()
|
|
190
|
+
expect(actor.getSnapshot().context.connection).toBeNull()
|
|
191
|
+
expect(actor.getSnapshot().context.retries).toBe(0)
|
|
192
|
+
actor.stop()
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it('should reset from configured state', () => {
|
|
196
|
+
const actor = createActor(createTestMachine())
|
|
197
|
+
actor.start()
|
|
198
|
+
|
|
199
|
+
actor.send({
|
|
200
|
+
type: 'CONFIGURE',
|
|
201
|
+
config: { opts: { servers: ['ws://localhost:4222'] }, maxRetries: 3 },
|
|
202
|
+
})
|
|
203
|
+
actor.send({ type: 'RESET' })
|
|
204
|
+
|
|
205
|
+
expect(actor.getSnapshot().value).toBe('not_configured')
|
|
206
|
+
actor.stop()
|
|
207
|
+
})
|
|
208
|
+
|
|
209
|
+
it('should reconnect from closed state', async () => {
|
|
210
|
+
const actor = createActor(createTestMachine())
|
|
211
|
+
actor.start()
|
|
212
|
+
configureAndConnect(actor)
|
|
213
|
+
|
|
214
|
+
await vi.waitFor(() => {
|
|
215
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
216
|
+
})
|
|
217
|
+
actor.send({ type: 'DISCONNECT' })
|
|
218
|
+
await vi.waitFor(() => {
|
|
219
|
+
expect(actor.getSnapshot().value).toBe('closed')
|
|
220
|
+
})
|
|
221
|
+
|
|
222
|
+
actor.send({ type: 'CONNECT' })
|
|
223
|
+
await vi.waitFor(() => {
|
|
224
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
225
|
+
})
|
|
226
|
+
actor.stop()
|
|
227
|
+
})
|
|
228
|
+
|
|
229
|
+
it('should reset from closed state', async () => {
|
|
230
|
+
const actor = createActor(createTestMachine())
|
|
231
|
+
actor.start()
|
|
232
|
+
configureAndConnect(actor)
|
|
233
|
+
|
|
234
|
+
await vi.waitFor(() => {
|
|
235
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
236
|
+
})
|
|
237
|
+
actor.send({ type: 'DISCONNECT' })
|
|
238
|
+
await vi.waitFor(() => {
|
|
239
|
+
expect(actor.getSnapshot().value).toBe('closed')
|
|
240
|
+
})
|
|
241
|
+
actor.send({ type: 'RESET' })
|
|
242
|
+
|
|
243
|
+
expect(actor.getSnapshot().value).toBe('not_configured')
|
|
244
|
+
actor.stop()
|
|
245
|
+
})
|
|
246
|
+
|
|
247
|
+
it('should transition to closed on CLOSE event from connected', async () => {
|
|
248
|
+
const actor = createActor(createTestMachine())
|
|
249
|
+
actor.start()
|
|
250
|
+
configureAndConnect(actor)
|
|
251
|
+
|
|
252
|
+
await vi.waitFor(() => {
|
|
253
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
254
|
+
})
|
|
255
|
+
actor.send({ type: 'CLOSE' })
|
|
256
|
+
|
|
257
|
+
await vi.waitFor(() => {
|
|
258
|
+
expect(actor.getSnapshot().value).toBe('closed')
|
|
259
|
+
})
|
|
260
|
+
actor.stop()
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
it('should transition to error when disconnect fails', async () => {
|
|
264
|
+
const disconnectError = new Error('disconnect failed')
|
|
265
|
+
const failDisconnectMachine = natsMachine.provide({
|
|
266
|
+
actors: {
|
|
267
|
+
connectToNats: fromPromise(async () => mockConnection),
|
|
268
|
+
disconnectNats: fromPromise(async () => {
|
|
269
|
+
throw disconnectError
|
|
270
|
+
}),
|
|
271
|
+
subject: mockSubjectMachine,
|
|
272
|
+
kv: mockKvMachine,
|
|
273
|
+
},
|
|
274
|
+
})
|
|
275
|
+
|
|
276
|
+
const actor = createActor(failDisconnectMachine)
|
|
277
|
+
actor.start()
|
|
278
|
+
configureAndConnect(actor)
|
|
279
|
+
|
|
280
|
+
await vi.waitFor(() => {
|
|
281
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
282
|
+
})
|
|
283
|
+
actor.send({ type: 'DISCONNECT' })
|
|
284
|
+
|
|
285
|
+
await vi.waitFor(() => {
|
|
286
|
+
expect(actor.getSnapshot().value).toBe('error')
|
|
287
|
+
})
|
|
288
|
+
expect(actor.getSnapshot().context.error).toBe(disconnectError)
|
|
289
|
+
actor.stop()
|
|
290
|
+
})
|
|
291
|
+
|
|
292
|
+
it('should forward SUBJECT.* events when connected', async () => {
|
|
293
|
+
const actor = createActor(createTestMachine())
|
|
294
|
+
actor.start()
|
|
295
|
+
configureAndConnect(actor)
|
|
296
|
+
|
|
297
|
+
await vi.waitFor(() => {
|
|
298
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
299
|
+
})
|
|
300
|
+
|
|
301
|
+
expect(() => {
|
|
302
|
+
actor.send({
|
|
303
|
+
type: 'SUBJECT.SUBSCRIBE',
|
|
304
|
+
config: { subject: 'test', callback: vi.fn() },
|
|
305
|
+
} as any)
|
|
306
|
+
}).not.toThrow()
|
|
307
|
+
|
|
308
|
+
actor.stop()
|
|
309
|
+
})
|
|
310
|
+
|
|
311
|
+
it('should forward KV.* events when connected', async () => {
|
|
312
|
+
const actor = createActor(createTestMachine())
|
|
313
|
+
actor.start()
|
|
314
|
+
configureAndConnect(actor)
|
|
315
|
+
|
|
316
|
+
await vi.waitFor(() => {
|
|
317
|
+
expect(actor.getSnapshot().value).toBe('connected')
|
|
318
|
+
})
|
|
319
|
+
|
|
320
|
+
expect(() => {
|
|
321
|
+
actor.send({
|
|
322
|
+
type: 'KV.SUBSCRIBE',
|
|
323
|
+
config: { bucket: 'test', key: 'k', callback: vi.fn() },
|
|
324
|
+
} as any)
|
|
325
|
+
}).not.toThrow()
|
|
326
|
+
|
|
327
|
+
actor.stop()
|
|
328
|
+
})
|
|
329
|
+
})
|