@ragbits/api-client 0.0.2 → 1.3.0
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 +207 -63
- package/{src/test → __tests__}/utils.ts +18 -1
- package/dist/autogen.types.d.ts +426 -0
- package/dist/index.cjs +149 -44
- package/dist/index.d.ts +17 -225
- package/dist/index.js +147 -44
- package/dist/types.d.ts +77 -0
- package/package.json +9 -4
- package/dist/index.d.cts +0 -257
- package/eslint.config.js +0 -21
- package/prettier.config.js +0 -6
- package/src/index.ts +0 -213
- package/src/test/RagbitsClient.test.ts +0 -484
- package/src/test/types.test.ts +0 -35
- package/src/types.ts +0 -259
- package/tsconfig.json +0 -16
- package/vitest.config.ts +0 -20
- /package/{src/test → __tests__}/mocks/handlers.ts +0 -0
- /package/{src/test → __tests__}/setup.ts +0 -0
package/src/index.ts
DELETED
|
@@ -1,213 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
ClientConfig,
|
|
3
|
-
StreamCallbacks,
|
|
4
|
-
ApiEndpointPath,
|
|
5
|
-
ApiEndpointResponse,
|
|
6
|
-
TypedApiRequestOptions,
|
|
7
|
-
StreamingEndpointPath,
|
|
8
|
-
StreamingEndpointRequest,
|
|
9
|
-
StreamingEndpointStream,
|
|
10
|
-
} from './types'
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Client for communicating with the Ragbits API
|
|
14
|
-
*/
|
|
15
|
-
export class RagbitsClient {
|
|
16
|
-
private readonly baseUrl: string
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* @param config - Configuration object
|
|
20
|
-
*/
|
|
21
|
-
constructor(config: ClientConfig = {}) {
|
|
22
|
-
this.baseUrl = config.baseUrl || 'http://127.0.0.1:8000'
|
|
23
|
-
|
|
24
|
-
// Validate the base URL
|
|
25
|
-
try {
|
|
26
|
-
new URL(this.baseUrl)
|
|
27
|
-
} catch {
|
|
28
|
-
throw new Error(
|
|
29
|
-
`Invalid base URL: ${this.baseUrl}. Please provide a valid URL.`
|
|
30
|
-
)
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
if (this.baseUrl.endsWith('/')) {
|
|
34
|
-
this.baseUrl = this.baseUrl.slice(0, -1)
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Get the base URL used by this client
|
|
40
|
-
*/
|
|
41
|
-
getBaseUrl(): string {
|
|
42
|
-
return this.baseUrl
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* Build full API URL from path
|
|
47
|
-
* @private
|
|
48
|
-
*/
|
|
49
|
-
private _buildApiUrl(path: string): string {
|
|
50
|
-
return `${this.baseUrl}${path}`
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Make a request to the API
|
|
55
|
-
* @private
|
|
56
|
-
*/
|
|
57
|
-
private async _makeRequest(
|
|
58
|
-
url: string,
|
|
59
|
-
options: RequestInit = {}
|
|
60
|
-
): Promise<Response> {
|
|
61
|
-
const defaultOptions: RequestInit = {
|
|
62
|
-
headers: {
|
|
63
|
-
'Content-Type': 'application/json',
|
|
64
|
-
},
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
const response = await fetch(url, { ...defaultOptions, ...options })
|
|
68
|
-
if (!response.ok) {
|
|
69
|
-
throw new Error(`HTTP error! status: ${response.status}`)
|
|
70
|
-
}
|
|
71
|
-
return response
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
/**
|
|
75
|
-
* Method to make API requests to known endpoints only
|
|
76
|
-
* @param endpoint - API endpoint path (must be predefined)
|
|
77
|
-
* @param options - Typed request options for the specific endpoint
|
|
78
|
-
*/
|
|
79
|
-
async makeRequest<T extends ApiEndpointPath>(
|
|
80
|
-
endpoint: T,
|
|
81
|
-
options?: TypedApiRequestOptions<T>
|
|
82
|
-
): Promise<ApiEndpointResponse<T>> {
|
|
83
|
-
const {
|
|
84
|
-
method = 'GET',
|
|
85
|
-
body,
|
|
86
|
-
headers = {},
|
|
87
|
-
...restOptions
|
|
88
|
-
} = options || {}
|
|
89
|
-
|
|
90
|
-
const requestOptions: RequestInit = {
|
|
91
|
-
method,
|
|
92
|
-
headers,
|
|
93
|
-
...restOptions, // This will include signal and other fetch options
|
|
94
|
-
}
|
|
95
|
-
|
|
96
|
-
if (body && method !== 'GET') {
|
|
97
|
-
requestOptions.body =
|
|
98
|
-
typeof body === 'string' ? body : JSON.stringify(body)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const response = await this._makeRequest(
|
|
102
|
-
this._buildApiUrl(endpoint),
|
|
103
|
-
requestOptions
|
|
104
|
-
)
|
|
105
|
-
return response.json()
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Method for streaming requests to known endpoints only
|
|
110
|
-
* @param endpoint - Streaming endpoint path (must be predefined)
|
|
111
|
-
* @param data - Request data
|
|
112
|
-
* @param callbacks - Stream callbacks
|
|
113
|
-
* @param signal - Optional AbortSignal for cancelling the request
|
|
114
|
-
*/
|
|
115
|
-
makeStreamRequest<T extends StreamingEndpointPath>(
|
|
116
|
-
endpoint: T,
|
|
117
|
-
data: StreamingEndpointRequest<T>,
|
|
118
|
-
callbacks: StreamCallbacks<StreamingEndpointStream<T>>,
|
|
119
|
-
signal?: AbortSignal
|
|
120
|
-
): () => void {
|
|
121
|
-
let isCancelled = false
|
|
122
|
-
|
|
123
|
-
const processStream = async (response: Response): Promise<void> => {
|
|
124
|
-
const reader = response.body
|
|
125
|
-
?.pipeThrough(new TextDecoderStream())
|
|
126
|
-
.getReader()
|
|
127
|
-
|
|
128
|
-
if (!reader) {
|
|
129
|
-
throw new Error('Response body is null')
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
while (!isCancelled && !signal?.aborted) {
|
|
133
|
-
try {
|
|
134
|
-
const { value, done } = await reader.read()
|
|
135
|
-
if (done) {
|
|
136
|
-
callbacks.onClose?.()
|
|
137
|
-
break
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const lines = value.split('\n')
|
|
141
|
-
for (const line of lines) {
|
|
142
|
-
if (!line.startsWith('data: ')) continue
|
|
143
|
-
|
|
144
|
-
try {
|
|
145
|
-
const jsonString = line.replace('data: ', '').trim()
|
|
146
|
-
const parsedData = JSON.parse(
|
|
147
|
-
jsonString
|
|
148
|
-
) as StreamingEndpointStream<T>
|
|
149
|
-
await callbacks.onMessage(parsedData)
|
|
150
|
-
} catch (parseError) {
|
|
151
|
-
console.error('Error parsing JSON:', parseError)
|
|
152
|
-
await callbacks.onError(
|
|
153
|
-
new Error('Error processing server response')
|
|
154
|
-
)
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
} catch (streamError) {
|
|
158
|
-
console.error('Stream error:', streamError)
|
|
159
|
-
await callbacks.onError(new Error('Error reading stream'))
|
|
160
|
-
break
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
const startStream = async (): Promise<void> => {
|
|
166
|
-
try {
|
|
167
|
-
const response = await fetch(this._buildApiUrl(endpoint), {
|
|
168
|
-
method: 'POST',
|
|
169
|
-
headers: {
|
|
170
|
-
'Content-Type': 'application/json',
|
|
171
|
-
Accept: 'text/event-stream',
|
|
172
|
-
},
|
|
173
|
-
body: JSON.stringify(data),
|
|
174
|
-
signal,
|
|
175
|
-
})
|
|
176
|
-
|
|
177
|
-
if (!response.ok) {
|
|
178
|
-
throw new Error(`HTTP error! status: ${response.status}`)
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
await processStream(response)
|
|
182
|
-
} catch (error) {
|
|
183
|
-
if (signal?.aborted) {
|
|
184
|
-
return
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
console.error('Request error:', error)
|
|
188
|
-
const errorMessage =
|
|
189
|
-
error instanceof Error
|
|
190
|
-
? error.message
|
|
191
|
-
: 'Error connecting to server'
|
|
192
|
-
await callbacks.onError(new Error(errorMessage))
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
try {
|
|
197
|
-
startStream()
|
|
198
|
-
} catch (error) {
|
|
199
|
-
const errorMessage =
|
|
200
|
-
error instanceof Error
|
|
201
|
-
? error.message
|
|
202
|
-
: 'Failed to start stream'
|
|
203
|
-
callbacks.onError(new Error(errorMessage))
|
|
204
|
-
}
|
|
205
|
-
|
|
206
|
-
return () => {
|
|
207
|
-
isCancelled = true
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
// Re-export types
|
|
213
|
-
export * from './types'
|
|
@@ -1,484 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest'
|
|
2
|
-
import { RagbitsClient } from '../index'
|
|
3
|
-
import { server } from './setup'
|
|
4
|
-
import { http, HttpResponse } from 'msw'
|
|
5
|
-
import type { FeedbackRequest } from '../types'
|
|
6
|
-
import { FeedbackType } from '../types'
|
|
7
|
-
import { defaultConfigResponse } from './utils'
|
|
8
|
-
|
|
9
|
-
describe('RagbitsClient', () => {
|
|
10
|
-
let client: RagbitsClient
|
|
11
|
-
|
|
12
|
-
beforeEach(() => {
|
|
13
|
-
client = new RagbitsClient()
|
|
14
|
-
})
|
|
15
|
-
|
|
16
|
-
describe('Constructor', () => {
|
|
17
|
-
it('should create client with default base URL', () => {
|
|
18
|
-
const defaultClient = new RagbitsClient()
|
|
19
|
-
expect(defaultClient).toBeInstanceOf(RagbitsClient)
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
it('should create client with custom base URL', () => {
|
|
23
|
-
const customClient = new RagbitsClient({
|
|
24
|
-
baseUrl: 'https://api.example.com',
|
|
25
|
-
})
|
|
26
|
-
expect(customClient).toBeInstanceOf(RagbitsClient)
|
|
27
|
-
})
|
|
28
|
-
|
|
29
|
-
it('should remove trailing slash from base URL', () => {
|
|
30
|
-
const clientWithSlash = new RagbitsClient({
|
|
31
|
-
baseUrl: 'https://api.example.com/',
|
|
32
|
-
})
|
|
33
|
-
expect(clientWithSlash).toBeInstanceOf(RagbitsClient)
|
|
34
|
-
})
|
|
35
|
-
|
|
36
|
-
it('should throw error for invalid base URL', () => {
|
|
37
|
-
expect(() => {
|
|
38
|
-
new RagbitsClient({ baseUrl: 'invalid-url' })
|
|
39
|
-
}).toThrow(
|
|
40
|
-
'Invalid base URL: invalid-url. Please provide a valid URL.'
|
|
41
|
-
)
|
|
42
|
-
})
|
|
43
|
-
})
|
|
44
|
-
|
|
45
|
-
describe('getBaseUrl', () => {
|
|
46
|
-
it('should return the base URL', () => {
|
|
47
|
-
expect(client.getBaseUrl()).toBe('http://127.0.0.1:8000')
|
|
48
|
-
})
|
|
49
|
-
})
|
|
50
|
-
|
|
51
|
-
describe('makeRequest', () => {
|
|
52
|
-
it('should make successful GET request', async () => {
|
|
53
|
-
const response = await client.makeRequest('/api/config')
|
|
54
|
-
|
|
55
|
-
expect(response).toEqual(defaultConfigResponse)
|
|
56
|
-
})
|
|
57
|
-
|
|
58
|
-
it('should make successful POST request with body', async () => {
|
|
59
|
-
const requestBody: FeedbackRequest = {
|
|
60
|
-
message_id: 'msg-123',
|
|
61
|
-
feedback: FeedbackType.LIKE,
|
|
62
|
-
payload: { reason: 'Great response!' },
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
const response = await client.makeRequest('/api/feedback', {
|
|
66
|
-
method: 'POST',
|
|
67
|
-
body: requestBody,
|
|
68
|
-
})
|
|
69
|
-
|
|
70
|
-
expect(response).toEqual({
|
|
71
|
-
status: 'success',
|
|
72
|
-
})
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('should handle request with custom headers', async () => {
|
|
76
|
-
server.use(
|
|
77
|
-
http.get('http://127.0.0.1:8000/api/config', ({ request }) => {
|
|
78
|
-
const customHeader = request.headers.get('X-Custom-Header')
|
|
79
|
-
return HttpResponse.json({
|
|
80
|
-
feedback: {
|
|
81
|
-
like: { enabled: true, form: null },
|
|
82
|
-
dislike: { enabled: false, form: null },
|
|
83
|
-
},
|
|
84
|
-
receivedHeader: customHeader,
|
|
85
|
-
})
|
|
86
|
-
})
|
|
87
|
-
)
|
|
88
|
-
|
|
89
|
-
const response = await client.makeRequest('/api/config', {
|
|
90
|
-
headers: {
|
|
91
|
-
'X-Custom-Header': 'test-value',
|
|
92
|
-
},
|
|
93
|
-
})
|
|
94
|
-
|
|
95
|
-
// Check that the header was received by accessing the custom property
|
|
96
|
-
const responseWithHeader = response as typeof response & {
|
|
97
|
-
receivedHeader: string
|
|
98
|
-
}
|
|
99
|
-
expect(responseWithHeader.receivedHeader).toBe('test-value')
|
|
100
|
-
})
|
|
101
|
-
|
|
102
|
-
it('should handle HTTP errors', async () => {
|
|
103
|
-
// Mock error response for testing
|
|
104
|
-
server.use(
|
|
105
|
-
http.get('http://127.0.0.1:8000/api/config', () => {
|
|
106
|
-
return new HttpResponse(null, { status: 500 })
|
|
107
|
-
})
|
|
108
|
-
)
|
|
109
|
-
|
|
110
|
-
await expect(client.makeRequest('/api/config')).rejects.toThrow(
|
|
111
|
-
'HTTP error! status: 500'
|
|
112
|
-
)
|
|
113
|
-
})
|
|
114
|
-
|
|
115
|
-
it('should handle request cancellation', async () => {
|
|
116
|
-
const abortController = new AbortController()
|
|
117
|
-
|
|
118
|
-
// Mock a slow config endpoint
|
|
119
|
-
server.use(
|
|
120
|
-
http.get('http://127.0.0.1:8000/api/config', () => {
|
|
121
|
-
return new Promise((resolve) => {
|
|
122
|
-
setTimeout(() => {
|
|
123
|
-
resolve(
|
|
124
|
-
HttpResponse.json({
|
|
125
|
-
feedback: {
|
|
126
|
-
like: { enabled: true, form: null },
|
|
127
|
-
dislike: { enabled: false, form: null },
|
|
128
|
-
},
|
|
129
|
-
})
|
|
130
|
-
)
|
|
131
|
-
}, 1000)
|
|
132
|
-
})
|
|
133
|
-
})
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
const requestPromise = client.makeRequest('/api/config', {
|
|
137
|
-
signal: abortController.signal,
|
|
138
|
-
})
|
|
139
|
-
|
|
140
|
-
// Cancel the request immediately
|
|
141
|
-
abortController.abort()
|
|
142
|
-
|
|
143
|
-
await expect(requestPromise).rejects.toThrow()
|
|
144
|
-
})
|
|
145
|
-
})
|
|
146
|
-
|
|
147
|
-
describe('makeStreamRequest', () => {
|
|
148
|
-
it('should handle streaming response', async () => {
|
|
149
|
-
const messages: unknown[] = []
|
|
150
|
-
const errors: Error[] = []
|
|
151
|
-
let closed = false
|
|
152
|
-
|
|
153
|
-
const cancelFn = client.makeStreamRequest(
|
|
154
|
-
'/api/chat',
|
|
155
|
-
{ message: 'Start streaming', history: [] },
|
|
156
|
-
{
|
|
157
|
-
onMessage: (data) => {
|
|
158
|
-
messages.push(data)
|
|
159
|
-
return Promise.resolve()
|
|
160
|
-
},
|
|
161
|
-
onError: (error) => {
|
|
162
|
-
errors.push(error)
|
|
163
|
-
return Promise.resolve()
|
|
164
|
-
},
|
|
165
|
-
onClose: () => {
|
|
166
|
-
closed = true
|
|
167
|
-
},
|
|
168
|
-
}
|
|
169
|
-
)
|
|
170
|
-
|
|
171
|
-
// Wait for stream to complete
|
|
172
|
-
await new Promise((resolve) => {
|
|
173
|
-
const checkComplete = () => {
|
|
174
|
-
if (closed || errors.length > 0) {
|
|
175
|
-
resolve(void 0)
|
|
176
|
-
} else {
|
|
177
|
-
setTimeout(checkComplete, 50)
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
checkComplete()
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
expect(messages).toHaveLength(4)
|
|
184
|
-
expect(messages[0]).toEqual({
|
|
185
|
-
type: 'text',
|
|
186
|
-
content: 'Hello there!',
|
|
187
|
-
})
|
|
188
|
-
expect(messages[1]).toEqual({
|
|
189
|
-
type: 'text',
|
|
190
|
-
content: 'How can I help you?',
|
|
191
|
-
})
|
|
192
|
-
expect(messages[2]).toEqual({
|
|
193
|
-
type: 'message_id',
|
|
194
|
-
content: 'msg-123',
|
|
195
|
-
})
|
|
196
|
-
expect(messages[3]).toEqual({
|
|
197
|
-
type: 'conversation_id',
|
|
198
|
-
content: 'conv-456',
|
|
199
|
-
})
|
|
200
|
-
expect(errors).toHaveLength(0)
|
|
201
|
-
expect(closed).toBe(true)
|
|
202
|
-
|
|
203
|
-
// Cleanup
|
|
204
|
-
cancelFn()
|
|
205
|
-
})
|
|
206
|
-
|
|
207
|
-
it('should handle stream cancellation', async () => {
|
|
208
|
-
const messages: unknown[] = []
|
|
209
|
-
const errors: Error[] = []
|
|
210
|
-
|
|
211
|
-
const cancelFn = client.makeStreamRequest(
|
|
212
|
-
'/api/chat',
|
|
213
|
-
{ message: 'Start streaming', history: [] },
|
|
214
|
-
{
|
|
215
|
-
onMessage: (data) => {
|
|
216
|
-
messages.push(data)
|
|
217
|
-
return Promise.resolve()
|
|
218
|
-
},
|
|
219
|
-
onError: (error) => {
|
|
220
|
-
errors.push(error)
|
|
221
|
-
return Promise.resolve()
|
|
222
|
-
},
|
|
223
|
-
}
|
|
224
|
-
)
|
|
225
|
-
|
|
226
|
-
// Cancel immediately
|
|
227
|
-
cancelFn()
|
|
228
|
-
|
|
229
|
-
// Wait a bit to ensure cancellation takes effect
|
|
230
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
231
|
-
|
|
232
|
-
expect(messages.length).toBeLessThan(4) // Should not receive all messages
|
|
233
|
-
})
|
|
234
|
-
|
|
235
|
-
it('should handle stream errors', async () => {
|
|
236
|
-
server.use(
|
|
237
|
-
http.post('http://127.0.0.1:8000/api/chat', () => {
|
|
238
|
-
return new HttpResponse(null, { status: 500 })
|
|
239
|
-
})
|
|
240
|
-
)
|
|
241
|
-
|
|
242
|
-
const errors: Error[] = []
|
|
243
|
-
|
|
244
|
-
client.makeStreamRequest(
|
|
245
|
-
'/api/chat',
|
|
246
|
-
{ message: 'Start streaming', history: [] },
|
|
247
|
-
{
|
|
248
|
-
onMessage: () => Promise.resolve(),
|
|
249
|
-
onError: (error) => {
|
|
250
|
-
errors.push(error)
|
|
251
|
-
return Promise.resolve()
|
|
252
|
-
},
|
|
253
|
-
}
|
|
254
|
-
)
|
|
255
|
-
|
|
256
|
-
// Wait for error to be captured
|
|
257
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
258
|
-
|
|
259
|
-
expect(errors).toHaveLength(1)
|
|
260
|
-
expect(errors[0].message).toContain('HTTP error! status: 500')
|
|
261
|
-
})
|
|
262
|
-
|
|
263
|
-
it('should handle malformed JSON in stream', async () => {
|
|
264
|
-
server.use(
|
|
265
|
-
http.post('http://127.0.0.1:8000/api/chat', () => {
|
|
266
|
-
const encoder = new TextEncoder()
|
|
267
|
-
|
|
268
|
-
const stream = new ReadableStream({
|
|
269
|
-
start(controller) {
|
|
270
|
-
controller.enqueue(
|
|
271
|
-
encoder.encode('data: invalid-json\n\n')
|
|
272
|
-
)
|
|
273
|
-
controller.close()
|
|
274
|
-
},
|
|
275
|
-
})
|
|
276
|
-
|
|
277
|
-
return new HttpResponse(stream, {
|
|
278
|
-
headers: {
|
|
279
|
-
'Content-Type': 'text/event-stream',
|
|
280
|
-
},
|
|
281
|
-
})
|
|
282
|
-
})
|
|
283
|
-
)
|
|
284
|
-
|
|
285
|
-
const errors: Error[] = []
|
|
286
|
-
|
|
287
|
-
client.makeStreamRequest(
|
|
288
|
-
'/api/chat',
|
|
289
|
-
{ message: 'Start streaming', history: [] },
|
|
290
|
-
{
|
|
291
|
-
onMessage: () => Promise.resolve(),
|
|
292
|
-
onError: (error) => {
|
|
293
|
-
errors.push(error)
|
|
294
|
-
return Promise.resolve()
|
|
295
|
-
},
|
|
296
|
-
}
|
|
297
|
-
)
|
|
298
|
-
|
|
299
|
-
// Wait for error to be captured
|
|
300
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
301
|
-
|
|
302
|
-
expect(errors).toHaveLength(1)
|
|
303
|
-
expect(errors[0].message).toBe('Error processing server response')
|
|
304
|
-
})
|
|
305
|
-
|
|
306
|
-
it('should handle stream with AbortSignal', async () => {
|
|
307
|
-
const abortController = new AbortController()
|
|
308
|
-
const messages: unknown[] = []
|
|
309
|
-
const errors: Error[] = []
|
|
310
|
-
|
|
311
|
-
client.makeStreamRequest(
|
|
312
|
-
'/api/chat',
|
|
313
|
-
{ message: 'Start streaming', history: [] },
|
|
314
|
-
{
|
|
315
|
-
onMessage: (data) => {
|
|
316
|
-
messages.push(data)
|
|
317
|
-
return Promise.resolve()
|
|
318
|
-
},
|
|
319
|
-
onError: (error) => {
|
|
320
|
-
errors.push(error)
|
|
321
|
-
return Promise.resolve()
|
|
322
|
-
},
|
|
323
|
-
},
|
|
324
|
-
abortController.signal
|
|
325
|
-
)
|
|
326
|
-
|
|
327
|
-
// Abort after receiving first message (shorter delay to be more reliable)
|
|
328
|
-
setTimeout(() => {
|
|
329
|
-
abortController.abort()
|
|
330
|
-
}, 5)
|
|
331
|
-
|
|
332
|
-
// Wait longer for abort to take effect
|
|
333
|
-
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
334
|
-
|
|
335
|
-
// Should receive fewer than 4 messages due to abort
|
|
336
|
-
// Allow for some variation in timing but ensure it's not all 4
|
|
337
|
-
expect(messages.length).toBeLessThanOrEqual(3)
|
|
338
|
-
})
|
|
339
|
-
|
|
340
|
-
it('should handle null response body', async () => {
|
|
341
|
-
server.use(
|
|
342
|
-
http.post('http://127.0.0.1:8000/api/chat', () => {
|
|
343
|
-
return new HttpResponse(null, {
|
|
344
|
-
headers: {
|
|
345
|
-
'Content-Type': 'text/event-stream',
|
|
346
|
-
},
|
|
347
|
-
})
|
|
348
|
-
})
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
const errors: Error[] = []
|
|
352
|
-
|
|
353
|
-
client.makeStreamRequest(
|
|
354
|
-
'/api/chat',
|
|
355
|
-
{ message: 'Start streaming', history: [] },
|
|
356
|
-
{
|
|
357
|
-
onMessage: () => Promise.resolve(),
|
|
358
|
-
onError: (error) => {
|
|
359
|
-
errors.push(error)
|
|
360
|
-
return Promise.resolve()
|
|
361
|
-
},
|
|
362
|
-
}
|
|
363
|
-
)
|
|
364
|
-
|
|
365
|
-
// Wait for error to be captured
|
|
366
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
367
|
-
|
|
368
|
-
expect(errors).toHaveLength(1)
|
|
369
|
-
expect(errors[0].message).toBe('Response body is null')
|
|
370
|
-
})
|
|
371
|
-
|
|
372
|
-
it('should handle reader stream errors', async () => {
|
|
373
|
-
server.use(
|
|
374
|
-
http.post('http://127.0.0.1:8000/api/chat', () => {
|
|
375
|
-
const stream = new ReadableStream({
|
|
376
|
-
start(controller) {
|
|
377
|
-
// Simulate a stream error by enqueuing something and then erroring
|
|
378
|
-
const encoder = new TextEncoder()
|
|
379
|
-
controller.enqueue(
|
|
380
|
-
encoder.encode(
|
|
381
|
-
'data: {"type": "text", "content": "Starting"}\n\n'
|
|
382
|
-
)
|
|
383
|
-
)
|
|
384
|
-
// Force an error in the stream
|
|
385
|
-
controller.error(new Error('Stream read error'))
|
|
386
|
-
},
|
|
387
|
-
})
|
|
388
|
-
|
|
389
|
-
return new HttpResponse(stream, {
|
|
390
|
-
headers: {
|
|
391
|
-
'Content-Type': 'text/event-stream',
|
|
392
|
-
},
|
|
393
|
-
})
|
|
394
|
-
})
|
|
395
|
-
)
|
|
396
|
-
|
|
397
|
-
const errors: Error[] = []
|
|
398
|
-
const messages: unknown[] = []
|
|
399
|
-
|
|
400
|
-
client.makeStreamRequest(
|
|
401
|
-
'/api/chat',
|
|
402
|
-
{ message: 'Start streaming', history: [] },
|
|
403
|
-
{
|
|
404
|
-
onMessage: (data) => {
|
|
405
|
-
messages.push(data)
|
|
406
|
-
return Promise.resolve()
|
|
407
|
-
},
|
|
408
|
-
onError: (error) => {
|
|
409
|
-
errors.push(error)
|
|
410
|
-
return Promise.resolve()
|
|
411
|
-
},
|
|
412
|
-
}
|
|
413
|
-
)
|
|
414
|
-
|
|
415
|
-
// Wait for error to be captured
|
|
416
|
-
await new Promise((resolve) => setTimeout(resolve, 200))
|
|
417
|
-
|
|
418
|
-
expect(errors).toHaveLength(1)
|
|
419
|
-
expect(errors[0].message).toBe('Error reading stream')
|
|
420
|
-
})
|
|
421
|
-
|
|
422
|
-
it('should handle synchronous errors in makeStreamRequest', async () => {
|
|
423
|
-
const errors: Error[] = []
|
|
424
|
-
|
|
425
|
-
// Create a spy to force a synchronous error
|
|
426
|
-
const originalFetch = global.fetch
|
|
427
|
-
global.fetch = vi.fn().mockImplementation(() => {
|
|
428
|
-
throw new Error('Synchronous fetch error')
|
|
429
|
-
})
|
|
430
|
-
|
|
431
|
-
client.makeStreamRequest(
|
|
432
|
-
'/api/chat',
|
|
433
|
-
{ message: 'Start streaming', history: [] },
|
|
434
|
-
{
|
|
435
|
-
onMessage: () => Promise.resolve(),
|
|
436
|
-
onError: (error) => {
|
|
437
|
-
errors.push(error)
|
|
438
|
-
return Promise.resolve()
|
|
439
|
-
},
|
|
440
|
-
}
|
|
441
|
-
)
|
|
442
|
-
|
|
443
|
-
// Wait for error to be captured
|
|
444
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
445
|
-
|
|
446
|
-
expect(errors).toHaveLength(1)
|
|
447
|
-
expect(errors[0].message).toBe('Synchronous fetch error')
|
|
448
|
-
|
|
449
|
-
// Restore original fetch
|
|
450
|
-
global.fetch = originalFetch
|
|
451
|
-
})
|
|
452
|
-
|
|
453
|
-
it('should handle non-Error exceptions in stream processing', async () => {
|
|
454
|
-
const errors: Error[] = []
|
|
455
|
-
|
|
456
|
-
// Mock fetch to throw a non-Error object
|
|
457
|
-
const originalFetch = global.fetch
|
|
458
|
-
global.fetch = vi.fn().mockImplementation(() => {
|
|
459
|
-
throw 'String error' // Non-Error exception
|
|
460
|
-
})
|
|
461
|
-
|
|
462
|
-
client.makeStreamRequest(
|
|
463
|
-
'/api/chat',
|
|
464
|
-
{ message: 'Start streaming', history: [] },
|
|
465
|
-
{
|
|
466
|
-
onMessage: () => Promise.resolve(),
|
|
467
|
-
onError: (error) => {
|
|
468
|
-
errors.push(error)
|
|
469
|
-
return Promise.resolve()
|
|
470
|
-
},
|
|
471
|
-
}
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
// Wait for error to be captured
|
|
475
|
-
await new Promise((resolve) => setTimeout(resolve, 100))
|
|
476
|
-
|
|
477
|
-
expect(errors).toHaveLength(1)
|
|
478
|
-
expect(errors[0].message).toBe('Error connecting to server')
|
|
479
|
-
|
|
480
|
-
// Restore original fetch
|
|
481
|
-
global.fetch = originalFetch
|
|
482
|
-
})
|
|
483
|
-
})
|
|
484
|
-
})
|