@jr200-labs/xstate-nats 0.9.13 → 0.9.14

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 (40) hide show
  1. package/dist/actions/kv.d.ts +1 -0
  2. package/dist/actions/kv.d.ts.map +1 -1
  3. package/dist/actions/kv.js +9 -2
  4. package/dist/actions/kv.js.map +1 -1
  5. package/dist/actions/subject.d.ts +2 -0
  6. package/dist/actions/subject.d.ts.map +1 -1
  7. package/dist/actions/subject.js +8 -12
  8. package/dist/actions/subject.js.map +1 -1
  9. package/dist/index.d.ts +1 -0
  10. package/dist/index.d.ts.map +1 -1
  11. package/dist/index.js +1 -0
  12. package/dist/index.js.map +1 -1
  13. package/dist/machines/kv.d.ts +17 -3
  14. package/dist/machines/kv.d.ts.map +1 -1
  15. package/dist/machines/kv.js +46 -10
  16. package/dist/machines/kv.js.map +1 -1
  17. package/dist/machines/root.d.ts +51 -5
  18. package/dist/machines/root.d.ts.map +1 -1
  19. package/dist/machines/root.js +9 -0
  20. package/dist/machines/root.js.map +1 -1
  21. package/dist/machines/subject.d.ts +17 -3
  22. package/dist/machines/subject.d.ts.map +1 -1
  23. package/dist/machines/subject.js +56 -10
  24. package/dist/machines/subject.js.map +1 -1
  25. package/dist/traffic.d.ts +33 -0
  26. package/dist/traffic.d.ts.map +1 -0
  27. package/dist/traffic.js +99 -0
  28. package/dist/traffic.js.map +1 -0
  29. package/package.json +1 -1
  30. package/src/actions/kv.test.ts +59 -0
  31. package/src/actions/kv.ts +10 -2
  32. package/src/actions/subject.test.ts +41 -0
  33. package/src/actions/subject.ts +10 -14
  34. package/src/index.ts +14 -0
  35. package/src/machines/kv.ts +63 -11
  36. package/src/machines/root.ts +11 -0
  37. package/src/machines/subject.test.ts +86 -1
  38. package/src/machines/subject.ts +74 -11
  39. package/src/traffic.test.ts +62 -0
  40. package/src/traffic.ts +150 -0
@@ -3,9 +3,20 @@ import { jetstream } from '@nats-io/jetstream'
3
3
  import { KvEntry, Kvm, KvOptions, KvStatus, KvWatchEntry } from '@nats-io/kv'
4
4
  import { assign, sendParent, setup } from 'xstate'
5
5
  import { KvSubscriptionKey, KvSubscriptionConfig, kvConsolidateState } from '../actions/kv'
6
+ import {
7
+ byteLength,
8
+ createEmptyTrafficMetrics,
9
+ NatsTrafficMetrics,
10
+ NatsTrafficRecordEvent,
11
+ NatsTrafficResetEvent,
12
+ recordTrafficMetric,
13
+ resetTrafficAll,
14
+ resetTrafficDownload,
15
+ resetTrafficUpload,
16
+ } from '../traffic'
6
17
 
7
18
  // internal events and events from nats connection
8
- type InternalEvents = { type: 'ERROR'; error: Error }
19
+ type InternalEvents = { type: 'ERROR'; error: Error } | NatsTrafficRecordEvent
9
20
 
10
21
  // events which can be sent to the machine from the user
11
22
  export type ExternalEvents =
@@ -49,6 +60,7 @@ export type ExternalEvents =
49
60
  | { type: 'KV.SUBSCRIBE'; config: KvSubscriptionConfig }
50
61
  | { type: 'KV.UNSUBSCRIBE'; bucket: string; key: string }
51
62
  | { type: 'KV.UNSUBSCRIBE_ALL' }
63
+ | NatsTrafficResetEvent
52
64
 
53
65
  export type Events = InternalEvents | ExternalEvents
54
66
 
@@ -60,6 +72,7 @@ export interface Context {
60
72
  subscriptions: Map<string, QueuedIterator<KvWatchEntry>>
61
73
  subscriptionConfigs: Map<string, KvSubscriptionConfig>
62
74
  syncRequired: number
75
+ trafficMetrics: NatsTrafficMetrics
63
76
  error?: Error
64
77
  }
65
78
  export const kvManagerLogic = setup({
@@ -86,19 +99,53 @@ export const kvManagerLogic = setup({
86
99
  subscriptions: new Map<string, QueuedIterator<KvWatchEntry>>(),
87
100
  subscriptionConfigs: new Map<string, KvSubscriptionConfig>(),
88
101
  syncRequired: 0,
102
+ trafficMetrics: createEmptyTrafficMetrics(),
89
103
  },
90
104
  on: {
105
+ 'METRICS.RECORD': {
106
+ actions: assign({
107
+ trafficMetrics: ({ context, event }) => recordTrafficMetric(context.trafficMetrics, event),
108
+ }),
109
+ },
110
+ 'METRICS.RESET_UPLOAD': {
111
+ actions: assign({
112
+ trafficMetrics: ({ context, event }) =>
113
+ resetTrafficUpload(context.trafficMetrics, event.source),
114
+ }),
115
+ },
116
+ 'METRICS.RESET_DOWNLOAD': {
117
+ actions: assign({
118
+ trafficMetrics: ({ context, event }) =>
119
+ resetTrafficDownload(context.trafficMetrics, event.source),
120
+ }),
121
+ },
122
+ 'METRICS.RESET_ALL': {
123
+ actions: assign({
124
+ trafficMetrics: ({ context, event }) =>
125
+ resetTrafficAll(context.trafficMetrics, event.source),
126
+ }),
127
+ },
91
128
  'KV.SUBSCRIBE': {
92
129
  actions: [
93
- assign({
94
- subscriptionConfigs: ({ context, event }) => {
95
- const { config } = event
96
- const newConfigs = new Map(context.subscriptionConfigs)
97
- const newKvKey = KvSubscriptionKey.key(config.bucket, config.key)
98
- newConfigs.set(newKvKey, config)
99
- return newConfigs
100
- },
101
- syncRequired: ({ context }) => context.syncRequired + 1,
130
+ assign(({ context, event, self }) => {
131
+ if (event.type !== 'KV.SUBSCRIBE') return {}
132
+
133
+ const config: KvSubscriptionConfig = {
134
+ ...event.config,
135
+ onDownloadBytes: (bytes) =>
136
+ self.send({
137
+ type: 'METRICS.RECORD',
138
+ source: 'nats.kv',
139
+ downloadBytes: bytes,
140
+ }),
141
+ }
142
+ const newConfigs = new Map(context.subscriptionConfigs)
143
+ const newKvKey = KvSubscriptionKey.key(config.bucket, config.key)
144
+ newConfigs.set(newKvKey, config)
145
+ return {
146
+ subscriptionConfigs: newConfigs,
147
+ syncRequired: context.syncRequired + 1,
148
+ }
102
149
  }),
103
150
  ],
104
151
  },
@@ -252,7 +299,7 @@ export const kvManagerLogic = setup({
252
299
  },
253
300
  },
254
301
  'KV.PUT': {
255
- actions: async ({ context, event }) => {
302
+ actions: async ({ context, event, self }) => {
256
303
  try {
257
304
  const kv = await context.cachedKvm?.open(event.bucket)
258
305
  if (!kv) {
@@ -260,6 +307,11 @@ export const kvManagerLogic = setup({
260
307
  return
261
308
  }
262
309
 
310
+ self.send({
311
+ type: 'METRICS.RECORD',
312
+ source: 'nats.kv',
313
+ uploadBytes: byteLength(event.value),
314
+ })
263
315
  await kv.put(event.key, event.value)
264
316
  event.onResult({ ok: true })
265
317
  } catch (error) {
@@ -6,6 +6,7 @@ import { connectToNats, disconnectNats } from '../actions/connection'
6
6
  import { type AuthConfig } from '../actions/types'
7
7
  import { InternalStatusEvents as NatsStatusEvents } from '../actions/connection'
8
8
  import { withSpan } from '../telemetry'
9
+ import { NatsTrafficResetEvent } from '../traffic'
9
10
 
10
11
  export interface NatsDiagnosticsConfig {
11
12
  lifecycle?: boolean
@@ -44,6 +45,7 @@ export type ExternalEvents =
44
45
  | { type: 'CONNECT' }
45
46
  | { type: 'DISCONNECT' }
46
47
  | { type: 'RESET' }
48
+ | NatsTrafficResetEvent
47
49
  | SubjectExternalEvents
48
50
  | KvExternalEvents
49
51
 
@@ -195,6 +197,15 @@ export const natsMachine = setup({
195
197
  },
196
198
  ],
197
199
  },
200
+ 'METRICS.RESET_UPLOAD': {
201
+ actions: [sendTo('subject', ({ event }) => event), sendTo('kv', ({ event }) => event)],
202
+ },
203
+ 'METRICS.RESET_DOWNLOAD': {
204
+ actions: [sendTo('subject', ({ event }) => event), sendTo('kv', ({ event }) => event)],
205
+ },
206
+ 'METRICS.RESET_ALL': {
207
+ actions: [sendTo('subject', ({ event }) => event), sendTo('kv', ({ event }) => event)],
208
+ },
198
209
  },
199
210
  states: {
200
211
  not_configured: {
@@ -18,7 +18,9 @@ function createMockConnection() {
18
18
  next: () => new Promise(() => {}),
19
19
  }),
20
20
  }),
21
- request: vi.fn().mockResolvedValue({ json: () => ({}), string: () => '{}' }),
21
+ request: vi
22
+ .fn()
23
+ .mockResolvedValue({ data: new Uint8Array([1, 2, 3]), json: () => ({}), string: () => '{}' }),
22
24
  publish: vi.fn(),
23
25
  } as any
24
26
  }
@@ -58,6 +60,9 @@ function createParentMachine() {
58
60
  assign({ childState: 'disconnected' }),
59
61
  ],
60
62
  },
63
+ 'METRICS.*': {
64
+ actions: sendTo('subject', ({ event }: any) => event),
65
+ },
61
66
  },
62
67
  states: {
63
68
  active: {},
@@ -241,6 +246,26 @@ describe('subjectManagerLogic', () => {
241
246
  parentActor.stop()
242
247
  })
243
248
 
249
+ it('should count publish upload bytes', async () => {
250
+ const parentActor = createActor(createParentMachine())
251
+ parentActor.start()
252
+
253
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection: createMockConnection() })
254
+ parentActor.send({
255
+ type: 'SUBJECT.PUBLISH',
256
+ subject: 'test.pub',
257
+ payload: { msg: 'hello' },
258
+ })
259
+
260
+ await vi.waitFor(() => {
261
+ const metrics = parentActor.getSnapshot().children.subject?.getSnapshot()
262
+ ?.context.trafficMetrics
263
+ expect(metrics?.uploadBytes).toBe(15)
264
+ expect(metrics?.bySource['nats.publish']).toEqual({ uploadBytes: 15, downloadBytes: 0 })
265
+ })
266
+ parentActor.stop()
267
+ })
268
+
244
269
  it('should handle SUBJECT.REQUEST when connected', () => {
245
270
  const parentActor = createActor(createParentMachine())
246
271
  parentActor.start()
@@ -264,6 +289,66 @@ describe('subjectManagerLogic', () => {
264
289
  parentActor.stop()
265
290
  })
266
291
 
292
+ it('should count request upload and reply download bytes', async () => {
293
+ const parentActor = createActor(createParentMachine())
294
+ parentActor.start()
295
+
296
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection: createMockConnection() })
297
+ parentActor.send({
298
+ type: 'SUBJECT.REQUEST',
299
+ subject: 'test.req',
300
+ payload: { data: 1 },
301
+ callback: vi.fn(),
302
+ })
303
+
304
+ await vi.waitFor(() => {
305
+ const metrics = parentActor.getSnapshot().children.subject?.getSnapshot()
306
+ ?.context.trafficMetrics
307
+ expect(metrics?.uploadBytes).toBe(10)
308
+ expect(metrics?.downloadBytes).toBe(3)
309
+ expect(metrics?.bySource['nats.request']).toEqual({ uploadBytes: 10, downloadBytes: 3 })
310
+ })
311
+ parentActor.stop()
312
+ })
313
+
314
+ it('should reset upload and download metrics without disconnecting', async () => {
315
+ const parentActor = createActor(createParentMachine())
316
+ parentActor.start()
317
+
318
+ const connection = createMockConnection()
319
+ parentActor.send({ type: 'SUBJECT.CONNECT', connection })
320
+ parentActor.send({
321
+ type: 'SUBJECT.REQUEST',
322
+ subject: 'test.req',
323
+ payload: { data: 1 },
324
+ callback: vi.fn(),
325
+ })
326
+
327
+ await vi.waitFor(() => {
328
+ const metrics = parentActor.getSnapshot().children.subject?.getSnapshot()
329
+ ?.context.trafficMetrics
330
+ expect(metrics?.downloadBytes).toBe(3)
331
+ })
332
+
333
+ parentActor.send({ type: 'METRICS.RESET_UPLOAD' })
334
+ await vi.waitFor(() => {
335
+ const snap = parentActor.getSnapshot().children.subject?.getSnapshot()
336
+ expect(snap?.value).toBe('subject_connected')
337
+ expect(snap?.context.cachedConnection).toBe(connection)
338
+ expect(snap?.context.trafficMetrics.uploadBytes).toBe(0)
339
+ expect(snap?.context.trafficMetrics.downloadBytes).toBe(3)
340
+ })
341
+
342
+ parentActor.send({ type: 'METRICS.RESET_DOWNLOAD' })
343
+ await vi.waitFor(() => {
344
+ const metrics = parentActor.getSnapshot().children.subject?.getSnapshot()
345
+ ?.context.trafficMetrics
346
+ expect(metrics?.uploadBytes).toBe(0)
347
+ expect(metrics?.downloadBytes).toBe(0)
348
+ })
349
+ parentActor.stop()
350
+ })
351
+
267
352
  it('should accept SUBJECT.SUBSCRIBE in idle state (global handler)', () => {
268
353
  const parentActor = createActor(createParentMachine())
269
354
  parentActor.start()
@@ -7,9 +7,20 @@ import {
7
7
  subjectRequest,
8
8
  subjectPublish,
9
9
  } from '../actions/subject'
10
+ import {
11
+ byteLength,
12
+ createEmptyTrafficMetrics,
13
+ NatsTrafficMetrics,
14
+ NatsTrafficRecordEvent,
15
+ NatsTrafficResetEvent,
16
+ recordTrafficMetric,
17
+ resetTrafficAll,
18
+ resetTrafficDownload,
19
+ resetTrafficUpload,
20
+ } from '../traffic'
10
21
 
11
22
  // internal events and events from nats connection
12
- type InternalEvents = { type: 'ERROR'; error: Error }
23
+ type InternalEvents = { type: 'ERROR'; error: Error } | NatsTrafficRecordEvent
13
24
 
14
25
  // events which can be sent to the machine from the user
15
26
  export type ExternalEvents =
@@ -34,6 +45,7 @@ export type ExternalEvents =
34
45
  | { type: 'SUBJECT.SUBSCRIBE'; config: SubjectSubscriptionConfig }
35
46
  | { type: 'SUBJECT.UNSUBSCRIBE'; subject: string }
36
47
  | { type: 'SUBJECT.UNSUBSCRIBE_ALL' }
48
+ | NatsTrafficResetEvent
37
49
 
38
50
  export type Events = InternalEvents | ExternalEvents
39
51
 
@@ -43,6 +55,7 @@ export interface Context {
43
55
  subscriptions: Map<string, Subscription>
44
56
  subscriptionConfigs: Map<string, SubjectSubscriptionConfig>
45
57
  syncRequired: number
58
+ trafficMetrics: NatsTrafficMetrics
46
59
  error?: Error
47
60
  }
48
61
 
@@ -65,19 +78,53 @@ export const subjectManagerLogic = setup({
65
78
  subscriptionConfigs: new Map<string, SubjectSubscriptionConfig>(),
66
79
  cachedConnection: null,
67
80
  syncRequired: 0,
81
+ trafficMetrics: createEmptyTrafficMetrics(),
68
82
  error: undefined,
69
83
  },
70
84
  on: {
85
+ 'METRICS.RECORD': {
86
+ actions: assign({
87
+ trafficMetrics: ({ context, event }) => recordTrafficMetric(context.trafficMetrics, event),
88
+ }),
89
+ },
90
+ 'METRICS.RESET_UPLOAD': {
91
+ actions: assign({
92
+ trafficMetrics: ({ context, event }) =>
93
+ resetTrafficUpload(context.trafficMetrics, event.source),
94
+ }),
95
+ },
96
+ 'METRICS.RESET_DOWNLOAD': {
97
+ actions: assign({
98
+ trafficMetrics: ({ context, event }) =>
99
+ resetTrafficDownload(context.trafficMetrics, event.source),
100
+ }),
101
+ },
102
+ 'METRICS.RESET_ALL': {
103
+ actions: assign({
104
+ trafficMetrics: ({ context, event }) =>
105
+ resetTrafficAll(context.trafficMetrics, event.source),
106
+ }),
107
+ },
71
108
  'SUBJECT.SUBSCRIBE': {
72
109
  actions: [
73
- assign({
74
- subscriptionConfigs: ({ context, event }) => {
75
- const { config } = event
76
- const newConfigs = new Map(context.subscriptionConfigs)
77
- newConfigs.set(config.subject, config)
78
- return newConfigs
79
- },
80
- syncRequired: ({ context }) => context.syncRequired + 1,
110
+ assign(({ context, event, self }) => {
111
+ if (event.type !== 'SUBJECT.SUBSCRIBE') return {}
112
+
113
+ const config: SubjectSubscriptionConfig = {
114
+ ...event.config,
115
+ onDownloadBytes: (bytes) =>
116
+ self.send({
117
+ type: 'METRICS.RECORD',
118
+ source: 'nats.subscribe',
119
+ downloadBytes: bytes,
120
+ }),
121
+ }
122
+ const newConfigs = new Map(context.subscriptionConfigs)
123
+ newConfigs.set(config.subject, config)
124
+ return {
125
+ subscriptionConfigs: newConfigs,
126
+ syncRequired: context.syncRequired + 1,
127
+ }
81
128
  }),
82
129
  ],
83
130
  },
@@ -150,7 +197,12 @@ export const subjectManagerLogic = setup({
150
197
  target: 'subject_disconnecting',
151
198
  },
152
199
  'SUBJECT.REQUEST': {
153
- actions: assign(({ event, context }) => {
200
+ actions: assign(({ event, context, self }) => {
201
+ self.send({
202
+ type: 'METRICS.RECORD',
203
+ source: 'nats.request',
204
+ uploadBytes: byteLength(event.payload),
205
+ })
154
206
  subjectRequest({
155
207
  input: {
156
208
  connection: context.cachedConnection!,
@@ -159,6 +211,12 @@ export const subjectManagerLogic = setup({
159
211
  opts: event.opts,
160
212
  callback: event.callback,
161
213
  onRequestResult: event.onRequestResult,
214
+ onDownloadBytes: (bytes) =>
215
+ self.send({
216
+ type: 'METRICS.RECORD',
217
+ source: 'nats.request',
218
+ downloadBytes: bytes,
219
+ }),
162
220
  },
163
221
  })
164
222
  return {}
@@ -166,7 +224,12 @@ export const subjectManagerLogic = setup({
166
224
  },
167
225
  'SUBJECT.PUBLISH': {
168
226
  actions: [
169
- ({ context, event }) => {
227
+ ({ context, event, self }) => {
228
+ self.send({
229
+ type: 'METRICS.RECORD',
230
+ source: 'nats.publish',
231
+ uploadBytes: byteLength(event.payload),
232
+ })
170
233
  subjectPublish({
171
234
  input: {
172
235
  connection: context.cachedConnection!,
@@ -0,0 +1,62 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import {
3
+ byteLength,
4
+ createEmptyTrafficMetrics,
5
+ recordTrafficMetric,
6
+ resetTrafficAll,
7
+ resetTrafficDownload,
8
+ resetTrafficUpload,
9
+ } from './traffic'
10
+
11
+ describe('traffic metrics', () => {
12
+ it('counts UTF-8 payload bytes', () => {
13
+ expect(byteLength('hello')).toBe(5)
14
+ expect(byteLength('£')).toBe(2)
15
+ expect(byteLength({ msg: 'hello' })).toBe(15)
16
+ expect(byteLength(new Uint8Array([1, 2, 3]))).toBe(3)
17
+ })
18
+
19
+ it('records upload and download bytes by source', () => {
20
+ const metrics = recordTrafficMetric(createEmptyTrafficMetrics(), {
21
+ type: 'METRICS.RECORD',
22
+ source: 'nats.request',
23
+ uploadBytes: 10,
24
+ downloadBytes: 25,
25
+ })
26
+
27
+ expect(metrics.uploadBytes).toBe(10)
28
+ expect(metrics.downloadBytes).toBe(25)
29
+ expect(metrics.bySource['nats.request']).toEqual({ uploadBytes: 10, downloadBytes: 25 })
30
+ })
31
+
32
+ it('resets upload and download independently', () => {
33
+ const metrics = recordTrafficMetric(
34
+ recordTrafficMetric(createEmptyTrafficMetrics(), {
35
+ type: 'METRICS.RECORD',
36
+ source: 'nats.request',
37
+ uploadBytes: 10,
38
+ downloadBytes: 25,
39
+ }),
40
+ {
41
+ type: 'METRICS.RECORD',
42
+ source: 'nats.subscribe',
43
+ downloadBytes: 7,
44
+ },
45
+ )
46
+
47
+ const uploadReset = resetTrafficUpload(metrics)
48
+ expect(uploadReset.uploadBytes).toBe(0)
49
+ expect(uploadReset.downloadBytes).toBe(32)
50
+ expect(uploadReset.bySource['nats.request']).toEqual({ uploadBytes: 0, downloadBytes: 25 })
51
+
52
+ const requestDownloadReset = resetTrafficDownload(metrics, 'nats.request')
53
+ expect(requestDownloadReset.uploadBytes).toBe(10)
54
+ expect(requestDownloadReset.downloadBytes).toBe(7)
55
+ expect(requestDownloadReset.bySource['nats.request']).toEqual({
56
+ uploadBytes: 10,
57
+ downloadBytes: 0,
58
+ })
59
+
60
+ expect(resetTrafficAll(metrics)).toEqual(createEmptyTrafficMetrics())
61
+ })
62
+ })
package/src/traffic.ts ADDED
@@ -0,0 +1,150 @@
1
+ export type NatsTrafficMetricSource = 'nats.publish' | 'nats.request' | 'nats.subscribe' | 'nats.kv'
2
+
3
+ export type NatsTrafficDirection = 'upload' | 'download'
4
+
5
+ export interface NatsTrafficMetricBucket {
6
+ uploadBytes: number
7
+ downloadBytes: number
8
+ }
9
+
10
+ export interface NatsTrafficMetrics extends NatsTrafficMetricBucket {
11
+ bySource: Partial<Record<NatsTrafficMetricSource, NatsTrafficMetricBucket>>
12
+ }
13
+
14
+ export type NatsTrafficRecordEvent = {
15
+ type: 'METRICS.RECORD'
16
+ source: NatsTrafficMetricSource
17
+ uploadBytes?: number
18
+ downloadBytes?: number
19
+ }
20
+
21
+ export type NatsTrafficResetEvent =
22
+ | { type: 'METRICS.RESET_UPLOAD'; source?: NatsTrafficMetricSource }
23
+ | { type: 'METRICS.RESET_DOWNLOAD'; source?: NatsTrafficMetricSource }
24
+ | { type: 'METRICS.RESET_ALL'; source?: NatsTrafficMetricSource }
25
+
26
+ export type NatsTrafficEvent = NatsTrafficRecordEvent | NatsTrafficResetEvent
27
+
28
+ const textEncoder = new TextEncoder()
29
+
30
+ export function createEmptyTrafficMetrics(): NatsTrafficMetrics {
31
+ return {
32
+ uploadBytes: 0,
33
+ downloadBytes: 0,
34
+ bySource: {},
35
+ }
36
+ }
37
+
38
+ export function byteLength(value: unknown): number {
39
+ if (value == null) return 0
40
+ if (value instanceof Uint8Array) return value.byteLength
41
+ if (value instanceof ArrayBuffer) return value.byteLength
42
+ if (ArrayBuffer.isView(value)) return value.byteLength
43
+ if (typeof value === 'string') return textEncoder.encode(value).byteLength
44
+
45
+ try {
46
+ return textEncoder.encode(JSON.stringify(value)).byteLength
47
+ } catch {
48
+ return 0
49
+ }
50
+ }
51
+
52
+ export function recordTrafficMetric(
53
+ metrics: NatsTrafficMetrics,
54
+ event: NatsTrafficRecordEvent,
55
+ ): NatsTrafficMetrics {
56
+ const uploadBytes = normaliseByteCount(event.uploadBytes)
57
+ const downloadBytes = normaliseByteCount(event.downloadBytes)
58
+
59
+ if (uploadBytes === 0 && downloadBytes === 0) return metrics
60
+
61
+ const currentSource = metrics.bySource[event.source] ?? { uploadBytes: 0, downloadBytes: 0 }
62
+ return {
63
+ uploadBytes: metrics.uploadBytes + uploadBytes,
64
+ downloadBytes: metrics.downloadBytes + downloadBytes,
65
+ bySource: {
66
+ ...metrics.bySource,
67
+ [event.source]: {
68
+ uploadBytes: currentSource.uploadBytes + uploadBytes,
69
+ downloadBytes: currentSource.downloadBytes + downloadBytes,
70
+ },
71
+ },
72
+ }
73
+ }
74
+
75
+ export function resetTrafficUpload(
76
+ metrics: NatsTrafficMetrics,
77
+ source?: NatsTrafficMetricSource,
78
+ ): NatsTrafficMetrics {
79
+ return resetTrafficDirection(metrics, 'upload', source)
80
+ }
81
+
82
+ export function resetTrafficDownload(
83
+ metrics: NatsTrafficMetrics,
84
+ source?: NatsTrafficMetricSource,
85
+ ): NatsTrafficMetrics {
86
+ return resetTrafficDirection(metrics, 'download', source)
87
+ }
88
+
89
+ export function resetTrafficAll(
90
+ metrics: NatsTrafficMetrics,
91
+ source?: NatsTrafficMetricSource,
92
+ ): NatsTrafficMetrics {
93
+ if (!source) return createEmptyTrafficMetrics()
94
+
95
+ const sourceMetrics = metrics.bySource[source] ?? { uploadBytes: 0, downloadBytes: 0 }
96
+ const bySource = { ...metrics.bySource }
97
+ delete bySource[source]
98
+
99
+ return {
100
+ uploadBytes: metrics.uploadBytes - sourceMetrics.uploadBytes,
101
+ downloadBytes: metrics.downloadBytes - sourceMetrics.downloadBytes,
102
+ bySource,
103
+ }
104
+ }
105
+
106
+ function resetTrafficDirection(
107
+ metrics: NatsTrafficMetrics,
108
+ direction: NatsTrafficDirection,
109
+ source?: NatsTrafficMetricSource,
110
+ ): NatsTrafficMetrics {
111
+ const key = direction === 'upload' ? 'uploadBytes' : 'downloadBytes'
112
+
113
+ if (!source) {
114
+ const bySource = Object.fromEntries(
115
+ Object.entries(metrics.bySource).map(([sourceName, sourceMetrics]) => [
116
+ sourceName,
117
+ {
118
+ ...sourceMetrics,
119
+ [key]: 0,
120
+ },
121
+ ]),
122
+ ) as NatsTrafficMetrics['bySource']
123
+
124
+ return {
125
+ ...metrics,
126
+ [key]: 0,
127
+ bySource,
128
+ }
129
+ }
130
+
131
+ const sourceMetrics = metrics.bySource[source]
132
+ if (!sourceMetrics) return metrics
133
+
134
+ return {
135
+ ...metrics,
136
+ [key]: metrics[key] - sourceMetrics[key],
137
+ bySource: {
138
+ ...metrics.bySource,
139
+ [source]: {
140
+ ...sourceMetrics,
141
+ [key]: 0,
142
+ },
143
+ },
144
+ }
145
+ }
146
+
147
+ function normaliseByteCount(value: number | undefined): number {
148
+ if (!Number.isFinite(value) || value === undefined || value <= 0) return 0
149
+ return Math.floor(value)
150
+ }