@peerbit/shared-log 13.2.22 → 13.2.24

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/dist/src/index.d.ts +12 -0
  2. package/dist/src/index.d.ts.map +1 -1
  3. package/dist/src/index.js +107 -12
  4. package/dist/src/index.js.map +1 -1
  5. package/dist/src/leader-plan-cache.d.ts +33 -0
  6. package/dist/src/leader-plan-cache.d.ts.map +1 -0
  7. package/dist/src/leader-plan-cache.js +49 -0
  8. package/dist/src/leader-plan-cache.js.map +1 -0
  9. package/dist/src/ranges.d.ts +2 -2
  10. package/dist/src/ranges.d.ts.map +1 -1
  11. package/dist/src/ranges.js +10 -8
  12. package/dist/src/ranges.js.map +1 -1
  13. package/package.json +21 -20
  14. package/src/index.ts +171 -12
  15. package/src/leader-plan-cache.ts +61 -0
  16. package/src/ranges.ts +22 -16
  17. package/dist/benchmark/adaptive-ingest.d.ts +0 -2
  18. package/dist/benchmark/adaptive-ingest.d.ts.map +0 -1
  19. package/dist/benchmark/adaptive-ingest.js +0 -275
  20. package/dist/benchmark/adaptive-ingest.js.map +0 -1
  21. package/dist/benchmark/get-samples.d.ts +0 -2
  22. package/dist/benchmark/get-samples.d.ts.map +0 -1
  23. package/dist/benchmark/get-samples.js +0 -194
  24. package/dist/benchmark/get-samples.js.map +0 -1
  25. package/dist/benchmark/index.d.ts +0 -2
  26. package/dist/benchmark/index.d.ts.map +0 -1
  27. package/dist/benchmark/index.js +0 -224
  28. package/dist/benchmark/index.js.map +0 -1
  29. package/dist/benchmark/join-backfill-repair.d.ts +0 -2
  30. package/dist/benchmark/join-backfill-repair.d.ts.map +0 -1
  31. package/dist/benchmark/join-backfill-repair.js +0 -288
  32. package/dist/benchmark/join-backfill-repair.js.map +0 -1
  33. package/dist/benchmark/memory/child.d.ts +0 -2
  34. package/dist/benchmark/memory/child.d.ts.map +0 -1
  35. package/dist/benchmark/memory/child.js +0 -204
  36. package/dist/benchmark/memory/child.js.map +0 -1
  37. package/dist/benchmark/memory/index.d.ts +0 -2
  38. package/dist/benchmark/memory/index.d.ts.map +0 -1
  39. package/dist/benchmark/memory/index.js +0 -81
  40. package/dist/benchmark/memory/index.js.map +0 -1
  41. package/dist/benchmark/memory/utils.d.ts +0 -13
  42. package/dist/benchmark/memory/utils.d.ts.map +0 -1
  43. package/dist/benchmark/memory/utils.js +0 -2
  44. package/dist/benchmark/memory/utils.js.map +0 -1
  45. package/dist/benchmark/native-graph.d.ts +0 -2
  46. package/dist/benchmark/native-graph.d.ts.map +0 -1
  47. package/dist/benchmark/native-graph.js +0 -359
  48. package/dist/benchmark/native-graph.js.map +0 -1
  49. package/dist/benchmark/network-preset-e2e.d.ts +0 -2
  50. package/dist/benchmark/network-preset-e2e.d.ts.map +0 -1
  51. package/dist/benchmark/network-preset-e2e.js +0 -497
  52. package/dist/benchmark/network-preset-e2e.js.map +0 -1
  53. package/dist/benchmark/partial-sync.d.ts +0 -3
  54. package/dist/benchmark/partial-sync.d.ts.map +0 -1
  55. package/dist/benchmark/partial-sync.js +0 -121
  56. package/dist/benchmark/partial-sync.js.map +0 -1
  57. package/dist/benchmark/pid-convergence.d.ts +0 -2
  58. package/dist/benchmark/pid-convergence.d.ts.map +0 -1
  59. package/dist/benchmark/pid-convergence.js +0 -138
  60. package/dist/benchmark/pid-convergence.js.map +0 -1
  61. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts +0 -2
  62. package/dist/benchmark/rateless-iblt-sender-startsync.d.ts.map +0 -1
  63. package/dist/benchmark/rateless-iblt-sender-startsync.js +0 -104
  64. package/dist/benchmark/rateless-iblt-sender-startsync.js.map +0 -1
  65. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts +0 -2
  66. package/dist/benchmark/rateless-iblt-startsync-cache.d.ts.map +0 -1
  67. package/dist/benchmark/rateless-iblt-startsync-cache.js +0 -112
  68. package/dist/benchmark/rateless-iblt-startsync-cache.js.map +0 -1
  69. package/dist/benchmark/receive-prune.d.ts +0 -2
  70. package/dist/benchmark/receive-prune.d.ts.map +0 -1
  71. package/dist/benchmark/receive-prune.js +0 -818
  72. package/dist/benchmark/receive-prune.js.map +0 -1
  73. package/dist/benchmark/replication-prune.d.ts +0 -2
  74. package/dist/benchmark/replication-prune.d.ts.map +0 -1
  75. package/dist/benchmark/replication-prune.js +0 -105
  76. package/dist/benchmark/replication-prune.js.map +0 -1
  77. package/dist/benchmark/replication.d.ts +0 -2
  78. package/dist/benchmark/replication.d.ts.map +0 -1
  79. package/dist/benchmark/replication.js +0 -88
  80. package/dist/benchmark/replication.js.map +0 -1
  81. package/dist/benchmark/sync-batch-sweep.d.ts +0 -2
  82. package/dist/benchmark/sync-batch-sweep.d.ts.map +0 -1
  83. package/dist/benchmark/sync-batch-sweep.js +0 -353
  84. package/dist/benchmark/sync-batch-sweep.js.map +0 -1
  85. package/dist/benchmark/sync-catchup.d.ts +0 -2
  86. package/dist/benchmark/sync-catchup.d.ts.map +0 -1
  87. package/dist/benchmark/sync-catchup.js +0 -277
  88. package/dist/benchmark/sync-catchup.js.map +0 -1
  89. package/dist/benchmark/sync-phase-profile.d.ts +0 -2
  90. package/dist/benchmark/sync-phase-profile.d.ts.map +0 -1
  91. package/dist/benchmark/sync-phase-profile.js +0 -303
  92. package/dist/benchmark/sync-phase-profile.js.map +0 -1
  93. package/dist/benchmark/to-rebalance.d.ts +0 -2
  94. package/dist/benchmark/to-rebalance.d.ts.map +0 -1
  95. package/dist/benchmark/to-rebalance.js +0 -117
  96. package/dist/benchmark/to-rebalance.js.map +0 -1
  97. package/dist/benchmark/utils.d.ts +0 -24
  98. package/dist/benchmark/utils.d.ts.map +0 -1
  99. package/dist/benchmark/utils.js +0 -47
  100. package/dist/benchmark/utils.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@peerbit/shared-log",
3
- "version": "13.2.22",
3
+ "version": "13.2.24",
4
4
  "description": "Shared log",
5
5
  "sideEffects": false,
6
6
  "type": "module",
@@ -24,6 +24,7 @@
24
24
  "files": [
25
25
  "src",
26
26
  "dist",
27
+ "!dist/benchmark",
27
28
  "!dist/e2e",
28
29
  "!dist/test",
29
30
  "!**/*.tsbuildinfo"
@@ -61,36 +62,36 @@
61
62
  "pidusage": "^4.0.1",
62
63
  "pino": "^9.4.0",
63
64
  "uint8arrays": "^5.1.0",
64
- "@peerbit/blocks": "4.2.8",
65
- "@peerbit/blocks-interface": "2.1.4",
65
+ "@peerbit/any-store": "2.2.14",
66
+ "@peerbit/blocks-interface": "2.1.5",
66
67
  "@peerbit/cache": "3.1.1",
67
- "@peerbit/crypto": "3.1.4",
68
- "@peerbit/any-store": "2.2.13",
69
- "@peerbit/indexer-interface": "3.0.8",
68
+ "@peerbit/blocks": "4.2.10",
69
+ "@peerbit/crypto": "3.1.5",
70
70
  "@peerbit/diagnostics": "0.0.1",
71
- "@peerbit/indexer-sqlite3": "3.0.11",
71
+ "@peerbit/indexer-interface": "3.0.9",
72
+ "@peerbit/indexer-sqlite3": "3.0.12",
73
+ "@peerbit/log": "6.2.14",
72
74
  "@peerbit/logger": "2.0.1",
73
- "@peerbit/log": "6.2.12",
74
- "@peerbit/program": "6.0.46",
75
- "@peerbit/pubsub": "5.3.11",
76
- "@peerbit/pubsub-interface": "5.1.9",
77
- "@peerbit/rpc": "6.1.14",
78
- "@peerbit/stream-interface": "6.0.14",
75
+ "@peerbit/program": "6.0.48",
76
+ "@peerbit/pubsub-interface": "5.2.0",
77
+ "@peerbit/riblt": "1.2.0",
78
+ "@peerbit/stream-interface": "6.0.15",
79
79
  "@peerbit/time": "3.0.1",
80
- "@peerbit/riblt": "1.2.0"
80
+ "@peerbit/pubsub": "5.4.0",
81
+ "@peerbit/rpc": "6.1.16"
81
82
  },
82
83
  "optionalDependencies": {
83
- "@peerbit/native-backbone": "0.2.4",
84
- "@peerbit/shared-log-rust": "0.1.3"
84
+ "@peerbit/native-backbone": "0.2.5",
85
+ "@peerbit/shared-log-rust": "0.1.4"
85
86
  },
86
87
  "devDependencies": {
87
88
  "@types/libsodium-wrappers": "^0.7.14",
88
89
  "@types/pidusage": "^2.0.5",
89
90
  "uuid": "^11.1.1",
90
- "@peerbit/indexer-rust": "1.0.6",
91
- "@peerbit/test-utils": "3.1.18",
92
- "peerbit": "5.3.18",
93
- "@peerbit/any-store-rust": "0.1.3"
91
+ "@peerbit/indexer-rust": "1.0.7",
92
+ "@peerbit/test-utils": "3.1.20",
93
+ "@peerbit/any-store-rust": "0.1.4",
94
+ "peerbit": "5.3.20"
94
95
  },
95
96
  "repository": {
96
97
  "type": "git",
package/src/index.ts CHANGED
@@ -186,6 +186,7 @@ import {
186
186
  type Numbers,
187
187
  createNumbers,
188
188
  } from "./integers.js";
189
+ import { LeaderPlanCache } from "./leader-plan-cache.js";
189
190
  import { TransportMessage } from "./message.js";
190
191
  import { PIDReplicationController } from "./pid.js";
191
192
  import {
@@ -1631,6 +1632,20 @@ type WaitForReplicatorsOptions<R extends "u32" | "u64"> =
1631
1632
 
1632
1633
  type WaitForReplicator = { key: string; replicator: boolean };
1633
1634
 
1635
+ type ReceiveLeaderObservation = {
1636
+ isLeader: boolean;
1637
+ fromIsLeader: boolean;
1638
+ };
1639
+
1640
+ // Keep long-lived role callbacks outside the receive activation so event-target
1641
+ // wrappers cannot retain decoded entries through the callback's closure context.
1642
+ const createReceiveLeaderObserver =
1643
+ (observation: ReceiveLeaderObservation, localKey: string, fromKey: string) =>
1644
+ (key: string) => {
1645
+ observation.isLeader ||= localKey === key;
1646
+ observation.fromIsLeader ||= fromKey === key;
1647
+ };
1648
+
1634
1649
  type PendingMaturityRecord<R extends "u32" | "u64"> = {
1635
1650
  range: ReplicationChange<ReplicationRangeIndexable<R>>;
1636
1651
  timeout: ReturnType<typeof setTimeout>;
@@ -2638,6 +2653,14 @@ const RECALCULATE_PARTICIPATION_MIN_RELATIVE_CHANGE_WITH_MEMORY_LIMIT = 0.001;
2638
2653
  const RECALCULATE_PARTICIPATION_RELATIVE_DENOMINATOR_FLOOR = 1e-3;
2639
2654
  const TOPIC_SUBSCRIBERS_CACHE_TTL_MS = 250;
2640
2655
  const LEADER_SELECTION_CONTEXT_CACHE_TTL_MS = 50;
2656
+ // Leader-plan results are invalidated structurally (replication:change,
2657
+ // replicator:mature, subscriber changes). The TTL bounds wall-clock maturity
2658
+ // flips that have no timer at all — a range re-crossing maturity after the
2659
+ // dynamic default roleAge regrew is only visible via expiry — so it is
2660
+ // load-bearing for that edge and must stay at the same order as the
2661
+ // leader-selection context TTL.
2662
+ const LEADER_PLAN_CACHE_TTL_MS = 50;
2663
+ const LEADER_PLAN_CACHE_MAX = 10_000;
2641
2664
  const ADAPTIVE_REBALANCE_IDLE_INTERVAL_MULTIPLIER = 5;
2642
2665
  const ADAPTIVE_REBALANCE_MIN_IDLE_AFTER_LOCAL_APPEND_MS = 10_000;
2643
2666
 
@@ -4536,6 +4559,7 @@ export class SharedLog<
4536
4559
  expiresAt: number;
4537
4560
  context: LeaderSelectionContext;
4538
4561
  };
4562
+ private _leaderPlanCache!: LeaderPlanCache;
4539
4563
  // Sync capability bits advertised by peers (SyncCapabilitiesMessage), keyed
4540
4564
  // by public key hash. Entries are dropped on unsubscribe/disconnect.
4541
4565
  private _peerSyncCapabilities!: Map<string, number>;
@@ -5931,6 +5955,34 @@ export class SharedLog<
5931
5955
  private invalidateLeaderSelectionContextCache() {
5932
5956
  this._receiveOwnershipRevision++;
5933
5957
  this._leaderSelectionContextCache = undefined;
5958
+ this._leaderPlanCache?.invalidate();
5959
+ }
5960
+
5961
+ /**
5962
+ * Cache key for a leader-plan lookup, or undefined when the call is
5963
+ * uncacheable: explicit freshness demands, per-call candidate filters,
5964
+ * roleAge outside the two hot buckets (dynamic default and 0), or an
5965
+ * in-flight replication-range mutation window (a plan observed
5966
+ * mid-transition must not outlive the transition).
5967
+ */
5968
+ private leaderPlanCacheBucket(options?: {
5969
+ roleAge?: number;
5970
+ candidates?: Iterable<string>;
5971
+ freshLeaderPlan?: boolean;
5972
+ }): string | undefined {
5973
+ if (options?.freshLeaderPlan || options?.candidates != null) {
5974
+ return undefined;
5975
+ }
5976
+ if (this._receiveOwnershipMutationAdmissions !== 0) {
5977
+ return undefined;
5978
+ }
5979
+ if (options?.roleAge == null) {
5980
+ return "d";
5981
+ }
5982
+ if (options.roleAge === 0) {
5983
+ return "0";
5984
+ }
5985
+ return undefined;
5934
5986
  }
5935
5987
 
5936
5988
  private isReceiveOwnershipSnapshotStable(revision: number): boolean {
@@ -7027,6 +7079,9 @@ export class SharedLog<
7027
7079
  }
7028
7080
 
7029
7081
  this._isReplicating = true;
7082
+ // Flipping replication mode changes getDefaultMinRoleAge and the
7083
+ // self-replicating context input even when no range is admitted.
7084
+ this.invalidateLeaderSelectionContextCache();
7030
7085
 
7031
7086
  if (isAdaptiveReplicatorOption(options!)) {
7032
7087
  this._isAdaptiveReplicating = true;
@@ -10967,9 +11022,12 @@ export class SharedLog<
10967
11022
 
10968
11023
  try {
10969
11024
  throwIfInactive();
11025
+ // Ownership decisions here feed prune/delete outcomes; never serve
11026
+ // them from the leader-plan cache.
10970
11027
  const currentLeaders = await this.findLeadersFromEntry(
10971
11028
  args.entry,
10972
11029
  decodeReplicas(args.entry).getValue(this),
11030
+ { freshLeaderPlan: true },
10973
11031
  );
10974
11032
  throwIfInactive();
10975
11033
  if (currentLeaders.size > 0) {
@@ -15817,6 +15875,10 @@ export class SharedLog<
15817
15875
  this._repairMetrics = createRepairMetrics();
15818
15876
  this._topicSubscribersCache = new Map();
15819
15877
  this._leaderSelectionContextCache = undefined;
15878
+ this._leaderPlanCache = new LeaderPlanCache({
15879
+ max: LEADER_PLAN_CACHE_MAX,
15880
+ ttl: LEADER_PLAN_CACHE_TTL_MS,
15881
+ });
15820
15882
  this._peerSyncCapabilities = new Map();
15821
15883
  this._liveRawGossipBatches = new Map();
15822
15884
  this._liveRawGossipFlushScheduled = false;
@@ -17300,7 +17362,10 @@ export class SharedLog<
17300
17362
  ) {
17301
17363
  return;
17302
17364
  }
17303
- this.uniqueReplicators.add(keyHash);
17365
+ if (!this.uniqueReplicators.has(keyHash)) {
17366
+ this.uniqueReplicators.add(keyHash);
17367
+ this.invalidateLeaderSelectionContextCache();
17368
+ }
17304
17369
 
17305
17370
  if (!this._replicatorJoinEmitted.has(keyHash)) {
17306
17371
  this._replicatorJoinEmitted.add(keyHash);
@@ -20422,6 +20487,10 @@ export class SharedLog<
20422
20487
  isLeader = true;
20423
20488
  fromIsLeader = leaders.has(contextFromHash);
20424
20489
  } else {
20490
+ const leaderObservation: ReceiveLeaderObservation = {
20491
+ isLeader: false,
20492
+ fromIsLeader: false,
20493
+ };
20425
20494
  leaders = await this._waitForEntryReplicators(
20426
20495
  latestEntry,
20427
20496
  maxMaxReplicas,
@@ -20435,15 +20504,15 @@ export class SharedLog<
20435
20504
  // Let raw receive confirm immediate leadership against the current replicator set.
20436
20505
  roleAge: 0,
20437
20506
  timeout: 2e4,
20438
- onLeader: (key) => {
20439
- isLeader =
20440
- isLeader ||
20441
- this.node.identity.publicKey.hashcode() === key;
20442
- fromIsLeader =
20443
- fromIsLeader || contextFromHash === key;
20444
- },
20507
+ onLeader: createReceiveLeaderObserver(
20508
+ leaderObservation,
20509
+ this.node.identity.publicKey.hashcode(),
20510
+ contextFromHash,
20511
+ ),
20445
20512
  },
20446
20513
  );
20514
+ isLeader = leaderObservation.isLeader;
20515
+ fromIsLeader = leaderObservation.fromIsLeader;
20447
20516
  }
20448
20517
  } else {
20449
20518
  if (plannedLeaders) {
@@ -22229,8 +22298,20 @@ export class SharedLog<
22229
22298
  timeout: this.waitForReplicatorTimeout,
22230
22299
  },
22231
22300
  ): Promise<LeaderMap | false> {
22301
+ // Leader rechecks can outlive the receive call while replication roles
22302
+ // settle. Keep the metadata they need, not the decoded entry's serialized
22303
+ // backing buffer (which can be as large as the transferred block).
22304
+ const leaderSelectionEntry =
22305
+ entry instanceof Entry
22306
+ ? deserialize(
22307
+ Uint8Array.from(serialize(entry.toShallow(true))),
22308
+ ShallowEntry,
22309
+ )
22310
+ : entry instanceof ShallowEntry
22311
+ ? deserialize(Uint8Array.from(serialize(entry)), ShallowEntry)
22312
+ : entry;
22232
22313
  if (
22233
- this.canPlanNativeHashGid(entry) &&
22314
+ this.canPlanNativeHashGid(leaderSelectionEntry) &&
22234
22315
  (this._nativeBackbone ??
22235
22316
  this._nativeSharedLogState ??
22236
22317
  this._nativeRangePlanner)
@@ -22240,7 +22321,7 @@ export class SharedLog<
22240
22321
  options,
22241
22322
  async (checkOptions) => {
22242
22323
  const plan = await this.planEntryLeaders(
22243
- entry,
22324
+ leaderSelectionEntry,
22244
22325
  replicas,
22245
22326
  checkOptions,
22246
22327
  );
@@ -22250,8 +22331,8 @@ export class SharedLog<
22250
22331
  }
22251
22332
 
22252
22333
  return this._waitForReplicators(
22253
- await this.createCoordinates(entry, replicas),
22254
- entry,
22334
+ await this.createCoordinates(leaderSelectionEntry, replicas),
22335
+ leaderSelectionEntry,
22255
22336
  waitFor,
22256
22337
  options,
22257
22338
  );
@@ -24903,6 +24984,43 @@ export class SharedLog<
24903
24984
  }
24904
24985
 
24905
24986
  private async _findLeaders(
24987
+ cursors: NumberFromType<R>[],
24988
+ options?: {
24989
+ roleAge?: number;
24990
+ candidates?: Iterable<string>;
24991
+ freshLeaderPlan?: boolean;
24992
+ },
24993
+ ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
24994
+ ): Promise<Map<string, { intersecting: boolean }>> {
24995
+ const cacheBucket = this.leaderPlanCacheBucket(options);
24996
+ if (cacheBucket == null) {
24997
+ return this._findLeadersUncached(
24998
+ cursors,
24999
+ options,
25000
+ ownershipLifecycleController,
25001
+ );
25002
+ }
25003
+ const cacheKey = `c:${cursors.join(",")}|${cacheBucket}`;
25004
+ const cached = this._leaderPlanCache.get(cacheKey);
25005
+ if (cached) {
25006
+ this.throwIfReplicationOwnershipLifecycleInactive(
25007
+ ownershipLifecycleController,
25008
+ );
25009
+ return cached;
25010
+ }
25011
+ const capturedVersion = this._leaderPlanCache.capture();
25012
+ const leaders = await this._findLeadersUncached(
25013
+ cursors,
25014
+ options,
25015
+ ownershipLifecycleController,
25016
+ );
25017
+ if (this._receiveOwnershipMutationAdmissions === 0) {
25018
+ this._leaderPlanCache.put(cacheKey, leaders, capturedVersion);
25019
+ }
25020
+ return leaders;
25021
+ }
25022
+
25023
+ private async _findLeadersUncached(
24906
25024
  cursors: NumberFromType<R>[],
24907
25025
  options?: {
24908
25026
  roleAge?: number;
@@ -25353,6 +25471,46 @@ export class SharedLog<
25353
25471
  }
25354
25472
 
25355
25473
  private async _findLeadersFromHashGid(
25474
+ gid: string,
25475
+ replicas: number,
25476
+ options?: {
25477
+ roleAge?: number;
25478
+ candidates?: Iterable<string>;
25479
+ freshLeaderPlan?: boolean;
25480
+ },
25481
+ ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
25482
+ ): Promise<Map<string, { intersecting: boolean }> | undefined> {
25483
+ const cacheBucket = this.leaderPlanCacheBucket(options);
25484
+ if (cacheBucket == null) {
25485
+ return this._findLeadersFromHashGidUncached(
25486
+ gid,
25487
+ replicas,
25488
+ options,
25489
+ ownershipLifecycleController,
25490
+ );
25491
+ }
25492
+ const cacheKey = `g:${gid}:${replicas}|${cacheBucket}`;
25493
+ const cached = this._leaderPlanCache.get(cacheKey);
25494
+ if (cached) {
25495
+ this.throwIfReplicationOwnershipLifecycleInactive(
25496
+ ownershipLifecycleController,
25497
+ );
25498
+ return cached;
25499
+ }
25500
+ const capturedVersion = this._leaderPlanCache.capture();
25501
+ const leaders = await this._findLeadersFromHashGidUncached(
25502
+ gid,
25503
+ replicas,
25504
+ options,
25505
+ ownershipLifecycleController,
25506
+ );
25507
+ if (leaders && this._receiveOwnershipMutationAdmissions === 0) {
25508
+ this._leaderPlanCache.put(cacheKey, leaders, capturedVersion);
25509
+ }
25510
+ return leaders;
25511
+ }
25512
+
25513
+ private async _findLeadersFromHashGidUncached(
25356
25514
  gid: string,
25357
25515
  replicas: number,
25358
25516
  options?: {
@@ -25480,6 +25638,7 @@ export class SharedLog<
25480
25638
  replicas: number,
25481
25639
  options?: {
25482
25640
  roleAge?: number;
25641
+ freshLeaderPlan?: boolean;
25483
25642
  },
25484
25643
  ownershipLifecycleController = this.captureReplicationOwnershipLifecycle(),
25485
25644
  ): Promise<Map<string, { intersecting: boolean }>> {
@@ -0,0 +1,61 @@
1
+ import { Cache } from "@peerbit/cache";
2
+
3
+ type LeaderMap = Map<string, { intersecting: boolean }>;
4
+
5
+ const cloneLeaders = (leaders: LeaderMap): LeaderMap => {
6
+ const cloned: LeaderMap = new Map();
7
+ for (const [hash, meta] of leaders) {
8
+ cloned.set(hash, { intersecting: meta.intersecting });
9
+ }
10
+ return cloned;
11
+ };
12
+
13
+ /**
14
+ * Memoizes leader-election plans between replication-topology changes.
15
+ *
16
+ * Coherence is structural, not temporal: a private monotonic version is
17
+ * bumped (and the store wiped) on every invalidation, and an async fill is
18
+ * only stored when the version captured before the computation still holds —
19
+ * a plan computed across a topology change is returned to its caller but
20
+ * never cached. The TTL bounds wall-clock maturity flips that fire no event
21
+ * (a range re-crossing maturity after the dynamic roleAge regrew has no
22
+ * pending timer), so it is load-bearing for that edge.
23
+ *
24
+ * Callers mutate returned maps and their value objects, so every hit and
25
+ * every store clones.
26
+ */
27
+ export class LeaderPlanCache {
28
+ private cache: Cache<{ leaders: LeaderMap }>;
29
+ private version = 0;
30
+
31
+ constructor(options: { max: number; ttl: number }) {
32
+ this.cache = new Cache(options);
33
+ }
34
+
35
+ invalidate(): void {
36
+ this.version++;
37
+ this.cache.clear();
38
+ }
39
+
40
+ get(key: string): LeaderMap | undefined {
41
+ const entry = this.cache.get(key);
42
+ return entry ? cloneLeaders(entry.leaders) : undefined;
43
+ }
44
+
45
+ /** Capture the version before an awaited plan computation. */
46
+ capture(): number {
47
+ return this.version;
48
+ }
49
+
50
+ /** Store only if no invalidation happened since `capturedVersion`. */
51
+ put(key: string, leaders: LeaderMap, capturedVersion: number): void {
52
+ if (capturedVersion !== this.version) {
53
+ return;
54
+ }
55
+ this.cache.add(
56
+ key,
57
+ { leaders: cloneLeaders(leaders) },
58
+ Math.max(1, leaders.size),
59
+ );
60
+ }
61
+ }
package/src/ranges.ts CHANGED
@@ -126,7 +126,7 @@ export class EntryReplicatedU32 implements EntryReplicated<"u32"> {
126
126
  @field({ type: Uint8Array })
127
127
  private _meta: Uint8Array;
128
128
 
129
- private _metaResolved: ShallowMeta;
129
+ private _metaResolved?: ShallowMeta;
130
130
 
131
131
  constructor(properties: {
132
132
  coordinates: number[];
@@ -154,13 +154,16 @@ export class EntryReplicatedU32 implements EntryReplicated<"u32"> {
154
154
  properties.meta?.clock.timestamp.wallTime ?? properties.wallTime!;
155
155
  this.hashNumber = properties.hashNumber;
156
156
  this._meta =
157
- properties.metaBytes ??
158
- serialize(
159
- properties.meta instanceof Meta
160
- ? new ShallowMeta(properties.meta)
161
- : properties.meta!,
162
- );
163
- this._metaResolved = properties.meta as ShallowMeta;
157
+ properties.metaBytes != null
158
+ ? Uint8Array.from(properties.metaBytes)
159
+ : Uint8Array.from(
160
+ serialize(
161
+ properties.meta instanceof Meta
162
+ ? new ShallowMeta(properties.meta)
163
+ : properties.meta!,
164
+ ),
165
+ );
166
+ this._metaResolved = undefined;
164
167
  this.assignedToRangeBoundary = properties.assignedToRangeBoundary;
165
168
  }
166
169
 
@@ -199,7 +202,7 @@ export class EntryReplicatedU64 implements EntryReplicated<"u64"> {
199
202
  @field({ type: Uint8Array })
200
203
  private _meta: Uint8Array;
201
204
 
202
- private _metaResolved: ShallowMeta;
205
+ private _metaResolved?: ShallowMeta;
203
206
 
204
207
  constructor(properties: {
205
208
  coordinates: bigint[];
@@ -227,13 +230,16 @@ export class EntryReplicatedU64 implements EntryReplicated<"u64"> {
227
230
  this.wallTime =
228
231
  properties.meta?.clock.timestamp.wallTime ?? properties.wallTime!;
229
232
  this._meta =
230
- properties.metaBytes ??
231
- serialize(
232
- properties.meta instanceof Meta
233
- ? new ShallowMeta(properties.meta)
234
- : properties.meta!,
235
- );
236
- this._metaResolved = properties.meta as ShallowMeta;
233
+ properties.metaBytes != null
234
+ ? Uint8Array.from(properties.metaBytes)
235
+ : Uint8Array.from(
236
+ serialize(
237
+ properties.meta instanceof Meta
238
+ ? new ShallowMeta(properties.meta)
239
+ : properties.meta!,
240
+ ),
241
+ );
242
+ this._metaResolved = undefined;
237
243
  this.assignedToRangeBoundary = properties.assignedToRangeBoundary;
238
244
  }
239
245
 
@@ -1,2 +0,0 @@
1
- export {};
2
- //# sourceMappingURL=adaptive-ingest.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"adaptive-ingest.d.ts","sourceRoot":"","sources":["../../benchmark/adaptive-ingest.ts"],"names":[],"mappings":""}