forked from Qortal/qortal
Initial implementation of arbitrary data build queue
This adds the loadAsynchronously() method to ArbitraryDataReader, in addition to the existing loadSynchronously() method. When requesting a website in a browser, previously the building of the resource's layers would be done synchronously in the API handler. This understandably caused many issues, so the building is now done asynchronously by a dedicated thread. A loading screen is shown in its place which auto refreshes every second until the build has completed.
This commit is contained in:
parent
c3b44cee94
commit
79bbadad2f
@ -7,6 +7,8 @@ import javax.ws.rs.*;
|
|||||||
import javax.ws.rs.core.Context;
|
import javax.ws.rs.core.Context;
|
||||||
import javax.ws.rs.core.MediaType;
|
import javax.ws.rs.core.MediaType;
|
||||||
import java.io.*;
|
import java.io.*;
|
||||||
|
import java.net.URL;
|
||||||
|
import java.nio.charset.StandardCharsets;
|
||||||
import java.nio.file.Files;
|
import java.nio.file.Files;
|
||||||
import java.nio.file.NoSuchFileException;
|
import java.nio.file.NoSuchFileException;
|
||||||
import java.nio.file.Paths;
|
import java.nio.file.Paths;
|
||||||
@ -14,6 +16,7 @@ import java.util.ArrayList;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
|
import com.google.common.io.Resources;
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
import io.swagger.v3.oas.annotations.Operation;
|
||||||
import io.swagger.v3.oas.annotations.media.Content;
|
import io.swagger.v3.oas.annotations.media.Content;
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
@ -293,8 +296,11 @@ public class WebsiteResource {
|
|||||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(resourceId, resourceIdType, service);
|
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(resourceId, resourceIdType, service);
|
||||||
arbitraryDataReader.setSecret58(secret58); // Optional, used for loading encrypted file hashes only
|
arbitraryDataReader.setSecret58(secret58); // Optional, used for loading encrypted file hashes only
|
||||||
try {
|
try {
|
||||||
// We could store the latest transaction signature in the extracted folder
|
if (!arbitraryDataReader.isCachedDataAvailable()) {
|
||||||
arbitraryDataReader.load(false);
|
arbitraryDataReader.loadAsynchronously();
|
||||||
|
return this.getLoadingResponse();
|
||||||
|
}
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
LOGGER.info(String.format("Unable to load %s %s: %s", service, resourceId, e.getMessage()));
|
LOGGER.info(String.format("Unable to load %s %s: %s", service, resourceId, e.getMessage()));
|
||||||
return this.getResponse(500, "Error 500: Internal Server Error");
|
return this.getResponse(500, "Error 500: Internal Server Error");
|
||||||
@ -365,6 +371,17 @@ public class WebsiteResource {
|
|||||||
return userPath;
|
return userPath;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private HttpServletResponse getLoadingResponse() {
|
||||||
|
String responseString = null;
|
||||||
|
URL url = Resources.getResource("loading/index.html");
|
||||||
|
try {
|
||||||
|
responseString = Resources.toString(url, StandardCharsets.UTF_8);
|
||||||
|
} catch (IOException e) {
|
||||||
|
LOGGER.info("Unable to show loading screen: {}", e.getMessage());
|
||||||
|
}
|
||||||
|
return this.getResponse(503, responseString);
|
||||||
|
}
|
||||||
|
|
||||||
private HttpServletResponse getResponse(int responseCode, String responseString) {
|
private HttpServletResponse getResponse(int responseCode, String responseString) {
|
||||||
try {
|
try {
|
||||||
byte[] responseData = responseString.getBytes();
|
byte[] responseData = responseString.getBytes();
|
||||||
|
@ -0,0 +1,69 @@
|
|||||||
|
package org.qortal.arbitrary;
|
||||||
|
|
||||||
|
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataFile.*;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
|
public class ArbitraryDataBuildQueueItem {
|
||||||
|
|
||||||
|
private String resourceId;
|
||||||
|
private ResourceIdType resourceIdType;
|
||||||
|
private Service service;
|
||||||
|
private Long buildStartTimestamp = null;
|
||||||
|
|
||||||
|
private static long BUILD_TIMEOUT = 60*1000L; // 60 seconds
|
||||||
|
|
||||||
|
public ArbitraryDataBuildQueueItem(String resourceId, ResourceIdType resourceIdType, Service service) {
|
||||||
|
this.resourceId = resourceId;
|
||||||
|
this.resourceIdType = resourceIdType;
|
||||||
|
this.service = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void build() throws IOException, DataException {
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
if (now == null) {
|
||||||
|
throw new IllegalStateException("NTP time hasn't synced yet");
|
||||||
|
}
|
||||||
|
|
||||||
|
this.buildStartTimestamp = now;
|
||||||
|
ArbitraryDataReader arbitraryDataReader =
|
||||||
|
new ArbitraryDataReader(this.resourceId, this.resourceIdType, this.service);
|
||||||
|
|
||||||
|
// We do not want to overwrite the existing cache, as this will be invalidated
|
||||||
|
// automatically if new data has arrived
|
||||||
|
arbitraryDataReader.loadSynchronously(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isBuilding() {
|
||||||
|
return this.buildStartTimestamp != null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isQueued() {
|
||||||
|
return this.buildStartTimestamp == null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean hasReachedBuildTimeout(Long now) {
|
||||||
|
if (now == null || this.buildStartTimestamp == null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return now - this.buildStartTimestamp > BUILD_TIMEOUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public String getResourceId() {
|
||||||
|
return this.resourceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Long getBuildStartTimestamp() {
|
||||||
|
return this.buildStartTimestamp;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return String.format("%s %s", this.service, this.resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -112,7 +112,7 @@ public class ArbitraryDataBuilder {
|
|||||||
String sig58 = Base58.encode(transactionData.getSignature());
|
String sig58 = Base58.encode(transactionData.getSignature());
|
||||||
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(sig58, ResourceIdType.TRANSACTION_DATA, this.service);
|
ArbitraryDataReader arbitraryDataReader = new ArbitraryDataReader(sig58, ResourceIdType.TRANSACTION_DATA, this.service);
|
||||||
arbitraryDataReader.setTransactionData(transactionData);
|
arbitraryDataReader.setTransactionData(transactionData);
|
||||||
arbitraryDataReader.load(true);
|
arbitraryDataReader.loadSynchronously(true);
|
||||||
Path path = arbitraryDataReader.getFilePath();
|
Path path = arbitraryDataReader.getFilePath();
|
||||||
if (path == null) {
|
if (path == null) {
|
||||||
throw new IllegalStateException(String.format("Null path when building data from transaction %s", sig58));
|
throw new IllegalStateException(String.format("Null path when building data from transaction %s", sig58));
|
||||||
|
@ -2,7 +2,7 @@ package org.qortal.arbitrary;
|
|||||||
|
|
||||||
import org.qortal.arbitrary.ArbitraryDataFile.*;
|
import org.qortal.arbitrary.ArbitraryDataFile.*;
|
||||||
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
import org.qortal.arbitrary.metadata.ArbitraryDataMetadataCache;
|
||||||
import org.qortal.controller.ArbitraryDataManager;
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
||||||
import org.qortal.repository.DataException;
|
import org.qortal.repository.DataException;
|
||||||
|
@ -4,6 +4,7 @@ import org.apache.commons.io.FileUtils;
|
|||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
|
|
||||||
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
import org.qortal.crypto.AES;
|
import org.qortal.crypto.AES;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
import org.qortal.data.transaction.ArbitraryTransactionData.*;
|
||||||
@ -59,7 +60,54 @@ public class ArbitraryDataReader {
|
|||||||
this.uncompressedPath = Paths.get(this.workingPath.toString() + File.separator + "data");
|
this.uncompressedPath = Paths.get(this.workingPath.toString() + File.separator + "data");
|
||||||
}
|
}
|
||||||
|
|
||||||
public void load(boolean overwrite) throws IllegalStateException, IOException, DataException {
|
public boolean isCachedDataAvailable() {
|
||||||
|
// If this resource is in the build queue then we shouldn't attempt to serve
|
||||||
|
// cached data, as it may not be fully built
|
||||||
|
ArbitraryDataBuildQueueItem queueItem =
|
||||||
|
new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service);
|
||||||
|
if (ArbitraryDataManager.getInstance().isInBuildQueue(queueItem)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in the build queue - so check the cache itself
|
||||||
|
ArbitraryDataCache cache = new ArbitraryDataCache(this.uncompressedPath, false,
|
||||||
|
this.resourceId, this.resourceIdType, this.service);
|
||||||
|
if (!cache.shouldInvalidate()) {
|
||||||
|
this.filePath = this.uncompressedPath;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadAsynchronously
|
||||||
|
*
|
||||||
|
* Attempts to load the resource asynchronously
|
||||||
|
* This adds the build task to a queue, and the result will be cached when complete
|
||||||
|
* To check the status of the build, periodically call isCachedDataAvailable()
|
||||||
|
* Once it returns true, you can then use getFilePath() to access the data itself.
|
||||||
|
* TODO: create API to check the status
|
||||||
|
* @return
|
||||||
|
*/
|
||||||
|
public boolean loadAsynchronously() {
|
||||||
|
ArbitraryDataBuildQueueItem queueItem =
|
||||||
|
new ArbitraryDataBuildQueueItem(this.resourceId, this.resourceIdType, this.service);
|
||||||
|
return ArbitraryDataManager.getInstance().addToBuildQueue(queueItem);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* loadSynchronously
|
||||||
|
*
|
||||||
|
* Attempts to load the resource synchronously
|
||||||
|
* Warning: this can block for a long time when building or fetching complex data
|
||||||
|
* If no exception is thrown, you can then use getFilePath() to access the data immediately after returning
|
||||||
|
*
|
||||||
|
* @param overwrite - set to true to force rebuild an existing cache
|
||||||
|
* @throws IllegalStateException
|
||||||
|
* @throws IOException
|
||||||
|
* @throws DataException
|
||||||
|
*/
|
||||||
|
public void loadSynchronously(boolean overwrite) throws IllegalStateException, IOException, DataException {
|
||||||
try {
|
try {
|
||||||
ArbitraryDataCache cache = new ArbitraryDataCache(this.uncompressedPath, overwrite,
|
ArbitraryDataCache cache = new ArbitraryDataCache(this.uncompressedPath, overwrite,
|
||||||
this.resourceId, this.resourceIdType, this.service);
|
this.resourceId, this.resourceIdType, this.service);
|
||||||
|
@ -45,7 +45,7 @@ public class ArbitraryDataMetadataCache extends ArbitraryDataMetadata {
|
|||||||
patch.put("timestamp", this.timestamp);
|
patch.put("timestamp", this.timestamp);
|
||||||
|
|
||||||
this.jsonString = patch.toString(2);
|
this.jsonString = patch.toString(2);
|
||||||
LOGGER.info("Cache metadata: {}", this.jsonString);
|
LOGGER.trace("Cache metadata: {}", this.jsonString);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@ -14,6 +14,7 @@ import org.qortal.block.Block;
|
|||||||
import org.qortal.block.BlockChain;
|
import org.qortal.block.BlockChain;
|
||||||
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
import org.qortal.block.BlockChain.BlockTimingByHeight;
|
||||||
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
import org.qortal.controller.Synchronizer.SynchronizationResult;
|
||||||
|
import org.qortal.controller.arbitrary.ArbitraryDataManager;
|
||||||
import org.qortal.controller.tradebot.TradeBot;
|
import org.qortal.controller.tradebot.TradeBot;
|
||||||
import org.qortal.data.account.MintingAccountData;
|
import org.qortal.data.account.MintingAccountData;
|
||||||
import org.qortal.data.account.RewardShareData;
|
import org.qortal.data.account.RewardShareData;
|
||||||
@ -332,7 +333,7 @@ public class Controller extends Thread {
|
|||||||
return this.savedArgs;
|
return this.savedArgs;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* package */ static boolean isStopping() {
|
/* package */ public static boolean isStopping() {
|
||||||
return isStopping;
|
return isStopping;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -0,0 +1,78 @@
|
|||||||
|
package org.qortal.controller.arbitrary;
|
||||||
|
|
||||||
|
import org.apache.logging.log4j.LogManager;
|
||||||
|
import org.apache.logging.log4j.Logger;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataBuildQueueItem;
|
||||||
|
import org.qortal.controller.Controller;
|
||||||
|
import org.qortal.repository.DataException;
|
||||||
|
import org.qortal.utils.NTP;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
|
||||||
|
public class ArbitraryDataBuildManager implements Runnable {
|
||||||
|
|
||||||
|
private static final Logger LOGGER = LogManager.getLogger(ArbitraryDataBuildManager.class);
|
||||||
|
|
||||||
|
public ArbitraryDataBuildManager() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public void run() {
|
||||||
|
Thread.currentThread().setName("Arbitrary Data Build Manager");
|
||||||
|
ArbitraryDataManager arbitraryDataManager = ArbitraryDataManager.getInstance();
|
||||||
|
|
||||||
|
while (!Controller.isStopping()) {
|
||||||
|
try {
|
||||||
|
Thread.sleep(1000);
|
||||||
|
|
||||||
|
if (arbitraryDataManager.arbitraryDataBuildQueue == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (arbitraryDataManager.arbitraryDataBuildQueue.isEmpty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find resources that are queued for building
|
||||||
|
Map.Entry<String, ArbitraryDataBuildQueueItem> next = arbitraryDataManager.arbitraryDataBuildQueue
|
||||||
|
.entrySet().stream().filter(e -> e.getValue().isQueued()).findFirst().get();
|
||||||
|
|
||||||
|
if (next == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
if (now == null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
String resourceId = next.getKey();
|
||||||
|
ArbitraryDataBuildQueueItem queueItem = next.getValue();
|
||||||
|
if (queueItem == null || queueItem.hasReachedBuildTimeout(now)) {
|
||||||
|
this.removeFromQueue(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Perform the build
|
||||||
|
LOGGER.info("Building {}...", queueItem);
|
||||||
|
queueItem.build();
|
||||||
|
this.removeFromQueue(resourceId);
|
||||||
|
LOGGER.info("Finished building {}", queueItem);
|
||||||
|
|
||||||
|
} catch (IOException | DataException e) {
|
||||||
|
// Something went wrong - so remove it from the queue
|
||||||
|
// TODO: we may want to keep track of this in a "cooloff" list to prevent frequent re-attempts
|
||||||
|
this.removeFromQueue(resourceId);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (InterruptedException e) {
|
||||||
|
// Time to exit
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void removeFromQueue(String resourceId) {
|
||||||
|
ArbitraryDataManager.getInstance().arbitraryDataBuildQueue.remove(resourceId);
|
||||||
|
}
|
||||||
|
}
|
@ -1,10 +1,14 @@
|
|||||||
package org.qortal.controller;
|
package org.qortal.controller.arbitrary;
|
||||||
|
|
||||||
import java.util.*;
|
import java.util.*;
|
||||||
|
import java.util.concurrent.ExecutorService;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import org.apache.logging.log4j.LogManager;
|
import org.apache.logging.log4j.LogManager;
|
||||||
import org.apache.logging.log4j.Logger;
|
import org.apache.logging.log4j.Logger;
|
||||||
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
import org.qortal.api.resource.TransactionsResource.ConfirmationStatus;
|
||||||
|
import org.qortal.arbitrary.ArbitraryDataBuildQueueItem;
|
||||||
|
import org.qortal.controller.Controller;
|
||||||
import org.qortal.data.transaction.ArbitraryTransactionData;
|
import org.qortal.data.transaction.ArbitraryTransactionData;
|
||||||
import org.qortal.data.transaction.TransactionData;
|
import org.qortal.data.transaction.TransactionData;
|
||||||
import org.qortal.network.Network;
|
import org.qortal.network.Network;
|
||||||
@ -70,6 +74,11 @@ public class ArbitraryDataManager extends Thread {
|
|||||||
*/
|
*/
|
||||||
private static long ARBITRARY_DATA_CACHE_TIMEOUT = 60 * 60 * 1000L; // 60 minutes
|
private static long ARBITRARY_DATA_CACHE_TIMEOUT = 60 * 60 * 1000L; // 60 minutes
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map to keep track of arbitrary transaction resources currently being built (or queued).
|
||||||
|
*/
|
||||||
|
public Map<String, ArbitraryDataBuildQueueItem> arbitraryDataBuildQueue = Collections.synchronizedMap(new HashMap<>());
|
||||||
|
|
||||||
|
|
||||||
private ArbitraryDataManager() {
|
private ArbitraryDataManager() {
|
||||||
}
|
}
|
||||||
@ -85,6 +94,11 @@ public class ArbitraryDataManager extends Thread {
|
|||||||
public void run() {
|
public void run() {
|
||||||
Thread.currentThread().setName("Arbitrary Data Manager");
|
Thread.currentThread().setName("Arbitrary Data Manager");
|
||||||
|
|
||||||
|
// Use a fixed thread pool to execute the arbitrary data build actions (currently just a single thread)
|
||||||
|
// This can be expanded to have multiple threads processing the build queue when needed
|
||||||
|
ExecutorService arbitraryDataBuildExecutor = Executors.newFixedThreadPool(1);
|
||||||
|
arbitraryDataBuildExecutor.execute(new ArbitraryDataBuildManager());
|
||||||
|
|
||||||
try {
|
try {
|
||||||
while (!isStopping) {
|
while (!isStopping) {
|
||||||
Thread.sleep(2000);
|
Thread.sleep(2000);
|
||||||
@ -247,11 +261,62 @@ public class ArbitraryDataManager extends Thread {
|
|||||||
this.arbitraryDataCachedResources = new HashMap<>();
|
this.arbitraryDataCachedResources = new HashMap<>();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Long now = NTP.getTime();
|
||||||
|
if (now == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// Set the timestamp to now + the timeout
|
// Set the timestamp to now + the timeout
|
||||||
Long timestamp = NTP.getTime() + ARBITRARY_DATA_CACHE_TIMEOUT;
|
Long timestamp = NTP.getTime() + ARBITRARY_DATA_CACHE_TIMEOUT;
|
||||||
this.arbitraryDataCachedResources.put(resourceId, timestamp);
|
this.arbitraryDataCachedResources.put(resourceId, timestamp);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build queue
|
||||||
|
public boolean addToBuildQueue(ArbitraryDataBuildQueueItem queueItem) {
|
||||||
|
String resourceId = queueItem.getResourceId();
|
||||||
|
if (resourceId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.arbitraryDataBuildQueue == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NTP.getTime() == null) {
|
||||||
|
// Can't use queues until we have synced the time
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.arbitraryDataBuildQueue.put(resourceId, queueItem) != null) {
|
||||||
|
// Already in queue
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
LOGGER.info("Added {} to build queue", resourceId);
|
||||||
|
|
||||||
|
// Added to queue
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isInBuildQueue(ArbitraryDataBuildQueueItem queueItem) {
|
||||||
|
String resourceId = queueItem.getResourceId();
|
||||||
|
if (resourceId == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.arbitraryDataBuildQueue == null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.arbitraryDataBuildQueue.containsKey(resourceId)) {
|
||||||
|
// Already in queue
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not in queue
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// Network handlers
|
// Network handlers
|
||||||
|
|
23
src/main/resources/loading/index.html
Normal file
23
src/main/resources/loading/index.html
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Loading...</title>
|
||||||
|
<script>
|
||||||
|
setTimeout(window.location.reload.bind(window.location), 1);
|
||||||
|
</script>
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: Arial;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
margin-top: 50px;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
</html>
|
||||||
|
<body>
|
||||||
|
<h1>Loading... please wait...</h1>
|
||||||
|
<p>This page will refresh automatically when the content becomes available</p>
|
||||||
|
<p>(We can show a Qortal branded loading screen here)</p>
|
||||||
|
</body>
|
||||||
|
</html>
|
Loading…
Reference in New Issue
Block a user