You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Slice C of #182. The big one. Refactor serve() to expose a pure-Rust signature: http::serve(endpoint, opts, svc: S) -> ServeHandle where S: Service<Request<Body>, Response = Response<Body>, Error = Infallible>. The current callback-shaped, handle-allocating FfiDispatcher becomes one specific Service implementation living in ffi/dispatcher.rs, handed to http::serve like any other service.
This slice is the one that flips "the FFI is the API" into "the FFI is one consumer of the API". After this, a Rust application can call iroh_http_core::http::serve(my_tower_service) without ever touching u64.
Evidence
crates/iroh-http-core/src/server.rs:243-260 — IrohHttpService exists but is not the user-facing entry point; serve(endpoint, opts, F) where F: Fn(RequestPayload) is.
server.rs is 979 LoC; the bulk is FfiDispatcher::dispatch (handle allocation, oneshot rendezvous, callback firing) interleaved with HTTP dispatch logic.
endpoint/mod.rs is 747 LoC because so much of it is FFI plumbing that needs the endpoint state.
Move FfiDispatcher to ffi/dispatcher.rs. It becomes one specific implementation of the above Service trait — the one that allocates handles, fires the JS callback, and awaits a oneshot::Receiver<ResponseHeadEntry>.
Add ffi::serve_with_callback(endpoint, opts, cb: Fn(RequestPayload)) that constructs the dispatcher and calls http::serve(endpoint, opts, dispatcher). JS adapters call this; pure-Rust callers call http::serve directly.
lib.rs re-exports ffi::serve_with_callback as serve for FFI binary compatibility (no FFI-side breaking change).
Move respond() into ffi/dispatcher.rs — it is the partner of the dispatcher's oneshot rendezvous and has no place in mod http.
After this slice, crates/iroh-http-core/src/http/server/mod.rs should be the axum-shaped accept loop — target ≤ 200 LoC, with the rest in pipeline.rs + stack.rs.
Dissolve crates/iroh-http-core/src/endpoint/ into crates/iroh-http-core/src/endpoint.rs at the crate root. Per-file disposition:
endpoint/ffi_bridge.rs → constructor / wiring folds into ffi/handles.rs (or a dedicated ffi/ module if it grows).
endpoint/http_runtime.rs and endpoint/transport.rs (16 LoC) → fold into endpoint.rs or http/transport/, whichever owns the data.
http::serve(endpoint, opts, svc) is callable from a pure-Rust integration test with a hand-rolled Service<Request<Body>, Response<Body>, Error = Infallible>. The test sends a request via http::fetch (or hyper directly) and asserts a response.
ffi::serve_with_callback(endpoint, opts, cb) reproduces today's JS-facing behaviour exactly (verified by all current adapter tests).
IrohHttpService (today's structure) is replaced by FfiDispatcher in ffi/dispatcher.rs. There is no service in mod http that knows about handles or callbacks.
crates/iroh-http-core/src/endpoint/ directory does not exist. crates/iroh-http-core/src/endpoint.rs exists and contains the IrohEndpoint facade plus the types listed in remediation step 10–11.
crates/iroh-http-core/src/{config,events,registry,stats}.rs no longer exist. Their contents are redistributed per remediation step 11. lib.rs re-exports the public symbols from their new paths so adapter callsites are unchanged.
This is the riskiest slice. Recommend landing as multiple sub-PRs:
C.1: introduce http::serve(svc) signature; today's serve(endpoint, opts, F) becomes a thin wrapper that internally constructs the dispatcher and calls http::serve. No code moves yet.
C.2: move FfiDispatcher into ffi/dispatcher.rs; rename wrapper to ffi::serve_with_callback.
C.6: dissolve endpoint/ directory into endpoint.rs; redistribute events.rs, registry.rs, stats.rs, and the config.rs remainder per remediation steps 10–11.
Each is independently revertable. Stop signal still applies (ADR-013): if any sub-PR takes more than ~2 compile iterations fighting tower types, stop and revisit the design in #182.
Slice B left the layer composition as one ServiceBuilder + one terminal boxed_clone(). That blocks two things Slice C wants:
a real cfg.decompression toggle (today: always-on, with a
doc-comment in StackConfig explaining why),
a pub(crate) fn build_decompression_layer() factory reusable on the
client side (today: inlined in build_stack).
Root cause: impl Layer<ServeService> + Clone as a return type erases
the <L::Service as Service<Request<Body>>>::Future: Send + 'static
bound that boxed_clone() needs further down. Either<Decomp, NoOp>
then fails Service<_> and the entire chain refuses to box.
Fix in this slice — adopt axum's boxed-per-layer pattern. Each
factory takes a ServeService and returns a ServeService, doing its
own boxed_clone():
Cost: one extra BoxCloneService allocation per layer per connection
(construction-time only, never per request). Benefit: every layer
factory is pub(crate) and reusable from build_client_stack, cfg.decompression becomes a real option_layer toggle, and AddExtensionLayer (#177), connection/request trackers (#178), trace
layer compose without type-soup gymnastics.
This is the runtime form of ADR-014 D2's "type-erased seam" — D2 only
required erasure at the outer boundary; this extends it to every layer.
Summary
Slice C of #182. The big one. Refactor
serve()to expose a pure-Rust signature:http::serve(endpoint, opts, svc: S) -> ServeHandlewhereS: Service<Request<Body>, Response = Response<Body>, Error = Infallible>. The current callback-shaped, handle-allocatingFfiDispatcherbecomes one specificServiceimplementation living inffi/dispatcher.rs, handed tohttp::servelike any other service.This slice is the one that flips "the FFI is the API" into "the FFI is one consumer of the API". After this, a Rust application can call
iroh_http_core::http::serve(my_tower_service)without ever touchingu64.Evidence
crates/iroh-http-core/src/server.rs:243-260—IrohHttpServiceexists but is not the user-facing entry point;serve(endpoint, opts, F)whereF: Fn(RequestPayload)is.server.rsis 979 LoC; the bulk isFfiDispatcher::dispatch(handle allocation, oneshot rendezvous, callback firing) interleaved with HTTP dispatch logic.endpoint/mod.rsis 747 LoC because so much of it is FFI plumbing that needs the endpoint state.remote_node_idis mutated viaconn_conc.get_mut().remote_node_id = …— see refactor(server): per-connection remote_node_id as request extension, not service field #177.Dropguards inline inserve_with_events— see refactor(server): extract ConnectionTracker / RequestTracker, retire ad-hoc inline Drop guards #178.CONNECT/Upgradebranch inFfiDispatcher::dispatchsinceraw_connectwas dropped — see refactor(server): delete dead duplex CONNECT/Upgrade branch in FfiDispatcher::dispatch #180.Remediation
serveAPI:FfiDispatchertoffi/dispatcher.rs. It becomes one specific implementation of the aboveServicetrait — the one that allocates handles, fires the JS callback, and awaits aoneshot::Receiver<ResponseHeadEntry>.ffi::serve_with_callback(endpoint, opts, cb: Fn(RequestPayload))that constructs the dispatcher and callshttp::serve(endpoint, opts, dispatcher). JS adapters call this; pure-Rust callers callhttp::servedirectly.lib.rsre-exportsffi::serve_with_callbackasservefor FFI binary compatibility (no FFI-side breaking change).remote_node_idvia a request extension inserted by a small per-connectionMapRequestLayer, not as a mutable service field. Closes refactor(server): per-connection remote_node_id as request extension, not service field #177.Dropguards (PeerConnectionGuard,ReqHeadCleanup,TotalGuard,ReqGuard) into typed lifecycle objects (ConnectionTracker,RequestTracker) owned by the per-connection task. Closes refactor(server): extract ConnectionTracker / RequestTracker, retire ad-hoc inline Drop guards #178.CONNECT/Upgradebranch inFfiDispatcher::dispatch(~80 LoC) —raw_connectwas dropped in8754da4, sessions use their own ALPN. Closes refactor(server): delete dead duplex CONNECT/Upgrade branch in FfiDispatcher::dispatch #180.respond()intoffi/dispatcher.rs— it is the partner of the dispatcher'soneshotrendezvous and has no place inmod http.crates/iroh-http-core/src/http/server/mod.rsshould be the axum-shaped accept loop — target ≤ 200 LoC, with the rest inpipeline.rs+stack.rs.crates/iroh-http-core/src/endpoint/intocrates/iroh-http-core/src/endpoint.rsat the crate root. Per-file disposition:endpoint/ffi_bridge.rs→ constructor / wiring folds intoffi/handles.rs(or a dedicatedffi/module if it grows).endpoint/http_runtime.rsandendpoint/transport.rs(16 LoC) → fold intoendpoint.rsorhttp/transport/, whichever owns the data.endpoint/session_runtime.rs→ fold intohttp/session.rs. If any piece is session-specific in a way that only Slice E (refactor(ffi): Slice E — collapse channel-backed pumps now that BodyReader: http_body::Body #187) can resolve cleanly, it may stay inendpoint.rsuntil that slice; document the carve-out inline.endpoint/mod.rs(IrohEndpointfacade) → becomesendpoint.rsat the crate root. The folder is deleted.events.rs→http/events.rs(producers live inhttp/transport/pool.rsand the endpoint;ffi → httpimport direction is preserved).registry.rs→ffi/registry.rs(FFI-only — only adapter crates touch it).stats.rstypes (EndpointStats,NodeAddrInfo,PeerStats,ConnectionEvent) → fold intoendpoint.rs.config.rsremainder (NetworkingOptions,DiscoveryOptions) → fold intoendpoint.rs. (CompressionOptionsalready moved in Slice B — see refactor(core): Slice B — typed StackConfig + option_layer composition shared by serve and fetch #184.)crates/iroh-http-core/src/directly contains exactly four entries:lib.rs,endpoint.rs,http/,ffi/. Slice E (refactor(ffi): Slice E — collapse channel-backed pumps now that BodyReader: http_body::Body #187) extendstests/architecture.rsto assert this.Acceptance criteria
http::serve(endpoint, opts, svc)is callable from a pure-Rust integration test with a hand-rolledService<Request<Body>, Response<Body>, Error = Infallible>. The test sends a request viahttp::fetch(or hyper directly) and asserts a response.ffi::serve_with_callback(endpoint, opts, cb)reproduces today's JS-facing behaviour exactly (verified by all current adapter tests).IrohHttpService(today's structure) is replaced byFfiDispatcherinffi/dispatcher.rs. There is no service inmod httpthat knows about handles or callbacks.remote_node_idis a request extension, not a service field. (Closes refactor(server): per-connection remote_node_id as request extension, not service field #177.)http/server/mod.rs≤ 200 LoC.crates/iroh-http-core/src/endpoint/directory does not exist.crates/iroh-http-core/src/endpoint.rsexists and contains theIrohEndpointfacade plus the types listed in remediation step 10–11.crates/iroh-http-core/src/{config,events,registry,stats}.rsno longer exist. Their contents are redistributed per remediation step 11.lib.rsre-exports the public symbols from their new paths so adapter callsites are unchanged.crates/iroh-http-core/src/directly contains exactly:lib.rs,endpoint.rs,http/,ffi/. (Architecture-test enforcement lands in Slice E refactor(ffi): Slice E — collapse channel-backed pumps now that BodyReader: http_body::Body #187.)npm run cigreen; 92 interop pairs pass.Subsumes
Notes
This is the riskiest slice. Recommend landing as multiple sub-PRs:
http::serve(svc)signature; today'sserve(endpoint, opts, F)becomes a thin wrapper that internally constructs the dispatcher and callshttp::serve. No code moves yet.FfiDispatcherintoffi/dispatcher.rs; rename wrapper toffi::serve_with_callback.endpoint/directory intoendpoint.rs; redistributeevents.rs,registry.rs,stats.rs, and theconfig.rsremainder per remediation steps 10–11.Each is independently revertable. Stop signal still applies (ADR-013): if any sub-PR takes more than ~2 compile iterations fighting tower types, stop and revisit the design in #182.
References
Carry-forward from Slice B (#184)
Slice B left the layer composition as one
ServiceBuilder+ one terminalboxed_clone(). That blocks two things Slice C wants:cfg.decompressiontoggle (today: always-on, with adoc-comment in
StackConfigexplaining why),pub(crate) fn build_decompression_layer()factory reusable on theclient side (today: inlined in
build_stack).Root cause:
impl Layer<ServeService> + Cloneas a return type erasesthe
<L::Service as Service<Request<Body>>>::Future: Send + 'staticbound that
boxed_clone()needs further down.Either<Decomp, NoOp>then fails
Service<_>and the entire chain refuses to box.Fix in this slice — adopt axum's boxed-per-layer pattern. Each
factory takes a
ServeServiceand returns aServeService, doing itsown
boxed_clone():Cost: one extra
BoxCloneServiceallocation per layer per connection(construction-time only, never per request). Benefit: every layer
factory is
pub(crate)and reusable frombuild_client_stack,cfg.decompressionbecomes a realoption_layertoggle, andAddExtensionLayer(#177), connection/request trackers (#178), tracelayer compose without type-soup gymnastics.
This is the runtime form of ADR-014 D2's "type-erased seam" — D2 only
required erasure at the outer boundary; this extends it to every layer.