@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,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
|
+
})
|