fix(webui): snapshot mutation replay frames

This commit is contained in:
chengyongru
2026-08-16 21:40:14 +08:00
committed by chengyongru
parent e51ffc8978
commit 2b6a0443c0
2 changed files with 24 additions and 10 deletions
+20 -6
View File
@@ -109,10 +109,8 @@ interface PendingRequest<T> {
timer: ReturnType<typeof setTimeout>; timer: ReturnType<typeof setTimeout>;
} }
type WebUIRequestFrame = Extract<Outbound, { type: "webui_request" }>;
interface PendingWebUIRequest extends PendingRequest<unknown> { interface PendingWebUIRequest extends PendingRequest<unknown> {
frame: WebUIRequestFrame; serializedFrame: string;
} }
export class WebUIMutationError extends Error { export class WebUIMutationError extends Error {
@@ -868,6 +866,13 @@ export class NanobotClient {
} }
return new Promise<T>((resolve, reject) => { return new Promise<T>((resolve, reject) => {
let serializedFrame: string;
try {
serializedFrame = JSON.stringify(frame);
} catch {
reject(new WebUIMutationError(503, "Could not encode WebUI request"));
return;
}
const timer = setTimeout(() => { const timer = setTimeout(() => {
this.pendingWebUIRequests.delete(requestId); this.pendingWebUIRequests.delete(requestId);
reject( reject(
@@ -881,10 +886,10 @@ export class NanobotClient {
resolve: (value) => resolve(value as T), resolve: (value) => resolve(value as T),
reject, reject,
timer, timer,
frame, serializedFrame,
}); });
try { try {
socket.send(JSON.stringify(frame)); socket.send(serializedFrame);
} catch { } catch {
clearTimeout(timer); clearTimeout(timer);
this.pendingWebUIRequests.delete(requestId); this.pendingWebUIRequests.delete(requestId);
@@ -1028,7 +1033,7 @@ export class NanobotClient {
this.rawSend({ type: "attach", chat_id: chatId }); this.rawSend({ type: "attach", chat_id: chatId });
} }
for (const pending of this.pendingWebUIRequests.values()) { for (const pending of this.pendingWebUIRequests.values()) {
this.rawSend(pending.frame); this.rawSendSerialized(pending.serializedFrame);
} }
// Flush anything queued during reconnect. // Flush anything queued during reconnect.
const queued = this.sendQueue.splice(0); const queued = this.sendQueue.splice(0);
@@ -1476,4 +1481,13 @@ export class NanobotClient {
this.sendQueue.push(frame); this.sendQueue.push(frame);
} }
} }
private rawSendSerialized(serializedFrame: string): void {
if (!this.socket) return;
try {
this.socket.send(serializedFrame);
} catch {
// The pending request remains available for the next successful reconnect.
}
}
} }
+4 -4
View File
@@ -183,12 +183,12 @@ describe("NanobotClient", () => {
const firstSocket = lastSocket(); const firstSocket = lastSocket();
firstSocket.fakeOpen(); firstSocket.fakeOpen();
const pending = client.requestMutation<{ ran: boolean }>( const payload = { id: "daily-summary", options: { force: false } };
"automation.run", const pending = client.requestMutation<{ ran: boolean }>("automation.run", payload);
{ id: "daily-summary" },
);
const frame = firstSocket.sent.at(-1) as string; const frame = firstSocket.sent.at(-1) as string;
const requestId = JSON.parse(frame).request_id; const requestId = JSON.parse(frame).request_id;
payload.id = "weekly-summary";
payload.options.force = true;
const settled = expect(pending).resolves.toEqual({ ran: true }); const settled = expect(pending).resolves.toEqual({ ran: true });
firstSocket.fakeCloseWithCode(1006); firstSocket.fakeCloseWithCode(1006);