@vafast/api-client 0.1.2 → 0.1.4

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/README.md CHANGED
@@ -5,7 +5,7 @@
5
5
  ## ✨ 特性
6
6
 
7
7
  - 🔒 **完整类型推断** - 从路由定义自动推断 API 类型,无需手动定义接口
8
- - 🎯 **无需 `as const`** - 使用 `route()` 函数,类型自动保留
8
+ - 🎯 **无需 `as const`** - `defineRoutes()` 自动保留字面量类型
9
9
  - 🌊 **SSE 流式响应** - 内置 Server-Sent Events 支持,包含自动重连
10
10
  - ⏹️ **请求取消** - 支持 AbortController 取消进行中的请求
11
11
  - 🔗 **链式调用** - 优雅的 `api.users({ id }).posts.get()` 语法
@@ -15,8 +15,6 @@
15
15
 
16
16
  ```bash
17
17
  npm install @vafast/api-client
18
- # 或
19
- bun add @vafast/api-client
20
18
  ```
21
19
 
22
20
  ## 🚀 快速开始
@@ -25,39 +23,52 @@ bun add @vafast/api-client
25
23
 
26
24
  ```typescript
27
25
  // server.ts
28
- import { defineRoutes, route, createHandler, createSSEHandler, Type } from 'vafast'
26
+ import { defineRoutes, createHandler, createSSEHandler, Type } from 'vafast'
29
27
 
30
28
  export const routes = defineRoutes([
31
- // ✨ 使用 route() 函数,无需 as const
32
- route('GET', '/users', createHandler(
33
- { query: Type.Object({ page: Type.Optional(Type.Number()) }) },
34
- async ({ query }) => ({ users: [], total: 0, page: query.page ?? 1 })
35
- )),
36
-
37
- route('POST', '/users', createHandler(
38
- { body: Type.Object({ name: Type.String(), email: Type.String() }) },
39
- async ({ body }) => ({ id: crypto.randomUUID(), ...body })
40
- )),
41
-
42
- route('GET', '/users/:id', createHandler(
43
- { params: Type.Object({ id: Type.String() }) },
44
- async ({ params }) => ({ id: params.id, name: 'User' })
45
- )),
46
-
29
+ // ✨ defineRoutes() 自动保留字面量类型,无需 as const
30
+ {
31
+ method: 'GET',
32
+ path: '/users',
33
+ handler: createHandler(
34
+ { query: Type.Object({ page: Type.Optional(Type.Number()) }) },
35
+ async ({ query }) => ({ users: [], total: 0, page: query.page ?? 1 })
36
+ )
37
+ },
38
+ {
39
+ method: 'POST',
40
+ path: '/users',
41
+ handler: createHandler(
42
+ { body: Type.Object({ name: Type.String(), email: Type.String() }) },
43
+ async ({ body }) => ({ id: crypto.randomUUID(), ...body })
44
+ )
45
+ },
46
+ {
47
+ method: 'GET',
48
+ path: '/users/:id',
49
+ handler: createHandler(
50
+ { params: Type.Object({ id: Type.String() }) },
51
+ async ({ params }) => ({ id: params.id, name: 'User' })
52
+ )
53
+ },
47
54
  // 🌊 SSE 流式响应
48
- route('GET', '/chat/stream', createSSEHandler(
49
- { query: Type.Object({ prompt: Type.String() }) },
50
- async function* ({ query }) {
51
- yield { event: 'start', data: { message: 'Starting...' } }
52
-
53
- for (const word of query.prompt.split(' ')) {
54
- yield { data: { text: word } }
55
- await new Promise(r => setTimeout(r, 100))
55
+ {
56
+ method: 'GET',
57
+ path: '/chat/stream',
58
+ handler: createSSEHandler(
59
+ { query: Type.Object({ prompt: Type.String() }) },
60
+ async function* ({ query }) {
61
+ yield { event: 'start', data: { message: 'Starting...' } }
62
+
63
+ for (const word of query.prompt.split(' ')) {
64
+ yield { data: { text: word } }
65
+ await new Promise(r => setTimeout(r, 100))
66
+ }
67
+
68
+ yield { event: 'end', data: { message: 'Done!' } }
56
69
  }
57
-
58
- yield { event: 'end', data: { message: 'Done!' } }
59
- }
60
- ))
70
+ )
71
+ }
61
72
  ])
62
73
 
63
74
  // 导出类型供客户端使用
@@ -280,8 +291,6 @@ interface RequestConfig {
280
291
 
281
292
  ```bash
282
293
  npm test
283
- # 或
284
- bun test
285
294
  ```
286
295
 
287
296
  ## 📄 许可证
@@ -0,0 +1,296 @@
1
+ //#region src/types/index.d.ts
2
+ /**
3
+ * 类型定义
4
+ */
5
+ /** HTTP 方法 */
6
+ type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS';
7
+ /**
8
+ * 请求配置
9
+ */
10
+ interface RequestConfig {
11
+ /** 请求头 */
12
+ headers?: Record<string, string>;
13
+ /** 超时时间(毫秒) */
14
+ timeout?: number;
15
+ /** 取消信号 */
16
+ signal?: AbortSignal;
17
+ }
18
+ /**
19
+ * API 响应
20
+ */
21
+ interface ApiResponse<T = unknown> {
22
+ /** 响应数据 */
23
+ data: T | null;
24
+ /** 错误信息 */
25
+ error: Error | null;
26
+ /** HTTP 状态码 */
27
+ status: number;
28
+ /** 响应头 */
29
+ headers: Headers;
30
+ /** 原始响应对象 */
31
+ response: Response;
32
+ }
33
+ //#endregion
34
+ //#region src/core/eden.d.ts
35
+ interface EdenConfig {
36
+ headers?: Record<string, string>;
37
+ onRequest?: (request: Request) => Request | Promise<Request>;
38
+ onResponse?: <T>(response: ApiResponse<T>) => ApiResponse<T> | Promise<ApiResponse<T>>;
39
+ onError?: (error: Error) => void;
40
+ timeout?: number;
41
+ }
42
+ /**
43
+ * SSE 事件
44
+ */
45
+ interface SSEEvent<T = unknown> {
46
+ event?: string;
47
+ data: T;
48
+ id?: string;
49
+ retry?: number;
50
+ }
51
+ /**
52
+ * SSE 订阅选项
53
+ */
54
+ interface SSESubscribeOptions {
55
+ /** 自定义请求头 */
56
+ headers?: Record<string, string>;
57
+ /** 重连间隔(毫秒) */
58
+ reconnectInterval?: number;
59
+ /** 最大重连次数 */
60
+ maxReconnects?: number;
61
+ /** 连接超时(毫秒) */
62
+ timeout?: number;
63
+ }
64
+ /**
65
+ * SSE 订阅结果
66
+ */
67
+ interface SSESubscription<T = unknown> {
68
+ /** 取消订阅 */
69
+ unsubscribe: () => void;
70
+ /** 是否已连接 */
71
+ readonly connected: boolean;
72
+ }
73
+ /**
74
+ * 从 TypeBox Schema 提取静态类型
75
+ * 使用 TypeBox 内部的 static 属性提取类型
76
+ */
77
+ type InferStatic<T> = T extends {
78
+ static: infer S;
79
+ } ? S : T;
80
+ /** 从 InferableHandler 提取返回类型 */
81
+ type ExtractReturn<T> = T extends {
82
+ __returnType: infer R;
83
+ } ? R : unknown;
84
+ /** 从 InferableHandler 提取 Schema */
85
+ type ExtractSchema<T> = T extends {
86
+ __schema: infer S;
87
+ } ? S : {};
88
+ /** 检查是否是 SSE Handler(使用品牌类型检测) */
89
+ type IsSSEHandler<T> = T extends {
90
+ __sse: {
91
+ readonly __brand: 'SSE';
92
+ };
93
+ } ? true : false;
94
+ /** 从 Schema 提取各部分类型 */
95
+ type GetQuery<S> = S extends {
96
+ query: infer Q;
97
+ } ? InferStatic<Q> : undefined;
98
+ type GetBody<S> = S extends {
99
+ body: infer B;
100
+ } ? InferStatic<B> : undefined;
101
+ type GetParams<S> = S extends {
102
+ params: infer P;
103
+ } ? InferStatic<P> : undefined;
104
+ /** 移除开头斜杠:/users → users */
105
+ type TrimSlash<P$1 extends string> = P$1 extends `/${infer R}` ? R : P$1;
106
+ /** 检查是否是动态参数段::id → true */
107
+ type IsDynamicSegment<S extends string> = S extends `:${string}` ? true : false;
108
+ /** 清理 undefined 字段 */
109
+ type Clean<T> = { [K in keyof T as T[K] extends undefined ? never : K]: T[K] };
110
+ /** SSE 标记类型 */
111
+ type SSEBrand = {
112
+ readonly __brand: 'SSE';
113
+ };
114
+ /** 从路由构建方法定义 */
115
+ type BuildMethodDef<R extends {
116
+ readonly handler: unknown;
117
+ }> = IsSSEHandler<R['handler']> extends true ? Clean<{
118
+ query: GetQuery<ExtractSchema<R['handler']>>;
119
+ params: GetParams<ExtractSchema<R['handler']>>;
120
+ return: ExtractReturn<R['handler']>;
121
+ sse: SSEBrand;
122
+ }> : Clean<{
123
+ query: GetQuery<ExtractSchema<R['handler']>>;
124
+ body: GetBody<ExtractSchema<R['handler']>>;
125
+ params: GetParams<ExtractSchema<R['handler']>>;
126
+ return: ExtractReturn<R['handler']>;
127
+ }>;
128
+ /**
129
+ * 递归构建嵌套路径结构
130
+ *
131
+ * 处理动态参数:/users/:id → { users: { ':id': { ... } } }
132
+ */
133
+ type BuildPath<Path extends string, Method extends string, Def> = Path extends `${infer First}/${infer Rest}` ? IsDynamicSegment<First> extends true ? {
134
+ ':id': BuildPath<Rest, Method, Def>;
135
+ } : { [K in First]: BuildPath<Rest, Method, Def> } : IsDynamicSegment<Path> extends true ? {
136
+ ':id': { [M in Method]: Def };
137
+ } : { [K in Path]: { [M in Method]: Def } };
138
+ /**
139
+ * 从单个路由生成嵌套类型结构
140
+ */
141
+ type RouteToTree<R extends {
142
+ readonly method: string;
143
+ readonly path: string;
144
+ readonly handler: unknown;
145
+ }> = BuildPath<TrimSlash<R['path']>, Lowercase<R['method']>, BuildMethodDef<R>>;
146
+ type DeepMerge<A, B$1> = { [K in keyof A | keyof B$1]: K extends keyof A & keyof B$1 ? A[K] extends object ? B$1[K] extends object ? DeepMerge<A[K], B$1[K]> : A[K] & B$1[K] : A[K] & B$1[K] : K extends keyof A ? A[K] : K extends keyof B$1 ? B$1[K] : never };
147
+ /** 递归合并路由数组为单一类型结构 */
148
+ type MergeRoutes<T extends readonly unknown[]> = T extends readonly [infer First extends {
149
+ readonly method: string;
150
+ readonly path: string;
151
+ readonly handler: unknown;
152
+ }] ? RouteToTree<First> : T extends readonly [infer First extends {
153
+ readonly method: string;
154
+ readonly path: string;
155
+ readonly handler: unknown;
156
+ }, ...infer Rest extends readonly {
157
+ readonly method: string;
158
+ readonly path: string;
159
+ readonly handler: unknown;
160
+ }[]] ? DeepMerge<RouteToTree<First>, MergeRoutes<Rest>> : {};
161
+ /**
162
+ * 从 vafast 路由数组自动推断 Eden 契约
163
+ *
164
+ * @example
165
+ * ```typescript
166
+ * import { defineRoutes, createHandler, Type } from 'vafast'
167
+ * import { eden, InferEden } from 'vafast-api-client'
168
+ *
169
+ * // ✨ defineRoutes() 自动保留字面量类型,无需 as const
170
+ * const routes = defineRoutes([
171
+ * {
172
+ * method: 'GET',
173
+ * path: '/users',
174
+ * handler: createHandler(
175
+ * { query: Type.Object({ page: Type.Number() }) },
176
+ * async ({ query }) => ({ users: [], total: 0 })
177
+ * )
178
+ * },
179
+ * {
180
+ * method: 'GET',
181
+ * path: '/chat/stream',
182
+ * handler: createSSEHandler(
183
+ * { query: Type.Object({ prompt: Type.String() }) },
184
+ * async function* ({ query }) {
185
+ * yield { data: { text: 'Hello' } }
186
+ * }
187
+ * )
188
+ * }
189
+ * ])
190
+ *
191
+ * type Api = InferEden<typeof routes>
192
+ * const api = eden<Api>('http://localhost:3000')
193
+ *
194
+ * // 普通请求
195
+ * const { data } = await api.users.get({ page: 1 })
196
+ *
197
+ * // SSE 流式请求
198
+ * api.chat.stream.subscribe({ prompt: 'Hi' }, {
199
+ * onMessage: (data) => console.log(data),
200
+ * onError: (err) => console.error(err)
201
+ * })
202
+ * ```
203
+ */
204
+ type InferEden<T extends readonly {
205
+ readonly method: string;
206
+ readonly path: string;
207
+ readonly handler: unknown;
208
+ }[]> = MergeRoutes<T>;
209
+ /** HTTP 方法定义 */
210
+ interface MethodDef {
211
+ query?: unknown;
212
+ body?: unknown;
213
+ params?: unknown;
214
+ return: unknown;
215
+ sse?: SSEBrand;
216
+ }
217
+ /** SSE 订阅回调 */
218
+ interface SSECallbacks<T> {
219
+ /** 收到消息 */
220
+ onMessage: (data: T) => void;
221
+ /** 发生错误 */
222
+ onError?: (error: Error) => void;
223
+ /** 连接打开 */
224
+ onOpen?: () => void;
225
+ /** 连接关闭 */
226
+ onClose?: () => void;
227
+ /** 正在重连 */
228
+ onReconnect?: (attempt: number, maxAttempts: number) => void;
229
+ /** 达到最大重连次数 */
230
+ onMaxReconnects?: () => void;
231
+ }
232
+ /** 从方法定义提取调用签名 */
233
+ type MethodCall<M$1 extends MethodDef, HasParams extends boolean = false> = M$1 extends {
234
+ sse: SSEBrand;
235
+ } ? M$1 extends {
236
+ query: infer Q;
237
+ } ? (query: Q, callbacks: SSECallbacks<M$1['return']>, options?: SSESubscribeOptions) => SSESubscription<M$1['return']> : (callbacks: SSECallbacks<M$1['return']>, options?: SSESubscribeOptions) => SSESubscription<M$1['return']> : HasParams extends true ? M$1 extends {
238
+ body: infer B;
239
+ } ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : (config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : M$1 extends {
240
+ query: infer Q;
241
+ } ? (query?: Q, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : M$1 extends {
242
+ body: infer B;
243
+ } ? (body: B, config?: RequestConfig) => Promise<ApiResponse<M$1['return']>> : (config?: RequestConfig) => Promise<ApiResponse<M$1['return']>>;
244
+ /** 检查是否是 SSE 端点(检测品牌类型标记) */
245
+ type IsSSEEndpoint<M$1> = M$1 extends {
246
+ sse: {
247
+ readonly __brand: 'SSE';
248
+ };
249
+ } ? true : false;
250
+ /** 端点类型 - 包含 subscribe 方法用于 SSE */
251
+ type Endpoint<T, HasParams extends boolean = false> = { [K in 'get' | 'post' | 'put' | 'patch' | 'delete' as T extends { [P in K]: MethodDef } ? K : never]: T extends { [P in K]: infer M extends MethodDef } ? MethodCall<M, HasParams> : never } & (T extends {
252
+ get: infer M extends MethodDef;
253
+ } ? IsSSEEndpoint<M> extends true ? {
254
+ subscribe: MethodCall<M, HasParams>;
255
+ } : {} : {});
256
+ /** HTTP 方法名 */
257
+ type HTTPMethods = 'get' | 'post' | 'put' | 'patch' | 'delete';
258
+ /** 递归构建客户端类型 */
259
+ type EdenClient<T, HasParams extends boolean = false> = { [K in keyof T as K extends HTTPMethods | `:${string}` ? never : K]: T[K] extends {
260
+ ':id': infer Child;
261
+ } ? ((params: Record<string, string>) => EdenClient<Child, true>) & EdenClient<T[K], false> : EdenClient<T[K], false> } & Endpoint<T, HasParams>;
262
+ /**
263
+ * 创建 Eden 风格的类型安全 API 客户端
264
+ *
265
+ * @param baseURL - API 基础 URL
266
+ * @param config - 可选配置
267
+ * @returns 类型安全的 API 客户端
268
+ *
269
+ * @example
270
+ * ```typescript
271
+ * // 使用自动推断的契约(无需 as const)
272
+ * const routes = defineRoutes([
273
+ * route('GET', '/users', createHandler(...)),
274
+ * route('GET', '/chat/stream', createSSEHandler(...))
275
+ * ])
276
+ *
277
+ * type Api = InferEden<typeof routes>
278
+ * const api = eden<Api>('http://localhost:3000')
279
+ *
280
+ * // 普通请求
281
+ * const { data } = await api.users.get({ page: 1 })
282
+ *
283
+ * // SSE 流式请求
284
+ * const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
285
+ * onMessage: (data) => console.log(data),
286
+ * onError: (err) => console.error(err)
287
+ * })
288
+ *
289
+ * // 取消订阅
290
+ * sub.unsubscribe()
291
+ * ```
292
+ */
293
+ declare function eden<T>(baseURL: string, config?: EdenConfig): EdenClient<T>;
294
+ //#endregion
295
+ export { type ApiResponse, type EdenClient, type EdenConfig, type HTTPMethod, type InferEden, type RequestConfig, type SSEEvent, type SSESubscribeOptions, type SSESubscription, eden };
296
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.mts","names":[],"sources":["../src/types/index.ts","../src/core/eden.ts"],"sourcesContent":[],"mappings":"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"}
package/dist/index.mjs ADDED
@@ -0,0 +1,226 @@
1
+ //#region src/core/eden.ts
2
+ /**
3
+ * 解析 SSE 事件流
4
+ */
5
+ async function* parseSSEStream(reader) {
6
+ const decoder = new TextDecoder();
7
+ let buffer = "";
8
+ while (true) {
9
+ const { done, value } = await reader.read();
10
+ if (done) break;
11
+ buffer += decoder.decode(value, { stream: true });
12
+ const events = buffer.split("\n\n");
13
+ buffer = events.pop() || "";
14
+ for (const eventStr of events) {
15
+ if (!eventStr.trim()) continue;
16
+ const event = { data: "" };
17
+ const lines = eventStr.split("\n");
18
+ let dataLines = [];
19
+ for (const line of lines) if (line.startsWith("event:")) event.event = line.slice(6).trim();
20
+ else if (line.startsWith("data:")) dataLines.push(line.slice(5).trim());
21
+ else if (line.startsWith("id:")) event.id = line.slice(3).trim();
22
+ else if (line.startsWith("retry:")) event.retry = parseInt(line.slice(6).trim(), 10);
23
+ const dataStr = dataLines.join("\n");
24
+ try {
25
+ event.data = JSON.parse(dataStr);
26
+ } catch {
27
+ event.data = dataStr;
28
+ }
29
+ yield event;
30
+ }
31
+ }
32
+ }
33
+ /**
34
+ * 创建 Eden 风格的类型安全 API 客户端
35
+ *
36
+ * @param baseURL - API 基础 URL
37
+ * @param config - 可选配置
38
+ * @returns 类型安全的 API 客户端
39
+ *
40
+ * @example
41
+ * ```typescript
42
+ * // 使用自动推断的契约(无需 as const)
43
+ * const routes = defineRoutes([
44
+ * route('GET', '/users', createHandler(...)),
45
+ * route('GET', '/chat/stream', createSSEHandler(...))
46
+ * ])
47
+ *
48
+ * type Api = InferEden<typeof routes>
49
+ * const api = eden<Api>('http://localhost:3000')
50
+ *
51
+ * // 普通请求
52
+ * const { data } = await api.users.get({ page: 1 })
53
+ *
54
+ * // SSE 流式请求
55
+ * const sub = api.chat.stream.subscribe({ prompt: 'Hello' }, {
56
+ * onMessage: (data) => console.log(data),
57
+ * onError: (err) => console.error(err)
58
+ * })
59
+ *
60
+ * // 取消订阅
61
+ * sub.unsubscribe()
62
+ * ```
63
+ */
64
+ function eden(baseURL, config) {
65
+ const { headers: defaultHeaders, onRequest, onResponse, onError, timeout } = config ?? {};
66
+ async function request(method, path, data, requestConfig) {
67
+ const url = new URL(path, baseURL);
68
+ if (method === "GET" && data && typeof data === "object") {
69
+ for (const [key, value] of Object.entries(data)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
70
+ }
71
+ const headers = {
72
+ "Content-Type": "application/json",
73
+ ...defaultHeaders,
74
+ ...requestConfig?.headers
75
+ };
76
+ const controller = new AbortController();
77
+ let timeoutId;
78
+ const userSignal = requestConfig?.signal;
79
+ const requestTimeout = requestConfig?.timeout ?? timeout;
80
+ if (userSignal) if (userSignal.aborted) controller.abort();
81
+ else userSignal.addEventListener("abort", () => controller.abort());
82
+ if (requestTimeout) timeoutId = setTimeout(() => controller.abort(), requestTimeout);
83
+ const fetchOptions = {
84
+ method,
85
+ headers,
86
+ signal: controller.signal
87
+ };
88
+ if (method !== "GET" && method !== "HEAD" && data) fetchOptions.body = JSON.stringify(data);
89
+ let req = new Request(url.toString(), fetchOptions);
90
+ if (onRequest) req = await onRequest(req);
91
+ try {
92
+ const response = await fetch(req);
93
+ if (timeoutId) clearTimeout(timeoutId);
94
+ const contentType = response.headers.get("content-type");
95
+ let responseData = null;
96
+ if (contentType?.includes("application/json")) responseData = await response.json();
97
+ else if (contentType?.includes("text/")) responseData = await response.text();
98
+ let result = {
99
+ data: responseData,
100
+ error: response.ok ? null : /* @__PURE__ */ new Error(`HTTP ${response.status}`),
101
+ status: response.status,
102
+ headers: response.headers,
103
+ response
104
+ };
105
+ if (onResponse) result = await onResponse(result);
106
+ if (!response.ok && onError) onError(result.error);
107
+ return result;
108
+ } catch (error) {
109
+ const err = error instanceof Error ? error : new Error(String(error));
110
+ if (onError) onError(err);
111
+ return {
112
+ data: null,
113
+ error: err,
114
+ status: 0,
115
+ headers: new Headers(),
116
+ response: new Response()
117
+ };
118
+ }
119
+ }
120
+ function subscribe(path, query, callbacks, options) {
121
+ const url = new URL(path, baseURL);
122
+ if (query) {
123
+ for (const [key, value] of Object.entries(query)) if (value !== void 0 && value !== null) url.searchParams.set(key, String(value));
124
+ }
125
+ let abortController = new AbortController();
126
+ let connected = false;
127
+ let reconnectCount = 0;
128
+ let isUnsubscribed = false;
129
+ let lastEventId;
130
+ const reconnectInterval = options?.reconnectInterval ?? 3e3;
131
+ const maxReconnects = options?.maxReconnects ?? 5;
132
+ const connect = async () => {
133
+ if (isUnsubscribed) return;
134
+ try {
135
+ abortController = new AbortController();
136
+ const headers = {
137
+ "Accept": "text/event-stream",
138
+ ...defaultHeaders,
139
+ ...options?.headers
140
+ };
141
+ if (lastEventId) headers["Last-Event-ID"] = lastEventId;
142
+ const response = await fetch(url.toString(), {
143
+ method: "GET",
144
+ headers,
145
+ signal: abortController.signal
146
+ });
147
+ if (!response.ok) throw new Error(`HTTP ${response.status}`);
148
+ if (!response.body) throw new Error("No response body");
149
+ connected = true;
150
+ reconnectCount = 0;
151
+ callbacks.onOpen?.();
152
+ const reader = response.body.getReader();
153
+ for await (const event of parseSSEStream(reader)) {
154
+ if (event.id) lastEventId = event.id;
155
+ if (event.event === "error") callbacks.onError?.(new Error(String(event.data)));
156
+ else callbacks.onMessage(event.data);
157
+ }
158
+ connected = false;
159
+ callbacks.onClose?.();
160
+ } catch (error) {
161
+ connected = false;
162
+ if (error.name === "AbortError" || isUnsubscribed) return;
163
+ callbacks.onError?.(error);
164
+ if (reconnectCount < maxReconnects) {
165
+ reconnectCount++;
166
+ callbacks.onReconnect?.(reconnectCount, maxReconnects);
167
+ setTimeout(() => {
168
+ if (!isUnsubscribed) connect();
169
+ }, reconnectInterval);
170
+ } else callbacks.onMaxReconnects?.();
171
+ }
172
+ };
173
+ connect();
174
+ return {
175
+ unsubscribe: () => {
176
+ isUnsubscribed = true;
177
+ abortController?.abort();
178
+ abortController = null;
179
+ connected = false;
180
+ },
181
+ get connected() {
182
+ return connected;
183
+ }
184
+ };
185
+ }
186
+ function createEndpoint(basePath) {
187
+ const methods = [
188
+ "get",
189
+ "post",
190
+ "put",
191
+ "patch",
192
+ "delete"
193
+ ];
194
+ const handler = (params) => {
195
+ const paramValue = Object.values(params)[0];
196
+ return createEndpoint(`${basePath}/${encodeURIComponent(paramValue)}`);
197
+ };
198
+ return new Proxy(handler, {
199
+ get(_, prop) {
200
+ if (methods.includes(prop)) {
201
+ const httpMethod = prop.toUpperCase();
202
+ return (data, cfg) => {
203
+ return request(httpMethod, basePath, data, cfg);
204
+ };
205
+ }
206
+ if (prop === "subscribe") return (queryOrCallbacks, callbacksOrOptions, options) => {
207
+ if (typeof queryOrCallbacks === "object" && "onMessage" in queryOrCallbacks) return subscribe(basePath, void 0, queryOrCallbacks, callbacksOrOptions);
208
+ else return subscribe(basePath, queryOrCallbacks, callbacksOrOptions, options);
209
+ };
210
+ return createEndpoint(`${basePath}/${prop}`);
211
+ },
212
+ apply(_, __, args) {
213
+ const params = args[0];
214
+ const paramValue = Object.values(params)[0];
215
+ return createEndpoint(`${basePath}/${encodeURIComponent(paramValue)}`);
216
+ }
217
+ });
218
+ }
219
+ return new Proxy({}, { get(_, prop) {
220
+ return createEndpoint(`/${prop}`);
221
+ } });
222
+ }
223
+
224
+ //#endregion
225
+ export { eden };
226
+ //# sourceMappingURL=index.mjs.map