@preference-sl/pref-viewer 2.10.0-beta.16 → 2.10.0-beta.17
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 +1 -1
- package/src/gltf-storage.js +147 -74
- package/src/index.js +30 -20
package/package.json
CHANGED
package/src/gltf-storage.js
CHANGED
|
@@ -1,137 +1,208 @@
|
|
|
1
|
-
//
|
|
2
|
-
export async function initDb(dbName, storeName) {
|
|
3
|
-
return new Promise((resolve, reject) => {
|
|
4
|
-
const request = indexedDB.open(dbName, 1);
|
|
1
|
+
// wwwroot/js/gltf-storage.js
|
|
5
2
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
};
|
|
3
|
+
// Public, inspectable namespace
|
|
4
|
+
const PC = (globalThis.PrefConfigurator ??= {});
|
|
5
|
+
PC.version = PC.version ?? "1.0.1"; // bump if your schema changes!!
|
|
6
|
+
PC.db = PC.db ?? null;
|
|
11
7
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
8
|
+
function _getDbOrThrow() {
|
|
9
|
+
const db = PC.db;
|
|
10
|
+
if (!db) throw new Error("Database not initialized. Call initDb(...) first.");
|
|
11
|
+
return db;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// --- internal helpers -------------------------------------------------------
|
|
15
|
+
function _openEnsuringStore(dbName, storeName) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const open = indexedDB.open(dbName, 5);
|
|
18
|
+
|
|
19
|
+
open.onblocked = () => reject(new Error("Open blocked by another tab or worker"));
|
|
20
|
+
open.onerror = () => reject(open.error);
|
|
21
|
+
open.onsuccess = (e) => {
|
|
22
|
+
resolve(e.target.result);
|
|
23
|
+
}
|
|
24
|
+
open.onupgradeneeded = (ev) => {
|
|
25
|
+
const upgradeDb = ev.target.result;
|
|
26
|
+
const store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
|
|
27
|
+
store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
|
|
19
28
|
};
|
|
20
29
|
});
|
|
21
30
|
}
|
|
22
31
|
|
|
32
|
+
// --- public API -------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
// Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
|
|
35
|
+
export async function initDb(dbName, storeName) {
|
|
36
|
+
const db = await _openEnsuringStore(dbName, storeName);
|
|
37
|
+
// Close any previous handle to avoid versionchange blocking
|
|
38
|
+
try { PC.db?.close?.(); } catch { }
|
|
39
|
+
PC.db = db;
|
|
40
|
+
|
|
41
|
+
// If another tab upgrades, close gracefully so next call can re-init
|
|
42
|
+
db.onversionchange = () => {
|
|
43
|
+
try { db.close(); } catch { }
|
|
44
|
+
if (PC.db === db) PC.db = null;
|
|
45
|
+
console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
23
49
|
// Guardar modelo
|
|
24
50
|
export async function saveModel(modelDataStr, storeName) {
|
|
25
51
|
return new Promise((resolve, reject) => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
52
|
+
let db;
|
|
53
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
54
|
+
|
|
55
|
+
let modelData;
|
|
56
|
+
try { modelData = JSON.parse(modelDataStr); }
|
|
57
|
+
catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
|
|
31
58
|
|
|
32
59
|
const dataToStore = {
|
|
33
60
|
...modelData,
|
|
34
61
|
data: modelData.data,
|
|
35
|
-
size: modelData
|
|
36
|
-
timestamp: new Date().toISOString(),
|
|
62
|
+
size: (modelData?.data?.length ?? 0)
|
|
37
63
|
};
|
|
38
64
|
|
|
39
|
-
const
|
|
40
|
-
const store =
|
|
41
|
-
const
|
|
65
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
66
|
+
const store = tx.objectStore(storeName);
|
|
67
|
+
const req = store.put(dataToStore);
|
|
42
68
|
|
|
43
|
-
|
|
44
|
-
|
|
69
|
+
req.onerror = () => reject(req.error);
|
|
70
|
+
req.onsuccess = () => resolve();
|
|
45
71
|
});
|
|
46
72
|
}
|
|
47
73
|
|
|
48
74
|
// Cargar modelo
|
|
49
75
|
export function loadModel(modelId, storeName) {
|
|
50
76
|
return new Promise((resolve, reject) => {
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const tx = globalThis.gltfDB.transaction([storeName], "readonly");
|
|
77
|
+
let db;
|
|
78
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
79
|
+
|
|
80
|
+
const tx = db.transaction([storeName], "readonly");
|
|
56
81
|
const store = tx.objectStore(storeName);
|
|
57
82
|
const req = store.get(modelId);
|
|
83
|
+
|
|
58
84
|
req.onerror = () => reject(req.error);
|
|
59
85
|
req.onsuccess = () => resolve(req.result ?? null);
|
|
60
86
|
});
|
|
61
87
|
}
|
|
62
88
|
|
|
89
|
+
// Descargar archivo desde base64
|
|
63
90
|
export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
|
|
64
91
|
const link = document.createElement("a");
|
|
65
92
|
link.download = fileName;
|
|
66
93
|
link.href = `data:${mimeType};base64,${bytesBase64}`;
|
|
67
94
|
document.body.appendChild(link);
|
|
68
95
|
link.click();
|
|
69
|
-
|
|
96
|
+
link.remove();
|
|
70
97
|
}
|
|
71
98
|
|
|
72
99
|
// Obtener todos los modelos (solo metadata)
|
|
73
100
|
export async function getAllModels(storeName) {
|
|
74
101
|
return new Promise((resolve, reject) => {
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
102
|
+
let db;
|
|
103
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
79
104
|
|
|
80
|
-
const
|
|
81
|
-
const store =
|
|
82
|
-
const
|
|
105
|
+
const tx = db.transaction([storeName], "readonly");
|
|
106
|
+
const store = tx.objectStore(storeName);
|
|
107
|
+
const req = store.getAll();
|
|
83
108
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const results =
|
|
109
|
+
req.onerror = () => reject(req.error);
|
|
110
|
+
req.onsuccess = () => {
|
|
111
|
+
const items = Array.isArray(req.result) ? req.result : [];
|
|
112
|
+
const results = items.map(item => ({
|
|
88
113
|
id: item.id,
|
|
89
114
|
metadata: item.metadata,
|
|
90
|
-
|
|
91
|
-
size: item.size
|
|
92
|
-
type: item.type,
|
|
115
|
+
timeStamp: item.timeStamp,
|
|
116
|
+
size: item.size
|
|
93
117
|
}));
|
|
118
|
+
// keep old behavior: return JSON string
|
|
94
119
|
resolve(JSON.stringify(results));
|
|
95
120
|
};
|
|
96
121
|
});
|
|
97
122
|
}
|
|
98
123
|
|
|
99
|
-
// Eliminar modelo
|
|
124
|
+
// Eliminar modelo por id
|
|
100
125
|
export async function deleteModel(modelId, storeName) {
|
|
101
126
|
return new Promise((resolve, reject) => {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
127
|
+
let db;
|
|
128
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
106
129
|
|
|
107
|
-
const
|
|
108
|
-
const store =
|
|
109
|
-
const
|
|
130
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
131
|
+
const store = tx.objectStore(storeName);
|
|
132
|
+
const req = store.delete(modelId);
|
|
110
133
|
|
|
111
|
-
|
|
112
|
-
|
|
134
|
+
req.onerror = () => reject(req.error);
|
|
135
|
+
req.onsuccess = () => resolve();
|
|
113
136
|
});
|
|
114
137
|
}
|
|
115
138
|
|
|
116
|
-
// Limpiar toda la
|
|
139
|
+
// Limpiar toda la store
|
|
117
140
|
export async function clearAll(storeName) {
|
|
118
141
|
return new Promise((resolve, reject) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
142
|
+
let db;
|
|
143
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
144
|
+
|
|
145
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
146
|
+
const store = tx.objectStore(storeName);
|
|
147
|
+
const req = store.clear();
|
|
148
|
+
|
|
149
|
+
req.onerror = () => reject(req.error);
|
|
150
|
+
req.onsuccess = () => resolve();
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Borrar modelos expirados usando el índice "expirationTimeStamp"
|
|
155
|
+
export async function cleanExpiredModels(storeName) {
|
|
156
|
+
return new Promise((resolve, reject) => {
|
|
157
|
+
let db;
|
|
158
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
159
|
+
|
|
160
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
161
|
+
const store = tx.objectStore(storeName);
|
|
162
|
+
|
|
163
|
+
const index = store.index("expirationTimeStamp");
|
|
164
|
+
const now = Date.now();
|
|
165
|
+
const range = IDBKeyRange.upperBound(now);
|
|
166
|
+
const cursorRequest = index.openCursor(range);
|
|
167
|
+
|
|
168
|
+
cursorRequest.onerror = () => reject(cursorRequest.error);
|
|
169
|
+
cursorRequest.onsuccess = (event) => {
|
|
170
|
+
const cursor = event.target.result;
|
|
171
|
+
if (cursor) {
|
|
172
|
+
cursor.delete();
|
|
173
|
+
cursor.continue();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
tx.oncomplete = () => resolve();
|
|
178
|
+
tx.onerror = () => reject(tx.error);
|
|
179
|
+
});
|
|
180
|
+
}
|
|
123
181
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
182
|
+
// Utilidades opcionales y visibles (por si las quieres usar en consola)
|
|
183
|
+
export function closeDb() {
|
|
184
|
+
if (PC.db) {
|
|
185
|
+
try { PC.db.close(); } catch { }
|
|
186
|
+
PC.db = null;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
127
189
|
|
|
128
|
-
|
|
129
|
-
|
|
190
|
+
export function deleteDatabase(dbName) {
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
// Close current handle if points to this DB
|
|
193
|
+
if (PC.db && PC.db.name === dbName) {
|
|
194
|
+
try { PC.db.close(); } catch { }
|
|
195
|
+
PC.db = null;
|
|
196
|
+
}
|
|
197
|
+
const req = indexedDB.deleteDatabase(dbName);
|
|
198
|
+
req.onblocked = () => reject(new Error("Delete blocked by another tab or worker"));
|
|
199
|
+
req.onerror = () => reject(req.error);
|
|
200
|
+
req.onsuccess = () => resolve();
|
|
130
201
|
});
|
|
131
202
|
}
|
|
132
203
|
|
|
204
|
+
// Attach a frozen, public API (no private state)
|
|
133
205
|
(function attachPublicAPI(global) {
|
|
134
|
-
const root = (global.PrefConfigurator ??= {});
|
|
135
206
|
const storage = {
|
|
136
207
|
initDb,
|
|
137
208
|
saveModel,
|
|
@@ -139,10 +210,12 @@ export async function clearAll(storeName) {
|
|
|
139
210
|
getAllModels,
|
|
140
211
|
deleteModel,
|
|
141
212
|
clearAll,
|
|
213
|
+
cleanExpiredModels,
|
|
142
214
|
downloadFileFromBytes,
|
|
215
|
+
// extras
|
|
216
|
+
closeDb,
|
|
217
|
+
deleteDatabase,
|
|
143
218
|
};
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
root.storage = Object.freeze(storage);
|
|
148
|
-
})(globalThis);
|
|
219
|
+
// free to inspect PC.db in devtools
|
|
220
|
+
global.PrefConfigurator.storage = Object.freeze(storage);
|
|
221
|
+
})(globalThis);
|
package/src/index.js
CHANGED
|
@@ -60,7 +60,7 @@ class PrefViewer extends HTMLElement {
|
|
|
60
60
|
storage: null,
|
|
61
61
|
visible: false,
|
|
62
62
|
size: null,
|
|
63
|
-
|
|
63
|
+
timeStamp: null,
|
|
64
64
|
changed: false,
|
|
65
65
|
},
|
|
66
66
|
environment: {
|
|
@@ -70,7 +70,7 @@ class PrefViewer extends HTMLElement {
|
|
|
70
70
|
storage: null,
|
|
71
71
|
visible: false,
|
|
72
72
|
size: null,
|
|
73
|
-
|
|
73
|
+
timeStamp: null,
|
|
74
74
|
changed: false,
|
|
75
75
|
},
|
|
76
76
|
materials: {
|
|
@@ -80,7 +80,7 @@ class PrefViewer extends HTMLElement {
|
|
|
80
80
|
show: true,
|
|
81
81
|
visible: false,
|
|
82
82
|
size: null,
|
|
83
|
-
|
|
83
|
+
timeStamp: null,
|
|
84
84
|
changed: false,
|
|
85
85
|
},
|
|
86
86
|
},
|
|
@@ -354,10 +354,14 @@ class PrefViewer extends HTMLElement {
|
|
|
354
354
|
return someChanged;
|
|
355
355
|
}
|
|
356
356
|
|
|
357
|
-
#storeChangedFlagsForContainer(container) {
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
357
|
+
#storeChangedFlagsForContainer(container, success) {
|
|
358
|
+
if (success) {
|
|
359
|
+
container.timeStamp = container.changed.timeStamp;
|
|
360
|
+
container.size = container.changed.size;
|
|
361
|
+
} else {
|
|
362
|
+
container.source = container.changed.source;
|
|
363
|
+
}
|
|
364
|
+
container.changed.success = success;
|
|
361
365
|
}
|
|
362
366
|
|
|
363
367
|
#resetChangedFlags() {
|
|
@@ -503,8 +507,8 @@ class PrefViewer extends HTMLElement {
|
|
|
503
507
|
xhr.onload = () => {
|
|
504
508
|
if (xhr.status === 200) {
|
|
505
509
|
const size = parseInt(xhr.getResponseHeader("Content-Length"));
|
|
506
|
-
const
|
|
507
|
-
resolve([size,
|
|
510
|
+
const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
|
|
511
|
+
resolve([size, timeStamp]);
|
|
508
512
|
} else {
|
|
509
513
|
resolve([0, null]);
|
|
510
514
|
}
|
|
@@ -564,7 +568,6 @@ class PrefViewer extends HTMLElement {
|
|
|
564
568
|
}
|
|
565
569
|
|
|
566
570
|
#setOptionsMaterial(optionMaterial) {
|
|
567
|
-
debugger;
|
|
568
571
|
if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
|
|
569
572
|
return false;
|
|
570
573
|
}
|
|
@@ -680,10 +683,10 @@ class PrefViewer extends HTMLElement {
|
|
|
680
683
|
await this.#initStorage(storage.db, storage.table);
|
|
681
684
|
const object = await loadModel(storage.id, storage.table);
|
|
682
685
|
source = object.data;
|
|
683
|
-
if (object.
|
|
686
|
+
if (object.timeStamp === container.timeStamp) {
|
|
684
687
|
return false;
|
|
685
688
|
} else {
|
|
686
|
-
container.changed
|
|
689
|
+
Object.assign(container.changed, { timeStamp: object.timeStamp, size: object.size, success: false });
|
|
687
690
|
}
|
|
688
691
|
}
|
|
689
692
|
|
|
@@ -699,20 +702,20 @@ class PrefViewer extends HTMLElement {
|
|
|
699
702
|
type: blob.type,
|
|
700
703
|
});
|
|
701
704
|
if (!container.changed) {
|
|
702
|
-
if (container.
|
|
705
|
+
if (container.timeStamp === null && container.size === size) {
|
|
703
706
|
return false;
|
|
704
707
|
} else {
|
|
705
|
-
container.changed
|
|
708
|
+
Object.assign(container.changed, { timeStamp: null, size: size, success: false });
|
|
706
709
|
}
|
|
707
710
|
}
|
|
708
711
|
} else {
|
|
709
712
|
const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
|
|
710
713
|
extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
|
|
711
714
|
const [fileSize, fileTimestamp ] = await this.#getServerFileDataHeader(source);
|
|
712
|
-
if (container.size === fileSize && container.
|
|
715
|
+
if (container.size === fileSize && container.timeStamp === fileTimestamp) {
|
|
713
716
|
return false;
|
|
714
717
|
} else {
|
|
715
|
-
container.changed
|
|
718
|
+
Object.assign(container.changed, { timeStamp: fileTimestamp, size: fileSize, success: false });
|
|
716
719
|
}
|
|
717
720
|
}
|
|
718
721
|
|
|
@@ -734,7 +737,7 @@ class PrefViewer extends HTMLElement {
|
|
|
734
737
|
promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
|
|
735
738
|
promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
|
|
736
739
|
promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
|
|
737
|
-
|
|
740
|
+
|
|
738
741
|
const loadingDetail = {
|
|
739
742
|
model: !!this.#data.containers.model.changed,
|
|
740
743
|
environment: !!this.#data.containers.environment.changed,
|
|
@@ -757,21 +760,25 @@ debugger;
|
|
|
757
760
|
|
|
758
761
|
if (modelContainer.status === "fulfilled" && modelContainer.value) {
|
|
759
762
|
this.#replaceContainer(this.#data.containers.model, modelContainer.value);
|
|
760
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.model);
|
|
763
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
|
|
761
764
|
} else {
|
|
762
765
|
this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
|
|
766
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.model, false);
|
|
763
767
|
}
|
|
764
768
|
|
|
765
769
|
if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
|
|
766
770
|
this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
|
|
767
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.environment);
|
|
771
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
|
|
768
772
|
} else {
|
|
769
773
|
this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
|
|
774
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.environment, false);
|
|
770
775
|
}
|
|
771
776
|
|
|
772
777
|
if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
|
|
773
778
|
this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
|
|
774
|
-
this.#storeChangedFlagsForContainer(this.#data.containers.materials);
|
|
779
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
|
|
780
|
+
} else {
|
|
781
|
+
this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
|
|
775
782
|
}
|
|
776
783
|
|
|
777
784
|
this.#setOptionsMaterials();
|
|
@@ -802,10 +809,13 @@ debugger;
|
|
|
802
809
|
}
|
|
803
810
|
|
|
804
811
|
// Containers
|
|
812
|
+
this.#data.containers.model.changed = { storage: this.#data.containers.model.storage || null };
|
|
805
813
|
this.#data.containers.model.storage = config.model?.storage || null;
|
|
806
814
|
this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
|
|
815
|
+
this.#data.containers.environment.changed = { storage: this.#data.containers.environment.storage || null };
|
|
807
816
|
this.#data.containers.environment.storage = config.scene?.storage || null;
|
|
808
817
|
this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
|
|
818
|
+
this.#data.containers.materials.changed = { storage: this.#data.containers.materials.storage || null };
|
|
809
819
|
this.#data.containers.materials.storage = config.materials?.storage || null;
|
|
810
820
|
|
|
811
821
|
// Options
|