1const sourcesByUrl = {};
2const sourcesByPort = {};
3
4class Source {
5 constructor(url) {
6 this.url = url;
7 this.eventSource = new EventSource(url);
8 this.listening = {};
9 this.clients = [];
10 this.listen('open');
11 this.listen('close');
12 this.listen('logout');
13 this.listen('notification-count');
14 this.listen('stopwatches');
15 this.listen('error');
16 }
17
18 register(port) {
19 if (this.clients.includes(port)) return;
20
21 this.clients.push(port);
22
23 port.postMessage({
24 type: 'status',
25 message: `registered to ${this.url}`,
26 });
27 }
28
29 deregister(port) {
30 const portIdx = this.clients.indexOf(port);
31 if (portIdx === -1) {
32 return this.clients.length;
33 }
34 this.clients.splice(portIdx, 1);
35 return this.clients.length;
36 }
37
38 close() {
39 if (!this.eventSource) return;
40
41 this.eventSource.close();
42 this.eventSource = null;
43 }
44
45 listen(eventType) {
46 if (this.listening[eventType]) return;
47 this.listening[eventType] = true;
48 this.eventSource.addEventListener(eventType, (event) => {
49 this.notifyClients({
50 type: eventType,
51 data: event.data,
52 });
53 });
54 }
55
56 notifyClients(event) {
57 for (const client of this.clients) {
58 client.postMessage(event);
59 }
60 }
61
62 status(port) {
63 port.postMessage({
64 type: 'status',
65 message: `url: ${this.url} readyState: ${this.eventSource.readyState}`,
66 });
67 }
68}
69
70self.addEventListener('connect', (e) => {
71 for (const port of e.ports) {
72 port.addEventListener('message', (event) => {
73 if (!self.EventSource) {
74 // some browsers (like PaleMoon, Firefox<53) don't support EventSource in SharedWorkerGlobalScope.
75 // this event handler needs EventSource when doing "new Source(url)", so just post a message back to the caller,
76 // in case the caller would like to use a fallback method to do its work.
77 port.postMessage({type: 'no-event-source'});
78 return;
79 }
80 if (event.data.type === 'start') {
81 const url = event.data.url;
82 if (sourcesByUrl[url]) {
83 // we have a Source registered to this url
84 const source = sourcesByUrl[url];
85 source.register(port);
86 sourcesByPort[port] = source;
87 return;
88 }
89 let source = sourcesByPort[port];
90 if (source) {
91 if (source.eventSource && source.url === url) return;
92
93 // How this has happened I don't understand...
94 // deregister from that source
95 const count = source.deregister(port);
96 // Clean-up
97 if (count === 0) {
98 source.close();
99 sourcesByUrl[source.url] = null;
100 }
101 }
102 // Create a new Source
103 source = new Source(url);
104 source.register(port);
105 sourcesByUrl[url] = source;
106 sourcesByPort[port] = source;
107 } else if (event.data.type === 'listen') {
108 const source = sourcesByPort[port];
109 source.listen(event.data.eventType);
110 } else if (event.data.type === 'close') {
111 const source = sourcesByPort[port];
112
113 if (!source) return;
114
115 const count = source.deregister(port);
116 if (count === 0) {
117 source.close();
118 sourcesByUrl[source.url] = null;
119 sourcesByPort[port] = null;
120 }
121 } else if (event.data.type === 'status') {
122 const source = sourcesByPort[port];
123 if (!source) {
124 port.postMessage({
125 type: 'status',
126 message: 'not connected',
127 });
128 return;
129 }
130 source.status(port);
131 } else {
132 // just send it back
133 port.postMessage({
134 type: 'error',
135 message: `received but don't know how to handle: ${event.data}`,
136 });
137 }
138 });
139 port.start();
140 }
141});