@peerbit/shared-log 13.2.35 → 14.0.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/errors.d.ts +11 -0
- package/dist/src/errors.d.ts.map +1 -1
- package/dist/src/errors.js +16 -0
- package/dist/src/errors.js.map +1 -1
- package/dist/src/index.d.ts +21 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +115 -16
- package/dist/src/index.js.map +1 -1
- package/dist/src/replication-info-v2-receive.d.ts +10 -0
- package/dist/src/replication-info-v2-receive.d.ts.map +1 -1
- package/dist/src/replication-info-v2-receive.js +32 -1
- package/dist/src/replication-info-v2-receive.js.map +1 -1
- package/package.json +8 -8
- package/src/errors.ts +19 -0
- package/src/index.ts +143 -24
- package/src/replication-info-v2-receive.ts +40 -1
package/src/index.ts
CHANGED
|
@@ -144,6 +144,7 @@ import {
|
|
|
144
144
|
debouncedAccumulatorMap,
|
|
145
145
|
} from "./debounce.js";
|
|
146
146
|
import {
|
|
147
|
+
CompatibilityModeRetiredError,
|
|
147
148
|
NativeDurableCommitError,
|
|
148
149
|
NoPeersError,
|
|
149
150
|
isNotStartedError,
|
|
@@ -428,6 +429,7 @@ export {
|
|
|
428
429
|
EntryReplicatedU32,
|
|
429
430
|
EntryReplicatedU64,
|
|
430
431
|
type CoverRange,
|
|
432
|
+
CompatibilityModeRetiredError,
|
|
431
433
|
NativeDurableCommitError,
|
|
432
434
|
NoPeersError,
|
|
433
435
|
};
|
|
@@ -1248,7 +1250,6 @@ export type SharedLogOptions<
|
|
|
1248
1250
|
waitForPruneDelay?: number;
|
|
1249
1251
|
distributionDebounceTime?: number;
|
|
1250
1252
|
strictFullReplicaFallback?: boolean;
|
|
1251
|
-
compatibility?: number;
|
|
1252
1253
|
domain?: ReplicationDomainConstructor<D>;
|
|
1253
1254
|
eagerBlocks?: EagerBlocksSetting;
|
|
1254
1255
|
fanout?: SharedLogFanoutOptions;
|
|
@@ -1346,6 +1347,13 @@ export const WAIT_FOR_REPLICATOR_TIMEOUT = 20000;
|
|
|
1346
1347
|
export const WAIT_FOR_ROLE_MATURITY = 5000;
|
|
1347
1348
|
export const WAIT_FOR_REPLICATOR_REQUEST_INTERVAL = 1000;
|
|
1348
1349
|
export const WAIT_FOR_REPLICATOR_REQUEST_MIN_ATTEMPTS = 3;
|
|
1350
|
+
// The V2 recovery scheduler is deliberately persistent (a subscribed peer is
|
|
1351
|
+
// re-solicited for as long as its topic session stays open), but consecutive
|
|
1352
|
+
// fruitless park/unpark cycles double the wait before the next unpark so a
|
|
1353
|
+
// silent-but-subscribed peer converges to one bounded request cycle per cap
|
|
1354
|
+
// window instead of one per base interval. Any applied V2 progress resets it.
|
|
1355
|
+
export const REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_DELAY = 300_000;
|
|
1356
|
+
const REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_EXPONENT = 20;
|
|
1349
1357
|
// TODO(prune): Investigate if/when a non-zero prune delay is required for correctness
|
|
1350
1358
|
// (e.g. responsibility/replication-info message reordering in multi-peer scenarios).
|
|
1351
1359
|
// Prefer making pruning robust without timing-based heuristics.
|
|
@@ -2087,12 +2095,22 @@ export class SharedLog<
|
|
|
2087
2095
|
private _replicationInfoRequestByPeer!: Map<
|
|
2088
2096
|
string,
|
|
2089
2097
|
{
|
|
2098
|
+
// Legacy scheduler: sends issued (bounded by maxAttempts). V2 recovery
|
|
2099
|
+
// scheduler: consecutive fruitless unparks — the escalation exponent
|
|
2100
|
+
// for the next unpark delay, reset on any applied V2 progress.
|
|
2090
2101
|
attempts: number;
|
|
2091
2102
|
timer?: ReturnType<typeof setTimeout>;
|
|
2092
2103
|
peerSession?: PeerSession;
|
|
2104
|
+
// V2 recovery scheduler only: when the current park was first observed.
|
|
2105
|
+
parkedSinceMs?: number;
|
|
2093
2106
|
}
|
|
2094
2107
|
>;
|
|
2095
2108
|
private _replicationInfoApplyQueueByPeer!: Map<string, Promise<void>>;
|
|
2109
|
+
// One in-flight targeted subscriber-snapshot request per session-less peer.
|
|
2110
|
+
// A capability burst from a peer whose Subscribe has not been observed must
|
|
2111
|
+
// coalesce into a single pubsub.requestSubscribers call (mirrors the
|
|
2112
|
+
// waitForReplicator in-flight coalescing); a later burst may request again.
|
|
2113
|
+
private _subscriberSnapshotRequestsByPeer!: Map<string, Promise<void>>;
|
|
2096
2114
|
// Range ids are global primary keys while receive lanes are per peer. Keep
|
|
2097
2115
|
// reads and writes that decide one mutation in a single global lane.
|
|
2098
2116
|
private _replicationRangeMutationTail: Promise<void> = Promise.resolve();
|
|
@@ -3633,6 +3651,7 @@ export class SharedLog<
|
|
|
3633
3651
|
this._pendingIHaveCallbacks = new Set();
|
|
3634
3652
|
this.latestReplicationInfoMessage = new Map();
|
|
3635
3653
|
this._replicationInfoRequestByPeer = new Map();
|
|
3654
|
+
this._subscriberSnapshotRequestsByPeer = new Map();
|
|
3636
3655
|
this._replicationInfoApplyQueueByPeer = new Map();
|
|
3637
3656
|
// The registry constructor runs resetForOpen(), which creates the
|
|
3638
3657
|
// replication-info blocked set (fence B5) alongside the session maps —
|
|
@@ -3742,7 +3761,10 @@ export class SharedLog<
|
|
|
3742
3761
|
}
|
|
3743
3762
|
|
|
3744
3763
|
get compatibility(): number | undefined {
|
|
3745
|
-
|
|
3764
|
+
// B12: the open option was removed and any defined value rejects at
|
|
3765
|
+
// open(); this is permanently undefined and dies with the residual
|
|
3766
|
+
// gates in a later cleanup stage.
|
|
3767
|
+
return (this._logProperties as any)?.compatibility;
|
|
3746
3768
|
}
|
|
3747
3769
|
|
|
3748
3770
|
/**
|
|
@@ -4282,6 +4304,9 @@ export class SharedLog<
|
|
|
4282
4304
|
);
|
|
4283
4305
|
if (generationAdvanced) {
|
|
4284
4306
|
this._v2Send.advancePeerCapability(peerHash);
|
|
4307
|
+
// A fresh signed capability generation is V2 progress from the peer:
|
|
4308
|
+
// recovery re-solicitation may restart from the base interval.
|
|
4309
|
+
this.resetReplicationInfoV2RecoveryEscalation(peerHash);
|
|
4285
4310
|
}
|
|
4286
4311
|
return true;
|
|
4287
4312
|
}
|
|
@@ -4312,6 +4337,36 @@ export class SharedLog<
|
|
|
4312
4337
|
});
|
|
4313
4338
|
}
|
|
4314
4339
|
|
|
4340
|
+
/**
|
|
4341
|
+
* Coalesced targeted subscriber-snapshot request for the
|
|
4342
|
+
* capability-before-Subscribe recovery path. The observed-capability gate
|
|
4343
|
+
* is sender-paced (any advancing timestamp passes), so a burst of frames
|
|
4344
|
+
* from one session-less peer must not fan out into one GetSubscribers
|
|
4345
|
+
* unicast per frame. One request per peer is in flight at a time; once it
|
|
4346
|
+
* settles, a genuinely new session-less capability may request again.
|
|
4347
|
+
*/
|
|
4348
|
+
private requestSubscriberSnapshotForCapability(target: PublicSignKey): void {
|
|
4349
|
+
const peerHash = target.hashcode();
|
|
4350
|
+
if (this._subscriberSnapshotRequestsByPeer.has(peerHash)) {
|
|
4351
|
+
return;
|
|
4352
|
+
}
|
|
4353
|
+
const request = Promise.resolve()
|
|
4354
|
+
.then(() =>
|
|
4355
|
+
this.node.services.pubsub.requestSubscribers(this.topic, target),
|
|
4356
|
+
)
|
|
4357
|
+
.catch((error) => {
|
|
4358
|
+
if (!isNotStartedError(error as Error)) {
|
|
4359
|
+
logger.error(error?.toString?.() ?? String(error));
|
|
4360
|
+
}
|
|
4361
|
+
})
|
|
4362
|
+
.finally(() => {
|
|
4363
|
+
if (this._subscriberSnapshotRequestsByPeer.get(peerHash) === request) {
|
|
4364
|
+
this._subscriberSnapshotRequestsByPeer.delete(peerHash);
|
|
4365
|
+
}
|
|
4366
|
+
});
|
|
4367
|
+
this._subscriberSnapshotRequestsByPeer.set(peerHash, request);
|
|
4368
|
+
}
|
|
4369
|
+
|
|
4315
4370
|
/**
|
|
4316
4371
|
* Live append gossip may use the raw exchange-heads path only when we
|
|
4317
4372
|
* opted into raw sync and every remote recipient advertised raw capability
|
|
@@ -14115,6 +14170,17 @@ export class SharedLog<
|
|
|
14115
14170
|
}
|
|
14116
14171
|
|
|
14117
14172
|
async open(options?: Args<T, D, R>): Promise<void> {
|
|
14173
|
+
// B12: replication-info network compatibility modes are retired. Read the
|
|
14174
|
+
// RAW argument value (the option no longer exists on the type) so untyped
|
|
14175
|
+
// JS callers cannot smuggle a value past the removed field, and reject
|
|
14176
|
+
// ANY defined value — including 10, which previously behaved like the
|
|
14177
|
+
// default — BEFORE any open-time side effect (rpc.open, index/native
|
|
14178
|
+
// setup, domain resolution, synchronizer creation, subscription setup).
|
|
14179
|
+
// An explicitly-present `undefined` stays accepted.
|
|
14180
|
+
const rawCompatibility = (options as any)?.compatibility;
|
|
14181
|
+
if (rawCompatibility !== undefined) {
|
|
14182
|
+
throw new CompatibilityModeRetiredError(rawCompatibility);
|
|
14183
|
+
}
|
|
14118
14184
|
this.ensureNativeDurabilityRuntimeState();
|
|
14119
14185
|
this._nativeStrictDurableTransactionsClosing = false;
|
|
14120
14186
|
this._replicationRangeMutationsClosing = false;
|
|
@@ -14176,7 +14242,8 @@ export class SharedLog<
|
|
|
14176
14242
|
this.domain = options?.domain
|
|
14177
14243
|
? (options.domain(this) as unknown as D)
|
|
14178
14244
|
: (createReplicationDomainHash(
|
|
14179
|
-
options?.compatibility !== undefined &&
|
|
14245
|
+
(options as any)?.compatibility !== undefined &&
|
|
14246
|
+
(options as any).compatibility < 10
|
|
14180
14247
|
? "u32"
|
|
14181
14248
|
: "u64",
|
|
14182
14249
|
)(this) as unknown as D);
|
|
@@ -14191,6 +14258,7 @@ export class SharedLog<
|
|
|
14191
14258
|
this._pendingIHaveCallbacks = new Set();
|
|
14192
14259
|
this.latestReplicationInfoMessage = new Map();
|
|
14193
14260
|
this._replicationInfoRequestByPeer = new Map();
|
|
14261
|
+
this._subscriberSnapshotRequestsByPeer = new Map();
|
|
14194
14262
|
// Terminal close/drop drains the previous lifecycle before another open can
|
|
14195
14263
|
// install fresh lanes and opaque per-subscription ownership tokens.
|
|
14196
14264
|
this._replicationInfoApplyQueueByPeer = new Map();
|
|
@@ -14785,7 +14853,7 @@ export class SharedLog<
|
|
|
14785
14853
|
sendOptions?: { priority?: number; signal?: AbortSignal },
|
|
14786
14854
|
) => this.trySendFusedRawExchangeHeads(hashes, to, sendOptions),
|
|
14787
14855
|
warn,
|
|
14788
|
-
compatibility: this._logProperties?.compatibility,
|
|
14856
|
+
compatibility: (this._logProperties as any)?.compatibility,
|
|
14789
14857
|
resolution: this.domain.resolution,
|
|
14790
14858
|
sync: options?.sync,
|
|
14791
14859
|
syncronizer: options?.syncronizer,
|
|
@@ -19384,18 +19452,7 @@ export class SharedLog<
|
|
|
19384
19452
|
// subscriber snapshot; the resulting Subscribe creates the real
|
|
19385
19453
|
// PeerSession and completes the symmetric capability handshake.
|
|
19386
19454
|
// Never synthesize membership from capability traffic alone.
|
|
19387
|
-
|
|
19388
|
-
.then(() =>
|
|
19389
|
-
this.node.services.pubsub.requestSubscribers(
|
|
19390
|
-
this.topic,
|
|
19391
|
-
context.from,
|
|
19392
|
-
),
|
|
19393
|
-
)
|
|
19394
|
-
.catch((error) => {
|
|
19395
|
-
if (!isNotStartedError(error as Error)) {
|
|
19396
|
-
logger.error(error?.toString?.() ?? String(error));
|
|
19397
|
-
}
|
|
19398
|
-
});
|
|
19455
|
+
this.requestSubscriberSnapshotForCapability(context.from);
|
|
19399
19456
|
}
|
|
19400
19457
|
}
|
|
19401
19458
|
}
|
|
@@ -19963,6 +20020,9 @@ export class SharedLog<
|
|
|
19963
20020
|
return;
|
|
19964
20021
|
}
|
|
19965
20022
|
this._liveness.markReplicatorActivity(fromHash);
|
|
20023
|
+
// A committed V2 announcement is applied progress: the peer answers,
|
|
20024
|
+
// so recovery re-solicitation may restart from the base interval.
|
|
20025
|
+
this.resetReplicationInfoV2RecoveryEscalation(fromHash);
|
|
19966
20026
|
if (
|
|
19967
20027
|
msg instanceof FullReplicationInfoV2Message &&
|
|
19968
20028
|
this.legacyReplicationInfoEnabled
|
|
@@ -23432,6 +23492,22 @@ export class SharedLog<
|
|
|
23432
23492
|
this._replicationInfoRequestByPeer.delete(peerHash);
|
|
23433
23493
|
}
|
|
23434
23494
|
|
|
23495
|
+
/**
|
|
23496
|
+
* Applied V2 progress from a peer (a committed Full/Added/Stopped, or a
|
|
23497
|
+
* rotated capability generation) proves the peer answers. Reset the
|
|
23498
|
+
* recovery scheduler's unpark escalation so a later stall restarts from
|
|
23499
|
+
* the base interval. Peer-session rotation resets implicitly: the recovery
|
|
23500
|
+
* scheduler creates a fresh per-session state.
|
|
23501
|
+
*/
|
|
23502
|
+
private resetReplicationInfoV2RecoveryEscalation(peerHash: string) {
|
|
23503
|
+
const state = this._replicationInfoRequestByPeer.get(peerHash);
|
|
23504
|
+
if (!state || state.peerSession === undefined) {
|
|
23505
|
+
return;
|
|
23506
|
+
}
|
|
23507
|
+
state.attempts = 0;
|
|
23508
|
+
state.parkedSinceMs = undefined;
|
|
23509
|
+
}
|
|
23510
|
+
|
|
23435
23511
|
private scheduleReplicationInfoV2Recovery(
|
|
23436
23512
|
peer: PublicSignKey,
|
|
23437
23513
|
replicationLifecycleController = this._instanceLifecycle
|
|
@@ -23463,6 +23539,7 @@ export class SharedLog<
|
|
|
23463
23539
|
attempts: number;
|
|
23464
23540
|
timer?: ReturnType<typeof setTimeout>;
|
|
23465
23541
|
peerSession: PeerSession;
|
|
23542
|
+
parkedSinceMs?: number;
|
|
23466
23543
|
} = {
|
|
23467
23544
|
attempts: 0,
|
|
23468
23545
|
peerSession,
|
|
@@ -23478,6 +23555,24 @@ export class SharedLog<
|
|
|
23478
23555
|
requestStates.delete(peerHash);
|
|
23479
23556
|
};
|
|
23480
23557
|
const intervalMs = Math.max(50, this.waitForReplicatorRequestIntervalMs);
|
|
23558
|
+
const maxUnparkDelayMs = Math.max(
|
|
23559
|
+
intervalMs,
|
|
23560
|
+
REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_DELAY,
|
|
23561
|
+
);
|
|
23562
|
+
const unparkDelayMs = () =>
|
|
23563
|
+
Math.min(
|
|
23564
|
+
maxUnparkDelayMs,
|
|
23565
|
+
intervalMs *
|
|
23566
|
+
2 **
|
|
23567
|
+
Math.min(
|
|
23568
|
+
state.attempts,
|
|
23569
|
+
REPLICATION_INFO_V2_RECOVERY_MAX_UNPARK_EXPONENT,
|
|
23570
|
+
),
|
|
23571
|
+
);
|
|
23572
|
+
const arm = (delayMs: number) => {
|
|
23573
|
+
state.timer = setTimeout(tick, delayMs);
|
|
23574
|
+
state.timer.unref?.();
|
|
23575
|
+
};
|
|
23481
23576
|
const tick = () => {
|
|
23482
23577
|
if (
|
|
23483
23578
|
!this.isReplicationLifecycleActive(replicationLifecycleController) ||
|
|
@@ -23487,14 +23582,38 @@ export class SharedLog<
|
|
|
23487
23582
|
cancel();
|
|
23488
23583
|
return;
|
|
23489
23584
|
}
|
|
23490
|
-
this.
|
|
23491
|
-
|
|
23492
|
-
peerSession,
|
|
23493
|
-
|
|
23494
|
-
|
|
23495
|
-
|
|
23496
|
-
|
|
23497
|
-
|
|
23585
|
+
const receiveEpoch = this._peerSessions.receiveEpoch(peerHash);
|
|
23586
|
+
if (
|
|
23587
|
+
!this._v2Receive.isRequestParked({ peerHash, peerSession, receiveEpoch })
|
|
23588
|
+
) {
|
|
23589
|
+
// Active, or a bounded request cycle is still running its own
|
|
23590
|
+
// exponential retries. Keep polling for the next park.
|
|
23591
|
+
state.parkedSinceMs = undefined;
|
|
23592
|
+
arm(intervalMs);
|
|
23593
|
+
return;
|
|
23594
|
+
}
|
|
23595
|
+
const now = Date.now();
|
|
23596
|
+
if (state.parkedSinceMs === undefined) {
|
|
23597
|
+
state.parkedSinceMs = now;
|
|
23598
|
+
}
|
|
23599
|
+
const resumeAtMs = state.parkedSinceMs + unparkDelayMs();
|
|
23600
|
+
if (now < resumeAtMs) {
|
|
23601
|
+
arm(Math.max(50, resumeAtMs - now));
|
|
23602
|
+
return;
|
|
23603
|
+
}
|
|
23604
|
+
if (
|
|
23605
|
+
this._v2Receive.resumeParkedRequest({
|
|
23606
|
+
peerHash,
|
|
23607
|
+
peerSession,
|
|
23608
|
+
receiveEpoch,
|
|
23609
|
+
})
|
|
23610
|
+
) {
|
|
23611
|
+
// Fruitless until proven otherwise: escalate the next unpark wait.
|
|
23612
|
+
// Applied progress resets via resetReplicationInfoV2RecoveryEscalation.
|
|
23613
|
+
state.attempts++;
|
|
23614
|
+
state.parkedSinceMs = undefined;
|
|
23615
|
+
}
|
|
23616
|
+
arm(intervalMs);
|
|
23498
23617
|
};
|
|
23499
23618
|
tick();
|
|
23500
23619
|
}
|
|
@@ -991,6 +991,28 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
991
991
|
return true;
|
|
992
992
|
}
|
|
993
993
|
|
|
994
|
+
/**
|
|
995
|
+
* Whether the request generation for this exact peer state is parked: the
|
|
996
|
+
* bounded retry cycle exhausted its attempts and no timer or send is in
|
|
997
|
+
* flight. Read-only probe for the host's recovery scheduler.
|
|
998
|
+
*/
|
|
999
|
+
isRequestParked(properties: {
|
|
1000
|
+
peerHash: string;
|
|
1001
|
+
peerSession: object;
|
|
1002
|
+
receiveEpoch: object | null;
|
|
1003
|
+
}): boolean {
|
|
1004
|
+
const state = this._receiveStates.get(properties.peerHash);
|
|
1005
|
+
return (
|
|
1006
|
+
state !== undefined &&
|
|
1007
|
+
state.peerSession === properties.peerSession &&
|
|
1008
|
+
state.receiveEpoch === properties.receiveEpoch &&
|
|
1009
|
+
state.phase !== "active" &&
|
|
1010
|
+
state.requestParked &&
|
|
1011
|
+
state.requestTimer === undefined &&
|
|
1012
|
+
state.requestInFlight === undefined
|
|
1013
|
+
);
|
|
1014
|
+
}
|
|
1015
|
+
|
|
994
1016
|
/**
|
|
995
1017
|
* Resume only a request generation that exhausted its bounded retries.
|
|
996
1018
|
* Wait/liveness callers may nudge recovery without invalidating an active,
|
|
@@ -1023,7 +1045,24 @@ export class ReplicationInfoV2ReceiveCoordinator {
|
|
|
1023
1045
|
}
|
|
1024
1046
|
state.requestAttempts = 0;
|
|
1025
1047
|
state.requestsSinceCapabilityRefresh = 0;
|
|
1026
|
-
state.capabilityRefreshRequired
|
|
1048
|
+
if (!state.capabilityRefreshRequired) {
|
|
1049
|
+
// Only rotate the grant when it is genuinely stale: the peer's
|
|
1050
|
+
// capability rotation paths already flagged a refresh, and a missing
|
|
1051
|
+
// or transport-outdated local grant cannot authorize a request. A
|
|
1052
|
+
// still-current grant must keep its challenge so a Full already in
|
|
1053
|
+
// flight for the pre-park request generation still applies.
|
|
1054
|
+
const ready = this._localCapabilityReadyBySession.get(state.peerSession);
|
|
1055
|
+
const grantCurrent =
|
|
1056
|
+
ready !== undefined &&
|
|
1057
|
+
ready.peerHash === state.peerHash &&
|
|
1058
|
+
ready.receiveEpoch === state.receiveEpoch &&
|
|
1059
|
+
ready.receiverTransportSession === state.receiverTransportSession &&
|
|
1060
|
+
ready.receiverTransportSession ===
|
|
1061
|
+
this.deps.getReceiverTransportSession();
|
|
1062
|
+
if (!grantCurrent) {
|
|
1063
|
+
state.capabilityRefreshRequired = true;
|
|
1064
|
+
}
|
|
1065
|
+
}
|
|
1027
1066
|
state.requestParked = false;
|
|
1028
1067
|
this.armRequest(state, 0);
|
|
1029
1068
|
return true;
|