S1: Split into a crate workspace and expose a Rust client API - #96
Open
passcod wants to merge 60 commits into
Open
S1: Split into a crate workspace and expose a Rust client API#96passcod wants to merge 60 commits into
passcod wants to merge 60 commits into
Conversation
Move the crate to crates/web-faith-napi and turn the root manifest into a workspace, so the component crates and the Rust client have somewhere to land. Shared package metadata (version, edition, MSRV 1.96, licence, repository, authors) and the dependency versions move to the root for the crates to inherit. The lib keeps the name `faith` so the built artifact stays libfaith.so, which the release workflow's cross-compile steps copy by name. build.rs walked to "Cargo.lock" relative to the crate; under a workspace the lock file sits at the root, so it now searches the ancestor directories. The benchmark HTTP/3 server is excluded rather than made a member: it keeps its own lockfile on purpose so the quinn/h3 stack stays out of this graph. No behaviour change; the generated index.js and index.d.ts are unchanged.
FaithError and FaithErrorKind were defined on a napi string_enum and carried napi conversions, so every module that reported an error was tied to a JavaScript runtime. The kinds and the error now live in web-faith, which builds and documents with no napi in its dependency graph, and the Node surface keeps what is genuinely its own: the mapping from a kind to a JavaScript error class, and turning an error into a value V8 can carry. The napi conversions arrive as an extension trait rather than inherent methods, the error no longer being this crate's type to add methods to. errorCodes() reads the codes from the kinds themselves, so the exported ERROR_CODES map still cannot drift from the errors. This drops FaithErrorKind from the native binding: it was emitted only because the enum carried napi's attribute, and the package's export map admits nothing but the wrapper, so no caller could reach it. The documented surface is wrapper.js's ERROR_CODES, which is unchanged, still holds all 22 codes, and is still checked against errorCodes() by test/error-codes.test.js.
Subresource Integrity moves to a crate of its own, which builds, tests, and documents with no JavaScript runtime in its graph and speaks only ssri. It names its own error for the two failures it can produce, and web-faith converts them at the boundary, so a caller still matches on one error type and the codes reported for a bad value and a mismatch are unchanged. The client gains an `integrity` feature, on by default, which drops the dependency when turned off. The nine tests move with the code.
Content coding moves to a crate of its own. It reached into the client for the body-stream type; it now names that shape itself, so the dependency no longer points upward, and it takes header types from http rather than through reqwest, which drops reqwest from its graph entirely. Nothing about the codings, the Accept-Encoding default, or the decode decision changes. The twenty tests move with the code.
The jar moves to a crate of its own. It took reqwest's Url and HeaderValue, which are the url and http types under another name, so it now names them directly, and the reqwest CookieStore impl sits behind a `reqwest` feature. That impl held the only way to store a response's cookies or read the header to send, so both are now inherent methods and the trait delegates to them: a caller without reqwest gets a jar that works rather than one it cannot drive. The crate's own tests exercise those methods, so they pass with no reqwest in the graph at all. The storage, matching, and eviction rules are unchanged.
The resolver moves to a crate of its own: the cache, the discovery ladder, the HTTPS record query, and Happy Eyeballs. It had no ties to the rest of the code at all, so the only coupling to break was reqwest's Resolve impl, which now sits behind a `reqwest` feature the way the jar's does. The resolver itself, its transports, and the server order are unchanged.
Reading the operating system's per-connection statistics moves to a crate of its own, along with the Linux, macOS, and Windows implementations behind it. This was the one component holding napi types: the view it returned was a napi object with JavaScript Date fields, so the tracker could not be compiled away from a JS runtime. It now reports a snapshot of plain SystemTime and integer values, and the napi crate turns that into the object `connections()` returns. The JavaScript surface is unchanged, Dates and all.
The Alt-Svc store and the HTTP/3 upgrade machinery move to a crate of its own, which draws on web-faith-dns for the resolution its probes need. It read the client's timing stamp out of the request extensions, so it needed a type from the layer above. It now names what it wants of that type as an ArrivalStamp trait and the middleware is generic over it: the stamp stays the client's, which is what reads it back to surface the timing, and this layer keeps being the one place a response's arrival is observed. The crate is pulled in by the http3 feature, so a build without HTTP/3 does not carry it. Both feature configurations build clean. This is the last of the six components; every one of them now compiles and tests with no JavaScript runtime in its graph.
The first of the client's own code to leave the binding crate. The body holder and its streams, the timing slot and what it measures, and the two retry layers in the request path were already free of napi, so they move as they are. Timing splits: the measuring is the client's, while the shape JavaScript receives it in stays behind as a napi object built from it. The client gains the http3 feature, since the stamp it owns is marked by the Alt-Svc layer, and the binding's own http3 feature now turns the client's on with it. Both feature configurations build clean.
The recipe that builds the agent's reqwest clients, the Node environment variables it layers on, the flow-control windows, and the HTTP cache store move to the client crate. This code was already free of napi by design: the recipe exists because a client has to be buildable more than once for a network change, and AgentOptions could not serve because it carries values belonging to the JS call that passed them. The redirect choice becomes the client's own RedirectPolicy. It carries no `manual` variant, that having never differed from `follow` here, and the Node surface's enum maps onto it, so JavaScript still takes every value it did. web-faith is now depended on with default features off, so the binding's http3 feature turns the client's on rather than the two drifting apart. Both configurations build clean and warning-free.
Spec references move out of doc comments and into normal comments beside the item they belong to: a spec id is traceability for this repository, not documentation for someone reading the crate on docs.rs. No reference is lost. The crate-level docs said why Faith needed each piece factored out, which is the wrong subject now that each is published on its own. They say what the crate is for and how to drive it instead. The cookie jar leads with the rules it keeps that a browser-shaped model does not, and carries a worked example; the resolver leads with the warmable cache; the Alt-Svc crate had no crate documentation at all, having been a module, and now describes the upgrade decision it exists to make. Making the components public also exposed several doc links to items that stayed private, which would have shipped as broken pages. Two are fixed by publishing what the prose already treated as public: the path-time floor a documented factor is measured against, and reading a content coding off the wire. The rest name the behaviour rather than the private item. `cargo doc --workspace --no-deps` is now warning-free. One line of index.d.ts changes with it: napi emits doc comments into TypeScript, where Rust's intra-doc link syntax was only ever noise.
The Rust surface's variables were gathered into a section of their own, which meant reading a variable's semantics in one place and whether it applied to you in another. Each section now opens by naming the surfaces it applies to, and the two that carry a mix say where the line falls: both surfaces read the OpenSSL trust-store and proxy variables, while the NODE_-prefixed ones are the Node surface's alone. Nothing about what any variable does changes.
…spell them The compatibility stance mixed the stance itself with Node API detail: the shapes fetch accepts, the browser-assuming options that are ignored, and the extensions that are additive were each spelled out here as well as at their own site. All three are already specified in REQ and in the response specs, so what is left is the stance a reader comes to this file for. The claim that these specs describe behaviour in JavaScript terms is gone too. It licensed the wrong thing: a spec covering both surfaces should name the concept and link to where it is defined, and one that covers a single surface says so. Following that, the environment spec now names the agent's extra roots rather than spelling the Node option, which is defined in TLS either way.
…e surface's names
Warming a name and warming an origin both take their argument loosely: a host with a scheme or a port it does not need, a URL with a path and userinfo that play no part in the origin. The parsing that decides what was meant is pure, so it moves to the client with its eight tests, leaving the verbs to act on the result. The origin key here and the one the Alt-Svc cache derives are the same format arrived at twice; worth merging when the agent itself moves.
The running counters are the agent's, and a response holds a share of them because a body finishing is what settles two of the four. Both surfaces report them, so they move to the client, which reads them as the u64 they are counted in. The Node surface keeps its own shape, converting from that reading: JavaScript takes them as i64, and a count past that saturates rather than wrapping, exactly as before.
The agent moves to web-faith. What stays behind is the Node surface's own work: validating whatever a caller passed as AgentOptions into a recipe and settings, and a napi class holding a handle on the client's agent. The verbs are the client's now. close and network_changed were pure already and move as they were. The cookie verbs take a parsed URL rather than a string, since parsing what a caller wrote belongs to the surface that took it. prefetch_dns and preconnect return a future rather than reaching for a JS promise: a warm-up that cannot be attempted is refused before the future exists, which is what lets the binding throw synchronously for a caller's mistake while a network failure stays quiet. The client's agent also carries the settings a request consults, so the split is between what builds a client and what each request reads. The Node surface is unchanged. index.d.ts loses only the spec markers that had been leaking out of doc comments into the published TypeScript; every line of documentation a caller reads is intact.
Where trailers land, what is known of the peer, and opening a destination for a body write are all the response's own business rather than JavaScript's, so they move to the client with the two tests that guard the trailers wait against spinning. The file destination becomes the client's own shape, with the defaulting the JS options object leaves implicit made explicit; the napi object converts into it.
web_faith::response::Response holds the response's state and the reads over it: disturbing the body, gathering it, and writing it out to a file. The napi class is a handle on one, and each JS method delegates. Writing to a file takes a progress closure rather than a threadsafe function, and reports in the counts it measures; the binding hands it a closure that crosses into JavaScript, and converts the report and the result into the shapes JS takes. The file destination's defaults, which the options object left implicit, are explicit in the client's own type. The integrity dependency stops being optional. The client's read path verifies unconditionally, and gating the call sites would have meant a build that quietly skips verification -- where the spec asks for a feature to remove the API that offers it instead. That belongs with the request API in step 9, so the feature comes back then rather than standing as a trap now. index.d.ts loses one more spec marker that had been leaking into the published TypeScript. Nothing else about the Node surface changes.
Sending a request and building the response that comes back is the client's, so it moves: web_faith::request::send takes an agent, a URL, the options, a body, and an optional abort future. The Node surface reads a fetch() call into those and wraps the result. The body arrives as the client's own shape rather than a napi buffer and a shared receiver: bytes for one already in hand, a stream for one arriving in chunks. The binding still takes the receiver out of its lock before anything can refuse the request, so a refusal drops the stream and whatever is feeding it stops. Cancellation is a future the caller supplies, rather than napi's signal reaching into the send. `same-origin` credentials resolve to `include` at the boundary, that distinction needing an origin the client does not have.
The client's crate documentation now says what it holds, and two doc links that broke when the code moved are fixed, so `cargo doc --workspace` is clean again. Step 8 is done: web-faith holds the agent, the request path, the response and its reads, the body, timing, and retry machinery, and the client recipe. web-faith-napi is the binding it always should have been -- JS option shapes, AgentOptions validation, the napi classes wrapping the client's types, and napi's own promise, stream, and threadsafe-function machinery.
The counters' listing goes: it spelled the fields in the Node surface's casing, and the type it returns already documents each one with its Rust name and its type, which no prose list can better. AgentStats is non-exhaustive. What an agent counts can grow, and adding a counter should not be a breaking change for anyone reading them. The rest is what the moves carried in and I left: spec references sitting in doc comments rather than beside the item, and JavaScript option paths naming settings the client holds under its own names. The agent's fields now say what they govern rather than which JS option they mirror.
There is no good reason for a caller to turn digest verification off, and the component is small enough that leaving it out saves nothing worth measuring. What a feature would buy is a build that quietly skips a check the caller asked for. So integrity has no feature, and the specs say why: RUST no longer claims every component has one, and RSAPI no longer promises that `integrity()` disappears.
…feature Subresource Integrity is a module of `web-faith` rather than a crate beneath it, and its functions report the client's own error, so the boundary conversion goes with the boundary. The nine tests come along. The specs had been treating component, crate, and feature as one thing: RUST promised a feature per component crate, and RSAPI promised the API a component gated. They are three axes that need not line up. A feature now names a capability, may or may not drop a dependency when turned off, and a subsystem the client is not built without carries none at all. The reasoning about why integrity is always built goes with it. A spec says what the system is, and the system is one where SRI is part of the client.
The default `User-Agent` names the client and the version of reqwest a request actually goes out on, so the client is what should compose it. web-faith gains a build script to read that version from the lock file, and USER_AGENT with it. The Node surface's constant now reads from the client's, so the value on the wire has one source. It is unchanged: `Faith/0.7.0 reqwest/0.13.4`. RSAPI has the Rust surface export the string but not the versions inside it, a Rust caller having its own package metadata to read those from, so only USER_AGENT is public here.
A Rust caller reads a response through the response rather than through the binding: status, status_text, ok, headers, url, redirected, version, peer, and body_used as accessors, and bytes, text, json, body_stream, discard, and the file write as the reads over the body, which is the set RSAPI names. json is generic over what it deserialises into, as a Rust caller expects, rather than handing back a parsed document. body_stream reports the chunk error as the client's own error instead of the string the pipeline carries internally, and can be called more than once: each call hands back the same shared stream. The napi methods now delegate to these, so the reading rules -- a second read fails, integrity is verified once the body is in hand, discarding settles the trailers -- are stated once.
The body implements http_body::Body over the chunks the response already streams, reporting the client's own error, and a response converts into an http::Response carrying its status, version, and headers. That is what lets a Faith response reach code written against the wider ecosystem without a shim. The conversion is fallible rather than infallible: taking the body can find it already being consumed, and a caller should hear that rather than get a body that yields nothing. A response that cannot carry a body converts to an empty one. The test asserts what a consumer observes -- draining yields no bytes -- after a first attempt asserted a size hint this does not implement, which the default hint is entitled not to give.
The ~440 lines that turn options into the recipe an agent's clients are built from move to web-faith, along with the option groups they read, the IPv6 wildcard probe, and the flow-control window reconciliation with its tests. The binding maps its JavaScript options object onto the client's shapes and calls from_options. Moved rather than reimplemented on purpose: both surfaces have to land on the same defaults, and a second implementation is how they drift. What the binding still owns is the JavaScript vocabulary -- a union for cookies, either spelling of a PEM, strings where Rust has enums -- and entering a tokio runtime, which it needs because a napi callback can run outside one. Agent::new() follows, so a Rust caller no longer has to assemble a recipe by hand.
The builder's methods mirror the option groups, and a group is reached through a closure so one left alone is absent from the call rather than spelled out as absent, which is the shape RSAPI asks for. Setters take what a Rust caller holds rather than what the option carries: Duration whichever unit the setting is stored in, IpAddr for the local address, anything that converts into a String elsewhere. Milliseconds and seconds saturate rather than wrap on a duration no setting could mean. Tests cover both halves of the absent-rather-than-spelled-out rule: a group reached but not set keeps its other settings unset, and a group never reached stays None.
No source file past 1000 lines, tests-only files excepted. Each split follows the internal seams: the dns crate into transport/resolver/ discovery/https/settings, alt-svc into cache/header/prober/middleware, the client's agent into build/warm and its request path into send/target/builder, and the binding's agent into options/convert. Tests move with the code they exercise, as a child module of it, which keeps their access to private items. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Each is default-on; turning one off drops the component crate, the reqwest feature behind it, and the API that only means something with it present. alt-svc gains a dns feature of its own for the HTTPS-record sink, so the Alt-Svc store is usable without the resolver crate. napi's derives do not honour cfg on an object field or an impl method, so the binding removes methods through separate gated impl blocks and refuses an option group the build cannot honour. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Also clears the binding's dead dependencies: it carried two dozen crates from before the extraction, several of which a feature claimed to drop while linking them anyway. A slim binding refuses an option it cannot honour, at agent construction for an option group and at fetch for a per-request one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The http3 option group was accepted and ignored without the feature; it now goes with it. reqwest's rustls feature moves out of the workspace root into tls-aws-lc-rs, with tls-ring the alternative, installing ring as the process crypto provider where it is the only choice. Neither backend is a compile error. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Restructures the single
faithcdylib into a Cargo workspace: a browser-shaped Rust client crateweb-faith, six standalone component crates split out beneath it (alt-svc, connection tracking, cookies, DNS, encoding, integrity), and a thinweb-faith-napibinding crate that continues to ship as@passcod/faith.crates/web-faith-napi, leaving the napi-specific glue there and pulling reusable, system-independent pieces out into their own crates.web-faithcrate as the entry point for the high-level Rust client, along with specs and a design mockup proposing itsAgent/fetch/Request/ResponseAPI shape, built onhttp/urlecosystem types.Cargo.toml/Cargo.lockto build the napi binding from its new crate location (-p web-faith-napi).This PR lands the workspace restructure and proposed Rust API design; it does not yet publish to crates.io.