From 627775b8e1aca8f359607020ff2c3bcc37b50787 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 9 Sep 2020 22:06:30 +0200 Subject: [PATCH] feat(client): Emit events for `connecting`, `connected` and `closed` --- src/client.ts | 49 +++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 2 deletions(-) diff --git a/src/client.ts b/src/client.ts index 374c462b..fbfe55c2 100644 --- a/src/client.ts +++ b/src/client.ts @@ -16,6 +16,19 @@ import { } from './message'; import { isObject } from './utils'; +export type EventConnecting = 'connecting'; +export type EventConnected = 'connected'; // connected = socket opened + acknowledged +export type EventClosed = 'closed'; +export type Event = EventConnecting | EventConnected | EventClosed; + +export type EventListener = E extends EventConnecting + ? () => void + : E extends EventConnected + ? (socket: WebSocket) => void + : E extends EventClosed + ? (event: CloseEvent) => void + : never; + type CancellerRef = { current: (() => void) | null }; /** Configuration used for the `create` client function. */ @@ -43,6 +56,10 @@ export interface ClientOptions { } export interface Client extends Disposable { + /** + * Listens on the client which dispatches events about the socket state. + */ + on(event: E, listener: EventListener): () => void; /** * Subscribes through the WebSocket following the config parameters. It * uses the `sink` to emit received data or errors. Returns a _cleanup_ @@ -61,6 +78,30 @@ export function createClient(options: ClientOptions): Client { retryTimeout = 3 * 1000, // 3 seconds } = options; + const emitter = (() => { + const listeners: { [event in Event]: EventListener[] } = { + connecting: [], + connected: [], + closed: [], + }; + + return { + on(event: E, listener: EventListener) { + const l = listeners[event] as EventListener[]; + l.push(listener); + return () => { + l.splice(l.indexOf(listener), 1); + }; + }, + emit(event: E, ...args: Parameters>) { + (listeners[event] as EventListener[]).forEach((listener) => { + // @ts-expect-error: The args do actually fit + listener(...args); + }); + }, + }; + })(); + let state = { socket: null as WebSocket | null, acknowledged: false, @@ -163,6 +204,7 @@ export function createClient(options: ClientOptions): Client { // establish connection and assign to singleton const socket = new WebSocket(url, GRAPHQL_TRANSPORT_WS_PROTOCOL); state = { ...state, acknowledged: false, socket }; + emitter.emit('connecting'); await new Promise((resolve, reject) => { let settled = false, @@ -192,6 +234,7 @@ export function createClient(options: ClientOptions): Client { // we always want to update the state, just not reject a settled promise state = { ...state, acknowledged: false, socket: null }; + emitter.emit('closed', event); if (!settled) { settled = true; @@ -213,6 +256,7 @@ export function createClient(options: ClientOptions): Client { } state = { ...state, acknowledged: true, socket }; + emitter.emit('connected', socket); // connected = socket opened + acknowledged if (!settled) { settled = true; @@ -297,9 +341,9 @@ export function createClient(options: ClientOptions): Client { return; } - // retries expired, throw + // retries expired, close for good if (state.retries >= retryAttempts) { - throw errOrCloseEvent; + return; } // otherwize wait a bit and retry @@ -311,6 +355,7 @@ export function createClient(options: ClientOptions): Client { } return { + on: emitter.on, subscribe(payload, sink) { const uuid = generateUUID();