feat(transaction): 添加游戏事务系统 | add game transaction system (#381)
- TransactionManager/TransactionContext 事务管理 - MemoryStorage/RedisStorage/MongoStorage 存储实现 - CurrencyOperation/InventoryOperation/TradeOperation 内置操作 - SagaOrchestrator 分布式 Saga 编排 - withTransactions() Room 集成 - 完整中英文文档
This commit is contained in:
229
packages/framework/transaction/src/storage/MemoryStorage.ts
Normal file
229
packages/framework/transaction/src/storage/MemoryStorage.ts
Normal 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)
|
||||
}
|
||||
303
packages/framework/transaction/src/storage/MongoStorage.ts
Normal file
303
packages/framework/transaction/src/storage/MongoStorage.ts
Normal 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)
|
||||
}
|
||||
244
packages/framework/transaction/src/storage/RedisStorage.ts
Normal file
244
packages/framework/transaction/src/storage/RedisStorage.ts
Normal 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)
|
||||
}
|
||||
8
packages/framework/transaction/src/storage/index.ts
Normal file
8
packages/framework/transaction/src/storage/index.ts
Normal 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'
|
||||
Reference in New Issue
Block a user