| ... | @@ -102,16 +102,7 @@ const DIFF_CACHE_TTL = Duration.seconds(60); | ... | @@ -102,16 +102,7 @@ const DIFF_CACHE_TTL = Duration.seconds(60); |
| 102 | const COMMIT_DIFF_CACHE_TTL = Duration.minutes(10); | 102 | const COMMIT_DIFF_CACHE_TTL = Duration.minutes(10); |
| 103 | /** Sized like the client's own stale time; a row's counts move only when somebody pushes. */ | 103 | /** Sized like the client's own stale time; a row's counts move only when somebody pushes. */ |
| 104 | const LIST_STATS_CACHE_TTL = Duration.seconds(60); | 104 | const LIST_STATS_CACHE_TTL = Duration.seconds(60); |
| 105 | /** | 105 | /** A diff can stay interactive while its next cached value is fetched off the critical path. */ |
| 106 | * How long a cache's last success may still be served while a fresh read runs behind it. | | |
| 107 | * Bounded by how the page actually revalidates: clients re-read on mount and once a minute | | |
| 108 | * while open, and every one of those reads repopulates the cache in the background — so in | | |
| 109 | * steady use a "stale" answer is at most a refresh cycle old, and the window only stretches | | |
| 110 | * that far when nobody has looked at the page for minutes. An explicit refresh or a mutation | | |
| 111 | * bumps the epochs and skips held answers entirely. | | |
| 112 | */ | | |
| 113 | const LIST_STALE_WINDOW = Duration.minutes(10); | | |
| 114 | const DETAIL_STALE_WINDOW = Duration.minutes(5); | | |
| 115 | const DIFF_STALE_WINDOW = Duration.minutes(10); | 106 | const DIFF_STALE_WINDOW = Duration.minutes(10); |
| 116 | /** How long one host's signed-in login is believed without asking its CLI again. */ | 107 | /** How long one host's signed-in login is believed without asking its CLI again. */ |
| 117 | const VIEWER_CACHE_TTL = Duration.minutes(10); | 108 | const VIEWER_CACHE_TTL = Duration.minutes(10); |
| ... | @@ -1826,30 +1817,22 @@ export const make = Effect.gen(function* () { | ... | @@ -1826,30 +1817,22 @@ export const make = Effect.gen(function* () { |
| 1826 | const runFork = Effect.runForkWith(context); | 1817 | const runFork = Effect.runForkWith(context); |
| 1827 | | 1818 | |
| 1828 | /** | 1819 | /** |
| 1829 | * Stale answers served while a fresh one is fetched behind them. Every read here leaves the | 1820 | * The diff is not live-polled and is expensive enough to keep its stale-while-revalidate path. |
| 1830 | * process for a CLI whose wall clock is the host's — seconds on a good day, tens of them on a | 1821 | * Explicit refreshes and mutations still strand held values through the reference epoch. |
| 1831 | * slow network — and the short cache windows below mean almost every page visit pays that | | |
| 1832 | * clock again. The last success per key is therefore held a while longer: a read inside the | | |
| 1833 | * window answers with it at once and refreshes the cache in the background, so the next read | | |
| 1834 | * is fresh without anyone having waited on it. | | |
| 1835 | * | | |
| 1836 | * Correctness leans on the epochs: an explicit refresh or a mutation bumps them, the epoch is | | |
| 1837 | * part of every key, and a held answer under the old key is simply never asked for again — so | | |
| 1838 | * "give me truly fresh" still means exactly that. | | |
| 1839 | */ | 1822 | */ |
| 1840 | const staleWhileRevalidate = <A>(staleFor: Duration.Duration, capacity: number) => { | 1823 | const staleDiff = (() => { |
| 1841 | const staleMs = Duration.toMillis(staleFor); | 1824 | const staleMs = Duration.toMillis(DIFF_STALE_WINDOW); |
| 1842 | const held = new Map<string, { readonly at: number; readonly value: A }>(); | 1825 | const held = new Map<string, { readonly at: number; readonly value: PullRequestDiffResult }>(); |
| 1843 | const record = (key: string, value: A) => | 1826 | const record = (key: string, value: PullRequestDiffResult) => |
| 1844 | Effect.map(Clock.currentTimeMillis, (at) => { | 1827 | Effect.map(Clock.currentTimeMillis, (at) => { |
| 1845 | held.delete(key); | 1828 | held.delete(key); |
| 1846 | if (held.size >= capacity) { | 1829 | if (held.size >= DIFF_CACHE_CAPACITY) { |
| 1847 | const oldest = held.keys().next().value; | 1830 | const oldest = held.keys().next().value; |
| 1848 | if (oldest !== undefined) held.delete(oldest); | 1831 | if (oldest !== undefined) held.delete(oldest); |
| 1849 | } | 1832 | } |
| 1850 | held.set(key, { at, value }); | 1833 | held.set(key, { at, value }); |
| 1851 | }); | 1834 | }); |
| 1852 | return <E>(key: string, read: Effect.Effect<A, E>): Effect.Effect<A, E> => { | 1835 | return <E>(key: string, read: Effect.Effect<PullRequestDiffResult, E>) => { |
| 1853 | const recorded = read.pipe(Effect.tap((value) => record(key, value))); | 1836 | const recorded = read.pipe(Effect.tap((value) => record(key, value))); |
| 1854 | return Effect.flatMap(Clock.currentTimeMillis, (now) => { | 1837 | return Effect.flatMap(Clock.currentTimeMillis, (now) => { |
| 1855 | const snapshot = held.get(key); | 1838 | const snapshot = held.get(key); |
| ... | @@ -1860,7 +1843,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1860,7 +1843,7 @@ export const make = Effect.gen(function* () { |
| 1860 | return Effect.sync(() => runFork(Effect.ignore(recorded))).pipe(Effect.as(snapshot.value)); | 1843 | return Effect.sync(() => runFork(Effect.ignore(recorded))).pipe(Effect.as(snapshot.value)); |
| 1861 | }); | 1844 | }); |
| 1862 | }; | 1845 | }; |
| 1863 | }; | 1846 | })(); |
| 1864 | | 1847 | |
| 1865 | // Epochs are the invalidation mechanism: a key carries its scope's epoch, so bumping the | 1848 | // Epochs are the invalidation mechanism: a key carries its scope's epoch, so bumping the |
| 1866 | // epoch strands every entry made under the old one — no enumerating a cache whose keys | 1849 | // epoch strands every entry made under the old one — no enumerating a cache whose keys |
| ... | @@ -1946,10 +1929,6 @@ export const make = Effect.gen(function* () { | ... | @@ -1946,10 +1929,6 @@ export const make = Effect.gen(function* () { |
| 1946 | timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_CACHE_TTL : Duration.zero), | 1929 | timeToLive: (exit) => (Exit.isSuccess(exit) ? LIST_CACHE_TTL : Duration.zero), |
| 1947 | }, | 1930 | }, |
| 1948 | ); | 1931 | ); |
| 1949 | const staleList = staleWhileRevalidate<PullRequestListResult>( | | |
| 1950 | LIST_STALE_WINDOW, | | |
| 1951 | LIST_CACHE_CAPACITY, | | |
| 1952 | ); | | |
| 1953 | const list: PullRequestService["Service"]["list"] = (input) => { | 1932 | const list: PullRequestService["Service"]["list"] = (input) => { |
| 1954 | const key = JSON.stringify([ | 1933 | const key = JSON.stringify([ |
| 1955 | listingsEpoch, | 1934 | listingsEpoch, |
| ... | @@ -1976,7 +1955,7 @@ export const make = Effect.gen(function* () { | ... | @@ -1976,7 +1955,7 @@ export const make = Effect.gen(function* () { |
| 1976 | ? null | 1955 | ? null |
| 1977 | : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right)), | 1956 | : Object.entries(input.cursors).toSorted(([left], [right]) => left.localeCompare(right)), |
| 1978 | ]); | 1957 | ]); |
| 1979 | return staleList(key, Cache.get(listCache, key)); | 1958 | return Cache.get(listCache, key); |
| 1980 | }; | 1959 | }; |
| 1981 | | 1960 | |
| 1982 | const detailCache = yield* Cache.makeWith( | 1961 | const detailCache = yield* Cache.makeWith( |
| ... | @@ -1989,13 +1968,9 @@ export const make = Effect.gen(function* () { | ... | @@ -1989,13 +1968,9 @@ export const make = Effect.gen(function* () { |
| 1989 | timeToLive: (exit) => (Exit.isSuccess(exit) ? DETAIL_CACHE_TTL : Duration.zero), | 1968 | timeToLive: (exit) => (Exit.isSuccess(exit) ? DETAIL_CACHE_TTL : Duration.zero), |
| 1990 | }, | 1969 | }, |
| 1991 | ); | 1970 | ); |
| 1992 | const staleDetail = staleWhileRevalidate<PullRequestDetail>( | | |
| 1993 | DETAIL_STALE_WINDOW, | | |
| 1994 | DETAIL_CACHE_CAPACITY, | | |
| 1995 | ); | | |
| 1996 | const detail: PullRequestService["Service"]["detail"] = (input) => { | 1971 | const detail: PullRequestService["Service"]["detail"] = (input) => { |
| 1997 | const key = JSON.stringify([refEpoch(input), input.projectId, input.repository, input.number]); | 1972 | const key = JSON.stringify([refEpoch(input), input.projectId, input.repository, input.number]); |
| 1998 | return staleDetail(key, Cache.get(detailCache, key)); | 1973 | return Cache.get(detailCache, key); |
| 1999 | }; | 1974 | }; |
| 2000 | | 1975 | |
| 2001 | const activityCache = yield* Cache.makeWith( | 1976 | const activityCache = yield* Cache.makeWith( |
| ... | @@ -2008,13 +1983,9 @@ export const make = Effect.gen(function* () { | ... | @@ -2008,13 +1983,9 @@ export const make = Effect.gen(function* () { |
| 2008 | timeToLive: (exit) => (Exit.isSuccess(exit) ? DETAIL_CACHE_TTL : Duration.zero), | 1983 | timeToLive: (exit) => (Exit.isSuccess(exit) ? DETAIL_CACHE_TTL : Duration.zero), |
| 2009 | }, | 1984 | }, |
| 2010 | ); | 1985 | ); |
| 2011 | const staleActivity = staleWhileRevalidate<PullRequestActivity>( | | |
| 2012 | DETAIL_STALE_WINDOW, | | |
| 2013 | DETAIL_CACHE_CAPACITY, | | |
| 2014 | ); | | |
| 2015 | const activity: PullRequestService["Service"]["activity"] = (input) => { | 1986 | const activity: PullRequestService["Service"]["activity"] = (input) => { |
| 2016 | const key = JSON.stringify([refEpoch(input), input.projectId, input.repository, input.number]); | 1987 | const key = JSON.stringify([refEpoch(input), input.projectId, input.repository, input.number]); |
| 2017 | return staleActivity(key, Cache.get(activityCache, key)); | 1988 | return Cache.get(activityCache, key); |
| 2018 | }; | 1989 | }; |
| 2019 | | 1990 | |
| 2020 | const diffCache = yield* Cache.makeWith( | 1991 | const diffCache = yield* Cache.makeWith( |
| ... | @@ -2044,10 +2015,6 @@ export const make = Effect.gen(function* () { | ... | @@ -2044,10 +2015,6 @@ export const make = Effect.gen(function* () { |
| 2044 | }, | 2015 | }, |
| 2045 | }, | 2016 | }, |
| 2046 | ); | 2017 | ); |
| 2047 | const staleDiff = staleWhileRevalidate<PullRequestDiffResult>( | | |
| 2048 | DIFF_STALE_WINDOW, | | |
| 2049 | DIFF_CACHE_CAPACITY, | | |
| 2050 | ); | | |
| 2051 | const diff: PullRequestService["Service"]["diff"] = (input) => { | 2018 | const diff: PullRequestService["Service"]["diff"] = (input) => { |
| 2052 | const key = JSON.stringify([ | 2019 | const key = JSON.stringify([ |
| 2053 | refEpoch(input), | 2020 | refEpoch(input), |
| ... | @@ -2076,10 +2043,6 @@ export const make = Effect.gen(function* () { | ... | @@ -2076,10 +2043,6 @@ export const make = Effect.gen(function* () { |
| 2076 | // shares between clients like every other read. Refs are sorted so one page's worth of rows | 2043 | // shares between clients like every other read. Refs are sorted so one page's worth of rows |
| 2077 | // is one key however the client assembled them, and the listings epoch rides along so the | 2044 | // is one key however the client assembled them, and the listings epoch rides along so the |
| 2078 | // refresh that forgets the listing forgets its decorations with it. | 2045 | // refresh that forgets the listing forgets its decorations with it. |
| 2079 | const staleListStats = staleWhileRevalidate<PullRequestListStatsResult>( | | |
| 2080 | LIST_STALE_WINDOW, | | |
| 2081 | LIST_STATS_CACHE_CAPACITY, | | |
| 2082 | ); | | |
| 2083 | const listStats: PullRequestService["Service"]["listStats"] = (input) => { | 2046 | const listStats: PullRequestService["Service"]["listStats"] = (input) => { |
| 2084 | if (input.refs.length === 0) return Effect.succeed({ stats: [] }); | 2047 | if (input.refs.length === 0) return Effect.succeed({ stats: [] }); |
| 2085 | const key = JSON.stringify([ | 2048 | const key = JSON.stringify([ |
| ... | @@ -2090,7 +2053,7 @@ export const make = Effect.gen(function* () { | ... | @@ -2090,7 +2053,7 @@ export const make = Effect.gen(function* () { |
| 2090 | `${left[0]} ${left[1]} ${left[2]}`.localeCompare(`${right[0]} ${right[1]} ${right[2]}`), | 2053 | `${left[0]} ${left[1]} ${left[2]}`.localeCompare(`${right[0]} ${right[1]} ${right[2]}`), |
| 2091 | ), | 2054 | ), |
| 2092 | ]); | 2055 | ]); |
| 2093 | return staleListStats(key, Cache.get(listStatsCache, key)); | 2056 | return Cache.get(listStatsCache, key); |
| 2094 | }; | 2057 | }; |
| 2095 | | 2058 | |
| 2096 | const invalidate: PullRequestService["Service"]["invalidate"] = (input) => | 2059 | const invalidate: PullRequestService["Service"]["invalidate"] = (input) => |