Last active
December 26, 2024 01:20
-
-
Save renatoargh/631da396c8f272ef0dae2a1a022f3292 to your computer and use it in GitHub Desktop.
Stop spamming your APIs with repeated fetch calls — try CachedFetch! Fully typed and leveraging the modern Locks API, it prevents multiple concurrent requests and caches responses for a custom TTL. Perfect for repetitive API calls
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { CacheEntry } from "./cacheEntry.ts" | |
import { DateTime, DurationLike } from "luxon" | |
export class CachedFetch<T> { | |
private cache: Map<string, CacheEntry<T>> = new Map() | |
constructor( | |
private readonly ttl: DurationLike, | |
private readonly parse: (response: unknown) => T, | |
) { } | |
private has(cacheKey: string): boolean { | |
if (!this.cache.has(cacheKey)) { | |
return false | |
} | |
const cacheEntry = | |
this.cache.get(cacheKey)! | |
if (cacheEntry.isExpired) { | |
this.cache.delete(cacheKey) | |
return false | |
} | |
return true | |
} | |
private getCacheKey(input: RequestInfo | URL, init?: RequestInit): string { | |
let cacheKey = '' | |
if (typeof input === 'string' || input instanceof URL) { | |
cacheKey = `${init ? init.method : 'get'}-${input.toString()}` | |
} else { | |
cacheKey = `${input.method}-${input.url}` | |
} | |
return cacheKey.toLowerCase() | |
} | |
async fetch(input: RequestInfo | URL, init?: RequestInit): Promise<T> { | |
const cacheKey = this.getCacheKey(input, init) | |
if (this.has(cacheKey)) { | |
return this.cache.get(cacheKey)!.data | |
} | |
return navigator.locks.request(cacheKey, async () => { | |
if (this.has(cacheKey)) { | |
return this.cache.get(cacheKey)!.data | |
} | |
const res = await fetch(input, init) | |
if (!res.ok) { | |
throw new Error(`Error ${res.status}: ${res.statusText}`) | |
} | |
const payload = await res.json() | |
const parsedPayload = this.parse(payload) | |
const cacheEntry = new CacheEntry( | |
parsedPayload, | |
this.ttl, | |
) | |
this.cache.set(cacheKey, cacheEntry) | |
return parsedPayload | |
}) | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import { DateTime, DurationLike } from "luxon" | |
export class CacheEntry<T> { | |
private readonly expiresAt: DateTime | |
constructor( | |
public readonly data: T, | |
public readonly ttl: DurationLike, | |
) { | |
this.expiresAt = DateTime.now().plus(ttl) | |
} | |
get isExpired(): boolean { | |
return DateTime.now() >= this.expiresAt | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
const listSnapshotsCachedFetch = new CachedFetch( | |
{ minute: 1 }, | |
(payload) => inventorySnapshotListSchema.parse(payload) | |
) | |
export const listSnapshotInventories = async (year: number, month: number): Promise<DateTime[]> => { | |
return listSnapshotsCachedFetch.fetch(`${env.apiBaseURL}/inventory-snapshots?year=${year}&month=${month}`, { | |
method: 'GET', | |
headers: new Headers({ | |
Authorization: await getIdToken(), | |
accept: 'application/json', | |
}), | |
}); | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment