babylon.database.ts 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606
  1. module BABYLON {
  2. export class Database {
  3. private callbackManifestChecked: (check: boolean) => any;
  4. private currentSceneUrl: string;
  5. private db: Nullable<IDBDatabase>;
  6. private _enableSceneOffline: boolean;
  7. private _enableTexturesOffline: boolean;
  8. private manifestVersionFound: number;
  9. private mustUpdateRessources: boolean;
  10. private hasReachedQuota: boolean;
  11. private isSupported: boolean;
  12. // Handling various flavors of prefixed version of IndexedDB
  13. private idbFactory = <IDBFactory>(window.indexedDB || window.mozIndexedDB || window.webkitIndexedDB || window.msIndexedDB);
  14. static IsUASupportingBlobStorage = true;
  15. static IDBStorageEnabled = true;
  16. public get enableSceneOffline(): boolean {
  17. return this._enableSceneOffline;
  18. }
  19. public get enableTexturesOffline(): boolean {
  20. return this._enableTexturesOffline;
  21. }
  22. constructor(urlToScene: string, callbackManifestChecked: (checked: boolean) => any) {
  23. this.callbackManifestChecked = callbackManifestChecked;
  24. this.currentSceneUrl = Database.ReturnFullUrlLocation(urlToScene);
  25. this.db = null;
  26. this._enableSceneOffline = false;
  27. this._enableTexturesOffline = false;
  28. this.manifestVersionFound = 0;
  29. this.mustUpdateRessources = false;
  30. this.hasReachedQuota = false;
  31. if (!Database.IDBStorageEnabled) {
  32. this.callbackManifestChecked(true);
  33. } else {
  34. this.checkManifestFile();
  35. }
  36. }
  37. static parseURL = (url: string) => {
  38. var a = document.createElement('a');
  39. a.href = url;
  40. var urlWithoutHash = url.substring(0, url.lastIndexOf("#"));
  41. var fileName = url.substring(urlWithoutHash.lastIndexOf("/") + 1, url.length);
  42. var absLocation = url.substring(0, url.indexOf(fileName, 0));
  43. return absLocation;
  44. }
  45. static ReturnFullUrlLocation = (url: string): string => {
  46. if (url.indexOf("http:/") === -1 && url.indexOf("https:/") === -1) {
  47. return (Database.parseURL(window.location.href) + url);
  48. }
  49. else {
  50. return url;
  51. }
  52. }
  53. public checkManifestFile() {
  54. var noManifestFile = () => {
  55. this._enableSceneOffline = false;
  56. this._enableTexturesOffline = false;
  57. this.callbackManifestChecked(false);
  58. }
  59. var timeStampUsed = false;
  60. var manifestURL = this.currentSceneUrl + ".manifest";
  61. var xhr: XMLHttpRequest = new XMLHttpRequest();
  62. if (navigator.onLine) {
  63. // Adding a timestamp to by-pass browsers' cache
  64. timeStampUsed = true;
  65. manifestURL = manifestURL + (manifestURL.match(/\?/) == null ? "?" : "&") + (new Date()).getTime();
  66. }
  67. xhr.open("GET", manifestURL, true);
  68. xhr.addEventListener("load", () => {
  69. if (xhr.status === 200 || Tools.ValidateXHRData(xhr, 1)) {
  70. try {
  71. var manifestFile = JSON.parse(xhr.response);
  72. this._enableSceneOffline = manifestFile.enableSceneOffline;
  73. this._enableTexturesOffline = manifestFile.enableTexturesOffline;
  74. if (manifestFile.version && !isNaN(parseInt(manifestFile.version))) {
  75. this.manifestVersionFound = manifestFile.version;
  76. }
  77. if (this.callbackManifestChecked) {
  78. this.callbackManifestChecked(true);
  79. }
  80. }
  81. catch (ex) {
  82. noManifestFile();
  83. }
  84. }
  85. else {
  86. noManifestFile();
  87. }
  88. }, false);
  89. xhr.addEventListener("error", event => {
  90. if (timeStampUsed) {
  91. timeStampUsed = false;
  92. // Let's retry without the timeStamp
  93. // It could fail when coupled with HTML5 Offline API
  94. var retryManifestURL = this.currentSceneUrl + ".manifest";
  95. xhr.open("GET", retryManifestURL, true);
  96. xhr.send();
  97. }
  98. else {
  99. noManifestFile();
  100. }
  101. }, false);
  102. try {
  103. xhr.send();
  104. }
  105. catch (ex) {
  106. Tools.Error("Error on XHR send request.");
  107. this.callbackManifestChecked(false);
  108. }
  109. }
  110. public openAsync(successCallback: () => void, errorCallback: () => void) {
  111. let handleError = () => {
  112. this.isSupported = false;
  113. if (errorCallback) errorCallback();
  114. }
  115. if (!this.idbFactory || !(this._enableSceneOffline || this._enableTexturesOffline)) {
  116. // Your browser doesn't support IndexedDB
  117. this.isSupported = false;
  118. if (errorCallback) errorCallback();
  119. }
  120. else {
  121. // If the DB hasn't been opened or created yet
  122. if (!this.db) {
  123. this.hasReachedQuota = false;
  124. this.isSupported = true;
  125. var request: IDBOpenDBRequest = this.idbFactory.open("babylonjs", 1);
  126. // Could occur if user is blocking the quota for the DB and/or doesn't grant access to IndexedDB
  127. request.onerror = event => {
  128. handleError();
  129. };
  130. // executes when a version change transaction cannot complete due to other active transactions
  131. request.onblocked = event => {
  132. Tools.Error("IDB request blocked. Please reload the page.");
  133. handleError();
  134. };
  135. // DB has been opened successfully
  136. request.onsuccess = event => {
  137. this.db = request.result;
  138. successCallback();
  139. };
  140. // Initialization of the DB. Creating Scenes & Textures stores
  141. request.onupgradeneeded = (event: IDBVersionChangeEvent) => {
  142. this.db = (<any>(event.target)).result;
  143. if (this.db) {
  144. try {
  145. this.db.createObjectStore("scenes", { keyPath: "sceneUrl" });
  146. this.db.createObjectStore("versions", { keyPath: "sceneUrl" });
  147. this.db.createObjectStore("textures", { keyPath: "textureUrl" });
  148. }
  149. catch (ex) {
  150. Tools.Error("Error while creating object stores. Exception: " + ex.message);
  151. handleError();
  152. }
  153. }
  154. };
  155. }
  156. // DB has already been created and opened
  157. else {
  158. if (successCallback) successCallback();
  159. }
  160. }
  161. }
  162. public loadImageFromDB(url: string, image: HTMLImageElement) {
  163. var completeURL = Database.ReturnFullUrlLocation(url);
  164. var saveAndLoadImage = () => {
  165. if (!this.hasReachedQuota && this.db !== null) {
  166. // the texture is not yet in the DB, let's try to save it
  167. this._saveImageIntoDBAsync(completeURL, image);
  168. }
  169. // If the texture is not in the DB and we've reached the DB quota limit
  170. // let's load it directly from the web
  171. else {
  172. image.src = url;
  173. }
  174. };
  175. if (!this.mustUpdateRessources) {
  176. this._loadImageFromDBAsync(completeURL, image, saveAndLoadImage);
  177. }
  178. // First time we're download the images or update requested in the manifest file by a version change
  179. else {
  180. saveAndLoadImage();
  181. }
  182. }
  183. private _loadImageFromDBAsync(url: string, image: HTMLImageElement, notInDBCallback: () => any) {
  184. if (this.isSupported && this.db !== null) {
  185. var texture: any;
  186. var transaction: IDBTransaction = this.db.transaction(["textures"]);
  187. transaction.onabort = event => {
  188. image.src = url;
  189. };
  190. transaction.oncomplete = event => {
  191. var blobTextureURL: string;
  192. if (texture) {
  193. var URL = window.URL || window.webkitURL;
  194. blobTextureURL = URL.createObjectURL(texture.data, { oneTimeOnly: true });
  195. image.onerror = () => {
  196. Tools.Error("Error loading image from blob URL: " + blobTextureURL + " switching back to web url: " + url);
  197. image.src = url;
  198. };
  199. image.src = blobTextureURL;
  200. }
  201. else {
  202. notInDBCallback();
  203. }
  204. };
  205. var getRequest: IDBRequest = transaction.objectStore("textures").get(url);
  206. getRequest.onsuccess = event => {
  207. texture = (<any>(event.target)).result;
  208. };
  209. getRequest.onerror = event => {
  210. Tools.Error("Error loading texture " + url + " from DB.");
  211. image.src = url;
  212. };
  213. }
  214. else {
  215. Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  216. image.src = url;
  217. }
  218. }
  219. private _saveImageIntoDBAsync(url: string, image: HTMLImageElement) {
  220. if (this.isSupported) {
  221. // In case of error (type not supported or quota exceeded), we're at least sending back XHR data to allow texture loading later on
  222. var generateBlobUrl = () => {
  223. var blobTextureURL;
  224. if (blob) {
  225. var URL = window.URL || window.webkitURL;
  226. try {
  227. blobTextureURL = URL.createObjectURL(blob, { oneTimeOnly: true });
  228. }
  229. // Chrome is raising a type error if we're setting the oneTimeOnly parameter
  230. catch (ex) {
  231. blobTextureURL = URL.createObjectURL(blob);
  232. }
  233. }
  234. if (blobTextureURL) {
  235. image.src = blobTextureURL;
  236. }
  237. };
  238. if (Database.IsUASupportingBlobStorage) { // Create XHR
  239. var xhr = new XMLHttpRequest(),
  240. blob: Blob;
  241. xhr.open("GET", url, true);
  242. xhr.responseType = "blob";
  243. xhr.addEventListener("load", () => {
  244. if (xhr.status === 200 && this.db) {
  245. // Blob as response (XHR2)
  246. blob = xhr.response;
  247. var transaction = this.db.transaction(["textures"], "readwrite");
  248. // the transaction could abort because of a QuotaExceededError error
  249. transaction.onabort = (event) => {
  250. try {
  251. //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
  252. let srcElement = <any>(event.srcElement || event.target);
  253. var error = srcElement.error;
  254. if (error && error.name === "QuotaExceededError") {
  255. this.hasReachedQuota = true;
  256. }
  257. }
  258. catch (ex) { }
  259. generateBlobUrl();
  260. };
  261. transaction.oncomplete = event => {
  262. generateBlobUrl();
  263. };
  264. var newTexture = { textureUrl: url, data: blob };
  265. try {
  266. // Put the blob into the dabase
  267. var addRequest = transaction.objectStore("textures").put(newTexture);
  268. addRequest.onsuccess = event => {
  269. };
  270. addRequest.onerror = event => {
  271. generateBlobUrl();
  272. };
  273. }
  274. catch (ex) {
  275. // "DataCloneError" generated by Chrome when you try to inject blob into IndexedDB
  276. if (ex.code === 25) {
  277. Database.IsUASupportingBlobStorage = false;
  278. }
  279. image.src = url;
  280. }
  281. }
  282. else {
  283. image.src = url;
  284. }
  285. }, false);
  286. xhr.addEventListener("error", event => {
  287. Tools.Error("Error in XHR request in BABYLON.Database.");
  288. image.src = url;
  289. }, false);
  290. xhr.send();
  291. }
  292. else {
  293. image.src = url;
  294. }
  295. }
  296. else {
  297. Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  298. image.src = url;
  299. }
  300. }
  301. private _checkVersionFromDB(url: string, versionLoaded: (version: number) => void) {
  302. var updateVersion = () => {
  303. // the version is not yet in the DB or we need to update it
  304. this._saveVersionIntoDBAsync(url, versionLoaded);
  305. };
  306. this._loadVersionFromDBAsync(url, versionLoaded, updateVersion);
  307. }
  308. private _loadVersionFromDBAsync(url: string, callback: (version: number) => void, updateInDBCallback: () => void) {
  309. if (this.isSupported && this.db) {
  310. var version: any;
  311. try {
  312. var transaction = this.db.transaction(["versions"]);
  313. transaction.oncomplete = event => {
  314. if (version) {
  315. // If the version in the JSON file is > than the version in DB
  316. if (this.manifestVersionFound > version.data) {
  317. this.mustUpdateRessources = true;
  318. updateInDBCallback();
  319. }
  320. else {
  321. callback(version.data);
  322. }
  323. }
  324. // version was not found in DB
  325. else {
  326. this.mustUpdateRessources = true;
  327. updateInDBCallback();
  328. }
  329. };
  330. transaction.onabort = event => {
  331. callback(-1);
  332. };
  333. var getRequest = transaction.objectStore("versions").get(url);
  334. getRequest.onsuccess = event => {
  335. version = (<any>(event.target)).result;
  336. };
  337. getRequest.onerror = event => {
  338. Tools.Error("Error loading version for scene " + url + " from DB.");
  339. callback(-1);
  340. };
  341. }
  342. catch (ex) {
  343. Tools.Error("Error while accessing 'versions' object store (READ OP). Exception: " + ex.message);
  344. callback(-1);
  345. }
  346. }
  347. else {
  348. Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  349. callback(-1);
  350. }
  351. }
  352. private _saveVersionIntoDBAsync(url: string, callback: (version: number) => void) {
  353. if (this.isSupported && !this.hasReachedQuota && this.db) {
  354. try {
  355. // Open a transaction to the database
  356. var transaction = this.db.transaction(["versions"], "readwrite");
  357. // the transaction could abort because of a QuotaExceededError error
  358. transaction.onabort = event => {
  359. try {//backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
  360. var error = (<any>event.srcElement)['error'];
  361. if (error && error.name === "QuotaExceededError") {
  362. this.hasReachedQuota = true;
  363. }
  364. }
  365. catch (ex) { }
  366. callback(-1);
  367. };
  368. transaction.oncomplete = event => {
  369. callback(this.manifestVersionFound);
  370. };
  371. var newVersion = { sceneUrl: url, data: this.manifestVersionFound };
  372. // Put the scene into the database
  373. var addRequest = transaction.objectStore("versions").put(newVersion);
  374. addRequest.onsuccess = event => {
  375. };
  376. addRequest.onerror = event => {
  377. Tools.Error("Error in DB add version request in BABYLON.Database.");
  378. };
  379. }
  380. catch (ex) {
  381. Tools.Error("Error while accessing 'versions' object store (WRITE OP). Exception: " + ex.message);
  382. callback(-1);
  383. }
  384. }
  385. else {
  386. callback(-1);
  387. }
  388. }
  389. public loadFileFromDB(url: string, sceneLoaded: (data: any) => void, progressCallBack?: (data: any) => void, errorCallback?: () => void, useArrayBuffer?: boolean) {
  390. var completeUrl = Database.ReturnFullUrlLocation(url);
  391. var saveAndLoadFile = () => {
  392. // the scene is not yet in the DB, let's try to save it
  393. this._saveFileIntoDBAsync(completeUrl, sceneLoaded, progressCallBack);
  394. };
  395. this._checkVersionFromDB(completeUrl, version => {
  396. if (version !== -1) {
  397. if (!this.mustUpdateRessources) {
  398. this._loadFileFromDBAsync(completeUrl, sceneLoaded, saveAndLoadFile, useArrayBuffer);
  399. }
  400. else {
  401. this._saveFileIntoDBAsync(completeUrl, sceneLoaded, progressCallBack, useArrayBuffer);
  402. }
  403. }
  404. else {
  405. if (errorCallback) {
  406. errorCallback();
  407. }
  408. }
  409. });
  410. }
  411. private _loadFileFromDBAsync(url: string, callback: (data?: any) => void, notInDBCallback: () => void, useArrayBuffer?: boolean) {
  412. if (this.isSupported && this.db) {
  413. var targetStore: string;
  414. if (url.indexOf(".babylon") !== -1) {
  415. targetStore = "scenes";
  416. }
  417. else {
  418. targetStore = "textures";
  419. }
  420. var file: any;
  421. var transaction = this.db.transaction([targetStore]);
  422. transaction.oncomplete = event => {
  423. if (file) {
  424. callback(file.data);
  425. }
  426. // file was not found in DB
  427. else {
  428. notInDBCallback();
  429. }
  430. };
  431. transaction.onabort = event => {
  432. notInDBCallback();
  433. };
  434. var getRequest = transaction.objectStore(targetStore).get(url);
  435. getRequest.onsuccess = event => {
  436. file = (<any>(event.target)).result;
  437. };
  438. getRequest.onerror = event => {
  439. Tools.Error("Error loading file " + url + " from DB.");
  440. notInDBCallback();
  441. };
  442. }
  443. else {
  444. Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  445. callback();
  446. }
  447. }
  448. private _saveFileIntoDBAsync(url: string, callback: (data?: any) => void, progressCallback?: (this: XMLHttpRequestEventTarget, ev: ProgressEvent) => any, useArrayBuffer?: boolean) {
  449. if (this.isSupported) {
  450. var targetStore: string;
  451. if (url.indexOf(".babylon") !== -1) {
  452. targetStore = "scenes";
  453. }
  454. else {
  455. targetStore = "textures";
  456. }
  457. // Create XHR
  458. var xhr = new XMLHttpRequest();
  459. var fileData: any;
  460. xhr.open("GET", url, true);
  461. if (useArrayBuffer) {
  462. xhr.responseType = "arraybuffer";
  463. }
  464. if (progressCallback) {
  465. xhr.onprogress = progressCallback;
  466. }
  467. xhr.addEventListener("load", () => {
  468. if (xhr.status === 200 || Tools.ValidateXHRData(xhr, !useArrayBuffer ? 1 : 6)) {
  469. // Blob as response (XHR2)
  470. //fileData = xhr.responseText;
  471. fileData = !useArrayBuffer ? xhr.responseText : xhr.response;
  472. if (!this.hasReachedQuota && this.db) {
  473. // Open a transaction to the database
  474. var transaction = this.db.transaction([targetStore], "readwrite");
  475. // the transaction could abort because of a QuotaExceededError error
  476. transaction.onabort = (event) => {
  477. try {
  478. //backwards compatibility with ts 1.0, srcElement doesn't have an "error" according to ts 1.3
  479. var error = (<any>event.srcElement)['error'];
  480. if (error && error.name === "QuotaExceededError") {
  481. this.hasReachedQuota = true;
  482. }
  483. }
  484. catch (ex) { }
  485. callback(fileData);
  486. };
  487. transaction.oncomplete = event => {
  488. callback(fileData);
  489. };
  490. var newFile;
  491. if (targetStore === "scenes") {
  492. newFile = { sceneUrl: url, data: fileData, version: this.manifestVersionFound };
  493. }
  494. else {
  495. newFile = { textureUrl: url, data: fileData };
  496. }
  497. try {
  498. // Put the scene into the database
  499. var addRequest = transaction.objectStore(targetStore).put(newFile);
  500. addRequest.onsuccess = event => {
  501. };
  502. addRequest.onerror = event => {
  503. Tools.Error("Error in DB add file request in BABYLON.Database.");
  504. };
  505. }
  506. catch (ex) {
  507. callback(fileData);
  508. }
  509. }
  510. else {
  511. callback(fileData);
  512. }
  513. }
  514. else {
  515. callback();
  516. }
  517. }, false);
  518. xhr.addEventListener("error", event => {
  519. Tools.Error("error on XHR request.");
  520. callback();
  521. }, false);
  522. xhr.send();
  523. }
  524. else {
  525. Tools.Error("Error: IndexedDB not supported by your browser or BabylonJS Database is not open.");
  526. callback();
  527. }
  528. }
  529. }
  530. }