@remit/web-client 0.0.269 → 0.0.271
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/components/mail/MessageList.run-reporting.test.ts +1 -1
- package/src/components/mail/SelectionWizardHost.tsx +15 -50
- package/src/components/mail/bulk-run-state.test.ts +100 -0
- package/src/components/mail/bulk-run-state.ts +99 -0
- package/src/hooks/escalated-run-lifetime.render.test.ts +87 -6
- package/src/hooks/update-restart-escalation.render.test.ts +340 -0
- package/src/hooks/use-system-update.ts +49 -2
- package/src/hooks/useEscalatedActions.ts +45 -24
- package/src/lib/bulk-action-copy.test.ts +14 -4
- package/src/lib/bulk-action-copy.ts +5 -3
- package/src/lib/bulk-actions.test.ts +115 -26
- package/src/lib/bulk-actions.ts +40 -10
- package/src/lib/error-classifier.test.ts +59 -0
- package/src/lib/error-classifier.ts +48 -2
- package/src/lib/self-update-state.test.ts +1 -0
- package/src/lib/self-update-state.ts +138 -30
|
@@ -72,6 +72,21 @@ export interface CheckPress {
|
|
|
72
72
|
since: string | undefined;
|
|
73
73
|
}
|
|
74
74
|
|
|
75
|
+
/**
|
|
76
|
+
* When this tab first saw the run the server reported as going, by its own
|
|
77
|
+
* clock. A run's `startedAt` is written on the box, and a box whose clock is
|
|
78
|
+
* twenty minutes behind the browser's would put that start far enough in the
|
|
79
|
+
* past to spend the whole apply budget before the first failed request — the
|
|
80
|
+
* screen would open on "the server never came back". `CheckPress` keeps its wait
|
|
81
|
+
* the client's own for the same reason. The runId travels with it so a second
|
|
82
|
+
* run never inherits the first one's wait.
|
|
83
|
+
*/
|
|
84
|
+
export interface RunSighting {
|
|
85
|
+
runId: string;
|
|
86
|
+
/** Epoch millis, this tab's clock, of the first answer that reported it going. */
|
|
87
|
+
observedAt: number;
|
|
88
|
+
}
|
|
89
|
+
|
|
75
90
|
export function checkAnswered(
|
|
76
91
|
press: CheckPress,
|
|
77
92
|
data: RemitImapSystemUpdateResponse | undefined,
|
|
@@ -126,6 +141,8 @@ export interface DeriveInput {
|
|
|
126
141
|
checkPress: CheckPress | null;
|
|
127
142
|
/** Why the request that press fired failed, or null when it did not. */
|
|
128
143
|
checkFailure: string | null;
|
|
144
|
+
/** When this tab first saw the server's run going, for a wait it can trust. */
|
|
145
|
+
sighting: RunSighting | null;
|
|
129
146
|
now: number;
|
|
130
147
|
}
|
|
131
148
|
|
|
@@ -163,6 +180,22 @@ export function isSurfaceAbsent(error: unknown): boolean {
|
|
|
163
180
|
return status === 404 || status === 403 || status === 401;
|
|
164
181
|
}
|
|
165
182
|
|
|
183
|
+
/**
|
|
184
|
+
* The run the server last reported as still going, or null when it reported
|
|
185
|
+
* none. The last answer outlives the failed requests that follow it, which is
|
|
186
|
+
* what lets any tab — not only the one that pressed install — know it is inside
|
|
187
|
+
* a restart rather than looking at an unreachable service (#468). A run the user
|
|
188
|
+
* has dismissed is not one either surface still speaks for.
|
|
189
|
+
*/
|
|
190
|
+
export function runInFlight(
|
|
191
|
+
data: RemitImapSystemUpdateResponse | undefined,
|
|
192
|
+
dismissedRunId: string | null,
|
|
193
|
+
): RemitImapSystemUpdateRun | null {
|
|
194
|
+
const run = data?.run ?? null;
|
|
195
|
+
if (run === null || run.outcome !== null) return null;
|
|
196
|
+
return run.runId === dismissedRunId ? null : run;
|
|
197
|
+
}
|
|
198
|
+
|
|
166
199
|
/**
|
|
167
200
|
* The available release from the check block, or undefined when the check
|
|
168
201
|
* reports none. Independent of what the pane is currently showing, so a retry
|
|
@@ -343,29 +376,75 @@ function ready(
|
|
|
343
376
|
return { surface: { status: "ready", section, overlay }, releaseHeld };
|
|
344
377
|
}
|
|
345
378
|
|
|
379
|
+
/**
|
|
380
|
+
* The wait every tab is in while the server it is polling is not answering: the
|
|
381
|
+
* run is known to be in flight, and the phase is the only one the client can
|
|
382
|
+
* claim from a dead connection.
|
|
383
|
+
*/
|
|
384
|
+
function reconnectingSurface(
|
|
385
|
+
runId: string,
|
|
386
|
+
previousVersion: string,
|
|
387
|
+
attemptedVersion: string,
|
|
388
|
+
elapsedSeconds: number,
|
|
389
|
+
): UpdateSurface {
|
|
390
|
+
return {
|
|
391
|
+
status: "ready",
|
|
392
|
+
section: applyingSection(
|
|
393
|
+
runId,
|
|
394
|
+
previousVersion,
|
|
395
|
+
attemptedVersion,
|
|
396
|
+
"reconnecting",
|
|
397
|
+
elapsedSeconds,
|
|
398
|
+
),
|
|
399
|
+
overlay: {
|
|
400
|
+
kind: "applying",
|
|
401
|
+
target: attemptedVersion,
|
|
402
|
+
phase: "reconnecting",
|
|
403
|
+
elapsedSeconds,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/** The client gave up waiting, and says so without claiming the rollback ran. */
|
|
409
|
+
function neverCameBackSurface(
|
|
410
|
+
runId: string,
|
|
411
|
+
previousVersion: string,
|
|
412
|
+
attemptedVersion: string,
|
|
413
|
+
elapsedSeconds: number,
|
|
414
|
+
logsCommand: string,
|
|
415
|
+
): UpdateSurface {
|
|
416
|
+
return {
|
|
417
|
+
status: "ready",
|
|
418
|
+
section: applyingSection(
|
|
419
|
+
runId,
|
|
420
|
+
previousVersion,
|
|
421
|
+
attemptedVersion,
|
|
422
|
+
"reconnecting",
|
|
423
|
+
elapsedSeconds,
|
|
424
|
+
),
|
|
425
|
+
overlay: {
|
|
426
|
+
kind: "neverCameBack",
|
|
427
|
+
attemptedVersion,
|
|
428
|
+
previousVersion,
|
|
429
|
+
elapsedSeconds,
|
|
430
|
+
logsCommand,
|
|
431
|
+
},
|
|
432
|
+
};
|
|
433
|
+
}
|
|
434
|
+
|
|
346
435
|
/**
|
|
347
436
|
* The client gave up waiting. The hold stays: the screen has to sit still, and
|
|
348
437
|
* its retry has to keep polling, until the server answers for itself.
|
|
349
438
|
*/
|
|
350
439
|
function neverCameBack(held: HeldRun, elapsedSeconds: number): DeriveResult {
|
|
351
440
|
return {
|
|
352
|
-
surface:
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
elapsedSeconds,
|
|
360
|
-
),
|
|
361
|
-
overlay: {
|
|
362
|
-
kind: "neverCameBack",
|
|
363
|
-
attemptedVersion: held.attemptedVersion,
|
|
364
|
-
previousVersion: held.previousVersion,
|
|
365
|
-
elapsedSeconds,
|
|
366
|
-
logsCommand: FALLBACK_LOGS_COMMAND,
|
|
367
|
-
},
|
|
368
|
-
},
|
|
441
|
+
surface: neverCameBackSurface(
|
|
442
|
+
held.runId,
|
|
443
|
+
held.previousVersion,
|
|
444
|
+
held.attemptedVersion,
|
|
445
|
+
elapsedSeconds,
|
|
446
|
+
FALLBACK_LOGS_COMMAND,
|
|
447
|
+
),
|
|
369
448
|
releaseHeld: false,
|
|
370
449
|
};
|
|
371
450
|
}
|
|
@@ -408,22 +487,15 @@ function deriveHeld(
|
|
|
408
487
|
if (elapsedSeconds > budgetLimitSeconds()) {
|
|
409
488
|
return neverCameBack(held, elapsedSeconds);
|
|
410
489
|
}
|
|
411
|
-
return
|
|
412
|
-
|
|
490
|
+
return {
|
|
491
|
+
surface: reconnectingSurface(
|
|
413
492
|
held.runId,
|
|
414
493
|
held.previousVersion,
|
|
415
494
|
held.attemptedVersion,
|
|
416
|
-
"reconnecting",
|
|
417
495
|
elapsedSeconds,
|
|
418
496
|
),
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
target: held.attemptedVersion,
|
|
422
|
-
phase: "reconnecting",
|
|
423
|
-
elapsedSeconds,
|
|
424
|
-
},
|
|
425
|
-
false,
|
|
426
|
-
);
|
|
497
|
+
releaseHeld: false,
|
|
498
|
+
};
|
|
427
499
|
}
|
|
428
500
|
|
|
429
501
|
// Nothing has come back yet. Silence is not a phase, so the surface waits
|
|
@@ -474,10 +546,46 @@ function deriveHeld(
|
|
|
474
546
|
}
|
|
475
547
|
|
|
476
548
|
function displayFromData(input: DeriveInput): UpdateSurface {
|
|
477
|
-
const {
|
|
478
|
-
|
|
549
|
+
const {
|
|
550
|
+
data,
|
|
551
|
+
isError,
|
|
552
|
+
dismissedRunId,
|
|
553
|
+
checkPress,
|
|
554
|
+
checkFailure,
|
|
555
|
+
sighting,
|
|
556
|
+
now,
|
|
557
|
+
} = input;
|
|
479
558
|
|
|
480
559
|
if (isError) {
|
|
560
|
+
const stopped = runInFlight(data, dismissedRunId);
|
|
561
|
+
// A tab that never pressed install is in the same restart as the one that
|
|
562
|
+
// did: the run the server last reported is still going, and the server
|
|
563
|
+
// stopping is how it goes. The wait runs from when this tab saw the run,
|
|
564
|
+
// never from the `startedAt` the box wrote — same budget, on a clock the
|
|
565
|
+
// tab owns. A run in flight this tab has no sighting of has only just
|
|
566
|
+
// arrived, so the wait starts here.
|
|
567
|
+
if (stopped !== null) {
|
|
568
|
+
const observedAt =
|
|
569
|
+
sighting !== null && sighting.runId === stopped.runId
|
|
570
|
+
? sighting.observedAt
|
|
571
|
+
: now;
|
|
572
|
+
const elapsedSeconds = elapsedSince(observedAt, now);
|
|
573
|
+
if (elapsedSeconds > budgetLimitSeconds()) {
|
|
574
|
+
return neverCameBackSurface(
|
|
575
|
+
stopped.runId,
|
|
576
|
+
stopped.fromVersion,
|
|
577
|
+
stopped.targetVersion,
|
|
578
|
+
elapsedSeconds,
|
|
579
|
+
stopped.logCommand,
|
|
580
|
+
);
|
|
581
|
+
}
|
|
582
|
+
return reconnectingSurface(
|
|
583
|
+
stopped.runId,
|
|
584
|
+
stopped.fromVersion,
|
|
585
|
+
stopped.targetVersion,
|
|
586
|
+
elapsedSeconds,
|
|
587
|
+
);
|
|
588
|
+
}
|
|
481
589
|
return {
|
|
482
590
|
status: "ready",
|
|
483
591
|
section: {
|