From c0cafb6bbe2a122fa6f5bf2ba0a0dd884453f1dd Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Wed, 22 Apr 2026 20:59:45 +0100 Subject: [PATCH] perf(plugins): cache normalized jiti aliases --- CHANGELOG.md | 1 + src/plugins/jiti-loader-cache.test.ts | 39 ++++++++++++++++++ src/plugins/sdk-alias.test.ts | 34 ++++++++++++++++ src/plugins/sdk-alias.ts | 57 ++++++++++++++++++++++++++- 4 files changed, 129 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d290b60b10f..f7ba9612f61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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:` DM targets and preserve `channels.discord.guilds..channels..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. diff --git a/src/plugins/jiti-loader-cache.test.ts b/src/plugins/jiti-loader-cache.test.ts index 35fd8fd2e6b..9231571a622 100644 --- a/src/plugins/jiti-loader-cache.test.ts +++ b/src/plugins/jiti-loader-cache.test.ts @@ -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 }).alias; + const secondAlias = (createJiti.mock.calls[1]?.[1] as { alias?: Record }).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)[marker]).toBe(true); + }); }); diff --git a/src/plugins/sdk-alias.test.ts b/src/plugins/sdk-alias.test.ts index aa316b21a91..474969f9825 100644 --- a/src/plugins/sdk-alias.test.ts +++ b/src/plugins/sdk-alias.test.ts @@ -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; + const second = buildPluginLoaderJitiOptions({ ...aliasMap }).alias as Record; + + expect(second).toBe(first); + expect((first as Record)[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; + + 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"); + }); +}); diff --git a/src/plugins/sdk-alias.ts b/src/plugins/sdk-alias.ts index 92d2a1733b9..212537d55f1 100644 --- a/src/plugins/sdk-alias.ts +++ b/src/plugins/sdk-alias.ts @@ -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(["/", "\\", 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>(); +const normalizedJitiAliasMapCache = new Map>(); const pluginLoaderJitiConfigCache = new Map< string, { @@ -510,6 +513,54 @@ function setBoundedCacheValue(cache: Map, key: string, value: T) { } } +function hasJitiNormalizedAliasMarker(aliasMap: Record) { + return Boolean((aliasMap as Record)[JITI_NORMALIZED_ALIAS_SYMBOL]); +} + +function createJitiAliasContentCacheKey(aliasMap: Record) { + return JSON.stringify( + Object.entries(aliasMap).toSorted(([left], [right]) => left.localeCompare(right)), + ); +} + +function normalizePluginLoaderAliasMapForJiti( + aliasMap: Record, +): Record { + 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) { + 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, } : {}), };