@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.
- package/dist/actions/kv.d.ts +1 -0
- package/dist/actions/kv.d.ts.map +1 -1
- package/dist/actions/kv.js +9 -2
- package/dist/actions/kv.js.map +1 -1
- package/dist/actions/subject.d.ts +2 -0
- package/dist/actions/subject.d.ts.map +1 -1
- package/dist/actions/subject.js +8 -12
- 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/kv.d.ts +17 -3
- package/dist/machines/kv.d.ts.map +1 -1
- package/dist/machines/kv.js +46 -10
- package/dist/machines/kv.js.map +1 -1
- package/dist/machines/root.d.ts +51 -5
- package/dist/machines/root.d.ts.map +1 -1
- package/dist/machines/root.js +9 -0
- package/dist/machines/root.js.map +1 -1
- package/dist/machines/subject.d.ts +17 -3
- package/dist/machines/subject.d.ts.map +1 -1
- package/dist/machines/subject.js +56 -10
- package/dist/machines/subject.js.map +1 -1
- package/dist/traffic.d.ts +33 -0
- package/dist/traffic.d.ts.map +1 -0
- package/dist/traffic.js +99 -0
- package/dist/traffic.js.map +1 -0
- package/package.json +1 -1
- package/src/actions/kv.test.ts +59 -0
- package/src/actions/kv.ts +10 -2
- package/src/actions/subject.test.ts +41 -0
- package/src/actions/subject.ts +10 -14
- package/src/index.ts +14 -0
- package/src/machines/kv.ts +63 -11
- package/src/machines/root.ts +11 -0
- package/src/machines/subject.test.ts +86 -1
- package/src/machines/subject.ts +74 -11
- package/src/traffic.test.ts +62 -0
- package/src/traffic.ts +150 -0
package/src/machines/kv.ts
CHANGED
|
@@ -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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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) {
|
package/src/machines/root.ts
CHANGED
|
@@ -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
|
|
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()
|
package/src/machines/subject.ts
CHANGED
|
@@ -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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
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
|
+
}
|