@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.
- package/{readme.md → README.md} +7 -0
- package/dist/index.min.js +31 -30
- package/dist/index.min.js.map +4 -4
- package/dist/src/cluster/cluster-repo.d.ts +27 -0
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +139 -18
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -2
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -7
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-node.d.ts +13 -2
- package/dist/src/libp2p-node.d.ts.map +1 -1
- package/dist/src/libp2p-node.js +35 -16
- package/dist/src/libp2p-node.js.map +1 -1
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +8 -7
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +7 -2
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +18 -3
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +26 -3
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +117 -22
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +13 -2
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +25 -12
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/storage/memory-storage.d.ts +15 -0
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +23 -4
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -2
- package/dist/src/sync/service.js.map +1 -1
- package/package.json +27 -21
- package/src/cluster/cluster-repo.ts +836 -711
- package/src/cluster/service.ts +44 -31
- package/src/index.ts +1 -1
- package/src/libp2p-key-network.ts +334 -334
- package/src/libp2p-node.ts +371 -339
- package/src/network/network-manager-service.ts +334 -334
- package/src/protocol-client.ts +53 -54
- package/src/repo/client.ts +112 -112
- package/src/repo/cluster-coordinator.ts +613 -592
- package/src/repo/coordinator-repo.ts +269 -137
- package/src/repo/service.ts +237 -219
- package/src/storage/block-storage.ts +182 -182
- package/src/storage/memory-storage.ts +24 -5
- package/src/storage/storage-repo.ts +321 -320
- package/src/sync/service.ts +7 -6
- package/dist/src/storage/file-storage.d.ts +0 -30
- package/dist/src/storage/file-storage.d.ts.map +0 -1
- package/dist/src/storage/file-storage.js +0 -127
- package/dist/src/storage/file-storage.js.map +0 -1
- 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
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
|
|
297
|
-
await storage.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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
|
+
}
|