From 9e7c0fe9a8c93cb69cd105570089382fc30afdaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hl=C3=B6=C3=B0ver=20Sigur=C3=B0sson?= Date: Fri, 17 Nov 2023 14:14:08 +0100 Subject: [PATCH] feat: more strict ts typings and subscription hooks --- main/background.ts | 40 ++++---- main/helpers/create-window.ts | 15 +-- main/preload.ts | 6 +- main/types/{Minor.ts => prometheus.ts} | 2 +- package-lock.json | 10 ++ package.json | 1 + renderer/components/Charts/DataRelated.tsx | 23 ++--- renderer/pages/dashboard.tsx | 102 +++++++++++---------- renderer/pages/home.tsx | 2 +- renderer/store/{index.tsx => index.ts} | 4 +- renderer/store/metricsSlice.ts | 66 +++++++++++++ renderer/store/metricsSliceHooks.ts | 33 +++++++ renderer/store/minorSlice.tsx | 56 ----------- renderer/store/selectors.ts | 25 +++++ tsconfig.json | 8 +- types/metrics.ts | 16 ++-- types/modules.d.ts | 1 + 17 files changed, 249 insertions(+), 161 deletions(-) rename main/types/{Minor.ts => prometheus.ts} (80%) rename renderer/store/{index.tsx => index.ts} (84%) create mode 100644 renderer/store/metricsSlice.ts create mode 100644 renderer/store/metricsSliceHooks.ts delete mode 100644 renderer/store/minorSlice.tsx create mode 100644 renderer/store/selectors.ts create mode 100644 types/modules.d.ts diff --git a/main/background.ts b/main/background.ts index b19067a..e97a73b 100644 --- a/main/background.ts +++ b/main/background.ts @@ -1,10 +1,10 @@ import path from "path"; -import { app, ipcMain, shell } from "electron"; +import { app, ipcMain, shell, BrowserWindow } from "electron"; import serve from "electron-serve"; -import { createWindow } from "./helpers"; import parsePrometheusTextFormat from "parse-prometheus-text-format"; -import { MinorParser } from "./types/Minor"; -import { Metrics } from "../types/metrics"; +import { createWindow } from "./helpers"; +import { PrometheusMetricParser } from "./types/prometheus"; +import { SetMetricsStateActionPayload } from "../types/metrics"; const isProd = process.env.NODE_ENV === "production"; @@ -14,7 +14,8 @@ if (isProd) { app.setPath("userData", `${app.getPath("userData")} (development)`); } -let mainWindow = null; +let mainWindow: BrowserWindow; + (async () => { await app.whenReady(); @@ -45,22 +46,22 @@ ipcMain.on("message", async (event, arg) => { event.reply("message", `${arg} World!`); }); -function metricStringParse(item): number | null { +function metricStringParse(item: PrometheusMetricParser | undefined): number | null { if (!item) return null; return +item.metrics[0].value; } -async function getMetrics(): Promise { +async function getMetrics(): Promise { console.log("DEBUG: getMetrics start"); const res = await fetch("http://testnet-3.arweave.net:1984/metrics"); const data = await res.text(); - const parsed: MinorParser[] = parsePrometheusTextFormat(data); + const parsed: PrometheusMetricParser[] = parsePrometheusTextFormat(data) || []; let dataUnpacked = 0; let dataPacked = 0; let storageAvailable = 0; const packingItem = parsed.find( - (item: MinorParser) => item.name === "v2_index_data_size_by_packing", + (item: PrometheusMetricParser) => item.name === "v2_index_data_size_by_packing", ); if (packingItem) { packingItem.metrics.forEach((item) => { @@ -77,14 +78,14 @@ async function getMetrics(): Promise { }); } const hashRate = metricStringParse( - parsed.find((item: MinorParser) => item.name === "average_network_hash_rate"), + parsed.find((item: PrometheusMetricParser) => item.name === "average_network_hash_rate"), ); const earnings = metricStringParse( - parsed.find((item: MinorParser) => item.name === "average_block_reward"), + parsed.find((item: PrometheusMetricParser) => item.name === "average_block_reward"), ); const vdf_step_time_milliseconds_bucket = parsed.find( - (item: MinorParser) => item.name === "vdf_step_time_milliseconds", + (item: PrometheusMetricParser) => item.name === "vdf_step_time_milliseconds", ); let vdfTimeLowerBound: number | null = null; if (vdf_step_time_milliseconds_bucket) { @@ -98,7 +99,7 @@ async function getMetrics(): Promise { } } const weaveSize = metricStringParse( - parsed.find((item: MinorParser) => item.name === "weave_size"), + parsed.find((item: PrometheusMetricParser) => item.name === "weave_size"), ); console.log("DEBUG: getMetrics complete"); return { @@ -114,19 +115,19 @@ async function getMetrics(): Promise { // TODO make generic function for creating pub+sub endpoints // TODO make class for subscription management -let cachedMetrics : Metrics | null = null; +let cachedMetrics: SetMetricsStateActionPayload | null = null; let cachedMetricsStr = ""; // TODO list of webContents // let cachedMetricsSubList = []; let cachedMetricsIsSubActive = false; -let cachedMetricsTimeout : NodeJS.Timeout | null = null; +let cachedMetricsTimeout: NodeJS.Timeout | null = null; let cachedMetricsUpdateInProgress = false; async function cachedMetricsUpdate() { try { cachedMetricsUpdateInProgress = true; cachedMetrics = await getMetrics(); - } catch(err) { + } catch (err) { console.error(err); } cachedMetricsUpdateInProgress = false; @@ -155,7 +156,7 @@ async function cachedMetricsUpdatePing() { // prod active value 1000 // debug active value 10000 (do not kill testnet node) const delay = cachedMetricsIsSubActive ? 10000 : 60000; - cachedMetricsTimeout = setTimeout(async ()=>{ + cachedMetricsTimeout = setTimeout(async () => { // extra check needed if (!isAlive) return; cachedMetricsTimeout = null; @@ -165,10 +166,10 @@ async function cachedMetricsUpdatePing() { }, delay); } -(async function(){ +(async function () { await cachedMetricsUpdate(); cachedMetricsUpdatePing(); -})() +})(); ipcMain.on("metricsSub", async () => { cachedMetricsIsSubActive = true; @@ -178,7 +179,6 @@ ipcMain.on("metricsUnsub", async () => { cachedMetricsIsSubActive = false; }); - ipcMain.on("open-url", async (_event, arg) => { shell.openExternal(arg); }); diff --git a/main/helpers/create-window.ts b/main/helpers/create-window.ts index 77e5974..c686dfd 100644 --- a/main/helpers/create-window.ts +++ b/main/helpers/create-window.ts @@ -15,11 +15,14 @@ export const createWindow = ( const key = "window-state"; const name = `window-state-${windowName}`; const store = new Store({ name }); - const defaultSize = { - width: options.width, - height: options.height, + const defaultSize: Rectangle = { + width: options.width || 800, + height: options.height || 600, + x: 0, + y: 0, }; - let state = {}; + + let state: Rectangle = { x: 0, y: 0, width: 0, height: 0 }; const restore = () => store.get(key, defaultSize); @@ -34,7 +37,7 @@ export const createWindow = ( }; }; - const windowWithinBounds = (windowState, bounds) => { + const windowWithinBounds = (windowState: Rectangle, bounds: Rectangle) => { return ( windowState.x >= bounds.x && windowState.y >= bounds.y && @@ -51,7 +54,7 @@ export const createWindow = ( }); }; - const ensureVisibleOnSomeDisplay = (windowState) => { + const ensureVisibleOnSomeDisplay = (windowState: Rectangle) => { const visible = screen.getAllDisplays().some((display) => { return windowWithinBounds(windowState, display.bounds); }); diff --git a/main/preload.ts b/main/preload.ts index d619e4a..fd0535c 100644 --- a/main/preload.ts +++ b/main/preload.ts @@ -1,5 +1,5 @@ import { contextBridge, ipcRenderer, IpcRendererEvent } from "electron"; -import { Metrics } from "../types/metrics"; +import { SetMetricsStateActionPayload } from "../types/metrics"; ipcRenderer.on("metricsPush", (_event, msg) => { console.log("DEBUG metricsPush FE", msg); @@ -8,12 +8,12 @@ const handler = { send(channel: string, value: unknown) { ipcRenderer.send(channel, value); }, - metricsSub: (handler: (_event: unknown, res: Metrics) => void) => { + metricsSub: (handler: (_event: unknown, res: SetMetricsStateActionPayload) => void) => { console.log("DEBUG metricsSub FE"); ipcRenderer.on("metricsPush", handler); ipcRenderer.send("metricsSub", {}); }, - metricsUnsub: (handler: (_event: unknown, res: Metrics) => void) => { + metricsUnsub: (handler: (_event: unknown, res: SetMetricsStateActionPayload) => void) => { console.log("DEBUG metricsUnsub FE"); ipcRenderer.off("metricsPush", handler); ipcRenderer.send("metricsUnsub", {}); diff --git a/main/types/Minor.ts b/main/types/prometheus.ts similarity index 80% rename from main/types/Minor.ts rename to main/types/prometheus.ts index 0dd0725..c18c0ef 100644 --- a/main/types/Minor.ts +++ b/main/types/prometheus.ts @@ -1,4 +1,4 @@ -export type MinorParser = { +export type PrometheusMetricParser = { name: string; help: string; type: string; diff --git a/package-lock.json b/package-lock.json index 7987156..0ff4ff2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@types/react": "^18.2.31", + "@types/react-dom": "^18.2.15", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", "autoprefixer": "^10.4.16", @@ -2953,6 +2954,15 @@ "csstype": "^3.0.2" } }, + "node_modules/@types/react-dom": { + "version": "18.2.15", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.2.15.tgz", + "integrity": "sha512-HWMdW+7r7MR5+PZqJF6YFNSCtjz1T0dsvo/f1BV6HkV+6erD/nA7wd9NM00KVG83zf2nJ7uATPO9ttdIPvi3gg==", + "devOptional": true, + "dependencies": { + "@types/react": "*" + } + }, "node_modules/@types/responselike": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/@types/responselike/-/responselike-1.0.2.tgz", diff --git a/package.json b/package.json index c8a6cfd..8aca06a 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ }, "devDependencies": { "@types/react": "^18.2.31", + "@types/react-dom": "^18.2.15", "@typescript-eslint/eslint-plugin": "^6.9.0", "@typescript-eslint/parser": "^6.9.0", "autoprefixer": "^10.4.16", diff --git a/renderer/components/Charts/DataRelated.tsx b/renderer/components/Charts/DataRelated.tsx index acebbb2..241e697 100644 --- a/renderer/components/Charts/DataRelated.tsx +++ b/renderer/components/Charts/DataRelated.tsx @@ -1,16 +1,11 @@ import { BottomArrow, TopArrow } from "./Arrows"; +import { useDataPacked, useStorageAvailable, useWeaveSize } from "../../store/metricsSliceHooks"; -interface DataRelatedChartProps { - dataPacked: number | null; - storageAvailable: number | null; - weaveSize: number | null; -} +export default function DataRelatedChart() { + const dataPacked = useDataPacked(); + const storageAvailable = useStorageAvailable(); + const weaveSize = useWeaveSize(); -export default function DataRelatedChart({ - dataPacked, - storageAvailable, - weaveSize, -}: DataRelatedChartProps) { // NOTE maybe this component should pick all stuff from storage directly return (
@@ -20,7 +15,7 @@ export default function DataRelatedChart({ width: "2%", }} > - + {typeof dataPacked === "number" && }
- + {typeof storageAvailable === "number" && ( + + )}
- + {typeof weaveSize === "number" && }
); diff --git a/renderer/pages/dashboard.tsx b/renderer/pages/dashboard.tsx index 6058095..f3a2344 100644 --- a/renderer/pages/dashboard.tsx +++ b/renderer/pages/dashboard.tsx @@ -1,10 +1,11 @@ import React, { useCallback, useEffect, useState } from "react"; -import { useDispatch, useSelector } from "react-redux"; +import { useDispatch } from "react-redux"; import ScrollSpy from "react-ui-scrollspy"; +import { useEarnings, useHashRate } from "../store/metricsSliceHooks"; import DataRelatedChart from "../components/Charts/DataRelated"; import { MainLayout } from "../layouts"; -import { setMetricsState, selectMinorState } from "../store/minorSlice"; -import { Metrics } from "../../types/metrics"; +import { setMetricsState } from "../store/metricsSlice"; +import { SetMetricsStateActionPayload } from "../../types/metrics"; interface MenuItems { label: string; @@ -16,45 +17,34 @@ interface SubMenuItems { target: string; } +const menuItems: MenuItems[] = [ + { + label: "Core", + target: "section-1", + subMenuItems: [ + { label: "Data Related", target: "sub-section-1-1" }, + { label: "Hash Rate", target: "sub-section-1-2" }, + { label: "Earnings", target: "sub-section-1-3" }, + ], + }, + { + label: "Advanced", + target: "section-2", + subMenuItems: [ + { label: "Performance", target: "sub-section-2-1" }, + { label: "Debug", target: "sub-section-2-2" }, + { label: "Raw Logs", target: "sub-section-2-3" }, + ], + }, +]; + export default function DashboardPage() { - // NOTE maybe later we should rename this store - const minorState = useSelector(selectMinorState); const dispatch = useDispatch(); + const hashRate = useHashRate(); + const earnings = useEarnings(); const [activeMenu, setActiveMenu] = useState(); - const menuItems: MenuItems[] = [ - { - label: "Core", - target: "section-1", - subMenuItems: [ - { label: "Data Related", target: "sub-section-1-1" }, - { label: "Hash Rate", target: "sub-section-1-2" }, - { label: "Earnings", target: "sub-section-1-3" }, - ], - }, - { - label: "Advanced", - target: "section-2", - subMenuItems: [ - { label: "Performance", target: "sub-section-2-1" }, - { label: "Debug", target: "sub-section-2-2" }, - { label: "Raw Logs", target: "sub-section-2-3" }, - ], - }, - ]; - - useEffect(() => { - const handler = (_event: unknown, data: Metrics) => { - console.log("DEBUG: requestMetrics", data); - dispatch(setMetricsState(data)); - }; - window.ipc.metricsSub(handler); - return () => { - window.ipc.metricsUnsub(handler); - }; - }, [dispatch]); - const handleMenuClick = useCallback( (event: React.MouseEvent) => { event.preventDefault(); @@ -75,9 +65,27 @@ export default function DashboardPage() { [setActiveMenu], ); - const handleScrollUpdate = (target: string) => { - setActiveMenu(target); - }; + const handleScrollUpdate = useCallback( + (target: string) => { + setActiveMenu(target); + }, + [setActiveMenu], + ); + + const handler = useCallback( + (_event: unknown, data: SetMetricsStateActionPayload) => { + console.log("DEBUG: requestMetrics", data); + dispatch(setMetricsState(data)); + }, + [dispatch, setMetricsState], + ); + + useEffect(() => { + window.ipc.metricsSub(handler); + return () => { + window.ipc.metricsUnsub(handler); + }; + }, [handler]); return ( @@ -129,19 +137,19 @@ export default function DashboardPage() {

Data Related

- +

Hash Rate

-

Hash rate: {minorState.hashRate}

+ {typeof hashRate === "number" && ( +

Hash rate: {hashRate}

+ )}

Earnings

-

Earnings: {minorState.earnings}

+ {typeof earnings === "number" && ( +

Earnings: {earnings}

+ )}
diff --git a/renderer/pages/home.tsx b/renderer/pages/home.tsx index e0382dd..bedb18d 100644 --- a/renderer/pages/home.tsx +++ b/renderer/pages/home.tsx @@ -2,7 +2,7 @@ import React from "react"; import { MainLayout } from "../layouts"; import { useRouter } from "next/router"; import { useSelector } from "react-redux"; -import { selectMinorState } from "../store/minorSlice"; +import { selectMinorState } from "../store/metricsSlice"; export default function HomePage() { const router = useRouter(); diff --git a/renderer/store/index.tsx b/renderer/store/index.ts similarity index 84% rename from renderer/store/index.tsx rename to renderer/store/index.ts index 395344d..156ad38 100644 --- a/renderer/store/index.tsx +++ b/renderer/store/index.ts @@ -1,11 +1,11 @@ import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit"; import { createWrapper } from "next-redux-wrapper"; -import { minorSlice } from "./minorSlice"; +import { metricsSlice } from "./metricsSlice"; export const makeStore = () => { return configureStore({ reducer: { - [minorSlice.name]: minorSlice.reducer, + [metricsSlice.name]: metricsSlice.reducer, }, devTools: true, }); diff --git a/renderer/store/metricsSlice.ts b/renderer/store/metricsSlice.ts new file mode 100644 index 0000000..23cbd08 --- /dev/null +++ b/renderer/store/metricsSlice.ts @@ -0,0 +1,66 @@ +import { createSlice } from "@reduxjs/toolkit"; +import { HYDRATE } from "next-redux-wrapper"; +import { SetMetricsStateActionPayload } from "../../types/metrics"; +import { AppState } from "./index"; + +// Type for our state +export interface MetricsSliceReducerState { + dataUnpacked: number | undefined; + dataPacked: number | undefined; + storageAvailable: number | undefined; + weaveSize: number | undefined; + hashRate: number | undefined; + earnings: number | undefined; + vdfTimeLowerBound: number | undefined; +} + +const initialMetricsState: MetricsSliceReducerState = { + dataUnpacked: undefined, + dataPacked: undefined, + storageAvailable: undefined, + weaveSize: undefined, + hashRate: undefined, + earnings: undefined, + vdfTimeLowerBound: undefined, +}; + +export const metricsSlice = createSlice({ + name: "metrics", + initialState: initialMetricsState, + reducers: { + setMetricsState( + state: MetricsSliceReducerState, + action: { payload: SetMetricsStateActionPayload }, + ) { + const { + dataUnpacked, + dataPacked, + storageAvailable, + weaveSize, + hashRate, + earnings, + vdfTimeLowerBound, + } = action.payload; + state.dataUnpacked = dataUnpacked ?? undefined; + state.dataPacked = dataPacked ?? undefined; + state.storageAvailable = storageAvailable ?? undefined; + state.weaveSize = weaveSize ?? undefined; + state.hashRate = hashRate ?? undefined; + state.earnings = earnings ?? undefined; + state.vdfTimeLowerBound = vdfTimeLowerBound ?? undefined; + }, + }, + + extraReducers: { + [HYDRATE]: (state, action) => { + return { + ...state, + ...action.payload, + }; + }, + }, +}); + +export const { setMetricsState } = metricsSlice.actions; +export const selectMinorState = (state: AppState) => state.metrics; +export default metricsSlice.reducer; diff --git a/renderer/store/metricsSliceHooks.ts b/renderer/store/metricsSliceHooks.ts new file mode 100644 index 0000000..8b5a98b --- /dev/null +++ b/renderer/store/metricsSliceHooks.ts @@ -0,0 +1,33 @@ +import { useSelector } from "react-redux"; +import { + selectHashRate, + selectEarnings, + selectDataPacked, + selectDataUnpacked, + selectStorageAvailable, + selectWeaveSize, +} from "./selectors"; + +export const useHashRate = () => { + return useSelector(selectHashRate); +}; + +export const useEarnings = () => { + return useSelector(selectEarnings); +}; + +export const useDataPacked = () => { + return useSelector(selectDataPacked); +}; + +export const useDataUnpacked = () => { + return useSelector(selectDataUnpacked); +}; + +export const useStorageAvailable = () => { + return useSelector(selectStorageAvailable); +}; + +export const useWeaveSize = () => { + return useSelector(selectWeaveSize); +}; diff --git a/renderer/store/minorSlice.tsx b/renderer/store/minorSlice.tsx deleted file mode 100644 index d9d1442..0000000 --- a/renderer/store/minorSlice.tsx +++ /dev/null @@ -1,56 +0,0 @@ -import { createSlice } from "@reduxjs/toolkit"; -import { AppState } from "./index"; -import { HYDRATE } from "next-redux-wrapper"; - -// Type for our state -export interface MinorState { - dataUnpacked: number | null; - dataPacked: number | null; - storageAvailable: number | null; - weaveSize: number | null; - hashRate: number | null; - earnings: number | null; - vdfTimeLowerBound: number | null; -} - -const initialState: MinorState = { - dataUnpacked: null, - dataPacked: null, - storageAvailable: null, - weaveSize: null, - hashRate: null, - earnings: null, - vdfTimeLowerBound: null, -}; - -export const minorSlice = createSlice({ - name: "minor", - initialState, - reducers: { - setMetricsState(state, action) { - state.dataUnpacked = action.payload.dataUnpacked; - state.dataPacked = action.payload.dataUnpacked; - state.storageAvailable = action.payload.storageAvailable; - state.weaveSize = action.payload.weaveSize; - state.hashRate = action.payload.hashRate; - state.earnings = action.payload.earnings; - state.vdfTimeLowerBound = action.payload.vdfTimeLowerBound; - }, - }, - - extraReducers: { - [HYDRATE]: (state, action) => { - return { - ...state, - ...action.payload.dataUnpacked, - ...action.payload.dataPacked, - ...action.payload.hashRate, - ...action.payload.earnings, - }; - }, - }, -}); - -export const { setMetricsState } = minorSlice.actions; -export const selectMinorState = (state: AppState) => state.minor; -export default minorSlice.reducer; diff --git a/renderer/store/selectors.ts b/renderer/store/selectors.ts new file mode 100644 index 0000000..259c4b0 --- /dev/null +++ b/renderer/store/selectors.ts @@ -0,0 +1,25 @@ +import { AppState } from "./index"; + +export const selectHashRate = (state: AppState) => ({ + hashRate: state.metrics.hashRate, +}); + +export const selectEarnings = (state: AppState) => ({ + earnings: state.metrics.earnings, +}); + +export const selectDataPacked = (state: AppState) => ({ + dataPacked: state.metrics.dataPacked, +}); + +export const selectDataUnpacked = (state: AppState) => ({ + dataUnpacked: state.metrics.dataUnpacked, +}); + +export const selectStorageAvailable = (state: AppState) => ({ + storageAvailable: state.metrics.storageAvailable, +}); + +export const selectWeaveSize = (state: AppState) => ({ + weaveSize: state.metrics.weaveSize, +}); diff --git a/tsconfig.json b/tsconfig.json index 7e2d690..03c6acb 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,16 +1,16 @@ { "compilerOptions": { - "target": "es5", + "target": "ESNext", "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, - "strict": false, + "strict": true, "forceConsistentCasingInFileNames": true, "noEmit": true, "incremental": true, "esModuleInterop": true, - "module": "esnext", - "moduleResolution": "node", + "module": "NodeNext", + "moduleResolution": "NodeNext", "resolveJsonModule": true, "isolatedModules": true, "jsx": "preserve" diff --git a/types/metrics.ts b/types/metrics.ts index 14ebb96..cc119e6 100644 --- a/types/metrics.ts +++ b/types/metrics.ts @@ -1,9 +1,9 @@ -export type Metrics = { - dataUnpacked: number; - dataPacked: number; - storageAvailable: number; - weaveSize: number; - hashRate: number; - earnings: number; - vdfTimeLowerBound: number; +export interface SetMetricsStateActionPayload { + dataUnpacked: number | null; + dataPacked: number | null; + storageAvailable: number | null; + weaveSize: number | null; + hashRate: number | null; + earnings: number | null; + vdfTimeLowerBound: number | null; } diff --git a/types/modules.d.ts b/types/modules.d.ts new file mode 100644 index 0000000..cd8e099 --- /dev/null +++ b/types/modules.d.ts @@ -0,0 +1 @@ +declare module "parse-prometheus-text-format";