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