- Await restorePersistedTunnels in applySyncedState so applyingSync stays true for the full restore. Prevents saveState() during tunnel startup from pushing back to autopass and causing a feedback loop. - Debounce onRemoteUpdate (600ms) so bursts of autopass 'update' events trigger a single apply. - Skip apply when remote state matches current state (canonical fingerprint) to avoid redundant restarts and re-pushes. - Clear remoteUpdateDebounceTimer in cleanup().
This commit is contained in:
@@ -12,6 +12,7 @@ const STATE_KEY = 'holesail-state';
|
||||
const IDENTITY_FILENAME = 'autopass-identity.json';
|
||||
const STATE_FILENAME = 'state.json';
|
||||
const AUTOPASS_DIRNAME = 'autopass';
|
||||
const REMOTE_UPDATE_DEBOUNCE_MS = 600;
|
||||
|
||||
let storageDir = null;
|
||||
let holesailManager = null;
|
||||
@@ -27,6 +28,7 @@ let initPromise = null;
|
||||
let applyingSync = false;
|
||||
let weJustPushed = false;
|
||||
let lastSyncedAt = null;
|
||||
let remoteUpdateDebounceTimer = null;
|
||||
|
||||
function setStoragePath(dir) {
|
||||
storageDir = dir;
|
||||
@@ -141,15 +143,45 @@ async function ensureInitialized() {
|
||||
await initPromise;
|
||||
}
|
||||
|
||||
async function onRemoteUpdate() {
|
||||
/**
|
||||
* Canonical string form of state for comparison (sorted keys at every level).
|
||||
*/
|
||||
function stateFingerprint(obj) {
|
||||
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
|
||||
if (Array.isArray(obj)) return '[' + obj.map(stateFingerprint).join(',') + ']';
|
||||
const keys = Object.keys(obj).sort();
|
||||
const parts = keys.map((k) => JSON.stringify(k) + ':' + stateFingerprint(obj[k]));
|
||||
return '{' + parts.join(',') + '}';
|
||||
}
|
||||
|
||||
function onRemoteUpdate() {
|
||||
if (weJustPushed) return;
|
||||
if (!pass || !holesailManager) return;
|
||||
if (remoteUpdateDebounceTimer) clearTimeout(remoteUpdateDebounceTimer);
|
||||
remoteUpdateDebounceTimer = setTimeout(() => {
|
||||
remoteUpdateDebounceTimer = null;
|
||||
applyRemoteUpdate().catch((e) => {
|
||||
if (process.stderr) process.stderr.write('[sync-manager] applyRemoteUpdate failed: ' + e.message + '\n');
|
||||
});
|
||||
}, REMOTE_UPDATE_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
async function applyRemoteUpdate() {
|
||||
if (weJustPushed || !pass || !holesailManager) return;
|
||||
try {
|
||||
const entry = await pass.get(STATE_KEY);
|
||||
if (!entry || entry.value == null) return;
|
||||
const str = typeof entry.value === 'string' ? entry.value : (entry.value && entry.value.toString ? entry.value.toString() : '');
|
||||
if (!str) return;
|
||||
const snapshot = JSON.parse(str);
|
||||
const statePath = getStateFilePath();
|
||||
if (statePath && fs.existsSync(statePath)) {
|
||||
try {
|
||||
const currentRaw = fs.readFileSync(statePath, 'utf8');
|
||||
const current = JSON.parse(currentRaw);
|
||||
if (stateFingerprint(current) === stateFingerprint(snapshot)) return;
|
||||
} catch (_) {}
|
||||
}
|
||||
await applySyncedState(snapshot);
|
||||
lastSyncedAt = Date.now();
|
||||
if (emitEvent) emitEvent('syncApplied', {});
|
||||
@@ -160,7 +192,8 @@ async function onRemoteUpdate() {
|
||||
|
||||
/**
|
||||
* Write state snapshot to state.json and run the same restore flow as backup restore.
|
||||
* Does not touch certs.
|
||||
* Does not touch certs. Keeps applyingSync true until all tunnels are restored so
|
||||
* saveState() calls during restore do not push back to autopass and cause a loop.
|
||||
*/
|
||||
async function applySyncedState(snapshot) {
|
||||
if (!holesailManager || !setTunnelsRestoredPromise || !restorePersistedTunnels) return;
|
||||
@@ -174,9 +207,11 @@ async function applySyncedState(snapshot) {
|
||||
fs.writeFileSync(statePath, json, 'utf8');
|
||||
await holesailManager.cleanup().catch(() => {});
|
||||
const restored = holesailManager.restorePersistedState();
|
||||
setTunnelsRestoredPromise(
|
||||
restorePersistedTunnels(holesailManager, restored).catch((e) => log('Post-sync tunnel restore failed:', e.message))
|
||||
const restorePromise = restorePersistedTunnels(holesailManager, restored).catch((e) =>
|
||||
log('Post-sync tunnel restore failed:', e.message)
|
||||
);
|
||||
setTunnelsRestoredPromise(restorePromise);
|
||||
await restorePromise;
|
||||
} finally {
|
||||
applyingSync = false;
|
||||
}
|
||||
@@ -287,6 +322,10 @@ async function cleanup() {
|
||||
initPromise = null;
|
||||
weJustPushed = false;
|
||||
applyingSync = false;
|
||||
if (remoteUpdateDebounceTimer) {
|
||||
clearTimeout(remoteUpdateDebounceTimer);
|
||||
remoteUpdateDebounceTimer = null;
|
||||
}
|
||||
if (pass && pass.off) pass.off('update', onRemoteUpdate);
|
||||
await closePass();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user