@optimystic/db-core 0.5.1 → 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/actions-engine.d.ts +1 -13
- package/dist/src/transaction/actions-engine.d.ts.map +1 -1
- package/dist/src/transaction/actions-engine.js +0 -7
- package/dist/src/transaction/actions-engine.js.map +1 -1
- 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 +77 -11
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/index.d.ts +4 -5
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +2 -2
- 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 +21 -3
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +21 -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/actions-engine.ts +0 -17
- package/src/transaction/coordinator.ts +87 -12
- package/src/transaction/index.ts +7 -6
- package/src/transaction/session.ts +34 -10
- package/src/transaction/transaction.ts +39 -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
|
-
import {
|
|
7
|
-
import { createTransactionStamp, createTransactionId } from "./transaction.js";
|
|
6
|
+
import { ActionsEngine } from "./actions-engine.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,11 +261,9 @@ 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
|
-
// Create ActionsEngine for execution (TransactionContext only supports actions)
|
|
241
|
-
const { ActionsEngine } = await import('./actions-engine.js');
|
|
242
267
|
const engine = new ActionsEngine(this);
|
|
243
268
|
|
|
244
269
|
// Execute through standard path
|
|
@@ -256,12 +281,22 @@ export class TransactionCoordinator {
|
|
|
256
281
|
* @returns Execution result with actions and results
|
|
257
282
|
*/
|
|
258
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
|
+
|
|
259
291
|
// 1. Validate engine matches transaction
|
|
260
292
|
// Note: We don't enforce this strictly since the engine is passed in explicitly
|
|
261
293
|
// The caller is responsible for ensuring the correct engine is used
|
|
262
294
|
|
|
295
|
+
const tEngine = Date.now();
|
|
263
296
|
const result = await engine.execute(transaction);
|
|
297
|
+
const engineMs = Date.now() - tEngine;
|
|
264
298
|
if (!result.success) {
|
|
299
|
+
log('execute:done trxId=%s engine=%dms success=false total=%dms', trxId, engineMs, Date.now() - t0);
|
|
265
300
|
return result;
|
|
266
301
|
}
|
|
267
302
|
|
|
@@ -270,6 +305,7 @@ export class TransactionCoordinator {
|
|
|
270
305
|
}
|
|
271
306
|
|
|
272
307
|
// 2. Apply actions to collections and collect transforms
|
|
308
|
+
const tApply = Date.now();
|
|
273
309
|
const collectionTransforms = new Map<CollectionId, Transforms>();
|
|
274
310
|
const criticalBlocks = new Map<CollectionId, BlockId>();
|
|
275
311
|
const actionResults = new Map<CollectionId, any[]>();
|
|
@@ -303,9 +339,12 @@ export class TransactionCoordinator {
|
|
|
303
339
|
({ type: 'delete' as const, collectionId, blockId })
|
|
304
340
|
)
|
|
305
341
|
]);
|
|
306
|
-
const operationsHash = this.hashOperations(allOperations);
|
|
342
|
+
const operationsHash = await this.hashOperations(allOperations);
|
|
343
|
+
|
|
344
|
+
const applyMs = Date.now() - tApply;
|
|
307
345
|
|
|
308
346
|
// 4. Coordinate (GATHER if multi-collection)
|
|
347
|
+
const tCoord = Date.now();
|
|
309
348
|
const coordResult = await this.coordinateTransaction(
|
|
310
349
|
transaction,
|
|
311
350
|
operationsHash,
|
|
@@ -313,11 +352,31 @@ export class TransactionCoordinator {
|
|
|
313
352
|
criticalBlocks
|
|
314
353
|
);
|
|
315
354
|
|
|
355
|
+
const coordMs = Date.now() - tCoord;
|
|
316
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);
|
|
317
358
|
return coordResult;
|
|
318
359
|
}
|
|
319
360
|
|
|
320
|
-
//
|
|
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);
|
|
321
380
|
return {
|
|
322
381
|
success: true,
|
|
323
382
|
actions: result.actions,
|
|
@@ -401,36 +460,48 @@ export class TransactionCoordinator {
|
|
|
401
460
|
collectionTransforms: Map<CollectionId, Transforms>,
|
|
402
461
|
criticalBlocks: Map<CollectionId, BlockId>
|
|
403
462
|
): Promise<{ success: boolean; error?: string }> {
|
|
463
|
+
const trxId = transaction.id;
|
|
464
|
+
const t0 = Date.now();
|
|
465
|
+
|
|
404
466
|
// 1. GATHER phase: collect critical cluster nominees (skip if single collection)
|
|
405
467
|
const criticalBlockIds = Array.from(criticalBlocks.values());
|
|
468
|
+
const tGather = Date.now();
|
|
406
469
|
const superclusterNominees = await this.gatherPhase(criticalBlockIds);
|
|
470
|
+
const gatherMs = Date.now() - tGather;
|
|
407
471
|
|
|
408
472
|
// 2. PEND phase: distribute to all block clusters
|
|
473
|
+
const tPend = Date.now();
|
|
409
474
|
const pendResult = await this.pendPhase(
|
|
410
475
|
transaction,
|
|
411
476
|
operationsHash,
|
|
412
477
|
collectionTransforms,
|
|
413
478
|
superclusterNominees
|
|
414
479
|
);
|
|
480
|
+
const pendMs = Date.now() - tPend;
|
|
415
481
|
if (!pendResult.success) {
|
|
482
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms (failed) total=%dms', trxId, gatherMs, pendMs, Date.now() - t0);
|
|
416
483
|
return pendResult;
|
|
417
484
|
}
|
|
418
485
|
|
|
419
486
|
// 3. COMMIT phase: commit to all critical blocks
|
|
487
|
+
const tCommit = Date.now();
|
|
420
488
|
const commitResult = await this.commitPhase(
|
|
421
489
|
transaction.id as ActionId,
|
|
422
490
|
criticalBlockIds,
|
|
423
491
|
pendResult.pendedBlockIds!
|
|
424
492
|
);
|
|
493
|
+
const commitMs = Date.now() - tCommit;
|
|
425
494
|
if (!commitResult.success) {
|
|
426
495
|
// Cancel pending actions on failure
|
|
427
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);
|
|
428
498
|
return commitResult;
|
|
429
499
|
}
|
|
430
500
|
|
|
431
501
|
// 4. PROPAGATE and CHECKPOINT phases are handled by clusters automatically
|
|
432
502
|
// (as per user's note: "managed by each cluster, the client doesn't have to worry about them")
|
|
433
503
|
|
|
504
|
+
log('trx:phases trxId=%s gather=%dms pend=%dms commit=%dms total=%dms', trxId, gatherMs, pendMs, commitMs, Date.now() - t0);
|
|
434
505
|
return { success: true };
|
|
435
506
|
}
|
|
436
507
|
|
|
@@ -520,6 +591,10 @@ export class TransactionCoordinator {
|
|
|
520
591
|
// Pend the transaction
|
|
521
592
|
const pendResult = await this.transactor.pend(pendRequest);
|
|
522
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
|
+
}
|
|
523
598
|
return {
|
|
524
599
|
success: false,
|
|
525
600
|
error: `Pend failed for collection ${collectionId}: ${pendResult.reason}`
|
package/src/transaction/index.ts
CHANGED
|
@@ -7,24 +7,25 @@ export type {
|
|
|
7
7
|
ExecutionResult,
|
|
8
8
|
CollectionActions,
|
|
9
9
|
ValidationResult,
|
|
10
|
-
ITransactionValidator
|
|
10
|
+
ITransactionValidator,
|
|
11
|
+
ActionsStatement
|
|
11
12
|
} from './transaction.js';
|
|
12
13
|
|
|
13
14
|
export {
|
|
14
15
|
createTransactionStamp,
|
|
15
|
-
createTransactionId
|
|
16
|
+
createTransactionId,
|
|
17
|
+
createActionsStatements,
|
|
18
|
+
DEFAULT_TRANSACTION_TTL_MS,
|
|
19
|
+
isTransactionExpired
|
|
16
20
|
} from './transaction.js';
|
|
17
21
|
|
|
18
22
|
export {
|
|
19
23
|
ActionsEngine,
|
|
20
24
|
ACTIONS_ENGINE_ID,
|
|
21
|
-
createActionsStatements
|
|
22
25
|
} from './actions-engine.js';
|
|
23
26
|
|
|
24
|
-
export type { ActionsStatement } from './actions-engine.js';
|
|
25
|
-
|
|
26
27
|
export { TransactionCoordinator } from './coordinator.js';
|
|
27
28
|
export { TransactionContext } from './context.js';
|
|
28
29
|
export { TransactionSession } from './session.js';
|
|
29
30
|
export { TransactionValidator } from './validator.js';
|
|
30
|
-
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
|
|
|
@@ -144,6 +157,22 @@ export type CollectionActions = {
|
|
|
144
157
|
actions: unknown[];
|
|
145
158
|
};
|
|
146
159
|
|
|
160
|
+
/**
|
|
161
|
+
* Helper to create an actions-based transaction statements array.
|
|
162
|
+
* Each CollectionActions becomes a separate JSON-encoded statement.
|
|
163
|
+
*/
|
|
164
|
+
export function createActionsStatements(collections: CollectionActions[]): string[] {
|
|
165
|
+
return collections.map(c => JSON.stringify(c));
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Statement format for the actions engine (array of CollectionActions).
|
|
170
|
+
* @deprecated Use CollectionActions[] directly
|
|
171
|
+
*/
|
|
172
|
+
export type ActionsStatement = {
|
|
173
|
+
collections: CollectionActions[];
|
|
174
|
+
};
|
|
175
|
+
|
|
147
176
|
/**
|
|
148
177
|
* Result of transaction validation.
|
|
149
178
|
*/
|
|
@@ -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
|
|