Stabilize channels UX, reload flow, and i18n consistency

This commit is contained in:
ashione
2026-03-08 00:30:26 +08:00
parent 4651f8ec56
commit 72585589af
9 changed files with 311 additions and 54 deletions

View File

@@ -220,6 +220,7 @@ export class GatewayManager extends EventEmitter {
}> = new Map();
private deviceIdentity: DeviceIdentity | null = null;
private restartDebounceTimer: NodeJS.Timeout | null = null;
private reloadDebounceTimer: NodeJS.Timeout | null = null;
private lifecycleEpoch = 0;
private deferredRestartPending = false;
private restartInFlight: Promise<void> | null = null;
@@ -640,6 +641,71 @@ export class GatewayManager extends EventEmitter {
}, delayMs);
}
/**
* Ask the Gateway process to reload config in-place when possible.
* Falls back to restart on unsupported platforms or signaling failures.
*/
async reload(): Promise<void> {
if (this.isRestartDeferred()) {
this.markDeferredRestart('reload');
return;
}
if (!this.process?.pid || this.status.state !== 'running') {
logger.warn('Gateway reload requested while not running; falling back to restart');
await this.restart();
return;
}
if (process.platform === 'win32') {
logger.debug('Windows detected, falling back to Gateway restart for reload');
await this.restart();
return;
}
const connectedForMs = this.status.connectedAt
? Date.now() - this.status.connectedAt
: Number.POSITIVE_INFINITY;
// Avoid signaling a process that just came up; it will already read latest config.
if (connectedForMs < 8000) {
logger.info(`Gateway connected ${connectedForMs}ms ago, skipping reload signal`);
return;
}
try {
process.kill(this.process.pid, 'SIGUSR1');
logger.info(`Sent SIGUSR1 to Gateway for config reload (pid=${this.process.pid})`);
// Some gateway builds do not handle SIGUSR1 as an in-process reload.
// If process state doesn't recover quickly, fall back to restart.
await new Promise((resolve) => setTimeout(resolve, 1500));
if (this.status.state !== 'running' || !this.process?.pid) {
logger.warn('Gateway did not stay running after reload signal, falling back to restart');
await this.restart();
}
} catch (error) {
logger.warn('Gateway reload signal failed, falling back to restart:', error);
await this.restart();
}
}
/**
* Debounced reload — coalesces multiple rapid config-change events into one
* in-process reload when possible.
*/
debouncedReload(delayMs = 1200): void {
if (this.reloadDebounceTimer) {
clearTimeout(this.reloadDebounceTimer);
}
logger.debug(`Gateway reload debounced (will fire in ${delayMs}ms)`);
this.reloadDebounceTimer = setTimeout(() => {
this.reloadDebounceTimer = null;
void this.reload().catch((err) => {
logger.warn('Debounced Gateway reload failed:', err);
});
}, delayMs);
}
/**
* Clear all active timers
*/
@@ -660,6 +726,10 @@ export class GatewayManager extends EventEmitter {
clearTimeout(this.restartDebounceTimer);
this.restartDebounceTimer = null;
}
if (this.reloadDebounceTimer) {
clearTimeout(this.reloadDebounceTimer);
this.reloadDebounceTimer = null;
}
}
/**