Merged in feature/warmup (pull request #1887)
* unified warmup function Approved-by: Linus Flood
This commit is contained in:
139
apps/scandic-web/app/api/web/warmup/route.ts
Normal file
139
apps/scandic-web/app/api/web/warmup/route.ts
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { type NextRequest, NextResponse } from "next/server"
|
||||||
|
|
||||||
|
import { env } from "@/env/server"
|
||||||
|
|
||||||
|
import { warmup } from "@/services/warmup"
|
||||||
|
import { isWarmupKey } from "@/services/warmup/warmupKeys"
|
||||||
|
import { createLogger } from "@/utils/logger"
|
||||||
|
|
||||||
|
export const dynamic = "force-dynamic"
|
||||||
|
|
||||||
|
const logger = createLogger("Warmup")
|
||||||
|
|
||||||
|
export async function GET(req: NextRequest) {
|
||||||
|
const url = new URL(req.url)
|
||||||
|
const key = url.searchParams.get("key")
|
||||||
|
|
||||||
|
if (!isAuthroized(req)) {
|
||||||
|
return unauthorizedResponse()
|
||||||
|
}
|
||||||
|
|
||||||
|
const executionStart = performance.now()
|
||||||
|
|
||||||
|
if (!isWarmupKey(key)) {
|
||||||
|
return invalidKeyResponse(key)
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.debug("Warming up:", key)
|
||||||
|
const warmupResult = await warmup(key)
|
||||||
|
const executionTime = performance.now() - executionStart
|
||||||
|
|
||||||
|
switch (warmupResult.status) {
|
||||||
|
case "completed":
|
||||||
|
return warmupCompletedResponse(warmupResult, key, executionTime)
|
||||||
|
case "error":
|
||||||
|
return warmupErrorResponse(warmupResult, key, executionTime)
|
||||||
|
case "skipped":
|
||||||
|
return warmupSkippedResponse(warmupResult, key, executionTime)
|
||||||
|
default:
|
||||||
|
const status = (warmupResult as unknown as { status: string }).status
|
||||||
|
throw new Error(`Unknown warmup status '${status}'`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isAuthroized(req: NextRequest) {
|
||||||
|
const authHeader = req.headers.get("Authorization")
|
||||||
|
if (!authHeader || !env.WARMUP_TOKEN) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.split(" ")[1]
|
||||||
|
if (!token) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return token === env.WARMUP_TOKEN
|
||||||
|
}
|
||||||
|
|
||||||
|
function warmupCompletedResponse(
|
||||||
|
warmupResult: Awaited<ReturnType<typeof warmup>>,
|
||||||
|
key: string,
|
||||||
|
executionTime: number
|
||||||
|
) {
|
||||||
|
logger.debug(`Warmup completed: ${key} in ${executionTime.toFixed(2)}ms`)
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
...warmupResult,
|
||||||
|
key,
|
||||||
|
executionTime: `${executionTime.toFixed(2)}ms`,
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function warmupSkippedResponse(
|
||||||
|
warmupResult: Awaited<ReturnType<typeof warmup>>,
|
||||||
|
key: string,
|
||||||
|
executionTime: number
|
||||||
|
) {
|
||||||
|
logger.debug("Warmup skipped:", key)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
status: warmupResult.status,
|
||||||
|
executionTime: `${executionTime.toFixed(2)}ms`,
|
||||||
|
},
|
||||||
|
{ status: 200 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function warmupErrorResponse(
|
||||||
|
warmupResult: Awaited<ReturnType<typeof warmup>>,
|
||||||
|
key: string,
|
||||||
|
executionTime: number
|
||||||
|
) {
|
||||||
|
if (warmupResult.status !== "error") {
|
||||||
|
throw new Error("Warmup result is not an error")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error("Warmup error", {
|
||||||
|
key,
|
||||||
|
error: warmupResult.error.message,
|
||||||
|
})
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
status: warmupResult.status,
|
||||||
|
error: warmupResult.error.message,
|
||||||
|
executionTime: `${executionTime.toFixed(2)}ms`,
|
||||||
|
},
|
||||||
|
{ status: 500 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function unauthorizedResponse() {
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
error: "Unauthorized access",
|
||||||
|
},
|
||||||
|
{ status: 401 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function invalidKeyResponse(key: string | null) {
|
||||||
|
if (isWarmupKey(key)) {
|
||||||
|
throw new Error("Invalid invocation with valid cache key")
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.error(`Invalid key ${key}`)
|
||||||
|
|
||||||
|
return NextResponse.json(
|
||||||
|
{
|
||||||
|
key,
|
||||||
|
error: `Invalid warmup key: '${key}'`,
|
||||||
|
},
|
||||||
|
{ status: 400 }
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -210,17 +210,17 @@ export default function SummaryUI({
|
|||||||
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
<Body color={showMemberPrice ? "red" : "uiTextHighContrast"}>
|
||||||
{showMemberPrice
|
{showMemberPrice
|
||||||
? formatPrice(
|
? formatPrice(
|
||||||
intl,
|
intl,
|
||||||
memberPrice.amount,
|
memberPrice.amount,
|
||||||
memberPrice.currency
|
memberPrice.currency
|
||||||
)
|
)
|
||||||
: formatPrice(
|
: formatPrice(
|
||||||
intl,
|
intl,
|
||||||
room.roomPrice.perStay.local.price,
|
room.roomPrice.perStay.local.price,
|
||||||
room.roomPrice.perStay.local.currency,
|
room.roomPrice.perStay.local.currency,
|
||||||
room.roomPrice.perStay.local.additionalPrice,
|
room.roomPrice.perStay.local.additionalPrice,
|
||||||
room.roomPrice.perStay.local.additionalPriceCurrency
|
room.roomPrice.perStay.local.additionalPriceCurrency
|
||||||
)}
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
<Caption color="uiTextMediumContrast">
|
<Caption color="uiTextMediumContrast">
|
||||||
@@ -272,22 +272,22 @@ export default function SummaryUI({
|
|||||||
</div>
|
</div>
|
||||||
{room.roomFeatures
|
{room.roomFeatures
|
||||||
? room.roomFeatures.map((feature) => (
|
? room.roomFeatures.map((feature) => (
|
||||||
<div className={styles.entry} key={feature.code}>
|
<div className={styles.entry} key={feature.code}>
|
||||||
<div>
|
<div>
|
||||||
|
<Body color="uiTextHighContrast">
|
||||||
|
{feature.description}
|
||||||
|
</Body>
|
||||||
|
</div>
|
||||||
|
|
||||||
<Body color="uiTextHighContrast">
|
<Body color="uiTextHighContrast">
|
||||||
{feature.description}
|
{formatPrice(
|
||||||
|
intl,
|
||||||
|
feature.localPrice.price,
|
||||||
|
feature.localPrice.currency
|
||||||
|
)}
|
||||||
</Body>
|
</Body>
|
||||||
</div>
|
</div>
|
||||||
|
))
|
||||||
<Body color="uiTextHighContrast">
|
|
||||||
{formatPrice(
|
|
||||||
intl,
|
|
||||||
feature.localPrice.price,
|
|
||||||
feature.localPrice.currency
|
|
||||||
)}
|
|
||||||
</Body>
|
|
||||||
</div>
|
|
||||||
))
|
|
||||||
: null}
|
: null}
|
||||||
{room.bedType ? (
|
{room.bedType ? (
|
||||||
<div className={styles.entry}>
|
<div className={styles.entry}>
|
||||||
|
|||||||
@@ -144,8 +144,8 @@ export default function ConfirmationStep({
|
|||||||
label={
|
label={
|
||||||
savedCreditCards?.length
|
savedCreditCards?.length
|
||||||
? intl.formatMessage({
|
? intl.formatMessage({
|
||||||
defaultMessage: "OTHER",
|
defaultMessage: "OTHER",
|
||||||
})
|
})
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -2,14 +2,13 @@
|
|||||||
import { Button as ButtonRAC } from "react-aria-components"
|
import { Button as ButtonRAC } from "react-aria-components"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MaterialIcon
|
MaterialIcon,
|
||||||
,type
|
type MaterialIconProps,
|
||||||
MaterialIconProps} from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
} from "@scandic-hotels/design-system/Icons/MaterialIcon"
|
||||||
import { Typography } from "@scandic-hotels/design-system/Typography"
|
import { Typography } from "@scandic-hotels/design-system/Typography"
|
||||||
|
|
||||||
import styles from "./button.module.css"
|
import styles from "./button.module.css"
|
||||||
|
|
||||||
|
|
||||||
interface ButtonProps extends React.PropsWithChildren {
|
interface ButtonProps extends React.PropsWithChildren {
|
||||||
icon: MaterialIconProps["icon"]
|
icon: MaterialIconProps["icon"]
|
||||||
isDisabled?: boolean
|
isDisabled?: boolean
|
||||||
|
|||||||
@@ -9,14 +9,13 @@ import {
|
|||||||
clearGlaSessionStorage,
|
clearGlaSessionStorage,
|
||||||
readGlaFromSessionStorage,
|
readGlaFromSessionStorage,
|
||||||
} from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/helpers"
|
} from "@/components/HotelReservation/EnterDetails/Payment/PaymentCallback/helpers"
|
||||||
import LoadingSpinner from "@/components/LoadingSpinner"
|
|
||||||
import { trackEvent } from "@/utils/tracking/base"
|
|
||||||
import { buildAncillaries } from "@/utils/tracking/myStay"
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
buildAncillaryPackages,
|
buildAncillaryPackages,
|
||||||
getAncillarySessionData,
|
getAncillarySessionData,
|
||||||
} from "@/components/HotelReservation/MyStay/utils/ancillaries"
|
} from "@/components/HotelReservation/MyStay/utils/ancillaries"
|
||||||
|
import LoadingSpinner from "@/components/LoadingSpinner"
|
||||||
|
import { trackEvent } from "@/utils/tracking/base"
|
||||||
|
import { buildAncillaries } from "@/utils/tracking/myStay"
|
||||||
|
|
||||||
interface TrackGuaranteeProps {
|
interface TrackGuaranteeProps {
|
||||||
status: string
|
status: string
|
||||||
@@ -93,21 +92,21 @@ export default function TrackGuarantee({
|
|||||||
case PaymentCallbackStatusEnum.Cancel:
|
case PaymentCallbackStatusEnum.Cancel:
|
||||||
isAncillaryFlow
|
isAncillaryFlow
|
||||||
? trackAncillaryPaymentEvent(
|
? trackAncillaryPaymentEvent(
|
||||||
"GuaranteeCancelAncillary",
|
"GuaranteeCancelAncillary",
|
||||||
"glacardsavecancelled"
|
"glacardsavecancelled"
|
||||||
)
|
)
|
||||||
: trackGuaranteePaymentEvent(
|
: trackGuaranteePaymentEvent(
|
||||||
"glaCardSaveCancelled",
|
"glaCardSaveCancelled",
|
||||||
"glacardsavecancelled"
|
"glacardsavecancelled"
|
||||||
)
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
case PaymentCallbackStatusEnum.Error:
|
case PaymentCallbackStatusEnum.Error:
|
||||||
isAncillaryFlow
|
isAncillaryFlow
|
||||||
? trackAncillaryPaymentEvent(
|
? trackAncillaryPaymentEvent(
|
||||||
"GuaranteeFailAncillary",
|
"GuaranteeFailAncillary",
|
||||||
"glacardsavefailed"
|
"glacardsavefailed"
|
||||||
)
|
)
|
||||||
: trackGuaranteePaymentEvent("glaCardSaveFailed", "glacardsavefailed")
|
: trackGuaranteePaymentEvent("glaCardSaveFailed", "glacardsavefailed")
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { type TbodyProps,tbodyVariants } from "./variants"
|
import { type TbodyProps, tbodyVariants } from "./variants"
|
||||||
|
|
||||||
export default function Tbody({ border, children }: TbodyProps) {
|
export default function Tbody({ border, children }: TbodyProps) {
|
||||||
const classNames = tbodyVariants({ border })
|
const classNames = tbodyVariants({ border })
|
||||||
|
|||||||
@@ -29,12 +29,8 @@ export default function Campaign({
|
|||||||
roomTypeCode,
|
roomTypeCode,
|
||||||
}: CampaignProps) {
|
}: CampaignProps) {
|
||||||
const intl = useIntl()
|
const intl = useIntl()
|
||||||
const {
|
const { roomNr, selectedFilter, selectedPackages, selectedRate } =
|
||||||
roomNr,
|
useRoomContext()
|
||||||
selectedFilter,
|
|
||||||
selectedPackages,
|
|
||||||
selectedRate,
|
|
||||||
} = useRoomContext()
|
|
||||||
const rateTitles = useRateTitles()
|
const rateTitles = useRateTitles()
|
||||||
|
|
||||||
const isCampaignRate = campaign.some(
|
const isCampaignRate = campaign.some(
|
||||||
@@ -78,21 +74,21 @@ export default function Campaign({
|
|||||||
|
|
||||||
const rateTermDetails = product.rateDefinitionMember
|
const rateTermDetails = product.rateDefinitionMember
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
title: product.rateDefinition.title,
|
title: product.rateDefinition.title,
|
||||||
terms: product.rateDefinition.generalTerms,
|
terms: product.rateDefinition.generalTerms,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: product.rateDefinitionMember.title,
|
title: product.rateDefinitionMember.title,
|
||||||
terms: product.rateDefinition.generalTerms,
|
terms: product.rateDefinition.generalTerms,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
{
|
{
|
||||||
title: product.rateDefinition.title,
|
title: product.rateDefinition.title,
|
||||||
terms: product.rateDefinition.generalTerms,
|
terms: product.rateDefinition.generalTerms,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
|
|
||||||
const isSelected = isSelectedPriceProduct(
|
const isSelected = isSelectedPriceProduct(
|
||||||
product,
|
product,
|
||||||
@@ -127,12 +123,12 @@ export default function Campaign({
|
|||||||
|
|
||||||
const pricePerNightMember = product.member
|
const pricePerNightMember = product.member
|
||||||
? calculatePricePerNightPriceProduct(
|
? calculatePricePerNightPriceProduct(
|
||||||
product.member.localPrice.pricePerNight,
|
product.member.localPrice.pricePerNight,
|
||||||
product.member.requestedPrice?.pricePerNight,
|
product.member.requestedPrice?.pricePerNight,
|
||||||
nights,
|
nights,
|
||||||
pkgsSum.price,
|
pkgsSum.price,
|
||||||
pkgsSumRequested.price
|
pkgsSumRequested.price
|
||||||
)
|
)
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
let approximateRatePrice = undefined
|
let approximateRatePrice = undefined
|
||||||
@@ -148,12 +144,12 @@ export default function Campaign({
|
|||||||
const approximateRate =
|
const approximateRate =
|
||||||
approximateRatePrice && product.public.requestedPrice
|
approximateRatePrice && product.public.requestedPrice
|
||||||
? {
|
? {
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
defaultMessage: "Approx.",
|
defaultMessage: "Approx.",
|
||||||
}),
|
}),
|
||||||
price: approximateRatePrice,
|
price: approximateRatePrice,
|
||||||
unit: product.public.requestedPrice.currency,
|
unit: product.public.requestedPrice.currency,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -167,12 +163,12 @@ export default function Campaign({
|
|||||||
memberRate={
|
memberRate={
|
||||||
pricePerNightMember
|
pricePerNightMember
|
||||||
? {
|
? {
|
||||||
label: intl.formatMessage({
|
label: intl.formatMessage({
|
||||||
defaultMessage: "Member price",
|
defaultMessage: "Member price",
|
||||||
}),
|
}),
|
||||||
price: pricePerNightMember.totalPrice,
|
price: pricePerNightMember.totalPrice,
|
||||||
unit: `${product.member!.localPrice.currency}/${night}`,
|
unit: `${product.member!.localPrice.currency}/${night}`,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
name={`rateCode-${roomNr}-${product.public.rateCode}`}
|
name={`rateCode-${roomNr}-${product.public.rateCode}`}
|
||||||
@@ -186,15 +182,15 @@ export default function Campaign({
|
|||||||
omnibusRate={
|
omnibusRate={
|
||||||
product.public.localPrice.omnibusPricePerNight
|
product.public.localPrice.omnibusPricePerNight
|
||||||
? {
|
? {
|
||||||
label: intl
|
label: intl
|
||||||
.formatMessage({
|
.formatMessage({
|
||||||
defaultMessage: "Lowest price (last 30 days)",
|
defaultMessage: "Lowest price (last 30 days)",
|
||||||
})
|
})
|
||||||
.toUpperCase(),
|
.toUpperCase(),
|
||||||
price:
|
price:
|
||||||
product.public.localPrice.omnibusPricePerNight.toString(),
|
product.public.localPrice.omnibusPricePerNight.toString(),
|
||||||
unit: product.public.localPrice.currency,
|
unit: product.public.localPrice.currency,
|
||||||
}
|
}
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
rateTermDetails={rateTermDetails}
|
rateTermDetails={rateTermDetails}
|
||||||
|
|||||||
@@ -79,9 +79,9 @@ export default function Redemptions({
|
|||||||
additionalPrice:
|
additionalPrice:
|
||||||
additionalPrice && additionalPriceCurrency
|
additionalPrice && additionalPriceCurrency
|
||||||
? {
|
? {
|
||||||
currency: additionalPriceCurrency,
|
currency: additionalPriceCurrency,
|
||||||
price: additionalPrice.toString(),
|
price: additionalPrice.toString(),
|
||||||
}
|
}
|
||||||
: undefined,
|
: undefined,
|
||||||
currency: "PTS",
|
currency: "PTS",
|
||||||
isDisabled: !r.redemption.hasEnoughPoints,
|
isDisabled: !r.redemption.hasEnoughPoints,
|
||||||
|
|||||||
2
apps/scandic-web/env/server.ts
vendored
2
apps/scandic-web/env/server.ts
vendored
@@ -199,6 +199,7 @@ export const env = createEnv({
|
|||||||
// transform to boolean
|
// transform to boolean
|
||||||
.transform((s) => s === "true")
|
.transform((s) => s === "true")
|
||||||
.default("true"),
|
.default("true"),
|
||||||
|
WARMUP_TOKEN: z.string().optional(),
|
||||||
},
|
},
|
||||||
emptyStringAsUndefined: true,
|
emptyStringAsUndefined: true,
|
||||||
runtimeEnv: {
|
runtimeEnv: {
|
||||||
@@ -293,6 +294,7 @@ export const env = createEnv({
|
|||||||
BRANCH: process.env.BRANCH,
|
BRANCH: process.env.BRANCH,
|
||||||
GIT_SHA: process.env.GIT_SHA,
|
GIT_SHA: process.env.GIT_SHA,
|
||||||
ENABLE_WARMUP_HOTEL: process.env.ENABLE_WARMUP_HOTEL,
|
ENABLE_WARMUP_HOTEL: process.env.ENABLE_WARMUP_HOTEL,
|
||||||
|
WARMUP_TOKEN: process.env.WARMUP_TOKEN,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import type { Config, Context } from "@netlify/functions"
|
|
||||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
|
||||||
|
|
||||||
export default async (_request: Request, _context: Context) => {
|
|
||||||
await warmupHotelDataOnLang(Lang.da)
|
|
||||||
return new Response("Warmup success", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
schedule: "0 4 * * *",
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import type { Config, Context } from "@netlify/functions"
|
|
||||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
|
||||||
|
|
||||||
export default async (_request: Request, _context: Context) => {
|
|
||||||
await warmupHotelDataOnLang(Lang.de)
|
|
||||||
return new Response("Warmup success", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
schedule: "5 4 * * *",
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import type { Config, Context } from "@netlify/functions"
|
|
||||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
|
||||||
|
|
||||||
export default async (_request: Request, _context: Context) => {
|
|
||||||
await warmupHotelDataOnLang(Lang.en)
|
|
||||||
return new Response("Warmup success", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
schedule: "10 4 * * *",
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import type { Config, Context } from "@netlify/functions"
|
|
||||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
|
||||||
|
|
||||||
export default async (_request: Request, _context: Context) => {
|
|
||||||
await warmupHotelDataOnLang(Lang.fi)
|
|
||||||
return new Response("Warmup success", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
schedule: "15 4 * * *",
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import type { Config, Context } from "@netlify/functions"
|
|
||||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
|
||||||
|
|
||||||
export default async (_request: Request, _context: Context) => {
|
|
||||||
await warmupHotelDataOnLang(Lang.no)
|
|
||||||
return new Response("Warmup success", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
schedule: "20 4 * * *",
|
|
||||||
}
|
|
||||||
@@ -1,12 +0,0 @@
|
|||||||
import { Lang } from "@/constants/languages"
|
|
||||||
import type { Config, Context } from "@netlify/functions"
|
|
||||||
import { warmupHotelDataOnLang } from "../utils/hoteldata"
|
|
||||||
|
|
||||||
export default async (_request: Request, _context: Context) => {
|
|
||||||
await warmupHotelDataOnLang(Lang.sv)
|
|
||||||
return new Response("Warmup success", { status: 200 })
|
|
||||||
}
|
|
||||||
|
|
||||||
export const config: Config = {
|
|
||||||
schedule: "25 4 * * *",
|
|
||||||
}
|
|
||||||
249
apps/scandic-web/netlify/functions/warmup-background.mts
Normal file
249
apps/scandic-web/netlify/functions/warmup-background.mts
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
import crypto from "crypto"
|
||||||
|
import jwt from "jsonwebtoken"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type WarmupFunctionsKey,
|
||||||
|
warmupKeys,
|
||||||
|
} from "@/services/warmup/warmupKeys"
|
||||||
|
import { safeTry } from "@/utils/safeTry"
|
||||||
|
import { timeout } from "@/utils/timeout"
|
||||||
|
|
||||||
|
import type { Config, Context } from "@netlify/functions"
|
||||||
|
|
||||||
|
async function WarmupHandler(request: Request, context: Context) {
|
||||||
|
const [_, error] = await safeTry(validateRequest(request, context))
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
if (error instanceof Error) {
|
||||||
|
switch (error.message as ErrorCode) {
|
||||||
|
case ErrorCodes.WARMUP_DISABLED:
|
||||||
|
console.warn("[WARMUP] Warmup is disabled")
|
||||||
|
return
|
||||||
|
case ErrorCodes.URLS_DONT_MATCH:
|
||||||
|
// This is expected, this webhook will be called for all deployments
|
||||||
|
// and we only want to warmup the ones that match our URL
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
console.error("[WARMUP] Warmup failed", error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[WARMUP] Request is valid, starting warmup")
|
||||||
|
await performWarmup(context)
|
||||||
|
console.log("[WARMUP] Warmup completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateRequest(
|
||||||
|
request: Request,
|
||||||
|
context: Context
|
||||||
|
): Promise<true> {
|
||||||
|
if (request.method !== "POST") {
|
||||||
|
throw new Error(ErrorCodes.METHOD_NOT_ALLOWED)
|
||||||
|
}
|
||||||
|
|
||||||
|
const warmupEnabled =
|
||||||
|
true ||
|
||||||
|
process.env.WARMUP_ENABLED === "true" ||
|
||||||
|
Netlify.env.get("WARMUP_ENABLED") === "true"
|
||||||
|
|
||||||
|
if (!warmupEnabled) {
|
||||||
|
throw new Error(ErrorCodes.WARMUP_DISABLED)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!request.body) {
|
||||||
|
throw new Error(ErrorCodes.MISSING_BODY)
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await request.text()
|
||||||
|
console.log("[WARMUP] Warmup body", body)
|
||||||
|
const deployment = JSON.parse(body) as DeploymentInfo
|
||||||
|
if (!deployment) {
|
||||||
|
throw new Error(ErrorCodes.UNABLE_TO_PARSE_DEPLOYMENT_INFO)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deployedUrl = deployment.deploy_ssl_url
|
||||||
|
if (deployedUrl !== context.url.origin) {
|
||||||
|
throw new Error(ErrorCodes.URLS_DONT_MATCH)
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log("[WARMUP] Warmup request", deployment)
|
||||||
|
let signature: string
|
||||||
|
try {
|
||||||
|
const headerValue = request.headers.get("x-webhook-signature")
|
||||||
|
if (!headerValue) {
|
||||||
|
throw new Error(ErrorCodes.MISSING_SIGNATURE_HEADER)
|
||||||
|
}
|
||||||
|
signature = headerValue
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("[WARMUP] Failed to parse signature", e)
|
||||||
|
throw new Error(ErrorCodes.FAILED_TO_PARSE_SIGNATURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!signature) {
|
||||||
|
throw new Error(ErrorCodes.MISSING_SIGNATURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
const isValid = await validateSignature(signature, body)
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
throw new Error(ErrorCodes.INVALID_SIGNATURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function performWarmup(context: Context) {
|
||||||
|
for (const key of warmupKeys) {
|
||||||
|
console.log("[WARMUP] Warming up cache", key)
|
||||||
|
await callWarmup(key, context)
|
||||||
|
// allow api to catch up
|
||||||
|
await timeout(1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function callWarmup(key: WarmupFunctionsKey, context: Context) {
|
||||||
|
const baseUrl = context.url.origin
|
||||||
|
|
||||||
|
const url = new URL("/api/web/warmup", baseUrl)
|
||||||
|
url.searchParams.set("key", key)
|
||||||
|
|
||||||
|
const warmupToken =
|
||||||
|
process.env.WARMUP_TOKEN || Netlify.env.get("WARMUP_TOKEN")
|
||||||
|
|
||||||
|
try {
|
||||||
|
const response = await fetch(url, {
|
||||||
|
headers: {
|
||||||
|
cache: "no-store",
|
||||||
|
Authorization: `Bearer ${warmupToken}`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
console.error(
|
||||||
|
`[WARMUP] Warmup failed '${url.href}' with error: ${response.status}: ${response.statusText}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error(
|
||||||
|
`[WARMUP] Warmup failed '${url.href}' with error: ${error instanceof Error ? error.message : error}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WarmupHandler
|
||||||
|
export const config: Config = {
|
||||||
|
method: "POST",
|
||||||
|
}
|
||||||
|
|
||||||
|
async function validateSignature(token: string, buffer: string) {
|
||||||
|
try {
|
||||||
|
const secret =
|
||||||
|
process.env.WARMUP_SIGNATURE_SECRET ||
|
||||||
|
Netlify.env.get("WARMUP_SIGNATURE_SECRET")
|
||||||
|
|
||||||
|
if (!secret) {
|
||||||
|
throw new Error("WARMUP_SIGNATURE_SECRET is not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
const options: jwt.VerifyOptions = {
|
||||||
|
issuer: "netlify",
|
||||||
|
algorithms: ["HS256"],
|
||||||
|
}
|
||||||
|
const decoded = jwt.verify(token, secret, options)
|
||||||
|
const hashedBody = crypto.createHash("sha256").update(buffer).digest("hex")
|
||||||
|
|
||||||
|
if (!hasSha256(decoded)) {
|
||||||
|
console.error(
|
||||||
|
"[WARMUP] Decoded jwt does not contain sha256, unable to verify signature"
|
||||||
|
)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return decoded.sha256 === hashedBody
|
||||||
|
} catch (error) {
|
||||||
|
console.error("[WARMUP] Failed to validate signature", error)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasSha256(decoded: unknown): decoded is { sha256: string } {
|
||||||
|
return (
|
||||||
|
typeof decoded === "object" &&
|
||||||
|
decoded !== null &&
|
||||||
|
"sha256" in decoded &&
|
||||||
|
typeof (decoded as { sha256: unknown }).sha256 === "string"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeploymentInfo = {
|
||||||
|
id: string
|
||||||
|
site_id: string
|
||||||
|
build_id: string
|
||||||
|
state: string
|
||||||
|
name: string
|
||||||
|
url: string
|
||||||
|
ssl_url: string
|
||||||
|
admin_url: string
|
||||||
|
deploy_url: string
|
||||||
|
deploy_ssl_url: string
|
||||||
|
created_at: string
|
||||||
|
updated_at: string
|
||||||
|
user_id: string
|
||||||
|
error_message: string | null
|
||||||
|
required: any[]
|
||||||
|
required_functions: any[]
|
||||||
|
commit_ref: string
|
||||||
|
review_id: string | null
|
||||||
|
branch: string
|
||||||
|
commit_url: string
|
||||||
|
skipped: any
|
||||||
|
locked: any
|
||||||
|
title: string
|
||||||
|
commit_message: string | null
|
||||||
|
review_url: string | null
|
||||||
|
published_at: string | null
|
||||||
|
context: string
|
||||||
|
deploy_time: number
|
||||||
|
available_functions: unknown[]
|
||||||
|
screenshot_url: string | null
|
||||||
|
committer: string
|
||||||
|
skipped_log: any
|
||||||
|
manual_deploy: boolean
|
||||||
|
plugin_state: string
|
||||||
|
lighthouse_plugin_scores: any
|
||||||
|
links: {
|
||||||
|
permalink: string
|
||||||
|
alias: string
|
||||||
|
branch: string | null
|
||||||
|
}
|
||||||
|
framework: string
|
||||||
|
entry_path: string | null
|
||||||
|
views_count: number | null
|
||||||
|
function_schedules: {
|
||||||
|
cron: string
|
||||||
|
name: string
|
||||||
|
}[]
|
||||||
|
public_repo: boolean
|
||||||
|
pending_review_reason: string | null
|
||||||
|
lighthouse: any
|
||||||
|
edge_functions_present: boolean
|
||||||
|
expires_at: string | null
|
||||||
|
blobs_region: string
|
||||||
|
}
|
||||||
|
|
||||||
|
const ErrorCodes = {
|
||||||
|
DEPLOYMENT_NOT_FOUND: "DEPLOYMENT NOT FOUND",
|
||||||
|
FAILED_TO_PARSE_SIGNATURE: "FAILED TO PARSE SIGNATURE",
|
||||||
|
INVALID_DEPLOYMENT_STATE: "INVALID DEPLOYMENT STATE",
|
||||||
|
INVALID_SIGNATURE: "INVALID SIGNATURE",
|
||||||
|
METHOD_NOT_ALLOWED: "METHOD NOT ALLOWED",
|
||||||
|
MISSING_BODY: "MISSING BODY",
|
||||||
|
MISSING_SIGNATURE_HEADER: "MISSING SIGNATURE HEADER",
|
||||||
|
MISSING_SIGNATURE: "MISSING SIGNATURE",
|
||||||
|
UNABLE_TO_PARSE_DEPLOYMENT_INFO: "UNABLE TO PARSE DEPLOYMENT INFO",
|
||||||
|
URLS_DONT_MATCH: "URLS DONT MATCH",
|
||||||
|
WARMUP_DISABLED: "WARMUP IS DISABLED",
|
||||||
|
} as const
|
||||||
|
type ErrorCode = (typeof ErrorCodes)[keyof typeof ErrorCodes]
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { performWarmup } from "./warmup-background.mjs"
|
||||||
|
|
||||||
|
import type { Config, Context } from "@netlify/functions"
|
||||||
|
|
||||||
|
async function WarmupHandler(_request: Request, context: Context) {
|
||||||
|
console.log("[WARMUP] Starting scheduled warmup")
|
||||||
|
await performWarmup(context)
|
||||||
|
console.log("[WARMUP] Scheduled warmup completed")
|
||||||
|
}
|
||||||
|
|
||||||
|
export default WarmupHandler
|
||||||
|
export const config: Config = {
|
||||||
|
schedule: "0 4 * * *",
|
||||||
|
}
|
||||||
@@ -88,6 +88,7 @@
|
|||||||
"input-otp": "^1.4.2",
|
"input-otp": "^1.4.2",
|
||||||
"ioredis": "^5.5.0",
|
"ioredis": "^5.5.0",
|
||||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||||
|
"jsonwebtoken": "^9.0.2",
|
||||||
"libphonenumber-js": "^1.10.60",
|
"libphonenumber-js": "^1.10.60",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"nanoid": "^5.0.9",
|
"nanoid": "^5.0.9",
|
||||||
@@ -125,6 +126,7 @@
|
|||||||
"@types/adm-zip": "^0.5.7",
|
"@types/adm-zip": "^0.5.7",
|
||||||
"@types/jest": "^29.5.12",
|
"@types/jest": "^29.5.12",
|
||||||
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
|
"@types/json-stable-stringify-without-jsonify": "^1.0.2",
|
||||||
|
"@types/jsonwebtoken": "^9",
|
||||||
"@types/lodash-es": "^4",
|
"@types/lodash-es": "^4",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^18",
|
"@types/react": "^18",
|
||||||
|
|||||||
@@ -666,6 +666,7 @@ export const hotelQueryRouter = router({
|
|||||||
lang: lang,
|
lang: lang,
|
||||||
serviceToken: ctx.serviceToken,
|
serviceToken: ctx.serviceToken,
|
||||||
})
|
})
|
||||||
|
|
||||||
return hotels
|
return hotels
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -674,15 +675,14 @@ export const hotelQueryRouter = router({
|
|||||||
return hotelData
|
return hotelData
|
||||||
}
|
}
|
||||||
|
|
||||||
if (warmup) {
|
|
||||||
return await fetchHotels()
|
|
||||||
}
|
|
||||||
|
|
||||||
const cacheClient = await getCacheClient()
|
const cacheClient = await getCacheClient()
|
||||||
return await cacheClient.cacheOrGet(
|
return await cacheClient.cacheOrGet(
|
||||||
`${lang}:getDestinationsMapData`,
|
`${lang}:getDestinationsMapData`,
|
||||||
fetchHotels,
|
fetchHotels,
|
||||||
"max"
|
"max",
|
||||||
|
{
|
||||||
|
cacheStrategy: warmup ? "fetch-then-cache" : "cache-first",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}),
|
}),
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -130,9 +130,11 @@ export async function getCity({
|
|||||||
export async function getCountries({
|
export async function getCountries({
|
||||||
lang,
|
lang,
|
||||||
serviceToken,
|
serviceToken,
|
||||||
|
warmup = false,
|
||||||
}: {
|
}: {
|
||||||
lang: Lang
|
lang: Lang
|
||||||
serviceToken: string
|
serviceToken: string
|
||||||
|
warmup?: boolean
|
||||||
}) {
|
}) {
|
||||||
const cacheClient = await getCacheClient()
|
const cacheClient = await getCacheClient()
|
||||||
return await cacheClient.cacheOrGet(
|
return await cacheClient.cacheOrGet(
|
||||||
@@ -166,7 +168,10 @@ export async function getCountries({
|
|||||||
|
|
||||||
return countries.data
|
return countries.data
|
||||||
},
|
},
|
||||||
"1d"
|
"1d",
|
||||||
|
{
|
||||||
|
cacheStrategy: warmup ? "fetch-then-cache" : "cache-first",
|
||||||
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,5 @@
|
|||||||
|
import type { CacheOrGetOptions } from "./cacheOrGetOptions"
|
||||||
|
|
||||||
const ONE_HOUR_IN_SECONDS = 3_600 as const
|
const ONE_HOUR_IN_SECONDS = 3_600 as const
|
||||||
const ONE_DAY_IN_SECONDS = 86_400 as const
|
const ONE_DAY_IN_SECONDS = 86_400 as const
|
||||||
|
|
||||||
@@ -60,6 +62,7 @@ export type DataCache = {
|
|||||||
* @param key The cache key
|
* @param key The cache key
|
||||||
* @param getDataFromSource An async function that provides a value to cache
|
* @param getDataFromSource An async function that provides a value to cache
|
||||||
* @param ttl Time to live, either a named cache time or a number of seconds
|
* @param ttl Time to live, either a named cache time or a number of seconds
|
||||||
|
* @param opts Options to control cache behavior when retrieving or storing data.
|
||||||
* @returns The cached value or the result from the callback
|
* @returns The cached value or the result from the callback
|
||||||
*/
|
*/
|
||||||
cacheOrGet: <T>(
|
cacheOrGet: <T>(
|
||||||
@@ -67,7 +70,8 @@ export type DataCache = {
|
|||||||
getDataFromSource: (
|
getDataFromSource: (
|
||||||
overrideTTL?: (cacheTime: CacheTime) => void
|
overrideTTL?: (cacheTime: CacheTime) => void
|
||||||
) => Promise<T>,
|
) => Promise<T>,
|
||||||
ttl: CacheTime
|
ttl: CacheTime,
|
||||||
|
opts?: CacheOrGetOptions
|
||||||
) => Promise<T>
|
) => Promise<T>
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CacheOrGetOptions,
|
||||||
|
shouldGetFromCache,
|
||||||
|
} from "../cacheOrGetOptions"
|
||||||
import { cacheLogger } from "../logger"
|
import { cacheLogger } from "../logger"
|
||||||
import { generateCacheKey } from "./generateCacheKey"
|
import { generateCacheKey } from "./generateCacheKey"
|
||||||
import { get } from "./get"
|
import { get } from "./get"
|
||||||
@@ -8,10 +12,15 @@ import { set } from "./set"
|
|||||||
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
||||||
key: string | string[],
|
key: string | string[],
|
||||||
callback: (overrideTTL: (cacheTime: CacheTime) => void) => Promise<T>,
|
callback: (overrideTTL: (cacheTime: CacheTime) => void) => Promise<T>,
|
||||||
ttl: CacheTime
|
ttl: CacheTime,
|
||||||
|
opts?: CacheOrGetOptions
|
||||||
) => {
|
) => {
|
||||||
const cacheKey = generateCacheKey(key)
|
const cacheKey = generateCacheKey(key)
|
||||||
const cachedValue = await get<T>(cacheKey)
|
|
||||||
|
let cachedValue: Awaited<T> | undefined = undefined
|
||||||
|
if (shouldGetFromCache(opts)) {
|
||||||
|
cachedValue = await get<T>(cacheKey)
|
||||||
|
}
|
||||||
|
|
||||||
let realTTL = ttl
|
let realTTL = ttl
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,18 @@
|
|||||||
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
||||||
import { cacheLogger } from "@/services/dataCache/logger"
|
import { cacheLogger } from "@/services/dataCache/logger"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CacheOrGetOptions,
|
||||||
|
shouldGetFromCache,
|
||||||
|
} from "../../cacheOrGetOptions"
|
||||||
import { get } from "./get"
|
import { get } from "./get"
|
||||||
import { set } from "./set"
|
import { set } from "./set"
|
||||||
|
|
||||||
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
||||||
key: string | string[],
|
key: string | string[],
|
||||||
callback: (overrideTTL?: (cacheTime: CacheTime) => void) => Promise<T>,
|
callback: (overrideTTL?: (cacheTime: CacheTime) => void) => Promise<T>,
|
||||||
ttl: CacheTime
|
ttl: CacheTime,
|
||||||
|
opts?: CacheOrGetOptions
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
if (Array.isArray(key)) {
|
if (Array.isArray(key)) {
|
||||||
key = key.join("-")
|
key = key.join("-")
|
||||||
@@ -18,14 +23,16 @@ export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
|||||||
realTTL = cacheTime
|
realTTL = cacheTime
|
||||||
}
|
}
|
||||||
|
|
||||||
const cached = await get(key)
|
let cached: Awaited<T> | undefined = undefined
|
||||||
|
if (shouldGetFromCache(opts)) {
|
||||||
|
cached = await get(key)
|
||||||
|
if (cached) {
|
||||||
|
return cached
|
||||||
|
}
|
||||||
|
|
||||||
if (cached) {
|
cacheLogger.debug(`Miss for key '${key}'`)
|
||||||
return cached as T
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cacheLogger.debug(`Miss for key '${key}'`)
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await callback(overrideTTL)
|
const data = await callback(overrideTTL)
|
||||||
await set(key, data, realTTL)
|
await set(key, data, realTTL)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { unstable_cache } from "next/cache"
|
import { revalidateTag, unstable_cache } from "next/cache"
|
||||||
|
|
||||||
import {
|
import {
|
||||||
type CacheTime,
|
type CacheTime,
|
||||||
@@ -6,18 +6,26 @@ import {
|
|||||||
getCacheTimeInSeconds,
|
getCacheTimeInSeconds,
|
||||||
} from "@/services/dataCache/Cache"
|
} from "@/services/dataCache/Cache"
|
||||||
|
|
||||||
|
import {
|
||||||
|
type CacheOrGetOptions,
|
||||||
|
shouldGetFromCache,
|
||||||
|
} from "../../cacheOrGetOptions"
|
||||||
import { cacheLogger } from "../../logger"
|
import { cacheLogger } from "../../logger"
|
||||||
|
|
||||||
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
||||||
key: string | string[],
|
key: string | string[],
|
||||||
callback: () => Promise<T>,
|
callback: () => Promise<T>,
|
||||||
ttl: CacheTime
|
ttl: CacheTime,
|
||||||
|
opts?: CacheOrGetOptions
|
||||||
): Promise<T> => {
|
): Promise<T> => {
|
||||||
if (!Array.isArray(key)) {
|
if (!Array.isArray(key)) {
|
||||||
key = [key]
|
key = [key]
|
||||||
}
|
}
|
||||||
|
|
||||||
const perf = performance.now()
|
const perf = performance.now()
|
||||||
|
if (!shouldGetFromCache(opts)) {
|
||||||
|
revalidateTag(key[0])
|
||||||
|
}
|
||||||
|
|
||||||
const res = await unstable_cache(callback, key, {
|
const res = await unstable_cache(callback, key, {
|
||||||
revalidate: getCacheTimeInSeconds(ttl),
|
revalidate: getCacheTimeInSeconds(ttl),
|
||||||
|
|||||||
27
apps/scandic-web/services/dataCache/cacheOrGetOptions.ts
Normal file
27
apps/scandic-web/services/dataCache/cacheOrGetOptions.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
/**
|
||||||
|
* Options to control cache behavior when retrieving or storing data.
|
||||||
|
*
|
||||||
|
* - "cache-first": Default behaviour, check if the needed data is available in the cache first. If the data is found, it is returned immediately. Otherwise, the data is fetched and then cached.
|
||||||
|
* - "fetch-then-cache": Always fetch the data first, and then update the cache with the freshly fetched data.
|
||||||
|
*/
|
||||||
|
export type CacheStrategy = "cache-first" | "fetch-then-cache"
|
||||||
|
export type CacheOrGetOptions = {
|
||||||
|
cacheStrategy?: CacheStrategy
|
||||||
|
}
|
||||||
|
|
||||||
|
export function defaultCacheOrGetOptions(
|
||||||
|
opts: CacheOrGetOptions = {}
|
||||||
|
): CacheOrGetOptions {
|
||||||
|
return {
|
||||||
|
cacheStrategy: "cache-first",
|
||||||
|
...opts,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldGetFromCache(
|
||||||
|
opts: CacheOrGetOptions | undefined
|
||||||
|
): boolean {
|
||||||
|
opts = defaultCacheOrGetOptions(opts)
|
||||||
|
|
||||||
|
return opts.cacheStrategy === "cache-first"
|
||||||
|
}
|
||||||
@@ -3,11 +3,10 @@ import { env } from "@/env/server"
|
|||||||
import { isEdge } from "@/utils/isEdge"
|
import { isEdge } from "@/utils/isEdge"
|
||||||
|
|
||||||
import { createMemoryCache } from "./MemoryCache/createMemoryCache"
|
import { createMemoryCache } from "./MemoryCache/createMemoryCache"
|
||||||
|
import { type DataCache } from "./Cache"
|
||||||
import { createDistributedCache } from "./DistributedCache"
|
import { createDistributedCache } from "./DistributedCache"
|
||||||
import { cacheLogger } from "./logger"
|
import { cacheLogger } from "./logger"
|
||||||
|
|
||||||
import type { DataCache } from "./Cache"
|
|
||||||
|
|
||||||
export type { CacheTime, DataCache } from "./Cache"
|
export type { CacheTime, DataCache } from "./Cache"
|
||||||
|
|
||||||
export async function getCacheClient(): Promise<DataCache> {
|
export async function getCacheClient(): Promise<DataCache> {
|
||||||
|
|||||||
50
apps/scandic-web/services/warmup/index.ts
Normal file
50
apps/scandic-web/services/warmup/index.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
import { warmupCountry } from "./warmupCountries"
|
||||||
|
import { warmupHotelData } from "./warmupHotelData"
|
||||||
|
import { warmupHotelIdsByCountry } from "./warmupHotelIdsByCountry"
|
||||||
|
|
||||||
|
import type { WarmupFunctionsKey } from "./warmupKeys"
|
||||||
|
|
||||||
|
export type WarmupFunction = () => Promise<WarmupResult>
|
||||||
|
|
||||||
|
type BaseWarmup = {
|
||||||
|
status: "skipped" | "completed"
|
||||||
|
}
|
||||||
|
|
||||||
|
type FailedWarmup = {
|
||||||
|
status: "error"
|
||||||
|
error: Error
|
||||||
|
}
|
||||||
|
|
||||||
|
export type WarmupResult = BaseWarmup | FailedWarmup
|
||||||
|
|
||||||
|
export const warmupFunctions: Record<WarmupFunctionsKey, WarmupFunction> = {
|
||||||
|
countries_en: warmupCountry(Lang.en),
|
||||||
|
countries_da: warmupCountry(Lang.da),
|
||||||
|
countries_de: warmupCountry(Lang.de),
|
||||||
|
countries_fi: warmupCountry(Lang.fi),
|
||||||
|
countries_sv: warmupCountry(Lang.sv),
|
||||||
|
countries_no: warmupCountry(Lang.no),
|
||||||
|
|
||||||
|
hotelsByCountry: warmupHotelIdsByCountry(),
|
||||||
|
|
||||||
|
hotelData_en: warmupHotelData(Lang.en),
|
||||||
|
hotelData_da: warmupHotelData(Lang.da),
|
||||||
|
hotelData_de: warmupHotelData(Lang.de),
|
||||||
|
hotelData_fi: warmupHotelData(Lang.fi),
|
||||||
|
hotelData_sv: warmupHotelData(Lang.sv),
|
||||||
|
hotelData_no: warmupHotelData(Lang.no),
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function warmup(key: WarmupFunctionsKey): Promise<WarmupResult> {
|
||||||
|
const func = warmupFunctions[key]
|
||||||
|
if (!func) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
error: new Error(`Warmup function ${key} not found`),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return func()
|
||||||
|
}
|
||||||
28
apps/scandic-web/services/warmup/warmupCountries.ts
Normal file
28
apps/scandic-web/services/warmup/warmupCountries.ts
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
import { getCountries } from "@/server/routers/hotels/utils"
|
||||||
|
import { getServiceToken } from "@/server/tokenManager"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
import type { WarmupFunction, WarmupResult } from "."
|
||||||
|
|
||||||
|
export const warmupCountry =
|
||||||
|
(lang: Lang): WarmupFunction =>
|
||||||
|
async (): Promise<WarmupResult> => {
|
||||||
|
try {
|
||||||
|
const serviceToken = await getServiceToken()
|
||||||
|
|
||||||
|
await getCountries({
|
||||||
|
lang: lang,
|
||||||
|
serviceToken: serviceToken.access_token,
|
||||||
|
warmup: true,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
error: error as Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "completed",
|
||||||
|
}
|
||||||
|
}
|
||||||
31
apps/scandic-web/services/warmup/warmupHotelData.ts
Normal file
31
apps/scandic-web/services/warmup/warmupHotelData.ts
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
import { env } from "@/env/server"
|
||||||
|
import { serverClient } from "@/lib/trpc/server"
|
||||||
|
|
||||||
|
import type { Lang } from "@/constants/languages"
|
||||||
|
import type { WarmupFunction, WarmupResult } from "."
|
||||||
|
|
||||||
|
export const warmupHotelData =
|
||||||
|
(lang: Lang): WarmupFunction =>
|
||||||
|
async (): Promise<WarmupResult> => {
|
||||||
|
if (!env.ENABLE_WARMUP_HOTEL) {
|
||||||
|
return {
|
||||||
|
status: "skipped",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await serverClient().hotel.hotels.getDestinationsMapData({
|
||||||
|
lang,
|
||||||
|
warmup: true,
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
error: error as Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "completed",
|
||||||
|
}
|
||||||
|
}
|
||||||
60
apps/scandic-web/services/warmup/warmupHotelIdsByCountry.ts
Normal file
60
apps/scandic-web/services/warmup/warmupHotelIdsByCountry.ts
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
import {
|
||||||
|
getCountries,
|
||||||
|
getHotelIdsByCountry,
|
||||||
|
} from "@/server/routers/hotels/utils"
|
||||||
|
import { getServiceToken } from "@/server/tokenManager"
|
||||||
|
|
||||||
|
import { safeTry } from "@/utils/safeTry"
|
||||||
|
|
||||||
|
import type { WarmupFunction, WarmupResult } from "."
|
||||||
|
|
||||||
|
export const warmupHotelIdsByCountry =
|
||||||
|
(): WarmupFunction => async (): Promise<WarmupResult> => {
|
||||||
|
try {
|
||||||
|
let serviceToken = await getServiceToken()
|
||||||
|
|
||||||
|
const [countries, countriesError] = await safeTry(
|
||||||
|
getCountries({
|
||||||
|
lang: Lang.en,
|
||||||
|
serviceToken: serviceToken.access_token,
|
||||||
|
warmup: true,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (!countries || countriesError) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
error: new Error("Unable to get countries"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const countryNames = countries.data.map((country) => country.name)
|
||||||
|
for (const countryName of countryNames) {
|
||||||
|
serviceToken = await getServiceToken()
|
||||||
|
const [_, error] = await safeTry(
|
||||||
|
getHotelIdsByCountry({
|
||||||
|
country: countryName,
|
||||||
|
serviceToken: serviceToken.access_token,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
console.error(
|
||||||
|
`[Warmup]: Error fetching hotel IDs for ${countryName}:`,
|
||||||
|
error
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
status: "completed",
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
status: "error",
|
||||||
|
error: error as Error,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
apps/scandic-web/services/warmup/warmupKeys.ts
Normal file
20
apps/scandic-web/services/warmup/warmupKeys.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Lang } from "@/constants/languages"
|
||||||
|
|
||||||
|
const langs = Object.keys(Lang) as Lang[]
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Keys for warmup functions, the order of the keys is the order in which they will be executed
|
||||||
|
*/
|
||||||
|
export const warmupKeys = [
|
||||||
|
...langs.map((lang) => `countries_${lang}` as const),
|
||||||
|
"hotelsByCountry",
|
||||||
|
...langs.map((lang) => `hotelData_${lang}` as const),
|
||||||
|
] as const
|
||||||
|
|
||||||
|
export type WarmupFunctionsKey = (typeof warmupKeys)[number]
|
||||||
|
|
||||||
|
export function isWarmupKey(key: unknown): key is WarmupFunctionsKey {
|
||||||
|
return (
|
||||||
|
typeof key === "string" && warmupKeys.includes(key as WarmupFunctionsKey)
|
||||||
|
)
|
||||||
|
}
|
||||||
16
apps/scandic-web/utils/logger/index.ts
Normal file
16
apps/scandic-web/utils/logger/index.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
export function createLogger(loggerPrefix: string | (() => Promise<string>)) {
|
||||||
|
const getLoggerPrefix: () => Promise<string> =
|
||||||
|
typeof loggerPrefix === "string" ? async () => loggerPrefix : loggerPrefix
|
||||||
|
|
||||||
|
return {
|
||||||
|
async debug(message: string, ...args: unknown[]): Promise<void> {
|
||||||
|
console.debug(`[${await getLoggerPrefix()}] ${message}`, ...args)
|
||||||
|
},
|
||||||
|
async warn(message: string, ...args: unknown[]): Promise<void> {
|
||||||
|
console.warn(`[${await getLoggerPrefix()}] Warning - ${message}`, ...args)
|
||||||
|
},
|
||||||
|
async error(message: string, ...args: unknown[]): Promise<void> {
|
||||||
|
console.error(`[${await getLoggerPrefix()}] Error - ${message}`, ...args)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
111
yarn.lock
111
yarn.lock
@@ -6402,6 +6402,7 @@ __metadata:
|
|||||||
"@types/geojson": "npm:^7946.0.16"
|
"@types/geojson": "npm:^7946.0.16"
|
||||||
"@types/jest": "npm:^29.5.12"
|
"@types/jest": "npm:^29.5.12"
|
||||||
"@types/json-stable-stringify-without-jsonify": "npm:^1.0.2"
|
"@types/json-stable-stringify-without-jsonify": "npm:^1.0.2"
|
||||||
|
"@types/jsonwebtoken": "npm:^9"
|
||||||
"@types/lodash-es": "npm:^4"
|
"@types/lodash-es": "npm:^4"
|
||||||
"@types/node": "npm:^20"
|
"@types/node": "npm:^20"
|
||||||
"@types/react": "npm:^18"
|
"@types/react": "npm:^18"
|
||||||
@@ -6446,6 +6447,7 @@ __metadata:
|
|||||||
jiti: "npm:^1.21.0"
|
jiti: "npm:^1.21.0"
|
||||||
json-sort-cli: "npm:^4.0.4"
|
json-sort-cli: "npm:^4.0.4"
|
||||||
json-stable-stringify-without-jsonify: "npm:^1.0.1"
|
json-stable-stringify-without-jsonify: "npm:^1.0.1"
|
||||||
|
jsonwebtoken: "npm:^9.0.2"
|
||||||
libphonenumber-js: "npm:^1.10.60"
|
libphonenumber-js: "npm:^1.10.60"
|
||||||
lint-staged: "npm:^15.2.2"
|
lint-staged: "npm:^15.2.2"
|
||||||
lodash-es: "npm:^4.17.21"
|
lodash-es: "npm:^4.17.21"
|
||||||
@@ -8221,6 +8223,16 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/jsonwebtoken@npm:^9":
|
||||||
|
version: 9.0.9
|
||||||
|
resolution: "@types/jsonwebtoken@npm:9.0.9"
|
||||||
|
dependencies:
|
||||||
|
"@types/ms": "npm:*"
|
||||||
|
"@types/node": "npm:*"
|
||||||
|
checksum: 10c0/d754a7b65fc021b298fc94e8d7a7d71f35dedf24296ac89286f80290abc5dbb0c7830a21440ee9ecbb340efc1b0a21f5609ea298a35b874cae5ad29a65440741
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4":
|
"@types/keyv@npm:^3.1.1, @types/keyv@npm:^3.1.4":
|
||||||
version: 3.1.4
|
version: 3.1.4
|
||||||
resolution: "@types/keyv@npm:3.1.4"
|
resolution: "@types/keyv@npm:3.1.4"
|
||||||
@@ -8253,6 +8265,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"@types/ms@npm:*":
|
||||||
|
version: 2.1.0
|
||||||
|
resolution: "@types/ms@npm:2.1.0"
|
||||||
|
checksum: 10c0/5ce692ffe1549e1b827d99ef8ff71187457e0eb44adbae38fdf7b9a74bae8d20642ee963c14516db1d35fa2652e65f47680fdf679dcbde52bbfadd021f497225
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"@types/mysql@npm:2.15.26":
|
"@types/mysql@npm:2.15.26":
|
||||||
version: 2.15.26
|
version: 2.15.26
|
||||||
resolution: "@types/mysql@npm:2.15.26"
|
resolution: "@types/mysql@npm:2.15.26"
|
||||||
@@ -10005,6 +10024,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"buffer-equal-constant-time@npm:1.0.1":
|
||||||
|
version: 1.0.1
|
||||||
|
resolution: "buffer-equal-constant-time@npm:1.0.1"
|
||||||
|
checksum: 10c0/fb2294e64d23c573d0dd1f1e7a466c3e978fe94a4e0f8183937912ca374619773bef8e2aceb854129d2efecbbc515bbd0cc78d2734a3e3031edb0888531bbc8e
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"buffer-from@npm:^1.0.0":
|
"buffer-from@npm:^1.0.0":
|
||||||
version: 1.1.2
|
version: 1.1.2
|
||||||
resolution: "buffer-from@npm:1.1.2"
|
resolution: "buffer-from@npm:1.1.2"
|
||||||
@@ -11609,6 +11635,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"ecdsa-sig-formatter@npm:1.0.11":
|
||||||
|
version: 1.0.11
|
||||||
|
resolution: "ecdsa-sig-formatter@npm:1.0.11"
|
||||||
|
dependencies:
|
||||||
|
safe-buffer: "npm:^5.0.1"
|
||||||
|
checksum: 10c0/ebfbf19d4b8be938f4dd4a83b8788385da353d63307ede301a9252f9f7f88672e76f2191618fd8edfc2f24679236064176fab0b78131b161ee73daa37125408c
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"ee-first@npm:1.1.1":
|
"ee-first@npm:1.1.1":
|
||||||
version: 1.1.1
|
version: 1.1.1
|
||||||
resolution: "ee-first@npm:1.1.1"
|
resolution: "ee-first@npm:1.1.1"
|
||||||
@@ -15400,6 +15435,24 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jsonwebtoken@npm:^9.0.2":
|
||||||
|
version: 9.0.2
|
||||||
|
resolution: "jsonwebtoken@npm:9.0.2"
|
||||||
|
dependencies:
|
||||||
|
jws: "npm:^3.2.2"
|
||||||
|
lodash.includes: "npm:^4.3.0"
|
||||||
|
lodash.isboolean: "npm:^3.0.3"
|
||||||
|
lodash.isinteger: "npm:^4.0.4"
|
||||||
|
lodash.isnumber: "npm:^3.0.3"
|
||||||
|
lodash.isplainobject: "npm:^4.0.6"
|
||||||
|
lodash.isstring: "npm:^4.0.1"
|
||||||
|
lodash.once: "npm:^4.0.0"
|
||||||
|
ms: "npm:^2.1.1"
|
||||||
|
semver: "npm:^7.5.4"
|
||||||
|
checksum: 10c0/d287a29814895e866db2e5a0209ce730cbc158441a0e5a70d5e940eb0d28ab7498c6bf45029cc8b479639bca94056e9a7f254e2cdb92a2f5750c7f358657a131
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"jsprim@npm:^2.0.2":
|
"jsprim@npm:^2.0.2":
|
||||||
version: 2.0.2
|
version: 2.0.2
|
||||||
resolution: "jsprim@npm:2.0.2"
|
resolution: "jsprim@npm:2.0.2"
|
||||||
@@ -15424,6 +15477,27 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"jwa@npm:^1.4.1":
|
||||||
|
version: 1.4.1
|
||||||
|
resolution: "jwa@npm:1.4.1"
|
||||||
|
dependencies:
|
||||||
|
buffer-equal-constant-time: "npm:1.0.1"
|
||||||
|
ecdsa-sig-formatter: "npm:1.0.11"
|
||||||
|
safe-buffer: "npm:^5.0.1"
|
||||||
|
checksum: 10c0/5c533540bf38702e73cf14765805a94027c66a0aa8b16bc3e89d8d905e61a4ce2791e87e21be97d1293a5ee9d4f3e5e47737e671768265ca4f25706db551d5e9
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"jws@npm:^3.2.2":
|
||||||
|
version: 3.2.2
|
||||||
|
resolution: "jws@npm:3.2.2"
|
||||||
|
dependencies:
|
||||||
|
jwa: "npm:^1.4.1"
|
||||||
|
safe-buffer: "npm:^5.0.1"
|
||||||
|
checksum: 10c0/e770704533d92df358adad7d1261fdecad4d7b66fa153ba80d047e03ca0f1f73007ce5ed3fbc04d2eba09ba6e7e6e645f351e08e5ab51614df1b0aa4f384dfff
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"kdbush@npm:^4.0.2":
|
"kdbush@npm:^4.0.2":
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
resolution: "kdbush@npm:4.0.2"
|
resolution: "kdbush@npm:4.0.2"
|
||||||
@@ -15899,6 +15973,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.includes@npm:^4.3.0":
|
||||||
|
version: 4.3.0
|
||||||
|
resolution: "lodash.includes@npm:4.3.0"
|
||||||
|
checksum: 10c0/7ca498b9b75bf602d04e48c0adb842dfc7d90f77bcb2a91a2b2be34a723ad24bc1c8b3683ec6b2552a90f216c723cdea530ddb11a3320e08fa38265703978f4b
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isarguments@npm:^3.1.0":
|
"lodash.isarguments@npm:^3.1.0":
|
||||||
version: 3.1.0
|
version: 3.1.0
|
||||||
resolution: "lodash.isarguments@npm:3.1.0"
|
resolution: "lodash.isarguments@npm:3.1.0"
|
||||||
@@ -15906,6 +15987,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isboolean@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "lodash.isboolean@npm:3.0.3"
|
||||||
|
checksum: 10c0/0aac604c1ef7e72f9a6b798e5b676606042401dd58e49f051df3cc1e3adb497b3d7695635a5cbec4ae5f66456b951fdabe7d6b387055f13267cde521f10ec7f7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isempty@npm:^4.4.0":
|
"lodash.isempty@npm:^4.4.0":
|
||||||
version: 4.4.0
|
version: 4.4.0
|
||||||
resolution: "lodash.isempty@npm:4.4.0"
|
resolution: "lodash.isempty@npm:4.4.0"
|
||||||
@@ -15913,6 +16001,20 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isinteger@npm:^4.0.4":
|
||||||
|
version: 4.0.4
|
||||||
|
resolution: "lodash.isinteger@npm:4.0.4"
|
||||||
|
checksum: 10c0/4c3e023a2373bf65bf366d3b8605b97ec830bca702a926939bcaa53f8e02789b6a176e7f166b082f9365bfec4121bfeb52e86e9040cb8d450e64c858583f61b7
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isnumber@npm:^3.0.3":
|
||||||
|
version: 3.0.3
|
||||||
|
resolution: "lodash.isnumber@npm:3.0.3"
|
||||||
|
checksum: 10c0/2d01530513a1ee4f72dd79528444db4e6360588adcb0e2ff663db2b3f642d4bb3d687051ae1115751ca9082db4fdef675160071226ca6bbf5f0c123dbf0aa12d
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.isplainobject@npm:^4.0.6":
|
"lodash.isplainobject@npm:^4.0.6":
|
||||||
version: 4.0.6
|
version: 4.0.6
|
||||||
resolution: "lodash.isplainobject@npm:4.0.6"
|
resolution: "lodash.isplainobject@npm:4.0.6"
|
||||||
@@ -15920,6 +16022,13 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
"lodash.isstring@npm:^4.0.1":
|
||||||
|
version: 4.0.1
|
||||||
|
resolution: "lodash.isstring@npm:4.0.1"
|
||||||
|
checksum: 10c0/09eaf980a283f9eef58ef95b30ec7fee61df4d6bf4aba3b5f096869cc58f24c9da17900febc8ffd67819b4e29de29793190e88dc96983db92d84c95fa85d1c92
|
||||||
|
languageName: node
|
||||||
|
linkType: hard
|
||||||
|
|
||||||
"lodash.memoize@npm:^4.1.2":
|
"lodash.memoize@npm:^4.1.2":
|
||||||
version: 4.1.2
|
version: 4.1.2
|
||||||
resolution: "lodash.memoize@npm:4.1.2"
|
resolution: "lodash.memoize@npm:4.1.2"
|
||||||
@@ -15934,7 +16043,7 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"lodash.once@npm:^4.1.1":
|
"lodash.once@npm:^4.0.0, lodash.once@npm:^4.1.1":
|
||||||
version: 4.1.1
|
version: 4.1.1
|
||||||
resolution: "lodash.once@npm:4.1.1"
|
resolution: "lodash.once@npm:4.1.1"
|
||||||
checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04
|
checksum: 10c0/46a9a0a66c45dd812fcc016e46605d85ad599fe87d71a02f6736220554b52ffbe82e79a483ad40f52a8a95755b0d1077fba259da8bfb6694a7abbf4a48f1fc04
|
||||||
|
|||||||
Reference in New Issue
Block a user