Skip to content
Merged
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
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ All notable changes to `@openclaw/libterminal` will be documented in this file.

### Fixed

- Recheck whether the application still needs a terminal hub connection when a delayed reconnect fires.

- Split large Worker output acknowledgements into decodable chunks so flow-control accounting stays valid above one MiB.

- Complete fanout delivery and teardown before event callbacks, preserving output order during reentrant publication and closing all subscribers when callbacks throw.
Expand Down
5 changes: 4 additions & 1 deletion src/browser-hub.ts
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,10 @@ export class TerminalHubClient {
}
this.reconnectTimer = setTimeout(() => {
this.reconnectTimer = undefined;
this.connect();
// The application's need for a connection can change during the delay.
if (this.shouldReconnect() && !this.closedByCaller) {
this.connect();
}
}, this.options.reconnectDelayMs ?? 1500);
}

Expand Down
52 changes: 52 additions & 0 deletions test/browser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,58 @@ describe("TerminalHubClient", () => {
}
});

it.each(["unused", "throws", "closes"])(
"rechecks reconnect eligibility when the delayed predicate %s",
async (mode) => {
vi.useFakeTimers();
const sockets: TestTerminalHubSocket[] = [];
const error = new Error("reconnect predicate failed");
const onError = vi.fn();
let delayed = false;
const client = new TerminalHubClient({
url: "wss://terminal.example",
reconnectDelayMs: 10,
shouldReconnect: () => {
if (!delayed) {
return true;
}
if (mode === "throws") {
throw error;
}
if (mode === "closes") {
client.close();
return true;
}
return false;
},
onError,
socketFactory: () => {
const socket = new TestTerminalHubSocket();
sockets.push(socket);
return socket;
},
});
try {
client.connect();
sockets[0].open();
sockets[0].emitClose();
expect(vi.getTimerCount()).toBe(1);
delayed = true;

await vi.advanceTimersByTimeAsync(10);

expect(sockets).toHaveLength(1);
expect(vi.getTimerCount()).toBe(0);
if (mode === "throws") {
expect(onError).toHaveBeenCalledWith(error);
}
} finally {
client.close();
vi.useRealTimers();
}
},
);

it("does not let a stalled prior socket block frames after reconnecting", async () => {
const sockets: TestTerminalHubSocket[] = [];
const frames: string[] = [];
Expand Down
Loading