Skip to content

Commit 847ac34

Browse files
committed
Display multi-tab error message when attempting to host two sessions
When hosting two sessions, the browser continually attempts to reconnect to the signal server since only one instance can be the session owner. This prevents that issue from popping up with an informative error message.
1 parent 4793dbd commit 847ac34

File tree

5 files changed

+97
-4
lines changed

5 files changed

+97
-4
lines changed

packages/metastream-app/src/components/lobby/Disconnect.css

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
.info {
1212
composes: center-vertical from '~styles/layout.css';
1313
composes: select-text from '~styles/text.css';
14+
padding: 0 1rem;
15+
max-width: 600px;
1416
}
1517

1618
.info svg {
19+
flex-shrink: 0;
1720
margin-right: 0.5rem;
1821
}
1922

packages/metastream-app/src/constants/network.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ export const enum NetworkDisconnectReason {
3737
InvalidClientInfo,
3838
VersionMismatch,
3939
Full,
40-
Kicked
40+
Kicked,
41+
MultiTab
4142
}
4243

4344
export const NetworkDisconnectMessages = {
@@ -46,7 +47,8 @@ export const NetworkDisconnectMessages = {
4647
[NetworkDisconnectReason.InvalidClientInfo]: 'networkDisconnectInvalidClientInfo',
4748
[NetworkDisconnectReason.VersionMismatch]: `networkDisconnectVersionMismatch`,
4849
[NetworkDisconnectReason.Full]: 'networkDisconnectFull',
49-
[NetworkDisconnectReason.Kicked]: 'networkDisconnectKicked'
50+
[NetworkDisconnectReason.Kicked]: 'networkDisconnectKicked',
51+
[NetworkDisconnectReason.MultiTab]: 'networkDisconnectMultiTab'
5052
}
5153

5254
export const NetworkDisconnectLabels = {
@@ -55,5 +57,6 @@ export const NetworkDisconnectLabels = {
5557
[NetworkDisconnectReason.InvalidClientInfo]: 'invalid-client-info',
5658
[NetworkDisconnectReason.VersionMismatch]: `version-mismatch`,
5759
[NetworkDisconnectReason.Full]: 'full',
58-
[NetworkDisconnectReason.Kicked]: 'kicked'
60+
[NetworkDisconnectReason.Kicked]: 'kicked',
61+
[NetworkDisconnectReason.MultiTab]: 'multi-tab'
5962
}

packages/metastream-app/src/containers/LobbyPage.tsx

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import { resetLobby, initLobby } from '../lobby/actions/common'
2727
import { IReactReduxProps } from 'types/redux-thunk'
2828
import { NetworkError, NetworkErrorCode } from '../network/error'
2929
import { addChat } from '../lobby/actions/chat'
30+
import { MultiTabObserver } from '../utils/multitab'
3031

3132
interface IRouteParams {
3233
lobbyId: string
@@ -66,6 +67,7 @@ export class _LobbyPage extends Component<PrivateProps, IState> {
6667
private mounted: boolean = false
6768
private server?: NetServer
6869
private host: boolean
70+
private tabObserver?: MultiTabObserver
6971

7072
private get supportsNetworking() {
7173
return this.props.sessionMode !== SessionMode.Offline
@@ -81,10 +83,27 @@ export class _LobbyPage extends Component<PrivateProps, IState> {
8183
this.host = props.match.params.lobbyId === localUserId()
8284
}
8385

86+
private async checkIsMultiTab() {
87+
if (!this.tabObserver) {
88+
this.tabObserver = new MultiTabObserver()
89+
}
90+
91+
const isMultiTab = await this.tabObserver.getIsMultiTab()
92+
if (isMultiTab) {
93+
this.disconnect(NetworkDisconnectReason.MultiTab)
94+
return true
95+
}
96+
97+
return false
98+
}
99+
84100
private async setupLobby(): Promise<void> {
85101
let successPromise
86102

87103
if (this.host) {
104+
const isMultiTab = await this.checkIsMultiTab()
105+
if (isMultiTab) return
106+
88107
successPromise = PlatformService.createLobby({
89108
p2p: true,
90109
websocket: true
@@ -113,6 +132,11 @@ export class _LobbyPage extends Component<PrivateProps, IState> {
113132
this.server = undefined
114133
}
115134

135+
if (this.tabObserver) {
136+
this.tabObserver.destroy()
137+
this.tabObserver = undefined
138+
}
139+
116140
PlatformService.leaveLobby(this.lobbyId)
117141
this.props.dispatch(NetActions.disconnect({ host: this.host }))
118142
}
@@ -152,7 +176,7 @@ export class _LobbyPage extends Component<PrivateProps, IState> {
152176
) => {
153177
this.setState({ connected: false })
154178

155-
if (immediate || this.host) {
179+
if (immediate) {
156180
this.props.dispatch(push('/'))
157181
return
158182
}

packages/metastream-app/src/locale/en-US.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,8 @@ export default {
6565
networkDisconnectVersionMismatch: `Client version mismatch.`,
6666
networkDisconnectFull: 'Session is full.',
6767
networkDisconnectKicked: 'Kicked from session.',
68+
networkDisconnectMultiTab:
69+
'You are hosting a Metastream session from more than one browser or tab. Please close the other tabs or other browsers, then reload the page.',
6870
networkTroubleshootingHelp: 'See <1>Network Troubleshooting guide</1> for help.',
6971
noticeAddedMedia: 'Added <Media id="{{mediaId}}">{{mediaTitle}}</Media>',
7072
noticeMediaError: 'There was an error requesting {{url}}',
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import shortid from 'shortid'
2+
3+
export class MultiTabObserver {
4+
private channel?: BroadcastChannel
5+
private observerId = shortid()
6+
7+
constructor() {
8+
if ('BroadcastChannel' in window) {
9+
this.channel = new BroadcastChannel('multitab_observer')
10+
this.channel.addEventListener('message', this.onMessage)
11+
}
12+
}
13+
14+
destroy() {
15+
if (this.channel) {
16+
this.channel.close()
17+
}
18+
}
19+
20+
private onMessage = (event: MessageEvent) => {
21+
const action = event.data
22+
if (action.id === this.observerId) return
23+
switch (action.type) {
24+
case 'ping':
25+
this.channel!.postMessage({ type: 'pong', payload: this.observerId })
26+
break
27+
}
28+
}
29+
30+
getIsMultiTab() {
31+
if (!this.channel) return Promise.resolve(false)
32+
return new Promise(resolve => {
33+
let done = false
34+
let timeoutId: any
35+
36+
const channel = this.channel!
37+
channel.postMessage({ type: 'ping', id: this.observerId })
38+
39+
const cleanup = () => {
40+
if (timeoutId) clearTimeout(timeoutId)
41+
channel.removeEventListener('message', onMessage)
42+
done = true
43+
}
44+
45+
const onMessage = (event: MessageEvent) => {
46+
const action = event.data
47+
if (action.type === 'pong' && action.id !== this.observerId) {
48+
cleanup()
49+
resolve(true)
50+
}
51+
}
52+
channel.addEventListener('message', onMessage)
53+
54+
timeoutId = setTimeout(() => {
55+
timeoutId = undefined
56+
cleanup()
57+
resolve(false)
58+
}, 200)
59+
})
60+
}
61+
}

0 commit comments

Comments
 (0)