Skip to content

Commit 2a61268

Browse files
authored
feat(server): Add onDisconnect callback (#94)
BREAKING CHANGE: The return function of `server.opened` (`closed`) now requires the close event code and reason for reporting to the `onDisconnect` callback.
1 parent 0ad1c4c commit 2a61268

File tree

6 files changed

+72
-11
lines changed

6 files changed

+72
-11
lines changed

docs/interfaces/_server_.server.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,9 @@ of the `Context`. You may pass the initial request or the
3535
original WebSocket, if you need it down the road.
3636

3737
Returns a function that should be called when the same socket
38-
has been closed, for whatever reason. The returned promise will
39-
resolve once the internal cleanup is complete.
38+
has been closed, for whatever reason. The close code and reason
39+
must be passed for reporting to the `onDisconnect` callback. Returned
40+
promise will resolve once the internal cleanup is complete.
4041

4142
#### Parameters:
4243

docs/interfaces/_server_.serveroptions.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ Name | Default |
2323
* [execute](_server_.serveroptions.md#execute)
2424
* [onComplete](_server_.serveroptions.md#oncomplete)
2525
* [onConnect](_server_.serveroptions.md#onconnect)
26+
* [onDisconnect](_server_.serveroptions.md#ondisconnect)
2627
* [onError](_server_.serveroptions.md#onerror)
2728
* [onNext](_server_.serveroptions.md#onnext)
2829
* [onOperation](_server_.serveroptions.md#onoperation)
@@ -130,6 +131,20 @@ in the close event reason.
130131

131132
___
132133

134+
### onDisconnect
135+
136+
`Optional` **onDisconnect**: undefined \| (ctx: [Context](_server_.context.md)<E\>, code: number, reason: string) => Promise<void\> \| void
137+
138+
Called when the socket/client closes/disconnects for
139+
whatever reason. Provides the close event too. Beware
140+
that this callback happens AFTER all subscriptions have
141+
been gracefuly completed.
142+
143+
If you are interested in tracking the subscriptions completions,
144+
consider using the `onComplete` callback.
145+
146+
___
147+
133148
### onError
134149

135150
`Optional` **onError**: undefined \| (ctx: [Context](_server_.context.md)<E\>, message: [ErrorMessage](_message_.errormessage.md), errors: readonly GraphQLError[]) => Promise<readonly GraphQLError[] \| void\> \| readonly GraphQLError[] \| void

docs/interfaces/_server_.websocket.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@ to validate agains the supported ones.
3636
**close**(`code`: number, `reason`: string): Promise<void\> \| void
3737

3838
Closes the socket gracefully. Will always provide
39-
the appropriate code and close reason.
39+
the appropriate code and close reason. `onDisconnect`
40+
callback will be called.
4041

4142
The returned promise is used to control the graceful
4243
closure.

src/server.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,20 @@ export interface ServerOptions<E = unknown> {
163163
| Record<string, unknown>
164164
| boolean
165165
| void;
166+
/**
167+
* Called when the socket/client closes/disconnects for
168+
* whatever reason. Provides the close event too. Beware
169+
* that this callback happens AFTER all subscriptions have
170+
* been gracefuly completed.
171+
*
172+
* If you are interested in tracking the subscriptions completions,
173+
* consider using the `onComplete` callback.
174+
*/
175+
onDisconnect?: (
176+
ctx: Context<E>,
177+
code: number,
178+
reason: string,
179+
) => Promise<void> | void;
166180
/**
167181
* The subscribe callback executed right after
168182
* acknowledging the request before any payload
@@ -294,10 +308,14 @@ export interface Server<E = undefined> {
294308
* original WebSocket, if you need it down the road.
295309
*
296310
* Returns a function that should be called when the same socket
297-
* has been closed, for whatever reason. The returned promise will
298-
* resolve once the internal cleanup is complete.
311+
* has been closed, for whatever reason. The close code and reason
312+
* must be passed for reporting to the `onDisconnect` callback. Returned
313+
* promise will resolve once the internal cleanup is complete.
299314
*/
300-
opened(socket: WebSocket, ctxExtra: E): () => Promise<void>; // closed
315+
opened(
316+
socket: WebSocket,
317+
ctxExtra: E,
318+
): (code: number, reason: string) => Promise<void>; // closed
301319
}
302320

303321
export interface WebSocket {
@@ -320,7 +338,8 @@ export interface WebSocket {
320338
send(data: string): Promise<void> | void;
321339
/**
322340
* Closes the socket gracefully. Will always provide
323-
* the appropriate code and close reason.
341+
* the appropriate code and close reason. `onDisconnect`
342+
* callback will be called.
324343
*
325344
* The returned promise is used to control the graceful
326345
* closure.
@@ -392,6 +411,7 @@ export function makeServer<E = unknown>(options: ServerOptions<E>): Server<E> {
392411
subscribe,
393412
connectionInitWaitTimeout = 3 * 1000, // 3 seconds
394413
onConnect,
414+
onDisconnect,
395415
onSubscribe,
396416
onOperation,
397417
onNext,
@@ -653,12 +673,13 @@ export function makeServer<E = unknown>(options: ServerOptions<E>): Server<E> {
653673
}
654674
});
655675

656-
// wait for close and cleanup
657-
return async () => {
676+
// wait for close, cleanup and the disconnect callback
677+
return async (code, reason) => {
658678
if (connectionInitWait) clearTimeout(connectionInitWait);
659679
for (const sub of Object.values(ctx.subscriptions)) {
660680
await sub?.return?.();
661681
}
682+
await onDisconnect?.(ctx, code, reason);
662683
};
663684
},
664685
};

src/tests/server.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1383,3 +1383,26 @@ describe('Subscribe', () => {
13831383
client.ws.terminate();
13841384
});
13851385
});
1386+
1387+
describe('Disconnect', () => {
1388+
it('should report close code and reason to disconnect callback', async (done) => {
1389+
const { url, waitForConnect } = await startTServer({
1390+
onDisconnect: (_ctx, code, reason) => {
1391+
expect(code).toBe(4321);
1392+
expect(reason).toBe('Byebye');
1393+
done();
1394+
},
1395+
});
1396+
1397+
const client = await createTClient(url);
1398+
1399+
client.ws.send(
1400+
stringifyMessage<MessageType.ConnectionInit>({
1401+
type: MessageType.ConnectionInit,
1402+
}),
1403+
);
1404+
await waitForConnect();
1405+
1406+
client.ws.close(4321, 'Byebye');
1407+
});
1408+
});

src/use/ws.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,10 @@ export function useServer(
105105
{ socket, request },
106106
);
107107

108-
socket.once('close', () => {
108+
socket.once('close', (code, reason) => {
109109
if (pongWait) clearTimeout(pongWait);
110110
if (pingInterval) clearInterval(pingInterval);
111-
closed();
111+
closed(code, reason);
112112
});
113113
});
114114

0 commit comments

Comments
 (0)