Skip to content

refactor(http): round-2 polish cluster — 12 follow-on beads (rf2-jklja)#1010

Merged
mike-thompson-day8 merged 12 commits into
mainfrom
refactor/rf2-http-batch-polish
May 14, 2026
Merged

refactor(http): round-2 polish cluster — 12 follow-on beads (rf2-jklja)#1010
mike-thompson-day8 merged 12 commits into
mainfrom
refactor/rf2-http-batch-polish

Conversation

@mike-thompson-day8
Copy link
Copy Markdown
Contributor

Summary

Eighth batched-by-surface refactor — 12-bead P2/P3 polish cluster on implementation/http/, dispatched from the rf2-jklja round-2 audit. All beads addressed in commit-per-bead form.

Commits

Bead P Title
rf2-o8879 P3 drop dead maybe-emit-decode-defaulted! gate
rf2-86nht P3 jitter ±25% per Spec 014 line 250 (was ±12.5%)
rf2-y03ky P3 trim narration comments in late-bind block
rf2-tja2y / rf2-exycf P3 memoise requiring-resolve for malli + cheshire (fuses with CLJ/CLJS deref asymmetry)
rf2-02vzz P2 fuse URL redact + denylist-hit into single walk
rf2-hzn1a P2 compute-actor-id short-circuits when no spawned actors
rf2-2utlm P2 dedupe dispatch-reply across transport + canned stubs
rf2-plngk P2 pin clear-in-flight! ownership to finalise sites
rf2-1z21y P2 align Spec 014 decode wording (Spec 010 owns schema language, not a decode pipeline)
rf2-0eyp2 P2 extract handlers into http-handlers sibling
rf2-5ijhk P2 split http-decode out of http-encoding (was 355 → 217 LoC; new decode 171 LoC)

(Plus a fix-up commit for the CLJS resolve-fn compilation issue introduced by rf2-tja2y / rf2-5ijhk — CLJS resolve is a compile-time macro that demands a literal quoted symbol.)

Net diff

Against the merge base — 577 insertions, 448 deletions, 11 files. New siblings: http_handlers.cljc (125 LoC), http_decode.cljc (177 LoC). The pre-existing http_encoding.cljc drops from 355 to 217 LoC; http_managed.cljc (the public façade) drops to 200 LoC.

Perf wins

  • compute-actor-id early-out: apps without state machines pay a single deref + map lookup + seq-check per request instead of a full structural walk.
  • prepare-emit-tags URL walk: one parse instead of two per retry-attempt, abort-on-actor-destroy, and decode-defaulted emit.
  • clear-in-flight! ownership: saves N request-side swap!s on actor-destroy with N in-flight; saves one on user-initiated aborts.
  • Malli / Cheshire resolves: ~4 requiring-resolve calls collapse to a single defonce delay per fn.
  • run-accept default: inlined; no closure allocation per response.

Test plan

  • cd implementation/http && clojure -M:test — 136 tests / 385 assertions
  • cd implementation && npm run test:cljs — 1817 tests / 5105 assertions
  • npm run test:perf-bundle — counter clean, counter-perf carries perf markers
  • npm run test:bundle-isolation — http symbols stay inside http; allow-list within budget

Mike Thompson added 12 commits May 14, 2026 13:54
The outer caller in decode-response-body already gates on the same
not-supplied condition. Inlining the body removes one indirection and
the duplicated decode-supplied? check inside the helper.

Per audit finding 1.3 (rf2-jklja round-2).
The earlier impl used (* j (- 0.5 (rand))) where j = 0.25 × capped,
yielding offset in [-0.125 × capped, +0.125 × capped] — a ±12.5%
spread, not the ±25% Spec 014 §Retry and backoff line 250 states.

Now uses (- 1.0 (* 2.0 (rand))) (uniform in [-1, +1]) scaled by
0.25 × capped for a true ±25% range.

Per audit finding 4.3 (rf2-jklja round-2).
The per-line bead-ref comments duplicated information that lives in
each sub-namespace's docstring. Replaced with a single block-header
comment pointing at the docstrings, and sorted the publications
alphabetically for scan-ability. The drift gate matches on keyword
only — ordering is free.

Per audit finding 1.7 (rf2-jklja round-2).
…, rf2-exycf)

malli-decode previously ran 3-4 requiring-resolve calls per decode
(decode + transformer + validate, with reader-conditional asymmetry
between JVM and CLJS). Cheshire's generate-string/parse-string had
the same per-call resolve cost on JVM.

Now: defonce'd delays cache the resolved fn once per runtime; the
CLJ/CLJS deref asymmetry is hidden behind a single resolve-fn helper
that normalises the return to a callable value. The decode call site
is platform-uniform (no #? around the invocations themselves) and the
malli pipeline cost reduces from 3 resolves + 3 derefs per call to a
single shared atom deref.

Closes both rf2-tja2y (memoise) and rf2-exycf (deref asymmetry — the
new resolve-fn wraps once).

Per audit findings 1.4 + 1.5 (rf2-jklja round-2).
prepare-emit-tags previously parsed each :url query string twice:
once via query-denylist-hit? to decide whether to stamp :sensitive?,
then a second time via redact-url inside redact-request-tags /
redact-failure.

Now: redact-request-tags-with-flag and redact-failure-with-flag
return [tags url-redacted?] tuples from the single redact-url-query-
string call, and prepare-emit-tags / prepare-emit-failure consume
the flag directly. The query-denylist-hit? helper is removed (the
shape-fused walker subsumes it).

Halves the per-trace-emit URL parse cost on every retry-attempt,
abort-on-actor-destroy, and decode-defaulted warning that carries a
:url with a query string.

Per audit finding 3.4 (rf2-jklja round-2).
…f2-hzn1a)

Every :rf.http/managed request previously walked the
[:rf/spawned <parent> <invoke-id>] registry looking for the
originating event-id, on every invocation. Apps that don't use state
machines (the common case) paid the deref + structural walk for every
HTTP request even though the registry is always empty.

Now: deref the db once, pluck :rf/spawned, and short-circuit on
seq-empty before the O(parents × invokes) walk. The deref itself
stays (it's how we read frame state without statically requiring
core/spawn machinery), but the walk skips on the empty registry.

The deeper refactor (reverse-index maintained by spawn/destroy) is
left for a later bead — this early-out covers the no-machines hot
path without touching the spawn lifecycle.

Per audit finding 3.1 (rf2-jklja round-2).
…utlm)

http-transport/dispatch-reply!, canned-success-handler, and canned-
failure-handler each manually composed build-reply-event with a
late-bind :router/dispatch! lookup. Three near-verbatim copies of
the same shape — and three places to keep in sync.

Now: encoding/dispatch-reply-via-late-bind! owns the composition.
Transport's dispatch-reply! is a thin wrapper that chooses the
explicit-on slot; the canned stubs delegate directly. http-machine-
wrapper no longer needs the late-bind import for the dispatch path
(it keeps it for :machines/reg-machine).

Per audit finding 1.6 (rf2-jklja round-2).
…-plngk)

Aborts previously cleared the in-flight registry 2-3 times per
cascade: managed-abort-handler pre-cleared by request-id, then the
abort-fn fired finalise-failure! which cleared again; abort-on-actor-
destroy walked the request-id index per-handle (N eager swaps) and
finalise-failure! re-walked.

Now the contract is pinned: the natural-completion sites
(finalise-success!, finalise-failure!, maybe-retry!'s retry-clear)
own per-request cleanup. The abort entry points fire the abort-fn
and rely on its finalise-failure! cascade.

The actor-side eager dissoc inside abort-on-actor-destroy remains: it
pins idempotency against re-entry (a re-entered call sees an empty
slot), and it's a single swap! regardless of handle count.

Saves N request-side swap!s on actor-destroy with N in-flight, and
one swap on user-initiated aborts.

Per audit finding 3.2 / Top-#3 (rf2-jklja round-2).
…not a decode pipeline (rf2-1z21y)

The previous wording claimed ':decode <schema> runs through 010's
decode pipeline' but Spec 010 has no decode pipeline. The decode
contract lives in Spec 014 (request body → parse by content-type →
apply schema-language decode-or-validate primitive); Spec 010
standardises the schema language itself (the :spec / reg-app-schema
surface and the pluggable validator seam).

The CLJS reference implementation already uses malli.core/decode +
malli.transform/json-transformer directly — it cannot route through
re-frame.schemas because the http artefact has no production dep on
schemas (per rf2-p7va schemas is optional; pulling it in would
violate the artefact split). The two surfaces share a schema
language but not a code path.

Per audit finding 4.2 (rf2-jklja round-2).
http-managed.cljc claimed to be a thin public façade but still owned
two non-trivial private handlers (normalise-args + managed-handler +
managed-abort-handler, ~80 LoC) — the only call sites for
middleware/run-interceptor-chain! and transport/check-cljs-only-keys!.

Now: re-frame.http-handlers owns the handler bodies. The façade is
true re-exports + the four fx/reg-fx calls + the late-bind
publications. Every concern has a per-named sibling
(encoding/registry/middleware/transport/privacy/machine-wrapper/handlers)
— the symmetry the audit named the conspicuous absence is closed.

Per audit finding 2.1 (rf2-jklja round-2).
http-encoding.cljc (355 LoC) held five distinct concerns under one
roof — URL encoding, body encoding, decode pipeline, :accept
normalisation, failure-map / reply addressing / backoff. Well over
the 250-LoC leaf-size ceiling (rf2-zkca8).

This commit extracts the response-side decode pipeline into a sibling
http-decode namespace:

- content-type-of (case-insensitive header lookup)
- sniff-decoder (Content-Type → :json | :text | :blob)
- malli-decode + the memoised resolves (rf2-tja2y delays)
- decode-response-body

Bonus: opportunistically inlines default-accept-fn into run-accept
(audit finding 3.3 — saves one closure alloc per response).

Post-split sizes:
- http-encoding.cljc: 217 LoC (under ceiling)
- http-decode.cljc:   171 LoC (under ceiling)

URL helpers (url-encode, params->query, merge-params) stay in
http-encoding for now. rf2-8vofn (in flight on another branch)
pre-sizes a http-url.cljc; once that lands, the URL slot moves
there in a follow-on.

Per audit finding 2.3 + 3.3 (rf2-jklja round-2).
…rf2-5ijhk)

The rf2-5ijhk split introduced a resolve-fn helper that factored the
JVM requiring-resolve / CLJS resolve asymmetry behind a single
symbol-taking fn. CLJS resolve is a compile-time macro that demands
a literal quoted symbol — passing the symbol as a runtime fn arg
fails analysis with 'Argument to resolve must be a quoted symbol'.

Inline each malli resolve into its own defonce delay so the symbol
sits as a literal in the macroexpansion point. The cache and the
delayed-once-per-runtime semantics are unchanged; the abstraction
just folds into three sibling delays instead of one helper + three
callers.
@mike-thompson-day8 mike-thompson-day8 force-pushed the refactor/rf2-http-batch-polish branch from 9321a56 to 8f455b6 Compare May 14, 2026 04:00
@mike-thompson-day8 mike-thompson-day8 merged commit 404f0c9 into main May 14, 2026
34 checks passed
@mike-thompson-day8 mike-thompson-day8 deleted the refactor/rf2-http-batch-polish branch May 14, 2026 04:37
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