/**
 *
 * client
 *
 */ import { createParser } from './parser' ; import { isObject } from './utils' ; import { RequestParams , Sink , StreamMessage , ExecutionResult , ExecutionPatchResult , TOKEN_HEADER_KEY , } from './common' ; /** This file is the entry point for browsers, re-export common elements. */ export * from './common' ; category export interface ClientOptions { default singleConnection ? : boolean ; default lazy ? : boolean ; default onNonLazyError ? : ( error : unknown ) => void ; /**
     * URL of the GraphQL over SSE server to connect.
     *
     * If the option is a function, it will be called on each connection attempt.
     * Returning a Promise is supported too and the connection phase will stall until it
     * resolves with the URL.
     *
     * A good use-case for having a function is when using the URL for authentication,
     * where subsequent reconnects (due to auth) may have a refreshed identity token in
     * the URL.
     */ url : string | ( ( ) => Promise < string > | string ) ; /**
     * HTTP headers to pass along the request.
     *
     * If the option is a function, it will be called on each connection attempt.
     * Returning a Promise is supported too and the connection phase will stall until it
     * resolves with the headers.
     *
     * A good use-case for having a function is when using the headers for authentication,
     * where subsequent reconnects (due to auth) may have a refreshed identity token in
     * the header.
     */ headers ? : Record < string , string > | ( ( ) => Promise < Record < string , string > > | Record < string , string > ) ; default fetchFn ? : unknown ; default abortControllerImpl ? : unknown ; /**
     * A custom ID generator for identifying subscriptions.
     *
     * The default generates a v4 UUID to be used as the ID using `Math`
     * as the random number generator. Supply your own generator
     * in case you need more uniqueness.
     *
     * Reference: https://gist.github.com/jed/982883
     */ generateID ? : ( ) => string ; default retryAttempts ? : number ; default retry ? : ( retries : number ) => Promise < void > ; } category export interface Client { /**
     * Subscribes to receive through a SSE connection.
     *
     * It uses the `sink` to emit received data or errors. Returns a _dispose_
     * function used for dropping the subscription and cleaning up.
     */ subscribe < Data = Record < string , unknown > , Extensions = unknown > ( request , sink ) : ( ) => void ; /**
     * Dispose of the client, destroy connections and clean up resources.
     */ dispose : ( ) => void ; } category export function createClient ( options ) { const { singleConnection = false , lazy = true , onNonLazyError = console . error , generateID = function generateUUID ( ) { return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx' . replace ( /[xy]/g , ( c ) => { const r = ( Math . random ( ) * 16 ) | 0 , v = c == 'x' ? r : ( r & 0x3 ) | 0x8 ; return v . toString ( 16 ) ; } ) ; } , retryAttempts = 5 , retry = async function randomisedExponentialBackoff ( retries ) { let retryDelay = 1000 ; for ( let i = 0 ; i < retries ; i ++ ) { retryDelay *= 2 ; } await new Promise ( ( resolve ) => setTimeout ( resolve , retryDelay + Math . floor ( Math . random ( ) * ( 3000 - 300 ) + 300 ) ) ) ; } , } = options ; const fetchFn = ( options . fetchFn || fetch ) as typeof fetch ; const AbortControllerImpl = ( options . abortControllerImpl || AbortController ) as typeof AbortController ; const client = ( ( ) => { let disposed = false ; const listeners = [ ] ; return { get disposed ( ) { return disposed ; } , onDispose ( cb ) { if ( disposed ) { setTimeout ( ( ) => cb ( ) , 0 ) ; return ( ) => { } ; } listeners . push ( cb ) ; return ( ) => { listeners . splice ( listeners . indexOf ( cb ) , 1 ) ; } ; } , dispose ( ) { if ( disposed ) return ; disposed = true ; for ( const listener of [ ... listeners ] ) { listener ( ) ; } } , } ; } ) ( ) ; let connCtrl , conn , locks = 0 , retryingErr = null as unknown , retries = 0 ; async function getOrConnect ( ) { try { if ( client . disposed ) throw new Error ( 'Client has been disposed' ) ; return await ( conn ?? ( conn = ( async ( ) => { if ( retryingErr ) { await retry ( retries ) ; if ( connCtrl . signal . aborted ) throw new Error ( 'Connection aborted by the client' ) ; retries ++ ; } connCtrl = new AbortControllerImpl ( ) ; const unlistenDispose = client . onDispose ( ( ) => connCtrl . abort ( ) ) ; connCtrl . signal . addEventListener ( 'abort' , ( ) => { unlistenDispose ( ) ; conn = undefined ; } ) ; const url = typeof options . url === 'function' ? await options . url ( ) : options . url ; if ( connCtrl . signal . aborted ) throw new Error ( 'Connection aborted by the client' ) ; const headers = typeof options . headers === 'function' ? await options . headers ( ) : options . headers ?? { } ; if ( connCtrl . signal . aborted ) throw new Error ( 'Connection aborted by the client' ) ; let res ; try { res = await fetchFn ( url , { signal : connCtrl . signal , method : 'PUT' , headers , } ) ; } catch ( err ) { throw new NetworkError ( err ) ; } if ( res . status !== 201 ) throw new NetworkError ( res ) ; const token = await res . text ( ) ; headers [ TOKEN_HEADER_KEY ] = token ; const connected = await connect ( { signal : connCtrl . signal , headers , url , fetchFn , } ) ; retryingErr = null ; retries = 0 ; connected . waitForThrow ( ) . catch ( ( ) => ( conn = undefined ) ) ; return connected ; } ) ( ) ) ) ; } catch ( err ) { conn = undefined ; throw err ; } } if ( singleConnection && ! lazy ) { ( async ( ) => { locks ++ ; for ( ; ; ) { try { const { waitForThrow } = await getOrConnect ( ) ; await waitForThrow ( ) ; } catch ( err ) { if ( client . disposed ) return ; if ( ! ( err instanceof NetworkError ) ) return onNonLazyError ?. ( err ) ; if ( ! retryAttempts || retries >= retryAttempts ) return onNonLazyError ?. ( err ) ; retryingErr = err ; } } } ) ( ) ; } return { subscribe ( request , sink ) { if ( ! singleConnection ) { const control = new AbortControllerImpl ( ) ; const unlisten = client . onDispose ( ( ) => { unlisten ( ) ; control . abort ( ) ; } ) ; ( async ( ) => { let retryingErr = null as unknown , retries = 0 ; for ( ; ; ) { try { if ( retryingErr ) { await retry ( retries ) ; if ( control . signal . aborted ) throw new Error ( 'Connection aborted by the client' ) ; retries ++ ; } const url = typeof options . url === 'function' ? await options . url ( ) : options . url ; if ( control . signal . aborted ) throw new Error ( 'Connection aborted by the client' ) ; const headers = typeof options . headers === 'function' ? await options . headers ( ) : options . headers ?? { } ; if ( control . signal . aborted ) throw new Error ( 'Connection aborted by the client' ) ; const { getResults } = await connect ( { signal : control . signal , headers , url , body : JSON . stringify ( request ) , fetchFn , } ) ; retryingErr = null ; retries = 0 ; for await ( const result of getResults ( ) ) { sink . next ( result as any ) ; } return control . abort ( ) ; } catch ( err ) { if ( control . signal . aborted ) return ; if ( ! ( err instanceof NetworkError ) ) throw err ; if ( ! retryAttempts || retries >= retryAttempts ) throw err ; retryingErr = err ; } } } ) ( ) . then ( ( ) => sink . complete ( ) ) . catch ( ( err ) => sink . error ( err ) ) ; return ( ) => control . abort ( ) ; } locks ++ ; const control = new AbortControllerImpl ( ) ; const unlisten = client . onDispose ( ( ) => { unlisten ( ) ; control . abort ( ) ; } ) ; ( async ( ) => { const operationId = generateID ( ) ; request = { ... request , extensions : { ... request . extensions , operationId } , } ; let complete = null ; for ( ; ; ) { complete = null ; try { const { url , headers , getResults } = await getOrConnect ( ) ; let res ; try { res = await fetchFn ( url , { signal : control . signal , method : 'POST' , headers , body : JSON . stringify ( request ) , } ) ; } catch ( err ) { throw new NetworkError ( err ) ; } if ( res . status !== 202 ) throw new NetworkError ( res ) ; complete = async ( ) => { let res ; try { const control = new AbortControllerImpl ( ) ; const unlisten = client . onDispose ( ( ) => { unlisten ( ) ; control . abort ( ) ; } ) ; res = await fetchFn ( url + '?operationId=' + operationId , { signal : control . signal , method : 'DELETE' , headers , } ) ; } catch ( err ) { throw new NetworkError ( err ) ; } if ( res . status !== 200 ) throw new NetworkError ( res ) ; } ; for await ( const result of getResults ( { signal : control . signal , operationId , } ) ) { sink . next ( result as any ) ; } complete = null ; return control . abort ( ) ; } catch ( err ) { if ( control . signal . aborted ) return await complete ?. ( ) ; if ( ! ( err instanceof NetworkError ) ) throw err ; if ( ! retryAttempts || retries >= retryAttempts ) throw err ; retryingErr = err ; } finally { if ( control . signal . aborted && -- locks === 0 ) connCtrl . abort ( ) ; } } } ) ( ) . then ( ( ) => sink . complete ( ) ) . catch ( ( err ) => sink . error ( err ) ) ; return ( ) => control . abort ( ) ; } , dispose ( ) { client . dispose ( ) ; } , } ; } category export class NetworkError < Response extends ResponseLike = ResponseLike > extends Error { /**
     * The underlyig response thats considered an error.
     *
     * Will be undefined when no response is received,
     * instead an unexpected network error.
     */ public response : Response | undefined ; constructor ( msgOrErrOrResponse ) { let message , response ; if ( isResponseLike ( msgOrErrOrResponse ) ) { response = msgOrErrOrResponse ; message = 'Server responded with ' + msgOrErrOrResponse . status + ': ' + msgOrErrOrResponse . statusText ; } else if ( msgOrErrOrResponse instanceof Error ) message = msgOrErrOrResponse . message ; else message = String ( msgOrErrOrResponse ) ; super ( message ) ; this . name = this . constructor . name ; this . response = response ; } } interface ResponseLike { readonly ok : boolean ; readonly status : number ; readonly statusText : string ; } function isResponseLike ( val ) { return ( isObject ( val ) && typeof val [ 'ok' ] === 'boolean' && typeof val [ 'status' ] === 'number' && typeof val [ 'statusText' ] === 'string' ) ; } interface Connection { url : string ; headers : Record < string , string > | undefined ; waitForThrow : ( ) => Promise < void > ; getResults : ( options ? : { signal : AbortSignal ; operationId : string ; } ) => AsyncIterable < ExecutionResult | ExecutionPatchResult > ; } interface ConnectOptions { signal : AbortSignal ; url : string ; headers ? : Record < string , string > | undefined ; body ? : string ; fetchFn : typeof fetch ; } async function connect ( options ) { const { signal , url , headers , body , fetchFn } = options ; const waiting = { } ; const queue = { } ; let res ; try { res = await fetchFn ( url , { signal , method : body ? 'POST' : 'GET' , headers : { ... headers , accept : 'text/event-stream' , } , body , } ) ; } catch ( err ) { throw new NetworkError ( err ) ; } if ( ! res . ok ) throw new NetworkError ( res ) ; if ( ! res . body ) throw new Error ( 'Missing response body' ) ; let error = null ; let waitingForThrow = null ; ( async ( ) => { try { const parse = createParser ( ) ; for await ( const chunk of toAsyncIterator ( res . body ! ) ) { if ( typeof chunk === 'string' ) throw new Error ( `Unexpected string chunk "${ chunk }"` ) ; const msgs = parse ( chunk ) ; if ( ! msgs ) continue ; for ( const msg of msgs ) { const operationId = msg . data && 'id' in msg . data ? msg . data . id : '' ; if ( ! ( operationId in queue ) ) queue [ operationId ] = [ ] ; switch ( msg . event ) { case 'next' : if ( operationId ) queue [ operationId ] . push ( ( msg as StreamMessage < true , 'next' > ) . data . payload ) ; else queue [ operationId ] . push ( ( msg as StreamMessage < false , 'next' > ) . data ) ; break ; case 'complete' : queue [ operationId ] . push ( 'complete' ) ; break ; default : throw new Error ( `Unexpected message event "${ msg . event }"` ) ; } waiting [ operationId ] ?. proceed ( ) ; } } } catch ( err ) { error = err ; if ( waitingForThrow ) waitingForThrow ( err ) ; } finally { Object . values ( waiting ) . forEach ( ( { proceed } ) => proceed ( ) ) ; } } ) ( ) ; return { url , headers , waitForThrow : ( ) => new Promise ( ( _ , reject ) => { if ( error ) return reject ( error ) ; waitingForThrow = reject ; } ) , async * getResults ( options ) { const { signal , operationId = '' } = options ?? { } ; try { for ( ; ; ) { while ( queue [ operationId ] ?. length ) { const result = queue [ operationId ] . shift ( ) ! ; if ( result === 'complete' ) return ; yield result ; } if ( error ) throw error ; if ( signal ?. aborted ) throw new Error ( 'Getting results aborted by the client' ) ; await new Promise < void > ( ( resolve ) => { const proceed = ( ) => { signal ?. removeEventListener ( 'abort' , proceed ) ; delete waiting [ operationId ] ; resolve ( ) ; } ; signal ?. addEventListener ( 'abort' , proceed ) ; waiting [ operationId ] = { proceed } ; } ) ; } } finally { delete queue [ operationId ] ; } } , } ; } /** Isomorphic ReadableStream to AsyncIterator converter. */ function toAsyncIterator ( val ) { if ( typeof Object ( val ) [ Symbol . asyncIterator ] === 'function' ) { val = val as NodeJS . ReadableStream ; return val [ Symbol . asyncIterator ] ( ) ; } return ( async function * ( ) { val = val as ReadableStream ; const reader = val . getReader ( ) ; for ( ; ; ) { const { value , done } = await reader . read ( ) ; if ( done ) return value ; yield value ; } } ) ( ) ; }