|
| 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