forked from OurHike/OurHike
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbackgroundStatus.ts
More file actions
191 lines (173 loc) · 7.53 KB
/
Copy pathbackgroundStatus.ts
File metadata and controls
191 lines (173 loc) · 7.53 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
// One answer about the background, out of however many archives it is made
// of (#192).
//
// The background is one thing a hiker chooses and downloads - the DEM, the
// raster sheet and the vector basemap are pieces of it, not items on a
// checklist (lib/packages.ts). The store underneath is still per-archive,
// because that is what lets one piece resume, fail or be evicted without
// touching the others. This is the join between the two: N archive states in,
// one status out, for a screen that shows one card.
//
// The precedence below is ordered by what a hiker most needs to know, not by
// what is most common:
//
// 1. Something is transferring - a live figure outranks everything.
// 1b. Something is being CHECKED - the phone reading back bytes it holds
// (#197). Work in progress, so it outranks
// every resting state below.
// 1c. Something was REFUSED - a finished archive matched no published
// build and was not kept (#238). Above
// eviction because it just happened, in
// this session, to the thing the hiker is
// watching - and its remedy (start over)
// contradicts every offer below it.
// 2. Something was EVICTED - "the phone removed your map" is the one
// sentence #190 exists for, and it is
// more urgent than "some is missing",
// because it explains why.
// 3. Part of it is here - offer to carry on, never to restart.
// 4. All of it is here - the finished state.
// 5. None of it is here.
//
// Case 3 covers two situations that behave identically and are worth naming:
// an archive whose transfer stopped partway, and an archive that never
// started while its siblings finished. Both mean the background is partly on
// the phone, both are fixed by carrying on from where it got to, and the
// "Stopped at X of Y" the card renders is literally true of each.
//
// EVERY MULTI-ARCHIVE PATH HERE IS TEST-ONLY TODAY. The raster sheet is the
// only piece the pipeline publishes (#185/#186 are the other two), so the
// shipped app always combines exactly one status and gets it back unchanged.
import type { DownloadStatus } from '../screens/DownloadCard'
/**
* Whether this sheet's next tap fetches its whole size from zero.
*
* Two questions turn on the same answer, which is why they share one
* function: whether a too-small disk is worth warning about before the tap
* (screens/Downloads.tsx), and whether the detail level is still a live
* choice (screens/DownloadCard.tsx). Both are asking "is a whole download
* about to start from here" - and a second copy of the state list would be
* a second place for the answer to drift.
*/
export function facingFullDownload(status: DownloadStatus): boolean {
return (
status.state === 'not-downloaded' ||
status.state === 'evicted' ||
status.state === 'hash-mismatch'
)
}
/** One archive's contribution to the whole. */
export interface ArchiveState {
status: DownloadStatus
/** Its measured published size. Never absent - see OfferedPackage. */
sizeBytes: number
}
const NOT_DOWNLOADED: DownloadStatus = { state: 'not-downloaded' }
/**
* Bytes here and bytes expected, for one archive.
*
* An archive that has not started contributes its PUBLISHED size as the
* expectation, so a background part-way through does not show a total that
* grows as each piece begins - which would read as a download that keeps
* getting bigger.
*/
function bytesOf({ status, sizeBytes }: ArchiveState): {
received: number
total: number
} {
switch (status.state) {
case 'downloading':
case 'failed':
return { received: status.receivedBytes, total: status.totalBytes }
case 'downloaded':
return { received: status.totalBytes, total: status.totalBytes }
case 'not-downloaded':
case 'evicted':
// A refused archive kept nothing, so its contribution is exactly an
// absent one's: zero here, its published size still owed (#238).
case 'hash-mismatch':
// An archive being checked holds partial bytes, but how many is not what
// `checkedBytes` counts - that is progress through the re-read. Its
// published size is the honest expectation until the transfer resumes and
// starts reporting real figures.
case 'checking':
return { received: 0, total: sizeBytes }
}
}
function sum(archives: readonly ArchiveState[]) {
return archives.reduce(
(running, archive) => {
const { received, total } = bytesOf(archive)
return { received: running.received + received, total: running.total + total }
},
{ received: 0, total: 0 },
)
}
/** The most recent completion among archives that have one, or null. Most
* recent rather than earliest: it answers "when was this map last whole". */
function latestCompletion(archives: readonly ArchiveState[]): Date | null {
const dates = archives
.map(({ status }) =>
status.state === 'downloaded' || status.state === 'evicted'
? status.completedAt
: null,
)
.filter((date): date is Date => date !== null)
if (dates.length === 0) return null
return dates.reduce((latest, date) => (date > latest ? date : latest))
}
export function combineBackgroundStatus(
archives: readonly ArchiveState[],
): DownloadStatus {
if (archives.length === 0) return NOT_DOWNLOADED
const states = archives.map(({ status }) => status.state)
const { received, total } = sum(archives)
if (states.includes('downloading')) {
return { state: 'downloading', receivedBytes: received, totalBytes: total }
}
// Below a live transfer and above everything else: it is work in progress,
// so offering "Resume" or announcing an eviction underneath it would be
// describing a phone that is already busy doing the thing.
if (states.includes('checking')) {
const checking = archives.filter(({ status }) => status.state === 'checking')
return {
state: 'checking',
checkedBytes: checking.reduce(
(n, { status }) => n + (status.state === 'checking' ? status.checkedBytes : 0),
0,
),
totalBytes: checking.reduce(
(n, { status }) => n + (status.state === 'checking' ? status.totalBytes : 0),
0,
),
}
}
// Above eviction and every resting state: the refusal happened in this
// session, to a download the hiker is watching, and the card's start-over
// offer must not be displaced by a sibling's Resume - "Stopped at X of Y"
// would be false of an archive that kept nothing (#238).
if (states.includes('hash-mismatch')) {
return { state: 'hash-mismatch' }
}
if (states.includes('evicted')) {
return { state: 'evicted', completedAt: latestCompletion(archives) }
}
const anyHere = states.some(
(state) => state === 'downloaded' || state === 'failed' || state === 'downloading',
)
const allHere = states.every((state) => state === 'downloaded')
if (allHere) {
const completedAt = latestCompletion(archives)
return {
state: 'downloaded',
totalBytes: total,
// Non-null whenever every archive is downloaded, since that state
// carries a date - the fallback keeps this total rather than partial.
completedAt: completedAt ?? new Date(),
}
}
if (anyHere) {
return { state: 'failed', receivedBytes: received, totalBytes: total }
}
return NOT_DOWNLOADED
}