fix(channels): narrow runtime channel registry caching

This commit is contained in:
Vincent Koc
2026-04-11 19:12:36 +01:00
parent 1ce87cda52
commit 97b60b992c
4 changed files with 124 additions and 92 deletions

View File

@@ -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<typeof listChannelPlugins>[number];
type ChannelPlugin = ReturnType<typeof listLoadedChannelPlugins>[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<typeof getActivePluginRegistry> | null = null;
let cachedRegistryVersion = -1;
let cachedNativeCommandSurfaces: Set<string> | null = null;
let cachedNativeRegistry: ReturnType<typeof getActivePluginRegistry> | 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<string> {
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;
}

View File

@@ -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<string, LoadedChannelPlugin>;
};
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<string>();
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<string, LoadedChannelPlugin>();
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);
}

View File

@@ -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<string>();
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<string, ChannelPlugin>;
};
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<string, ChannelPlugin>();
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 {

View File

@@ -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);
}