* Move EventSource to SharedWorker (#12095) Backport #12095 Move EventSource to use a SharedWorker. This prevents issues with HTTP/1.1 open browser connections from preventing gitea from opening multiple tabs. Also allow setting EVENT_SOURCE_UPDATE_TIME to disable EventSource updating Fix #11978 Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: techknowlogick <techknowlogick@gitea.io> * Bugfix for shared event source For some reason our eslint configuration is not working correctly and a bug has become apparent when trying to backport this to 1.12. Signed-off-by: Andrew Thornton <art27@cantab.net> * Re-fix #12095 again Unfortunately some of the suggested changes to #12095 introduced bugs which due to caching behaviour of sharedworkers were not caught on simple tests. These are as follows: * Changing from simple for loop to use includes here: ```js register(port) { if (!this.clients.includes(port)) return; this.clients.push(port); port.postMessage({ type: 'status', message: `registered to ${this.url}`, }); } ``` The additional `!` prevents any clients from being added and should read: ```js if (this.clients.includes(port)) return; ``` * Dropping the use of jQuery `$(...)` selection and using DOM `querySelector` here: ```js async function receiveUpdateCount(event) { try { const data = JSON.parse(event.data); const notificationCount = document.querySelector('.notification_count'); if (data.Count > 0) { notificationCount.classList.remove('hidden'); } else { notificationCount.classList.add('hidden'); } notificationCount.text() = `${data.Count}`; await updateNotificationTable(); } catch (error) { console.error(error, event); } } ``` Requires that `notificationCount.text()` be changed to use `textContent` instead. Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: silverwind <me@silverwind.io> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
8f64017058
commit
e46dbec294
9 changed files with 210 additions and 36 deletions
|
@ -25,7 +25,7 @@ globals:
|
||||||
Tribute: false
|
Tribute: false
|
||||||
|
|
||||||
overrides:
|
overrides:
|
||||||
- files: ["web_src/**/*.worker.js", "web_src/js/serviceworker.js"]
|
- files: ["web_src/**/*worker.js"]
|
||||||
env:
|
env:
|
||||||
worker: true
|
worker: true
|
||||||
rules:
|
rules:
|
||||||
|
|
|
@ -211,7 +211,7 @@ MIN_TIMEOUT = 10s
|
||||||
MAX_TIMEOUT = 60s
|
MAX_TIMEOUT = 60s
|
||||||
TIMEOUT_STEP = 10s
|
TIMEOUT_STEP = 10s
|
||||||
; This setting determines how often the db is queried to get the latest notification counts.
|
; This setting determines how often the db is queried to get the latest notification counts.
|
||||||
; If the browser client supports EventSource, it will be used in preference to polling notification.
|
; If the browser client supports EventSource and SharedWorker, a SharedWorker will be used in preference to polling notification. Set to -1 to disable the EventSource
|
||||||
EVENT_SOURCE_UPDATE_TIME = 10s
|
EVENT_SOURCE_UPDATE_TIME = 10s
|
||||||
|
|
||||||
[markdown]
|
[markdown]
|
||||||
|
|
|
@ -148,8 +148,7 @@ Values containing `#` or `;` must be quoted using `` ` `` or `"""`.
|
||||||
- `MIN_TIMEOUT`: **10s**: These options control how often notification endpoint is polled to update the notification count. On page load the notification count will be checked after `MIN_TIMEOUT`. The timeout will increase to `MAX_TIMEOUT` by `TIMEOUT_STEP` if the notification count is unchanged. Set MIN_TIMEOUT to 0 to turn off.
|
- `MIN_TIMEOUT`: **10s**: These options control how often notification endpoint is polled to update the notification count. On page load the notification count will be checked after `MIN_TIMEOUT`. The timeout will increase to `MAX_TIMEOUT` by `TIMEOUT_STEP` if the notification count is unchanged. Set MIN_TIMEOUT to 0 to turn off.
|
||||||
- `MAX_TIMEOUT`: **60s**.
|
- `MAX_TIMEOUT`: **60s**.
|
||||||
- `TIMEOUT_STEP`: **10s**.
|
- `TIMEOUT_STEP`: **10s**.
|
||||||
- `EVENT_SOURCE_UPDATE_TIME`: **10s**: This setting determines how often the database is queried to update notification counts. If the browser client supports `EventSource`, it will be used in preference to polling notification endpoint.
|
- `EVENT_SOURCE_UPDATE_TIME`: **10s**: This setting determines how often the database is queried to update notification counts. If the browser client supports `EventSource` and `SharedWorker`, a `SharedWorker` will be used in preference to polling notification endpoint. Set to **-1** to disable the `EventSource`.
|
||||||
|
|
||||||
|
|
||||||
## Markdown (`markdown`)
|
## Markdown (`markdown`)
|
||||||
|
|
||||||
|
|
|
@ -17,6 +17,9 @@ import (
|
||||||
|
|
||||||
// Init starts this eventsource
|
// Init starts this eventsource
|
||||||
func (m *Manager) Init() {
|
func (m *Manager) Init() {
|
||||||
|
if setting.UI.Notification.EventSourceUpdateTime <= 0 {
|
||||||
|
return
|
||||||
|
}
|
||||||
go graceful.GetManager().RunWithShutdownContext(m.Run)
|
go graceful.GetManager().RunWithShutdownContext(m.Run)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -289,8 +289,8 @@ func NewFuncMap() []template.FuncMap {
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"NotificationSettings": func() map[string]int {
|
"NotificationSettings": func() map[string]interface{} {
|
||||||
return map[string]int{
|
return map[string]interface{}{
|
||||||
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
|
"MinTimeout": int(setting.UI.Notification.MinTimeout / time.Millisecond),
|
||||||
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
|
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
|
||||||
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
|
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
|
||||||
|
|
135
web_src/js/features/eventsource.sharedworker.js
Normal file
135
web_src/js/features/eventsource.sharedworker.js
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
self.name = 'eventsource.sharedworker.js';
|
||||||
|
|
||||||
|
const sourcesByUrl = {};
|
||||||
|
const sourcesByPort = {};
|
||||||
|
|
||||||
|
class Source {
|
||||||
|
constructor(url) {
|
||||||
|
this.url = url;
|
||||||
|
this.eventSource = new EventSource(url);
|
||||||
|
this.listening = {};
|
||||||
|
this.clients = [];
|
||||||
|
this.listen('open');
|
||||||
|
this.listen('logout');
|
||||||
|
this.listen('notification-count');
|
||||||
|
this.listen('error');
|
||||||
|
}
|
||||||
|
|
||||||
|
register(port) {
|
||||||
|
if (this.clients.includes(port)) return;
|
||||||
|
|
||||||
|
this.clients.push(port);
|
||||||
|
|
||||||
|
port.postMessage({
|
||||||
|
type: 'status',
|
||||||
|
message: `registered to ${this.url}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deregister(port) {
|
||||||
|
const portIdx = this.clients.indexOf(port);
|
||||||
|
if (portIdx < 0) {
|
||||||
|
return this.clients.length;
|
||||||
|
}
|
||||||
|
this.clients.splice(portIdx, 1);
|
||||||
|
return this.clients.length;
|
||||||
|
}
|
||||||
|
|
||||||
|
close() {
|
||||||
|
if (!this.eventSource) return;
|
||||||
|
|
||||||
|
this.eventSource.close();
|
||||||
|
this.eventSource = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
listen(eventType) {
|
||||||
|
if (this.listening[eventType]) return;
|
||||||
|
this.listening[eventType] = true;
|
||||||
|
const self = this;
|
||||||
|
this.eventSource.addEventListener(eventType, (event) => {
|
||||||
|
self.notifyClients({
|
||||||
|
type: eventType,
|
||||||
|
data: event.data
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
notifyClients(event) {
|
||||||
|
for (const client of this.clients) {
|
||||||
|
client.postMessage(event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
status(port) {
|
||||||
|
port.postMessage({
|
||||||
|
type: 'status',
|
||||||
|
message: `url: ${this.url} readyState: ${this.eventSource.readyState}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
self.onconnect = (e) => {
|
||||||
|
for (const port of e.ports) {
|
||||||
|
port.addEventListener('message', (event) => {
|
||||||
|
if (event.data.type === 'start') {
|
||||||
|
const url = event.data.url;
|
||||||
|
if (sourcesByUrl[url]) {
|
||||||
|
// we have a Source registered to this url
|
||||||
|
const source = sourcesByUrl[url];
|
||||||
|
source.register(port);
|
||||||
|
sourcesByPort[port] = source;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let source = sourcesByPort[port];
|
||||||
|
if (source) {
|
||||||
|
if (source.eventSource && source.url === url) return;
|
||||||
|
|
||||||
|
// How this has happened I don't understand...
|
||||||
|
// deregister from that source
|
||||||
|
const count = source.deregister(port);
|
||||||
|
// Clean-up
|
||||||
|
if (count === 0) {
|
||||||
|
source.close();
|
||||||
|
sourcesByUrl[source.url] = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create a new Source
|
||||||
|
source = new Source(url);
|
||||||
|
source.register(port);
|
||||||
|
sourcesByUrl[url] = source;
|
||||||
|
sourcesByPort[port] = source;
|
||||||
|
} else if (event.data.type === 'listen') {
|
||||||
|
const source = sourcesByPort[port];
|
||||||
|
source.listen(event.data.eventType);
|
||||||
|
} else if (event.data.type === 'close') {
|
||||||
|
const source = sourcesByPort[port];
|
||||||
|
|
||||||
|
if (!source) return;
|
||||||
|
|
||||||
|
const count = source.deregister(port);
|
||||||
|
if (count === 0) {
|
||||||
|
source.close();
|
||||||
|
sourcesByUrl[source.url] = null;
|
||||||
|
sourcesByPort[port] = null;
|
||||||
|
}
|
||||||
|
} else if (event.data.type === 'status') {
|
||||||
|
const source = sourcesByPort[port];
|
||||||
|
if (!source) {
|
||||||
|
port.postMessage({
|
||||||
|
type: 'status',
|
||||||
|
message: 'not connected',
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
source.status(port);
|
||||||
|
} else {
|
||||||
|
// just send it back
|
||||||
|
port.postMessage({
|
||||||
|
type: 'error',
|
||||||
|
message: `received but don't know how to handle: ${event.data}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
port.start();
|
||||||
|
}
|
||||||
|
};
|
|
@ -18,7 +18,25 @@ export function initNotificationsTable() {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function initNotificationCount() {
|
async function receiveUpdateCount(event) {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(event.data);
|
||||||
|
|
||||||
|
const notificationCount = document.querySelector('.notification_count');
|
||||||
|
if (data.Count > 0) {
|
||||||
|
notificationCount.classList.remove('hidden');
|
||||||
|
} else {
|
||||||
|
notificationCount.classList.add('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationCount.textContent = `${data.Count}`;
|
||||||
|
await updateNotificationTable();
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error, event);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function initNotificationCount() {
|
||||||
const notificationCount = $('.notification_count');
|
const notificationCount = $('.notification_count');
|
||||||
|
|
||||||
if (!notificationCount.length) {
|
if (!notificationCount.length) {
|
||||||
|
@ -26,37 +44,53 @@ export function initNotificationCount() {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (NotificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource) {
|
if (NotificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource) {
|
||||||
// Try to connect to the event source first
|
// Try to connect to the event source via the shared worker first
|
||||||
const source = new EventSource(`${AppSubUrl}/user/events`);
|
if (window.SharedWorker) {
|
||||||
source.addEventListener('notification-count', async (e) => {
|
const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
|
||||||
try {
|
worker.addEventListener('error', (event) => {
|
||||||
const data = JSON.parse(e.data);
|
console.error(event);
|
||||||
|
|
||||||
const notificationCount = $('.notification_count');
|
|
||||||
if (data.Count === 0) {
|
|
||||||
notificationCount.addClass('hidden');
|
|
||||||
} else {
|
|
||||||
notificationCount.removeClass('hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
notificationCount.text(`${data.Count}`);
|
|
||||||
await updateNotificationTable();
|
|
||||||
} catch (error) {
|
|
||||||
console.error(error);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
source.addEventListener('logout', async (e) => {
|
worker.port.onmessageerror = () => {
|
||||||
if (e.data !== 'here') {
|
console.error('Unable to deserialize message');
|
||||||
|
};
|
||||||
|
worker.port.postMessage({
|
||||||
|
type: 'start',
|
||||||
|
url: `${window.location.origin}${AppSubUrl}/user/events`,
|
||||||
|
});
|
||||||
|
worker.port.addEventListener('message', (event) => {
|
||||||
|
if (!event.data || !event.data.type) {
|
||||||
|
console.error(event);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
source.close();
|
if (event.data.type === 'notification-count') {
|
||||||
|
receiveUpdateCount(event.data);
|
||||||
|
} else if (event.data.type === 'error') {
|
||||||
|
console.error(event.data);
|
||||||
|
} else if (event.data.type === 'logout') {
|
||||||
|
if (event.data !== 'here') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
worker.port.postMessage({
|
||||||
|
type: 'close',
|
||||||
|
});
|
||||||
|
worker.port.close();
|
||||||
window.location.href = AppSubUrl;
|
window.location.href = AppSubUrl;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
worker.port.addEventListener('error', (e) => {
|
||||||
|
console.error(e);
|
||||||
|
});
|
||||||
|
worker.port.start();
|
||||||
window.addEventListener('beforeunload', () => {
|
window.addEventListener('beforeunload', () => {
|
||||||
source.close();
|
worker.port.postMessage({
|
||||||
|
type: 'close',
|
||||||
});
|
});
|
||||||
|
worker.port.close();
|
||||||
|
});
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (NotificationSettings.MinTimeout <= 0) {
|
if (NotificationSettings.MinTimeout <= 0) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -2455,7 +2455,6 @@ $(document).ready(async () => {
|
||||||
initTemplateSearch();
|
initTemplateSearch();
|
||||||
initContextPopups();
|
initContextPopups();
|
||||||
initNotificationsTable();
|
initNotificationsTable();
|
||||||
initNotificationCount();
|
|
||||||
initTribute();
|
initTribute();
|
||||||
|
|
||||||
// Repo clone url.
|
// Repo clone url.
|
||||||
|
@ -2502,6 +2501,7 @@ $(document).ready(async () => {
|
||||||
initClipboard(),
|
initClipboard(),
|
||||||
initUserHeatmap(),
|
initUserHeatmap(),
|
||||||
initServiceWorker(),
|
initServiceWorker(),
|
||||||
|
initNotificationCount(),
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -38,6 +38,9 @@ module.exports = {
|
||||||
serviceworker: [
|
serviceworker: [
|
||||||
resolve(__dirname, 'web_src/js/serviceworker.js'),
|
resolve(__dirname, 'web_src/js/serviceworker.js'),
|
||||||
],
|
],
|
||||||
|
'eventsource.sharedworker': [
|
||||||
|
resolve(__dirname, 'web_src/js/features/eventsource.sharedworker.js'),
|
||||||
|
],
|
||||||
icons: glob('node_modules/@primer/octicons/build/svg/**/*.svg'),
|
icons: glob('node_modules/@primer/octicons/build/svg/**/*.svg'),
|
||||||
...themes,
|
...themes,
|
||||||
},
|
},
|
||||||
|
|
Reference in a new issue