- features/apps/index.js (/apps*) and features/app-detail/index.js (/app*)
as two features; /apps* registered first so wildcard precedence holds.
Both drive the system-loader-pre-initialized singletons (appsManager /
appTabbedManager) via .initialize(), mirroring the legacy handlers exactly
(incl. app-detail's legacy ?app=/?=name parsing + ?tab=/?config= rewrite).
- kernel/lifecycle.js: ctx.nav(path, addToHistory) so app-detail's empty-name
redirect matches navigate('/apps', false) exactly.
unmount is a no-op for both (shared singletons); the app-tabbed-manager
listener-rebind leak is pre-existing and handled in a follow-up.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Signed-off-by: librelad <librelad@digitalangels.vip>
71 lines
2.9 KiB
JavaScript
71 lines
2.9 KiB
JavaScript
// kernel/lifecycle.js — the MountContext a feature module receives.
|
|
//
|
|
// Part of the feature-module architecture (docs/frontend-modularization.md §2.4).
|
|
// A feature exports mount(ctx)/unmount(ctx); the SPA shell drives them and hands
|
|
// in this context. In this first form the content helpers delegate to the SPA
|
|
// shell (so rendering is byte-identical to the legacy handlers), plus a teardown
|
|
// ledger: any listener registered via ctx.on() or subscription via ctx.sub() is
|
|
// auto-released on unmount, so a feature can't leak document listeners or live
|
|
// streams across navigations. Shared-service injection (ctx.services) lands with
|
|
// the Phase 2 DI container.
|
|
(function () {
|
|
const LP = (window.LP = window.LP || {});
|
|
|
|
class MountContext {
|
|
constructor(shell, route) {
|
|
this.shell = shell; // the LibrePortalSPAClean instance
|
|
this.route = route || {};
|
|
this.path = this.route.path || (location.pathname + location.search);
|
|
this._ac = new AbortController(); // backs ctx.on() — abort kills all
|
|
this._unsubs = []; // backs ctx.sub() — SSE/bus/timer handles
|
|
}
|
|
|
|
// Lazy-load the feature's controller scripts (idempotent across re-mounts).
|
|
loadScripts(list) {
|
|
return Promise.all((list || []).map(src => this.shell.loadScript(src)));
|
|
}
|
|
|
|
// Fetch an HTML fragment (same path the legacy handlers used).
|
|
loadFragment(url) {
|
|
return this.shell.fetchContent(url);
|
|
}
|
|
|
|
// Inject fragment HTML into #main-content + set the document title +
|
|
// refresh nav highlighting — identical to the legacy loadContent().
|
|
setContent(html, title) {
|
|
return this.shell.loadContent(html, title);
|
|
}
|
|
|
|
// SPA navigation (for redirects from within a feature). addToHistory mirrors
|
|
// navigate()'s second arg so a feature can do a no-history redirect exactly
|
|
// like the legacy handlers (e.g. handleAppDetail's navigate('/apps', false)).
|
|
nav(path, addToHistory = true) {
|
|
return this.shell.navigate(path, addToHistory);
|
|
}
|
|
|
|
// addEventListener bound to this mount's AbortController — auto-removed on
|
|
// teardown. target = window | document | a bus | an element.
|
|
on(target, event, fn, opts) {
|
|
target.addEventListener(event, fn, Object.assign({}, opts, { signal: this._ac.signal }));
|
|
}
|
|
|
|
// Track an arbitrary unsubscribe fn (SSE handle, bus off(), clearInterval
|
|
// wrapper) so it's called on teardown.
|
|
sub(unsub) {
|
|
if (typeof unsub === 'function') this._unsubs.push(unsub);
|
|
return unsub;
|
|
}
|
|
|
|
// Release everything opened during this mount. Called by the shell after
|
|
// the feature's own unmount().
|
|
teardown() {
|
|
try { this._ac.abort(); } catch (_) {}
|
|
this._unsubs.forEach(u => { try { u(); } catch (_) {} });
|
|
this._unsubs.length = 0;
|
|
}
|
|
}
|
|
|
|
LP.kernel = LP.kernel || {};
|
|
LP.kernel.MountContext = MountContext;
|
|
})();
|