@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.
@@ -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
- status: "ready",
354
- section: applyingSection(
355
- held.runId,
356
- held.previousVersion,
357
- held.attemptedVersion,
358
- "reconnecting",
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 ready(
412
- applyingSection(
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
- kind: "applying",
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 { data, isError, dismissedRunId, checkPress, checkFailure, now } =
478
- input;
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: {