Skip to content

Commit fec59e2

Browse files
authored
Merge branch 'master' into memory-leak-testing
2 parents aafc384 + 54af7cd commit fec59e2

File tree

12 files changed

+627
-342
lines changed

12 files changed

+627
-342
lines changed

docs/guides/development-environment.md

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ sidebar_label: Development Environment
66

77
## TypeScript
88

9-
### Basic Configuration
9+
### TS-Node ***(recommended way)***
1010

1111
To setup your development environment easily, we recommend to use [TypeScript](http://www.typescriptlang.org/).
1212

@@ -62,7 +62,7 @@ Next, add the following scripts to your `package.json`:
6262
- `build` will use `tsc` compiler to compile your code to JavaScript.
6363
- `start` will run the compiled server using pure Node.
6464

65-
### `paths`
65+
#### `paths`
6666

6767
TypeScript has an **[aliasing mechanism](https://www.typescriptlang.org/docs/handbook/module-resolution.html)** that can make it easier to work with modules directories.
6868

@@ -104,7 +104,7 @@ This way, you can now import files between modules like that:
104104
import { SomeProvider } from '@modules/my-module';
105105
```
106106

107-
### Import from `.graphql` files
107+
#### Import from `.graphql` files
108108

109109
You can also treat `.graphql` files as text files and import from them easily. It's useful because most IDEs detects `.graphql` files and have syntax highlighting for it.
110110

@@ -119,7 +119,7 @@ import 'graphql-import-node'; // You should add this at the begininng of your en
119119
import * as UserTypeDefs from './user.graphql';
120120
```
121121

122-
## Webpack
122+
### Webpack
123123

124124
If you are using Webpack, we recommend to use **[ts-loader](https://github.com/TypeStrong/ts-loader) or [awesome-typescript-loader](https://github.com/s-panferov/awesome-typescript-loader)** to load your TypeScript files.
125125

@@ -149,6 +149,20 @@ module.exports = {
149149
};
150150
```
151151

152+
### Babel-TypeScript
153+
154+
You can use Babel for TypeScript with GraphQL-Modules. [Check out this boilerplate](https://github.com/Microsoft/TypeScript-Babel-Starter)
155+
156+
But if you use DI, you have to decorate each property and argument in the providers manually even for the classes like below;
157+
158+
```ts
159+
import { Injectable, Inject } from '@graphql-modules/di';
160+
@Injectable()
161+
export class SomeProvider {
162+
constructor(@Inject(OtherProvider) private otherProvider: OtherProvider){}
163+
}
164+
```
165+
152166
## JavaScript Usage
153167

154168
If you are using JavaScript in your project and not TypeScript, you can either **[add support for TypeScript](https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html)**, or use GraphQL Modules with it's JavaScript API.
@@ -159,7 +173,7 @@ If you are using [Babel](http://babeljs.io) to transpile your JavaScript files,
159173

160174
### Without decorators
161175

162-
You can use `Inject` and `Injectable` as regular functions to wrap your arguments and classes.
176+
You can use `Inject` and `Injectable` as regular functions to wrap your arguments and classes from `tslib
163177

164178
## Testing Environment
165179

docs/introduction/dependency-injection.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -342,6 +342,9 @@ export class MyProvider implements OnDisconnect {
342342
> `OnDisconnect` hook is called once for each WebSocket GraphQL connection.
343343
[API of `OnDisconnect` is available here](/docs/api/core/api-interfaces-ondisconnect)
344344

345+
> The other `OnOperation` and `OnOperationComplete` hooks work similar to GraphQL Subscription Server implementation;
346+
[See more in SubscriptionServer docs](https://github.com/apollographql/subscriptions-transport-ws)
347+
345348
## Provider Scopes
346349

347350
You can define different life-time for your provider. You have three options;

docs/introduction/subscriptions.md

Lines changed: 31 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -4,11 +4,12 @@ title: Subscriptions
44
sidebar_label: Subscriptions
55
---
66

7-
Subscriptions are GraphQL operations that watch events emitted from your backend. GraphQL-Modules supports GraphQL subscriptions with a little modification in your server code. You can **[read more](https://www.apollographql.com/docs/apollo-server/features/subscriptions.html)** about subscriptions.
7+
Subscriptions are GraphQL operations that watch events emitted from your backend. GraphQL-Modules supports GraphQL subscriptions with a little modification in your server code. You can **[read more](https://github.com/apollographql/subscriptions-transport-ws)** about subscriptions.
88

99
Subscriptions need to have defined `PubSub` implementation in your GraphQL-Modules application.
1010

1111
```typescript
12+
import { PubSub } from 'graphql-subscriptions';
1213
export const CommonModule = new GraphQLModule({
1314
providers: [
1415
PubSub
@@ -72,12 +73,37 @@ You have to export `subscriptions` from your `AppModule`, and pass it to your Gr
7273
]
7374
});
7475

75-
const server = new ApolloServer({
76-
schema,
77-
context: session => session,
78-
subscriptions
76+
import { createServer } from 'http';
77+
import { SubscriptionServer } from 'subscriptions-transport-ws';
78+
import { execute, subscribe } from 'graphql';
79+
import { schema } from './my-schema';
80+
81+
const WS_PORT = 5000;
82+
83+
// Create WebSocket listener server
84+
const websocketServer = createServer((request, response) => {
85+
response.writeHead(404);
86+
response.end();
7987
});
8088

89+
// Bind it to port and start listening
90+
websocketServer.listen(WS_PORT, () => console.log(
91+
`Websocket Server is now running on http://localhost:${WS_PORT}`
92+
));
93+
94+
const subscriptionServer = SubscriptionServer.create(
95+
{
96+
schema,
97+
execute,
98+
subscribe,
99+
...subscriptions,
100+
},
101+
{
102+
server: websocketServer,
103+
path: '/graphql',
104+
},
105+
);
106+
81107
server.listen().then(({ url, subscriptionsUrl }) => {
82108
console.log(`🚀 Server ready at ${url}`);
83109
console.log(`🚀 Subscriptions ready at ${subscriptionsUrl}`);

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@
2626
"@types/graphql": "14.2.0",
2727
"@types/jest": "24.0.11",
2828
"graphql": "14.2.1",
29-
"jest": "24.6.0",
29+
"jest": "24.7.0",
3030
"lerna": "2.11.0",
3131
"reflect-metadata": "0.1.13",
3232
"replace-in-file": "3.4.4",

packages/core/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
"apollo-client": "2.5.1",
4747
"apollo-link-schema": "1.2.2",
4848
"graphql-tag": "2.10.1",
49-
"jest": "24.6.0",
49+
"jest": "24.7.0",
5050
"tslint": "5.15.0",
5151
"typescript": "3.4.1"
5252
},

packages/core/src/graphql-module.ts

Lines changed: 121 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ModuleSessionInfo } from './module-session-info';
2828
import { ModuleContext, SubscriptionHooks } from './types';
2929
import { asArray, normalizeSession } from './helpers';
3030
import { KeyValueCache, InMemoryLRUCache } from 'apollo-server-caching';
31+
import { ServerResponse } from 'http';
3132

3233
type MaybePromise<T> = Promise<T> | T;
3334

@@ -554,6 +555,8 @@ export class GraphQLModule<Config = any, Session extends object = any, Context =
554555
return this._cache.schemaDirectives;
555556
}
556557

558+
private paramsOnOperationResult$Map = new WeakMap<any, Promise<any>>();
559+
private opIdOnOperationCompleteResult$Map = new WeakMap<any, Promise<any>>();
557560
get subscriptions(): SubscriptionHooks {
558561
if (typeof this._cache.subscriptionHooks === 'undefined') {
559562
const subscriptionHooks = new Array<SubscriptionHooks>();
@@ -565,17 +568,17 @@ export class GraphQLModule<Config = any, Session extends object = any, Context =
565568
}
566569
}
567570
this._cache.subscriptionHooks = {
568-
onConnect: (connectionParams, websocket, connectionSession) => {
569-
if (!this._sessionContext$Map.has(connectionSession)) {
570-
this._sessionContext$Map.set(connectionSession, new Promise(async (resolve, reject) => {
571+
onConnect: (connectionParams, websocket, connectionContext) => {
572+
if (!this._sessionContext$Map.has(websocket)) {
573+
this._sessionContext$Map.set(websocket, new Promise(async (resolve, reject) => {
571574
try {
572575
const importsOnConnectHooks$ = subscriptionHooks.map(
573-
async ({ onConnect }) => onConnect && onConnect(connectionParams, websocket, connectionSession),
576+
async ({ onConnect }) => onConnect && onConnect(connectionParams, websocket, connectionContext),
574577
);
575578
const importsOnConnectHooks = await Promise.all(importsOnConnectHooks$);
576579
const importsResult = importsOnConnectHooks.reduce((acc, curr) => ({ ...acc, ...(curr || {}) }), {});
577-
const connectionContext = await this.context(connectionSession);
578-
const sessionInjector = connectionContext.injector;
580+
const connectionModuleContext = await this.context(websocket);
581+
const sessionInjector = connectionModuleContext.injector;
579582
const hookResult = await sessionInjector.callHookWithArgs({
580583
hook: 'onConnect',
581584
args: [
@@ -588,34 +591,111 @@ export class GraphQLModule<Config = any, Session extends object = any, Context =
588591
});
589592
resolve({
590593
...importsResult,
591-
...connectionContext,
594+
...connectionModuleContext,
592595
...hookResult,
593596
});
594597
} catch (e) {
595598
reject(e);
596599
}
597600
}));
598601
}
599-
return this._sessionContext$Map.get(connectionSession);
602+
return this._sessionContext$Map.get(websocket);
600603
},
601-
onDisconnect: async (websocket, connectionSession) => {
602-
const importsOnDisconnectHooks$ = subscriptionHooks.map(
603-
async ({ onDisconnect }) => onDisconnect && onDisconnect(websocket, connectionSession),
604-
);
605-
const importsOnDisconnectHooks = await Promise.all(importsOnDisconnectHooks$);
606-
importsOnDisconnectHooks.reduce((acc, curr) => ({ ...acc, ...(curr || {}) }), {});
607-
const connectionContext = await this.context(connectionSession);
608-
const sessionInjector = connectionContext.injector;
609-
await sessionInjector.callHookWithArgs({
610-
hook: 'onDisconnect',
611-
args: [
612-
websocket,
613-
connectionContext,
614-
],
615-
instantiate: true,
616-
async: true,
617-
});
618-
this.destroySelfSession(connectionSession);
604+
onOperation: (message, params, websocket) => {
605+
if (!this.paramsOnOperationResult$Map.has(params)) {
606+
this.paramsOnOperationResult$Map.set(params, new Promise(async (resolve, reject) => {
607+
try {
608+
const importsOnOperationHooks$ = subscriptionHooks.map(
609+
async ({ onOperation }) => onOperation && onOperation(message, params, websocket),
610+
);
611+
const importsOnOperationHooks = await Promise.all(importsOnOperationHooks$);
612+
const importsResult = importsOnOperationHooks.reduce((acc, curr) => ({ ...acc, ...acc(curr || {})}), {});
613+
const connectionModuleContext = await this.context(websocket);
614+
const sessionInjector = connectionModuleContext.injector;
615+
const moduleOnOperationResult = await sessionInjector.callHookWithArgs({
616+
hook: 'onOperation',
617+
args: [
618+
message,
619+
params,
620+
websocket,
621+
],
622+
instantiate: true,
623+
async: true,
624+
});
625+
resolve({
626+
...importsResult,
627+
...moduleOnOperationResult,
628+
});
629+
} catch (e) {
630+
reject(e);
631+
}
632+
}));
633+
}
634+
return this.paramsOnOperationResult$Map.get(params);
635+
},
636+
onOperationComplete: (websocket, opId: any) => {
637+
// tslint:disable-next-line: no-construct
638+
opId = new String(opId);
639+
if (!this.opIdOnOperationCompleteResult$Map.has(opId)) {
640+
this.opIdOnOperationCompleteResult$Map.set(opId, new Promise(async (resolve, reject) => {
641+
try {
642+
const importsOnOperationCompleteHooks$ = subscriptionHooks.map(
643+
async ({ onOperationComplete }) => onOperationComplete && onOperationComplete(websocket, opId),
644+
);
645+
const importsOnOperationCompleteHooks = await Promise.all(importsOnOperationCompleteHooks$);
646+
const importsResult = importsOnOperationCompleteHooks.reduce((acc, curr) => ({ ...acc, ...acc(curr || {})}), {});
647+
const connectionModuleContext = await this.context(websocket);
648+
const sessionInjector = connectionModuleContext.injector;
649+
const moduleOnOperationCompleteResult = await sessionInjector.callHookWithArgs({
650+
hook: 'onOperationComplete',
651+
args: [
652+
websocket,
653+
opId,
654+
],
655+
instantiate: true,
656+
async: true,
657+
});
658+
resolve({
659+
...importsResult,
660+
...moduleOnOperationCompleteResult,
661+
});
662+
} catch (e) {
663+
reject(e);
664+
}
665+
}));
666+
}
667+
return this.opIdOnOperationCompleteResult$Map.get(opId);
668+
},
669+
onDisconnect: (websocket, connectionContext) => {
670+
websocket['_moduleOnDisconnect$Map'] = websocket['_moduleOnDisconnect$Map'] || new WeakMap();
671+
const moduleOnDisconnect$Map: WeakMap<GraphQLModule, Promise<void>> = websocket['_moduleOnDisconnect$Map'];
672+
if (!moduleOnDisconnect$Map.has(this)) {
673+
moduleOnDisconnect$Map.set(this, new Promise(async (resolve, reject) => {
674+
try {
675+
const importsOnDisconnectHooks$ = subscriptionHooks.map(
676+
async ({ onDisconnect }) => onDisconnect && onDisconnect(websocket, connectionContext),
677+
);
678+
const importsOnDisconnectHooks = await Promise.all(importsOnDisconnectHooks$);
679+
importsOnDisconnectHooks.reduce((acc, curr) => ({ ...acc, ...(curr || {}) }), {});
680+
const connectionModuleContext = await this.context(websocket);
681+
const sessionInjector = connectionModuleContext.injector;
682+
await sessionInjector.callHookWithArgs({
683+
hook: 'onDisconnect',
684+
args: [
685+
websocket,
686+
connectionContext,
687+
],
688+
instantiate: true,
689+
async: true,
690+
});
691+
this.destroySelfSession(websocket);
692+
resolve();
693+
} catch (e) {
694+
reject(e);
695+
}
696+
}));
697+
}
698+
return moduleOnDisconnect$Map.get(this);
619699
},
620700
};
621701
}
@@ -1073,21 +1153,26 @@ export class GraphQLModule<Config = any, Session extends object = any, Context =
10731153
}
10741154
}
10751155
moduleSessionInfo.context = Object.assign<any, Context>(importsContext, moduleContext);
1076-
if ('res' in session && 'once' in session['res']) {
1077-
if (!('_onceFinishListeners' in session['res'])) {
1078-
session['res']['_onceFinishListeners'] = [];
1079-
session['res'].once('finish', (e: any) => {
1080-
const onceFinishListeners = session['res']['_onceFinishListeners'];
1081-
onceFinishListeners.map((onceFinishListener: any) => onceFinishListener(e));
1082-
delete session['res']['_onceFinishListeners'];
1156+
const res: ServerResponse = session && session['res'];
1157+
if (res && 'once' in res) {
1158+
if (!('_onceFinishListeners' in res)) {
1159+
res['_onceFinishListeners'] = [];
1160+
res.once('finish', () => {
1161+
const onceFinishListeners = res['_onceFinishListeners'];
1162+
for (const onceFinishListener of onceFinishListeners) {
1163+
onceFinishListener();
1164+
}
1165+
delete res['_onceFinishListeners'];
10831166
});
10841167
}
1085-
session['res']['_onceFinishListeners'].push(() => {
1086-
sessionInjector.callHookWithArgsAsync({
1168+
res['_onceFinishListeners'].push(() => {
1169+
sessionInjector.callHookWithArgs({
10871170
hook: 'onResponse',
10881171
args: [moduleSessionInfo],
10891172
instantiate: true,
1090-
}).then(() => this.destroySelfSession(session));
1173+
async: false,
1174+
});
1175+
this.destroySelfSession(session);
10911176
});
10921177
}
10931178
sessionInjector.onInstanceCreated = ({ instance }) => {

0 commit comments

Comments
 (0)