generated from bywhitebird/starter-node
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathget-client.ts
138 lines (116 loc) · 4 KB
/
get-client.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
import type { AnyRoute, DefaultClient, RequestOptions } from '@agrume/types'
/**
* Gets the default client.
* @param {RequestOptions} requestOptions The request options.
* @returns {DefaultClient} The default client.
*/
export function getClient<R extends AnyRoute>(
requestOptions: RequestOptions,
): DefaultClient<R> {
function isAsyncGenerator<T, R, N>(
// eslint-disable-next-line ts/no-explicit-any
value: any,
): value is AsyncGenerator<T, R, N> {
if (!value) {
return false
}
if (Symbol.asyncIterator !== undefined) {
const asyncGenerator
= (async function* () { yield undefined }()).constructor
return value.constructor.constructor === asyncGenerator.constructor
}
else {
// This is a fallback. For example, the polyfill used in Expo does not
// have a "normal" behavior.
// `asyncGenerator` will be `AsyncGenerator` instead of `AsyncGeneratorFunction`.
// Then, the constructor of the constructor will be `Function` instead of `AsyncGenerator`.
// So, in environments where `Symbol.asyncIterator` is not defined (where
// async generators are not natively supported), we fallback to this less
// reliable method.
return value.constructor.name.startsWith('AsyncGenerator')
}
}
return async function (parameters: Parameters<R>[0]) {
const agrumeRid = crypto.randomUUID()
const response = await fetch(requestOptions.url, {
...requestOptions,
...(!isAsyncGenerator(parameters) && {
body: JSON.stringify(parameters),
}),
headers: {
...requestOptions.headers,
...(isAsyncGenerator(parameters) && {
'X-Agrume-Rid-Stream': agrumeRid,
}),
},
})
if (isAsyncGenerator(parameters)) {
fetch(`${requestOptions.url}/__agrume_send_stream`, {
body: new ReadableStream({
async start(controller) {
while (true) {
const { done, value } = await parameters.next()
const YIELD_PREFIX = 'YIELD'
const RETURN_PREFIX = 'RETURN'
if (done) {
if (value !== undefined) {
controller.enqueue(`${RETURN_PREFIX}${JSON.stringify(value)}`)
}
controller.close()
return
}
controller.enqueue(`${YIELD_PREFIX}${JSON.stringify(value)}`)
}
},
}),
// @ts-expect-error `duplex` is correct
duplex: 'half',
headers: {
...requestOptions.headers,
'Content-Type': 'application/octet-stream',
'X-Agrume-Rid-Stream': agrumeRid,
},
method: 'POST',
})
}
if (response.headers.get('content-type')?.includes('application/json')) {
return response.json()
}
if (response.headers.get('content-type')?.includes('text/event-stream')) {
const getAsyncGenerator = async function* () {
const reader = response
.body
?.pipeThrough(new TextDecoderStream())
.getReader()
if (reader === undefined) {
return
}
while (true) {
const { done, value: unformattedValue } = await reader.read()
if (done) {
return
}
const unformattedValues = unformattedValue.split('\n\n')
for (const unformattedValue of unformattedValues) {
if (unformattedValue === '') {
continue
}
const DATA_PREFIX = 'data: '
const data = unformattedValue.startsWith(DATA_PREFIX)
? unformattedValue.slice(DATA_PREFIX.length)
: unformattedValue
if (data === 'DONE') {
return
}
const RETURN_PREFIX = 'RETURN'
if (data.startsWith(RETURN_PREFIX)) {
return JSON.parse(data.slice(RETURN_PREFIX.length))
}
yield JSON.parse(data)
}
}
}
return getAsyncGenerator()
}
} as never
}