Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move EventSource to SharedWorker #12095

Merged
merged 22 commits into from
Jul 3, 2020
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -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.
zeripath marked this conversation as resolved.
Show resolved Hide resolved
EVENT_SOURCE_UPDATE_TIME = 10s

[markdown]
Expand Down
3 changes: 1 addition & 2 deletions docs/content/doc/advanced/config-cheat-sheet.en-us.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` `SharedWorker`.
zeripath marked this conversation as resolved.
Show resolved Hide resolved
zeripath marked this conversation as resolved.
Show resolved Hide resolved

## Markdown (`markdown`)

Expand Down
3 changes: 3 additions & 0 deletions modules/eventsource/manager_run.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
4 changes: 2 additions & 2 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,8 +282,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),
Expand Down
151 changes: 151 additions & 0 deletions web_src/js/features/eventsource.sharedworker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
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) {
const portIdx = this.clients.indexOf(port);
if (portIdx > -1) {
return;
}
zeripath marked this conversation as resolved.
Show resolved Hide resolved
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;
}
zeripath marked this conversation as resolved.
Show resolved Hide resolved
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
}, false);
zeripath marked this conversation as resolved.
Show resolved Hide resolved
});
}

notifyClients(event) {
const len = this.clients.length;
for (let i = 0; i < len; i++) {
const port = this.clients[i];
port.postMessage(event);
}
zeripath marked this conversation as resolved.
Show resolved Hide resolved
}

status(port) {
port.postMessage({
type: 'status',
message: `url: ${this.url} readyState: ${this.eventSource.readyState}`,
});
}
}

self.onconnect = (e) => {
e.ports.forEach((port) => {
zeripath marked this conversation as resolved.
Show resolved Hide resolved
port.addEventListener('message', (event) => {
switch (event.data.type) {
case 'start': {
const url = event.data.url;
if (sourcesByUrl[url]) {
// we have a Source registered to this url
zeripath marked this conversation as resolved.
Show resolved Hide resolved
const source = sourcesByUrl[url];
source.register(port);
sourcesByPort[port] = source;
return;
}
let source = sourcesByPort[port];
if (source) {
if (source.eventSource && source.url === url) {
// We have a valid source for this port...
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;
}
case 'listen': {
const source = sourcesByPort[port];
source.listen(event.data.eventType);
}
return;
zeripath marked this conversation as resolved.
Show resolved Hide resolved
case '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;
}
case 'status': {
const source = sourcesByPort[port];
if (!source) {
port.postMessage({
type: 'status',
message: 'not connected',
});
return;
}
source.status(port);
return;
}
default:
// just send it back
port.postMessage({
type: 'error',
message: `received but don't know how to handle: ${event.data}`,
});
return;
}
});
port.start();
});
zeripath marked this conversation as resolved.
Show resolved Hide resolved
};
29 changes: 29 additions & 0 deletions web_src/js/features/eventsource.worker.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
let eventSource;

const listening = {};

self.addEventListener('message', (event) => {
if (event.data.type === 'start') {
eventSource = new EventSource(event.data.url);
listen('open');
listen('error');
listen('notification-count');
this.listen('logout');
} else if (event.data.type === 'listen') {
listen(event.data.eventType);
} else if (event.data.type === 'close' && eventSource) {
eventSource.close();
eventSource = null;
}
}, false);

function listen (eventType) {
if (listening[eventType]) return;
listening[eventType] = true;
eventSource.addEventListener(eventType, (event) => {
self.postMessage({
type: eventType,
data: event.data
}, false);
});
}
100 changes: 71 additions & 29 deletions web_src/js/features/notification.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,25 @@ export function initNotificationsTable() {
});
}

export function initNotificationCount() {
async function receiveUpdateCount(event) {
try {
const data = JSON.parse(event.data);

const notificationCount = $('.notification_count');
if (data.Count === 0) {
notificationCount.addClass('hidden');
} else {
notificationCount.removeClass('hidden');
}
zeripath marked this conversation as resolved.
Show resolved Hide resolved

notificationCount.text(`${data.Count}`);
await updateNotificationTable();
} catch (error) {
console.error(error, event);
}
}

export async function initNotificationCount() {
const notificationCount = $('.notification_count');

if (!notificationCount.length) {
Expand All @@ -27,35 +45,59 @@ 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');
} else {
notificationCount.removeClass('hidden');
}

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;
if (window.SharedWorker) {
const worker = new SharedWorker(`${__webpack_public_path__}js/eventsource.sharedworker.js`, 'notification-worker');
worker.addEventListener('error', (event) => {
console.error(event);
}, false);
zeripath marked this conversation as resolved.
Show resolved Hide resolved
worker.port.onmessageeerror = (event) => {
console.error(event);
};
zeripath marked this conversation as resolved.
Show resolved Hide resolved
worker.port.postMessage({
type: 'start',
url: `${window.location.protocol}//${window.location.host}${AppSubUrl}/user/events`,
zeripath marked this conversation as resolved.
Show resolved Hide resolved
});
worker.port.addEventListener('message', (e) => {
if (!e.data || !e.data.type) {
console.error(e);
return;
}
switch (event.data.type) {
case 'notification-count':
receiveUpdateCount(e.data);
return;
case 'error':
console.error(e.data);
return;
case 'logout': {
if (e.data !== 'here') {
return;
}
worker.port.postMessage({
type: 'close',
});
worker.port.close();
window.location.href = AppSubUrl;
return;
}
default:
return;
}
}, false);
zeripath marked this conversation as resolved.
Show resolved Hide resolved
worker.port.addEventListener('error', (e) => {
console.error(e);
}, false);
zeripath marked this conversation as resolved.
Show resolved Hide resolved
worker.port.start();
window.addEventListener('beforeunload', () => {
worker.port.postMessage({
type: 'close',
});
worker.port.close();
}, false);
zeripath marked this conversation as resolved.
Show resolved Hide resolved

return;
}
}

if (NotificationSettings.MinTimeout <= 0) {
Expand Down
2 changes: 1 addition & 1 deletion web_src/js/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2445,7 +2445,6 @@ $(document).ready(async () => {
initContextPopups();
initTableSort();
initNotificationsTable();
initNotificationCount();

// Repo clone url.
if ($('#repo-clone-url').length > 0) {
Expand Down Expand Up @@ -2491,6 +2490,7 @@ $(document).ready(async () => {
initClipboard(),
initUserHeatmap(),
initServiceWorker(),
initNotificationCount(),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change is not strictly necessary but as it is technically async this seems reasonable to do so.

]);
});

Expand Down
3 changes: 3 additions & 0 deletions webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,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'),
Expand Down