maintenance

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

View File

@@ -1,8 +1,9 @@
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 { AuthenticationResponse, PhenixWebSocket } from './net/websockets/PhenixWebSocket';
import { PhenixWebSocketMessage } from './net/websockets/PhenixWebSocketMessage';
import UserStoreService from './user-store';
//TEMPORARY
import config from '../config';
@@ -10,6 +11,7 @@ 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 {
@@ -25,6 +27,10 @@ class AuthenticationService {
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
@@ -45,6 +51,8 @@ class AuthenticationService {
if (authenticationResponse.status === 'ok') {
this._phenixWebSocket.sessionId = authenticationResponse.sessionId;
await UserStoreService.set('applicationId', applicationId);
await UserStoreService.set('secret', secret);
}
return authenticationResponse;

View File

@@ -0,0 +1,249 @@
/**
* 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

@@ -4,6 +4,7 @@ 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);
}

View File

@@ -1,165 +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 _userAgent: string = navigator.userAgent;
private static _areClientHintsSupported: boolean = 'userAgentData' in navigator;
private static _platform: string = '?';
private static _platformVersion: string = '?';
private static _browser: string = 'Unknown';
private static _version: string | number = '?';
private static 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;
private static _initialized: 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 {
this.initializeIfNeeded();
return this._platform;
return PlatformDetectionService._platform;
}
static get platformVersion(): string {
this.initializeIfNeeded();
return this._platformVersion;
return PlatformDetectionService._platformVersion;
}
static get userAgent(): string {
return this._userAgent;
return PlatformDetectionService._userAgent;
}
static get browser(): string {
this.initializeIfNeeded();
return this._browser;
static get browserName(): string {
return PlatformDetectionService._browserName;
}
static get version(): string | number {
this.initializeIfNeeded();
return this._version;
static get browserVersion(): string {
return PlatformDetectionService._browserVersion;
}
static get isWebview(): boolean {
this.initializeIfNeeded();
return this._isWebview;
return PlatformDetectionService._isWebview;
}
static get areClientHintsSupported(): boolean {
return this._areClientHintsSupported;
return PlatformDetectionService._areClientHintsSupported;
}
private static initializeIfNeeded(): void {
if (this._initialized) return;
this.initialize();
}
/**
* 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']);
private static initialize(): void {
try {
const browserVersionMatch = this._userAgent.match(/(Chrome|Chromium|Firefox|Opera|Safari|Edge|OPR)\/([0-9]+)/);
if (browserVersionMatch) {
const [, browser, version] = browserVersionMatch;
PlatformDetectionService._browser = browser === 'OPR' ? 'Opera' : browser;
PlatformDetectionService._version = parseInt(version, 10)?.toString() || '?';
} else if (this._userAgent.match(/^\(?Mozilla/)) {
PlatformDetectionService._browser = 'Mozilla';
// Check for IE/Edge
if (this._userAgent.match(/MSIE/) || this._userAgent.match(/; Trident\/.*rv:[0-9]+/)) {
PlatformDetectionService._browser = 'IE';
const ieVersionMatch = this._userAgent.match(/rv:([0-9]+)/);
if (ieVersionMatch) {
PlatformDetectionService._version = parseInt(ieVersionMatch[1], 10)?.toString() || '?';
}
} else if (this._userAgent.match(/Edge\//)) {
PlatformDetectionService._browser = 'Edge';
const edgeVersionMatch = this._userAgent.match(/Edge\/([0-9]+)/);
if (edgeVersionMatch) {
PlatformDetectionService._version = parseInt(edgeVersionMatch[1], 10)?.toString() || '?';
}
}
if (values.platformVersion) {
PlatformDetectionService._platformVersion = values.platformVersion;
}
// Handle Opera masquerading as other browsers
if (this._userAgent.match(/OPR\//)) {
PlatformDetectionService._browser = 'Opera';
const operaVersionMatch = this._userAgent.match(/OPR\/([0-9]+)/);
if (operaVersionMatch) {
PlatformDetectionService._version = parseInt(operaVersionMatch[1], 10)?.toString() || '?';
}
}
// Safari and iOS webviews
if (this._userAgent.match(/AppleWebKit/i)) {
if (this._userAgent.match(/iphone|ipod|ipad/i)) {
PlatformDetectionService._browser = 'Safari';
PlatformDetectionService._isWebview = true;
const iosVersionMatch = this._userAgent.match(/OS\s([0-9]+)/);
if (iosVersionMatch) {
PlatformDetectionService._version = parseInt(iosVersionMatch[1], 10)?.toString() || '?';
}
} else if (this._userAgent.match(/Safari\//) && !this._userAgent.match(/Chrome/)) {
PlatformDetectionService._browser = 'Safari';
const safariVersionMatch = this._userAgent.match(/Version\/([0-9]+)/);
if (safariVersionMatch) {
PlatformDetectionService._version = parseInt(safariVersionMatch[1], 10)?.toString() || '?';
}
}
}
// Android webviews
if (this._userAgent.match(/; wv/) || (this._userAgent.match(/Android/) && this._userAgent.match(/Version\/[0-9].[0-9]/))) {
PlatformDetectionService._isWebview = true;
}
// React Native
if (globalThis.navigator.product === 'ReactNative') {
PlatformDetectionService._browser = 'ReactNative';
PlatformDetectionService._version = navigator.productSub || '?';
}
// platform information
if (this._userAgent.match(/Windows/)) {
PlatformDetectionService._platform = 'Windows';
const windowsVersionMatch = this._userAgent.match(/Windows NT ([0-9.]+)/);
if (windowsVersionMatch) {
PlatformDetectionService._platformVersion = windowsVersionMatch[1];
}
} else if (this._userAgent.match(/Mac OS X/)) {
PlatformDetectionService._platform = 'macOS';
const macVersionMatch = this._userAgent.match(/Mac OS X ([0-9._]+)/);
if (macVersionMatch) {
PlatformDetectionService._platformVersion = macVersionMatch[1].replace(/_/g, '.');
}
} else if (this._userAgent.match(/Linux/)) {
PlatformDetectionService._platform = 'Linux';
} else if (this._userAgent.match(/Android/)) {
PlatformDetectionService._platform = 'Android';
const androidVersionMatch = this._userAgent.match(/Android ([0-9.]+)/);
if (androidVersionMatch) {
PlatformDetectionService._platformVersion = androidVersionMatch[1];
}
} else if (this._userAgent.match(/iPhone|iPad|iPod/)) {
PlatformDetectionService._platform = 'iOS';
const iosVersionMatch = this._userAgent.match(/OS ([0-9_]+)/);
if (iosVersionMatch) {
PlatformDetectionService._platformVersion = iosVersionMatch[1].replace(/_/g, '.');
}
}
this._initialized = true;
} catch (error) {
console.warn('Failed to initialize PlatformDetectionService:', error);
// fallback values
this._browser = 'Unknown';
this._version = '?';
this._platform = '?';
this._platformVersion = '?';
this._isWebview = false;
this._initialized = true;
}
}
}
// ---- 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

@@ -5,15 +5,15 @@ export class IndexedDB implements IUserDataStore {
return 'indexedDB' in window;
}
public getItem(key: string): string | null {
public getItem(ignoredKey: string): string | null {
throw new Error('Not Implemented');
}
public setItem(key: string, value: string): void {
public setItem(ignoredKey: string, ignoredValue: string): void {
throw new Error('Not Implemented');
}
public removeItem(key: string): void {
public removeItem(ignoredKey: string): void {
throw new Error('Not Implemented');
}

View File

@@ -5,15 +5,15 @@ export class LocalStorage implements IUserDataStore {
return 'localStorage' in window;
}
public getItem(key: string): string | null {
public getItem(ignoredKey: string): string | null {
throw new Error('Not Implemented');
}
public setItem(key: string, value: string): void {
public setItem(ignoredKey: string, ignoredValue: string): void {
throw new Error('Not Implemented');
}
public removeItem(key: string): void {
public removeItem(ignoredKey: string): void {
throw new Error('Not Implemented');
}

View File

@@ -5,15 +5,15 @@ export class ObjectStrore implements IUserDataStore {
return true;
}
public getItem(key: string): string | null {
public getItem(ignoredKey: string): string | null {
throw new Error('Not Implemented');
}
public setItem(key: string, value: string): void {
public setItem(ignoredKey: string, ignoredValue: string): void {
throw new Error('Not Implemented');
}
public removeItem(key: string): void {
public removeItem(ignoredKey: string): void {
throw new Error('Not Implemented');
}

View File

@@ -1,191 +0,0 @@
/**
* 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,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();
}
}