139 lines
4.4 KiB
JavaScript
139 lines
4.4 KiB
JavaScript
'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;
|
|
})();
|