Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 169 additions & 0 deletions scripts/test-icon-asset-cache.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,169 @@
#!/usr/bin/env node

import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import vm from 'node:vm';
import { createRequire } from 'node:module';
import { performance } from 'node:perf_hooks';

const require = createRequire(import.meta.url);
const ts = require('typescript');

function createIconRuntimeHarness() {
const moduleCache = new Map();
const existingPaths = new Set();
let statCalls = 0;
let nowMs = 0;

const windowStub = {
electron: {
statSync(filePath) {
statCalls += 1;
const exists = existingPaths.has(filePath);
return {
exists,
isDirectory: false,
isFile: exists,
size: exists ? 123 : 0,
};
},
},
};
const performanceStub = {
now() {
return nowMs;
},
};

function loadTsModule(filePath) {
const resolvedPath = path.resolve(filePath);
if (moduleCache.has(resolvedPath)) return moduleCache.get(resolvedPath).exports;

const source = fs.readFileSync(resolvedPath, 'utf8');
const transpiled = ts.transpileModule(source, {
compilerOptions: {
module: ts.ModuleKind.CommonJS,
target: ts.ScriptTarget.ES2022,
jsx: ts.JsxEmit.React,
esModuleInterop: true,
importsNotUsedAsValues: ts.ImportsNotUsedAsValues.Remove,
},
fileName: resolvedPath,
});

const module = { exports: {} };
moduleCache.set(resolvedPath, module);

const localRequire = (request) => {
if (request.startsWith('.')) {
const candidate = path.resolve(path.dirname(resolvedPath), request);
for (const suffix of ['', '.ts', '.tsx', '.js', '.jsx', '/index.ts', '/index.tsx']) {
const nextPath = `${candidate}${suffix}`;
if (fs.existsSync(nextPath) && fs.statSync(nextPath).isFile()) {
if (nextPath.endsWith('.ts') || nextPath.endsWith('.tsx')) return loadTsModule(nextPath);
return require(nextPath);
}
}
}
return require(request);
};

const sandbox = {
module,
exports: module.exports,
require: localRequire,
console,
URL,
window: windowStub,
performance: performanceStub,
document: {
documentElement: {
classList: {
contains: () => false,
},
},
},
};

vm.runInNewContext(transpiled.outputText, sandbox, { filename: resolvedPath });
return module.exports;
}

return {
assets: loadTsModule('src/renderer/src/raycast-api/icon-runtime-assets.tsx'),
config: loadTsModule('src/renderer/src/raycast-api/icon-runtime-config.ts'),
addExistingPath(filePath) {
existingPaths.add(filePath);
},
advanceTime(ms) {
nowMs += ms;
},
get statCalls() {
return statCalls;
},
};
}

test('icon asset existence cache', async (t) => {
await t.test('resolves existing extension-relative assets by local path', () => {
const harness = createIconRuntimeHarness();
const assetsPath = '/tmp/supercmd-assets';
const iconPath = `${assetsPath}/icon.png`;
const expectedUrl = harness.assets.toScAssetUrl(iconPath);
harness.addExistingPath(iconPath);

assert.equal(harness.assets.resolveIconSrc('icon.png', assetsPath), expectedUrl);

assert.equal(harness.statCalls, 1);
});

await t.test('does not permanently cache misses so newly available assets resolve after the stale window', () => {
const harness = createIconRuntimeHarness();
const assetsPath = '/tmp/supercmd-assets';
const iconPath = `${assetsPath}/late.png`;
const expectedUrl = harness.assets.toScAssetUrl(iconPath);

assert.equal(harness.assets.resolveIconSrc('late.png', assetsPath), '');
harness.addExistingPath(iconPath);
assert.equal(harness.assets.resolveIconSrc('late.png', assetsPath), '');

harness.advanceTime(300);
assert.equal(harness.assets.resolveIconSrc('late.png', assetsPath), expectedUrl);

assert.equal(harness.statCalls, 2);
});

await t.test('uses configured assetsPath fallback without changing asset URL semantics', () => {
const harness = createIconRuntimeHarness();
const assetsPath = '/tmp/supercmd-extension-assets';
const iconPath = `${assetsPath}/nested/icon dark.png`;
const expectedUrl = harness.assets.toScAssetUrl(iconPath);
harness.addExistingPath(iconPath);
harness.config.configureIconRuntime({
getExtensionContext: () => ({ assetsPath }),
});

assert.equal(harness.assets.resolveIconSrc('nested/icon dark.png'), expectedUrl);
assert.equal(expectedUrl, 'sc-asset://ext-asset/tmp/supercmd-extension-assets/nested/icon%20dark.png');
assert.equal(harness.statCalls, 1);
});

await t.test('repeated missing measurement avoids repeated statSync calls within the stale window', () => {
const harness = createIconRuntimeHarness();
const assetsPath = '/tmp/supercmd-assets';
const iterations = 10_000;

const start = performance.now();
let result = 'not-run';
for (let i = 0; i < iterations; i += 1) {
result = harness.assets.resolveIconSrc('missing.png', assetsPath);
}
const elapsedMs = performance.now() - start;

assert.equal(result, '');
assert.equal(harness.statCalls, 1);
console.log(`icon-asset-miss-cache measurement: iterations=${iterations} statSync=${harness.statCalls} elapsedMs=${elapsedMs.toFixed(3)}`);
});
});
34 changes: 33 additions & 1 deletion src/renderer/src/raycast-api/icon-runtime-assets.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
import React from 'react';
import { getIconRuntimeContext } from './icon-runtime-config';

const LOCAL_PATH_MISS_CACHE_MAX = 4096;
const LOCAL_PATH_MISS_CACHE_TTL_MS = 250;
const negativeLocalPathExistsCache = new Map<string, number>();

type RgbColor = {
r: number;
g: number;
Expand Down Expand Up @@ -41,14 +45,42 @@ export function toScAssetUrl(filePath: string): string {

function localPathExists(filePath: string): boolean {
if (!filePath) return false;

const nowMs = getLocalPathCacheNowMs();
const missExpiresAt = negativeLocalPathExistsCache.get(filePath);
if (typeof missExpiresAt === 'number') {
if (missExpiresAt > nowMs) return false;
negativeLocalPathExistsCache.delete(filePath);
}

try {
const stat = (window as any).electron?.statSync?.(filePath);
return Boolean(stat?.exists);
const exists = Boolean(stat?.exists);
if (exists) {
negativeLocalPathExistsCache.delete(filePath);
} else {
cacheLocalPathMiss(filePath, nowMs);
}
return exists;
} catch {
cacheLocalPathMiss(filePath, nowMs);
return false;
}
}

function getLocalPathCacheNowMs(): number {
const performanceNow = (globalThis as any).performance?.now;
if (typeof performanceNow === 'function') return performanceNow.call((globalThis as any).performance);
return Date.now();
}

function cacheLocalPathMiss(filePath: string, nowMs: number): void {
if (negativeLocalPathExistsCache.size >= LOCAL_PATH_MISS_CACHE_MAX) {
negativeLocalPathExistsCache.clear();
}
negativeLocalPathExistsCache.set(filePath, nowMs + LOCAL_PATH_MISS_CACHE_TTL_MS);
}

function localPathFromScAssetUrl(src: string): string | null {
try {
const parsed = new URL(src);
Expand Down
Loading