fix: activity tab

This commit is contained in:
pa
2026-03-23 09:47:48 +09:00
parent 7d4a229d1f
commit f2050dc520
3 changed files with 416 additions and 186 deletions

View File

@@ -23,7 +23,6 @@
<SelectValue /> <SelectValue />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem value="180">{{ t('dialog.user.activity.period_180') }}</SelectItem>
<SelectItem value="90">{{ t('dialog.user.activity.period_90') }}</SelectItem> <SelectItem value="90">{{ t('dialog.user.activity.period_90') }}</SelectItem>
<SelectItem value="30">{{ t('dialog.user.activity.period_30') }}</SelectItem> <SelectItem value="30">{{ t('dialog.user.activity.period_30') }}</SelectItem>
<SelectItem value="7">{{ t('dialog.user.activity.period_7') }}</SelectItem> <SelectItem value="7">{{ t('dialog.user.activity.period_7') }}</SelectItem>
@@ -241,6 +240,7 @@
}); });
let activeRequestId = 0; let activeRequestId = 0;
let activeOverlapRequestId = 0;
let lastLoadedUserId = ''; let lastLoadedUserId = '';
const pendingWorldThumbnailFetches = new Set(); const pendingWorldThumbnailFetches = new Set();
@@ -281,6 +281,7 @@
mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] }; mainHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] }; overlapHeatmapView.value = { rawBuckets: [], normalizedBuckets: [] };
activeRequestId++; activeRequestId++;
activeOverlapRequestId++;
lastLoadedUserId = ''; lastLoadedUserId = '';
} }
@@ -291,6 +292,7 @@
} }
const requestId = ++activeRequestId; const requestId = ++activeRequestId;
++activeOverlapRequestId;
if (!silent) { if (!silent) {
isLoading.value = true; isLoading.value = true;
} }
@@ -386,20 +388,56 @@
await refreshData(); await refreshData();
} }
async function refreshOverlapOnly() {
const userId = userDialog.value.id;
if (!userId || isSelf.value || !hasAnyData.value) {
return;
}
const requestId = ++activeOverlapRequestId;
isOverlapLoading.value = true;
try {
const rangeDays = parseInt(selectedPeriod.value, 10) || 30;
const overlapView = await activityStore.loadOverlapView({
currentUserId: currentUser.value.id,
targetUserId: userId,
rangeDays,
dayLabels: dayLabels.value,
forceRefresh: false,
excludeHours: {
enabled: excludeHoursEnabled.value,
startHour: parseInt(excludeStartHour.value, 10),
endHour: parseInt(excludeEndHour.value, 10)
}
});
if (requestId !== activeOverlapRequestId || userDialog.value.id !== userId) {
return;
}
overlapHeatmapView.value = {
rawBuckets: overlapView.rawBuckets,
normalizedBuckets: overlapView.normalizedBuckets
};
hasOverlapData.value = overlapView.hasOverlapData;
overlapPercent.value = overlapView.overlapPercent;
bestOverlapTime.value = overlapView.bestOverlapTime;
} finally {
if (requestId === activeOverlapRequestId) {
isOverlapLoading.value = false;
}
}
}
async function onExcludeToggle(value) { async function onExcludeToggle(value) {
excludeHoursEnabled.value = value; excludeHoursEnabled.value = value;
await configRepository.setBool('VRCX_overlapExcludeEnabled', value); await configRepository.setBool('VRCX_overlapExcludeEnabled', value);
if (!isSelf.value && hasAnyData.value) { await refreshOverlapOnly();
await refreshData({ silent: true });
}
} }
async function onExcludeRangeChange() { async function onExcludeRangeChange() {
await configRepository.setString('VRCX_overlapExcludeStart', excludeStartHour.value); await configRepository.setString('VRCX_overlapExcludeStart', excludeStartHour.value);
await configRepository.setString('VRCX_overlapExcludeEnd', excludeEndHour.value); await configRepository.setString('VRCX_overlapExcludeEnd', excludeEndHour.value);
if (!isSelf.value && hasAnyData.value) { await refreshOverlapOnly();
await refreshData({ silent: true });
}
} }
async function fetchMissingTopWorldThumbnails(worlds) { async function fetchMissingTopWorldThumbnails(worlds) {
@@ -510,19 +548,34 @@
ensureActivityChart(); ensureActivityChart();
if (!activityChart) return; if (!activityChart) return;
activityChart.setOption(buildHeatmapOption({ activityChart.setOption(
data: toHeatmapSeriesData(mainHeatmapView.value.normalizedBuckets, weekStartsOn.value), buildHeatmapOption({
rawBuckets: mainHeatmapView.value.rawBuckets, data: toHeatmapSeriesData(mainHeatmapView.value.normalizedBuckets, weekStartsOn.value),
dayLabels: displayDayLabels.value, rawBuckets: mainHeatmapView.value.rawBuckets,
hourLabels, dayLabels: displayDayLabels.value,
weekStartsOn: weekStartsOn.value, hourLabels,
isDarkMode: isDarkMode.value, weekStartsOn: weekStartsOn.value,
emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)', isDarkMode: isDarkMode.value,
scaleColors: isDarkMode.value emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)',
? ['hsl(160, 40%, 24%)', 'hsl(150, 48%, 32%)', 'hsl(142, 55%, 38%)', 'hsl(142, 65%, 46%)', 'hsl(142, 80%, 55%)'] scaleColors: isDarkMode.value
: ['hsl(160, 40%, 82%)', 'hsl(155, 45%, 68%)', 'hsl(142, 55%, 55%)', 'hsl(142, 65%, 40%)', 'hsl(142, 76%, 30%)'], ? [
unitLabel: t('dialog.user.activity.minutes_online') 'hsl(160, 40%, 24%)',
}), { notMerge: true }); 'hsl(150, 48%, 32%)',
'hsl(142, 55%, 38%)',
'hsl(142, 65%, 46%)',
'hsl(142, 80%, 55%)'
]
: [
'hsl(160, 40%, 82%)',
'hsl(155, 45%, 68%)',
'hsl(142, 55%, 55%)',
'hsl(142, 65%, 40%)',
'hsl(142, 76%, 30%)'
],
unitLabel: t('dialog.user.activity.minutes_online')
}),
{ replaceMerge: ['series'] }
);
} }
function renderOverlapChart() { function renderOverlapChart() {
@@ -533,19 +586,34 @@
ensureOverlapChart(); ensureOverlapChart();
if (!overlapChart) return; if (!overlapChart) return;
overlapChart.setOption(buildHeatmapOption({ overlapChart.setOption(
data: toHeatmapSeriesData(overlapHeatmapView.value.normalizedBuckets, weekStartsOn.value), buildHeatmapOption({
rawBuckets: overlapHeatmapView.value.rawBuckets, data: toHeatmapSeriesData(overlapHeatmapView.value.normalizedBuckets, weekStartsOn.value),
dayLabels: displayDayLabels.value, rawBuckets: overlapHeatmapView.value.rawBuckets,
hourLabels, dayLabels: displayDayLabels.value,
weekStartsOn: weekStartsOn.value, hourLabels,
isDarkMode: isDarkMode.value, weekStartsOn: weekStartsOn.value,
emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)', isDarkMode: isDarkMode.value,
scaleColors: isDarkMode.value emptyColor: isDarkMode.value ? 'hsl(220, 15%, 12%)' : 'hsl(210, 30%, 95%)',
? ['hsl(260, 30%, 26%)', 'hsl(260, 42%, 36%)', 'hsl(260, 50%, 45%)', 'hsl(260, 60%, 54%)', 'hsl(260, 70%, 62%)'] scaleColors: isDarkMode.value
: ['hsl(260, 35%, 85%)', 'hsl(260, 42%, 70%)', 'hsl(260, 48%, 58%)', 'hsl(260, 55%, 48%)', 'hsl(260, 60%, 38%)'], ? [
unitLabel: t('dialog.user.activity.overlap.minutes_overlap') 'hsl(260, 30%, 26%)',
}), { notMerge: true }); 'hsl(260, 42%, 36%)',
'hsl(260, 50%, 45%)',
'hsl(260, 60%, 54%)',
'hsl(260, 70%, 62%)'
]
: [
'hsl(260, 35%, 85%)',
'hsl(260, 42%, 70%)',
'hsl(260, 48%, 58%)',
'hsl(260, 55%, 48%)',
'hsl(260, 60%, 38%)'
],
unitLabel: t('dialog.user.activity.overlap.minutes_overlap')
}),
{ replaceMerge: ['series'] }
);
} }
function rebuildCharts() { function rebuildCharts() {
@@ -557,7 +625,7 @@
} }
function onChartRightClick() { function onChartRightClick() {
toast(t('dialog.user.activity.easter_egg'), { position: 'bottom-center', icon: h(Tractor) }); toast(t('dialog.user.activity.chart_hint'), { position: 'bottom-center', icon: h(Tractor) });
clearTimeout(easterEggTimer); clearTimeout(easterEggTimer);
easterEggTimer = setTimeout(() => { easterEggTimer = setTimeout(() => {
easterEggTimer = null; easterEggTimer = null;
@@ -566,7 +634,7 @@
function onOverlapChartRightClick() { function onOverlapChartRightClick() {
if (easterEggTimer) { if (easterEggTimer) {
toast(t('dialog.user.activity.easter_egg_reply'), { position: 'bottom-center', icon: h(Sprout) }); toast(t('dialog.user.activity.chart_hint_reply'), { position: 'bottom-center', icon: h(Sprout) });
} }
} }
@@ -577,40 +645,60 @@
void loadForVisibleTab(); void loadForVisibleTab();
} }
watch(() => userDialog.value.id, () => { watch(
resetActivityState(); () => userDialog.value.id,
rebuildCharts(); () => {
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') { resetActivityState();
void nextTick(() => loadForVisibleTab()); rebuildCharts();
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
void nextTick(() => loadForVisibleTab());
}
} }
}); );
watch([locale, isDarkMode, weekStartsOn], rebuildCharts); watch([locale, isDarkMode, weekStartsOn], rebuildCharts);
watch(() => selectedPeriod.value, () => { watch(
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') { () => selectedPeriod.value,
void onPeriodChange(); () => {
if (userDialog.value.visible && userDialog.value.activeTab === 'Activity') {
void onPeriodChange();
}
} }
}); );
watch(() => mainHeatmapView.value, () => { watch(
nextTick(() => renderActivityChart()); () => mainHeatmapView.value,
}, { deep: true }); () => {
watch(() => overlapHeatmapView.value, () => { nextTick(() => renderActivityChart());
nextTick(() => renderOverlapChart()); },
}, { deep: true }); { deep: true }
watch(() => userDialog.value.visible, (visible) => { );
if (!visible) return; watch(
nextTick(() => { () => overlapHeatmapView.value,
activityChart?.resize(); () => {
overlapChart?.resize(); nextTick(() => renderOverlapChart());
}); },
if (userDialog.value.activeTab === 'Activity') { { deep: true }
void loadForVisibleTab(); );
watch(
() => userDialog.value.visible,
(visible) => {
if (!visible) return;
nextTick(() => {
activityChart?.resize();
overlapChart?.resize();
});
if (userDialog.value.activeTab === 'Activity') {
void loadForVisibleTab();
}
} }
}); );
watch(() => userDialog.value.activeTab, (activeTab) => { watch(
if (activeTab === 'Activity' && userDialog.value.visible) { () => userDialog.value.activeTab,
void loadForVisibleTab(); (activeTab) => {
if (activeTab === 'Activity' && userDialog.value.visible) {
void loadForVisibleTab();
}
} }
}); );
onMounted(async () => { onMounted(async () => {
await initializeSettings(); await initializeSettings();

View File

@@ -1435,7 +1435,6 @@
"most_active_day": "Most active day:", "most_active_day": "Most active day:",
"most_active_time": "Peak hours:", "most_active_time": "Peak hours:",
"period": "Period:", "period": "Period:",
"period_180": "Last 180 Days",
"period_90": "Last 90 Days", "period_90": "Last 90 Days",
"period_30": "Last 30 Days", "period_30": "Last 30 Days",
"period_7": "Last 7 Days", "period_7": "Last 7 Days",
@@ -1450,8 +1449,8 @@
"sat": "Sat", "sat": "Sat",
"sun": "Sun" "sun": "Sun"
}, },
"easter_egg": "Did you farm your green squares today?", "chart_hint": "Did you farm your green squares today?",
"easter_egg_reply": "You can't farm this.", "chart_hint_reply": "You can't farm this.",
"overlap": { "overlap": {
"header": "Online Overlap", "header": "Online Overlap",
"peak_overlap": "Peak overlap:", "peak_overlap": "Peak overlap:",

View File

@@ -134,8 +134,21 @@ export const useActivityStore = defineStore('Activity', () => {
return false; return false;
} }
async function loadActivity(userId, { isSelf = false, rangeDays = 30, normalizeConfig, dayLabels, forceRefresh = false }) { async function loadActivity(
const snapshot = await ensureSnapshot(userId, { isSelf, rangeDays, forceRefresh }); userId,
{
isSelf = false,
rangeDays = 30,
normalizeConfig,
dayLabels,
forceRefresh = false
}
) {
const snapshot = await ensureSnapshot(userId, {
isSelf,
rangeDays,
forceRefresh
});
const cacheKey = String(rangeDays); const cacheKey = String(rangeDays);
const currentCursor = snapshot.sync.sourceLastCreatedAt || ''; const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
@@ -175,38 +188,55 @@ export const useActivityStore = defineStore('Activity', () => {
builtAt: new Date().toISOString() builtAt: new Date().toISOString()
}; };
snapshot.activityViews.set(cacheKey, view); snapshot.activityViews.set(cacheKey, view);
deferWrite(() => database.upsertActivityBucketCacheV2({ deferWrite(() =>
ownerUserId: userId, database.upsertActivityBucketCacheV2({
rangeDays, ownerUserId: userId,
viewKind: database.ACTIVITY_VIEW_KIND.ACTIVITY, rangeDays,
builtFromCursor: currentCursor, viewKind: database.ACTIVITY_VIEW_KIND.ACTIVITY,
rawBuckets: view.rawBuckets, builtFromCursor: currentCursor,
normalizedBuckets: view.normalizedBuckets, rawBuckets: view.rawBuckets,
summary: { normalizedBuckets: view.normalizedBuckets,
peakDay: view.peakDay, summary: {
peakTime: view.peakTime, peakDay: view.peakDay,
filteredEventCount: view.filteredEventCount peakTime: view.peakTime,
}, filteredEventCount: view.filteredEventCount
builtAt: view.builtAt },
})); builtAt: view.builtAt
})
);
return buildActivityResponse(snapshot, view); return buildActivityResponse(snapshot, view);
} }
async function loadOverlap(currentUserId, targetUserId, { async function loadOverlap(
rangeDays = 30, currentUserId,
dayLabels, targetUserId,
normalizeConfig, {
excludeHours, rangeDays = 30,
forceRefresh = false dayLabels,
}) { normalizeConfig,
excludeHours,
forceRefresh = false
}
) {
const [selfSnapshot, targetSnapshot] = await Promise.all([ const [selfSnapshot, targetSnapshot] = await Promise.all([
ensureSnapshot(currentUserId, { isSelf: true, rangeDays, forceRefresh }), ensureSnapshot(currentUserId, {
ensureSnapshot(targetUserId, { isSelf: false, rangeDays, forceRefresh }) isSelf: true,
rangeDays,
forceRefresh
}),
ensureSnapshot(targetUserId, {
isSelf: false,
rangeDays,
forceRefresh
})
]); ]);
const excludeKey = overlapExcludeKey(excludeHours); const excludeKey = overlapExcludeKey(excludeHours);
const cacheKey = `${targetUserId}:${rangeDays}:${excludeKey}`; const cacheKey = `${targetUserId}:${rangeDays}:${excludeKey}`;
const cursor = pairCursor(selfSnapshot.sync.sourceLastCreatedAt, targetSnapshot.sync.sourceLastCreatedAt); const cursor = pairCursor(
selfSnapshot.sync.sourceLastCreatedAt,
targetSnapshot.sync.sourceLastCreatedAt
);
let view = targetSnapshot.overlapViews.get(cacheKey); let view = targetSnapshot.overlapViews.get(cacheKey);
if (view?.builtFromCursor === cursor) { if (view?.builtFromCursor === cursor) {
@@ -246,26 +276,35 @@ export const useActivityStore = defineStore('Activity', () => {
builtAt: new Date().toISOString() builtAt: new Date().toISOString()
}; };
targetSnapshot.overlapViews.set(cacheKey, view); targetSnapshot.overlapViews.set(cacheKey, view);
deferWrite(() => database.upsertActivityBucketCacheV2({ deferWrite(() =>
ownerUserId: currentUserId, database.upsertActivityBucketCacheV2({
targetUserId, ownerUserId: currentUserId,
rangeDays, targetUserId,
viewKind: database.ACTIVITY_VIEW_KIND.OVERLAP, rangeDays,
excludeKey, viewKind: database.ACTIVITY_VIEW_KIND.OVERLAP,
builtFromCursor: cursor, excludeKey,
rawBuckets: view.rawBuckets, builtFromCursor: cursor,
normalizedBuckets: view.normalizedBuckets, rawBuckets: view.rawBuckets,
summary: { normalizedBuckets: view.normalizedBuckets,
overlapPercent: view.overlapPercent, summary: {
bestOverlapTime: view.bestOverlapTime overlapPercent: view.overlapPercent,
}, bestOverlapTime: view.bestOverlapTime
builtAt: view.builtAt },
})); builtAt: view.builtAt
})
);
return view; return view;
} }
async function loadTopWorlds(userId, { rangeDays = 30, limit = 5, isSelf = true, forceRefresh = false }) { async function loadTopWorlds(
const snapshot = await ensureSnapshot(userId, { isSelf, rangeDays, forceRefresh }); userId,
{ rangeDays = 30, limit = 5, isSelf = true, forceRefresh = false }
) {
const snapshot = await ensureSnapshot(userId, {
isSelf,
rangeDays,
forceRefresh
});
const cacheKey = `${rangeDays}:${limit}`; const cacheKey = `${rangeDays}:${limit}`;
const currentCursor = snapshot.sync.sourceLastCreatedAt || ''; const currentCursor = snapshot.sync.sourceLastCreatedAt || '';
@@ -275,7 +314,10 @@ export const useActivityStore = defineStore('Activity', () => {
} }
if (!forceRefresh) { if (!forceRefresh) {
const persisted = await database.getActivityTopWorldsCacheV2(userId, rangeDays); const persisted = await database.getActivityTopWorldsCacheV2(
userId,
rangeDays
);
if (persisted?.builtFromCursor === currentCursor) { if (persisted?.builtFromCursor === currentCursor) {
snapshot.topWorldsViews.set(cacheKey, persisted); snapshot.topWorldsViews.set(cacheKey, persisted);
return persisted.worlds; return persisted.worlds;
@@ -292,14 +334,16 @@ export const useActivityStore = defineStore('Activity', () => {
}; };
snapshot.topWorldsViews.set(cacheKey, entry); snapshot.topWorldsViews.set(cacheKey, entry);
deferWrite(() => database.replaceActivityTopWorldsCacheV2(entry)); deferWrite(() => database.replaceActivityTopWorldsCacheV2(entry));
deferWrite(() => database.upsertActivityRangeCacheV2({ deferWrite(() =>
userId, database.upsertActivityRangeCacheV2({
rangeDays, userId,
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.TOP_WORLDS, rangeDays,
isComplete: true, cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.TOP_WORLDS,
builtFromCursor: currentCursor, isComplete: true,
builtAt: entry.builtAt builtFromCursor: currentCursor,
})); builtAt: entry.builtAt
})
);
return worlds; return worlds;
} }
@@ -307,7 +351,13 @@ export const useActivityStore = defineStore('Activity', () => {
return loadActivity(userId, { ...options, forceRefresh: true }); return loadActivity(userId, { ...options, forceRefresh: true });
} }
async function loadActivityView({ userId, isSelf = false, rangeDays = 30, dayLabels, forceRefresh = false }) { async function loadActivityView({
userId,
isSelf = false,
rangeDays = 30,
dayLabels,
forceRefresh = false
}) {
const response = await loadActivity(userId, { const response = await loadActivity(userId, {
isSelf, isSelf,
rangeDays, rangeDays,
@@ -325,7 +375,14 @@ export const useActivityStore = defineStore('Activity', () => {
}; };
} }
async function loadOverlapView({ currentUserId, targetUserId, rangeDays = 30, dayLabels, excludeHours, forceRefresh = false }) { async function loadOverlapView({
currentUserId,
targetUserId,
rangeDays = 30,
dayLabels,
excludeHours,
forceRefresh = false
}) {
const response = await loadOverlap(currentUserId, targetUserId, { const response = await loadOverlap(currentUserId, targetUserId, {
rangeDays, rangeDays,
dayLabels, dayLabels,
@@ -342,7 +399,12 @@ export const useActivityStore = defineStore('Activity', () => {
}; };
} }
async function loadTopWorldsView({ userId, rangeDays = 30, limit = 5, forceRefresh = false }) { async function loadTopWorldsView({
userId,
rangeDays = 30,
limit = 5,
forceRefresh = false
}) {
return loadTopWorlds(userId, { return loadTopWorlds(userId, {
rangeDays, rangeDays,
limit, limit,
@@ -399,7 +461,10 @@ async function hydrateSnapshot(userId, isSelf) {
snapshot.sync = { snapshot.sync = {
...snapshot.sync, ...snapshot.sync,
...syncState, ...syncState,
isSelf: typeof syncState.isSelf === 'boolean' ? syncState.isSelf : snapshot.isSelf isSelf:
typeof syncState.isSelf === 'boolean'
? syncState.isSelf
: snapshot.isSelf
}; };
} }
if (sessions.length > 0) { if (sessions.length > 0) {
@@ -408,7 +473,10 @@ async function hydrateSnapshot(userId, isSelf) {
return snapshot; return snapshot;
} }
async function ensureSnapshot(userId, { isSelf, rangeDays, forceRefresh = false }) { async function ensureSnapshot(
userId,
{ isSelf, rangeDays, forceRefresh = false }
) {
const jobKey = `${userId}:${isSelf}:${rangeDays}:${forceRefresh ? 'force' : 'normal'}`; const jobKey = `${userId}:${isSelf}:${rangeDays}:${forceRefresh ? 'force' : 'normal'}`;
const existingJob = inFlightJobs.get(jobKey); const existingJob = inFlightJobs.get(jobKey);
if (existingJob) { if (existingJob) {
@@ -440,7 +508,10 @@ async function fullRefresh(snapshot, rangeDays) {
isSelf: snapshot.isSelf, isSelf: snapshot.isSelf,
fromDays: rangeDays fromDays: rangeDays
}); });
const sourceLastCreatedAt = sourceItems.length > 0 ? sourceItems[sourceItems.length - 1].created_at : ''; const sourceLastCreatedAt =
sourceItems.length > 0
? sourceItems[sourceItems.length - 1].created_at
: '';
const result = await workerCall('computeSessionsSnapshot', { const result = await workerCall('computeSessionsSnapshot', {
sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence', sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence',
rows: snapshot.isSelf ? sourceItems : undefined, rows: snapshot.isSelf ? sourceItems : undefined,
@@ -462,16 +533,20 @@ async function fullRefresh(snapshot, rangeDays) {
}; };
clearDerivedViews(snapshot); clearDerivedViews(snapshot);
deferWrite(() => database.replaceActivitySessionsV2(snapshot.userId, snapshot.sessions)); deferWrite(() =>
database.replaceActivitySessionsV2(snapshot.userId, snapshot.sessions)
);
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync)); deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
deferWrite(() => database.upsertActivityRangeCacheV2({ deferWrite(() =>
userId: snapshot.userId, database.upsertActivityRangeCacheV2({
rangeDays, userId: snapshot.userId,
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS, rangeDays,
isComplete: true, cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS,
builtFromCursor: snapshot.sync.sourceLastCreatedAt, isComplete: true,
builtAt: snapshot.sync.updatedAt builtFromCursor: snapshot.sync.sourceLastCreatedAt,
})); builtAt: snapshot.sync.updatedAt
})
);
} }
async function incrementalRefresh(snapshot) { async function incrementalRefresh(snapshot) {
@@ -496,15 +571,18 @@ async function incrementalRefresh(snapshot) {
sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence', sourceType: snapshot.isSelf ? 'self_gamelog' : 'friend_presence',
rows: snapshot.isSelf ? sourceItems : undefined, rows: snapshot.isSelf ? sourceItems : undefined,
events: snapshot.isSelf ? undefined : sourceItems, events: snapshot.isSelf ? undefined : sourceItems,
initialStart: snapshot.isSelf ? null : snapshot.sync.pendingSessionStartAt, initialStart: snapshot.isSelf
? null
: snapshot.sync.pendingSessionStartAt,
nowMs: Date.now(), nowMs: Date.now(),
mayHaveOpenTail: snapshot.isSelf, mayHaveOpenTail: snapshot.isSelf,
sourceRevision: sourceLastCreatedAt sourceRevision: sourceLastCreatedAt
}); });
const replaceFromStartAt = snapshot.sessions.length > 0 const replaceFromStartAt =
? snapshot.sessions[Math.max(snapshot.sessions.length - 1, 0)].start snapshot.sessions.length > 0
: null; ? snapshot.sessions[Math.max(snapshot.sessions.length - 1, 0)].start
: null;
const merged = mergeSessions(snapshot.sessions, result.sessions); const merged = mergeSessions(snapshot.sessions, result.sessions);
snapshot.sessions = merged; snapshot.sessions = merged;
snapshot.sync = { snapshot.sync = {
@@ -515,14 +593,17 @@ async function incrementalRefresh(snapshot) {
}; };
clearDerivedViews(snapshot); clearDerivedViews(snapshot);
const tailSessions = replaceFromStartAt === null const tailSessions =
? merged replaceFromStartAt === null
: merged.filter((session) => session.start >= replaceFromStartAt); ? merged
deferWrite(() => database.appendActivitySessionsV2({ : merged.filter((session) => session.start >= replaceFromStartAt);
userId: snapshot.userId, deferWrite(() =>
sessions: tailSessions, database.appendActivitySessionsV2({
replaceFromStartAt userId: snapshot.userId,
})); sessions: tailSessions,
replaceFromStartAt
})
);
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync)); deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
} }
@@ -550,58 +631,120 @@ async function expandRange(snapshot, rangeDays) {
if (result.sessions.length > 0) { if (result.sessions.length > 0) {
snapshot.sessions = mergeSessions(result.sessions, snapshot.sessions); snapshot.sessions = mergeSessions(result.sessions, snapshot.sessions);
deferWrite(() => database.replaceActivitySessionsV2(snapshot.userId, snapshot.sessions)); deferWrite(() =>
database.replaceActivitySessionsV2(
snapshot.userId,
snapshot.sessions
)
);
} }
snapshot.sync.cachedRangeDays = rangeDays; snapshot.sync.cachedRangeDays = rangeDays;
snapshot.sync.updatedAt = new Date().toISOString(); snapshot.sync.updatedAt = new Date().toISOString();
clearDerivedViews(snapshot); clearDerivedViews(snapshot);
deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync)); deferWrite(() => database.upsertActivitySyncStateV2(snapshot.sync));
deferWrite(() => database.upsertActivityRangeCacheV2({ deferWrite(() =>
userId: snapshot.userId, database.upsertActivityRangeCacheV2({
rangeDays, userId: snapshot.userId,
cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS, rangeDays,
isComplete: true, cacheKind: database.ACTIVITY_RANGE_CACHE_KIND.SESSIONS,
builtFromCursor: snapshot.sync.sourceLastCreatedAt, isComplete: true,
builtAt: snapshot.sync.updatedAt builtFromCursor: snapshot.sync.sourceLastCreatedAt,
})); builtAt: snapshot.sync.updatedAt
})
);
} }
function pickActivityNormalizeConfig(isSelf, rangeDays) { function pickActivityNormalizeConfig(isSelf, rangeDays) {
const role = isSelf ? 'self' : 'friend'; const role = isSelf ? 'self' : 'friend';
return { return (
self: { {
7: { floorPercentile: 10, capPercentile: 80, rankWeight: 0.15, targetCoverage: 0.12, targetVolume: 40 }, self: {
30: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.25, targetVolume: 60 }, 7: {
90: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.30, targetVolume: 50 }, floorPercentile: 10,
180: { floorPercentile: 20, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.35, targetVolume: 40 } capPercentile: 80,
}, rankWeight: 0.15,
friend: { targetCoverage: 0.12,
7: { floorPercentile: 10, capPercentile: 80, rankWeight: 0.15, targetCoverage: 0.12, targetVolume: 40 }, targetVolume: 40
30: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.25, targetVolume: 60 }, },
90: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.30, targetVolume: 50 }, 30: {
180: { floorPercentile: 20, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.35, targetVolume: 40 } floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.25,
targetVolume: 60
},
90: {
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.3,
targetVolume: 50
}
},
friend: {
7: {
floorPercentile: 10,
capPercentile: 80,
rankWeight: 0.15,
targetCoverage: 0.12,
targetVolume: 40
},
30: {
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.25,
targetVolume: 60
},
90: {
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.3,
targetVolume: 50
}
}
}[role][rangeDays] || {
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.25,
targetVolume: 60
} }
}[role][rangeDays] || { );
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.20,
targetCoverage: 0.25,
targetVolume: 60
};
} }
function pickOverlapNormalizeConfig(rangeDays) { function pickOverlapNormalizeConfig(rangeDays) {
return { return (
7: { floorPercentile: 10, capPercentile: 80, rankWeight: 0.15, targetCoverage: 0.08, targetVolume: 15 }, {
30: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.15, targetVolume: 25 }, 7: {
90: { floorPercentile: 15, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.18, targetVolume: 20 }, floorPercentile: 10,
180: { floorPercentile: 20, capPercentile: 85, rankWeight: 0.20, targetCoverage: 0.20, targetVolume: 15 } capPercentile: 80,
}[rangeDays] || { rankWeight: 0.15,
floorPercentile: 15, targetCoverage: 0.08,
capPercentile: 85, targetVolume: 15
rankWeight: 0.20, },
targetCoverage: 0.15, 30: {
targetVolume: 25 floorPercentile: 15,
}; capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.15,
targetVolume: 25
},
90: {
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.18,
targetVolume: 20
}
}[rangeDays] || {
floorPercentile: 15,
capPercentile: 85,
rankWeight: 0.2,
targetCoverage: 0.15,
targetVolume: 25
}
);
} }