diff --git a/webui/src/lib/nanobot-client.ts b/webui/src/lib/nanobot-client.ts index 4b4d9d079..29afba534 100644 --- a/webui/src/lib/nanobot-client.ts +++ b/webui/src/lib/nanobot-client.ts @@ -109,10 +109,8 @@ interface PendingRequest { timer: ReturnType; } -type WebUIRequestFrame = Extract; - interface PendingWebUIRequest extends PendingRequest { - frame: WebUIRequestFrame; + serializedFrame: string; } export class WebUIMutationError extends Error { @@ -868,6 +866,13 @@ export class NanobotClient { } return new Promise((resolve, reject) => { + let serializedFrame: string; + try { + serializedFrame = JSON.stringify(frame); + } catch { + reject(new WebUIMutationError(503, "Could not encode WebUI request")); + return; + } const timer = setTimeout(() => { this.pendingWebUIRequests.delete(requestId); reject( @@ -881,10 +886,10 @@ export class NanobotClient { resolve: (value) => resolve(value as T), reject, timer, - frame, + serializedFrame, }); try { - socket.send(JSON.stringify(frame)); + socket.send(serializedFrame); } catch { clearTimeout(timer); this.pendingWebUIRequests.delete(requestId); @@ -1028,7 +1033,7 @@ export class NanobotClient { this.rawSend({ type: "attach", chat_id: chatId }); } for (const pending of this.pendingWebUIRequests.values()) { - this.rawSend(pending.frame); + this.rawSendSerialized(pending.serializedFrame); } // Flush anything queued during reconnect. const queued = this.sendQueue.splice(0); @@ -1476,4 +1481,13 @@ export class NanobotClient { 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. + } + } } diff --git a/webui/src/tests/nanobot-client.test.ts b/webui/src/tests/nanobot-client.test.ts index 6256e904a..3d15965bc 100644 --- a/webui/src/tests/nanobot-client.test.ts +++ b/webui/src/tests/nanobot-client.test.ts @@ -183,12 +183,12 @@ describe("NanobotClient", () => { const firstSocket = lastSocket(); firstSocket.fakeOpen(); - const pending = client.requestMutation<{ ran: boolean }>( - "automation.run", - { id: "daily-summary" }, - ); + const payload = { id: "daily-summary", options: { force: false } }; + const pending = client.requestMutation<{ ran: boolean }>("automation.run", payload); const frame = firstSocket.sent.at(-1) as string; const requestId = JSON.parse(frame).request_id; + payload.id = "weekly-summary"; + payload.options.force = true; const settled = expect(pending).resolves.toEqual({ ran: true }); firstSocket.fakeCloseWithCode(1006);