feature: framework-level abort handling for page lifecycle
component.js now creates an AbortController for each page mount, passing it to load(). On unmount, the controller is aborted to cancel in-flight requests that would otherwise mutate unmounted state. Page load functions consistently pass the signal to apiFetch and guard state mutations with abort checks. This eliminates the need for per-page abortController boilerplate and prevents stale errors from appearing on rapid navigation. Users page now guards catch block and loading state cleanup against aborted requests, matching passkeys.js pattern.
This commit is contained in:
+1
-1
@@ -1,4 +1,4 @@
|
||||
import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js?v=10';
|
||||
import { h, render, Link, hComp, ToastContainer, connect, apiFetch, modelRegister, modelFetch, reactive, initAuth, getAuthToken, checkSession } from '/static/hoover/index.js?v=11';
|
||||
|
||||
import DashboardPage from '/static/pages/dashboard.js?v=11';
|
||||
import InterfacesPage from '/static/pages/interfaces.js?v=9';
|
||||
|
||||
@@ -70,17 +70,19 @@ export function mountComponent(key, renderer) {
|
||||
|
||||
if (entry) {
|
||||
// Re-mount: component already exists with its state.
|
||||
// Don't re-run load — that re-render was triggered by a reactive update.
|
||||
return;
|
||||
}
|
||||
|
||||
// Abort previous in-flight load and re-run.
|
||||
if (entry.abortController) entry.abortController.abort();
|
||||
entry.abortController = null;
|
||||
} else {
|
||||
entry = { state: pd.state };
|
||||
_mounted.set(key, entry);
|
||||
|
||||
pd.state.error = null;
|
||||
}
|
||||
|
||||
if (pd.load) {
|
||||
Promise.resolve().then(() => pd.load(pd.state));
|
||||
const abortController = new AbortController();
|
||||
entry.abortController = abortController;
|
||||
Promise.resolve().then(() => pd.load(pd.state, abortController));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,6 +96,9 @@ export function unmountComponent(key, renderer) {
|
||||
|
||||
const pd = renderer._pageDef;
|
||||
|
||||
// Abort in-flight load requests so they don't mutate unmounted state
|
||||
if (entry.abortController) entry.abortController.abort();
|
||||
|
||||
if (pd.onUnmount) {
|
||||
try { pd.onUnmount(entry.state); } catch (_) {}
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export { html } from './html.js?v=9';
|
||||
export { render } from './render.js?v=9';
|
||||
|
||||
/* ── Component ───────────────────────────────────────────────── */
|
||||
export { definePage, hComp } from './component.js?v=9';
|
||||
export { definePage, hComp } from './component.js?v=10';
|
||||
|
||||
/* ── Router ──────────────────────────────────────────────────── */
|
||||
export { createRouter, Link } from './router.js?v=9';
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
_vnodeDom, createDom, getDom, patchNode, sweepDom,
|
||||
setMountFn, setUnmountFn,
|
||||
} from './vdom.js?v=9';
|
||||
import { mountComponent, unmountComponent } from './component.js?v=9';
|
||||
import { mountComponent, unmountComponent } from './component.js?v=10';
|
||||
|
||||
/** Container → previous root vnodes */
|
||||
export const _renderSlots = new Map();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { h, html, PageHeader, Badge, Empty, Table, renderGuard, renderGuardMulti, ConfirmDelete, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup } from '/static/hoover/index.js?v=9';
|
||||
import { h, html, PageHeader, Badge, Empty, Table, renderGuard, renderGuardMulti, ConfirmDelete, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup } from '/static/hoover/index.js?v=10';
|
||||
import { openModal, closeModal, isModalProcessing, setModalProcessing, refreshModals } from '/static/hoover/components/modal.js?v=9';
|
||||
import { _deleting } from '/static/hoover/components/data.js?v=9';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, modalVNodes, refreshModals, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll, formAction } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Empty, Table, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, modalVNodes, refreshModals, definePage, getModel, modelFetch, ActionCell, certStatusBadge, poll, formAction } from '/static/hoover/index.js?v=10';
|
||||
import { isModalProcessing, setModalProcessing } from '/static/hoover/components/modal.js?v=9';
|
||||
|
||||
function _accountCard(account) {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js?v=10';
|
||||
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton } from '/static/hoover/index.js?v=11';
|
||||
|
||||
function fmtBytes(bytes) {
|
||||
if (bytes === 0) return '0 B';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=9';
|
||||
import { html, h, PageHeader, ConfirmDelete, Tabs, Table, ServiceStatus, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, QuickModal } from '/static/hoover/index.js?v=10';
|
||||
|
||||
function makeAddRange(activeZones, interfaces) {
|
||||
const opts = [
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js?v=10';
|
||||
|
||||
async function changeZone(name, zone, state) {
|
||||
const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', {
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
handleLoginSuccess,
|
||||
webauthnSupported,
|
||||
startAuthentication,
|
||||
} from '/static/hoover/index.js?v=12';
|
||||
} from '/static/hoover/index.js?v=14';
|
||||
|
||||
function LoginPage() {
|
||||
const hasWebAuthn = webauthnSupported();
|
||||
@@ -214,7 +214,7 @@ const Page = definePage({
|
||||
|
||||
if (getAuthToken()) {
|
||||
try {
|
||||
const res = await apiFetch('/api/auth/session');
|
||||
const res = await apiFetch('/api/auth/session', { signal: abortController?.signal });
|
||||
if (res.ok) {
|
||||
window.location.hash = '/dashboard';
|
||||
return;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Tabs, esc, definePage, renderGuard, modelFetch, getModel } from '/static/hoover/index.js?v=10';
|
||||
|
||||
const logTabs = [
|
||||
{ key: 'journal', label: 'Journal' },
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js?v=10';
|
||||
|
||||
const addFwd = QuickModal({
|
||||
title: 'Add Port Forward',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, definePage } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, definePage } from '/static/hoover/index.js?v=10';
|
||||
|
||||
export default definePage({
|
||||
init() {
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
webauthnSupported,
|
||||
isModalProcessing,
|
||||
setModalProcessing,
|
||||
} from '/static/hoover/index.js?v=12';
|
||||
} from '/static/hoover/index.js?v=14';
|
||||
|
||||
const state = reactive({ credentials: [], loading: true, refreshing: false, error: null });
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, ConfirmDelete, certStatusBadge, ActionGroup, formAction } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Badge, Empty, Table, renderGuardMulti, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionCell, ConfirmDelete, certStatusBadge, ActionGroup, formAction } from '/static/hoover/index.js?v=10';
|
||||
import { openModal, formModal, closeModal } from '/static/hoover/components/modal.js?v=9';
|
||||
import { openBackendModal } from '/static/pages/backends.js?v=11';
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Empty, Card, ConfirmDelete, Table, renderGuard, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, MonoText, QuickModal } from '/static/hoover/index.js?v=10';
|
||||
|
||||
const addRule = QuickModal({
|
||||
title: 'Add Rich Rule',
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
* Requires auth: rw permission.
|
||||
*/
|
||||
|
||||
import { h, definePage, reactive } from '/static/hoover/index.js?v=11';
|
||||
import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js?v=11';
|
||||
import { h, definePage, reactive } from '/static/hoover/index.js?v=13';
|
||||
import { html, PageHeader, Table, Badge, ConfirmDelete, Empty, Card, openModal, closeModal, formModal, apiFetch, toast, esc } from '/static/hoover/index.js?v=13';
|
||||
|
||||
const SUBSYSTEMS = [
|
||||
{ key: 'firewall', label: 'Firewall' },
|
||||
@@ -40,8 +40,8 @@ async function loadUsers(abortController) {
|
||||
|
||||
try {
|
||||
const [usersRes, countsRes] = await Promise.all([
|
||||
apiFetch('/api/auth/users'),
|
||||
apiFetch('/api/auth/webauthn/credential-counts'),
|
||||
apiFetch('/api/auth/users', { signal: abortController?.signal }),
|
||||
apiFetch('/api/auth/webauthn/credential-counts', { signal: abortController?.signal }),
|
||||
]);
|
||||
if (abortController?.signal?.aborted) return;
|
||||
|
||||
@@ -55,11 +55,15 @@ async function loadUsers(abortController) {
|
||||
state.error = usersRes.error || 'Failed to load users';
|
||||
}
|
||||
} catch (e) {
|
||||
if (!abortController?.signal?.aborted) {
|
||||
state.error = 'Failed to load users';
|
||||
}
|
||||
} finally {
|
||||
if (!abortController?.signal?.aborted) {
|
||||
state.loading = false;
|
||||
state.refreshing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function permissionLevel(perms, subsystem) {
|
||||
return perms[subsystem] || '—';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
/** WireGuard page — tunnel & peer management. */
|
||||
import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob, formAction, ApplyConfirm, qrSVG } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Badge, StatusDot, Empty, Table, ServiceStatus, renderGuard, esc, enc, $val, apiFetch, toast, openModal, closeModal, formModal, definePage, getModel, modelFetch, ActionButton, ActionCell, MonoText, ActionGroup, QuickModal, downloadBlob, formAction, ApplyConfirm, qrSVG } from '/static/hoover/index.js?v=10';
|
||||
|
||||
/* ── LAN detection helper ────────────────────────────────────── */
|
||||
function getLanSubnets() {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=9';
|
||||
import { html, PageHeader, Badge, Empty, ConfirmDelete, renderGuard, enc, esc, $val, definePage, getModel, MultiSelectModal, QuickModal } from '/static/hoover/index.js?v=10';
|
||||
|
||||
const addZone = QuickModal({
|
||||
title: 'Add Zone',
|
||||
|
||||
Reference in New Issue
Block a user