fix(lint): change hydration guard and entity registry rules from warn to error

- Change local-rules/require-hydration-guard from warn to error
- Change local-rules/require-entity-registry from warn to error
- Improve require-hydration-guard rule to skip effects with { dispatch: false }
  since they only perform side effects (audio, UI) and never dispatch actions
- Add skipWhileApplyingRemoteOps() guard to autoShowOverlay$ effect
- Add skipWhileApplyingRemoteOps() guard to triggerIdleWhenEnabled$ effect
- Add eslint-disable comments to test files that intentionally test
  unknown entity type handling
This commit is contained in:
Johannes Millan 2025-12-29 22:35:40 +01:00
parent 55484faffd
commit 8408ac5c0e
6 changed files with 37 additions and 2 deletions

View file

@ -25,8 +25,8 @@
],
"plugins": ["@typescript-eslint", "prettier", "prefer-arrow", "local-rules"],
"rules": {
"local-rules/require-hydration-guard": "warn",
"local-rules/require-entity-registry": "warn",
"local-rules/require-hydration-guard": "error",
"local-rules/require-entity-registry": "error",
"@typescript-eslint/no-explicit-any": 0,
"@typescript-eslint/ban-ts-comment": 0,
"@typescript-eslint/no-empty-function": 0,

View file

@ -24,6 +24,8 @@
* `withLatestFrom(this.store.select(...))` to get current state
* - Selectors inside operator callbacks (e.g., inside `switchMap`)
* - Selectors passed to `withLatestFrom`, `combineLatest`, etc. as secondary sources
* - Effects with `{ dispatch: false }` option - they only perform side effects (audio, UI)
* and never dispatch actions that could create duplicate operations during sync
*
* ## Examples
*
@ -352,6 +354,32 @@ module.exports = {
return results;
};
/**
* Check if the createEffect options object has dispatch: false
*/
const hasDispatchFalse = (node) => {
// node is a CallExpression for createEffect()
// Second argument is the options object: createEffect(() => ..., { dispatch: false })
if (node.arguments.length < 2) return false;
const options = node.arguments[1];
if (!options || options.type !== 'ObjectExpression') return false;
for (const prop of options.properties) {
if (
prop.type === 'Property' &&
prop.key.type === 'Identifier' &&
prop.key.name === 'dispatch' &&
prop.value.type === 'Literal' &&
prop.value.value === false
) {
return true;
}
}
return false;
};
/**
* Check if a createEffect call contains unguarded selector usage
*/
@ -359,6 +387,9 @@ module.exports = {
// node is a CallExpression for createEffect()
if (node.arguments.length === 0) return;
// Skip effects with { dispatch: false } - they don't dispatch actions during sync
if (hasDispatchFalse(node)) return;
const effectFn = node.arguments[0];
if (!effectFn) return;

View file

@ -61,6 +61,7 @@ export class FocusModeEffects {
this.store.select(selectFocusModeConfig),
this.store.select(selectIsFocusModeEnabled),
]).pipe(
skipWhileApplyingRemoteOps(),
switchMap(([cfg, isFocusModeEnabled]) =>
isFocusModeEnabled && cfg?.isSyncSessionWithTracking && !cfg?.isStartInBackground
? this.taskService.currentTaskId$.pipe(

View file

@ -107,6 +107,7 @@ export class IdleEffects {
// while the dialog is open, preventing the flickering between two different values.
triggerIdleWhenEnabled$ = createEffect(() =>
this._store.select(selectIdleConfig).pipe(
skipWhileApplyingRemoteOps(),
switchMap(
({
isEnableIdleTimeTracking,

View file

@ -513,6 +513,7 @@ describe('LWW Update Store Application Integration', () => {
type: '[UNKNOWN_TYPE] LWW Update',
id: 'unknown1',
title: 'Unknown',
// eslint-disable-next-line local-rules/require-entity-registry -- intentionally testing unknown entity type handling
meta: { isPersistent: true, entityType: 'UNKNOWN_TYPE', entityId: 'unknown1' },
};

View file

@ -307,6 +307,7 @@ describe('lwwUpdateMetaReducer', () => {
type: '[UNKNOWN_ENTITY] LWW Update',
id: 'unknown-1',
title: 'Updated Title',
// eslint-disable-next-line local-rules/require-entity-registry -- intentionally testing unknown entity type handling
meta: { isPersistent: true, entityType: 'UNKNOWN_ENTITY', entityId: 'unknown-1' },
};