diff --git a/eslint.config.ts b/eslint.config.ts index 54d89d6..58aa2d3 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -1,9 +1,9 @@ -import js from "@eslint/js"; -import globals from "globals"; -import tseslint from "typescript-eslint"; -import { defineConfig } from "eslint/config"; +import js from '@eslint/js'; +import globals from 'globals'; +import tseslint from 'typescript-eslint'; +import {defineConfig} from 'eslint/config'; export default defineConfig([ - { files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: {...globals.browser, ...globals.node} } }, - tseslint.configs.recommended, + {files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], plugins: {js}, extends: ['js/recommended'], languageOptions: {globals: {...globals.browser, ...globals.node}}}, + tseslint.configs.recommended ]); diff --git a/examples/src/index.ts b/examples/src/index.ts index af48a02..5ddd990 100644 --- a/examples/src/index.ts +++ b/examples/src/index.ts @@ -1,12 +1,15 @@ -import PCastApi from "../../src/index"; +import PCastApi from '../../src/index'; -const pacstUri = "https://pcast-stg.phenixrts.com" +const pcastUri = 'https://pcast-stg.phenixrts.com'; const application = { - id: "phenixrts.com-alex.zinn", - secret: "AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg" -} -const pcastApi = new PCastApi(pacstUri, application); + id: 'phenixrts.com-alex.zinn', + secret: 'AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg' +}; +const pcastApi = PCastApi.create(pcastUri, application); +console.log('pcastApi [%o]', pcastApi); +console.log(); +console.log('ChannelsApi [%o]', pcastApi.channels); const channelsList = await pcastApi.channels.list(); // const start = hrtime.bigint(); @@ -19,7 +22,6 @@ const channelsList = await pcastApi.channels.list(); // console.log(publishingReportCsv); // console.log(`Time taken: ${Number(endPublishing - start) / 1_000_000_000} seconds`); - // const viewingReportCsv = await pcastApi.reporting.generateReport(ReportKind.Viewing, { // kind: ViewingReportKind.HLS, // start: moment().subtract(1, 'day').toISOString(), @@ -34,4 +36,4 @@ const channelsList = await pcastApi.channels.list(); const channelMembers = await pcastApi.channels.getMembers(channelsList[1].channelId); console.log('[%o]', channelMembers); -console.log('[%o]', channelMembers.streams); \ No newline at end of file +console.log('[%o]', channelMembers.streams); diff --git a/package.json b/package.json index 4a64dae..c49bf7d 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,15 @@ { "name": "@techniker-me/pcast-api", - "version": "2025.0.10", + "version": "2025.1.0", "type": "module", "scripts": { "ci-build": "bun run build", - "format": "prettier --write ./", "test": "bun test", "test:watch": "bun test --watch", "test:coverage": "bun test --coverage", - "prelint": "bun install", + "preformat": "bun install", + "format": "prettier --write ./", + "prelint": "bun format", "lint": "eslint src/**/*.ts", "lint:fix": "eslint src/**/*.ts --fix", "build:node": "bun build src/index.ts --outdir dist/node --target node --format esm --production", @@ -21,7 +22,6 @@ "build:dev": "bun run build:node:dev && bun run build:browser:dev && bun run build:types:dev", "prebuild": "bun run clean", "build": "bun run build:node && bun run build:browser && bun run build:types", - "postbuild": "bash scripts/prepare-package-json.sh", "postclean": "bun run lint", "clean": "rm -rf dist", "prepublish": "bash scripts/pre-publish.sh" diff --git a/scripts/pre-publish.sh b/scripts/pre-publish.sh index 0a52925..f019910 100755 --- a/scripts/pre-publish.sh +++ b/scripts/pre-publish.sh @@ -4,10 +4,7 @@ set -o errexit set -o nounset set -o pipefail -echo "Preparing package.json for distributions" +echo "Preparing package.json for distribution" bash scripts/prepare-package-json.sh -echo "Copying .npmrc to dist/" -cp .npmrc dist/ - echo "Pre-publish done" \ No newline at end of file diff --git a/src/PCastApi.ts b/src/PCastApi.ts index 24812dc..c2237ae 100644 --- a/src/PCastApi.ts +++ b/src/PCastApi.ts @@ -1,34 +1,25 @@ import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests, Reporting} from './pcast'; export class PCastApi { - private readonly _pcastUri: string; - private readonly _applicationCredentials: ApplicationCredentials; - private readonly _pcastHttpRequests: PCastHttpRequests; private readonly _channels: Channels; private readonly _streams: Streams; private readonly _reporting: Reporting; - private constructor(pcastUri: string, applicationCredentials: ApplicationCredentials, channels: Channels) { - const normalized = pcastUri.replace(/\/+$/, ''); - this._pcastUri = normalized.endsWith('/pcast') ? normalized : `${normalized}/pcast`; - - this._applicationCredentials = applicationCredentials; - this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials); + private constructor(channels: Channels, streams: Streams, reporting: Reporting) { this._channels = channels; - this._streams = new Streams(this._pcastHttpRequests); - this._reporting = new Reporting(this._pcastHttpRequests); + this._streams = streams; + this._reporting = reporting; } - public static async create(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise { + public static create(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise { const pcastHttpRequests = new PCastHttpRequests(pcastUri.replace(/\/+$/, '').endsWith('/pcast') ? pcastUri : `${pcastUri}/pcast`, applicationCredentials); - const channels = await Channels.create(pcastHttpRequests); - return new PCastApi(pcastUri, applicationCredentials, channels); + const channels = new Channels(pcastHttpRequests); + const streams = new Streams(pcastHttpRequests); + const reporting = new Reporting(pcastHttpRequests); + + return new PCastApi(channels, streams, reporting); } - get pcastUri(): string { - return this._pcastUri; - } - get channels(): Channels { return this._channels; } diff --git a/src/net/http/HttpRequests.ts b/src/net/http/HttpRequests.ts index 602a91f..2f60f94 100644 --- a/src/net/http/HttpRequests.ts +++ b/src/net/http/HttpRequests.ts @@ -52,7 +52,7 @@ export class HttpRequests { try { const requestPath = `${this._baseUri}${path}`; - + const response = await fetch(requestPath, options); if (!response.ok) { diff --git a/src/pcast/Channels.ts b/src/pcast/Channels.ts index 6806849..f3ad4aa 100644 --- a/src/pcast/Channels.ts +++ b/src/pcast/Channels.ts @@ -20,16 +20,16 @@ export type Member = { screenName: string; role: string; streams: [ - { - type: string; - uri: string; - audioState: string; - videoState: string; - } - ], + { + type: string; + uri: string; + audioState: string; + videoState: string; + } + ]; state: string; lastUpdate: number; -} +}; type GetChannelParams = { alias?: string; @@ -49,31 +49,26 @@ export class ChannelError extends Error { export class Channels { private readonly _httpRequests: PCastHttpRequests; private readonly _channelsByAlias: Map = new Map(); - private _initialized: Promise | boolean = false; private constructor(pcastHttpRequests: PCastHttpRequests) { this._httpRequests = pcastHttpRequests; - } - - public static async create(pcastHttpRequests: PCastHttpRequests): Promise { - const instance = new Channels(pcastHttpRequests); - await instance.initialize(); - return instance; + this.initialize(); } public async createChannel(name: string, description: string, channelOptions: string[] = []): Promise { - // 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 = { + const createChannel = { channel: { name: name.trim(), alias: name.trim(), @@ -82,15 +77,14 @@ export class Channels { } }; - const response = await this._httpRequests.request(HttpMethod.PUT, '/channel', { - body: JSON.stringify(createChannelRequestBody) - }); + const route = '/channel'; + const requestOptions = {body: JSON.stringify(createChannel)}; + const response = await this._httpRequests.request(HttpMethod.PUT, route, requestOptions); 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; @@ -113,7 +107,8 @@ export class Channels { } public async refreshCache(): Promise { - await this.list(); // This clears and repopulates the cache + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const ignored = await this.list(); } public async getChannelInfoByAlias(alias: string): Promise { @@ -125,41 +120,22 @@ export class Channels { throw new ChannelError('Either alias or channelId must be provided', 'MISSING_PARAMETER'); } - if (this._initialized === false) { - await this.initialize(); - } else if (this._initialized instanceof Promise) { - await this._initialized; + if (alias && this._channelsByAlias.has(alias)) { + return this._channelsByAlias.get(alias); } - if (alias) { - // Check cache first - if (this._channelsByAlias.has(alias)) { - return this._channelsByAlias.get(alias); - } + const channelList = await this.list(); - // Instead of fetching full list, try to fetch single channel if possible - // Assuming API has /channel/{alias}, but based on code, it doesn't; fallback to list - const channelList = await this.list(); - - // Update cache - return channelList.find(channel => channel.alias === alias); - } - - if (channelId) { - // Similar fallback - const channelList = await this.list(); - - return channelList.find(channel => channel.channelId === channelId); - } + return alias ? channelList.find(channel => channel.alias === alias) : channelList.find(channel => channel.channelId === channelId); } - public async getChannelPublisherCount(channelId: string): Promise { + public async getPublisherCount(channelId: string): Promise { const response = await this._httpRequests.request(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/publishers/count`); return parseInt(response, 10); } - public async getChannelMembers(channelId: string): Promise { + public async getMembers(channelId: string): Promise { if (!channelId || channelId.trim().length === 0) { throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID'); } @@ -173,29 +149,32 @@ export class Channels { return response.members; } - public async getChannelMembersByAlias(alias: string): Promise { + public async getMembersByChannelAlias(alias: string): Promise { const channel = await this.get({alias}); if (!channel) { throw new ChannelError(`Channel not found: ${alias}`, 'CHANNEL_NOT_FOUND'); } - + return this.getChannelMembers(channel.channelId); } - public async deleteChannel(channelId: string): Promise { - return this.delete(channelId); - } - - public async delete(channelId: string): Promise { - if (!channelId || channelId.trim().length === 0) { - throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID'); + public async delete({channelId, alias}: {channelId?: string; alias?: string}): Promise { + if (!channelId && !alias) { + throw new ChannelError('Deleting a channel requires either a channelId or alias', 'INVALID_ARGUMENTS'); } - const response = await this._httpRequests.request(HttpMethod.DELETE, `/channel/${encodeURIComponent(channelId)}`); + const channelIdToDelete = alias ? (await this.get({alias}))?.channelId : channelId; + + if (!channelIdToDelete) { + throw new ChannelError('Unable to find room to delete', 'NOT_FOUND'); + } + + const route = `/channel/${encodeURIComponent(channelId)}`; + const response = await this._httpRequests.request(HttpMethod.DELETE, route); if (!response.channel) { - throw new ChannelError(`Invalid response format for deleted channel: ${channelId}`, 'INVALID_RESPONSE'); + throw new ChannelError(`Invalid response format for deleted channel [${channelId}]`, 'INVALID_RESPONSE'); } // Remove from cache if it exists @@ -207,14 +186,8 @@ export class Channels { return deletedChannel; } - // Cache management methods - public isInitialized(): boolean { - return this._initialized !== false && !(this._initialized instanceof Promise); - } - public clearCache(): void { this._channelsByAlias.clear(); - this._initialized = false; } public getCacheSize(): number { @@ -222,25 +195,18 @@ export class Channels { } private async initialize(): Promise { - this._initialized = (async () => { - try { - const channelsList = await this.list(); - if (!channelsList) { - console.warn('[Channels] Failed to initialize cache - no channels returned'); - return; - } - - for (const channel of channelsList) { - this._channelsByAlias.set(channel.alias, channel); - } - - this._initialized = true; - } catch (error) { - console.error('[Channels] Failed to initialize cache:', error); - this._initialized = true; // Mark as initialized even on error to avoid repeated attempts + try { + const channelsList = await this.list(); + if (!channelsList) { + console.warn('[Channels] Failed to initialize cache - no channels returned'); + return; } - })(); - - await this._initialized; + + for (const channel of channelsList) { + this._channelsByAlias.set(channel.alias, channel); + } + } catch (error) { + console.error('[Channels] Failed to initialize cache:', error); + } } } diff --git a/src/pcast/PCastRequests.ts b/src/pcast/PCastRequests.ts index da93786..8ef1ad9 100644 --- a/src/pcast/PCastRequests.ts +++ b/src/pcast/PCastRequests.ts @@ -10,9 +10,7 @@ export class PCastHttpRequests extends HttpRequests { constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) { const credentials = `${applicationCredentials.id}:${applicationCredentials.secret}`; - const basic = typeof btoa === 'function' - ? btoa(credentials) - : Buffer.from(credentials, 'utf-8').toString('base64'); + const basic = typeof btoa === 'function' ? btoa(credentials) : Buffer.from(credentials, 'utf-8').toString('base64'); const baseHeaders = new Headers({ 'Content-Type': 'application/json', diff --git a/src/pcast/Reporting.ts b/src/pcast/Reporting.ts index 992bf14..93e128a 100644 --- a/src/pcast/Reporting.ts +++ b/src/pcast/Reporting.ts @@ -2,7 +2,7 @@ import {HttpMethod} from '../net/http/HttpMethod'; import type {PCastHttpRequests} from './PCastRequests'; import assertUnreachable from '../lang/assertUnreachable'; import {ReportKind} from './ReportKind'; -import { ViewingReportKind, ViewingReportKindMapping } from './ViewingReportKind'; +import {ViewingReportKind, ViewingReportKindMapping} from './ViewingReportKind'; export type PublishingReportOptions = { applicationIds?: string[]; @@ -16,7 +16,6 @@ export type PublishingReportOptions = { end: string; }; - export type ViewingReportOptions = { kind: ViewingReportKind; applicationIds?: string[]; @@ -78,7 +77,7 @@ export class Reporting { }; const response = await this._httpRequests.request(HttpMethod.PUT, '/reporting/viewing', requestViewingOptions); - + return response; } } diff --git a/src/pcast/ViewingReportKind.ts b/src/pcast/ViewingReportKind.ts index 659750e..a8d6951 100644 --- a/src/pcast/ViewingReportKind.ts +++ b/src/pcast/ViewingReportKind.ts @@ -1,37 +1,37 @@ -import assertUnreachable from "../lang/assertUnreachable"; +import assertUnreachable from '../lang/assertUnreachable'; -export enum ViewingReportKind { - RealTime = 0, - HLS = 1, - DASH = 2 +export enum ViewingReportKind { + RealTime = 0, + HLS = 1, + DASH = 2 } export type ViewingReportKindType = 'RealTime' | 'HLS' | 'DASH'; export class ViewingReportKindMapping { - public static convertViewingReportKindTypeToViewingReportKind(viewingReportKindType: ViewingReportKindType): ViewingReportKind { - switch (viewingReportKindType) { - case 'RealTime': - return ViewingReportKind.RealTime; - case 'HLS': - return ViewingReportKind.HLS; - case 'DASH': - return ViewingReportKind.DASH; - default: - assertUnreachable(viewingReportKindType); - } + public static convertViewingReportKindTypeToViewingReportKind(viewingReportKindType: ViewingReportKindType): ViewingReportKind { + switch (viewingReportKindType) { + case 'RealTime': + return ViewingReportKind.RealTime; + case 'HLS': + return ViewingReportKind.HLS; + case 'DASH': + return ViewingReportKind.DASH; + default: + assertUnreachable(viewingReportKindType); } + } - public static convertViewingReportKindToViewingReportKindType(viewingReportKind: ViewingReportKind): ViewingReportKindType { - switch (viewingReportKind) { - case ViewingReportKind.RealTime: - return 'RealTime'; - case ViewingReportKind.HLS: - return 'HLS'; - case ViewingReportKind.DASH: - return 'DASH'; - default: - assertUnreachable(viewingReportKind); - } + public static convertViewingReportKindToViewingReportKindType(viewingReportKind: ViewingReportKind): ViewingReportKindType { + switch (viewingReportKind) { + case ViewingReportKind.RealTime: + return 'RealTime'; + case ViewingReportKind.HLS: + return 'HLS'; + case ViewingReportKind.DASH: + return 'DASH'; + default: + assertUnreachable(viewingReportKind); } -} \ No newline at end of file + } +} diff --git a/src/pcast/index.ts b/src/pcast/index.ts index 677cf58..7c8f7f3 100644 --- a/src/pcast/index.ts +++ b/src/pcast/index.ts @@ -3,4 +3,4 @@ export * from './Reporting'; export * from './PCastRequests'; export * from './Stream'; export * from './ReportKind'; -export * from './ViewingReportKind'; \ No newline at end of file +export * from './ViewingReportKind';