feat(console-web): add bounded iframe keep-alive

- opt service and settings pages into document retention
- preserve iframe identity and local Solid state across navigation
- cap retained documents with deterministic LRU eviction
- deliver activate, deactivate, and dispose lifecycle messages
- reject commands emitted by inactive page documents
- expose page activity as a Solid accessor
- test retention, recreation, eviction, and invalid limits
This commit is contained in:
Maofeng
2026-07-30 09:43:08 +08:00
parent ccad05fa5a
commit 1b2c50c951
9 changed files with 360 additions and 52 deletions
+12 -7
View File
@@ -14,6 +14,7 @@ export const SDK_VERSION = 1;
export type RuntimeAction = "start" | "stop" | "restart";
export type ServiceAction = "start" | "stop" | "restart";
export type PageLifecyclePhase = "activate" | "deactivate" | "dispose";
export type HostSharedState = {
// This is a serializable snapshot, never a Solid signal crossing realms.
@@ -67,7 +68,8 @@ export type HostMessage =
channel: typeof SDK_CHANNEL;
version: typeof SDK_VERSION;
source: "host";
type: "dispose";
type: "lifecycle";
phase: PageLifecyclePhase;
};
export function isPageMessage(value: unknown): value is PageMessage {
@@ -82,12 +84,15 @@ export function isPageMessage(value: unknown): value is PageMessage {
export function isHostMessage(value: unknown): value is HostMessage {
if (!isRecord(value)) return false;
return (
value.channel === SDK_CHANNEL &&
value.version === SDK_VERSION &&
value.source === "host" &&
(value.type === "state" || value.type === "dispose")
);
if (value.channel !== SDK_CHANNEL || value.version !== SDK_VERSION || value.source !== "host") {
return false;
}
if (value.type === "state") return isRecord(value.state);
return value.type === "lifecycle" && isPageLifecyclePhase(value.phase);
}
function isPageLifecyclePhase(value: unknown): value is PageLifecyclePhase {
return value === "activate" || value === "deactivate" || value === "dispose";
}
function isRecord(value: unknown): value is Record<string, unknown> {