@peerbit/shared-log 16.0.10 → 16.0.11
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/index.d.ts +6 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +390 -87
- package/dist/src/index.js.map +1 -1
- package/package.json +12 -12
- package/src/index.ts +469 -92
package/src/index.ts
CHANGED
|
@@ -904,6 +904,7 @@ type PreparedPayloadCommitOnlyResult<T, R extends "u32" | "u64"> = {
|
|
|
904
904
|
entry: Entry<T>;
|
|
905
905
|
removed: ShallowOrFullEntry<T>[];
|
|
906
906
|
removedHashes?: string[];
|
|
907
|
+
removedGids?: string[];
|
|
907
908
|
appendCommit: PreparedLocalAppendCommit<R>;
|
|
908
909
|
};
|
|
909
910
|
|
|
@@ -1407,6 +1408,8 @@ const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_CPU_LIMIT = 0.005;
|
|
|
1407
1408
|
const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_MEMORY_LIMIT = 0.001;
|
|
1408
1409
|
const RECALCULATE_PARTICIPATION_RELATIVE_DENOMINATOR_FLOOR = 1e-3;
|
|
1409
1410
|
const TOPIC_SUBSCRIBERS_CACHE_TTL_MS = 250;
|
|
1411
|
+
const LOCAL_REACHABLE_PEERS_CACHE_TTL_MS = 250;
|
|
1412
|
+
const LOCAL_REACHABLE_PEERS_MAX = 64;
|
|
1410
1413
|
const LEADER_SELECTION_CONTEXT_CACHE_TTL_MS = 50;
|
|
1411
1414
|
// Leader-plan results are invalidated structurally (replication:change,
|
|
1412
1415
|
// replicator:mature, subscriber changes). The TTL bounds wall-clock maturity
|
|
@@ -1430,6 +1433,8 @@ const ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS = 10_000;
|
|
|
1430
1433
|
// raw exchange message size the receive path is tuned for.
|
|
1431
1434
|
const LIVE_RAW_GOSSIP_MAX_ENTRIES = 256;
|
|
1432
1435
|
const LIVE_RAW_GOSSIP_MAX_BYTES = 128 * 1024;
|
|
1436
|
+
const GID_PEER_HISTORY_CLEANUP_BATCH_SIZE = 256;
|
|
1437
|
+
const GID_PEER_HISTORY_CLEANUP_PENDING_CAPACITY = 4096;
|
|
1433
1438
|
|
|
1434
1439
|
type LiveRawGossipBatch = {
|
|
1435
1440
|
to: string[];
|
|
@@ -1763,6 +1768,7 @@ type TrustedLowerLogNativePreparedCommit = {
|
|
|
1763
1768
|
hashDigestBytes?: Uint8Array;
|
|
1764
1769
|
trimmedEntries?: unknown[];
|
|
1765
1770
|
trimmedEntryHashes?: string[];
|
|
1771
|
+
trimmedEntryGids?: string[];
|
|
1766
1772
|
nativeBlocksDeleted?: boolean;
|
|
1767
1773
|
nativeDeleteCleanupToken?: unknown;
|
|
1768
1774
|
nativeCommitOwnershipToken?: unknown;
|
|
@@ -1774,6 +1780,8 @@ type TrustedLowerLogNativePreparedCommit = {
|
|
|
1774
1780
|
type TrustedLowerLogPreparedAppendResult<T> = {
|
|
1775
1781
|
entry: Entry<T>;
|
|
1776
1782
|
removed: ShallowOrFullEntry<T>[];
|
|
1783
|
+
removedHashes?: string[];
|
|
1784
|
+
removedGids?: string[];
|
|
1777
1785
|
change: Change<T>;
|
|
1778
1786
|
appendFacts: PreparedAppendFacts;
|
|
1779
1787
|
};
|
|
@@ -1784,6 +1792,7 @@ type TrustedLowerLogCommitOnlyAppendResult<T> = {
|
|
|
1784
1792
|
shallowEntry: ShallowEntry;
|
|
1785
1793
|
removed: ShallowOrFullEntry<T>[];
|
|
1786
1794
|
removedHashes?: string[];
|
|
1795
|
+
removedGids?: string[];
|
|
1787
1796
|
appendFacts: PreparedAppendFacts;
|
|
1788
1797
|
documentTrimmedHeadsProcessed?: boolean;
|
|
1789
1798
|
documentPreviousContext?: PreparedLocalAppendCommit<"u32">["documentPreviousContext"];
|
|
@@ -1795,6 +1804,7 @@ type TrustedLowerLogCommitOnlyAppendBatchResult<T> = {
|
|
|
1795
1804
|
materializeEntries: Array<() => Entry<T>>;
|
|
1796
1805
|
removed: ShallowOrFullEntry<T>[];
|
|
1797
1806
|
removedHashes?: string[];
|
|
1807
|
+
removedGids?: string[];
|
|
1798
1808
|
appendFacts: PreparedAppendFacts[];
|
|
1799
1809
|
documentTrimmedHeadsProcessed?: boolean[];
|
|
1800
1810
|
nativeCommittedAppendFinalizer?: TrustedLowerLogNativeCommitFinalizer;
|
|
@@ -2082,17 +2092,19 @@ export class SharedLog<
|
|
|
2082
2092
|
// GROWTH SHAPE. Rows are released by `deleteGidPeerHistory` on the two prune
|
|
2083
2093
|
// paths, by `removePeerFromGidPeerHistory` once a gid's last peer drops (the
|
|
2084
2094
|
// routine disconnect outcome), by `rebalanceAll({ clearCache: true })`, and
|
|
2085
|
-
// wholesale on close/reset.
|
|
2086
|
-
//
|
|
2087
|
-
//
|
|
2095
|
+
// wholesale on close/reset. Trim now carries compact distinct removed-gid
|
|
2096
|
+
// facts beside its hash-only result. After the lower mutation commits, a
|
|
2097
|
+
// coalesced cleanup checks authoritative local-head liveness and drops a row
|
|
2098
|
+
// only when no sibling remains. A gid names a graph, not an entry: an
|
|
2088
2099
|
// entry with `meta.next` inherits `min(next.meta.gid)` (see
|
|
2089
2100
|
// packages/log/src/entry-v0.ts), so document updates fold into the gid of
|
|
2090
2101
|
// the first put and the row count tracks distinct chain roots -- distinct
|
|
2091
2102
|
// document ids -- rather than entry count. Insert-only workloads mint a
|
|
2092
|
-
// fresh gid per append and so do grow one row per entry.
|
|
2093
|
-
//
|
|
2094
|
-
//
|
|
2095
|
-
//
|
|
2103
|
+
// fresh gid per append and so do grow one row per entry. Merge-shadowed gids
|
|
2104
|
+
// are a separate, smaller source that still needs a fast-path-compatible
|
|
2105
|
+
// post-commit metadata seam. Registering the lower log's `onGidRemoved` hook
|
|
2106
|
+
// here would disable native prepared, commit-only, and batch append paths, so
|
|
2107
|
+
// this bounded slice deliberately addresses trim only.
|
|
2096
2108
|
//
|
|
2097
2109
|
// WHY TRIM DOES NOT SIMPLY CALL `deleteGidPeerHistory` AS WELL. Both prune
|
|
2098
2110
|
// callers delete a whole row from a single entry's gid, and under the
|
|
@@ -2110,10 +2122,11 @@ export class SharedLog<
|
|
|
2110
2122
|
// on the common path, paying for the freed memory in repeated re-delivery of
|
|
2111
2123
|
// entries that are still here. That trade is not worth it.
|
|
2112
2124
|
//
|
|
2113
|
-
//
|
|
2114
|
-
//
|
|
2115
|
-
//
|
|
2116
|
-
//
|
|
2125
|
+
// Cleanup work is coalesced, checked in bounded batches, and capped at 4096
|
|
2126
|
+
// pending gids. Saturation forgets the overflowing suppression row
|
|
2127
|
+
// immediately. That bounded fallback can cause redundant delivery for a live
|
|
2128
|
+
// sibling, but missing history is deliberately correctness-safe as described
|
|
2129
|
+
// above; it cannot affect ownership, quorum, or retained data.
|
|
2117
2130
|
//
|
|
2118
2131
|
// Existing, deliberate imprecision: under the time domain the coordinate is
|
|
2119
2132
|
// `meta.clock.timestamp.wallTime` (replication-domain-time.ts) and is
|
|
@@ -2121,6 +2134,14 @@ export class SharedLog<
|
|
|
2121
2134
|
// and prune's whole-row delete is already over-eager there. The cost is the
|
|
2122
2135
|
// same bounded extra traffic, never a wrong prune.
|
|
2123
2136
|
_gidPeersHistory!: Map<string, Set<string>>;
|
|
2137
|
+
private _gidPeerHistoryCleanupState!: {
|
|
2138
|
+
history: Map<string, Set<string>>;
|
|
2139
|
+
tail: Promise<void>;
|
|
2140
|
+
pending: Set<string>;
|
|
2141
|
+
draining: boolean;
|
|
2142
|
+
highWater: number;
|
|
2143
|
+
wake?: () => void;
|
|
2144
|
+
};
|
|
2124
2145
|
|
|
2125
2146
|
private _onSubscriptionFn!: (arg: any) => any;
|
|
2126
2147
|
private _onUnsubscriptionFn!: (arg: any) => any;
|
|
@@ -3427,6 +3448,14 @@ export class SharedLog<
|
|
|
3427
3448
|
string,
|
|
3428
3449
|
{ expiresAt: number; keys: PublicSignKey[] }
|
|
3429
3450
|
>;
|
|
3451
|
+
private _localReachablePeerHashesCache!: Map<
|
|
3452
|
+
string,
|
|
3453
|
+
{
|
|
3454
|
+
expiresAt: number;
|
|
3455
|
+
hashes?: string[];
|
|
3456
|
+
inFlight?: Promise<string[]>;
|
|
3457
|
+
}
|
|
3458
|
+
>;
|
|
3430
3459
|
private _leaderSelectionContextCache?: {
|
|
3431
3460
|
expiresAt: number;
|
|
3432
3461
|
context: LeaderSelectionContext;
|
|
@@ -3744,6 +3773,7 @@ export class SharedLog<
|
|
|
3744
3773
|
this._receiveHandlerDrainByPeer = new Map();
|
|
3745
3774
|
this._openingSyncCapabilitiesByPeer = new Map();
|
|
3746
3775
|
this._gidPeersHistory = new Map();
|
|
3776
|
+
this.resetGidPeerHistoryCleanupState();
|
|
3747
3777
|
this._repairRetryTimers = new Set();
|
|
3748
3778
|
this._recentRepairDispatch = new Map();
|
|
3749
3779
|
this._repairSweepRunning = false;
|
|
@@ -3766,6 +3796,7 @@ export class SharedLog<
|
|
|
3766
3796
|
this._joinAuthoritativeRepairPeersByDelay = new Map();
|
|
3767
3797
|
this._appendBackfillPendingByTarget = new Map();
|
|
3768
3798
|
this._topicSubscribersCache = new Map();
|
|
3799
|
+
this._localReachablePeerHashesCache = new Map();
|
|
3769
3800
|
this._peerSyncCapabilities = new Map();
|
|
3770
3801
|
this._peerSyncCapabilitySessions = new Map();
|
|
3771
3802
|
this._peerSyncCapabilityTimestamps = new Map();
|
|
@@ -5134,6 +5165,93 @@ export class SharedLog<
|
|
|
5134
5165
|
);
|
|
5135
5166
|
}
|
|
5136
5167
|
|
|
5168
|
+
private async _getLocalReachablePeerHashes(topic: string): Promise<string[]> {
|
|
5169
|
+
const cache = (this._localReachablePeerHashesCache ??= new Map());
|
|
5170
|
+
const cached = cache.get(topic);
|
|
5171
|
+
if (cached?.hashes && cached.expiresAt > Date.now()) {
|
|
5172
|
+
return cached.hashes.slice();
|
|
5173
|
+
}
|
|
5174
|
+
if (cached?.inFlight) {
|
|
5175
|
+
return (await cached.inFlight).slice();
|
|
5176
|
+
}
|
|
5177
|
+
|
|
5178
|
+
const entry: {
|
|
5179
|
+
expiresAt: number;
|
|
5180
|
+
hashes?: string[];
|
|
5181
|
+
inFlight?: Promise<string[]>;
|
|
5182
|
+
} = { expiresAt: 0 };
|
|
5183
|
+
const inFlight = (async () => {
|
|
5184
|
+
const selfHash = this.node.identity.publicKey.hashcode();
|
|
5185
|
+
const hashes: string[] = [];
|
|
5186
|
+
const seen = new Set<string>();
|
|
5187
|
+
const addHash = (hash: unknown) => {
|
|
5188
|
+
if (
|
|
5189
|
+
typeof hash !== "string" ||
|
|
5190
|
+
hash.length === 0 ||
|
|
5191
|
+
hash === selfHash ||
|
|
5192
|
+
seen.has(hash) ||
|
|
5193
|
+
hashes.length >= LOCAL_REACHABLE_PEERS_MAX
|
|
5194
|
+
) {
|
|
5195
|
+
return;
|
|
5196
|
+
}
|
|
5197
|
+
seen.add(hash);
|
|
5198
|
+
hashes.push(hash);
|
|
5199
|
+
};
|
|
5200
|
+
|
|
5201
|
+
const pubsub = this.node.services.pubsub as any;
|
|
5202
|
+
const peerMap = pubsub?.peers;
|
|
5203
|
+
if (peerMap?.entries) {
|
|
5204
|
+
for (const [hash, peer] of peerMap.entries()) {
|
|
5205
|
+
addHash(peer?.publicKey?.hashcode?.());
|
|
5206
|
+
addHash(hash);
|
|
5207
|
+
if (hashes.length >= LOCAL_REACHABLE_PEERS_MAX) break;
|
|
5208
|
+
}
|
|
5209
|
+
}
|
|
5210
|
+
|
|
5211
|
+
if (hashes.length < LOCAL_REACHABLE_PEERS_MAX) {
|
|
5212
|
+
const connectionManager = pubsub?.components?.connectionManager;
|
|
5213
|
+
for (const conn of connectionManager?.getConnections?.() ?? []) {
|
|
5214
|
+
const peerId = conn?.remotePeer;
|
|
5215
|
+
if (!peerId) continue;
|
|
5216
|
+
try {
|
|
5217
|
+
addHash(getPublicKeyFromPeerId(peerId).hashcode());
|
|
5218
|
+
} catch {
|
|
5219
|
+
// Best-effort only.
|
|
5220
|
+
}
|
|
5221
|
+
if (hashes.length >= LOCAL_REACHABLE_PEERS_MAX) break;
|
|
5222
|
+
}
|
|
5223
|
+
}
|
|
5224
|
+
|
|
5225
|
+
if (hashes.length < LOCAL_REACHABLE_PEERS_MAX) {
|
|
5226
|
+
try {
|
|
5227
|
+
for (const subscriber of (await pubsub.getSubscribers(topic)) ?? []) {
|
|
5228
|
+
addHash(subscriber?.hashcode?.());
|
|
5229
|
+
if (hashes.length >= LOCAL_REACHABLE_PEERS_MAX) break;
|
|
5230
|
+
}
|
|
5231
|
+
} catch {
|
|
5232
|
+
// Local reachability is best-effort.
|
|
5233
|
+
}
|
|
5234
|
+
}
|
|
5235
|
+
|
|
5236
|
+
return hashes;
|
|
5237
|
+
})();
|
|
5238
|
+
entry.inFlight = inFlight;
|
|
5239
|
+
cache.set(topic, entry);
|
|
5240
|
+
|
|
5241
|
+
try {
|
|
5242
|
+
const hashes = await inFlight;
|
|
5243
|
+
if (cache.get(topic) === entry) {
|
|
5244
|
+
entry.hashes = hashes;
|
|
5245
|
+
entry.expiresAt = Date.now() + LOCAL_REACHABLE_PEERS_CACHE_TTL_MS;
|
|
5246
|
+
entry.inFlight = undefined;
|
|
5247
|
+
}
|
|
5248
|
+
return hashes.slice();
|
|
5249
|
+
} catch (error) {
|
|
5250
|
+
if (cache.get(topic) === entry) cache.delete(topic);
|
|
5251
|
+
throw error;
|
|
5252
|
+
}
|
|
5253
|
+
}
|
|
5254
|
+
|
|
5137
5255
|
private async _getTopicSubscribers(
|
|
5138
5256
|
topic: string,
|
|
5139
5257
|
): Promise<PublicSignKey[] | undefined> {
|
|
@@ -5258,6 +5376,7 @@ export class SharedLog<
|
|
|
5258
5376
|
for (const topic of topics) {
|
|
5259
5377
|
if (!topic) continue;
|
|
5260
5378
|
this._topicSubscribersCache.delete(topic);
|
|
5379
|
+
this._localReachablePeerHashesCache?.delete(topic);
|
|
5261
5380
|
}
|
|
5262
5381
|
this.invalidateLeaderSelectionContextCache();
|
|
5263
5382
|
}
|
|
@@ -10293,6 +10412,8 @@ export class SharedLog<
|
|
|
10293
10412
|
): Promise<{
|
|
10294
10413
|
entry: Entry<T>;
|
|
10295
10414
|
removed: ShallowOrFullEntry<T>[];
|
|
10415
|
+
removedHashes?: string[];
|
|
10416
|
+
removedGids?: string[];
|
|
10296
10417
|
appendCommit: PreparedLocalAppendCommit<R>;
|
|
10297
10418
|
}> {
|
|
10298
10419
|
this.throwIfNativeDurableCommitFailed();
|
|
@@ -10334,6 +10455,8 @@ export class SharedLog<
|
|
|
10334
10455
|
return {
|
|
10335
10456
|
entry: result.entry,
|
|
10336
10457
|
removed: result.removed,
|
|
10458
|
+
removedHashes: result.removedHashes,
|
|
10459
|
+
removedGids: result.removedGids,
|
|
10337
10460
|
appendCommit: nativePreparedCommit,
|
|
10338
10461
|
};
|
|
10339
10462
|
}
|
|
@@ -10400,6 +10523,8 @@ export class SharedLog<
|
|
|
10400
10523
|
return {
|
|
10401
10524
|
entry: result.entry,
|
|
10402
10525
|
removed: result.removed,
|
|
10526
|
+
removedHashes: result.removedHashes,
|
|
10527
|
+
removedGids: result.removedGids,
|
|
10403
10528
|
appendCommit: this.createPreparedLocalAppendCommitFromFacts(
|
|
10404
10529
|
result.appendFacts,
|
|
10405
10530
|
nativeAppendPlan,
|
|
@@ -10412,6 +10537,8 @@ export class SharedLog<
|
|
|
10412
10537
|
entry?: Entry<T>;
|
|
10413
10538
|
materializeEntry?: () => Entry<T>;
|
|
10414
10539
|
removed: ShallowOrFullEntry<T>[];
|
|
10540
|
+
removedHashes?: string[];
|
|
10541
|
+
removedGids?: string[];
|
|
10415
10542
|
change?: Change<T>;
|
|
10416
10543
|
appendFacts: PreparedAppendFacts;
|
|
10417
10544
|
},
|
|
@@ -10439,6 +10566,7 @@ export class SharedLog<
|
|
|
10439
10566
|
|
|
10440
10567
|
const plannedCoordinateDeleteHashes =
|
|
10441
10568
|
result.change?.removed.map((entry) => entry.hash) ??
|
|
10569
|
+
result.removedHashes ??
|
|
10442
10570
|
result.removed.map((entry) => entry.hash);
|
|
10443
10571
|
const nativeAppendPlan = await this.planNativeLocalAppendFacts(
|
|
10444
10572
|
result.appendFacts,
|
|
@@ -10473,6 +10601,8 @@ export class SharedLog<
|
|
|
10473
10601
|
{
|
|
10474
10602
|
forgetNativeCoordinates:
|
|
10475
10603
|
!nativeAppendPlan.committedNativeCoordinateDeletes,
|
|
10604
|
+
removedHashes: result.removedHashes,
|
|
10605
|
+
removedGids: result.removedGids,
|
|
10476
10606
|
ownershipLifecycleController,
|
|
10477
10607
|
},
|
|
10478
10608
|
);
|
|
@@ -11257,6 +11387,7 @@ export class SharedLog<
|
|
|
11257
11387
|
},
|
|
11258
11388
|
removed: prepared.removed,
|
|
11259
11389
|
removedHashes: prepared.removedHashes,
|
|
11390
|
+
removedGids: prepared.removedGids,
|
|
11260
11391
|
appendCommit,
|
|
11261
11392
|
};
|
|
11262
11393
|
};
|
|
@@ -11288,7 +11419,10 @@ export class SharedLog<
|
|
|
11288
11419
|
prepared.appendFacts,
|
|
11289
11420
|
prepared.removed,
|
|
11290
11421
|
prepared.materializeEntry,
|
|
11291
|
-
{
|
|
11422
|
+
{
|
|
11423
|
+
removedHashes: prepared.removedHashes,
|
|
11424
|
+
removedGids: prepared.removedGids,
|
|
11425
|
+
},
|
|
11292
11426
|
);
|
|
11293
11427
|
try {
|
|
11294
11428
|
await this.completeNativeStrictDurableTransaction(
|
|
@@ -11518,6 +11652,11 @@ export class SharedLog<
|
|
|
11518
11652
|
const nativeTrimmedHashes =
|
|
11519
11653
|
backboneAppend.trimmedHashes ??
|
|
11520
11654
|
backboneAppend.trimmed.map((entry) => entry.hash);
|
|
11655
|
+
const nativeTrimmedGids =
|
|
11656
|
+
backboneAppend.trimmedGids ??
|
|
11657
|
+
(backboneAppend.trimmed.length > 0
|
|
11658
|
+
? backboneAppend.trimmed.map((entry) => entry.gid)
|
|
11659
|
+
: undefined);
|
|
11521
11660
|
const committedHash =
|
|
11522
11661
|
backboneAppend.entry.cid ?? backboneAppend.entry.hash;
|
|
11523
11662
|
const committedNext = backboneAppend.entry.next ?? next;
|
|
@@ -11600,6 +11739,9 @@ export class SharedLog<
|
|
|
11600
11739
|
trimmedEntryHashes: useTrimmedHashesOnly
|
|
11601
11740
|
? backboneAppend.trimmedHashes
|
|
11602
11741
|
: undefined,
|
|
11742
|
+
trimmedEntryGids: useTrimmedHashesOnly
|
|
11743
|
+
? nativeTrimmedGids
|
|
11744
|
+
: undefined,
|
|
11603
11745
|
nativeBlocksDeleted: commitBlocksInBackbone,
|
|
11604
11746
|
nativeDeleteCleanupToken,
|
|
11605
11747
|
documentPreviousContext: backboneAppend.documentPreviousContext,
|
|
@@ -11804,6 +11946,7 @@ export class SharedLog<
|
|
|
11804
11946
|
},
|
|
11805
11947
|
removed: prepared.removed,
|
|
11806
11948
|
removedHashes: prepared.removedHashes,
|
|
11949
|
+
removedGids: prepared.removedGids,
|
|
11807
11950
|
appendCommit,
|
|
11808
11951
|
};
|
|
11809
11952
|
};
|
|
@@ -11930,6 +12073,7 @@ export class SharedLog<
|
|
|
11930
12073
|
{
|
|
11931
12074
|
forgetNativeCoordinates: false,
|
|
11932
12075
|
removedHashes: prepared.removedHashes,
|
|
12076
|
+
removedGids: prepared.removedGids,
|
|
11933
12077
|
},
|
|
11934
12078
|
);
|
|
11935
12079
|
try {
|
|
@@ -11976,6 +12120,7 @@ export class SharedLog<
|
|
|
11976
12120
|
materializeEntry: () => Entry<T>;
|
|
11977
12121
|
removed: ShallowOrFullEntry<T>[];
|
|
11978
12122
|
removedHashes?: string[];
|
|
12123
|
+
removedGids?: string[];
|
|
11979
12124
|
appendFacts: PreparedAppendFacts;
|
|
11980
12125
|
}
|
|
11981
12126
|
| undefined,
|
|
@@ -11998,6 +12143,7 @@ export class SharedLog<
|
|
|
11998
12143
|
result.materializeEntry,
|
|
11999
12144
|
{
|
|
12000
12145
|
removedHashes: result.removedHashes,
|
|
12146
|
+
removedGids: result.removedGids,
|
|
12001
12147
|
ownershipLifecycleController,
|
|
12002
12148
|
},
|
|
12003
12149
|
);
|
|
@@ -12027,6 +12173,7 @@ export class SharedLog<
|
|
|
12027
12173
|
},
|
|
12028
12174
|
removed: result.removed,
|
|
12029
12175
|
removedHashes: result.removedHashes,
|
|
12176
|
+
removedGids: result.removedGids,
|
|
12030
12177
|
appendCommit: this.createPreparedLocalAppendCommitFromFacts(
|
|
12031
12178
|
result.appendFacts,
|
|
12032
12179
|
),
|
|
@@ -12040,6 +12187,7 @@ export class SharedLog<
|
|
|
12040
12187
|
},
|
|
12041
12188
|
removed: result.removed,
|
|
12042
12189
|
removedHashes: result.removedHashes,
|
|
12190
|
+
removedGids: result.removedGids,
|
|
12043
12191
|
appendCommit: this.createPreparedLocalAppendCommitFromFacts(
|
|
12044
12192
|
result.appendFacts,
|
|
12045
12193
|
),
|
|
@@ -12070,6 +12218,7 @@ export class SharedLog<
|
|
|
12070
12218
|
materializeEntry: () => Entry<T>;
|
|
12071
12219
|
removed: ShallowOrFullEntry<T>[];
|
|
12072
12220
|
removedHashes?: string[];
|
|
12221
|
+
removedGids?: string[];
|
|
12073
12222
|
appendFacts: PreparedAppendFacts;
|
|
12074
12223
|
},
|
|
12075
12224
|
options: SharedAppendOptions<T> | undefined,
|
|
@@ -12122,6 +12271,7 @@ export class SharedLog<
|
|
|
12122
12271
|
materializeEntry: () => Entry<T>;
|
|
12123
12272
|
removed: ShallowOrFullEntry<T>[];
|
|
12124
12273
|
removedHashes?: string[];
|
|
12274
|
+
removedGids?: string[];
|
|
12125
12275
|
appendFacts: PreparedAppendFacts;
|
|
12126
12276
|
},
|
|
12127
12277
|
minReplicasValue: number,
|
|
@@ -12147,6 +12297,7 @@ export class SharedLog<
|
|
|
12147
12297
|
},
|
|
12148
12298
|
removed: result.removed,
|
|
12149
12299
|
removedHashes: result.removedHashes,
|
|
12300
|
+
removedGids: result.removedGids,
|
|
12150
12301
|
appendCommit: nativePreparedCommit,
|
|
12151
12302
|
};
|
|
12152
12303
|
}
|
|
@@ -12157,6 +12308,7 @@ export class SharedLog<
|
|
|
12157
12308
|
materializeEntry?: () => Entry<T>;
|
|
12158
12309
|
removed: ShallowOrFullEntry<T>[];
|
|
12159
12310
|
removedHashes?: string[];
|
|
12311
|
+
removedGids?: string[];
|
|
12160
12312
|
change?: Change<T>;
|
|
12161
12313
|
appendFacts: PreparedAppendFacts;
|
|
12162
12314
|
},
|
|
@@ -12208,6 +12360,7 @@ export class SharedLog<
|
|
|
12208
12360
|
forgetNativeCoordinates:
|
|
12209
12361
|
!nativeAppendPlan.committedNativeCoordinateDeletes,
|
|
12210
12362
|
removedHashes: result.removedHashes,
|
|
12363
|
+
removedGids: result.removedGids,
|
|
12211
12364
|
ownershipLifecycleController,
|
|
12212
12365
|
},
|
|
12213
12366
|
);
|
|
@@ -12290,6 +12443,7 @@ export class SharedLog<
|
|
|
12290
12443
|
materializeEntry: () => Entry<T>;
|
|
12291
12444
|
removed: ShallowOrFullEntry<T>[];
|
|
12292
12445
|
removedHashes?: string[];
|
|
12446
|
+
removedGids?: string[];
|
|
12293
12447
|
appendFacts: PreparedAppendFacts;
|
|
12294
12448
|
},
|
|
12295
12449
|
options: SharedAppendOptions<T> | undefined,
|
|
@@ -12310,6 +12464,8 @@ export class SharedLog<
|
|
|
12310
12464
|
return result.entry;
|
|
12311
12465
|
},
|
|
12312
12466
|
removed: result.removed,
|
|
12467
|
+
removedHashes: result.removedHashes,
|
|
12468
|
+
removedGids: result.removedGids,
|
|
12313
12469
|
appendCommit: nativePreparedCommit,
|
|
12314
12470
|
};
|
|
12315
12471
|
}
|
|
@@ -12324,6 +12480,7 @@ export class SharedLog<
|
|
|
12324
12480
|
result.materializeEntry,
|
|
12325
12481
|
{
|
|
12326
12482
|
removedHashes: result.removedHashes,
|
|
12483
|
+
removedGids: result.removedGids,
|
|
12327
12484
|
ownershipLifecycleController,
|
|
12328
12485
|
},
|
|
12329
12486
|
);
|
|
@@ -12385,6 +12542,7 @@ export class SharedLog<
|
|
|
12385
12542
|
},
|
|
12386
12543
|
removed: result.removed,
|
|
12387
12544
|
removedHashes: result.removedHashes,
|
|
12545
|
+
removedGids: result.removedGids,
|
|
12388
12546
|
appendCommit: this.createPreparedLocalAppendCommitFromFacts(
|
|
12389
12547
|
result.appendFacts,
|
|
12390
12548
|
nativeAppendPlan,
|
|
@@ -12711,6 +12869,11 @@ export class SharedLog<
|
|
|
12711
12869
|
nativeIndexMutationLockOwner:
|
|
12712
12870
|
nativeStrictTransaction?.lowerHashMutationLockOwner,
|
|
12713
12871
|
trimmedEntryHashes: append.trimmedHashes,
|
|
12872
|
+
trimmedEntryGids:
|
|
12873
|
+
append.trimmedGids ??
|
|
12874
|
+
(append.trimmed.length > 0
|
|
12875
|
+
? append.trimmed.map((entry) => entry.gid)
|
|
12876
|
+
: undefined),
|
|
12714
12877
|
nativeBlocksDeleted: true,
|
|
12715
12878
|
nativeDeleteCleanupToken,
|
|
12716
12879
|
documentTrimmedHeadsProcessed: append.documentTrimmedHeadsProcessed,
|
|
@@ -13042,6 +13205,11 @@ export class SharedLog<
|
|
|
13042
13205
|
{
|
|
13043
13206
|
forgetNativeCoordinates: false,
|
|
13044
13207
|
removedHashes: plannedCoordinateDeleteHashes,
|
|
13208
|
+
removedGids:
|
|
13209
|
+
backboneAppend.trimmedGids ??
|
|
13210
|
+
(backboneAppend.trimmed.length > 0
|
|
13211
|
+
? backboneAppend.trimmed.map((entry) => entry.gid)
|
|
13212
|
+
: undefined),
|
|
13045
13213
|
},
|
|
13046
13214
|
);
|
|
13047
13215
|
if (!runtimeOnlyCoordinates && this.remoteBlocks.hasNotifyStoredHook()) {
|
|
@@ -14495,6 +14663,7 @@ export class SharedLog<
|
|
|
14495
14663
|
this._appendBackfillPendingByTarget = new Map();
|
|
14496
14664
|
this._repairMetrics = createRepairMetrics();
|
|
14497
14665
|
this._topicSubscribersCache = new Map();
|
|
14666
|
+
this._localReachablePeerHashesCache = new Map();
|
|
14498
14667
|
this._leaderSelectionContextCache = undefined;
|
|
14499
14668
|
this._leaderPlanCache = new LeaderPlanCache({
|
|
14500
14669
|
max: LEADER_PLAN_CACHE_MAX,
|
|
@@ -14715,31 +14884,73 @@ export class SharedLog<
|
|
|
14715
14884
|
// compatible eager path with bounded validation and storage budgets.
|
|
14716
14885
|
eagerBlocks: options?.eagerBlocks ?? false,
|
|
14717
14886
|
resolveProviders: async (cid, opts) => {
|
|
14718
|
-
|
|
14887
|
+
const maxPeers = 8;
|
|
14888
|
+
const localCandidates =
|
|
14889
|
+
(await this.resolveCandidatePeersForHash(cid, {
|
|
14890
|
+
signal: opts?.signal,
|
|
14891
|
+
maxPeers,
|
|
14892
|
+
})) ?? [];
|
|
14893
|
+
if (opts?.signal?.aborted) return [];
|
|
14894
|
+
const locallyReachable = new Set(
|
|
14895
|
+
await this._getLocalReachablePeerHashes(this.topic),
|
|
14896
|
+
);
|
|
14897
|
+
const confirmed = this._checkedPrune.getConfirmedReplicators(cid);
|
|
14898
|
+
const contacted = this._checkedPrune.getContactedReplicators(cid);
|
|
14899
|
+
const hasLiveCandidate = localCandidates.some(
|
|
14900
|
+
(peer) =>
|
|
14901
|
+
locallyReachable.has(peer) &&
|
|
14902
|
+
(confirmed?.has(peer) ||
|
|
14903
|
+
contacted?.has(peer) ||
|
|
14904
|
+
this.uniqueReplicators.has(peer)),
|
|
14905
|
+
);
|
|
14906
|
+
|
|
14907
|
+
// Only reachability corroborated by provider/replicator evidence may
|
|
14908
|
+
// bypass the initial CID lookup. Arbitrary bootstrap connections are
|
|
14909
|
+
// useful fallbacks, but are not evidence that they hold this block.
|
|
14910
|
+
if (hasLiveCandidate && !opts?.refresh) {
|
|
14911
|
+
return localCandidates;
|
|
14912
|
+
}
|
|
14913
|
+
|
|
14914
|
+
let directoryProviders: string[] = [];
|
|
14719
14915
|
try {
|
|
14720
|
-
|
|
14721
|
-
|
|
14722
|
-
|
|
14723
|
-
|
|
14724
|
-
|
|
14725
|
-
|
|
14726
|
-
|
|
14727
|
-
|
|
14728
|
-
|
|
14729
|
-
|
|
14730
|
-
|
|
14916
|
+
directoryProviders =
|
|
14917
|
+
(await fanoutService?.queryProviders(
|
|
14918
|
+
blockProviderNamespace(cid),
|
|
14919
|
+
{
|
|
14920
|
+
want: maxPeers,
|
|
14921
|
+
timeoutMs: 2_000,
|
|
14922
|
+
queryTimeoutMs: 500,
|
|
14923
|
+
bootstrapMaxPeers: 2,
|
|
14924
|
+
signal: opts?.signal,
|
|
14925
|
+
},
|
|
14926
|
+
)) ?? [];
|
|
14731
14927
|
} catch {
|
|
14732
|
-
//
|
|
14928
|
+
// Ignore discovery failures; local evidence remains usable.
|
|
14733
14929
|
}
|
|
14734
14930
|
|
|
14735
|
-
|
|
14736
|
-
|
|
14737
|
-
|
|
14738
|
-
|
|
14739
|
-
|
|
14740
|
-
|
|
14741
|
-
|
|
14742
|
-
|
|
14931
|
+
const selected: string[] = [];
|
|
14932
|
+
const selectedSet = new Set<string>();
|
|
14933
|
+
const add = (peer: string | undefined) => {
|
|
14934
|
+
if (
|
|
14935
|
+
!peer ||
|
|
14936
|
+
selectedSet.has(peer) ||
|
|
14937
|
+
selected.length >= maxPeers
|
|
14938
|
+
) {
|
|
14939
|
+
return;
|
|
14940
|
+
}
|
|
14941
|
+
selectedSet.add(peer);
|
|
14942
|
+
selected.push(peer);
|
|
14943
|
+
};
|
|
14944
|
+
for (
|
|
14945
|
+
let index = 0;
|
|
14946
|
+
selected.length < maxPeers &&
|
|
14947
|
+
(index < localCandidates.length || index < directoryProviders.length);
|
|
14948
|
+
index++
|
|
14949
|
+
) {
|
|
14950
|
+
add(localCandidates[index]);
|
|
14951
|
+
add(directoryProviders[index]);
|
|
14952
|
+
}
|
|
14953
|
+
return selected;
|
|
14743
14954
|
},
|
|
14744
14955
|
watchProviders: fanoutService
|
|
14745
14956
|
? (cid, opts) =>
|
|
@@ -14783,6 +14994,7 @@ export class SharedLog<
|
|
|
14783
14994
|
})) > 0;
|
|
14784
14995
|
|
|
14785
14996
|
this._gidPeersHistory = new Map();
|
|
14997
|
+
this.resetGidPeerHistoryCleanupState();
|
|
14786
14998
|
const replicationChangeOwnershipLifecycleController =
|
|
14787
14999
|
this.captureReplicationOwnershipLifecycle();
|
|
14788
15000
|
let replicationChangeDebounceFn!: typeof this.replicationChangeDebounceFn;
|
|
@@ -16112,73 +16324,99 @@ export class SharedLog<
|
|
|
16112
16324
|
if (options?.signal?.aborted) return undefined;
|
|
16113
16325
|
|
|
16114
16326
|
const maxPeers = options?.maxPeers ?? 8;
|
|
16327
|
+
if (maxPeers <= 0) return undefined;
|
|
16115
16328
|
const self = this.node.identity.publicKey.hashcode();
|
|
16116
16329
|
const seed = hashToSeed32(hash);
|
|
16117
16330
|
|
|
16118
|
-
|
|
16119
|
-
|
|
16120
|
-
|
|
16121
|
-
|
|
16122
|
-
|
|
16123
|
-
|
|
16124
|
-
|
|
16125
|
-
|
|
16331
|
+
// Replication/provider knowledge deliberately outlives a transport session.
|
|
16332
|
+
// That is useful for repair, but it must not fill the bounded fetch target set
|
|
16333
|
+
// with departed peers while a reachable holder is available. The local
|
|
16334
|
+
// snapshot performs no tracker/provider discovery or key resolution.
|
|
16335
|
+
const reachable = await this._getLocalReachablePeerHashes(this.topic);
|
|
16336
|
+
if (options?.signal?.aborted) return undefined;
|
|
16337
|
+
const reachableSet = new Set(reachable);
|
|
16338
|
+
const confirmed = this._checkedPrune.getConfirmedReplicators(hash);
|
|
16126
16339
|
const contacted = this._checkedPrune.getContactedReplicators(hash);
|
|
16127
|
-
|
|
16128
|
-
|
|
16129
|
-
|
|
16130
|
-
|
|
16131
|
-
|
|
16132
|
-
}
|
|
16133
|
-
|
|
16134
|
-
let candidates: string[] | undefined;
|
|
16135
|
-
const replicatorCandidates = [...this.uniqueReplicators].filter(
|
|
16136
|
-
(p) => p !== self,
|
|
16137
|
-
);
|
|
16138
|
-
if (replicatorCandidates.length > 0) {
|
|
16139
|
-
candidates = replicatorCandidates;
|
|
16140
|
-
} else {
|
|
16141
|
-
try {
|
|
16142
|
-
const subscribers = await this._getTopicSubscribers(this.topic);
|
|
16143
|
-
const subscriberCandidates =
|
|
16144
|
-
subscribers?.map((k) => k.hashcode()).filter((p) => p !== self) ?? [];
|
|
16145
|
-
candidates =
|
|
16146
|
-
subscriberCandidates.length > 0 ? subscriberCandidates : undefined;
|
|
16147
|
-
} catch {
|
|
16148
|
-
// Best-effort only.
|
|
16149
|
-
}
|
|
16340
|
+
const evidenceTiers: readonly Iterable<string>[] = [
|
|
16341
|
+
confirmed ?? [],
|
|
16342
|
+
contacted ?? [],
|
|
16343
|
+
this.uniqueReplicators,
|
|
16344
|
+
];
|
|
16150
16345
|
|
|
16151
|
-
|
|
16152
|
-
|
|
16153
|
-
|
|
16154
|
-
|
|
16346
|
+
// Peers supported by both current transport state and provider history are
|
|
16347
|
+
// the strongest signal. Afterwards alternate live-only and historical-only
|
|
16348
|
+
// evidence so either source can widen a saturated bounded set. Derive the
|
|
16349
|
+
// live intersection from the bounded reachability snapshot; never clone or
|
|
16350
|
+
// scan an unbounded historical replicator set per missing block.
|
|
16351
|
+
const liveEvidenceTiers: string[][] = [[], [], []];
|
|
16352
|
+
for (const peer of reachable) {
|
|
16353
|
+
if (confirmed?.has(peer)) liveEvidenceTiers[0].push(peer);
|
|
16354
|
+
else if (contacted?.has(peer)) liveEvidenceTiers[1].push(peer);
|
|
16355
|
+
else if (this.uniqueReplicators.has(peer)) liveEvidenceTiers[2].push(peer);
|
|
16356
|
+
}
|
|
16357
|
+
const liveKnown: string[] = [];
|
|
16358
|
+
for (let tier = 0; tier < liveEvidenceTiers.length; tier++) {
|
|
16359
|
+
liveKnown.push(
|
|
16360
|
+
...pickDeterministicSubset(
|
|
16361
|
+
liveEvidenceTiers[tier],
|
|
16362
|
+
(seed ^ Math.imul(tier + 1, 0x9e3779b1)) >>> 0,
|
|
16363
|
+
maxPeers - liveKnown.length,
|
|
16364
|
+
),
|
|
16365
|
+
);
|
|
16366
|
+
if (liveKnown.length >= maxPeers) break;
|
|
16367
|
+
}
|
|
16368
|
+
const liveKnownSet = new Set(liveKnown);
|
|
16369
|
+
const liveOnly = pickDeterministicSubset(
|
|
16370
|
+
reachable.filter((peer) => !liveKnownSet.has(peer)),
|
|
16371
|
+
(seed ^ 0x85ebca6b) >>> 0,
|
|
16372
|
+
maxPeers,
|
|
16373
|
+
);
|
|
16374
|
+
const historicalOnly: string[] = [];
|
|
16375
|
+
const historicalSeen = new Set<string>();
|
|
16376
|
+
for (let tier = 0; tier < evidenceTiers.length; tier++) {
|
|
16377
|
+
const available: string[] = [];
|
|
16378
|
+
let inspected = 0;
|
|
16379
|
+
for (const peer of evidenceTiers[tier]) {
|
|
16380
|
+
if (inspected++ >= LOCAL_REACHABLE_PEERS_MAX) break;
|
|
16381
|
+
if (
|
|
16382
|
+
!peer ||
|
|
16383
|
+
peer === self ||
|
|
16384
|
+
reachableSet.has(peer) ||
|
|
16385
|
+
historicalSeen.has(peer)
|
|
16386
|
+
) {
|
|
16387
|
+
continue;
|
|
16155
16388
|
}
|
|
16389
|
+
historicalSeen.add(peer);
|
|
16390
|
+
available.push(peer);
|
|
16156
16391
|
}
|
|
16157
|
-
|
|
16158
|
-
|
|
16159
|
-
|
|
16160
|
-
|
|
16161
|
-
|
|
16162
|
-
|
|
16163
|
-
|
|
16164
|
-
|
|
16165
|
-
|
|
16166
|
-
|
|
16167
|
-
|
|
16168
|
-
|
|
16169
|
-
|
|
16170
|
-
|
|
16171
|
-
|
|
16172
|
-
|
|
16173
|
-
|
|
16174
|
-
|
|
16392
|
+
historicalOnly.push(
|
|
16393
|
+
...pickDeterministicSubset(
|
|
16394
|
+
available,
|
|
16395
|
+
(seed ^ Math.imul(tier + 5, 0x9e3779b1)) >>> 0,
|
|
16396
|
+
maxPeers - historicalOnly.length,
|
|
16397
|
+
),
|
|
16398
|
+
);
|
|
16399
|
+
if (historicalOnly.length >= maxPeers) break;
|
|
16400
|
+
}
|
|
16401
|
+
const selected = liveKnown.slice(0, maxPeers);
|
|
16402
|
+
let liveIndex = 0;
|
|
16403
|
+
let historicalIndex = 0;
|
|
16404
|
+
while (
|
|
16405
|
+
selected.length < maxPeers &&
|
|
16406
|
+
(liveIndex < liveOnly.length || historicalIndex < historicalOnly.length)
|
|
16407
|
+
) {
|
|
16408
|
+
if (liveIndex < liveOnly.length) {
|
|
16409
|
+
selected.push(liveOnly[liveIndex++]);
|
|
16410
|
+
}
|
|
16411
|
+
if (
|
|
16412
|
+
selected.length < maxPeers &&
|
|
16413
|
+
historicalIndex < historicalOnly.length
|
|
16414
|
+
) {
|
|
16415
|
+
selected.push(historicalOnly[historicalIndex++]);
|
|
16175
16416
|
}
|
|
16176
16417
|
}
|
|
16177
16418
|
|
|
16178
|
-
|
|
16179
|
-
const peers = candidates.filter((p) => p !== self);
|
|
16180
|
-
if (peers.length === 0) return undefined;
|
|
16181
|
-
return pickDeterministicSubset(peers, seed, maxPeers);
|
|
16419
|
+
return selected.length > 0 ? selected : undefined;
|
|
16182
16420
|
}
|
|
16183
16421
|
|
|
16184
16422
|
async getMemoryUsage() {
|
|
@@ -16580,6 +16818,9 @@ export class SharedLog<
|
|
|
16580
16818
|
(removed) => removed.hash,
|
|
16581
16819
|
);
|
|
16582
16820
|
this.onEntryRemovedHashes(deferredCoordinateDeleteHashes);
|
|
16821
|
+
this.scheduleDeadGidPeerHistoryReclaim(
|
|
16822
|
+
change.removed.map((removed) => removed.meta.gid),
|
|
16823
|
+
);
|
|
16583
16824
|
if (options?.forgetNativeCoordinates === false) {
|
|
16584
16825
|
this._coordinates.forgetResidentCoordinateStateForHashes(
|
|
16585
16826
|
deferredCoordinateDeleteHashes,
|
|
@@ -16610,6 +16851,7 @@ export class SharedLog<
|
|
|
16610
16851
|
options?: {
|
|
16611
16852
|
forgetNativeCoordinates?: boolean;
|
|
16612
16853
|
removedHashes?: string[];
|
|
16854
|
+
removedGids?: string[];
|
|
16613
16855
|
ownershipLifecycleController?: AbortController;
|
|
16614
16856
|
},
|
|
16615
16857
|
): string[] | undefined {
|
|
@@ -16630,6 +16872,11 @@ export class SharedLog<
|
|
|
16630
16872
|
? normalizedHashValues(removedHashes)
|
|
16631
16873
|
: removed.map((entry) => entry.hash);
|
|
16632
16874
|
this.onEntryRemovedHashes(deferredCoordinateDeleteHashes);
|
|
16875
|
+
this.scheduleDeadGidPeerHistoryReclaim(
|
|
16876
|
+
removed.length > 0
|
|
16877
|
+
? removed.map((entry) => entry.meta.gid)
|
|
16878
|
+
: options?.removedGids,
|
|
16879
|
+
);
|
|
16633
16880
|
if (options?.forgetNativeCoordinates === false) {
|
|
16634
16881
|
this._coordinates.forgetResidentCoordinateStateForHashes(
|
|
16635
16882
|
deferredCoordinateDeleteHashes,
|
|
@@ -16663,9 +16910,135 @@ export class SharedLog<
|
|
|
16663
16910
|
}
|
|
16664
16911
|
this.onEntryRemoved(removed.hash);
|
|
16665
16912
|
}
|
|
16913
|
+
this.scheduleDeadGidPeerHistoryReclaim(
|
|
16914
|
+
removedEntries.map((removed) => removed.meta.gid),
|
|
16915
|
+
);
|
|
16666
16916
|
return undefined;
|
|
16667
16917
|
}
|
|
16668
16918
|
|
|
16919
|
+
private async reclaimDeadGidPeerHistory(
|
|
16920
|
+
gids: Iterable<string> | undefined,
|
|
16921
|
+
history = this._gidPeersHistory,
|
|
16922
|
+
): Promise<void> {
|
|
16923
|
+
if (!gids) return;
|
|
16924
|
+
const candidates = [...new Set(gids)].filter(
|
|
16925
|
+
(gid) =>
|
|
16926
|
+
history === this._gidPeersHistory && !!gid && history.has(gid),
|
|
16927
|
+
);
|
|
16928
|
+
if (candidates.length === 0) return;
|
|
16929
|
+
const hasHeads = await this.hasAnyHeadForGidSets(
|
|
16930
|
+
candidates.map((gid) => [gid]),
|
|
16931
|
+
);
|
|
16932
|
+
if (history !== this._gidPeersHistory) return;
|
|
16933
|
+
for (let index = 0; index < candidates.length; index++) {
|
|
16934
|
+
const gid = candidates[index]!;
|
|
16935
|
+
if (!hasHeads[index] && history.has(gid)) {
|
|
16936
|
+
this.deleteGidPeerHistory(gid);
|
|
16937
|
+
}
|
|
16938
|
+
}
|
|
16939
|
+
}
|
|
16940
|
+
|
|
16941
|
+
private resetGidPeerHistoryCleanupState(): void {
|
|
16942
|
+
this._gidPeerHistoryCleanupState = {
|
|
16943
|
+
history: this._gidPeersHistory,
|
|
16944
|
+
tail: Promise.resolve(),
|
|
16945
|
+
pending: new Set(),
|
|
16946
|
+
draining: false,
|
|
16947
|
+
highWater: 0,
|
|
16948
|
+
};
|
|
16949
|
+
}
|
|
16950
|
+
|
|
16951
|
+
private scheduleDeadGidPeerHistoryReclaim(
|
|
16952
|
+
gids: Iterable<string> | undefined,
|
|
16953
|
+
): void {
|
|
16954
|
+
if (!gids) return;
|
|
16955
|
+
const state = this._gidPeerHistoryCleanupState;
|
|
16956
|
+
const history = state.history;
|
|
16957
|
+
for (const gid of gids) {
|
|
16958
|
+
if (!gid || !history.has(gid)) continue;
|
|
16959
|
+
if (state.pending.has(gid)) continue;
|
|
16960
|
+
if (
|
|
16961
|
+
state.pending.size >= GID_PEER_HISTORY_CLEANUP_PENDING_CAPACITY
|
|
16962
|
+
) {
|
|
16963
|
+
// History is only a suppression memo. Under sustained synchronous
|
|
16964
|
+
// production, forgetting an overflow row is the bounded, safe fallback:
|
|
16965
|
+
// it can cause redundant delivery but cannot affect ownership or data.
|
|
16966
|
+
this.deleteGidPeerHistory(gid);
|
|
16967
|
+
continue;
|
|
16968
|
+
}
|
|
16969
|
+
state.pending.add(gid);
|
|
16970
|
+
}
|
|
16971
|
+
state.highWater = Math.max(state.highWater, state.pending.size);
|
|
16972
|
+
if (state.pending.size === 0) return;
|
|
16973
|
+
// A macrotask gate lets sequential awaited appends coalesce; a Promise
|
|
16974
|
+
// microtask runs before the caller can enqueue the next trimmed gid. The
|
|
16975
|
+
// fixed early-wake threshold also amortizes latest-only (retain=1) logs,
|
|
16976
|
+
// while the separate pending cap remains the absolute memory bound.
|
|
16977
|
+
const wakeThreshold = GID_PEER_HISTORY_CLEANUP_BATCH_SIZE;
|
|
16978
|
+
if (state.draining) {
|
|
16979
|
+
if (state.pending.size >= wakeThreshold) state.wake?.();
|
|
16980
|
+
return;
|
|
16981
|
+
}
|
|
16982
|
+
state.draining = true;
|
|
16983
|
+
let wake!: () => void;
|
|
16984
|
+
const batchReady = new Promise<void>((resolve) => {
|
|
16985
|
+
let settled = false;
|
|
16986
|
+
let cancel = () => {};
|
|
16987
|
+
wake = () => {
|
|
16988
|
+
if (settled) return;
|
|
16989
|
+
settled = true;
|
|
16990
|
+
cancel();
|
|
16991
|
+
if (state.wake === wake) state.wake = undefined;
|
|
16992
|
+
resolve();
|
|
16993
|
+
};
|
|
16994
|
+
state.wake = wake;
|
|
16995
|
+
if (typeof setImmediate === "function") {
|
|
16996
|
+
const handle = setImmediate(wake);
|
|
16997
|
+
cancel = () => clearImmediate(handle);
|
|
16998
|
+
} else {
|
|
16999
|
+
const handle = setTimeout(wake, 0);
|
|
17000
|
+
cancel = () => clearTimeout(handle);
|
|
17001
|
+
}
|
|
17002
|
+
});
|
|
17003
|
+
if (state.pending.size >= wakeThreshold) wake();
|
|
17004
|
+
const drain = state.tail
|
|
17005
|
+
.then(async () => {
|
|
17006
|
+
await batchReady;
|
|
17007
|
+
while (history === this._gidPeersHistory) {
|
|
17008
|
+
const batch = [...state.pending].slice(
|
|
17009
|
+
0,
|
|
17010
|
+
GID_PEER_HISTORY_CLEANUP_BATCH_SIZE,
|
|
17011
|
+
);
|
|
17012
|
+
if (batch.length === 0) break;
|
|
17013
|
+
for (const gid of batch) state.pending.delete(gid);
|
|
17014
|
+
try {
|
|
17015
|
+
await this.reclaimDeadGidPeerHistory(batch, history);
|
|
17016
|
+
} catch (error) {
|
|
17017
|
+
warn(`Failed to reclaim gid peer history: ${String(error)}`);
|
|
17018
|
+
if (history === this._gidPeersHistory) {
|
|
17019
|
+
for (const gid of batch) this.deleteGidPeerHistory(gid);
|
|
17020
|
+
}
|
|
17021
|
+
}
|
|
17022
|
+
}
|
|
17023
|
+
})
|
|
17024
|
+
.catch((error) => {
|
|
17025
|
+
warn(`Failed to reclaim gid peer history: ${String(error)}`);
|
|
17026
|
+
});
|
|
17027
|
+
state.tail = drain.finally(() => {
|
|
17028
|
+
state.draining = false;
|
|
17029
|
+
if (history !== this._gidPeersHistory) {
|
|
17030
|
+
state.pending.clear();
|
|
17031
|
+
}
|
|
17032
|
+
if (
|
|
17033
|
+
history === this._gidPeersHistory &&
|
|
17034
|
+
state.pending.size > 0 &&
|
|
17035
|
+
state === this._gidPeerHistoryCleanupState
|
|
17036
|
+
) {
|
|
17037
|
+
this.scheduleDeadGidPeerHistoryReclaim([]);
|
|
17038
|
+
}
|
|
17039
|
+
});
|
|
17040
|
+
}
|
|
17041
|
+
|
|
16669
17042
|
async canAppend(entry: Entry<T>) {
|
|
16670
17043
|
try {
|
|
16671
17044
|
if (!entry.meta.data) {
|
|
@@ -17213,6 +17586,7 @@ export class SharedLog<
|
|
|
17213
17586
|
this.recentlyRebalanced?.clear();
|
|
17214
17587
|
this.uniqueReplicators?.clear();
|
|
17215
17588
|
this._topicSubscribersCache?.clear();
|
|
17589
|
+
this._localReachablePeerHashesCache?.clear();
|
|
17216
17590
|
this._closeController.abort();
|
|
17217
17591
|
clearInterval(this.interval);
|
|
17218
17592
|
this._liveness?.stopReplicatorLivenessSweep();
|
|
@@ -24678,7 +25052,8 @@ export class SharedLog<
|
|
|
24678
25052
|
|
|
24679
25053
|
async rebalanceAll(options?: { clearCache?: boolean }) {
|
|
24680
25054
|
if (options?.clearCache) {
|
|
24681
|
-
this._gidPeersHistory
|
|
25055
|
+
this._gidPeersHistory = new Map();
|
|
25056
|
+
this.resetGidPeerHistoryCleanupState();
|
|
24682
25057
|
this._nativeSharedLogState?.clearGidPeers();
|
|
24683
25058
|
this._nativeBackbone?.clearGidPeers();
|
|
24684
25059
|
}
|
|
@@ -25262,9 +25637,11 @@ export class SharedLog<
|
|
|
25262
25637
|
|
|
25263
25638
|
const fromHash = evt.detail.from.hashcode();
|
|
25264
25639
|
const subscriptionEpoch = this._peerSessions.rotate(fromHash, "opening");
|
|
25265
|
-
this.remoteBlocks.onReachable(evt.detail.from);
|
|
25266
25640
|
this._peerSessions.blockReplicationInfo(fromHash);
|
|
25267
25641
|
this.invalidateSharedLogTopicSubscribersCache();
|
|
25642
|
+
// Invalidate the local reachability snapshot before waking block reads;
|
|
25643
|
+
// their forced resolver runs synchronously from the reachable event.
|
|
25644
|
+
this.remoteBlocks.onReachable(evt.detail.from);
|
|
25268
25645
|
|
|
25269
25646
|
await this.handleSubscriptionChange(
|
|
25270
25647
|
evt.detail.from,
|