Let the model layer default the response row; require requestId

This commit is contained in:
Gregory Schier
2026-08-17 07:57:30 -07:00
parent 43a2d140ad
commit 1216881bb3
2 changed files with 41 additions and 53 deletions
+5 -7
View File
@@ -71,13 +71,11 @@ const HANDLERS: Partial<Record<AppCmd, Handler>> = {
// The tab renders and stores; a stateless proxy puts the bytes on the wire. // The tab renders and stores; a stateless proxy puts the bytes on the wire.
// See send.ts for the whole shape of it. // See send.ts for the whole shape of it.
cmd_send_http_request: (payload, db) => cmd_send_http_request: (payload, db) => {
sendHttpRequest( const requestId = str(payload, "requestId");
db, if (requestId == null) throw new Error("cmd_send_http_request needs a requestId");
text(payload, "requestId"), return sendHttpRequest(db, requestId, str(payload, "environmentId"), str(payload, "cookieJarId"));
str(payload, "environmentId"), },
str(payload, "cookieJarId"),
),
/* -------------------------------- app ---------------------------------- */ /* -------------------------------- app ---------------------------------- */
+36 -46
View File
@@ -36,27 +36,36 @@ interface HttpResponseHeader {
value: string; value: string;
} }
/** The subset of the `http_response` model this sender writes. */ /**
* The fields of the `http_response` model this sender writes as a send
* progresses. Every one is optional: a field this file never sets takes the
* model layer's default, the same way the desktop's row does. Defaults live in
* Rust, once.
*/
interface ResponsePatch { interface ResponsePatch {
state?: "initialized" | "connected" | "closed";
url?: string;
status?: number;
statusReason?: string | null;
version?: string | null;
remoteAddr?: string | null;
headers?: HttpResponseHeader[];
requestHeaders?: HttpResponseHeader[];
contentLength?: number | null;
contentLengthCompressed?: number | null;
elapsed?: number;
elapsedHeaders?: number;
elapsedDns?: number;
error?: string | null;
}
/** The row itself: what identifies it, plus whatever has been written so far. */
type ResponseRow = {
model: "http_response"; model: "http_response";
id: string; id?: string;
requestId: string; requestId: string;
workspaceId: string; workspaceId: string;
state: "initialized" | "connected" | "closed"; } & ResponsePatch;
url: string;
status: number;
statusReason: string | null;
version: string | null;
remoteAddr: string | null;
headers: HttpResponseHeader[];
requestHeaders: HttpResponseHeader[];
contentLength: number | null;
contentLengthCompressed: number | null;
elapsed: number;
elapsedHeaders: number;
elapsedDns: number;
error: string | null;
}
interface CookieJarModel { interface CookieJarModel {
model: "cookie_jar"; model: "cookie_jar";
@@ -88,26 +97,7 @@ export async function sendHttpRequest(
// a failure to render or to reach the proxy lands in the response pane as // a failure to render or to reach the proxy lands in the response pane as
// that response's error rather than as a toast that names no request. // that response's error rather than as a toast that names no request.
const workspaceId = await workspaceIdOfRequest(db, requestId); const workspaceId = await workspaceIdOfRequest(db, requestId);
const response = new ResponseWriter(db, { const response = new ResponseWriter(db, { model: "http_response", requestId, workspaceId });
model: "http_response",
id: "",
requestId,
workspaceId,
state: "initialized",
url: "",
status: 0,
statusReason: null,
version: null,
remoteAddr: null,
headers: [],
requestHeaders: [],
contentLength: null,
contentLengthCompressed: null,
elapsed: 0,
elapsedHeaders: 0,
elapsedDns: 0,
error: null,
});
await response.create(); await response.create();
const cancel = new AbortController(); const cancel = new AbortController();
@@ -237,7 +227,7 @@ async function runSend(
}); });
} }
function headOf(frame: ProxySendResponse): Partial<ResponsePatch> { function headOf(frame: ProxySendResponse): ResponsePatch {
return { return {
state: "connected", state: "connected",
status: frame.status, status: frame.status,
@@ -261,34 +251,34 @@ function headOf(frame: ProxySendResponse): Partial<ResponsePatch> {
* `models_upsert`, so every tab on this database sees the response land. * `models_upsert`, so every tab on this database sees the response land.
*/ */
class ResponseWriter { class ResponseWriter {
private state: ResponsePatch; private state: ResponseRow;
constructor( constructor(
private readonly db: WorkerConnection, private readonly db: WorkerConnection,
initial: ResponsePatch, initial: ResponseRow,
) { ) {
this.state = initial; this.state = initial;
} }
get id(): string { get id(): string {
return this.state.id; return this.state.id ?? "";
} }
get workspaceId(): string { get workspaceId(): string {
return this.state.workspaceId; return this.state.workspaceId;
} }
current(): ResponsePatch { current(): ResponseRow {
return this.state; return this.state;
} }
/** Create the row. Everything but its identity is the model layer's default. */
async create(): Promise<void> { async create(): Promise<void> {
const { id: _, ...withoutId } = this.state; const id = await this.db.rpc<string>("models_upsert", { model: this.state });
const id = await this.db.rpc<string>("models_upsert", { model: withoutId });
this.state = { ...this.state, id }; this.state = { ...this.state, id };
} }
async patch(patch: Partial<ResponsePatch>): Promise<void> { async patch(patch: ResponsePatch): Promise<void> {
// Structured clone carries `undefined` across to the worker as a present // Structured clone carries `undefined` across to the worker as a present
// key, and the model layer reads that as "wrong type" and refuses the whole // key, and the model layer reads that as "wrong type" and refuses the whole
// model. Nothing here should produce one, but a missing wire field must // model. Nothing here should produce one, but a missing wire field must
@@ -298,7 +288,7 @@ class ResponseWriter {
await this.db.rpc("models_upsert", { model: this.state }); await this.db.rpc("models_upsert", { model: this.state });
} }
async finish(patch: Partial<ResponsePatch>): Promise<void> { async finish(patch: ResponsePatch): Promise<void> {
await this.patch({ ...patch, state: "closed" }); await this.patch({ ...patch, state: "closed" });
} }
} }