Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 23 additions & 12 deletions extensions/git-info/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -158,18 +158,24 @@ export default function gitInfo(pi: ExtensionAPI) {
const refresh = (ctx: ExtensionContext, forcePullRequest = false) =>
refreshCoordinator.run(refreshEffect(ctx, forcePullRequest, generation));

const refreshIfIdle = (ctx: ExtensionContext) =>
refreshCoordinator.runIfIdle(refreshEffect(ctx, false, generation));
const refreshIfIdle = (
ctx: ExtensionContext,
refreshGeneration = generation,
) =>
refreshCoordinator.runIfIdle(refreshEffect(ctx, false, refreshGeneration));

const reportBackgroundDefect = (defect: unknown) =>
Effect.logError("git-info background task defect", defect);

const poll = () =>
const poll = (ctx: ExtensionContext, pollGeneration: number) =>
Effect.suspend(() =>
currentContext ? refreshIfIdle(currentContext) : Effect.void,
pollGeneration === generation
? refreshIfIdle(ctx, pollGeneration)
: Effect.fail("stale polling session"),
).pipe(
Effect.catchDefect(reportBackgroundDefect),
Effect.repeat(Schedule.fixed(POLL_INTERVAL_MS)),
Effect.catch(() => Effect.void),
Effect.delay(POLL_INTERVAL_MS),
Effect.asVoid,
);
Expand All @@ -183,24 +189,29 @@ export default function gitInfo(pi: ExtensionAPI) {
forkBackground(refreshIfIdle(ctx));
};

const stopPolling = async (activeRuntime = runtime) => {
const previousPollingFiber = pollingFiber;
pollingFiber = undefined;
if (previousPollingFiber && activeRuntime) {
await activeRuntime.runPromise(Fiber.interrupt(previousPollingFiber));
}
};

const stopRefreshListener = pi.events.on(REFRESH_CHANNEL, () => {
if (currentContext) refreshInBackground(currentContext);
});

pi.on("session_start", async (_event, ctx) => {
generation += 1;
const sessionGeneration = ++generation;
queriedPrBranch = null;

const previousPollingFiber = pollingFiber;
pollingFiber = undefined;
if (previousPollingFiber) {
await getRuntime().runPromise(Fiber.interrupt(previousPollingFiber));
}
await stopPolling();
if (sessionGeneration !== generation) return;

// Do not block Pi startup on GitHub/network I/O. The initial refresh publishes
// state when it completes; polling continues to keep it current afterwards.
refreshInBackground(ctx);
pollingFiber = forkBackground(poll());
pollingFiber = forkBackground(poll(ctx, sessionGeneration));
});

pi.on("input", (_event, ctx) => {
Expand All @@ -216,8 +227,8 @@ export default function gitInfo(pi: ExtensionAPI) {
stopRefreshListener();
generation += 1;
currentContext = undefined;
pollingFiber = undefined;
const closing = runtime;
await stopPolling(closing);
runtime = undefined;
await closing?.dispose();
});
Expand Down
69 changes: 69 additions & 0 deletions tests/extensions/git-info/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -186,3 +186,72 @@ printf '%s\\n' '{"number":42,"url":"https://example.test/pr/42","state":"OPEN","
rmSync(root, { recursive: true, force: true });
}
});

test("rapid session starts do not leave a stale polling context", async () => {
const root = process.cwd();
const hooks = new Map<
string,
(event: unknown, ctx: ExtensionContext) => unknown
>();
let activeSession = 1;
let staleReads = 0;
let currentSessionReads = 0;
const api = {
events: {
on: () => () => undefined,
emit: () => undefined,
},
on: (
event: string,
handler: (event: unknown, ctx: ExtensionContext) => unknown,
) => {
hooks.set(event, handler);
},
registerCommand: () => undefined,
} as unknown as ExtensionAPI;
const makeContext = (session: number) => {
const ctx = {
mode: "tui",
signal: undefined,
ui: { notify: () => undefined },
} as Record<string, unknown>;
Object.defineProperty(ctx, "cwd", {
get: () => {
if (session !== activeSession) {
staleReads += 1;
throw new Error(
"This extension ctx is stale after session replacement",
);
}
currentSessionReads += 1;
return root;
},
});
return ctx as unknown as ExtensionContext;
};
const start = (event: unknown, ctx: ExtensionContext) =>
hooks.get("session_start")?.(event, ctx);
const ctx1 = makeContext(1);

gitInfo(api);
try {
await start({}, ctx1);
await new Promise((resolve) => setTimeout(resolve, 100));

activeSession = 10;
const starts = Array.from({ length: 9 }, (_, index) =>
start({}, makeContext(index + 2)),
);
await Promise.all(starts);
await new Promise((resolve) => setTimeout(resolve, 1_000));
assert.equal(currentSessionReads > 0, true);
currentSessionReads = 0;
staleReads = 0;
await new Promise((resolve) => setTimeout(resolve, 6_000));

assert.equal(currentSessionReads > 0, true);
assert.equal(staleReads, 0);
} finally {
await hooks.get("session_shutdown")?.({}, makeContext(activeSession));
}
});
Loading