Skip to content

Commit 647822a

Browse files
authored
Use SSE as module (#281)
This allows each component to start its own subscription with a cleaner interface.
1 parent 6de3e8d commit 647822a

File tree

16 files changed

+182
-199
lines changed

16 files changed

+182
-199
lines changed

assets/js/Chat/Widget.js

Lines changed: 19 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import {service} from './ChatService.js'
22
import {html} from 'uhtml/node.js'
3+
import * as sse from '../Common/EventSource.js'
34

45
customElements.define('chat-widget', class extends HTMLElement {
56
connectedCallback() {
6-
this._onDisconnect = [];
7+
this._sseAbortController = new AbortController();
78

89
this.append(this._rootElement = html`
910
<div class="card gp-loading" style="height: 400px" id="chat">
@@ -33,26 +34,33 @@ customElements.define('chat-widget', class extends HTMLElement {
3334
this._secondsBeforeRetryAfterLoadMessageFailure = parseInt(this.getAttribute('seconds-before-retry'));
3435

3536
const chatId = this.getAttribute('chat-id');
36-
if (chatId) {
37-
this._initialize(chatId);
38-
}
37+
if (chatId) this._initialize(chatId);
3938

40-
this._registerEventHandler();
39+
window.addEventListener('WebInterface.UserArrived', this._onUserArrived);
40+
sse.subscribe(this.getAttribute('game-channel'), {
41+
'ConnectFour.ChatAssigned': this._onChatAssigned.bind(this)
42+
}, this._sseAbortController.signal);
4143
}
4244

4345
disconnectedCallback() {
44-
this._onDisconnect.forEach(f => f());
46+
window.removeEventListener('WebInterface.UserArrived', this._onUserArrived);
47+
this._sseAbortController.abort();
4548
}
4649

4750
/**
4851
* @param {String} chatId
4952
*/
5053
_initialize(chatId) {
51-
if (this._chatId === '') {
52-
this._chatId = chatId;
54+
if (this._chatId !== '') return;
5355

54-
this._loadMessages(chatId);
55-
}
56+
this._chatId = chatId;
57+
this._loadMessages(chatId);
58+
59+
this._input.addEventListener('keypress', this._onKeyPress.bind(this));
60+
61+
sse.subscribe(`chat-${chatId}`, {
62+
'Chat.MessageWritten': this._onMessageWritten.bind(this)
63+
}, this._sseAbortController.signal);
5664
}
5765

5866
/**
@@ -164,36 +172,15 @@ customElements.define('chat-widget', class extends HTMLElement {
164172
}
165173

166174
_onChatAssigned(event) {
167-
window.dispatchEvent(new CustomEvent('sse:addsubscription', {detail: {name: 'chat-' + event.detail.chatId}}));
168-
169175
this._initialize(event.detail.chatId);
170176
}
171177

172-
_onUserArrived(event) {
178+
_onUserArrived = event => {
173179
this._authorId = event.detail.userId;
174180

175181
this.querySelectorAll(`[data-author-id="${this._authorId}"]`).forEach(message => {
176182
message.querySelector('.chat-bubble').classList.add('chat-bubble-me');
177183
message.querySelector('.row').classList.add('align-items-end', 'justify-content-end');
178184
});
179185
}
180-
181-
_registerEventHandler() {
182-
this._input.addEventListener('keypress', this._onKeyPress.bind(this));
183-
184-
((n, f) => {
185-
window.addEventListener(n, f);
186-
this._onDisconnect.push(() => window.removeEventListener(n, f));
187-
})('Chat.MessageWritten', this._onMessageWritten.bind(this));
188-
189-
((n, f) => {
190-
window.addEventListener(n, f);
191-
this._onDisconnect.push(() => window.removeEventListener(n, f));
192-
})('ConnectFour.ChatAssigned', this._onChatAssigned.bind(this));
193-
194-
((n, f) => {
195-
window.addEventListener(n, f);
196-
this._onDisconnect.push(() => window.removeEventListener(n, f));
197-
})('WebInterface.UserArrived', this._onUserArrived.bind(this));
198-
}
199186
});

assets/js/Common/EventSource.js

Lines changed: 63 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -1,63 +1,63 @@
1-
customElements.define('event-source', class extends HTMLElement {
2-
connectedCallback() {
3-
this._eventSource = null;
4-
this._lastEventId = null;
5-
this._reconnectTimeout = null;
6-
this._subscriptions = this.getAttribute('subscriptions')?.split(',') ?? [];
7-
this._verbose = this.hasAttribute('verbose');
8-
9-
window.addEventListener('sse:addsubscription', this._onAddSubscription);
10-
window.addEventListener('app:load', this._connect);
11-
}
12-
13-
disconnectedCallback() {
14-
window.removeEventListener('sse:addsubscription', this._onAddSubscription);
15-
window.removeEventListener('app:load', this._connect);
16-
17-
this._eventSource && this._eventSource.close();
18-
clearTimeout(this._reconnectTimeout);
19-
}
20-
21-
_connect = () => {
22-
this._eventSource && this._eventSource.close();
23-
clearTimeout(this._reconnectTimeout);
24-
25-
let url = '/sse/sub?id=' + this._subscriptions.join(',');
26-
if (this._lastEventId !== null) url += '&last_event_id=' + this._lastEventId;
27-
28-
this._eventSource = new EventSource(url);
29-
this._eventSource.onmessage = this._onMessage;
30-
this._eventSource.onopen = this._onOpen;
31-
this._eventSource.onerror = this._onError;
32-
}
33-
34-
_onMessage = (message) => {
35-
this._lastEventId = message.lastEventId;
36-
37-
let [, eventName, eventData] = message.data.split(/([^:]+):(.*)/);
38-
let payload = JSON.parse(eventData);
39-
40-
this.dispatchEvent(new CustomEvent(eventName, {bubbles: true, detail: payload}));
41-
42-
this._verbose && console.log(eventName, payload);
43-
};
44-
45-
_onOpen = () => {
46-
this.dispatchEvent(new CustomEvent('sse:open', {bubbles: true}))
47-
}
48-
49-
_onError = () => {
50-
this.dispatchEvent(new CustomEvent('sse:error', {bubbles: true}));
51-
52-
if (this._eventSource.readyState !== EventSource.CLOSED) return;
53-
54-
this._reconnectTimeout = setTimeout(() => this._connect(), 3000 + Math.floor(Math.random() * 2000));
55-
}
56-
57-
_onAddSubscription = (event) => {
58-
if (this._subscriptions.indexOf(event.detail.name) !== -1) return;
59-
60-
this._subscriptions.push(event.detail.name);
61-
this._connect();
62-
}
63-
});
1+
/**
2+
* @typedef {{[key: string]: Function}} Listeners
3+
*/
4+
5+
const eventTarget = new EventTarget();
6+
const globalConfig = document.querySelector('meta[name="sse-config"]');
7+
let currentSubscriptionId = 0;
8+
const subscriptions = {};
9+
let eventSource = null;
10+
const baseUrl = globalConfig?.getAttribute('data-base-url') || '/sse/sub?id=';
11+
let debounceTimeout = null;
12+
const globalDebounceTimeoutMs = globalConfig?.getAttribute('data-debounce-ms') ?? 150;
13+
14+
function connect(debounceTimeoutMs = null) {
15+
clearTimeout(debounceTimeout);
16+
debounceTimeout = setTimeout(() => {
17+
if (eventSource) eventSource.close();
18+
19+
const uniqueChannels = [...new Set(Object.values(subscriptions).map(s => s.channel))];
20+
if (uniqueChannels.length === 0) return;
21+
22+
eventSource = new EventSource(baseUrl + uniqueChannels.join(','));
23+
eventSource.onmessage = onMessage;
24+
eventSource.onopen = onOpen;
25+
eventSource.onerror = onError;
26+
}, debounceTimeoutMs ?? globalDebounceTimeoutMs);
27+
}
28+
29+
function onMessage(event) {
30+
const [, type, payload] = event.data.split(/([^:]+):(.*)/);
31+
32+
Object.values(subscriptions).forEach(s => s.listeners[type]?.({type, detail: JSON.parse(payload)}));
33+
}
34+
35+
function onOpen() {
36+
eventTarget.dispatchEvent(new CustomEvent('open'))
37+
}
38+
39+
function onError () {
40+
eventTarget.dispatchEvent(new CustomEvent('error'));
41+
42+
if (eventSource.readyState !== EventSource.CLOSED) return;
43+
44+
connect(3000 + Math.floor(Math.random() * 2000));
45+
}
46+
47+
/**
48+
* @param {String} channel
49+
* @param {Listeners} listeners
50+
* @param {AbortSignal|null} signal
51+
*/
52+
export function subscribe(channel, listeners, signal = null) {
53+
const subscriptionId = ++currentSubscriptionId;
54+
subscriptions[subscriptionId] = {channel, listeners};
55+
signal?.addEventListener('abort', () => {
56+
delete subscriptions[subscriptionId];
57+
connect();
58+
});
59+
connect();
60+
}
61+
62+
export const addEventListener = (...args) => eventTarget.addEventListener(...args);
63+
export const removeEventListener = (...args) => eventTarget.removeEventListener(...args);

assets/js/Common/EventSourceStatus.js

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {html} from 'uhtml/node.js'
2+
import * as sse from '../Common/EventSource.js'
23

34
customElements.define('event-source-status', class extends HTMLElement {
45
connectedCallback() {
@@ -13,19 +14,14 @@ customElements.define('event-source-status', class extends HTMLElement {
1314
this._isInErrorState = false;
1415
this._tooltipTimeout = null;
1516

16-
document.addEventListener('sse:open', this._open);
17-
document.addEventListener('sse:error', this._error);
18-
window.addEventListener('app:load', this._appLoad);
17+
this._open();
18+
sse.addEventListener('open', this._open);
19+
sse.addEventListener('error', this._error);
1920
}
2021

2122
disconnectedCallback() {
22-
document.removeEventListener('sse:connected', this._open);
23-
document.removeEventListener('sse:disconnected', this._error);
24-
window.removeEventListener('app:load', this._appLoad);
25-
}
26-
27-
_appLoad = () => {
28-
if (!document.querySelector('event-source')) this._open();
23+
sse.removeEventListener('open', this._open);
24+
sse.removeEventListener('error', this._error);
2925
}
3026

3127
_open = () => {

assets/js/ConnectFour/AbortButton.js

Lines changed: 12 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import {service} from './GameService.js'
22
import 'confirmation-button'
33
import {html} from 'uhtml/node.js'
4+
import * as sse from '../Common/EventSource.js'
45

56
customElements.define('connect-four-abort-button', class extends HTMLElement {
67
connectedCallback() {
8+
this._sseAbortController = new AbortController();
9+
710
this.replaceChildren(html`
811
<confirmation-button @confirmation-button:yes="${this._onYes.bind(this)}">
912
${Array.from(this.children)}
@@ -16,23 +19,22 @@ customElements.define('connect-four-abort-button', class extends HTMLElement {
1619

1720
this._changeVisibility();
1821

19-
window.addEventListener('ConnectFour.PlayerJoined', this._onPlayerJoined);
2022
window.addEventListener('ConnectFour.PlayerMoved', this._onPlayerMoved);
2123
window.addEventListener('ConnectFour.PlayerMovedFailed', this._onPlayerMovedFailed);
22-
window.addEventListener('ConnectFour.GameAborted', this._remove);
23-
window.addEventListener('ConnectFour.GameWon', this._remove);
24-
window.addEventListener('ConnectFour.GameResigned', this._remove);
25-
window.addEventListener('ConnectFour.GameDrawn', this._remove);
24+
sse.subscribe(`connect-four-${this.getAttribute('game-id')}`, {
25+
'ConnectFour.PlayerJoined': this._onPlayerJoined,
26+
'ConnectFour.PlayerMoved': this._onPlayerMoved,
27+
'ConnectFour.GameAborted': this._remove,
28+
'ConnectFour.GameWon': this._remove,
29+
'ConnectFour.GameResigned': this._remove,
30+
'ConnectFour.GameDrawn': this._remove
31+
}, this._sseAbortController.signal);
2632
}
2733

2834
disconnectedCallback() {
29-
window.removeEventListener('ConnectFour.PlayerJoined', this._onPlayerJoined);
3035
window.removeEventListener('ConnectFour.PlayerMoved', this._onPlayerMoved);
3136
window.removeEventListener('ConnectFour.PlayerMovedFailed', this._onPlayerMovedFailed);
32-
window.removeEventListener('ConnectFour.GameWon', this._remove);
33-
window.removeEventListener('ConnectFour.GameAborted', this._remove);
34-
window.removeEventListener('ConnectFour.GameResigned', this._remove);
35-
window.removeEventListener('ConnectFour.GameDrawn', this._remove);
37+
this._sseAbortController.abort();
3638
}
3739

3840
_onYes(e) {

assets/js/ConnectFour/Game.js

Lines changed: 15 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
import {service} from './GameService.js'
22
import {Game as GameModel} from './Model/Game.js'
33
import {html} from 'uhtml/node.js'
4+
import * as sse from '../Common/EventSource.js'
45

56
customElements.define('connect-four-game', class extends HTMLElement {
67
connectedCallback() {
8+
this._sseAbortController = new AbortController();
79
let game = JSON.parse(this.getAttribute('game'));
810

911
this.append(this._gameNode = html`
@@ -33,12 +35,7 @@ customElements.define('connect-four-game', class extends HTMLElement {
3335
}
3436

3537
disconnectedCallback() {
36-
window.removeEventListener('ConnectFour.PlayerJoined', this._onPlayerJoined);
37-
window.removeEventListener('ConnectFour.PlayerMoved', this._onPlayerMoved);
38-
window.removeEventListener('ConnectFour.GameWon', this._onGameWon);
39-
window.removeEventListener('ConnectFour.GameDrawn', this._onGameFinished);
40-
window.removeEventListener('ConnectFour.GameAborted', this._onGameFinished);
41-
window.removeEventListener('ConnectFour.GameResigned', this._onGameFinished);
38+
this._sseAbortController.abort();
4239
}
4340

4441
/**
@@ -254,13 +251,7 @@ customElements.define('connect-four-game', class extends HTMLElement {
254251

255252
_registerEventHandler() {
256253
this.addEventListener('ConnectFour.PlayerMovedFailed', this._onPlayerMovedFailed.bind(this));
257-
258-
window.addEventListener('ConnectFour.PlayerJoined', this._onPlayerJoined);
259-
window.addEventListener('ConnectFour.PlayerMoved', this._onPlayerMoved);
260-
window.addEventListener('ConnectFour.GameWon', this._onGameWon);
261-
window.addEventListener('ConnectFour.GameDrawn', this._onGameFinished);
262-
window.addEventListener('ConnectFour.GameAborted', this._onGameFinished);
263-
window.addEventListener('ConnectFour.GameResigned', this._onGameFinished);
254+
this.addEventListener('ConnectFour.PlayerMoved', this._onPlayerMoved);
264255

265256
this._fields.forEach(field => {
266257
field.addEventListener('click', this._onFieldClick.bind(this));
@@ -271,5 +262,16 @@ customElements.define('connect-four-game', class extends HTMLElement {
271262
this._previousMoveButton.addEventListener('click', this._onPreviousMoveClick.bind(this));
272263
this._nextMoveButton.addEventListener('click', this._onNextMoveClick.bind(this));
273264
this._followMovesButton.addEventListener('click', this._onFollowMovesClick.bind(this));
265+
266+
if (!['open', 'running'].includes(this._game.state)) return;
267+
268+
sse.subscribe(`connect-four-${this._game.gameId}`, {
269+
'ConnectFour.PlayerJoined': this._onPlayerJoined,
270+
'ConnectFour.PlayerMoved': this._onPlayerMoved,
271+
'ConnectFour.GameWon': this._onGameWon,
272+
'ConnectFour.GameDrawn': this._onGameFinished,
273+
'ConnectFour.GameAborted': this._onGameFinished,
274+
'ConnectFour.GameResigned': this._onGameFinished
275+
}, this._sseAbortController.signal);
274276
}
275277
});

0 commit comments

Comments
 (0)