From 49f0454bf4e44d278b533c4fb5e19205791a3d66 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 19:10:21 +0100 Subject: [PATCH 01/10] bam --- frontend/src/ts/constants/default-snapshot.ts | 8 +- frontend/src/ts/db.ts | 413 +++++++++--------- .../src/ts/elements/test-activity-calendar.ts | 8 + frontend/src/ts/elements/test-activity.ts | 14 +- frontend/src/ts/pages/account.ts | 20 +- 5 files changed, 246 insertions(+), 217 deletions(-) diff --git a/frontend/src/ts/constants/default-snapshot.ts b/frontend/src/ts/constants/default-snapshot.ts index 96b54209f405..0b634ab4f464 100644 --- a/frontend/src/ts/constants/default-snapshot.ts +++ b/frontend/src/ts/constants/default-snapshot.ts @@ -1,5 +1,6 @@ import { ResultFilters, + TestActivity, User, UserProfileDetails, UserTag, @@ -8,10 +9,6 @@ import { getDefaultConfig } from "./default-config"; import { Mode } from "@monkeytype/schemas/shared"; import { Result } from "@monkeytype/schemas/results"; import { Config, Difficulty, FunboxName } from "@monkeytype/schemas/configs"; -import { - ModifiableTestActivityCalendar, - TestActivityCalendar, -} from "../elements/test-activity-calendar"; import { Preset } from "@monkeytype/schemas/presets"; import { Language } from "@monkeytype/schemas/languages"; import { ConnectionStatus } from "@monkeytype/schemas/connections"; @@ -83,8 +80,7 @@ export type Snapshot = Omit< presets: SnapshotPreset[]; results?: SnapshotResult[]; xp: number; - testActivity?: ModifiableTestActivityCalendar; - testActivityByYear?: { [key: string]: TestActivityCalendar }; + testActivityData?: TestActivity; connections: Record; }; diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index f71418abdb90..08011afe0efc 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -36,8 +36,14 @@ import { get as getServerConfiguration, } from "./ape/server-configuration"; import { Connection } from "@monkeytype/schemas/connections"; +import { createStore, unwrap } from "solid-js/store"; -let dbSnapshot: Snapshot | undefined; +const [snapshot, setSnapshotStore] = createStore< + // oxlint-disable-next-line typescript/no-unnecessary-type-arguments + Snapshot | Record +>({}); + +// let dbSnapshot: Snapshot | undefined; const firstDayOfTheWeek = getFirstDayOfTheWeek(); export class SnapshotInitError extends Error { @@ -54,16 +60,32 @@ export class SnapshotInitError extends Error { } export function getSnapshot(): Snapshot | undefined { - return dbSnapshot; + if (Object.keys(snapshot).length === 0) { + return undefined; + } + return structuredClone(unwrap(snapshot)) as Snapshot; } export function setSnapshot( newSnapshot: Snapshot | undefined, options?: { dispatchEvent?: boolean }, ): void { - const originalBanned = dbSnapshot?.banned; - const originalVerified = dbSnapshot?.verified; - const lbOptOut = dbSnapshot?.lbOptOut; + if (newSnapshot === undefined) { + setSnapshotStore({}); + + if (options?.dispatchEvent !== false) { + AuthEvent.dispatch({ + type: "snapshotUpdated", + data: { isInitial: false }, + }); + } + return; + } + + const currentSnapshot = getSnapshot(); + const originalBanned = currentSnapshot?.banned; + const originalVerified = currentSnapshot?.verified; + const lbOptOut = currentSnapshot?.lbOptOut; //not allowing user to override these values i guess? try { @@ -75,12 +97,11 @@ export function setSnapshot( try { delete newSnapshot?.lbOptOut; } catch {} - dbSnapshot = newSnapshot; - if (dbSnapshot) { - dbSnapshot.banned = originalBanned; - dbSnapshot.verified = originalVerified; - dbSnapshot.lbOptOut = lbOptOut; - } + newSnapshot.banned = originalBanned; + newSnapshot.verified = originalVerified; + newSnapshot.lbOptOut = lbOptOut; + + setSnapshotStore(newSnapshot); if (options?.dispatchEvent !== false) { AuthEvent.dispatch({ type: "snapshotUpdated", data: { isInitial: false } }); @@ -190,11 +211,12 @@ export async function initSnapshot(): Promise { snap.allTimeLbs = userData.allTimeLbs; if (userData.testActivity !== undefined) { - snap.testActivity = new ModifiableTestActivityCalendar( - userData.testActivity.testsByDays, - new Date(userData.testActivity.lastDay), - firstDayOfTheWeek, - ); + // snap.testActivity = new ModifiableTestActivityCalendar( + // userData.testActivity.testsByDays, + // new Date(userData.testActivity.lastDay), + // firstDayOfTheWeek, + // ); + snap.testActivityData = userData.testActivity; } const hourOffset = userData?.streak?.hourOffset; @@ -273,10 +295,10 @@ export async function initSnapshot(): Promise { snap.connections = convertConnections(connectionsData); - dbSnapshot = snap; - return dbSnapshot; + setSnapshot(snap, { dispatchEvent: false }); + return snap; } catch (e) { - dbSnapshot = getDefaultSnapshot(); + setSnapshot(getDefaultSnapshot(), { dispatchEvent: false }); throw e; } } @@ -284,6 +306,8 @@ export async function initSnapshot(): Promise { export async function getUserResults(offset?: number): Promise { if (!isAuthenticated()) return false; + const dbSnapshot = getSnapshot(); + if (!dbSnapshot) return false; if ( dbSnapshot.results !== undefined && @@ -336,16 +360,14 @@ export async function getUserResults(offset?: number): Promise { } else { dbSnapshot.results = results; } + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } -function _getCustomThemeById(themeID: string): CustomTheme | undefined { - return dbSnapshot?.customThemes?.find((t) => t._id === themeID); -} - export async function addCustomTheme( theme: Omit, ): Promise { + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return false; dbSnapshot.customThemes ??= []; @@ -372,6 +394,8 @@ export async function addCustomTheme( }; dbSnapshot.customThemes.push(newCustomTheme); + + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } @@ -380,6 +404,7 @@ export async function editCustomTheme( newTheme: Omit, ): Promise { if (!isAuthenticated()) return false; + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return false; dbSnapshot.customThemes ??= []; @@ -408,12 +433,13 @@ export async function editCustomTheme( dbSnapshot.customThemes[dbSnapshot.customThemes.indexOf(customTheme)] = newCustomTheme; - + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } export async function deleteCustomTheme(themeId: string): Promise { if (!isAuthenticated()) return false; + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return false; const customTheme = dbSnapshot.customThemes?.find((t) => t._id === themeId); @@ -428,7 +454,7 @@ export async function deleteCustomTheme(themeId: string): Promise { dbSnapshot.customThemes = dbSnapshot.customThemes?.filter( (t) => t._id !== themeId, ); - + setSnapshot(dbSnapshot, { dispatchEvent: false }); return true; } @@ -512,7 +538,9 @@ export async function getUserAverage10( } const retval: [number, number] = - snapshot === null || (await getUserResults()) === null ? [0, 0] : cont(); + snapshot === undefined || (await getUserResults()) === null + ? [0, 0] + : cont(); return retval; } @@ -623,6 +651,8 @@ export async function getLocalPB( lazyMode: boolean, funboxes: FunboxMetadata[], ): Promise { + const dbSnapshot = getSnapshot(); + if (!funboxes.every((f) => f.canGetPb)) { return undefined; } @@ -655,29 +685,27 @@ function saveLocalPB( consistency: number, ): void { if (mode === "quote") return; + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return; - function cont(): void { - if (!dbSnapshot) return; - let found = false; + let found = false; - dbSnapshot.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; + dbSnapshot.personalBests ??= { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; - dbSnapshot.personalBests[mode] ??= { - [mode2]: [], - }; + dbSnapshot.personalBests[mode] ??= { + [mode2]: [], + }; - dbSnapshot.personalBests[mode][mode2] ??= - [] as unknown as PersonalBests[M][Mode2]; + dbSnapshot.personalBests[mode][mode2] ??= + [] as unknown as PersonalBests[M][Mode2]; - ( - dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[] - ).forEach((pb) => { + (dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[]).forEach( + (pb) => { if ( (pb.punctuation ?? false) === punctuation && (pb.numbers ?? false) === numbers && @@ -693,29 +721,25 @@ function saveLocalPB( pb.consistency = consistency; pb.lazyMode = lazyMode; } + }, + ); + if (!found) { + //nothing found + (dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[]).push({ + language, + difficulty, + lazyMode, + punctuation, + numbers, + wpm, + acc, + raw, + timestamp: Date.now(), + consistency, }); - if (!found) { - //nothing found - (dbSnapshot.personalBests[mode][mode2] as unknown as PersonalBest[]).push( - { - language, - difficulty, - lazyMode, - punctuation, - numbers, - wpm, - acc, - raw, - timestamp: Date.now(), - consistency, - }, - ); - } } - if (dbSnapshot !== null) { - cont(); - } + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export async function getLocalTagPB( @@ -728,11 +752,12 @@ export async function getLocalTagPB( difficulty: Difficulty, lazyMode: boolean, ): Promise { - if (dbSnapshot === null) return 0; + const dbSnapshot = getSnapshot(); + if (dbSnapshot === undefined) return 0; let ret = 0; - const filteredtag = (getSnapshot()?.tags ?? []).find((t) => t._id === tagId); + const filteredtag = (dbSnapshot.tags ?? []).find((t) => t._id === tagId); if (filteredtag === undefined) return ret; @@ -780,108 +805,108 @@ export async function saveLocalTagPB( acc: number, raw: number, consistency: number, -): Promise { +): Promise { + const dbSnapshot = getSnapshot(); if (!dbSnapshot) return; if (mode === "quote") return; - function cont(): void { - const filteredtag = dbSnapshot?.tags?.find( - (t) => t._id === tagId, - ) as SnapshotUserTag; + const filteredtag = dbSnapshot?.tags?.find( + (t) => t._id === tagId, + ) as SnapshotUserTag; - filteredtag.personalBests ??= { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; + filteredtag.personalBests ??= { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; - filteredtag.personalBests[mode] ??= { - [mode2]: [], - }; + filteredtag.personalBests[mode] ??= { + [mode2]: [], + }; - filteredtag.personalBests[mode][mode2] ??= - [] as unknown as PersonalBests[M][Mode2]; + filteredtag.personalBests[mode][mode2] ??= + [] as unknown as PersonalBests[M][Mode2]; - try { - let found = false; + try { + let found = false; + ( + filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] + ).forEach((pb) => { + if ( + (pb.punctuation ?? false) === punctuation && + (pb.numbers ?? false) === numbers && + pb.difficulty === difficulty && + pb.language === language && + (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)) + ) { + found = true; + pb.wpm = wpm; + pb.acc = acc; + pb.raw = raw; + pb.timestamp = Date.now(); + pb.consistency = consistency; + pb.lazyMode = lazyMode; + } + }); + if (!found) { + //nothing found ( filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] - ).forEach((pb) => { - if ( - (pb.punctuation ?? false) === punctuation && - (pb.numbers ?? false) === numbers && - pb.difficulty === difficulty && - pb.language === language && - (pb.lazyMode === lazyMode || (pb.lazyMode === undefined && !lazyMode)) - ) { - found = true; - pb.wpm = wpm; - pb.acc = acc; - pb.raw = raw; - pb.timestamp = Date.now(); - pb.consistency = consistency; - pb.lazyMode = lazyMode; - } + ).push({ + language, + difficulty, + lazyMode, + punctuation, + numbers, + wpm, + acc, + raw, + timestamp: Date.now(), + consistency, }); - if (!found) { - //nothing found - ( - filteredtag.personalBests[mode][mode2] as unknown as PersonalBest[] - ).push({ - language, - difficulty, - lazyMode, - punctuation, - numbers, - wpm, - acc, - raw, - timestamp: Date.now(), - consistency, - }); - } - } catch (e) { - //that mode or mode2 is not found - filteredtag.personalBests = { - time: {}, - words: {}, - quote: {}, - zen: {}, - custom: {}, - }; - filteredtag.personalBests[mode][mode2] = [ - { - language: language, - difficulty: difficulty, - lazyMode: lazyMode, - punctuation: punctuation, - numbers: numbers, - wpm: wpm, - acc: acc, - raw: raw, - timestamp: Date.now(), - consistency: consistency, - }, - ] as unknown as PersonalBests[M][Mode2]; } + } catch (e) { + //that mode or mode2 is not found + filteredtag.personalBests = { + time: {}, + words: {}, + quote: {}, + zen: {}, + custom: {}, + }; + filteredtag.personalBests[mode][mode2] = [ + { + language: language, + difficulty: difficulty, + lazyMode: lazyMode, + punctuation: punctuation, + numbers: numbers, + wpm: wpm, + acc: acc, + raw: raw, + timestamp: Date.now(), + consistency: consistency, + }, + ] as unknown as PersonalBests[M][Mode2]; } - if (dbSnapshot !== null) { - cont(); - } - - return; + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export function deleteLocalTag(tagId: string): void { - getSnapshot()?.results?.forEach((result) => { + const dbSnapshot = getSnapshot(); + if (dbSnapshot === undefined) return; + + for (const result of dbSnapshot.results ?? []) { const tagIndex = result.tags.indexOf(tagId); if (tagIndex > -1) { result.tags.splice(tagIndex, 1); } - }); + } + + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export async function updateLocalTagPB( @@ -894,9 +919,10 @@ export async function updateLocalTagPB( difficulty: Difficulty, lazyMode: boolean, ): Promise { - if (dbSnapshot === null) return; + const dbSnapshot = getSnapshot(); + if (dbSnapshot === undefined) return; - const filteredtag = (getSnapshot()?.tags ?? []).find((t) => t._id === tagId); + const filteredtag = (dbSnapshot?.tags ?? []).find((t) => t._id === tagId); if (filteredtag === undefined) return; @@ -907,7 +933,7 @@ export async function updateLocalTagPB( consistency: 0, }; - getSnapshot()?.results?.forEach((result) => { + dbSnapshot.results?.forEach((result) => { if (result.tags.includes(tagId) && result.wpm > pb.wpm) { if ( result.mode === mode && @@ -940,6 +966,8 @@ export async function updateLocalTagPB( pb.rawWpm, pb.consistency, ); + + setSnapshot(dbSnapshot, { dispatchEvent: false }); } export async function updateLbMemory( @@ -973,7 +1001,7 @@ export async function updateLbMemory( body: { mode, mode2, language, rank }, }); } - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } } @@ -1010,8 +1038,14 @@ export function saveLocalResult(data: SaveLocalResultData): void { if (snapshot?.results !== undefined) { snapshot.results.unshift(data.result); } - if (snapshot.testActivity !== undefined) { - snapshot.testActivity.increment(new Date(data.result.timestamp)); + if (snapshot.testActivityData !== undefined) { + const calendar = new ModifiableTestActivityCalendar( + snapshot.testActivityData.testsByDays, + new Date(snapshot.testActivityData.lastDay), + firstDayOfTheWeek, + ); + calendar.increment(new Date(data.result.timestamp)); + snapshot.testActivityData = calendar.getRawData(); } snapshot.typingStats ??= { timeTyping: 0, @@ -1079,7 +1113,7 @@ export function updateInboxUnreadSize(newSize: number): void { if (!snapshot) return; snapshot.inboxUnreadSize = newSize; - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } export function addBadge(badge: Badge): void { @@ -1090,22 +1124,37 @@ export function addBadge(badge: Badge): void { badges: [], }; snapshot.inventory.badges.push(badge); - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } +const testActivityByYear = new Map(); + export async function getTestActivityCalendar( yearString: string, ): Promise { + const dbSnapshot = getSnapshot(); if (!isAuthenticated() || dbSnapshot === undefined) return undefined; - if (yearString === "current") return dbSnapshot.testActivity; + if (yearString === "current") { + const calendar = new ModifiableTestActivityCalendar( + dbSnapshot.testActivityData?.testsByDays ?? [], + new Date(dbSnapshot.testActivityData?.lastDay ?? Date.now()), + firstDayOfTheWeek, + ); + return calendar; + } const currentYear = new Date().getFullYear().toString(); if (yearString === currentYear) { - return dbSnapshot.testActivity?.getFullYearCalendar(); + const calendar = new ModifiableTestActivityCalendar( + dbSnapshot.testActivityData?.testsByDays ?? [], + new Date(dbSnapshot.testActivityData?.lastDay ?? Date.now()), + firstDayOfTheWeek, + ); + return calendar.getFullYearCalendar(); } - if (dbSnapshot.testActivityByYear === undefined) { + if (testActivityByYear.size === 0) { if (!ConnectionState.get()) { return undefined; } @@ -1118,7 +1167,6 @@ export async function getTestActivityCalendar( return undefined; } - dbSnapshot.testActivityByYear = {}; for (const year in response.body.data) { if (year === currentYear) continue; const testsByDays = response.body.data[year] ?? []; @@ -1127,17 +1175,15 @@ export async function getTestActivityCalendar( testsByDays.length, ); - dbSnapshot.testActivityByYear[year] = new TestActivityCalendar( - testsByDays, - lastDay, - firstDayOfTheWeek, - true, + testActivityByYear.set( + year, + new TestActivityCalendar(testsByDays, lastDay, firstDayOfTheWeek, true), ); } Loader.hide(); } - return dbSnapshot.testActivityByYear[yearString]; + return testActivityByYear.get(yearString); } export function mergeConnections(connections: Connection[]): void { @@ -1150,7 +1196,7 @@ export function mergeConnections(connections: Connection[]): void { snapshot.connections[key] = value; } - setSnapshot(snapshot); + setSnapshot(snapshot, { dispatchEvent: false }); } function convertConnections( @@ -1181,36 +1227,3 @@ export function isFriend(uid: string | undefined): boolean { ([receiverUid, status]) => receiverUid === uid && status === "accepted", ); } - -// export async function DB.getLocalTagPB(tagId) { -// function cont() { -// let ret = 0; -// try { -// ret = dbSnapshot.tags.filter((t) => t.id === tagId)[0].pb; -// if (ret === undefined) { -// ret = 0; -// } -// return ret; -// } catch (e) { -// return ret; -// } -// } - -// const retval = dbSnapshot !== null ? cont() : undefined; - -// return retval; -// } - -// export async functio(tagId, wpm) { -// function cont() { -// dbSnapshot.tags.forEach((tag) => { -// if (tag._id === tagId) { -// tag.pb = wpm; -// } -// }); -// } - -// if (dbSnapshot !== null) { -// cont(); -// } -// } diff --git a/frontend/src/ts/elements/test-activity-calendar.ts b/frontend/src/ts/elements/test-activity-calendar.ts index 01b72b158c92..1ee8ec835599 100644 --- a/frontend/src/ts/elements/test-activity-calendar.ts +++ b/frontend/src/ts/elements/test-activity-calendar.ts @@ -1,4 +1,5 @@ import { UTCDateMini } from "@date-fns/utc/date/mini"; +import { TestActivity } from "@monkeytype/schemas/users"; import { safeNumber } from "@monkeytype/util/numbers"; import { format, @@ -203,6 +204,13 @@ export class TestActivityCalendar implements TestActivityCalendar { private nextLastDayOfWeek(date: Date): Date { return nextDay(date, ((this.firstDayOfWeek + 6) % 7) as Day); } + + getRawData(): TestActivity { + return { + testsByDays: this.data.map((v) => (v === undefined ? null : v)), + lastDay: this.endDay.getTime(), + }; + } } export class ModifiableTestActivityCalendar diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 1793bf67fafb..5eeef3cb5ae9 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -8,18 +8,28 @@ import { TestActivityMonth, } from "./test-activity-calendar"; import { safeNumber } from "@monkeytype/util/numbers"; +import { TestActivity } from "@monkeytype/schemas/users"; +import { getFirstDayOfTheWeek } from "../utils/date-and-time"; let yearSelector: SlimSelect | undefined = undefined; +let calendar: TestActivityCalendar | undefined = undefined; export function init( element: HTMLElement, - calendar?: TestActivityCalendar, + testActivityData?: TestActivity, userSignUpDate?: Date, ): void { - if (calendar === undefined) { + if (testActivityData === undefined) { clear(element); return; } + + calendar ??= new TestActivityCalendar( + testActivityData.testsByDays, + new Date(testActivityData.lastDay), + getFirstDayOfTheWeek(), + ); + element.classList.remove("hidden"); if (element.querySelector(".yearSelect") !== null) { diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 9e6c9d9d580a..d970ebb684ed 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -71,6 +71,8 @@ function loadMoreLines(lineIndex?: number): void { } function buildResultRow(result: SnapshotResult): HTMLTableRowElement { + const snapshot = DB.getSnapshot(); + let diff = result.difficulty ?? "normal"; let icons = ` { - DB.getSnapshot()?.tags?.forEach((snaptag) => { + snapshot?.tags?.forEach((snaptag) => { if (tag === snaptag._id) { tagNames += snaptag.display + ", "; } @@ -223,7 +225,7 @@ async function fillContent(): Promise { TestActivity.init( testActivityEl as HTMLElement, - snapshot.testActivity, + snapshot.testActivityData, new Date(snapshot.addedAt), ); void ResultBatches.update(); @@ -284,7 +286,7 @@ async function fillContent(): Promise { filteredResults = []; qs(".pageAccount .history table tbody")?.empty(); - DB.getSnapshot()?.results?.forEach((result) => { + snapshot.results?.forEach((result) => { // totalSeconds += tt; //apply filters @@ -439,14 +441,14 @@ async function fillContent(): Promise { let tagHide = true; if (result.tags === undefined || result.tags.length === 0) { //no tags, show when no tag is enabled - if ((DB.getSnapshot()?.tags?.length ?? 0) > 0) { + if ((snapshot.tags?.length ?? 0) > 0) { if (ResultFilters.getFilter("tags", "none")) tagHide = false; } else { tagHide = false; } } else { //tags exist - const validTags = DB.getSnapshot()?.tags?.map((t) => t._id); + const validTags = snapshot.tags?.map((t) => t._id); if (validTags === undefined) return; @@ -1005,10 +1007,11 @@ async function update(): Promise { export function updateTagsForResult(resultId: string, tagIds: string[]): void { const tagNames: string[] = []; + const snapshot = DB.getSnapshot(); if (tagIds.length > 0) { for (const tag of tagIds) { - DB.getSnapshot()?.tags?.forEach((snaptag) => { + snapshot?.tags?.forEach((snaptag) => { if (tag === snaptag._id) { tagNames.push(snaptag.display); } @@ -1124,9 +1127,8 @@ qs(".pageAccount")?.onChild( //update local cache result.chartData = chartData; - const dbResult = DB.getSnapshot()?.results?.find( - (it) => it._id === result._id, - ); + const snapshot = DB.getSnapshot(); + const dbResult = snapshot?.results?.find((it) => it._id === result._id); if (dbResult !== undefined) { dbResult["chartData"] = result.chartData; } From a656027ee9b12f8e2db97262e231cf0743c9c914 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 19:17:04 +0100 Subject: [PATCH 02/10] pointless check --- frontend/src/ts/db.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 08011afe0efc..9c5a70e84a4d 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -538,9 +538,7 @@ export async function getUserAverage10( } const retval: [number, number] = - snapshot === undefined || (await getUserResults()) === null - ? [0, 0] - : cont(); + (await getUserResults()) === null ? [0, 0] : cont(); return retval; } From 85f9178d340cdb8ab8aae0f20208ea8ea206360b Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 19:17:54 +0100 Subject: [PATCH 03/10] clear --- frontend/src/ts/db.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 9c5a70e84a4d..206a54722205 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -72,6 +72,7 @@ export function setSnapshot( ): void { if (newSnapshot === undefined) { setSnapshotStore({}); + testActivityByYear.clear(); if (options?.dispatchEvent !== false) { AuthEvent.dispatch({ From fc7a37ba976456c2a0bfc7998c8abb6599dd6070 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 19:20:30 +0100 Subject: [PATCH 04/10] fix --- frontend/src/ts/pages/profile.ts | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/frontend/src/ts/pages/profile.ts b/frontend/src/ts/pages/profile.ts index abe923649a42..49e596f6b1b6 100644 --- a/frontend/src/ts/pages/profile.ts +++ b/frontend/src/ts/pages/profile.ts @@ -9,13 +9,9 @@ import * as Skeleton from "../utils/skeleton"; import { UserProfile } from "@monkeytype/schemas/users"; import { PersonalBests } from "@monkeytype/schemas/shared"; import * as TestActivity from "../elements/test-activity"; -import { TestActivityCalendar } from "../elements/test-activity-calendar"; -import { getFirstDayOfTheWeek } from "../utils/date-and-time"; import { addFriend } from "./friends"; import { onDOMReady, qs, qsr } from "../utils/dom"; -const firstDayOfTheWeek = getFirstDayOfTheWeek(); - function reset(): void { qs(".page.pageProfile .error")?.hide(); qs(".page.pageProfile .preloader")?.show(); @@ -216,12 +212,11 @@ async function update(options: UpdateOptions): Promise { ) as HTMLElement; if (profile.testActivity !== undefined) { - const calendar = new TestActivityCalendar( - profile.testActivity.testsByDays, + TestActivity.init( + testActivity, + profile.testActivity, new Date(profile.testActivity.lastDay), - firstDayOfTheWeek, ); - TestActivity.init(testActivity, calendar); const title = testActivity.querySelector(".top .title") as HTMLElement; title.innerHTML = title?.innerHTML + " in last 12 months"; } else { From 4f708a7acae0431dbc33865e4ad064112448bb9a Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 20:32:03 +0100 Subject: [PATCH 05/10] move yearly cache to test-activity --- frontend/src/ts/db.ts | 72 ++++------------------- frontend/src/ts/elements/test-activity.ts | 32 +++++++++- 2 files changed, 42 insertions(+), 62 deletions(-) diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 206a54722205..384146901ac0 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -4,11 +4,7 @@ import { isAuthenticated, getAuthenticatedUser } from "./firebase"; import * as ConnectionState from "./states/connection"; import { lastElementFromArray } from "./utils/arrays"; import { migrateConfig } from "./utils/config"; -import * as Dates from "date-fns"; -import { - TestActivityCalendar, - ModifiableTestActivityCalendar, -} from "./elements/test-activity-calendar"; +import { ModifiableTestActivityCalendar } from "./elements/test-activity-calendar"; import * as Loader from "./elements/loader"; import { Badge, CustomTheme } from "@monkeytype/schemas/users"; @@ -37,6 +33,7 @@ import { } from "./ape/server-configuration"; import { Connection } from "@monkeytype/schemas/connections"; import { createStore, unwrap } from "solid-js/store"; +import { GetTestActivityResponse } from "@monkeytype/contracts/users"; const [snapshot, setSnapshotStore] = createStore< // oxlint-disable-next-line typescript/no-unnecessary-type-arguments @@ -72,7 +69,6 @@ export function setSnapshot( ): void { if (newSnapshot === undefined) { setSnapshotStore({}); - testActivityByYear.clear(); if (options?.dispatchEvent !== false) { AuthEvent.dispatch({ @@ -1126,63 +1122,19 @@ export function addBadge(badge: Badge): void { setSnapshot(snapshot, { dispatchEvent: false }); } -const testActivityByYear = new Map(); - -export async function getTestActivityCalendar( - yearString: string, -): Promise { - const dbSnapshot = getSnapshot(); - if (!isAuthenticated() || dbSnapshot === undefined) return undefined; - - if (yearString === "current") { - const calendar = new ModifiableTestActivityCalendar( - dbSnapshot.testActivityData?.testsByDays ?? [], - new Date(dbSnapshot.testActivityData?.lastDay ?? Date.now()), - firstDayOfTheWeek, - ); - return calendar; - } - - const currentYear = new Date().getFullYear().toString(); - if (yearString === currentYear) { - const calendar = new ModifiableTestActivityCalendar( - dbSnapshot.testActivityData?.testsByDays ?? [], - new Date(dbSnapshot.testActivityData?.lastDay ?? Date.now()), - firstDayOfTheWeek, - ); - return calendar.getFullYearCalendar(); - } - - if (testActivityByYear.size === 0) { - if (!ConnectionState.get()) { - return undefined; - } - - Loader.show(); - const response = await Ape.users.getTestActivity(); - if (response.status !== 200) { - Notifications.add("Error getting test activities", -1, { response }); - Loader.hide(); - return undefined; - } - - for (const year in response.body.data) { - if (year === currentYear) continue; - const testsByDays = response.body.data[year] ?? []; - const lastDay = Dates.addDays( - new Date(parseInt(year), 0, 1), - testsByDays.length, - ); - - testActivityByYear.set( - year, - new TestActivityCalendar(testsByDays, lastDay, firstDayOfTheWeek, true), - ); - } +export async function getTestActivity(): Promise< + GetTestActivityResponse["data"] | undefined +> { + Loader.show(); + const response = await Ape.users.getTestActivity(); + if (response.status !== 200) { + Notifications.add("Error getting test activity", -1, { response }); Loader.hide(); + return undefined; } + Loader.hide(); - return testActivityByYear.get(yearString); + return response.body.data ?? undefined; } export function mergeConnections(connections: Connection[]): void { diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 5eeef3cb5ae9..9fc2129d9a13 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -1,6 +1,5 @@ import SlimSelect from "slim-select"; import { DataObjectPartial } from "slim-select/store"; -import { getTestActivityCalendar } from "../db"; import * as ServerConfiguration from "../ape/server-configuration"; import * as DB from "../db"; import { @@ -10,9 +9,29 @@ import { import { safeNumber } from "@monkeytype/util/numbers"; import { TestActivity } from "@monkeytype/schemas/users"; import { getFirstDayOfTheWeek } from "../utils/date-and-time"; +import { addDays } from "date-fns/addDays"; +import * as AuthEvent from "../observables/auth-event"; let yearSelector: SlimSelect | undefined = undefined; let calendar: TestActivityCalendar | undefined = undefined; +let activityByYear: Map | undefined = undefined; + +async function initActivityByYear(): Promise { + if (activityByYear !== undefined) return; + activityByYear = new Map(); + + const data = await DB.getTestActivity(); + if (data === undefined) return; + + for (const year in data) { + const testsByDays = data[year] as (number | null)[]; + const lastDay = addDays(new Date(parseInt(year), 0, 1), testsByDays.length); + activityByYear.set( + year, + new TestActivityCalendar(testsByDays, lastDay, getFirstDayOfTheWeek()), + ); + } +} export function init( element: HTMLElement, @@ -147,7 +166,10 @@ function getYearSelector(element: HTMLElement): SlimSelect { // oxlint-disable-next-line no-unsafe-call yearSelector?.disable(); const selected = newVal[0]?.value as string; - const activity = await getTestActivityCalendar(selected); + if (activityByYear === undefined) { + await initActivityByYear(); + } + const activity = activityByYear?.get(selected); update(element, activity); // oxlint-disable-next-line no-unsafe-call if ((yearSelector?.getData() ?? []).length > 1) { @@ -196,3 +218,9 @@ function updateLabels(element: HTMLElement, firstDayOfWeek: number): void { (element.querySelector(".daysFull") as HTMLElement).innerHTML = buildHtml(); (element.querySelector(".days") as HTMLElement).innerHTML = buildHtml(3); } + +AuthEvent.subscribe((data) => { + if (data.type === "snapshotUpdated" && DB.getSnapshot() === undefined) { + activityByYear?.clear(); + } +}); From db7ad202093586983396d89699d507eb5cbe4454 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 20:48:09 +0100 Subject: [PATCH 06/10] rewrite --- .../src/ts/elements/test-activity-calendar.ts | 32 ++++++++--------- frontend/src/ts/elements/test-activity.ts | 35 +++++++++++++++---- 2 files changed, 44 insertions(+), 23 deletions(-) diff --git a/frontend/src/ts/elements/test-activity-calendar.ts b/frontend/src/ts/elements/test-activity-calendar.ts index 1ee8ec835599..2c03d100caae 100644 --- a/frontend/src/ts/elements/test-activity-calendar.ts +++ b/frontend/src/ts/elements/test-activity-calendar.ts @@ -37,6 +37,7 @@ export class TestActivityCalendar implements TestActivityCalendar { protected endDay: Date; protected isFullYear: boolean; public firstDayOfWeek: Day; + protected lastDay: Date; constructor( data: (number | null | undefined)[], @@ -45,6 +46,7 @@ export class TestActivityCalendar implements TestActivityCalendar { fullYear = false, ) { this.firstDayOfWeek = firstDayOfWeek; + this.lastDay = new UTCDateMini(lastDay); const local = new UTCDateMini(lastDay); const interval = this.getInterval(local, fullYear); @@ -211,14 +213,26 @@ export class TestActivityCalendar implements TestActivityCalendar { lastDay: this.endDay.getTime(), }; } + + getFullYearCalendar(): TestActivityCalendar { + const today = new Date(); + if (this.lastDay.getFullYear() !== new UTCDateMini(today).getFullYear()) { + return new TestActivityCalendar([], today, this.firstDayOfWeek, true); + } else { + return new TestActivityCalendar( + this.data, + this.lastDay, + this.firstDayOfWeek, + true, + ); + } + } } export class ModifiableTestActivityCalendar extends TestActivityCalendar implements ModifiableTestActivityCalendar { - private lastDay: Date; - constructor(data: (number | null)[], lastDay: Date, firstDayOfWeek: Day) { super(data, lastDay, firstDayOfWeek); this.lastDay = new UTCDateMini(lastDay); @@ -247,18 +261,4 @@ export class ModifiableTestActivityCalendar this.data = this.buildData(this.data, this.lastDay); } - - getFullYearCalendar(): TestActivityCalendar { - const today = new Date(); - if (this.lastDay.getFullYear() !== new UTCDateMini(today).getFullYear()) { - return new TestActivityCalendar([], today, this.firstDayOfWeek, true); - } else { - return new TestActivityCalendar( - this.data, - this.lastDay, - this.firstDayOfWeek, - true, - ); - } - } } diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index 9fc2129d9a13..bab44ce614f2 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -68,13 +68,19 @@ export function clear(element?: HTMLElement): void { element?.querySelector(".activity")?.replaceChildren(); } -function update(element: HTMLElement, calendar?: TestActivityCalendar): void { +function update( + element: HTMLElement, + calendar?: TestActivityCalendar, + fullYear = false, +): void { const container = element.querySelector(".activity"); if (container === null) { return; } + let calendarToShow = calendar; + container.innerHTML = ""; if (calendar === undefined) { @@ -84,7 +90,13 @@ function update(element: HTMLElement, calendar?: TestActivityCalendar): void { return; } - updateMonths(calendar.getMonths()); + if (fullYear) { + calendarToShow = calendar.getFullYearCalendar(); + } else { + calendarToShow = calendar; + } + + updateMonths(calendarToShow.getMonths()); element.querySelector(".nodata")?.classList.add("hidden"); const title = element.querySelector(".title"); @@ -94,7 +106,7 @@ function update(element: HTMLElement, calendar?: TestActivityCalendar): void { } } - for (const day of calendar.getDays()) { + for (const day of calendarToShow.getDays()) { const elem = document.createElement("div"); elem.setAttribute("data-level", day.level); if (day.label !== undefined) { @@ -166,11 +178,20 @@ function getYearSelector(element: HTMLElement): SlimSelect { // oxlint-disable-next-line no-unsafe-call yearSelector?.disable(); const selected = newVal[0]?.value as string; - if (activityByYear === undefined) { - await initActivityByYear(); + const currentYear = new Date().getFullYear().toString(); + + if (selected === "current") { + update(element, calendar, false); + } else if (selected === currentYear) { + update(element, calendar, true); + } else { + if (activityByYear === undefined) { + await initActivityByYear(); + } + const activity = activityByYear?.get(selected); + update(element, activity, true); } - const activity = activityByYear?.get(selected); - update(element, activity); + // oxlint-disable-next-line no-unsafe-call if ((yearSelector?.getData() ?? []).length > 1) { // oxlint-disable-next-line no-unsafe-call From a3ff7548917174afb4cabc7bb34cfcc20c1bd5b7 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 20:55:52 +0100 Subject: [PATCH 07/10] dont clone --- frontend/src/ts/db.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 384146901ac0..7ed23efc04a2 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -60,7 +60,7 @@ export function getSnapshot(): Snapshot | undefined { if (Object.keys(snapshot).length === 0) { return undefined; } - return structuredClone(unwrap(snapshot)) as Snapshot; + return unwrap(snapshot) as Snapshot; } export function setSnapshot( From c20d59290b344f4baf61e6a6ee9114f3786e098d Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 21:00:28 +0100 Subject: [PATCH 08/10] last --- frontend/src/ts/elements/test-activity-calendar.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/ts/elements/test-activity-calendar.ts b/frontend/src/ts/elements/test-activity-calendar.ts index 2c03d100caae..a75a52083589 100644 --- a/frontend/src/ts/elements/test-activity-calendar.ts +++ b/frontend/src/ts/elements/test-activity-calendar.ts @@ -210,7 +210,7 @@ export class TestActivityCalendar implements TestActivityCalendar { getRawData(): TestActivity { return { testsByDays: this.data.map((v) => (v === undefined ? null : v)), - lastDay: this.endDay.getTime(), + lastDay: this.lastDay.getTime(), }; } From 700853dfd8857b57349d22ee758d76128c5fcd9a Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 21:00:58 +0100 Subject: [PATCH 09/10] remove --- frontend/src/ts/db.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index 7ed23efc04a2..a83e29b8ed74 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -208,11 +208,6 @@ export async function initSnapshot(): Promise { snap.allTimeLbs = userData.allTimeLbs; if (userData.testActivity !== undefined) { - // snap.testActivity = new ModifiableTestActivityCalendar( - // userData.testActivity.testsByDays, - // new Date(userData.testActivity.lastDay), - // firstDayOfTheWeek, - // ); snap.testActivityData = userData.testActivity; } From a5bc665dbd84064954a0d1803e9d0ff55fa35d70 Mon Sep 17 00:00:00 2001 From: Miodec Date: Thu, 15 Jan 2026 21:02:04 +0100 Subject: [PATCH 10/10] fix --- frontend/src/ts/elements/test-activity.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/frontend/src/ts/elements/test-activity.ts b/frontend/src/ts/elements/test-activity.ts index bab44ce614f2..ee3deb1f3f7d 100644 --- a/frontend/src/ts/elements/test-activity.ts +++ b/frontend/src/ts/elements/test-activity.ts @@ -43,7 +43,7 @@ export function init( return; } - calendar ??= new TestActivityCalendar( + calendar = new TestActivityCalendar( testActivityData.testsByDays, new Date(testActivityData.lastDay), getFirstDayOfTheWeek(), @@ -79,8 +79,6 @@ function update( return; } - let calendarToShow = calendar; - container.innerHTML = ""; if (calendar === undefined) { @@ -90,6 +88,7 @@ function update( return; } + let calendarToShow: TestActivityCalendar; if (fullYear) { calendarToShow = calendar.getFullYearCalendar(); } else {