ca27ea5522
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.
124 lines
3.4 KiB
JavaScript
124 lines
3.4 KiB
JavaScript
/**
|
|
* Hoover — component.js
|
|
*
|
|
* Component wrapper: definePage, lifecycle hooks, state caching.
|
|
*
|
|
* definePage wraps a page definition into a renderer function compatible
|
|
* with hoover's render engine. Handles reactive state creation and
|
|
* lifecycle management. Data loading is handled by the model layer.
|
|
*
|
|
* Usage:
|
|
* export default definePage({
|
|
* init() { return { firewall: getModel('firewall') }; },
|
|
* async load(state) { ... }, // optional, for one-time setup
|
|
* render(state) { return [vnodes],
|
|
* });
|
|
*/
|
|
|
|
import { reactive } from './reactivity.js?v=9';
|
|
import { h } from './vdom.js?v=9';
|
|
import { _compExpandedCache } from './render.js?v=9';
|
|
|
|
/** Registry of mounted components: key → { state } */
|
|
const _mounted = new Map();
|
|
|
|
/**
|
|
* Define a page component.
|
|
*
|
|
* @param {object} def — Page definition
|
|
* @param {function} def.init — Return initial state object
|
|
* @param {function} [def.load] — Optional one-time setup called on mount
|
|
* @param {function} def.render — Render function that returns vnodes
|
|
* @returns {object} — Component renderer compatible with h('#comp', ...)
|
|
*/
|
|
export function definePage(def) {
|
|
let state = null;
|
|
let stateInitialized = false;
|
|
|
|
const renderer = () => {
|
|
if (!stateInitialized) {
|
|
state = reactive(def.init());
|
|
stateInitialized = true;
|
|
}
|
|
return def.render(state);
|
|
};
|
|
|
|
renderer._pageDef = {
|
|
get state() {
|
|
if (!stateInitialized) {
|
|
state = reactive(def.init());
|
|
stateInitialized = true;
|
|
}
|
|
return state;
|
|
},
|
|
load: def.load || null,
|
|
onUnmount: def.onUnmount || null,
|
|
};
|
|
|
|
return renderer;
|
|
}
|
|
|
|
/**
|
|
* Mount a page component. Called by the render engine when a #comp vnode
|
|
* enters the tree for the first time.
|
|
*/
|
|
export function mountComponent(key, renderer) {
|
|
const pd = renderer._pageDef;
|
|
if (!pd) return;
|
|
|
|
let entry = _mounted.get(key);
|
|
|
|
if (entry) {
|
|
// Re-mount: component already exists with its state.
|
|
// 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) {
|
|
const abortController = new AbortController();
|
|
entry.abortController = abortController;
|
|
Promise.resolve().then(() => pd.load(pd.state, abortController));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Unmount a page component. Called by the render engine when a #comp vnode
|
|
* is removed from the tree.
|
|
*/
|
|
export function unmountComponent(key, renderer) {
|
|
const entry = _mounted.get(key);
|
|
if (!entry) return;
|
|
|
|
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 (_) {}
|
|
}
|
|
|
|
_compExpandedCache.delete(key);
|
|
_mounted.delete(key);
|
|
}
|
|
|
|
/**
|
|
* Get the state of a mounted component.
|
|
*/
|
|
export function getComponentState(key) {
|
|
const entry = _mounted.get(key);
|
|
return entry ? entry.state : null;
|
|
}
|
|
|
|
/**
|
|
* Create a component vnode that the render engine will wire up to lifecycle.
|
|
*/
|
|
export function hComp(renderer, key) {
|
|
return h('#comp', { component: renderer, key }, []);
|
|
}
|