@bagelink/sdk 1.7.101 → 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,247 @@
1
+ /* eslint-disable ts/no-unnecessary-condition */
2
+ /* eslint-disable no-await-in-loop */
3
+ /* eslint-disable ts/strict-boolean-expressions */
4
+ /* eslint-disable ts/use-unknown-in-catch-callback-variable */
5
+ /**
6
+ * SSE (Server-Sent Events) Client Utilities
7
+ * Provides utilities for consuming Server-Sent Events streams
8
+ */
9
+
10
+ import type { StreamEventMap } from './StreamController'
11
+ import { StreamController } from './StreamController'
12
+
13
+ export { type SSEEvent, StreamController, type StreamEventMap } from './StreamController'
14
+
15
+ export interface SSEStreamOptions {
16
+ /** Additional headers to send with the request */
17
+ headers?: Record<string, string>
18
+ /** Whether to include credentials (cookies) with the request */
19
+ withCredentials?: boolean
20
+ }
21
+
22
+ /**
23
+ * Creates an SSE stream consumer with elegant event-based API
24
+ * @param url - The SSE endpoint URL
25
+ * @param options - Stream options
26
+ * @returns StreamController for chainable event handling
27
+ *
28
+ * @example
29
+ * ```typescript
30
+ * const stream = createSSEStream(url)
31
+ * .on('token', data => console.log(data))
32
+ * .on('done', () => console.log('Complete!'))
33
+ * .on('error', err => console.error(err))
34
+ *
35
+ * // Close when needed
36
+ * stream.close()
37
+ * ```
38
+ */
39
+ export function createSSEStream<TEventMap extends StreamEventMap = StreamEventMap>(
40
+ url: string,
41
+ options: SSEStreamOptions = {}
42
+ ): StreamController<TEventMap> {
43
+ const { headers = {}, withCredentials = true } = options
44
+
45
+ return new StreamController((onEvent, onError, onComplete) => {
46
+ const controller = new AbortController()
47
+ const { signal } = controller
48
+
49
+ // Start the fetch request
50
+ fetch(url, {
51
+ method: 'GET',
52
+ headers: {
53
+ Accept: 'text/event-stream',
54
+ ...headers,
55
+ },
56
+ credentials: withCredentials ? 'include' : 'same-origin',
57
+ signal,
58
+ })
59
+ .then(async (response) => {
60
+ if (!response.ok) {
61
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
62
+ }
63
+
64
+ if (!response.body) {
65
+ throw new Error('Response body is null')
66
+ }
67
+
68
+ const reader = response.body.getReader()
69
+ const decoder = new TextDecoder()
70
+ let buffer = ''
71
+
72
+ try {
73
+ while (true) {
74
+ const { done, value } = await reader.read()
75
+
76
+ if (done) {
77
+ onComplete()
78
+ break
79
+ }
80
+
81
+ // Decode the chunk and add to buffer
82
+ buffer += decoder.decode(value, { stream: true })
83
+
84
+ // Process complete events in the buffer
85
+ const lines = buffer.split('\n')
86
+ buffer = lines.pop() || '' // Keep incomplete line in buffer
87
+
88
+ let eventType = ''
89
+ let eventData = ''
90
+
91
+ for (const line of lines) {
92
+ if (line.startsWith('event:')) {
93
+ eventType = line.slice(6).trim()
94
+ } else if (line.startsWith('data:')) {
95
+ eventData = line.slice(5).trim()
96
+ } else if (line === '' && eventData) {
97
+ // Empty line indicates end of event
98
+ try {
99
+ const parsedData = JSON.parse(eventData)
100
+ onEvent({
101
+ type: eventType || 'message',
102
+ data: parsedData,
103
+ raw: eventData,
104
+ })
105
+ } catch {
106
+ // If not JSON, pass as string
107
+ onEvent({
108
+ type: eventType || 'message',
109
+ data: eventData,
110
+ raw: eventData,
111
+ })
112
+ }
113
+ eventType = ''
114
+ eventData = ''
115
+ }
116
+ }
117
+ }
118
+ } catch (error) {
119
+ if (error instanceof Error && error.name !== 'AbortError') {
120
+ onError(error)
121
+ }
122
+ }
123
+ })
124
+ .catch((error) => {
125
+ if (error.name !== 'AbortError') {
126
+ onError(error)
127
+ }
128
+ })
129
+
130
+ // Return cleanup function
131
+ return () => { controller.abort() }
132
+ })
133
+ }
134
+
135
+ /**
136
+ * Creates an SSE stream consumer for POST requests with elegant event-based API
137
+ * @param url - The SSE endpoint URL
138
+ * @param body - Request body
139
+ * @param options - Stream options
140
+ * @returns StreamController for chainable event handling
141
+ *
142
+ * @example
143
+ * ```typescript
144
+ * const stream = createSSEStreamPost(url, { message: 'Hello' })
145
+ * .on('token', data => console.log(data))
146
+ * .on('done', () => console.log('Complete!'))
147
+ * .on('error', err => console.error(err))
148
+ * ```
149
+ */
150
+ export function createSSEStreamPost<TEventMap extends StreamEventMap = StreamEventMap, TBody = any>(
151
+ url: string,
152
+ body: TBody,
153
+ options: SSEStreamOptions = {}
154
+ ): StreamController<TEventMap> {
155
+ const { headers = {}, withCredentials = true } = options
156
+
157
+ return new StreamController((onEvent, onError, onComplete) => {
158
+ const controller = new AbortController()
159
+ const { signal } = controller
160
+
161
+ // Start the fetch request
162
+ fetch(url, {
163
+ method: 'POST',
164
+ headers: {
165
+ 'Accept': 'text/event-stream',
166
+ 'Content-Type': 'application/json',
167
+ ...headers,
168
+ },
169
+ credentials: withCredentials ? 'include' : 'same-origin',
170
+ body: JSON.stringify(body),
171
+ signal,
172
+ })
173
+ .then(async (response) => {
174
+ if (!response.ok) {
175
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
176
+ }
177
+
178
+ if (!response.body) {
179
+ throw new Error('Response body is null')
180
+ }
181
+
182
+ const reader = response.body.getReader()
183
+ const decoder = new TextDecoder()
184
+ let buffer = ''
185
+
186
+ try {
187
+ while (true) {
188
+ const { done, value } = await reader.read()
189
+
190
+ if (done) {
191
+ onComplete()
192
+ break
193
+ }
194
+
195
+ // Decode the chunk and add to buffer
196
+ buffer += decoder.decode(value, { stream: true })
197
+
198
+ // Process complete events in the buffer
199
+ const lines = buffer.split('\n')
200
+ buffer = lines.pop() || '' // Keep incomplete line in buffer
201
+
202
+ let eventType = ''
203
+ let eventData = ''
204
+
205
+ for (const line of lines) {
206
+ if (line.startsWith('event:')) {
207
+ eventType = line.slice(6).trim()
208
+ } else if (line.startsWith('data:')) {
209
+ eventData = line.slice(5).trim()
210
+ } else if (line === '' && eventData) {
211
+ // Empty line indicates end of event
212
+ try {
213
+ const parsedData = JSON.parse(eventData)
214
+ onEvent({
215
+ type: eventType || 'message',
216
+ data: parsedData,
217
+ raw: eventData,
218
+ })
219
+ } catch {
220
+ // If not JSON, pass as string
221
+ onEvent({
222
+ type: eventType || 'message',
223
+ data: eventData,
224
+ raw: eventData,
225
+ })
226
+ }
227
+ eventType = ''
228
+ eventData = ''
229
+ }
230
+ }
231
+ }
232
+ } catch (error) {
233
+ if (error instanceof Error && error.name !== 'AbortError') {
234
+ onError(error)
235
+ }
236
+ }
237
+ })
238
+ .catch((error) => {
239
+ if (error.name !== 'AbortError') {
240
+ onError(error)
241
+ }
242
+ })
243
+
244
+ // Return cleanup function
245
+ return () => { controller.abort() }
246
+ })
247
+ }
@@ -0,0 +1,44 @@
1
+ import type { OperationObject as OpenAPIOperation } from './types'
2
+
3
+ /**
4
+ * Detects if an operation is an SSE (Server-Sent Events) stream endpoint
5
+ * @param operation - The OpenAPI operation object
6
+ * @returns Whether the operation is an SSE stream
7
+ */
8
+ export function isSSEStream(operation: OpenAPIOperation): boolean {
9
+ const description = operation.description?.toLowerCase() || ''
10
+ const summary = operation.summary?.toLowerCase() || ''
11
+
12
+ // Check for SSE indicators in description or summary
13
+ const hasSSEKeywords
14
+ = description.includes('sse')
15
+ || description.includes('server-sent events')
16
+ || description.includes('event stream')
17
+ || summary.includes('stream')
18
+
19
+ // Check for text/event-stream content type in responses
20
+ const responses = operation.responses || {}
21
+ const hasEventStreamContentType = Object.values(responses).some((response: any) => {
22
+ if ('content' in response) {
23
+ return 'text/event-stream' in (response.content || {})
24
+ }
25
+ return false
26
+ })
27
+
28
+ return hasSSEKeywords || hasEventStreamContentType
29
+ }
30
+
31
+ /**
32
+ * Extracts SSE event types from operation description
33
+ * @param operation - The OpenAPI operation object
34
+ * @returns Array of event types or undefined
35
+ */
36
+ export function extractSSEEventTypes(operation: OpenAPIOperation): string[] | undefined {
37
+ const description = operation.description || ''
38
+
39
+ // Look for patterns like: type: "token", type: "done", etc.
40
+ const typeMatches = description.matchAll(/type:\s*"([^"]+)"/g)
41
+ const types = Array.from(typeMatches).map(match => match[1])
42
+
43
+ return types.length > 0 ? types : undefined
44
+ }