@peerbit/pubsub 5.2.11 → 5.3.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.
Files changed (67) hide show
  1. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts +3 -0
  2. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.d.ts.map +1 -0
  3. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js +90 -0
  4. package/dist/benchmark/fanout-tree-parent-upgrade-default-ready.js.map +1 -0
  5. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts +2 -0
  6. package/dist/benchmark/fanout-tree-parent-upgrade-eval.d.ts.map +1 -0
  7. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js +712 -0
  8. package/dist/benchmark/fanout-tree-parent-upgrade-eval.js.map +1 -0
  9. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts +2 -0
  10. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.d.ts.map +1 -0
  11. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js +1591 -0
  12. package/dist/benchmark/fanout-tree-parent-upgrade-multi-eval.js.map +1 -0
  13. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts +2 -0
  14. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.d.ts.map +1 -0
  15. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js +297 -0
  16. package/dist/benchmark/fanout-tree-parent-upgrade-prepush.js.map +1 -0
  17. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts +73 -0
  18. package/dist/benchmark/fanout-tree-parent-upgrade-preset.d.ts.map +1 -0
  19. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js +183 -0
  20. package/dist/benchmark/fanout-tree-parent-upgrade-preset.js.map +1 -0
  21. package/dist/benchmark/fanout-tree-sim-lib.d.ts +101 -0
  22. package/dist/benchmark/fanout-tree-sim-lib.d.ts.map +1 -1
  23. package/dist/benchmark/fanout-tree-sim-lib.js +540 -38
  24. package/dist/benchmark/fanout-tree-sim-lib.js.map +1 -1
  25. package/dist/benchmark/fanout-tree-sim.d.ts +0 -9
  26. package/dist/benchmark/fanout-tree-sim.d.ts.map +1 -1
  27. package/dist/benchmark/fanout-tree-sim.js +369 -28
  28. package/dist/benchmark/fanout-tree-sim.js.map +1 -1
  29. package/dist/benchmark/index.js +20 -0
  30. package/dist/benchmark/index.js.map +1 -1
  31. package/dist/benchmark/pubsub-topic-sim-lib.d.ts +4 -0
  32. package/dist/benchmark/pubsub-topic-sim-lib.d.ts.map +1 -1
  33. package/dist/benchmark/pubsub-topic-sim-lib.js +58 -3
  34. package/dist/benchmark/pubsub-topic-sim-lib.js.map +1 -1
  35. package/dist/benchmark/pubsub-topic-sim.js +18 -0
  36. package/dist/benchmark/pubsub-topic-sim.js.map +1 -1
  37. package/dist/src/debounced-set.d.ts +1 -1
  38. package/dist/src/debounced-set.d.ts.map +1 -1
  39. package/dist/src/debounced-set.js +5 -1
  40. package/dist/src/debounced-set.js.map +1 -1
  41. package/dist/src/fanout-tree-codec.d.ts +330 -0
  42. package/dist/src/fanout-tree-codec.d.ts.map +1 -0
  43. package/dist/src/fanout-tree-codec.js +1209 -0
  44. package/dist/src/fanout-tree-codec.js.map +1 -0
  45. package/dist/src/fanout-tree-parent-upgrade.d.ts +117 -0
  46. package/dist/src/fanout-tree-parent-upgrade.d.ts.map +1 -0
  47. package/dist/src/fanout-tree-parent-upgrade.js +135 -0
  48. package/dist/src/fanout-tree-parent-upgrade.js.map +1 -0
  49. package/dist/src/fanout-tree.d.ts +241 -73
  50. package/dist/src/fanout-tree.d.ts.map +1 -1
  51. package/dist/src/fanout-tree.js +2224 -1050
  52. package/dist/src/fanout-tree.js.map +1 -1
  53. package/dist/src/index.d.ts +57 -0
  54. package/dist/src/index.d.ts.map +1 -1
  55. package/dist/src/index.js +293 -41
  56. package/dist/src/index.js.map +1 -1
  57. package/dist/src/topic-root-control-plane.d.ts +13 -0
  58. package/dist/src/topic-root-control-plane.d.ts.map +1 -1
  59. package/dist/src/topic-root-control-plane.js +45 -0
  60. package/dist/src/topic-root-control-plane.js.map +1 -1
  61. package/package.json +7 -7
  62. package/src/debounced-set.ts +5 -0
  63. package/src/fanout-tree-codec.ts +1580 -0
  64. package/src/fanout-tree-parent-upgrade.ts +353 -0
  65. package/src/fanout-tree.ts +4467 -2204
  66. package/src/index.ts +392 -56
  67. package/src/topic-root-control-plane.ts +48 -0
@@ -1,10 +1,11 @@
1
1
  import { PreHash, SignatureWithKey } from "@peerbit/crypto";
2
+ import { InMemoryNetwork, InMemorySession, } from "@peerbit/libp2p-test-utils/inmemory-libp2p.js";
2
3
  import { delay } from "@peerbit/time";
3
4
  import { anySignal } from "any-signal";
4
5
  import { monitorEventLoopDelay } from "node:perf_hooks";
5
6
  import { FanoutTree } from "../src/index.js";
6
- import { InMemoryNetwork, InMemorySession, } from "@peerbit/libp2p-test-utils/inmemory-libp2p.js";
7
- import { int, mulberry32, quantile, runWithConcurrency } from "./sim/bench-utils.js";
7
+ import { parentUpgradeRuntimeOptions } from "./fanout-tree-parent-upgrade-preset.js";
8
+ import { int, mulberry32, quantile, runWithConcurrency, } from "./sim/bench-utils.js";
8
9
  class SimFanoutTree extends FanoutTree {
9
10
  constructor(c, opts) {
10
11
  super(c, opts);
@@ -25,6 +26,18 @@ class SimFanoutTree extends FanoutTree {
25
26
  return true;
26
27
  }
27
28
  }
29
+ const effectiveMaxChildrenForUpload = (params, ch, maxChildren) => {
30
+ const requested = Math.max(0, Math.floor(maxChildren));
31
+ const uploadLimitBps = Math.max(0, Math.floor(ch.uploadLimitBps ?? 0));
32
+ if (uploadLimitBps <= 0)
33
+ return 0;
34
+ const msgRate = Math.max(1, Math.floor(params.msgRate));
35
+ const msgSize = Math.max(1, Math.floor(params.msgSize));
36
+ const uploadOverheadBytes = Math.max(0, Math.floor(ch.uploadOverheadBytes ?? 128));
37
+ const perChildBytes = Math.max(1, 1 + msgSize + uploadOverheadBytes);
38
+ const perChildBps = Math.max(1, Math.floor(msgRate * perChildBytes));
39
+ return Math.max(0, Math.min(requested, Math.floor(uploadLimitBps / perChildBps)));
40
+ };
28
41
  const pickDistinct = (rng, n, k, exclude) => {
29
42
  if (k <= 0)
30
43
  return [];
@@ -56,6 +69,14 @@ export const resolveFanoutTreeSimParams = (input) => {
56
69
  : msgRate > 0
57
70
  ? Math.floor(1000 / msgRate)
58
71
  : 0;
72
+ const parentUpgradeMaxChildLoadRatio = Number(input.parentUpgradeMaxChildLoadRatio ?? 0.5);
73
+ const parentUpgradeRootMaxChildLoadRatio = Number(input.parentUpgradeRootMaxChildLoadRatio ??
74
+ Math.min(parentUpgradeMaxChildLoadRatio, 0.4));
75
+ const parentUpgradeMode = input.parentUpgradeMode === "probe" || input.parentUpgradeMode === "shadow"
76
+ ? input.parentUpgradeMode
77
+ : input.parentUpgradeMode === "direct"
78
+ ? "direct"
79
+ : "shadow";
59
80
  return {
60
81
  nodes,
61
82
  rootIndex: Number(input.rootIndex ?? 0),
@@ -64,6 +85,8 @@ export const resolveFanoutTreeSimParams = (input) => {
64
85
  subscribers: Number(input.subscribers ?? subscribersDefault),
65
86
  relayFraction: Number(input.relayFraction ?? 0.25),
66
87
  messages: Number(input.messages ?? 200),
88
+ secondBatchMessages: Number(input.secondBatchMessages ?? 0),
89
+ secondBatchSettleMs: Number(input.secondBatchSettleMs ?? 0),
67
90
  msgRate,
68
91
  msgSize: Number(input.msgSize ?? 1024),
69
92
  intervalMs,
@@ -105,12 +128,53 @@ export const resolveFanoutTreeSimParams = (input) => {
105
128
  input.candidateScoringMode === "ranked-shuffle"
106
129
  ? input.candidateScoringMode
107
130
  : "ranked-shuffle",
131
+ parentUpgradeIntervalMs: Number(input.parentUpgradeIntervalMs ?? 0),
132
+ parentUpgradeLeafOnly: Boolean(input.parentUpgradeLeafOnly ?? true),
133
+ parentUpgradeMinLevelGain: Number(input.parentUpgradeMinLevelGain ?? 1),
134
+ parentUpgradeRootMinLevelGain: Number(input.parentUpgradeRootMinLevelGain ?? 3),
135
+ parentUpgradeRootMinSubtreeGain: Number(input.parentUpgradeRootMinSubtreeGain ??
136
+ input.parentUpgradeRootMinLevelGain ??
137
+ 3),
138
+ parentUpgradeNonRootMinLevelGain: Number(input.parentUpgradeNonRootMinLevelGain ?? 2),
139
+ parentUpgradeMinFreeSlots: Number(input.parentUpgradeMinFreeSlots ?? 8),
140
+ parentUpgradeRootMinFreeSlots: Number(input.parentUpgradeRootMinFreeSlots ??
141
+ input.parentUpgradeMinFreeSlots ??
142
+ 8),
143
+ parentUpgradeMaxChildLoadRatio,
144
+ parentUpgradeRootMaxChildLoadRatio,
145
+ parentUpgradeCooldownMs: Number(input.parentUpgradeCooldownMs ?? 5_000),
146
+ parentUpgradeFailedBackoffMinMs: Number(input.parentUpgradeFailedBackoffMinMs ?? 5_000),
147
+ parentUpgradeFailedBackoffMaxMs: Number(input.parentUpgradeFailedBackoffMaxMs ?? 60_000),
148
+ parentUpgradeQuietMs: Number(input.parentUpgradeQuietMs ?? 5_000),
149
+ parentUpgradeRepairQuietMs: Number(input.parentUpgradeRepairQuietMs ?? input.parentUpgradeQuietMs ?? 5_000),
150
+ parentUpgradeMaxPerPeer: Number(input.parentUpgradeMaxPerPeer ?? 2),
151
+ parentUpgradeRepairGuard: Boolean(input.parentUpgradeRepairGuard ?? true),
152
+ parentUpgradeDataGuard: Boolean(input.parentUpgradeDataGuard ?? true),
153
+ parentUpgradeMode,
154
+ parentUpgradeVerifyStaleRootCapacity: Boolean(input.parentUpgradeVerifyStaleRootCapacity ??
155
+ parentUpgradeMode === "shadow"),
156
+ parentUpgradeStaleRootProbeProbability: Number(input.parentUpgradeStaleRootProbeProbability ?? 0.0625),
157
+ parentProbeTimeoutMs: Number(input.parentProbeTimeoutMs ?? 500),
158
+ parentProbeMaxPerRound: Number(input.parentProbeMaxPerRound ?? 2),
159
+ parentProbeMaxLagMessages: Number(input.parentProbeMaxLagMessages ?? 0),
160
+ parentProbeRejectCooldownMs: Number(input.parentProbeRejectCooldownMs ?? 10_000),
161
+ parentProbeRejectCooldownMaxMs: Number(input.parentProbeRejectCooldownMaxMs ?? 60_000),
162
+ parentShadowObserveMs: Number(input.parentShadowObserveMs ?? 2_000),
163
+ parentShadowMinObservations: Number(input.parentShadowMinObservations ?? 2),
164
+ parentShadowDualPathMs: Number(input.parentShadowDualPathMs ??
165
+ (parentUpgradeMode === "shadow" ? 5_000 : 0)),
166
+ parentShadowDualPathMinMessages: Number(input.parentShadowDualPathMinMessages ??
167
+ (parentUpgradeMode === "shadow" ? 32 : 1)),
108
168
  bootstrapEnsureIntervalMs: Number(input.bootstrapEnsureIntervalMs ?? -1),
109
169
  trackerQueryIntervalMs: Number(input.trackerQueryIntervalMs ?? -1),
110
170
  joinAttemptsPerRound: Number(input.joinAttemptsPerRound ?? -1),
111
171
  candidateCooldownMs: Number(input.candidateCooldownMs ?? -1),
112
172
  joinPhases: Boolean(input.joinPhases ?? false),
113
173
  joinPhaseSettleMs: Number(input.joinPhaseSettleMs ?? 2_000),
174
+ lateRootConnectAfterMs: Number(input.lateRootConnectAfterMs ?? -1),
175
+ lateRootDuringPublish: Boolean(input.lateRootDuringPublish ?? false),
176
+ lateRootMaxChildren: Number(input.lateRootMaxChildren ?? 0),
177
+ lateRootConnectFraction: Number(input.lateRootConnectFraction ?? 1),
114
178
  maxLatencySamples: Number(input.maxLatencySamples ?? 1_000_000),
115
179
  profile: Boolean(input.profile ?? false),
116
180
  progress: Boolean(input.progress ?? false),
@@ -131,6 +195,8 @@ export const resolveFanoutTreeSimParams = (input) => {
131
195
  assertAttachP95Ms: Number(input.assertAttachP95Ms ?? 0),
132
196
  assertMaxTreeLevelP95: Number(input.assertMaxTreeLevelP95 ?? 0),
133
197
  assertMaxFormationScore: Number(input.assertMaxFormationScore ?? 0),
198
+ assertMinReparentUpgradeTotal: Number(input.assertMinReparentUpgradeTotal ?? 0),
199
+ assertMinActiveShadowPromoteTotal: Number(input.assertMinActiveShadowPromoteTotal ?? 0),
134
200
  assertMaxOrphans: Number(input.assertMaxOrphans ?? 0),
135
201
  assertRecoveryP95Ms: Number(input.assertRecoveryP95Ms ?? 0),
136
202
  assertMaxReparentsPerMin: Number(input.assertMaxReparentsPerMin ?? 0),
@@ -142,12 +208,22 @@ export const formatFanoutTreeSimResult = (r) => {
142
208
  return [
143
209
  "fanout-tree-sim",
144
210
  `nodes=${p.nodes} bootstraps=${r.bootstrapCount} bootstrapMaxPeers=${p.bootstrapMaxPeers} subscribers=${r.subscriberCount} relays=${r.relayCount}`,
211
+ ...(p.lateRootConnectAfterMs >= 0
212
+ ? [
213
+ `lateRoot: afterMs=${p.lateRootConnectAfterMs} maxChildren=${p.lateRootMaxChildren} connectFraction=${p.lateRootConnectFraction}`,
214
+ ]
215
+ : []),
145
216
  `joined=${r.joinedCount}/${r.subscriberCount} (${r.joinedPct.toFixed(2)}%)`,
146
217
  `join: ${(r.joinMs / 1000).toFixed(3)}s`,
147
218
  `attachMs samples=${r.attachSamples} p50=${r.attachP50.toFixed(1)} p95=${r.attachP95.toFixed(1)} p99=${r.attachP99.toFixed(1)} max=${r.attachMax.toFixed(1)}`,
148
219
  `formationPaths: underlayEdges=${r.formationUnderlayEdges} dist(p95/max)=${r.formationUnderlayDistP95.toFixed(1)}/${r.formationUnderlayDistMax.toFixed(1)} stretch(p95/max)=${r.formationStretchP95.toFixed(2)}/${r.formationStretchMax.toFixed(2)} score=${r.formationScore.toFixed(2)}`,
149
220
  `formationTree: maxLevel=${r.formationTreeMaxLevel} p95Level=${r.formationTreeLevelP95.toFixed(1)} avgLevel=${r.formationTreeLevelAvg.toFixed(2)} orphans=${r.formationTreeOrphans} rootChildren=${r.formationTreeRootChildren} children(p95/max)=${r.formationTreeChildrenP95.toFixed(1)}/${r.formationTreeChildrenMax}`,
150
221
  `publish: ${(r.publishMs / 1000).toFixed(3)}s intervalMs=${p.intervalMs}`,
222
+ ...(p.secondBatchMessages > 0
223
+ ? [
224
+ `secondBatch: messages=${p.secondBatchMessages} settleMs=${p.secondBatchSettleMs} delivered=${r.secondBatchDelivered}/${r.secondBatchExpected} (${r.secondBatchDeliveredPct.toFixed(2)}%) deadline=${r.secondBatchDeliveredWithinDeadline}/${r.secondBatchExpected} (${r.secondBatchDeliveredWithinDeadlinePct.toFixed(2)}%) latencyP95=${r.secondBatchLatencyP95.toFixed(1)}`,
225
+ ]
226
+ : []),
151
227
  `churn: everyMs=${p.churnEveryMs} downMs=${p.churnDownMs} fraction=${p.churnFraction} events=${r.churnEvents} peers=${r.churnedPeersTotal}`,
152
228
  ...(r.maintSamples > 0
153
229
  ? [
@@ -160,13 +236,23 @@ export const formatFanoutTreeSimResult = (r) => {
160
236
  : `deadline=off${p.maxDataAgeMs > 0 ? ` maxAgeMs=${p.maxDataAgeMs}` : ""}`,
161
237
  `latencyMs p50=${r.latencyP50.toFixed(1)} p95=${r.latencyP95.toFixed(1)} p99=${r.latencyP99.toFixed(1)} max=${r.latencyMax.toFixed(1)}`,
162
238
  `drops: forward total=${r.droppedForwardsTotal} max=${r.droppedForwardsMax} node=${r.droppedForwardsMaxNode ?? "-"} stale total=${r.staleForwardsDroppedTotal} max=${r.staleForwardsDroppedMax} node=${r.staleForwardsDroppedMaxNode ?? "-"} write total=${r.dataWriteDropsTotal} max=${r.dataWriteDropsMax} node=${r.dataWriteDropsMaxNode ?? "-"}`,
163
- `reparent: disconnect=${r.reparentDisconnectTotal} stale=${r.reparentStaleTotal} kicked=${r.reparentKickedTotal}`,
239
+ ...(p.lateRootDuringPublish ||
240
+ r.publishActiveReparentUpgradeSkipDataTotal > 0
241
+ ? [
242
+ `publishActiveParentUpgrade: upgrade=${r.publishActiveReparentUpgradeTotal} dataSkips=${r.publishActiveReparentUpgradeSkipDataTotal} repairSkips=${r.publishActiveReparentUpgradeSkipRepairTotal} quietSkips=${r.publishActiveReparentUpgradeSkipQuietTotal} probes=${r.publishActiveParentProbeReqSentTotal} shadowStart=${r.publishActiveParentShadowStartTotal} shadowPromote=${r.publishActiveParentShadowPromoteTotal}`,
243
+ ]
244
+ : []),
245
+ `reparent: disconnect=${r.reparentDisconnectTotal} stale=${r.reparentStaleTotal} kicked=${r.reparentKickedTotal} upgrade=${r.reparentUpgradeTotal}`,
246
+ `reparentUpgradeSkipped: leaf=${r.reparentUpgradeSkipLeafTotal} repair=${r.reparentUpgradeSkipRepairTotal} data=${r.reparentUpgradeSkipDataTotal} cooldown=${r.reparentUpgradeSkipCooldownTotal} quiet=${r.reparentUpgradeSkipQuietTotal} budget=${r.reparentUpgradeSkipBudgetTotal} candidateLevel=${r.reparentUpgradeSkipCandidateLevelTotal} candidateSlots=${r.reparentUpgradeSkipCandidateSlotsTotal} candidatePressure=${r.reparentUpgradeSkipCandidatePressureTotal} rootPressure=${r.reparentUpgradeSkipRootPressureTotal}`,
247
+ `parentProbe: req=${r.parentProbeReqSentTotal}/${r.parentProbeReqReceivedTotal} reply=${r.parentProbeReplySentTotal}/${r.parentProbeReplyReceivedTotal} skipped noReply=${r.reparentUpgradeSkipProbeNoReplyTotal} notRooted=${r.reparentUpgradeSkipProbeNotRootedTotal} repair=${r.reparentUpgradeSkipProbeRepairTotal} lag=${r.reparentUpgradeSkipProbeLagTotal} overloaded=${r.reparentUpgradeSkipProbeOverloadedTotal} cooldown=${r.reparentUpgradeSkipProbeCooldownTotal}`,
248
+ `parentRootReservations: created=${r.parentUpgradeRootReservationCreatedTotal} consumed=${r.parentUpgradeRootReservationConsumedTotal} rejected=${r.parentUpgradeRootReservationRejectedTotal} marginRejected=${r.parentUpgradeRootReservationMarginRejectedTotal} blocked=${r.parentUpgradeRootReservationBlockedTotal} expired=${r.parentUpgradeRootReservationExpiredTotal}`,
249
+ `parentShadow: start=${r.parentShadowStartTotal} observe=${r.parentShadowObserveTotal} promote=${r.parentShadowPromoteTotal} reset=${r.parentShadowResetTotal} reject noReply=${r.parentShadowRejectNoReplyTotal} notRooted=${r.parentShadowRejectNotRootedTotal} capacity=${r.parentShadowRejectCapacityTotal} repair=${r.parentShadowRejectRepairTotal} lag=${r.parentShadowRejectLagTotal} overloaded=${r.parentShadowRejectOverloadedTotal} level=${r.parentShadowRejectLevelTotal}`,
164
250
  `tree: maxLevel=${r.treeMaxLevel} p95Level=${r.treeLevelP95.toFixed(1)} avgLevel=${r.treeLevelAvg.toFixed(2)} orphans=${r.treeOrphans} rootChildren=${r.treeRootChildren} children(p95/max)=${r.treeChildrenP95.toFixed(1)}/${r.treeChildrenMax}`,
165
- `upload: max=${r.maxUploadBps} B/s (${r.maxUploadFracPct.toFixed(1)}% of cap) node=${r.maxUploadNode ?? "-"}`,
251
+ `upload: max=${r.maxUploadBps} B/s (${r.maxUploadFracPct.toFixed(1)}% of cap) node=${r.maxUploadNode ?? "-"} root=${r.rootUploadBps} B/s (${r.rootUploadFracPct.toFixed(1)}% of cap)`,
166
252
  `stream: queuedBytes total=${r.streamQueuedBytesTotal} max=${r.streamQueuedBytesMax} p95=${r.streamQueuedBytesP95.toFixed(0)} node=${r.streamQueuedBytesMaxNode ?? "-"} lanes=${r.streamQueuedBytesByLane.join(",")}`,
167
253
  `overhead: dataFactor=${r.overheadFactorData.toFixed(3)} (sentPayloadBytes / ideal)`,
168
254
  `economics: earningsTotal=${r.earningsTotal} relayCount=${r.earningsRelayCount} p50=${r.earningsRelayP50} p95=${r.earningsRelayP95} max=${r.earningsRelayMax}`,
169
- `protocol: controlBytesSent=${r.protocolControlBytesSent} (join=${r.protocolControlBytesSentJoin} tracker=${r.protocolControlBytesSentTracker} repair=${r.protocolControlBytesSentRepair}) bpp=${r.controlBpp.toFixed(4)} (tracker=${r.trackerBpp.toFixed(4)} repair=${r.repairBpp.toFixed(4)}) dataPayloadBytesSent=${r.protocolDataPayloadBytesSent} fetchReqSent=${r.protocolFetchReqSent} ihaveSent=${r.protocolIHaveSent} trackerFeedbackSent=${r.protocolTrackerFeedbackSent} holeFills=${r.protocolHoleFillsFromNeighbor} routeCache(h/m/x/e)=${r.protocolRouteCacheHits}/${r.protocolRouteCacheMisses}/${r.protocolRouteCacheExpirations}/${r.protocolRouteCacheEvictions} routeProxy(q/t/f)=${r.protocolRouteProxyQueries}/${r.protocolRouteProxyTimeouts}/${r.protocolRouteProxyFanout}`,
255
+ `protocol: controlBytesSent=${r.protocolControlBytesSent} (join=${r.protocolControlBytesSentJoin} tracker=${r.protocolControlBytesSentTracker} repair=${r.protocolControlBytesSentRepair}) bpp=${r.controlBpp.toFixed(4)} (tracker=${r.trackerBpp.toFixed(4)} repair=${r.repairBpp.toFixed(4)}) dataPayloadBytesSent=${r.protocolDataPayloadBytesSent} fetchReqSent=${r.protocolFetchReqSent} ihaveSent=${r.protocolIHaveSent} tracker(a/q/r/f)=${r.protocolTrackerAnnounceSent}/${r.protocolTrackerQuerySent}/${r.protocolTrackerReplySent}/${r.protocolTrackerFeedbackSent} holeFills=${r.protocolHoleFillsFromNeighbor} routeCache(h/m/x/e)=${r.protocolRouteCacheHits}/${r.protocolRouteCacheMisses}/${r.protocolRouteCacheExpirations}/${r.protocolRouteCacheEvictions} routeProxy(q/t/f)=${r.protocolRouteProxyQueries}/${r.protocolRouteProxyTimeouts}/${r.protocolRouteProxyFanout}`,
170
256
  `network: dials=${r.network.dials} connsOpened=${r.network.connectionsOpened} streamsOpened=${r.network.streamsOpened} framesSent=${r.network.framesSent} bytesSent=${r.network.bytesSent} framesDropped=${r.network.framesDropped} bytesDropped=${r.network.bytesDropped}`,
171
257
  ...(r.profile
172
258
  ? [
@@ -296,7 +382,9 @@ export const runFanoutTreeSim = async (input) => {
296
382
  role: "root",
297
383
  msgRate: params.msgRate,
298
384
  msgSize: params.msgSize,
299
- ...(params.maxDataAgeMs > 0 ? { maxDataAgeMs: params.maxDataAgeMs } : {}),
385
+ ...(params.maxDataAgeMs > 0
386
+ ? { maxDataAgeMs: params.maxDataAgeMs }
387
+ : {}),
300
388
  uploadLimitBps: params.rootUploadLimitBps,
301
389
  maxChildren: params.rootMaxChildren,
302
390
  bidPerByte: params.bidPerByte,
@@ -317,7 +405,9 @@ export const runFanoutTreeSim = async (input) => {
317
405
  ? { neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs }
318
406
  : {}),
319
407
  ...(params.neighborMeshRefreshIntervalMs >= 0
320
- ? { neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs }
408
+ ? {
409
+ neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs,
410
+ }
321
411
  : {}),
322
412
  ...(params.neighborHaveTtlMs >= 0
323
413
  ? { neighborHaveTtlMs: params.neighborHaveTtlMs }
@@ -343,7 +433,8 @@ export const runFanoutTreeSim = async (input) => {
343
433
  const mu = process.memoryUsage();
344
434
  const rssMb = mu.rss / (1024 * 1024);
345
435
  const heapUsedMb = mu.heapUsed / (1024 * 1024);
346
- const openConns = Math.max(0, network.metrics.connectionsOpened - network.metrics.connectionsClosed);
436
+ const openConns = Math.max(0, network.metrics.connectionsOpened -
437
+ network.metrics.connectionsClosed);
347
438
  console.log(`[fanout-tree-sim] join progress ok=${joinOk}/${subscriberIndices.length} done=${joinCompleted}/${subscriberIndices.length} openConns=${openConns} dials=${network.metrics.dials} streamsOpened=${network.metrics.streamsOpened} rssMb=${rssMb.toFixed(1)} heapUsedMb=${heapUsedMb.toFixed(1)}`);
348
439
  }, progressEveryMs);
349
440
  joinProgressTimer.unref?.();
@@ -355,15 +446,21 @@ export const runFanoutTreeSim = async (input) => {
355
446
  await node.joinChannel(params.topic, rootId, {
356
447
  msgRate: params.msgRate,
357
448
  msgSize: params.msgSize,
358
- ...(params.maxDataAgeMs > 0 ? { maxDataAgeMs: params.maxDataAgeMs } : {}),
449
+ ...(params.maxDataAgeMs > 0
450
+ ? { maxDataAgeMs: params.maxDataAgeMs }
451
+ : {}),
359
452
  uploadLimitBps: isRelay ? params.relayUploadLimitBps : 0,
360
453
  maxChildren: isRelay ? params.relayMaxChildren : 0,
361
- bidPerByte: isRelay ? params.bidPerByteRelay : params.bidPerByteLeaf,
454
+ bidPerByte: isRelay
455
+ ? params.bidPerByteRelay
456
+ : params.bidPerByteLeaf,
362
457
  allowKick: params.allowKick,
363
458
  repair: params.repair,
364
459
  repairWindowMessages: params.repairWindowMessages,
365
460
  ...(params.repairMaxBackfillMessages >= 0
366
- ? { repairMaxBackfillMessages: params.repairMaxBackfillMessages }
461
+ ? {
462
+ repairMaxBackfillMessages: params.repairMaxBackfillMessages,
463
+ }
367
464
  : {}),
368
465
  repairIntervalMs: params.repairIntervalMs,
369
466
  repairMaxPerReq: params.repairMaxPerReq,
@@ -373,10 +470,14 @@ export const runFanoutTreeSim = async (input) => {
373
470
  ? { neighborMeshPeers: params.neighborMeshPeers }
374
471
  : {}),
375
472
  ...(params.neighborAnnounceIntervalMs >= 0
376
- ? { neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs }
473
+ ? {
474
+ neighborAnnounceIntervalMs: params.neighborAnnounceIntervalMs,
475
+ }
377
476
  : {}),
378
477
  ...(params.neighborMeshRefreshIntervalMs >= 0
379
- ? { neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs }
478
+ ? {
479
+ neighborMeshRefreshIntervalMs: params.neighborMeshRefreshIntervalMs,
480
+ }
380
481
  : {}),
381
482
  ...(params.neighborHaveTtlMs >= 0
382
483
  ? { neighborHaveTtlMs: params.neighborHaveTtlMs }
@@ -389,7 +490,9 @@ export const runFanoutTreeSim = async (input) => {
389
490
  : {}),
390
491
  }, {
391
492
  timeoutMs: Math.max(10_000, Math.min(120_000, timeoutMs || 120_000)),
392
- ...(params.maxDataAgeMs > 0 ? { staleAfterMs: params.maxDataAgeMs } : {}),
493
+ ...(params.maxDataAgeMs > 0
494
+ ? { staleAfterMs: params.maxDataAgeMs }
495
+ : {}),
393
496
  ...(params.joinReqTimeoutMs >= 0
394
497
  ? { joinReqTimeoutMs: params.joinReqTimeoutMs }
395
498
  : {}),
@@ -397,8 +500,11 @@ export const runFanoutTreeSim = async (input) => {
397
500
  ? { candidateShuffleTopK: params.candidateShuffleTopK }
398
501
  : {}),
399
502
  candidateScoringMode: params.candidateScoringMode,
503
+ ...parentUpgradeRuntimeOptions(params),
400
504
  ...(params.bootstrapEnsureIntervalMs >= 0
401
- ? { bootstrapEnsureIntervalMs: params.bootstrapEnsureIntervalMs }
505
+ ? {
506
+ bootstrapEnsureIntervalMs: params.bootstrapEnsureIntervalMs,
507
+ }
402
508
  : {}),
403
509
  ...(params.trackerQueryIntervalMs >= 0
404
510
  ? { trackerQueryIntervalMs: params.trackerQueryIntervalMs }
@@ -465,7 +571,9 @@ export const runFanoutTreeSim = async (input) => {
465
571
  const openConns = Math.max(0, network.metrics.connectionsOpened - network.metrics.connectionsClosed);
466
572
  console.log(`[fanout-tree-sim] phase=join_done ok=${joinOk}/${subscriberIndices.length} openConns=${openConns} rssMb=${rssMb.toFixed(1)} heapUsedMb=${heapUsedMb.toFixed(1)} joinMs=${joinDone - joinStart}`);
467
573
  }
468
- const attachDurations = attachDurationsByPos.filter((d) => d >= 0).sort((a, b) => a - b);
574
+ const attachDurations = attachDurationsByPos
575
+ .filter((d) => d >= 0)
576
+ .sort((a, b) => a - b);
469
577
  const attachSamples = attachDurations.length;
470
578
  const attachP50 = attachSamples > 0 ? quantile(attachDurations, 0.5) : NaN;
471
579
  const attachP95 = attachSamples > 0 ? quantile(attachDurations, 0.95) : NaN;
@@ -479,13 +587,17 @@ export const runFanoutTreeSim = async (input) => {
479
587
  const computeTreeShapeStats = () => {
480
588
  const levels = [];
481
589
  const levelByIndex = new Array(params.nodes).fill(NaN);
590
+ const childrenByHash = new Map();
482
591
  const childrenCounts = [];
483
592
  let treeOrphans = 0;
484
593
  let treeRootChildren = 0;
485
594
  for (let i = 0; i < session.peers.length; i++) {
595
+ const hash = session.peers[i].services.fanout.publicKeyHash;
486
596
  const s = session.peers[i].services.fanout.getChannelStats(params.topic, rootId);
487
597
  if (!s)
488
598
  continue;
599
+ if (!childrenByHash.has(hash))
600
+ childrenByHash.set(hash, []);
489
601
  if (Number.isFinite(s.level)) {
490
602
  levels.push(s.level);
491
603
  levelByIndex[i] = s.level;
@@ -493,6 +605,11 @@ export const runFanoutTreeSim = async (input) => {
493
605
  if (s.effectiveMaxChildren > 0) {
494
606
  childrenCounts.push(s.children);
495
607
  }
608
+ if (s.parent) {
609
+ const children = childrenByHash.get(s.parent) ?? [];
610
+ children.push(hash);
611
+ childrenByHash.set(s.parent, children);
612
+ }
496
613
  if (s.level === 0) {
497
614
  treeRootChildren = s.children;
498
615
  }
@@ -504,9 +621,13 @@ export const runFanoutTreeSim = async (input) => {
504
621
  childrenCounts.sort((a, b) => a - b);
505
622
  const treeMaxLevel = levels.length > 0 ? levels[levels.length - 1] : 0;
506
623
  const treeLevelP95 = levels.length > 0 ? quantile(levels, 0.95) : 0;
507
- const treeLevelAvg = levels.length > 0 ? levels.reduce((a, b) => a + b, 0) / levels.length : 0;
624
+ const treeLevelAvg = levels.length > 0
625
+ ? levels.reduce((a, b) => a + b, 0) / levels.length
626
+ : 0;
508
627
  const treeChildrenP95 = childrenCounts.length > 0 ? quantile(childrenCounts, 0.95) : 0;
509
- const treeChildrenMax = childrenCounts.length > 0 ? childrenCounts[childrenCounts.length - 1] : 0;
628
+ const treeChildrenMax = childrenCounts.length > 0
629
+ ? childrenCounts[childrenCounts.length - 1]
630
+ : 0;
510
631
  return {
511
632
  treeMaxLevel,
512
633
  treeLevelP95,
@@ -516,6 +637,7 @@ export const runFanoutTreeSim = async (input) => {
516
637
  treeChildrenMax,
517
638
  treeRootChildren,
518
639
  levelByIndex,
640
+ childrenByHash,
519
641
  };
520
642
  };
521
643
  const formationTree = computeTreeShapeStats();
@@ -557,7 +679,9 @@ export const runFanoutTreeSim = async (input) => {
557
679
  .map((i) => underlayDist[i])
558
680
  .filter((d) => Number.isFinite(d))
559
681
  .sort((a, b) => a - b);
560
- const formationUnderlayDistP95 = formationUnderlayDists.length > 0 ? quantile(formationUnderlayDists, 0.95) : NaN;
682
+ const formationUnderlayDistP95 = formationUnderlayDists.length > 0
683
+ ? quantile(formationUnderlayDists, 0.95)
684
+ : NaN;
561
685
  const formationUnderlayDistMax = formationUnderlayDists.length > 0
562
686
  ? formationUnderlayDists[formationUnderlayDists.length - 1]
563
687
  : NaN;
@@ -565,14 +689,20 @@ export const runFanoutTreeSim = async (input) => {
565
689
  .map((i) => {
566
690
  const overlay = formationTree.levelByIndex[i];
567
691
  const under = underlayDist[i];
568
- if (!Number.isFinite(overlay) || !Number.isFinite(under) || under <= 0)
692
+ if (!Number.isFinite(overlay) ||
693
+ !Number.isFinite(under) ||
694
+ under <= 0)
569
695
  return NaN;
570
696
  return overlay / under;
571
697
  })
572
698
  .filter((x) => Number.isFinite(x))
573
699
  .sort((a, b) => a - b);
574
- const formationStretchP95 = formationStretches.length > 0 ? quantile(formationStretches, 0.95) : NaN;
575
- const formationStretchMax = formationStretches.length > 0 ? formationStretches[formationStretches.length - 1] : NaN;
700
+ const formationStretchP95 = formationStretches.length > 0
701
+ ? quantile(formationStretches, 0.95)
702
+ : NaN;
703
+ const formationStretchMax = formationStretches.length > 0
704
+ ? formationStretches[formationStretches.length - 1]
705
+ : NaN;
576
706
  const formationOrphanPct = joinedCount === 0 ? 0 : (100 * formationTree.treeOrphans) / joinedCount;
577
707
  const formationStretchPenalty = Number.isFinite(formationStretchP95)
578
708
  ? Math.max(0, formationStretchP95 - 1) * 10
@@ -582,10 +712,16 @@ export const runFanoutTreeSim = async (input) => {
582
712
  formationOrphanPct +
583
713
  formationStretchPenalty;
584
714
  const churnController = new AbortController();
585
- const churnSignal = anySignal([timeoutSignal, churnController.signal]);
715
+ const churnSignal = anySignal([
716
+ timeoutSignal,
717
+ churnController.signal,
718
+ ]);
586
719
  let churnEvents = 0;
587
720
  let churnedPeersTotal = 0;
588
- const wantsMaintenance = (params.churnEveryMs > 0 && params.churnDownMs > 0 && params.churnFraction > 0) ||
721
+ const wantsMaintenance = (params.churnEveryMs > 0 &&
722
+ params.churnDownMs > 0 &&
723
+ params.churnFraction > 0) ||
724
+ params.parentUpgradeIntervalMs > 0 ||
589
725
  params.assertMaxOrphans > 0 ||
590
726
  params.assertMaxOrphanArea > 0 ||
591
727
  params.assertRecoveryP95Ms > 0 ||
@@ -607,20 +743,29 @@ export const runFanoutTreeSim = async (input) => {
607
743
  let maintReparentsTotal = 0;
608
744
  let maintMaxReparentsPerPeer = 0;
609
745
  // Delivery tracking
746
+ const firstBatchMessages = Math.max(0, Math.floor(params.messages));
747
+ const secondBatchMessages = Math.max(0, Math.floor(params.secondBatchMessages));
748
+ const totalMessages = firstBatchMessages + secondBatchMessages;
749
+ const secondBatchStartSeq = firstBatchMessages;
610
750
  const publishAt = new Map();
611
751
  const joinedHashList = [...joinedHashes];
612
752
  const hashToIndex = new Map();
613
753
  for (let i = 0; i < joinedHashList.length; i++) {
614
754
  hashToIndex.set(joinedHashList[i], i);
615
755
  }
616
- const bitsetBytes = Math.ceil(Math.max(0, params.messages) / 8);
756
+ const bitsetBytes = Math.ceil(totalMessages / 8);
617
757
  const receivedBits = joinedHashList.map(() => new Uint8Array(bitsetBytes));
618
758
  const receivedCounts = new Uint32Array(joinedHashList.length);
759
+ const secondBatchReceivedCounts = new Uint32Array(joinedHashList.length);
619
760
  let duplicates = 0;
620
761
  let deliveredWithinDeadline = 0;
621
762
  let deliveredSamples = [];
763
+ let secondBatchDeliveredWithinDeadline = 0;
764
+ let secondBatchDeliveredSamples = [];
765
+ const secondBatchDeliveredSamplesByPeer = joinedHashList.map(() => []);
622
766
  const sampleCap = Math.max(1, Math.floor(params.maxLatencySamples));
623
767
  let sampleSeen = 0;
768
+ let secondBatchSampleSeen = 0;
624
769
  const makeOnData = (localHash) => (ev) => {
625
770
  const d = ev?.detail;
626
771
  if (!d)
@@ -633,7 +778,7 @@ export const runFanoutTreeSim = async (input) => {
633
778
  const index = hashToIndex.get(localHash);
634
779
  if (index == null)
635
780
  return; // not joined / not tracked
636
- if (seq >= params.messages)
781
+ if (seq >= totalMessages)
637
782
  return;
638
783
  const bits = receivedBits[index];
639
784
  const byteIndex = seq >>> 3;
@@ -646,6 +791,9 @@ export const runFanoutTreeSim = async (input) => {
646
791
  }
647
792
  bits[byteIndex] |= mask;
648
793
  receivedCounts[index] += 1;
794
+ if (seq >= secondBatchStartSeq) {
795
+ secondBatchReceivedCounts[index] += 1;
796
+ }
649
797
  const sentAt = publishAt.get(seq);
650
798
  if (sentAt != null) {
651
799
  const latency = Date.now() - sentAt;
@@ -661,6 +809,21 @@ export const runFanoutTreeSim = async (input) => {
661
809
  if (j < sampleCap)
662
810
  deliveredSamples[j] = latency;
663
811
  }
812
+ if (seq >= secondBatchStartSeq) {
813
+ if (params.deadlineMs > 0 && latency <= params.deadlineMs) {
814
+ secondBatchDeliveredWithinDeadline += 1;
815
+ }
816
+ secondBatchDeliveredSamplesByPeer[index].push(latency);
817
+ secondBatchSampleSeen += 1;
818
+ if (secondBatchDeliveredSamples.length < sampleCap) {
819
+ secondBatchDeliveredSamples.push(latency);
820
+ }
821
+ else {
822
+ const j = int(rng, secondBatchSampleSeen);
823
+ if (j < sampleCap)
824
+ secondBatchDeliveredSamples[j] = latency;
825
+ }
826
+ }
664
827
  }
665
828
  };
666
829
  for (let i = 0; i < subscriberIndices.length; i++) {
@@ -830,18 +993,105 @@ export const runFanoutTreeSim = async (input) => {
830
993
  }));
831
994
  }
832
995
  };
996
+ const collectParentUpgradeActivity = () => {
997
+ let reparentUpgrade = 0;
998
+ let reparentUpgradeSkipData = 0;
999
+ let reparentUpgradeSkipRepair = 0;
1000
+ let reparentUpgradeSkipQuiet = 0;
1001
+ let parentProbeReqSent = 0;
1002
+ let parentShadowStart = 0;
1003
+ let parentShadowPromote = 0;
1004
+ for (const p of session.peers) {
1005
+ const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
1006
+ reparentUpgrade += m.reparentUpgrade;
1007
+ reparentUpgradeSkipData += m.reparentUpgradeSkipData;
1008
+ reparentUpgradeSkipRepair += m.reparentUpgradeSkipRepair;
1009
+ reparentUpgradeSkipQuiet += m.reparentUpgradeSkipQuiet;
1010
+ parentProbeReqSent += m.parentProbeReqSent;
1011
+ parentShadowStart += m.parentShadowStart;
1012
+ parentShadowPromote += m.parentShadowPromote;
1013
+ }
1014
+ return {
1015
+ reparentUpgrade,
1016
+ reparentUpgradeSkipData,
1017
+ reparentUpgradeSkipRepair,
1018
+ reparentUpgradeSkipQuiet,
1019
+ parentProbeReqSent,
1020
+ parentShadowStart,
1021
+ parentShadowPromote,
1022
+ };
1023
+ };
1024
+ const diffParentUpgradeActivity = (after, before) => ({
1025
+ reparentUpgrade: Math.max(0, after.reparentUpgrade - before.reparentUpgrade),
1026
+ reparentUpgradeSkipData: Math.max(0, after.reparentUpgradeSkipData - before.reparentUpgradeSkipData),
1027
+ reparentUpgradeSkipRepair: Math.max(0, after.reparentUpgradeSkipRepair - before.reparentUpgradeSkipRepair),
1028
+ reparentUpgradeSkipQuiet: Math.max(0, after.reparentUpgradeSkipQuiet - before.reparentUpgradeSkipQuiet),
1029
+ parentProbeReqSent: Math.max(0, after.parentProbeReqSent - before.parentProbeReqSent),
1030
+ parentShadowStart: Math.max(0, after.parentShadowStart - before.parentShadowStart),
1031
+ parentShadowPromote: Math.max(0, after.parentShadowPromote - before.parentShadowPromote),
1032
+ });
1033
+ const publishStartParentUpgrade = collectParentUpgradeActivity();
1034
+ let publishActiveParentUpgrade = diffParentUpgradeActivity(publishStartParentUpgrade, publishStartParentUpgrade);
833
1035
  const publishStart = Date.now();
1036
+ let publishActiveMs = 0;
834
1037
  if (wantsMaintenance) {
835
1038
  for (const p of session.peers) {
836
1039
  const nodeHash = p.services.fanout.publicKeyHash;
837
1040
  const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
838
- reparentBaselineByHash.set(nodeHash, m.reparentDisconnect + m.reparentStale + m.reparentKicked);
1041
+ reparentBaselineByHash.set(nodeHash, m.reparentDisconnect +
1042
+ m.reparentStale +
1043
+ m.reparentKicked +
1044
+ m.reparentUpgrade);
839
1045
  }
840
1046
  }
841
1047
  const maintenancePromise = maintenanceLoop().catch(() => { });
842
1048
  const churnPromise = churnLoop().catch(() => { });
1049
+ const lateAfterMs = Math.max(-1, Math.floor(params.lateRootConnectAfterMs));
1050
+ let lateRootApplied = false;
1051
+ const applyLateRootTopology = async () => {
1052
+ if (lateRootApplied || lateAfterMs < 0)
1053
+ return;
1054
+ lateRootApplied = true;
1055
+ const lateMaxChildren = Math.max(0, Math.floor(params.lateRootMaxChildren));
1056
+ if (lateMaxChildren > 0) {
1057
+ const id = root.getChannelId(params.topic, rootId);
1058
+ const ch = root.channelsBySuffixKey?.get?.(id.suffixKey);
1059
+ if (ch) {
1060
+ ch.maxChildren = Math.max(ch.maxChildren ?? 0, lateMaxChildren);
1061
+ const uploadBoundedMaxChildren = effectiveMaxChildrenForUpload(params, ch, lateMaxChildren);
1062
+ ch.effectiveMaxChildren = Math.max(ch.effectiveMaxChildren ?? 0, uploadBoundedMaxChildren);
1063
+ void root
1064
+ .announceToTrackers?.(ch, timeoutSignal)
1065
+ ?.catch?.(() => { });
1066
+ }
1067
+ }
1068
+ const fraction = Math.max(0, Math.min(1, Number(params.lateRootConnectFraction)));
1069
+ const target = Math.min(joinedSubscriberIndices.length, Math.max(0, Math.ceil(joinedSubscriberIndices.length * fraction)));
1070
+ for (let i = 0; i < target; i++) {
1071
+ const idx = joinedSubscriberIndices[i];
1072
+ if (idx === rootIndex)
1073
+ continue;
1074
+ try {
1075
+ await session.peers[idx].dial(session.peers[rootIndex].getMultiaddrs());
1076
+ }
1077
+ catch {
1078
+ // best-effort late underlay shortcut
1079
+ }
1080
+ }
1081
+ };
1082
+ const lateRootDuringPublishPromise = params.lateRootDuringPublish && lateAfterMs >= 0
1083
+ ? (async () => {
1084
+ if (lateAfterMs > 0) {
1085
+ await delay(lateAfterMs, { signal: timeoutSignal });
1086
+ }
1087
+ await applyLateRootTopology();
1088
+ })().catch((error) => {
1089
+ if (!timeoutSignal.aborted)
1090
+ throw error;
1091
+ })
1092
+ : undefined;
843
1093
  try {
844
- for (let seq = 0; seq < params.messages; seq++) {
1094
+ for (let seq = 0; seq < firstBatchMessages; seq++) {
845
1095
  if (timeoutSignal.aborted) {
846
1096
  throw timeoutSignal.reason ?? new Error("fanout-tree-sim aborted");
847
1097
  }
@@ -853,17 +1103,58 @@ export const runFanoutTreeSim = async (input) => {
853
1103
  }
854
1104
  }
855
1105
  finally {
1106
+ if (firstBatchMessages > 0) {
1107
+ publishActiveMs += Math.max(0, Date.now() - publishStart);
1108
+ }
1109
+ publishActiveParentUpgrade = diffParentUpgradeActivity(collectParentUpgradeActivity(), publishStartParentUpgrade);
856
1110
  churnController.abort();
857
1111
  await churnPromise;
858
1112
  churnSignal.clear?.();
1113
+ await lateRootDuringPublishPromise;
859
1114
  }
860
- const publishDone = Date.now();
1115
+ let publishDone = Date.now();
861
1116
  // Signal end-of-stream so subscribers can detect tail gaps and repair.
862
- if (params.repair && params.messages > 0) {
863
- await root.publishEnd(params.topic, rootId, params.messages);
1117
+ if (params.repair && firstBatchMessages > 0) {
1118
+ await root.publishEnd(params.topic, rootId, firstBatchMessages);
864
1119
  }
865
1120
  if (params.settleMs > 0) {
866
- await delay(params.settleMs, { signal: timeoutSignal });
1121
+ if (!params.lateRootDuringPublish &&
1122
+ lateAfterMs >= 0 &&
1123
+ lateAfterMs < params.settleMs) {
1124
+ if (lateAfterMs > 0) {
1125
+ await delay(lateAfterMs, { signal: timeoutSignal });
1126
+ }
1127
+ await applyLateRootTopology();
1128
+ const remaining = Math.max(0, params.settleMs - lateAfterMs);
1129
+ if (remaining > 0) {
1130
+ await delay(remaining, { signal: timeoutSignal });
1131
+ }
1132
+ }
1133
+ else {
1134
+ await delay(params.settleMs, { signal: timeoutSignal });
1135
+ }
1136
+ }
1137
+ if (secondBatchMessages > 0) {
1138
+ const secondPublishStart = Date.now();
1139
+ for (let seq = secondBatchStartSeq; seq < totalMessages; seq++) {
1140
+ if (timeoutSignal.aborted) {
1141
+ throw timeoutSignal.reason ?? new Error("fanout-tree-sim aborted");
1142
+ }
1143
+ publishAt.set(seq, Date.now());
1144
+ await root.publishData(params.topic, rootId, payload);
1145
+ if (params.intervalMs > 0) {
1146
+ await delay(params.intervalMs, { signal: timeoutSignal });
1147
+ }
1148
+ }
1149
+ publishDone = Date.now();
1150
+ publishActiveMs += Math.max(0, publishDone - secondPublishStart);
1151
+ if (params.repair) {
1152
+ await root.publishEnd(params.topic, rootId, totalMessages);
1153
+ }
1154
+ const secondSettleMs = Math.max(0, Math.floor(params.secondBatchSettleMs));
1155
+ if (secondSettleMs > 0) {
1156
+ await delay(secondSettleMs, { signal: timeoutSignal });
1157
+ }
867
1158
  }
868
1159
  maintenanceController.abort();
869
1160
  await maintenancePromise;
@@ -876,7 +1167,10 @@ export const runFanoutTreeSim = async (input) => {
876
1167
  for (const p of session.peers) {
877
1168
  const nodeHash = p.services.fanout.publicKeyHash;
878
1169
  const m = p.services.fanout.getChannelMetrics(params.topic, rootId);
879
- const total = m.reparentDisconnect + m.reparentStale + m.reparentKicked;
1170
+ const total = m.reparentDisconnect +
1171
+ m.reparentStale +
1172
+ m.reparentKicked +
1173
+ m.reparentUpgrade;
880
1174
  const base = reparentBaselineByHash.get(nodeHash) ?? 0;
881
1175
  const delta = Math.max(0, total - base);
882
1176
  maintReparentsTotal += delta;
@@ -887,13 +1181,23 @@ export const runFanoutTreeSim = async (input) => {
887
1181
  const maintReparentsPerMin = maintDurationMin > 0 ? maintReparentsTotal / maintDurationMin : 0;
888
1182
  const maintOrphanArea = maintOrphanAreaMs / 1_000;
889
1183
  // Compute delivery
890
- const expected = joinedCount * params.messages;
1184
+ const expected = joinedCount * totalMessages;
891
1185
  let delivered = 0;
892
1186
  for (const c of receivedCounts)
893
1187
  delivered += c;
1188
+ const secondBatchExpected = joinedCount * secondBatchMessages;
1189
+ let secondBatchDelivered = 0;
1190
+ for (const c of secondBatchReceivedCounts)
1191
+ secondBatchDelivered += c;
894
1192
  const joinedPct = subscriberCount === 0 ? 100 : (100 * joinedCount) / subscriberCount;
895
1193
  const deliveredPct = expected === 0 ? 100 : (100 * delivered) / expected;
896
1194
  const deliveredWithinDeadlinePct = expected === 0 ? 100 : (100 * deliveredWithinDeadline) / expected;
1195
+ const secondBatchDeliveredPct = secondBatchExpected === 0
1196
+ ? 100
1197
+ : (100 * secondBatchDelivered) / secondBatchExpected;
1198
+ const secondBatchDeliveredWithinDeadlinePct = secondBatchExpected === 0
1199
+ ? 100
1200
+ : (100 * secondBatchDeliveredWithinDeadline) / secondBatchExpected;
897
1201
  // Internal protocol drops (from upload shaping / overload logic)
898
1202
  let droppedForwardsTotal = 0;
899
1203
  let droppedForwardsMax = 0;
@@ -967,6 +1271,11 @@ export const runFanoutTreeSim = async (input) => {
967
1271
  let maxUploadFracPct = 0;
968
1272
  let maxUploadNode;
969
1273
  let maxUploadBps = 0;
1274
+ const rootUploadBps = session.network.peerMetricsByHash.get(root.publicKeyHash)
1275
+ ?.maxBytesPerSecond ?? 0;
1276
+ const rootUploadFracPct = params.rootUploadLimitBps > 0
1277
+ ? (100 * rootUploadBps) / params.rootUploadLimitBps
1278
+ : 0;
970
1279
  for (const [hash, pm] of session.network.peerMetricsByHash) {
971
1280
  const cap = uploadCapByHash.get(hash);
972
1281
  if (!cap || cap <= 0)
@@ -979,6 +1288,15 @@ export const runFanoutTreeSim = async (input) => {
979
1288
  }
980
1289
  }
981
1290
  deliveredSamples.sort((a, b) => a - b);
1291
+ secondBatchDeliveredSamples.sort((a, b) => a - b);
1292
+ const secondBatchLatencyP95ByHash = {};
1293
+ for (let i = 0; i < joinedHashList.length; i++) {
1294
+ const samples = secondBatchDeliveredSamplesByPeer[i];
1295
+ if (samples.length === 0)
1296
+ continue;
1297
+ samples.sort((a, b) => a - b);
1298
+ secondBatchLatencyP95ByHash[joinedHashList[i]] = quantile(samples, 0.95);
1299
+ }
982
1300
  let protocolControlSends = 0;
983
1301
  let protocolControlBytesSent = 0;
984
1302
  let protocolControlBytesSentJoin = 0;
@@ -993,6 +1311,9 @@ export const runFanoutTreeSim = async (input) => {
993
1311
  let protocolRepairReqSent = 0;
994
1312
  let protocolFetchReqSent = 0;
995
1313
  let protocolIHaveSent = 0;
1314
+ let protocolTrackerAnnounceSent = 0;
1315
+ let protocolTrackerQuerySent = 0;
1316
+ let protocolTrackerReplySent = 0;
996
1317
  let protocolTrackerFeedbackSent = 0;
997
1318
  let protocolCacheHitsServed = 0;
998
1319
  let protocolHoleFillsFromNeighbor = 0;
@@ -1012,6 +1333,45 @@ export const runFanoutTreeSim = async (input) => {
1012
1333
  let reparentDisconnectTotal = 0;
1013
1334
  let reparentStaleTotal = 0;
1014
1335
  let reparentKickedTotal = 0;
1336
+ let reparentUpgradeTotal = 0;
1337
+ const upgradedPeerHashes = [];
1338
+ let reparentUpgradeSkipLeafTotal = 0;
1339
+ let reparentUpgradeSkipRepairTotal = 0;
1340
+ let reparentUpgradeSkipDataTotal = 0;
1341
+ let reparentUpgradeSkipCooldownTotal = 0;
1342
+ let reparentUpgradeSkipQuietTotal = 0;
1343
+ let reparentUpgradeSkipBudgetTotal = 0;
1344
+ let reparentUpgradeSkipCandidateLevelTotal = 0;
1345
+ let reparentUpgradeSkipCandidateSlotsTotal = 0;
1346
+ let reparentUpgradeSkipCandidatePressureTotal = 0;
1347
+ let reparentUpgradeSkipRootPressureTotal = 0;
1348
+ let reparentUpgradeSkipProbeNoReplyTotal = 0;
1349
+ let reparentUpgradeSkipProbeNotRootedTotal = 0;
1350
+ let reparentUpgradeSkipProbeRepairTotal = 0;
1351
+ let reparentUpgradeSkipProbeLagTotal = 0;
1352
+ let reparentUpgradeSkipProbeOverloadedTotal = 0;
1353
+ let reparentUpgradeSkipProbeCooldownTotal = 0;
1354
+ let parentProbeReqSentTotal = 0;
1355
+ let parentProbeReqReceivedTotal = 0;
1356
+ let parentProbeReplySentTotal = 0;
1357
+ let parentProbeReplyReceivedTotal = 0;
1358
+ let parentUpgradeRootReservationCreatedTotal = 0;
1359
+ let parentUpgradeRootReservationConsumedTotal = 0;
1360
+ let parentUpgradeRootReservationRejectedTotal = 0;
1361
+ let parentUpgradeRootReservationMarginRejectedTotal = 0;
1362
+ let parentUpgradeRootReservationBlockedTotal = 0;
1363
+ let parentUpgradeRootReservationExpiredTotal = 0;
1364
+ let parentShadowStartTotal = 0;
1365
+ let parentShadowObserveTotal = 0;
1366
+ let parentShadowPromoteTotal = 0;
1367
+ let parentShadowResetTotal = 0;
1368
+ let parentShadowRejectNoReplyTotal = 0;
1369
+ let parentShadowRejectNotRootedTotal = 0;
1370
+ let parentShadowRejectCapacityTotal = 0;
1371
+ let parentShadowRejectRepairTotal = 0;
1372
+ let parentShadowRejectLagTotal = 0;
1373
+ let parentShadowRejectOverloadedTotal = 0;
1374
+ let parentShadowRejectLevelTotal = 0;
1015
1375
  let earningsTotal = 0;
1016
1376
  const earningsByHash = new Map();
1017
1377
  for (const p of session.peers) {
@@ -1030,6 +1390,60 @@ export const runFanoutTreeSim = async (input) => {
1030
1390
  reparentDisconnectTotal += m.reparentDisconnect;
1031
1391
  reparentStaleTotal += m.reparentStale;
1032
1392
  reparentKickedTotal += m.reparentKicked;
1393
+ reparentUpgradeTotal += m.reparentUpgrade;
1394
+ if (m.reparentUpgrade > 0)
1395
+ upgradedPeerHashes.push(nodeHash);
1396
+ reparentUpgradeSkipLeafTotal += m.reparentUpgradeSkipLeaf;
1397
+ reparentUpgradeSkipRepairTotal += m.reparentUpgradeSkipRepair;
1398
+ reparentUpgradeSkipDataTotal += m.reparentUpgradeSkipData;
1399
+ reparentUpgradeSkipCooldownTotal += m.reparentUpgradeSkipCooldown;
1400
+ reparentUpgradeSkipQuietTotal += m.reparentUpgradeSkipQuiet;
1401
+ reparentUpgradeSkipBudgetTotal += m.reparentUpgradeSkipBudget;
1402
+ reparentUpgradeSkipCandidateLevelTotal +=
1403
+ m.reparentUpgradeSkipCandidateLevel;
1404
+ reparentUpgradeSkipCandidateSlotsTotal +=
1405
+ m.reparentUpgradeSkipCandidateSlots;
1406
+ reparentUpgradeSkipCandidatePressureTotal +=
1407
+ m.reparentUpgradeSkipCandidatePressure;
1408
+ reparentUpgradeSkipRootPressureTotal +=
1409
+ m.reparentUpgradeSkipRootPressure;
1410
+ reparentUpgradeSkipProbeNoReplyTotal +=
1411
+ m.reparentUpgradeSkipProbeNoReply;
1412
+ reparentUpgradeSkipProbeNotRootedTotal +=
1413
+ m.reparentUpgradeSkipProbeNotRooted;
1414
+ reparentUpgradeSkipProbeRepairTotal += m.reparentUpgradeSkipProbeRepair;
1415
+ reparentUpgradeSkipProbeLagTotal += m.reparentUpgradeSkipProbeLag;
1416
+ reparentUpgradeSkipProbeOverloadedTotal +=
1417
+ m.reparentUpgradeSkipProbeOverloaded;
1418
+ reparentUpgradeSkipProbeCooldownTotal +=
1419
+ m.reparentUpgradeSkipProbeCooldown;
1420
+ parentProbeReqSentTotal += m.parentProbeReqSent;
1421
+ parentProbeReqReceivedTotal += m.parentProbeReqReceived;
1422
+ parentProbeReplySentTotal += m.parentProbeReplySent;
1423
+ parentProbeReplyReceivedTotal += m.parentProbeReplyReceived;
1424
+ parentUpgradeRootReservationCreatedTotal +=
1425
+ m.parentUpgradeRootReservationCreated;
1426
+ parentUpgradeRootReservationConsumedTotal +=
1427
+ m.parentUpgradeRootReservationConsumed;
1428
+ parentUpgradeRootReservationRejectedTotal +=
1429
+ m.parentUpgradeRootReservationRejected;
1430
+ parentUpgradeRootReservationMarginRejectedTotal +=
1431
+ m.parentUpgradeRootReservationMarginRejected;
1432
+ parentUpgradeRootReservationBlockedTotal +=
1433
+ m.parentUpgradeRootReservationBlocked;
1434
+ parentUpgradeRootReservationExpiredTotal +=
1435
+ m.parentUpgradeRootReservationExpired;
1436
+ parentShadowStartTotal += m.parentShadowStart;
1437
+ parentShadowObserveTotal += m.parentShadowObserve;
1438
+ parentShadowPromoteTotal += m.parentShadowPromote;
1439
+ parentShadowResetTotal += m.parentShadowReset;
1440
+ parentShadowRejectNoReplyTotal += m.parentShadowRejectNoReply;
1441
+ parentShadowRejectNotRootedTotal += m.parentShadowRejectNotRooted;
1442
+ parentShadowRejectCapacityTotal += m.parentShadowRejectCapacity;
1443
+ parentShadowRejectRepairTotal += m.parentShadowRejectRepair;
1444
+ parentShadowRejectLagTotal += m.parentShadowRejectLag;
1445
+ parentShadowRejectOverloadedTotal += m.parentShadowRejectOverloaded;
1446
+ parentShadowRejectLevelTotal += m.parentShadowRejectLevel;
1033
1447
  earningsTotal += m.earnings;
1034
1448
  earningsByHash.set(nodeHash, m.earnings);
1035
1449
  protocolControlSends += m.controlSends;
@@ -1046,6 +1460,9 @@ export const runFanoutTreeSim = async (input) => {
1046
1460
  protocolRepairReqSent += m.repairReqSent;
1047
1461
  protocolFetchReqSent += m.fetchReqSent;
1048
1462
  protocolIHaveSent += m.ihaveSent;
1463
+ protocolTrackerAnnounceSent += m.trackerAnnounceSent;
1464
+ protocolTrackerQuerySent += m.trackerQuerySent;
1465
+ protocolTrackerReplySent += m.trackerReplySent;
1049
1466
  protocolTrackerFeedbackSent += m.trackerFeedbackSent;
1050
1467
  protocolCacheHitsServed += m.cacheHitsServed;
1051
1468
  protocolHoleFillsFromNeighbor += m.holeFillsFromNeighbor;
@@ -1057,8 +1474,25 @@ export const runFanoutTreeSim = async (input) => {
1057
1474
  protocolRouteProxyTimeouts += m.routeProxyTimeouts;
1058
1475
  protocolRouteProxyFanout += m.routeProxyFanout;
1059
1476
  }
1477
+ const upgradedBranchHashSet = new Set();
1478
+ for (const hash of upgradedPeerHashes) {
1479
+ const stack = [hash];
1480
+ while (stack.length > 0) {
1481
+ const next = stack.pop();
1482
+ if (upgradedBranchHashSet.has(next))
1483
+ continue;
1484
+ if (joinedHashes.has(next))
1485
+ upgradedBranchHashSet.add(next);
1486
+ for (const child of tree.childrenByHash.get(next) ?? []) {
1487
+ stack.push(child);
1488
+ }
1489
+ }
1490
+ }
1491
+ const upgradedBranchPeerHashes = [...upgradedBranchHashSet];
1060
1492
  const relayHashes = [...uploadCapByHash.keys()];
1061
- const relayEarnings = relayHashes.map((h) => earningsByHash.get(h) ?? 0).sort((a, b) => a - b);
1493
+ const relayEarnings = relayHashes
1494
+ .map((h) => earningsByHash.get(h) ?? 0)
1495
+ .sort((a, b) => a - b);
1062
1496
  const earningsRelayCount = relayEarnings.length;
1063
1497
  const earningsRelayP50 = earningsRelayCount > 0 ? quantile(relayEarnings, 0.5) : 0;
1064
1498
  const earningsRelayP95 = earningsRelayCount > 0 ? quantile(relayEarnings, 0.95) : 0;
@@ -1068,7 +1502,9 @@ export const runFanoutTreeSim = async (input) => {
1068
1502
  ? 1
1069
1503
  : protocolDataPayloadBytesSent / idealPayloadBytes;
1070
1504
  const deliveredPayloadBytes = delivered * Math.max(0, params.msgSize);
1071
- const controlBpp = deliveredPayloadBytes <= 0 ? 0 : protocolControlBytesSent / deliveredPayloadBytes;
1505
+ const controlBpp = deliveredPayloadBytes <= 0
1506
+ ? 0
1507
+ : protocolControlBytesSent / deliveredPayloadBytes;
1072
1508
  const trackerBpp = deliveredPayloadBytes <= 0
1073
1509
  ? 0
1074
1510
  : protocolControlBytesSentTracker / deliveredPayloadBytes;
@@ -1101,13 +1537,25 @@ export const runFanoutTreeSim = async (input) => {
1101
1537
  formationStretchP95,
1102
1538
  formationStretchMax,
1103
1539
  formationScore,
1104
- publishMs: publishDone - publishStart,
1540
+ publishMs: publishActiveMs,
1105
1541
  expected,
1106
1542
  delivered,
1107
1543
  deliveredPct,
1108
1544
  deliveredWithinDeadline,
1109
1545
  deliveredWithinDeadlinePct,
1110
1546
  duplicates,
1547
+ secondBatchExpected,
1548
+ secondBatchDelivered,
1549
+ secondBatchDeliveredPct,
1550
+ secondBatchDeliveredWithinDeadline,
1551
+ secondBatchDeliveredWithinDeadlinePct,
1552
+ secondBatchLatencySamples: secondBatchDeliveredSamples.length,
1553
+ secondBatchLatencyP50: quantile(secondBatchDeliveredSamples, 0.5),
1554
+ secondBatchLatencyP95: quantile(secondBatchDeliveredSamples, 0.95),
1555
+ secondBatchLatencyP99: quantile(secondBatchDeliveredSamples, 0.99),
1556
+ secondBatchLatencyMax: secondBatchDeliveredSamples[secondBatchDeliveredSamples.length - 1] ??
1557
+ NaN,
1558
+ secondBatchLatencyP95ByHash,
1111
1559
  latencySamples: deliveredSamples.length,
1112
1560
  latencyP50: quantile(deliveredSamples, 0.5),
1113
1561
  latencyP95: quantile(deliveredSamples, 0.95),
@@ -1125,6 +1573,53 @@ export const runFanoutTreeSim = async (input) => {
1125
1573
  reparentDisconnectTotal,
1126
1574
  reparentStaleTotal,
1127
1575
  reparentKickedTotal,
1576
+ publishActiveReparentUpgradeTotal: publishActiveParentUpgrade.reparentUpgrade,
1577
+ publishActiveReparentUpgradeSkipDataTotal: publishActiveParentUpgrade.reparentUpgradeSkipData,
1578
+ publishActiveReparentUpgradeSkipRepairTotal: publishActiveParentUpgrade.reparentUpgradeSkipRepair,
1579
+ publishActiveReparentUpgradeSkipQuietTotal: publishActiveParentUpgrade.reparentUpgradeSkipQuiet,
1580
+ publishActiveParentProbeReqSentTotal: publishActiveParentUpgrade.parentProbeReqSent,
1581
+ publishActiveParentShadowStartTotal: publishActiveParentUpgrade.parentShadowStart,
1582
+ publishActiveParentShadowPromoteTotal: publishActiveParentUpgrade.parentShadowPromote,
1583
+ reparentUpgradeTotal,
1584
+ upgradedPeerHashes,
1585
+ upgradedBranchPeerHashes,
1586
+ reparentUpgradeSkipLeafTotal,
1587
+ reparentUpgradeSkipRepairTotal,
1588
+ reparentUpgradeSkipDataTotal,
1589
+ reparentUpgradeSkipCooldownTotal,
1590
+ reparentUpgradeSkipQuietTotal,
1591
+ reparentUpgradeSkipBudgetTotal,
1592
+ reparentUpgradeSkipCandidateLevelTotal,
1593
+ reparentUpgradeSkipCandidateSlotsTotal,
1594
+ reparentUpgradeSkipCandidatePressureTotal,
1595
+ reparentUpgradeSkipRootPressureTotal,
1596
+ reparentUpgradeSkipProbeNoReplyTotal,
1597
+ reparentUpgradeSkipProbeNotRootedTotal,
1598
+ reparentUpgradeSkipProbeRepairTotal,
1599
+ reparentUpgradeSkipProbeLagTotal,
1600
+ reparentUpgradeSkipProbeOverloadedTotal,
1601
+ reparentUpgradeSkipProbeCooldownTotal,
1602
+ parentProbeReqSentTotal,
1603
+ parentProbeReqReceivedTotal,
1604
+ parentProbeReplySentTotal,
1605
+ parentProbeReplyReceivedTotal,
1606
+ parentUpgradeRootReservationCreatedTotal,
1607
+ parentUpgradeRootReservationConsumedTotal,
1608
+ parentUpgradeRootReservationRejectedTotal,
1609
+ parentUpgradeRootReservationMarginRejectedTotal,
1610
+ parentUpgradeRootReservationBlockedTotal,
1611
+ parentUpgradeRootReservationExpiredTotal,
1612
+ parentShadowStartTotal,
1613
+ parentShadowObserveTotal,
1614
+ parentShadowPromoteTotal,
1615
+ parentShadowResetTotal,
1616
+ parentShadowRejectNoReplyTotal,
1617
+ parentShadowRejectNotRootedTotal,
1618
+ parentShadowRejectCapacityTotal,
1619
+ parentShadowRejectRepairTotal,
1620
+ parentShadowRejectLagTotal,
1621
+ parentShadowRejectOverloadedTotal,
1622
+ parentShadowRejectLevelTotal,
1128
1623
  treeMaxLevel,
1129
1624
  treeLevelP95,
1130
1625
  treeLevelAvg,
@@ -1135,6 +1630,8 @@ export const runFanoutTreeSim = async (input) => {
1135
1630
  maxUploadBps,
1136
1631
  maxUploadFracPct,
1137
1632
  maxUploadNode,
1633
+ rootUploadBps,
1634
+ rootUploadFracPct,
1138
1635
  streamQueuedBytesTotal,
1139
1636
  streamQueuedBytesMax,
1140
1637
  streamQueuedBytesP95,
@@ -1176,6 +1673,9 @@ export const runFanoutTreeSim = async (input) => {
1176
1673
  protocolRepairReqSent,
1177
1674
  protocolFetchReqSent,
1178
1675
  protocolIHaveSent,
1676
+ protocolTrackerAnnounceSent,
1677
+ protocolTrackerQuerySent,
1678
+ protocolTrackerReplySent,
1179
1679
  protocolTrackerFeedbackSent,
1180
1680
  protocolCacheHitsServed,
1181
1681
  protocolHoleFillsFromNeighbor,
@@ -1207,7 +1707,9 @@ export const runFanoutTreeSim = async (input) => {
1207
1707
  timeoutSignal.addEventListener("abort", onAbort, { once: true });
1208
1708
  })
1209
1709
  : undefined;
1210
- const result = abortPromise ? await Promise.race([run(), abortPromise]) : await run();
1710
+ const result = abortPromise
1711
+ ? await Promise.race([run(), abortPromise])
1712
+ : await run();
1211
1713
  if (profileEnabled && profileCpuStart) {
1212
1714
  try {
1213
1715
  profileEld?.disable();