Load Bases via hidden embed

This commit is contained in:
Jack Williams 2026-06-15 22:39:27 +10:00
parent 33f7ab6105
commit 8713c66b1c
2 changed files with 62 additions and 94 deletions

View file

@ -1,4 +1,4 @@
import type { App, OpenViewState, ViewState, WorkspaceLeaf } from 'obsidian';
import type { App, Component } from 'obsidian';
import { TFile } from 'obsidian';
import { debugWarn } from '../../utils/logger';
@ -11,9 +11,6 @@ type ResolveOpts = {
pollMs?: number;
};
const nextFrame = (): Promise<void> =>
new Promise<void>((resolve) => window.requestAnimationFrame(() => resolve()));
const sleep = (ms: number): Promise<void> =>
new Promise<void>((resolve) => window.setTimeout(resolve, ms));
@ -22,27 +19,25 @@ const sleep = (ms: number): Promise<void> =>
type BasesControllerLike = {
results?: unknown;
initialScan?: unknown;
currentFile?: unknown;
};
type BasesViewLike = {
getViewType: () => string;
type BasesEmbedLike = Component & {
controller?: BasesControllerLike;
containingFile?: unknown;
loadFile: () => Promise<void>;
};
type BasesViewState = { file?: string; viewName?: string };
type EmbedInfo = { app: App; containerEl: HTMLElement };
function getBasesView(leaf: WorkspaceLeaf): BasesViewLike | undefined {
const view = leaf.view as unknown as BasesViewLike;
return view.getViewType() === 'bases' ? view : undefined;
}
function leafMatchesBase(leaf: WorkspaceLeaf, basePath: string, viewName: string): boolean {
const vs: ViewState = leaf.getViewState();
if (vs.type !== 'bases') return false;
const st = (vs.state ?? {}) as BasesViewState;
return st.file === basePath && st.viewName === viewName;
}
// `app.embedRegistry.embedByExtension['base']` is an undocumented internal API.
type AppWithEmbedRegistry = App & {
embedRegistry?: {
embedByExtension?: {
base?: (info: EmbedInfo, file: TFile, subpath: string) => BasesEmbedLike;
};
};
};
// ---- Results extraction ----
@ -60,52 +55,6 @@ function isResultsLike(value: unknown): value is ResultsLike {
);
}
function getUserLeaf(app: App): WorkspaceLeaf | null {
return app.workspace.getMostRecentLeaf() ?? app.workspace.getLeaf(false);
}
async function openBaseIntoLeaf(
app: App,
leaf: WorkspaceLeaf,
baseFile: TFile,
viewName: string,
): Promise<void> {
// Make the target leaf active so openLinkText opens into it.
app.workspace.setActiveLeaf(leaf, { focus: true });
await nextFrame();
const linktext = `${baseFile.path}#${viewName}`;
const openState: OpenViewState = { active: true };
await app.workspace.openLinkText(linktext, baseFile.path, false, openState);
// Give Obsidian time to attach the view and controller.
await nextFrame();
await nextFrame();
}
async function awaitBasesControllerReady(
leaf: WorkspaceLeaf,
basesView: BasesViewLike,
basePath: string,
viewName: string,
timeoutMs: number,
pollMs: number,
): Promise<BasesControllerLike> {
const started = performance.now();
const deadline = started + timeoutMs;
while (performance.now() < deadline) {
if (leafMatchesBase(leaf, basePath, viewName)) {
const controller = basesView.controller;
if (controller) return controller;
}
await sleep(pollMs);
}
throw new Error('[Glicko][Bases] Timed out waiting for Bases controller');
}
/**
* Wait for Bases to finish producing results.
*
@ -155,8 +104,30 @@ function extractMarkdownFilesFromControllerResults(controller: BasesControllerLi
}
/**
* Resolve Markdown files from a .base file + view name by opening the Base,
* waiting for results to settle, extracting `TFile`s, then cleaning up.
* Load a .base file + view into a hidden embed.
*/
function createHiddenBaseEmbed(
app: App,
containerEl: HTMLElement,
baseFile: TFile,
viewName: string,
): BasesEmbedLike {
const embedFactory = (app as AppWithEmbedRegistry).embedRegistry?.embedByExtension?.base;
if (typeof embedFactory !== 'function') {
throw new Error('[Glicko][Bases] Bases embed registry is unavailable');
}
const embed = embedFactory({ app, containerEl }, baseFile, viewName);
embed.containingFile = baseFile;
if (embed.controller) embed.controller.currentFile = baseFile;
return embed;
}
/**
* Resolve Markdown files from a .base file + view name by loading the Base into a hidden
* embed, waiting for results to settle, extracting TFiles, then tearing the embed down.
*/
export async function resolveFilesFromBaseView(
app: App,
@ -173,48 +144,36 @@ export async function resolveFilesFromBaseView(
const pollMs = opts?.pollMs ?? DEFAULT_POLL_MS;
const timeoutMs = opts?.timeoutMs ?? TIMEOUT_MS;
const previousLeaf = getUserLeaf(app);
let leaf: WorkspaceLeaf | null = null;
const containerEl = app.workspace.containerEl.createDiv({
cls: 'glicko-offscreen',
attr: { 'aria-hidden': 'true' },
});
let embed: BasesEmbedLike | null = null;
try {
leaf = app.workspace.getLeaf('tab');
if (!leaf) throw new Error('[Glicko][Bases] Could not create a workspace leaf');
embed = createHiddenBaseEmbed(app, containerEl, baseFile, viewName);
embed.load();
await embed.loadFile();
await openBaseIntoLeaf(app, leaf, baseFile, viewName);
const basesView = getBasesView(leaf);
if (!basesView) {
const viewType = leaf.view.getViewType();
throw new Error(`[Glicko][Bases] Unexpected view type: ${String(viewType)}`);
const controller = embed.controller;
if (!controller) {
throw new Error('[Glicko][Bases] Bases controller unavailable after loadFile');
}
const controller = await awaitBasesControllerReady(
leaf,
basesView,
baseFile.path,
viewName,
timeoutMs,
pollMs,
);
await waitForResultsToSettle(controller, timeoutMs, pollMs);
return extractMarkdownFilesFromControllerResults(controller);
} finally {
// Restore the user's previous leaf
try {
if (leaf && previousLeaf && previousLeaf !== leaf) {
app.workspace.setActiveLeaf(previousLeaf, { focus: true });
await nextFrame();
}
embed?.unload();
} catch (e) {
debugWarn('Bases resolver: failed to restore previous leaf', e);
debugWarn('Bases resolver: failed to unload hidden base embed', e);
}
try {
leaf?.detach();
containerEl.remove();
} catch (e) {
debugWarn('Bases resolver: failed to detach temporary leaf', e);
debugWarn('Bases resolver: failed to remove hidden container', e);
}
}
}

View file

@ -208,3 +208,12 @@
font-size: var(--font-ui-medium);
flex: 1 1 7rem;
}
/* Off-screen container used to resolve Base results without a visible tab. */
.glicko-offscreen {
position: absolute;
left: -9999px;
width: 1px;
height: 1px;
overflow: hidden;
}