@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 +1 -1
- package/src/gltf-storage.js +176 -70
- package/src/index.js +732 -139
package/package.json
CHANGED
package/src/gltf-storage.js
CHANGED
|
@@ -1,137 +1,241 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
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
|
|
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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
const db =
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
|
36
|
-
timestamp: new Date().toISOString(),
|
|
95
|
+
size: (modelData?.data?.length ?? 0)
|
|
37
96
|
};
|
|
38
97
|
|
|
39
|
-
const
|
|
40
|
-
const store =
|
|
41
|
-
const
|
|
98
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
99
|
+
const store = tx.objectStore(storeName);
|
|
100
|
+
const req = store.put(dataToStore);
|
|
42
101
|
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
return;
|
|
78
|
-
}
|
|
135
|
+
let db;
|
|
136
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
79
137
|
|
|
80
|
-
const
|
|
81
|
-
const store =
|
|
82
|
-
const
|
|
138
|
+
const tx = db.transaction([storeName], "readonly");
|
|
139
|
+
const store = tx.objectStore(storeName);
|
|
140
|
+
const req = store.getAll();
|
|
83
141
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
const results =
|
|
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
|
-
|
|
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
|
-
|
|
103
|
-
|
|
104
|
-
return;
|
|
105
|
-
}
|
|
160
|
+
let db;
|
|
161
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
106
162
|
|
|
107
|
-
const
|
|
108
|
-
const store =
|
|
109
|
-
const
|
|
163
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
164
|
+
const store = tx.objectStore(storeName);
|
|
165
|
+
const req = store.delete(modelId);
|
|
110
166
|
|
|
111
|
-
|
|
112
|
-
|
|
167
|
+
req.onerror = () => reject(req.error);
|
|
168
|
+
req.onsuccess = () => resolve();
|
|
113
169
|
});
|
|
114
170
|
}
|
|
115
171
|
|
|
116
|
-
// Limpiar toda la
|
|
172
|
+
// Limpiar toda la store
|
|
117
173
|
export async function clearAll(storeName) {
|
|
118
174
|
return new Promise((resolve, reject) => {
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
return;
|
|
122
|
-
}
|
|
175
|
+
let db;
|
|
176
|
+
try { db = _getDbOrThrow(); } catch (e) { reject(e); return; }
|
|
123
177
|
|
|
124
|
-
const
|
|
125
|
-
const store =
|
|
126
|
-
const
|
|
178
|
+
const tx = db.transaction([storeName], "readwrite");
|
|
179
|
+
const store = tx.objectStore(storeName);
|
|
180
|
+
const req = store.clear();
|
|
127
181
|
|
|
128
|
-
|
|
129
|
-
|
|
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
|
-
|
|
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);
|