Skip to content

Commit bcb6d22

Browse files
authored
Ensure that only one tab or window is open per browser (#1700)
When two tabs are open a bug occurs that changes will repeat infinitely. This is caused by each tab or window having it's own web socket while sharing the same instace of indexedDB. When a change happens in one tab it gets made in the second tab via the web socket and indexedDB.
1 parent a88fe4e commit bcb6d22

File tree

5 files changed

+98
-0
lines changed

5 files changed

+98
-0
lines changed

RELEASE-NOTES.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
### Fixes
88

99
- Fixed markdown code styles [#1702](https://github.com/Automattic/simplenote-electron/pull/1702)
10+
- Only allow app to load in one instance per browser [#1700](https://github.com/Automattic/simplenote-electron/pull/1700)
1011

1112
### Other Changes
1213

lib/boot.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import './utils/ensure-single-browser-tab-only';
12
import 'core-js/stable';
23
import 'regenerator-runtime/runtime';
34
import 'unorm';
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import React from 'react';
2+
import PropTypes from 'prop-types';
3+
4+
import './style';
5+
6+
const BootWarning = () => {
7+
return (
8+
<h3 className="boot-warning__message">
9+
Simplenote cannot be opened simultaneously in more than one tab or window
10+
per browser.
11+
</h3>
12+
);
13+
};
14+
15+
export default BootWarning;
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
h3.boot-warning__message {
2+
margin: 50px auto;
3+
width: 50%;
4+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import React from 'react';
2+
import ReactDOM from 'react-dom';
3+
4+
import BootWarning from '../components/boot-warning';
5+
6+
const HEARTBEAT_DELAY = 1000;
7+
const clientId = uuidv4();
8+
const emptyLock = [null, -Infinity];
9+
const foundElectron = window.process && window.process.type;
10+
11+
if (!foundElectron) {
12+
if ('lock-acquired' !== grabSessionLock()) {
13+
ReactDOM.render(<BootWarning />, document.getElementById('root'));
14+
throw new Error('Simplenote can only be opened in one tab');
15+
}
16+
let keepGoing = true;
17+
loop(() => {
18+
if (!keepGoing) {
19+
return false;
20+
}
21+
switch (grabSessionLock()) {
22+
case 'lock-acquired':
23+
return true; // keep updating the lock and look for other sessions which may have taken it
24+
25+
default:
26+
window.alert(
27+
"We've detected another session running Simplenote, this may cause problems while editing notes. Please refresh the page."
28+
);
29+
return false; // stop watching - the user can proceed at their own risk
30+
}
31+
});
32+
33+
window.addEventListener('beforeunload', function() {
34+
keepGoing = false;
35+
const [lastClient] =
36+
JSON.parse(localStorage.getItem('session-lock')) || emptyLock;
37+
38+
lastClient === clientId && localStorage.removeItem('session-lock');
39+
});
40+
}
41+
42+
function uuidv4() {
43+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
44+
var r = (Math.random() * 16) | 0,
45+
v = c == 'x' ? r : (r & 0x3) | 0x8;
46+
return v.toString(16);
47+
});
48+
}
49+
50+
function loop(f, delay = HEARTBEAT_DELAY) {
51+
f() && setTimeout(() => loop(f, delay), delay);
52+
}
53+
54+
function grabSessionLock() {
55+
const [lastClient, lastBeat] =
56+
JSON.parse(localStorage.getItem('session-lock')) || emptyLock;
57+
const now = Date.now();
58+
// easy case - someone else clearly has the lock
59+
// add some hysteresis to prevent fighting between sessions
60+
if (lastClient !== clientId && now - lastBeat < HEARTBEAT_DELAY * 5) {
61+
return 'lock-unavailable';
62+
}
63+
64+
// maybe nobody clearly has the lock, let's try and set it
65+
localStorage.setItem('session-lock', JSON.stringify([clientId, now]));
66+
67+
// hard case - localStorage is shared mutable state across sessions
68+
const [thisClient, thisBeat] =
69+
JSON.parse(localStorage.getItem('session-lock')) || emptyLock;
70+
71+
// someone else set localStorage between the previous two lines of code
72+
if (!(thisClient === clientId && thisBeat === now)) {
73+
return 'lock-unavailable';
74+
}
75+
76+
return 'lock-acquired';
77+
}

0 commit comments

Comments
 (0)