perf(team): reduce render hot path lookups
This commit is contained in:
parent
97eb98466a
commit
5e3e77bf65
2 changed files with 47 additions and 12 deletions
|
|
@ -19,10 +19,11 @@ export function linkifyMentionsInMarkdown(
|
||||||
text: string,
|
text: string,
|
||||||
memberColorMap: Map<string, string>
|
memberColorMap: Map<string, string>
|
||||||
): string {
|
): string {
|
||||||
if (memberColorMap.size === 0) return text;
|
if (memberColorMap.size === 0 || !text.includes('@')) return text;
|
||||||
|
|
||||||
// Sort by name length descending for greedy matching
|
// Sort by name length descending for greedy matching
|
||||||
const names = [...memberColorMap.keys()].sort((a, b) => b.length - a.length);
|
const names = [...memberColorMap.keys()].sort((a, b) => b.length - a.length);
|
||||||
|
const canonicalNameByLower = new Map(names.map((name) => [name.toLowerCase(), name]));
|
||||||
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
const escaped = names.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
const pattern = new RegExp(
|
const pattern = new RegExp(
|
||||||
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp
|
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp
|
||||||
|
|
@ -32,7 +33,7 @@ export function linkifyMentionsInMarkdown(
|
||||||
|
|
||||||
return text.replace(pattern, (_match: string, prefix: string, name: string) => {
|
return text.replace(pattern, (_match: string, prefix: string, name: string) => {
|
||||||
// Find the canonical name (case-insensitive lookup)
|
// Find the canonical name (case-insensitive lookup)
|
||||||
const canonical = names.find((n) => n.toLowerCase() === name.toLowerCase()) ?? name;
|
const canonical = canonicalNameByLower.get(name.toLowerCase()) ?? name;
|
||||||
const color = memberColorMap.get(canonical) ?? '';
|
const color = memberColorMap.get(canonical) ?? '';
|
||||||
return `${prefix}[@${canonical}](mention://${encodeURIComponent(color)}/${encodeURIComponent(canonical)})`;
|
return `${prefix}[@${canonical}](mention://${encodeURIComponent(color)}/${encodeURIComponent(canonical)})`;
|
||||||
});
|
});
|
||||||
|
|
@ -51,10 +52,11 @@ export function linkifyTeamMentionsInMarkdown(
|
||||||
teamNames: ReadonlySet<string> | readonly string[]
|
teamNames: ReadonlySet<string> | readonly string[]
|
||||||
): string {
|
): string {
|
||||||
const names: readonly string[] = Array.isArray(teamNames) ? teamNames : [...teamNames];
|
const names: readonly string[] = Array.isArray(teamNames) ? teamNames : [...teamNames];
|
||||||
if (names.length === 0) return text;
|
if (names.length === 0 || !text.includes('@')) return text;
|
||||||
|
|
||||||
// Sort by name length descending for greedy matching
|
// Sort by name length descending for greedy matching
|
||||||
const sorted = [...names].sort((a, b) => b.length - a.length);
|
const sorted = [...names].sort((a, b) => b.length - a.length);
|
||||||
|
const canonicalNameByLower = new Map(sorted.map((name) => [name.toLowerCase(), name]));
|
||||||
const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
const escaped = sorted.map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
const pattern = new RegExp(
|
const pattern = new RegExp(
|
||||||
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp
|
// eslint-disable-next-line no-useless-escape -- backslash-quote and backslash-hyphen needed in template literal for RegExp
|
||||||
|
|
@ -63,7 +65,7 @@ export function linkifyTeamMentionsInMarkdown(
|
||||||
);
|
);
|
||||||
|
|
||||||
return text.replace(pattern, (_match: string, prefix: string, name: string) => {
|
return text.replace(pattern, (_match: string, prefix: string, name: string) => {
|
||||||
const canonical = sorted.find((n) => n.toLowerCase() === name.toLowerCase()) ?? name;
|
const canonical = canonicalNameByLower.get(name.toLowerCase()) ?? name;
|
||||||
return `${prefix}[${canonical}](team://${encodeURIComponent(canonical)})`;
|
return `${prefix}[${canonical}](team://${encodeURIComponent(canonical)})`;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,8 @@ type RuntimeAwareProviderStatus = Pick<
|
||||||
CliProviderStatus,
|
CliProviderStatus,
|
||||||
'providerId' | 'authMethod' | 'backend' | 'modelCatalog'
|
'providerId' | 'authMethod' | 'backend' | 'modelCatalog'
|
||||||
>;
|
>;
|
||||||
|
type RuntimeAwareModelCatalog = NonNullable<RuntimeAwareProviderStatus['modelCatalog']>;
|
||||||
|
type RuntimeAwareCatalogModel = RuntimeAwareModelCatalog['models'][number];
|
||||||
|
|
||||||
export interface TeamProviderModelOption {
|
export interface TeamProviderModelOption {
|
||||||
value: string;
|
value: string;
|
||||||
|
|
@ -216,6 +218,34 @@ const SUPPORTED_ANTHROPIC_TEAM_MODELS = new Set<string>([
|
||||||
'claude-haiku-4-5-20251001',
|
'claude-haiku-4-5-20251001',
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
const runtimeCatalogModelIndexCache = new WeakMap<
|
||||||
|
RuntimeAwareModelCatalog,
|
||||||
|
Map<string, RuntimeAwareCatalogModel>
|
||||||
|
>();
|
||||||
|
|
||||||
|
function getRuntimeCatalogModelIndex(
|
||||||
|
modelCatalog: RuntimeAwareModelCatalog
|
||||||
|
): Map<string, RuntimeAwareCatalogModel> {
|
||||||
|
const cached = runtimeCatalogModelIndexCache.get(modelCatalog);
|
||||||
|
if (cached) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const index = new Map<string, RuntimeAwareCatalogModel>();
|
||||||
|
for (const model of modelCatalog.models) {
|
||||||
|
const launchModel = model.launchModel.trim();
|
||||||
|
if (launchModel) {
|
||||||
|
index.set(launchModel, model);
|
||||||
|
}
|
||||||
|
const id = model.id.trim();
|
||||||
|
if (id) {
|
||||||
|
index.set(id, model);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
runtimeCatalogModelIndexCache.set(modelCatalog, index);
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
export function isSupportedAnthropicTeamModel(model: string | undefined): boolean {
|
export function isSupportedAnthropicTeamModel(model: string | undefined): boolean {
|
||||||
const trimmed = model?.trim();
|
const trimmed = model?.trim();
|
||||||
if (!trimmed) {
|
if (!trimmed) {
|
||||||
|
|
@ -294,17 +324,13 @@ function getRuntimeCatalogModel(
|
||||||
providerId: SupportedProviderId | undefined,
|
providerId: SupportedProviderId | undefined,
|
||||||
model: string | undefined,
|
model: string | undefined,
|
||||||
providerStatus?: RuntimeAwareProviderStatus | null
|
providerStatus?: RuntimeAwareProviderStatus | null
|
||||||
): NonNullable<RuntimeAwareProviderStatus['modelCatalog']>['models'][number] | null {
|
): RuntimeAwareCatalogModel | null {
|
||||||
const trimmed = model?.trim();
|
const trimmed = model?.trim();
|
||||||
if (!providerId || !trimmed || providerStatus?.modelCatalog?.providerId !== providerId) {
|
if (!providerId || !trimmed || providerStatus?.modelCatalog?.providerId !== providerId) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return getRuntimeCatalogModelIndex(providerStatus.modelCatalog).get(trimmed) ?? null;
|
||||||
providerStatus.modelCatalog.models.find(
|
|
||||||
(item) => item.launchModel === trimmed || item.id === trimmed
|
|
||||||
) ?? null
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getTeamModelBadgeLabel(
|
export function getTeamModelBadgeLabel(
|
||||||
|
|
@ -456,11 +482,18 @@ export function sortTeamProviderModels(
|
||||||
return sorted;
|
return sorted;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const freeByModel = new Map(
|
||||||
|
sorted.map((model) => [
|
||||||
|
model,
|
||||||
|
isFreeOpenCodeModelForOrdering(providerId, model, providerStatus),
|
||||||
|
])
|
||||||
|
);
|
||||||
|
|
||||||
return sorted
|
return sorted
|
||||||
.map((model, index) => ({ model, index }))
|
.map((model, index) => ({ model, index }))
|
||||||
.sort((left, right) => {
|
.sort((left, right) => {
|
||||||
const leftFree = isFreeOpenCodeModelForOrdering(providerId, left.model, providerStatus);
|
const leftFree = freeByModel.get(left.model) ?? false;
|
||||||
const rightFree = isFreeOpenCodeModelForOrdering(providerId, right.model, providerStatus);
|
const rightFree = freeByModel.get(right.model) ?? false;
|
||||||
if (leftFree !== rightFree) {
|
if (leftFree !== rightFree) {
|
||||||
return leftFree ? -1 : 1;
|
return leftFree ? -1 : 1;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue