@hey-api/openapi-ts 0.80.18 → 0.81.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.
@@ -0,0 +1,235 @@
1
+ import type { Config } from './types';
2
+
3
+ export type ServerSentEventsOptions<TData = unknown> = Omit<
4
+ RequestInit,
5
+ 'method'
6
+ > &
7
+ Pick<Config, 'method' | 'responseTransformer' | 'responseValidator'> & {
8
+ /**
9
+ * Callback invoked when a network or parsing error occurs during streaming.
10
+ *
11
+ * This option applies only if the endpoint returns a stream of events.
12
+ *
13
+ * @param error The error that occurred.
14
+ */
15
+ onSseError?: (error: unknown) => void;
16
+ /**
17
+ * Callback invoked when an event is streamed from the server.
18
+ *
19
+ * This option applies only if the endpoint returns a stream of events.
20
+ *
21
+ * @param event Event streamed from the server.
22
+ * @returns Nothing (void).
23
+ */
24
+ onSseEvent?: (event: StreamEvent<TData>) => void;
25
+ /**
26
+ * Default retry delay in milliseconds.
27
+ *
28
+ * This option applies only if the endpoint returns a stream of events.
29
+ *
30
+ * @default 3000
31
+ */
32
+ sseDefaultRetryDelay?: number;
33
+ /**
34
+ * Maximum number of retry attempts before giving up.
35
+ */
36
+ sseMaxRetryAttempts?: number;
37
+ /**
38
+ * Maximum retry delay in milliseconds.
39
+ *
40
+ * Applies only when exponential backoff is used.
41
+ *
42
+ * This option applies only if the endpoint returns a stream of events.
43
+ *
44
+ * @default 30000
45
+ */
46
+ sseMaxRetryDelay?: number;
47
+ /**
48
+ * Optional sleep function for retry backoff.
49
+ *
50
+ * Defaults to using `setTimeout`.
51
+ */
52
+ sseSleepFn?: (ms: number) => Promise<void>;
53
+ url: string;
54
+ };
55
+
56
+ export interface StreamEvent<TData = unknown> {
57
+ data: TData;
58
+ event?: string;
59
+ id?: string;
60
+ retry?: number;
61
+ }
62
+
63
+ export type ServerSentEventsResult<
64
+ TData = unknown,
65
+ TReturn = void,
66
+ TNext = unknown,
67
+ > = {
68
+ stream: AsyncGenerator<
69
+ TData extends Record<string, unknown> ? TData[keyof TData] : TData,
70
+ TReturn,
71
+ TNext
72
+ >;
73
+ };
74
+
75
+ export const createSseClient = <TData = unknown>({
76
+ onSseError,
77
+ onSseEvent,
78
+ responseTransformer,
79
+ responseValidator,
80
+ sseDefaultRetryDelay,
81
+ sseMaxRetryAttempts,
82
+ sseMaxRetryDelay,
83
+ sseSleepFn,
84
+ url,
85
+ ...options
86
+ }: ServerSentEventsOptions): ServerSentEventsResult<TData> => {
87
+ let lastEventId: string | undefined;
88
+
89
+ const sleep =
90
+ sseSleepFn ??
91
+ ((ms: number) => new Promise((resolve) => setTimeout(resolve, ms)));
92
+
93
+ const createStream = async function* () {
94
+ let retryDelay: number = sseDefaultRetryDelay ?? 3000;
95
+ let attempt = 0;
96
+ const signal = options.signal ?? new AbortController().signal;
97
+
98
+ while (true) {
99
+ if (signal.aborted) break;
100
+
101
+ attempt++;
102
+
103
+ const headers =
104
+ options.headers instanceof Headers
105
+ ? options.headers
106
+ : new Headers(options.headers as Record<string, string> | undefined);
107
+
108
+ if (lastEventId !== undefined) {
109
+ headers.set('Last-Event-ID', lastEventId);
110
+ }
111
+
112
+ try {
113
+ const response = await fetch(url, { ...options, headers, signal });
114
+
115
+ if (!response.ok)
116
+ throw new Error(
117
+ `SSE failed: ${response.status} ${response.statusText}`,
118
+ );
119
+
120
+ if (!response.body) throw new Error('No body in SSE response');
121
+
122
+ const reader = response.body
123
+ .pipeThrough(new TextDecoderStream())
124
+ .getReader();
125
+
126
+ let buffer = '';
127
+
128
+ const abortHandler = () => {
129
+ try {
130
+ reader.cancel();
131
+ } catch {
132
+ // noop
133
+ }
134
+ };
135
+
136
+ signal.addEventListener('abort', abortHandler);
137
+
138
+ try {
139
+ while (true) {
140
+ const { done, value } = await reader.read();
141
+ if (done) break;
142
+ buffer += value;
143
+
144
+ const chunks = buffer.split('\n\n');
145
+ buffer = chunks.pop() ?? '';
146
+
147
+ for (const chunk of chunks) {
148
+ const lines = chunk.split('\n');
149
+ const dataLines: Array<string> = [];
150
+ let eventName: string | undefined;
151
+
152
+ for (const line of lines) {
153
+ if (line.startsWith('data:')) {
154
+ dataLines.push(line.replace(/^data:\s*/, ''));
155
+ } else if (line.startsWith('event:')) {
156
+ eventName = line.replace(/^event:\s*/, '');
157
+ } else if (line.startsWith('id:')) {
158
+ lastEventId = line.replace(/^id:\s*/, '');
159
+ } else if (line.startsWith('retry:')) {
160
+ const parsed = Number.parseInt(
161
+ line.replace(/^retry:\s*/, ''),
162
+ 10,
163
+ );
164
+ if (!Number.isNaN(parsed)) {
165
+ retryDelay = parsed;
166
+ }
167
+ }
168
+ }
169
+
170
+ let data: unknown;
171
+ let parsedJson = false;
172
+
173
+ if (dataLines.length) {
174
+ const rawData = dataLines.join('\n');
175
+ try {
176
+ data = JSON.parse(rawData);
177
+ parsedJson = true;
178
+ } catch {
179
+ data = rawData;
180
+ }
181
+ }
182
+
183
+ if (parsedJson) {
184
+ if (responseValidator) {
185
+ await responseValidator(data);
186
+ }
187
+
188
+ if (responseTransformer) {
189
+ data = await responseTransformer(data);
190
+ }
191
+ }
192
+
193
+ onSseEvent?.({
194
+ data,
195
+ event: eventName,
196
+ id: lastEventId,
197
+ retry: retryDelay,
198
+ });
199
+
200
+ if (dataLines.length) {
201
+ yield data as any;
202
+ }
203
+ }
204
+ }
205
+ } finally {
206
+ signal.removeEventListener('abort', abortHandler);
207
+ reader.releaseLock();
208
+ }
209
+
210
+ break; // exit loop on normal completion
211
+ } catch (error) {
212
+ // connection failed or aborted; retry after delay
213
+ onSseError?.(error);
214
+
215
+ if (
216
+ sseMaxRetryAttempts !== undefined &&
217
+ attempt >= sseMaxRetryAttempts
218
+ ) {
219
+ break; // stop after firing error
220
+ }
221
+
222
+ // exponential backoff: double retry each attempt, cap at 30s
223
+ const backoff = Math.min(
224
+ retryDelay * 2 ** (attempt - 1),
225
+ sseMaxRetryDelay ?? 30000,
226
+ );
227
+ await sleep(backoff);
228
+ }
229
+ }
230
+ };
231
+
232
+ const stream = createStream();
233
+
234
+ return { stream };
235
+ };
@@ -0,0 +1,112 @@
1
+ import type { QuerySerializer } from './bodySerializer';
2
+ import {
3
+ type ArraySeparatorStyle,
4
+ serializeArrayParam,
5
+ serializeObjectParam,
6
+ serializePrimitiveParam,
7
+ } from './pathSerializer';
8
+
9
+ export interface PathSerializer {
10
+ path: Record<string, unknown>;
11
+ url: string;
12
+ }
13
+
14
+ export const PATH_PARAM_RE = /\{[^{}]+\}/g;
15
+
16
+ export const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
17
+ let url = _url;
18
+ const matches = _url.match(PATH_PARAM_RE);
19
+ if (matches) {
20
+ for (const match of matches) {
21
+ let explode = false;
22
+ let name = match.substring(1, match.length - 1);
23
+ let style: ArraySeparatorStyle = 'simple';
24
+
25
+ if (name.endsWith('*')) {
26
+ explode = true;
27
+ name = name.substring(0, name.length - 1);
28
+ }
29
+
30
+ if (name.startsWith('.')) {
31
+ name = name.substring(1);
32
+ style = 'label';
33
+ } else if (name.startsWith(';')) {
34
+ name = name.substring(1);
35
+ style = 'matrix';
36
+ }
37
+
38
+ const value = path[name];
39
+
40
+ if (value === undefined || value === null) {
41
+ continue;
42
+ }
43
+
44
+ if (Array.isArray(value)) {
45
+ url = url.replace(
46
+ match,
47
+ serializeArrayParam({ explode, name, style, value }),
48
+ );
49
+ continue;
50
+ }
51
+
52
+ if (typeof value === 'object') {
53
+ url = url.replace(
54
+ match,
55
+ serializeObjectParam({
56
+ explode,
57
+ name,
58
+ style,
59
+ value: value as Record<string, unknown>,
60
+ valueOnly: true,
61
+ }),
62
+ );
63
+ continue;
64
+ }
65
+
66
+ if (style === 'matrix') {
67
+ url = url.replace(
68
+ match,
69
+ `;${serializePrimitiveParam({
70
+ name,
71
+ value: value as string,
72
+ })}`,
73
+ );
74
+ continue;
75
+ }
76
+
77
+ const replaceValue = encodeURIComponent(
78
+ style === 'label' ? `.${value as string}` : (value as string),
79
+ );
80
+ url = url.replace(match, replaceValue);
81
+ }
82
+ }
83
+ return url;
84
+ };
85
+
86
+ export const getUrl = ({
87
+ baseUrl,
88
+ path,
89
+ query,
90
+ querySerializer,
91
+ url: _url,
92
+ }: {
93
+ baseUrl?: string;
94
+ path?: Record<string, unknown>;
95
+ query?: Record<string, unknown>;
96
+ querySerializer: QuerySerializer;
97
+ url: string;
98
+ }) => {
99
+ const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
100
+ let url = (baseUrl ?? '') + pathUrl;
101
+ if (path) {
102
+ url = defaultPathSerializer({ path, url });
103
+ }
104
+ let search = query ? querySerializer(query) : '';
105
+ if (search.startsWith('?')) {
106
+ search = search.substring(1);
107
+ }
108
+ if (search) {
109
+ url += `?${search}`;
110
+ }
111
+ return url;
112
+ };
@@ -1,4 +1,10 @@
1
- import type { Client, Config, ResolvedRequestOptions } from './types';
1
+ import { createSseClient } from '../core/serverSentEvents';
2
+ import type {
3
+ Client,
4
+ Config,
5
+ RequestOptions,
6
+ ResolvedRequestOptions,
7
+ } from './types';
2
8
  import {
3
9
  buildUrl,
4
10
  createConfig,
@@ -31,7 +37,7 @@ export const createClient = (config: Config = {}): Client => {
31
37
  ResolvedRequestOptions
32
38
  >();
33
39
 
34
- const request: Client['request'] = async (options) => {
40
+ const beforeRequest = async (options: RequestOptions) => {
35
41
  const opts = {
36
42
  ..._config,
37
43
  ...options,
@@ -61,6 +67,13 @@ export const createClient = (config: Config = {}): Client => {
61
67
  }
62
68
 
63
69
  const url = buildUrl(opts);
70
+
71
+ return { opts, url };
72
+ };
73
+
74
+ const request: Client['request'] = async (options) => {
75
+ // @ts-expect-error
76
+ const { opts, url } = await beforeRequest(options);
64
77
  const requestInit: ReqInit = {
65
78
  redirect: 'follow',
66
79
  ...opts,
@@ -178,20 +191,35 @@ export const createClient = (config: Config = {}): Client => {
178
191
  };
179
192
  };
180
193
 
194
+ const makeMethod = (method: Required<Config>['method']) => {
195
+ const fn = (options: RequestOptions) => request({ ...options, method });
196
+ fn.sse = async (options: RequestOptions) => {
197
+ const { opts, url } = await beforeRequest(options);
198
+ return createSseClient({
199
+ ...opts,
200
+ body: opts.body as BodyInit | null | undefined,
201
+ headers: opts.headers as unknown as Record<string, string>,
202
+ method,
203
+ url,
204
+ });
205
+ };
206
+ return fn;
207
+ };
208
+
181
209
  return {
182
210
  buildUrl,
183
- connect: (options) => request({ ...options, method: 'CONNECT' }),
184
- delete: (options) => request({ ...options, method: 'DELETE' }),
185
- get: (options) => request({ ...options, method: 'GET' }),
211
+ connect: makeMethod('CONNECT'),
212
+ delete: makeMethod('DELETE'),
213
+ get: makeMethod('GET'),
186
214
  getConfig,
187
- head: (options) => request({ ...options, method: 'HEAD' }),
215
+ head: makeMethod('HEAD'),
188
216
  interceptors,
189
- options: (options) => request({ ...options, method: 'OPTIONS' }),
190
- patch: (options) => request({ ...options, method: 'PATCH' }),
191
- post: (options) => request({ ...options, method: 'POST' }),
192
- put: (options) => request({ ...options, method: 'PUT' }),
217
+ options: makeMethod('OPTIONS'),
218
+ patch: makeMethod('PATCH'),
219
+ post: makeMethod('POST'),
220
+ put: makeMethod('PUT'),
193
221
  request,
194
222
  setConfig,
195
- trace: (options) => request({ ...options, method: 'TRACE' }),
196
- };
223
+ trace: makeMethod('TRACE'),
224
+ } as Client;
197
225
  };
@@ -1,4 +1,8 @@
1
1
  import type { Auth } from '../core/auth';
2
+ import type {
3
+ ServerSentEventsOptions,
4
+ ServerSentEventsResult,
5
+ } from '../core/serverSentEvents';
2
6
  import type {
3
7
  Client as CoreClient,
4
8
  Config as CoreConfig,
@@ -59,13 +63,22 @@ export interface Config<T extends ClientOptions = ClientOptions>
59
63
  }
60
64
 
61
65
  export interface RequestOptions<
66
+ TData = unknown,
62
67
  TResponseStyle extends ResponseStyle = 'fields',
63
68
  ThrowOnError extends boolean = boolean,
64
69
  Url extends string = string,
65
70
  > extends Config<{
66
- responseStyle: TResponseStyle;
67
- throwOnError: ThrowOnError;
68
- }> {
71
+ responseStyle: TResponseStyle;
72
+ throwOnError: ThrowOnError;
73
+ }>,
74
+ Pick<
75
+ ServerSentEventsOptions<TData>,
76
+ | 'onSseError'
77
+ | 'onSseEvent'
78
+ | 'sseDefaultRetryDelay'
79
+ | 'sseMaxRetryAttempts'
80
+ | 'sseMaxRetryDelay'
81
+ > {
69
82
  /**
70
83
  * Any body that you want to add to your request.
71
84
  *
@@ -85,7 +98,7 @@ export interface ResolvedRequestOptions<
85
98
  TResponseStyle extends ResponseStyle = 'fields',
86
99
  ThrowOnError extends boolean = boolean,
87
100
  Url extends string = string,
88
- > extends RequestOptions<TResponseStyle, ThrowOnError, Url> {
101
+ > extends RequestOptions<unknown, TResponseStyle, ThrowOnError, Url> {
89
102
  serializedBody?: string;
90
103
  }
91
104
 
@@ -140,23 +153,39 @@ export interface ClientOptions {
140
153
  throwOnError?: boolean;
141
154
  }
142
155
 
143
- type MethodFn = <
156
+ type MethodFnBase = <
144
157
  TData = unknown,
145
158
  TError = unknown,
146
159
  ThrowOnError extends boolean = false,
147
160
  TResponseStyle extends ResponseStyle = 'fields',
148
161
  >(
149
- options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'>,
162
+ options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
150
163
  ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
151
164
 
165
+ type MethodFnServerSentEvents = <
166
+ TData = unknown,
167
+ TError = unknown,
168
+ ThrowOnError extends boolean = false,
169
+ TResponseStyle extends ResponseStyle = 'fields',
170
+ >(
171
+ options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'>,
172
+ ) => Promise<ServerSentEventsResult<TData, TError>>;
173
+
174
+ type MethodFn = MethodFnBase & {
175
+ sse: MethodFnServerSentEvents;
176
+ };
177
+
152
178
  type RequestFn = <
153
179
  TData = unknown,
154
180
  TError = unknown,
155
181
  ThrowOnError extends boolean = false,
156
182
  TResponseStyle extends ResponseStyle = 'fields',
157
183
  >(
158
- options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> &
159
- Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>,
184
+ options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
185
+ Pick<
186
+ Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
187
+ 'method'
188
+ >,
160
189
  ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
161
190
 
162
191
  type BuildUrlFn = <
@@ -199,9 +228,10 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
199
228
  export type Options<
200
229
  TData extends TDataShape = TDataShape,
201
230
  ThrowOnError extends boolean = boolean,
231
+ TResponse = unknown,
202
232
  TResponseStyle extends ResponseStyle = 'fields',
203
233
  > = OmitKeys<
204
- RequestOptions<TResponseStyle, ThrowOnError>,
234
+ RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
205
235
  'body' | 'path' | 'query' | 'url'
206
236
  > &
207
237
  Omit<TData, 'url'>;
@@ -213,18 +243,22 @@ export type OptionsLegacyParser<
213
243
  > = TData extends { body?: any }
214
244
  ? TData extends { headers?: any }
215
245
  ? OmitKeys<
216
- RequestOptions<TResponseStyle, ThrowOnError>,
246
+ RequestOptions<unknown, TResponseStyle, ThrowOnError>,
217
247
  'body' | 'headers' | 'url'
218
248
  > &
219
249
  TData
220
- : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> &
250
+ : OmitKeys<
251
+ RequestOptions<unknown, TResponseStyle, ThrowOnError>,
252
+ 'body' | 'url'
253
+ > &
221
254
  TData &
222
- Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'>
255
+ Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'>
223
256
  : TData extends { headers?: any }
224
257
  ? OmitKeys<
225
- RequestOptions<TResponseStyle, ThrowOnError>,
258
+ RequestOptions<unknown, TResponseStyle, ThrowOnError>,
226
259
  'headers' | 'url'
227
260
  > &
228
261
  TData &
229
- Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'>
230
- : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
262
+ Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'>
263
+ : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> &
264
+ TData;
@@ -1,97 +1,14 @@
1
1
  import { getAuthToken } from '../core/auth';
2
- import type {
3
- QuerySerializer,
4
- QuerySerializerOptions,
5
- } from '../core/bodySerializer';
2
+ import type { QuerySerializerOptions } from '../core/bodySerializer';
6
3
  import { jsonBodySerializer } from '../core/bodySerializer';
7
4
  import {
8
5
  serializeArrayParam,
9
6
  serializeObjectParam,
10
7
  serializePrimitiveParam,
11
8
  } from '../core/pathSerializer';
9
+ import { getUrl } from '../core/utils';
12
10
  import type { Client, ClientOptions, Config, RequestOptions } from './types';
13
11
 
14
- interface PathSerializer {
15
- path: Record<string, unknown>;
16
- url: string;
17
- }
18
-
19
- const PATH_PARAM_RE = /\{[^{}]+\}/g;
20
-
21
- type ArrayStyle = 'form' | 'spaceDelimited' | 'pipeDelimited';
22
- type MatrixStyle = 'label' | 'matrix' | 'simple';
23
- type ArraySeparatorStyle = ArrayStyle | MatrixStyle;
24
-
25
- const defaultPathSerializer = ({ path, url: _url }: PathSerializer) => {
26
- let url = _url;
27
- const matches = _url.match(PATH_PARAM_RE);
28
- if (matches) {
29
- for (const match of matches) {
30
- let explode = false;
31
- let name = match.substring(1, match.length - 1);
32
- let style: ArraySeparatorStyle = 'simple';
33
-
34
- if (name.endsWith('*')) {
35
- explode = true;
36
- name = name.substring(0, name.length - 1);
37
- }
38
-
39
- if (name.startsWith('.')) {
40
- name = name.substring(1);
41
- style = 'label';
42
- } else if (name.startsWith(';')) {
43
- name = name.substring(1);
44
- style = 'matrix';
45
- }
46
-
47
- const value = path[name];
48
-
49
- if (value === undefined || value === null) {
50
- continue;
51
- }
52
-
53
- if (Array.isArray(value)) {
54
- url = url.replace(
55
- match,
56
- serializeArrayParam({ explode, name, style, value }),
57
- );
58
- continue;
59
- }
60
-
61
- if (typeof value === 'object') {
62
- url = url.replace(
63
- match,
64
- serializeObjectParam({
65
- explode,
66
- name,
67
- style,
68
- value: value as Record<string, unknown>,
69
- valueOnly: true,
70
- }),
71
- );
72
- continue;
73
- }
74
-
75
- if (style === 'matrix') {
76
- url = url.replace(
77
- match,
78
- `;${serializePrimitiveParam({
79
- name,
80
- value: value as string,
81
- })}`,
82
- );
83
- continue;
84
- }
85
-
86
- const replaceValue = encodeURIComponent(
87
- style === 'label' ? `.${value as string}` : (value as string),
88
- );
89
- url = url.replace(match, replaceValue);
90
- }
91
- }
92
- return url;
93
- };
94
-
95
12
  export const createQuerySerializer = <T = unknown>({
96
13
  allowReserved,
97
14
  array,
@@ -243,8 +160,8 @@ export const setAuthParams = async ({
243
160
  }
244
161
  };
245
162
 
246
- export const buildUrl: Client['buildUrl'] = (options) => {
247
- const url = getUrl({
163
+ export const buildUrl: Client['buildUrl'] = (options) =>
164
+ getUrl({
248
165
  baseUrl: options.baseUrl as string,
249
166
  path: options.path,
250
167
  query: options.query,
@@ -254,36 +171,6 @@ export const buildUrl: Client['buildUrl'] = (options) => {
254
171
  : createQuerySerializer(options.querySerializer),
255
172
  url: options.url,
256
173
  });
257
- return url;
258
- };
259
-
260
- export const getUrl = ({
261
- baseUrl,
262
- path,
263
- query,
264
- querySerializer,
265
- url: _url,
266
- }: {
267
- baseUrl?: string;
268
- path?: Record<string, unknown>;
269
- query?: Record<string, unknown>;
270
- querySerializer: QuerySerializer;
271
- url: string;
272
- }) => {
273
- const pathUrl = _url.startsWith('/') ? _url : `/${_url}`;
274
- let url = (baseUrl ?? '') + pathUrl;
275
- if (path) {
276
- url = defaultPathSerializer({ path, url });
277
- }
278
- let search = query ? querySerializer(query) : '';
279
- if (search.startsWith('?')) {
280
- search = search.substring(1);
281
- }
282
- if (search) {
283
- url += `?${search}`;
284
- }
285
- return url;
286
- };
287
174
 
288
175
  export const mergeConfigs = (a: Config, b: Config): Config => {
289
176
  const config = { ...a, ...b };