Skip to content

Commit

Permalink
✨ Add updated CachedXMLHttpRequest and CachedXHRExtensions
Browse files Browse the repository at this point in the history
BenV committed Apr 20, 2017
1 parent 62eee37 commit 9ddd85d
Showing 13 changed files with 305 additions and 32 deletions.
9 changes: 9 additions & 0 deletions Assets/Plugins/WebGLCachedXHRExtensions.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

56 changes: 56 additions & 0 deletions Assets/Plugins/WebGLCachedXHRExtensions/CachedXHRExtensions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using UnityEngine;
using System.Runtime.InteropServices;

namespace Kongregate {
public class CacheEntryQuery : CustomYieldInstruction {
private readonly string Url;
private int Status = 0;

public CacheEntryQuery(string url) {
Url = url;
CachedXHRExtensions_SearchCache(url);
}

public override bool keepWaiting {
get {
Status = CachedXHRExtensions_CheckStatus(Url);
return Status == 0;
}
}

public bool IsCached {
get {
if (Status == 0) {
Debug.Log("CacheEntryQuery: returning IsCached=false since query is pending");
return false;
}

return Status == 1;
}
}

#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
private static extern void CachedXHRExtensions_SearchCache(string url);

[DllImport("__Internal")]
private static extern int CachedXHRExtensions_CheckStatus(string url);
#else
private static void CachedXHRExtensions_SearchCache(string url) { }
private static int CachedXHRExtensions_CheckStatus(string url) { return -1; }
#endif
}

public class CachedXHRExtensions {
public static void CleanCache() {
CachedXHRExtensions_CleanCache();
}

#if UNITY_WEBGL && !UNITY_EDITOR
[DllImport("__Internal")]
static extern int CachedXHRExtensions_CleanCache();
#else
static void CachedXHRExtensions_CleanCache() {}
#endif
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Assets/Plugins/WebGLCachedXHRExtensions/CachedXHRExtensions.jslib
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
mergeInto(LibraryManager.library, {
CachedXHRExtensions_SearchCache: function(url) { CachedXHRExtensions.searchCache(url); },
CachedXHRExtensions_CheckStatus: function(url) { return CachedXHRExtensions.checkStatus(url); },
CachedXHRExtensions_CleanCache: function() {
try {
var self = CachedXMLHttpRequest.cache;
self.db.transaction([self.store], "readwrite").objectStore(self.store).clear().onerror = function(){
e.preventDefault();
};
} catch(e) {}
}
});

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

32 changes: 32 additions & 0 deletions Assets/Plugins/WebGLCachedXHRExtensions/CachedXHRExtensions.jspre
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
setTimeout(function(){
var enabled = typeof CachedXMLHttpRequest !== "undefined" && !!CachedXMLHttpRequest.cache;
console.log("CachedXHRExtensions initialized, enabled=" + enabled);
var CachedXHRExtensions = function() {
this._cacheStates = {};
};

CachedXHRExtensions.prototype.searchCache = function(url) {
if (!enabled) return;
var self = this;
url = typeof url === "string" ? url : Pointer_stringify(url);
delete this._cacheStates[url];

CachedXMLHttpRequest.cache.get(url, function(err, result) {
if (err || !result || !result.meta) {
self._cacheStates[url] = false;
} else {
self._cacheStates[url] = true;
}
});
};

CachedXHRExtensions.prototype.checkStatus = function(url) {
if (!enabled) return -1;

url = typeof url === "string" ? url : Pointer_stringify(url);
if (this._cacheStates[url] === undefined) return 0;
return this._cacheStates[url] ? 1 : -1;
};

window.CachedXHRExtensions = new CachedXHRExtensions();
}, 0);

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 12 additions & 0 deletions Assets/Plugins/WebGLCachedXHRExtensions/Readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
This plugin provides additional functionality on top of the CachedXMLHttpRequest
addon. The classes referred to below are in the Kongregate namespace.

* Clearing the cache by calling CachedXHRExtensions.CleanCache()
* Asynchronously querying the cache to determine if an item exists:
IEnumerator CheckIfAssetExists() {
var query = new CacheEntryQuery("https://whatever.io/file.xml");
yield return query;
if (query.IsCached) {
Debug.Log("Asset exists in cache!");
}
}
8 changes: 8 additions & 0 deletions Assets/Plugins/WebGLCachedXHRExtensions/Readme.txt.meta

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -15,7 +15,7 @@ function CachedXMLHttpRequest() {
meta.size = xhr.response.byteLength;
CachedXMLHttpRequest.cache.put(cache.requestURL, meta, xhr.response, function (err) {
CachedXMLHttpRequest.log("'" + cache.requestURL + "' downloaded successfully (" + xhr.response.byteLength + " bytes) " +
(err ? "but not stored in indexedDB cache due to error." : "and stored in indexedDB cache."));
(err ? "but not stored in indexedDB cache due to error: " + err.message : "and stored in indexedDB cache."));
if (onload)
onload(e);
});
@@ -27,22 +27,28 @@ function CachedXMLHttpRequest() {
if (onload)
onload(e);
}
}
};
return xhr.send.apply(xhr, arguments);
}

function revalidateCrossOriginRequest(meta, self, sendArguments) {
var headXHR = new CachedXMLHttpRequest.XMLHttpRequest();
headXHR.open("HEAD", meta.requestURL, false);
headXHR.send();
cache.override = meta.lastModified ? meta.lastModified == headXHR.getResponseHeader("Last-Modified") : meta.eTag && meta.eTag == headXHR.getResponseHeader("ETag");
if (!cache.override)
return send.apply(self, sendArguments);
function loadComplete() {
CachedXMLHttpRequest.log("'" + cache.requestURL + "' served from indexedDB cache (" + cache.response.byteLength + " bytes).");
if (xhr.onload)
xhr.onload();
}

function revalidateCrossOriginRequest(meta, self, sendArguments) {
var headXHR = new CachedXMLHttpRequest.XMLHttpRequest();
headXHR.open("HEAD", meta.requestURL, cache.async);
headXHR.onload = function() {
cache.override = meta.lastModified ? meta.lastModified == headXHR.getResponseHeader("Last-Modified") : meta.eTag && meta.eTag == getETag(headXHR);
if (!cache.override)
return send.apply(self, sendArguments);
loadComplete();
};
headXHR.send();
}

Object.defineProperty(self, "open", { value: function (method, url, async) {
cache = { method: method, requestURL: CachedXMLHttpRequest.cache.requestURL(url), async: async };
return xhr.open.apply(xhr, arguments);
@@ -65,6 +71,12 @@ function CachedXMLHttpRequest() {
cache.statusText = "OK";
cache.response = result.response;
cache.responseURL = result.meta.responseURL;

if (CachedXMLHttpRequest.checkBlacklist(Module.CachedXMLHttpRequestRevalidateBlacklist, cache.requestURL)) {
cache.override = true;
return loadComplete();
}

if (window.location.href.lastIndexOf(absoluteUrlMatch[0], 0))
return revalidateCrossOriginRequest(result.meta, self, sendArguments);
if (result.meta.lastModified)
@@ -97,6 +109,17 @@ CachedXMLHttpRequest.log = function (message) {
console.log("[CachedXMLHttpRequest] " + message);
};

CachedXMLHttpRequest.checkBlacklist = function(list, url) {
list = list || [];
list = Array.isArray(list) ? list : [list];
for (var i = 0; i < list.length; i++) {
var regexp = list[i];
if (typeof regexp === "string") regexp = new RegExp(regexp);
if (regexp instanceof RegExp && regexp.test(url)) return true;
}
return false;
};

CachedXMLHttpRequest.cache = {
database: "CachedXMLHttpRequest",
version: 1,
@@ -115,19 +138,24 @@ CachedXMLHttpRequest.cache = {
queue: [],
processQueue: function () {
var self = this;
self.queue.forEach(function (queued) { self[queued.action].apply(self, queued.arguments) });
self.queue.forEach(function (queued) { self[queued.action].apply(self, queued.arguments); });
self.queue = [];

},
init: function () {
var self = this;
var self = this, onError = function(e) {
CachedXMLHttpRequest.log("can not open indexedDB database: " + e.message);
self.indexedDB = null;
self.processQueue();
if (e.preventDefault) e.preventDefault();
};
if (!self.indexedDB)
return CachedXMLHttpRequest.log("indexedDB is not available");
var openDB;
try {
openDB = indexedDB.open(self.database, self.version);
} catch(e) {
return CachedXMLHttpRequest.log("indexedDB access denied");
return onError(new Error("indexedDB access denied"));
}
openDB.onupgradeneeded = function (e) {
var db = e.target.result;
@@ -140,56 +168,57 @@ CachedXMLHttpRequest.cache = {
objectStore.createIndex("meta", "meta", {unique: false});
}
objectStore.clear();
}
openDB.onerror = function (e) {
CachedXMLHttpRequest.log("can not open indexedDB database");
self.indexedDB = null;
self.processQueue();
}
};
openDB.onerror = onError;
openDB.onsuccess = function (e) {
self.db = e.target.result;
self.processQueue();
}
};

},
put: function (requestURL, meta, response, callback) {
if (CachedXMLHttpRequest.checkBlacklist(Module.CachedXMLHttpRequestBlacklist, requestURL))
return callback(new Error("requestURL was on the cache blacklist"));

var self = this;
if (!self.indexedDB)
return callback(new Error("indexedDB is not available"));
if (!self.db)
return self.queue.push({action: "put", arguments: arguments});
meta.version = self.version;
var putDB = self.db.transaction([self.store], "readwrite").objectStore(self.store).put({id: self.id(requestURL), meta: meta, response: response});
putDB.onerror = function (e) { callback(new Error("failed to put request into indexedDB cache")); }
putDB.onsuccess = function (e) { callback(null); }
putDB.onerror = function (e) { e.preventDefault(); callback(new Error("failed to put request into indexedDB cache")); };
putDB.onsuccess = function () { callback(null); };

},
get: function (requestURL, callback) {
if (CachedXMLHttpRequest.checkBlacklist(Module.CachedXMLHttpRequestBlacklist, requestURL))
return callback(new Error("requestURL was on the cache blacklist"));

var self = this;
if (!self.indexedDB)
return callback(new Error("indexedDB is not available"));
if (!self.db)
return self.queue.push({action: "get", arguments: arguments});
var getDB = self.db.transaction([self.store], "readonly").objectStore(self.store).get(self.id(requestURL));
getDB.onerror = function (e) { callback(new Error("failed to get request from indexedDB cache")); }
getDB.onsuccess = function (e) { callback(null, e.target.result); }

},
getDB.onerror = function (e) { e.preventDefault(); callback(new Error("failed to get request from indexedDB cache")); };
getDB.onsuccess = function (e) { callback(null, e.target.result); };
}
};

CachedXMLHttpRequest.cache.init();

CachedXMLHttpRequest.wrap = function (func) {
return function () {
var realXMLHttpRequest = XMLHttpRequest;
XMLHttpRequest = CachedXMLHttpRequest;
var realXMLHttpRequest = XMLHttpRequest, result;
window.XMLHttpRequest = CachedXMLHttpRequest;
try {
var result = func.apply(this, arguments);
result = func.apply(this, arguments);
} catch (e) {
XMLHttpRequest = realXMLHttpRequest;
window.XMLHttpRequest = realXMLHttpRequest;
throw e;
}
XMLHttpRequest = realXMLHttpRequest;
window.XMLHttpRequest = realXMLHttpRequest;
return result;
};
};

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 5 additions & 1 deletion Assets/Plugins/WebGLCachedXMLHttpRequest/readme.txt
Original file line number Diff line number Diff line change
@@ -2,7 +2,7 @@ CachedXMLHttpRequest implements automatic caching of WWW or WebRequest responses

The difference between caching asset bundles with LoadFromCacheOrDownload and CachedXMLHttpRequest is the following:

All the files previousely cached with LoadFromCacheOrDownload are loaded from the indexedDB into the memory file system on application startup. This might include cached files that will be used at some point in the future or might not be used at all, therefore wasting the main memory. LoadFromCacheOrDownload requires a version number provided by the application.
All the files previously cached with LoadFromCacheOrDownload are loaded from the indexedDB into the memory file system on application startup. This might include cached files that will be used at some point in the future or might not be used at all, therefore wasting the main memory. LoadFromCacheOrDownload requires a version number provided by the application.

When downloading asset bundles with WWW or WebRequest while CachedXMLHttpRequest is enabled, only the currently requested file will be copied from the indexedDB into the main memory (or downloaded), after the file content is transferred to the application, this memory will get garbage collected, therefore no main memory is wasted. CachedXMLHttpRequest file versioning is based on the Last-Modified and ETag headers, provided by the server.

@@ -14,11 +14,15 @@ CachedXMLHttpRequest can be configured using the following Module variables:
Set Module.CachedXMLHttpRequestDisable to true in order to fully disable indexedDB caching.
Set Module.CachedXMLHttpRequestSilent to true in order to disable cache logs in the console.
Set Module.CachedXMLHttpRequestLoader to true in order to enable caching of the initially loaded .js, .data and .mem files (applies to Unity 5.4 and above)
Set Module.CachedXMLHttpRequestBlacklist to an array of RegExp or string objects to disable caching for matching URLs (useful to prevent caching of API endpoints, etc)
Set Module.CachedXMLHttpRequestRevalidateBlacklist to an array of RegExp or string objects to disable re-validation for matching URLs (helpful if you use explicit versioning on your asset bundles)

Module variables can be initialized directly in the index.html in the following way (note that initialization is optional):

var Module = {
TOTAL_MEMORY: 268435456,
CachedXMLHttpRequestLoader: true,
CachedXMLHttpRequestCacheBlacklist: [/\.xml/, /\.php/, 'xhr_nocache'],
CachedXMLHttpRequestRevalidateBlacklist: [/\.unity3d/]
...
};
48 changes: 47 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Unity-WebGL-Utilities
Some helpful utilities for Unity WebGL games
Some helpful utilities for Unity WebGL games based on and inspired by the Unity team's blog posts and WebGL essentials asset packages.

## Installing
Grab the [latest release](https://github.com/kongregate/Unity-WebGL-Utilities/releases) and import the `unitypackage` into your project.

## WebGLMemoryStats

@@ -8,3 +11,46 @@ This is a simple behavior that you can add to a persistent game object. It will
![](http://kong.dreamhosters.com/grabs/Default_unity_-_mtx-unity_-_WebGL__Personal___OpenGL_4_1__1E96A487.png)

![](http://kong.dreamhosters.com/grabs/Play_webgl-test__a_free_online_game_on_Kongregate_1E96A97A.png)

## Updated CachedXMLHttpRequest

The original version of CachedXMLHttpRequest unfortunately has a few bugs. This package includes an updated drop-in replacement that resolves the following issues:

* An error dialog is displayed in Firefox private browsing mode
* When used with Safari and content in an iframe the plugin is non-functional
* Synchronous XHR requests are used to revalidate resources

The updated version also adds some functionality to give you finer control over the XHR cache:

* `Module.CachedXMLHttpRequestBlacklist` can be set to an array of `RegExp` or string objects to disable caching for matching URLs (useful to prevent caching of API endpoints, etc)
* `Module.CachedXMLHttpRequestRevalidateBlacklist` can be set to an array of `RegExp` or string objects to disable re-validation for matching URLs (helpful if you use explicit versioning on your asset bundles)

For example, the following configuration will never cache files containing `.xml`, `.php`, or `xhr_nocache`, and requests with `.unity3d` in them will not be re-validated when being loaded:

```js
var Module = {
TOTAL_MEMORY: 268435456,
CachedXMLHttpRequestCacheBlacklist: [/\.xml/, /\.php/, 'xhr_nocache'],
CachedXMLHttpRequestRevalidateBlacklist: [/\.unity3d/]
};
```

## CachedXHRExtensions

This package also provides extensions to the `CachedXMLHttpRequest` addon which allow you to clear and query for the existence of items in the cache. This can be useful if you pre-fetch your asset bundles on startup to avoid doing so multiple times. The classes used below are in the `Kongregate` namespace.

**Clear the cache:**
```csharp
CachedXHRExtensions.CleanCache();
```

**Query the cache:**
```csharp
IEnumerator CheckIfAssetExists() {
var query = new CacheEntryQuery("https://whatever.io/file.xml");
yield return query;
if (query.IsCached) {
Debug.Log("Asset exists in cache!");
}
}
```

0 comments on commit 9ddd85d

Please sign in to comment.