From 97b60b992ca8cfed8c4fb683c3547fbd287e029f Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sat, 11 Apr 2026 19:12:36 +0100 Subject: [PATCH] fix(channels): narrow runtime channel registry caching --- src/auto-reply/commands-registry.data.ts | 26 +++--- src/channels/plugins/registry-loaded.ts | 100 +++++++++++++++++++++++ src/channels/plugins/registry.ts | 83 +------------------ src/plugins/runtime-channel-state.ts | 7 ++ 4 files changed, 124 insertions(+), 92 deletions(-) create mode 100644 src/channels/plugins/registry-loaded.ts diff --git a/src/auto-reply/commands-registry.data.ts b/src/auto-reply/commands-registry.data.ts index fe1db58c623..8b3b892c9b2 100644 --- a/src/auto-reply/commands-registry.data.ts +++ b/src/auto-reply/commands-registry.data.ts @@ -1,5 +1,5 @@ -import { listChannelPlugins } from "../channels/plugins/index.js"; -import { getActivePluginRegistry } from "../plugins/runtime.js"; +import { listLoadedChannelPlugins } from "../channels/plugins/registry-loaded.js"; +import { getActivePluginChannelRegistryVersionFromState } from "../plugins/runtime-channel-state.js"; import { assertCommandRegistry, buildBuiltinChatCommands, @@ -7,7 +7,7 @@ import { } from "./commands-registry.shared.js"; import type { ChatCommandDefinition } from "./commands-registry.types.js"; -type ChannelPlugin = ReturnType[number]; +type ChannelPlugin = ReturnType[number]; function supportsNativeCommands(plugin: ChannelPlugin): boolean { return plugin.capabilities?.nativeCommands === true; @@ -24,14 +24,14 @@ function defineDockCommand(plugin: ChannelPlugin): ChatCommandDefinition { } let cachedCommands: ChatCommandDefinition[] | null = null; -let cachedRegistry: ReturnType | null = null; +let cachedRegistryVersion = -1; let cachedNativeCommandSurfaces: Set | null = null; -let cachedNativeRegistry: ReturnType | null = null; +let cachedNativeRegistryVersion = -1; function buildChatCommands(): ChatCommandDefinition[] { const commands: ChatCommandDefinition[] = [ ...buildBuiltinChatCommands(), - ...listChannelPlugins() + ...listLoadedChannelPlugins() .filter(supportsNativeCommands) .map((plugin) => defineDockCommand(plugin)), ]; @@ -41,27 +41,27 @@ function buildChatCommands(): ChatCommandDefinition[] { } export function getChatCommands(): ChatCommandDefinition[] { - const registry = getActivePluginRegistry(); - if (cachedCommands && registry === cachedRegistry) { + const registryVersion = getActivePluginChannelRegistryVersionFromState(); + if (cachedCommands && registryVersion === cachedRegistryVersion) { return cachedCommands; } const commands = buildChatCommands(); cachedCommands = commands; - cachedRegistry = registry; + cachedRegistryVersion = registryVersion; cachedNativeCommandSurfaces = null; return commands; } export function getNativeCommandSurfaces(): Set { - const registry = getActivePluginRegistry(); - if (cachedNativeCommandSurfaces && registry === cachedNativeRegistry) { + const registryVersion = getActivePluginChannelRegistryVersionFromState(); + if (cachedNativeCommandSurfaces && registryVersion === cachedNativeRegistryVersion) { return cachedNativeCommandSurfaces; } cachedNativeCommandSurfaces = new Set( - listChannelPlugins() + listLoadedChannelPlugins() .filter(supportsNativeCommands) .map((plugin) => plugin.id), ); - cachedNativeRegistry = registry; + cachedNativeRegistryVersion = registryVersion; return cachedNativeCommandSurfaces; } diff --git a/src/channels/plugins/registry-loaded.ts b/src/channels/plugins/registry-loaded.ts new file mode 100644 index 00000000000..7e5f771829e --- /dev/null +++ b/src/channels/plugins/registry-loaded.ts @@ -0,0 +1,100 @@ +import { + getActivePluginChannelRegistryFromState, + getActivePluginChannelRegistryVersionFromState, +} from "../../plugins/runtime-channel-state.js"; +import { normalizeOptionalString } from "../../shared/string-coerce.js"; +import { CHAT_CHANNEL_ORDER, type ChatChannelId } from "../registry.js"; + +export type LoadedChannelPlugin = { + id: string; + meta: { + order?: number; + }; + capabilities?: { + nativeCommands?: boolean; + }; +}; + +type CachedChannelPlugins = { + registryVersion: number; + registryRef: object | null; + sorted: LoadedChannelPlugin[]; + byId: Map; +}; + +const EMPTY_CHANNEL_PLUGIN_CACHE: CachedChannelPlugins = { + registryVersion: -1, + registryRef: null, + sorted: [], + byId: new Map(), +}; + +let cachedChannelPlugins = EMPTY_CHANNEL_PLUGIN_CACHE; + +function dedupeChannels(channels: LoadedChannelPlugin[]): LoadedChannelPlugin[] { + const seen = new Set(); + const resolved: LoadedChannelPlugin[] = []; + for (const plugin of channels) { + const id = normalizeOptionalString(plugin.id) ?? ""; + if (!id || seen.has(id)) { + continue; + } + seen.add(id); + resolved.push(plugin); + } + return resolved; +} + +function resolveCachedChannelPlugins(): CachedChannelPlugins { + const registry = getActivePluginChannelRegistryFromState(); + const registryVersion = getActivePluginChannelRegistryVersionFromState(); + const cached = cachedChannelPlugins; + if (cached.registryVersion === registryVersion && cached.registryRef === registry) { + return cached; + } + + const channelPlugins: LoadedChannelPlugin[] = []; + if (registry && Array.isArray(registry.channels)) { + for (const entry of registry.channels) { + if (entry?.plugin) { + channelPlugins.push(entry.plugin); + } + } + } + + const sorted = dedupeChannels(channelPlugins).toSorted((a, b) => { + const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id); + const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id); + const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA); + const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB); + if (orderA !== orderB) { + return orderA - orderB; + } + return a.id.localeCompare(b.id); + }); + const byId = new Map(); + for (const plugin of sorted) { + byId.set(plugin.id, plugin); + } + + const next: CachedChannelPlugins = { + registryVersion, + registryRef: registry, + sorted, + byId, + }; + cachedChannelPlugins = next; + return next; +} + +export function listLoadedChannelPlugins(): LoadedChannelPlugin[] { + return resolveCachedChannelPlugins().sorted.slice(); +} + +export function getLoadedChannelPluginById(id: string): LoadedChannelPlugin | undefined { + const resolvedId = normalizeOptionalString(id) ?? ""; + if (!resolvedId) { + return undefined; + } + return resolveCachedChannelPlugins().byId.get(resolvedId); +} diff --git a/src/channels/plugins/registry.ts b/src/channels/plugins/registry.ts index b4018b14067..772252e428a 100644 --- a/src/channels/plugins/registry.ts +++ b/src/channels/plugins/registry.ts @@ -1,87 +1,12 @@ -import { - getActivePluginChannelRegistryVersion, - requireActivePluginChannelRegistry, -} from "../../plugins/runtime.js"; import { normalizeOptionalString } from "../../shared/string-coerce.js"; -import { CHAT_CHANNEL_ORDER, type ChatChannelId, normalizeAnyChannelId } from "../registry.js"; +import { normalizeAnyChannelId } from "../registry.js"; import { getBundledChannelPlugin } from "./bundled.js"; +import { getLoadedChannelPluginById, listLoadedChannelPlugins } from "./registry-loaded.js"; import type { ChannelPlugin } from "./types.plugin.js"; import type { ChannelId } from "./types.public.js"; -function dedupeChannels(channels: ChannelPlugin[]): ChannelPlugin[] { - const seen = new Set(); - const resolved: ChannelPlugin[] = []; - for (const plugin of channels) { - const id = normalizeOptionalString(plugin.id) ?? ""; - if (!id || seen.has(id)) { - continue; - } - seen.add(id); - resolved.push(plugin); - } - return resolved; -} - -type CachedChannelPlugins = { - registryVersion: number; - registryRef: object | null; - sorted: ChannelPlugin[]; - byId: Map; -}; - -const EMPTY_CHANNEL_PLUGIN_CACHE: CachedChannelPlugins = { - registryVersion: -1, - registryRef: null, - sorted: [], - byId: new Map(), -}; - -let cachedChannelPlugins = EMPTY_CHANNEL_PLUGIN_CACHE; - -function resolveCachedChannelPlugins(): CachedChannelPlugins { - const registry = requireActivePluginChannelRegistry(); - const registryVersion = getActivePluginChannelRegistryVersion(); - const cached = cachedChannelPlugins; - if (cached.registryVersion === registryVersion && cached.registryRef === registry) { - return cached; - } - - const channelPlugins: ChannelPlugin[] = []; - if (Array.isArray(registry.channels)) { - for (const entry of registry.channels) { - if (entry?.plugin) { - channelPlugins.push(entry.plugin); - } - } - } - - const sorted = dedupeChannels(channelPlugins).toSorted((a, b) => { - const indexA = CHAT_CHANNEL_ORDER.indexOf(a.id as ChatChannelId); - const indexB = CHAT_CHANNEL_ORDER.indexOf(b.id as ChatChannelId); - const orderA = a.meta.order ?? (indexA === -1 ? 999 : indexA); - const orderB = b.meta.order ?? (indexB === -1 ? 999 : indexB); - if (orderA !== orderB) { - return orderA - orderB; - } - return a.id.localeCompare(b.id); - }); - const byId = new Map(); - for (const plugin of sorted) { - byId.set(plugin.id, plugin); - } - - const next: CachedChannelPlugins = { - registryVersion, - registryRef: registry, - sorted, - byId, - }; - cachedChannelPlugins = next; - return next; -} - export function listChannelPlugins(): ChannelPlugin[] { - return resolveCachedChannelPlugins().sorted.slice(); + return listLoadedChannelPlugins() as ChannelPlugin[]; } export function getLoadedChannelPlugin(id: ChannelId): ChannelPlugin | undefined { @@ -89,7 +14,7 @@ export function getLoadedChannelPlugin(id: ChannelId): ChannelPlugin | undefined if (!resolvedId) { return undefined; } - return resolveCachedChannelPlugins().byId.get(resolvedId); + return getLoadedChannelPluginById(resolvedId) as ChannelPlugin | undefined; } export function getChannelPlugin(id: ChannelId): ChannelPlugin | undefined { diff --git a/src/plugins/runtime-channel-state.ts b/src/plugins/runtime-channel-state.ts index e9727517d68..75b7308bbac 100644 --- a/src/plugins/runtime-channel-state.ts +++ b/src/plugins/runtime-channel-state.ts @@ -17,9 +17,11 @@ type RuntimeTrackedChannelRegistry = { type GlobalChannelRegistryState = typeof globalThis & { [PLUGIN_REGISTRY_STATE]?: { + activeVersion?: number; activeRegistry?: RuntimeTrackedChannelRegistry | null; channel?: { registry: RuntimeTrackedChannelRegistry | null; + version?: number; }; }; }; @@ -28,3 +30,8 @@ export function getActivePluginChannelRegistryFromState(): RuntimeTrackedChannel const state = (globalThis as GlobalChannelRegistryState)[PLUGIN_REGISTRY_STATE]; return state?.channel?.registry ?? state?.activeRegistry ?? null; } + +export function getActivePluginChannelRegistryVersionFromState(): number { + const state = (globalThis as GlobalChannelRegistryState)[PLUGIN_REGISTRY_STATE]; + return state?.channel?.registry ? (state.channel.version ?? 0) : (state?.activeVersion ?? 0); +}