@preference-sl/pref-viewer 2.10.0-beta.16 → 2.10.0-beta.18

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.16",
3
+ "version": "2.10.0-beta.18",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -1,137 +1,214 @@
1
- // Inicializar IndexedDB
2
- export async function initDb(dbName, storeName) {
1
+ // wwwroot/js/gltf-storage.js
2
+
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;
7
+
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) {
3
16
  return new Promise((resolve, reject) => {
4
- const request = indexedDB.open(dbName, 1);
17
+ const open = indexedDB.open(dbName, 5);
5
18
 
6
- request.onerror = () => reject(request.error);
7
- request.onsuccess = () => {
8
- window.gltfDB = request.result;
9
- resolve();
10
- };
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;
11
26
 
12
- request.onupgradeneeded = (event) => {
13
- const db = event.target.result;
14
- if (!db.objectStoreNames.contains(storeName)) {
15
- const store = db.createObjectStore(storeName, { keyPath: "id" });
16
- store.createIndex("type", "type", { unique: false });
17
- store.createIndex("timestamp", "timestamp", { unique: false });
27
+ // Eliminar la object store si ya existe
28
+ if (upgradeDb.objectStoreNames.contains(storeName)) {
29
+ upgradeDb.deleteObjectStore(storeName);
18
30
  }
31
+
32
+ const store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
33
+ store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
19
34
  };
20
35
  });
21
36
  }
22
37
 
38
+ // --- public API -------------------------------------------------------------
39
+
40
+ // Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
41
+ export async function initDb(dbName, storeName) {
42
+ const db = await _openEnsuringStore(dbName, storeName);
43
+ // Close any previous handle to avoid versionchange blocking
44
+ try { PC.db?.close?.(); } catch { }
45
+ PC.db = db;
46
+
47
+ // If another tab upgrades, close gracefully so next call can re-init
48
+ db.onversionchange = () => {
49
+ try { db.close(); } catch { }
50
+ if (PC.db === db) PC.db = null;
51
+ console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
52
+ };
53
+ }
54
+
23
55
  // Guardar modelo
24
56
  export async function saveModel(modelDataStr, storeName) {
25
57
  return new Promise((resolve, reject) => {
26
- if (!window.gltfDB) {
27
- reject(new Error("Database not initialized"));
28
- return;
29
- }
30
- let modelData = JSON.parse(modelDataStr);
58
+ let db;
59
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
60
+
61
+ let modelData;
62
+ try { modelData = JSON.parse(modelDataStr); }
63
+ catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
31
64
 
32
65
  const dataToStore = {
33
66
  ...modelData,
34
67
  data: modelData.data,
35
- size: modelData.data.length,
36
- timestamp: new Date().toISOString(),
68
+ size: (modelData?.data?.length ?? 0)
37
69
  };
38
70
 
39
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
40
- const store = transaction.objectStore(storeName);
41
- const request = store.put(dataToStore);
71
+ const tx = db.transaction([storeName], "readwrite");
72
+ const store = tx.objectStore(storeName);
73
+ const req = store.put(dataToStore);
42
74
 
43
- request.onerror = () => reject(request.error);
44
- request.onsuccess = () => resolve();
75
+ req.onerror = () => reject(req.error);
76
+ req.onsuccess = () => resolve();
45
77
  });
46
78
  }
47
79
 
48
80
  // Cargar modelo
49
81
  export function loadModel(modelId, storeName) {
50
82
  return new Promise((resolve, reject) => {
51
- if (!globalThis.gltfDB) {
52
- reject(new Error("Database not initialized"));
53
- return;
54
- }
55
- const tx = globalThis.gltfDB.transaction([storeName], "readonly");
83
+ let db;
84
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
85
+
86
+ const tx = db.transaction([storeName], "readonly");
56
87
  const store = tx.objectStore(storeName);
57
88
  const req = store.get(modelId);
89
+
58
90
  req.onerror = () => reject(req.error);
59
91
  req.onsuccess = () => resolve(req.result ?? null);
60
92
  });
61
93
  }
62
94
 
95
+ // Descargar archivo desde base64
63
96
  export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
64
97
  const link = document.createElement("a");
65
98
  link.download = fileName;
66
99
  link.href = `data:${mimeType};base64,${bytesBase64}`;
67
100
  document.body.appendChild(link);
68
101
  link.click();
69
- document.body.removeChild(link);
102
+ link.remove();
70
103
  }
71
104
 
72
105
  // Obtener todos los modelos (solo metadata)
73
106
  export async function getAllModels(storeName) {
74
107
  return new Promise((resolve, reject) => {
75
- if (!window.gltfDB) {
76
- reject(new Error("Database not initialized"));
77
- return;
78
- }
108
+ let db;
109
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
79
110
 
80
- const transaction = window.gltfDB.transaction([storeName], "readonly");
81
- const store = transaction.objectStore(storeName);
82
- const request = store.getAll();
111
+ const tx = db.transaction([storeName], "readonly");
112
+ const store = tx.objectStore(storeName);
113
+ const req = store.getAll();
83
114
 
84
- request.onerror = () => reject(request.error);
85
- request.onsuccess = () => {
86
- // Excluir los datos binarios para evitar transferir demasiados datos
87
- const results = request.result.map((item) => ({
115
+ req.onerror = () => reject(req.error);
116
+ req.onsuccess = () => {
117
+ const items = Array.isArray(req.result) ? req.result : [];
118
+ const results = items.map(item => ({
88
119
  id: item.id,
89
120
  metadata: item.metadata,
90
- timestamp: item.timestamp,
91
- size: item.size,
92
- type: item.type,
121
+ timeStamp: item.timeStamp,
122
+ size: item.size
93
123
  }));
124
+ // keep old behavior: return JSON string
94
125
  resolve(JSON.stringify(results));
95
126
  };
96
127
  });
97
128
  }
98
129
 
99
- // Eliminar modelo
130
+ // Eliminar modelo por id
100
131
  export async function deleteModel(modelId, storeName) {
101
132
  return new Promise((resolve, reject) => {
102
- if (!window.gltfDB) {
103
- reject(new Error("Database not initialized"));
104
- return;
105
- }
133
+ let db;
134
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
106
135
 
107
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
108
- const store = transaction.objectStore(storeName);
109
- const request = store.delete(modelId);
136
+ const tx = db.transaction([storeName], "readwrite");
137
+ const store = tx.objectStore(storeName);
138
+ const req = store.delete(modelId);
110
139
 
111
- request.onerror = () => reject(request.error);
112
- request.onsuccess = () => resolve();
140
+ req.onerror = () => reject(req.error);
141
+ req.onsuccess = () => resolve();
113
142
  });
114
143
  }
115
144
 
116
- // Limpiar toda la base de datos
145
+ // Limpiar toda la store
117
146
  export async function clearAll(storeName) {
118
147
  return new Promise((resolve, reject) => {
119
- if (!window.gltfDB) {
120
- reject(new Error("Database not initialized"));
121
- return;
122
- }
148
+ let db;
149
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
150
+
151
+ const tx = db.transaction([storeName], "readwrite");
152
+ const store = tx.objectStore(storeName);
153
+ const req = store.clear();
154
+
155
+ req.onerror = () => reject(req.error);
156
+ req.onsuccess = () => resolve();
157
+ });
158
+ }
159
+
160
+ // Borrar modelos expirados usando el índice "expirationTimeStamp"
161
+ export async function cleanExpiredModels(storeName) {
162
+ return new Promise((resolve, reject) => {
163
+ let db;
164
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
165
+
166
+ const tx = db.transaction([storeName], "readwrite");
167
+ const store = tx.objectStore(storeName);
168
+
169
+ const index = store.index("expirationTimeStamp");
170
+ const now = Date.now();
171
+ const range = IDBKeyRange.upperBound(now);
172
+ const cursorRequest = index.openCursor(range);
173
+
174
+ cursorRequest.onerror = () => reject(cursorRequest.error);
175
+ cursorRequest.onsuccess = (event) => {
176
+ const cursor = event.target.result;
177
+ if (cursor) {
178
+ cursor.delete();
179
+ cursor.continue();
180
+ }
181
+ };
182
+
183
+ tx.oncomplete = () => resolve();
184
+ tx.onerror = () => reject(tx.error);
185
+ });
186
+ }
123
187
 
124
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
125
- const store = transaction.objectStore(storeName);
126
- const request = store.clear();
188
+ // Utilidades opcionales y visibles (por si las quieres usar en consola)
189
+ export function closeDb() {
190
+ if (PC.db) {
191
+ try { PC.db.close(); } catch { }
192
+ PC.db = null;
193
+ }
194
+ }
127
195
 
128
- request.onerror = () => reject(request.error);
129
- request.onsuccess = () => resolve();
196
+ export function deleteDatabase(dbName) {
197
+ return new Promise((resolve, reject) => {
198
+ // Close current handle if points to this DB
199
+ if (PC.db && PC.db.name === dbName) {
200
+ try { PC.db.close(); } catch { }
201
+ PC.db = null;
202
+ }
203
+ const req = indexedDB.deleteDatabase(dbName);
204
+ req.onblocked = () => reject(new Error("Delete blocked by another tab or worker"));
205
+ req.onerror = () => reject(req.error);
206
+ req.onsuccess = () => resolve();
130
207
  });
131
208
  }
132
209
 
210
+ // Attach a frozen, public API (no private state)
133
211
  (function attachPublicAPI(global) {
134
- const root = (global.PrefConfigurator ??= {});
135
212
  const storage = {
136
213
  initDb,
137
214
  saveModel,
@@ -139,10 +216,12 @@ export async function clearAll(storeName) {
139
216
  getAllModels,
140
217
  deleteModel,
141
218
  clearAll,
219
+ cleanExpiredModels,
142
220
  downloadFileFromBytes,
221
+ // extras
222
+ closeDb,
223
+ deleteDatabase,
143
224
  };
144
-
145
- // versionado del módulo público
146
- root.version = root.version ?? "1.0.0";
147
- root.storage = Object.freeze(storage);
225
+ // free to inspect PC.db in devtools
226
+ global.PrefConfigurator.storage = Object.freeze(storage);
148
227
  })(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
- timestamp: null,
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
- timestamp: null,
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
- timestamp: null,
83
+ timeStamp: null,
84
84
  changed: false,
85
85
  },
86
86
  },
@@ -299,7 +299,6 @@ class PrefViewer extends HTMLElement {
299
299
  }
300
300
 
301
301
  #setStatusOptionsLoaded() {
302
-
303
302
  const toLoadDetail = {
304
303
  inneWallMaterial: !!this.#data.options.materials.innerWall.changed,
305
304
  outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
@@ -310,7 +309,7 @@ class PrefViewer extends HTMLElement {
310
309
  inneWallMaterial: !!this.#data.options.materials.innerWall.changed?.success,
311
310
  outerWallMaterial: !!this.#data.options.materials.outerWall.changed?.success,
312
311
  innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed?.success,
313
- _outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
312
+ outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed?.success,
314
313
  };
315
314
 
316
315
  const detail = {
@@ -354,10 +353,17 @@ class PrefViewer extends HTMLElement {
354
353
  return someChanged;
355
354
  }
356
355
 
357
- #storeChangedFlagsForContainer(container) {
358
- container.timestamp = container.changed.timestamp;
359
- container.size = container.changed.size;
360
- container.changed.success = true;
356
+ #storeChangedFlagsForContainer(container, success) {
357
+ if (success) {
358
+ container.timeStamp = container.changed.timeStamp;
359
+ container.size = container.changed.size;
360
+ container.changed.success = success;
361
+ } else if(container.changed) {
362
+ container.source = container.changed.source;
363
+ container.changed.success = success;
364
+ } else {
365
+ container.changed = { success: success };
366
+ }
361
367
  }
362
368
 
363
369
  #resetChangedFlags() {
@@ -451,6 +457,7 @@ class PrefViewer extends HTMLElement {
451
457
  }
452
458
 
453
459
  #createLights() {
460
+
454
461
  // 1) Stronger ambient fill
455
462
  this.#hemiLight = new HemisphericLight("hemiLight", new Vector3(-10, 10, -10), this.#scene);
456
463
  this.#hemiLight.intensity = 0.6;
@@ -503,8 +510,8 @@ class PrefViewer extends HTMLElement {
503
510
  xhr.onload = () => {
504
511
  if (xhr.status === 200) {
505
512
  const size = parseInt(xhr.getResponseHeader("Content-Length"));
506
- const timestamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
507
- resolve([size, timestamp]);
513
+ const timeStamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
514
+ resolve([size, timeStamp]);
508
515
  } else {
509
516
  resolve([0, null]);
510
517
  }
@@ -564,7 +571,6 @@ class PrefViewer extends HTMLElement {
564
571
  }
565
572
 
566
573
  #setOptionsMaterial(optionMaterial) {
567
- debugger;
568
574
  if (!optionMaterial || !optionMaterial.prefix || !optionMaterial.value) {
569
575
  return false;
570
576
  }
@@ -680,10 +686,10 @@ class PrefViewer extends HTMLElement {
680
686
  await this.#initStorage(storage.db, storage.table);
681
687
  const object = await loadModel(storage.id, storage.table);
682
688
  source = object.data;
683
- if (object.timestamp === container.timestamp) {
689
+ if (object.timeStamp === container.timeStamp) {
684
690
  return false;
685
691
  } else {
686
- container.changed = { timestamp: object.timestamp, size: object.size, success: false };
692
+ Object.assign(container.changed, { timeStamp: object.timeStamp, size: object.size, success: false });
687
693
  }
688
694
  }
689
695
 
@@ -699,20 +705,20 @@ class PrefViewer extends HTMLElement {
699
705
  type: blob.type,
700
706
  });
701
707
  if (!container.changed) {
702
- if (container.timestamp === null && container.size === size) {
708
+ if (container.timeStamp === null && container.size === size) {
703
709
  return false;
704
710
  } else {
705
- container.changed = { timestamp: null, size: size, success: false };
711
+ Object.assign(container.changed, { timeStamp: null, size: size, success: false });
706
712
  }
707
713
  }
708
714
  } else {
709
715
  const extMatch = source.match(/\.(gltf|glb)(\?|#|$)/i);
710
716
  extension = extMatch ? `.${extMatch[1].toLowerCase()}` : ".gltf";
711
717
  const [fileSize, fileTimestamp ] = await this.#getServerFileDataHeader(source);
712
- if (container.size === fileSize && container.timestamp === fileTimestamp) {
718
+ if (container.size === fileSize && container.timeStamp === fileTimestamp) {
713
719
  return false;
714
720
  } else {
715
- container.changed = { timestamp: fileTimestamp, size: fileSize, success: false };
721
+ Object.assign(container.changed, { timeStamp: fileTimestamp, size: fileSize, success: false });
716
722
  }
717
723
  }
718
724
 
@@ -730,24 +736,12 @@ class PrefViewer extends HTMLElement {
730
736
  }
731
737
 
732
738
  async #loadContainers(loadModel = true, loadEnvironment = true, loadMaterials = true) {
739
+ this.#setStatusSceneLoading();
740
+
733
741
  const promiseArray = [];
734
742
  promiseArray.push(loadModel ? this.#loadAssetContainer(this.#data.containers.model) : false);
735
743
  promiseArray.push(loadEnvironment ? this.#loadAssetContainer(this.#data.containers.environment) : false);
736
744
  promiseArray.push(loadMaterials ? this.#loadAssetContainer(this.#data.containers.materials) : false);
737
- debugger;
738
- const loadingDetail = {
739
- model: !!this.#data.containers.model.changed,
740
- environment: !!this.#data.containers.environment.changed,
741
- materials: !!this.#data.containers.materials.changed,
742
- options: {
743
- camera: !!this.#data.options.camera.changed,
744
- inneWallMaterial: !!this.#data.options.materials.innerWall.changed,
745
- outerWallMaterial: !!this.#data.options.materials.outerWall.changed,
746
- innerFloorMaterial: !!this.#data.options.materials.innerFloor.changed,
747
- outerFloorMaterial: !!this.#data.options.materials.outerFloor.changed,
748
- },
749
- };
750
- this.#setStatusSceneLoading(loadingDetail);
751
745
 
752
746
  Promise.allSettled(promiseArray)
753
747
  .then(async (values) => {
@@ -757,21 +751,25 @@ debugger;
757
751
 
758
752
  if (modelContainer.status === "fulfilled" && modelContainer.value) {
759
753
  this.#replaceContainer(this.#data.containers.model, modelContainer.value);
760
- this.#storeChangedFlagsForContainer(this.#data.containers.model);
754
+ this.#storeChangedFlagsForContainer(this.#data.containers.model, true);
761
755
  } else {
762
756
  this.#data.containers.model.show ? this.#addContainer(this.#data.containers.model) : this.#removeContainer(this.#data.containers.model);
757
+ this.#storeChangedFlagsForContainer(this.#data.containers.model, false);
763
758
  }
764
759
 
765
760
  if (environmentContainer.status === "fulfilled" && environmentContainer.value) {
766
761
  this.#replaceContainer(this.#data.containers.environment, environmentContainer.value);
767
- this.#storeChangedFlagsForContainer(this.#data.containers.environment);
762
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment, true);
768
763
  } else {
769
764
  this.#data.containers.environment.show ? this.#addContainer(this.#data.containers.environment) : this.#removeContainer(this.#data.containers.environment);
765
+ this.#storeChangedFlagsForContainer(this.#data.containers.environment, false);
770
766
  }
771
767
 
772
768
  if (materialsContainer.status === "fulfilled" && materialsContainer.value) {
773
769
  this.#replaceContainer(this.#data.containers.materials, materialsContainer.value);
774
- this.#storeChangedFlagsForContainer(this.#data.containers.materials);
770
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, true);
771
+ } else {
772
+ this.#storeChangedFlagsForContainer(this.#data.containers.materials, false);
775
773
  }
776
774
 
777
775
  this.#setOptionsMaterials();
@@ -802,10 +800,13 @@ debugger;
802
800
  }
803
801
 
804
802
  // Containers
803
+ this.#data.containers.model.changed = { storage: this.#data.containers.model.storage || null };
805
804
  this.#data.containers.model.storage = config.model?.storage || null;
806
805
  this.#data.containers.model.show = config.model?.visible !== undefined ? config.model.visible : this.#data.containers.model.show;
806
+ this.#data.containers.environment.changed = { storage: this.#data.containers.environment.storage || null };
807
807
  this.#data.containers.environment.storage = config.scene?.storage || null;
808
808
  this.#data.containers.environment.show = config.scene?.visible !== undefined ? config.scene.visible : this.#data.containers.environment.show;
809
+ this.#data.containers.materials.changed = { storage: this.#data.containers.materials.storage || null };
809
810
  this.#data.containers.materials.storage = config.materials?.storage || null;
810
811
 
811
812
  // Options
@@ -843,6 +844,7 @@ debugger;
843
844
  if (!model) {
844
845
  return false;
845
846
  }
847
+ this.#data.containers.model.changed = { storage: this.#data.containers.model.storage || null };
846
848
  this.#data.containers.model.storage = model.storage || null;
847
849
  this.#data.containers.model.show = model.visible !== undefined ? model.visible : this.#data.containers.model.show;
848
850
  this.initialized && this.#loadContainers(true, false, false);
@@ -853,11 +855,22 @@ debugger;
853
855
  if (!scene) {
854
856
  return false;
855
857
  }
858
+ this.#data.containers.environment.changed = { storage: this.#data.containers.environment.storage || null };
856
859
  this.#data.containers.environment.storage = scene.storage || null;
857
860
  this.#data.containers.environment.show = scene.visible !== undefined ? scene.visible : this.#data.containers.environment.show;
858
861
  this.initialized && this.#loadContainers(false, true, false);
859
862
  }
860
863
 
864
+ loadMaterials(materials) {
865
+ materials = typeof materials === "string" ? JSON.parse(materials) : materials;
866
+ if (!materials) {
867
+ return false;
868
+ }
869
+ this.#data.containers.materials.changed = { storage: this.#data.containers.materials.storage || null };
870
+ this.#data.containers.materials.storage = materials.storage || null;
871
+ this.initialized && this.#loadContainers(false, false, true);
872
+ }
873
+
861
874
  showModel() {
862
875
  this.#data.containers.model.show = true;
863
876
  this.#addContainer(this.#data.containers.model);