@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.
Files changed (92) hide show
  1. package/dist/src/btree/btree.d.ts +2 -0
  2. package/dist/src/btree/btree.d.ts.map +1 -1
  3. package/dist/src/btree/btree.js +72 -52
  4. package/dist/src/btree/btree.js.map +1 -1
  5. package/dist/src/cluster/structs.d.ts +13 -0
  6. package/dist/src/cluster/structs.d.ts.map +1 -1
  7. package/dist/src/collection/collection.d.ts +3 -0
  8. package/dist/src/collection/collection.d.ts.map +1 -1
  9. package/dist/src/collection/collection.js +6 -0
  10. package/dist/src/collection/collection.js.map +1 -1
  11. package/dist/src/index.d.ts +1 -0
  12. package/dist/src/index.d.ts.map +1 -1
  13. package/dist/src/index.js +1 -0
  14. package/dist/src/index.js.map +1 -1
  15. package/dist/src/log/log.js +1 -1
  16. package/dist/src/log/log.js.map +1 -1
  17. package/dist/src/logger.d.ts +4 -0
  18. package/dist/src/logger.d.ts.map +1 -0
  19. package/dist/src/logger.js +8 -0
  20. package/dist/src/logger.js.map +1 -0
  21. package/dist/src/transaction/coordinator.d.ts +9 -1
  22. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  23. package/dist/src/transaction/coordinator.js +76 -8
  24. package/dist/src/transaction/coordinator.js.map +1 -1
  25. package/dist/src/transaction/index.d.ts +2 -2
  26. package/dist/src/transaction/index.d.ts.map +1 -1
  27. package/dist/src/transaction/index.js +1 -1
  28. package/dist/src/transaction/index.js.map +1 -1
  29. package/dist/src/transaction/session.d.ts +7 -3
  30. package/dist/src/transaction/session.d.ts.map +1 -1
  31. package/dist/src/transaction/session.js +23 -10
  32. package/dist/src/transaction/session.js.map +1 -1
  33. package/dist/src/transaction/transaction.d.ts +9 -3
  34. package/dist/src/transaction/transaction.d.ts.map +1 -1
  35. package/dist/src/transaction/transaction.js +14 -7
  36. package/dist/src/transaction/transaction.js.map +1 -1
  37. package/dist/src/transaction/validator.d.ts +9 -2
  38. package/dist/src/transaction/validator.d.ts.map +1 -1
  39. package/dist/src/transaction/validator.js +26 -6
  40. package/dist/src/transaction/validator.js.map +1 -1
  41. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  42. package/dist/src/transactor/network-transactor.js +84 -9
  43. package/dist/src/transactor/network-transactor.js.map +1 -1
  44. package/dist/src/transactor/transactor-source.d.ts +4 -0
  45. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  46. package/dist/src/transactor/transactor-source.js +25 -9
  47. package/dist/src/transactor/transactor-source.js.map +1 -1
  48. package/dist/src/transform/atomic-proxy.d.ts +26 -0
  49. package/dist/src/transform/atomic-proxy.d.ts.map +1 -0
  50. package/dist/src/transform/atomic-proxy.js +47 -0
  51. package/dist/src/transform/atomic-proxy.js.map +1 -0
  52. package/dist/src/transform/cache-source.d.ts +3 -2
  53. package/dist/src/transform/cache-source.d.ts.map +1 -1
  54. package/dist/src/transform/cache-source.js +15 -3
  55. package/dist/src/transform/cache-source.js.map +1 -1
  56. package/dist/src/transform/index.d.ts +1 -0
  57. package/dist/src/transform/index.d.ts.map +1 -1
  58. package/dist/src/transform/index.js +1 -0
  59. package/dist/src/transform/index.js.map +1 -1
  60. package/dist/src/utility/batch-coordinator.d.ts.map +1 -1
  61. package/dist/src/utility/batch-coordinator.js +6 -1
  62. package/dist/src/utility/batch-coordinator.js.map +1 -1
  63. package/dist/src/utility/hash-string.d.ts +3 -6
  64. package/dist/src/utility/hash-string.d.ts.map +1 -1
  65. package/dist/src/utility/hash-string.js +8 -11
  66. package/dist/src/utility/hash-string.js.map +1 -1
  67. package/dist/src/utility/lru-map.d.ts +18 -0
  68. package/dist/src/utility/lru-map.d.ts.map +1 -0
  69. package/dist/src/utility/lru-map.js +52 -0
  70. package/dist/src/utility/lru-map.js.map +1 -0
  71. package/package.json +15 -8
  72. package/src/btree/btree.ts +71 -50
  73. package/src/cluster/structs.ts +11 -0
  74. package/src/collection/collection.ts +9 -0
  75. package/src/index.ts +1 -0
  76. package/src/log/log.ts +1 -1
  77. package/src/logger.ts +10 -0
  78. package/src/transaction/coordinator.ts +86 -9
  79. package/src/transaction/index.ts +4 -2
  80. package/src/transaction/session.ts +34 -10
  81. package/src/transaction/transaction.ts +23 -10
  82. package/src/transaction/validator.ts +34 -7
  83. package/src/transactor/network-transactor.ts +92 -11
  84. package/src/transactor/transactor-source.ts +28 -9
  85. package/src/transform/atomic-proxy.ts +49 -0
  86. package/src/transform/cache-source.ts +18 -4
  87. package/src/transform/index.ts +1 -0
  88. package/src/utility/batch-coordinator.ts +7 -1
  89. package/src/utility/hash-string.ts +14 -17
  90. package/src/utility/lru-map.ts +55 -0
  91. package/dist/index.min.js +0 -9
  92. 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
- // 4. Return results from actions
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}`
@@ -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 = new TransactionSession(coordinator, engine);
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
- peerId: string = 'local', // TODO: Get from coordinator or config
34
- schemaHash: string = '' // TODO: Get from engine
33
+ stamp: TransactionStamp
35
34
  ) {
36
- // Create stamp at BEGIN (stable throughout transaction)
37
- this.stamp = createTransactionStamp(
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: [], // TODO: Track reads during statement execution
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
- ): TransactionStamp {
80
- const stampData = JSON.stringify({ peerId, timestamp, schemaHash, engineId });
81
- const id = `stamp:${hashString(stampData)}`;
82
- return { peerId, timestamp, schemaHash, engineId, id };
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
- // TODO: Implement read dependency validation
74
- // For now, we skip this check - will be implemented with proper block versioning
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