@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 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.17",
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,208 @@
1
- // Inicializar IndexedDB
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
- request.onerror = () => reject(request.error);
7
- request.onsuccess = () => {
8
- window.gltfDB = request.result;
9
- resolve();
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
- 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 });
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
- if (!window.gltfDB) {
27
- reject(new Error("Database not initialized"));
28
- return;
29
- }
30
- let modelData = JSON.parse(modelDataStr);
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.data.length,
36
- timestamp: new Date().toISOString(),
62
+ size: (modelData?.data?.length ?? 0)
37
63
  };
38
64
 
39
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
40
- const store = transaction.objectStore(storeName);
41
- const request = store.put(dataToStore);
65
+ const tx = db.transaction([storeName], "readwrite");
66
+ const store = tx.objectStore(storeName);
67
+ const req = store.put(dataToStore);
42
68
 
43
- request.onerror = () => reject(request.error);
44
- request.onsuccess = () => resolve();
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
- if (!globalThis.gltfDB) {
52
- reject(new Error("Database not initialized"));
53
- return;
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
- document.body.removeChild(link);
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
- if (!window.gltfDB) {
76
- reject(new Error("Database not initialized"));
77
- return;
78
- }
102
+ let db;
103
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
79
104
 
80
- const transaction = window.gltfDB.transaction([storeName], "readonly");
81
- const store = transaction.objectStore(storeName);
82
- const request = store.getAll();
105
+ const tx = db.transaction([storeName], "readonly");
106
+ const store = tx.objectStore(storeName);
107
+ const req = store.getAll();
83
108
 
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) => ({
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
- timestamp: item.timestamp,
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
- if (!window.gltfDB) {
103
- reject(new Error("Database not initialized"));
104
- return;
105
- }
127
+ let db;
128
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
106
129
 
107
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
108
- const store = transaction.objectStore(storeName);
109
- const request = store.delete(modelId);
130
+ const tx = db.transaction([storeName], "readwrite");
131
+ const store = tx.objectStore(storeName);
132
+ const req = store.delete(modelId);
110
133
 
111
- request.onerror = () => reject(request.error);
112
- request.onsuccess = () => resolve();
134
+ req.onerror = () => reject(req.error);
135
+ req.onsuccess = () => resolve();
113
136
  });
114
137
  }
115
138
 
116
- // Limpiar toda la base de datos
139
+ // Limpiar toda la store
117
140
  export async function clearAll(storeName) {
118
141
  return new Promise((resolve, reject) => {
119
- if (!window.gltfDB) {
120
- reject(new Error("Database not initialized"));
121
- return;
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
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
125
- const store = transaction.objectStore(storeName);
126
- const request = store.clear();
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
- request.onerror = () => reject(request.error);
129
- request.onsuccess = () => resolve();
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
- // versionado del módulo público
146
- root.version = root.version ?? "1.0.0";
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
- 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
  },
@@ -354,10 +354,14 @@ class PrefViewer extends HTMLElement {
354
354
  return someChanged;
355
355
  }
356
356
 
357
- #storeChangedFlagsForContainer(container) {
358
- container.timestamp = container.changed.timestamp;
359
- container.size = container.changed.size;
360
- container.changed.success = true;
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 timestamp = new Date(xhr.getResponseHeader("Last-Modified")).toISOString();
507
- resolve([size, timestamp]);
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.timestamp === container.timestamp) {
686
+ if (object.timeStamp === container.timeStamp) {
684
687
  return false;
685
688
  } else {
686
- container.changed = { timestamp: object.timestamp, size: object.size, success: false };
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.timestamp === null && container.size === size) {
705
+ if (container.timeStamp === null && container.size === size) {
703
706
  return false;
704
707
  } else {
705
- container.changed = { timestamp: null, size: size, success: false };
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.timestamp === fileTimestamp) {
715
+ if (container.size === fileSize && container.timeStamp === fileTimestamp) {
713
716
  return false;
714
717
  } else {
715
- container.changed = { timestamp: fileTimestamp, size: fileSize, success: false };
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
- debugger;
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