@optimystic/db-p2p 0.1.1 → 0.1.3

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 (64) hide show
  1. package/{readme.md → README.md} +7 -0
  2. package/dist/index.min.js +31 -30
  3. package/dist/index.min.js.map +4 -4
  4. package/dist/src/cluster/cluster-repo.d.ts +27 -0
  5. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  6. package/dist/src/cluster/cluster-repo.js +139 -18
  7. package/dist/src/cluster/cluster-repo.js.map +1 -1
  8. package/dist/src/cluster/service.d.ts +13 -2
  9. package/dist/src/cluster/service.d.ts.map +1 -1
  10. package/dist/src/cluster/service.js +17 -7
  11. package/dist/src/cluster/service.js.map +1 -1
  12. package/dist/src/index.d.ts +1 -1
  13. package/dist/src/index.d.ts.map +1 -1
  14. package/dist/src/index.js +1 -1
  15. package/dist/src/index.js.map +1 -1
  16. package/dist/src/libp2p-node.d.ts +13 -2
  17. package/dist/src/libp2p-node.d.ts.map +1 -1
  18. package/dist/src/libp2p-node.js +35 -16
  19. package/dist/src/libp2p-node.js.map +1 -1
  20. package/dist/src/protocol-client.d.ts.map +1 -1
  21. package/dist/src/protocol-client.js +8 -7
  22. package/dist/src/protocol-client.js.map +1 -1
  23. package/dist/src/repo/cluster-coordinator.d.ts +7 -2
  24. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  25. package/dist/src/repo/cluster-coordinator.js +18 -3
  26. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  27. package/dist/src/repo/coordinator-repo.d.ts +26 -3
  28. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  29. package/dist/src/repo/coordinator-repo.js +117 -22
  30. package/dist/src/repo/coordinator-repo.js.map +1 -1
  31. package/dist/src/repo/service.d.ts +13 -2
  32. package/dist/src/repo/service.d.ts.map +1 -1
  33. package/dist/src/repo/service.js +25 -12
  34. package/dist/src/repo/service.js.map +1 -1
  35. package/dist/src/storage/memory-storage.d.ts +15 -0
  36. package/dist/src/storage/memory-storage.d.ts.map +1 -1
  37. package/dist/src/storage/memory-storage.js +23 -4
  38. package/dist/src/storage/memory-storage.js.map +1 -1
  39. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  40. package/dist/src/storage/storage-repo.js.map +1 -1
  41. package/dist/src/sync/service.d.ts.map +1 -1
  42. package/dist/src/sync/service.js +7 -2
  43. package/dist/src/sync/service.js.map +1 -1
  44. package/package.json +27 -21
  45. package/src/cluster/cluster-repo.ts +836 -711
  46. package/src/cluster/service.ts +44 -31
  47. package/src/index.ts +1 -1
  48. package/src/libp2p-key-network.ts +334 -334
  49. package/src/libp2p-node.ts +371 -339
  50. package/src/network/network-manager-service.ts +334 -334
  51. package/src/protocol-client.ts +53 -54
  52. package/src/repo/client.ts +112 -112
  53. package/src/repo/cluster-coordinator.ts +613 -592
  54. package/src/repo/coordinator-repo.ts +269 -137
  55. package/src/repo/service.ts +237 -219
  56. package/src/storage/block-storage.ts +182 -182
  57. package/src/storage/memory-storage.ts +24 -5
  58. package/src/storage/storage-repo.ts +321 -320
  59. package/src/sync/service.ts +7 -6
  60. package/dist/src/storage/file-storage.d.ts +0 -30
  61. package/dist/src/storage/file-storage.d.ts.map +0 -1
  62. package/dist/src/storage/file-storage.js +0 -127
  63. package/dist/src/storage/file-storage.js.map +0 -1
  64. package/src/storage/file-storage.ts +0 -163
@@ -1,320 +1,321 @@
1
- import type {
2
- IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
3
- ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
4
- Transforms,
5
- GetBlockResult,
6
- PendValidationHook
7
- } from "@optimystic/db-core";
8
- import {
9
- Latches, transformForBlockId, applyTransform, groupBy, concatTransform, emptyTransforms,
10
- blockIdsForTransforms, transformsFromTransform
11
- } from "@optimystic/db-core";
12
- import { asyncIteratorToArray } from "../it-utility.js";
13
- import type { IBlockStorage } from "./i-block-storage.js";
14
-
15
- export type StorageRepoOptions = {
16
- /** Optional hook to validate transactions in PendRequests */
17
- validatePend?: PendValidationHook;
18
- };
19
-
20
- export class StorageRepo implements IRepo {
21
- private readonly validatePend?: PendValidationHook;
22
-
23
- constructor(
24
- private readonly createBlockStorage: (blockId: BlockId) => IBlockStorage,
25
- options?: StorageRepoOptions
26
- ) {
27
- this.validatePend = options?.validatePend;
28
- }
29
-
30
- async get({ blockIds, context }: BlockGets, options?: MessageOptions): Promise<GetBlockResults> {
31
- const distinctBlockIds = Array.from(new Set(blockIds));
32
- const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
33
- const blockStorage = this.createBlockStorage(blockId);
34
-
35
- // Ensure that all outstanding transactions in the context are committed
36
- if (context) {
37
- const latest = await blockStorage.getLatest();
38
- const missing = latest
39
- ? context.committed.filter(c => c.rev > latest.rev)
40
- : context.committed;
41
- for (const { actionId, rev } of missing.sort((a, b) => a.rev - b.rev)) {
42
- const pending = await blockStorage.getPendingTransaction(actionId);
43
- if (pending) {
44
- await this.internalCommit(blockId, actionId, rev, blockStorage);
45
- }
46
- }
47
- }
48
-
49
- const blockRev = await blockStorage.getBlock(context?.rev);
50
- if (!blockRev) {
51
- return [blockId, { state: {} } as GetBlockResult];
52
- }
53
-
54
- // Include pending action if requested
55
- if (context?.actionId !== undefined) {
56
- const pendingTransform = await blockStorage.getPendingTransaction(context.actionId);
57
- if (!pendingTransform) {
58
- throw new Error(`Pending action ${context.actionId} not found`);
59
- }
60
- const block = applyTransform(blockRev.block, pendingTransform);
61
- return [blockId, {
62
- block,
63
- state: {
64
- latest: await blockStorage.getLatest(),
65
- pendings: [context.actionId]
66
- }
67
- }];
68
- }
69
-
70
- const pendings = await asyncIteratorToArray(blockStorage.listPendingTransactions());
71
- return [blockId, {
72
- block: blockRev.block,
73
- state: {
74
- latest: await blockStorage.getLatest(),
75
- pendings
76
- }
77
- }];
78
- }));
79
- return Object.fromEntries(results);
80
- }
81
-
82
- async pend(request: PendRequest, _options?: MessageOptions): Promise<PendResult> {
83
- // Validate transaction if present and validation hook is configured
84
- if (this.validatePend && request.transaction && request.operationsHash) {
85
- const validationResult = await this.validatePend(request.transaction, request.operationsHash);
86
- if (!validationResult.valid) {
87
- return {
88
- success: false,
89
- reason: validationResult.reason ?? 'Transaction validation failed'
90
- };
91
- }
92
- }
93
-
94
- const blockIds = blockIdsForTransforms(request.transforms);
95
- const pendings: ActionPending[] = [];
96
- const missing: ActionTransforms[] = [];
97
-
98
- // Potential race condition: A concurrent commit operation could complete
99
- // between the conflict checks (latest.rev, listPendingTransactions) and the
100
- // savePendingTransaction call below. This pend operation might succeed based on
101
- // stale information, but the subsequent commit for this pend would likely
102
- // fail correctly later if a conflict arose. Locking here could make the initial
103
- // check more accurate but adds overhead. The current approach prioritizes
104
- // letting the commit be the final arbiter.
105
- for (const blockId of blockIds) {
106
- const blockStorage = this.createBlockStorage(blockId);
107
- const transforms = transformForBlockId(request.transforms, blockId);
108
-
109
- // First handle any pending actions
110
- const pending = await asyncIteratorToArray(blockStorage.listPendingTransactions());
111
- pendings.push(...pending.map(actionId => ({ blockId, actionId })));
112
-
113
- // Handle any conflicting revisions
114
- if (request.rev !== undefined || transforms.insert) {
115
- const latest = await blockStorage.getLatest();
116
- if (latest && latest.rev >= (request.rev ?? 0)) {
117
- const transforms = await asyncIteratorToArray(blockStorage.listRevisions(request.rev ?? 0, latest.rev));
118
- for (const actionRev of transforms) {
119
- const transform = await blockStorage.getTransaction(actionRev.actionId);
120
- if (!transform) {
121
- throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
122
- }
123
- missing.push({
124
- actionId: actionRev.actionId,
125
- rev: actionRev.rev,
126
- transforms: transformsFromTransform(transform, blockId)
127
- });
128
- }
129
- }
130
- }
131
- }
132
-
133
- if (missing.length) {
134
- return {
135
- success: false,
136
- missing
137
- };
138
- }
139
-
140
- if (pendings.length > 0) {
141
- if (request.policy === 'f') { // Fail on pending actions
142
- return { success: false, pending: pendings };
143
- } else if (request.policy === 'r') { // Return populated pending actions
144
- return {
145
- success: false,
146
- pending: await Promise.all(pendings.map(async action => {
147
- const blockStorage = this.createBlockStorage(action.blockId);
148
- return {
149
- blockId: action.blockId,
150
- actionId: action.actionId,
151
- transform: (await blockStorage.getPendingTransaction(action.actionId))
152
- ?? (await blockStorage.getTransaction(action.actionId))! // Possible that since enumeration, the action has been promoted
153
- }
154
- }))
155
- };
156
- }
157
- }
158
-
159
-
160
- // Simultaneously save pending action for each block
161
- // Note: that this is not atomic, after we checked for conflicts and pending actions
162
- // new pending or committed actions may have been added. This is okay, because
163
- // this check during pend is conservative.
164
- await Promise.all(blockIds.map(blockId => {
165
- const blockStorage = this.createBlockStorage(blockId);
166
- const blockTransform = transformForBlockId(request.transforms, blockId);
167
- return blockStorage.savePendingTransaction(request.actionId, blockTransform);
168
- }));
169
-
170
- return {
171
- success: true,
172
- pending: pendings,
173
- blockIds
174
- } as PendSuccess;
175
- }
176
-
177
- async cancel(actionRef: ActionBlocks, _options?: MessageOptions): Promise<void> {
178
- await Promise.all(actionRef.blockIds.map(blockId => {
179
- const blockStorage = this.createBlockStorage(blockId);
180
- return blockStorage.deletePendingTransaction(actionRef.actionId);
181
- }));
182
- }
183
-
184
- async commit(request: CommitRequest, options?: MessageOptions): Promise<CommitResult> {
185
- const uniqueBlockIds = Array.from(new Set(request.blockIds)).sort();
186
- const releases: (() => void)[] = [];
187
-
188
- try {
189
- // Acquire locks sequentially based on sorted IDs to prevent deadlocks
190
- for (const id of uniqueBlockIds) {
191
- const lockId = `StorageRepo.commit:${id}`;
192
- const release = await Latches.acquire(lockId);
193
- releases.push(release);
194
- }
195
-
196
- // --- Start of Critical Section ---
197
-
198
- const blockStorages = request.blockIds.map(blockId => ({
199
- blockId,
200
- storage: this.createBlockStorage(blockId)
201
- }));
202
-
203
- // Check for stale revisions and collect missing actions
204
- const missedCommits: { blockId: BlockId, transforms: ActionTransform[] }[] = [];
205
- for (const { blockId, storage } of blockStorages) {
206
- const latest = await storage.getLatest();
207
- if (latest && latest.rev >= request.rev) {
208
- const transforms: ActionTransform[] = [];
209
- for await (const actionRev of storage.listRevisions(request.rev, latest.rev)) {
210
- const transform = await storage.getTransaction(actionRev.actionId);
211
- if (!transform) {
212
- throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
213
- }
214
- transforms.push({
215
- actionId: actionRev.actionId,
216
- rev: actionRev.rev,
217
- transform
218
- });
219
- }
220
- missedCommits.push({ blockId, transforms }); // Push, even if transforms is empty, because we want to reject the older version
221
- }
222
- }
223
-
224
- if (missedCommits.length) {
225
- return { // Return directly, locks will be released in finally
226
- success: false,
227
- missing: perBlockActionTransformsToPerAction(missedCommits)
228
- };
229
- }
230
-
231
- // Check for missing pending actions
232
- const missingPends: { blockId: BlockId, actionId: ActionId }[] = [];
233
- for (const { blockId, storage } of blockStorages) {
234
- const pendingAction = await storage.getPendingTransaction(request.actionId);
235
- if (!pendingAction) {
236
- missingPends.push({ blockId, actionId: request.actionId });
237
- }
238
- }
239
-
240
- if (missingPends.length) {
241
- throw new Error(`Pending action ${request.actionId} not found for block(s): ${missingPends.map(p => p.blockId).join(', ')}`);
242
- }
243
-
244
- // Commit the action for each block
245
- // This loop will execute atomically for all blocks due to the acquired locks
246
- for (const { blockId, storage } of blockStorages) {
247
- try {
248
- // internalCommit will throw if it encounters an issue
249
- await this.internalCommit(blockId, request.actionId, request.rev, storage);
250
- } catch (err) {
251
- // TODO: Recover as best we can. Rollback or handle partial commit? For now, return failure.
252
- return {
253
- success: false,
254
- reason: err instanceof Error ? err.message : 'Unknown error during commit'
255
- };
256
- }
257
- }
258
- }
259
- finally {
260
- // Release locks in reverse order of acquisition
261
- releases.reverse().forEach(release => release());
262
- }
263
-
264
- return { success: true };
265
- }
266
-
267
- private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage): Promise<void> {
268
- // Note: This method is called within the locked critical section of commit()
269
- // So, operations like getPendingTransaction, getLatest, getBlock, saveMaterializedBlock,
270
- // saveRevision, promotePendingTransaction, setLatest are protected against
271
- // concurrent commits for the *same blockId*.
272
-
273
- const transform = await storage.getPendingTransaction(actionId);
274
- // No need to check if !transform here, as the caller (commit) already verified this.
275
- // If it's null here, it indicates a logic error or race condition bypassed the lock (unlikely).
276
- if (!transform) {
277
- throw new Error(`Consistency Error: Pending action ${actionId} disappeared for block ${blockId} within critical section.`);
278
- }
279
-
280
- // Get prior materialized block if it exists
281
- const latest = await storage.getLatest();
282
- const priorBlock = latest
283
- ? (await storage.getBlock(latest.rev))?.block
284
- : undefined;
285
-
286
- // Apply transform and save materialized block
287
- // applyTransform handles undefined priorBlock correctly for inserts
288
- const newBlock = applyTransform(priorBlock, transform);
289
- if (newBlock) {
290
- await storage.saveMaterializedBlock(actionId, newBlock);
291
- }
292
-
293
- // Save revision and promote action *before* updating latest
294
- // This ensures that if the process crashes between these steps,
295
- // the 'latest' pointer doesn't point to a revision that hasn't been fully recorded.
296
- await storage.saveRevision(rev, actionId);
297
- await storage.promotePendingTransaction(actionId);
298
-
299
- // Update latest revision *last*
300
- await storage.setLatest({ actionId, rev });
301
- }
302
- }
303
-
304
- /** Converts list of missing actions per block into a list of missing actions across blocks. */
305
- function perBlockActionTransformsToPerAction(missing: { blockId: BlockId; transforms: ActionTransform[]; }[]) {
306
- const missingFlat = missing.flatMap(({ blockId, transforms }) =>
307
- transforms.map(transform => ({ blockId, transform }))
308
- );
309
- const missingByActionId = groupBy(missingFlat, ({ transform }) => transform.actionId);
310
- return Object.entries(missingByActionId).map(([actionId, items]) =>
311
- items.reduce((acc, { blockId, transform }) => {
312
- concatTransform(acc.transforms, blockId, transform.transform);
313
- return acc;
314
- }, {
315
- actionId: actionId as ActionId,
316
- rev: items[0]!.transform.rev, // Assumption: all missing actionIds share the same revision
317
- transforms: emptyTransforms()
318
- })
319
- );
320
- }
1
+ import type {
2
+ IRepo, MessageOptions, BlockId, CommitRequest, CommitResult, GetBlockResults, PendRequest, PendResult, ActionBlocks,
3
+ ActionId, BlockGets, ActionPending, PendSuccess, ActionTransform, ActionTransforms,
4
+ Transforms,
5
+ GetBlockResult,
6
+ PendValidationHook
7
+ } from "@optimystic/db-core";
8
+ import {
9
+ Latches, transformForBlockId, applyTransform, groupBy, concatTransform, emptyTransforms,
10
+ blockIdsForTransforms, transformsFromTransform
11
+ } from "@optimystic/db-core";
12
+ import { asyncIteratorToArray } from "../it-utility.js";
13
+ import type { IBlockStorage } from "./i-block-storage.js";
14
+
15
+ export type StorageRepoOptions = {
16
+ /** Optional hook to validate transactions in PendRequests */
17
+ validatePend?: PendValidationHook;
18
+ };
19
+
20
+ export class StorageRepo implements IRepo {
21
+ private readonly validatePend?: PendValidationHook;
22
+
23
+ constructor(
24
+ private readonly createBlockStorage: (blockId: BlockId) => IBlockStorage,
25
+ options?: StorageRepoOptions
26
+ ) {
27
+ this.validatePend = options?.validatePend;
28
+ }
29
+
30
+ async get({ blockIds, context }: BlockGets, options?: MessageOptions): Promise<GetBlockResults> {
31
+ const distinctBlockIds = Array.from(new Set(blockIds));
32
+ const results = await Promise.all(distinctBlockIds.map(async (blockId) => {
33
+ const blockStorage = this.createBlockStorage(blockId);
34
+
35
+ // Ensure that all outstanding transactions in the context are committed
36
+ if (context) {
37
+ const latest = await blockStorage.getLatest();
38
+ const missing = latest
39
+ ? context.committed.filter(c => c.rev > latest.rev)
40
+ : context.committed;
41
+ for (const { actionId, rev } of missing.sort((a, b) => a.rev - b.rev)) {
42
+ const pending = await blockStorage.getPendingTransaction(actionId);
43
+ if (pending) {
44
+ await this.internalCommit(blockId, actionId, rev, blockStorage);
45
+ }
46
+ }
47
+ }
48
+
49
+ const blockRev = await blockStorage.getBlock(context?.rev);
50
+ if (!blockRev) {
51
+ return [blockId, { state: {} } as GetBlockResult];
52
+ }
53
+
54
+ // Include pending action if requested
55
+ if (context?.actionId !== undefined) {
56
+ const pendingTransform = await blockStorage.getPendingTransaction(context.actionId);
57
+ if (!pendingTransform) {
58
+ throw new Error(`Pending action ${context.actionId} not found`);
59
+ }
60
+ const block = applyTransform(blockRev.block, pendingTransform);
61
+ return [blockId, {
62
+ block,
63
+ state: {
64
+ latest: await blockStorage.getLatest(),
65
+ pendings: [context.actionId]
66
+ }
67
+ }];
68
+ }
69
+
70
+ const pendings = await asyncIteratorToArray(blockStorage.listPendingTransactions());
71
+ return [blockId, {
72
+ block: blockRev.block,
73
+ state: {
74
+ latest: await blockStorage.getLatest(),
75
+ pendings
76
+ }
77
+ }];
78
+ }));
79
+ return Object.fromEntries(results);
80
+ }
81
+
82
+ async pend(request: PendRequest, _options?: MessageOptions): Promise<PendResult> {
83
+ // Validate transaction if present and validation hook is configured
84
+ if (this.validatePend && request.transaction && request.operationsHash) {
85
+ const validationResult = await this.validatePend(request.transaction, request.operationsHash);
86
+ if (!validationResult.valid) {
87
+ return {
88
+ success: false,
89
+ reason: validationResult.reason ?? 'Transaction validation failed'
90
+ };
91
+ }
92
+ }
93
+
94
+ const blockIds = blockIdsForTransforms(request.transforms);
95
+ const pendings: ActionPending[] = [];
96
+ const missing: ActionTransforms[] = [];
97
+
98
+ // Potential race condition: A concurrent commit operation could complete
99
+ // between the conflict checks (latest.rev, listPendingTransactions) and the
100
+ // savePendingTransaction call below. This pend operation might succeed based on
101
+ // stale information, but the subsequent commit for this pend would likely
102
+ // fail correctly later if a conflict arose. Locking here could make the initial
103
+ // check more accurate but adds overhead. The current approach prioritizes
104
+ // letting the commit be the final arbiter.
105
+ for (const blockId of blockIds) {
106
+ const blockStorage = this.createBlockStorage(blockId);
107
+ const transforms = transformForBlockId(request.transforms, blockId);
108
+
109
+ // First handle any pending actions
110
+ const pending = await asyncIteratorToArray(blockStorage.listPendingTransactions());
111
+ pendings.push(...pending.map(actionId => ({ blockId, actionId })));
112
+
113
+ // Handle any conflicting revisions
114
+ if (request.rev !== undefined || transforms.insert) {
115
+ const latest = await blockStorage.getLatest();
116
+ if (latest && latest.rev >= (request.rev ?? 0)) {
117
+ const transforms = await asyncIteratorToArray(blockStorage.listRevisions(request.rev ?? 0, latest.rev));
118
+ for (const actionRev of transforms) {
119
+ const transform = await blockStorage.getTransaction(actionRev.actionId);
120
+ if (!transform) {
121
+ throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
122
+ }
123
+ missing.push({
124
+ actionId: actionRev.actionId,
125
+ rev: actionRev.rev,
126
+ transforms: transformsFromTransform(transform, blockId)
127
+ });
128
+ }
129
+ }
130
+ }
131
+ }
132
+
133
+ if (missing.length) {
134
+ return {
135
+ success: false,
136
+ missing
137
+ };
138
+ }
139
+
140
+ if (pendings.length > 0) {
141
+ if (request.policy === 'f') { // Fail on pending actions
142
+ return { success: false, pending: pendings };
143
+ } else if (request.policy === 'r') { // Return populated pending actions
144
+ return {
145
+ success: false,
146
+ pending: await Promise.all(pendings.map(async action => {
147
+ const blockStorage = this.createBlockStorage(action.blockId);
148
+ return {
149
+ blockId: action.blockId,
150
+ actionId: action.actionId,
151
+ transform: (await blockStorage.getPendingTransaction(action.actionId))
152
+ ?? (await blockStorage.getTransaction(action.actionId))! // Possible that since enumeration, the action has been promoted
153
+ }
154
+ }))
155
+ };
156
+ }
157
+ }
158
+
159
+
160
+ // Simultaneously save pending action for each block
161
+ // Note: that this is not atomic, after we checked for conflicts and pending actions
162
+ // new pending or committed actions may have been added. This is okay, because
163
+ // this check during pend is conservative.
164
+ await Promise.all(blockIds.map(blockId => {
165
+ const blockStorage = this.createBlockStorage(blockId);
166
+ const blockTransform = transformForBlockId(request.transforms, blockId);
167
+ return blockStorage.savePendingTransaction(request.actionId, blockTransform);
168
+ }));
169
+
170
+ return {
171
+ success: true,
172
+ pending: pendings,
173
+ blockIds
174
+ } as PendSuccess;
175
+ }
176
+
177
+ async cancel(actionRef: ActionBlocks, _options?: MessageOptions): Promise<void> {
178
+ await Promise.all(actionRef.blockIds.map(blockId => {
179
+ const blockStorage = this.createBlockStorage(blockId);
180
+ return blockStorage.deletePendingTransaction(actionRef.actionId);
181
+ }));
182
+ }
183
+
184
+ async commit(request: CommitRequest, options?: MessageOptions): Promise<CommitResult> {
185
+ const uniqueBlockIds = Array.from(new Set(request.blockIds)).sort();
186
+ const releases: (() => void)[] = [];
187
+
188
+ try {
189
+ // Acquire locks sequentially based on sorted IDs to prevent deadlocks
190
+ for (const id of uniqueBlockIds) {
191
+ const lockId = `StorageRepo.commit:${id}`;
192
+ const release = await Latches.acquire(lockId);
193
+ releases.push(release);
194
+ }
195
+
196
+ // --- Start of Critical Section ---
197
+
198
+ const blockStorages = request.blockIds.map(blockId => ({
199
+ blockId,
200
+ storage: this.createBlockStorage(blockId)
201
+ }));
202
+
203
+ // Check for stale revisions and collect missing actions
204
+ const missedCommits: { blockId: BlockId, transforms: ActionTransform[] }[] = [];
205
+ for (const { blockId, storage } of blockStorages) {
206
+ const latest = await storage.getLatest();
207
+ if (latest && latest.rev >= request.rev) {
208
+ const transforms: ActionTransform[] = [];
209
+ for await (const actionRev of storage.listRevisions(request.rev, latest.rev)) {
210
+ const transform = await storage.getTransaction(actionRev.actionId);
211
+ if (!transform) {
212
+ throw new Error(`Missing action ${actionRev.actionId} for block ${blockId}`);
213
+ }
214
+ transforms.push({
215
+ actionId: actionRev.actionId,
216
+ rev: actionRev.rev,
217
+ transform
218
+ });
219
+ }
220
+ missedCommits.push({ blockId, transforms }); // Push, even if transforms is empty, because we want to reject the older version
221
+ }
222
+ }
223
+
224
+ if (missedCommits.length) {
225
+ return { // Return directly, locks will be released in finally
226
+ success: false,
227
+ missing: perBlockActionTransformsToPerAction(missedCommits)
228
+ };
229
+ }
230
+
231
+ // Check for missing pending actions
232
+ const missingPends: { blockId: BlockId, actionId: ActionId }[] = [];
233
+ for (const { blockId, storage } of blockStorages) {
234
+ const pendingAction = await storage.getPendingTransaction(request.actionId);
235
+ if (!pendingAction) {
236
+ missingPends.push({ blockId, actionId: request.actionId });
237
+ }
238
+ }
239
+
240
+ if (missingPends.length) {
241
+ throw new Error(`Pending action ${request.actionId} not found for block(s): ${missingPends.map(p => p.blockId).join(', ')}`);
242
+ }
243
+
244
+ // Commit the action for each block
245
+ // This loop will execute atomically for all blocks due to the acquired locks
246
+ for (const { blockId, storage } of blockStorages) {
247
+ try {
248
+ // internalCommit will throw if it encounters an issue
249
+ await this.internalCommit(blockId, request.actionId, request.rev, storage);
250
+ } catch (err) {
251
+ // TODO: Recover as best we can. Rollback or handle partial commit? For now, return failure.
252
+ return {
253
+ success: false,
254
+ reason: err instanceof Error ? err.message : 'Unknown error during commit'
255
+ };
256
+ }
257
+ }
258
+ }
259
+ finally {
260
+ // Release locks in reverse order of acquisition
261
+ releases.reverse().forEach(release => release());
262
+ }
263
+
264
+ return { success: true };
265
+ }
266
+
267
+ private async internalCommit(blockId: BlockId, actionId: ActionId, rev: number, storage: IBlockStorage): Promise<void> {
268
+ // Note: This method is called within the locked critical section of commit()
269
+ // So, operations like getPendingTransaction, getLatest, getBlock, saveMaterializedBlock,
270
+ // saveRevision, promotePendingTransaction, setLatest are protected against
271
+ // concurrent commits for the *same blockId*.
272
+
273
+ const transform = await storage.getPendingTransaction(actionId);
274
+ // No need to check if !transform here, as the caller (commit) already verified this.
275
+ // If it's null here, it indicates a logic error or race condition bypassed the lock (unlikely).
276
+ if (!transform) {
277
+ throw new Error(`Consistency Error: Pending action ${actionId} disappeared for block ${blockId} within critical section.`);
278
+ }
279
+
280
+ // Get prior materialized block if it exists
281
+ const latest = await storage.getLatest();
282
+ const priorBlock = latest
283
+ ? (await storage.getBlock(latest.rev))?.block
284
+ : undefined;
285
+
286
+ // Apply transform and save materialized block
287
+ // applyTransform handles undefined priorBlock correctly for inserts
288
+ const newBlock = applyTransform(priorBlock, transform);
289
+
290
+ if (newBlock) {
291
+ await storage.saveMaterializedBlock(actionId, newBlock);
292
+ }
293
+
294
+ // Save revision and promote action *before* updating latest
295
+ // This ensures that if the process crashes between these steps,
296
+ // the 'latest' pointer doesn't point to a revision that hasn't been fully recorded.
297
+ await storage.saveRevision(rev, actionId);
298
+ await storage.promotePendingTransaction(actionId);
299
+
300
+ // Update latest revision *last*
301
+ await storage.setLatest({ actionId, rev });
302
+ }
303
+ }
304
+
305
+ /** Converts list of missing actions per block into a list of missing actions across blocks. */
306
+ function perBlockActionTransformsToPerAction(missing: { blockId: BlockId; transforms: ActionTransform[]; }[]) {
307
+ const missingFlat = missing.flatMap(({ blockId, transforms }) =>
308
+ transforms.map(transform => ({ blockId, transform }))
309
+ );
310
+ const missingByActionId = groupBy(missingFlat, ({ transform }) => transform.actionId);
311
+ return Object.entries(missingByActionId).map(([actionId, items]) =>
312
+ items.reduce((acc, { blockId, transform }) => {
313
+ concatTransform(acc.transforms, blockId, transform.transform);
314
+ return acc;
315
+ }, {
316
+ actionId: actionId as ActionId,
317
+ rev: items[0]!.transform.rev, // Assumption: all missing actionIds share the same revision
318
+ transforms: emptyTransforms()
319
+ })
320
+ );
321
+ }