@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.
Files changed (97) 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/actions-engine.d.ts +1 -13
  22. package/dist/src/transaction/actions-engine.d.ts.map +1 -1
  23. package/dist/src/transaction/actions-engine.js +0 -7
  24. package/dist/src/transaction/actions-engine.js.map +1 -1
  25. package/dist/src/transaction/coordinator.d.ts +9 -1
  26. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  27. package/dist/src/transaction/coordinator.js +77 -11
  28. package/dist/src/transaction/coordinator.js.map +1 -1
  29. package/dist/src/transaction/index.d.ts +4 -5
  30. package/dist/src/transaction/index.d.ts.map +1 -1
  31. package/dist/src/transaction/index.js +2 -2
  32. package/dist/src/transaction/index.js.map +1 -1
  33. package/dist/src/transaction/session.d.ts +7 -3
  34. package/dist/src/transaction/session.d.ts.map +1 -1
  35. package/dist/src/transaction/session.js +23 -10
  36. package/dist/src/transaction/session.js.map +1 -1
  37. package/dist/src/transaction/transaction.d.ts +21 -3
  38. package/dist/src/transaction/transaction.d.ts.map +1 -1
  39. package/dist/src/transaction/transaction.js +21 -7
  40. package/dist/src/transaction/transaction.js.map +1 -1
  41. package/dist/src/transaction/validator.d.ts +9 -2
  42. package/dist/src/transaction/validator.d.ts.map +1 -1
  43. package/dist/src/transaction/validator.js +26 -6
  44. package/dist/src/transaction/validator.js.map +1 -1
  45. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  46. package/dist/src/transactor/network-transactor.js +84 -9
  47. package/dist/src/transactor/network-transactor.js.map +1 -1
  48. package/dist/src/transactor/transactor-source.d.ts +4 -0
  49. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  50. package/dist/src/transactor/transactor-source.js +25 -9
  51. package/dist/src/transactor/transactor-source.js.map +1 -1
  52. package/dist/src/transform/atomic-proxy.d.ts +26 -0
  53. package/dist/src/transform/atomic-proxy.d.ts.map +1 -0
  54. package/dist/src/transform/atomic-proxy.js +47 -0
  55. package/dist/src/transform/atomic-proxy.js.map +1 -0
  56. package/dist/src/transform/cache-source.d.ts +3 -2
  57. package/dist/src/transform/cache-source.d.ts.map +1 -1
  58. package/dist/src/transform/cache-source.js +15 -3
  59. package/dist/src/transform/cache-source.js.map +1 -1
  60. package/dist/src/transform/index.d.ts +1 -0
  61. package/dist/src/transform/index.d.ts.map +1 -1
  62. package/dist/src/transform/index.js +1 -0
  63. package/dist/src/transform/index.js.map +1 -1
  64. package/dist/src/utility/batch-coordinator.d.ts.map +1 -1
  65. package/dist/src/utility/batch-coordinator.js +6 -1
  66. package/dist/src/utility/batch-coordinator.js.map +1 -1
  67. package/dist/src/utility/hash-string.d.ts +3 -6
  68. package/dist/src/utility/hash-string.d.ts.map +1 -1
  69. package/dist/src/utility/hash-string.js +8 -11
  70. package/dist/src/utility/hash-string.js.map +1 -1
  71. package/dist/src/utility/lru-map.d.ts +18 -0
  72. package/dist/src/utility/lru-map.d.ts.map +1 -0
  73. package/dist/src/utility/lru-map.js +52 -0
  74. package/dist/src/utility/lru-map.js.map +1 -0
  75. package/package.json +15 -8
  76. package/src/btree/btree.ts +71 -50
  77. package/src/cluster/structs.ts +11 -0
  78. package/src/collection/collection.ts +9 -0
  79. package/src/index.ts +1 -0
  80. package/src/log/log.ts +1 -1
  81. package/src/logger.ts +10 -0
  82. package/src/transaction/actions-engine.ts +0 -17
  83. package/src/transaction/coordinator.ts +87 -12
  84. package/src/transaction/index.ts +7 -6
  85. package/src/transaction/session.ts +34 -10
  86. package/src/transaction/transaction.ts +39 -10
  87. package/src/transaction/validator.ts +34 -7
  88. package/src/transactor/network-transactor.ts +92 -11
  89. package/src/transactor/transactor-source.ts +28 -9
  90. package/src/transform/atomic-proxy.ts +49 -0
  91. package/src/transform/cache-source.ts +18 -4
  92. package/src/transform/index.ts +1 -0
  93. package/src/utility/batch-coordinator.ts +7 -1
  94. package/src/utility/hash-string.ts +14 -17
  95. package/src/utility/lru-map.ts +55 -0
  96. package/dist/index.min.js +0 -9
  97. 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 { createActionsStatements } from "./actions-engine.js";
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
- // 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);
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}`
@@ -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 = 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
 
@@ -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
- // 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