import { execute as defaultExecute , subscribe as defaultSubscribe , validateSchema as defaultValidateSchema , validate as defaultValidate , parse as defaultParse , GraphQLSchema , ExecutionResult , ExecutionArgs , DocumentNode , GraphQLError , specifiedRules as defaultValidationRules , ValidationRule , getOperationAST , OperationDefinitionNode , } from "graphql" ; import { isAsyncIterableIterator } from "./isAsyncIterableIterator" ; import type { Server as IOServer , Socket as IOSocket } from "socket.io" ; export type PromiseOrPlain < T > = T | Promise < T > ; type DocumentSourceString = string ; type MaybeDocumentNode = Record < string , unknown > | DocumentNode ; const isDocumentNode = ( input ) : input is DocumentNode => { return input [ "kind" ] === "Document" && Array . isArray ( input [ "definitions" ] ) ; } ; type PromiseOrValue < T > = T | Promise < T > ; export type ExecuteFunction = ( args : ExecutionArgs ) => PromiseOrPlain < AsyncIterableIterator < ExecutionResult > | ExecutionResult > ; export type GetParameterFunctionParameter = { socket : IOSocket ; graphQLPayload : { source : DocumentSourceString | MaybeDocumentNode ; variableValues : { [ key ] : any ; } | null ; operationName : string | null ; /** Additional extensions that can be used for authentication tokens etc. */ extensions : { [ key ] : any ; } | null ; } ; } ; export type GetParameterFunction = ( parameter : GetParameterFunctionParameter ) => PromiseOrPlain < { graphQLExecutionParameter : { schema : GraphQLSchema ; contextValue ? : unknown ; rootValue ? : unknown ; source ? : DocumentSourceString | DocumentNode ; variableValues ? : { [ key ] : any ; } | null ; operationName ? : string ; } ; execute ? : ExecuteFunction ; subscribe ? : typeof defaultSubscribe ; parse ? : typeof defaultParse ; validateSchema ? : typeof defaultValidateSchema ; validate ? : typeof defaultValidate ; validationRules ? : ValidationRule [ ] ; } > ; const isSubscriptionOperation = ( def ) => def . operation === "subscription" ; type MessagePayload = { id : number ; operation : DocumentSourceString | MaybeDocumentNode ; variables : { [ key ] : any ; } | null ; operationName : string | null ; extensions : { [ key ] : any ; } | null ; } ; const decodeMessage = ( message ) : MessagePayload | Error => { let id ; let operation = null ; let variables = null ; let operationName = null ; let extensions = null ; if ( typeof message === "object" && message !== null ) { const maybeId = ( message as any ) . id ; if ( typeof maybeId === "number" ) { id = maybeId ; } else { return new Error ( "Invalid message format. Field 'id' is invalid." ) ; } const maybeOperation = ( message as any ) . operation ; if ( typeof maybeOperation === "string" || ( typeof maybeOperation === "object" && maybeOperation !== null ) ) { operation = maybeOperation as DocumentSourceString | MaybeDocumentNode ; } else { return new Error ( "Invalid message format. Field 'operation' is invalid. Must be DocumentSourceString or DocumentNode." ) ; } const maybeVariables = ( message as any ) . variables ?? null ; if ( typeof maybeVariables === "object" ) { variables = maybeVariables ; } else { return new Error ( "Invalid message format. Field 'variableValues' is invalid." ) ; } const maybeOperationName = ( message as any ) . operationName ?? null ; if ( maybeOperationName === null || typeof maybeOperationName === "string" ) { operationName = maybeOperationName ; } else { return new Error ( "Invalid message format. Field 'operationName' is invalid." ) ; } const maybeExtensions = ( message as any ) . extensions ?? null ; if ( typeof maybeExtensions === "object" ) { extensions = maybeExtensions ; } else { return new Error ( "Invalid message format. Field 'extensions' is invalid." ) ; } return { id , operation , variables , operationName , extensions , } ; } return new Error ( "Invalid message format. Sent message is not an object." ) ; } ; const decodeUnsubscribeMessage = ( message ) : { id : number ; } | Error => { if ( typeof message === "object" && message !== null ) { const maybeId = ( message as any ) . id ; if ( typeof maybeId === "number" ) { return { id : maybeId } ; } else { return new Error ( "Invalid message format. Field 'id' is invalid." ) ; } } return new Error ( "Invalid message format. Sent message is not an object." ) ; } ; export type DecodeErrorHandler = ( error : Error ) => void ; export type RegisterSocketIOGraphQLServerParameter = { socketServer : IOServer ; getParameter : GetParameterFunction ; onMessageDecodeError ? : DecodeErrorHandler ; isLazy ? : boolean ; } ; export type UnsubscribeHandler = ( ) => void ; export type SocketIOGraphQLServer = { registerSocket : ( socket : IOSocket ) => UnsubscribeHandler ; disposeSocket : ( socket : IOSocket ) => void ; destroy : ( ) => void ; } ; export const registerSocketIOGraphQLServer = ( { socketServer , getParameter , onMessageDecodeError = console . error , isLazy = false , } ) : SocketIOGraphQLServer => { let acceptNewConnections = true ; const disposeHandlers = new Map < IOSocket , UnsubscribeHandler > ( ) ; const registerSocket = ( socket ) => { const dispose = disposeHandlers . get ( socket ) ; if ( dispose ) { return dispose ; } const subscriptions = new Map < number , ( ) => void > ( ) ; const executeHandler = async ( rawMessage ) => { const message = decodeMessage ( rawMessage ) ; if ( message instanceof Error ) { onMessageDecodeError ( message ) ; return ; } const emitFinalResult = ( executionResult ) => socket . emit ( "@graphql/result" , { ... executionResult , id , isFinal : true , } ) ; const { id , operation : source , variables : variableValues , operationName , extensions , } = message ; const { graphQLExecutionParameter , subscribe = defaultSubscribe , execute = defaultExecute , parse = defaultParse , validateSchema = defaultValidateSchema , validate = defaultValidate , validationRules = defaultValidationRules , } = await getParameter ( { socket , graphQLPayload : { source , variableValues , operationName , extensions , } , } ) ; const schemaValidationErrors = validateSchema ( graphQLExecutionParameter . schema ) ; if ( schemaValidationErrors . length > 0 ) { emitFinalResult ( { errors : schemaValidationErrors } ) ; return ; } let documentAst ; if ( typeof source === "string" ) { try { documentAst = parse ( source ) ; } catch ( syntaxError ) { emitFinalResult ( { errors : [ syntaxError as GraphQLError ] } ) ; return ; } } else if ( isDocumentNode ( source ) ) { documentAst = source ; } else { emitFinalResult ( { errors : [ new GraphQLError ( "Invalid DocumentNode. The provided document AST node is invalid." ) , ] , } ) ; return ; } const validationErrors = validate ( graphQLExecutionParameter . schema , documentAst , validationRules ) ; if ( validationErrors . length > 0 ) { emitFinalResult ( { errors : validationErrors , } ) ; return ; } const executionParameter = { document : documentAst , operationName , source , variableValues , ... graphQLExecutionParameter , } ; const asyncIteratorHandler = async ( result ) => { if ( isAsyncIterableIterator ( result ) ) { subscriptions . set ( id , ( ) => result . return ?. ( ) ) ; for await ( const subscriptionResult of result ) { socket . emit ( "@graphql/result" , { ... subscriptionResult , id } ) ; } } else { emitFinalResult ( result ) ; } } ; let executionResult ; const mainOperation = getOperationAST ( documentAst , operationName ) ; if ( ! mainOperation ) { executionResult = { errors : [ new GraphQLError ( "No executable operation sent." ) ] , } ; } else { try { if ( isSubscriptionOperation ( mainOperation ) ) { executionResult = await subscribe ( { ... executionParameter , document : documentAst , } ) ; } else { executionResult = execute ( executionParameter ) as PromiseOrValue < ExecutionResult | AsyncIterableIterator < ExecutionResult > > ; } } catch ( contextError ) { console . error ( "Unexpected error occurred." , contextError ) ; executionResult = { errors : [ new GraphQLError ( "A unexpected error occurred." ) ] , } ; } } Promise . resolve ( executionResult ) . then ( ( result ) => { if ( isAsyncIterableIterator ( result ) ) { return asyncIteratorHandler ( result ) ; } else { emitFinalResult ( result ) ; } } ) . catch ( ( err ) => { emitFinalResult ( { errors : [ err ] , } ) ; } ) ; } ; socket . on ( "@graphql/execute" , executeHandler ) ; const unsubscribeHandler = ( rawMessage ) => { const message = decodeUnsubscribeMessage ( rawMessage ) ; if ( message instanceof Error ) { onMessageDecodeError ( message ) ; return ; } const id = message . id ; const subscription = subscriptions . get ( id ) ; subscription ?. ( ) ; subscriptions . delete ( id ) ; } ; socket . on ( "@graphql/unsubscribe" , unsubscribeHandler ) ; const disconnectHandler = ( ) => { subscriptions . forEach ( ( unsubscribe ) => unsubscribe ( ) ) ; disposeHandlers . delete ( socket ) ; } ; socket . once ( "disconnect" , disconnectHandler ) ; const disposeHandler = ( ) => { socket . off ( "@graphql/execute" , executeHandler ) ; socket . off ( "@graphql/unsubscribe" , unsubscribeHandler ) ; socket . off ( "disconnect" , disconnectHandler ) ; disconnectHandler ( ) ; } ; disposeHandlers . set ( socket , disposeHandler ) ; return disposeHandler ; } ; if ( isLazy === false && acceptNewConnections === true ) { socketServer . on ( "connection" , registerSocket ) ; } return { registerSocket : ( socket ) => disposeHandlers . get ( socket ) ?? registerSocket ( socket ) , disposeSocket : ( socket ) => disposeHandlers . get ( socket ) ?. ( ) , destroy : ( ) => { socketServer . off ( "connection" , registerSocket ) ; for ( const dispose of disposeHandlers . values ( ) ) { dispose ( ) ; } } , } ; } ;