Skip to content

Commit 64a0a12

Browse files
authoredMar 15, 2025
Add server logs listener composable (#3074)
1 parent 3956e31 commit 64a0a12

File tree

2 files changed

+179
-0
lines changed

2 files changed

+179
-0
lines changed
 

‎src/composables/useServerLogs.ts

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import { useEventListener } from '@vueuse/core'
2+
import { onUnmounted, ref } from 'vue'
3+
4+
import { LogsWsMessage } from '@/schemas/apiSchema'
5+
import { api } from '@/scripts/api'
6+
7+
const LOGS_MESSAGE_TYPE = 'logs'
8+
9+
interface UseServerLogsOptions {
10+
immediate?: boolean
11+
messageFilter?: (message: string) => boolean
12+
}
13+
14+
export const useServerLogs = (options: UseServerLogsOptions = {}) => {
15+
const {
16+
immediate = false,
17+
messageFilter = (msg: string) => Boolean(msg.trim())
18+
} = options
19+
20+
const logs = ref<string[]>([])
21+
let stop: ReturnType<typeof useEventListener> | null = null
22+
23+
const isValidLogEvent = (event: CustomEvent<LogsWsMessage>) =>
24+
event?.type === LOGS_MESSAGE_TYPE && event.detail?.entries?.length > 0
25+
26+
const parseLogMessage = (event: CustomEvent<LogsWsMessage>) =>
27+
event.detail.entries.map((e) => e.m).filter(messageFilter)
28+
29+
const handleLogMessage = (event: CustomEvent<LogsWsMessage>) => {
30+
if (isValidLogEvent(event)) {
31+
logs.value.push(...parseLogMessage(event))
32+
}
33+
}
34+
35+
const start = () => {
36+
api.subscribeLogs(true)
37+
stop = useEventListener(api, LOGS_MESSAGE_TYPE, handleLogMessage)
38+
}
39+
40+
const stopListening = () => {
41+
stop?.()
42+
stop = null
43+
api.subscribeLogs(false)
44+
}
45+
46+
if (immediate) {
47+
start()
48+
}
49+
50+
onUnmounted(() => {
51+
stopListening()
52+
logs.value = []
53+
})
54+
55+
return {
56+
logs,
57+
startListening: start,
58+
stopListening
59+
}
60+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
import { useEventListener } from '@vueuse/core'
2+
import { beforeEach, describe, expect, it, vi } from 'vitest'
3+
import { nextTick } from 'vue'
4+
5+
import { useServerLogs } from '@/composables/useServerLogs'
6+
import { LogsWsMessage } from '@/schemas/apiSchema'
7+
import { api } from '@/scripts/api'
8+
9+
vi.mock('@/scripts/api', () => ({
10+
api: {
11+
subscribeLogs: vi.fn(),
12+
addEventListener: vi.fn(),
13+
removeEventListener: vi.fn()
14+
}
15+
}))
16+
17+
vi.mock('@vueuse/core', () => ({
18+
useEventListener: vi.fn().mockReturnValue(vi.fn())
19+
}))
20+
21+
describe('useServerLogs', () => {
22+
beforeEach(() => {
23+
vi.clearAllMocks()
24+
})
25+
26+
it('should initialize with empty logs array', () => {
27+
const { logs } = useServerLogs()
28+
expect(logs.value).toEqual([])
29+
})
30+
31+
it('should not subscribe to logs by default', () => {
32+
useServerLogs()
33+
expect(api.subscribeLogs).not.toHaveBeenCalled()
34+
})
35+
36+
it('should subscribe to logs when immediate is true', () => {
37+
useServerLogs({ immediate: true })
38+
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
39+
})
40+
41+
it('should start listening when startListening is called', () => {
42+
const { startListening } = useServerLogs()
43+
44+
startListening()
45+
46+
expect(api.subscribeLogs).toHaveBeenCalledWith(true)
47+
})
48+
49+
it('should stop listening when stopListening is called', () => {
50+
const { startListening, stopListening } = useServerLogs()
51+
52+
startListening()
53+
stopListening()
54+
55+
expect(api.subscribeLogs).toHaveBeenCalledWith(false)
56+
})
57+
58+
it('should register event listener when starting', () => {
59+
const { startListening } = useServerLogs()
60+
61+
startListening()
62+
63+
expect(vi.mocked(useEventListener)).toHaveBeenCalledWith(
64+
api,
65+
'logs',
66+
expect.any(Function)
67+
)
68+
})
69+
70+
it('should handle log messages correctly', async () => {
71+
const { logs, startListening } = useServerLogs()
72+
startListening()
73+
74+
// Get the callback that was registered with useEventListener
75+
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
76+
event: CustomEvent<LogsWsMessage>
77+
) => void
78+
79+
// Simulate receiving a log event
80+
const mockEvent = new CustomEvent('logs', {
81+
detail: {
82+
type: 'logs',
83+
entries: [{ m: 'Log message 1' }, { m: 'Log message 2' }]
84+
} as unknown as LogsWsMessage
85+
}) as CustomEvent<LogsWsMessage>
86+
87+
eventCallback(mockEvent)
88+
await nextTick()
89+
90+
expect(logs.value).toEqual(['Log message 1', 'Log message 2'])
91+
})
92+
93+
it('should use the message filter if provided', async () => {
94+
const { logs, startListening } = useServerLogs({
95+
messageFilter: (msg) => msg !== 'remove me'
96+
})
97+
startListening()
98+
99+
const eventCallback = vi.mocked(useEventListener).mock.calls[0][2] as (
100+
event: CustomEvent<LogsWsMessage>
101+
) => void
102+
103+
const mockEvent = new CustomEvent('logs', {
104+
detail: {
105+
type: 'logs',
106+
entries: [
107+
{ m: 'Log message 1 dont remove me' },
108+
{ m: 'remove me' },
109+
{ m: '' }
110+
]
111+
} as unknown as LogsWsMessage
112+
}) as CustomEvent<LogsWsMessage>
113+
114+
eventCallback(mockEvent)
115+
await nextTick()
116+
117+
expect(logs.value).toEqual(['Log message 1 dont remove me', ''])
118+
})
119+
})

0 commit comments

Comments
 (0)