Skip to content

Commit

Permalink
feat: support environments without TextDecoderStream support
Browse files Browse the repository at this point in the history
  • Loading branch information
rexxars committed Apr 27, 2024
1 parent 0a17ddd commit e97538f
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 7 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ npm install --save eventsource-client
- Firefox >= 105
- Edge >= 79

Basically, any environment that supports the [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) (with [pipeThrough](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream/pipeThrough) support) and the [TextDecoderStream](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoderStream) APIs.
Basically, any environment that supports the [ReadableStream](https://developer.mozilla.org/en-US/docs/Web/API/ReadableStream) and the [TextDecoder](https://developer.mozilla.org/en-US/docs/Web/API/TextDecoder) APIs.

## Usage (async iterator)

Expand Down Expand Up @@ -96,7 +96,7 @@ es.close()
- [ ] Configurable stalled connection detection (eg no data)
- [ ] Configurable reconnection policy
- [ ] Deno support/tests
- [ ] Bun support/tests (blocked: no `TextDecoderStream` support)
- [ ] Bun support/tests
- [ ] Consider legacy build

## License
Expand Down
10 changes: 6 additions & 4 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -188,19 +188,21 @@ export function createEventSource(
// Ensure that the response stream is a web stream
// @todo Figure out a way to make this work without casting
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const bodyStream = getStream(body as any)
const stream = getStream(body as any)
const decoder = new TextDecoder()

// EventSources are always UTF-8 per spec
const stream = bodyStream.pipeThrough<string>(new TextDecoderStream())
const reader = stream.getReader()
let open = true

readyState = OPEN

do {
const {done, value} = await reader.read()
if (value) {
parser.feed(decoder.decode(value, {stream: !done}))
}

if (!done) {
parser.feed(value)
continue
}

Expand Down
4 changes: 4 additions & 0 deletions test/fixtures.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export const unicodeLines = [
'🦄 are cool. 🐾 in the snow. Allyson Felix, 🏃🏽‍♀️ 🥇 2012 London!',
'Espen ♥ Kokos',
]
52 changes: 52 additions & 0 deletions test/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import {resolve as resolvePath} from 'node:path'

import esbuild from 'esbuild'

import {unicodeLines} from './fixtures'

export function getServer(port: number): Promise<Server> {
return new Promise((resolve, reject) => {
const server = createServer(onRequest)
Expand All @@ -14,6 +16,11 @@ export function getServer(port: number): Promise<Server> {
}

function onRequest(req: IncomingMessage, res: ServerResponse) {
// Disable Nagle's algorithm for testing
if (res.socket) {
res.socket.setNoDelay(true)
}

switch (req.url) {
// Server-Sent Event endpoints
case '/':
Expand All @@ -36,6 +43,8 @@ function onRequest(req: IncomingMessage, res: ServerResponse) {
return writeStalledConnection(req, res)
case '/trickle':
return writeTricklingConnection(req, res)
case '/unicode':
return writeUnicode(req, res)

// Browser test endpoints (HTML/JS)
case '/browser-test':
Expand Down Expand Up @@ -170,6 +179,49 @@ async function writeStalledConnection(req: IncomingMessage, res: ServerResponse)
// Intentionally not closing on first-connect that never sends data after welcome
}

async function writeUnicode(_req: IncomingMessage, res: ServerResponse) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive',
})

res.write(
formatEvent({
event: 'welcome',
data: 'Connected - I will now send some chonks (cuter chunks) with unicode',
}),
)

res.write(
formatEvent({
event: 'unicode',
data: unicodeLines[0],
}),
)

await delay(100)

// Start of a valid SSE chunk
res.write('event: unicode\ndata: ')

// Write "Espen ❤️ Kokos" in two halves:
// 1st: Espen � [..., 226, 153]
// 2st: � Kokos [165, 32, ...]
res.write(new Uint8Array([69, 115, 112, 101, 110, 32, 226, 153]))

// Give time to the client to process the first half
await delay(1000)

res.write(new Uint8Array([165, 32, 75, 111, 107, 111, 115]))

// Closing end of packet
res.write('\n\n\n\n')

res.write(formatEvent({event: 'disconnect', data: 'Thanks for listening'}))
res.end()
}

async function writeTricklingConnection(_req: IncomingMessage, res: ServerResponse) {
res.writeHead(200, {
'Content-Type': 'text/event-stream',
Expand Down
28 changes: 27 additions & 1 deletion test/tests.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import {CLOSED, CONNECTING, OPEN} from '../src/constants'
import type {createEventSource as CreateEventSourceFn} from '../src/default'
import type {createEventSource as CreateEventSourceFn, EventSourceMessage} from '../src/default'
import {unicodeLines} from './fixtures'
import {deferClose, expect, getCallCounter} from './helpers'
import type {TestRunner} from './waffletest'

Expand Down Expand Up @@ -41,6 +42,31 @@ export function registerTests(options: {
await deferClose(es)
})

test('can handle unicode data correctly', async () => {
const onMessage = getCallCounter()
const es = createEventSource({
url: new URL(`${baseUrl}:${port}/unicode`),
fetch,
onMessage,
})

const messages: EventSourceMessage[] = []
for await (const event of es) {
if (event.event === 'unicode') {
messages.push(event)
}

if (messages.length === 2) {
break
}
}

expect(messages[0].data).toBe(unicodeLines[0])
expect(messages[1].data).toBe(unicodeLines[1])

await deferClose(es)
})

test('will reconnect with last received message id if server disconnects', async () => {
const onMessage = getCallCounter()
const onDisconnect = getCallCounter()
Expand Down

0 comments on commit e97538f

Please sign in to comment.