@optimystic/db-core 0.5.2 → 0.6.0
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/src/btree/btree.d.ts +2 -0
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +72 -52
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +13 -0
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/collection/collection.d.ts +3 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +6 -0
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.js +1 -1
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/logger.d.ts +4 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +8 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/transaction/coordinator.d.ts +9 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +76 -8
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/index.d.ts +2 -2
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/session.d.ts +7 -3
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +23 -10
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +9 -3
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +14 -7
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts +9 -2
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +26 -6
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +84 -9
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +4 -0
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -9
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic-proxy.d.ts +26 -0
- package/dist/src/transform/atomic-proxy.d.ts.map +1 -0
- package/dist/src/transform/atomic-proxy.js +47 -0
- package/dist/src/transform/atomic-proxy.js.map +1 -0
- package/dist/src/transform/cache-source.d.ts +3 -2
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +15 -3
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/index.d.ts +1 -0
- package/dist/src/transform/index.d.ts.map +1 -1
- package/dist/src/transform/index.js +1 -0
- package/dist/src/transform/index.js.map +1 -1
- package/dist/src/utility/batch-coordinator.d.ts.map +1 -1
- package/dist/src/utility/batch-coordinator.js +6 -1
- package/dist/src/utility/batch-coordinator.js.map +1 -1
- package/dist/src/utility/hash-string.d.ts +3 -6
- package/dist/src/utility/hash-string.d.ts.map +1 -1
- package/dist/src/utility/hash-string.js +8 -11
- package/dist/src/utility/hash-string.js.map +1 -1
- package/dist/src/utility/lru-map.d.ts +18 -0
- package/dist/src/utility/lru-map.d.ts.map +1 -0
- package/dist/src/utility/lru-map.js +52 -0
- package/dist/src/utility/lru-map.js.map +1 -0
- package/package.json +15 -8
- package/src/btree/btree.ts +71 -50
- package/src/cluster/structs.ts +11 -0
- package/src/collection/collection.ts +9 -0
- package/src/index.ts +1 -0
- package/src/log/log.ts +1 -1
- package/src/logger.ts +10 -0
- package/src/transaction/coordinator.ts +86 -9
- package/src/transaction/index.ts +4 -2
- package/src/transaction/session.ts +34 -10
- package/src/transaction/transaction.ts +23 -10
- package/src/transaction/validator.ts +34 -7
- package/src/transactor/network-transactor.ts +92 -11
- package/src/transactor/transactor-source.ts +28 -9
- package/src/transform/atomic-proxy.ts +49 -0
- package/src/transform/cache-source.ts +18 -4
- package/src/transform/index.ts +1 -0
- package/src/utility/batch-coordinator.ts +7 -1
- package/src/utility/hash-string.ts +14 -17
- package/src/utility/lru-map.ts +55 -0
- package/dist/index.min.js +0 -9
- package/dist/index.min.js.map +0 -7
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import type { ITransactor, BlockId, CollectionId, Transforms, PendRequest, CommitRequest, ActionId, IBlock, BlockOperations } from "../index.js";
|
|
2
|
-
import type { Transaction, ExecutionResult, ITransactionEngine, CollectionActions } from "./transaction.js";
|
|
2
|
+
import type { Transaction, ExecutionResult, ITransactionEngine, CollectionActions, ReadDependency } from "./transaction.js";
|
|
3
3
|
import type { PeerId } from "../network/types.js";
|
|
4
4
|
import type { Collection } from "../collection/collection.js";
|
|
5
5
|
import { TransactionContext } from "./context.js";
|
|
6
6
|
import { ActionsEngine } from "./actions-engine.js";
|
|
7
|
-
import { createActionsStatements, createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
7
|
+
import { createActionsStatements, createTransactionStamp, createTransactionId, isTransactionExpired } from "./transaction.js";
|
|
8
8
|
import { Log, blockIdsForTransforms, transformsFromTransform, hashString } from "../index.js";
|
|
9
|
+
import { createLogger } from "../logger.js";
|
|
10
|
+
|
|
11
|
+
const log = createLogger('trx:coordinator');
|
|
9
12
|
|
|
10
13
|
/**
|
|
11
14
|
* Represents an operation on a block within a collection.
|
|
@@ -70,6 +73,10 @@ export class TransactionCoordinator {
|
|
|
70
73
|
* @param transaction - The transaction to commit
|
|
71
74
|
*/
|
|
72
75
|
async commit(transaction: Transaction): Promise<void> {
|
|
76
|
+
if (isTransactionExpired(transaction.stamp)) {
|
|
77
|
+
throw new Error(`Transaction expired at ${transaction.stamp.expiration}`);
|
|
78
|
+
}
|
|
79
|
+
|
|
73
80
|
// Collect transforms and determine critical blocks for each affected collection
|
|
74
81
|
const collectionData = Array.from(this.collections.entries())
|
|
75
82
|
.map(([collectionId, collection]) => ({
|
|
@@ -122,7 +129,7 @@ export class TransactionCoordinator {
|
|
|
122
129
|
)
|
|
123
130
|
]);
|
|
124
131
|
|
|
125
|
-
const operationsHash = this.hashOperations(allOperations);
|
|
132
|
+
const operationsHash = await this.hashOperations(allOperations);
|
|
126
133
|
|
|
127
134
|
// Execute consensus phases (GATHER, PEND, COMMIT)
|
|
128
135
|
const coordResult = await this.coordinateTransaction(
|
|
@@ -190,14 +197,34 @@ export class TransactionCoordinator {
|
|
|
190
197
|
}
|
|
191
198
|
}
|
|
192
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Collect read dependencies from all participating collections.
|
|
202
|
+
*/
|
|
203
|
+
getReadDependencies(): ReadDependency[] {
|
|
204
|
+
const reads: ReadDependency[] = [];
|
|
205
|
+
for (const collection of this.collections.values()) {
|
|
206
|
+
reads.push(...collection.getReadDependencies());
|
|
207
|
+
}
|
|
208
|
+
return reads;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Clear read dependencies from all collections.
|
|
213
|
+
*/
|
|
214
|
+
clearReadDependencies(): void {
|
|
215
|
+
for (const collection of this.collections.values()) {
|
|
216
|
+
collection.clearReadDependencies();
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
193
220
|
/**
|
|
194
221
|
* Compute hash of all operations in a transaction.
|
|
195
222
|
* This hash is used for validation - validators re-execute the transaction
|
|
196
223
|
* and compare their computed operations hash with this one.
|
|
197
224
|
*/
|
|
198
|
-
private hashOperations(operations: readonly Operation[]): string {
|
|
225
|
+
private async hashOperations(operations: readonly Operation[]): Promise<string> {
|
|
199
226
|
const operationsData = JSON.stringify(operations);
|
|
200
|
-
return `ops:${hashString(operationsData)}`;
|
|
227
|
+
return `ops:${await hashString(operationsData)}`;
|
|
201
228
|
}
|
|
202
229
|
|
|
203
230
|
/**
|
|
@@ -223,7 +250,7 @@ export class TransactionCoordinator {
|
|
|
223
250
|
const reads = context.getReads();
|
|
224
251
|
|
|
225
252
|
// Create stamp from context
|
|
226
|
-
const stamp = createTransactionStamp(
|
|
253
|
+
const stamp = await createTransactionStamp(
|
|
227
254
|
'local', // TODO: Get from context or coordinator
|
|
228
255
|
Date.now(),
|
|
229
256
|
'', // TODO: Get from engine
|
|
@@ -234,7 +261,7 @@ export class TransactionCoordinator {
|
|
|
234
261
|
stamp,
|
|
235
262
|
statements,
|
|
236
263
|
reads,
|
|
237
|
-
id: createTransactionId(stamp.id, statements, reads)
|
|
264
|
+
id: await createTransactionId(stamp.id, statements, reads)
|
|
238
265
|
};
|
|
239
266
|
|
|
240
267
|
const engine = new ActionsEngine(this);
|
|
@@ -254,12 +281,22 @@ export class TransactionCoordinator {
|
|
|
254
281
|
* @returns Execution result with actions and results
|
|
255
282
|
*/
|
|
256
283
|
async execute(transaction: Transaction, engine: ITransactionEngine): Promise<ExecutionResult> {
|
|
284
|
+
const trxId = transaction.id;
|
|
285
|
+
const t0 = Date.now();
|
|
286
|
+
|
|
287
|
+
if (isTransactionExpired(transaction.stamp)) {
|
|
288
|
+
return { success: false, error: `Transaction expired at ${transaction.stamp.expiration}` };
|
|
289
|
+
}
|
|
290
|
+
|
|
257
291
|
// 1. Validate engine matches transaction
|
|
258
292
|
// Note: We don't enforce this strictly since the engine is passed in explicitly
|
|
259
293
|
// The caller is responsible for ensuring the correct engine is used
|
|
260
294
|
|
|
295
|
+
const tEngine = Date.now();
|
|
261
296
|
const result = await engine.execute(transaction);
|
|
297
|
+
const engineMs = Date.now() - tEngine;
|
|
262
298
|
if (!result.success) {
|
|
299
|
+
log('execute:done trxId=%s engine=%dms success=false total=%dms', trxId, engineMs, Date.now() - t0);
|
|
263
300
|
return result;
|
|
264
301
|
}
|
|
265
302
|
|
|
@@ -268,6 +305,7 @@ export class TransactionCoordinator {
|
|
|
268
305
|
}
|
|
269
306
|
|
|
270
307
|
// 2. Apply actions to collections and collect transforms
|
|
308
|
+
const tApply = Date.now();
|
|
271
309
|
const collectionTransforms = new Map<CollectionId, Transforms>();
|
|
272
310
|
const criticalBlocks = new Map<CollectionId, BlockId>();
|
|
273
311
|
const actionResults = new Map<CollectionId, any[]>();
|
|
@@ -301,9 +339,12 @@ export class TransactionCoordinator {
|
|
|
301
339
|
({ type: 'delete' as const, collectionId, blockId })
|
|
302
340
|
)
|
|
303
341
|
]);
|
|
304
|
-
const operationsHash = this.hashOperations(allOperations);
|
|
342
|
+
const operationsHash = await this.hashOperations(allOperations);
|
|
343
|
+
|
|
344
|
+
const applyMs = Date.now() - tApply;
|
|
305
345
|
|
|
306
346
|
// 4. Coordinate (GATHER if multi-collection)
|
|
347
|
+
const tCoord = Date.now();
|
|
307
348
|
const coordResult = await this.coordinateTransaction(
|
|
308
349
|
transaction,
|
|
309
350
|
operationsHash,
|
|
@@ -311,11 +352,31 @@ export class TransactionCoordinator {
|
|
|
311
352
|
criticalBlocks
|
|
312
353
|
);
|
|
313
354
|
|
|
355
|
+
const coordMs = Date.now() - tCoord;
|
|
314
356
|
if (!coordResult.success) {
|
|
357
|
+
log('execute:done trxId=%s engine=%dms apply=%dms coordinate=%dms success=false total=%dms', trxId, engineMs, applyMs, coordMs, Date.now() - t0);
|
|
315
358
|
return coordResult;
|
|
316
359
|
}
|
|
317
360
|
|
|
318
|
-
//
|
|
361
|
+
// 5. Update actionContext and reset trackers after successful commit
|
|
362
|
+
for (const collectionActions of result.actions) {
|
|
363
|
+
const collection = this.collections.get(collectionActions.collectionId);
|
|
364
|
+
if (collection) {
|
|
365
|
+
const newRev = (collection['source'].actionContext?.rev ?? 0) + 1;
|
|
366
|
+
const actionId = transaction.id;
|
|
367
|
+
collection['source'].actionContext = {
|
|
368
|
+
committed: [
|
|
369
|
+
...(collection['source'].actionContext?.committed ?? []),
|
|
370
|
+
{ actionId, rev: newRev }
|
|
371
|
+
],
|
|
372
|
+
rev: newRev,
|
|
373
|
+
};
|
|
374
|
+
collection.tracker.reset();
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// 6. Return results from actions
|
|
379
|
+
log('execute:done trxId=%s engine=%dms apply=%dms coordinate=%dms total=%dms', trxId, engineMs, applyMs, coordMs, Date.now() - t0);
|
|
319
380
|
return {
|
|
320
381
|
success: true,
|
|
321
382
|
actions: result.actions,
|
|
@@ -399,36 +460,48 @@ export class TransactionCoordinator {
|
|
|
399
460
|
collectionTransforms: Map<CollectionId, Transforms>,
|
|
400
461
|
criticalBlocks: Map<CollectionId, BlockId>
|
|
401
462
|
): Promise<{ success: boolean; error?: string }> {
|
|
463
|
+
const trxId = transaction.id;
|
|
464
|
+
const t0 = Date.now();
|
|
465
|
+
|
|
402
466
|
// 1. GATHER phase: collect critical cluster nominees (skip if single collection)
|
|
403
467
|
const criticalBlockIds = Array.from(criticalBlocks.values());
|
|
468
|
+
const tGather = Date.now();
|
|
404
469
|
const superclusterNominees = await this.gatherPhase(criticalBlockIds);
|
|
470
|
+
const gatherMs = Date.now() - tGather;
|
|
405
471
|
|
|
406
472
|
// 2. PEND phase: distribute to all block clusters
|
|
473
|
+
const tPend = Date.now();
|
|
407
474
|
const pendResult = await this.pendPhase(
|
|
408
475
|
transaction,
|
|
409
476
|
operationsHash,
|
|
410
477
|
collectionTransforms,
|
|
411
478
|
superclusterNominees
|
|
412
479
|
);
|
|
480
|
+
const pendMs = Date.now() - tPend;
|
|
413
481
|
if (!pendResult.success) {
|
|
482
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms (failed) total=%dms', trxId, gatherMs, pendMs, Date.now() - t0);
|
|
414
483
|
return pendResult;
|
|
415
484
|
}
|
|
416
485
|
|
|
417
486
|
// 3. COMMIT phase: commit to all critical blocks
|
|
487
|
+
const tCommit = Date.now();
|
|
418
488
|
const commitResult = await this.commitPhase(
|
|
419
489
|
transaction.id as ActionId,
|
|
420
490
|
criticalBlockIds,
|
|
421
491
|
pendResult.pendedBlockIds!
|
|
422
492
|
);
|
|
493
|
+
const commitMs = Date.now() - tCommit;
|
|
423
494
|
if (!commitResult.success) {
|
|
424
495
|
// Cancel pending actions on failure
|
|
425
496
|
await this.cancelPhase(transaction.id as ActionId, collectionTransforms);
|
|
497
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms commit=%dms (failed) total=%dms', trxId, gatherMs, pendMs, commitMs, Date.now() - t0);
|
|
426
498
|
return commitResult;
|
|
427
499
|
}
|
|
428
500
|
|
|
429
501
|
// 4. PROPAGATE and CHECKPOINT phases are handled by clusters automatically
|
|
430
502
|
// (as per user's note: "managed by each cluster, the client doesn't have to worry about them")
|
|
431
503
|
|
|
504
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms commit=%dms total=%dms', trxId, gatherMs, pendMs, commitMs, Date.now() - t0);
|
|
432
505
|
return { success: true };
|
|
433
506
|
}
|
|
434
507
|
|
|
@@ -518,6 +591,10 @@ export class TransactionCoordinator {
|
|
|
518
591
|
// Pend the transaction
|
|
519
592
|
const pendResult = await this.transactor.pend(pendRequest);
|
|
520
593
|
if (!pendResult.success) {
|
|
594
|
+
// Cancel any already-pended collections before returning
|
|
595
|
+
for (const [pendedCollectionId, pendedBlockIdList] of pendedBlockIds.entries()) {
|
|
596
|
+
await this.transactor.cancel({ actionId, blockIds: pendedBlockIdList });
|
|
597
|
+
}
|
|
521
598
|
return {
|
|
522
599
|
success: false,
|
|
523
600
|
error: `Pend failed for collection ${collectionId}: ${pendResult.reason}`
|
package/src/transaction/index.ts
CHANGED
|
@@ -14,7 +14,9 @@ export type {
|
|
|
14
14
|
export {
|
|
15
15
|
createTransactionStamp,
|
|
16
16
|
createTransactionId,
|
|
17
|
-
createActionsStatements
|
|
17
|
+
createActionsStatements,
|
|
18
|
+
DEFAULT_TRANSACTION_TTL_MS,
|
|
19
|
+
isTransactionExpired
|
|
18
20
|
} from './transaction.js';
|
|
19
21
|
|
|
20
22
|
export {
|
|
@@ -26,4 +28,4 @@ export { TransactionCoordinator } from './coordinator.js';
|
|
|
26
28
|
export { TransactionContext } from './context.js';
|
|
27
29
|
export { TransactionSession } from './session.js';
|
|
28
30
|
export { TransactionValidator } from './validator.js';
|
|
29
|
-
export type { EngineRegistration, ValidationCoordinatorFactory } from './validator.js';
|
|
31
|
+
export type { EngineRegistration, ValidationCoordinatorFactory, BlockStateProvider } from './validator.js';
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { TransactionCoordinator } from "./coordinator.js";
|
|
2
2
|
import type { Transaction, ExecutionResult, ITransactionEngine, TransactionStamp, CollectionActions } from "./transaction.js";
|
|
3
|
-
import { createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
3
|
+
import { createTransactionStamp, createTransactionId, isTransactionExpired } from "./transaction.js";
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* TransactionSession manages incremental transaction building.
|
|
@@ -14,7 +14,7 @@ import { createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
|
14
14
|
* - The Transaction is then committed through coordinator.commit() for PEND/COMMIT orchestration
|
|
15
15
|
*
|
|
16
16
|
* Usage:
|
|
17
|
-
* const session =
|
|
17
|
+
* const session = await TransactionSession.create(coordinator, engine);
|
|
18
18
|
* await session.execute('INSERT INTO users (id, name) VALUES (?, ?)', [1, 'Alice']);
|
|
19
19
|
* await session.execute('SELECT * FROM orders WHERE user_id = ?', [1]);
|
|
20
20
|
* const result = await session.commit();
|
|
@@ -27,19 +27,33 @@ export class TransactionSession {
|
|
|
27
27
|
private committed = false;
|
|
28
28
|
private rolledBack = false;
|
|
29
29
|
|
|
30
|
-
constructor(
|
|
30
|
+
private constructor(
|
|
31
31
|
private readonly coordinator: TransactionCoordinator,
|
|
32
32
|
private readonly engine: ITransactionEngine,
|
|
33
|
-
|
|
34
|
-
schemaHash: string = '' // TODO: Get from engine
|
|
33
|
+
stamp: TransactionStamp
|
|
35
34
|
) {
|
|
36
|
-
|
|
37
|
-
|
|
35
|
+
this.stamp = stamp;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Create a new TransactionSession.
|
|
40
|
+
* Uses async factory because stamp creation requires SHA-256 hashing.
|
|
41
|
+
*/
|
|
42
|
+
static async create(
|
|
43
|
+
coordinator: TransactionCoordinator,
|
|
44
|
+
engine: ITransactionEngine,
|
|
45
|
+
peerId: string = 'local',
|
|
46
|
+
schemaHash: string = '',
|
|
47
|
+
ttlMs?: number
|
|
48
|
+
): Promise<TransactionSession> {
|
|
49
|
+
const stamp = await createTransactionStamp(
|
|
38
50
|
peerId,
|
|
39
51
|
Date.now(),
|
|
40
52
|
schemaHash,
|
|
41
|
-
'unknown' // TODO: Get engine ID from engine
|
|
53
|
+
'unknown', // TODO: Get engine ID from engine
|
|
54
|
+
ttlMs
|
|
42
55
|
);
|
|
56
|
+
return new TransactionSession(coordinator, engine, stamp);
|
|
43
57
|
}
|
|
44
58
|
|
|
45
59
|
/**
|
|
@@ -107,18 +121,27 @@ export class TransactionSession {
|
|
|
107
121
|
if (this.rolledBack) {
|
|
108
122
|
return { success: false, error: 'Transaction already rolled back' };
|
|
109
123
|
}
|
|
124
|
+
if (isTransactionExpired(this.stamp)) {
|
|
125
|
+
return { success: false, error: `Transaction expired at ${this.stamp.expiration}` };
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Collect read dependencies from all participating collections
|
|
129
|
+
const reads = this.coordinator.getReadDependencies();
|
|
110
130
|
|
|
111
131
|
// Create the complete transaction
|
|
112
132
|
const transaction: Transaction = {
|
|
113
133
|
stamp: this.stamp,
|
|
114
134
|
statements: this.statements,
|
|
115
|
-
reads
|
|
116
|
-
id: createTransactionId(this.stamp.id, this.statements,
|
|
135
|
+
reads,
|
|
136
|
+
id: await createTransactionId(this.stamp.id, this.statements, reads)
|
|
117
137
|
};
|
|
118
138
|
|
|
119
139
|
// Commit through coordinator (which will orchestrate PEND/COMMIT)
|
|
120
140
|
await this.coordinator.commit(transaction);
|
|
121
141
|
|
|
142
|
+
// Clear read dependencies after successful commit
|
|
143
|
+
this.coordinator.clearReadDependencies();
|
|
144
|
+
|
|
122
145
|
this.committed = true;
|
|
123
146
|
return { success: true };
|
|
124
147
|
}
|
|
@@ -140,6 +163,7 @@ export class TransactionSession {
|
|
|
140
163
|
|
|
141
164
|
// Rollback through coordinator
|
|
142
165
|
await this.coordinator.rollback(this.stamp.id);
|
|
166
|
+
this.coordinator.clearReadDependencies();
|
|
143
167
|
this.rolledBack = true;
|
|
144
168
|
this.statements.length = 0;
|
|
145
169
|
}
|
|
@@ -21,6 +21,9 @@ export type TransactionStamp = {
|
|
|
21
21
|
/** Which engine (e.g., 'quereus@0.5.3', 'actions@1.0.0') */
|
|
22
22
|
engineId: string;
|
|
23
23
|
|
|
24
|
+
/** Absolute ms epoch after which transaction is invalid */
|
|
25
|
+
expiration: number;
|
|
26
|
+
|
|
24
27
|
/** Hash of the stamp fields (computed) - stable identifier throughout transaction */
|
|
25
28
|
id: string;
|
|
26
29
|
};
|
|
@@ -67,32 +70,42 @@ export type ReadDependency = {
|
|
|
67
70
|
*/
|
|
68
71
|
export type TransactionRef = string; // The transaction ID
|
|
69
72
|
|
|
73
|
+
/** Default transaction time-to-live in milliseconds (30 seconds). */
|
|
74
|
+
export const DEFAULT_TRANSACTION_TTL_MS = 30_000;
|
|
75
|
+
|
|
76
|
+
/** Check whether a transaction stamp has expired. */
|
|
77
|
+
export function isTransactionExpired(stamp: TransactionStamp): boolean {
|
|
78
|
+
return Date.now() > stamp.expiration;
|
|
79
|
+
}
|
|
80
|
+
|
|
70
81
|
/**
|
|
71
82
|
* Create a transaction stamp with computed id.
|
|
72
|
-
* The id is a hash of the stamp fields.
|
|
83
|
+
* The id is a hash of the stamp fields (including expiration).
|
|
73
84
|
*/
|
|
74
|
-
export function createTransactionStamp(
|
|
85
|
+
export async function createTransactionStamp(
|
|
75
86
|
peerId: string,
|
|
76
87
|
timestamp: number,
|
|
77
88
|
schemaHash: string,
|
|
78
|
-
engineId: string
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
89
|
+
engineId: string,
|
|
90
|
+
ttlMs: number = DEFAULT_TRANSACTION_TTL_MS
|
|
91
|
+
): Promise<TransactionStamp> {
|
|
92
|
+
const expiration = timestamp + ttlMs;
|
|
93
|
+
const stampData = JSON.stringify({ peerId, timestamp, schemaHash, engineId, expiration });
|
|
94
|
+
const id = `stamp:${await hashString(stampData)}`;
|
|
95
|
+
return { peerId, timestamp, schemaHash, engineId, expiration, id };
|
|
83
96
|
}
|
|
84
97
|
|
|
85
98
|
/**
|
|
86
99
|
* Create a transaction id from stamp id, statements, and reads.
|
|
87
100
|
* This is the final transaction identity used in logs.
|
|
88
101
|
*/
|
|
89
|
-
export function createTransactionId(
|
|
102
|
+
export async function createTransactionId(
|
|
90
103
|
stampId: string,
|
|
91
104
|
statements: string[],
|
|
92
105
|
reads: ReadDependency[]
|
|
93
|
-
): string {
|
|
106
|
+
): Promise<string> {
|
|
94
107
|
const txData = JSON.stringify({ stampId, statements, reads });
|
|
95
|
-
return `tx:${hashString(txData)}`;
|
|
108
|
+
return `tx:${await hashString(txData)}`;
|
|
96
109
|
}
|
|
97
110
|
|
|
98
111
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BlockId, CollectionId, IBlock, BlockOperations, Transforms, ITransactor } from '../index.js';
|
|
2
|
-
import type { Transaction, ITransactionEngine, ITransactionValidator, ValidationResult, CollectionActions } from './transaction.js';
|
|
2
|
+
import type { Transaction, ITransactionEngine, ITransactionValidator, ValidationResult, CollectionActions, ReadDependency } from './transaction.js';
|
|
3
|
+
import type { BlockActionState } from '../network/struct.js';
|
|
4
|
+
import { isTransactionExpired } from './transaction.js';
|
|
3
5
|
import type { Collection } from '../collection/collection.js';
|
|
4
6
|
import { Tracker } from '../transform/tracker.js';
|
|
5
7
|
import { hashString } from '../utility/hash-string.js';
|
|
@@ -36,6 +38,12 @@ export type ValidationCoordinatorFactory = () => {
|
|
|
36
38
|
dispose(): void;
|
|
37
39
|
};
|
|
38
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Provides current block state for read dependency validation.
|
|
43
|
+
* Returns the latest BlockActionState for a given block, or undefined if the block doesn't exist.
|
|
44
|
+
*/
|
|
45
|
+
export type BlockStateProvider = (blockId: BlockId) => Promise<BlockActionState | undefined>;
|
|
46
|
+
|
|
39
47
|
/**
|
|
40
48
|
* Transaction validator implementation.
|
|
41
49
|
*
|
|
@@ -45,12 +53,21 @@ export type ValidationCoordinatorFactory = () => {
|
|
|
45
53
|
export class TransactionValidator implements ITransactionValidator {
|
|
46
54
|
constructor(
|
|
47
55
|
private readonly engines: Map<string, EngineRegistration>,
|
|
48
|
-
private readonly createValidationCoordinator: ValidationCoordinatorFactory
|
|
56
|
+
private readonly createValidationCoordinator: ValidationCoordinatorFactory,
|
|
57
|
+
private readonly blockStateProvider?: BlockStateProvider
|
|
49
58
|
) {}
|
|
50
59
|
|
|
51
60
|
async validate(transaction: Transaction, operationsHash: string): Promise<ValidationResult> {
|
|
52
61
|
const { stamp, statements } = transaction;
|
|
53
62
|
|
|
63
|
+
// 0. Check expiration before any other work
|
|
64
|
+
if (isTransactionExpired(stamp)) {
|
|
65
|
+
return {
|
|
66
|
+
valid: false,
|
|
67
|
+
reason: `Transaction expired at ${stamp.expiration}`
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
// 1. Verify engine exists
|
|
55
72
|
const registration = this.engines.get(stamp.engineId);
|
|
56
73
|
if (!registration) {
|
|
@@ -70,8 +87,18 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
70
87
|
}
|
|
71
88
|
|
|
72
89
|
// 3. Verify read dependencies (optimistic concurrency)
|
|
73
|
-
|
|
74
|
-
|
|
90
|
+
if (this.blockStateProvider && transaction.reads.length > 0) {
|
|
91
|
+
for (const read of transaction.reads) {
|
|
92
|
+
const currentState = await this.blockStateProvider(read.blockId);
|
|
93
|
+
const currentRev = currentState?.latest?.rev ?? 0;
|
|
94
|
+
if (currentRev !== read.revision) {
|
|
95
|
+
return {
|
|
96
|
+
valid: false,
|
|
97
|
+
reason: `Stale read: block ${read.blockId} was at revision ${read.revision} but is now at ${currentRev}`
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
}
|
|
75
102
|
|
|
76
103
|
// 4. Create isolated validation coordinator
|
|
77
104
|
const validationCoordinator = this.createValidationCoordinator();
|
|
@@ -96,7 +123,7 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
96
123
|
const allOperations = this.collectOperations(transforms);
|
|
97
124
|
|
|
98
125
|
// 8. Compute hash
|
|
99
|
-
const computedHash = this.hashOperations(allOperations);
|
|
126
|
+
const computedHash = await this.hashOperations(allOperations);
|
|
100
127
|
|
|
101
128
|
// 9. Compare with sender's hash
|
|
102
129
|
if (computedHash !== operationsHash) {
|
|
@@ -139,9 +166,9 @@ export class TransactionValidator implements ITransactionValidator {
|
|
|
139
166
|
* Compute hash of all operations.
|
|
140
167
|
* Must match TransactionCoordinator.hashOperations for consistent validation.
|
|
141
168
|
*/
|
|
142
|
-
private hashOperations(operations: readonly Operation[]): string {
|
|
169
|
+
private async hashOperations(operations: readonly Operation[]): Promise<string> {
|
|
143
170
|
const operationsData = JSON.stringify(operations);
|
|
144
|
-
return `ops:${hashString(operationsData)}`;
|
|
171
|
+
return `ops:${await hashString(operationsData)}`;
|
|
145
172
|
}
|
|
146
173
|
}
|
|
147
174
|
|