@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,205 @@
|
|
|
1
|
+
import { Subscription, PublishOptions, NatsConnection, RequestOptions } from '@nats-io/nats-core'
|
|
2
|
+
import { assign, sendParent, setup } from 'xstate'
|
|
3
|
+
import {
|
|
4
|
+
SubjectSubscriptionConfig,
|
|
5
|
+
subjectConsolidateState,
|
|
6
|
+
subjectRequest,
|
|
7
|
+
subjectPublish,
|
|
8
|
+
} from '../actions/subject'
|
|
9
|
+
|
|
10
|
+
// internal events and events from nats connection
|
|
11
|
+
type InternalEvents = { type: 'ERROR'; error: Error }
|
|
12
|
+
|
|
13
|
+
// events which can be sent to the machine from the user
|
|
14
|
+
export type ExternalEvents =
|
|
15
|
+
| { type: 'SUBJECT.CONNECT'; connection: NatsConnection }
|
|
16
|
+
| { type: 'SUBJECT.CONNECTED' }
|
|
17
|
+
| { type: 'SUBJECT.DISCONNECTED' }
|
|
18
|
+
| {
|
|
19
|
+
type: 'SUBJECT.PUBLISH'
|
|
20
|
+
subject: string
|
|
21
|
+
payload: any
|
|
22
|
+
opts?: PublishOptions
|
|
23
|
+
onPublishResult?: (result: { ok: true } | { ok: false; error: Error }) => void
|
|
24
|
+
}
|
|
25
|
+
| {
|
|
26
|
+
type: 'SUBJECT.REQUEST'
|
|
27
|
+
subject: string
|
|
28
|
+
payload: any
|
|
29
|
+
opts?: RequestOptions
|
|
30
|
+
callback: (data: any) => void
|
|
31
|
+
}
|
|
32
|
+
| { type: 'SUBJECT.SUBSCRIBE'; config: SubjectSubscriptionConfig }
|
|
33
|
+
| { type: 'SUBJECT.UNSUBSCRIBE'; subject: string }
|
|
34
|
+
| { type: 'SUBJECT.UNSUBSCRIBE_ALL' }
|
|
35
|
+
|
|
36
|
+
export type Events = InternalEvents | ExternalEvents
|
|
37
|
+
|
|
38
|
+
export interface Context {
|
|
39
|
+
uid: string
|
|
40
|
+
cachedConnection: NatsConnection | null
|
|
41
|
+
subscriptions: Map<string, Subscription>
|
|
42
|
+
subscriptionConfigs: Map<string, SubjectSubscriptionConfig>
|
|
43
|
+
syncRequired: number
|
|
44
|
+
error?: Error
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export const subjectManagerLogic = setup({
|
|
48
|
+
types: {
|
|
49
|
+
context: {} as Context,
|
|
50
|
+
events: {} as Events,
|
|
51
|
+
},
|
|
52
|
+
guards: {
|
|
53
|
+
hasPendingSync: ({ context }) => {
|
|
54
|
+
return context.syncRequired > 0
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}).createMachine({
|
|
58
|
+
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOlgFcAjAKzEwBcB9XCAGzAGIBlAVQCEAUgFEAwgBVGASQByksZICCAGUlchAbQAMAXUSgADgHtYuerkP49IAB6IATJs0kAnAFYAzAEY7rgDQgAT3tXTxJNAA53KOiYrwBfOP80LDxCUgoaOiYCU1x0VlwTfCgOa1h6dHowEnQAMyqAJ2RXRyIOZJwCYjIqWgZmfFz8woIoLV0kECMTMwsrWwQANk0AFhJPNy8ffyCEHzsSV0jY2M8EpIxOtJ7M-swLQgZIbn5hcUZePi4RACVJPg0Ois01yc0mC08jnCJAA7O4Vu4YX5AvYVjDDnZ4YjXOcQB1Ut0Mn0mPd8I8qhAXoJRBIeNJPt8-gDxsDjKDLODEEiDu5FjDPOEkTtEJ4Yc5cfiuulellGKTyc9Pm8JCIlEIFD8PvxGf9ARMDGzZhzQAs4a4SHY7ItXIttiiEN5oTjcfhDBA4FZJWlWTNzMabIgALSLYUIYMSy4E6W3bJsMA+9nzRArOyhy3QiInU4RlJSm7EgZDApFKAJo1JhArTzmzTV23I3bpsLHLNRM6JPGRvNE2XyrKQMt+iswpEkRaChv2eGHHNXQkyu4WWCGAoQSpgRjldeDsEmkWY0IrFYebyTvZomcdr3zmOMMANBqGBo7-0LFaLC0CoX26KHFuthIEiAA */
|
|
59
|
+
initial: 'subject_idle',
|
|
60
|
+
context: {
|
|
61
|
+
uid: new Date().toISOString(),
|
|
62
|
+
subscriptions: new Map<string, Subscription>(),
|
|
63
|
+
subscriptionConfigs: new Map<string, SubjectSubscriptionConfig>(),
|
|
64
|
+
cachedConnection: null,
|
|
65
|
+
syncRequired: 0,
|
|
66
|
+
error: undefined,
|
|
67
|
+
},
|
|
68
|
+
on: {
|
|
69
|
+
'SUBJECT.SUBSCRIBE': {
|
|
70
|
+
actions: [
|
|
71
|
+
assign({
|
|
72
|
+
subscriptionConfigs: ({ context, event }) => {
|
|
73
|
+
const { config } = event
|
|
74
|
+
const newConfigs = new Map(context.subscriptionConfigs)
|
|
75
|
+
newConfigs.set(config.subject, config)
|
|
76
|
+
return newConfigs
|
|
77
|
+
},
|
|
78
|
+
syncRequired: ({ context }) => context.syncRequired + 1,
|
|
79
|
+
}),
|
|
80
|
+
],
|
|
81
|
+
},
|
|
82
|
+
'SUBJECT.UNSUBSCRIBE': {
|
|
83
|
+
actions: [
|
|
84
|
+
assign(({ context, event }) => {
|
|
85
|
+
const newConfigs = new Map(context.subscriptionConfigs)
|
|
86
|
+
newConfigs.delete(event.subject)
|
|
87
|
+
return {
|
|
88
|
+
subscriptionConfigs: newConfigs,
|
|
89
|
+
syncRequired: context.syncRequired + 1,
|
|
90
|
+
}
|
|
91
|
+
}),
|
|
92
|
+
],
|
|
93
|
+
},
|
|
94
|
+
'SUBJECT.UNSUBSCRIBE_ALL': {
|
|
95
|
+
actions: assign({
|
|
96
|
+
subscriptionConfigs: new Map(),
|
|
97
|
+
syncRequired: ({ context }) => context.syncRequired + 1,
|
|
98
|
+
}),
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
states: {
|
|
102
|
+
subject_idle: {
|
|
103
|
+
on: {
|
|
104
|
+
'SUBJECT.CONNECT': {
|
|
105
|
+
target: 'subject_check_sync',
|
|
106
|
+
actions: [
|
|
107
|
+
assign({
|
|
108
|
+
cachedConnection: ({ event }) => event.connection,
|
|
109
|
+
}),
|
|
110
|
+
],
|
|
111
|
+
},
|
|
112
|
+
},
|
|
113
|
+
},
|
|
114
|
+
subject_disconnecting: {
|
|
115
|
+
target: 'subject_idle',
|
|
116
|
+
entry: [
|
|
117
|
+
// dont close the connection here, it will be closed by the nats connection machine
|
|
118
|
+
assign({
|
|
119
|
+
cachedConnection: null,
|
|
120
|
+
subscriptions: new Map<string, Subscription>(),
|
|
121
|
+
}),
|
|
122
|
+
sendParent({ type: 'SUBJECT.DISCONNECTED' }),
|
|
123
|
+
],
|
|
124
|
+
},
|
|
125
|
+
subject_connected: {
|
|
126
|
+
entry: [sendParent({ type: 'SUBJECT.CONNECTED' })],
|
|
127
|
+
always: {
|
|
128
|
+
target: 'subject_syncing',
|
|
129
|
+
guard: 'hasPendingSync',
|
|
130
|
+
},
|
|
131
|
+
on: {
|
|
132
|
+
'SUBJECT.REQUEST': {
|
|
133
|
+
actions: assign(({ event, context }) => {
|
|
134
|
+
subjectRequest({
|
|
135
|
+
input: {
|
|
136
|
+
connection: context.cachedConnection!,
|
|
137
|
+
subject: event.subject,
|
|
138
|
+
payload: event.payload,
|
|
139
|
+
opts: event.opts,
|
|
140
|
+
callback: event.callback,
|
|
141
|
+
},
|
|
142
|
+
})
|
|
143
|
+
return {}
|
|
144
|
+
}),
|
|
145
|
+
},
|
|
146
|
+
'SUBJECT.PUBLISH': {
|
|
147
|
+
actions: [
|
|
148
|
+
({ context, event }) => {
|
|
149
|
+
subjectPublish({
|
|
150
|
+
input: {
|
|
151
|
+
connection: context.cachedConnection!,
|
|
152
|
+
subject: event.subject,
|
|
153
|
+
payload: event.payload,
|
|
154
|
+
options: event.opts,
|
|
155
|
+
onPublishResult: event.onPublishResult,
|
|
156
|
+
},
|
|
157
|
+
})
|
|
158
|
+
},
|
|
159
|
+
],
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
},
|
|
163
|
+
subject_check_sync: {
|
|
164
|
+
always: [
|
|
165
|
+
{
|
|
166
|
+
target: 'subject_syncing',
|
|
167
|
+
guard: 'hasPendingSync',
|
|
168
|
+
},
|
|
169
|
+
{
|
|
170
|
+
target: 'subject_connected',
|
|
171
|
+
},
|
|
172
|
+
],
|
|
173
|
+
},
|
|
174
|
+
subject_syncing: {
|
|
175
|
+
entry: [
|
|
176
|
+
({ context }) => {
|
|
177
|
+
// either going to be 0 or 1 (if there were multiple syncs pending)
|
|
178
|
+
context.syncRequired = Math.min(context.syncRequired - 1, 1)
|
|
179
|
+
},
|
|
180
|
+
assign(({ context }) => {
|
|
181
|
+
const consolidatedContext = subjectConsolidateState({
|
|
182
|
+
input: {
|
|
183
|
+
connection: context.cachedConnection!,
|
|
184
|
+
currentSubscriptions: context.subscriptions,
|
|
185
|
+
targetSubscriptions: context.subscriptionConfigs,
|
|
186
|
+
},
|
|
187
|
+
})
|
|
188
|
+
return {
|
|
189
|
+
...consolidatedContext,
|
|
190
|
+
}
|
|
191
|
+
}),
|
|
192
|
+
],
|
|
193
|
+
always: {
|
|
194
|
+
target: 'subject_connected',
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
subject_error: {
|
|
198
|
+
on: {
|
|
199
|
+
'SUBJECT.CONNECT': {
|
|
200
|
+
target: 'subject_check_sync',
|
|
201
|
+
},
|
|
202
|
+
},
|
|
203
|
+
},
|
|
204
|
+
},
|
|
205
|
+
})
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest'
|
|
2
|
+
import { Pair } from './utils'
|
|
3
|
+
|
|
4
|
+
describe('Pair', () => {
|
|
5
|
+
it('should create a pair and convert to key', () => {
|
|
6
|
+
const pair = new Pair('test', 123)
|
|
7
|
+
const key = pair.toKey()
|
|
8
|
+
|
|
9
|
+
expect(key).toBe('Pair(test, 123)')
|
|
10
|
+
expect(pair.x).toBe('test')
|
|
11
|
+
expect(pair.y).toBe(123)
|
|
12
|
+
})
|
|
13
|
+
|
|
14
|
+
it('should parse pair from key', () => {
|
|
15
|
+
const key = 'Pair(hello, 456)'
|
|
16
|
+
const pair = Pair.fromKey(key)
|
|
17
|
+
|
|
18
|
+
expect(pair.x).toBe('hello')
|
|
19
|
+
expect(pair.y).toBe('456')
|
|
20
|
+
})
|
|
21
|
+
|
|
22
|
+
it('should throw on invalid key format', () => {
|
|
23
|
+
expect(() => Pair.fromKey('invalid')).toThrow('Invalid Pair key format: invalid')
|
|
24
|
+
expect(() => Pair.fromKey('Pair(only_one)')).toThrow('Invalid Pair key format')
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('should check equality correctly', () => {
|
|
28
|
+
const pair1 = new Pair('a', 'b')
|
|
29
|
+
const pair2 = new Pair('a', 'b')
|
|
30
|
+
const pair3 = new Pair('a', 'c')
|
|
31
|
+
|
|
32
|
+
expect(pair1.equals(pair2)).toBe(true)
|
|
33
|
+
expect(pair1.equals(pair3)).toBe(false)
|
|
34
|
+
})
|
|
35
|
+
})
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
export class Pair<A, B> {
|
|
2
|
+
x: A
|
|
3
|
+
y: B
|
|
4
|
+
|
|
5
|
+
constructor(x: A, y: B) {
|
|
6
|
+
this.x = x
|
|
7
|
+
this.y = y
|
|
8
|
+
}
|
|
9
|
+
toKey() {
|
|
10
|
+
return `Pair(${this.x}, ${this.y})`
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
equals(other: Pair<A, B>) {
|
|
14
|
+
return this.x === other.x && this.y === other.y
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
static fromKey<A, B>(key: string) {
|
|
18
|
+
// Parse the Pair(x, y) format
|
|
19
|
+
const match = key.match(/^Pair\((.*), (.*)\)$/)
|
|
20
|
+
if (!match) {
|
|
21
|
+
throw new Error(`Invalid Pair key format: ${key}`)
|
|
22
|
+
}
|
|
23
|
+
const [, x, y] = match
|
|
24
|
+
return new Pair<A, B>(x as A, y as B)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
static key<X, Y>(x: X, y: Y) {
|
|
28
|
+
return new Pair(x, y).toKey()
|
|
29
|
+
}
|
|
30
|
+
}
|