Overview & Philosophy
ln-http is a window-level HTTP concurrency supervisor (window.lnHttp). It transparently intercepts browser fetch() requests to automatically abort superseded in-flight GET/HEAD queries targeting the exact same URL via AbortController.abort(). It also provides an event-driven channel listening for ln-http:request CustomEvents with unique keys, instantly canceling stale requests with matching keys before executing the network call and broadcasting ln-http:response or ln-http:error.
In ln-ashlar, this coordinator prevents race conditions and stale UI states by handling socket cancellations automatically on two parallel channels.
The Concurrency Doctrine
A concurrency coordinator does not inject credentials, authorize tokens, or format JSON bodies. It focuses strictly on request cancellations. It runs as a global utility capturing fetch promises and aborting obsolete ones, leaving custom headers and payloads fully to the caller. The browser's native AbortController is the underlying contract.
Two Parallel Concurrency Channels
-
Path A (Transparent URL-Keyed GET de-duplication): Wraps the global
window.fetchnatively. Whenever a GET or HEAD request is fired, it scans in-flight records. If it finds a matching URL (e.g. during fast keystroke searches), it automatically aborts the previous request. POST, PUT, and DELETE methods are **never** auto-canceled here to prevent dropping user intent. -
Path B (Explicit Key-Based Event Cancellation): Listens for
ln-http:requestevents bubbling from descendants. If the request carries an explicit string `key` (e.g. `reorder-list-1`), any matching in-flight request—regardless of HTTP method—is instantly aborted. This prevents double-click issues on destructive actions and lists reordering.
How the Cancellation Flow Coheres
When a de-duplicated request is canceled:
- A new request arrives on Path A or B, matching the active URL or key.
- The coordinator calls
.abort()on the older request's `AbortController` instantly, and wipes its cache pointer. - The browser terminates the older network socket immediately. The initial fetch promise rejects with an
AbortError, which is safely captured and ignored by presenters, while the new request runs cleanly.
For the full guide see the ln-http README and the architecture reference.
Demo 1 — Search-as-you-type (Path A)
Type rapidly. Each keystroke fires a GET to /api/search?q=…. Different URLs (different q) run independently; same URLs (e.g. backspacing to a previously-typed query) collapse — the older aborts. The log shows abort errors with AbortError; only the latest typed query's response renders.
Event log
Results
Demo 2 — Drag-reorder POST with explicit key (Path B)
Click Move up / Move down rapidly. Each click dispatches a POST ln-http:request with key: 'reorder-list-1'. Path A would NOT cancel POSTs by URL; Path B aborts the previous in-flight one because the keys match. The log shows that fast clicks produce only one ln-http:response at the end.
Event log
Demo 3 — Independent POSTs (no dedup)
Click Create both to fire two POSTs to /api/users with different bodies. Path A does not auto-cancel non-idempotent methods; both succeed independently. Each call is a raw fetch() — no ln-http:request dispatch needed.
Event log
Demo 4 — Cancel a slow GET by URL (Path A)
Click Start slow GET to fire a fetch('/api/slow') that takes 4s. Click Cancel /api/slow to call lnHttp.cancel('/api/slow'). The original fetch promise rejects with AbortError; the result panel shows that.
Event log
Demo 5 — Cancel a Path B request by key
Click Start reorder POST to dispatch a Path B request with key: 'demo5-key'. Click Cancel demo5-key to call lnHttp.cancelByKey('demo5-key'). Aborted Path B requests fire NO event (silent abort); the log records the cancel call and the absence of any ln-http:response.
Event log
Demo 6 — Inflight inspector
Live readout of lnHttp.inflight. Path A entries appear as { method, url }; Path B entries appear as { key }. A Path B GET with a key appears in BOTH (one entry per map). Polled every 200 ms.
Currently in flight
[]
ln-http vs ln-ajax
| ln-http | ln-ajax | |
|---|---|---|
| Purpose | Transport coordination (dedup, cancel) | HTML page navigation |
Wraps window.fetch? | Yes (Path A) — transparent | No — calls native fetch |
| Event API | ln-http:request → ln-http:response / ln-http:error (Path B) | ln-ajax:before-start / :start / :complete / :error |
| Per-element attribute | None (global service) | data-ln-ajax on element / link / form |
| Auto-cancel | GET/HEAD on same URL (Path A); any method by key (Path B) | None |
| Header injection | None | Adds X-Requested-With + X-CSRF-TOKEN from meta |
| Body handling | Forwarded raw — consumer composes | Builds FormData from form, appends _token |
| Response parsing | None — forwards raw Response | .json() on the response |
| Visual feedback | None | Spinner + ln-ajax--loading class on element |
| Use when | You want dedup / cancel coordination across raw fetch calls | You want links / forms to swap server-rendered HTML without a full page load |
The two coexist. ln-ajax's internal fetch calls go through ln-http's wrapper transparently — its GET path inherits Path A dedup. Their concerns don't overlap.