import { EVENT_PLUGIN_UNLOADED } from "@vrtmrz/livesync-commonlib/compat/events/coreEvents"; import type { ServiceContext } from "@vrtmrz/livesync-commonlib/context"; import { setupDialogContext, type ComponentHasResult, type DialogContext, type DialogHostProps, type DialogSvelteComponentBaseProps, type SvelteDialogManagerDependencies, } from "@vrtmrz/livesync-commonlib/compat/services/implements/base/SvelteDialog"; import { fireAndForget, promiseWithResolvers, type PromiseWithResolvers } from "octagonal-wheels/promises"; import { mount, unmount, type Component } from "svelte"; /** Host dialogue surface controlled by an explicit session. */ export interface SvelteDialogSurface { readonly contentEl: HTMLElement; setTitle(title: string): unknown; close(): void; } type MountedDialog = ReturnType; /** Injectable renderer used to keep the dialogue lifecycle independently testable. */ export interface SvelteDialogRenderer { mount(dialogHost: Component, target: HTMLElement, props: DialogHostProps): MountedDialog; unmount(component: MountedDialog): Promise; } export interface SvelteDialogSessionOptions { surface: SvelteDialogSurface; context: TContext; dependencies: SvelteDialogManagerDependencies; dialogHost: Component; component: ComponentHasResult; initialData?: TInitial; renderer?: SvelteDialogRenderer; signal?: AbortSignal; } const svelteDialogRenderer: SvelteDialogRenderer = { mount(dialogHost, target, props) { return mount(dialogHost, { target, props }); }, async unmount(component) { await unmount(component); }, }; /** Owns one Svelte dialogue result and mount lifecycle for a concrete host surface. */ export class SvelteDialogSession { private readonly surface: SvelteDialogSurface; private readonly context: TContext; private readonly dependencies: SvelteDialogManagerDependencies; private readonly dialogHost: Component; private readonly component: ComponentHasResult; private readonly initialData?: TInitial; private readonly renderer: SvelteDialogRenderer; private readonly signal?: AbortSignal; private result?: TResult; private pendingResult?: PromiseWithResolvers; private mountedDialog?: MountedDialog; private unregisterUnload?: () => void; private unregisterAbort?: () => void; constructor(options: SvelteDialogSessionOptions) { this.surface = options.surface; this.context = options.context; this.dependencies = options.dependencies; this.dialogHost = options.dialogHost; this.component = options.component; this.initialData = options.initialData; this.renderer = options.renderer ?? svelteDialogRenderer; this.signal = options.signal; } onOpen(): void { if (this.signal?.aborted) throw this.signal.reason ?? new Error("Plug-in unloaded"); this.surface.contentEl.replaceChildren(); this.unregisterAbort?.(); this.unregisterAbort = undefined; this.unregisterUnload?.(); this.unregisterUnload = undefined; if (this.pendingResult) { this.pendingResult.reject(new Error("Dialogue opened again")); } this.result = undefined; const pendingResult = promiseWithResolvers(); this.pendingResult = pendingResult; const cancel = (reason: unknown) => { if (this.pendingResult !== pendingResult) { return; } this.pendingResult = undefined; pendingResult.reject(reason); this.surface.close(); }; this.unregisterUnload = this.context.events.onceEvent(EVENT_PLUGIN_UNLOADED, () => { cancel(new Error("Plug-in unloaded")); }); const signal = this.signal; if (signal) { const onAbort = () => cancel(signal.reason ?? new Error("Plug-in unloaded")); signal.addEventListener("abort", onAbort, { once: true }); this.unregisterAbort = () => signal.removeEventListener("abort", onAbort); } this.mountedDialog = this.renderer.mount(this.dialogHost, this.surface.contentEl, { onSetupContext: (props: DialogSvelteComponentBaseProps) => { setupDialogContext({ ...props, context: this.context, services: this.dependencies, } satisfies DialogContext); }, setTitle: (title: string) => { this.surface.setTitle(title); }, closeDialog: () => { this.surface.close(); }, setResult: (result: TResult) => { this.result = result; }, getInitialData: () => this.initialData, mountComponent: this.component, }); } waitForClose(): Promise { if (!this.pendingResult) { throw new Error("Dialogue has not been opened"); } return this.pendingResult.promise; } onClose(): void { this.unregisterAbort?.(); this.unregisterAbort = undefined; this.unregisterUnload?.(); this.unregisterUnload = undefined; this.pendingResult?.resolve(this.result); this.pendingResult = undefined; const mountedDialog = this.mountedDialog; this.mountedDialog = undefined; if (mountedDialog) { fireAndForget(() => this.renderer.unmount(mountedDialog)); } } }