123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641 |
- import { Tools } from "../Misc/tools.js";
- import { Logger } from "../Misc/logger.js";
- import { GetTGAHeader } from "../Misc/tga.js";
- import { Engine } from "../Engines/engine.js";
- import { WebRequest } from "../Misc/webRequest.js";
- // Sets the default offline provider to Babylon.js
- Engine.OfflineProviderFactory = (urlToScene, callbackManifestChecked, disableManifestCheck = false) => {
- return new Database(urlToScene, callbackManifestChecked, disableManifestCheck);
- };
- /**
- * Class used to enable access to IndexedDB
- * @see https://doc.babylonjs.com/features/featuresDeepDive/scene/optimizeCached
- */
- export class Database {
- /**
- * Gets a boolean indicating if scene must be saved in the database
- */
- get enableSceneOffline() {
- return this._enableSceneOffline;
- }
- /**
- * Gets a boolean indicating if textures must be saved in the database
- */
- get enableTexturesOffline() {
- return this._enableTexturesOffline;
- }
- /**
- * Creates a new Database
- * @param urlToScene defines the url to load the scene
- * @param callbackManifestChecked defines the callback to use when manifest is checked
- * @param disableManifestCheck defines a boolean indicating that we want to skip the manifest validation (it will be considered validated and up to date)
- */
- constructor(urlToScene, callbackManifestChecked, disableManifestCheck = false) {
- // Handling various flavors of prefixed version of IndexedDB
- this._idbFactory = (typeof indexedDB !== "undefined" ? indexedDB : undefined);
- this._currentSceneUrl = Database._ReturnFullUrlLocation(urlToScene);
- this._db = null;
- this._enableSceneOffline = false;
- this._enableTexturesOffline = false;
- this._manifestVersionFound = 0;
- this._mustUpdateRessources = false;
- this._hasReachedQuota = false;
- if (!Database.IDBStorageEnabled) {
- callbackManifestChecked(true);
- }
- else {
- if (disableManifestCheck) {
- this._enableSceneOffline = true;
- this._enableTexturesOffline = true;
- this._manifestVersionFound = 1;
- Tools.SetImmediate(() => {
- callbackManifestChecked(true);
- });
- }
- else {
- this._checkManifestFile(callbackManifestChecked);
- }
- }
- }
- _checkManifestFile(callbackManifestChecked) {
- const noManifestFile = () => {
- this._enableSceneOffline = false;
- this._enableTexturesOffline = false;
- callbackManifestChecked(false);
- };
- const createManifestURL = () => {
- try {
- // make sure we have a valid URL.
- if (typeof URL === "function" && this._currentSceneUrl.indexOf("http") === 0) {
- // we don't have the base url, so the URL string must have a protocol
- const url = new URL(this._currentSceneUrl);
- url.pathname += ".manifest";
- return url.toString();
- }
- }
- catch (e) {
- // defensive - if this fails for any reason, fall back to the older method
- }
- return `${this._currentSceneUrl}.manifest`;
- };
- let timeStampUsed = false;
- let manifestURL = createManifestURL();
- const xhr = new WebRequest();
- if (navigator.onLine) {
- // Adding a timestamp to by-pass browsers' cache
- timeStampUsed = true;
- manifestURL = manifestURL + (manifestURL.match(/\?/) == null ? "?" : "&") + Date.now();
- }
- xhr.open("GET", manifestURL);
- xhr.addEventListener("load", () => {
- if (xhr.status === 200 || Database._ValidateXHRData(xhr, 1)) {
- try {
- const manifestFile = JSON.parse(xhr.response);
- this._enableSceneOffline = manifestFile.enableSceneOffline;
- this._enableTexturesOffline = manifestFile.enableTexturesOffline && Database._IsUASupportingBlobStorage;
- if (manifestFile.version && !isNaN(parseInt(manifestFile.version))) {
- this._manifestVersionFound = manifestFile.version;
- }
- callbackManifestChecked(true);
- }
- catch (ex) {
- noManifestFile();
- }
- }
- else {
- noManifestFile();
- }
- }, false);
- xhr.addEventListener("error", () => {
- if (timeStampUsed) {
- timeStampUsed = false;
- // Let's retry without the timeStamp
- // It could fail when coupled with HTML5 Offline API
- const retryManifestURL = createManifestURL();
- xhr.open("GET", retryManifestURL);
- xhr.send();
- }
- else {
- noManifestFile();
- }
- }, false);
- try {
- xhr.send();
- }
- catch (ex) {
- Logger.Error("Error on XHR send request.");
- callbackManifestChecked(false);
- }
- }
- /**
- * Open the database and make it available
- * @param successCallback defines the callback to call on success
- * @param errorCallback defines the callback to call on error
- */
- open(successCallback, errorCallback) {
- const handleError = () => {
- this._isSupported = false;
- if (errorCallback) {
- errorCallback();
- }
- };
- if (!this._idbFactory || !(this._enableSceneOffline || this._enableTexturesOffline)) {
- // Your browser doesn't support IndexedDB
- this._isSupported = false;
- if (errorCallback) {
- errorCallback();
- }
- }
- else {
- // If the DB hasn't been opened or created yet
- if (!this._db) {
- this._hasReachedQuota = false;
- this._isSupported = true;
- const request = this._idbFactory.open("babylonjs", 1);
- // Could occur if user is blocking the quota for the DB and/or doesn't grant access to IndexedDB
- request.onerror = () => {
- handleError();
- };
- // executes when a version change transaction cannot complete due to other active transactions
- request.onblocked = () => {
- Logger.Error("IDB request blocked. Please reload the page.");
- handleError();
- };
- // DB has been opened successfully
- request.onsuccess = () => {
- this._db = request.result;
- successCallback();
- };
- // Initialization of the DB. Creating Scenes & Textures stores
- request.onupgradeneeded = (event) => {
- this._db = event.target.result;
- if (this._db) {
- try {
- this._db.createObjectStore("scenes", { keyPath: "sceneUrl" });
- this._db.createObjectStore("versions", { keyPath: "sceneUrl" });
- this._db.createObjectStore("textures", { keyPath: "textureUrl" });
- }
- catch (ex) {
- Logger.Error("Error while creating object stores. Exception: " + ex.message);
- handleError();
- }
- }
- };
- }
- // DB has already been created and opened
- else {
- if (successCallback) {
- successCallback();
- }
- }
- }
- }
- /**
- * Loads an image from the database
- * @param url defines the url to load from
- * @param image defines the target DOM image
- */
- loadImage(url, image) {
- const completeURL = Database._ReturnFullUrlLocation(url);
- const saveAndLoadImage = () => {
- if (!this._hasReachedQuota && this._db !== null) {
- // the texture is not yet in the DB, let's try to save it
- this._saveImageIntoDBAsync(completeURL, image);
- }
- // If the texture is not in the DB and we've reached the DB quota limit
- // let's load it directly from the web
- else {
- image.src = url;
- }
- };
- if (!this._mustUpdateRessources) {
- this._loadImageFromDBAsync(completeURL, image, saveAndLoadImage);
- }
- // First time we're download the images or update requested in the manifest file by a version change
- else {
- saveAndLoadImage();
- }
- }
- _loadImageFromDBAsync(url, image, notInDBCallback) {
- if (this._isSupported && this._db !== null) {
- let texture;
- const transaction = this._db.transaction(["textures"]);
- transaction.onabort = () => {
- image.src = url;
- };
- transaction.oncomplete = () => {
- let blobTextureURL;
- if (texture && typeof URL === "function") {
- blobTextureURL = URL.createObjectURL(texture.data);
- image.onerror = () => {
- Logger.Error("Error loading image from blob URL: " + blobTextureURL + " switching back to web url: " + url);
- image.src = url;
- };
- image.src = blobTextureURL;
- }
- else {
- notInDBCallback();
- }
- };
- const getRequest = transaction.objectStore("textures").get(url);
- getRequest.onsuccess = (event) => {
- texture = event.target.result;
- };
- getRequest.onerror = () => {
- Logger.Error("Error loading texture " + url + " from DB.");
- image.src = url;
- };
- }
- else {
- Logger.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
- image.src = url;
- }
- }
- _saveImageIntoDBAsync(url, image) {
- let blob;
- if (this._isSupported) {
- // In case of error (type not supported or quota exceeded), we're at least sending back XHR data to allow texture loading later on
- const generateBlobUrl = () => {
- let blobTextureURL;
- if (blob && typeof URL === "function") {
- try {
- blobTextureURL = URL.createObjectURL(blob);
- }
- catch (ex) {
- // Chrome is raising a type error if we're setting the oneTimeOnly parameter
- blobTextureURL = URL.createObjectURL(blob);
- }
- }
- if (blobTextureURL) {
- image.src = blobTextureURL;
- }
- };
- if (Database._IsUASupportingBlobStorage) {
- // Create XHR
- const xhr = new WebRequest();
- xhr.open("GET", url);
- xhr.responseType = "blob";
- xhr.addEventListener("load", () => {
- if (xhr.status === 200 && this._db) {
- // Blob as response
- blob = xhr.response;
- const transaction = this._db.transaction(["textures"], "readwrite");
- // the transaction could abort because of a QuotaExceededError error
- transaction.onabort = (event) => {
- try {
- //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
- const srcElement = event.target;
- const error = srcElement.error;
- if (error && error.name === "QuotaExceededError") {
- this._hasReachedQuota = true;
- }
- }
- catch (ex) { }
- generateBlobUrl();
- };
- transaction.oncomplete = () => {
- generateBlobUrl();
- };
- const newTexture = { textureUrl: url, data: blob };
- try {
- // Put the blob into the dabase
- const addRequest = transaction.objectStore("textures").put(newTexture);
- addRequest.onsuccess = () => { };
- addRequest.onerror = () => {
- generateBlobUrl();
- };
- }
- catch (ex) {
- // "DataCloneError" generated by Chrome when you try to inject blob into IndexedDB
- if (ex.code === 25) {
- Database._IsUASupportingBlobStorage = false;
- this._enableTexturesOffline = false;
- }
- image.src = url;
- }
- }
- else {
- image.src = url;
- }
- }, false);
- xhr.addEventListener("error", () => {
- Logger.Error("Error in XHR request in BABYLON.Database.");
- image.src = url;
- }, false);
- xhr.send();
- }
- else {
- image.src = url;
- }
- }
- else {
- Logger.Error("Error: IndexedDB not supported by your browser or Babylon.js database is not open.");
- image.src = url;
- }
- }
- _checkVersionFromDB(url, versionLoaded) {
- const updateVersion = () => {
- // the version is not yet in the DB or we need to update it
- this._saveVersionIntoDBAsync(url, versionLoaded);
- };
- this._loadVersionFromDBAsync(url, versionLoaded, updateVersion);
- }
- _loadVersionFromDBAsync(url, callback, updateInDBCallback) {
- if (this._isSupported && this._db) {
- let version;
- try {
- const transaction = this._db.transaction(["versions"]);
- transaction.oncomplete = () => {
- if (version) {
- // If the version in the JSON file is different from the version in DB
- if (this._manifestVersionFound !== version.data) {
- this._mustUpdateRessources = true;
- updateInDBCallback();
- }
- else {
- callback(version.data);
- }
- }
- // version was not found in DB
- else {
- this._mustUpdateRessources = true;
- updateInDBCallback();
- }
- };
- transaction.onabort = () => {
- callback(-1);
- };
- const getRequest = transaction.objectStore("versions").get(url);
- getRequest.onsuccess = (event) => {
- version = event.target.result;
- };
- getRequest.onerror = () => {
- Logger.Error("Error loading version for scene " + url + " from DB.");
- callback(-1);
- };
- }
- catch (ex) {
- Logger.Error("Error while accessing 'versions' object store (READ OP). Exception: " + ex.message);
- callback(-1);
- }
- }
- else {
- Logger.Error("Error: IndexedDB not supported by your browser or Babylon.js database is not open.");
- callback(-1);
- }
- }
- _saveVersionIntoDBAsync(url, callback) {
- if (this._isSupported && !this._hasReachedQuota && this._db) {
- try {
- // Open a transaction to the database
- const transaction = this._db.transaction(["versions"], "readwrite");
- // the transaction could abort because of a QuotaExceededError error
- transaction.onabort = (event) => {
- try {
- //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
- const error = event.target["error"];
- if (error && error.name === "QuotaExceededError") {
- this._hasReachedQuota = true;
- }
- }
- catch (ex) { }
- callback(-1);
- };
- transaction.oncomplete = () => {
- callback(this._manifestVersionFound);
- };
- const newVersion = { sceneUrl: url, data: this._manifestVersionFound };
- // Put the scene into the database
- const addRequest = transaction.objectStore("versions").put(newVersion);
- addRequest.onsuccess = () => { };
- addRequest.onerror = () => {
- Logger.Error("Error in DB add version request in BABYLON.Database.");
- };
- }
- catch (ex) {
- Logger.Error("Error while accessing 'versions' object store (WRITE OP). Exception: " + ex.message);
- callback(-1);
- }
- }
- else {
- callback(-1);
- }
- }
- /**
- * Loads a file from database
- * @param url defines the URL to load from
- * @param sceneLoaded defines a callback to call on success
- * @param progressCallBack defines a callback to call when progress changed
- * @param errorCallback defines a callback to call on error
- * @param useArrayBuffer defines a boolean to use array buffer instead of text string
- */
- loadFile(url, sceneLoaded, progressCallBack, errorCallback, useArrayBuffer) {
- const completeUrl = Database._ReturnFullUrlLocation(url);
- const saveAndLoadFile = () => {
- // the scene is not yet in the DB, let's try to save it
- this._saveFileAsync(completeUrl, sceneLoaded, progressCallBack, useArrayBuffer, errorCallback);
- };
- this._checkVersionFromDB(completeUrl, (version) => {
- if (version !== -1) {
- if (!this._mustUpdateRessources) {
- this._loadFileAsync(completeUrl, sceneLoaded, saveAndLoadFile);
- }
- else {
- this._saveFileAsync(completeUrl, sceneLoaded, progressCallBack, useArrayBuffer, errorCallback);
- }
- }
- else {
- if (errorCallback) {
- errorCallback();
- }
- }
- });
- }
- _loadFileAsync(url, callback, notInDBCallback) {
- if (this._isSupported && this._db) {
- let targetStore;
- if (url.indexOf(".babylon") !== -1) {
- targetStore = "scenes";
- }
- else {
- targetStore = "textures";
- }
- let file;
- const transaction = this._db.transaction([targetStore]);
- transaction.oncomplete = () => {
- if (file) {
- callback(file.data);
- }
- // file was not found in DB
- else {
- notInDBCallback();
- }
- };
- transaction.onabort = () => {
- notInDBCallback();
- };
- const getRequest = transaction.objectStore(targetStore).get(url);
- getRequest.onsuccess = (event) => {
- file = event.target.result;
- };
- getRequest.onerror = () => {
- Logger.Error("Error loading file " + url + " from DB.");
- notInDBCallback();
- };
- }
- else {
- Logger.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
- callback();
- }
- }
- _saveFileAsync(url, callback, progressCallback, useArrayBuffer, errorCallback) {
- if (this._isSupported) {
- let targetStore;
- if (url.indexOf(".babylon") !== -1) {
- targetStore = "scenes";
- }
- else {
- targetStore = "textures";
- }
- // Create XHR
- const xhr = new WebRequest();
- let fileData;
- xhr.open("GET", url + (url.match(/\?/) == null ? "?" : "&") + Date.now());
- if (useArrayBuffer) {
- xhr.responseType = "arraybuffer";
- }
- if (progressCallback) {
- xhr.onprogress = progressCallback;
- }
- xhr.addEventListener("load", () => {
- if (xhr.status === 200 || (xhr.status < 400 && Database._ValidateXHRData(xhr, !useArrayBuffer ? 1 : 6))) {
- // Blob as response
- fileData = !useArrayBuffer ? xhr.responseText : xhr.response;
- if (!this._hasReachedQuota && this._db) {
- // Open a transaction to the database
- const transaction = this._db.transaction([targetStore], "readwrite");
- // the transaction could abort because of a QuotaExceededError error
- transaction.onabort = (event) => {
- try {
- //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
- const error = event.target["error"];
- if (error && error.name === "QuotaExceededError") {
- this._hasReachedQuota = true;
- }
- }
- catch (ex) { }
- callback(fileData);
- };
- transaction.oncomplete = () => {
- callback(fileData);
- };
- let newFile;
- if (targetStore === "scenes") {
- newFile = { sceneUrl: url, data: fileData, version: this._manifestVersionFound };
- }
- else {
- newFile = { textureUrl: url, data: fileData };
- }
- try {
- // Put the scene into the database
- const addRequest = transaction.objectStore(targetStore).put(newFile);
- addRequest.onsuccess = () => { };
- addRequest.onerror = () => {
- Logger.Error("Error in DB add file request in BABYLON.Database.");
- };
- }
- catch (ex) {
- callback(fileData);
- }
- }
- else {
- callback(fileData);
- }
- }
- else {
- if (xhr.status >= 400 && errorCallback) {
- errorCallback(xhr);
- }
- else {
- callback();
- }
- }
- }, false);
- xhr.addEventListener("error", () => {
- Logger.Error("error on XHR request.");
- errorCallback && errorCallback();
- }, false);
- xhr.send();
- }
- else {
- Logger.Error("Error: IndexedDB not supported by your browser or Babylon.js database is not open.");
- errorCallback && errorCallback();
- }
- }
- /**
- * Validates if xhr data is correct
- * @param xhr defines the request to validate
- * @param dataType defines the expected data type
- * @returns true if data is correct
- */
- static _ValidateXHRData(xhr, dataType = 7) {
- // 1 for text (.babylon, manifest and shaders), 2 for TGA, 4 for DDS, 7 for all
- try {
- if (dataType & 1) {
- if (xhr.responseText && xhr.responseText.length > 0) {
- return true;
- }
- else if (dataType === 1) {
- return false;
- }
- }
- if (dataType & 2) {
- // Check header width and height since there is no "TGA" magic number
- const tgaHeader = GetTGAHeader(xhr.response);
- if (tgaHeader.width && tgaHeader.height && tgaHeader.width > 0 && tgaHeader.height > 0) {
- return true;
- }
- else if (dataType === 2) {
- return false;
- }
- }
- if (dataType & 4) {
- // Check for the "DDS" magic number
- const ddsHeader = new Uint8Array(xhr.response, 0, 3);
- if (ddsHeader[0] === 68 && ddsHeader[1] === 68 && ddsHeader[2] === 83) {
- return true;
- }
- else {
- return false;
- }
- }
- }
- catch (e) {
- // Global protection
- }
- return false;
- }
- }
- /** Gets a boolean indicating if the user agent supports blob storage (this value will be updated after creating the first Database object) */
- Database._IsUASupportingBlobStorage = true;
- /**
- * Gets a boolean indicating if Database storage is enabled (off by default)
- */
- Database.IDBStorageEnabled = false;
- Database._ParseURL = (url) => {
- const a = document.createElement("a");
- a.href = url;
- const urlWithoutHash = url.substring(0, url.lastIndexOf("#"));
- const fileName = url.substring(urlWithoutHash.lastIndexOf("/") + 1, url.length);
- const absLocation = url.substring(0, url.indexOf(fileName, 0));
- return absLocation;
- };
- Database._ReturnFullUrlLocation = (url) => {
- if (url.indexOf("http:/") === -1 && url.indexOf("https:/") === -1 && typeof window !== "undefined") {
- return Database._ParseURL(window.location.href) + url;
- }
- else {
- return url;
- }
- };
- //# sourceMappingURL=database.js.map
|