@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.
Files changed (43) hide show
  1. package/README.md +75 -10
  2. package/dist/actions/connection.d.ts +2 -0
  3. package/dist/actions/connection.d.ts.map +1 -1
  4. package/dist/actions/connection.js +20 -9
  5. package/dist/actions/connection.js.map +1 -1
  6. package/dist/actions/subject.d.ts +5 -1
  7. package/dist/actions/subject.d.ts.map +1 -1
  8. package/dist/actions/subject.js +60 -16
  9. package/dist/actions/subject.js.map +1 -1
  10. package/dist/index.d.ts +1 -0
  11. package/dist/index.d.ts.map +1 -1
  12. package/dist/index.js +1 -0
  13. package/dist/index.js.map +1 -1
  14. package/dist/machines/credentials.d.ts +112 -0
  15. package/dist/machines/credentials.d.ts.map +1 -0
  16. package/dist/machines/credentials.js +252 -0
  17. package/dist/machines/credentials.js.map +1 -0
  18. package/dist/machines/root.d.ts +190 -3
  19. package/dist/machines/root.d.ts.map +1 -1
  20. package/dist/machines/root.js +107 -13
  21. package/dist/machines/root.js.map +1 -1
  22. package/dist/machines/subject.d.ts +7 -2
  23. package/dist/machines/subject.d.ts.map +1 -1
  24. package/dist/machines/subject.js +11 -1
  25. package/dist/machines/subject.js.map +1 -1
  26. package/dist/telemetry.d.ts +2 -0
  27. package/dist/telemetry.d.ts.map +1 -1
  28. package/dist/telemetry.js +18 -1
  29. package/dist/telemetry.js.map +1 -1
  30. package/package.json +1 -1
  31. package/src/actions/connection.test.ts +62 -7
  32. package/src/actions/connection.ts +28 -8
  33. package/src/actions/subject.test.ts +120 -0
  34. package/src/actions/subject.ts +79 -19
  35. package/src/index.ts +9 -0
  36. package/src/machines/credentials.telemetry.test.ts +66 -0
  37. package/src/machines/credentials.test.ts +175 -0
  38. package/src/machines/credentials.ts +320 -0
  39. package/src/machines/root.test.ts +88 -1
  40. package/src/machines/root.ts +115 -13
  41. package/src/machines/subject.test.ts +60 -0
  42. package/src/machines/subject.ts +18 -1
  43. package/src/telemetry.ts +35 -1
@@ -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': context.natsConfig?.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: ['configureNatsAfterClose', 'reconnectAfterClose', lifecycleAction('connected')],
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 { ...event, connection: context.connection }
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()
@@ -1,8 +1,15 @@
1
- import { Subscription, PublishOptions, NatsConnection, RequestOptions } from '@nats-io/nats-core'
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 { context as otelContext, propagation, SpanStatusCode, trace } from '@opentelemetry/api'
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)