diff --git a/src/components/token/Tokens.js b/src/components/token/Tokens.js index ae9b8b6..4e5456f 100644 --- a/src/components/token/Tokens.js +++ b/src/components/token/Tokens.js @@ -18,7 +18,7 @@ const listTokenClassName = "list-token"; function Tokens({ onMapTokenStateCreate }) { const { userId } = useContext(AuthContext); - const { ownedTokens, tokens } = useContext(TokenDataContext); + const { ownedTokens, tokens, updateToken } = useContext(TokenDataContext); const [fullScreen] = useSetting("map.fullScreen"); function handleProxyDragEnd(isOnMap, token) { @@ -39,6 +39,8 @@ function Tokens({ onMapTokenStateCreate }) { locked: false, visible: true, }); + // Update last used for cache invalidation + updateToken(token.id, { lastUsed: Date.now() }); } } diff --git a/src/contexts/MapDataContext.js b/src/contexts/MapDataContext.js index 860b9e5..dd682cb 100644 --- a/src/contexts/MapDataContext.js +++ b/src/contexts/MapDataContext.js @@ -7,6 +7,9 @@ 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: {}, // An index into the draw actions array to which only actions before the @@ -70,12 +73,19 @@ export function MapDataProvider({ children }) { loadMaps(); }, [userId, database]); + /** + * Adds a map to the database, also adds an assosiated state for that map + * @param {Object} map map to add + */ async function addMap(map) { await database.table("maps").add(map); const state = { ...defaultMapState, mapId: map.id }; await database.table("states").add(state); setMaps((prevMaps) => [map, ...prevMaps]); setMapStates((prevStates) => [state, ...prevStates]); + if (map.owner !== userId) { + await updateCache(); + } } async function removeMap(id) { @@ -129,6 +139,11 @@ export function MapDataProvider({ children }) { }); } + /** + * 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 + */ async function putMap(map) { await database.table("maps").put(map); setMaps((prevMaps) => { @@ -141,6 +156,31 @@ export function MapDataProvider({ children }) { } return newMaps; }); + if (map.owner !== userId) { + await updateCache(); + } + } + + /** + * Keep up to cachedMapMax amount of maps that you don't own + * Sorted by when they we're last used + */ + async function updateCache() { + 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(); + setMaps((prevMaps) => { + return prevMaps.filter((map) => !idsToDelete.includes(map.id)); + }); + } } function getMap(mapId) { diff --git a/src/contexts/TokenDataContext.js b/src/contexts/TokenDataContext.js index 632878b..aee9f79 100644 --- a/src/contexts/TokenDataContext.js +++ b/src/contexts/TokenDataContext.js @@ -7,6 +7,8 @@ import { tokens as defaultTokens } from "../tokens"; const TokenDataContext = React.createContext(); +const cachedTokenMax = 100; + export function TokenDataProvider({ children }) { const { database } = useContext(DatabaseContext); const { userId } = useContext(AuthContext); @@ -45,6 +47,9 @@ export function TokenDataProvider({ children }) { async function addToken(token) { await database.table("tokens").add(token); setTokens((prevTokens) => [token, ...prevTokens]); + if (token.owner !== userId) { + await updateCache(); + } } async function removeToken(id) { @@ -80,6 +85,31 @@ export function TokenDataProvider({ children }) { } return newTokens; }); + if (token.owner !== userId) { + await updateCache(); + } + } + + /** + * Keep up to cachedTokenMax amount of tokens that you don't own + * Sorted by when they we're last used + */ + async function updateCache() { + 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(); + setTokens((prevTokens) => { + return prevTokens.filter((token) => !idsToDelete.includes(token.id)); + }); + } } function getToken(tokenId) { diff --git a/src/database.js b/src/database.js index edab6c0..26d82ce 100644 --- a/src/database.js +++ b/src/database.js @@ -184,6 +184,28 @@ function loadVersions(db) { delete token.isVehicle; }); }); + // v1.5.2 - Added automatic cache invalidation to maps + db.version(11) + .stores({}) + .upgrade(async (tx) => { + return tx + .table("maps") + .toCollection() + .modify((map) => { + map.lastUsed = map.lastModified; + }); + }); + // v1.5.2 - Added automatic cache invalidation to tokens + db.version(12) + .stores({}) + .upgrade(async (tx) => { + return tx + .table("tokens") + .toCollection() + .modify((token) => { + token.lastUsed = token.lastModified; + }); + }); } // Get the dexie database used in DatabaseContext diff --git a/src/modals/SelectMapModal.js b/src/modals/SelectMapModal.js index 949717f..f05d63a 100644 --- a/src/modals/SelectMapModal.js +++ b/src/modals/SelectMapModal.js @@ -149,6 +149,7 @@ function SelectMapModal({ id: shortid.generate(), created: Date.now(), lastModified: Date.now(), + lastUsed: Date.now(), owner: userId, ...defaultMapProps, }); @@ -213,7 +214,13 @@ function SelectMapModal({ } if (selectedMapId) { await applyMapChanges(); - onMapChange(selectedMapWithChanges, selectedMapStateWithChanges); + // Update last used for cache invalidation + const lastUsed = Date.now(); + await updateMap(selectedMapId, { lastUsed }); + onMapChange( + { ...selectedMapWithChanges, lastUsed }, + selectedMapStateWithChanges + ); } else { onMapChange(null, null); } diff --git a/src/modals/SelectTokensModal.js b/src/modals/SelectTokensModal.js index 2d14233..d355f3c 100644 --- a/src/modals/SelectTokensModal.js +++ b/src/modals/SelectTokensModal.js @@ -75,6 +75,7 @@ function SelectTokensModal({ isOpen, onRequestClose }) { type: "file", created: Date.now(), lastModified: Date.now(), + lastUsed: Date.now(), owner: userId, defaultSize: 1, category: "character", diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 5405a35..8a80dbd 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -32,7 +32,7 @@ function NetworkedMapAndTokens({ session }) { isLoading, } = useContext(MapLoadingContext); - const { putToken, getToken } = useContext(TokenDataContext); + const { putToken, getToken, updateToken } = useContext(TokenDataContext); const { putMap, updateMap, getMapFromDB } = useContext(MapDataContext); const [currentMap, setCurrentMap] = useState(null); @@ -245,11 +245,15 @@ function NetworkedMapAndTokens({ session }) { if (newMap && newMap.type === "file") { const cachedMap = await getMapFromDB(newMap.id); if (cachedMap && cachedMap.lastModified >= newMap.lastModified) { - setCurrentMap(cachedMap); + // Update last used for cache invalidation + const lastUsed = Date.now(); + await updateMap(cachedMap.id, { lastUsed }); + setCurrentMap({ ...cachedMap, lastUsed }); } else { // Save map data but remove last modified so if there is an error - // during the map request the cache is invalid - await putMap({ ...newMap, lastModified: 0 }); + // during the map request the cache is invalid. Also add last used + // for cache invalidation + await putMap({ ...newMap, lastModified: 0, lastUsed: Date.now() }); reply("mapRequest", newMap.id, "map"); } } else { @@ -326,16 +330,21 @@ function NetworkedMapAndTokens({ session }) { if (newToken && newToken.type === "file") { const cachedToken = getToken(newToken.id); if ( - !cachedToken || - cachedToken.lastModified !== newToken.lastModified + cachedToken && + cachedToken.lastModified >= newToken.lastModified ) { + // Update last used for cache invalidation + const lastUsed = Date.now(); + await updateToken(cachedToken.id, { lastUsed }); + } else { reply("tokenRequest", newToken.id, "token"); } } } if (id === "tokenRequest") { const token = getToken(data); - reply("tokenResponse", token, "token"); + // Add a last used property for cache invalidation + reply("tokenResponse", { ...token, lastUsed: Date.now() }, "token"); } if (id === "tokenResponse") { const newToken = data;