authorgravatar for julius0216@outlook.comJulius Marminge <julius0216@outlook.com> 2026-09-09 16:33:25-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-09 16:33:25-07:00
log33242d0164d47c6935ba5c7725296539384de65d
tree99f5d68ae0d84de2d3cbba4244302438c2c6a0d9
parentde37964db26bd3260d253f138bfde2ac60fe254e
signature Signed by PGP key B5690EEEBB952194

fix(server): preserve recent PR reads across server restarts (#11007)


5 files changed, 275 insertions(+), 28 deletions(-)

apps/server/src/pullRequest/PullRequestReadCache.test.ts created+78
...@@ -0,0 +1,78 @@
1import { assert, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import { PullRequestOperationError } from "@t3tools/contracts";
4import * as Deferred from "effect/Deferred";
5import * as Effect from "effect/Effect";
6import * as Fiber from "effect/Fiber";
7import * as FileSystem from "effect/FileSystem";
8import * as Layer from "effect/Layer";
9import * as TestClock from "effect/testing/TestClock";
10import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore";
11import * as Persistence from "effect/unstable/persistence/Persistence";
12import * as PullRequestReadCache from "./PullRequestReadCache.ts";
13
14const cacheLayer = (directory: string) =>
15 PullRequestReadCache.make.pipe(
16 Effect.provide(
17 Persistence.layerKvs.pipe(Layer.provideMerge(KeyValueStore.layerFileSystem(directory))),
18 ),
19 );
20
21it.layer(NodeServices.layer)("PR filesystem cache", (it) => {
22 it.effect("reuses files after restart and respects the original expiry", () =>
23 Effect.gen(function* () {
24 const fs = yield* FileSystem.FileSystem;
25 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pr-cache-" });
26 let reads = 0;
27 const lookup = Effect.sync(() => String(++reads));
28 const first = yield* cacheLayer(directory);
29 const key = "long/repository/key".repeat(100);
30 assert.strictEqual(yield* first.get(key, lookup), "1");
31 yield* TestClock.adjust("59 seconds");
32 const restarted = yield* cacheLayer(directory);
33 assert.strictEqual(yield* restarted.get(key, lookup), "1");
34 yield* TestClock.adjust("1 second");
35 assert.strictEqual(yield* restarted.get(key, lookup), "2");
36 assert.strictEqual(reads, 2);
37 assert.strictEqual((yield* fs.readDirectory(directory)).length, 1);
38 }),
39 );
40
41 it.effect("clears in-flight reads before a new service can reuse them", () =>
42 Effect.gen(function* () {
43 const fs = yield* FileSystem.FileSystem;
44 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pr-cache-" });
45 const started = yield* Deferred.make<void>();
46 const release = yield* Deferred.make<void>();
47 const cache = yield* cacheLayer(directory);
48 const read = yield* cache
49 .get(
50 "summary",
51 Deferred.succeed(started, undefined).pipe(
52 Effect.andThen(Deferred.await(release)),
53 Effect.as("old"),
54 ),
55 )
56 .pipe(Effect.forkChild);
57 yield* Deferred.await(started);
58 const invalidate = yield* cache.invalidate.pipe(Effect.forkChild({ startImmediately: true }));
59 yield* Deferred.succeed(release, undefined);
60 yield* Fiber.join(read);
61 yield* Fiber.join(invalidate);
62 const restarted = yield* cacheLayer(directory);
63 assert.strictEqual(yield* restarted.get("summary", Effect.succeed("new")), "new");
64 }),
65 );
66
67 it.effect("does not persist failed GitHub reads", () =>
68 Effect.gen(function* () {
69 const fs = yield* FileSystem.FileSystem;
70 const directory = yield* fs.makeTempDirectoryScoped({ prefix: "t3-pr-cache-" });
71 const cache = yield* cacheLayer(directory);
72 const error = new PullRequestOperationError({ operation: "summary", detail: "unavailable" });
73 yield* cache.get("summary", Effect.fail(error)).pipe(Effect.flip);
74 const restarted = yield* cacheLayer(directory);
75 assert.strictEqual(yield* restarted.get("summary", Effect.succeed("recovered")), "recovered");
76 }),
77 );
78});
apps/server/src/pullRequest/PullRequestReadCache.ts created+125
...@@ -0,0 +1,125 @@
1import * as Cache from "effect/Cache";
2import * as Clock from "effect/Clock";
3import * as Equal from "effect/Equal";
4import * as Hash from "effect/Hash";
5import { PullRequestOperationError, PullRequestUnavailableError } from "@t3tools/contracts";
6import * as Crypto from "effect/Crypto";
7import * as Encoding from "effect/Encoding";
8import * as Option from "effect/Option";
9import * as Context from "effect/Context";
10import * as Duration from "effect/Duration";
11import * as Effect from "effect/Effect";
12import * as Exit from "effect/Exit";
13import * as Layer from "effect/Layer";
14import * as Path from "effect/Path";
15import * as Schema from "effect/Schema";
16import * as Semaphore from "effect/Semaphore";
17import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore";
18import * as Persistable from "effect/unstable/persistence/Persistable";
19import * as PersistedCache from "effect/unstable/persistence/PersistedCache";
20import * as Persistence from "effect/unstable/persistence/Persistence";
21import { ServerConfig } from "../config.ts";
22
23const CONCURRENT_READS = 512;
24type ReadError = PullRequestOperationError | PullRequestUnavailableError;
25
26class Read extends Persistable.Class<{
27 payload: { key: string; lookup: Effect.Effect<string, ReadError> };
28}>()("PullRequestRead", {
29 primaryKey: ({ key }) => key,
30 success: Schema.Struct({ payload: Schema.String, expiresAt: Schema.Finite }),
31 error: Schema.Union([PullRequestOperationError, PullRequestUnavailableError]),
32}) {
33 [Equal.symbol](that: unknown): boolean {
34 return that instanceof Read && that.key === this.key;
35 }
36 [Hash.symbol](): number {
37 return Hash.string(this.key);
38 }
39}
40
41export class PullRequestReadCache extends Context.Service<
42 PullRequestReadCache,
43 {
44 readonly get: (
45 key: string,
46 lookup: Effect.Effect<string, ReadError>,
47 ) => Effect.Effect<string, ReadError>;
48 readonly invalidate: Effect.Effect<void>;
49 }
50>()("t3/pullRequest/PullRequestReadCache") {}
51
52export const make = Effect.gen(function* () {
53 const backing = yield* KeyValueStore.KeyValueStore;
54 const crypto = yield* Crypto.Crypto;
55 const clock = yield* Clock.Clock;
56 let enabled = true;
57 const lock = yield* Semaphore.make(CONCURRENT_READS);
58 const timeToLive: Persistable.TimeToLiveFn<Read> = (exit) =>
59 Exit.isSuccess(exit)
60 ? Duration.millis(Math.max(0, exit.value.expiresAt - clock.currentTimeMillisUnsafe()))
61 : Duration.zero;
62 const cache = yield* PersistedCache.make(
63 (request: Read) =>
64 request.lookup.pipe(
65 Effect.map((payload) => ({ payload, expiresAt: clock.currentTimeMillisUnsafe() + 60_000 })),
66 ),
67 {
68 storeId: "pr-v2",
69 timeToLive,
70 inMemoryTTL: timeToLive,
71 inMemoryCapacity: CONCURRENT_READS,
72 },
73 );
74 return PullRequestReadCache.of({
75 get: Effect.fn("PullRequestReadCache.get")(function* (key, lookup) {
76 if (!enabled) return yield* lookup;
77 const digest = yield* crypto
78 .digest("SHA-256", new TextEncoder().encode(key))
79 .pipe(Effect.option);
80 if (Option.isNone(digest)) return yield* lookup;
81 const read = yield* Effect.cached(lookup);
82 return yield* cache
83 .get(new Read({ key: Encoding.encodeHex(digest.value), lookup: read }))
84 .pipe(
85 Effect.map((result) => result.payload),
86 Effect.catchTags({
87 PersistenceError: () => read,
88 SchemaError: () => read,
89 }),
90 Effect.uninterruptible,
91 lock.withPermits(1),
92 );
93 }),
94 // Let existing reads finish before clearing, so they cannot repopulate stale entries.
95 invalidate: Cache.invalidateAll(cache.inMemory).pipe(
96 Effect.andThen(backing.clear),
97 Effect.catch(() => {
98 enabled = false;
99 return Effect.logWarning("PR cache disabled after clearing failed");
100 }),
101 lock.withPermits(CONCURRENT_READS),
102 ),
103 });
104});
105
106export const layer = Layer.unwrap(
107 Effect.gen(function* () {
108 const config = yield* ServerConfig;
109 const path = yield* Path.Path;
110 return Layer.effect(PullRequestReadCache, make).pipe(
111 Layer.provide(Persistence.layerKvs),
112 Layer.provide(
113 KeyValueStore.layerFileSystem(
114 path.join(config.providerStatusCacheDir, "pull-requests"),
115 ).pipe(
116 Layer.catch(() =>
117 Layer.effectDiscard(
118 Effect.logWarning("PR cache directory unavailable; using memory cache"),
119 ).pipe(Layer.provideMerge(KeyValueStore.layerMemory)),
120 ),
121 ),
122 ),
123 );
124 }),
125);
apps/server/src/pullRequest/PullRequestService.test.ts+9
...@@ -1,3 +1,6 @@...@@ -1,3 +1,6 @@
1import * as NodeServices from "@effect/platform-node/NodeServices";
2import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore";
3import * as Persistence from "effect/unstable/persistence/Persistence";
1import { assert, it } from "@effect/vitest";4import { assert, it } from "@effect/vitest";
2import * as Deferred from "effect/Deferred";5import * as Deferred from "effect/Deferred";
3import * as Effect from "effect/Effect";6import * as Effect from "effect/Effect";
...@@ -24,6 +27,7 @@ import {...@@ -24,6 +27,7 @@ import {
24} from "./PullRequestProvider.ts";27} from "./PullRequestProvider.ts";
25import { PullRequestProviderRegistry, fromProviders } from "./PullRequestProviderRegistry.ts";28import { PullRequestProviderRegistry, fromProviders } from "./PullRequestProviderRegistry.ts";
26import * as PullRequestService from "./PullRequestService.ts";29import * as PullRequestService from "./PullRequestService.ts";
30import * as PullRequestReadCache from "./PullRequestReadCache.ts";
2731
28function project(input: {32function project(input: {
29 readonly id: string;33 readonly id: string;
...@@ -198,6 +202,11 @@ function makeService(input: {...@@ -198,6 +202,11 @@ function makeService(input: {
198 }),202 }),
199 }),203 }),
200 SourceControlRateLimit.layer,204 SourceControlRateLimit.layer,
205 Layer.effect(PullRequestReadCache.PullRequestReadCache, PullRequestReadCache.make).pipe(
206 Layer.provide(Persistence.layerKvs),
207 Layer.provide(KeyValueStore.layerMemory),
208 Layer.provide(NodeServices.layer),
209 ),
201 ),210 ),
202 ),211 ),
203 );212 );
apps/server/src/pullRequest/PullRequestService.ts+61-28
...@@ -11,6 +11,7 @@ import * as Effect from "effect/Effect";...@@ -11,6 +11,7 @@ import * as Effect from "effect/Effect";
11import * as Exit from "effect/Exit";11import * as Exit from "effect/Exit";
12import * as Layer from "effect/Layer";12import * as Layer from "effect/Layer";
13import * as PubSub from "effect/PubSub";13import * as PubSub from "effect/PubSub";
14import * as Option from "effect/Option";
14import * as Schema from "effect/Schema";15import * as Schema from "effect/Schema";
15import type * as Scope from "effect/Scope";16import type * as Scope from "effect/Scope";
16import * as Stream from "effect/Stream";17import * as Stream from "effect/Stream";
...@@ -50,8 +51,8 @@ import {...@@ -50,8 +51,8 @@ import {
50 type PullRequestLabelCandidateList,51 type PullRequestLabelCandidateList,
51 type PullRequestLabelChangeInput,52 type PullRequestLabelChangeInput,
52 type PullRequestSubmitReviewInput,53 type PullRequestSubmitReviewInput,
53 type PullRequestStack,54 PullRequestStack,
54 type PullRequestSummary,55 PullRequestSummary,
55 type PullRequestThreadReplyInput,56 type PullRequestThreadReplyInput,
56 type PullRequestThreadResolutionInput,57 type PullRequestThreadResolutionInput,
57 type PullRequestThreadCommentsInput,58 type PullRequestThreadCommentsInput,
...@@ -71,6 +72,7 @@ import {...@@ -71,6 +72,7 @@ import {
71 type PullRequestProviderApi,72 type PullRequestProviderApi,
72 PullRequestProviderError,73 PullRequestProviderError,
73} from "./PullRequestProvider.ts";74} from "./PullRequestProvider.ts";
75import * as PullRequestReadCache from "./PullRequestReadCache.ts";
74import { PullRequestProviderRegistry } from "./PullRequestProviderRegistry.ts";76import { PullRequestProviderRegistry } from "./PullRequestProviderRegistry.ts";
7577
76export interface PullRequestMergeEvent extends PullRequestRef {78export interface PullRequestMergeEvent extends PullRequestRef {
...@@ -114,7 +116,6 @@ const REPOSITORY_SEARCH_CHUNK = 100;...@@ -114,7 +116,6 @@ const REPOSITORY_SEARCH_CHUNK = 100;
114 * `invalidate` rather than a flag on the read, so an ordinary read can never opt out.116 * `invalidate` rather than a flag on the read, so an ordinary read can never opt out.
115 */117 */
116const LIST_CACHE_TTL = Duration.seconds(30);118const LIST_CACHE_TTL = Duration.seconds(30);
117const SUMMARY_CACHE_TTL = Duration.seconds(60);
118const DETAIL_CACHE_TTL = Duration.seconds(15);119const DETAIL_CACHE_TTL = Duration.seconds(15);
119const DIFF_CACHE_TTL = Duration.seconds(60);120const DIFF_CACHE_TTL = Duration.seconds(60);
120/** A commit is content-addressed, so its own diff cannot change under its key. */121/** A commit is content-addressed, so its own diff cannot change under its key. */
...@@ -535,6 +536,7 @@ export const make = Effect.gen(function* () {...@@ -535,6 +536,7 @@ export const make = Effect.gen(function* () {
535 const projections = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;536 const projections = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery;
536 const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;537 const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry;
537 const rateLimits = yield* SourceControlRateLimit.SourceControlRateLimit;538 const rateLimits = yield* SourceControlRateLimit.SourceControlRateLimit;
539 const readCache = yield* PullRequestReadCache.PullRequestReadCache;
538540
539 const refineUnknownProjectKinds = (541 const refineUnknownProjectKinds = (
540 projects: ReadonlyArray<OrchestrationProjectShell>,542 projects: ReadonlyArray<OrchestrationProjectShell>,
...@@ -2333,18 +2335,49 @@ export const make = Effect.gen(function* () {...@@ -2333,18 +2335,49 @@ export const make = Effect.gen(function* () {
2333 };2335 };
2334 };2336 };
23352337
2336 const summaryCache = yield* Cache.makeWith(2338 const persistedRead = Effect.fn("PullRequestService.persistedRead")(function* <A>(
2337 (key: string) => {2339 input: PullRequestRef,
2338 return summaryUncached(refOfCacheKey(key));2340 operation: string,
2339 },2341 codec: Schema.Codec<A, string>,
2340 {2342 read: Effect.Effect<A, PullRequestError>,
2341 capacity: DETAIL_CACHE_CAPACITY,2343 ) {
2342 timeToLive: (exit) => (Exit.isSuccess(exit) ? SUMMARY_CACHE_TTL : Duration.zero),2344 const project = yield* requireProject(input);
2343 },2345 const key = [
2344 );2346 operation,
2347 project.api.kind,
2348 project.host.toLowerCase(),
2349 project.repository.toLowerCase(),
2350 project.project.id,
2351 project.project.workspaceRoot,
2352 String(input.number),
2353 ]
2354 .map(encodeURIComponent)
2355 .join(":");
2356 const lookup = yield* Effect.cached(read);
2357 const encodedRead = lookup.pipe(
2358 Effect.flatMap((value) =>
2359 Schema.encodeEffect(codec)(value).pipe(
2360 Effect.mapError(
2361 (cause) =>
2362 new PullRequestOperationError({
2363 operation: "cache",
2364 detail: "Could not encode PR cache data.",
2365 cause,
2366 }),
2367 ),
2368 ),
2369 ),
2370 );
2371 const payload = yield* readCache.get(key, encodedRead);
2372 const decoded = yield* Schema.decodeUnknownEffect(codec)(payload).pipe(Effect.option);
2373 return Option.isSome(decoded) ? decoded.value : yield* lookup;
2374 });
2375 const summaryCodec = Schema.fromJsonString(PullRequestSummary);
2376 const stackCodec = Schema.fromJsonString(Schema.NullOr(PullRequestStack));
2377
2345 const summary: PullRequestService["Service"]["summary"] = (input, options) => {2378 const summary: PullRequestService["Service"]["summary"] = (input, options) => {
2346 const key = refCacheKey(input);2379 const key = refCacheKey(input);
2347 const cached = Cache.get(summaryCache, key);2380 const cached = persistedRead(input, "summary", summaryCodec, summaryUncached(input));
2348 const held = lastGoodSummary.peek(key);2381 const held = lastGoodSummary.peek(key);
2349 return held !== undefined &&2382 return held !== undefined &&
2350 (options?.recoverTransientFailure !== false || held.state === "merged")2383 (options?.recoverTransientFailure !== false || held.state === "merged")
...@@ -2356,18 +2389,13 @@ export const make = Effect.gen(function* () {...@@ -2356,18 +2389,13 @@ export const make = Effect.gen(function* () {
2356 );2389 );
2357 };2390 };
23582391
2359 const stackCache = yield* Cache.makeWith(
2360 (key: string) => {
2361 const [referenceKey, includeDetails] = JSON.parse(key) as [string, boolean];
2362 return stackUncached(refOfCacheKey(referenceKey), { includeDetails });
2363 },
2364 {
2365 capacity: DETAIL_CACHE_CAPACITY,
2366 timeToLive: (exit) => (Exit.isSuccess(exit) ? SUMMARY_CACHE_TTL : Duration.zero),
2367 },
2368 );
2369 const stack: PullRequestService["Service"]["stack"] = (input, options) =>2392 const stack: PullRequestService["Service"]["stack"] = (input, options) =>
2370 Cache.get(stackCache, JSON.stringify([refCacheKey(input), options?.includeDetails !== false]));2393 persistedRead(
2394 input,
2395 `stack:${options?.includeDetails !== false}`,
2396 stackCodec,
2397 stackUncached(input, options),
2398 );
23712399
2372 // Keys serialize positionally and parse back in the lookup, so the cache is the only holder2400 // Keys serialize positionally and parse back in the lookup, so the cache is the only holder
2373 // of in-flight state: concurrent identical reads coalesce on the key into one host request.2401 // of in-flight state: concurrent identical reads coalesce on the key into one host request.
...@@ -2642,7 +2670,7 @@ export const make = Effect.gen(function* () {...@@ -2642,7 +2670,7 @@ export const make = Effect.gen(function* () {
2642 const invalidate: PullRequestService["Service"]["invalidate"] = (input) => {2670 const invalidate: PullRequestService["Service"]["invalidate"] = (input) => {
2643 const reference = input.reference;2671 const reference = input.reference;
2644 if (reference !== undefined) {2672 if (reference !== undefined) {
2645 return Effect.sync(() => bumpRefEpoch(reference));2673 return readCache.invalidate.pipe(Effect.andThen(Effect.sync(() => bumpRefEpoch(reference))));
2646 }2674 }
2647 return Effect.sync(() => {2675 return Effect.sync(() => {
2648 listingsEpoch = ++epochCounter;2676 listingsEpoch = ++epochCounter;
...@@ -2652,7 +2680,9 @@ export const make = Effect.gen(function* () {...@@ -2652,7 +2680,9 @@ export const make = Effect.gen(function* () {
26522680
2653 const refreshAfterTurn: PullRequestService["Service"]["refreshAfterTurn"] = Effect.suspend(() => {2681 const refreshAfterTurn: PullRequestService["Service"]["refreshAfterTurn"] = Effect.suspend(() => {
2654 turnRefreshEpoch = listingsEpoch = ++epochCounter;2682 turnRefreshEpoch = listingsEpoch = ++epochCounter;
2655 return SubscriptionRef.set(pullRequestRefreshes, turnRefreshEpoch);2683 return readCache.invalidate.pipe(
2684 Effect.andThen(SubscriptionRef.set(pullRequestRefreshes, turnRefreshEpoch)),
2685 );
2656 });2686 });
26572687
2658 // A mutation's own client re-reads right after it, and every other client's next read must2688 // A mutation's own client re-reads right after it, and every other client's next read must
...@@ -2663,7 +2693,9 @@ export const make = Effect.gen(function* () {...@@ -2663,7 +2693,9 @@ export const make = Effect.gen(function* () {
2663 method: (input: I) => Effect.Effect<void, PullRequestError>,2693 method: (input: I) => Effect.Effect<void, PullRequestError>,
2664 ): ((input: I) => Effect.Effect<void, PullRequestError>) =>2694 ): ((input: I) => Effect.Effect<void, PullRequestError>) =>
2665 (input) =>2695 (input) =>
2666 method(input).pipe(2696 readCache.invalidate.pipe(
2697 Effect.andThen(method(input)),
2698 Effect.ensuring(readCache.invalidate),
2667 Effect.tap(() =>2699 Effect.tap(() =>
2668 Effect.sync(() => {2700 Effect.sync(() => {
2669 bumpRefEpoch(input);2701 bumpRefEpoch(input);
...@@ -2674,7 +2706,8 @@ export const make = Effect.gen(function* () {...@@ -2674,7 +2706,8 @@ export const make = Effect.gen(function* () {
2674 const runActionAndInvalidate: PullRequestService["Service"]["runAction"] = Effect.fn(2706 const runActionAndInvalidate: PullRequestService["Service"]["runAction"] = Effect.fn(
2675 "PullRequestService.runActionAndInvalidate",2707 "PullRequestService.runActionAndInvalidate",
2676 )(function* (input) {2708 )(function* (input) {
2677 const repository = yield* runAction(input);2709 yield* readCache.invalidate;
2710 const repository = yield* runAction(input).pipe(Effect.ensuring(readCache.invalidate));
2678 bumpRefEpoch({ ...input, repository });2711 bumpRefEpoch({ ...input, repository });
2679 listingsEpoch = ++epochCounter;2712 listingsEpoch = ++epochCounter;
2680 if (input.action === "merge") {2713 if (input.action === "merge") {
apps/server/src/server.ts+2
...@@ -93,6 +93,7 @@ import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts";...@@ -93,6 +93,7 @@ import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts";
93import * as GitWorkflowService from "./git/GitWorkflowService.ts";93import * as GitWorkflowService from "./git/GitWorkflowService.ts";
94import * as ReviewService from "./review/ReviewService.ts";94import * as ReviewService from "./review/ReviewService.ts";
95import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts";95import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts";
96import * as PullRequestReadCache from "./pullRequest/PullRequestReadCache.ts";
96import * as SourceControlRateLimit from "./sourceControl/SourceControlRateLimit.ts";97import * as SourceControlRateLimit from "./sourceControl/SourceControlRateLimit.ts";
97import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";98import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts";
98import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts";99import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts";
...@@ -318,6 +319,7 @@ const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.lay...@@ -318,6 +319,7 @@ const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.lay
318319
319const PullRequestServiceLive = PullRequestService.layer.pipe(320const PullRequestServiceLive = PullRequestService.layer.pipe(
320 Layer.provide(PullRequestProviderRegistry.layer),321 Layer.provide(PullRequestProviderRegistry.layer),
322 Layer.provide(PullRequestReadCache.layer),
321 Layer.provide(SourceControlProviderRegistryLayerLive),323 Layer.provide(SourceControlProviderRegistryLayerLive),
322 Layer.provide(SourceControlRateLimit.layer),324 Layer.provide(SourceControlRateLimit.layer),
323);325);