feat(rpc): expose snapshot, compare and restore for request versions

Three commands, all host-independent so the browser build answers them from
the same model layer as the desktop:

- models_snapshot_request, for the edit-session boundaries only the frontend
  can see. It takes a reason and nothing else — the caller does not decide
  whether anything changed, because content addressing already has.
- models_request_version, which returns a version and the live request's
  content together so they are guaranteed comparable, plus the same
  content-hash verdict the backend uses rather than a second opinion formed in
  TypeScript.
- models_restore_request_version.

The browser host also snapshots before its own send, since its send pipeline
is in TypeScript rather than in the shared crate.

Bindings regenerated with CI's `cargo test --all --features
yaak-app-client/wry`, which also drops a stale ImportSourceResource from the
rpc-schema copy and adds a missing ImportSource to the plugins copy.
This commit is contained in:
Gregory Schier
2026-09-05 21:16:29 -07:00
parent 19a43e3785
commit b08b3277da
12 changed files with 407 additions and 36 deletions
+14 -2
View File
@@ -37,8 +37,8 @@ use yaak_grpc::ServiceDefinition;
use yaak_models::blob_manager::BlobManager;
use yaak_models::models::{
GraphQlIntrospection, GrpcEvent, HttpRequest, HttpRequestHeader, HttpResponse,
HttpResponseEvent, ImportSource, Plugin, Settings, WebsocketConnection, WebsocketEvent,
WorkspaceMeta,
HttpResponseEvent, ImportSource, ModelVersion, Plugin, RequestVersionComparison, Settings,
WebsocketConnection, WebsocketEvent, WorkspaceMeta,
};
use yaak_models::query_manager::QueryManager;
use yaak_models::util::{BatchUpsertResult, ImportPlan};
@@ -653,6 +653,18 @@ async fn models_duplicate<R: Runtime>(ctx: ClientCtx<R>, req: ModelsDuplicateReq
Ok(yaak_commands::models::models_duplicate(ctx, req).await?)
}
async fn models_snapshot_request<R: Runtime>(ctx: ClientCtx<R>, req: ModelsSnapshotRequestReq) -> Result<ModelVersion> {
Ok(yaak_commands::models::models_snapshot_request(ctx, req).await?)
}
async fn models_request_version<R: Runtime>(ctx: ClientCtx<R>, req: ModelsRequestVersionReq) -> Result<RequestVersionComparison> {
Ok(yaak_commands::models::models_request_version(ctx, req).await?)
}
async fn models_restore_request_version<R: Runtime>(ctx: ClientCtx<R>, req: ModelsRestoreRequestVersionReq) -> Result<String> {
Ok(yaak_commands::models::models_restore_request_version(ctx, req).await?)
}
async fn models_websocket_events<R: Runtime>(ctx: ClientCtx<R>, req: ModelsWebsocketEventsReq) -> Result<Vec<WebsocketEvent>> {
Ok(yaak_commands::models::models_websocket_events(ctx, req).await?)
}
+51 -11
View File
@@ -138,6 +138,10 @@ export type GrpcConnection = {
state: GrpcConnectionState;
trailers: { [key in string]?: string };
url: string;
/**
* The request version this connection was opened from, when one was captured.
*/
versionId: string | null;
};
export type GrpcConnectionState = "initialized" | "connected" | "closed";
@@ -242,6 +246,10 @@ export type HttpResponse = {
state: HttpResponseState;
url: string;
version: string | null;
/**
* The request version this response was sent from, when one was captured.
*/
versionId: string | null;
};
export type HttpResponseEvent = {
@@ -331,17 +339,6 @@ export type ImportSource = {
lastImportedAt: string;
};
export type ImportSourceResource = {
model: "import_source_resource";
createdAt: string;
updatedAt: string;
importSourceId: string;
sourceKey: string;
modelType: string;
modelId: string;
snapshot: string;
};
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
@@ -358,6 +355,28 @@ export type KeyValue = {
value: string;
};
export type ModelVersion = {
model: "model_version";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
/**
* The `model` field of the versioned model, eg. `http_request`.
*/
modelType: string;
modelId: string;
contentHash: string;
document: Record<string, any>;
reason: ModelVersionReason;
};
/**
* Why a version was captured. Not a UI label — the frontend decides how to
* phrase these — but it is what makes a history readable when debugging.
*/
export type ModelVersionReason = "send" | "switch" | "idle" | "restore" | "manual";
export type Plugin = {
model: "plugin";
id: string;
@@ -385,6 +404,23 @@ export type ProxySetting =
export type ProxySettingAuth = { user: string; password: string };
/**
* One version, next to the request as it stands now.
*
* Both halves come from the same place so they are guaranteed comparable: the
* frontend renders them side by side, and `differs` is the same content-hash
* comparison the backend uses everywhere else rather than a second opinion
* formed in TypeScript.
*/
export type RequestVersionComparison = {
version: ModelVersion;
/**
* The live request's editable content, in the same shape as the version's document.
*/
currentDocument: Record<string, any>;
differs: boolean;
};
export type Settings = {
model: "settings";
id: string;
@@ -449,6 +485,10 @@ export type WebsocketConnection = {
state: WebsocketConnectionState;
status: number;
url: string;
/**
* The request version this connection was opened from, when one was captured.
*/
versionId: string | null;
};
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
File diff suppressed because one or more lines are too long
+27 -2
View File
@@ -21,8 +21,8 @@ use yaak_git::{
use yaak_grpc::ServiceDefinition;
use yaak_models::models::{
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequest, HttpRequestHeader, HttpResponse,
HttpResponseEvent, ImportSource, Plugin, Settings, WebsocketConnection, WebsocketEvent,
WorkspaceMeta,
HttpResponseEvent, ImportSource, ModelVersion, ModelVersionReason, Plugin,
RequestVersionComparison, Settings, WebsocketConnection, WebsocketEvent, WorkspaceMeta,
};
use yaak_models::util::{BatchUpsertResult, ImportDestination, ImportPlan};
use yaak_plugins::api::{PluginNameVersion, PluginSearchResponse, PluginUpdatesResponse};
@@ -534,6 +534,28 @@ pub struct ModelsDuplicateReq {
pub model_id: String,
}
#[derive(Debug, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_rpc.ts")]
pub struct ModelsSnapshotRequestReq {
pub request_id: String,
pub reason: ModelVersionReason,
}
#[derive(Debug, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_rpc.ts")]
pub struct ModelsRequestVersionReq {
pub version_id: String,
}
#[derive(Debug, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_rpc.ts")]
pub struct ModelsRestoreRequestVersionReq {
pub version_id: String,
}
#[derive(Debug, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_rpc.ts")]
@@ -981,6 +1003,9 @@ macro_rules! with_commands {
models_upsert(ModelsUpsertReq) -> String,
models_delete(ModelsDeleteReq) -> String,
models_duplicate(ModelsDuplicateReq) -> String,
models_snapshot_request(ModelsSnapshotRequestReq) -> ModelVersion,
models_request_version(ModelsRequestVersionReq) -> RequestVersionComparison,
models_restore_request_version(ModelsRestoreRequestVersionReq) -> String,
models_websocket_events(ModelsWebsocketEventsReq) -> Vec<WebsocketEvent>,
models_grpc_events(ModelsGrpcEventsReq) -> Vec<GrpcEvent>,
models_get_settings(ModelsGetSettingsReq) -> Settings,
+39 -2
View File
@@ -4,9 +4,10 @@
use crate::error::Result;
use crate::host::{Host, PluginHost};
use yaak_models::models::{
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequestHeader, Settings, WebsocketEvent,
WorkspaceMeta,
AnyModel, GraphQlIntrospection, GrpcEvent, HttpRequestHeader, ModelVersion,
RequestVersionComparison, Settings, WebsocketEvent, WorkspaceMeta,
};
use yaak_models::versions::version_document;
use yaak_models::queries::workspaces::default_headers;
use yaak_rpc_schema::*;
@@ -45,6 +46,42 @@ pub async fn models_duplicate<H: Host>(host: H, req: ModelsDuplicateReq) -> Resu
})?)
}
/// Capture the request's current content, from an edit-session boundary the
/// frontend can see: switching away, losing focus, closing, or falling idle.
///
/// The frontend does not track whether anything actually changed — versions are
/// content-addressed, so an unchanged request returns the version it already
/// had and the trigger code stays a one-liner.
pub async fn models_snapshot_request<H: Host>(
host: H,
req: ModelsSnapshotRequestReq,
) -> Result<ModelVersion> {
Ok(host.db().snapshot_request_by_id(&req.request_id, req.reason)?)
}
/// A version and the live request side by side, for the diff and for deciding
/// whether there is anything worth offering.
pub async fn models_request_version<H: Host>(
host: H,
req: ModelsRequestVersionReq,
) -> Result<RequestVersionComparison> {
let db = host.db();
let version = db.get_model_version(&req.version_id)?;
let current_document = version_document(&db.get_any_request(&version.model_id)?.to_value()?)?;
let differs = !db.request_matches_version(&version)?;
Ok(RequestVersionComparison { version, current_document, differs })
}
/// Returns the id of the request that was restored.
pub async fn models_restore_request_version<H: Host>(
host: H,
req: ModelsRestoreRequestVersionReq,
) -> Result<String> {
let source = host.update_source();
let restored = host.db().restore_request_version(&req.version_id, &source)?;
Ok(restored.id().to_string())
}
pub async fn models_websocket_events<H: Host>(
host: H,
req: ModelsWebsocketEventsReq,
+51
View File
@@ -139,6 +139,10 @@ export type GrpcConnection = {
state: GrpcConnectionState;
trailers: { [key in string]?: string };
url: string;
/**
* The request version this connection was opened from, when one was captured.
*/
versionId: string | null;
};
export type GrpcConnectionState = "initialized" | "connected" | "closed";
@@ -243,6 +247,10 @@ export type HttpResponse = {
state: HttpResponseState;
url: string;
version: string | null;
/**
* The request version this response was sent from, when one was captured.
*/
versionId: string | null;
};
export type HttpResponseEvent = {
@@ -382,6 +390,28 @@ export type ModelPayload = {
change: ModelChangeEvent;
};
export type ModelVersion = {
model: "model_version";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
/**
* The `model` field of the versioned model, eg. `http_request`.
*/
modelType: string;
modelId: string;
contentHash: string;
document: Record<string, any>;
reason: ModelVersionReason;
};
/**
* Why a version was captured. Not a UI label — the frontend decides how to
* phrase these — but it is what makes a history readable when debugging.
*/
export type ModelVersionReason = "send" | "switch" | "idle" | "restore" | "manual";
export type ParentAuthentication = {
authentication: Record<string, any>;
authenticationType: string | null;
@@ -425,6 +455,23 @@ export type ProxySetting =
export type ProxySettingAuth = { user: string; password: string };
/**
* One version, next to the request as it stands now.
*
* Both halves come from the same place so they are guaranteed comparable: the
* frontend renders them side by side, and `differs` is the same content-hash
* comparison the backend uses everywhere else rather than a second opinion
* formed in TypeScript.
*/
export type RequestVersionComparison = {
version: ModelVersion;
/**
* The live request's editable content, in the same shape as the version's document.
*/
currentDocument: Record<string, any>;
differs: boolean;
};
export type Settings = {
model: "settings";
id: string;
@@ -488,6 +535,10 @@ export type WebsocketConnection = {
state: WebsocketConnectionState;
status: number;
url: string;
/**
* The request version this connection was opened from, when one was captured.
*/
versionId: string | null;
};
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
+17
View File
@@ -3285,6 +3285,23 @@ impl UpsertModelInfo for ModelVersion {
}
}
/// One version, next to the request as it stands now.
///
/// Both halves come from the same place so they are guaranteed comparable: the
/// frontend renders them side by side, and `differs` is the same content-hash
/// comparison the backend uses everywhere else rather than a second opinion
/// formed in TypeScript.
#[derive(Debug, Clone, Serialize, Deserialize, TS)]
#[serde(rename_all = "camelCase")]
#[ts(export, export_to = "gen_models.ts")]
pub struct RequestVersionComparison {
pub version: ModelVersion,
/// The live request's editable content, in the same shape as the version's document.
#[ts(type = "Record<string, any>")]
pub current_document: Value,
pub differs: bool,
}
/// Only used as a `from_row` fallback for an unparseable settings column. The
/// value a *new* model gets comes from that model's `Default` impl.
fn default_request_message_size_setting() -> InheritedIntSetting {
+62 -8
View File
@@ -33,13 +33,46 @@ pub fn version_document<T: Serialize>(model: &T) -> Result<Value> {
}
/// The hash a version is addressed by.
///
/// Canonical by construction: `serde_json::Map` is a `BTreeMap` here, so
/// serialization already visits keys in sorted order and two documents that
/// differ only in key order hash the same.
pub fn content_hash(document: &Value) -> Result<String> {
let canonical = serde_json::to_vec(document)?;
Ok(hex::encode(Sha256::digest(&canonical)))
let mut canonical = String::new();
write_canonical(document, &mut canonical);
Ok(hex::encode(Sha256::digest(canonical.as_bytes())))
}
/// Serialize with object keys in sorted order.
///
/// Plain `to_string` would not do: whether `serde_json::Map` preserves
/// insertion order or sorts is a workspace-wide feature decision, and a
/// document read back from SQLite has whatever order it was written in. Sorting
/// here makes the hash depend on the content and nothing else, in every build.
fn write_canonical(value: &Value, out: &mut String) {
match value {
Value::Object(map) => {
let mut keys = map.keys().collect::<Vec<_>>();
keys.sort_unstable();
out.push('{');
for (i, key) in keys.into_iter().enumerate() {
if i > 0 {
out.push(',');
}
write_canonical(&Value::String(key.clone()), out);
out.push(':');
write_canonical(&map[key], out);
}
out.push('}');
}
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
scalar => out.push_str(&scalar.to_string()),
}
}
/// Lay a version's document back over a live model.
@@ -142,8 +175,10 @@ mod tests {
);
}
/// The hash has to survive being written to and read back from the
/// database, which does not preserve key order.
/// The hash has to survive a round trip through SQLite, which stores the
/// document as text and hands back whatever order it was written in. It
/// also has to survive `serde_json`'s `preserve_order` feature being on in
/// one build of the workspace and off in another.
#[test]
fn key_order_does_not_change_the_hash() {
let a: Value = serde_json::from_str(r#"{"url":"a","method":"GET"}"#).unwrap();
@@ -151,6 +186,25 @@ mod tests {
assert_eq!(content_hash(&a).unwrap(), content_hash(&b).unwrap());
}
#[test]
fn key_order_does_not_change_the_hash_when_nested() {
let a: Value =
serde_json::from_str(r#"{"body":{"text":"x","type":"json"},"headers":[{"a":1,"b":2}]}"#)
.unwrap();
let b: Value =
serde_json::from_str(r#"{"headers":[{"b":2,"a":1}],"body":{"type":"json","text":"x"}}"#)
.unwrap();
assert_eq!(content_hash(&a).unwrap(), content_hash(&b).unwrap());
}
/// Sorting keys must not make different documents collide.
#[test]
fn array_order_still_changes_the_hash() {
let a: Value = serde_json::from_str(r#"{"headers":[{"n":"a"},{"n":"b"}]}"#).unwrap();
let b: Value = serde_json::from_str(r#"{"headers":[{"n":"b"},{"n":"a"}]}"#).unwrap();
assert_ne!(content_hash(&a).unwrap(), content_hash(&b).unwrap());
}
#[test]
fn applying_a_document_keeps_the_live_model_identity() {
let live = serde_json::to_value(request()).unwrap();
+25
View File
@@ -11,6 +11,7 @@ export type AnyModel =
| HttpRequest
| HttpResponse
| HttpResponseEvent
| ImportSource
| KeyValue
| Plugin
| Settings
@@ -137,6 +138,10 @@ export type GrpcConnection = {
state: GrpcConnectionState;
trailers: { [key in string]?: string };
url: string;
/**
* The request version this connection was opened from, when one was captured.
*/
versionId: string | null;
};
export type GrpcConnectionState = "initialized" | "connected" | "closed";
@@ -241,6 +246,10 @@ export type HttpResponse = {
state: HttpResponseState;
url: string;
version: string | null;
/**
* The request version this response was sent from, when one was captured.
*/
versionId: string | null;
};
export type HttpResponseEvent = {
@@ -318,6 +327,18 @@ export type HttpUrlParameter = {
export type HttpVersion = "auto" | "http1" | "http2";
export type ImportSource = {
model: "import_source";
id: string;
createdAt: string;
updatedAt: string;
workspaceId: string;
importer: string;
origin: string;
originLabel: string;
lastImportedAt: string;
};
export type InheritedBoolSetting = { enabled?: boolean; value: boolean };
export type InheritedHttpVersionSetting = { enabled?: boolean; value: HttpVersion };
@@ -417,6 +438,10 @@ export type WebsocketConnection = {
state: WebsocketConnectionState;
status: number;
url: string;
/**
* The request version this connection was opened from, when one was captured.
*/
versionId: string | null;
};
export type WebsocketConnectionState = "initialized" | "connected" | "closing" | "closed";
+46 -1
View File
@@ -32,12 +32,13 @@ use yaak_models::blob_manager::{BlobManager, BodyChunk};
use yaak_models::cookies::apply_cookie_changes;
use yaak_models::models::{
AnyModel, Cookie, CookieJar, HttpRequest, HttpResponseEvent, HttpResponseEventData,
HttpSendSettings,
HttpSendSettings, ModelVersionReason, RequestVersionComparison,
};
use yaak_models::models_ops;
use yaak_models::query_manager::QueryManager;
use yaak_models::render::render_http_request;
use yaak_models::util::{ModelPayload, UpdateSource};
use yaak_models::versions::version_document;
use yaak_templates::{RenderOptions, TemplateCallback};
/// Names inside the VFS, not paths on any disk. Two files because the desktop
@@ -218,6 +219,19 @@ struct UpsertIntrospectionReq {
content: Option<String>,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct SnapshotRequestReq {
request_id: String,
reason: ModelVersionReason,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct VersionIdReq {
version_id: String,
}
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
struct ResponseIdReq {
@@ -319,6 +333,37 @@ fn dispatch(
to_json(id)
}
"models_snapshot_request" => {
let req: SnapshotRequestReq = from_js(payload)?;
to_json(
host.queries
.connect()
.snapshot_request_by_id(&req.request_id, req.reason)
.map_err(js_error)?,
)
}
"models_request_version" => {
let req: VersionIdReq = from_js(payload)?;
let db = host.queries.connect();
let version = db.get_model_version(&req.version_id).map_err(js_error)?;
let request = db.get_any_request(&version.model_id).map_err(js_error)?;
let current_document =
version_document(&request.to_value().map_err(js_error)?).map_err(js_error)?;
let differs = !db.request_matches_version(&version).map_err(js_error)?;
to_json(RequestVersionComparison { version, current_document, differs })
}
"models_restore_request_version" => {
let req: VersionIdReq = from_js(payload)?;
let restored = host
.queries
.connect()
.restore_request_version(&req.version_id, source)
.map_err(js_error)?;
to_json(restored.id().to_string())
}
"models_get_settings" => to_json(host.queries.connect().get_settings()),
"models_get_graphql_introspection" => {
+26 -5
View File
@@ -63,6 +63,10 @@ const HANDLERS: Partial<Record<AppCmd, Handler>> = {
db.rpc("models_get_graphql_introspection", payload),
models_upsert_graphql_introspection: (payload, db) =>
db.rpc("models_upsert_graphql_introspection", payload),
models_snapshot_request: (payload, db) => db.rpc("models_snapshot_request", payload),
models_request_version: (payload, db) => db.rpc("models_request_version", payload),
models_restore_request_version: (payload, db) =>
db.rpc("models_restore_request_version", payload),
models_grpc_events: (payload, db) => db.rpc("models_grpc_events", payload),
models_websocket_events: (payload, db) => db.rpc("models_websocket_events", payload),
cmd_get_workspace_meta: (payload, db) => db.rpc("cmd_get_workspace_meta", payload),
@@ -76,7 +80,12 @@ const HANDLERS: Partial<Record<AppCmd, Handler>> = {
cmd_send_http_request: (payload, db) => {
const requestId = str(payload, "requestId");
if (requestId == null) throw new Error("cmd_send_http_request needs a requestId");
return sendHttpRequest(db, requestId, str(payload, "environmentId"), str(payload, "cookieJarId"));
return sendHttpRequest(
db,
requestId,
str(payload, "environmentId"),
str(payload, "cookieJarId"),
);
},
/* -------------------------------- app ---------------------------------- */
@@ -262,10 +271,16 @@ const DECLINED: Partial<Record<AppCmd, [reason: string, capability: CapabilityNa
cmd_ws_connect: ["WebSocket requests aren't available in the browser yet", "websocket"],
cmd_ws_send: ["WebSocket requests aren't available in the browser yet", "websocket"],
cmd_ws_close: ["WebSocket requests aren't available in the browser yet", "websocket"],
cmd_ws_delete_connections: ["WebSocket requests aren't available in the browser yet", "websocket"],
cmd_ws_delete_connections: [
"WebSocket requests aren't available in the browser yet",
"websocket",
],
// Anything that needs files the page can't reach.
cmd_import_data: ["Importing from a file needs a filesystem, which a browser tab has no", "localFiles"],
cmd_import_data: [
"Importing from a file needs a filesystem, which a browser tab has no",
"localFiles",
],
cmd_import_url: ["Importing from a URL needs the Yaak server, which isn't available yet", null],
cmd_commit_import: ["Importing needs a plugin, which this host doesn't run", null],
cmd_list_import_sources: ["Importing isn't available in the browser yet", null],
@@ -298,8 +313,14 @@ const DECLINED: Partial<Record<AppCmd, [reason: string, capability: CapabilityNa
cmd_plugins_uninstall: ["Plugins aren't available in the browser yet", "plugins"],
cmd_plugins_updates: ["Plugins aren't available in the browser yet", "plugins"],
cmd_plugins_update_all: ["Plugins aren't available in the browser yet", "plugins"],
cmd_template_function_config: ["Template functions come from plugins, which this host doesn't run", "plugins"],
cmd_template_tokens_to_string: ["Template functions come from plugins, which this host doesn't run", "plugins"],
cmd_template_function_config: [
"Template functions come from plugins, which this host doesn't run",
"plugins",
],
cmd_template_tokens_to_string: [
"Template functions come from plugins, which this host doesn't run",
"plugins",
],
cmd_call_http_request_action: ["Plugins aren't available in the browser yet", "plugins"],
cmd_call_websocket_request_action: ["Plugins aren't available in the browser yet", "plugins"],
cmd_call_grpc_request_action: ["Plugins aren't available in the browser yet", "plugins"],
+41 -3
View File
@@ -30,6 +30,7 @@ import type {
HttpResponse,
HttpResponseEventData,
HttpSendSettings,
ModelVersion,
} from "@yaakapp-internal/models";
import type { Frame, SendRequest } from "@yaakapp-internal/web";
import type { WorkerConnection } from "./connection";
@@ -71,7 +72,13 @@ export async function sendHttpRequest(
// a failure to render or to reach the server lands in the response pane as
// that response's error rather than as a toast that names no request.
const workspaceId = await workspaceIdOfRequest(db, requestId);
const response = new ResponseWriter(db, { model: "http_response", requestId, workspaceId });
const versionId = await snapshotRequestVersion(db, requestId);
const response = new ResponseWriter(db, {
model: "http_response",
requestId,
workspaceId,
versionId,
});
await response.create();
const cancel = new AbortController();
@@ -88,6 +95,29 @@ export async function sendHttpRequest(
return response.current();
}
/**
* Capture what is about to be sent, so the response can offer it back later.
* The desktop does this inside its send pipeline; this host's pipeline is here,
* so this is where it goes. Versions are content-addressed, so repeated sends
* of an unchanged request all point at the same one.
*/
async function snapshotRequestVersion(
db: WorkerConnection,
requestId: string,
): Promise<string | undefined> {
try {
const version = await db.rpc<ModelVersion>("models_snapshot_request", {
requestId,
reason: "send",
});
return version.id;
} catch (err) {
// History is not worth failing a send over
console.warn("Failed to snapshot request version", err);
return undefined;
}
}
async function runSend(
db: WorkerConnection,
response: ResponseWriter,
@@ -315,8 +345,16 @@ class TimelineWriter {
* yaak-models), so an edit made while the send was in flight survives rather
* than being written over by the send's stale snapshot.
*/
async function persistCookies(db: WorkerConnection, jar: CookieJar, cookies: Cookie[]): Promise<void> {
await db.rpc("web_persist_send_cookies", { cookieJarId: jar.id, before: jar.cookies, after: cookies });
async function persistCookies(
db: WorkerConnection,
jar: CookieJar,
cookies: Cookie[],
): Promise<void> {
await db.rpc("web_persist_send_cookies", {
cookieJarId: jar.id,
before: jar.cookies,
after: cookies,
});
}
/**