From 4f2371fa8baf4eb8652a83597ca1aed6a034e492 Mon Sep 17 00:00:00 2001 From: pinks Date: Thu, 19 Oct 2023 23:37:03 +0200 Subject: [PATCH] exactOptionalPropertyTypes --- api/mod.ts | 2 +- api/sessionsRoute.ts | 4 ++-- api/usersRoute.ts | 4 ++-- api/workersRoute.ts | 13 ++++++------ app/generationQueue.ts | 11 +++++----- app/generationStore.ts | 8 +++---- app/kvMemoize.ts | 47 ++++++++++++++++++++++++++++++----------- app/uploadQueue.ts | 2 +- bot/cancelCommand.ts | 12 +++++++---- bot/mod.ts | 43 ++++++++++++++++++++++++------------- bot/parsePngInfo.ts | 16 +++++++++----- bot/pnginfoCommand.ts | 24 +++++++++++++-------- bot/queueCommand.ts | 16 ++++++++------ deno.json | 4 +++- ui/AppHeader.tsx | 3 ++- ui/Counter.tsx | 10 ++++----- ui/WorkersPage.tsx | 2 +- utils/formatUserChat.ts | 6 +++++- utils/omitUndef.ts | 11 ++++++++++ 19 files changed, 157 insertions(+), 81 deletions(-) create mode 100644 utils/omitUndef.ts diff --git a/api/mod.ts b/api/mod.ts index 183ce60..13043fd 100644 --- a/api/mod.ts +++ b/api/mod.ts @@ -1,7 +1,7 @@ import { route } from "reroute"; import { serveSpa } from "serve_spa"; import { serveApi } from "./serveApi.ts"; -import { fromFileUrl } from "std/path/mod.ts" +import { fromFileUrl } from "std/path/mod.ts"; export async function serveUi() { const server = Deno.serve({ port: 5999 }, (request) => diff --git a/api/sessionsRoute.ts b/api/sessionsRoute.ts index 67b1d0e..a2be5c8 100644 --- a/api/sessionsRoute.ts +++ b/api/sessionsRoute.ts @@ -4,7 +4,7 @@ import { ulid } from "ulid"; export const sessions = new Map(); export interface Session { - userId?: number; + userId?: number | undefined; } export const sessionsRoute = createPathFilter({ @@ -24,7 +24,7 @@ export const sessionsRoute = createPathFilter({ GET: createEndpoint( { query: null, body: null }, async ({ params }) => { - const id = params.sessionId; + const id = params.sessionId!; const session = sessions.get(id); if (!session) { return { status: 401, body: { type: "text/plain", data: "Session not found" } }; diff --git a/api/usersRoute.ts b/api/usersRoute.ts index b3d0041..e1a6694 100644 --- a/api/usersRoute.ts +++ b/api/usersRoute.ts @@ -7,7 +7,7 @@ export const usersRoute = createPathFilter({ GET: createEndpoint( { query: null, body: null }, async ({ params }) => { - const chat = await bot.api.getChat(params.userId); + const chat = await bot.api.getChat(params.userId!); if (chat.type !== "private") { throw new Error("Chat is not private"); } @@ -36,7 +36,7 @@ export const usersRoute = createPathFilter({ GET: createEndpoint( { query: null, body: null }, async ({ params }) => { - const chat = await bot.api.getChat(params.userId); + const chat = await bot.api.getChat(params.userId!); if (chat.type !== "private") { throw new Error("Chat is not private"); } diff --git a/api/workersRoute.ts b/api/workersRoute.ts index b140ffb..df65dbe 100644 --- a/api/workersRoute.ts +++ b/api/workersRoute.ts @@ -12,6 +12,7 @@ import { workerInstanceStore, } from "../app/workerInstanceStore.ts"; import { getAuthHeader } from "../utils/getAuthHeader.ts"; +import { omitUndef } from "../utils/omitUndef.ts"; import { withUser } from "./withUser.ts"; export type WorkerData = Omit & { @@ -105,7 +106,7 @@ export const workersRoute = createPathFilter({ GET: createEndpoint( { query: null, body: null }, async ({ params }) => { - const workerInstance = await workerInstanceStore.getById(params.workerId); + const workerInstance = await workerInstanceStore.getById(params.workerId!); if (!workerInstance) { return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; } @@ -126,7 +127,7 @@ export const workersRoute = createPathFilter({ }, }, async ({ params, query, body }) => { - const workerInstance = await workerInstanceStore.getById(params.workerId); + const workerInstance = await workerInstanceStore.getById(params.workerId!); if (!workerInstance) { return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; } @@ -136,7 +137,7 @@ export const workersRoute = createPathFilter({ JSON.stringify(body.data) }`, ); - await workerInstance.update(body.data); + await workerInstance.update(omitUndef(body.data)); return { status: 200, body: { type: "application/json", data: await getWorkerData(workerInstance) }, @@ -152,7 +153,7 @@ export const workersRoute = createPathFilter({ body: null, }, async ({ params, query }) => { - const workerInstance = await workerInstanceStore.getById(params.workerId); + const workerInstance = await workerInstanceStore.getById(params.workerId!); if (!workerInstance) { return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; } @@ -169,7 +170,7 @@ export const workersRoute = createPathFilter({ GET: createEndpoint( { query: null, body: null }, async ({ params }) => { - const workerInstance = await workerInstanceStore.getById(params.workerId); + const workerInstance = await workerInstanceStore.getById(params.workerId!); if (!workerInstance) { return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; } @@ -200,7 +201,7 @@ export const workersRoute = createPathFilter({ GET: createEndpoint( { query: null, body: null }, async ({ params }) => { - const workerInstance = await workerInstanceStore.getById(params.workerId); + const workerInstance = await workerInstanceStore.getById(params.workerId!); if (!workerInstance) { return { status: 404, body: { type: "text/plain", data: `Worker not found` } }; } diff --git a/app/generationQueue.ts b/app/generationQueue.ts index 7b8fc39..5ad3b24 100644 --- a/app/generationQueue.ts +++ b/app/generationQueue.ts @@ -11,6 +11,7 @@ import { PngInfo } from "../bot/parsePngInfo.ts"; import { formatOrdinal } from "../utils/formatOrdinal.ts"; import { formatUserChat } from "../utils/formatUserChat.ts"; import { getAuthHeader } from "../utils/getAuthHeader.ts"; +import { omitUndef } from "../utils/omitUndef.ts"; import { SdError } from "./SdError.ts"; import { getConfig } from "./config.ts"; import { db, fs } from "./db.ts"; @@ -161,7 +162,7 @@ async function processGenerationJob( // reduce size if worker can't handle the resolution const size = limitSize( - { ...config.defaultParams, ...state.task.params }, + omitUndef({ ...config.defaultParams, ...state.task.params }), 1024 * 1024, ); function limitSize( @@ -182,18 +183,18 @@ async function processGenerationJob( // start generating the image const responsePromise = state.task.type === "txt2img" ? workerSdClient.POST("/sdapi/v1/txt2img", { - body: { + body: omitUndef({ ...config.defaultParams, ...state.task.params, ...size, negative_prompt: state.task.params.negative_prompt ? state.task.params.negative_prompt : config.defaultParams?.negative_prompt, - }, + }), }) : state.task.type === "img2img" ? workerSdClient.POST("/sdapi/v1/img2img", { - body: { + body: omitUndef({ ...config.defaultParams, ...state.task.params, ...size, @@ -209,7 +210,7 @@ async function processGenerationJob( ).then((resp) => resp.arrayBuffer()), ), ], - }, + }), }) : undefined; diff --git a/app/generationStore.ts b/app/generationStore.ts index a1bd5a2..266dfcb 100644 --- a/app/generationStore.ts +++ b/app/generationStore.ts @@ -5,10 +5,10 @@ import { db } from "./db.ts"; export interface GenerationSchema { from: User; chat: Chat; - sdInstanceId?: string; // TODO: change to workerInstanceKey - info?: SdGenerationInfo; - startDate?: Date; - endDate?: Date; + sdInstanceId?: string | undefined; // TODO: change to workerInstanceKey + info?: SdGenerationInfo | undefined; + startDate?: Date | undefined; + endDate?: Date | undefined; } /** diff --git a/app/kvMemoize.ts b/app/kvMemoize.ts index 0b74d4d..cc837c3 100644 --- a/app/kvMemoize.ts +++ b/app/kvMemoize.ts @@ -1,19 +1,42 @@ +export interface KvMemoizeOptions { + /** + * The time in milliseconds until the cached result expires. + */ + expireIn?: ((result: R, ...args: A) => number) | number | undefined; + /** + * Whether to recalculate the result if it was already cached. + * + * Runs whenever the result is retrieved from the cache. + */ + shouldRecalculate?: ((result: R, ...args: A) => boolean) | undefined; + /** + * Whether to cache the result after computing it. + * + * Runs whenever a new result is computed. + */ + shouldCache?: ((result: R, ...args: A) => boolean) | undefined; + /** + * Override the default KV store functions. + */ + override?: { + set: ( + key: Deno.KvKey, + args: A, + value: R, + options: { expireIn?: number }, + ) => Promise; + get: (key: Deno.KvKey, args: A) => Promise; + }; +} + /** - * Memoizes the function result in KV storage. + * Memoizes the function result in KV store. */ export function kvMemoize( db: Deno.Kv, key: Deno.KvKey, fn: (...args: A) => Promise, - options?: { - expireIn?: number | ((result: R, ...args: A) => number); - shouldRecalculate?: (result: R, ...args: A) => boolean; - shouldCache?: (result: R, ...args: A) => boolean; - override?: { - set: (key: Deno.KvKey, args: A, value: R, options: { expireIn?: number }) => Promise; - get: (key: Deno.KvKey, args: A) => Promise; - }; - }, + options?: KvMemoizeOptions, ): (...args: A) => Promise { return async (...args) => { const cachedResult = options?.override?.get @@ -34,9 +57,9 @@ export function kvMemoize( if (options?.shouldCache?.(result, ...args) ?? (result != null)) { if (options?.override?.set) { - await options.override.set(key, args, result, { expireIn }); + await options.override.set(key, args, result, expireIn != null ? { expireIn } : {}); } else { - await db.set([...key, ...args], result, { expireIn }); + await db.set([...key, ...args], result, expireIn != null ? { expireIn } : {}); } } diff --git a/app/uploadQueue.ts b/app/uploadQueue.ts index 1ee335d..9eea8d3 100644 --- a/app/uploadQueue.ts +++ b/app/uploadQueue.ts @@ -92,7 +92,7 @@ export async function processUploadQueue() { // send caption in separate message if it couldn't fit if (caption.text.length > 1024 && caption.text.length <= 4096) { await bot.api.sendMessage(state.chat.id, caption.text, { - reply_to_message_id: resultMessages[0].message_id, + reply_to_message_id: resultMessages[0]!.message_id, allow_sending_without_reply: true, entities: caption.entities, maxWait: 60, diff --git a/bot/cancelCommand.ts b/bot/cancelCommand.ts index 4077212..fd64dfe 100644 --- a/bot/cancelCommand.ts +++ b/bot/cancelCommand.ts @@ -1,4 +1,5 @@ import { generationQueue } from "../app/generationQueue.ts"; +import { omitUndef } from "../utils/omitUndef.ts"; import { ErisContext } from "./mod.ts"; export async function cancelCommand(ctx: ErisContext) { @@ -7,8 +8,11 @@ export async function cancelCommand(ctx: ErisContext) { .filter((job) => job.lockUntil < new Date()) .filter((j) => j.state.from.id === ctx.from?.id); for (const job of userJobs) await generationQueue.deleteJob(job.id); - await ctx.reply(`Cancelled ${userJobs.length} jobs`, { - reply_to_message_id: ctx.message?.message_id, - allow_sending_without_reply: true, - }); + await ctx.reply( + `Cancelled ${userJobs.length} jobs`, + omitUndef({ + reply_to_message_id: ctx.message?.message_id, + allow_sending_without_reply: true, + }), + ); } diff --git a/bot/mod.ts b/bot/mod.ts index 0e1c380..4196e4e 100644 --- a/bot/mod.ts +++ b/bot/mod.ts @@ -6,6 +6,7 @@ import { error, info, warning } from "std/log/mod.ts"; import { sessions } from "../api/sessionsRoute.ts"; import { getConfig, setConfig } from "../app/config.ts"; import { formatUserChat } from "../utils/formatUserChat.ts"; +import { omitUndef } from "../utils/omitUndef.ts"; import { broadcastCommand } from "./broadcastCommand.ts"; import { cancelCommand } from "./cancelCommand.ts"; import { img2imgCommand, img2imgQuestion } from "./img2imgCommand.ts"; @@ -19,11 +20,11 @@ interface SessionData { } interface ErisChatData { - language?: string; + language?: string | undefined; } interface ErisUserData { - params?: Record; + params?: Record | undefined; } export type ErisContext = @@ -94,10 +95,13 @@ bot.use(async (ctx, next) => { await next(); } catch (err) { try { - await ctx.reply(`Handling update failed: ${err}`, { - reply_to_message_id: ctx.message?.message_id, - allow_sending_without_reply: true, - }); + await ctx.reply( + `Handling update failed: ${err}`, + omitUndef({ + reply_to_message_id: ctx.message?.message_id, + allow_sending_without_reply: true, + }), + ); } catch { throw err; } @@ -122,24 +126,33 @@ bot.command("start", async (ctx) => { const id = ctx.match.trim(); const session = sessions.get(id); if (session == null) { - await ctx.reply("Login failed: Invalid session ID", { - reply_to_message_id: ctx.message?.message_id, - }); + await ctx.reply( + "Login failed: Invalid session ID", + omitUndef({ + reply_to_message_id: ctx.message?.message_id, + }), + ); return; } session.userId = ctx.from?.id; sessions.set(id, session); info(`User ${formatUserChat(ctx)} logged in`); // TODO: show link to web ui - await ctx.reply("Login successful! You can now return to the WebUI.", { - reply_to_message_id: ctx.message?.message_id, - }); + await ctx.reply( + "Login successful! You can now return to the WebUI.", + omitUndef({ + reply_to_message_id: ctx.message?.message_id, + }), + ); return; } - await ctx.reply("Hello! Use the /txt2img command to generate an image", { - reply_to_message_id: ctx.message?.message_id, - }); + await ctx.reply( + "Hello! Use the /txt2img command to generate an image", + omitUndef({ + reply_to_message_id: ctx.message?.message_id, + }), + ); }); bot.command("txt2img", txt2imgCommand); diff --git a/bot/parsePngInfo.ts b/bot/parsePngInfo.ts index 45ada41..9bf2d40 100644 --- a/bot/parsePngInfo.ts +++ b/bot/parsePngInfo.ts @@ -25,7 +25,11 @@ interface PngInfoExtra extends PngInfo { upscale?: number; } -export function parsePngInfo(pngInfo: string, baseParams?: Partial, shouldParseSeed?: boolean): Partial { +export function parsePngInfo( + pngInfo: string, + baseParams?: Partial, + shouldParseSeed?: boolean, +): Partial { const tags = pngInfo.split(/[,;]+|\.+\s|\n/u); let part: "prompt" | "negative_prompt" | "params" = "prompt"; const params: Partial = {}; @@ -34,7 +38,7 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial, sho for (const tag of tags) { const paramValuePair = tag.trim().match(/^(\w+\s*\w*):\s+(.*)$/u); if (paramValuePair) { - const [, param, value] = paramValuePair; + const [_match, param = "", value = ""] = paramValuePair; switch (param.replace(/\s+/u, "").toLowerCase()) { case "positiveprompt": case "positive": @@ -67,7 +71,7 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial, sho case "size": case "resolution": { part = "params"; - const [width, height] = value.trim() + const [width = 0, height = 0] = value.trim() .split(/\s*[x,]\s*/u, 2) .map((v) => v.trim()) .map(Number); @@ -103,9 +107,11 @@ export function parsePngInfo(pngInfo: string, baseParams?: Partial, sho part = "params"; if (shouldParseSeed) { const seed = Number(value.trim()); - params.seed = seed; - break; + if (Number.isFinite(seed)) { + params.seed = seed; + } } + break; } case "model": case "modelhash": diff --git a/bot/pnginfoCommand.ts b/bot/pnginfoCommand.ts index d44ed81..bf54230 100644 --- a/bot/pnginfoCommand.ts +++ b/bot/pnginfoCommand.ts @@ -1,6 +1,7 @@ import { CommandContext } from "grammy"; import { bold, fmt } from "grammy_parse_mode"; import { StatelessQuestion } from "grammy_stateless_question"; +import { omitUndef } from "../utils/omitUndef.ts"; import { ErisContext } from "./mod.ts"; import { getPngInfo, parsePngInfo } from "./parsePngInfo.ts"; @@ -23,11 +24,13 @@ async function pnginfo(ctx: ErisContext, includeRepliedTo: boolean): Promise) { let formattedMessage = await getMessageText(); - const queueMessage = await ctx.replyFmt(formattedMessage, { - disable_notification: true, - reply_to_message_id: ctx.message?.message_id, - }); + const queueMessage = await ctx.replyFmt( + formattedMessage, + omitUndef({ + disable_notification: true, + reply_to_message_id: ctx.message?.message_id, + }), + ); handleFutureUpdates().catch(() => undefined); async function getMessageText() { diff --git a/deno.json b/deno.json index b0c34fa..fd4ba93 100644 --- a/deno.json +++ b/deno.json @@ -5,7 +5,9 @@ "start": "deno run --unstable --allow-env --allow-read --allow-write --allow-net main.ts" }, "compilerOptions": { - "jsx": "react" + "exactOptionalPropertyTypes": true, + "jsx": "react", + "noUncheckedIndexedAccess": true }, "fmt": { "lineWidth": 100 diff --git a/ui/AppHeader.tsx b/ui/AppHeader.tsx index fa22c51..2caefd2 100644 --- a/ui/AppHeader.tsx +++ b/ui/AppHeader.tsx @@ -34,7 +34,8 @@ export function AppHeader( ); const bot = useSWR( - ['bot',"GET",{}] as const, (args) => fetchApi(...args).then(handleResponse), + ["bot", "GET", {}] as const, + (args) => fetchApi(...args).then(handleResponse), ); const userPhoto = useSWR( diff --git a/ui/Counter.tsx b/ui/Counter.tsx index ea6757b..0e2454c 100644 --- a/ui/Counter.tsx +++ b/ui/Counter.tsx @@ -1,7 +1,7 @@ import { cx } from "@twind/core"; import React from "react"; -function CounterDigit(props: { value: number; transitionDurationMs?: number }) { +function CounterDigit(props: { value: number; transitionDurationMs?: number | undefined }) { const { value, transitionDurationMs = 1500 } = props; const rads = -(Math.floor(value) % 1_000_000) * 2 * Math.PI * 0.1; @@ -36,10 +36,10 @@ const CounterText = (props: { children: React.ReactNode }) => ( export function Counter(props: { value: number; digits: number; - fractionDigits?: number; - transitionDurationMs?: number; - className?: string; - postfix?: string; + fractionDigits?: number | undefined; + transitionDurationMs?: number | undefined; + className?: string | undefined; + postfix?: string | undefined; }) { const { value, digits, fractionDigits = 0, transitionDurationMs, className, postfix } = props; diff --git a/ui/WorkersPage.tsx b/ui/WorkersPage.tsx index 0ac28bb..8d77a1f 100644 --- a/ui/WorkersPage.tsx +++ b/ui/WorkersPage.tsx @@ -159,7 +159,7 @@ export function WorkersPage(props: { sessionId?: string }) { ); } -function WorkerListItem(props: { worker: WorkerData; sessionId?: string }) { +function WorkerListItem(props: { worker: WorkerData; sessionId: string | undefined }) { const { worker, sessionId } = props; const editWorkerModalRef = useRef(null); const deleteWorkerModalRef = useRef(null); diff --git a/utils/formatUserChat.ts b/utils/formatUserChat.ts index b8106a6..9e0234e 100644 --- a/utils/formatUserChat.ts +++ b/utils/formatUserChat.ts @@ -1,7 +1,11 @@ import { Chat, User } from "grammy_types"; export function formatUserChat( - ctx: { from?: User; chat?: Chat; workerInstanceKey?: string }, + ctx: { + from?: User | undefined; + chat?: Chat | undefined; + workerInstanceKey?: string | undefined; + }, ) { const msg: string[] = []; if (ctx.from) { diff --git a/utils/omitUndef.ts b/utils/omitUndef.ts new file mode 100644 index 0000000..308848b --- /dev/null +++ b/utils/omitUndef.ts @@ -0,0 +1,11 @@ +/** + * Removes all undefined properties from an object. + */ +export function omitUndef(object: O): + & { [K in keyof O as undefined extends O[K] ? never : K]: O[K] } + & { [K in keyof O as undefined extends O[K] ? K : never]?: O[K] & ({} | null) } { + if (object == undefined) return object as never; + return Object.fromEntries( + Object.entries(object).filter(([, v]) => v !== undefined), + ) as never; +}