Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/rotten-rocks-argue.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@svenvw/fdm-app": minor
---

Now some application calculation results are cached in the browser local storage. The server only performs a cached calculation again if the cache's input hash doesn't match the current input's hash.
6 changes: 4 additions & 2 deletions fdm-app/app/components/blocks/header/balance.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,10 @@ export function HeaderBalance({
<>
<BreadcrumbSeparator />
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink href={`/farm/${b_id_farm}/${calendar}/balance`}>
Nutriëntenbalans
<BreadcrumbLink asChild>
<NavLink to={`/farm/${b_id_farm}/${calendar}/balance`}>
Nutriëntenbalans
</NavLink>
</BreadcrumbLink>
</BreadcrumbItem>
<BreadcrumbSeparator />
Expand Down
11 changes: 7 additions & 4 deletions fdm-app/app/components/blocks/header/nutrient-advice.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,13 @@ export function HeaderNutrientAdvice({
<>
<BreadcrumbSeparator />
<BreadcrumbItem className="hidden md:block">
<BreadcrumbLink
href={`/farm/${b_id_farm}/${calendar}/nutrient_advice`}
>
Bemestingsadvies
<BreadcrumbLink asChild>
<NavLink
reloadDocument
to={`/farm/${b_id_farm}/${calendar}/nutrient_advice/${b_id}${location.search}`}
>
Bemestingsadvies
</NavLink>
</BreadcrumbLink>
</BreadcrumbItem>
{b_id ? (
Expand Down
47 changes: 47 additions & 0 deletions fdm-app/app/lib/middleware.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { redirect } from "react-router"
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

React router supports middleware as a stable feature since v7.9.0. Have you considered using those functions? https://reactrouter.com/how-to/middleware

import type { Route } from "../+types/root"
import type { CacheStore, DataWithInputHash } from "../store/calculation-cache"

/**
* Client middleware that redirects with the most recent cacheHash, obtained from the provided cache store, when the route matches the provided matcher, if needed.
*
* @param matcherProvider function that return a regexp that matches url strings like `/farm/b_id_farm/calendar/balance/nitrogen`
* @param storeProvider cache store to use for this match
* @param getId function to oobtain the id out of the client middleware function args
*
* @returns a client middleware function that either throws redirect or calls next as needed
*/
export function splatCacheMiddleware<T extends DataWithInputHash>(
matcherProvider: () => RegExp,
storeProvider: () => CacheStore<T>,
getId: (args: Parameters<Route.ClientMiddlewareFunction>[0]) => string,
): Route.ClientMiddlewareFunction {
return (args, next) => {
const { request } = args
if (typeof window === "undefined") return next()

const requestUrl = new URL(request.url)
if (!matcherProvider().test(requestUrl.pathname)) return next()

const previousCacheHash = requestUrl.searchParams.get("cacheHash")
let newCacheHash: string | null = previousCacheHash

// Get cache hash for the cache we (possibly) have
const cachedData = storeProvider().get(getId(args))
if (cachedData?.inputHash) {
newCacheHash = cachedData.inputHash
} else {
newCacheHash = null
}

// Redirect if the `cacheHash` search param was wrong
if (previousCacheHash !== newCacheHash) {
newCacheHash
? requestUrl.searchParams.set("cacheHash", newCacheHash)
: requestUrl.searchParams.delete("cacheHash")
throw redirect(requestUrl.toString())
}

return next()
}
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -11,14 +11,16 @@ import {
CircleAlert,
CircleCheck,
} from "lucide-react"
import { Suspense, use } from "react"
import hash from "object-hash"
import { Suspense, use, useEffect } from "react"
import {
data,
type LoaderFunctionArgs,
type MetaFunction,
NavLink,
useLoaderData,
useLocation,
useSearchParams,
} from "react-router"
import { NitrogenBalanceChart } from "~/components/blocks/balance/nitrogen-chart"
import NitrogenBalanceDetails from "~/components/blocks/balance/nitrogen-details"
Expand All @@ -38,6 +40,7 @@ import { clientConfig } from "~/lib/config"
import { fdm } from "~/lib/fdm.server"
import { useCalendarStore } from "~/store/calendar"
import { serverConfig } from "../lib/config.server"
import { useFieldNitrogenBalanceCache } from "../store/calculation-cache"

// Meta
export const meta: MetaFunction = () => {
Expand Down Expand Up @@ -91,6 +94,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
// Get details of field
const field = await getField(fdm, session.principal_id, b_id)

const url = new URL(request.url)
const cacheHash = url.searchParams.get("cacheHash")

// Return promise directly for React Router v7 Suspense pattern
const nitrogenBalancePromise = collectInputForNitrogenBalance(
fdm,
Expand All @@ -100,6 +106,10 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
datasetsUrl,
)
.then(async (input) => {
const inputHash = hash(input)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are various typescript warnings with this block of code. Please check

if (inputHash === cacheHash) {
return { useCache: true }
}
const result = await calculateNitrogenBalance(input)
return {
input: input.fields.find(
Expand All @@ -110,6 +120,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
(field: { b_id: string }) => field.b_id === b_id,
),
errorMessage: null,
inputHash: inputHash,
}
})
.catch((error) => ({
Expand Down Expand Up @@ -154,12 +165,38 @@ function NitrogenBalance({
field,
nitrogenBalanceResult,
}: Awaited<ReturnType<typeof loader>>) {
const { input, result, errorMessage } = use(nitrogenBalanceResult)
const data = use(nitrogenBalanceResult)

const location = useLocation()
const [searchParams, setSearchParams] = useSearchParams()
const page = location.pathname
const calendar = useCalendarStore((state) => state.calendar)

const fieldNitrogenBalanceCache = useFieldNitrogenBalanceCache()
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would prefer to move this into a separate function. The idea is that we can obtain the results of this calculation on various pages with a single function that handles the input collection, cache checking, calculation and cache storage. Otherwise we have to repeat this code on every page that will use the results of the calculation


const cachedData = fieldNitrogenBalanceCache.get(field.b_id)

useEffect(() => {
if (
(!data.useCache || !cachedData?.inputHash) &&
!data.errorMessage &&
data.inputHash
) {
fieldNitrogenBalanceCache.set(field.b_id, data)
}
}, [field.b_id, data, cachedData?.inputHash, fieldNitrogenBalanceCache.set])

if (data.useCache && !cachedData && searchParams.get("cacheHash")) {
setSearchParams((searchParams) => {
searchParams.delete("cacheHash")
return searchParams
})
return null
}

const { input, result, errorMessage } =
data.useCache && cachedData ? cachedData : data

Comment thread
coderabbitai[bot] marked this conversation as resolved.
if (!input) {
return (
<div className="flex items-center justify-center">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@ import {
CircleAlert,
CircleCheck,
} from "lucide-react"
import { Suspense, use } from "react"
import hash from "object-hash"
import { Suspense, use, useEffect } from "react"
import {
data,
type LoaderFunctionArgs,
type MetaFunction,
NavLink,
useLoaderData,
useLocation,
useSearchParams,
} from "react-router"
import { NitrogenBalanceChart } from "~/components/blocks/balance/nitrogen-chart"
import { NitrogenBalanceFallback } from "~/components/blocks/balance/skeletons"
Expand All @@ -36,6 +38,7 @@ import { getTimeframe } from "~/lib/calendar"
import { clientConfig } from "~/lib/config"
import { fdm } from "~/lib/fdm.server"
import { useFieldFilterStore } from "~/store/field-filter"
import { useFarmNitrogenBalanceCache } from "../store/calculation-cache"

// Meta
export const meta: MetaFunction = () => {
Expand Down Expand Up @@ -78,6 +81,9 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
// Get details of fields
const fields = await getFields(fdm, session.principal_id, b_id_farm)

const url = new URL(request.url)
const cacheHash = url.searchParams.get("cacheHash")

const asyncData = (async () => {
// Collect input data for nutrient balance calculation
const nitrogenBalanceInput = await collectInputForNitrogenBalance(
Expand All @@ -87,6 +93,11 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
timeframe,
)

const inputHash = hash(nitrogenBalanceInput)
if (inputHash === cacheHash) {
return { useCache: true }
}

let nitrogenBalanceResult = null as NitrogenBalanceNumeric | null
let errorMessage = null as string | null
try {
Expand All @@ -99,6 +110,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
return {
nitrogenBalanceResult: nitrogenBalanceResult,
errorMessage: errorMessage,
inputHash: inputHash,
}
})()

Expand Down Expand Up @@ -140,10 +152,42 @@ function FarmBalanceNitrogenOverview({
}: Awaited<ReturnType<typeof loader>>) {
const location = useLocation()
const page = location.pathname
const { nitrogenBalanceResult, errorMessage } = use(asyncData)
const [searchParams, setSearchParams] = useSearchParams()
const data = use(asyncData)
const { showProductiveOnly } = useFieldFilterStore()

const resolvedNitrogenBalanceResult = nitrogenBalanceResult
const farmNitrogenBalanceCache = useFarmNitrogenBalanceCache()

const cachedData = farmNitrogenBalanceCache.get(farm.b_id_farm)

useEffect(() => {
if (
(!data.useCache || !cachedData?.inputHash) &&
!data.errorMessage &&
data.inputHash
) {
farmNitrogenBalanceCache.set(farm.b_id_farm, data)
}
}, [
farm.b_id_farm,
data,
cachedData?.inputHash,
farmNitrogenBalanceCache.set,
])

if (data.useCache && !cachedData && searchParams.get("cacheHash")) {
setSearchParams((searchParams) => {
searchParams.delete("cacheHash")
return searchParams
})
return null
}

const {
nitrogenBalanceResult: resolvedNitrogenBalanceResult,
errorMessage,
} = data.useCache && cachedData ? cachedData : data

if (errorMessage) {
return (
<div className="flex items-center justify-center">
Expand Down
49 changes: 49 additions & 0 deletions fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { redirect } from "react-router"
import { splatCacheMiddleware } from "~/lib/middleware"
import {
useFarmNitrogenBalanceCache,
useFieldNitrogenBalanceCache,
} from "~/store/calculation-cache"
import type { Route } from "./+types/farm.$b_id_farm.$calendar.balance"

// In case the user navigated directly by URL
export function loader({ params, request }: Route.LoaderArgs) {
if (/\/balance\/?($|\?)/.test(request.url)) {
throw redirect(
`/farm/${params.b_id_farm}/${params.calendar}/balance/nitrogen`,
)
}

return {}
}

// In case the user navigated within the application
const redirectMiddleware: Route.ClientMiddlewareFunction = (
{ request, params },
next,
) => {
if (/\/balance\/?($|\?)/.test(request.url)) {
throw redirect(
`/farm/${params.b_id_farm}/${params.calendar}/balance/nitrogen`,
)
}

return next()
}

export const clientMiddleware = [
// Redirect to nitrogen balance if what kind of balance analysis needed is not known yet
redirectMiddleware,
// Farm nitrogen
splatCacheMiddleware(
() => /\/nitrogen\/?$/,
() => useFarmNitrogenBalanceCache.getState(),
({ params }) => params.b_id_farm || "",
),
// Field nitrogen
splatCacheMiddleware(
() => /\/nitrogen\/.+\/?$/,
() => useFieldNitrogenBalanceCache.getState(),
({ params }) => params.b_id || "",
),
]
Loading