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 {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
|
@ -14,6 +14,7 @@ import { Result } from "../result"
|
||||||
import { RequestError } from "../request"
|
import { RequestError } from "../request"
|
||||||
import { Remote } from "../remote"
|
import { Remote } from "../remote"
|
||||||
import { getCaption } from "../api/caption"
|
import { getCaption } from "../api/caption"
|
||||||
|
import { useCache } from "../cache"
|
||||||
|
|
||||||
// @PERSONAL_NOTE
|
// @PERSONAL_NOTE
|
||||||
// NOTE: There are multiple school of thoughts on how to structure a component.
|
// NOTE: There are multiple school of thoughts on how to structure a component.
|
||||||
|
|
@ -188,39 +189,41 @@ type Msg =
|
||||||
| { tag: "previousButtonClicked" }
|
| { tag: "previousButtonClicked" }
|
||||||
| { tag: "nextButtonClicked" }
|
| { tag: "nextButtonClicked" }
|
||||||
| { tag: "imageClicked"; imageId: ImageId }
|
| { tag: "imageClicked"; imageId: ImageId }
|
||||||
// TODO: Move
|
|
||||||
// | { tag: "captionReceived"; forImage: ImageId; result: Result<string, AppError> }
|
|
||||||
| { tag: "modalCloseButtonClicked" }
|
| { 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())
|
const [state, dispatch] = useReducer(update, State.init())
|
||||||
|
|
||||||
// === Caching API calls ===
|
// === Caching API calls ===
|
||||||
// @PERSONAL_NOTE
|
const getImageIdsCached = useCache((page) => Page.key(page), getImageIds)
|
||||||
// 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 getImageCaptionCached: GetImageCaptionCached = useCache(
|
||||||
const cacheRef = useRef<Map<PageKey, ImageId[]>>(new Map())
|
(imageId) => imageId,
|
||||||
|
getImageCaption,
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// === effects: initialization & reloading ===
|
// === effects: initialization & reloading ===
|
||||||
|
|
||||||
// == Page Refresh ==
|
// == Page Refresh ==
|
||||||
useEffect(
|
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) => {
|
void getImageIdsCached(state.page).then((result) => {
|
||||||
dispatch({ tag: "imagesReceived", forPage: state.page, result })
|
dispatch({ tag: "imagesReceived", forPage: state.page, result })
|
||||||
})
|
})
|
||||||
|
|
@ -231,17 +234,6 @@ function useApp(): [State, Dispatch] {
|
||||||
)
|
)
|
||||||
|
|
||||||
// == AI Captions ==
|
// == 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 {
|
function update(state: State, msg: Msg): State {
|
||||||
switch (msg.tag) {
|
switch (msg.tag) {
|
||||||
case "imagesReceived":
|
case "imagesReceived":
|
||||||
|
|
@ -266,18 +258,6 @@ function useApp(): [State, Dispatch] {
|
||||||
case "imageClicked": {
|
case "imageClicked": {
|
||||||
return { ...state, selectedImage: msg.imageId }
|
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": {
|
case "modalCloseButtonClicked": {
|
||||||
return { ...state, selectedImage: undefined }
|
return { ...state, selectedImage: undefined }
|
||||||
}
|
}
|
||||||
|
|
@ -286,7 +266,7 @@ function useApp(): [State, Dispatch] {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return [state, dispatch]
|
return [state, dispatch, getImageCaptionCached]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Dispatch = (msg: Msg) => void
|
type Dispatch = (msg: Msg) => void
|
||||||
|
|
@ -436,7 +416,6 @@ function Image({
|
||||||
}
|
}
|
||||||
|
|
||||||
function ImageModal({ selectedImage }: { selectedImage: ImageId | undefined }) {
|
function ImageModal({ selectedImage }: { selectedImage: ImageId | undefined }) {
|
||||||
// TODO: Display captions
|
|
||||||
const dispatch = useDispatch()
|
const dispatch = useDispatch()
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog
|
||||||
|
|
@ -459,12 +438,14 @@ function OpenImageModal({ imageId }: { imageId: ImageId }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [state, setState] = useState<State>({ caption: Remote.loading() })
|
const [state, setState] = useState<State>({ caption: Remote.loading() })
|
||||||
|
const getImageCaptionCached = useGetImageCaptionCached()
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(
|
||||||
|
() => {
|
||||||
let isAlive = true
|
let isAlive = true
|
||||||
// TODO: Do I need to set `Remote.loading()` here? I don't like that.
|
// TODO: Do I need to set `Remote.loading()` here? I don't like that.
|
||||||
|
|
||||||
void getImageCaption(imageId).then((caption) => {
|
void getImageCaptionCached(imageId).then((caption) => {
|
||||||
if (isAlive) {
|
if (isAlive) {
|
||||||
setState({ caption })
|
setState({ caption })
|
||||||
}
|
}
|
||||||
|
|
@ -473,7 +454,10 @@ function OpenImageModal({ imageId }: { imageId: ImageId }) {
|
||||||
return () => {
|
return () => {
|
||||||
isAlive = false
|
isAlive = false
|
||||||
}
|
}
|
||||||
}, [imageId])
|
},
|
||||||
|
// This call triggers eslint warning, but it's fine. We don't want to declare `getImageCaptionCached` as a dependency.
|
||||||
|
[imageId],
|
||||||
|
)
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box>
|
||||||
<Image imageId={imageId} dimension={Dimension.big} />
|
<Image imageId={imageId} dimension={Dimension.big} />
|
||||||
|
|
@ -518,9 +502,10 @@ function CaptionView({ caption }: { caption: Remote<string, AppError> }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function Picturarium() {
|
export default function Picturarium() {
|
||||||
const [state, dispatch] = useApp()
|
const [state, dispatch, getImageCaptionCached] = useApp()
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
<ImageCaptionCacheContext.Provider value={getImageCaptionCached}>
|
||||||
<DispatchContext.Provider value={dispatch}>
|
<DispatchContext.Provider value={dispatch}>
|
||||||
<Box
|
<Box
|
||||||
component="main"
|
component="main"
|
||||||
|
|
@ -574,10 +559,10 @@ export default function Picturarium() {
|
||||||
|
|
||||||
<ImageModal selectedImage={state.selectedImage} />
|
<ImageModal selectedImage={state.selectedImage} />
|
||||||
</DispatchContext.Provider>
|
</DispatchContext.Provider>
|
||||||
|
</ImageCaptionCacheContext.Provider>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// === Error view ===
|
// === Error view ===
|
||||||
function ErrorView({
|
function ErrorView({
|
||||||
title,
|
title,
|
||||||
|
|
@ -610,4 +595,3 @@ function ErrorView({
|
||||||
</Box>
|
</Box>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue