Skip to content

FP 3.0.0 / RC 2.0.0: route steps and templates on transformer stacks, plus performance - #34

Merged
luizmb merged 2 commits into
mainfrom
feature/fp-3-rc-2
Oct 16, 2026
Merged

luizmb merged 2 commits into
mainfrom
feature/fp-3-rc-2

Conversation

@luizmb

@luizmb luizmb commented Oct 16, 2026

Copy link
Copy Markdown
Owner

What

Moves NetworkTools to FP 3.0.0 and ReactiveConcurrency 2.0.0 on lawful transformer stacks, and speeds up templating and routing.

Why

RC 2 removed the operators on nested shapes, so the routing DSL's >=> on Reader<Env, Publisher> stopped compiling, and under FP 3 >>- on a bare Reader<Env, Result> is Reader's own bind, which breaks loadTemplate(…) >>- { render($0, ctx) } (used by PhotoCulling and CV).

Changes

Port (breaking)

  • Route steps return RouteEffect<Env, O> = RC's ReaderTPublisher<Env, ResponseError, O>; >=> is the stack's lawful Kleisli composition (first failure skips the remaining steps). DSL code is unchanged.
  • Router.handle(_:) still returns the plain Reader<Env, Publisher<Response, ResponseError>>; Router.pipeline is the stack form.
  • render / loadTemplate return ReaderTResult<HTMLEnvironment, TemplateError, String>; the >>- chain keeps its meaning. Callers add .rawValue before runReader.
  • HTMLEnvironment is Sendable (the stack bind needs it).
  • README examples and CHANGELOG updated. No other public API used the changed operators (grepped for nested-shape >>-/<*>/£>, stream <*>, Publisher >>-).

Performance

  • Templates render into one buffer (#each copied everything rendered so far for every item), read each fragment once per render (an #include inside #each was read once per item), and find {{ }} on UTF-8.
  • esc / escAttr: one pass, input returned untouched when nothing needs escaping.
  • Router is a list of routes: <|> concatenates (associative, empty is the identity, new law tests) and one publisher tries the routes in order instead of one nested catch per route. Only a 404 falls through, as before.
  • Route patterns are split once, Request.path is stored, request bodies are not copied per chunk.
Benchmark (p50) Before After Mallocs
render, #each 1k rows with #include 13.1 ms 2.3 ms (5.7×) 20012 → 1035
render, #each 1k cells 2.56 ms 0.54 ms (4.8×) 3006 → 26
render, 1k variables 1.53 ms 0.48 ms (3.2×) 1010 → 10
esc, 11 KB prose 377 µs 99 µs (3.8×) 27 → 2
esc, 9 KB nothing to escape 254 µs 29 µs (8.8×) 6 → 0
escAttr, 11 KB prose 536 µs 140 µs (3.8×) 38 → 2
Router, 20 routes, first matches 236 µs 109 µs (2.2×) 477 → 145
Router, 20 routes, last matches 991 µs 761 µs (1.3×) 1352 → 1009

Routing still costs ~35 µs per route tried: one RC publisher run each. That goes away with the deferred RC same-queue dispatch, not here.

Tests: 123 pass (new: stack bind and short-circuit, fragment read once, multi-byte escaping, router identity / associativity / non-404 stops fall-through).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NRQYrVoN7VHHinrt7TxhV3

luizmb and others added 2 commits October 16, 2026 04:12
…es are transformer stacks

Route steps are Kleisli arrows into ReaderTPublisher (RouteEffect<Env, O>), so
>=> is the stack's lawful bind instead of an operator on the nested
Reader<Env, Publisher> shape, which RC 2 removed. Router keeps handle(_:)
returning the plain Reader; the stack is its `pipeline`.

render and loadTemplate return ReaderTResult, so `loadTemplate(name) >>- { render($0, ctx) }`
keeps its meaning under FP 3 (on a bare Reader, >>- is Reader's bind).
HTMLEnvironment is Sendable.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRQYrVoN7VHHinrt7TxhV3
Templates render into one output buffer (each item of #each was appended
by copying everything rendered so far), read each fragment once per
render, and find {{ }} on UTF-8 bytes. esc/escAttr make one pass and
return the input untouched when there is nothing to escape.

Router is a list of routes: <|> concatenates (associative, empty is the
identity) and one publisher tries them in order, instead of one nested
catch per route. Only a 404 falls through, as before.

Route patterns are split once, Request.path is stored, and request bodies
are no longer copied per chunk.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NRQYrVoN7VHHinrt7TxhV3
@luizmb
luizmb merged commit eb36b58 into main Oct 16, 2026
4 checks passed
@luizmb
luizmb deleted the feature/fp-3-rc-2 branch October 16, 2026 11:07
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant