commit b6717e0cb108330b100c9041c85c64b139fdb750 Author: Alex Zinn Date: Sun Aug 31 16:52:20 2025 -0400 initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,24 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* +lerna-debug.log* + +node_modules +dist +dist-ssr +*.local + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +.idea +.DS_Store +*.suo +*.ntvs* +*.njsproj +*.sln +*.sw? diff --git a/.npmrc b/.npmrc new file mode 100644 index 0000000..60d85b5 --- /dev/null +++ b/.npmrc @@ -0,0 +1,3 @@ +save-exact=true +package-lock=false +@techniker-me:registry=https://registry-node.techniker.me diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 0000000..2bd5a0a --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +22 diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..caa814d --- /dev/null +++ b/.prettierrc @@ -0,0 +1,12 @@ +{ + "arrowParens": "avoid", + "bracketSameLine": true, + "bracketSpacing": false, + "printWidth": 160, + "semi": true, + "singleAttributePerLine": false, + "singleQuote": true, + "tabWidth": 2, + "trailingComma": "none", + "useTabs": false +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..220d11f --- /dev/null +++ b/README.md @@ -0,0 +1,69 @@ +# React + TypeScript + Vite + +This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules. + +Currently, two official plugins are available: + +- [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react) uses [Babel](https://babeljs.io/) for Fast Refresh +- [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh + +## Expanding the ESLint configuration + +If you are developing a production application, we recommend updating the configuration to enable type-aware lint rules: + +```js +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + + // Remove tseslint.configs.recommended and replace with this + ...tseslint.configs.recommendedTypeChecked, + // Alternatively, use this for stricter rules + ...tseslint.configs.strictTypeChecked, + // Optionally, add this for stylistic rules + ...tseslint.configs.stylisticTypeChecked + + // Other configs... + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname + } + // other options... + } + } +]); +``` + +You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: + +```js +// eslint.config.js +import reactX from 'eslint-plugin-react-x'; +import reactDom from 'eslint-plugin-react-dom'; + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [ + // Other configs... + // Enable lint rules for React + reactX.configs['recommended-typescript'], + // Enable lint rules for React DOM + reactDom.configs.recommended + ], + languageOptions: { + parserOptions: { + project: ['./tsconfig.node.json', './tsconfig.app.json'], + tsconfigRootDir: import.meta.dirname + } + // other options... + } + } +]); +``` diff --git a/bunfig.toml b/bunfig.toml new file mode 100644 index 0000000..0aa5546 --- /dev/null +++ b/bunfig.toml @@ -0,0 +1,10 @@ +[install] +exact = true +saveTextLockfile = false +frozenLockfile = true + +[install.lockfile] +save = false + +[install.scopes] +"@techniker-me"="https://registry-node.techniker.me" diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..667a401 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,28 @@ +import js from '@eslint/js'; +import globals from 'globals'; +import reactHooks from 'eslint-plugin-react-hooks'; +import reactRefresh from 'eslint-plugin-react-refresh'; +import tseslint from 'typescript-eslint'; +import {globalIgnores} from 'eslint/config'; + +export default tseslint.config([ + globalIgnores(['dist']), + { + files: ['**/*.{ts,tsx}'], + extends: [js.configs.recommended, tseslint.configs.recommended, reactHooks.configs['recommended-latest'], reactRefresh.configs.vite], + languageOptions: { + ecmaVersion: 2020, + globals: globals.browser + }, + rules: { + '@typescript-eslint/no-unused-vars': [ + 'error', + { + 'argsIgnorePattern': '^ignored', + 'varsIgnorePattern': '^ignored', + 'caughtErrorsIgnorePattern': '^ignored' + } + ] + } + } +]); diff --git a/index.html b/index.html new file mode 100644 index 0000000..a52eeb4 --- /dev/null +++ b/index.html @@ -0,0 +1,18 @@ + + + + + + + + + + Phenix Customer Portal + + +
+ + + \ No newline at end of file diff --git a/package.json b/package.json new file mode 100644 index 0000000..8996a05 --- /dev/null +++ b/package.json @@ -0,0 +1,50 @@ +{ + "name": "webcontrolcenter", + "private": true, + "version": "2025.2.0", + "type": "module", + "scripts": { + "generate-version": "node scripts/generate-version.js", + "dev": "vite", + "format": "prettier --write ./", + "prelint": "bun run format", + "lint": "eslint .", + "lint:fix": "eslint --fix .", + "build": "tsc -b && vite build", + "preview": "vite preview" + }, + "dependencies": { + "@redux-devtools/extension": "3.3.0", + "@reduxjs/toolkit": "2.8.2", + "@techniker-me/pcast-api": "2025.1.4", + "@techniker-me/tools": "2025.0.16", + "@types/node": "24.3.0", + "phenix-web-closest-endpoint-resolver": "2020.0.3", + "phenix-web-detect-browser": "2021.0.1", + "phenix-web-proto": "2020.0.3", + "react": "19.1.1", + "react-dom": "19.1.1", + "react-redux": "9.2.0" + }, + "devDependencies": { + "@eslint/js": "9.34.0", + "@types/react": "19.1.12", + "@types/react-dom": "19.1.9", + "@vitejs/plugin-react-swc": "4.0.1", + "babel-plugin-transform-amd-to-commonjs": "1.6.0", + "commander": "14.0.0", + "eslint": "9.34.0", + "eslint-plugin-react-hooks": "5.2.0", + "eslint-plugin-react-refresh": "0.4.20", + "globals": "16.3.0", + "prettier": "3.6.2", + "typescript": "5.9.2", + "typescript-eslint": "8.41.0", + "vite": "7.1.3", + "vite-plugin-babel": "1.3.2", + "vite-plugin-commonjs": "0.10.4" + }, + "trustedDependencies": [ + "@swc/core" + ] +} diff --git a/scripts/generate-version.js b/scripts/generate-version.js new file mode 100644 index 0000000..66427b4 --- /dev/null +++ b/scripts/generate-version.js @@ -0,0 +1,21 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {writeFileSync} from 'fs'; +import {join} from 'path'; +import {program} from 'commander'; +import packageJson from './../package.json' with {type: 'json'}; + +program.option('-e --environment ', 'Specific environment (optional)'); + +program.parse(process.argv); + +const {environment} = program.opts(); +const prefix = environment ? `${environment}-` : 'local-'; +const version = `${prefix}${new Date().toISOString()} (${packageJson.version})`; +const fileLocation = join(process.cwd(), 'src', 'config', 'version.json'); +const controlVersion = {version}; + +writeFileSync(fileLocation, JSON.stringify(controlVersion, null, 2)); + +console.log(`Generated control center version [${version}]`); diff --git a/src/App.tsx b/src/App.tsx new file mode 100644 index 0000000..2085e33 --- /dev/null +++ b/src/App.tsx @@ -0,0 +1,47 @@ +import {useAppSelector, useAppDispatch} from 'hooks/store'; +import {WebSocketStatusViewComponent, LoginForm} from './components'; +import AuthenticationService from './services/authentication.service'; +import {useEffect, useState} from 'react'; +import {authenticationActions, selectError} from 'store/slices/Authentication.slice'; +import hostService from 'services/url.service'; +import LoggerFactory from './services/logger/LoggerFactory'; + +export default function App() { + const dispatch = useAppDispatch(); + const [isInitialized, setIsInitialized] = useState(false); + + useEffect(() => { + // Initialize logger after all modules are loaded to avoid circular dependencies + LoggerFactory.applyLoggerConfig(); + + // Set WebSocket URI only once during initialization + if (!isInitialized) { + AuthenticationService.setWebSocketUri(hostService.getWebSocketUrl()); + setIsInitialized(true); + } + + // Subscribe to WebSocket status changes + AuthenticationService.status?.subscribe(status => + dispatch(authenticationActions.setStatus(status)) + ); + }, [dispatch, isInitialized]); + + const error = useAppSelector(selectError); + + return ( + <> + {error &&
{error}
} +
+ + +
+ + ); +} diff --git a/src/components/FlexContainer/FlexContainerComponent.tsx b/src/components/FlexContainer/FlexContainerComponent.tsx new file mode 100644 index 0000000..e832798 --- /dev/null +++ b/src/components/FlexContainer/FlexContainerComponent.tsx @@ -0,0 +1,16 @@ +import {PropsWithChildren} from 'react'; + +export interface IFlexContainerProps { + style?: Record; + children: PropsWithChildren['children']; +} + +export function FlexContainer({style, children}: IFlexContainerProps) { + const flexContainerStyle = { + display: 'flex', + width: '100%', + height: '100%', + ...style + }; + return
{children}
; +} diff --git a/src/components/FlexContainer/index.ts b/src/components/FlexContainer/index.ts new file mode 100644 index 0000000..1524312 --- /dev/null +++ b/src/components/FlexContainer/index.ts @@ -0,0 +1 @@ +export * from './FlexContainerComponent'; diff --git a/src/components/InputField/InputField.tsx b/src/components/InputField/InputField.tsx new file mode 100644 index 0000000..2cf50bc --- /dev/null +++ b/src/components/InputField/InputField.tsx @@ -0,0 +1,34 @@ +import {JSX} from 'react'; + +export interface IInputFieldProps { + label?: string; + type: 'text' | 'password' | 'number'; + placeholder?: string; + style?: Record; + disabled?: boolean; + value: string; + onValueChange: (value: string) => void; + isRequired?: boolean; +} + +export function InputField({label, type, value, onValueChange, placeholder, style, disabled, isRequired}: IInputFieldProps): JSX.Element { + return ( + <> + + + ); +} diff --git a/src/components/InputField/index.ts b/src/components/InputField/index.ts new file mode 100644 index 0000000..9cd7022 --- /dev/null +++ b/src/components/InputField/index.ts @@ -0,0 +1 @@ +export * from './InputField'; diff --git a/src/components/LoginForm/LoginForm.tsx b/src/components/LoginForm/LoginForm.tsx new file mode 100644 index 0000000..f4ef22f --- /dev/null +++ b/src/components/LoginForm/LoginForm.tsx @@ -0,0 +1,61 @@ +import {JSX, useState} from 'react'; +import {useAppSelector, useAppDispatch} from '../../hooks'; +import {authenticateCredentialsThunk, selectIsAuthenticating, selectStatus} from '../../store/slices/Authentication.slice'; +import {InputField} from '../InputField'; + +export function LoginForm(): JSX.Element { + const dispatch = useAppDispatch(); + const [applicationId, setApplicationId] = useState('phenixrts.com-alex.zinn'); + const [secret, setSecret] = useState('AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg'); + + const isAuthenticating = useAppSelector(selectIsAuthenticating); + const connectionStatus = useAppSelector(selectStatus); + + // Only allow authentication when WebSocket is Online + const isConnectionReady = connectionStatus === 'Online'; + const isDisabled = isAuthenticating || !isConnectionReady; + + const handleSubmit = (e: React.FormEvent) => { + e.preventDefault(); + + if (!isConnectionReady) { + alert('Please wait for WebSocket connection to be established before attempting to authenticate.'); + return; + } + + dispatch(authenticateCredentialsThunk({applicationId, secret})); + setApplicationId(''); + setSecret(''); + }; + + return ( + <> +
+ setApplicationId(value)} + disabled={isDisabled} + isRequired={true} + /> + setSecret(value)} + disabled={isDisabled} + isRequired={true} + /> + + {!isConnectionReady && ( +
+ Waiting for WebSocket connection... +
+ )} + + + ); +} diff --git a/src/components/LoginForm/index.ts b/src/components/LoginForm/index.ts new file mode 100644 index 0000000..8026749 --- /dev/null +++ b/src/components/LoginForm/index.ts @@ -0,0 +1 @@ +export * from './LoginForm'; diff --git a/src/components/WebSocketStatusView/WebSocketStatusViewComponent.tsx b/src/components/WebSocketStatusView/WebSocketStatusViewComponent.tsx new file mode 100644 index 0000000..8305b0d --- /dev/null +++ b/src/components/WebSocketStatusView/WebSocketStatusViewComponent.tsx @@ -0,0 +1,9 @@ +import {useAppSelector} from 'hooks/store'; +import {selectStatus} from 'store/slices/Authentication.slice'; + +export function WebSocketStatusViewComponent() { + const storeStatus = useAppSelector(selectStatus); + const displayStatus = storeStatus ?? 'unknown'; + + return <>Status: {displayStatus}; +} diff --git a/src/components/WebSocketStatusView/index.ts b/src/components/WebSocketStatusView/index.ts new file mode 100644 index 0000000..26cf3ca --- /dev/null +++ b/src/components/WebSocketStatusView/index.ts @@ -0,0 +1 @@ +export * from './WebSocketStatusViewComponent'; diff --git a/src/components/index.ts b/src/components/index.ts new file mode 100644 index 0000000..b8464ae --- /dev/null +++ b/src/components/index.ts @@ -0,0 +1,4 @@ +export * from './InputField'; +export * from './LoginForm'; +export * from './FlexContainer'; +export * from './WebSocketStatusView'; diff --git a/src/config/index.ts b/src/config/index.ts new file mode 100644 index 0000000..f428ed3 --- /dev/null +++ b/src/config/index.ts @@ -0,0 +1,6 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import controlVersion from 'config/version.json'; + +export default {controlCenterVersion: controlVersion['version']}; diff --git a/src/config/version.json b/src/config/version.json new file mode 100644 index 0000000..b909f16 --- /dev/null +++ b/src/config/version.json @@ -0,0 +1,3 @@ +{ + "version": "local-2025-08-31T19:58:27.686Z (2025.2.0)" +} diff --git a/src/hooks/index.ts b/src/hooks/index.ts new file mode 100644 index 0000000..d406816 --- /dev/null +++ b/src/hooks/index.ts @@ -0,0 +1 @@ +export * from './store'; diff --git a/src/hooks/store.ts b/src/hooks/store.ts new file mode 100644 index 0000000..9805158 --- /dev/null +++ b/src/hooks/store.ts @@ -0,0 +1,10 @@ +import {useDispatch, useSelector} from 'react-redux'; +import {store} from '../store'; +// Use throughout the app instead of plain `useDispatch` and `useSelector` +// Infer the `RootState`, `AppDispatch`, and `AppStore` types from the store itself +export type RootState = ReturnType; +// Inferred type: {posts: PostsState, comments: C +export type AppDispatch = typeof store.dispatch; +export type AppStore = typeof store; +export const useAppDispatch = useDispatch.withTypes(); +export const useAppSelector = useSelector.withTypes(); diff --git a/src/index.css b/src/index.css new file mode 100644 index 0000000..0ee9476 --- /dev/null +++ b/src/index.css @@ -0,0 +1,74 @@ +:root { + font-family: system-ui, Avenir, Helvetica, Arial, sans-serif; + line-height: 1.5; + font-weight: 400; + + color-scheme: light dark; + color: rgba(255, 255, 255, 0.87); + background-color: #242424; + + font-synthesis: none; + text-rendering: optimizeLegibility; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +a { + font-weight: 500; + color: #646cff; + text-decoration: inherit; +} +a:hover { + color: #535bf2; +} + +#root { + width: 100vw; + height: 100vh; +} + +body { + margin: 0; + display: flex; + place-items: center; + min-width: 320px; + min-height: 100vh; +} + +h1 { + font-size: 3.2em; + line-height: 1.1; +} + +button { + text-align: 'center'; + border-radius: 8px; + border: 1px solid transparent; + padding: 0.6em 1.2em; + font-size: 1em; + font-weight: 500; + font-family: inherit; + background-color: #1a1a1a; + cursor: pointer; + transition: border-color 0.25s; +} +button:hover { + border-color: #646cff; +} +button:focus, +button:focus-visible { + outline: 4px auto -webkit-focus-ring-color; +} + +@media (prefers-color-scheme: light) { + :root { + color: #213547; + background-color: #ffffff; + } + a:hover { + color: #747bff; + } + button { + background-color: #f9f9f9; + } +} diff --git a/src/lang/Strings.ts b/src/lang/Strings.ts new file mode 100644 index 0000000..57f54ac --- /dev/null +++ b/src/lang/Strings.ts @@ -0,0 +1,11 @@ +export default class Strings { + public static randomString(length: number): string { + return Math.random() + .toString(36) + .substring(2, 2 + length); + } + + private constructor() { + throw new Error('Strings is a static class that may not be instantiated'); + } +} diff --git a/src/lang/assertUnreachable.ts b/src/lang/assertUnreachable.ts new file mode 100644 index 0000000..77e30d9 --- /dev/null +++ b/src/lang/assertUnreachable.ts @@ -0,0 +1,3 @@ +export default function assertUnreachable(x: never): never { + throw new Error(`Error: Reached un-reachable code with [${x}]`); +} diff --git a/src/main.tsx b/src/main.tsx new file mode 100644 index 0000000..d0bd37a --- /dev/null +++ b/src/main.tsx @@ -0,0 +1,11 @@ +import {createRoot} from 'react-dom/client'; +import {Provider} from 'react-redux'; +import {store} from './store'; +import App from './App'; +import './index.css'; + +createRoot(document.getElementById('root')!).render( + + + +); diff --git a/src/routes/index.ts b/src/routes/index.ts new file mode 100644 index 0000000..b6dcd3d --- /dev/null +++ b/src/routes/index.ts @@ -0,0 +1,32 @@ +export default { + fetchChannelsUrl: () => '/pcast/channels', + createChannelUrl: () => '/pcast/channel', + deleteChannelUrl: () => '/pcast/channel', + fetchChannelUrl: (channelId: string): string => `/pcast/channel/${channelId}`, + fetchChannelMembersUrl: (channelId: string): string => `/pcast/channel/${channelId}/members`, + fetchRoomsUrl: () => '/pcast/rooms', + deleteRoomUrl: () => '/pcast/room', + createRoomUrl: () => '/pcast/room', + fetchRoomUrl: (roomId: string): string => `/pcast/room/${roomId}`, + fetchRoomMembersUrl: (roomId: string): string => `/pcast/room/${roomId}/members`, + stateOfChannelUrl: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/publishers/count`, + stateOfRoomUrl: (roomId: string): string => `/pcast/room/${encodeURIComponent(roomId)}/publishers/count`, + sendChannelMessageUrl: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/message`, + getChannelMessagesUrl: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/messages`, + getRoomMessagesUrl: (roomId: string): string => `/pcast/room/${encodeURIComponent(roomId)}/messages`, + playlistInformation: () => '/pcast/stream/playlist', + killChannel: (channelId: string): string => `/pcast/channel/${encodeURIComponent(channelId)}/kill`, + forkChannel: (destinationChannelId: string, sourceChannelId: string): string => + `/pcast/channel/${encodeURIComponent(destinationChannelId)}/fork/${encodeURIComponent(sourceChannelId)}`, + viewReporting: () => '/pcast/reporting/viewing', + forkHistoryReporting: () => '/pcast/analytics/fork/history', + publishReporting: () => '/pcast/reporting/publishing', + concurrentViewers: () => '/pcast/streams/concurrent', + ingestReports: () => '/pcast/ingest/buffer', + terminateStream: () => '/pcast/stream', + edgeAuth: () => '/pcast/edgeAuth', + authTokenUrl: () => '/pcast/auth', + publishUri: (tenancy: string, publicationType: string): string => `/pcast/${tenancy}/stream/publish/uri/${publicationType}`, + networkThroughput: () => '/pcast/network/throughput', + fetchPlaylist: () => '/pcast/stream/playlist' +}; diff --git a/src/services/authentication.service.ts b/src/services/authentication.service.ts new file mode 100644 index 0000000..f1a2aaf --- /dev/null +++ b/src/services/authentication.service.ts @@ -0,0 +1,49 @@ +import PlatformDetectionService from './platform-detection.service'; +import {Strings, Subject, ReadOnlySubject} from '@techniker-me/tools'; +import {PhenixWebSocket, PhenixWebSocketStatus, PhenixWebSocketMessage} from 'services/net/PhenixWebSocket'; +import config from 'config'; + +class AuthenticationService { + private static readonly _instance: AuthenticationService = new AuthenticationService(); + private readonly _phenixWebSocket: Subject = new Subject(null); + private readonly _sessionId: string = Strings.random(10); + + public static getInstance(): AuthenticationService { + return AuthenticationService._instance; + } + + get status(): ReadOnlySubject | undefined { + return this._phenixWebSocket.value?.status || undefined; + } + + public setWebSocketUri(webSocketUri: string): void { + this._phenixWebSocket.value?.dispose(); + this._phenixWebSocket.value = new PhenixWebSocket(webSocketUri); + } + + public async authenticateCredentials(applicationId: string, secret: string): Promise<{error: unknown; response: unknown}> { + const authenicate = { + applicationId, + secret, + clientVersion: config.controlCenterVersion, + deviceId: '', + platform: PlatformDetectionService.platform, + platformVersion: PlatformDetectionService.platformVersion, + sessionId: this._sessionId, + authenticationToken: secret + }; + + return this._phenixWebSocket.value?.sendMessage(PhenixWebSocketMessage.Authenticate, authenicate) as Promise<{error: unknown; response: unknown}>; + } + + public async signout(): Promise<{error: unknown; response: unknown}> { + const {error, response} = await (this._phenixWebSocket.value?.sendMessage(PhenixWebSocketMessage.Bye, {sessionId: this._sessionId}) as Promise<{ + error: unknown; + response: unknown; + }>); + + return {error, response}; + } +} + +export default AuthenticationService.getInstance(); diff --git a/src/services/logger/Appenders.ts b/src/services/logger/Appenders.ts new file mode 100644 index 0000000..5339cbb --- /dev/null +++ b/src/services/logger/Appenders.ts @@ -0,0 +1,26 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {IAppender} from './IAppender'; + +export default class Appenders { + private _appenders: Array = []; + + get value(): Array { + return this._appenders; + } + + add(appender: IAppender): void { + this._appenders.push(appender); + } + + remove(appender: IAppender): void { + this._appenders = this._appenders.reduce((items, item) => { + if (!(item === appender)) { + items.push(item); + } + + return items; + }, [] as Array); + } +} diff --git a/src/services/logger/ConsoleAppender.ts b/src/services/logger/ConsoleAppender.ts new file mode 100644 index 0000000..c375fe8 --- /dev/null +++ b/src/services/logger/ConsoleAppender.ts @@ -0,0 +1,29 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {IAppender} from './IAppender'; +import {LoggingLevel} from './Logger'; + +export default class ConsoleAppender implements IAppender { + private readonly _threshold: LoggingLevel; + + log(logLevel: LoggingLevel, message: string, category: string, date: Date): void { + if (logLevel < this._threshold) { + return; + } + + const fullMessage = `${date.toISOString()} [${category}] [${LoggingLevel[logLevel]}] ${message}`; + + if (logLevel < LoggingLevel.Warn) { + console.log(fullMessage); + + return; + } + + console.error(fullMessage); + } + + constructor(threshold: LoggingLevel) { + this._threshold = threshold; + } +} diff --git a/src/services/logger/IAppender.ts b/src/services/logger/IAppender.ts new file mode 100644 index 0000000..8ec65fb --- /dev/null +++ b/src/services/logger/IAppender.ts @@ -0,0 +1,8 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {LoggingLevel} from './Logger'; + +export interface IAppender { + log: (logLevel: LoggingLevel, message: string, category: string, date: Date) => void; +} diff --git a/src/services/logger/Logger.ts b/src/services/logger/Logger.ts new file mode 100644 index 0000000..8fcc4f4 --- /dev/null +++ b/src/services/logger/Logger.ts @@ -0,0 +1,197 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import {IAppender} from './IAppender'; +import Appenders from './Appenders'; +import LoggingThreshold from './LoggingThreshold'; + +export enum LoggingLevel { + All = -1, + Trace = 10, + Debug = 20, + Info = 30, + Warn = 40, + Error = 50, + Fatal = 60, + Off = 100 +} + +export type LoggingLevelType = 'Off' | 'Trace' | 'Debug' | 'Info' | 'Warn' | 'Error' | 'Fatal' | 'All'; + +export default class Logger { + private readonly _category: string; + private readonly _appenders: Appenders; + private readonly _threshold: LoggingThreshold; + + get category(): string { + return this._category; + } + + get appenders(): Appenders { + return this._appenders; + } + + get threshold(): LoggingThreshold { + return this._threshold; + } + + trace(...args: any): void { + if (!this._threshold.value || this._threshold.value > LoggingLevel.Trace) { + return; + } + + this.log(LoggingLevel.Trace, args); + } + + debug(...args: any): void { + if (!this._threshold.value || this._threshold.value > LoggingLevel.Debug) { + return; + } + + this.log(LoggingLevel.Debug, args); + } + + info(...args: any): void { + if (!this._threshold.value || this._threshold.value > LoggingLevel.Info) { + return; + } + + this.log(LoggingLevel.Info, args); + } + + warn(...args: any): void { + if (!this._threshold.value || this._threshold.value > LoggingLevel.Warn) { + return; + } + + this.log(LoggingLevel.Warn, args); + } + + error(...args: any): void { + if (!this._threshold.value || this._threshold.value > LoggingLevel.Error) { + return; + } + + this.log(LoggingLevel.Error, args); + } + + fatal(...args: any): void { + if (!this._threshold.value || this._threshold.value > LoggingLevel.Fatal) { + return; + } + + this.log(LoggingLevel.Fatal, args); + } + + private log(level: number, args: any): void { + const date = new Date(); + const message = this.replacePlaceholders(args); + + this._appenders.value.forEach((appender: IAppender) => { + appender.log(level, message, this.category, date); + }); + } + + private replacePlaceholders(args: any): string { + let replacePlaceholdersString = args[0]; + let index = 0; + + while (replacePlaceholdersString.indexOf && args.length > 1 && index >= 0) { + index = replacePlaceholdersString.indexOf('%', index); + + if (index > 0) { + const type = replacePlaceholdersString.substring(index + 1, index + 2); + + switch (type) { + case '%': + // Escaped '%%' turns into '%' + replacePlaceholdersString = replacePlaceholdersString.substring(0, index) + replacePlaceholdersString.substring(index + 1); + index++; + + break; + case 's': + case 'd': + // Replace '%d' or '%s' with the argument + args[0] = replacePlaceholdersString = this.replaceArgument(this.toString(args[1]), replacePlaceholdersString, index); + args.splice(1, 1); + + break; + case 'j': + // Replace %j' with the argument + args[0] = replacePlaceholdersString = this.replaceArgument(this.stringify(args[1]), replacePlaceholdersString, index); + + args.splice(1, 1); + + break; + default: + return args.toString(); + } + } + } + + if (args.length > 1) { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args = args.reduce((accumulator: any, currentValue: any, index: number, array: any[]) => { + if (index + 1 === array.length && currentValue instanceof Error) { + return accumulator + '\n' + this.toString(currentValue.stack); + } + + return accumulator + `[${this.toString(currentValue)}]`; + }); + } + + return args.toString(); + } + + private stringify(arg: any): string { + try { + return JSON.stringify(arg instanceof Error ? this.toString(arg) : arg, null, 2); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return '[object invalid JSON.stringify]'; + } + } + + private replaceArgument(argument: any, replacePlaceholdersString: string, index: number): string { + return replacePlaceholdersString.substring(0, index) + this.toString(argument) + replacePlaceholdersString.substring(index + 2); + } + + private toString(data: any): string { + if (typeof data === 'string') { + return data; + } + + if (typeof data === 'boolean') { + return data ? 'true' : 'false'; + } + + if (typeof data === 'number') { + return data.toString(); + } + + let toStringStr = ''; + + if (data) { + if (typeof data === 'function') { + toStringStr = data.toString(); + } else if (data instanceof Object) { + try { + toStringStr = data.toString(); + + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + toStringStr = '[object invalid toString()]'; + } + } + } + + return toStringStr; + } + + constructor(category: string, appenders: Appenders, threshold: LoggingThreshold) { + this._category = category; + this._appenders = appenders; + this._threshold = threshold; + } +} diff --git a/src/services/logger/LoggerDefaults.ts b/src/services/logger/LoggerDefaults.ts new file mode 100644 index 0000000..3d2becc --- /dev/null +++ b/src/services/logger/LoggerDefaults.ts @@ -0,0 +1,51 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {LoggingLevel, LoggingLevelType} from '../logger/Logger'; + +declare const __FEATURES__: { + sendLogs: LoggingLevelType; + logToConsole: LoggingLevelType; +}; + +export class BuildFeatures { + private static _sendLogs: LoggingLevelType; + private static _logToConsole: LoggingLevelType; + + static get sendLogs(): LoggingLevelType { + return this._sendLogs; + } + + static get logToConsole(): LoggingLevelType { + return this._logToConsole; + } + + static applyFeatures(): void { + try { + const features = __FEATURES__; + + this._sendLogs = 'sendLogs' in features ? features.sendLogs : 'All'; + this._logToConsole = 'logToConsole' in features ? features.logToConsole : 'All'; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + this._sendLogs = 'All'; + this._logToConsole = 'All'; + } + } +} + +BuildFeatures.applyFeatures(); + +export default class LoggerDefaults { + static get defaultLoggingLevel(): LoggingLevel { + return LoggingLevel[BuildFeatures.sendLogs]; + } + + static get defaultConsoleLoggingLevel(): LoggingLevel { + return LoggingLevel[BuildFeatures.logToConsole]; + } + + static get defaultTelemetryLoggingLevel(): LoggingLevel { + return LoggingLevel.Info; + } +} diff --git a/src/services/logger/LoggerFactory.ts b/src/services/logger/LoggerFactory.ts new file mode 100644 index 0000000..57ce4b4 --- /dev/null +++ b/src/services/logger/LoggerFactory.ts @@ -0,0 +1,86 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import PlatformDetectionService from 'services/platform-detection.service'; +import hostService from 'services/url.service'; +import TelemetryConfiguration from 'services/telemetry/TelemetryConfiguration'; +import TelemetryAppender from 'services/telemetry/TelemetryApender'; +// import userStore from 'services/user-store'; +import {ILogger} from './LoggerInterface'; +import Logger, {LoggingLevel} from './Logger'; +import Appenders from './Appenders'; +import LoggingThreshold from './LoggingThreshold'; +import ConsoleAppender from './ConsoleAppender'; +import LoggerDefaults from './LoggerDefaults'; + +export default class LoggerFactory { + private static _loggers: {[category: string]: ILogger} = {}; + private static _appenders: Appenders = new Appenders(); + private static _threshold: LoggingThreshold = new LoggingThreshold(); + private static _telemetryConfiguration: TelemetryConfiguration = new TelemetryConfiguration(); + private static _initialized: boolean = false; + + static get telemetryConfiguration(): TelemetryConfiguration { + return this._telemetryConfiguration; + } + + static applyLoggerConfig(): void { + if (this._initialized) return; + + LoggerFactory.applyConsoleLogger(LoggingLevel['All']); + LoggerFactory.applyLoggingLevel(); + LoggerFactory.applyTelemetryLogger(); + this._initialized = true; + } + + static getLogger(category: string): ILogger { + if (typeof category !== 'string') { + category = 'portal'; + } + + const logger = LoggerFactory._loggers[category]; + + if (logger) { + return logger; + } + + return (LoggerFactory._loggers[category] = new Logger(category, this._appenders, this._threshold)); + } + + static applyLoggingLevel(): void { + this._threshold.setThreshold(LoggingLevel['All']); + } + + static applyConsoleLogger(level: LoggingLevel): void { + this._appenders.add(new ConsoleAppender(level || LoggerDefaults.defaultConsoleLoggingLevel)); + } + + static async applyTelemetryConfiguration(level: LoggingLevel): Promise { + this._telemetryConfiguration.threshold = level || LoggerDefaults.defaultTelemetryLoggingLevel; + this._telemetryConfiguration.url = hostService.getTelemetryUrl(); + this._telemetryConfiguration.environment = hostService.getBaseUrlOrigin(); + // this._telemetryConfiguration.tenancy = await userStore.get('applicationId'); + // this._telemetryConfiguration.userId = await userStore.get('applicationId'); + // this._telemetryConfiguration.sessionId = await userStore.get('sessionId'); + + // Lazy access to PlatformDetectionService to avoid circular dependency + try { + this._telemetryConfiguration.browser = PlatformDetectionService.browser + ? `${PlatformDetectionService.browser}/${PlatformDetectionService.version}` + : 'unknown'; + } catch (error) { + console.warn('Failed to get platform detection info for telemetry:', error); + this._telemetryConfiguration.browser = 'unknown'; + } + } + + private static applyTelemetryLogger(): void { + LoggerFactory.applyTelemetryConfiguration(LoggingLevel['Info']); + + this._appenders.add(new TelemetryAppender(this._telemetryConfiguration)); + } + + private constructor() { + throw new Error('LoggerFactory is a static class that may not be instantiated'); + } +} diff --git a/src/services/logger/LoggerInterface.ts b/src/services/logger/LoggerInterface.ts new file mode 100644 index 0000000..d61c4d3 --- /dev/null +++ b/src/services/logger/LoggerInterface.ts @@ -0,0 +1,27 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +/* eslint-disable @typescript-eslint/no-explicit-any */ +import Appenders from './Appenders'; +import LoggingThreshold from './LoggingThreshold'; + +export interface ILogger { + readonly category: string; + + readonly appenders: Appenders; + + readonly threshold: LoggingThreshold; + + trace: (...args: any) => void; + + debug: (...args: any) => void; + + info: (...args: any) => void; + + warn: (...args: any) => void; + + error: (...args: any) => void; + + fatal: (...args: any) => void; +} +/* eslint-enable */ diff --git a/src/services/logger/LoggingLevelMapping.ts b/src/services/logger/LoggingLevelMapping.ts new file mode 100644 index 0000000..ec90dd7 --- /dev/null +++ b/src/services/logger/LoggingLevelMapping.ts @@ -0,0 +1,56 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {LoggingLevel, LoggingLevelType} from './Logger'; + +function assertUnreachable(x: never): never { + throw new Error(`Unexpected value [${x}]. This should never be reached`); +} + +export default class LoggingLevelMapping { + static convertLoggingLevelToLoggingLevelType(loggingLevel: LoggingLevel): LoggingLevelType { + switch (loggingLevel) { + case LoggingLevel.Off: + return 'Off'; + case LoggingLevel.Trace: + return 'Trace'; + case LoggingLevel.Debug: + return 'Debug'; + case LoggingLevel.Info: + return 'Trace'; + case LoggingLevel.Warn: + return 'Warn'; + case LoggingLevel.Error: + return 'Error'; + case LoggingLevel.Fatal: + return 'Fatal'; + case LoggingLevel.All: + return 'All'; + default: + assertUnreachable(loggingLevel); + } + } + + static convertLoggingLevelTypeToLoggingLevel(loggingLevelType: LoggingLevelType): LoggingLevel { + switch (loggingLevelType) { + case 'Off': + return LoggingLevel.Off; + case 'Trace': + return LoggingLevel.Trace; + case 'Debug': + return LoggingLevel.Debug; + case 'Info': + return LoggingLevel.Info; + case 'Warn': + return LoggingLevel.Warn; + case 'Error': + return LoggingLevel.Error; + case 'Fatal': + return LoggingLevel.Fatal; + case 'All': + return LoggingLevel.All; + default: + assertUnreachable(loggingLevelType); + } + } +} diff --git a/src/services/logger/LoggingThreshold.ts b/src/services/logger/LoggingThreshold.ts new file mode 100644 index 0000000..d441f83 --- /dev/null +++ b/src/services/logger/LoggingThreshold.ts @@ -0,0 +1,17 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import LoggerDefaults from './LoggerDefaults'; +import {LoggingLevel} from './Logger'; + +export default class LoggingThreshold { + private _threshold: LoggingLevel = LoggerDefaults.defaultLoggingLevel; + + get value(): LoggingLevel { + return this._threshold; + } + + setThreshold(threshold: LoggingLevel): void { + this._threshold = threshold; + } +} diff --git a/src/services/messaging/analytics.proto.json b/src/services/messaging/analytics.proto.json new file mode 100644 index 0000000..17725c1 --- /dev/null +++ b/src/services/messaging/analytics.proto.json @@ -0,0 +1,391 @@ +{ + "package": "analytics", + "messages": [ + { + "name": "Usage", + "fields": [ + { + "rule": "required", + "type": "uint64", + "name": "streams", + "id": 1 + }, + { + "rule": "required", + "type": "uint64", + "name": "users", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "devices", + "id": 3 + }, + { + "rule": "required", + "type": "uint64", + "name": "minutes", + "id": 4 + } + ] + }, + { + "name": "UsageByType", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "type", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "subtype", + "id": 2 + }, + { + "rule": "required", + "type": "Usage", + "name": "usage", + "id": 3 + } + ] + }, + { + "name": "UsageByCountry", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "continent", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "country", + "id": 2 + }, + { + "rule": "repeated", + "type": "UsageByType", + "name": "usageByType", + "id": 3 + } + ] + }, + { + "name": "GetGeographicUsage", + "fields": [ + { + "rule": "repeated", + "type": "string", + "name": "applicationIds", + "id": 1 + }, + { + "rule": "required", + "type": "uint64", + "name": "start", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "end", + "id": 3 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 5 + } + ] + }, + { + "name": "GetGeographicUsageResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "Usage", + "name": "usage", + "id": 2 + }, + { + "rule": "repeated", + "type": "UsageByType", + "name": "usageByType", + "id": 3 + }, + { + "rule": "repeated", + "type": "UsageByCountry", + "name": "usageByCountry", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 6 + } + ] + }, + { + "name": "CDF", + "fields": [ + { + "rule": "repeated", + "type": "double", + "name": "data", + "id": 1 + } + ] + }, + { + "name": "GetTimeToFirstFrameCDF", + "fields": [ + { + "rule": "repeated", + "type": "string", + "name": "applicationIds", + "id": 1 + }, + { + "rule": "required", + "type": "uint64", + "name": "start", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "end", + "id": 3 + }, + { + "rule": "required", + "type": "Kind", + "name": "kind", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 6 + } + ], + "enums": [ + { + "name": "Kind", + "values": [ + { + "name": "All", + "id": 0 + }, + { + "name": "RealTime", + "id": 1 + }, + { + "name": "Live", + "id": 2 + }, + { + "name": "Dash", + "id": 3 + }, + { + "name": "Hls", + "id": 4 + }, + { + "name": "PeerAssisted", + "id": 5 + } + ] + } + ] + }, + { + "name": "GetTimeToFirstFrameCDFResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "uint64", + "name": "count", + "id": 2 + }, + { + "rule": "optional", + "type": "double", + "name": "average", + "id": 3 + }, + { + "rule": "optional", + "type": "CDF", + "name": "cdf", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 6 + } + ] + }, + { + "name": "GetActiveUsers", + "fields": [ + { + "rule": "repeated", + "type": "string", + "name": "applicationIds", + "id": 1 + }, + { + "rule": "required", + "type": "uint64", + "name": "snapshotTime", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 3 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 4 + } + ] + }, + { + "name": "UsersAndSessionsGrouped", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "groupName", + "id": 1 + }, + { + "rule": "required", + "type": "uint64", + "name": "users", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "sessions", + "id": 3 + } + ] + }, + { + "name": "GetActiveUsersResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "uint64", + "name": "users", + "id": 2 + }, + { + "rule": "optional", + "type": "uint64", + "name": "sessions", + "id": 3 + }, + { + "rule": "repeated", + "type": "UsersAndSessionsGrouped", + "name": "byPlatform", + "id": 4 + }, + { + "rule": "repeated", + "type": "UsersAndSessionsGrouped", + "name": "byManufacturer", + "id": 5 + }, + { + "rule": "repeated", + "type": "UsersAndSessionsGrouped", + "name": "byCity", + "id": 6 + }, + { + "rule": "repeated", + "type": "UsersAndSessionsGrouped", + "name": "byCountry", + "id": 7 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 8 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 9 + } + ] + } + ] +} diff --git a/src/services/messaging/pcast.proto.json b/src/services/messaging/pcast.proto.json new file mode 100644 index 0000000..d3fdd3b --- /dev/null +++ b/src/services/messaging/pcast.proto.json @@ -0,0 +1,1777 @@ +{ + "package": "pcast", + "messages": [ + { + "name": "Authenticate", + "fields": [ + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 9, + "options": { + "default": 0 + } + }, + { + "rule": "required", + "type": "string", + "name": "clientVersion", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "device", + "id": 12 + }, + { + "rule": "required", + "type": "string", + "name": "deviceId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "manufacturer", + "id": 13 + }, + { + "rule": "required", + "type": "string", + "name": "platform", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "platformVersion", + "id": 4 + }, + { + "rule": "required", + "type": "string", + "name": "authenticationToken", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "connectionId", + "id": 6 + }, + { + "rule": "optional", + "type": "string", + "name": "connectionRouteKey", + "id": 10 + }, + { + "rule": "optional", + "type": "string", + "name": "remoteAddress", + "id": 11 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 7 + }, + { + "rule": "optional", + "type": "string", + "name": "applicationId", + "id": 8 + } + ] + }, + { + "name": "AuthenticateResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "redirect", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "roles", + "id": 4 + } + ] + }, + { + "name": "Bye", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 2 + } + ] + }, + { + "name": "ByeResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "SessionDescription", + "fields": [ + { + "rule": "required", + "type": "Type", + "name": "type", + "id": 1, + "options": { + "default": "Offer" + } + }, + { + "rule": "required", + "type": "string", + "name": "sdp", + "id": 2 + } + ], + "enums": [ + { + "name": "Type", + "values": [ + { + "name": "Offer", + "id": 0 + }, + { + "name": "Answer", + "id": 1 + } + ] + } + ] + }, + { + "name": "CreateStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "originStreamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "optional", + "type": "string", + "name": "connectUri", + "id": 8 + }, + { + "rule": "repeated", + "type": "string", + "name": "connectOptions", + "id": 9 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + }, + { + "rule": "optional", + "type": "SetRemoteDescription", + "name": "setRemoteDescription", + "id": 5 + }, + { + "rule": "optional", + "type": "CreateOfferDescription", + "name": "createOfferDescription", + "id": 6 + }, + { + "rule": "optional", + "type": "CreateAnswerDescription", + "name": "createAnswerDescription", + "id": 7 + } + ] + }, + { + "name": "IceServer", + "fields": [ + { + "rule": "repeated", + "type": "string", + "name": "urls", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "username", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "credential", + "id": 3 + } + ] + }, + { + "name": "RtcConfiguration", + "fields": [ + { + "rule": "optional", + "type": "BundlePolicy", + "name": "bundlePolicy", + "id": 1 + }, + { + "rule": "optional", + "type": "uint32", + "name": "iceCandidatePoolSize", + "id": 3 + }, + { + "rule": "repeated", + "type": "IceServer", + "name": "iceServers", + "id": 4 + }, + { + "rule": "optional", + "type": "IceTransportPolicy", + "name": "iceTransportPolicy", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "peerIdentity", + "id": 6 + }, + { + "rule": "optional", + "type": "RtcpMuxPolicy", + "name": "rtcpMuxPolicy", + "id": 7 + } + ], + "enums": [ + { + "name": "BundlePolicy", + "values": [ + { + "name": "BundlePolicyBalanced", + "id": 1 + }, + { + "name": "BundlePolicyMaxCompat", + "id": 2 + }, + { + "name": "BundlePolicyMaxBundle", + "id": 3 + } + ] + }, + { + "name": "IceTransportPolicy", + "values": [ + { + "name": "IceTransportPolicyAll", + "id": 1 + }, + { + "name": "IceTransportPolicyPublic", + "id": 2 + }, + { + "name": "IceTransportPolicyRelay", + "id": 3 + } + ] + }, + { + "name": "RtcpMuxPolicy", + "values": [ + { + "name": "RtcpMuxPolicyNegotiate", + "id": 1 + }, + { + "name": "RtcpMuxPolicyRequire", + "id": 2 + } + ] + } + ] + }, + { + "name": "CreateStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "instanceRouteKey", + "id": 5 + }, + { + "rule": "repeated", + "type": "string", + "name": "streamUris", + "id": 8 + }, + { + "rule": "optional", + "type": "RtcConfiguration", + "name": "rtcConfiguration", + "id": 9 + }, + { + "rule": "optional", + "type": "SetRemoteDescriptionResponse", + "name": "setRemoteDescriptionResponse", + "id": 3 + }, + { + "rule": "optional", + "type": "CreateOfferDescriptionResponse", + "name": "createOfferDescriptionResponse", + "id": 4 + }, + { + "rule": "optional", + "type": "CreateAnswerDescriptionResponse", + "name": "createAnswerDescriptionResponse", + "id": 6 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 7 + }, + { + "rule": "optional", + "type": "uint64", + "name": "offset", + "id": 10, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "SetLocalDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "required", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "SetLocalDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + } + ] + }, + { + "name": "SetRemoteDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "required", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "SetRemoteDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "CreateOfferDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "CreateOfferDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "CreateAnswerDescription", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "CreateAnswerDescriptionResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "SessionDescription", + "name": "sessionDescription", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "IceCandidate", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "candidate", + "id": 1 + }, + { + "rule": "required", + "type": "uint32", + "name": "sdpMLineIndex", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "sdpMid", + "id": 3 + } + ] + }, + { + "name": "AddIceCandidates", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "repeated", + "type": "IceCandidate", + "name": "candidates", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 4, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "AddIceCandidatesResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + } + ] + }, + { + "name": "UpdateStreamState", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "signalingState", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "iceGatheringState", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "iceConnectionState", + "id": 4 + }, + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 5, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "UpdateStreamStateResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 2 + } + ] + }, + { + "name": "DestroyStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "reason", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + } + ] + }, + { + "name": "DestroyStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "ConnectionDisconnected", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "connectionId", + "id": 1 + }, + { + "rule": "required", + "type": "uint32", + "name": "reasonCode", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "description", + "id": 3 + } + ] + }, + { + "name": "ConnectionDisconnectedResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "StreamStarted", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 3 + } + ] + }, + { + "name": "SourceStreamStarted", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + } + ] + }, + { + "name": "StreamEnded", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 5 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 6 + } + ] + }, + { + "name": "SourceStreamEnded", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 5 + } + ] + }, + { + "name": "StreamEndedResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "continuationId", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "routeKey", + "id": 3 + } + ] + }, + { + "name": "StreamIdle", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + } + ] + }, + { + "name": "StreamArchived", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "startTime", + "id": 4 + }, + { + "rule": "required", + "type": "string", + "name": "uri", + "id": 3 + } + ] + }, + { + "name": "SessionEnded", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "reason", + "id": 2 + }, + { + "rule": "required", + "type": "float", + "name": "duration", + "id": 3 + } + ] + }, + { + "name": "ResourceIdle", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "routeKey", + "id": 2 + } + ] + }, + { + "name": "ResourceIdleResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "StreamPlaylist", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "PlaylistType", + "name": "playlistType", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "uri", + "id": 4 + } + ], + "enums": [ + { + "name": "PlaylistType", + "values": [ + { + "name": "Live", + "id": 0 + }, + { + "name": "OnDemand", + "id": 1 + } + ] + } + ] + }, + { + "name": "SendEventToClient", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "connectionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "type", + "id": 2 + }, + { + "rule": "required", + "type": "bytes", + "name": "payload", + "id": 3 + } + ] + }, + { + "name": "SendEventToClientResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "SendRequestToClient", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "connectionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "type", + "id": 2 + }, + { + "rule": "required", + "type": "bytes", + "name": "payload", + "id": 3 + } + ] + }, + { + "name": "SendRequestToClientResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "type", + "id": 2 + }, + { + "rule": "optional", + "type": "bytes", + "name": "payload", + "id": 3 + } + ] + }, + { + "name": "SetupStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamToken", + "id": 1 + }, + { + "rule": "required", + "type": "CreateStream", + "name": "createStream", + "id": 2 + } + ] + }, + { + "name": "SetupStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "CreateStreamResponse", + "name": "createStreamResponse", + "id": 2 + } + ] + }, + { + "name": "SetupPlaylistStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamToken", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 3 + }, + { + "rule": "repeated", + "type": "string", + "name": "tags", + "id": 4 + } + ] + }, + { + "name": "PlaylistStreamManifest", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "manifest", + "id": 1 + }, + { + "rule": "required", + "type": "bool", + "name": "isProtectedContent", + "id": 2 + } + ] + }, + { + "name": "SetupPlaylistStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "repeated", + "type": "PlaylistStreamManifest", + "name": "manifests", + "id": 2 + }, + { + "rule": "optional", + "type": "uint64", + "name": "offset", + "id": 3, + "options": { + "default": 0 + } + } + ] + }, + { + "name": "StreamDataQuality", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 2 + }, + { + "rule": "required", + "type": "uint64", + "name": "timestamp", + "id": 3 + }, + { + "rule": "required", + "type": "DataQualityStatus", + "name": "status", + "id": 4 + }, + { + "rule": "required", + "type": "DataQualityReason", + "name": "reason", + "id": 5 + } + ], + "enums": [ + { + "name": "DataQualityStatus", + "values": [ + { + "name": "NoData", + "id": 0 + }, + { + "name": "AudioOnly", + "id": 1 + }, + { + "name": "All", + "id": 2 + } + ] + }, + { + "name": "DataQualityReason", + "values": [ + { + "name": "None", + "id": 0 + }, + { + "name": "UploadLimited", + "id": 1 + }, + { + "name": "DownloadLimited", + "id": 2 + }, + { + "name": "PublisherLimited", + "id": 3 + }, + { + "name": "NetworkLimited", + "id": 4 + } + ] + } + ] + }, + { + "name": "StreamDataQualityResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "CallbackEvent", + "fields": [ + { + "rule": "optional", + "type": "uint32", + "name": "apiVersion", + "id": 1, + "options": { + "default": 0 + } + }, + { + "rule": "required", + "type": "string", + "name": "entity", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "what", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "data", + "id": 4 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 5 + } + ] + }, + { + "name": "Uri", + "fields": [ + { + "rule": "optional", + "type": "string", + "name": "protocol", + "id": 1, + "options": { + "default": "http" + } + }, + { + "rule": "required", + "type": "string", + "name": "host", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "port", + "id": 3, + "options": { + "default": 80 + } + }, + { + "rule": "optional", + "type": "string", + "name": "method", + "id": 4, + "options": { + "default": "POST" + } + }, + { + "rule": "optional", + "type": "string", + "name": "path", + "id": 5, + "options": { + "default": "/" + } + } + ] + }, + { + "name": "SetApplicationCallback", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "required", + "type": "Uri", + "name": "callback", + "id": 3 + } + ] + }, + { + "name": "SetApplicationCallbackResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "IssueAuthenticationToken", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 3 + } + ] + }, + { + "name": "IssueAuthenticationTokenResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "authenticationToken", + "id": 2 + } + ] + }, + { + "name": "IssueStreamToken", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "required", + "type": "string", + "name": "sessionId", + "id": 3 + }, + { + "rule": "optional", + "type": "string", + "name": "originStreamId", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 5 + } + ] + }, + { + "name": "IssueStreamTokenResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "streamToken", + "id": 2 + } + ] + }, + { + "name": "IssueDrmToken", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "sessionId", + "id": 3 + }, + { + "rule": "required", + "type": "string", + "name": "originStreamId", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "capabilities", + "id": 5 + } + ] + }, + { + "name": "IssueDrmTokenResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "drmToken", + "id": 2 + } + ] + }, + { + "name": "TerminateStream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "streamId", + "id": 3, + "oneof": "streamOrToken" + }, + { + "rule": "optional", + "type": "string", + "name": "streamToken", + "id": 5, + "oneof": "streamOrToken" + }, + { + "rule": "optional", + "type": "string", + "name": "reason", + "id": 4 + } + ], + "oneofs": { + "streamOrToken": [3, 5] + } + }, + { + "name": "TerminateStreamResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + } + ] + }, + { + "name": "Stream", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "streamId", + "id": 1 + } + ] + }, + { + "name": "ListStreams", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "applicationId", + "id": 1 + }, + { + "rule": "required", + "type": "string", + "name": "secret", + "id": 2 + }, + { + "rule": "optional", + "type": "string", + "name": "start", + "id": 3 + }, + { + "rule": "required", + "type": "uint32", + "name": "length", + "id": 4 + }, + { + "rule": "repeated", + "type": "string", + "name": "options", + "id": 5 + } + ] + }, + { + "name": "ListStreamsResponse", + "fields": [ + { + "rule": "required", + "type": "string", + "name": "status", + "id": 1 + }, + { + "rule": "optional", + "type": "string", + "name": "start", + "id": 2 + }, + { + "rule": "optional", + "type": "uint32", + "name": "length", + "id": 3 + }, + { + "rule": "repeated", + "type": "Stream", + "name": "streams", + "id": 4 + } + ] + } + ] +} diff --git a/src/services/net/PhenixWebSocket.ts b/src/services/net/PhenixWebSocket.ts new file mode 100644 index 0000000..08e375c --- /dev/null +++ b/src/services/net/PhenixWebSocket.ts @@ -0,0 +1,140 @@ +import {MQWebSocket} from 'phenix-web-proto'; +import {ILogger} from 'services/logger/LoggerInterface'; +import LoggerFactory from 'services/logger/LoggerFactory'; +import assertUnreachable from 'lang/assertUnreachable'; +import {Subject, ReadOnlySubject, IDisposable} from '@techniker-me/tools'; +import pcastProtocol from 'services/messaging/pcast.proto.json'; +import analyticsProtocol from 'services/messaging/analytics.proto.json'; + +export enum PhenixWebSocketStatus { + Offline = 0, + Connecting = 1, + Online = 2, + Reconnecting = 3, + Error = 4 +} + +export type PhenixWebSocketStatusType = 'Offline' | 'Connecting' | 'Online' | 'Reconnecting' | 'Error'; + +export class PhenixWebSocketStatusMapping { + public static convertPhenixWebSocketStatusToPhenixWebSocketStatusType(status: PhenixWebSocketStatus): PhenixWebSocketStatusType { + switch (status) { + case PhenixWebSocketStatus.Error: + return 'Error'; + case PhenixWebSocketStatus.Offline: + return 'Offline'; + case PhenixWebSocketStatus.Connecting: + return 'Connecting'; + case PhenixWebSocketStatus.Online: + return 'Online'; + case PhenixWebSocketStatus.Reconnecting: + return 'Reconnecting'; + default: + assertUnreachable(status); + } + } + + public static convertPhenixWebSocketStatusTypeToPhenixWebSocketStatus(statusType: PhenixWebSocketStatusType): PhenixWebSocketStatus { + switch (statusType) { + case 'Error': + return PhenixWebSocketStatus.Error; + case 'Offline': + return PhenixWebSocketStatus.Offline; + case 'Connecting': + return PhenixWebSocketStatus.Connecting; + case 'Online': + return PhenixWebSocketStatus.Online; + case 'Reconnecting': + return PhenixWebSocketStatus.Reconnecting; + default: + assertUnreachable(statusType); + } + } +} + +export enum PhenixWebSocketMessage { + Authenticate = 'pcast.Authenticate', + Bye = 'pcast.Bye' +} + +export class PhenixWebSocket implements IDisposable { + private readonly _logger: ILogger = LoggerFactory.getLogger('PhenixWebSocket'); + private readonly _status: Subject = new Subject(PhenixWebSocketStatus.Offline); + private readonly _readOnlyStatus: ReadOnlySubject = new ReadOnlySubject(this._status); + private readonly _websocketMQ: MQWebSocket; + + constructor(uri: string) { + console.log('PhenixWebSocket constructor', uri); + this._websocketMQ = new MQWebSocket(uri, this._logger, [pcastProtocol, analyticsProtocol]); + this.initialize(); + } + + get status(): ReadOnlySubject { + return this._readOnlyStatus; + } + + public isConnected(): boolean { + return this._status.value === PhenixWebSocketStatus.Online; + } + + public isConnecting(): boolean { + return this._status.value === PhenixWebSocketStatus.Connecting; + } + + public isReconnecting(): boolean { + return this._status.value === PhenixWebSocketStatus.Reconnecting; + } + + public sendMessage(message: PhenixWebSocketMessage, data: T): Promise { + if (!Object.values(PhenixWebSocketMessage).includes(message)) { + throw new Error(`Invalid message: ${message}`); + } + + // Check if WebSocket is in a valid state for sending messages + const currentStatus = this._status.value; + if (currentStatus !== PhenixWebSocketStatus.Online) { + return Promise.reject(new Error(`WebSocket is not ready. Current status: ${PhenixWebSocketStatus[currentStatus]}`)); + } + + return new Promise((resolve, reject) => { + return this._websocketMQ.sendRequest(message, data, (error: unknown, response: unknown) => { + if (error) { + reject(error); + } else { + resolve({error, response}); + } + }); + }); + } + + public dispose(): void { + this._websocketMQ.disconnect(); + } + + private initialize(): void { + this._websocketMQ.onEvent('connecting', () => { + this._logger.info('WebSocket connecting...'); + this._status.value = PhenixWebSocketStatus.Connecting; + }); + + this._websocketMQ.onEvent('connected', () => { + this._logger.info('WebSocket connected successfully'); + this._status.value = PhenixWebSocketStatus.Online; + }); + + this._websocketMQ.onEvent('disconnected', () => { + this._logger.info('WebSocket disconnected'); + this._status.value = PhenixWebSocketStatus.Offline; + }); + + this._websocketMQ.onEvent('reconnecting', () => { + this._logger.info('WebSocket reconnecting...'); + this._status.value = PhenixWebSocketStatus.Reconnecting; + }); + + this._websocketMQ.onEvent('error', (error: unknown) => { + this._logger.error('WebSocket error occurred:', error); + this._status.value = PhenixWebSocketStatus.Error; + }); + } +} diff --git a/src/services/platform-detection.service.ts b/src/services/platform-detection.service.ts new file mode 100644 index 0000000..643a6d1 --- /dev/null +++ b/src/services/platform-detection.service.ts @@ -0,0 +1,165 @@ +export default class PlatformDetectionService { + private static _userAgent: string = navigator.userAgent; + private static _areClientHintsSupported: boolean = 'userAgentData' in navigator; + private static _platform: string = '?'; + private static _platformVersion: string = '?'; + private static _browser: string = 'Unknown'; + private static _version: string | number = '?'; + private static _isWebview: boolean = false; + private static _initialized: boolean = false; + + private constructor() { + throw new Error('PlatformDetectionService is a static class that may not be instantiated'); + } + + static get platform(): string { + this.initializeIfNeeded(); + return this._platform; + } + + static get platformVersion(): string { + this.initializeIfNeeded(); + return this._platformVersion; + } + + static get userAgent(): string { + return this._userAgent; + } + + static get browser(): string { + this.initializeIfNeeded(); + return this._browser; + } + + static get version(): string | number { + this.initializeIfNeeded(); + return this._version; + } + + static get isWebview(): boolean { + this.initializeIfNeeded(); + return this._isWebview; + } + + static get areClientHintsSupported(): boolean { + return this._areClientHintsSupported; + } + + private static initializeIfNeeded(): void { + if (this._initialized) return; + this.initialize(); + } + + private static initialize(): void { + try { + const browserVersionMatch = this._userAgent.match(/(Chrome|Chromium|Firefox|Opera|Safari|Edge|OPR)\/([0-9]+)/); + + if (browserVersionMatch) { + const [, browser, version] = browserVersionMatch; + PlatformDetectionService._browser = browser === 'OPR' ? 'Opera' : browser; + PlatformDetectionService._version = parseInt(version, 10)?.toString() || '?'; + } else if (this._userAgent.match(/^\(?Mozilla/)) { + PlatformDetectionService._browser = 'Mozilla'; + + // Check for IE/Edge + if (this._userAgent.match(/MSIE/) || this._userAgent.match(/; Trident\/.*rv:[0-9]+/)) { + PlatformDetectionService._browser = 'IE'; + const ieVersionMatch = this._userAgent.match(/rv:([0-9]+)/); + + if (ieVersionMatch) { + PlatformDetectionService._version = parseInt(ieVersionMatch[1], 10)?.toString() || '?'; + } + } else if (this._userAgent.match(/Edge\//)) { + PlatformDetectionService._browser = 'Edge'; + const edgeVersionMatch = this._userAgent.match(/Edge\/([0-9]+)/); + + if (edgeVersionMatch) { + PlatformDetectionService._version = parseInt(edgeVersionMatch[1], 10)?.toString() || '?'; + } + } + } + + // Handle Opera masquerading as other browsers + if (this._userAgent.match(/OPR\//)) { + PlatformDetectionService._browser = 'Opera'; + const operaVersionMatch = this._userAgent.match(/OPR\/([0-9]+)/); + if (operaVersionMatch) { + PlatformDetectionService._version = parseInt(operaVersionMatch[1], 10)?.toString() || '?'; + } + } + + // Safari and iOS webviews + if (this._userAgent.match(/AppleWebKit/i)) { + if (this._userAgent.match(/iphone|ipod|ipad/i)) { + PlatformDetectionService._browser = 'Safari'; + PlatformDetectionService._isWebview = true; + const iosVersionMatch = this._userAgent.match(/OS\s([0-9]+)/); + if (iosVersionMatch) { + PlatformDetectionService._version = parseInt(iosVersionMatch[1], 10)?.toString() || '?'; + } + } else if (this._userAgent.match(/Safari\//) && !this._userAgent.match(/Chrome/)) { + PlatformDetectionService._browser = 'Safari'; + const safariVersionMatch = this._userAgent.match(/Version\/([0-9]+)/); + if (safariVersionMatch) { + PlatformDetectionService._version = parseInt(safariVersionMatch[1], 10)?.toString() || '?'; + } + } + } + + // Android webviews + if (this._userAgent.match(/; wv/) || (this._userAgent.match(/Android/) && this._userAgent.match(/Version\/[0-9].[0-9]/))) { + PlatformDetectionService._isWebview = true; + } + + // React Native + if (globalThis.navigator.product === 'ReactNative') { + PlatformDetectionService._browser = 'ReactNative'; + PlatformDetectionService._version = navigator.productSub || '?'; + } + + // platform information + if (this._userAgent.match(/Windows/)) { + PlatformDetectionService._platform = 'Windows'; + const windowsVersionMatch = this._userAgent.match(/Windows NT ([0-9.]+)/); + + if (windowsVersionMatch) { + PlatformDetectionService._platformVersion = windowsVersionMatch[1]; + } + } else if (this._userAgent.match(/Mac OS X/)) { + PlatformDetectionService._platform = 'macOS'; + const macVersionMatch = this._userAgent.match(/Mac OS X ([0-9._]+)/); + + if (macVersionMatch) { + PlatformDetectionService._platformVersion = macVersionMatch[1].replace(/_/g, '.'); + } + } else if (this._userAgent.match(/Linux/)) { + PlatformDetectionService._platform = 'Linux'; + } else if (this._userAgent.match(/Android/)) { + PlatformDetectionService._platform = 'Android'; + const androidVersionMatch = this._userAgent.match(/Android ([0-9.]+)/); + + if (androidVersionMatch) { + PlatformDetectionService._platformVersion = androidVersionMatch[1]; + } + } else if (this._userAgent.match(/iPhone|iPad|iPod/)) { + PlatformDetectionService._platform = 'iOS'; + const iosVersionMatch = this._userAgent.match(/OS ([0-9_]+)/); + + if (iosVersionMatch) { + PlatformDetectionService._platformVersion = iosVersionMatch[1].replace(/_/g, '.'); + } + } + + this._initialized = true; + } catch (error) { + console.warn('Failed to initialize PlatformDetectionService:', error); + // fallback values + this._browser = 'Unknown'; + this._version = '?'; + this._platform = '?'; + this._platformVersion = '?'; + this._isWebview = false; + this._initialized = true; + } + } +} diff --git a/src/services/telemetry/TelemetryApender.ts b/src/services/telemetry/TelemetryApender.ts new file mode 100644 index 0000000..746c22b --- /dev/null +++ b/src/services/telemetry/TelemetryApender.ts @@ -0,0 +1,25 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import TelemetryService from './TelemetryService'; +import TelemetryConfiguration from './TelemetryConfiguration'; +import {IAppender} from '../logger/IAppender'; +import {LoggingLevel} from '../logger/Logger'; + +export default class TelemetryAppender implements IAppender { + private readonly _telemetryService: TelemetryService; + private readonly _threshold: LoggingLevel; + + constructor(telemetryConfiguration: TelemetryConfiguration) { + this._threshold = telemetryConfiguration.threshold; + this._telemetryService = new TelemetryService(telemetryConfiguration); + } + + async log(logLevel: LoggingLevel, message: string, category: string, date: Date): Promise { + if (logLevel < this._threshold) { + return; + } + + this._telemetryService.push(logLevel, message, category, date); + } +} diff --git a/src/services/telemetry/TelemetryConfiguration.ts b/src/services/telemetry/TelemetryConfiguration.ts new file mode 100644 index 0000000..844b82d --- /dev/null +++ b/src/services/telemetry/TelemetryConfiguration.ts @@ -0,0 +1,75 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import {LoggingLevel} from '../logger/Logger'; +import LoggerDefaults from '../logger/LoggerDefaults'; + +export default class TelemetryConfiguration { + private _url = 'https://telemetry.phenixrts.com/telemetry/logs'; + private _tenancy = ''; + private _userId = ''; + private _sessionId = ''; + private _environment = ''; + private _threshold = LoggerDefaults.defaultTelemetryLoggingLevel; + private _browser = ''; + + get url(): string { + return this._url; + } + + set url(url: string) { + const telemetryUrl = new URL(url); + + telemetryUrl.pathname = telemetryUrl.pathname + '/logs'; + + this._url = telemetryUrl.toString(); + } + + get environment(): string { + return this._environment; + } + + set environment(environment: string) { + this._environment = environment; + } + + get browser(): string { + return this._browser; + } + + set browser(browser: string) { + this._browser = browser; + } + + get tenancy(): string { + return this._tenancy; + } + + set tenancy(tenancy: string) { + this._tenancy = tenancy; + } + + get userId(): string { + return this._userId; + } + + set userId(userId: string) { + this._userId = userId; + } + + get sessionId(): string { + return this._sessionId; + } + + set sessionId(sessionId: string) { + this._sessionId = sessionId; + } + + get threshold(): LoggingLevel { + return this._threshold; + } + + set threshold(threshold: LoggingLevel) { + this._threshold = threshold; + } +} diff --git a/src/services/telemetry/TelemetryService.ts b/src/services/telemetry/TelemetryService.ts new file mode 100644 index 0000000..724f700 --- /dev/null +++ b/src/services/telemetry/TelemetryService.ts @@ -0,0 +1,137 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ +import config from 'config'; +import {LoggingLevel} from '../logger/Logger'; +import TelemetryConfiguration from './TelemetryConfiguration'; + +// Extend Window interface to include custom properties +declare global { + interface Window { + __phenixPageLoadTime?: number; + __pageLoadTime?: number; + } +} + +const requestSizeLimit = 8192; +const pageLoadTime = window.__phenixPageLoadTime || window.__pageLoadTime || Date.now(); + +interface ILogItem { + timestamp: string; + tenancy: string; + level: string; + category: string; + message: string; + sessionId: string; + userId: string; + version: string; + environment: string; + fullQualifiedName: string; + source: string; + runtime: number; +} + +export default class TelemetryService { + private readonly _telemetryConfiguration: TelemetryConfiguration; + + private _logs: Array = []; + private _isSending: boolean = false; + private _domain = location.hostname; + + constructor(telemetryConfiguration: TelemetryConfiguration) { + this._telemetryConfiguration = telemetryConfiguration; + } + + push(logLevel: LoggingLevel, message: string, category: string, timestamp: Date): void { + const now = Date.now(); + const runtime = (now - pageLoadTime) / 1000; + const logRecord = { + timestamp: timestamp.toISOString(), + tenancy: this._telemetryConfiguration.tenancy, + userId: this._telemetryConfiguration.userId, + level: LoggingLevel[logLevel], + runtime, + category, + message, + sessionId: this._telemetryConfiguration.sessionId, + version: config.controlCenterVersion, + environment: this._telemetryConfiguration.environment, + fullQualifiedName: this._domain, + source: `Portal (${this._telemetryConfiguration.browser})` + } as ILogItem; + + if (logLevel < LoggingLevel.Error) { + this._logs.push(logRecord); + } else { + this._logs.unshift(logRecord); + } + + + + // @ts-expect-error: Unused variable intentionally + const ignored = this.sendLogsIfAble(); + } + + private async sendLogs(logMessages: Array): Promise { + const formData = new FormData(); + + formData.append('jsonBody', JSON.stringify({records: logMessages})); + + return await fetch(this._telemetryConfiguration.url, { + method: 'POST', + body: formData + }); + } + + private async sendLogsIfAble(): Promise { + if (this._logs.length <= 0 || this._isSending) { + return; + } + + let numberOfLogsToSend = 0; + let sizeOfLogsToSend = 0; + + this._isSending = true; + + const getLogSize = (log: ILogItem): number => Object.values(log).reduce((sum, item) => sum + (item ? `${item}`.length : 0), 0); + + while (this._logs.length > numberOfLogsToSend && getLogSize(this._logs[numberOfLogsToSend]) + sizeOfLogsToSend < requestSizeLimit) { + sizeOfLogsToSend += getLogSize(this._logs[numberOfLogsToSend]); + numberOfLogsToSend++; + } + + if (!numberOfLogsToSend) { + this._logs[numberOfLogsToSend].message = this._logs[numberOfLogsToSend].message.substring( + 0, + getLogSize(this._logs[numberOfLogsToSend]) + (requestSizeLimit - getLogSize(this._logs[numberOfLogsToSend])) + ); + numberOfLogsToSend = 1; + } + + const logMessages = this._logs.slice(0, numberOfLogsToSend); + + this._logs = this._logs.slice(numberOfLogsToSend); + + return this.sendLogs(logMessages) + .then(response => { + this._isSending = false; + + + + // @ts-expect-error: Unused variable intentionally + + const ignored = this.sendLogsIfAble(); + + return response; + }) + .catch(() => { + this._isSending = false; + + + + // @ts-expect-error: Unused variable intentionally + + const ignored = this.sendLogsIfAble(); + }); + } +} diff --git a/src/services/url.service.ts b/src/services/url.service.ts new file mode 100644 index 0000000..7d8c632 --- /dev/null +++ b/src/services/url.service.ts @@ -0,0 +1,76 @@ +/** + * Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved. + */ + +export type UrlType = 'pcast' | 'telemetry' | 'websocket'; + +export default class UrlService { + private static getBaseUrlOrigin(url: string = window?.location?.href, urlType: UrlType = 'pcast'): string { + if (!url) { + throw new Error('Invalid URL'); + } + + const baseURL = new URL(url); + const backendUrlParam = baseURL.searchParams.get('backend'); + + if (backendUrlParam) { + const backendUrl = new URL(backendUrlParam); + if (urlType === 'websocket') { + backendUrl.protocol = 'wss:'; + backendUrl.pathname = '/ws'; + } + + return backendUrl.toString(); + } + + const segments = baseURL.hostname.split('.'); + const prefix = urlType === 'telemetry' ? 'telemetry' : 'pcast'; + + // Normalize WebSocket protocols to HTTP + switch (baseURL.protocol) { + case 'ws:': + baseURL.protocol = 'http:'; + break; + case 'wss:': + baseURL.protocol = 'https:'; + break; + default: + break; + } + + // Apply routing logic based on URL structure + if (segments.length === 2 || (segments.length === 3 && segments[segments.length - 2].length <= 2 && segments[segments.length - 1].length <= 3)) { + segments.unshift(prefix); + } else if (segments[0].startsWith('stg-') || segments[0].endsWith('-stg') || segments[0].includes('-stg-') || segments[0] === 'stg') { + segments[0] = `${prefix}-stg`; + } else if (segments[0].startsWith('local') || segments[0].endsWith('-local')) { + // Leave URL unchanged for local development + } else { + segments[0] = prefix; + } + + baseURL.hostname = segments.join('.'); + + + + return baseURL.origin; + } + + public static getPcastBaseUrlOrigin(url: string = window?.location?.href): string { + return this.getBaseUrlOrigin(url, 'pcast'); + } + + public static getTelemetryUrl(url: string = window?.location?.href): string { + try { + const baseUrlOrigin = this.getBaseUrlOrigin(url, 'telemetry'); + return `${baseUrlOrigin}/telemetry`; + // eslint-disable-next-line @typescript-eslint/no-unused-vars + } catch (e) { + return url; + } + } + + public static getWebSocketUrl(url: string = window?.location?.href): string { + return this.getBaseUrlOrigin(url, 'websocket'); + } +} diff --git a/src/store/index.ts b/src/store/index.ts new file mode 100644 index 0000000..d406816 --- /dev/null +++ b/src/store/index.ts @@ -0,0 +1 @@ +export * from './store'; diff --git a/src/store/middlewares/PhenixWebSocket.middleware.ts b/src/store/middlewares/PhenixWebSocket.middleware.ts new file mode 100644 index 0000000..7197b9e --- /dev/null +++ b/src/store/middlewares/PhenixWebSocket.middleware.ts @@ -0,0 +1,12 @@ +import { Middleware } from '@reduxjs/toolkit'; + +const phenixWebSocketMiddleware: Middleware = (storeApi) => (next) => (action) => { + console.log('phenixWebSocketMiddleware', action); + if (typeof action === 'function') { + return action(storeApi.getState(), storeApi.dispatch); + } + + return next(action); +}; + +export default phenixWebSocketMiddleware; \ No newline at end of file diff --git a/src/store/reducer.ts b/src/store/reducer.ts new file mode 100644 index 0000000..b4daa09 --- /dev/null +++ b/src/store/reducer.ts @@ -0,0 +1,8 @@ +import {combineReducers} from '@reduxjs/toolkit'; +import {authenticationReducer} from './slices'; + +const rootReducer = combineReducers({ + authentication: authenticationReducer +}); + +export default rootReducer; diff --git a/src/store/slices/Authentication.slice.ts b/src/store/slices/Authentication.slice.ts new file mode 100644 index 0000000..09ea1bc --- /dev/null +++ b/src/store/slices/Authentication.slice.ts @@ -0,0 +1,110 @@ +import {createSlice, createAsyncThunk, createSelector} from '@reduxjs/toolkit'; +import AuthenticationService from 'services/authentication.service'; +import {PhenixWebSocketStatusMapping, PhenixWebSocketStatusType} from 'services/net/PhenixWebSocket'; + +export interface IAuthenticationState { + applicationId: string | null; + secret: string | null; + isAuthenticated: boolean; + isAuthenticating: boolean; + error: string | null; + status: PhenixWebSocketStatusType; +} + +const initialAuthenticationState: IAuthenticationState = { + applicationId: null, + secret: null, + isAuthenticated: false, + isAuthenticating: false, + error: null, + status: 'Offline' +}; + +// Memoized selectors +export const selectAuthentication = (state: {authentication: IAuthenticationState}) => state.authentication; + +export const selectIsAuthenticating = createSelector([selectAuthentication], authentication => authentication.isAuthenticating); + +export const selectIsAuthenticated = createSelector([selectAuthentication], authentication => authentication.isAuthenticated); + +export const selectError = createSelector([selectAuthentication], authentication => authentication.error); + +export const selectStatus = createSelector([selectAuthentication], authentication => authentication.status); + +export const selectCredentials = createSelector([selectAuthentication], authentication => ({ + applicationId: authentication.applicationId, + secret: authentication.secret +})); + +const authenticateCredentialsThunk = createAsyncThunk( + 'authentication/authenticate', + async (credentials: {applicationId: string; secret: string}, {rejectWithValue}) => { + return AuthenticationService.authenticateCredentials(credentials.applicationId, credentials.secret).catch(rejectWithValue); + } +); + +const authenticationSlice = createSlice({ + name: 'authentication', + initialState: {...initialAuthenticationState}, + reducers: { + setStatus: (state, action) => { + state.status = PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(action.payload); + }, + signout: state => { + state.isAuthenticated = false; + state.isAuthenticating = false; + state.applicationId = null; + state.secret = null; + state.error = null; + }, + error: (state, action) => { + // Store only the error message string, not the Error object + if (typeof action.payload === 'string') { + state.error = action.payload; + } else if (action.payload instanceof Error) { + state.error = action.payload.message; + } else if (action.payload && typeof action.payload === 'object' && 'message' in action.payload) { + state.error = String((action.payload as {message: unknown}).message); + } else { + state.error = action.payload ? String(action.payload) : null; + } + } + }, + extraReducers: builder => { + builder + .addCase(authenticateCredentialsThunk.pending, state => { + state.isAuthenticating = true; + state.error = null; + }) + .addCase(authenticateCredentialsThunk.fulfilled, state => { + state.isAuthenticating = false; + state.error = null; + state.isAuthenticated = true; + }) + .addCase(authenticateCredentialsThunk.rejected, (state, action) => { + state.isAuthenticating = false; + // Extract error message string instead of creating new Error object + let errorMessage = 'Authentication failed'; + + if (action.payload) { + if (action.payload instanceof Error) { + errorMessage = action.payload.message; + } else if (typeof action.payload === 'object' && action.payload !== null && 'message' in action.payload) { + errorMessage = String((action.payload as {message: unknown}).message); + } else if (typeof action.payload === 'string') { + errorMessage = action.payload; + } else { + errorMessage = String(action.payload); + } + } + + state.error = errorMessage; + state.isAuthenticated = false; + state.applicationId = null; + state.secret = null; + }); + } +}); + +export const {reducer: authenticationReducer, actions: authenticationActions} = authenticationSlice; +export {authenticateCredentialsThunk}; diff --git a/src/store/slices/index.ts b/src/store/slices/index.ts new file mode 100644 index 0000000..2c7d54f --- /dev/null +++ b/src/store/slices/index.ts @@ -0,0 +1 @@ +export * from './Authentication.slice'; diff --git a/src/store/store.ts b/src/store/store.ts new file mode 100644 index 0000000..4fc30f1 --- /dev/null +++ b/src/store/store.ts @@ -0,0 +1,7 @@ +import {configureStore} from '@reduxjs/toolkit'; +import reducer from './reducer'; +import phenixWebSocketMiddleware from './middlewares/PhenixWebSocket.middleware'; + +export const store = configureStore({reducer, middleware: getDefaultMiddleware => getDefaultMiddleware().concat(phenixWebSocketMiddleware)}); + +export type RootState = ReturnType; diff --git a/src/types/phenix-web-proto.d.ts b/src/types/phenix-web-proto.d.ts new file mode 100644 index 0000000..fb6a54b --- /dev/null +++ b/src/types/phenix-web-proto.d.ts @@ -0,0 +1,22 @@ +declare module 'phenix-web-proto' { + import { ILogger } from 'services/logger/LoggerInterface'; + + export class MQWebSocket { + constructor(uri: string, logger: ILogger, protocols: any[], apiVersion?: string); + onEvent(eventName: string, handler: (event: unknown) => void): void; + onRequest(requestName: string, handler: (request: unknown) => void): void; + sendRequest(type: string, message: unknown, callback?: (error: unknown, response: unknown) => void, settings?: unknown): void; + sendResponse(requestId: string, type: string, message: unknown, callback?: (error: unknown, response: unknown) => void): void; + disconnect(): void; + disposeOfPendingRequests(): void; + getApiVersion(): string; + } + + export class BatchHttpProto { + // Add methods as needed + } + + export class MQService { + // Add methods as needed + } +} diff --git a/src/vite-env.d.ts b/src/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/src/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/tsconfig.app.json b/tsconfig.app.json new file mode 100644 index 0000000..026f2da --- /dev/null +++ b/tsconfig.app.json @@ -0,0 +1,46 @@ +{ + "compilerOptions": { + "target": "es2022", + "module": "esnext", + "moduleResolution": "bundler", + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "downlevelIteration": true, + "sourceMap": true, + "noEmit": true, + "noEmitHelpers": true, + "importHelpers": true, + "strictNullChecks": true, + "noUnusedParameters": true, + "noUnusedLocals": true, + "noImplicitAny": true, // TODO(AZ): Fix types to enable setting this to true + "lib": ["dom", "dom.iterable", "es2022"], + "baseUrl": "./src", + "paths": { + // Relative to baseUrl https://www.typescriptlang.org/tsconfig/#paths + "assets/*": ["./assets/*"], + "components/*": ["./components/*"], + "config/*": ["./config/*"], + "constant-data/*": ["./constant-data/*"], + "hooks/*": ["./hooks/*"], + "interfaces/*": ["./interfaces/*"], + "routers/*": ["./routers/*"], + "store/*": ["./store/*"], + "services/*": ["./services/*"], + "lang/*": ["./lang/*"], + "utility/*": ["./utility/*"], + "views/*": ["./views/*"] + }, + "allowJs": true, + "skipLibCheck": true, + "esModuleInterop": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "jsx": "react-jsx", + "noFallthroughCasesInSwitch": true + }, + "exclude": ["node_modules", "public"], + "include": ["src", "test"] +} diff --git a/tsconfig.app.json.orig b/tsconfig.app.json.orig new file mode 100644 index 0000000..227a6c6 --- /dev/null +++ b/tsconfig.app.json.orig @@ -0,0 +1,27 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", + "target": "ES2022", + "useDefineForClassFields": true, + "lib": ["ES2022", "DOM", "DOM.Iterable"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + "jsx": "react-jsx", + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["src"] +} diff --git a/tsconfig.app.tsbuildinfo b/tsconfig.app.tsbuildinfo new file mode 100644 index 0000000..ccd0f6f --- /dev/null +++ b/tsconfig.app.tsbuildinfo @@ -0,0 +1 @@ +{"root":["./src/app.tsx","./src/main.tsx","./src/vite-env.d.ts","./src/components/index.ts","./src/components/flexcontainer/flexcontainercomponent.tsx","./src/components/flexcontainer/index.ts","./src/components/inputfield/inputfield.tsx","./src/components/inputfield/index.ts","./src/components/loginform/loginform.tsx","./src/components/loginform/index.ts","./src/components/websocketstatusview/websocketstatusviewcomponent.tsx","./src/components/websocketstatusview/index.ts","./src/config/index.ts","./src/hooks/index.ts","./src/hooks/store.ts","./src/lang/strings.ts","./src/lang/assertunreachable.ts","./src/routes/index.ts","./src/services/authentication.service.ts","./src/services/host-url.service.ts","./src/services/platform-detection.service.ts","./src/services/logger/appenders.ts","./src/services/logger/consoleappender.ts","./src/services/logger/iappender.ts","./src/services/logger/logger.ts","./src/services/logger/loggerdefaults.ts","./src/services/logger/loggerfactory.ts","./src/services/logger/loggerinterface.ts","./src/services/logger/logginglevelmapping.ts","./src/services/logger/loggingthreshold.ts","./src/services/net/phenixwebsocket.ts","./src/services/telemetry/telemetryapender.ts","./src/services/telemetry/telemetryconfiguration.ts","./src/services/telemetry/telemetryservice.ts","./src/store/index.ts","./src/store/reducer.ts","./src/store/store.ts","./src/store/middlewares/phenixwebsocket.middleware.ts","./src/store/slices/authentication.slice.ts","./src/store/slices/index.ts","./src/types/phenix-web-proto.d.ts"],"version":"5.9.2"} \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..2340846 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,4 @@ +{ + "files": [], + "references": [{"path": "./tsconfig.app.json"}, {"path": "./tsconfig.node.json"}] +} diff --git a/tsconfig.node.json b/tsconfig.node.json new file mode 100644 index 0000000..f85a399 --- /dev/null +++ b/tsconfig.node.json @@ -0,0 +1,25 @@ +{ + "compilerOptions": { + "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", + "target": "ES2023", + "lib": ["ES2023"], + "module": "ESNext", + "skipLibCheck": true, + + /* Bundler mode */ + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "moduleDetection": "force", + "noEmit": true, + + /* Linting */ + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "erasableSyntaxOnly": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedSideEffectImports": true + }, + "include": ["vite.config.ts"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 0000000..ad740a5 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,33 @@ +import path from 'path'; +import {defineConfig} from 'vite'; +import react from '@vitejs/plugin-react-swc'; +import babel from 'vite-plugin-babel'; + +console.log('process.cwd()', process.cwd()); +// https://vite.dev/config/ +export default defineConfig({ + plugins: [ + react(), + babel({ + babelConfig: { + plugins: ['transform-amd-to-commonjs'] + } + }) + ], + resolve: { + alias: { + assets: path.resolve(process.cwd(), 'src', 'assets'), + components: path.resolve(process.cwd(), 'src/components'), + config: path.resolve(process.cwd(), 'src', 'config'), + 'constant-data': path.resolve(process.cwd(), 'src', 'constant-data'), + hooks: path.resolve(process.cwd(), 'src', 'hooks'), + interfaces: path.resolve(process.cwd(), 'src', 'interfaces'), + routers: path.resolve(process.cwd(), 'src', 'routers'), + store: path.resolve(process.cwd(), 'src', 'store'), + services: path.resolve(process.cwd(), 'src', 'services'), + lang: path.resolve(process.cwd(), 'src', 'lang'), + utility: path.resolve(process.cwd(), 'src', 'utility'), + views: path.resolve(process.cwd(), 'src', 'views') + } + } +});