UNPKG

fast-md5-web

Version:

A TypeScript project with tsup bundler for Rust WASM MD5 calculation

440 lines (438 loc) 13.9 kB
"use client" // src/index.ts import WasmInit, { Md5Calculator } from "../wasm/pkg"; import { v4 as uuidv4 } from "uuid"; var DEFAULT_CHUNK_SIZE = 8 * 1024 * 1024; var DEFAULT_SHARED_MEMORY_SIZE = 512 * 1024 * 1024; var MEMORY_CLEANUP_THRESHOLD = 0.8; var Md5CalculatorPool = class { constructor(poolSize = 4, sharedMemoryConfig, maxConcurrentTasks) { this.workers = []; this.availableWorkers = []; this.pendingTasks = []; this.activeTasks = /* @__PURE__ */ new Map(); this.taskCallbacks = /* @__PURE__ */ new Map(); this.sharedMemory = null; this.sharedMemoryView = null; this.memoryBlocks = []; this.poolSize = poolSize; this.maxConcurrentTasks = maxConcurrentTasks || poolSize; this.sharedMemoryConfig = sharedMemoryConfig || { enabled: false, memorySize: DEFAULT_SHARED_MEMORY_SIZE, chunkSize: DEFAULT_CHUNK_SIZE }; if (this.sharedMemoryConfig.enabled && this.isSharedArrayBufferSupported()) { this.initializeSharedMemory(); } this.initializeWorkers(); } isSharedArrayBufferSupported() { return typeof SharedArrayBuffer !== "undefined"; } initializeSharedMemory() { try { this.sharedMemory = new SharedArrayBuffer( this.sharedMemoryConfig.memorySize ); this.sharedMemoryView = new Uint8Array(this.sharedMemory); this.memoryBlocks = [ { offset: 0, size: this.sharedMemoryConfig.memorySize, inUse: false } ]; } catch (error) { this.sharedMemoryConfig.enabled = false; } } initializeWorkers() { for (let i = 0; i < this.poolSize; i++) { const worker = this.createWorker(); this.workers.push(worker); this.availableWorkers.push(worker); if (this.sharedMemoryConfig.enabled && this.sharedMemory) { worker.postMessage({ id: `init-${i}`, type: "init_shared_memory", data: { sharedMemory: this.sharedMemory } }); } } } createWorker() { const worker = new Worker(new URL("./md5-worker.js", import.meta.url), { type: "module" }); worker.onmessage = (e) => { const { id, type, data } = e.data; if (type === "result" || type === "error") { const callbacks = this.taskCallbacks.get(id); const task = this.activeTasks.get(id); if (callbacks) { this.taskCallbacks.delete(id); this.activeTasks.delete(id); if (task?.isLargeFile) { this.releaseSharedMemory(id); } if (type === "result") { callbacks.resolve(data.result); } else { callbacks.reject(new Error(data.error)); } } this.availableWorkers.push(worker); this.processNextTask(); } else if (type === "progress" && data?.progress !== void 0) { const callbacks = this.taskCallbacks.get(id); if (callbacks?.onProgress) { callbacks.onProgress(data.progress); } } }; worker.onerror = (error) => { for (const [taskId, callbacks] of this.taskCallbacks.entries()) { callbacks.reject( new Error(`Worker error: ${error.message || "Unknown worker error"}`) ); this.taskCallbacks.delete(taskId); } const index = this.workers.indexOf(worker); if (index !== -1) { this.workers[index] = this.createWorker(); const availableIndex = this.availableWorkers.indexOf(worker); if (availableIndex !== -1) { this.availableWorkers[availableIndex] = this.workers[index]; } else { this.availableWorkers.push(this.workers[index]); } } }; return worker; } processNextTask() { if (this.activeTasks.size >= this.maxConcurrentTasks) { return; } if (this.pendingTasks.length > 0 && this.availableWorkers.length > 0) { this.pendingTasks.sort((a, b) => b.priority - a.priority); const task = this.pendingTasks.shift(); const worker = this.availableWorkers.shift(); this.activeTasks.set(task.id, task); this.taskCallbacks.set(task.id, { resolve: task.resolve, reject: task.reject, onProgress: task.onProgress }); if (task.isLargeFile && task.data instanceof File) { this.processLargeFile(task, worker); } else { this.processSmallFile(task, worker); } } } async processLargeFile(task, worker) { const file = task.data; const chunkSize = this.sharedMemoryConfig.chunkSize; const totalChunks = Math.ceil(file.size / chunkSize); const memoryOffset = this.allocateSharedMemory(chunkSize, task.id); if (memoryOffset === -1) { const uint8Array = new Uint8Array(await file.arrayBuffer()); this.processSmallFile({ ...task, data: uint8Array }, worker); return; } worker.postMessage({ id: task.id, type: "calculate", data: { dataOffset: memoryOffset, dataLength: file.size, md5Length: task.md5Length, isStreamMode: true, totalChunks } }); for (let i = 0; i < totalChunks; i++) { const start = i * chunkSize; const end = Math.min(start + chunkSize, file.size); const chunk = file.slice(start, end); const chunkData = new Uint8Array(await chunk.arrayBuffer()); if (this.sharedMemoryView) { this.sharedMemoryView.set(chunkData, memoryOffset); worker.postMessage({ id: task.id, type: "calculate_chunk", data: { chunkIndex: i, dataLength: chunkData.length, dataOffset: memoryOffset } }); } await new Promise((resolve) => setTimeout(resolve, 0)); } } async processSmallFile(task, worker) { let data; if (task.data instanceof File) { const arrayBuffer = await task.data.arrayBuffer(); data = new Uint8Array(arrayBuffer); } else { data = task.data; } if (this.sharedMemoryConfig.enabled && this.sharedMemoryView && data.length <= this.sharedMemoryConfig.memorySize) { const dataOffset = this.allocateSharedMemory(data.length, task.id); if (dataOffset !== -1) { this.sharedMemoryView.set(data, dataOffset); worker.postMessage({ id: task.id, type: "calculate", data: { dataOffset, dataLength: data.length, md5Length: task.md5Length, isStreamMode: false } }); return; } } let buffer; if (data.byteLength === 0) { buffer = new ArrayBuffer(0); } else if (data.byteOffset === 0 && data.byteLength === data.buffer.byteLength) { if (data.buffer instanceof ArrayBuffer) { buffer = data.buffer; } else { buffer = new ArrayBuffer(data.byteLength); new Uint8Array(buffer).set(data); } } else { buffer = new ArrayBuffer(data.byteLength); new Uint8Array(buffer).set(data); } worker.postMessage( { id: task.id, type: "calculate", data: { fileData: buffer, md5Length: task.md5Length, isStreamMode: false } }, [buffer] ); } async calculateMd5(data, md5Length = 32, timeout = 6e4, onProgress, priority = 0) { return new Promise((resolve, reject) => { const taskId = uuidv4(); const isLargeFile = data instanceof File ? data.size > DEFAULT_CHUNK_SIZE : data.length > DEFAULT_CHUNK_SIZE; const timeoutId = setTimeout(() => { const callbacks = this.taskCallbacks.get(taskId); if (callbacks) { this.taskCallbacks.delete(taskId); this.activeTasks.delete(taskId); this.releaseSharedMemory(taskId); reject(new Error(`MD5 calculation timeout after ${timeout}ms`)); } }, timeout); const wrappedResolve = (result) => { clearTimeout(timeoutId); resolve(result); }; const wrappedReject = (error) => { clearTimeout(timeoutId); reject(error); }; const task = { id: taskId, data, md5Length, resolve: wrappedResolve, reject: wrappedReject, priority, isLargeFile, onProgress }; if (this.activeTasks.size >= this.maxConcurrentTasks) { this.pendingTasks.push(task); } else if (this.availableWorkers.length > 0) { const worker = this.availableWorkers.shift(); this.activeTasks.set(taskId, task); this.taskCallbacks.set(taskId, { resolve: wrappedResolve, reject: wrappedReject, onProgress }); if (task.isLargeFile && task.data instanceof File) { this.processLargeFile(task, worker); } else { this.processSmallFile(task, worker).catch(wrappedReject); } } else { this.pendingTasks.push(task); } }); } destroy() { this.workers.forEach((worker) => { worker.terminate(); }); this.workers = []; this.availableWorkers = []; this.pendingTasks = []; this.taskCallbacks.clear(); } allocateSharedMemory(size, taskId) { if (!this.sharedMemoryView) { return -1; } for (let i = 0; i < this.memoryBlocks.length; i++) { const block = this.memoryBlocks[i]; if (!block.inUse && block.size >= size) { block.inUse = true; block.taskId = taskId; if (block.size > size) { this.memoryBlocks.splice(i + 1, 0, { offset: block.offset + size, size: block.size - size, inUse: false }); block.size = size; } return block.offset; } } this.defragmentMemory(); for (let i = 0; i < this.memoryBlocks.length; i++) { const block = this.memoryBlocks[i]; if (!block.inUse && block.size >= size) { block.inUse = true; block.taskId = taskId; if (block.size > size) { this.memoryBlocks.splice(i + 1, 0, { offset: block.offset + size, size: block.size - size, inUse: false }); block.size = size; } return block.offset; } } return -1; } releaseSharedMemory(taskId) { for (const block of this.memoryBlocks) { if (block.taskId === taskId) { block.inUse = false; block.taskId = void 0; } } this.mergeAdjacentBlocks(); } defragmentMemory() { this.mergeAdjacentBlocks(); const totalUsed = this.memoryBlocks.filter((block) => block.inUse).reduce((sum, block) => sum + block.size, 0); const usageRatio = totalUsed / this.sharedMemoryConfig.memorySize; if (usageRatio > MEMORY_CLEANUP_THRESHOLD) { } } mergeAdjacentBlocks() { this.memoryBlocks.sort((a, b) => a.offset - b.offset); for (let i = 0; i < this.memoryBlocks.length - 1; i++) { const current = this.memoryBlocks[i]; const next = this.memoryBlocks[i + 1]; if (!current.inUse && !next.inUse && current.offset + current.size === next.offset) { current.size += next.size; this.memoryBlocks.splice(i + 1, 1); i--; } } } getPoolStatus() { const status = { totalWorkers: this.workers.length, availableWorkers: this.availableWorkers.length, pendingTasks: this.pendingTasks.length, activeTasks: this.activeTasks.size, maxConcurrentTasks: this.maxConcurrentTasks, sharedMemoryEnabled: this.sharedMemoryConfig.enabled }; if (this.sharedMemoryConfig.enabled && this.sharedMemory) { const totalUsed = this.memoryBlocks.filter((block) => block.inUse).reduce((sum, block) => sum + block.size, 0); const freeBlocks = this.memoryBlocks.filter((block) => !block.inUse).length; status.sharedMemoryUsage = { total: this.sharedMemoryConfig.memorySize, used: totalUsed, available: this.sharedMemoryConfig.memorySize - totalUsed, fragmentation: freeBlocks }; } return status; } enableSharedMemory(memorySize = DEFAULT_SHARED_MEMORY_SIZE, chunkSize = DEFAULT_CHUNK_SIZE) { if (!this.isSharedArrayBufferSupported()) { return false; } this.sharedMemoryConfig = { enabled: true, memorySize, chunkSize }; this.initializeSharedMemory(); this.destroy(); this.initializeWorkers(); return this.sharedMemoryConfig.enabled; } disableSharedMemory() { this.sharedMemoryConfig.enabled = false; this.sharedMemory = null; this.sharedMemoryView = null; this.memoryBlocks = []; } // 添加批量处理方法 async calculateMd5Batch(files, md5Length = 32, onProgress) { const results = []; let completed = 0; const promises = files.map( (file, index) => this.calculateMd5(file, md5Length, 6e4, void 0, files.length - index).then((result) => { results[index] = result; completed++; if (onProgress) { onProgress(completed, files.length); } return result; }) ); await Promise.all(promises); return results; } // 添加取消任务的方法 cancelTask(taskId) { const task = this.activeTasks.get(taskId); if (task) { this.activeTasks.delete(taskId); this.taskCallbacks.delete(taskId); this.releaseSharedMemory(taskId); task.reject(new Error("Task cancelled")); return true; } const pendingIndex = this.pendingTasks.findIndex((t) => t.id === taskId); if (pendingIndex !== -1) { const task2 = this.pendingTasks.splice(pendingIndex, 1)[0]; task2.reject(new Error("Task cancelled")); return true; } return false; } }; export { Md5Calculator, Md5CalculatorPool, WasmInit };