Skip to content

Commit a39b908

Browse files
feat: include request in events (#31)
Signed-off-by: Lukas Reining <lukas.reining@codecentric.de> # Conflicts: # src/eventsource.ts
1 parent 7857095 commit a39b908

File tree

2 files changed

+89
-34
lines changed

2 files changed

+89
-34
lines changed

src/eventsource.spec.ts

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,22 @@ describe('EventSource', () => {
2121
};
2222
});
2323

24+
it('includes request and response in open event', (done) => {
25+
mockChunks();
26+
27+
const ev = new EventSource('http://localhost/sse', {
28+
disableRetry: true,
29+
});
30+
31+
ev.onopen = (event) => {
32+
expect(event).toBeInstanceOf(Event);
33+
expect(event.request).toBeInstanceOf(Request);
34+
expect(event.response).toBeInstanceOf(Response);
35+
done();
36+
};
37+
});
38+
39+
2440
it('sends custom headers to the backend', (done) => {
2541
server.use(
2642
http.get('http://localhost/sse', (request) => {
@@ -83,6 +99,32 @@ describe('EventSource', () => {
8399
};
84100
});
85101

102+
it('fails fatally if wrong status code is returned', (done) => {
103+
server.use(
104+
http.get('http://localhost/sse', () => {
105+
return new MswHttpResponse(new ReadableStream(), {
106+
status: 401,
107+
headers: {
108+
'Content-Type': 'text/event-stream',
109+
},
110+
});
111+
}),
112+
);
113+
114+
const ev = new EventSource('http://localhost/sse', {
115+
disableRetry: true,
116+
});
117+
118+
ev.onerror = (event: CustomEvent) => {
119+
expect(ev.readyState).toEqual(ev.CLOSED);
120+
expect(event.request).toBeInstanceOf(Request);
121+
expect(event.request?.url).toEqual("http://localhost/sse");
122+
expect(event.response).toBeInstanceOf(Response);
123+
expect(event.response?.status).toEqual(401);
124+
done();
125+
};
126+
});
127+
86128
it('fails fatally if wrong content type is returned', (done) => {
87129
server.use(
88130
http.get('http://localhost/sse', () => {
@@ -104,7 +146,7 @@ describe('EventSource', () => {
104146
};
105147
});
106148

107-
it('fails fatally if wrong status code is returned', (done) => {
149+
it('return request and ', (done) => {
108150
server.use(
109151
http.get('http://localhost/sse', () => {
110152
return new MswHttpResponse(new ReadableStream(), {

src/eventsource.ts

Lines changed: 46 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ export type EventSourceOptions = {
3737
} & Omit<RequestInit, 'cache' | 'credentials' | 'signal'>;
3838

3939
export type CustomEvent = Event & {
40+
request?: Request;
4041
response?: Response;
4142
};
4243

@@ -109,40 +110,43 @@ export class CustomEventSource extends EventTarget implements EventSource {
109110
return;
110111
}
111112

113+
this.abortController = new AbortController();
114+
this.readyState = this.CONNECTING;
115+
116+
const fetchOptions: RequestInit = {
117+
...this.options,
118+
headers: lastEventId
119+
? {
120+
...this.options.headers,
121+
Accept: ContentTypeEventStream,
122+
'Last-Event-ID': lastEventId,
123+
}
124+
: {
125+
...this.options.headers,
126+
Accept: ContentTypeEventStream,
127+
},
128+
cache: 'no-store',
129+
credentials: this.options.omitCredentials
130+
? 'omit'
131+
: this.withCredentials
132+
? 'include'
133+
: 'same-origin',
134+
signal: this.abortController?.signal,
135+
};
136+
137+
const request = new Request(this.url, fetchOptions);
138+
112139
try {
113140
// https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource
114-
this.abortController = new AbortController();
115-
this.readyState = this.CONNECTING;
116-
117-
const fetchOptions: RequestInit = {
118-
...this.options,
119-
headers: lastEventId
120-
? {
121-
...this.options.headers,
122-
Accept: ContentTypeEventStream,
123-
'Last-Event-ID': lastEventId,
124-
}
125-
: {
126-
...this.options.headers,
127-
Accept: ContentTypeEventStream,
128-
},
129-
cache: 'no-store',
130-
credentials: this.options.omitCredentials
131-
? 'omit'
132-
: this.withCredentials
133-
? 'include'
134-
: 'same-origin',
135-
signal: this.abortController?.signal,
136-
};
137-
138141
const response = this.options.fetch
139-
? await this.options.fetch(this.url, fetchOptions)
140-
: await globalThis.fetch(this.url, fetchOptions);
142+
? await this.options.fetch(request)
143+
: await globalThis.fetch(request);
141144

142145
// https://html.spec.whatwg.org/multipage/server-sent-events.html#dom-eventsource (Step 15)
143146
if (response.status !== 200) {
144147
return this.failConnection(
145148
`Request failed with status code ${response.status}`,
149+
request,
146150
response,
147151
);
148152
} else if (
@@ -152,16 +156,18 @@ export class CustomEventSource extends EventTarget implements EventSource {
152156
`Request failed with wrong content type '${response.headers.get(
153157
'Content-Type',
154158
)}'`,
159+
request,
155160
response,
156161
);
157162
} else if (!response?.body) {
158163
return this.failConnection(
159164
`Request failed with empty response body'`,
165+
request,
160166
response,
161167
);
162168
}
163169

164-
this.announceConnection(response);
170+
this.announceConnection(request, response);
165171

166172
const reader: ReadableStreamDefaultReader<Uint8Array> =
167173
response.body.getReader();
@@ -194,16 +200,21 @@ export class CustomEventSource extends EventTarget implements EventSource {
194200
return;
195201
}
196202

197-
await this.reconnect('Reconnecting EventSource because of error', error);
203+
await this.reconnect(
204+
'Reconnecting EventSource because of error',
205+
error,
206+
request,
207+
);
198208
return;
199209
}
200210

201-
await this.reconnect('Reconnecting because EventSource connection closed');
211+
await this.reconnect('Reconnecting because EventSource connection closed', request);
202212
}
203213

204214
// https://html.spec.whatwg.org/multipage/server-sent-events.html#reestablish-the-connection
205-
private async reconnect(msg?: string, error?: unknown) {
206-
const event = new Event('error');
215+
private async reconnect(msg?: string, error?: unknown, request?: Request) {
216+
const event: CustomEvent = new Event('error');
217+
event.request = request;
207218
this.dispatchEvent(event);
208219
this.onerror?.(event);
209220

@@ -246,20 +257,22 @@ export class CustomEventSource extends EventTarget implements EventSource {
246257
}
247258

248259
// https://html.spec.whatwg.org/multipage/server-sent-events.html#fail-the-connection
249-
private failConnection(error: unknown, response: Response) {
260+
private failConnection(error: unknown, request: Request, response: Response) {
250261
this.logger?.error('Fatal error occurred in EventSource', error);
251262
this.readyState = this.CLOSED;
252263
const event: CustomEvent = new Event('error');
264+
event.request = request;
253265
event.response = response;
254266
this.dispatchEvent(event);
255267
this.onerror?.(event);
256268
}
257269

258270
// https://html.spec.whatwg.org/multipage/server-sent-events.html#announce-the-connection
259-
private announceConnection(response: Response) {
271+
private announceConnection(request: Request, response: Response) {
260272
this.logger?.debug('Connection established');
261273
this.readyState = this.OPEN;
262274
const event: CustomEvent = new Event('open');
275+
event.request = request;
263276
event.response = response;
264277
this.dispatchEvent(event);
265278
this.onopen?.(event);

0 commit comments

Comments
 (0)