@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,286 @@
1
+ import { ConnectionOptions, NatsConnection } from '@nats-io/nats-core'
2
+ import { assign, sendTo, setup } from 'xstate'
3
+ import { kvManagerLogic, ExternalEvents as KvExternalEvents } from './kv'
4
+ import { subjectManagerLogic, ExternalEvents as SubjectExternalEvents } from './subject'
5
+ import { connectToNats, disconnectNats } from '../actions/connection'
6
+ import { type AuthConfig } from '../actions/types'
7
+ import { InternalStatusEvents as NatsStatusEvents } from '../actions/connection'
8
+
9
+ export interface NatsConnectionConfig {
10
+ opts: ConnectionOptions
11
+ auth?: AuthConfig
12
+ maxRetries: number
13
+ }
14
+
15
+ export interface Context {
16
+ connection: NatsConnection | null
17
+ error?: Error
18
+ natsConfig?: NatsConnectionConfig
19
+ retries: number
20
+ subjectManagerReady: boolean
21
+ kvManagerReady: boolean
22
+ }
23
+
24
+ // internal events and events from nats connection
25
+ type InternalEvents =
26
+ | { type: 'CONNECTED'; connection: NatsConnection }
27
+ | { type: 'DISCONNECTED' }
28
+ | { type: 'FAIL'; error: Error }
29
+ | { type: 'RECONNECT' }
30
+ | { type: 'CLOSE' }
31
+ | NatsStatusEvents
32
+
33
+ // events which can be sent to the machine from the user
34
+ export type ExternalEvents =
35
+ | { type: 'CONFIGURE'; config: NatsConnectionConfig }
36
+ | { type: 'CONNECT' }
37
+ | { type: 'DISCONNECT' }
38
+ | { type: 'RESET' }
39
+ | SubjectExternalEvents
40
+ | KvExternalEvents
41
+
42
+ type Events = InternalEvents | ExternalEvents
43
+
44
+ export const natsMachine = setup({
45
+ types: {
46
+ context: {} as Context,
47
+ events: {} as Events,
48
+ },
49
+ actions: {
50
+ doReset: assign({
51
+ natsConfig: (_) => undefined,
52
+ connection: (_) => null,
53
+ error: (_) => undefined,
54
+ retries: (_) => 0,
55
+ subjectManagerReady: (_) => false,
56
+ kvManagerReady: (_) => false,
57
+ }),
58
+ },
59
+ guards: {
60
+ allManagersReady: ({ context }) => {
61
+ const hasValidConnection = context.connection !== null
62
+ const subjectReady = context.subjectManagerReady
63
+ const kvReady = context.kvManagerReady
64
+ return hasValidConnection && subjectReady && kvReady
65
+ },
66
+ },
67
+ actors: {
68
+ connectToNats: connectToNats,
69
+ disconnectNats: disconnectNats,
70
+ subject: subjectManagerLogic,
71
+ kv: kvManagerLogic,
72
+ },
73
+ }).createMachine({
74
+ /** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOnwHsAXAfU3PwDNcoBXAJ0gGIBhAeQDkAYgEkA4gFUASgFEA2gAYAuolAAHcrFyVc9FSAAeiABwBOeSQBsAdgCMRgMwAmRxftWALPYCsAGhABPRHd3c3cvext7IwsveRCrAF8EvzQsPEJSOkZmdi4+fn5pbgAVBWUkEHVNbV0KwwQbGw9LJysLExNHSPkjP0CEdzaSMwsXaPt5LxMbeQsklIwcAmISLMJMbXwoTgh6MBICADdyAGt91KWM1fp1zagEI-JMdBr8MrK9Kq0dfD16m3CVhIUUcVis8isJjCjlmfUQ4XcLUcRiakXcJicc2SIAu6RWazAGwI2zAbDY5DYJFUABsXgwKagSLjlpkboS7g98Mdnq93kpPhpvrVQP9AcCjKDwZDobCAkF3DYSE0TNZ2p1ptF3PMcYs8az8Lc8gAZXgAZTk-IqX1ef0QNlGQK60TMEUGVi8NjhCFciJmGIxRmlYJs2uZV0w1MFWx2ewOXNO511LNWkc0W053JePz55TUgptdUQyK8JDiFiMXhC8hco3k9i9mJIVk17Rs03LYPsoaT4dTxM4pPJlJpdIZTJ7+L76cePOzSg+VvzP1tCGLpfc5cr8mrFlr9blDUrJFi2-kKs6NlB3bSyYjGi4MnNpUteeqy8LCCm9mB3kDtjBPRhA21bAme7bupukzXpcKyDhSnCPtIz65pUS7CgYdoWI0x4RG2IRNJ4jjuF6jgqk2LheJCbQRPYWrahQEBwHoYbEAKb7ofUAC0Fhetx0F6mQVC0PQTCsBwEBsUKvwflYXTkTE9qdq49gmA2jgls2FgbhuowmB4-G3iJOTiZJBYinacSKtYKoWHW7pGIGPEHm6JAhKMXikXYNieHRCw3uGbJElspnvuZDQRI4JAokRHkqjCG69M54TDLMXT2CpERmIk2IsfqhoSYu7HSWFTQqcMTrpVhKkool-SVhYx4uKlMylXYBm9lGUAhRxQSOF69omEq8gAruESTB5kTtZO94Fa+UkruiiqpdCHSOLhTn9E4GktjppH6TlE6kHBbDdcVGENFYZUXg49hVSYNVqeYMIRW07TqR42VJEAA */
75
+ initial: 'not_configured',
76
+ context: {
77
+ connection: null,
78
+ error: undefined,
79
+ natsConfig: undefined,
80
+ retries: 0,
81
+ subjectManagerReady: false,
82
+ kvManagerReady: false,
83
+ },
84
+ invoke: [
85
+ { src: 'subject', id: 'subject', systemId: 'subject' },
86
+ { src: 'kv', id: 'kv' },
87
+ ],
88
+ on: {
89
+ 'NATS_CONNECTION.*': {
90
+ actions: [
91
+ ({ event }: { event: any }) => {
92
+ console.log('root received NATS status event', event)
93
+ },
94
+ ],
95
+ },
96
+ },
97
+ states: {
98
+ not_configured: {
99
+ on: {
100
+ CONFIGURE: {
101
+ target: 'configured',
102
+ actions: [
103
+ assign({
104
+ natsConfig: ({ event }) => event.config,
105
+ }),
106
+ ],
107
+ '*': {
108
+ actions: [
109
+ ({ event }: { event: any }) => {
110
+ console.error('root not_configured received unexpected event', event)
111
+ },
112
+ ],
113
+ },
114
+ },
115
+ },
116
+ },
117
+ configured: {
118
+ on: {
119
+ CONNECT: {
120
+ target: 'connecting',
121
+ },
122
+ RESET: {
123
+ target: 'not_configured',
124
+ actions: ['doReset'],
125
+ },
126
+ '*': {
127
+ actions: [
128
+ ({ event }: { event: any }) => {
129
+ console.error('root configured received unexpected event', event)
130
+ },
131
+ ],
132
+ },
133
+ },
134
+ },
135
+ connecting: {
136
+ invoke: [
137
+ {
138
+ src: 'connectToNats',
139
+ input: ({ context }) => ({
140
+ opts: context.natsConfig!.opts,
141
+ auth: context.natsConfig!.auth,
142
+ }),
143
+ onDone: {
144
+ target: 'initialise_managers',
145
+ actions: [
146
+ assign({
147
+ connection: ({ event }) => event.output,
148
+ retries: (_) => 0,
149
+ }),
150
+ ],
151
+ },
152
+ onError: {
153
+ target: 'error',
154
+ actions: assign({
155
+ error: ({ event }) => event.error as Error,
156
+ retries: ({ context }) => context.retries + 1,
157
+ }),
158
+ },
159
+ },
160
+ ],
161
+ },
162
+ initialise_managers: {
163
+ entry: [
164
+ sendTo('subject', ({ context }) => ({
165
+ type: 'SUBJECT.CONNECT',
166
+ connection: context.connection!,
167
+ })),
168
+ sendTo('kv', ({ context }) => ({ type: 'KV.CONNECT', connection: context.connection! })),
169
+ ],
170
+ on: {
171
+ 'SUBJECT.CONNECTED': {
172
+ actions: [assign({ subjectManagerReady: (_) => true })],
173
+ },
174
+ 'KV.CONNECTED': {
175
+ actions: [assign({ kvManagerReady: (_) => true })],
176
+ },
177
+ },
178
+ always: [
179
+ {
180
+ guard: 'allManagersReady',
181
+ target: 'connected',
182
+ },
183
+ ],
184
+ },
185
+ connected: {
186
+ entry: [
187
+ (event) => {
188
+ console.log('CONNECTED', event.context.connection?.getServer())
189
+ },
190
+ ],
191
+ on: {
192
+ DISCONNECT: {
193
+ target: 'closing',
194
+ },
195
+ CLOSE: {
196
+ target: 'closing',
197
+ },
198
+ 'SUBJECT.*': {
199
+ actions: [
200
+ ({ event }: any) => {
201
+ console.log('xstat-nats forwarding subject event', event)
202
+ },
203
+ sendTo(
204
+ 'subject',
205
+ ({ event, context }: { event: SubjectExternalEvents; context: Context }) => {
206
+ return { ...event, connection: context.connection }
207
+ },
208
+ ),
209
+ ],
210
+ },
211
+ 'KV.*': {
212
+ actions: [
213
+ ({ event }: any) => {
214
+ console.log('xstat-nats forwarding kv event', event)
215
+ },
216
+ sendTo('kv', ({ event, context }: { event: KvExternalEvents; context: Context }) => {
217
+ return { ...event, connection: context.connection }
218
+ }),
219
+ ],
220
+ },
221
+ '*': {
222
+ actions: [
223
+ ({ event }: { event: any }) => {
224
+ console.error('root connected received unexpected event', event)
225
+ },
226
+ ],
227
+ },
228
+ },
229
+ },
230
+ closing: {
231
+ entry: [
232
+ (event) => {
233
+ console.log('CLOSING', event.context.connection?.getServer())
234
+ },
235
+ sendTo('subject', { type: 'SUBJECT.DISCONNECTED' }),
236
+ sendTo('kv', { type: 'KV.DISCONNECTED' }),
237
+ ],
238
+ invoke: {
239
+ src: 'disconnectNats',
240
+ input: ({ context }) => ({ connection: context.connection }),
241
+ onDone: {
242
+ target: 'closed',
243
+ },
244
+ onError: {
245
+ target: 'error',
246
+ actions: assign({
247
+ error: ({ event }) => event.error as Error,
248
+ }),
249
+ },
250
+ },
251
+ },
252
+ closed: {
253
+ on: {
254
+ RESET: {
255
+ target: 'not_configured',
256
+ actions: ['doReset'],
257
+ },
258
+ CONNECT: {
259
+ target: 'connecting',
260
+ },
261
+ },
262
+ '*': {
263
+ actions: [
264
+ ({ event }: { event: any }) => {
265
+ console.error('root closing received unexpected event', event)
266
+ },
267
+ ],
268
+ },
269
+ },
270
+ error: {
271
+ on: {
272
+ RESET: {
273
+ target: 'not_configured',
274
+ actions: ['doReset'],
275
+ },
276
+ },
277
+ '*': {
278
+ actions: [
279
+ ({ event }: { event: any }) => {
280
+ console.error('root error received unexpected event', event)
281
+ },
282
+ ],
283
+ },
284
+ },
285
+ },
286
+ })
@@ -0,0 +1,272 @@
1
+ import { describe, it, expect, vi, beforeEach } from 'vitest'
2
+ import { createActor, createMachine, assign, fromPromise, sendTo, setup } from 'xstate'
3
+ import { subjectManagerLogic } from './subject'
4
+
5
+ vi.mock('@nats-io/nats-core', () => ({
6
+ wsconnect: vi.fn(),
7
+ }))
8
+
9
+ function createMockConnection() {
10
+ return {
11
+ subscribe: vi.fn().mockReturnValue({
12
+ unsubscribe: vi.fn(),
13
+ [Symbol.asyncIterator]: () => ({
14
+ next: () => new Promise(() => {}),
15
+ }),
16
+ }),
17
+ request: vi.fn().mockResolvedValue({ json: () => ({}), string: () => '{}' }),
18
+ publish: vi.fn(),
19
+ } as any
20
+ }
21
+
22
+ // Create a parent machine that wraps the subject manager so sendParent works
23
+ function createParentMachine() {
24
+ return setup({
25
+ types: {
26
+ context: {} as { childState: string },
27
+ events: {} as any,
28
+ },
29
+ actors: {
30
+ subject: subjectManagerLogic,
31
+ },
32
+ }).createMachine({
33
+ initial: 'active',
34
+ context: { childState: '' },
35
+ invoke: {
36
+ src: 'subject',
37
+ id: 'subject',
38
+ },
39
+ on: {
40
+ // Forward all events to the subject child
41
+ 'SUBJECT.*': {
42
+ actions: sendTo('subject', ({ event, context }: any) => {
43
+ return { ...event, connection: event.connection }
44
+ }),
45
+ },
46
+ 'SUBJECT.CONNECTED': {
47
+ actions: assign({ childState: 'connected' }),
48
+ },
49
+ 'SUBJECT.DISCONNECTED': {
50
+ actions: assign({ childState: 'disconnected' }),
51
+ },
52
+ },
53
+ states: {
54
+ active: {},
55
+ },
56
+ })
57
+ }
58
+
59
+ describe('subjectManagerLogic', () => {
60
+ beforeEach(() => {
61
+ vi.spyOn(console, 'log').mockImplementation(() => {})
62
+ vi.spyOn(console, 'error').mockImplementation(() => {})
63
+ })
64
+
65
+ it('should start in subject_idle state', () => {
66
+ const parentActor = createActor(createParentMachine())
67
+ parentActor.start()
68
+ const childSnap = parentActor.getSnapshot().children.subject?.getSnapshot()
69
+ expect(childSnap?.value).toBe('subject_idle')
70
+ parentActor.stop()
71
+ })
72
+
73
+ it('should have empty subscriptions initially', () => {
74
+ const parentActor = createActor(createParentMachine())
75
+ parentActor.start()
76
+ const ctx = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
77
+ expect(ctx?.subscriptions.size).toBe(0)
78
+ expect(ctx?.subscriptionConfigs.size).toBe(0)
79
+ expect(ctx?.cachedConnection).toBeNull()
80
+ expect(ctx?.syncRequired).toBe(0)
81
+ parentActor.stop()
82
+ })
83
+
84
+ it('should transition to subject_connected on SUBJECT.CONNECT', () => {
85
+ const parentActor = createActor(createParentMachine())
86
+ parentActor.start()
87
+
88
+ const connection = createMockConnection()
89
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
90
+
91
+ const childSnap = parentActor.getSnapshot().children.subject?.getSnapshot()
92
+ expect(childSnap?.value).toBe('subject_connected')
93
+ expect(childSnap?.context.cachedConnection).toBe(connection)
94
+ parentActor.stop()
95
+ })
96
+
97
+ it('should add subscription config on SUBJECT.SUBSCRIBE', () => {
98
+ const parentActor = createActor(createParentMachine())
99
+ parentActor.start()
100
+
101
+ const callback = vi.fn()
102
+ parentActor.send({
103
+ type: 'SUBJECT.SUBSCRIBE',
104
+ config: { subject: 'test.sub', callback },
105
+ })
106
+
107
+ const ctx = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
108
+ expect(ctx?.subscriptionConfigs.has('test.sub')).toBe(true)
109
+ expect(ctx?.syncRequired).toBe(1)
110
+ parentActor.stop()
111
+ })
112
+
113
+ it('should remove subscription config on SUBJECT.UNSUBSCRIBE', () => {
114
+ const parentActor = createActor(createParentMachine())
115
+ parentActor.start()
116
+
117
+ parentActor.send({
118
+ type: 'SUBJECT.SUBSCRIBE',
119
+ config: { subject: 'test.sub', callback: vi.fn() },
120
+ })
121
+ parentActor.send({ type: 'SUBJECT.UNSUBSCRIBE', subject: 'test.sub' })
122
+
123
+ const ctx = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
124
+ expect(ctx?.subscriptionConfigs.has('test.sub')).toBe(false)
125
+ expect(ctx?.syncRequired).toBe(2)
126
+ parentActor.stop()
127
+ })
128
+
129
+ it('should clear all subscription configs on SUBJECT.UNSUBSCRIBE_ALL', () => {
130
+ const parentActor = createActor(createParentMachine())
131
+ parentActor.start()
132
+
133
+ parentActor.send({
134
+ type: 'SUBJECT.SUBSCRIBE',
135
+ config: { subject: 'test.sub1', callback: vi.fn() },
136
+ })
137
+ parentActor.send({
138
+ type: 'SUBJECT.SUBSCRIBE',
139
+ config: { subject: 'test.sub2', callback: vi.fn() },
140
+ })
141
+ parentActor.send({ type: 'SUBJECT.UNSUBSCRIBE_ALL' })
142
+
143
+ const ctx = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
144
+ expect(ctx?.subscriptionConfigs.size).toBe(0)
145
+ parentActor.stop()
146
+ })
147
+
148
+ it('should sync subscriptions when connecting with pending configs', () => {
149
+ const parentActor = createActor(createParentMachine())
150
+ parentActor.start()
151
+
152
+ const callback = vi.fn()
153
+ parentActor.send({
154
+ type: 'SUBJECT.SUBSCRIBE',
155
+ config: { subject: 'test.sub', callback },
156
+ })
157
+
158
+ const connection = createMockConnection()
159
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
160
+
161
+ const childSnap = parentActor.getSnapshot().children.subject?.getSnapshot()
162
+ expect(childSnap?.value).toBe('subject_connected')
163
+ expect(connection.subscribe).toHaveBeenCalledWith('test.sub', undefined)
164
+ parentActor.stop()
165
+ })
166
+
167
+ it('should sync when new subscription added while connected', () => {
168
+ const parentActor = createActor(createParentMachine())
169
+ parentActor.start()
170
+
171
+ const connection = createMockConnection()
172
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
173
+
174
+ parentActor.send({
175
+ type: 'SUBJECT.SUBSCRIBE',
176
+ config: { subject: 'new.sub', callback: vi.fn() },
177
+ })
178
+
179
+ const childSnap = parentActor.getSnapshot().children.subject?.getSnapshot()
180
+ expect(childSnap?.value).toBe('subject_connected')
181
+ expect(connection.subscribe).toHaveBeenCalledWith('new.sub', undefined)
182
+ parentActor.stop()
183
+ })
184
+
185
+ it('should handle SUBJECT.PUBLISH when connected', () => {
186
+ const parentActor = createActor(createParentMachine())
187
+ parentActor.start()
188
+
189
+ const connection = createMockConnection()
190
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
191
+
192
+ parentActor.send({
193
+ type: 'SUBJECT.PUBLISH',
194
+ subject: 'test.pub',
195
+ payload: { msg: 'hello' },
196
+ })
197
+
198
+ expect(connection.publish).toHaveBeenCalledWith('test.pub', { msg: 'hello' }, undefined)
199
+ parentActor.stop()
200
+ })
201
+
202
+ it('should handle SUBJECT.REQUEST when connected', () => {
203
+ const parentActor = createActor(createParentMachine())
204
+ parentActor.start()
205
+
206
+ const connection = createMockConnection()
207
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
208
+
209
+ const callback = vi.fn()
210
+ parentActor.send({
211
+ type: 'SUBJECT.REQUEST',
212
+ subject: 'test.req',
213
+ payload: { data: 1 },
214
+ callback,
215
+ })
216
+
217
+ expect(connection.request).toHaveBeenCalledWith('test.req', { data: 1 }, undefined)
218
+ parentActor.stop()
219
+ })
220
+
221
+ it('should accept SUBJECT.SUBSCRIBE in idle state (global handler)', () => {
222
+ const parentActor = createActor(createParentMachine())
223
+ parentActor.start()
224
+
225
+ parentActor.send({
226
+ type: 'SUBJECT.SUBSCRIBE',
227
+ config: { subject: 'pre.connect', callback: vi.fn() },
228
+ })
229
+
230
+ const ctx = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
231
+ expect(ctx?.subscriptionConfigs.has('pre.connect')).toBe(true)
232
+ parentActor.stop()
233
+ })
234
+
235
+ it('should notify parent of SUBJECT.CONNECTED', () => {
236
+ const parentActor = createActor(createParentMachine())
237
+ parentActor.start()
238
+
239
+ const connection = createMockConnection()
240
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
241
+
242
+ expect(parentActor.getSnapshot().context.childState).toBe('connected')
243
+ parentActor.stop()
244
+ })
245
+
246
+ it('should handle multiple subscribe/unsubscribe cycles', () => {
247
+ const parentActor = createActor(createParentMachine())
248
+ parentActor.start()
249
+
250
+ const connection = createMockConnection()
251
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
252
+
253
+ parentActor.send({
254
+ type: 'SUBJECT.SUBSCRIBE',
255
+ config: { subject: 'sub1', callback: vi.fn() },
256
+ })
257
+ parentActor.send({
258
+ type: 'SUBJECT.SUBSCRIBE',
259
+ config: { subject: 'sub2', callback: vi.fn() },
260
+ })
261
+
262
+ const ctx1 = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
263
+ expect(ctx1?.subscriptionConfigs.size).toBe(2)
264
+
265
+ parentActor.send({ type: 'SUBJECT.UNSUBSCRIBE', subject: 'sub1' })
266
+
267
+ const ctx2 = parentActor.getSnapshot().children.subject?.getSnapshot()?.context
268
+ expect(ctx2?.subscriptionConfigs.size).toBe(1)
269
+ expect(ctx2?.subscriptionConfigs.has('sub2')).toBe(true)
270
+ parentActor.stop()
271
+ })
272
+ })