module BABYLON { export class Database { private callbackManifestChecked: (boolean) => any; private currentSceneUrl: string; private db: IDBDatabase; private enableSceneOffline: boolean; private enableTexturesOffline: boolean; private manifestVersionFound: number; private mustUpdateRessources: boolean; private hasReachedQuota: boolean; private isSupported: boolean; // Handling various flavors of prefixed version of IndexedDB private idbFactory = (window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB); static isUASupportingBlobStorage: boolean = true; constructor(urlToScene: string, callbackManifestChecked: (boolean) => any) { this.callbackManifestChecked = callbackManifestChecked; this.currentSceneUrl = BABYLON.Database.ReturnFullUrlLocation(urlToScene); this.db = null; this.enableSceneOffline = false; this.enableTexturesOffline = false; this.manifestVersionFound = 0; this.mustUpdateRessources = false; this.hasReachedQuota = false; this.checkManifestFile(); } static parseURL = (url: string) => { var a = document.createElement('a'); a.href = url; var urlWithoutHash = url.substring(0, url.lastIndexOf("#")); var fileName = url.substring(urlWithoutHash.lastIndexOf("/") + 1, url.length); var absLocation = url.substring(0, url.indexOf(fileName, 0)); return absLocation; } static ReturnFullUrlLocation = (url: string): string => { if (url.indexOf("http:/") === -1) { return (BABYLON.Database.parseURL(window.location.href) + url); } else { return url; } } public checkManifestFile() { function noManifestFile() { BABYLON.Tools.Log("Valid manifest file not found. Scene & textures will be loaded directly from the web server."); that.enableSceneOffline = false; that.enableTexturesOffline = false; that.callbackManifestChecked(false); } var that = this; var manifestURL = this.currentSceneUrl + ".manifest"; var xhr: XMLHttpRequest = new XMLHttpRequest(); var manifestURLTimeStamped = manifestURL + (manifestURL.match(/\?/) == null ? "?" : "&") + (new Date()).getTime(); xhr.open("GET", manifestURLTimeStamped, true); xhr.addEventListener("load", () => { if (xhr.status === 200 || BABYLON.Tools.ValidateXHRData(xhr, 1)) { try { var manifestFile = JSON.parse(xhr.response); this.enableSceneOffline = manifestFile.enableSceneOffline; this.enableTexturesOffline = manifestFile.enableTexturesOffline; if (manifestFile.version && !isNaN(parseInt(manifestFile.version))) { this.manifestVersionFound = manifestFile.version; } if (this.callbackManifestChecked) { this.callbackManifestChecked(true); } } catch (ex) { noManifestFile(); } } else { noManifestFile(); } }, false); xhr.addEventListener("error", event => { noManifestFile(); }, false); try { xhr.send(); } catch (ex) { BABYLON.Tools.Error("Error on XHR send request."); that.callbackManifestChecked(false); } } public openAsync(successCallback, errorCallback) { function handleError() { that.isSupported = false; if (errorCallback) errorCallback(); } var that = this; 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; var request: IDBOpenDBRequest = 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 = event => { handleError(); }; // executes when a version change transaction cannot complete due to other active transactions request.onblocked = event => { BABYLON.Tools.Error("IDB request blocked. Please reload the page."); handleError(); }; // DB has been opened successfully request.onsuccess = event => { this.db = request.result; successCallback(); }; // Initialization of the DB. Creating Scenes & Textures stores request.onupgradeneeded = (event: IDBVersionChangeEvent) => { this.db = ((event.target)).result; try { var scenesStore = this.db.createObjectStore("scenes", { keyPath: "sceneUrl" }); var versionsStore = this.db.createObjectStore("versions", { keyPath: "sceneUrl" }); var texturesStore = this.db.createObjectStore("textures", { keyPath: "textureUrl" }); } catch (ex) { BABYLON.Tools.Error("Error while creating object stores. Exception: " + ex.message); handleError(); } }; } // DB has already been created and opened else { if (successCallback) successCallback(); } } } public loadImageFromDB(url: string, image: HTMLImageElement) { var completeURL = BABYLON.Database.ReturnFullUrlLocation(url); var 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(); } } private _loadImageFromDBAsync(url: string, image: HTMLImageElement, notInDBCallback: () => any) { if (this.isSupported && this.db !== null) { var texture; var transaction: IDBTransaction = this.db.transaction(["textures"]); transaction.onabort = event => { image.src = url; }; transaction.oncomplete = event => { var blobTextureURL; if (texture) { var URL = window.URL || window.webkitURL; blobTextureURL = URL.createObjectURL(texture.data, { oneTimeOnly: true }); image.onerror = () => { BABYLON.Tools.Error("Error loading image from blob URL: " + blobTextureURL + " switching back to web url: " + url); image.src = url; }; image.src = blobTextureURL; } else { notInDBCallback(); } }; var getRequest: IDBRequest = transaction.objectStore("textures").get(url); getRequest.onsuccess = event => { texture = ((event.target)).result; }; getRequest.onerror = event => { BABYLON.Tools.Error("Error loading texture " + url + " from DB."); image.src = url; }; } else { BABYLON.Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open."); image.src = url; } } private _saveImageIntoDBAsync(url: string, image: HTMLImageElement) { 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 var generateBlobUrl = () => { var blobTextureURL; if (blob) { var URL = window.URL || window.webkitURL; try { blobTextureURL = URL.createObjectURL(blob, { oneTimeOnly: true }); } // Chrome is raising a type error if we're setting the oneTimeOnly parameter catch (ex) { blobTextureURL = URL.createObjectURL(blob); } } image.src = blobTextureURL; }; if (BABYLON.Database.isUASupportingBlobStorage) { // Create XHR var xhr = new XMLHttpRequest(), blob: Blob; xhr.open("GET", url, true); xhr.responseType = "blob"; xhr.addEventListener("load", () => { if (xhr.status === 200) { // Blob as response (XHR2) blob = xhr.response; var transaction = this.db.transaction(["textures"], "readwrite"); // the transaction could abort because of a QuotaExceededError error transaction.onabort = function (event) { try { //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3 if (event.srcElement['error'] && event.srcElement['error'].name === "QuotaExceededError") { this.hasReachedQuota = true; } } catch (ex) { } generateBlobUrl(); }; transaction.oncomplete = event => { generateBlobUrl(); }; var newTexture = { textureUrl: url, data: blob }; try { // Put the blob into the dabase var addRequest = transaction.objectStore("textures").put(newTexture); addRequest.onsuccess = event => { }; addRequest.onerror = event => { generateBlobUrl(); }; } catch (ex) { // "DataCloneError" generated by Chrome when you try to inject blob into IndexedDB if (ex.code === 25) { BABYLON.Database.isUASupportingBlobStorage = false; } image.src = url; } } else { image.src = url; } }, false); xhr.addEventListener("error", event => { BABYLON.Tools.Error("Error in XHR request in BABYLON.Database."); image.src = url; }, false); xhr.send(); } else { image.src = url; } } else { BABYLON.Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open."); image.src = url; } } private _checkVersionFromDB(url: string, versionLoaded) { var updateVersion = event => { // the version is not yet in the DB or we need to update it this._saveVersionIntoDBAsync(url, versionLoaded); }; this._loadVersionFromDBAsync(url, versionLoaded, updateVersion); } private _loadVersionFromDBAsync(url: string, callback, updateInDBCallback) { if (this.isSupported) { var version; try { var transaction = this.db.transaction(["versions"]); transaction.oncomplete = event => { if (version) { // If the version in the JSON file is > than 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 = event => { callback(-1); }; var getRequest = transaction.objectStore("versions").get(url); getRequest.onsuccess = event => { version = ((event.target)).result; }; getRequest.onerror = event => { BABYLON.Tools.Error("Error loading version for scene " + url + " from DB."); callback(-1); }; } catch (ex) { BABYLON.Tools.Error("Error while accessing 'versions' object store (READ OP). Exception: " + ex.message); callback(-1); } } else { BABYLON.Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open."); callback(-1); } } private _saveVersionIntoDBAsync(url: string, callback) { if (this.isSupported && !this.hasReachedQuota) { try { // Open a transaction to the database var 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 if (event.srcElement['error'] && event.srcElement['error'].name === "QuotaExceededError") { this.hasReachedQuota = true; } } catch (ex) { } callback(-1); }; transaction.oncomplete = event => { callback(this.manifestVersionFound); }; var newVersion = { sceneUrl: url, data: this.manifestVersionFound }; // Put the scene into the database var addRequest = transaction.objectStore("versions").put(newVersion); addRequest.onsuccess = event => { }; addRequest.onerror = event => { BABYLON.Tools.Error("Error in DB add version request in BABYLON.Database."); }; } catch (ex) { BABYLON.Tools.Error("Error while accessing 'versions' object store (WRITE OP). Exception: " + ex.message); callback(-1); } } else { callback(-1); } } private loadFileFromDB(url: string, sceneLoaded, progressCallBack, errorCallback, useArrayBuffer?: boolean) { var completeUrl = BABYLON.Database.ReturnFullUrlLocation(url); var saveAndLoadFile = event => { // the scene is not yet in the DB, let's try to save it this._saveFileIntoDBAsync(completeUrl, sceneLoaded, progressCallBack); }; this._checkVersionFromDB(completeUrl, version => { if (version !== -1) { if (!this.mustUpdateRessources) { this._loadFileFromDBAsync(completeUrl, sceneLoaded, saveAndLoadFile, useArrayBuffer); } else { this._saveFileIntoDBAsync(completeUrl, sceneLoaded, progressCallBack, useArrayBuffer); } } else { errorCallback(); } }); } private _loadFileFromDBAsync(url, callback, notInDBCallback, useArrayBuffer?: boolean) { if (this.isSupported) { var targetStore: string; if (url.indexOf(".babylon") !== -1) { targetStore = "scenes"; } else { targetStore = "textures"; } var file; var transaction = this.db.transaction([targetStore]); transaction.oncomplete = event => { if (file) { callback(file.data); } // file was not found in DB else { notInDBCallback(); } }; transaction.onabort = event => { notInDBCallback(); }; var getRequest = transaction.objectStore(targetStore).get(url); getRequest.onsuccess = event => { file = ((event.target)).result; }; getRequest.onerror = event => { BABYLON.Tools.Error("Error loading file " + url + " from DB."); notInDBCallback(); }; } else { BABYLON.Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open."); callback(); } } private _saveFileIntoDBAsync(url: string, callback, progressCallback, useArrayBuffer?: boolean) { if (this.isSupported) { var targetStore: string; if (url.indexOf(".babylon") !== -1) { targetStore = "scenes"; } else { targetStore = "textures"; } // Create XHR var xhr = new XMLHttpRequest(), fileData; xhr.open("GET", url, true); if (useArrayBuffer) { xhr.responseType = "arraybuffer"; } xhr.onprogress = progressCallback; xhr.addEventListener("load", () => { if (xhr.status === 200 || BABYLON.Tools.ValidateXHRData(xhr, !useArrayBuffer ? 1 : 6)) { // Blob as response (XHR2) //fileData = xhr.responseText; fileData = !useArrayBuffer ? xhr.responseText : xhr.response if (!this.hasReachedQuota) { // Open a transaction to the database var transaction = this.db.transaction([targetStore], "readwrite"); // the transaction could abort because of a QuotaExceededError error transaction.onabort = function (event) { try { //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3 if (event.srcElement['error'] && event.srcElement['error'].name === "QuotaExceededError") { this.hasReachedQuota = true; } } catch (ex) { } callback(fileData); }; transaction.oncomplete = event => { callback(fileData); }; var 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 var addRequest = transaction.objectStore(targetStore).put(newFile); addRequest.onsuccess = event => { }; addRequest.onerror = event => { BABYLON.Tools.Error("Error in DB add file request in BABYLON.Database."); }; } catch (ex) { callback(fileData); } } else { callback(fileData); } } else { callback(); } }, false); xhr.addEventListener("error", event => { BABYLON.Tools.Error("error on XHR request."); callback(); }, false); xhr.send(); } else { BABYLON.Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open."); callback(); } } } }