Skip to content

Commit 6274f44

Browse files
authored
feat(client): Event listeners for both operation modes (enisdenjo#84)
1 parent 8ecdf3c commit 6274f44

File tree

3 files changed

+415
-37
lines changed

3 files changed

+415
-37
lines changed

src/client.ts

+85-23
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ import {
1919
/** This file is the entry point for browsers, re-export common elements. */
2020
export * from './common';
2121

22+
/** @category Client */
23+
export interface EventListeners<SingleConnection extends boolean = false> {
24+
/**
25+
* Emitted when the client starts connecting to the server.
26+
*
27+
* @param reconnecting - Whether the client is reconnecting after the connection was broken.
28+
*/
29+
connecting?: (reconnecting: boolean) => void;
30+
/**
31+
* Emitted when the client receives a message from the server.
32+
*/
33+
message?: (message: StreamMessage<SingleConnection, StreamEvent>) => void;
34+
/**
35+
* Emitted when the client has successfully connected to the server.
36+
*
37+
* @param reconnecting - Whether the client has reconnected after the connection was broken.
38+
*/
39+
connected?: (reconnected: boolean) => void;
40+
}
41+
2242
/** @category Client */
2343
export interface ClientOptions<SingleConnection extends boolean = false> {
2444
/**
@@ -44,7 +64,7 @@ export interface ClientOptions<SingleConnection extends boolean = false> {
4464
* - `true`: Establish a connection on first subscribe and close on last unsubscribe.
4565
*
4666
* Note that the `lazy` option has NO EFFECT when using the client
47-
* in "distinct connection mode" (`singleConnection = false`).
67+
* in "distinct connections mode" (`singleConnection = false`).
4868
*
4969
* @default true
5070
*/
@@ -56,7 +76,7 @@ export interface ClientOptions<SingleConnection extends boolean = false> {
5676
* Meant to be used in combination with `lazy`.
5777
*
5878
* Note that the `lazy` option has NO EFFECT when using the client
59-
* in "distinct connection mode" (`singleConnection = false`).
79+
* in "distinct connections mode" (`singleConnection = false`).
6080
*
6181
* @default 0
6282
*/
@@ -190,28 +210,44 @@ export interface ClientOptions<SingleConnection extends boolean = false> {
190210
* and because `graphql-sse` implements a custom SSE parser - received messages will **not** appear in browser's DevTools.
191211
*
192212
* Use this function if you want to inspect valid messages received through the active SSE connection.
213+
*
214+
* @deprecated Consider using {@link ClientOptions.on} instead.
193215
*/
194216
onMessage?: (message: StreamMessage<SingleConnection, StreamEvent>) => void;
217+
/**
218+
* Event listeners for events happening in teh SSE connection.
219+
*
220+
* Will emit events for both the "single connection mode" and the default "distinct connections mode".
221+
*
222+
* Beware that the `connecting` event will be called for **each** subscription when using with "distinct connections mode".
223+
*/
224+
on?: EventListeners<SingleConnection>;
195225
}
196226

197227
/** @category Client */
198-
export interface Client {
228+
export interface Client<SingleConnection extends boolean = false> {
199229
/**
200230
* Subscribes to receive through a SSE connection.
201231
*
202232
* It uses the `sink` to emit received data or errors. Returns a _dispose_
203233
* function used for dropping the subscription and cleaning up.
234+
*
235+
* @param on - The event listener for "distinct connections mode". Note that **no events will be emitted** in "single connection mode"; for that, consider using the event listener in {@link ClientOptions}.
204236
*/
205237
subscribe<Data = Record<string, unknown>, Extensions = unknown>(
206238
request: RequestParams,
207239
sink: Sink<ExecutionResult<Data, Extensions>>,
240+
on?: SingleConnection extends true ? never : EventListeners<false>,
208241
): () => void;
209242
/**
210243
* Subscribes and iterates over emitted results from an SSE connection
211244
* through the returned async iterator.
245+
*
246+
* @param on - The event listener for "distinct connections mode". Note that **no events will be emitted** in "single connection mode"; for that, consider using the event listener in {@link ClientOptions}.
212247
*/
213248
iterate<Data = Record<string, unknown>, Extensions = unknown>(
214249
request: RequestParams,
250+
on?: SingleConnection extends true ? never : EventListeners<false>,
215251
): AsyncIterableIterator<ExecutionResult<Data, Extensions>>;
216252
/**
217253
* Dispose of the client, destroy connections and clean up resources.
@@ -235,7 +271,7 @@ export interface Client {
235271
*/
236272
export function createClient<SingleConnection extends boolean = false>(
237273
options: ClientOptions<SingleConnection>,
238-
): Client {
274+
): Client<SingleConnection> {
239275
const {
240276
singleConnection = false,
241277
lazy = true,
@@ -274,6 +310,7 @@ export function createClient<SingleConnection extends boolean = false>(
274310
referrer,
275311
referrerPolicy,
276312
onMessage,
313+
on: clientOn,
277314
} = options;
278315
const fetchFn = (options.fetchFn || fetch) as typeof fetch;
279316
const AbortControllerImpl = (options.abortControllerImpl ||
@@ -333,6 +370,8 @@ export function createClient<SingleConnection extends boolean = false>(
333370
retries++;
334371
}
335372

373+
clientOn?.connecting?.(!!retryingErr);
374+
336375
// we must create a new controller here because lazy mode aborts currently active ones
337376
connCtrl = new AbortControllerImpl();
338377
const unlistenDispose = client.onDispose(() => connCtrl.abort());
@@ -381,9 +420,14 @@ export function createClient<SingleConnection extends boolean = false>(
381420
referrerPolicy,
382421
url,
383422
fetchFn,
384-
onMessage,
423+
onMessage: (msg) => {
424+
clientOn?.message?.(msg);
425+
onMessage?.(msg); // @deprecated
426+
},
385427
});
386428

429+
clientOn?.connected?.(!!retryingErr);
430+
387431
connected.waitForThrow().catch(() => (conn = undefined));
388432

389433
return connected;
@@ -423,7 +467,11 @@ export function createClient<SingleConnection extends boolean = false>(
423467
})();
424468
}
425469

426-
function subscribe(request: RequestParams, sink: Sink) {
470+
function subscribe(
471+
request: RequestParams,
472+
sink: Sink,
473+
on?: EventListeners<false>,
474+
) {
427475
if (!singleConnection) {
428476
// distinct connections mode
429477

@@ -449,6 +497,9 @@ export function createClient<SingleConnection extends boolean = false>(
449497
retries++;
450498
}
451499

500+
clientOn?.connecting?.(!!retryingErr);
501+
on?.connecting?.(!!retryingErr);
502+
452503
const url =
453504
typeof options.url === 'function'
454505
? await options.url()
@@ -475,9 +526,16 @@ export function createClient<SingleConnection extends boolean = false>(
475526
url,
476527
body: JSON.stringify(request),
477528
fetchFn,
478-
onMessage,
529+
onMessage: (msg) => {
530+
clientOn?.message?.(msg);
531+
on?.message?.(msg);
532+
onMessage?.(msg); // @deprecated
533+
},
479534
});
480535

536+
clientOn?.connected?.(!!retryingErr);
537+
on?.connected?.(!!retryingErr);
538+
481539
for await (const result of getResults()) {
482540
// only after receiving results are future connects not considered retries.
483541
// this is because a client might successfully connect, but the server
@@ -645,7 +703,7 @@ export function createClient<SingleConnection extends boolean = false>(
645703

646704
return {
647705
subscribe,
648-
iterate(request) {
706+
iterate(request, on) {
649707
const pending: ExecutionResult<
650708
// TODO: how to not use `any` and not have a redundant function signature?
651709
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -660,22 +718,26 @@ export function createClient<SingleConnection extends boolean = false>(
660718
// noop
661719
},
662720
};
663-
const dispose = subscribe(request, {
664-
next(val) {
665-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
666-
pending.push(val as any);
667-
deferred.resolve();
668-
},
669-
error(err) {
670-
deferred.done = true;
671-
deferred.error = err;
672-
deferred.resolve();
673-
},
674-
complete() {
675-
deferred.done = true;
676-
deferred.resolve();
721+
const dispose = subscribe(
722+
request,
723+
{
724+
next(val) {
725+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
726+
pending.push(val as any);
727+
deferred.resolve();
728+
},
729+
error(err) {
730+
deferred.done = true;
731+
deferred.error = err;
732+
deferred.resolve();
733+
},
734+
complete() {
735+
deferred.done = true;
736+
deferred.resolve();
737+
},
677738
},
678-
});
739+
on,
740+
);
679741

680742
const iterator = (async function* iterator() {
681743
for (;;) {

0 commit comments

Comments
 (0)