📚 Document promesa API shapes in memory

Two API families with inconsistent argument order, verified against
the 12.0.1 jar: fmap/mcat are fn-first (they pair with ->>), then/catch
are promise-first (they pair with ->). The bind-style ops require the
fn to return a promise; a plain value crashes with "expected
thenable". The repo convention prefers ->> composition, and legacy ->
chains stay as they are. Routes from critical-info and exporter/core.

AI-assisted-by: glm-5.3-flash
This commit is contained in:
Andrey Antukh 2026-10-07 08:34:21 +00:00
parent a9a105509b
commit fe636282cb
3 changed files with 75 additions and 0 deletions

View File

@ -0,0 +1,73 @@
# Promesa (funcool, 12.0.1) — verified API shapes
The promise lib of the monorepo: JVM (backend, `promesa.exec` thread
pools) and CLJS (frontend, exporter). Declared in `common/deps.edn`;
heavy users: `frontend/src/app/main/**` (Rx interop), `exporter/src/app/**`
(management client, auth), `backend/src/app/storage/s3.clj` and
`worker/cron.clj` (JVM await paths). Doc: `doc/promises.md` upstream.
## Two API families (verified against the 12.0.1 jar)
Argument order is inconsistent BY DESIGN; each op belongs to one of
two families:
**promise-first `([p f ...])` — pairs with `->` threading** (the
promise arrives as the threaded first argument):
- `then`, `then'`, `bind`, `handle`, `finally`, `catch` (`([p f])`,
2-arity with predicate: `([p pred-or-type f])`), `chain`
`([p f & fs])`.
**fn-first `([f p ...])` — pairs with `->>` threading** (the promise
arrives as the threaded LAST argument):
- `map`/`fmap`, `mapcat`/`mcat`, `hmap`/`hcat`, `fnly`, `merr`.
Every 3-arity in a family inserts its executor FIRST for the fn-first
family (`([executor f p])`) and LAST for the promise-first family
(`([p f executor])`).
**What `f` must return:**
- `then`/`catch`/`fmap`/`hmap`: plain value OK — auto unwrapping.
- the bind-style ops `mcat` and `merr`: **`f` MUST return a promise
instance** (`merr` may also throw). A plain value does NOT get
wrapped: the impl's `fbind` pushes a BIND task that demands a
thenable and crashes with `TypeError: expected thenable`
(first-hand crash in the exporter worker, 2026-10-07).
`finally`/`fnly`: run `f`, mirror the original promise value.
## promesa's own -> / ->> macros (do NOT confuse with clojure.core)
`p/->` and `p/->>` exist in `promesa.core` (12.x). Their purpose is NOT
arg positioning: they expand to a `chain` over `then` and REALIZE the
promise at each step, so every form receives the realized value
(e.g. `(p/-> (js/fetch ...) .-body .-status)`).
## Repo convention
**Prefer `->>` composition** over the map family (fn-first ops); it is
how new promise code should read:
(->> (fetch ...)
(p/fmap (fn [v] ...))
(p/mcat (fn [v] (p/...))))
A `p/do` terminator inside `->>` receives the threaded promise as its
LAST expression — it runs AFTER the earlier exprs, so it can never act
as the chain's continuation; terminate by wrapping the chain in an
explicit `(p/do chain ...)` (`p/let` avoids the same trap).
Legacy code threads promises with `->` over the promise-first
family (`(-> p (p/then ...))`, `(p/then p f)` calls). Read the args in
that order when touching it; do not rewrite it unless the task touches
that code anyway.
For JVM blocking code, `promesa.exec/await!` exists but is deprecated
since 12.0.0 → `p/await`.
## Async test gotcha (CLJS)
An async cljs.test var only proves its chain when the chain is really
awaited: a broken link outside the `p/let`/`p/do` body ends silently as
"Ran N tests containing 0 assertions" with 0 failures. If a test passes
with 0 assertions, the chain broke upstream; add a print in the last
link (`CHAIN RAN`) to prove plumbing before trusting green.

View File

@ -8,6 +8,7 @@ You are working on the GitHub project `penpot/penpot`, a monorepo.
- Cross-cutting testing principles, TDD workflow, and anti-patterns: `mem:testing`.
- Verified Clojure language behaviors that contradict common assumptions (e.g. `int?` covers `Long`; `integer?` is the general predicate): `mem:clojure/idioms` — read before assuming stdlib predicate semantics.
- When designing a solution or an API, read `mem:clojure/design-rules` (function shape, boundary checks, naming).
- Promise code (JVM + CLJS): promesa's two API families and their arg orders (fmap/mcat fn-first, then/catch promise-first, `mcat` f must return a promise), the repo `->>` preference, and async-test silent-green traps: `mem:clojure/promesa` — read before writing or reading promise chains.
# Development workflow

View File

@ -9,6 +9,7 @@
- Because exporter consumes `common/`, shared file/shape/model changes may need exporter verification even when the immediate change is not under `exporter/`.
- Cross-cutting testing principles and anti-patterns: `mem:testing`.
- Exporter test conventions and CI: `mem:exporter/testing`.
- Promise chains in the exporter (management client, auth) follow promesa's two API families; arg orders, what each fn must return, the `->>` preference and the async-test silent-green trap: `mem:clojure/promesa` — read before touching promise code.
## HTTP and browser pool