mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-09-05 22:28:01 +08:00
refactor: add trycatch util (@miodec) (#6492)
Adds trycatch util to cleanup try catch code.
This commit is contained in:
parent
a59f99a533
commit
e06f7f41cf
24 changed files with 331 additions and 203 deletions
|
@ -63,6 +63,7 @@ import {
|
|||
checkCompatibility,
|
||||
stringToFunboxNames,
|
||||
} from "@monkeytype/funbox";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
try {
|
||||
if (!anticheatImplemented()) throw new Error("undefined");
|
||||
|
@ -318,13 +319,7 @@ export async function addResult(
|
|||
// );
|
||||
// return res.status(400).json({ message: "Time traveler detected" });
|
||||
|
||||
//get latest result ordered by timestamp
|
||||
let lastResultTimestamp: null | number = null;
|
||||
try {
|
||||
lastResultTimestamp = (await ResultDAL.getLastResult(uid)).timestamp;
|
||||
} catch (e) {
|
||||
//
|
||||
}
|
||||
const { data: lastResult } = await tryCatch(ResultDAL.getLastResult(uid));
|
||||
|
||||
//convert result test duration to miliseconds
|
||||
completedEvent.timestamp = Math.floor(Date.now() / 1000) * 1000;
|
||||
|
@ -333,13 +328,13 @@ export async function addResult(
|
|||
const testDurationMilis = completedEvent.testDuration * 1000;
|
||||
const incompleteTestsMilis = completedEvent.incompleteTestSeconds * 1000;
|
||||
const earliestPossible =
|
||||
(lastResultTimestamp ?? 0) + testDurationMilis + incompleteTestsMilis;
|
||||
(lastResult?.timestamp ?? 0) + testDurationMilis + incompleteTestsMilis;
|
||||
const nowNoMilis = Math.floor(Date.now() / 1000) * 1000;
|
||||
if (lastResultTimestamp && nowNoMilis < earliestPossible - 1000) {
|
||||
if (lastResult?.timestamp && nowNoMilis < earliestPossible - 1000) {
|
||||
void addLog(
|
||||
"invalid_result_spacing",
|
||||
{
|
||||
lastTimestamp: lastResultTimestamp,
|
||||
lastTimestamp: lastResult.timestamp,
|
||||
earliestPossible,
|
||||
now: nowNoMilis,
|
||||
testDuration: testDurationMilis,
|
||||
|
@ -786,17 +781,16 @@ async function calculateXp(
|
|||
const accuracyModifier = (acc - 50) / 50;
|
||||
|
||||
let dailyBonus = 0;
|
||||
let lastResultTimestamp: number | undefined;
|
||||
const { data: lastResult, error: getLastResultError } = await tryCatch(
|
||||
ResultDAL.getLastResult(uid)
|
||||
);
|
||||
|
||||
try {
|
||||
const { timestamp } = await ResultDAL.getLastResult(uid);
|
||||
lastResultTimestamp = timestamp;
|
||||
} catch (err) {
|
||||
Logger.error(`Could not fetch last result: ${err}`);
|
||||
if (getLastResultError) {
|
||||
Logger.error(`Could not fetch last result: ${getLastResultError}`);
|
||||
}
|
||||
|
||||
if (lastResultTimestamp) {
|
||||
const lastResultDay = getStartOfDayTimestamp(lastResultTimestamp);
|
||||
if (lastResult?.timestamp) {
|
||||
const lastResultDay = getStartOfDayTimestamp(lastResult.timestamp);
|
||||
const today = getCurrentDayTimestamp();
|
||||
if (lastResultDay !== today) {
|
||||
const proportionalXp = Math.round(currentTotalXp * 0.05);
|
||||
|
|
|
@ -88,13 +88,11 @@ import {
|
|||
} from "@monkeytype/contracts/users";
|
||||
import { MILLISECONDS_IN_DAY } from "@monkeytype/util/date-and-time";
|
||||
import { MonkeyRequest } from "../types";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
async function verifyCaptcha(captcha: string): Promise<void> {
|
||||
let verified = false;
|
||||
try {
|
||||
verified = await verify(captcha);
|
||||
} catch (e) {
|
||||
//fetch to recaptcha api can sometimes fail
|
||||
const { data: verified, error } = await tryCatch(verify(captcha));
|
||||
if (error) {
|
||||
throw new MonkeyError(
|
||||
422,
|
||||
"Request to the Captcha API failed, please try again later"
|
||||
|
@ -177,18 +175,19 @@ export async function sendVerificationEmail(
|
|||
);
|
||||
}
|
||||
|
||||
let link = "";
|
||||
try {
|
||||
link = await FirebaseAdmin()
|
||||
const { data: link, error } = await tryCatch(
|
||||
FirebaseAdmin()
|
||||
.auth()
|
||||
.generateEmailVerificationLink(email, {
|
||||
url: isDevEnvironment()
|
||||
? "http://localhost:3000"
|
||||
: "https://monkeytype.com",
|
||||
});
|
||||
} catch (e) {
|
||||
if (isFirebaseError(e)) {
|
||||
if (e.errorInfo.code === "auth/user-not-found") {
|
||||
})
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (isFirebaseError(error)) {
|
||||
if (error.errorInfo.code === "auth/user-not-found") {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
"Auth user not found when the user was found in the database. Contact support with this error message and your email",
|
||||
|
@ -198,11 +197,11 @@ export async function sendVerificationEmail(
|
|||
}),
|
||||
userInfo.uid
|
||||
);
|
||||
} else if (e.errorInfo.code === "auth/too-many-requests") {
|
||||
} else if (error.errorInfo.code === "auth/too-many-requests") {
|
||||
throw new MonkeyError(429, "Too many requests. Please try again later");
|
||||
} else if (
|
||||
e.errorInfo.code === "auth/internal-error" &&
|
||||
e.errorInfo.message.toLowerCase().includes("too_many_attempts")
|
||||
error.errorInfo.code === "auth/internal-error" &&
|
||||
error.errorInfo.message.toLowerCase().includes("too_many_attempts")
|
||||
) {
|
||||
throw new MonkeyError(
|
||||
429,
|
||||
|
@ -212,12 +211,12 @@ export async function sendVerificationEmail(
|
|||
throw new MonkeyError(
|
||||
500,
|
||||
"Firebase failed to generate an email verification link: " +
|
||||
e.errorInfo.message,
|
||||
JSON.stringify(e)
|
||||
error.errorInfo.message,
|
||||
JSON.stringify(error)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const message = getErrorMessage(e);
|
||||
const message = getErrorMessage(error);
|
||||
if (message === undefined) {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
|
@ -233,12 +232,13 @@ export async function sendVerificationEmail(
|
|||
throw new MonkeyError(
|
||||
500,
|
||||
"Failed to generate an email verification link: " + message,
|
||||
(e as Error).stack
|
||||
error.stack
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await emailQueue.sendVerificationEmail(email, userInfo.name, link);
|
||||
|
||||
return new MonkeyResponse("Email sent", null);
|
||||
|
@ -259,22 +259,20 @@ export async function sendForgotPasswordEmail(
|
|||
export async function deleteUser(req: MonkeyRequest): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
let userInfo:
|
||||
| Pick<UserDAL.DBUser, "banned" | "name" | "email" | "discordId">
|
||||
| undefined;
|
||||
|
||||
try {
|
||||
userInfo = await UserDAL.getPartialUser(uid, "delete user", [
|
||||
const { data: userInfo, error } = await tryCatch(
|
||||
UserDAL.getPartialUser(uid, "delete user", [
|
||||
"banned",
|
||||
"name",
|
||||
"email",
|
||||
"discordId",
|
||||
]);
|
||||
} catch (e) {
|
||||
if (e instanceof MonkeyError && e.status === 404) {
|
||||
])
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof MonkeyError && error.status === 404) {
|
||||
//userinfo was already deleted. We ignore this and still try to remove the other data
|
||||
} else {
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -533,12 +531,12 @@ function getRelevantUserInfo(user: UserDAL.DBUser): RelevantUserInfo {
|
|||
export async function getUser(req: MonkeyRequest): Promise<GetUserResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
|
||||
let userInfo: UserDAL.DBUser;
|
||||
try {
|
||||
userInfo = await UserDAL.getUser(uid, "get user");
|
||||
} catch (e) {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
|
||||
if (e.status === 404) {
|
||||
const { data: userInfo, error } = await tryCatch(
|
||||
UserDAL.getUser(uid, "get user")
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (error instanceof MonkeyError && error.status === 404) {
|
||||
//if the user is in the auth system but not in the db, its possible that the user was created by bypassing captcha
|
||||
//since there is no data in the database anyway, we can just delete the user from the auth system
|
||||
//and ask them to sign up again
|
||||
|
@ -564,7 +562,7 @@ export async function getUser(req: MonkeyRequest): Promise<GetUserResponse> {
|
|||
}
|
||||
}
|
||||
} else {
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -3,13 +3,17 @@ import { getMiddleware as getSwaggerMiddleware } from "swagger-stats";
|
|||
import { isDevEnvironment } from "../../utils/misc";
|
||||
import { readFileSync } from "fs";
|
||||
import Logger from "../../utils/logger";
|
||||
import { tryCatchSync } from "@monkeytype/util/trycatch";
|
||||
|
||||
function addSwaggerMiddlewares(app: Application): void {
|
||||
const openApiSpec = __dirname + "/../../../dist/static/api/openapi.json";
|
||||
let spec = {};
|
||||
try {
|
||||
spec = JSON.parse(readFileSync(openApiSpec, "utf8")) as string;
|
||||
} catch (err) {
|
||||
|
||||
const { data: spec, error } = tryCatchSync(
|
||||
() =>
|
||||
JSON.parse(readFileSync(openApiSpec, "utf8")) as Record<string, unknown>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
Logger.warning(
|
||||
`Cannot read openApi specification from ${openApiSpec}. Swagger stats will not fully work.`
|
||||
);
|
||||
|
@ -21,7 +25,7 @@ function addSwaggerMiddlewares(app: Application): void {
|
|||
uriPath: "/stats",
|
||||
authentication: !isDevEnvironment(),
|
||||
apdexThreshold: 100,
|
||||
swaggerSpec: spec,
|
||||
swaggerSpec: spec ?? {},
|
||||
onAuthenticate: (_req, username, password) => {
|
||||
return (
|
||||
username === process.env["STATS_USERNAME"] &&
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { SimpleGit, simpleGit } from "simple-git";
|
||||
import { simpleGit } from "simple-git";
|
||||
import { Collection, ObjectId } from "mongodb";
|
||||
import path from "path";
|
||||
import { existsSync, writeFileSync } from "fs";
|
||||
|
@ -10,6 +10,7 @@ import { ApproveQuote, Quote } from "@monkeytype/contracts/schemas/quotes";
|
|||
import { WithObjectId } from "../utils/misc";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { z } from "zod";
|
||||
import { tryCatchSync } from "@monkeytype/util/trycatch";
|
||||
|
||||
const JsonQuoteSchema = z.object({
|
||||
text: z.string(),
|
||||
|
@ -28,12 +29,12 @@ const QuoteDataSchema = z.object({
|
|||
|
||||
const PATH_TO_REPO = "../../../../monkeytype-new-quotes";
|
||||
|
||||
let git: SimpleGit | undefined;
|
||||
try {
|
||||
git = simpleGit(path.join(__dirname, PATH_TO_REPO));
|
||||
} catch (e) {
|
||||
console.error(`Failed to initialize git: ${e}`);
|
||||
git = undefined;
|
||||
const { data: git, error } = tryCatchSync(() =>
|
||||
simpleGit(path.join(__dirname, PATH_TO_REPO))
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error(`Failed to initialize git: ${error}`);
|
||||
}
|
||||
|
||||
type AddQuoteReturn = {
|
||||
|
@ -145,7 +146,7 @@ export async function approve(
|
|||
editSource: string | undefined,
|
||||
name: string
|
||||
): Promise<ApproveReturn> {
|
||||
if (git === undefined) throw new MonkeyError(500, "Git not available.");
|
||||
if (git === null) throw new MonkeyError(500, "Git not available.");
|
||||
//check mod status
|
||||
const targetQuote = await getNewQuoteCollection().findOne({
|
||||
_id: new ObjectId(quoteId),
|
||||
|
|
|
@ -8,8 +8,9 @@ import {
|
|||
import MonkeyError from "../utils/error";
|
||||
import * as db from "../init/db";
|
||||
|
||||
import { getUser, getTags, DBUser } from "./user";
|
||||
import { getUser, getTags } from "./user";
|
||||
import { DBResult } from "../utils/result";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
export const getResultCollection = (): Collection<DBResult> =>
|
||||
db.collection<DBResult>("results");
|
||||
|
@ -18,12 +19,8 @@ export async function addResult(
|
|||
uid: string,
|
||||
result: DBResult
|
||||
): Promise<{ insertedId: ObjectId }> {
|
||||
let user: DBUser | null = null;
|
||||
try {
|
||||
user = await getUser(uid, "add result");
|
||||
} catch (e) {
|
||||
user = null;
|
||||
}
|
||||
const { data: user } = await tryCatch(getUser(uid, "add result"));
|
||||
|
||||
if (!user) throw new MonkeyError(404, "User not found", "add result");
|
||||
if (result.uid === undefined) result.uid = uid;
|
||||
// result.ir = true;
|
||||
|
|
|
@ -8,6 +8,7 @@ import { recordEmail } from "../utils/prometheus";
|
|||
import type { EmailTaskContexts, EmailType } from "../queues/email-queue";
|
||||
import { isDevEnvironment } from "../utils/misc";
|
||||
import { getErrorMessage } from "../utils/error";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
type EmailMetadata = {
|
||||
subject: string;
|
||||
|
@ -109,14 +110,15 @@ export async function sendEmail(
|
|||
|
||||
type Result = { response: string; accepted: string[] };
|
||||
|
||||
let result: Result;
|
||||
try {
|
||||
result = (await transporter.sendMail(mailOptions)) as Result;
|
||||
} catch (e) {
|
||||
const { data: result, error } = await tryCatch(
|
||||
transporter.sendMail(mailOptions) as Promise<Result>
|
||||
);
|
||||
|
||||
if (error) {
|
||||
recordEmail(templateName, "fail");
|
||||
return {
|
||||
success: false,
|
||||
message: getErrorMessage(e) ?? "Unknown error",
|
||||
message: getErrorMessage(error) ?? "Unknown error",
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -11,6 +11,7 @@ import { getCurrentWeekTimestamp } from "@monkeytype/util/date-and-time";
|
|||
import MonkeyError from "../utils/error";
|
||||
import { omit } from "lodash";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { tryCatchSync } from "@monkeytype/util/trycatch";
|
||||
|
||||
type AddResultOpts = {
|
||||
entry: RedisXpLeaderboardEntry;
|
||||
|
@ -225,11 +226,11 @@ export class WeeklyXpLeaderboard {
|
|||
return null;
|
||||
}
|
||||
|
||||
// safely parse the result with error handling
|
||||
let parsed: RedisXpLeaderboardEntry;
|
||||
try {
|
||||
parsed = parseJsonWithSchema(result, RedisXpLeaderboardEntrySchema);
|
||||
} catch (error) {
|
||||
const { data: parsed, error } = tryCatchSync(() =>
|
||||
parseJsonWithSchema(result, RedisXpLeaderboardEntrySchema)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
throw new MonkeyError(
|
||||
500,
|
||||
`Failed to parse leaderboard entry: ${
|
||||
|
|
|
@ -19,6 +19,7 @@ import {
|
|||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { Mode } from "@monkeytype/contracts/schemas/shared";
|
||||
import { CompletedEvent } from "@monkeytype/contracts/schemas/results";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
let challengeLoading = false;
|
||||
|
||||
|
@ -219,11 +220,9 @@ export async function setup(challengeName: string): Promise<boolean> {
|
|||
|
||||
UpdateConfig.setFunbox("none");
|
||||
|
||||
let list;
|
||||
try {
|
||||
list = await JSONData.getChallengeList();
|
||||
} catch (e) {
|
||||
const message = Misc.createErrorMessage(e, "Failed to setup challenge");
|
||||
const { data: list, error } = await tryCatch(JSONData.getChallengeList());
|
||||
if (error) {
|
||||
const message = Misc.createErrorMessage(error, "Failed to setup challenge");
|
||||
Notifications.add(message, -1);
|
||||
ManualRestart.set();
|
||||
setTimeout(() => {
|
||||
|
|
|
@ -41,6 +41,7 @@ import {
|
|||
isFunboxActiveWithProperty,
|
||||
getActiveFunboxNames,
|
||||
} from "../test/funbox/list";
|
||||
import { tryCatchSync } from "@monkeytype/util/trycatch";
|
||||
|
||||
let dontInsertSpace = false;
|
||||
let correctShiftUsed = true;
|
||||
|
@ -344,18 +345,16 @@ async function handleSpace(): Promise<void> {
|
|||
TestState.activeWordIndex - TestUI.activeWordElementOffset - 1
|
||||
]?.offsetTop ?? 0
|
||||
);
|
||||
let nextTop: number;
|
||||
try {
|
||||
nextTop = Math.floor(
|
||||
|
||||
const { data: nextTop } = tryCatchSync(() =>
|
||||
Math.floor(
|
||||
document.querySelectorAll<HTMLElement>("#words .word")[
|
||||
TestState.activeWordIndex - TestUI.activeWordElementOffset
|
||||
]?.offsetTop ?? 0
|
||||
);
|
||||
} catch (e) {
|
||||
nextTop = 0;
|
||||
}
|
||||
)
|
||||
);
|
||||
|
||||
if (nextTop > currentTop) {
|
||||
if ((nextTop ?? 0) > currentTop) {
|
||||
void TestUI.lineJump(currentTop);
|
||||
} //end of line wrap
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import { cachedFetchJson } from "../utils/json-data";
|
|||
import { subscribe } from "../observables/config-event";
|
||||
import * as DB from "../db";
|
||||
import Ape from "../ape";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
export type Quote = {
|
||||
text: string;
|
||||
|
@ -59,20 +60,19 @@ class QuotesController {
|
|||
const normalizedLanguage = removeLanguageSize(language);
|
||||
|
||||
if (this.quoteCollection.language !== normalizedLanguage) {
|
||||
let data: QuoteData;
|
||||
try {
|
||||
data = await cachedFetchJson<QuoteData>(
|
||||
`quotes/${normalizedLanguage}.json`
|
||||
);
|
||||
} catch (e) {
|
||||
const { data, error } = await tryCatch(
|
||||
cachedFetchJson<QuoteData>(`quotes/${normalizedLanguage}.json`)
|
||||
);
|
||||
|
||||
if (error) {
|
||||
if (
|
||||
e instanceof Error &&
|
||||
(e?.message?.includes("404") ||
|
||||
e?.message?.includes("Content is not JSON"))
|
||||
error instanceof Error &&
|
||||
(error?.message?.includes("404") ||
|
||||
error?.message?.includes("Content is not JSON"))
|
||||
) {
|
||||
return defaultQuoteCollection;
|
||||
} else {
|
||||
throw e;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import * as Notifications from "../elements/notifications";
|
|||
import * as Loader from "../elements/loader";
|
||||
import * as AnalyticsController from "../controllers/analytics-controller";
|
||||
import { debounce } from "throttle-debounce";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
export let randomTheme: string | null = null;
|
||||
let isPreviewingTheme = false;
|
||||
|
@ -248,12 +249,10 @@ export async function clearPreview(applyTheme = true): Promise<void> {
|
|||
let themesList: string[] = [];
|
||||
|
||||
async function changeThemeList(): Promise<void> {
|
||||
let themes;
|
||||
try {
|
||||
themes = await JSONData.getThemesList();
|
||||
} catch (e) {
|
||||
const { data: themes, error } = await tryCatch(JSONData.getThemesList());
|
||||
if (error) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(e, "Failed to update random theme list")
|
||||
Misc.createErrorMessage(error, "Failed to update random theme list")
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
|
|
@ -18,6 +18,7 @@ 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";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
export function mergeWithDefaultFilters(
|
||||
filters: Partial<ResultFilters>
|
||||
|
@ -745,14 +746,16 @@ export async function appendButtons(
|
|||
): Promise<void> {
|
||||
selectChangeCallbackFn = selectChangeCallback;
|
||||
|
||||
let languageList;
|
||||
try {
|
||||
languageList = await JSONData.getLanguageList();
|
||||
} catch (e) {
|
||||
const { data: languageList, error } = await tryCatch(
|
||||
JSONData.getLanguageList()
|
||||
);
|
||||
|
||||
if (error) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(e, "Failed to append language buttons")
|
||||
Misc.createErrorMessage(error, "Failed to append language buttons")
|
||||
);
|
||||
}
|
||||
|
||||
if (languageList) {
|
||||
let html = "";
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ import * as ConfigEvent from "../../observables/config-event";
|
|||
import { isAuthenticated } from "../../firebase";
|
||||
import * as ActivePage from "../../states/active-page";
|
||||
import { CustomThemeColors } from "@monkeytype/contracts/schemas/configs";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
function updateActiveButton(): void {
|
||||
let activeThemeName = Config.theme;
|
||||
|
@ -172,12 +173,13 @@ export async function refreshButtons(): Promise<void> {
|
|||
activeThemeName = ThemeController.randomTheme;
|
||||
}
|
||||
|
||||
let themes;
|
||||
try {
|
||||
themes = await JSONData.getSortedThemesList();
|
||||
} catch (e) {
|
||||
const { data: themes, error } = await tryCatch(
|
||||
JSONData.getSortedThemesList()
|
||||
);
|
||||
|
||||
if (error) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Failed to refresh theme buttons"),
|
||||
Misc.createErrorMessage(error, "Failed to refresh theme buttons"),
|
||||
-1
|
||||
);
|
||||
return;
|
||||
|
|
|
@ -8,6 +8,7 @@ import AnimatedModal, {
|
|||
ShowOptions,
|
||||
} from "../utils/animated-modal";
|
||||
import { LayoutsList } from "../constants/layouts";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
type FilterPreset = {
|
||||
display: string;
|
||||
|
@ -98,19 +99,17 @@ const presets: Record<string, FilterPreset> = {
|
|||
|
||||
async function initSelectOptions(): Promise<void> {
|
||||
$("#wordFilterModal .languageInput").empty();
|
||||
|
||||
$("#wordFilterModal .layoutInput").empty();
|
||||
|
||||
$("wordFilterModal .presetInput").empty();
|
||||
|
||||
let LanguageList;
|
||||
const { data: LanguageList, error } = await tryCatch(
|
||||
JSONData.getLanguageList()
|
||||
);
|
||||
|
||||
try {
|
||||
LanguageList = await JSONData.getLanguageList();
|
||||
} catch (e) {
|
||||
if (error) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(
|
||||
e,
|
||||
error,
|
||||
"Failed to initialise word filter popup language list"
|
||||
)
|
||||
);
|
||||
|
@ -187,12 +186,12 @@ async function filter(language: string): Promise<string[]> {
|
|||
const regexcl = new RegExp(filterout, "i");
|
||||
const filteredWords = [];
|
||||
|
||||
let languageWordList;
|
||||
try {
|
||||
languageWordList = await JSONData.getLanguage(language);
|
||||
} catch (e) {
|
||||
const { data: languageWordList, error } = await tryCatch(
|
||||
JSONData.getLanguage(language)
|
||||
);
|
||||
if (error) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Failed to filter language words"),
|
||||
Misc.createErrorMessage(error, "Failed to filter language words"),
|
||||
-1
|
||||
);
|
||||
return [];
|
||||
|
|
|
@ -12,6 +12,7 @@ import {
|
|||
SpeedHistogram,
|
||||
} from "@monkeytype/contracts/schemas/public";
|
||||
import { getNumberWithMagnitude, numberWithSpaces } from "../utils/numbers";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
function reset(): void {
|
||||
$(".pageAbout .contributors").empty();
|
||||
|
@ -126,26 +127,24 @@ async function getStatsAndHistogramData(): Promise<void> {
|
|||
}
|
||||
|
||||
async function fill(): Promise<void> {
|
||||
let supporters: string[];
|
||||
try {
|
||||
supporters = await JSONData.getSupportersList();
|
||||
} catch (e) {
|
||||
const { data: supporters, error: supportersError } = await tryCatch(
|
||||
JSONData.getSupportersList()
|
||||
);
|
||||
if (supportersError) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Failed to get supporters"),
|
||||
Misc.createErrorMessage(supportersError, "Failed to get supporters"),
|
||||
-1
|
||||
);
|
||||
supporters = [];
|
||||
}
|
||||
|
||||
let contributors: string[];
|
||||
try {
|
||||
contributors = await JSONData.getContributorsList();
|
||||
} catch (e) {
|
||||
const { data: contributors, error: contributorsError } = await tryCatch(
|
||||
JSONData.getContributorsList()
|
||||
);
|
||||
if (contributorsError) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Failed to get contributors"),
|
||||
Misc.createErrorMessage(contributorsError, "Failed to get contributors"),
|
||||
-1
|
||||
);
|
||||
contributors = [];
|
||||
}
|
||||
|
||||
void getStatsAndHistogramData().then(() => {
|
||||
|
@ -154,7 +153,7 @@ async function fill(): Promise<void> {
|
|||
|
||||
const supportersEl = document.querySelector(".pageAbout .supporters");
|
||||
let supportersHTML = "";
|
||||
for (const supporter of supporters) {
|
||||
for (const supporter of supporters ?? []) {
|
||||
supportersHTML += `<div>${supporter}</div>`;
|
||||
}
|
||||
if (supportersEl) {
|
||||
|
@ -163,7 +162,7 @@ async function fill(): Promise<void> {
|
|||
|
||||
const contributorsEl = document.querySelector(".pageAbout .contributors");
|
||||
let contributorsHTML = "";
|
||||
for (const contributor of contributors) {
|
||||
for (const contributor of contributors ?? []) {
|
||||
contributorsHTML += `<div>${contributor}</div>`;
|
||||
}
|
||||
if (contributorsEl) {
|
||||
|
|
|
@ -33,6 +33,7 @@ import { getActiveFunboxNames } from "../test/funbox/list";
|
|||
import { SnapshotPreset } from "../constants/default-snapshot";
|
||||
import { LayoutsList } from "../constants/layouts";
|
||||
import { DataArrayPartial, Optgroup } from "slim-select/store";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
type SettingsGroups<T extends ConfigValue> = Record<string, SettingsGroup<T>>;
|
||||
|
||||
|
@ -461,13 +462,13 @@ async function fillSettingsPage(): Promise<void> {
|
|||
|
||||
// Language Selection Combobox
|
||||
|
||||
let languageGroups;
|
||||
try {
|
||||
languageGroups = await JSONData.getLanguageGroups();
|
||||
} catch (e) {
|
||||
const { data: languageGroups, error: getLanguageGroupsError } =
|
||||
await tryCatch(JSONData.getLanguageGroups());
|
||||
|
||||
if (getLanguageGroupsError) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(
|
||||
e,
|
||||
getLanguageGroupsError,
|
||||
"Failed to initialize settings language picker"
|
||||
)
|
||||
);
|
||||
|
@ -522,12 +523,15 @@ async function fillSettingsPage(): Promise<void> {
|
|||
select: keymapLayoutSelectElement,
|
||||
});
|
||||
|
||||
let themes;
|
||||
try {
|
||||
themes = await JSONData.getThemesList();
|
||||
} catch (e) {
|
||||
const { data: themes, error: getThemesListError } = await tryCatch(
|
||||
JSONData.getThemesList()
|
||||
);
|
||||
if (getThemesListError) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(e, "Failed to load themes into dropdown boxes")
|
||||
Misc.createErrorMessage(
|
||||
getThemesListError,
|
||||
"Failed to load themes into dropdown boxes"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -625,12 +629,15 @@ async function fillSettingsPage(): Promise<void> {
|
|||
|
||||
let fontsElHTML = "";
|
||||
|
||||
let fontsList;
|
||||
try {
|
||||
fontsList = await JSONData.getFontsList();
|
||||
} catch (e) {
|
||||
const { data: fontsList, error: getFontsListError } = await tryCatch(
|
||||
JSONData.getFontsList()
|
||||
);
|
||||
if (getFontsListError) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(e, "Failed to update fonts settings buttons")
|
||||
Misc.createErrorMessage(
|
||||
getFontsListError,
|
||||
"Failed to update fonts settings buttons"
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,6 +18,7 @@ import {
|
|||
getActiveFunboxesWithProperty,
|
||||
} from "./list";
|
||||
import { checkForcedConfig } from "./funbox-validation";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
export function toggleScript(...params: string[]): void {
|
||||
if (Config.funbox === "none") return;
|
||||
|
@ -118,12 +119,12 @@ export async function activate(funbox?: string): Promise<boolean | undefined> {
|
|||
|
||||
$("#wordsWrapper").removeClass("hidden");
|
||||
|
||||
let language;
|
||||
try {
|
||||
language = await JSONData.getCurrentLanguage(Config.language);
|
||||
} catch (e) {
|
||||
const { data: language, error } = await tryCatch(
|
||||
JSONData.getCurrentLanguage(Config.language)
|
||||
);
|
||||
if (error) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Failed to activate funbox"),
|
||||
Misc.createErrorMessage(error, "Failed to activate funbox"),
|
||||
-1
|
||||
);
|
||||
UpdateConfig.setFunbox("none", true);
|
||||
|
|
|
@ -32,8 +32,8 @@ export async function getCharFromEvent(
|
|||
|
||||
return altVersion || nonAltVersion || defaultVersion;
|
||||
}
|
||||
let layout;
|
||||
|
||||
let layout;
|
||||
try {
|
||||
layout = await JSONData.getLayout(Config.layout);
|
||||
} catch (e) {
|
||||
|
|
|
@ -74,6 +74,7 @@ import { getFunboxesFromString } from "@monkeytype/funbox";
|
|||
import * as CompositionState from "../states/composition";
|
||||
import { SnapshotResult } from "../constants/default-snapshot";
|
||||
import { WordGenError } from "../utils/word-gen-error";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
let failReason = "";
|
||||
const koInputVisual = document.getElementById("koInputVisual") as HTMLElement;
|
||||
|
@ -406,12 +407,12 @@ export async function init(): Promise<void> {
|
|||
TestInput.input.resetHistory();
|
||||
TestInput.input.current = "";
|
||||
|
||||
let language;
|
||||
try {
|
||||
language = await JSONData.getLanguage(Config.language);
|
||||
} catch (e) {
|
||||
const { data: language, error } = await tryCatch(
|
||||
JSONData.getLanguage(Config.language)
|
||||
);
|
||||
if (error) {
|
||||
Notifications.add(
|
||||
Misc.createErrorMessage(e, "Failed to load language"),
|
||||
Misc.createErrorMessage(error, "Failed to load language"),
|
||||
-1
|
||||
);
|
||||
}
|
||||
|
|
|
@ -4,6 +4,7 @@ import * as Strings from "../utils/strings";
|
|||
import * as JSONData from "../utils/json-data";
|
||||
import { z } from "zod";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { tryCatch } from "@monkeytype/util/trycatch";
|
||||
|
||||
export async function getTLD(
|
||||
languageGroup: JSONData.LanguageGroup
|
||||
|
@ -262,16 +263,16 @@ export async function getSection(language: string): Promise<JSONData.Section> {
|
|||
// get TLD for wikipedia according to language group
|
||||
let urlTLD = "en";
|
||||
|
||||
let currentLanguageGroup: JSONData.LanguageGroup | undefined;
|
||||
try {
|
||||
currentLanguageGroup = await JSONData.getCurrentGroup(language);
|
||||
} catch (e) {
|
||||
const { data: currentLanguageGroup, error } = await tryCatch(
|
||||
JSONData.getCurrentGroup(language)
|
||||
);
|
||||
if (error) {
|
||||
console.error(
|
||||
Misc.createErrorMessage(e, "Failed to find current language group")
|
||||
Misc.createErrorMessage(error, "Failed to find current language group")
|
||||
);
|
||||
}
|
||||
|
||||
if (currentLanguageGroup !== undefined) {
|
||||
if (currentLanguageGroup !== null && currentLanguageGroup !== undefined) {
|
||||
urlTLD = await getTLD(currentLanguageGroup);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@ import { ZodIssue } from "zod";
|
|||
import { deepClone } from "./misc";
|
||||
import { isZodError } from "@monkeytype/util/zod";
|
||||
import * as Notifications from "../elements/notifications";
|
||||
import { tryCatchSync } from "@monkeytype/util/trycatch";
|
||||
|
||||
export class LocalStorageWithSchema<T> {
|
||||
private key: string;
|
||||
|
@ -28,13 +29,13 @@ export class LocalStorageWithSchema<T> {
|
|||
return this.fallback;
|
||||
}
|
||||
|
||||
let jsonParsed: unknown;
|
||||
try {
|
||||
jsonParsed = JSON.parse(value);
|
||||
} catch (e) {
|
||||
const { data: jsonParsed, error } = tryCatchSync(
|
||||
() => JSON.parse(value) as unknown
|
||||
);
|
||||
if (error) {
|
||||
console.log(
|
||||
`Value from localStorage ${this.key} was not a valid JSON, using fallback`,
|
||||
e
|
||||
error
|
||||
);
|
||||
window.localStorage.removeItem(this.key);
|
||||
return this.fallback;
|
||||
|
|
|
@ -26,6 +26,7 @@ import {
|
|||
} from "@monkeytype/contracts/schemas/configs";
|
||||
import { z } from "zod";
|
||||
import { parseWithSchema as parseJsonWithSchema } from "@monkeytype/util/json";
|
||||
import { tryCatchSync } from "@monkeytype/util/trycatch";
|
||||
|
||||
export async function linkDiscord(hashOverride: string): Promise<void> {
|
||||
if (!hashOverride) return;
|
||||
|
@ -80,15 +81,12 @@ export function loadCustomThemeFromUrl(getOverride?: string): void {
|
|||
const getValue = Misc.findGetParameter("customTheme", getOverride);
|
||||
if (getValue === null) return;
|
||||
|
||||
let decoded: z.infer<typeof customThemeUrlDataSchema>;
|
||||
try {
|
||||
decoded = parseJsonWithSchema(atob(getValue), customThemeUrlDataSchema);
|
||||
} catch (e) {
|
||||
console.log("Custom theme URL decoding failed", e);
|
||||
Notifications.add(
|
||||
"Failed to load theme from URL: " + (e as Error).message,
|
||||
0
|
||||
);
|
||||
const { data: decoded, error } = tryCatchSync(() =>
|
||||
parseJsonWithSchema(atob(getValue), customThemeUrlDataSchema)
|
||||
);
|
||||
if (error) {
|
||||
console.log("Custom theme URL decoding failed", error);
|
||||
Notifications.add("Failed to load theme from URL: " + error.message, 0);
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -156,20 +154,17 @@ const TestSettingsSchema = z.tuple([
|
|||
z.string().nullable(), //funbox
|
||||
]);
|
||||
|
||||
type SharedTestSettings = z.infer<typeof TestSettingsSchema>;
|
||||
|
||||
export function loadTestSettingsFromUrl(getOverride?: string): void {
|
||||
const getValue = Misc.findGetParameter("testSettings", getOverride);
|
||||
if (getValue === null) return;
|
||||
|
||||
let de: SharedTestSettings;
|
||||
try {
|
||||
const decompressed = decompressFromURI(getValue) ?? "";
|
||||
de = parseJsonWithSchema(decompressed, TestSettingsSchema);
|
||||
} catch (e) {
|
||||
console.error("Failed to parse test settings:", e);
|
||||
const { data: de, error } = tryCatchSync(() =>
|
||||
parseJsonWithSchema(decompressFromURI(getValue) ?? "", TestSettingsSchema)
|
||||
);
|
||||
if (error) {
|
||||
console.error("Failed to parse test settings:", error);
|
||||
Notifications.add(
|
||||
"Failed to load test settings from URL: " + (e as Error).message,
|
||||
"Failed to load test settings from URL: " + error.message,
|
||||
0
|
||||
);
|
||||
return;
|
||||
|
|
92
packages/util/__test__/trycatch.spec.ts
Normal file
92
packages/util/__test__/trycatch.spec.ts
Normal file
|
@ -0,0 +1,92 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { tryCatch, tryCatchSync } from "../src/trycatch";
|
||||
|
||||
describe("tryCatch", () => {
|
||||
it("should return data on successful promise resolution", async () => {
|
||||
const result = await tryCatch(Promise.resolve("success"));
|
||||
expect(result.data).toBe("success");
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("should return error on promise rejection", async () => {
|
||||
const testError = new Error("test error");
|
||||
const result = await tryCatch(Promise.reject(testError));
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe(testError);
|
||||
});
|
||||
|
||||
it("should handle custom error types", async () => {
|
||||
class CustomError extends Error {
|
||||
code: string;
|
||||
constructor(message: string, code: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const customError = new CustomError("custom error", "E123");
|
||||
const result = await tryCatch<string, CustomError>(
|
||||
Promise.reject(customError)
|
||||
);
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe(customError);
|
||||
expect(result.error?.code).toBe("E123");
|
||||
});
|
||||
|
||||
it("should handle exceptions in async functions", async () => {
|
||||
const testError = new Error("test error");
|
||||
const fn = async (): Promise<void> => {
|
||||
throw testError;
|
||||
};
|
||||
|
||||
const result = await tryCatch(fn());
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe(testError);
|
||||
});
|
||||
});
|
||||
|
||||
describe("tryCatchSync", () => {
|
||||
it("should return data on successful function execution", () => {
|
||||
const result = tryCatchSync(() => "success");
|
||||
expect(result.data).toBe("success");
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("should return error when function throws", () => {
|
||||
const testError = new Error("test error");
|
||||
const result = tryCatchSync(() => {
|
||||
throw testError;
|
||||
});
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe(testError);
|
||||
});
|
||||
|
||||
it("should handle complex data structures", () => {
|
||||
const complexData = {
|
||||
foo: "bar",
|
||||
numbers: [1, 2, 3],
|
||||
nested: { value: true },
|
||||
};
|
||||
const result = tryCatchSync(() => complexData);
|
||||
expect(result.data).toEqual(complexData);
|
||||
expect(result.error).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle custom error types", () => {
|
||||
class CustomError extends Error {
|
||||
code: string;
|
||||
constructor(message: string, code: string) {
|
||||
super(message);
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
const customError = new CustomError("custom error", "E123");
|
||||
const result = tryCatchSync<string, CustomError>(() => {
|
||||
throw customError;
|
||||
});
|
||||
expect(result.data).toBeNull();
|
||||
expect(result.error).toBe(customError);
|
||||
expect(result.error?.code).toBe("E123");
|
||||
});
|
||||
});
|
33
packages/util/src/trycatch.ts
Normal file
33
packages/util/src/trycatch.ts
Normal file
|
@ -0,0 +1,33 @@
|
|||
// based on https://gist.github.com/t3dotgg/a486c4ae66d32bf17c09c73609dacc5b
|
||||
|
||||
type Success<T> = {
|
||||
data: T;
|
||||
error: null;
|
||||
};
|
||||
|
||||
type Failure<E> = {
|
||||
data: null;
|
||||
error: E;
|
||||
};
|
||||
|
||||
type Result<T, E = Error> = Success<T> | Failure<E>;
|
||||
|
||||
export async function tryCatch<T, E = Error>(
|
||||
promiseOrFunction: Promise<T>
|
||||
): Promise<Result<T, E>> {
|
||||
try {
|
||||
let data = await promiseOrFunction;
|
||||
return { data, error: null };
|
||||
} catch (error) {
|
||||
return { data: null, error: error as E };
|
||||
}
|
||||
}
|
||||
|
||||
export function tryCatchSync<T, E = Error>(fn: () => T): Result<T, E> {
|
||||
try {
|
||||
let data = fn();
|
||||
return { data, error: null };
|
||||
} catch (error) {
|
||||
return { data: null, error: error as E };
|
||||
}
|
||||
}
|
Loading…
Add table
Reference in a new issue