Skip to content

Commit

Permalink
feat: single flight for repo and avatar request
Browse files Browse the repository at this point in the history
  • Loading branch information
xhofe committed Sep 22, 2024
1 parent 53cd67e commit c158535
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 5 deletions.
4 changes: 2 additions & 2 deletions app/(empty)/iframe/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@ export default function RootLayout({
children: React.ReactNode
}) {
return (
<html lang="en" className="h-full">
<body className="h-full">{children}</body>
<html lang="en" className="h-full bg-transparent">
<body className="h-full bg-transparent">{children}</body>
</html>
)
}
9 changes: 6 additions & 3 deletions app/api/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { LRUCache } from "lru-cache"
import { GhUser, GhUserUse } from "./types"
import fs from "node:fs/promises"
import path from "node:path"
import { throttle } from "@/utils"
import { throttle, Singleflight } from "@/utils"

const repoCache = new LRUCache<string, GhUserUse[]>({
max: 500,
Expand Down Expand Up @@ -90,6 +90,9 @@ export function usedBy(per_page: number, page: number) {
}
}

const repoSF = new Singleflight()
const avatarSF = new Singleflight()

async function fetchRepoOnePage(repo: string, page: number) {
const cacheKey = `${repo}-${page}`
if (repoCache.has(cacheKey)) {
Expand Down Expand Up @@ -158,7 +161,7 @@ export async function fetchRepos(repos: string[], maxPages?: number) {
const users = (
await Promise.all(
repos.map(async (repo) => {
const users = await fetchRepo(repo, maxPages)
const users = await repoSF.do(repo, () => fetchRepo(repo, maxPages))
return users
})
)
Expand All @@ -174,7 +177,7 @@ export async function fetchAvatar(url: string) {
if (avatarCache.has(url)) {
return avatarCache.get(url)!
}
const response = await fetch(url)
const response = await avatarSF.do(url, () => fetch(url))
const res = Buffer.from(await response.arrayBuffer())
avatarCache.set(url, res)
return res
Expand Down
1 change: 1 addition & 0 deletions utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./fetch"
export * from "./svg"
export * from "./func"
export * from "./single-flight"
64 changes: 64 additions & 0 deletions utils/single-flight.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
export type Fn<T = any> = () => T | Promise<T>
export type ResolveFn = (res: any) => void

export type KeyType = string | symbol

export interface Call {
resolveFns: ResolveFn[]
rejectFns: ResolveFn[]
}

let resolveInstance: Promise<void>

export class Singleflight {
private singleFlightQueue = new Map<KeyType, Call>()

async do<T = any>(key: KeyType, fn: Fn<T>): Promise<T> {
const [res] = await this.doWithFresh(key, fn)
return res
}

async doWithFresh<T = any>(
key: KeyType,
fn: Fn<T>
): Promise<[data: T, fresh: boolean]> {
const promise: Promise<[data: T, fresh: boolean]> = new Promise(
(resolve, reject) => {
const call: Call = this.singleFlightQueue.get(key) || {
resolveFns: [],
rejectFns: [],
}

call.resolveFns.push(resolve)
call.rejectFns.push(reject)
this.singleFlightQueue.set(key, call)
if (call.resolveFns.length === 1) {
if (!resolveInstance) {
resolveInstance = Promise.resolve()
}
// ensure always work even fn is sync function
resolveInstance
.then(() => fn())
.then((res) => {
const waitCall = this.singleFlightQueue.get(key)!
waitCall.resolveFns.forEach((resolve, i) => {
if (i === 0) {
resolve([res, true])
} else {
resolve([res, false])
}
})
this.singleFlightQueue.delete(key)
})
.catch((err) => {
const waitCall = this.singleFlightQueue.get(key)!
waitCall.rejectFns.forEach((reject) => reject(err))
this.singleFlightQueue.delete(key)
})
}
}
)

return promise
}
}

0 comments on commit c158535

Please sign in to comment.