fix(transport): preserve backpressure across framing and routes

Ordinary daemon writes must remain visible to terminal catch-up and socket admission. Pace chunked writes and explicit completion requests, include waiting logical messages in pressure readings, and propagate origin completion through routed client transports.
This commit is contained in:
Mohamed Boudra
2026-10-30 15:51:50 +01:00
parent 98f667f93e
commit f28f0fee3d
9 changed files with 278 additions and 56 deletions
+37
View File
@@ -8504,3 +8504,40 @@ test("uses the frame budget selected by an asynchronously opened transport", asy
true,
);
});
test.each([false, true])("routed sends wait for origin drain (binary=%s)", async (binary) => {
const mock = createMockTransport();
let drain!: () => void;
let blocked = false;
const send = mock.transport.send;
mock.transport.send = (frame) => {
send(frame);
if (blocked)
return new Promise<void>((resolve) => {
drain = resolve;
});
};
const client = new DaemonClient({
url: "ws://test",
clientId: "routed-drain",
transportFactory: () => mock.transport,
reconnect: { enabled: false },
});
clients.push(client);
const connecting = client.connect();
mock.triggerOpen({ features: { binaryChunkingV1: true } });
await connecting;
blocked = true;
let finished = false;
const write = Promise.resolve(
client.sendHostFrame("route", binary ? new Uint8Array([1]) : "hello"),
).then(() => {
finished = true;
return undefined;
});
await Promise.resolve();
expect(finished).toBe(false);
drain();
await write;
expect(finished).toBe(true);
});
+23 -18
View File
@@ -1884,9 +1884,9 @@ export class DaemonClient {
})
).routeId;
}
sendHostFrame(routeId: string, data: string | Uint8Array): void {
sendHostFrame(routeId: string, data: string | Uint8Array): Promise<void> {
if (typeof data === "string") {
this.sendSessionMessageStrict({
return this.sendSessionMessageStrict({
type: "hosts.route.frame.request",
routeId,
data,
@@ -1894,7 +1894,7 @@ export class DaemonClient {
});
} else {
if (!this.isConnected || !this.transport) throw this.unavailableError();
this.sendTransportFrame(encodeHostRouteFrame(routeId, data));
return this.sendTransportFrame(encodeHostRouteFrame(routeId, data));
}
}
private readonly hostFrameListeners = new Set<(frame: HostRouteFrame) => void>();
@@ -2060,7 +2060,7 @@ export class DaemonClient {
messageType: string,
message: unknown,
submitted?: () => void,
): void {
): Promise<void> {
const isOpen =
this.config.trace?.isEnabled() === true &&
this.beginTraceSection("paseo.ws.message.outbound", {
@@ -2069,25 +2069,30 @@ export class DaemonClient {
...describeRequestIdentity(message),
});
try {
this.sendTransportFrame(JSON.stringify(message), submitted);
return this.sendTransportFrame(JSON.stringify(message), submitted);
} finally {
this.endTraceSection(isOpen);
}
}
private sendSessionPayload(payload: SessionInboundMessage): void {
this.sendJsonMessage("session", payload.type, { type: "session", message: payload }, () => {
if (!("requestId" in payload) || typeof payload.requestId !== "string") return;
for (const waiter of this.waiters) {
if (waiter.requestId === payload.requestId) waiter.submitted = true;
}
});
private sendSessionPayload(payload: SessionInboundMessage): Promise<void> {
return this.sendJsonMessage(
"session",
payload.type,
{ type: "session", message: payload },
() => {
if (!("requestId" in payload) || typeof payload.requestId !== "string") return;
for (const waiter of this.waiters) {
if (waiter.requestId === payload.requestId) waiter.submitted = true;
}
},
);
}
private sendTransportFrame(
frame: string | Uint8Array | ArrayBuffer,
submitted?: () => void,
): void {
): Promise<void> {
if (!this.transport) {
throw this.unavailableError();
}
@@ -2102,9 +2107,9 @@ export class DaemonClient {
submitted?.();
const payload = typeof frame === "string" ? frame : asUint8Array(frame)!;
const transport = this.transport;
void this.chunkedTransport!.send(payload).catch(() =>
transport.close(4003, "Transport send failed"),
);
const write = this.chunkedTransport!.send(payload);
void write.catch(() => transport.close(4003, "Transport send failed"));
return write;
} finally {
this.endTraceSection(isOpen);
}
@@ -2376,13 +2381,13 @@ export class DaemonClient {
});
}
private sendSessionMessageStrict(message: SessionInboundMessage): void {
private sendSessionMessageStrict(message: SessionInboundMessage): Promise<void> {
if (!this.transport || this.connectionState.status !== "connected") {
throw this.unavailableError();
}
const payload = SessionInboundMessageSchema.parse(message);
try {
this.sendSessionPayload(payload);
return this.sendSessionPayload(payload);
} catch (error) {
throw error instanceof Error ? error : new Error(String(error));
}
+1 -1
View File
@@ -92,7 +92,7 @@ export function createHostChannelTransport(
code: "host_offline",
serverId,
});
origin.sendHostFrame(
return origin.sendHostFrame(
routeId,
typeof data === "string" || data instanceof Uint8Array ? data : new Uint8Array(data),
);