Skip to content

Instantly share code, notes, and snippets.

@renatoargh
Last active December 26, 2024 01:20
Show Gist options
  • Save renatoargh/631da396c8f272ef0dae2a1a022f3292 to your computer and use it in GitHub Desktop.
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
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
})
}
}
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
}
}
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