1
0
mirror of synced 2025-12-19 09:57:42 -05:00

perf: optimize journey path resolver and middleware (#58955)

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Kevin Heis
2025-12-17 14:34:23 -08:00
committed by GitHub
parent e31ddba6e5
commit f22fea3519
2 changed files with 74 additions and 26 deletions

View File

@@ -68,6 +68,13 @@ type ContentContext = {
// Cache for journey pages so we only filter all pages once
let cachedJourneyPages: JourneyPage[] | null = null
// Cache for guide paths to quickly check if a page is part of any journey
let cachedGuidePaths: Set<string> | null = null
let hasDynamicGuides = false
function needsRendering(str: string): boolean {
return str.includes('{{') || str.includes('{%') || str.includes('[') || str.includes('<')
}
function getJourneyPages(pages: Pages): JourneyPage[] {
if (!cachedJourneyPages) {
@@ -78,6 +85,27 @@ function getJourneyPages(pages: Pages): JourneyPage[] {
return cachedJourneyPages
}
function getGuidePaths(pages: Pages): Set<string> {
if (!cachedGuidePaths) {
cachedGuidePaths = new Set()
const journeyPages = getJourneyPages(pages)
for (const page of journeyPages) {
if (!page.journeyTracks) continue
for (const track of page.journeyTracks) {
if (!track.guides) continue
for (const guide of track.guides) {
if (needsRendering(guide.href)) {
hasDynamicGuides = true
} else {
cachedGuidePaths.add(normalizeGuidePath(guide.href))
}
}
}
}
}
return cachedGuidePaths
}
function normalizeGuidePath(path: string): string {
// First ensure we have a leading slash for consistent processing
const pathWithSlash = path.startsWith('/') ? path : `/${path}`
@@ -133,6 +161,16 @@ export async function resolveJourneyContext(
): Promise<JourneyContext | null> {
const normalizedPath = normalizeGuidePath(articlePath)
// Optimization: Fast path check
// If we are not forcing a specific journey page, check our global cache
if (!currentJourneyPage) {
const guidePaths = getGuidePaths(pages)
// If we have no dynamic guides and this path isn't in our known guides, return null early.
if (!hasDynamicGuides && !guidePaths.has(normalizedPath)) {
return null
}
}
// Use the current journey page if provided, otherwise find all journey pages
const journeyPages = currentJourneyPage ? [currentJourneyPage] : getJourneyPages(pages)
@@ -165,15 +203,17 @@ export async function resolveJourneyContext(
let renderedGuidePath = guidePath
// Handle Liquid conditionals in guide paths
try {
renderedGuidePath = await executeWithFallback(
context,
() => renderContent(guidePath, context, { textOnly: true }),
() => guidePath,
)
} catch {
// If rendering fails, use the original path rather than erroring
renderedGuidePath = guidePath
if (needsRendering(guidePath)) {
try {
renderedGuidePath = await executeWithFallback(
context,
() => renderContent(guidePath, context, { textOnly: true }),
() => guidePath,
)
} catch {
// If rendering fails, use the original path rather than erroring
renderedGuidePath = guidePath
}
}
const normalizedGuidePath = normalizeGuidePath(renderedGuidePath)
@@ -189,15 +229,17 @@ export async function resolveJourneyContext(
let renderedAlternativeNextStep = alternativeNextStep
// Handle Liquid conditionals in branching text which likely has links
try {
renderedAlternativeNextStep = await executeWithFallback(
context,
() => renderContent(alternativeNextStep, context),
() => alternativeNextStep,
)
} catch {
// If rendering fails, use the original branching text rather than erroring
renderedAlternativeNextStep = alternativeNextStep
if (needsRendering(alternativeNextStep)) {
try {
renderedAlternativeNextStep = await executeWithFallback(
context,
() => renderContent(alternativeNextStep, context),
() => alternativeNextStep,
)
} catch {
// If rendering fails, use the original branching text rather than erroring
renderedAlternativeNextStep = alternativeNextStep
}
}
result = {
@@ -278,10 +320,14 @@ export async function resolveJourneyTracks(
const result = await Promise.all(
journeyTracks.map(async (track) => {
// Render Liquid templates in title and description
const renderedTitle = await renderContent(track.title, context, { textOnly: true })
const renderedDescription = track.description
? await renderContent(track.description, context, { textOnly: true })
: undefined
const renderedTitle = needsRendering(track.title)
? await renderContent(track.title, context, { textOnly: true })
: track.title
const renderedDescription =
track.description && needsRendering(track.description)
? await renderContent(track.description, context, { textOnly: true })
: track.description
const guides = await Promise.all(
track.guides.map(async (guide: { href: string; alternativeNextStep?: string }) => {

View File

@@ -1,20 +1,22 @@
import type { Response, NextFunction } from 'express'
import type { ExtendedRequest, Context } from '@/types'
import { resolveJourneyTracks, resolveJourneyContext } from '../lib/journey-path-resolver'
export default async function journeyTrack(
req: ExtendedRequest & { context: Context },
res: Response,
next: NextFunction,
) {
if (req.method !== 'GET' && req.method !== 'HEAD') return next()
if (!req.context) throw new Error('request is not contextualized')
if (!req.context.page) return next()
try {
const journeyResolver = await import('../lib/journey-path-resolver')
// If this page has journey tracks defined, resolve them for the landing page
if ((req.context.page as any).journeyTracks) {
const resolvedTracks = await journeyResolver.resolveJourneyTracks(
const resolvedTracks = await resolveJourneyTracks(
(req.context.page as any).journeyTracks,
req.context,
)
@@ -24,7 +26,7 @@ export default async function journeyTrack(
}
// Always try to resolve journey context (for navigation on guide articles)
const journeyContext = await journeyResolver.resolveJourneyContext(
const journeyContext = await resolveJourneyContext(
req.pagePath || '',
req.context.pages || {},
req.context,