spiceflow 1.17.11 → 1.18.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 +168 -3
- package/dist/client/errors.d.ts +2 -1
- package/dist/client/errors.d.ts.map +1 -1
- package/dist/client/errors.js +3 -1
- package/dist/client/errors.js.map +1 -1
- package/dist/client/fetch.d.ts +86 -0
- package/dist/client/fetch.d.ts.map +1 -0
- package/dist/client/fetch.js +143 -0
- package/dist/client/fetch.js.map +1 -0
- package/dist/client/index.d.ts +4 -9
- package/dist/client/index.d.ts.map +1 -1
- package/dist/client/index.js +39 -151
- package/dist/client/index.js.map +1 -1
- package/dist/client/shared.d.ts +47 -0
- package/dist/client/shared.d.ts.map +1 -0
- package/dist/client/shared.js +314 -0
- package/dist/client/shared.js.map +1 -0
- package/dist/client/types.d.ts +3 -1
- package/dist/client/types.d.ts.map +1 -1
- package/dist/client.test.js +43 -0
- package/dist/client.test.js.map +1 -1
- package/dist/fetch-client.test.d.ts +2 -0
- package/dist/fetch-client.test.d.ts.map +1 -0
- package/dist/fetch-client.test.js +362 -0
- package/dist/fetch-client.test.js.map +1 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp-client-transport.d.ts.map +1 -1
- package/dist/mcp-client-transport.js +5 -2
- package/dist/mcp-client-transport.js.map +1 -1
- package/dist/mcp.d.ts +1 -1
- package/dist/mcp.d.ts.map +1 -1
- package/dist/openapi.d.ts +1 -1
- package/dist/openapi.d.ts.map +1 -1
- package/dist/spiceflow.d.ts +36 -14
- package/dist/spiceflow.d.ts.map +1 -1
- package/dist/spiceflow.js +49 -16
- package/dist/spiceflow.js.map +1 -1
- package/dist/spiceflow.test.js +205 -1
- package/dist/spiceflow.test.js.map +1 -1
- package/dist/stream.test.js +1 -1
- package/dist/stream.test.js.map +1 -1
- package/package.json +3 -3
- package/src/client/errors.ts +3 -0
- package/src/client/fetch.ts +447 -0
- package/src/client/index.ts +73 -192
- package/src/client/shared.ts +406 -0
- package/src/client/types.ts +3 -1
- package/src/client.test.ts +52 -0
- package/src/fetch-client.test.ts +411 -0
- package/src/index.ts +1 -1
- package/src/mcp-client-transport.ts +5 -2
- package/src/spiceflow.test.ts +315 -1
- package/src/spiceflow.ts +106 -32
- package/src/stream.test.ts +1 -1
package/src/client/index.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import type { AnySpiceflow, Spiceflow } from '../spiceflow.ts'
|
|
2
|
-
import superjson from 'superjson'
|
|
3
|
-
import { EventSourceParserStream } from 'eventsource-parser/stream'
|
|
4
2
|
|
|
5
3
|
import type { SpiceflowClient } from './types.ts'
|
|
6
4
|
|
|
@@ -10,6 +8,25 @@ import { SpiceflowFetchError } from './errors.ts'
|
|
|
10
8
|
|
|
11
9
|
import { parseStringifiedValue } from './utils.ts'
|
|
12
10
|
|
|
11
|
+
import {
|
|
12
|
+
isServer,
|
|
13
|
+
isFile,
|
|
14
|
+
hasFile,
|
|
15
|
+
createNewFile,
|
|
16
|
+
processHeaders,
|
|
17
|
+
streamSSEResponse,
|
|
18
|
+
tryParsingSSEJson,
|
|
19
|
+
superjsonDeserialize,
|
|
20
|
+
TextDecoderStream,
|
|
21
|
+
isAbortError,
|
|
22
|
+
type SSEEvent,
|
|
23
|
+
} from './shared.ts'
|
|
24
|
+
|
|
25
|
+
export { streamSSEResponse, TextDecoderStream }
|
|
26
|
+
|
|
27
|
+
export { createSpiceflowFetch } from './fetch.ts'
|
|
28
|
+
export type { SpiceflowFetch } from './fetch.ts'
|
|
29
|
+
|
|
13
30
|
const method = [
|
|
14
31
|
'get',
|
|
15
32
|
'post',
|
|
@@ -22,177 +39,6 @@ const method = [
|
|
|
22
39
|
'subscribe',
|
|
23
40
|
] as const
|
|
24
41
|
|
|
25
|
-
const isServer = typeof FileList === 'undefined'
|
|
26
|
-
|
|
27
|
-
const isFile = (v: any) => {
|
|
28
|
-
if (isServer) return v instanceof Blob
|
|
29
|
-
|
|
30
|
-
return v instanceof FileList || v instanceof File
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
// FormData is 1 level deep
|
|
34
|
-
const hasFile = (obj: Record<string, any>) => {
|
|
35
|
-
if (!obj) return false
|
|
36
|
-
|
|
37
|
-
for (const key in obj) {
|
|
38
|
-
if (isFile(obj[key])) return true
|
|
39
|
-
|
|
40
|
-
if (Array.isArray(obj[key]) && (obj[key] as unknown[]).find(isFile))
|
|
41
|
-
return true
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
return false
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
const createNewFile = (v: File) =>
|
|
48
|
-
isServer
|
|
49
|
-
? v
|
|
50
|
-
: new Promise<File>((resolve) => {
|
|
51
|
-
const reader = new FileReader()
|
|
52
|
-
|
|
53
|
-
reader.onload = () => {
|
|
54
|
-
const file = new File([reader.result!], v.name, {
|
|
55
|
-
lastModified: v.lastModified,
|
|
56
|
-
type: v.type,
|
|
57
|
-
})
|
|
58
|
-
resolve(file)
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
reader.readAsArrayBuffer(v)
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
const processHeaders = (
|
|
65
|
-
h: SpiceflowClient.Config['headers'],
|
|
66
|
-
path: string,
|
|
67
|
-
options: RequestInit = {},
|
|
68
|
-
headers: Record<string, string> = {},
|
|
69
|
-
): Record<string, string> => {
|
|
70
|
-
if (Array.isArray(h)) {
|
|
71
|
-
for (const value of h)
|
|
72
|
-
if (!Array.isArray(value))
|
|
73
|
-
headers = processHeaders(value, path, options, headers)
|
|
74
|
-
else {
|
|
75
|
-
const key = value[0]
|
|
76
|
-
if (typeof key === 'string')
|
|
77
|
-
headers[key.toLowerCase()] = value[1] as string
|
|
78
|
-
else
|
|
79
|
-
for (const [k, value] of key)
|
|
80
|
-
headers[k.toLowerCase()] = value as string
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
return headers
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!h) return headers
|
|
87
|
-
|
|
88
|
-
switch (typeof h) {
|
|
89
|
-
case 'function':
|
|
90
|
-
if (typeof Headers !== 'undefined' && h instanceof Headers)
|
|
91
|
-
return processHeaders(h, path, options, headers)
|
|
92
|
-
|
|
93
|
-
const v = h(path, options)
|
|
94
|
-
if (v) return processHeaders(v, path, options, headers)
|
|
95
|
-
return headers
|
|
96
|
-
|
|
97
|
-
case 'object':
|
|
98
|
-
if (typeof Headers !== 'undefined' && h instanceof Headers) {
|
|
99
|
-
h.forEach((value, key) => {
|
|
100
|
-
headers[key.toLowerCase()] = value
|
|
101
|
-
})
|
|
102
|
-
return headers
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
for (const [key, value] of Object.entries(h))
|
|
106
|
-
headers[key.toLowerCase()] = value as string
|
|
107
|
-
|
|
108
|
-
return headers
|
|
109
|
-
|
|
110
|
-
default:
|
|
111
|
-
return headers
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
interface SSEEvent {
|
|
116
|
-
event?: string
|
|
117
|
-
data: any
|
|
118
|
-
id?: string
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
export class TextDecoderStream extends TransformStream<Uint8Array, string> {
|
|
122
|
-
constructor() {
|
|
123
|
-
const decoder = new TextDecoder('utf-8', {
|
|
124
|
-
fatal: true,
|
|
125
|
-
ignoreBOM: true,
|
|
126
|
-
})
|
|
127
|
-
super({
|
|
128
|
-
transform(
|
|
129
|
-
chunk: Uint8Array,
|
|
130
|
-
controller: TransformStreamDefaultController<string>,
|
|
131
|
-
) {
|
|
132
|
-
const decoded = decoder.decode(chunk, { stream: true })
|
|
133
|
-
if (decoded.length > 0) {
|
|
134
|
-
controller.enqueue(decoded)
|
|
135
|
-
}
|
|
136
|
-
},
|
|
137
|
-
flush(controller: TransformStreamDefaultController<string>) {
|
|
138
|
-
const output = decoder.decode()
|
|
139
|
-
if (output.length > 0) {
|
|
140
|
-
controller.enqueue(output)
|
|
141
|
-
}
|
|
142
|
-
},
|
|
143
|
-
})
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
function isAbortError(error: unknown): error is Error {
|
|
148
|
-
return (
|
|
149
|
-
(error instanceof Error || error instanceof DOMException) &&
|
|
150
|
-
(error.name === 'AbortError' ||
|
|
151
|
-
error.name === 'ResponseAborted' || // Next.js
|
|
152
|
-
error.name === 'TimeoutError')
|
|
153
|
-
)
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
export async function* streamSSEResponse(
|
|
157
|
-
response: Response,
|
|
158
|
-
map: (x: SSEEvent) => any,
|
|
159
|
-
): AsyncGenerator<SSEEvent> {
|
|
160
|
-
const body = response.body
|
|
161
|
-
if (!body) return
|
|
162
|
-
|
|
163
|
-
const eventStream = response.body
|
|
164
|
-
.pipeThrough(new TextDecoderStream())
|
|
165
|
-
.pipeThrough(new EventSourceParserStream())
|
|
166
|
-
|
|
167
|
-
let reader = eventStream.getReader()
|
|
168
|
-
try {
|
|
169
|
-
while (true) {
|
|
170
|
-
const { done, value: event } = await reader.read()
|
|
171
|
-
if (done) break
|
|
172
|
-
if (event?.event === 'error') {
|
|
173
|
-
throw new SpiceflowFetchError(500, superjsonDeserialize(event.data))
|
|
174
|
-
}
|
|
175
|
-
if (event) {
|
|
176
|
-
yield map({ ...event, data: event.data })
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
} catch (error) {
|
|
180
|
-
if (isAbortError(error)) {
|
|
181
|
-
return
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
throw error
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
function tryParsingSSEJson(data: string): any {
|
|
189
|
-
try {
|
|
190
|
-
return superjsonDeserialize(JSON.parse(data))
|
|
191
|
-
} catch (error) {
|
|
192
|
-
return data
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
|
|
196
42
|
const createProxy = (
|
|
197
43
|
domain: string,
|
|
198
44
|
config: SpiceflowClient.Config & { state?: any },
|
|
@@ -226,7 +72,13 @@ const createProxy = (
|
|
|
226
72
|
const method = methodPaths.pop()
|
|
227
73
|
const path = '/' + methodPaths.join('/')
|
|
228
74
|
|
|
229
|
-
let {
|
|
75
|
+
let {
|
|
76
|
+
fetch: fetcher = fetch,
|
|
77
|
+
headers,
|
|
78
|
+
onRequest,
|
|
79
|
+
onResponse,
|
|
80
|
+
retries = 0,
|
|
81
|
+
} = config
|
|
230
82
|
|
|
231
83
|
const isGetOrHead =
|
|
232
84
|
method === 'get' || method === 'head' || method === 'subscribe'
|
|
@@ -394,11 +246,46 @@ const createProxy = (
|
|
|
394
246
|
}
|
|
395
247
|
|
|
396
248
|
const url = domain + path + q
|
|
397
|
-
|
|
398
|
-
const
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
249
|
+
|
|
250
|
+
const executeRequest = async (): Promise<Response> => {
|
|
251
|
+
let attempt = 0
|
|
252
|
+
let response: Response
|
|
253
|
+
let lastError: Error | null = null
|
|
254
|
+
|
|
255
|
+
while (attempt <= retries) {
|
|
256
|
+
try {
|
|
257
|
+
response = await (instance?.handle(
|
|
258
|
+
new Request(url, fetchInit),
|
|
259
|
+
{ state: config.state },
|
|
260
|
+
) ?? fetcher!(url, fetchInit))
|
|
261
|
+
|
|
262
|
+
if (response.status < 500 || attempt === retries) {
|
|
263
|
+
break
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
lastError = new Error(
|
|
267
|
+
`Server error: ${response.status} ${response.statusText}`,
|
|
268
|
+
)
|
|
269
|
+
} catch (err) {
|
|
270
|
+
lastError = err as Error
|
|
271
|
+
if (attempt === retries) {
|
|
272
|
+
throw err
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
attempt++
|
|
277
|
+
const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000)
|
|
278
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs))
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!response!) {
|
|
282
|
+
throw lastError || new Error('Failed to fetch after retries')
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return response
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
const response = await executeRequest()
|
|
402
289
|
|
|
403
290
|
let data = null as any
|
|
404
291
|
let error = null as any
|
|
@@ -429,8 +316,13 @@ const createProxy = (
|
|
|
429
316
|
|
|
430
317
|
switch (response.headers.get('Content-Type')?.split(';')[0]) {
|
|
431
318
|
case 'text/event-stream':
|
|
432
|
-
data = streamSSEResponse(
|
|
433
|
-
|
|
319
|
+
data = streamSSEResponse({
|
|
320
|
+
response,
|
|
321
|
+
map: (x) => {
|
|
322
|
+
return tryParsingSSEJson(x.data)
|
|
323
|
+
},
|
|
324
|
+
executeRequest,
|
|
325
|
+
maxRetries: retries,
|
|
434
326
|
})
|
|
435
327
|
|
|
436
328
|
break
|
|
@@ -512,14 +404,3 @@ export const createSpiceflowClient = <const App extends AnySpiceflow>(
|
|
|
512
404
|
|
|
513
405
|
return createProxy('http://e.ly', config || {}, [], domain)
|
|
514
406
|
}
|
|
515
|
-
|
|
516
|
-
function superjsonDeserialize(data: any) {
|
|
517
|
-
if (data?.__superjsonMeta) {
|
|
518
|
-
const { __superjsonMeta, ...rest } = data
|
|
519
|
-
return superjson.deserialize({
|
|
520
|
-
json: rest,
|
|
521
|
-
meta: __superjsonMeta,
|
|
522
|
-
})
|
|
523
|
-
}
|
|
524
|
-
return data
|
|
525
|
-
}
|
|
@@ -0,0 +1,406 @@
|
|
|
1
|
+
// Shared utilities used by both the proxy client and the fetch client
|
|
2
|
+
import superjson from 'superjson'
|
|
3
|
+
import { EventSourceParserStream } from 'eventsource-parser/stream'
|
|
4
|
+
|
|
5
|
+
import type { SpiceflowClient } from './types.ts'
|
|
6
|
+
import { SpiceflowFetchError } from './errors.ts'
|
|
7
|
+
import { parseStringifiedValue } from './utils.ts'
|
|
8
|
+
import type { AnySpiceflow } from '../spiceflow.ts'
|
|
9
|
+
|
|
10
|
+
export const isServer = typeof FileList === 'undefined'
|
|
11
|
+
|
|
12
|
+
export const isFile = (v: any) => {
|
|
13
|
+
if (isServer) return v instanceof Blob
|
|
14
|
+
return v instanceof FileList || v instanceof File
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export const hasFile = (obj: Record<string, any>) => {
|
|
18
|
+
if (!obj) return false
|
|
19
|
+
for (const key in obj) {
|
|
20
|
+
if (isFile(obj[key])) return true
|
|
21
|
+
if (Array.isArray(obj[key]) && (obj[key] as unknown[]).find(isFile))
|
|
22
|
+
return true
|
|
23
|
+
}
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const createNewFile = (v: File) =>
|
|
28
|
+
isServer
|
|
29
|
+
? v
|
|
30
|
+
: new Promise<File>((resolve) => {
|
|
31
|
+
const reader = new FileReader()
|
|
32
|
+
reader.onload = () => {
|
|
33
|
+
const file = new File([reader.result!], v.name, {
|
|
34
|
+
lastModified: v.lastModified,
|
|
35
|
+
type: v.type,
|
|
36
|
+
})
|
|
37
|
+
resolve(file)
|
|
38
|
+
}
|
|
39
|
+
reader.readAsArrayBuffer(v)
|
|
40
|
+
})
|
|
41
|
+
|
|
42
|
+
export const processHeaders = (
|
|
43
|
+
h: SpiceflowClient.Config['headers'],
|
|
44
|
+
path: string,
|
|
45
|
+
options: RequestInit = {},
|
|
46
|
+
headers: Record<string, string> = {},
|
|
47
|
+
): Record<string, string> => {
|
|
48
|
+
if (Array.isArray(h)) {
|
|
49
|
+
for (const value of h)
|
|
50
|
+
if (!Array.isArray(value))
|
|
51
|
+
headers = processHeaders(value, path, options, headers)
|
|
52
|
+
else {
|
|
53
|
+
const key = value[0]
|
|
54
|
+
if (typeof key === 'string')
|
|
55
|
+
headers[key.toLowerCase()] = value[1] as string
|
|
56
|
+
else
|
|
57
|
+
for (const [k, value] of key)
|
|
58
|
+
headers[k.toLowerCase()] = value as string
|
|
59
|
+
}
|
|
60
|
+
return headers
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!h) return headers
|
|
64
|
+
|
|
65
|
+
switch (typeof h) {
|
|
66
|
+
case 'function':
|
|
67
|
+
if (typeof Headers !== 'undefined' && h instanceof Headers)
|
|
68
|
+
return processHeaders(h, path, options, headers)
|
|
69
|
+
const v = h(path, options)
|
|
70
|
+
if (v) return processHeaders(v, path, options, headers)
|
|
71
|
+
return headers
|
|
72
|
+
|
|
73
|
+
case 'object':
|
|
74
|
+
if (typeof Headers !== 'undefined' && h instanceof Headers) {
|
|
75
|
+
h.forEach((value, key) => {
|
|
76
|
+
headers[key.toLowerCase()] = value
|
|
77
|
+
})
|
|
78
|
+
return headers
|
|
79
|
+
}
|
|
80
|
+
for (const [key, value] of Object.entries(h))
|
|
81
|
+
headers[key.toLowerCase()] = value as string
|
|
82
|
+
return headers
|
|
83
|
+
|
|
84
|
+
default:
|
|
85
|
+
return headers
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface SSEEvent {
|
|
90
|
+
event?: string
|
|
91
|
+
data: any
|
|
92
|
+
id?: string
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export class TextDecoderStream extends TransformStream<Uint8Array, string> {
|
|
96
|
+
constructor() {
|
|
97
|
+
const decoder = new TextDecoder('utf-8', {
|
|
98
|
+
fatal: true,
|
|
99
|
+
ignoreBOM: true,
|
|
100
|
+
})
|
|
101
|
+
super({
|
|
102
|
+
transform(
|
|
103
|
+
chunk: Uint8Array,
|
|
104
|
+
controller: TransformStreamDefaultController<string>,
|
|
105
|
+
) {
|
|
106
|
+
const decoded = decoder.decode(chunk, { stream: true })
|
|
107
|
+
if (decoded.length > 0) {
|
|
108
|
+
controller.enqueue(decoded)
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
flush(controller: TransformStreamDefaultController<string>) {
|
|
112
|
+
const output = decoder.decode()
|
|
113
|
+
if (output.length > 0) {
|
|
114
|
+
controller.enqueue(output)
|
|
115
|
+
}
|
|
116
|
+
},
|
|
117
|
+
})
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function isAbortError(error: unknown): error is Error {
|
|
122
|
+
return (
|
|
123
|
+
(error instanceof Error || error instanceof DOMException) &&
|
|
124
|
+
(error.name === 'AbortError' ||
|
|
125
|
+
error.name === 'ResponseAborted' || // Next.js
|
|
126
|
+
error.name === 'TimeoutError')
|
|
127
|
+
)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export async function* streamSSEResponse({
|
|
131
|
+
response,
|
|
132
|
+
map,
|
|
133
|
+
executeRequest,
|
|
134
|
+
maxRetries = 0,
|
|
135
|
+
}: {
|
|
136
|
+
response: Response
|
|
137
|
+
map: (x: SSEEvent) => any
|
|
138
|
+
executeRequest?: () => Promise<Response>
|
|
139
|
+
maxRetries?: number
|
|
140
|
+
}): AsyncGenerator<SSEEvent> {
|
|
141
|
+
let currentResponse = response
|
|
142
|
+
let retriesLeft = maxRetries
|
|
143
|
+
|
|
144
|
+
while (true) {
|
|
145
|
+
const body = currentResponse.body
|
|
146
|
+
if (!body) return
|
|
147
|
+
|
|
148
|
+
const eventStream = body
|
|
149
|
+
.pipeThrough(new TextDecoderStream())
|
|
150
|
+
.pipeThrough(new EventSourceParserStream())
|
|
151
|
+
|
|
152
|
+
let reader = eventStream.getReader()
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
while (true) {
|
|
156
|
+
const { done, value: event } = await reader.read()
|
|
157
|
+
if (done) return
|
|
158
|
+
|
|
159
|
+
if (event?.event === 'error') {
|
|
160
|
+
const error = superjsonDeserialize(event.data)
|
|
161
|
+
throw new SpiceflowFetchError(500, error)
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (event) {
|
|
165
|
+
yield map({ ...event, data: event.data })
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
if (isAbortError(error)) {
|
|
170
|
+
return
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (
|
|
174
|
+
executeRequest &&
|
|
175
|
+
error instanceof SpiceflowFetchError &&
|
|
176
|
+
error.status >= 500 &&
|
|
177
|
+
retriesLeft > 0
|
|
178
|
+
) {
|
|
179
|
+
retriesLeft--
|
|
180
|
+
const backoffMs = Math.min(
|
|
181
|
+
1000 * 2 ** (maxRetries - retriesLeft - 1),
|
|
182
|
+
10000,
|
|
183
|
+
)
|
|
184
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs))
|
|
185
|
+
|
|
186
|
+
try {
|
|
187
|
+
currentResponse = await executeRequest()
|
|
188
|
+
if (currentResponse.status >= 500) {
|
|
189
|
+
if (retriesLeft === 0) {
|
|
190
|
+
throw error
|
|
191
|
+
}
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
} catch (retryError) {
|
|
195
|
+
if (retriesLeft === 0) {
|
|
196
|
+
throw retryError
|
|
197
|
+
}
|
|
198
|
+
continue
|
|
199
|
+
}
|
|
200
|
+
} else {
|
|
201
|
+
throw error
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
export function tryParsingSSEJson(data: string): any {
|
|
208
|
+
try {
|
|
209
|
+
return superjsonDeserialize(JSON.parse(data))
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return data
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
export function superjsonDeserialize(data: any) {
|
|
216
|
+
if (data?.__superjsonMeta) {
|
|
217
|
+
const { __superjsonMeta, ...rest } = data
|
|
218
|
+
return superjson.deserialize({
|
|
219
|
+
json: rest,
|
|
220
|
+
meta: __superjsonMeta,
|
|
221
|
+
})
|
|
222
|
+
}
|
|
223
|
+
return data
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
export function buildQueryString(
|
|
227
|
+
query: Record<string, string | string[] | number | boolean | object | undefined | null> | undefined,
|
|
228
|
+
): string {
|
|
229
|
+
if (!query) return ''
|
|
230
|
+
let q = ''
|
|
231
|
+
const append = (key: string, value: string) => {
|
|
232
|
+
q +=
|
|
233
|
+
(q ? '&' : '?') +
|
|
234
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value)}`
|
|
235
|
+
}
|
|
236
|
+
for (const [key, value] of Object.entries(query)) {
|
|
237
|
+
if (value === undefined || value === null) continue
|
|
238
|
+
if (Array.isArray(value)) {
|
|
239
|
+
for (const v of value) append(key, v)
|
|
240
|
+
continue
|
|
241
|
+
}
|
|
242
|
+
if (typeof value === 'object') {
|
|
243
|
+
append(key, JSON.stringify(value))
|
|
244
|
+
continue
|
|
245
|
+
}
|
|
246
|
+
append(key, `${value}`)
|
|
247
|
+
}
|
|
248
|
+
return q
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export async function serializeBody({
|
|
252
|
+
body,
|
|
253
|
+
fetchInit,
|
|
254
|
+
isGetOrHead,
|
|
255
|
+
}: {
|
|
256
|
+
body: any
|
|
257
|
+
fetchInit: RequestInit
|
|
258
|
+
isGetOrHead: boolean
|
|
259
|
+
}): Promise<void> {
|
|
260
|
+
if (isGetOrHead) {
|
|
261
|
+
delete fetchInit.body
|
|
262
|
+
return
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
if (hasFile(body)) {
|
|
266
|
+
const formData = new FormData()
|
|
267
|
+
for (const [key, field] of Object.entries(body)) {
|
|
268
|
+
if (isServer) {
|
|
269
|
+
formData.append(key, field as any)
|
|
270
|
+
continue
|
|
271
|
+
}
|
|
272
|
+
if (field instanceof File) {
|
|
273
|
+
formData.append(key, await createNewFile(field as any))
|
|
274
|
+
continue
|
|
275
|
+
}
|
|
276
|
+
if (field instanceof FileList) {
|
|
277
|
+
for (let i = 0; i < field.length; i++)
|
|
278
|
+
formData.append(key as any, await createNewFile((field as any)[i]))
|
|
279
|
+
continue
|
|
280
|
+
}
|
|
281
|
+
if (Array.isArray(field)) {
|
|
282
|
+
for (let i = 0; i < field.length; i++) {
|
|
283
|
+
const value = (field as any)[i]
|
|
284
|
+
formData.append(
|
|
285
|
+
key as any,
|
|
286
|
+
value instanceof File ? await createNewFile(value) : value,
|
|
287
|
+
)
|
|
288
|
+
}
|
|
289
|
+
continue
|
|
290
|
+
}
|
|
291
|
+
formData.append(key, field as string)
|
|
292
|
+
}
|
|
293
|
+
fetchInit.body = formData
|
|
294
|
+
} else if (typeof body === 'object' && body !== null) {
|
|
295
|
+
;(fetchInit.headers as Record<string, string>)['content-type'] =
|
|
296
|
+
'application/json'
|
|
297
|
+
fetchInit.body = JSON.stringify(body)
|
|
298
|
+
} else if (body !== undefined && body !== null) {
|
|
299
|
+
;(fetchInit.headers as Record<string, string>)['content-type'] =
|
|
300
|
+
'text/plain'
|
|
301
|
+
fetchInit.body = body
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export async function parseResponseData({
|
|
306
|
+
response,
|
|
307
|
+
executeRequest,
|
|
308
|
+
retries,
|
|
309
|
+
}: {
|
|
310
|
+
response: Response
|
|
311
|
+
executeRequest: () => Promise<Response>
|
|
312
|
+
retries: number
|
|
313
|
+
}): Promise<{ data: any; error: any }> {
|
|
314
|
+
let data = null as any
|
|
315
|
+
let error = null as any
|
|
316
|
+
|
|
317
|
+
switch (response.headers.get('Content-Type')?.split(';')[0]) {
|
|
318
|
+
case 'text/event-stream':
|
|
319
|
+
data = streamSSEResponse({
|
|
320
|
+
response,
|
|
321
|
+
map: (x) => tryParsingSSEJson(x.data),
|
|
322
|
+
executeRequest,
|
|
323
|
+
maxRetries: retries,
|
|
324
|
+
})
|
|
325
|
+
break
|
|
326
|
+
|
|
327
|
+
case 'application/json':
|
|
328
|
+
data = await response.json()
|
|
329
|
+
data = superjsonDeserialize(data)
|
|
330
|
+
break
|
|
331
|
+
|
|
332
|
+
case 'application/octet-stream':
|
|
333
|
+
data = await response.arrayBuffer()
|
|
334
|
+
break
|
|
335
|
+
|
|
336
|
+
case 'multipart/form-data': {
|
|
337
|
+
const temp = await response.formData()
|
|
338
|
+
data = {}
|
|
339
|
+
temp.forEach((value, key) => {
|
|
340
|
+
data[key] = value
|
|
341
|
+
})
|
|
342
|
+
break
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
default:
|
|
346
|
+
data = await response.text().then(parseStringifiedValue)
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
if (response.status >= 300 || response.status < 200) {
|
|
350
|
+
error = new SpiceflowFetchError(response.status, data || 'Unknown error')
|
|
351
|
+
data = null
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
return { data, error }
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
export async function executeWithRetries({
|
|
358
|
+
url,
|
|
359
|
+
fetchInit,
|
|
360
|
+
fetcher,
|
|
361
|
+
instance,
|
|
362
|
+
state,
|
|
363
|
+
retries,
|
|
364
|
+
}: {
|
|
365
|
+
url: string
|
|
366
|
+
fetchInit: RequestInit
|
|
367
|
+
fetcher: typeof fetch
|
|
368
|
+
instance?: AnySpiceflow
|
|
369
|
+
state?: any
|
|
370
|
+
retries: number
|
|
371
|
+
}): Promise<Response> {
|
|
372
|
+
let attempt = 0
|
|
373
|
+
let response!: Response
|
|
374
|
+
let lastError: Error | null = null
|
|
375
|
+
|
|
376
|
+
while (attempt <= retries) {
|
|
377
|
+
try {
|
|
378
|
+
response = await (instance?.handle(new Request(url, fetchInit), {
|
|
379
|
+
state,
|
|
380
|
+
}) ?? fetcher(url, fetchInit))
|
|
381
|
+
|
|
382
|
+
if (response.status < 500 || attempt === retries) {
|
|
383
|
+
break
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
lastError = new Error(
|
|
387
|
+
`Server error: ${response.status} ${response.statusText}`,
|
|
388
|
+
)
|
|
389
|
+
} catch (err) {
|
|
390
|
+
lastError = err as Error
|
|
391
|
+
if (attempt === retries) {
|
|
392
|
+
throw err
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
attempt++
|
|
397
|
+
const backoffMs = Math.min(1000 * 2 ** (attempt - 1), 10000)
|
|
398
|
+
await new Promise((resolve) => setTimeout(resolve, backoffMs))
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
if (!response) {
|
|
402
|
+
throw lastError || new Error('Failed to fetch after retries')
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return response
|
|
406
|
+
}
|