mirror of
https://github.com/williamsjack/pairwise-glicko-ranking.git
synced 2026-07-22 06:44:09 +00:00
Load Bases via hidden embed
This commit is contained in:
parent
33f7ab6105
commit
8713c66b1c
2 changed files with 62 additions and 94 deletions
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue