Initial Commit

This commit is contained in:
2025-09-07 01:46:37 -04:00
commit 66986cca51
272 changed files with 15331 additions and 0 deletions

View File

@@ -0,0 +1,74 @@
import LoggerFactory from './logger/LoggerFactory';
import ILogger from './logger/LoggerInterface';
import PlatformDetectionService from './PlatformDetection.service';
import {AuthenticationResponse, PhenixWebSocket} from './net/websockets/PhenixWebSocket';
import {PhenixWebSocketMessage} from './net/websockets/PhenixWebSocketMessage';
import UserStoreService from './user-store';
//TEMPORARY
import config from '../config';
class AuthenticationService {
private static readonly _instance = new AuthenticationService();
private readonly _logger: ILogger = LoggerFactory.getLogger('AuthenticationService');
private _token: string | null = null;
private _phenixWebSocket: PhenixWebSocket;
public static getInstance(): AuthenticationService {
return AuthenticationService._instance;
}
constructor() {
const backenUrl = 'wss://pcast-stg.phenixrts.com/ws';
this._phenixWebSocket = new PhenixWebSocket(backenUrl);
}
get sessionId(): string | null {
return this._phenixWebSocket.sessionId;
}
get token(): string | null {
return this._token;
}
async authenticate(applicationId: string, secret: string): Promise<AuthenticationResponse> {
const authenticate = {
// @ts-expect-error TODO(AZ): phenix-web-proto does not have Typescript types defined definition
apiVersion: this._phenixWebSocket.getApiVersion(), // TODO(AZ): add types for phenix-proto-web
clientVersion: config.controlCenterVersion,
deviceId: '',
platform: PlatformDetectionService.platform,
platformVersion: PlatformDetectionService.platformVersion,
browser: PlatformDetectionService.browser,
browserVersion: PlatformDetectionService.version,
applicationId,
authenticationToken: secret,
sessionId: this.sessionId
};
try {
const authenticationResponse = await this._phenixWebSocket.sendMessage(PhenixWebSocketMessage.Authenticate, authenticate);
if (authenticationResponse.status === 'ok') {
this._phenixWebSocket.sessionId = authenticationResponse.sessionId;
await UserStoreService.set('applicationId', applicationId);
await UserStoreService.set('secret', secret);
}
return authenticationResponse;
} catch (error) {
this._logger.error('Authentication failed [%s]', error);
throw error;
}
}
public async signout(): Promise<void> {
await this._phenixWebSocket.sendMessage(PhenixWebSocketMessage.Bye, {
sessionId: this.sessionId,
reason: 'signout'
});
}
}
export default AuthenticationService.getInstance();

View File

@@ -0,0 +1,247 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import {Channel, Channels} from '@techniker-me/pcast-api';
import PCastApiService from './PCastApi.service';
import LoggerFactory from './logger/LoggerFactory';
const logger = LoggerFactory.getLogger('services/Channel.service');
export interface CreateChannelParams {
name: string;
description?: string;
options?: string[];
}
export interface UpdateChannelParams {
channelId: string;
name?: string;
alias?: string;
description?: string;
tags?: string[];
capabilities?: string[];
}
export interface ForkChannelParams {
sourceChannelId: string;
destinationChannelId: string;
streamCapabilities?: string[];
streamTags?: string[];
options?: string[];
}
export interface DeleteChannelParams {
channelId: string;
alias: string;
}
export interface KillChannelParams {
channelId: string;
reason?: string;
options?: string[];
enteredAlias?: string;
}
/**
* Channel Service providing CRUD operations for channels
*/
class ChannelService {
private phenixChannelService: PhenixChannelService | null = null;
public async initializeWithPCastApi(channels: Channels) {
try {
// Wait for PCastApiService to be initialized
this.phenixChannelService = channels;
} catch (error) {
logger.error('Failed to initialize ChannelService', error);
}
}
/**
* Get all channels
*/
async listChannels(): Promise<Channel[]> {
try {
logger.debug('Fetching channel list');
if (!this.phenixChannelService) {
return PCastApiService.channels.list();
}
return await this.phenixChannelService.list();
} catch (error) {
logger.error('Failed to list channels', error);
throw error;
}
}
/**
* Create a new channel
*/
async createChannel(params: CreateChannelParams): Promise<Channel> {
try {
logger.debug('Creating channel', params);
const channelData = {
name: params.name,
alias: params.alias,
description: params.description || '',
tags: params.tags || [],
capabilities: params.capabilities || []
};
if (!this.phenixChannelService) {
return PCastApiService.channels.create(name, description, options);
}
return await this.phenixChannelService.create(channelData);
} catch (error) {
logger.error('Failed to create channel', error);
throw error;
}
}
/**
* Update an existing channel
*/
async updateChannel(params: UpdateChannelParams): Promise<Channel> {
try {
logger.debug('Updating channel', params);
const updateData = {
...(params.name && {name: params.name}),
...(params.alias && {alias: params.alias}),
...(params.description && {description: params.description}),
...(params.tags && {tags: params.tags}),
...(params.capabilities && {capabilities: params.capabilities})
};
if (!this.phenixChannelService) {
return PCastApiService.channels.update(params.channelId, updateData);
}
return await this.phenixChannelService.update(params.channelId, updateData);
} catch (error) {
logger.error('Failed to update channel', error);
throw error;
}
}
/**
* Delete a channel
*/
async deleteChannel(params: DeleteChannelParams): Promise<void> {
try {
logger.debug('Deleting channel', params);
if (!this.phenixChannelService) {
return PCastApiService.channels.delete(params.channelId);
}
await this.phenixChannelService.delete(params.channelId);
} catch (error) {
logger.error('Failed to delete channel', error);
throw error;
}
}
/**
* Fork a channel
*/
async forkChannel(params: ForkChannelParams): Promise<void> {
try {
logger.debug('Forking channel', params);
const forkData = {
destinationChannelId: params.destinationChannelId,
streamCapabilities: params.streamCapabilities || [],
streamTags: params.streamTags || [],
options: params.options || []
};
// Phenix SDK fork method
if (!this.phenixChannelService) {
return PCastApiService.channels.fork(params.sourceChannelId, forkData);
}
await this.phenixChannelService.fork(params.sourceChannelId, forkData);
} catch (error) {
logger.error('Failed to fork channel', error);
throw error;
}
}
/**
* Kill a channel
*/
async killChannel(params: KillChannelParams): Promise<void> {
try {
logger.debug('Killing channel', params);
const killData = {
reason: params.reason || 'portal:killed',
options: params.options || []
};
if (!this.phenixChannelService) {
return PCastApiService.channels.kill(params.channelId, killData);
}
await this.phenixChannelService.kill(params.channelId, killData);
} catch (error) {
logger.error('Failed to kill channel', error);
throw error;
}
}
/**
* Get channel by ID
*/
async getChannel(channelId: string): Promise<Channel | null> {
try {
logger.debug('Getting channel', {channelId});
if (!this.phenixChannelService) {
return PCastApiService.channels.get(channelId);
}
return await this.phenixChannelService.get(channelId);
} catch (error) {
logger.error('Failed to get channel', error);
throw error;
}
}
/**
* Get publisher count for a channel
*/
async getPublisherCount(channelId: string): Promise<number> {
try {
logger.debug('Getting publisher count', {channelId});
if (!this.phenixChannelService) {
return PCastApiService.channels.getPublisherCount(channelId);
}
return await this.phenixChannelService.getPublisherCount(channelId);
} catch (error) {
logger.error('Failed to get publisher count', error);
return 0;
}
}
}
// Export singleton instance
const channelService = new ChannelService();
// Named exports for individual operations
export const listChannels = channelService.listChannels.bind(channelService);
export const createChannel = channelService.createChannel.bind(channelService);
export const updateChannel = channelService.updateChannel.bind(channelService);
export const deleteChannel = channelService.deleteChannel.bind(channelService);
export const forkChannel = channelService.forkChannel.bind(channelService);
export const killChannel = channelService.killChannel.bind(channelService);
export const getChannel = channelService.getChannel.bind(channelService);
export const getPublisherCount = channelService.getPublisherCount.bind(channelService);
export default channelService;

View File

@@ -0,0 +1,42 @@
import {ApplicationCredentials, Channels, PCastApi, Reporting, Streams} from '@techniker-me/pcast-api';
export default class PCastApiService {
private static _instance: PCastApi;
public static initialize(pcastUri: string, applciationCredentials: ApplicationCredentials) {
console.log(`${new Date().toISOString()} [PCastApiService] initialize pcastUri [%o] applciation [%o]`, pcastUri, applciationCredentials);
PCastApiService._instance = PCastApi.create(pcastUri, applciationCredentials);
}
public static getInstance(): PCastApiService {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance;
}
static get channels(): Channels {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance.channels;
}
static get streams(): Streams {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance.streams;
}
static get reporting(): Reporting {
if (!PCastApiService._instance) {
throw new Error('PCastApiService has not been initialized');
}
return PCastApiService._instance.reporting;
}
}

View File

@@ -0,0 +1,191 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
type NavigatorUAData = {
brands?: {brand: string; version: string}[];
mobile?: boolean;
platform?: string;
getHighEntropyValues?: (hints: string[]) => Promise<Record<string, string>>;
toJSON?: () => object;
};
export default class PlatformDetectionService {
private static readonly _userAgent: string = globalThis.navigator?.userAgent ?? '';
// @ts-expect-error NavigatorUAData is experimental and not defined in the lib dom yet https://developer.mozilla.org/en-US/docs/Web/API/Navigator/userAgentData
private static readonly _userAgentData: NavigatorUAData | undefined = globalThis.navigator?.userAgentData;
private static readonly _areClientHintsSupported: boolean = !!PlatformDetectionService._userAgentData;
private static _platform: string = 'Unknown';
private static _platformVersion: string = '';
private static _browserName: string = 'Unknown';
private static _browserVersion: string = '?';
private static _isWebview: boolean = false;
static {
if (PlatformDetectionService._areClientHintsSupported) {
PlatformDetectionService.initFromClientHints();
} else {
PlatformDetectionService.initFromUserAgent();
}
}
private constructor() {
throw new Error('PlatformDetectionService is a static class that may not be instantiated');
}
// ---- Public API ----
static get platform(): string {
return PlatformDetectionService._platform;
}
static get platformVersion(): string {
return PlatformDetectionService._platformVersion;
}
static get userAgent(): string {
return PlatformDetectionService._userAgent;
}
static get browserName(): string {
return PlatformDetectionService._browserName;
}
static get browserVersion(): string {
return PlatformDetectionService._browserVersion;
}
static get isWebview(): boolean {
return PlatformDetectionService._isWebview;
}
static get areClientHintsSupported(): boolean {
return PlatformDetectionService._areClientHintsSupported;
}
/**
* Optional async initialization for high-entropy values like platformVersion
*/
static async initAsync(): Promise<void> {
if (PlatformDetectionService._areClientHintsSupported && PlatformDetectionService._userAgentData?.getHighEntropyValues) {
const values = await PlatformDetectionService._userAgentData.getHighEntropyValues(['platformVersion']);
if (values.platformVersion) {
PlatformDetectionService._platformVersion = values.platformVersion;
}
}
}
// ---- Init strategies ----
private static initFromClientHints() {
const data = PlatformDetectionService._userAgentData as NavigatorUAData;
const nonChromiumBrand = data.brands?.find(b => b.brand !== 'Chromium');
PlatformDetectionService._browserName = nonChromiumBrand?.brand ?? 'Unknown';
PlatformDetectionService._browserVersion = nonChromiumBrand?.version ?? '?';
PlatformDetectionService._platform = data.platform ?? 'Unknown';
PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent(); // Fallback check
}
private static initFromUserAgent() {
PlatformDetectionService._platform = PlatformDetectionService.extractPlatformFromUserAgent();
PlatformDetectionService._platformVersion = PlatformDetectionService.extractPlatformVersionFromUserAgent();
PlatformDetectionService._browserName = PlatformDetectionService.extractBrowserNameFromUserAgent();
PlatformDetectionService._browserVersion = PlatformDetectionService.extractBrowserVersionFromUserAgent();
PlatformDetectionService._isWebview = PlatformDetectionService.extractIsWebviewFromUserAgent();
}
// ---- Helpers ----
private static extractBrowserNameFromUserAgent(): string {
if (/Edg\//.test(PlatformDetectionService._userAgent)) {
return 'Edge';
}
if (/OPR\//.test(PlatformDetectionService._userAgent)) {
return 'Opera';
}
if (/Firefox\//.test(PlatformDetectionService._userAgent)) {
return 'Firefox';
}
if (/Trident\/.*rv:/.test(PlatformDetectionService._userAgent)) {
return 'IE';
}
if (/Chrome\//.test(PlatformDetectionService._userAgent)) {
return 'Chrome';
}
if (/Safari\//.test(PlatformDetectionService._userAgent)) {
return 'Safari';
}
if (/ReactNative\//.test(PlatformDetectionService._userAgent)) {
return 'ReactNative';
}
return 'Unknown';
}
private static extractBrowserVersionFromUserAgent(): string {
return (
PlatformDetectionService.matchVersion(/Edg\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/OPR\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/Firefox\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/rv:([\d.]+)/) ?? // IE
PlatformDetectionService.matchVersion(/Chrome\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/Version\/([\d.]+)/) ?? // Safari often uses "Version/"
PlatformDetectionService.matchVersion(/Safari\/([\d.]+)/) ??
PlatformDetectionService.matchVersion(/ReactNative\/([\d.]+)/) ??
'?'
);
}
private static extractPlatformFromUserAgent(): string {
if (/Windows/.test(PlatformDetectionService._userAgent)) {
return 'Windows';
}
if (/iPhone|iPad|iPod/.test(PlatformDetectionService._userAgent)) {
return 'iOS';
}
if (/Mac OS X/.test(PlatformDetectionService._userAgent)) {
return 'macOS';
}
if (/Android/.test(PlatformDetectionService._userAgent)) {
return 'Android';
}
if (/Linux/.test(PlatformDetectionService._userAgent)) {
return 'Linux';
}
return 'Unknown';
}
private static extractPlatformVersionFromUserAgent(): string {
switch (PlatformDetectionService._platform) {
case 'Windows':
return PlatformDetectionService.matchVersion(/Windows NT ([\d.]+)/) ?? '';
case 'iOS':
return PlatformDetectionService.matchVersion(/OS ([\d_]+)/)?.replace(/_/g, '.') ?? '';
case 'macOS':
return PlatformDetectionService.matchVersion(/Mac OS X ([\d_]+)/)?.replace(/_/g, '.') ?? '';
case 'Android':
return PlatformDetectionService.matchVersion(/Android ([\d.]+)/) ?? '';
default:
return '';
}
}
private static extractIsWebviewFromUserAgent(): boolean {
return (
/; wv/.test(PlatformDetectionService._userAgent) || // Android webview
(/Android/.test(PlatformDetectionService._userAgent) && /Version\/[\d.]+/.test(PlatformDetectionService._userAgent)) || // Some Android webviews
/(iPhone|iPod|iPad).*AppleWebKit(?!.*Safari)/.test(PlatformDetectionService._userAgent) // IOS webview
);
}
private static matchVersion(pattern: RegExp): string | null {
const match = PlatformDetectionService._userAgent.match(pattern);
return match ? match[1] : null;
}
}

View File

@@ -0,0 +1,6 @@
export default interface IUserDataStore {
setItem(key: string, value: string): void;
getItem(key: string): string | null;
removeItem(key: string): void;
clear(): void;
}

View File

@@ -0,0 +1,23 @@
import IUserDataStore from './IUserDataStore';
export class IndexedDB implements IUserDataStore {
static isSupported(): boolean {
return 'indexedDB' in window;
}
public getItem(ignoredKey: string): string | null {
throw new Error('Not Implemented');
}
public setItem(ignoredKey: string, ignoredValue: string): void {
throw new Error('Not Implemented');
}
public removeItem(ignoredKey: string): void {
throw new Error('Not Implemented');
}
public clear(): void {
throw new Error('Not Implemented');
}
}

View File

@@ -0,0 +1,23 @@
import IUserDataStore from './IUserDataStore';
export class LocalStorage implements IUserDataStore {
static isSupported(): boolean {
return 'localStorage' in window;
}
public getItem(ignoredKey: string): string | null {
throw new Error('Not Implemented');
}
public setItem(ignoredKey: string, ignoredValue: string): void {
throw new Error('Not Implemented');
}
public removeItem(ignoredKey: string): void {
throw new Error('Not Implemented');
}
public clear(): void {
throw new Error('Not Implemented');
}
}

View File

@@ -0,0 +1,23 @@
import IUserDataStore from './IUserDataStore';
export class ObjectStrore implements IUserDataStore {
static isSupported(): boolean {
return true;
}
public getItem(ignoredKey: string): string | null {
throw new Error('Not Implemented');
}
public setItem(ignoredKey: string, ignoredValue: string): void {
throw new Error('Not Implemented');
}
public removeItem(ignoredKey: string): void {
throw new Error('Not Implemented');
}
public clear(): void {
throw new Error('Not Implemented');
}
}

View File

@@ -0,0 +1,24 @@
import IUserDataStore from './IUserDataStore';
import {IndexedDB} from './IndexedDB';
import {LocalStorage} from './LocalStorage';
import {ObjectStrore} from './ObjectStore';
class UserDataStoreService {
private static _instance: IUserDataStore;
static {
if (IndexedDB.isSupported()) {
this._instance = new IndexedDB();
} else if (LocalStorage.isSupported()) {
this._instance = new LocalStorage();
} else {
this._instance = new ObjectStrore();
}
}
public static getInstance(): IUserDataStore {
return this._instance;
}
}
export default UserDataStoreService.getInstance();

View File

@@ -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<IAppender> = [];
get value(): Array<IAppender> {
return this._appenders;
}
add(appender: IAppender): void {
this._appenders.push(appender);
}
remove(appender): void {
this._appenders = this._appenders.reduce((items, item) => {
if (!(item === appender)) {
items.push(item);
}
return items;
}, [] as Array<IAppender>);
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}

View File

@@ -0,0 +1,195 @@
/**
* 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) {
args = args.reduce((accumulator, currentValue, index, array) => {
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;
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,78 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
// import TelemetryUrl from '/telemetry/TelemetryUrl';
import PlatformDetectionService from '../PlatformDetection.service';
import TelemetryConfiguration from '../telemetry/TelemetryConfiguration';
import TelemetryAppender from '../telemetry/TelemetryApender';
// import hostService from 'services/host-url.service';
// 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();
static get telemetryConfiguration(): TelemetryConfiguration {
return this._telemetryConfiguration;
}
static applyLoggerConfig(): void {
LoggerFactory.applyConsoleLogger(LoggingLevel['All']);
LoggerFactory.applyLoggingLevel();
LoggerFactory.applyTelemetryLogger();
}
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<void> {
const browser = PlatformDetectionService.browser;
const applicationId = 'phenixrts.com-alex.zinn'; // TEMPORARY --> FOR DEVELOPMENT ONLY
this._telemetryConfiguration.threshold = level || LoggerDefaults.defaultTelemetryLoggingLevel;
this._telemetryConfiguration.url = 'https://pcast-stg.phenixrts.com/telemetry'; //TelemetryUrl.getTelemetryUrl();
this._telemetryConfiguration.environment = 'https://pcast-stg.phenixrts.com'; // TODO(AZ): hostService.getHostUrl();
this._telemetryConfiguration.tenancy = applicationId; // TODO(AZ): await userStore.get('applicationId');
this._telemetryConfiguration.userId = applicationId;
this._telemetryConfiguration.sessionId = 'some-session-id'; // TODOD(AZ): await userStore.get('sessionId');
this._telemetryConfiguration.browser = browser ? `${browser}/${PlatformDetectionService.version}` : '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');
}
}
LoggerFactory.applyLoggerConfig();

View File

@@ -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 default 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 */

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,118 @@
import Strings from 'lang/Strings';
import ILogger from '../../logger/LoggerInterface';
import LoggerFactory from '../../logger/LoggerFactory';
import {Subject, ReadOnlySubject} from '@techniker-me/tools';
import {MQWebSocket} from 'phenix-web-proto'; //TODO(AZ): add types
import PCastProtobuf from './proto/pcast.proto.json' with {type: 'json'};
import AnalyticsProtobuf from './proto/Analytics.proto.json' with {type: 'json'};
import {PhenixWebSocketStatus, PhenixWebSocketStatusMapping} from './PhenixWebSocketStatus';
import {PhenixWebSocketMessage, PhenixWebSocketMessageMapping} from './PhenixWebSocketMessage';
export type AuthenticationResponse = {
status: string;
applicationId?: string;
sessionId?: string;
};
export interface IPhenixWebSocketResponse {
status: 'ok';
applicationId: string;
sessionId: string;
redirect: string;
roles: string[];
[key: string]: unknown;
}
export class PhenixWebSocket extends MQWebSocket {
private readonly _logger: ILogger;
private readonly _status: Subject<PhenixWebSocketStatus> = new Subject<PhenixWebSocketStatus>(PhenixWebSocketStatus.Offline);
private readonly _readOnlySubject: ReadOnlySubject<PhenixWebSocketStatus> = new ReadOnlySubject<PhenixWebSocketStatus>(this._status);
private readonly _socketId: string = Strings.randomString(10);
private _sessionId: string | null = null;
private _pendingRequests: number = 0;
constructor(url: string) {
const logger = LoggerFactory.getLogger('PhenixWebSocket');
super(url, logger, [PCastProtobuf, AnalyticsProtobuf]);
this._logger = logger;
this.initialize();
}
get status(): ReadOnlySubject<PhenixWebSocketStatus> {
return this._readOnlySubject;
}
get pendingRequests(): number {
return this._pendingRequests;
}
get socketId(): string {
return this._socketId;
}
get sessionId(): string | null {
return this._sessionId;
}
set sessionId(sessionId: string | null) {
this._sessionId = sessionId;
}
public async sendMessage<T>(kind: PhenixWebSocketMessage, message: T): Promise<IPhenixWebSocketResponse> {
if (this._status.value !== PhenixWebSocketStatus.Online) {
throw new Error(
`Unable to send message, web socket is not [Online] WebSocket status [${PhenixWebSocketStatusMapping.convertPhenixWebSocketStatusToPhenixWebSocketStatusType(this._status.value)}]`
);
}
this._pendingRequests++;
const messageKind = PhenixWebSocketMessageMapping.convertPhenixWebSocketMessageToPhenixWebSocketMessageType(kind);
this._logger.debug(`Sending [${messageKind}] message [%j]`, message);
return new Promise<IPhenixWebSocketResponse>((resolve, reject) => {
super.sendRequest(messageKind, message, (error: unknown, response: IPhenixWebSocketResponse) => {
this._pendingRequests--;
if (error) {
reject(error);
} else {
resolve(response);
}
});
});
}
private setStatus(status: PhenixWebSocketStatus): void {
this._status.value = status;
}
private initialize(): void {
super.onEvent('connected', () => {
this.setStatus(PhenixWebSocketStatus.Online);
});
super.onEvent('disconnected', () => {
this.setStatus(PhenixWebSocketStatus.Offline);
});
super.onEvent('error', (error: unknown) => {
this._logger.error('Error [%s]', error);
this.setStatus(PhenixWebSocketStatus.Error);
});
super.onEvent('reconnecting', () => {
this.setStatus(PhenixWebSocketStatus.Reconnecting);
});
super.onEvent('reconnected', () => {
this.setStatus(PhenixWebSocketStatus.Online);
});
super.onEvent('timeout', () => {
this.setStatus(PhenixWebSocketStatus.Error);
});
}
}

View File

@@ -0,0 +1,33 @@
import assertUnreachable from 'lang/assertUnreachable';
export enum PhenixWebSocketMessage {
Authenticate = 0,
Bye = 1
}
export type PhenixWebSocketMessageType = 'pcast.Authenticate' | 'pcast.Bye';
export class PhenixWebSocketMessageMapping {
public static convertPhenixWebSocketMessageToPhenixWebSocketMessageType(message: PhenixWebSocketMessage): PhenixWebSocketMessageType {
switch (message) {
case PhenixWebSocketMessage.Authenticate:
return 'pcast.Authenticate';
case PhenixWebSocketMessage.Bye:
return 'pcast.Bye';
default:
assertUnreachable(message);
}
}
public static convertPhenixWebSocketMessageTypeToPhenixWebSocketMessage(messageType: PhenixWebSocketMessageType): PhenixWebSocketMessage {
switch (messageType) {
case 'pcast.Authenticate':
return PhenixWebSocketMessage.Authenticate;
case 'pcast.Bye':
return PhenixWebSocketMessage.Bye;
default:
assertUnreachable(messageType);
}
}
}

View File

@@ -0,0 +1,44 @@
import assertUnreachable from '../../../lang/assertUnreachable';
export enum PhenixWebSocketStatus {
Offline = 0,
Online = 1,
Reconnecting = 2,
Error = 3
}
export type PhenixWebSocketStatusType = 'Offline' | 'Online' | 'Reconnecting' | 'Error';
export class PhenixWebSocketStatusMapping {
public static convertPhenixWebSocketStatusToPhenixWebSocketStatusType(status: PhenixWebSocketStatus): PhenixWebSocketStatusType {
switch (status) {
case PhenixWebSocketStatus.Offline:
return 'Offline';
case PhenixWebSocketStatus.Online:
return 'Online';
case PhenixWebSocketStatus.Reconnecting:
return 'Reconnecting';
case PhenixWebSocketStatus.Error:
return 'Error';
default:
assertUnreachable(status);
}
}
public static convertPhenixWebSocketStatusTypeToPhenixWebSocketStatus(statusType: PhenixWebSocketStatusType): PhenixWebSocketStatus {
switch (statusType) {
case 'Offline':
return PhenixWebSocketStatus.Offline;
case 'Online':
return PhenixWebSocketStatus.Online;
case 'Reconnecting':
return PhenixWebSocketStatus.Reconnecting;
case 'Error':
return PhenixWebSocketStatus.Error;
default:
assertUnreachable(statusType);
}
}
}

View File

@@ -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
}
]
}
]
}

File diff suppressed because it is too large Load Diff

View File

@@ -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<void> {
if (logLevel < this._threshold) {
return;
}
this._telemetryService.push(logLevel, message, category, date);
}
}

View File

@@ -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;
}
}

View File

@@ -0,0 +1,131 @@
/**
* 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<ILogItem> = [];
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<ILogItem>): Promise<Response | void> {
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<Response | void> {
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();
});
}
}

View File

@@ -0,0 +1,4 @@
export default interface IUserDataStore {
get(key: string, defaultValue: string): Promise<string>;
set(key: string, value: string): Promise<void>;
}

View File

@@ -0,0 +1,27 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import IndexedDb from 'services/user-store/indexed-db';
import LocalStorage from 'services/user-store/local-storage';
import ObjectStore from 'services/user-store/object-store';
import IUserDataStore from 'services/user-store/IUserDataStore';
class UserStoreService {
private static _instance: IUserDataStore;
public static getInstance(): IUserDataStore {
return this._instance;
}
static {
if (IndexedDb.browserSupported()) {
this._instance = new IndexedDb();
} else if (LocalStorage.browserSupported()) {
this._instance = new LocalStorage();
} else {
this._instance = new ObjectStore();
}
}
}
export default UserStoreService.getInstance();

View File

@@ -0,0 +1,90 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import IUserDataStore from './IUserDataStore';
export default class IndexedDb implements IUserDataStore {
private readonly _dbName = '__phenix_store__';
private readonly _storeName = 'user';
private readonly _version = 1;
private _db: IDBDatabase | null = null;
private _initializationPromise: Promise<void> | null = null;
constructor() {
// Database will be initialized on first use
}
public static browserSupported(): boolean {
return 'indexedDB' in window;
}
public async get(key: string, defaultValue: string): Promise<string> {
await this.ensureInitialized();
return new Promise((resolve): void => {
const transaction = this._db!.transaction(this._storeName, 'readonly');
const store = transaction.objectStore(this._storeName);
const request = store.get(key);
request.onsuccess = (): void => {
resolve(request.result !== undefined ? request.result : defaultValue);
};
request.onerror = (): void => {
resolve(defaultValue);
};
});
}
public async set(key: string, value: string): Promise<void> {
await this.ensureInitialized();
return new Promise((resolve, reject): void => {
const transaction = this._db!.transaction(this._storeName, 'readwrite');
const store = transaction.objectStore(this._storeName);
const request = store.put(value, key);
request.onsuccess = (): void => {
resolve();
};
request.onerror = (): void => {
reject(new Error('Failed to store value in IndexedDB'));
};
});
}
private async ensureInitialized(): Promise<void> {
if (this._db) {
return;
}
if (this._initializationPromise) {
return this._initializationPromise;
}
this._initializationPromise = this.initialize();
return this._initializationPromise;
}
private async initialize(): Promise<void> {
return new Promise((resolve, reject): void => {
const openRequest = indexedDB.open(this._dbName, this._version);
openRequest.onupgradeneeded = (event): void => {
const db = (event.target as IDBOpenDBRequest).result;
if (!db.objectStoreNames.contains(this._storeName)) {
db.createObjectStore(this._storeName);
}
};
openRequest.onsuccess = (): void => {
this._db = openRequest.result;
resolve();
};
openRequest.onerror = (): void => {
reject(new Error('Failed to open IndexedDB'));
};
});
}
}

View File

@@ -0,0 +1,31 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import IUserDataStore from './IUserDataStore';
export default class LocalStorageUserStore implements IUserDataStore {
public async get(key: string, defaultValue: string): Promise<string> {
return window.localStorage.getItem(key) || defaultValue;
}
public async set(key: string, value: string): Promise<void> {
return window.localStorage.setItem(key, value);
}
public static browserSupported(): boolean {
try {
if (!window.localStorage) {
return false;
}
const name = '__enableability_test__';
localStorage.setItem(name, 'Evaluating whether local storage is enabled and accessible');
localStorage.removeItem(name);
return true;
} catch (ignoredEx) {
return false;
}
}
}

View File

@@ -0,0 +1,25 @@
/**
* Copyright 2024 Phenix Real Time Solutions, Inc. Confidential and Proprietary. All Rights Reserved.
*/
import IUserDataStore from './IUserDataStore';
export default class ObjectStoreUserStore implements IUserDataStore {
private store: Record<string, string> = {};
constructor() {
this.store = {};
this.get = this.get.bind(this);
this.set = this.set.bind(this);
}
public async get(key: string, defaultValue: string): Promise<string> {
return Promise.resolve(this.store[key] || defaultValue);
}
public async set(key: string, value: string): Promise<void> {
this.store[key] = value;
return Promise.resolve();
}
}