@preference-sl/pref-viewer 2.13.0-beta.13 → 2.13.0-beta.14

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gltf-storage.js +48 -55
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@preference-sl/pref-viewer",
3
- "version": "2.13.0-beta.13",
3
+ "version": "2.13.0-beta.14",
4
4
  "description": "Web Component to preview GLTF models with Babylon.js",
5
5
  "author": "Alex Moreno Palacio <amoreno@preference.es>",
6
6
  "scripts": {
@@ -4,6 +4,8 @@
4
4
  const PC = (globalThis.PrefConfigurator ??= {});
5
5
  PC.version = PC.version ?? "1.0.1"; // bump if your schema changes!!
6
6
  PC.db = PC.db ?? null;
7
+ PC.dbName = PC.dbName ?? null;
8
+ PC.storeName = PC.storeName ?? null;
7
9
 
8
10
  function _getDbOrThrow() {
9
11
  const db = PC.db;
@@ -23,22 +25,50 @@ function _openEnsuringStore(dbName, storeName) {
23
25
  }
24
26
  open.onupgradeneeded = (ev) => {
25
27
  const upgradeDb = ev.target.result;
28
+ const tx = ev.target.transaction;
26
29
 
27
- // Eliminar la object store si ya existe
28
- if (upgradeDb.objectStoreNames.contains(storeName)) {
29
- upgradeDb.deleteObjectStore(storeName);
30
+ let store;
31
+ if (!upgradeDb.objectStoreNames.contains(storeName)) {
32
+ store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
33
+ } else if (tx) {
34
+ store = tx.objectStore(storeName);
30
35
  }
31
36
 
32
- const store = upgradeDb.createObjectStore(storeName, { keyPath: 'id' });
33
- store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
37
+ if (store && !store.indexNames.contains('expirationTimeStamp')) {
38
+ store.createIndex('expirationTimeStamp', 'expirationTimeStamp', { unique: false });
39
+ }
34
40
  };
35
41
  });
36
42
  }
37
43
 
44
+ function _isRecoverableDbError(err) {
45
+ const msg = String(err?.message ?? err ?? "");
46
+ if (!msg) return false;
47
+ return msg.includes("Database not initialized") ||
48
+ msg.includes("The database connection is closing") ||
49
+ msg.includes("InvalidStateError") ||
50
+ msg.includes("NotFoundError");
51
+ }
52
+
53
+ async function _withDbRetry(work) {
54
+ try {
55
+ return await work(_getDbOrThrow());
56
+ } catch (e) {
57
+ if (!_isRecoverableDbError(e) || !PC.dbName || !PC.storeName) {
58
+ throw e;
59
+ }
60
+ await initDb(PC.dbName, PC.storeName);
61
+ return await work(_getDbOrThrow());
62
+ }
63
+ }
64
+
38
65
  // --- public API -------------------------------------------------------------
39
66
 
40
67
  // Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
41
68
  export async function initDb(dbName, storeName) {
69
+ PC.dbName = dbName;
70
+ PC.storeName = storeName;
71
+
42
72
  const db = await _openEnsuringStore(dbName, storeName);
43
73
  // Close any previous handle to avoid versionchange blocking
44
74
  try { PC.db?.close?.(); } catch { }
@@ -54,10 +84,7 @@ export async function initDb(dbName, storeName) {
54
84
 
55
85
  // Guardar modelo
56
86
  export async function saveModel(modelDataStr, storeName) {
57
- return new Promise((resolve, reject) => {
58
- let db;
59
- try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
60
-
87
+ return _withDbRetry((db) => new Promise((resolve, reject) => {
61
88
  let modelData;
62
89
  try { modelData = JSON.parse(modelDataStr); }
63
90
  catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
@@ -74,59 +101,34 @@ export async function saveModel(modelDataStr, storeName) {
74
101
 
75
102
  req.onerror = () => reject(req.error);
76
103
  req.onsuccess = () => resolve();
77
- });
104
+ }));
78
105
  }
79
106
 
80
107
  // Cargar modelo
81
108
  export function loadModel(modelId, storeName) {
82
- return new Promise((resolve, reject) => {
83
- let db;
84
- try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
85
-
109
+ return _withDbRetry((db) => new Promise((resolve, reject) => {
86
110
  const tx = db.transaction([storeName], "readonly");
87
111
  const store = tx.objectStore(storeName);
88
112
  const req = store.get(modelId);
89
113
 
90
114
  req.onerror = () => reject(req.error);
91
115
  req.onsuccess = () => resolve(req.result ?? null);
92
- });
116
+ }));
93
117
  }
94
118
 
95
119
  // Descargar archivo desde base64
96
120
  export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
97
- let objectURL = null;
98
121
  const link = document.createElement("a");
99
122
  link.download = fileName;
100
- try {
101
- const decoded = atob(bytesBase64);
102
- const bytes = new Uint8Array(decoded.length);
103
- for (let i = 0; i < decoded.length; i++) {
104
- bytes[i] = decoded.charCodeAt(i);
105
- }
106
- const blob = new Blob([bytes], { type: mimeType || "application/octet-stream" });
107
- objectURL = URL.createObjectURL(blob);
108
- link.href = objectURL;
109
- } catch {
110
- // Fallback if base64 decoding fails for any reason.
111
- link.href = `data:${mimeType};base64,${bytesBase64}`;
112
- }
123
+ link.href = `data:${mimeType};base64,${bytesBase64}`;
113
124
  document.body.appendChild(link);
114
125
  link.click();
115
- link.href = "";
116
126
  link.remove();
117
- if (objectURL) {
118
- setTimeout(() => {
119
- URL.revokeObjectURL(objectURL);
120
- }, 0);
121
- }
122
127
  }
123
128
 
124
129
  // Obtener todos los modelos (solo metadata)
125
130
  export async function getAllModels(storeName) {
126
- return new Promise((resolve, reject) => {
127
- let db;
128
- try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
129
-
131
+ return _withDbRetry((db) => new Promise((resolve, reject) => {
130
132
  const tx = db.transaction([storeName], "readonly");
131
133
  const store = tx.objectStore(storeName);
132
134
  const req = store.getAll();
@@ -143,45 +145,36 @@ export async function getAllModels(storeName) {
143
145
  // keep old behavior: return JSON string
144
146
  resolve(JSON.stringify(results));
145
147
  };
146
- });
148
+ }));
147
149
  }
148
150
 
149
151
  // Eliminar modelo por id
150
152
  export async function deleteModel(modelId, storeName) {
151
- return new Promise((resolve, reject) => {
152
- let db;
153
- try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
154
-
153
+ return _withDbRetry((db) => new Promise((resolve, reject) => {
155
154
  const tx = db.transaction([storeName], "readwrite");
156
155
  const store = tx.objectStore(storeName);
157
156
  const req = store.delete(modelId);
158
157
 
159
158
  req.onerror = () => reject(req.error);
160
159
  req.onsuccess = () => resolve();
161
- });
160
+ }));
162
161
  }
163
162
 
164
163
  // Limpiar toda la store
165
164
  export async function clearAll(storeName) {
166
- return new Promise((resolve, reject) => {
167
- let db;
168
- try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
169
-
165
+ return _withDbRetry((db) => new Promise((resolve, reject) => {
170
166
  const tx = db.transaction([storeName], "readwrite");
171
167
  const store = tx.objectStore(storeName);
172
168
  const req = store.clear();
173
169
 
174
170
  req.onerror = () => reject(req.error);
175
171
  req.onsuccess = () => resolve();
176
- });
172
+ }));
177
173
  }
178
174
 
179
175
  // Borrar modelos expirados usando el índice "expirationTimeStamp"
180
176
  export async function cleanExpiredModels(storeName) {
181
- return new Promise((resolve, reject) => {
182
- let db;
183
- try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
184
-
177
+ return _withDbRetry((db) => new Promise((resolve, reject) => {
185
178
  const tx = db.transaction([storeName], "readwrite");
186
179
  const store = tx.objectStore(storeName);
187
180
 
@@ -201,7 +194,7 @@ export async function cleanExpiredModels(storeName) {
201
194
 
202
195
  tx.oncomplete = () => resolve();
203
196
  tx.onerror = () => reject(tx.error);
204
- });
197
+ }));
205
198
  }
206
199
 
207
200
  // Utilidades opcionales y visibles (por si las quieres usar en consola)