|
|
|
@ -5,7 +5,12 @@ type KnownCategories = 'people'|'planets';
@@ -5,7 +5,12 @@ type KnownCategories = 'people'|'planets';
|
|
|
|
|
const API_PATH = 'https://swapi.dev/api/' |
|
|
|
|
|
|
|
|
|
export class HTTPError extends Error { |
|
|
|
|
constructor (response: Response) { |
|
|
|
|
name: 'HTTPError' |
|
|
|
|
status: number |
|
|
|
|
statusText: string |
|
|
|
|
url: string |
|
|
|
|
|
|
|
|
|
constructor (response: { status: number; statusText: string; url: string; }) { |
|
|
|
|
const { status, statusText, url } = response |
|
|
|
|
super(`Fetching ${url} failed with status ${status}`) |
|
|
|
|
|
|
|
|
@ -27,7 +32,7 @@ function paramsToString (params: GenericData) {
@@ -27,7 +32,7 @@ function paramsToString (params: GenericData) {
|
|
|
|
|
* |
|
|
|
|
* returns fetched data in JSON format or HTTPError |
|
|
|
|
*/ |
|
|
|
|
async function fetchFromApi (url): IPlanet|IPerson[]|HTTPError { |
|
|
|
|
async function fetchFromApi (url: string): Promise<IPlanet|IPeopleResponse|HTTPError> { |
|
|
|
|
let response |
|
|
|
|
|
|
|
|
|
try { |
|
|
|
@ -58,31 +63,46 @@ async function fetchFromApi (url): IPlanet|IPerson[]|HTTPError {
@@ -58,31 +63,46 @@ async function fetchFromApi (url): IPlanet|IPerson[]|HTTPError {
|
|
|
|
|
* |
|
|
|
|
* returns fetched data in JSON format or HTTPError |
|
|
|
|
*/ |
|
|
|
|
async function fetchByCategory (category: KnownCategories, id = '', params = {}): IPlanet|IPerson[] { |
|
|
|
|
async function fetchByCategory (category: KnownCategories, id = '', params = {}): Promise<IPlanet|IPeopleResponse|HTTPError> { |
|
|
|
|
const paramString = paramsToString(params) |
|
|
|
|
const url = `${API_PATH}${category}/${id}${paramString}` |
|
|
|
|
|
|
|
|
|
const response = fetchFromApi(url) |
|
|
|
|
const response = await fetchFromApi(url) |
|
|
|
|
return response |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const cache = reactive({ |
|
|
|
|
type CachedData = { |
|
|
|
|
error: boolean; |
|
|
|
|
loading: boolean; |
|
|
|
|
people: IPerson[]; |
|
|
|
|
planets: { [key: number]: IPlanet }; |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const cache = reactive<CachedData>({ |
|
|
|
|
error: false, // generic error state. TODO: is that enough?
|
|
|
|
|
loading: false, // still fetching?
|
|
|
|
|
people: [], // people are filled in batches of 10 (SWAPI default)
|
|
|
|
|
planets: {}, // use object here to avoid a sparse array
|
|
|
|
|
}) |
|
|
|
|
|
|
|
|
|
async function fetchPlanet (url: string): IPlanet { |
|
|
|
|
/* fetchPlanet, fetches a planet by its URL |
|
|
|
|
* url: string, the API URL to the planet |
|
|
|
|
* |
|
|
|
|
* returns IPlanet or HTTPError |
|
|
|
|
*/ |
|
|
|
|
async function fetchPlanet (url: string): Promise<IPlanet|HTTPError> { |
|
|
|
|
const id = getIdFromUrl(url) |
|
|
|
|
// Planet already fetched, yeah!
|
|
|
|
|
if (cache.planets[id] !== undefined) return cache.planets[id] |
|
|
|
|
|
|
|
|
|
// Planet not fetched yet
|
|
|
|
|
const response = fetchFromApi(url) |
|
|
|
|
cache.planets[id] = response |
|
|
|
|
const response = await fetchFromApi(url) |
|
|
|
|
if (response instanceof HTTPError) { |
|
|
|
|
console.error('unhandled HTTPError', response) |
|
|
|
|
} |
|
|
|
|
cache.planets[id] = response as IPlanet // TODO: breaks when network request failed
|
|
|
|
|
|
|
|
|
|
return response |
|
|
|
|
return response as IPlanet |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* fetchPeoplesHomes, fetches all missing planets from given list of people |
|
|
|
@ -90,12 +110,13 @@ async function fetchPlanet (url: string): IPlanet {
@@ -90,12 +110,13 @@ async function fetchPlanet (url: string): IPlanet {
|
|
|
|
|
* |
|
|
|
|
* doesn't return but instead works in the shadows |
|
|
|
|
*/ |
|
|
|
|
async function fetchPeoplesHomes (people: IPerson[]) { |
|
|
|
|
async function fetchPeoplesHomes (people: IPerson[]): Promise<undefined> { |
|
|
|
|
for (let person of people) { |
|
|
|
|
if (typeof person.homeworld === 'string') { |
|
|
|
|
person.homeworld = await fetchPlanet(person.homeworld) |
|
|
|
|
person.homeworld = await fetchPlanet(person.homeworld) as IPlanet |
|
|
|
|
} |
|
|
|
|
} |
|
|
|
|
return |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* fetchAllPeople, recursively fetches all entries from SWAPI/people |
|
|
|
@ -103,7 +124,7 @@ async function fetchPeoplesHomes (people: IPerson[]) {
@@ -103,7 +124,7 @@ async function fetchPeoplesHomes (people: IPerson[]) {
|
|
|
|
|
* |
|
|
|
|
* returns first batch of people |
|
|
|
|
*/ |
|
|
|
|
async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError { |
|
|
|
|
async function fetchAllPeople (page: number = 1): Promise<IPerson[]|HTTPError> { |
|
|
|
|
// avoid being called twice unless it is inside the recursive chain
|
|
|
|
|
if (cache.loading && page === 1) return cache.people |
|
|
|
|
|
|
|
|
@ -120,41 +141,27 @@ async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError {
@@ -120,41 +141,27 @@ async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError {
|
|
|
|
|
cache.error = false // looks like we overcame the error
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
const newPeople: IPerson[] = response.results |
|
|
|
|
const peopleResponse = response as IPeopleResponse |
|
|
|
|
const newPeople: IPerson[] = peopleResponse.results |
|
|
|
|
// store result in cache to make component code simpler
|
|
|
|
|
cache.people = [...cache.people, ...newPeople] |
|
|
|
|
fetchPeoplesHomes(newPeople) |
|
|
|
|
|
|
|
|
|
if (response.next) { // more people to fetch
|
|
|
|
|
if (peopleResponse.next) { // more people to fetch
|
|
|
|
|
fetchAllPeople(page + 1) |
|
|
|
|
} else { |
|
|
|
|
cache.loading = false // all done!
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
return response.results |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* fetch all people from SWAPI |
|
|
|
|
* |
|
|
|
|
* returns fetched data in JSON format or, in case the request failed, a HTTPError object |
|
|
|
|
*/ |
|
|
|
|
async function getPeople (): IPerson[]|HTTPError { |
|
|
|
|
if (cache.people.length > 0) return cache.people // yeah, cached already
|
|
|
|
|
|
|
|
|
|
const people = await fetchAllPeople() |
|
|
|
|
if (people instanceof HTTPError) return people |
|
|
|
|
return cache.people |
|
|
|
|
return peopleResponse.results |
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
/* useAPI |
|
|
|
|
* prefetch: boolean, fetch people immediately, defaults to true |
|
|
|
|
* no parameters |
|
|
|
|
* |
|
|
|
|
* returns { |
|
|
|
|
async getPlanet (id: number): IPlanet|HTTPError |
|
|
|
|
async getPeople (): IPerson[]|HTTPError |
|
|
|
|
* } |
|
|
|
|
* returns CachedData |
|
|
|
|
*/ |
|
|
|
|
export default function useAPI () { |
|
|
|
|
export default function useAPI (): CachedData { |
|
|
|
|
fetchAllPeople() // start fetching immediately
|
|
|
|
|
return cache |
|
|
|
|
} |
|
|
|
|