feat(framework): server testing utils, transaction storage simplify, pathfinding tests (#384)
## Server Testing Utils - Add TestServer, TestClient, MockRoom for unit testing - Export testing utilities from @esengine/server/testing ## Transaction Storage (BREAKING) - Simplify RedisStorage/MongoStorage to factory pattern only - Remove DI client injection option - Add lazy connection and Symbol.asyncDispose support - Add 161 unit tests with full coverage ## Pathfinding Tests - Add 150 unit tests covering all components - BinaryHeap, Heuristics, AStarPathfinder, GridMap, NavMesh, PathSmoother ## Docs - Update storage.md for new factory pattern API
This commit is contained in:
@@ -12,15 +12,15 @@ import type {
|
||||
TransactionOptions,
|
||||
TransactionLog,
|
||||
OperationLog,
|
||||
OperationResult,
|
||||
} from './types.js'
|
||||
OperationResult
|
||||
} from './types.js';
|
||||
|
||||
/**
|
||||
* @zh 生成唯一 ID
|
||||
* @en Generate unique ID
|
||||
*/
|
||||
function generateId(): string {
|
||||
return `tx_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 11)}`
|
||||
return `tx_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 11)}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,22 +39,22 @@ function generateId(): string {
|
||||
* ```
|
||||
*/
|
||||
export class TransactionContext implements ITransactionContext {
|
||||
private _id: string
|
||||
private _state: TransactionState = 'pending'
|
||||
private _timeout: number
|
||||
private _operations: ITransactionOperation[] = []
|
||||
private _storage: ITransactionStorage | null
|
||||
private _metadata: Record<string, unknown>
|
||||
private _contextData: Map<string, unknown> = new Map()
|
||||
private _startTime: number = 0
|
||||
private _distributed: boolean
|
||||
private _id: string;
|
||||
private _state: TransactionState = 'pending';
|
||||
private _timeout: number;
|
||||
private _operations: ITransactionOperation[] = [];
|
||||
private _storage: ITransactionStorage | null;
|
||||
private _metadata: Record<string, unknown>;
|
||||
private _contextData: Map<string, unknown> = new Map();
|
||||
private _startTime: number = 0;
|
||||
private _distributed: boolean;
|
||||
|
||||
constructor(options: TransactionOptions & { storage?: ITransactionStorage } = {}) {
|
||||
this._id = generateId()
|
||||
this._timeout = options.timeout ?? 30000
|
||||
this._storage = options.storage ?? null
|
||||
this._metadata = options.metadata ?? {}
|
||||
this._distributed = options.distributed ?? false
|
||||
this._id = generateId();
|
||||
this._timeout = options.timeout ?? 30000;
|
||||
this._storage = options.storage ?? null;
|
||||
this._metadata = options.metadata ?? {};
|
||||
this._distributed = options.distributed ?? false;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -62,27 +62,27 @@ export class TransactionContext implements ITransactionContext {
|
||||
// =========================================================================
|
||||
|
||||
get id(): string {
|
||||
return this._id
|
||||
return this._id;
|
||||
}
|
||||
|
||||
get state(): TransactionState {
|
||||
return this._state
|
||||
return this._state;
|
||||
}
|
||||
|
||||
get timeout(): number {
|
||||
return this._timeout
|
||||
return this._timeout;
|
||||
}
|
||||
|
||||
get operations(): ReadonlyArray<ITransactionOperation> {
|
||||
return this._operations
|
||||
return this._operations;
|
||||
}
|
||||
|
||||
get storage(): ITransactionStorage | null {
|
||||
return this._storage
|
||||
return this._storage;
|
||||
}
|
||||
|
||||
get metadata(): Record<string, unknown> {
|
||||
return this._metadata
|
||||
return this._metadata;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -95,10 +95,10 @@ export class TransactionContext implements ITransactionContext {
|
||||
*/
|
||||
addOperation<T extends ITransactionOperation>(operation: T): this {
|
||||
if (this._state !== 'pending') {
|
||||
throw new Error(`Cannot add operation to transaction in state: ${this._state}`)
|
||||
throw new Error(`Cannot add operation to transaction in state: ${this._state}`);
|
||||
}
|
||||
this._operations.push(operation)
|
||||
return this
|
||||
this._operations.push(operation);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,64 +112,64 @@ export class TransactionContext implements ITransactionContext {
|
||||
transactionId: this._id,
|
||||
results: [],
|
||||
error: `Transaction already in state: ${this._state}`,
|
||||
duration: 0,
|
||||
}
|
||||
duration: 0
|
||||
};
|
||||
}
|
||||
|
||||
this._startTime = Date.now()
|
||||
this._state = 'executing'
|
||||
this._startTime = Date.now();
|
||||
this._state = 'executing';
|
||||
|
||||
const results: OperationResult[] = []
|
||||
let executedCount = 0
|
||||
const results: OperationResult[] = [];
|
||||
let executedCount = 0;
|
||||
|
||||
try {
|
||||
await this._saveLog()
|
||||
await this._saveLog();
|
||||
|
||||
for (let i = 0; i < this._operations.length; i++) {
|
||||
if (this._isTimedOut()) {
|
||||
throw new Error('Transaction timed out')
|
||||
throw new Error('Transaction timed out');
|
||||
}
|
||||
|
||||
const op = this._operations[i]
|
||||
const op = this._operations[i];
|
||||
|
||||
const isValid = await op.validate(this)
|
||||
const isValid = await op.validate(this);
|
||||
if (!isValid) {
|
||||
throw new Error(`Validation failed for operation: ${op.name}`)
|
||||
throw new Error(`Validation failed for operation: ${op.name}`);
|
||||
}
|
||||
|
||||
const result = await op.execute(this)
|
||||
results.push(result)
|
||||
executedCount++
|
||||
const result = await op.execute(this);
|
||||
results.push(result);
|
||||
executedCount++;
|
||||
|
||||
await this._updateOperationLog(i, 'executed')
|
||||
await this._updateOperationLog(i, 'executed');
|
||||
|
||||
if (!result.success) {
|
||||
throw new Error(result.error ?? `Operation ${op.name} failed`)
|
||||
throw new Error(result.error ?? `Operation ${op.name} failed`);
|
||||
}
|
||||
}
|
||||
|
||||
this._state = 'committed'
|
||||
await this._updateTransactionState('committed')
|
||||
this._state = 'committed';
|
||||
await this._updateTransactionState('committed');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
transactionId: this._id,
|
||||
results,
|
||||
data: this._collectResultData(results) as T,
|
||||
duration: Date.now() - this._startTime,
|
||||
}
|
||||
duration: Date.now() - this._startTime
|
||||
};
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
|
||||
await this._compensate(executedCount - 1)
|
||||
await this._compensate(executedCount - 1);
|
||||
|
||||
return {
|
||||
success: false,
|
||||
transactionId: this._id,
|
||||
results,
|
||||
error: errorMessage,
|
||||
duration: Date.now() - this._startTime,
|
||||
}
|
||||
duration: Date.now() - this._startTime
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,10 +179,10 @@ export class TransactionContext implements ITransactionContext {
|
||||
*/
|
||||
async rollback(): Promise<void> {
|
||||
if (this._state === 'committed' || this._state === 'rolledback') {
|
||||
return
|
||||
return;
|
||||
}
|
||||
|
||||
await this._compensate(this._operations.length - 1)
|
||||
await this._compensate(this._operations.length - 1);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -190,7 +190,7 @@ export class TransactionContext implements ITransactionContext {
|
||||
* @en Get context data
|
||||
*/
|
||||
get<T>(key: string): T | undefined {
|
||||
return this._contextData.get(key) as T | undefined
|
||||
return this._contextData.get(key) as T | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -198,7 +198,7 @@ export class TransactionContext implements ITransactionContext {
|
||||
* @en Set context data
|
||||
*/
|
||||
set<T>(key: string, value: T): void {
|
||||
this._contextData.set(key, value)
|
||||
this._contextData.set(key, value);
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -206,28 +206,28 @@ export class TransactionContext implements ITransactionContext {
|
||||
// =========================================================================
|
||||
|
||||
private _isTimedOut(): boolean {
|
||||
return Date.now() - this._startTime > this._timeout
|
||||
return Date.now() - this._startTime > this._timeout;
|
||||
}
|
||||
|
||||
private async _compensate(fromIndex: number): Promise<void> {
|
||||
this._state = 'rolledback'
|
||||
this._state = 'rolledback';
|
||||
|
||||
for (let i = fromIndex; i >= 0; i--) {
|
||||
const op = this._operations[i]
|
||||
const op = this._operations[i];
|
||||
try {
|
||||
await op.compensate(this)
|
||||
await this._updateOperationLog(i, 'compensated')
|
||||
await op.compensate(this);
|
||||
await this._updateOperationLog(i, 'compensated');
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
await this._updateOperationLog(i, 'failed', errorMessage)
|
||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||
await this._updateOperationLog(i, 'failed', errorMessage);
|
||||
}
|
||||
}
|
||||
|
||||
await this._updateTransactionState('rolledback')
|
||||
await this._updateTransactionState('rolledback');
|
||||
}
|
||||
|
||||
private async _saveLog(): Promise<void> {
|
||||
if (!this._storage) return
|
||||
if (!this._storage) return;
|
||||
|
||||
const log: TransactionLog = {
|
||||
id: this._id,
|
||||
@@ -238,19 +238,19 @@ export class TransactionContext implements ITransactionContext {
|
||||
operations: this._operations.map((op) => ({
|
||||
name: op.name,
|
||||
data: op.data,
|
||||
state: 'pending' as const,
|
||||
state: 'pending' as const
|
||||
})),
|
||||
metadata: this._metadata,
|
||||
distributed: this._distributed,
|
||||
}
|
||||
distributed: this._distributed
|
||||
};
|
||||
|
||||
await this._storage.saveTransaction(log)
|
||||
await this._storage.saveTransaction(log);
|
||||
}
|
||||
|
||||
private async _updateTransactionState(state: TransactionState): Promise<void> {
|
||||
this._state = state
|
||||
this._state = state;
|
||||
if (this._storage) {
|
||||
await this._storage.updateTransactionState(this._id, state)
|
||||
await this._storage.updateTransactionState(this._id, state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -260,18 +260,18 @@ export class TransactionContext implements ITransactionContext {
|
||||
error?: string
|
||||
): Promise<void> {
|
||||
if (this._storage) {
|
||||
await this._storage.updateOperationState(this._id, index, state, error)
|
||||
await this._storage.updateOperationState(this._id, index, state, error);
|
||||
}
|
||||
}
|
||||
|
||||
private _collectResultData(results: OperationResult[]): unknown {
|
||||
const data: Record<string, unknown> = {}
|
||||
const data: Record<string, unknown> = {};
|
||||
for (const result of results) {
|
||||
if (result.data !== undefined) {
|
||||
Object.assign(data, result.data)
|
||||
Object.assign(data, result.data);
|
||||
}
|
||||
}
|
||||
return Object.keys(data).length > 0 ? data : undefined
|
||||
return Object.keys(data).length > 0 ? data : undefined;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -282,5 +282,5 @@ export class TransactionContext implements ITransactionContext {
|
||||
export function createTransactionContext(
|
||||
options: TransactionOptions & { storage?: ITransactionStorage } = {}
|
||||
): ITransactionContext {
|
||||
return new TransactionContext(options)
|
||||
return new TransactionContext(options);
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ import type {
|
||||
TransactionManagerConfig,
|
||||
TransactionOptions,
|
||||
TransactionLog,
|
||||
TransactionResult,
|
||||
} from './types.js'
|
||||
import { TransactionContext } from './TransactionContext.js'
|
||||
TransactionResult
|
||||
} from './types.js';
|
||||
import { TransactionContext } from './TransactionContext.js';
|
||||
|
||||
/**
|
||||
* @zh 事务管理器
|
||||
@@ -35,17 +35,17 @@ import { TransactionContext } from './TransactionContext.js'
|
||||
* ```
|
||||
*/
|
||||
export class TransactionManager {
|
||||
private _storage: ITransactionStorage | null
|
||||
private _defaultTimeout: number
|
||||
private _serverId: string
|
||||
private _autoRecover: boolean
|
||||
private _activeTransactions: Map<string, ITransactionContext> = new Map()
|
||||
private _storage: ITransactionStorage | null;
|
||||
private _defaultTimeout: number;
|
||||
private _serverId: string;
|
||||
private _autoRecover: boolean;
|
||||
private _activeTransactions: Map<string, ITransactionContext> = new Map();
|
||||
|
||||
constructor(config: TransactionManagerConfig = {}) {
|
||||
this._storage = config.storage ?? null
|
||||
this._defaultTimeout = config.defaultTimeout ?? 30000
|
||||
this._serverId = config.serverId ?? this._generateServerId()
|
||||
this._autoRecover = config.autoRecover ?? true
|
||||
this._storage = config.storage ?? null;
|
||||
this._defaultTimeout = config.defaultTimeout ?? 30000;
|
||||
this._serverId = config.serverId ?? this._generateServerId();
|
||||
this._autoRecover = config.autoRecover ?? true;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -57,7 +57,7 @@ export class TransactionManager {
|
||||
* @en Server ID
|
||||
*/
|
||||
get serverId(): string {
|
||||
return this._serverId
|
||||
return this._serverId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,7 +65,7 @@ export class TransactionManager {
|
||||
* @en Storage instance
|
||||
*/
|
||||
get storage(): ITransactionStorage | null {
|
||||
return this._storage
|
||||
return this._storage;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +73,7 @@ export class TransactionManager {
|
||||
* @en Active transaction count
|
||||
*/
|
||||
get activeCount(): number {
|
||||
return this._activeTransactions.size
|
||||
return this._activeTransactions.size;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -93,14 +93,14 @@ export class TransactionManager {
|
||||
storage: this._storage ?? undefined,
|
||||
metadata: {
|
||||
...options.metadata,
|
||||
serverId: this._serverId,
|
||||
serverId: this._serverId
|
||||
},
|
||||
distributed: options.distributed,
|
||||
})
|
||||
distributed: options.distributed
|
||||
});
|
||||
|
||||
this._activeTransactions.set(ctx.id, ctx)
|
||||
this._activeTransactions.set(ctx.id, ctx);
|
||||
|
||||
return ctx
|
||||
return ctx;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,14 +115,14 @@ export class TransactionManager {
|
||||
builder: (ctx: ITransactionContext) => void | Promise<void>,
|
||||
options: TransactionOptions = {}
|
||||
): Promise<TransactionResult<T>> {
|
||||
const ctx = this.begin(options)
|
||||
const ctx = this.begin(options);
|
||||
|
||||
try {
|
||||
await builder(ctx)
|
||||
const result = await ctx.execute<T>()
|
||||
return result
|
||||
await builder(ctx);
|
||||
const result = await ctx.execute<T>();
|
||||
return result;
|
||||
} finally {
|
||||
this._activeTransactions.delete(ctx.id)
|
||||
this._activeTransactions.delete(ctx.id);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ export class TransactionManager {
|
||||
* @en Get active transaction
|
||||
*/
|
||||
getTransaction(id: string): ITransactionContext | undefined {
|
||||
return this._activeTransactions.get(id)
|
||||
return this._activeTransactions.get(id);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -139,21 +139,21 @@ export class TransactionManager {
|
||||
* @en Recover pending transactions
|
||||
*/
|
||||
async recover(): Promise<number> {
|
||||
if (!this._storage) return 0
|
||||
if (!this._storage) return 0;
|
||||
|
||||
const pendingTransactions = await this._storage.getPendingTransactions(this._serverId)
|
||||
let recoveredCount = 0
|
||||
const pendingTransactions = await this._storage.getPendingTransactions(this._serverId);
|
||||
let recoveredCount = 0;
|
||||
|
||||
for (const log of pendingTransactions) {
|
||||
try {
|
||||
await this._recoverTransaction(log)
|
||||
recoveredCount++
|
||||
await this._recoverTransaction(log);
|
||||
recoveredCount++;
|
||||
} catch (error) {
|
||||
console.error(`Failed to recover transaction ${log.id}:`, error)
|
||||
console.error(`Failed to recover transaction ${log.id}:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
return recoveredCount
|
||||
return recoveredCount;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -161,8 +161,8 @@ export class TransactionManager {
|
||||
* @en Acquire distributed lock
|
||||
*/
|
||||
async acquireLock(key: string, ttl: number = 10000): Promise<string | null> {
|
||||
if (!this._storage) return null
|
||||
return this._storage.acquireLock(key, ttl)
|
||||
if (!this._storage) return null;
|
||||
return this._storage.acquireLock(key, ttl);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -170,8 +170,8 @@ export class TransactionManager {
|
||||
* @en Release distributed lock
|
||||
*/
|
||||
async releaseLock(key: string, token: string): Promise<boolean> {
|
||||
if (!this._storage) return false
|
||||
return this._storage.releaseLock(key, token)
|
||||
if (!this._storage) return false;
|
||||
return this._storage.releaseLock(key, token);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -183,15 +183,15 @@ export class TransactionManager {
|
||||
fn: () => Promise<T>,
|
||||
ttl: number = 10000
|
||||
): Promise<T> {
|
||||
const token = await this.acquireLock(key, ttl)
|
||||
const token = await this.acquireLock(key, ttl);
|
||||
if (!token) {
|
||||
throw new Error(`Failed to acquire lock for key: ${key}`)
|
||||
throw new Error(`Failed to acquire lock for key: ${key}`);
|
||||
}
|
||||
|
||||
try {
|
||||
return await fn()
|
||||
return await fn();
|
||||
} finally {
|
||||
await this.releaseLock(key, token)
|
||||
await this.releaseLock(key, token);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -200,24 +200,24 @@ export class TransactionManager {
|
||||
* @en Clean up completed transaction logs
|
||||
*/
|
||||
async cleanup(beforeTimestamp?: number): Promise<number> {
|
||||
if (!this._storage) return 0
|
||||
if (!this._storage) return 0;
|
||||
|
||||
const timestamp = beforeTimestamp ?? Date.now() - 24 * 60 * 60 * 1000 // 默认清理24小时前
|
||||
const timestamp = beforeTimestamp ?? Date.now() - 24 * 60 * 60 * 1000; // 默认清理24小时前
|
||||
|
||||
const pendingTransactions = await this._storage.getPendingTransactions()
|
||||
let cleanedCount = 0
|
||||
const pendingTransactions = await this._storage.getPendingTransactions();
|
||||
let cleanedCount = 0;
|
||||
|
||||
for (const log of pendingTransactions) {
|
||||
if (
|
||||
log.createdAt < timestamp &&
|
||||
(log.state === 'committed' || log.state === 'rolledback')
|
||||
) {
|
||||
await this._storage.deleteTransaction(log.id)
|
||||
cleanedCount++
|
||||
await this._storage.deleteTransaction(log.id);
|
||||
cleanedCount++;
|
||||
}
|
||||
}
|
||||
|
||||
return cleanedCount
|
||||
return cleanedCount;
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
@@ -225,20 +225,20 @@ export class TransactionManager {
|
||||
// =========================================================================
|
||||
|
||||
private _generateServerId(): string {
|
||||
return `server_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`
|
||||
return `server_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
||||
}
|
||||
|
||||
private async _recoverTransaction(log: TransactionLog): Promise<void> {
|
||||
if (log.state === 'executing') {
|
||||
const executedOps = log.operations.filter((op) => op.state === 'executed')
|
||||
const executedOps = log.operations.filter((op) => op.state === 'executed');
|
||||
|
||||
if (executedOps.length > 0 && this._storage) {
|
||||
for (let i = executedOps.length - 1; i >= 0; i--) {
|
||||
await this._storage.updateOperationState(log.id, i, 'compensated')
|
||||
await this._storage.updateOperationState(log.id, i, 'compensated');
|
||||
}
|
||||
await this._storage.updateTransactionState(log.id, 'rolledback')
|
||||
await this._storage.updateTransactionState(log.id, 'rolledback');
|
||||
} else {
|
||||
await this._storage?.updateTransactionState(log.id, 'failed')
|
||||
await this._storage?.updateTransactionState(log.id, 'failed');
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -251,5 +251,5 @@ export class TransactionManager {
|
||||
export function createTransactionManager(
|
||||
config: TransactionManagerConfig = {}
|
||||
): TransactionManager {
|
||||
return new TransactionManager(config)
|
||||
return new TransactionManager(config);
|
||||
}
|
||||
|
||||
@@ -13,8 +13,8 @@ export type {
|
||||
TransactionManagerConfig,
|
||||
ITransactionStorage,
|
||||
ITransactionOperation,
|
||||
ITransactionContext,
|
||||
} from './types.js'
|
||||
ITransactionContext
|
||||
} from './types.js';
|
||||
|
||||
export { TransactionContext, createTransactionContext } from './TransactionContext.js'
|
||||
export { TransactionManager, createTransactionManager } from './TransactionManager.js'
|
||||
export { TransactionContext, createTransactionContext } from './TransactionContext.js';
|
||||
export { TransactionManager, createTransactionManager } from './TransactionManager.js';
|
||||
|
||||
@@ -279,6 +279,15 @@ export interface TransactionManagerConfig {
|
||||
* @en Transaction storage interface
|
||||
*/
|
||||
export interface ITransactionStorage {
|
||||
/**
|
||||
* @zh 关闭存储连接
|
||||
* @en Close storage connection
|
||||
*
|
||||
* @zh 释放所有资源,关闭数据库连接
|
||||
* @en Release all resources, close database connections
|
||||
*/
|
||||
close?(): Promise<void>
|
||||
|
||||
/**
|
||||
* @zh 获取分布式锁
|
||||
* @en Acquire distributed lock
|
||||
|
||||
Reference in New Issue
Block a user