diff --git a/src/contexts/MapLoadingContext.js b/src/contexts/MapLoadingContext.js index 94cb5b6..3c12990 100644 --- a/src/contexts/MapLoadingContext.js +++ b/src/contexts/MapLoadingContext.js @@ -1,46 +1,44 @@ -import React, { useState, useRef, useContext } from "react"; -import { omit, isEmpty } from "../helpers/shared"; +import React, { useState, useRef, useContext, useCallback } from "react"; const MapLoadingContext = React.createContext(); export function MapLoadingProvider({ children }) { - const [loadingAssetCount, setLoadingAssetCount] = useState(0); - - function assetLoadStart() { - setLoadingAssetCount((prevLoadingAssets) => prevLoadingAssets + 1); - } - - function assetLoadFinish() { - setLoadingAssetCount((prevLoadingAssets) => prevLoadingAssets - 1); - } - + const [isLoading, setIsLoading] = useState(false); + // Mapping from asset id to the count and total number of pieces loaded const assetProgressRef = useRef({}); + // Loading progress of all assets between 0 and 1 const loadingProgressRef = useRef(null); - function assetProgressUpdate({ id, count, total }) { - if (count === total) { - assetProgressRef.current = omit(assetProgressRef.current, [id]); - } else { - assetProgressRef.current = { - ...assetProgressRef.current, - [id]: { count, total }, - }; - } - if (!isEmpty(assetProgressRef.current)) { - let total = 0; - let count = 0; - for (let progress of Object.values(assetProgressRef.current)) { - total += progress.total; - count += progress.count; - } - loadingProgressRef.current = count / total; - } - } - const isLoading = loadingAssetCount > 0; + const assetLoadStart = useCallback((id) => { + setIsLoading(true); + // Add asset at a 0% progress + assetProgressRef.current = { + ...assetProgressRef.current, + [id]: { count: 0, total: 1 }, + }; + }, []); + + const assetProgressUpdate = useCallback(({ id, count, total }) => { + assetProgressRef.current = { + ...assetProgressRef.current, + [id]: { count, total }, + }; + // Update loading progress + let complete = 0; + const progresses = Object.values(assetProgressRef.current); + for (let progress of progresses) { + complete += progress.count / progress.total; + } + loadingProgressRef.current = complete / progresses.length; + // All loading is complete + if (loadingProgressRef.current === 1) { + setIsLoading(false); + assetProgressRef.current = {}; + } + }, []); const value = { assetLoadStart, - assetLoadFinish, isLoading, assetProgressUpdate, loadingProgressRef, diff --git a/src/network/Connection.js b/src/network/Connection.js index 4871094..984a1dd 100644 --- a/src/network/Connection.js +++ b/src/network/Connection.js @@ -55,13 +55,18 @@ class Connection extends SimplePeer { } } - // Custom send function with encoding, chunking and data channel support - // Uses `write` to send the data to allow for buffer / backpressure handling - sendObject(object, channel) { + /** + * Custom send function with encoding, chunking and data channel support + * Uses `write` to send the data to allow for buffer / backpressure handling + * @param {any} object + * @param {string=} channel + * @param {string=} chunkId Optional ID to use for chunking + */ + sendObject(object, channel, chunkId) { try { const packedData = encode(object); if (packedData.byteLength > MAX_BUFFER_SIZE) { - const chunks = this.chunk(packedData); + const chunks = this.chunk(packedData, chunkId); for (let chunk of chunks) { if (this.dataChannels[channel]) { this.dataChannels[channel].write(encode(chunk)); @@ -100,11 +105,17 @@ class Connection extends SimplePeer { } // Converted from https://github.com/peers/peerjs/ - chunk(data) { + /** + * Chunk byte array + * @param {Uint8Array} data + * @param {string=} chunkId + * @returns {Uint8Array[]} + */ + chunk(data, chunkId) { const chunks = []; const size = data.byteLength; const total = Math.ceil(size / MAX_BUFFER_SIZE); - const id = shortid.generate(); + const id = chunkId || shortid.generate(); let index = 0; let start = 0; diff --git a/src/network/NetworkedMapAndTokens.js b/src/network/NetworkedMapAndTokens.js index 13ec0a1..d007d3a 100644 --- a/src/network/NetworkedMapAndTokens.js +++ b/src/network/NetworkedMapAndTokens.js @@ -39,12 +39,7 @@ function NetworkedMapAndTokens({ session }) { const { addToast } = useToasts(); const { userId } = useAuth(); const partyState = useParty(); - const { - assetLoadStart, - assetLoadFinish, - assetProgressUpdate, - isLoading, - } = useMapLoading(); + const { assetLoadStart, assetProgressUpdate, isLoading } = useMapLoading(); const { updateMapState } = useMapData(); const { getAsset, putAsset } = useAssets(); @@ -115,7 +110,7 @@ function NetworkedMapAndTokens({ session }) { const requestingAssetsRef = useRef(new Set()); useEffect(() => { - if (!assetManifest) { + if (!assetManifest || !userId) { return; } @@ -132,6 +127,9 @@ function NetworkedMapAndTokens({ session }) { (player) => player.userId === asset.owner ); + // Ensure requests are added before any async operation to prevent them from sending twice + requestingAssetsRef.current.add(asset.id); + const cachedAsset = await getAsset(asset.id); if (!owner) { // Add no owner toast if we don't have asset in out cache @@ -139,21 +137,29 @@ function NetworkedMapAndTokens({ session }) { // TODO: Stop toast from appearing multiple times addToast("Unable to find owner for asset"); } + requestingAssetsRef.current.delete(asset.id); continue; } - requestingAssetsRef.current.add(asset.id); - if (cachedAsset) { requestingAssetsRef.current.delete(asset.id); } else { session.sendTo(owner.sessionId, "assetRequest", asset.id); + assetLoadStart(asset.id); } } } requestAssetsIfNeeded(); - }, [assetManifest, partyState, session, userId, addToast, getAsset]); + }, [ + assetManifest, + partyState, + session, + userId, + addToast, + getAsset, + assetLoadStart, + ]); /** * Map state @@ -376,21 +382,16 @@ function NetworkedMapAndTokens({ session }) { async function handlePeerData({ id, data, reply }) { if (id === "assetRequest") { const asset = await getAsset(data); - reply("assetResponse", asset); + reply("assetResponse", asset, undefined, asset.id); } if (id === "assetResponse") { await putAsset(data); requestingAssetsRef.current.delete(data.id); - assetLoadFinish(); } } function handlePeerDataProgress({ id, total, count }) { - if (count === 1) { - // Corresponding asset load finished called in asset response - assetLoadStart(); - } assetProgressUpdate({ id, total, count }); } diff --git a/src/network/Session.js b/src/network/Session.js index b646708..ce2ad57 100644 --- a/src/network/Session.js +++ b/src/network/Session.js @@ -19,7 +19,8 @@ import { logError } from "../helpers/logging"; * @callback peerReply * @param {string} id - The id of the event * @param {object} data - The data to send - * @param {string} channel - The channel to send to + * @param {string=} channel - The channel to send to + * @param {string=} chunkId */ /** @@ -120,9 +121,10 @@ class Session extends EventEmitter { * @param {string} sessionId - The socket id of the player to send to * @param {string} eventId - The id of the event to send * @param {object} data - * @param {string} channel + * @param {string=} channel + * @param {string=} chunkId */ - sendTo(sessionId, eventId, data, channel) { + sendTo(sessionId, eventId, data, channel, chunkId) { if (!(sessionId in this.peers)) { if (!this._addPeer(sessionId, true)) { return; @@ -133,7 +135,8 @@ class Session extends EventEmitter { this.peers[sessionId].connection.once("connect", () => { this.peers[sessionId].connection.sendObject( { id: eventId, data }, - channel + channel, + chunkId ); }); } else { @@ -226,8 +229,8 @@ class Session extends EventEmitter { const peer = { id, connection, initiator, ready: false }; - function sendPeer(id, data, channel) { - peer.connection.sendObject({ id, data }, channel); + function reply(id, data, channel, chunkId) { + peer.connection.sendObject({ id, data }, channel, chunkId); } function handleSignal(signal) { @@ -246,7 +249,7 @@ class Session extends EventEmitter { * @property {SessionPeer} peer * @property {peerReply} reply */ - this.emit("peerConnect", { peer, reply: sendPeer }); + this.emit("peerConnect", { peer, reply }); } function handleDataComplete(data) { @@ -264,7 +267,7 @@ class Session extends EventEmitter { peer, id: data.id, data: data.data, - reply: sendPeer, + reply, }); } @@ -274,7 +277,7 @@ class Session extends EventEmitter { id, count, total, - reply: sendPeer, + reply, }); }