Skip to content

Commit

Permalink
fix(frontend): clean up webrtc connection properly to allow reconnect…
Browse files Browse the repository at this point in the history
…ion without a reload
  • Loading branch information
DecentM committed Oct 8, 2023
1 parent c9ab98c commit 7483afc
Show file tree
Hide file tree
Showing 7 changed files with 175 additions and 81 deletions.
76 changes: 72 additions & 4 deletions apps/frontend/src/hooks/chess-rtc-connection.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,14 @@
import { Ref, ref } from 'vue'
import * as Sentry from '@sentry/vue'

import { DataRtcMessage, RtcMessage, useRtcConnection } from './rtc-connection'
import {
DataRtcMessage,
ErrorRtcMessage,
RtcMessage,
useRtcConnection,
} from './rtc-connection'
import { useNotify } from './notify'
import { useRouter } from 'vue-router'

type ExecuteNodeIndexMessage = {
type: 'execute-node-index'
Expand All @@ -17,17 +24,20 @@ type ChessMessage = ExecuteNodeIndexMessage | SideAssignmentMessage

export type ChessRtcConnection = {
connect: (connectId: string) => void
mode: Ref<'initial' | 'client' | 'server'>
peerId: Ref<string | null>
sendMessage: (message: ChessMessage) => void
open: Ref<boolean>
disconnect: () => void

serverSide: Ref<'white' | 'black' | null>
moveHistory: Ref<number[]>
isHost: Ref<boolean>
}

export const useChessRtcConnection = (): ChessRtcConnection => {
const { notify } = useNotify()
const router = useRouter()

const sendMessage = (message: ChessMessage) => {
sendData(Buffer.from(JSON.stringify(message), 'utf8'))
}
Expand All @@ -38,6 +48,7 @@ export const useChessRtcConnection = (): ChessRtcConnection => {

const receiveOpenMessage = () => {
open.value = true
router.push(`/play/online/${peerId.value}`)
}

const receiveDataMessage = (rtcMessage: DataRtcMessage) => {
Expand Down Expand Up @@ -74,27 +85,84 @@ export const useChessRtcConnection = (): ChessRtcConnection => {
}
}

const receiveCloseMessage = () => {
notify({ message: 'Connection closed', icon: 'link_off' })
router.push('/play')
}

const receiveErrorMessage = (message: ErrorRtcMessage) => {
notify({
message: 'Connection error',
caption: message.value.message,
icon: 'link_off',
iconColor: 'red',
})
}

const receiveReadyMessage = () => {
notify({
message: 'WebRTC connection ready',
icon: 'check',
iconColor: 'green',
})
}

const receiveRejectedMessage = () => {
notify({
message: 'Refused secondary connection',
icon: 'link_off',
iconColor: 'red',
})
}

const receiveAcceptedMessage = () => {
notify({ message: 'Connected to peer', icon: 'link' })
router.push(`/play/online/${peerId.value}`)
}

const receiveClosedMessage = () => {
notify({ message: 'WebRTC connection closed', icon: 'link_off' })
}

const receiveMessage = (rtcMessage: RtcMessage) => {
switch (rtcMessage.type) {
case 'data':
return receiveDataMessage(rtcMessage)

case 'open':
return receiveOpenMessage()

case 'close':
return receiveCloseMessage()

case 'error':
return receiveErrorMessage(rtcMessage)

case 'connection-ready':
return receiveReadyMessage()

case 'connection-rejected':
return receiveRejectedMessage()

case 'connection-accepted':
return receiveAcceptedMessage()

case 'connection-closed':
return receiveClosedMessage()
}
}

const { connect, mode, peerId, sendData, disconnect } =
const { connect, peerId, sendData, disconnect, isHost } =
useRtcConnection(receiveMessage)

return {
connect,
mode,
peerId,
sendMessage,
open,
disconnect,
serverSide,
moveHistory,
isHost,
}
}
147 changes: 88 additions & 59 deletions apps/frontend/src/hooks/rtc-connection.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
import { DataConnection, PeerError, Peer } from 'peerjs'
import { onBeforeUnmount, onMounted, ref } from 'vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'

import { Hex } from '../lib/hex'
import { useNotify } from './notify'

type RtcMessageBase = {
value: unknown
Expand Down Expand Up @@ -34,19 +33,47 @@ export type OpenRtcMessage = RtcMessageBase & {
type: 'open'
}

export type CloseRtcMessage = RtcMessageBase & {
value: void
type: 'close'
}

export type ConnectionRejectedRtcMessage = RtcMessageBase & {
value: void
type: 'connection-rejected'
}

export type ConnectionAcceptedRtcMessage = RtcMessageBase & {
value: void
type: 'connection-accepted'
}

export type ConnectionReadyRtcMessage = RtcMessageBase & {
value: void
type: 'connection-ready'
}

export type ConnectionClosedRtcMessage = RtcMessageBase & {
value: void
type: 'connection-closed'
}

export type RtcMessage =
| DataRtcMessage
| ErrorRtcMessage
| StateRtcMessage
| OpenRtcMessage
| CloseRtcMessage
| ConnectionAcceptedRtcMessage
| ConnectionReadyRtcMessage
| ConnectionRejectedRtcMessage
| ConnectionClosedRtcMessage

export const useRtcConnection = (onMessage: (message: RtcMessage) => void) => {
const peer = ref<Peer | null>(null)
const { notify } = useNotify()

const peerId = ref<string | null>(null)
const connection = ref<DataConnection | null>()
const mode = ref<'initial' | 'client' | 'server'>('initial')

const attachEventListeners = (newConnection: DataConnection) => {
const handleData = (data: unknown) => {
Expand Down Expand Up @@ -88,26 +115,18 @@ export const useRtcConnection = (onMessage: (message: RtcMessage) => void) => {
onMessage({ type: 'error', value: error })

connection.value?.close()

notify({
message: 'Connection error',
caption: error.message,
icon: 'link_off',
iconColor: 'red',
})
}

const handleClose = () => {
connection.value?.on('data', handleData)
connection.value?.on('error', handleError)
connection.value?.on('close', handleClose)
connection.value?.on('iceStateChanged', handleStateChange)
connection.value?.on('open', handleOpen)
connection.value?.off('data', handleData)
connection.value?.off('error', handleError)
connection.value?.off('close', handleClose)
connection.value?.off('iceStateChanged', handleStateChange)
connection.value?.off('open', handleOpen)

mode.value = 'initial'
connection.value = null
onMessage({ type: 'close', value: undefined })

notify({ message: 'Connection closed', icon: 'link_off' })
connection.value = null
}

newConnection.on('data', handleData)
Expand All @@ -117,41 +136,6 @@ export const useRtcConnection = (onMessage: (message: RtcMessage) => void) => {
newConnection.on('open', handleOpen)
}

onMounted(() => {
peer.value = new Peer()

peer.value.on('open', (id) => {
peerId.value = Hex.utf8ToHex(id)

notify({ message: 'Connection ready', icon: 'check', iconColor: 'green' })
})

peer.value.on('connection', (newConnection) => {
// Someone might connect after we're already connected
if (connection.value) {
notify({
message: 'Refused secondary connection',
icon: 'link_off',
iconColor: 'red',
})
newConnection.close()
return
}

mode.value = 'server'
attachEventListeners(newConnection)

connection.value = newConnection

notify({ message: 'Connected to guest', icon: 'link' })
})
})

onBeforeUnmount(() => {
peer.value?.destroy()
connection.value?.close()
})

const connect = (connectId: string) => {
// Refuse to connect to a server if we're already connected
if (connection.value) {
Expand All @@ -163,14 +147,15 @@ export const useRtcConnection = (onMessage: (message: RtcMessage) => void) => {
}

const serverId = Hex.hexToUtf8(connectId)
const newConnection = peer.value.connect(serverId)
const newConnection = peer.value.connect(serverId, {
metadata: {
guestId: peerId.value,
},
})

mode.value = 'client'
attachEventListeners(newConnection)

connection.value = newConnection

notify({ message: 'Connected to host', icon: 'link' })
}

const sendData = (data: Buffer): boolean => {
Expand All @@ -196,11 +181,55 @@ export const useRtcConnection = (onMessage: (message: RtcMessage) => void) => {
return true
}

onMounted(() => {
peer.value = new Peer()

peer.value.on('open', (id) => {
peerId.value = Hex.utf8ToHex(id)

onMessage({ type: 'connection-ready', value: undefined })
})

peer.value.on('disconnected', () => {
peerId.value = null
connection.value = null

onMessage({ type: 'connection-closed', value: undefined })
})

peer.value.on('connection', (newConnection) => {
// Someone might connect after we're already connected
if (connection.value) {
onMessage({ type: 'connection-rejected', value: undefined })
newConnection.close()
return
}

attachEventListeners(newConnection)

connection.value = newConnection

onMessage({ type: 'connection-accepted', value: undefined })
})
})

onBeforeUnmount(() => {
connection.value?.close()
peer.value?.disconnect()

connection.value = null
peer.value = null
})

const isHost = computed(() => {
return connection.value?.metadata?.guestId !== peerId.value
})

return {
connect,
mode,
peerId,
sendData,
disconnect,
isHost,
}
}
12 changes: 1 addition & 11 deletions apps/frontend/src/layouts/play-layout.vue
Original file line number Diff line number Diff line change
@@ -1,26 +1,16 @@
<script setup lang="ts">
import { watch } from 'vue'
import { useRouter } from 'vue-router'
import { useChessRtcConnection } from '../hooks/chess-rtc-connection'
const router = useRouter()
const chessRtcConnection = useChessRtcConnection()
watch(chessRtcConnection.mode, (newValue) => {
if (newValue === 'initial') {
router.push('/play')
}
})
watch(chessRtcConnection.open, (newValue) => {
if (!newValue) {
return
}
router.push(`/play/online/${chessRtcConnection.peerId.value}`)
if (chessRtcConnection.mode.value === 'server') {
if (chessRtcConnection.isHost.value) {
chessRtcConnection.sendMessage({
type: 'side-assignment',
value: Math.random() > 0.5 ? 'white' : 'black',
Expand Down
9 changes: 7 additions & 2 deletions apps/frontend/src/pages/index-page.vue
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,13 @@ import ChessPiece from '../components/chess-piece.vue'
</div>

<div class="text-body1 q-mb-md">
<q-icon size="xl" class="col-2" name="arrow_left" />
Click "Play" on the left sidebar to begin, or read on for more.
<q-btn
class="full-width"
color="primary"
icon="sports_esports"
label="Click here to play, or read on for more"
to="/play"
/>
</div>
</q-card-section>
</q-card>
Expand Down
2 changes: 1 addition & 1 deletion apps/frontend/src/pages/play/online-game.vue
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const perspective = computed(() => {
return null
}
if (props.connection.mode.value === 'server') {
if (props.connection.isHost.value) {
return props.connection.serverSide.value
}
Expand Down
Loading

0 comments on commit 7483afc

Please sign in to comment.