Skip to content

Commit bca39c0

Browse files
committed
fix(server): preserve local-only status refreshes when sharing work
1 parent f240b2e commit bca39c0

2 files changed

Lines changed: 49 additions & 9 deletions

File tree

‎apps/server/src/vcs/VcsStatusBroadcaster.test.ts‎

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ function layerTestFor(state: {
7878
remoteStatusRefreshUpstreamValues?: Array<boolean | undefined>;
7979
backgroundWorkEnabled?: boolean;
8080
automaticFetchInterval?: Effect.Effect<Duration.Duration>;
81-
beforeRemoteStatus?: Effect.Effect<void>;
81+
beforeRemoteStatus?: Effect.Effect<void, GitManagerError>;
8282
}) {
8383
return VcsStatusBroadcaster.layer.pipe(
8484
Layer.provideMerge(NodeServices.layer),
@@ -754,4 +754,48 @@ describe("VcsStatusBroadcaster", () => {
754754
assert.equal(state.remoteStatusCalls, 3);
755755
}).pipe(Effect.provide(layerTestFor(state)));
756756
});
757+
it.effect("a failed refresh releases interrupted waiters and runs the shared follow-up", () => {
758+
const state: Parameters<typeof layerTestFor>[0] = {
759+
currentLocalStatus: baseLocalStatus,
760+
currentRemoteStatus: remoteStatusWithPr,
761+
localStatusCalls: 0,
762+
remoteStatusCalls: 0,
763+
localInvalidationCalls: 0,
764+
remoteInvalidationCalls: 0,
765+
};
766+
767+
return Effect.gen(function* () {
768+
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
769+
const started = yield* Deferred.make<void>();
770+
const release = yield* Deferred.make<void>();
771+
state.beforeRemoteStatus = Deferred.succeed(started, undefined).pipe(
772+
Effect.andThen(Deferred.await(release)),
773+
Effect.andThen(
774+
Effect.fail(
775+
new GitManagerError({
776+
operation: "VcsStatusBroadcaster.test",
777+
cwd: "/repo",
778+
detail: "remote status failed",
779+
}),
780+
),
781+
),
782+
);
783+
const first = yield* broadcaster.refreshStatus("/repo").pipe(Effect.exit, Effect.forkScoped);
784+
yield* Deferred.await(started);
785+
const interrupted = yield* broadcaster
786+
.refreshStatus("/repo")
787+
.pipe(Effect.forkScoped({ startImmediately: true }));
788+
const remaining = yield* broadcaster
789+
.refreshStatus("/repo")
790+
.pipe(Effect.forkScoped({ startImmediately: true }));
791+
yield* Fiber.interrupt(interrupted);
792+
state.beforeRemoteStatus = Effect.void;
793+
yield* Deferred.succeed(release, undefined);
794+
assert.isTrue(Exit.isFailure(yield* Fiber.join(first)));
795+
assert.deepStrictEqual((yield* Fiber.join(remaining)).pr, remoteStatusWithPr.pr);
796+
assert.equal(state.remoteInvalidationCalls, 2);
797+
yield* broadcaster.refreshStatus("/repo");
798+
assert.equal(state.remoteInvalidationCalls, 3);
799+
}).pipe(Effect.provide(layerTestFor(state)));
800+
});
757801
});

‎apps/server/src/vcs/VcsStatusBroadcaster.ts‎

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -773,13 +773,8 @@ export const make = Effect.gen(function* () {
773773
}),
774774
);
775775

776-
// A caller that arrives while a refresh for its cwd runs cannot reuse that
777-
// refresh: it may have read Git before the caller's change, such as a PR an
778-
// agent just opened. Those callers share one follow-up refresh instead of
779-
// each queueing another full status read and PR lookup behind the remote
780-
// lock. Keyed by the raw cwd so each refresh keeps its caller's auto-pull
781-
// policy input.
782-
// No entry means idle; `null` means a refresh runs with no follow-up queued.
776+
// Requests during a refresh need a later read; share one follow-up per raw cwd.
777+
// Raw paths preserve auto-pull policy. Missing means idle; null means running without a follow-up.
783778
type SharedRefresh = Deferred.Deferred<VcsStatusResult, GitManagerServiceError>;
784779
const queuedRefreshesRef = yield* Ref.make(new Map<string, SharedRefresh | null>());
785780

@@ -805,7 +800,8 @@ export const make = Effect.gen(function* () {
805800

806801
const shareQueuedRefreshStatus: VcsStatusBroadcaster["Service"]["refreshStatus"] = Effect.fn(
807802
"VcsStatusBroadcaster.shareQueuedRefreshStatus",
808-
)(function* (rawCwd) {
803+
)(function* (rawCwd, options) {
804+
if (options?.localOnly) return yield* refreshStatus(rawCwd, options);
809805
const requested = yield* Deferred.make<VcsStatusResult, GitManagerServiceError>();
810806
// Registering and forking together keeps an interrupt from leaving an entry with no worker.
811807
const shared = yield* Effect.uninterruptible(

0 commit comments

Comments
 (0)