Skip to content

Commit 945a3c6

Browse files
committed
feat: add structured status messages for WebSocket Client integration
1 parent 18a7b77 commit 945a3c6

7 files changed

Lines changed: 102 additions & 11 deletions

File tree

packages/timeline-state-resolver-types/src/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,8 @@ exports[`index imports 1`] = `
126126
"VizMSEStatusCode",
127127
"VizMSEStatusMessages",
128128
"VmixActions",
129+
"WebSocketClientStatusCode",
130+
"WebSocketClientStatusMessages",
129131
"WebsocketClientActions",
130132
"fillStateFromDatastore",
131133
"interpolateTemplateString",

packages/timeline-state-resolver-types/src/integrations/websocketClient.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DeviceType } from '../generated/index.js'
2+
import type { DeviceStatusDetail } from '../deviceStatusDetail.js'
23

34
export enum TimelineContentTypeWebSocketClient {
45
WEBSOCKET_MESSAGE = 'websocketMessage',
@@ -16,3 +17,29 @@ export interface TimelineContentWebSocketMessage extends TimelineContentWebSocke
1617
}
1718

1819
export type TimelineContentWebSocketClientAny = TimelineContentWebSocketMessage
20+
21+
export const WebSocketClientStatusCode = {
22+
NOT_CONNECTED: 'DEVICE_WEBSOCKET_CLIENT_NOT_CONNECTED',
23+
CONNECTION_FAILED: 'DEVICE_WEBSOCKET_CLIENT_CONNECTION_FAILED',
24+
} as const
25+
export type WebSocketClientStatusCode = (typeof WebSocketClientStatusCode)[keyof typeof WebSocketClientStatusCode]
26+
27+
export interface WebSocketClientStatusContextMap {
28+
[WebSocketClientStatusCode.NOT_CONNECTED]: {
29+
uri?: string
30+
reason?: string
31+
}
32+
[WebSocketClientStatusCode.CONNECTION_FAILED]: {
33+
uri?: string
34+
error?: string
35+
statusCode?: number
36+
}
37+
}
38+
39+
export type WebSocketClientStatusDetail<T extends WebSocketClientStatusCode = WebSocketClientStatusCode> =
40+
DeviceStatusDetail<T, WebSocketClientStatusContextMap[T]>
41+
42+
export const WebSocketClientStatusMessages: Record<WebSocketClientStatusCode, string> = {
43+
[WebSocketClientStatusCode.NOT_CONNECTED]: 'WS Disconnected: {{uri}} ({{reason}})',
44+
[WebSocketClientStatusCode.CONNECTION_FAILED]: 'WS Connection failed to {{uri}}: {{error}}',
45+
}

packages/timeline-state-resolver/src/__tests__/__snapshots__/index.spec.ts.snap

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -145,6 +145,8 @@ exports[`index imports 1`] = `
145145
"VizMSEStatusCode",
146146
"VizMSEStatusMessages",
147147
"VmixActions",
148+
"WebSocketClientStatusCode",
149+
"WebSocketClientStatusMessages",
148150
"WebsocketClientActions",
149151
"fillStateFromDatastore",
150152
"interpolateTemplateString",

packages/timeline-state-resolver/src/integrations/websocketClient/__tests__/websocketClient.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -104,7 +104,7 @@ describe('WebSocketClientDevice', () => {
104104
MockWebSocketConnection.prototype.connected.mockReturnValue(true)
105105
expect(device.getStatus()).toEqual({
106106
statusCode: StatusCode.BAD,
107-
messages: ['No Connection'],
107+
statusDetails: [],
108108
})
109109

110110
//@ts-expect-error - is set to private

packages/timeline-state-resolver/src/integrations/websocketClient/connection.ts

Lines changed: 50 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,19 @@
11
import WebSocket from 'ws'
2-
import { DeviceStatus, StatusCode, WebsocketClientOptions } from 'timeline-state-resolver-types'
2+
import {
3+
DeviceStatusInput,
4+
StatusCode,
5+
WebsocketClientOptions,
6+
WebSocketClientStatusDetail,
7+
WebSocketClientStatusCode,
8+
} from 'timeline-state-resolver-types'
9+
import { createWebSocketClientStatusDetail } from './messages.js'
310

411
export class WebSocketConnection {
512
private ws?: WebSocket
613
private isWsConnected = false
714
private readonly options: WebsocketClientOptions
15+
private lastError?: { error: string; uri?: string }
16+
private disconnectReason?: string
817

918
constructor(options: WebsocketClientOptions) {
1019
this.options = options
@@ -26,21 +35,38 @@ export class WebSocketConnection {
2635
this.ws.on('open', () => {
2736
clearTimeout(timeout)
2837
this.isWsConnected = true
38+
this.lastError = undefined
39+
this.disconnectReason = undefined
2940
resolve()
3041
})
3142

3243
this.ws.on('error', (error) => {
3344
clearTimeout(timeout)
45+
this.lastError = {
46+
error: error.message || error.toString(),
47+
uri: this.options.webSocket?.uri,
48+
}
3449
reject(error)
3550
})
3651
})
3752

38-
this.ws.on('close', () => {
53+
this.ws.on('close', (code, reason) => {
3954
this.isWsConnected = false
55+
if (reason) {
56+
this.disconnectReason = reason.toString()
57+
} else if (code) {
58+
this.disconnectReason = `Code ${code}`
59+
} else {
60+
this.disconnectReason = undefined
61+
}
4062
})
4163
}
4264
} catch (error) {
4365
this.isWsConnected = false
66+
this.lastError = {
67+
error: error instanceof Error ? error.message : String(error),
68+
uri: this.options.webSocket?.uri,
69+
}
4470
throw error
4571
}
4672
}
@@ -49,13 +75,30 @@ export class WebSocketConnection {
4975
return this.isWsConnected ? true : false
5076
}
5177

52-
connectionStatus(): Omit<DeviceStatus, 'active'> {
53-
const messages: string[] = []
54-
// Prepare for more detailed status messages:
55-
messages.push(this.isWsConnected ? 'WS Connected' : 'WS Disconnected')
78+
connectionStatus(): DeviceStatusInput {
79+
const statusDetails: WebSocketClientStatusDetail[] = []
80+
81+
if (!this.isWsConnected) {
82+
if (this.lastError) {
83+
statusDetails.push(
84+
createWebSocketClientStatusDetail(WebSocketClientStatusCode.CONNECTION_FAILED, {
85+
uri: this.lastError.uri,
86+
error: this.lastError.error,
87+
})
88+
)
89+
} else {
90+
statusDetails.push(
91+
createWebSocketClientStatusDetail(WebSocketClientStatusCode.NOT_CONNECTED, {
92+
uri: this.options.webSocket?.uri,
93+
reason: this.disconnectReason,
94+
})
95+
)
96+
}
97+
}
98+
5699
return {
57100
statusCode: this.isWsConnected ? StatusCode.GOOD : StatusCode.BAD,
58-
messages,
101+
statusDetails,
59102
}
60103
}
61104

packages/timeline-state-resolver/src/integrations/websocketClient/index.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import type {
77
} from 'timeline-state-resolver-api'
88
import {
99
ActionExecutionResultCode,
10-
DeviceStatus,
10+
DeviceStatusInput,
1111
DeviceType,
1212
StatusCode,
1313
TimelineContentTypeWebSocketClient,
@@ -74,8 +74,8 @@ export class WebSocketClientDevice implements Device<
7474
return this.connection?.connected() ?? false
7575
}
7676

77-
public getStatus(): Omit<DeviceStatus, 'active'> {
78-
return this.connection?.connectionStatus() ?? { statusCode: StatusCode.BAD, messages: ['No Connection'] }
77+
public getStatus(): DeviceStatusInput {
78+
return this.connection?.connectionStatus() ?? { statusCode: StatusCode.BAD, statusDetails: [] }
7979
}
8080

8181
public convertTimelineStateToDeviceState(state: DeviceTimelineState<TSRTimelineContent>): WebSocketClientDeviceState {
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
import {
2+
WebSocketClientStatusDetail,
3+
WebSocketClientStatusCode,
4+
WebSocketClientStatusContextMap,
5+
WebSocketClientStatusMessages,
6+
interpolateTemplateString,
7+
} from 'timeline-state-resolver-types'
8+
9+
/**
10+
* Type-safe helper for creating WebSocketClient device status details
11+
*/
12+
export function createWebSocketClientStatusDetail<T extends WebSocketClientStatusCode>(
13+
code: T,
14+
context: WebSocketClientStatusContextMap[T]
15+
): WebSocketClientStatusDetail<T> {
16+
return { code, context, message: interpolateTemplateString(WebSocketClientStatusMessages[code] ?? code, context) }
17+
}

0 commit comments

Comments
 (0)