fix(desktop): stabilize workstream PR subscriptions

Signed-off-by: loganj <loganj@squareup.com>
Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Logan Johnson
2026-08-18 20:44:24 +00:00
parent 2c86020962
commit 4ba6dc36c7
2 changed files with 63 additions and 3 deletions

View File

@@ -236,6 +236,51 @@ test("workstream status hook preserves a synchronous live callback", async () =>
cleanup();
});
test("workstream status hook keeps subscriptions stable across equivalent rerenders", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { useWorkstreamPullRequestStatuses } = await import(
"./workstreamPullRequestStatus.ts"
);
const reference = parseWorkstreamPullRequestReference(
"https://github.com/block/buzz/pull/42",
);
let subscribes = 0;
let unsubscribes = 0;
const registry = {
subscribe: (_reference, listener) => {
subscribes += 1;
listener({ status: "loading" });
return () => {
unsubscribes += 1;
};
},
};
const { rerender, unmount } = renderHook(
({ refs }) => useWorkstreamPullRequestStatuses(refs, registry),
{ initialProps: { refs: [reference] } },
);
await act(async () => {
rerender({ refs: [{ ...reference }] });
rerender({ refs: [{ ...reference }] });
});
assert.equal(subscribes, 1);
assert.equal(unsubscribes, 0);
const replacement = parseWorkstreamPullRequestReference(
"https://github.com/block/buzz/pull/43",
);
await act(async () => {
rerender({ refs: [replacement] });
});
assert.equal(subscribes, 2);
assert.equal(unsubscribes, 1);
unmount();
assert.equal(unsubscribes, 2);
cleanup();
});
test("workstream status hook removes stale references and preserves unsupported state", async () => {
const { act, cleanup, renderHook } = await import("@testing-library/react");
const { useWorkstreamPullRequestStatuses } = await import(

View File

@@ -519,11 +519,26 @@ export function useWorkstreamPullRequestStatuses(
const [states, setStates] = React.useState<
ReadonlyMap<string, WorkstreamPullRequestDisplayState>
>(() => new Map());
const referencesByIdentity = React.useMemo(() => {
// Cards rebuild parsed references on every render. Subscribe by their
// semantic identity instead of that transient array/object identity so a
// synchronous registry listener cannot cause an effect resubscribe loop.
const referenceIdentityKey = references
.map((reference) => reference.identity)
.sort()
.join("\u0000");
const referencesByIdentityRef = React.useRef<{
identityKey: string;
references: ReadonlyMap<string, WorkstreamPullRequestReference>;
} | null>(null);
if (referencesByIdentityRef.current?.identityKey !== referenceIdentityKey) {
const next = new Map<string, WorkstreamPullRequestReference>();
for (const reference of references) next.set(reference.identity, reference);
return next;
}, [references]);
referencesByIdentityRef.current = {
identityKey: referenceIdentityKey,
references: next,
};
}
const referencesByIdentity = referencesByIdentityRef.current.references;
React.useEffect(() => {
const initial = new Map<string, WorkstreamPullRequestDisplayState>();