Changed asset loading bar to show progress as a group of assets

This commit is contained in:
Mitchell McCaffrey 2021-04-29 13:49:39 +10:00
parent 994b9b5ebb
commit 0ccee84cbf
4 changed files with 77 additions and 64 deletions

View File

@ -1,46 +1,44 @@
import React, { useState, useRef, useContext } from "react"; import React, { useState, useRef, useContext, useCallback } from "react";
import { omit, isEmpty } from "../helpers/shared";
const MapLoadingContext = React.createContext(); const MapLoadingContext = React.createContext();
export function MapLoadingProvider({ children }) { export function MapLoadingProvider({ children }) {
const [loadingAssetCount, setLoadingAssetCount] = useState(0); const [isLoading, setIsLoading] = useState(false);
// Mapping from asset id to the count and total number of pieces loaded
function assetLoadStart() {
setLoadingAssetCount((prevLoadingAssets) => prevLoadingAssets + 1);
}
function assetLoadFinish() {
setLoadingAssetCount((prevLoadingAssets) => prevLoadingAssets - 1);
}
const assetProgressRef = useRef({}); const assetProgressRef = useRef({});
// Loading progress of all assets between 0 and 1
const loadingProgressRef = useRef(null); 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 = { const value = {
assetLoadStart, assetLoadStart,
assetLoadFinish,
isLoading, isLoading,
assetProgressUpdate, assetProgressUpdate,
loadingProgressRef, loadingProgressRef,

View File

@ -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 * Custom send function with encoding, chunking and data channel support
sendObject(object, channel) { * 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 { try {
const packedData = encode(object); const packedData = encode(object);
if (packedData.byteLength > MAX_BUFFER_SIZE) { if (packedData.byteLength > MAX_BUFFER_SIZE) {
const chunks = this.chunk(packedData); const chunks = this.chunk(packedData, chunkId);
for (let chunk of chunks) { for (let chunk of chunks) {
if (this.dataChannels[channel]) { if (this.dataChannels[channel]) {
this.dataChannels[channel].write(encode(chunk)); this.dataChannels[channel].write(encode(chunk));
@ -100,11 +105,17 @@ class Connection extends SimplePeer {
} }
// Converted from https://github.com/peers/peerjs/ // 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 chunks = [];
const size = data.byteLength; const size = data.byteLength;
const total = Math.ceil(size / MAX_BUFFER_SIZE); const total = Math.ceil(size / MAX_BUFFER_SIZE);
const id = shortid.generate(); const id = chunkId || shortid.generate();
let index = 0; let index = 0;
let start = 0; let start = 0;

View File

@ -39,12 +39,7 @@ function NetworkedMapAndTokens({ session }) {
const { addToast } = useToasts(); const { addToast } = useToasts();
const { userId } = useAuth(); const { userId } = useAuth();
const partyState = useParty(); const partyState = useParty();
const { const { assetLoadStart, assetProgressUpdate, isLoading } = useMapLoading();
assetLoadStart,
assetLoadFinish,
assetProgressUpdate,
isLoading,
} = useMapLoading();
const { updateMapState } = useMapData(); const { updateMapState } = useMapData();
const { getAsset, putAsset } = useAssets(); const { getAsset, putAsset } = useAssets();
@ -115,7 +110,7 @@ function NetworkedMapAndTokens({ session }) {
const requestingAssetsRef = useRef(new Set()); const requestingAssetsRef = useRef(new Set());
useEffect(() => { useEffect(() => {
if (!assetManifest) { if (!assetManifest || !userId) {
return; return;
} }
@ -132,6 +127,9 @@ function NetworkedMapAndTokens({ session }) {
(player) => player.userId === asset.owner (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); const cachedAsset = await getAsset(asset.id);
if (!owner) { if (!owner) {
// Add no owner toast if we don't have asset in out cache // 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 // TODO: Stop toast from appearing multiple times
addToast("Unable to find owner for asset"); addToast("Unable to find owner for asset");
} }
requestingAssetsRef.current.delete(asset.id);
continue; continue;
} }
requestingAssetsRef.current.add(asset.id);
if (cachedAsset) { if (cachedAsset) {
requestingAssetsRef.current.delete(asset.id); requestingAssetsRef.current.delete(asset.id);
} else { } else {
session.sendTo(owner.sessionId, "assetRequest", asset.id); session.sendTo(owner.sessionId, "assetRequest", asset.id);
assetLoadStart(asset.id);
} }
} }
} }
requestAssetsIfNeeded(); requestAssetsIfNeeded();
}, [assetManifest, partyState, session, userId, addToast, getAsset]); }, [
assetManifest,
partyState,
session,
userId,
addToast,
getAsset,
assetLoadStart,
]);
/** /**
* Map state * Map state
@ -376,21 +382,16 @@ function NetworkedMapAndTokens({ session }) {
async function handlePeerData({ id, data, reply }) { async function handlePeerData({ id, data, reply }) {
if (id === "assetRequest") { if (id === "assetRequest") {
const asset = await getAsset(data); const asset = await getAsset(data);
reply("assetResponse", asset); reply("assetResponse", asset, undefined, asset.id);
} }
if (id === "assetResponse") { if (id === "assetResponse") {
await putAsset(data); await putAsset(data);
requestingAssetsRef.current.delete(data.id); requestingAssetsRef.current.delete(data.id);
assetLoadFinish();
} }
} }
function handlePeerDataProgress({ id, total, count }) { function handlePeerDataProgress({ id, total, count }) {
if (count === 1) {
// Corresponding asset load finished called in asset response
assetLoadStart();
}
assetProgressUpdate({ id, total, count }); assetProgressUpdate({ id, total, count });
} }

View File

@ -19,7 +19,8 @@ import { logError } from "../helpers/logging";
* @callback peerReply * @callback peerReply
* @param {string} id - The id of the event * @param {string} id - The id of the event
* @param {object} data - The data to send * @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} sessionId - The socket id of the player to send to
* @param {string} eventId - The id of the event to send * @param {string} eventId - The id of the event to send
* @param {object} data * @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 (!(sessionId in this.peers)) {
if (!this._addPeer(sessionId, true)) { if (!this._addPeer(sessionId, true)) {
return; return;
@ -133,7 +135,8 @@ class Session extends EventEmitter {
this.peers[sessionId].connection.once("connect", () => { this.peers[sessionId].connection.once("connect", () => {
this.peers[sessionId].connection.sendObject( this.peers[sessionId].connection.sendObject(
{ id: eventId, data }, { id: eventId, data },
channel channel,
chunkId
); );
}); });
} else { } else {
@ -226,8 +229,8 @@ class Session extends EventEmitter {
const peer = { id, connection, initiator, ready: false }; const peer = { id, connection, initiator, ready: false };
function sendPeer(id, data, channel) { function reply(id, data, channel, chunkId) {
peer.connection.sendObject({ id, data }, channel); peer.connection.sendObject({ id, data }, channel, chunkId);
} }
function handleSignal(signal) { function handleSignal(signal) {
@ -246,7 +249,7 @@ class Session extends EventEmitter {
* @property {SessionPeer} peer * @property {SessionPeer} peer
* @property {peerReply} reply * @property {peerReply} reply
*/ */
this.emit("peerConnect", { peer, reply: sendPeer }); this.emit("peerConnect", { peer, reply });
} }
function handleDataComplete(data) { function handleDataComplete(data) {
@ -264,7 +267,7 @@ class Session extends EventEmitter {
peer, peer,
id: data.id, id: data.id,
data: data.data, data: data.data,
reply: sendPeer, reply,
}); });
} }
@ -274,7 +277,7 @@ class Session extends EventEmitter {
id, id,
count, count,
total, total,
reply: sendPeer, reply,
}); });
} }