id-scanner-lib 1.3.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +55 -460
- package/dist/id-scanner-lib.esm.js +4641 -0
- package/dist/id-scanner-lib.esm.js.map +1 -0
- package/dist/id-scanner-lib.js +14755 -0
- package/dist/id-scanner-lib.js.map +1 -0
- package/dist/types/core/base-module.d.ts +44 -0
- package/dist/types/core/camera-manager.d.ts +258 -0
- package/dist/types/core/config.d.ts +88 -0
- package/dist/types/core/errors.d.ts +111 -0
- package/dist/types/core/event-emitter.d.ts +55 -0
- package/dist/types/core/logger.d.ts +277 -0
- package/dist/types/core/module-manager.d.ts +78 -0
- package/dist/types/core/plugin-manager.d.ts +158 -0
- package/dist/types/core/resource-manager.d.ts +246 -0
- package/dist/types/core/result.d.ts +83 -0
- package/dist/types/core/scanner-factory.d.ts +93 -0
- package/dist/types/index.bundle.d.ts +1303 -0
- package/dist/types/index.d.ts +86 -0
- package/dist/types/interfaces/external-types.d.ts +174 -0
- package/dist/types/interfaces/face-detection.d.ts +293 -0
- package/dist/types/interfaces/scanner-module.d.ts +280 -0
- package/dist/types/modules/face/face-detector.d.ts +170 -0
- package/dist/types/modules/face/index.d.ts +56 -0
- package/dist/types/modules/face/liveness-detector.d.ts +177 -0
- package/dist/types/modules/face/types.d.ts +136 -0
- package/dist/types/modules/id-card/anti-fake-detector.d.ts +170 -0
- package/dist/types/modules/id-card/id-card-detector.d.ts +131 -0
- package/dist/types/modules/id-card/index.d.ts +89 -0
- package/dist/types/modules/id-card/ocr-processor.d.ts +110 -0
- package/dist/types/modules/id-card/ocr-worker.d.ts +31 -0
- package/dist/types/modules/id-card/types.d.ts +181 -0
- package/dist/types/modules/qrcode/index.d.ts +51 -0
- package/dist/types/modules/qrcode/qr-code-scanner.d.ts +64 -0
- package/dist/types/modules/qrcode/types.d.ts +67 -0
- package/dist/types/utils/camera.d.ts +81 -0
- package/dist/types/utils/image-processing.d.ts +176 -0
- package/dist/types/utils/index.d.ts +175 -0
- package/dist/types/utils/performance.d.ts +81 -0
- package/dist/types/utils/resource-manager.d.ts +53 -0
- package/dist/types/utils/types.d.ts +166 -0
- package/dist/types/utils/worker.d.ts +52 -0
- package/dist/types/version.d.ts +7 -0
- package/package.json +76 -77
- package/src/core/base-module.ts +78 -0
- package/src/core/camera-manager.ts +798 -0
- package/src/core/config.ts +268 -0
- package/src/core/errors.ts +174 -0
- package/src/core/event-emitter.ts +110 -0
- package/src/core/logger.ts +549 -0
- package/src/core/module-manager.ts +165 -0
- package/src/core/plugin-manager.ts +429 -0
- package/src/core/resource-manager.ts +762 -0
- package/src/core/result.ts +163 -0
- package/src/core/scanner-factory.ts +237 -0
- package/src/index.ts +113 -936
- package/src/interfaces/external-types.ts +200 -0
- package/src/interfaces/face-detection.ts +309 -0
- package/src/interfaces/scanner-module.ts +384 -0
- package/src/modules/face/face-detector.ts +931 -0
- package/src/modules/face/index.ts +208 -0
- package/src/modules/face/liveness-detector.ts +908 -0
- package/src/modules/face/types.ts +133 -0
- package/src/modules/id-card/anti-fake-detector.ts +732 -0
- package/src/modules/id-card/id-card-detector.ts +474 -0
- package/src/modules/id-card/index.ts +425 -0
- package/src/modules/id-card/ocr-processor.ts +538 -0
- package/src/modules/id-card/ocr-worker.ts +259 -0
- package/src/modules/id-card/types.ts +178 -0
- package/src/modules/qrcode/index.ts +175 -0
- package/src/modules/qrcode/qr-code-scanner.ts +230 -0
- package/src/modules/qrcode/types.ts +65 -0
- package/src/types/browser-image-compression.d.ts +19 -0
- package/src/types/tesseract.d.ts +280 -0
- package/src/utils/image-processing.ts +432 -49
- package/src/utils/index.ts +426 -0
- package/src/utils/performance.ts +168 -131
- package/src/utils/resource-manager.ts +65 -146
- package/src/utils/types.ts +90 -2
- package/src/utils/worker.ts +123 -84
- package/src/version.ts +11 -0
- package/tools/scaffold.js +543 -0
- package/dist/id-scanner-core.esm.js +0 -11076
- package/dist/id-scanner-core.esm.js.map +0 -1
- package/dist/id-scanner-core.js +0 -11088
- package/dist/id-scanner-core.js.map +0 -1
- package/dist/id-scanner-core.min.js +0 -1
- package/dist/id-scanner-core.min.js.map +0 -1
- package/dist/id-scanner-ocr.esm.js +0 -1802
- package/dist/id-scanner-ocr.esm.js.map +0 -1
- package/dist/id-scanner-ocr.js +0 -1811
- package/dist/id-scanner-ocr.js.map +0 -1
- package/dist/id-scanner-ocr.min.js +0 -1
- package/dist/id-scanner-ocr.min.js.map +0 -1
- package/dist/id-scanner-qr.esm.js +0 -1023
- package/dist/id-scanner-qr.esm.js.map +0 -1
- package/dist/id-scanner-qr.js +0 -1032
- package/dist/id-scanner-qr.js.map +0 -1
- package/dist/id-scanner-qr.min.js +0 -1
- package/dist/id-scanner-qr.min.js.map +0 -1
- package/dist/id-scanner.js +0 -3740
- package/dist/id-scanner.js.map +0 -1
- package/dist/id-scanner.min.js +0 -1
- package/dist/id-scanner.min.js.map +0 -1
- package/src/core.ts +0 -138
- package/src/demo/demo.ts +0 -204
- package/src/id-recognition/anti-fake-detector.ts +0 -317
- package/src/id-recognition/data-extractor.ts +0 -262
- package/src/id-recognition/id-detector.ts +0 -363
- package/src/id-recognition/ocr-processor.ts +0 -334
- package/src/id-recognition/ocr-worker.ts +0 -156
- package/src/index-umd.ts +0 -477
- package/src/ocr-module.ts +0 -187
- package/src/qr-module.ts +0 -179
- package/src/scanner/barcode-scanner.ts +0 -251
- package/src/scanner/qr-scanner.ts +0 -167
|
@@ -0,0 +1,762 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file 资源管理器
|
|
3
|
+
* @description 提供资源加载、缓存和释放功能
|
|
4
|
+
* @module core/resource-manager
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import { ConfigManager } from './config';
|
|
8
|
+
import { Logger } from './logger';
|
|
9
|
+
import { ResourceLoadError } from './errors';
|
|
10
|
+
import { EventEmitter } from './event-emitter';
|
|
11
|
+
import { Result } from './result';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* 资源类型枚举
|
|
15
|
+
*/
|
|
16
|
+
export enum ResourceType {
|
|
17
|
+
MODEL = 'model', // 模型文件
|
|
18
|
+
WASM = 'wasm', // WebAssembly文件
|
|
19
|
+
IMAGE = 'image', // 图片文件
|
|
20
|
+
JSON = 'json', // JSON文件
|
|
21
|
+
TEXT = 'text', // 文本文件
|
|
22
|
+
ARRAYBUFFER = 'buffer', // 二进制数据
|
|
23
|
+
WORKER = 'worker', // Web Worker脚本
|
|
24
|
+
OTHER = 'other' // 其他资源
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* 资源接口
|
|
29
|
+
*/
|
|
30
|
+
export interface Resource<T = any> {
|
|
31
|
+
/** 资源ID */
|
|
32
|
+
id: string;
|
|
33
|
+
/** 资源类型 */
|
|
34
|
+
type: ResourceType;
|
|
35
|
+
/** 资源URL或数据 */
|
|
36
|
+
url: string;
|
|
37
|
+
/** 是否已加载 */
|
|
38
|
+
loaded: boolean;
|
|
39
|
+
/** 加载的数据 */
|
|
40
|
+
data?: T;
|
|
41
|
+
/** 上次使用时间戳 */
|
|
42
|
+
lastUsed: number;
|
|
43
|
+
/** 是否为永久资源(不自动释放) */
|
|
44
|
+
permanent: boolean;
|
|
45
|
+
/** 获取资源大小(如果可计算) */
|
|
46
|
+
getSize(): number;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* 资源加载选项
|
|
51
|
+
*/
|
|
52
|
+
export interface ResourceLoadOptions {
|
|
53
|
+
/** 是否缓存 */
|
|
54
|
+
cache?: boolean;
|
|
55
|
+
/** 是否为永久资源(不自动释放) */
|
|
56
|
+
permanent?: boolean;
|
|
57
|
+
/** 资源类型(自动推断) */
|
|
58
|
+
type?: ResourceType;
|
|
59
|
+
/** 加载超时(ms) */
|
|
60
|
+
timeout?: number;
|
|
61
|
+
/** 是否替换现有资源 */
|
|
62
|
+
forceReload?: boolean;
|
|
63
|
+
/** 是否使用凭证 */
|
|
64
|
+
credentials?: RequestCredentials;
|
|
65
|
+
/** 自定义请求头 */
|
|
66
|
+
headers?: Record<string, string>;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* 资源统计信息
|
|
71
|
+
*/
|
|
72
|
+
export interface ResourceStats {
|
|
73
|
+
/** 总资源数 */
|
|
74
|
+
totalCount: number;
|
|
75
|
+
/** 总内存使用(字节) */
|
|
76
|
+
totalSize: number;
|
|
77
|
+
/** 各类型资源数 */
|
|
78
|
+
byType: Record<ResourceType, number>;
|
|
79
|
+
/** 各类型资源大小(字节) */
|
|
80
|
+
sizeByType: Record<ResourceType, number>;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* 资源管理器事件
|
|
85
|
+
*/
|
|
86
|
+
export enum ResourceManagerEvent {
|
|
87
|
+
/** 资源加载开始 */
|
|
88
|
+
LOAD_START = 'resource:load:start',
|
|
89
|
+
/** 资源加载成功 */
|
|
90
|
+
LOAD_SUCCESS = 'resource:load:success',
|
|
91
|
+
/** 资源加载失败 */
|
|
92
|
+
LOAD_ERROR = 'resource:load:error',
|
|
93
|
+
/** 资源加载进度 */
|
|
94
|
+
LOAD_PROGRESS = 'resource:load:progress',
|
|
95
|
+
/** 资源被释放 */
|
|
96
|
+
RESOURCE_RELEASED = 'resource:released',
|
|
97
|
+
/** 资源统计更新 */
|
|
98
|
+
STATS_UPDATED = 'resource:stats:updated'
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* 资源管理器
|
|
103
|
+
* 提供统一的资源加载、缓存和管理功能
|
|
104
|
+
*/
|
|
105
|
+
export class ResourceManager extends EventEmitter {
|
|
106
|
+
/** 单例实例 */
|
|
107
|
+
private static instance: ResourceManager;
|
|
108
|
+
/** 资源映射表 */
|
|
109
|
+
private resources: Map<string, Resource> = new Map();
|
|
110
|
+
/** 配置管理器 */
|
|
111
|
+
private config: ConfigManager;
|
|
112
|
+
/** 日志记录器 */
|
|
113
|
+
private logger: Logger;
|
|
114
|
+
/** 缓存清理计时器ID */
|
|
115
|
+
private cleanupTimerId: number | null = null;
|
|
116
|
+
/** 默认基础路径 */
|
|
117
|
+
private basePath: string = '';
|
|
118
|
+
/** 加载中的资源请求 */
|
|
119
|
+
private pendingRequests: Map<string, Promise<any>> = new Map();
|
|
120
|
+
private initialized: boolean = false;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* 私有构造函数
|
|
124
|
+
*/
|
|
125
|
+
private constructor() {
|
|
126
|
+
super();
|
|
127
|
+
this.config = ConfigManager.getInstance();
|
|
128
|
+
this.logger = Logger.getInstance();
|
|
129
|
+
|
|
130
|
+
// 初始化资源清理定时器
|
|
131
|
+
this.setupCleanupTimer();
|
|
132
|
+
|
|
133
|
+
// 页面卸载时尝试释放资源
|
|
134
|
+
window.addEventListener('beforeunload', () => {
|
|
135
|
+
if (this.config.get('autoReleaseResources', true)) {
|
|
136
|
+
this.releaseAll();
|
|
137
|
+
}
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* 获取单例实例
|
|
143
|
+
*/
|
|
144
|
+
public static getInstance(): ResourceManager {
|
|
145
|
+
if (!ResourceManager.instance) {
|
|
146
|
+
ResourceManager.instance = new ResourceManager();
|
|
147
|
+
}
|
|
148
|
+
return ResourceManager.instance;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* 设置基础路径
|
|
153
|
+
* @param path 基础路径
|
|
154
|
+
*/
|
|
155
|
+
setBasePath(path: string): void {
|
|
156
|
+
if (path && !path.endsWith('/')) {
|
|
157
|
+
path += '/';
|
|
158
|
+
}
|
|
159
|
+
this.basePath = path;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* 获取资源完整URL
|
|
164
|
+
*/
|
|
165
|
+
private getFullUrl(url: string): string {
|
|
166
|
+
if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('blob:') || url.startsWith('data:')) {
|
|
167
|
+
return url;
|
|
168
|
+
}
|
|
169
|
+
return this.basePath + url;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/**
|
|
173
|
+
* 从URL推断资源类型
|
|
174
|
+
*/
|
|
175
|
+
private inferResourceType(url: string): ResourceType {
|
|
176
|
+
if (url.startsWith('data:')) {
|
|
177
|
+
const mimeType = url.split(',')[0].split(':')[1].split(';')[0];
|
|
178
|
+
if (mimeType.startsWith('image/')) return ResourceType.IMAGE;
|
|
179
|
+
if (mimeType === 'application/json') return ResourceType.JSON;
|
|
180
|
+
if (mimeType === 'text/plain') return ResourceType.TEXT;
|
|
181
|
+
return ResourceType.OTHER;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// 获取文件扩展名
|
|
185
|
+
const ext = url.split('?')[0].split('#')[0].split('.').pop()?.toLowerCase() || '';
|
|
186
|
+
|
|
187
|
+
switch (ext) {
|
|
188
|
+
case 'json': return ResourceType.JSON;
|
|
189
|
+
case 'png': case 'jpg': case 'jpeg': case 'gif': case 'webp': case 'bmp': case 'svg':
|
|
190
|
+
return ResourceType.IMAGE;
|
|
191
|
+
case 'wasm': case 'wat':
|
|
192
|
+
return ResourceType.WASM;
|
|
193
|
+
case 'txt': case 'md': case 'csv': case 'tsv': case 'html': case 'xml': case 'css': case 'js':
|
|
194
|
+
return ResourceType.TEXT;
|
|
195
|
+
case 'bin': case 'dat':
|
|
196
|
+
return ResourceType.ARRAYBUFFER;
|
|
197
|
+
default:
|
|
198
|
+
return ResourceType.OTHER;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* 加载资源
|
|
204
|
+
* @param id 资源ID
|
|
205
|
+
* @param url 资源URL
|
|
206
|
+
* @param options 加载选项
|
|
207
|
+
*/
|
|
208
|
+
async load<T = any>(id: string, url: string, options: ResourceLoadOptions = {}): Promise<Result<T>> {
|
|
209
|
+
const {
|
|
210
|
+
cache = true,
|
|
211
|
+
permanent = false,
|
|
212
|
+
type = this.inferResourceType(url),
|
|
213
|
+
timeout = 30000,
|
|
214
|
+
forceReload = false,
|
|
215
|
+
credentials = 'same-origin',
|
|
216
|
+
headers = {}
|
|
217
|
+
} = options;
|
|
218
|
+
|
|
219
|
+
const fullUrl = this.getFullUrl(url);
|
|
220
|
+
|
|
221
|
+
// 检查资源是否已存在
|
|
222
|
+
if (!forceReload && this.resources.has(id)) {
|
|
223
|
+
const resource = this.resources.get(id)!;
|
|
224
|
+
resource.lastUsed = Date.now();
|
|
225
|
+
|
|
226
|
+
if (resource.loaded && resource.data !== undefined) {
|
|
227
|
+
this.logger.debug('ResourceManager', `Resource ${id} loaded from cache`);
|
|
228
|
+
return Result.success(resource.data as T);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// 检查是否在加载队列中
|
|
233
|
+
if (this.pendingRequests.has(id)) {
|
|
234
|
+
try {
|
|
235
|
+
const data = await this.pendingRequests.get(id)!;
|
|
236
|
+
return Result.success(data as T);
|
|
237
|
+
} catch (error) {
|
|
238
|
+
return Result.failure(new ResourceLoadError(id, (error as Error).message));
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// 开始加载资源
|
|
243
|
+
this.emit(ResourceManagerEvent.LOAD_START, { id, url: fullUrl });
|
|
244
|
+
|
|
245
|
+
let loadPromise: Promise<any>;
|
|
246
|
+
|
|
247
|
+
switch (type) {
|
|
248
|
+
case ResourceType.IMAGE:
|
|
249
|
+
loadPromise = this.loadImage(fullUrl);
|
|
250
|
+
break;
|
|
251
|
+
case ResourceType.JSON:
|
|
252
|
+
loadPromise = this.loadJson(fullUrl, { credentials, headers });
|
|
253
|
+
break;
|
|
254
|
+
case ResourceType.TEXT:
|
|
255
|
+
loadPromise = this.loadText(fullUrl, { credentials, headers });
|
|
256
|
+
break;
|
|
257
|
+
case ResourceType.ARRAYBUFFER:
|
|
258
|
+
loadPromise = this.loadArrayBuffer(fullUrl, { credentials, headers });
|
|
259
|
+
break;
|
|
260
|
+
case ResourceType.WASM:
|
|
261
|
+
loadPromise = this.loadWasm(fullUrl, { credentials, headers });
|
|
262
|
+
break;
|
|
263
|
+
default:
|
|
264
|
+
loadPromise = this.loadGeneric(fullUrl, type, { credentials, headers });
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// 添加超时处理
|
|
268
|
+
const timeoutPromise = new Promise<never>((_, reject) => {
|
|
269
|
+
const timerId = setTimeout(() => {
|
|
270
|
+
reject(new Error(`Resource ${id} load timeout after ${timeout}ms`));
|
|
271
|
+
}, timeout);
|
|
272
|
+
|
|
273
|
+
// 请求完成后清除计时器
|
|
274
|
+
loadPromise.then(() => clearTimeout(timerId), () => clearTimeout(timerId));
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
// 添加到加载队列
|
|
278
|
+
const racePromise = Promise.race([loadPromise, timeoutPromise]);
|
|
279
|
+
this.pendingRequests.set(id, racePromise);
|
|
280
|
+
|
|
281
|
+
try {
|
|
282
|
+
const data = await racePromise;
|
|
283
|
+
|
|
284
|
+
// 创建或更新资源
|
|
285
|
+
const resource: Resource<T> = {
|
|
286
|
+
id,
|
|
287
|
+
type,
|
|
288
|
+
url: fullUrl,
|
|
289
|
+
loaded: true,
|
|
290
|
+
data,
|
|
291
|
+
lastUsed: Date.now(),
|
|
292
|
+
permanent,
|
|
293
|
+
getSize: () => this.calculateResourceSize(data, type)
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
if (cache) {
|
|
297
|
+
this.resources.set(id, resource);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
this.emit(ResourceManagerEvent.LOAD_SUCCESS, { id, resource });
|
|
301
|
+
this.updateStats();
|
|
302
|
+
|
|
303
|
+
return Result.success(data);
|
|
304
|
+
} catch (error) {
|
|
305
|
+
const errorMessage = (error as Error).message || String(error);
|
|
306
|
+
this.logger.error('ResourceManager', `Failed to load resource ${id}: ${errorMessage}`);
|
|
307
|
+
|
|
308
|
+
this.emit(ResourceManagerEvent.LOAD_ERROR, { id, error });
|
|
309
|
+
|
|
310
|
+
return Result.failure(new ResourceLoadError(id, errorMessage));
|
|
311
|
+
} finally {
|
|
312
|
+
this.pendingRequests.delete(id);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* 加载图片资源
|
|
318
|
+
*/
|
|
319
|
+
private loadImage(url: string): Promise<HTMLImageElement> {
|
|
320
|
+
return new Promise((resolve, reject) => {
|
|
321
|
+
const image = new Image();
|
|
322
|
+
|
|
323
|
+
image.onload = () => resolve(image);
|
|
324
|
+
image.onerror = () => reject(new Error(`Failed to load image: ${url}`));
|
|
325
|
+
|
|
326
|
+
image.crossOrigin = 'anonymous';
|
|
327
|
+
image.src = url;
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* 加载JSON资源
|
|
333
|
+
*/
|
|
334
|
+
private async loadJson(url: string, options: RequestInit): Promise<any> {
|
|
335
|
+
const response = await fetch(url, options);
|
|
336
|
+
if (!response.ok) {
|
|
337
|
+
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
338
|
+
}
|
|
339
|
+
return response.json();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
/**
|
|
343
|
+
* 加载文本资源
|
|
344
|
+
*/
|
|
345
|
+
private async loadText(url: string, options: RequestInit): Promise<string> {
|
|
346
|
+
const response = await fetch(url, options);
|
|
347
|
+
if (!response.ok) {
|
|
348
|
+
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
349
|
+
}
|
|
350
|
+
return response.text();
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* 加载二进制数据
|
|
355
|
+
*/
|
|
356
|
+
private async loadArrayBuffer(url: string, options: RequestInit): Promise<ArrayBuffer> {
|
|
357
|
+
const response = await fetch(url, options);
|
|
358
|
+
if (!response.ok) {
|
|
359
|
+
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
360
|
+
}
|
|
361
|
+
return response.arrayBuffer();
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* 加载WebAssembly模块
|
|
366
|
+
*/
|
|
367
|
+
private async loadWasm(url: string, options: RequestInit): Promise<WebAssembly.Module> {
|
|
368
|
+
const buffer = await this.loadArrayBuffer(url, options);
|
|
369
|
+
return WebAssembly.compile(buffer);
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/**
|
|
373
|
+
* 加载通用资源
|
|
374
|
+
*/
|
|
375
|
+
private async loadGeneric(url: string, type: ResourceType, options: RequestInit): Promise<any> {
|
|
376
|
+
const response = await fetch(url, options);
|
|
377
|
+
if (!response.ok) {
|
|
378
|
+
throw new Error(`HTTP error ${response.status}: ${response.statusText}`);
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// 根据响应类型决定如何处理数据
|
|
382
|
+
const contentType = response.headers.get('content-type') || '';
|
|
383
|
+
|
|
384
|
+
if (contentType.includes('application/json')) {
|
|
385
|
+
return response.json();
|
|
386
|
+
} else if (contentType.includes('text/')) {
|
|
387
|
+
return response.text();
|
|
388
|
+
} else {
|
|
389
|
+
return response.arrayBuffer();
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* 预加载多个资源
|
|
395
|
+
* @param resources 资源配置数组,每项包含id和url
|
|
396
|
+
*/
|
|
397
|
+
async preload(resources: Array<{ id: string; url: string; options?: ResourceLoadOptions }>): Promise<Result<Record<string, any>>> {
|
|
398
|
+
const results: Record<string, any> = {};
|
|
399
|
+
const errors: Array<{ id: string; error: Error }> = [];
|
|
400
|
+
|
|
401
|
+
// 并行加载所有资源
|
|
402
|
+
const promises = resources.map(async ({ id, url, options }) => {
|
|
403
|
+
const result = await this.load(id, url, options);
|
|
404
|
+
|
|
405
|
+
if (result.isSuccess() && result.data !== undefined) {
|
|
406
|
+
results[id] = result.data;
|
|
407
|
+
} else if (result.isFailure() && result.error) {
|
|
408
|
+
errors.push({ id, error: result.error });
|
|
409
|
+
}
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
await Promise.all(promises);
|
|
413
|
+
|
|
414
|
+
// 如果有错误,返回失败结果
|
|
415
|
+
if (errors.length > 0) {
|
|
416
|
+
const errorMessages = errors.map(e => `${e.id}: ${e.error.message}`).join('; ');
|
|
417
|
+
return Result.failure(
|
|
418
|
+
new ResourceLoadError('multiple', `Failed to load resources: ${errorMessages}`),
|
|
419
|
+
{
|
|
420
|
+
successfulLoads: results,
|
|
421
|
+
failedLoads: errors.map(e => e.id)
|
|
422
|
+
}
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
return Result.success(results);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* 获取资源
|
|
431
|
+
* @param id 资源ID
|
|
432
|
+
*/
|
|
433
|
+
get<T = any>(id: string): T | undefined {
|
|
434
|
+
const resource = this.resources.get(id);
|
|
435
|
+
|
|
436
|
+
if (resource) {
|
|
437
|
+
resource.lastUsed = Date.now();
|
|
438
|
+
return resource.data as T;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
return undefined;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* 检查资源是否存在
|
|
446
|
+
* @param id 资源ID
|
|
447
|
+
*/
|
|
448
|
+
has(id: string): boolean {
|
|
449
|
+
return this.resources.has(id);
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* 获取资源
|
|
454
|
+
* 如果不存在,则使用工厂函数创建并缓存
|
|
455
|
+
*
|
|
456
|
+
* @param id 资源ID
|
|
457
|
+
* @param factory 资源工厂函数
|
|
458
|
+
* @param type 资源类型
|
|
459
|
+
* @param permanent 是否永久保留
|
|
460
|
+
*/
|
|
461
|
+
getOrCreate<T = any>(
|
|
462
|
+
id: string,
|
|
463
|
+
factory: () => T | Promise<T>,
|
|
464
|
+
type: ResourceType = ResourceType.OTHER,
|
|
465
|
+
permanent: boolean = false
|
|
466
|
+
): Promise<T> {
|
|
467
|
+
// 检查资源是否已存在
|
|
468
|
+
if (this.resources.has(id)) {
|
|
469
|
+
const resource = this.resources.get(id)!;
|
|
470
|
+
resource.lastUsed = Date.now();
|
|
471
|
+
return Promise.resolve(resource.data as T);
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
// 检查是否在加载队列中
|
|
475
|
+
if (this.pendingRequests.has(id)) {
|
|
476
|
+
return this.pendingRequests.get(id) as Promise<T>;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// 创建资源
|
|
480
|
+
const createPromise = Promise.resolve().then(async () => {
|
|
481
|
+
try {
|
|
482
|
+
const data = await factory();
|
|
483
|
+
|
|
484
|
+
// 创建或更新资源
|
|
485
|
+
const resource: Resource<T> = {
|
|
486
|
+
id,
|
|
487
|
+
type,
|
|
488
|
+
url: '',
|
|
489
|
+
loaded: true,
|
|
490
|
+
data,
|
|
491
|
+
lastUsed: Date.now(),
|
|
492
|
+
permanent,
|
|
493
|
+
getSize: () => this.calculateResourceSize(data, type)
|
|
494
|
+
};
|
|
495
|
+
|
|
496
|
+
this.resources.set(id, resource);
|
|
497
|
+
this.emit(ResourceManagerEvent.LOAD_SUCCESS, { id, resource });
|
|
498
|
+
this.updateStats();
|
|
499
|
+
|
|
500
|
+
return data;
|
|
501
|
+
} catch (error) {
|
|
502
|
+
this.logger.error('ResourceManager', `Failed to create resource ${id}: ${error}`);
|
|
503
|
+
this.emit(ResourceManagerEvent.LOAD_ERROR, { id, error });
|
|
504
|
+
throw error;
|
|
505
|
+
} finally {
|
|
506
|
+
this.pendingRequests.delete(id);
|
|
507
|
+
}
|
|
508
|
+
});
|
|
509
|
+
|
|
510
|
+
// 添加到加载队列
|
|
511
|
+
this.pendingRequests.set(id, createPromise);
|
|
512
|
+
|
|
513
|
+
return createPromise;
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
/**
|
|
517
|
+
* 释放资源
|
|
518
|
+
* @param id 资源ID
|
|
519
|
+
*/
|
|
520
|
+
release(id: string): boolean {
|
|
521
|
+
if (!this.resources.has(id)) {
|
|
522
|
+
return false;
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const resource = this.resources.get(id)!;
|
|
526
|
+
|
|
527
|
+
// 执行特定类型的清理
|
|
528
|
+
this.cleanupResource(resource);
|
|
529
|
+
|
|
530
|
+
// 从映射中删除
|
|
531
|
+
this.resources.delete(id);
|
|
532
|
+
|
|
533
|
+
this.emit(ResourceManagerEvent.RESOURCE_RELEASED, { id });
|
|
534
|
+
this.updateStats();
|
|
535
|
+
|
|
536
|
+
this.logger.debug('ResourceManager', `Released resource ${id}`);
|
|
537
|
+
return true;
|
|
538
|
+
}
|
|
539
|
+
|
|
540
|
+
/**
|
|
541
|
+
* 释放资源组
|
|
542
|
+
* @param pattern 资源ID匹配模式,可以是字符串前缀或正则表达式
|
|
543
|
+
*/
|
|
544
|
+
releaseGroup(pattern: string | RegExp): number {
|
|
545
|
+
let count = 0;
|
|
546
|
+
|
|
547
|
+
for (const [id, resource] of this.resources.entries()) {
|
|
548
|
+
// 如果是永久资源则跳过
|
|
549
|
+
if (resource.permanent) continue;
|
|
550
|
+
|
|
551
|
+
let matches = false;
|
|
552
|
+
if (typeof pattern === 'string') {
|
|
553
|
+
matches = id.startsWith(pattern);
|
|
554
|
+
} else {
|
|
555
|
+
matches = pattern.test(id);
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
if (matches && this.release(id)) {
|
|
559
|
+
count++;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return count;
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
/**
|
|
567
|
+
* 释放所有非永久资源
|
|
568
|
+
*/
|
|
569
|
+
releaseAll(): number {
|
|
570
|
+
let count = 0;
|
|
571
|
+
|
|
572
|
+
for (const id of this.resources.keys()) {
|
|
573
|
+
const resource = this.resources.get(id)!;
|
|
574
|
+
|
|
575
|
+
// 跳过永久资源
|
|
576
|
+
if (resource.permanent) continue;
|
|
577
|
+
|
|
578
|
+
if (this.release(id)) {
|
|
579
|
+
count++;
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return count;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
/**
|
|
587
|
+
* 释放过期资源
|
|
588
|
+
* @param maxAge 最大闲置时间(毫秒)
|
|
589
|
+
*/
|
|
590
|
+
releaseExpired(maxAge: number): number {
|
|
591
|
+
const now = Date.now();
|
|
592
|
+
let count = 0;
|
|
593
|
+
|
|
594
|
+
for (const [id, resource] of this.resources.entries()) {
|
|
595
|
+
// 跳过永久资源
|
|
596
|
+
if (resource.permanent) continue;
|
|
597
|
+
|
|
598
|
+
// 检查是否过期
|
|
599
|
+
const age = now - resource.lastUsed;
|
|
600
|
+
if (age > maxAge && this.release(id)) {
|
|
601
|
+
count++;
|
|
602
|
+
}
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
return count;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
/**
|
|
609
|
+
* 计算资源大小
|
|
610
|
+
* @param data 资源数据
|
|
611
|
+
* @param type 资源类型
|
|
612
|
+
*/
|
|
613
|
+
private calculateResourceSize(data: any, type: ResourceType): number {
|
|
614
|
+
if (!data) return 0;
|
|
615
|
+
|
|
616
|
+
switch (type) {
|
|
617
|
+
case ResourceType.IMAGE:
|
|
618
|
+
// 粗略估计图像大小
|
|
619
|
+
if (data instanceof HTMLImageElement) {
|
|
620
|
+
return data.width * data.height * 4; // 假设4字节/像素 (RGBA)
|
|
621
|
+
}
|
|
622
|
+
break;
|
|
623
|
+
case ResourceType.ARRAYBUFFER:
|
|
624
|
+
return (data as ArrayBuffer).byteLength;
|
|
625
|
+
case ResourceType.TEXT:
|
|
626
|
+
return (data as string).length * 2; // 假设2字节/字符
|
|
627
|
+
case ResourceType.JSON:
|
|
628
|
+
return JSON.stringify(data).length * 2; // 假设2字节/字符
|
|
629
|
+
default:
|
|
630
|
+
// 尝试推断大小
|
|
631
|
+
if (data.byteLength) {
|
|
632
|
+
return data.byteLength;
|
|
633
|
+
}
|
|
634
|
+
if (typeof data === 'string') {
|
|
635
|
+
return data.length * 2;
|
|
636
|
+
}
|
|
637
|
+
if (typeof data === 'object') {
|
|
638
|
+
return JSON.stringify(data).length * 2;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
// 无法计算的资源返回0
|
|
643
|
+
return 0;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* 清理特定资源
|
|
648
|
+
* @param resource 资源对象
|
|
649
|
+
*/
|
|
650
|
+
private cleanupResource(resource: Resource): void {
|
|
651
|
+
if (!resource.data) return;
|
|
652
|
+
|
|
653
|
+
switch (resource.type) {
|
|
654
|
+
case ResourceType.IMAGE:
|
|
655
|
+
// 释放图像
|
|
656
|
+
if (resource.data instanceof HTMLImageElement) {
|
|
657
|
+
// 将src设置为空白图像可以帮助浏览器释放内存
|
|
658
|
+
(resource.data as HTMLImageElement).src = '';
|
|
659
|
+
}
|
|
660
|
+
break;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// 移除资源数据引用
|
|
664
|
+
resource.data = undefined;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
/**
|
|
668
|
+
* 设置资源清理定时器
|
|
669
|
+
*/
|
|
670
|
+
private setupCleanupTimer(): void {
|
|
671
|
+
const interval = 60000; // 每分钟检查一次
|
|
672
|
+
|
|
673
|
+
this.cleanupTimerId = window.setInterval(() => {
|
|
674
|
+
// 检查缓存设置
|
|
675
|
+
if (!this.config.get('performance.useCache', true)) {
|
|
676
|
+
// 如果禁用缓存,释放所有非永久资源
|
|
677
|
+
this.releaseAll();
|
|
678
|
+
return;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
// 默认10分钟不使用自动释放
|
|
682
|
+
const maxAge = 10 * 60 * 1000;
|
|
683
|
+
this.releaseExpired(maxAge);
|
|
684
|
+
}, interval);
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
/**
|
|
688
|
+
* 获取资源统计信息
|
|
689
|
+
*/
|
|
690
|
+
getStats(): ResourceStats {
|
|
691
|
+
const stats: ResourceStats = {
|
|
692
|
+
totalCount: 0,
|
|
693
|
+
totalSize: 0,
|
|
694
|
+
byType: {} as Record<ResourceType, number>,
|
|
695
|
+
sizeByType: {} as Record<ResourceType, number>
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
// 初始化类型计数器
|
|
699
|
+
Object.values(ResourceType).forEach(type => {
|
|
700
|
+
stats.byType[type] = 0;
|
|
701
|
+
stats.sizeByType[type] = 0;
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
// 统计资源
|
|
705
|
+
for (const resource of this.resources.values()) {
|
|
706
|
+
stats.totalCount++;
|
|
707
|
+
|
|
708
|
+
const size = resource.getSize();
|
|
709
|
+
stats.totalSize += size;
|
|
710
|
+
|
|
711
|
+
stats.byType[resource.type]++;
|
|
712
|
+
stats.sizeByType[resource.type] += size;
|
|
713
|
+
}
|
|
714
|
+
|
|
715
|
+
return stats;
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
/**
|
|
719
|
+
* 更新并发布资源统计信息
|
|
720
|
+
*/
|
|
721
|
+
private updateStats(): void {
|
|
722
|
+
const stats = this.getStats();
|
|
723
|
+
this.emit(ResourceManagerEvent.STATS_UPDATED, stats);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
/**
|
|
727
|
+
* 初始化资源管理器
|
|
728
|
+
* @param options 初始化选项
|
|
729
|
+
*/
|
|
730
|
+
public async initialize(options?: {
|
|
731
|
+
basePath?: string;
|
|
732
|
+
preloadResources?: Array<{ id: string; url: string; type?: ResourceType }>;
|
|
733
|
+
}): Promise<void> {
|
|
734
|
+
if (this.initialized) {
|
|
735
|
+
this.logger.debug('ResourceManager', '资源管理器已初始化');
|
|
736
|
+
return;
|
|
737
|
+
}
|
|
738
|
+
|
|
739
|
+
this.logger.debug('ResourceManager', '初始化资源管理器');
|
|
740
|
+
|
|
741
|
+
// 设置基础路径
|
|
742
|
+
if (options?.basePath) {
|
|
743
|
+
this.setBasePath(options.basePath);
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// 预加载资源
|
|
747
|
+
if (options?.preloadResources) {
|
|
748
|
+
const loadPromises = options.preloadResources.map(resource => {
|
|
749
|
+
const loadOptions: ResourceLoadOptions = {};
|
|
750
|
+
if (resource.type) {
|
|
751
|
+
loadOptions.type = resource.type;
|
|
752
|
+
}
|
|
753
|
+
return this.load(resource.id, resource.url, loadOptions);
|
|
754
|
+
});
|
|
755
|
+
await Promise.all(loadPromises);
|
|
756
|
+
}
|
|
757
|
+
|
|
758
|
+
this.initialized = true;
|
|
759
|
+
this.emit('manager:initialized', {});
|
|
760
|
+
this.logger.debug('ResourceManager', '资源管理器初始化完成');
|
|
761
|
+
}
|
|
762
|
+
}
|