fix(runtime): fail closed on hemx request errors
Preserve pending recovery while making failed HTTP responses non-applicable as effects. The runtime now restores aria-busy with pending state and emits inspectable hemx:error details with HTTP status for failed requests. req: runtime/005 req: convention/007
This commit is contained in:
+27
-1
@@ -11,6 +11,7 @@
|
||||
const everyTimers = new WeakMap();
|
||||
const pendingClassStates = new WeakMap();
|
||||
const indicatorStates = new WeakMap();
|
||||
const busyStates = new WeakMap();
|
||||
const disabledStates = new WeakMap();
|
||||
const sseSources = new WeakMap();
|
||||
const atomStores = new WeakMap();
|
||||
@@ -75,6 +76,7 @@
|
||||
function showPending(el, on) {
|
||||
const klass = el.getAttribute("data-hemx-pending-class");
|
||||
if (klass) togglePendingClass(el, klass, on);
|
||||
toggleBusy(el, on);
|
||||
const root = rootOf(el) || document;
|
||||
forEachElement(root, (i) => { if (i.hasAttribute("data-hemx-indicator")) toggleIndicator(i, on); });
|
||||
if (el.hasAttribute("data-hemx-disable-while-pending")) {
|
||||
@@ -85,6 +87,23 @@
|
||||
}
|
||||
}
|
||||
|
||||
function toggleBusy(el, on) {
|
||||
const state = busyStates.get(el);
|
||||
if (on) {
|
||||
if (state) state.count += 1;
|
||||
else busyStates.set(el, { count: 1, value: el.getAttribute("aria-busy") });
|
||||
el.setAttribute("aria-busy", "true");
|
||||
return;
|
||||
}
|
||||
if (!state) return;
|
||||
state.count -= 1;
|
||||
if (state.count <= 0) {
|
||||
if (state.value === null) el.removeAttribute("aria-busy");
|
||||
else el.setAttribute("aria-busy", state.value);
|
||||
busyStates.delete(el);
|
||||
}
|
||||
}
|
||||
|
||||
function togglePendingClass(el, klass, on) {
|
||||
const state = pendingClassStates.get(el);
|
||||
if (on) {
|
||||
@@ -169,6 +188,12 @@
|
||||
return url.href;
|
||||
}
|
||||
|
||||
function httpError(response) {
|
||||
const error = new Error(`HTTP ${response.status}`);
|
||||
error.status = response.status;
|
||||
return error;
|
||||
}
|
||||
|
||||
async function send(el, eventName, source = el) {
|
||||
if (el.getAttribute("data-hemx-confirm") && !confirm(el.getAttribute("data-hemx-confirm"))) return;
|
||||
const { form, data, multipart } = formDataFor(el, eventName, source);
|
||||
@@ -209,9 +234,10 @@
|
||||
signal: abort.signal,
|
||||
});
|
||||
if (pending.get(target)?.abort !== abort && policy === "latest") return;
|
||||
if (!response.ok) throw httpError(response);
|
||||
await applyResponse(response, rootOf(target));
|
||||
} catch (error) {
|
||||
if (error.name !== "AbortError") emit(rootOf(target), "hemx:error", String(error));
|
||||
if (error.name !== "AbortError") emit(rootOf(target), "hemx:error", { message: String(error), status: error.status || null });
|
||||
} finally {
|
||||
if (pending.get(target)?.abort === abort) {
|
||||
pending.delete(target);
|
||||
|
||||
@@ -16,6 +16,19 @@ fn runtime_exposes_debug_api_before_startup_side_effects() {
|
||||
assert!(source.contains("emit(root, \"hemx:bind-error\", String(error));"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_reports_http_failures_without_applying_effects() {
|
||||
// req: runtime/005
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
|
||||
assert!(source.contains("function httpError(response)"));
|
||||
assert!(source.contains("if (!response.ok) throw httpError(response)"));
|
||||
assert!(source.contains("error.status = response.status"));
|
||||
assert!(source.contains(
|
||||
"emit(rootOf(target), \"hemx:error\", { message: String(error), status: error.status || null })"
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn runtime_posts_urlencoded_forms_by_default() {
|
||||
let source = hemx_js::RUNTIME_JS;
|
||||
@@ -110,6 +123,11 @@ fn runtime_toggles_pending_conventions_around_requests() {
|
||||
assert!(source.contains("hadClass: el.classList.contains(klass)"));
|
||||
assert!(source.contains("if (state.hadClass) el.classList.add(state.className)"));
|
||||
assert!(source.contains("const indicatorStates = new WeakMap()"));
|
||||
assert!(source.contains("const busyStates = new WeakMap()"));
|
||||
assert!(source.contains("function toggleBusy(el, on)"));
|
||||
assert!(source.contains("el.setAttribute(\"aria-busy\", \"true\")"));
|
||||
assert!(source.contains("el.removeAttribute(\"aria-busy\")"));
|
||||
assert!(source.contains("toggleBusy(el, on)"));
|
||||
assert!(source.contains("function toggleIndicator(indicator, on)"));
|
||||
assert!(source.contains("toggleIndicator(i, on)"));
|
||||
assert!(source.contains("indicator.hidden = state.hidden"));
|
||||
|
||||
Reference in New Issue
Block a user