Files
openscreen/src/lib/assetPath.ts
T
Enriquefft 702b733074 resolve asset base path synchronously from preload
Every consumer of /wallpapers/*.jpg — SettingsPanel, VideoPlayback,
frameRenderer — was doing async IPC round trips, useEffect dances, and
Promise.all for a value that is a build-time constant per process. Each
consumer showed briefly-empty or briefly-404ing state on first paint
until the handler's reply resolved.

The asset base URL depends only on process.defaultApp and
process.resourcesPath / __dirname — all available in preload at
context-bridge time. Compute once there, expose as a sync string.

- preload.ts resolves baseDir (process.resourcesPath packaged,
  <appRoot>/public unpackaged) and emits assetBaseUrl synchronously.
- get-asset-base-path IPC handler + main-process branching deleted.
- getAssetPath() is now sync. Returns string, not Promise<string>.
  Throws AssetBaseUnavailableError (new) when electronAPI.assetBaseUrl
  is missing — catastrophic preload failure, not silent 404.
- resolveImageWallpaperUrl() sync; same sync throw semantics.
- SettingsPanel: Promise.all + useState + useEffect collapse to one
  useMemo. First paint has real URLs, no 18× ERR_FILE_NOT_FOUND, no
  flicker.
- VideoPlayback: wallpaper-resolve useEffect collapses to useMemo.
- frameRenderer.setupBackground: drops the await.
- electronAPI type decls updated in both .d.ts files.
- 35 unit tests updated to reflect sync signature + new
  AssetBaseUnavailableError contract.

Silent-fallback behavior from getAssetPath (returning /relative when
electronAPI failed) is gone. Renderers now surface preload failures
instead of rendering 404s.
2026-04-24 18:33:03 -05:00

53 lines
1.3 KiB
TypeScript

export class UnsafeAssetPathError extends Error {
constructor(segment: string) {
super(`Unsafe asset path segment: ${segment}`);
this.name = "UnsafeAssetPathError";
}
}
export class AssetBaseUnavailableError extends Error {
constructor() {
super("electronAPI.assetBaseUrl is not available; preload did not load correctly");
this.name = "AssetBaseUnavailableError";
}
}
function encodeRelativeAssetPath(relativePath: string): string {
return relativePath
.replace(/^\/+/, "")
.split("/")
.filter(Boolean)
.map((part) => {
const decoded = decodeURIComponent(part);
if (decoded === "." || decoded === "..") {
throw new UnsafeAssetPathError(decoded);
}
return encodeURIComponent(decoded);
})
.join("/");
}
function ensureTrailingSlash(value: string): string {
return value.endsWith("/") ? value : `${value}/`;
}
export function getAssetPath(relativePath: string): string {
const encoded = encodeRelativeAssetPath(relativePath);
if (typeof window === "undefined") {
return `/${encoded}`;
}
if (window.location?.protocol?.startsWith("http")) {
return `/${encoded}`;
}
const base = window.electronAPI?.assetBaseUrl;
if (!base) {
throw new AssetBaseUnavailableError();
}
return new URL(encoded, ensureTrailingSlash(base)).toString();
}
export default getAssetPath;