@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 +6 -5
- package/src/file-storage.js +288 -0
- package/src/gltf-storage.js +167 -193
- package/src/index.js +606 -428
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@preference-sl/pref-viewer",
|
|
3
|
-
"version": "2.
|
|
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.
|
|
38
|
-
"@babylonjs/loaders": "^8.
|
|
39
|
-
"@babylonjs/serializers": "^8.
|
|
40
|
-
"babylonjs-gltf2interface": "^8.
|
|
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
|
+
}
|