| ... | ... | @@ -102,16 +102,7 @@ const DIFF_CACHE_TTL = Duration.seconds(60); |
| 102 | 102 | const COMMIT_DIFF_CACHE_TTL = Duration.minutes(10); |
| 103 | 103 | /** Sized like the client's own stale time; a row's counts move only when somebody pushes. */ |
| 104 | 104 | const LIST_STATS_CACHE_TTL = Duration.seconds(60); |
| 105 | | /** |
| 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); |
| 105 | /** A diff can stay interactive while its next cached value is fetched off the critical path. */ |
| 115 | 106 | const DIFF_STALE_WINDOW = Duration.minutes(10); |
| 116 | 107 | /** How long one host's signed-in login is believed without asking its CLI again. */ |
| 117 | 108 | const VIEWER_CACHE_TTL = Duration.minutes(10); |
| ... | ... | @@ -1826,30 +1817,22 @@ export const make = Effect.gen(function* () { |
| 1826 | 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 |
| 1830 | | * process for a CLI whose wall clock is the host's — seconds on a good day, tens of them on a |
| 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. |
| 1820 | * The diff is not live-polled and is expensive enough to keep its stale-while-revalidate path. |
| 1821 | * Explicit refreshes and mutations still strand held values through the reference epoch. |
| 1839 | 1822 | */ |
| 1840 | | const staleWhileRevalidate = <A>(staleFor: Duration.Duration, capacity: number) => { |
| 1841 | | const staleMs = Duration.toMillis(staleFor); |
| 1842 | | const held = new Map<string, { readonly at: number; readonly value: A }>(); |
| 1843 | | const record = (key: string, value: A) => |
| 1823 | const staleDiff = (() => { |
| 1824 | const staleMs = Duration.toMillis(DIFF_STALE_WINDOW); |
| 1825 | const held = new Map<string, { readonly at: number; readonly value: PullRequestDiffResult }>(); |
| 1826 | const record = (key: string, value: PullRequestDiffResult) => |
| 1844 | 1827 | Effect.map(Clock.currentTimeMillis, (at) => { |
| 1845 | 1828 | held.delete(key); |
| 1846 | | if (held.size >= capacity) { |
| 1829 | if (held.size >= DIFF_CACHE_CAPACITY) { |
| 1847 | 1830 | const oldest = held.keys().next().value; |
| 1848 | 1831 | if (oldest !== undefined) held.delete(oldest); |
| 1849 | 1832 | } |
| 1850 | 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 | 1836 | const recorded = read.pipe(Effect.tap((value) => record(key, value))); |
| 1854 | 1837 | return Effect.flatMap(Clock.currentTimeMillis, (now) => { |
| 1855 | 1838 | const snapshot = held.get(key); |
| ... | ... | @@ -1860,7 +1843,7 @@ export const make = Effect.gen(function* () { |
| 1860 | 1843 | return Effect.sync(() => runFork(Effect.ignore(recorded))).pipe(Effect.as(snapshot.value)); |
| 1861 | 1844 | }); |
| 1862 | 1845 | }; |
| 1863 | | }; |
| 1846 | })(); |
| 1864 | 1847 | |
| 1865 | 1848 | // Epochs are the invalidation mechanism: a key carries its scope's epoch, so bumping the |
| 1866 | 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 | 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 | 1932 | const list: PullRequestService["Service"]["list"] = (input) => { |
| 1954 | 1933 | const key = JSON.stringify([ |
| 1955 | 1934 | listingsEpoch, |
| ... | ... | @@ -1976,7 +1955,7 @@ export const make = Effect.gen(function* () { |
| 1976 | 1955 | ? null |
| 1977 | 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 | 1961 | const detailCache = yield* Cache.makeWith( |
| ... | ... | @@ -1989,13 +1968,9 @@ export const make = Effect.gen(function* () { |
| 1989 | 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 | 1971 | const detail: PullRequestService["Service"]["detail"] = (input) => { |
| 1997 | 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 | 1976 | const activityCache = yield* Cache.makeWith( |
| ... | ... | @@ -2008,13 +1983,9 @@ export const make = Effect.gen(function* () { |
| 2008 | 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 | 1986 | const activity: PullRequestService["Service"]["activity"] = (input) => { |
| 2016 | 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 | 1991 | const diffCache = yield* Cache.makeWith( |
| ... | ... | @@ -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 | 2018 | const diff: PullRequestService["Service"]["diff"] = (input) => { |
| 2052 | 2019 | const key = JSON.stringify([ |
| 2053 | 2020 | refEpoch(input), |
| ... | ... | @@ -2076,10 +2043,6 @@ export const make = Effect.gen(function* () { |
| 2076 | 2043 | // shares between clients like every other read. Refs are sorted so one page's worth of rows |
| 2077 | 2044 | // is one key however the client assembled them, and the listings epoch rides along so the |
| 2078 | 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 | 2046 | const listStats: PullRequestService["Service"]["listStats"] = (input) => { |
| 2084 | 2047 | if (input.refs.length === 0) return Effect.succeed({ stats: [] }); |
| 2085 | 2048 | const key = JSON.stringify([ |
| ... | ... | @@ -2090,7 +2053,7 @@ export const make = Effect.gen(function* () { |
| 2090 | 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 | 2059 | const invalidate: PullRequestService["Service"]["invalidate"] = (input) => |