mirror of
https://github.com/HeyPuter/gex.git
synced 2026-11-13 20:12:43 -10:00
A reload does not wait for the snapshot pagehide starts, and the 30 s timer had often not come round: a save made seconds before a reload was gone after it. The global kernel now counts the changes the program finishes under the persist root (k_fs_watch, k_fs_saves): a close of a handle that wrote, created or truncated, a rename into or out of it, an unlink. The persist worker reads the count every 250 ms and snapshots 1 s after the last change, or 5 s after the first unsnapshotted one if they keep coming. The timer, visibilitychange and pagehide stay as the backstop. With no root watched nothing is counted; with nothing saved the cost is one load per poll. fsync is not counted: in the page it reaches the global kernel through no call of its own, and the close or rename after it is. tests/eject/persist-save: a probe saves through a symlink into /save, by a temporary file fsynced and renamed over the save and by stdio over it, keeps running, and is reloaded 3 s later, three visits for each way. Without the count the second visit finds no save. Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_8b3490cccb4848b495dde1c5cdcacba0
81 lines
3.1 KiB
JavaScript
81 lines
3.1 KiB
JavaScript
// A guest directory that outlives the page (an ejected program's saves and
|
|
// settings), kept in IndexedDB.
|
|
//
|
|
// - `restore(db, root)`, before the program starts: the files stored last
|
|
// time, to lay over the bundle's.
|
|
// - `persist(db, root, spec)`, once it runs: a Worker (persist-worker.js)
|
|
// snapshots `root` from the global kernel's filesystem about a second
|
|
// after the program finishes a change under it (a close after a write, a
|
|
// rename, an unlink), and also every 30 s, when the page is hidden and
|
|
// when it is left, storing what changed. A reload does not wait for the
|
|
// last of these, so the first is what keeps a save.
|
|
//
|
|
// What a snapshot sees is whatever the program has written so far; a save
|
|
// half-written at the instant of a snapshot is stored half-written, and the
|
|
// next snapshot fixes it (IDBFS's syncfs has the same window).
|
|
|
|
import { openDb, readAll } from './persist-db.js';
|
|
import { logOn } from '@gex/log.mjs';
|
|
|
|
export async function restore(dbName, root) {
|
|
try {
|
|
const db = await openDb(dbName);
|
|
const all = await readAll(db);
|
|
db.close();
|
|
const prefix = `${root.replace(/\/+$/, '')}/`;
|
|
return all.filter(([p]) => p.startsWith(prefix));
|
|
} catch (e) {
|
|
console.warn(`[persist] nothing restored: ${e?.message ?? e}`);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export function persist(dbName, root, spec, { every = 30000, onSync = null } = {}) {
|
|
if (!spec?.module || !spec?.memory) {
|
|
console.warn('[persist] no global kernel (no SharedArrayBuffer): saves will not outlive the page');
|
|
return null;
|
|
}
|
|
const worker = new Worker(new URL('./persist-worker.js', import.meta.url), { type: 'module', name: 'persist' });
|
|
let seq = 0;
|
|
let busy = false;
|
|
const waiters = new Map();
|
|
worker.onmessage = (e) => {
|
|
const m = e.data;
|
|
if (m.error && !('synced' in m)) console.warn(`[persist] ${m.error}`);
|
|
if (m.auto) {
|
|
if ((m.wrote || m.removed) && logOn('debug')) console.log(`[persist] after a save: stored ${m.wrote} file(s) (${m.bytes} bytes), removed ${m.removed}; ${m.files} under ${root}`);
|
|
onSync?.(m);
|
|
}
|
|
if ('synced' in m) {
|
|
busy = false;
|
|
if (m.error) console.warn(`[persist] snapshot failed: ${m.error}`);
|
|
else if ((m.wrote || m.removed) && logOn('debug')) console.log(`[persist] stored ${m.wrote} file(s) (${m.bytes} bytes), removed ${m.removed}; ${m.files} under ${root}`);
|
|
onSync?.(m);
|
|
waiters.get(m.synced)?.(m);
|
|
waiters.delete(m.synced);
|
|
}
|
|
};
|
|
worker.postMessage({ spec: { module: spec.module, memory: spec.memory }, root, db: dbName });
|
|
const sync = () => {
|
|
// One snapshot at a time; a request while one runs is left to it.
|
|
if (busy) return Promise.resolve(null);
|
|
busy = true;
|
|
const id = ++seq;
|
|
const done = new Promise((resolve) => waiters.set(id, resolve));
|
|
worker.postMessage({ sync: id });
|
|
return done;
|
|
};
|
|
const timer = setInterval(sync, every);
|
|
addEventListener('pagehide', sync);
|
|
document.addEventListener('visibilitychange', () => {
|
|
if (document.hidden) sync();
|
|
});
|
|
return {
|
|
sync,
|
|
stop() {
|
|
clearInterval(timer);
|
|
worker.terminate();
|
|
},
|
|
};
|
|
}
|