| author | |
| committer | |
| log | 33242d0164d47c6935ba5c7725296539384de65d |
| tree | 99f5d68ae0d84de2d3cbba4244302438c2c6a0d9 |
| parent | de37964db26bd3260d253f138bfde2ac60fe254e |
| signature | Signed by PGP key B5690EEEBB952194 |
5 files changed, 275 insertions(+), 28 deletions(-)
apps/server/src/pullRequest/PullRequestReadCache.test.ts created+78| ... | ... | @@ -0,0 +1,78 @@ |
| 1 | import { assert, it } from "@effect/vitest"; | |
| 2 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 3 | import { PullRequestOperationError } from "@t3tools/contracts"; | |
| 4 | import * as Deferred from "effect/Deferred"; | |
| 5 | import * as Effect from "effect/Effect"; | |
| 6 | import * as Fiber from "effect/Fiber"; | |
| 7 | import * as FileSystem from "effect/FileSystem"; | |
| 8 | import * as Layer from "effect/Layer"; | |
| 9 | import * as TestClock from "effect/testing/TestClock"; | |
| 10 | import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"; | |
| 11 | import * as Persistence from "effect/unstable/persistence/Persistence"; | |
| 12 | import * as PullRequestReadCache from "./PullRequestReadCache.ts"; | |
| 13 | ||
| 14 | const cacheLayer = (directory: string) => | |
| 15 | PullRequestReadCache.make.pipe( | |
| 16 | Effect.provide( | |
| 17 | Persistence.layerKvs.pipe(Layer.provideMerge(KeyValueStore.layerFileSystem(directory))), | |
| 18 | ), | |
| 19 | ); | |
| 20 | ||
| 21 | it.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 @@ |
| 1 | import * as Cache from "effect/Cache"; | |
| 2 | import * as Clock from "effect/Clock"; | |
| 3 | import * as Equal from "effect/Equal"; | |
| 4 | import * as Hash from "effect/Hash"; | |
| 5 | import { PullRequestOperationError, PullRequestUnavailableError } from "@t3tools/contracts"; | |
| 6 | import * as Crypto from "effect/Crypto"; | |
| 7 | import * as Encoding from "effect/Encoding"; | |
| 8 | import * as Option from "effect/Option"; | |
| 9 | import * as Context from "effect/Context"; | |
| 10 | import * as Duration from "effect/Duration"; | |
| 11 | import * as Effect from "effect/Effect"; | |
| 12 | import * as Exit from "effect/Exit"; | |
| 13 | import * as Layer from "effect/Layer"; | |
| 14 | import * as Path from "effect/Path"; | |
| 15 | import * as Schema from "effect/Schema"; | |
| 16 | import * as Semaphore from "effect/Semaphore"; | |
| 17 | import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"; | |
| 18 | import * as Persistable from "effect/unstable/persistence/Persistable"; | |
| 19 | import * as PersistedCache from "effect/unstable/persistence/PersistedCache"; | |
| 20 | import * as Persistence from "effect/unstable/persistence/Persistence"; | |
| 21 | import { ServerConfig } from "../config.ts"; | |
| 22 | ||
| 23 | const CONCURRENT_READS = 512; | |
| 24 | type ReadError = PullRequestOperationError | PullRequestUnavailableError; | |
| 25 | ||
| 26 | class 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 | ||
| 41 | export 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 | ||
| 52 | export 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 | ||
| 106 | export 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 | import * as NodeServices from "@effect/platform-node/NodeServices"; | |
| 2 | import * as KeyValueStore from "effect/unstable/persistence/KeyValueStore"; | |
| 3 | import * as Persistence from "effect/unstable/persistence/Persistence"; | |
| 1 | 4 | import { assert, it } from "@effect/vitest"; |
| 2 | 5 | import * as Deferred from "effect/Deferred"; |
| 3 | 6 | import * as Effect from "effect/Effect"; |
| ... | ... | @@ -24,6 +27,7 @@ import { |
| 24 | 27 | } from "./PullRequestProvider.ts"; |
| 25 | 28 | import { PullRequestProviderRegistry, fromProviders } from "./PullRequestProviderRegistry.ts"; |
| 26 | 29 | import * as PullRequestService from "./PullRequestService.ts"; |
| 30 | import * as PullRequestReadCache from "./PullRequestReadCache.ts"; | |
| 27 | 31 | |
| 28 | 32 | function project(input: { |
| 29 | 33 | readonly id: string; |
| ... | ... | @@ -198,6 +202,11 @@ function makeService(input: { |
| 198 | 202 | }), |
| 199 | 203 | }), |
| 200 | 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 | 11 | import * as Exit from "effect/Exit"; |
| 12 | 12 | import * as Layer from "effect/Layer"; |
| 13 | 13 | import * as PubSub from "effect/PubSub"; |
| 14 | import * as Option from "effect/Option"; | |
| 14 | 15 | import * as Schema from "effect/Schema"; |
| 15 | 16 | import type * as Scope from "effect/Scope"; |
| 16 | 17 | import * as Stream from "effect/Stream"; |
| ... | ... | @@ -50,8 +51,8 @@ import { |
| 50 | 51 | type PullRequestLabelCandidateList, |
| 51 | 52 | type PullRequestLabelChangeInput, |
| 52 | 53 | type PullRequestSubmitReviewInput, |
| 53 | type PullRequestStack, | |
| 54 | type PullRequestSummary, | |
| 54 | PullRequestStack, | |
| 55 | PullRequestSummary, | |
| 55 | 56 | type PullRequestThreadReplyInput, |
| 56 | 57 | type PullRequestThreadResolutionInput, |
| 57 | 58 | type PullRequestThreadCommentsInput, |
| ... | ... | @@ -71,6 +72,7 @@ import { |
| 71 | 72 | type PullRequestProviderApi, |
| 72 | 73 | PullRequestProviderError, |
| 73 | 74 | } from "./PullRequestProvider.ts"; |
| 75 | import * as PullRequestReadCache from "./PullRequestReadCache.ts"; | |
| 74 | 76 | import { PullRequestProviderRegistry } from "./PullRequestProviderRegistry.ts"; |
| 75 | 77 | |
| 76 | 78 | export interface PullRequestMergeEvent extends PullRequestRef { |
| ... | ... | @@ -114,7 +116,6 @@ const REPOSITORY_SEARCH_CHUNK = 100; |
| 114 | 116 | * `invalidate` rather than a flag on the read, so an ordinary read can never opt out. |
| 115 | 117 | */ |
| 116 | 118 | const LIST_CACHE_TTL = Duration.seconds(30); |
| 117 | const SUMMARY_CACHE_TTL = Duration.seconds(60); | |
| 118 | 119 | const DETAIL_CACHE_TTL = Duration.seconds(15); |
| 119 | 120 | const DIFF_CACHE_TTL = Duration.seconds(60); |
| 120 | 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 | 536 | const projections = yield* ProjectionSnapshotQuery.ProjectionSnapshotQuery; |
| 536 | 537 | const sourceControlProviders = yield* SourceControlProviderRegistry.SourceControlProviderRegistry; |
| 537 | 538 | const rateLimits = yield* SourceControlRateLimit.SourceControlRateLimit; |
| 539 | const readCache = yield* PullRequestReadCache.PullRequestReadCache; | |
| 538 | 540 | |
| 539 | 541 | const refineUnknownProjectKinds = ( |
| 540 | 542 | projects: ReadonlyArray<OrchestrationProjectShell>, |
| ... | ... | @@ -2333,18 +2335,49 @@ export const make = Effect.gen(function* () { |
| 2333 | 2335 | }; |
| 2334 | 2336 | }; |
| 2335 | 2337 | |
| 2336 | const summaryCache = yield* Cache.makeWith( | |
| 2337 | (key: string) => { | |
| 2338 | return summaryUncached(refOfCacheKey(key)); | |
| 2339 | }, | |
| 2340 | { | |
| 2341 | capacity: DETAIL_CACHE_CAPACITY, | |
| 2342 | timeToLive: (exit) => (Exit.isSuccess(exit) ? SUMMARY_CACHE_TTL : Duration.zero), | |
| 2343 | }, | |
| 2344 | ); | |
| 2338 | const persistedRead = Effect.fn("PullRequestService.persistedRead")(function* <A>( | |
| 2339 | input: PullRequestRef, | |
| 2340 | operation: string, | |
| 2341 | codec: Schema.Codec<A, string>, | |
| 2342 | read: Effect.Effect<A, PullRequestError>, | |
| 2343 | ) { | |
| 2344 | const project = yield* requireProject(input); | |
| 2345 | const key = [ | |
| 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 | 2378 | const summary: PullRequestService["Service"]["summary"] = (input, options) => { |
| 2346 | 2379 | const key = refCacheKey(input); |
| 2347 | const cached = Cache.get(summaryCache, key); | |
| 2380 | const cached = persistedRead(input, "summary", summaryCodec, summaryUncached(input)); | |
| 2348 | 2381 | const held = lastGoodSummary.peek(key); |
| 2349 | 2382 | return held !== undefined && |
| 2350 | 2383 | (options?.recoverTransientFailure !== false || held.state === "merged") |
| ... | ... | @@ -2356,18 +2389,13 @@ export const make = Effect.gen(function* () { |
| 2356 | 2389 | ); |
| 2357 | 2390 | }; |
| 2358 | 2391 | |
| 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 | 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 | ); | |
| 2371 | 2399 | |
| 2372 | 2400 | // Keys serialize positionally and parse back in the lookup, so the cache is the only holder |
| 2373 | 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 | 2670 | const invalidate: PullRequestService["Service"]["invalidate"] = (input) => { |
| 2643 | 2671 | const reference = input.reference; |
| 2644 | 2672 | if (reference !== undefined) { |
| 2645 | return Effect.sync(() => bumpRefEpoch(reference)); | |
| 2673 | return readCache.invalidate.pipe(Effect.andThen(Effect.sync(() => bumpRefEpoch(reference)))); | |
| 2646 | 2674 | } |
| 2647 | 2675 | return Effect.sync(() => { |
| 2648 | 2676 | listingsEpoch = ++epochCounter; |
| ... | ... | @@ -2652,7 +2680,9 @@ export const make = Effect.gen(function* () { |
| 2652 | 2680 | |
| 2653 | 2681 | const refreshAfterTurn: PullRequestService["Service"]["refreshAfterTurn"] = Effect.suspend(() => { |
| 2654 | 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 | }); |
| 2657 | 2687 | |
| 2658 | 2688 | // 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 | 2693 | method: (input: I) => Effect.Effect<void, PullRequestError>, |
| 2664 | 2694 | ): ((input: I) => Effect.Effect<void, PullRequestError>) => |
| 2665 | 2695 | (input) => |
| 2666 | method(input).pipe( | |
| 2696 | readCache.invalidate.pipe( | |
| 2697 | Effect.andThen(method(input)), | |
| 2698 | Effect.ensuring(readCache.invalidate), | |
| 2667 | 2699 | Effect.tap(() => |
| 2668 | 2700 | Effect.sync(() => { |
| 2669 | 2701 | bumpRefEpoch(input); |
| ... | ... | @@ -2674,7 +2706,8 @@ export const make = Effect.gen(function* () { |
| 2674 | 2706 | const runActionAndInvalidate: PullRequestService["Service"]["runAction"] = Effect.fn( |
| 2675 | 2707 | "PullRequestService.runActionAndInvalidate", |
| 2676 | 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 | 2711 | bumpRefEpoch({ ...input, repository }); |
| 2679 | 2712 | listingsEpoch = ++epochCounter; |
| 2680 | 2713 | if (input.action === "merge") { |
apps/server/src/server.ts+2| ... | ... | @@ -93,6 +93,7 @@ import * as VcsStatusBroadcaster from "./vcs/VcsStatusBroadcaster.ts"; |
| 93 | 93 | import * as GitWorkflowService from "./git/GitWorkflowService.ts"; |
| 94 | 94 | import * as ReviewService from "./review/ReviewService.ts"; |
| 95 | 95 | import * as SourceControlProviderRegistry from "./sourceControl/SourceControlProviderRegistry.ts"; |
| 96 | import * as PullRequestReadCache from "./pullRequest/PullRequestReadCache.ts"; | |
| 96 | 97 | import * as SourceControlRateLimit from "./sourceControl/SourceControlRateLimit.ts"; |
| 97 | 98 | import * as SourceControlRepositoryService from "./sourceControl/SourceControlRepositoryService.ts"; |
| 98 | 99 | import * as ProjectSetupScriptRunner from "./project/ProjectSetupScriptRunner.ts"; |
| ... | ... | @@ -318,6 +319,7 @@ const SourceControlProviderRegistryLayerLive = SourceControlProviderRegistry.lay |
| 318 | 319 | |
| 319 | 320 | const PullRequestServiceLive = PullRequestService.layer.pipe( |
| 320 | 321 | Layer.provide(PullRequestProviderRegistry.layer), |
| 322 | Layer.provide(PullRequestReadCache.layer), | |
| 321 | 323 | Layer.provide(SourceControlProviderRegistryLayerLive), |
| 322 | 324 | Layer.provide(SourceControlRateLimit.layer), |
| 323 | 325 | ); |