@optimystic/db-core 0.5.1 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/btree/btree.d.ts +2 -0
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +72 -52
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +13 -0
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/collection/collection.d.ts +3 -0
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +6 -0
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.js +1 -1
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/logger.d.ts +4 -0
- package/dist/src/logger.d.ts.map +1 -0
- package/dist/src/logger.js +8 -0
- package/dist/src/logger.js.map +1 -0
- package/dist/src/transaction/actions-engine.d.ts +1 -13
- package/dist/src/transaction/actions-engine.d.ts.map +1 -1
- package/dist/src/transaction/actions-engine.js +0 -7
- package/dist/src/transaction/actions-engine.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +9 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +77 -11
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/index.d.ts +4 -5
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +2 -2
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/session.d.ts +7 -3
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +23 -10
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +21 -3
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +21 -7
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts +9 -2
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +26 -6
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +84 -9
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +4 -0
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -9
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic-proxy.d.ts +26 -0
- package/dist/src/transform/atomic-proxy.d.ts.map +1 -0
- package/dist/src/transform/atomic-proxy.js +47 -0
- package/dist/src/transform/atomic-proxy.js.map +1 -0
- package/dist/src/transform/cache-source.d.ts +3 -2
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +15 -3
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/index.d.ts +1 -0
- package/dist/src/transform/index.d.ts.map +1 -1
- package/dist/src/transform/index.js +1 -0
- package/dist/src/transform/index.js.map +1 -1
- package/dist/src/utility/batch-coordinator.d.ts.map +1 -1
- package/dist/src/utility/batch-coordinator.js +6 -1
- package/dist/src/utility/batch-coordinator.js.map +1 -1
- package/dist/src/utility/hash-string.d.ts +3 -6
- package/dist/src/utility/hash-string.d.ts.map +1 -1
- package/dist/src/utility/hash-string.js +8 -11
- package/dist/src/utility/hash-string.js.map +1 -1
- package/dist/src/utility/lru-map.d.ts +18 -0
- package/dist/src/utility/lru-map.d.ts.map +1 -0
- package/dist/src/utility/lru-map.js +52 -0
- package/dist/src/utility/lru-map.js.map +1 -0
- package/package.json +15 -8
- package/src/btree/btree.ts +71 -50
- package/src/cluster/structs.ts +11 -0
- package/src/collection/collection.ts +9 -0
- package/src/index.ts +1 -0
- package/src/log/log.ts +1 -1
- package/src/logger.ts +10 -0
- package/src/transaction/actions-engine.ts +0 -17
- package/src/transaction/coordinator.ts +87 -12
- package/src/transaction/index.ts +7 -6
- package/src/transaction/session.ts +34 -10
- package/src/transaction/transaction.ts +39 -10
- package/src/transaction/validator.ts +34 -7
- package/src/transactor/network-transactor.ts +92 -11
- package/src/transactor/transactor-source.ts +28 -9
- package/src/transform/atomic-proxy.ts +49 -0
- package/src/transform/cache-source.ts +18 -4
- package/src/transform/index.ts +1 -0
- package/src/utility/batch-coordinator.ts +7 -1
- package/src/utility/hash-string.ts +14 -17
- package/src/utility/lru-map.ts +55 -0
- package/dist/index.min.js +0 -9
- package/dist/index.min.js.map +0 -7
|
@@ -5,6 +5,9 @@ import { transformForBlockId, groupBy, concatTransforms, concatTransform, transf
|
|
|
5
5
|
import { blockIdToBytes } from "../utility/block-id-to-bytes.js";
|
|
6
6
|
import { isRecordEmpty } from "../utility/is-record-empty.js";
|
|
7
7
|
import { type CoordinatorBatch, makeBatchesByPeer, incompleteBatches, everyBatch, allBatches, mergeBlocks, processBatches, createBatchesForPayload } from "../utility/batch-coordinator.js";
|
|
8
|
+
import { createLogger, verbose } from "../logger.js";
|
|
9
|
+
|
|
10
|
+
const log = createLogger('network-transactor');
|
|
8
11
|
|
|
9
12
|
type NetworkTransactorInit = {
|
|
10
13
|
timeoutMs: number;
|
|
@@ -31,6 +34,8 @@ export class NetworkTransactor implements ITransactor {
|
|
|
31
34
|
async get(blockGets: BlockGets): Promise<GetBlockResults> {
|
|
32
35
|
// Group by block id
|
|
33
36
|
const distinctBlockIds = Array.from(new Set(blockGets.blockIds));
|
|
37
|
+
const t0 = Date.now();
|
|
38
|
+
log('get blockIds=%d', distinctBlockIds.length);
|
|
34
39
|
|
|
35
40
|
const batches = await this.batchesForPayload<BlockId[], GetBlockResults>(
|
|
36
41
|
distinctBlockIds,
|
|
@@ -77,6 +82,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
77
82
|
) as CoordinatorBatch<BlockId[], GetBlockResults>[];
|
|
78
83
|
|
|
79
84
|
if (retryable.length > 0 && Date.now() < expiration) {
|
|
85
|
+
log('get:retry retryable=%d', retryable.length);
|
|
80
86
|
try {
|
|
81
87
|
const excludedByRoot = new Map<CoordinatorBatch<BlockId[], GetBlockResults>, Set<PeerId>>();
|
|
82
88
|
for (const b of retryable) {
|
|
@@ -128,6 +134,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
128
134
|
// Ensure we have at least one response per requested block id
|
|
129
135
|
const missingIds = distinctBlockIds.filter(bid => !resultEntries.has(bid));
|
|
130
136
|
if (missingIds.length > 0) {
|
|
137
|
+
log('get:missing blockIds=%o', missingIds);
|
|
131
138
|
const details = this.formatBatchStatuses(batches,
|
|
132
139
|
b => (b.request?.isResponse as boolean) ?? false,
|
|
133
140
|
b => {
|
|
@@ -139,6 +146,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
139
146
|
throw aggregate;
|
|
140
147
|
}
|
|
141
148
|
|
|
149
|
+
log('get:done blockIds=%d ms=%d', distinctBlockIds.length, Date.now() - t0);
|
|
142
150
|
return Object.fromEntries(resultEntries) as GetBlockResults;
|
|
143
151
|
}
|
|
144
152
|
|
|
@@ -181,24 +189,77 @@ export class NetworkTransactor implements ITransactor {
|
|
|
181
189
|
transforms: Transforms,
|
|
182
190
|
transformForBlock: (payload: Transforms, blockId: BlockId, mergeWith?: Transforms) => Transforms
|
|
183
191
|
): Promise<CoordinatorBatch<Transforms, PendResult>[]> {
|
|
184
|
-
|
|
185
|
-
|
|
192
|
+
// Use cluster intersections to minimize the number of coordinators.
|
|
193
|
+
// For each block, find its full cluster, then greedily assign blocks to
|
|
194
|
+
// peers that appear in the most clusters — reducing round trips when
|
|
195
|
+
// blocks share cluster members.
|
|
196
|
+
|
|
197
|
+
// Step 1: Get cluster peer sets for each block
|
|
198
|
+
const blockClusterPeerIds: Map<BlockId, Set<string>> = new Map();
|
|
199
|
+
const fallbackBlocks: BlockId[] = [];
|
|
200
|
+
|
|
201
|
+
await Promise.all(blockIds.map(async bid => {
|
|
202
|
+
try {
|
|
203
|
+
const clusterPeers = await this.keyNetwork.findCluster(await blockIdToBytes(bid));
|
|
204
|
+
blockClusterPeerIds.set(bid, new Set(Object.keys(clusterPeers)));
|
|
205
|
+
} catch {
|
|
206
|
+
fallbackBlocks.push(bid);
|
|
207
|
+
}
|
|
208
|
+
}));
|
|
209
|
+
|
|
210
|
+
// Step 2: Build peer → blocks index (which blocks each peer can coordinate)
|
|
211
|
+
const peerBlocks = new Map<string, BlockId[]>();
|
|
212
|
+
for (const [blockId, peerIds] of blockClusterPeerIds) {
|
|
213
|
+
for (const peerId of peerIds) {
|
|
214
|
+
const blocks = peerBlocks.get(peerId) ?? [];
|
|
215
|
+
blocks.push(blockId);
|
|
216
|
+
peerBlocks.set(peerId, blocks);
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Step 3: Greedy set cover — assign blocks to peers covering the most uncovered blocks
|
|
221
|
+
const uncovered = new Set(blockClusterPeerIds.keys());
|
|
222
|
+
const assignments = new Map<string, BlockId[]>(); // peerIdStr → assigned blockIds
|
|
223
|
+
|
|
224
|
+
while (uncovered.size > 0) {
|
|
225
|
+
let bestPeer: string | undefined;
|
|
226
|
+
let bestCount = 0;
|
|
227
|
+
|
|
228
|
+
for (const [peerId, blocks] of peerBlocks) {
|
|
229
|
+
const coverCount = blocks.filter(bid => uncovered.has(bid)).length;
|
|
230
|
+
if (coverCount > bestCount) {
|
|
231
|
+
bestCount = coverCount;
|
|
232
|
+
bestPeer = peerId;
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
if (!bestPeer || bestCount === 0) break;
|
|
237
|
+
|
|
238
|
+
const covered = peerBlocks.get(bestPeer)!.filter(bid => uncovered.has(bid));
|
|
239
|
+
assignments.set(bestPeer, covered);
|
|
240
|
+
for (const bid of covered) uncovered.delete(bid);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// Step 4: Any remaining uncovered blocks fall back to findCoordinator
|
|
244
|
+
for (const bid of uncovered) fallbackBlocks.push(bid);
|
|
245
|
+
|
|
246
|
+
const fallbackCoordinators = await Promise.all(
|
|
247
|
+
fallbackBlocks.map(async bid => ({
|
|
186
248
|
blockId: bid,
|
|
187
249
|
coordinator: await this.keyNetwork.findCoordinator(await blockIdToBytes(bid), { excludedPeers: [] })
|
|
188
250
|
}))
|
|
189
251
|
);
|
|
190
|
-
|
|
191
|
-
const byCoordinator = new Map<string, BlockId[]>();
|
|
192
|
-
for (const { blockId, coordinator } of blockCoordinators) {
|
|
252
|
+
for (const { blockId, coordinator } of fallbackCoordinators) {
|
|
193
253
|
const key = coordinator.toString();
|
|
194
|
-
const
|
|
195
|
-
|
|
196
|
-
|
|
254
|
+
const existing = assignments.get(key) ?? [];
|
|
255
|
+
existing.push(blockId);
|
|
256
|
+
assignments.set(key, existing);
|
|
197
257
|
}
|
|
198
258
|
|
|
259
|
+
// Step 5: Convert assignments to batches
|
|
199
260
|
const batches: CoordinatorBatch<Transforms, PendResult>[] = [];
|
|
200
|
-
for (const [
|
|
201
|
-
const
|
|
261
|
+
for (const [peerIdStr, consolidatedBlocks] of assignments) {
|
|
262
|
+
const peerId = peerIdFromString(peerIdStr);
|
|
202
263
|
|
|
203
264
|
let batchTransforms: Transforms = { inserts: {}, updates: {}, deletes: [] };
|
|
204
265
|
for (const bid of consolidatedBlocks) {
|
|
@@ -207,7 +268,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
207
268
|
}
|
|
208
269
|
|
|
209
270
|
batches.push({
|
|
210
|
-
peerId
|
|
271
|
+
peerId,
|
|
211
272
|
payload: batchTransforms,
|
|
212
273
|
blockId: consolidatedBlocks[0]!,
|
|
213
274
|
coordinatingBlockIds: consolidatedBlocks,
|
|
@@ -219,6 +280,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
219
280
|
}
|
|
220
281
|
|
|
221
282
|
async pend(blockAction: PendRequest): Promise<PendResult> {
|
|
283
|
+
const t0 = Date.now();
|
|
222
284
|
const transformForBlock = (payload: Transforms, blockId: BlockId, mergeWithPayload: Transforms | undefined): Transforms => {
|
|
223
285
|
const filteredTransform = transformForBlockId(payload, blockId);
|
|
224
286
|
return mergeWithPayload
|
|
@@ -227,6 +289,17 @@ export class NetworkTransactor implements ITransactor {
|
|
|
227
289
|
};
|
|
228
290
|
const blockIds = blockIdsForTransforms(blockAction.transforms);
|
|
229
291
|
const batches = await this.consolidateCoordinators(blockIds, blockAction.transforms, transformForBlock);
|
|
292
|
+
log('pend actionId=%s blockIds=%d batches=%d', blockAction.actionId, blockIds.length, batches.length);
|
|
293
|
+
if (verbose) {
|
|
294
|
+
const batchSummary = batches.map(b => ({
|
|
295
|
+
peer: b.peerId.toString().substring(0, 12),
|
|
296
|
+
blocks: (b as any).coordinatingBlockIds ?? [b.blockId],
|
|
297
|
+
inserts: Object.keys(b.payload.inserts ?? {}).length,
|
|
298
|
+
updates: Object.keys(b.payload.updates ?? {}).length,
|
|
299
|
+
deletes: b.payload.deletes?.length ?? 0
|
|
300
|
+
}));
|
|
301
|
+
log('pend:batches actionId=%s detail=%o', blockAction.actionId, batchSummary);
|
|
302
|
+
}
|
|
230
303
|
const expiration = Date.now() + this.timeoutMs;
|
|
231
304
|
|
|
232
305
|
let error: Error | undefined;
|
|
@@ -274,9 +347,11 @@ export class NetworkTransactor implements ITransactor {
|
|
|
274
347
|
}
|
|
275
348
|
|
|
276
349
|
if (error) { // If any failures, cancel all pending actions as background microtask
|
|
350
|
+
log('pend:cancel actionId=%s', blockAction.actionId);
|
|
277
351
|
void Promise.resolve().then(() => this.cancelBatch(batches, { blockIds, actionId: blockAction.actionId }));
|
|
278
352
|
const stale = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && !b.request!.response!.success));
|
|
279
353
|
if (stale.length > 0) { // Any active stale failures should preempt reporting connection or other potential transient errors (we have information)
|
|
354
|
+
log('pend:stale actionId=%s staleCount=%d', blockAction.actionId, stale.length);
|
|
280
355
|
return {
|
|
281
356
|
success: false,
|
|
282
357
|
missing: distinctBlockActionTransforms(stale.flatMap(b => (b.request!.response! as StaleFailure).missing).filter((x): x is ActionTransforms => x !== undefined)),
|
|
@@ -287,6 +362,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
287
362
|
|
|
288
363
|
// Collect replies back into result structure
|
|
289
364
|
const completed = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && b.request!.response!.success));
|
|
365
|
+
log('pend:done actionId=%s ms=%d batches=%d', blockAction.actionId, Date.now() - t0, batches.length);
|
|
290
366
|
return {
|
|
291
367
|
success: true,
|
|
292
368
|
pending: completed.flatMap(b => (b.request!.response! as PendSuccess).pending),
|
|
@@ -295,6 +371,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
295
371
|
}
|
|
296
372
|
|
|
297
373
|
async cancel(actionRef: ActionBlocks): Promise<void> {
|
|
374
|
+
log('cancel actionId=%s blockIds=%d', actionRef.actionId, actionRef.blockIds.length);
|
|
298
375
|
const batches = await this.batchesForPayload<BlockId[], void>(
|
|
299
376
|
actionRef.blockIds,
|
|
300
377
|
actionRef.blockIds,
|
|
@@ -320,6 +397,8 @@ export class NetworkTransactor implements ITransactor {
|
|
|
320
397
|
}
|
|
321
398
|
|
|
322
399
|
async commit(request: CommitRequest): Promise<CommitResult> {
|
|
400
|
+
const t0 = Date.now();
|
|
401
|
+
log('commit actionId=%s rev=%d blockIds=%d', request.actionId, request.rev, request.blockIds.length);
|
|
323
402
|
const allBlockIds = [...new Set([...request.blockIds, request.tailId])];
|
|
324
403
|
|
|
325
404
|
// Commit the header block if provided and not already in blockIds
|
|
@@ -350,6 +429,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
350
429
|
}
|
|
351
430
|
}
|
|
352
431
|
|
|
432
|
+
log('commit:done actionId=%s ms=%d', request.actionId, Date.now() - t0);
|
|
353
433
|
return { success: true };
|
|
354
434
|
}
|
|
355
435
|
|
|
@@ -372,6 +452,7 @@ export class NetworkTransactor implements ITransactor {
|
|
|
372
452
|
private async commitBlocks({ blockIds, actionId, rev }: RepoCommitRequest) {
|
|
373
453
|
const expiration = Date.now() + this.timeoutMs;
|
|
374
454
|
const batches = await this.batchesForPayload<BlockId[], CommitResult>(blockIds, blockIds, mergeBlocks, []);
|
|
455
|
+
log('commitBlocks actionId=%s rev=%d batches=%d', actionId, rev, batches.length);
|
|
375
456
|
let error: Error | undefined;
|
|
376
457
|
try {
|
|
377
458
|
await processBatches(
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import { randomBytes } from '@noble/hashes/utils.js'
|
|
2
2
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
3
|
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId, StaleFailure, ActionContext, BlockType, BlockSource, Transforms } from "../index.js";
|
|
4
|
+
import type { ReadDependency } from "../transaction/transaction.js";
|
|
4
5
|
|
|
5
6
|
export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlock> {
|
|
7
|
+
private readDependencies: ReadDependency[] = [];
|
|
8
|
+
|
|
6
9
|
constructor(
|
|
7
10
|
private readonly collectionId: BlockId,
|
|
8
11
|
private readonly transactor: ITransactor,
|
|
@@ -26,12 +29,22 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
26
29
|
const result = await this.transactor.get({ blockIds: [id], context: this.actionContext });
|
|
27
30
|
if (result) {
|
|
28
31
|
const { block, state } = result[id]!;
|
|
32
|
+
// Record read dependency for optimistic concurrency control
|
|
33
|
+
this.readDependencies.push({ blockId: id, revision: state.latest?.rev ?? 0 });
|
|
29
34
|
// TODO: if the state reports that there is a pending action, record this so that we are sure to update before syncing
|
|
30
35
|
//state.pendings
|
|
31
36
|
return block as TBlock;
|
|
32
37
|
}
|
|
33
38
|
}
|
|
34
39
|
|
|
40
|
+
getReadDependencies(): ReadDependency[] {
|
|
41
|
+
return this.readDependencies;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clearReadDependencies(): void {
|
|
45
|
+
this.readDependencies = [];
|
|
46
|
+
}
|
|
47
|
+
|
|
35
48
|
/**
|
|
36
49
|
* Attempts to apply the given transforms in a transactional manner.
|
|
37
50
|
* @param transform - The transforms to apply.
|
|
@@ -50,15 +63,21 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
50
63
|
return pendResult;
|
|
51
64
|
}
|
|
52
65
|
const isNew = transform.inserts && Object.hasOwn(transform.inserts, headerId);
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
66
|
+
try {
|
|
67
|
+
const commitResult = await this.transactor.commit({
|
|
68
|
+
headerId: isNew ? headerId : undefined,
|
|
69
|
+
tailId,
|
|
70
|
+
blockIds: pendResult.blockIds,
|
|
71
|
+
actionId,
|
|
72
|
+
rev
|
|
73
|
+
});
|
|
74
|
+
if (!commitResult.success) {
|
|
75
|
+
await this.transactor.cancel({ actionId, blockIds: pendResult.blockIds });
|
|
76
|
+
return commitResult;
|
|
77
|
+
}
|
|
78
|
+
} catch (e) {
|
|
79
|
+
await this.transactor.cancel({ actionId, blockIds: pendResult.blockIds });
|
|
80
|
+
throw e;
|
|
62
81
|
}
|
|
63
82
|
}
|
|
64
83
|
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { Atomic } from './atomic.js';
|
|
2
|
+
import type { IBlock, BlockId, BlockStore, BlockType, BlockHeader, BlockOperation } from '../index.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A BlockStore proxy that enables scoped atomic operations.
|
|
6
|
+
* Operations normally delegate directly to the underlying store,
|
|
7
|
+
* but during an `atomic()` call, they route through an Atomic tracker
|
|
8
|
+
* that commits all-or-nothing on success, or rolls back on error.
|
|
9
|
+
*
|
|
10
|
+
* Both the BTree and its trunk should share the same AtomicProxy instance
|
|
11
|
+
* so that all mutations (including root pointer updates) are part of the
|
|
12
|
+
* same atomic batch.
|
|
13
|
+
*/
|
|
14
|
+
export class AtomicProxy<T extends IBlock> implements BlockStore<T> {
|
|
15
|
+
private _base: BlockStore<T>;
|
|
16
|
+
private _active: BlockStore<T>;
|
|
17
|
+
|
|
18
|
+
constructor(store: BlockStore<T>) {
|
|
19
|
+
this._base = store;
|
|
20
|
+
this._active = store;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async tryGet(id: BlockId): Promise<T | undefined> { return this._active.tryGet(id); }
|
|
24
|
+
insert(block: T): void { this._active.insert(block); }
|
|
25
|
+
update(blockId: BlockId, op: BlockOperation): void { this._active.update(blockId, op); }
|
|
26
|
+
delete(blockId: BlockId): void { this._active.delete(blockId); }
|
|
27
|
+
generateId(): BlockId { return this._active.generateId(); }
|
|
28
|
+
createBlockHeader(type: BlockType, newId?: BlockId): BlockHeader { return this._active.createBlockHeader(type, newId); }
|
|
29
|
+
|
|
30
|
+
/** Execute fn within an atomic scope. All store mutations are collected
|
|
31
|
+
* and committed on success, or discarded on error. Re-entrant safe. */
|
|
32
|
+
async atomic<R>(fn: () => Promise<R>): Promise<R> {
|
|
33
|
+
if (this._active !== this._base) {
|
|
34
|
+
return fn(); // Already in atomic context
|
|
35
|
+
}
|
|
36
|
+
const atomic = new Atomic<T>(this._base);
|
|
37
|
+
this._active = atomic;
|
|
38
|
+
try {
|
|
39
|
+
const result = await fn();
|
|
40
|
+
atomic.commit();
|
|
41
|
+
return result;
|
|
42
|
+
} catch (e) {
|
|
43
|
+
atomic.reset();
|
|
44
|
+
throw e;
|
|
45
|
+
} finally {
|
|
46
|
+
this._active = this._base;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -1,19 +1,33 @@
|
|
|
1
1
|
import type { IBlock, BlockHeader, BlockId, BlockSource, BlockType, Transforms } from "../index.js";
|
|
2
2
|
import { applyOperation } from "../index.js";
|
|
3
|
+
import { LruMap } from "../utility/lru-map.js";
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
|
+
|
|
6
|
+
const log = createLogger('cache');
|
|
7
|
+
|
|
8
|
+
const DefaultMaxSize = 128;
|
|
3
9
|
|
|
4
10
|
export class CacheSource<T extends IBlock> implements BlockSource<T> {
|
|
5
|
-
protected cache
|
|
11
|
+
protected cache: LruMap<BlockId, T>;
|
|
6
12
|
|
|
7
13
|
constructor(
|
|
8
|
-
protected readonly source: BlockSource<T
|
|
9
|
-
|
|
14
|
+
protected readonly source: BlockSource<T>,
|
|
15
|
+
maxSize = DefaultMaxSize
|
|
16
|
+
) {
|
|
17
|
+
this.cache = new LruMap(maxSize);
|
|
18
|
+
}
|
|
10
19
|
|
|
11
20
|
async tryGet(id: BlockId): Promise<T | undefined> {
|
|
12
21
|
let block = this.cache.get(id);
|
|
13
|
-
if (
|
|
22
|
+
if (block) {
|
|
23
|
+
log('hit id=%s', id);
|
|
24
|
+
} else {
|
|
14
25
|
block = await this.source.tryGet(id);
|
|
15
26
|
if (block) {
|
|
16
27
|
this.cache.set(id, block);
|
|
28
|
+
log('miss:loaded id=%s cacheSize=%d', id, this.cache.size);
|
|
29
|
+
} else {
|
|
30
|
+
log('miss:absent id=%s', id);
|
|
17
31
|
}
|
|
18
32
|
}
|
|
19
33
|
return structuredClone(block);
|
package/src/transform/index.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import type { PeerId } from "../network/types.js";
|
|
2
2
|
import type { BlockId } from "../index.js";
|
|
3
3
|
import { Pending } from "./pending.js";
|
|
4
|
+
import { createLogger } from "../logger.js";
|
|
5
|
+
|
|
6
|
+
const log = createLogger('batch-coordinator');
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* Represents a batch of operations for a specific block coordinated by a peer
|
|
@@ -122,6 +125,7 @@ export async function processBatches<TPayload, TResponse>(
|
|
|
122
125
|
.catch(async e => {
|
|
123
126
|
if (expiration > Date.now()) {
|
|
124
127
|
const excludedPeers = [batch.peerId, ...(batch.excludedPeers ?? [])];
|
|
128
|
+
log('retry peer=%s excluded=%d', batch.peerId.toString(), excludedPeers.length);
|
|
125
129
|
const retries = await createBatchesForPayload<TPayload, TResponse>(
|
|
126
130
|
getBlockIds(batch),
|
|
127
131
|
batch.payload,
|
|
@@ -170,5 +174,7 @@ export async function createBatchesForPayload<TPayload, TResponse>(
|
|
|
170
174
|
);
|
|
171
175
|
|
|
172
176
|
// Group blocks around their coordinating peers
|
|
173
|
-
|
|
177
|
+
const batches = makeBatchesByPeer<TPayload, TResponse>(blockIdPeerId, payload, getBlockPayload, excludedPeers);
|
|
178
|
+
log('createBatches blockIds=%d batches=%d excluded=%d', distinctBlockIds.size, batches.length, excludedPeers.length);
|
|
179
|
+
return batches;
|
|
174
180
|
}
|
|
@@ -1,17 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* @param str - The string to hash
|
|
8
|
-
* @returns A
|
|
9
|
-
*/
|
|
10
|
-
export function hashString(str: string): string {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
return Math.abs(hash).toString(36);
|
|
16
|
-
}
|
|
17
|
-
|
|
1
|
+
import { sha256 } from 'multiformats/hashes/sha2';
|
|
2
|
+
import { toString } from 'uint8arrays/to-string';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* SHA-256 string hash function.
|
|
6
|
+
*
|
|
7
|
+
* @param str - The string to hash
|
|
8
|
+
* @returns A base64url-encoded SHA-256 hash string
|
|
9
|
+
*/
|
|
10
|
+
export async function hashString(str: string): Promise<string> {
|
|
11
|
+
const input = new TextEncoder().encode(str);
|
|
12
|
+
const mh = await sha256.digest(input);
|
|
13
|
+
return toString(mh.digest, 'base64url');
|
|
14
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A simple LRU (Least Recently Used) map backed by JavaScript's Map insertion order.
|
|
3
|
+
* Accessing or setting an entry refreshes it to the most-recently-used position.
|
|
4
|
+
* When the map exceeds maxSize, the least-recently-used entry is evicted.
|
|
5
|
+
*/
|
|
6
|
+
export class LruMap<K, V> {
|
|
7
|
+
private readonly map = new Map<K, V>();
|
|
8
|
+
|
|
9
|
+
constructor(private readonly maxSize: number) {
|
|
10
|
+
if (maxSize < 1) throw new Error('LruMap maxSize must be >= 1');
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
get(key: K): V | undefined {
|
|
14
|
+
const value = this.map.get(key);
|
|
15
|
+
if (value !== undefined) {
|
|
16
|
+
// Refresh: delete and re-insert to move to end (most recent)
|
|
17
|
+
this.map.delete(key);
|
|
18
|
+
this.map.set(key, value);
|
|
19
|
+
}
|
|
20
|
+
return value;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
set(key: K, value: V): this {
|
|
24
|
+
// If already present, delete first to refresh position
|
|
25
|
+
if (this.map.has(key)) {
|
|
26
|
+
this.map.delete(key);
|
|
27
|
+
} else if (this.map.size >= this.maxSize) {
|
|
28
|
+
// Evict the oldest (first) entry
|
|
29
|
+
const oldest = this.map.keys().next().value!;
|
|
30
|
+
this.map.delete(oldest);
|
|
31
|
+
}
|
|
32
|
+
this.map.set(key, value);
|
|
33
|
+
return this;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
has(key: K): boolean {
|
|
37
|
+
return this.map.has(key);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
delete(key: K): boolean {
|
|
41
|
+
return this.map.delete(key);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
clear(): void {
|
|
45
|
+
this.map.clear();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get size(): number {
|
|
49
|
+
return this.map.size;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
[Symbol.iterator](): IterableIterator<[K, V]> {
|
|
53
|
+
return this.map[Symbol.iterator]();
|
|
54
|
+
}
|
|
55
|
+
}
|