diff options
| author | Fuwn <[email protected]> | 2026-03-01 19:56:11 -0800 |
|---|---|---|
| committer | Fuwn <[email protected]> | 2026-03-01 19:56:18 -0800 |
| commit | 469dfeeac3acffe10b0f7cf374fb7f0d11ecd90f (patch) | |
| tree | bd7e5963a2eec25088f0135cedb68104bd4fae59 /src/lib/Media/Anime/Airing/Subtitled | |
| parent | fix(ci): stabilize quality graphql generation and trigger workflow paths (diff) | |
| download | due.moe-469dfeeac3acffe10b0f7cf374fb7f0d11ecd90f.tar.xz due.moe-469dfeeac3acffe10b0f7cf374fb7f0d11ecd90f.zip | |
fix(match): prevent cached airing injection mutation regressions
Diffstat (limited to 'src/lib/Media/Anime/Airing/Subtitled')
| -rw-r--r-- | src/lib/Media/Anime/Airing/Subtitled/match.ts | 53 |
1 files changed, 37 insertions, 16 deletions
diff --git a/src/lib/Media/Anime/Airing/Subtitled/match.ts b/src/lib/Media/Anime/Airing/Subtitled/match.ts index d3950948..0c91f3ac 100644 --- a/src/lib/Media/Anime/Airing/Subtitled/match.ts +++ b/src/lib/Media/Anime/Airing/Subtitled/match.ts @@ -171,6 +171,17 @@ const scheduleIndexCache = new WeakMap<SubsPlease, ScheduleIndex>(); const closestMatchCache = new Map<string, Time | null>(); const injectAiringTimeCache = new Map<string, Media>(); +const hashString = (input: string): string => { + let hash = 2166136261; + + for (let index = 0; index < input.length; index += 1) { + hash ^= input.charCodeAt(index); + hash = Math.imul(hash, 16777619); + } + + return (hash >>> 0).toString(36); +}; + const setBoundedCacheValue = <T>( cache: Map<string, T>, key: string, @@ -188,6 +199,12 @@ const animeTitleFingerprint = (anime: Media) => .map(preprocessTitle) .join("|"); +const localTimeZone = () => + Intl.DateTimeFormat().resolvedOptions().timeZone || "local"; + +const airingDayOf = (airingAt: number | undefined) => + new Date((airingAt || 0) * 1000).toLocaleString("en-US", { weekday: "long" }); + const fallbackClosestMatch = ( dayIndex: DayScheduleIndex, searchTitles: string[], @@ -230,14 +247,8 @@ const buildScheduleIndex = (subsPlease: SubsPlease): ScheduleIndex => { for (const [day, value] of Object.entries(subsPlease.schedule)) { const flattenedValue = Array.isArray(value) ? value.flat() : []; - const firstEntry = flattenedValue[0]; - const lastEntry = flattenedValue[flattenedValue.length - 1]; - versionParts.push( - `${day}:${flattenedValue.length}:${firstEntry?.title || ""}:${firstEntry?.time || ""}:${ - lastEntry?.title || "" - }:${lastEntry?.time || ""}`, - ); + versionParts.push(day); const dayIndex: DayScheduleIndex = { entries: [], @@ -251,6 +262,9 @@ const buildScheduleIndex = (subsPlease: SubsPlease): ScheduleIndex => { time: scheduleTime.time, day, }; + + versionParts.push(`${day}\u001f${time.title}\u001f${time.time}`); + const normalizedTitle = preprocessTitle(time.title); const tokens = normalizedTitle.split(" ").filter(isMeaningfulToken); const entryIndex = dayIndex.entries.length; @@ -271,7 +285,7 @@ const buildScheduleIndex = (subsPlease: SubsPlease): ScheduleIndex => { return { byDay, - version: `${subsPlease.tz}:${versionParts.join("|")}`, + version: hashString(`${subsPlease.tz}\u001e${versionParts.join("\u001d")}`), }; }; @@ -290,16 +304,13 @@ export const findClosestMatch = ( return null; } + const airingDay = airingDayOf(anime.nextAiringEpisode?.airingAt); const cacheKey = `${anime.id}:${anime.nextAiringEpisode?.airingAt || 0}:${animeTitleFingerprint( anime, - )}:${scheduleIndex.version}`; + )}:${airingDay}:${localTimeZone()}:${scheduleIndex.version}`; const cached = closestMatchCache.get(cacheKey); if (cached !== undefined) return cached; - - const airingDay = new Date( - (anime.nextAiringEpisode?.airingAt || 0) * 1000, - ).toLocaleString("en-US", { weekday: "long" }); const dayIndex = scheduleIndex.byDay.get(airingDay); if (!dayIndex || dayIndex.entries.length === 0) { @@ -526,6 +537,14 @@ const buildInjectAiringTimeCacheKey = ( animeTitleFingerprint(anime), ].join(":"); +const cloneInjectedMedia = (media: Media): Media => + ({ + ...media, + nextAiringEpisode: media.nextAiringEpisode + ? { ...media.nextAiringEpisode } + : undefined, + }) as Media; + export const injectAiringTime = ( anime: Media, subsPlease: SubsPlease | null, @@ -543,7 +562,7 @@ export const injectAiringTime = ( ); const cached = injectAiringTimeCache.get(cacheKey); - if (cached) return cached; + if (cached) return cloneInjectedMedia(cached); const airingAt = anime.nextAiringEpisode?.airingAt; const now = new Date(); @@ -604,12 +623,14 @@ export const injectAiringTime = ( }, } as Media; + const cachedValue = cloneInjectedMedia(injected); + setBoundedCacheValue( injectAiringTimeCache, cacheKey, - injected, + cachedValue, MAX_INJECT_CACHE_ENTRIES, ); - return injected; + return cloneInjectedMedia(cachedValue); }; |