fix: default snapshot and config not actually cloning (@miodec) (#6333)

!nuf Closes #6280
This commit is contained in:
Jack 2025-03-04 18:52:51 +01:00 committed by GitHub
parent b52391ec7b
commit b84f400113
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 161 additions and 133 deletions

View file

@ -1,5 +1,5 @@
import { getDefaultConfig } from "../../src/ts/constants/default-config";
import { migrateConfig } from "../../src/ts/utils/config";
import DefaultConfig from "../../src/ts/constants/default-config";
import {
PartialConfig,
ShowAverageSchema,
@ -11,8 +11,8 @@ describe("config.ts", () => {
const partialConfig = {} as PartialConfig;
const result = migrateConfig(partialConfig);
expect(result).toEqual(expect.objectContaining(DefaultConfig));
for (const [key, value] of Object.entries(DefaultConfig)) {
expect(result).toEqual(expect.objectContaining(getDefaultConfig()));
for (const [key, value] of Object.entries(getDefaultConfig())) {
expect(result).toHaveProperty(key, value);
}
});
@ -22,7 +22,7 @@ describe("config.ts", () => {
} as PartialConfig;
const result = migrateConfig(partialConfig);
expect(result).toEqual(expect.objectContaining(DefaultConfig));
expect(result).toEqual(expect.objectContaining(getDefaultConfig()));
expect(result).not.toHaveProperty("legacy");
});
it("should correctly merge properties of various types", () => {

View file

@ -1,5 +1,5 @@
import { getDefaultConfig } from "../../src/ts/constants/default-config";
import { Formatting } from "../../src/ts/utils/format";
import DefaultConfig from "../../src/ts/constants/default-config";
import { Config } from "@monkeytype/contracts/schemas/configs";
describe("format.ts", () => {
@ -274,6 +274,6 @@ describe("format.ts", () => {
});
function getInstance(config?: Partial<Config>): Formatting {
const target: Config = { ...DefaultConfig, ...config };
const target: Config = { ...getDefaultConfig(), ...config };
return new Formatting(target);
}

View file

@ -7,7 +7,6 @@ import {
isConfigValueValid,
} from "./config-validation";
import * as ConfigEvent from "./observables/config-event";
import DefaultConfig from "./constants/default-config";
import { isAuthenticated } from "./firebase";
import * as AnalyticsController from "./controllers/analytics-controller";
import * as AccountButton from "./elements/account-button";
@ -29,14 +28,15 @@ import { Language, LanguageSchema } from "@monkeytype/contracts/schemas/util";
import { LocalStorageWithSchema } from "./utils/local-storage-with-schema";
import { migrateConfig } from "./utils/config";
import { roundTo1 } from "@monkeytype/util/numbers";
import { getDefaultConfig } from "./constants/default-config";
const configLS = new LocalStorageWithSchema({
key: "config",
schema: ConfigSchemas.ConfigSchema,
fallback: DefaultConfig,
fallback: getDefaultConfig(),
migrate: (value, _issues) => {
if (!isObject(value)) {
return DefaultConfig;
return getDefaultConfig();
}
//todo maybe send a full config to db so that it removes legacy values
@ -47,7 +47,7 @@ const configLS = new LocalStorageWithSchema({
let loadDone: (value?: unknown) => void;
const config = {
...DefaultConfig,
...getDefaultConfig(),
};
let configToSend = {} as Config;
@ -1095,7 +1095,7 @@ export function setTimeConfig(
time: ConfigSchemas.TimeConfig,
nosave?: boolean
): boolean {
time = isNaN(time) || time < 0 ? DefaultConfig.time : time;
time = isNaN(time) || time < 0 ? getDefaultConfig().time : time;
if (!isConfigValueValid("time", time, ConfigSchemas.TimeConfigSchema))
return false;
@ -1168,7 +1168,7 @@ export function setWordCount(
nosave?: boolean
): boolean {
wordCount =
wordCount < 0 || wordCount > 100000 ? DefaultConfig.words : wordCount;
wordCount < 0 || wordCount > 100000 ? getDefaultConfig().words : wordCount;
if (!isConfigValueValid("words", wordCount, ConfigSchemas.WordCountSchema))
return false;
@ -1382,7 +1382,7 @@ export function setAutoSwitchTheme(
return false;
}
boolean = boolean ?? DefaultConfig.autoSwitchTheme;
boolean = boolean ?? getDefaultConfig().autoSwitchTheme;
config.autoSwitchTheme = boolean;
saveToLocalStorage("autoSwitchTheme", nosave);
ConfigEvent.dispatch("autoSwitchTheme", config.autoSwitchTheme);
@ -1985,9 +1985,9 @@ export async function apply(
ConfigEvent.dispatch("fullConfigChange");
const configObj = configToApply as Config;
(Object.keys(DefaultConfig) as (keyof Config)[]).forEach((configKey) => {
(Object.keys(getDefaultConfig()) as (keyof Config)[]).forEach((configKey) => {
if (configObj[configKey] === undefined) {
const newValue = DefaultConfig[configKey];
const newValue = getDefaultConfig()[configKey];
(configObj[configKey] as typeof newValue) = newValue;
}
});
@ -2095,7 +2095,7 @@ export async function apply(
}
export async function reset(): Promise<void> {
await apply(DefaultConfig);
await apply(getDefaultConfig());
await DB.resetConfig();
saveFullConfigToLocalStorage(true);
}
@ -2116,7 +2116,7 @@ export function getConfigChanges(): Partial<Config> {
const configChanges: Partial<Config> = {};
typedKeys(config)
.filter((key) => {
return config[key] !== DefaultConfig[key];
return config[key] !== getDefaultConfig()[key];
})
.forEach((key) => {
//@ts-expect-error this is fine

View file

@ -105,4 +105,6 @@ const obj = {
maxLineWidth: 0,
} as Config;
export default deepClone(obj);
export function getDefaultConfig(): Config {
return deepClone(obj);
}

View file

@ -1,5 +1,94 @@
import {
ResultFilters,
User,
UserProfileDetails,
UserTag,
} from "@monkeytype/contracts/schemas/users";
import { deepClone } from "../utils/misc";
import defaultConfig from "./default-config";
import { getDefaultConfig } from "./default-config";
import { Mode } from "@monkeytype/contracts/schemas/shared";
import { Result } from "@monkeytype/contracts/schemas/results";
import { Config } from "@monkeytype/contracts/schemas/configs";
import {
ModifiableTestActivityCalendar,
TestActivityCalendar,
} from "../elements/test-activity-calendar";
import { Preset } from "@monkeytype/contracts/schemas/presets";
export type SnapshotUserTag = UserTag & {
active?: boolean;
display: string;
};
export type SnapshotResult<M extends Mode> = Omit<
Result<M>,
| "_id"
| "bailedOut"
| "blindMode"
| "lazyMode"
| "difficulty"
| "funbox"
| "language"
| "numbers"
| "punctuation"
| "quoteLength"
| "restartCount"
| "incompleteTestSeconds"
| "afkDuration"
| "tags"
> & {
_id: string;
bailedOut: boolean;
blindMode: boolean;
lazyMode: boolean;
difficulty: string;
funbox: string;
language: string;
numbers: boolean;
punctuation: boolean;
quoteLength: number;
restartCount: number;
incompleteTestSeconds: number;
afkDuration: number;
tags: string[];
};
export type Snapshot = Omit<
User,
| "timeTyping"
| "startedTests"
| "completedTests"
| "profileDetails"
| "streak"
| "resultFilterPresets"
| "tags"
| "xp"
| "testActivity"
> & {
typingStats: {
timeTyping: number;
startedTests: number;
completedTests: number;
};
details?: UserProfileDetails;
inboxUnreadSize: number;
streak: number;
maxStreak: number;
filterPresets: ResultFilters[];
isPremium: boolean;
streakHourOffset?: number;
config: Config;
tags: SnapshotUserTag[];
presets: SnapshotPreset[];
results?: SnapshotResult<Mode>[];
xp: number;
testActivity?: ModifiableTestActivityCalendar;
testActivityByYear?: { [key: string]: TestActivityCalendar };
};
export type SnapshotPreset = Preset & {
display: string;
};
const defaultSnap = {
results: undefined,
@ -14,7 +103,7 @@ const defaultSnap = {
email: "",
uid: "",
isPremium: false,
config: defaultConfig,
config: getDefaultConfig(),
customThemes: [],
presets: [],
tags: [],
@ -42,6 +131,8 @@ const defaultSnap = {
60: { english: { count: 0, rank: 0 } },
},
},
};
} as Snapshot;
export default deepClone(defaultSnap);
export function getDefaultSnapshot(): Snapshot {
return deepClone(defaultSnap);
}

View file

@ -49,6 +49,7 @@ import { FirebaseError } from "firebase/app";
import * as PSA from "../elements/psa";
import defaultResultFilters from "../constants/default-result-filters";
import { getActiveFunboxesWithFunction } from "../test/funbox/list";
import { Snapshot } from "../constants/default-snapshot";
export const gmailProvider = new GoogleAuthProvider();
export const githubProvider = new GithubAuthProvider();
@ -124,7 +125,7 @@ async function getDataAndInit(): Promise<boolean> {
LoadingPage.updateBar(45);
}
LoadingPage.updateText("Applying settings...");
const snapshot = DB.getSnapshot() as DB.Snapshot;
const snapshot = DB.getSnapshot() as Snapshot;
AccountButton.update(snapshot);
Alerts.setNotificationBubbleVisible(snapshot.inboxUnreadSize > 0);
showFavoriteQuoteLength();

View file

@ -5,6 +5,7 @@ import * as Notifications from "../elements/notifications";
import * as TestLogic from "../test/test-logic";
import { migrateConfig, replaceLegacyValues } from "../utils/config";
import * as TagController from "./tag-controller";
import { SnapshotPreset } from "../constants/default-snapshot";
export async function apply(_id: string): Promise<void> {
const snapshot = DB.getSnapshot();
@ -41,7 +42,7 @@ export async function apply(_id: string): Promise<void> {
});
UpdateConfig.saveFullConfigToLocalStorage();
}
function isPartialPreset(preset: DB.SnapshotPreset): boolean {
function isPartialPreset(preset: SnapshotPreset): boolean {
return preset.settingGroups !== undefined && preset.settingGroups !== null;
}

View file

@ -1,7 +1,6 @@
import Ape from "./ape";
import * as Notifications from "./elements/notifications";
import * as LoadingPage from "./pages/loading";
import DefaultConfig from "./constants/default-config";
import { isAuthenticated } from "./firebase";
import * as ConnectionState from "./states/connection";
import { lastElementFromArray } from "./utils/arrays";
@ -13,14 +12,7 @@ import {
} from "./elements/test-activity-calendar";
import * as Loader from "./elements/loader";
import {
Badge,
CustomTheme,
ResultFilters,
User,
UserProfileDetails,
UserTag,
} from "@monkeytype/contracts/schemas/users";
import { Badge, CustomTheme } from "@monkeytype/contracts/schemas/users";
import { Config, Difficulty } from "@monkeytype/contracts/schemas/configs";
import {
Mode,
@ -28,86 +20,16 @@ import {
PersonalBest,
PersonalBests,
} from "@monkeytype/contracts/schemas/shared";
import { Preset } from "@monkeytype/contracts/schemas/presets";
import defaultSnapshot from "./constants/default-snapshot";
import { Result } from "@monkeytype/contracts/schemas/results";
import {
getDefaultSnapshot,
Snapshot,
SnapshotPreset,
SnapshotResult,
SnapshotUserTag,
} from "./constants/default-snapshot";
import { getDefaultConfig } from "./constants/default-config";
import { FunboxMetadata } from "../../../packages/funbox/src/types";
export type SnapshotUserTag = UserTag & {
active?: boolean;
display: string;
};
export type SnapshotResult<M extends Mode> = Omit<
Result<M>,
| "_id"
| "bailedOut"
| "blindMode"
| "lazyMode"
| "difficulty"
| "funbox"
| "language"
| "numbers"
| "punctuation"
| "quoteLength"
| "restartCount"
| "incompleteTestSeconds"
| "afkDuration"
| "tags"
> & {
_id: string;
bailedOut: boolean;
blindMode: boolean;
lazyMode: boolean;
difficulty: string;
funbox: string;
language: string;
numbers: boolean;
punctuation: boolean;
quoteLength: number;
restartCount: number;
incompleteTestSeconds: number;
afkDuration: number;
tags: string[];
};
export type Snapshot = Omit<
User,
| "timeTyping"
| "startedTests"
| "completedTests"
| "profileDetails"
| "streak"
| "resultFilterPresets"
| "tags"
| "xp"
| "testActivity"
> & {
typingStats: {
timeTyping: number;
startedTests: number;
completedTests: number;
};
details?: UserProfileDetails;
inboxUnreadSize: number;
streak: number;
maxStreak: number;
filterPresets: ResultFilters[];
isPremium: boolean;
streakHourOffset?: number;
config: Config;
tags: SnapshotUserTag[];
presets: SnapshotPreset[];
results?: SnapshotResult<Mode>[];
xp: number;
testActivity?: ModifiableTestActivityCalendar;
testActivityByYear?: { [key: string]: TestActivityCalendar };
};
export type SnapshotPreset = Preset & {
display: string;
};
let dbSnapshot: Snapshot | undefined;
export class SnapshotInitError extends Error {
@ -147,7 +69,7 @@ export function setSnapshot(newSnapshot: Snapshot | undefined): void {
export async function initSnapshot(): Promise<Snapshot | number | boolean> {
//send api request with token that returns tags, presets, and data needed for snap
const snap = defaultSnapshot as Snapshot;
const snap = getDefaultSnapshot();
try {
if (!isAuthenticated()) return false;
// if (ActivePage.get() === "loading") {
@ -260,7 +182,7 @@ export async function initSnapshot(): Promise<Snapshot | number | boolean> {
// LoadingPage.updateText("Downloading config...");
if (configData === undefined || configData === null) {
snap.config = {
...DefaultConfig,
...getDefaultConfig(),
};
} else {
snap.config = migrateConfig(configData);
@ -340,7 +262,7 @@ export async function initSnapshot(): Promise<Snapshot | number | boolean> {
dbSnapshot = snap;
return dbSnapshot;
} catch (e) {
dbSnapshot = defaultSnapshot;
dbSnapshot = getDefaultSnapshot();
throw e;
}
}

View file

@ -4,8 +4,8 @@ import {
SupportsFlags,
} from "../controllers/user-flag-controller";
import { isAuthenticated } from "../firebase";
import { Snapshot } from "../db";
import * as XpBar from "./xp-bar";
import { Snapshot } from "../constants/default-snapshot";
let usingAvatar = false;

View file

@ -17,6 +17,7 @@ import {
import { LocalStorageWithSchema } from "../../utils/local-storage-with-schema";
import defaultResultFilters from "../../constants/default-result-filters";
import { getAllFunboxes } from "@monkeytype/funbox";
import { SnapshotUserTag } from "../../constants/default-snapshot";
export function mergeWithDefaultFilters(
filters: Partial<ResultFilters>
@ -270,7 +271,7 @@ function setAllFilters(group: ResultFiltersGroup, value: boolean): void {
});
}
export function loadTags(tags: DB.SnapshotUserTag[]): void {
export function loadTags(tags: SnapshotUserTag[]): void {
tags.forEach((tag) => {
defaultResultFilters.tags[tag._id] = true;
});

View file

@ -15,9 +15,10 @@ import { UserProfile, RankAndCount } from "@monkeytype/contracts/schemas/users";
import { abbreviateNumber, convertRemToPixels } from "../utils/numbers";
import { secondsToString } from "../utils/date-and-time";
import { Auth } from "../firebase";
import { Snapshot } from "../constants/default-snapshot";
type ProfileViewPaths = "profile" | "account";
type UserProfileOrSnapshot = UserProfile | DB.Snapshot;
type UserProfileOrSnapshot = UserProfile | Snapshot;
//this is probably the dirtiest code ive ever written
@ -130,7 +131,7 @@ export async function update(
const results = DB.getSnapshot()?.results;
const lastResult = results?.[0];
const streakOffset = (profile as DB.Snapshot).streakHourOffset;
const streakOffset = (profile as Snapshot).streakHourOffset;
const dayInMilis = 1000 * 60 * 60 * 24;

View file

@ -11,7 +11,6 @@ import {
PresetTypeSchema,
} from "@monkeytype/contracts/schemas/presets";
import { getPreset } from "../controllers/preset-controller";
import defaultConfig from "../constants/default-config";
import {
ConfigGroupName,
ConfigGroupNameSchema,
@ -19,6 +18,8 @@ import {
ConfigKey,
Config as ConfigType,
} from "@monkeytype/contracts/schemas/configs";
import { getDefaultConfig } from "../constants/default-config";
import { SnapshotPreset } from "../constants/default-snapshot";
const state = {
presetType: "full" as PresetType,
@ -275,12 +276,12 @@ async function apply(): Promise<void> {
}),
display: propPresetName,
_id: response.body.data.presetId,
} as DB.SnapshotPreset);
} as SnapshotPreset);
}
} else if (action === "edit") {
const preset = snapshotPresets.filter(
(preset: DB.SnapshotPreset) => preset._id === presetId
)[0] as DB.SnapshotPreset;
(preset: SnapshotPreset) => preset._id === presetId
)[0] as SnapshotPreset;
if (preset === undefined) {
Notifications.add("Preset not found", -1);
return;
@ -325,7 +326,7 @@ async function apply(): Promise<void> {
);
} else {
Notifications.add("Preset removed", 1);
snapshotPresets.forEach((preset: DB.SnapshotPreset, index: number) => {
snapshotPresets.forEach((preset: SnapshotPreset, index: number) => {
if (preset._id === presetId) {
snapshotPresets.splice(index, 1);
}
@ -345,6 +346,7 @@ function getPartialConfigChanges(
configChanges: Partial<ConfigType>
): Partial<ConfigType> {
const activeConfigChanges: Partial<ConfigType> = {};
const defaultConfig = getDefaultConfig();
(Object.keys(defaultConfig) as ConfigKey[])
.filter((settingName) => {

View file

@ -4,8 +4,9 @@ import * as Notifications from "../elements/notifications";
import * as Loader from "../elements/loader";
// import * as Settings from "../pages/settings";
import * as ConnectionState from "../states/connection";
import { getSnapshot, setSnapshot, Snapshot } from "../db";
import { getSnapshot, setSnapshot } from "../db";
import AnimatedModal from "../utils/animated-modal";
import { Snapshot } from "../constants/default-snapshot";
export function show(): void {
if (!ConnectionState.get()) {

View file

@ -37,6 +37,7 @@ import {
import { ResultFiltersGroupItem } from "@monkeytype/contracts/schemas/users";
import { findLineByLeastSquares } from "../utils/numbers";
import defaultResultFilters from "../constants/default-result-filters";
import { SnapshotResult } from "../constants/default-snapshot";
let filterDebug = false;
//toggle filterdebug
@ -47,7 +48,7 @@ export function toggleFilterDebug(): void {
}
}
let filteredResults: DB.SnapshotResult<Mode>[] = [];
let filteredResults: SnapshotResult<Mode>[] = [];
let visibleTableLines = 0;
function loadMoreLines(lineIndex?: number): void {
@ -1071,7 +1072,7 @@ function sortAndRefreshHistory(
$(headerClass).append('<i class="fas fa-sort-up", aria-hidden="true"></i>');
}
const temp: DB.SnapshotResult<Mode>[] = [];
const temp: SnapshotResult<Mode>[] = [];
const parsedIndexes: number[] = [];
while (temp.length < filteredResults.length) {

View file

@ -30,6 +30,7 @@ import {
checkCompatibility,
} from "@monkeytype/funbox";
import { getActiveFunboxNames } from "../test/funbox/list";
import { SnapshotPreset } from "../constants/default-snapshot";
type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
@ -793,7 +794,7 @@ function refreshTagsSettingsSection(): void {
function refreshPresetsSettingsSection(): void {
if (isAuthenticated() && DB.getSnapshot()) {
const presetsEl = $(".pageSettings .section.presets .presetsList").empty();
DB.getSnapshot()?.presets?.forEach((preset: DB.SnapshotPreset) => {
DB.getSnapshot()?.presets?.forEach((preset: SnapshotPreset) => {
presetsEl.append(`
<div class="buttons preset" data-id="${preset._id}" data-name="${preset.name}" data-display="${preset.display}">
<button class="presetButton">${preset.display}</button>

View file

@ -43,6 +43,7 @@ import {
isFunboxActiveWithProperty,
} from "./funbox/list";
import { getFunboxesFromString } from "@monkeytype/funbox";
import { SnapshotUserTag } from "../constants/default-snapshot";
let result: CompletedEvent;
let maxChartVal: number;
@ -552,7 +553,7 @@ export function showConfetti(): void {
}
async function updateTags(dontSave: boolean): Promise<void> {
const activeTags: DB.SnapshotUserTag[] = [];
const activeTags: SnapshotUserTag[] = [];
const userTagsCount = DB.getSnapshot()?.tags?.length ?? 0;
try {
DB.getSnapshot()?.tags?.forEach((tag) => {

View file

@ -71,6 +71,7 @@ import {
} from "./funbox/list";
import { getFunboxesFromString } from "@monkeytype/funbox";
import * as CompositionState from "../states/composition";
import { SnapshotResult } from "../constants/default-snapshot";
let failReason = "";
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
@ -1199,7 +1200,7 @@ async function saveResult(
// into a snapshot result - might not cuase issues but worth investigating
const result = Misc.deepClone(
completedEvent
) as unknown as DB.SnapshotResult<Mode>;
) as unknown as SnapshotResult<Mode>;
result._id = data.insertedId;
if (data.isPb !== undefined && data.isPb) {
result.isPb = true;

View file

@ -3,9 +3,9 @@ import {
ConfigValue,
PartialConfig,
} from "@monkeytype/contracts/schemas/configs";
import DefaultConfig from "../constants/default-config";
import { typedKeys } from "./misc";
import * as ConfigSchemas from "@monkeytype/contracts/schemas/configs";
import { getDefaultConfig } from "../constants/default-config";
/**
* migrates possible outdated config and merges with the default config values
@ -17,9 +17,10 @@ export function migrateConfig(config: PartialConfig | object): Config {
}
function mergeWithDefaultConfig(config: PartialConfig): Config {
const defaultConfig = getDefaultConfig();
const mergedConfig = {} as Config;
for (const key of typedKeys(DefaultConfig)) {
const newValue = config[key] ?? (DefaultConfig[key] as ConfigValue);
for (const key of typedKeys(defaultConfig)) {
const newValue = config[key] ?? (defaultConfig[key] as ConfigValue);
//@ts-expect-error cant be bothered to deal with this
mergedConfig[key] = newValue;
}

View file

@ -4,6 +4,7 @@ import * as DB from "../db";
import * as TestLogic from "../test/test-logic";
import { deepClone } from "./misc";
import { Mode } from "@monkeytype/contracts/schemas/shared";
import { SnapshotResult } from "../constants/default-snapshot";
export async function syncNotSignedInLastResult(uid: string): Promise<void> {
const notSignedInLastResult = TestLogic.notSignedInLastResult;
@ -26,7 +27,7 @@ export async function syncNotSignedInLastResult(uid: string): Promise<void> {
// into a snapshot result - might not cuase issues but worth investigating
const result = deepClone(
notSignedInLastResult
) as unknown as DB.SnapshotResult<Mode>;
) as unknown as SnapshotResult<Mode>;
result._id = response.body.data.insertedId;
if (response.body.data.isPb) {
result.isPb = true;