From 0c42ed078752a3d2301e850cfc9cf8dc1d0364d3 Mon Sep 17 00:00:00 2001 From: Yashaswini K P Date: Sun, 19 Jul 2026 23:46:01 +0530 Subject: [PATCH 1/2] feat: implement incremental streak tracking and backfill initialization --- scripts/backfill-streaks.js | 83 +++++++++++++++++++++++++++++++++++++ scripts/sync-leaderboard.js | 34 +++++++++++++++ 2 files changed, 117 insertions(+) create mode 100644 scripts/backfill-streaks.js diff --git a/scripts/backfill-streaks.js b/scripts/backfill-streaks.js new file mode 100644 index 00000000..c8478351 --- /dev/null +++ b/scripts/backfill-streaks.js @@ -0,0 +1,83 @@ +"use strict"; + +const fs = require("fs").promises; +const fsSync = require("fs"); +const path = require("path"); + +async function calculateStreaks(history) { + if (!history || history.length === 0) { + return { currentStreak: 0, longestStreak: 0, lastUpdated: null }; + } + + const sortedHistory = [...history].sort( + (a, b) => new Date(a.date) - new Date(b.date), + ); + + let currentStreak = 0; + let longestStreak = 0; + let lastActiveDate = null; + let previousTotal = -1; + + for (const entry of sortedHistory) { + const currentTotal = + (entry.easy || 0) + (entry.medium || 0) + (entry.hard || 0); + const entryDate = new Date(entry.date); + + if (currentTotal > previousTotal) { + if (!lastActiveDate) { + currentStreak = 1; + } else { + const diffDays = Math.round( + (entryDate - lastActiveDate) / (1000 * 60 * 60 * 24), + ); + + if (diffDays === 1) { + currentStreak++; + } else if (diffDays > 1) { + currentStreak = 1; + } + } + + longestStreak = Math.max(longestStreak, currentStreak); + lastActiveDate = entryDate; + } + + previousTotal = currentTotal; + } + + return { + currentStreak, + longestStreak, + lastUpdated: lastActiveDate + ? lastActiveDate.toISOString().split("T")[0] + : null, + }; +} + +async function runBackfill() { + const DATA_DIR = process.env.DATA_DIR || path.join(__dirname, "..", "data"); + const userDataDir = path.join(DATA_DIR, "user-data"); + + try { + const files = await fs.readdir(userDataDir); + const jsonFiles = files.filter((f) => f.endsWith(".json")); + + for (const file of jsonFiles) { + const filePath = path.join(userDataDir, file); + const userData = JSON.parse(await fs.readFile(filePath, "utf8")); + + const streaks = await calculateStreaks(userData.history); + + userData.currentStreak = streaks.currentStreak; + userData.longestStreak = streaks.longestStreak; + userData.streakLastUpdated = streaks.lastUpdated; + + await fs.writeFile(filePath, JSON.stringify(userData, null, 2)); + } + } catch (err) { + console.error(`Backfill failed: ${err.message}`); + process.exit(1); + } +} + +runBackfill(); diff --git a/scripts/sync-leaderboard.js b/scripts/sync-leaderboard.js index 0d85d551..e03ba1f1 100644 --- a/scripts/sync-leaderboard.js +++ b/scripts/sync-leaderboard.js @@ -95,6 +95,40 @@ async function updateUserDataAsync(user, DATA_DIR, ranksObj = null) { if (ranksObj) { userData.leaderboardRanks = ranksObj; } + + const today = new Date().toISOString().split("T")[0]; + const yesterday = new Date(); + yesterday.setDate(yesterday.getDate() - 1); + const yesterdayStr = yesterday.toISOString().split("T")[0]; + + const totalSolvedToday = + (user.data.easySolved || 0) + + (user.data.mediumSolved || 0) + + (user.data.hardSolved || 0); + + const totalSolvedYesterday = + history.length > 1 + ? history[history.length - 2].easy + + history[history.length - 2].medium + + history[history.length - 2].hard + : 0; + + if (userData.streakLastUpdated && userData.streakLastUpdated < yesterdayStr) { + userData.currentStreak = 0; + } + + if ( + totalSolvedToday > totalSolvedYesterday && + userData.streakLastUpdated !== today + ) { + userData.currentStreak = (userData.currentStreak || 0) + 1; + userData.streakLastUpdated = today; + + if (userData.currentStreak > (userData.longestStreak || 0)) { + userData.longestStreak = userData.currentStreak; + } + } + await atomicWrite(userDataPath, userData); } From 05625f1694184c7a8e48fd73f677c779c0c08e91 Mon Sep 17 00:00:00 2001 From: Yashaswini K P Date: Mon, 20 Jul 2026 00:08:56 +0530 Subject: [PATCH 2/2] refactor: transition streak data to nested object structure --- scripts/backfill-streaks.js | 8 +++++--- scripts/sync-leaderboard.js | 21 ++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/scripts/backfill-streaks.js b/scripts/backfill-streaks.js index c8478351..8456441a 100644 --- a/scripts/backfill-streaks.js +++ b/scripts/backfill-streaks.js @@ -68,9 +68,11 @@ async function runBackfill() { const streaks = await calculateStreaks(userData.history); - userData.currentStreak = streaks.currentStreak; - userData.longestStreak = streaks.longestStreak; - userData.streakLastUpdated = streaks.lastUpdated; + userData.streak = { + current: streaks.currentStreak, + longest: streaks.longestStreak, + lastUpdated: streaks.lastUpdated, + }; await fs.writeFile(filePath, JSON.stringify(userData, null, 2)); } diff --git a/scripts/sync-leaderboard.js b/scripts/sync-leaderboard.js index e03ba1f1..1309a165 100644 --- a/scripts/sync-leaderboard.js +++ b/scripts/sync-leaderboard.js @@ -101,6 +101,10 @@ async function updateUserDataAsync(user, DATA_DIR, ranksObj = null) { yesterday.setDate(yesterday.getDate() - 1); const yesterdayStr = yesterday.toISOString().split("T")[0]; + if (!userData.streak) { + userData.streak = { current: 0, longest: 0, lastUpdated: null }; + } + const totalSolvedToday = (user.data.easySolved || 0) + (user.data.mediumSolved || 0) + @@ -113,19 +117,22 @@ async function updateUserDataAsync(user, DATA_DIR, ranksObj = null) { history[history.length - 2].hard : 0; - if (userData.streakLastUpdated && userData.streakLastUpdated < yesterdayStr) { - userData.currentStreak = 0; + if ( + userData.streak.lastUpdated && + userData.streak.lastUpdated < yesterdayStr + ) { + userData.streak.current = 0; } if ( totalSolvedToday > totalSolvedYesterday && - userData.streakLastUpdated !== today + userData.streak.lastUpdated !== today ) { - userData.currentStreak = (userData.currentStreak || 0) + 1; - userData.streakLastUpdated = today; + userData.streak.current += 1; + userData.streak.lastUpdated = today; - if (userData.currentStreak > (userData.longestStreak || 0)) { - userData.longestStreak = userData.currentStreak; + if (userData.streak.current > (userData.streak.longest || 0)) { + userData.streak.longest = userData.streak.current; } }