Skip to content

Commit 8760d06

Browse files
authored
fix(server): refresh Changes totals after remote polls (#28600)
1 parent 8b4de61 commit 8760d06

2 files changed

Lines changed: 77 additions & 29 deletions

File tree

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

Lines changed: 74 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -656,9 +656,12 @@ describe("VcsStatusBroadcaster", () => {
656656
},
657657
);
658658

659-
it.effect("an explicit refresh reads local totals after the fetch", () => {
659+
it.effect("a remote poll refreshes Changes totals when divergence counts stay the same", () => {
660660
const state: Parameters<typeof layerTestFor>[0] = {
661-
currentLocalStatus: baseLocalStatus,
661+
currentLocalStatus: {
662+
...baseLocalStatus,
663+
branchChanges: { baseRef: "origin/main", insertions: 745, deletions: 24 },
664+
},
662665
currentRemoteStatus: baseRemoteStatus,
663666
localStatusCalls: 0,
664667
remoteStatusCalls: 0,
@@ -668,24 +671,79 @@ describe("VcsStatusBroadcaster", () => {
668671

669672
return Effect.gen(function* () {
670673
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
671-
const fetchStarted = yield* Deferred.make<void>();
672-
const finishFetch = yield* Deferred.make<void>();
673-
state.beforeRemoteStatus = Deferred.succeed(fetchStarted, undefined).pipe(
674-
Effect.andThen(Deferred.await(finishFetch)),
674+
yield* broadcaster.getStatus({ cwd: "/repo" });
675+
const subscribed = yield* Deferred.make<void>();
676+
const polled = yield* Deferred.make<void>();
677+
yield* broadcaster.streamStatus({ cwd: "/repo" }).pipe(
678+
Stream.runForEach((event) => {
679+
if (event._tag === "snapshot") return Deferred.succeed(subscribed, undefined);
680+
if (event._tag === "remoteUpdated") return Deferred.succeed(polled, undefined);
681+
return Effect.void;
682+
}),
683+
Effect.forkScoped,
675684
);
676-
const refresh = yield* broadcaster.refreshStatus("/repo").pipe(Effect.forkScoped);
677-
yield* Deferred.await(fetchStarted);
678-
679-
const fetchedLocal: VcsStatusLocalResult = {
685+
yield* Deferred.await(subscribed);
686+
state.currentLocalStatus = {
680687
...baseLocalStatus,
681-
branchChanges: { baseRef: "origin/main", insertions: 0, deletions: 0 },
688+
branchChanges: { baseRef: "origin/main", insertions: 107, deletions: 4 },
682689
};
683-
state.currentLocalStatus = fetchedLocal;
684-
yield* Deferred.succeed(finishFetch, undefined);
690+
state.currentRemoteStatus = remoteStatusWithPr;
691+
yield* TestClock.adjust("30 seconds");
692+
yield* Deferred.await(polled);
693+
const status = yield* broadcaster.getStatus({ cwd: "/repo" });
694+
assert.deepStrictEqual(status.branchChanges, state.currentLocalStatus.branchChanges);
695+
}).pipe(Effect.provide(layerTestFor(state)), Effect.scoped);
696+
});
685697

686-
const status = yield* Fiber.join(refresh);
687-
assert.deepStrictEqual(status.branchChanges, fetchedLocal.branchChanges);
688-
}).pipe(Effect.provide(layerTestFor(state)));
698+
it.effect("an explicit refresh reads local totals after the fetch", () => {
699+
return Effect.gen(function* () {
700+
const fetchStarted = yield* Deferred.make<void>();
701+
const finishFetch = yield* Deferred.make<void>();
702+
let local: VcsStatusLocalResult = {
703+
...baseLocalStatus,
704+
branchChanges: { baseRef: "origin/main", insertions: 745, deletions: 24 },
705+
};
706+
let cachedLocal: VcsStatusLocalResult | undefined;
707+
const readLocalStatus = () => Effect.sync(() => (cachedLocal ??= local));
708+
const layerTest = VcsStatusBroadcaster.layer.pipe(
709+
Layer.provideMerge(NodeServices.layer),
710+
Layer.provide(layerBackgroundPolicy(() => true)),
711+
Layer.provide(
712+
Layer.mock(GitWorkflowService.GitWorkflowService)({
713+
localStatus: readLocalStatus,
714+
invalidateStatus: () =>
715+
Effect.sync(() => {
716+
cachedLocal = undefined;
717+
}),
718+
invalidateLocalStatus: () =>
719+
Effect.sync(() => {
720+
cachedLocal = undefined;
721+
}),
722+
currentBranch: () => Effect.succeed(baseLocalStatus.refName),
723+
invalidateRemoteStatus: () => Effect.void,
724+
remoteStatus: () =>
725+
Deferred.succeed(fetchStarted, undefined).pipe(
726+
Effect.andThen(Deferred.await(finishFetch)),
727+
Effect.as(baseRemoteStatus),
728+
),
729+
}),
730+
),
731+
);
732+
yield* Effect.gen(function* () {
733+
const broadcaster = yield* VcsStatusBroadcaster.VcsStatusBroadcaster;
734+
const refresh = yield* broadcaster.refreshStatus("/repo").pipe(Effect.forkScoped);
735+
yield* Deferred.await(fetchStarted);
736+
// Another reader can refill the local cache while fetch is still running.
737+
yield* readLocalStatus();
738+
local = {
739+
...baseLocalStatus,
740+
branchChanges: { baseRef: "origin/main", insertions: 0, deletions: 0 },
741+
};
742+
yield* Deferred.succeed(finishFetch, undefined);
743+
const status = yield* Fiber.join(refresh);
744+
assert.deepStrictEqual(status.branchChanges, local.branchChanges);
745+
}).pipe(Effect.provide(layerTest));
746+
});
689747
});
690748

691749
it.effect("publishes local status while an earlier lookup holds the remote lock", () => {

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

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -477,23 +477,14 @@ export const make = Effect.gen(function* () {
477477
return yield* withRemoteWriteLock(
478478
cwd,
479479
Effect.gen(function* () {
480-
const previousRemote = (yield* getCachedStatus(cwd))?.remote?.value;
481480
const remoteRead = yield* readRemoteStatus(cwd, options);
482-
const remote = remoteRead?.value;
483481
const pulled =
484482
options?.refreshUpstream === false
485483
? null
486484
: yield* maybeAutoPull(cwd, remoteRead, options?.policyCwds ?? [cwd]);
487485
if (pulled !== null) return pulled.remote;
488-
if (
489-
remote &&
490-
(!previousRemote ||
491-
previousRemote.aheadCount !== remote.aheadCount ||
492-
previousRemote.behindCount !== remote.behindCount ||
493-
previousRemote.aheadOfDefaultCount !== remote.aheadOfDefaultCount)
494-
) {
495-
yield* refreshLocalStatusCore(cwd);
496-
}
486+
// Base refs and merge bases can move without changing divergence counts.
487+
yield* refreshLocalStatusCore(cwd);
497488
return yield* updateCachedRemoteStatus(cwd, remoteRead, { publish: true });
498489
}),
499490
);
@@ -530,8 +521,7 @@ export const make = Effect.gen(function* () {
530521
yield* workflow.invalidateStatus(cwd);
531522
const remoteRead = yield* readRemoteStatus(cwd, { refreshUpstream });
532523
// Fetch can move the base used for local Changes totals.
533-
const local = yield* workflow.localStatus({ cwd });
534-
yield* updateCachedLocalStatus(cwd, local, { publish: true });
524+
yield* refreshLocalStatusCore(cwd);
535525
// Without a fetch, the cached behind count cannot justify an automatic pull.
536526
const pulled = refreshUpstream ? yield* maybeAutoPull(cwd, remoteRead, [rawCwd]) : null;
537527
if (pulled !== null) return mergeGitStatusParts(pulled.local, pulled.remote);

0 commit comments

Comments
 (0)