Skip to content

Commit b18877c

Browse files
authored
add connection closed message on ws close (#148)
1 parent 5e1a5e9 commit b18877c

File tree

6 files changed

+37
-3
lines changed

6 files changed

+37
-3
lines changed

javascript/standalone/src/client.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {
99
ItemInputAudioTranscriptionFailedMessage,
1010
MessageRole,
1111
RealtimeError,
12+
ConnectionClosedMessage,
1213
Response,
1314
ResponseAudioDeltaMessage,
1415
ResponseAudioDoneMessage,
@@ -85,6 +86,12 @@ export class LowLevelRTClient {
8586
}
8687
},
8788
serialize: (message: UserMessageType) => JSON.stringify(message),
89+
createClosedMessage: (_event): ServerMessageType => ({
90+
type: "connection.closed",
91+
event_id: generateId("evt", 32),
92+
code: _event.code,
93+
reason: _event.reason
94+
} as ConnectionClosedMessage),
8895
};
8996

9097
return new WebSocketClient<UserMessageType, ServerMessageType>(

javascript/standalone/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export {
66
AssistantMessageItem,
77
AudioFormat,
88
ClientMessageBase,
9+
ConnectionClosedMessage,
910
ErrorMessage,
1011
FunctionCallItem,
1112
FunctionCallOutputItem,

javascript/standalone/src/model-utils.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ export const isServerMessageType = (
1212
isRealtimeEvent(message) &&
1313
[
1414
"error",
15+
"connection.closed",
1516
"session.created",
1617
"session.updated",
1718
"input_audio_buffer.committed",

javascript/standalone/src/models.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -210,6 +210,12 @@ export interface ErrorMessage extends ServerMessageBase {
210210
error: RealtimeError;
211211
}
212212

213+
export interface ConnectionClosedMessage extends ServerMessageBase {
214+
type: "connection.closed";
215+
code?: number;
216+
reason?: string;
217+
}
218+
213219
export interface Session {
214220
id: string;
215221
model: string;
@@ -547,6 +553,7 @@ export type UserMessageType =
547553

548554
export type ServerMessageType =
549555
| ErrorMessage
556+
| ConnectionClosedMessage
550557
| SessionCreatedMessage
551558
| SessionUpdatedMessage
552559
| InputAudioBufferCommittedMessage

javascript/standalone/src/util/websocket-client.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ export type SerializeMessage<T> = (
4040
export interface MessageProtocolHandler<U, D> {
4141
validate: ValidateProtocolMessage<D>;
4242
serialize: SerializeMessage<U>;
43+
// Optional: create a synthetic protocol message when the websocket closes
44+
createClosedMessage?: (event: CloseEvent) => D;
4345
}
4446

4547
export class WebSocketClient<U, D> implements AsyncIterable<D> {
@@ -50,6 +52,7 @@ export class WebSocketClient<U, D> implements AsyncIterable<D> {
5052
private messageQueue: D[] = [];
5153
private validate: ValidateProtocolMessage<D>;
5254
private serialize: SerializeMessage<U>;
55+
private createClosedMessage?: (event: CloseEvent) => D;
5356

5457
private receiverQueue: [ResolveFn<D>, RejectFn<Error>][] = [];
5558
private done: boolean = false;
@@ -60,6 +63,7 @@ export class WebSocketClient<U, D> implements AsyncIterable<D> {
6063
) {
6164
this.validate = handler.validate;
6265
this.serialize = handler.serialize;
66+
this.createClosedMessage = handler.createClosedMessage;
6367
this.connectedPromise = new Promise(async (resolve, reject) => {
6468
this.socket = await getWebsocket(settings);
6569
this.socket.onopen = () => {
@@ -88,7 +92,17 @@ export class WebSocketClient<U, D> implements AsyncIterable<D> {
8892
private getClosedHandler(
8993
closeResolve: (_: void) => void,
9094
): (_: CloseEvent) => void {
91-
return (_: CloseEvent) => {
95+
return (event: CloseEvent) => {
96+
// If provided, enqueue a synthetic closed message for consumers
97+
if (this.createClosedMessage) {
98+
const closedMsg = this.createClosedMessage(event);
99+
if (this.receiverQueue.length > 0) {
100+
const [resolve, _] = this.receiverQueue.shift()!;
101+
resolve({ value: closedMsg, done: false });
102+
} else {
103+
this.messageQueue.push(closedMsg);
104+
}
105+
}
92106
this.done = true;
93107
while (this.receiverQueue.length > 0) {
94108
const [resolve, reject] = this.receiverQueue.shift()!;
@@ -126,11 +140,11 @@ export class WebSocketClient<U, D> implements AsyncIterable<D> {
126140
next: (): Promise<IteratorResult<D>> => {
127141
if (this.error) {
128142
return Promise.reject(this.error);
129-
} else if (this.done) {
130-
return Promise.resolve({ value: undefined, done: true });
131143
} else if (this.messageQueue.length > 0) {
132144
const message = this.messageQueue.shift()!;
133145
return Promise.resolve({ value: message, done: false });
146+
} else if (this.done) {
147+
return Promise.resolve({ value: undefined, done: true });
134148
} else {
135149
return new Promise((resolve, reject) => {
136150
this.receiverQueue.push([resolve, reject]);

javascript/standalone/test/model-utils.spec.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ describe("isServerMessageType", () => {
246246
},
247247
],
248248
},
249+
{
250+
type: "connection.closed",
251+
event_id: "event32",
252+
},
249253
];
250254

251255
validMessages.forEach((message) => {

0 commit comments

Comments
 (0)