forked from enisdenjo/graphql-ws
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat(server): Implement following the new transport protocol (#1)
* feat(server): begin # Conflicts: # package.json * feat(server): be more strict about protocol * test: manually control server * refactor: dispose waits for server to close * test(server): failing for graceful dispose * refactor: improve disposal and event listeners * feat: return resulting server on creation * refactor: explicit server naming as creation result * refactor: a bit of cleanup * feat: report server errors by closing the connections * refactor: consistency * refactor: unnecessary client setup [skip ci] * refactor: comply with the new protocol * refactor: use parseMessage [skip ci] * refactor: documentation comments * feat: more type safety * feat: add more server options * feat: comply with updated protocol * refactor: use promise resolutions and smaller waits * feat: implement onConnect callback and wait timeout * test(onConnect): longer promise resolution * refactor: smaller refinements [skip ci] * chore(deps): add missing ws types after rebase * style: make linter happy * feat(stringifyMessage): implement and use * refactor(message): simplify message parsing errors * style: typescript can infer * feat(Subscribe): terminate socket if connection is not acknowledged * feat(message): query can be of DocumentNode type too * refactor(Error): payload is an array of errors * refactor(message): parameters are readonly * refactor: schema is optional in onSubscribe exec args * feat: implement query and mutation operation * test(Subscribe): simple query operation * test(Subscribe): simple query validation errors * test(Subscribe): socket shouldnt close or error because of GraphQL errors * refactor: typo * test(Subscribe): support operations with `DocumentNode` type query * test(Subscribe): close socket on request if schema is undefined * test(Subscribe): pick up the schema from `onSubscribe` * feat: implement subscription operation * refactor: typo * refactor: use fixture
- Loading branch information
Showing
14 changed files
with
1,515 additions
and
4 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,147 @@ | ||
/** | ||
* | ||
* message | ||
* | ||
*/ | ||
|
||
import { GraphQLError, ExecutionResult, DocumentNode } from 'graphql'; | ||
import { | ||
isObject, | ||
hasOwnProperty, | ||
hasOwnObjectProperty, | ||
hasOwnStringProperty, | ||
hasOwnArrayProperty, | ||
} from './utils'; | ||
|
||
/** Types of messages allowed to be sent by the client/server over the WS protocol. */ | ||
export enum MessageType { | ||
ConnectionInit = 'connection_init', // Client -> Server | ||
ConnectionAck = 'connection_ack', // Server -> Client | ||
|
||
Subscribe = 'subscribe', // Client -> Server | ||
Next = 'next', // Server -> Client | ||
Error = 'error', // Server -> Client | ||
Complete = 'complete', // bidirectional | ||
} | ||
|
||
export interface ConnectionInitMessage { | ||
readonly type: MessageType.ConnectionInit; | ||
readonly payload?: Record<string, unknown>; // connectionParams | ||
} | ||
|
||
export interface ConnectionAckMessage { | ||
readonly type: MessageType.ConnectionAck; | ||
} | ||
|
||
export interface SubscribeMessage { | ||
readonly id: string; | ||
readonly type: MessageType.Subscribe; | ||
readonly payload: { | ||
readonly operationName: string; | ||
readonly query: string | DocumentNode; | ||
readonly variables: Record<string, unknown>; | ||
}; | ||
} | ||
|
||
export interface NextMessage { | ||
readonly id: string; | ||
readonly type: MessageType.Next; | ||
readonly payload: ExecutionResult; | ||
} | ||
|
||
export interface ErrorMessage { | ||
readonly id: string; | ||
readonly type: MessageType.Error; | ||
readonly payload: readonly GraphQLError[]; | ||
} | ||
|
||
export interface CompleteMessage { | ||
readonly id: string; | ||
readonly type: MessageType.Complete; | ||
} | ||
|
||
export type Message< | ||
T extends MessageType = MessageType | ||
> = T extends MessageType.ConnectionAck | ||
? ConnectionAckMessage | ||
: T extends MessageType.ConnectionInit | ||
? ConnectionInitMessage | ||
: T extends MessageType.Subscribe | ||
? SubscribeMessage | ||
: T extends MessageType.Next | ||
? NextMessage | ||
: T extends MessageType.Error | ||
? ErrorMessage | ||
: T extends MessageType.Complete | ||
? CompleteMessage | ||
: never; | ||
|
||
export function isMessage(val: unknown): val is Message { | ||
if (isObject(val)) { | ||
// all messages must have the `type` prop | ||
if (!hasOwnProperty(val, 'type')) { | ||
return false; | ||
} | ||
// validate other properties depending on the `type` | ||
switch (val.type) { | ||
case MessageType.ConnectionInit: | ||
// the connection init message can have optional object `connectionParams` in the payload | ||
return !hasOwnProperty(val, 'payload') || isObject(val.payload); | ||
case MessageType.ConnectionAck: | ||
return true; | ||
case MessageType.Subscribe: | ||
return ( | ||
hasOwnStringProperty(val, 'id') && | ||
hasOwnObjectProperty(val, 'payload') && | ||
hasOwnStringProperty(val.payload, 'operationName') && | ||
(hasOwnStringProperty(val.payload, 'query') || // string query | ||
hasOwnObjectProperty(val.payload, 'query')) && // document node query | ||
hasOwnObjectProperty(val.payload, 'variables') | ||
); | ||
case MessageType.Next: | ||
return ( | ||
hasOwnStringProperty(val, 'id') && | ||
hasOwnObjectProperty(val, 'payload') && | ||
// ExecutionResult | ||
(hasOwnObjectProperty(val.payload, 'data') || | ||
hasOwnObjectProperty(val.payload, 'errors')) | ||
); | ||
case MessageType.Error: | ||
return ( | ||
hasOwnStringProperty(val, 'id') && | ||
// GraphQLError | ||
hasOwnArrayProperty(val, 'payload') && | ||
val.payload.length > 0 // must be at least one error | ||
); | ||
case MessageType.Complete: | ||
return hasOwnStringProperty(val, 'id'); | ||
default: | ||
return false; | ||
} | ||
} | ||
return false; | ||
} | ||
|
||
export function parseMessage(data: unknown): Message { | ||
if (isMessage(data)) { | ||
return data; | ||
} | ||
if (typeof data === 'string') { | ||
const message = JSON.parse(data); | ||
if (!isMessage(message)) { | ||
throw new Error('Invalid message'); | ||
} | ||
return message; | ||
} | ||
throw new Error('Message not parsable'); | ||
} | ||
|
||
/** Helps stringifying a valid message ready to be sent through the socket. */ | ||
export function stringifyMessage<T extends MessageType>( | ||
msg: Message<T>, | ||
): string { | ||
if (!isMessage(msg)) { | ||
throw new Error('Cannot stringify invalid message'); | ||
} | ||
return JSON.stringify(msg); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
/** | ||
* | ||
* protocol | ||
* | ||
*/ | ||
|
||
export const GRAPHQL_TRANSPORT_WS_PROTOCOL = 'graphql-transport-ws'; |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from './server'; |
Oops, something went wrong.