@unrdf/kgc-runtime 26.4.2
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/IMPLEMENTATION_SUMMARY.json +150 -0
- package/PLUGIN_SYSTEM_SUMMARY.json +149 -0
- package/README.md +98 -0
- package/TRANSACTION_IMPLEMENTATION.json +119 -0
- package/capability-map.md +93 -0
- package/docs/api-stability.md +269 -0
- package/docs/extensions/plugin-development.md +382 -0
- package/package.json +40 -0
- package/plugins/registry.json +35 -0
- package/src/admission-gate.mjs +414 -0
- package/src/api-version.mjs +373 -0
- package/src/atomic-admission.mjs +310 -0
- package/src/bounds.mjs +289 -0
- package/src/bulkhead-manager.mjs +280 -0
- package/src/capsule.mjs +524 -0
- package/src/crdt.mjs +361 -0
- package/src/enhanced-bounds.mjs +614 -0
- package/src/executor.mjs +73 -0
- package/src/freeze-restore.mjs +521 -0
- package/src/index.mjs +62 -0
- package/src/materialized-views.mjs +371 -0
- package/src/merge.mjs +472 -0
- package/src/plugin-isolation.mjs +392 -0
- package/src/plugin-manager.mjs +441 -0
- package/src/projections-api.mjs +336 -0
- package/src/projections-cli.mjs +238 -0
- package/src/projections-docs.mjs +300 -0
- package/src/projections-ide.mjs +278 -0
- package/src/receipt.mjs +340 -0
- package/src/rollback.mjs +258 -0
- package/src/saga-orchestrator.mjs +355 -0
- package/src/schemas.mjs +1330 -0
- package/src/storage-optimization.mjs +359 -0
- package/src/tool-registry.mjs +272 -0
- package/src/transaction.mjs +466 -0
- package/src/validators.mjs +485 -0
- package/src/work-item.mjs +449 -0
- package/templates/plugin-template/README.md +58 -0
- package/templates/plugin-template/index.mjs +162 -0
- package/templates/plugin-template/plugin.json +19 -0
- package/test/admission-gate.test.mjs +583 -0
- package/test/api-version.test.mjs +74 -0
- package/test/atomic-admission.test.mjs +155 -0
- package/test/bounds.test.mjs +341 -0
- package/test/bulkhead-manager.test.mjs +236 -0
- package/test/capsule.test.mjs +625 -0
- package/test/crdt.test.mjs +215 -0
- package/test/enhanced-bounds.test.mjs +487 -0
- package/test/freeze-restore.test.mjs +472 -0
- package/test/materialized-views.test.mjs +243 -0
- package/test/merge.test.mjs +665 -0
- package/test/plugin-isolation.test.mjs +109 -0
- package/test/plugin-manager.test.mjs +208 -0
- package/test/projections-api.test.mjs +293 -0
- package/test/projections-cli.test.mjs +204 -0
- package/test/projections-docs.test.mjs +173 -0
- package/test/projections-ide.test.mjs +230 -0
- package/test/receipt.test.mjs +295 -0
- package/test/rollback.test.mjs +132 -0
- package/test/saga-orchestrator.test.mjs +279 -0
- package/test/schemas.test.mjs +716 -0
- package/test/storage-optimization.test.mjs +503 -0
- package/test/tool-registry.test.mjs +341 -0
- package/test/transaction.test.mjs +189 -0
- package/test/validators.test.mjs +463 -0
- package/test/work-item.test.mjs +548 -0
- package/test/work-item.test.mjs.bak +548 -0
- package/var/kgc/test-atomic-log.json +519 -0
- package/var/kgc/test-cascading-log.json +145 -0
- package/vitest.config.mjs +18 -0
|
@@ -0,0 +1,466 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @fileoverview Two-Phase Commit Transaction Manager for KGC Runtime
|
|
3
|
+
* Implements atomic transactions with prepare/commit/rollback phases
|
|
4
|
+
*
|
|
5
|
+
* Pattern: Pure functions + Zod validation + Receipt generation
|
|
6
|
+
* Guarantees: ACID properties for capsule operations
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { blake3 } from 'hash-wasm';
|
|
10
|
+
import { z } from 'zod';
|
|
11
|
+
import { generateReceipt } from './receipt.mjs';
|
|
12
|
+
|
|
13
|
+
// ============================================================================
|
|
14
|
+
// Schemas
|
|
15
|
+
// ============================================================================
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Operation schema - represents a single atomic operation
|
|
19
|
+
*/
|
|
20
|
+
const OperationSchema = z.object({
|
|
21
|
+
id: z.string(),
|
|
22
|
+
type: z.enum(['add_capsule', 'remove_capsule', 'update_state', 'merge']),
|
|
23
|
+
data: z.any(),
|
|
24
|
+
undo: z.any().optional(), // Undo information for rollback
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Transaction schema
|
|
29
|
+
*/
|
|
30
|
+
const TransactionSchema = z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
timestamp: z.string(),
|
|
33
|
+
operations: z.array(OperationSchema),
|
|
34
|
+
status: z.enum(['pending', 'prepared', 'committed', 'aborted']),
|
|
35
|
+
preparedAt: z.string().optional(),
|
|
36
|
+
committedAt: z.string().optional(),
|
|
37
|
+
abortedAt: z.string().optional(),
|
|
38
|
+
hash: z.string().optional(),
|
|
39
|
+
parentHash: z.string().optional().nullable(),
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* @typedef {z.infer<typeof TransactionSchema>} Transaction
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @typedef {z.infer<typeof OperationSchema>} Operation
|
|
48
|
+
*/
|
|
49
|
+
|
|
50
|
+
// ============================================================================
|
|
51
|
+
// TransactionManager Class
|
|
52
|
+
// ============================================================================
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* TransactionManager - Implements two-phase commit protocol
|
|
56
|
+
*
|
|
57
|
+
* Phase 1 (Prepare):
|
|
58
|
+
* - Validate all operations
|
|
59
|
+
* - Reserve resources
|
|
60
|
+
* - Create undo log entries
|
|
61
|
+
* - Check constraints
|
|
62
|
+
*
|
|
63
|
+
* Phase 2 (Commit):
|
|
64
|
+
* - Apply all changes atomically
|
|
65
|
+
* - Generate receipts
|
|
66
|
+
* - Update state
|
|
67
|
+
*
|
|
68
|
+
* Rollback:
|
|
69
|
+
* - If phase 2 fails, undo phase 1 changes
|
|
70
|
+
* - Use undo log to restore previous state
|
|
71
|
+
*
|
|
72
|
+
* @example
|
|
73
|
+
* const txManager = new TransactionManager();
|
|
74
|
+
* const tx = txManager.begin([
|
|
75
|
+
* { id: 'op1', type: 'add_capsule', data: capsule1 },
|
|
76
|
+
* { id: 'op2', type: 'add_capsule', data: capsule2 }
|
|
77
|
+
* ]);
|
|
78
|
+
*
|
|
79
|
+
* const prepared = await txManager.prepare(tx.id);
|
|
80
|
+
* if (prepared.success) {
|
|
81
|
+
* const committed = await txManager.commit(tx.id);
|
|
82
|
+
* } else {
|
|
83
|
+
* await txManager.rollback(tx.id);
|
|
84
|
+
* }
|
|
85
|
+
*/
|
|
86
|
+
export class TransactionManager {
|
|
87
|
+
/**
|
|
88
|
+
* @param {Object} options - Configuration options
|
|
89
|
+
* @param {Function} options.onRollback - Callback for rollback events
|
|
90
|
+
* @param {string} options.logPath - Path to rollback log file
|
|
91
|
+
*/
|
|
92
|
+
constructor(options = {}) {
|
|
93
|
+
/** @type {Map<string, Transaction>} */
|
|
94
|
+
this.transactions = new Map();
|
|
95
|
+
|
|
96
|
+
/** @type {string[]} */
|
|
97
|
+
this.transactionHistory = [];
|
|
98
|
+
|
|
99
|
+
/** @type {Function} */
|
|
100
|
+
this.onRollback = options.onRollback || (() => {});
|
|
101
|
+
|
|
102
|
+
/** @type {string} */
|
|
103
|
+
this.logPath = options.logPath || './var/kgc/undo-log.json';
|
|
104
|
+
|
|
105
|
+
/** @type {any} */
|
|
106
|
+
this.state = {};
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Begin a new transaction
|
|
111
|
+
*
|
|
112
|
+
* @param {Operation[]} operations - Operations to execute
|
|
113
|
+
* @param {string} [parentHash] - Parent transaction hash for chaining
|
|
114
|
+
* @returns {Transaction} New transaction
|
|
115
|
+
*/
|
|
116
|
+
begin(operations, parentHash = null) {
|
|
117
|
+
const timestamp = new Date().toISOString();
|
|
118
|
+
const id = `tx_${Date.now()}_${Math.random().toString(36).substring(7)}`;
|
|
119
|
+
|
|
120
|
+
const transaction = {
|
|
121
|
+
id,
|
|
122
|
+
timestamp,
|
|
123
|
+
operations: z.array(OperationSchema).parse(operations),
|
|
124
|
+
status: 'pending',
|
|
125
|
+
parentHash,
|
|
126
|
+
};
|
|
127
|
+
|
|
128
|
+
this.transactions.set(id, transaction);
|
|
129
|
+
this.transactionHistory.push(id);
|
|
130
|
+
|
|
131
|
+
return TransactionSchema.parse(transaction);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Phase 1: Prepare transaction
|
|
136
|
+
* Validates operations and reserves resources
|
|
137
|
+
*
|
|
138
|
+
* @param {string} txId - Transaction ID
|
|
139
|
+
* @returns {Promise<{success: boolean, errors: string[], undoOps: any[]}>} Prepare result
|
|
140
|
+
*/
|
|
141
|
+
async prepare(txId) {
|
|
142
|
+
const tx = this.transactions.get(txId);
|
|
143
|
+
if (!tx) {
|
|
144
|
+
return { success: false, errors: ['Transaction not found'], undoOps: [] };
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
if (tx.status !== 'pending') {
|
|
148
|
+
return { success: false, errors: ['Transaction not in pending state'], undoOps: [] };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const errors = [];
|
|
152
|
+
const undoOps = [];
|
|
153
|
+
|
|
154
|
+
// Validate each operation and create undo entries
|
|
155
|
+
for (const op of tx.operations) {
|
|
156
|
+
try {
|
|
157
|
+
// Validate operation structure
|
|
158
|
+
OperationSchema.parse(op);
|
|
159
|
+
|
|
160
|
+
// Create undo operation
|
|
161
|
+
const undoOp = this._createUndoOperation(op);
|
|
162
|
+
undoOps.push(undoOp);
|
|
163
|
+
|
|
164
|
+
// Store undo info in operation
|
|
165
|
+
op.undo = undoOp;
|
|
166
|
+
|
|
167
|
+
// Validate constraints (without applying changes)
|
|
168
|
+
const validation = this._validateOperation(op);
|
|
169
|
+
if (!validation.success) {
|
|
170
|
+
errors.push(...validation.errors);
|
|
171
|
+
}
|
|
172
|
+
} catch (error) {
|
|
173
|
+
errors.push(`Operation ${op.id} validation failed: ${error.message}`);
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
if (errors.length > 0) {
|
|
178
|
+
tx.status = 'aborted';
|
|
179
|
+
tx.abortedAt = new Date().toISOString();
|
|
180
|
+
return { success: false, errors, undoOps: [] };
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// All validations passed - mark as prepared
|
|
184
|
+
tx.status = 'prepared';
|
|
185
|
+
tx.preparedAt = new Date().toISOString();
|
|
186
|
+
|
|
187
|
+
// Generate transaction hash
|
|
188
|
+
const hash = await this._hashTransaction(tx);
|
|
189
|
+
tx.hash = hash;
|
|
190
|
+
|
|
191
|
+
return { success: true, errors: [], undoOps };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Phase 2: Commit transaction
|
|
196
|
+
* Applies all changes atomically
|
|
197
|
+
*
|
|
198
|
+
* @param {string} txId - Transaction ID
|
|
199
|
+
* @returns {Promise<{success: boolean, receipts: any[], errors: string[]}>} Commit result
|
|
200
|
+
*/
|
|
201
|
+
async commit(txId) {
|
|
202
|
+
const tx = this.transactions.get(txId);
|
|
203
|
+
if (!tx) {
|
|
204
|
+
return { success: false, receipts: [], errors: ['Transaction not found'] };
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (tx.status !== 'prepared') {
|
|
208
|
+
return { success: false, receipts: [], errors: ['Transaction not prepared'] };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const receipts = [];
|
|
212
|
+
const errors = [];
|
|
213
|
+
const appliedOps = [];
|
|
214
|
+
|
|
215
|
+
try {
|
|
216
|
+
// Apply all operations
|
|
217
|
+
for (const op of tx.operations) {
|
|
218
|
+
try {
|
|
219
|
+
// Apply operation to state
|
|
220
|
+
this._applyOperation(op);
|
|
221
|
+
appliedOps.push(op);
|
|
222
|
+
|
|
223
|
+
// Generate receipt
|
|
224
|
+
const receipt = await generateReceipt(
|
|
225
|
+
op.type,
|
|
226
|
+
{ operation_id: op.id, data: op.data },
|
|
227
|
+
{ success: true, transaction_id: txId },
|
|
228
|
+
tx.parentHash
|
|
229
|
+
);
|
|
230
|
+
receipts.push(receipt);
|
|
231
|
+
} catch (error) {
|
|
232
|
+
errors.push(`Operation ${op.id} failed: ${error.message}`);
|
|
233
|
+
|
|
234
|
+
// Rollback applied operations
|
|
235
|
+
for (const appliedOp of appliedOps.reverse()) {
|
|
236
|
+
if (appliedOp.undo) {
|
|
237
|
+
this._applyOperation(appliedOp.undo);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
tx.status = 'aborted';
|
|
242
|
+
tx.abortedAt = new Date().toISOString();
|
|
243
|
+
return { success: false, receipts: [], errors };
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// All operations applied successfully
|
|
248
|
+
tx.status = 'committed';
|
|
249
|
+
tx.committedAt = new Date().toISOString();
|
|
250
|
+
|
|
251
|
+
return { success: true, receipts, errors: [] };
|
|
252
|
+
} catch (error) {
|
|
253
|
+
tx.status = 'aborted';
|
|
254
|
+
tx.abortedAt = new Date().toISOString();
|
|
255
|
+
return { success: false, receipts: [], errors: [error.message] };
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Rollback transaction
|
|
261
|
+
* Undoes all changes made during prepare/commit
|
|
262
|
+
*
|
|
263
|
+
* @param {string} txId - Transaction ID
|
|
264
|
+
* @returns {Promise<{success: boolean, undone: number}>} Rollback result
|
|
265
|
+
*/
|
|
266
|
+
async rollback(txId) {
|
|
267
|
+
const tx = this.transactions.get(txId);
|
|
268
|
+
if (!tx) {
|
|
269
|
+
return { success: false, undone: 0 };
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
let undone = 0;
|
|
273
|
+
|
|
274
|
+
// Undo operations in reverse order
|
|
275
|
+
for (const op of tx.operations.slice().reverse()) {
|
|
276
|
+
if (op.undo) {
|
|
277
|
+
try {
|
|
278
|
+
this._applyOperation(op.undo);
|
|
279
|
+
undone++;
|
|
280
|
+
} catch (error) {
|
|
281
|
+
// Log error but continue rollback
|
|
282
|
+
console.error(`Failed to undo operation ${op.id}:`, error);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
tx.status = 'aborted';
|
|
288
|
+
tx.abortedAt = new Date().toISOString();
|
|
289
|
+
|
|
290
|
+
// Notify callback
|
|
291
|
+
this.onRollback({ transaction_id: txId, operations_undone: undone });
|
|
292
|
+
|
|
293
|
+
return { success: true, undone };
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/**
|
|
297
|
+
* Get transaction by ID
|
|
298
|
+
* @param {string} txId - Transaction ID
|
|
299
|
+
* @returns {Transaction|null} Transaction or null
|
|
300
|
+
*/
|
|
301
|
+
getTransaction(txId) {
|
|
302
|
+
const tx = this.transactions.get(txId);
|
|
303
|
+
return tx ? TransactionSchema.parse(tx) : null;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
/**
|
|
307
|
+
* Get all transactions
|
|
308
|
+
* @returns {Transaction[]} All transactions
|
|
309
|
+
*/
|
|
310
|
+
getAllTransactions() {
|
|
311
|
+
return this.transactionHistory.map(id => this.getTransaction(id)).filter(Boolean);
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/**
|
|
315
|
+
* Create undo operation for given operation
|
|
316
|
+
* @private
|
|
317
|
+
*/
|
|
318
|
+
_createUndoOperation(operation) {
|
|
319
|
+
switch (operation.type) {
|
|
320
|
+
case 'add_capsule':
|
|
321
|
+
return {
|
|
322
|
+
id: `undo_${operation.id}`,
|
|
323
|
+
type: 'remove_capsule',
|
|
324
|
+
data: { capsule_id: operation.data.id },
|
|
325
|
+
};
|
|
326
|
+
case 'remove_capsule':
|
|
327
|
+
return {
|
|
328
|
+
id: `undo_${operation.id}`,
|
|
329
|
+
type: 'add_capsule',
|
|
330
|
+
data: this.state.capsules?.[operation.data.capsule_id] || operation.data,
|
|
331
|
+
};
|
|
332
|
+
case 'update_state':
|
|
333
|
+
return {
|
|
334
|
+
id: `undo_${operation.id}`,
|
|
335
|
+
type: 'update_state',
|
|
336
|
+
data: {
|
|
337
|
+
key: operation.data.key,
|
|
338
|
+
value: this.state[operation.data.key],
|
|
339
|
+
previous: operation.data.value,
|
|
340
|
+
},
|
|
341
|
+
};
|
|
342
|
+
case 'merge':
|
|
343
|
+
return {
|
|
344
|
+
id: `undo_${operation.id}`,
|
|
345
|
+
type: 'update_state',
|
|
346
|
+
data: {
|
|
347
|
+
key: 'last_merge',
|
|
348
|
+
value: this.state.last_merge || null,
|
|
349
|
+
},
|
|
350
|
+
};
|
|
351
|
+
default:
|
|
352
|
+
return {
|
|
353
|
+
id: `undo_${operation.id}`,
|
|
354
|
+
type: 'update_state',
|
|
355
|
+
data: {},
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Validate operation without applying it
|
|
362
|
+
* @private
|
|
363
|
+
*/
|
|
364
|
+
_validateOperation(operation) {
|
|
365
|
+
const errors = [];
|
|
366
|
+
|
|
367
|
+
switch (operation.type) {
|
|
368
|
+
case 'add_capsule':
|
|
369
|
+
if (!operation.data?.id) {
|
|
370
|
+
errors.push('Capsule must have an id');
|
|
371
|
+
}
|
|
372
|
+
break;
|
|
373
|
+
case 'remove_capsule':
|
|
374
|
+
if (!operation.data?.capsule_id) {
|
|
375
|
+
errors.push('Remove operation must specify capsule_id');
|
|
376
|
+
}
|
|
377
|
+
break;
|
|
378
|
+
case 'update_state':
|
|
379
|
+
if (!operation.data?.key) {
|
|
380
|
+
errors.push('Update operation must specify key');
|
|
381
|
+
}
|
|
382
|
+
break;
|
|
383
|
+
case 'merge':
|
|
384
|
+
if (!operation.data?.capsules) {
|
|
385
|
+
errors.push('Merge operation must specify capsules');
|
|
386
|
+
}
|
|
387
|
+
break;
|
|
388
|
+
default:
|
|
389
|
+
errors.push(`Unknown operation type: ${operation.type}`);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
return { success: errors.length === 0, errors };
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Apply operation to state
|
|
397
|
+
* @private
|
|
398
|
+
*/
|
|
399
|
+
_applyOperation(operation) {
|
|
400
|
+
if (!this.state.capsules) {
|
|
401
|
+
this.state.capsules = {};
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
switch (operation.type) {
|
|
405
|
+
case 'add_capsule':
|
|
406
|
+
this.state.capsules[operation.data.id] = operation.data;
|
|
407
|
+
break;
|
|
408
|
+
case 'remove_capsule':
|
|
409
|
+
delete this.state.capsules[operation.data.capsule_id];
|
|
410
|
+
break;
|
|
411
|
+
case 'update_state':
|
|
412
|
+
this.state[operation.data.key] = operation.data.value;
|
|
413
|
+
break;
|
|
414
|
+
case 'merge':
|
|
415
|
+
this.state.last_merge = {
|
|
416
|
+
capsules: operation.data.capsules,
|
|
417
|
+
timestamp: new Date().toISOString(),
|
|
418
|
+
};
|
|
419
|
+
break;
|
|
420
|
+
default:
|
|
421
|
+
throw new Error(`Unknown operation type: ${operation.type}`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
/**
|
|
426
|
+
* Hash transaction for integrity verification
|
|
427
|
+
* @private
|
|
428
|
+
*/
|
|
429
|
+
async _hashTransaction(tx) {
|
|
430
|
+
const data = JSON.stringify({
|
|
431
|
+
id: tx.id,
|
|
432
|
+
timestamp: tx.timestamp,
|
|
433
|
+
operations: tx.operations.map(op => ({
|
|
434
|
+
id: op.id,
|
|
435
|
+
type: op.type,
|
|
436
|
+
data: op.data,
|
|
437
|
+
})),
|
|
438
|
+
status: tx.status,
|
|
439
|
+
}, null, 0);
|
|
440
|
+
|
|
441
|
+
return await blake3(data);
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
/**
|
|
445
|
+
* Get current state
|
|
446
|
+
* @returns {any} Current state
|
|
447
|
+
*/
|
|
448
|
+
getState() {
|
|
449
|
+
return { ...this.state };
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
/**
|
|
453
|
+
* Reset state (for testing)
|
|
454
|
+
*/
|
|
455
|
+
reset() {
|
|
456
|
+
this.transactions.clear();
|
|
457
|
+
this.transactionHistory = [];
|
|
458
|
+
this.state = {};
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// ============================================================================
|
|
463
|
+
// Exports
|
|
464
|
+
// ============================================================================
|
|
465
|
+
|
|
466
|
+
export default TransactionManager;
|