Added automatic cache invalidation for maps and tokens

This commit is contained in:
Mitchell McCaffrey 2020-09-11 16:56:40 +10:00
parent 529fd2caae
commit e92c561a3a
7 changed files with 120 additions and 9 deletions

View File

@ -18,7 +18,7 @@ const listTokenClassName = "list-token";
function Tokens({ onMapTokenStateCreate }) { function Tokens({ onMapTokenStateCreate }) {
const { userId } = useContext(AuthContext); const { userId } = useContext(AuthContext);
const { ownedTokens, tokens } = useContext(TokenDataContext); const { ownedTokens, tokens, updateToken } = useContext(TokenDataContext);
const [fullScreen] = useSetting("map.fullScreen"); const [fullScreen] = useSetting("map.fullScreen");
function handleProxyDragEnd(isOnMap, token) { function handleProxyDragEnd(isOnMap, token) {
@ -39,6 +39,8 @@ function Tokens({ onMapTokenStateCreate }) {
locked: false, locked: false,
visible: true, visible: true,
}); });
// Update last used for cache invalidation
updateToken(token.id, { lastUsed: Date.now() });
} }
} }

View File

@ -7,6 +7,9 @@ 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: {},
// An index into the draw actions array to which only actions before the // An index into the draw actions array to which only actions before the
@ -70,12 +73,19 @@ export function MapDataProvider({ children }) {
loadMaps(); loadMaps();
}, [userId, database]); }, [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) { async function addMap(map) {
await database.table("maps").add(map); await database.table("maps").add(map);
const state = { ...defaultMapState, mapId: map.id }; const state = { ...defaultMapState, mapId: map.id };
await database.table("states").add(state); await database.table("states").add(state);
setMaps((prevMaps) => [map, ...prevMaps]); setMaps((prevMaps) => [map, ...prevMaps]);
setMapStates((prevStates) => [state, ...prevStates]); setMapStates((prevStates) => [state, ...prevStates]);
if (map.owner !== userId) {
await updateCache();
}
} }
async function removeMap(id) { 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) { async function putMap(map) {
await database.table("maps").put(map); await database.table("maps").put(map);
setMaps((prevMaps) => { setMaps((prevMaps) => {
@ -141,6 +156,31 @@ export function MapDataProvider({ children }) {
} }
return newMaps; 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) { function getMap(mapId) {

View File

@ -7,6 +7,8 @@ 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 } = useContext(DatabaseContext); const { database } = useContext(DatabaseContext);
const { userId } = useContext(AuthContext); const { userId } = useContext(AuthContext);
@ -45,6 +47,9 @@ export function TokenDataProvider({ children }) {
async function addToken(token) { async function addToken(token) {
await database.table("tokens").add(token); await database.table("tokens").add(token);
setTokens((prevTokens) => [token, ...prevTokens]); setTokens((prevTokens) => [token, ...prevTokens]);
if (token.owner !== userId) {
await updateCache();
}
} }
async function removeToken(id) { async function removeToken(id) {
@ -80,6 +85,31 @@ export function TokenDataProvider({ children }) {
} }
return newTokens; 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) { function getToken(tokenId) {

View File

@ -184,6 +184,28 @@ function loadVersions(db) {
delete token.isVehicle; 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 // Get the dexie database used in DatabaseContext

View File

@ -149,6 +149,7 @@ function SelectMapModal({
id: shortid.generate(), id: shortid.generate(),
created: Date.now(), created: Date.now(),
lastModified: Date.now(), lastModified: Date.now(),
lastUsed: Date.now(),
owner: userId, owner: userId,
...defaultMapProps, ...defaultMapProps,
}); });
@ -213,7 +214,13 @@ function SelectMapModal({
} }
if (selectedMapId) { if (selectedMapId) {
await applyMapChanges(); await applyMapChanges();
onMapChange(selectedMapWithChanges, selectedMapStateWithChanges); // Update last used for cache invalidation
const lastUsed = Date.now();
await updateMap(selectedMapId, { lastUsed });
onMapChange(
{ ...selectedMapWithChanges, lastUsed },
selectedMapStateWithChanges
);
} else { } else {
onMapChange(null, null); onMapChange(null, null);
} }

View File

@ -75,6 +75,7 @@ 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,
category: "character", category: "character",

View File

@ -32,7 +32,7 @@ function NetworkedMapAndTokens({ session }) {
isLoading, isLoading,
} = useContext(MapLoadingContext); } = useContext(MapLoadingContext);
const { putToken, getToken } = useContext(TokenDataContext); const { putToken, getToken, updateToken } = useContext(TokenDataContext);
const { putMap, updateMap, getMapFromDB } = useContext(MapDataContext); const { putMap, updateMap, getMapFromDB } = useContext(MapDataContext);
const [currentMap, setCurrentMap] = useState(null); const [currentMap, setCurrentMap] = useState(null);
@ -245,11 +245,15 @@ function NetworkedMapAndTokens({ session }) {
if (newMap && newMap.type === "file") { if (newMap && newMap.type === "file") {
const cachedMap = await getMapFromDB(newMap.id); const cachedMap = await getMapFromDB(newMap.id);
if (cachedMap && cachedMap.lastModified >= newMap.lastModified) { 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 { } else {
// Save map data but remove last modified so if there is an error // Save map data but remove last modified so if there is an error
// during the map request the cache is invalid // during the map request the cache is invalid. Also add last used
await putMap({ ...newMap, lastModified: 0 }); // for cache invalidation
await putMap({ ...newMap, lastModified: 0, lastUsed: Date.now() });
reply("mapRequest", newMap.id, "map"); reply("mapRequest", newMap.id, "map");
} }
} else { } else {
@ -326,16 +330,21 @@ function NetworkedMapAndTokens({ session }) {
if (newToken && newToken.type === "file") { if (newToken && newToken.type === "file") {
const cachedToken = getToken(newToken.id); const cachedToken = getToken(newToken.id);
if ( if (
!cachedToken || cachedToken &&
cachedToken.lastModified !== newToken.lastModified 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"); reply("tokenRequest", newToken.id, "token");
} }
} }
} }
if (id === "tokenRequest") { if (id === "tokenRequest") {
const token = getToken(data); 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") { if (id === "tokenResponse") {
const newToken = data; const newToken = data;