init
This commit is contained in:
+138
@@ -0,0 +1,138 @@
|
||||
'use strict';
|
||||
|
||||
// IndexedDB-backed persistence for profiles and settings — the browser-side
|
||||
// replacement for the old file store. IndexedDB (not localStorage) is used
|
||||
// because uploaded .srs rule-set files can exceed localStorage's ~5MB limit.
|
||||
//
|
||||
// Shapes match the former Go store exactly:
|
||||
// Profile { id, name, config, inputs, createdAt, updatedAt, ruleSetFiles }
|
||||
// Settings { dohServer }
|
||||
//
|
||||
// Exposed as window.singvisStorage (app.js is a classic script).
|
||||
|
||||
(function () {
|
||||
const DB_NAME = 'singvis';
|
||||
const DB_VERSION = 1;
|
||||
const STORE_PROFILES = 'profiles';
|
||||
const STORE_META = 'meta';
|
||||
const SETTINGS_KEY = 'settings';
|
||||
|
||||
const DEFAULT_SETTINGS = { dohServer: 'https://1.1.1.1/dns-query' };
|
||||
|
||||
let dbPromise = null;
|
||||
|
||||
function openDB() {
|
||||
if (dbPromise) return dbPromise;
|
||||
dbPromise = new Promise((resolve, reject) => {
|
||||
const req = indexedDB.open(DB_NAME, DB_VERSION);
|
||||
req.onupgradeneeded = () => {
|
||||
const db = req.result;
|
||||
if (!db.objectStoreNames.contains(STORE_PROFILES)) {
|
||||
db.createObjectStore(STORE_PROFILES, { keyPath: 'id' });
|
||||
}
|
||||
if (!db.objectStoreNames.contains(STORE_META)) {
|
||||
db.createObjectStore(STORE_META);
|
||||
}
|
||||
};
|
||||
req.onsuccess = () => resolve(req.result);
|
||||
req.onerror = () => reject(req.error || new Error('failed to open IndexedDB'));
|
||||
});
|
||||
return dbPromise;
|
||||
}
|
||||
|
||||
// tx runs fn(store) inside a transaction and resolves with `out` (a value fn
|
||||
// can set) once the transaction completes.
|
||||
async function tx(storeName, mode, fn) {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const t = db.transaction(storeName, mode);
|
||||
const store = t.objectStore(storeName);
|
||||
let out;
|
||||
const ret = (v) => { out = v; };
|
||||
Promise.resolve(fn(store, ret)).catch(reject);
|
||||
t.oncomplete = () => resolve(out);
|
||||
t.onerror = () => reject(t.error);
|
||||
t.onabort = () => reject(t.error || new Error('transaction aborted'));
|
||||
});
|
||||
}
|
||||
|
||||
function reqPromise(request) {
|
||||
return new Promise((resolve, reject) => {
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
request.onerror = () => reject(request.error);
|
||||
});
|
||||
}
|
||||
|
||||
function newId() {
|
||||
// Millisecond timestamp plus a short random suffix to avoid collisions when
|
||||
// several profiles are created within the same millisecond.
|
||||
return Date.now().toString(36) + Math.random().toString(36).slice(2, 7);
|
||||
}
|
||||
|
||||
const api = {
|
||||
async listProfiles() {
|
||||
const list = await tx(STORE_PROFILES, 'readonly', async (store, ret) => {
|
||||
ret(await reqPromise(store.getAll()));
|
||||
});
|
||||
const out = list || [];
|
||||
out.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||
return out;
|
||||
},
|
||||
|
||||
async getProfile(id) {
|
||||
const p = await tx(STORE_PROFILES, 'readonly', async (store, ret) => {
|
||||
ret(await reqPromise(store.get(id)));
|
||||
});
|
||||
if (!p) throw new Error('profile not found');
|
||||
return p;
|
||||
},
|
||||
|
||||
// saveProfile creates (when profile.id is empty) or updates a profile,
|
||||
// assigning id/createdAt/updatedAt the same way the old server did.
|
||||
async saveProfile(profile) {
|
||||
const now = Date.now();
|
||||
const p = Object.assign({}, profile);
|
||||
if (!p.id) {
|
||||
p.id = newId();
|
||||
p.createdAt = now;
|
||||
}
|
||||
if (!p.createdAt) p.createdAt = now;
|
||||
p.updatedAt = now;
|
||||
p.ruleSetFiles = p.ruleSetFiles || {};
|
||||
await tx(STORE_PROFILES, 'readwrite', (store) => {
|
||||
store.put(p);
|
||||
});
|
||||
return p;
|
||||
},
|
||||
|
||||
async deleteProfile(id) {
|
||||
await tx(STORE_PROFILES, 'readwrite', (store) => {
|
||||
store.delete(id);
|
||||
});
|
||||
},
|
||||
|
||||
async getSettings() {
|
||||
const s = await tx(STORE_META, 'readonly', async (store, ret) => {
|
||||
ret(await reqPromise(store.get(SETTINGS_KEY)));
|
||||
});
|
||||
const merged = Object.assign({}, DEFAULT_SETTINGS, s || {});
|
||||
if (!merged.dohServer) merged.dohServer = DEFAULT_SETTINGS.dohServer;
|
||||
return merged;
|
||||
},
|
||||
|
||||
async saveSettings(settings) {
|
||||
const merged = Object.assign({}, DEFAULT_SETTINGS, settings || {});
|
||||
if (!merged.dohServer) merged.dohServer = DEFAULT_SETTINGS.dohServer;
|
||||
await tx(STORE_META, 'readwrite', (store) => {
|
||||
store.put(merged, SETTINGS_KEY);
|
||||
});
|
||||
return merged;
|
||||
},
|
||||
|
||||
defaultSettings() {
|
||||
return Object.assign({}, DEFAULT_SETTINGS);
|
||||
},
|
||||
};
|
||||
|
||||
window.singvisStorage = api;
|
||||
})();
|
||||
Reference in New Issue
Block a user