Cleanup of old code. Abstract out cache of remote requests. Cache for AI captions.
This commit is contained in:
parent
f3784e75f4
commit
8ea8fa6446
2 changed files with 125 additions and 109 deletions
32
src/cache.ts
Normal file
32
src/cache.ts
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
import { useRef } from "react"
|
||||
import type { RequestError } from "./request"
|
||||
import { Result } from "./result"
|
||||
|
||||
// KeyHash is supposed to be something where `===` compares for structural equality (so in JS: number, string).
|
||||
export type KeyHash = string | number
|
||||
export function useCache<Key, Value>(
|
||||
hash: (key: Key) => KeyHash,
|
||||
f: (k: Key) => Promise<Result<Value, RequestError>>,
|
||||
) {
|
||||
// @PERSONAL_NOTE
|
||||
// Could also cache `Promise<ImageId[]>` in case two requests are made really fast one after another (not really the case in this app so whatever)
|
||||
const cacheRef = useRef<Map<KeyHash, Value>>(new Map())
|
||||
|
||||
async function getCached(key: Key): Promise<Result<Value, RequestError>> {
|
||||
const keyHash = hash(key)
|
||||
const maybeValue = cacheRef.current.get(keyHash)
|
||||
if (maybeValue === undefined) {
|
||||
// console.log("CACHE-MISS")
|
||||
const result = await f(key)
|
||||
if (result.tag === "ok") {
|
||||
cacheRef.current.set(keyHash, result.value)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
// console.log("CACHE-HIT")
|
||||
return Result.ok(maybeValue)
|
||||
}
|
||||
}
|
||||
|
||||
return getCached
|
||||
}
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { useReducer, useRef, useEffect, createContext, useContext, useState } from "react"
|
||||
import { useReducer, useEffect, createContext, useContext, useState } from "react"
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
|
|
@ -14,6 +14,7 @@ import { Result } from "../result"
|
|||
import { RequestError } from "../request"
|
||||
import { Remote } from "../remote"
|
||||
import { getCaption } from "../api/caption"
|
||||
import { useCache } from "../cache"
|
||||
|
||||
// @PERSONAL_NOTE
|
||||
// NOTE: There are multiple school of thoughts on how to structure a component.
|
||||
|
|
@ -188,39 +189,41 @@ type Msg =
|
|||
| { tag: "previousButtonClicked" }
|
||||
| { tag: "nextButtonClicked" }
|
||||
| { tag: "imageClicked"; imageId: ImageId }
|
||||
// TODO: Move
|
||||
// | { tag: "captionReceived"; forImage: ImageId; result: Result<string, AppError> }
|
||||
| { tag: "modalCloseButtonClicked" }
|
||||
|
||||
function useApp(): [State, Dispatch] {
|
||||
// === Caching Context for AI captions ===
|
||||
type GetImageCaptionCached = (imageId: ImageId) => Promise<Result<string, AppError>>
|
||||
|
||||
const ImageCaptionCacheContext = createContext<GetImageCaptionCached | null>(null)
|
||||
|
||||
function useGetImageCaptionCached(): GetImageCaptionCached {
|
||||
const getImageCaptionCached = useContext(ImageCaptionCacheContext)
|
||||
|
||||
if (getImageCaptionCached === null) {
|
||||
throw new Error(
|
||||
"useGetImageCaptionCached must be used inside ImageCaptionCacheContext.Provider",
|
||||
)
|
||||
}
|
||||
|
||||
return getImageCaptionCached
|
||||
}
|
||||
|
||||
function useApp(): [State, Dispatch, GetImageCaptionCached] {
|
||||
const [state, dispatch] = useReducer(update, State.init())
|
||||
|
||||
// === Caching API calls ===
|
||||
// @PERSONAL_NOTE
|
||||
// Could also cache `Promise<ImageId[]>` in case two requests are made really fast one after another (not really the case in this app so whatever)
|
||||
const cacheRef = useRef<Map<PageKey, ImageId[]>>(new Map())
|
||||
|
||||
async function getImageIdsCached(page: Page): Promise<Result<ImageId[], AppError>> {
|
||||
const pageKey = Page.key(page)
|
||||
const maybeImages = cacheRef.current.get(pageKey)
|
||||
if (maybeImages === undefined) {
|
||||
// console.log("CACHE-MISS")
|
||||
const result = await getImageIds(page)
|
||||
if (result.tag === "ok") {
|
||||
cacheRef.current.set(pageKey, result.value)
|
||||
}
|
||||
return result
|
||||
} else {
|
||||
// console.log("CACHE-HIT")
|
||||
return Result.ok(maybeImages)
|
||||
}
|
||||
}
|
||||
const getImageIdsCached = useCache((page) => Page.key(page), getImageIds)
|
||||
const getImageCaptionCached: GetImageCaptionCached = useCache(
|
||||
(imageId) => imageId,
|
||||
getImageCaption,
|
||||
)
|
||||
|
||||
// === effects: initialization & reloading ===
|
||||
|
||||
// == Page Refresh ==
|
||||
useEffect(
|
||||
() => {
|
||||
// This call triggers eslint warning, but it's fine. We don't want to declare `getImageIdsCached` as a dependency.
|
||||
void getImageIdsCached(state.page).then((result) => {
|
||||
dispatch({ tag: "imagesReceived", forPage: state.page, result })
|
||||
})
|
||||
|
|
@ -231,17 +234,6 @@ function useApp(): [State, Dispatch] {
|
|||
)
|
||||
|
||||
// == AI Captions ==
|
||||
// TODO: MOVE
|
||||
// useEffect(() => {
|
||||
// if (state.selectedImage !== undefined) {
|
||||
// const imageId = state.selectedImage.imageId
|
||||
// void getImageCaption(imageId).then((result) => {
|
||||
// dispatch({ tag: "captionReceived", forImage: imageId, result })
|
||||
// })
|
||||
// // TODO: Need to fetch the image (with some url of smaller dimensions)
|
||||
// }
|
||||
// }, [state.selectedImage?.imageId])
|
||||
|
||||
function update(state: State, msg: Msg): State {
|
||||
switch (msg.tag) {
|
||||
case "imagesReceived":
|
||||
|
|
@ -266,18 +258,6 @@ function useApp(): [State, Dispatch] {
|
|||
case "imageClicked": {
|
||||
return { ...state, selectedImage: msg.imageId }
|
||||
}
|
||||
// TODO: move
|
||||
// case "captionReceived": {
|
||||
// if (state.selectedImage !== undefined && state.selectedImage.imageId === msg.forImage) {
|
||||
// const modalState: OpenImageModalState = {
|
||||
// ...state.selectedImage,
|
||||
// caption: msg.result,
|
||||
// }
|
||||
// return { ...state, selectedImage: modalState }
|
||||
// } else {
|
||||
// return state
|
||||
// }
|
||||
// }
|
||||
case "modalCloseButtonClicked": {
|
||||
return { ...state, selectedImage: undefined }
|
||||
}
|
||||
|
|
@ -286,7 +266,7 @@ function useApp(): [State, Dispatch] {
|
|||
}
|
||||
}
|
||||
|
||||
return [state, dispatch]
|
||||
return [state, dispatch, getImageCaptionCached]
|
||||
}
|
||||
|
||||
type Dispatch = (msg: Msg) => void
|
||||
|
|
@ -436,7 +416,6 @@ function Image({
|
|||
}
|
||||
|
||||
function ImageModal({ selectedImage }: { selectedImage: ImageId | undefined }) {
|
||||
// TODO: Display captions
|
||||
const dispatch = useDispatch()
|
||||
return (
|
||||
<Dialog
|
||||
|
|
@ -459,21 +438,26 @@ function OpenImageModal({ imageId }: { imageId: ImageId }) {
|
|||
}
|
||||
|
||||
const [state, setState] = useState<State>({ caption: Remote.loading() })
|
||||
const getImageCaptionCached = useGetImageCaptionCached()
|
||||
|
||||
useEffect(() => {
|
||||
let isAlive = true
|
||||
// TODO: Do I need to set `Remote.loading()` here? I don't like that.
|
||||
useEffect(
|
||||
() => {
|
||||
let isAlive = true
|
||||
// TODO: Do I need to set `Remote.loading()` here? I don't like that.
|
||||
|
||||
void getImageCaption(imageId).then((caption) => {
|
||||
if (isAlive) {
|
||||
setState({ caption })
|
||||
void getImageCaptionCached(imageId).then((caption) => {
|
||||
if (isAlive) {
|
||||
setState({ caption })
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
isAlive = false
|
||||
}
|
||||
})
|
||||
|
||||
return () => {
|
||||
isAlive = false
|
||||
}
|
||||
}, [imageId])
|
||||
},
|
||||
// This call triggers eslint warning, but it's fine. We don't want to declare `getImageCaptionCached` as a dependency.
|
||||
[imageId],
|
||||
)
|
||||
return (
|
||||
<Box>
|
||||
<Image imageId={imageId} dimension={Dimension.big} />
|
||||
|
|
@ -518,66 +502,67 @@ function CaptionView({ caption }: { caption: Remote<string, AppError> }) {
|
|||
}
|
||||
|
||||
export default function Picturarium() {
|
||||
const [state, dispatch] = useApp()
|
||||
const [state, dispatch, getImageCaptionCached] = useApp()
|
||||
|
||||
return (
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Typography variant="h2">Picturarium</Typography>
|
||||
|
||||
<RemoteImages images={state.imageIds} />
|
||||
|
||||
<ImageCaptionCacheContext.Provider value={getImageCaptionCached}>
|
||||
<DispatchContext.Provider value={dispatch}>
|
||||
<Box
|
||||
component="main"
|
||||
sx={{
|
||||
minHeight: "100vh",
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 2,
|
||||
justifyContent: "center",
|
||||
flexDirection: "column",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch({ tag: "previousButtonClicked" })
|
||||
}}
|
||||
disabled={Page.isFirst(state.page)}
|
||||
>
|
||||
prev
|
||||
</Button>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
minWidth: 60,
|
||||
textAlign: "center",
|
||||
color: "oklch(65% 0.02 260)",
|
||||
}}
|
||||
>
|
||||
{state.page.index}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch({ tag: "nextButtonClicked" })
|
||||
}}
|
||||
>
|
||||
next
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
<Typography variant="h2">Picturarium</Typography>
|
||||
|
||||
<ImageModal selectedImage={state.selectedImage} />
|
||||
</DispatchContext.Provider>
|
||||
<RemoteImages images={state.imageIds} />
|
||||
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
flexDirection: "row",
|
||||
gap: 2,
|
||||
justifyContent: "center",
|
||||
alignItems: "center",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch({ tag: "previousButtonClicked" })
|
||||
}}
|
||||
disabled={Page.isFirst(state.page)}
|
||||
>
|
||||
prev
|
||||
</Button>
|
||||
<Typography
|
||||
variant="body1"
|
||||
sx={{
|
||||
minWidth: 60,
|
||||
textAlign: "center",
|
||||
color: "oklch(65% 0.02 260)",
|
||||
}}
|
||||
>
|
||||
{state.page.index}
|
||||
</Typography>
|
||||
<Button
|
||||
onClick={() => {
|
||||
dispatch({ tag: "nextButtonClicked" })
|
||||
}}
|
||||
>
|
||||
next
|
||||
</Button>
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
<ImageModal selectedImage={state.selectedImage} />
|
||||
</DispatchContext.Provider>
|
||||
</ImageCaptionCacheContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
// === Error view ===
|
||||
function ErrorView({
|
||||
title,
|
||||
|
|
@ -610,4 +595,3 @@ function ErrorView({
|
|||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue