From 3f5c42e3deb7d014258417d20df29ef9e1fb1ed7 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Tue, 3 Nov 2020 23:01:13 +0100 Subject: [PATCH 01/21] test(server): should call `onComplete` if socket terminates --- src/tests/server.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/tests/server.ts b/src/tests/server.ts index 356b1e7d..cdc55f72 100644 --- a/src/tests/server.ts +++ b/src/tests/server.ts @@ -1461,6 +1461,45 @@ describe('Subscribe', () => { }), ); }); + + it('should call `onComplete` callback even if socket terminates abruptly', async (done) => { + const server = await startTServer({ + onComplete: () => { + done(); + }, + }); + + const client = await createTClient(server.url); + client.ws.send( + stringifyMessage({ + type: MessageType.ConnectionInit, + }), + ); + + await client.waitForMessage(({ data }) => { + expect(parseMessage(data).type).toBe(MessageType.ConnectionAck); + }); + + client.ws.send( + stringifyMessage({ + id: '1', + type: MessageType.Subscribe, + payload: { + query: 'subscription { ping }', + }, + }), + ); + await server.waitForOperation(); + + // just to make sure we're streaming + server.pong(); + await client.waitForMessage(({ data }) => { + expect(parseMessage(data).type).toBe(MessageType.Next); + }); + + // terminate socket abruptly + client.ws.terminate(); + }); }); describe('Keep-Alive', () => { From ab213b37e42470b2cf5808593e5a914a84f22e7d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 12:12:34 +0100 Subject: [PATCH 02/21] docs: correct apollo client usage recipe [skip ci] Closes #48 --- README.md | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index ab0b059c..137f032a 100644 --- a/README.md +++ b/README.md @@ -273,14 +273,14 @@ export const network = Network.create(fetchOrSubscribe, fetchOrSubscribe); ```typescript import { ApolloLink, Operation, FetchResult, Observable } from '@apollo/client'; import { print } from 'graphql'; -import { createClient, Config, Client } from 'graphql-ws'; +import { createClient, ClientOptions, Client } from 'graphql-ws'; class WebSocketLink extends ApolloLink { private client: Client; - constructor(config: Config) { + constructor(options: ClientOptions) { super(); - this.client = createClient(config); + this.client = createClient(options); } public request(operation: Operation): Observable { @@ -288,7 +288,8 @@ class WebSocketLink extends ApolloLink { return this.client.subscribe( { ...operation, query: print(operation.query) }, { - ...sink, + next: sink.next, + complete: sink.complete, error: (err) => { if (err instanceof Error) { sink.error(err); From 74be9b820ce06ddfa356d81187155bd6aeb78fb6 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 12:22:33 +0000 Subject: [PATCH 03/21] refactor: support array of protocols (#50) --- src/server.ts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/server.ts b/src/server.ts index 37267f01..37e18cc5 100644 --- a/src/server.ts +++ b/src/server.ts @@ -353,10 +353,9 @@ export function createServer( function handleConnection(socket: WebSocket, request: http.IncomingMessage) { if ( - socket.protocol === undefined || - socket.protocol !== GRAPHQL_TRANSPORT_WS_PROTOCOL || - (Array.isArray(socket.protocol) && - socket.protocol.indexOf(GRAPHQL_TRANSPORT_WS_PROTOCOL) === -1) + Array.isArray(socket.protocol) + ? socket.protocol.indexOf(GRAPHQL_TRANSPORT_WS_PROTOCOL) === -1 + : socket.protocol !== GRAPHQL_TRANSPORT_WS_PROTOCOL ) { return socket.close(1002, 'Protocol Error'); } From 5cc9770c60069159feea9be993bccd71c9ce8485 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 12:30:02 +0000 Subject: [PATCH 04/21] refactor: tweak types and checks for timeouts/intervals (#51) --- src/server.ts | 62 +++++++++++++++++++++++++-------------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/src/server.ts b/src/server.ts index 37e18cc5..c3c0abc0 100644 --- a/src/server.ts +++ b/src/server.ts @@ -373,42 +373,42 @@ export function createServer( // kick the client off (close socket) if the connection has // not been initialised after the specified wait timeout const connectionInitWait = - connectionInitWaitTimeout && // even 0 disables it - connectionInitWaitTimeout !== Infinity && - setTimeout(() => { - if (!ctxRef.current.connectionInitReceived) { - ctxRef.current.socket.close( - 4408, - 'Connection initialisation timeout', - ); - } - }, connectionInitWaitTimeout); + connectionInitWaitTimeout > 0 && isFinite(connectionInitWaitTimeout) + ? setTimeout(() => { + if (!ctxRef.current.connectionInitReceived) { + ctxRef.current.socket.close( + 4408, + 'Connection initialisation timeout', + ); + } + }, connectionInitWaitTimeout) + : null; // keep alive through ping-pong messages // read more about the websocket heartbeat here: https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers#Pings_and_Pongs_The_Heartbeat_of_WebSockets - let pongWait: NodeJS.Timeout | null; + let pongWait: NodeJS.Timeout | null = null; const pingInterval = - keepAlive && // even 0 disables it - keepAlive !== Infinity && - setInterval(() => { - // ping pong on open sockets only - if (socket.readyState === WebSocket.OPEN) { - // terminate the connection after pong wait has passed because the client is idle - pongWait = setTimeout(() => { - socket.terminate(); - }, keepAlive); - - // listen for client's pong and stop socket termination - socket.once('pong', () => { - if (pongWait) { - clearTimeout(pongWait); - pongWait = null; - } - }); + keepAlive > 0 && isFinite(keepAlive) + ? setInterval(() => { + // ping pong on open sockets only + if (socket.readyState === WebSocket.OPEN) { + // terminate the connection after pong wait has passed because the client is idle + pongWait = setTimeout(() => { + socket.terminate(); + }, keepAlive); + + // listen for client's pong and stop socket termination + socket.once('pong', () => { + if (pongWait) { + clearTimeout(pongWait); + pongWait = null; + } + }); - socket.ping(); - } - }, keepAlive); + socket.ping(); + } + }, keepAlive) + : null; function errorOrCloseHandler( errorOrClose: WebSocket.ErrorEvent | WebSocket.CloseEvent, From 1bb9218ad3ee9442442122c1d10910d51951b763 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 13:17:13 +0000 Subject: [PATCH 05/21] perf: Reduce runtime prototype traversal for hasOwnProperty (#52) --- src/utils.ts | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/utils.ts b/src/utils.ts index dc93c969..128b75fa 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -5,6 +5,9 @@ */ import { GraphQLError } from 'graphql'; +// Extremely small optimisation, reduces runtime prototype traversal +const baseHasOwnProperty = Object.prototype.hasOwnProperty; + export function isObject(val: unknown): val is Record { return typeof val === 'object' && val !== null; } @@ -29,31 +32,26 @@ export function hasOwnProperty< O extends Record, P extends PropertyKey >(obj: O, prop: P): obj is O & Record { - return Object.prototype.hasOwnProperty.call(obj, prop); + return baseHasOwnProperty.call(obj, prop); } export function hasOwnObjectProperty< O extends Record, P extends PropertyKey >(obj: O, prop: P): obj is O & Record> { - return Object.prototype.hasOwnProperty.call(obj, prop) && isObject(obj[prop]); + return baseHasOwnProperty.call(obj, prop) && isObject(obj[prop]); } export function hasOwnArrayProperty< O extends Record, P extends PropertyKey >(obj: O, prop: P): obj is O & Record { - return ( - Object.prototype.hasOwnProperty.call(obj, prop) && Array.isArray(obj[prop]) - ); + return baseHasOwnProperty.call(obj, prop) && Array.isArray(obj[prop]); } export function hasOwnStringProperty< O extends Record, P extends PropertyKey >(obj: O, prop: P): obj is O & Record { - return ( - Object.prototype.hasOwnProperty.call(obj, prop) && - typeof obj[prop] === 'string' - ); + return baseHasOwnProperty.call(obj, prop) && typeof obj[prop] === 'string'; } From bff92a8b846ddb852b4073cc6a62b773ea87d488 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 13:59:47 +0000 Subject: [PATCH 06/21] refactor: use Object.values instead of .entries (#57) --- src/server.ts | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/server.ts b/src/server.ts index c3c0abc0..0ec7f5ce 100644 --- a/src/server.ts +++ b/src/server.ts @@ -430,11 +430,9 @@ export function createServer( ); } - Object.entries(ctxRef.current.subscriptions).forEach( - ([, subscription]) => { - subscription.return?.(); - }, - ); + Object.values(ctxRef.current.subscriptions).forEach((subscription) => { + subscription.return?.(); + }); } socket.onerror = errorOrCloseHandler; From 7aa3bcdd38d40e83306f867a5b6b1bd612ec5fe3 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 19:45:31 +0000 Subject: [PATCH 07/21] fix(server): Consistently set `rootValue` and `contextValue`, if not overridden (#49) --- docs/interfaces/_server_.serveroptions.md | 15 +++---- docs/modules/_server_.md | 2 +- src/server.ts | 25 ++++++------ src/tests/server.ts | 48 ++++++++++++++++++++++- 4 files changed, 69 insertions(+), 21 deletions(-) diff --git a/docs/interfaces/_server_.serveroptions.md b/docs/interfaces/_server_.serveroptions.md index 4e3ef748..0003be31 100644 --- a/docs/interfaces/_server_.serveroptions.md +++ b/docs/interfaces/_server_.serveroptions.md @@ -204,10 +204,11 @@ If you return `ExecutionArgs` from the callback, it will be used instead of trying to build one internally. In this case, you are responsible for providing a ready set of arguments which will -be directly plugged in the operation execution. Beware, -the `context` server option is an exception. Only if you -dont provide a context alongside the returned value -here, the `context` server option will be used instead. +be directly plugged in the operation execution. + +Omitting the fields `contextValue` or `rootValue` +from the returned value will have the provided server +options fill in the gaps. To report GraphQL errors simply return an array of them from the callback, they will be reported @@ -233,9 +234,9 @@ The GraphQL root fields or resolvers to go alongside the schema. Learn more about them here: https://graphql.org/learn/execution/#root-fields-resolvers. -If you return from the `onSubscribe` callback, the -root field value will NOT be injected. You should add it -in the returned `ExecutionArgs` from the callback. +If you return from `onSubscribe`, and the returned value is +missing the `rootValue` field, the relevant operation root +will be used instead. ___ diff --git a/docs/modules/_server_.md b/docs/modules/_server_.md index f333a4a1..07c73d72 100644 --- a/docs/modules/_server_.md +++ b/docs/modules/_server_.md @@ -25,7 +25,7 @@ ### GraphQLExecutionContextValue -Ƭ **GraphQLExecutionContextValue**: object \| symbol \| number \| string \| boolean \| null +Ƭ **GraphQLExecutionContextValue**: object \| symbol \| number \| string \| boolean \| undefined \| null A concrete GraphQL execution context value type. diff --git a/src/server.ts b/src/server.ts index 0ec7f5ce..ab23aa8b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -58,6 +58,7 @@ export type GraphQLExecutionContextValue = | number | string | boolean + | undefined | null; export interface ServerOptions { @@ -96,9 +97,9 @@ export interface ServerOptions { * alongside the schema. Learn more about them * here: https://graphql.org/learn/execution/#root-fields-resolvers. * - * If you return from the `onSubscribe` callback, the - * root field value will NOT be injected. You should add it - * in the returned `ExecutionArgs` from the callback. + * If you return from `onSubscribe`, and the returned value is + * missing the `rootValue` field, the relevant operation root + * will be used instead. */ roots?: { [operation in OperationTypeNode]?: Record< @@ -177,10 +178,11 @@ export interface ServerOptions { * it will be used instead of trying to build one * internally. In this case, you are responsible * for providing a ready set of arguments which will - * be directly plugged in the operation execution. Beware, - * the `context` server option is an exception. Only if you - * dont provide a context alongside the returned value - * here, the `context` server option will be used instead. + * be directly plugged in the operation execution. + * + * Omitting the fields `contextValue` or `rootValue` + * from the returned value will have the provided server + * options fill in the gaps. * * To report GraphQL errors simply return an array * of them from the callback, they will be reported @@ -590,14 +592,13 @@ export function createServer( ]); } - // if onsubscribe didnt return anything, inject roots - if (!maybeExecArgsOrErrors) { + // if `onSubscribe` didnt specify a rootValue, inject one + if (!('rootValue' in execArgs)) { execArgs.rootValue = roots?.[operationAST.operation]; } - // inject the context, if provided, before the operation. - // but, only if the `onSubscribe` didnt provide one already - if (context !== undefined && !execArgs.contextValue) { + // if `onSubscribe` didn't specify a context, inject one + if (!('contextValue' in execArgs)) { execArgs.contextValue = typeof context === 'function' ? context(ctx, message, execArgs) diff --git a/src/tests/server.ts b/src/tests/server.ts index cdc55f72..c3ccfc65 100644 --- a/src/tests/server.ts +++ b/src/tests/server.ts @@ -364,6 +364,51 @@ it('should pass the `onSubscribe` exec args to the `context` option and use it', ); }); +it('should use the root from the `roots` option if the `onSubscribe` doesnt provide one', async (done) => { + const rootValue = {}; + const execArgs = { + // no rootValue here + schema, + document: parse(`query { getValue }`), + }; + + const { url } = await startTServer({ + roots: { + query: rootValue, + }, + onSubscribe: () => { + return execArgs; + }, + execute: (args) => { + expect(args).toBe(execArgs); // from `onSubscribe` + expect(args.rootValue).toBe(rootValue); // injected by `roots` + done(); + return execute(args); + }, + subscribe, + }); + + const client = await createTClient(url); + client.ws.send( + stringifyMessage({ + type: MessageType.ConnectionInit, + }), + ); + await client.waitForMessage(({ data }) => { + expect(parseMessage(data).type).toBe(MessageType.ConnectionAck); + }); + + client.ws.send( + stringifyMessage({ + id: '1', + type: MessageType.Subscribe, + payload: { + query: `{ getValue }`, + }, + }), + ); +}); + it('should prefer the `onSubscribe` context value even if `context` option is set', async (done) => { const context = 'not-me'; const execArgs = { @@ -790,6 +835,7 @@ describe('Subscribe', () => { schema, operationName: 'Nope', document: parse(`query Nope { getValue }`), + rootValue: null, }; const { url } = await startTServer({ schema: undefined, @@ -798,7 +844,7 @@ describe('Subscribe', () => { }, execute: (args) => { expect(args.schema).toBe(nopeArgs.schema); // schema from nopeArgs - expect(args.rootValue).toBeUndefined(); // nopeArgs didnt provide any root value + expect(args.rootValue).toBeNull(); // nopeArgs provided rootValue: null, so don't overwrite expect(args.operationName).toBe('Nope'); expect(args.variableValues).toBeUndefined(); // nopeArgs didnt provide variables return execute(args); From 70317b2619b7729e5d5556b4e5388f142414b082 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 19:49:01 +0000 Subject: [PATCH 08/21] fix(server): Don't surface bad request error details in production (#55) * fix: don't surface error details in production * refactor: shorten error Co-authored-by: enisdenjo --- src/server.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index ab23aa8b..feb668f6 100644 --- a/src/server.ts +++ b/src/server.ts @@ -663,7 +663,7 @@ export function createServer( } } catch (err) { // TODO-db-201031 we perceive this as a client bad request error, but is it always? - ctx.socket.close(4400, err.message); + ctx.socket.close(4400, isProd ? 'Bad Request' : err.message); } }; } From 0464a54eee09dfdf66d65bf539a4d8f596be2697 Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 20:01:30 +0000 Subject: [PATCH 09/21] fix(server): Close socket if `onSubscribe` returns invalid array (#53) * fix: throw useful error if onSubscribe returns invalid array * refactor: misconfiguration is serious * test: close socket Co-authored-by: enisdenjo --- src/server.ts | 4 ++++ src/tests/server.ts | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/src/server.ts b/src/server.ts index feb668f6..8994a49f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -552,6 +552,10 @@ export function createServer( if (maybeExecArgsOrErrors) { if (areGraphQLErrors(maybeExecArgsOrErrors)) { return await emit.error(maybeExecArgsOrErrors); + } else if (Array.isArray(maybeExecArgsOrErrors)) { + throw new Error( + 'Invalid return value from onSubscribe hook, expected an array of GraphQLError objects', + ); } // not errors, is exec args execArgs = maybeExecArgsOrErrors; diff --git a/src/tests/server.ts b/src/tests/server.ts index c3ccfc65..06b77ee4 100644 --- a/src/tests/server.ts +++ b/src/tests/server.ts @@ -941,6 +941,44 @@ describe('Subscribe', () => { }, 30); }); + it('should close the socket on empty arrays returned from `onSubscribe`', async () => { + const { url } = await startTServer({ + onSubscribe: () => { + return []; + }, + }); + + const client = await createTClient(url); + + client.ws.send( + stringifyMessage({ + type: MessageType.ConnectionInit, + }), + ); + + await client.waitForMessage(({ data }) => { + expect(parseMessage(data).type).toBe(MessageType.ConnectionAck); + }); + + client.ws.send( + stringifyMessage({ + id: '1', + type: MessageType.Subscribe, + payload: { + query: 'subscription { ping }', + }, + }), + ); + + await client.waitForClose((event) => { + expect(event.code).toBe(4400); + expect(event.reason).toBe( + 'Invalid return value from onSubscribe hook, expected an array of GraphQLError objects', + ); + expect(event.wasClean).toBeTruthy(); + }); + }); + it('should use the execution result returned from `onNext`', async () => { const { url } = await startTServer({ onNext: (_ctx, _message) => { From b96dbb98bb6f71956321ce1202af0af50df4e40e Mon Sep 17 00:00:00 2001 From: Benjie Gillam Date: Wed, 4 Nov 2020 20:33:24 +0000 Subject: [PATCH 10/21] fix(server): Distribute server error to all clients even if one error listener throws (#56) * fix: distribute error to all clients even if one error handler throws * feat: rethrow first error and test Co-authored-by: enisdenjo --- src/server.ts | 15 +++++++++++++-- src/tests/server.ts | 35 +++++++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index 8994a49f..0a008a6b 100644 --- a/src/server.ts +++ b/src/server.ts @@ -443,9 +443,20 @@ export function createServer( } webSocketServer.on('connection', handleConnection); webSocketServer.on('error', (err) => { + // catch the first thrown error and re-throw it once all clients have been notified + let firstErr: Error | null = null; + + // report server errors by erroring out all clients with the same error for (const client of webSocketServer.clients) { - // report server errors by erroring out all clients with the same error - client.emit('error', err); + try { + client.emit('error', err); + } catch (err) { + firstErr = firstErr ?? err; + } + } + + if (firstErr) { + throw firstErr; } }); diff --git a/src/tests/server.ts b/src/tests/server.ts index 06b77ee4..8ffeeeca 100644 --- a/src/tests/server.ts +++ b/src/tests/server.ts @@ -452,6 +452,41 @@ it('should prefer the `onSubscribe` context value even if `context` option is se ); }); +it('should handle errors thrown from client error listeners', async () => { + const { server, url } = await startTServer(); + + const client = await createTClient(url); + client.ws.send( + stringifyMessage({ + type: MessageType.ConnectionInit, + }), + ); + await client.waitForMessage(({ data }) => { + expect(parseMessage(data).type).toBe(MessageType.ConnectionAck); + }); + + const surpriseErr1 = new Error('Well hello there!'); + const surpriseErr2 = new Error('I wont be thrown!'); // first to throw stops emission + for (const client of server.webSocketServer.clients) { + client.on('error', () => { + throw surpriseErr1; + }); + client.on('error', () => { + throw surpriseErr2; + }); + } + + expect(() => { + server.webSocketServer.emit('error', new Error('I am a nice error')); + }).toThrowError(surpriseErr1); + + await client.waitForClose((event) => { + expect(event.code).toBe(1011); + expect(event.reason).toBe('I am a nice error'); + expect(event.wasClean).toBeTruthy(); + }); +}); + describe('Connect', () => { it('should refuse connection and close socket if returning `false`', async () => { const { url } = await startTServer({ From af4c0c2ff598c6bf67d69efb75720e680851add3 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 21:45:12 +0100 Subject: [PATCH 11/21] docs: abrupt closure calls onComplete too [skip ci] --- docs/interfaces/_server_.serveroptions.md | 4 ++++ src/server.ts | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/docs/interfaces/_server_.serveroptions.md b/docs/interfaces/_server_.serveroptions.md index 0003be31..e9547ea8 100644 --- a/docs/interfaces/_server_.serveroptions.md +++ b/docs/interfaces/_server_.serveroptions.md @@ -104,6 +104,10 @@ Throwing an error from within this function will close the socket with the `Error` message in the close event reason. +Since the library makes sure to complete streaming +operations even after an abrupt closure, this callback +will still be called. + ___ ### onConnect diff --git a/src/server.ts b/src/server.ts index 0a008a6b..7bcd7e08 100644 --- a/src/server.ts +++ b/src/server.ts @@ -278,6 +278,10 @@ export interface ServerOptions { * Throwing an error from within this function will * close the socket with the `Error` message * in the close event reason. + * + * Since the library makes sure to complete streaming + * operations even after an abrupt closure, this callback + * will still be called. */ onComplete?: (ctx: Context, message: CompleteMessage) => Promise | void; } From 4716121906922d0c9f1a2336de4e588b6f506762 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 21:49:03 +0100 Subject: [PATCH 12/21] style: functions are hoisted --- src/server.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/server.ts b/src/server.ts index 7bcd7e08..1f19359c 100644 --- a/src/server.ts +++ b/src/server.ts @@ -357,6 +357,7 @@ export function createServer( ? websocketOptionsOrServer : new WebSocket.Server(websocketOptionsOrServer); + webSocketServer.on('connection', handleConnection); function handleConnection(socket: WebSocket, request: http.IncomingMessage) { if ( Array.isArray(socket.protocol) @@ -445,7 +446,7 @@ export function createServer( socket.onclose = errorOrCloseHandler; socket.onmessage = makeOnMessage(ctxRef.current); } - webSocketServer.on('connection', handleConnection); + webSocketServer.on('error', (err) => { // catch the first thrown error and re-throw it once all clients have been notified let firstErr: Error | null = null; From de69b4ea39905f9b3343711e9defe68c6746b842 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 21:59:00 +0100 Subject: [PATCH 13/21] fix: Support more `graphql` versions --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index bba3b8aa..f4f2d393 100644 --- a/package.json +++ b/package.json @@ -42,7 +42,7 @@ "release": "semantic-release" }, "peerDependencies": { - "graphql": "^15.0.0" + "graphql": ">=0.11 <=15" }, "dependencies": { "ws": "^7.3.1" From 04c0b5cfce51de4c75a1e064514d0dbe3cc87cfc Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 22:01:27 +0100 Subject: [PATCH 14/21] chore(deps): update lockfile --- yarn.lock | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yarn.lock b/yarn.lock index 79e0aa4a..28f6f2ae 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4833,7 +4833,7 @@ fsevents@^2.1.2: typescript: ^4.0.5 ws: ^7.3.1 peerDependencies: - graphql: ^15.0.0 + graphql: ">=0.11 <=15" languageName: unknown linkType: soft From 19844d72f4638f9f7126870f9d9a744cdb4814c4 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 22:07:41 +0100 Subject: [PATCH 15/21] fix: Node 10 is the min supported version --- package.json | 3 +++ 1 file changed, 3 insertions(+) diff --git a/package.json b/package.json index f4f2d393..93460102 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,9 @@ "type": "git", "url": "git+https://github.com/enisdenjo/graphql-ws.git" }, + "engines": { + "node": ">=10" + }, "main": "lib/index.js", "browser": "lib/client.js", "types": "lib/index.d.ts", From 351963beaf87e7b8acec2abc21791c0e51a5b8fd Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 23:19:38 +0100 Subject: [PATCH 16/21] style: union with unknown collapses to unknown --- docs/interfaces/_types_.sink.md | 11 ++++++----- docs/modules/_types_.md | 2 ++ src/types.ts | 11 +++++------ 3 files changed, 13 insertions(+), 11 deletions(-) diff --git a/docs/interfaces/_types_.sink.md b/docs/interfaces/_types_.sink.md index b2da9d42..a733dfac 100644 --- a/docs/interfaces/_types_.sink.md +++ b/docs/interfaces/_types_.sink.md @@ -38,18 +38,19 @@ ___ ### error -▸ **error**(`error`: Error \| readonly GraphQLError[] \| unknown): void +▸ **error**(`error`: unknown): void An error that has occured. Calling this function "closes" the sink. -The error can be also `CloseEvent`, but to avoid bundling DOM typings -because the client can run in Node env too, you should assert -the close event type during implementation. +Besides the errors being `Error` and `readonly GraphQLError[]`, it +can also be a `CloseEvent`, but to avoid bundling DOM typings because +the client can run in Node env too, you should assert the close event +type during implementation. #### Parameters: Name | Type | ------ | ------ | -`error` | Error \| readonly GraphQLError[] \| unknown | +`error` | unknown | **Returns:** void diff --git a/docs/modules/_types_.md b/docs/modules/_types_.md index 830ec255..645a4339 100644 --- a/docs/modules/_types_.md +++ b/docs/modules/_types_.md @@ -4,6 +4,8 @@ # Module: "types" +types + ## Index ### Interfaces diff --git a/src/types.ts b/src/types.ts index 2777fcd9..54a68adb 100644 --- a/src/types.ts +++ b/src/types.ts @@ -4,8 +4,6 @@ * */ -import { GraphQLError } from 'graphql'; - /** * ID is a string type alias representing * the globally unique ID used for identifying @@ -26,11 +24,12 @@ export interface Sink { next(value: T): void; /** * An error that has occured. Calling this function "closes" the sink. - * The error can be also `CloseEvent`, but to avoid bundling DOM typings - * because the client can run in Node env too, you should assert - * the close event type during implementation. + * Besides the errors being `Error` and `readonly GraphQLError[]`, it + * can also be a `CloseEvent`, but to avoid bundling DOM typings because + * the client can run in Node env too, you should assert the close event + * type during implementation. */ - error(error: Error | readonly GraphQLError[] | unknown): void; + error(error: unknown): void; /** The sink has completed. This function "closes" the sink. */ complete(): void; } From ebb8dfe8a1e50507bcc2b0929600d755ddd98b1d Mon Sep 17 00:00:00 2001 From: Denis Badurina Date: Wed, 4 Nov 2020 23:31:15 +0100 Subject: [PATCH 17/21] feat: `cjs`, `esm` and `umd` builds with minification and compression for the browser (#58) * chore: cjs, esm and umd with minification and compression * fix: node module resolution * refactor: drop unnecessary comments * chore: flatten build structure for unpkg support * docs(recipe): client usage in browser --- .gitignore | 4 ++- README.md | 30 ++++++++++++++++ package.json | 18 +++++++--- rollup.config.js | 18 ++++++++++ tsconfig.build.json | 4 --- tsconfig.cjs.json | 8 +++++ tsconfig.esm.json | 8 +++++ tsconfig.json | 23 ++++++------ yarn.lock | 87 ++++++++++++++++++++++++++++++++++++++++++--- 9 files changed, 174 insertions(+), 26 deletions(-) create mode 100644 rollup.config.js delete mode 100644 tsconfig.build.json create mode 100644 tsconfig.cjs.json create mode 100644 tsconfig.esm.json diff --git a/.gitignore b/.gitignore index f390a854..7ae9fff1 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,8 @@ **/.DS_Store node_modules -lib +cjs +esm +umd dist .vscode .yarn/* diff --git a/README.md b/README.md index 137f032a..dc29494b 100644 --- a/README.md +++ b/README.md @@ -330,6 +330,36 @@ const link = new WebSocketLink({ +
+Client usage in browser + +```html + + + + + GraphQL over WebSocket + + + + + + +``` + +
+
Client usage in Node diff --git a/package.json b/package.json index 93460102..68c7327b 100644 --- a/package.json +++ b/package.json @@ -24,11 +24,14 @@ "engines": { "node": ">=10" }, - "main": "lib/index.js", - "browser": "lib/client.js", - "types": "lib/index.d.ts", + "main": "cjs/index.js", + "module": "esm/index.js", + "browser": "umd/graphql-ws.js", + "types": "esm/index.d.ts", "files": [ - "lib", + "cjs", + "esm", + "umd", "README.md", "LICENSE.md", "PROTOCOL.md" @@ -41,7 +44,10 @@ "lint": "eslint 'src'", "type-check": "tsc --noEmit", "test": "jest", - "build": "tsc -b tsconfig.build.json", + "build:cjs": "tsc -b tsconfig.cjs.json", + "build:esm": "tsc -b tsconfig.esm.json", + "build:umd": "rollup -c && gzip umd/graphql-ws.min.js -c > umd/graphql-ws.min.js.gz", + "build": "yarn build:cjs && yarn build:esm && yarn build:umd", "release": "semantic-release" }, "peerDependencies": { @@ -71,6 +77,8 @@ "graphql": "^15.4.0", "jest": "^26.6.1", "prettier": "^2.1.2", + "rollup": "^2.33.1", + "rollup-plugin-terser": "^7.0.2", "semantic-release": "^17.2.2", "typedoc": "^0.19.2", "typedoc-plugin-markdown": "^3.0.11", diff --git a/rollup.config.js b/rollup.config.js new file mode 100644 index 00000000..febf09e4 --- /dev/null +++ b/rollup.config.js @@ -0,0 +1,18 @@ +import { terser } from 'rollup-plugin-terser'; + +export default { + input: './esm/client.js', + output: [ + { + file: './umd/graphql-ws.js', + format: 'umd', + name: 'graphqlWs', + }, + { + file: './umd/graphql-ws.min.js', + format: 'umd', + name: 'graphqlWs', + plugins: [terser()], + }, + ], +}; diff --git a/tsconfig.build.json b/tsconfig.build.json deleted file mode 100644 index 78513920..00000000 --- a/tsconfig.build.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.json", - "exclude": ["src/tests", "lib"] -} diff --git a/tsconfig.cjs.json b/tsconfig.cjs.json new file mode 100644 index 00000000..ff45ec3f --- /dev/null +++ b/tsconfig.cjs.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "./cjs" + }, + "exclude": ["src/tests", "cjs", "esm"] +} diff --git a/tsconfig.esm.json b/tsconfig.esm.json new file mode 100644 index 00000000..c3bd7f0c --- /dev/null +++ b/tsconfig.esm.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "es2015", + "outDir": "./esm" + }, + "exclude": ["src/tests", "cjs", "esm"] +} diff --git a/tsconfig.json b/tsconfig.json index 3588f871..bcaadf83 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,18 +1,17 @@ { "compilerOptions": { /* Visit https://aka.ms/tsconfig.json to read more about this file */ - "target": "es2017" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */, - "module": "commonjs" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */, - "declaration": true /* Generates corresponding '.d.ts' file. */, - "outDir": "./lib" /* Redirect output structure to the directory. */, - "rootDir": "./src" /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */, - "strict": true /* Enable all strict type-checking options. */, - "noUnusedLocals": true /* Report errors on unused locals. */, - "noUnusedParameters": true /* Report errors on unused parameters. */, - "baseUrl": "./src" /* Base directory to resolve non-absolute module names. */, - "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, - "skipLibCheck": true /* Skip type checking of declaration files. */, - "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ + "moduleResolution": "node", + "target": "es2017", + "declaration": true, + "rootDir": "./src", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "baseUrl": "./src", + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true }, "exclude": ["lib"] } diff --git a/yarn.lock b/yarn.lock index 28f6f2ae..86252c0e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3129,6 +3129,13 @@ __metadata: languageName: node linkType: hard +"commander@npm:^2.20.0": + version: 2.20.3 + resolution: "commander@npm:2.20.3" + checksum: b73428e97de7624323f81ba13f8ed9271de487017432d18b4da3f07cfc528ad754bbd199004bd5d14e0ccd67d1fdfe0ec8dbbd4c438b401df3c4cc387bfd1daa + languageName: node + linkType: hard + "compare-func@npm:^2.0.0": version: 2.0.0 resolution: "compare-func@npm:2.0.0" @@ -4540,7 +4547,7 @@ __metadata: languageName: node linkType: hard -fsevents@^2.1.2: +"fsevents@^2.1.2, fsevents@~2.1.2": version: 2.1.3 resolution: "fsevents@npm:2.1.3" dependencies: @@ -4549,7 +4556,7 @@ fsevents@^2.1.2: languageName: node linkType: hard -"fsevents@patch:fsevents@^2.1.2#builtin": +"fsevents@patch:fsevents@^2.1.2#builtin, fsevents@patch:fsevents@~2.1.2#builtin": version: 2.1.3 resolution: "fsevents@patch:fsevents@npm%3A2.1.3#builtin::version=2.1.3&hash=127e8e" dependencies: @@ -4827,6 +4834,8 @@ fsevents@^2.1.2: graphql: ^15.4.0 jest: ^26.6.1 prettier: ^2.1.2 + rollup: ^2.33.1 + rollup-plugin-terser: ^7.0.2 semantic-release: ^17.2.2 typedoc: ^0.19.2 typedoc-plugin-markdown: ^3.0.11 @@ -6187,6 +6196,17 @@ fsevents@^2.1.2: languageName: node linkType: hard +"jest-worker@npm:^26.2.1": + version: 26.6.2 + resolution: "jest-worker@npm:26.6.2" + dependencies: + "@types/node": "*" + merge-stream: ^2.0.0 + supports-color: ^7.0.0 + checksum: 5eb349833b5e9750ce8700388961dfd5d5e207c913122221e418e48b9cda3c17b0fb418f6a90f1614cfdc3ca836158b720c5dc1de82cb1e708266b4d76e31a38 + languageName: node + linkType: hard + "jest-worker@npm:^26.6.1": version: 26.6.1 resolution: "jest-worker@npm:26.6.1" @@ -8573,6 +8593,15 @@ fsevents@^2.1.2: languageName: node linkType: hard +"randombytes@npm:^2.1.0": + version: 2.1.0 + resolution: "randombytes@npm:2.1.0" + dependencies: + safe-buffer: ^5.1.0 + checksum: ede2693af09732ceab1c273dd70db787f34a7b8d95bab13f1aca763483c0113452a78e53d61ff18d393dcea586d388e01f198a5132a4a85cebba31ec54164b75 + languageName: node + linkType: hard + "rc@npm:^1.0.1, rc@npm:^1.1.6, rc@npm:^1.2.8": version: 1.2.8 resolution: "rc@npm:1.2.8" @@ -9089,6 +9118,34 @@ fsevents@^2.1.2: languageName: node linkType: hard +"rollup-plugin-terser@npm:^7.0.2": + version: 7.0.2 + resolution: "rollup-plugin-terser@npm:7.0.2" + dependencies: + "@babel/code-frame": ^7.10.4 + jest-worker: ^26.2.1 + serialize-javascript: ^4.0.0 + terser: ^5.0.0 + peerDependencies: + rollup: ^2.0.0 + checksum: 553cc21efcea3e4d46c61fbd41cb4a82a3ab8e02ae4ce7c03f9248dea93e5a91c3624e2271490ee05b2bb481568305733b496d968d3ac9c99b777a588a336f01 + languageName: node + linkType: hard + +"rollup@npm:^2.33.1": + version: 2.33.1 + resolution: "rollup@npm:2.33.1" + dependencies: + fsevents: ~2.1.2 + dependenciesMeta: + fsevents: + optional: true + bin: + rollup: dist/bin/rollup + checksum: cf727ca4d1c6d6ad7679c8cf0a8258e41339d7d3eef5a7671cd54f4506c23f5492f6331c05d76697fa608d870407a5d2eb796647ce6add68101ca3bbb77c7d66 + languageName: node + linkType: hard + "rsvp@npm:^4.8.4": version: 4.8.5 resolution: "rsvp@npm:4.8.5" @@ -9269,6 +9326,15 @@ fsevents@^2.1.2: languageName: node linkType: hard +"serialize-javascript@npm:^4.0.0": + version: 4.0.0 + resolution: "serialize-javascript@npm:4.0.0" + dependencies: + randombytes: ^2.1.0 + checksum: f17305aaabab9ae443505d1bf477c13b09adb7031c397d18400bec16f43f788febdd3311ca6043fdebd1d446cfa70a5804ef7268da54351dec51080f56d52fa9 + languageName: node + linkType: hard + "set-blocking@npm:^2.0.0, set-blocking@npm:~2.0.0": version: 2.0.0 resolution: "set-blocking@npm:2.0.0" @@ -9492,7 +9558,7 @@ fsevents@^2.1.2: languageName: node linkType: hard -"source-map-support@npm:^0.5.6": +"source-map-support@npm:^0.5.6, source-map-support@npm:~0.5.19": version: 0.5.19 resolution: "source-map-support@npm:0.5.19" dependencies: @@ -9523,7 +9589,7 @@ fsevents@^2.1.2: languageName: node linkType: hard -"source-map@npm:^0.7.3": +"source-map@npm:^0.7.3, source-map@npm:~0.7.2": version: 0.7.3 resolution: "source-map@npm:0.7.3" checksum: 351ce26ffa1ebf203660c0d70d7566c81e65d2d994d1c2d94da140808e02da34961673ce12ecea9b40797b96fbeb8c70bf71a4ad9f779f1a4fdbba75530bb386 @@ -10027,6 +10093,19 @@ fsevents@^2.1.2: languageName: node linkType: hard +"terser@npm:^5.0.0": + version: 5.3.8 + resolution: "terser@npm:5.3.8" + dependencies: + commander: ^2.20.0 + source-map: ~0.7.2 + source-map-support: ~0.5.19 + bin: + terser: bin/terser + checksum: 3fc070378ba9981d8088a4012060f16428a97dc8f98d5a731b2ab51b8c9bc8cb651ca068aef499faad863584ae4d4883872c4f51db053bc2575c2e0bc11ab411 + languageName: node + linkType: hard + "test-exclude@npm:^6.0.0": version: 6.0.0 resolution: "test-exclude@npm:6.0.0" From 8385aa0552feccfbd2f004ac4280c148dfebc887 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 23:39:26 +0100 Subject: [PATCH 18/21] ci: upload/download lib artifacts [skip ci] --- .github/workflows/build-and-release.yml | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 1f1ddbaf..9fb51ec6 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -28,7 +28,10 @@ jobs: uses: actions/upload-artifact@v2 with: name: lib - path: lib + path: | + cjs + esm + umd release: name: Release @@ -58,7 +61,6 @@ jobs: uses: actions/download-artifact@v2 with: name: lib - path: lib - name: Release env: GH_TOKEN: ${{ secrets.GH_TOKEN }} From 9f4ad7d202fe110a4f0979f200e5dce15a20126d Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 23:40:44 +0100 Subject: [PATCH 19/21] ci: test artifacts transmission first [skip ci] --- .github/workflows/build-and-release.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index 9fb51ec6..d65cc48c 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -61,8 +61,5 @@ jobs: uses: actions/download-artifact@v2 with: name: lib - - name: Release - env: - GH_TOKEN: ${{ secrets.GH_TOKEN }} - NPM_TOKEN: ${{ secrets.NPM_TOKEN }} - run: yarn release + - name: List + run: ls -a From 889e27882330255649280f88a65675d63cd1e3e1 Mon Sep 17 00:00:00 2001 From: enisdenjo Date: Wed, 4 Nov 2020 23:44:04 +0100 Subject: [PATCH 20/21] ci: release ready [skip ci] --- .github/workflows/build-and-release.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-and-release.yml b/.github/workflows/build-and-release.yml index d65cc48c..9fb51ec6 100644 --- a/.github/workflows/build-and-release.yml +++ b/.github/workflows/build-and-release.yml @@ -61,5 +61,8 @@ jobs: uses: actions/download-artifact@v2 with: name: lib - - name: List - run: ls -a + - name: Release + env: + GH_TOKEN: ${{ secrets.GH_TOKEN }} + NPM_TOKEN: ${{ secrets.NPM_TOKEN }} + run: yarn release From 28f06eb73a96f6b3dc7e76147465b934f8503446 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Wed, 4 Nov 2020 22:45:48 +0000 Subject: [PATCH 21/21] =?UTF-8?q?chore(release):=20=F0=9F=8E=89=201.11.0?= =?UTF-8?q?=20[skip=20ci]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit # [1.11.0](https://github.com/enisdenjo/graphql-ws/compare/v1.10.0...v1.11.0) (2020-11-04) ### Bug Fixes * Node 10 is the min supported version ([19844d7](https://github.com/enisdenjo/graphql-ws/commit/19844d72f4638f9f7126870f9d9a744cdb4814c4)) * Support more `graphql` versions ([de69b4e](https://github.com/enisdenjo/graphql-ws/commit/de69b4ea39905f9b3343711e9defe68c6746b842)) * **server:** Close socket if `onSubscribe` returns invalid array ([#53](https://github.com/enisdenjo/graphql-ws/issues/53)) ([0464a54](https://github.com/enisdenjo/graphql-ws/commit/0464a54eee09dfdf66d65bf539a4d8f596be2697)) * **server:** Consistently set `rootValue` and `contextValue`, if not overridden ([#49](https://github.com/enisdenjo/graphql-ws/issues/49)) ([7aa3bcd](https://github.com/enisdenjo/graphql-ws/commit/7aa3bcdd38d40e83306f867a5b6b1bd612ec5fe3)) * **server:** Distribute server error to all clients even if one error listener throws ([#56](https://github.com/enisdenjo/graphql-ws/issues/56)) ([b96dbb9](https://github.com/enisdenjo/graphql-ws/commit/b96dbb98bb6f71956321ce1202af0af50df4e40e)) * **server:** Don't surface bad request error details in production ([#55](https://github.com/enisdenjo/graphql-ws/issues/55)) ([70317b2](https://github.com/enisdenjo/graphql-ws/commit/70317b2619b7729e5d5556b4e5388f142414b082)) ### Features * `cjs`, `esm` and `umd` builds with minification and compression for the browser ([#58](https://github.com/enisdenjo/graphql-ws/issues/58)) ([ebb8dfe](https://github.com/enisdenjo/graphql-ws/commit/ebb8dfe8a1e50507bcc2b0929600d755ddd98b1d)) ### Performance Improvements * Reduce runtime prototype traversal for hasOwnProperty ([#52](https://github.com/enisdenjo/graphql-ws/issues/52)) ([1bb9218](https://github.com/enisdenjo/graphql-ws/commit/1bb9218ad3ee9442442122c1d10910d51951b763)) --- CHANGELOG.md | 22 ++++++++++++++++++++++ package.json | 2 +- 2 files changed, 23 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2d383679..d15d9246 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,25 @@ +# [1.11.0](https://github.com/enisdenjo/graphql-ws/compare/v1.10.0...v1.11.0) (2020-11-04) + + +### Bug Fixes + +* Node 10 is the min supported version ([19844d7](https://github.com/enisdenjo/graphql-ws/commit/19844d72f4638f9f7126870f9d9a744cdb4814c4)) +* Support more `graphql` versions ([de69b4e](https://github.com/enisdenjo/graphql-ws/commit/de69b4ea39905f9b3343711e9defe68c6746b842)) +* **server:** Close socket if `onSubscribe` returns invalid array ([#53](https://github.com/enisdenjo/graphql-ws/issues/53)) ([0464a54](https://github.com/enisdenjo/graphql-ws/commit/0464a54eee09dfdf66d65bf539a4d8f596be2697)) +* **server:** Consistently set `rootValue` and `contextValue`, if not overridden ([#49](https://github.com/enisdenjo/graphql-ws/issues/49)) ([7aa3bcd](https://github.com/enisdenjo/graphql-ws/commit/7aa3bcdd38d40e83306f867a5b6b1bd612ec5fe3)) +* **server:** Distribute server error to all clients even if one error listener throws ([#56](https://github.com/enisdenjo/graphql-ws/issues/56)) ([b96dbb9](https://github.com/enisdenjo/graphql-ws/commit/b96dbb98bb6f71956321ce1202af0af50df4e40e)) +* **server:** Don't surface bad request error details in production ([#55](https://github.com/enisdenjo/graphql-ws/issues/55)) ([70317b2](https://github.com/enisdenjo/graphql-ws/commit/70317b2619b7729e5d5556b4e5388f142414b082)) + + +### Features + +* `cjs`, `esm` and `umd` builds with minification and compression for the browser ([#58](https://github.com/enisdenjo/graphql-ws/issues/58)) ([ebb8dfe](https://github.com/enisdenjo/graphql-ws/commit/ebb8dfe8a1e50507bcc2b0929600d755ddd98b1d)) + + +### Performance Improvements + +* Reduce runtime prototype traversal for hasOwnProperty ([#52](https://github.com/enisdenjo/graphql-ws/issues/52)) ([1bb9218](https://github.com/enisdenjo/graphql-ws/commit/1bb9218ad3ee9442442122c1d10910d51951b763)) + # [1.10.0](https://github.com/enisdenjo/graphql-ws/compare/v1.9.3...v1.10.0) (2020-11-03) diff --git a/package.json b/package.json index 68c7327b..857070e2 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "graphql-ws", - "version": "1.10.0", + "version": "1.11.0", "description": "Coherent, zero-dependency, lazy, simple, GraphQL over WebSocket Protocol compliant server and client", "keywords": [ "protocol",