InfoLens / client /src /ts /storage /demoResourceLoader.ts
dqy08's picture
initial beta release
494c9e4
/**
* Demo 资源加载器 - 统一资源路由
*
* 支持两种资源协议:
* 1. `/path/to/demo.json` - 服务器资源
* 2. `local://filename.json` - 本地缓存资源(浏览器缓存)
*/
import type { AnalysisData } from '../api/GLTR_API';
import type { IDemoStorage, LoadResult } from './demoStorage';
import { ServerStorage } from './demoStorage';
import { LocalDemoCache } from './localDemoCache';
import type { TextAnalysisAPI } from '../api/GLTR_API';
import { ensureJsonExtension, validateDemoFormat } from '../utils/localFileUtils';
import { extractErrorMessage } from '../utils/errorUtils';
import { isValidHash } from '../utils/hashUtils';
import { trf } from '../lang/i18n-lite';
export type ResourceIdentifier = string; // "/path/file.json" 或 "local://file.json"
/**
* 解析资源标识符
* 本地资源格式: local://filename.json~hash
* 使用最后一个 ~ 作为分隔符,解决文件名中包含 ~ 的冲突
*/
function parseResourceIdentifier(identifier: ResourceIdentifier): {
type: 'server' | 'local';
path: string;
hash?: string;
filename?: string;
} {
if (identifier.startsWith('local://')) {
const rest = identifier.substring('local://'.length);
// 使用最后一个 ~ 作为分隔符,解决文件名中包含 ~ 的冲突
const hashIndex = rest.lastIndexOf('~');
if (hashIndex >= 0) {
const filename = rest.substring(0, hashIndex);
const hash = rest.substring(hashIndex + 1);
// 验证 hash 格式(4位十六进制)
if (!isValidHash(hash)) {
throw new Error(trf('Invalid hash format: "{hash}", expected 4 hexadecimal characters', { hash }));
}
return {
type: 'local',
path: hash, // 本地资源使用 hash 作为 path
hash,
filename
};
}
// 没有 hash,视为无效(不再兼容)
throw new Error(trf('Local resource identifier missing hash: "{identifier}", format should be local://filename.json~hash', { identifier }));
}
return {
type: 'server',
path: identifier
};
}
/**
* 统一资源加载器
* 根据资源标识符路由到相应的存储层
*/
export class DemoResourceLoader {
private serverStorage: ServerStorage;
private localDemoCache: LocalDemoCache;
constructor(api: TextAnalysisAPI) {
this.serverStorage = new ServerStorage(api);
this.localDemoCache = new LocalDemoCache();
}
/**
* 加载资源(统一入口,包含验证)
* @param identifier 资源标识符("/path/file.json" 或 "local://file.json~hash")
*/
async load(identifier: ResourceIdentifier): Promise<LoadResult> {
const { type, path, hash, filename } = parseResourceIdentifier(identifier);
const storage: IDemoStorage = type === 'local'
? this.localDemoCache
: this.serverStorage;
// 对于本地资源,构造完整的 key: filename~hash
// 如果解析失败(缺少 hash 或格式无效),parseResourceIdentifier 会抛出错误
const loadKey = type === 'local' && filename && hash
? `${filename}~${hash}`
: path;
// 调用底层存储加载数据
const result = await storage.load(loadKey);
// 统一验证数据格式(在此层进行,避免各存储层重复验证)
if (result.success && result.data) {
try {
validateDemoFormat(result.data);
} catch (error) {
const errorMessage = extractErrorMessage(error, '数据格式无效');
console.error(`资源验证失败 [${identifier}]:`, error);
return {
success: false,
message: `数据格式无效: ${errorMessage}`
};
}
}
return result;
}
/**
* 获取本地 Demo 缓存实例(用于保存和管理本地缓存)
*/
getLocalDemoCache(): LocalDemoCache {
return this.localDemoCache;
}
/**
* 获取服务器存储实例(用于保存到服务器)
*/
getServerStorage(): ServerStorage {
return this.serverStorage;
}
/**
* 检查标识符是否为本地资源
*/
static isLocalResource(identifier: ResourceIdentifier): boolean {
return identifier.startsWith('local://');
}
/**
* 创建本地资源标识符
* @param filename 文件名
* @param hash 4位哈希值
*/
static createLocalIdentifier(filename: string, hash: string): ResourceIdentifier {
const name = ensureJsonExtension(filename);
return `local://${name}~${hash}`;
}
/**
* 从本地资源标识符中提取文件名和哈希
* @throws 如果标识符格式无效或缺少哈希值
*/
static extractLocalInfo(identifier: ResourceIdentifier): { filename: string; hash: string } {
const parsed = parseResourceIdentifier(identifier);
if (parsed.type === 'local' && parsed.hash && parsed.filename) {
return {
filename: parsed.filename,
hash: parsed.hash
};
}
throw new Error(trf('Unable to extract local resource info: "{identifier}"', { identifier }));
}
}