@preference-sl/pref-viewer 2.10.0-beta.2 → 2.10.0-beta.21

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.2",
3
+ "version": "2.10.0-beta.21",
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,241 @@
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
+
16
+ function _createStoreIfMissing(db, storeName) {
17
+ if (!db.objectStoreNames.contains(storeName)) {
18
+ const store = db.createObjectStore(storeName, { keyPath: "id" });
19
+ store.createIndex("expirationTimeStamp", "expirationTimeStamp", { unique: false });
20
+ }
21
+ }
22
+
23
+ /**
24
+ * Opens a DB without specifying a version. If the required object store doesn't
25
+ * exist, it re-opens with (currentVersion + 1) to create it.
26
+ */
27
+ function _openEnsuringStore(dbName, storeName) {
3
28
  return new Promise((resolve, reject) => {
4
- const request = indexedDB.open(dbName, 1);
29
+ const open = indexedDB.open(dbName);
30
+
31
+ open.onblocked = () => reject(new Error("Open blocked by another tab or worker"));
32
+ open.onerror = () => reject(open.error);
5
33
 
6
- request.onerror = () => reject(request.error);
7
- request.onsuccess = () => {
8
- window.gltfDB = request.result;
9
- resolve();
34
+ open.onupgradeneeded = (e) => {
35
+ // First creation path (brand-new DB)
36
+ const db = e.target.result;
37
+ _createStoreIfMissing(db, storeName);
10
38
  };
11
39
 
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 });
40
+ open.onsuccess = (e) => {
41
+ const db = e.target.result;
42
+
43
+ // If the store exists, we're done.
44
+ if (db.objectStoreNames.contains(storeName)) {
45
+ resolve(db);
46
+ return;
18
47
  }
48
+
49
+ // Otherwise bump version just enough to add the store.
50
+ const nextVersion = db.version + 1;
51
+ db.close();
52
+
53
+ const upgrade = indexedDB.open(dbName, nextVersion);
54
+ upgrade.onblocked = () => reject(new Error("Upgrade blocked by another tab or worker"));
55
+ upgrade.onerror = () => reject(upgrade.error);
56
+ upgrade.onupgradeneeded = (ev) => {
57
+ const upDb = ev.target.result;
58
+ _createStoreIfMissing(upDb, storeName);
59
+ };
60
+ upgrade.onsuccess = (ev) => resolve(ev.target.result);
19
61
  };
20
62
  });
21
63
  }
22
64
 
65
+ // --- public API -------------------------------------------------------------
66
+
67
+ // Inicializar IndexedDB y dejar el handle en PrefConfigurator.db (público)
68
+ export async function initDb(dbName, storeName) {
69
+ const db = await _openEnsuringStore(dbName, storeName);
70
+ // Close any previous handle to avoid versionchange blocking
71
+ try { PC.db?.close?.(); } catch { }
72
+ PC.db = db;
73
+
74
+ // If another tab upgrades, close gracefully so next call can re-init
75
+ db.onversionchange = () => {
76
+ try { db.close(); } catch { }
77
+ if (PC.db === db) PC.db = null;
78
+ console.warn("[PrefConfigurator] DB connection closed due to versionchange. Re-run initDb().");
79
+ };
80
+ }
81
+
23
82
  // Guardar modelo
24
83
  export async function saveModel(modelDataStr, storeName) {
25
84
  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);
85
+ let db;
86
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
87
+
88
+ let modelData;
89
+ try { modelData = JSON.parse(modelDataStr); }
90
+ catch (e) { reject(new Error("saveModel: modelDataStr is not valid JSON")); return; }
31
91
 
32
92
  const dataToStore = {
33
93
  ...modelData,
34
94
  data: modelData.data,
35
- size: modelData.data.length,
36
- timestamp: new Date().toISOString(),
95
+ size: (modelData?.data?.length ?? 0)
37
96
  };
38
97
 
39
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
40
- const store = transaction.objectStore(storeName);
41
- const request = store.put(dataToStore);
98
+ const tx = db.transaction([storeName], "readwrite");
99
+ const store = tx.objectStore(storeName);
100
+ const req = store.put(dataToStore);
42
101
 
43
- request.onerror = () => reject(request.error);
44
- request.onsuccess = () => resolve();
102
+ req.onerror = () => reject(req.error);
103
+ req.onsuccess = () => resolve();
45
104
  });
46
105
  }
47
106
 
48
107
  // Cargar modelo
49
108
  export function loadModel(modelId, storeName) {
50
109
  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");
110
+ let db;
111
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
112
+
113
+ const tx = db.transaction([storeName], "readonly");
56
114
  const store = tx.objectStore(storeName);
57
115
  const req = store.get(modelId);
116
+
58
117
  req.onerror = () => reject(req.error);
59
118
  req.onsuccess = () => resolve(req.result ?? null);
60
119
  });
61
120
  }
62
121
 
122
+ // Descargar archivo desde base64
63
123
  export function downloadFileFromBytes(fileName, bytesBase64, mimeType) {
64
124
  const link = document.createElement("a");
65
125
  link.download = fileName;
66
126
  link.href = `data:${mimeType};base64,${bytesBase64}`;
67
127
  document.body.appendChild(link);
68
128
  link.click();
69
- document.body.removeChild(link);
129
+ link.remove();
70
130
  }
71
131
 
72
132
  // Obtener todos los modelos (solo metadata)
73
133
  export async function getAllModels(storeName) {
74
134
  return new Promise((resolve, reject) => {
75
- if (!window.gltfDB) {
76
- reject(new Error("Database not initialized"));
77
- return;
78
- }
135
+ let db;
136
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
79
137
 
80
- const transaction = window.gltfDB.transaction([storeName], "readonly");
81
- const store = transaction.objectStore(storeName);
82
- const request = store.getAll();
138
+ const tx = db.transaction([storeName], "readonly");
139
+ const store = tx.objectStore(storeName);
140
+ const req = store.getAll();
83
141
 
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) => ({
142
+ req.onerror = () => reject(req.error);
143
+ req.onsuccess = () => {
144
+ const items = Array.isArray(req.result) ? req.result : [];
145
+ const results = items.map(item => ({
88
146
  id: item.id,
89
147
  metadata: item.metadata,
90
- timestamp: item.timestamp,
91
- size: item.size,
92
- type: item.type,
148
+ timeStamp: item.timeStamp,
149
+ size: item.size
93
150
  }));
151
+ // keep old behavior: return JSON string
94
152
  resolve(JSON.stringify(results));
95
153
  };
96
154
  });
97
155
  }
98
156
 
99
- // Eliminar modelo
157
+ // Eliminar modelo por id
100
158
  export async function deleteModel(modelId, storeName) {
101
159
  return new Promise((resolve, reject) => {
102
- if (!window.gltfDB) {
103
- reject(new Error("Database not initialized"));
104
- return;
105
- }
160
+ let db;
161
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
106
162
 
107
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
108
- const store = transaction.objectStore(storeName);
109
- const request = store.delete(modelId);
163
+ const tx = db.transaction([storeName], "readwrite");
164
+ const store = tx.objectStore(storeName);
165
+ const req = store.delete(modelId);
110
166
 
111
- request.onerror = () => reject(request.error);
112
- request.onsuccess = () => resolve();
167
+ req.onerror = () => reject(req.error);
168
+ req.onsuccess = () => resolve();
113
169
  });
114
170
  }
115
171
 
116
- // Limpiar toda la base de datos
172
+ // Limpiar toda la store
117
173
  export async function clearAll(storeName) {
118
174
  return new Promise((resolve, reject) => {
119
- if (!window.gltfDB) {
120
- reject(new Error("Database not initialized"));
121
- return;
122
- }
175
+ let db;
176
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
123
177
 
124
- const transaction = window.gltfDB.transaction([storeName], "readwrite");
125
- const store = transaction.objectStore(storeName);
126
- const request = store.clear();
178
+ const tx = db.transaction([storeName], "readwrite");
179
+ const store = tx.objectStore(storeName);
180
+ const req = store.clear();
127
181
 
128
- request.onerror = () => reject(request.error);
129
- request.onsuccess = () => resolve();
182
+ req.onerror = () => reject(req.error);
183
+ req.onsuccess = () => resolve();
130
184
  });
131
185
  }
132
186
 
187
+ // Borrar modelos expirados usando el índice "expirationTimeStamp"
188
+ export async function cleanExpiredModels(storeName) {
189
+ return new Promise((resolve, reject) => {
190
+ let db;
191
+ try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
192
+
193
+ const tx = db.transaction([storeName], "readwrite");
194
+ const store = tx.objectStore(storeName);
195
+
196
+ const index = store.index("expirationTimeStamp");
197
+ const now = Date.now();
198
+ const range = IDBKeyRange.upperBound(now);
199
+ const cursorRequest = index.openCursor(range);
200
+
201
+ cursorRequest.onerror = () => reject(cursorRequest.error);
202
+ cursorRequest.onsuccess = (event) => {
203
+ const cursor = event.target.result;
204
+ if (cursor) {
205
+ cursor.delete();
206
+ cursor.continue();
207
+ }
208
+ };
209
+
210
+ tx.oncomplete = () => resolve();
211
+ tx.onerror = () => reject(tx.error);
212
+ });
213
+ }
214
+
215
+ // Utilidades opcionales y visibles (por si las quieres usar en consola)
216
+ export function closeDb() {
217
+ if (PC.db) {
218
+ try { PC.db.close(); } catch { }
219
+ PC.db = null;
220
+ }
221
+ }
222
+
223
+ export function deleteDatabase(dbName) {
224
+ return new Promise((resolve, reject) => {
225
+ // Close current handle if points to this DB
226
+ if (PC.db && PC.db.name === dbName) {
227
+ try { PC.db.close(); } catch { }
228
+ PC.db = null;
229
+ }
230
+ const req = indexedDB.deleteDatabase(dbName);
231
+ req.onblocked = () => reject(new Error("Delete blocked by another tab or worker"));
232
+ req.onerror = () => reject(req.error);
233
+ req.onsuccess = () => resolve();
234
+ });
235
+ }
236
+
237
+ // Attach a frozen, public API (no private state)
133
238
  (function attachPublicAPI(global) {
134
- const root = (global.PrefConfigurator ??= {});
135
239
  const storage = {
136
240
  initDb,
137
241
  saveModel,
@@ -139,10 +243,12 @@ export async function clearAll(storeName) {
139
243
  getAllModels,
140
244
  deleteModel,
141
245
  clearAll,
246
+ cleanExpiredModels,
142
247
  downloadFileFromBytes,
248
+ // extras
249
+ closeDb,
250
+ deleteDatabase,
143
251
  };
144
-
145
- // versionado del módulo público
146
- root.version = root.version ?? "1.0.0";
147
- root.storage = Object.freeze(storage);
252
+ // free to inspect PC.db in devtools
253
+ global.PrefConfigurator.storage = Object.freeze(storage);
148
254
  })(globalThis);