From c8b167ed5fbd54fb62b0d6734f1f91abc31b2784 Mon Sep 17 00:00:00 2001 From: Alex Freska Date: Tue, 19 Nov 2024 09:29:40 -0500 Subject: [PATCH] refactor(renterd): config only patch changed --- .changeset/short-mice-know.md | 5 + apps/renterd/contexts/config/patchPayloads.ts | 73 +++++++++++++ apps/renterd/contexts/config/useOnValid.tsx | 101 ++++++++++-------- 3 files changed, 136 insertions(+), 43 deletions(-) create mode 100644 .changeset/short-mice-know.md create mode 100644 apps/renterd/contexts/config/patchPayloads.ts diff --git a/.changeset/short-mice-know.md b/.changeset/short-mice-know.md new file mode 100644 index 000000000..ffdd9a2d6 --- /dev/null +++ b/.changeset/short-mice-know.md @@ -0,0 +1,5 @@ +--- +'renterd': minor +--- + +The configuration now makes less network requests and only patch updates individual settings that have changed. diff --git a/apps/renterd/contexts/config/patchPayloads.ts b/apps/renterd/contexts/config/patchPayloads.ts new file mode 100644 index 000000000..5c8941f88 --- /dev/null +++ b/apps/renterd/contexts/config/patchPayloads.ts @@ -0,0 +1,73 @@ +import { + AutopilotConfig, + SettingsGouging, + SettingsPinned, + SettingsUpload, +} from '@siafoundation/renterd-types' +import { isObject, isEqual, union, keys } from '@technically/lodash' +import { ResourcesRequiredLoaded } from './useResources' + +/** + * @param param0 An object containing the resources and payloads. + * @returns An object with each patch payload or undefined if no changes. + */ +export function getPatchPayloads({ + resources, + payloads, +}: { + resources: ResourcesRequiredLoaded + payloads: { + autopilot?: AutopilotConfig + gouging: SettingsGouging + pinned: SettingsPinned + upload: SettingsUpload + } +}) { + return { + autopilot: getPatch(resources.autopilot.data, payloads.autopilot), + gouging: getPatch(resources.gouging.data, payloads.gouging), + pinned: getPatch(resources.pinned.data, payloads.pinned), + upload: getPatch(resources.upload.data, payloads.upload), + } +} + +/** + * @param existing The existing object. + * @param updated The updated object. + * @returns A partial object of T with the differences between existing and updated. + */ +function getPatch(existing: T, updated: T): DeepPartial | undefined { + if (isEqual(existing, updated)) { + return undefined + } + + if (!isObject(existing) || !isObject(updated)) { + return updated as DeepPartial + } + + const keysList = union(keys(existing), keys(updated)) as Array + + const result = {} as DeepPartial + let isChanged = false + + keysList.forEach((key) => { + const origValue = existing[key] + const stagedValue = updated[key] + + const valueChange = getPatch(origValue, stagedValue) + + if (valueChange !== undefined) { + // eslint-disable-next-line @typescript-eslint/no-extra-semi, @typescript-eslint/no-explicit-any + ;(result as any)[key] = valueChange + isChanged = true + } + }) + + return isChanged ? result : undefined +} + +type DeepPartial = T extends Array + ? Array> + : T extends object + ? { [K in keyof T]?: DeepPartial } + : T diff --git a/apps/renterd/contexts/config/useOnValid.tsx b/apps/renterd/contexts/config/useOnValid.tsx index 742146d04..63d2498b8 100644 --- a/apps/renterd/contexts/config/useOnValid.tsx +++ b/apps/renterd/contexts/config/useOnValid.tsx @@ -4,23 +4,20 @@ import { } from '@siafoundation/design-system' import { delay, useMutate } from '@siafoundation/react-core' import { - useAutopilotConfigUpdate, useAutopilotTrigger, useBusState, - useSettingsGougingUpdate, - useSettingsPinnedUpdate, - useSettingsUploadUpdate, + useAutopilotConfigUpdate, + useSettingsGougingPatch, + useSettingsPinnedPatch, + useSettingsUploadPatch, } from '@siafoundation/renterd-react' import { busHostsRoute } from '@siafoundation/renterd-types' import { useCallback } from 'react' -import { - ResourcesMaybeLoaded, - ResourcesRequiredLoaded, - checkIfAllResourcesLoaded, -} from './useResources' +import { ResourcesMaybeLoaded, checkIfAllResourcesLoaded } from './useResources' import { transformUp } from './transformUp' import { InputValues, SubmitValues } from './types' -import { useApp } from '../app' +import { getPatchPayloads } from './patchPayloads' +import { useApp } from '../../contexts/app' export function useOnValid({ resources, @@ -33,9 +30,9 @@ export function useOnValid({ const isAutopilotEnabled = !!app.autopilotInfo.data?.isAutopilotEnabled const autopilotTrigger = useAutopilotTrigger() const autopilotUpdate = useAutopilotConfigUpdate() - const settingsGougingUpdate = useSettingsGougingUpdate() - const settingsPinnedUpdate = useSettingsPinnedUpdate() - const settingsUploadUpdate = useSettingsUploadUpdate() + const settingsGougingPatch = useSettingsGougingPatch() + const settingsPinnedPatch = useSettingsPinnedPatch() + const settingsUploadPatch = useSettingsUploadPatch() const renterdState = useBusState() const mutate = useMutate() const onValid = useCallback( @@ -49,45 +46,63 @@ export function useOnValid({ isAutopilotEnabled && !resources.autopilot.data const { payloads } = transformUp({ - resources: resources as ResourcesRequiredLoaded, + resources: loaded, renterdState: renterdState.data, isAutopilotEnabled, values: values as SubmitValues, }) - const autopilotResponse = payloads.autopilot - ? await autopilotUpdate.put({ + const patches = getPatchPayloads({ + resources: loaded, + payloads, + }) + + const promises = [] + + if (isAutopilotEnabled && payloads.autopilot && patches.autopilot) { + promises.push( + autopilotUpdate.put({ payload: payloads.autopilot, }) - : undefined - - const [gougingResponse, pinnedResponse, uploadResponse] = - await Promise.all([ - settingsGougingUpdate.put({ - payload: payloads.gouging, - }), - settingsPinnedUpdate.put({ - payload: payloads.pinned, - }), - settingsUploadUpdate.put({ - payload: payloads.upload, - }), - ]) - - const error = - autopilotResponse?.error || - gougingResponse.error || - pinnedResponse.error || - uploadResponse.error - if (error) { + ) + } + + if (patches.gouging) { + promises.push( + settingsGougingPatch.patch({ + payload: patches.gouging, + }) + ) + } + + if (patches.pinned) { + promises.push( + settingsPinnedPatch.patch({ + payload: patches.pinned, + }) + ) + } + + if (patches.upload) { + promises.push( + settingsUploadPatch.patch({ + payload: patches.upload, + }) + ) + } + + const results = await Promise.all(promises) + + const err = results.find((result) => result.error) + if (err) { triggerErrorToast({ title: 'Error updating configuration', - body: error, + body: err.error, }) return } - if (isAutopilotEnabled && payloads.autopilot) { + if (isAutopilotEnabled && patches.autopilot) { // Trigger the autopilot loop with new settings applied. autopilotTrigger.post({ payload: { @@ -116,11 +131,11 @@ export function useOnValid({ resources, renterdState.data, isAutopilotEnabled, - autopilotUpdate, - settingsGougingUpdate, - settingsPinnedUpdate, - settingsUploadUpdate, revalidateAndResetForm, + autopilotUpdate, + settingsGougingPatch, + settingsPinnedPatch, + settingsUploadPatch, autopilotTrigger, mutate, ]