Apply sync incrementally so unchanged tunnels stay running
CI / Build & Test (push) Successful in 4m21s

- Add getStateSnapshot, applySnapshotData, setStateSaveSuppressed to holesail-manager
- Sync diffs snapshot vs current state and only removes/adds/updates changed tunnels
- Suppress saves during apply and write state.json once at the end
- Fall back to full cleanup + restore when incremental APIs are unavailable
This commit is contained in:
Raven Scott
2026-03-15 03:28:47 -04:00
parent cce71920e9
commit 0f4b82681d
2 changed files with 177 additions and 4 deletions
+56
View File
@@ -26,6 +26,16 @@ function emit(event, payload) { if (eventEmit) eventEmit(event, payload); }
// Collects current state from all sub-modules and persists it.
let onStateSaved = null;
let stateSaveSuppressed = false;
/**
* When true, saveState() is a no-op. Used during sync apply so only the final
* state is written once by the sync-manager.
* @param {boolean} v
*/
function setStateSaveSuppressed(v) {
stateSaveSuppressed = !!v;
}
/**
* Register a callback invoked after each saveState() with the snapshot object.
@@ -37,6 +47,7 @@ function setOnStateSaved(fn) {
}
function saveState() {
if (stateSaveSuppressed) return;
const serversList = serversModule.getServers().map(s => ({
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
}));
@@ -60,6 +71,48 @@ function saveState() {
if (onStateSaved) onStateSaved(snapshot);
}
/**
* Return current in-memory state in the same shape as saveState() snapshot.
* Used by sync-manager to diff and apply only changes.
* @returns {{ version: number, settings: object, nextServerId: number, nextServiceTunnelId: number, servers: Array, virtualHosts: Array, serviceTunnels: Array, sshConnections: Array, rdpConnections: Array }}
*/
function getStateSnapshot() {
const serversList = serversModule.getServers().map(s => ({
id: s.id, port: s.port, host: s.host, secure: s.secure, udp: s.udp || false, label: s.label || ''
}));
const virtualHostsList = vhostsModule.getVirtualHosts()
.map(v => ({ hostname: v.hostname, hsUrl: v.hsUrl, useTls: v.useTls === true }));
const serviceTunnelsList = svcModule.getServiceTunnels().map(t => ({
id: t.id, label: t.label, hsUrl: t.hsUrl, localPort: t.localPort
}));
return {
version: 2,
settings: settingsModule.getSettings(),
nextServerId: serversModule.getNextServerId(),
nextServiceTunnelId: svcModule.getNextServiceTunnelId(),
servers: serversList,
virtualHosts: virtualHostsList,
serviceTunnels: serviceTunnelsList,
sshConnections: connectionsModule.getSshConnections(),
rdpConnections: connectionsModule.getRdpConnections()
};
}
/**
* Apply non-tunnel state from a snapshot (settings, connection lists, ID counters).
* Does not start/stop tunnels; used by sync-manager before applying tunnel diffs.
* @param {object} snapshot - Snapshot with settings, sshConnections, rdpConnections, nextServerId, nextServiceTunnelId
*/
function applySnapshotData(snapshot) {
if (!snapshot || typeof snapshot !== 'object') return;
if (snapshot.settings != null) settingsModule.applyLoaded(snapshot.settings);
const ssh = Array.isArray(snapshot.sshConnections) ? snapshot.sshConnections : [];
const rdp = Array.isArray(snapshot.rdpConnections) ? snapshot.rdpConnections : [];
connectionsModule.applyLoaded(ssh, rdp);
if (typeof snapshot.nextServerId === 'number') serversModule.applyLoaded(snapshot.nextServerId);
if (typeof snapshot.nextServiceTunnelId === 'number') svcModule.applyLoaded(snapshot.nextServiceTunnelId);
}
// Inject the shared saveState and emit callbacks into each sub-module
settingsModule.init(saveState);
connectionsModule.init(saveState);
@@ -127,6 +180,9 @@ module.exports = {
setStoragePath,
ensureStorageDir,
setOnStateSaved,
setStateSaveSuppressed,
getStateSnapshot,
applySnapshotData,
// Settings
getSettings: settingsModule.getSettings,
updateSettings: settingsModule.updateSettings,