perf(plugins): cache normalized jiti aliases

This commit is contained in:
Peter Steinberger
2026-04-22 20:59:45 +01:00
parent 834e50f83c
commit c0cafb6bbe
4 changed files with 129 additions and 2 deletions

View File

@@ -21,6 +21,7 @@ Docs: https://docs.openclaw.ai
- Discord: harden inbound thread metadata handling against partial Carbon channel getters, so non-command thread messages and queued jobs no longer crash when `name`, `parentId`, `parent`, or `ownerId` requires fetched raw data.
- Discord: let `message` tool reactions resolve `user:<id>` DM targets and preserve `channels.discord.guilds.<guild>.channels.<channel>.requireMention: false` during reply-stage activation fallback. Fixes #70165 and #69441.
- Plugins/startup: pre-normalize and cache Jiti alias maps before creating plugin loaders, so module-scoped loader filenames do not reintroduce per-plugin alias-normalization startup cost. Fixes #70186.
- Telegram/webhooks: lower the grammY webhook callback timeout to 5s so Telegram gets an early 200 response instead of retrying long-running updates as read timeouts. (#70146) Thanks @friday-james.
- Telegram/polling: rebuild the polling HTTP transport after `getUpdates` 409 conflicts, so retries use a fresh TCP connection instead of looping on a Telegram-terminated keep-alive socket. (#69873) Thanks @hclsys.
- Media delivery: strip persisted base64 audio payloads from webchat history, resolve stored `media://inbound/*` attachments before local-root checks, suppress duplicate Telegram voice/audio sends when TTS emits the same media twice, and support custom image-model IDs that already include their provider prefix.

View File

@@ -163,4 +163,43 @@ describe("getCachedPluginJitiLoader", () => {
expect(createJiti).toHaveBeenCalledTimes(1);
expect(cache.size).toBe(1);
});
it("reuses pre-normalized alias options across module-scoped loader filenames", async () => {
const { createJiti, getCachedPluginJitiLoader } =
await loadCachedPluginJitiLoader("module-filename-aliases");
const cache = new Map();
getCachedPluginJitiLoader({
cache,
modulePath: "/repo/extensions/demo-a/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
jitiFilename: "/repo/extensions/demo-a/index.ts",
aliasMap: {
alpha: "/repo/alpha",
beta: "alpha/sub",
},
tryNative: false,
});
getCachedPluginJitiLoader({
cache,
modulePath: "/repo/extensions/demo-b/index.ts",
importerUrl: "file:///repo/src/plugins/loader.ts",
jitiFilename: "/repo/extensions/demo-b/index.ts",
aliasMap: {
beta: "alpha/sub",
alpha: "/repo/alpha",
},
tryNative: false,
});
const marker = Symbol.for("pathe:normalizedAlias");
const firstAlias = (createJiti.mock.calls[0]?.[1] as { alias?: Record<string, string> }).alias;
const secondAlias = (createJiti.mock.calls[1]?.[1] as { alias?: Record<string, string> }).alias;
expect(createJiti).toHaveBeenCalledTimes(2);
expect(cache.size).toBe(2);
expect(secondAlias).toBe(firstAlias);
expect(firstAlias?.beta).toBe("/repo/alpha/sub");
expect((firstAlias as Record<symbol, unknown>)[marker]).toBe(true);
});
});

View File

@@ -1267,3 +1267,37 @@ describe("buildPluginLoaderAliasMap memoization", () => {
expect(Object.keys(second).toSorted()).toEqual(Object.keys(first).toSorted());
});
});
describe("buildPluginLoaderJitiOptions", () => {
it("pre-normalizes and marks alias maps for Jiti", () => {
const marker = Symbol.for("pathe:normalizedAlias");
const aliasMap = {
"openclaw/plugin-sdk/core": "/repo/src/plugin-sdk/core.ts",
"openclaw/plugin-sdk": "/repo/src/plugin-sdk/root-alias.cjs",
"@openclaw/plugin-sdk": "/repo/src/plugin-sdk/root-alias.cjs",
};
const first = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
const second = buildPluginLoaderJitiOptions({ ...aliasMap }).alias as Record<string, string>;
expect(second).toBe(first);
expect((first as Record<symbol, unknown>)[marker]).toBe(true);
expect(Object.prototype.propertyIsEnumerable.call(first, marker)).toBe(false);
});
it("applies Jiti alias-target normalization before caching", () => {
const aliasMap = {
alpha: "/repo/alpha",
beta: "alpha/sub",
};
const alias = buildPluginLoaderJitiOptions(aliasMap).alias as Record<string, string>;
expect(alias).not.toBe(aliasMap);
expect(alias.beta).toBe("/repo/alpha/sub");
});
it("does not attach an empty alias map", () => {
expect(buildPluginLoaderJitiOptions({})).not.toHaveProperty("alias");
});
});

View File

@@ -481,12 +481,15 @@ export function resolveExtensionApiAlias(params: LoaderModuleResolveParams = {})
}
const MAX_PLUGIN_LOADER_ALIAS_CACHE_ENTRIES = 512;
const JITI_NORMALIZED_ALIAS_SYMBOL = Symbol.for("pathe:normalizedAlias");
const JITI_ALIAS_ROOT_SENTINELS = new Set<string | undefined>(["/", "\\", undefined]);
// Memoize loader alias/config by effective resolution context so repeated
// loader setup avoids rebuilding the same filesystem-derived map and cache key.
// Include cwd/env inputs because the fallback root and private QA alias
// surfaces depend on them.
const aliasMapCache = new Map<string, Record<string, string>>();
const normalizedJitiAliasMapCache = new Map<string, Record<string, string>>();
const pluginLoaderJitiConfigCache = new Map<
string,
{
@@ -510,6 +513,54 @@ function setBoundedCacheValue<T>(cache: Map<string, T>, key: string, value: T) {
}
}
function hasJitiNormalizedAliasMarker(aliasMap: Record<string, string>) {
return Boolean((aliasMap as Record<symbol, unknown>)[JITI_NORMALIZED_ALIAS_SYMBOL]);
}
function createJitiAliasContentCacheKey(aliasMap: Record<string, string>) {
return JSON.stringify(
Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)),
);
}
function normalizePluginLoaderAliasMapForJiti(
aliasMap: Record<string, string>,
): Record<string, string> {
if (hasJitiNormalizedAliasMarker(aliasMap)) {
return aliasMap;
}
const cacheKey = createJitiAliasContentCacheKey(aliasMap);
const cached = normalizedJitiAliasMapCache.get(cacheKey);
if (cached) {
return cached;
}
const normalizedAliasMap = Object.fromEntries(
Object.entries(aliasMap).toSorted(
([left], [right]) => right.split("/").length - left.split("/").length,
),
);
for (const aliasKey in normalizedAliasMap) {
for (const candidateKey in normalizedAliasMap) {
if (
candidateKey === aliasKey ||
aliasKey.startsWith(candidateKey) ||
!normalizedAliasMap[aliasKey]?.startsWith(candidateKey) ||
!JITI_ALIAS_ROOT_SENTINELS.has(normalizedAliasMap[aliasKey]?.[candidateKey.length])
) {
continue;
}
normalizedAliasMap[aliasKey] =
normalizedAliasMap[candidateKey] + normalizedAliasMap[aliasKey].slice(candidateKey.length);
}
}
Object.defineProperty(normalizedAliasMap, JITI_NORMALIZED_ALIAS_SYMBOL, {
value: true,
enumerable: false,
});
setBoundedCacheValue(normalizedJitiAliasMapCache, cacheKey, normalizedAliasMap);
return normalizedAliasMap;
}
function buildPluginLoaderAliasMapCacheKey(params: {
modulePath: string;
argv1?: string;
@@ -626,15 +677,17 @@ export function resolvePluginRuntimeModulePath(
}
export function buildPluginLoaderJitiOptions(aliasMap: Record<string, string>) {
const hasAliases = Object.keys(aliasMap).length > 0;
const jitiAliasMap = hasAliases ? normalizePluginLoaderAliasMapForJiti(aliasMap) : aliasMap;
return {
interopDefault: true,
// Prefer Node's native sync ESM loader for built dist/*.js modules so
// bundled plugins and plugin-sdk subpaths stay on the canonical module graph.
tryNative: true,
extensions: [".ts", ".tsx", ".mts", ".cts", ".mtsx", ".ctsx", ".js", ".mjs", ".cjs", ".json"],
...(Object.keys(aliasMap).length > 0
...(hasAliases
? {
alias: aliasMap,
alias: jitiAliasMap,
}
: {}),
};