import { isEqual } from "es-toolkit"; import type { Observable } from "rxjs"; import { assertEvent, assign, fromCallback, fromObservable, not, raise, setup, } from "xstate"; import type { ApplicationList } from "../core/applications/application-list"; import { logger } from "../core/log"; import { os } from "../runtime/bus"; import type { PayloadOf, ValueOf } from "../runtime/topics"; type PanelsMode = NonNullable>; type Panel = NonNullable["value"]>; type PanelRequest = NonNullable>; const panelKey = (appId: string, name: string) => `${appId}:${name}`; // Each app's named panels remember their own aside width, so a mode request // reopens a panel at the width it was last dragged to. A size request records a // new width and applies it live when that panel is the open aside, which lets an // app resize with one event. const applyPanelRequest = ( sizes: Record, current: Panel | null, value: PanelRequest | null, appId: string, ): { sizes: Record; panel: Panel | null } => { if (!value) return { sizes, panel: null }; const key = panelKey(appId, value.name); if ("mode" in value) { const panel: Panel = value.mode === "aside" ? { appId, name: value.name, mode: "aside", size: sizes[key] } : { appId, name: value.name, mode: "full" }; return { sizes, panel }; } const opensThisAside = current?.mode === "aside" && current.appId === appId && current.name === value.name; return { sizes: { ...sizes, [key]: value.size }, panel: opensThisAside ? { ...current, size: value.size } : current, }; }; const panelRequests = fromCallback(({ sendBack }) => { const controller = new AbortController(); os.subscribe( "panels.mode.set", (message) => sendBack({ type: "panel.set", value: message.payload, appId: message.meta.appId, }), { signal: controller.signal }, ); return () => controller.abort(); }); type PanelsInput = { /** * The organization's applications, or `null` while there is no list to check a * request against — not yet resolved, or failed. */ applications: Observable; }; const applicationsFeed = fromObservable< ApplicationList | null, PanelsInput["applications"] >(({ input }) => input); type PanelsContext = { applications$: PanelsInput["applications"]; applications: ApplicationList | null; /** The one open panel. */ panel: Panel | null; /** Panel widths, keyed by `${appId}:${name}`. */ panelSizes: Record; }; type PanelsEvent = | { type: "panel.set"; value: PanelRequest | null; appId: string } | { type: "applications.changed"; applications: ApplicationList | null }; /** * Owns the open panel and each app's remembered panel widths. Panels are * addressed by the app id the bus stamps on a request, checked against the panel * views that app declares. */ export const panelsLogic = setup({ types: { input: {} as PanelsInput, context: {} as PanelsContext, events: {} as PanelsEvent, }, actors: { panelRequests, applicationsFeed }, guards: { // An app drives only its own panel. The dock opens an app's panel by // speaking as that app, so the host needs no exception here. appOwnsPanel: ({ context, event }) => { assertEvent(event, "panel.set"); return event.value !== null || context.panel?.appId === event.appId; }, appDeclaresPanel: ({ context, event }) => { assertEvent(event, "panel.set"); const { applications } = context; const request = event.value; if (!applications || !request || !("mode" in request)) return true; const matches = applications.applications.filter( (candidate) => candidate.id === event.appId, ); const application = matches.find((candidate) => candidate.isLocal) ?? matches[0]; return Boolean( application ?.interfaces("panel") .some((view) => view.name === request.name), ); }, }, actions: { storeApplications: assign({ applications: ({ event }) => { assertEvent(event, "applications.changed"); return event.applications; }, }), warnUndeclaredPanel: ({ event }) => { assertEvent(event, "panel.set"); logger.warn("Ignored a panel request: the app declares no such panel.", { appId: event.appId, name: event.value?.name, }); }, publishPanel: ({ context, event }) => { assertEvent(event, "panel.set"); const { panel } = applyPanelRequest( context.panelSizes, context.panel, event.value, event.appId, ); if (isEqual(panel, context.panel)) return; os.emit("panels.mode", { ok: true, value: panel }); }, storePanel: assign(({ context, event }) => { assertEvent(event, "panel.set"); const { sizes, panel } = applyPanelRequest( context.panelSizes, context.panel, event.value, event.appId, ); return { panelSizes: sizes, panel }; }), }, }).createMachine({ id: "panels", invoke: [ { src: "panelRequests" }, { src: "applicationsFeed", input: ({ context }) => context.applications$, onSnapshot: { guard: ({ event }) => event.snapshot.context !== undefined, actions: raise(({ event }) => ({ type: "applications.changed" as const, applications: event.snapshot.context ?? null, })), }, }, ], context: ({ input }) => ({ applications$: input.applications, applications: null, panel: null, panelSizes: {}, }), on: { "applications.changed": { actions: [{ type: "storeApplications" }] }, "panel.set": [ { guard: not("appDeclaresPanel"), actions: [{ type: "warnUndeclaredPanel" }], }, { guard: "appOwnsPanel", actions: [{ type: "publishPanel" }, { type: "storePanel" }], }, ], }, });