spiceflow 1.17.12 → 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.
Files changed (47) hide show
  1. package/README.md +167 -2
  2. package/dist/client/errors.d.ts +2 -1
  3. package/dist/client/errors.d.ts.map +1 -1
  4. package/dist/client/errors.js +3 -1
  5. package/dist/client/errors.js.map +1 -1
  6. package/dist/client/fetch.d.ts +86 -0
  7. package/dist/client/fetch.d.ts.map +1 -0
  8. package/dist/client/fetch.js +143 -0
  9. package/dist/client/fetch.js.map +1 -0
  10. package/dist/client/index.d.ts +4 -14
  11. package/dist/client/index.d.ts.map +1 -1
  12. package/dist/client/index.js +3 -176
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/client/shared.d.ts +47 -0
  15. package/dist/client/shared.d.ts.map +1 -0
  16. package/dist/client/shared.js +314 -0
  17. package/dist/client/shared.js.map +1 -0
  18. package/dist/client/types.d.ts +2 -1
  19. package/dist/client/types.d.ts.map +1 -1
  20. package/dist/fetch-client.test.d.ts +2 -0
  21. package/dist/fetch-client.test.d.ts.map +1 -0
  22. package/dist/fetch-client.test.js +362 -0
  23. package/dist/fetch-client.test.js.map +1 -0
  24. package/dist/index.d.ts +1 -1
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +1 -1
  27. package/dist/index.js.map +1 -1
  28. package/dist/mcp.d.ts +1 -1
  29. package/dist/mcp.d.ts.map +1 -1
  30. package/dist/openapi.d.ts +1 -1
  31. package/dist/openapi.d.ts.map +1 -1
  32. package/dist/spiceflow.d.ts +36 -14
  33. package/dist/spiceflow.d.ts.map +1 -1
  34. package/dist/spiceflow.js +49 -16
  35. package/dist/spiceflow.js.map +1 -1
  36. package/dist/spiceflow.test.js +205 -1
  37. package/dist/spiceflow.test.js.map +1 -1
  38. package/package.json +3 -3
  39. package/src/client/errors.ts +3 -0
  40. package/src/client/fetch.ts +447 -0
  41. package/src/client/index.ts +19 -229
  42. package/src/client/shared.ts +406 -0
  43. package/src/client/types.ts +2 -1
  44. package/src/fetch-client.test.ts +411 -0
  45. package/src/index.ts +1 -1
  46. package/src/spiceflow.test.ts +315 -1
  47. package/src/spiceflow.ts +106 -32
@@ -0,0 +1,447 @@
1
+ // Type-safe fetch-like client for Spiceflow. Uses a familiar fetch(path, options)
2
+ // interface instead of the proxy-based chainable API.
3
+ import type { AnySpiceflow, Spiceflow } from '../spiceflow.ts'
4
+ import type { ExtractParamsFromPath } from '../types.ts'
5
+
6
+ import type { SpiceflowClient } from './types.ts'
7
+ import type { ReplaceGeneratorWithAsyncGenerator } from './types.ts'
8
+ import { SpiceflowFetchError } from './errors.ts'
9
+
10
+ import {
11
+ processHeaders,
12
+ buildQueryString,
13
+ serializeBody,
14
+ parseResponseData,
15
+ executeWithRetries,
16
+ } from './shared.ts'
17
+
18
+ // ─── Type utilities ──────────────────────────────────────────────────────────
19
+
20
+ type HttpMethodLower =
21
+ | 'get'
22
+ | 'post'
23
+ | 'put'
24
+ | 'delete'
25
+ | 'patch'
26
+ | 'options'
27
+ | 'head'
28
+ | 'connect'
29
+ | 'subscribe'
30
+
31
+ // Navigate the nested ClientRoutes tree given a path string.
32
+ // Reverses what CreateClient does: `/users/:id` → Routes['users'][':id']
33
+ type NavigateRoutes<Routes, Path extends string> =
34
+ Path extends `/${infer Rest}`
35
+ ? Rest extends ''
36
+ ? 'index' extends keyof Routes
37
+ ? Routes['index']
38
+ : never
39
+ : _NavigateRoutes<Routes, Rest>
40
+ : _NavigateRoutes<Routes, Path>
41
+
42
+ type _NavigateRoutes<Routes, Path extends string> =
43
+ Path extends `${infer Segment}/${infer Rest}`
44
+ ? Segment extends keyof Routes
45
+ ? _NavigateRoutes<Routes[Segment], Rest>
46
+ : never
47
+ : Path extends keyof Routes
48
+ ? Routes[Path]
49
+ : never
50
+
51
+ type RouteAtPath<
52
+ Routes extends Record<string, any>,
53
+ Path extends string,
54
+ > = NavigateRoutes<Routes, Path>
55
+
56
+ type MethodsAtPath<
57
+ Routes extends Record<string, any>,
58
+ Path extends string,
59
+ > = Extract<keyof RouteAtPath<Routes, Path>, HttpMethodLower>
60
+
61
+ type AllowedMethod<
62
+ Routes extends Record<string, any>,
63
+ Path extends string,
64
+ > = Uppercase<MethodsAtPath<Routes, Path>> | MethodsAtPath<Routes, Path>
65
+
66
+ type RouteInfoForMethod<
67
+ Routes extends Record<string, any>,
68
+ Path extends string,
69
+ Method extends string,
70
+ > = Lowercase<Method> extends keyof RouteAtPath<Routes, Path>
71
+ ? RouteAtPath<Routes, Path>[Lowercase<Method>]
72
+ : never
73
+
74
+ // ─── Options type ────────────────────────────────────────────────────────────
75
+
76
+ // Params option: required if path has :params, omitted otherwise
77
+ type ParamsOption<Path extends string> =
78
+ ExtractParamsFromPath<Path> extends undefined
79
+ ? { params?: Record<string, string> }
80
+ : { params: ExtractParamsFromPath<Path> }
81
+
82
+ // Query option: typed from route schema if available
83
+ type QueryOption<
84
+ Routes extends Record<string, any>,
85
+ Path extends string,
86
+ Method extends string,
87
+ > = RouteInfoForMethod<Routes, Path, Method> extends {
88
+ query: infer Q
89
+ }
90
+ ? undefined extends Q
91
+ ? { query?: Record<string, unknown> }
92
+ : { query: Q }
93
+ : { query?: Record<string, unknown> }
94
+
95
+ // Body option: typed from route schema, only for non-GET/HEAD/SUBSCRIBE methods
96
+ type BodyOption<
97
+ Routes extends Record<string, any>,
98
+ Path extends string,
99
+ Method extends string,
100
+ > = Lowercase<Method> extends 'get' | 'head' | 'subscribe'
101
+ ? {}
102
+ : RouteInfoForMethod<Routes, Path, Method> extends {
103
+ request: infer Body
104
+ }
105
+ ? undefined extends Body
106
+ ? { body?: unknown }
107
+ : { body: Body }
108
+ : { body?: unknown }
109
+
110
+ // Check if options has any required fields
111
+ type HasRequiredFields<
112
+ Routes extends Record<string, any>,
113
+ Path extends string,
114
+ Method extends string,
115
+ > =
116
+ // params required?
117
+ ExtractParamsFromPath<Path> extends undefined
118
+ ? // query required?
119
+ RouteInfoForMethod<Routes, Path, Method> extends { query: infer Q }
120
+ ? undefined extends Q
121
+ ? // body required?
122
+ Lowercase<Method> extends 'get' | 'head' | 'subscribe'
123
+ ? false
124
+ : RouteInfoForMethod<Routes, Path, Method> extends {
125
+ request: infer Body
126
+ }
127
+ ? undefined extends Body
128
+ ? false
129
+ : true
130
+ : false
131
+ : true
132
+ : // body required?
133
+ Lowercase<Method> extends 'get' | 'head' | 'subscribe'
134
+ ? false
135
+ : RouteInfoForMethod<Routes, Path, Method> extends {
136
+ request: infer Body
137
+ }
138
+ ? undefined extends Body
139
+ ? false
140
+ : true
141
+ : false
142
+ : true
143
+
144
+ type FetchOptionsTyped<
145
+ Routes extends Record<string, any>,
146
+ Path extends string,
147
+ Method extends string,
148
+ > = {
149
+ method?: Method
150
+ headers?: RequestInit['headers']
151
+ signal?: AbortSignal
152
+ } & ParamsOption<Path> &
153
+ QueryOption<Routes, Path, Method> &
154
+ BodyOption<Routes, Path, Method>
155
+
156
+ type FetchOptionsFallback = {
157
+ method?: string
158
+ body?: BodyInit | Record<string, unknown> | null
159
+ query?: Record<string, unknown>
160
+ params?: Record<string, string>
161
+ headers?: RequestInit['headers']
162
+ signal?: AbortSignal
163
+ [key: string]: unknown
164
+ }
165
+
166
+ type FetchOptions<
167
+ Routes extends Record<string, any>,
168
+ Path extends string,
169
+ Method extends string,
170
+ > = [RouteAtPath<Routes, Path>] extends [never]
171
+ ? FetchOptionsFallback
172
+ : FetchOptionsTyped<Routes, Path, Method>
173
+
174
+ // ─── Response type ───────────────────────────────────────────────────────────
175
+
176
+ type FetchResultData<
177
+ Routes extends Record<string, any>,
178
+ Path extends string,
179
+ Method extends string,
180
+ > = [RouteAtPath<Routes, Path>] extends [never]
181
+ ? any
182
+ : RouteInfoForMethod<Routes, Path, Method> extends {
183
+ response: infer Res extends Record<number, unknown>
184
+ }
185
+ ? ReplaceGeneratorWithAsyncGenerator<Res>[200]
186
+ : any
187
+
188
+ type FetchResultError<
189
+ Routes extends Record<string, any>,
190
+ Path extends string,
191
+ Method extends string,
192
+ > = [RouteAtPath<Routes, Path>] extends [never]
193
+ ? SpiceflowFetchError<number, any>
194
+ : RouteInfoForMethod<Routes, Path, Method> extends {
195
+ response: infer Res extends Record<number, unknown>
196
+ }
197
+ ? Exclude<keyof Res, 200> extends never
198
+ ? SpiceflowFetchError<number, any>
199
+ : {
200
+ [Status in keyof Res]: SpiceflowFetchError<Status, Res[Status]>
201
+ }[Exclude<keyof Res, 200>]
202
+ : SpiceflowFetchError<number, any>
203
+
204
+ type FetchResult<
205
+ Routes extends Record<string, any>,
206
+ Path extends string,
207
+ Method extends string,
208
+ > = FetchResultError<Routes, Path, Method> | FetchResultData<Routes, Path, Method>
209
+
210
+ // ─── Public type ─────────────────────────────────────────────────────────────
211
+
212
+ // Resolves options for a given App/Path/Method combination.
213
+ // Returns the appropriate options type, or FetchOptionsFallback for unknown paths.
214
+ type ResolveOptions<
215
+ App extends AnySpiceflow,
216
+ Path extends string,
217
+ Method extends string,
218
+ > = App extends {
219
+ _types: { ClientRoutes: infer Routes extends Record<string, any> }
220
+ }
221
+ ? FetchOptions<Routes, Path, Method>
222
+ : FetchOptionsFallback
223
+
224
+ // Resolves the result type for a given App/Path/Method combination.
225
+ type ResolveResult<
226
+ App extends AnySpiceflow,
227
+ Path extends string,
228
+ Method extends string,
229
+ > = App extends {
230
+ _types: { ClientRoutes: infer Routes extends Record<string, any> }
231
+ }
232
+ ? FetchResult<Routes, Path, Method>
233
+ : SpiceflowFetchError<number, any> | any
234
+
235
+ // Check if options are required for a given App/Path/Method
236
+ type IsOptionsRequired<
237
+ App extends AnySpiceflow,
238
+ Path extends string,
239
+ Method extends string,
240
+ > = App extends {
241
+ _types: { ClientRoutes: infer Routes extends Record<string, any> }
242
+ }
243
+ ? [RouteAtPath<Routes, Path>] extends [never]
244
+ ? false
245
+ : HasRequiredFields<Routes, Path, Method>
246
+ : false
247
+
248
+ export interface SpiceflowFetch<App extends AnySpiceflow> {
249
+ // Overload: options required when route demands params/query/body
250
+ <const Path extends string, const Method extends string = 'GET'>(
251
+ ...args: IsOptionsRequired<App, Path, Method> extends true
252
+ ? [path: Path, options: ResolveOptions<App, Path, Method>]
253
+ : [path: Path, options?: ResolveOptions<App, Path, Method>]
254
+ ): Promise<ResolveResult<App, Path, Method>>
255
+ }
256
+
257
+ // ─── Factory ─────────────────────────────────────────────────────────────────
258
+
259
+ export function createSpiceflowFetch<const App extends AnySpiceflow>(
260
+ domain: App | string,
261
+ config: SpiceflowClient.Config &
262
+ (App extends Spiceflow<any, any, infer Singleton, any, any, any, any>
263
+ ? { state?: Singleton['state'] }
264
+ : {}) = {} as any,
265
+ ): SpiceflowFetch<App> {
266
+ let baseUrl: string
267
+ let instance: AnySpiceflow | undefined
268
+
269
+ if (typeof domain === 'string') {
270
+ baseUrl = domain.endsWith('/') ? domain.slice(0, -1) : domain
271
+ } else {
272
+ baseUrl = 'http://e.ly'
273
+ instance = domain
274
+
275
+ if (typeof window !== 'undefined') {
276
+ console.warn(
277
+ 'Spiceflow instance server found on client side, this is not recommended for security reason. Use generic type instead.',
278
+ )
279
+ }
280
+ }
281
+
282
+ if ((config as any).state && !instance) {
283
+ throw new Error('State is only available when using a Spiceflow instance')
284
+ }
285
+
286
+ const spiceflowFetch = async (
287
+ path: string,
288
+ options: any = {},
289
+ ): Promise<any> => {
290
+ let {
291
+ fetch: fetcher = fetch,
292
+ headers: configHeaders,
293
+ onRequest,
294
+ onResponse,
295
+ retries = 0,
296
+ } = config as SpiceflowClient.Config
297
+
298
+ const {
299
+ method: rawMethod = 'GET',
300
+ body,
301
+ query,
302
+ params,
303
+ headers: optionHeaders,
304
+ signal,
305
+ ...restInit
306
+ } = options
307
+
308
+ const methodUpper = rawMethod.toUpperCase()
309
+ const isGetOrHead =
310
+ methodUpper === 'GET' ||
311
+ methodUpper === 'HEAD' ||
312
+ methodUpper === 'SUBSCRIBE'
313
+
314
+ // Resolve path params (replace :param with values)
315
+ // Sort by key length descending to avoid :id replacing inside :id2
316
+ let resolvedPath = path
317
+ if (params && typeof params === 'object') {
318
+ const entries = Object.entries(params).sort(
319
+ ([a], [b]) => b.length - a.length,
320
+ )
321
+ for (const [key, value] of entries) {
322
+ if (key === '*') {
323
+ resolvedPath = resolvedPath.split('*').join(String(value))
324
+ } else {
325
+ resolvedPath = resolvedPath.split(`:${key}`).join(String(value))
326
+ }
327
+ }
328
+ }
329
+
330
+ const queryString = buildQueryString(query)
331
+
332
+ // Support absolute URLs — skip baseUrl concatenation
333
+ const isAbsoluteUrl = /^https?:\/\//i.test(resolvedPath)
334
+ const url = isAbsoluteUrl
335
+ ? resolvedPath + queryString
336
+ : baseUrl + resolvedPath + queryString
337
+
338
+ let headers = processHeaders(configHeaders, resolvedPath, {
339
+ method: methodUpper,
340
+ signal,
341
+ })
342
+ headers = {
343
+ ...headers,
344
+ ...processHeaders(optionHeaders, resolvedPath, {
345
+ method: methodUpper,
346
+ signal,
347
+ }),
348
+ }
349
+
350
+ let fetchInit: RequestInit = {
351
+ method: methodUpper,
352
+ headers,
353
+ signal,
354
+ ...restInit,
355
+ }
356
+
357
+ // Apply onRequest hooks (first pass, before body serialization)
358
+ if (onRequest) {
359
+ const hooks = Array.isArray(onRequest) ? onRequest : [onRequest]
360
+ for (const hook of hooks) {
361
+ const temp = await hook(resolvedPath, fetchInit)
362
+ if (typeof temp === 'object') {
363
+ fetchInit = {
364
+ ...fetchInit,
365
+ ...temp,
366
+ headers: {
367
+ ...fetchInit.headers,
368
+ ...processHeaders(temp.headers, resolvedPath, fetchInit),
369
+ },
370
+ }
371
+ }
372
+ }
373
+ }
374
+
375
+ // Ensure GET/HEAD has no body before serialization
376
+ if (isGetOrHead) delete fetchInit.body
377
+
378
+ // Serialize body
379
+ if (!isGetOrHead && body !== undefined) {
380
+ fetchInit.body = body
381
+ await serializeBody({ body, fetchInit, isGetOrHead })
382
+ }
383
+
384
+ if (isGetOrHead) {
385
+ delete fetchInit.body
386
+ }
387
+
388
+ // Add x-spiceflow-agent header
389
+ ;(fetchInit.headers as Record<string, string>)['x-spiceflow-agent'] =
390
+ 'spiceflow-client'
391
+
392
+ // Apply onRequest hooks (second pass, after body serialization — matches proxy client behavior)
393
+ if (onRequest) {
394
+ const hooks = Array.isArray(onRequest) ? onRequest : [onRequest]
395
+ for (const hook of hooks) {
396
+ const temp = await hook(resolvedPath, fetchInit)
397
+ if (typeof temp === 'object') {
398
+ fetchInit = {
399
+ ...fetchInit,
400
+ ...temp,
401
+ headers: {
402
+ ...fetchInit.headers,
403
+ ...processHeaders(temp.headers, resolvedPath, fetchInit),
404
+ } as Record<string, string>,
405
+ }
406
+ }
407
+ }
408
+ }
409
+
410
+ // Execute request with retries
411
+ const executeRequest = () =>
412
+ executeWithRetries({
413
+ url,
414
+ fetchInit,
415
+ fetcher: fetcher || fetch,
416
+ instance,
417
+ state: (config as any).state,
418
+ retries,
419
+ })
420
+
421
+ const response = await executeRequest()
422
+
423
+ // Process onResponse hooks
424
+ if (onResponse) {
425
+ const hooks = Array.isArray(onResponse) ? onResponse : [onResponse]
426
+ for (const hook of hooks) {
427
+ await hook(response.clone())
428
+ }
429
+ }
430
+
431
+ // Parse response
432
+ const { data, error } = await parseResponseData({
433
+ response,
434
+ executeRequest,
435
+ retries,
436
+ })
437
+
438
+ if (error) {
439
+ error.response = response
440
+ return error
441
+ }
442
+
443
+ return data
444
+ }
445
+
446
+ return spiceflowFetch as any
447
+ }
@@ -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,222 +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,
158
- map,
159
- executeRequest,
160
- maxRetries = 0,
161
- }: {
162
- response: Response
163
- map: (x: SSEEvent) => any
164
- executeRequest?: () => Promise<Response>
165
- maxRetries?: number
166
- }): AsyncGenerator<SSEEvent> {
167
- let currentResponse = response
168
- let retriesLeft = maxRetries
169
-
170
- while (true) {
171
- const body = currentResponse.body
172
- if (!body) return
173
-
174
- const eventStream = body
175
- .pipeThrough(new TextDecoderStream())
176
- .pipeThrough(new EventSourceParserStream())
177
-
178
- let reader = eventStream.getReader()
179
-
180
- try {
181
- while (true) {
182
- const { done, value: event } = await reader.read()
183
- if (done) return
184
-
185
- if (event?.event === 'error') {
186
- const error = superjsonDeserialize(event.data)
187
- throw new SpiceflowFetchError(500, error)
188
- }
189
-
190
- if (event) {
191
- yield map({ ...event, data: event.data })
192
- }
193
- }
194
- } catch (error) {
195
- if (isAbortError(error)) {
196
- return
197
- }
198
-
199
- if (
200
- executeRequest &&
201
- error instanceof SpiceflowFetchError &&
202
- error.status >= 500 &&
203
- retriesLeft > 0
204
- ) {
205
- retriesLeft--
206
- const backoffMs = Math.min(
207
- 1000 * 2 ** (maxRetries - retriesLeft - 1),
208
- 10000,
209
- )
210
- await new Promise((resolve) => setTimeout(resolve, backoffMs))
211
-
212
- try {
213
- currentResponse = await executeRequest()
214
- if (currentResponse.status >= 500) {
215
- if (retriesLeft === 0) {
216
- throw error
217
- }
218
- continue
219
- }
220
- } catch (retryError) {
221
- if (retriesLeft === 0) {
222
- throw retryError
223
- }
224
- continue
225
- }
226
- } else {
227
- throw error
228
- }
229
- }
230
- }
231
- }
232
-
233
- function tryParsingSSEJson(data: string): any {
234
- try {
235
- return superjsonDeserialize(JSON.parse(data))
236
- } catch (error) {
237
- return data
238
- }
239
- }
240
-
241
42
  const createProxy = (
242
43
  domain: string,
243
44
  config: SpiceflowClient.Config & { state?: any },
@@ -603,14 +404,3 @@ export const createSpiceflowClient = <const App extends AnySpiceflow>(
603
404
 
604
405
  return createProxy('http://e.ly', config || {}, [], domain)
605
406
  }
606
-
607
- function superjsonDeserialize(data: any) {
608
- if (data?.__superjsonMeta) {
609
- const { __superjsonMeta, ...rest } = data
610
- return superjson.deserialize({
611
- json: rest,
612
- meta: __superjsonMeta,
613
- })
614
- }
615
- return data
616
- }