mirror of
https://github.com/mountain-loop/yaak.git
synced 2026-08-18 01:15:12 +02:00
Generate the proxy wire types with ts-rs (@yaakapp-internal/send-proxy)
This commit is contained in:
Generated
+1
@@ -11689,6 +11689,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"tokio",
|
||||
"tower-http",
|
||||
"ts-rs",
|
||||
"url",
|
||||
"uuid",
|
||||
"yaak-http",
|
||||
|
||||
@@ -29,6 +29,7 @@ serde = { workspace = true, features = ["derive"] }
|
||||
serde_json = { workspace = true }
|
||||
tokio = { workspace = true, features = ["rt-multi-thread", "macros", "signal", "sync", "io-util", "time", "net"] }
|
||||
tower-http = { version = "0.6", features = ["cors", "trace"] }
|
||||
ts-rs = { workspace = true }
|
||||
url = "2"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
yaak-http = { workspace = true }
|
||||
|
||||
@@ -121,6 +121,11 @@ construction, cancellable by closing the connection, readable with `curl`, and
|
||||
needs no upgrade handling on either side. A WebSocket only earns its keep when
|
||||
traffic is bidirectional, which a single send is not.
|
||||
|
||||
The TypeScript side of this contract is generated from `src/wire.rs` by ts-rs
|
||||
into `bindings/` (run `cargo test -p yaak-send-proxy` after changing a frame)
|
||||
and published to the tab as `@yaakapp-internal/send-proxy`, so a change to the
|
||||
wire on one side is a type error on the other.
|
||||
|
||||
`GET /v1/health` reports the version and the effective limits.
|
||||
|
||||
## What comes later
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
|
||||
export type Cookie = { name: string, value: string, domain: CookieDomain, expires: CookieExpires, path: string, secure: boolean, httpOnly: boolean, sameSite: CookieSameSite | null, };
|
||||
|
||||
export type CookieDomain = { "HostOnly": string } | { "Suffix": string } | "NotPresent" | "Empty";
|
||||
|
||||
export type CookieExpires = { "AtUtc": string } | "SessionEnd";
|
||||
|
||||
export type CookieSameSite = "Strict" | "Lax" | "None";
|
||||
|
||||
export type HttpRequest = { model: "http_request", id: string, createdAt: string, updatedAt: string, workspaceId: string, folderId: string | null, authentication: Record<string, any>, authenticationType: string | null, body: Record<string, any>, bodyType: string | null, description: string, headers: Array<HttpRequestHeader>, method: string, name: string, sortPriority: number, url: string,
|
||||
/**
|
||||
* URL parameters used for both path placeholders (`:id`) and query string entries.
|
||||
*/
|
||||
urlParameters: Array<HttpUrlParameter>, settingSendCookies: InheritedBoolSetting, settingStoreCookies: InheritedBoolSetting, settingValidateCertificates: InheritedBoolSetting, settingFollowRedirects: InheritedBoolSetting, settingRequestTimeout: InheritedIntSetting, };
|
||||
|
||||
export type HttpRequestHeader = { enabled?: boolean, name: string, value: string, id?: string, };
|
||||
|
||||
/**
|
||||
* Serializable representation of HTTP response events for DB storage.
|
||||
* This mirrors `yaak_http::sender::HttpResponseEvent` but with serde support.
|
||||
* The `From` impl is in yaak-http to avoid circular dependencies.
|
||||
*/
|
||||
export type HttpResponseEventData = { "type": "setting", name: string, value: string, source_model?: string, source_id?: string, source_name?: string, } | { "type": "info", message: string, } | { "type": "redirect", url: string, status: number, behavior: string, dropped_body: boolean, dropped_headers: Array<string>, } | { "type": "send_url", method: string, scheme: string, username: string, password: string, host: string, port: number, path: string, query: string, fragment: string, } | { "type": "receive_url", version: string, status: string, } | { "type": "header_up", name: string, value: string, } | { "type": "header_down", name: string, value: string, } | { "type": "chunk_sent", bytes: number, } | { "type": "chunk_received", bytes: number, } | { "type": "dns_resolved", hostname: string, addresses: Array<string>, duration: bigint, overridden: boolean, };
|
||||
|
||||
export type HttpResponseHeader = { name: string, value: string, };
|
||||
|
||||
/**
|
||||
* The resolved send settings, values only: what an executor has to obey, with the sources
|
||||
* (which model each came from) left behind in [`ResolvedHttpRequestSettings`]. This is what
|
||||
* crosses from a tab to the send proxy, and what the proxy reads.
|
||||
*/
|
||||
export type HttpSendSettings = { validateCertificates: boolean, followRedirects: boolean,
|
||||
/**
|
||||
* Milliseconds. Zero or negative means no timeout.
|
||||
*/
|
||||
timeoutMs: number, sendCookies: boolean, storeCookies: boolean, };
|
||||
|
||||
export type HttpUrlParameter = { enabled?: boolean,
|
||||
/**
|
||||
* Colon-prefixed parameters are treated as path parameters if they match, like `/users/:id`
|
||||
* Other entries are appended as query parameters
|
||||
*/
|
||||
name: string, value: string, id?: string, };
|
||||
|
||||
export type InheritedBoolSetting = { enabled?: boolean, value: boolean, };
|
||||
|
||||
export type InheritedIntSetting = { enabled?: boolean, value: number, };
|
||||
@@ -0,0 +1,64 @@
|
||||
// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually.
|
||||
import type { Cookie, HttpRequest, HttpResponseEventData, HttpResponseHeader, HttpSendSettings } from "./gen_models";
|
||||
|
||||
/**
|
||||
* One line of the reply stream. Tags are snake_case like the timeline event tags; fields are
|
||||
* camelCase like every model the tab stores.
|
||||
*/
|
||||
export type Frame = { "type": "event", event: HttpResponseEventData, } | { "type": "response", status: number, statusReason: string | null,
|
||||
/**
|
||||
* The URL that answered, after redirects.
|
||||
*/
|
||||
url: string, remoteAddr: string | null, version: string | null, headers: Array<HttpResponseHeader>,
|
||||
/**
|
||||
* The headers that were actually sent on the final hop, cookies and all.
|
||||
*/
|
||||
requestHeaders: Array<HttpResponseHeader>,
|
||||
/**
|
||||
* `Content-Length` as declared by the server, if it declared one.
|
||||
*/
|
||||
contentLength: number | null,
|
||||
/**
|
||||
* Milliseconds from the start of the send to the response head.
|
||||
*/
|
||||
elapsedHeaders: number,
|
||||
/**
|
||||
* Milliseconds spent in DNS on the last lookup, or zero.
|
||||
*/
|
||||
elapsedDns: number, } | { "type": "body", data: string, } | { "type": "done",
|
||||
/**
|
||||
* Milliseconds from the start of the send to the end of the body.
|
||||
*/
|
||||
elapsed: number,
|
||||
/**
|
||||
* Bytes of body relayed, after decompression.
|
||||
*/
|
||||
contentLength: number,
|
||||
/**
|
||||
* Bytes on the wire as declared by the server, or the relayed size when unknown.
|
||||
*/
|
||||
contentLengthCompressed: number,
|
||||
/**
|
||||
* The jar as the send left it, for the tab to persist. `None` when the tab sent none.
|
||||
*/
|
||||
cookies: Array<Cookie> | null, } | { "type": "error", message: string, cookies: Array<Cookie> | null, };
|
||||
|
||||
/**
|
||||
* The body of `POST /v1/http/send`.
|
||||
*/
|
||||
export type SendRequest = {
|
||||
/**
|
||||
* The request to send, in the desktop's own model shape but with every template already
|
||||
* rendered by the tab. The proxy builds the URL, headers and body from it exactly the way
|
||||
* the desktop does after rendering.
|
||||
*/
|
||||
request: HttpRequest,
|
||||
/**
|
||||
* The resolved settings, values only. Where they came from is the tab's to record in
|
||||
* its timeline; the proxy only needs to obey them.
|
||||
*/
|
||||
settings: HttpSendSettings,
|
||||
/**
|
||||
* The cookies to start with. `None` means no jar at all: nothing sent, nothing kept.
|
||||
*/
|
||||
cookies: Array<Cookie> | null, };
|
||||
@@ -0,0 +1,4 @@
|
||||
// The send proxy's wire contract, generated by ts-rs from src/wire.rs
|
||||
// (`cargo test -p yaak-send-proxy`). The tab imports these so a change to a
|
||||
// frame on the Rust side is a type error in packages/platform/src/web.
|
||||
export type { Frame, SendRequest } from "./bindings/gen_send_proxy";
|
||||
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"name": "@yaakapp-internal/send-proxy",
|
||||
"version": "1.0.0",
|
||||
"private": true,
|
||||
"main": "index.ts"
|
||||
}
|
||||
@@ -9,15 +9,21 @@
|
||||
//! Nothing here names a workspace, a request id, or a response id. The proxy
|
||||
//! does not know what the tab will call this response; it only knows what came
|
||||
//! back.
|
||||
//!
|
||||
//! The TypeScript side of this contract is generated from these types into
|
||||
//! `bindings/` (`cargo test -p yaak-send-proxy`) and published to the tab as
|
||||
//! `@yaakapp-internal/send-proxy`, so a change here is a type error there.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ts_rs::TS;
|
||||
use yaak_models::models::{
|
||||
Cookie, HttpRequest, HttpResponseEventData, HttpResponseHeader, HttpSendSettings,
|
||||
};
|
||||
|
||||
/// The body of `POST /v1/http/send`.
|
||||
#[derive(Deserialize, Debug)]
|
||||
#[derive(Deserialize, Debug, TS)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
#[ts(export, export_to = "gen_send_proxy.ts")]
|
||||
pub struct SendRequest {
|
||||
/// The request to send, in the desktop's own model shape but with every template already
|
||||
/// rendered by the tab. The proxy builds the URL, headers and body from it exactly the way
|
||||
@@ -33,12 +39,13 @@ pub struct SendRequest {
|
||||
|
||||
/// One line of the reply stream. Tags are snake_case like the timeline event tags; fields are
|
||||
/// camelCase like every model the tab stores.
|
||||
#[derive(Serialize, Debug)]
|
||||
#[derive(Serialize, Debug, TS)]
|
||||
#[serde(
|
||||
tag = "type",
|
||||
rename_all = "snake_case",
|
||||
rename_all_fields = "camelCase"
|
||||
)]
|
||||
#[ts(export, export_to = "gen_send_proxy.ts")]
|
||||
pub enum Frame {
|
||||
/// A timeline event, in the same shape the desktop stores. Interleaved with everything
|
||||
/// else in the order the engine produced it.
|
||||
@@ -56,10 +63,13 @@ pub enum Frame {
|
||||
/// The headers that were actually sent on the final hop, cookies and all.
|
||||
request_headers: Vec<HttpResponseHeader>,
|
||||
/// `Content-Length` as declared by the server, if it declared one.
|
||||
#[ts(type = "number | null")]
|
||||
content_length: Option<u64>,
|
||||
/// Milliseconds from the start of the send to the response head.
|
||||
#[ts(type = "number")]
|
||||
elapsed_headers: u64,
|
||||
/// Milliseconds spent in DNS on the last lookup, or zero.
|
||||
#[ts(type = "number")]
|
||||
elapsed_dns: u64,
|
||||
},
|
||||
/// A piece of the response body, decompressed, base64-encoded.
|
||||
@@ -67,10 +77,13 @@ pub enum Frame {
|
||||
/// The send finished. The last frame on a successful stream.
|
||||
Done {
|
||||
/// Milliseconds from the start of the send to the end of the body.
|
||||
#[ts(type = "number")]
|
||||
elapsed: u64,
|
||||
/// Bytes of body relayed, after decompression.
|
||||
#[ts(type = "number")]
|
||||
content_length: u64,
|
||||
/// Bytes on the wire as declared by the server, or the relayed size when unknown.
|
||||
#[ts(type = "number")]
|
||||
content_length_compressed: u64,
|
||||
/// The jar as the send left it, for the tab to persist. `None` when the tab sent none.
|
||||
cookies: Option<Vec<Cookie>>,
|
||||
|
||||
Generated
+10
@@ -70,6 +70,7 @@
|
||||
"crates/yaak-web",
|
||||
"crates/yaak-ws",
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
"crates-server/yaak-send-proxy",
|
||||
"apps/yaak-client",
|
||||
"apps/yaak-proxy"
|
||||
],
|
||||
@@ -272,6 +273,10 @@
|
||||
"name": "@yaakapp-internal/proxy-lib",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"crates-server/yaak-send-proxy": {
|
||||
"name": "@yaakapp-internal/send-proxy",
|
||||
"version": "1.0.0"
|
||||
},
|
||||
"crates-tauri/yaak-app-client": {
|
||||
"name": "@yaakapp-internal/tauri-client",
|
||||
"version": "1.0.0"
|
||||
@@ -5645,6 +5650,10 @@
|
||||
"resolved": "crates/common/yaak-rpc-schema",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/send-proxy": {
|
||||
"resolved": "crates-server/yaak-send-proxy",
|
||||
"link": true
|
||||
},
|
||||
"node_modules/@yaakapp-internal/sse": {
|
||||
"resolved": "crates/yaak-sse",
|
||||
"link": true
|
||||
@@ -15801,6 +15810,7 @@
|
||||
"@tauri-apps/plugin-os": "^2.3.2",
|
||||
"@yaakapp-internal/models": "^1.0.0",
|
||||
"@yaakapp-internal/rpc-schema": "^1.0.0",
|
||||
"@yaakapp-internal/send-proxy": "^1.0.0",
|
||||
"@yaakapp-internal/web": "^1.0.0"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -69,6 +69,7 @@
|
||||
"crates/yaak-web",
|
||||
"crates/yaak-ws",
|
||||
"crates-proxy/yaak-proxy-lib",
|
||||
"crates-server/yaak-send-proxy",
|
||||
"apps/yaak-client",
|
||||
"apps/yaak-proxy"
|
||||
],
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
"dependencies": {
|
||||
"@yaakapp-internal/models": "^1.0.0",
|
||||
"@yaakapp-internal/rpc-schema": "^1.0.0",
|
||||
"@yaakapp-internal/send-proxy": "^1.0.0",
|
||||
"@yaakapp-internal/web": "^1.0.0",
|
||||
"@tauri-apps/api": "^2.11.0",
|
||||
"@tauri-apps/plugin-clipboard-manager": "^2.3.2",
|
||||
|
||||
@@ -1,19 +1,11 @@
|
||||
/**
|
||||
* The wire to the send proxy: where it is, what goes up, and what comes back.
|
||||
* The wire to the send proxy: where it is, and how to read what comes back.
|
||||
*
|
||||
* These shapes mirror `crates-server/yaak-send-proxy/src/wire.rs` by hand. The
|
||||
* proxy is a separate binary with its own release cadence, so the contract is
|
||||
* written down on both sides rather than generated across them; a change to one
|
||||
* is a change to the other, and the frame `type` tags are the versioning.
|
||||
* The shapes themselves are generated from `crates-server/yaak-send-proxy/src/wire.rs`
|
||||
* into `@yaakapp-internal/send-proxy`, so the two sides cannot drift silently.
|
||||
*/
|
||||
|
||||
import type {
|
||||
Cookie,
|
||||
HttpRequest,
|
||||
HttpResponseEventData,
|
||||
HttpResponseHeader,
|
||||
HttpSendSettings,
|
||||
} from "@yaakapp-internal/models";
|
||||
import type { Frame } from "@yaakapp-internal/send-proxy";
|
||||
|
||||
/* ------------------------------- location -------------------------------- */
|
||||
|
||||
@@ -33,54 +25,12 @@ export function proxySendUrl(): string {
|
||||
return `${proxyBaseUrl()}/v1/http/send`;
|
||||
}
|
||||
|
||||
/* --------------------------------- up ------------------------------------ */
|
||||
|
||||
/** The body of `POST /v1/http/send`. */
|
||||
export interface ProxyRequestBody {
|
||||
/** The rendered request, in the model shape (see `wire.rs` `SendRequest.request`). */
|
||||
request: HttpRequest;
|
||||
settings: HttpSendSettings;
|
||||
/** The jar's cookies to start from, or `null` for no jar at all. */
|
||||
cookies: Cookie[] | null;
|
||||
}
|
||||
|
||||
/* -------------------------------- down ----------------------------------- */
|
||||
|
||||
export interface ProxySendResponse {
|
||||
type: "response";
|
||||
status: number;
|
||||
statusReason: string | null;
|
||||
url: string;
|
||||
remoteAddr: string | null;
|
||||
version: string | null;
|
||||
headers: HttpResponseHeader[];
|
||||
requestHeaders: HttpResponseHeader[];
|
||||
contentLength: number | null;
|
||||
elapsedHeaders: number;
|
||||
elapsedDns: number;
|
||||
}
|
||||
|
||||
export type ProxyFrame =
|
||||
/** A timeline event in the `http_response_event.event` shape. */
|
||||
| { type: "event"; event: HttpResponseEventData }
|
||||
| ProxySendResponse
|
||||
/** A body chunk, decompressed, base64. */
|
||||
| { type: "body"; data: string }
|
||||
| {
|
||||
type: "done";
|
||||
elapsed: number;
|
||||
contentLength: number;
|
||||
contentLengthCompressed: number;
|
||||
cookies: Cookie[] | null;
|
||||
}
|
||||
| { type: "error"; message: string; cookies: Cookie[] | null };
|
||||
|
||||
/**
|
||||
* Yield frames from an NDJSON stream as they arrive. A partial trailing line is
|
||||
* held until its newline comes; anything left when the stream ends is dropped,
|
||||
* because a frame without its newline is a frame the proxy didn't finish writing.
|
||||
*/
|
||||
export async function* readFrames(stream: ReadableStream<Uint8Array>): AsyncGenerator<ProxyFrame> {
|
||||
export async function* readFrames(stream: ReadableStream<Uint8Array>): AsyncGenerator<Frame> {
|
||||
const reader = stream.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let buffer = "";
|
||||
@@ -93,7 +43,7 @@ export async function* readFrames(stream: ReadableStream<Uint8Array>): AsyncGene
|
||||
while (newline !== -1) {
|
||||
const line = buffer.slice(0, newline);
|
||||
buffer = buffer.slice(newline + 1);
|
||||
if (line.trim() !== "") yield JSON.parse(line) as ProxyFrame;
|
||||
if (line.trim() !== "") yield JSON.parse(line) as Frame;
|
||||
newline = buffer.indexOf("\n");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -31,8 +31,8 @@ import type {
|
||||
HttpResponseEventData,
|
||||
HttpSendSettings,
|
||||
} from "@yaakapp-internal/models";
|
||||
import type { Frame, SendRequest } from "@yaakapp-internal/send-proxy";
|
||||
import type { WorkerConnection } from "./connection";
|
||||
import type { ProxyFrame, ProxyRequestBody, ProxySendResponse } from "./proxy";
|
||||
import { proxySendUrl, readFrames } from "./proxy";
|
||||
|
||||
/* -------------------------------- shapes --------------------------------- */
|
||||
@@ -106,7 +106,7 @@ async function runSend(
|
||||
const timeline = new TimelineWriter(db, response.id, response.workspaceId);
|
||||
timeline.push(prepared.settingEvents);
|
||||
|
||||
const body: ProxyRequestBody = {
|
||||
const body: SendRequest = {
|
||||
request: prepared.request,
|
||||
settings: prepared.settings,
|
||||
cookies: prepared.cookieJar?.cookies ?? null,
|
||||
@@ -139,7 +139,7 @@ async function runSend(
|
||||
const chunks: Uint8Array[] = [];
|
||||
let received = 0;
|
||||
let lastProgress = startedAt;
|
||||
let terminal: ProxyFrame | null = null;
|
||||
let terminal: Frame | null = null;
|
||||
|
||||
for await (const frame of readFrames(res.body)) {
|
||||
switch (frame.type) {
|
||||
@@ -201,7 +201,7 @@ async function runSend(
|
||||
});
|
||||
}
|
||||
|
||||
function headOf(frame: ProxySendResponse): ResponsePatch {
|
||||
function headOf(frame: Extract<Frame, { type: "response" }>): ResponsePatch {
|
||||
return {
|
||||
state: "connected",
|
||||
status: frame.status,
|
||||
|
||||
Reference in New Issue
Block a user