@podium/client 5.0.0-next.10 → 5.0.0-next.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # [5.0.0-next.12](https://github.com/podium-lib/client/compare/v5.0.0-next.11...v5.0.0-next.12) (2022-09-20)
2
+
3
+
4
+ ### Features
5
+
6
+ * replace request with undici for http requests ([31163db](https://github.com/podium-lib/client/commit/31163db538d87cb797135d7febcc6cc244eac17a))
7
+
8
+ # [5.0.0-next.11](https://github.com/podium-lib/client/compare/v5.0.0-next.10...v5.0.0-next.11) (2022-09-08)
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * Fix inspect method on response object ([#294](https://github.com/podium-lib/client/issues/294)) ([94d1485](https://github.com/podium-lib/client/commit/94d14856ca2280888aa50e9cb7e7f782cd49666a))
14
+
1
15
  # [5.0.0-next.10](https://github.com/podium-lib/client/compare/v5.0.0-next.9...v5.0.0-next.10) (2022-09-07)
2
16
 
3
17
 
package/lib/http.js ADDED
@@ -0,0 +1,23 @@
1
+ import { Client } from 'undici';
2
+
3
+ export default class HTTP {
4
+ // #client;
5
+
6
+ // constructor() {
7
+ // // this.#client = { request };
8
+ // }
9
+
10
+ async request(url, options) {
11
+ const u = new URL(url);
12
+ const client = new Client(u.origin, {
13
+ ...options,
14
+ connect: { rejectUnauthorized: options.rejectUnauthorized },
15
+ });
16
+
17
+ const { statusCode, headers, body } = await client.request({
18
+ ...options,
19
+ path: u.pathname,
20
+ });
21
+ return { statusCode, headers, body };
22
+ }
23
+ }
@@ -1,21 +1,22 @@
1
1
  /* eslint-disable no-param-reassign */
2
-
3
2
  import { pipeline } from 'stream';
4
3
  import Metrics from '@metrics/client';
5
- import request from 'request';
6
4
  import abslog from 'abslog';
7
5
  import * as putils from '@podium/utils';
8
6
  import { Boom, badGateway } from '@hapi/boom';
9
-
10
7
  import { join, dirname } from 'path';
11
8
  import { fileURLToPath } from 'url';
12
9
  import fs from 'fs';
13
10
  import * as utils from './utils.js';
14
11
  import Response from './response.js';
12
+ import HTTP from './http.js';
15
13
 
16
14
  const currentDirectory = dirname(fileURLToPath(import.meta.url));
17
15
 
18
- const pkgJson = fs.readFileSync(join(currentDirectory, '../package.json'), 'utf-8');
16
+ const pkgJson = fs.readFileSync(
17
+ join(currentDirectory, '../package.json'),
18
+ 'utf-8',
19
+ );
19
20
  const pkg = JSON.parse(pkgJson);
20
21
 
21
22
  const UA_STRING = `${pkg.name} ${pkg.version}`;
@@ -24,13 +25,11 @@ export default class PodletClientContentResolver {
24
25
  #log;
25
26
  #metrics;
26
27
  #histogram;
27
- #httpAgent;
28
- #httpsAgent;
28
+ #http;
29
29
  constructor(options = {}) {
30
+ this.#http = options.http || new HTTP();
30
31
  const name = options.clientName;
31
32
  this.#log = abslog(options.logger);
32
- this.#httpAgent = options.httpAgent;
33
- this.#httpsAgent = options.httpsAgent;
34
33
  this.#metrics = new Metrics();
35
34
  this.#histogram = this.#metrics.histogram({
36
35
  name: 'podium_client_resolver_content_resolve',
@@ -43,7 +42,7 @@ export default class PodletClientContentResolver {
43
42
  buckets: [0.001, 0.01, 0.1, 0.5, 1, 2, 10],
44
43
  });
45
44
 
46
- this.#metrics.on('error', error => {
45
+ this.#metrics.on('error', (error) => {
47
46
  this.#log.error(
48
47
  'Error emitted by metric stream in @podium/client module',
49
48
  error,
@@ -55,207 +54,177 @@ export default class PodletClientContentResolver {
55
54
  return this.#metrics;
56
55
  }
57
56
 
58
- resolve(outgoing) {
59
- return new Promise((resolve, reject) => {
60
- if (outgoing.kill && outgoing.throwable) {
61
- this.#log.warn(
62
- `recursion detected - failed to resolve fetching of podlet ${outgoing.recursions} times - throwing - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
63
- );
64
- reject(
65
- badGateway(
66
- `Recursion detected - failed to resolve fetching of podlet ${outgoing.recursions} times`,
67
- ),
68
- );
69
- return;
70
- }
57
+ async resolve(outgoing) {
58
+ if (outgoing.kill && outgoing.throwable) {
59
+ this.#log.warn(
60
+ `recursion detected - failed to resolve fetching of podlet ${outgoing.recursions} times - throwing - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
61
+ );
62
+ throw badGateway(
63
+ `Recursion detected - failed to resolve fetching of podlet ${outgoing.recursions} times`,
64
+ );
65
+ }
71
66
 
72
- if (outgoing.kill) {
73
- this.#log.warn(
74
- `recursion detected - failed to resolve fetching of podlet ${outgoing.recursions} times - serving fallback - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
75
- );
76
- outgoing.success = true;
77
- outgoing.pushFallback();
78
- resolve(outgoing);
79
- return;
80
- }
67
+ if (outgoing.kill) {
68
+ this.#log.warn(
69
+ `recursion detected - failed to resolve fetching of podlet ${outgoing.recursions} times - serving fallback - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
70
+ );
71
+ outgoing.success = true;
72
+ outgoing.pushFallback();
73
+ return outgoing;
74
+ }
75
+
76
+ if (outgoing.status === 'empty' && outgoing.throwable) {
77
+ this.#log.warn(
78
+ `no manifest available - cannot read content - throwing - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
79
+ );
80
+ throw badGateway(`No manifest available - Cannot read content`);
81
+ }
81
82
 
82
- if (outgoing.status === 'empty' && outgoing.throwable) {
83
- this.#log.warn(
84
- `no manifest available - cannot read content - throwing - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
85
- );
86
- reject(
87
- badGateway(`No manifest available - Cannot read content`),
83
+ if (outgoing.status === 'empty') {
84
+ this.#log.warn(
85
+ `no manifest available - cannot read content - serving fallback - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
86
+ );
87
+ outgoing.success = true;
88
+ outgoing.pushFallback();
89
+ return outgoing;
90
+ }
91
+
92
+ const headers = {
93
+ ...outgoing.reqOptions.headers,
94
+ 'User-Agent': UA_STRING,
95
+ };
96
+
97
+ putils.serializeContext(headers, outgoing.context, outgoing.name);
98
+
99
+ const uri = putils.uriBuilder(
100
+ outgoing.reqOptions.pathname,
101
+ outgoing.contentUri,
102
+ )
103
+
104
+ const reqOptions = {
105
+ rejectUnauthorized: outgoing.rejectUnauthorized,
106
+ bodyTimeout: outgoing.timeout,
107
+ method: 'GET',
108
+ query: outgoing.reqOptions.query,
109
+ headers,
110
+ };
111
+
112
+ if (outgoing.redirectable) {
113
+ reqOptions.follow = false;
114
+ }
115
+
116
+ const timer = this.#histogram.timer({
117
+ labels: {
118
+ podlet: outgoing.name,
119
+ },
120
+ });
121
+
122
+ this.#log.debug(
123
+ `start reading content from remote resource - manifest version is ${outgoing.manifest.version} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
124
+ );
125
+
126
+ try {
127
+ const {
128
+ statusCode,
129
+ headers: hdrs,
130
+ body,
131
+ } = await this.#http.request(uri, reqOptions);
132
+
133
+ // Remote responds but with an http error code
134
+ const resError = statusCode >= 400;
135
+ if (resError && outgoing.throwable) {
136
+ timer({
137
+ labels: {
138
+ status: 'failure',
139
+ },
140
+ });
141
+
142
+ this.#log.debug(
143
+ `remote resource responded with non 200 http status code for content - code: ${statusCode} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
88
144
  );
89
- return;
145
+
146
+ const errorMessage = `Could not read content. Resource responded with ${statusCode} on ${outgoing.contentUri}`;
147
+
148
+ const errorOptions = {
149
+ statusCode,
150
+ decorate: {
151
+ statusCode,
152
+ },
153
+ };
154
+
155
+ throw new Boom(errorMessage, errorOptions);
90
156
  }
157
+ if (resError) {
158
+ timer({
159
+ labels: {
160
+ status: 'failure',
161
+ },
162
+ });
91
163
 
92
- if (outgoing.status === 'empty') {
93
164
  this.#log.warn(
94
- `no manifest available - cannot read content - serving fallback - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
165
+ `remote resource responded with non 200 http status code for content - code: ${statusCode} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
95
166
  );
96
167
  outgoing.success = true;
97
168
  outgoing.pushFallback();
98
- resolve(outgoing);
99
- return;
100
- }
101
-
102
- const headers = {
103
- ...outgoing.reqOptions.headers,
104
- 'User-Agent': UA_STRING,
105
- };
106
-
107
- putils.serializeContext(headers, outgoing.context, outgoing.name);
108
-
109
- const reqOptions = {
110
- rejectUnauthorized: outgoing.rejectUnauthorized,
111
- timeout: outgoing.timeout,
112
- method: 'GET',
113
- agent: outgoing.contentUri.startsWith('https://')
114
- ? this.#httpsAgent
115
- : this.#httpAgent,
116
- uri: putils.uriBuilder(
117
- outgoing.reqOptions.pathname,
118
- outgoing.contentUri,
119
- ),
120
- qs: outgoing.reqOptions.query,
121
- headers,
122
- };
123
-
124
- if (outgoing.redirectable) {
125
- reqOptions.followRedirect = false;
169
+ return outgoing;
126
170
  }
127
171
 
128
- const timer = this.#histogram.timer({
129
- labels: {
130
- podlet: outgoing.name,
131
- },
132
- });
172
+ const contentVersion = utils.isHeaderDefined(hdrs, 'podlet-version')
173
+ ? hdrs['podlet-version']
174
+ : undefined;
133
175
 
134
176
  this.#log.debug(
135
- `start reading content from remote resource - manifest version is ${outgoing.manifest.version} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
177
+ `got head response from remote resource for content - header version is ${hdrs['podlet-version']} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
136
178
  );
137
179
 
138
- const r = request(reqOptions);
139
-
140
- r.on('response', response => {
141
- // Remote responds but with an http error code
142
- const resError = response.statusCode >= 400;
143
- if (resError && outgoing.throwable) {
144
- timer({
145
- labels: {
146
- status: 'failure',
147
- },
148
- });
149
-
150
- this.#log.debug(
151
- `remote resource responded with non 200 http status code for content - code: ${response.statusCode} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
152
- );
153
-
154
- const errorMessage = `Could not read content. Resource responded with ${response.statusCode} on ${outgoing.contentUri}`;
155
-
156
- const errorOptions = {
157
- statusCode: response.statusCode,
158
- decorate: {
159
- statusCode: response.statusCode,
160
- },
161
- };
162
-
163
- reject(new Boom(errorMessage, errorOptions));
164
- return;
165
- }
166
- if (resError) {
167
- timer({
168
- labels: {
169
- status: 'failure',
170
- },
171
- });
172
-
173
- this.#log.warn(
174
- `remote resource responded with non 200 http status code for content - code: ${response.statusCode} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
175
- );
176
- outgoing.success = true;
177
- outgoing.pushFallback();
178
- resolve(outgoing);
179
- return;
180
- }
181
-
182
- const contentVersion = utils.isHeaderDefined(
183
- response.headers,
184
- 'podlet-version',
185
- )
186
- ? response.headers['podlet-version']
187
- : undefined;
180
+ if (
181
+ contentVersion !== outgoing.manifest.version &&
182
+ contentVersion !== undefined
183
+ ) {
184
+ timer({
185
+ labels: {
186
+ status: 'success',
187
+ },
188
+ });
188
189
 
189
- this.#log.debug(
190
- `got head response from remote resource for content - header version is ${response.headers['podlet-version']} - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
190
+ this.#log.info(
191
+ `podlet version number in header differs from cached version number - aborting request to remote resource for content - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
191
192
  );
193
+ // TODO r.abort();
194
+ outgoing.status = 'stale';
195
+ return outgoing;
196
+ }
192
197
 
193
- if (
194
- contentVersion !== outgoing.manifest.version &&
195
- contentVersion !== undefined
196
- ) {
197
- timer({
198
- labels: {
199
- status: 'success',
200
- },
201
- });
202
-
203
- this.#log.info(
204
- `podlet version number in header differs from cached version number - aborting request to remote resource for content - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
205
- );
206
- r.abort();
207
- outgoing.status = 'stale';
208
- return;
209
- }
210
-
211
- outgoing.success = true;
212
- outgoing.headers = response.headers;
213
-
214
- if (outgoing.redirectable && response.statusCode >= 300) {
215
- outgoing.redirect = {
216
- statusCode: response.statusCode,
217
- location: response.headers && response.headers.location,
218
- };
219
- }
198
+ outgoing.success = true;
199
+ outgoing.headers = hdrs;
220
200
 
221
- outgoing.emit(
222
- 'beforeStream',
223
- new Response({
224
- headers: outgoing.headers,
225
- js: outgoing.manifest.js,
226
- css: outgoing.manifest.css,
227
- redirect: outgoing.redirect,
228
- }),
229
- );
201
+ if (outgoing.redirectable && statusCode >= 300) {
202
+ outgoing.redirect = {
203
+ statusCode,
204
+ location: hdrs && hdrs.location,
205
+ };
206
+ }
230
207
 
231
- pipeline([r, outgoing], (err) => {
232
- if (err) {
233
- this.#log.warn('error while piping content stream', err);
234
- }
235
- });
236
- });
208
+ outgoing.emit(
209
+ 'beforeStream',
210
+ new Response({
211
+ headers: outgoing.headers,
212
+ js: outgoing.manifest.js,
213
+ css: outgoing.manifest.css,
214
+ redirect: outgoing.redirect,
215
+ }),
216
+ );
237
217
 
238
- r.on('error', error => {
239
- // Network error
240
- if (outgoing.throwable) {
241
- timer({
242
- labels: {
243
- status: 'failure',
244
- },
245
- });
246
-
247
- this.#log.warn(
248
- `could not create network connection to remote resource when trying to request content - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
249
- );
250
- reject(
251
- badGateway(
252
- `Error reading content at ${outgoing.contentUri}`,
253
- error,
254
- ),
255
- );
256
- return;
218
+ pipeline([body, outgoing], (err) => {
219
+ if (err) {
220
+ this.#log.warn('error while piping content stream', err);
257
221
  }
222
+ });
223
+ } catch (error) {
224
+ if (error.isBoom) throw error;
258
225
 
226
+ // Network error
227
+ if (outgoing.throwable) {
259
228
  timer({
260
229
  labels: {
261
230
  status: 'failure',
@@ -265,30 +234,43 @@ export default class PodletClientContentResolver {
265
234
  this.#log.warn(
266
235
  `could not create network connection to remote resource when trying to request content - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
267
236
  );
237
+ throw badGateway(
238
+ `Error reading content at ${outgoing.contentUri}`,
239
+ error,
240
+ );
241
+ }
268
242
 
269
- outgoing.success = true;
270
-
271
- outgoing.pushFallback();
272
- resolve(outgoing);
243
+ timer({
244
+ labels: {
245
+ status: 'failure',
246
+ },
273
247
  });
274
248
 
275
- r.on('end', () => {
276
- timer({
277
- labels: {
278
- status: 'success',
279
- },
280
- });
249
+ this.#log.warn(
250
+ `could not create network connection to remote resource when trying to request content - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
251
+ );
281
252
 
282
- this.#log.debug(
283
- `successfully read content from remote resource - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
284
- );
253
+ outgoing.success = true;
285
254
 
286
- resolve(outgoing);
287
- });
255
+ outgoing.pushFallback();
256
+
257
+ return outgoing;
258
+ }
259
+
260
+ timer({
261
+ labels: {
262
+ status: 'success',
263
+ },
288
264
  });
265
+
266
+ this.#log.debug(
267
+ `successfully read content from remote resource - resource: ${outgoing.name} - url: ${outgoing.contentUri}`,
268
+ );
269
+
270
+ return outgoing;
289
271
  }
290
272
 
291
273
  get [Symbol.toStringTag]() {
292
274
  return 'PodletClientContentResolver';
293
275
  }
294
- };
276
+ }
@@ -1,16 +1,18 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
 
3
- import request from 'request';
4
3
  import abslog from 'abslog';
5
4
  import Metrics from '@metrics/client';
6
-
7
5
  import { join, dirname } from 'path';
8
6
  import { fileURLToPath } from 'url';
9
7
  import fs from 'fs';
8
+ import HTTP from './http.js';
10
9
 
11
10
  const currentDirectory = dirname(fileURLToPath(import.meta.url));
12
11
 
13
- const pkgJson = fs.readFileSync(join(currentDirectory, '../package.json'), 'utf-8');
12
+ const pkgJson = fs.readFileSync(
13
+ join(currentDirectory, '../package.json'),
14
+ 'utf-8',
15
+ );
14
16
  const pkg = JSON.parse(pkgJson);
15
17
 
16
18
  const UA_STRING = `${pkg.name} ${pkg.version}`;
@@ -19,14 +21,12 @@ export default class PodletClientFallbackResolver {
19
21
  #log;
20
22
  #metrics;
21
23
  #histogram;
22
- #httpAgent;
23
- #httpsAgent;
24
+ #http;
24
25
  constructor(options = {}) {
26
+ this.#http = options.http || new HTTP();
25
27
  const name = options.clientName;
26
28
  this.#log = abslog(options.logger);
27
29
  this.#metrics = new Metrics();
28
- this.#httpAgent = options.httpAgent;
29
- this.#httpsAgent = options.httpsAgent;
30
30
  this.#histogram = this.#metrics.histogram({
31
31
  name: 'podium_client_resolver_fallback_resolve',
32
32
  description: 'Time taken for success/failure of fallback request',
@@ -38,7 +38,7 @@ export default class PodletClientFallbackResolver {
38
38
  buckets: [0.001, 0.01, 0.1, 0.5, 1, 2, 10],
39
39
  });
40
40
 
41
- this.#metrics.on('error', error => {
41
+ this.#metrics.on('error', (error) => {
42
42
  this.#log.error(
43
43
  'Error emitted by metric stream in @podium/client module',
44
44
  error,
@@ -50,113 +50,103 @@ export default class PodletClientFallbackResolver {
50
50
  return this.#metrics;
51
51
  }
52
52
 
53
- resolve(outgoing) {
54
- return new Promise(resolve => {
55
- if (outgoing.status === 'cached') {
56
- resolve(outgoing);
57
- return;
58
- }
53
+ async resolve(outgoing) {
54
+ if (outgoing.status === 'cached') {
55
+ return outgoing;
56
+ }
57
+
58
+ // Manifest has no fallback, fetching of manifest likely failed.
59
+ // Its not possible to fetch anything
60
+ // Do not set fallback so we can serve any previous fallback we might have
61
+ if (outgoing.manifest.fallback === undefined) {
62
+ return outgoing;
63
+ }
64
+
65
+ // If manifest fallback is empty, there is no fallback content to fetch
66
+ // Set fallback to empty string
67
+ if (outgoing.manifest.fallback === '') {
68
+ this.#log.debug(
69
+ `no fallback defined in manifest - resource: ${outgoing.name}`,
70
+ );
71
+ outgoing.fallback = '';
72
+ return outgoing;
73
+ }
74
+
75
+ // The manifest fallback holds a URI, fetch its content
76
+ const headers = {
77
+ 'User-Agent': UA_STRING,
78
+ };
79
+
80
+ const reqOptions = {
81
+ rejectUnauthorized: outgoing.rejectUnauthorized,
82
+ timeout: outgoing.timeout,
83
+ method: 'GET',
84
+ headers,
85
+ };
86
+
87
+ const timer = this.#histogram.timer({
88
+ labels: {
89
+ podlet: outgoing.name,
90
+ },
91
+ });
59
92
 
60
- // Manifest has no fallback, fetching of manifest likely failed.
61
- // Its not possible to fetch anything
62
- // Do not set fallback so we can serve any previous fallback we might have
63
- if (outgoing.manifest.fallback === undefined) {
64
- resolve(outgoing);
65
- return;
66
- }
93
+ try {
94
+ this.#log.debug(
95
+ `start reading fallback content from remote resource - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
96
+ );
97
+ const { statusCode, body } = await this.#http.request(
98
+ outgoing.fallbackUri,
99
+ reqOptions,
100
+ );
101
+
102
+ // Remote responds but with an http error code
103
+ const resError = statusCode !== 200;
104
+ if (resError) {
105
+ timer({
106
+ labels: {
107
+ status: 'failure',
108
+ },
109
+ });
67
110
 
68
- // If manifest fallback is empty, there is no fallback content to fetch
69
- // Set fallback to empty string
70
- if (outgoing.manifest.fallback === '') {
71
- this.#log.debug(
72
- `no fallback defined in manifest - resource: ${outgoing.name}`,
111
+ this.#log.warn(
112
+ `remote resource responded with non 200 http status code for fallback content - code: ${statusCode} - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
73
113
  );
114
+
74
115
  outgoing.fallback = '';
75
- resolve(outgoing);
76
- return;
116
+ return outgoing;
77
117
  }
78
118
 
79
- // The manifest fallback holds a URI, fetch its content
80
- const headers = {
81
- 'User-Agent': UA_STRING,
82
- };
83
-
84
- const reqOptions = {
85
- rejectUnauthorized: outgoing.rejectUnauthorized,
86
- timeout: outgoing.timeout,
87
- method: 'GET',
88
- agent: outgoing.fallbackUri.startsWith('https://')
89
- ? this.#httpsAgent
90
- : this.#httpAgent,
91
- uri: outgoing.fallbackUri,
92
- headers,
93
- };
94
-
95
- const timer = this.#histogram.timer({
119
+ // Response is OK. Store response body as fallback html for caching
120
+ timer({
96
121
  labels: {
97
- podlet: outgoing.name,
122
+ status: 'success',
98
123
  },
99
124
  });
100
125
 
101
- request(reqOptions, (error, res, body) => {
102
- this.#log.debug(
103
- `start reading fallback content from remote resource - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
104
- );
126
+ // Set fallback to the fetched content
127
+ outgoing.fallback = await body.text();
105
128
 
106
- // Network error
107
- if (error) {
108
- timer({
109
- labels: {
110
- status: 'failure',
111
- },
112
- });
113
-
114
- this.#log.warn(
115
- `could not create network connection to remote resource for fallback content - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
116
- );
117
-
118
- outgoing.fallback = '';
119
- resolve(outgoing);
120
- return;
121
- }
122
-
123
- // Remote responds but with an http error code
124
- const resError = res.statusCode !== 200;
125
- if (resError) {
126
- timer({
127
- labels: {
128
- status: 'failure',
129
- },
130
- });
131
-
132
- this.#log.warn(
133
- `remote resource responded with non 200 http status code for fallback content - code: ${res.statusCode} - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
134
- );
135
-
136
- outgoing.fallback = '';
137
- resolve(outgoing);
138
- return;
139
- }
140
-
141
- // Response is OK. Store response body as fallback html for caching
142
- timer({
143
- labels: {
144
- status: 'success',
145
- },
146
- });
129
+ this.#log.debug(
130
+ `successfully read fallback from remote resource - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
131
+ );
132
+ return outgoing;
133
+ } catch (error) {
134
+ timer({
135
+ labels: {
136
+ status: 'failure',
137
+ },
138
+ });
147
139
 
148
- // Set fallback to the fetched content
149
- outgoing.fallback = body;
140
+ this.#log.warn(
141
+ `could not create network connection to remote resource for fallback content - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
142
+ );
150
143
 
151
- this.#log.debug(
152
- `successfully read fallback from remote resource - resource: ${outgoing.name} - url: ${outgoing.fallbackUri}`,
153
- );
154
- resolve(outgoing);
155
- });
156
- });
144
+ outgoing.fallback = '';
145
+ return outgoing;
146
+ }
157
147
  }
158
148
 
159
149
  get [Symbol.toStringTag]() {
160
150
  return 'PodletClientFallbackResolver';
161
151
  }
162
- };
152
+ }
package/lib/resolver.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import Metrics from '@metrics/client';
2
2
  import abslog from 'abslog';
3
3
  import assert from 'assert';
4
+ import HTTP from './http.js';
4
5
  import Manifest from './resolver.manifest.js';
5
6
  import Fallback from './resolver.fallback.js';
6
7
  import Content from './resolver.content.js';
@@ -19,10 +20,11 @@ export default class PodletClientResolver {
19
20
  );
20
21
 
21
22
  const log = abslog(options.logger);
23
+ const http = new HTTP();
22
24
  this.#cache = new Cache(registry, options);
23
- this.#manifest = new Manifest(options);
24
- this.#fallback = new Fallback(options);
25
- this.#content = new Content(options);
25
+ this.#manifest = new Manifest({ ...options, http });
26
+ this.#fallback = new Fallback({ ...options, http });
27
+ this.#content = new Content({ ...options, http });
26
28
  this.#metrics = new Metrics();
27
29
 
28
30
  this.#metrics.on('error', error => {
@@ -1,19 +1,21 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
 
3
3
  import CachePolicy from 'http-cache-semantics';
4
- import {manifest as validateManifest } from'@podium/schemas';
5
- import request from 'request';
4
+ import { manifest as validateManifest } from '@podium/schemas';
6
5
  import Metrics from '@metrics/client';
7
6
  import abslog from 'abslog';
8
7
  import * as utils from '@podium/utils';
9
-
10
8
  import { join, dirname } from 'path';
11
9
  import { fileURLToPath } from 'url';
12
10
  import fs from 'fs';
11
+ import HTTP from './http.js';
13
12
 
14
13
  const currentDirectory = dirname(fileURLToPath(import.meta.url));
15
14
 
16
- const pkgJson = fs.readFileSync(join(currentDirectory, '../package.json'), 'utf-8');
15
+ const pkgJson = fs.readFileSync(
16
+ join(currentDirectory, '../package.json'),
17
+ 'utf-8',
18
+ );
17
19
  const pkg = JSON.parse(pkgJson);
18
20
 
19
21
  const UA_STRING = `${pkg.name} ${pkg.version}`;
@@ -21,15 +23,13 @@ const UA_STRING = `${pkg.name} ${pkg.version}`;
21
23
  export default class PodletClientManifestResolver {
22
24
  #log;
23
25
  #metrics;
24
- #httpAgent;
25
- #httpsAgent;
26
26
  #histogram;
27
+ #http;
27
28
  constructor(options = {}) {
29
+ this.#http = options.http || new HTTP();
28
30
  const name = options.clientName;
29
31
  this.#log = abslog(options.logger);
30
32
  this.#metrics = new Metrics();
31
- this.#httpAgent = options.httpAgent;
32
- this.#httpsAgent = options.httpsAgent;
33
33
  this.#histogram = this.#metrics.histogram({
34
34
  name: 'podium_client_resolver_manifest_resolve',
35
35
  description: 'Time taken for success/failure of manifest request',
@@ -41,7 +41,7 @@ export default class PodletClientManifestResolver {
41
41
  buckets: [0.001, 0.01, 0.1, 0.5, 1, 2, 10],
42
42
  });
43
43
 
44
- this.#metrics.on('error', error => {
44
+ this.#metrics.on('error', (error) => {
45
45
  this.#log.error(
46
46
  'Error emitted by metric stream in @podium/client module',
47
47
  error,
@@ -53,161 +53,154 @@ export default class PodletClientManifestResolver {
53
53
  return this.#metrics;
54
54
  }
55
55
 
56
- resolve(outgoing) {
57
- return new Promise(resolve => {
58
- if (outgoing.status === 'cached') {
59
- resolve(outgoing);
60
- return;
61
- }
56
+ async resolve(outgoing) {
57
+ if (outgoing.status === 'cached') {
58
+ return outgoing;
59
+ }
62
60
 
63
- const headers = {
64
- 'User-Agent': UA_STRING,
65
- };
61
+ const headers = {
62
+ 'User-Agent': UA_STRING,
63
+ };
66
64
 
67
- const reqOptions = {
68
- rejectUnauthorized: outgoing.rejectUnauthorized,
69
- timeout: outgoing.timeout,
70
- method: 'GET',
71
- agent: outgoing.manifestUri.startsWith('https://')
72
- ? this.#httpsAgent
73
- : this.#httpAgent,
74
- json: true,
75
- uri: outgoing.manifestUri,
76
- headers,
77
- };
65
+ const reqOptions = {
66
+ rejectUnauthorized: outgoing.rejectUnauthorized,
67
+ timeout: outgoing.timeout,
68
+ method: 'GET',
69
+ json: true,
70
+ headers,
71
+ };
78
72
 
79
- const timer = this.#histogram.timer({
80
- labels: {
81
- podlet: outgoing.name,
82
- },
83
- });
73
+ const timer = this.#histogram.timer({
74
+ labels: {
75
+ podlet: outgoing.name,
76
+ },
77
+ });
84
78
 
85
- request(reqOptions, (error, res, body) => {
86
- this.#log.debug(
87
- `start reading manifest from remote resource - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
88
- );
79
+ this.#log.debug(
80
+ `start reading manifest from remote resource - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
81
+ );
89
82
 
90
- // Network error or JSON parsing error
91
- if (error) {
92
- timer({
93
- labels: {
94
- status: 'failure',
95
- },
96
- });
97
-
98
- this.#log.warn(
99
- `could not create network connection to remote manifest - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
100
- );
101
- resolve(outgoing);
102
- return;
103
- }
104
-
105
- // Remote responds but with an http error code
106
- const resError = res.statusCode !== 200;
107
- if (resError) {
108
- timer({
109
- labels: {
110
- status: 'failure',
111
- },
112
- });
113
-
114
- this.#log.warn(
115
- `remote resource responded with non 200 http status code for manifest - code: ${res.statusCode} - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
116
- );
117
- resolve(outgoing);
118
- return;
119
- }
120
-
121
- const manifest = validateManifest(body);
122
-
123
- // Manifest validation error
124
- if (manifest.error) {
125
- timer({
126
- labels: {
127
- status: 'failure',
128
- },
129
- });
130
-
131
- this.#log.warn(
132
- `could not parse manifest - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
133
- );
134
- resolve(outgoing);
135
- return;
136
- }
137
-
138
- // Manifest is valid, calculate maxAge for caching and continue
83
+ try {
84
+ const {
85
+ statusCode,
86
+ headers: hdrs,
87
+ body,
88
+ } = await this.#http.request(outgoing.manifestUri, reqOptions);
89
+
90
+ // Remote responds but with an http error code
91
+ const resError = statusCode !== 200;
92
+ if (resError) {
139
93
  timer({
140
94
  labels: {
141
- status: 'success',
95
+ status: 'failure',
142
96
  },
143
97
  });
144
98
 
145
- const resValues = {
146
- status: res.statusCode,
147
- headers: res.headers,
148
- };
149
-
150
- const cachePolicy = new CachePolicy(reqOptions, resValues, {
151
- ignoreCargoCult: true,
152
- });
153
- const maxAge = cachePolicy.timeToLive();
154
- if (maxAge !== 0) {
155
- this.#log.debug(
156
- `remote resource has cache header which yelds a max age of ${maxAge}ms, using this as cache ttl - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
157
- );
158
- outgoing.maxAge = maxAge;
159
- }
160
-
161
- // Build absolute content and fallback URIs
162
- if (manifest.value.fallback !== '') {
163
- manifest.value.fallback = utils.uriRelativeToAbsolute(
164
- manifest.value.fallback,
165
- outgoing.manifestUri,
166
- );
167
- }
168
-
169
- manifest.value.content = utils.uriRelativeToAbsolute(
170
- manifest.value.content,
171
- outgoing.manifestUri,
99
+ this.#log.warn(
100
+ `remote resource responded with non 200 http status code for manifest - code: ${statusCode} - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
172
101
  );
102
+ return outgoing;
103
+ }
173
104
 
174
- // Construct css and js objects with absolute URIs
175
- manifest.value.css = manifest.value.css.map(obj => {
176
- obj.value = utils.uriRelativeToAbsolute(
177
- obj.value,
178
- outgoing.manifestUri,
179
- );
180
- return new utils.AssetCss(obj);
181
- });
105
+ const manifest = validateManifest(await body.json());
182
106
 
183
- manifest.value.js = manifest.value.js.map(obj => {
184
- obj.value = utils.uriRelativeToAbsolute(
185
- obj.value,
186
- outgoing.manifestUri,
187
- );
188
- return new utils.AssetJs(obj);
107
+ // Manifest validation error
108
+ if (manifest.error) {
109
+ timer({
110
+ labels: {
111
+ status: 'failure',
112
+ },
189
113
  });
190
114
 
191
- // Build absolute proxy URIs
192
- Object.keys(manifest.value.proxy).forEach(key => {
193
- manifest.value.proxy[key] = utils.uriRelativeToAbsolute(
194
- manifest.value.proxy[key],
195
- outgoing.manifestUri,
196
- );
197
- });
115
+ this.#log.warn(
116
+ `could not parse manifest - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
117
+ );
118
+ return outgoing;
119
+ }
198
120
 
199
- outgoing.manifest = manifest.value;
200
- outgoing.status = 'fresh';
121
+ // Manifest is valid, calculate maxAge for caching and continue
122
+ timer({
123
+ labels: {
124
+ status: 'success',
125
+ },
126
+ });
127
+
128
+ const resValues = {
129
+ status: statusCode,
130
+ headers: hdrs,
131
+ };
201
132
 
133
+ const cachePolicy = new CachePolicy(reqOptions, resValues, {
134
+ ignoreCargoCult: true,
135
+ });
136
+ const maxAge = cachePolicy.timeToLive();
137
+ if (maxAge !== 0) {
202
138
  this.#log.debug(
203
- `successfully read manifest from remote resource - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
139
+ `remote resource has cache header which yelds a max age of ${maxAge}ms, using this as cache ttl - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
140
+ );
141
+ outgoing.maxAge = maxAge;
142
+ }
143
+
144
+ // Build absolute content and fallback URIs
145
+ if (manifest.value.fallback !== '') {
146
+ manifest.value.fallback = utils.uriRelativeToAbsolute(
147
+ manifest.value.fallback,
148
+ outgoing.manifestUri,
149
+ );
150
+ }
151
+
152
+ manifest.value.content = utils.uriRelativeToAbsolute(
153
+ manifest.value.content,
154
+ outgoing.manifestUri,
155
+ );
156
+
157
+ // Construct css and js objects with absolute URIs
158
+ manifest.value.css = manifest.value.css.map((obj) => {
159
+ obj.value = utils.uriRelativeToAbsolute(
160
+ obj.value,
161
+ outgoing.manifestUri,
204
162
  );
205
- resolve(outgoing);
163
+ return new utils.AssetCss(obj);
206
164
  });
207
- });
165
+
166
+ manifest.value.js = manifest.value.js.map((obj) => {
167
+ obj.value = utils.uriRelativeToAbsolute(
168
+ obj.value,
169
+ outgoing.manifestUri,
170
+ );
171
+ return new utils.AssetJs(obj);
172
+ });
173
+
174
+ // Build absolute proxy URIs
175
+ Object.keys(manifest.value.proxy).forEach((key) => {
176
+ manifest.value.proxy[key] = utils.uriRelativeToAbsolute(
177
+ manifest.value.proxy[key],
178
+ outgoing.manifestUri,
179
+ );
180
+ });
181
+
182
+ outgoing.manifest = manifest.value;
183
+ outgoing.status = 'fresh';
184
+
185
+ this.#log.debug(
186
+ `successfully read manifest from remote resource - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
187
+ );
188
+ return outgoing;
189
+ } catch (error) {
190
+ timer({
191
+ labels: {
192
+ status: 'failure',
193
+ },
194
+ });
195
+
196
+ this.#log.warn(
197
+ `could not create network connection to remote manifest - resource: ${outgoing.name} - url: ${outgoing.manifestUri}`,
198
+ );
199
+ return outgoing;
200
+ }
208
201
  }
209
202
 
210
203
  get [Symbol.toStringTag]() {
211
204
  return 'PodletClientManifestResolver';
212
205
  }
213
- };
206
+ }
package/lib/resource.js CHANGED
@@ -1,7 +1,6 @@
1
1
  /* eslint-disable no-param-reassign */
2
2
 
3
3
  import Metrics from '@metrics/client';
4
- import stream from 'stream';
5
4
  import abslog from 'abslog';
6
5
  import assert from 'assert';
7
6
 
@@ -63,22 +62,16 @@ export default class PodiumClientResource {
63
62
 
64
63
  this.#state.setInitializingState();
65
64
 
66
- const chunks = [];
67
- const to = new stream.Writable({
68
- write: (chunk, encoding, next) => {
69
- chunks.push(chunk);
70
- next();
71
- },
72
- });
73
-
74
- stream.pipeline([outgoing, to], () => {
75
- // noop
76
- });
77
-
78
65
  const { manifest, headers, redirect } = await this.#resolver.resolve(
79
66
  outgoing,
80
67
  );
81
68
 
69
+ const chunks = [];
70
+ // eslint-disable-next-line no-restricted-syntax
71
+ for await (const chunk of outgoing) {
72
+ chunks.push(chunk)
73
+ }
74
+
82
75
  const content = !outgoing.redirect
83
76
  ? Buffer.concat(chunks).toString()
84
77
  : '';
package/lib/response.js CHANGED
@@ -60,15 +60,11 @@ export default class PodiumClientResponse {
60
60
 
61
61
  [inspect]() {
62
62
  return {
63
- referrerpolicy: this.referrerpolicy,
64
- crossorigin: this.crossorigin,
65
- integrity: this.integrity,
66
- nomodule: this.nomodule,
67
- value: this.value,
68
- async: this.async,
69
- defer: this.defer,
70
- type: this.type,
71
- data: this.data,
63
+ redirect: this.redirect,
64
+ content: this.content,
65
+ headers: this.headers,
66
+ css: this.css,
67
+ js: this.js,
72
68
  };
73
69
  }
74
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@podium/client",
3
- "version": "5.0.0-next.10",
3
+ "version": "5.0.0-next.12",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -42,8 +42,8 @@
42
42
  "abslog": "2.4.0",
43
43
  "http-cache-semantics": "^4.0.3",
44
44
  "lodash.clonedeep": "^4.5.0",
45
- "request": "^2.88.0",
46
- "ttl-mem-cache": "4.1.0"
45
+ "ttl-mem-cache": "4.1.0",
46
+ "undici": "^5.10.0"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@podium/test-utils": "2.5.2",