diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js index 7146984..554ec97 100644 --- a/src/components/token/Tokens.js +++ b/src/components/token/Tokens.js @@ -19,7 +19,7 @@ const listTokenClassName = "list-token"; function Tokens({ onMapTokenStateCreate }) { const { userId } = useAuth(); - const { ownedTokens, tokens, updateToken } = useTokenData(); + const { ownedTokens, tokens } = useTokenData(); const [fullScreen] = useSetting("map.fullScreen"); function handleProxyDragEnd(isOnMap, token) { @@ -51,13 +51,6 @@ function Tokens({ onMapTokenStateCreate }) { tokenState.key = token.key; } onMapTokenStateCreate(tokenState); - // TODO: Remove when cache is moved to assets - // Update last used for cache invalidation - // Keep last modified the same - updateToken(token.id, { - lastUsed: Date.now(), - lastModified: token.lastModified, - }); } } diff --git a/src/contexts/AssetsContext.js b/src/contexts/AssetsContext.js index 48f456c..fccf30b 100644 --- a/src/contexts/AssetsContext.js +++ b/src/contexts/AssetsContext.js @@ -44,9 +44,16 @@ import { omit } from "../helpers/shared"; */ const AssetsContext = React.createContext(); +// 100 MB max cache size +const maxCacheSize = 1e8; + export function AssetsProvider({ children }) { const { worker, database } = useDatabase(); + useEffect(() => { + worker.cleanAssetCache(maxCacheSize); + }, [worker]); + const getAsset = useCallback( async (assetId) => { return await database.table("assets").get(assetId); @@ -56,7 +63,7 @@ export function AssetsProvider({ children }) { const addAssets = useCallback( async (assets) => { - return database.table("assets").bulkAdd(assets); + await database.table("assets").bulkAdd(assets); }, [database] ); diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index e879474..4ed8379 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -1,10 +1,4 @@ -import React, { - useEffect, - useState, - useContext, - useCallback, - useRef, -} from "react"; +import React, { useEffect, useState, useContext, useCallback } from "react"; import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; @@ -14,9 +8,6 @@ import { maps as defaultMaps } from "../maps"; const MapDataContext = React.createContext(); -// Maximum number of maps to keep in the cache -const cachedMapMax = 15; - const defaultMapState = { tokens: {}, drawShapes: {}, @@ -89,16 +80,7 @@ export function MapDataProvider({ children }) { loadMaps(); }, [userId, database, databaseStatus, worker]); - const mapsRef = useRef(maps); - useEffect(() => { - mapsRef.current = maps; - }, [maps]); - - const getMap = useCallback((mapId) => { - return mapsRef.current.find((map) => map.id === mapId); - }, []); - - const getMapFromDB = useCallback( + const getMap = useCallback( async (mapId) => { let map = await database.table("maps").get(mapId); return map; @@ -106,7 +88,7 @@ export function MapDataProvider({ children }) { [database] ); - const getMapStateFromDB = useCallback( + const getMapState = useCallback( async (mapId) => { let mapState = await database.table("states").get(mapId); return mapState; @@ -114,25 +96,6 @@ export function MapDataProvider({ children }) { [database] ); - /** - * Keep up to cachedMapMax amount of maps that you don't own - * Sorted by when they we're last used - */ - const updateCache = useCallback(async () => { - const cachedMaps = await database - .table("maps") - .where("owner") - .notEqual(userId) - .sortBy("lastUsed"); - if (cachedMaps.length > cachedMapMax) { - const cacheDeleteCount = cachedMaps.length - cachedMapMax; - const idsToDelete = cachedMaps - .slice(0, cacheDeleteCount) - .map((map) => map.id); - database.table("maps").where("id").anyOf(idsToDelete).delete(); - } - }, [database, userId]); - /** * Adds a map to the database, also adds an assosiated state for that map * @param {Object} map map to add @@ -143,11 +106,8 @@ export function MapDataProvider({ children }) { const state = { ...defaultMapState, mapId: map.id }; await database.table("maps").add(map); await database.table("states").add(state); - if (map.owner !== userId) { - await updateCache(); - } }, - [database, updateCache, userId] + [database] ); const removeMaps = useCallback( @@ -182,16 +142,9 @@ export function MapDataProvider({ children }) { const updateMap = useCallback( async (id, update) => { - // fake-indexeddb throws an error when updating maps in production. - // Catch that error and use put when it fails - try { - await database.table("maps").update(id, update); - } catch (error) { - const map = (await getMapFromDB(id)) || {}; - await database.table("maps").put({ ...map, id, ...update }); - } + await database.table("maps").update(id, update); }, - [database, getMapFromDB] + [database] ); const updateMaps = useCallback( @@ -210,21 +163,6 @@ export function MapDataProvider({ children }) { [database] ); - /** - * Adds a map to the database if none exists or replaces a map if it already exists - * Note: this does not add a map state to do that use AddMap - * @param {Object} map the map to put - */ - const putMap = useCallback( - async (map) => { - await database.table("maps").put(map); - if (map.owner !== userId) { - await updateCache(); - } - }, - [database, updateCache, userId] - ); - // Create DB observable to sync creating and deleting useEffect(() => { if (!database || databaseStatus === "loading") { @@ -301,11 +239,9 @@ export function MapDataProvider({ children }) { updateMap, updateMaps, updateMapState, - putMap, getMap, - getMapFromDB, mapsLoading, - getMapStateFromDB, + getMapState, }; return ( {children} diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index 92bfc56..1e5d458 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -1,10 +1,4 @@ -import React, { - useEffect, - useState, - useContext, - useCallback, - useRef, -} from "react"; +import React, { useEffect, useState, useContext, useCallback } from "react"; import { decode } from "@msgpack/msgpack"; import { useAuth } from "./AuthContext"; @@ -14,8 +8,6 @@ import { tokens as defaultTokens } from "../tokens"; const TokenDataContext = React.createContext(); -const cachedTokenMax = 100; - export function TokenDataProvider({ children }) { const { database, databaseStatus, worker } = useDatabase(); const { userId } = useAuth(); @@ -62,16 +54,7 @@ export function TokenDataProvider({ children }) { loadTokens(); }, [userId, database, databaseStatus, worker]); - const tokensRef = useRef(tokens); - useEffect(() => { - tokensRef.current = tokens; - }, [tokens]); - - const getToken = useCallback((tokenId) => { - return tokensRef.current.find((token) => token.id === tokenId); - }, []); - - const getTokenFromDB = useCallback( + const getToken = useCallback( async (tokenId) => { let token = await database.table("tokens").get(tokenId); return token; @@ -79,33 +62,11 @@ export function TokenDataProvider({ children }) { [database] ); - /** - * Keep up to cachedTokenMax amount of tokens that you don't own - * Sorted by when they we're last used - */ - const updateCache = useCallback(async () => { - const cachedTokens = await database - .table("tokens") - .where("owner") - .notEqual(userId) - .sortBy("lastUsed"); - if (cachedTokens.length > cachedTokenMax) { - const cacheDeleteCount = cachedTokens.length - cachedTokenMax; - const idsToDelete = cachedTokens - .slice(0, cacheDeleteCount) - .map((token) => token.id); - database.table("tokens").where("id").anyOf(idsToDelete).delete(); - } - }, [database, userId]); - const addToken = useCallback( async (token) => { await database.table("tokens").add(token); - if (token.owner !== userId) { - await updateCache(); - } }, - [database, updateCache, userId] + [database] ); const removeTokens = useCallback( @@ -142,16 +103,6 @@ export function TokenDataProvider({ children }) { [database] ); - const putToken = useCallback( - async (token) => { - await database.table("tokens").put(token); - if (token.owner !== userId) { - await updateCache(); - } - }, - [database, updateCache, userId] - ); - // Create DB observable to sync creating and deleting useEffect(() => { if (!database || databaseStatus === "loading") { @@ -209,11 +160,9 @@ export function TokenDataProvider({ children }) { removeTokens, updateToken, updateTokens, - putToken, - getToken, tokensById, tokensLoading, - getTokenFromDB, + getToken, }; return ( diff --git a/src/database.js b/src/database.js index 1da8bea..fb0f5df 100644 --- a/src/database.js +++ b/src/database.js @@ -556,6 +556,7 @@ const versions = { delete map[res]; } } + delete map.lastUsed; }); }); }, @@ -573,6 +574,7 @@ const versions = { } else { token.outline = "rect"; } + delete token.lastUsed; }); }); }, diff --git a/src/modals/EditTokenModal.js b/src/modals/EditTokenModal.js index 4fe8ba8..7fff8b4 100644 --- a/src/modals/EditTokenModal.js +++ b/src/modals/EditTokenModal.js @@ -13,14 +13,14 @@ import { isEmpty } from "../helpers/shared"; import useResponsiveLayout from "../hooks/useResponsiveLayout"; function EditTokenModal({ isOpen, onDone, tokenId }) { - const { updateToken, getTokenFromDB } = useTokenData(); + const { updateToken, getToken } = useTokenData(); const [isLoading, setIsLoading] = useState(true); const [token, setToken] = useState(); useEffect(() => { async function loadToken() { setIsLoading(true); - setToken(await getTokenFromDB(tokenId)); + setToken(await getToken(tokenId)); setIsLoading(false); } @@ -29,7 +29,7 @@ function EditTokenModal({ isOpen, onDone, tokenId }) { } else { setToken(); } - }, [isOpen, tokenId, getTokenFromDB]); + }, [isOpen, tokenId, getToken]); function handleClose() { setTokenSettingChanges({}); diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index f2d24fc..2752a36 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -67,11 +67,9 @@ function SelectMapModal({ addMap, removeMaps, resetMap, - updateMap, updateMaps, mapsLoading, - getMapFromDB, - getMapStateFromDB, + getMapState, } = useMapData(); const { addAssets } = useAssets(); @@ -260,7 +258,11 @@ function SelectMapModal({ } // Create thumbnail const thumbnailImage = await createThumbnail(image, file.type); - const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId }; + const thumbnail = { + ...thumbnailImage, + id: uuid(), + owner: userId, + }; assets.push(thumbnail); const fileAsset = { @@ -297,7 +299,6 @@ function SelectMapModal({ id: uuid(), created: Date.now(), lastModified: Date.now(), - lastUsed: Date.now(), owner: userId, ...defaultMapProps, }; @@ -392,19 +393,11 @@ function SelectMapModal({ return; } if (selectedMapIds.length === 1) { - // Update last used for cache invalidation - const lastUsed = Date.now(); + setIsLoading(true); const map = selectedMaps[0]; - const mapState = await getMapStateFromDB(map.id); - if (map.type === "file") { - setIsLoading(true); - await updateMap(map.id, { lastUsed }); - const updatedMap = await getMapFromDB(map.id); - onMapChange(updatedMap, mapState); - setIsLoading(false); - } else { - onMapChange(map, mapState); - } + const mapState = await getMapState(map.id); + onMapChange(map, mapState); + setIsLoading(false); } else { onMapChange(null, null); } diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index 6ed465d..d0b95e7 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -196,7 +196,6 @@ function SelectTokensModal({ isOpen, onRequestClose }) { type: "file", created: Date.now(), lastModified: Date.now(), - lastUsed: Date.now(), owner: userId, defaultSize: 1, defaultCategory: "character", diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index 619a166..6c611f8 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -141,6 +141,43 @@ let service = { }); importDB.close(); }, + + /** + * Ensure the asset cache doesn't go over `maxCacheSize` by removing cached assets + * Removes largest assets first + * @param {number} maxCacheSize Max size of cache in bytes + */ + async cleanAssetCache(maxCacheSize) { + try { + let db = getDatabase({}); + const userId = (await db.table("user").get("userId")).value; + const cachedAssets = await db + .table("assets") + .where("owner") + .notEqual(userId) + .toArray(); + const totalSize = cachedAssets.reduce( + (acc, cur) => acc + cur.file.byteLength, + 0 + ); + if (totalSize > maxCacheSize) { + // Remove largest assets first + const largestAssets = cachedAssets.sort( + (a, b) => b.file.byteLength - a.file.byteLength + ); + let assetsToDelete = []; + let deletedBytes = 0; + for (let asset of largestAssets) { + assetsToDelete.push(asset.id); + deletedBytes += asset.file.byteLength; + if (totalSize - deletedBytes < maxCacheSize) { + break; + } + } + await db.table("assets").bulkDelete(assetsToDelete); + } + } catch {} + }, }; Comlink.expose(service);