Merged in feature/redis (pull request #1478)
Distributed cache * cache deleteKey now uses an options object instead of a lonely argument variable fuzzy * merge * remove debug logs and cleanup * cleanup * add fault handling * add fault handling * add pid when logging redis client creation * add identifier when logging redis client creation * cleanup * feat: add redis-api as it's own app * feature: use http wrapper for redis * feat: add the possibility to fallback to unstable_cache * Add error handling if redis cache is unresponsive * add logging for unstable_cache * merge * don't cache errors * fix: metadatabase on branchdeploys * Handle when /en/destinations throws add ErrorBoundary * Add sentry-logging when ErrorBoundary catches exception * Fix error handling for distributed cache * cleanup code * Added Application Insights back * Update generateApiKeys script and remove duplicate * Merge branch 'feature/redis' of bitbucket.org:scandic-swap/web into feature/redis * merge Approved-by: Linus Flood
This commit is contained in:
committed by
Linus Flood
parent
a8304e543e
commit
fa63b20ed0
@@ -12,3 +12,5 @@ netlify.toml
|
||||
package.json
|
||||
package-lock.json
|
||||
.gitignore
|
||||
*.bicep
|
||||
*.ico
|
||||
@@ -3,11 +3,13 @@
|
||||
The web is using OAuth 2.0 to handle auth. We host our own instance of [Curity](https://curity.io), which is our identity and access management solution.
|
||||
|
||||
## Session management in Next
|
||||
|
||||
We use [Auth.js](https://authjs.dev) to handle everything regarding auth in the web. We use the JWT session strategy, which means that everything regarding the session is stored in a JWT, which is stored in the browser in an encrypted cookie.
|
||||
|
||||
## Keeping the access token alive
|
||||
|
||||
When the user performs a navigation the web app often does multiple requests to Next. If the access token has expired Next will do a request to Curity to renew the tokens. Since we only allow a single refresh token to be used only once only the first request will succeed and the following requests will fail.
|
||||
|
||||
To avoid that we have a component whose only purpose is to keep the access token alive. As long as no other request is happening at the same time this will work fine.
|
||||
|
||||
To avoid a session that keeps on refreshing forever, if the user have the page open in the background e.g., we have a timeout that stops the refreshing if the user is not active.
|
||||
To avoid a session that keeps on refreshing forever, if the user have the page open in the background e.g., we have a timeout that stops the refreshing if the user is not active.
|
||||
|
||||
@@ -18,6 +18,19 @@ yarn dev
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
### Caching
|
||||
|
||||
You have the choice to either use redis (via redis-api; a tiny http proxy) or in-memory/unstable_cache (depending on edge or node).
|
||||
Setting `REDIS_API_HOST` will configure it to use the distributed cache, not providing it will fall back to in-memory/unstable_cache
|
||||
When pointing to the azure hosted variant you also need to provide `REDIS_API_KEY`
|
||||
|
||||
Locally it's easiest is to spin everything up using docker/podman - `podman compose up` or `docker-compose up`
|
||||
This will also spin up [Redis Insight ](https://redis.io/insight/) so that you can debug the cache.
|
||||
|
||||
- Navigate to `http://localhost:5540`
|
||||
- Click **'Add Redis database'**
|
||||
- Provide Connection URL `redis://redis:6379`
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
@@ -6,6 +6,7 @@ import { startTransition, useEffect, useRef } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
import { env } from "@/env/client"
|
||||
import { SESSION_EXPIRED } from "@/server/errors/trpc"
|
||||
|
||||
import styles from "./error.module.css"
|
||||
@@ -61,6 +62,9 @@ export default function Error({
|
||||
<section className={styles.layout}>
|
||||
<div className={styles.content}>
|
||||
{intl.formatMessage({ id: "Something went wrong!" })}
|
||||
{env.NEXT_PUBLIC_NODE_ENV === "development" && (
|
||||
<pre>{error.stack || error.message}</pre>
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
)
|
||||
|
||||
@@ -25,8 +25,7 @@ export default async function CurrentContentPage({
|
||||
{
|
||||
locale: params.lang,
|
||||
url: searchParams.uri,
|
||||
},
|
||||
{ cache: "no-store" }
|
||||
}
|
||||
)
|
||||
|
||||
if (!response.data?.all_current_blocks_page?.total) {
|
||||
@@ -39,8 +38,7 @@ export default async function CurrentContentPage({
|
||||
// This is currently to be considered a temporary solution to provide the tracking with a few values in english to align with existing reports
|
||||
const pageDataForTracking = await request<TrackingData>(
|
||||
GetCurrentBlockPageTrackingData,
|
||||
{ uid: response.data.all_current_blocks_page.items[0].system.uid },
|
||||
{ cache: "no-store" }
|
||||
{ uid: response.data.all_current_blocks_page.items[0].system.uid }
|
||||
)
|
||||
|
||||
const pageData = response.data.all_current_blocks_page.items[0]
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { generateHotelUrlTag } from "@/utils/generateTag"
|
||||
|
||||
import type { NextRequest } from "next/server"
|
||||
@@ -63,6 +64,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
console.info(`Revalidating hotel url tag: ${tag}`)
|
||||
revalidateTag(tag)
|
||||
const cacheClient = await getCacheClient()
|
||||
await cacheClient.deleteKey(tag, { fuzzy: true })
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
} catch (error) {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import { badRequest, internalServerError, notFound } from "@/server/errors/next"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { generateLoyaltyConfigTag } from "@/utils/generateTag"
|
||||
|
||||
import type { NextRequest } from "next/server"
|
||||
@@ -82,6 +83,9 @@ export async function POST(request: NextRequest) {
|
||||
console.info(`Revalidating loyalty config tag: ${tag}`)
|
||||
revalidateTag(tag)
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
await cacheClient.deleteKey(tag, { fuzzy: true })
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
} catch (error) {
|
||||
console.error("Failed to revalidate tag(s) for loyalty config")
|
||||
|
||||
@@ -4,6 +4,7 @@ import { headers } from "next/headers"
|
||||
import { env } from "@/env/server"
|
||||
import { badRequest, internalServerError } from "@/server/errors/next"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
|
||||
import type { Lang } from "@/constants/languages"
|
||||
@@ -27,23 +28,8 @@ export async function POST() {
|
||||
const affix = headersList.get("x-affix")
|
||||
const identifier = headersList.get("x-identifier")
|
||||
const lang = headersList.get("x-lang")
|
||||
if (lang && identifier) {
|
||||
if (affix) {
|
||||
const tag = generateTag(lang as Lang, identifier, affix)
|
||||
console.info(
|
||||
`Revalidated tag for [lang: ${lang}, identifier: ${identifier}, affix: ${affix}]`
|
||||
)
|
||||
console.info(`Tag: ${tag}`)
|
||||
revalidateTag(tag)
|
||||
} else {
|
||||
const tag = generateTag(lang as Lang, identifier)
|
||||
console.info(
|
||||
`Revalidated tag for [lang: ${lang}, identifier: ${identifier}]`
|
||||
)
|
||||
console.info(`Tag: ${tag}`)
|
||||
revalidateTag(tag)
|
||||
}
|
||||
} else {
|
||||
|
||||
if (!lang || !identifier) {
|
||||
console.info(`Missing lang and/or identifier`)
|
||||
console.info(`lang: ${lang}, identifier: ${identifier}`)
|
||||
return badRequest({
|
||||
@@ -52,6 +38,18 @@ export async function POST() {
|
||||
})
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
const tag = generateTag(lang as Lang, identifier, affix)
|
||||
|
||||
console.info(
|
||||
`Revalidated tag for [lang: ${lang}, identifier: ${identifier}${affix ? `, affix: ${affix}` : ""}]`
|
||||
)
|
||||
console.info(`Tag: ${tag}`)
|
||||
|
||||
revalidateTag(tag)
|
||||
cacheClient.deleteKey(tag, { fuzzy: true })
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
} catch (error) {
|
||||
console.error("Failed to revalidate tag(s)")
|
||||
|
||||
@@ -10,6 +10,7 @@ import { languageSwitcherAffix } from "@/server/routers/contentstack/languageSwi
|
||||
import { affix as metadataAffix } from "@/server/routers/contentstack/metadata/utils"
|
||||
import { affix as pageSettingsAffix } from "@/server/routers/contentstack/pageSettings/utils"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import {
|
||||
generateRefsResponseTag,
|
||||
generateRefTag,
|
||||
@@ -87,23 +88,31 @@ export async function POST(request: NextRequest) {
|
||||
)
|
||||
const metadataTag = generateTag(entryLocale, entry.uid, metadataAffix)
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
console.info(`Revalidating refsTag: ${refsTag}`)
|
||||
revalidateTag(refsTag)
|
||||
await cacheClient.deleteKey(refsTag, { fuzzy: true })
|
||||
|
||||
console.info(`Revalidating refTag: ${refTag}`)
|
||||
revalidateTag(refTag)
|
||||
await cacheClient.deleteKey(refTag, { fuzzy: true })
|
||||
|
||||
console.info(`Revalidating tag: ${tag}`)
|
||||
revalidateTag(tag)
|
||||
await cacheClient.deleteKey(tag, { fuzzy: true })
|
||||
|
||||
console.info(`Revalidating language switcher tag: ${languageSwitcherTag}`)
|
||||
revalidateTag(languageSwitcherTag)
|
||||
await cacheClient.deleteKey(languageSwitcherTag, { fuzzy: true })
|
||||
|
||||
console.info(`Revalidating metadataTag: ${metadataTag}`)
|
||||
revalidateTag(metadataTag)
|
||||
await cacheClient.deleteKey(metadataTag, { fuzzy: true })
|
||||
|
||||
console.info(`Revalidating contentEntryTag: ${contentEntryTag}`)
|
||||
revalidateTag(contentEntryTag)
|
||||
await cacheClient.deleteKey(contentEntryTag, { fuzzy: true })
|
||||
|
||||
if (entry.breadcrumbs) {
|
||||
const breadcrumbsRefsTag = generateRefsResponseTag(
|
||||
@@ -119,9 +128,11 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
console.info(`Revalidating breadcrumbsRefsTag: ${breadcrumbsRefsTag}`)
|
||||
revalidateTag(breadcrumbsRefsTag)
|
||||
await cacheClient.deleteKey(breadcrumbsRefsTag, { fuzzy: true })
|
||||
|
||||
console.info(`Revalidating breadcrumbsTag: ${breadcrumbsTag}`)
|
||||
revalidateTag(breadcrumbsTag)
|
||||
await cacheClient.deleteKey(breadcrumbsTag, { fuzzy: true })
|
||||
}
|
||||
|
||||
if (entry.page_settings) {
|
||||
@@ -133,6 +144,7 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
console.info(`Revalidating pageSettingsTag: ${pageSettingsTag}`)
|
||||
revalidateTag(pageSettingsTag)
|
||||
await cacheClient.deleteKey(pageSettingsTag, { fuzzy: true })
|
||||
}
|
||||
|
||||
return Response.json({ revalidated: true, now: Date.now() })
|
||||
|
||||
@@ -24,7 +24,7 @@ export function SessionRefresher() {
|
||||
const session = useSession()
|
||||
const pathname = usePathname()
|
||||
const searchParams = useSearchParams()
|
||||
const timeoutId = useRef<NodeJS.Timeout>()
|
||||
const timeoutId = useRef<Timer>()
|
||||
|
||||
// Simple inactivity control. Reset when the URL changes.
|
||||
const stopPreRefreshAt = useMemo(
|
||||
|
||||
@@ -15,7 +15,10 @@
|
||||
}
|
||||
|
||||
.partial {
|
||||
grid-template-columns: minmax(auto, 150px) min-content minmax(auto, 150px) auto;
|
||||
grid-template-columns: minmax(auto, 150px) min-content minmax(
|
||||
auto,
|
||||
150px
|
||||
) auto;
|
||||
}
|
||||
|
||||
.icon {
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Map, type MapProps, useMap } from "@vis.gl/react-google-maps"
|
||||
import { type PropsWithChildren, useEffect } from "react"
|
||||
import { useIntl } from "react-intl"
|
||||
|
||||
import ErrorBoundary from "@/components/ErrorBoundary/ErrorBoundary"
|
||||
import { CloseLargeIcon, MinusIcon, PlusIcon } from "@/components/Icons"
|
||||
import Button from "@/components/TempDesignSystem/Button"
|
||||
import { useHandleKeyUp } from "@/hooks/useHandleKeyUp"
|
||||
@@ -84,7 +85,9 @@ export default function DynamicMap({
|
||||
|
||||
return (
|
||||
<div className={styles.mapWrapper}>
|
||||
<Map {...mapOptions}>{children}</Map>
|
||||
<ErrorBoundary fallback={<h2>Unable to display map</h2>}>
|
||||
<Map {...mapOptions}>{children}</Map>
|
||||
</ErrorBoundary>
|
||||
<div className={styles.ctaButtons}>
|
||||
{onClose && (
|
||||
<Button
|
||||
|
||||
@@ -69,8 +69,8 @@
|
||||
@media screen and (min-width: 1367px) {
|
||||
.pageContainer {
|
||||
--hotel-page-scroll-margin-top: calc(
|
||||
var(--hotel-page-navigation-height) + var(--booking-widget-desktop-height) +
|
||||
var(--Spacing-x2)
|
||||
var(--hotel-page-navigation-height) +
|
||||
var(--booking-widget-desktop-height) + var(--Spacing-x2)
|
||||
);
|
||||
grid-template-areas:
|
||||
"header mapContainer"
|
||||
|
||||
@@ -20,4 +20,4 @@
|
||||
border-radius: 18px;
|
||||
outline: 0 none;
|
||||
padding: 5px 15px;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ export default function OfflineBanner() {
|
||||
return (
|
||||
<div className={`${styles.banner} ${styles.hidden}`}>
|
||||
You are offline, some content may be out of date.
|
||||
<button className={styles.reloadBtn} type="button">Reload</button>
|
||||
<button className={styles.reloadBtn} type="button">
|
||||
Reload
|
||||
</button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -12,17 +12,11 @@ export default function Breadcrumbs({
|
||||
<ul className={styles.list}>
|
||||
{parent ? (
|
||||
<li className={styles.parent}>
|
||||
<a href={parent.href}>
|
||||
{parent.title}
|
||||
</a>
|
||||
<a href={parent.href}>{parent.title}</a>
|
||||
</li>
|
||||
) : null}
|
||||
{breadcrumbs.map((breadcrumb) => (
|
||||
<li
|
||||
className={styles.li}
|
||||
itemProp="breadcrumb"
|
||||
key={breadcrumb.href}
|
||||
>
|
||||
<li className={styles.li} itemProp="breadcrumb" key={breadcrumb.href}>
|
||||
<a className={styles.link} href={breadcrumb.href}>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
|
||||
@@ -11,16 +11,12 @@ export default async function SubnavMobile({
|
||||
<ul className="breadcrumb-list hidden-small hidden-medium hidden-large">
|
||||
{parent ? (
|
||||
<li className="breadcrumb-list__parent hidden-medium hidden-large">
|
||||
<a href={parent.href}>
|
||||
{parent.title}
|
||||
</a>
|
||||
<a href={parent.href}>{parent.title}</a>
|
||||
</li>
|
||||
) : null}
|
||||
{breadcrumbs.map((breadcrumb) => (
|
||||
<li className="breadcrumb-list__body" key={breadcrumb.href}>
|
||||
<a href={breadcrumb.href}>
|
||||
{breadcrumb.title}
|
||||
</a>
|
||||
<a href={breadcrumb.href}>{breadcrumb.title}</a>
|
||||
</li>
|
||||
))}
|
||||
<li className="breadcrumb-list__body">
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
import React from "react"
|
||||
|
||||
type ErrorBoundaryProps = {
|
||||
children: React.ReactNode
|
||||
fallback?: React.ReactNode
|
||||
}
|
||||
type ErrorBoundaryState = { hasError: boolean; error?: Error }
|
||||
|
||||
class ErrorBoundary extends React.Component<
|
||||
ErrorBoundaryProps,
|
||||
ErrorBoundaryState
|
||||
> {
|
||||
constructor(props: ErrorBoundaryProps) {
|
||||
super(props)
|
||||
this.state = { hasError: false }
|
||||
}
|
||||
|
||||
static getDerivedStateFromError(error: Error) {
|
||||
return { hasError: true, error }
|
||||
}
|
||||
|
||||
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
||||
console.error("ErrorBoundary caught an error:", error, errorInfo)
|
||||
Sentry.captureException(error, { extra: { errorInfo } })
|
||||
}
|
||||
|
||||
render() {
|
||||
if (this.state.hasError) {
|
||||
if (this.props.fallback) {
|
||||
return this.props.fallback
|
||||
}
|
||||
|
||||
return <h2>Something went wrong.</h2>
|
||||
}
|
||||
return this.props.children
|
||||
}
|
||||
}
|
||||
|
||||
export default ErrorBoundary
|
||||
@@ -41,4 +41,4 @@
|
||||
gap: var(--Spacing-x2);
|
||||
justify-self: flex-end;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+4
-2
@@ -14,7 +14,8 @@
|
||||
}
|
||||
|
||||
.link:nth-of-type(1) .promo {
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.36) 37.88%,
|
||||
@@ -24,7 +25,8 @@
|
||||
}
|
||||
|
||||
.link:nth-of-type(2) .promo {
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.36) 37.88%,
|
||||
|
||||
@@ -152,9 +152,9 @@ export default function Details({ user }: DetailsProps) {
|
||||
{isPaymentNext
|
||||
? intl.formatMessage({ id: "Proceed to payment method" })
|
||||
: intl.formatMessage(
|
||||
{ id: "Continue to room {nextRoomNumber}" },
|
||||
{ nextRoomNumber: roomNr + 1 }
|
||||
)}
|
||||
{ id: "Continue to room {nextRoomNumber}" },
|
||||
{ nextRoomNumber: roomNr + 1 }
|
||||
)}
|
||||
</Button>
|
||||
</footer>
|
||||
<MemberPriceModal
|
||||
|
||||
+2
-1
@@ -2,7 +2,8 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
background-image:
|
||||
linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||
|
||||
@@ -19,7 +19,8 @@
|
||||
}
|
||||
|
||||
.link .promo {
|
||||
background-image: linear-gradient(
|
||||
background-image:
|
||||
linear-gradient(
|
||||
180deg,
|
||||
rgba(0, 0, 0, 0) 0%,
|
||||
rgba(0, 0, 0, 0.36) 37.88%,
|
||||
|
||||
@@ -107,7 +107,8 @@
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
background-image:
|
||||
linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||
|
||||
+5
-7
@@ -18,13 +18,11 @@ import type { Rate } from "@/types/components/hotelReservation/selectRate/select
|
||||
|
||||
export default function SelectedRoomPanel() {
|
||||
const intl = useIntl()
|
||||
const { isUserLoggedIn, roomCategories } = useRatesStore(
|
||||
(state) => ({
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
})
|
||||
)
|
||||
const { isUserLoggedIn, roomCategories } = useRatesStore((state) => ({
|
||||
isUserLoggedIn: state.isUserLoggedIn,
|
||||
rateDefinitions: state.roomsAvailability?.rateDefinitions,
|
||||
roomCategories: state.roomCategories,
|
||||
}))
|
||||
const {
|
||||
actions: { modifyRate },
|
||||
isMainRoom,
|
||||
|
||||
@@ -40,7 +40,7 @@ export function useRoomsAvailability(
|
||||
toDateString: string,
|
||||
lang: Lang,
|
||||
childArray?: Child[],
|
||||
bookingCode?: string,
|
||||
bookingCode?: string
|
||||
) {
|
||||
const returnValue =
|
||||
trpc.hotel.availability.roomsCombinedAvailability.useQuery({
|
||||
|
||||
@@ -33,7 +33,8 @@
|
||||
aspect-ratio: 16/9;
|
||||
width: 100%;
|
||||
background-color: #fff;
|
||||
background-image: linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
background-image:
|
||||
linear-gradient(45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(-45deg, #000000 25%, transparent 25%),
|
||||
linear-gradient(45deg, transparent 75%, #000000 75%),
|
||||
linear-gradient(-45deg, transparent 75%, #000000 75%);
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
services:
|
||||
redis-api:
|
||||
build:
|
||||
context: ../redis-api
|
||||
dockerfile: Dockerfile
|
||||
ports:
|
||||
- "3001:3001"
|
||||
depends_on:
|
||||
- redis
|
||||
environment:
|
||||
- REDIS_CONNECTION=redis:6379
|
||||
- PRIMARY_API_KEY=
|
||||
- SECONDARY_API_KEY=
|
||||
- NODE_ENV=development
|
||||
redis:
|
||||
image: redis:6
|
||||
ports:
|
||||
- "6379:6379"
|
||||
|
||||
redisinsight:
|
||||
image: redis/redisinsight:latest
|
||||
ports:
|
||||
- "5540:5540"
|
||||
depends_on:
|
||||
- redis
|
||||
Vendored
+12
@@ -185,6 +185,13 @@ export const env = createEnv({
|
||||
.number()
|
||||
.default(10 * 60)
|
||||
.transform((val) => (process.env.CMS_ENVIRONMENT === "test" ? 60 : val)),
|
||||
REDIS_API_HOST: z.string().optional(),
|
||||
REDIS_API_KEY: z.string().optional(),
|
||||
BRANCH:
|
||||
process.env.NODE_ENV !== "development"
|
||||
? z.string()
|
||||
: z.string().optional().default("dev"),
|
||||
GIT_SHA: z.string().optional(),
|
||||
},
|
||||
emptyStringAsUndefined: true,
|
||||
runtimeEnv: {
|
||||
@@ -273,6 +280,11 @@ export const env = createEnv({
|
||||
CACHE_TIME_HOTELDATA: process.env.CACHE_TIME_HOTELDATA,
|
||||
CACHE_TIME_HOTELS: process.env.CACHE_TIME_HOTELS,
|
||||
CACHE_TIME_CITY_SEARCH: process.env.CACHE_TIME_CITY_SEARCH,
|
||||
|
||||
REDIS_API_HOST: process.env.REDIS_API_HOST,
|
||||
REDIS_API_KEY: process.env.REDIS_API_KEY,
|
||||
BRANCH: process.env.BRANCH,
|
||||
GIT_SHA: process.env.GIT_SHA,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
|
||||
import { env } from "./env/server"
|
||||
import { isEdge } from "./utils/isEdge"
|
||||
|
||||
export async function register() {
|
||||
/*
|
||||
Order matters!
|
||||
|
||||
|
||||
Sentry hooks into OpenTelemetry, modifying its behavior.
|
||||
Application Insights relies on OpenTelemetry exporters,
|
||||
Application Insights relies on OpenTelemetry exporters,
|
||||
and these may not work correctly if Sentry has already altered the instrumentation pipeline.
|
||||
*/
|
||||
await configureApplicationInsights()
|
||||
@@ -45,12 +46,9 @@ async function configureApplicationInsights() {
|
||||
}
|
||||
|
||||
async function configureSentry() {
|
||||
switch (process.env.NEXT_RUNTIME) {
|
||||
case "edge": {
|
||||
await import("./sentry.edge.config")
|
||||
}
|
||||
case "nodejs": {
|
||||
await import("./sentry.server.config")
|
||||
}
|
||||
if (isEdge) {
|
||||
await import("./sentry.edge.config")
|
||||
} else {
|
||||
await import("./sentry.server.config")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,14 +9,20 @@ import { request } from "./request"
|
||||
import type { BatchRequestDocument } from "graphql-request"
|
||||
|
||||
import type { Data } from "@/types/request"
|
||||
import type { CacheTime } from "@/services/dataCache"
|
||||
|
||||
export async function batchRequest<T>(
|
||||
queries: (BatchRequestDocument & { options?: RequestInit })[]
|
||||
queries: (BatchRequestDocument & {
|
||||
cacheOptions?: {
|
||||
key: string | string[]
|
||||
ttl: CacheTime
|
||||
}
|
||||
})[]
|
||||
): Promise<Data<T>> {
|
||||
try {
|
||||
const response = await Promise.allSettled(
|
||||
queries.map((query) =>
|
||||
request<T>(query.document, query.variables, query.options)
|
||||
request<T>(query.document, query.variables, query.cacheOptions)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import fetchRetry from "fetch-retry"
|
||||
import { GraphQLClient } from "graphql-request"
|
||||
import { cache } from "react"
|
||||
import { cache as reactCache } from "react"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import { getPreviewHash, isPreviewByUid } from "@/lib/previewContext"
|
||||
|
||||
import { type CacheTime, getCacheClient } from "@/services/dataCache"
|
||||
|
||||
import { request as _request } from "./_request"
|
||||
|
||||
import type { DocumentNode } from "graphql"
|
||||
@@ -14,7 +16,28 @@ import type { Data } from "@/types/request"
|
||||
export async function request<T>(
|
||||
query: string | DocumentNode,
|
||||
variables?: Record<string, any>,
|
||||
params?: RequestInit
|
||||
cacheOptions?: {
|
||||
key: string | string[]
|
||||
ttl: CacheTime
|
||||
}
|
||||
): Promise<Data<T>> {
|
||||
const doCall = () => internalRequest<T>(query, variables)
|
||||
if (!cacheOptions) {
|
||||
console.warn("[NO CACHE] for query", query)
|
||||
return doCall()
|
||||
}
|
||||
|
||||
const cacheKey: string = Array.isArray(cacheOptions.key)
|
||||
? cacheOptions.key.join("_")
|
||||
: cacheOptions.key
|
||||
|
||||
const _dataCache = await getCacheClient()
|
||||
return _dataCache.cacheOrGet(cacheKey, doCall, cacheOptions.ttl)
|
||||
}
|
||||
|
||||
function internalRequest<T>(
|
||||
query: string | DocumentNode,
|
||||
variables?: Record<string, any>
|
||||
): Promise<Data<T>> {
|
||||
const shouldUsePreview = variables?.uid
|
||||
? isPreviewByUid(variables.uid)
|
||||
@@ -24,7 +47,10 @@ export async function request<T>(
|
||||
|
||||
// Creating a new client for each request to avoid conflicting parameters
|
||||
const client = new GraphQLClient(cmsUrl, {
|
||||
fetch: cache(async function (url: URL | RequestInfo, params?: RequestInit) {
|
||||
fetch: reactCache(async function (
|
||||
url: URL | RequestInfo,
|
||||
params?: RequestInit
|
||||
) {
|
||||
const wrappedFetch = fetchRetry(fetch, {
|
||||
retries: 3,
|
||||
retryDelay: function (attempt) {
|
||||
@@ -38,16 +64,12 @@ export async function request<T>(
|
||||
const mergedParams =
|
||||
shouldUsePreview && previewHash
|
||||
? {
|
||||
...params,
|
||||
headers: {
|
||||
...params?.headers,
|
||||
live_preview: previewHash,
|
||||
preview_token: env.CMS_PREVIEW_TOKEN,
|
||||
},
|
||||
cache: undefined,
|
||||
next: undefined,
|
||||
}
|
||||
: params
|
||||
: {}
|
||||
|
||||
return _request(client, query, variables, mergedParams)
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type NextMiddleware,NextResponse } from "next/server"
|
||||
import { type NextMiddleware, NextResponse } from "next/server"
|
||||
|
||||
import { REDEMPTION, SEARCHTYPE } from "@/constants/booking"
|
||||
import { login } from "@/constants/routes/handleAuth"
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { type NextMiddleware,NextResponse } from "next/server"
|
||||
import { type NextMiddleware, NextResponse } from "next/server"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
|
||||
|
||||
@@ -40,4 +40,4 @@ schedule = "@daily"
|
||||
[[headers]]
|
||||
for = "/_next/static/*"
|
||||
[headers.values]
|
||||
cache-control = "public, max-age=31536001, immutable"
|
||||
cache-control = "public, max-age=31536000, immutable"
|
||||
|
||||
@@ -13,6 +13,10 @@ jiti("./env/client")
|
||||
|
||||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
env: {
|
||||
BRANCH: process.env.BRANCH || "local",
|
||||
GIT_SHA: process.env.COMMIT_REF || "",
|
||||
},
|
||||
poweredByHeader: false,
|
||||
eslint: { ignoreDuringBuilds: true },
|
||||
trailingSlash: false,
|
||||
|
||||
@@ -77,6 +77,7 @@
|
||||
"ics": "^3.8.1",
|
||||
"immer": "10.1.1",
|
||||
"input-otp": "^1.4.2",
|
||||
"ioredis": "^5.5.0",
|
||||
"json-stable-stringify-without-jsonify": "^1.0.1",
|
||||
"libphonenumber-js": "^1.10.60",
|
||||
"nanoid": "^5.0.9",
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
GetAccountPageRefsSchema,
|
||||
GetAccountPageSchema,
|
||||
} from "@/types/trpc/routers/contentstack/accountPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const meter = metrics.getMeter("trpc.accountPage")
|
||||
|
||||
@@ -64,10 +63,8 @@ export const accountPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(lang, uid)],
|
||||
},
|
||||
key: generateRefsResponseTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -128,10 +125,8 @@ export const accountPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackBaseProcedure, router } from "@/server/trpc"
|
||||
import { langInput } from "@/server/utils"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import {
|
||||
generateRefsResponseTag,
|
||||
generateTag,
|
||||
@@ -107,10 +108,8 @@ const getContactConfig = cache(async (lang: Lang) => {
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [`${lang}:contact`],
|
||||
},
|
||||
key: `${lang}:contact`,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -176,10 +175,8 @@ export const baseQueryRouter = router({
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(lang, "header")],
|
||||
},
|
||||
key: generateRefsResponseTag(lang, "header"),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -244,7 +241,7 @@ export const baseQueryRouter = router({
|
||||
const response = await request<GetHeaderData>(
|
||||
GetHeader,
|
||||
{ locale: lang },
|
||||
{ cache: "force-cache", next: { tags } }
|
||||
{ key: tags, ttl: "max" }
|
||||
)
|
||||
|
||||
if (!response.data) {
|
||||
@@ -305,10 +302,8 @@ export const baseQueryRouter = router({
|
||||
locale: input.lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(input.lang, "current_header")],
|
||||
},
|
||||
key: generateRefsResponseTag(input.lang, "current_header"),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
getCurrentHeaderCounter.add(1, { lang: input.lang })
|
||||
@@ -326,10 +321,8 @@ export const baseQueryRouter = router({
|
||||
GetCurrentHeader,
|
||||
{ locale: input.lang },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(input.lang, currentHeaderUID)],
|
||||
},
|
||||
key: generateTag(input.lang, currentHeaderUID),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -397,10 +390,8 @@ export const baseQueryRouter = router({
|
||||
locale: input.lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(input.lang, "current_footer")],
|
||||
},
|
||||
key: generateRefsResponseTag(input.lang, "current_footer"),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
// There's currently no error handling/validation for the responseRef, should it be added?
|
||||
@@ -422,10 +413,8 @@ export const baseQueryRouter = router({
|
||||
locale: input.lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(input.lang, currentFooterUID)],
|
||||
},
|
||||
key: generateTag(input.lang, currentFooterUID),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -486,10 +475,8 @@ export const baseQueryRouter = router({
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(lang, "footer")],
|
||||
},
|
||||
key: generateRefsResponseTag(lang, "footer"),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -563,10 +550,8 @@ export const baseQueryRouter = router({
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -620,157 +605,164 @@ export const baseQueryRouter = router({
|
||||
.input(langInput)
|
||||
.query(async ({ input, ctx }) => {
|
||||
const lang = input.lang ?? ctx.lang
|
||||
|
||||
getSiteConfigRefCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig.ref start",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
const responseRef = await request<GetSiteConfigRefData>(
|
||||
GetSiteConfigRef,
|
||||
{
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(lang, "site_config")],
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!responseRef.data) {
|
||||
const notFoundError = notFound(responseRef)
|
||||
getSiteConfigRefFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "not_found",
|
||||
error: JSON.stringify({ code: notFoundError.code }),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.siteConfig.refs not found error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang,
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
generateRefsResponseTag(lang, "site_config", "root"),
|
||||
async () => {
|
||||
getSiteConfigRefCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig.ref start",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
const responseRef = await request<GetSiteConfigRefData>(
|
||||
GetSiteConfigRef,
|
||||
{
|
||||
locale: lang,
|
||||
},
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedSiteConfigRef = siteConfigRefSchema.safeParse(
|
||||
responseRef.data
|
||||
)
|
||||
|
||||
if (!validatedSiteConfigRef.success) {
|
||||
getSiteConfigRefFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedSiteConfigRef.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.siteConfig.refs validation error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang,
|
||||
},
|
||||
error: validatedSiteConfigRef.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const connections = getSiteConfigConnections(validatedSiteConfigRef.data)
|
||||
const siteConfigUid = responseRef.data.all_site_config.items[0].system.uid
|
||||
|
||||
const tags = [
|
||||
generateTagsFromSystem(lang, connections),
|
||||
generateTag(lang, siteConfigUid),
|
||||
].flat()
|
||||
|
||||
getSiteConfigRefSuccessCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig.refs success",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
|
||||
getSiteConfigCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig start",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
const [siteConfigResponse, contactConfig] = await Promise.all([
|
||||
request<GetSiteConfigData>(
|
||||
GetSiteConfig,
|
||||
{
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: { tags },
|
||||
}
|
||||
),
|
||||
getContactConfig(lang),
|
||||
])
|
||||
|
||||
if (!siteConfigResponse.data) {
|
||||
const notFoundError = notFound(siteConfigResponse)
|
||||
|
||||
getSiteConfigFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "not_found",
|
||||
error: JSON.stringify({ code: notFoundError.code }),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"contentstack.siteConfig not found error",
|
||||
JSON.stringify({
|
||||
query: { lang },
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedSiteConfig = siteConfigSchema.safeParse(
|
||||
siteConfigResponse.data
|
||||
)
|
||||
|
||||
if (!validatedSiteConfig.success) {
|
||||
getSiteConfigFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedSiteConfig.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.siteConfig validation error",
|
||||
JSON.stringify({
|
||||
query: { lang },
|
||||
error: validatedSiteConfig.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getSiteConfigSuccessCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig success",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
|
||||
const { sitewideAlert } = validatedSiteConfig.data
|
||||
|
||||
return {
|
||||
...validatedSiteConfig.data,
|
||||
sitewideAlert: sitewideAlert
|
||||
? {
|
||||
...sitewideAlert,
|
||||
phoneContact: contactConfig
|
||||
? getAlertPhoneContactData(sitewideAlert, contactConfig)
|
||||
: null,
|
||||
{
|
||||
key: generateRefsResponseTag(lang, "site_config"),
|
||||
ttl: "max",
|
||||
}
|
||||
: null,
|
||||
}
|
||||
)
|
||||
|
||||
if (!responseRef.data) {
|
||||
const notFoundError = notFound(responseRef)
|
||||
getSiteConfigRefFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "not_found",
|
||||
error: JSON.stringify({ code: notFoundError.code }),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.siteConfig.refs not found error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang,
|
||||
},
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedSiteConfigRef = siteConfigRefSchema.safeParse(
|
||||
responseRef.data
|
||||
)
|
||||
|
||||
if (!validatedSiteConfigRef.success) {
|
||||
getSiteConfigRefFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedSiteConfigRef.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.siteConfig.refs validation error",
|
||||
JSON.stringify({
|
||||
query: {
|
||||
lang,
|
||||
},
|
||||
error: validatedSiteConfigRef.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const connections = getSiteConfigConnections(
|
||||
validatedSiteConfigRef.data
|
||||
)
|
||||
const siteConfigUid =
|
||||
responseRef.data.all_site_config.items[0].system.uid
|
||||
|
||||
const tags = [
|
||||
generateTagsFromSystem(lang, connections),
|
||||
generateTag(lang, siteConfigUid),
|
||||
].flat()
|
||||
|
||||
getSiteConfigRefSuccessCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig.refs success",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
|
||||
getSiteConfigCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig start",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
const [siteConfigResponse, contactConfig] = await Promise.all([
|
||||
request<GetSiteConfigData>(
|
||||
GetSiteConfig,
|
||||
{
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
),
|
||||
getContactConfig(lang),
|
||||
])
|
||||
|
||||
if (!siteConfigResponse.data) {
|
||||
const notFoundError = notFound(siteConfigResponse)
|
||||
|
||||
getSiteConfigFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "not_found",
|
||||
error: JSON.stringify({ code: notFoundError.code }),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"contentstack.siteConfig not found error",
|
||||
JSON.stringify({
|
||||
query: { lang },
|
||||
error: { code: notFoundError.code },
|
||||
})
|
||||
)
|
||||
|
||||
throw notFoundError
|
||||
}
|
||||
|
||||
const validatedSiteConfig = siteConfigSchema.safeParse(
|
||||
siteConfigResponse.data
|
||||
)
|
||||
|
||||
if (!validatedSiteConfig.success) {
|
||||
getSiteConfigFailCounter.add(1, {
|
||||
lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedSiteConfig.error),
|
||||
})
|
||||
console.error(
|
||||
"contentstack.siteConfig validation error",
|
||||
JSON.stringify({
|
||||
query: { lang },
|
||||
error: validatedSiteConfig.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getSiteConfigSuccessCounter.add(1, { lang })
|
||||
console.info(
|
||||
"contentstack.siteConfig success",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
|
||||
const { sitewideAlert } = validatedSiteConfig.data
|
||||
|
||||
return {
|
||||
...validatedSiteConfig.data,
|
||||
sitewideAlert: sitewideAlert
|
||||
? {
|
||||
...sitewideAlert,
|
||||
phoneContact: contactConfig
|
||||
? getAlertPhoneContactData(sitewideAlert, contactConfig)
|
||||
: null,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
},
|
||||
"max"
|
||||
)
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -37,6 +37,8 @@ import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
import { contentstackExtendedProcedureUID, router } from "@/server/trpc"
|
||||
|
||||
import { generateRefsResponseTag } from "@/utils/generateTag"
|
||||
|
||||
import { breadcrumbsRefsSchema, breadcrumbsSchema } from "./output"
|
||||
import { getTags } from "./utils"
|
||||
|
||||
@@ -46,7 +48,6 @@ import type {
|
||||
RawBreadcrumbsSchema,
|
||||
} from "@/types/trpc/routers/contentstack/breadcrumbs"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
import { generateRefsResponseTag } from "@/utils/generateTag"
|
||||
|
||||
const meter = metrics.getMeter("trpc.breadcrumbs")
|
||||
|
||||
@@ -89,8 +90,8 @@ const getBreadcrumbs = cache(async function fetchMemoizedBreadcrumbs<T>(
|
||||
refQuery,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: `force-cache`,
|
||||
next: { tags: [generateRefsResponseTag(lang, uid)] },
|
||||
key: generateRefsResponseTag(lang, uid, "breadcrumbs"),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -129,8 +130,8 @@ const getBreadcrumbs = cache(async function fetchMemoizedBreadcrumbs<T>(
|
||||
query,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: { tags },
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ import {
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { GetCollectionPageSchema } from "@/types/trpc/routers/contentstack/collectionPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export const collectionPageQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
@@ -45,10 +44,8 @@ export const collectionPageQueryRouter = router({
|
||||
GetCollectionPage,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,20 +1,21 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { GetCollectionPageRefs } from "@/lib/graphql/Query/CollectionPage/CollectionPage.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||
|
||||
import { collectionPageRefsSchema } from "./output"
|
||||
|
||||
import { CollectionPageEnum } from "@/types/enums/collectionPage"
|
||||
import { System } from "@/types/requests/system"
|
||||
import {
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type {
|
||||
CollectionPageRefs,
|
||||
GetCollectionPageRefsSchema,
|
||||
} from "@/types/trpc/routers/contentstack/collectionPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const meter = metrics.getMeter("trpc.collectionPage")
|
||||
// OpenTelemetry metrics: CollectionPage
|
||||
@@ -41,15 +42,17 @@ export async function fetchCollectionPageRefs(lang: Lang, uid: string) {
|
||||
query: { lang, uid },
|
||||
})
|
||||
)
|
||||
const refsResponse = await request<GetCollectionPageRefsSchema>(
|
||||
GetCollectionPageRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
}
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
const cacheKey = generateTag(lang, uid)
|
||||
const refsResponse = await cacheClient.cacheOrGet(
|
||||
cacheKey,
|
||||
async () =>
|
||||
await request<GetCollectionPageRefsSchema>(GetCollectionPageRefs, {
|
||||
locale: lang,
|
||||
uid,
|
||||
}),
|
||||
"max"
|
||||
)
|
||||
|
||||
if (!refsResponse.data) {
|
||||
|
||||
@@ -17,7 +17,6 @@ import {
|
||||
|
||||
import type { TrackingSDKPageData } from "@/types/components/tracking"
|
||||
import type { GetContentPageSchema } from "@/types/trpc/routers/contentstack/contentPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
export const contentPageQueryRouter = router({
|
||||
get: contentstackExtendedProcedureUID.query(async ({ ctx }) => {
|
||||
@@ -43,33 +42,27 @@ export const contentPageQueryRouter = router({
|
||||
{
|
||||
document: GetContentPage,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
cacheOptions: {
|
||||
key: `${tags.join(",")}:contentPage`,
|
||||
ttl: "max",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
document: GetContentPageBlocksBatch1,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
cacheOptions: {
|
||||
key: `${tags.join(",")}:contentPageBlocksBatch1`,
|
||||
ttl: "max",
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
document: GetContentPageBlocksBatch2,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
cacheOptions: {
|
||||
key: `${tags.join(",")}:contentPageBlocksBatch2`,
|
||||
ttl: "max",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -49,21 +49,17 @@ export async function fetchContentPageRefs(lang: Lang, uid: string) {
|
||||
{
|
||||
document: GetContentPageRefs,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
cacheOptions: {
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
},
|
||||
},
|
||||
{
|
||||
document: GetContentPageBlocksRefs,
|
||||
variables: { locale: lang, uid },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid + 1)],
|
||||
},
|
||||
cacheOptions: {
|
||||
key: generateTag(lang, uid + 1),
|
||||
ttl: "max",
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -46,10 +46,8 @@ export const destinationCityPageQueryRouter = router({
|
||||
GetDestinationCityPageRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -109,10 +107,8 @@ export const destinationCityPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!response.data) {
|
||||
@@ -153,7 +149,11 @@ export const destinationCityPageQueryRouter = router({
|
||||
}
|
||||
const destinationCityPage = validatedResponse.data.destination_city_page
|
||||
const cityIdentifier = destinationCityPage.destination_settings.city
|
||||
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
|
||||
const city = await getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
lang,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
if (!city) {
|
||||
getDestinationCityPageFailCounter.add(1, {
|
||||
|
||||
@@ -14,8 +14,6 @@ import {
|
||||
getCityPageUrlsSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import type { BatchRequestDocument } from "graphql-request"
|
||||
|
||||
import { DestinationCityPageEnum } from "@/types/enums/destinationCityPage"
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type {
|
||||
@@ -78,17 +76,15 @@ export async function getCityPageCount(lang: Lang) {
|
||||
"contentstack.cityPageCount start",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
const tags = [`${lang}:city_page_count`]
|
||||
|
||||
const response = await request<GetCityPageCountData>(
|
||||
GetCityPageCount,
|
||||
{
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: `${lang}:city_page_count`,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!response.data) {
|
||||
@@ -148,21 +144,14 @@ export async function getCityPageUrls(lang: Lang) {
|
||||
// The `batchRequest` function is not working here, because the arrayMerge is
|
||||
// used for other purposes.
|
||||
const amountOfRequests = Math.ceil(count / 100)
|
||||
const requests: (BatchRequestDocument & { options?: RequestInit })[] =
|
||||
Array.from({ length: amountOfRequests }).map((_, i) => ({
|
||||
document: GetCityPageUrls,
|
||||
variables: { locale: lang, skip: i * 100 },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [`${lang}:city_page_urls_batch_${i}`],
|
||||
},
|
||||
},
|
||||
}))
|
||||
|
||||
const batchedResponse = await Promise.all(
|
||||
requests.map((req) =>
|
||||
request<GetCityPageUrlsData>(req.document, req.variables, req.options)
|
||||
Array.from({ length: amountOfRequests }).map((_, i) =>
|
||||
request<GetCityPageUrlsData>(
|
||||
GetCityPageUrls,
|
||||
{ locale: lang, skip: i * 100 },
|
||||
{ key: `${lang}:city_page_urls_batch_${i}`, ttl: "max" }
|
||||
)
|
||||
)
|
||||
)
|
||||
const validatedResponse = batchedCityPageUrlsSchema.safeParse(batchedResponse)
|
||||
|
||||
@@ -51,10 +51,8 @@ export const destinationCountryPageQueryRouter = router({
|
||||
GetDestinationCountryPageRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -114,10 +112,8 @@ export const destinationCountryPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!response.data) {
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { env } from "@/env/server"
|
||||
import { GetDestinationCityListData } from "@/lib/graphql/Query/DestinationCityPage/DestinationCityListData.graphql"
|
||||
import { GetCountryPageUrls } from "@/lib/graphql/Query/DestinationCountryPage/DestinationCountryPageUrl.graphql"
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { generateTag, generateTagsFromSystem } from "@/utils/generateTag"
|
||||
|
||||
@@ -20,7 +18,6 @@ import {
|
||||
|
||||
import { ApiCountry, type Country } from "@/types/enums/country"
|
||||
import { DestinationCountryPageEnum } from "@/types/enums/destinationCountryPage"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type { GetDestinationCityListDataResponse } from "@/types/trpc/routers/contentstack/destinationCityPage"
|
||||
import type {
|
||||
@@ -85,7 +82,7 @@ export async function getCityListDataByCityIdentifier(
|
||||
"contentstack.cityListData start",
|
||||
JSON.stringify({ query: { lang, cityIdentifier } })
|
||||
)
|
||||
const tag = `${lang}:city_list_data:${cityIdentifier}`
|
||||
|
||||
const response = await request<GetDestinationCityListDataResponse>(
|
||||
GetDestinationCityListData,
|
||||
{
|
||||
@@ -93,10 +90,8 @@ export async function getCityListDataByCityIdentifier(
|
||||
cityIdentifier,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [tag],
|
||||
},
|
||||
key: `${lang}:city_list_data:${cityIdentifier}`,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -148,23 +143,12 @@ export async function getCityPages(
|
||||
serviceToken: string,
|
||||
country: Country
|
||||
) {
|
||||
const apiLang = toApiLang(lang)
|
||||
const params = new URLSearchParams({
|
||||
language: apiLang,
|
||||
})
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const apiCountry = ApiCountry[lang][country]
|
||||
const cities = await getCitiesByCountry([apiCountry], options, params, lang)
|
||||
const cities = await getCitiesByCountry({
|
||||
countries: [apiCountry],
|
||||
lang,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
const publishedCities = cities[apiCountry].filter((city) => city.isPublished)
|
||||
|
||||
@@ -201,10 +185,8 @@ export async function getCountryPageUrls(lang: Lang) {
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [tag],
|
||||
},
|
||||
key: tag,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { env } from "@/env/server"
|
||||
import {
|
||||
GetDestinationOverviewPage,
|
||||
GetDestinationOverviewPageRefs,
|
||||
@@ -10,9 +9,9 @@ import {
|
||||
router,
|
||||
serviceProcedure,
|
||||
} from "@/server/trpc"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { generateTag } from "@/utils/generateTag"
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import {
|
||||
getCitiesByCountry,
|
||||
@@ -42,7 +41,6 @@ import {
|
||||
TrackingChannelEnum,
|
||||
type TrackingSDKPageData,
|
||||
} from "@/types/components/tracking"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type {
|
||||
GetDestinationOverviewPageData,
|
||||
GetDestinationOverviewPageRefsSchema,
|
||||
@@ -66,10 +64,8 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!refsResponse.data) {
|
||||
@@ -133,10 +129,8 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!response.data) {
|
||||
@@ -207,23 +201,11 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
}),
|
||||
destinations: router({
|
||||
get: serviceProcedure.query(async function ({ ctx }) {
|
||||
const apiLang = toApiLang(ctx.lang)
|
||||
const params = new URLSearchParams({
|
||||
language: apiLang,
|
||||
const countries = await getCountries({
|
||||
lang: ctx.lang,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const countries = await getCountries(options, params, ctx.lang)
|
||||
const countryPages = await getCountryPageUrls(ctx.lang)
|
||||
|
||||
if (!countries) {
|
||||
@@ -232,13 +214,12 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
|
||||
const countryNames = countries.data.map((country) => country.name)
|
||||
|
||||
const citiesByCountry = await getCitiesByCountry(
|
||||
countryNames,
|
||||
options,
|
||||
params,
|
||||
ctx.lang,
|
||||
true
|
||||
)
|
||||
const citiesByCountry = await getCitiesByCountry({
|
||||
lang: ctx.lang,
|
||||
countries: countryNames,
|
||||
serviceToken: ctx.serviceToken,
|
||||
onlyPublished: true,
|
||||
})
|
||||
|
||||
const cityPages = await getCityPageUrls(ctx.lang)
|
||||
|
||||
@@ -246,15 +227,11 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
Object.entries(citiesByCountry).map(async ([country, cities]) => {
|
||||
const citiesWithHotelCount = await Promise.all(
|
||||
cities.map(async (city) => {
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: apiLang,
|
||||
city: city.id,
|
||||
})
|
||||
|
||||
const hotels = await getHotelIdsByCityId(
|
||||
city.id,
|
||||
options,
|
||||
hotelIdsParams
|
||||
const [hotels] = await safeTry(
|
||||
getHotelIdsByCityId({
|
||||
cityId: city.id,
|
||||
serviceToken: ctx.serviceToken,
|
||||
})
|
||||
)
|
||||
|
||||
const cityPage = cityPages.find(
|
||||
@@ -268,7 +245,7 @@ export const destinationOverviewPageQueryRouter = router({
|
||||
return {
|
||||
id: city.id,
|
||||
name: city.name,
|
||||
hotelIds: hotels,
|
||||
hotelIds: hotels || [],
|
||||
hotelCount: hotels?.length ?? 0,
|
||||
url: cityPage.url,
|
||||
}
|
||||
|
||||
@@ -31,10 +31,8 @@ export const hotelPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!response.data) {
|
||||
|
||||
@@ -23,8 +23,6 @@ import {
|
||||
getHotelPageUrlsSuccessCounter,
|
||||
} from "./telemetry"
|
||||
|
||||
import type { BatchRequestDocument } from "graphql-request"
|
||||
|
||||
import { HotelPageEnum } from "@/types/enums/hotelPage"
|
||||
import type { System } from "@/types/requests/system"
|
||||
import type {
|
||||
@@ -48,10 +46,8 @@ export async function fetchHotelPageRefs(lang: Lang, uid: string) {
|
||||
GetHotelPageRefs,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!refsResponse.data) {
|
||||
@@ -149,17 +145,14 @@ export async function getHotelPageCount(lang: Lang) {
|
||||
"contentstack.hotelPageCount start",
|
||||
JSON.stringify({ query: { lang } })
|
||||
)
|
||||
const tags = [`${lang}:hotel_page_count`]
|
||||
const response = await request<GetHotelPageCountData>(
|
||||
GetHotelPageCount,
|
||||
{
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: `${lang}:hotel_page_count`,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -220,21 +213,18 @@ export async function getHotelPageUrls(lang: Lang) {
|
||||
// The `batchRequest` function is not working here, because the arrayMerge is
|
||||
// used for other purposes.
|
||||
const amountOfRequests = Math.ceil(count / 100)
|
||||
const requests: (BatchRequestDocument & { options?: RequestInit })[] =
|
||||
Array.from({ length: amountOfRequests }).map((_, i) => ({
|
||||
document: GetHotelPageUrls,
|
||||
variables: { locale: lang, skip: i * 100 },
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [`${lang}:hotel_page_urls_batch_${i}`],
|
||||
},
|
||||
},
|
||||
}))
|
||||
const requests = Array.from({ length: amountOfRequests }).map((_, i) => ({
|
||||
document: GetHotelPageUrls,
|
||||
variables: { locale: lang, skip: i * 100 },
|
||||
cacheKey: `${lang}:hotel_page_urls_batch_${i}`,
|
||||
}))
|
||||
|
||||
const batchedResponse = await Promise.all(
|
||||
requests.map((req) =>
|
||||
request<GetHotelPageUrlsData>(req.document, req.variables, req.options)
|
||||
request<GetHotelPageUrlsData>(req.document, req.variables, {
|
||||
key: req.cacheKey,
|
||||
ttl: "max",
|
||||
})
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -149,21 +149,17 @@ export async function getUrlsOfAllLanguages(
|
||||
{
|
||||
document: daDeEnDocument,
|
||||
variables,
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: tagsDaDeEn,
|
||||
},
|
||||
cacheOptions: {
|
||||
ttl: "max",
|
||||
key: tagsDaDeEn,
|
||||
},
|
||||
},
|
||||
{
|
||||
document: fiNoSvDocument,
|
||||
variables,
|
||||
options: {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: tagsFiNoSv,
|
||||
},
|
||||
cacheOptions: {
|
||||
ttl: "max",
|
||||
key: tagsFiNoSv,
|
||||
},
|
||||
},
|
||||
])
|
||||
|
||||
@@ -63,7 +63,7 @@ export const getAllLoyaltyLevels = cache(async (ctx: Context) => {
|
||||
const loyaltyLevelsConfigResponse = await request<LoyaltyLevelsResponse>(
|
||||
GetAllLoyaltyLevels,
|
||||
{ lang: ctx.lang, level_ids: allLevelIds },
|
||||
{ next: { tags }, cache: "force-cache" }
|
||||
{ key: tags, ttl: "max" }
|
||||
)
|
||||
|
||||
if (!loyaltyLevelsConfigResponse.data) {
|
||||
@@ -113,10 +113,8 @@ export const getLoyaltyLevel = cache(
|
||||
GetLoyaltyLevel,
|
||||
{ lang: ctx.lang, level_id },
|
||||
{
|
||||
next: {
|
||||
tags: [generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id)],
|
||||
},
|
||||
cache: "force-cache",
|
||||
key: generateLoyaltyConfigTag(ctx.lang, "loyalty_level", level_id),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (
|
||||
|
||||
@@ -25,7 +25,6 @@ import type {
|
||||
GetLoyaltyPageRefsSchema,
|
||||
GetLoyaltyPageSchema,
|
||||
} from "@/types/trpc/routers/contentstack/loyaltyPage"
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const meter = metrics.getMeter("trpc.loyaltyPage")
|
||||
// OpenTelemetry metrics: LoyaltyPage
|
||||
@@ -64,10 +63,8 @@ export const loyaltyPageQueryRouter = router({
|
||||
GetLoyaltyPageRefs,
|
||||
variables,
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateRefsResponseTag(lang, uid)],
|
||||
},
|
||||
key: generateRefsResponseTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
@@ -133,8 +130,8 @@ export const loyaltyPageQueryRouter = router({
|
||||
GetLoyaltyPage,
|
||||
variables,
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: { tags },
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -64,10 +64,8 @@ const fetchMetadata = cache(async function fetchMemoizedMetadata<T>(
|
||||
query,
|
||||
{ locale: lang, uid },
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid, affix)],
|
||||
},
|
||||
key: generateTag(lang, uid, affix),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!response.data) {
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { ApiLang, type Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import { type Lang } from "@/constants/languages"
|
||||
import { getFiltersFromHotels } from "@/stores/destination-data/helper"
|
||||
|
||||
import { getIntl } from "@/i18n"
|
||||
@@ -12,7 +11,6 @@ import {
|
||||
} from "../../hotels/utils"
|
||||
|
||||
import { ApiCountry } from "@/types/enums/country"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import { RTETypeEnum } from "@/types/rte/enums"
|
||||
import type {
|
||||
MetadataInputSchema,
|
||||
@@ -218,17 +216,19 @@ export async function getCityData(
|
||||
const cityIdentifier = cities[0]
|
||||
|
||||
if (cityIdentifier) {
|
||||
const cityData = await getCityByCityIdentifier(
|
||||
const cityData = await getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
serviceToken
|
||||
)
|
||||
serviceToken,
|
||||
lang,
|
||||
})
|
||||
const hotelIds = await getHotelIdsByCityIdentifier(
|
||||
cityIdentifier,
|
||||
serviceToken
|
||||
)
|
||||
const hotels = await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
||||
let filterType
|
||||
|
||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||
|
||||
let filterType
|
||||
if (filter) {
|
||||
const allFilters = getFiltersFromHotels(hotels)
|
||||
const facilityFilter = allFilters.facilityFilters.find(
|
||||
@@ -264,28 +264,12 @@ export async function getCountryData(
|
||||
const translatedCountry = ApiCountry[lang][country]
|
||||
let filterType
|
||||
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: ApiLang.En,
|
||||
const hotelIds = await getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken,
|
||||
})
|
||||
const hotelIds = await getHotelIdsByCountry(
|
||||
country,
|
||||
options,
|
||||
hotelIdsParams
|
||||
)
|
||||
|
||||
const hotels = await getHotelsByHotelIds(hotelIds, lang, serviceToken)
|
||||
const hotels = await getHotelsByHotelIds({ hotelIds, lang, serviceToken })
|
||||
|
||||
if (filter) {
|
||||
const allFilters = getFiltersFromHotels(hotels)
|
||||
|
||||
@@ -84,10 +84,8 @@ export const pageSettingsQueryRouter = router({
|
||||
locale: lang,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid, affix)],
|
||||
},
|
||||
key: generateTag(lang, uid, affix),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -32,10 +32,8 @@ export const getSasTierComparison = cache(async (ctx: Context) => {
|
||||
GetAllSasTierComparison,
|
||||
{ lang: ctx.lang },
|
||||
{
|
||||
next: {
|
||||
tags: [tag],
|
||||
},
|
||||
cache: "force-cache",
|
||||
key: tag,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
} from "@/server/trpc"
|
||||
import { langInput } from "@/server/utils"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
|
||||
import { getAllLoyaltyLevels, getLoyaltyLevel } from "../loyaltyLevel/query"
|
||||
import {
|
||||
rewardsAllInput,
|
||||
@@ -46,8 +48,6 @@ import {
|
||||
getUnwrapSurpriseSuccessCounter,
|
||||
} from "./utils"
|
||||
|
||||
const ONE_HOUR = 60 * 60
|
||||
|
||||
export const rewardQueryRouter = router({
|
||||
all: contentStackBaseWithServiceProcedure
|
||||
.input(rewardsAllInput)
|
||||
@@ -174,131 +174,139 @@ export const rewardQueryRouter = router({
|
||||
? api.endpoints.v1.Profile.Reward.reward
|
||||
: api.endpoints.v1.Profile.reward
|
||||
|
||||
const apiResponse = await api.get(endpoint, {
|
||||
cache: undefined, // override defaultOptions
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
next: { revalidate: ONE_HOUR },
|
||||
})
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.reward error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
return cacheClient.cacheOrGet(
|
||||
endpoint,
|
||||
async () => {
|
||||
const apiResponse = await api.get(endpoint, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
|
||||
const validatedApiRewards = isNewEndpoint
|
||||
? validateCategorizedRewardsSchema.safeParse(data)
|
||||
: validateApiRewardSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiRewards.success) {
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
locale: ctx.lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiRewards.error),
|
||||
})
|
||||
console.error(validatedApiRewards.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
JSON.stringify({
|
||||
query: { locale: ctx.lang },
|
||||
error: validatedApiRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const rewardIds = getNonRedeemedRewardIds(validatedApiRewards.data)
|
||||
|
||||
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||
|
||||
if (!cmsRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
const wrappedSurprisesIds = validatedApiRewards.data
|
||||
.filter(
|
||||
(reward) =>
|
||||
reward.type === "coupon" &&
|
||||
reward.rewardType === "Surprise" &&
|
||||
"coupon" in reward &&
|
||||
reward.coupon.some(({ unwrapped }) => !unwrapped)
|
||||
)
|
||||
.map(({ rewardId }) => rewardId)
|
||||
|
||||
const rewards = cmsRewards
|
||||
.filter(
|
||||
(cmsReward) => !wrappedSurprisesIds.includes(cmsReward.reward_id)
|
||||
)
|
||||
.map((cmsReward) => {
|
||||
const apiReward = validatedApiRewards.data.find(
|
||||
({ rewardId }) => rewardId === cmsReward.reward_id
|
||||
)
|
||||
|
||||
const redeemableCoupons =
|
||||
(apiReward &&
|
||||
"coupon" in apiReward &&
|
||||
apiReward.coupon.filter(
|
||||
(coupon) => coupon.state !== "redeemed" && coupon.unwrapped
|
||||
)) ||
|
||||
[]
|
||||
|
||||
const firstRedeemableCouponToExpire = redeemableCoupons.reduce(
|
||||
(earliest, coupon) => {
|
||||
if (dt(coupon.expiresAt).isBefore(dt(earliest.expiresAt))) {
|
||||
return coupon
|
||||
}
|
||||
return earliest
|
||||
},
|
||||
redeemableCoupons[0]
|
||||
)?.couponCode
|
||||
|
||||
return {
|
||||
...cmsReward,
|
||||
id: apiReward?.id,
|
||||
rewardType: apiReward?.rewardType,
|
||||
redeemLocation: apiReward?.redeemLocation,
|
||||
rewardTierLevel:
|
||||
apiReward && "rewardTierLevel" in apiReward
|
||||
? apiReward.rewardTierLevel
|
||||
: undefined,
|
||||
operaRewardId:
|
||||
apiReward && "operaRewardId" in apiReward
|
||||
? apiReward.operaRewardId
|
||||
: "",
|
||||
categories:
|
||||
apiReward && "categories" in apiReward
|
||||
? apiReward.categories || []
|
||||
: [],
|
||||
couponCode: firstRedeemableCouponToExpire,
|
||||
coupons:
|
||||
apiReward && "coupon" in apiReward ? apiReward.coupon || [] : [],
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.reward error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
})
|
||||
|
||||
getCurrentRewardSuccessCounter.add(1)
|
||||
const data = await apiResponse.json()
|
||||
|
||||
return { rewards }
|
||||
const validatedApiRewards = isNewEndpoint
|
||||
? validateCategorizedRewardsSchema.safeParse(data)
|
||||
: validateApiRewardSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiRewards.success) {
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
locale: ctx.lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiRewards.error),
|
||||
})
|
||||
console.error(validatedApiRewards.error)
|
||||
console.error(
|
||||
"contentstack.rewards validation error",
|
||||
JSON.stringify({
|
||||
query: { locale: ctx.lang },
|
||||
error: validatedApiRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const rewardIds = getNonRedeemedRewardIds(validatedApiRewards.data)
|
||||
|
||||
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||
|
||||
if (!cmsRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
const wrappedSurprisesIds = validatedApiRewards.data
|
||||
.filter(
|
||||
(reward) =>
|
||||
reward.type === "coupon" &&
|
||||
reward.rewardType === "Surprise" &&
|
||||
"coupon" in reward &&
|
||||
reward.coupon.some(({ unwrapped }) => !unwrapped)
|
||||
)
|
||||
.map(({ rewardId }) => rewardId)
|
||||
|
||||
const rewards = cmsRewards
|
||||
.filter(
|
||||
(cmsReward) => !wrappedSurprisesIds.includes(cmsReward.reward_id)
|
||||
)
|
||||
.map((cmsReward) => {
|
||||
const apiReward = validatedApiRewards.data.find(
|
||||
({ rewardId }) => rewardId === cmsReward.reward_id
|
||||
)
|
||||
|
||||
const redeemableCoupons =
|
||||
(apiReward &&
|
||||
"coupon" in apiReward &&
|
||||
apiReward.coupon.filter(
|
||||
(coupon) => coupon.state !== "redeemed" && coupon.unwrapped
|
||||
)) ||
|
||||
[]
|
||||
|
||||
const firstRedeemableCouponToExpire = redeemableCoupons.reduce(
|
||||
(earliest, coupon) => {
|
||||
if (dt(coupon.expiresAt).isBefore(dt(earliest.expiresAt))) {
|
||||
return coupon
|
||||
}
|
||||
return earliest
|
||||
},
|
||||
redeemableCoupons[0]
|
||||
)?.couponCode
|
||||
|
||||
return {
|
||||
...cmsReward,
|
||||
id: apiReward?.id,
|
||||
rewardType: apiReward?.rewardType,
|
||||
redeemLocation: apiReward?.redeemLocation,
|
||||
rewardTierLevel:
|
||||
apiReward && "rewardTierLevel" in apiReward
|
||||
? apiReward.rewardTierLevel
|
||||
: undefined,
|
||||
operaRewardId:
|
||||
apiReward && "operaRewardId" in apiReward
|
||||
? apiReward.operaRewardId
|
||||
: "",
|
||||
categories:
|
||||
apiReward && "categories" in apiReward
|
||||
? apiReward.categories || []
|
||||
: [],
|
||||
couponCode: firstRedeemableCouponToExpire,
|
||||
coupons:
|
||||
apiReward && "coupon" in apiReward
|
||||
? apiReward.coupon || []
|
||||
: [],
|
||||
}
|
||||
})
|
||||
|
||||
getCurrentRewardSuccessCounter.add(1)
|
||||
|
||||
return { rewards }
|
||||
},
|
||||
"1h"
|
||||
)
|
||||
}),
|
||||
surprises: contentStackBaseWithProtectedProcedure
|
||||
.input(langInput.optional()) // lang is required for client, but not for server
|
||||
@@ -310,114 +318,120 @@ export const rewardQueryRouter = router({
|
||||
? api.endpoints.v1.Profile.Reward.reward
|
||||
: api.endpoints.v1.Profile.reward
|
||||
|
||||
const apiResponse = await api.get(endpoint, {
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
next: { revalidate: ONE_HOUR },
|
||||
})
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.reward error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
endpoint,
|
||||
async () => {
|
||||
const apiResponse = await api.get(endpoint, {
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiRewards = isNewEndpoint
|
||||
? validateCategorizedRewardsSchema.safeParse(data)
|
||||
: validateApiRewardSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiRewards.success) {
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
locale: ctx.lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiRewards.error),
|
||||
})
|
||||
console.error(validatedApiRewards.error)
|
||||
console.error(
|
||||
"contentstack.surprises validation error",
|
||||
JSON.stringify({
|
||||
query: { locale: ctx.lang },
|
||||
error: validatedApiRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const rewardIds = validatedApiRewards.data
|
||||
.map((reward) => reward?.rewardId)
|
||||
.filter((rewardId): rewardId is string => !!rewardId)
|
||||
.sort()
|
||||
|
||||
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||
|
||||
if (!cmsRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
getCurrentRewardSuccessCounter.add(1)
|
||||
|
||||
const surprises: Surprise[] = validatedApiRewards.data
|
||||
// TODO: Add predicates once legacy endpoints are removed
|
||||
.filter((reward) => {
|
||||
if (reward?.rewardType !== "Surprise") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("coupon" in reward)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const unwrappedCoupons =
|
||||
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
|
||||
if (unwrappedCoupons.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.map((surprise) => {
|
||||
const reward = cmsRewards.find(
|
||||
({ reward_id }) => surprise.rewardId === reward_id
|
||||
)
|
||||
|
||||
if (!reward) {
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.reward error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...reward,
|
||||
id: surprise.id,
|
||||
rewardType: surprise.rewardType,
|
||||
rewardTierLevel: undefined,
|
||||
redeemLocation: surprise.redeemLocation,
|
||||
categories:
|
||||
"categories" in surprise ? surprise.categories || [] : [],
|
||||
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
|
||||
}
|
||||
})
|
||||
.flatMap((surprises) => (surprises ? [surprises] : []))
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiRewards = isNewEndpoint
|
||||
? validateCategorizedRewardsSchema.safeParse(data)
|
||||
: validateApiRewardSchema.safeParse(data)
|
||||
|
||||
return surprises
|
||||
if (!validatedApiRewards.success) {
|
||||
getCurrentRewardFailCounter.add(1, {
|
||||
locale: ctx.lang,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiRewards.error),
|
||||
})
|
||||
console.error(validatedApiRewards.error)
|
||||
console.error(
|
||||
"contentstack.surprises validation error",
|
||||
JSON.stringify({
|
||||
query: { locale: ctx.lang },
|
||||
error: validatedApiRewards.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const rewardIds = validatedApiRewards.data
|
||||
.map((reward) => reward?.rewardId)
|
||||
.filter((rewardId): rewardId is string => !!rewardId)
|
||||
.sort()
|
||||
|
||||
const cmsRewards = await getCmsRewards(ctx.lang, rewardIds)
|
||||
|
||||
if (!cmsRewards) {
|
||||
return null
|
||||
}
|
||||
|
||||
getCurrentRewardSuccessCounter.add(1)
|
||||
|
||||
const surprises: Surprise[] = validatedApiRewards.data
|
||||
// TODO: Add predicates once legacy endpoints are removed
|
||||
.filter((reward) => {
|
||||
if (reward?.rewardType !== "Surprise") {
|
||||
return false
|
||||
}
|
||||
|
||||
if (!("coupon" in reward)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const unwrappedCoupons =
|
||||
reward.coupon.filter((coupon) => !coupon.unwrapped) || []
|
||||
if (unwrappedCoupons.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
.map((surprise) => {
|
||||
const reward = cmsRewards.find(
|
||||
({ reward_id }) => surprise.rewardId === reward_id
|
||||
)
|
||||
|
||||
if (!reward) {
|
||||
return null
|
||||
}
|
||||
|
||||
return {
|
||||
...reward,
|
||||
id: surprise.id,
|
||||
rewardType: surprise.rewardType,
|
||||
rewardTierLevel: undefined,
|
||||
redeemLocation: surprise.redeemLocation,
|
||||
coupons: "coupon" in surprise ? surprise.coupon || [] : [],
|
||||
categories:
|
||||
"categories" in surprise ? surprise.categories || [] : [],
|
||||
}
|
||||
})
|
||||
.flatMap((surprises) => (surprises ? [surprises] : []))
|
||||
|
||||
return surprises
|
||||
},
|
||||
"1h"
|
||||
)
|
||||
}),
|
||||
unwrap: protectedProcedure
|
||||
.input(rewardsUpdateInput)
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
@@ -11,6 +10,7 @@ import {
|
||||
import { request } from "@/lib/graphql/request"
|
||||
import { notFound } from "@/server/errors/trpc"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { generateLoyaltyConfigTag, generateTag } from "@/utils/generateTag"
|
||||
|
||||
import {
|
||||
@@ -85,8 +85,6 @@ export const getAllCMSRewardRefsSuccessCounter = meter.createCounter(
|
||||
"trpc.contentstack.reward.all-success"
|
||||
)
|
||||
|
||||
const ONE_HOUR = 60 * 60
|
||||
|
||||
export function getUniqueRewardIds(rewardIds: string[]) {
|
||||
const uniqueRewardIds = new Set(rewardIds)
|
||||
return Array.from(uniqueRewardIds)
|
||||
@@ -96,123 +94,133 @@ export function getUniqueRewardIds(rewardIds: string[]) {
|
||||
* Uses the legacy profile/v1/Profile/tierRewards endpoint.
|
||||
* TODO: Delete when the new endpoint is out in production.
|
||||
*/
|
||||
export const getAllCachedApiRewards = unstable_cache(
|
||||
async function (token) {
|
||||
const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
export async function getAllCachedApiRewards(token: string) {
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
return await cacheClient.cacheOrGet(
|
||||
"getAllApiRewards",
|
||||
async () => {
|
||||
const apiResponse = await api.get(api.endpoints.v1.Profile.tierRewards, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
})
|
||||
console.error(
|
||||
"api.rewards.tierRewards error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
console.error(
|
||||
"api.rewards.tierRewards error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw apiResponse
|
||||
}
|
||||
throw apiResponse
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiTierRewards = validateApiTierRewardsSchema.safeParse(data)
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiTierRewards =
|
||||
validateApiTierRewardsSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiTierRewards.error),
|
||||
})
|
||||
console.error(validatedApiTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiTierRewards.error,
|
||||
if (!validatedApiTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiTierRewards.error),
|
||||
})
|
||||
)
|
||||
throw validatedApiTierRewards.error
|
||||
}
|
||||
console.error(validatedApiTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiTierRewards.error,
|
||||
})
|
||||
)
|
||||
throw validatedApiTierRewards.error
|
||||
}
|
||||
|
||||
return validatedApiTierRewards.data
|
||||
},
|
||||
["getAllApiRewards"],
|
||||
{ revalidate: ONE_HOUR }
|
||||
)
|
||||
return validatedApiTierRewards.data
|
||||
},
|
||||
"1h"
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Cached for 1 hour.
|
||||
*/
|
||||
export const getCachedAllTierRewards = unstable_cache(
|
||||
async function (token) {
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Profile.Reward.allTiers,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
export async function getCachedAllTierRewards(token: string) {
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
}),
|
||||
})
|
||||
console.error(
|
||||
"api.rewards.allTiers error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
return await cacheClient.cacheOrGet(
|
||||
"getAllTierRewards",
|
||||
async () => {
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Profile.Reward.allTiers,
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${token}`,
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const text = await apiResponse.text()
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify({
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
}),
|
||||
})
|
||||
)
|
||||
console.error(
|
||||
"api.rewards.allTiers error ",
|
||||
JSON.stringify({
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
text,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
throw apiResponse
|
||||
}
|
||||
throw apiResponse
|
||||
}
|
||||
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiAllTierRewards = validateApiAllTiersSchema.safeParse(data)
|
||||
const data = await apiResponse.json()
|
||||
const validatedApiAllTierRewards =
|
||||
validateApiAllTiersSchema.safeParse(data)
|
||||
|
||||
if (!validatedApiAllTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiAllTierRewards.error),
|
||||
})
|
||||
console.error(validatedApiAllTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiAllTierRewards.error,
|
||||
if (!validatedApiAllTierRewards.success) {
|
||||
getAllRewardFailCounter.add(1, {
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedApiAllTierRewards.error),
|
||||
})
|
||||
)
|
||||
throw validatedApiAllTierRewards.error
|
||||
}
|
||||
console.error(validatedApiAllTierRewards.error)
|
||||
console.error(
|
||||
"api.rewards validation error",
|
||||
JSON.stringify({
|
||||
error: validatedApiAllTierRewards.error,
|
||||
})
|
||||
)
|
||||
throw validatedApiAllTierRewards.error
|
||||
}
|
||||
|
||||
return validatedApiAllTierRewards.data
|
||||
},
|
||||
["getApiAllTierRewards"],
|
||||
{ revalidate: ONE_HOUR }
|
||||
)
|
||||
return validatedApiAllTierRewards.data
|
||||
},
|
||||
"1h"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
||||
const tags = rewardIds.map((id) =>
|
||||
@@ -235,10 +243,8 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
||||
rewardIds,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: rewardIds.map((rewardId) => generateTag(lang, rewardId)),
|
||||
},
|
||||
key: rewardIds.map((rewardId) => generateTag(lang, rewardId)),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!refsResponse.data) {
|
||||
@@ -292,7 +298,10 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
||||
locale: lang,
|
||||
rewardIds,
|
||||
},
|
||||
{ next: { tags }, cache: "force-cache" }
|
||||
{
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
} else {
|
||||
cmsRewardsResponse = await request<CmsRewardsResponse>(
|
||||
@@ -301,7 +310,7 @@ export async function getCmsRewards(lang: Lang, rewardIds: string[]) {
|
||||
locale: lang,
|
||||
rewardIds,
|
||||
},
|
||||
{ next: { tags }, cache: "force-cache" }
|
||||
{ key: tags, ttl: "max" }
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -46,10 +46,8 @@ export const startPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags: [generateTag(lang, uid)],
|
||||
},
|
||||
key: generateTag(lang, uid),
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
if (!refsResponse.data) {
|
||||
@@ -118,10 +116,8 @@ export const startPageQueryRouter = router({
|
||||
uid,
|
||||
},
|
||||
{
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
tags,
|
||||
},
|
||||
key: tags,
|
||||
ttl: "max",
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ export const countriesSchema = z.object({
|
||||
}),
|
||||
})
|
||||
|
||||
export type Cities = z.infer<typeof citiesSchema>
|
||||
export const citiesSchema = z
|
||||
.object({
|
||||
data: z.array(citySchema),
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,8 @@
|
||||
import { z } from "zod"
|
||||
|
||||
import {
|
||||
productTypePriceSchema,
|
||||
productTypePointsSchema,
|
||||
productTypePriceSchema,
|
||||
} from "../productTypePrice"
|
||||
|
||||
export const productTypeSchema = z
|
||||
|
||||
@@ -1,14 +1,16 @@
|
||||
import deepmerge from "deepmerge"
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
import { Lang } from "@/constants/languages"
|
||||
import { env } from "@/env/server"
|
||||
import * as api from "@/lib/api"
|
||||
import { toApiLang } from "@/server/utils"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
|
||||
import { getHotelPageUrls } from "../contentstack/hotelPage/utils"
|
||||
import { metrics } from "./metrics"
|
||||
import {
|
||||
type Cities,
|
||||
citiesByCountrySchema,
|
||||
citiesSchema,
|
||||
countriesSchema,
|
||||
@@ -18,12 +20,10 @@ import {
|
||||
import { getHotel } from "./query"
|
||||
|
||||
import { PointOfInterestGroupEnum } from "@/types/enums/pointOfInterest"
|
||||
import type { RequestOptionsWithOutBody } from "@/types/fetch"
|
||||
import type { HotelDataWithUrl } from "@/types/hotel"
|
||||
import type {
|
||||
CitiesGroupedByCountry,
|
||||
CityLocation,
|
||||
HotelLocation,
|
||||
} from "@/types/trpc/routers/hotel/locations"
|
||||
import type { Endpoint } from "@/lib/api/endpoints"
|
||||
|
||||
@@ -58,18 +58,21 @@ export function getPoiGroupByCategoryName(category: string | undefined) {
|
||||
export const locationsAffix = "locations"
|
||||
|
||||
export const TWENTYFOUR_HOURS = 60 * 60 * 24
|
||||
export async function getCity(
|
||||
cityUrl: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
lang: Lang,
|
||||
relationshipCity: HotelLocation["relationships"]["city"]
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (locationCityUrl: string) {
|
||||
const url = new URL(locationCityUrl)
|
||||
export async function getCity({
|
||||
cityUrl,
|
||||
serviceToken,
|
||||
}: {
|
||||
cityUrl: string
|
||||
serviceToken: string
|
||||
}): Promise<Cities> {
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
cityUrl,
|
||||
async () => {
|
||||
const url = new URL(cityUrl)
|
||||
const cityResponse = await api.get(
|
||||
url.pathname as Endpoint,
|
||||
options,
|
||||
{ headers: { Authorization: `Bearer ${serviceToken}` } },
|
||||
url.searchParams
|
||||
)
|
||||
|
||||
@@ -81,33 +84,44 @@ export async function getCity(
|
||||
const city = citiesSchema.safeParse(cityJson)
|
||||
if (!city.success) {
|
||||
console.info(`Validation of city failed`)
|
||||
console.info(`cityUrl: ${locationCityUrl}`)
|
||||
console.info(`cityUrl: ${cityUrl}`)
|
||||
console.error(city.error)
|
||||
return null
|
||||
}
|
||||
|
||||
return city.data
|
||||
},
|
||||
[cityUrl, `${lang}:${relationshipCity}`],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(cityUrl)
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCountries(
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
export async function getCountries({
|
||||
lang,
|
||||
serviceToken,
|
||||
}: {
|
||||
lang: Lang
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (searchParams) {
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${lang}:${locationsAffix}:countries`,
|
||||
async () => {
|
||||
const params = new URLSearchParams({
|
||||
language: toApiLang(lang),
|
||||
})
|
||||
|
||||
const countryResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.countries,
|
||||
options,
|
||||
searchParams
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
throw new Error("Unable to fetch countries")
|
||||
}
|
||||
|
||||
const countriesJson = await countryResponse.json()
|
||||
@@ -120,114 +134,128 @@ export async function getCountries(
|
||||
|
||||
return countries.data
|
||||
},
|
||||
[`${lang}:${locationsAffix}:countries`, params.toString()],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params)
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getCitiesByCountry(
|
||||
countries: string[],
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
lang: Lang,
|
||||
onlyPublished = false, // false by default as it might be used in other places
|
||||
affix: string = locationsAffix
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
searchedCountries: string[]
|
||||
) {
|
||||
const citiesGroupedByCountry: CitiesGroupedByCountry = {}
|
||||
|
||||
await Promise.all(
|
||||
searchedCountries.map(async (country) => {
|
||||
export async function getCitiesByCountry({
|
||||
countries,
|
||||
lang,
|
||||
onlyPublished = false,
|
||||
affix = locationsAffix,
|
||||
serviceToken,
|
||||
}: {
|
||||
countries: string[]
|
||||
lang: Lang
|
||||
onlyPublished?: boolean // false by default as it might be used in other places
|
||||
affix?: string
|
||||
serviceToken: string
|
||||
}): Promise<CitiesGroupedByCountry> {
|
||||
const cacheClient = await getCacheClient()
|
||||
const allCitiesByCountries = await Promise.all(
|
||||
countries.map(async (country) => {
|
||||
return cacheClient.cacheOrGet(
|
||||
`${lang}:${affix}:cities-by-country:${country}`,
|
||||
async () => {
|
||||
const params = new URLSearchParams({
|
||||
language: toApiLang(lang),
|
||||
})
|
||||
const countryResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.Cities.country(country),
|
||||
options,
|
||||
searchParams
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!countryResponse.ok) {
|
||||
return null
|
||||
throw new Error(`Unable to fetch cities by country ${country}`)
|
||||
}
|
||||
|
||||
const countryJson = await countryResponse.json()
|
||||
const citiesByCountry = citiesByCountrySchema.safeParse(countryJson)
|
||||
if (!citiesByCountry.success) {
|
||||
console.info(`Failed to validate Cities by Country payload`)
|
||||
console.error(`Unable to parse cities by country ${country}`)
|
||||
console.error(citiesByCountry.error)
|
||||
return null
|
||||
throw new Error(`Unable to parse cities by country ${country}`)
|
||||
}
|
||||
|
||||
const cities = onlyPublished
|
||||
? citiesByCountry.data.data.filter((city) => city.isPublished)
|
||||
: citiesByCountry.data.data
|
||||
citiesGroupedByCountry[country] = cities
|
||||
return true
|
||||
})
|
||||
return { ...citiesByCountry.data, country }
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
})
|
||||
)
|
||||
|
||||
return citiesGroupedByCountry
|
||||
},
|
||||
[
|
||||
`${lang}:${affix}:cities-by-country`,
|
||||
params.toString(),
|
||||
JSON.stringify(countries),
|
||||
],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, countries)
|
||||
const filteredCitiesByCountries = allCitiesByCountries.map((country) => ({
|
||||
...country,
|
||||
data: onlyPublished
|
||||
? country.data.filter((city) => city.isPublished)
|
||||
: country.data,
|
||||
}))
|
||||
|
||||
const groupedCitiesByCountry: CitiesGroupedByCountry =
|
||||
filteredCitiesByCountries.reduce((acc, { country, data }) => {
|
||||
acc[country] = data
|
||||
return acc
|
||||
}, {} as CitiesGroupedByCountry)
|
||||
|
||||
return groupedCitiesByCountry
|
||||
}
|
||||
|
||||
export async function getLocations(
|
||||
lang: Lang,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams,
|
||||
export async function getLocations({
|
||||
lang,
|
||||
citiesByCountry,
|
||||
serviceToken,
|
||||
}: {
|
||||
lang: Lang
|
||||
citiesByCountry: CitiesGroupedByCountry | null
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (
|
||||
searchParams: URLSearchParams,
|
||||
groupedCitiesByCountry: CitiesGroupedByCountry | null
|
||||
) {
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${lang}:locations`.toLowerCase(),
|
||||
async () => {
|
||||
const params = new URLSearchParams({
|
||||
language: toApiLang(lang),
|
||||
})
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.locations,
|
||||
options,
|
||||
searchParams
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
params
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
if (apiResponse.status === 401) {
|
||||
return { error: true, cause: "unauthorized" } as const
|
||||
throw new Error("unauthorized")
|
||||
} else if (apiResponse.status === 403) {
|
||||
return { error: true, cause: "forbidden" } as const
|
||||
throw new Error("forbidden")
|
||||
}
|
||||
return null
|
||||
throw new Error("downstream error")
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const verifiedLocations = locationsSchema.safeParse(apiJson)
|
||||
if (!verifiedLocations.success) {
|
||||
console.info(`Locations Verification Failed`)
|
||||
console.error(verifiedLocations.error)
|
||||
return null
|
||||
throw new Error("Unable to parse locations")
|
||||
}
|
||||
|
||||
return await Promise.all(
|
||||
verifiedLocations.data.data.map(async (location) => {
|
||||
if (location.type === "cities") {
|
||||
if (groupedCitiesByCountry) {
|
||||
const country = Object.keys(groupedCitiesByCountry).find(
|
||||
(country) => {
|
||||
if (
|
||||
groupedCitiesByCountry[country].find(
|
||||
(loc) => loc.name === location.name
|
||||
)
|
||||
) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
if (citiesByCountry) {
|
||||
const country = Object.keys(citiesByCountry).find((country) =>
|
||||
citiesByCountry[country].find(
|
||||
(loc) => loc.name === location.name
|
||||
)
|
||||
)
|
||||
if (country) {
|
||||
return {
|
||||
@@ -243,12 +271,10 @@ export async function getLocations(
|
||||
}
|
||||
} else if (location.type === "hotels") {
|
||||
if (location.relationships.city?.url) {
|
||||
const city = await getCity(
|
||||
location.relationships.city.url,
|
||||
options,
|
||||
lang,
|
||||
location.relationships.city
|
||||
)
|
||||
const city = await getCity({
|
||||
cityUrl: location.relationships.city.url,
|
||||
serviceToken,
|
||||
})
|
||||
if (city) {
|
||||
return deepmerge(location, {
|
||||
relationships: {
|
||||
@@ -263,44 +289,51 @@ export async function getLocations(
|
||||
})
|
||||
)
|
||||
},
|
||||
[
|
||||
`${lang}:${locationsAffix}`,
|
||||
params.toString(),
|
||||
JSON.stringify(citiesByCountry),
|
||||
],
|
||||
{ revalidate: TWENTYFOUR_HOURS }
|
||||
)(params, citiesByCountry)
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCityId(
|
||||
cityId: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (params: URLSearchParams) {
|
||||
metrics.hotelIds.counter.add(1, { params: params.toString() })
|
||||
export async function getHotelIdsByCityId({
|
||||
cityId,
|
||||
serviceToken,
|
||||
}: {
|
||||
cityId: string
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${cityId}:hotelsByCityId`,
|
||||
async () => {
|
||||
const searchParams = new URLSearchParams({
|
||||
city: cityId,
|
||||
})
|
||||
metrics.hotelIds.counter.add(1, { params: searchParams.toString() })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids start",
|
||||
JSON.stringify({ params: params.toString() })
|
||||
JSON.stringify({ params: searchParams.toString() })
|
||||
)
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.hotels,
|
||||
options,
|
||||
params
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
searchParams
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
const responseMessage = await apiResponse.text()
|
||||
metrics.hotelIds.fail.add(1, {
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error_type: "http_error",
|
||||
error: responseMessage,
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids fetch error",
|
||||
JSON.stringify({
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error: {
|
||||
status: apiResponse.status,
|
||||
statusText: apiResponse.statusText,
|
||||
@@ -309,59 +342,73 @@ export async function getHotelIdsByCityId(
|
||||
})
|
||||
)
|
||||
|
||||
return []
|
||||
throw new Error("Unable to fetch hotelIds by cityId")
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
const validatedHotelIds = getHotelIdsSchema.safeParse(apiJson)
|
||||
if (!validatedHotelIds.success) {
|
||||
metrics.hotelIds.fail.add(1, {
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedHotelIds.error),
|
||||
})
|
||||
console.error(
|
||||
"api.hotel.hotel-ids validation error",
|
||||
JSON.stringify({
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
error: validatedHotelIds.error,
|
||||
})
|
||||
)
|
||||
return []
|
||||
|
||||
throw new Error("Unable to parse data for hotelIds by cityId")
|
||||
}
|
||||
|
||||
metrics.hotelIds.success.add(1, { cityId })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids success",
|
||||
JSON.stringify({
|
||||
params: params.toString(),
|
||||
params: searchParams.toString(),
|
||||
response: validatedHotelIds.data,
|
||||
})
|
||||
)
|
||||
|
||||
return validatedHotelIds.data
|
||||
},
|
||||
[`hotelsByCityId`, params.toString()],
|
||||
{ revalidate: env.CACHE_TIME_HOTELS }
|
||||
)(params)
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCountry(
|
||||
country: string,
|
||||
options: RequestOptionsWithOutBody,
|
||||
params: URLSearchParams
|
||||
) {
|
||||
return unstable_cache(
|
||||
async function (params: URLSearchParams) {
|
||||
export async function getHotelIdsByCountry({
|
||||
country,
|
||||
serviceToken,
|
||||
}: {
|
||||
country: string
|
||||
serviceToken: string
|
||||
}) {
|
||||
const cacheClient = await getCacheClient()
|
||||
|
||||
return await cacheClient.cacheOrGet(
|
||||
`${country}:hotelsByCountry`,
|
||||
async () => {
|
||||
metrics.hotelIds.counter.add(1, { country })
|
||||
console.info(
|
||||
"api.hotel.hotel-ids start",
|
||||
JSON.stringify({ query: { country } })
|
||||
)
|
||||
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
country,
|
||||
})
|
||||
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Hotel.hotels,
|
||||
options,
|
||||
params
|
||||
{
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
},
|
||||
hotelIdsParams
|
||||
)
|
||||
|
||||
if (!apiResponse.ok) {
|
||||
@@ -383,7 +430,7 @@ export async function getHotelIdsByCountry(
|
||||
})
|
||||
)
|
||||
|
||||
return []
|
||||
throw new Error("Unable to fetch hotelIds by country")
|
||||
}
|
||||
|
||||
const apiJson = await apiResponse.json()
|
||||
@@ -401,7 +448,7 @@ export async function getHotelIdsByCountry(
|
||||
error: validatedHotelIds.error,
|
||||
})
|
||||
)
|
||||
return []
|
||||
throw new Error("Unable to parse hotelIds by country")
|
||||
}
|
||||
|
||||
metrics.hotelIds.success.add(1, { country })
|
||||
@@ -412,62 +459,45 @@ export async function getHotelIdsByCountry(
|
||||
|
||||
return validatedHotelIds.data
|
||||
},
|
||||
[`hotelsByCountry`, params.toString()],
|
||||
{ revalidate: env.CACHE_TIME_HOTELS }
|
||||
)(params)
|
||||
env.CACHE_TIME_HOTELS
|
||||
)
|
||||
}
|
||||
|
||||
export async function getHotelIdsByCityIdentifier(
|
||||
cityIdentifier: string,
|
||||
serviceToken: string
|
||||
) {
|
||||
const apiLang = toApiLang(Lang.en)
|
||||
const city = await getCityByCityIdentifier(cityIdentifier, serviceToken)
|
||||
const city = await getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
lang: Lang.en,
|
||||
serviceToken,
|
||||
})
|
||||
|
||||
if (!city) {
|
||||
return []
|
||||
}
|
||||
|
||||
const hotelIdsParams = new URLSearchParams({
|
||||
language: apiLang,
|
||||
city: city.id,
|
||||
const hotelIds = await getHotelIdsByCityId({
|
||||
cityId: city.id,
|
||||
serviceToken,
|
||||
})
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const hotelIds = await getHotelIdsByCityId(city.id, options, hotelIdsParams)
|
||||
return hotelIds
|
||||
}
|
||||
|
||||
export async function getCityByCityIdentifier(
|
||||
cityIdentifier: string,
|
||||
export async function getCityByCityIdentifier({
|
||||
cityIdentifier,
|
||||
lang,
|
||||
serviceToken,
|
||||
}: {
|
||||
cityIdentifier: string
|
||||
lang: Lang
|
||||
serviceToken: string
|
||||
) {
|
||||
const lang = Lang.en
|
||||
const apiLang = toApiLang(lang)
|
||||
const options: RequestOptionsWithOutBody = {
|
||||
// needs to clear default option as only
|
||||
// cache or next.revalidate is permitted
|
||||
cache: undefined,
|
||||
headers: {
|
||||
Authorization: `Bearer ${serviceToken}`,
|
||||
},
|
||||
next: {
|
||||
revalidate: env.CACHE_TIME_HOTELS,
|
||||
},
|
||||
}
|
||||
const params = new URLSearchParams({
|
||||
language: apiLang,
|
||||
}) {
|
||||
const locations = await getLocations({
|
||||
lang,
|
||||
citiesByCountry: null,
|
||||
serviceToken,
|
||||
})
|
||||
const locations = await getLocations(lang, options, params, null)
|
||||
if (!locations || "error" in locations) {
|
||||
return null
|
||||
}
|
||||
@@ -479,11 +509,15 @@ export async function getCityByCityIdentifier(
|
||||
return city ?? null
|
||||
}
|
||||
|
||||
export async function getHotelsByHotelIds(
|
||||
hotelIds: string[],
|
||||
lang: Lang,
|
||||
export async function getHotelsByHotelIds({
|
||||
hotelIds,
|
||||
lang,
|
||||
serviceToken,
|
||||
}: {
|
||||
hotelIds: string[]
|
||||
lang: Lang
|
||||
serviceToken: string
|
||||
) {
|
||||
}) {
|
||||
const hotelPages = await getHotelPageUrls(lang)
|
||||
const hotels = await Promise.all(
|
||||
hotelIds.map(async (hotelId) => {
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { publicProcedure, router } from "@/server/trpc"
|
||||
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
|
||||
import { jobylonFeedSchema } from "./output"
|
||||
import {
|
||||
getJobylonFeedCounter,
|
||||
@@ -29,66 +31,74 @@ export const jobylonQueryRouter = router({
|
||||
JSON.stringify({ query: { url: urlString } })
|
||||
)
|
||||
|
||||
const response = await fetch(url, {
|
||||
cache: "force-cache",
|
||||
next: {
|
||||
revalidate: TWENTYFOUR_HOURS,
|
||||
const cacheClient = await getCacheClient()
|
||||
return await cacheClient.cacheOrGet(
|
||||
"jobylon:feed",
|
||||
async () => {
|
||||
const response = await fetch(url, {
|
||||
cache: "no-cache",
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
const error = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
text,
|
||||
}
|
||||
getJobylonFeedFailCounter.add(1, {
|
||||
url: urlString,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify(error),
|
||||
})
|
||||
console.error(
|
||||
"jobylon.feed error",
|
||||
JSON.stringify({
|
||||
query: { url: urlString },
|
||||
error,
|
||||
})
|
||||
)
|
||||
|
||||
throw new Error(
|
||||
`Failed to fetch Jobylon feed: ${JSON.stringify(error)}`
|
||||
)
|
||||
}
|
||||
|
||||
const responseJson = await response.json()
|
||||
const validatedResponse = jobylonFeedSchema.safeParse(responseJson)
|
||||
|
||||
if (!validatedResponse.success) {
|
||||
getJobylonFeedFailCounter.add(1, {
|
||||
urlString,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedResponse.error),
|
||||
})
|
||||
|
||||
const errorData = JSON.stringify({
|
||||
query: { url: urlString },
|
||||
error: validatedResponse.error,
|
||||
})
|
||||
|
||||
console.error("jobylon.feed error", errorData)
|
||||
throw new Error(
|
||||
`Failed to parse Jobylon feed: ${JSON.stringify(errorData)}`
|
||||
)
|
||||
}
|
||||
|
||||
getJobylonFeedSuccessCounter.add(1, {
|
||||
url: urlString,
|
||||
})
|
||||
console.info(
|
||||
"jobylon.feed success",
|
||||
JSON.stringify({
|
||||
query: { url: urlString },
|
||||
})
|
||||
)
|
||||
|
||||
return validatedResponse.data
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
const text = await response.text()
|
||||
const error = {
|
||||
status: response.status,
|
||||
statusText: response.statusText,
|
||||
text,
|
||||
}
|
||||
getJobylonFeedFailCounter.add(1, {
|
||||
url: urlString,
|
||||
error_type: "http_error",
|
||||
error: JSON.stringify(error),
|
||||
})
|
||||
console.error(
|
||||
"jobylon.feed error",
|
||||
JSON.stringify({
|
||||
query: { url: urlString },
|
||||
error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
const responseJson = await response.json()
|
||||
const validatedResponse = jobylonFeedSchema.safeParse(responseJson)
|
||||
|
||||
if (!validatedResponse.success) {
|
||||
getJobylonFeedFailCounter.add(1, {
|
||||
urlString,
|
||||
error_type: "validation_error",
|
||||
error: JSON.stringify(validatedResponse.error),
|
||||
})
|
||||
|
||||
console.error(
|
||||
"jobylon.feed error",
|
||||
JSON.stringify({
|
||||
query: { url: urlString },
|
||||
error: validatedResponse.error,
|
||||
})
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
getJobylonFeedSuccessCounter.add(1, {
|
||||
url: urlString,
|
||||
})
|
||||
console.info(
|
||||
"jobylon.feed success",
|
||||
JSON.stringify({
|
||||
query: { url: urlString },
|
||||
})
|
||||
"1d"
|
||||
)
|
||||
|
||||
return validatedResponse.data
|
||||
}),
|
||||
}),
|
||||
})
|
||||
|
||||
@@ -641,11 +641,9 @@ export const userQueryRouter = router({
|
||||
const apiResponse = await api.get(
|
||||
api.endpoints.v1.Profile.Transaction.friendTransactions,
|
||||
{
|
||||
cache: undefined, // override defaultOptions
|
||||
headers: {
|
||||
Authorization: `Bearer ${ctx.session.token.access_token}`,
|
||||
},
|
||||
next: { revalidate: 30 * 60 * 1000 },
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
import { metrics } from "@opentelemetry/api"
|
||||
import { revalidateTag, unstable_cache } from "next/cache"
|
||||
import { metrics, trace } from "@opentelemetry/api"
|
||||
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { generateServiceTokenTag } from "@/utils/generateTag"
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
|
||||
import type { ServiceTokenResponse } from "@/types/tokens"
|
||||
|
||||
@@ -12,13 +11,49 @@ const meter = metrics.getMeter("trpc.context.serviceToken")
|
||||
const fetchServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-new-token"
|
||||
)
|
||||
const fetchTempServiceTokenCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-temporary"
|
||||
)
|
||||
|
||||
const fetchServiceTokenFailCounter = meter.createCounter(
|
||||
"trpc.context.serviceToken.fetch-fail"
|
||||
)
|
||||
|
||||
export async function getServiceToken() {
|
||||
const tracer = trace.getTracer("getServiceToken")
|
||||
|
||||
return await tracer.startActiveSpan("getServiceToken", async () => {
|
||||
let scopes: string[] = []
|
||||
if (env.ENABLE_BOOKING_FLOW) {
|
||||
scopes = ["profile", "hotel", "booking", "package", "availability"]
|
||||
} else {
|
||||
scopes = ["profile"]
|
||||
}
|
||||
const cacheKey = getServiceTokenCacheKey(scopes)
|
||||
|
||||
const cacheClient = await getCacheClient()
|
||||
const token =
|
||||
await cacheClient.get<Awaited<ReturnType<typeof getJwt>>>(cacheKey)
|
||||
console.log("[DEBUG] getServiceToken", typeof token, token)
|
||||
if (!token || token.expiresAt < Date.now()) {
|
||||
return await tracer.startActiveSpan("fetch new token", async () => {
|
||||
const newToken = await getJwt(scopes)
|
||||
const relativeTime = (newToken.expiresAt - Date.now()) / 1000
|
||||
await cacheClient.set(cacheKey, newToken, relativeTime)
|
||||
|
||||
return newToken.jwt
|
||||
})
|
||||
}
|
||||
|
||||
return token.jwt
|
||||
})
|
||||
}
|
||||
|
||||
async function getJwt(scopes: string[]) {
|
||||
fetchServiceTokenCounter.add(1)
|
||||
const jwt = await fetchServiceToken(scopes)
|
||||
|
||||
const expiresAt = Date.now() + jwt.expires_in * 1000
|
||||
return { expiresAt, jwt }
|
||||
}
|
||||
|
||||
async function fetchServiceToken(scopes: string[]) {
|
||||
fetchServiceTokenCounter.add(1)
|
||||
|
||||
@@ -69,41 +104,6 @@ async function fetchServiceToken(scopes: string[]) {
|
||||
return response.json() as Promise<ServiceTokenResponse>
|
||||
}
|
||||
|
||||
export async function getServiceToken() {
|
||||
let scopes: string[] = []
|
||||
if (env.ENABLE_BOOKING_FLOW) {
|
||||
scopes = ["profile", "hotel", "booking", "package", "availability"]
|
||||
} else {
|
||||
scopes = ["profile"]
|
||||
}
|
||||
|
||||
const tag = generateServiceTokenTag(scopes)
|
||||
const getCachedJwt = unstable_cache(
|
||||
async (scopes) => {
|
||||
const jwt = await fetchServiceToken(scopes)
|
||||
|
||||
const expiresAt = Date.now() + jwt.expires_in * 1000
|
||||
return { expiresAt, jwt }
|
||||
},
|
||||
[tag],
|
||||
{ tags: [tag] }
|
||||
)
|
||||
|
||||
const cachedJwt = await getCachedJwt(scopes)
|
||||
if (cachedJwt.expiresAt < Date.now()) {
|
||||
console.log(
|
||||
"trpc.context.serviceToken: Service token expired, revalidating tag"
|
||||
)
|
||||
|
||||
revalidateTag(tag)
|
||||
|
||||
console.log(
|
||||
"trpc.context.serviceToken: Fetching new temporary service token."
|
||||
)
|
||||
fetchTempServiceTokenCounter.add(1)
|
||||
const newToken = await fetchServiceToken(scopes)
|
||||
return newToken
|
||||
}
|
||||
|
||||
return cachedJwt.jwt
|
||||
function getServiceTokenCacheKey(scopes: string[]): string {
|
||||
return `serviceToken:${scopes.join(",")}`
|
||||
}
|
||||
|
||||
@@ -138,7 +138,9 @@ export const safeProtectedProcedure = baseProcedure.use(async function (opts) {
|
||||
})
|
||||
|
||||
export const serviceProcedure = baseProcedure.use(async (opts) => {
|
||||
const { access_token } = await getServiceToken()
|
||||
const token = await getServiceToken()
|
||||
console.log("[DEBUG] token", typeof token, token)
|
||||
const { access_token } = token
|
||||
if (!access_token) {
|
||||
throw internalServerError(`[serviceProcedure] No service token`)
|
||||
}
|
||||
|
||||
@@ -1,50 +1,23 @@
|
||||
import { getCacheClient } from "@/services/dataCache"
|
||||
import { resolve as resolveEntry } from "@/utils/entry"
|
||||
|
||||
import type { Lang } from "@/constants/languages"
|
||||
|
||||
const entryResponseCache: Map<
|
||||
string,
|
||||
{
|
||||
contentType: string | null
|
||||
uid: string | null
|
||||
expiresAt: number
|
||||
}
|
||||
> = new Map()
|
||||
|
||||
let size: number = 0
|
||||
|
||||
export const fetchAndCacheEntry = async (path: string, lang: Lang) => {
|
||||
const cacheKey = `${path + lang}`
|
||||
const cachedResponse = entryResponseCache.get(cacheKey)
|
||||
path = path || "/"
|
||||
const cacheKey = `${lang}:resolveentry:${path}`
|
||||
const cache = await getCacheClient()
|
||||
|
||||
if (cachedResponse && cachedResponse.expiresAt > Date.now() / 1000) {
|
||||
console.log("[CMS MIDDLEWARE]: CACHE HIT")
|
||||
return cachedResponse
|
||||
}
|
||||
return cache.cacheOrGet(
|
||||
cacheKey,
|
||||
async () => {
|
||||
const { contentType, uid } = await resolveEntry(path, lang)
|
||||
|
||||
if (cachedResponse && cachedResponse.expiresAt < Date.now() / 1000) {
|
||||
console.log("[CMS MIDDLEWARE]: CACHE STALE")
|
||||
size -= JSON.stringify(cachedResponse).length
|
||||
entryResponseCache.delete(cacheKey)
|
||||
} else {
|
||||
console.log("[CMS MIDDLEWARE]: CACHE MISS")
|
||||
}
|
||||
|
||||
const { contentType, uid } = await resolveEntry(path, lang)
|
||||
let expiresAt = Date.now() / 1000
|
||||
if (!contentType || !uid) {
|
||||
expiresAt += 600
|
||||
} else {
|
||||
expiresAt += 3600 * 12
|
||||
}
|
||||
const entryCache = { contentType, uid, expiresAt }
|
||||
size += JSON.stringify(entryCache).length
|
||||
console.log("[CMS MIDDLEWARE] Adding to cache", entryCache)
|
||||
console.log("[CMS MIDDLEWARE] Cache size (total)", size)
|
||||
entryResponseCache.set(cacheKey, entryCache)
|
||||
|
||||
return {
|
||||
contentType,
|
||||
uid,
|
||||
}
|
||||
return {
|
||||
contentType,
|
||||
uid,
|
||||
}
|
||||
},
|
||||
"1d"
|
||||
)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,96 @@
|
||||
const ONE_HOUR_IN_SECONDS = 3_600 as const
|
||||
const ONE_DAY_IN_SECONDS = 86_400 as const
|
||||
|
||||
export const namedCacheTimeMap: Record<NamedCacheTimes, number> = {
|
||||
"no cache": 0,
|
||||
"1m": 60,
|
||||
"5m": 300,
|
||||
"10m": 600,
|
||||
"1h": ONE_HOUR_IN_SECONDS,
|
||||
"3h": ONE_HOUR_IN_SECONDS * 3,
|
||||
"6h": ONE_HOUR_IN_SECONDS * 6,
|
||||
"1d": ONE_DAY_IN_SECONDS,
|
||||
"3d": ONE_DAY_IN_SECONDS * 3,
|
||||
max: ONE_DAY_IN_SECONDS * 30,
|
||||
} as const
|
||||
|
||||
export const namedCacheTimes = [
|
||||
"no cache",
|
||||
"1m",
|
||||
"5m",
|
||||
"10m",
|
||||
"1h",
|
||||
"3h",
|
||||
"6h",
|
||||
"1d",
|
||||
"3d",
|
||||
"max",
|
||||
] as const
|
||||
|
||||
export type NamedCacheTimes = (typeof namedCacheTimes)[number]
|
||||
|
||||
/**
|
||||
* Retrieves the cache time in seconds based on the given cache time.
|
||||
* @param cacheTime - The time value to determine, either a named cache time or a number of seconds.
|
||||
* @returns The cache time in seconds.
|
||||
*/
|
||||
export const getCacheTimeInSeconds = (cacheTime: CacheTime): number => {
|
||||
if (typeof cacheTime === "number") {
|
||||
if (cacheTime < 0 || !Number.isInteger(cacheTime)) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return cacheTime
|
||||
}
|
||||
|
||||
return namedCacheTimeMap[cacheTime] ?? 0
|
||||
}
|
||||
|
||||
export type CacheTime = NamedCacheTimes | number
|
||||
|
||||
export type DataCache = {
|
||||
/**
|
||||
* Type of cache
|
||||
*/
|
||||
type: "edge" | "redis" | "in-memory" | "unstable-cache"
|
||||
|
||||
/**
|
||||
* Helper function that retrieves from the cache if it exists, otherwise calls the callback and caches the result.
|
||||
* If the call fails, the cache is not updated.
|
||||
* @param key The cache key
|
||||
* @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
|
||||
* @returns The cached value or the result from the callback
|
||||
*/
|
||||
cacheOrGet: <T>(
|
||||
key: string | string[],
|
||||
getDataFromSource: () => Promise<T>,
|
||||
ttl: CacheTime
|
||||
) => Promise<T>
|
||||
|
||||
/**
|
||||
* Get a value from the cache, if it exists
|
||||
* @see `cacheOrGet` for a more convenient way to cache values
|
||||
* @param key The cache key to retrieve the value for
|
||||
* @returns The cached value or undefined if not found
|
||||
*/
|
||||
get: <T>(key: string) => Promise<T | undefined>
|
||||
|
||||
/**
|
||||
* Sets a value in the cache.
|
||||
* @see `cacheOrGet` for a more convenient way to cache values
|
||||
* @param key CacheKey to set
|
||||
* @param obj Value to be cached
|
||||
* @param ttl Time to live, either a named cache time or a number of seconds
|
||||
* @returns A promise that resolves when the value has been cached
|
||||
*/
|
||||
set: <T>(key: string, obj: T, ttl: CacheTime) => Promise<void>
|
||||
|
||||
/**
|
||||
* Deletes a key from the cache
|
||||
* @param key CacheKey to delete
|
||||
* @param fuzzy If true, does a wildcard delete. *key*
|
||||
* @returns
|
||||
*/
|
||||
deleteKey: (key: string, opts?: { fuzzy?: boolean }) => Promise<void>
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
import { cacheLogger } from "../logger"
|
||||
import { generateCacheKey } from "./generateCacheKey"
|
||||
import { get } from "./get"
|
||||
import { set } from "./set"
|
||||
|
||||
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
||||
key: string | string[],
|
||||
callback: () => Promise<T>,
|
||||
ttl: CacheTime
|
||||
) => {
|
||||
const cacheKey = generateCacheKey(key)
|
||||
const cachedValue = await get<T>(cacheKey)
|
||||
|
||||
if (!cachedValue) {
|
||||
const perf = performance.now()
|
||||
const data = await callback()
|
||||
|
||||
cacheLogger.debug(
|
||||
`Getting data '${cacheKey}' took ${(performance.now() - perf).toFixed(2)}ms`
|
||||
)
|
||||
|
||||
await set<T>(cacheKey, data, ttl)
|
||||
return data
|
||||
}
|
||||
|
||||
return cachedValue
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { cacheOrGet } from "./cacheOrGet"
|
||||
import { deleteKey } from "./deleteKey"
|
||||
import { get } from "./get"
|
||||
import { set } from "./set"
|
||||
|
||||
import type { DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
export const API_KEY = env.REDIS_API_KEY ?? ""
|
||||
export async function createDistributedCache(): Promise<DataCache> {
|
||||
return {
|
||||
type: "redis",
|
||||
get,
|
||||
set,
|
||||
cacheOrGet,
|
||||
deleteKey,
|
||||
} satisfies DataCache
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
|
||||
import { cacheLogger } from "../logger"
|
||||
import { API_KEY } from "./client"
|
||||
import { getCacheEndpoint } from "./endpoints"
|
||||
|
||||
export async function deleteKey<T>(key: string) {
|
||||
const perf = performance.now()
|
||||
|
||||
const response = await fetch(getCacheEndpoint(key), {
|
||||
method: "DELETE",
|
||||
cache: "no-cache",
|
||||
headers: {
|
||||
"x-api-key": API_KEY,
|
||||
},
|
||||
})
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status !== 404) {
|
||||
Sentry.captureMessage("Unable to DELETE cachekey", {
|
||||
level: "error",
|
||||
extra: {
|
||||
cacheKey: key,
|
||||
statusCode: response?.status,
|
||||
statusText: response?.statusText,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
return undefined
|
||||
}
|
||||
|
||||
const data = (await response.json()) as { data: T }
|
||||
cacheLogger.debug(
|
||||
`Delete '${key}' took ${(performance.now() - perf).toFixed(2)}ms`
|
||||
)
|
||||
return data.data
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { generateCacheKey } from "./generateCacheKey"
|
||||
|
||||
export function getCacheEndpoint(key: string) {
|
||||
if (!env.REDIS_API_HOST) {
|
||||
throw new Error("REDIS_API_HOST is not set")
|
||||
}
|
||||
|
||||
const url = new URL(`/api/cache`, env.REDIS_API_HOST)
|
||||
url.searchParams.set("key", encodeURIComponent(generateCacheKey(key)))
|
||||
|
||||
return url
|
||||
}
|
||||
|
||||
export function getClearCacheEndpoint() {
|
||||
if (!env.REDIS_API_HOST) {
|
||||
throw new Error("REDIS_API_HOST is not set")
|
||||
}
|
||||
|
||||
const url = new URL(`/api/cache/clear`, env.REDIS_API_HOST)
|
||||
|
||||
return url
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
export function generateCacheKey(key: string | string[]): string {
|
||||
const prefix = getPrefix()
|
||||
key = Array.isArray(key) ? key.join("_") : key
|
||||
|
||||
return `${prefix ? `${prefix}:` : ""}${key}`
|
||||
}
|
||||
|
||||
function getPrefix(): string {
|
||||
if (process.env.NODE_ENV === "development") {
|
||||
const devPrefix = process.env.USER || process.env.USERNAME || "dev"
|
||||
return `${devPrefix}`
|
||||
}
|
||||
|
||||
const branch = env.BRANCH.trim()
|
||||
const gitSha = env.GIT_SHA?.trim().substring(0, 7)
|
||||
|
||||
return `${branch}:${gitSha}`
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import { cacheLogger } from "../logger"
|
||||
import { API_KEY } from "./client"
|
||||
import { deleteKey } from "./deleteKey"
|
||||
import { getCacheEndpoint } from "./endpoints"
|
||||
|
||||
export async function get<T>(key: string) {
|
||||
const perf = performance.now()
|
||||
|
||||
const [response, error] = await safeTry(
|
||||
fetch(getCacheEndpoint(key), {
|
||||
method: "GET",
|
||||
cache: "no-cache",
|
||||
headers: {
|
||||
"x-api-key": API_KEY,
|
||||
},
|
||||
})
|
||||
)
|
||||
|
||||
if (!response || error || !response.ok) {
|
||||
if (response?.status === 404) {
|
||||
cacheLogger.debug(
|
||||
`Miss '${key}' took ${(performance.now() - perf).toFixed(2)}ms`
|
||||
)
|
||||
return undefined
|
||||
}
|
||||
|
||||
Sentry.captureMessage("Unable to GET cachekey", {
|
||||
level: "error",
|
||||
extra: {
|
||||
cacheKey: key,
|
||||
errorMessage: error instanceof Error ? error.message : undefined,
|
||||
statusCode: response?.status,
|
||||
statusText: response?.statusText,
|
||||
},
|
||||
})
|
||||
return undefined
|
||||
}
|
||||
|
||||
const [data, jsonError] = await safeTry(
|
||||
response.json() as Promise<{ data: T }>
|
||||
)
|
||||
|
||||
if (jsonError) {
|
||||
cacheLogger.error("Failed to parse cache response", {
|
||||
key,
|
||||
error: jsonError,
|
||||
})
|
||||
|
||||
await deleteKey(key)
|
||||
return undefined
|
||||
}
|
||||
|
||||
cacheLogger.debug(
|
||||
`Hit '${key}' took ${(performance.now() - perf).toFixed(2)}ms`
|
||||
)
|
||||
|
||||
return data?.data
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
export { createDistributedCache } from "./client"
|
||||
@@ -0,0 +1,33 @@
|
||||
import * as Sentry from "@sentry/nextjs"
|
||||
|
||||
import { safeTry } from "@/utils/safeTry"
|
||||
|
||||
import { type CacheTime, getCacheTimeInSeconds } from "../Cache"
|
||||
import { API_KEY } from "./client"
|
||||
import { getCacheEndpoint } from "./endpoints"
|
||||
|
||||
export async function set<T>(key: string, value: T, ttl: CacheTime) {
|
||||
const [response, error] = await safeTry(
|
||||
fetch(getCacheEndpoint(key), {
|
||||
method: "PUT",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
"x-api-key": API_KEY,
|
||||
},
|
||||
body: JSON.stringify({ data: value, ttl: getCacheTimeInSeconds(ttl) }),
|
||||
cache: "no-cache",
|
||||
})
|
||||
)
|
||||
|
||||
if (!response || error || !response.ok) {
|
||||
Sentry.captureMessage("Unable to SET cachekey", {
|
||||
level: "error",
|
||||
extra: {
|
||||
cacheKey: key,
|
||||
errorMessage: error instanceof Error ? error.message : undefined,
|
||||
statusCode: response?.status,
|
||||
statusText: response?.statusText,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
export function shouldHaveFullTtl() {
|
||||
return env.BRANCH === "release"
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
export const cacheMap = new Map<
|
||||
string,
|
||||
{
|
||||
/** Absolute expiration timestamp (`Date.now()`) */
|
||||
expiresAt: number
|
||||
/** The cached data */
|
||||
data: unknown
|
||||
}
|
||||
>()
|
||||
@@ -0,0 +1,36 @@
|
||||
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
||||
import { cacheLogger } from "@/services/dataCache/logger"
|
||||
|
||||
import { get } from "./get"
|
||||
import { set } from "./set"
|
||||
|
||||
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
||||
key: string | string[],
|
||||
callback: () => Promise<T>,
|
||||
ttl: CacheTime
|
||||
): Promise<T> => {
|
||||
if (Array.isArray(key)) {
|
||||
key = key.join("-")
|
||||
}
|
||||
|
||||
const cached = await get(key)
|
||||
|
||||
if (cached) {
|
||||
return cached as T
|
||||
}
|
||||
|
||||
cacheLogger.debug(`Miss for key '${key}'`)
|
||||
|
||||
try {
|
||||
const data = await callback()
|
||||
await set(key, data, ttl)
|
||||
|
||||
return data
|
||||
} catch (e) {
|
||||
cacheLogger.error(
|
||||
`Error while fetching data for key '${key}', avoid caching`,
|
||||
e
|
||||
)
|
||||
throw e
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { cacheLogger } from "@/services/dataCache/logger"
|
||||
|
||||
import { cacheMap } from "./cacheMap"
|
||||
|
||||
export async function deleteAll() {
|
||||
cacheLogger.debug("Deleting all keys")
|
||||
|
||||
cacheMap.clear()
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { cacheLogger } from "@/services/dataCache/logger"
|
||||
|
||||
import { cacheMap } from "./cacheMap"
|
||||
|
||||
export async function deleteKey(key: string, opts?: { fuzzy?: boolean }) {
|
||||
cacheLogger.debug("Deleting key", key)
|
||||
if (opts?.fuzzy) {
|
||||
cacheMap.forEach((_, k) => {
|
||||
if (k.includes(key)) {
|
||||
cacheMap.delete(k)
|
||||
}
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
cacheMap.delete(key)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
import { cacheLogger } from "@/services/dataCache/logger"
|
||||
|
||||
import { cacheMap } from "./cacheMap"
|
||||
|
||||
export async function get<T>(key: string): Promise<T | undefined> {
|
||||
const cached = cacheMap.get(key)
|
||||
if (!cached) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
if (cached.expiresAt < Date.now()) {
|
||||
cacheLogger.debug(`Expired for key '${key}'`)
|
||||
cacheMap.delete(key)
|
||||
return undefined
|
||||
}
|
||||
if (cached.data === undefined) {
|
||||
cacheLogger.debug(`Data is undefined for key '${key}'`)
|
||||
cacheMap.delete(key)
|
||||
return undefined
|
||||
}
|
||||
|
||||
return cached.data as T
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { cacheOrGet } from "./cacheOrGet"
|
||||
import { deleteKey } from "./deleteKey"
|
||||
import { get } from "./get"
|
||||
import { set } from "./set"
|
||||
|
||||
import type { DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
export async function createInMemoryCache(): Promise<DataCache> {
|
||||
return { type: "in-memory", cacheOrGet, deleteKey, get, set }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
type CacheTime,
|
||||
getCacheTimeInSeconds,
|
||||
} from "@/services/dataCache/Cache"
|
||||
|
||||
import { cacheMap } from "./cacheMap"
|
||||
|
||||
export async function set<T>(
|
||||
key: string,
|
||||
data: T,
|
||||
ttl: CacheTime
|
||||
): Promise<void> {
|
||||
cacheMap.set(key, {
|
||||
data: data,
|
||||
expiresAt: Date.now() + getCacheTimeInSeconds(ttl) * 1000,
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { unstable_cache } from "next/cache"
|
||||
|
||||
import {
|
||||
type CacheTime,
|
||||
type DataCache,
|
||||
getCacheTimeInSeconds,
|
||||
} from "@/services/dataCache/Cache"
|
||||
|
||||
import { cacheLogger } from "../../logger"
|
||||
|
||||
export const cacheOrGet: DataCache["cacheOrGet"] = async <T>(
|
||||
key: string | string[],
|
||||
callback: () => Promise<T>,
|
||||
ttl: CacheTime
|
||||
): Promise<T> => {
|
||||
if (!Array.isArray(key)) {
|
||||
key = [key]
|
||||
}
|
||||
|
||||
const perf = performance.now()
|
||||
|
||||
const res = await unstable_cache(callback, key, {
|
||||
revalidate: getCacheTimeInSeconds(ttl),
|
||||
tags: key,
|
||||
})()
|
||||
cacheLogger.debug(`'${key}' took ${(performance.now() - perf).toFixed(2)}ms`)
|
||||
|
||||
return res
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
import { revalidateTag } from "next/cache"
|
||||
|
||||
export async function deleteKey(key: string) {
|
||||
revalidateTag(key)
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import type { DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
/**
|
||||
* This function is not implemented for unstable_cache due to underlying cache implementation.
|
||||
* @see cacheOrGet
|
||||
* @param _key
|
||||
* @returns
|
||||
*/
|
||||
export const get: DataCache["get"] = async <T>(
|
||||
_key: string
|
||||
): Promise<T | undefined> => {
|
||||
console.warn("UnstableCache.get is not implemented, use cacheOrGet")
|
||||
return undefined
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { cacheOrGet } from "./cacheOrGet"
|
||||
import { deleteKey } from "./deleteKey"
|
||||
import { get } from "./get"
|
||||
import { set } from "./set"
|
||||
|
||||
import type { DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
export async function createUnstableCache(): Promise<DataCache> {
|
||||
return { type: "unstable-cache", cacheOrGet, deleteKey, get, set }
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
import { type CacheTime, type DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
/**
|
||||
* This function is not implemented for unstable_cache due to underlying cache implementation.
|
||||
* @see cacheOrGet
|
||||
* @param _key
|
||||
* @returns
|
||||
*/
|
||||
export const set: DataCache["set"] = async <T>(
|
||||
_key: string,
|
||||
_data: T,
|
||||
_ttl: CacheTime
|
||||
): Promise<void> => {
|
||||
console.warn("UnstableCache.set is not implemented, use cacheOrGet")
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { isEdge } from "@/utils/isEdge"
|
||||
|
||||
import { createInMemoryCache } from "./InMemoryCache"
|
||||
import { createUnstableCache } from "./UnstableCache"
|
||||
|
||||
import type { DataCache } from "@/services/dataCache/Cache"
|
||||
|
||||
export function createMemoryCache(): Promise<DataCache> {
|
||||
if (isEdge) {
|
||||
/**
|
||||
* unstable_cache is not available on the edge runtime
|
||||
*/
|
||||
return createInMemoryCache()
|
||||
}
|
||||
|
||||
return createUnstableCache()
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
import { env } from "@/env/server"
|
||||
|
||||
import { isEdge } from "@/utils/isEdge"
|
||||
|
||||
import { createMemoryCache } from "./MemoryCache/createMemoryCache"
|
||||
import { createDistributedCache } from "./DistributedCache"
|
||||
import { cacheLogger } from "./logger"
|
||||
|
||||
import type { DataCache } from "./Cache"
|
||||
|
||||
export type { CacheTime, DataCache } from "./Cache"
|
||||
|
||||
export async function getCacheClient(): Promise<DataCache> {
|
||||
if (global.cacheClient) {
|
||||
return global.cacheClient
|
||||
}
|
||||
|
||||
global.cacheClient = env.REDIS_API_HOST
|
||||
? createDistributedCache()
|
||||
: createMemoryCache()
|
||||
|
||||
const cacheClient = await global.cacheClient
|
||||
cacheLogger.debug(
|
||||
`Creating ${cacheClient.type} cache on ${isEdge ? "edge" : "server"} runtime`
|
||||
)
|
||||
|
||||
return global.cacheClient
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
export const cacheLogger = {
|
||||
async debug(message: string, ...args: unknown[]): Promise<void> {
|
||||
console.debug(`${await loggerPrefix()} ${message}`, ...args)
|
||||
},
|
||||
async warn(message: string, ...args: unknown[]): Promise<void> {
|
||||
console.warn(`${await loggerPrefix()} Warning - ${message}`, ...args)
|
||||
},
|
||||
async error(message: string, ...args: unknown[]): Promise<void> {
|
||||
console.error(`${await loggerPrefix()} Error - ${message}`, ...args)
|
||||
},
|
||||
}
|
||||
|
||||
async function loggerPrefix() {
|
||||
const instancePrefix = await getCachePrefix()
|
||||
return `[Cache] ${instancePrefix ?? ""}`.trim()
|
||||
}
|
||||
|
||||
async function getCachePrefix() {
|
||||
const cacheCreated = await isPromiseResolved(global.cacheClient)
|
||||
|
||||
if (!cacheCreated.resolved) {
|
||||
return null
|
||||
}
|
||||
|
||||
const instanceType = cacheCreated.value?.type
|
||||
if (!instanceType) {
|
||||
return null
|
||||
}
|
||||
|
||||
return `[${instanceType}]`
|
||||
}
|
||||
|
||||
function isPromiseResolved<T>(promise: Promise<T> | undefined) {
|
||||
if (!promise) {
|
||||
return { resolved: false, value: undefined }
|
||||
}
|
||||
|
||||
const check = Promise.race([promise, Promise.resolve("__PENDING__")])
|
||||
|
||||
return check.then((result) => ({
|
||||
resolved: result !== "__PENDING__",
|
||||
value: result !== "__PENDING__" ? (result as Awaited<T>) : undefined,
|
||||
}))
|
||||
}
|
||||
@@ -258,7 +258,8 @@ export function createDetailsStore(
|
||||
}
|
||||
|
||||
const stateTotalLocalPrice = state.totalPrice.local.price
|
||||
const stateTotalLocalRegularPrice = state.totalPrice.local.regularPrice
|
||||
const stateTotalLocalRegularPrice =
|
||||
state.totalPrice.local.regularPrice
|
||||
|
||||
const addToTotalPrice =
|
||||
(currentRoom.room.breakfast === undefined ||
|
||||
@@ -292,9 +293,9 @@ export function createDetailsStore(
|
||||
currency: breakfast.localPrice.currency,
|
||||
price: stateTotalLocalPrice + breakfastTotalPrice,
|
||||
regularPrice: stateTotalLocalRegularPrice
|
||||
? stateTotalLocalRegularPrice + breakfastTotalPrice
|
||||
: undefined,
|
||||
},
|
||||
? stateTotalLocalRegularPrice + breakfastTotalPrice
|
||||
: undefined,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -326,9 +327,9 @@ export function createDetailsStore(
|
||||
if (localPrice < 0) {
|
||||
localPrice = 0
|
||||
}
|
||||
let regularPrice = stateTotalLocalRegularPrice
|
||||
? stateTotalLocalRegularPrice - currentBreakfastTotalPrice
|
||||
: undefined
|
||||
let regularPrice = stateTotalLocalRegularPrice
|
||||
? stateTotalLocalRegularPrice - currentBreakfastTotalPrice
|
||||
: undefined
|
||||
|
||||
state.totalPrice = {
|
||||
requested: state.totalPrice.requested && {
|
||||
|
||||
@@ -274,18 +274,18 @@ export function createRatesStore({
|
||||
bookingRoom: room,
|
||||
rooms: selectedPackage
|
||||
? allRooms.filter((r) =>
|
||||
r.features.find((f) => f.code === selectedPackage)
|
||||
)
|
||||
r.features.find((f) => f.code === selectedPackage)
|
||||
)
|
||||
: allRooms,
|
||||
selectedPackage,
|
||||
selectedRate:
|
||||
selectedRate && product
|
||||
? {
|
||||
features: selectedRate.features,
|
||||
product,
|
||||
roomType: selectedRate.roomType,
|
||||
roomTypeCode: selectedRate.roomTypeCode,
|
||||
}
|
||||
features: selectedRate.features,
|
||||
product,
|
||||
roomType: selectedRate.roomType,
|
||||
roomTypeCode: selectedRate.roomTypeCode,
|
||||
}
|
||||
: null,
|
||||
}
|
||||
}),
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
import type { TextCols } from "@/types/trpc/routers/contentstack/blocks"
|
||||
|
||||
export interface TextColProps extends Pick<TextCols, "text_cols"> { }
|
||||
export interface TextColProps extends Pick<TextCols, "text_cols"> {}
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { BookingConfirmation } from "@/types/trpc/routers/booking/confirmat
|
||||
import type { linkedReservationSchema } from "@/server/routers/booking/output"
|
||||
|
||||
export interface LinkedReservationSchema
|
||||
extends z.output<typeof linkedReservationSchema> { }
|
||||
extends z.output<typeof linkedReservationSchema> {}
|
||||
|
||||
export interface BookingConfirmationRoomsProps
|
||||
extends Pick<BookingConfirmation, "booking"> {
|
||||
|
||||
Vendored
+5
@@ -0,0 +1,5 @@
|
||||
import type { DataCache } from "@/services/dataCache"
|
||||
|
||||
declare global {
|
||||
var cacheClient: Promise<DataCache> | undefined
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user