mirror of
https://github.com/mempool/mempool.git
synced 2026-08-13 12:33:11 +02:00
Feat: RBf diff ux improvements
This commit is contained in:
parent
f52c48153b
commit
47ad080b6b
3 changed files with 200 additions and 110 deletions
|
|
@ -1,75 +1,77 @@
|
|||
<div class="rbf-timeline box" [class.mined]="replacements.mined">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline" *ngFor="let timeline of rows; let j = index">
|
||||
<div class="intervals" *ngIf="j < rowLimit || timelineExpanded">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<div class="node-spacer"></div>
|
||||
<ng-container *ngIf="i < timeline.length - 1">
|
||||
<div class="interval" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="cell.replacement.interval" [relative]="false"></app-time>
|
||||
<div class="timeline-viewport">
|
||||
<div class="timeline-wrapper">
|
||||
<div class="timeline" *ngFor="let timeline of rows; let j = index">
|
||||
<div class="intervals" *ngIf="j < rowLimit || timelineExpanded">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<div class="node-spacer"></div>
|
||||
<ng-container *ngIf="i < timeline.length - 1">
|
||||
<div class="interval" *ngIf="cell.replacement?.interval != null; else intervalSpacer">
|
||||
<div class="interval-time">
|
||||
<app-time [time]="cell.replacement.interval" [relative]="false"></app-time>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="nodes" *ngIf="j < rowLimit || timelineExpanded">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
|
||||
<div class="node"
|
||||
[id]="'node-'+cell.replacement.tx.txid"
|
||||
[class.selected]="txid === cell.replacement.tx.txid"
|
||||
[class.mined]="cell.replacement.tx.mined"
|
||||
[class.first-node]="cell.first"
|
||||
[class.diff-old]="highlightedOldTxid === cell.replacement.tx.txid"
|
||||
[class.diff-new]="highlightedNewTxid === cell.replacement.tx.txid"
|
||||
[class.diff-pending]="pendingAnchorTxid === cell.replacement.tx.txid"
|
||||
>
|
||||
<div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
|
||||
<div class="track right" [class.fullrbf]="cell.fullRbf"></div>
|
||||
<!-- with the diff open the node picks the pair to compare, so the
|
||||
link is disabled rather than navigating away mid-comparison.
|
||||
Dropping routerLink also drops the href, so the role, tab stop
|
||||
and keyboard handlers are restored explicitly. -->
|
||||
<a class="shape-border"
|
||||
[class.rbf]="cell.replacement.tx.rbf"
|
||||
[routerLink]="showDiff ? null : ['/tx/' | relativeUrl, cell.replacement.tx.txid]"
|
||||
[attr.role]="showDiff ? 'button' : null"
|
||||
[attr.tabindex]="showDiff ? 0 : null"
|
||||
(click)="onNodeClick($event, cell.replacement);"
|
||||
(keydown.enter)="onNodeClick($event, cell.replacement);"
|
||||
(keydown.space)="onNodeClick($event, cell.replacement);"
|
||||
(pointerover)="onHover($event, cell.replacement);"
|
||||
(pointerout)="onBlur($event);"
|
||||
>
|
||||
<div class="shape"></div>
|
||||
<span class="visually-hidden" *ngIf="showDiff" i18n="rbf-diff.select-transaction">Select this transaction to compare</span>
|
||||
</a>
|
||||
<span class="fee-rate"><app-fee-rate [fee]="cell.replacement.tx.fee" [weight]="cell.replacement.tx.vsize * 4" [unitStyle]="{ display: 'block', marginTop: '-0.5em'}"></app-fee-rate></span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #nonNode>
|
||||
<ng-container [ngSwitch]="cell.connector">
|
||||
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf" [class.last-pipe]="!timelineExpanded && j === rowLimit - 1"></div></div>
|
||||
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||
<div class="node-spacer" *ngSwitchDefault></div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="i < timeline.length - 1">
|
||||
<!-- the segment joining two dots: with the diff open it compares that
|
||||
pair directly, so it needs to be reachable by keyboard too -->
|
||||
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer"
|
||||
[class.diff-selectable]="showDiff && cell.replacement?.replacedBy"
|
||||
[attr.role]="showDiff && cell.replacement?.replacedBy ? 'button' : null"
|
||||
[attr.tabindex]="showDiff && cell.replacement?.replacedBy ? 0 : null"
|
||||
(click)="onEdgeClick($event, cell.replacement)"
|
||||
(keydown.enter)="onEdgeClick($event, cell.replacement)"
|
||||
(keydown.space)="onEdgeClick($event, cell.replacement)">
|
||||
<div class="track" [class.fullrbf]="cell.fullRbf"></div>
|
||||
<span class="visually-hidden" *ngIf="showDiff && cell.replacement?.replacedBy" i18n="rbf-diff.compare-pair">Compare these two transactions</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div class="nodes" *ngIf="j < rowLimit || timelineExpanded">
|
||||
<ng-container *ngFor="let cell of timeline; let i = index;">
|
||||
<ng-container *ngIf="cell.replacement?.tx; else nonNode">
|
||||
<div class="node"
|
||||
[id]="'node-'+cell.replacement.tx.txid"
|
||||
[class.selected]="txid === cell.replacement.tx.txid"
|
||||
[class.mined]="cell.replacement.tx.mined"
|
||||
[class.first-node]="cell.first"
|
||||
[class.diff-old]="highlightedOldTxid === cell.replacement.tx.txid"
|
||||
[class.diff-new]="highlightedNewTxid === cell.replacement.tx.txid"
|
||||
[class.diff-pending]="pendingAnchorTxid === cell.replacement.tx.txid"
|
||||
>
|
||||
<div class="track left" [class.fullrbf]="cell.replacement?.tx?.fullRbf"></div>
|
||||
<div class="track right" [class.fullrbf]="cell.fullRbf"></div>
|
||||
<!-- with the diff open the node picks the pair to compare, so the
|
||||
link is disabled rather than navigating away mid-comparison.
|
||||
Dropping routerLink also drops the href, so the role, tab stop
|
||||
and keyboard handlers are restored explicitly. -->
|
||||
<a class="shape-border"
|
||||
[class.rbf]="cell.replacement.tx.rbf"
|
||||
[routerLink]="showDiff ? null : ['/tx/' | relativeUrl, cell.replacement.tx.txid]"
|
||||
[attr.role]="showDiff ? 'button' : null"
|
||||
[attr.tabindex]="showDiff ? 0 : null"
|
||||
(click)="onNodeClick($event, cell.replacement);"
|
||||
(keydown.enter)="onNodeClick($event, cell.replacement);"
|
||||
(keydown.space)="onNodeClick($event, cell.replacement);"
|
||||
(pointerover)="onHover($event, cell.replacement);"
|
||||
(pointerout)="onBlur($event);"
|
||||
>
|
||||
<div class="shape"></div>
|
||||
<span class="visually-hidden" *ngIf="showDiff" i18n="rbf-diff.select-transaction">Select this transaction to compare</span>
|
||||
</a>
|
||||
<span class="fee-rate"><app-fee-rate [fee]="cell.replacement.tx.fee" [weight]="cell.replacement.tx.vsize * 4" [unitStyle]="{ display: 'block', marginTop: '-0.5em'}"></app-fee-rate></span>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ng-template #nonNode>
|
||||
<ng-container [ngSwitch]="cell.connector">
|
||||
<div class="connector" [class.fullrbf]="cell.fullRbf" *ngSwitchCase="'pipe'"><div class="pipe" [class.fullrbf]="cell.fullRbf" [class.last-pipe]="!timelineExpanded && j === rowLimit - 1"></div></div>
|
||||
<div class="connector" *ngSwitchCase="'corner'"><div class="corner" [class.fullrbf]="cell.fullRbf"></div></div>
|
||||
<div class="node-spacer" *ngSwitchDefault></div>
|
||||
</ng-container>
|
||||
</ng-template>
|
||||
<ng-container *ngIf="i < timeline.length - 1">
|
||||
<!-- the segment joining two dots: with the diff open it compares that
|
||||
pair directly, so it needs to be reachable by keyboard too -->
|
||||
<div class="interval-spacer" *ngIf="cell.replacement?.interval != null; else intervalSpacer"
|
||||
[class.diff-selectable]="showDiff && cell.replacement?.replacedBy"
|
||||
[attr.role]="showDiff && cell.replacement?.replacedBy ? 'button' : null"
|
||||
[attr.tabindex]="showDiff && cell.replacement?.replacedBy ? 0 : null"
|
||||
(click)="onEdgeClick($event, cell.replacement)"
|
||||
(keydown.enter)="onEdgeClick($event, cell.replacement)"
|
||||
(keydown.space)="onEdgeClick($event, cell.replacement)">
|
||||
<div class="track" [class.fullrbf]="cell.fullRbf"></div>
|
||||
<span class="visually-hidden" *ngIf="showDiff && cell.replacement?.replacedBy" i18n="rbf-diff.compare-pair">Compare these two transactions</span>
|
||||
</div>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -97,13 +99,15 @@
|
|||
</div>
|
||||
</div>
|
||||
|
||||
<!-- RBF Diff Error -->
|
||||
<div class="diff-error alert alert-danger" *ngIf="diffError" i18n="rbf-diff.load-error">
|
||||
Failed to load transaction diff.
|
||||
<!-- The comparison could not be loaded after retrying. Kept deliberately quiet:
|
||||
the previous comparison stays on screen and clicking again retries. -->
|
||||
<div class="diff-notice" *ngIf="showDiff && diffError" i18n="rbf-diff.load-error">
|
||||
This comparison could not be loaded. Select it again to retry.
|
||||
</div>
|
||||
|
||||
<!-- RBF Structural Diff Content -->
|
||||
<div class="rbf-diff-section" *ngIf="showDiff && !diffLoading && diffView && selectedOldTx && selectedNewTx">
|
||||
<!-- no !diffLoading here: the previous comparison stays visible while the next one loads -->
|
||||
<div class="rbf-diff-section" *ngIf="showDiff && diffView && selectedOldTx && selectedNewTx">
|
||||
<div class="row">
|
||||
<!-- Left Column: Previous Transaction -->
|
||||
<div class="col-sm">
|
||||
|
|
@ -177,8 +181,9 @@
|
|||
<td i18n="transaction.fee|Transaction fee">Fee</td>
|
||||
<td>
|
||||
<app-amount [satoshis]="tx.fee" [noFiat]="true" [forceSats]="true"></app-amount>
|
||||
<!-- up is green and down is red, consistently for every metric -->
|
||||
<span *ngIf="side === 'current' && diff.feePercent !== null" class="difference"
|
||||
[class.positive]="!diff.feeIncreased" [class.negative]="diff.feeIncreased">
|
||||
[class.positive]="diff.feeIncreased" [class.negative]="!diff.feeIncreased">
|
||||
{{ diff.feePercent > 0 ? '+' : '' }}{{ diff.feePercent | number: '1.0-1' }}%
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -200,7 +205,7 @@
|
|||
<span *only-weight [innerHTML]="'‎' + (tx.weight | wuBytes: 2)"></span>
|
||||
<span *only-vsize [innerHTML]="'‎' + (tx.weight / 4 | vbytes: 2)"></span>
|
||||
<span *ngIf="side === 'current' && diff.weightPercent !== null" class="difference"
|
||||
[class.positive]="!diff.weightIncreased" [class.negative]="diff.weightIncreased">
|
||||
[class.positive]="diff.weightIncreased" [class.negative]="!diff.weightIncreased">
|
||||
{{ diff.weightPercent > 0 ? '+' : '' }}{{ diff.weightPercent | number: '1.0-1' }}%
|
||||
</span>
|
||||
</td>
|
||||
|
|
@ -246,29 +251,36 @@
|
|||
bump is not flagged as a change at all.
|
||||
-->
|
||||
<tr *ngFor="let row of diff.outputRows" [class.address-changed]="row.addressChanged">
|
||||
<td i18n="rbf-diff.output-address">Output address</td>
|
||||
<td>
|
||||
<span *ngIf="row.addressChanged" i18n="rbf-diff.output-address-changed">Output (address changed)</span>
|
||||
<span *ngIf="!row.addressChanged && row.feeAdjusted" i18n="rbf-diff.output-fee-adjusted">Output (fee adjusted)</span>
|
||||
<span *ngIf="!row.addressChanged && !row.feeAdjusted" i18n="transaction.output">Output</span>
|
||||
</td>
|
||||
<td>
|
||||
<ng-container *ngIf="(side === 'previous' ? row.previous : row.current) as out; else notApplicable">
|
||||
<!-- app-truncate only ellipsizes when its flex container is bounded, so
|
||||
this stays block-level rather than inline -->
|
||||
<span class="output-address" *ngIf="out.scriptpubkey_address; else scriptType">
|
||||
<app-truncate [text]="out.scriptpubkey_address" [lastChars]="8"
|
||||
[link]="['/address/' | relativeUrl, out.scriptpubkey_address]"></app-truncate>
|
||||
<ng-container *ngIf="(side === 'previous' ? row.previous : row.current) as out; else noOutput">
|
||||
<span class="output-address-row">
|
||||
<!-- app-truncate only ellipsizes when its flex container is bounded, so
|
||||
this stays block-level rather than inline -->
|
||||
<span class="output-address" *ngIf="out.scriptpubkey_address; else scriptType">
|
||||
<app-truncate [text]="out.scriptpubkey_address" [lastChars]="8"
|
||||
[link]="['/address/' | relativeUrl, out.scriptpubkey_address]"></app-truncate>
|
||||
</span>
|
||||
<ng-template #scriptType>
|
||||
<span class="output-script-type">{{ out.scriptpubkey_type | scriptpubkeyType }}</span>
|
||||
</ng-template>
|
||||
<span class="output-warning" *ngIf="side === 'current' && row.addressChanged" role="img"
|
||||
i18n-ngbTooltip="rbf-diff.output-address-changed" ngbTooltip="Output address changed"
|
||||
i18n-aria-label="rbf-diff.output-address-changed" aria-label="Output address changed">⚠️</span>
|
||||
</span>
|
||||
<ng-template #scriptType>
|
||||
<span class="output-script-type">{{ out.scriptpubkey_type | scriptpubkeyType }}</span>
|
||||
</ng-template>
|
||||
<span class="output-value">
|
||||
<app-amount [satoshis]="out.value" [noFiat]="true"></app-amount>
|
||||
<ng-container *ngIf="side === 'current' && row.addressChanged">
|
||||
<ng-container *ngTemplateOutlet="changedDot"></ng-container>
|
||||
</ng-container>
|
||||
</span>
|
||||
</ng-container>
|
||||
<!-- an added or removed output has nothing on one side, but the placeholder
|
||||
still has to be as tall as an address plus its amount, otherwise the two
|
||||
tables stop lining up row by row -->
|
||||
<ng-template #noOutput>
|
||||
<span class="output-address-row">
|
||||
<span class="text-muted null-placeholder" i18n-aria-label="rbf-diff.not-applicable" aria-label="Not applicable">—</span>
|
||||
</span>
|
||||
<span class="output-value"> </span>
|
||||
</ng-template>
|
||||
</td>
|
||||
</tr>
|
||||
</ng-template>
|
||||
|
|
|
|||
|
|
@ -3,19 +3,15 @@
|
|||
width: 100%;
|
||||
padding: 1em 0;
|
||||
|
||||
.timeline-wrapper {
|
||||
// Non-scrolling frame around the scroller. The edge fades live here because an
|
||||
// absolutely positioned child of an overflow container scrolls away with its
|
||||
// contents, which left the fade sitting mid-timeline covering the track.
|
||||
// It wraps only the timeline, so the fades never reach the diff tables below.
|
||||
.timeline-viewport {
|
||||
position: relative;
|
||||
width: calc(100% - 2em);
|
||||
margin: auto;
|
||||
overflow-x: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
|
||||
// scoped to the timeline so the gradients don't overlay the diff tables below
|
||||
&::after, &::before {
|
||||
content: '';
|
||||
display: block;
|
||||
|
|
@ -37,7 +33,7 @@
|
|||
background: linear-gradient(to left, var(--box-bg), var(--box-bg), transparent);
|
||||
}
|
||||
|
||||
// On a phone the wrapper is barely wider than a couple of nodes, so a 2em
|
||||
// On a phone the viewport is barely wider than a couple of nodes, so a 2em
|
||||
// band of solid background reads as the line being cut rather than fading.
|
||||
@media (max-width: 575.98px) {
|
||||
&::before, &::after {
|
||||
|
|
@ -54,6 +50,18 @@
|
|||
}
|
||||
}
|
||||
|
||||
// owns horizontal scrolling and nothing else
|
||||
.timeline-wrapper {
|
||||
width: 100%;
|
||||
overflow-x: auto;
|
||||
-ms-overflow-style: none;
|
||||
scrollbar-width: none;
|
||||
|
||||
&::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.fade-out {
|
||||
position: relative;
|
||||
|
||||
|
|
@ -118,6 +126,12 @@
|
|||
|
||||
&:hover .track {
|
||||
background: var(--info);
|
||||
|
||||
// a full RBF track is already --info, so the usual highlight would be
|
||||
// invisible on exactly the lines that need to look clickable
|
||||
&.fullrbf {
|
||||
background: var(--fg);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -295,6 +309,15 @@
|
|||
width: 100%;
|
||||
padding: 1rem 0;
|
||||
}
|
||||
|
||||
.diff-notice {
|
||||
width: calc(100% - 2em);
|
||||
margin: 0.5em auto 0;
|
||||
padding: 0 0.5em;
|
||||
text-align: center;
|
||||
font-size: 0.85rem;
|
||||
color: var(--transparent-fg);
|
||||
}
|
||||
}
|
||||
|
||||
.rbf-diff-section {
|
||||
|
|
@ -402,20 +425,43 @@
|
|||
color: var(--red);
|
||||
}
|
||||
|
||||
.output-address {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
// the warning sits next to the address, so the address has to be the part that
|
||||
// shrinks when the column is narrow
|
||||
.output-address-row {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.35rem;
|
||||
// set here rather than on .output-address so the placeholder for an added or
|
||||
// removed output measures exactly the same as a real address
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.output-address {
|
||||
min-width: 0;
|
||||
flex: 1 1 auto;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.output-warning {
|
||||
flex: 0 0 auto;
|
||||
font-size: 0.8rem;
|
||||
cursor: help;
|
||||
}
|
||||
|
||||
.output-script-type {
|
||||
font-size: 0.9rem;
|
||||
color: var(--transparent-fg);
|
||||
}
|
||||
|
||||
// explicit line heights so a populated cell and a placeholder cell measure the
|
||||
// same, which is what keeps the two tables aligned row by row
|
||||
.output-address-row {
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
.output-value {
|
||||
display: block;
|
||||
line-height: 1.6;
|
||||
font-size: 0.85rem;
|
||||
color: var(--transparent-fg);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,8 +3,8 @@ import { Router } from '@angular/router';
|
|||
import { RbfTree, RbfTransaction } from '@interfaces/node-api.interface';
|
||||
import { StateService } from '@app/services/state.service';
|
||||
import { ApiService } from '@app/services/api.service';
|
||||
import { forkJoin, of, Subject } from 'rxjs';
|
||||
import { catchError, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { forkJoin, of, Observable, Subject } from 'rxjs';
|
||||
import { catchError, map, retry, switchMap, takeUntil } from 'rxjs/operators';
|
||||
import { Transaction, Vout } from '@interfaces/electrs.interface';
|
||||
import { calculateRbfDiff } from '@app/shared/rbf-diff.utils';
|
||||
|
||||
|
|
@ -89,6 +89,9 @@ export class RbfTimelineComponent implements OnInit, OnChanges, OnDestroy {
|
|||
// First half of a two-click selection, waiting for the user to pick the other end
|
||||
pendingAnchorTxid: string | null = null;
|
||||
|
||||
// the pair the tables currently show, which can lag behind the selection while
|
||||
// a new comparison is loading or after one failed to load
|
||||
private renderedPair: { oldTxid: string, newTxid: string } | null = null;
|
||||
private nodeIndex = new Map<string, RbfTree>();
|
||||
private destroy$ = new Subject<void>();
|
||||
// Comparisons go through one stream so a slower earlier request can never land
|
||||
|
|
@ -108,8 +111,8 @@ export class RbfTimelineComponent implements OnInit, OnChanges, OnDestroy {
|
|||
// can already have queued a comparison
|
||||
this.diffRequest$.pipe(
|
||||
switchMap((request) => request ? forkJoin({
|
||||
oldTx: this.apiService.getRbfCachedTx$(request.oldTxid).pipe(catchError(() => of(null))),
|
||||
newTx: this.apiService.getRbfCachedTx$(request.newTxid).pipe(catchError(() => of(null))),
|
||||
oldTx: this.fetchCachedTx$(request.oldTxid),
|
||||
newTx: this.fetchCachedTx$(request.newTxid),
|
||||
}) : of(null)),
|
||||
takeUntil(this.destroy$),
|
||||
).subscribe((result) => {
|
||||
|
|
@ -119,11 +122,18 @@ export class RbfTimelineComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.diffLoading = false;
|
||||
if (!result.oldTx || !result.newTx) {
|
||||
this.diffError = true;
|
||||
// stay on the comparison that is still on screen, so the highlighted
|
||||
// pair keeps matching the table below it
|
||||
if (this.renderedPair) {
|
||||
this.diffOldTxid = this.renderedPair.oldTxid;
|
||||
this.diffNewTxid = this.renderedPair.newTxid;
|
||||
}
|
||||
return;
|
||||
}
|
||||
this.selectedOldTx = result.oldTx;
|
||||
this.selectedNewTx = result.newTx;
|
||||
this.diffView = this.buildDiffView(result.oldTx, result.newTx);
|
||||
this.renderedPair = { oldTxid: result.oldTx.txid, newTxid: result.newTx.txid };
|
||||
});
|
||||
}
|
||||
|
||||
|
|
@ -481,6 +491,7 @@ export class RbfTimelineComponent implements OnInit, OnChanges, OnDestroy {
|
|||
this.selectedOldTx = null;
|
||||
this.selectedNewTx = null;
|
||||
this.diffView = null;
|
||||
this.renderedPair = null;
|
||||
}
|
||||
|
||||
private loadDiff(): void {
|
||||
|
|
@ -489,10 +500,31 @@ export class RbfTimelineComponent implements OnInit, OnChanges, OnDestroy {
|
|||
}
|
||||
this.diffError = false;
|
||||
this.diffLoading = true;
|
||||
this.diffView = null;
|
||||
// diffView is deliberately left alone: a slow or failed comparison shouldn't
|
||||
// wipe the table the user is already reading
|
||||
this.diffRequest$.next({ oldTxid: this.diffOldTxid, newTxid: this.diffNewTxid });
|
||||
}
|
||||
|
||||
/**
|
||||
* The RBF cache lives in the memory of each backend instance, so the same txid
|
||||
* can come back empty from one instance and populated from another. An empty
|
||||
* body arrives as HTTP 204, which is a *success* with a null body rather than
|
||||
* an error, so it has to be turned into one for the retry to see it. Retrying
|
||||
* usually lands on an instance that has the transaction.
|
||||
*/
|
||||
private fetchCachedTx$(txid: string): Observable<Transaction | null> {
|
||||
return this.apiService.getRbfCachedTx$(txid).pipe(
|
||||
map((tx) => {
|
||||
if (!tx) {
|
||||
throw new Error(`no cached transaction for ${txid}`);
|
||||
}
|
||||
return tx;
|
||||
}),
|
||||
retry({ count: 2, delay: 400 }),
|
||||
catchError(() => of(null)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Reduces the structural diff to just what the tables render. Rows for
|
||||
* unchanged fields are left out entirely rather than rendered as noise.
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue