Remove cache functions from map and tokens and add to assets

This commit is contained in:
Mitchell McCaffrey 2021-04-30 11:56:37 +10:00
parent fa94275ac8
commit a91d559ba6
9 changed files with 72 additions and 156 deletions

View File

@ -19,7 +19,7 @@ const listTokenClassName = "list-token";
function Tokens({ onMapTokenStateCreate }) { function Tokens({ onMapTokenStateCreate }) {
const { userId } = useAuth(); const { userId } = useAuth();
const { ownedTokens, tokens, updateToken } = useTokenData(); const { ownedTokens, tokens } = useTokenData();
const [fullScreen] = useSetting("map.fullScreen"); const [fullScreen] = useSetting("map.fullScreen");
function handleProxyDragEnd(isOnMap, token) { function handleProxyDragEnd(isOnMap, token) {
@ -51,13 +51,6 @@ function Tokens({ onMapTokenStateCreate }) {
tokenState.key = token.key; tokenState.key = token.key;
} }
onMapTokenStateCreate(tokenState); 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,
});
} }
} }

View File

@ -44,9 +44,16 @@ import { omit } from "../helpers/shared";
*/ */
const AssetsContext = React.createContext(); const AssetsContext = React.createContext();
// 100 MB max cache size
const maxCacheSize = 1e8;
export function AssetsProvider({ children }) { export function AssetsProvider({ children }) {
const { worker, database } = useDatabase(); const { worker, database } = useDatabase();
useEffect(() => {
worker.cleanAssetCache(maxCacheSize);
}, [worker]);
const getAsset = useCallback( const getAsset = useCallback(
async (assetId) => { async (assetId) => {
return await database.table("assets").get(assetId); return await database.table("assets").get(assetId);
@ -56,7 +63,7 @@ export function AssetsProvider({ children }) {
const addAssets = useCallback( const addAssets = useCallback(
async (assets) => { async (assets) => {
return database.table("assets").bulkAdd(assets); await database.table("assets").bulkAdd(assets);
}, },
[database] [database]
); );

View File

@ -1,10 +1,4 @@
import React, { import React, { useEffect, useState, useContext, useCallback } from "react";
useEffect,
useState,
useContext,
useCallback,
useRef,
} from "react";
import { decode } from "@msgpack/msgpack"; import { decode } from "@msgpack/msgpack";
import { useAuth } from "./AuthContext"; import { useAuth } from "./AuthContext";
@ -14,9 +8,6 @@ import { maps as defaultMaps } from "../maps";
const MapDataContext = React.createContext(); const MapDataContext = React.createContext();
// Maximum number of maps to keep in the cache
const cachedMapMax = 15;
const defaultMapState = { const defaultMapState = {
tokens: {}, tokens: {},
drawShapes: {}, drawShapes: {},
@ -89,16 +80,7 @@ export function MapDataProvider({ children }) {
loadMaps(); loadMaps();
}, [userId, database, databaseStatus, worker]); }, [userId, database, databaseStatus, worker]);
const mapsRef = useRef(maps); const getMap = useCallback(
useEffect(() => {
mapsRef.current = maps;
}, [maps]);
const getMap = useCallback((mapId) => {
return mapsRef.current.find((map) => map.id === mapId);
}, []);
const getMapFromDB = useCallback(
async (mapId) => { async (mapId) => {
let map = await database.table("maps").get(mapId); let map = await database.table("maps").get(mapId);
return map; return map;
@ -106,7 +88,7 @@ export function MapDataProvider({ children }) {
[database] [database]
); );
const getMapStateFromDB = useCallback( const getMapState = useCallback(
async (mapId) => { async (mapId) => {
let mapState = await database.table("states").get(mapId); let mapState = await database.table("states").get(mapId);
return mapState; return mapState;
@ -114,25 +96,6 @@ export function MapDataProvider({ children }) {
[database] [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 * Adds a map to the database, also adds an assosiated state for that map
* @param {Object} map map to add * @param {Object} map map to add
@ -143,11 +106,8 @@ export function MapDataProvider({ children }) {
const state = { ...defaultMapState, mapId: map.id }; const state = { ...defaultMapState, mapId: map.id };
await database.table("maps").add(map); await database.table("maps").add(map);
await database.table("states").add(state); await database.table("states").add(state);
if (map.owner !== userId) {
await updateCache();
}
}, },
[database, updateCache, userId] [database]
); );
const removeMaps = useCallback( const removeMaps = useCallback(
@ -182,16 +142,9 @@ export function MapDataProvider({ children }) {
const updateMap = useCallback( const updateMap = useCallback(
async (id, update) => { async (id, update) => {
// fake-indexeddb throws an error when updating maps in production. await database.table("maps").update(id, update);
// 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 });
}
}, },
[database, getMapFromDB] [database]
); );
const updateMaps = useCallback( const updateMaps = useCallback(
@ -210,21 +163,6 @@ export function MapDataProvider({ children }) {
[database] [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 // Create DB observable to sync creating and deleting
useEffect(() => { useEffect(() => {
if (!database || databaseStatus === "loading") { if (!database || databaseStatus === "loading") {
@ -301,11 +239,9 @@ export function MapDataProvider({ children }) {
updateMap, updateMap,
updateMaps, updateMaps,
updateMapState, updateMapState,
putMap,
getMap, getMap,
getMapFromDB,
mapsLoading, mapsLoading,
getMapStateFromDB, getMapState,
}; };
return ( return (
<MapDataContext.Provider value={value}>{children}</MapDataContext.Provider> <MapDataContext.Provider value={value}>{children}</MapDataContext.Provider>

View File

@ -1,10 +1,4 @@
import React, { import React, { useEffect, useState, useContext, useCallback } from "react";
useEffect,
useState,
useContext,
useCallback,
useRef,
} from "react";
import { decode } from "@msgpack/msgpack"; import { decode } from "@msgpack/msgpack";
import { useAuth } from "./AuthContext"; import { useAuth } from "./AuthContext";
@ -14,8 +8,6 @@ import { tokens as defaultTokens } from "../tokens";
const TokenDataContext = React.createContext(); const TokenDataContext = React.createContext();
const cachedTokenMax = 100;
export function TokenDataProvider({ children }) { export function TokenDataProvider({ children }) {
const { database, databaseStatus, worker } = useDatabase(); const { database, databaseStatus, worker } = useDatabase();
const { userId } = useAuth(); const { userId } = useAuth();
@ -62,16 +54,7 @@ export function TokenDataProvider({ children }) {
loadTokens(); loadTokens();
}, [userId, database, databaseStatus, worker]); }, [userId, database, databaseStatus, worker]);
const tokensRef = useRef(tokens); const getToken = useCallback(
useEffect(() => {
tokensRef.current = tokens;
}, [tokens]);
const getToken = useCallback((tokenId) => {
return tokensRef.current.find((token) => token.id === tokenId);
}, []);
const getTokenFromDB = useCallback(
async (tokenId) => { async (tokenId) => {
let token = await database.table("tokens").get(tokenId); let token = await database.table("tokens").get(tokenId);
return token; return token;
@ -79,33 +62,11 @@ export function TokenDataProvider({ children }) {
[database] [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( const addToken = useCallback(
async (token) => { async (token) => {
await database.table("tokens").add(token); await database.table("tokens").add(token);
if (token.owner !== userId) {
await updateCache();
}
}, },
[database, updateCache, userId] [database]
); );
const removeTokens = useCallback( const removeTokens = useCallback(
@ -142,16 +103,6 @@ export function TokenDataProvider({ children }) {
[database] [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 // Create DB observable to sync creating and deleting
useEffect(() => { useEffect(() => {
if (!database || databaseStatus === "loading") { if (!database || databaseStatus === "loading") {
@ -209,11 +160,9 @@ export function TokenDataProvider({ children }) {
removeTokens, removeTokens,
updateToken, updateToken,
updateTokens, updateTokens,
putToken,
getToken,
tokensById, tokensById,
tokensLoading, tokensLoading,
getTokenFromDB, getToken,
}; };
return ( return (

View File

@ -556,6 +556,7 @@ const versions = {
delete map[res]; delete map[res];
} }
} }
delete map.lastUsed;
}); });
}); });
}, },
@ -573,6 +574,7 @@ const versions = {
} else { } else {
token.outline = "rect"; token.outline = "rect";
} }
delete token.lastUsed;
}); });
}); });
}, },

View File

@ -13,14 +13,14 @@ import { isEmpty } from "../helpers/shared";
import useResponsiveLayout from "../hooks/useResponsiveLayout"; import useResponsiveLayout from "../hooks/useResponsiveLayout";
function EditTokenModal({ isOpen, onDone, tokenId }) { function EditTokenModal({ isOpen, onDone, tokenId }) {
const { updateToken, getTokenFromDB } = useTokenData(); const { updateToken, getToken } = useTokenData();
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [token, setToken] = useState(); const [token, setToken] = useState();
useEffect(() => { useEffect(() => {
async function loadToken() { async function loadToken() {
setIsLoading(true); setIsLoading(true);
setToken(await getTokenFromDB(tokenId)); setToken(await getToken(tokenId));
setIsLoading(false); setIsLoading(false);
} }
@ -29,7 +29,7 @@ function EditTokenModal({ isOpen, onDone, tokenId }) {
} else { } else {
setToken(); setToken();
} }
}, [isOpen, tokenId, getTokenFromDB]); }, [isOpen, tokenId, getToken]);
function handleClose() { function handleClose() {
setTokenSettingChanges({}); setTokenSettingChanges({});

View File

@ -67,11 +67,9 @@ function SelectMapModal({
addMap, addMap,
removeMaps, removeMaps,
resetMap, resetMap,
updateMap,
updateMaps, updateMaps,
mapsLoading, mapsLoading,
getMapFromDB, getMapState,
getMapStateFromDB,
} = useMapData(); } = useMapData();
const { addAssets } = useAssets(); const { addAssets } = useAssets();
@ -260,7 +258,11 @@ function SelectMapModal({
} }
// Create thumbnail // Create thumbnail
const thumbnailImage = await createThumbnail(image, file.type); const thumbnailImage = await createThumbnail(image, file.type);
const thumbnail = { ...thumbnailImage, id: uuid(), owner: userId }; const thumbnail = {
...thumbnailImage,
id: uuid(),
owner: userId,
};
assets.push(thumbnail); assets.push(thumbnail);
const fileAsset = { const fileAsset = {
@ -297,7 +299,6 @@ function SelectMapModal({
id: uuid(), id: uuid(),
created: Date.now(), created: Date.now(),
lastModified: Date.now(), lastModified: Date.now(),
lastUsed: Date.now(),
owner: userId, owner: userId,
...defaultMapProps, ...defaultMapProps,
}; };
@ -392,19 +393,11 @@ function SelectMapModal({
return; return;
} }
if (selectedMapIds.length === 1) { if (selectedMapIds.length === 1) {
// Update last used for cache invalidation setIsLoading(true);
const lastUsed = Date.now();
const map = selectedMaps[0]; const map = selectedMaps[0];
const mapState = await getMapStateFromDB(map.id); const mapState = await getMapState(map.id);
if (map.type === "file") { onMapChange(map, mapState);
setIsLoading(true); setIsLoading(false);
await updateMap(map.id, { lastUsed });
const updatedMap = await getMapFromDB(map.id);
onMapChange(updatedMap, mapState);
setIsLoading(false);
} else {
onMapChange(map, mapState);
}
} else { } else {
onMapChange(null, null); onMapChange(null, null);
} }

View File

@ -196,7 +196,6 @@ function SelectTokensModal({ isOpen, onRequestClose }) {
type: "file", type: "file",
created: Date.now(), created: Date.now(),
lastModified: Date.now(), lastModified: Date.now(),
lastUsed: Date.now(),
owner: userId, owner: userId,
defaultSize: 1, defaultSize: 1,
defaultCategory: "character", defaultCategory: "character",

View File

@ -141,6 +141,43 @@ let service = {
}); });
importDB.close(); 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); Comlink.expose(service);