@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,720 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
+
import { createActor, fromPromise, sendTo, setup, assign } from 'xstate'
|
|
3
|
+
import { kvManagerLogic } from './kv'
|
|
4
|
+
|
|
5
|
+
vi.mock('@nats-io/nats-core', () => ({
|
|
6
|
+
wsconnect: vi.fn(),
|
|
7
|
+
}))
|
|
8
|
+
|
|
9
|
+
// We'll set up specific Kvm mock behavior per test
|
|
10
|
+
const mockKvStore = {
|
|
11
|
+
get: vi.fn(),
|
|
12
|
+
put: vi.fn(),
|
|
13
|
+
delete: vi.fn(),
|
|
14
|
+
keys: vi.fn(),
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const mockKvmInstance = {
|
|
18
|
+
open: vi.fn().mockResolvedValue(mockKvStore),
|
|
19
|
+
list: vi.fn(),
|
|
20
|
+
create: vi.fn(),
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
vi.mock('@nats-io/kv', () => ({
|
|
24
|
+
Kvm: class MockKvm {
|
|
25
|
+
open = mockKvmInstance.open
|
|
26
|
+
list = mockKvmInstance.list
|
|
27
|
+
create = mockKvmInstance.create
|
|
28
|
+
},
|
|
29
|
+
}))
|
|
30
|
+
|
|
31
|
+
vi.mock('@nats-io/jetstream', () => ({
|
|
32
|
+
jetstream: vi.fn(() => ({
|
|
33
|
+
jetstreamManager: vi.fn().mockResolvedValue({
|
|
34
|
+
streams: {
|
|
35
|
+
delete: vi.fn().mockResolvedValue(true),
|
|
36
|
+
},
|
|
37
|
+
}),
|
|
38
|
+
})),
|
|
39
|
+
}))
|
|
40
|
+
|
|
41
|
+
function createMockConnection() {
|
|
42
|
+
return {
|
|
43
|
+
subscribe: vi.fn(),
|
|
44
|
+
publish: vi.fn(),
|
|
45
|
+
status: () => ({
|
|
46
|
+
[Symbol.asyncIterator]: () => ({
|
|
47
|
+
next: () => new Promise(() => {}),
|
|
48
|
+
}),
|
|
49
|
+
}),
|
|
50
|
+
} as any
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Parent machine wrapper so sendParent works
|
|
54
|
+
function createParentMachine(kvLogic?: any) {
|
|
55
|
+
const actualLogic = kvLogic || kvManagerLogic
|
|
56
|
+
return setup({
|
|
57
|
+
types: {
|
|
58
|
+
context: {} as { childState: string },
|
|
59
|
+
events: {} as any,
|
|
60
|
+
},
|
|
61
|
+
actors: {
|
|
62
|
+
kv: actualLogic,
|
|
63
|
+
},
|
|
64
|
+
}).createMachine({
|
|
65
|
+
initial: 'active',
|
|
66
|
+
context: { childState: '' },
|
|
67
|
+
invoke: {
|
|
68
|
+
src: 'kv',
|
|
69
|
+
id: 'kv',
|
|
70
|
+
},
|
|
71
|
+
on: {
|
|
72
|
+
'KV.*': {
|
|
73
|
+
actions: sendTo('kv', ({ event }: any) => {
|
|
74
|
+
return { ...event }
|
|
75
|
+
}),
|
|
76
|
+
},
|
|
77
|
+
'KV.CONNECTED': {
|
|
78
|
+
actions: assign({ childState: 'connected' }),
|
|
79
|
+
},
|
|
80
|
+
'KV.DISCONNECTED': {
|
|
81
|
+
actions: assign({ childState: 'disconnected' }),
|
|
82
|
+
},
|
|
83
|
+
},
|
|
84
|
+
states: {
|
|
85
|
+
active: {},
|
|
86
|
+
},
|
|
87
|
+
})
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
describe('kvManagerLogic', () => {
|
|
91
|
+
beforeEach(() => {
|
|
92
|
+
vi.spyOn(console, 'log').mockImplementation(() => {})
|
|
93
|
+
vi.spyOn(console, 'error').mockImplementation(() => {})
|
|
94
|
+
})
|
|
95
|
+
|
|
96
|
+
it('should start in kv_idle state', () => {
|
|
97
|
+
const parentActor = createActor(createParentMachine())
|
|
98
|
+
parentActor.start()
|
|
99
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
100
|
+
expect(childSnap?.value).toBe('kv_idle')
|
|
101
|
+
parentActor.stop()
|
|
102
|
+
})
|
|
103
|
+
|
|
104
|
+
it('should have empty initial context', () => {
|
|
105
|
+
const parentActor = createActor(createParentMachine())
|
|
106
|
+
parentActor.start()
|
|
107
|
+
const ctx = parentActor.getSnapshot().children.kv?.getSnapshot()?.context
|
|
108
|
+
expect(ctx?.cachedConnection).toBeNull()
|
|
109
|
+
expect(ctx?.cachedKvm).toBeNull()
|
|
110
|
+
expect(ctx?.subscriptions.size).toBe(0)
|
|
111
|
+
expect(ctx?.subscriptionConfigs.size).toBe(0)
|
|
112
|
+
expect(ctx?.syncRequired).toBe(0)
|
|
113
|
+
parentActor.stop()
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
it('should transition to kv_connected on KV.CONNECT (no pending sync)', () => {
|
|
117
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
118
|
+
actors: {
|
|
119
|
+
kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })),
|
|
120
|
+
},
|
|
121
|
+
})
|
|
122
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
123
|
+
parentActor.start()
|
|
124
|
+
|
|
125
|
+
const connection = createMockConnection()
|
|
126
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
127
|
+
|
|
128
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
129
|
+
expect(childSnap?.value).toBe('kv_connected')
|
|
130
|
+
expect(childSnap?.context.cachedConnection).toBe(connection)
|
|
131
|
+
expect(childSnap?.context.cachedKvm).not.toBeNull()
|
|
132
|
+
parentActor.stop()
|
|
133
|
+
})
|
|
134
|
+
|
|
135
|
+
it('should add subscription config on KV.SUBSCRIBE', () => {
|
|
136
|
+
const parentActor = createActor(createParentMachine())
|
|
137
|
+
parentActor.start()
|
|
138
|
+
|
|
139
|
+
parentActor.send({
|
|
140
|
+
type: 'KV.SUBSCRIBE',
|
|
141
|
+
config: { bucket: 'test-bucket', key: 'test-key', callback: vi.fn() },
|
|
142
|
+
})
|
|
143
|
+
|
|
144
|
+
const ctx = parentActor.getSnapshot().children.kv?.getSnapshot()?.context
|
|
145
|
+
const key = 'Pair(test-bucket, test-key)'
|
|
146
|
+
expect(ctx?.subscriptionConfigs.has(key)).toBe(true)
|
|
147
|
+
expect(ctx?.syncRequired).toBe(1)
|
|
148
|
+
parentActor.stop()
|
|
149
|
+
})
|
|
150
|
+
|
|
151
|
+
it('should remove subscription config on KV.UNSUBSCRIBE', () => {
|
|
152
|
+
const parentActor = createActor(createParentMachine())
|
|
153
|
+
parentActor.start()
|
|
154
|
+
|
|
155
|
+
parentActor.send({
|
|
156
|
+
type: 'KV.SUBSCRIBE',
|
|
157
|
+
config: { bucket: 'b', key: 'k', callback: vi.fn() },
|
|
158
|
+
})
|
|
159
|
+
parentActor.send({ type: 'KV.UNSUBSCRIBE', bucket: 'b', key: 'k' })
|
|
160
|
+
|
|
161
|
+
const ctx = parentActor.getSnapshot().children.kv?.getSnapshot()?.context
|
|
162
|
+
expect(ctx?.subscriptionConfigs.has('Pair(b, k)')).toBe(false)
|
|
163
|
+
expect(ctx?.syncRequired).toBe(2)
|
|
164
|
+
parentActor.stop()
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
it('should clear all configs on KV.UNSUBSCRIBE_ALL', () => {
|
|
168
|
+
const parentActor = createActor(createParentMachine())
|
|
169
|
+
parentActor.start()
|
|
170
|
+
|
|
171
|
+
parentActor.send({
|
|
172
|
+
type: 'KV.SUBSCRIBE',
|
|
173
|
+
config: { bucket: 'b1', key: 'k1', callback: vi.fn() },
|
|
174
|
+
})
|
|
175
|
+
parentActor.send({
|
|
176
|
+
type: 'KV.SUBSCRIBE',
|
|
177
|
+
config: { bucket: 'b2', key: 'k2', callback: vi.fn() },
|
|
178
|
+
})
|
|
179
|
+
parentActor.send({ type: 'KV.UNSUBSCRIBE_ALL' })
|
|
180
|
+
|
|
181
|
+
const ctx = parentActor.getSnapshot().children.kv?.getSnapshot()?.context
|
|
182
|
+
expect(ctx?.subscriptionConfigs.size).toBe(0)
|
|
183
|
+
parentActor.stop()
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
it('should sync when connecting with pending subscriptions', async () => {
|
|
187
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
188
|
+
actors: {
|
|
189
|
+
kvConsolidateState: fromPromise(async () => ({
|
|
190
|
+
subscriptions: new Map([['Pair(b, k)', {} as any]]),
|
|
191
|
+
})),
|
|
192
|
+
},
|
|
193
|
+
})
|
|
194
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
195
|
+
parentActor.start()
|
|
196
|
+
|
|
197
|
+
parentActor.send({
|
|
198
|
+
type: 'KV.SUBSCRIBE',
|
|
199
|
+
config: { bucket: 'b', key: 'k', callback: vi.fn() },
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
const connection = createMockConnection()
|
|
203
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
204
|
+
|
|
205
|
+
await vi.waitFor(() => {
|
|
206
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
207
|
+
expect(childSnap?.value).toBe('kv_connected')
|
|
208
|
+
})
|
|
209
|
+
parentActor.stop()
|
|
210
|
+
})
|
|
211
|
+
|
|
212
|
+
it('should handle KV.BUCKET_LIST with bucket name', async () => {
|
|
213
|
+
mockKvStore.keys.mockResolvedValue({
|
|
214
|
+
[Symbol.asyncIterator]: () => {
|
|
215
|
+
let i = 0
|
|
216
|
+
const keys = ['key1', 'key2']
|
|
217
|
+
return {
|
|
218
|
+
next: () =>
|
|
219
|
+
i < keys.length
|
|
220
|
+
? Promise.resolve({ value: keys[i++], done: false })
|
|
221
|
+
: Promise.resolve({ value: undefined, done: true }),
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
})
|
|
225
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
226
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
227
|
+
})
|
|
228
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
229
|
+
parentActor.start()
|
|
230
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
231
|
+
|
|
232
|
+
const onResult = vi.fn()
|
|
233
|
+
parentActor.send({ type: 'KV.BUCKET_LIST', bucket: 'mybucket', onResult })
|
|
234
|
+
|
|
235
|
+
await vi.waitFor(() => {
|
|
236
|
+
expect(onResult).toHaveBeenCalled()
|
|
237
|
+
})
|
|
238
|
+
expect(onResult).toHaveBeenCalledWith(['key1', 'key2'])
|
|
239
|
+
parentActor.stop()
|
|
240
|
+
})
|
|
241
|
+
|
|
242
|
+
it('should handle KV.BUCKET_LIST without bucket name', async () => {
|
|
243
|
+
mockKvmInstance.list.mockReturnValue({
|
|
244
|
+
[Symbol.asyncIterator]: () => {
|
|
245
|
+
let done = false
|
|
246
|
+
return {
|
|
247
|
+
next: () => {
|
|
248
|
+
if (!done) {
|
|
249
|
+
done = true
|
|
250
|
+
return Promise.resolve({ value: { bucket: 'b1' }, done: false })
|
|
251
|
+
}
|
|
252
|
+
return Promise.resolve({ value: undefined, done: true })
|
|
253
|
+
},
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
})
|
|
257
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
258
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
259
|
+
})
|
|
260
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
261
|
+
parentActor.start()
|
|
262
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
263
|
+
|
|
264
|
+
const onResult = vi.fn()
|
|
265
|
+
parentActor.send({ type: 'KV.BUCKET_LIST', onResult })
|
|
266
|
+
|
|
267
|
+
await vi.waitFor(() => {
|
|
268
|
+
expect(onResult).toHaveBeenCalled()
|
|
269
|
+
})
|
|
270
|
+
expect(onResult).toHaveBeenCalledWith([{ bucket: 'b1' }])
|
|
271
|
+
parentActor.stop()
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
it('should handle KV.BUCKET_LIST error', async () => {
|
|
275
|
+
mockKvmInstance.open.mockRejectedValueOnce(new Error('open failed'))
|
|
276
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
277
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
278
|
+
})
|
|
279
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
280
|
+
parentActor.start()
|
|
281
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
282
|
+
|
|
283
|
+
const onResult = vi.fn()
|
|
284
|
+
parentActor.send({ type: 'KV.BUCKET_LIST', bucket: 'fail', onResult })
|
|
285
|
+
|
|
286
|
+
await vi.waitFor(() => {
|
|
287
|
+
expect(onResult).toHaveBeenCalled()
|
|
288
|
+
})
|
|
289
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
290
|
+
parentActor.stop()
|
|
291
|
+
})
|
|
292
|
+
|
|
293
|
+
it('should handle KV.BUCKET_CREATE success', async () => {
|
|
294
|
+
mockKvmInstance.list.mockReturnValue({
|
|
295
|
+
[Symbol.asyncIterator]: () => ({
|
|
296
|
+
next: () => Promise.resolve({ value: undefined, done: true }),
|
|
297
|
+
}),
|
|
298
|
+
})
|
|
299
|
+
mockKvmInstance.create.mockResolvedValue({})
|
|
300
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
301
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
302
|
+
})
|
|
303
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
304
|
+
parentActor.start()
|
|
305
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
306
|
+
|
|
307
|
+
const onResult = vi.fn()
|
|
308
|
+
parentActor.send({ type: 'KV.BUCKET_CREATE', bucket: 'new-bucket', onResult })
|
|
309
|
+
|
|
310
|
+
await vi.waitFor(() => {
|
|
311
|
+
expect(onResult).toHaveBeenCalled()
|
|
312
|
+
})
|
|
313
|
+
expect(onResult).toHaveBeenCalledWith({ ok: true })
|
|
314
|
+
parentActor.stop()
|
|
315
|
+
})
|
|
316
|
+
|
|
317
|
+
it('should handle KV.BUCKET_CREATE when bucket already exists', async () => {
|
|
318
|
+
mockKvmInstance.list.mockReturnValue({
|
|
319
|
+
[Symbol.asyncIterator]: () => {
|
|
320
|
+
let done = false
|
|
321
|
+
return {
|
|
322
|
+
next: () => {
|
|
323
|
+
if (!done) {
|
|
324
|
+
done = true
|
|
325
|
+
return Promise.resolve({ value: { bucket: 'existing' }, done: false })
|
|
326
|
+
}
|
|
327
|
+
return Promise.resolve({ value: undefined, done: true })
|
|
328
|
+
},
|
|
329
|
+
}
|
|
330
|
+
},
|
|
331
|
+
})
|
|
332
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
333
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
334
|
+
})
|
|
335
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
336
|
+
parentActor.start()
|
|
337
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
338
|
+
|
|
339
|
+
const onResult = vi.fn()
|
|
340
|
+
parentActor.send({ type: 'KV.BUCKET_CREATE', bucket: 'existing', onResult })
|
|
341
|
+
|
|
342
|
+
await vi.waitFor(() => {
|
|
343
|
+
expect(onResult).toHaveBeenCalled()
|
|
344
|
+
})
|
|
345
|
+
expect(onResult).toHaveBeenCalledWith({ ok: false })
|
|
346
|
+
parentActor.stop()
|
|
347
|
+
})
|
|
348
|
+
|
|
349
|
+
it('should handle KV.BUCKET_DELETE', async () => {
|
|
350
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
351
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
352
|
+
})
|
|
353
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
354
|
+
parentActor.start()
|
|
355
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
356
|
+
|
|
357
|
+
const onResult = vi.fn()
|
|
358
|
+
parentActor.send({ type: 'KV.BUCKET_DELETE', bucket: 'old-bucket', onResult })
|
|
359
|
+
|
|
360
|
+
await vi.waitFor(() => {
|
|
361
|
+
expect(onResult).toHaveBeenCalled()
|
|
362
|
+
})
|
|
363
|
+
parentActor.stop()
|
|
364
|
+
})
|
|
365
|
+
|
|
366
|
+
it('should handle KV.BUCKET_DELETE when stream delete fails', async () => {
|
|
367
|
+
const { jetstream } = await import('@nats-io/jetstream')
|
|
368
|
+
vi.mocked(jetstream).mockImplementationOnce(
|
|
369
|
+
() =>
|
|
370
|
+
({
|
|
371
|
+
jetstreamManager: vi.fn().mockRejectedValue(new Error('stream error')),
|
|
372
|
+
}) as any,
|
|
373
|
+
)
|
|
374
|
+
|
|
375
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
376
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
377
|
+
})
|
|
378
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
379
|
+
parentActor.start()
|
|
380
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
381
|
+
|
|
382
|
+
const onResult = vi.fn()
|
|
383
|
+
parentActor.send({ type: 'KV.BUCKET_DELETE', bucket: 'fail-bucket', onResult })
|
|
384
|
+
|
|
385
|
+
await vi.waitFor(() => {
|
|
386
|
+
expect(onResult).toHaveBeenCalled()
|
|
387
|
+
})
|
|
388
|
+
expect(onResult).toHaveBeenCalledWith({ ok: false })
|
|
389
|
+
parentActor.stop()
|
|
390
|
+
})
|
|
391
|
+
|
|
392
|
+
it('should handle KV.GET', async () => {
|
|
393
|
+
mockKvStore.get.mockResolvedValue({ key: 'k', value: 'v' })
|
|
394
|
+
mockKvmInstance.open.mockResolvedValue(mockKvStore)
|
|
395
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
396
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
397
|
+
})
|
|
398
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
399
|
+
parentActor.start()
|
|
400
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
401
|
+
|
|
402
|
+
const onResult = vi.fn()
|
|
403
|
+
parentActor.send({ type: 'KV.GET', bucket: 'b', key: 'k', onResult })
|
|
404
|
+
|
|
405
|
+
await vi.waitFor(() => {
|
|
406
|
+
expect(onResult).toHaveBeenCalled()
|
|
407
|
+
})
|
|
408
|
+
expect(onResult).toHaveBeenCalledWith({ key: 'k', value: 'v' })
|
|
409
|
+
parentActor.stop()
|
|
410
|
+
})
|
|
411
|
+
|
|
412
|
+
it('should handle KV.PUT', async () => {
|
|
413
|
+
mockKvStore.put.mockResolvedValue(1)
|
|
414
|
+
mockKvmInstance.open.mockResolvedValue(mockKvStore)
|
|
415
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
416
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
417
|
+
})
|
|
418
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
419
|
+
parentActor.start()
|
|
420
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
421
|
+
|
|
422
|
+
const onResult = vi.fn()
|
|
423
|
+
parentActor.send({ type: 'KV.PUT', bucket: 'b', key: 'k', value: 'hello', onResult })
|
|
424
|
+
|
|
425
|
+
await vi.waitFor(() => {
|
|
426
|
+
expect(onResult).toHaveBeenCalled()
|
|
427
|
+
})
|
|
428
|
+
expect(onResult).toHaveBeenCalledWith({ ok: true })
|
|
429
|
+
parentActor.stop()
|
|
430
|
+
})
|
|
431
|
+
|
|
432
|
+
it('should handle KV.DELETE', async () => {
|
|
433
|
+
mockKvStore.delete.mockResolvedValue(undefined)
|
|
434
|
+
mockKvmInstance.open.mockResolvedValue(mockKvStore)
|
|
435
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
436
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
437
|
+
})
|
|
438
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
439
|
+
parentActor.start()
|
|
440
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
441
|
+
|
|
442
|
+
const onResult = vi.fn()
|
|
443
|
+
parentActor.send({ type: 'KV.DELETE', bucket: 'b', key: 'k', onResult })
|
|
444
|
+
|
|
445
|
+
await vi.waitFor(() => {
|
|
446
|
+
expect(onResult).toHaveBeenCalled()
|
|
447
|
+
})
|
|
448
|
+
expect(onResult).toHaveBeenCalledWith({ ok: true })
|
|
449
|
+
parentActor.stop()
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
it('should handle KV.GET when bucket open returns null', async () => {
|
|
453
|
+
mockKvmInstance.open.mockResolvedValueOnce(null)
|
|
454
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
455
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
456
|
+
})
|
|
457
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
458
|
+
parentActor.start()
|
|
459
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
460
|
+
|
|
461
|
+
const onResult = vi.fn()
|
|
462
|
+
parentActor.send({ type: 'KV.GET', bucket: 'missing', key: 'k', onResult })
|
|
463
|
+
|
|
464
|
+
await vi.waitFor(() => {
|
|
465
|
+
expect(onResult).toHaveBeenCalled()
|
|
466
|
+
})
|
|
467
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
468
|
+
parentActor.stop()
|
|
469
|
+
})
|
|
470
|
+
|
|
471
|
+
it('should handle KV.PUT when bucket open returns null', async () => {
|
|
472
|
+
mockKvmInstance.open.mockResolvedValueOnce(null)
|
|
473
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
474
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
475
|
+
})
|
|
476
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
477
|
+
parentActor.start()
|
|
478
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
479
|
+
|
|
480
|
+
const onResult = vi.fn()
|
|
481
|
+
parentActor.send({ type: 'KV.PUT', bucket: 'missing', key: 'k', value: 'v', onResult })
|
|
482
|
+
|
|
483
|
+
await vi.waitFor(() => {
|
|
484
|
+
expect(onResult).toHaveBeenCalled()
|
|
485
|
+
})
|
|
486
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
487
|
+
parentActor.stop()
|
|
488
|
+
})
|
|
489
|
+
|
|
490
|
+
it('should handle KV.DELETE when bucket open returns null', async () => {
|
|
491
|
+
mockKvmInstance.open.mockResolvedValueOnce(null)
|
|
492
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
493
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
494
|
+
})
|
|
495
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
496
|
+
parentActor.start()
|
|
497
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
498
|
+
|
|
499
|
+
const onResult = vi.fn()
|
|
500
|
+
parentActor.send({ type: 'KV.DELETE', bucket: 'missing', key: 'k', onResult })
|
|
501
|
+
|
|
502
|
+
await vi.waitFor(() => {
|
|
503
|
+
expect(onResult).toHaveBeenCalled()
|
|
504
|
+
})
|
|
505
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
506
|
+
parentActor.stop()
|
|
507
|
+
})
|
|
508
|
+
|
|
509
|
+
it('should handle KV.PUT error from kv.put', async () => {
|
|
510
|
+
mockKvStore.put.mockRejectedValueOnce(new Error('put failed'))
|
|
511
|
+
mockKvmInstance.open.mockResolvedValue(mockKvStore)
|
|
512
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
513
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
514
|
+
})
|
|
515
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
516
|
+
parentActor.start()
|
|
517
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
518
|
+
|
|
519
|
+
const onResult = vi.fn()
|
|
520
|
+
parentActor.send({ type: 'KV.PUT', bucket: 'b', key: 'k', value: 'v', onResult })
|
|
521
|
+
|
|
522
|
+
await vi.waitFor(() => {
|
|
523
|
+
expect(onResult).toHaveBeenCalled()
|
|
524
|
+
})
|
|
525
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
526
|
+
parentActor.stop()
|
|
527
|
+
})
|
|
528
|
+
|
|
529
|
+
it('should handle KV.DELETE error from kv.delete', async () => {
|
|
530
|
+
mockKvStore.delete.mockRejectedValueOnce(new Error('delete failed'))
|
|
531
|
+
mockKvmInstance.open.mockResolvedValue(mockKvStore)
|
|
532
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
533
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
534
|
+
})
|
|
535
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
536
|
+
parentActor.start()
|
|
537
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
538
|
+
|
|
539
|
+
const onResult = vi.fn()
|
|
540
|
+
parentActor.send({ type: 'KV.DELETE', bucket: 'b', key: 'k', onResult })
|
|
541
|
+
|
|
542
|
+
await vi.waitFor(() => {
|
|
543
|
+
expect(onResult).toHaveBeenCalled()
|
|
544
|
+
})
|
|
545
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
546
|
+
parentActor.stop()
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
it('should handle KV.GET error from kv.get', async () => {
|
|
550
|
+
mockKvStore.get.mockRejectedValueOnce(new Error('get failed'))
|
|
551
|
+
mockKvmInstance.open.mockResolvedValue(mockKvStore)
|
|
552
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
553
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
554
|
+
})
|
|
555
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
556
|
+
parentActor.start()
|
|
557
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
558
|
+
|
|
559
|
+
const onResult = vi.fn()
|
|
560
|
+
parentActor.send({ type: 'KV.GET', bucket: 'b', key: 'k', onResult })
|
|
561
|
+
|
|
562
|
+
await vi.waitFor(() => {
|
|
563
|
+
expect(onResult).toHaveBeenCalled()
|
|
564
|
+
})
|
|
565
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
566
|
+
parentActor.stop()
|
|
567
|
+
})
|
|
568
|
+
|
|
569
|
+
it('should handle KV.BUCKET_LIST with no KVM initialized', async () => {
|
|
570
|
+
// Test the !context.cachedKvm branch
|
|
571
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
572
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
573
|
+
})
|
|
574
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
575
|
+
parentActor.start()
|
|
576
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
577
|
+
|
|
578
|
+
// Manually null out cachedKvm - we can't easily do this through the machine
|
|
579
|
+
// So we test the error path via a thrown exception instead
|
|
580
|
+
mockKvmInstance.open.mockRejectedValueOnce(new Error('kvm error'))
|
|
581
|
+
const onResult = vi.fn()
|
|
582
|
+
parentActor.send({ type: 'KV.BUCKET_LIST', bucket: 'b', onResult })
|
|
583
|
+
|
|
584
|
+
await vi.waitFor(() => {
|
|
585
|
+
expect(onResult).toHaveBeenCalled()
|
|
586
|
+
})
|
|
587
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
588
|
+
parentActor.stop()
|
|
589
|
+
})
|
|
590
|
+
|
|
591
|
+
it('should handle KV.BUCKET_CREATE error', async () => {
|
|
592
|
+
mockKvmInstance.list.mockReturnValue({
|
|
593
|
+
[Symbol.asyncIterator]: () => ({
|
|
594
|
+
next: () => Promise.reject(new Error('list failed')),
|
|
595
|
+
}),
|
|
596
|
+
})
|
|
597
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
598
|
+
actors: { kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })) },
|
|
599
|
+
})
|
|
600
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
601
|
+
parentActor.start()
|
|
602
|
+
parentActor.send({ type: 'KV.CONNECT', connection: createMockConnection() })
|
|
603
|
+
|
|
604
|
+
const onResult = vi.fn()
|
|
605
|
+
parentActor.send({ type: 'KV.BUCKET_CREATE', bucket: 'fail', onResult })
|
|
606
|
+
|
|
607
|
+
await vi.waitFor(() => {
|
|
608
|
+
expect(onResult).toHaveBeenCalled()
|
|
609
|
+
})
|
|
610
|
+
expect(onResult).toHaveBeenCalledWith({ error: expect.any(Error) })
|
|
611
|
+
parentActor.stop()
|
|
612
|
+
})
|
|
613
|
+
|
|
614
|
+
it('should transition to kv_error on sync failure', async () => {
|
|
615
|
+
const kvWithFailSync = kvManagerLogic.provide({
|
|
616
|
+
actors: {
|
|
617
|
+
kvConsolidateState: fromPromise(async () => {
|
|
618
|
+
throw new Error('sync failed')
|
|
619
|
+
}),
|
|
620
|
+
},
|
|
621
|
+
})
|
|
622
|
+
const parentActor = createActor(createParentMachine(kvWithFailSync))
|
|
623
|
+
parentActor.start()
|
|
624
|
+
|
|
625
|
+
parentActor.send({
|
|
626
|
+
type: 'KV.SUBSCRIBE',
|
|
627
|
+
config: { bucket: 'b', key: 'k', callback: vi.fn() },
|
|
628
|
+
})
|
|
629
|
+
|
|
630
|
+
const connection = createMockConnection()
|
|
631
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
632
|
+
|
|
633
|
+
await vi.waitFor(() => {
|
|
634
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
635
|
+
expect(childSnap?.value).toBe('kv_error')
|
|
636
|
+
})
|
|
637
|
+
|
|
638
|
+
const ctx = parentActor.getSnapshot().children.kv?.getSnapshot()?.context
|
|
639
|
+
expect(ctx?.error).toBeDefined()
|
|
640
|
+
parentActor.stop()
|
|
641
|
+
})
|
|
642
|
+
|
|
643
|
+
it('should recover from kv_error on KV.CONNECT', async () => {
|
|
644
|
+
let callCount = 0
|
|
645
|
+
const kvWithRecovery = kvManagerLogic.provide({
|
|
646
|
+
actors: {
|
|
647
|
+
kvConsolidateState: fromPromise(async () => {
|
|
648
|
+
callCount++
|
|
649
|
+
if (callCount === 1) throw new Error('sync failed')
|
|
650
|
+
return { subscriptions: new Map() }
|
|
651
|
+
}),
|
|
652
|
+
},
|
|
653
|
+
})
|
|
654
|
+
const parentActor = createActor(createParentMachine(kvWithRecovery))
|
|
655
|
+
parentActor.start()
|
|
656
|
+
|
|
657
|
+
parentActor.send({
|
|
658
|
+
type: 'KV.SUBSCRIBE',
|
|
659
|
+
config: { bucket: 'b', key: 'k', callback: vi.fn() },
|
|
660
|
+
})
|
|
661
|
+
|
|
662
|
+
const connection = createMockConnection()
|
|
663
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
664
|
+
|
|
665
|
+
await vi.waitFor(() => {
|
|
666
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
667
|
+
expect(childSnap?.value).toBe('kv_error')
|
|
668
|
+
})
|
|
669
|
+
|
|
670
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
671
|
+
|
|
672
|
+
await vi.waitFor(() => {
|
|
673
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
674
|
+
expect(childSnap?.value).toBe('kv_connected')
|
|
675
|
+
})
|
|
676
|
+
parentActor.stop()
|
|
677
|
+
})
|
|
678
|
+
|
|
679
|
+
it('should notify parent of KV.CONNECTED', () => {
|
|
680
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
681
|
+
actors: {
|
|
682
|
+
kvConsolidateState: fromPromise(async () => ({ subscriptions: new Map() })),
|
|
683
|
+
},
|
|
684
|
+
})
|
|
685
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
686
|
+
parentActor.start()
|
|
687
|
+
|
|
688
|
+
const connection = createMockConnection()
|
|
689
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
690
|
+
|
|
691
|
+
expect(parentActor.getSnapshot().context.childState).toBe('connected')
|
|
692
|
+
parentActor.stop()
|
|
693
|
+
})
|
|
694
|
+
|
|
695
|
+
it('should sync when new subscription added while connected', async () => {
|
|
696
|
+
const kvWithMockSync = kvManagerLogic.provide({
|
|
697
|
+
actors: {
|
|
698
|
+
kvConsolidateState: fromPromise(async () => ({
|
|
699
|
+
subscriptions: new Map(),
|
|
700
|
+
})),
|
|
701
|
+
},
|
|
702
|
+
})
|
|
703
|
+
const parentActor = createActor(createParentMachine(kvWithMockSync))
|
|
704
|
+
parentActor.start()
|
|
705
|
+
|
|
706
|
+
const connection = createMockConnection()
|
|
707
|
+
parentActor.send({ type: 'KV.CONNECT', connection })
|
|
708
|
+
|
|
709
|
+
parentActor.send({
|
|
710
|
+
type: 'KV.SUBSCRIBE',
|
|
711
|
+
config: { bucket: 'b', key: 'k', callback: vi.fn() },
|
|
712
|
+
})
|
|
713
|
+
|
|
714
|
+
await vi.waitFor(() => {
|
|
715
|
+
const childSnap = parentActor.getSnapshot().children.kv?.getSnapshot()
|
|
716
|
+
expect(childSnap?.value).toBe('kv_connected')
|
|
717
|
+
})
|
|
718
|
+
parentActor.stop()
|
|
719
|
+
})
|
|
720
|
+
})
|