@bagelink/sdk 1.7.98 → 1.7.104

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,372 @@
1
+ /* eslint-disable ts/no-non-null-assertion */
2
+ /* eslint-disable ts/no-unnecessary-condition */
3
+ /**
4
+ * StreamController - Elegant event-based SSE stream management
5
+ * Provides a beautiful, type-safe API for consuming Server-Sent Events
6
+ */
7
+
8
+ export interface SSEEvent<T = any> {
9
+ /** Event type (e.g., "token", "tool_call", "done") */
10
+ type: string
11
+ /** Event data payload */
12
+ data: T
13
+ /** Raw event string */
14
+ raw?: string
15
+ }
16
+
17
+ type EventHandler<T = any> = (data: T) => void
18
+ type ErrorHandler = (error: Error) => void
19
+ type VoidHandler = () => void
20
+
21
+ /**
22
+ * Type-safe event map for stream events
23
+ * Maps event names to their data types
24
+ */
25
+ export type StreamEventMap = Record<string, any>
26
+
27
+ /**
28
+ * StreamController - Chainable event emitter for SSE streams with full type safety
29
+ * @template TEventMap - Map of event names to their data types
30
+ *
31
+ * @example
32
+ * type ChatEvents = {
33
+ * token: { content: string }
34
+ * tool_call: { name: string, args: any }
35
+ * done: { message: string }
36
+ * }
37
+ *
38
+ * const stream: StreamController<ChatEvents> = ...
39
+ * stream.on('token', (data) => {
40
+ * // `data` is typed as { content: string }
41
+ * console.log(data.content)
42
+ * })
43
+ */
44
+ export class StreamController<TEventMap extends StreamEventMap = StreamEventMap> {
45
+ private handlers: Map<string, Set<EventHandler>> = new Map()
46
+ private errorHandlers: Set<ErrorHandler> = new Set()
47
+ private completeHandlers: Set<VoidHandler> = new Set()
48
+ private abortController: AbortController
49
+ private _closed = false
50
+ private _promise: Promise<any> | null = null
51
+ private _resolvePromise: ((value: any) => void) | null = null
52
+ private _rejectPromise: ((error: Error) => void) | null = null
53
+
54
+ constructor(
55
+ private streamFn: (
56
+ onEvent: (event: SSEEvent) => void,
57
+ onError: (error: Error) => void,
58
+ onComplete: () => void
59
+ ) => () => void
60
+ ) {
61
+ this.abortController = new AbortController()
62
+ this.start()
63
+ }
64
+
65
+ private start() {
66
+ const cleanup = this.streamFn(
67
+ (event) => { this.emit(event.type, event.data) },
68
+ (error) => { this.emitError(error) },
69
+ () => { this.emitComplete() }
70
+ )
71
+
72
+ // Store cleanup function
73
+ this.abortController.signal.addEventListener('abort', () => {
74
+ cleanup()
75
+ this._closed = true
76
+ })
77
+ }
78
+
79
+ /**
80
+ * Register an event handler (fully typed!)
81
+ * @param event - Event type to listen for
82
+ * @param handler - Handler function (data type inferred from event)
83
+ * @returns this (for chaining)
84
+ */
85
+ on<K extends keyof TEventMap | 'error' | 'complete'>(
86
+ event: K,
87
+ handler: K extends 'error'
88
+ ? ErrorHandler
89
+ : K extends 'complete'
90
+ ? VoidHandler
91
+ : K extends keyof TEventMap
92
+ ? (data: TEventMap[K]) => void
93
+ : EventHandler
94
+ ): this {
95
+ if (event === 'error') {
96
+ this.errorHandlers.add(handler as ErrorHandler)
97
+ } else if (event === 'complete') {
98
+ this.completeHandlers.add(handler as VoidHandler)
99
+ } else {
100
+ if (!this.handlers.has(event as string)) {
101
+ this.handlers.set(event as string, new Set())
102
+ }
103
+ this.handlers.get(event as string)!.add(handler as EventHandler)
104
+ }
105
+ return this
106
+ }
107
+
108
+ /**
109
+ * Register a one-time event handler (fully typed!)
110
+ * @param event - Event type to listen for
111
+ * @param handler - Handler function (called once then removed, data type inferred from event)
112
+ * @returns this (for chaining)
113
+ */
114
+ once<K extends keyof TEventMap | 'error' | 'complete'>(
115
+ event: K,
116
+ handler: K extends 'error'
117
+ ? ErrorHandler
118
+ : K extends 'complete'
119
+ ? VoidHandler
120
+ : K extends keyof TEventMap
121
+ ? (data: TEventMap[K]) => void
122
+ : EventHandler
123
+ ): this {
124
+ const wrappedHandler = (data: any) => {
125
+ (handler as any)(data)
126
+ this.off(event, wrappedHandler as any)
127
+ }
128
+ return this.on(event, wrappedHandler as any)
129
+ }
130
+
131
+ /**
132
+ * Remove an event handler (fully typed!)
133
+ * @param event - Event type
134
+ * @param handler - Handler to remove
135
+ * @returns this (for chaining)
136
+ */
137
+ off<K extends keyof TEventMap | 'error' | 'complete'>(
138
+ event: K,
139
+ handler: K extends 'error'
140
+ ? ErrorHandler
141
+ : K extends 'complete'
142
+ ? VoidHandler
143
+ : K extends keyof TEventMap
144
+ ? (data: TEventMap[K]) => void
145
+ : EventHandler
146
+ ): this {
147
+ if (event === 'error') {
148
+ this.errorHandlers.delete(handler as ErrorHandler)
149
+ } else if (event === 'complete') {
150
+ this.completeHandlers.delete(handler as VoidHandler)
151
+ } else {
152
+ this.handlers.get(event as string)?.delete(handler as EventHandler)
153
+ }
154
+ return this
155
+ }
156
+
157
+ /**
158
+ * Remove all handlers for an event (or all events if no event specified)
159
+ */
160
+ removeAllListeners(event?: keyof TEventMap | 'error' | 'complete'): this {
161
+ if (event === undefined) {
162
+ this.handlers.clear()
163
+ this.errorHandlers.clear()
164
+ this.completeHandlers.clear()
165
+ } else if (event === 'error') {
166
+ this.errorHandlers.clear()
167
+ } else if (event === 'complete') {
168
+ this.completeHandlers.clear()
169
+ } else {
170
+ this.handlers.delete(event as string)
171
+ }
172
+ return this
173
+ }
174
+
175
+ private emit(event: string, data: any) {
176
+ const handlers = this.handlers.get(event)
177
+ if (handlers) {
178
+ handlers.forEach((handler) => {
179
+ try {
180
+ handler(data)
181
+ } catch (error) {
182
+ console.error(`Error in handler for event "${event}":`, error)
183
+ }
184
+ })
185
+ }
186
+
187
+ // Resolve promise on 'done' event
188
+ if (event === 'done' && this._resolvePromise) {
189
+ this._resolvePromise(data)
190
+ }
191
+ }
192
+
193
+ private emitError(error: Error) {
194
+ this.errorHandlers.forEach((handler) => {
195
+ try {
196
+ handler(error)
197
+ } catch (err) {
198
+ console.error('Error in error handler:', err)
199
+ }
200
+ })
201
+
202
+ // Reject promise on error
203
+ if (this._rejectPromise) {
204
+ this._rejectPromise(error)
205
+ }
206
+ }
207
+
208
+ private emitComplete() {
209
+ this.completeHandlers.forEach((handler) => {
210
+ try {
211
+ handler()
212
+ } catch (error) {
213
+ console.error('Error in complete handler:', error)
214
+ }
215
+ })
216
+ }
217
+
218
+ /**
219
+ * Close the stream
220
+ */
221
+ close(): void {
222
+ if (!this._closed) {
223
+ this.abortController.abort()
224
+ this._closed = true
225
+ }
226
+ }
227
+
228
+ /**
229
+ * Check if stream is closed
230
+ */
231
+ get closed(): boolean {
232
+ return this._closed
233
+ }
234
+
235
+ /**
236
+ * Convert stream to a Promise that resolves when 'done' event fires
237
+ * @returns Promise that resolves with the 'done' event data
238
+ */
239
+ toPromise<T = any>(): Promise<T> {
240
+ if (!this._promise) {
241
+ this._promise = new Promise<T>((resolve, reject) => {
242
+ this._resolvePromise = resolve as any
243
+ this._rejectPromise = reject
244
+ })
245
+ }
246
+ return this._promise
247
+ }
248
+
249
+ /**
250
+ * Make the stream async iterable
251
+ * Usage: for await (const event of stream) { ... }
252
+ */
253
+ async* [Symbol.asyncIterator](): AsyncIterableIterator<SSEEvent> {
254
+ const events: SSEEvent[] = []
255
+ let resolveNext: ((event: SSEEvent | null) => void) | null = null
256
+ let done = false
257
+
258
+ // Capture all event types
259
+ const eventHandler = (type: string) => (data: any) => {
260
+ const event: SSEEvent = { type, data }
261
+ if (resolveNext) {
262
+ resolveNext(event)
263
+ resolveNext = null
264
+ } else {
265
+ events.push(event)
266
+ }
267
+ }
268
+
269
+ // Listen to all events by capturing them
270
+ const originalEmit = this.emit.bind(this)
271
+ const capturedEvents: string[] = []
272
+ this.emit = (event: string, data: any) => {
273
+ if (!capturedEvents.includes(event)) {
274
+ capturedEvents.push(event)
275
+
276
+ this.on(event as any, eventHandler(event))
277
+ }
278
+ originalEmit(event, data)
279
+ }
280
+
281
+ this.on('complete', () => {
282
+ done = true
283
+ if (resolveNext) {
284
+ resolveNext(null)
285
+ resolveNext = null
286
+ }
287
+ })
288
+
289
+ try {
290
+ // eslint-disable-next-line no-unmodified-loop-condition
291
+ while (!done) {
292
+ if (events.length > 0) {
293
+ const event = events.shift()
294
+ if (event) yield event
295
+ } else {
296
+ // eslint-disable-next-line no-await-in-loop
297
+ const event = await new Promise<SSEEvent | null>((resolve) => {
298
+ resolveNext = resolve
299
+ })
300
+ if (event === null) break
301
+ yield event
302
+ }
303
+ }
304
+ } finally {
305
+ this.close()
306
+ }
307
+ }
308
+
309
+ /**
310
+ * Collect all events into an array until stream completes
311
+ * @returns Promise<SSEEvent[]>
312
+ */
313
+ async toArray(): Promise<SSEEvent[]> {
314
+ const events: SSEEvent[] = []
315
+ for await (const event of this) {
316
+ events.push(event)
317
+ }
318
+ return events
319
+ }
320
+
321
+ /**
322
+ * Pipe stream events through a transform function
323
+ */
324
+ map<R>(transform: (event: SSEEvent) => R): StreamController<TEventMap> {
325
+ const mappedController = new StreamController<TEventMap>(
326
+ (onEvent, onError, onComplete) => {
327
+ this.on('error' as any, onError)
328
+ this.on('complete' as any, onComplete)
329
+
330
+ // Forward all events through transform
331
+ this.handlers.forEach((_, eventType) => {
332
+ this.on(eventType as any, (data: any) => {
333
+ try {
334
+ const transformed = transform({ type: eventType, data })
335
+ onEvent({ type: eventType, data: transformed, raw: undefined })
336
+ } catch (error) {
337
+ onError(error as Error)
338
+ }
339
+ })
340
+ })
341
+
342
+ return () => { this.close() }
343
+ }
344
+ )
345
+ return mappedController
346
+ }
347
+
348
+ /**
349
+ * Filter events based on a predicate
350
+ */
351
+ filter(predicate: (event: SSEEvent) => boolean): StreamController<TEventMap> {
352
+ const filteredController = new StreamController<TEventMap>(
353
+ (onEvent, onError, onComplete) => {
354
+ this.on('error' as any, onError)
355
+ this.on('complete' as any, onComplete)
356
+
357
+ // Forward filtered events
358
+ this.handlers.forEach((_, eventType) => {
359
+ this.on(eventType as any, (data: any) => {
360
+ const event = { type: eventType, data }
361
+ if (predicate(event)) {
362
+ onEvent(event)
363
+ }
364
+ })
365
+ })
366
+
367
+ return () => { this.close() }
368
+ }
369
+ )
370
+ return filteredController
371
+ }
372
+ }
@@ -1,3 +1,8 @@
1
+ /* eslint-disable ts/strict-boolean-expressions */
2
+ /* eslint-disable ts/no-unnecessary-condition */
3
+ /* eslint-disable jsdoc/check-param-names */
4
+ /* eslint-disable ts/no-non-null-assertion */
5
+ /* eslint-disable prefer-destructuring */
1
6
  import type {
2
7
  OperationObject as OpenAPIOperation,
3
8
  PathItemObject as OpenAPIPath,
@@ -8,8 +13,9 @@ import type {
8
13
  ReferenceObject,
9
14
  SchemaObject,
10
15
  } from './types'
11
- import { dereference, isReferenceObject } from './types/utils'
16
+ import { isSSEStream, extractSSEEventTypes } from './streamDetector'
12
17
 
18
+ import { dereference, isReferenceObject } from './types/utils'
13
19
  import {
14
20
  cleanPath,
15
21
  formatType,
@@ -56,6 +62,7 @@ export interface PathOperation {
56
62
  // Tracking for function generation
57
63
  const functionsInventory: Record<string, string> = {}
58
64
  const pathOperations: PathOperation[] = []
65
+ const streamEventTypes: Record<string, string[]> = {} // Track stream endpoints and their event types
59
66
 
60
67
  /**
61
68
  * Collects non-primitive types for import statements
@@ -339,6 +346,115 @@ function combineAllParams(
339
346
  return `{ ${destructuredNames} }: { ${typeDefinition} } = {}`
340
347
  }
341
348
 
349
+ /**
350
+ * Generates a unique type name for a stream endpoint
351
+ */
352
+ function generateStreamTypeName(path: string): string {
353
+ return `${toPascalCase(
354
+ path
355
+ .split('/')
356
+ .filter(p => p && !/\{|\}/.test(p))
357
+ .join('_')
358
+ )}StreamEvents`
359
+ }
360
+
361
+ /**
362
+ * Generates a Stream function for SSE endpoints with full type safety
363
+ * @param method - The HTTP method
364
+ * @param path - The original API path
365
+ * @param formattedPath - The formatted API path
366
+ * @param allParams - The combined parameter string
367
+ * @param requestBodyPayload - The request body payload
368
+ * @param eventTypes - Array of SSE event types
369
+ * @returns A string with the SSE stream function
370
+ */
371
+ function generateStreamFunction(
372
+ method: string,
373
+ path: string,
374
+ formattedPath: string,
375
+ allParams: string,
376
+ requestBodyPayload: string,
377
+ eventTypes?: string[]
378
+ ): string {
379
+ if (allParams === 'undefined') { allParams = '' }
380
+
381
+ // Extract parameter names for the stream call (unused but kept for future use)
382
+ // const paramNames = allParams ? allParams.match(/\w+(?=\s*[?:])/g) || [] : []
383
+
384
+ const bodyVar = requestBodyPayload || '{}'
385
+ const baseUrlRef = 'axios.defaults.baseURL || ""'
386
+
387
+ // Generate type name for this stream
388
+ const streamTypeName = generateStreamTypeName(path)
389
+
390
+ // Store event types for later type generation
391
+ if (eventTypes?.length) {
392
+ streamEventTypes[streamTypeName] = eventTypes
393
+ }
394
+
395
+ const eventTypesComment = eventTypes?.length
396
+ ? `\n * Event types: ${eventTypes.map(t => `"${t}"`).join(', ')}`
397
+ : ''
398
+
399
+ const eventTypesExample = eventTypes?.length
400
+ ? eventTypes.map(t => `\n * .on('${t}', (data) => console.log(data))`).join('')
401
+ : '\n * .on(\'message\', (data) => console.log(data))'
402
+
403
+ const typeAnnotation = eventTypes?.length
404
+ ? `StreamController<${streamTypeName}>`
405
+ : 'StreamController'
406
+
407
+ if (method === 'post') {
408
+ return `{
409
+ /**
410
+ * Stream SSE events from this endpoint (returns StreamController)${eventTypesComment}
411
+ *
412
+ * @example
413
+ * const stream = api.endpoint.stream(params)${eventTypesExample}
414
+ * .on('error', (err) => console.error(err))
415
+ * .on('complete', () => console.log('Done!'))
416
+ *
417
+ * // Close stream when needed
418
+ * stream.close()
419
+ */
420
+ stream: (${allParams}, options?: SSEStreamOptions): ${typeAnnotation} => {
421
+ const url = \`\${${baseUrlRef}}${formattedPath}\`
422
+ return createSSEStreamPost<${streamTypeName}>(url, ${bodyVar}, options)
423
+ },
424
+ /**
425
+ * Regular POST request (non-streaming)
426
+ */
427
+ post: async (${allParams}): Promise<AxiosResponse<any>> => {
428
+ return axios.post(${formattedPath}, ${bodyVar})
429
+ }
430
+ }`
431
+ } else {
432
+ return `{
433
+ /**
434
+ * Stream SSE events from this endpoint (returns StreamController)${eventTypesComment}
435
+ *
436
+ * @example
437
+ * const stream = api.endpoint.stream(params)${eventTypesExample}
438
+ * .on('error', (err) => console.error(err))
439
+ * .on('complete', () => console.log('Done!'))
440
+ *
441
+ * // Close stream when needed
442
+ * stream.close()
443
+ */
444
+ stream: (${allParams}, options?: SSEStreamOptions): ${typeAnnotation} => {
445
+ const url = \`\${${baseUrlRef}}${formattedPath}\`
446
+ return createSSEStream<${streamTypeName}>(url, options)
447
+ },
448
+ /**
449
+ * Regular GET request (non-streaming)
450
+ */
451
+ get: async (${allParams}): Promise<AxiosResponse<any>> => {
452
+ return axios.get(${formattedPath})
453
+ }
454
+ }`
455
+ }
456
+ }
457
+
342
458
  /**
343
459
  * Generates an Axios function call as a string
344
460
  * @param method - The HTTP method
@@ -444,6 +560,9 @@ function generateFunctionForOperation(
444
560
  ): string {
445
561
  if (!operation) { return '' }
446
562
 
563
+ // Check if this is an SSE stream endpoint
564
+ const isStream = isSSEStream(operation)
565
+
447
566
  // Validate: GET and DELETE requests should not have request bodies
448
567
  const methodLower = method.toLowerCase()
449
568
  if (['get', 'delete'].includes(methodLower) && operation.requestBody) {
@@ -493,6 +612,19 @@ function generateFunctionForOperation(
493
612
  // Create JSDoc comment with OpenAPI documentation
494
613
  const functionComment = buildJSDocComment(operation, method, path)
495
614
 
615
+ // Generate stream function for SSE endpoints
616
+ if (isStream) {
617
+ const eventTypes = extractSSEEventTypes(operation)
618
+ return functionComment + generateStreamFunction(
619
+ method,
620
+ path,
621
+ formatPathWithParams(path),
622
+ allParams,
623
+ requestBodyPayload,
624
+ eventTypes
625
+ )
626
+ }
627
+
496
628
  return functionComment + generateAxiosFunction(
497
629
  method,
498
630
  formatPathWithParams(path),
@@ -667,6 +799,35 @@ export const ${parent} = ${JSON.stringify(object, undefined, 2)};\n`
667
799
  return fileTemplate(tsString, allTypes, baseUrl)
668
800
  }
669
801
 
802
+ /**
803
+ * Generates TypeScript type definitions for stream events
804
+ * @returns TypeScript type definitions string
805
+ */
806
+ function generateStreamEventTypeDefinitions(): string {
807
+ if (Object.keys(streamEventTypes).length === 0) {
808
+ return ''
809
+ }
810
+
811
+ let typeDefs = '\n// ============================================================================\n'
812
+ typeDefs += '// Stream Event Type Definitions (Fully Typed!)\n'
813
+ typeDefs += '// ============================================================================\n\n'
814
+
815
+ for (const [typeName, events] of Object.entries(streamEventTypes)) {
816
+ typeDefs += `/**\n * Event types for ${typeName.replace('StreamEvents', '')} stream\n`
817
+ typeDefs += ` * Events: ${events.map(e => `"${e}"`).join(', ')}\n */\n`
818
+ typeDefs += `export interface ${typeName} {\n`
819
+
820
+ for (const event of events) {
821
+ typeDefs += ` /** ${event} event data */\n`
822
+ typeDefs += ` ${event}: any // TODO: Define specific type from OpenAPI schema\n`
823
+ }
824
+
825
+ typeDefs += '}\n\n'
826
+ }
827
+
828
+ return typeDefs
829
+ }
830
+
670
831
  /**
671
832
  * Generates the TypeScript file template
672
833
  * @param tsString - The generated TypeScript code
@@ -679,9 +840,12 @@ function fileTemplate(
679
840
  typeForImport: string[],
680
841
  baseURL: string
681
842
  ): string {
843
+ const streamTypeDefs = generateStreamEventTypeDefinitions()
844
+
682
845
  const templateCode = `import ax from 'axios';
683
846
  import type { AxiosResponse } from 'axios';
684
847
  import type { ${typeForImport.join(', ')} } from './types.d';
848
+ import { createSSEStream, createSSEStreamPost, StreamController, type SSEStreamOptions, type SSEEvent } from './streamClient';
685
849
 
686
850
  /**
687
851
  * Options for file upload operations
@@ -695,6 +859,24 @@ export interface UploadOptions {
695
859
  tags?: string[]
696
860
  }
697
861
 
862
+ /**
863
+ * Export SSE stream utilities for direct use
864
+ *
865
+ * @example Chainable event handlers
866
+ * const stream = createSSEStream(url)
867
+ * .on('token', (data) => console.log(data))
868
+ * .on('done', () => console.log('Complete!'))
869
+ *
870
+ * @example Async iteration
871
+ * for await (const event of createSSEStream(url)) {
872
+ * console.log(event.type, event.data)
873
+ * }
874
+ *
875
+ * @example Promise-based
876
+ * const result = await createSSEStream(url).toPromise()
877
+ */
878
+ export { createSSEStream, createSSEStreamPost, StreamController, type SSEStreamOptions, type SSEEvent } from './streamClient';
879
+ ${streamTypeDefs}
698
880
  /**
699
881
  * Configured axios instance for API requests
700
882
  * @example
@@ -35,4 +35,6 @@ export default async (openApiUrl: string, baseUrl: string): Promise<OpenAPIRespo
35
35
  }
36
36
  }
37
37
 
38
+ export * from './streamClient'
39
+ export * from './streamDetector'
38
40
  export * from './types'