import { isEqual } from "es-toolkit"; import { assertEvent, assign, fromObservable, raise, sendTo, setup, } from "xstate"; import { ApplicationList, type Application, } from "../../core/applications/application-list"; import type { RemoteModule } from "../../core/remote-module"; import { os } from "../../runtime/bus"; import type { NavigationAdapter } from "../navigation/machine"; import { applicationStore$, type ApplicationStoreInput, type OrganizationApplications, } from "./application-store"; type ApplicationsInput = ApplicationStoreInput & { location: NavigationAdapter["location"]; }; type ApplicationsContext = { instance: ApplicationStoreInput["instance"]; organizationId: ApplicationStoreInput["organizationId"]; localApplications: ApplicationStoreInput["localApplications"]; appConfigs: ApplicationStoreInput["appConfigs"]; projects: ApplicationStoreInput["projects"]; applications: | ApplicationList> | undefined; /** Why the list could not be assembled; `undefined` while it still can be. */ listError: unknown; published: { foreground: Application | null; /** * `undefined` until the first sync, so an organization that resolves to no * config still publishes that `null` once. */ mediaLibraryConfig: RemoteModule | null | undefined; }; pathname: string | undefined; commits$: ApplicationsInput["location"]; }; type ApplicationsEvent = // Raised from the invoked `applicationStore` actor: a whole rebuilt list, // whenever any resource it draws on changes. | { type: "applications.sync"; applications: NonNullable; mediaLibraryConfig: RemoteModule | null; } | { type: "applications.error"; error?: unknown } | { type: "location.updated"; pathname: string }; // Derived on demand from committed state, so nothing has to keep the // foreground in sync with the location and the list separately. const foregroundOf = ({ applications, pathname }: ApplicationsContext) => pathname === undefined || applications === undefined ? null : (applications.findByPath(pathname) ?? null); const applicationStoreLogic = fromObservable< OrganizationApplications, ApplicationStoreInput >(({ input }) => applicationStore$(input)); /** * Owns the application list's state and everything published from it. Assembly * lives in the invoked `applicationStore` actor, so tests can drive this machine * with a stub actor instead of standing up five resource stores. */ export const applicationsLogic = setup({ types: { input: {} as ApplicationsInput, context: {} as ApplicationsContext, events: {} as ApplicationsEvent, tags: {} as "applications-resolved" | "error", }, actors: { applicationStore: applicationStoreLogic, commitFeed: fromObservable( ({ input }) => input, ), }, actions: { // Reads context, so it must run after `sync` has assigned. publishApplications: ({ context }) => { if (!context.applications) return; os.emit("applications.list", { ok: true, value: context.applications.toJSON(), }); }, publishApplicationsError: () => { os.emit("applications.list", { ok: false }); }, /** * Reads the event, not context: the config is published straight from the * emission that carried it, so nothing has to be relayed through context to * get here. */ publishMediaLibraryConfig: ({ context, event }) => { assertEvent(event, "applications.sync"); if ( isEqual(event.mediaLibraryConfig, context.published.mediaLibraryConfig) ) { return; } os.emit("media-libraries.config", event.mediaLibraryConfig); }, storeApplicationsError: assign({ listError: ({ event }) => { assertEvent(event, "applications.error"); // Normalised so `undefined` always means "still fine", even when the // failure carried no error value. return event.error ?? new Error("The application list failed to load."); }, }), sync: assign({ applications: ({ event }) => { assertEvent(event, "applications.sync"); return event.applications; }, }), storeLocation: assign({ pathname: ({ event }) => { assertEvent(event, "location.updated"); return event.pathname; }, }), publishForeground: ({ context }) => { const foreground = foregroundOf(context); if ( ApplicationList.areApplicationsEqual( foreground, context.published.foreground, ) ) { return; } os.emit("applications.foreground", foreground?.id ?? null); }, /** * Hands the remotes supervisor the whole application list. It owns what a * remote is — which apps are federated, and how each maps to its workers * and panels — so this machine stays about the data/classes and just passes * them on. The supervisor reconciles a per-app remote against the list, and * each remote registers itself, runs its workers, and warms its panels. */ syncRemotes: sendTo( ({ self }) => self.system.get("remotes"), ({ event }) => { assertEvent(event, "applications.sync"); return { type: "remotes.sync", applications: event.applications.applications, }; }, ), recordPublished: assign({ published: ({ context, event }) => ({ foreground: foregroundOf(context), // Only a sync carries a config; navigation leaves the last one standing. mediaLibraryConfig: event.type === "applications.sync" ? event.mediaLibraryConfig : context.published.mediaLibraryConfig, }), }), }, }).createMachine({ id: "applications", invoke: [ { src: "commitFeed", input: ({ context }) => context.commits$, onSnapshot: { guard: ({ event }) => event.snapshot.context !== undefined, actions: raise(({ event }) => ({ type: "location.updated" as const, // Only the path decides which application is in the foreground. pathname: new URL(event.snapshot.context!, window.location.origin) .pathname, })), }, }, { src: "applicationStore", input: ({ context }) => ({ instance: context.instance, organizationId: context.organizationId, localApplications: context.localApplications, appConfigs: context.appConfigs, projects: context.projects, }), // Raised as an event so the actor's emissions and any future sender share // one path for "the list changed". onSnapshot: { guard: ({ event }) => Boolean(event.snapshot.context), actions: raise(({ event }) => ({ type: "applications.sync" as const, applications: event.snapshot.context!.applications, mediaLibraryConfig: event.snapshot.context!.mediaLibraryConfig, })), }, onError: { actions: raise(({ event }) => ({ type: "applications.error" as const, error: event.error, })), }, }, ], context: ({ input }) => ({ instance: input.instance, organizationId: input.organizationId, localApplications: input.localApplications, appConfigs: input.appConfigs, projects: input.projects, commits$: input.location, applications: undefined, listError: undefined, pathname: undefined, published: { foreground: null, mediaLibraryConfig: undefined }, }), initial: "loading", states: { loading: {}, // No entry actions, so the re-entry every `applications.sync` causes is free. ready: { tags: ["applications-resolved"] }, failed: { tags: ["error"] }, }, on: { "location.updated": { actions: [ { type: "storeLocation" }, { type: "publishForeground" }, { type: "recordPublished" }, ], }, "applications.error": { target: ".failed", actions: [ { type: "publishApplicationsError" }, { type: "storeApplicationsError" }, ], }, "applications.sync": { target: ".ready", actions: [ // Order matters: state is assigned before anything publishes from it, // and the remotes are synced before the list is published — so no // island can mount and request a load before its remote exists. { type: "sync" }, { type: "syncRemotes" }, { type: "publishApplications" }, { type: "publishMediaLibraryConfig" }, { type: "publishForeground" }, { type: "recordPublished" }, ], }, }, });