@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.
Files changed (55) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +251 -0
  3. package/dist/actions/connection.d.ts +28 -0
  4. package/dist/actions/connection.d.ts.map +1 -0
  5. package/dist/actions/connection.js +102 -0
  6. package/dist/actions/connection.js.map +1 -0
  7. package/dist/actions/kv.d.ts +21 -0
  8. package/dist/actions/kv.d.ts.map +1 -0
  9. package/dist/actions/kv.js +66 -0
  10. package/dist/actions/kv.js.map +1 -0
  11. package/dist/actions/subject.d.ts +39 -0
  12. package/dist/actions/subject.d.ts.map +1 -0
  13. package/dist/actions/subject.js +79 -0
  14. package/dist/actions/subject.js.map +1 -0
  15. package/dist/actions/types.d.ts +8 -0
  16. package/dist/actions/types.d.ts.map +1 -0
  17. package/dist/actions/types.js +2 -0
  18. package/dist/actions/types.js.map +1 -0
  19. package/dist/index.d.ts +8 -0
  20. package/dist/index.d.ts.map +1 -0
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -0
  23. package/dist/machines/kv.d.ts +190 -0
  24. package/dist/machines/kv.d.ts.map +1 -0
  25. package/dist/machines/kv.js +273 -0
  26. package/dist/machines/kv.js.map +1 -0
  27. package/dist/machines/root.d.ts +510 -0
  28. package/dist/machines/root.d.ts.map +1 -0
  29. package/dist/machines/root.js +245 -0
  30. package/dist/machines/root.js.map +1 -0
  31. package/dist/machines/subject.d.ts +95 -0
  32. package/dist/machines/subject.d.ts.map +1 -0
  33. package/dist/machines/subject.js +162 -0
  34. package/dist/machines/subject.js.map +1 -0
  35. package/dist/utils.d.ts +10 -0
  36. package/dist/utils.d.ts.map +1 -0
  37. package/dist/utils.js +27 -0
  38. package/dist/utils.js.map +1 -0
  39. package/package.json +55 -0
  40. package/src/actions/connection.test.ts +324 -0
  41. package/src/actions/connection.ts +135 -0
  42. package/src/actions/kv.test.ts +439 -0
  43. package/src/actions/kv.ts +92 -0
  44. package/src/actions/subject.test.ts +460 -0
  45. package/src/actions/subject.ts +127 -0
  46. package/src/actions/types.ts +7 -0
  47. package/src/index.ts +20 -0
  48. package/src/machines/kv.test.ts +720 -0
  49. package/src/machines/kv.ts +327 -0
  50. package/src/machines/root.test.ts +329 -0
  51. package/src/machines/root.ts +286 -0
  52. package/src/machines/subject.test.ts +272 -0
  53. package/src/machines/subject.ts +205 -0
  54. package/src/utils.test.ts +35 -0
  55. 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
+ })