@bagelink/sdk 1.15.558 → 1.15.562

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/dist/index.mjs CHANGED
@@ -1,6 +1,262 @@
1
- import axios$1 from 'axios';
1
+ import ax from 'axios';
2
2
  import { ref } from 'vue';
3
3
 
4
+ class StreamController {
5
+ constructor(cleanup) {
6
+ this.cleanup = cleanup;
7
+ }
8
+ handlers = /* @__PURE__ */ new Map();
9
+ _closed = false;
10
+ /**
11
+ * Register an event handler (fully typed!)
12
+ * @param event - Event type to listen for
13
+ * @param handler - Handler function (data type inferred from event)
14
+ * @returns this (for chaining)
15
+ */
16
+ on(event, handler) {
17
+ if (!this.handlers.has(event)) {
18
+ this.handlers.set(event, /* @__PURE__ */ new Set());
19
+ }
20
+ this.handlers.get(event).add(handler);
21
+ return this;
22
+ }
23
+ /**
24
+ * Emit an event to all registered handlers
25
+ * @internal
26
+ */
27
+ emit(event, data) {
28
+ const handlers = this.handlers.get(event);
29
+ if (handlers) {
30
+ handlers.forEach((handler) => {
31
+ try {
32
+ handler(data);
33
+ } catch (error) {
34
+ console.error(`Error in ${event} handler:`, error);
35
+ }
36
+ });
37
+ }
38
+ }
39
+ /**
40
+ * Close the stream
41
+ */
42
+ close() {
43
+ if (!this._closed) {
44
+ this.cleanup();
45
+ this._closed = true;
46
+ }
47
+ }
48
+ }
49
+
50
+ async function resolveHeaders(headers) {
51
+ return typeof headers === "function" ? await headers() : headers ?? {};
52
+ }
53
+ function createSSEStream(url, options = {}) {
54
+ const {
55
+ headers,
56
+ withCredentials = true,
57
+ lastEventId: initialLastEventId,
58
+ maxReconnects = 5,
59
+ reconnectDelayMs = 1e3
60
+ } = options;
61
+ const controller = new AbortController();
62
+ const stream = new StreamController(() => {
63
+ controller.abort();
64
+ });
65
+ let lastEventId = initialLastEventId;
66
+ let reconnectAttempts = 0;
67
+ async function connect() {
68
+ if (controller.signal.aborted) return;
69
+ const resolvedHeaders = await resolveHeaders(headers);
70
+ const requestHeaders = {
71
+ "Accept": "text/event-stream",
72
+ "Cache-Control": "no-cache",
73
+ ...resolvedHeaders
74
+ };
75
+ if (lastEventId !== void 0) {
76
+ requestHeaders["Last-Event-ID"] = lastEventId;
77
+ }
78
+ let response;
79
+ try {
80
+ response = await fetch(url, {
81
+ method: "GET",
82
+ headers: requestHeaders,
83
+ credentials: withCredentials ? "include" : "same-origin",
84
+ signal: controller.signal,
85
+ keepalive: false
86
+ });
87
+ } catch (error) {
88
+ if (error?.name === "AbortError") return;
89
+ return handleDisconnect(error);
90
+ }
91
+ if (!response.ok) {
92
+ stream.emit("error", new Error(`HTTP ${response.status}: ${response.statusText}`));
93
+ return;
94
+ }
95
+ if (!response.body) {
96
+ stream.emit("error", new Error("Response body is null"));
97
+ return;
98
+ }
99
+ const reader = response.body.getReader();
100
+ const decoder = new TextDecoder();
101
+ let buffer = "";
102
+ let terminalEventReceived = false;
103
+ let currentEvent = "message";
104
+ let currentData = "";
105
+ let currentId;
106
+ try {
107
+ while (true) {
108
+ const { done, value } = await reader.read();
109
+ if (done) {
110
+ if (!controller.signal.aborted && !terminalEventReceived) {
111
+ return handleDisconnect(new Error("Stream ended before a terminal event"));
112
+ }
113
+ return;
114
+ }
115
+ buffer += decoder.decode(value, { stream: true });
116
+ const lines = buffer.split("\n");
117
+ buffer = lines.pop() || "";
118
+ for (const line of lines) {
119
+ if (line.startsWith("id:")) {
120
+ currentId = line.slice(3).trim();
121
+ } else if (line.startsWith("event:")) {
122
+ currentEvent = line.slice(6).trim();
123
+ } else if (line.startsWith("data:")) {
124
+ currentData = currentData ? `${currentData}
125
+ ${line.slice(5).trim()}` : line.slice(5).trim();
126
+ } else if (line === "" && currentData) {
127
+ if (currentId !== void 0) {
128
+ if (currentId !== lastEventId) reconnectAttempts = 0;
129
+ lastEventId = currentId;
130
+ }
131
+ try {
132
+ const parsed = JSON.parse(currentData);
133
+ stream.emit(currentEvent, parsed);
134
+ if (currentEvent === "done" || currentEvent === "error") {
135
+ terminalEventReceived = true;
136
+ stream.emit("complete", {});
137
+ controller.abort();
138
+ try {
139
+ await reader.cancel();
140
+ } catch {
141
+ }
142
+ return;
143
+ }
144
+ } catch {
145
+ stream.emit(currentEvent, currentData);
146
+ }
147
+ currentEvent = "message";
148
+ currentData = "";
149
+ currentId = void 0;
150
+ }
151
+ }
152
+ }
153
+ } catch (error) {
154
+ if (error?.name !== "AbortError") {
155
+ return handleDisconnect(error);
156
+ }
157
+ } finally {
158
+ try {
159
+ reader.releaseLock();
160
+ } catch {
161
+ }
162
+ }
163
+ }
164
+ async function handleDisconnect(error) {
165
+ if (controller.signal.aborted) return;
166
+ if (reconnectAttempts >= maxReconnects) {
167
+ const msg = error?.message || "Stream connection lost";
168
+ stream.emit("error", new Error(`${msg} (gave up after ${maxReconnects} reconnects)`));
169
+ return;
170
+ }
171
+ reconnectAttempts++;
172
+ const delay = reconnectDelayMs * Math.pow(2, reconnectAttempts - 1);
173
+ await new Promise((resolve) => setTimeout(resolve, delay));
174
+ if (!controller.signal.aborted) await connect();
175
+ }
176
+ connect().catch(() => {
177
+ });
178
+ return stream;
179
+ }
180
+ function createSSEStreamPost(url, body, options = {}) {
181
+ const { headers, withCredentials = true } = options;
182
+ const controller = new AbortController();
183
+ const stream = new StreamController(() => {
184
+ controller.abort();
185
+ });
186
+ resolveHeaders(headers).then((resolvedHeaders) => fetch(url, {
187
+ method: "POST",
188
+ headers: {
189
+ "Accept": "text/event-stream",
190
+ "Content-Type": "application/json",
191
+ "Cache-Control": "no-cache",
192
+ ...resolvedHeaders
193
+ },
194
+ credentials: withCredentials ? "include" : "same-origin",
195
+ body: JSON.stringify(body),
196
+ signal: controller.signal,
197
+ // Disable keepalive for streaming
198
+ keepalive: false
199
+ })).then(async (response) => {
200
+ if (!response.ok) {
201
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
202
+ }
203
+ if (!response.body) {
204
+ throw new Error("Response body is null");
205
+ }
206
+ const reader = response.body.getReader();
207
+ const decoder = new TextDecoder();
208
+ let buffer = "";
209
+ let currentEvent = "message";
210
+ let currentData = "";
211
+ try {
212
+ while (true) {
213
+ const { done, value } = await reader.read();
214
+ if (done) {
215
+ stream.emit("done", {});
216
+ stream.emit("complete", {});
217
+ break;
218
+ }
219
+ buffer += decoder.decode(value, { stream: true });
220
+ const lines = buffer.split("\n");
221
+ buffer = lines.pop() || "";
222
+ for (const line of lines) {
223
+ if (line.startsWith("event:")) {
224
+ currentEvent = line.slice(6).trim();
225
+ } else if (line.startsWith("data:")) {
226
+ currentData = currentData ? `${currentData}
227
+ ${line.slice(5).trim()}` : line.slice(5).trim();
228
+ } else if (line === "" && currentData) {
229
+ try {
230
+ const parsed = JSON.parse(currentData);
231
+ stream.emit(currentEvent, parsed);
232
+ } catch {
233
+ stream.emit(currentEvent, currentData);
234
+ }
235
+ currentEvent = "message";
236
+ currentData = "";
237
+ }
238
+ }
239
+ }
240
+ } catch (error) {
241
+ if (error?.name !== "AbortError") {
242
+ const errorMessage = error?.message || "Stream connection error";
243
+ stream.emit("error", new Error(errorMessage));
244
+ }
245
+ } finally {
246
+ try {
247
+ reader.releaseLock();
248
+ } catch {
249
+ }
250
+ }
251
+ }).catch((error) => {
252
+ if (error?.name !== "AbortError") {
253
+ const errorMessage = error?.message || "Failed to establish stream connection";
254
+ stream.emit("error", new Error(errorMessage));
255
+ }
256
+ });
257
+ return stream;
258
+ }
259
+
4
260
  function isSSEStream(operation) {
5
261
  const responses = operation.responses || {};
6
262
  for (const [statusCode, response] of Object.entries(responses)) {
@@ -605,7 +861,6 @@ function generateStreamFunction(method, path, formattedPath, allParams, requestB
605
861
  allParams = "";
606
862
  }
607
863
  const bodyVar = requestBodyPayload || "{}";
608
- const baseUrlRef = "getStreamBaseURL()";
609
864
  const streamTypeName = generateStreamTypeName(path);
610
865
  if (eventTypes?.length) {
611
866
  streamEventTypes[streamTypeName] = eventTypes;
@@ -615,7 +870,6 @@ function generateStreamFunction(method, path, formattedPath, allParams, requestB
615
870
  const eventTypesExample = eventTypes?.length ? eventTypes.map((t) => `
616
871
  * .on('${t}', (data) => console.log(data))`).join("") : "\n * .on('message', (data) => console.log(data))";
617
872
  const typeAnnotation = eventTypes?.length ? `StreamController<${streamTypeName}>` : "StreamController";
618
- const pathForUrl = formattedPath.startsWith("`") ? formattedPath.slice(1, -1) : formattedPath.slice(1, -1);
619
873
  const streamParams = allParams ? `${allParams}, options?: SSEStreamOptions` : `options?: SSEStreamOptions`;
620
874
  const regularParams = allParams;
621
875
  if (method === "post") {
@@ -632,14 +886,13 @@ function generateStreamFunction(method, path, formattedPath, allParams, requestB
632
886
  * stream.close()
633
887
  */
634
888
  stream: (${streamParams}): ${typeAnnotation} => {
635
- const url = \`\${${baseUrlRef}}${pathForUrl}\`
636
- return createSSEStreamPost<${streamTypeName}>(url, ${bodyVar}, options)
889
+ return __transport.createSSEStreamPost<${streamTypeName}>(${formattedPath}, ${bodyVar}, options)
637
890
  },
638
891
  /**
639
892
  * Regular POST request (non-streaming)
640
893
  */
641
894
  post: async (${regularParams}): Promise<AxiosResponse<any>> => {
642
- return axios.post(${formattedPath}, ${bodyVar})
895
+ return __transport.axios.post(${formattedPath}, ${bodyVar})
643
896
  }
644
897
  }`;
645
898
  } else {
@@ -660,7 +913,7 @@ function generateStreamFunction(method, path, formattedPath, allParams, requestB
660
913
  ].join("\n ");
661
914
  }
662
915
  const regularParamStr = queryParams?.length ? `, { params: {${queryParams.join(", ")}} }` : "";
663
- const urlLine = queryParams?.length ? `const url = \`\${${baseUrlRef}}${pathForUrl}\` + __qsSuffix` : `const url = \`\${${baseUrlRef}}${pathForUrl}\``;
916
+ const urlLine = queryParams?.length ? `const url = ${formattedPath} + __qsSuffix` : `const url = ${formattedPath}`;
664
917
  const streamBody = queryParams?.length ? `
665
918
  ${queryStringCode}
666
919
  ${urlLine}` : `
@@ -678,13 +931,13 @@ function generateStreamFunction(method, path, formattedPath, allParams, requestB
678
931
  * stream.close()
679
932
  */
680
933
  stream: (${streamParams}): ${typeAnnotation} => {${streamBody}
681
- return createSSEStream<${streamTypeName}>(url, options)
934
+ return __transport.createSSEStream<${streamTypeName}>(url, options)
682
935
  },
683
936
  /**
684
937
  * Regular GET request (non-streaming)
685
938
  */
686
939
  get: async (${regularParams}): Promise<AxiosResponse<any>> => {
687
- return axios.get(${formattedPath}${regularParamStr})
940
+ return __transport.axios.get(${formattedPath}${regularParamStr})
688
941
  }
689
942
  }`;
690
943
  }
@@ -706,14 +959,14 @@ function generateAxiosFunction(method, formattedPath, allParams, responseTypeStr
706
959
  const formData = new FormData()
707
960
  formData.append('file', file)
708
961
  formData.append('upload', file)
709
- const response = await axios.${method}(${formattedPath}, formData, {
962
+ const response = await __transport.axios.${method}(${formattedPath}, formData, {
710
963
  headers: { 'Content-Type': 'multipart/form-data' },
711
964
  onUploadProgress: options?.onUploadProgress${paramStr ? `, ${paramStr}` : ""}
712
965
  })
713
966
  return ${returnExpression}`;
714
967
  } else {
715
968
  axiosFunction += `
716
- const response = await axios.${method}(${formattedPath}, formData, {
969
+ const response = await __transport.axios.${method}(${formattedPath}, formData, {
717
970
  headers: { 'Content-Type': 'multipart/form-data' }${paramStr ? `, ${paramStr}` : ""}
718
971
  })
719
972
  return ${returnExpression}`;
@@ -722,7 +975,7 @@ function generateAxiosFunction(method, formattedPath, allParams, responseTypeStr
722
975
  const configParams = paramStr ? `, { ${paramStr} }` : "";
723
976
  const bodyVar = requestBodyPayload || "{}";
724
977
  axiosFunction += `
725
- const response = await axios.${method}(${formattedPath}${["get", "delete"].includes(method) ? configParams : `, ${bodyVar}${configParams}`})
978
+ const response = await __transport.axios.${method}(${formattedPath}${["get", "delete"].includes(method) ? configParams : `, ${bodyVar}${configParams}`})
726
979
  return ${returnExpression}`;
727
980
  }
728
981
  axiosFunction += "}";
@@ -860,9 +1113,15 @@ function handlePathSegment(path, operation, existingObj = {}) {
860
1113
  }
861
1114
  return { ...obj, ...existingObj };
862
1115
  }
863
- function generateFunctions(paths, baseUrl) {
1116
+ function generateFunctionsInternal(paths, baseUrl, moduleFactoryName) {
1117
+ allTypes.length = 0;
1118
+ pathOperations.length = 0;
1119
+ for (const inventory of [functionsInventory, streamEventTypes, streamEventInfo, streamEventSchemas]) {
1120
+ for (const key of Object.keys(inventory)) delete inventory[key];
1121
+ }
864
1122
  const body = {};
865
1123
  const allPathsClean = Object.keys(paths).map(cleanPath);
1124
+ const httpMethods = /* @__PURE__ */ new Set(["get", "put", "post", "delete", "options", "head", "patch", "trace"]);
866
1125
  for (const [path, operation] of Object.entries(paths)) {
867
1126
  const splitPath = path.split("/").filter((p) => p && !/\{|\}/.test(p));
868
1127
  splitPath.reduce((acc, key, index, array) => {
@@ -870,7 +1129,8 @@ function generateFunctions(paths, baseUrl) {
870
1129
  if (!objFuncKey) {
871
1130
  return acc;
872
1131
  }
873
- const methods = Object.keys(operation);
1132
+ const methods = Object.keys(operation).filter((method) => httpMethods.has(method));
1133
+ if (methods.length === 0) return acc;
874
1134
  const currentPathClean = cleanPath(path);
875
1135
  const currentPathPrefix = splitPath.slice(0, index + 1).join("/");
876
1136
  const prefixWithSlash = `${currentPathPrefix}/`;
@@ -891,14 +1151,14 @@ function generateFunctions(paths, baseUrl) {
891
1151
  return acc[objFuncKey];
892
1152
  }, body);
893
1153
  }
894
- let tsString = "";
1154
+ let tsString = "function createEndpointNamespaces(__transport: ApiTransport) {\n";
895
1155
  const apiObjectEntries = [];
896
1156
  for (const [parent, object] of Object.entries(body)) {
897
1157
  tsString += `
898
1158
  /**
899
1159
  * API functions for ${parent} endpoints
900
1160
  */
901
- export const ${parent} = ${JSON.stringify(object, void 0, 2)};
1161
+ const ${parent} = ${JSON.stringify(object, void 0, 2)};
902
1162
  `;
903
1163
  apiObjectEntries.push(parent);
904
1164
  }
@@ -906,14 +1166,22 @@ export const ${parent} = ${JSON.stringify(object, void 0, 2)};
906
1166
  /**
907
1167
  * Aggregated API object containing all endpoints
908
1168
  */
909
- export const api = {
1169
+ const api = {
910
1170
  ${apiObjectEntries.map((name) => ` ${name},`).join("\n")}
911
1171
  };
1172
+ return { api, ${apiObjectEntries.join(", ")} }
1173
+ }
912
1174
  `;
913
1175
  Object.entries(functionsInventory).forEach(([key, value]) => {
914
1176
  tsString = tsString.replace(`"${key}"`, value);
915
1177
  });
916
- return fileTemplate(tsString, allTypes, baseUrl);
1178
+ return moduleFactoryName ? moduleFileTemplate(tsString, allTypes, moduleFactoryName) : fileTemplate(tsString, allTypes, baseUrl, apiObjectEntries);
1179
+ }
1180
+ function generateFunctions(paths, baseUrl) {
1181
+ return generateFunctionsInternal(paths, baseUrl);
1182
+ }
1183
+ function generateModuleFunctions(paths, baseUrl, moduleFactoryName) {
1184
+ return generateFunctionsInternal(paths, baseUrl, moduleFactoryName);
917
1185
  }
918
1186
  function generateStreamEventTypeDefinitions() {
919
1187
  if (Object.keys(streamEventTypes).length === 0) {
@@ -998,18 +1266,16 @@ function generateStreamEventTypeDefinitions() {
998
1266
  }
999
1267
  return typeDefs;
1000
1268
  }
1001
- function fileTemplate(tsString, typeForImport, baseURL) {
1269
+ function fileTemplate(tsString, typeForImport, baseURL, apiObjectEntries) {
1002
1270
  const streamTypeDefs = generateStreamEventTypeDefinitions();
1003
1271
  const hasStreamEndpoints = Object.keys(streamEventTypes).length > 0;
1004
- const allFunctionBodies = Object.values(functionsInventory).join("\n");
1005
- const hasGetStreams = allFunctionBodies.includes("createSSEStream<");
1006
- const hasPostStreams = allFunctionBodies.includes("createSSEStreamPost<");
1007
- const streamImports = hasStreamEndpoints ? `${hasPostStreams ? "createSSEStreamPost, " : ""}${hasGetStreams ? "createSSEStream, " : ""}type StreamController, type SSEStreamOptions,` : "";
1008
- const templateCode = `import ax from 'axios';
1009
- import type { AxiosResponse } from 'axios';
1272
+ const streamImports = hasStreamEndpoints ? `type StreamController, type SSEStreamOptions,` : "";
1273
+ const templateCode = `import type { AxiosResponse } from 'axios';
1010
1274
  import type { ${typeForImport.join(", ")} } from './types.d';
1011
1275
  import {
1012
1276
  ApiResponse,
1277
+ createApiTransport,
1278
+ configureApiTransport,
1013
1279
  parseApiError,
1014
1280
  formatFieldErrors,
1015
1281
  useApiRequest,
@@ -1024,9 +1290,10 @@ import {
1024
1290
  type ApiRequestState,
1025
1291
  type RequestInterceptor,
1026
1292
  type ResponseInterceptor,
1293
+ type ApiTransport,
1027
1294
  type Unwrapped
1028
1295
  } from '@bagelink/sdk';
1029
- import { ref, type Ref } from 'vue';
1296
+ import type { Ref } from 'vue';
1030
1297
 
1031
1298
  /**
1032
1299
  * Options for file upload operations
@@ -1042,22 +1309,29 @@ export interface UploadOptions {
1042
1309
 
1043
1310
  ${streamTypeDefs}
1044
1311
 
1045
- /**
1046
- * Configured axios instance for API requests
1047
- */
1048
- export const axios = ax.create({baseURL: '${baseURL}', withCredentials: true, paramsSerializer: { indexes: null }});
1312
+ const defaultTransport = createApiTransport({ baseURL: '${baseURL}' })
1049
1313
 
1050
1314
  ${tsString}
1051
1315
 
1052
- // ============================================================================
1316
+ const defaultBindings = createEndpointNamespaces(defaultTransport)
1317
+ export const api = defaultBindings.api
1318
+ ${apiObjectEntries.map((name) => `export const ${name} = defaultBindings.${name}`).join("\n")}
1319
+ export const axios = defaultTransport.axios
1320
+
1321
+ /** Bind all generated endpoint namespaces to an isolated transport. */
1322
+ export function createApiClient(transport: ApiTransport) {
1323
+ return createEndpointNamespaces(transport)
1324
+ }
1325
+
1326
+ /** Resolve the default client's SSE base URL. */
1327
+ export function getStreamBaseURL(): string {
1328
+ return defaultTransport.getStreamBaseURL()
1329
+ }
1330
+
1331
+ // ============================================================================
1053
1332
  // API Instance & Configuration
1054
1333
  // ============================================================================
1055
1334
 
1056
- /**
1057
- * Last response metadata (accessible via useApi().$lastResponse)
1058
- */
1059
- const lastResponse = ref<AxiosResponse | null>(null)
1060
-
1061
1335
  /**
1062
1336
  * API instance type with enhanced response objects
1063
1337
  */
@@ -1071,13 +1345,6 @@ type ApiInstance = typeof api & {
1071
1345
 
1072
1346
  // Global state
1073
1347
  let apiInstance: ApiInstance | null = null
1074
- let apiConfig = ref<ApiConfig | null>(null)
1075
- let _streamBaseURL: string | undefined
1076
-
1077
- /** Resolve base URL for SSE stream endpoints. Uses streamBaseURL if set, otherwise falls back to axios baseURL. */
1078
- export function getStreamBaseURL(): string {
1079
- return _streamBaseURL || axios.defaults.baseURL || ""
1080
- }
1081
1348
 
1082
1349
  /**
1083
1350
  * Create and configure the API instance
@@ -1085,77 +1352,17 @@ export function getStreamBaseURL(): string {
1085
1352
  * @returns Configured API instance
1086
1353
  */
1087
1354
  export function createApi(config: ApiConfig = {}): ApiInstance {
1088
- // Store config for access via useApi().$config
1089
- apiConfig.value = config
1090
-
1091
- // Configure axios defaults
1092
- if (config.baseURL) {
1093
- axios.defaults.baseURL = config.baseURL
1094
- }
1095
- _streamBaseURL = config.streamBaseURL
1096
- if (config.withCredentials !== undefined) {
1097
- axios.defaults.withCredentials = config.withCredentials
1098
- }
1099
- if (config.axiosConfig) {
1100
- Object.assign(axios.defaults, config.axiosConfig)
1101
- }
1102
-
1103
- // Clear existing interceptors
1104
- axios.interceptors.request.clear()
1105
- axios.interceptors.response.clear()
1106
-
1107
- // Setup auth interceptor
1108
- if (config.auth) {
1109
- axios.interceptors.request.use(async (reqConfig) => {
1110
- const token = await config.auth!.getToken?.()
1111
- if (token) {
1112
- const prefix = config.auth!.tokenPrefix ?? 'Bearer'
1113
- reqConfig.headers.Authorization = \`\${prefix} \${token}\`
1114
- }
1115
- return reqConfig
1116
- })
1117
- }
1118
-
1119
- // Setup custom request interceptor
1120
- if (config.interceptors?.request) {
1121
- axios.interceptors.request.use(config.interceptors.request)
1122
- }
1123
-
1124
- // Setup response interceptor for error handling
1125
- axios.interceptors.response.use(
1126
- (response) => {
1127
- // Store last response for metadata access
1128
- lastResponse.value = response
1129
-
1130
- // Apply custom response interceptor if provided
1131
- if (config.interceptors?.response) {
1132
- return config.interceptors.response(response)
1133
- }
1134
- return response
1135
- },
1136
- async (error) => {
1137
- // Handle 401 Unauthorized
1138
- if (error.response?.status === 401) {
1139
- await config.auth?.onUnauthorized?.()
1140
- }
1141
-
1142
- // Parse and handle error
1143
- const parsedError = parseApiError(error)
1144
- config.onError?.(parsedError, error)
1145
-
1146
- throw error
1147
- }
1148
- )
1355
+ configureApiTransport(defaultTransport, config)
1149
1356
 
1150
1357
  // API already returns ApiResponse directly!
1151
1358
  // Just add the special properties
1152
1359
  apiInstance = {
1153
1360
  ...api,
1154
1361
  $raw: api,
1155
- $lastResponse: lastResponse,
1362
+ $lastResponse: defaultTransport.lastResponse,
1156
1363
  $axios: axios,
1157
1364
  $ApiResponse: ApiResponse,
1158
- $config: apiConfig
1365
+ $config: defaultTransport.config
1159
1366
  } as ApiInstance
1160
1367
 
1161
1368
  return apiInstance
@@ -1202,6 +1409,35 @@ export {
1202
1409
  `;
1203
1410
  return templateCode.replace(/"([^"]+)":/g, "$1:");
1204
1411
  }
1412
+ function moduleFileTemplate(tsString, typeForImport, moduleFactoryName) {
1413
+ const streamTypeDefs = generateStreamEventTypeDefinitions();
1414
+ const hasStreamEndpoints = Object.keys(streamEventTypes).length > 0;
1415
+ const streamImports = hasStreamEndpoints ? "type StreamController, type SSEStreamOptions," : "";
1416
+ const typeImport = typeForImport.length ? `import type { ${typeForImport.join(", ")} } from './types.d';` : "";
1417
+ const templateCode = `${typeImport}
1418
+ import {
1419
+ ApiResponse,
1420
+ ${streamImports}
1421
+ type ApiTransport
1422
+ } from '@bagelink/sdk';
1423
+
1424
+ export interface UploadOptions {
1425
+ onUploadProgress?: (progressEvent: any) => void
1426
+ dirPath?: string
1427
+ tags?: string[]
1428
+ }
1429
+
1430
+ ${streamTypeDefs}
1431
+
1432
+ ${tsString}
1433
+
1434
+ /** Bind this module's generated endpoint namespaces to the app transport. */
1435
+ export function ${moduleFactoryName}(transport: ApiTransport) {
1436
+ return createEndpointNamespaces(transport)
1437
+ }
1438
+ `;
1439
+ return templateCode.replace(/"([^"]+)":/g, "$1:");
1440
+ }
1205
1441
 
1206
1442
  function generateTypes(schemas) {
1207
1443
  if (!schemas) {
@@ -1245,263 +1481,122 @@ function generateTypes(schemas) {
1245
1481
  }).join("\n");
1246
1482
  }
1247
1483
 
1248
- class StreamController {
1249
- constructor(cleanup) {
1250
- this.cleanup = cleanup;
1251
- }
1252
- handlers = /* @__PURE__ */ new Map();
1253
- _closed = false;
1254
- /**
1255
- * Register an event handler (fully typed!)
1256
- * @param event - Event type to listen for
1257
- * @param handler - Handler function (data type inferred from event)
1258
- * @returns this (for chaining)
1259
- */
1260
- on(event, handler) {
1261
- if (!this.handlers.has(event)) {
1262
- this.handlers.set(event, /* @__PURE__ */ new Set());
1484
+ const METHODS = ["get", "put", "post", "delete", "options", "head", "patch", "trace"];
1485
+ function isReference(value) {
1486
+ return Boolean(value && typeof value === "object" && "$ref" in value);
1487
+ }
1488
+ function operationOwners(path, operation, manifest) {
1489
+ return manifest.modules.filter(
1490
+ (module) => module.pathPrefixes?.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)) || operation.tags?.some((tag) => module.tags?.includes(tag))
1491
+ );
1492
+ }
1493
+ function collectSchemaRefs(value, refs) {
1494
+ if (!value || typeof value !== "object") return;
1495
+ if (isReference(value) && value.$ref.startsWith("#/components/schemas/")) {
1496
+ refs.add(value.$ref.slice("#/components/schemas/".length));
1497
+ }
1498
+ for (const child of Object.values(value)) collectSchemaRefs(child, refs);
1499
+ }
1500
+ function schemaClosure(openApi, seed) {
1501
+ const schemas = openApi.components?.schemas ?? {};
1502
+ const result = new Set(seed);
1503
+ const pending = [...result];
1504
+ while (pending.length) {
1505
+ const name = pending.pop();
1506
+ const schema = schemas[name];
1507
+ if (!schema) continue;
1508
+ const refs = /* @__PURE__ */ new Set();
1509
+ collectSchemaRefs(schema, refs);
1510
+ for (const ref of refs) {
1511
+ if (!result.has(ref)) {
1512
+ result.add(ref);
1513
+ pending.push(ref);
1514
+ }
1263
1515
  }
1264
- this.handlers.get(event).add(handler);
1265
- return this;
1266
1516
  }
1267
- /**
1268
- * Emit an event to all registered handlers
1269
- * @internal
1270
- */
1271
- emit(event, data) {
1272
- const handlers = this.handlers.get(event);
1273
- if (handlers) {
1274
- handlers.forEach((handler) => {
1275
- try {
1276
- handler(data);
1277
- } catch (error) {
1278
- console.error(`Error in ${event} handler:`, error);
1279
- }
1280
- });
1281
- }
1517
+ return [...result].sort();
1518
+ }
1519
+ function moduleFactoryName(name) {
1520
+ return `create${name.split("-").filter(Boolean).map((part) => part[0].toUpperCase() + part.slice(1)).join("")}Api`;
1521
+ }
1522
+ function validateOwnershipManifest(openApi, manifest) {
1523
+ const issues = [];
1524
+ if (manifest.version !== 1) {
1525
+ issues.push({ kind: "invalid-manifest", message: `Unsupported manifest version: ${manifest.version}` });
1282
1526
  }
1283
- /**
1284
- * Close the stream
1285
- */
1286
- close() {
1287
- if (!this._closed) {
1288
- this.cleanup();
1289
- this._closed = true;
1290
- }
1527
+ const names = manifest.modules.map((module) => module.name);
1528
+ for (const name of new Set(names.filter((name2, index) => names.indexOf(name2) !== index))) {
1529
+ issues.push({ kind: "invalid-manifest", message: `Duplicate module name: ${name}` });
1291
1530
  }
1292
- }
1293
-
1294
- function createSSEStream(url, options = {}) {
1295
- const {
1296
- headers = {},
1297
- withCredentials = true,
1298
- lastEventId: initialLastEventId,
1299
- maxReconnects = 5,
1300
- reconnectDelayMs = 1e3
1301
- } = options;
1302
- const controller = new AbortController();
1303
- const stream = new StreamController(() => {
1304
- controller.abort();
1305
- });
1306
- let lastEventId = initialLastEventId;
1307
- let reconnectAttempts = 0;
1308
- async function connect() {
1309
- if (controller.signal.aborted) return;
1310
- const requestHeaders = {
1311
- "Accept": "text/event-stream",
1312
- "Cache-Control": "no-cache",
1313
- ...headers
1314
- };
1315
- if (lastEventId !== void 0) {
1316
- requestHeaders["Last-Event-ID"] = lastEventId;
1531
+ const schemas = openApi.components?.schemas ?? {};
1532
+ for (const module of manifest.modules) {
1533
+ if (!module.name || !/^[a-z][a-z0-9-]*$/.test(module.name)) {
1534
+ issues.push({ kind: "invalid-manifest", message: `Invalid module name: ${module.name || "(empty)"}` });
1317
1535
  }
1318
- let response;
1319
- try {
1320
- response = await fetch(url, {
1321
- method: "GET",
1322
- headers: requestHeaders,
1323
- credentials: withCredentials ? "include" : "same-origin",
1324
- signal: controller.signal,
1325
- keepalive: false
1326
- });
1327
- } catch (error) {
1328
- if (error?.name === "AbortError") return;
1329
- return handleDisconnect(error);
1330
- }
1331
- if (!response.ok) {
1332
- stream.emit("error", new Error(`HTTP ${response.status}: ${response.statusText}`));
1333
- return;
1334
- }
1335
- if (!response.body) {
1336
- stream.emit("error", new Error("Response body is null"));
1337
- return;
1536
+ if (!(module.tags?.length || module.pathPrefixes?.length)) {
1537
+ issues.push({ kind: "invalid-manifest", message: `Module ${module.name} must own tags or pathPrefixes` });
1338
1538
  }
1339
- const reader = response.body.getReader();
1340
- const decoder = new TextDecoder();
1341
- let buffer = "";
1342
- let terminalEventReceived = false;
1343
- let currentEvent = "message";
1344
- let currentData = "";
1345
- let currentId;
1346
- try {
1347
- while (true) {
1348
- const { done, value } = await reader.read();
1349
- if (done) {
1350
- if (!controller.signal.aborted && !terminalEventReceived) {
1351
- return handleDisconnect(new Error("Stream ended before a terminal event"));
1352
- }
1353
- return;
1354
- }
1355
- buffer += decoder.decode(value, { stream: true });
1356
- const lines = buffer.split("\n");
1357
- buffer = lines.pop() || "";
1358
- for (const line of lines) {
1359
- if (line.startsWith("id:")) {
1360
- currentId = line.slice(3).trim();
1361
- } else if (line.startsWith("event:")) {
1362
- currentEvent = line.slice(6).trim();
1363
- } else if (line.startsWith("data:")) {
1364
- currentData = currentData ? `${currentData}
1365
- ${line.slice(5).trim()}` : line.slice(5).trim();
1366
- } else if (line === "" && currentData) {
1367
- if (currentId !== void 0) {
1368
- if (currentId !== lastEventId) reconnectAttempts = 0;
1369
- lastEventId = currentId;
1370
- }
1371
- try {
1372
- const parsed = JSON.parse(currentData);
1373
- stream.emit(currentEvent, parsed);
1374
- if (currentEvent === "done" || currentEvent === "error") {
1375
- terminalEventReceived = true;
1376
- stream.emit("complete", {});
1377
- controller.abort();
1378
- try {
1379
- await reader.cancel();
1380
- } catch {
1381
- }
1382
- return;
1383
- }
1384
- } catch {
1385
- stream.emit(currentEvent, currentData);
1386
- }
1387
- currentEvent = "message";
1388
- currentData = "";
1389
- currentId = void 0;
1390
- }
1391
- }
1392
- }
1393
- } catch (error) {
1394
- if (error?.name !== "AbortError") {
1395
- return handleDisconnect(error);
1396
- }
1397
- } finally {
1398
- try {
1399
- reader.releaseLock();
1400
- } catch {
1401
- }
1539
+ for (const schema of module.schemas ?? []) {
1540
+ if (!schemas[schema]) issues.push({ kind: "unknown-schema", message: `Module ${module.name} owns unknown schema ${schema}` });
1402
1541
  }
1403
1542
  }
1404
- async function handleDisconnect(error) {
1405
- if (controller.signal.aborted) return;
1406
- if (reconnectAttempts >= maxReconnects) {
1407
- const msg = error?.message || "Stream connection lost";
1408
- stream.emit("error", new Error(`${msg} (gave up after ${maxReconnects} reconnects)`));
1409
- return;
1543
+ for (const [path, item] of Object.entries(openApi.paths ?? {})) {
1544
+ for (const method of METHODS) {
1545
+ const operation = item[method];
1546
+ if (!operation) continue;
1547
+ const owners = operationOwners(path, operation, manifest);
1548
+ if (owners.length === 0) {
1549
+ issues.push({ kind: "unowned-operation", message: `${method.toUpperCase()} ${path} is not owned` });
1550
+ } else if (owners.length > 1) {
1551
+ issues.push({
1552
+ kind: "multiply-owned-operation",
1553
+ message: `${method.toUpperCase()} ${path} is owned by ${owners.map((owner) => owner.name).join(", ")}`
1554
+ });
1555
+ }
1410
1556
  }
1411
- reconnectAttempts++;
1412
- const delay = reconnectDelayMs * Math.pow(2, reconnectAttempts - 1);
1413
- await new Promise((resolve) => setTimeout(resolve, delay));
1414
- if (!controller.signal.aborted) await connect();
1415
1557
  }
1416
- connect().catch(() => {
1417
- });
1418
- return stream;
1419
- }
1420
- function createSSEStreamPost(url, body, options = {}) {
1421
- const { headers = {}, withCredentials = true } = options;
1422
- const controller = new AbortController();
1423
- const stream = new StreamController(() => {
1424
- controller.abort();
1425
- });
1426
- fetch(url, {
1427
- method: "POST",
1428
- headers: {
1429
- "Accept": "text/event-stream",
1430
- "Content-Type": "application/json",
1431
- "Cache-Control": "no-cache",
1432
- ...headers
1433
- },
1434
- credentials: withCredentials ? "include" : "same-origin",
1435
- body: JSON.stringify(body),
1436
- signal: controller.signal,
1437
- // Disable keepalive for streaming
1438
- keepalive: false
1439
- }).then(async (response) => {
1440
- if (!response.ok) {
1441
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1442
- }
1443
- if (!response.body) {
1444
- throw new Error("Response body is null");
1445
- }
1446
- const reader = response.body.getReader();
1447
- const decoder = new TextDecoder();
1448
- let buffer = "";
1449
- let currentEvent = "message";
1450
- let currentData = "";
1451
- try {
1452
- while (true) {
1453
- const { done, value } = await reader.read();
1454
- if (done) {
1455
- stream.emit("done", {});
1456
- stream.emit("complete", {});
1457
- break;
1458
- }
1459
- buffer += decoder.decode(value, { stream: true });
1460
- const lines = buffer.split("\n");
1461
- buffer = lines.pop() || "";
1462
- for (const line of lines) {
1463
- if (line.startsWith("event:")) {
1464
- currentEvent = line.slice(6).trim();
1465
- } else if (line.startsWith("data:")) {
1466
- currentData = currentData ? `${currentData}
1467
- ${line.slice(5).trim()}` : line.slice(5).trim();
1468
- } else if (line === "" && currentData) {
1469
- try {
1470
- const parsed = JSON.parse(currentData);
1471
- stream.emit(currentEvent, parsed);
1472
- } catch {
1473
- stream.emit(currentEvent, currentData);
1474
- }
1475
- currentEvent = "message";
1476
- currentData = "";
1477
- }
1558
+ return issues;
1559
+ }
1560
+ function generateModules(openApi, manifest, baseUrl) {
1561
+ const issues = validateOwnershipManifest(openApi, manifest);
1562
+ if (issues.length) throw new Error(issues.map((issue) => issue.message).join("\n"));
1563
+ const schemas = openApi.components?.schemas ?? {};
1564
+ return [...manifest.modules].sort((a, b) => a.name.localeCompare(b.name)).map((module) => {
1565
+ const paths = {};
1566
+ const refs = new Set(module.schemas ?? []);
1567
+ for (const [path, item] of Object.entries(openApi.paths ?? {})) {
1568
+ const ownedItem = {};
1569
+ for (const method of METHODS) {
1570
+ const operation = item[method];
1571
+ if (operation && operationOwners(path, operation, manifest)[0]?.name === module.name) {
1572
+ ownedItem[method] = operation;
1573
+ collectSchemaRefs(operation, refs);
1478
1574
  }
1479
1575
  }
1480
- } catch (error) {
1481
- if (error?.name !== "AbortError") {
1482
- const errorMessage = error?.message || "Stream connection error";
1483
- stream.emit("error", new Error(errorMessage));
1484
- }
1485
- } finally {
1486
- try {
1487
- reader.releaseLock();
1488
- } catch {
1576
+ if (Object.keys(ownedItem).length) {
1577
+ if (item.parameters) ownedItem.parameters = item.parameters;
1578
+ paths[path] = ownedItem;
1489
1579
  }
1490
1580
  }
1491
- }).catch((error) => {
1492
- if (error?.name !== "AbortError") {
1493
- const errorMessage = error?.message || "Failed to establish stream connection";
1494
- stream.emit("error", new Error(errorMessage));
1495
- }
1581
+ const ownedSchemas = schemaClosure(openApi, refs);
1582
+ const subset = Object.fromEntries(
1583
+ ownedSchemas.map((name) => [name, schemas[name]]).filter((entry) => Boolean(entry[1]))
1584
+ );
1585
+ return {
1586
+ name: module.name,
1587
+ paths: Object.keys(paths).sort(),
1588
+ schemas: ownedSchemas.map(formatType),
1589
+ code: generateModuleFunctions(paths, baseUrl, moduleFactoryName(module.name)),
1590
+ types: generateTypes(subset)
1591
+ };
1496
1592
  });
1497
- return stream;
1498
1593
  }
1499
1594
 
1500
1595
  const basicAuthHeader = { Authorization: "Basic YmFnZWxfdXNlcm5hbWU6Tm90U2VjdXJlQGJhZ2Vs" };
1501
1596
  const index = async (openApiUrl, baseUrl) => {
1502
1597
  try {
1503
1598
  console.log(`Fetching OpenAPI spec from: ${openApiUrl}`);
1504
- const { data: openApi } = await axios$1.get(openApiUrl, { headers: basicAuthHeader });
1599
+ const { data: openApi } = await ax.get(openApiUrl, { headers: basicAuthHeader });
1505
1600
  const schemas = openApi.components?.schemas;
1506
1601
  if (!schemas) {
1507
1602
  throw new Error("No schemas found in OpenAPI document");
@@ -1736,6 +1831,107 @@ function parseApiError(error) {
1736
1831
  function formatFieldErrors(fields) {
1737
1832
  return Object.entries(fields).map(([field, errors]) => `${field}: ${errors.join(", ")}`).join("\n");
1738
1833
  }
1834
+ function joinURL(baseURL, path) {
1835
+ if (/^https?:\/\//i.test(path)) return path;
1836
+ if (!baseURL) return path;
1837
+ return `${baseURL.replace(/\/$/, "")}/${path.replace(/^\//, "")}`;
1838
+ }
1839
+ async function getTransportHeaders(config, options) {
1840
+ const optionHeaders = typeof options.headers === "function" ? await options.headers() : options.headers ?? {};
1841
+ const configuredHeaders = config.axiosConfig?.headers;
1842
+ const headers = typeof configuredHeaders?.toJSON === "function" ? configuredHeaders.toJSON() : configuredHeaders ?? {};
1843
+ const token = await config.auth?.getToken?.();
1844
+ return {
1845
+ ...headers,
1846
+ ...token ? { Authorization: `${config.auth?.tokenPrefix ?? "Bearer"} ${token}` } : {},
1847
+ ...optionHeaders
1848
+ };
1849
+ }
1850
+ function createApiTransport(config = {}) {
1851
+ const configRef = ref(null);
1852
+ const lastResponse = ref(null);
1853
+ const axios2 = ax.create({
1854
+ baseURL: config.baseURL,
1855
+ withCredentials: config.withCredentials ?? config.axiosConfig?.withCredentials ?? true,
1856
+ paramsSerializer: { indexes: null },
1857
+ ...config.axiosConfig
1858
+ });
1859
+ const streamOptions = async (options = {}) => {
1860
+ const currentConfig = configRef.value ?? {};
1861
+ return {
1862
+ ...options,
1863
+ withCredentials: options.withCredentials ?? currentConfig.withCredentials,
1864
+ headers: await getTransportHeaders(currentConfig, options)
1865
+ };
1866
+ };
1867
+ const streamURL = (path) => joinURL(
1868
+ configRef.value?.streamBaseURL ?? axios2.defaults.baseURL ?? "",
1869
+ path
1870
+ );
1871
+ const transport = {
1872
+ axios: axios2,
1873
+ config: configRef,
1874
+ lastResponse,
1875
+ getStreamBaseURL: () => configRef.value?.streamBaseURL ?? axios2.defaults.baseURL ?? "",
1876
+ createSSEStream: (path, options = {}) => createSSEStream(streamURL(path), {
1877
+ ...options,
1878
+ headers: async () => (await streamOptions(options)).headers,
1879
+ withCredentials: options.withCredentials ?? configRef.value?.withCredentials
1880
+ }),
1881
+ createSSEStreamPost: (path, body, options = {}) => createSSEStreamPost(streamURL(path), body, {
1882
+ ...options,
1883
+ headers: async () => (await streamOptions(options)).headers,
1884
+ withCredentials: options.withCredentials ?? configRef.value?.withCredentials
1885
+ })
1886
+ };
1887
+ return configureApiTransport(transport, config);
1888
+ }
1889
+ function configureApiTransport(transport, config = {}) {
1890
+ const previousConfig = transport.config.value ?? {};
1891
+ const resolvedConfig = {
1892
+ ...previousConfig,
1893
+ ...config,
1894
+ axiosConfig: {
1895
+ ...previousConfig.axiosConfig,
1896
+ ...config.axiosConfig
1897
+ },
1898
+ withCredentials: config.withCredentials ?? config.axiosConfig?.withCredentials ?? previousConfig.withCredentials ?? true
1899
+ };
1900
+ transport.config.value = resolvedConfig;
1901
+ transport.axios.defaults.baseURL = resolvedConfig.baseURL;
1902
+ transport.axios.defaults.withCredentials = resolvedConfig.withCredentials;
1903
+ if (resolvedConfig.axiosConfig) {
1904
+ Object.assign(transport.axios.defaults, resolvedConfig.axiosConfig);
1905
+ }
1906
+ transport.axios.interceptors.request.clear();
1907
+ transport.axios.interceptors.response.clear();
1908
+ if (resolvedConfig.auth) {
1909
+ transport.axios.interceptors.request.use(async (requestConfig) => {
1910
+ const token = await resolvedConfig.auth.getToken?.();
1911
+ if (token) {
1912
+ requestConfig.headers.Authorization = `${resolvedConfig.auth.tokenPrefix ?? "Bearer"} ${token}`;
1913
+ }
1914
+ return requestConfig;
1915
+ });
1916
+ }
1917
+ if (resolvedConfig.interceptors?.request) {
1918
+ transport.axios.interceptors.request.use(resolvedConfig.interceptors.request);
1919
+ }
1920
+ transport.axios.interceptors.response.use(
1921
+ (response) => {
1922
+ transport.lastResponse.value = response;
1923
+ return resolvedConfig.interceptors?.response?.(response) ?? response;
1924
+ },
1925
+ async (error) => {
1926
+ if (error.response?.status === 401) {
1927
+ await resolvedConfig.auth?.onUnauthorized?.();
1928
+ }
1929
+ resolvedConfig.onError?.(parseApiError(error), error);
1930
+ throw error;
1931
+ }
1932
+ );
1933
+ return transport;
1934
+ }
1739
1935
  function useApiRequest(requestFn, options = {}) {
1740
1936
  const loading = ref(false);
1741
1937
  const error = ref(null);
@@ -1821,7 +2017,7 @@ function wrapApiForDirectReturn(apiObj) {
1821
2017
  }
1822
2018
  return apiObj;
1823
2019
  }
1824
- const axios = axios$1.create({
2020
+ const axios = ax.create({
1825
2021
  withCredentials: true
1826
2022
  });
1827
2023
  class DataRequest {
@@ -2091,4 +2287,4 @@ function createOptionsGetter(fetchFn, mapFn) {
2091
2287
  };
2092
2288
  }
2093
2289
 
2094
- export { ApiResponse, Bagel, StreamController, createOptionsGetter, createSSEStream, createSSEStreamPost, dereference, extractSSEEventInfo, extractSSEEventSchemas, extractSSEEventTypes, formatAPIErrorMessage, formatFieldErrors, getPath, isReferenceObject, isSSEStream, isSchemaObject, index as openAPI, parseApiError, unwrap, useApiRequest, useCancellableRequest, wrapApiForDirectReturn };
2290
+ export { ApiResponse, Bagel, StreamController, configureApiTransport, createApiTransport, createOptionsGetter, createSSEStream, createSSEStreamPost, dereference, extractSSEEventInfo, extractSSEEventSchemas, extractSSEEventTypes, formatAPIErrorMessage, formatFieldErrors, generateModules, getPath, isReferenceObject, isSSEStream, isSchemaObject, index as openAPI, parseApiError, unwrap, useApiRequest, useCancellableRequest, validateOwnershipManifest, wrapApiForDirectReturn };