Skip to content

Commit ec20895

Browse files
committed
cancel request example illustration
1 parent 3ae9b7e commit ec20895

File tree

8 files changed

+131
-41
lines changed

8 files changed

+131
-41
lines changed

.gitignore

+2-1
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
node_modules
22
.DS_Store
3-
.env
3+
.env
4+
final/server/store.sqlite

final/client/package.json

+14-10
Original file line numberDiff line numberDiff line change
@@ -3,20 +3,23 @@
33
"version": "0.1.0",
44
"private": true,
55
"dependencies": {
6-
"@apollo/client": "^3.0.2",
7-
"@reach/router": "^1.2.1",
8-
"@types/node": "^12.12.14",
6+
"@apollo/client": "^3.1.5",
7+
"@reach/router": "^1.3.4",
8+
"@types/node": "^12.12.58",
99
"@types/reach__router": "^1.2.6",
10-
"@types/react": "^16.9.15",
10+
"@types/react": "^16.9.49",
1111
"@types/react-dom": "^16.9.4",
12+
"@types/uuid": "^8.3.0",
1213
"emotion": "^9.2.12",
13-
"graphql": "^14.4.2",
14-
"polished": "^3.4.1",
14+
"graphql": "^14.7.0",
15+
"polished": "^3.6.6",
1516
"react": "^16.12.0",
1617
"react-dom": "^16.12.0",
1718
"react-emotion": "^9.2.12",
18-
"react-scripts": "^3.4.1",
19-
"typescript": "^3.7.3"
19+
"react-scripts": "^3.4.3",
20+
"shortid": "^2.2.15",
21+
"typescript": "^3.9.7",
22+
"uuid": "^8.3.0"
2023
},
2124
"scripts": {
2225
"start": "react-scripts start",
@@ -39,8 +42,9 @@
3942
"@testing-library/jest-dom": "^4.0.0",
4043
"@testing-library/react": "^8.0.7",
4144
"@types/jest": "^24.0.23",
42-
"apollo": "^2.16.3",
43-
"artillery": "^1.6.0-26",
45+
"@types/shortid": "0.0.29",
46+
"apollo": "^2.30.3",
47+
"artillery": "^1.6.1",
4448
"npm-watch": "^0.6.0"
4549
}
4650
}

final/client/src/cancelRequest.ts

+54
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import {
2+
ApolloLink,
3+
Observable
4+
} from '@apollo/client';
5+
6+
7+
const connections: { [key: string]: any } = {};
8+
9+
export const cancelRequestLink = new ApolloLink(
10+
(operation, forward) =>
11+
new Observable(observer => {
12+
// Set x-CSRF token (not related to abort use case)
13+
const context = operation.getContext();
14+
/** Final touch to cleanup */
15+
16+
const connectionHandle = forward(operation).subscribe({
17+
next: (...arg) => observer.next(...arg),
18+
error: (...arg) => {
19+
cleanUp();
20+
observer.error(...arg);
21+
},
22+
complete: (...arg) => {
23+
cleanUp();
24+
observer.complete(...arg)
25+
}
26+
});
27+
28+
const cleanUp = () => {
29+
connectionHandle?.unsubscribe();
30+
delete connections[context.requestTrackerId];
31+
}
32+
33+
if (context.requestTrackerId) {
34+
const controller = new AbortController();
35+
controller.signal.onabort = cleanUp;
36+
operation.setContext({
37+
...context,
38+
fetchOptions: {
39+
signal: controller.signal,
40+
...context?.fetchOptions
41+
},
42+
});
43+
44+
if (connections[context.requestTrackerId]) {
45+
// If a controller exists, that means this operation should be aborted.
46+
connections[context.requestTrackerId]?.abort();
47+
}
48+
49+
connections[context.requestTrackerId] = controller;
50+
}
51+
52+
return connectionHandle;
53+
})
54+
);

final/client/src/components/login-form.tsx

+5
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,12 @@ import { ReactComponent as Curve } from '../assets/curve.svg';
99
import { ReactComponent as Rocket } from '../assets/rocket.svg';
1010
import { colors, unit } from '../styles';
1111
import * as LoginTypes from '../pages/__generated__/login';
12+
import { Loading } from '../components';
1213

1314
interface LoginFormProps {
1415
login: (a: { variables: LoginTypes.LoginVariables }) => void;
16+
error: any,
17+
loading: boolean
1518
}
1619

1720
interface LoginFormState {
@@ -32,6 +35,7 @@ export default class LoginForm extends Component<LoginFormProps, LoginFormState>
3235
};
3336

3437
render() {
38+
if (this.props.error) return <p>An error occurred</p>;
3539
return (
3640
<Container>
3741
<Header>
@@ -50,6 +54,7 @@ export default class LoginForm extends Component<LoginFormProps, LoginFormState>
5054
onChange={(e) => this.onChange(e)}
5155
/>
5256
<Button type="submit">Log in</Button>
57+
{this.props.loading ? <Loading /> : null}
5358
</StyledForm>
5459
</Container>
5560
);

final/client/src/index.tsx

+23-8
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,11 @@ import {
66
ApolloProvider,
77
useQuery,
88
gql,
9+
ApolloLink,
10+
from,
11+
createHttpLink,
912
} from '@apollo/client';
13+
import { cancelRequestLink } from './cancelRequest';
1014

1115
import Pages from './pages';
1216
import Login from './pages/login';
@@ -20,18 +24,29 @@ export const typeDefs = gql`
2024
}
2125
`;
2226

27+
const httpLink = createHttpLink({ uri: 'http://localhost:4000/graphql' });
28+
29+
const authMiddleware = new ApolloLink((operation, forward) => {
30+
// add the authorization to the headers
31+
operation.setContext({
32+
headers: {
33+
authorization: localStorage.getItem('token') || '',
34+
'client-name': 'Space Explorer [web]',
35+
'client-version': '1.0.0',
36+
},
37+
typeDefs,
38+
resolvers: {},
39+
});
40+
41+
return forward(operation);
42+
})
43+
2344
// Set up our apollo-client to point at the server we created
2445
// this can be local or a remote endpoint
2546
const client: ApolloClient<NormalizedCacheObject> = new ApolloClient({
2647
cache,
27-
uri: 'http://localhost:4000/graphql',
28-
headers: {
29-
authorization: localStorage.getItem('token') || '',
30-
'client-name': 'Space Explorer [web]',
31-
'client-version': '1.0.0',
32-
},
33-
typeDefs,
34-
resolvers: {},
48+
link: from([ authMiddleware, cancelRequestLink, httpLink ]),
49+
queryDeduplication: false
3550
});
3651

3752
/**

final/client/src/pages/login.tsx

+7-6
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import React from 'react';
22
import { gql, useMutation } from '@apollo/client';
33

4-
import { LoginForm, Loading } from '../components';
4+
import { LoginForm } from '../components';
55
import { isLoggedInVar } from '../cache';
66
import * as LoginTypes from './__generated__/login';
7+
import { v5 as uuidNameSpace, v4 as getNS } from 'uuid';
8+
9+
const RequestNameSpace = getNS();
710

811
export const LOGIN_USER = gql`
912
mutation Login($email: String!) {
@@ -25,12 +28,10 @@ export default function Login() {
2528
localStorage.setItem('token', login.token as string);
2629
localStorage.setItem('userId', login.id as string);
2730
isLoggedInVar(true);
28-
}
31+
},
32+
context:{ requestTrackerId: uuidNameSpace('LOGIN', RequestNameSpace) }
2933
}
3034
);
3135

32-
if (loading) return <Loading />;
33-
if (error) return <p>An error occurred</p>;
34-
35-
return <LoginForm login={login} />;
36+
return <LoginForm error={error} loading={loading} login={login} />;
3637
}

final/server/package.json

+15-15
Original file line numberDiff line numberDiff line change
@@ -13,26 +13,26 @@
1313
"dependencies": {
1414
"apollo-datasource": "^0.1.3",
1515
"apollo-datasource-rest": "^0.1.5",
16-
"apollo-server": "^2.15.0",
17-
"apollo-server-testing": "^2.15.0",
18-
"aws-sdk": "^2.585.0",
16+
"apollo-server": "^2.17.0",
17+
"apollo-server-testing": "^2.17.0",
18+
"aws-sdk": "^2.751.0",
1919
"dotenv": "^6.2.0",
20-
"graphql": "^14.6.0",
20+
"graphql": "^14.7.0",
2121
"isemail": "^3.1.3",
22-
"mime": "^2.4.4",
22+
"mime": "^2.4.6",
2323
"nodemon": "^1.19.4",
24-
"sequelize": "^5.21.2",
25-
"sqlite3": "^4.1.1",
26-
"uuid": "^3.3.3"
24+
"sequelize": "^5.22.3",
25+
"sqlite3": "^4.2.0",
26+
"uuid": "^3.4.0"
2727
},
2828
"devDependencies": {
29-
"apollo": "^2.1.8",
30-
"apollo-link": "^1.2.3",
31-
"apollo-link-http": "^1.5.5",
32-
"jest": "^25.0.0",
33-
"nock": "^10.0.2",
34-
"node-fetch": "^2.2.1",
35-
"now": "^12.1.3"
29+
"apollo": "^2.30.3",
30+
"apollo-link": "^1.2.14",
31+
"apollo-link-http": "^1.5.17",
32+
"jest": "^25.5.4",
33+
"nock": "^10.0.6",
34+
"node-fetch": "^2.6.1",
35+
"now": "^12.1.14"
3636
},
3737
"jest": {
3838
"testPathIgnorePatterns": [

final/server/src/resolvers.js

+11-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
const { paginateResults } = require('./utils');
22

3+
4+
//Simulate the delay
5+
function sleeper(ms) {
6+
return function(x) {
7+
return new Promise(resolve => setTimeout(() => resolve(x), ms));
8+
};
9+
}
10+
311
module.exports = {
412
Query: {
513
launches: async (_, { pageSize = 20, after }, { dataSources }) => {
@@ -64,7 +72,9 @@ module.exports = {
6472
};
6573
},
6674
login: async (_, { email }, { dataSources }) => {
67-
const user = await dataSources.userAPI.findOrCreateUser({ email });
75+
76+
//Intentional delay to show the cancelrequest @ client-side
77+
const user = await dataSources.userAPI.findOrCreateUser({ email }).then(sleeper(5000));
6878
if (user) {
6979
user.token = new Buffer(email).toString('base64');
7080
return user;

0 commit comments

Comments
 (0)