Skip to content

Commit

Permalink
Debug/websocket disconnect (#126)
Browse files Browse the repository at this point in the history
* style: fix broken match header

* fix: some refetching not working

* fix: match view not displaying data

* fix: empty match

* style: player match item

* debug: log failed requests in debug logs

* fix: misleading websocket state indicator

* fix: reconnect websocket on close
  • Loading branch information
LinusBolls authored Nov 25, 2024
1 parent 90efdbd commit aa73888
Show file tree
Hide file tree
Showing 15 changed files with 316 additions and 49 deletions.
15 changes: 12 additions & 3 deletions mobile-app/api/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ export class ProfileImpl {

export class TeamMemberImpl {
public id: string;
public team: 'red' | 'blue';
public team!: 'red' | 'blue';

public get name(): string {
return this.player!.profile!.name;
Expand Down Expand Up @@ -111,9 +111,12 @@ export class TeamMemberImpl {
return this.player.profile.avatarUrl;
}

public setTeamColor(color: 'red' | 'blue'): void {
this.team = color;
}

constructor(_data: Components.Schemas.TeamMemberDto) {
this.id = _data.id!;
this.team = _data.teamId as 'red' | 'blue';

this.playerId = _data.playerId!;
}
Expand All @@ -128,7 +131,7 @@ export class TeamMemberImpl {
}
public toJSON(): TeamMember {
return {
id: this.id,
id: this.playerId,
change: this.change,
moves: this.moves.map((i) => i.toJSON()),
name: this.name,
Expand Down Expand Up @@ -259,6 +262,12 @@ export class MatchImpl {
);
}
}
for (const player of this._blueTeam.members) {
player.setTeamColor('blue');
}
for (const player of this._redTeam.members) {
player.setTeamColor('red');
}
}
public get redCups(): number {
return this._redTeam.points!;
Expand Down
4 changes: 2 additions & 2 deletions mobile-app/api/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,8 +37,8 @@ const getDayName = (date: Dayjs) => {
};

export const env = {
apiBaseUrl: process.env.EXPO_PUBLIC_API_BASE_URL, //assertEnvString('EXPO_PUBLIC_API_BASE_URL'),
realtimeBaseUrl: process.env.EXPO_PUBLIC_API_WS_URL, //assertEnvString('EXPO_PUBLIC_API_WS_URL')!,
apiBaseUrl: process.env.EXPO_PUBLIC_API_BASE_URL!, //assertEnvString('EXPO_PUBLIC_API_BASE_URL'),
realtimeBaseUrl: process.env.EXPO_PUBLIC_API_WS_URL!, //assertEnvString('EXPO_PUBLIC_API_WS_URL')!,

groupCode: {
format: groupCodeFormat,
Expand Down
4 changes: 4 additions & 0 deletions mobile-app/api/realtime/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ export class RealtimeClient {

this.ws.addEventListener('close', () => {
this.logger.info('connection closed');
this.connect();
});

this.ws.addEventListener('error', (e) => {
Expand Down Expand Up @@ -137,4 +138,7 @@ export class RealtimeClient {
this.registerHandler('*', handler);
},
};
public get isOpen(): boolean {
return this.ws.OPEN === 1;
}
}
17 changes: 6 additions & 11 deletions mobile-app/api/realtime/useRealtimeConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,7 @@ import { Logs } from '@/utils/logging';
import { useLogging } from '@/utils/useLogging';
import { useGroupStore } from '@/zustand/group/stateGroupStore';

import {
RealtimeAffectedEntity,
RealtimeClient,
RealtimeEvent,
RealtimeEventHandler,
} from '.';
import { RealtimeClient, RealtimeEventHandler } from '.';
import { env } from '../env';
import { ignoreSeason, QK } from '../utils/reactQuery';

Expand Down Expand Up @@ -39,7 +34,7 @@ export function useRealtimeConnection() {

// refetch because of PlayerDto.statistics.matches
qc.invalidateQueries({
queryKey: [QK.group, e.groupId, QK.players],
predicate: ignoreSeason([QK.group, e.groupId, QK.players]),
});

client.current.logger.info('refetching matches');
Expand All @@ -54,18 +49,18 @@ export function useRealtimeConnection() {

// refetch because a newly created season will have new players
qc.invalidateQueries({
queryKey: [QK.group, e.groupId, QK.players],
predicate: ignoreSeason([QK.group, e.groupId, QK.players]),
});

// refetch because a newly created season will have no matches
qc.invalidateQueries({
queryKey: [QK.group, e.groupId, QK.matches],
predicate: ignoreSeason([QK.group, e.groupId, QK.matches]),
});

client.current.logger.info('refetching seasons');

qc.invalidateQueries({
queryKey: [QK.group, e.groupId, QK.seasons],
predicate: ignoreSeason([QK.group, e.groupId, QK.seasons]),
});
break;
case 'PLAYERS':
Expand All @@ -74,7 +69,7 @@ export function useRealtimeConnection() {

// TODO: only refetch matches on player delete
qc.invalidateQueries({
queryKey: [QK.group, e.groupId, QK.matches],
predicate: ignoreSeason([QK.group, e.groupId, QK.matches]),
});

client.current.logger.info('refetching players');
Expand Down
51 changes: 51 additions & 0 deletions mobile-app/api/utils/create-api.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import * as Sentry from '@sentry/react-native';
import OpenAPIClientAxios, { Document } from 'openapi-client-axios';
import React, {
createContext,
Expand All @@ -7,6 +8,8 @@ import React, {
useState,
} from 'react';

import { useLogging } from '@/utils/useLogging';

import beerpongDefinition from '../../api/generated/openapi.json';
import { Client as BeerPongClient } from '../../openapi/openapi';
import { env } from '../env';
Expand All @@ -33,6 +36,7 @@ export function ApiProvider({ children }: { children: ReactNode }) {
const api = openApi.getClient<BeerPongClient>();

const realtime = useRealtimeConnection();
const { writeLog } = useLogging();

const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
Expand All @@ -41,6 +45,53 @@ export function ApiProvider({ children }: { children: ReactNode }) {
const initializeApi = async () => {
try {
await openApi.init();

const awaitedApi = await api;

awaitedApi.interceptors.response.use(
(res) => res,
(err) => {
if (err.response) {
writeLog(
'[api] request failed:',
err.config?.method,
err.config?.url,
err.response.status,
err.response.data
);
Sentry.captureException(err, {
extra: {
url: err.config?.url,
method: err.config?.method,
status: err.response.status,
statusText: err.response.statusText,
responseData: err.response.data,
},
});
} else if (err.request) {
writeLog(
'[api] no response received:',
err.config?.method,
err.config?.url
);
Sentry.captureException(err, {
extra: {
url: err.config?.url,
method: err.config?.method,
request: err.request,
},
});
} else {
writeLog('[api] setup error:', err.message);
Sentry.captureException(err, {
extra: {
message: err.message,
},
});
}
return Promise.reject(err);
}
);
} catch (err) {
setError(
err instanceof Error
Expand Down
21 changes: 20 additions & 1 deletion mobile-app/app/debugLog.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
import dayjs from 'dayjs';
import { Stack } from 'expo-router';
import React, { useEffect, useState } from 'react';
import { ScrollView } from 'react-native';
import { GestureHandlerRootView } from 'react-native-gesture-handler';

import { env } from '@/api/env';
import { useApi } from '@/api/utils/create-api';
import { navStyles } from '@/app/navigation/navStyles';
import { Heading } from '@/components/Menu/MenuSection';
import Text from '@/components/Text';
Expand All @@ -28,6 +30,16 @@ function stringifyLogs(logs: Logs): string {
export default function Page() {
const { logs } = useLogging();

const [isRealtimeOpen, setIsRealtimeOpen] = useState(false);

const { realtime } = useApi();

useEffect(() => {
// we need to keep this in state because `realtime` is a ref and will not cause a rerender if it changes,
// so the indicator could be misleading
setIsRealtimeOpen(realtime.isOpen);
}, [realtime.isOpen]);

return (
<GestureHandlerRootView>
<Stack.Screen
Expand All @@ -48,7 +60,14 @@ export default function Page() {
paddingBottom: 128,
}}
>
<Heading title="Debug Logs" />
<Heading
title={
<>
Debug Logs{' '}
{env.isDev ? (isRealtimeOpen ? '✅' : '❌') : ''}
</>
}
/>
{logs.map((i, idx) => (
<Text color="primary" key={idx} style={{ fontSize: 12 }}>
<Text color="secondary" style={{ fontSize: 12 }}>
Expand Down
61 changes: 35 additions & 26 deletions mobile-app/components/MatchPlayers/Player.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,35 @@ import {
} from 'react-native';
import Icon from 'react-native-vector-icons/MaterialCommunityIcons';

import { env } from '@/api/env';
import { TeamMember } from '@/api/utils/matchDtoToMatch';
import { useNavigation } from '@/app/navigation/useNavigation';
import Avatar from '@/components/Avatar';
import { theme } from '@/theme';

import Text from '../Text';

function Change({ value }: { value: number }) {
return (
<>
<Icon
color={value >= 0 ? theme.color.positive : theme.color.negative}
size={8}
name="triangle"
style={{
marginLeft: 8,
marginRight: 2,
marginTop: 1,
transform: value >= 0 ? undefined : [{ rotateX: '180deg' }],
}}
/>
<Text variant="body2" color={value >= 0 ? 'positive' : 'negative'}>
{Math.abs(value)}
</Text>
</>
);
}

export interface PlayerProps {
player: TeamMember;

Expand Down Expand Up @@ -48,7 +70,7 @@ export default function Player({
// Interpolate the animated value to control height
const contentHeight = animation.interpolate({
inputRange: [0, 1],
outputRange: [0, 44 * 8], // customize the height range based on your content
outputRange: [0, 44 * moves.length], // customize the height range based on your content
});

const nav = useNavigation();
Expand Down Expand Up @@ -94,37 +116,24 @@ export default function Player({
{points} points
</Text>
) : (
<Text variant="body2" color="tertiary">
<Text
variant="body2"
color="tertiary"
style={{
fontStyle:
moves.length < 1
? 'italic'
: undefined,
}}
>
{moves.length < 1 && 'No moves'}
{moves
.filter((i) => i.count > 0)
.map((i) => i.count + ' ' + i.title)
.join(', ')}
</Text>
)}
<Icon
color={
change >= 0
? theme.color.positive
: theme.color.negative
}
size={8}
name="triangle"
style={{
marginLeft: 8,
marginRight: 2,
marginTop: 1,
transform:
change >= 0
? undefined
: [{ rotateX: '180deg' }],
}}
/>
<Text
variant="body2"
color={change >= 0 ? 'positive' : 'negative'}
>
{Math.abs(change)}
</Text>
{env.isDev && <Change value={change} />}
</View>
</View>
{editable ? (
Expand Down
18 changes: 14 additions & 4 deletions mobile-app/components/MatchVsHeader.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,12 @@ export default function MatchVsHeader({
{Array(Math.max(MAX_ITEMS - match.blueTeam.length, 0))
.fill(null)
.map((_, index) => {
return <Avatar key={index} style={{ opacity: 0 }} />;
return (
<Avatar
key={index}
style={{ opacity: 0, marginLeft: -16 }}
/>
);
})}
{match.blueTeam.slice(0, MAX_ITEMS).map((i, index) => (
<Avatar
Expand All @@ -59,7 +64,7 @@ export default function MatchVsHeader({
}
name={i.name}
borderColor={theme.color.team.blue}
style={{ marginLeft: index === 0 ? 0 : -16 }}
style={{ marginLeft: -16 }}
/>
))}
</View>
Expand Down Expand Up @@ -110,13 +115,18 @@ export default function MatchVsHeader({
}
name={i.name}
borderColor={theme.color.team.red}
style={{ marginLeft: index === 0 ? 0 : -16 }}
style={{ marginRight: -16 }}
/>
))}
{Array(Math.max(MAX_ITEMS - match.redTeam.length, 0))
.fill(null)
.map((_, index) => {
return <Avatar key={index} style={{ opacity: 0 }} />;
return (
<Avatar
key={index}
style={{ opacity: 0, marginRight: -16 }}
/>
);
})}
</View>
</View>
Expand Down
2 changes: 1 addition & 1 deletion mobile-app/components/Menu/MenuSection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ export function Heading({
}

export interface MenuSectionProps extends PropsWithChildren {
title?: string;
title?: JSX.Element | string;
titleHeadIcon?: JSX.Element;

background?: boolean;
Expand Down
Loading

0 comments on commit aa73888

Please sign in to comment.