diff --git a/.changeset/many-mails-double.md b/.changeset/many-mails-double.md new file mode 100644 index 0000000..11f8ff1 --- /dev/null +++ b/.changeset/many-mails-double.md @@ -0,0 +1,5 @@ +--- +'gov4git-desktop-app': patch +--- + +Centralize utility functions diff --git a/.vscode/settings.json b/.vscode/settings.json index b391d60..b141c9a 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -28,4 +28,4 @@ "files.watcherExclude": { "**/target": true }, -} +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index 9fd5473..d6a390d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -71,7 +71,8 @@ "tsup": "^8.0.1", "tsx": "^4.6.0", "typescript": "^5.3.2", - "vite": "^5.0.3" + "vite": "^5.0.3", + "vite-tsconfig-paths": "^4.2.1" } }, "node_modules/@aashutoshrathi/word-wrap": { @@ -10042,6 +10043,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, "node_modules/gopd": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", @@ -18898,6 +18905,26 @@ } } }, + "node_modules/tsconfck": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-2.1.2.tgz", + "integrity": "sha512-ghqN1b0puy3MhhviwO2kGF8SeMDNhEbnKxjK7h6+fvY9JAxqvXi8y5NAHSQv687OVboS2uZIByzGd45/YxrRHg==", + "dev": true, + "bin": { + "tsconfck": "bin/tsconfck.js" + }, + "engines": { + "node": "^14.13.1 || ^16 || >=18" + }, + "peerDependencies": { + "typescript": "^4.3.5 || ^5.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/tsconfig-paths": { "version": "3.14.2", "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz", @@ -19794,6 +19821,25 @@ } } }, + "node_modules/vite-tsconfig-paths": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/vite-tsconfig-paths/-/vite-tsconfig-paths-4.2.1.tgz", + "integrity": "sha512-GNUI6ZgPqT3oervkvzU+qtys83+75N/OuDaQl7HmOqFTb0pjZsuARrRipsyJhJ3enqV8beI1xhGbToR4o78nSQ==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "globrex": "^0.1.2", + "tsconfck": "^2.1.0" + }, + "peerDependencies": { + "vite": "*" + }, + "peerDependenciesMeta": { + "vite": { + "optional": true + } + } + }, "node_modules/vite/node_modules/@esbuild/android-arm": { "version": "0.19.8", "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.8.tgz", diff --git a/package.json b/package.json index e7e3411..85f5075 100644 --- a/package.json +++ b/package.json @@ -86,7 +86,8 @@ "tsup": "^8.0.1", "tsx": "^4.6.0", "typescript": "^5.3.2", - "vite": "^5.0.3" + "vite": "^5.0.3", + "vite-tsconfig-paths": "^4.2.1" }, "dependencies": { "@fluentui/react-components": "^9.41.0", @@ -109,4 +110,4 @@ "type": "git", "url": "https://github.com/gov4git/desktop-application.git" } -} \ No newline at end of file +} diff --git a/src/electron/db/schema.ts b/src/electron/db/schema.ts index 3dd88fa..32e8275 100644 --- a/src/electron/db/schema.ts +++ b/src/electron/db/schema.ts @@ -1,6 +1,6 @@ import { integer, real, sqliteTable, text } from 'drizzle-orm/sqlite-core' -import { type Ballot as BallotType, Config } from '~/shared' +import { type Ballot as BallotType } from '~/shared' export const ballots = sqliteTable('ballots', { identifier: text('identifier').primaryKey(), diff --git a/src/electron/lib/error.ts b/src/electron/lib/error.ts deleted file mode 100644 index abae2d9..0000000 --- a/src/electron/lib/error.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function throwIfError(err?: string) { - if (err != null && err.trim() !== '') { - throw new Error(err) - } -} diff --git a/src/electron/lib/index.ts b/src/electron/lib/index.ts index 1dedcef..967979f 100644 --- a/src/electron/lib/index.ts +++ b/src/electron/lib/index.ts @@ -1,4 +1,2 @@ export * from './paths.js' -export * from './records.js' -export * from './error.js' export * from './stdout.js' diff --git a/src/electron/lib/records.ts b/src/electron/lib/records.ts deleted file mode 100644 index e37314e..0000000 --- a/src/electron/lib/records.ts +++ /dev/null @@ -1,53 +0,0 @@ -import objectPath from 'object-path' - -export function isRecord(obj: unknown): obj is Record { - return obj != null && typeof obj === 'object' && !Array.isArray(obj) -} - -export function mergeDeep = any>( - target: unknown, - ...sources: unknown[] -): T { - if (!isRecord(target)) { - throw new Error('Cannot merge. Target is not mergable.') - } - - if (sources.length === 0) return target as T - const source = sources.shift() - - if (isRecord(source)) { - for (const key in source) { - if (isRecord(source[key])) { - if (!(key in target)) Object.assign(target, { [key]: {} }) - if (!isRecord(target[key])) { - Object.assign(target, { [key]: source[key] }) - } else { - mergeDeep(target[key], source[key]) - } - } else { - Object.assign(target, { [key]: source[key] }) - } - } - } - - return mergeDeep(target, ...sources) as T -} - -export function hasRequiredKeys>( - obj: T, - keys: string[], -): boolean { - for (const k of keys) { - if ( - objectPath.get( - obj, - (k as string).split('.').map((v) => v.replace(/###/g, '.')), - undefined, - ) === undefined - ) { - return false - } - } - - return true -} diff --git a/src/electron/services/AppUpdaterService.ts b/src/electron/services/AppUpdaterService.ts index 970ce89..33d2e92 100644 --- a/src/electron/services/AppUpdaterService.ts +++ b/src/electron/services/AppUpdaterService.ts @@ -62,7 +62,7 @@ export class AppUpdaterService extends AbstractAppUpdaterService { const updateInfo = await autoUpdater.checkForUpdates() if (updateInfo == null) return null if (updateInfo.downloadPromise != null) { - this.updating = updateInfo.downloadPromise.then((r) => { + this.updating = updateInfo.downloadPromise.then(() => { return { ready: true, version: updateInfo.updateInfo.version, diff --git a/src/electron/services/BallotService.ts b/src/electron/services/BallotService.ts index 841abb9..6f88107 100644 --- a/src/electron/services/BallotService.ts +++ b/src/electron/services/BallotService.ts @@ -4,6 +4,7 @@ import { AbstractBallotService, Ballot, CreateBallotOptions, + serialAsync, VoteOption, } from '~/shared' @@ -294,7 +295,7 @@ export class BallotService extends AbstractBallotService { return this.loadBallot(user.username, community.url, ballotId) } - public loadBallots = async () => { + public loadBallots = serialAsync(async () => { const [userRows, communityRows] = await Promise.all([ this.db.select().from(users).limit(1), this.db @@ -328,7 +329,7 @@ export class BallotService extends AbstractBallotService { } await Promise.all(ballotPromises) - } + }) public getBallots = async () => { const selectedCommunity = ( diff --git a/src/electron/services/LogService.ts b/src/electron/services/LogService.ts index 79d14ba..3cb5ed8 100644 --- a/src/electron/services/LogService.ts +++ b/src/electron/services/LogService.ts @@ -11,9 +11,9 @@ import { dirname, resolve } from 'node:path' import fastRedact from 'fast-redact' import { AbstractLogService } from '~/shared' +import { isRecord } from '~/shared' import { toResolvedPath } from '../lib/paths.js' -import { isRecord } from '../lib/records.js' export type Redacter = (...args: any[]) => string diff --git a/src/electron/services/Services.ts b/src/electron/services/Services.ts index eae5827..b3f9ff9 100644 --- a/src/electron/services/Services.ts +++ b/src/electron/services/Services.ts @@ -1,6 +1,5 @@ import { InvokeServiceProps, ServiceId } from '~/shared' - -import { isRecord } from '../lib/records.js' +import { isRecord } from '~/shared' export class Services { protected declare services: Record diff --git a/src/electron/services/UserService.ts b/src/electron/services/UserService.ts index 52ff53a..7213482 100644 --- a/src/electron/services/UserService.ts +++ b/src/electron/services/UserService.ts @@ -1,6 +1,6 @@ import { and, eq } from 'drizzle-orm' -import { AbstractUserService } from '~/shared' +import { AbstractUserService, serialAsync } from '~/shared' import { DB } from '../db/db.js' import { @@ -209,7 +209,7 @@ export class UserService extends AbstractUserService { return [] } - public loadUser = async () => { + public loadUser = serialAsync(async () => { const [allUsers, selectedCommunities] = await Promise.all([ this.db.select().from(users), this.db.select().from(communities).where(eq(communities.selected, true)), @@ -271,7 +271,7 @@ export class UserService extends AbstractUserService { communities: community, userCommunities: userCommunity, } - } + }) public getUser = async () => { const userInfos = ( diff --git a/src/renderer/src/components/BubbleSlider.tsx b/src/renderer/src/components/BubbleSlider.tsx index a24107a..ba25a61 100644 --- a/src/renderer/src/components/BubbleSlider.tsx +++ b/src/renderer/src/components/BubbleSlider.tsx @@ -7,7 +7,8 @@ import { useState, } from 'react' -import { formatDecimal } from '../lib/numbers.js' +import { formatDecimal } from '~/shared' + import { useBubbleSliderStyles } from './BubbleSlider.styles.js' export type BubbleSliderProps = { diff --git a/src/renderer/src/components/CreateForm.tsx b/src/renderer/src/components/CreateForm.tsx index cfbf948..7fdc826 100644 --- a/src/renderer/src/components/CreateForm.tsx +++ b/src/renderer/src/components/CreateForm.tsx @@ -10,9 +10,10 @@ import { import { FormEvent, useCallback, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { createNestedRecord, mergeDeep } from '~/shared' + import { routes } from '../App/index.js' import { useCatchError } from '../hooks/useCatchError.js' -import { createNestedRecord, mergeDeep } from '../lib/index.js' import { ballotService } from '../services/BallotService.js' import { useButtonStyles } from '../styles/index.js' import { useSettingsFormsStyles } from './SettingsForm.styles.js' diff --git a/src/renderer/src/components/DataLoader.tsx b/src/renderer/src/components/DataLoader.tsx index d139fb2..edb9189 100644 --- a/src/renderer/src/components/DataLoader.tsx +++ b/src/renderer/src/components/DataLoader.tsx @@ -2,10 +2,11 @@ import { useSetAtom } from 'jotai' import { FC, useCallback, useEffect, useMemo, useState } from 'react' import { useNavigate } from 'react-router-dom' +import { serialAsync } from '~/shared' + import { routes } from '../App/Router.js' import { useCatchError } from '../hooks/useCatchError.js' -import { eventBus } from '../lib/eventBus.js' -import { debounceAsync } from '../lib/functions.js' +import { eventBus } from '../lib/index.js' import { appUpdaterService } from '../services/AppUpdaterService.js' import { ballotService } from '../services/BallotService.js' import { cacheService } from '../services/CacheService.js' @@ -20,7 +21,6 @@ import { userAtom, userLoadedAtom } from '../state/user.js' export const DataLoader: FC = function DataLoader() { const catchError = useCatchError() const setUpdates = useSetAtom(updatesAtom) - // const setConfig = useSetAtom(configAtom) const setBallots = useSetAtom(ballotsAtom) const setUser = useSetAtom(userAtom) const setUserLoaded = useSetAtom(userLoadedAtom) @@ -48,7 +48,7 @@ export const DataLoader: FC = function DataLoader() { }, [setUpdates, catchError]) const checkForUpdates = useMemo(() => { - return debounceAsync(_checkForUpdates) + return serialAsync(_checkForUpdates) }, [_checkForUpdates]) const _refreshCache = useCallback(async () => { @@ -56,22 +56,9 @@ export const DataLoader: FC = function DataLoader() { }, []) const refreshCache = useMemo(() => { - return debounceAsync(_refreshCache) + return serialAsync(_refreshCache) }, [_refreshCache]) - // const _getConfig = useCallback(async () => { - // try { - // const config = await configService.getConfig() - // setConfig(config) - // } catch (ex) { - // await catchError(`Failed to load config. ${ex}`) - // } - // }, [catchError, setConfig]) - - // const getConfig = useMemo(() => { - // return debounceAsync(_getConfig) - // }, [_getConfig]) - const _getUser = useCallback(async () => { try { const user = await userService.getUser() @@ -83,7 +70,7 @@ export const DataLoader: FC = function DataLoader() { }, [catchError, setUser, setUserLoaded]) const getUser = useMemo(() => { - return debounceAsync(_getUser) + return serialAsync(_getUser) }, [_getUser]) const _getCommunity = useCallback(async () => { @@ -96,7 +83,7 @@ export const DataLoader: FC = function DataLoader() { }, [catchError, setCommunity]) const getCommunity = useMemo(() => { - return debounceAsync(_getCommunity) + return serialAsync(_getCommunity) }, [_getCommunity]) const _getBallots = useCallback(async () => { @@ -109,22 +96,9 @@ export const DataLoader: FC = function DataLoader() { }, [setBallots, catchError]) const getBallots = useMemo(() => { - return debounceAsync(_getBallots) + return serialAsync(_getBallots) }, [_getBallots]) - // const _updateBallotCache = useCallback(async () => { - // // try { - // // const ballots = await ballotService.updateCache() - // // setBallots(ballots) - // // } catch (ex) { - // // await catchError(`Failed to load ballots. ${ex}`) - // // } - // }, [setBallots, catchError]) - - // const updateBallotCache = useMemo(() => { - // return debounceAsync(_updateBallotCache) - // }, [_updateBallotCache]) - const _getBallot = useCallback( async (e: CustomEvent<{ ballotId: string }>) => { try { @@ -153,7 +127,7 @@ export const DataLoader: FC = function DataLoader() { ) const getBallot = useMemo(() => { - return debounceAsync(_getBallot) + return serialAsync(_getBallot) }, [_getBallot]) useEffect(() => { @@ -183,7 +157,6 @@ export const DataLoader: FC = function DataLoader() { listeners.push( eventBus.subscribe('user-logged-in', async () => { - // const prom = getConfig().then(updateBallotCache).then(getUser) const prom = Promise.all([getUser(), getCommunity(), getBallots()]) addToQueue(prom) await prom @@ -192,12 +165,11 @@ export const DataLoader: FC = function DataLoader() { ) listeners.push( eventBus.subscribe('voted', async (e) => { - // await getBallot(e).then(getUser) await Promise.all([getBallot(e), getUser()]) }), ) listeners.push( - eventBus.subscribe('refresh', async (e) => { + eventBus.subscribe('refresh', async () => { addToQueue( refreshCache().then(async () => { await Promise.all([getBallots(), getUser()]) diff --git a/src/renderer/src/components/IssueBallot.tsx b/src/renderer/src/components/IssueBallot.tsx index 9b2849a..c6c4ad8 100644 --- a/src/renderer/src/components/IssueBallot.tsx +++ b/src/renderer/src/components/IssueBallot.tsx @@ -19,11 +19,10 @@ import { useState, } from 'react' -import { Ballot } from '~/shared' +import { Ballot, formatDecimal } from '~/shared' import { useCatchError } from '../hooks/useCatchError.js' -import { eventBus } from '../lib/eventBus.js' -import { formatDecimal } from '../lib/index.js' +import { eventBus } from '../lib/index.js' import { ballotService } from '../services/index.js' import { communityAtom } from '../state/community.js' import { userAtom } from '../state/user.js' @@ -54,7 +53,7 @@ export const IssueBallot: FC = function IssueBallot({ const [voteError, setVoteError] = useState(null) const [inputWidth, setInputWidth] = useState(0) const [successMessage, setSuccessMessage] = useState(null) - const [, setTimer] = useState(null) + const [, setTimer] = useState(null) useEffect(() => { return eventBus.subscribe( @@ -189,12 +188,11 @@ export const IssueBallot: FC = function IssueBallot({ const onMouseDown = useCallback( (direction: 'up' | 'down') => { - console.log('============ DOWN ====================') switch (direction) { case 'up': setTimer((t) => { if (t != null) clearInterval(t) - return setInterval(() => { + return window.setInterval(() => { change(1) }, 100) }) @@ -202,7 +200,7 @@ export const IssueBallot: FC = function IssueBallot({ case 'down': setTimer((t) => { if (t != null) clearInterval(t) - return setInterval(() => { + return window.setInterval(() => { change(-1) }, 100) }) diff --git a/src/renderer/src/components/LogViewer.tsx b/src/renderer/src/components/LogViewer.tsx index 1b3d03a..26c3414 100644 --- a/src/renderer/src/components/LogViewer.tsx +++ b/src/renderer/src/components/LogViewer.tsx @@ -1,4 +1,4 @@ -import { Button, Text } from '@fluentui/react-components' +import { Button } from '@fluentui/react-components' import { type FC, useCallback, useState } from 'react' import { useLogs } from './LogViewer.hooks.js' diff --git a/src/renderer/src/components/RefreshButton.tsx b/src/renderer/src/components/RefreshButton.tsx index 868e008..1a86233 100644 --- a/src/renderer/src/components/RefreshButton.tsx +++ b/src/renderer/src/components/RefreshButton.tsx @@ -1,7 +1,7 @@ import { Tooltip } from '@fluentui/react-tooltip' import { FC, useCallback } from 'react' -import { eventBus } from '../lib/eventBus.js' +import { eventBus } from '../lib/index.js' import { useRefreshButtonStyles } from './RefreshButton.styles.js' export const RefreshButton: FC = function RefreshButton() { diff --git a/src/renderer/src/components/SettingsForm.tsx b/src/renderer/src/components/SettingsForm.tsx index 98b0edf..df9786f 100644 --- a/src/renderer/src/components/SettingsForm.tsx +++ b/src/renderer/src/components/SettingsForm.tsx @@ -1,19 +1,11 @@ -import { - Button, - Card, - Field, - Input, - Label, - LabelProps, -} from '@fluentui/react-components' -import { InfoLabel } from '@fluentui/react-components/unstable' +import { Button, Card, Field, Input } from '@fluentui/react-components' import { useAtom, useAtomValue } from 'jotai' -import { FC, FormEvent, useCallback, useEffect, useState } from 'react' +import { FC, FormEvent, useCallback, useState } from 'react' import { useNavigate } from 'react-router-dom' import { routes } from '../App/index.js' import { useCatchError } from '../hooks/useCatchError.js' -import { eventBus } from '../lib/eventBus.js' +import { eventBus } from '../lib/index.js' import { communityService } from '../services/CommunityService.js' import { settingsService } from '../services/SettingsService.js' import { userService } from '../services/UserService.js' @@ -122,7 +114,7 @@ export const SettingsForm = function SettingsForm() { className={styles.field} // @ts-expect-error children signature label={{ - children: (_: unknown, slotProps: LabelProps) => ( + children: () => ( @@ -142,7 +134,7 @@ export const SettingsForm = function SettingsForm() { className={styles.field} // @ts-expect-error children signature label={{ - children: (_: unknown, slotProps: LabelProps) => ( + children: () => ( diff --git a/src/renderer/src/components/UpdateNotification.styles.ts b/src/renderer/src/components/UpdateNotification.styles.ts index 029a01b..33c809d 100644 --- a/src/renderer/src/components/UpdateNotification.styles.ts +++ b/src/renderer/src/components/UpdateNotification.styles.ts @@ -1,4 +1,4 @@ -import { makeStyles, shorthands } from '@fluentui/react-components' +import { makeStyles } from '@fluentui/react-components' import { gov4GitTokens } from '../App/theme/index.js' diff --git a/src/renderer/src/components/UserBadge.tsx b/src/renderer/src/components/UserBadge.tsx index 622181d..3be711f 100644 --- a/src/renderer/src/components/UserBadge.tsx +++ b/src/renderer/src/components/UserBadge.tsx @@ -3,8 +3,9 @@ import { useAtomValue } from 'jotai' import type { FC } from 'react' import { Link } from 'react-router-dom' +import { formatDecimal } from '~/shared' + import { routes } from '../App/index.js' -import { formatDecimal } from '../lib/numbers.js' import { userAtom } from '../state/user.js' import { useUserBadgeStyles } from './UserBadge.styles.js' diff --git a/src/renderer/src/lib/functions.ts b/src/renderer/src/lib/functions.ts deleted file mode 100644 index 63b542a..0000000 --- a/src/renderer/src/lib/functions.ts +++ /dev/null @@ -1,25 +0,0 @@ -export function debounceAsync Promise>( - fn: X, - delay = 100, -): (...args: Parameters) => Promise>> { - let existingPromise: Promise> | null = null - let timer: NodeJS.Timeout - function generatePromise(...args: Parameters) { - existingPromise = new Promise>((res, rej) => { - if (timer) clearTimeout(timer) - timer = setTimeout(() => { - // @ts-expect-error error - res(fn(...args)) - }, delay) - }) - return existingPromise.then((r) => { - existingPromise = null - return r - }) - } - - return async (...args: Parameters) => { - if (existingPromise != null) return await existingPromise - return await generatePromise(...args) - } -} diff --git a/src/renderer/src/lib/index.ts b/src/renderer/src/lib/index.ts index ba11574..e018362 100644 --- a/src/renderer/src/lib/index.ts +++ b/src/renderer/src/lib/index.ts @@ -1,3 +1,2 @@ -export * from './record.js' export * from './atoms.js' -export * from './numbers.js' +export * from './eventBus.js' diff --git a/src/renderer/src/lib/numbers.ts b/src/renderer/src/lib/numbers.ts deleted file mode 100644 index 010d8b4..0000000 --- a/src/renderer/src/lib/numbers.ts +++ /dev/null @@ -1,5 +0,0 @@ -export function formatDecimal(num: number, decimals = 2): string { - return new Intl.NumberFormat(undefined, { - maximumFractionDigits: decimals, - }).format(num) -} diff --git a/src/renderer/src/pages/Login.styles.ts b/src/renderer/src/pages/Login.styles.ts deleted file mode 100644 index 013e67b..0000000 --- a/src/renderer/src/pages/Login.styles.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { makeStyles } from '@fluentui/react-components' - -export const useLoginStyles = makeStyles({ - root: { - display: 'flex', - width: '100%', - height: '100%', - justifyContent: 'center', - alignItems: 'center', - }, - row: { - flexGrow: 1, - flexShrink: 1, - width: '100%', - maxWidth: '400px', - textAlign: 'center', - }, -}) diff --git a/src/renderer/src/pages/Login.tsx b/src/renderer/src/pages/Login.tsx index 6569575..148d4de 100644 --- a/src/renderer/src/pages/Login.tsx +++ b/src/renderer/src/pages/Login.tsx @@ -1,8 +1,5 @@ import { SettingsForm } from '../components/index.js' -import { useLoginStyles } from './Login.styles.js' export const LoginPage = function LoginPage() { - const classes = useLoginStyles() - return } diff --git a/src/shared/index.ts b/src/shared/index.ts index 6572d75..f74957f 100644 --- a/src/shared/index.ts +++ b/src/shared/index.ts @@ -1,3 +1,4 @@ export * from './ipc/index.js' export * from './services/index.js' export * from './types/index.js' +export * from './lib/index.js' diff --git a/src/shared/lib/functions.ts b/src/shared/lib/functions.ts new file mode 100644 index 0000000..a32d226 --- /dev/null +++ b/src/shared/lib/functions.ts @@ -0,0 +1,30 @@ +export function serialAsync Promise>( + fn: X, +): (...args: Parameters) => Promise>> { + const existingPromises: Record< + string, + Promise> | null + > = {} + function generatePromise(...args: Parameters) { + const key = JSON.stringify(args) + // @ts-expect-error error + existingPromises[key] = fn(...args) + // existingPromises[key] = new Promise>((res) => { + // if (timer) clearTimeout(timer) + // timer = setTimeout(() => { + // // @ts-expect-error error + // res(fn(...args)) + // }, delay) + // }) + return existingPromises[key]!.then((r) => { + existingPromises[key] = null + return r + }) + } + + return async (...args: Parameters) => { + const key = JSON.stringify(args) + if (existingPromises[key] != null) return await existingPromises[key] + return await generatePromise(...args) + } +} diff --git a/src/shared/lib/index.ts b/src/shared/lib/index.ts new file mode 100644 index 0000000..3d8d662 --- /dev/null +++ b/src/shared/lib/index.ts @@ -0,0 +1,3 @@ +export * from './functions.js' +export * from './numbers.js' +export * from './records.js' diff --git a/src/electron/lib/numbers.ts b/src/shared/lib/numbers.ts similarity index 100% rename from src/electron/lib/numbers.ts rename to src/shared/lib/numbers.ts diff --git a/src/renderer/src/lib/record.ts b/src/shared/lib/records.ts similarity index 71% rename from src/renderer/src/lib/record.ts rename to src/shared/lib/records.ts index 9b6f6ae..0f371a5 100644 --- a/src/renderer/src/lib/record.ts +++ b/src/shared/lib/records.ts @@ -1,19 +1,5 @@ import objectPath from 'object-path' -export function clone(obj: T): T { - return structuredClone(obj) -} - -export function createNestedRecord>( - record: Record, -): T { - const obj = {} - for (const key in record) { - objectPath.set(obj, key, record[key]) - } - return obj as T -} - export function isRecord(obj: unknown): obj is Record { return obj != null && typeof obj === 'object' && !Array.isArray(obj) } @@ -47,15 +33,33 @@ export function mergeDeep = any>( return mergeDeep(target, ...sources) as T } -export function hasRequiredKeys( - obj: Record, - keys: string[], -): boolean { +export function hasRequiredKeys>( + obj: T, + keys: Array, +): [boolean, string[]] { + const missingKeys: string[] = [] + let pass = true for (const k of keys) { - if (obj[k] == null || obj[k].trim() === '') { - return false + const p = Array.isArray(k) ? k : k.split('.') + const value = objectPath.get(obj, p, undefined) + if ( + value === undefined || + (typeof value === 'string' && (value as string).trim() === '') + ) { + pass = false + missingKeys.push(p.join('#')) } } - return true + return [pass, missingKeys] +} + +export function createNestedRecord>( + record: Record, +): T { + const obj = {} + for (const key in record) { + objectPath.set(obj, key, record[key]) + } + return obj as T } diff --git a/vite.webapp.config.mts b/vite.webapp.config.mts index 37ed95a..0bf7b8b 100644 --- a/vite.webapp.config.mts +++ b/vite.webapp.config.mts @@ -2,9 +2,10 @@ import react from '@vitejs/plugin-react' import { resolve } from 'path' import { defineConfig } from 'vite' +import tsconfigPaths from 'vite-tsconfig-paths' export default defineConfig({ - plugins: [react()], + plugins: [tsconfigPaths(), react()], root: resolve(__dirname, './src/renderer'), base: './', build: {