babylon.database.ts 29 KB

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