@preference-sl/pref-viewer 2.10.0-beta.5 → 2.10.0-beta.7

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