aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorzeripath <art27@cantab.net>2020-07-03 10:55:36 +0100
committerGitHub <noreply@github.com>2020-07-03 10:55:36 +0100
commitae56411e9f78415745c235fb92fbb7abc139b966 (patch)
tree20ce8e22055394dbf80ee9355cb5c1a03cb19479
parentcd994f635427388d4e2abb6612e0b830a8f485b9 (diff)
downloadgitea-ae56411e9f78415745c235fb92fbb7abc139b966.tar.gz
gitea-ae56411e9f78415745c235fb92fbb7abc139b966.zip
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>
-rw-r--r--.eslintrc2
-rw-r--r--custom/conf/app.example.ini2
-rw-r--r--docs/content/doc/advanced/config-cheat-sheet.en-us.md3
-rw-r--r--modules/eventsource/manager_run.go3
-rw-r--r--modules/templates/helper.go4
-rw-r--r--web_src/js/features/eventsource.sharedworker.js140
-rw-r--r--web_src/js/features/notification.js97
-rw-r--r--web_src/js/index.js2
-rw-r--r--webpack.config.js3
9 files changed, 220 insertions, 36 deletions
diff --git a/.eslintrc b/.eslintrc
index 1a7551aee7..8e478f4a5a 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -28,7 +28,7 @@ globals:
Tribute: false
overrides:
- - files: ["web_src/**/*.worker.js", "web_src/js/serviceworker.js"]
+ - files: ["web_src/**/*worker.js"]
env:
worker: true
diff --git a/custom/conf/app.example.ini b/custom/conf/app.example.ini
index 648cf59fec..d6c73fbe57 100644
--- a/custom/conf/app.example.ini
+++ b/custom/conf/app.example.ini
@@ -218,7 +218,7 @@ MIN_TIMEOUT = 10s
MAX_TIMEOUT = 60s
TIMEOUT_STEP = 10s
; 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
[markdown]
diff --git a/docs/content/doc/advanced/config-cheat-sheet.en-us.md b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
index 2cbe4c59ea..4fa0910b1f 100644
--- a/docs/content/doc/advanced/config-cheat-sheet.en-us.md
+++ b/docs/content/doc/advanced/config-cheat-sheet.en-us.md
@@ -150,8 +150,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.
- `MAX_TIMEOUT`: **60s**.
- `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`)
diff --git a/modules/eventsource/manager_run.go b/modules/eventsource/manager_run.go
index 75d3ee5b01..ccfe2e0709 100644
--- a/modules/eventsource/manager_run.go
+++ b/modules/eventsource/manager_run.go
@@ -17,6 +17,9 @@ import (
// Init starts this eventsource
func (m *Manager) Init() {
+ if setting.UI.Notification.EventSourceUpdateTime <= 0 {
+ return
+ }
go graceful.GetManager().RunWithShutdownContext(m.Run)
}
diff --git a/modules/templates/helper.go b/modules/templates/helper.go
index 53354a0d3d..5001963e90 100644
--- a/modules/templates/helper.go
+++ b/modules/templates/helper.go
@@ -289,8 +289,8 @@ func NewFuncMap() []template.FuncMap {
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),
"TimeoutStep": int(setting.UI.Notification.TimeoutStep / time.Millisecond),
"MaxTimeout": int(setting.UI.Notification.MaxTimeout / time.Millisecond),
diff --git a/web_src/js/features/eventsource.sharedworker.js b/web_src/js/features/eventsource.sharedworker.js
new file mode 100644
index 0000000000..7dccd8994e
--- /dev/null
+++ b/web_src/js/features/eventsource.sharedworker.js
@@ -0,0 +1,140 @@
+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();
+ }
+};
diff --git a/web_src/js/features/notification.js b/web_src/js/features/notification.js
index 0ea7f93c8d..2b7fc45237 100644
--- a/web_src/js/features/notification.js
+++ b/web_src/js/features/notification.js
@@ -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.text(`${data.Count}`);
+ await updateNotificationTable();
+ } catch (error) {
+ console.error(error, event);
+ }
+}
+
+export async function initNotificationCount() {
const notificationCount = $('.notification_count');
if (!notificationCount.length) {
@@ -26,36 +44,57 @@ export function initNotificationCount() {
}
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 {
- 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) {
diff --git a/web_src/js/index.js b/web_src/js/index.js
index 0cfba4c66a..15ca32cfdc 100644
--- a/web_src/js/index.js
+++ b/web_src/js/index.js
@@ -2432,7 +2432,6 @@ $(document).ready(async () => {
initContextPopups();
initTableSort();
initNotificationsTable();
- initNotificationCount();
// Repo clone url.
if ($('#repo-clone-url').length > 0) {
@@ -2477,6 +2476,7 @@ $(document).ready(async () => {
initClipboard(),
initUserHeatmap(),
initServiceWorker(),
+ initNotificationCount(),
]);
});
diff --git a/webpack.config.js b/webpack.config.js
index 2ed374ba49..72d83522d2 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -52,6 +52,9 @@ module.exports = {
serviceworker: [
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'),
...glob('assets/svg/*.svg'),