77 lines
3.1 KiB
JavaScript
77 lines
3.1 KiB
JavaScript
/* ============================================================================
|
|
* device-db.js — this device's own file registry (IndexedDB "ytp-device")
|
|
*
|
|
* One record per saved video, next to the bytes in OPFS:
|
|
* { videoId, cid, size, savedAt, lastCheckedAt, state }
|
|
* cid SHA-256 of the stored file (P2P content id) or null
|
|
* state 'verified' hash computed on save and equal to the server's
|
|
* 'unverified' hash computed, but the server sent none to compare
|
|
* 'unhashed' saved before hashing existed / main-thread fallback
|
|
* The P2P client (p2p-client.js) reads this to report holdings; OPFS stays the
|
|
* source of truth for what is playable (a record without a file is ignored).
|
|
* Every call resolves (null / [] on failure) — private windows can block IDB.
|
|
* See docs/p2p-architecture.md.
|
|
* ========================================================================== */
|
|
(function () {
|
|
'use strict';
|
|
|
|
const DB_NAME = 'ytp-device';
|
|
const VERSION = 1;
|
|
let _open = null;
|
|
|
|
function open() {
|
|
if (!_open) {
|
|
_open = new Promise((resolve, reject) => {
|
|
const r = indexedDB.open(DB_NAME, VERSION);
|
|
r.onupgradeneeded = () => {
|
|
const d = r.result;
|
|
if (!d.objectStoreNames.contains('files')) {
|
|
const s = d.createObjectStore('files', { keyPath: 'videoId' });
|
|
s.createIndex('cid', 'cid', { unique: false });
|
|
}
|
|
};
|
|
r.onsuccess = () => resolve(r.result);
|
|
r.onerror = () => reject(r.error);
|
|
r.onblocked = () => reject(new Error('device-db blocked'));
|
|
});
|
|
_open.catch(() => { _open = null; });
|
|
}
|
|
return _open;
|
|
}
|
|
|
|
const done = (req) => new Promise((resolve, reject) => {
|
|
req.onsuccess = () => resolve(req.result);
|
|
req.onerror = () => reject(req.error);
|
|
});
|
|
|
|
async function store(mode) {
|
|
const d = await open();
|
|
return d.transaction('files', mode).objectStore('files');
|
|
}
|
|
|
|
async function safe(fn, fallback) {
|
|
try { return await fn(); } catch { return fallback; }
|
|
}
|
|
|
|
window.DeviceDB = {
|
|
isSupported: () => typeof indexedDB !== 'undefined',
|
|
putFile: (rec) => safe(async () => {
|
|
if (!rec || !rec.videoId) return null;
|
|
await done((await store('readwrite')).put({
|
|
videoId: String(rec.videoId),
|
|
cid: rec.cid || null,
|
|
size: Number(rec.size) || 0,
|
|
savedAt: Number(rec.savedAt) || Date.now(),
|
|
lastCheckedAt: Number(rec.lastCheckedAt) || Date.now(),
|
|
state: rec.state || 'unhashed',
|
|
}));
|
|
return true;
|
|
}, null),
|
|
getFile: (videoId) => safe(async () => (await done((await store('readonly')).get(String(videoId)))) || null, null),
|
|
getByCid: (cid) => safe(async () => (await done((await store('readonly')).index('cid').get(String(cid)))) || null, null),
|
|
listFiles: () => safe(async () => (await done((await store('readonly')).getAll())) || [], []),
|
|
deleteFile: (videoId) => safe(async () => { await done((await store('readwrite')).delete(String(videoId))); return true; }, null),
|
|
clear: () => safe(async () => { await done((await store('readwrite')).clear()); return true; }, null),
|
|
};
|
|
}());
|