Skip to content

Commit

Permalink
Ensure that only one tab or window is open per browser (#1700)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
belcherj authored Nov 11, 2019
1 parent a88fe4e commit bcb6d22
Show file tree
Hide file tree
Showing 5 changed files with 98 additions and 0 deletions.
1 change: 1 addition & 0 deletions RELEASE-NOTES.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
### Fixes

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

### Other Changes

Expand Down
1 change: 1 addition & 0 deletions lib/boot.js
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import './utils/ensure-single-browser-tab-only';
import 'core-js/stable';
import 'regenerator-runtime/runtime';
import 'unorm';
Expand Down
15 changes: 15 additions & 0 deletions lib/components/boot-warning/index.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import React from 'react';
import PropTypes from 'prop-types';

import './style';

const BootWarning = () => {
return (
<h3 className="boot-warning__message">
Simplenote cannot be opened simultaneously in more than one tab or window
per browser.
</h3>
);
};

export default BootWarning;
4 changes: 4 additions & 0 deletions lib/components/boot-warning/style.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
h3.boot-warning__message {
margin: 50px auto;
width: 50%;
}
77 changes: 77 additions & 0 deletions lib/utils/ensure-single-browser-tab-only.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import React from 'react';
import ReactDOM from 'react-dom';

import BootWarning from '../components/boot-warning';

const HEARTBEAT_DELAY = 1000;
const clientId = uuidv4();
const emptyLock = [null, -Infinity];
const foundElectron = window.process && window.process.type;

if (!foundElectron) {
if ('lock-acquired' !== grabSessionLock()) {
ReactDOM.render(<BootWarning />, document.getElementById('root'));
throw new Error('Simplenote can only be opened in one tab');
}
let keepGoing = true;
loop(() => {
if (!keepGoing) {
return false;
}
switch (grabSessionLock()) {
case 'lock-acquired':
return true; // keep updating the lock and look for other sessions which may have taken it

default:
window.alert(
"We've detected another session running Simplenote, this may cause problems while editing notes. Please refresh the page."
);
return false; // stop watching - the user can proceed at their own risk
}
});

window.addEventListener('beforeunload', function() {
keepGoing = false;
const [lastClient] =
JSON.parse(localStorage.getItem('session-lock')) || emptyLock;

lastClient === clientId && localStorage.removeItem('session-lock');
});
}

function uuidv4() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = (Math.random() * 16) | 0,
v = c == 'x' ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}

function loop(f, delay = HEARTBEAT_DELAY) {
f() && setTimeout(() => loop(f, delay), delay);
}

function grabSessionLock() {
const [lastClient, lastBeat] =
JSON.parse(localStorage.getItem('session-lock')) || emptyLock;
const now = Date.now();
// easy case - someone else clearly has the lock
// add some hysteresis to prevent fighting between sessions
if (lastClient !== clientId && now - lastBeat < HEARTBEAT_DELAY * 5) {
return 'lock-unavailable';
}

// maybe nobody clearly has the lock, let's try and set it
localStorage.setItem('session-lock', JSON.stringify([clientId, now]));

// hard case - localStorage is shared mutable state across sessions
const [thisClient, thisBeat] =
JSON.parse(localStorage.getItem('session-lock')) || emptyLock;

// someone else set localStorage between the previous two lines of code
if (!(thisClient === clientId && thisBeat === now)) {
return 'lock-unavailable';
}

return 'lock-acquired';
}

0 comments on commit bcb6d22

Please sign in to comment.