From 9f11161b235898434b33c719431c97b89e4cb64c Mon Sep 17 00:00:00 2001 From: Mitchell McCaffrey Date: Thu, 22 Apr 2021 16:53:35 +1000 Subject: [PATCH] Moved assets into new table in the database --- package.json | 1 + src/App.js | 72 +++---- src/components/map/MapEditor.js | 8 +- src/components/map/MapGrid.js | 9 +- src/components/map/MapInteraction.js | 9 +- src/components/map/MapTile.js | 6 +- src/components/map/MapToken.js | 4 +- src/components/token/ListToken.js | 6 +- src/components/token/TokenPreview.js | 10 +- src/components/token/TokenTile.js | 6 +- src/contexts/AssetsContext.js | 273 +++++++++++++++++++++++++++ src/contexts/ImageSourceContext.js | 157 --------------- src/database.js | 133 ++++++++++++- src/helpers/KonvaBridge.js | 113 +++++------ src/hooks/useMapImage.js | 16 +- src/workers/DatabaseWorker.js | 13 +- yarn.lock | 5 + 17 files changed, 544 insertions(+), 297 deletions(-) create mode 100644 src/contexts/AssetsContext.js delete mode 100644 src/contexts/ImageSourceContext.js diff --git a/package.json b/package.json index 5fa1a20..628ff9a 100644 --- a/package.json +++ b/package.json @@ -57,6 +57,7 @@ "source-map-explorer": "^2.5.2", "theme-ui": "^0.3.1", "use-image": "^1.0.7", + "uuid": "^8.3.2", "webrtc-adapter": "^7.7.1" }, "resolutions": { diff --git a/src/App.js b/src/App.js index a77ae2b..7962f7b 100644 --- a/src/App.js +++ b/src/App.js @@ -18,7 +18,7 @@ import { TokenDataProvider } from "./contexts/TokenDataContext"; import { MapLoadingProvider } from "./contexts/MapLoadingContext"; import { SettingsProvider } from "./contexts/SettingsContext"; import { KeyboardProvider } from "./contexts/KeyboardContext"; -import { ImageSourcesProvider } from "./contexts/ImageSourceContext"; +import { AssetsProvider, AssetURLsProvider } from "./contexts/AssetsContext"; import { ToastProvider } from "./components/Toast"; @@ -30,40 +30,42 @@ function App() { - - - - - - - {/* Legacy support camel case routes */} - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + {/* Legacy support camel case routes */} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/components/map/MapEditor.js b/src/components/map/MapEditor.js index f8dbb8a..b042ab0 100644 --- a/src/components/map/MapEditor.js +++ b/src/components/map/MapEditor.js @@ -23,7 +23,7 @@ import MapGrid from "./MapGrid"; import MapGridEditor from "./MapGridEditor"; function MapEditor({ map, onSettingsChange }) { - const [mapImageSource] = useMapImage(map); + const [mapImage] = useMapImage(map); const [stageWidth, setStageWidth] = useState(1); const [stageHeight, setStageHeight] = useState(1); @@ -132,11 +132,7 @@ function MapEditor({ map, onSettingsChange }) { )} > - + {showGridControls && canEditGrid && ( <> diff --git a/src/components/map/MapGrid.js b/src/components/map/MapGrid.js index 5cfbf94..6b30859 100644 --- a/src/components/map/MapGrid.js +++ b/src/components/map/MapGrid.js @@ -1,7 +1,7 @@ import React, { useEffect, useState } from "react"; import useImage from "use-image"; -import { useImageSource } from "../../contexts/ImageSourceContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import { mapSources as defaultMapSources } from "../../maps"; @@ -13,13 +13,14 @@ function MapGrid({ map }) { let mapSourceMap = map; // Use lowest resolution for grid lightness if (map && map.type === "file" && map.resolutions) { + // FIXME - move to resolutions array const resolutionArray = Object.keys(map.resolutions); if (resolutionArray.length > 0) { - mapSourceMap = map.resolutions[resolutionArray[0]]; + mapSourceMap.quality = resolutionArray[0]; } } - const mapSource = useImageSource(mapSourceMap, defaultMapSources); - const [mapImage, mapLoadingStatus] = useImage(mapSource); + const mapURL = useDataURL(mapSourceMap, defaultMapSources); + const [mapImage, mapLoadingStatus] = useImage(mapURL); const [isImageLight, setIsImageLight] = useState(true); diff --git a/src/components/map/MapInteraction.js b/src/components/map/MapInteraction.js index e1bb369..3d63a34 100644 --- a/src/components/map/MapInteraction.js +++ b/src/components/map/MapInteraction.js @@ -28,7 +28,7 @@ function MapInteraction({ onSelectedToolChange, disabledControls, }) { - const [mapImageSource, mapImageSourceStatus] = useMapImage(map); + const [mapImage, mapImageStatus] = useMapImage(map); // Map loaded taking in to account different resolutions const [mapLoaded, setMapLoaded] = useState(false); @@ -36,14 +36,15 @@ function MapInteraction({ if ( !map || !mapState || + // FIXME (map.type === "file" && !map.file && !map.resolutions) || mapState.mapId !== map.id ) { setMapLoaded(false); - } else if (mapImageSourceStatus === "loaded") { + } else if (mapImageStatus === "loaded") { setMapLoaded(true); } - }, [mapImageSourceStatus, map, mapState]); + }, [mapImageStatus, map, mapState]); const [stageWidth, setStageWidth] = useState(1); const [stageHeight, setStageHeight] = useState(1); @@ -211,7 +212,7 @@ function MapInteraction({ > onMapSelect(map)} diff --git a/src/components/map/MapToken.js b/src/components/map/MapToken.js index 1f7258f..e866c89 100644 --- a/src/components/map/MapToken.js +++ b/src/components/map/MapToken.js @@ -16,7 +16,7 @@ import { useDebouncedStageScale, } from "../../contexts/MapInteractionContext"; import { useGridCellPixelSize } from "../../contexts/GridContext"; -import { useImageSource } from "../../contexts/ImageSourceContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import TokenStatus from "../token/TokenStatus"; import TokenLabel from "../token/TokenLabel"; @@ -43,7 +43,7 @@ function MapToken({ const gridCellPixelSize = useGridCellPixelSize(); - const tokenSource = useImageSource(token, tokenSources, unknownSource); + const tokenSource = useDataURL(token, tokenSources, unknownSource); const [tokenSourceImage, tokenSourceStatus] = useImage(tokenSource); const [tokenAspectRatio, setTokenAspectRatio] = useState(1); diff --git a/src/components/token/ListToken.js b/src/components/token/ListToken.js index 9a094e3..7ab1720 100644 --- a/src/components/token/ListToken.js +++ b/src/components/token/ListToken.js @@ -3,12 +3,12 @@ import { Box, Image } from "theme-ui"; import usePreventTouch from "../../hooks/usePreventTouch"; -import { useImageSource } from "../../contexts/ImageSourceContext"; +import { useDataURL } from "../../contexts/AssetsContext"; import { tokenSources, unknownSource } from "../../tokens"; function ListToken({ token, className }) { - const tokenSource = useImageSource( + const tokenURL = useDataURL( token, tokenSources, unknownSource, @@ -22,7 +22,7 @@ function ListToken({ token, className }) { return ( onTokenSelect(token)} diff --git a/src/contexts/AssetsContext.js b/src/contexts/AssetsContext.js new file mode 100644 index 0000000..77b40c2 --- /dev/null +++ b/src/contexts/AssetsContext.js @@ -0,0 +1,273 @@ +import React, { useState, useContext, useCallback, useEffect } from "react"; +import { decode } from "@msgpack/msgpack"; + +import { useDatabase } from "./DatabaseContext"; + +import { omit } from "../helpers/shared"; + +/** + * @typedef Asset + * @property {string} id + * @property {number} width + * @property {number} height + * @property {Uint8Array} file + * @property {string} mime + */ + +/** + * @callback getAsset + * @param {string} assetId + * @returns {Promise} + */ + +/** + * @typedef AssetsContext + * @property {getAsset} getAsset + */ + +/** + * @type {React.Context} + */ +const AssetsContext = React.createContext(); + +export function AssetsProvider({ children }) { + const { worker } = useDatabase(); + + const getAsset = useCallback( + async (assetId) => { + const packed = await worker.loadData("assets", assetId); + return decode(packed); + }, + [worker] + ); + + return ( + + {children} + + ); +} + +export function useAssets() { + const context = useContext(AssetsContext); + if (context === undefined) { + throw new Error("useAssets must be used within a AssetsProvider"); + } + return context; +} + +/** + * @typedef AssetURL + * @property {string} url + * @property {string} id + * @property {number} references + */ + +/** + * @type React.Context> + */ +export const AssetURLsStateContext = React.createContext(); + +/** + * @type React.Context>> + */ +export const AssetURLsUpdaterContext = React.createContext(); + +/** + * Helper to manage sharing of custom image sources between uses of useAssetURL + */ +export function AssetURLsProvider({ children }) { + const [assetURLs, setAssetURLs] = useState({}); + + // Revoke url when no more references + useEffect(() => { + let urlsToCleanup = []; + for (let url of Object.values(assetURLs)) { + if (url.references <= 0) { + URL.revokeObjectURL(url.url); + urlsToCleanup.push(url.id); + } + } + if (urlsToCleanup.length > 0) { + setAssetURLs((prevURLs) => omit(prevURLs, urlsToCleanup)); + } + }, [assetURLs]); + + return ( + + + {children} + + + ); +} + +/** + * Helper function to load either file or default asset into a URL + * @param {string} assetId + * @param {"file"|"default"} type + * @param {Object.} defaultSources + * @param {string} unknownSource + * @returns {string} + */ +export function useAssetURL(assetId, type, defaultSources, unknownSource = "") { + const assetURLs = useContext(AssetURLsStateContext); + if (assetURLs === undefined) { + throw new Error("useAssetURL must be used within a AssetURLsProvider"); + } + const setAssetURLs = useContext(AssetURLsUpdaterContext); + if (setAssetURLs === undefined) { + throw new Error("useAssetURL must be used within a AssetURLsProvider"); + } + + const { getAsset } = useAssets(); + + useEffect(() => { + if (!assetId || type !== "file") { + return; + } + + async function updateAssetURL() { + const asset = await getAsset(assetId); + if (asset) { + setAssetURLs((prevURLs) => { + if (assetId in prevURLs) { + // Check if the asset url is already added + return { + ...prevURLs, + [assetId]: { + ...prevURLs[assetId], + // Increase references + references: prevURLs[assetId].references + 1, + }, + }; + } else { + const url = URL.createObjectURL( + new Blob([asset.file], { type: asset.mime }) + ); + return { + ...prevURLs, + [assetId]: { url, id: assetId, references: 1 }, + }; + } + }); + } + } + + updateAssetURL(); + + return () => { + // Decrease references + setAssetURLs((prevURLs) => { + if (assetId in prevURLs) { + return { + ...prevURLs, + [assetId]: { + ...prevURLs[assetId], + references: prevURLs[assetId].references - 1, + }, + }; + } else { + return prevURLs; + } + }); + }; + }, [assetId, setAssetURLs, getAsset, type]); + + if (!assetId) { + return unknownSource; + } + + if (type === "default") { + return defaultSources[assetId]; + } + + if (type === "file") { + return assetURLs[assetId]?.url; + } + + return unknownSource; +} + +const dataResolutions = ["ultra", "high", "medium", "low"]; + +/** + * @typedef FileData + * @property {string} file + * @property {"file"} type + * @property {string} thumbnail + * @property {string=} quality + * @property {Object.=} resolutions + */ + +/** + * @typedef DefaultData + * @property {string} key + * @property {"default"} type + */ + +/** + * Load a map or token into a URL taking into account a thumbnail and multiple resolutions + * @param {FileData|DefaultData} data + * @param {Object.} defaultSources + * @param {string} unknownSource + * @param {boolean} thumbnail + * @returns {string} + */ +export function useDataURL( + data, + defaultSources, + unknownSource = "", + thumbnail = false +) { + const { database } = useDatabase(); + const [assetId, setAssetId] = useState(); + + useEffect(() => { + if (!data) { + return; + } + async function loastAssetId() { + if (data.type === "default") { + setAssetId(data.key); + } else { + if (thumbnail) { + setAssetId(data.thumbnail); + } else if (data.resolutions) { + const fileKeys = await database + .table("assets") + .where("id") + .equals(data.file) + .primaryKeys(); + const fileExists = fileKeys.length > 0; + // Check if a resolution is specified + if (data.quality && data.resolutions[data.quality]) { + setAssetId(data.resolutions[data.quality]); + } + // If no file available fallback to the highest resolution + else if (!fileExists) { + for (let res of dataResolutions) { + if (res in data.resolutions) { + setAssetId(data.resolutions[res]); + break; + } + } + } else { + setAssetId(data.file); + } + } else { + setAssetId(data.file); + } + } + } + + loastAssetId(); + }, [data, thumbnail, database]); + + const type = data?.type || "default"; + + const assetURL = useAssetURL(assetId, type, defaultSources, unknownSource); + return assetURL; +} + +export default AssetsContext; diff --git a/src/contexts/ImageSourceContext.js b/src/contexts/ImageSourceContext.js deleted file mode 100644 index b558383..0000000 --- a/src/contexts/ImageSourceContext.js +++ /dev/null @@ -1,157 +0,0 @@ -import React, { useContext, useState, useEffect } from "react"; - -import { omit } from "../helpers/shared"; - -export const ImageSourcesStateContext = React.createContext(); -export const ImageSourcesUpdaterContext = React.createContext(() => {}); - -/** - * Helper to manage sharing of custom image sources between uses of useImageSource - */ -export function ImageSourcesProvider({ children }) { - const [imageSources, setImageSources] = useState({}); - - // Revoke url when no more references - useEffect(() => { - let sourcesToCleanup = []; - for (let source of Object.values(imageSources)) { - if (source.references <= 0) { - URL.revokeObjectURL(source.url); - sourcesToCleanup.push(source.id); - } - } - if (sourcesToCleanup.length > 0) { - setImageSources((prevSources) => omit(prevSources, sourcesToCleanup)); - } - }, [imageSources]); - - return ( - - - {children} - - - ); -} - -/** - * Get id from image data - */ -function getImageFileId(data, thumbnail) { - if (thumbnail) { - return `${data.id}-thumbnail`; - } - if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - return `${data.id}-${data.quality}`; - } else if (!data.file) { - // Fallback to the highest resolution - const resolutionArray = Object.keys(data.resolutions); - const resolution = resolutionArray[resolutionArray.length - 1]; - return `${data.id}-${resolution.id}`; - } - } - return data.id; -} - -/** - * Helper function to load either file or default image into a URL - */ -export function useImageSource(data, defaultSources, unknownSource, thumbnail) { - const imageSources = useContext(ImageSourcesStateContext); - if (imageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - const setImageSources = useContext(ImageSourcesUpdaterContext); - if (setImageSources === undefined) { - throw new Error( - "useImageSource must be used within a ImageSourcesProvider" - ); - } - - useEffect(() => { - if (!data || data.type !== "file") { - return; - } - const id = getImageFileId(data, thumbnail); - - function updateImageSource(file) { - if (file) { - setImageSources((prevSources) => { - if (id in prevSources) { - // Check if the image source is already added - return { - ...prevSources, - [id]: { - ...prevSources[id], - // Increase references - references: prevSources[id].references + 1, - }, - }; - } else { - const url = URL.createObjectURL(new Blob([file])); - return { - ...prevSources, - [id]: { url, id, references: 1 }, - }; - } - }); - } - } - - if (thumbnail) { - updateImageSource(data.thumbnail.file); - } else if (data.resolutions) { - // Check is a resolution is specified - if (data.quality && data.resolutions[data.quality]) { - updateImageSource(data.resolutions[data.quality].file); - } - // If no file available fallback to the highest resolution - else if (!data.file) { - const resolutionArray = Object.keys(data.resolutions); - updateImageSource( - data.resolutions[resolutionArray[resolutionArray.length - 1]].file - ); - } else { - updateImageSource(data.file); - } - } else { - updateImageSource(data.file); - } - - return () => { - // Decrease references - setImageSources((prevSources) => { - if (id in prevSources) { - return { - ...prevSources, - [id]: { - ...prevSources[id], - references: prevSources[id].references - 1, - }, - }; - } else { - return prevSources; - } - }); - }; - }, [data, unknownSource, thumbnail, setImageSources]); - - if (!data) { - return unknownSource; - } - - if (data.type === "default") { - return defaultSources[data.key]; - } - - if (data.type === "file") { - const id = getImageFileId(data, thumbnail); - return imageSources[id]?.url; - } - - return unknownSource; -} diff --git a/src/database.js b/src/database.js index 5ed9ed2..3257635 100644 --- a/src/database.js +++ b/src/database.js @@ -2,6 +2,7 @@ import Dexie, { Version, DexieOptions } from "dexie"; import "dexie-observable"; import shortid from "shortid"; +import { v4 as uuid } from "uuid"; import blobToBuffer from "./helpers/blobToBuffer"; import { getGridDefaultInset } from "./helpers/grid"; @@ -431,9 +432,139 @@ const versions = { }); }); }, + // v1.9.0 - Move map assets into new table + 23(v) { + v.stores({ assets: "id" }).upgrade((tx) => { + tx.table("maps").each((map) => { + let assets = []; + assets.push({ + id: uuid(), + file: map.file, + width: map.width, + height: map.height, + mime: "", + prevId: map.id, + prevType: "map", + }); + + for (let resolution in map.resolutions) { + const mapRes = map.resolutions[resolution]; + assets.push({ + id: uuid(), + file: mapRes.file, + width: mapRes.width, + height: mapRes.height, + mime: "", + prevId: map.id, + prevType: "mapResolution", + resolution, + }); + } + + assets.push({ + id: uuid(), + file: map.thumbnail.file, + width: map.thumbnail.width, + height: map.thumbnail.height, + mime: "", + prevId: map.id, + prevType: "mapThumbnail", + }); + + tx.table("assets").bulkAdd(assets); + }); + }); + }, + // v1.9.0 - Move token assets into new table + 24(v) { + v.stores().upgrade((tx) => { + tx.table("tokens").each((token) => { + let assets = []; + assets.push({ + id: uuid(), + file: token.file, + width: token.width, + height: token.height, + mime: "", + prevId: token.id, + prevType: "token", + }); + assets.push({ + id: uuid(), + file: token.thumbnail.file, + width: token.thumbnail.width, + height: token.thumbnail.height, + mime: "", + prevId: token.id, + prevType: "tokenThumbnail", + }); + tx.table("assets").bulkAdd(assets); + }); + }); + }, + // v1.9.0 - Create foreign keys for assets + 25(v) { + v.stores().upgrade((tx) => { + tx.table("assets").each((asset) => { + if (asset.prevType === "map") { + tx.table("maps").update(asset.prevId, { + file: asset.id, + width: undefined, + height: undefined, + }); + } else if (asset.prevType === "token") { + tx.table("tokens").update(asset.prevId, { + file: asset.id, + width: undefined, + height: undefined, + }); + } else if (asset.prevType === "mapThumbnail") { + tx.table("maps").update(asset.prevId, { thumbnail: asset.id }); + } else if (asset.prevType === "tokenThumbnail") { + tx.table("tokens").update(asset.prevId, { thumbnail: asset.id }); + } else if (asset.prevType === "mapResolution") { + tx.table("maps").update(asset.prevId, { + resolutions: undefined, + [asset.resolution]: asset.id, + }); + } + }); + }); + }, + // v1.9.0 - Remove asset migration helpers + 26(v) { + v.stores().upgrade((tx) => { + tx.table("assets") + .toCollection() + .modify((asset) => { + delete asset.prevId; + if (asset.prevType === "mapResolution") { + delete asset.resolution; + } + delete asset.prevType; + }); + }); + }, + // v1.9.0 - Remap map resolution assets + 27(v) { + v.stores().upgrade((tx) => { + tx.table("maps") + .toCollection() + .modify((map) => { + const resolutions = ["low", "medium", "high", "ultra"]; + map.resolutions = {}; + for (let res of resolutions) { + if (res in map) { + map.resolutions[res] = map[res]; + delete map[res]; + } + } + }); + }); + }, }; -const latestVersion = 22; +const latestVersion = 27; /** * Load versions onto a database up to a specific version number diff --git a/src/helpers/KonvaBridge.js b/src/helpers/KonvaBridge.js index b8fb3db..0573859 100644 --- a/src/helpers/KonvaBridge.js +++ b/src/helpers/KonvaBridge.js @@ -23,10 +23,10 @@ import AuthContext, { useAuth } from "../contexts/AuthContext"; import SettingsContext, { useSettings } from "../contexts/SettingsContext"; import KeyboardContext from "../contexts/KeyboardContext"; import TokenDataContext, { useTokenData } from "../contexts/TokenDataContext"; -import { - ImageSourcesStateContext, - ImageSourcesUpdaterContext, -} from "../contexts/ImageSourceContext"; +import AssetsContext, { + AssetURLsStateContext, + AssetURLsUpdaterContext, +} from "../contexts/AssetsContext"; import { useGrid, useGridCellPixelSize, @@ -52,8 +52,9 @@ function KonvaBridge({ stageRender, children }) { const auth = useAuth(); const settings = useSettings(); const tokenData = useTokenData(); - const imageSources = useContext(ImageSourcesStateContext); - const setImageSources = useContext(ImageSourcesUpdaterContext); + const assets = useContext(AssetsContext); + const assetURLs = useContext(AssetURLsStateContext); + const setAssetURLs = useContext(AssetURLsUpdaterContext); const keyboardValue = useContext(KeyboardContext); const stageScale = useStageScale(); @@ -78,61 +79,63 @@ function KonvaBridge({ stageRender, children }) { - - - - - + + + + - - - - - - - - - + + + + + + + + - - - - - {children} - - - - - - - - - - - - - - - - - - + + {children} + + + + + + + + + + + + + + + + + + + diff --git a/src/hooks/useMapImage.js b/src/hooks/useMapImage.js index a67dc2b..02cf5b6 100644 --- a/src/hooks/useMapImage.js +++ b/src/hooks/useMapImage.js @@ -1,23 +1,23 @@ import { useEffect, useState } from "react"; import useImage from "use-image"; -import { useImageSource } from "../contexts/ImageSourceContext"; +import { useDataURL } from "../contexts/AssetsContext"; import { mapSources as defaultMapSources } from "../maps"; function useMapImage(map) { - const mapSource = useImageSource(map, defaultMapSources); - const [mapSourceImage, mapSourceImageStatus] = useImage(mapSource); + const mapURL = useDataURL(map, defaultMapSources); + const [mapImage, mapImageStatus] = useImage(mapURL); // Create a map source that only updates when the image is fully loaded - const [loadedMapSourceImage, setLoadedMapSourceImage] = useState(); + const [loadedMapImage, setLoadedMapImage] = useState(); useEffect(() => { - if (mapSourceImageStatus === "loaded") { - setLoadedMapSourceImage(mapSourceImage); + if (mapImageStatus === "loaded") { + setLoadedMapImage(mapImage); } - }, [mapSourceImage, mapSourceImageStatus]); + }, [mapImage, mapImageStatus]); - return [loadedMapSourceImage, mapSourceImageStatus]; + return [loadedMapImage, mapImageStatus]; } export default useMapImage; diff --git a/src/workers/DatabaseWorker.js b/src/workers/DatabaseWorker.js index 28d6c9c..8e4278f 100644 --- a/src/workers/DatabaseWorker.js +++ b/src/workers/DatabaseWorker.js @@ -15,26 +15,21 @@ let service = { * Load either a whole table or individual item from the DB * @param {string} table Table to load from * @param {string=} key Optional database key to load, if undefined whole table will be loaded - * @param {bool} excludeFiles Optional exclude files from loaded data when using whole table loading */ - async loadData(table, key, excludeFiles = true) { + async loadData(table, key) { try { let db = getDatabase({}); if (key) { // Load specific item const data = await db.table(table).get(key); - return data; + const packed = encode(data); + return Comlink.transfer(packed, [packed.buffer]); } else { // Load entire table let items = []; // Use a cursor instead of toArray to prevent IPC max size error await db.table(table).each((item) => { - if (excludeFiles) { - const { file, resolutions, ...rest } = item; - items.push(rest); - } else { - items.push(item); - } + items.push(item); }); // Pack data with msgpack so we can use transfer to avoid memory issues diff --git a/yarn.lock b/yarn.lock index 439014e..9ea015e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13070,6 +13070,11 @@ uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== + v8-compile-cache@^2.0.3: version "2.2.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.2.0.tgz#9471efa3ef9128d2f7c6a7ca39c4dd6b5055b132"