feat(transaction): 添加游戏事务系统 | add game transaction system (#381)

- TransactionManager/TransactionContext 事务管理
- MemoryStorage/RedisStorage/MongoStorage 存储实现
- CurrencyOperation/InventoryOperation/TradeOperation 内置操作
- SagaOrchestrator 分布式 Saga 编排
- withTransactions() Room 集成
- 完整中英文文档
This commit is contained in:
YHH
2025-12-29 10:54:00 +08:00
committed by GitHub
parent 2d46ccf896
commit d4cef828e1
38 changed files with 6631 additions and 16 deletions

View File

@@ -0,0 +1,51 @@
---
"@esengine/transaction": minor
---
feat(transaction): 添加游戏事务系统 | add game transaction system
**@esengine/transaction** - 游戏事务系统 | Game transaction system
### 核心功能 | Core Features
- **TransactionManager** - 事务生命周期管理 | Transaction lifecycle management
- begin()/run() 创建事务 | Create transactions
- 分布式锁支持 | Distributed lock support
- 自动恢复未完成事务 | Auto-recover pending transactions
- **TransactionContext** - 事务上下文 | Transaction context
- 操作链式添加 | Chain operation additions
- 上下文数据共享 | Context data sharing
- 超时控制 | Timeout control
- **Saga 模式** - 补偿式事务 | Compensating transactions
- execute/compensate 成对操作 | Paired execute/compensate
- 自动回滚失败事务 | Auto-rollback on failure
### 存储实现 | Storage Implementations
- **MemoryStorage** - 内存存储,用于开发测试 | In-memory for dev/testing
- **RedisStorage** - Redis 分布式锁和缓存 | Redis distributed lock & cache
- **MongoStorage** - MongoDB 持久化事务日志 | MongoDB persistent transaction logs
### 内置操作 | Built-in Operations
- **CurrencyOperation** - 货币增减操作 | Currency add/deduct
- **InventoryOperation** - 背包物品操作 | Inventory item operations
- **TradeOperation** - 玩家交易操作 | Player trade operations
### 分布式事务 | Distributed Transactions
- **SagaOrchestrator** - 跨服务器 Saga 编排 | Cross-server Saga orchestration
- 完整的 Saga 日志记录 | Complete Saga logging
- 未完成 Saga 恢复 | Incomplete Saga recovery
### Room 集成 | Room Integration
- **withTransactions()** - Room mixin 扩展 | Room mixin extension
- **TransactionRoom** - 预配置的事务 Room 基类 | Pre-configured transaction Room base
### 文档 | Documentation
- 完整的中英文文档 | Complete bilingual documentation
- 核心概念、存储层、操作、分布式事务 | Core concepts, storage, operations, distributed

View File

@@ -34,6 +34,7 @@ ESEngine provides a rich set of modules that can be imported as needed.
| Module | Package | Description | | Module | Package | Description |
|--------|---------|-------------| |--------|---------|-------------|
| [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking | | [Network](/en/modules/network/) | `@esengine/network` | Multiplayer game networking |
| [Transaction](/en/modules/transaction/) | `@esengine/transaction` | Game transactions with distributed support |
## Installation ## Installation

View File

@@ -0,0 +1,261 @@
---
title: "Core Concepts"
description: "Transaction system core concepts: context, manager, and Saga pattern"
---
## Transaction State
A transaction can be in the following states:
```typescript
type TransactionState =
| 'pending' // Waiting to execute
| 'executing' // Executing
| 'committed' // Committed
| 'rolledback' // Rolled back
| 'failed' // Failed
```
## TransactionContext
The transaction context encapsulates transaction state, operations, and execution logic.
### Creating Transactions
```typescript
import { TransactionManager } from '@esengine/transaction';
const manager = new TransactionManager();
// Method 1: Manual management with begin()
const tx = manager.begin({ timeout: 5000 });
tx.addOperation(op1);
tx.addOperation(op2);
const result = await tx.execute();
// Method 2: Automatic management with run()
const result = await manager.run((tx) => {
tx.addOperation(op1);
tx.addOperation(op2);
});
```
### Chaining Operations
```typescript
const result = await manager.run((tx) => {
tx.addOperation(new CurrencyOperation({ ... }))
.addOperation(new InventoryOperation({ ... }))
.addOperation(new InventoryOperation({ ... }));
});
```
### Context Data
Operations can share data through the context:
```typescript
class CustomOperation extends BaseOperation<MyData, MyResult> {
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// Read data set by previous operations
const previousResult = ctx.get<number>('previousValue');
// Set data for subsequent operations
ctx.set('myResult', { value: 123 });
return this.success({ ... });
}
}
```
## TransactionManager
The transaction manager is responsible for creating, executing, and recovering transactions.
### Configuration Options
```typescript
interface TransactionManagerConfig {
storage?: ITransactionStorage; // Storage instance
defaultTimeout?: number; // Default timeout (ms)
serverId?: string; // Server ID (for distributed)
autoRecover?: boolean; // Auto-recover pending transactions
}
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
defaultTimeout: 10000,
serverId: 'server-1',
autoRecover: true,
});
```
### Distributed Locking
```typescript
// Acquire lock
const token = await manager.acquireLock('player:123:inventory', 10000);
if (token) {
try {
// Perform operations
await doSomething();
} finally {
// Release lock
await manager.releaseLock('player:123:inventory', token);
}
}
// Or use withLock for convenience
await manager.withLock('player:123:inventory', async () => {
await doSomething();
}, 10000);
```
### Transaction Recovery
Recover pending transactions after server restart:
```typescript
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
serverId: 'server-1',
});
// Recover pending transactions
const recoveredCount = await manager.recover();
console.log(`Recovered ${recoveredCount} transactions`);
```
## Saga Pattern
The transaction system uses the Saga pattern. Each operation must implement `execute` and `compensate` methods:
```typescript
interface ITransactionOperation<TData, TResult> {
readonly name: string;
readonly data: TData;
// Validate preconditions
validate(ctx: ITransactionContext): Promise<boolean>;
// Forward execution
execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>;
// Compensate (rollback)
compensate(ctx: ITransactionContext): Promise<void>;
}
```
### Execution Flow
```
Begin Transaction
┌─────────────────────┐
│ validate(op1) │──fail──► Return failure
└─────────────────────┘
│success
┌─────────────────────┐
│ execute(op1) │──fail──┐
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ validate(op2) │──fail──┤
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ execute(op2) │──fail──┤
└─────────────────────┘ │
│success ▼
▼ ┌─────────────────────┐
Commit Transaction │ compensate(op1) │
└─────────────────────┘
Return failure (rolled back)
```
### Custom Operations
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
interface UpgradeData {
playerId: string;
itemId: string;
targetLevel: number;
}
interface UpgradeResult {
newLevel: number;
}
class UpgradeOperation extends BaseOperation<UpgradeData, UpgradeResult> {
readonly name = 'upgrade';
private _previousLevel: number = 0;
async validate(ctx: ITransactionContext): Promise<boolean> {
// Validate item exists and can be upgraded
const item = await this.getItem(ctx);
return item !== null && item.level < this.data.targetLevel;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<UpgradeResult>> {
const item = await this.getItem(ctx);
if (!item) {
return this.failure('Item not found', 'ITEM_NOT_FOUND');
}
this._previousLevel = item.level;
item.level = this.data.targetLevel;
await this.saveItem(ctx, item);
return this.success({ newLevel: item.level });
}
async compensate(ctx: ITransactionContext): Promise<void> {
const item = await this.getItem(ctx);
if (item) {
item.level = this._previousLevel;
await this.saveItem(ctx, item);
}
}
private async getItem(ctx: ITransactionContext) {
// Get item from storage
}
private async saveItem(ctx: ITransactionContext, item: any) {
// Save item to storage
}
}
```
## Transaction Result
```typescript
interface TransactionResult<T = unknown> {
success: boolean; // Whether succeeded
transactionId: string; // Transaction ID
results: OperationResult[]; // Operation results
data?: T; // Final data
error?: string; // Error message
duration: number; // Execution time (ms)
}
const result = await manager.run((tx) => { ... });
console.log(`Transaction ${result.transactionId}`);
console.log(`Success: ${result.success}`);
console.log(`Duration: ${result.duration}ms`);
if (!result.success) {
console.log(`Error: ${result.error}`);
}
```

View File

@@ -0,0 +1,355 @@
---
title: "Distributed Transactions"
description: "Saga orchestrator and cross-server transaction support"
---
## Saga Orchestrator
`SagaOrchestrator` is used to orchestrate distributed transactions across servers.
### Basic Usage
```typescript
import { SagaOrchestrator, RedisStorage } from '@esengine/transaction';
const orchestrator = new SagaOrchestrator({
storage: new RedisStorage({ client: redis }),
timeout: 30000,
serverId: 'orchestrator-1',
});
const result = await orchestrator.execute([
{
name: 'deduct_currency',
serverId: 'game-server-1',
data: { playerId: 'player1', amount: 100 },
execute: async (data) => {
// Call game server API to deduct currency
const response = await gameServerApi.deductCurrency(data);
return { success: response.ok };
},
compensate: async (data) => {
// Call game server API to restore currency
await gameServerApi.addCurrency(data);
},
},
{
name: 'add_item',
serverId: 'inventory-server-1',
data: { playerId: 'player1', itemId: 'sword' },
execute: async (data) => {
const response = await inventoryServerApi.addItem(data);
return { success: response.ok };
},
compensate: async (data) => {
await inventoryServerApi.removeItem(data);
},
},
]);
if (result.success) {
console.log('Saga completed successfully');
} else {
console.log('Saga failed:', result.error);
console.log('Completed steps:', result.completedSteps);
console.log('Failed at:', result.failedStep);
}
```
### Configuration Options
```typescript
interface SagaOrchestratorConfig {
storage?: ITransactionStorage; // Storage instance
timeout?: number; // Timeout in milliseconds
serverId?: string; // Orchestrator server ID
}
```
### Saga Step
```typescript
interface SagaStep<T = unknown> {
name: string; // Step name
serverId?: string; // Target server ID
data: T; // Step data
execute: (data: T) => Promise<OperationResult>; // Execute function
compensate: (data: T) => Promise<void>; // Compensate function
}
```
### Saga Result
```typescript
interface SagaResult {
success: boolean; // Whether succeeded
sagaId: string; // Saga ID
completedSteps: string[]; // Completed steps
failedStep?: string; // Failed step
error?: string; // Error message
duration: number; // Execution time (ms)
}
```
## Execution Flow
```
Start Saga
┌─────────────────────┐
│ Step 1: execute │──fail──┐
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ Step 2: execute │──fail──┤
└─────────────────────┘ │
│success │
▼ │
┌─────────────────────┐ │
│ Step 3: execute │──fail──┤
└─────────────────────┘ │
│success ▼
▼ ┌─────────────────────┐
Saga Complete │ Step 2: compensate │
└─────────────────────┘
┌─────────────────────┐
│ Step 1: compensate │
└─────────────────────┘
Saga Failed (compensated)
```
## Saga Logs
The orchestrator records detailed execution logs:
```typescript
interface SagaLog {
id: string; // Saga ID
state: SagaLogState; // State
steps: SagaStepLog[]; // Step logs
createdAt: number; // Creation time
updatedAt: number; // Update time
metadata?: Record<string, unknown>;
}
type SagaLogState =
| 'pending' // Waiting to execute
| 'running' // Executing
| 'completed' // Completed
| 'compensating' // Compensating
| 'compensated' // Compensated
| 'failed' // Failed
interface SagaStepLog {
name: string; // Step name
serverId?: string; // Server ID
state: SagaStepState; // State
startedAt?: number; // Start time
completedAt?: number; // Completion time
error?: string; // Error message
}
type SagaStepState =
| 'pending' // Waiting to execute
| 'executing' // Executing
| 'completed' // Completed
| 'compensating' // Compensating
| 'compensated' // Compensated
| 'failed' // Failed
```
### Query Saga Logs
```typescript
const log = await orchestrator.getSagaLog('saga_xxx');
if (log) {
console.log('Saga state:', log.state);
for (const step of log.steps) {
console.log(` ${step.name}: ${step.state}`);
}
}
```
## Cross-Server Transaction Examples
### Scenario: Cross-Server Purchase
A player purchases an item on a game server, with currency on an account server and items on an inventory server.
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'purchase-orchestrator',
});
async function crossServerPurchase(
playerId: string,
itemId: string,
price: number
): Promise<SagaResult> {
return orchestrator.execute([
// Step 1: Deduct balance on account server
{
name: 'deduct_balance',
serverId: 'account-server',
data: { playerId, amount: price },
execute: async (data) => {
const result = await accountService.deduct(data.playerId, data.amount);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await accountService.refund(data.playerId, data.amount);
},
},
// Step 2: Add item on inventory server
{
name: 'add_item',
serverId: 'inventory-server',
data: { playerId, itemId },
execute: async (data) => {
const result = await inventoryService.addItem(data.playerId, data.itemId);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
},
// Step 3: Record purchase log
{
name: 'log_purchase',
serverId: 'log-server',
data: { playerId, itemId, price, timestamp: Date.now() },
execute: async (data) => {
await logService.recordPurchase(data);
return { success: true };
},
compensate: async (data) => {
await logService.cancelPurchase(data);
},
},
]);
}
```
### Scenario: Cross-Server Trade
Two players on different servers trade with each other.
```typescript
async function crossServerTrade(
playerA: { id: string; server: string; items: string[] },
playerB: { id: string; server: string; items: string[] }
): Promise<SagaResult> {
const steps: SagaStep[] = [];
// Remove items from player A
for (const itemId of playerA.items) {
steps.push({
name: `remove_${playerA.id}_${itemId}`,
serverId: playerA.server,
data: { playerId: playerA.id, itemId },
execute: async (data) => {
return await inventoryService.removeItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.addItem(data.playerId, data.itemId);
},
});
}
// Add items to player B (from A)
for (const itemId of playerA.items) {
steps.push({
name: `add_${playerB.id}_${itemId}`,
serverId: playerB.server,
data: { playerId: playerB.id, itemId },
execute: async (data) => {
return await inventoryService.addItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
});
}
// Similarly handle player B's items...
return orchestrator.execute(steps);
}
```
## Recovering Incomplete Sagas
Recover incomplete Sagas after server restart:
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'my-orchestrator',
});
// Recover incomplete Sagas (will execute compensation)
const recoveredCount = await orchestrator.recover();
console.log(`Recovered ${recoveredCount} sagas`);
```
## Best Practices
### 1. Idempotency
Ensure all operations are idempotent:
```typescript
{
execute: async (data) => {
// Use unique ID to ensure idempotency
const result = await service.process(data.requestId, data);
return { success: result.ok };
},
compensate: async (data) => {
// Compensation must also be idempotent
await service.rollback(data.requestId);
},
}
```
### 2. Timeout Handling
Set appropriate timeout values:
```typescript
const orchestrator = new SagaOrchestrator({
timeout: 60000, // Cross-server operations need longer timeout
});
```
### 3. Monitoring and Alerts
Log Saga execution results:
```typescript
const result = await orchestrator.execute(steps);
if (!result.success) {
// Send alert
alertService.send({
type: 'saga_failed',
sagaId: result.sagaId,
failedStep: result.failedStep,
error: result.error,
});
// Log details
const log = await orchestrator.getSagaLog(result.sagaId);
logger.error('Saga failed', { log });
}
```

View File

@@ -0,0 +1,238 @@
---
title: "Transaction System"
description: "Game transaction system with distributed support for shop purchases, player trading, and more"
---
`@esengine/transaction` provides comprehensive game transaction capabilities based on the Saga pattern, supporting shop purchases, player trading, multi-step tasks, and distributed transactions with Redis/MongoDB.
## Overview
The transaction system solves common data consistency problems in games:
| Scenario | Problem | Solution |
|----------|---------|----------|
| Shop Purchase | Payment succeeded but item not delivered | Atomic transaction with auto-rollback |
| Player Trade | One party transferred items but other didn't receive | Saga compensation mechanism |
| Cross-Server | Data inconsistency across servers | Distributed lock + transaction log |
## Installation
```bash
npm install @esengine/transaction
```
Optional dependencies (install based on storage needs):
```bash
npm install ioredis # Redis storage
npm install mongodb # MongoDB storage
```
## Architecture
```
┌─────────────────────────────────────────────────────────────────┐
│ Transaction Layer │
├─────────────────────────────────────────────────────────────────┤
│ TransactionManager - Manages transaction lifecycle │
│ TransactionContext - Encapsulates operations and state │
│ SagaOrchestrator - Distributed Saga orchestrator │
├─────────────────────────────────────────────────────────────────┤
│ Storage Layer │
├─────────────────────────────────────────────────────────────────┤
│ MemoryStorage - In-memory (dev/test) │
│ RedisStorage - Redis (distributed lock + cache) │
│ MongoStorage - MongoDB (persistent log) │
├─────────────────────────────────────────────────────────────────┤
│ Operation Layer │
├─────────────────────────────────────────────────────────────────┤
│ CurrencyOperation - Currency operations │
│ InventoryOperation - Inventory operations │
│ TradeOperation - Trade operations │
└─────────────────────────────────────────────────────────────────┘
```
## Quick Start
### Basic Usage
```typescript
import {
TransactionManager,
MemoryStorage,
CurrencyOperation,
InventoryOperation,
} from '@esengine/transaction';
// Create transaction manager
const manager = new TransactionManager({
storage: new MemoryStorage(),
defaultTimeout: 10000,
});
// Execute transaction
const result = await manager.run((tx) => {
// Deduct gold
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
// Add item
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
}));
});
if (result.success) {
console.log('Purchase successful!');
} else {
console.log('Purchase failed:', result.error);
}
```
### Player Trading
```typescript
import { TradeOperation } from '@esengine/transaction';
const result = await manager.run((tx) => {
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
}));
}, { timeout: 30000 });
```
### Using Redis Storage
```typescript
import Redis from 'ioredis';
import { TransactionManager, RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({ client: redis });
const manager = new TransactionManager({ storage });
```
### Using MongoDB Storage
```typescript
import { MongoClient } from 'mongodb';
import { TransactionManager, MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({ db });
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
## Room Integration
```typescript
import { Room } from '@esengine/server';
import { withTransactions, CurrencyOperation, RedisStorage } from '@esengine/transaction';
class GameRoom extends withTransactions(Room, {
storage: new RedisStorage({ client: redisClient }),
}) {
@onMessage('Buy')
async handleBuy(data: { itemId: string }, player: Player) {
const result = await this.runTransaction((tx) => {
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: player.id,
currency: 'gold',
amount: getItemPrice(data.itemId),
}));
});
if (result.success) {
player.send('buy_success', { itemId: data.itemId });
} else {
player.send('buy_failed', { error: result.error });
}
}
}
```
## Documentation
- [Core Concepts](/en/modules/transaction/core/) - Transaction context, manager, Saga pattern
- [Storage Layer](/en/modules/transaction/storage/) - MemoryStorage, RedisStorage, MongoStorage
- [Operations](/en/modules/transaction/operations/) - Currency, inventory, trade operations
- [Distributed Transactions](/en/modules/transaction/distributed/) - Saga orchestrator, cross-server transactions
- [API Reference](/en/modules/transaction/api/) - Complete API documentation
## Service Tokens
For dependency injection:
```typescript
import {
TransactionManagerToken,
TransactionStorageToken,
} from '@esengine/transaction';
const manager = services.get(TransactionManagerToken);
```
## Best Practices
### 1. Operation Granularity
```typescript
// ✅ Good: Fine-grained operations, easy to rollback
tx.addOperation(new CurrencyOperation({ type: 'deduct', ... }));
tx.addOperation(new InventoryOperation({ type: 'add', ... }));
// ❌ Bad: Coarse-grained operation, hard to partially rollback
tx.addOperation(new ComplexPurchaseOperation({ ... }));
```
### 2. Timeout Settings
```typescript
// Simple operations: short timeout
await manager.run(tx => { ... }, { timeout: 5000 });
// Complex trades: longer timeout
await manager.run(tx => { ... }, { timeout: 30000 });
// Cross-server: even longer timeout
await manager.run(tx => { ... }, { timeout: 60000, distributed: true });
```
### 3. Error Handling
```typescript
const result = await manager.run((tx) => { ... });
if (!result.success) {
// Log the error
logger.error('Transaction failed', {
transactionId: result.transactionId,
error: result.error,
duration: result.duration,
});
// Notify user
player.send('error', { message: getErrorMessage(result.error) });
}
```

View File

@@ -0,0 +1,313 @@
---
title: "Operations"
description: "Built-in transaction operations: currency, inventory, trade"
---
## BaseOperation
Base class for all operations, providing a common implementation template.
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
class MyOperation extends BaseOperation<MyData, MyResult> {
readonly name = 'myOperation';
async validate(ctx: ITransactionContext): Promise<boolean> {
// Validate preconditions
return true;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// Execute operation
return this.success({ result: 'ok' });
// or
return this.failure('Something went wrong', 'ERROR_CODE');
}
async compensate(ctx: ITransactionContext): Promise<void> {
// Rollback operation
}
}
```
## CurrencyOperation
Handles currency addition and deduction.
### Deduct Currency
```typescript
import { CurrencyOperation } from '@esengine/transaction';
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
reason: 'purchase_item',
}));
```
### Add Currency
```typescript
tx.addOperation(new CurrencyOperation({
type: 'add',
playerId: 'player1',
currency: 'diamond',
amount: 50,
reason: 'daily_reward',
}));
```
### Operation Data
```typescript
interface CurrencyOperationData {
type: 'add' | 'deduct'; // Operation type
playerId: string; // Player ID
currency: string; // Currency type
amount: number; // Amount
reason?: string; // Reason/source
}
```
### Operation Result
```typescript
interface CurrencyOperationResult {
beforeBalance: number; // Balance before operation
afterBalance: number; // Balance after operation
}
```
### Custom Data Provider
```typescript
interface ICurrencyProvider {
getBalance(playerId: string, currency: string): Promise<number>;
setBalance(playerId: string, currency: string, amount: number): Promise<void>;
}
class MyCurrencyProvider implements ICurrencyProvider {
async getBalance(playerId: string, currency: string): Promise<number> {
// Get balance from database
return await db.getCurrency(playerId, currency);
}
async setBalance(playerId: string, currency: string, amount: number): Promise<void> {
// Save to database
await db.setCurrency(playerId, currency, amount);
}
}
// Use custom provider
const op = new CurrencyOperation({ ... });
op.setProvider(new MyCurrencyProvider());
tx.addOperation(op);
```
## InventoryOperation
Handles item addition, removal, and updates.
### Add Item
```typescript
import { InventoryOperation } from '@esengine/transaction';
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
properties: { enchant: 'fire' },
}));
```
### Remove Item
```typescript
tx.addOperation(new InventoryOperation({
type: 'remove',
playerId: 'player1',
itemId: 'potion_hp',
quantity: 5,
}));
```
### Update Item
```typescript
tx.addOperation(new InventoryOperation({
type: 'update',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1, // Optional, keeps original if not provided
properties: { enchant: 'lightning', level: 5 },
}));
```
### Operation Data
```typescript
interface InventoryOperationData {
type: 'add' | 'remove' | 'update'; // Operation type
playerId: string; // Player ID
itemId: string; // Item ID
quantity: number; // Quantity
properties?: Record<string, unknown>; // Item properties
reason?: string; // Reason/source
}
```
### Operation Result
```typescript
interface InventoryOperationResult {
beforeItem?: ItemData; // Item before operation
afterItem?: ItemData; // Item after operation
}
interface ItemData {
itemId: string;
quantity: number;
properties?: Record<string, unknown>;
}
```
### Custom Data Provider
```typescript
interface IInventoryProvider {
getItem(playerId: string, itemId: string): Promise<ItemData | null>;
setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void>;
hasCapacity?(playerId: string, count: number): Promise<boolean>;
}
class MyInventoryProvider implements IInventoryProvider {
async getItem(playerId: string, itemId: string): Promise<ItemData | null> {
return await db.getItem(playerId, itemId);
}
async setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void> {
if (item) {
await db.saveItem(playerId, itemId, item);
} else {
await db.deleteItem(playerId, itemId);
}
}
async hasCapacity(playerId: string, count: number): Promise<boolean> {
const current = await db.getItemCount(playerId);
const max = await db.getMaxCapacity(playerId);
return current + count <= max;
}
}
```
## TradeOperation
Handles item and currency exchange between players.
### Basic Usage
```typescript
import { TradeOperation } from '@esengine/transaction';
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
currencies: [{ currency: 'diamond', amount: 10 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
reason: 'player_trade',
}));
```
### Operation Data
```typescript
interface TradeOperationData {
tradeId: string; // Trade ID
partyA: TradeParty; // Trade initiator
partyB: TradeParty; // Trade receiver
reason?: string; // Reason/note
}
interface TradeParty {
playerId: string; // Player ID
items?: TradeItem[]; // Items to give
currencies?: TradeCurrency[]; // Currencies to give
}
interface TradeItem {
itemId: string;
quantity: number;
}
interface TradeCurrency {
currency: string;
amount: number;
}
```
### Execution Flow
TradeOperation internally generates the following sub-operation sequence:
```
1. Remove partyA's items
2. Add items to partyB (from partyA)
3. Deduct partyA's currencies
4. Add currencies to partyB (from partyA)
5. Remove partyB's items
6. Add items to partyA (from partyB)
7. Deduct partyB's currencies
8. Add currencies to partyA (from partyB)
```
If any step fails, all previous operations are rolled back.
### Using Custom Providers
```typescript
const op = new TradeOperation({ ... });
op.setProvider({
currencyProvider: new MyCurrencyProvider(),
inventoryProvider: new MyInventoryProvider(),
});
tx.addOperation(op);
```
## Factory Functions
Each operation class provides a factory function:
```typescript
import {
createCurrencyOperation,
createInventoryOperation,
createTradeOperation,
} from '@esengine/transaction';
tx.addOperation(createCurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
tx.addOperation(createInventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword',
quantity: 1,
}));
```

View File

@@ -0,0 +1,215 @@
---
title: "Storage Layer"
description: "Transaction storage interface and implementations: MemoryStorage, RedisStorage, MongoStorage"
---
## Storage Interface
All storage implementations must implement the `ITransactionStorage` interface:
```typescript
interface ITransactionStorage {
// Distributed lock
acquireLock(key: string, ttl: number): Promise<string | null>;
releaseLock(key: string, token: string): Promise<boolean>;
// Transaction log
saveTransaction(tx: TransactionLog): Promise<void>;
getTransaction(id: string): Promise<TransactionLog | null>;
updateTransactionState(id: string, state: TransactionState): Promise<void>;
updateOperationState(txId: string, opIndex: number, state: string, error?: string): Promise<void>;
getPendingTransactions(serverId?: string): Promise<TransactionLog[]>;
deleteTransaction(id: string): Promise<void>;
// Data operations
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
}
```
## MemoryStorage
In-memory storage, suitable for development and testing.
```typescript
import { MemoryStorage } from '@esengine/transaction';
const storage = new MemoryStorage({
maxTransactions: 1000, // Maximum transaction log count
});
const manager = new TransactionManager({ storage });
```
### Characteristics
- ✅ No external dependencies
- ✅ Fast, good for debugging
- ❌ Data only stored in memory
- ❌ No true distributed locking
- ❌ Data lost on restart
### Test Helpers
```typescript
// Clear all data
storage.clear();
// Get transaction count
console.log(storage.transactionCount);
```
## RedisStorage
Redis storage, suitable for production distributed systems.
```typescript
import Redis from 'ioredis';
import { RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({
client: redis,
prefix: 'tx:', // Key prefix
transactionTTL: 86400, // Transaction log TTL (seconds)
});
const manager = new TransactionManager({ storage });
```
### Characteristics
- ✅ High-performance distributed locking
- ✅ Fast read/write
- ✅ Supports TTL auto-expiration
- ✅ Suitable for high concurrency
- ❌ Requires Redis server
### Distributed Lock Implementation
Uses Redis `SET NX EX` for distributed locking:
```typescript
// Acquire lock (atomic operation)
SET tx:lock:player:123 <token> NX EX 10
// Release lock (Lua script for atomicity)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
```
### Key Structure
```
tx:lock:{key} - Distributed locks
tx:tx:{id} - Transaction logs
tx:server:{id}:txs - Server transaction index
tx:data:{key} - Business data
```
## MongoStorage
MongoDB storage, suitable for scenarios requiring persistence and complex queries.
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({
db,
transactionCollection: 'transactions', // Transaction log collection
dataCollection: 'transaction_data', // Business data collection
lockCollection: 'transaction_locks', // Lock collection
});
// Create indexes (run on first startup)
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
### Characteristics
- ✅ Persistent storage
- ✅ Supports complex queries
- ✅ Transaction logs are traceable
- ✅ Suitable for audit requirements
- ❌ Slightly lower performance than Redis
- ❌ Requires MongoDB server
### Index Structure
```javascript
// transactions collection
{ state: 1 }
{ 'metadata.serverId': 1 }
{ createdAt: 1 }
// transaction_locks collection
{ expireAt: 1 } // TTL index
// transaction_data collection
{ expireAt: 1 } // TTL index
```
### Distributed Lock Implementation
Uses MongoDB unique index for distributed locking:
```typescript
// Acquire lock
db.transaction_locks.insertOne({
_id: 'player:123',
token: '<token>',
expireAt: new Date(Date.now() + 10000)
});
// If key exists, check if expired
db.transaction_locks.updateOne(
{ _id: 'player:123', expireAt: { $lt: new Date() } },
{ $set: { token: '<token>', expireAt: new Date(Date.now() + 10000) } }
);
```
## Storage Selection Guide
| Scenario | Recommended Storage | Reason |
|----------|---------------------|--------|
| Development/Testing | MemoryStorage | No dependencies, fast startup |
| Single-machine Production | RedisStorage | High performance, simple |
| Distributed System | RedisStorage | True distributed locking |
| Audit Required | MongoStorage | Persistent logs |
| Mixed Requirements | Redis + Mongo | Redis for locks, Mongo for logs |
## Custom Storage
Implement `ITransactionStorage` interface to create custom storage:
```typescript
import { ITransactionStorage, TransactionLog, TransactionState } from '@esengine/transaction';
class MyCustomStorage implements ITransactionStorage {
async acquireLock(key: string, ttl: number): Promise<string | null> {
// Implement distributed lock acquisition
}
async releaseLock(key: string, token: string): Promise<boolean> {
// Implement distributed lock release
}
async saveTransaction(tx: TransactionLog): Promise<void> {
// Save transaction log
}
// ... implement other methods
}
```

View File

@@ -35,6 +35,7 @@ ESEngine 提供了丰富的功能模块,可以按需引入到你的项目中
| 模块 | 包名 | 描述 | | 模块 | 包名 | 描述 |
|------|------|------| |------|------|------|
| [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 | | [网络同步](/modules/network/) | `@esengine/network` | 多人游戏网络同步 |
| [事务系统](/modules/transaction/) | `@esengine/transaction` | 游戏事务处理,支持分布式事务 |
## 安装 ## 安装

View File

@@ -0,0 +1,261 @@
---
title: "核心概念"
description: "事务系统的核心概念事务上下文、事务管理器、Saga 模式"
---
## 事务状态
事务有以下几种状态:
```typescript
type TransactionState =
| 'pending' // 等待执行
| 'executing' // 执行中
| 'committed' // 已提交
| 'rolledback' // 已回滚
| 'failed' // 失败
```
## TransactionContext
事务上下文封装了事务的状态、操作和执行逻辑。
### 创建事务
```typescript
import { TransactionManager } from '@esengine/transaction';
const manager = new TransactionManager();
// 方式 1使用 begin() 手动管理
const tx = manager.begin({ timeout: 5000 });
tx.addOperation(op1);
tx.addOperation(op2);
const result = await tx.execute();
// 方式 2使用 run() 自动管理
const result = await manager.run((tx) => {
tx.addOperation(op1);
tx.addOperation(op2);
});
```
### 链式添加操作
```typescript
const result = await manager.run((tx) => {
tx.addOperation(new CurrencyOperation({ ... }))
.addOperation(new InventoryOperation({ ... }))
.addOperation(new InventoryOperation({ ... }));
});
```
### 上下文数据
操作之间可以通过上下文共享数据:
```typescript
class CustomOperation extends BaseOperation<MyData, MyResult> {
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// 读取之前操作设置的数据
const previousResult = ctx.get<number>('previousValue');
// 设置数据供后续操作使用
ctx.set('myResult', { value: 123 });
return this.success({ ... });
}
}
```
## TransactionManager
事务管理器负责创建、执行和恢复事务。
### 配置选项
```typescript
interface TransactionManagerConfig {
storage?: ITransactionStorage; // 存储实例
defaultTimeout?: number; // 默认超时(毫秒)
serverId?: string; // 服务器 ID分布式用
autoRecover?: boolean; // 自动恢复未完成事务
}
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
defaultTimeout: 10000,
serverId: 'server-1',
autoRecover: true,
});
```
### 分布式锁
```typescript
// 获取锁
const token = await manager.acquireLock('player:123:inventory', 10000);
if (token) {
try {
// 执行操作
await doSomething();
} finally {
// 释放锁
await manager.releaseLock('player:123:inventory', token);
}
}
// 或使用 withLock 简化
await manager.withLock('player:123:inventory', async () => {
await doSomething();
}, 10000);
```
### 事务恢复
服务器重启时恢复未完成的事务:
```typescript
const manager = new TransactionManager({
storage: new RedisStorage({ client: redis }),
serverId: 'server-1',
});
// 恢复未完成的事务
const recoveredCount = await manager.recover();
console.log(`Recovered ${recoveredCount} transactions`);
```
## Saga 模式
事务系统采用 Saga 模式,每个操作必须实现 `execute``compensate` 方法:
```typescript
interface ITransactionOperation<TData, TResult> {
readonly name: string;
readonly data: TData;
// 验证前置条件
validate(ctx: ITransactionContext): Promise<boolean>;
// 正向执行
execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>;
// 补偿操作(回滚)
compensate(ctx: ITransactionContext): Promise<void>;
}
```
### 执行流程
```
开始事务
┌─────────────────────┐
│ validate(op1) │──失败──► 返回失败
└─────────────────────┘
│成功
┌─────────────────────┐
│ execute(op1) │──失败──┐
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ validate(op2) │──失败──┤
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ execute(op2) │──失败──┤
└─────────────────────┘ │
│成功 ▼
▼ ┌─────────────────────┐
提交事务 │ compensate(op1) │
└─────────────────────┘
返回失败(已回滚)
```
### 自定义操作
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
interface UpgradeData {
playerId: string;
itemId: string;
targetLevel: number;
}
interface UpgradeResult {
newLevel: number;
}
class UpgradeOperation extends BaseOperation<UpgradeData, UpgradeResult> {
readonly name = 'upgrade';
private _previousLevel: number = 0;
async validate(ctx: ITransactionContext): Promise<boolean> {
// 验证物品存在且可升级
const item = await this.getItem(ctx);
return item !== null && item.level < this.data.targetLevel;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<UpgradeResult>> {
const item = await this.getItem(ctx);
if (!item) {
return this.failure('Item not found', 'ITEM_NOT_FOUND');
}
this._previousLevel = item.level;
item.level = this.data.targetLevel;
await this.saveItem(ctx, item);
return this.success({ newLevel: item.level });
}
async compensate(ctx: ITransactionContext): Promise<void> {
const item = await this.getItem(ctx);
if (item) {
item.level = this._previousLevel;
await this.saveItem(ctx, item);
}
}
private async getItem(ctx: ITransactionContext) {
// 从存储获取物品
}
private async saveItem(ctx: ITransactionContext, item: any) {
// 保存物品到存储
}
}
```
## 事务结果
```typescript
interface TransactionResult<T = unknown> {
success: boolean; // 是否成功
transactionId: string; // 事务 ID
results: OperationResult[]; // 各操作结果
data?: T; // 最终数据
error?: string; // 错误信息
duration: number; // 执行时间(毫秒)
}
const result = await manager.run((tx) => { ... });
console.log(`Transaction ${result.transactionId}`);
console.log(`Success: ${result.success}`);
console.log(`Duration: ${result.duration}ms`);
if (!result.success) {
console.log(`Error: ${result.error}`);
}
```

View File

@@ -0,0 +1,355 @@
---
title: "分布式事务"
description: "Saga 编排器和跨服务器事务支持"
---
## Saga 编排器
`SagaOrchestrator` 用于编排跨服务器的分布式事务。
### 基本用法
```typescript
import { SagaOrchestrator, RedisStorage } from '@esengine/transaction';
const orchestrator = new SagaOrchestrator({
storage: new RedisStorage({ client: redis }),
timeout: 30000,
serverId: 'orchestrator-1',
});
const result = await orchestrator.execute([
{
name: 'deduct_currency',
serverId: 'game-server-1',
data: { playerId: 'player1', amount: 100 },
execute: async (data) => {
// 调用游戏服务器 API 扣除货币
const response = await gameServerApi.deductCurrency(data);
return { success: response.ok };
},
compensate: async (data) => {
// 调用游戏服务器 API 恢复货币
await gameServerApi.addCurrency(data);
},
},
{
name: 'add_item',
serverId: 'inventory-server-1',
data: { playerId: 'player1', itemId: 'sword' },
execute: async (data) => {
const response = await inventoryServerApi.addItem(data);
return { success: response.ok };
},
compensate: async (data) => {
await inventoryServerApi.removeItem(data);
},
},
]);
if (result.success) {
console.log('Saga completed successfully');
} else {
console.log('Saga failed:', result.error);
console.log('Completed steps:', result.completedSteps);
console.log('Failed at:', result.failedStep);
}
```
### 配置选项
```typescript
interface SagaOrchestratorConfig {
storage?: ITransactionStorage; // 存储实例
timeout?: number; // 超时时间(毫秒)
serverId?: string; // 编排器服务器 ID
}
```
### Saga 步骤
```typescript
interface SagaStep<T = unknown> {
name: string; // 步骤名称
serverId?: string; // 目标服务器 ID
data: T; // 步骤数据
execute: (data: T) => Promise<OperationResult>; // 执行函数
compensate: (data: T) => Promise<void>; // 补偿函数
}
```
### Saga 结果
```typescript
interface SagaResult {
success: boolean; // 是否成功
sagaId: string; // Saga ID
completedSteps: string[]; // 已完成的步骤
failedStep?: string; // 失败的步骤
error?: string; // 错误信息
duration: number; // 执行时间(毫秒)
}
```
## 执行流程
```
开始 Saga
┌─────────────────────┐
│ Step 1: execute │──失败──┐
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ Step 2: execute │──失败──┤
└─────────────────────┘ │
│成功 │
▼ │
┌─────────────────────┐ │
│ Step 3: execute │──失败──┤
└─────────────────────┘ │
│成功 ▼
▼ ┌─────────────────────┐
Saga 完成 │ Step 2: compensate │
└─────────────────────┘
┌─────────────────────┐
│ Step 1: compensate │
└─────────────────────┘
Saga 失败(已补偿)
```
## Saga 日志
编排器会记录详细的执行日志:
```typescript
interface SagaLog {
id: string; // Saga ID
state: SagaLogState; // 状态
steps: SagaStepLog[]; // 步骤日志
createdAt: number; // 创建时间
updatedAt: number; // 更新时间
metadata?: Record<string, unknown>;
}
type SagaLogState =
| 'pending' // 等待执行
| 'running' // 执行中
| 'completed' // 已完成
| 'compensating' // 补偿中
| 'compensated' // 已补偿
| 'failed' // 失败
interface SagaStepLog {
name: string; // 步骤名称
serverId?: string; // 服务器 ID
state: SagaStepState; // 状态
startedAt?: number; // 开始时间
completedAt?: number; // 完成时间
error?: string; // 错误信息
}
type SagaStepState =
| 'pending' // 等待执行
| 'executing' // 执行中
| 'completed' // 已完成
| 'compensating' // 补偿中
| 'compensated' // 已补偿
| 'failed' // 失败
```
### 查询 Saga 日志
```typescript
const log = await orchestrator.getSagaLog('saga_xxx');
if (log) {
console.log('Saga state:', log.state);
for (const step of log.steps) {
console.log(` ${step.name}: ${step.state}`);
}
}
```
## 跨服务器事务示例
### 场景:跨服购买
玩家在游戏服务器购买物品,货币在账户服务器,物品在背包服务器。
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'purchase-orchestrator',
});
async function crossServerPurchase(
playerId: string,
itemId: string,
price: number
): Promise<SagaResult> {
return orchestrator.execute([
// 步骤 1在账户服务器扣款
{
name: 'deduct_balance',
serverId: 'account-server',
data: { playerId, amount: price },
execute: async (data) => {
const result = await accountService.deduct(data.playerId, data.amount);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await accountService.refund(data.playerId, data.amount);
},
},
// 步骤 2在背包服务器添加物品
{
name: 'add_item',
serverId: 'inventory-server',
data: { playerId, itemId },
execute: async (data) => {
const result = await inventoryService.addItem(data.playerId, data.itemId);
return { success: result.ok, error: result.error };
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
},
// 步骤 3记录购买日志
{
name: 'log_purchase',
serverId: 'log-server',
data: { playerId, itemId, price, timestamp: Date.now() },
execute: async (data) => {
await logService.recordPurchase(data);
return { success: true };
},
compensate: async (data) => {
await logService.cancelPurchase(data);
},
},
]);
}
```
### 场景:跨服交易
两个玩家在不同服务器上进行交易。
```typescript
async function crossServerTrade(
playerA: { id: string; server: string; items: string[] },
playerB: { id: string; server: string; items: string[] }
): Promise<SagaResult> {
const steps: SagaStep[] = [];
// 移除 A 的物品
for (const itemId of playerA.items) {
steps.push({
name: `remove_${playerA.id}_${itemId}`,
serverId: playerA.server,
data: { playerId: playerA.id, itemId },
execute: async (data) => {
return await inventoryService.removeItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.addItem(data.playerId, data.itemId);
},
});
}
// 添加物品到 B
for (const itemId of playerA.items) {
steps.push({
name: `add_${playerB.id}_${itemId}`,
serverId: playerB.server,
data: { playerId: playerB.id, itemId },
execute: async (data) => {
return await inventoryService.addItem(data.playerId, data.itemId);
},
compensate: async (data) => {
await inventoryService.removeItem(data.playerId, data.itemId);
},
});
}
// 类似地处理 B 的物品...
return orchestrator.execute(steps);
}
```
## 恢复未完成的 Saga
服务器重启后恢复未完成的 Saga
```typescript
const orchestrator = new SagaOrchestrator({
storage: redisStorage,
serverId: 'my-orchestrator',
});
// 恢复未完成的 Saga会执行补偿
const recoveredCount = await orchestrator.recover();
console.log(`Recovered ${recoveredCount} sagas`);
```
## 最佳实践
### 1. 幂等性
确保所有操作都是幂等的:
```typescript
{
execute: async (data) => {
// 使用唯一 ID 确保幂等
const result = await service.process(data.requestId, data);
return { success: result.ok };
},
compensate: async (data) => {
// 补偿也要幂等
await service.rollback(data.requestId);
},
}
```
### 2. 超时处理
设置合适的超时时间:
```typescript
const orchestrator = new SagaOrchestrator({
timeout: 60000, // 跨服务器操作需要更长超时
});
```
### 3. 监控和告警
记录 Saga 执行结果:
```typescript
const result = await orchestrator.execute(steps);
if (!result.success) {
// 发送告警
alertService.send({
type: 'saga_failed',
sagaId: result.sagaId,
failedStep: result.failedStep,
error: result.error,
});
// 记录详细日志
const log = await orchestrator.getSagaLog(result.sagaId);
logger.error('Saga failed', { log });
}
```

View File

@@ -0,0 +1,238 @@
---
title: "事务系统 (Transaction)"
description: "游戏事务处理系统,支持商店购买、玩家交易、分布式事务"
---
`@esengine/transaction` 提供完整的游戏事务处理能力,基于 Saga 模式实现,支持商店购买、玩家交易、多步骤任务等场景,并提供 Redis/MongoDB 分布式事务支持。
## 概述
事务系统解决游戏中常见的数据一致性问题:
| 场景 | 问题 | 解决方案 |
|------|------|----------|
| 商店购买 | 扣款成功但物品未发放 | 原子事务,失败自动回滚 |
| 玩家交易 | 一方物品转移另一方未收到 | Saga 补偿机制 |
| 跨服操作 | 多服务器数据不一致 | 分布式锁 + 事务日志 |
## 安装
```bash
npm install @esengine/transaction
```
可选依赖(根据存储需求安装):
```bash
npm install ioredis # Redis 存储
npm install mongodb # MongoDB 存储
```
## 架构
```
┌─────────────────────────────────────────────────────────────────┐
│ Transaction Layer │
├─────────────────────────────────────────────────────────────────┤
│ TransactionManager - 事务管理器,协调事务生命周期 │
│ TransactionContext - 事务上下文,封装操作和状态 │
│ SagaOrchestrator - 分布式 Saga 编排器 │
├─────────────────────────────────────────────────────────────────┤
│ Storage Layer │
├─────────────────────────────────────────────────────────────────┤
│ MemoryStorage - 内存存储(开发/测试) │
│ RedisStorage - Redis分布式锁 + 缓存) │
│ MongoStorage - MongoDB持久化日志
├─────────────────────────────────────────────────────────────────┤
│ Operation Layer │
├─────────────────────────────────────────────────────────────────┤
│ CurrencyOperation - 货币操作 │
│ InventoryOperation - 背包操作 │
│ TradeOperation - 交易操作 │
└─────────────────────────────────────────────────────────────────┘
```
## 快速开始
### 基础用法
```typescript
import {
TransactionManager,
MemoryStorage,
CurrencyOperation,
InventoryOperation,
} from '@esengine/transaction';
// 创建事务管理器
const manager = new TransactionManager({
storage: new MemoryStorage(),
defaultTimeout: 10000,
});
// 执行事务
const result = await manager.run((tx) => {
// 扣除金币
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
// 添加物品
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
}));
});
if (result.success) {
console.log('购买成功!');
} else {
console.log('购买失败:', result.error);
}
```
### 玩家交易
```typescript
import { TradeOperation } from '@esengine/transaction';
const result = await manager.run((tx) => {
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
}));
}, { timeout: 30000 });
```
### 使用 Redis 存储
```typescript
import Redis from 'ioredis';
import { TransactionManager, RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({ client: redis });
const manager = new TransactionManager({ storage });
```
### 使用 MongoDB 存储
```typescript
import { MongoClient } from 'mongodb';
import { TransactionManager, MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({ db });
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
## 与 Room 集成
```typescript
import { Room } from '@esengine/server';
import { withTransactions, CurrencyOperation, RedisStorage } from '@esengine/transaction';
class GameRoom extends withTransactions(Room, {
storage: new RedisStorage({ client: redisClient }),
}) {
@onMessage('Buy')
async handleBuy(data: { itemId: string }, player: Player) {
const result = await this.runTransaction((tx) => {
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: player.id,
currency: 'gold',
amount: getItemPrice(data.itemId),
}));
});
if (result.success) {
player.send('buy_success', { itemId: data.itemId });
} else {
player.send('buy_failed', { error: result.error });
}
}
}
```
## 文档导航
- [核心概念](/modules/transaction/core/) - 事务上下文、管理器、Saga 模式
- [存储层](/modules/transaction/storage/) - MemoryStorage、RedisStorage、MongoStorage
- [操作类](/modules/transaction/operations/) - 货币、背包、交易操作
- [分布式事务](/modules/transaction/distributed/) - Saga 编排器、跨服务器事务
- [API 参考](/modules/transaction/api/) - 完整 API 文档
## 服务令牌
用于依赖注入:
```typescript
import {
TransactionManagerToken,
TransactionStorageToken,
} from '@esengine/transaction';
const manager = services.get(TransactionManagerToken);
```
## 最佳实践
### 1. 操作粒度
```typescript
// ✅ 好:细粒度操作,便于回滚
tx.addOperation(new CurrencyOperation({ type: 'deduct', ... }));
tx.addOperation(new InventoryOperation({ type: 'add', ... }));
// ❌ 差:粗粒度操作,难以部分回滚
tx.addOperation(new ComplexPurchaseOperation({ ... }));
```
### 2. 超时设置
```typescript
// 简单操作:短超时
await manager.run(tx => { ... }, { timeout: 5000 });
// 复杂交易:长超时
await manager.run(tx => { ... }, { timeout: 30000 });
// 跨服务器:更长超时
await manager.run(tx => { ... }, { timeout: 60000, distributed: true });
```
### 3. 错误处理
```typescript
const result = await manager.run((tx) => { ... });
if (!result.success) {
// 记录日志
logger.error('Transaction failed', {
transactionId: result.transactionId,
error: result.error,
duration: result.duration,
});
// 通知用户
player.send('error', { message: getErrorMessage(result.error) });
}
```

View File

@@ -0,0 +1,313 @@
---
title: "操作类"
description: "内置的事务操作:货币、背包、交易"
---
## BaseOperation
所有操作类的基类,提供通用的实现模板。
```typescript
import { BaseOperation, ITransactionContext, OperationResult } from '@esengine/transaction';
class MyOperation extends BaseOperation<MyData, MyResult> {
readonly name = 'myOperation';
async validate(ctx: ITransactionContext): Promise<boolean> {
// 验证前置条件
return true;
}
async execute(ctx: ITransactionContext): Promise<OperationResult<MyResult>> {
// 执行操作
return this.success({ result: 'ok' });
// 或
return this.failure('Something went wrong', 'ERROR_CODE');
}
async compensate(ctx: ITransactionContext): Promise<void> {
// 回滚操作
}
}
```
## CurrencyOperation
处理货币的增加和扣除。
### 扣除货币
```typescript
import { CurrencyOperation } from '@esengine/transaction';
tx.addOperation(new CurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
reason: 'purchase_item',
}));
```
### 增加货币
```typescript
tx.addOperation(new CurrencyOperation({
type: 'add',
playerId: 'player1',
currency: 'diamond',
amount: 50,
reason: 'daily_reward',
}));
```
### 操作数据
```typescript
interface CurrencyOperationData {
type: 'add' | 'deduct'; // 操作类型
playerId: string; // 玩家 ID
currency: string; // 货币类型
amount: number; // 数量
reason?: string; // 原因/来源
}
```
### 操作结果
```typescript
interface CurrencyOperationResult {
beforeBalance: number; // 操作前余额
afterBalance: number; // 操作后余额
}
```
### 自定义数据提供者
```typescript
interface ICurrencyProvider {
getBalance(playerId: string, currency: string): Promise<number>;
setBalance(playerId: string, currency: string, amount: number): Promise<void>;
}
class MyCurrencyProvider implements ICurrencyProvider {
async getBalance(playerId: string, currency: string): Promise<number> {
// 从数据库获取余额
return await db.getCurrency(playerId, currency);
}
async setBalance(playerId: string, currency: string, amount: number): Promise<void> {
// 保存到数据库
await db.setCurrency(playerId, currency, amount);
}
}
// 使用自定义提供者
const op = new CurrencyOperation({ ... });
op.setProvider(new MyCurrencyProvider());
tx.addOperation(op);
```
## InventoryOperation
处理物品的添加、移除和更新。
### 添加物品
```typescript
import { InventoryOperation } from '@esengine/transaction';
tx.addOperation(new InventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1,
properties: { enchant: 'fire' },
}));
```
### 移除物品
```typescript
tx.addOperation(new InventoryOperation({
type: 'remove',
playerId: 'player1',
itemId: 'potion_hp',
quantity: 5,
}));
```
### 更新物品
```typescript
tx.addOperation(new InventoryOperation({
type: 'update',
playerId: 'player1',
itemId: 'sword_001',
quantity: 1, // 可选,不传则保持原数量
properties: { enchant: 'lightning', level: 5 },
}));
```
### 操作数据
```typescript
interface InventoryOperationData {
type: 'add' | 'remove' | 'update'; // 操作类型
playerId: string; // 玩家 ID
itemId: string; // 物品 ID
quantity: number; // 数量
properties?: Record<string, unknown>; // 物品属性
reason?: string; // 原因/来源
}
```
### 操作结果
```typescript
interface InventoryOperationResult {
beforeItem?: ItemData; // 操作前物品
afterItem?: ItemData; // 操作后物品
}
interface ItemData {
itemId: string;
quantity: number;
properties?: Record<string, unknown>;
}
```
### 自定义数据提供者
```typescript
interface IInventoryProvider {
getItem(playerId: string, itemId: string): Promise<ItemData | null>;
setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void>;
hasCapacity?(playerId: string, count: number): Promise<boolean>;
}
class MyInventoryProvider implements IInventoryProvider {
async getItem(playerId: string, itemId: string): Promise<ItemData | null> {
return await db.getItem(playerId, itemId);
}
async setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void> {
if (item) {
await db.saveItem(playerId, itemId, item);
} else {
await db.deleteItem(playerId, itemId);
}
}
async hasCapacity(playerId: string, count: number): Promise<boolean> {
const current = await db.getItemCount(playerId);
const max = await db.getMaxCapacity(playerId);
return current + count <= max;
}
}
```
## TradeOperation
处理玩家之间的物品和货币交换。
### 基本用法
```typescript
import { TradeOperation } from '@esengine/transaction';
tx.addOperation(new TradeOperation({
tradeId: 'trade_001',
partyA: {
playerId: 'player1',
items: [{ itemId: 'sword', quantity: 1 }],
currencies: [{ currency: 'diamond', amount: 10 }],
},
partyB: {
playerId: 'player2',
currencies: [{ currency: 'gold', amount: 1000 }],
},
reason: 'player_trade',
}));
```
### 操作数据
```typescript
interface TradeOperationData {
tradeId: string; // 交易 ID
partyA: TradeParty; // 交易发起方
partyB: TradeParty; // 交易接收方
reason?: string; // 原因/备注
}
interface TradeParty {
playerId: string; // 玩家 ID
items?: TradeItem[]; // 给出的物品
currencies?: TradeCurrency[]; // 给出的货币
}
interface TradeItem {
itemId: string;
quantity: number;
}
interface TradeCurrency {
currency: string;
amount: number;
}
```
### 执行流程
TradeOperation 内部会生成以下子操作序列:
```
1. 移除 partyA 的物品
2. 添加 partyB 的物品(来自 partyA
3. 扣除 partyA 的货币
4. 增加 partyB 的货币(来自 partyA
5. 移除 partyB 的物品
6. 添加 partyA 的物品(来自 partyB
7. 扣除 partyB 的货币
8. 增加 partyA 的货币(来自 partyB
```
任何一步失败都会回滚之前的所有操作。
### 使用自定义提供者
```typescript
const op = new TradeOperation({ ... });
op.setProvider({
currencyProvider: new MyCurrencyProvider(),
inventoryProvider: new MyInventoryProvider(),
});
tx.addOperation(op);
```
## 创建工厂函数
每个操作类都提供工厂函数:
```typescript
import {
createCurrencyOperation,
createInventoryOperation,
createTradeOperation,
} from '@esengine/transaction';
tx.addOperation(createCurrencyOperation({
type: 'deduct',
playerId: 'player1',
currency: 'gold',
amount: 100,
}));
tx.addOperation(createInventoryOperation({
type: 'add',
playerId: 'player1',
itemId: 'sword',
quantity: 1,
}));
```

View File

@@ -0,0 +1,215 @@
---
title: "存储层"
description: "事务存储接口和实现MemoryStorage、RedisStorage、MongoStorage"
---
## 存储接口
所有存储实现都需要实现 `ITransactionStorage` 接口:
```typescript
interface ITransactionStorage {
// 分布式锁
acquireLock(key: string, ttl: number): Promise<string | null>;
releaseLock(key: string, token: string): Promise<boolean>;
// 事务日志
saveTransaction(tx: TransactionLog): Promise<void>;
getTransaction(id: string): Promise<TransactionLog | null>;
updateTransactionState(id: string, state: TransactionState): Promise<void>;
updateOperationState(txId: string, opIndex: number, state: string, error?: string): Promise<void>;
getPendingTransactions(serverId?: string): Promise<TransactionLog[]>;
deleteTransaction(id: string): Promise<void>;
// 数据操作
get<T>(key: string): Promise<T | null>;
set<T>(key: string, value: T, ttl?: number): Promise<void>;
delete(key: string): Promise<boolean>;
}
```
## MemoryStorage
内存存储,适用于开发和测试环境。
```typescript
import { MemoryStorage } from '@esengine/transaction';
const storage = new MemoryStorage({
maxTransactions: 1000, // 最大事务日志数量
});
const manager = new TransactionManager({ storage });
```
### 特点
- ✅ 无需外部依赖
- ✅ 快速,适合开发调试
- ❌ 数据仅保存在内存中
- ❌ 不支持真正的分布式锁
- ❌ 服务重启后数据丢失
### 测试辅助
```typescript
// 清空所有数据
storage.clear();
// 获取事务数量
console.log(storage.transactionCount);
```
## RedisStorage
Redis 存储,适用于生产环境的分布式系统。
```typescript
import Redis from 'ioredis';
import { RedisStorage } from '@esengine/transaction';
const redis = new Redis('redis://localhost:6379');
const storage = new RedisStorage({
client: redis,
prefix: 'tx:', // 键前缀
transactionTTL: 86400, // 事务日志过期时间(秒)
});
const manager = new TransactionManager({ storage });
```
### 特点
- ✅ 高性能分布式锁
- ✅ 快速读写
- ✅ 支持 TTL 自动过期
- ✅ 适合高并发场景
- ❌ 需要 Redis 服务器
### 分布式锁实现
使用 Redis `SET NX EX` 实现分布式锁:
```typescript
// 获取锁(原子操作)
SET tx:lock:player:123 <token> NX EX 10
// 释放锁Lua 脚本保证原子性)
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
```
### 键结构
```
tx:lock:{key} - 分布式锁
tx:tx:{id} - 事务日志
tx:server:{id}:txs - 服务器事务索引
tx:data:{key} - 业务数据
```
## MongoStorage
MongoDB 存储,适用于需要持久化和复杂查询的场景。
```typescript
import { MongoClient } from 'mongodb';
import { MongoStorage } from '@esengine/transaction';
const client = new MongoClient('mongodb://localhost:27017');
await client.connect();
const db = client.db('game');
const storage = new MongoStorage({
db,
transactionCollection: 'transactions', // 事务日志集合
dataCollection: 'transaction_data', // 业务数据集合
lockCollection: 'transaction_locks', // 锁集合
});
// 创建索引(首次运行时执行)
await storage.ensureIndexes();
const manager = new TransactionManager({ storage });
```
### 特点
- ✅ 持久化存储
- ✅ 支持复杂查询
- ✅ 事务日志可追溯
- ✅ 适合需要审计的场景
- ❌ 相比 Redis 性能略低
- ❌ 需要 MongoDB 服务器
### 索引结构
```javascript
// transactions 集合
{ state: 1 }
{ 'metadata.serverId': 1 }
{ createdAt: 1 }
// transaction_locks 集合
{ expireAt: 1 } // TTL 索引
// transaction_data 集合
{ expireAt: 1 } // TTL 索引
```
### 分布式锁实现
使用 MongoDB 唯一索引实现分布式锁:
```typescript
// 获取锁
db.transaction_locks.insertOne({
_id: 'player:123',
token: '<token>',
expireAt: new Date(Date.now() + 10000)
});
// 如果键已存在,检查是否过期
db.transaction_locks.updateOne(
{ _id: 'player:123', expireAt: { $lt: new Date() } },
{ $set: { token: '<token>', expireAt: new Date(Date.now() + 10000) } }
);
```
## 存储选择指南
| 场景 | 推荐存储 | 理由 |
|------|----------|------|
| 开发/测试 | MemoryStorage | 无依赖,快速启动 |
| 单机生产 | RedisStorage | 高性能,简单 |
| 分布式系统 | RedisStorage | 真正的分布式锁 |
| 需要审计 | MongoStorage | 持久化日志 |
| 混合需求 | Redis + Mongo | Redis 做锁Mongo 做日志 |
## 自定义存储
实现 `ITransactionStorage` 接口创建自定义存储:
```typescript
import { ITransactionStorage, TransactionLog, TransactionState } from '@esengine/transaction';
class MyCustomStorage implements ITransactionStorage {
async acquireLock(key: string, ttl: number): Promise<string | null> {
// 实现分布式锁获取逻辑
}
async releaseLock(key: string, token: string): Promise<boolean> {
// 实现分布式锁释放逻辑
}
async saveTransaction(tx: TransactionLog): Promise<void> {
// 保存事务日志
}
// ... 实现其他方法
}
```

View File

@@ -0,0 +1,23 @@
{
"id": "transaction",
"name": "@esengine/transaction",
"globalKey": "transaction",
"displayName": "Transaction System",
"description": "游戏事务系统,支持商店购买、玩家交易、分布式事务 | Game transaction system for shop, trading, distributed transactions",
"version": "1.0.0",
"category": "Other",
"icon": "Receipt",
"tags": ["transaction", "distributed", "saga", "database"],
"isCore": false,
"defaultEnabled": false,
"isEngineModule": true,
"canContainContent": false,
"platforms": ["nodejs"],
"dependencies": ["core", "server"],
"exports": {
"services": ["TransactionManager"],
"interfaces": ["ITransactionStorage", "ITransactionOperation"]
},
"requiresWasm": false,
"outputPath": "dist/index.js"
}

View File

@@ -0,0 +1,50 @@
{
"name": "@esengine/transaction",
"version": "1.0.0",
"description": "Game transaction system with distributed support | 游戏事务系统,支持分布式事务",
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": [
"dist",
"module.json"
],
"scripts": {
"build": "tsup && tsc -p tsconfig.build.json --emitDeclarationOnly",
"build:watch": "tsup --watch",
"type-check": "tsc --noEmit",
"clean": "rimraf dist"
},
"dependencies": {
"@esengine/server": "workspace:*"
},
"peerDependencies": {
"ioredis": "^5.3.0",
"mongodb": "^6.0.0"
},
"peerDependenciesMeta": {
"ioredis": {
"optional": true
},
"mongodb": {
"optional": true
}
},
"devDependencies": {
"@esengine/ecs-framework": "workspace:*",
"@esengine/build-config": "workspace:*",
"tsup": "^8.0.0",
"typescript": "^5.8.0",
"rimraf": "^5.0.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,286 @@
/**
* @zh 事务上下文实现
* @en Transaction context implementation
*/
import type {
ITransactionContext,
ITransactionOperation,
ITransactionStorage,
TransactionState,
TransactionResult,
TransactionOptions,
TransactionLog,
OperationLog,
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)}`
}
/**
* @zh 事务上下文
* @en Transaction context
*
* @zh 封装事务的状态、操作和执行逻辑
* @en Encapsulates transaction state, operations, and execution logic
*
* @example
* ```typescript
* const ctx = new TransactionContext({ timeout: 5000 })
* ctx.addOperation(new DeductCurrency({ playerId: '1', amount: 100 }))
* ctx.addOperation(new AddItem({ playerId: '1', itemId: 'sword' }))
* const result = await ctx.execute()
* ```
*/
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
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
}
// =========================================================================
// 只读属性 | Readonly properties
// =========================================================================
get id(): string {
return this._id
}
get state(): TransactionState {
return this._state
}
get timeout(): number {
return this._timeout
}
get operations(): ReadonlyArray<ITransactionOperation> {
return this._operations
}
get storage(): ITransactionStorage | null {
return this._storage
}
get metadata(): Record<string, unknown> {
return this._metadata
}
// =========================================================================
// 公共方法 | Public methods
// =========================================================================
/**
* @zh 添加操作
* @en Add operation
*/
addOperation<T extends ITransactionOperation>(operation: T): this {
if (this._state !== 'pending') {
throw new Error(`Cannot add operation to transaction in state: ${this._state}`)
}
this._operations.push(operation)
return this
}
/**
* @zh 执行事务
* @en Execute transaction
*/
async execute<T = unknown>(): Promise<TransactionResult<T>> {
if (this._state !== 'pending') {
return {
success: false,
transactionId: this._id,
results: [],
error: `Transaction already in state: ${this._state}`,
duration: 0,
}
}
this._startTime = Date.now()
this._state = 'executing'
const results: OperationResult[] = []
let executedCount = 0
try {
await this._saveLog()
for (let i = 0; i < this._operations.length; i++) {
if (this._isTimedOut()) {
throw new Error('Transaction timed out')
}
const op = this._operations[i]
const isValid = await op.validate(this)
if (!isValid) {
throw new Error(`Validation failed for operation: ${op.name}`)
}
const result = await op.execute(this)
results.push(result)
executedCount++
await this._updateOperationLog(i, 'executed')
if (!result.success) {
throw new Error(result.error ?? `Operation ${op.name} failed`)
}
}
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,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
await this._compensate(executedCount - 1)
return {
success: false,
transactionId: this._id,
results,
error: errorMessage,
duration: Date.now() - this._startTime,
}
}
}
/**
* @zh 手动回滚事务
* @en Manually rollback transaction
*/
async rollback(): Promise<void> {
if (this._state === 'committed' || this._state === 'rolledback') {
return
}
await this._compensate(this._operations.length - 1)
}
/**
* @zh 获取上下文数据
* @en Get context data
*/
get<T>(key: string): T | undefined {
return this._contextData.get(key) as T | undefined
}
/**
* @zh 设置上下文数据
* @en Set context data
*/
set<T>(key: string, value: T): void {
this._contextData.set(key, value)
}
// =========================================================================
// 私有方法 | Private methods
// =========================================================================
private _isTimedOut(): boolean {
return Date.now() - this._startTime > this._timeout
}
private async _compensate(fromIndex: number): Promise<void> {
this._state = 'rolledback'
for (let i = fromIndex; i >= 0; i--) {
const op = this._operations[i]
try {
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)
}
}
await this._updateTransactionState('rolledback')
}
private async _saveLog(): Promise<void> {
if (!this._storage) return
const log: TransactionLog = {
id: this._id,
state: this._state,
createdAt: this._startTime,
updatedAt: this._startTime,
timeout: this._timeout,
operations: this._operations.map((op) => ({
name: op.name,
data: op.data,
state: 'pending' as const,
})),
metadata: this._metadata,
distributed: this._distributed,
}
await this._storage.saveTransaction(log)
}
private async _updateTransactionState(state: TransactionState): Promise<void> {
this._state = state
if (this._storage) {
await this._storage.updateTransactionState(this._id, state)
}
}
private async _updateOperationLog(
index: number,
state: OperationLog['state'],
error?: string
): Promise<void> {
if (this._storage) {
await this._storage.updateOperationState(this._id, index, state, error)
}
}
private _collectResultData(results: OperationResult[]): unknown {
const data: Record<string, unknown> = {}
for (const result of results) {
if (result.data !== undefined) {
Object.assign(data, result.data)
}
}
return Object.keys(data).length > 0 ? data : undefined
}
}
/**
* @zh 创建事务上下文
* @en Create transaction context
*/
export function createTransactionContext(
options: TransactionOptions & { storage?: ITransactionStorage } = {}
): ITransactionContext {
return new TransactionContext(options)
}

View File

@@ -0,0 +1,255 @@
/**
* @zh 事务管理器
* @en Transaction manager
*/
import type {
ITransactionContext,
ITransactionStorage,
TransactionManagerConfig,
TransactionOptions,
TransactionLog,
TransactionResult,
} from './types.js'
import { TransactionContext } from './TransactionContext.js'
/**
* @zh 事务管理器
* @en Transaction manager
*
* @zh 管理事务的创建、执行和恢复
* @en Manages transaction creation, execution, and recovery
*
* @example
* ```typescript
* const manager = new TransactionManager({
* storage: new RedisStorage({ url: 'redis://localhost:6379' }),
* defaultTimeout: 10000,
* })
*
* const tx = manager.begin({ timeout: 5000 })
* tx.addOperation(new DeductCurrency({ ... }))
* tx.addOperation(new AddItem({ ... }))
*
* const result = await tx.execute()
* ```
*/
export class TransactionManager {
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
}
// =========================================================================
// 只读属性 | Readonly properties
// =========================================================================
/**
* @zh 服务器 ID
* @en Server ID
*/
get serverId(): string {
return this._serverId
}
/**
* @zh 存储实例
* @en Storage instance
*/
get storage(): ITransactionStorage | null {
return this._storage
}
/**
* @zh 活跃事务数量
* @en Active transaction count
*/
get activeCount(): number {
return this._activeTransactions.size
}
// =========================================================================
// 公共方法 | Public methods
// =========================================================================
/**
* @zh 开始新事务
* @en Begin new transaction
*
* @param options - @zh 事务选项 @en Transaction options
* @returns @zh 事务上下文 @en Transaction context
*/
begin(options: TransactionOptions = {}): ITransactionContext {
const ctx = new TransactionContext({
timeout: options.timeout ?? this._defaultTimeout,
storage: this._storage ?? undefined,
metadata: {
...options.metadata,
serverId: this._serverId,
},
distributed: options.distributed,
})
this._activeTransactions.set(ctx.id, ctx)
return ctx
}
/**
* @zh 执行事务(便捷方法)
* @en Execute transaction (convenience method)
*
* @param builder - @zh 事务构建函数 @en Transaction builder function
* @param options - @zh 事务选项 @en Transaction options
* @returns @zh 事务结果 @en Transaction result
*/
async run<T = unknown>(
builder: (ctx: ITransactionContext) => void | Promise<void>,
options: TransactionOptions = {}
): Promise<TransactionResult<T>> {
const ctx = this.begin(options)
try {
await builder(ctx)
const result = await ctx.execute<T>()
return result
} finally {
this._activeTransactions.delete(ctx.id)
}
}
/**
* @zh 获取活跃事务
* @en Get active transaction
*/
getTransaction(id: string): ITransactionContext | undefined {
return this._activeTransactions.get(id)
}
/**
* @zh 恢复未完成的事务
* @en Recover pending transactions
*/
async recover(): Promise<number> {
if (!this._storage) return 0
const pendingTransactions = await this._storage.getPendingTransactions(this._serverId)
let recoveredCount = 0
for (const log of pendingTransactions) {
try {
await this._recoverTransaction(log)
recoveredCount++
} catch (error) {
console.error(`Failed to recover transaction ${log.id}:`, error)
}
}
return recoveredCount
}
/**
* @zh 获取分布式锁
* @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)
}
/**
* @zh 释放分布式锁
* @en Release distributed lock
*/
async releaseLock(key: string, token: string): Promise<boolean> {
if (!this._storage) return false
return this._storage.releaseLock(key, token)
}
/**
* @zh 使用分布式锁执行
* @en Execute with distributed lock
*/
async withLock<T>(
key: string,
fn: () => Promise<T>,
ttl: number = 10000
): Promise<T> {
const token = await this.acquireLock(key, ttl)
if (!token) {
throw new Error(`Failed to acquire lock for key: ${key}`)
}
try {
return await fn()
} finally {
await this.releaseLock(key, token)
}
}
/**
* @zh 清理已完成的事务日志
* @en Clean up completed transaction logs
*/
async cleanup(beforeTimestamp?: number): Promise<number> {
if (!this._storage) return 0
const timestamp = beforeTimestamp ?? Date.now() - 24 * 60 * 60 * 1000 // 默认清理24小时前
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++
}
}
return cleanedCount
}
// =========================================================================
// 私有方法 | Private methods
// =========================================================================
private _generateServerId(): string {
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')
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.updateTransactionState(log.id, 'rolledback')
} else {
await this._storage?.updateTransactionState(log.id, 'failed')
}
}
}
}
/**
* @zh 创建事务管理器
* @en Create transaction manager
*/
export function createTransactionManager(
config: TransactionManagerConfig = {}
): TransactionManager {
return new TransactionManager(config)
}

View File

@@ -0,0 +1,20 @@
/**
* @zh 核心模块导出
* @en Core module exports
*/
export type {
TransactionState,
OperationResult,
TransactionResult,
OperationLog,
TransactionLog,
TransactionOptions,
TransactionManagerConfig,
ITransactionStorage,
ITransactionOperation,
ITransactionContext,
} from './types.js'
export { TransactionContext, createTransactionContext } from './TransactionContext.js'
export { TransactionManager, createTransactionManager } from './TransactionManager.js'

View File

@@ -0,0 +1,484 @@
/**
* @zh 事务系统核心类型定义
* @en Transaction system core type definitions
*/
// =============================================================================
// 事务状态 | Transaction State
// =============================================================================
/**
* @zh 事务状态
* @en Transaction state
*/
export type TransactionState =
| 'pending' // 等待执行 | Waiting to execute
| 'executing' // 执行中 | Executing
| 'committed' // 已提交 | Committed
| 'rolledback' // 已回滚 | Rolled back
| 'failed' // 失败 | Failed
// =============================================================================
// 操作结果 | Operation Result
// =============================================================================
/**
* @zh 操作结果
* @en Operation result
*/
export interface OperationResult<T = unknown> {
/**
* @zh 是否成功
* @en Whether succeeded
*/
success: boolean
/**
* @zh 返回数据
* @en Return data
*/
data?: T
/**
* @zh 错误信息
* @en Error message
*/
error?: string
/**
* @zh 错误代码
* @en Error code
*/
errorCode?: string
}
/**
* @zh 事务结果
* @en Transaction result
*/
export interface TransactionResult<T = unknown> {
/**
* @zh 是否成功
* @en Whether succeeded
*/
success: boolean
/**
* @zh 事务 ID
* @en Transaction ID
*/
transactionId: string
/**
* @zh 操作结果列表
* @en Operation results
*/
results: OperationResult[]
/**
* @zh 最终数据
* @en Final data
*/
data?: T
/**
* @zh 错误信息
* @en Error message
*/
error?: string
/**
* @zh 执行时间(毫秒)
* @en Execution time in milliseconds
*/
duration: number
}
// =============================================================================
// 事务日志 | Transaction Log
// =============================================================================
/**
* @zh 操作日志
* @en Operation log
*/
export interface OperationLog {
/**
* @zh 操作名称
* @en Operation name
*/
name: string
/**
* @zh 操作数据
* @en Operation data
*/
data: unknown
/**
* @zh 操作状态
* @en Operation state
*/
state: 'pending' | 'executed' | 'compensated' | 'failed'
/**
* @zh 执行时间
* @en Execution timestamp
*/
executedAt?: number
/**
* @zh 补偿时间
* @en Compensation timestamp
*/
compensatedAt?: number
/**
* @zh 错误信息
* @en Error message
*/
error?: string
}
/**
* @zh 事务日志
* @en Transaction log
*/
export interface TransactionLog {
/**
* @zh 事务 ID
* @en Transaction ID
*/
id: string
/**
* @zh 事务状态
* @en Transaction state
*/
state: TransactionState
/**
* @zh 创建时间
* @en Creation timestamp
*/
createdAt: number
/**
* @zh 更新时间
* @en Update timestamp
*/
updatedAt: number
/**
* @zh 超时时间(毫秒)
* @en Timeout in milliseconds
*/
timeout: number
/**
* @zh 操作日志列表
* @en Operation logs
*/
operations: OperationLog[]
/**
* @zh 元数据
* @en Metadata
*/
metadata?: Record<string, unknown>
/**
* @zh 是否分布式事务
* @en Whether distributed transaction
*/
distributed?: boolean
/**
* @zh 参与的服务器列表
* @en Participating servers
*/
participants?: string[]
}
// =============================================================================
// 事务配置 | Transaction Configuration
// =============================================================================
/**
* @zh 事务选项
* @en Transaction options
*/
export interface TransactionOptions {
/**
* @zh 超时时间(毫秒),默认 30000
* @en Timeout in milliseconds, default 30000
*/
timeout?: number
/**
* @zh 是否分布式事务
* @en Whether distributed transaction
*/
distributed?: boolean
/**
* @zh 元数据
* @en Metadata
*/
metadata?: Record<string, unknown>
/**
* @zh 重试次数,默认 0
* @en Retry count, default 0
*/
retryCount?: number
/**
* @zh 重试间隔(毫秒),默认 1000
* @en Retry interval in milliseconds, default 1000
*/
retryInterval?: number
}
/**
* @zh 事务管理器配置
* @en Transaction manager configuration
*/
export interface TransactionManagerConfig {
/**
* @zh 存储实例
* @en Storage instance
*/
storage?: ITransactionStorage
/**
* @zh 默认超时时间(毫秒)
* @en Default timeout in milliseconds
*/
defaultTimeout?: number
/**
* @zh 服务器 ID分布式用
* @en Server ID for distributed transactions
*/
serverId?: string
/**
* @zh 是否自动恢复未完成事务
* @en Whether to auto-recover pending transactions
*/
autoRecover?: boolean
}
// =============================================================================
// 存储接口 | Storage Interface
// =============================================================================
/**
* @zh 事务存储接口
* @en Transaction storage interface
*/
export interface ITransactionStorage {
/**
* @zh 获取分布式锁
* @en Acquire distributed lock
*
* @param key - @zh 锁的键 @en Lock key
* @param ttl - @zh 锁的生存时间(毫秒) @en Lock TTL in milliseconds
* @returns @zh 锁令牌,获取失败返回 null @en Lock token, null if failed
*/
acquireLock(key: string, ttl: number): Promise<string | null>
/**
* @zh 释放分布式锁
* @en Release distributed lock
*
* @param key - @zh 锁的键 @en Lock key
* @param token - @zh 锁令牌 @en Lock token
* @returns @zh 是否成功释放 @en Whether released successfully
*/
releaseLock(key: string, token: string): Promise<boolean>
/**
* @zh 保存事务日志
* @en Save transaction log
*/
saveTransaction(tx: TransactionLog): Promise<void>
/**
* @zh 获取事务日志
* @en Get transaction log
*/
getTransaction(id: string): Promise<TransactionLog | null>
/**
* @zh 更新事务状态
* @en Update transaction state
*/
updateTransactionState(id: string, state: TransactionState): Promise<void>
/**
* @zh 更新操作状态
* @en Update operation state
*/
updateOperationState(
transactionId: string,
operationIndex: number,
state: OperationLog['state'],
error?: string
): Promise<void>
/**
* @zh 获取待恢复的事务列表
* @en Get pending transactions for recovery
*/
getPendingTransactions(serverId?: string): Promise<TransactionLog[]>
/**
* @zh 删除事务日志
* @en Delete transaction log
*/
deleteTransaction(id: string): Promise<void>
/**
* @zh 获取数据
* @en Get data
*/
get<T>(key: string): Promise<T | null>
/**
* @zh 设置数据
* @en Set data
*/
set<T>(key: string, value: T, ttl?: number): Promise<void>
/**
* @zh 删除数据
* @en Delete data
*/
delete(key: string): Promise<boolean>
}
// =============================================================================
// 操作接口 | Operation Interface
// =============================================================================
/**
* @zh 事务操作接口
* @en Transaction operation interface
*/
export interface ITransactionOperation<TData = unknown, TResult = unknown> {
/**
* @zh 操作名称
* @en Operation name
*/
readonly name: string
/**
* @zh 操作数据
* @en Operation data
*/
readonly data: TData
/**
* @zh 验证前置条件
* @en Validate preconditions
*
* @param ctx - @zh 事务上下文 @en Transaction context
* @returns @zh 是否验证通过 @en Whether validation passed
*/
validate(ctx: ITransactionContext): Promise<boolean>
/**
* @zh 执行操作
* @en Execute operation
*
* @param ctx - @zh 事务上下文 @en Transaction context
* @returns @zh 操作结果 @en Operation result
*/
execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>
/**
* @zh 补偿操作(回滚)
* @en Compensate operation (rollback)
*
* @param ctx - @zh 事务上下文 @en Transaction context
*/
compensate(ctx: ITransactionContext): Promise<void>
}
// =============================================================================
// 事务上下文接口 | Transaction Context Interface
// =============================================================================
/**
* @zh 事务上下文接口
* @en Transaction context interface
*/
export interface ITransactionContext {
/**
* @zh 事务 ID
* @en Transaction ID
*/
readonly id: string
/**
* @zh 事务状态
* @en Transaction state
*/
readonly state: TransactionState
/**
* @zh 超时时间(毫秒)
* @en Timeout in milliseconds
*/
readonly timeout: number
/**
* @zh 操作列表
* @en Operations
*/
readonly operations: ReadonlyArray<ITransactionOperation>
/**
* @zh 存储实例
* @en Storage instance
*/
readonly storage: ITransactionStorage | null
/**
* @zh 元数据
* @en Metadata
*/
readonly metadata: Record<string, unknown>
/**
* @zh 添加操作
* @en Add operation
*/
addOperation<T extends ITransactionOperation>(operation: T): this
/**
* @zh 执行事务
* @en Execute transaction
*/
execute<T = unknown>(): Promise<TransactionResult<T>>
/**
* @zh 回滚事务
* @en Rollback transaction
*/
rollback(): Promise<void>
/**
* @zh 获取上下文数据
* @en Get context data
*/
get<T>(key: string): T | undefined
/**
* @zh 设置上下文数据
* @en Set context data
*/
set<T>(key: string, value: T): void
}

View File

@@ -0,0 +1,350 @@
/**
* @zh Saga 编排器
* @en Saga Orchestrator
*
* @zh 实现分布式事务的 Saga 模式编排
* @en Implements Saga pattern orchestration for distributed transactions
*/
import type {
ITransactionStorage,
TransactionLog,
TransactionState,
OperationResult,
} from '../core/types.js'
/**
* @zh Saga 步骤状态
* @en Saga step state
*/
export type SagaStepState = 'pending' | 'executing' | 'completed' | 'compensating' | 'compensated' | 'failed'
/**
* @zh Saga 步骤
* @en Saga step
*/
export interface SagaStep<T = unknown> {
/**
* @zh 步骤名称
* @en Step name
*/
name: string
/**
* @zh 目标服务器 ID分布式用
* @en Target server ID (for distributed)
*/
serverId?: string
/**
* @zh 执行函数
* @en Execute function
*/
execute: (data: T) => Promise<OperationResult>
/**
* @zh 补偿函数
* @en Compensate function
*/
compensate: (data: T) => Promise<void>
/**
* @zh 步骤数据
* @en Step data
*/
data: T
}
/**
* @zh Saga 步骤日志
* @en Saga step log
*/
export interface SagaStepLog {
name: string
serverId?: string
state: SagaStepState
startedAt?: number
completedAt?: number
error?: string
}
/**
* @zh Saga 日志
* @en Saga log
*/
export interface SagaLog {
id: string
state: 'pending' | 'running' | 'completed' | 'compensating' | 'compensated' | 'failed'
steps: SagaStepLog[]
createdAt: number
updatedAt: number
metadata?: Record<string, unknown>
}
/**
* @zh Saga 结果
* @en Saga result
*/
export interface SagaResult {
success: boolean
sagaId: string
completedSteps: string[]
failedStep?: string
error?: string
duration: number
}
/**
* @zh Saga 编排器配置
* @en Saga orchestrator configuration
*/
export interface SagaOrchestratorConfig {
/**
* @zh 存储实例
* @en Storage instance
*/
storage?: ITransactionStorage
/**
* @zh 默认超时时间(毫秒)
* @en Default timeout in milliseconds
*/
timeout?: number
/**
* @zh 服务器 ID
* @en Server ID
*/
serverId?: string
}
/**
* @zh 生成 Saga ID
* @en Generate Saga ID
*/
function generateSagaId(): string {
return `saga_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 11)}`
}
/**
* @zh Saga 编排器
* @en Saga Orchestrator
*
* @zh 管理分布式事务的 Saga 模式执行流程
* @en Manages Saga pattern execution flow for distributed transactions
*
* @example
* ```typescript
* const orchestrator = new SagaOrchestrator({
* storage: redisStorage,
* serverId: 'server1',
* })
*
* const result = await orchestrator.execute([
* {
* name: 'deduct_currency',
* serverId: 'server1',
* execute: async (data) => {
* // 扣除货币
* return { success: true }
* },
* compensate: async (data) => {
* // 恢复货币
* },
* data: { playerId: '1', amount: 100 },
* },
* {
* name: 'add_item',
* serverId: 'server2',
* execute: async (data) => {
* // 添加物品
* return { success: true }
* },
* compensate: async (data) => {
* // 移除物品
* },
* data: { playerId: '1', itemId: 'sword' },
* },
* ])
* ```
*/
export class SagaOrchestrator {
private _storage: ITransactionStorage | null
private _timeout: number
private _serverId: string
constructor(config: SagaOrchestratorConfig = {}) {
this._storage = config.storage ?? null
this._timeout = config.timeout ?? 30000
this._serverId = config.serverId ?? 'default'
}
/**
* @zh 执行 Saga
* @en Execute Saga
*/
async execute<T>(steps: SagaStep<T>[]): Promise<SagaResult> {
const sagaId = generateSagaId()
const startTime = Date.now()
const completedSteps: string[] = []
const sagaLog: SagaLog = {
id: sagaId,
state: 'pending',
steps: steps.map((s) => ({
name: s.name,
serverId: s.serverId,
state: 'pending' as SagaStepState,
})),
createdAt: startTime,
updatedAt: startTime,
metadata: { orchestratorServerId: this._serverId },
}
await this._saveSagaLog(sagaLog)
try {
sagaLog.state = 'running'
await this._saveSagaLog(sagaLog)
for (let i = 0; i < steps.length; i++) {
const step = steps[i]
if (Date.now() - startTime > this._timeout) {
throw new Error('Saga execution timed out')
}
sagaLog.steps[i].state = 'executing'
sagaLog.steps[i].startedAt = Date.now()
await this._saveSagaLog(sagaLog)
const result = await step.execute(step.data)
if (!result.success) {
sagaLog.steps[i].state = 'failed'
sagaLog.steps[i].error = result.error
await this._saveSagaLog(sagaLog)
throw new Error(result.error ?? `Step ${step.name} failed`)
}
sagaLog.steps[i].state = 'completed'
sagaLog.steps[i].completedAt = Date.now()
completedSteps.push(step.name)
await this._saveSagaLog(sagaLog)
}
sagaLog.state = 'completed'
sagaLog.updatedAt = Date.now()
await this._saveSagaLog(sagaLog)
return {
success: true,
sagaId,
completedSteps,
duration: Date.now() - startTime,
}
} catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error)
const failedStepIndex = completedSteps.length
sagaLog.state = 'compensating'
await this._saveSagaLog(sagaLog)
for (let i = completedSteps.length - 1; i >= 0; i--) {
const step = steps[i]
sagaLog.steps[i].state = 'compensating'
await this._saveSagaLog(sagaLog)
try {
await step.compensate(step.data)
sagaLog.steps[i].state = 'compensated'
} catch (compError) {
const compErrorMessage = compError instanceof Error ? compError.message : String(compError)
sagaLog.steps[i].state = 'failed'
sagaLog.steps[i].error = `Compensation failed: ${compErrorMessage}`
}
await this._saveSagaLog(sagaLog)
}
sagaLog.state = 'compensated'
sagaLog.updatedAt = Date.now()
await this._saveSagaLog(sagaLog)
return {
success: false,
sagaId,
completedSteps,
failedStep: steps[failedStepIndex]?.name,
error: errorMessage,
duration: Date.now() - startTime,
}
}
}
/**
* @zh 恢复未完成的 Saga
* @en Recover pending Sagas
*/
async recover(): Promise<number> {
if (!this._storage) return 0
const pendingSagas = await this._getPendingSagas()
let recoveredCount = 0
for (const saga of pendingSagas) {
try {
await this._recoverSaga(saga)
recoveredCount++
} catch (error) {
console.error(`Failed to recover saga ${saga.id}:`, error)
}
}
return recoveredCount
}
/**
* @zh 获取 Saga 日志
* @en Get Saga log
*/
async getSagaLog(sagaId: string): Promise<SagaLog | null> {
if (!this._storage) return null
return this._storage.get<SagaLog>(`saga:${sagaId}`)
}
private async _saveSagaLog(log: SagaLog): Promise<void> {
if (!this._storage) return
log.updatedAt = Date.now()
await this._storage.set(`saga:${log.id}`, log)
}
private async _getPendingSagas(): Promise<SagaLog[]> {
return []
}
private async _recoverSaga(saga: SagaLog): Promise<void> {
if (saga.state === 'running' || saga.state === 'compensating') {
const completedSteps = saga.steps
.filter((s) => s.state === 'completed')
.map((s) => s.name)
saga.state = 'compensated'
saga.updatedAt = Date.now()
if (this._storage) {
await this._storage.set(`saga:${saga.id}`, saga)
}
}
}
}
/**
* @zh 创建 Saga 编排器
* @en Create Saga orchestrator
*/
export function createSagaOrchestrator(config: SagaOrchestratorConfig = {}): SagaOrchestrator {
return new SagaOrchestrator(config)
}

View File

@@ -0,0 +1,15 @@
/**
* @zh 分布式模块导出
* @en Distributed module exports
*/
export {
SagaOrchestrator,
createSagaOrchestrator,
type SagaOrchestratorConfig,
type SagaStep,
type SagaStepState,
type SagaStepLog,
type SagaLog,
type SagaResult,
} from './SagaOrchestrator.js'

View File

@@ -0,0 +1,165 @@
/**
* @zh @esengine/transaction 事务系统
* @en @esengine/transaction Transaction System
*
* @zh 提供游戏事务处理能力,支持商店购买、玩家交易、分布式事务
* @en Provides game transaction capabilities, supporting shop purchases, player trading, and distributed transactions
*
* @example
* ```typescript
* import {
* TransactionManager,
* MemoryStorage,
* CurrencyOperation,
* InventoryOperation,
* } from '@esengine/transaction'
*
* // 创建事务管理器
* const manager = new TransactionManager({
* storage: new MemoryStorage(),
* })
*
* // 执行事务
* const result = await manager.run((tx) => {
* tx.addOperation(new CurrencyOperation({
* type: 'deduct',
* playerId: 'player1',
* currency: 'gold',
* amount: 100,
* }))
* tx.addOperation(new InventoryOperation({
* type: 'add',
* playerId: 'player1',
* itemId: 'sword',
* quantity: 1,
* }))
* })
*
* if (result.success) {
* console.log('Transaction completed!')
* }
* ```
*/
// =============================================================================
// Core | 核心
// =============================================================================
export type {
TransactionState,
OperationResult,
TransactionResult,
OperationLog,
TransactionLog,
TransactionOptions,
TransactionManagerConfig,
ITransactionStorage,
ITransactionOperation,
ITransactionContext,
} from './core/types.js'
export {
TransactionContext,
createTransactionContext,
} from './core/TransactionContext.js'
export {
TransactionManager,
createTransactionManager,
} from './core/TransactionManager.js'
// =============================================================================
// Storage | 存储
// =============================================================================
export {
MemoryStorage,
createMemoryStorage,
type MemoryStorageConfig,
} from './storage/MemoryStorage.js'
export {
RedisStorage,
createRedisStorage,
type RedisStorageConfig,
type RedisClient,
} from './storage/RedisStorage.js'
export {
MongoStorage,
createMongoStorage,
type MongoStorageConfig,
type MongoDb,
type MongoCollection,
} from './storage/MongoStorage.js'
// =============================================================================
// Operations | 操作
// =============================================================================
export { BaseOperation } from './operations/BaseOperation.js'
export {
CurrencyOperation,
createCurrencyOperation,
type CurrencyOperationType,
type CurrencyOperationData,
type CurrencyOperationResult,
type ICurrencyProvider,
} from './operations/CurrencyOperation.js'
export {
InventoryOperation,
createInventoryOperation,
type InventoryOperationType,
type InventoryOperationData,
type InventoryOperationResult,
type IInventoryProvider,
type ItemData,
} from './operations/InventoryOperation.js'
export {
TradeOperation,
createTradeOperation,
type TradeOperationData,
type TradeOperationResult,
type TradeItem,
type TradeCurrency,
type TradeParty,
type ITradeProvider,
} from './operations/TradeOperation.js'
// =============================================================================
// Distributed | 分布式
// =============================================================================
export {
SagaOrchestrator,
createSagaOrchestrator,
type SagaOrchestratorConfig,
type SagaStep,
type SagaStepState,
type SagaStepLog,
type SagaLog,
type SagaResult,
} from './distributed/SagaOrchestrator.js'
// =============================================================================
// Integration | 集成
// =============================================================================
export {
withTransactions,
TransactionRoom,
type TransactionRoomConfig,
type ITransactionRoom,
} from './integration/RoomTransactionMixin.js'
// =============================================================================
// Tokens | 令牌
// =============================================================================
export {
TransactionManagerToken,
TransactionStorageToken,
} from './tokens.js'

View File

@@ -0,0 +1,174 @@
/**
* @zh Room 事务扩展
* @en Room transaction extension
*/
import type {
ITransactionStorage,
ITransactionContext,
TransactionOptions,
TransactionResult,
} from '../core/types.js'
import { TransactionManager } from '../core/TransactionManager.js'
/**
* @zh 事务 Room 配置
* @en Transaction Room configuration
*/
export interface TransactionRoomConfig {
/**
* @zh 存储实例
* @en Storage instance
*/
storage?: ITransactionStorage
/**
* @zh 默认超时时间(毫秒)
* @en Default timeout in milliseconds
*/
defaultTimeout?: number
/**
* @zh 服务器 ID
* @en Server ID
*/
serverId?: string
}
/**
* @zh 事务 Room 接口
* @en Transaction Room interface
*/
export interface ITransactionRoom {
/**
* @zh 事务管理器
* @en Transaction manager
*/
readonly transactions: TransactionManager
/**
* @zh 开始事务
* @en Begin transaction
*/
beginTransaction(options?: TransactionOptions): ITransactionContext
/**
* @zh 执行事务
* @en Run transaction
*/
runTransaction<T = unknown>(
builder: (ctx: ITransactionContext) => void | Promise<void>,
options?: TransactionOptions
): Promise<TransactionResult<T>>
}
/**
* @zh 创建事务 Room mixin
* @en Create transaction Room mixin
*
* @example
* ```typescript
* import { Room } from '@esengine/server'
* import { withTransactions, RedisStorage } from '@esengine/transaction'
*
* class GameRoom extends withTransactions(Room, {
* storage: new RedisStorage({ client: redisClient }),
* }) {
* async handleBuy(itemId: string, player: Player) {
* const result = await this.runTransaction((tx) => {
* tx.addOperation(new CurrencyOperation({
* type: 'deduct',
* playerId: player.id,
* currency: 'gold',
* amount: 100,
* }))
* })
*
* if (result.success) {
* player.send('buy_success', { itemId })
* }
* }
* }
* ```
*/
export function withTransactions<TBase extends new (...args: any[]) => any>(
Base: TBase,
config: TransactionRoomConfig = {}
): TBase & (new (...args: any[]) => ITransactionRoom) {
return class TransactionRoom extends Base implements ITransactionRoom {
private _transactionManager: TransactionManager
constructor(...args: any[]) {
super(...args)
this._transactionManager = new TransactionManager({
storage: config.storage,
defaultTimeout: config.defaultTimeout,
serverId: config.serverId,
})
}
get transactions(): TransactionManager {
return this._transactionManager
}
beginTransaction(options?: TransactionOptions): ITransactionContext {
return this._transactionManager.begin(options)
}
runTransaction<T = unknown>(
builder: (ctx: ITransactionContext) => void | Promise<void>,
options?: TransactionOptions
): Promise<TransactionResult<T>> {
return this._transactionManager.run<T>(builder, options)
}
}
}
/**
* @zh 事务 Room 抽象基类
* @en Transaction Room abstract base class
*
* @zh 可以直接继承使用,也可以使用 withTransactions mixin
* @en Can be extended directly or use withTransactions mixin
*
* @example
* ```typescript
* class GameRoom extends TransactionRoom {
* constructor() {
* super({ storage: new RedisStorage({ client: redisClient }) })
* }
*
* async handleTrade(data: TradeData, player: Player) {
* const result = await this.runTransaction((tx) => {
* // 添加交易操作
* })
* }
* }
* ```
*/
export abstract class TransactionRoom implements ITransactionRoom {
private _transactionManager: TransactionManager
constructor(config: TransactionRoomConfig = {}) {
this._transactionManager = new TransactionManager({
storage: config.storage,
defaultTimeout: config.defaultTimeout,
serverId: config.serverId,
})
}
get transactions(): TransactionManager {
return this._transactionManager
}
beginTransaction(options?: TransactionOptions): ITransactionContext {
return this._transactionManager.begin(options)
}
runTransaction<T = unknown>(
builder: (ctx: ITransactionContext) => void | Promise<void>,
options?: TransactionOptions
): Promise<TransactionResult<T>> {
return this._transactionManager.run<T>(builder, options)
}
}

View File

@@ -0,0 +1,11 @@
/**
* @zh 集成模块导出
* @en Integration module exports
*/
export {
withTransactions,
TransactionRoom,
type TransactionRoomConfig,
type ITransactionRoom,
} from './RoomTransactionMixin.js'

View File

@@ -0,0 +1,64 @@
/**
* @zh 操作基类
* @en Base operation class
*/
import type {
ITransactionOperation,
ITransactionContext,
OperationResult,
} from '../core/types.js'
/**
* @zh 操作基类
* @en Base operation class
*
* @zh 提供通用的操作实现模板
* @en Provides common operation implementation template
*/
export abstract class BaseOperation<TData = unknown, TResult = unknown>
implements ITransactionOperation<TData, TResult>
{
abstract readonly name: string
readonly data: TData
constructor(data: TData) {
this.data = data
}
/**
* @zh 验证前置条件(默认通过)
* @en Validate preconditions (passes by default)
*/
async validate(_ctx: ITransactionContext): Promise<boolean> {
return true
}
/**
* @zh 执行操作
* @en Execute operation
*/
abstract execute(ctx: ITransactionContext): Promise<OperationResult<TResult>>
/**
* @zh 补偿操作
* @en Compensate operation
*/
abstract compensate(ctx: ITransactionContext): Promise<void>
/**
* @zh 创建成功结果
* @en Create success result
*/
protected success(data?: TResult): OperationResult<TResult> {
return { success: true, data }
}
/**
* @zh 创建失败结果
* @en Create failure result
*/
protected failure(error: string, errorCode?: string): OperationResult<TResult> {
return { success: false, error, errorCode }
}
}

View File

@@ -0,0 +1,208 @@
/**
* @zh 货币操作
* @en Currency operation
*/
import type { ITransactionContext, OperationResult } from '../core/types.js'
import { BaseOperation } from './BaseOperation.js'
/**
* @zh 货币操作类型
* @en Currency operation type
*/
export type CurrencyOperationType = 'add' | 'deduct'
/**
* @zh 货币操作数据
* @en Currency operation data
*/
export interface CurrencyOperationData {
/**
* @zh 操作类型
* @en Operation type
*/
type: CurrencyOperationType
/**
* @zh 玩家 ID
* @en Player ID
*/
playerId: string
/**
* @zh 货币类型(如 gold, diamond 等)
* @en Currency type (e.g., gold, diamond)
*/
currency: string
/**
* @zh 数量
* @en Amount
*/
amount: number
/**
* @zh 原因/来源
* @en Reason/source
*/
reason?: string
}
/**
* @zh 货币操作结果
* @en Currency operation result
*/
export interface CurrencyOperationResult {
/**
* @zh 操作前余额
* @en Balance before operation
*/
beforeBalance: number
/**
* @zh 操作后余额
* @en Balance after operation
*/
afterBalance: number
}
/**
* @zh 货币数据提供者接口
* @en Currency data provider interface
*/
export interface ICurrencyProvider {
/**
* @zh 获取货币余额
* @en Get currency balance
*/
getBalance(playerId: string, currency: string): Promise<number>
/**
* @zh 设置货币余额
* @en Set currency balance
*/
setBalance(playerId: string, currency: string, amount: number): Promise<void>
}
/**
* @zh 货币操作
* @en Currency operation
*
* @zh 用于处理货币的增加和扣除
* @en Used for handling currency addition and deduction
*
* @example
* ```typescript
* // 扣除金币
* tx.addOperation(new CurrencyOperation({
* type: 'deduct',
* playerId: 'player1',
* currency: 'gold',
* amount: 100,
* reason: 'purchase_item',
* }))
*
* // 增加钻石
* tx.addOperation(new CurrencyOperation({
* type: 'add',
* playerId: 'player1',
* currency: 'diamond',
* amount: 50,
* }))
* ```
*/
export class CurrencyOperation extends BaseOperation<CurrencyOperationData, CurrencyOperationResult> {
readonly name = 'currency'
private _provider: ICurrencyProvider | null = null
private _beforeBalance: number = 0
/**
* @zh 设置货币数据提供者
* @en Set currency data provider
*/
setProvider(provider: ICurrencyProvider): this {
this._provider = provider
return this
}
async validate(ctx: ITransactionContext): Promise<boolean> {
if (this.data.amount <= 0) {
return false
}
if (this.data.type === 'deduct') {
const balance = await this._getBalance(ctx)
return balance >= this.data.amount
}
return true
}
async execute(ctx: ITransactionContext): Promise<OperationResult<CurrencyOperationResult>> {
const { type, playerId, currency, amount } = this.data
this._beforeBalance = await this._getBalance(ctx)
let afterBalance: number
if (type === 'add') {
afterBalance = this._beforeBalance + amount
} else {
if (this._beforeBalance < amount) {
return this.failure('Insufficient balance', 'INSUFFICIENT_BALANCE')
}
afterBalance = this._beforeBalance - amount
}
await this._setBalance(ctx, afterBalance)
ctx.set(`currency:${playerId}:${currency}:before`, this._beforeBalance)
ctx.set(`currency:${playerId}:${currency}:after`, afterBalance)
return this.success({
beforeBalance: this._beforeBalance,
afterBalance,
})
}
async compensate(ctx: ITransactionContext): Promise<void> {
await this._setBalance(ctx, this._beforeBalance)
}
private async _getBalance(ctx: ITransactionContext): Promise<number> {
const { playerId, currency } = this.data
if (this._provider) {
return this._provider.getBalance(playerId, currency)
}
if (ctx.storage) {
const balance = await ctx.storage.get<number>(`player:${playerId}:currency:${currency}`)
return balance ?? 0
}
return 0
}
private async _setBalance(ctx: ITransactionContext, amount: number): Promise<void> {
const { playerId, currency } = this.data
if (this._provider) {
await this._provider.setBalance(playerId, currency, amount)
return
}
if (ctx.storage) {
await ctx.storage.set(`player:${playerId}:currency:${currency}`, amount)
}
}
}
/**
* @zh 创建货币操作
* @en Create currency operation
*/
export function createCurrencyOperation(data: CurrencyOperationData): CurrencyOperation {
return new CurrencyOperation(data)
}

View File

@@ -0,0 +1,291 @@
/**
* @zh 背包操作
* @en Inventory operation
*/
import type { ITransactionContext, OperationResult } from '../core/types.js'
import { BaseOperation } from './BaseOperation.js'
/**
* @zh 背包操作类型
* @en Inventory operation type
*/
export type InventoryOperationType = 'add' | 'remove' | 'update'
/**
* @zh 物品数据
* @en Item data
*/
export interface ItemData {
/**
* @zh 物品 ID
* @en Item ID
*/
itemId: string
/**
* @zh 数量
* @en Quantity
*/
quantity: number
/**
* @zh 物品属性
* @en Item properties
*/
properties?: Record<string, unknown>
}
/**
* @zh 背包操作数据
* @en Inventory operation data
*/
export interface InventoryOperationData {
/**
* @zh 操作类型
* @en Operation type
*/
type: InventoryOperationType
/**
* @zh 玩家 ID
* @en Player ID
*/
playerId: string
/**
* @zh 物品 ID
* @en Item ID
*/
itemId: string
/**
* @zh 数量
* @en Quantity
*/
quantity: number
/**
* @zh 物品属性(用于更新)
* @en Item properties (for update)
*/
properties?: Record<string, unknown>
/**
* @zh 原因/来源
* @en Reason/source
*/
reason?: string
}
/**
* @zh 背包操作结果
* @en Inventory operation result
*/
export interface InventoryOperationResult {
/**
* @zh 操作前的物品数据
* @en Item data before operation
*/
beforeItem?: ItemData
/**
* @zh 操作后的物品数据
* @en Item data after operation
*/
afterItem?: ItemData
}
/**
* @zh 背包数据提供者接口
* @en Inventory data provider interface
*/
export interface IInventoryProvider {
/**
* @zh 获取物品
* @en Get item
*/
getItem(playerId: string, itemId: string): Promise<ItemData | null>
/**
* @zh 设置物品
* @en Set item
*/
setItem(playerId: string, itemId: string, item: ItemData | null): Promise<void>
/**
* @zh 检查背包容量
* @en Check inventory capacity
*/
hasCapacity?(playerId: string, count: number): Promise<boolean>
}
/**
* @zh 背包操作
* @en Inventory operation
*
* @zh 用于处理物品的添加、移除和更新
* @en Used for handling item addition, removal, and update
*
* @example
* ```typescript
* // 添加物品
* tx.addOperation(new InventoryOperation({
* type: 'add',
* playerId: 'player1',
* itemId: 'sword_001',
* quantity: 1,
* }))
*
* // 移除物品
* tx.addOperation(new InventoryOperation({
* type: 'remove',
* playerId: 'player1',
* itemId: 'potion_hp',
* quantity: 5,
* }))
* ```
*/
export class InventoryOperation extends BaseOperation<InventoryOperationData, InventoryOperationResult> {
readonly name = 'inventory'
private _provider: IInventoryProvider | null = null
private _beforeItem: ItemData | null = null
/**
* @zh 设置背包数据提供者
* @en Set inventory data provider
*/
setProvider(provider: IInventoryProvider): this {
this._provider = provider
return this
}
async validate(ctx: ITransactionContext): Promise<boolean> {
const { type, quantity } = this.data
if (quantity <= 0) {
return false
}
if (type === 'remove') {
const item = await this._getItem(ctx)
return item !== null && item.quantity >= quantity
}
if (type === 'add' && this._provider?.hasCapacity) {
return this._provider.hasCapacity(this.data.playerId, 1)
}
return true
}
async execute(ctx: ITransactionContext): Promise<OperationResult<InventoryOperationResult>> {
const { type, playerId, itemId, quantity, properties } = this.data
this._beforeItem = await this._getItem(ctx)
let afterItem: ItemData | null = null
switch (type) {
case 'add': {
if (this._beforeItem) {
afterItem = {
...this._beforeItem,
quantity: this._beforeItem.quantity + quantity,
}
} else {
afterItem = {
itemId,
quantity,
properties,
}
}
break
}
case 'remove': {
if (!this._beforeItem || this._beforeItem.quantity < quantity) {
return this.failure('Insufficient item quantity', 'INSUFFICIENT_ITEM')
}
const newQuantity = this._beforeItem.quantity - quantity
if (newQuantity > 0) {
afterItem = {
...this._beforeItem,
quantity: newQuantity,
}
} else {
afterItem = null
}
break
}
case 'update': {
if (!this._beforeItem) {
return this.failure('Item not found', 'ITEM_NOT_FOUND')
}
afterItem = {
...this._beforeItem,
quantity: quantity > 0 ? quantity : this._beforeItem.quantity,
properties: properties ?? this._beforeItem.properties,
}
break
}
}
await this._setItem(ctx, afterItem)
ctx.set(`inventory:${playerId}:${itemId}:before`, this._beforeItem)
ctx.set(`inventory:${playerId}:${itemId}:after`, afterItem)
return this.success({
beforeItem: this._beforeItem ?? undefined,
afterItem: afterItem ?? undefined,
})
}
async compensate(ctx: ITransactionContext): Promise<void> {
await this._setItem(ctx, this._beforeItem)
}
private async _getItem(ctx: ITransactionContext): Promise<ItemData | null> {
const { playerId, itemId } = this.data
if (this._provider) {
return this._provider.getItem(playerId, itemId)
}
if (ctx.storage) {
return ctx.storage.get<ItemData>(`player:${playerId}:inventory:${itemId}`)
}
return null
}
private async _setItem(ctx: ITransactionContext, item: ItemData | null): Promise<void> {
const { playerId, itemId } = this.data
if (this._provider) {
await this._provider.setItem(playerId, itemId, item)
return
}
if (ctx.storage) {
if (item) {
await ctx.storage.set(`player:${playerId}:inventory:${itemId}`, item)
} else {
await ctx.storage.delete(`player:${playerId}:inventory:${itemId}`)
}
}
}
}
/**
* @zh 创建背包操作
* @en Create inventory operation
*/
export function createInventoryOperation(data: InventoryOperationData): InventoryOperation {
return new InventoryOperation(data)
}

View File

@@ -0,0 +1,331 @@
/**
* @zh 交易操作
* @en Trade operation
*/
import type { ITransactionContext, OperationResult } from '../core/types.js'
import { BaseOperation } from './BaseOperation.js'
import { CurrencyOperation, type CurrencyOperationData, type ICurrencyProvider } from './CurrencyOperation.js'
import { InventoryOperation, type InventoryOperationData, type IInventoryProvider, type ItemData } from './InventoryOperation.js'
/**
* @zh 交易物品
* @en Trade item
*/
export interface TradeItem {
/**
* @zh 物品 ID
* @en Item ID
*/
itemId: string
/**
* @zh 数量
* @en Quantity
*/
quantity: number
}
/**
* @zh 交易货币
* @en Trade currency
*/
export interface TradeCurrency {
/**
* @zh 货币类型
* @en Currency type
*/
currency: string
/**
* @zh 数量
* @en Amount
*/
amount: number
}
/**
* @zh 交易方数据
* @en Trade party data
*/
export interface TradeParty {
/**
* @zh 玩家 ID
* @en Player ID
*/
playerId: string
/**
* @zh 给出的物品
* @en Items to give
*/
items?: TradeItem[]
/**
* @zh 给出的货币
* @en Currencies to give
*/
currencies?: TradeCurrency[]
}
/**
* @zh 交易操作数据
* @en Trade operation data
*/
export interface TradeOperationData {
/**
* @zh 交易 ID
* @en Trade ID
*/
tradeId: string
/**
* @zh 交易发起方
* @en Trade initiator
*/
partyA: TradeParty
/**
* @zh 交易接收方
* @en Trade receiver
*/
partyB: TradeParty
/**
* @zh 原因/备注
* @en Reason/note
*/
reason?: string
}
/**
* @zh 交易操作结果
* @en Trade operation result
*/
export interface TradeOperationResult {
/**
* @zh 交易 ID
* @en Trade ID
*/
tradeId: string
/**
* @zh 交易是否成功
* @en Whether trade succeeded
*/
completed: boolean
}
/**
* @zh 交易数据提供者
* @en Trade data provider
*/
export interface ITradeProvider {
currencyProvider?: ICurrencyProvider
inventoryProvider?: IInventoryProvider
}
/**
* @zh 交易操作
* @en Trade operation
*
* @zh 用于处理玩家之间的物品和货币交换
* @en Used for handling item and currency exchange between players
*
* @example
* ```typescript
* tx.addOperation(new TradeOperation({
* tradeId: 'trade_001',
* partyA: {
* playerId: 'player1',
* items: [{ itemId: 'sword', quantity: 1 }],
* },
* partyB: {
* playerId: 'player2',
* currencies: [{ currency: 'gold', amount: 1000 }],
* },
* }))
* ```
*/
export class TradeOperation extends BaseOperation<TradeOperationData, TradeOperationResult> {
readonly name = 'trade'
private _provider: ITradeProvider | null = null
private _subOperations: (CurrencyOperation | InventoryOperation)[] = []
private _executedCount = 0
/**
* @zh 设置交易数据提供者
* @en Set trade data provider
*/
setProvider(provider: ITradeProvider): this {
this._provider = provider
return this
}
async validate(ctx: ITransactionContext): Promise<boolean> {
this._buildSubOperations()
for (const op of this._subOperations) {
const isValid = await op.validate(ctx)
if (!isValid) {
return false
}
}
return true
}
async execute(ctx: ITransactionContext): Promise<OperationResult<TradeOperationResult>> {
this._buildSubOperations()
this._executedCount = 0
try {
for (const op of this._subOperations) {
const result = await op.execute(ctx)
if (!result.success) {
await this._compensateExecuted(ctx)
return this.failure(result.error ?? 'Trade operation failed', 'TRADE_FAILED')
}
this._executedCount++
}
return this.success({
tradeId: this.data.tradeId,
completed: true,
})
} catch (error) {
await this._compensateExecuted(ctx)
const errorMessage = error instanceof Error ? error.message : String(error)
return this.failure(errorMessage, 'TRADE_ERROR')
}
}
async compensate(ctx: ITransactionContext): Promise<void> {
await this._compensateExecuted(ctx)
}
private _buildSubOperations(): void {
if (this._subOperations.length > 0) return
const { partyA, partyB } = this.data
if (partyA.items) {
for (const item of partyA.items) {
const removeOp = new InventoryOperation({
type: 'remove',
playerId: partyA.playerId,
itemId: item.itemId,
quantity: item.quantity,
reason: `trade:${this.data.tradeId}:give`,
})
const addOp = new InventoryOperation({
type: 'add',
playerId: partyB.playerId,
itemId: item.itemId,
quantity: item.quantity,
reason: `trade:${this.data.tradeId}:receive`,
})
if (this._provider?.inventoryProvider) {
removeOp.setProvider(this._provider.inventoryProvider)
addOp.setProvider(this._provider.inventoryProvider)
}
this._subOperations.push(removeOp, addOp)
}
}
if (partyA.currencies) {
for (const curr of partyA.currencies) {
const deductOp = new CurrencyOperation({
type: 'deduct',
playerId: partyA.playerId,
currency: curr.currency,
amount: curr.amount,
reason: `trade:${this.data.tradeId}:give`,
})
const addOp = new CurrencyOperation({
type: 'add',
playerId: partyB.playerId,
currency: curr.currency,
amount: curr.amount,
reason: `trade:${this.data.tradeId}:receive`,
})
if (this._provider?.currencyProvider) {
deductOp.setProvider(this._provider.currencyProvider)
addOp.setProvider(this._provider.currencyProvider)
}
this._subOperations.push(deductOp, addOp)
}
}
if (partyB.items) {
for (const item of partyB.items) {
const removeOp = new InventoryOperation({
type: 'remove',
playerId: partyB.playerId,
itemId: item.itemId,
quantity: item.quantity,
reason: `trade:${this.data.tradeId}:give`,
})
const addOp = new InventoryOperation({
type: 'add',
playerId: partyA.playerId,
itemId: item.itemId,
quantity: item.quantity,
reason: `trade:${this.data.tradeId}:receive`,
})
if (this._provider?.inventoryProvider) {
removeOp.setProvider(this._provider.inventoryProvider)
addOp.setProvider(this._provider.inventoryProvider)
}
this._subOperations.push(removeOp, addOp)
}
}
if (partyB.currencies) {
for (const curr of partyB.currencies) {
const deductOp = new CurrencyOperation({
type: 'deduct',
playerId: partyB.playerId,
currency: curr.currency,
amount: curr.amount,
reason: `trade:${this.data.tradeId}:give`,
})
const addOp = new CurrencyOperation({
type: 'add',
playerId: partyA.playerId,
currency: curr.currency,
amount: curr.amount,
reason: `trade:${this.data.tradeId}:receive`,
})
if (this._provider?.currencyProvider) {
deductOp.setProvider(this._provider.currencyProvider)
addOp.setProvider(this._provider.currencyProvider)
}
this._subOperations.push(deductOp, addOp)
}
}
}
private async _compensateExecuted(ctx: ITransactionContext): Promise<void> {
for (let i = this._executedCount - 1; i >= 0; i--) {
await this._subOperations[i].compensate(ctx)
}
}
}
/**
* @zh 创建交易操作
* @en Create trade operation
*/
export function createTradeOperation(data: TradeOperationData): TradeOperation {
return new TradeOperation(data)
}

View File

@@ -0,0 +1,36 @@
/**
* @zh 操作模块导出
* @en Operations module exports
*/
export { BaseOperation } from './BaseOperation.js'
export {
CurrencyOperation,
createCurrencyOperation,
type CurrencyOperationType,
type CurrencyOperationData,
type CurrencyOperationResult,
type ICurrencyProvider,
} from './CurrencyOperation.js'
export {
InventoryOperation,
createInventoryOperation,
type InventoryOperationType,
type InventoryOperationData,
type InventoryOperationResult,
type IInventoryProvider,
type ItemData,
} from './InventoryOperation.js'
export {
TradeOperation,
createTradeOperation,
type TradeOperationData,
type TradeOperationResult,
type TradeItem,
type TradeCurrency,
type TradeParty,
type ITradeProvider,
} from './TradeOperation.js'

View File

@@ -0,0 +1,229 @@
/**
* @zh 内存存储实现
* @en Memory storage implementation
*
* @zh 用于开发和测试环境,不支持分布式
* @en For development and testing, does not support distributed scenarios
*/
import type {
ITransactionStorage,
TransactionLog,
TransactionState,
OperationLog,
} from '../core/types.js'
/**
* @zh 内存存储配置
* @en Memory storage configuration
*/
export interface MemoryStorageConfig {
/**
* @zh 最大事务日志数量
* @en Maximum transaction log count
*/
maxTransactions?: number
}
/**
* @zh 内存存储
* @en Memory storage
*
* @zh 适用于单机开发和测试,数据仅保存在内存中
* @en Suitable for single-machine development and testing, data is stored in memory only
*/
export class MemoryStorage implements ITransactionStorage {
private _transactions: Map<string, TransactionLog> = new Map()
private _data: Map<string, { value: unknown; expireAt?: number }> = new Map()
private _locks: Map<string, { token: string; expireAt: number }> = new Map()
private _maxTransactions: number
constructor(config: MemoryStorageConfig = {}) {
this._maxTransactions = config.maxTransactions ?? 1000
}
// =========================================================================
// 分布式锁 | Distributed Lock
// =========================================================================
async acquireLock(key: string, ttl: number): Promise<string | null> {
this._cleanExpiredLocks()
const existing = this._locks.get(key)
if (existing && existing.expireAt > Date.now()) {
return null
}
const token = `lock_${Date.now()}_${Math.random().toString(36).substring(2)}`
this._locks.set(key, {
token,
expireAt: Date.now() + ttl,
})
return token
}
async releaseLock(key: string, token: string): Promise<boolean> {
const lock = this._locks.get(key)
if (!lock || lock.token !== token) {
return false
}
this._locks.delete(key)
return true
}
// =========================================================================
// 事务日志 | Transaction Log
// =========================================================================
async saveTransaction(tx: TransactionLog): Promise<void> {
if (this._transactions.size >= this._maxTransactions) {
this._cleanOldTransactions()
}
this._transactions.set(tx.id, { ...tx })
}
async getTransaction(id: string): Promise<TransactionLog | null> {
const tx = this._transactions.get(id)
return tx ? { ...tx } : null
}
async updateTransactionState(id: string, state: TransactionState): Promise<void> {
const tx = this._transactions.get(id)
if (tx) {
tx.state = state
tx.updatedAt = Date.now()
}
}
async updateOperationState(
transactionId: string,
operationIndex: number,
state: OperationLog['state'],
error?: string
): Promise<void> {
const tx = this._transactions.get(transactionId)
if (tx && tx.operations[operationIndex]) {
tx.operations[operationIndex].state = state
if (error) {
tx.operations[operationIndex].error = error
}
if (state === 'executed') {
tx.operations[operationIndex].executedAt = Date.now()
} else if (state === 'compensated') {
tx.operations[operationIndex].compensatedAt = Date.now()
}
tx.updatedAt = Date.now()
}
}
async getPendingTransactions(serverId?: string): Promise<TransactionLog[]> {
const result: TransactionLog[] = []
for (const tx of this._transactions.values()) {
if (tx.state === 'pending' || tx.state === 'executing') {
if (!serverId || tx.metadata?.serverId === serverId) {
result.push({ ...tx })
}
}
}
return result
}
async deleteTransaction(id: string): Promise<void> {
this._transactions.delete(id)
}
// =========================================================================
// 数据操作 | Data Operations
// =========================================================================
async get<T>(key: string): Promise<T | null> {
this._cleanExpiredData()
const entry = this._data.get(key)
if (!entry) return null
if (entry.expireAt && entry.expireAt < Date.now()) {
this._data.delete(key)
return null
}
return entry.value as T
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
this._data.set(key, {
value,
expireAt: ttl ? Date.now() + ttl : undefined,
})
}
async delete(key: string): Promise<boolean> {
return this._data.delete(key)
}
// =========================================================================
// 辅助方法 | Helper methods
// =========================================================================
/**
* @zh 清空所有数据(测试用)
* @en Clear all data (for testing)
*/
clear(): void {
this._transactions.clear()
this._data.clear()
this._locks.clear()
}
/**
* @zh 获取事务数量
* @en Get transaction count
*/
get transactionCount(): number {
return this._transactions.size
}
private _cleanExpiredLocks(): void {
const now = Date.now()
for (const [key, lock] of this._locks) {
if (lock.expireAt < now) {
this._locks.delete(key)
}
}
}
private _cleanExpiredData(): void {
const now = Date.now()
for (const [key, entry] of this._data) {
if (entry.expireAt && entry.expireAt < now) {
this._data.delete(key)
}
}
}
private _cleanOldTransactions(): void {
const sorted = Array.from(this._transactions.entries())
.sort((a, b) => a[1].createdAt - b[1].createdAt)
const toRemove = sorted
.slice(0, Math.floor(this._maxTransactions * 0.2))
.filter(([_, tx]) => tx.state === 'committed' || tx.state === 'rolledback')
for (const [id] of toRemove) {
this._transactions.delete(id)
}
}
}
/**
* @zh 创建内存存储
* @en Create memory storage
*/
export function createMemoryStorage(config: MemoryStorageConfig = {}): MemoryStorage {
return new MemoryStorage(config)
}

View File

@@ -0,0 +1,303 @@
/**
* @zh MongoDB 存储实现
* @en MongoDB storage implementation
*
* @zh 支持持久化事务日志和查询
* @en Supports persistent transaction logs and queries
*/
import type {
ITransactionStorage,
TransactionLog,
TransactionState,
OperationLog,
} from '../core/types.js'
/**
* @zh MongoDB Collection 接口
* @en MongoDB Collection interface
*/
export interface MongoCollection<T> {
findOne(filter: object): Promise<T | null>
find(filter: object): {
toArray(): Promise<T[]>
}
insertOne(doc: T): Promise<{ insertedId: unknown }>
updateOne(filter: object, update: object): Promise<{ modifiedCount: number }>
deleteOne(filter: object): Promise<{ deletedCount: number }>
createIndex(spec: object, options?: object): Promise<string>
}
/**
* @zh MongoDB 数据库接口
* @en MongoDB database interface
*/
export interface MongoDb {
collection<T = unknown>(name: string): MongoCollection<T>
}
/**
* @zh MongoDB 存储配置
* @en MongoDB storage configuration
*/
export interface MongoStorageConfig {
/**
* @zh MongoDB 数据库实例
* @en MongoDB database instance
*/
db: MongoDb
/**
* @zh 事务日志集合名称
* @en Transaction log collection name
*/
transactionCollection?: string
/**
* @zh 数据集合名称
* @en Data collection name
*/
dataCollection?: string
/**
* @zh 锁集合名称
* @en Lock collection name
*/
lockCollection?: string
}
interface LockDocument {
_id: string
token: string
expireAt: Date
}
interface DataDocument {
_id: string
value: unknown
expireAt?: Date
}
/**
* @zh MongoDB 存储
* @en MongoDB storage
*
* @zh 基于 MongoDB 的事务存储,支持持久化和复杂查询
* @en MongoDB-based transaction storage with persistence and complex query support
*
* @example
* ```typescript
* import { MongoClient } from 'mongodb'
*
* const client = new MongoClient('mongodb://localhost:27017')
* await client.connect()
* const db = client.db('game')
*
* const storage = new MongoStorage({ db })
* await storage.ensureIndexes()
* ```
*/
export class MongoStorage implements ITransactionStorage {
private _db: MongoDb
private _transactionCollection: string
private _dataCollection: string
private _lockCollection: string
constructor(config: MongoStorageConfig) {
this._db = config.db
this._transactionCollection = config.transactionCollection ?? 'transactions'
this._dataCollection = config.dataCollection ?? 'transaction_data'
this._lockCollection = config.lockCollection ?? 'transaction_locks'
}
/**
* @zh 确保索引存在
* @en Ensure indexes exist
*/
async ensureIndexes(): Promise<void> {
const txColl = this._db.collection<TransactionLog>(this._transactionCollection)
await txColl.createIndex({ state: 1 })
await txColl.createIndex({ 'metadata.serverId': 1 })
await txColl.createIndex({ createdAt: 1 })
const lockColl = this._db.collection<LockDocument>(this._lockCollection)
await lockColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
const dataColl = this._db.collection<DataDocument>(this._dataCollection)
await dataColl.createIndex({ expireAt: 1 }, { expireAfterSeconds: 0 })
}
// =========================================================================
// 分布式锁 | Distributed Lock
// =========================================================================
async acquireLock(key: string, ttl: number): Promise<string | null> {
const coll = this._db.collection<LockDocument>(this._lockCollection)
const token = `${Date.now()}_${Math.random().toString(36).substring(2)}`
const expireAt = new Date(Date.now() + ttl)
try {
await coll.insertOne({
_id: key,
token,
expireAt,
})
return token
} catch (error) {
const existing = await coll.findOne({ _id: key })
if (existing && existing.expireAt < new Date()) {
const result = await coll.updateOne(
{ _id: key, expireAt: { $lt: new Date() } },
{ $set: { token, expireAt } }
)
if (result.modifiedCount > 0) {
return token
}
}
return null
}
}
async releaseLock(key: string, token: string): Promise<boolean> {
const coll = this._db.collection<LockDocument>(this._lockCollection)
const result = await coll.deleteOne({ _id: key, token })
return result.deletedCount > 0
}
// =========================================================================
// 事务日志 | Transaction Log
// =========================================================================
async saveTransaction(tx: TransactionLog): Promise<void> {
const coll = this._db.collection<TransactionLog & { _id: string }>(this._transactionCollection)
const existing = await coll.findOne({ _id: tx.id })
if (existing) {
await coll.updateOne(
{ _id: tx.id },
{ $set: { ...tx, _id: tx.id } }
)
} else {
await coll.insertOne({ ...tx, _id: tx.id })
}
}
async getTransaction(id: string): Promise<TransactionLog | null> {
const coll = this._db.collection<TransactionLog & { _id: string }>(this._transactionCollection)
const doc = await coll.findOne({ _id: id })
if (!doc) return null
const { _id, ...tx } = doc
return tx as TransactionLog
}
async updateTransactionState(id: string, state: TransactionState): Promise<void> {
const coll = this._db.collection(this._transactionCollection)
await coll.updateOne(
{ _id: id },
{ $set: { state, updatedAt: Date.now() } }
)
}
async updateOperationState(
transactionId: string,
operationIndex: number,
state: OperationLog['state'],
error?: string
): Promise<void> {
const coll = this._db.collection(this._transactionCollection)
const update: Record<string, unknown> = {
[`operations.${operationIndex}.state`]: state,
updatedAt: Date.now(),
}
if (error) {
update[`operations.${operationIndex}.error`] = error
}
if (state === 'executed') {
update[`operations.${operationIndex}.executedAt`] = Date.now()
} else if (state === 'compensated') {
update[`operations.${operationIndex}.compensatedAt`] = Date.now()
}
await coll.updateOne(
{ _id: transactionId },
{ $set: update }
)
}
async getPendingTransactions(serverId?: string): Promise<TransactionLog[]> {
const coll = this._db.collection<TransactionLog & { _id: string }>(this._transactionCollection)
const filter: Record<string, unknown> = {
state: { $in: ['pending', 'executing'] },
}
if (serverId) {
filter['metadata.serverId'] = serverId
}
const docs = await coll.find(filter).toArray()
return docs.map(({ _id, ...tx }) => tx as TransactionLog)
}
async deleteTransaction(id: string): Promise<void> {
const coll = this._db.collection(this._transactionCollection)
await coll.deleteOne({ _id: id })
}
// =========================================================================
// 数据操作 | Data Operations
// =========================================================================
async get<T>(key: string): Promise<T | null> {
const coll = this._db.collection<DataDocument>(this._dataCollection)
const doc = await coll.findOne({ _id: key })
if (!doc) return null
if (doc.expireAt && doc.expireAt < new Date()) {
await coll.deleteOne({ _id: key })
return null
}
return doc.value as T
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const coll = this._db.collection<DataDocument>(this._dataCollection)
const doc: DataDocument = {
_id: key,
value,
}
if (ttl) {
doc.expireAt = new Date(Date.now() + ttl)
}
const existing = await coll.findOne({ _id: key })
if (existing) {
await coll.updateOne({ _id: key }, { $set: doc })
} else {
await coll.insertOne(doc)
}
}
async delete(key: string): Promise<boolean> {
const coll = this._db.collection(this._dataCollection)
const result = await coll.deleteOne({ _id: key })
return result.deletedCount > 0
}
}
/**
* @zh 创建 MongoDB 存储
* @en Create MongoDB storage
*/
export function createMongoStorage(config: MongoStorageConfig): MongoStorage {
return new MongoStorage(config)
}

View File

@@ -0,0 +1,244 @@
/**
* @zh Redis 存储实现
* @en Redis storage implementation
*
* @zh 支持分布式锁和快速缓存
* @en Supports distributed locking and fast caching
*/
import type {
ITransactionStorage,
TransactionLog,
TransactionState,
OperationLog,
} from '../core/types.js'
/**
* @zh Redis 客户端接口(兼容 ioredis
* @en Redis client interface (compatible with ioredis)
*/
export interface RedisClient {
get(key: string): Promise<string | null>
set(key: string, value: string, ...args: string[]): Promise<string | null>
del(...keys: string[]): Promise<number>
eval(script: string, numkeys: number, ...args: (string | number)[]): Promise<unknown>
hget(key: string, field: string): Promise<string | null>
hset(key: string, ...args: (string | number)[]): Promise<number>
hdel(key: string, ...fields: string[]): Promise<number>
hgetall(key: string): Promise<Record<string, string>>
keys(pattern: string): Promise<string[]>
expire(key: string, seconds: number): Promise<number>
}
/**
* @zh Redis 存储配置
* @en Redis storage configuration
*/
export interface RedisStorageConfig {
/**
* @zh Redis 客户端实例
* @en Redis client instance
*/
client: RedisClient
/**
* @zh 键前缀
* @en Key prefix
*/
prefix?: string
/**
* @zh 事务日志过期时间(秒)
* @en Transaction log expiration time in seconds
*/
transactionTTL?: number
}
const LOCK_SCRIPT = `
if redis.call("get", KEYS[1]) == ARGV[1] then
return redis.call("del", KEYS[1])
else
return 0
end
`
/**
* @zh Redis 存储
* @en Redis storage
*
* @zh 基于 Redis 的分布式事务存储,支持分布式锁
* @en Redis-based distributed transaction storage with distributed locking support
*
* @example
* ```typescript
* import Redis from 'ioredis'
*
* const redis = new Redis('redis://localhost:6379')
* const storage = new RedisStorage({ client: redis })
* ```
*/
export class RedisStorage implements ITransactionStorage {
private _client: RedisClient
private _prefix: string
private _transactionTTL: number
constructor(config: RedisStorageConfig) {
this._client = config.client
this._prefix = config.prefix ?? 'tx:'
this._transactionTTL = config.transactionTTL ?? 86400 // 24 hours
}
// =========================================================================
// 分布式锁 | Distributed Lock
// =========================================================================
async acquireLock(key: string, ttl: number): Promise<string | null> {
const lockKey = `${this._prefix}lock:${key}`
const token = `${Date.now()}_${Math.random().toString(36).substring(2)}`
const ttlSeconds = Math.ceil(ttl / 1000)
const result = await this._client.set(lockKey, token, 'NX', 'EX', String(ttlSeconds))
return result === 'OK' ? token : null
}
async releaseLock(key: string, token: string): Promise<boolean> {
const lockKey = `${this._prefix}lock:${key}`
const result = await this._client.eval(LOCK_SCRIPT, 1, lockKey, token)
return result === 1
}
// =========================================================================
// 事务日志 | Transaction Log
// =========================================================================
async saveTransaction(tx: TransactionLog): Promise<void> {
const key = `${this._prefix}tx:${tx.id}`
await this._client.set(key, JSON.stringify(tx))
await this._client.expire(key, this._transactionTTL)
if (tx.metadata?.serverId) {
const serverKey = `${this._prefix}server:${tx.metadata.serverId}:txs`
await this._client.hset(serverKey, tx.id, String(tx.createdAt))
}
}
async getTransaction(id: string): Promise<TransactionLog | null> {
const key = `${this._prefix}tx:${id}`
const data = await this._client.get(key)
return data ? JSON.parse(data) : null
}
async updateTransactionState(id: string, state: TransactionState): Promise<void> {
const tx = await this.getTransaction(id)
if (tx) {
tx.state = state
tx.updatedAt = Date.now()
await this.saveTransaction(tx)
}
}
async updateOperationState(
transactionId: string,
operationIndex: number,
state: OperationLog['state'],
error?: string
): Promise<void> {
const tx = await this.getTransaction(transactionId)
if (tx && tx.operations[operationIndex]) {
tx.operations[operationIndex].state = state
if (error) {
tx.operations[operationIndex].error = error
}
if (state === 'executed') {
tx.operations[operationIndex].executedAt = Date.now()
} else if (state === 'compensated') {
tx.operations[operationIndex].compensatedAt = Date.now()
}
tx.updatedAt = Date.now()
await this.saveTransaction(tx)
}
}
async getPendingTransactions(serverId?: string): Promise<TransactionLog[]> {
const result: TransactionLog[] = []
if (serverId) {
const serverKey = `${this._prefix}server:${serverId}:txs`
const txIds = await this._client.hgetall(serverKey)
for (const id of Object.keys(txIds)) {
const tx = await this.getTransaction(id)
if (tx && (tx.state === 'pending' || tx.state === 'executing')) {
result.push(tx)
}
}
} else {
const pattern = `${this._prefix}tx:*`
const keys = await this._client.keys(pattern)
for (const key of keys) {
const data = await this._client.get(key)
if (data) {
const tx: TransactionLog = JSON.parse(data)
if (tx.state === 'pending' || tx.state === 'executing') {
result.push(tx)
}
}
}
}
return result
}
async deleteTransaction(id: string): Promise<void> {
const key = `${this._prefix}tx:${id}`
const tx = await this.getTransaction(id)
await this._client.del(key)
if (tx?.metadata?.serverId) {
const serverKey = `${this._prefix}server:${tx.metadata.serverId}:txs`
await this._client.hdel(serverKey, id)
}
}
// =========================================================================
// 数据操作 | Data Operations
// =========================================================================
async get<T>(key: string): Promise<T | null> {
const fullKey = `${this._prefix}data:${key}`
const data = await this._client.get(fullKey)
return data ? JSON.parse(data) : null
}
async set<T>(key: string, value: T, ttl?: number): Promise<void> {
const fullKey = `${this._prefix}data:${key}`
if (ttl) {
const ttlSeconds = Math.ceil(ttl / 1000)
await this._client.set(fullKey, JSON.stringify(value), 'EX', String(ttlSeconds))
} else {
await this._client.set(fullKey, JSON.stringify(value))
}
}
async delete(key: string): Promise<boolean> {
const fullKey = `${this._prefix}data:${key}`
const result = await this._client.del(fullKey)
return result > 0
}
}
/**
* @zh 创建 Redis 存储
* @en Create Redis storage
*/
export function createRedisStorage(config: RedisStorageConfig): RedisStorage {
return new RedisStorage(config)
}

View File

@@ -0,0 +1,8 @@
/**
* @zh 存储模块导出
* @en Storage module exports
*/
export { MemoryStorage, createMemoryStorage, type MemoryStorageConfig } from './MemoryStorage.js'
export { RedisStorage, createRedisStorage, type RedisStorageConfig, type RedisClient } from './RedisStorage.js'
export { MongoStorage, createMongoStorage, type MongoStorageConfig, type MongoDb, type MongoCollection } from './MongoStorage.js'

View File

@@ -0,0 +1,20 @@
/**
* @zh Transaction 模块服务令牌
* @en Transaction module service tokens
*/
import { createServiceToken } from '@esengine/ecs-framework'
import type { TransactionManager } from './core/TransactionManager.js'
import type { ITransactionStorage } from './core/types.js'
/**
* @zh 事务管理器令牌
* @en Transaction manager token
*/
export const TransactionManagerToken = createServiceToken<TransactionManager>('transactionManager')
/**
* @zh 事务存储令牌
* @en Transaction storage token
*/
export const TransactionStorageToken = createServiceToken<ITransactionStorage>('transactionStorage')

View File

@@ -0,0 +1,12 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"composite": false,
"declaration": true,
"declarationMap": true,
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}

View File

@@ -0,0 +1,16 @@
{
"extends": "../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"composite": true,
"declaration": true,
"declarationMap": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"],
"references": [
{ "path": "../../core" },
{ "path": "../server" }
]
}

View File

@@ -0,0 +1,12 @@
import { defineConfig } from 'tsup'
import { runtimeOnlyPreset } from '../../tools/build-config/src/presets/plugin-tsup'
export default defineConfig({
...runtimeOnlyPreset({
external: ['ioredis', 'mongodb'],
}),
tsconfig: 'tsconfig.build.json',
// tsup 的 DTS bundler 无法正确解析 workspace 包的类型继承链
// tsup's DTS bundler cannot correctly resolve workspace package type inheritance
dts: false,
})

223
pnpm-lock.yaml generated
View File

@@ -158,16 +158,16 @@ importers:
dependencies: dependencies:
'@astrojs/starlight': '@astrojs/starlight':
specifier: ^0.37.1 specifier: ^0.37.1
version: 0.37.1(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) version: 0.37.1(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))
'@astrojs/vue': '@astrojs/vue':
specifier: ^5.1.3 specifier: ^5.1.3
version: 5.1.3(@types/node@22.19.3)(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2) version: 5.1.3(@types/node@22.19.3)(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: ^4.1.18 specifier: ^4.1.18
version: 4.1.18(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) version: 4.1.18(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
astro: astro:
specifier: ^5.6.1 specifier: ^5.6.1
version: 5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) version: 5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
sharp: sharp:
specifier: ^0.34.2 specifier: ^0.34.2
version: 0.34.5 version: 0.34.5
@@ -1763,6 +1763,34 @@ importers:
specifier: ^5.8.3 specifier: ^5.8.3
version: 5.9.3 version: 5.9.3
packages/framework/transaction:
dependencies:
'@esengine/server':
specifier: workspace:*
version: link:../server
ioredis:
specifier: ^5.3.0
version: 5.8.2
mongodb:
specifier: ^6.0.0
version: 6.21.0(socks@2.8.7)
devDependencies:
'@esengine/build-config':
specifier: workspace:*
version: link:../../tools/build-config
'@esengine/ecs-framework':
specifier: workspace:*
version: link:../core/dist
rimraf:
specifier: ^5.0.0
version: 5.0.10
tsup:
specifier: ^8.0.0
version: 8.5.1(@microsoft/api-extractor@7.55.2(@types/node@22.19.3))(@swc/core@1.15.7(@swc/helpers@0.5.18))(jiti@2.6.1)(postcss@8.5.6)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
typescript:
specifier: ^5.8.0
version: 5.9.3
packages/framework/world-streaming: packages/framework/world-streaming:
dependencies: dependencies:
'@esengine/ecs-framework': '@esengine/ecs-framework':
@@ -3824,6 +3852,9 @@ packages:
'@types/node': '@types/node':
optional: true optional: true
'@ioredis/commands@1.4.0':
resolution: {integrity: sha512-aFT2yemJJo+TZCmieA7qnYGQooOS7QfNmYrzGtsYd3g9j5iDP8AimYYAesf79ohjbLG12XxC4nG5DyEnC88AsQ==}
'@isaacs/balanced-match@4.0.1': '@isaacs/balanced-match@4.0.1':
resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==} resolution: {integrity: sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==}
engines: {node: 20 || >=22} engines: {node: 20 || >=22}
@@ -3984,6 +4015,9 @@ packages:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0
'@mongodb-js/saslprep@1.4.4':
resolution: {integrity: sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==}
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==} resolution: {integrity: sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw==}
cpu: [arm64] cpu: [arm64]
@@ -5238,6 +5272,12 @@ packages:
'@types/web-bluetooth@0.0.21': '@types/web-bluetooth@0.0.21':
resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==} resolution: {integrity: sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA==}
'@types/webidl-conversions@7.0.3':
resolution: {integrity: sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==}
'@types/whatwg-url@11.0.5':
resolution: {integrity: sha512-coYR071JRaHa+xoEvvYqvnIHaVqaYrLPbsufM9BF63HkwI5Lgmy2QR8Q5K/lYDYo5AK82wOvSOS0UsLTpTG7uQ==}
'@types/ws@8.18.1': '@types/ws@8.18.1':
resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==} resolution: {integrity: sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==}
@@ -5825,6 +5865,10 @@ packages:
bser@2.1.1: bser@2.1.1:
resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==} resolution: {integrity: sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==}
bson@6.10.4:
resolution: {integrity: sha512-WIsKqkSC0ABoBJuT1LEX+2HEvNmNKKgnTAyd0fL8qzK4SH2i9NXg+t08YtdZp/V9IZ33cxe3iV4yM0qg8lMQng==}
engines: {node: '>=16.20.1'}
buffer-from@1.1.2: buffer-from@1.1.2:
resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==}
@@ -6011,6 +6055,10 @@ packages:
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
engines: {node: '>=6'} engines: {node: '>=6'}
cluster-key-slot@1.1.2:
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
engines: {node: '>=0.10.0'}
cmd-shim@6.0.3: cmd-shim@6.0.3:
resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==} resolution: {integrity: sha512-FMabTRlc5t5zjdenF6mS0MBeFZm0XqHqeOkcskKFb/LYCcRQ5fVgLOHVc4Lq9CqABd9zhjwPjMBCJvMCziSVtA==}
engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0}
@@ -6378,6 +6426,10 @@ packages:
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
engines: {node: '>=0.4.0'} engines: {node: '>=0.4.0'}
denque@2.1.0:
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
engines: {node: '>=0.10'}
deprecation@2.3.1: deprecation@2.3.1:
resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==} resolution: {integrity: sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ==}
@@ -7400,6 +7452,10 @@ packages:
resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==} resolution: {integrity: sha512-CYdFeFexxhv/Bcny+Q0BfOV+ltRlJcd4BBZBYFX/O0u4npJrgZtIcjokegtiSMAvlMTJ+Koq0GBCc//3bueQxw==}
engines: {node: '>=8'} engines: {node: '>=8'}
ioredis@5.8.2:
resolution: {integrity: sha512-C6uC+kleiIMmjViJINWk80sOQw5lEzse1ZmvD+S/s8p8CWapftSaC+kocGTx6xrbrJ4WmYQGC08ffHLr6ToR6Q==}
engines: {node: '>=12.22.0'}
ip-address@10.1.0: ip-address@10.1.0:
resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==} resolution: {integrity: sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==}
engines: {node: '>= 12'} engines: {node: '>= 12'}
@@ -8052,6 +8108,9 @@ packages:
lodash.debounce@4.0.8: lodash.debounce@4.0.8:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==}
lodash.defaults@4.2.0:
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
lodash.escaperegexp@4.1.2: lodash.escaperegexp@4.1.2:
resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==} resolution: {integrity: sha512-TM9YBvyC84ZxE3rgfefxUWiQKLilstD6k7PTGt6wfbtXF8ixIJLOL3VYyV/z+ZiPLsVxAsKAFVwWlWeb2Y8Yyw==}
@@ -8059,6 +8118,9 @@ packages:
resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==} resolution: {integrity: sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==}
deprecated: This package is deprecated. Use the optional chaining (?.) operator instead. deprecated: This package is deprecated. Use the optional chaining (?.) operator instead.
lodash.isarguments@3.1.0:
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
lodash.isequal@4.5.0: lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead. deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
@@ -8286,6 +8348,9 @@ packages:
resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==} resolution: {integrity: sha512-qvwipnozMohxLXG1pOqoLiZKNkC4r4qqRucSoDwXowsNGDSULiqFTRUF05vcZWnwJSG22qTsynQhxbaMtnX9gw==}
engines: {node: '>=8'} engines: {node: '>=8'}
memory-pager@1.5.0:
resolution: {integrity: sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==}
meow@12.1.1: meow@12.1.1:
resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==} resolution: {integrity: sha512-BhXM0Au22RwUneMPwSCnyhTOizdWoIEPU9sp0Aqa1PnDMR5Wv2FGXYDjuzJEIX+Eo2Rb8xuYe5jrnm5QowQFkw==}
engines: {node: '>=16.10'} engines: {node: '>=16.10'}
@@ -8543,6 +8608,36 @@ packages:
monaco-editor@0.55.1: monaco-editor@0.55.1:
resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==} resolution: {integrity: sha512-jz4x+TJNFHwHtwuV9vA9rMujcZRb0CEilTEwG2rRSpe/A7Jdkuj8xPKttCgOh+v/lkHy7HsZ64oj+q3xoAFl9A==}
mongodb-connection-string-url@3.0.2:
resolution: {integrity: sha512-rMO7CGo/9BFwyZABcKAWL8UJwH/Kc2x0g72uhDWzG48URRax5TCIcJ7Rc3RZqffZzO/Gwff/jyKwCU9TN8gehA==}
mongodb@6.21.0:
resolution: {integrity: sha512-URyb/VXMjJ4da46OeSXg+puO39XH9DeQpWCslifrRn9JWugy0D+DvvBvkm2WxmHe61O/H19JM66p1z7RHVkZ6A==}
engines: {node: '>=16.20.1'}
peerDependencies:
'@aws-sdk/credential-providers': ^3.188.0
'@mongodb-js/zstd': ^1.1.0 || ^2.0.0
gcp-metadata: ^5.2.0
kerberos: ^2.0.1
mongodb-client-encryption: '>=6.0.0 <7'
snappy: ^7.3.2
socks: ^2.7.1
peerDependenciesMeta:
'@aws-sdk/credential-providers':
optional: true
'@mongodb-js/zstd':
optional: true
gcp-metadata:
optional: true
kerberos:
optional: true
mongodb-client-encryption:
optional: true
snappy:
optional: true
socks:
optional: true
mri@1.2.0: mri@1.2.0:
resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==} resolution: {integrity: sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==}
engines: {node: '>=4'} engines: {node: '>=4'}
@@ -9457,6 +9552,14 @@ packages:
resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==} resolution: {integrity: sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==}
engines: {node: '>=8'} engines: {node: '>=8'}
redis-errors@1.2.0:
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
engines: {node: '>=4'}
redis-parser@3.0.0:
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
engines: {node: '>=4'}
reflect-metadata@0.2.2: reflect-metadata@0.2.2:
resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==} resolution: {integrity: sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==}
@@ -9843,6 +9946,9 @@ packages:
space-separated-tokens@2.0.2: space-separated-tokens@2.0.2:
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
sparse-bitfield@3.0.3:
resolution: {integrity: sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==}
spawn-error-forwarder@1.0.0: spawn-error-forwarder@1.0.0:
resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==} resolution: {integrity: sha512-gRjMgK5uFjbCvdibeGJuy3I5OYz6VLoVdsOJdA6wV0WlfQVLFueoqMxwwYD9RODdgb6oUIvlRlsyFSiQkMKu0g==}
@@ -9889,6 +9995,9 @@ packages:
resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==} resolution: {integrity: sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==}
engines: {node: '>=10'} engines: {node: '>=10'}
standard-as-callback@2.1.0:
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
state-local@1.0.7: state-local@1.0.7:
resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==} resolution: {integrity: sha512-HTEHMNieakEnoe33shBYcZ7NX83ACUjCu8c40iOGEZsngj9zRnkqS9j1pqQPXwobB0ZcVTk27REb7COQ0UR59w==}
@@ -10141,6 +10250,10 @@ packages:
resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==} resolution: {integrity: sha512-l7FvfAHlcmulp8kr+flpQZmVwtu7nfRV7NZujtN0OqES8EL4O4e0qqzL0DC5gAvx/ZC/9lk6rhcUwYvkBnBnYA==}
engines: {node: '>=12'} engines: {node: '>=12'}
tr46@5.1.1:
resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==}
engines: {node: '>=18'}
traverse@0.6.8: traverse@0.6.8:
resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==} resolution: {integrity: sha512-aXJDbk6SnumuaZSANd21XAo15ucCDE38H4fkqiGsc3MhCK+wOlZvLP9cB/TvpHT0mOyWgC4Z8EwRlzqYSUzdsA==}
engines: {node: '>= 0.4'} engines: {node: '>= 0.4'}
@@ -10904,6 +11017,10 @@ packages:
resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==} resolution: {integrity: sha512-RKT8HExMpoYx4igMiVMY83lN6UeITKJlBQ+vR/8ZJ8OCdSiN3RwCq+9gH0+Xzj0+5IrM6i4j/6LuvzbZIQgEcQ==}
engines: {node: '>=12'} engines: {node: '>=12'}
whatwg-url@14.2.0:
resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==}
engines: {node: '>=18'}
whatwg-url@5.0.0: whatwg-url@5.0.0:
resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==}
@@ -11262,12 +11379,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@astrojs/mdx@4.3.13(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))': '@astrojs/mdx@4.3.13(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))':
dependencies: dependencies:
'@astrojs/markdown-remark': 6.3.10 '@astrojs/markdown-remark': 6.3.10
'@mdx-js/mdx': 3.1.1 '@mdx-js/mdx': 3.1.1
acorn: 8.15.0 acorn: 8.15.0
astro: 5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) astro: 5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
es-module-lexer: 1.7.0 es-module-lexer: 1.7.0
estree-util-visit: 2.0.0 estree-util-visit: 2.0.0
hast-util-to-html: 9.0.5 hast-util-to-html: 9.0.5
@@ -11291,17 +11408,17 @@ snapshots:
stream-replace-string: 2.0.0 stream-replace-string: 2.0.0
zod: 3.25.76 zod: 3.25.76
'@astrojs/starlight@0.37.1(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))': '@astrojs/starlight@0.37.1(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))':
dependencies: dependencies:
'@astrojs/markdown-remark': 6.3.10 '@astrojs/markdown-remark': 6.3.10
'@astrojs/mdx': 4.3.13(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) '@astrojs/mdx': 4.3.13(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))
'@astrojs/sitemap': 3.6.0 '@astrojs/sitemap': 3.6.0
'@pagefind/default-ui': 1.4.0 '@pagefind/default-ui': 1.4.0
'@types/hast': 3.0.4 '@types/hast': 3.0.4
'@types/js-yaml': 4.0.9 '@types/js-yaml': 4.0.9
'@types/mdast': 4.0.4 '@types/mdast': 4.0.4
astro: 5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) astro: 5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
astro-expressive-code: 0.41.5(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)) astro-expressive-code: 0.41.5(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))
bcp-47: 2.1.0 bcp-47: 2.1.0
hast-util-from-html: 2.0.3 hast-util-from-html: 2.0.3
hast-util-select: 6.0.4 hast-util-select: 6.0.4
@@ -11337,12 +11454,12 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
'@astrojs/vue@5.1.3(@types/node@22.19.3)(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)': '@astrojs/vue@5.1.3(@types/node@22.19.3)(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2))(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(vue@3.5.26(typescript@5.9.3))(yaml@2.8.2)':
dependencies: dependencies:
'@vitejs/plugin-vue': 5.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@vitejs/plugin-vue': 5.2.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
'@vitejs/plugin-vue-jsx': 4.2.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) '@vitejs/plugin-vue-jsx': 4.2.0(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
'@vue/compiler-sfc': 3.5.26 '@vue/compiler-sfc': 3.5.26
astro: 5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) astro: 5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vite-plugin-vue-devtools: 7.7.9(rollup@4.54.0)(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3)) vite-plugin-vue-devtools: 7.7.9(rollup@4.54.0)(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))(vue@3.5.26(typescript@5.9.3))
vue: 3.5.26(typescript@5.9.3) vue: 3.5.26(typescript@5.9.3)
@@ -12902,6 +13019,8 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 22.19.3 '@types/node': 22.19.3
'@ioredis/commands@1.4.0': {}
'@isaacs/balanced-match@4.0.1': {} '@isaacs/balanced-match@4.0.1': {}
'@isaacs/brace-expansion@5.0.0': '@isaacs/brace-expansion@5.0.0':
@@ -13359,6 +13478,10 @@ snapshots:
react: 18.3.1 react: 18.3.1
react-dom: 18.3.1(react@18.3.1) react-dom: 18.3.1(react@18.3.1)
'@mongodb-js/saslprep@1.4.4':
dependencies:
sparse-bitfield: 3.0.3
'@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3': '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3':
optional: true optional: true
@@ -14732,6 +14855,12 @@ snapshots:
'@types/web-bluetooth@0.0.21': {} '@types/web-bluetooth@0.0.21': {}
'@types/webidl-conversions@7.0.3': {}
'@types/whatwg-url@11.0.5':
dependencies:
'@types/webidl-conversions': 7.0.3
'@types/ws@8.18.1': '@types/ws@8.18.1':
dependencies: dependencies:
'@types/node': 20.19.27 '@types/node': 20.19.27
@@ -15280,12 +15409,12 @@ snapshots:
astring@1.9.0: {} astring@1.9.0: {}
astro-expressive-code@0.41.5(astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)): astro-expressive-code@0.41.5(astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)):
dependencies: dependencies:
astro: 5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2) astro: 5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2)
rehype-expressive-code: 0.41.5 rehype-expressive-code: 0.41.5
astro@5.16.6(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2): astro@5.16.6(@types/node@22.19.3)(ioredis@5.8.2)(jiti@2.6.1)(lightningcss@1.30.2)(rollup@4.54.0)(terser@5.44.1)(tsx@4.21.0)(typescript@5.9.3)(yaml@2.8.2):
dependencies: dependencies:
'@astrojs/compiler': 2.13.0 '@astrojs/compiler': 2.13.0
'@astrojs/internal-helpers': 0.7.5 '@astrojs/internal-helpers': 0.7.5
@@ -15340,7 +15469,7 @@ snapshots:
ultrahtml: 1.6.0 ultrahtml: 1.6.0
unifont: 0.6.0 unifont: 0.6.0
unist-util-visit: 5.0.0 unist-util-visit: 5.0.0
unstorage: 1.17.3 unstorage: 1.17.3(ioredis@5.8.2)
vfile: 6.0.3 vfile: 6.0.3
vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2) vite: 6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)
vitefu: 1.1.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2)) vitefu: 1.1.1(vite@6.4.1(@types/node@22.19.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.1)(tsx@4.21.0)(yaml@2.8.2))
@@ -15571,6 +15700,8 @@ snapshots:
dependencies: dependencies:
node-int64: 0.4.0 node-int64: 0.4.0
bson@6.10.4: {}
buffer-from@1.1.2: {} buffer-from@1.1.2: {}
buffer@5.7.1: buffer@5.7.1:
@@ -15750,6 +15881,8 @@ snapshots:
clsx@2.1.1: {} clsx@2.1.1: {}
cluster-key-slot@1.1.2: {}
cmd-shim@6.0.3: {} cmd-shim@6.0.3: {}
co@4.6.0: {} co@4.6.0: {}
@@ -16094,6 +16227,8 @@ snapshots:
delayed-stream@1.0.0: {} delayed-stream@1.0.0: {}
denque@2.1.0: {}
deprecation@2.3.1: {} deprecation@2.3.1: {}
dequal@2.0.3: {} dequal@2.0.3: {}
@@ -17421,6 +17556,20 @@ snapshots:
invert-kv@3.0.1: {} invert-kv@3.0.1: {}
ioredis@5.8.2:
dependencies:
'@ioredis/commands': 1.4.0
cluster-key-slot: 1.1.2
debug: 4.4.3
denque: 2.1.0
lodash.defaults: 4.2.0
lodash.isarguments: 3.1.0
redis-errors: 1.2.0
redis-parser: 3.0.0
standard-as-callback: 2.1.0
transitivePeerDependencies:
- supports-color
ip-address@10.1.0: {} ip-address@10.1.0: {}
iron-webcrypto@1.2.1: {} iron-webcrypto@1.2.1: {}
@@ -18307,10 +18456,14 @@ snapshots:
lodash.debounce@4.0.8: {} lodash.debounce@4.0.8: {}
lodash.defaults@4.2.0: {}
lodash.escaperegexp@4.1.2: {} lodash.escaperegexp@4.1.2: {}
lodash.get@4.4.2: {} lodash.get@4.4.2: {}
lodash.isarguments@3.1.0: {}
lodash.isequal@4.5.0: {} lodash.isequal@4.5.0: {}
lodash.isfunction@3.0.9: {} lodash.isfunction@3.0.9: {}
@@ -18662,6 +18815,8 @@ snapshots:
mimic-fn: 2.1.0 mimic-fn: 2.1.0
p-is-promise: 2.1.0 p-is-promise: 2.1.0
memory-pager@1.5.0: {}
meow@12.1.1: {} meow@12.1.1: {}
meow@8.1.2: meow@8.1.2:
@@ -19078,6 +19233,19 @@ snapshots:
dompurify: 3.2.7 dompurify: 3.2.7
marked: 14.0.0 marked: 14.0.0
mongodb-connection-string-url@3.0.2:
dependencies:
'@types/whatwg-url': 11.0.5
whatwg-url: 14.2.0
mongodb@6.21.0(socks@2.8.7):
dependencies:
'@mongodb-js/saslprep': 1.4.4
bson: 6.10.4
mongodb-connection-string-url: 3.0.2
optionalDependencies:
socks: 2.8.7
mri@1.2.0: {} mri@1.2.0: {}
mrmime@2.0.1: {} mrmime@2.0.1: {}
@@ -20008,6 +20176,12 @@ snapshots:
indent-string: 4.0.0 indent-string: 4.0.0
strip-indent: 3.0.0 strip-indent: 3.0.0
redis-errors@1.2.0: {}
redis-parser@3.0.0:
dependencies:
redis-errors: 1.2.0
reflect-metadata@0.2.2: {} reflect-metadata@0.2.2: {}
regenerate-unicode-properties@10.2.2: regenerate-unicode-properties@10.2.2:
@@ -20545,6 +20719,10 @@ snapshots:
space-separated-tokens@2.0.2: {} space-separated-tokens@2.0.2: {}
sparse-bitfield@3.0.3:
dependencies:
memory-pager: 1.5.0
spawn-error-forwarder@1.0.0: {} spawn-error-forwarder@1.0.0: {}
spawndamnit@3.0.1: spawndamnit@3.0.1:
@@ -20592,6 +20770,8 @@ snapshots:
dependencies: dependencies:
escape-string-regexp: 2.0.0 escape-string-regexp: 2.0.0
standard-as-callback@2.1.0: {}
state-local@1.0.7: {} state-local@1.0.7: {}
stream-combiner2@1.1.1: stream-combiner2@1.1.1:
@@ -20853,6 +21033,10 @@ snapshots:
dependencies: dependencies:
punycode: 2.3.1 punycode: 2.3.1
tr46@5.1.1:
dependencies:
punycode: 2.3.1
traverse@0.6.8: {} traverse@0.6.8: {}
tree-kill@1.2.2: {} tree-kill@1.2.2: {}
@@ -21321,7 +21505,7 @@ snapshots:
picomatch: 4.0.3 picomatch: 4.0.3
webpack-virtual-modules: 0.6.2 webpack-virtual-modules: 0.6.2
unstorage@1.17.3: unstorage@1.17.3(ioredis@5.8.2):
dependencies: dependencies:
anymatch: 3.1.3 anymatch: 3.1.3
chokidar: 4.0.3 chokidar: 4.0.3
@@ -21331,6 +21515,8 @@ snapshots:
node-fetch-native: 1.6.7 node-fetch-native: 1.6.7
ofetch: 1.5.1 ofetch: 1.5.1
ufo: 1.6.1 ufo: 1.6.1
optionalDependencies:
ioredis: 5.8.2
upath@2.0.1: {} upath@2.0.1: {}
@@ -21661,6 +21847,11 @@ snapshots:
tr46: 3.0.0 tr46: 3.0.0
webidl-conversions: 7.0.0 webidl-conversions: 7.0.0
whatwg-url@14.2.0:
dependencies:
tr46: 5.1.1
webidl-conversions: 7.0.0
whatwg-url@5.0.0: whatwg-url@5.0.0:
dependencies: dependencies:
tr46: 0.0.3 tr46: 0.0.3