import { css, html, PropertyValues } from "lit"; import { applyCommand, AppState, clone, isAppViewEqual, } from "../lib/app/mod.ts"; import { BaseView, createDefaultAppState, SHELL_COMMAND } from "./BaseView.ts"; import { Command, isCommand } from "../lib/app/commands.ts"; import { AppUpdateEvent } from "../lib/app/events.ts"; import { KeyStore } from "@commontools/identity"; import { property, state } from "lit/decorators.js"; import { Task } from "@lit/task"; import { type MemorySpace, Runtime } from "@commontools/runner"; import { RuntimeInternals } from "../lib/runtime.ts"; import { runtimeContext, spaceContext } from "@commontools/ui"; import { provide } from "@lit/context"; // The root element for the shell application. // // Derives `RuntimeInternals` for the application from its `AppState`. // `Command` mutates the app state, which can be fired as events // from children elements. export class XRootView extends BaseView { static override styles = css` :host { display: block; width: 100%; height: 100vh; padding: var(--padding-desktop, 15px); } @media (max-width: 767px) { :host { padding: var(--padding-mobile, 5px); } } #body { height: 100%; width: 100%; } `; @state() app = createDefaultAppState(); @property() keyStore?: KeyStore; @provide({ context: runtimeContext }) @state() private runtime?: Runtime; @provide({ context: spaceContext }) @state() private space?: MemorySpace; // The runtime task runs when AppState changes, and determines // if a new RuntimeInternals must be created, like when // identity or space change. This is manually run in `updated()` // because we want to compare to previous values, leaving this // function responsible for cleaning up previous runtimes, and // creating a new one. private _rt = new Task<[AppState | undefined], RuntimeInternals | undefined>( this, { // Do not define `args` -- this is run in "manual mode", // or manually triggered from parsing `AppState` in `updated()` // to determine if we need to dispose or recreate a runtime, // whereas in a task we don't have access to necessary info // like previous app state. task: async ([app]: [AppState | undefined], { signal }) => { const previous = this._rt.value; if (previous) { previous.dispose().catch(console.error); } if (!app || !app.identity) { // Clear the runtime and space when no app state this.runtime = undefined; this.space = undefined; return undefined; } const rt = await RuntimeInternals.create({ identity: app.identity, view: app.view, apiUrl: app.apiUrl, }); if (signal.aborted) { rt.dispose().catch(console.error); this.runtime = undefined; this.space = undefined; return; } // Update the provided runtime and space values this.runtime = rt.runtime(); this.space = rt.space() as MemorySpace; // Use the DID from the session return rt; }, }, ); override connectedCallback(): void { super.connectedCallback(); this.addEventListener(SHELL_COMMAND, this.onCommand); } override disconnectedCallback(): void { this.removeEventListener(SHELL_COMMAND, this.onCommand); super.disconnectedCallback(); } protected override updated(changedProperties: PropertyValues): void { if (!changedProperties.has("app")) { return; } const previous = changedProperties.get("app"); const current = this.app; // If the first set, or if removed, run const flipState = (!previous && current) || !current; let spaceChanged = false; if (previous && !isAppViewEqual(previous.view, current.view)) { // Check that if the view has changed, we may still // be in the same space if ("spaceName" in previous.view && "spaceName" in current.view) { spaceChanged = previous.view.spaceName !== current.view.spaceName; } else { spaceChanged = true; } } // If host, view's space, or identity changes, we'll // need to recreate the runtime. const stateChanged = !!previous && (previous.apiUrl !== current.apiUrl || previous.identity !== current.identity || spaceChanged); if (flipState || stateChanged) { this._rt.run([current]); } } onCommand = (e: Event) => { const { detail: command } = e as CustomEvent; if (!isCommand(command)) { throw new Error(`Received a non-command: ${command}`); } this.processCommand(command); }; apply(command: Command): Promise { this.processCommand(command); this.requestUpdate(); return this.updateComplete.then((_) => undefined); } state(): AppState { return clone(this.app); } private processCommand(command: Command) { try { // Apply command synchronously for state changes const state = applyCommand(this.app, command); this.app = state; this.dispatchEvent(new AppUpdateEvent(command, { state })); } catch (e) { const error = e as Error; this.dispatchEvent( new AppUpdateEvent(command, { error: error as Error }), ); throw new Error(error.message, { cause: error }); } } override render() { return html` `; } } globalThis.customElements.define("x-root-view", XRootView);