@preference-sl/pref-viewer 2.10.0-beta.9 → 2.11.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.10.0-beta.9",
3
+ "version": "2.11.0-beta.0",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -34,10 +34,11 @@
34
34
  "index.d.ts"
35
35
  ],
36
36
  "dependencies": {
37
- "@babylonjs/core": "^8.28.2",
38
- "@babylonjs/loaders": "^8.28.2",
39
- "@babylonjs/serializers": "^8.28.2",
40
- "babylonjs-gltf2interface": "^8.28.2"
37
+ "@babylonjs/core": "^8.31.3",
38
+ "@babylonjs/loaders": "^8.31.3",
39
+ "@babylonjs/serializers": "^8.31.3",
40
+ "babylonjs-gltf2interface": "^8.31.3",
41
+ "idb": "^8.0.3"
41
42
  },
42
43
  "devDependencies": {
43
44
  "esbuild": "^0.25.10",
@@ -0,0 +1,288 @@
1
+ /**
2
+ * FileStore class to manage file storage in IndexedDB using a promise-based wrapper around the IndexedDB API.
3
+ * @see {@link https://github.com/jakearchibald/idb|GitHub - jakearchibald/idb: IndexedDB, but with promises}
4
+ * @see {@link https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API|IndexedDB API - MDN}
5
+ * @see {@link https://www.npmjs.com/package/idb|idb - npm}
6
+ */
7
+
8
+ import { openDB } from "idb";
9
+
10
+ export class FileStorage {
11
+ #supported = "indexedDB" in window && openDB; // true if IndexedDB is available in the browser and the idb helper is loaded
12
+ #dbVersion = 1; // single DB version; cache is managed per-file
13
+ #db = undefined; // IndexedDB database handle
14
+
15
+ constructor(dbName = "FilesDB", osName = "FilesObjectStore") {
16
+ this.dbName = dbName; // database name
17
+ this.osName = osName; // object store name used for file cache
18
+ }
19
+
20
+ /**
21
+ * Create the object store if it does not exist.
22
+ * @param {IDBDatabase} db IndexedDB database instance
23
+ */
24
+ #createObjectStore = (db, osName) => {
25
+ if (!db.objectStoreNames.contains(osName)) {
26
+ db.createObjectStore(osName);
27
+ }
28
+ };
29
+
30
+ /**
31
+ * Open the IndexedDB database (creating it if needed) and ensure the object store for files exists.
32
+ * @returns {Promise<IDBDatabase|null>} Resolves with the opened database or null if IndexedDB is not supported or opening fails.
33
+ */
34
+ #openDB = async () => {
35
+ if (!this.#supported) {
36
+ return null;
37
+ }
38
+ const self = this;
39
+ try {
40
+ return await openDB(this.dbName, this.#dbVersion, {
41
+ upgrade(db) {
42
+ self.#createObjectStore(db, self.osName);
43
+ },
44
+ });
45
+ } catch (error) {
46
+ return null;
47
+ }
48
+ };
49
+
50
+ /**
51
+ * Download the file from the server (forced download).
52
+ * @param {String} uri File URI
53
+ * @returns {Promise<{blob: Blob, timeStamp: string}|undefined>} Resolves with:
54
+ * - { blob, timeStamp }: object with the downloaded Blob and the Last-Modified timestamp (ISO string) on success
55
+ * - undefined: if the download fails (non-200 status or network error)
56
+ */
57
+ #getServerFile = async (uri) => {
58
+ let file = undefined;
59
+ return new Promise((resolve) => {
60
+ const xhr = new XMLHttpRequest();
61
+ xhr.open("GET", uri, true);
62
+ xhr.responseType = "blob";
63
+ xhr.onload = () => {
64
+ if (xhr.status === 200) {
65
+ const blob = xhr.response;
66
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
67
+ file = { blob: blob, timeStamp: timeStamp };
68
+ resolve(file);
69
+ } else {
70
+ resolve(file);
71
+ }
72
+ };
73
+ xhr.onerror = () => {
74
+ resolve(file);
75
+ };
76
+ xhr.send();
77
+ });
78
+ };
79
+
80
+ /**
81
+ * Get the Last-Modified timestamp of the file on the server without downloading its content.
82
+ * @param {String} uri File URI
83
+ * @returns {Promise<string|null>} Resolves with:
84
+ * - ISO 8601 string from the Last-Modified header if available
85
+ * - null if the header is not present or on error
86
+ */
87
+ #getServerFileTimeStamp = async (uri) => {
88
+ let timeStamp = null;
89
+ return new Promise((resolve) => {
90
+ const xhr = new XMLHttpRequest();
91
+ xhr.open("HEAD", uri, true);
92
+ xhr.responseType = "blob";
93
+ xhr.onload = () => {
94
+ if (xhr.status === 200) {
95
+ timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
96
+ resolve(timeStamp);
97
+ } else {
98
+ resolve(timeStamp);
99
+ }
100
+ };
101
+ xhr.onerror = () => {
102
+ resolve(timeStamp);
103
+ };
104
+ xhr.send();
105
+ });
106
+ };
107
+
108
+ /**
109
+ * Get the size of the file on the server without downloading its content.
110
+ * @param {String} uri File URI
111
+ * @returns {Promise<number>} Promise that resolves with:
112
+ * - number: Content-Length in bytes when the HEAD request succeeds and the header is present
113
+ * - 0: when the header is missing or the request fails
114
+ */
115
+ #getServerFileSize = async (uri) => {
116
+ let size = 0;
117
+ return new Promise((resolve) => {
118
+ const xhr = new XMLHttpRequest();
119
+ xhr.open("HEAD", uri, true);
120
+ xhr.responseType = "blob";
121
+ xhr.onload = () => {
122
+ if (xhr.status === 200) {
123
+ size = parseInt(xhr.getResponseHeader("Content-Length"));
124
+ resolve(size);
125
+ } else {
126
+ resolve(size);
127
+ }
128
+ };
129
+ xhr.onerror = () => {
130
+ resolve(size);
131
+ };
132
+ xhr.send();
133
+ });
134
+ };
135
+
136
+ /**
137
+ * Stores a file record in IndexedDB under the provided key (uri).
138
+ * @param {Object|Blob} file The value to store (for example an object {blob, timeStamp} or a Blob).
139
+ * @param {String} uri Key to use for storage (the original URI).
140
+ * @returns {Promise<any|undefined>} Resolves with the value returned by the underlying idb store.put (usually the record key — string or number)
141
+ * - resolves to the put result on success
142
+ * - resolves to undefined if the database could not be opened
143
+ * - rejects if the underlying put operation throws
144
+ */
145
+ #putFile = async (file, uri) => {
146
+ if (this.#db === undefined) {
147
+ this.#db = await this.#openDB();
148
+ }
149
+ if (this.#db === null) {
150
+ return undefined;
151
+ }
152
+ const transation = this.#db.transaction(this.osName, "readwrite");
153
+ const store = transation.objectStore(this.osName);
154
+ return store.put(file, uri);
155
+ };
156
+
157
+ /**
158
+ * Retrieve a file record from IndexedDB.
159
+ * @param {String} uri File URI
160
+ * @returns {Promise<{blob: Blob, timeStamp: string}|undefined|false>} Resolves with:
161
+ * - {blob, timeStamp}: object with the Blob and ISO timestamp if the entry exists in IndexedDB
162
+ * - undefined: if there is no stored entry for that URI
163
+ * - false: if the database could not be opened (IndexedDB unsupported or open failure)
164
+ */
165
+ #getFile = async (uri) => {
166
+ if (this.#db === undefined) {
167
+ this.#db = await this.#openDB();
168
+ }
169
+ if (this.#db === null) {
170
+ return false;
171
+ }
172
+ const transation = this.#db.transaction(this.osName, "readonly");
173
+ const store = transation.objectStore(this.osName);
174
+ return store.get(uri);
175
+ };
176
+
177
+ /**
178
+ * Get a URL to the file: either an object URL for the cached Blob, or the original URI if the server copy is available and IndexedDB isn't supported.
179
+ * @param {String} uri File URI
180
+ * @returns {Promise<string|boolean>} Resolves with:
181
+ * - object URL string for the cached Blob
182
+ * - original URI string if IndexedDB unsupported but server file exists
183
+ * - false if the file is not available on the server
184
+ */
185
+ getURL = async (uri) => {
186
+ if (!this.#supported) {
187
+ return (await this.#getServerFileTimeStamp(uri)) ? uri : false;
188
+ }
189
+ const storedFile = await this.get(uri);
190
+ return storedFile ? URL.createObjectURL(storedFile.blob) : (await this.#getServerFileTimeStamp(uri)) ? uri : false;
191
+ };
192
+
193
+ /**
194
+ * Get the cached or server Last-Modified timestamp for a file.
195
+ * @param {String} uri File URI
196
+ * @returns {Promise<string|null>} Resolves with:
197
+ * - ISO 8601 string: Last-Modified timestamp from the cached record (when using IndexedDB) or from the server HEAD response
198
+ * - null: when no timestamp is available or on error
199
+ */
200
+ getTimeStamp = async (uri) => {
201
+ if (!this.#supported) {
202
+ const serverFileTimeStamp = await this.#getServerFileTimeStamp(uri);
203
+ return serverFileTimeStamp ? serverFileTimeStamp : null;
204
+ }
205
+ const storedFile = await this.get(uri);
206
+ return storedFile ? storedFile.timeStamp : null;
207
+ };
208
+
209
+ /**
210
+ * Get the cached or server size of a file in bytes.
211
+ * @param {String} uri File URI
212
+ * @returns {Promise<number|null>} Resolves with:
213
+ * - number: size in bytes when available (from cache or server)
214
+ * - null: when size is not available or on error
215
+ */
216
+ getSize = async (uri) => {
217
+ if (!this.#supported) {
218
+ const serverFileSize = await this.#getServerFileSize(uri);
219
+ return serverFileSize ? serverFileSize : null;
220
+ }
221
+ const storedFile = await this.get(uri);
222
+ return storedFile ? storedFile.blob.size : null;
223
+ };
224
+
225
+ /**
226
+ * Retrieve the Blob for a file from cache or server.
227
+ * @param {String} uri File URI
228
+ * @returns {Promise<Blob|false>} Resolves with:
229
+ * - Blob: the file blob when available (from cache or server)
230
+ * - false: when the file cannot be retrieved
231
+ */
232
+ getBlob = async (uri) => {
233
+ if (!this.#supported) {
234
+ const serverFile = await this.#getServerFile(uri);
235
+ return serverFile ? serverFile.blob : false;
236
+ }
237
+ const storedFile = await this.get(uri);
238
+ return storedFile ? storedFile.blob : false;
239
+ };
240
+
241
+ /**
242
+ * Get the file Blob from IndexedDB if stored; otherwise download and store it, then return the Blob.
243
+ * @param {String} uri File URI
244
+ * @returns {Promise<Blob|undefined|false>} Resolves with:
245
+ * - Blob of the stored file
246
+ * - undefined if the download fails
247
+ * - false if uri is falsy
248
+ * @description
249
+ * The method controls the cache version of the file. If the server copy has a different timestamp than the stored one,
250
+ * the file is re-downloaded from the server and stored again in IndexedDB.
251
+ * If the server file is not available at that time to check its age but is stored, the stored file is returned.
252
+ */
253
+ get = async (uri) => {
254
+ if (!uri) {
255
+ return false;
256
+ }
257
+ if (!this.#supported) {
258
+ return await this.#getServerFile(uri);
259
+ }
260
+ let storedFile = await this.#getFile(uri);
261
+ const serverFileTimeStamp = storedFile ? await this.#getServerFileTimeStamp(uri) : 0;
262
+ const storedFileTimeStamp = storedFile ? storedFile.timeStamp : 0;
263
+ if (!storedFile || (storedFile && serverFileTimeStamp !== null && serverFileTimeStamp !== storedFileTimeStamp)) {
264
+ const fileToStore = await this.#getServerFile(uri);
265
+ if (fileToStore && !!(await this.#putFile(fileToStore, uri))) {
266
+ storedFile = await this.#getFile(uri);
267
+ } else {
268
+ storedFile = fileToStore;
269
+ }
270
+ }
271
+ return storedFile;
272
+ };
273
+
274
+ /**
275
+ * Store the file in IndexedDB.
276
+ * @param {String} uri File URI
277
+ * @returns {Promise<boolean>} Resolves with:
278
+ * - true if the file was stored successfully
279
+ * - false if IndexedDB is not supported or storing failed
280
+ */
281
+ put = async (uri) => {
282
+ if (!this.#supported) {
283
+ return false;
284
+ }
285
+ const fileToStore = await this.#getServerFile(uri);
286
+ return fileToStore ? !!(await this.#putFile(fileToStore, uri)) : false;
287
+ };
288
+ }