Skip to content

Commit 538c69f

Browse files
committed
Implement Last-Event-ID support (closes #50)
This change implements support for the `lastEventId` attribute and setting the `Last-Event-ID` header on reconnection requests, per the EventSource specification.
1 parent 4cf1158 commit 538c69f

File tree

3 files changed

+90
-6
lines changed

3 files changed

+90
-6
lines changed

Diff for: README.md

+11-1
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ is the UNIX timestamp of the _reception_ of the event.
9494
Additionally, the events will have the following fields:
9595

9696
- `id`: the event ID, if present; `null` otherwise
97+
- `lastEventId`: the last seen event ID, or the empty string if no event
98+
with an ID was received
9799
- `data`: the event data, unparsed
98100

99101
`SSE`, like `EventSource`, will emit the following events:
@@ -155,10 +157,18 @@ request that the outgoing HTTP request be made with a CORS credentials
155157
mode of `include`, as per the [HTML Living
156158
Standard](https://fetch.spec.whatwg.org/#concept-request-credentials-mode).
157159

160+
## Reconnecting after failure
161+
162+
SSE.js does not (yet) automatically reconnect on failure; you can listen
163+
for the `abort` event and decide whether to reconnect and restart the
164+
event stream by calling `stream()`.
165+
166+
SSE.js _will_ set the `Last-Event-ID` header on reconnection to the last
167+
seen event ID value (if any), as per the EventSource specification.
168+
158169
## TODOs and caveats
159170

160171
- Internet Explorer 11 does not support arbitrary values in
161172
`CustomEvent`s. A dependency on `custom-event-polyfill` is necessary
162173
for IE11 compatibility.
163174
- Improve `XmlHttpRequest` error handling and connection states
164-
- Automatically reconnect with `Last-Event-ID`

Diff for: lib/sse.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,8 @@ var SSE = function (url, options) {
4848
this.progress = 0;
4949
/** @type {string} */
5050
this.chunk = '';
51+
/** @type {string} */
52+
this.lastEventId = '';
5153

5254
/**
5355
* @type AddEventListener
@@ -222,9 +224,14 @@ var SSE = function (url, options) {
222224
}
223225
}.bind(this));
224226

225-
var event = new CustomEvent(e.event || 'message');
226-
event.data = e.data || '';
227+
if (e.id !== null) {
228+
this.lastEventId = e.id;
229+
}
230+
231+
const event = new CustomEvent(e.event || 'message');
227232
event.id = e.id;
233+
event.data = e.data || '';
234+
event.lastEventId = this.lastEventId;
228235
return event;
229236
};
230237

@@ -261,6 +268,9 @@ var SSE = function (url, options) {
261268
for (let header in this.headers) {
262269
this.xhr.setRequestHeader(header, this.headers[header]);
263270
}
271+
if (this.lastEventId.length > 0) {
272+
this.xhr.setRequestHeader("Last-Event-ID", this.lastEventId);
273+
}
264274
this.xhr.withCredentials = this.withCredentials;
265275
this.xhr.send(this.payload);
266276
};

Diff for: lib/sse.test.js

+67-3
Original file line numberDiff line numberDiff line change
@@ -115,6 +115,21 @@ describe('SSE Lifecycle', () => {
115115
sse.xhr.trigger('abort', {});
116116
expect(sse.readyState).toBe(sse.CLOSED);
117117
});
118+
119+
it('should sent Last-Event-ID on reconnection', () => {
120+
sse.stream();
121+
expect(sse.xhr.setRequestHeader).toHaveBeenCalledTimes(0);
122+
sse.xhr.responseText = 'id: event-1\ndata: Test message\n\n';
123+
sse.xhr.trigger('progress', {});
124+
expect(sse.lastEventId).toBe('event-1');
125+
126+
sse.xhr.trigger('abort', {});
127+
expect(sse.readyState).toBe(sse.CLOSED);
128+
expect(sse.lastEventId).toBe('event-1');
129+
130+
sse.stream();
131+
expect(sse.xhr.setRequestHeader).toHaveBeenCalledWith('Last-Event-ID', 'event-1');
132+
});
118133
});
119134

120135
describe('SSE Event handling and Listeners', () => {
@@ -147,22 +162,71 @@ describe('SSE Event handling and Listeners', () => {
147162
expect(listener).toHaveBeenCalledTimes(1);
148163
expect(listener.mock.calls[0][0].data).toBe('Test message');
149164
expect(listener.mock.calls[0][0].event).toBe(undefined);
150-
expect(listener.mock.calls[0][0].id).toBe("1");
165+
expect(listener.mock.calls[0][0].id).toBe('1');
166+
expect(listener.mock.calls[0][0].lastEventId).toBe('1');
151167
})
152168

153169
it('should handle multiple data events', () => {
154-
sse.xhr.responseText = 'data: First message\n\n';
170+
sse.xhr.responseText = 'id: id1\ndata: First message\n\n';
171+
sse.xhr.trigger('progress', {});
172+
sse.xhr.responseText += 'id: id2\ndata: Second message\n\n';
173+
sse.xhr.trigger('progress', {});
174+
175+
expect(listener).toHaveBeenCalledTimes(2);
176+
expect(listener.mock.calls[0][0].data).toBe('First message');
177+
expect(listener.mock.calls[0][0].event).toBe(undefined);
178+
expect(listener.mock.calls[0][0].id).toBe('id1');
179+
expect(listener.mock.calls[0][0].lastEventId).toBe('id1');
180+
expect(listener.mock.calls[1][0].data).toBe('Second message');
181+
expect(listener.mock.calls[1][0].event).toBe(undefined);
182+
expect(listener.mock.calls[1][0].id).toBe('id2');
183+
expect(listener.mock.calls[1][0].lastEventId).toBe('id2');
184+
});
185+
186+
it('should set lastEventId only when id field is in the event', () => {
187+
sse.xhr.responseText = 'id: id1\ndata: First message\n\n';
155188
sse.xhr.trigger('progress', {});
156189
sse.xhr.responseText += 'data: Second message\n\n';
157190
sse.xhr.trigger('progress', {});
158191

159192
expect(listener).toHaveBeenCalledTimes(2);
160193
expect(listener.mock.calls[0][0].data).toBe('First message');
161194
expect(listener.mock.calls[0][0].event).toBe(undefined);
162-
expect(listener.mock.calls[0][0].id).toBe(null);
195+
expect(listener.mock.calls[0][0].id).toBe('id1');
196+
expect(listener.mock.calls[0][0].lastEventId).toBe('id1');
163197
expect(listener.mock.calls[1][0].data).toBe('Second message');
164198
expect(listener.mock.calls[1][0].event).toBe(undefined);
165199
expect(listener.mock.calls[1][0].id).toBe(null);
200+
expect(listener.mock.calls[1][0].lastEventId).toBe('id1');
201+
});
202+
203+
it('should reset lastEventId when id is empty', () => {
204+
sse.xhr.responseText = 'data: First message\n\n';
205+
sse.xhr.trigger('progress', {});
206+
sse.xhr.responseText += 'data: Second message\nid: id2\n\n';
207+
sse.xhr.trigger('progress', {});
208+
sse.xhr.responseText += 'data: Third message\n\n';
209+
sse.xhr.trigger('progress', {});
210+
sse.xhr.responseText += 'data: Fourth message\nid\n\n';
211+
sse.xhr.trigger('progress', {});
212+
213+
expect(listener).toHaveBeenCalledTimes(4);
214+
expect(listener.mock.calls[0][0].data).toBe('First message');
215+
expect(listener.mock.calls[0][0].event).toBe(undefined);
216+
expect(listener.mock.calls[0][0].id).toBe(null);
217+
expect(listener.mock.calls[0][0].lastEventId).toBe('');
218+
expect(listener.mock.calls[1][0].data).toBe('Second message');
219+
expect(listener.mock.calls[1][0].event).toBe(undefined);
220+
expect(listener.mock.calls[1][0].id).toBe('id2');
221+
expect(listener.mock.calls[1][0].lastEventId).toBe('id2');
222+
expect(listener.mock.calls[2][0].data).toBe('Third message');
223+
expect(listener.mock.calls[2][0].event).toBe(undefined);
224+
expect(listener.mock.calls[2][0].id).toBe(null);
225+
expect(listener.mock.calls[2][0].lastEventId).toBe('id2');
226+
expect(listener.mock.calls[3][0].data).toBe('Fourth message');
227+
expect(listener.mock.calls[3][0].event).toBe(undefined);
228+
expect(listener.mock.calls[3][0].id).toBe('');
229+
expect(listener.mock.calls[3][0].lastEventId).toBe('');
166230
});
167231

168232
it('should handle repeat data elements', () => {

0 commit comments

Comments
 (0)