@hey-api/openapi-ts 0.80.18 → 0.81.1

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
+ };
@@ -5,29 +5,36 @@ import type {
5
5
  QuerySerializerOptions,
6
6
  } from './bodySerializer';
7
7
 
8
- export interface Client<
8
+ export type HttpMethod =
9
+ | 'connect'
10
+ | 'delete'
11
+ | 'get'
12
+ | 'head'
13
+ | 'options'
14
+ | 'patch'
15
+ | 'post'
16
+ | 'put'
17
+ | 'trace';
18
+
19
+ export type Client<
9
20
  RequestFn = never,
10
21
  Config = unknown,
11
22
  MethodFn = never,
12
23
  BuildUrlFn = never,
13
- > {
24
+ SseFn = never,
25
+ > = {
14
26
  /**
15
27
  * Returns the final request URL.
16
28
  */
17
29
  buildUrl: BuildUrlFn;
18
- connect: MethodFn;
19
- delete: MethodFn;
20
- get: MethodFn;
21
30
  getConfig: () => Config;
22
- head: MethodFn;
23
- options: MethodFn;
24
- patch: MethodFn;
25
- post: MethodFn;
26
- put: MethodFn;
27
31
  request: RequestFn;
28
32
  setConfig: (config: Config) => Config;
29
- trace: MethodFn;
30
- }
33
+ } & {
34
+ [K in HttpMethod]: MethodFn;
35
+ } & ([SseFn] extends [never]
36
+ ? { sse?: never }
37
+ : { sse: { [K in HttpMethod]: SseFn } });
31
38
 
32
39
  export interface Config {
33
40
  /**
@@ -63,16 +70,7 @@ export interface Config {
63
70
  *
64
71
  * {@link https://developer.mozilla.org/docs/Web/API/fetch#method See more}
65
72
  */
66
- method?:
67
- | 'CONNECT'
68
- | 'DELETE'
69
- | 'GET'
70
- | 'HEAD'
71
- | 'OPTIONS'
72
- | 'PATCH'
73
- | 'POST'
74
- | 'PUT'
75
- | 'TRACE';
73
+ method?: Uppercase<HttpMethod>;
76
74
  /**
77
75
  * A function for serializing request query parameters. By default, arrays
78
76
  * will be exploded in form style, objects will be exploded in deepObject
@@ -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,11 @@
1
- import type { Client, Config, ResolvedRequestOptions } from './types';
1
+ import { createSseClient } from '../core/serverSentEvents';
2
+ import type { HttpMethod } from '../core/types';
3
+ import type {
4
+ Client,
5
+ Config,
6
+ RequestOptions,
7
+ ResolvedRequestOptions,
8
+ } from './types';
2
9
  import {
3
10
  buildUrl,
4
11
  createConfig,
@@ -31,7 +38,7 @@ export const createClient = (config: Config = {}): Client => {
31
38
  ResolvedRequestOptions
32
39
  >();
33
40
 
34
- const request: Client['request'] = async (options) => {
41
+ const beforeRequest = async (options: RequestOptions) => {
35
42
  const opts = {
36
43
  ..._config,
37
44
  ...options,
@@ -61,6 +68,13 @@ export const createClient = (config: Config = {}): Client => {
61
68
  }
62
69
 
63
70
  const url = buildUrl(opts);
71
+
72
+ return { opts, url };
73
+ };
74
+
75
+ const request: Client['request'] = async (options) => {
76
+ // @ts-expect-error
77
+ const { opts, url } = await beforeRequest(options);
64
78
  const requestInit: ReqInit = {
65
79
  redirect: 'follow',
66
80
  ...opts,
@@ -178,20 +192,47 @@ export const createClient = (config: Config = {}): Client => {
178
192
  };
179
193
  };
180
194
 
195
+ const makeMethodFn =
196
+ (method: Uppercase<HttpMethod>) => (options: RequestOptions) =>
197
+ request({ ...options, method });
198
+
199
+ const makeSseFn =
200
+ (method: Uppercase<HttpMethod>) => async (options: RequestOptions) => {
201
+ const { opts, url } = await beforeRequest(options);
202
+ return createSseClient({
203
+ ...opts,
204
+ body: opts.body as BodyInit | null | undefined,
205
+ headers: opts.headers as unknown as Record<string, string>,
206
+ method,
207
+ url,
208
+ });
209
+ };
210
+
181
211
  return {
182
212
  buildUrl,
183
- connect: (options) => request({ ...options, method: 'CONNECT' }),
184
- delete: (options) => request({ ...options, method: 'DELETE' }),
185
- get: (options) => request({ ...options, method: 'GET' }),
213
+ connect: makeMethodFn('CONNECT'),
214
+ delete: makeMethodFn('DELETE'),
215
+ get: makeMethodFn('GET'),
186
216
  getConfig,
187
- head: (options) => request({ ...options, method: 'HEAD' }),
217
+ head: makeMethodFn('HEAD'),
188
218
  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' }),
219
+ options: makeMethodFn('OPTIONS'),
220
+ patch: makeMethodFn('PATCH'),
221
+ post: makeMethodFn('POST'),
222
+ put: makeMethodFn('PUT'),
193
223
  request,
194
224
  setConfig,
195
- trace: (options) => request({ ...options, method: 'TRACE' }),
196
- };
225
+ sse: {
226
+ connect: makeSseFn('CONNECT'),
227
+ delete: makeSseFn('DELETE'),
228
+ get: makeSseFn('GET'),
229
+ head: makeSseFn('HEAD'),
230
+ options: makeSseFn('OPTIONS'),
231
+ patch: makeSseFn('PATCH'),
232
+ post: makeSseFn('POST'),
233
+ put: makeSseFn('PUT'),
234
+ trace: makeSseFn('TRACE'),
235
+ },
236
+ trace: makeMethodFn('TRACE'),
237
+ } as Client;
197
238
  };
@@ -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
 
@@ -146,17 +159,29 @@ type MethodFn = <
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 SseFn = <
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
+
152
174
  type RequestFn = <
153
175
  TData = unknown,
154
176
  TError = unknown,
155
177
  ThrowOnError extends boolean = false,
156
178
  TResponseStyle extends ResponseStyle = 'fields',
157
179
  >(
158
- options: Omit<RequestOptions<TResponseStyle, ThrowOnError>, 'method'> &
159
- Pick<Required<RequestOptions<TResponseStyle, ThrowOnError>>, 'method'>,
180
+ options: Omit<RequestOptions<TData, TResponseStyle, ThrowOnError>, 'method'> &
181
+ Pick<
182
+ Required<RequestOptions<TData, TResponseStyle, ThrowOnError>>,
183
+ 'method'
184
+ >,
160
185
  ) => RequestResult<TData, TError, ThrowOnError, TResponseStyle>;
161
186
 
162
187
  type BuildUrlFn = <
@@ -170,7 +195,13 @@ type BuildUrlFn = <
170
195
  options: Pick<TData, 'url'> & Options<TData>,
171
196
  ) => string;
172
197
 
173
- export type Client = CoreClient<RequestFn, Config, MethodFn, BuildUrlFn> & {
198
+ export type Client = CoreClient<
199
+ RequestFn,
200
+ Config,
201
+ MethodFn,
202
+ BuildUrlFn,
203
+ SseFn
204
+ > & {
174
205
  interceptors: Middleware<Request, Response, unknown, ResolvedRequestOptions>;
175
206
  };
176
207
 
@@ -199,9 +230,10 @@ type OmitKeys<T, K> = Pick<T, Exclude<keyof T, K>>;
199
230
  export type Options<
200
231
  TData extends TDataShape = TDataShape,
201
232
  ThrowOnError extends boolean = boolean,
233
+ TResponse = unknown,
202
234
  TResponseStyle extends ResponseStyle = 'fields',
203
235
  > = OmitKeys<
204
- RequestOptions<TResponseStyle, ThrowOnError>,
236
+ RequestOptions<TResponse, TResponseStyle, ThrowOnError>,
205
237
  'body' | 'path' | 'query' | 'url'
206
238
  > &
207
239
  Omit<TData, 'url'>;
@@ -213,18 +245,22 @@ export type OptionsLegacyParser<
213
245
  > = TData extends { body?: any }
214
246
  ? TData extends { headers?: any }
215
247
  ? OmitKeys<
216
- RequestOptions<TResponseStyle, ThrowOnError>,
248
+ RequestOptions<unknown, TResponseStyle, ThrowOnError>,
217
249
  'body' | 'headers' | 'url'
218
250
  > &
219
251
  TData
220
- : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'body' | 'url'> &
252
+ : OmitKeys<
253
+ RequestOptions<unknown, TResponseStyle, ThrowOnError>,
254
+ 'body' | 'url'
255
+ > &
221
256
  TData &
222
- Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'headers'>
257
+ Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'headers'>
223
258
  : TData extends { headers?: any }
224
259
  ? OmitKeys<
225
- RequestOptions<TResponseStyle, ThrowOnError>,
260
+ RequestOptions<unknown, TResponseStyle, ThrowOnError>,
226
261
  'headers' | 'url'
227
262
  > &
228
263
  TData &
229
- Pick<RequestOptions<TResponseStyle, ThrowOnError>, 'body'>
230
- : OmitKeys<RequestOptions<TResponseStyle, ThrowOnError>, 'url'> & TData;
264
+ Pick<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'body'>
265
+ : OmitKeys<RequestOptions<unknown, TResponseStyle, ThrowOnError>, 'url'> &
266
+ TData;