mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-15 19:21:08 +00:00
fix(channels): narrow runtime channel registry caching
This commit is contained in:
@@ -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;
|
||||
}
|
||||
|
||||
100
src/channels/plugins/registry-loaded.ts
Normal file
100
src/channels/plugins/registry-loaded.ts
Normal 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);
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user