This commit is contained in:
2025-08-17 07:19:39 -04:00
parent 2bc62eed01
commit 1c032da3df
10 changed files with 347 additions and 50 deletions

View File

@@ -1,14 +1,188 @@
import {HttpMethod} from '../net/http/HttpMethod';
import type {HttpRequests} from '../net/http/HttpRequests';
import type {PCastHttpRequests} from './PCastRequests';
import type {ChannelResponse, ChannelsResponse, MembersResponse} from './IResponse';
export class Channels {
private readonly _httpRequests: HttpRequests;
export type ChannelId = string;
export type Channel = {
options: string[];
alias: string;
name: string;
description: string;
type: string;
streamKey: string;
created: string;
lastUpdated: string;
channelId: string;
};
constructor(httpRequests: HttpRequests) {
this._httpRequests = httpRequests;
}
type GetChannelParams = {
alias?: string;
channelId?: string;
};
public async getChannels() {
return this._httpRequests.request(HttpMethod.Get, '/channels');
export class ChannelError extends Error {
constructor(message: string, public readonly code: string) {
super(message);
this.name = 'ChannelError';
}
}
export class Channels {
private readonly _httpRequests: PCastHttpRequests;
private readonly _channelsByAlias: Map<ChannelId, Channel> = new Map();
private _initialized = false;
constructor(pcastHttpRequests: PCastHttpRequests) {
this._httpRequests = pcastHttpRequests;
this.initialize();
}
private async initialize() {
try {
const channelsList = await this.list();
if (!channelsList) {
console.warn('[Channels] Failed to initialize cache - no channels returned');
return;
}
console.log('[Channels] [initialize] Populating local cache with [%o] channels', channelsList.length);
for (let channel of channelsList) {
this._channelsByAlias.set(channel.alias, channel);
}
this._initialized = true;
console.log('[Channels] [initialize] Cache populated successfully');
} catch (error) {
console.error('[Channels] Failed to initialize cache:', error);
}
}
public async create(name: string, description: string, channelOptions: string[] = []): Promise<Channel> {
// Input validation
if (!name || name.trim().length === 0) {
throw new ChannelError('Channel name cannot be empty', 'INVALID_NAME');
}
if (!description || description.trim().length === 0) {
throw new ChannelError('Channel description cannot be empty', 'INVALID_DESCRIPTION');
}
if (!Array.isArray(channelOptions)) {
throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS');
}
const createChannelRequestBody = {
channel: {
name: name.trim(),
alias: name.trim(),
description: description.trim(),
options: channelOptions
}
};
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.PUT, '/channel', {
body: JSON.stringify(createChannelRequestBody)
});
if (!response.channel) {
throw new ChannelError('Invalid response format - missing channel data', 'INVALID_RESPONSE');
}
// Update cache with new channel
this._channelsByAlias.set(response.channel.alias, response.channel);
return response.channel;
}
public async list(): Promise<Channel[]> {
const response = await this._httpRequests.request<ChannelsResponse>(HttpMethod.GET, '/channels');
if (!response.channels) {
throw new ChannelError('Invalid response format - missing channels data', 'INVALID_RESPONSE');
}
console.log('[Channels] [list] Response [%o]', response);
return response.channels;
}
public async get({alias, channelId}: GetChannelParams): Promise<Channel | undefined> {
if (!alias && !channelId) {
throw new ChannelError('Either alias or channelId must be provided', 'MISSING_PARAMETER');
}
if (alias) {
// Check cache first
if (this._channelsByAlias.has(alias)) {
return this._channelsByAlias.get(alias);
}
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.GET, `/channel/${encodeURIComponent(alias)}`);
if (!response.channel) {
throw new ChannelError(`Invalid response format for channel: ${alias}`, 'INVALID_RESPONSE');
}
// Update cache
this._channelsByAlias.set(alias, response.channel);
return response.channel;
}
if (channelId) {
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}`);
if (!response.channel) {
throw new ChannelError(`Invalid response format for channel ID: ${channelId}`, 'INVALID_RESPONSE');
}
return response.channel;
}
}
public async getMembers(channelId: string): Promise<Channel[]> {
if (!channelId || channelId.trim().length === 0) {
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
}
const response = await this._httpRequests.request<MembersResponse>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/members`);
if (!response.members) {
throw new ChannelError(`Invalid response format for channel members: ${channelId}`, 'INVALID_RESPONSE');
}
return response.members;
}
public async delete(channelId: string): Promise<Channel> {
if (!channelId || channelId.trim().length === 0) {
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
}
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.DELETE, `/channel/${encodeURIComponent(channelId)}`);
if (!response.channel) {
throw new ChannelError(`Invalid response format for deleted channel: ${channelId}`, 'INVALID_RESPONSE');
}
// Remove from cache if it exists
const deletedChannel = response.channel;
if (this._channelsByAlias.has(deletedChannel.alias)) {
this._channelsByAlias.delete(deletedChannel.alias);
}
return deletedChannel;
}
// Cache management methods
public isInitialized(): boolean {
return this._initialized;
}
public clearCache(): void {
this._channelsByAlias.clear();
this._initialized = false;
}
public getCacheSize(): number {
return this._channelsByAlias.size;
}
}

19
src/pcast/IResponse.ts Normal file
View File

@@ -0,0 +1,19 @@
import type { Channel } from './Channels';
export default interface IResponse<Key extends string, T> {
status: string;
[key: string]: T | string;
}
// More specific response types for better type safety
export interface ChannelResponse extends IResponse<'channel', Channel> {
channel: Channel;
}
export interface ChannelsResponse extends IResponse<'channels', Channel[]> {
channels: Channel[];
}
export interface MembersResponse extends IResponse<'members', Channel[]> {
members: Channel[];
}

View File

@@ -0,0 +1,26 @@
import { HttpRequests} from "../net/http/HttpRequests";
export type ApplicationCredentials = {
id: string;
secret: string;
}
export class PCastHttpRequests extends HttpRequests {
private readonly _tenancy: string;
constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) {
const baseHeaders = new Headers({
'Content-Type': 'application/json',
Accept: 'application/json',
Authorization: `Basic ${btoa(`${applicationCredentials.id}:${applicationCredentials.secret}`)}`
});
super(baseUri, baseHeaders, options);
this._tenancy = applicationCredentials.id;
}
get tenancy(): string {
return this._tenancy;
}
}

33
src/pcast/Stream.ts Normal file
View File

@@ -0,0 +1,33 @@
import { HttpMethod } from "../net/http/HttpMethod";
import type IResponse from "./IResponse";
import type { PCastHttpRequests } from "./PCastRequests";
export class Streams {
private readonly _httpRequests: PCastHttpRequests;
constructor(httpRequests: PCastHttpRequests) {
this._httpRequests = httpRequests;
}
public async publishUri(mediaUri: string, token: string) {
const mediaType = mediaUri.split('.')?[mediaUri.at(-1)];
if (!mediaType) {
throw new Error('Invalid media URI no media type found');
}
const response = await this._httpRequests.request<IResponse<'publishUri', string>>(HttpMethod.PUT, `/stream/publish/uri/${mediaType}`, {
body: JSON.stringify({
token,
uri: mediaUri,
options: []
})
});
return response;
}
}

3
src/pcast/index.ts Normal file
View File

@@ -0,0 +1,3 @@
export * from './Channels';
export * from './Stream';
export * from './PCastRequests';