maintenance
This commit is contained in:
@@ -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;
|
||||
|
||||
249
src/services/Channel.service.ts
Normal file
249
src/services/Channel.service.ts
Normal 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;
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
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