fix(game-frontend): bound realtime dashboard refreshes
This commit is contained in:
@@ -6,8 +6,11 @@ import { trpc } from '../utils/trpc';
|
||||
import { useMapViewerStore } from './mapViewer';
|
||||
import { useSessionStore } from './session';
|
||||
import { createLatestRefreshQueue } from '../utils/latestRefreshQueue';
|
||||
import { createRateLimitedRefreshQueue } from '../utils/rateLimitedRefreshQueue';
|
||||
import { structurallyShare } from '../utils/structuralShare';
|
||||
|
||||
const REALTIME_FULL_REFRESH_MIN_INTERVAL_MS = 5_000;
|
||||
|
||||
const resolveErrorMessage = (value: unknown): string => {
|
||||
if (value instanceof Error) {
|
||||
return value.message;
|
||||
@@ -39,6 +42,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
const frontStatusError = ref<string | null>(null);
|
||||
const realtimeEnabled = ref(true);
|
||||
const realtimeStatus = ref<'idle' | 'connected' | 'paused'>('idle');
|
||||
const realtimeActive = ref(false);
|
||||
|
||||
const general = ref<PresentGeneralContext['general'] | null>(null);
|
||||
const city = ref<PresentGeneralContext['city'] | null>(null);
|
||||
@@ -373,6 +377,9 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
|
||||
const refreshQueue = createLatestRefreshQueue(refreshMainData);
|
||||
const loadMainData = () => refreshQueue.request();
|
||||
const realtimeRefreshQueue = createRateLimitedRefreshQueue(() => refreshQueue.request(), {
|
||||
minIntervalMs: REALTIME_FULL_REFRESH_MIN_INTERVAL_MS,
|
||||
});
|
||||
|
||||
const refreshMessages = async () => {
|
||||
const id = generalId.value;
|
||||
@@ -593,6 +600,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
|
||||
let realtimeSource: EventSource | null = null;
|
||||
let realtimeToken: string | null = null;
|
||||
let visibilityListenerInstalled = false;
|
||||
|
||||
const isAccessToken = (token: string | null): boolean => Boolean(token?.startsWith('ga_'));
|
||||
|
||||
@@ -663,7 +671,13 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
if (typeof window === 'undefined') {
|
||||
return;
|
||||
}
|
||||
if (!realtimeEnabled.value || !session.isReady || !session.hasGeneral) {
|
||||
if (
|
||||
!realtimeActive.value ||
|
||||
document.visibilityState === 'hidden' ||
|
||||
!realtimeEnabled.value ||
|
||||
!session.isReady ||
|
||||
!session.hasGeneral
|
||||
) {
|
||||
return;
|
||||
}
|
||||
const token = await ensureAccessToken();
|
||||
@@ -688,7 +702,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
realtimeStatus.value = realtimeEnabled.value ? 'idle' : 'paused';
|
||||
});
|
||||
source.addEventListener('turnCompleted', () => {
|
||||
void loadMainData();
|
||||
realtimeRefreshQueue.request();
|
||||
});
|
||||
source.addEventListener('messageCreated', (event) => {
|
||||
const payload = parseRealtimePayload(event);
|
||||
@@ -706,9 +720,48 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
});
|
||||
};
|
||||
|
||||
const handleVisibilityChange = () => {
|
||||
if (!realtimeActive.value) return;
|
||||
if (document.visibilityState === 'hidden') {
|
||||
realtimeRefreshQueue.cancelPending();
|
||||
closeRealtimeSource();
|
||||
realtimeStatus.value = 'idle';
|
||||
return;
|
||||
}
|
||||
realtimeRefreshQueue.beginCooldown();
|
||||
void connectRealtime();
|
||||
realtimeRefreshQueue.request();
|
||||
};
|
||||
|
||||
const startRealtime = () => {
|
||||
if (typeof window === 'undefined' || realtimeActive.value) return;
|
||||
realtimeActive.value = true;
|
||||
realtimeRefreshQueue.beginCooldown();
|
||||
if (!visibilityListenerInstalled) {
|
||||
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||
visibilityListenerInstalled = true;
|
||||
}
|
||||
};
|
||||
|
||||
const stopRealtime = () => {
|
||||
realtimeActive.value = false;
|
||||
realtimeRefreshQueue.cancelPending();
|
||||
closeRealtimeSource();
|
||||
if (visibilityListenerInstalled) {
|
||||
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||
visibilityListenerInstalled = false;
|
||||
}
|
||||
realtimeStatus.value = realtimeEnabled.value ? 'idle' : 'paused';
|
||||
};
|
||||
|
||||
watch(
|
||||
() => [realtimeEnabled.value, session.isReady, session.hasGeneral, session.gameToken],
|
||||
([enabled, ready, hasGeneral]) => {
|
||||
() => [realtimeActive.value, realtimeEnabled.value, session.isReady, session.hasGeneral, session.gameToken],
|
||||
([active, enabled, ready, hasGeneral]) => {
|
||||
if (!active) {
|
||||
closeRealtimeSource();
|
||||
realtimeStatus.value = enabled ? 'idle' : 'paused';
|
||||
return;
|
||||
}
|
||||
if (!enabled) {
|
||||
closeRealtimeSource();
|
||||
realtimeStatus.value = 'paused';
|
||||
@@ -720,8 +773,7 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
return;
|
||||
}
|
||||
void connectRealtime();
|
||||
},
|
||||
{ immediate: true }
|
||||
}
|
||||
);
|
||||
|
||||
return {
|
||||
@@ -755,6 +807,8 @@ export const useMainDashboardStore = defineStore('mainDashboard', () => {
|
||||
statusLine,
|
||||
realtimeLabel,
|
||||
setRealtimeEnabled,
|
||||
startRealtime,
|
||||
stopRealtime,
|
||||
dismissSurveyNotice,
|
||||
loadMainData,
|
||||
refreshMessages,
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
export type RateLimitedRefreshQueue = {
|
||||
request: () => void;
|
||||
beginCooldown: () => void;
|
||||
cancelPending: () => void;
|
||||
isRunning: () => boolean;
|
||||
};
|
||||
|
||||
type TimerHandle = ReturnType<typeof setTimeout>;
|
||||
|
||||
export type RateLimitedRefreshQueueOptions = {
|
||||
minIntervalMs: number;
|
||||
now?: () => number;
|
||||
setTimer?: (callback: () => void, delayMs: number) => TimerHandle;
|
||||
clearTimer?: (timer: TimerHandle) => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* Keeps a sustained notification stream from turning into a sustained request
|
||||
* stream. One trailing refresh is retained, while starts are bounded by the
|
||||
* configured interval.
|
||||
*/
|
||||
export const createRateLimitedRefreshQueue = (
|
||||
refresh: () => Promise<void>,
|
||||
options: RateLimitedRefreshQueueOptions
|
||||
): RateLimitedRefreshQueue => {
|
||||
const minIntervalMs = Math.max(0, options.minIntervalMs);
|
||||
const now = options.now ?? Date.now;
|
||||
const setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
|
||||
const clearTimer = options.clearTimer ?? ((timer) => clearTimeout(timer));
|
||||
|
||||
let active = false;
|
||||
let pending = false;
|
||||
let timer: TimerHandle | null = null;
|
||||
let lastStartedAt = Number.NEGATIVE_INFINITY;
|
||||
|
||||
const schedule = () => {
|
||||
if (!pending || active || timer) return;
|
||||
const delayMs = Math.max(0, lastStartedAt + minIntervalMs - now());
|
||||
timer = setTimer(() => {
|
||||
timer = null;
|
||||
if (!pending) return;
|
||||
pending = false;
|
||||
active = true;
|
||||
lastStartedAt = now();
|
||||
void refresh().finally(() => {
|
||||
active = false;
|
||||
schedule();
|
||||
});
|
||||
}, delayMs);
|
||||
};
|
||||
|
||||
return {
|
||||
request: () => {
|
||||
pending = true;
|
||||
schedule();
|
||||
},
|
||||
beginCooldown: () => {
|
||||
lastStartedAt = now();
|
||||
},
|
||||
cancelPending: () => {
|
||||
pending = false;
|
||||
if (timer) {
|
||||
clearTimer(timer);
|
||||
timer = null;
|
||||
}
|
||||
},
|
||||
isRunning: () => active || timer !== null,
|
||||
};
|
||||
};
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onUnmounted, ref, watch } from 'vue';
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue';
|
||||
import { storeToRefs } from 'pinia';
|
||||
import { useMediaQuery } from '@vueuse/core';
|
||||
import PanelCard from '../components/ui/PanelCard.vue';
|
||||
@@ -92,6 +92,11 @@ onUnmounted(() => {
|
||||
if (surveyNoticeTimer) {
|
||||
clearTimeout(surveyNoticeTimer);
|
||||
}
|
||||
dashboard.stopRealtime();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
dashboard.startRealtime();
|
||||
});
|
||||
|
||||
const shiftGeneralTurns = (amount: number) => {
|
||||
|
||||
Reference in New Issue
Block a user