Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
import { describe, expect, test } from "bun:test";
import { createInFlightGuard } from "./inFlightGuard";

function deferred<T = void>(): {
promise: Promise<T>;
resolve: (value: T) => void;
} {
let resolve!: (value: T) => void;
const promise = new Promise<T>((r) => {
resolve = r;
});
return { promise, resolve };
}

describe("createInFlightGuard", () => {
test("reproduces #4384: without a guard, rapid Cmd+T presses all spawn a terminal once the daemon bootstrap unblocks", async () => {
// Simulates the cold-start daemon bootstrap that blocks
// `terminal.createSession`. Every call sees the same pending promise
// and unblocks together when it resolves.
const bootstrap = deferred<void>();
let terminalsCreated = 0;
const createTerminal = async () => {
await bootstrap.promise;
terminalsCreated += 1;
};

// User mashes Cmd+T five times during the bootstrap window.
const presses = Array.from({ length: 5 }, () => createTerminal());

bootstrap.resolve();
await Promise.all(presses);

// Without an in-flight guard, every queued press creates a terminal.
expect(terminalsCreated).toBe(5);
});

test("with the guard, rapid presses while a creation is in flight are dropped", async () => {
const bootstrap = deferred<void>();
let terminalsCreated = 0;
const guard = createInFlightGuard();

const press = () =>
guard.run(async () => {
await bootstrap.promise;
terminalsCreated += 1;
});

const presses = Array.from({ length: 5 }, () => press());

bootstrap.resolve();
await Promise.all(presses);

expect(terminalsCreated).toBe(1);
});

test("releases the lock so subsequent presses succeed once the prior call settles", async () => {
const guard = createInFlightGuard();
let terminalsCreated = 0;
const press = () =>
guard.run(async () => {
terminalsCreated += 1;
});

await press();
await press();
await press();

expect(terminalsCreated).toBe(3);
});

test("releases the lock when the wrapped function rejects", async () => {
const guard = createInFlightGuard();
let attempts = 0;

await expect(
guard.run(async () => {
attempts += 1;
throw new Error("boom");
}),
).rejects.toThrow("boom");

await guard.run(async () => {
attempts += 1;
});

expect(attempts).toBe(2);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
/**
* Drops re-entrant calls while a prior invocation is still in flight. Used
* to coalesce rapid Cmd+T / "New Terminal" presses during the cold-start
* daemon bootstrap so they don't all spawn a terminal once the bootstrap
* unblocks.
*/
export function createInFlightGuard(): {
run: (fn: () => Promise<void>) => Promise<void>;
} {
let inFlight = false;
return {
async run(fn) {
if (inFlight) return;
inFlight = true;
try {
await fn();
} finally {
inFlight = false;
}
},
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import type {
TerminalPaneData,
} from "../../types";
import type { TerminalLauncher } from "../useV2TerminalLauncher";
import { createInFlightGuard } from "../useV2TerminalLauncher/inFlightGuard";

export function useWorkspaceHotkeys({
store,
Expand All @@ -44,17 +45,23 @@ export function useWorkspaceHotkeys({

// --- Tab creation ---

useHotkey("NEW_GROUP", async () => {
const terminalId = await launcher.create();
store.getState().addTab({
panes: [
{
kind: "terminal",
data: { terminalId } as TerminalPaneData,
},
],
});
});
// Drop repeats while a creation is in flight so rapid Cmd+T presses
// during the cold-start daemon bootstrap don't all spawn a terminal once
// it unblocks (#4384).
const newGroupGuardRef = useRef(createInFlightGuard());
useHotkey("NEW_GROUP", () =>
newGroupGuardRef.current.run(async () => {
const terminalId = await launcher.create();
store.getState().addTab({
panes: [
{
kind: "terminal",
data: { terminalId } as TerminalPaneData,
},
],
});
}),
);

useHotkey("NEW_CHAT", () => {
store.getState().addTab({
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { WorkspaceStore } from "@superset/panes";
import { useCallback } from "react";
import { useCallback, useRef } from "react";
import type { StoreApi } from "zustand/vanilla";
import type {
BrowserPaneData,
Expand All @@ -10,6 +10,7 @@ import type {
TerminalPaneData,
} from "../../types";
import type { TerminalLauncher } from "../useV2TerminalLauncher";
import { createInFlightGuard } from "../useV2TerminalLauncher/inFlightGuard";

export function useWorkspacePaneOpeners({
store,
Expand Down Expand Up @@ -93,17 +94,25 @@ export function useWorkspacePaneOpeners({
[store],
);

const addTerminalTab = useCallback(async () => {
const terminalId = await launcher.create();
store.getState().addTab({
panes: [
{
kind: "terminal",
data: { terminalId } as TerminalPaneData,
},
],
});
}, [store, launcher]);
// Drop rapid `addTerminalTab` invocations while one is pending so users
// who mash Cmd+T / "New Terminal" during the cold-start daemon bootstrap
// don't queue up several creations that all unblock together (#4384).
const addTerminalGuardRef = useRef(createInFlightGuard());
const addTerminalTab = useCallback(
() =>
addTerminalGuardRef.current.run(async () => {
const terminalId = await launcher.create();
store.getState().addTab({
panes: [
{
kind: "terminal",
data: { terminalId } as TerminalPaneData,
},
],
});
}),
[store, launcher],
);

const addChatTab = useCallback(() => {
store.getState().addTab({
Expand Down