@preference-sl/pref-viewer 2.10.0-beta.5 → 2.10.0-beta.6
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 +1 -1
- package/src/gltf-storage.js +226 -121
package/package.json
CHANGED
package/src/gltf-storage.js
CHANGED
|
@@ -1,148 +1,253 @@
|
|
|
1
|
-
//
|
|
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
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
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
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
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
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
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
|
|
173
|
+
// Limpiar toda la store
|
|
117
174
|
export async function clearAll(storeName) {
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
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
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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);
|