@esengine/transaction 2.0.7 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/core/TransactionContext.d.ts +79 -0
- package/dist/core/TransactionContext.d.ts.map +1 -0
- package/dist/core/TransactionManager.d.ts +104 -0
- package/dist/core/TransactionManager.d.ts.map +1 -0
- package/dist/core/index.d.ts +8 -0
- package/dist/core/index.d.ts.map +1 -0
- package/dist/core/types.d.ts +393 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/distributed/SagaOrchestrator.d.ts +173 -0
- package/dist/distributed/SagaOrchestrator.d.ts.map +1 -0
- package/dist/distributed/index.d.ts +6 -0
- package/dist/distributed/index.d.ts.map +1 -0
- package/dist/index.d.ts +56 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1621 -0
- package/dist/index.js.map +1 -0
- package/dist/integration/RoomTransactionMixin.d.ts +108 -0
- package/dist/integration/RoomTransactionMixin.d.ts.map +1 -0
- package/dist/integration/index.d.ts +6 -0
- package/dist/integration/index.d.ts.map +1 -0
- package/dist/operations/BaseOperation.d.ts +43 -0
- package/dist/operations/BaseOperation.d.ts.map +1 -0
- package/dist/operations/CurrencyOperation.d.ts +122 -0
- package/dist/operations/CurrencyOperation.d.ts.map +1 -0
- package/dist/operations/InventoryOperation.d.ts +152 -0
- package/dist/operations/InventoryOperation.d.ts.map +1 -0
- package/dist/operations/TradeOperation.d.ts +155 -0
- package/dist/operations/TradeOperation.d.ts.map +1 -0
- package/dist/operations/index.d.ts +9 -0
- package/dist/operations/index.d.ts.map +1 -0
- package/dist/storage/MemoryStorage.d.ts +63 -0
- package/dist/storage/MemoryStorage.d.ts.map +1 -0
- package/dist/storage/MongoStorage.d.ts +118 -0
- package/dist/storage/MongoStorage.d.ts.map +1 -0
- package/dist/storage/RedisStorage.d.ts +125 -0
- package/dist/storage/RedisStorage.d.ts.map +1 -0
- package/dist/storage/index.d.ts +8 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/tokens.d.ts +17 -0
- package/dist/tokens.d.ts.map +1 -0
- package/package.json +2 -2
package/dist/index.js
ADDED
|
@@ -0,0 +1,1621 @@
|
|
|
1
|
+
var __defProp = Object.defineProperty;
|
|
2
|
+
var __defNormalProp = (obj, key, value) => key in obj ? __defProp(obj, key, { enumerable: true, configurable: true, writable: true, value }) : obj[key] = value;
|
|
3
|
+
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
4
|
+
var __publicField = (obj, key, value) => __defNormalProp(obj, typeof key !== "symbol" ? key + "" : key, value);
|
|
5
|
+
|
|
6
|
+
// src/core/TransactionContext.ts
|
|
7
|
+
function generateId() {
|
|
8
|
+
return `tx_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 11)}`;
|
|
9
|
+
}
|
|
10
|
+
__name(generateId, "generateId");
|
|
11
|
+
var _TransactionContext = class _TransactionContext {
|
|
12
|
+
constructor(options = {}) {
|
|
13
|
+
__publicField(this, "_id");
|
|
14
|
+
__publicField(this, "_state", "pending");
|
|
15
|
+
__publicField(this, "_timeout");
|
|
16
|
+
__publicField(this, "_operations", []);
|
|
17
|
+
__publicField(this, "_storage");
|
|
18
|
+
__publicField(this, "_metadata");
|
|
19
|
+
__publicField(this, "_contextData", /* @__PURE__ */ new Map());
|
|
20
|
+
__publicField(this, "_startTime", 0);
|
|
21
|
+
__publicField(this, "_distributed");
|
|
22
|
+
this._id = generateId();
|
|
23
|
+
this._timeout = options.timeout ?? 3e4;
|
|
24
|
+
this._storage = options.storage ?? null;
|
|
25
|
+
this._metadata = options.metadata ?? {};
|
|
26
|
+
this._distributed = options.distributed ?? false;
|
|
27
|
+
}
|
|
28
|
+
// =========================================================================
|
|
29
|
+
// 只读属性 | Readonly properties
|
|
30
|
+
// =========================================================================
|
|
31
|
+
get id() {
|
|
32
|
+
return this._id;
|
|
33
|
+
}
|
|
34
|
+
get state() {
|
|
35
|
+
return this._state;
|
|
36
|
+
}
|
|
37
|
+
get timeout() {
|
|
38
|
+
return this._timeout;
|
|
39
|
+
}
|
|
40
|
+
get operations() {
|
|
41
|
+
return this._operations;
|
|
42
|
+
}
|
|
43
|
+
get storage() {
|
|
44
|
+
return this._storage;
|
|
45
|
+
}
|
|
46
|
+
get metadata() {
|
|
47
|
+
return this._metadata;
|
|
48
|
+
}
|
|
49
|
+
// =========================================================================
|
|
50
|
+
// 公共方法 | Public methods
|
|
51
|
+
// =========================================================================
|
|
52
|
+
/**
|
|
53
|
+
* @zh 添加操作
|
|
54
|
+
* @en Add operation
|
|
55
|
+
*/
|
|
56
|
+
addOperation(operation) {
|
|
57
|
+
if (this._state !== "pending") {
|
|
58
|
+
throw new Error(`Cannot add operation to transaction in state: ${this._state}`);
|
|
59
|
+
}
|
|
60
|
+
this._operations.push(operation);
|
|
61
|
+
return this;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* @zh 执行事务
|
|
65
|
+
* @en Execute transaction
|
|
66
|
+
*/
|
|
67
|
+
async execute() {
|
|
68
|
+
if (this._state !== "pending") {
|
|
69
|
+
return {
|
|
70
|
+
success: false,
|
|
71
|
+
transactionId: this._id,
|
|
72
|
+
results: [],
|
|
73
|
+
error: `Transaction already in state: ${this._state}`,
|
|
74
|
+
duration: 0
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
this._startTime = Date.now();
|
|
78
|
+
this._state = "executing";
|
|
79
|
+
const results = [];
|
|
80
|
+
let executedCount = 0;
|
|
81
|
+
try {
|
|
82
|
+
await this._saveLog();
|
|
83
|
+
for (let i = 0; i < this._operations.length; i++) {
|
|
84
|
+
if (this._isTimedOut()) {
|
|
85
|
+
throw new Error("Transaction timed out");
|
|
86
|
+
}
|
|
87
|
+
const op = this._operations[i];
|
|
88
|
+
const isValid = await op.validate(this);
|
|
89
|
+
if (!isValid) {
|
|
90
|
+
throw new Error(`Validation failed for operation: ${op.name}`);
|
|
91
|
+
}
|
|
92
|
+
const result = await op.execute(this);
|
|
93
|
+
results.push(result);
|
|
94
|
+
executedCount++;
|
|
95
|
+
await this._updateOperationLog(i, "executed");
|
|
96
|
+
if (!result.success) {
|
|
97
|
+
throw new Error(result.error ?? `Operation ${op.name} failed`);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
this._state = "committed";
|
|
101
|
+
await this._updateTransactionState("committed");
|
|
102
|
+
return {
|
|
103
|
+
success: true,
|
|
104
|
+
transactionId: this._id,
|
|
105
|
+
results,
|
|
106
|
+
data: this._collectResultData(results),
|
|
107
|
+
duration: Date.now() - this._startTime
|
|
108
|
+
};
|
|
109
|
+
} catch (error) {
|
|
110
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
111
|
+
await this._compensate(executedCount - 1);
|
|
112
|
+
return {
|
|
113
|
+
success: false,
|
|
114
|
+
transactionId: this._id,
|
|
115
|
+
results,
|
|
116
|
+
error: errorMessage,
|
|
117
|
+
duration: Date.now() - this._startTime
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* @zh 手动回滚事务
|
|
123
|
+
* @en Manually rollback transaction
|
|
124
|
+
*/
|
|
125
|
+
async rollback() {
|
|
126
|
+
if (this._state === "committed" || this._state === "rolledback") {
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
await this._compensate(this._operations.length - 1);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* @zh 获取上下文数据
|
|
133
|
+
* @en Get context data
|
|
134
|
+
*/
|
|
135
|
+
get(key) {
|
|
136
|
+
return this._contextData.get(key);
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* @zh 设置上下文数据
|
|
140
|
+
* @en Set context data
|
|
141
|
+
*/
|
|
142
|
+
set(key, value) {
|
|
143
|
+
this._contextData.set(key, value);
|
|
144
|
+
}
|
|
145
|
+
// =========================================================================
|
|
146
|
+
// 私有方法 | Private methods
|
|
147
|
+
// =========================================================================
|
|
148
|
+
_isTimedOut() {
|
|
149
|
+
return Date.now() - this._startTime > this._timeout;
|
|
150
|
+
}
|
|
151
|
+
async _compensate(fromIndex) {
|
|
152
|
+
this._state = "rolledback";
|
|
153
|
+
for (let i = fromIndex; i >= 0; i--) {
|
|
154
|
+
const op = this._operations[i];
|
|
155
|
+
try {
|
|
156
|
+
await op.compensate(this);
|
|
157
|
+
await this._updateOperationLog(i, "compensated");
|
|
158
|
+
} catch (error) {
|
|
159
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
160
|
+
await this._updateOperationLog(i, "failed", errorMessage);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
await this._updateTransactionState("rolledback");
|
|
164
|
+
}
|
|
165
|
+
async _saveLog() {
|
|
166
|
+
if (!this._storage) return;
|
|
167
|
+
const log = {
|
|
168
|
+
id: this._id,
|
|
169
|
+
state: this._state,
|
|
170
|
+
createdAt: this._startTime,
|
|
171
|
+
updatedAt: this._startTime,
|
|
172
|
+
timeout: this._timeout,
|
|
173
|
+
operations: this._operations.map((op) => ({
|
|
174
|
+
name: op.name,
|
|
175
|
+
data: op.data,
|
|
176
|
+
state: "pending"
|
|
177
|
+
})),
|
|
178
|
+
metadata: this._metadata,
|
|
179
|
+
distributed: this._distributed
|
|
180
|
+
};
|
|
181
|
+
await this._storage.saveTransaction(log);
|
|
182
|
+
}
|
|
183
|
+
async _updateTransactionState(state) {
|
|
184
|
+
this._state = state;
|
|
185
|
+
if (this._storage) {
|
|
186
|
+
await this._storage.updateTransactionState(this._id, state);
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
async _updateOperationLog(index, state, error) {
|
|
190
|
+
if (this._storage) {
|
|
191
|
+
await this._storage.updateOperationState(this._id, index, state, error);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
_collectResultData(results) {
|
|
195
|
+
const data = {};
|
|
196
|
+
for (const result of results) {
|
|
197
|
+
if (result.data !== void 0) {
|
|
198
|
+
Object.assign(data, result.data);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
return Object.keys(data).length > 0 ? data : void 0;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
__name(_TransactionContext, "TransactionContext");
|
|
205
|
+
var TransactionContext = _TransactionContext;
|
|
206
|
+
function createTransactionContext(options = {}) {
|
|
207
|
+
return new TransactionContext(options);
|
|
208
|
+
}
|
|
209
|
+
__name(createTransactionContext, "createTransactionContext");
|
|
210
|
+
|
|
211
|
+
// src/core/TransactionManager.ts
|
|
212
|
+
var _TransactionManager = class _TransactionManager {
|
|
213
|
+
constructor(config = {}) {
|
|
214
|
+
__publicField(this, "_storage");
|
|
215
|
+
__publicField(this, "_defaultTimeout");
|
|
216
|
+
__publicField(this, "_serverId");
|
|
217
|
+
__publicField(this, "_autoRecover");
|
|
218
|
+
__publicField(this, "_activeTransactions", /* @__PURE__ */ new Map());
|
|
219
|
+
this._storage = config.storage ?? null;
|
|
220
|
+
this._defaultTimeout = config.defaultTimeout ?? 3e4;
|
|
221
|
+
this._serverId = config.serverId ?? this._generateServerId();
|
|
222
|
+
this._autoRecover = config.autoRecover ?? true;
|
|
223
|
+
}
|
|
224
|
+
// =========================================================================
|
|
225
|
+
// 只读属性 | Readonly properties
|
|
226
|
+
// =========================================================================
|
|
227
|
+
/**
|
|
228
|
+
* @zh 服务器 ID
|
|
229
|
+
* @en Server ID
|
|
230
|
+
*/
|
|
231
|
+
get serverId() {
|
|
232
|
+
return this._serverId;
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* @zh 存储实例
|
|
236
|
+
* @en Storage instance
|
|
237
|
+
*/
|
|
238
|
+
get storage() {
|
|
239
|
+
return this._storage;
|
|
240
|
+
}
|
|
241
|
+
/**
|
|
242
|
+
* @zh 活跃事务数量
|
|
243
|
+
* @en Active transaction count
|
|
244
|
+
*/
|
|
245
|
+
get activeCount() {
|
|
246
|
+
return this._activeTransactions.size;
|
|
247
|
+
}
|
|
248
|
+
// =========================================================================
|
|
249
|
+
// 公共方法 | Public methods
|
|
250
|
+
// =========================================================================
|
|
251
|
+
/**
|
|
252
|
+
* @zh 开始新事务
|
|
253
|
+
* @en Begin new transaction
|
|
254
|
+
*
|
|
255
|
+
* @param options - @zh 事务选项 @en Transaction options
|
|
256
|
+
* @returns @zh 事务上下文 @en Transaction context
|
|
257
|
+
*/
|
|
258
|
+
begin(options = {}) {
|
|
259
|
+
const ctx = new TransactionContext({
|
|
260
|
+
timeout: options.timeout ?? this._defaultTimeout,
|
|
261
|
+
storage: this._storage ?? void 0,
|
|
262
|
+
metadata: {
|
|
263
|
+
...options.metadata,
|
|
264
|
+
serverId: this._serverId
|
|
265
|
+
},
|
|
266
|
+
distributed: options.distributed
|
|
267
|
+
});
|
|
268
|
+
this._activeTransactions.set(ctx.id, ctx);
|
|
269
|
+
return ctx;
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* @zh 执行事务(便捷方法)
|
|
273
|
+
* @en Execute transaction (convenience method)
|
|
274
|
+
*
|
|
275
|
+
* @param builder - @zh 事务构建函数 @en Transaction builder function
|
|
276
|
+
* @param options - @zh 事务选项 @en Transaction options
|
|
277
|
+
* @returns @zh 事务结果 @en Transaction result
|
|
278
|
+
*/
|
|
279
|
+
async run(builder, options = {}) {
|
|
280
|
+
const ctx = this.begin(options);
|
|
281
|
+
try {
|
|
282
|
+
await builder(ctx);
|
|
283
|
+
const result = await ctx.execute();
|
|
284
|
+
return result;
|
|
285
|
+
} finally {
|
|
286
|
+
this._activeTransactions.delete(ctx.id);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
/**
|
|
290
|
+
* @zh 获取活跃事务
|
|
291
|
+
* @en Get active transaction
|
|
292
|
+
*/
|
|
293
|
+
getTransaction(id) {
|
|
294
|
+
return this._activeTransactions.get(id);
|
|
295
|
+
}
|
|
296
|
+
/**
|
|
297
|
+
* @zh 恢复未完成的事务
|
|
298
|
+
* @en Recover pending transactions
|
|
299
|
+
*/
|
|
300
|
+
async recover() {
|
|
301
|
+
if (!this._storage) return 0;
|
|
302
|
+
const pendingTransactions = await this._storage.getPendingTransactions(this._serverId);
|
|
303
|
+
let recoveredCount = 0;
|
|
304
|
+
for (const log of pendingTransactions) {
|
|
305
|
+
try {
|
|
306
|
+
await this._recoverTransaction(log);
|
|
307
|
+
recoveredCount++;
|
|
308
|
+
} catch (error) {
|
|
309
|
+
console.error(`Failed to recover transaction ${log.id}:`, error);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
return recoveredCount;
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* @zh 获取分布式锁
|
|
316
|
+
* @en Acquire distributed lock
|
|
317
|
+
*/
|
|
318
|
+
async acquireLock(key, ttl = 1e4) {
|
|
319
|
+
if (!this._storage) return null;
|
|
320
|
+
return this._storage.acquireLock(key, ttl);
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* @zh 释放分布式锁
|
|
324
|
+
* @en Release distributed lock
|
|
325
|
+
*/
|
|
326
|
+
async releaseLock(key, token) {
|
|
327
|
+
if (!this._storage) return false;
|
|
328
|
+
return this._storage.releaseLock(key, token);
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* @zh 使用分布式锁执行
|
|
332
|
+
* @en Execute with distributed lock
|
|
333
|
+
*/
|
|
334
|
+
async withLock(key, fn, ttl = 1e4) {
|
|
335
|
+
const token = await this.acquireLock(key, ttl);
|
|
336
|
+
if (!token) {
|
|
337
|
+
throw new Error(`Failed to acquire lock for key: ${key}`);
|
|
338
|
+
}
|
|
339
|
+
try {
|
|
340
|
+
return await fn();
|
|
341
|
+
} finally {
|
|
342
|
+
await this.releaseLock(key, token);
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
/**
|
|
346
|
+
* @zh 清理已完成的事务日志
|
|
347
|
+
* @en Clean up completed transaction logs
|
|
348
|
+
*/
|
|
349
|
+
async cleanup(beforeTimestamp) {
|
|
350
|
+
if (!this._storage) return 0;
|
|
351
|
+
const timestamp = beforeTimestamp ?? Date.now() - 24 * 60 * 60 * 1e3;
|
|
352
|
+
const pendingTransactions = await this._storage.getPendingTransactions();
|
|
353
|
+
let cleanedCount = 0;
|
|
354
|
+
for (const log of pendingTransactions) {
|
|
355
|
+
if (log.createdAt < timestamp && (log.state === "committed" || log.state === "rolledback")) {
|
|
356
|
+
await this._storage.deleteTransaction(log.id);
|
|
357
|
+
cleanedCount++;
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
return cleanedCount;
|
|
361
|
+
}
|
|
362
|
+
// =========================================================================
|
|
363
|
+
// 私有方法 | Private methods
|
|
364
|
+
// =========================================================================
|
|
365
|
+
_generateServerId() {
|
|
366
|
+
return `server_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 8)}`;
|
|
367
|
+
}
|
|
368
|
+
async _recoverTransaction(log) {
|
|
369
|
+
if (log.state === "executing") {
|
|
370
|
+
const executedOps = log.operations.filter((op) => op.state === "executed");
|
|
371
|
+
if (executedOps.length > 0 && this._storage) {
|
|
372
|
+
for (let i = executedOps.length - 1; i >= 0; i--) {
|
|
373
|
+
await this._storage.updateOperationState(log.id, i, "compensated");
|
|
374
|
+
}
|
|
375
|
+
await this._storage.updateTransactionState(log.id, "rolledback");
|
|
376
|
+
} else {
|
|
377
|
+
await this._storage?.updateTransactionState(log.id, "failed");
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
};
|
|
382
|
+
__name(_TransactionManager, "TransactionManager");
|
|
383
|
+
var TransactionManager = _TransactionManager;
|
|
384
|
+
function createTransactionManager(config = {}) {
|
|
385
|
+
return new TransactionManager(config);
|
|
386
|
+
}
|
|
387
|
+
__name(createTransactionManager, "createTransactionManager");
|
|
388
|
+
|
|
389
|
+
// src/storage/MemoryStorage.ts
|
|
390
|
+
var _MemoryStorage = class _MemoryStorage {
|
|
391
|
+
constructor(config = {}) {
|
|
392
|
+
__publicField(this, "_transactions", /* @__PURE__ */ new Map());
|
|
393
|
+
__publicField(this, "_data", /* @__PURE__ */ new Map());
|
|
394
|
+
__publicField(this, "_locks", /* @__PURE__ */ new Map());
|
|
395
|
+
__publicField(this, "_maxTransactions");
|
|
396
|
+
this._maxTransactions = config.maxTransactions ?? 1e3;
|
|
397
|
+
}
|
|
398
|
+
// =========================================================================
|
|
399
|
+
// 分布式锁 | Distributed Lock
|
|
400
|
+
// =========================================================================
|
|
401
|
+
async acquireLock(key, ttl) {
|
|
402
|
+
this._cleanExpiredLocks();
|
|
403
|
+
const existing = this._locks.get(key);
|
|
404
|
+
if (existing && existing.expireAt > Date.now()) {
|
|
405
|
+
return null;
|
|
406
|
+
}
|
|
407
|
+
const token = `lock_${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
408
|
+
this._locks.set(key, {
|
|
409
|
+
token,
|
|
410
|
+
expireAt: Date.now() + ttl
|
|
411
|
+
});
|
|
412
|
+
return token;
|
|
413
|
+
}
|
|
414
|
+
async releaseLock(key, token) {
|
|
415
|
+
const lock = this._locks.get(key);
|
|
416
|
+
if (!lock || lock.token !== token) {
|
|
417
|
+
return false;
|
|
418
|
+
}
|
|
419
|
+
this._locks.delete(key);
|
|
420
|
+
return true;
|
|
421
|
+
}
|
|
422
|
+
// =========================================================================
|
|
423
|
+
// 事务日志 | Transaction Log
|
|
424
|
+
// =========================================================================
|
|
425
|
+
async saveTransaction(tx) {
|
|
426
|
+
if (this._transactions.size >= this._maxTransactions) {
|
|
427
|
+
this._cleanOldTransactions();
|
|
428
|
+
}
|
|
429
|
+
this._transactions.set(tx.id, {
|
|
430
|
+
...tx
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
async getTransaction(id) {
|
|
434
|
+
const tx = this._transactions.get(id);
|
|
435
|
+
return tx ? {
|
|
436
|
+
...tx
|
|
437
|
+
} : null;
|
|
438
|
+
}
|
|
439
|
+
async updateTransactionState(id, state) {
|
|
440
|
+
const tx = this._transactions.get(id);
|
|
441
|
+
if (tx) {
|
|
442
|
+
tx.state = state;
|
|
443
|
+
tx.updatedAt = Date.now();
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
async updateOperationState(transactionId, operationIndex, state, error) {
|
|
447
|
+
const tx = this._transactions.get(transactionId);
|
|
448
|
+
if (tx && tx.operations[operationIndex]) {
|
|
449
|
+
tx.operations[operationIndex].state = state;
|
|
450
|
+
if (error) {
|
|
451
|
+
tx.operations[operationIndex].error = error;
|
|
452
|
+
}
|
|
453
|
+
if (state === "executed") {
|
|
454
|
+
tx.operations[operationIndex].executedAt = Date.now();
|
|
455
|
+
} else if (state === "compensated") {
|
|
456
|
+
tx.operations[operationIndex].compensatedAt = Date.now();
|
|
457
|
+
}
|
|
458
|
+
tx.updatedAt = Date.now();
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
async getPendingTransactions(serverId) {
|
|
462
|
+
const result = [];
|
|
463
|
+
for (const tx of this._transactions.values()) {
|
|
464
|
+
if (tx.state === "pending" || tx.state === "executing") {
|
|
465
|
+
if (!serverId || tx.metadata?.serverId === serverId) {
|
|
466
|
+
result.push({
|
|
467
|
+
...tx
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return result;
|
|
473
|
+
}
|
|
474
|
+
async deleteTransaction(id) {
|
|
475
|
+
this._transactions.delete(id);
|
|
476
|
+
}
|
|
477
|
+
// =========================================================================
|
|
478
|
+
// 数据操作 | Data Operations
|
|
479
|
+
// =========================================================================
|
|
480
|
+
async get(key) {
|
|
481
|
+
this._cleanExpiredData();
|
|
482
|
+
const entry = this._data.get(key);
|
|
483
|
+
if (!entry) return null;
|
|
484
|
+
if (entry.expireAt && entry.expireAt < Date.now()) {
|
|
485
|
+
this._data.delete(key);
|
|
486
|
+
return null;
|
|
487
|
+
}
|
|
488
|
+
return entry.value;
|
|
489
|
+
}
|
|
490
|
+
async set(key, value, ttl) {
|
|
491
|
+
this._data.set(key, {
|
|
492
|
+
value,
|
|
493
|
+
expireAt: ttl ? Date.now() + ttl : void 0
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
async delete(key) {
|
|
497
|
+
return this._data.delete(key);
|
|
498
|
+
}
|
|
499
|
+
// =========================================================================
|
|
500
|
+
// 辅助方法 | Helper methods
|
|
501
|
+
// =========================================================================
|
|
502
|
+
/**
|
|
503
|
+
* @zh 清空所有数据(测试用)
|
|
504
|
+
* @en Clear all data (for testing)
|
|
505
|
+
*/
|
|
506
|
+
clear() {
|
|
507
|
+
this._transactions.clear();
|
|
508
|
+
this._data.clear();
|
|
509
|
+
this._locks.clear();
|
|
510
|
+
}
|
|
511
|
+
/**
|
|
512
|
+
* @zh 获取事务数量
|
|
513
|
+
* @en Get transaction count
|
|
514
|
+
*/
|
|
515
|
+
get transactionCount() {
|
|
516
|
+
return this._transactions.size;
|
|
517
|
+
}
|
|
518
|
+
_cleanExpiredLocks() {
|
|
519
|
+
const now = Date.now();
|
|
520
|
+
for (const [key, lock] of this._locks) {
|
|
521
|
+
if (lock.expireAt < now) {
|
|
522
|
+
this._locks.delete(key);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
_cleanExpiredData() {
|
|
527
|
+
const now = Date.now();
|
|
528
|
+
for (const [key, entry] of this._data) {
|
|
529
|
+
if (entry.expireAt && entry.expireAt < now) {
|
|
530
|
+
this._data.delete(key);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
_cleanOldTransactions() {
|
|
535
|
+
const sorted = Array.from(this._transactions.entries()).sort((a, b) => a[1].createdAt - b[1].createdAt);
|
|
536
|
+
const toRemove = sorted.slice(0, Math.floor(this._maxTransactions * 0.2)).filter(([_, tx]) => tx.state === "committed" || tx.state === "rolledback");
|
|
537
|
+
for (const [id] of toRemove) {
|
|
538
|
+
this._transactions.delete(id);
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
__name(_MemoryStorage, "MemoryStorage");
|
|
543
|
+
var MemoryStorage = _MemoryStorage;
|
|
544
|
+
function createMemoryStorage(config = {}) {
|
|
545
|
+
return new MemoryStorage(config);
|
|
546
|
+
}
|
|
547
|
+
__name(createMemoryStorage, "createMemoryStorage");
|
|
548
|
+
|
|
549
|
+
// src/storage/RedisStorage.ts
|
|
550
|
+
var LOCK_SCRIPT = `
|
|
551
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
552
|
+
return redis.call("del", KEYS[1])
|
|
553
|
+
else
|
|
554
|
+
return 0
|
|
555
|
+
end
|
|
556
|
+
`;
|
|
557
|
+
var _RedisStorage = class _RedisStorage {
|
|
558
|
+
constructor(config) {
|
|
559
|
+
__publicField(this, "_client", null);
|
|
560
|
+
__publicField(this, "_factory");
|
|
561
|
+
__publicField(this, "_prefix");
|
|
562
|
+
__publicField(this, "_transactionTTL");
|
|
563
|
+
__publicField(this, "_closed", false);
|
|
564
|
+
this._factory = config.factory;
|
|
565
|
+
this._prefix = config.prefix ?? "tx:";
|
|
566
|
+
this._transactionTTL = config.transactionTTL ?? 86400;
|
|
567
|
+
}
|
|
568
|
+
// =========================================================================
|
|
569
|
+
// 生命周期 | Lifecycle
|
|
570
|
+
// =========================================================================
|
|
571
|
+
/**
|
|
572
|
+
* @zh 获取 Redis 客户端(惰性连接)
|
|
573
|
+
* @en Get Redis client (lazy connection)
|
|
574
|
+
*/
|
|
575
|
+
async _getClient() {
|
|
576
|
+
if (this._closed) {
|
|
577
|
+
throw new Error("RedisStorage is closed");
|
|
578
|
+
}
|
|
579
|
+
if (!this._client) {
|
|
580
|
+
this._client = await this._factory();
|
|
581
|
+
}
|
|
582
|
+
return this._client;
|
|
583
|
+
}
|
|
584
|
+
/**
|
|
585
|
+
* @zh 关闭存储连接
|
|
586
|
+
* @en Close storage connection
|
|
587
|
+
*/
|
|
588
|
+
async close() {
|
|
589
|
+
if (this._closed) return;
|
|
590
|
+
this._closed = true;
|
|
591
|
+
if (this._client) {
|
|
592
|
+
await this._client.quit();
|
|
593
|
+
this._client = null;
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* @zh 支持 await using 语法
|
|
598
|
+
* @en Support await using syntax
|
|
599
|
+
*/
|
|
600
|
+
async [Symbol.asyncDispose]() {
|
|
601
|
+
await this.close();
|
|
602
|
+
}
|
|
603
|
+
// =========================================================================
|
|
604
|
+
// 分布式锁 | Distributed Lock
|
|
605
|
+
// =========================================================================
|
|
606
|
+
async acquireLock(key, ttl) {
|
|
607
|
+
const client = await this._getClient();
|
|
608
|
+
const lockKey = `${this._prefix}lock:${key}`;
|
|
609
|
+
const token = `${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
610
|
+
const ttlSeconds = Math.ceil(ttl / 1e3);
|
|
611
|
+
const result = await client.set(lockKey, token, "NX", "EX", String(ttlSeconds));
|
|
612
|
+
return result === "OK" ? token : null;
|
|
613
|
+
}
|
|
614
|
+
async releaseLock(key, token) {
|
|
615
|
+
const client = await this._getClient();
|
|
616
|
+
const lockKey = `${this._prefix}lock:${key}`;
|
|
617
|
+
const result = await client.eval(LOCK_SCRIPT, 1, lockKey, token);
|
|
618
|
+
return result === 1;
|
|
619
|
+
}
|
|
620
|
+
// =========================================================================
|
|
621
|
+
// 事务日志 | Transaction Log
|
|
622
|
+
// =========================================================================
|
|
623
|
+
async saveTransaction(tx) {
|
|
624
|
+
const client = await this._getClient();
|
|
625
|
+
const key = `${this._prefix}tx:${tx.id}`;
|
|
626
|
+
await client.set(key, JSON.stringify(tx));
|
|
627
|
+
await client.expire(key, this._transactionTTL);
|
|
628
|
+
if (tx.metadata?.serverId) {
|
|
629
|
+
const serverKey = `${this._prefix}server:${tx.metadata.serverId}:txs`;
|
|
630
|
+
await client.hset(serverKey, tx.id, String(tx.createdAt));
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
async getTransaction(id) {
|
|
634
|
+
const client = await this._getClient();
|
|
635
|
+
const key = `${this._prefix}tx:${id}`;
|
|
636
|
+
const data = await client.get(key);
|
|
637
|
+
return data ? JSON.parse(data) : null;
|
|
638
|
+
}
|
|
639
|
+
async updateTransactionState(id, state) {
|
|
640
|
+
const tx = await this.getTransaction(id);
|
|
641
|
+
if (tx) {
|
|
642
|
+
tx.state = state;
|
|
643
|
+
tx.updatedAt = Date.now();
|
|
644
|
+
await this.saveTransaction(tx);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
async updateOperationState(transactionId, operationIndex, state, error) {
|
|
648
|
+
const tx = await this.getTransaction(transactionId);
|
|
649
|
+
if (tx && tx.operations[operationIndex]) {
|
|
650
|
+
tx.operations[operationIndex].state = state;
|
|
651
|
+
if (error) {
|
|
652
|
+
tx.operations[operationIndex].error = error;
|
|
653
|
+
}
|
|
654
|
+
if (state === "executed") {
|
|
655
|
+
tx.operations[operationIndex].executedAt = Date.now();
|
|
656
|
+
} else if (state === "compensated") {
|
|
657
|
+
tx.operations[operationIndex].compensatedAt = Date.now();
|
|
658
|
+
}
|
|
659
|
+
tx.updatedAt = Date.now();
|
|
660
|
+
await this.saveTransaction(tx);
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
async getPendingTransactions(serverId) {
|
|
664
|
+
const client = await this._getClient();
|
|
665
|
+
const result = [];
|
|
666
|
+
if (serverId) {
|
|
667
|
+
const serverKey = `${this._prefix}server:${serverId}:txs`;
|
|
668
|
+
const txIds = await client.hgetall(serverKey);
|
|
669
|
+
for (const id of Object.keys(txIds)) {
|
|
670
|
+
const tx = await this.getTransaction(id);
|
|
671
|
+
if (tx && (tx.state === "pending" || tx.state === "executing")) {
|
|
672
|
+
result.push(tx);
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
} else {
|
|
676
|
+
const pattern = `${this._prefix}tx:*`;
|
|
677
|
+
const keys = await client.keys(pattern);
|
|
678
|
+
for (const key of keys) {
|
|
679
|
+
const data = await client.get(key);
|
|
680
|
+
if (data) {
|
|
681
|
+
const tx = JSON.parse(data);
|
|
682
|
+
if (tx.state === "pending" || tx.state === "executing") {
|
|
683
|
+
result.push(tx);
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
return result;
|
|
689
|
+
}
|
|
690
|
+
async deleteTransaction(id) {
|
|
691
|
+
const client = await this._getClient();
|
|
692
|
+
const key = `${this._prefix}tx:${id}`;
|
|
693
|
+
const tx = await this.getTransaction(id);
|
|
694
|
+
await client.del(key);
|
|
695
|
+
if (tx?.metadata?.serverId) {
|
|
696
|
+
const serverKey = `${this._prefix}server:${tx.metadata.serverId}:txs`;
|
|
697
|
+
await client.hdel(serverKey, id);
|
|
698
|
+
}
|
|
699
|
+
}
|
|
700
|
+
// =========================================================================
|
|
701
|
+
// 数据操作 | Data Operations
|
|
702
|
+
// =========================================================================
|
|
703
|
+
async get(key) {
|
|
704
|
+
const client = await this._getClient();
|
|
705
|
+
const fullKey = `${this._prefix}data:${key}`;
|
|
706
|
+
const data = await client.get(fullKey);
|
|
707
|
+
return data ? JSON.parse(data) : null;
|
|
708
|
+
}
|
|
709
|
+
async set(key, value, ttl) {
|
|
710
|
+
const client = await this._getClient();
|
|
711
|
+
const fullKey = `${this._prefix}data:${key}`;
|
|
712
|
+
if (ttl) {
|
|
713
|
+
const ttlSeconds = Math.ceil(ttl / 1e3);
|
|
714
|
+
await client.set(fullKey, JSON.stringify(value), "EX", String(ttlSeconds));
|
|
715
|
+
} else {
|
|
716
|
+
await client.set(fullKey, JSON.stringify(value));
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
async delete(key) {
|
|
720
|
+
const client = await this._getClient();
|
|
721
|
+
const fullKey = `${this._prefix}data:${key}`;
|
|
722
|
+
const result = await client.del(fullKey);
|
|
723
|
+
return result > 0;
|
|
724
|
+
}
|
|
725
|
+
};
|
|
726
|
+
__name(_RedisStorage, "RedisStorage");
|
|
727
|
+
var RedisStorage = _RedisStorage;
|
|
728
|
+
function createRedisStorage(config) {
|
|
729
|
+
return new RedisStorage(config);
|
|
730
|
+
}
|
|
731
|
+
__name(createRedisStorage, "createRedisStorage");
|
|
732
|
+
|
|
733
|
+
// src/storage/MongoStorage.ts
|
|
734
|
+
var _MongoStorage = class _MongoStorage {
|
|
735
|
+
constructor(config) {
|
|
736
|
+
__publicField(this, "_connection");
|
|
737
|
+
__publicField(this, "_transactionCollection");
|
|
738
|
+
__publicField(this, "_dataCollection");
|
|
739
|
+
__publicField(this, "_lockCollection");
|
|
740
|
+
__publicField(this, "_closed", false);
|
|
741
|
+
this._connection = config.connection;
|
|
742
|
+
this._transactionCollection = config.transactionCollection ?? "transactions";
|
|
743
|
+
this._dataCollection = config.dataCollection ?? "transaction_data";
|
|
744
|
+
this._lockCollection = config.lockCollection ?? "transaction_locks";
|
|
745
|
+
}
|
|
746
|
+
// =========================================================================
|
|
747
|
+
// 生命周期 | Lifecycle
|
|
748
|
+
// =========================================================================
|
|
749
|
+
/**
|
|
750
|
+
* @zh 获取集合
|
|
751
|
+
* @en Get collection
|
|
752
|
+
*/
|
|
753
|
+
_getCollection(name) {
|
|
754
|
+
if (this._closed) {
|
|
755
|
+
throw new Error("MongoStorage is closed");
|
|
756
|
+
}
|
|
757
|
+
if (!this._connection.isConnected()) {
|
|
758
|
+
throw new Error("MongoDB connection is not connected");
|
|
759
|
+
}
|
|
760
|
+
return this._connection.collection(name);
|
|
761
|
+
}
|
|
762
|
+
/**
|
|
763
|
+
* @zh 关闭存储
|
|
764
|
+
* @en Close storage
|
|
765
|
+
*
|
|
766
|
+
* @zh 不会关闭共享连接,只标记存储为已关闭
|
|
767
|
+
* @en Does not close shared connection, only marks storage as closed
|
|
768
|
+
*/
|
|
769
|
+
async close() {
|
|
770
|
+
this._closed = true;
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* @zh 支持 await using 语法
|
|
774
|
+
* @en Support await using syntax
|
|
775
|
+
*/
|
|
776
|
+
async [Symbol.asyncDispose]() {
|
|
777
|
+
await this.close();
|
|
778
|
+
}
|
|
779
|
+
/**
|
|
780
|
+
* @zh 确保索引存在
|
|
781
|
+
* @en Ensure indexes exist
|
|
782
|
+
*/
|
|
783
|
+
async ensureIndexes() {
|
|
784
|
+
const txColl = this._getCollection(this._transactionCollection);
|
|
785
|
+
await txColl.createIndex({
|
|
786
|
+
state: 1
|
|
787
|
+
});
|
|
788
|
+
await txColl.createIndex({
|
|
789
|
+
"metadata.serverId": 1
|
|
790
|
+
});
|
|
791
|
+
await txColl.createIndex({
|
|
792
|
+
createdAt: 1
|
|
793
|
+
});
|
|
794
|
+
const lockColl = this._getCollection(this._lockCollection);
|
|
795
|
+
await lockColl.createIndex({
|
|
796
|
+
expireAt: 1
|
|
797
|
+
}, {
|
|
798
|
+
expireAfterSeconds: 0
|
|
799
|
+
});
|
|
800
|
+
const dataColl = this._getCollection(this._dataCollection);
|
|
801
|
+
await dataColl.createIndex({
|
|
802
|
+
expireAt: 1
|
|
803
|
+
}, {
|
|
804
|
+
expireAfterSeconds: 0
|
|
805
|
+
});
|
|
806
|
+
}
|
|
807
|
+
// =========================================================================
|
|
808
|
+
// 分布式锁 | Distributed Lock
|
|
809
|
+
// =========================================================================
|
|
810
|
+
async acquireLock(key, ttl) {
|
|
811
|
+
const coll = this._getCollection(this._lockCollection);
|
|
812
|
+
const token = `${Date.now()}_${Math.random().toString(36).substring(2)}`;
|
|
813
|
+
const expireAt = new Date(Date.now() + ttl);
|
|
814
|
+
try {
|
|
815
|
+
await coll.insertOne({
|
|
816
|
+
_id: key,
|
|
817
|
+
token,
|
|
818
|
+
expireAt
|
|
819
|
+
});
|
|
820
|
+
return token;
|
|
821
|
+
} catch {
|
|
822
|
+
const existing = await coll.findOne({
|
|
823
|
+
_id: key
|
|
824
|
+
});
|
|
825
|
+
if (existing && existing.expireAt < /* @__PURE__ */ new Date()) {
|
|
826
|
+
const result = await coll.updateOne({
|
|
827
|
+
_id: key,
|
|
828
|
+
expireAt: {
|
|
829
|
+
$lt: /* @__PURE__ */ new Date()
|
|
830
|
+
}
|
|
831
|
+
}, {
|
|
832
|
+
$set: {
|
|
833
|
+
token,
|
|
834
|
+
expireAt
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
if (result.modifiedCount > 0) {
|
|
838
|
+
return token;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
return null;
|
|
842
|
+
}
|
|
843
|
+
}
|
|
844
|
+
async releaseLock(key, token) {
|
|
845
|
+
const coll = this._getCollection(this._lockCollection);
|
|
846
|
+
const result = await coll.deleteOne({
|
|
847
|
+
_id: key,
|
|
848
|
+
token
|
|
849
|
+
});
|
|
850
|
+
return result.deletedCount > 0;
|
|
851
|
+
}
|
|
852
|
+
// =========================================================================
|
|
853
|
+
// 事务日志 | Transaction Log
|
|
854
|
+
// =========================================================================
|
|
855
|
+
async saveTransaction(tx) {
|
|
856
|
+
const coll = this._getCollection(this._transactionCollection);
|
|
857
|
+
const existing = await coll.findOne({
|
|
858
|
+
_id: tx.id
|
|
859
|
+
});
|
|
860
|
+
if (existing) {
|
|
861
|
+
await coll.updateOne({
|
|
862
|
+
_id: tx.id
|
|
863
|
+
}, {
|
|
864
|
+
$set: {
|
|
865
|
+
...tx,
|
|
866
|
+
_id: tx.id
|
|
867
|
+
}
|
|
868
|
+
});
|
|
869
|
+
} else {
|
|
870
|
+
await coll.insertOne({
|
|
871
|
+
...tx,
|
|
872
|
+
_id: tx.id
|
|
873
|
+
});
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
async getTransaction(id) {
|
|
877
|
+
const coll = this._getCollection(this._transactionCollection);
|
|
878
|
+
const doc = await coll.findOne({
|
|
879
|
+
_id: id
|
|
880
|
+
});
|
|
881
|
+
if (!doc) return null;
|
|
882
|
+
const { _id, ...tx } = doc;
|
|
883
|
+
return tx;
|
|
884
|
+
}
|
|
885
|
+
async updateTransactionState(id, state) {
|
|
886
|
+
const coll = this._getCollection(this._transactionCollection);
|
|
887
|
+
await coll.updateOne({
|
|
888
|
+
_id: id
|
|
889
|
+
}, {
|
|
890
|
+
$set: {
|
|
891
|
+
state,
|
|
892
|
+
updatedAt: Date.now()
|
|
893
|
+
}
|
|
894
|
+
});
|
|
895
|
+
}
|
|
896
|
+
async updateOperationState(transactionId, operationIndex, state, error) {
|
|
897
|
+
const coll = this._getCollection(this._transactionCollection);
|
|
898
|
+
const update = {
|
|
899
|
+
[`operations.${operationIndex}.state`]: state,
|
|
900
|
+
updatedAt: Date.now()
|
|
901
|
+
};
|
|
902
|
+
if (error) {
|
|
903
|
+
update[`operations.${operationIndex}.error`] = error;
|
|
904
|
+
}
|
|
905
|
+
if (state === "executed") {
|
|
906
|
+
update[`operations.${operationIndex}.executedAt`] = Date.now();
|
|
907
|
+
} else if (state === "compensated") {
|
|
908
|
+
update[`operations.${operationIndex}.compensatedAt`] = Date.now();
|
|
909
|
+
}
|
|
910
|
+
await coll.updateOne({
|
|
911
|
+
_id: transactionId
|
|
912
|
+
}, {
|
|
913
|
+
$set: update
|
|
914
|
+
});
|
|
915
|
+
}
|
|
916
|
+
async getPendingTransactions(serverId) {
|
|
917
|
+
const coll = this._getCollection(this._transactionCollection);
|
|
918
|
+
const filter = {
|
|
919
|
+
state: {
|
|
920
|
+
$in: [
|
|
921
|
+
"pending",
|
|
922
|
+
"executing"
|
|
923
|
+
]
|
|
924
|
+
}
|
|
925
|
+
};
|
|
926
|
+
if (serverId) {
|
|
927
|
+
filter["metadata.serverId"] = serverId;
|
|
928
|
+
}
|
|
929
|
+
const docs = await coll.find(filter);
|
|
930
|
+
return docs.map(({ _id, ...tx }) => tx);
|
|
931
|
+
}
|
|
932
|
+
async deleteTransaction(id) {
|
|
933
|
+
const coll = this._getCollection(this._transactionCollection);
|
|
934
|
+
await coll.deleteOne({
|
|
935
|
+
_id: id
|
|
936
|
+
});
|
|
937
|
+
}
|
|
938
|
+
// =========================================================================
|
|
939
|
+
// 数据操作 | Data Operations
|
|
940
|
+
// =========================================================================
|
|
941
|
+
async get(key) {
|
|
942
|
+
const coll = this._getCollection(this._dataCollection);
|
|
943
|
+
const doc = await coll.findOne({
|
|
944
|
+
_id: key
|
|
945
|
+
});
|
|
946
|
+
if (!doc) return null;
|
|
947
|
+
if (doc.expireAt && doc.expireAt < /* @__PURE__ */ new Date()) {
|
|
948
|
+
await coll.deleteOne({
|
|
949
|
+
_id: key
|
|
950
|
+
});
|
|
951
|
+
return null;
|
|
952
|
+
}
|
|
953
|
+
return doc.value;
|
|
954
|
+
}
|
|
955
|
+
async set(key, value, ttl) {
|
|
956
|
+
const coll = this._getCollection(this._dataCollection);
|
|
957
|
+
const doc = {
|
|
958
|
+
_id: key,
|
|
959
|
+
value
|
|
960
|
+
};
|
|
961
|
+
if (ttl) {
|
|
962
|
+
doc.expireAt = new Date(Date.now() + ttl);
|
|
963
|
+
}
|
|
964
|
+
const existing = await coll.findOne({
|
|
965
|
+
_id: key
|
|
966
|
+
});
|
|
967
|
+
if (existing) {
|
|
968
|
+
await coll.updateOne({
|
|
969
|
+
_id: key
|
|
970
|
+
}, {
|
|
971
|
+
$set: doc
|
|
972
|
+
});
|
|
973
|
+
} else {
|
|
974
|
+
await coll.insertOne(doc);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
async delete(key) {
|
|
978
|
+
const coll = this._getCollection(this._dataCollection);
|
|
979
|
+
const result = await coll.deleteOne({
|
|
980
|
+
_id: key
|
|
981
|
+
});
|
|
982
|
+
return result.deletedCount > 0;
|
|
983
|
+
}
|
|
984
|
+
};
|
|
985
|
+
__name(_MongoStorage, "MongoStorage");
|
|
986
|
+
var MongoStorage = _MongoStorage;
|
|
987
|
+
function createMongoStorage(connection, options) {
|
|
988
|
+
return new MongoStorage({
|
|
989
|
+
connection,
|
|
990
|
+
...options
|
|
991
|
+
});
|
|
992
|
+
}
|
|
993
|
+
__name(createMongoStorage, "createMongoStorage");
|
|
994
|
+
|
|
995
|
+
// src/operations/BaseOperation.ts
|
|
996
|
+
var _BaseOperation = class _BaseOperation {
|
|
997
|
+
constructor(data) {
|
|
998
|
+
__publicField(this, "data");
|
|
999
|
+
this.data = data;
|
|
1000
|
+
}
|
|
1001
|
+
/**
|
|
1002
|
+
* @zh 验证前置条件(默认通过)
|
|
1003
|
+
* @en Validate preconditions (passes by default)
|
|
1004
|
+
*/
|
|
1005
|
+
async validate(_ctx) {
|
|
1006
|
+
return true;
|
|
1007
|
+
}
|
|
1008
|
+
/**
|
|
1009
|
+
* @zh 创建成功结果
|
|
1010
|
+
* @en Create success result
|
|
1011
|
+
*/
|
|
1012
|
+
success(data) {
|
|
1013
|
+
return {
|
|
1014
|
+
success: true,
|
|
1015
|
+
data
|
|
1016
|
+
};
|
|
1017
|
+
}
|
|
1018
|
+
/**
|
|
1019
|
+
* @zh 创建失败结果
|
|
1020
|
+
* @en Create failure result
|
|
1021
|
+
*/
|
|
1022
|
+
failure(error, errorCode) {
|
|
1023
|
+
return {
|
|
1024
|
+
success: false,
|
|
1025
|
+
error,
|
|
1026
|
+
errorCode
|
|
1027
|
+
};
|
|
1028
|
+
}
|
|
1029
|
+
};
|
|
1030
|
+
__name(_BaseOperation, "BaseOperation");
|
|
1031
|
+
var BaseOperation = _BaseOperation;
|
|
1032
|
+
|
|
1033
|
+
// src/operations/CurrencyOperation.ts
|
|
1034
|
+
var _CurrencyOperation = class _CurrencyOperation extends BaseOperation {
|
|
1035
|
+
constructor() {
|
|
1036
|
+
super(...arguments);
|
|
1037
|
+
__publicField(this, "name", "currency");
|
|
1038
|
+
__publicField(this, "_provider", null);
|
|
1039
|
+
__publicField(this, "_beforeBalance", 0);
|
|
1040
|
+
}
|
|
1041
|
+
/**
|
|
1042
|
+
* @zh 设置货币数据提供者
|
|
1043
|
+
* @en Set currency data provider
|
|
1044
|
+
*/
|
|
1045
|
+
setProvider(provider) {
|
|
1046
|
+
this._provider = provider;
|
|
1047
|
+
return this;
|
|
1048
|
+
}
|
|
1049
|
+
async validate(ctx) {
|
|
1050
|
+
if (this.data.amount <= 0) {
|
|
1051
|
+
return false;
|
|
1052
|
+
}
|
|
1053
|
+
if (this.data.type === "deduct") {
|
|
1054
|
+
const balance = await this._getBalance(ctx);
|
|
1055
|
+
return balance >= this.data.amount;
|
|
1056
|
+
}
|
|
1057
|
+
return true;
|
|
1058
|
+
}
|
|
1059
|
+
async execute(ctx) {
|
|
1060
|
+
const { type, playerId, currency, amount } = this.data;
|
|
1061
|
+
this._beforeBalance = await this._getBalance(ctx);
|
|
1062
|
+
let afterBalance;
|
|
1063
|
+
if (type === "add") {
|
|
1064
|
+
afterBalance = this._beforeBalance + amount;
|
|
1065
|
+
} else {
|
|
1066
|
+
if (this._beforeBalance < amount) {
|
|
1067
|
+
return this.failure("Insufficient balance", "INSUFFICIENT_BALANCE");
|
|
1068
|
+
}
|
|
1069
|
+
afterBalance = this._beforeBalance - amount;
|
|
1070
|
+
}
|
|
1071
|
+
await this._setBalance(ctx, afterBalance);
|
|
1072
|
+
ctx.set(`currency:${playerId}:${currency}:before`, this._beforeBalance);
|
|
1073
|
+
ctx.set(`currency:${playerId}:${currency}:after`, afterBalance);
|
|
1074
|
+
return this.success({
|
|
1075
|
+
beforeBalance: this._beforeBalance,
|
|
1076
|
+
afterBalance
|
|
1077
|
+
});
|
|
1078
|
+
}
|
|
1079
|
+
async compensate(ctx) {
|
|
1080
|
+
await this._setBalance(ctx, this._beforeBalance);
|
|
1081
|
+
}
|
|
1082
|
+
async _getBalance(ctx) {
|
|
1083
|
+
const { playerId, currency } = this.data;
|
|
1084
|
+
if (this._provider) {
|
|
1085
|
+
return this._provider.getBalance(playerId, currency);
|
|
1086
|
+
}
|
|
1087
|
+
if (ctx.storage) {
|
|
1088
|
+
const balance = await ctx.storage.get(`player:${playerId}:currency:${currency}`);
|
|
1089
|
+
return balance ?? 0;
|
|
1090
|
+
}
|
|
1091
|
+
return 0;
|
|
1092
|
+
}
|
|
1093
|
+
async _setBalance(ctx, amount) {
|
|
1094
|
+
const { playerId, currency } = this.data;
|
|
1095
|
+
if (this._provider) {
|
|
1096
|
+
await this._provider.setBalance(playerId, currency, amount);
|
|
1097
|
+
return;
|
|
1098
|
+
}
|
|
1099
|
+
if (ctx.storage) {
|
|
1100
|
+
await ctx.storage.set(`player:${playerId}:currency:${currency}`, amount);
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
__name(_CurrencyOperation, "CurrencyOperation");
|
|
1105
|
+
var CurrencyOperation = _CurrencyOperation;
|
|
1106
|
+
function createCurrencyOperation(data) {
|
|
1107
|
+
return new CurrencyOperation(data);
|
|
1108
|
+
}
|
|
1109
|
+
__name(createCurrencyOperation, "createCurrencyOperation");
|
|
1110
|
+
|
|
1111
|
+
// src/operations/InventoryOperation.ts
|
|
1112
|
+
var _InventoryOperation = class _InventoryOperation extends BaseOperation {
|
|
1113
|
+
constructor() {
|
|
1114
|
+
super(...arguments);
|
|
1115
|
+
__publicField(this, "name", "inventory");
|
|
1116
|
+
__publicField(this, "_provider", null);
|
|
1117
|
+
__publicField(this, "_beforeItem", null);
|
|
1118
|
+
}
|
|
1119
|
+
/**
|
|
1120
|
+
* @zh 设置背包数据提供者
|
|
1121
|
+
* @en Set inventory data provider
|
|
1122
|
+
*/
|
|
1123
|
+
setProvider(provider) {
|
|
1124
|
+
this._provider = provider;
|
|
1125
|
+
return this;
|
|
1126
|
+
}
|
|
1127
|
+
async validate(ctx) {
|
|
1128
|
+
const { type, quantity } = this.data;
|
|
1129
|
+
if (quantity <= 0) {
|
|
1130
|
+
return false;
|
|
1131
|
+
}
|
|
1132
|
+
if (type === "remove") {
|
|
1133
|
+
const item = await this._getItem(ctx);
|
|
1134
|
+
return item !== null && item.quantity >= quantity;
|
|
1135
|
+
}
|
|
1136
|
+
if (type === "add" && this._provider?.hasCapacity) {
|
|
1137
|
+
return this._provider.hasCapacity(this.data.playerId, 1);
|
|
1138
|
+
}
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
async execute(ctx) {
|
|
1142
|
+
const { type, playerId, itemId, quantity, properties } = this.data;
|
|
1143
|
+
this._beforeItem = await this._getItem(ctx);
|
|
1144
|
+
let afterItem = null;
|
|
1145
|
+
switch (type) {
|
|
1146
|
+
case "add": {
|
|
1147
|
+
if (this._beforeItem) {
|
|
1148
|
+
afterItem = {
|
|
1149
|
+
...this._beforeItem,
|
|
1150
|
+
quantity: this._beforeItem.quantity + quantity
|
|
1151
|
+
};
|
|
1152
|
+
} else {
|
|
1153
|
+
afterItem = {
|
|
1154
|
+
itemId,
|
|
1155
|
+
quantity,
|
|
1156
|
+
properties
|
|
1157
|
+
};
|
|
1158
|
+
}
|
|
1159
|
+
break;
|
|
1160
|
+
}
|
|
1161
|
+
case "remove": {
|
|
1162
|
+
if (!this._beforeItem || this._beforeItem.quantity < quantity) {
|
|
1163
|
+
return this.failure("Insufficient item quantity", "INSUFFICIENT_ITEM");
|
|
1164
|
+
}
|
|
1165
|
+
const newQuantity = this._beforeItem.quantity - quantity;
|
|
1166
|
+
if (newQuantity > 0) {
|
|
1167
|
+
afterItem = {
|
|
1168
|
+
...this._beforeItem,
|
|
1169
|
+
quantity: newQuantity
|
|
1170
|
+
};
|
|
1171
|
+
} else {
|
|
1172
|
+
afterItem = null;
|
|
1173
|
+
}
|
|
1174
|
+
break;
|
|
1175
|
+
}
|
|
1176
|
+
case "update": {
|
|
1177
|
+
if (!this._beforeItem) {
|
|
1178
|
+
return this.failure("Item not found", "ITEM_NOT_FOUND");
|
|
1179
|
+
}
|
|
1180
|
+
afterItem = {
|
|
1181
|
+
...this._beforeItem,
|
|
1182
|
+
quantity: quantity > 0 ? quantity : this._beforeItem.quantity,
|
|
1183
|
+
properties: properties ?? this._beforeItem.properties
|
|
1184
|
+
};
|
|
1185
|
+
break;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
await this._setItem(ctx, afterItem);
|
|
1189
|
+
ctx.set(`inventory:${playerId}:${itemId}:before`, this._beforeItem);
|
|
1190
|
+
ctx.set(`inventory:${playerId}:${itemId}:after`, afterItem);
|
|
1191
|
+
return this.success({
|
|
1192
|
+
beforeItem: this._beforeItem ?? void 0,
|
|
1193
|
+
afterItem: afterItem ?? void 0
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
async compensate(ctx) {
|
|
1197
|
+
await this._setItem(ctx, this._beforeItem);
|
|
1198
|
+
}
|
|
1199
|
+
async _getItem(ctx) {
|
|
1200
|
+
const { playerId, itemId } = this.data;
|
|
1201
|
+
if (this._provider) {
|
|
1202
|
+
return this._provider.getItem(playerId, itemId);
|
|
1203
|
+
}
|
|
1204
|
+
if (ctx.storage) {
|
|
1205
|
+
return ctx.storage.get(`player:${playerId}:inventory:${itemId}`);
|
|
1206
|
+
}
|
|
1207
|
+
return null;
|
|
1208
|
+
}
|
|
1209
|
+
async _setItem(ctx, item) {
|
|
1210
|
+
const { playerId, itemId } = this.data;
|
|
1211
|
+
if (this._provider) {
|
|
1212
|
+
await this._provider.setItem(playerId, itemId, item);
|
|
1213
|
+
return;
|
|
1214
|
+
}
|
|
1215
|
+
if (ctx.storage) {
|
|
1216
|
+
if (item) {
|
|
1217
|
+
await ctx.storage.set(`player:${playerId}:inventory:${itemId}`, item);
|
|
1218
|
+
} else {
|
|
1219
|
+
await ctx.storage.delete(`player:${playerId}:inventory:${itemId}`);
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
};
|
|
1224
|
+
__name(_InventoryOperation, "InventoryOperation");
|
|
1225
|
+
var InventoryOperation = _InventoryOperation;
|
|
1226
|
+
function createInventoryOperation(data) {
|
|
1227
|
+
return new InventoryOperation(data);
|
|
1228
|
+
}
|
|
1229
|
+
__name(createInventoryOperation, "createInventoryOperation");
|
|
1230
|
+
|
|
1231
|
+
// src/operations/TradeOperation.ts
|
|
1232
|
+
var _TradeOperation = class _TradeOperation extends BaseOperation {
|
|
1233
|
+
constructor() {
|
|
1234
|
+
super(...arguments);
|
|
1235
|
+
__publicField(this, "name", "trade");
|
|
1236
|
+
__publicField(this, "_provider", null);
|
|
1237
|
+
__publicField(this, "_subOperations", []);
|
|
1238
|
+
__publicField(this, "_executedCount", 0);
|
|
1239
|
+
}
|
|
1240
|
+
/**
|
|
1241
|
+
* @zh 设置交易数据提供者
|
|
1242
|
+
* @en Set trade data provider
|
|
1243
|
+
*/
|
|
1244
|
+
setProvider(provider) {
|
|
1245
|
+
this._provider = provider;
|
|
1246
|
+
return this;
|
|
1247
|
+
}
|
|
1248
|
+
async validate(ctx) {
|
|
1249
|
+
this._buildSubOperations();
|
|
1250
|
+
for (const op of this._subOperations) {
|
|
1251
|
+
const isValid = await op.validate(ctx);
|
|
1252
|
+
if (!isValid) {
|
|
1253
|
+
return false;
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
return true;
|
|
1257
|
+
}
|
|
1258
|
+
async execute(ctx) {
|
|
1259
|
+
this._buildSubOperations();
|
|
1260
|
+
this._executedCount = 0;
|
|
1261
|
+
try {
|
|
1262
|
+
for (const op of this._subOperations) {
|
|
1263
|
+
const result = await op.execute(ctx);
|
|
1264
|
+
if (!result.success) {
|
|
1265
|
+
await this._compensateExecuted(ctx);
|
|
1266
|
+
return this.failure(result.error ?? "Trade operation failed", "TRADE_FAILED");
|
|
1267
|
+
}
|
|
1268
|
+
this._executedCount++;
|
|
1269
|
+
}
|
|
1270
|
+
return this.success({
|
|
1271
|
+
tradeId: this.data.tradeId,
|
|
1272
|
+
completed: true
|
|
1273
|
+
});
|
|
1274
|
+
} catch (error) {
|
|
1275
|
+
await this._compensateExecuted(ctx);
|
|
1276
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1277
|
+
return this.failure(errorMessage, "TRADE_ERROR");
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async compensate(ctx) {
|
|
1281
|
+
await this._compensateExecuted(ctx);
|
|
1282
|
+
}
|
|
1283
|
+
_buildSubOperations() {
|
|
1284
|
+
if (this._subOperations.length > 0) return;
|
|
1285
|
+
const { partyA, partyB } = this.data;
|
|
1286
|
+
if (partyA.items) {
|
|
1287
|
+
for (const item of partyA.items) {
|
|
1288
|
+
const removeOp = new InventoryOperation({
|
|
1289
|
+
type: "remove",
|
|
1290
|
+
playerId: partyA.playerId,
|
|
1291
|
+
itemId: item.itemId,
|
|
1292
|
+
quantity: item.quantity,
|
|
1293
|
+
reason: `trade:${this.data.tradeId}:give`
|
|
1294
|
+
});
|
|
1295
|
+
const addOp = new InventoryOperation({
|
|
1296
|
+
type: "add",
|
|
1297
|
+
playerId: partyB.playerId,
|
|
1298
|
+
itemId: item.itemId,
|
|
1299
|
+
quantity: item.quantity,
|
|
1300
|
+
reason: `trade:${this.data.tradeId}:receive`
|
|
1301
|
+
});
|
|
1302
|
+
if (this._provider?.inventoryProvider) {
|
|
1303
|
+
removeOp.setProvider(this._provider.inventoryProvider);
|
|
1304
|
+
addOp.setProvider(this._provider.inventoryProvider);
|
|
1305
|
+
}
|
|
1306
|
+
this._subOperations.push(removeOp, addOp);
|
|
1307
|
+
}
|
|
1308
|
+
}
|
|
1309
|
+
if (partyA.currencies) {
|
|
1310
|
+
for (const curr of partyA.currencies) {
|
|
1311
|
+
const deductOp = new CurrencyOperation({
|
|
1312
|
+
type: "deduct",
|
|
1313
|
+
playerId: partyA.playerId,
|
|
1314
|
+
currency: curr.currency,
|
|
1315
|
+
amount: curr.amount,
|
|
1316
|
+
reason: `trade:${this.data.tradeId}:give`
|
|
1317
|
+
});
|
|
1318
|
+
const addOp = new CurrencyOperation({
|
|
1319
|
+
type: "add",
|
|
1320
|
+
playerId: partyB.playerId,
|
|
1321
|
+
currency: curr.currency,
|
|
1322
|
+
amount: curr.amount,
|
|
1323
|
+
reason: `trade:${this.data.tradeId}:receive`
|
|
1324
|
+
});
|
|
1325
|
+
if (this._provider?.currencyProvider) {
|
|
1326
|
+
deductOp.setProvider(this._provider.currencyProvider);
|
|
1327
|
+
addOp.setProvider(this._provider.currencyProvider);
|
|
1328
|
+
}
|
|
1329
|
+
this._subOperations.push(deductOp, addOp);
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
if (partyB.items) {
|
|
1333
|
+
for (const item of partyB.items) {
|
|
1334
|
+
const removeOp = new InventoryOperation({
|
|
1335
|
+
type: "remove",
|
|
1336
|
+
playerId: partyB.playerId,
|
|
1337
|
+
itemId: item.itemId,
|
|
1338
|
+
quantity: item.quantity,
|
|
1339
|
+
reason: `trade:${this.data.tradeId}:give`
|
|
1340
|
+
});
|
|
1341
|
+
const addOp = new InventoryOperation({
|
|
1342
|
+
type: "add",
|
|
1343
|
+
playerId: partyA.playerId,
|
|
1344
|
+
itemId: item.itemId,
|
|
1345
|
+
quantity: item.quantity,
|
|
1346
|
+
reason: `trade:${this.data.tradeId}:receive`
|
|
1347
|
+
});
|
|
1348
|
+
if (this._provider?.inventoryProvider) {
|
|
1349
|
+
removeOp.setProvider(this._provider.inventoryProvider);
|
|
1350
|
+
addOp.setProvider(this._provider.inventoryProvider);
|
|
1351
|
+
}
|
|
1352
|
+
this._subOperations.push(removeOp, addOp);
|
|
1353
|
+
}
|
|
1354
|
+
}
|
|
1355
|
+
if (partyB.currencies) {
|
|
1356
|
+
for (const curr of partyB.currencies) {
|
|
1357
|
+
const deductOp = new CurrencyOperation({
|
|
1358
|
+
type: "deduct",
|
|
1359
|
+
playerId: partyB.playerId,
|
|
1360
|
+
currency: curr.currency,
|
|
1361
|
+
amount: curr.amount,
|
|
1362
|
+
reason: `trade:${this.data.tradeId}:give`
|
|
1363
|
+
});
|
|
1364
|
+
const addOp = new CurrencyOperation({
|
|
1365
|
+
type: "add",
|
|
1366
|
+
playerId: partyA.playerId,
|
|
1367
|
+
currency: curr.currency,
|
|
1368
|
+
amount: curr.amount,
|
|
1369
|
+
reason: `trade:${this.data.tradeId}:receive`
|
|
1370
|
+
});
|
|
1371
|
+
if (this._provider?.currencyProvider) {
|
|
1372
|
+
deductOp.setProvider(this._provider.currencyProvider);
|
|
1373
|
+
addOp.setProvider(this._provider.currencyProvider);
|
|
1374
|
+
}
|
|
1375
|
+
this._subOperations.push(deductOp, addOp);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
}
|
|
1379
|
+
async _compensateExecuted(ctx) {
|
|
1380
|
+
for (let i = this._executedCount - 1; i >= 0; i--) {
|
|
1381
|
+
await this._subOperations[i].compensate(ctx);
|
|
1382
|
+
}
|
|
1383
|
+
}
|
|
1384
|
+
};
|
|
1385
|
+
__name(_TradeOperation, "TradeOperation");
|
|
1386
|
+
var TradeOperation = _TradeOperation;
|
|
1387
|
+
function createTradeOperation(data) {
|
|
1388
|
+
return new TradeOperation(data);
|
|
1389
|
+
}
|
|
1390
|
+
__name(createTradeOperation, "createTradeOperation");
|
|
1391
|
+
|
|
1392
|
+
// src/distributed/SagaOrchestrator.ts
|
|
1393
|
+
function generateSagaId() {
|
|
1394
|
+
return `saga_${Date.now().toString(36)}_${Math.random().toString(36).substring(2, 11)}`;
|
|
1395
|
+
}
|
|
1396
|
+
__name(generateSagaId, "generateSagaId");
|
|
1397
|
+
var _SagaOrchestrator = class _SagaOrchestrator {
|
|
1398
|
+
constructor(config = {}) {
|
|
1399
|
+
__publicField(this, "_storage");
|
|
1400
|
+
__publicField(this, "_timeout");
|
|
1401
|
+
__publicField(this, "_serverId");
|
|
1402
|
+
this._storage = config.storage ?? null;
|
|
1403
|
+
this._timeout = config.timeout ?? 3e4;
|
|
1404
|
+
this._serverId = config.serverId ?? "default";
|
|
1405
|
+
}
|
|
1406
|
+
/**
|
|
1407
|
+
* @zh 执行 Saga
|
|
1408
|
+
* @en Execute Saga
|
|
1409
|
+
*/
|
|
1410
|
+
async execute(steps) {
|
|
1411
|
+
const sagaId = generateSagaId();
|
|
1412
|
+
const startTime = Date.now();
|
|
1413
|
+
const completedSteps = [];
|
|
1414
|
+
const sagaLog = {
|
|
1415
|
+
id: sagaId,
|
|
1416
|
+
state: "pending",
|
|
1417
|
+
steps: steps.map((s) => ({
|
|
1418
|
+
name: s.name,
|
|
1419
|
+
serverId: s.serverId,
|
|
1420
|
+
state: "pending"
|
|
1421
|
+
})),
|
|
1422
|
+
createdAt: startTime,
|
|
1423
|
+
updatedAt: startTime,
|
|
1424
|
+
metadata: {
|
|
1425
|
+
orchestratorServerId: this._serverId
|
|
1426
|
+
}
|
|
1427
|
+
};
|
|
1428
|
+
await this._saveSagaLog(sagaLog);
|
|
1429
|
+
try {
|
|
1430
|
+
sagaLog.state = "running";
|
|
1431
|
+
await this._saveSagaLog(sagaLog);
|
|
1432
|
+
for (let i = 0; i < steps.length; i++) {
|
|
1433
|
+
const step = steps[i];
|
|
1434
|
+
if (Date.now() - startTime > this._timeout) {
|
|
1435
|
+
throw new Error("Saga execution timed out");
|
|
1436
|
+
}
|
|
1437
|
+
sagaLog.steps[i].state = "executing";
|
|
1438
|
+
sagaLog.steps[i].startedAt = Date.now();
|
|
1439
|
+
await this._saveSagaLog(sagaLog);
|
|
1440
|
+
const result = await step.execute(step.data);
|
|
1441
|
+
if (!result.success) {
|
|
1442
|
+
sagaLog.steps[i].state = "failed";
|
|
1443
|
+
sagaLog.steps[i].error = result.error;
|
|
1444
|
+
await this._saveSagaLog(sagaLog);
|
|
1445
|
+
throw new Error(result.error ?? `Step ${step.name} failed`);
|
|
1446
|
+
}
|
|
1447
|
+
sagaLog.steps[i].state = "completed";
|
|
1448
|
+
sagaLog.steps[i].completedAt = Date.now();
|
|
1449
|
+
completedSteps.push(step.name);
|
|
1450
|
+
await this._saveSagaLog(sagaLog);
|
|
1451
|
+
}
|
|
1452
|
+
sagaLog.state = "completed";
|
|
1453
|
+
sagaLog.updatedAt = Date.now();
|
|
1454
|
+
await this._saveSagaLog(sagaLog);
|
|
1455
|
+
return {
|
|
1456
|
+
success: true,
|
|
1457
|
+
sagaId,
|
|
1458
|
+
completedSteps,
|
|
1459
|
+
duration: Date.now() - startTime
|
|
1460
|
+
};
|
|
1461
|
+
} catch (error) {
|
|
1462
|
+
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
1463
|
+
const failedStepIndex = completedSteps.length;
|
|
1464
|
+
sagaLog.state = "compensating";
|
|
1465
|
+
await this._saveSagaLog(sagaLog);
|
|
1466
|
+
for (let i = completedSteps.length - 1; i >= 0; i--) {
|
|
1467
|
+
const step = steps[i];
|
|
1468
|
+
sagaLog.steps[i].state = "compensating";
|
|
1469
|
+
await this._saveSagaLog(sagaLog);
|
|
1470
|
+
try {
|
|
1471
|
+
await step.compensate(step.data);
|
|
1472
|
+
sagaLog.steps[i].state = "compensated";
|
|
1473
|
+
} catch (compError) {
|
|
1474
|
+
const compErrorMessage = compError instanceof Error ? compError.message : String(compError);
|
|
1475
|
+
sagaLog.steps[i].state = "failed";
|
|
1476
|
+
sagaLog.steps[i].error = `Compensation failed: ${compErrorMessage}`;
|
|
1477
|
+
}
|
|
1478
|
+
await this._saveSagaLog(sagaLog);
|
|
1479
|
+
}
|
|
1480
|
+
sagaLog.state = "compensated";
|
|
1481
|
+
sagaLog.updatedAt = Date.now();
|
|
1482
|
+
await this._saveSagaLog(sagaLog);
|
|
1483
|
+
return {
|
|
1484
|
+
success: false,
|
|
1485
|
+
sagaId,
|
|
1486
|
+
completedSteps,
|
|
1487
|
+
failedStep: steps[failedStepIndex]?.name,
|
|
1488
|
+
error: errorMessage,
|
|
1489
|
+
duration: Date.now() - startTime
|
|
1490
|
+
};
|
|
1491
|
+
}
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* @zh 恢复未完成的 Saga
|
|
1495
|
+
* @en Recover pending Sagas
|
|
1496
|
+
*/
|
|
1497
|
+
async recover() {
|
|
1498
|
+
if (!this._storage) return 0;
|
|
1499
|
+
const pendingSagas = await this._getPendingSagas();
|
|
1500
|
+
let recoveredCount = 0;
|
|
1501
|
+
for (const saga of pendingSagas) {
|
|
1502
|
+
try {
|
|
1503
|
+
await this._recoverSaga(saga);
|
|
1504
|
+
recoveredCount++;
|
|
1505
|
+
} catch (error) {
|
|
1506
|
+
console.error(`Failed to recover saga ${saga.id}:`, error);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
return recoveredCount;
|
|
1510
|
+
}
|
|
1511
|
+
/**
|
|
1512
|
+
* @zh 获取 Saga 日志
|
|
1513
|
+
* @en Get Saga log
|
|
1514
|
+
*/
|
|
1515
|
+
async getSagaLog(sagaId) {
|
|
1516
|
+
if (!this._storage) return null;
|
|
1517
|
+
return this._storage.get(`saga:${sagaId}`);
|
|
1518
|
+
}
|
|
1519
|
+
async _saveSagaLog(log) {
|
|
1520
|
+
if (!this._storage) return;
|
|
1521
|
+
log.updatedAt = Date.now();
|
|
1522
|
+
await this._storage.set(`saga:${log.id}`, log);
|
|
1523
|
+
}
|
|
1524
|
+
async _getPendingSagas() {
|
|
1525
|
+
return [];
|
|
1526
|
+
}
|
|
1527
|
+
async _recoverSaga(saga) {
|
|
1528
|
+
if (saga.state === "running" || saga.state === "compensating") {
|
|
1529
|
+
const completedSteps = saga.steps.filter((s) => s.state === "completed").map((s) => s.name);
|
|
1530
|
+
saga.state = "compensated";
|
|
1531
|
+
saga.updatedAt = Date.now();
|
|
1532
|
+
if (this._storage) {
|
|
1533
|
+
await this._storage.set(`saga:${saga.id}`, saga);
|
|
1534
|
+
}
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
__name(_SagaOrchestrator, "SagaOrchestrator");
|
|
1539
|
+
var SagaOrchestrator = _SagaOrchestrator;
|
|
1540
|
+
function createSagaOrchestrator(config = {}) {
|
|
1541
|
+
return new SagaOrchestrator(config);
|
|
1542
|
+
}
|
|
1543
|
+
__name(createSagaOrchestrator, "createSagaOrchestrator");
|
|
1544
|
+
|
|
1545
|
+
// src/integration/RoomTransactionMixin.ts
|
|
1546
|
+
function withTransactions(Base, config = {}) {
|
|
1547
|
+
var _a;
|
|
1548
|
+
return _a = class extends Base {
|
|
1549
|
+
constructor(...args) {
|
|
1550
|
+
super(...args);
|
|
1551
|
+
__publicField(this, "_transactionManager");
|
|
1552
|
+
this._transactionManager = new TransactionManager({
|
|
1553
|
+
storage: config.storage,
|
|
1554
|
+
defaultTimeout: config.defaultTimeout,
|
|
1555
|
+
serverId: config.serverId
|
|
1556
|
+
});
|
|
1557
|
+
}
|
|
1558
|
+
get transactions() {
|
|
1559
|
+
return this._transactionManager;
|
|
1560
|
+
}
|
|
1561
|
+
beginTransaction(options) {
|
|
1562
|
+
return this._transactionManager.begin(options);
|
|
1563
|
+
}
|
|
1564
|
+
runTransaction(builder, options) {
|
|
1565
|
+
return this._transactionManager.run(builder, options);
|
|
1566
|
+
}
|
|
1567
|
+
}, __name(_a, "TransactionRoom"), _a;
|
|
1568
|
+
}
|
|
1569
|
+
__name(withTransactions, "withTransactions");
|
|
1570
|
+
var _TransactionRoom = class _TransactionRoom {
|
|
1571
|
+
constructor(config = {}) {
|
|
1572
|
+
__publicField(this, "_transactionManager");
|
|
1573
|
+
this._transactionManager = new TransactionManager({
|
|
1574
|
+
storage: config.storage,
|
|
1575
|
+
defaultTimeout: config.defaultTimeout,
|
|
1576
|
+
serverId: config.serverId
|
|
1577
|
+
});
|
|
1578
|
+
}
|
|
1579
|
+
get transactions() {
|
|
1580
|
+
return this._transactionManager;
|
|
1581
|
+
}
|
|
1582
|
+
beginTransaction(options) {
|
|
1583
|
+
return this._transactionManager.begin(options);
|
|
1584
|
+
}
|
|
1585
|
+
runTransaction(builder, options) {
|
|
1586
|
+
return this._transactionManager.run(builder, options);
|
|
1587
|
+
}
|
|
1588
|
+
};
|
|
1589
|
+
__name(_TransactionRoom, "TransactionRoom");
|
|
1590
|
+
var TransactionRoom = _TransactionRoom;
|
|
1591
|
+
|
|
1592
|
+
// src/tokens.ts
|
|
1593
|
+
import { createServiceToken } from "@esengine/ecs-framework";
|
|
1594
|
+
var TransactionManagerToken = createServiceToken("transactionManager");
|
|
1595
|
+
var TransactionStorageToken = createServiceToken("transactionStorage");
|
|
1596
|
+
export {
|
|
1597
|
+
BaseOperation,
|
|
1598
|
+
CurrencyOperation,
|
|
1599
|
+
InventoryOperation,
|
|
1600
|
+
MemoryStorage,
|
|
1601
|
+
MongoStorage,
|
|
1602
|
+
RedisStorage,
|
|
1603
|
+
SagaOrchestrator,
|
|
1604
|
+
TradeOperation,
|
|
1605
|
+
TransactionContext,
|
|
1606
|
+
TransactionManager,
|
|
1607
|
+
TransactionManagerToken,
|
|
1608
|
+
TransactionRoom,
|
|
1609
|
+
TransactionStorageToken,
|
|
1610
|
+
createCurrencyOperation,
|
|
1611
|
+
createInventoryOperation,
|
|
1612
|
+
createMemoryStorage,
|
|
1613
|
+
createMongoStorage,
|
|
1614
|
+
createRedisStorage,
|
|
1615
|
+
createSagaOrchestrator,
|
|
1616
|
+
createTradeOperation,
|
|
1617
|
+
createTransactionContext,
|
|
1618
|
+
createTransactionManager,
|
|
1619
|
+
withTransactions
|
|
1620
|
+
};
|
|
1621
|
+
//# sourceMappingURL=index.js.map
|