diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx
index 97c8ed505..72191d79b 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.nitrogen._index.tsx
@@ -12,7 +12,8 @@ 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,
@@ -20,6 +21,7 @@ import {
NavLink,
useLoaderData,
useLocation,
+ useSearchParams,
} from "react-router"
import { NitrogenBalanceChart } from "~/components/blocks/balance/nitrogen-chart"
import { NitrogenBalanceFallback } from "~/components/blocks/balance/skeletons"
@@ -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 = () => {
@@ -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(
@@ -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 {
@@ -99,6 +110,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
return {
nitrogenBalanceResult: nitrogenBalanceResult,
errorMessage: errorMessage,
+ inputHash: inputHash,
}
})()
@@ -140,10 +152,42 @@ function FarmBalanceNitrogenOverview({
}: Awaited
>) {
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 (
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx
new file mode 100644
index 000000000..20e139081
--- /dev/null
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.balance.tsx
@@ -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 || "",
+ ),
+]
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
index fd3f464de..6f7dbd37b 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.norms.tsx
@@ -4,14 +4,17 @@ import {
type GebruiksnormResult,
} from "@svenvw/fdm-calculator"
import { getFarm, getFarms, getFields } from "@svenvw/fdm-core"
-import { Suspense, use } from "react"
+import hash from "object-hash"
+import { Suspense, use, useEffect } from "react"
import {
data,
type LoaderFunctionArgs,
type MetaFunction,
NavLink,
+ redirect,
useLoaderData,
useLocation,
+ useSearchParams,
} from "react-router"
import { FarmTitle } from "~/components/blocks/farm/farm-title"
import { Header } from "~/components/blocks/header/base"
@@ -30,7 +33,9 @@ import { getCalendar, getTimeframe } from "~/lib/calendar"
import { clientConfig } from "~/lib/config"
import { handleLoaderError } from "~/lib/error"
import { fdm } from "~/lib/fdm.server"
+import { useFarmNormsCache } from "~/store/calculation-cache"
import { useFieldFilterStore } from "~/store/field-filter"
+import type { Route } from "./+types/farm.$b_id_farm.$calendar.norms"
interface FieldNorm {
b_id: string
@@ -119,19 +124,45 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
let errorMessage = null as string | null
let hasFieldNormErrors = false
const fieldErrorMessages: string[] = []
+
+ const url = new URL(request.url)
+ const cacheHash = url.searchParams.get("cacheHash")
+
+ let inputHash: string | undefined
try {
// Calculate norms per field
const functionsForms = createFunctionsForNorms("NL", calendar)
- const fieldNormPromises = fields.map(async (field) => {
+ const inputPromises = fields.map(async (field) => {
try {
// Collect the input
- const input = await functionsForms.collectInputForNorms(
+ return await functionsForms.collectInputForNorms(
fdm,
session.principal_id,
field.b_id,
)
+ } catch (error) {
+ return {
+ b_id: field.b_id,
+ b_area: field.b_area,
+ errorMessage: String(error).replace("Error: ", ""),
+ }
+ }
+ })
+
+ const inputs = await Promise.all(inputPromises)
+ inputHash = hash(inputs)
+ if (inputHash === cacheHash) {
+ return { useCache: true }
+ }
+
+ const fieldNormPromises = inputs.map(async (input) => {
+ if (input.errorMessage) {
+ return input
+ }
+ const { field } = input
+ try {
// Calculate the norms
const [normManure, normPhosphate, normNitrogen] =
await Promise.all([
@@ -219,6 +250,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
// Return user information from loader
return {
+ inputHash: inputHash,
errorMessage: errorMessage,
fieldNorms: fieldNorms,
farmNorms: farmNorms,
@@ -241,6 +273,42 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}
}
+/**
+ * Client middleware that redirects with the most recent cacheHash search parameter as needed
+ */
+const clientCacheMiddleware: Route.ClientMiddlewareFunction = async (
+ { params, request },
+ next,
+) => {
+ if (typeof window === "undefined") return next()
+ const requestUrl = new URL(request.url)
+
+ const previousCacheHash = requestUrl.searchParams.get("cacheHash")
+ let newCacheHash: string | null = previousCacheHash
+
+ // Get cache hash for the cache we (possibly) have
+ const cachedData = useFarmNormsCache.getState().get(params.b_id_farm)
+ 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()
+}
+
+export const clientMiddleware: Route.ClientMiddlewareFunction[] = [
+ clientCacheMiddleware,
+]
+
export default function FarmNormsBlock() {
const loaderData = useLoaderData()
@@ -281,13 +349,42 @@ export default function FarmNormsBlock() {
* would not render until `asyncData` resolves and the fallback would never be shown.
*/
function Norms(loaderData: Awaited>) {
+ const [searchParams, setSearchParams] = useSearchParams()
+ const data = use(loaderData.asyncData)
+
+ const farmNormsCache = useFarmNormsCache()
+
+ const cachedData = farmNormsCache.get(loaderData.b_id_farm)
+
+ useEffect(() => {
+ if (
+ (!data.useCache || !cachedData?.inputHash) &&
+ !data.errorMessage &&
+ data.inputHash
+ ) {
+ farmNormsCache.set(loaderData.b_id_farm, data)
+ }
+ }, [loaderData.b_id_farm, data, cachedData?.inputHash, farmNormsCache.set])
+
+ if (data.useCache && !cachedData && searchParams.get("cacheHash")) {
+ setSearchParams((searchParams) => {
+ searchParams.delete("cacheHash")
+ return searchParams
+ })
+ return null
+ }
+
const {
farmNorms,
fieldNorms,
errorMessage,
hasFieldNormErrors,
fieldErrorMessages,
- } = use(loaderData.asyncData)
+ } =
+ data.useCache && cachedData
+ ? farmNormsCache.get(loaderData.b_id_farm)
+ : data
+
const { showProductiveOnly } = useFieldFilterStore()
const location = useLocation()
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx
index ef0605278..d95059743 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.$b_id.tsx
@@ -6,12 +6,14 @@ import {
getFertilizers,
getField,
} from "@svenvw/fdm-core"
-import { Suspense, use } from "react"
+import hash from "object-hash"
+import { Suspense, use, useEffect } from "react"
import {
type LoaderFunctionArgs,
type MetaFunction,
useLoaderData,
useLocation,
+ useSearchParams,
} from "react-router"
import { FieldNutrientAdviceLayout } from "~/components/blocks/nutrient-advice/layout"
import { getNutrientsDescription } from "~/components/blocks/nutrient-advice/nutrients"
@@ -28,6 +30,7 @@ import { getCalendar, getTimeframe } from "~/lib/calendar"
import { clientConfig } from "~/lib/config"
import { handleLoaderError } from "~/lib/error"
import { fdm } from "~/lib/fdm.server"
+import { useFieldNutrientAdviceCache } from "../store/calculation-cache"
// Meta
export const meta: MetaFunction = () => {
@@ -75,6 +78,8 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
const field = await getField(fdm, session.principal_id, b_id)
+ const url = new URL(request.url)
+ const cacheHash = url.searchParams.get("cacheHash")
const asyncData = (async () => {
try {
const currentSoilData = getCurrentSoilData(
@@ -120,6 +125,16 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
// For now take the first cultivation
const b_lu_catalogue = cultivations[0].b_lu_catalogue
+ const inputHash = hash([
+ resolvedCurrentSoilData,
+ resolvedFertilizerApplications,
+ resolvedFertilizers,
+ b_lu_catalogue,
+ ])
+ if (inputHash === cacheHash) {
+ return { useCache: true }
+ }
+
const doses = calculateDose({
applications: resolvedFertilizerApplications,
fertilizers: resolvedFertilizers,
@@ -138,6 +153,7 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
fertilizerApplications: resolvedFertilizerApplications,
fertilizers: resolvedFertilizers,
errorMessage: undefined,
+ inputHash: inputHash,
}
} catch (error) {
return { errorMessage: String(error).replace("Error: ", "") }
@@ -211,8 +227,42 @@ function FieldNutrientAdvice({
traceNutrients: NutrientDescription[]
}) {
const { field, calendar, nutrientsDescription } = loaderData
- const asyncData = use(loaderData.asyncData)
+ const serverAsyncData = use(loaderData.asyncData)
const location = useLocation()
+ const [searchParams, setSearchParams] = useSearchParams()
+
+ const fieldNutrientAdviceCache = useFieldNutrientAdviceCache()
+ const cachedData = fieldNutrientAdviceCache.get(field.b_id)
+ useEffect(() => {
+ if (
+ (!serverAsyncData.useCache || !cachedData?.inputHash) &&
+ !serverAsyncData.errorMessage &&
+ serverAsyncData.inputHash
+ ) {
+ fieldNutrientAdviceCache.set(field.b_id, serverAsyncData)
+ }
+ }, [
+ field.b_id,
+ serverAsyncData,
+ cachedData?.inputHash,
+ fieldNutrientAdviceCache.set,
+ ])
+
+ if (
+ serverAsyncData.useCache &&
+ !cachedData &&
+ searchParams.get("cacheHash")
+ ) {
+ setSearchParams((searchParams) => {
+ searchParams.delete("cacheHash")
+ return searchParams
+ })
+ return null
+ }
+
+ const asyncData = (
+ serverAsyncData.useCache ? cachedData : serverAsyncData
+ ) as typeof serverAsyncData
if (typeof asyncData.errorMessage === "string") {
return (
diff --git a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx
index 54e8cf3fb..44df482e5 100644
--- a/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx
+++ b/fdm-app/app/routes/farm.$b_id_farm.$calendar.nutrient_advice.tsx
@@ -4,6 +4,7 @@ import {
type LoaderFunctionArgs,
type MetaFunction,
Outlet,
+ redirect,
useLoaderData,
} from "react-router"
import { FarmTitle } from "~/components/blocks/farm/farm-title"
@@ -16,6 +17,9 @@ import { getTimeframe } from "~/lib/calendar"
import { clientConfig } from "~/lib/config"
import { handleLoaderError } from "~/lib/error"
import { fdm } from "~/lib/fdm.server"
+import { splatCacheMiddleware } from "../lib/middleware"
+import { useFieldNutrientAdviceCache } from "../store/calculation-cache"
+import type { Route } from "./+types/farm.$b_id_farm.$calendar.nutrient_advice"
// Meta
export const meta: MetaFunction = () => {
@@ -118,6 +122,43 @@ export async function loader({ request, params }: LoaderFunctionArgs) {
}
}
+// In case the user navigated within the application
+const redirectMiddleware: Route.ClientMiddlewareFunction = (
+ { request, params },
+ next,
+) => {
+ const url = new URL(request.url)
+
+ if (/\/nutrient_advice\/?$/.test(url.pathname)) {
+ const nutrientAdviceCache = useFieldNutrientAdviceCache.getState()
+
+ const cachedFieldId = Object.keys(nutrientAdviceCache.db)[0]
+
+ if (cachedFieldId) {
+ const cachedData = nutrientAdviceCache.get(cachedFieldId)
+
+ if (cachedData?.inputHash) {
+ throw redirect(
+ `/farm/${params.b_id_farm}/${params.calendar}/nutrient_advice/${cachedFieldId}?cacheHash=${cachedData.inputHash}`,
+ )
+ }
+ }
+ }
+
+ return next()
+}
+
+export const clientMiddleware = [
+ // Redirect to nitrogen balance if what kind of balance analysis needed is not known yet
+ redirectMiddleware,
+ // Farm nitrogen
+ splatCacheMiddleware(
+ () => /\/nutrient_advice\/.+\/?$/,
+ () => useFieldNutrientAdviceCache.getState(),
+ ({ params }) => params.b_id || "",
+ ),
+]
+
/**
* Renders the layout for managing farm settings.
*
diff --git a/fdm-app/app/store/calculation-cache.ts b/fdm-app/app/store/calculation-cache.ts
new file mode 100644
index 000000000..5920b0bd6
--- /dev/null
+++ b/fdm-app/app/store/calculation-cache.ts
@@ -0,0 +1,37 @@
+import { create } from "zustand"
+import { persist } from "zustand/middleware"
+
+export interface DataWithInputHash {
+ inputHash?: string | undefined
+}
+
+export interface CacheStore {
+ db: Record
+ get: (id: string) => T | undefined
+ set: (id: string, val: T) => void
+}
+
+function createCache(name: string) {
+ return create(
+ persist>(
+ (_set, _get) => ({
+ db: {},
+ get: (id) => _get().db[id],
+ set: (id, val) => _set({ db: { ..._get().db, [id]: val } }),
+ }),
+ {
+ name,
+ version: `fdm-calculator:${PUBLIC_FDM_CALCULATOR_VERSION}`,
+ },
+ ),
+ )
+}
+
+export const useFarmNormsCache = createCache("farm-norms-cache")
+export const useFarmNitrogenBalanceCache = createCache(
+ "farm-nitrogen-balance-cache",
+)
+export const useFieldNitrogenBalanceCache = createCache(
+ "field-nitrogen-balance-cache",
+)
+export const useFieldNutrientAdviceCache = createCache("field-norms-cache")
diff --git a/fdm-app/package.json b/fdm-app/package.json
index fa98e58c9..8ac23b3de 100644
--- a/fdm-app/package.json
+++ b/fdm-app/package.json
@@ -52,6 +52,7 @@
"lucide-react": "^0.544.0",
"mapbox-gl": "^3.15.0",
"next-themes": "^0.4.6",
+ "object-hash": "^3.0.0",
"postgres": "^3.4.7",
"posthog-js": "^1.266.0",
"posthog-node": "^5.8.4",
@@ -93,6 +94,7 @@
"@types/mapbox-gl": "^3.4.1",
"@types/mapbox__geojson-extent": "^1.0.3",
"@types/mapbox__mapbox-gl-geocoder": "^5.0.0",
+ "@types/object-hash": "^3.0.6",
"@types/react": "^19.1.13",
"@types/react-dom": "^19.1.9",
"@types/react-map-gl": "^6.1.7",
diff --git a/fdm-app/vite.config.ts b/fdm-app/vite.config.ts
index 8b7b5fa19..9ea1230f6 100644
--- a/fdm-app/vite.config.ts
+++ b/fdm-app/vite.config.ts
@@ -1,3 +1,5 @@
+import fs from "node:fs/promises"
+
import { reactRouter } from "@react-router/dev/vite"
import {
type SentryReactRouterBuildOptions,
@@ -7,7 +9,16 @@ import tailwindcss from "@tailwindcss/vite"
import { defineConfig } from "vite"
import tsconfigPaths from "vite-tsconfig-paths"
-export default defineConfig((config) => {
+export default defineConfig(async (config) => {
+ // We need to go one directory up since package.json is not inside the dist folder
+ const fdmCalculatorPackageJsonPath = new URL(
+ "../package.json",
+ import.meta.resolve("@svenvw/fdm-calculator"),
+ )
+ const fdmCalculatorPackage = JSON.parse(
+ await fs.readFile(fdmCalculatorPackageJsonPath, { encoding: "utf-8" }),
+ )
+
return {
plugins: [
reactRouter(),
@@ -35,6 +46,9 @@ export default defineConfig((config) => {
envPrefix: "PUBLIC_",
define: {
global: {},
+ PUBLIC_FDM_CALCULATOR_VERSION: JSON.stringify(
+ fdmCalculatorPackage.version || "0.7.0",
+ ),
},
ssr: {
noExternal: ["posthog-js", "posthog-js/react"],
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index abaf3dcbd..1a5a5ec26 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -193,6 +193,9 @@ importers:
next-themes:
specifier: ^0.4.6
version: 0.4.6(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
+ object-hash:
+ specifier: ^3.0.0
+ version: 3.0.0
postgres:
specifier: ^3.4.7
version: 3.4.7
@@ -302,6 +305,9 @@ importers:
'@types/mapbox__mapbox-gl-geocoder':
specifier: ^5.0.0
version: 5.0.0
+ '@types/object-hash':
+ specifier: ^3.0.6
+ version: 3.0.6
'@types/react':
specifier: ^19.1.13
version: 19.1.13
@@ -4823,6 +4829,9 @@ packages:
'@types/normalize-package-data@2.4.4':
resolution: {integrity: sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==}
+ '@types/object-hash@3.0.6':
+ resolution: {integrity: sha512-fOBV8C1FIu2ELinoILQ+ApxcUKz4ngq+IWUYrxSGjXzzjUALijilampwkMgEtJ+h2njAW3pi853QpzNVCHB73w==}
+
'@types/pbf@3.0.5':
resolution: {integrity: sha512-j3pOPiEcWZ34R6a6mN07mUkM4o4Lwf6hPNt8eilOeZhTFbxFXmKhvXl9Y28jotFPaI1bpPDJsbCprUoNke6OrA==}
@@ -8084,6 +8093,10 @@ packages:
resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==}
engines: {node: '>=0.10.0'}
+ object-hash@3.0.0:
+ resolution: {integrity: sha512-RSn9F68PjH9HqtltsSnqYC1XXoWe9Bju5+213R98cNGttag9q9yAOTzdbsqvIa7aNm5WffBZFpWYr2aWrklWAw==}
+ engines: {node: '>= 6'}
+
object-inspect@1.13.4:
resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==}
engines: {node: '>= 0.4'}
@@ -16671,6 +16684,8 @@ snapshots:
'@types/normalize-package-data@2.4.4': {}
+ '@types/object-hash@3.0.6': {}
+
'@types/pbf@3.0.5': {}
'@types/pg-pool@2.0.6':
@@ -20419,6 +20434,8 @@ snapshots:
object-assign@4.1.1: {}
+ object-hash@3.0.0: {}
+
object-inspect@1.13.4: {}
object-keys@1.1.1: {}