mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-31 02:46:04 +01:00 
			
		
		
		
	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>
This commit is contained in:
		| @@ -28,7 +28,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 | ||||||
|  |  | ||||||
|   | |||||||
| @@ -218,7 +218,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] | ||||||
|   | |||||||
| @@ -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. | - `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), | ||||||
|   | |||||||
							
								
								
									
										140
									
								
								web_src/js/features/eventsource.sharedworker.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										140
									
								
								web_src/js/features/eventsource.sharedworker.js
									
									
									
									
									
										Normal file
									
								
							| @@ -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(); | ||||||
|  |   } | ||||||
|  | }; | ||||||
| @@ -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'); |   const notificationCount = $('.notification_count'); | ||||||
|  |  | ||||||
|   if (!notificationCount.length) { |   if (!notificationCount.length) { | ||||||
| @@ -26,36 +44,57 @@ 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'); |       worker.port.onmessageerror = () => { | ||||||
|         if (data.Count === 0) { |         console.error('Unable to deserialize message'); | ||||||
|           notificationCount.addClass('hidden'); |       }; | ||||||
|         } else { |       worker.port.postMessage({ | ||||||
|           notificationCount.removeClass('hidden'); |         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 { | ||||||
|  |           return; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |       worker.port.addEventListener('error', (e) => { | ||||||
|  |         console.error(e); | ||||||
|  |       }); | ||||||
|  |       worker.port.start(); | ||||||
|  |       window.addEventListener('beforeunload', () => { | ||||||
|  |         worker.port.postMessage({ | ||||||
|  |           type: 'close', | ||||||
|  |         }); | ||||||
|  |         worker.port.close(); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|         notificationCount.text(`${data.Count}`); |       return; | ||||||
|         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; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   if (NotificationSettings.MinTimeout <= 0) { |   if (NotificationSettings.MinTimeout <= 0) { | ||||||
|   | |||||||
| @@ -2432,7 +2432,6 @@ $(document).ready(async () => { | |||||||
|   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) { | ||||||
| @@ -2477,6 +2476,7 @@ $(document).ready(async () => { | |||||||
|     initClipboard(), |     initClipboard(), | ||||||
|     initUserHeatmap(), |     initUserHeatmap(), | ||||||
|     initServiceWorker(), |     initServiceWorker(), | ||||||
|  |     initNotificationCount(), | ||||||
|   ]); |   ]); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -52,6 +52,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: [ |     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'), | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user