111 lines
3.9 KiB
TypeScript
111 lines
3.9 KiB
TypeScript
import type { ContextProfile, FontSpec, MeasureResult, MeasureWidthImpl, TextWrapContext } from './types';
|
||
import { LruCache } from './cache';
|
||
import { assertFontSpecComplete, buildFontSpecKey } from './fontSpecKey';
|
||
|
||
// 模块级常驻缓存(两级)
|
||
const textWidthCache = new LruCache<Promise<number>>(2000);
|
||
const loggedMeasureFailures = new Set<string>();
|
||
|
||
function buildTextKey(args: { contextProfile: ContextProfile; fontSpecKey: string; text: string }): string {
|
||
// key 拼接规则必须确定性:<contextProfile>|<fontSpecKey>|<text>
|
||
return `${args.contextProfile}|${args.fontSpecKey}|${args.text}`;
|
||
}
|
||
|
||
function isValidWidth(v: unknown): v is number {
|
||
return typeof v === 'number' && Number.isFinite(v) && v >= 0;
|
||
}
|
||
|
||
/**
|
||
* 测量文本宽度(带缓存)。
|
||
*
|
||
* 约束:
|
||
* - 若启用测量(提供 measureWidthImpl),fontSpec 必须完整;缺字段直接报错(简体中文)
|
||
* - 若不启用测量(measureWidthImpl 缺失或 context=WIDGET 且明确不启用),进入 approx mode:width=null
|
||
*/
|
||
export async function measureWidthCached(args: {
|
||
text: string;
|
||
context: TextWrapContext;
|
||
contextProfile: ContextProfile;
|
||
fontSpec?: Partial<FontSpec> | null;
|
||
measureWidthImpl?: MeasureWidthImpl;
|
||
/** WIDGET 场景是否启用测量;默认 false(未启用即 WIDTH_UNKNOWN) */
|
||
widgetEnableMeasure?: boolean;
|
||
}): Promise<MeasureResult> {
|
||
const text = String(args.text ?? '');
|
||
|
||
const enabled =
|
||
typeof args.measureWidthImpl === 'function' && (args.context === 'APP' || args.widgetEnableMeasure === true);
|
||
|
||
if (!enabled) {
|
||
return { width: null, meta: { isApprox: true, reason: 'WIDTH_UNKNOWN' } };
|
||
}
|
||
|
||
// 启用测量时:fontSpec 缺字段必须报错
|
||
const fontSpec = args.fontSpec;
|
||
assertFontSpecComplete(fontSpec);
|
||
const fontSpecKey = buildFontSpecKey(fontSpec);
|
||
const key = buildTextKey({ contextProfile: args.contextProfile, fontSpecKey, text });
|
||
|
||
const cached = textWidthCache.get(key);
|
||
if (cached) {
|
||
try {
|
||
const w = await cached;
|
||
return isValidWidth(w) ? { width: w, meta: { isApprox: false } } : { width: null, meta: { isApprox: true, reason: 'MEASURE_FAILED' } };
|
||
} catch {
|
||
if (__DEV__ && !loggedMeasureFailures.has(key)) {
|
||
loggedMeasureFailures.add(key);
|
||
console.log('[TextWrap][Measure] measureWidthCached: 命中缓存但 Promise 失败(MEASURE_FAILED)', {
|
||
context: args.context,
|
||
contextProfile: args.contextProfile,
|
||
fontSpec: args.fontSpec,
|
||
textPreview: text.slice(0, 80),
|
||
textLength: text.length,
|
||
});
|
||
}
|
||
return { width: null, meta: { isApprox: true, reason: 'MEASURE_FAILED' } };
|
||
}
|
||
}
|
||
|
||
const promise = (async () => {
|
||
try {
|
||
const w = await args.measureWidthImpl!({
|
||
text,
|
||
context: args.context,
|
||
contextProfile: args.contextProfile,
|
||
fontSpec,
|
||
});
|
||
if (!isValidWidth(w)) {
|
||
throw new Error('测量结果非法(必须为有限且非负的 number)。');
|
||
}
|
||
return w;
|
||
} catch (error) {
|
||
if (__DEV__ && !loggedMeasureFailures.has(key)) {
|
||
loggedMeasureFailures.add(key);
|
||
console.log('[TextWrap][Measure] measureWidthImpl 抛错(MEASURE_FAILED)', {
|
||
context: args.context,
|
||
contextProfile: args.contextProfile,
|
||
fontSpec,
|
||
textPreview: text.slice(0, 80),
|
||
textLength: text.length,
|
||
errorName: (error as any)?.name,
|
||
errorMessage: String((error as any)?.message ?? error),
|
||
errorStack: (error as any)?.stack,
|
||
});
|
||
}
|
||
throw error;
|
||
}
|
||
})();
|
||
|
||
textWidthCache.set(key, promise);
|
||
|
||
try {
|
||
const w = await promise;
|
||
return { width: w, meta: { isApprox: false } };
|
||
} catch {
|
||
// 若失败,删除缓存,避免缓存住失败结果
|
||
textWidthCache.delete(key);
|
||
return { width: null, meta: { isApprox: true, reason: 'MEASURE_FAILED' } };
|
||
}
|
||
}
|
||
|