Initial Commit
This commit is contained in:
74
src/services/Authentication.service.ts
Normal file
74
src/services/Authentication.service.ts
Normal 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();
|
||||
247
src/services/Channel.service.ts
Normal file
247
src/services/Channel.service.ts
Normal 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;
|
||||
42
src/services/PCastApi.service.ts
Normal file
42
src/services/PCastApi.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
191
src/services/PlatformDetection.service.ts
Normal file
191
src/services/PlatformDetection.service.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
6
src/services/UserDataStore/IUserDataStore.ts
Normal file
6
src/services/UserDataStore/IUserDataStore.ts
Normal 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;
|
||||
}
|
||||
23
src/services/UserDataStore/IndexedDB.ts
Normal file
23
src/services/UserDataStore/IndexedDB.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
23
src/services/UserDataStore/LocalStorage.ts
Normal file
23
src/services/UserDataStore/LocalStorage.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
23
src/services/UserDataStore/ObjectStore.ts
Normal file
23
src/services/UserDataStore/ObjectStore.ts
Normal 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');
|
||||
}
|
||||
}
|
||||
24
src/services/UserDataStore/index.ts
Normal file
24
src/services/UserDataStore/index.ts
Normal 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();
|
||||
26
src/services/logger/Appenders.ts
Normal file
26
src/services/logger/Appenders.ts
Normal 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>);
|
||||
}
|
||||
}
|
||||
29
src/services/logger/ConsoleAppender.ts
Normal file
29
src/services/logger/ConsoleAppender.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
8
src/services/logger/IAppender.ts
Normal file
8
src/services/logger/IAppender.ts
Normal 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;
|
||||
}
|
||||
195
src/services/logger/Logger.ts
Normal file
195
src/services/logger/Logger.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
51
src/services/logger/LoggerDefaults.ts
Normal file
51
src/services/logger/LoggerDefaults.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
78
src/services/logger/LoggerFactory.ts
Normal file
78
src/services/logger/LoggerFactory.ts
Normal 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();
|
||||
27
src/services/logger/LoggerInterface.ts
Normal file
27
src/services/logger/LoggerInterface.ts
Normal 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 */
|
||||
56
src/services/logger/LoggingLevelMapping.ts
Normal file
56
src/services/logger/LoggingLevelMapping.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/services/logger/LoggingThreshold.ts
Normal file
17
src/services/logger/LoggingThreshold.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
118
src/services/net/websockets/PhenixWebSocket.ts
Normal file
118
src/services/net/websockets/PhenixWebSocket.ts
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
33
src/services/net/websockets/PhenixWebSocketMessage.ts
Normal file
33
src/services/net/websockets/PhenixWebSocketMessage.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
44
src/services/net/websockets/PhenixWebSocketStatus.ts
Normal file
44
src/services/net/websockets/PhenixWebSocketStatus.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
391
src/services/net/websockets/proto/analytics.proto.json
Normal file
391
src/services/net/websockets/proto/analytics.proto.json
Normal 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
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1777
src/services/net/websockets/proto/pcast.proto.json
Normal file
1777
src/services/net/websockets/proto/pcast.proto.json
Normal file
File diff suppressed because it is too large
Load Diff
25
src/services/telemetry/TelemetryApender.ts
Normal file
25
src/services/telemetry/TelemetryApender.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
75
src/services/telemetry/TelemetryConfiguration.ts
Normal file
75
src/services/telemetry/TelemetryConfiguration.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
131
src/services/telemetry/TelemetryService.ts
Normal file
131
src/services/telemetry/TelemetryService.ts
Normal 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();
|
||||
});
|
||||
}
|
||||
}
|
||||
4
src/services/user-store/IUserDataStore.ts
Normal file
4
src/services/user-store/IUserDataStore.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
export default interface IUserDataStore {
|
||||
get(key: string, defaultValue: string): Promise<string>;
|
||||
set(key: string, value: string): Promise<void>;
|
||||
}
|
||||
27
src/services/user-store/index.ts
Normal file
27
src/services/user-store/index.ts
Normal 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();
|
||||
90
src/services/user-store/indexed-db.ts
Normal file
90
src/services/user-store/indexed-db.ts
Normal 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'));
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
31
src/services/user-store/local-storage.ts
Normal file
31
src/services/user-store/local-storage.ts
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
25
src/services/user-store/object-store.ts
Normal file
25
src/services/user-store/object-store.ts
Normal 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();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user