From 3077d5a7c5efa7374054bf75f730d1a9cc19f3b2 Mon Sep 17 00:00:00 2001 From: landaiqing Date: Mon, 29 Sep 2025 00:26:05 +0800 Subject: [PATCH] :recycle: Refactor document selector and cache management logic --- .../voidraft/internal/services/models.ts | 91 ++- .../internal/services/systemservice.ts | 26 + frontend/src/common/async-operation/index.ts | 3 - .../src/common/async-operation/manager.ts | 296 --------- frontend/src/common/async-operation/types.ts | 82 --- frontend/src/common/async/README.md | 285 +++++++++ frontend/src/common/async/index.ts | 78 +++ frontend/src/common/async/manager.ts | 574 ++++++++++++++++++ frontend/src/common/async/types.ts | 289 +++++++++ frontend/src/common/cache/README.md | 35 +- frontend/src/common/cache/cache-manager.ts | 133 ---- frontend/src/common/cache/doublyLinkedList.ts | 166 +++++ frontend/src/common/cache/index.ts | 28 +- frontend/src/common/cache/interfaces.ts | 124 ++++ frontend/src/common/cache/lru-cache.ts | 200 ------ frontend/src/common/cache/lruCache.ts | 276 +++++++++ frontend/src/common/cache/manager.ts | 263 ++++++++ frontend/src/common/cache/types.ts | 39 -- frontend/src/common/cache/utils.ts | 116 +++- frontend/src/components/tabs/TabContainer.vue | 233 +++++++ frontend/src/components/tabs/TabItem.vue | 273 +++++++++ .../components/titlebar/WindowsTitleBar.vue | 20 +- .../components/toolbar/DocumentSelector.vue | 524 ++++++---------- frontend/src/stores/documentStore.ts | 145 ++--- frontend/src/stores/editorCacheStore.ts | 310 +++++----- frontend/src/stores/editorStore.ts | 93 +-- frontend/src/stores/systemStore.ts | 25 +- frontend/src/stores/tabStore.ts | 253 ++++++++ internal/services/service_manager.go | 5 + internal/services/system_service.go | 52 ++ main.go | 5 +- 31 files changed, 3660 insertions(+), 1382 deletions(-) delete mode 100644 frontend/src/common/async-operation/index.ts delete mode 100644 frontend/src/common/async-operation/manager.ts delete mode 100644 frontend/src/common/async-operation/types.ts create mode 100644 frontend/src/common/async/README.md create mode 100644 frontend/src/common/async/index.ts create mode 100644 frontend/src/common/async/manager.ts create mode 100644 frontend/src/common/async/types.ts delete mode 100644 frontend/src/common/cache/cache-manager.ts create mode 100644 frontend/src/common/cache/doublyLinkedList.ts create mode 100644 frontend/src/common/cache/interfaces.ts delete mode 100644 frontend/src/common/cache/lru-cache.ts create mode 100644 frontend/src/common/cache/lruCache.ts create mode 100644 frontend/src/common/cache/manager.ts delete mode 100644 frontend/src/common/cache/types.ts create mode 100644 frontend/src/components/tabs/TabContainer.vue create mode 100644 frontend/src/components/tabs/TabItem.vue create mode 100644 frontend/src/stores/tabStore.ts diff --git a/frontend/bindings/voidraft/internal/services/models.ts b/frontend/bindings/voidraft/internal/services/models.ts index 0aa520b..99b4101 100644 --- a/frontend/bindings/voidraft/internal/services/models.ts +++ b/frontend/bindings/voidraft/internal/services/models.ts @@ -119,6 +119,42 @@ export enum MigrationStatus { MigrationStatusFailed = "failed", }; +/** + * OSInfo 操作系统信息 + */ +export class OSInfo { + "id": string; + "name": string; + "version": string; + "branding": string; + + /** Creates a new OSInfo instance. */ + constructor($$source: Partial = {}) { + if (!("id" in $$source)) { + this["id"] = ""; + } + if (!("name" in $$source)) { + this["name"] = ""; + } + if (!("version" in $$source)) { + this["version"] = ""; + } + if (!("branding" in $$source)) { + this["branding"] = ""; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new OSInfo instance from a string or object. + */ + static createFrom($$source: any = {}): OSInfo { + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + return new OSInfo($$parsedSource as Partial); + } +} + /** * SelfUpdateResult 自我更新结果 */ @@ -202,6 +238,54 @@ export class SelfUpdateResult { } } +/** + * SystemInfo 系统信息 + */ +export class SystemInfo { + "os": string; + "arch": string; + "debug": boolean; + "osInfo": OSInfo | null; + "platformInfo": { [_: string]: any }; + + /** Creates a new SystemInfo instance. */ + constructor($$source: Partial = {}) { + if (!("os" in $$source)) { + this["os"] = ""; + } + if (!("arch" in $$source)) { + this["arch"] = ""; + } + if (!("debug" in $$source)) { + this["debug"] = false; + } + if (!("osInfo" in $$source)) { + this["osInfo"] = null; + } + if (!("platformInfo" in $$source)) { + this["platformInfo"] = {}; + } + + Object.assign(this, $$source); + } + + /** + * Creates a new SystemInfo instance from a string or object. + */ + static createFrom($$source: any = {}): SystemInfo { + const $$createField3_0 = $$createType1; + const $$createField4_0 = $$createType2; + let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; + if ("osInfo" in $$parsedSource) { + $$parsedSource["osInfo"] = $$createField3_0($$parsedSource["osInfo"]); + } + if ("platformInfo" in $$parsedSource) { + $$parsedSource["platformInfo"] = $$createField4_0($$parsedSource["platformInfo"]); + } + return new SystemInfo($$parsedSource as Partial); + } +} + /** * WindowInfo 窗口信息(简化版) */ @@ -229,7 +313,7 @@ export class WindowInfo { * Creates a new WindowInfo instance from a string or object. */ static createFrom($$source: any = {}): WindowInfo { - const $$createField0_0 = $$createType1; + const $$createField0_0 = $$createType4; let $$parsedSource = typeof $$source === 'string' ? JSON.parse($$source) : $$source; if ("Window" in $$parsedSource) { $$parsedSource["Window"] = $$createField0_0($$parsedSource["Window"]); @@ -259,5 +343,8 @@ export class WindowSnapService { } // Private type creation functions -const $$createType0 = application$0.WebviewWindow.createFrom; +const $$createType0 = OSInfo.createFrom; const $$createType1 = $Create.Nullable($$createType0); +const $$createType2 = $Create.Map($Create.Any, $Create.Any); +const $$createType3 = application$0.WebviewWindow.createFrom; +const $$createType4 = $Create.Nullable($$createType3); diff --git a/frontend/bindings/voidraft/internal/services/systemservice.ts b/frontend/bindings/voidraft/internal/services/systemservice.ts index bface09..15b5e67 100644 --- a/frontend/bindings/voidraft/internal/services/systemservice.ts +++ b/frontend/bindings/voidraft/internal/services/systemservice.ts @@ -10,6 +10,10 @@ // @ts-ignore: Unused imports import {Call as $Call, Create as $Create} from "@wailsio/runtime"; +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore: Unused imports +import * as application$0 from "../../../github.com/wailsapp/wails/v3/pkg/application/models.js"; + // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore: Unused imports import * as $models from "./models.js"; @@ -34,6 +38,26 @@ export function GetMemoryStats(): Promise<$models.MemoryStats> & { cancel(): voi return $typingPromise; } +/** + * GetSystemInfo 获取系统环境信息 + */ +export function GetSystemInfo(): Promise<$models.SystemInfo | null> & { cancel(): void } { + let $resultPromise = $Call.ByID(2629436820) as any; + let $typingPromise = $resultPromise.then(($result: any) => { + return $$createType2($result); + }) as any; + $typingPromise.cancel = $resultPromise.cancel.bind($resultPromise); + return $typingPromise; +} + +/** + * SetAppReferences 设置应用引用 + */ +export function SetAppReferences(app: application$0.App | null): Promise & { cancel(): void } { + let $resultPromise = $Call.ByID(3873053414, app) as any; + return $resultPromise; +} + /** * TriggerGC 手动触发垃圾回收 */ @@ -44,3 +68,5 @@ export function TriggerGC(): Promise & { cancel(): void } { // Private type creation functions const $$createType0 = $models.MemoryStats.createFrom; +const $$createType1 = $models.SystemInfo.createFrom; +const $$createType2 = $Create.Nullable($$createType1); diff --git a/frontend/src/common/async-operation/index.ts b/frontend/src/common/async-operation/index.ts deleted file mode 100644 index a565f44..0000000 --- a/frontend/src/common/async-operation/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { AsyncOperationManager } from './manager'; -export * from './types'; -export { AsyncOperationManager as default } from './manager'; \ No newline at end of file diff --git a/frontend/src/common/async-operation/manager.ts b/frontend/src/common/async-operation/manager.ts deleted file mode 100644 index ff7c651..0000000 --- a/frontend/src/common/async-operation/manager.ts +++ /dev/null @@ -1,296 +0,0 @@ -import { - OperationStatus, - OperationInfo, - AsyncOperationManagerConfig, - OperationCallbacks, - OperationExecutor, - OperationResult -} from './types'; - -/** - * 异步操作管理器 - * 用于控制异步操作的竞态条件,确保操作的正确性和一致性 - */ -export class AsyncOperationManager { - private operationSequence = 0; - private pendingOperations = new Map(); - private currentResourceOperation = new Map(); - private config: Required; - private callbacks: OperationCallbacks; - - constructor( - config: AsyncOperationManagerConfig = {}, - callbacks: OperationCallbacks = {} - ) { - this.config = { - timeout: 0, - autoCleanup: true, - maxConcurrent: 0, - debug: false, - ...config - }; - this.callbacks = callbacks; - } - - /** - * 生成新的操作ID - */ - private getNextOperationId(): number { - return ++this.operationSequence; - } - - /** - * 记录日志(调试模式下) - */ - private log(message: string, ...args: any[]): void { - if (this.config.debug) { - console.log(`[AsyncOperationManager] ${message}`, ...args); - } - } - - /** - * 创建操作信息 - */ - private createOperation( - resourceId: string | number, - type?: string - ): OperationInfo { - const operation: OperationInfo = { - id: this.getNextOperationId(), - resourceId, - status: OperationStatus.PENDING, - abortController: new AbortController(), - createdAt: Date.now(), - type - }; - - this.log(`Created operation ${operation.id} for resource ${resourceId}`, operation); - return operation; - } - - /** - * 清理已完成的操作 - */ - private cleanupCompletedOperations(): void { - if (!this.config.autoCleanup) return; - - const completedStatuses = [ - OperationStatus.COMPLETED, - OperationStatus.CANCELLED, - OperationStatus.FAILED - ]; - - for (const [id, operation] of this.pendingOperations.entries()) { - if (completedStatuses.includes(operation.status)) { - this.pendingOperations.delete(id); - this.log(`Cleaned up operation ${id}`); - } - } - } - - /** - * 取消指定资源的所有操作(除了指定的操作ID) - */ - public cancelResourceOperations( - resourceId: string | number, - excludeOperationId?: number - ): void { - this.log(`Cancelling operations for resource ${resourceId}, exclude: ${excludeOperationId}`); - - for (const [id, operation] of this.pendingOperations.entries()) { - if ( - operation.resourceId === resourceId && - id !== excludeOperationId && - operation.status === OperationStatus.RUNNING - ) { - this.cancelOperation(id); - } - } - } - - /** - * 取消指定操作 - */ - public cancelOperation(operationId: number): boolean { - const operation = this.pendingOperations.get(operationId); - if (!operation) return false; - - if (operation.status === OperationStatus.RUNNING) { - operation.abortController.abort(); - operation.status = OperationStatus.CANCELLED; - this.log(`Cancelled operation ${operationId}`); - - this.callbacks.onCancel?.(operation); - this.cleanupCompletedOperations(); - return true; - } - - return false; - } - - /** - * 取消所有操作 - */ - public cancelAllOperations(): void { - this.log('Cancelling all operations'); - - for (const [id] of this.pendingOperations.entries()) { - this.cancelOperation(id); - } - - this.currentResourceOperation.clear(); - } - - /** - * 检查操作是否仍然有效 - */ - public isOperationValid( - operationId: number, - resourceId?: string | number - ): boolean { - const operation = this.pendingOperations.get(operationId); - - if (!operation) return false; - if (operation.abortController.signal.aborted) return false; - if (operation.status !== OperationStatus.RUNNING) return false; - - // 如果指定了资源ID,检查是否为当前资源的活跃操作 - if (resourceId !== undefined) { - const currentOperationId = this.currentResourceOperation.get(resourceId); - if (currentOperationId !== operationId) return false; - } - - return true; - } - - /** - * 执行异步操作 - */ - public async executeOperation( - resourceId: string | number, - executor: OperationExecutor, - operationType?: string - ): Promise> { - // 检查并发限制 - if (this.config.maxConcurrent > 0) { - const runningCount = Array.from(this.pendingOperations.values()) - .filter(op => op.status === OperationStatus.RUNNING).length; - - if (runningCount >= this.config.maxConcurrent) { - throw new Error(`Maximum concurrent operations limit reached: ${this.config.maxConcurrent}`); - } - } - - const operation = this.createOperation(resourceId, operationType); - - try { - // 取消同一资源的其他操作 - this.cancelResourceOperations(resourceId, operation.id); - - // 设置当前资源的活跃操作 - this.currentResourceOperation.set(resourceId, operation.id); - - // 添加到待处理操作列表 - this.pendingOperations.set(operation.id, operation); - - // 设置超时 - if (this.config.timeout > 0) { - setTimeout(() => { - if (this.isOperationValid(operation.id)) { - this.cancelOperation(operation.id); - } - }, this.config.timeout); - } - - // 更新状态为运行中 - operation.status = OperationStatus.RUNNING; - this.callbacks.onStart?.(operation); - this.log(`Started operation ${operation.id} for resource ${resourceId}`); - - // 执行操作 - const result = await executor(operation.abortController.signal, operation.id); - - // 检查操作是否仍然有效 - if (!this.isOperationValid(operation.id, resourceId)) { - throw new Error('Operation was cancelled'); - } - - // 操作成功完成 - operation.status = OperationStatus.COMPLETED; - this.callbacks.onComplete?.(operation); - this.log(`Completed operation ${operation.id}`); - - this.cleanupCompletedOperations(); - - return { - success: true, - data: result, - operation - }; - - } catch (error) { - const err = error instanceof Error ? error : new Error(String(error)); - - if (operation.abortController.signal.aborted) { - operation.status = OperationStatus.CANCELLED; - this.log(`Operation ${operation.id} was cancelled`); - } else { - operation.status = OperationStatus.FAILED; - this.callbacks.onError?.(operation, err); - this.log(`Operation ${operation.id} failed:`, err.message); - } - - this.cleanupCompletedOperations(); - - return { - success: false, - error: err, - operation - }; - } finally { - // 清理当前资源操作记录 - if (this.currentResourceOperation.get(resourceId) === operation.id) { - this.currentResourceOperation.delete(resourceId); - } - } - } - - /** - * 获取操作信息 - */ - public getOperation(operationId: number): OperationInfo | undefined { - return this.pendingOperations.get(operationId); - } - - /** - * 获取资源的当前操作ID - */ - public getCurrentOperationId(resourceId: string | number): number | undefined { - return this.currentResourceOperation.get(resourceId); - } - - /** - * 获取所有待处理操作 - */ - public getPendingOperations(): OperationInfo[] { - return Array.from(this.pendingOperations.values()); - } - - /** - * 获取运行中的操作数量 - */ - public getRunningOperationsCount(): number { - return Array.from(this.pendingOperations.values()) - .filter(op => op.status === OperationStatus.RUNNING).length; - } - - /** - * 销毁管理器,取消所有操作 - */ - public destroy(): void { - this.log('Destroying AsyncOperationManager'); - this.cancelAllOperations(); - this.pendingOperations.clear(); - this.currentResourceOperation.clear(); - } -} \ No newline at end of file diff --git a/frontend/src/common/async-operation/types.ts b/frontend/src/common/async-operation/types.ts deleted file mode 100644 index 4c3243e..0000000 --- a/frontend/src/common/async-operation/types.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * 异步操作竞态条件控制相关类型定义 - */ - -/** - * 操作状态枚举 - */ -export enum OperationStatus { - PENDING = 'pending', - RUNNING = 'running', - COMPLETED = 'completed', - CANCELLED = 'cancelled', - FAILED = 'failed' -} - -/** - * 操作信息接口 - */ -export interface OperationInfo { - /** 操作ID */ - id: number; - /** 资源ID(如文档ID、用户ID等) */ - resourceId: string | number; - /** 操作状态 */ - status: OperationStatus; - /** 取消控制器 */ - abortController: AbortController; - /** 创建时间 */ - createdAt: number; - /** 操作类型(可选,用于调试) */ - type?: string; -} - -/** - * 操作管理器配置 - */ -export interface AsyncOperationManagerConfig { - /** 操作超时时间(毫秒),0表示不超时 */ - timeout?: number; - /** 是否自动清理已完成的操作 */ - autoCleanup?: boolean; - /** 最大并发操作数,0表示无限制 */ - maxConcurrent?: number; - /** 调试模式 */ - debug?: boolean; -} - -/** - * 操作回调函数类型 - */ -export interface OperationCallbacks { - /** 操作开始回调 */ - onStart?: (operation: OperationInfo) => void; - /** 操作完成回调 */ - onComplete?: (operation: OperationInfo) => void; - /** 操作取消回调 */ - onCancel?: (operation: OperationInfo) => void; - /** 操作失败回调 */ - onError?: (operation: OperationInfo, error: Error) => void; -} - -/** - * 操作执行器函数类型 - */ -export type OperationExecutor = ( - signal: AbortSignal, - operationId: number -) => Promise; - -/** - * 操作结果类型 - */ -export interface OperationResult { - /** 操作是否成功 */ - success: boolean; - /** 操作结果数据 */ - data?: T; - /** 错误信息 */ - error?: Error; - /** 操作信息 */ - operation: OperationInfo; -} \ No newline at end of file diff --git a/frontend/src/common/async/README.md b/frontend/src/common/async/README.md new file mode 100644 index 0000000..1937e45 --- /dev/null +++ b/frontend/src/common/async/README.md @@ -0,0 +1,285 @@ +# 异步操作管理器 (AsyncOperationManager) + +一个用于控制异步操作竞态条件的 TypeScript 模块,确保操作的正确性和一致性。 + +## 功能特性 + +- 🚀 **竞态条件控制**: 自动取消同一资源的过时操作 +- 🔄 **操作生命周期管理**: 完整的状态跟踪和回调支持 +- 🎯 **资源隔离**: 基于资源ID的操作隔离机制 +- ⚡ **并发控制**: 支持最大并发数限制 +- ⏰ **超时处理**: 可配置的操作超时机制 +- 🧹 **内存管理**: 自动清理已完成的操作 +- 🐛 **调试友好**: 内置日志系统 + +## 安装和导入 + +```typescript +import { AsyncOperationManager } from '@/common/async'; +// 或者 +import AsyncOperationManager from '@/common/async'; +``` + +## 基本用法 + +### 创建管理器实例 + +```typescript +const operationManager = new AsyncOperationManager({ + timeout: 5000, // 5秒超时 + autoCleanup: true, // 自动清理已完成操作 + maxConcurrent: 3, // 最大并发数 + debug: true // 启用调试日志 +}); +``` + +### 执行异步操作 + +```typescript +const result = await operationManager.executeOperation( + 'document-123', // 资源ID + async (signal, operationId) => { + // 检查操作是否被取消 + if (signal.aborted) { + throw new Error('Operation cancelled'); + } + + // 执行异步操作 + const data = await fetchData(); + + // 再次检查取消状态 + if (signal.aborted) { + throw new Error('Operation cancelled'); + } + + return data; + }, + 'fetch-data' // 操作类型(可选,用于调试) +); + +if (result.success) { + console.log('操作成功:', result.data); +} else { + console.error('操作失败:', result.error); +} +``` + +## API 文档 + +### 构造函数 + +```typescript +new AsyncOperationManager(config?, callbacks?) +``` + +#### 配置选项 (AsyncOperationManagerConfig) + +| 参数 | 类型 | 默认值 | 描述 | +|------|------|--------|------| +| `timeout` | `number` | `0` | 操作超时时间(毫秒),0表示不超时 | +| `autoCleanup` | `boolean` | `true` | 是否自动清理已完成的操作 | +| `maxConcurrent` | `number` | `0` | 最大并发操作数,0表示无限制 | +| `debug` | `boolean` | `false` | 是否启用调试模式 | + +#### 回调函数 (OperationCallbacks) + +```typescript +{ + onStart?: (operation: OperationInfo) => void; // 操作开始 + onComplete?: (operation: OperationInfo) => void; // 操作完成 + onCancel?: (operation: OperationInfo) => void; // 操作取消 + onError?: (operation: OperationInfo, error: Error) => void; // 操作失败 +} +``` + +### 主要方法 + +#### executeOperation(resourceId, executor, operationType?) + +执行异步操作的核心方法。 + +**参数:** +- `resourceId: string | number` - 资源标识符 +- `executor: OperationExecutor` - 操作执行函数 +- `operationType?: string` - 操作类型(可选) + +**返回:** `Promise>` + +#### cancelResourceOperations(resourceId, excludeOperationId?) + +取消指定资源的所有操作。 + +#### cancelOperation(operationId) + +取消指定的操作。 + +#### cancelAllOperations() + +取消所有正在进行的操作。 + +#### isOperationValid(operationId, resourceId?) + +检查操作是否仍然有效。 + +### 查询方法 + +- `getOperation(operationId)` - 获取操作信息 +- `getCurrentOperationId(resourceId)` - 获取资源的当前操作ID +- `getPendingOperations()` - 获取所有待处理操作 +- `getRunningOperationsCount()` - 获取运行中的操作数量 + +## 使用场景 + +### 1. 编辑器文档切换 + +```typescript +// 防止快速切换文档时的内容混乱 +const loadDocument = async (documentId: number) => { + const result = await operationManager.executeOperation( + documentId, + async (signal) => { + // 保存当前文档 + await saveCurrentDocument(); + + if (signal.aborted) return; + + // 加载新文档 + const content = await loadDocumentContent(documentId); + + if (signal.aborted) return; + + return content; + }, + 'load-document' + ); + + if (result.success) { + updateEditor(result.data); + } +}; +``` + +### 2. 搜索功能 + +```typescript +// 取消过时的搜索请求 +const search = async (query: string) => { + const result = await operationManager.executeOperation( + 'search', + async (signal) => { + const results = await searchAPI(query, { signal }); + return results; + }, + 'search-query' + ); + + if (result.success) { + displaySearchResults(result.data); + } +}; +``` + +### 3. 数据加载 + +```typescript +// 避免页面切换时的数据竞态 +const loadPageData = async (pageId: string) => { + const result = await operationManager.executeOperation( + `page-${pageId}`, + async (signal) => { + const [userData, pageContent, settings] = await Promise.all([ + fetchUserData(signal), + fetchPageContent(pageId, signal), + fetchSettings(signal) + ]); + + return { userData, pageContent, settings }; + }, + 'load-page' + ); + + if (result.success) { + renderPage(result.data); + } +}; +``` + +## 操作状态 + +操作在生命周期中会经历以下状态: + +- `PENDING` - 待处理 +- `RUNNING` - 运行中 +- `COMPLETED` - 已完成 +- `CANCELLED` - 已取消 +- `FAILED` - 失败 + +## 最佳实践 + +### 1. 合理使用资源ID + +```typescript +// ✅ 好的做法:使用具体的资源标识 +operationManager.executeOperation('document-123', executor); +operationManager.executeOperation('user-456', executor); + +// ❌ 避免:使用过于宽泛的标识 +operationManager.executeOperation('global', executor); +``` + +### 2. 及时检查取消状态 + +```typescript +// ✅ 在关键点检查取消状态 +const executor = async (signal) => { + const step1 = await longRunningTask1(); + if (signal.aborted) throw new Error('Cancelled'); + + const step2 = await longRunningTask2(); + if (signal.aborted) throw new Error('Cancelled'); + + return processResults(step1, step2); +}; +``` + +### 3. 使用有意义的操作类型 + +```typescript +// ✅ 使用描述性的操作类型 +operationManager.executeOperation(docId, executor, 'auto-save'); +operationManager.executeOperation(docId, executor, 'manual-save'); +operationManager.executeOperation(docId, executor, 'export-pdf'); +``` + +### 4. 适当的错误处理 + +```typescript +const result = await operationManager.executeOperation(resourceId, executor); + +if (!result.success) { + if (result.operation.status === OperationStatus.CANCELLED) { + // 操作被取消,通常不需要显示错误 + console.log('Operation was cancelled'); + } else { + // 真正的错误,需要处理 + console.error('Operation failed:', result.error); + showErrorMessage(result.error.message); + } +} +``` + +## 注意事项 + +1. **内存管理**: 启用 `autoCleanup` 以防止内存泄漏 +2. **并发控制**: 根据应用需求设置合适的 `maxConcurrent` 值 +3. **超时设置**: 为长时间运行的操作设置合理的超时时间 +4. **错误处理**: 区分取消和真正的错误,提供适当的用户反馈 +5. **调试模式**: 在开发环境启用 `debug` 模式以便排查问题 + +## 类型定义 + +完整的类型定义请参考 `types.ts` 文件。 + +## 许可证 + +本模块遵循项目的整体许可证。 \ No newline at end of file diff --git a/frontend/src/common/async/index.ts b/frontend/src/common/async/index.ts new file mode 100644 index 0000000..544a80e --- /dev/null +++ b/frontend/src/common/async/index.ts @@ -0,0 +1,78 @@ +/** + * 异步操作管理模块 + * + * 该模块提供了用于管理异步操作竞态条件的完整解决方案。 + * 主要用于防止同一资源上的并发操作冲突,确保操作的正确性和一致性。 + * + * @fileoverview 异步操作管理模块入口文件 + * @author VoidRaft Team + * @since 1.0.0 + * + * @example + * ```typescript + * import { AsyncOperationManager, OperationStatus } from '@/common/async'; + * + * // 创建管理器实例 + * const manager = new AsyncOperationManager({ + * timeout: 30000, + * maxConcurrent: 5, + * debug: true + * }); + * + * // 执行异步操作 + * const result = await manager.executeOperation( + * 'document-123', + * async (signal, operationId) => { + * if (signal.aborted) throw new Error('Cancelled'); + * return await saveDocument(data); + * } + * ); + * + * if (result.success) { + * console.log('Operation completed:', result.data); + * } + * ``` + */ + +/** + * 导出异步操作管理器类 + * + * AsyncOperationManager 是该模块的核心类,提供了完整的异步操作管理功能。 + * + * @see {@link AsyncOperationManager} 异步操作管理器类的详细文档 + */ +export { AsyncOperationManager } from './manager'; + +/** + * 导出所有类型定义 + * + * 包括操作状态枚举、接口定义、配置选项等所有相关类型。 + * 这些类型为使用异步操作管理器提供了完整的 TypeScript 类型支持。 + * + * 导出的类型包括: + * - OperationStatus: 操作状态枚举 + * - OperationInfo: 操作信息接口 + * - AsyncOperationManagerConfig: 管理器配置接口 + * - OperationCallbacks: 操作回调函数接口 + * - OperationExecutor: 操作执行器函数类型 + * - OperationResult: 操作结果接口 + * + * @see {@link ./types} 类型定义文件 + */ +export * from './types'; + +/** + * 默认导出异步操作管理器类 + * + * 提供默认导出,方便使用 `import AsyncOperationManager from '@/common/async'` 的方式导入。 + * + * @default AsyncOperationManager + * + * @example + * ```typescript + * import AsyncOperationManager from '@/common/async'; + * + * const manager = new AsyncOperationManager(); + * ``` + */ +export { AsyncOperationManager as default } from './manager'; \ No newline at end of file diff --git a/frontend/src/common/async/manager.ts b/frontend/src/common/async/manager.ts new file mode 100644 index 0000000..8bbb3a6 --- /dev/null +++ b/frontend/src/common/async/manager.ts @@ -0,0 +1,574 @@ +import { + OperationStatus, + OperationInfo, + AsyncOperationManagerConfig, + OperationCallbacks, + OperationExecutor, + OperationResult +} from './types'; + +/** + * 异步操作管理器 + * + * 用于控制异步操作的竞态条件,确保操作的正确性和一致性。 + * 该管理器提供了操作的生命周期管理、并发控制、超时处理等功能。 + * + * @class AsyncOperationManager + * @author VoidRaft Team + * @since 1.0.0 + * + * @example + * ```typescript + * import { AsyncOperationManager } from './manager'; + * + * const manager = new AsyncOperationManager({ + * timeout: 30000, + * maxConcurrent: 5, + * debug: true + * }); + * + * // 执行异步操作 + * const result = await manager.executeOperation( + * 'document-123', + * async (signal, operationId) => { + * // 执行实际的异步操作 + * return await saveDocument(documentData); + * }, + * 'save-document' + * ); + * ``` + */ +export class AsyncOperationManager { + /** + * 操作序列号生成器 + * @private + * @type {number} + */ + private operationSequence = 0; + + /** + * 待处理操作映射表 + * @private + * @type {Map} + */ + private pendingOperations = new Map(); + + /** + * 当前资源操作映射表 + * 记录每个资源当前正在执行的操作ID + * @private + * @type {Map} + */ + private currentResourceOperation = new Map(); + + /** + * 管理器配置 + * @private + * @type {Required} + */ + private config: Required; + + /** + * 操作回调函数集合 + * @private + * @type {OperationCallbacks} + */ + private callbacks: OperationCallbacks; + + /** + * 创建异步操作管理器实例 + * + * @param {AsyncOperationManagerConfig} config - 管理器配置选项 + * @param {OperationCallbacks} callbacks - 操作生命周期回调函数 + * + * @example + * ```typescript + * const manager = new AsyncOperationManager( + * { + * timeout: 30000, + * autoCleanup: true, + * maxConcurrent: 10, + * debug: false + * }, + * { + * onStart: (op) => console.log(`Operation ${op.id} started`), + * onComplete: (op) => console.log(`Operation ${op.id} completed`), + * onError: (op, err) => console.error(`Operation ${op.id} failed:`, err) + * } + * ); + * ``` + */ + constructor( + config: AsyncOperationManagerConfig = {}, + callbacks: OperationCallbacks = {} + ) { + this.config = { + timeout: 0, + autoCleanup: true, + maxConcurrent: 0, + debug: false, + ...config + }; + this.callbacks = callbacks; + } + + /** + * 生成新的操作ID + * + * @private + * @returns {number} 新的操作ID + */ + private getNextOperationId(): number { + return ++this.operationSequence; + } + + /** + * 记录调试日志 + * + * 仅在调试模式下输出日志信息。 + * + * @private + * @param {string} message - 日志消息 + * @param {...any} args - 额外的日志参数 + */ + private log(message: string, ...args: any[]): void { + if (this.config.debug) { + console.log(`[AsyncOperationManager] ${message}`, ...args); + } + } + + /** + * 创建操作信息对象 + * + * @private + * @param {string | number} resourceId - 资源ID + * @param {string} [type] - 操作类型标识 + * @returns {OperationInfo} 新创建的操作信息 + */ + private createOperation( + resourceId: string | number, + type?: string + ): OperationInfo { + const operation: OperationInfo = { + id: this.getNextOperationId(), + resourceId, + status: OperationStatus.PENDING, + abortController: new AbortController(), + createdAt: Date.now(), + type + }; + + this.log(`Created operation ${operation.id} for resource ${resourceId}`, operation); + return operation; + } + + /** + * 清理已完成的操作 + * + * 自动从内存中移除已完成、已取消或失败的操作,释放内存资源。 + * 仅在启用自动清理配置时执行。 + * + * @private + */ + private cleanupCompletedOperations(): void { + if (!this.config.autoCleanup) return; + + const completedStatuses = [ + OperationStatus.COMPLETED, + OperationStatus.CANCELLED, + OperationStatus.FAILED + ]; + + for (const [id, operation] of this.pendingOperations.entries()) { + if (completedStatuses.includes(operation.status)) { + this.pendingOperations.delete(id); + this.log(`Cleaned up operation ${id}`); + } + } + } + + /** + * 取消指定资源的所有操作 + * + * 取消指定资源上正在运行的所有操作,可以排除指定的操作ID。 + * 这对于防止竞态条件非常有用,确保同一资源上只有最新的操作在执行。 + * + * @public + * @param {string | number} resourceId - 要取消操作的资源ID + * @param {number} [excludeOperationId] - 要排除的操作ID(不会被取消) + * + * @example + * ```typescript + * // 取消文档 'doc-123' 上的所有操作,除了操作 456 + * manager.cancelResourceOperations('doc-123', 456); + * ``` + */ + public cancelResourceOperations( + resourceId: string | number, + excludeOperationId?: number + ): void { + this.log(`Cancelling operations for resource ${resourceId}, exclude: ${excludeOperationId}`); + + for (const [id, operation] of this.pendingOperations.entries()) { + if ( + operation.resourceId === resourceId && + id !== excludeOperationId && + operation.status === OperationStatus.RUNNING + ) { + this.cancelOperation(id); + } + } + } + + /** + * 取消指定的操作 + * + * 通过操作ID取消正在运行的操作。只有状态为 RUNNING 的操作才能被取消。 + * + * @public + * @param {number} operationId - 要取消的操作ID + * @returns {boolean} 是否成功取消操作 + * + * @example + * ```typescript + * const cancelled = manager.cancelOperation(123); + * if (cancelled) { + * console.log('Operation 123 was cancelled'); + * } else { + * console.log('Operation 123 could not be cancelled (not found or not running)'); + * } + * ``` + */ + public cancelOperation(operationId: number): boolean { + const operation = this.pendingOperations.get(operationId); + if (!operation) return false; + + if (operation.status === OperationStatus.RUNNING) { + operation.abortController.abort(); + operation.status = OperationStatus.CANCELLED; + this.log(`Cancelled operation ${operationId}`); + + this.callbacks.onCancel?.(operation); + this.cleanupCompletedOperations(); + return true; + } + + return false; + } + + /** + * 取消所有操作 + * + * 取消管理器中所有正在运行的操作,并清空资源操作映射表。 + * + * @public + * + * @example + * ```typescript + * // 在应用关闭或重置时取消所有操作 + * manager.cancelAllOperations(); + * ``` + */ + public cancelAllOperations(): void { + this.log('Cancelling all operations'); + + for (const [id] of this.pendingOperations.entries()) { + this.cancelOperation(id); + } + + this.currentResourceOperation.clear(); + } + + /** + * 检查操作是否仍然有效 + * + * 验证操作是否存在、未被取消、状态为运行中,以及(如果指定了资源ID) + * 是否为该资源的当前活跃操作。 + * + * @public + * @param {number} operationId - 要检查的操作ID + * @param {string | number} [resourceId] - 可选的资源ID,用于验证是否为当前活跃操作 + * @returns {boolean} 操作是否有效 + * + * @example + * ```typescript + * // 在长时间运行的操作中定期检查有效性 + * const saveDocument = async (signal: AbortSignal, operationId: number) => { + * for (let i = 0; i < 100; i++) { + * if (!manager.isOperationValid(operationId, 'doc-123')) { + * throw new Error('Operation is no longer valid'); + * } + * await processChunk(i); + * } + * }; + * ``` + */ + public isOperationValid( + operationId: number, + resourceId?: string | number + ): boolean { + const operation = this.pendingOperations.get(operationId); + + if (!operation) return false; + if (operation.abortController.signal.aborted) return false; + if (operation.status !== OperationStatus.RUNNING) return false; + + // 如果指定了资源ID,检查是否为当前资源的活跃操作 + if (resourceId !== undefined) { + const currentOperationId = this.currentResourceOperation.get(resourceId); + if (currentOperationId !== operationId) return false; + } + + return true; + } + + /** + * 执行异步操作 + * + * 这是管理器的核心方法,用于执行异步操作并管理其生命周期。 + * 该方法会自动处理并发控制、竞态条件、超时管理等复杂逻辑。 + * + * @public + * @template T - 操作返回值的类型 + * @param {string | number} resourceId - 操作关联的资源ID + * @param {OperationExecutor} executor - 实际执行操作的函数 + * @param {string} [operationType] - 操作类型标识,用于调试和日志 + * @returns {Promise>} 操作执行结果 + * + * @throws {Error} 当达到最大并发限制时抛出错误 + * + * @example + * ```typescript + * // 执行文档保存操作 + * const result = await manager.executeOperation( + * 'document-123', + * async (signal, operationId) => { + * // 检查操作是否被取消 + * if (signal.aborted) { + * throw new Error('Operation was cancelled'); + * } + * + * // 执行实际的保存逻辑 + * const saved = await api.saveDocument(documentData); + * return saved; + * }, + * 'save-document' + * ); + * + * if (result.success) { + * console.log('Document saved successfully:', result.data); + * } else { + * console.error('Failed to save document:', result.error); + * } + * ``` + */ + public async executeOperation( + resourceId: string | number, + executor: OperationExecutor, + operationType?: string + ): Promise> { + // 检查并发限制 + if (this.config.maxConcurrent > 0) { + const runningCount = Array.from(this.pendingOperations.values()) + .filter(op => op.status === OperationStatus.RUNNING).length; + + if (runningCount >= this.config.maxConcurrent) { + throw new Error(`Maximum concurrent operations limit reached: ${this.config.maxConcurrent}`); + } + } + + const operation = this.createOperation(resourceId, operationType); + + try { + // 取消同一资源的其他操作 + this.cancelResourceOperations(resourceId, operation.id); + + // 设置当前资源的活跃操作 + this.currentResourceOperation.set(resourceId, operation.id); + + // 添加到待处理操作列表 + this.pendingOperations.set(operation.id, operation); + + // 设置超时 + if (this.config.timeout > 0) { + setTimeout(() => { + if (this.isOperationValid(operation.id)) { + this.cancelOperation(operation.id); + } + }, this.config.timeout); + } + + // 更新状态为运行中 + operation.status = OperationStatus.RUNNING; + this.callbacks.onStart?.(operation); + this.log(`Started operation ${operation.id} for resource ${resourceId}`); + + // 执行操作 + const result = await executor(operation.abortController.signal, operation.id); + + // 检查操作是否仍然有效 + if (!this.isOperationValid(operation.id, resourceId)) { + throw new Error('Operation was cancelled'); + } + + // 操作成功完成 + operation.status = OperationStatus.COMPLETED; + this.callbacks.onComplete?.(operation); + this.log(`Completed operation ${operation.id}`); + + this.cleanupCompletedOperations(); + + return { + success: true, + data: result, + operation + }; + + } catch (error) { + const err = error instanceof Error ? error : new Error(String(error)); + + if (operation.abortController.signal.aborted) { + operation.status = OperationStatus.CANCELLED; + this.log(`Operation ${operation.id} was cancelled`); + } else { + operation.status = OperationStatus.FAILED; + this.callbacks.onError?.(operation, err); + this.log(`Operation ${operation.id} failed:`, err.message); + } + + this.cleanupCompletedOperations(); + + return { + success: false, + error: err, + operation + }; + } finally { + // 清理当前资源操作记录 + if (this.currentResourceOperation.get(resourceId) === operation.id) { + this.currentResourceOperation.delete(resourceId); + } + } + } + + /** + * 获取操作信息 + * + * 根据操作ID获取操作的详细信息。 + * + * @public + * @param {number} operationId - 操作ID + * @returns {OperationInfo | undefined} 操作信息,如果操作不存在则返回 undefined + * + * @example + * ```typescript + * const operation = manager.getOperation(123); + * if (operation) { + * console.log(`Operation ${operation.id} status: ${operation.status}`); + * } else { + * console.log('Operation not found'); + * } + * ``` + */ + public getOperation(operationId: number): OperationInfo | undefined { + return this.pendingOperations.get(operationId); + } + + /** + * 获取资源的当前操作ID + * + * 获取指定资源当前正在执行的操作ID。 + * + * @public + * @param {string | number} resourceId - 资源ID + * @returns {number | undefined} 当前操作ID,如果没有正在执行的操作则返回 undefined + * + * @example + * ```typescript + * const currentOpId = manager.getCurrentOperationId('document-123'); + * if (currentOpId) { + * console.log(`Document 123 is currently being processed by operation ${currentOpId}`); + * } else { + * console.log('No active operation for document 123'); + * } + * ``` + */ + public getCurrentOperationId(resourceId: string | number): number | undefined { + return this.currentResourceOperation.get(resourceId); + } + + /** + * 获取所有待处理操作 + * + * 返回管理器中所有待处理操作的列表,包括待执行、正在执行和已完成的操作。 + * + * @public + * @returns {OperationInfo[]} 所有待处理操作的数组 + * + * @example + * ```typescript + * const allOperations = manager.getPendingOperations(); + * console.log(`Total operations: ${allOperations.length}`); + * + * allOperations.forEach(op => { + * console.log(`Operation ${op.id}: ${op.status} (${op.type || 'unknown'})`); + * }); + * ``` + */ + public getPendingOperations(): OperationInfo[] { + return Array.from(this.pendingOperations.values()); + } + + /** + * 获取运行中的操作数量 + * + * 返回当前正在执行的操作数量,用于监控并发情况。 + * + * @public + * @returns {number} 正在运行的操作数量 + * + * @example + * ```typescript + * const runningCount = manager.getRunningOperationsCount(); + * console.log(`Currently running operations: ${runningCount}`); + * + * if (runningCount >= maxConcurrent) { + * console.warn('Approaching maximum concurrent operations limit'); + * } + * ``` + */ + public getRunningOperationsCount(): number { + return Array.from(this.pendingOperations.values()) + .filter(op => op.status === OperationStatus.RUNNING).length; + } + + /** + * 销毁管理器 + * + * 清理管理器的所有资源,取消所有正在运行的操作,清空所有映射表。 + * 通常在应用关闭或组件卸载时调用。 + * + * @public + * + * @example + * ```typescript + * // 在组件卸载时清理资源 + * useEffect(() => { + * return () => { + * manager.destroy(); + * }; + * }, []); + * + * // 或在应用关闭时 + * window.addEventListener('beforeunload', () => { + * manager.destroy(); + * }); + * ``` + */ + public destroy(): void { + this.log('Destroying AsyncOperationManager'); + this.cancelAllOperations(); + this.pendingOperations.clear(); + this.currentResourceOperation.clear(); + } +} \ No newline at end of file diff --git a/frontend/src/common/async/types.ts b/frontend/src/common/async/types.ts new file mode 100644 index 0000000..6f50135 --- /dev/null +++ b/frontend/src/common/async/types.ts @@ -0,0 +1,289 @@ +/** + * 异步操作竞态条件控制相关类型定义 + * + * 该模块提供了用于管理异步操作竞态条件的类型定义, + * 包括操作状态、配置选项、回调函数等核心类型。 + * + * @fileoverview 异步操作管理器类型定义 + * @author VoidRaft Team + * @since 1.0.0 + */ + +/** + * 操作状态枚举 + * + * 定义异步操作在其生命周期中可能的状态。 + * + * @enum {string} + * @readonly + * + * @example + * ```typescript + * import { OperationStatus } from './types'; + * + * const status = OperationStatus.RUNNING; + * console.log(status); // 'running' + * ``` + */ +export enum OperationStatus { + /** 操作已创建但尚未开始执行 */ + PENDING = 'pending', + /** 操作正在执行中 */ + RUNNING = 'running', + /** 操作已成功完成 */ + COMPLETED = 'completed', + /** 操作已被取消 */ + CANCELLED = 'cancelled', + /** 操作执行失败 */ + FAILED = 'failed' +} + +/** + * 操作信息接口 + * + * 描述单个异步操作的完整信息,包括标识符、状态、控制器等。 + * + * @interface OperationInfo + * + * @example + * ```typescript + * const operation: OperationInfo = { + * id: 1, + * resourceId: 'document-123', + * status: OperationStatus.RUNNING, + * abortController: new AbortController(), + * createdAt: Date.now(), + * type: 'save-document' + * }; + * ``` + */ +export interface OperationInfo { + /** + * 操作的唯一标识符 + * @type {number} + */ + id: number; + + /** + * 关联的资源ID(如文档ID、用户ID等) + * 用于标识操作所作用的资源,支持字符串或数字类型 + * @type {string | number} + */ + resourceId: string | number; + + /** + * 当前操作状态 + * @type {OperationStatus} + */ + status: OperationStatus; + + /** + * 用于取消操作的控制器 + * 通过调用 abortController.abort() 可以取消正在执行的操作 + * @type {AbortController} + */ + abortController: AbortController; + + /** + * 操作创建时间戳(毫秒) + * @type {number} + */ + createdAt: number; + + /** + * 操作类型标识(可选) + * 用于调试和日志记录,帮助识别不同类型的操作 + * @type {string} + * @optional + */ + type?: string; +} + +/** + * 异步操作管理器配置接口 + * + * 定义异步操作管理器的配置选项,用于控制操作的行为和限制。 + * + * @interface AsyncOperationManagerConfig + * + * @example + * ```typescript + * const config: AsyncOperationManagerConfig = { + * timeout: 30000, // 30秒超时 + * autoCleanup: true, // 自动清理已完成操作 + * maxConcurrent: 5, // 最多5个并发操作 + * debug: true // 启用调试模式 + * }; + * ``` + */ +export interface AsyncOperationManagerConfig { + /** + * 操作超时时间(毫秒) + * 设置为 0 表示不设置超时限制 + * @type {number} + * @default 0 + * @optional + */ + timeout?: number; + + /** + * 是否自动清理已完成的操作 + * 启用后会自动从内存中移除已完成、已取消或失败的操作 + * @type {boolean} + * @default true + * @optional + */ + autoCleanup?: boolean; + + /** + * 最大并发操作数 + * 设置为 0 表示无并发限制 + * @type {number} + * @default 0 + * @optional + */ + maxConcurrent?: number; + + /** + * 调试模式开关 + * 启用后会在控制台输出详细的操作日志 + * @type {boolean} + * @default false + * @optional + */ + debug?: boolean; +} + +/** + * 操作回调函数集合接口 + * + * 定义在操作生命周期的不同阶段可以执行的回调函数。 + * + * @interface OperationCallbacks + * + * @example + * ```typescript + * const callbacks: OperationCallbacks = { + * onStart: (operation) => console.log(`Operation ${operation.id} started`), + * onComplete: (operation) => console.log(`Operation ${operation.id} completed`), + * onCancel: (operation) => console.log(`Operation ${operation.id} cancelled`), + * onError: (operation, error) => console.error(`Operation ${operation.id} failed:`, error) + * }; + * ``` + */ +export interface OperationCallbacks { + /** + * 操作开始时的回调函数 + * @param {OperationInfo} operation - 操作信息 + * @optional + */ + onStart?: (operation: OperationInfo) => void; + + /** + * 操作成功完成时的回调函数 + * @param {OperationInfo} operation - 操作信息 + * @optional + */ + onComplete?: (operation: OperationInfo) => void; + + /** + * 操作被取消时的回调函数 + * @param {OperationInfo} operation - 操作信息 + * @optional + */ + onCancel?: (operation: OperationInfo) => void; + + /** + * 操作执行失败时的回调函数 + * @param {OperationInfo} operation - 操作信息 + * @param {Error} error - 错误对象 + * @optional + */ + onError?: (operation: OperationInfo, error: Error) => void; +} + +/** + * 操作执行器函数类型 + * + * 定义实际执行异步操作的函数签名。 + * + * @template T - 操作返回值的类型 + * @param {AbortSignal} signal - 用于检测操作是否被取消的信号 + * @param {number} operationId - 操作的唯一标识符 + * @returns {Promise} 操作执行结果的 Promise + * + * @example + * ```typescript + * const saveDocument: OperationExecutor = async (signal, operationId) => { + * // 检查操作是否被取消 + * if (signal.aborted) { + * throw new Error('Operation was cancelled'); + * } + * + * // 执行实际的保存操作 + * const result = await api.saveDocument(documentData); + * return result.success; + * }; + * ``` + */ +export type OperationExecutor = ( + signal: AbortSignal, + operationId: number +) => Promise; + +/** + * 操作执行结果接口 + * + * 封装异步操作的执行结果,包括成功状态、数据和错误信息。 + * + * @template T - 操作结果数据的类型 + * @interface OperationResult + * + * @example + * ```typescript + * // 成功的操作结果 + * const successResult: OperationResult = { + * success: true, + * data: 'Operation completed successfully', + * operation: operationInfo + * }; + * + * // 失败的操作结果 + * const failureResult: OperationResult = { + * success: false, + * error: new Error('Operation failed'), + * operation: operationInfo + * }; + * ``` + */ +export interface OperationResult { + /** + * 操作是否成功执行 + * true 表示操作成功完成,false 表示操作失败或被取消 + * @type {boolean} + */ + success: boolean; + + /** + * 操作成功时的结果数据 + * 仅在 success 为 true 时有值 + * @type {T} + * @optional + */ + data?: T; + + /** + * 操作失败时的错误信息 + * 仅在 success 为 false 时有值 + * @type {Error} + * @optional + */ + error?: Error; + + /** + * 关联的操作信息 + * 包含操作的完整元数据 + * @type {OperationInfo} + */ + operation: OperationInfo; +} \ No newline at end of file diff --git a/frontend/src/common/cache/README.md b/frontend/src/common/cache/README.md index 59c5799..2fa1f5f 100644 --- a/frontend/src/common/cache/README.md +++ b/frontend/src/common/cache/README.md @@ -10,16 +10,18 @@ - 📊 缓存统计信息 - ⏰ TTL 过期支持 - 🎯 简洁易用的 API +- 🔐 多种哈希算法支持 +- 🏗️ 模块化设计,易于扩展 ## 基础用法 ### 创建缓存 ```typescript -import { LRUCache, CacheManager, createCacheItem } from '@/common/cache'; +import { LruCache, CacheManager, createCacheItem } from '@/common/cache'; // 直接创建缓存 -const cache = new LRUCache({ +const cache = new LruCache({ maxSize: 100, ttl: 5 * 60 * 1000, // 5 分钟 onEvict: (item) => console.log('Evicted:', item) @@ -76,14 +78,26 @@ const item: MyItem = { cache.set('resource1', item); ``` +### 哈希工具使用 + +```typescript +import { createHash, generateCacheKey } from '@/common/cache'; + +// 生成简单哈希 +const hash = createHash('some content'); + +// 生成缓存键 +const key = generateCacheKey('user', userId, 'profile'); +``` + ## API 参考 -### LRUCache +### LruCache -- `get(id)` - 获取缓存项 -- `set(id, item)` - 设置缓存项 -- `remove(id)` - 移除缓存项 -- `has(id)` - 检查是否存在 +- `get(id)` - 获取缓存项(O(1)) +- `set(id, item)` - 设置缓存项(O(1)) +- `remove(id)` - 移除缓存项(O(1)) +- `has(id)` - 检查是否存在(O(1)) - `clear()` - 清空缓存 - `size()` - 获取缓存大小 - `getStats()` - 获取统计信息 @@ -96,11 +110,10 @@ cache.set('resource1', item); - `removeCache(name)` - 删除缓存 - `clearAll()` - 清空所有缓存 - `getAllStats()` - 获取所有统计信息 +- `cleanupAll()` - 清理所有缓存的过期项 ## 工具函数 - `generateCacheKey(...parts)` - 生成缓存键 -- `createCacheItem(id, data)` - 创建缓存项 -- `createContentHash(content)` - 创建内容哈希 -- `debounce(func, wait)` - 防抖函数 -- `throttle(func, limit)` - 节流函数 \ No newline at end of file +- `createHash(content)` - 创建内容哈希 +- `createCacheItem(id, data)` - 创建缓存项 \ No newline at end of file diff --git a/frontend/src/common/cache/cache-manager.ts b/frontend/src/common/cache/cache-manager.ts deleted file mode 100644 index ad4f606..0000000 --- a/frontend/src/common/cache/cache-manager.ts +++ /dev/null @@ -1,133 +0,0 @@ -import type { CacheItem, CacheConfig, CacheStats } from './types'; -import { LRUCache } from './lru-cache'; - -export class CacheManager { - private caches = new Map>(); - private cleanupInterval?: number; - - constructor(options?: { - /** 自动清理间隔(毫秒),默认 5 分钟 */ - cleanupInterval?: number; - }) { - if (options?.cleanupInterval) { - this.startAutoCleanup(options.cleanupInterval); - } - } - - /** - * 创建或获取缓存实例 - */ - getCache(name: string, config?: CacheConfig): LRUCache { - if (!this.caches.has(name)) { - if (!config) { - throw new Error(`Cache "${name}" does not exist and no config provided`); - } - this.caches.set(name, new LRUCache(config)); - } - return this.caches.get(name)!; - } - - /** - * 创建新的缓存实例 - */ - createCache(name: string, config: CacheConfig): LRUCache { - if (this.caches.has(name)) { - throw new Error(`Cache "${name}" already exists`); - } - const cache = new LRUCache(config); - this.caches.set(name, cache); - return cache; - } - - /** - * 删除缓存实例 - */ - removeCache(name: string): boolean { - const cache = this.caches.get(name); - if (cache) { - cache.clear(); - this.caches.delete(name); - return true; - } - return false; - } - - /** - * 检查缓存是否存在 - */ - hasCache(name: string): boolean { - return this.caches.has(name); - } - - /** - * 获取所有缓存名称 - */ - getCacheNames(): string[] { - return Array.from(this.caches.keys()); - } - - /** - * 清空所有缓存 - */ - clearAll(): void { - for (const cache of this.caches.values()) { - cache.clear(); - } - } - - /** - * 获取所有缓存的统计信息 - */ - getAllStats(): Record { - const stats: Record = {}; - for (const [name, cache] of this.caches.entries()) { - stats[name] = cache.getStats(); - } - return stats; - } - - /** - * 清理所有缓存中的过期项 - */ - cleanupAll(): Record { - const results: Record = {}; - for (const [name, cache] of this.caches.entries()) { - results[name] = cache.cleanup(); - } - return results; - } - - /** - * 启动自动清理 - */ - startAutoCleanup(interval: number): void { - this.stopAutoCleanup(); - this.cleanupInterval = window.setInterval(() => { - this.cleanupAll(); - }, interval); - } - - /** - * 停止自动清理 - */ - stopAutoCleanup(): void { - if (this.cleanupInterval) { - clearInterval(this.cleanupInterval); - this.cleanupInterval = undefined; - } - } - - /** - * 销毁管理器 - */ - destroy(): void { - this.stopAutoCleanup(); - this.clearAll(); - this.caches.clear(); - } -} - -// 全局缓存管理器实例 -export const globalCacheManager = new CacheManager({ - cleanupInterval: 5 * 60 * 1000 // 5 分钟 -}); \ No newline at end of file diff --git a/frontend/src/common/cache/doublyLinkedList.ts b/frontend/src/common/cache/doublyLinkedList.ts new file mode 100644 index 0000000..3d2478b --- /dev/null +++ b/frontend/src/common/cache/doublyLinkedList.ts @@ -0,0 +1,166 @@ +import type { DoublyLinkedNode } from './interfaces'; + +/** + * 双向链表实现 + * 用于高效管理LRU缓存的访问顺序,所有操作都是O(1)时间复杂度 + * + * @template T 节点值的类型 + */ +export class DoublyLinkedList { + /** 头节点(虚拟节点) */ + private head: DoublyLinkedNode; + /** 尾节点(虚拟节点) */ + private tail: DoublyLinkedNode; + /** 当前节点数量 */ + private count: number = 0; + + /** + * 构造函数 + * 创建头尾虚拟节点,简化边界处理 + */ + constructor() { + // 创建虚拟头节点 + this.head = { + value: null as any, + key: 'head', + prev: null, + next: null + }; + + // 创建虚拟尾节点 + this.tail = { + value: null as any, + key: 'tail', + prev: null, + next: null + }; + + // 连接头尾节点 + this.head.next = this.tail; + this.tail.prev = this.head; + } + + /** + * 在头部添加节点 + * 时间复杂度: O(1) + * + * @param node 要添加的节点 + */ + addToHead(node: DoublyLinkedNode): void { + node.prev = this.head; + node.next = this.head.next; + + if (this.head.next) { + this.head.next.prev = node; + } + this.head.next = node; + + this.count++; + } + + /** + * 移除指定节点 + * 时间复杂度: O(1) + * + * @param node 要移除的节点 + */ + removeNode(node: DoublyLinkedNode): void { + if (node.prev) { + node.prev.next = node.next; + } + if (node.next) { + node.next.prev = node.prev; + } + + this.count--; + } + + /** + * 移除尾部节点 + * 时间复杂度: O(1) + * + * @returns 被移除的节点,如果链表为空则返回null + */ + removeTail(): DoublyLinkedNode | null { + const lastNode = this.tail.prev; + + if (lastNode === this.head) { + return null; // 链表为空 + } + + this.removeNode(lastNode!); + return lastNode!; + } + + /** + * 将节点移动到头部 + * 时间复杂度: O(1) + * + * @param node 要移动的节点 + */ + moveToHead(node: DoublyLinkedNode): void { + this.removeNode(node); + this.addToHead(node); + } + + /** + * 创建新节点 + * + * @param key 节点键 + * @param value 节点值 + * @returns 新创建的节点 + */ + createNode(key: string | number, value: T): DoublyLinkedNode { + return { + key, + value, + prev: null, + next: null + }; + } + + /** + * 获取链表大小 + * + * @returns 当前节点数量 + */ + size(): number { + return this.count; + } + + /** + * 检查链表是否为空 + * + * @returns 是否为空 + */ + isEmpty(): boolean { + return this.count === 0; + } + + /** + * 清空链表 + */ + clear(): void { + this.head.next = this.tail; + this.tail.prev = this.head; + this.count = 0; + } + + /** + * 获取所有节点的值(从头到尾) + * 主要用于调试和测试 + * + * @returns 所有节点值的数组 + */ + toArray(): T[] { + const result: T[] = []; + let current = this.head.next; + + while (current && current !== this.tail) { + result.push(current.value); + current = current.next; + } + + return result; + } +} \ No newline at end of file diff --git a/frontend/src/common/cache/index.ts b/frontend/src/common/cache/index.ts index 1ac3ecd..f80d951 100644 --- a/frontend/src/common/cache/index.ts +++ b/frontend/src/common/cache/index.ts @@ -1,19 +1,21 @@ export type { - CacheItem, - DisposableCacheItem, - CacheConfig, - CacheStats -} from './types'; -export { LRUCache } from './lru-cache'; -export { CacheManager, globalCacheManager } from './cache-manager'; - -export { + CacheItem, + DisposableCacheItem, + CacheConfig, + CacheStats, + CacheStrategy, + DoublyLinkedNode +} from './interfaces'; +export { LruCache } from './lruCache'; +export { CacheManager, globalCacheManager } from './manager'; +export { DoublyLinkedList } from './doublyLinkedList'; +export { + createHash, generateCacheKey, - createCacheItem, - calculateHitRate, - formatCacheSize, + createCacheItem, + calculateHitRate, + formatCacheSize, isExpired, - createContentHash, debounce, throttle } from './utils'; \ No newline at end of file diff --git a/frontend/src/common/cache/interfaces.ts b/frontend/src/common/cache/interfaces.ts new file mode 100644 index 0000000..4a3c66f --- /dev/null +++ b/frontend/src/common/cache/interfaces.ts @@ -0,0 +1,124 @@ +/** + * 缓存项基础接口 + */ +export interface CacheItem { + /** 缓存项的唯一标识 */ + id: string | number; + /** 最后访问时间 */ + lastAccessed: Date; + /** 创建时间 */ + createdAt: Date; +} + +/** + * 可清理的缓存项接口 + */ +export interface DisposableCacheItem extends CacheItem { + /** 清理资源的方法 */ + dispose(): void | Promise; +} + +/** + * 缓存配置接口 + */ +export interface CacheConfig { + /** 最大缓存数量 */ + maxSize: number; + /** 生存时间(毫秒),可选 */ + ttl?: number; + /** 驱逐回调函数,可选 */ + onEvict?: (item: any) => void | Promise; +} + +/** + * 缓存统计信息接口 + */ +export interface CacheStats { + /** 当前缓存项数量 */ + size: number; + /** 最大容量 */ + maxSize: number; + /** 命中次数 */ + hits: number; + /** 未命中次数 */ + misses: number; + /** 命中率 */ + hitRate: number; +} + +/** + * 通用缓存策略接口 + * 所有缓存实现都应该实现这个接口 + */ +export interface CacheStrategy { + /** + * 获取缓存项 + * @param id 缓存项ID + * @returns 缓存项或null + */ + get(id: string | number): T | null; + + /** + * 设置缓存项 + * @param id 缓存项ID + * @param item 缓存项 + */ + set(id: string | number, item: T): void; + + /** + * 移除缓存项 + * @param id 缓存项ID + * @returns 是否成功移除 + */ + remove(id: string | number): boolean; + + /** + * 检查是否存在 + * @param id 缓存项ID + * @returns 是否存在 + */ + has(id: string | number): boolean; + + /** + * 清空缓存 + */ + clear(): void; + + /** + * 获取所有项 + * @returns 所有缓存项 + */ + getAll(): T[]; + + /** + * 获取缓存大小 + * @returns 当前缓存项数量 + */ + size(): number; + + /** + * 获取统计信息 + * @returns 缓存统计信息 + */ + getStats(): CacheStats; + + /** + * 清理过期项 + * @returns 清理的项数量 + */ + cleanup(): number; +} + +/** + * 双向链表节点接口 + */ +export interface DoublyLinkedNode { + /** 节点值 */ + value: T; + /** 节点键 */ + key: string | number; + /** 前一个节点 */ + prev: DoublyLinkedNode | null; + /** 下一个节点 */ + next: DoublyLinkedNode | null; +} \ No newline at end of file diff --git a/frontend/src/common/cache/lru-cache.ts b/frontend/src/common/cache/lru-cache.ts deleted file mode 100644 index f52ccb6..0000000 --- a/frontend/src/common/cache/lru-cache.ts +++ /dev/null @@ -1,200 +0,0 @@ -import type { CacheItem, CacheConfig, CacheStats, DisposableCacheItem } from './types'; - -export class LRUCache { - private items = new Map(); - private accessOrder: (string | number)[] = []; - private config: CacheConfig; - private stats = { - hits: 0, - misses: 0 - }; - - constructor(config: CacheConfig) { - this.config = config; - } - - /** - * 获取缓存项 - */ - get(id: string | number): T | null { - const item = this.items.get(id); - - if (!item) { - this.stats.misses++; - return null; - } - - // 检查是否过期 - if (this.isExpired(item)) { - this.remove(id); - this.stats.misses++; - return null; - } - - // 更新访问时间和顺序 - item.lastAccessed = new Date(); - this.updateAccessOrder(id); - this.stats.hits++; - - return item; - } - - /** - * 设置缓存项 - */ - set(id: string | number, item: T): void { - // 如果已存在,先移除旧的 - if (this.items.has(id)) { - this.remove(id); - } - - // 检查容量,必要时驱逐最旧的项 - while (this.items.size >= this.config.maxSize) { - const oldestId = this.accessOrder.shift(); - if (oldestId !== undefined) { - this.evict(oldestId); - } - } - - // 添加新项 - this.items.set(id, item); - this.accessOrder.push(id); - } - - /** - * 移除缓存项 - */ - remove(id: string | number): boolean { - const item = this.items.get(id); - if (!item) return false; - - this.items.delete(id); - const index = this.accessOrder.indexOf(id); - if (index > -1) { - this.accessOrder.splice(index, 1); - } - - // 调用清理逻辑 - this.disposeItem(item); - return true; - } - - /** - * 检查是否存在 - */ - has(id: string | number): boolean { - const item = this.items.get(id); - if (!item) return false; - - if (this.isExpired(item)) { - this.remove(id); - return false; - } - - return true; - } - - /** - * 清空缓存 - */ - clear(): void { - for (const item of this.items.values()) { - this.disposeItem(item); - } - this.items.clear(); - this.accessOrder = []; - this.stats.hits = 0; - this.stats.misses = 0; - } - - /** - * 获取所有项 - */ - getAll(): T[] { - return Array.from(this.items.values()); - } - - /** - * 获取缓存大小 - */ - size(): number { - return this.items.size; - } - - /** - * 获取统计信息 - */ - getStats(): CacheStats { - const total = this.stats.hits + this.stats.misses; - return { - size: this.items.size, - maxSize: this.config.maxSize, - hits: this.stats.hits, - misses: this.stats.misses, - hitRate: total > 0 ? this.stats.hits / total : 0 - }; - } - - /** - * 清理过期项 - */ - cleanup(): number { - let cleanedCount = 0; - const now = Date.now(); - - if (!this.config.ttl) return cleanedCount; - - for (const [id, item] of this.items.entries()) { - if (now - item.lastAccessed.getTime() > this.config.ttl) { - this.remove(id); - cleanedCount++; - } - } - - return cleanedCount; - } - - // 私有方法 - - private isExpired(item: T): boolean { - if (!this.config.ttl) return false; - return Date.now() - item.lastAccessed.getTime() > this.config.ttl; - } - - private updateAccessOrder(id: string | number): void { - const index = this.accessOrder.indexOf(id); - if (index > -1) { - this.accessOrder.splice(index, 1); - } - this.accessOrder.push(id); - } - - private evict(id: string | number): void { - const item = this.items.get(id); - if (item) { - if (this.config.onEvict) { - this.config.onEvict(item); - } - this.remove(id); - } - } - - private disposeItem(item: T): void { - if (this.isDisposable(item)) { - try { - const result = item.dispose(); - if (result instanceof Promise) { - result.catch(error => { - console.warn('Failed to dispose cache item:', error); - }); - } - } catch (error) { - console.warn('Failed to dispose cache item:', error); - } - } - } - - private isDisposable(item: T): item is T & DisposableCacheItem { - return 'dispose' in item && typeof (item as any).dispose === 'function'; - } -} \ No newline at end of file diff --git a/frontend/src/common/cache/lruCache.ts b/frontend/src/common/cache/lruCache.ts new file mode 100644 index 0000000..6d2e4f6 --- /dev/null +++ b/frontend/src/common/cache/lruCache.ts @@ -0,0 +1,276 @@ +import type { CacheItem, CacheConfig, CacheStats, DisposableCacheItem, CacheStrategy, DoublyLinkedNode } from './interfaces'; +import { DoublyLinkedList } from './doublyLinkedList'; + +/** + * 高性能LRU缓存实现 + * 使用双向链表 + Map 的组合,所有核心操作都是O(1)时间复杂度 + * + * @template T 缓存项类型,必须继承自CacheItem + */ +export class LruCache implements CacheStrategy { + /** 存储缓存项的Map,提供O(1)的查找性能 */ + private items = new Map>(); + + /** 双向链表,管理访问顺序,提供O(1)的插入/删除性能 */ + private accessList = new DoublyLinkedList(); + + /** 缓存配置 */ + private config: CacheConfig; + + /** 统计信息 */ + private stats = { + hits: 0, + misses: 0 + }; + + /** + * 构造函数 + * + * @param config 缓存配置 + */ + constructor(config: CacheConfig) { + this.config = config; + } + + /** + * 获取缓存项 + * 时间复杂度: O(1) + * + * @param id 缓存项ID + * @returns 缓存项或null + */ + get(id: string | number): T | null { + const node = this.items.get(id); + + if (!node) { + this.stats.misses++; + return null; + } + + // 检查是否过期 + if (this.isExpired(node.value)) { + this.remove(id); + this.stats.misses++; + return null; + } + + // 更新访问时间 + node.value.lastAccessed = new Date(); + + // 将节点移动到链表头部(最近访问) + this.accessList.moveToHead(node); + + this.stats.hits++; + return node.value; + } + + /** + * 设置缓存项 + * 时间复杂度: O(1) + * + * @param id 缓存项ID + * @param item 缓存项 + */ + set(id: string | number, item: T): void { + const existingNode = this.items.get(id); + + // 如果已存在,更新值并移动到头部 + if (existingNode) { + existingNode.value = item; + this.accessList.moveToHead(existingNode); + return; + } + + // 检查容量,必要时驱逐最旧的项 + while (this.items.size >= this.config.maxSize) { + this.evictLeastRecentlyUsed(); + } + + // 创建新节点并添加到头部 + const newNode = this.accessList.createNode(id, item); + this.accessList.addToHead(newNode); + this.items.set(id, newNode); + } + + /** + * 移除缓存项 + * 时间复杂度: O(1) + * + * @param id 缓存项ID + * @returns 是否成功移除 + */ + remove(id: string | number): boolean { + const node = this.items.get(id); + if (!node) return false; + + // 从链表中移除 + this.accessList.removeNode(node); + + // 从Map中移除 + this.items.delete(id); + + // 调用清理逻辑 + this.disposeItem(node.value); + + return true; + } + + /** + * 检查是否存在 + * 时间复杂度: O(1) + * + * @param id 缓存项ID + * @returns 是否存在 + */ + has(id: string | number): boolean { + const node = this.items.get(id); + if (!node) return false; + + if (this.isExpired(node.value)) { + this.remove(id); + return false; + } + + return true; + } + + /** + * 清空缓存 + */ + clear(): void { + // 清理所有项 + for (const node of this.items.values()) { + this.disposeItem(node.value); + } + + this.items.clear(); + this.accessList.clear(); + this.stats.hits = 0; + this.stats.misses = 0; + } + + /** + * 获取所有项 + * 按访问顺序返回(最近访问的在前) + * + * @returns 所有缓存项 + */ + getAll(): T[] { + return this.accessList.toArray(); + } + + /** + * 获取缓存大小 + * + * @returns 当前缓存项数量 + */ + size(): number { + return this.items.size; + } + + /** + * 获取统计信息 + * + * @returns 缓存统计信息 + */ + getStats(): CacheStats { + const total = this.stats.hits + this.stats.misses; + return { + size: this.items.size, + maxSize: this.config.maxSize, + hits: this.stats.hits, + misses: this.stats.misses, + hitRate: total > 0 ? this.stats.hits / total : 0 + }; + } + + /** + * 清理过期项 + * + * @returns 清理的项数量 + */ + cleanup(): number { + let cleanedCount = 0; + + if (!this.config.ttl) return cleanedCount; + + // 收集过期的键 + const expiredKeys: (string | number)[] = []; + for (const [id, node] of this.items.entries()) { + if (this.isExpired(node.value)) { + expiredKeys.push(id); + } + } + + // 移除过期项 + for (const key of expiredKeys) { + if (this.remove(key)) { + cleanedCount++; + } + } + + return cleanedCount; + } + + // 私有方法 + + /** + * 检查项是否过期 + * + * @param item 缓存项 + * @returns 是否过期 + */ + private isExpired(item: T): boolean { + if (!this.config.ttl) return false; + return Date.now() - item.lastAccessed.getTime() > this.config.ttl; + } + + /** + * 驱逐最近最少使用的项 + */ + private evictLeastRecentlyUsed(): void { + const tailNode = this.accessList.removeTail(); + if (tailNode) { + // 调用驱逐回调 + if (this.config.onEvict) { + this.config.onEvict(tailNode.value); + } + + // 从Map中移除 + this.items.delete(tailNode.key); + + // 清理资源 + this.disposeItem(tailNode.value); + } + } + + /** + * 清理缓存项资源 + * + * @param item 要清理的缓存项 + */ + private disposeItem(item: T): void { + if (this.isDisposable(item)) { + try { + const result = item.dispose(); + if (result instanceof Promise) { + result.catch(error => { + console.warn('Failed to dispose cache item:', error); + }); + } + } catch (error) { + console.warn('Failed to dispose cache item:', error); + } + } + } + + /** + * 检查项是否可清理 + * + * @param item 缓存项 + * @returns 是否可清理 + */ + private isDisposable(item: T): item is T & DisposableCacheItem { + return 'dispose' in item && typeof (item as any).dispose === 'function'; + } +} \ No newline at end of file diff --git a/frontend/src/common/cache/manager.ts b/frontend/src/common/cache/manager.ts new file mode 100644 index 0000000..61487ee --- /dev/null +++ b/frontend/src/common/cache/manager.ts @@ -0,0 +1,263 @@ +import type { CacheItem, CacheConfig, CacheStats, CacheStrategy } from './interfaces'; +import { LruCache } from './lruCache'; + +/** + * 缓存管理器 + * 统一管理多个缓存实例,提供全局缓存操作和自动清理功能 + * 支持不同的缓存策略,默认使用LRU缓存 + */ +export class CacheManager { + /** 存储所有缓存实例的Map */ + private caches = new Map>(); + + /** 自动清理定时器ID */ + private cleanupInterval?: number; + + /** + * 构造函数 + * + * @param options 配置选项 + * @param options.cleanupInterval 自动清理间隔(毫秒),默认不启用 + */ + constructor(options?: { + /** 自动清理间隔(毫秒),默认 5 分钟 */ + cleanupInterval?: number; + }) { + if (options?.cleanupInterval) { + this.startAutoCleanup(options.cleanupInterval); + } + } + + /** + * 创建或获取缓存实例 + * 如果缓存不存在且提供了配置,则创建新的缓存实例 + * + * @template T 缓存项类型 + * @param name 缓存名称 + * @param config 缓存配置(仅在创建新缓存时需要) + * @param strategy 缓存策略构造函数,默认使用LruCache + * @returns 缓存实例 + * @throws 如果缓存不存在且未提供配置 + * @example + * ```typescript + * const userCache = manager.getCache('users', { + * maxSize: 100, + * ttl: 5 * 60 * 1000 // 5分钟 + * }); + * ``` + */ + getCache( + name: string, + config?: CacheConfig, + strategy: new (config: CacheConfig) => CacheStrategy = LruCache + ): CacheStrategy { + if (!this.caches.has(name)) { + if (!config) { + throw new Error(`Cache "${name}" does not exist and no config provided`); + } + this.caches.set(name, new strategy(config)); + } + return this.caches.get(name)!; + } + + /** + * 创建新的缓存实例 + * 如果同名缓存已存在,则抛出错误 + * + * @template T 缓存项类型 + * @param name 缓存名称 + * @param config 缓存配置 + * @param strategy 缓存策略构造函数,默认使用LruCache + * @returns 新创建的缓存实例 + * @throws 如果同名缓存已存在 + * @example + * ```typescript + * const productCache = manager.createCache('products', { + * maxSize: 200, + * ttl: 10 * 60 * 1000 // 10分钟 + * }); + * ``` + */ + createCache( + name: string, + config: CacheConfig, + strategy: new (config: CacheConfig) => CacheStrategy = LruCache + ): CacheStrategy { + if (this.caches.has(name)) { + throw new Error(`Cache "${name}" already exists`); + } + const cache = new strategy(config); + this.caches.set(name, cache); + return cache; + } + + /** + * 删除缓存实例 + * 会先清空缓存内容,然后从管理器中移除 + * + * @param name 缓存名称 + * @returns 是否成功删除 + * @example + * ```typescript + * const removed = manager.removeCache('temp-cache'); + * console.log(removed); // true 或 false + * ``` + */ + removeCache(name: string): boolean { + const cache = this.caches.get(name); + if (cache) { + cache.clear(); + this.caches.delete(name); + return true; + } + return false; + } + + /** + * 检查缓存是否存在 + * + * @param name 缓存名称 + * @returns 是否存在 + * @example + * ```typescript + * if (manager.hasCache('users')) { + * const userCache = manager.getCache('users'); + * } + * ``` + */ + hasCache(name: string): boolean { + return this.caches.has(name); + } + + /** + * 获取所有缓存名称 + * + * @returns 缓存名称数组 + * @example + * ```typescript + * const cacheNames = manager.getCacheNames(); + * console.log('Active caches:', cacheNames); + * ``` + */ + getCacheNames(): string[] { + return Array.from(this.caches.keys()); + } + + /** + * 清空所有缓存 + * 清空所有缓存实例的内容,但不删除缓存实例本身 + * + * @example + * ```typescript + * manager.clearAll(); // 清空所有缓存内容 + * ``` + */ + clearAll(): void { + for (const cache of this.caches.values()) { + cache.clear(); + } + } + + /** + * 获取所有缓存的统计信息 + * + * @returns 包含所有缓存统计信息的对象 + * @example + * ```typescript + * const stats = manager.getAllStats(); + * console.log('Cache stats:', stats); + * // 输出: { users: { size: 50, hits: 100, ... }, products: { ... } } + * ``` + */ + getAllStats(): Record { + const stats: Record = {}; + for (const [name, cache] of this.caches.entries()) { + stats[name] = cache.getStats(); + } + return stats; + } + + /** + * 清理所有缓存中的过期项 + * + * @returns 包含每个缓存清理项数量的对象 + * @example + * ```typescript + * const results = manager.cleanupAll(); + * console.log('Cleanup results:', results); + * // 输出: { users: 5, products: 2 } // 表示清理的项数量 + * ``` + */ + cleanupAll(): Record { + const results: Record = {}; + for (const [name, cache] of this.caches.entries()) { + results[name] = cache.cleanup(); + } + return results; + } + + /** + * 启动自动清理 + * 定期清理所有缓存中的过期项 + * + * @param interval 清理间隔(毫秒) + * @example + * ```typescript + * manager.startAutoCleanup(5 * 60 * 1000); // 每5分钟清理一次 + * ``` + */ + startAutoCleanup(interval: number): void { + this.stopAutoCleanup(); + this.cleanupInterval = window.setInterval(() => { + this.cleanupAll(); + }, interval); + } + + /** + * 停止自动清理 + * + * @example + * ```typescript + * manager.stopAutoCleanup(); + * ``` + */ + stopAutoCleanup(): void { + if (this.cleanupInterval) { + clearInterval(this.cleanupInterval); + this.cleanupInterval = undefined; + } + } + + /** + * 销毁管理器 + * 停止自动清理,清空所有缓存,并移除所有缓存实例 + * + * @example + * ```typescript + * manager.destroy(); // 完全清理管理器 + * ``` + */ + destroy(): void { + this.stopAutoCleanup(); + this.clearAll(); + this.caches.clear(); + } +} + +/** + * 全局缓存管理器实例 + * 提供开箱即用的缓存管理功能,默认启用5分钟自动清理 + * + * @example + * ```typescript + * import { globalCacheManager } from './cache'; + * + * const userCache = globalCacheManager.getCache('users', { + * maxSize: 100, + * ttl: 5 * 60 * 1000 + * }); + * ``` + */ +export const globalCacheManager = new CacheManager({ + cleanupInterval: 5 * 60 * 1000 // 5 分钟 +}); \ No newline at end of file diff --git a/frontend/src/common/cache/types.ts b/frontend/src/common/cache/types.ts deleted file mode 100644 index 575e55e..0000000 --- a/frontend/src/common/cache/types.ts +++ /dev/null @@ -1,39 +0,0 @@ -// 缓存项基础接口 -export interface CacheItem { - /** 缓存项的唯一标识 */ - id: string | number; - /** 最后访问时间 */ - lastAccessed: Date; - /** 创建时间 */ - createdAt: Date; -} - -// 可清理的缓存项接口 -export interface DisposableCacheItem extends CacheItem { - /** 清理资源的方法 */ - dispose(): void | Promise; -} - -// 缓存配置 -export interface CacheConfig { - /** 最大缓存数量 */ - maxSize: number; - /** 生存时间(毫秒),可选 */ - ttl?: number; - /** 驱逐回调函数,可选 */ - onEvict?: (item: any) => void | Promise; -} - -// 缓存统计信息 -export interface CacheStats { - /** 当前缓存项数量 */ - size: number; - /** 最大容量 */ - maxSize: number; - /** 命中次数 */ - hits: number; - /** 未命中次数 */ - misses: number; - /** 命中率 */ - hitRate: number; -} \ No newline at end of file diff --git a/frontend/src/common/cache/utils.ts b/frontend/src/common/cache/utils.ts index dfc2f14..97e89dc 100644 --- a/frontend/src/common/cache/utils.ts +++ b/frontend/src/common/cache/utils.ts @@ -1,7 +1,42 @@ -import type { CacheItem } from './types'; +import type { CacheItem } from './interfaces'; + +/** + * 简单哈希函数 + * 使用FNV-1a算法生成哈希值,提供良好的分布性 + * + * @param content 要哈希的内容 + * @returns 哈希值字符串 + * @example + * ```typescript + * const hash = createHash('some content'); + * // 结果: 类似 '1a2b3c4d' + * ``` + */ +export function createHash(content: string): string { + const FNV_OFFSET_BASIS = 2166136261; + const FNV_PRIME = 16777619; + + let hash = FNV_OFFSET_BASIS; + + for (let i = 0; i < content.length; i++) { + hash ^= content.charCodeAt(i); + hash = (hash * FNV_PRIME) >>> 0; // 无符号32位整数 + } + + return hash.toString(36); +} /** * 生成缓存键 + * 将多个部分组合成一个缓存键 + * + * @param parts 键的各个部分 + * @returns 组合后的缓存键 + * @example + * ```typescript + * const key = generateCacheKey('user', 123, 'profile'); + * // 结果: 'user:123:profile' + * ``` */ export function generateCacheKey(...parts: (string | number)[]): string { return parts.join(':'); @@ -9,6 +44,17 @@ export function generateCacheKey(...parts: (string | number)[]): string { /** * 创建基础缓存项 + * 为任意数据创建符合CacheItem接口的缓存项 + * + * @template T 数据类型 + * @param id 缓存项的唯一标识 + * @param data 要缓存的数据 + * @returns 包含缓存元数据的缓存项 + * @example + * ```typescript + * const cacheItem = createCacheItem('user:123', { name: 'John', age: 30 }); + * // 结果包含 id, lastAccessed, createdAt 以及原始数据 + * ``` */ export function createCacheItem>( id: string | number, @@ -25,6 +71,16 @@ export function createCacheItem>( /** * 计算缓存命中率 + * 根据命中次数和未命中次数计算命中率 + * + * @param hits 命中次数 + * @param misses 未命中次数 + * @returns 命中率(0-1之间的数值) + * @example + * ```typescript + * const hitRate = calculateHitRate(80, 20); + * // 结果: 0.8 (80% 命中率) + * ``` */ export function calculateHitRate(hits: number, misses: number): number { const total = hits + misses; @@ -33,6 +89,16 @@ export function calculateHitRate(hits: number, misses: number): number { /** * 格式化缓存大小 + * 将字节数格式化为人类可读的大小字符串 + * + * @param size 大小(字节) + * @returns 格式化后的大小字符串 + * @example + * ```typescript + * formatCacheSize(1024); // '1.0 KB' + * formatCacheSize(1048576); // '1.0 MB' + * formatCacheSize(500); // '500 B' + * ``` */ export function formatCacheSize(size: number): string { if (size < 1024) return `${size} B`; @@ -43,29 +109,34 @@ export function formatCacheSize(size: number): string { /** * 检查项是否过期 + * 根据最后访问时间和TTL判断缓存项是否过期 + * + * @param item 缓存项 + * @param ttl 生存时间(毫秒) + * @returns 是否过期 + * @example + * ```typescript + * const item = createCacheItem('test', { data: 'value' }); + * const expired = isExpired(item, 5000); // 5秒TTL + * ``` */ export function isExpired(item: CacheItem, ttl: number): boolean { return Date.now() - item.lastAccessed.getTime() > ttl; } -/** - * 创建内容哈希(简单实现) - */ -export function createContentHash(content: string): string { - let hash = 0; - if (content.length === 0) return hash.toString(); - - for (let i = 0; i < content.length; i++) { - const char = content.charCodeAt(i); - hash = ((hash << 5) - hash) + char; - hash = hash & hash; // 转换为 32 位整数 - } - - return Math.abs(hash).toString(36); -} - /** * 防抖函数,用于缓存操作 + * 在指定时间内多次调用只执行最后一次 + * + * @template T 函数类型 + * @param func 要防抖的函数 + * @param wait 等待时间(毫秒) + * @returns 防抖后的函数 + * @example + * ```typescript + * const debouncedSave = debounce(saveToCache, 300); + * debouncedSave(data); // 只有在300ms内没有新调用时才执行 + * ``` */ export function debounce any>( func: T, @@ -81,6 +152,17 @@ export function debounce any>( /** * 节流函数,用于缓存清理 + * 在指定时间内最多执行一次 + * + * @template T 函数类型 + * @param func 要节流的函数 + * @param limit 限制时间间隔(毫秒) + * @returns 节流后的函数 + * @example + * ```typescript + * const throttledCleanup = throttle(cleanupCache, 1000); + * throttledCleanup(); // 1秒内最多执行一次 + * ``` */ export function throttle any>( func: T, diff --git a/frontend/src/components/tabs/TabContainer.vue b/frontend/src/components/tabs/TabContainer.vue new file mode 100644 index 0000000..66846b7 --- /dev/null +++ b/frontend/src/components/tabs/TabContainer.vue @@ -0,0 +1,233 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/tabs/TabItem.vue b/frontend/src/components/tabs/TabItem.vue new file mode 100644 index 0000000..ef8fe29 --- /dev/null +++ b/frontend/src/components/tabs/TabItem.vue @@ -0,0 +1,273 @@ + + + + + \ No newline at end of file diff --git a/frontend/src/components/titlebar/WindowsTitleBar.vue b/frontend/src/components/titlebar/WindowsTitleBar.vue index aee0b8a..6e00b7f 100644 --- a/frontend/src/components/titlebar/WindowsTitleBar.vue +++ b/frontend/src/components/titlebar/WindowsTitleBar.vue @@ -4,7 +4,11 @@
voidraft
-
{{ titleText }}
+ + +
+ +
@@ -40,6 +44,7 @@ import {computed, onMounted, ref} from 'vue'; import {useI18n} from 'vue-i18n'; import * as runtime from '@wailsio/runtime'; import {useDocumentStore} from '@/stores/documentStore'; +import TabContainer from '@/components/tabs/TabContainer.vue'; const {t} = useI18n(); const isMaximized = ref(false); @@ -118,6 +123,7 @@ onMounted(async () => { font-size: 12px; font-weight: 400; cursor: default; + min-width: 0; /* 允许内容收缩 */ -webkit-context-menu: none; -moz-context-menu: none; @@ -127,6 +133,7 @@ onMounted(async () => { .titlebar-content .titlebar-icon { width: 16px; height: 16px; + flex-shrink: 0; img { width: 100%; @@ -135,9 +142,14 @@ onMounted(async () => { } } -.titlebar-title { - font-size: 12px; - color: var(--toolbar-text); +.titlebar-tabs { + flex: 1; + height: 100%; + align-items: center; + overflow: hidden; + margin-left: 8px; + min-width: 0; + //margin-right: 8px; } .titlebar-controls { diff --git a/frontend/src/components/toolbar/DocumentSelector.vue b/frontend/src/components/toolbar/DocumentSelector.vue index 4fde6df..3878b57 100644 --- a/frontend/src/components/toolbar/DocumentSelector.vue +++ b/frontend/src/components/toolbar/DocumentSelector.vue @@ -1,45 +1,47 @@ @@ -369,7 +284,7 @@ onUnmounted(() => {