Skip to content

Commit 90f4880

Browse files
committed
improve fetchSSE (#532)
1 parent 0f3ec38 commit 90f4880

File tree

6 files changed

+151
-48
lines changed

6 files changed

+151
-48
lines changed

package-lock.json

Lines changed: 0 additions & 9 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,6 @@
2525
"claude-ai": "^1.2.2",
2626
"countries-list": "^2.6.1",
2727
"diff": "^5.1.0",
28-
"eventsource-parser": "^1.0.0",
2928
"file-saver": "^2.0.5",
3029
"github-markdown-css": "^5.2.0",
3130
"gpt-3-encoder": "^1.1.4",

src/utils/eventsource-parser.mjs

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
// https://www.npmjs.com/package/eventsource-parser/v/1.1.1
2+
3+
function createParser(onParse) {
4+
let isFirstChunk
5+
let bytes
6+
let buffer
7+
let startingPosition
8+
let startingFieldLength
9+
let eventId
10+
let eventName
11+
let data
12+
reset()
13+
return {
14+
feed,
15+
reset,
16+
}
17+
function reset() {
18+
isFirstChunk = true
19+
bytes = []
20+
buffer = ''
21+
startingPosition = 0
22+
startingFieldLength = -1
23+
eventId = void 0
24+
eventName = void 0
25+
data = ''
26+
}
27+
28+
function feed(chunk) {
29+
bytes = bytes.concat(Array.from(chunk))
30+
buffer = new TextDecoder().decode(new Uint8Array(bytes))
31+
if (isFirstChunk && hasBom(buffer)) {
32+
buffer = buffer.slice(BOM.length)
33+
}
34+
isFirstChunk = false
35+
const length = buffer.length
36+
let position = 0
37+
let discardTrailingNewline = false
38+
while (position < length) {
39+
if (discardTrailingNewline) {
40+
if (buffer[position] === '\n') {
41+
++position
42+
}
43+
discardTrailingNewline = false
44+
}
45+
let lineLength = -1
46+
let fieldLength = startingFieldLength
47+
let character
48+
for (let index = startingPosition; lineLength < 0 && index < length; ++index) {
49+
character = buffer[index]
50+
if (character === ':' && fieldLength < 0) {
51+
fieldLength = index - position
52+
} else if (character === '\r') {
53+
discardTrailingNewline = true
54+
lineLength = index - position
55+
} else if (character === '\n') {
56+
lineLength = index - position
57+
}
58+
}
59+
if (lineLength < 0) {
60+
startingPosition = length - position
61+
startingFieldLength = fieldLength
62+
break
63+
} else {
64+
startingPosition = 0
65+
startingFieldLength = -1
66+
}
67+
parseEventStreamLine(buffer, position, fieldLength, lineLength)
68+
position += lineLength + 1
69+
}
70+
if (position === length) {
71+
bytes = []
72+
buffer = ''
73+
} else if (position > 0) {
74+
bytes = bytes.slice(new TextEncoder().encode(buffer.slice(0, position)).length)
75+
buffer = buffer.slice(position)
76+
}
77+
}
78+
79+
function parseEventStreamLine(lineBuffer, index, fieldLength, lineLength) {
80+
if (lineLength === 0) {
81+
if (data.length > 0) {
82+
onParse({
83+
type: 'event',
84+
id: eventId,
85+
event: eventName || void 0,
86+
data: data.slice(0, -1),
87+
// remove trailing newline
88+
})
89+
90+
data = ''
91+
eventId = void 0
92+
}
93+
eventName = void 0
94+
return
95+
}
96+
const noValue = fieldLength < 0
97+
const field = lineBuffer.slice(index, index + (noValue ? lineLength : fieldLength))
98+
let step = 0
99+
if (noValue) {
100+
step = lineLength
101+
} else if (lineBuffer[index + fieldLength + 1] === ' ') {
102+
step = fieldLength + 2
103+
} else {
104+
step = fieldLength + 1
105+
}
106+
const position = index + step
107+
const valueLength = lineLength - step
108+
const value = lineBuffer.slice(position, position + valueLength).toString()
109+
if (field === 'data') {
110+
data += value ? ''.concat(value, '\n') : '\n'
111+
} else if (field === 'event') {
112+
eventName = value
113+
} else if (field === 'id' && !value.includes('\0')) {
114+
eventId = value
115+
} else if (field === 'retry') {
116+
const retry = parseInt(value, 10)
117+
if (!Number.isNaN(retry)) {
118+
onParse({
119+
type: 'reconnect-interval',
120+
value: retry,
121+
})
122+
}
123+
}
124+
}
125+
}
126+
const BOM = [239, 187, 191]
127+
function hasBom(buffer) {
128+
return BOM.every((charCode, index) => buffer.charCodeAt(index) === charCode)
129+
}
130+
export { createParser }

src/utils/fetch-sse.mjs

Lines changed: 20 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
import { createParser } from 'eventsource-parser'
2-
import { streamAsyncIterable } from './stream-async-iterable'
1+
import { createParser } from './eventsource-parser.mjs'
32

43
export async function fetchSSE(resource, options) {
54
const { onMessage, onStart, onEnd, onError, ...fetchOptions } = options
@@ -13,34 +12,32 @@ export async function fetchSSE(resource, options) {
1312
}
1413
const parser = createParser((event) => {
1514
if (event.type === 'event') {
16-
if (event.data === '[DONE]') {
17-
onMessage(event.data)
18-
} else {
19-
try {
20-
JSON.parse(event.data)
21-
onMessage(event.data)
22-
} catch (error) {
23-
console.error('json error', error)
24-
onMessage(
25-
event.data
26-
.replace(/^"|"$/g, '')
27-
.replaceAll('\\"', '"')
28-
.replaceAll('\\\\u', '\\u')
29-
.replaceAll('\\\\n', '\\n'),
30-
)
31-
}
32-
}
15+
onMessage(event.data)
3316
}
3417
})
3518
let hasStarted = false
36-
for await (const chunk of streamAsyncIterable(resp.body)) {
37-
const str = new TextDecoder().decode(chunk)
38-
parser.feed(str)
39-
19+
const reader = resp.body.getReader()
20+
let result
21+
while (!(result = await reader.read()).done) {
22+
const chunk = result.value
4023
if (!hasStarted) {
24+
const str = new TextDecoder().decode(chunk)
4125
hasStarted = true
4226
await onStart(str)
27+
28+
let fakeSseData
29+
try {
30+
const commonResponse = JSON.parse(str)
31+
fakeSseData = 'data: ' + JSON.stringify(commonResponse) + '\n\ndata: [DONE]\n\n'
32+
} catch (error) {
33+
console.debug('not common response', error)
34+
}
35+
if (fakeSseData) {
36+
parser.feed(new TextEncoder().encode(fakeSseData))
37+
break
38+
}
4339
}
40+
parser.feed(chunk)
4441
}
4542
await onEnd()
4643
}

src/utils/index.mjs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,5 +16,5 @@ export * from './open-url'
1616
export * from './parse-float-with-clamp'
1717
export * from './parse-int-with-clamp'
1818
export * from './set-element-position-in-viewport'
19-
export * from './stream-async-iterable'
19+
export * from './eventsource-parser.mjs'
2020
export * from './update-ref-height'

src/utils/stream-async-iterable.mjs

Lines changed: 0 additions & 14 deletions
This file was deleted.

0 commit comments

Comments
 (0)