@tramvai/module-http-client 6.63.1 → 6.65.0

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/tests.js CHANGED
@@ -2,34 +2,32 @@
2
2
 
3
3
  Object.defineProperty(exports, '__esModule', { value: true });
4
4
 
5
- var fetch = require('node-fetch');
6
5
  var testHelpers = require('@tramvai/test-helpers');
7
6
  var testMocks = require('@tramvai/test-mocks');
8
- var dippy = require('@tinkoff/dippy');
9
7
  var core = require('@tramvai/core');
10
8
  var tokensHttpClient = require('@tramvai/tokens-http-client');
11
- var tokensCommon = require('@tramvai/tokens-common');
12
- var isNil = require('@tinkoff/utils/is/nil');
13
- var compose = require('@tinkoff/utils/function/compose');
14
- var tinkoffRequestHttpClientAdapter = require('@tramvai/tinkoff-request-http-client-adapter');
15
- var identity = require('@tinkoff/utils/function/identity');
16
- var flatten = require('@tinkoff/utils/array/flatten');
17
- var pick = require('@tinkoff/utils/object/pick');
18
9
  var tokensServer = require('@tramvai/tokens-server');
19
10
  var find = require('@tinkoff/utils/array/find');
11
+ var flatten = require('@tinkoff/utils/array/flatten');
20
12
  var httpClient = require('@tramvai/http-client');
13
+ var dippy = require('@tinkoff/dippy');
21
14
  var papi = require('@tramvai/papi');
22
15
  var tokensServerPrivate = require('@tramvai/tokens-server-private');
16
+ var identity = require('@tinkoff/utils/function/identity');
17
+ var pick = require('@tinkoff/utils/object/pick');
18
+ var tokensCommon = require('@tramvai/tokens-common');
19
+ var isNil = require('@tinkoff/utils/is/nil');
20
+ var compose = require('@tinkoff/utils/function/compose');
21
+ var tinkoffRequestHttpClientAdapter = require('@tramvai/tinkoff-request-http-client-adapter');
23
22
 
24
23
  function _interopDefaultLegacy (e) { return e && typeof e === 'object' && 'default' in e ? e : { 'default': e }; }
25
24
 
26
- var fetch__default = /*#__PURE__*/_interopDefaultLegacy(fetch);
27
- var isNil__default = /*#__PURE__*/_interopDefaultLegacy(isNil);
28
- var compose__default = /*#__PURE__*/_interopDefaultLegacy(compose);
29
- var identity__default = /*#__PURE__*/_interopDefaultLegacy(identity);
25
+ var find__default = /*#__PURE__*/_interopDefaultLegacy(find);
30
26
  var flatten__default = /*#__PURE__*/_interopDefaultLegacy(flatten);
27
+ var identity__default = /*#__PURE__*/_interopDefaultLegacy(identity);
31
28
  var pick__default = /*#__PURE__*/_interopDefaultLegacy(pick);
32
- var find__default = /*#__PURE__*/_interopDefaultLegacy(find);
29
+ var isNil__default = /*#__PURE__*/_interopDefaultLegacy(isNil);
30
+ var compose__default = /*#__PURE__*/_interopDefaultLegacy(compose);
33
31
 
34
32
  const fillHeaderIp = ({ requestManager, }) => {
35
33
  if (!requestManager) {
@@ -68,6 +66,57 @@ const comparePathWithPattern = (path, pattern) => new RegExp(`^${pattern.replace
68
66
 
69
67
  const getPathParams = (path, pattern) => path.match(new RegExp(`^${pattern.replace(/:(\w+)/g, '(?<$1>[^\\/]+)')}$`))?.groups ?? {};
70
68
 
69
+ class PapiService extends httpClient.BaseHttpClient {
70
+ constructor({ papi, di }) {
71
+ super();
72
+ this.papi = flatten__default["default"](papi || []);
73
+ this.di = di;
74
+ }
75
+ async request({ path, query, body, headers, }) {
76
+ const pathWithLeadingSlash = path?.startsWith('/') ? path : `/${path}`;
77
+ const papiRoute = find__default["default"]((papi$1) => comparePathWithPattern(pathWithLeadingSlash, papi.getPapiParameters(papi$1).path), this.papi ?? []);
78
+ if (!papiRoute) {
79
+ throw new Error(`papi handler '${path}' not found`);
80
+ }
81
+ const req = {
82
+ headers: { ...headers, host: 'localhost' },
83
+ cookies: {},
84
+ query,
85
+ body,
86
+ params: getPathParams(pathWithLeadingSlash, papi.getPapiParameters(papiRoute).path),
87
+ };
88
+ const res = {};
89
+ const childDi = dippy.createChildContainer(this.di, [
90
+ {
91
+ provide: tokensServerPrivate.FASTIFY_REQUEST,
92
+ useValue: req,
93
+ },
94
+ {
95
+ provide: tokensServerPrivate.FASTIFY_RESPONSE,
96
+ useValue: res,
97
+ },
98
+ ]);
99
+ const papiExecutor = childDi.get(tokensServerPrivate.PAPI_EXECUTOR);
100
+ const payload = await papiExecutor(papiRoute);
101
+ return { payload, status: 200, headers: {} };
102
+ }
103
+ }
104
+
105
+ const PapiClientModule = /* @__PURE__ */ core.Module({
106
+ providers: [
107
+ core.provide({
108
+ provide: tokensHttpClient.PAPI_SERVICE,
109
+ scope: core.Scope.SINGLETON,
110
+ useClass: PapiService,
111
+ deps: {
112
+ di: core.DI_TOKEN,
113
+ papi: { token: tokensServer.SERVER_MODULE_PAPI_PUBLIC_ROUTE, optional: true },
114
+ },
115
+ }),
116
+ ],
117
+ })(class PapiClientModule {
118
+ });
119
+
71
120
  const createUserAgent = ({ appInfo, envManager, }) => {
72
121
  const { appName } = appInfo;
73
122
  const appVersion = envManager.get('APP_VERSION');
@@ -84,7 +133,7 @@ const createUserAgent = ({ appInfo, envManager, }) => {
84
133
  };
85
134
 
86
135
  /**
87
- * `node-fetch` sends "User-Agent: node-fetch" header
136
+ * nodejs sends "User-Agent: node" header
88
137
  * on the server by default. For logging purpose, we are
89
138
  * replacing "User-Agent" value with custom one, containing
90
139
  * both application name and version.
@@ -128,7 +177,6 @@ const httpClientFactory = ({ logger, envManager, appInfo, requestManager, header
128
177
  failureThreshold: 75,
129
178
  minimumFailureCount: 10,
130
179
  },
131
- // TODO: remove any after [resolving](https://github.com/southpolesteve/node-abort-controller/issues/31)
132
180
  signal: commandLineExecutionContext?.()?.abortSignal,
133
181
  ...environmentDependentOptions,
134
182
  }, defaultOptions ? tinkoffRequestHttpClientAdapter.mergeOptions(defaultOptions, interceptors) : interceptors), options);
@@ -162,164 +210,156 @@ const httpClientFactory = ({ logger, envManager, appInfo, requestManager, header
162
210
  };
163
211
  };
164
212
 
165
- class PapiService extends httpClient.BaseHttpClient {
166
- constructor({ papi, di }) {
167
- super();
168
- this.papi = flatten__default["default"](papi || []);
169
- this.di = di;
170
- }
171
- async request({ path, query, body, headers, }) {
172
- const pathWithLeadingSlash = path?.startsWith('/') ? path : `/${path}`;
173
- const papiRoute = find__default["default"]((papi$1) => comparePathWithPattern(pathWithLeadingSlash, papi.getPapiParameters(papi$1).path), this.papi ?? []);
174
- if (!papiRoute) {
175
- throw new Error(`papi handler '${path}' not found`);
176
- }
177
- const req = {
178
- headers: { ...headers, host: 'localhost' },
179
- cookies: {},
180
- query,
181
- body,
182
- params: getPathParams(pathWithLeadingSlash, papi.getPapiParameters(papiRoute).path),
183
- };
184
- const res = {};
185
- const childDi = dippy.createChildContainer(this.di, [
186
- {
187
- provide: tokensServerPrivate.FASTIFY_REQUEST,
188
- useValue: req,
189
- },
190
- {
191
- provide: tokensServerPrivate.FASTIFY_RESPONSE,
192
- useValue: res,
213
+ const createCacheToken = dippy.createToken('httpClient createCache');
214
+ const providers = [
215
+ core.provide({
216
+ provide: tokensHttpClient.HTTP_CLIENT_FACTORY,
217
+ useFactory: httpClientFactory,
218
+ deps: {
219
+ logger: tokensCommon.LOGGER_TOKEN,
220
+ envManager: tokensCommon.ENV_MANAGER_TOKEN,
221
+ appInfo: core.APP_INFO_TOKEN,
222
+ createCache: createCacheToken,
223
+ makeRequestRegistry: 'makeRequestRegistry',
224
+ requestManager: {
225
+ token: tokensCommon.REQUEST_MANAGER_TOKEN,
226
+ optional: true,
193
227
  },
194
- ]);
195
- const papiExecutor = childDi.get(tokensServerPrivate.PAPI_EXECUTOR);
196
- const payload = await papiExecutor(papiRoute);
197
- return { payload, status: 200, headers: {} };
198
- }
199
- }
200
-
201
- const PapiClientModule = /* @__PURE__ */ core.Module({
202
- providers: [
203
- core.provide({
204
- provide: tokensHttpClient.PAPI_SERVICE,
205
- scope: core.Scope.SINGLETON,
206
- useClass: PapiService,
207
- deps: {
208
- di: core.DI_TOKEN,
209
- papi: { token: tokensServer.SERVER_MODULE_PAPI_PUBLIC_ROUTE, optional: true },
228
+ headersList: {
229
+ token: tokensHttpClient.API_CLIENT_PASS_HEADERS,
230
+ optional: true,
210
231
  },
211
- }),
212
- ],
213
- })(class PapiClientModule {
214
- });
215
-
216
- const createCacheToken = dippy.createToken('httpClient createCache');
217
- const HttpClientModule = /* @__PURE__ */ core.Module({
218
- imports: [PapiClientModule],
219
- providers: [
220
- core.provide({
221
- provide: tokensHttpClient.HTTP_CLIENT_FACTORY,
222
- useFactory: httpClientFactory,
223
- deps: {
224
- logger: tokensCommon.LOGGER_TOKEN,
225
- envManager: tokensCommon.ENV_MANAGER_TOKEN,
226
- appInfo: core.APP_INFO_TOKEN,
227
- createCache: createCacheToken,
228
- makeRequestRegistry: 'makeRequestRegistry',
229
- requestManager: {
230
- token: tokensCommon.REQUEST_MANAGER_TOKEN,
231
- optional: true,
232
- },
233
- headersList: {
234
- token: tokensHttpClient.API_CLIENT_PASS_HEADERS,
235
- optional: true,
236
- },
237
- agent: {
238
- token: tokensHttpClient.HTTP_CLIENT_AGENT,
239
- optional: true,
240
- },
241
- disableCircuitBreaker: {
242
- token: tokensHttpClient.DISABLE_CIRCUIT_BREAKER,
243
- optional: true,
244
- },
245
- defaultOptions: {
246
- token: tokensHttpClient.DEFAULT_HTTP_CLIENT_FACTORY_OPTIONS,
247
- optional: true,
248
- },
249
- defaultInterceptors: {
250
- token: tokensHttpClient.DEFAULT_HTTP_CLIENT_INTERCEPTORS,
251
- optional: true,
252
- },
253
- commandLineExecutionContext: {
254
- token: tokensCommon.COMMAND_LINE_EXECUTION_CONTEXT_TOKEN,
255
- optional: true,
256
- },
232
+ agent: {
233
+ token: tokensHttpClient.HTTP_CLIENT_AGENT,
234
+ optional: true,
257
235
  },
258
- }),
259
- core.provide({
260
- provide: tokensHttpClient.HTTP_CLIENT,
261
- useFactory: ({ factory }) => {
262
- return factory({
263
- name: 'http-client',
264
- enableCircuitBreaker: false,
265
- });
236
+ disableCircuitBreaker: {
237
+ token: tokensHttpClient.DISABLE_CIRCUIT_BREAKER,
238
+ optional: true,
266
239
  },
267
- deps: {
268
- factory: tokensHttpClient.HTTP_CLIENT_FACTORY,
240
+ defaultOptions: {
241
+ token: tokensHttpClient.DEFAULT_HTTP_CLIENT_FACTORY_OPTIONS,
242
+ optional: true,
269
243
  },
270
- }),
271
- core.provide({
272
- provide: tokensCommon.ENV_USED_TOKEN,
273
- useValue: [
274
- { key: 'HTTP_CLIENT_CACHE_DISABLED', optional: true, dehydrate: false },
275
- { key: 'HTTP_CLIENT_CIRCUIT_BREAKER_DISABLED', optional: true, dehydrate: false },
276
- ],
277
- }),
278
- /**
279
- * хранилище для экземпляров @tinkoff/request
280
- *
281
- * требуется хранить экземпляры в единственном виде,
282
- * т.к. многие плагины @tinkoff/request после инициализации имеют состояние
283
- * (cache, circuit breaker), и не будут корректно работать на сервере,
284
- * если создавать новые экземпляры на Scope.REQUEST
285
- */
286
- core.provide({
287
- provide: 'makeRequestRegistry',
288
- scope: core.Scope.SINGLETON,
289
- useFactory: () => new Map(),
290
- }),
291
- /**
292
- * `CREATE_CACHE_TOKEN` имеет проверку, если токен используется провайдером,
293
- * который имеет Scope.SINGLETON, то инстанс кэша сохраняется в общее хранилище,
294
- * и доступен для очистки через `/papi/clear-cache`.
295
- * Scope.REQUEST игнорируется, т.к. это верная утечка памяти,
296
- * инстансов кэши было бы неограниченное количество.
297
- *
298
- * HTTP клиенты создаются со Scope.REQUEST, но инстансы @tinkoff/request
299
- * (и соответственно кэшей) создаются только один раз, благодаря `makeRequestRegistry`.
300
- * это гарантирует отсутствие утечек памяти, поэтому мы обходим проверку
301
- * на Scope.SINGLETON c помощью обертки `createCacheToken`
302
- */
303
- core.provide({
304
- provide: createCacheToken,
305
- scope: core.Scope.SINGLETON,
306
- useFactory: ({ createCache }) => {
307
- return createCache;
244
+ defaultInterceptors: {
245
+ token: tokensHttpClient.DEFAULT_HTTP_CLIENT_INTERCEPTORS,
246
+ optional: true,
308
247
  },
309
- deps: {
310
- createCache: tokensCommon.CREATE_CACHE_TOKEN,
248
+ commandLineExecutionContext: {
249
+ token: tokensCommon.COMMAND_LINE_EXECUTION_CONTEXT_TOKEN,
250
+ optional: true,
311
251
  },
312
- }),
313
- core.provide({
314
- provide: tokensHttpClient.API_CLIENT_PASS_HEADERS,
315
- useValue: ['x-request-id'],
316
- }),
252
+ },
253
+ }),
254
+ core.provide({
255
+ provide: tokensHttpClient.HTTP_CLIENT,
256
+ useFactory: ({ factory }) => {
257
+ return factory({
258
+ name: 'http-client',
259
+ enableCircuitBreaker: false,
260
+ });
261
+ },
262
+ deps: {
263
+ factory: tokensHttpClient.HTTP_CLIENT_FACTORY,
264
+ },
265
+ }),
266
+ core.provide({
267
+ provide: tokensCommon.ENV_USED_TOKEN,
268
+ useValue: [
269
+ { key: 'HTTP_CLIENT_CACHE_DISABLED', optional: true, dehydrate: false },
270
+ { key: 'HTTP_CLIENT_CIRCUIT_BREAKER_DISABLED', optional: true, dehydrate: false },
271
+ ],
272
+ }),
273
+ /**
274
+ * хранилище для экземпляров @tinkoff/request
275
+ *
276
+ * требуется хранить экземпляры в единственном виде,
277
+ * т.к. многие плагины @tinkoff/request после инициализации имеют состояние
278
+ * (cache, circuit breaker), и не будут корректно работать на сервере,
279
+ * если создавать новые экземпляры на Scope.REQUEST
280
+ */
281
+ core.provide({
282
+ provide: 'makeRequestRegistry',
283
+ scope: core.Scope.SINGLETON,
284
+ useFactory: () => new Map(),
285
+ }),
286
+ /**
287
+ * `CREATE_CACHE_TOKEN` имеет проверку, если токен используется провайдером,
288
+ * который имеет Scope.SINGLETON, то инстанс кэша сохраняется в общее хранилище,
289
+ * и доступен для очистки через `/papi/clear-cache`.
290
+ * Scope.REQUEST игнорируется, т.к. это верная утечка памяти,
291
+ * инстансов кэши было бы неограниченное количество.
292
+ *
293
+ * HTTP клиенты создаются со Scope.REQUEST, но инстансы @tinkoff/request
294
+ * (и соответственно кэшей) создаются только один раз, благодаря `makeRequestRegistry`.
295
+ * это гарантирует отсутствие утечек памяти, поэтому мы обходим проверку
296
+ * на Scope.SINGLETON c помощью обертки `createCacheToken`
297
+ */
298
+ core.provide({
299
+ provide: createCacheToken,
300
+ scope: core.Scope.SINGLETON,
301
+ useFactory: ({ createCache }) => {
302
+ return createCache;
303
+ },
304
+ deps: {
305
+ createCache: tokensCommon.CREATE_CACHE_TOKEN,
306
+ },
307
+ }),
308
+ core.provide({
309
+ provide: tokensHttpClient.API_CLIENT_PASS_HEADERS,
310
+ useValue: ['x-request-id'],
311
+ }),
312
+ ];
313
+
314
+ const HttpClientModule = /* @__PURE__ */ core.declareModule({
315
+ name: 'HttpClientModule',
316
+ imports: [PapiClientModule],
317
+ providers: [
318
+ ...providers,
319
+ // we can't separate this module to browser and server files through "browser"
320
+ // package.json field, because jest not supports this field resolving out of the box
321
+ // and many unit tests rely on jsdom environment and fails because of resolving to server version
322
+ // TODO: separate jest preset for jsdom unit tests with custom resolver to support "browser" field
323
+ // @see https://github.com/marko-js/jest/blob/main/src/preset/browser/jest-preset.ts
324
+ ...(typeof window === 'undefined'
325
+ ? [
326
+ core.provide({
327
+ provide: core.commandLineListTokens.init,
328
+ useFactory: ({ agent }) => {
329
+ return () => {
330
+ const { setGlobalDispatcher } = require('undici');
331
+ setGlobalDispatcher(agent.http);
332
+ };
333
+ },
334
+ deps: {
335
+ agent: tokensHttpClient.HTTP_CLIENT_AGENT,
336
+ },
337
+ }),
338
+ core.provide({
339
+ provide: tokensHttpClient.HTTP_CLIENT_AGENT,
340
+ useFactory: ({ options, interceptors }) => {
341
+ const { Agent } = require('undici');
342
+ const agent = new Agent(options).compose(...(interceptors ?? []));
343
+ return {
344
+ http: agent,
345
+ https: agent,
346
+ };
347
+ },
348
+ deps: {
349
+ options: tokensHttpClient.HTTP_CLIENT_AGENT_OPTIONS,
350
+ interceptors: core.optional(tokensHttpClient.HTTP_CLIENT_AGENT_INTERCEPTORS),
351
+ },
352
+ }),
353
+ core.provide({
354
+ provide: tokensHttpClient.HTTP_CLIENT_AGENT_OPTIONS,
355
+ useValue: {},
356
+ }),
357
+ ]
358
+ : []),
317
359
  ],
318
- })(class HttpClientModule {
319
360
  });
320
361
 
321
- jest.mock('node-fetch');
322
- const { Response } = jest.requireActual('node-fetch');
362
+ const fetch = jest.spyOn(require('undici'), 'fetch');
323
363
  const testApi = (options) => {
324
364
  const caches = [];
325
365
  const { modules = [], providers = [], env } = options;
@@ -337,19 +377,17 @@ const testApi = (options) => {
337
377
  ],
338
378
  providers: [...providers],
339
379
  });
340
- const fetchMock = fetch__default["default"];
380
+ const fetchMock = fetch;
341
381
  const clearCaches = () => {
342
382
  caches.forEach((cache) => cache.clear());
343
383
  };
344
384
  return {
345
385
  di,
346
386
  fetchMock,
347
- mockJsonResponse: async (body, init = {}) => {
387
+ mockJsonResponse: async (body, { status, headers } = {}) => {
348
388
  clearCaches();
349
- const { headers = {} } = init;
350
389
  fetchMock.mockImplementation(() => Promise.resolve(new Response(JSON.stringify(body), {
351
- status: 200,
352
- ...init,
390
+ status: status ?? 200,
353
391
  headers: {
354
392
  'content-type': 'application/json',
355
393
  ...headers,