Browse Source

Move EventSource to SharedWorker (#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>
tags/v1.13.0-rc1
zeripath 3 years ago
parent
commit
ae56411e9f
No account linked to committer's email address

+ 1
- 1
.eslintrc View File

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



+ 1
- 1
custom/conf/app.example.ini View File

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]

+ 1
- 2
docs/content/doc/advanced/config-cheat-sheet.en-us.md View File

- `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`)



+ 3
- 0
modules/eventsource/manager_run.go View File



// 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)
} }



+ 2
- 2
modules/templates/helper.go View File

return "" return ""
} }
}, },
"NotificationSettings": func() map[string]int {
return map[string]int{
"NotificationSettings": func() map[string]interface{} {
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),

+ 140
- 0
web_src/js/features/eventsource.sharedworker.js View File

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;
return;
} else if (event.data.type === 'listen') {
const source = sourcesByPort[port];
source.listen(event.data.eventType);
return;
} 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;
}
return;
} else if (event.data.type === 'status') {
const source = sourcesByPort[port];
if (!source) {
port.postMessage({
type: 'status',
message: 'not connected',
});
return;
}
source.status(port);
return;
} else {
// just send it back
port.postMessage({
type: 'error',
message: `received but don't know how to handle: ${event.data}`,
});
return;
}
});
port.start();
}
};

+ 68
- 29
web_src/js/features/notification.js View File

}); });
} }


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.text(`${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) {
} }


if (NotificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource) { if (NotificationSettings.EventSourceUpdateTime > 0 && !!window.EventSource) {
// Try to connect to the event source first
const source = new EventSource(`${AppSubUrl}/user/events`);
source.addEventListener('notification-count', async (e) => {
try {
const data = JSON.parse(e.data);

const notificationCount = $('.notification_count');
if (data.Count === 0) {
notificationCount.addClass('hidden');
// Try to connect to the event source via the shared worker first
if (window.SharedWorker) {
const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
worker.addEventListener('error', (event) => {
console.error(event);
});
worker.port.onmessageerror = () => {
console.error('Unable to deserialize message');
};
worker.port.postMessage({
type: 'start',
url: `${window.location.origin}${AppSubUrl}/user/events`,
});
worker.port.addEventListener('message', (e) => {
if (!e.data || !e.data.type) {
console.error(e);
return;
}
if (event.data.type === 'notification-count') {
receiveUpdateCount(e.data);
return;
} else if (event.data.type === 'error') {
console.error(e.data);
return;
} else if (event.data.type === 'logout') {
if (e.data !== 'here') {
return;
}
worker.port.postMessage({
type: 'close',
});
worker.port.close();
window.location.href = AppSubUrl;
return;
} else { } else {
notificationCount.removeClass('hidden');
return;
} }

notificationCount.text(`${data.Count}`);
await updateNotificationTable();
} catch (error) {
console.error(error);
}
});
source.addEventListener('logout', async (e) => {
if (e.data !== 'here') {
return;
}
source.close();
window.location.href = AppSubUrl;
});
window.addEventListener('beforeunload', () => {
source.close();
});
return;
});
worker.port.addEventListener('error', (e) => {
console.error(e);
});
worker.port.start();
window.addEventListener('beforeunload', () => {
worker.port.postMessage({
type: 'close',
});
worker.port.close();
});

return;
}
} }


if (NotificationSettings.MinTimeout <= 0) { if (NotificationSettings.MinTimeout <= 0) {

+ 1
- 1
web_src/js/index.js View File

initContextPopups(); initContextPopups();
initTableSort(); initTableSort();
initNotificationsTable(); initNotificationsTable();
initNotificationCount();


// Repo clone url. // Repo clone url.
if ($('#repo-clone-url').length > 0) { if ($('#repo-clone-url').length > 0) {
initClipboard(), initClipboard(),
initUserHeatmap(), initUserHeatmap(),
initServiceWorker(), initServiceWorker(),
initNotificationCount(),
]); ]);
}); });



+ 3
- 0
webpack.config.js View File

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: [ icons: [
...glob('node_modules/@primer/octicons/build/svg/**/*.svg'), ...glob('node_modules/@primer/octicons/build/svg/**/*.svg'),
...glob('assets/svg/*.svg'), ...glob('assets/svg/*.svg'),

Loading…
Cancel
Save