mirror of
https://github.com/unraid/api.git
synced 2025-12-31 13:39:52 -06:00
fix: recreate watcher on path change (#1203)
<!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit - **Bug Fixes** - Improved the notifications system by refreshing the monitoring process only when configuration changes occur, leading to a more reliable experience. - **Chores** - Updated internal synchronization timestamps in multiple files to ensure consistency and accurate tracking of recent events. - Removed logging functionality for notifications state to streamline the logging process. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -8,7 +8,6 @@ import { dynamicRemoteAccessReducer } from '@app/store/modules/dynamic-remote-ac
|
|||||||
import { dynamix } from '@app/store/modules/dynamix.js';
|
import { dynamix } from '@app/store/modules/dynamix.js';
|
||||||
import { emhttp } from '@app/store/modules/emhttp.js';
|
import { emhttp } from '@app/store/modules/emhttp.js';
|
||||||
import { mothership } from '@app/store/modules/minigraph.js';
|
import { mothership } from '@app/store/modules/minigraph.js';
|
||||||
import { notificationReducer } from '@app/store/modules/notifications.js';
|
|
||||||
import { paths } from '@app/store/modules/paths.js';
|
import { paths } from '@app/store/modules/paths.js';
|
||||||
import { registration } from '@app/store/modules/registration.js';
|
import { registration } from '@app/store/modules/registration.js';
|
||||||
import { remoteGraphQLReducer } from '@app/store/modules/remote-graphql.js';
|
import { remoteGraphQLReducer } from '@app/store/modules/remote-graphql.js';
|
||||||
@@ -23,7 +22,6 @@ export const store = configureStore({
|
|||||||
emhttp: emhttp.reducer,
|
emhttp: emhttp.reducer,
|
||||||
registration: registration.reducer,
|
registration: registration.reducer,
|
||||||
remoteGraphQL: remoteGraphQLReducer,
|
remoteGraphQL: remoteGraphQLReducer,
|
||||||
notifications: notificationReducer,
|
|
||||||
cache: cache.reducer,
|
cache: cache.reducer,
|
||||||
docker: docker.reducer,
|
docker: docker.reducer,
|
||||||
upnp: upnp.reducer,
|
upnp: upnp.reducer,
|
||||||
@@ -46,7 +44,6 @@ export const getters = {
|
|||||||
dynamix: () => store.getState().dynamix,
|
dynamix: () => store.getState().dynamix,
|
||||||
emhttp: () => store.getState().emhttp,
|
emhttp: () => store.getState().emhttp,
|
||||||
minigraph: () => store.getState().minigraph,
|
minigraph: () => store.getState().minigraph,
|
||||||
notifications: () => store.getState().notifications,
|
|
||||||
paths: () => store.getState().paths,
|
paths: () => store.getState().paths,
|
||||||
registration: () => store.getState().registration,
|
registration: () => store.getState().registration,
|
||||||
remoteGraphQL: () => store.getState().remoteGraphQL,
|
remoteGraphQL: () => store.getState().remoteGraphQL,
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import { logger } from '@app/core/log.js';
|
|
||||||
import { startAppListening } from '@app/store/listeners/listener-middleware.js';
|
|
||||||
import { clearAllNotifications } from '@app/store/modules/notifications.js';
|
|
||||||
|
|
||||||
export const enableNotificationPathListener = () =>
|
|
||||||
startAppListening({
|
|
||||||
predicate(_, currentState, previousState) {
|
|
||||||
if (
|
|
||||||
currentState.dynamix.notify?.path !== '' &&
|
|
||||||
previousState.dynamix.notify?.path !== currentState.dynamix.notify?.path
|
|
||||||
) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
},
|
|
||||||
async effect(_, { dispatch }) {
|
|
||||||
logger.debug('Notification Path Changed or Loaded, Recreating Watcher');
|
|
||||||
dispatch(clearAllNotifications());
|
|
||||||
},
|
|
||||||
});
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
import type { Notification } from '@app/graphql/generated/api/types.js';
|
|
||||||
import { logger } from '@app/core/log.js';
|
|
||||||
import { pubsub, PUBSUB_CHANNEL } from '@app/core/pubsub.js';
|
|
||||||
import { type NotificationIni } from '@app/core/types/states/notification.js';
|
|
||||||
import { parseConfig } from '@app/core/utils/misc/parse-config.js';
|
|
||||||
import { NotificationSchema } from '@app/graphql/generated/api/operations.js';
|
|
||||||
import { Importance, NotificationType } from '@app/graphql/generated/api/types.js';
|
|
||||||
import { type AppDispatch, type RootState } from '@app/store/index.js';
|
|
||||||
|
|
||||||
interface NotificationState {
|
|
||||||
notifications: Record<string, Notification>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const notificationInitialState: NotificationState = {
|
|
||||||
notifications: {},
|
|
||||||
};
|
|
||||||
|
|
||||||
const fileImportanceToGqlImportance = (importance: NotificationIni['importance']): Importance => {
|
|
||||||
switch (importance) {
|
|
||||||
case 'alert':
|
|
||||||
return Importance.ALERT;
|
|
||||||
case 'warning':
|
|
||||||
return Importance.WARNING;
|
|
||||||
default:
|
|
||||||
return Importance.INFO;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const parseNotificationDateToIsoDate = (unixStringSeconds: string | undefined): string | null => {
|
|
||||||
if (unixStringSeconds && !isNaN(Number(unixStringSeconds))) {
|
|
||||||
return new Date(Number(unixStringSeconds) * 1_000).toISOString();
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const loadNotification = createAsyncThunk<
|
|
||||||
{ id: string; notification: Notification },
|
|
||||||
{ path: string },
|
|
||||||
{ state: RootState; dispatch: AppDispatch }
|
|
||||||
>('notifications/loadNotification', ({ path }) => {
|
|
||||||
const notificationFile = parseConfig<NotificationIni>({
|
|
||||||
filePath: path,
|
|
||||||
type: 'ini',
|
|
||||||
});
|
|
||||||
|
|
||||||
const notification: Notification = {
|
|
||||||
id: path,
|
|
||||||
title: notificationFile.event,
|
|
||||||
subject: notificationFile.subject,
|
|
||||||
description: notificationFile.description ?? '',
|
|
||||||
importance: fileImportanceToGqlImportance(notificationFile.importance),
|
|
||||||
link: notificationFile.link,
|
|
||||||
timestamp: parseNotificationDateToIsoDate(notificationFile.timestamp),
|
|
||||||
type: NotificationType.UNREAD,
|
|
||||||
};
|
|
||||||
const convertedNotification = NotificationSchema().parse(notification);
|
|
||||||
|
|
||||||
if (convertedNotification) {
|
|
||||||
pubsub.publish(PUBSUB_CHANNEL.NOTIFICATION, { notificationAdded: convertedNotification });
|
|
||||||
return { id: path, notification: convertedNotification };
|
|
||||||
}
|
|
||||||
throw new Error('Failed to parse notification');
|
|
||||||
});
|
|
||||||
|
|
||||||
export const notificationsStore = createSlice({
|
|
||||||
name: 'notifications',
|
|
||||||
initialState: notificationInitialState,
|
|
||||||
reducers: {
|
|
||||||
clearNotification: (state, action: PayloadAction<{ path: string }>) => {
|
|
||||||
if (state.notifications[action.payload.path]) {
|
|
||||||
delete state.notifications[action.payload.path];
|
|
||||||
}
|
|
||||||
},
|
|
||||||
clearAllNotifications: (state) => {
|
|
||||||
state.notifications = {};
|
|
||||||
},
|
|
||||||
},
|
|
||||||
extraReducers: (builder) => {
|
|
||||||
builder.addCase(loadNotification.fulfilled, (state, { payload }) => {
|
|
||||||
state.notifications[payload.id] = payload.notification;
|
|
||||||
});
|
|
||||||
builder.addCase(loadNotification.rejected, (_, action) => {
|
|
||||||
logger.debug('Failed to load notification with error %o', action.error);
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const notificationReducer = notificationsStore.reducer;
|
|
||||||
export const { clearNotification, clearAllNotifications } = notificationsStore.actions;
|
|
||||||
@@ -41,10 +41,6 @@ export const startStoreSync = async () => {
|
|||||||
join(state.paths.states, 'graphql.log'),
|
join(state.paths.states, 'graphql.log'),
|
||||||
JSON.stringify(state.minigraph, null, 2)
|
JSON.stringify(state.minigraph, null, 2)
|
||||||
);
|
);
|
||||||
writeFileSync(
|
|
||||||
join(state.paths.states, 'notifications.log'),
|
|
||||||
JSON.stringify(state.notifications, null, 2)
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
lastState = state;
|
lastState = state;
|
||||||
|
|||||||
@@ -33,6 +33,10 @@ import { batchProcess, formatDatetime, isFulfilled, isRejected, unraidTimestamp
|
|||||||
export class NotificationsService {
|
export class NotificationsService {
|
||||||
private logger = new Logger(NotificationsService.name);
|
private logger = new Logger(NotificationsService.name);
|
||||||
private static watcher: FSWatcher | null = null;
|
private static watcher: FSWatcher | null = null;
|
||||||
|
/**
|
||||||
|
* The path to the notification directory - will be updated if the user changes the notifier path
|
||||||
|
*/
|
||||||
|
private path: string | null = null;
|
||||||
|
|
||||||
private static overview: NotificationOverview = {
|
private static overview: NotificationOverview = {
|
||||||
unread: {
|
unread: {
|
||||||
@@ -50,7 +54,8 @@ export class NotificationsService {
|
|||||||
};
|
};
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
NotificationsService.watcher = this.getNotificationsWatcher();
|
this.path = getters.dynamix().notify!.path;
|
||||||
|
void this.getNotificationsWatcher(this.path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,6 +68,13 @@ export class NotificationsService {
|
|||||||
*/
|
*/
|
||||||
public paths(): Record<'basePath' | NotificationType, string> {
|
public paths(): Record<'basePath' | NotificationType, string> {
|
||||||
const basePath = getters.dynamix().notify!.path;
|
const basePath = getters.dynamix().notify!.path;
|
||||||
|
|
||||||
|
if (this.path !== basePath) {
|
||||||
|
// Recreate the watcher with force = true
|
||||||
|
void this.getNotificationsWatcher(basePath, true);
|
||||||
|
this.path = basePath;
|
||||||
|
}
|
||||||
|
|
||||||
const makePath = (type: NotificationType) => join(basePath, type.toLowerCase());
|
const makePath = (type: NotificationType) => join(basePath, type.toLowerCase());
|
||||||
return {
|
return {
|
||||||
basePath,
|
basePath,
|
||||||
@@ -78,12 +90,11 @@ export class NotificationsService {
|
|||||||
* events to their event handlers.
|
* events to their event handlers.
|
||||||
*------------------------------------------------------------------------**/
|
*------------------------------------------------------------------------**/
|
||||||
|
|
||||||
private getNotificationsWatcher() {
|
private async getNotificationsWatcher(basePath: string, recreate = false): Promise<FSWatcher> {
|
||||||
const { basePath } = this.paths();
|
if (NotificationsService.watcher && !recreate) {
|
||||||
|
|
||||||
if (NotificationsService.watcher) {
|
|
||||||
return NotificationsService.watcher;
|
return NotificationsService.watcher;
|
||||||
}
|
}
|
||||||
|
await NotificationsService.watcher?.close().catch((e) => this.logger.error(e));
|
||||||
|
|
||||||
NotificationsService.watcher = watch(basePath, { usePolling: CHOKIDAR_USEPOLLING }).on(
|
NotificationsService.watcher = watch(basePath, { usePolling: CHOKIDAR_USEPOLLING }).on(
|
||||||
'add',
|
'add',
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
1739911366509
|
1740588065597
|
||||||
@@ -1 +1 @@
|
|||||||
1739911365729
|
1740588063495
|
||||||
@@ -1 +1 @@
|
|||||||
1739911366308
|
1740588063858
|
||||||
@@ -1 +1 @@
|
|||||||
1739911366763
|
1740588065854
|
||||||
Reference in New Issue
Block a user