maintenance

This commit is contained in:
2025-09-04 20:25:15 -04:00
parent 1469c7f52f
commit e8f2df9e69
214 changed files with 8507 additions and 1836 deletions

View File

@@ -0,0 +1,164 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {createAsyncThunk, createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit';
import {Channel} from '@phenixrts/sdk';
import channelService from '../../services/channel.service';
import {RootState} from '../index';
// Publishing state interface
export interface ChannelPublishingState {
channelId: string;
isOnline: boolean;
publisherCount: number;
streamCount: number;
lastUpdated: string;
}
export interface ChannelsPublishingState {
publishingState: ChannelPublishingState[];
isLoading: boolean;
error: string | null;
lastFetched: string | null;
}
const initialState: ChannelsPublishingState = {
publishingState: [],
isLoading: false,
error: null,
lastFetched: null
};
// Selectors
export const channelsPublishingSelector = (state: RootState): ChannelsPublishingState =>
state.channelsPublishing;
export const selectChannelsPublishingState = createSelector(
[channelsPublishingSelector],
(channelsPublishing: ChannelsPublishingState) => channelsPublishing.publishingState
);
export const selectChannelsPublishingLoading = createSelector(
[channelsPublishingSelector],
(channelsPublishing: ChannelsPublishingState) => channelsPublishing.isLoading
);
export const selectChannelPublishingState = createSelector(
[selectChannelsPublishingState, (_: RootState, channelId: string) => channelId],
(publishingStates: ChannelPublishingState[], channelId: string) =>
publishingStates.find(state => state.channelId === channelId)
);
// Async thunks
export const fetchChannelsPublishingState = createAsyncThunk(
'channelsPublishing/fetchChannelsPublishingState',
async (channels: Channel[], {rejectWithValue}) => {
try {
const publishingStates = await Promise.all(
channels.map(async (channel): Promise<ChannelPublishingState> => {
try {
const publisherCount = await channelService.getPublisherCount(channel.channelId);
return {
channelId: channel.channelId,
isOnline: publisherCount > 0,
publisherCount,
streamCount: publisherCount, // Assuming 1:1 for now
lastUpdated: new Date().toISOString()
};
} catch (error) {
// If we can't get publisher count, assume offline
return {
channelId: channel.channelId,
isOnline: false,
publisherCount: 0,
streamCount: 0,
lastUpdated: new Date().toISOString()
};
}
})
);
return publishingStates;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fetch publishing state');
}
}
);
export const updateChannelPublishingState = createAsyncThunk(
'channelsPublishing/updateChannelPublishingState',
async (channelId: string, {rejectWithValue}) => {
try {
const publisherCount = await channelService.getPublisherCount(channelId);
return {
channelId,
isOnline: publisherCount > 0,
publisherCount,
streamCount: publisherCount,
lastUpdated: new Date().toISOString()
} as ChannelPublishingState;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to update publishing state');
}
}
);
// Slice
const channelsPublishingSlice = createSlice({
name: 'channelsPublishing',
initialState,
reducers: {
clearPublishingState: (state) => {
state.publishingState = [];
state.error = null;
},
setPublishingError: (state, action: PayloadAction<string | null>) => {
state.error = action.payload;
},
updateChannelState: (state, action: PayloadAction<ChannelPublishingState>) => {
const index = state.publishingState.findIndex(
item => item.channelId === action.payload.channelId
);
if (index >= 0) {
state.publishingState[index] = action.payload;
} else {
state.publishingState.push(action.payload);
}
}
},
extraReducers: (builder) => {
builder
.addCase(fetchChannelsPublishingState.pending, (state) => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchChannelsPublishingState.fulfilled, (state, action) => {
state.isLoading = false;
state.publishingState = action.payload;
state.lastFetched = new Date().toISOString();
state.error = null;
})
.addCase(fetchChannelsPublishingState.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
.addCase(updateChannelPublishingState.fulfilled, (state, action) => {
const index = state.publishingState.findIndex(
item => item.channelId === action.payload.channelId
);
if (index >= 0) {
state.publishingState[index] = action.payload;
} else {
state.publishingState.push(action.payload);
}
})
.addCase(updateChannelPublishingState.rejected, (state, action) => {
state.error = action.payload as string;
});
}
});
export const {clearPublishingState, setPublishingError, updateChannelState} = channelsPublishingSlice.actions;
export default channelsPublishingSlice.reducer;

View File

@@ -0,0 +1,126 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {createAsyncThunk, createSelector} from '@reduxjs/toolkit';
import channelService, {CreateChannelParams, DeleteChannelParams, ForkChannelParams, KillChannelParams} from '../../services/channel.service';
import {RootState} from '../index';
import {IChannelsState} from '../slices/Channels.slice';
// Selectors
export const channelsSelector = (state: RootState): IChannelsState => state.channels;
export const selectChannelList = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.channels
);
export const selectChannelsLoading = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.isLoading
);
export const selectChannelsError = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.error
);
export const selectSelectedChannel = createSelector(
[channelsSelector],
(channels: IChannelsState) => channels.selectedChannel
);
// Async thunks for channel operations
export const listChannels = createAsyncThunk(
'channels/listChannels',
async (_, {rejectWithValue}) => {
try {
const channels = await channelService.listChannels();
return channels;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fetch channels');
}
}
);
export const createChannelThunk = createAsyncThunk(
'channels/createChannel',
async (params: CreateChannelParams, {rejectWithValue, dispatch}) => {
try {
const newChannel = await channelService.createChannel(params);
// Refresh the channel list after creation
dispatch(listChannels());
return newChannel;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to create channel');
}
}
);
export const deleteChannelThunk = createAsyncThunk(
'channels/deleteChannel',
async (params: DeleteChannelParams, {rejectWithValue, dispatch}) => {
try {
await channelService.deleteChannel(params);
// Refresh the channel list after deletion
dispatch(listChannels());
return params.channelId;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to delete channel');
}
}
);
export const forkChannelThunk = createAsyncThunk(
'channels/forkChannel',
async (params: ForkChannelParams, {rejectWithValue, dispatch}) => {
try {
await channelService.forkChannel(params);
// Refresh the channel list after forking
dispatch(listChannels());
return params;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to fork channel');
}
}
);
export const killChannelThunk = createAsyncThunk(
'channels/killChannel',
async (params: KillChannelParams, {rejectWithValue, dispatch}) => {
try {
await channelService.killChannel(params);
// Refresh the channel list after killing
dispatch(listChannels());
return params.channelId;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to kill channel');
}
}
);
export const getChannelThunk = createAsyncThunk(
'channels/getChannel',
async (channelId: string, {rejectWithValue}) => {
try {
const channel = await channelService.getChannel(channelId);
return channel;
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to get channel');
}
}
);
export const getPublisherCountThunk = createAsyncThunk(
'channels/getPublisherCount',
async (channelId: string, {rejectWithValue}) => {
try {
const count = await channelService.getPublisherCount(channelId);
return {channelId, count};
} catch (error) {
return rejectWithValue(error instanceof Error ? error.message : 'Failed to get publisher count');
}
}
);
// Export all actions and selectors
export * from '../slices/Channels.slice';

125
src/store/action/screens.ts Normal file
View File

@@ -0,0 +1,125 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {createSelector, createSlice, PayloadAction} from '@reduxjs/toolkit';
import {RootState} from '../index';
// Screen types
export enum StoreScreensType {
ChannelList = 'channelList',
ChannelDetail = 'channelDetail',
Settings = 'settings',
Login = 'login',
Channels = "Channels"
}
// Screen state interface
export interface ScreenProps {
[key: string]: unknown;
}
export interface ScreenState {
currentScreen: StoreScreensType;
screenProps: ScreenProps;
previousScreen: StoreScreensType | null;
navigationHistory: StoreScreensType[];
}
const initialState: ScreenState = {
currentScreen: StoreScreensType.Login,
screenProps: {},
previousScreen: null,
navigationHistory: []
};
// Selectors
export const screensSelector = (state: RootState): ScreenState => state.screens;
export const selectCurrentScreen = createSelector(
[screensSelector],
(screens: ScreenState) => screens.currentScreen
);
export const selectScreenProps = createSelector(
[screensSelector],
(screens: ScreenState) => screens.screenProps
);
export const selectPreviousScreen = createSelector(
[screensSelector],
(screens: ScreenState) => screens.previousScreen
);
export const selectNavigationHistory = createSelector(
[screensSelector],
(screens: ScreenState) => screens.navigationHistory
);
// Slice
const screensSlice = createSlice({
name: 'screens',
initialState,
reducers: {
setCurrentScreen: (state, action: PayloadAction<StoreScreensType>) => {
state.previousScreen = state.currentScreen;
state.currentScreen = action.payload;
// Add to navigation history (keep last 10)
state.navigationHistory.push(action.payload);
if (state.navigationHistory.length > 10) {
state.navigationHistory.shift();
}
},
setScreenProps: (state, action: PayloadAction<ScreenProps>) => {
state.screenProps = action.payload;
},
updateScreenProps: (state, action: PayloadAction<Partial<ScreenProps>>) => {
state.screenProps = {
...state.screenProps,
...action.payload
};
},
navigateToScreen: (state, action: PayloadAction<{screen: StoreScreensType; props?: ScreenProps}>) => {
state.previousScreen = state.currentScreen;
state.currentScreen = action.payload.screen;
if (action.payload.props) {
state.screenProps = action.payload.props;
}
// Add to navigation history
state.navigationHistory.push(action.payload.screen);
if (state.navigationHistory.length > 10) {
state.navigationHistory.shift();
}
},
navigateBack: (state) => {
if (state.previousScreen) {
const temp = state.currentScreen;
state.currentScreen = state.previousScreen;
state.previousScreen = temp;
}
},
clearScreenProps: (state) => {
state.screenProps = {};
},
resetNavigation: (state) => {
state.currentScreen = StoreScreensType.Login;
state.previousScreen = null;
state.screenProps = {};
state.navigationHistory = [];
}
}
});
export const {
setCurrentScreen,
setScreenProps,
updateScreenProps,
navigateToScreen,
navigateBack,
clearScreenProps,
resetNavigation
} = screensSlice.actions;
export default screensSlice.reducer;

View File

@@ -1,6 +1,5 @@
import {
authenticateCredentialsThunk,
setError,
selectIsAuthenticated,
selectIsLoading,
selectApplicationId,
@@ -17,7 +16,7 @@ export const authenticateRequestMiddleware: Middleware = store => next => async
const secret = selectSecret(state);
console.log(
'[authenticateRequest] action [%o] isAuthenticated [%o] isLoading [%o] applicationId [%o] secret [%o]',
'[authenticateRequestMiddleware] action [%o] isAuthenticated [%o] isLoading [%o] applicationId [%o] secret [%o]',
action,
isAuthenticated,
isLoading,
@@ -31,8 +30,8 @@ export const authenticateRequestMiddleware: Middleware = store => next => async
typeof action === 'object' &&
action !== null &&
'type' in action &&
typeof (action as any).type === 'string' &&
(action as any).type.startsWith('authentication/')
typeof (action as {type: string}).type === 'string' &&
(action as {type: string}).type.startsWith('authentication/')
) {
return next(action);
}
@@ -57,13 +56,13 @@ export const authenticateRequestMiddleware: Middleware = store => next => async
try {
console.log('[authenticateRequest] Attempting auto-authentication');
// Use the Redux thunk to properly update the state
const authResult = await store.dispatch(authenticateCredentialsThunk({applicationId, secret}) as any);
const authResult = await store.dispatch(authenticateCredentialsThunk({ applicationId, secret }) as any);
if (authResult.type.endsWith('/rejected') || authResult.payload === 'Authentication failed') {
console.log('[authenticateRequest] Authentication failed');
return next(setUnauthorized());
}
console.log('[authenticateRequest] Auto-authentication successful, proceeding with action');
return next(action);
} catch (error) {

View File

@@ -4,7 +4,7 @@ import {Middleware} from '@reduxjs/toolkit';
* Logs all actions and states after they are dispatched.
*/
export const loggerMiddleware: Middleware = store => next => action => {
console.group((action as any).type);
console.group((action as {type: string}).type);
console.info('dispatching', action);
const result = next(action);
console.log('next state', store.getState());

View File

@@ -1,9 +1,9 @@
import {Middleware} from '@reduxjs/toolkit';
export const vanillaPromiseMiddleware: Middleware = store => next => (action: any) => {
if (typeof action.then !== 'function') {
export const vanillaPromiseMiddleware: Middleware = store => next => (action: unknown) => {
if (typeof (action as {then?: unknown}).then !== 'function') {
return next(action);
}
return Promise.resolve(action).then((resolvedAction: any) => store.dispatch(resolvedAction));
return Promise.resolve(action).then((resolvedAction: unknown) => store.dispatch(resolvedAction));
};

View File

@@ -0,0 +1,7 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
// Re-export from the action module for backwards compatibility
export {StoreScreensType} from '../action/screens';
export type {ScreenState, ScreenProps} from '../action/screens';

View File

@@ -2,6 +2,8 @@ import {createSlice, PayloadAction, createAsyncThunk, createSelector} from '@red
import AuthenticationService from '../../services/Authentication.service';
import {PhenixWebSocketStatusType} from 'services/net/websockets/PhenixWebSocketStatus';
import {IPhenixWebSocketResponse} from 'services/net/websockets/PhenixWebSocket';
import PCastApiService from 'services/PCastApi.service';
import ChannelService from 'services/Channel.service';
export interface IAuthenticationState {
applicationId: string | null;
@@ -137,6 +139,12 @@ const authenticationSlice = createSlice({
state.roles = authenticationResponse.roles ?? [];
state.status = 'Online';
state.isLoading = false;
PCastApiService.initialize('https://pcast-stg.phenixrts.com', {
id: state.applicationId ?? 'phenixrts.com-alex.zinn',
secret: state.secret ?? ''
});
ChannelService.initializeWithPCastApi(PCastApiService.channels);
} else {
state.applicationId = null;
state.sessionId = null;

View File

@@ -1,6 +1,7 @@
import {createAsyncThunk, createSelector, createSlice, PayloadAction, WritableDraft} from '@reduxjs/toolkit';
import {ApplicationCredentials, Channel} from '@techniker-me/pcast-api';
import PCastApiService from 'services/PCastApi.service';
import {listChannels, createChannelThunk, deleteChannelThunk, getChannelThunk, getPublisherCountThunk} from '../action/channels';
export interface IChannelsState {
isLoading: boolean;
@@ -23,6 +24,7 @@ export const selectChannelList = createSelector([selectChannels], channels => ch
export const fetchChannelList = createAsyncThunk('channels/fetchChannelList', async (_, {rejectWithValue}) => {
try {
return PCastApiService.channels.list();
} catch (error) {
return rejectWithValue(error);
}
@@ -72,30 +74,110 @@ const channelsSlice = createSlice({
}
},
extraReducers: builder => {
builder.addCase(fetchChannelList.pending, state => {
state.isLoading = true;
});
builder.addCase(fetchChannelList.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
});
builder.addCase(fetchChannelList.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
builder.addCase(fetchChannelsListPublisherStatus.pending, state => {
state.isLoading = true;
});
builder.addCase(fetchChannelsListPublisherStatus.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
});
builder.addCase(fetchChannelsListPublisherStatus.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
builder
// fetchChannelList cases
.addCase(fetchChannelList.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchChannelList.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
})
.addCase(fetchChannelList.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// fetchChannelsListPublisherStatus cases
.addCase(fetchChannelsListPublisherStatus.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(fetchChannelsListPublisherStatus.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
})
.addCase(fetchChannelsListPublisherStatus.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// listChannels cases (used by the component)
.addCase(listChannels.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(listChannels.fulfilled, (state, action) => {
state.channels = action.payload as WritableDraft<Channel[]>;
state.isLoading = false;
state.error = null;
})
.addCase(listChannels.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// createChannelThunk cases
.addCase(createChannelThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(createChannelThunk.fulfilled, (state, _action) => {
// Channel is already added to the list by the thunk calling listChannels
state.isLoading = false;
state.error = null;
})
.addCase(createChannelThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// deleteChannelThunk cases
.addCase(deleteChannelThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(deleteChannelThunk.fulfilled, (state, _action) => {
// Channel is already removed from the list by the thunk calling listChannels
state.isLoading = false;
state.error = null;
})
.addCase(deleteChannelThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// getChannelThunk cases
.addCase(getChannelThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getChannelThunk.fulfilled, (state, action) => {
state.selectedChannel = action.payload as WritableDraft<Channel>;
state.isLoading = false;
state.error = null;
})
.addCase(getChannelThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
})
// getPublisherCountThunk cases
.addCase(getPublisherCountThunk.pending, state => {
state.isLoading = true;
state.error = null;
})
.addCase(getPublisherCountThunk.fulfilled, (state, action) => {
// Update the specific channel with publisher count
const {channelId, count} = action.payload;
const channelIndex = state.channels.findIndex(channel => channel.channelId === channelId);
if (channelIndex !== -1) {
(state.channels[channelIndex] as any).publisherCount = count;
}
state.isLoading = false;
state.error = null;
})
.addCase(getPublisherCountThunk.rejected, (state, action) => {
state.isLoading = false;
state.error = action.payload as string;
});
}
});

View File

@@ -1,12 +1,16 @@
import {configureStore} from '@reduxjs/toolkit';
import AuthenticationReducer from './slices/Authentication.slice';
import ChannelsReducer from './slices/Channels.slice';
import ChannelsPublishingReducer from './action/channels-publishing';
import ScreensReducer from './action/screens';
import {authenticateRequestMiddleware, loggerMiddleware, vanillaPromiseMiddleware} from './middlewares';
const store = configureStore({
reducer: {
authentication: AuthenticationReducer,
channels: ChannelsReducer
channels: ChannelsReducer,
channelsPublishing: ChannelsPublishingReducer,
screens: ScreensReducer
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware({