
6 changed files with 160 additions and 98 deletions
@ -0,0 +1,140 @@
@@ -0,0 +1,140 @@
|
||||
import { reactive } from 'vue' |
||||
|
||||
type KnownCategories = 'people'|'planets'; |
||||
const API_PATH = 'https://swapi.dev/api/' |
||||
|
||||
export class HTTPError extends Error { |
||||
constructor (response: Response) { |
||||
const { status, statusText, url } = response |
||||
super(`Fetching ${url} failed with status ${status}`) |
||||
|
||||
this.name = 'HTTPError' |
||||
this.status = status |
||||
this.statusText = statusText |
||||
this.url = url |
||||
} |
||||
} |
||||
|
||||
function paramsToString (params: GenericData) { |
||||
const keys = Object.keys(params) |
||||
const pairs = keys.map(key => `${key}=${params[key]}`) |
||||
return `?${pairs.join('&')}` |
||||
} |
||||
|
||||
/* fetch item(s) from SWAPI |
||||
* category: string "people" or "planets" |
||||
* id: optional string if a single item is to be requested |
||||
* |
||||
* returns fetched data in JSON format |
||||
* throws HTTPError when request fails |
||||
*/ |
||||
async function fetchFromApi (category: KnownCategories, id = '', params = {}): IPlanet|IPerson[] { |
||||
const paramString = paramsToString(params) |
||||
const url = `${API_PATH}${category}/${id}${paramString}` |
||||
|
||||
let response |
||||
|
||||
try { |
||||
response = await fetch(url) |
||||
} catch (err) { |
||||
// fetch throws very rarely, only in cases where the browser probably
|
||||
// already intervenes but in case we catch one of the rare exceptions
|
||||
// we generate a HTTPError, because implementing yet another Error type
|
||||
// for such a rare occasion is not worth the effort (in this application)
|
||||
console.error('Network Error', err) |
||||
throw new HTTPError({ |
||||
status: 500, |
||||
statusText: 'Network Error, even if it looks like a server issue, it is not.', |
||||
url, |
||||
}) |
||||
} |
||||
|
||||
// Much more common: HTTP error codes
|
||||
if (!response.ok) throw new HTTPError(response) |
||||
|
||||
const jsonData = await response.json() |
||||
return jsonData |
||||
} |
||||
|
||||
const cache = reactive({ |
||||
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
|
||||
}) |
||||
|
||||
/* fetchAllPeople, recursively fetches all entries from SWAPI/people |
||||
* page: number, API page to fetch |
||||
* |
||||
* returns first batch of people |
||||
*/ |
||||
async function fetchAllPeople (page: number = 1): IPerson[]|HTTPError { |
||||
console.log('fetching people, page', page) |
||||
// avoid being called twice unless it is inside the recursive chain
|
||||
if (cache.loading && page === 1) return cache.people |
||||
|
||||
cache.loading = true |
||||
const response = await fetchFromApi('people', '', { page }) |
||||
if (response instanceof HTTPError) { |
||||
if (!cache.error) { // no error yet, so we try again
|
||||
if (page === 1) cache.loading = false // we need to restart
|
||||
fetchAllPeople(page) |
||||
} |
||||
cache.error = true // set error so that we don't end up in a retry loop
|
||||
return response |
||||
} |
||||
|
||||
const newPeople: IPerson[] = response.results |
||||
// store result in cache to make component code simpler
|
||||
cache.people = [...cache.people, ...newPeople] |
||||
|
||||
if (response.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 |
||||
} |
||||
|
||||
/* fetch a planet from SWAPI |
||||
* id: number, id of planet to fetch |
||||
* |
||||
* returns fetched data in JSON format or, in case the request failed, a HTTPError object |
||||
*/ |
||||
async function getPlanet (id: number): IPlanet|HTTPError { // we always fetch *single* planets
|
||||
if (planets[id]) return planets[id] // yeah, cached already
|
||||
|
||||
const planet = await fetchFromApi('planets', `${id}`) |
||||
if (planet instanceof HTTPError) return planet |
||||
// see getPeople why results are cached
|
||||
// important: internally, IDs are numbers but the fetchFromApi function
|
||||
// wants a string because it uses an empty string to fetch all entries
|
||||
cache.planets[id] = planet |
||||
return planet |
||||
} |
||||
|
||||
/* useAPI |
||||
* prefetch: boolean, fetch people immediately, defaults to true |
||||
* |
||||
* returns { |
||||
async getPlanet (id: number): IPlanet|HTTPError |
||||
async getPeople (): IPerson[]|HTTPError |
||||
* } |
||||
*/ |
||||
export default function useAPI () { |
||||
fetchAllPeople() // start fetching immediately
|
||||
return cache |
||||
} |
Loading…
Reference in new issue