import { type ActorRefFrom, assertEvent, enqueueActions, setup } from "xstate"; import type { Application } from "../core/applications/application-list"; import { type FederationInstance } from "./module-federation"; import { remoteLogic } from "./remote.machine"; /** The shared federation instance, supplied by the root machine. */ type RemotesInput = { instance: FederationInstance; }; type RemotesContext = { instance: FederationInstance; /** One {@link remoteLogic} per federated application, keyed by app id. */ children: Map>; }; type RemotesEvent = // The full application list, sent by the applications machine whenever it // changes. The supervisor reconciles one child per federated app against it. | { type: "remotes.sync"; applications: ReadonlyArray } // The UI asking to load an expose (a panel, or the App view as its island // mounts); routed to the owning app's remote. | { type: "remote.load.request"; moduleId: string } // The UI asking to warm an expose's assets ahead of time (e.g. the App view // on dock hover); routed to the owning app's remote. | { type: "remote.preload.request"; moduleId: string }; /** The remote name (app id) that owns a fully-qualified `moduleId`. */ function appIdOf(moduleId: string): string { return moduleId.slice(0, moduleId.indexOf("/")); } /** * Supervisor for federated application remotes. * * Aligned with how an OS loads its installed applications at startup: the * applications machine sends the whole list via `remotes.sync`, and this spawns * one {@link remoteLogic} per federated app (keyed by id). Each child * registers its own remote, runs its workers, and warms its panels — so the * whole set is known and warmed upfront rather than loaded lazily at first use. * * On every `remotes.sync` it reconciles: a newly-listed app is spawned, a * removed one is stopped (disposing its workers and warmed panels by cascade), * and a still-listed app is handed the fresh data so it can reconcile its own * interfaces. On-demand loads and preloads (`remote.load.request` / * `remote.preload.request`) are routed to the owning app's remote. * * Uses the shared {@link FederationInstance} (the `workbench-applications` * instance, supplied by the root machine). Invoked at the root of the OS * machine for inspector visibility — boot does not gate on it. * * @internal */ export const remotesLogic = setup({ types: { input: {} as RemotesInput, context: {} as RemotesContext, events: {} as RemotesEvent, }, actors: { remote: remoteLogic, }, actions: { /** * Keep the per-app remotes in step with the list: an app that appeared is * spawned, one that left is stopped (disposing its workers and panels by * cascade), and a survivor is handed the fresh app to update its own * interfaces. */ reconcileRemotes: enqueueActions(({ context, event, enqueue }) => { assertEvent(event, "remotes.sync"); const desired = new Map( event.applications .filter((application) => application.isFederated) .map((application) => [application.id, application]), ); for (const [id, ref] of context.children) { if (desired.has(id)) { enqueue.sendTo(ref, { type: "app.update", app: desired.get(id)! }); } else { enqueue.stopChild(ref); } } enqueue.assign({ children: ({ context: ctx, spawn }) => { const next = new Map>(); for (const [id, ref] of ctx.children) { if (desired.has(id)) next.set(id, ref); } for (const [id, app] of desired) { if (next.has(id)) continue; next.set( id, spawn("remote", { id, systemId: `remotes.${id}`, input: { app, instance: ctx.instance }, }), ); } return next; }, }); }), /** Forward an on-demand load to the app remote that owns the moduleId. */ routeLoad: enqueueActions(({ context, event, enqueue }) => { assertEvent(event, "remote.load.request"); const child = context.children.get(appIdOf(event.moduleId)); // No child yet (app not listed / not federated) → nothing to load. if (child) { enqueue.sendTo(child, { type: "expose.load.request", moduleId: event.moduleId, }); } }), /** Forward a warm-ahead-of-time to the app remote that owns the moduleId. */ routePreload: enqueueActions(({ context, event, enqueue }) => { assertEvent(event, "remote.preload.request"); const child = context.children.get(appIdOf(event.moduleId)); // No child yet (app not listed / not federated) → nothing to warm. if (child) { enqueue.sendTo(child, { type: "expose.preload.request", moduleId: event.moduleId, }); } }), }, }).createMachine({ id: "remotes", context: ({ input }) => ({ instance: input.instance, children: new Map(), }), on: { "remotes.sync": { actions: "reconcileRemotes" }, "remote.load.request": { actions: "routeLoad" }, "remote.preload.request": { actions: "routePreload" }, }, });