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 2 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
3 changes: 3 additions & 0 deletions custom/conf/app.example.ini
Original file line number Diff line number Diff line change
Expand Up @@ -220,6 +220,9 @@ 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.
EVENT_SOURCE_UPDATE_TIME = 10s
USE_SHARED_WORKER=true ; use a eventsource in shared worker
USE_WORKER=false ; use eventsource in worker
USE_PLAIN_EVENT_SOURCE=false ; allow direct use of event source outside of worker
zeripath marked this conversation as resolved.
Show resolved Hide resolved

[markdown]
; Render soft line breaks as hard line breaks, which means a single newline character between
Expand Down
6 changes: 4 additions & 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,10 @@ 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`, it will be used in preference to polling notification endpoint. Set to **-1** to disable the `EventSource`.
- `USE_SHARED_WORKER`: **true**: If the client supports `SharedWorker` and the `EVENT_SOURCE_UPDATE_TIME` is greater than `0`. Use a `SharedWorker` `EventSource` in preference to polling.
- `USE_WORKER`: **false**: If the client supports `Worker` and the `EVENT_SOURCE_UPDATE_TIME` is greater than `0`. Use a `Worker` `EventSource` in preference to polling. (Disabled by default due to limited browser connections under HTTP/1.1.)
- `USE_PLAIN_EVENT_SOURCE`: **false**: If the client supports `EventSource` and the `EVENT_SOURCE_UPDATE_TIME` is greater than `0`. Use a `EventSource` in preference to polling. (Disabled by default due to limited browser connections under HTTP/1.1.)

## 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
9 changes: 9 additions & 0 deletions modules/setting/setting.go
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,9 @@ var (
TimeoutStep time.Duration
MaxTimeout time.Duration
EventSourceUpdateTime time.Duration
UseSharedWorker bool
UseWorker bool
UsePlainEventSource bool
} `ini:"ui.notification"`

Admin struct {
Expand Down Expand Up @@ -220,11 +223,17 @@ var (
TimeoutStep time.Duration
MaxTimeout time.Duration
EventSourceUpdateTime time.Duration
UseSharedWorker bool
UseWorker bool
UsePlainEventSource bool
}{
MinTimeout: 10 * time.Second,
TimeoutStep: 10 * time.Second,
MaxTimeout: 60 * time.Second,
EventSourceUpdateTime: 10 * time.Second,
UseSharedWorker: true,
UseWorker: false,
UsePlainEventSource: false,
},
Admin: struct {
UserPagingNum int
Expand Down
7 changes: 5 additions & 2 deletions modules/templates/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,12 +282,15 @@ 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),
"EventSourceUpdateTime": int(setting.UI.Notification.EventSourceUpdateTime / time.Millisecond),
"UseSharedWorker": setting.UI.Notification.UseSharedWorker,
"UseWorker": setting.UI.Notification.UseWorker,
"UsePlainEventSource": setting.UI.Notification.UsePlainEventSource,
}
},
"contain": func(s []int64, id int64) bool {
Expand Down
3 changes: 3 additions & 0 deletions templates/base/head.tmpl
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,9 @@
TimeoutStep: {{NotificationSettings.TimeoutStep}},
MaxTimeout: {{NotificationSettings.MaxTimeout}},
EventSourceUpdateTime: {{NotificationSettings.EventSourceUpdateTime}},
UseSharedWorker: {{NotificationSettings.UseSharedWorker}},
UseWorker: {{NotificationSettings.UseWorker}},
UsePlainEventSource: {{NotificationSettings.UsePlainEventSource}},
},
{{if .RequireTribute}}
tributeValues: [
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);
});
}
Loading