Skip to content

Commit

Permalink
clients/web: use dedicated library to connect to SSE
Browse files Browse the repository at this point in the history
The builtin EventSource class doesn't support to pass custom headers to the request, like Authorization.

From what I read, this class is somewhat deprecated and browser developers encourage to use `fetch` instead. The thing is, it's a much lower level API which doesn't do the work of reading and parsing the stream. Ref: whatwg/html#2177

I found `event-source-plus`, a library which does all that work on top of `fetch`.
  • Loading branch information
frankie567 committed Dec 13, 2024
1 parent c27ee1f commit 8e0977c
Show file tree
Hide file tree
Showing 3 changed files with 48 additions and 16 deletions.
1 change: 1 addition & 0 deletions clients/apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
"d3": "^7.9.0",
"d3-scale-chromatic": "^3.1.0",
"date-fns": "^3.6.0",
"event-source-plus": "^0.1.8",
"eventemitter3": "^5.0.1",
"framer-motion": "^10.18.0",
"geist": "^1.3.1",
Expand Down
39 changes: 23 additions & 16 deletions clients/apps/web/src/hooks/sse/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { getServerURL } from '@/utils/api'
import { EventSourcePlus } from 'event-source-plus'
import EventEmitter from 'eventemitter3'
import { useEffect } from 'react'
import { onBenefitGranted, onBenefitRevoked } from './benefits'
Expand All @@ -16,29 +17,30 @@ const ACTIONS: {

const emitter = new EventEmitter()

const useSSE = (streamURL: string): EventEmitter => {
const useSSE = (streamURL: string, token?: string): EventEmitter => {
useEffect(() => {
const connection = new EventSource(streamURL, {
withCredentials: true,
const eventSource = new EventSourcePlus(streamURL, {
credentials: 'include',
headers: { ...(token ? { Authorization: `Bearer ${token}` } : {}) },
})

const controller = eventSource.listen({
onMessage: async (message) => {
const data = JSON.parse(message.data)
const handler = ACTIONS[data.key]
if (handler) {
await handler(data.payload)
}
emitter.emit(data.key, data.payload)
},
})

const cleanup = () => {
connection.close()
}
// TODO: Add types for event. Just want to get the structure
// up and running first before getting stuck in protocol land.
connection.onmessage = async (event) => {
const data = JSON.parse(event.data)
const handler = ACTIONS[data.key]
if (handler) {
await handler(data.payload)
}
emitter.emit(data.key, data.payload)
controller.abort()
}

connection.onerror = (_event) => cleanup
return cleanup
}, [streamURL])
}, [streamURL, token])

return emitter
}
Expand All @@ -48,3 +50,8 @@ export const useOrganizationSSE = (organizationId: string) =>
useSSE(getServerURL(`/v1/stream/organizations/${organizationId}`))
export const useCheckoutClientSSE = (clientSecret: string) =>
useSSE(getServerURL(`/v1/checkouts/custom/client/${clientSecret}/stream`))
export const useCustomerSSE = (customerSessionToken?: string) =>
useSSE(
getServerURL('/v1/customer-portal/customers/stream'),
customerSessionToken,
)
24 changes: 24 additions & 0 deletions clients/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 8e0977c

Please sign in to comment.