@jr200-labs/xstate-nats 0.9.21 → 0.9.22
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/README.md +75 -10
- package/dist/actions/connection.d.ts +2 -0
- package/dist/actions/connection.d.ts.map +1 -1
- package/dist/actions/connection.js +20 -9
- package/dist/actions/connection.js.map +1 -1
- package/dist/actions/subject.d.ts +5 -1
- package/dist/actions/subject.d.ts.map +1 -1
- package/dist/actions/subject.js +60 -16
- package/dist/actions/subject.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/machines/credentials.d.ts +112 -0
- package/dist/machines/credentials.d.ts.map +1 -0
- package/dist/machines/credentials.js +252 -0
- package/dist/machines/credentials.js.map +1 -0
- package/dist/machines/root.d.ts +190 -3
- package/dist/machines/root.d.ts.map +1 -1
- package/dist/machines/root.js +107 -13
- package/dist/machines/root.js.map +1 -1
- package/dist/machines/subject.d.ts +7 -2
- package/dist/machines/subject.d.ts.map +1 -1
- package/dist/machines/subject.js +11 -1
- package/dist/machines/subject.js.map +1 -1
- package/dist/telemetry.d.ts +2 -0
- package/dist/telemetry.d.ts.map +1 -1
- package/dist/telemetry.js +18 -1
- package/dist/telemetry.js.map +1 -1
- package/package.json +1 -1
- package/src/actions/connection.test.ts +62 -7
- package/src/actions/connection.ts +28 -8
- package/src/actions/subject.test.ts +120 -0
- package/src/actions/subject.ts +79 -19
- package/src/index.ts +9 -0
- package/src/machines/credentials.telemetry.test.ts +66 -0
- package/src/machines/credentials.test.ts +175 -0
- package/src/machines/credentials.ts +320 -0
- package/src/machines/root.test.ts +88 -1
- package/src/machines/root.ts +115 -13
- package/src/machines/subject.test.ts +60 -0
- package/src/machines/subject.ts +18 -1
- package/src/telemetry.ts +35 -1
package/src/machines/root.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { ConnectionOptions, NatsConnection } from '@nats-io/nats-core'
|
|
2
|
-
import { assign, sendTo, setup } from 'xstate'
|
|
2
|
+
import { assign, sendTo, setup, stopChild } from 'xstate'
|
|
3
3
|
import { kvManagerLogic, ExternalEvents as KvExternalEvents } from './kv'
|
|
4
4
|
import { subjectManagerLogic, ExternalEvents as SubjectExternalEvents } from './subject'
|
|
5
5
|
import { connectToNats, disconnectNats } from '../actions/connection'
|
|
@@ -7,6 +7,13 @@ import { type AuthConfig } from '../actions/types'
|
|
|
7
7
|
import { InternalStatusEvents as NatsStatusEvents } from '../actions/connection'
|
|
8
8
|
import { withSpan } from '../telemetry'
|
|
9
9
|
import { NatsTrafficResetEvent } from '../traffic'
|
|
10
|
+
import { rejectUnavailableRequest } from '../actions/subject'
|
|
11
|
+
import {
|
|
12
|
+
CredentialActor,
|
|
13
|
+
credentialMachine,
|
|
14
|
+
getCredentials,
|
|
15
|
+
NatsCredentialConfig,
|
|
16
|
+
} from './credentials'
|
|
10
17
|
|
|
11
18
|
export interface NatsDiagnosticsConfig {
|
|
12
19
|
lifecycle?: boolean
|
|
@@ -15,6 +22,7 @@ export interface NatsDiagnosticsConfig {
|
|
|
15
22
|
export interface NatsConnectionConfig {
|
|
16
23
|
opts: ConnectionOptions
|
|
17
24
|
auth?: AuthConfig
|
|
25
|
+
credentials?: NatsCredentialConfig
|
|
18
26
|
maxRetries: number
|
|
19
27
|
diagnostics?: NatsDiagnosticsConfig
|
|
20
28
|
}
|
|
@@ -28,6 +36,7 @@ export interface Context {
|
|
|
28
36
|
kvManagerReady: boolean
|
|
29
37
|
configureAfterClose: boolean
|
|
30
38
|
connectAfterClose: boolean
|
|
39
|
+
credentialActor?: CredentialActor
|
|
31
40
|
}
|
|
32
41
|
|
|
33
42
|
// internal events and events from nats connection
|
|
@@ -37,6 +46,7 @@ type InternalEvents =
|
|
|
37
46
|
| { type: 'FAIL'; error: Error }
|
|
38
47
|
| { type: 'RECONNECT' }
|
|
39
48
|
| { type: 'CLOSE' }
|
|
49
|
+
| { type: 'CREDENTIALS.UNAVAILABLE'; error: Error }
|
|
40
50
|
| NatsStatusEvents
|
|
41
51
|
|
|
42
52
|
// events which can be sent to the machine from the user
|
|
@@ -45,6 +55,7 @@ export type ExternalEvents =
|
|
|
45
55
|
| { type: 'CONNECT' }
|
|
46
56
|
| { type: 'DISCONNECT' }
|
|
47
57
|
| { type: 'RESET' }
|
|
58
|
+
| { type: 'CREDENTIALS.RELOAD' }
|
|
48
59
|
| NatsTrafficResetEvent
|
|
49
60
|
| SubjectExternalEvents
|
|
50
61
|
| KvExternalEvents
|
|
@@ -72,7 +83,9 @@ function lifecycleAttributes(
|
|
|
72
83
|
'nats.debug': Boolean(context.natsConfig?.opts.debug),
|
|
73
84
|
'nats.verbose': Boolean(context.natsConfig?.opts.verbose),
|
|
74
85
|
'nats.max_retries': context.natsConfig?.maxRetries,
|
|
75
|
-
'nats.auth.type':
|
|
86
|
+
'nats.auth.type':
|
|
87
|
+
context.credentialActor?.getSnapshot().context.current?.auth?.type ??
|
|
88
|
+
context.natsConfig?.auth?.type,
|
|
76
89
|
'nats.has_connection': context.connection !== null,
|
|
77
90
|
'nats.subject_manager_ready': context.subjectManagerReady,
|
|
78
91
|
'nats.kv_manager_ready': context.kvManagerReady,
|
|
@@ -103,6 +116,17 @@ export const natsMachine = setup({
|
|
|
103
116
|
events: {} as Events,
|
|
104
117
|
},
|
|
105
118
|
actions: {
|
|
119
|
+
stopCredentialActor: stopChild(({ context }) => context.credentialActor as CredentialActor),
|
|
120
|
+
reloadCredentials: ({ context }) => {
|
|
121
|
+
context.credentialActor?.send({ type: 'CREDENTIALS.RELOAD' })
|
|
122
|
+
},
|
|
123
|
+
recordCredentialError: assign({
|
|
124
|
+
error: ({ event }) => (event.type === 'CREDENTIALS.UNAVAILABLE' ? event.error : undefined),
|
|
125
|
+
}),
|
|
126
|
+
rejectUnavailableRequest: ({ event }) => {
|
|
127
|
+
if (event.type !== 'SUBJECT.REQUEST') return
|
|
128
|
+
rejectUnavailableRequest(event)
|
|
129
|
+
},
|
|
106
130
|
doReset: assign({
|
|
107
131
|
natsConfig: (_) => undefined,
|
|
108
132
|
connection: (_) => null,
|
|
@@ -112,6 +136,7 @@ export const natsMachine = setup({
|
|
|
112
136
|
kvManagerReady: (_) => false,
|
|
113
137
|
configureAfterClose: (_) => false,
|
|
114
138
|
connectAfterClose: (_) => false,
|
|
139
|
+
credentialActor: (_) => undefined,
|
|
115
140
|
}),
|
|
116
141
|
configureNats: assign({
|
|
117
142
|
natsConfig: ({ event }) => {
|
|
@@ -127,6 +152,22 @@ export const natsMachine = setup({
|
|
|
127
152
|
kvManagerReady: (_) => false,
|
|
128
153
|
configureAfterClose: (_) => false,
|
|
129
154
|
connectAfterClose: (_) => false,
|
|
155
|
+
credentialActor: ({ event, spawn, self }) => {
|
|
156
|
+
if (event.type !== 'CONFIGURE' || !event.config.credentials) return undefined
|
|
157
|
+
const onUnavailable = event.config.credentials.onUnavailable
|
|
158
|
+
return spawn('credentials', {
|
|
159
|
+
input: {
|
|
160
|
+
...event.config.credentials,
|
|
161
|
+
onUnavailable: (error) => {
|
|
162
|
+
try {
|
|
163
|
+
onUnavailable?.(error)
|
|
164
|
+
} finally {
|
|
165
|
+
self.send({ type: 'CREDENTIALS.UNAVAILABLE', error })
|
|
166
|
+
}
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
})
|
|
170
|
+
},
|
|
130
171
|
}),
|
|
131
172
|
configureNatsAfterClose: assign({
|
|
132
173
|
natsConfig: ({ event }) => {
|
|
@@ -140,6 +181,22 @@ export const natsMachine = setup({
|
|
|
140
181
|
subjectManagerReady: (_) => false,
|
|
141
182
|
kvManagerReady: (_) => false,
|
|
142
183
|
configureAfterClose: (_) => true,
|
|
184
|
+
credentialActor: ({ event, spawn, self }) => {
|
|
185
|
+
if (event.type !== 'CONFIGURE' || !event.config.credentials) return undefined
|
|
186
|
+
const onUnavailable = event.config.credentials.onUnavailable
|
|
187
|
+
return spawn('credentials', {
|
|
188
|
+
input: {
|
|
189
|
+
...event.config.credentials,
|
|
190
|
+
onUnavailable: (error) => {
|
|
191
|
+
try {
|
|
192
|
+
onUnavailable?.(error)
|
|
193
|
+
} finally {
|
|
194
|
+
self.send({ type: 'CREDENTIALS.UNAVAILABLE', error })
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
})
|
|
199
|
+
},
|
|
143
200
|
}),
|
|
144
201
|
reconnectAfterClose: assign({
|
|
145
202
|
connectAfterClose: (_) => true,
|
|
@@ -168,6 +225,7 @@ export const natsMachine = setup({
|
|
|
168
225
|
disconnectNats: disconnectNats,
|
|
169
226
|
subject: subjectManagerLogic,
|
|
170
227
|
kv: kvManagerLogic,
|
|
228
|
+
credentials: credentialMachine,
|
|
171
229
|
},
|
|
172
230
|
}).createMachine({
|
|
173
231
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOnwHsAXAfU3PwDNcoBXAJ0gGIBhAeQDkAYgEkA4gFUASgFEA2gAYAuolAAHcrFyVc9FSAAeiABwBOeSQBsAdgCMRgMwAmRxftWALPYCsAGhABPRHd3c3cvext7IwsveRCrAF8EvzQsPEJSOkZmdi4+fn5pbgAVBWUkEHVNbV0KwwQbGw9LJysLExNHSPkjP0CEdzaSMwsXaPt5LxMbeQsklIwcAmISLMJMbXwoTgh6MBICADdyAGt91KWM1fp1zagEI-JMdBr8MrK9Kq0dfD16m3CVhIUUcVis8isJjCjlmfUQ4XcLUcRiakXcJicc2SIAu6RWazAGwI2zAbDY5DYJFUABsXgwKagSLjlpkboS7g98Mdnq93kpPhpvrVQP9AcCjKDwZDobCAkF3DYSE0TNZ2p1ptF3PMcYs8az8Lc8gAZXgAZTk-IqX1ef0QNlGQK60TMEUGVi8NjhCFciJmGIxRmlYJs2uZV0w1MFWx2ewOXNO511LNWkc0W053JePz55TUgptdUQyK8JDiFiMXhC8hco3k9i9mJIVk17Rs03LYPsoaT4dTxM4pPJlJpdIZTJ7+L76cePOzSg+VvzP1tCGLpfc5cr8mrFlr9blDUrJFi2-kKs6NlB3bSyYjGi4MnNpUteeqy8LCCm9mB3kDtjBPRhA21bAme7bupukzXpcKyDhSnCPtIz65pUS7CgYdoWI0x4RG2IRNJ4jjuF6jgqk2LheJCbQRPYWrahQEBwHoYbEAKb7ofUAC0Fhetx0F6mQVC0PQTCsBwEBsUKvwflYXTkTE9qdq49gmA2jgls2FgbhuowmB4-G3iJOTiZJBYinacSKtYKoWHW7pGIGPEHm6JAhKMXikXYNieHRCw3uGbJElspnvuZDQRI4JAokRHkqjCG69M54TDLMXT2CpERmIk2IsfqhoSYu7HSWFTQqcMTrpVhKkool-SVhYx4uKlMylXYBm9lGUAhRxQSOF69omEq8gAruESTB5kTtZO94Fa+UkruiiqpdCHSOLhTn9E4GktjppH6TlE6kHBbDdcVGENFYZUXg49hVSYNVqeYMIRW07TqR42VJEAA */
|
|
@@ -181,12 +239,14 @@ export const natsMachine = setup({
|
|
|
181
239
|
kvManagerReady: false,
|
|
182
240
|
configureAfterClose: false,
|
|
183
241
|
connectAfterClose: false,
|
|
242
|
+
credentialActor: undefined,
|
|
184
243
|
},
|
|
185
244
|
invoke: [
|
|
186
245
|
{ src: 'subject', id: 'subject', systemId: 'subject' },
|
|
187
246
|
{ src: 'kv', id: 'kv' },
|
|
188
247
|
],
|
|
189
248
|
on: {
|
|
249
|
+
'CREDENTIALS.RELOAD': { actions: 'reloadCredentials' },
|
|
190
250
|
'NATS_CONNECTION.CLOSE': {
|
|
191
251
|
target: '.closed',
|
|
192
252
|
actions: [
|
|
@@ -221,9 +281,10 @@ export const natsMachine = setup({
|
|
|
221
281
|
states: {
|
|
222
282
|
not_configured: {
|
|
223
283
|
on: {
|
|
284
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
224
285
|
CONFIGURE: {
|
|
225
286
|
target: 'configured',
|
|
226
|
-
actions: ['configureNats'],
|
|
287
|
+
actions: ['stopCredentialActor', 'configureNats'],
|
|
227
288
|
'*': {
|
|
228
289
|
actions: [
|
|
229
290
|
({ event }: { event: any }) => {
|
|
@@ -237,8 +298,9 @@ export const natsMachine = setup({
|
|
|
237
298
|
configured: {
|
|
238
299
|
entry: ['clearDeferredCloseActions'],
|
|
239
300
|
on: {
|
|
301
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
240
302
|
CONFIGURE: {
|
|
241
|
-
actions: ['configureNats', lifecycleAction('configured')],
|
|
303
|
+
actions: ['stopCredentialActor', 'configureNats', lifecycleAction('configured')],
|
|
242
304
|
},
|
|
243
305
|
CONNECT: {
|
|
244
306
|
target: 'connecting',
|
|
@@ -246,7 +308,7 @@ export const natsMachine = setup({
|
|
|
246
308
|
},
|
|
247
309
|
RESET: {
|
|
248
310
|
target: 'not_configured',
|
|
249
|
-
actions: ['doReset'],
|
|
311
|
+
actions: ['stopCredentialActor', 'doReset'],
|
|
250
312
|
},
|
|
251
313
|
'*': {
|
|
252
314
|
actions: [
|
|
@@ -260,10 +322,11 @@ export const natsMachine = setup({
|
|
|
260
322
|
connecting: {
|
|
261
323
|
entry: ['clearDeferredCloseActions', lifecycleAction('connecting')],
|
|
262
324
|
on: {
|
|
325
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
263
326
|
CONFIGURE: {
|
|
264
327
|
target: 'connecting',
|
|
265
328
|
reenter: true,
|
|
266
|
-
actions: ['configureNats', lifecycleAction('connecting')],
|
|
329
|
+
actions: ['stopCredentialActor', 'configureNats', lifecycleAction('connecting')],
|
|
267
330
|
},
|
|
268
331
|
CONNECT: {
|
|
269
332
|
actions: lifecycleAction('connecting'),
|
|
@@ -275,6 +338,7 @@ export const natsMachine = setup({
|
|
|
275
338
|
input: ({ context, self }) => ({
|
|
276
339
|
opts: context.natsConfig!.opts,
|
|
277
340
|
auth: context.natsConfig!.auth,
|
|
341
|
+
credentialActor: context.credentialActor,
|
|
278
342
|
onStatus: (event: NatsStatusEvents) => self.send(event),
|
|
279
343
|
}),
|
|
280
344
|
onDone: {
|
|
@@ -283,6 +347,7 @@ export const natsMachine = setup({
|
|
|
283
347
|
lifecycleAction('connecting'),
|
|
284
348
|
assign({
|
|
285
349
|
connection: ({ event }) => event.output,
|
|
350
|
+
error: (_) => undefined,
|
|
286
351
|
retries: (_) => 0,
|
|
287
352
|
}),
|
|
288
353
|
],
|
|
@@ -310,9 +375,11 @@ export const natsMachine = setup({
|
|
|
310
375
|
sendTo('kv', ({ context }) => ({ type: 'KV.CONNECT', connection: context.connection! })),
|
|
311
376
|
],
|
|
312
377
|
on: {
|
|
378
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
313
379
|
CONFIGURE: {
|
|
314
380
|
target: 'closing',
|
|
315
381
|
actions: [
|
|
382
|
+
'stopCredentialActor',
|
|
316
383
|
'configureNatsAfterClose',
|
|
317
384
|
'reconnectAfterClose',
|
|
318
385
|
lifecycleAction('initialise_managers'),
|
|
@@ -341,6 +408,14 @@ export const natsMachine = setup({
|
|
|
341
408
|
lifecycleAction('initialise_managers'),
|
|
342
409
|
],
|
|
343
410
|
},
|
|
411
|
+
'CREDENTIALS.UNAVAILABLE': {
|
|
412
|
+
target: 'closing',
|
|
413
|
+
actions: [
|
|
414
|
+
'recordCredentialError',
|
|
415
|
+
'clearDeferredCloseActions',
|
|
416
|
+
lifecycleAction('initialise_managers'),
|
|
417
|
+
],
|
|
418
|
+
},
|
|
344
419
|
},
|
|
345
420
|
always: [
|
|
346
421
|
{
|
|
@@ -373,7 +448,12 @@ export const natsMachine = setup({
|
|
|
373
448
|
},
|
|
374
449
|
CONFIGURE: {
|
|
375
450
|
target: 'closing',
|
|
376
|
-
actions: [
|
|
451
|
+
actions: [
|
|
452
|
+
'stopCredentialActor',
|
|
453
|
+
'configureNatsAfterClose',
|
|
454
|
+
'reconnectAfterClose',
|
|
455
|
+
lifecycleAction('connected'),
|
|
456
|
+
],
|
|
377
457
|
},
|
|
378
458
|
DISCONNECT: {
|
|
379
459
|
target: 'closing',
|
|
@@ -383,6 +463,14 @@ export const natsMachine = setup({
|
|
|
383
463
|
target: 'closing',
|
|
384
464
|
actions: ['clearDeferredCloseActions', lifecycleAction('connected')],
|
|
385
465
|
},
|
|
466
|
+
'CREDENTIALS.UNAVAILABLE': {
|
|
467
|
+
target: 'closing',
|
|
468
|
+
actions: [
|
|
469
|
+
'recordCredentialError',
|
|
470
|
+
'clearDeferredCloseActions',
|
|
471
|
+
lifecycleAction('connected'),
|
|
472
|
+
],
|
|
473
|
+
},
|
|
386
474
|
'SUBJECT.*': {
|
|
387
475
|
actions: [
|
|
388
476
|
({ context, event }: { context: Context; event: any }) => {
|
|
@@ -393,7 +481,18 @@ export const natsMachine = setup({
|
|
|
393
481
|
sendTo(
|
|
394
482
|
'subject',
|
|
395
483
|
({ event, context }: { event: SubjectExternalEvents; context: Context }) => {
|
|
396
|
-
return {
|
|
484
|
+
return {
|
|
485
|
+
...event,
|
|
486
|
+
connection: context.connection,
|
|
487
|
+
requestHeaders: context.credentialActor
|
|
488
|
+
? () =>
|
|
489
|
+
getCredentials(context.credentialActor!).then(
|
|
490
|
+
(value) => value.requestHeaders,
|
|
491
|
+
)
|
|
492
|
+
: event.type === 'SUBJECT.REQUEST'
|
|
493
|
+
? event.requestHeaders
|
|
494
|
+
: undefined,
|
|
495
|
+
}
|
|
397
496
|
},
|
|
398
497
|
),
|
|
399
498
|
],
|
|
@@ -460,11 +559,12 @@ export const natsMachine = setup({
|
|
|
460
559
|
},
|
|
461
560
|
},
|
|
462
561
|
on: {
|
|
562
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
463
563
|
'NATS_CONNECTION.*': {
|
|
464
564
|
actions: lifecycleAction('closing'),
|
|
465
565
|
},
|
|
466
566
|
CONFIGURE: {
|
|
467
|
-
actions: ['configureNatsAfterClose', lifecycleAction('closing')],
|
|
567
|
+
actions: ['stopCredentialActor', 'configureNatsAfterClose', lifecycleAction('closing')],
|
|
468
568
|
},
|
|
469
569
|
CONNECT: {
|
|
470
570
|
actions: ['reconnectAfterClose', lifecycleAction('closing')],
|
|
@@ -485,13 +585,14 @@ export const natsMachine = setup({
|
|
|
485
585
|
}),
|
|
486
586
|
],
|
|
487
587
|
on: {
|
|
588
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
488
589
|
CONFIGURE: {
|
|
489
590
|
target: 'configured',
|
|
490
|
-
actions: ['configureNats', lifecycleAction('closed')],
|
|
591
|
+
actions: ['stopCredentialActor', 'configureNats', lifecycleAction('closed')],
|
|
491
592
|
},
|
|
492
593
|
RESET: {
|
|
493
594
|
target: 'not_configured',
|
|
494
|
-
actions: ['doReset'],
|
|
595
|
+
actions: ['stopCredentialActor', 'doReset'],
|
|
495
596
|
},
|
|
496
597
|
CONNECT: {
|
|
497
598
|
target: 'connecting',
|
|
@@ -509,17 +610,18 @@ export const natsMachine = setup({
|
|
|
509
610
|
error: {
|
|
510
611
|
entry: ['clearDeferredCloseActions', lifecycleAction('error')],
|
|
511
612
|
on: {
|
|
613
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
512
614
|
CONNECT: {
|
|
513
615
|
target: 'connecting',
|
|
514
616
|
actions: lifecycleAction('error'),
|
|
515
617
|
},
|
|
516
618
|
CONFIGURE: {
|
|
517
619
|
target: 'configured',
|
|
518
|
-
actions: ['configureNats', lifecycleAction('error')],
|
|
620
|
+
actions: ['stopCredentialActor', 'configureNats', lifecycleAction('error')],
|
|
519
621
|
},
|
|
520
622
|
RESET: {
|
|
521
623
|
target: 'not_configured',
|
|
522
|
-
actions: ['doReset'],
|
|
624
|
+
actions: ['stopCredentialActor', 'doReset'],
|
|
523
625
|
},
|
|
524
626
|
},
|
|
525
627
|
'*': {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
2
|
import { createActor, createMachine, assign, fromPromise, sendTo, setup } from 'xstate'
|
|
3
|
+
import { headers } from '@nats-io/nats-core'
|
|
3
4
|
import { subjectManagerLogic } from './subject'
|
|
4
5
|
|
|
5
6
|
vi.mock('@nats-io/nats-core', async () => {
|
|
@@ -84,6 +85,26 @@ describe('subjectManagerLogic', () => {
|
|
|
84
85
|
parentActor.stop()
|
|
85
86
|
})
|
|
86
87
|
|
|
88
|
+
it('fails requests immediately while disconnected', () => {
|
|
89
|
+
const parentActor = createActor(createParentMachine())
|
|
90
|
+
const onRequestResult = vi.fn()
|
|
91
|
+
parentActor.start()
|
|
92
|
+
|
|
93
|
+
parentActor.send({
|
|
94
|
+
type: 'SUBJECT.REQUEST',
|
|
95
|
+
subject: 'test.req',
|
|
96
|
+
payload: {},
|
|
97
|
+
callback: vi.fn(),
|
|
98
|
+
onRequestResult,
|
|
99
|
+
})
|
|
100
|
+
|
|
101
|
+
expect(onRequestResult).toHaveBeenCalledWith({
|
|
102
|
+
ok: false,
|
|
103
|
+
error: expect.objectContaining({ message: 'NATS connection is not available' }),
|
|
104
|
+
})
|
|
105
|
+
parentActor.stop()
|
|
106
|
+
})
|
|
107
|
+
|
|
87
108
|
it('should have empty subscriptions initially', () => {
|
|
88
109
|
const parentActor = createActor(createParentMachine())
|
|
89
110
|
parentActor.start()
|
|
@@ -289,6 +310,45 @@ describe('subjectManagerLogic', () => {
|
|
|
289
310
|
parentActor.stop()
|
|
290
311
|
})
|
|
291
312
|
|
|
313
|
+
it('gets request headers immediately before sending', async () => {
|
|
314
|
+
const parentActor = createActor(createParentMachine())
|
|
315
|
+
parentActor.start()
|
|
316
|
+
|
|
317
|
+
const connection = createMockConnection()
|
|
318
|
+
parentActor.send({ type: 'SUBJECT.CONNECT', connection })
|
|
319
|
+
const requestHeaders = vi.fn(async () => {
|
|
320
|
+
const authHeaders = headers()
|
|
321
|
+
authHeaders.set('authorization', 'Bearer refreshed-token')
|
|
322
|
+
return authHeaders
|
|
323
|
+
})
|
|
324
|
+
const staleHeaders = headers()
|
|
325
|
+
staleHeaders.set('authorization', 'Bearer stale-token')
|
|
326
|
+
|
|
327
|
+
parentActor.send({
|
|
328
|
+
type: 'SUBJECT.REQUEST',
|
|
329
|
+
subject: 'test.req',
|
|
330
|
+
payload: { data: 1 },
|
|
331
|
+
opts: { headers: staleHeaders, timeout: 1000 },
|
|
332
|
+
callback: vi.fn(),
|
|
333
|
+
requestHeaders,
|
|
334
|
+
})
|
|
335
|
+
|
|
336
|
+
await vi.waitFor(() => {
|
|
337
|
+
expect(connection.request).toHaveBeenCalledWith(
|
|
338
|
+
'test.req',
|
|
339
|
+
{ data: 1 },
|
|
340
|
+
expect.objectContaining({
|
|
341
|
+
headers: expect.objectContaining({ get: expect.any(Function) }),
|
|
342
|
+
}),
|
|
343
|
+
)
|
|
344
|
+
})
|
|
345
|
+
expect(requestHeaders).toHaveBeenCalledOnce()
|
|
346
|
+
expect(connection.request.mock.calls[0][2].headers.get('authorization')).toBe(
|
|
347
|
+
'Bearer refreshed-token',
|
|
348
|
+
)
|
|
349
|
+
parentActor.stop()
|
|
350
|
+
})
|
|
351
|
+
|
|
292
352
|
it('should count request upload and reply download bytes', async () => {
|
|
293
353
|
const parentActor = createActor(createParentMachine())
|
|
294
354
|
parentActor.start()
|
package/src/machines/subject.ts
CHANGED
|
@@ -1,8 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
MsgHdrs,
|
|
3
|
+
Subscription,
|
|
4
|
+
PublishOptions,
|
|
5
|
+
NatsConnection,
|
|
6
|
+
RequestOptions,
|
|
7
|
+
} from '@nats-io/nats-core'
|
|
2
8
|
import { assign, sendParent, setup } from 'xstate'
|
|
3
9
|
import {
|
|
4
10
|
RequestResult,
|
|
5
11
|
SubjectSubscriptionConfig,
|
|
12
|
+
rejectUnavailableRequest,
|
|
6
13
|
subjectConsolidateState,
|
|
7
14
|
subjectRequest,
|
|
8
15
|
subjectPublish,
|
|
@@ -41,6 +48,7 @@ export type ExternalEvents =
|
|
|
41
48
|
opts?: RequestOptions
|
|
42
49
|
callback: (data: any) => void
|
|
43
50
|
onRequestResult?: (result: RequestResult) => void
|
|
51
|
+
requestHeaders?: () => Promise<MsgHdrs | undefined>
|
|
44
52
|
}
|
|
45
53
|
| { type: 'SUBJECT.SUBSCRIBE'; config: SubjectSubscriptionConfig }
|
|
46
54
|
| { type: 'SUBJECT.UNSUBSCRIBE'; subject: string }
|
|
@@ -69,6 +77,12 @@ export const subjectManagerLogic = setup({
|
|
|
69
77
|
return context.syncRequired > 0
|
|
70
78
|
},
|
|
71
79
|
},
|
|
80
|
+
actions: {
|
|
81
|
+
rejectUnavailableRequest: ({ event }) => {
|
|
82
|
+
if (event.type !== 'SUBJECT.REQUEST') return
|
|
83
|
+
rejectUnavailableRequest(event)
|
|
84
|
+
},
|
|
85
|
+
},
|
|
72
86
|
}).createMachine({
|
|
73
87
|
/** @xstate-layout N4IgpgJg5mDOIC5QAoC2BDAxgCwJYDswBKAOlgFcAjAKzEwBcB9XCAGzAGIBlAVQCEAUgFEAwgBVGASQByksZICCAGUlchAbQAMAXUSgADgHtYuerkP49IAB6IATJs0kAnAFYAzAEY7rgDQgAT3tXTxJNAA53KOiYrwBfOP80LDxCUgoaOiYCU1x0VlwTfCgOa1h6dHowEnQAMyqAJ2RXRyIOZJwCYjIqWgZmfFz8woIoLV0kECMTMwsrWwQANk0AFhJPNy8ffyCEHzsSV0jY2M8EpIxOtJ7M-swLQgZIbn5hcUZePi4RACVJPg0Ois01yc0mC08jnCJAA7O4Vu4YX5AvYVjDDnZ4YjXOcQB1Ut0Mn0mPd8I8qhAXoJRBIeNJPt8-gDxsDjKDLODEEiDu5FjDPOEkTtEJ4Yc5cfiuulellGKTyc9Pm8JCIlEIFD8PvxGf9ARMDGzZhzQAs4a4SHY7ItXIttiiEN5oTjcfhDBA4FZJWlWTNzMabIgALSLYUIYMSy4E6W3bJsMA+9nzRArOyhy3QiInU4RlJSm7EgZDApFKAJo1JhArTzmzTV23I3bpsLHLNRM6JPGRvNE2XyrKQMt+iswpEkRaChv2eGHHNXQkyu4WWCGAoQSpgRjldeDsEmkWY0IrFYebyTvZomcdr3zmOMMANBqGBo7-0LFaLC0CoX26KHFuthIEiAA */
|
|
74
88
|
initial: 'subject_idle',
|
|
@@ -150,6 +164,7 @@ export const subjectManagerLogic = setup({
|
|
|
150
164
|
states: {
|
|
151
165
|
subject_idle: {
|
|
152
166
|
on: {
|
|
167
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
153
168
|
'SUBJECT.DISCONNECTED': {},
|
|
154
169
|
'SUBJECT.CONNECT': {
|
|
155
170
|
target: 'subject_check_sync',
|
|
@@ -211,6 +226,7 @@ export const subjectManagerLogic = setup({
|
|
|
211
226
|
opts: event.opts,
|
|
212
227
|
callback: event.callback,
|
|
213
228
|
onRequestResult: event.onRequestResult,
|
|
229
|
+
requestHeaders: event.requestHeaders,
|
|
214
230
|
onDownloadBytes: (bytes) =>
|
|
215
231
|
self.send({
|
|
216
232
|
type: 'METRICS.RECORD',
|
|
@@ -290,6 +306,7 @@ export const subjectManagerLogic = setup({
|
|
|
290
306
|
},
|
|
291
307
|
subject_error: {
|
|
292
308
|
on: {
|
|
309
|
+
'SUBJECT.REQUEST': { actions: 'rejectUnavailableRequest' },
|
|
293
310
|
'SUBJECT.DISCONNECTED': {
|
|
294
311
|
target: 'subject_disconnecting',
|
|
295
312
|
},
|
package/src/telemetry.ts
CHANGED
|
@@ -14,12 +14,46 @@
|
|
|
14
14
|
// the per-message span so cross-process traces nest automatically.
|
|
15
15
|
|
|
16
16
|
import type { Context, Span, Tracer } from '@opentelemetry/api'
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
context as otelContext,
|
|
19
|
+
metrics,
|
|
20
|
+
propagation,
|
|
21
|
+
SpanStatusCode,
|
|
22
|
+
trace,
|
|
23
|
+
} from '@opentelemetry/api'
|
|
18
24
|
import type { MsgHdrs } from '@nats-io/nats-core'
|
|
19
25
|
import { headers as natsHeaders } from '@nats-io/nats-core'
|
|
20
26
|
import pkg from '../package.json' with { type: 'json' }
|
|
21
27
|
|
|
22
28
|
const TRACER_NAME = '@jr200-labs/xstate-nats'
|
|
29
|
+
const meter = metrics.getMeter(TRACER_NAME, pkg.version)
|
|
30
|
+
const credentialOperations = meter.createCounter('xstate.nats.credentials.operations', {
|
|
31
|
+
description: 'Completed NATS credential operations',
|
|
32
|
+
})
|
|
33
|
+
const credentialDuration = meter.createHistogram('xstate.nats.credentials.operation.duration', {
|
|
34
|
+
description: 'NATS credential operation duration',
|
|
35
|
+
unit: 'ms',
|
|
36
|
+
})
|
|
37
|
+
const credentialStateTransitions = meter.createCounter(
|
|
38
|
+
'xstate.nats.credentials.state.transitions',
|
|
39
|
+
{ description: 'NATS credential actor state transitions' },
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
export function recordCredentialOperation(
|
|
43
|
+
operation: 'load' | 'refresh',
|
|
44
|
+
outcome: 'success' | 'error',
|
|
45
|
+
durationMs: number,
|
|
46
|
+
): void {
|
|
47
|
+
const attributes = { operation, outcome }
|
|
48
|
+
credentialOperations.add(1, attributes)
|
|
49
|
+
credentialDuration.record(durationMs, attributes)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function recordCredentialState(
|
|
53
|
+
state: 'loading' | 'ready' | 'refreshing' | 'expired' | 'failed',
|
|
54
|
+
): void {
|
|
55
|
+
credentialStateTransitions.add(1, { state })
|
|
56
|
+
}
|
|
23
57
|
|
|
24
58
|
// Do not cache — `trace.getTracer` already returns a lightweight ProxyTracer,
|
|
25
59
|
// and caching across global-provider swaps (e.g. test teardown / re-register)
|