mirror of
https://github.com/MrUnknownDE/VRCX.git
synced 2026-05-06 14:46:04 +02:00
fix: add activity store and user activity caching
This commit is contained in:
@@ -0,0 +1,222 @@
|
||||
import { defineStore } from 'pinia';
|
||||
import { toast } from 'vue-sonner';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
|
||||
import { database } from '../services/database';
|
||||
import {
|
||||
buildSessionsFromGamelog,
|
||||
ONLINE_SESSION_MERGE_GAP_MS
|
||||
} from '../shared/utils/overlapCalculator';
|
||||
|
||||
const ACTIVITY_CACHE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
||||
const refreshJobs = new Map();
|
||||
|
||||
function buildSessionsAndPendingFromEvents(events, initialStart = null) {
|
||||
const sessions = [];
|
||||
let currentStart = initialStart;
|
||||
|
||||
for (const event of events) {
|
||||
const ts = new Date(event.created_at).getTime();
|
||||
if (event.type === 'Online') {
|
||||
// Treat consecutive Online events as reconnect boundaries.
|
||||
if (currentStart !== null) {
|
||||
sessions.push({ start: currentStart, end: ts });
|
||||
}
|
||||
currentStart = ts;
|
||||
} else if (event.type === 'Offline' && currentStart !== null) {
|
||||
sessions.push({ start: currentStart, end: ts });
|
||||
currentStart = null;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
pendingSessionStartAt: currentStart,
|
||||
sessions
|
||||
};
|
||||
}
|
||||
|
||||
function mergeWithLastSession(lastSession, newSessions) {
|
||||
if (!lastSession || newSessions.length === 0) {
|
||||
return { replaceLastSession: null, sessions: newSessions };
|
||||
}
|
||||
|
||||
const firstSession = newSessions[0];
|
||||
if (firstSession.start > lastSession.end + ONLINE_SESSION_MERGE_GAP_MS) {
|
||||
return { replaceLastSession: null, sessions: newSessions };
|
||||
}
|
||||
|
||||
const mergedFirst = {
|
||||
start: Math.min(lastSession.start, firstSession.start),
|
||||
end: Math.max(lastSession.end, firstSession.end)
|
||||
};
|
||||
return {
|
||||
replaceLastSession: lastSession,
|
||||
sessions: [mergedFirst, ...newSessions.slice(1)]
|
||||
};
|
||||
}
|
||||
|
||||
export const useActivityStore = defineStore('Activity', () => {
|
||||
const { t } = useI18n();
|
||||
|
||||
function getCache(userId) {
|
||||
return database.getActivityCache(userId);
|
||||
}
|
||||
|
||||
function isExpired(cacheEntry) {
|
||||
if (!cacheEntry?.updatedAt) {
|
||||
return true;
|
||||
}
|
||||
const updatedAtMs = Date.parse(cacheEntry.updatedAt);
|
||||
if (Number.isNaN(updatedAtMs)) {
|
||||
return true;
|
||||
}
|
||||
return Date.now() - updatedAtMs >= ACTIVITY_CACHE_TTL_MS;
|
||||
}
|
||||
|
||||
function isRefreshing(userId) {
|
||||
return refreshJobs.has(userId);
|
||||
}
|
||||
|
||||
async function fullRefresh(userId, isSelf) {
|
||||
if (isSelf) {
|
||||
const rows = await database.getCurrentUserOnlineSessions();
|
||||
const sessions = buildSessionsFromGamelog(rows);
|
||||
const sourceLastCreatedAt =
|
||||
rows.length > 0 ? rows[rows.length - 1].created_at : '';
|
||||
const entry = {
|
||||
userId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
isSelf,
|
||||
sourceLastCreatedAt,
|
||||
pendingSessionStartAt: null,
|
||||
sessions
|
||||
};
|
||||
await database.replaceActivityCache(entry);
|
||||
return database.getActivityCache(userId);
|
||||
}
|
||||
|
||||
const events = await database.getOnlineOfflineSessions(userId);
|
||||
const { sessions, pendingSessionStartAt } = buildSessionsAndPendingFromEvents(events);
|
||||
const sourceLastCreatedAt =
|
||||
events.length > 0 ? events[events.length - 1].created_at : '';
|
||||
const entry = {
|
||||
userId,
|
||||
updatedAt: new Date().toISOString(),
|
||||
isSelf,
|
||||
sourceLastCreatedAt,
|
||||
pendingSessionStartAt,
|
||||
sessions
|
||||
};
|
||||
await database.replaceActivityCache(entry);
|
||||
return database.getActivityCache(userId);
|
||||
}
|
||||
|
||||
async function incrementalRefresh(meta) {
|
||||
const updatedAt = new Date().toISOString();
|
||||
|
||||
if (meta.isSelf) {
|
||||
if (!meta.sourceLastCreatedAt) {
|
||||
return fullRefresh(meta.userId, true);
|
||||
}
|
||||
|
||||
const rows = await database.getCurrentUserOnlineSessionsAfter(
|
||||
meta.sourceLastCreatedAt
|
||||
);
|
||||
if (rows.length === 0) {
|
||||
await database.touchActivityCacheMeta({
|
||||
...meta,
|
||||
updatedAt
|
||||
});
|
||||
return database.getActivityCache(meta.userId);
|
||||
}
|
||||
|
||||
const sourceLastCreatedAt = rows[rows.length - 1].created_at;
|
||||
const newSessionsRaw = buildSessionsFromGamelog(rows);
|
||||
const lastSession = await database.getLastActivityCacheSession(meta.userId);
|
||||
const merged = mergeWithLastSession(lastSession, newSessionsRaw);
|
||||
|
||||
await database.appendActivityCache({
|
||||
...meta,
|
||||
updatedAt,
|
||||
sourceLastCreatedAt,
|
||||
pendingSessionStartAt: null,
|
||||
sessions: merged.sessions,
|
||||
replaceLastSession: merged.replaceLastSession
|
||||
});
|
||||
return database.getActivityCache(meta.userId);
|
||||
}
|
||||
|
||||
if (!meta.sourceLastCreatedAt) {
|
||||
return fullRefresh(meta.userId, false);
|
||||
}
|
||||
|
||||
const events = await database.getOnlineOfflineSessionsAfter(
|
||||
meta.userId,
|
||||
meta.sourceLastCreatedAt
|
||||
);
|
||||
if (events.length === 0) {
|
||||
await database.touchActivityCacheMeta({
|
||||
...meta,
|
||||
updatedAt
|
||||
});
|
||||
return database.getActivityCache(meta.userId);
|
||||
}
|
||||
|
||||
const { sessions, pendingSessionStartAt } = buildSessionsAndPendingFromEvents(
|
||||
events,
|
||||
meta.pendingSessionStartAt
|
||||
);
|
||||
const sourceLastCreatedAt = events[events.length - 1].created_at;
|
||||
|
||||
await database.appendActivityCache({
|
||||
...meta,
|
||||
updatedAt,
|
||||
sourceLastCreatedAt,
|
||||
pendingSessionStartAt,
|
||||
sessions
|
||||
});
|
||||
return database.getActivityCache(meta.userId);
|
||||
}
|
||||
|
||||
function refreshActivityCache(userId, isSelf, options = {}) {
|
||||
const { notifyStart = false, notifyComplete = false } = options;
|
||||
|
||||
const existing = refreshJobs.get(userId);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
if (notifyStart) {
|
||||
toast.info(t('dialog.user.activity.refresh_started'), {
|
||||
position: 'bottom-center'
|
||||
});
|
||||
}
|
||||
|
||||
const job = (async () => {
|
||||
const meta = await database.getActivityCacheMeta(userId);
|
||||
const entry =
|
||||
meta && meta.isSelf === isSelf
|
||||
? await incrementalRefresh(meta)
|
||||
: await fullRefresh(userId, isSelf);
|
||||
if (notifyComplete) {
|
||||
toast.success(t('dialog.user.activity.refresh_complete'), {
|
||||
position: 'bottom-center'
|
||||
});
|
||||
}
|
||||
return entry;
|
||||
})().finally(() => {
|
||||
refreshJobs.delete(userId);
|
||||
});
|
||||
|
||||
refreshJobs.set(userId, job);
|
||||
return job;
|
||||
}
|
||||
|
||||
return {
|
||||
getCache,
|
||||
isExpired,
|
||||
isRefreshing,
|
||||
refreshActivityCache,
|
||||
ttlMs: ACTIVITY_CACHE_TTL_MS
|
||||
};
|
||||
});
|
||||
@@ -2,6 +2,7 @@ import { createPinia } from 'pinia';
|
||||
|
||||
import { getSentry, isSentryOptedIn } from '../plugins';
|
||||
import { useAdvancedSettingsStore } from './settings/advanced';
|
||||
import { useActivityStore } from './activity';
|
||||
import { useAppearanceSettingsStore } from './settings/appearance';
|
||||
import { useAuthStore } from './auth';
|
||||
import { useAvatarProviderStore } from './avatarProvider';
|
||||
@@ -124,6 +125,7 @@ export async function initPiniaPlugins() {
|
||||
export function createGlobalStores() {
|
||||
return {
|
||||
advancedSettings: useAdvancedSettingsStore(),
|
||||
activity: useActivityStore(),
|
||||
appearanceSettings: useAppearanceSettingsStore(),
|
||||
discordPresenceSettings: useDiscordPresenceSettingsStore(),
|
||||
generalSettings: useGeneralSettingsStore(),
|
||||
@@ -186,6 +188,7 @@ export {
|
||||
useChartsStore,
|
||||
useDashboardStore,
|
||||
useAdvancedSettingsStore,
|
||||
useActivityStore,
|
||||
useAppearanceSettingsStore,
|
||||
useDiscordPresenceSettingsStore,
|
||||
useGeneralSettingsStore,
|
||||
|
||||
+19
-18
@@ -71,6 +71,11 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
windowState: '',
|
||||
externalNotifierVersion: 0
|
||||
});
|
||||
const databaseUpgradeState = ref({
|
||||
visible: false,
|
||||
fromVersion: 0,
|
||||
toVersion: 0
|
||||
});
|
||||
|
||||
const currentlyDroppingFile = ref(null);
|
||||
const isRegistryBackupDialogVisible = ref(false);
|
||||
@@ -182,15 +187,13 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
*/
|
||||
async function updateDatabaseVersion() {
|
||||
// requires dbVars.userPrefix to be already set
|
||||
const databaseVersion = 13;
|
||||
let msgBox;
|
||||
const databaseVersion = 14;
|
||||
if (state.databaseVersion < databaseVersion) {
|
||||
if (state.databaseVersion) {
|
||||
msgBox = toast.warning(
|
||||
'DO NOT CLOSE VRCX, database upgrade in progress...',
|
||||
{ duration: Infinity, position: 'bottom-right' }
|
||||
);
|
||||
}
|
||||
databaseUpgradeState.value = {
|
||||
visible: state.databaseVersion > 0,
|
||||
fromVersion: state.databaseVersion,
|
||||
toVersion: databaseVersion
|
||||
};
|
||||
console.log(
|
||||
`Updating database from ${state.databaseVersion} to ${databaseVersion}...`
|
||||
);
|
||||
@@ -212,19 +215,16 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
databaseVersion
|
||||
);
|
||||
console.log('Database update complete.');
|
||||
toast.dismiss(msgBox);
|
||||
if (state.databaseVersion) {
|
||||
// only display when database exists
|
||||
toast.success(t('message.database.upgrade_complete'));
|
||||
}
|
||||
state.databaseVersion = databaseVersion;
|
||||
databaseUpgradeState.value.visible = false;
|
||||
} catch (err) {
|
||||
console.error(err);
|
||||
toast.dismiss(msgBox);
|
||||
toast.error(
|
||||
'Database upgrade failed, check console for details',
|
||||
{ duration: 120000 }
|
||||
);
|
||||
databaseUpgradeState.value.visible = false;
|
||||
await modalStore.alert({
|
||||
title: t('message.database.upgrade_failed_title'),
|
||||
description: t('message.database.upgrade_failed_description'),
|
||||
dismissible: false
|
||||
});
|
||||
AppApi.ShowDevTools();
|
||||
}
|
||||
}
|
||||
@@ -817,6 +817,7 @@ export const useVrcxStore = defineStore('Vrcx', () => {
|
||||
state,
|
||||
|
||||
appStartAt,
|
||||
databaseUpgradeState,
|
||||
proxyServer,
|
||||
setProxyServer,
|
||||
setIpcEnabled,
|
||||
|
||||
Reference in New Issue
Block a user