Skip to content

Commit 525e256

Browse files
AnotherHermitnodkz
authored andcommitted
feat: add progress middleware (thanks @AnotherHermit)
* Add Progress and Format as raw/format middleware * Extend NetworkLayer and fetch with raw middleware option * Fix tests * Fix whitespace in eslintrc * Update progress mw with simpler check for support * refactor after review * Update README
1 parent 650e6cd commit 525e256

File tree

10 files changed

+188
-23
lines changed

10 files changed

+188
-23
lines changed

.eslintrc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@
4242
"$FlowFixMe": true,
4343
"Blob": true,
4444
"Class": true,
45+
"Response": true,
4546
"window": true,
4647
"$PropertyType": true
4748
}

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,9 @@ import { RelayNetworkLayer } from 'react-relay-network-modern/es';
103103
* **errorMiddleware** - display `errors` data to console from graphql response. If you want see stackTrace for errors, you should provide `formatError` to `express-graphql` (see example below where `graphqlServer` accept `formatError` function).
104104
* `logger` - log function (default: `console.error.bind(console)`)
105105
* `prefix` - prefix message (default: `[RELAY-NETWORK] GRAPHQL SERVER ERROR:`)
106+
* **progressMiddleware** - enable onProgress callback for modern browsers with support for Stream API.
107+
* `onProgress` - on progress callback function (`function(bytesCurrent: number, bytesTotal: number | null) => void`, total size will be null if size header is not set)
108+
* `sizeHeader` - response header with total size of response (default: `Content-Length`, useful when `Transfer-Encoding: chunked` is set)
106109

107110
### Standalone package middlewares:
108111

@@ -122,6 +125,7 @@ import {
122125
retryMiddleware,
123126
authMiddleware,
124127
cacheMiddleware,
128+
progressMiddleware,
125129
} from 'react-relay-network-modern';
126130

127131
const network = new RelayNetworkLayer(
@@ -163,6 +167,11 @@ const network = new RelayNetworkLayer(
163167
.catch(err => console.log('[client.js] ERROR can not refresh token', err));
164168
},
165169
}),
170+
progressMiddleware({
171+
onProgress: (current, total) => {
172+
console.log('Downloaded: ' + current + ' B, total: ' + total + ' B');
173+
},
174+
}),
166175

167176
// example of the custom inline middleware
168177
next => async req => {
@@ -229,6 +238,8 @@ return new RRNL.RelayNetworkLayer([
229238

230239
Middlewares on bottom layer use [fetch](https://github.com/github/fetch) method. So `req` is compliant with a `fetch()` options. And `res` can be obtained via `resPromise.then(res => ...)`, which returned by `fetch()`.
231240

241+
Middleware that needs access to the raw response body from fetch (before it has been consumed) can set `isRawMiddleware = true`, see `progressMiddleware` for example. It is important to note that `response.body` can only be consumed once, so make sure to `clone()` the response first.
242+
232243
Middlewares have 3 phases:
233244

234245
* `setup phase`, which runs only once, when middleware added to the NetworkLayer

src/RelayNetworkLayer.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import fetchWithMiddleware from './fetchWithMiddleware';
66
import type {
77
Middleware,
88
MiddlewareSync,
9+
MiddlewareRaw,
910
FetchFunction,
1011
FetchHookFunction,
1112
SubscribeFunction,
@@ -20,14 +21,19 @@ type RelayNetworkLayerOpts = {|
2021

2122
export default class RelayNetworkLayer {
2223
_middlewares: Middleware[];
24+
_rawMiddlewares: MiddlewareRaw[];
2325
_middlewaresSync: RNLExecuteFunction[];
2426
execute: RNLExecuteFunction;
2527
+fetchFn: FetchFunction;
2628
+subscribeFn: ?SubscribeFunction;
2729
+noThrow: boolean;
2830

29-
constructor(middlewares: Array<?Middleware | MiddlewareSync>, opts?: RelayNetworkLayerOpts) {
31+
constructor(
32+
middlewares: Array<?Middleware | MiddlewareSync | MiddlewareRaw>,
33+
opts?: RelayNetworkLayerOpts
34+
) {
3035
this._middlewares = [];
36+
this._rawMiddlewares = [];
3137
this._middlewaresSync = [];
3238
this.noThrow = false;
3339

@@ -36,6 +42,8 @@ export default class RelayNetworkLayer {
3642
if (mw) {
3743
if (mw.execute) {
3844
this._middlewaresSync.push(mw.execute);
45+
} else if (mw.isRawMiddleware) {
46+
this._rawMiddlewares.push(mw);
3947
} else {
4048
this._middlewares.push(mw);
4149
}
@@ -59,7 +67,7 @@ export default class RelayNetworkLayer {
5967
}
6068

6169
const req = new RelayRequest(operation, variables, cacheConfig, uploadables);
62-
return fetchWithMiddleware(req, this._middlewares, this.noThrow);
70+
return fetchWithMiddleware(req, this._middlewares, this._rawMiddlewares, this.noThrow);
6371
};
6472

6573
const network = Network.create(this.fetchFn, this.subscribeFn);

src/RelayResponse.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
/* @flow */
22

3-
import type { PayloadData } from './definition';
3+
import type { PayloadData, FetchResponse } from './definition';
44

55
export default class RelayResponse {
66
_res: any; // response from low-level method, eg. fetch
@@ -16,7 +16,7 @@ export default class RelayResponse {
1616
text: ?string;
1717
json: mixed;
1818

19-
static async createFromFetch(res: Object): Promise<RelayResponse> {
19+
static async createFromFetch(res: FetchResponse): Promise<RelayResponse> {
2020
const r = new RelayResponse();
2121
r._res = res;
2222
r.ok = res.ok;

src/__tests__/RelayNetworkLayer-test.js

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,4 +76,52 @@ describe('RelayNetworkLayer', () => {
7676
expect(asyncMW).toHaveBeenCalled();
7777
});
7878
});
79+
80+
it('should correctly call raw middlewares', async () => {
81+
fetchMock.mock({
82+
matcher: '/graphql',
83+
response: {
84+
status: 200,
85+
body: {
86+
data: { text: 'response' },
87+
},
88+
sendAsJson: true,
89+
},
90+
method: 'POST',
91+
});
92+
93+
const regularMiddleware = next => async req => {
94+
(req: any).fetchOpts.headers.reqId += ':regular';
95+
const res: any = await next(req);
96+
res.data.text += ':regular';
97+
return res;
98+
};
99+
100+
const createRawMiddleware = (id: number): any => {
101+
const rawMiddleware = next => async req => {
102+
(req: any).fetchOpts.headers.reqId += `:raw${id}`;
103+
const res: any = await next(req);
104+
const parentJsonFN = res.json;
105+
res.json = async () => {
106+
const json = await parentJsonFN.bind(res)();
107+
json.data.text += `:raw${id}`;
108+
return json;
109+
};
110+
return res;
111+
};
112+
rawMiddleware.isRawMiddleware = true;
113+
return rawMiddleware;
114+
};
115+
116+
// rawMiddlewares should be called the last
117+
const network = new RelayNetworkLayer([
118+
createRawMiddleware(1),
119+
createRawMiddleware(2),
120+
regularMiddleware,
121+
]);
122+
const observable: any = network.execute(mockOperation, {}, {});
123+
const result = await observable.toPromise();
124+
expect(fetchMock.lastOptions().headers.reqId).toEqual('undefined:regular:raw1:raw2');
125+
expect(result.response.data).toEqual({ text: 'undefined:raw2:raw1:regular' });
126+
});
79127
});

src/__tests__/fetchWithMiddleware-test.js

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ describe('fetchWithMiddleware', () => {
1313
it('should make a successfull request without middlewares', async () => {
1414
fetchMock.post('/graphql', { id: 1, data: { user: 123 } });
1515
const req = new RelayRequest(({}: any), {}, {}, null);
16-
const res = await fetchWithMiddleware(req, []);
16+
const res = await fetchWithMiddleware(req, [], []);
1717
expect(res.data).toEqual({ user: 123 });
1818
});
1919

@@ -37,11 +37,15 @@ describe('fetchWithMiddleware', () => {
3737
reqId: 'request',
3838
};
3939

40-
const res: any = await fetchWithMiddleware(req, [
41-
numPlus5,
42-
numMultiply10, // should be last, when changing request
43-
// should be first, when changing response
44-
]);
40+
const res: any = await fetchWithMiddleware(
41+
req,
42+
[
43+
numPlus5,
44+
numMultiply10, // should be last, when changing request
45+
// should be first, when changing response
46+
],
47+
[]
48+
);
4549
expect(res.data.text).toEqual('response:mw2:mw1');
4650
expect(fetchMock.lastOptions().headers.reqId).toEqual('request:mw1:mw2');
4751
});
@@ -58,7 +62,7 @@ describe('fetchWithMiddleware', () => {
5862

5963
expect.assertions(2);
6064
try {
61-
await fetchWithMiddleware(req, []);
65+
await fetchWithMiddleware(req, [], []);
6266
} catch (e) {
6367
expect(e instanceof Error).toBeTruthy();
6468
expect(e.toString()).toMatch('Network connection error');
@@ -81,7 +85,7 @@ describe('fetchWithMiddleware', () => {
8185

8286
expect.assertions(2);
8387
try {
84-
await fetchWithMiddleware(req, []);
88+
await fetchWithMiddleware(req, [], []);
8589
} catch (e) {
8690
expect(e instanceof Error).toBeTruthy();
8791
expect(e.toString()).toMatch('major error');
@@ -103,7 +107,7 @@ describe('fetchWithMiddleware', () => {
103107
const req = new RelayRequest(({}: any), {}, {}, null);
104108

105109
expect.assertions(1);
106-
const res = await fetchWithMiddleware(req, [], true);
110+
const res = await fetchWithMiddleware(req, [], [], true);
107111
expect(res.errors).toEqual([{ location: 1, message: 'major error' }]);
108112
});
109113

@@ -121,7 +125,7 @@ describe('fetchWithMiddleware', () => {
121125

122126
expect.assertions(2);
123127
try {
124-
await fetchWithMiddleware(req, []);
128+
await fetchWithMiddleware(req, [], []);
125129
} catch (e) {
126130
expect(e instanceof Error).toBeTruthy();
127131
expect(e.toString()).toMatch('Something went completely wrong');
@@ -143,7 +147,7 @@ describe('fetchWithMiddleware', () => {
143147

144148
expect.assertions(2);
145149
try {
146-
await fetchWithMiddleware(req, []);
150+
await fetchWithMiddleware(req, [], []);
147151
} catch (e) {
148152
expect(e instanceof Error).toBeTruthy();
149153
expect(e.toString()).toMatch('Server return empty response.data');

src/definition.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,12 @@ import type RelayResponse from './RelayResponse';
77
export type RelayRequestAny = RelayRequest | RelayRequestBatch;
88
export type MiddlewareNextFn = (req: RelayRequestAny) => Promise<RelayResponse>;
99
export type Middleware = (next: MiddlewareNextFn) => MiddlewareNextFn;
10+
export type MiddlewareRawNextFn = (req: RelayRequestAny) => Promise<FetchResponse>;
11+
12+
export type MiddlewareRaw = {
13+
isRawMiddleware: true,
14+
$call: (next: MiddlewareRawNextFn) => MiddlewareRawNextFn,
15+
};
1016

1117
export type MiddlewareSync = {|
1218
execute: (
@@ -30,6 +36,8 @@ export type FetchOpts = {
3036
[name: string]: mixed,
3137
};
3238

39+
export type FetchResponse = Response;
40+
3341
export type GraphQLResponseErrors = Array<{
3442
message: string,
3543
locations?: Array<{

src/fetchWithMiddleware.js

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,16 @@
33

44
import { createRequestError } from './createRequestError';
55
import RelayResponse from './RelayResponse';
6-
import type { Middleware, MiddlewareNextFn, RelayRequestAny } from './definition';
6+
import type {
7+
Middleware,
8+
MiddlewareNextFn,
9+
RelayRequestAny,
10+
MiddlewareRaw,
11+
MiddlewareRawNextFn,
12+
FetchResponse,
13+
} from './definition';
714

8-
async function runFetch(req: RelayRequestAny): Promise<RelayResponse> {
15+
function runFetch(req: RelayRequestAny): Promise<FetchResponse> {
916
let { url } = req.fetchOpts;
1017
if (!url) url = '/graphql';
1118

@@ -14,21 +21,32 @@ async function runFetch(req: RelayRequestAny): Promise<RelayResponse> {
1421
req.fetchOpts.headers['Content-Type'] = 'application/json';
1522
}
1623

17-
// $FlowFixMe
18-
const resFromFetch = await fetch(url, req.fetchOpts);
24+
return fetch(url, (req.fetchOpts: any));
25+
}
26+
27+
// convert fetch response to RelayResponse object
28+
const convertResponse: (next: MiddlewareRawNextFn) => MiddlewareNextFn = next => async req => {
29+
const resFromFetch = await next(req);
30+
1931
const res = await RelayResponse.createFromFetch(resFromFetch);
2032
if (res.status && res.status >= 400) {
2133
throw createRequestError(req, res);
2234
}
2335
return res;
24-
}
36+
};
2537

2638
export default function fetchWithMiddleware(
2739
req: RelayRequestAny,
28-
middlewares: Middleware[],
40+
middlewares: Middleware[], // works with RelayResponse
41+
rawFetchMiddlewares: MiddlewareRaw[], // works with raw fetch response
2942
noThrow?: boolean
3043
): Promise<RelayResponse> {
31-
const wrappedFetch: MiddlewareNextFn = compose(...middlewares)(runFetch);
44+
// $FlowFixMe
45+
const wrappedFetch: MiddlewareNextFn = compose(
46+
...middlewares,
47+
convertResponse,
48+
...rawFetchMiddlewares
49+
)((runFetch: any));
3250

3351
return wrappedFetch(req).then(res => {
3452
if (!noThrow && (!res || res.errors || !res.data)) {
@@ -54,6 +72,6 @@ function compose(...funcs) {
5472
} else {
5573
const last = funcs[funcs.length - 1];
5674
const rest = funcs.slice(0, -1);
57-
return (...args) => rest.reduceRight((composed, f) => f(composed), last(...args));
75+
return (...args) => rest.reduceRight((composed, f) => f((composed: any)), last(...args));
5876
}
5977
}

src/index.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import perfMiddleware from './middlewares/perf';
99
import loggerMiddleware from './middlewares/logger';
1010
import errorMiddleware from './middlewares/error';
1111
import cacheMiddleware from './middlewares/cache';
12+
import progressMiddleware from './middlewares/progress';
1213
import graphqlBatchHTTPWrapper from './express-middleware/graphqlBatchHTTPWrapper';
1314
import RelayNetworkLayerRequest from './RelayRequest';
1415
import RelayNetworkLayerRequestBatch from './RelayRequestBatch';
@@ -27,5 +28,6 @@ export {
2728
loggerMiddleware,
2829
errorMiddleware,
2930
cacheMiddleware,
31+
progressMiddleware,
3032
graphqlBatchHTTPWrapper,
3133
};

0 commit comments

Comments
 (0)