feat: Add hardened error handling and retries (#2451)

- Closes #1830
 - Closes #2099
This commit is contained in:
Dermot Duffy
2026-06-30 17:45:12 -07:00
committed by dermotduffy
parent 4bc787e2b7
commit 47bcce93d3
182 changed files with 7877 additions and 4043 deletions
+22 -19
View File
@@ -1,4 +1,5 @@
import { isEqual } from 'lodash-es';
import { ConfigParseError } from './error.js';
import { copyConfig, isConfigUpgradeable } from '../../config/management.js';
import { setProfiles } from '../../config/profiles/set-profiles.js';
import {
@@ -56,7 +57,7 @@ export class ConfigManager {
public setConfig(inputConfig?: RawAdvancedCameraCardConfig): void {
if (!inputConfig) {
throw new Error(localize('error.invalid_configuration'));
throw new ConfigParseError(localize('error.invalid_configuration'));
}
const parseResult = advancedCameraCardConfigSchema.safeParse(inputConfig);
@@ -67,7 +68,7 @@ export class ConfigManager {
if (isConfigUpgradeable(inputConfig)) {
upgradeMessage = `${localize('error.upgrade_available')}. `;
}
throw new Error(
throw new ConfigParseError(
upgradeMessage +
`${localize('error.invalid_configuration')}: ` +
(hint ?? localize('error.invalid_configuration_no_hint')),
@@ -109,7 +110,6 @@ export class ConfigManager {
this._api.getInitializationManager().uninitialize(InitializationAspect.VIEW);
this._api.getViewManager().reset();
this._api.getMessageManager().reset();
this._api.getStatusBarItemManager().removeAllDynamicStatusBarItems();
this._processOverrideConfig();
@@ -118,11 +118,28 @@ export class ConfigManager {
}
private _processOverrideConfig(): void {
const overriddenConfig = this._getOverriddenConfig();
/* istanbul ignore if: No (current) way to reach this code -- @preserve */
if (!this._config) {
return;
}
let overriddenConfig: AdvancedCameraCardConfig;
try {
overriddenConfig = this._overridesManager.getConfig(this._config);
} catch (ev) {
this._api.getIssueManager().trigger('config_error', { error: ev });
return;
}
// Clear any prior config error on success. This method runs both from
// setConfig() and from condition-change-driven override recomputations;
// resetting here ensures a transient override error is cleared when
// conditions change to produce a valid config again.
this._api.getIssueManager().reset('config_error');
// Save on Lit re-rendering costs by only updating the configuration if it
// actually changes.
if (!overriddenConfig || isEqual(overriddenConfig, this._overriddenConfig)) {
if (isEqual(overriddenConfig, this._overriddenConfig)) {
return;
}
@@ -189,20 +206,6 @@ export class ConfigManager {
/* async */ this._initializeBackgroundAndUpdate(previousConfig);
}
private _getOverriddenConfig(): AdvancedCameraCardConfig | null {
/* istanbul ignore if: No (current) way to reach this code -- @preserve */
if (!this._config) {
return null;
}
try {
return this._overridesManager.getConfig(this._config);
} catch (ev) {
this._api.getMessageManager().setErrorIfHigherPriority(ev);
return null;
}
}
/**
* Initialize config dependent items in the background. For items that the
* card hard requires, use InitializationManager instead.
+5
View File
@@ -0,0 +1,5 @@
import { AdvancedCameraCardError } from '../../types.js';
class ConfigError extends AdvancedCameraCardError {}
export class ConfigParseError extends ConfigError {}
+1 -1
View File
@@ -7,6 +7,6 @@ export const setFoldersFromConfig = (api: CardConfigLoaderAPI): void => {
.getFoldersManager()
.addFolders(api.getConfigManager().getConfig()?.folders ?? []);
} catch (ev) {
api.getMessageManager().setErrorIfHigherPriority(ev);
api.getIssueManager().trigger('config_error', { error: ev });
}
};
@@ -1,4 +1,4 @@
import { merge } from 'lodash-es';
import { get, merge } from 'lodash-es';
import { ConditionsManager } from '../../conditions/conditions-manager';
import { ConditionStateManagerReadonlyInterface } from '../../conditions/types';
import {
@@ -92,9 +92,24 @@ export class OverridesManager {
const parseResult = advancedCameraCardConfigSchema.safeParse(output);
if (!parseResult.success) {
// Surface one co-located failure object per Zod issue — path, the value
// the user actually wrote, and the most informative "expected" field for
// this issue code. Avoids dumping the full merged config (which is
// mostly schema defaults the user never wrote) and keeps the reader from
// cross-referencing two parallel arrays.
const failures = parseResult.error.issues.map((issue) => ({
path: issue.path.map(String).join('.'),
received: get(output, issue.path),
expected:
issue.code === 'invalid_value'
? issue.values
: issue.code === 'invalid_type'
? issue.expected
: issue.message,
}));
throw new OverrideConfigurationError(
localize('error.invalid_configuration_override'),
[parseResult.error.issues, output],
{ failures },
);
}
return parseResult.data;