Update and improve things
This commit is contained in:
@@ -1,9 +1,9 @@
|
|||||||
import js from "@eslint/js";
|
import js from '@eslint/js';
|
||||||
import globals from "globals";
|
import globals from 'globals';
|
||||||
import tseslint from "typescript-eslint";
|
import tseslint from 'typescript-eslint';
|
||||||
import { defineConfig } from "eslint/config";
|
import {defineConfig} from 'eslint/config';
|
||||||
|
|
||||||
export default defineConfig([
|
export default defineConfig([
|
||||||
{ files: ["**/*.{js,mjs,cjs,ts,mts,cts}"], plugins: { js }, extends: ["js/recommended"], languageOptions: { globals: {...globals.browser, ...globals.node} } },
|
{files: ['**/*.{js,mjs,cjs,ts,mts,cts}'], plugins: {js}, extends: ['js/recommended'], languageOptions: {globals: {...globals.browser, ...globals.node}}},
|
||||||
tseslint.configs.recommended,
|
tseslint.configs.recommended
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -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 = {
|
const application = {
|
||||||
id: "phenixrts.com-alex.zinn",
|
id: 'phenixrts.com-alex.zinn',
|
||||||
secret: "AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg"
|
secret: 'AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg'
|
||||||
}
|
};
|
||||||
const pcastApi = new PCastApi(pacstUri, application);
|
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 channelsList = await pcastApi.channels.list();
|
||||||
|
|
||||||
// const start = hrtime.bigint();
|
// const start = hrtime.bigint();
|
||||||
@@ -19,7 +22,6 @@ const channelsList = await pcastApi.channels.list();
|
|||||||
// console.log(publishingReportCsv);
|
// console.log(publishingReportCsv);
|
||||||
// console.log(`Time taken: ${Number(endPublishing - start) / 1_000_000_000} seconds`);
|
// console.log(`Time taken: ${Number(endPublishing - start) / 1_000_000_000} seconds`);
|
||||||
|
|
||||||
|
|
||||||
// const viewingReportCsv = await pcastApi.reporting.generateReport(ReportKind.Viewing, {
|
// const viewingReportCsv = await pcastApi.reporting.generateReport(ReportKind.Viewing, {
|
||||||
// kind: ViewingReportKind.HLS,
|
// kind: ViewingReportKind.HLS,
|
||||||
// start: moment().subtract(1, 'day').toISOString(),
|
// start: moment().subtract(1, 'day').toISOString(),
|
||||||
|
|||||||
@@ -1,14 +1,15 @@
|
|||||||
{
|
{
|
||||||
"name": "@techniker-me/pcast-api",
|
"name": "@techniker-me/pcast-api",
|
||||||
"version": "2025.0.10",
|
"version": "2025.1.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ci-build": "bun run build",
|
"ci-build": "bun run build",
|
||||||
"format": "prettier --write ./",
|
|
||||||
"test": "bun test",
|
"test": "bun test",
|
||||||
"test:watch": "bun test --watch",
|
"test:watch": "bun test --watch",
|
||||||
"test:coverage": "bun test --coverage",
|
"test:coverage": "bun test --coverage",
|
||||||
"prelint": "bun install",
|
"preformat": "bun install",
|
||||||
|
"format": "prettier --write ./",
|
||||||
|
"prelint": "bun format",
|
||||||
"lint": "eslint src/**/*.ts",
|
"lint": "eslint src/**/*.ts",
|
||||||
"lint:fix": "eslint src/**/*.ts --fix",
|
"lint:fix": "eslint src/**/*.ts --fix",
|
||||||
"build:node": "bun build src/index.ts --outdir dist/node --target node --format esm --production",
|
"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",
|
"build:dev": "bun run build:node:dev && bun run build:browser:dev && bun run build:types:dev",
|
||||||
"prebuild": "bun run clean",
|
"prebuild": "bun run clean",
|
||||||
"build": "bun run build:node && bun run build:browser && bun run build:types",
|
"build": "bun run build:node && bun run build:browser && bun run build:types",
|
||||||
"postbuild": "bash scripts/prepare-package-json.sh",
|
|
||||||
"postclean": "bun run lint",
|
"postclean": "bun run lint",
|
||||||
"clean": "rm -rf dist",
|
"clean": "rm -rf dist",
|
||||||
"prepublish": "bash scripts/pre-publish.sh"
|
"prepublish": "bash scripts/pre-publish.sh"
|
||||||
|
|||||||
@@ -4,10 +4,7 @@ set -o errexit
|
|||||||
set -o nounset
|
set -o nounset
|
||||||
set -o pipefail
|
set -o pipefail
|
||||||
|
|
||||||
echo "Preparing package.json for distributions"
|
echo "Preparing package.json for distribution"
|
||||||
bash scripts/prepare-package-json.sh
|
bash scripts/prepare-package-json.sh
|
||||||
|
|
||||||
echo "Copying .npmrc to dist/"
|
|
||||||
cp .npmrc dist/
|
|
||||||
|
|
||||||
echo "Pre-publish done"
|
echo "Pre-publish done"
|
||||||
@@ -1,32 +1,23 @@
|
|||||||
import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests, Reporting} from './pcast';
|
import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests, Reporting} from './pcast';
|
||||||
|
|
||||||
export class PCastApi {
|
export class PCastApi {
|
||||||
private readonly _pcastUri: string;
|
|
||||||
private readonly _applicationCredentials: ApplicationCredentials;
|
|
||||||
private readonly _pcastHttpRequests: PCastHttpRequests;
|
|
||||||
private readonly _channels: Channels;
|
private readonly _channels: Channels;
|
||||||
private readonly _streams: Streams;
|
private readonly _streams: Streams;
|
||||||
private readonly _reporting: Reporting;
|
private readonly _reporting: Reporting;
|
||||||
|
|
||||||
private constructor(pcastUri: string, applicationCredentials: ApplicationCredentials, channels: Channels) {
|
private constructor(channels: Channels, streams: Streams, reporting: Reporting) {
|
||||||
const normalized = pcastUri.replace(/\/+$/, '');
|
|
||||||
this._pcastUri = normalized.endsWith('/pcast') ? normalized : `${normalized}/pcast`;
|
|
||||||
|
|
||||||
this._applicationCredentials = applicationCredentials;
|
|
||||||
this._pcastHttpRequests = new PCastHttpRequests(this._pcastUri, this._applicationCredentials);
|
|
||||||
this._channels = channels;
|
this._channels = channels;
|
||||||
this._streams = new Streams(this._pcastHttpRequests);
|
this._streams = streams;
|
||||||
this._reporting = new Reporting(this._pcastHttpRequests);
|
this._reporting = reporting;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static async create(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise<PCastApi> {
|
public static create(pcastUri: string, applicationCredentials: ApplicationCredentials): Promise<PCastApi> {
|
||||||
const pcastHttpRequests = new PCastHttpRequests(pcastUri.replace(/\/+$/, '').endsWith('/pcast') ? pcastUri : `${pcastUri}/pcast`, applicationCredentials);
|
const pcastHttpRequests = new PCastHttpRequests(pcastUri.replace(/\/+$/, '').endsWith('/pcast') ? pcastUri : `${pcastUri}/pcast`, applicationCredentials);
|
||||||
const channels = await Channels.create(pcastHttpRequests);
|
const channels = new Channels(pcastHttpRequests);
|
||||||
return new PCastApi(pcastUri, applicationCredentials, channels);
|
const streams = new Streams(pcastHttpRequests);
|
||||||
}
|
const reporting = new Reporting(pcastHttpRequests);
|
||||||
|
|
||||||
get pcastUri(): string {
|
return new PCastApi(channels, streams, reporting);
|
||||||
return this._pcastUri;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
get channels(): Channels {
|
get channels(): Channels {
|
||||||
|
|||||||
@@ -26,10 +26,10 @@ export type Member = {
|
|||||||
audioState: string;
|
audioState: string;
|
||||||
videoState: string;
|
videoState: string;
|
||||||
}
|
}
|
||||||
],
|
];
|
||||||
state: string;
|
state: string;
|
||||||
lastUpdate: number;
|
lastUpdate: number;
|
||||||
}
|
};
|
||||||
|
|
||||||
type GetChannelParams = {
|
type GetChannelParams = {
|
||||||
alias?: string;
|
alias?: string;
|
||||||
@@ -49,31 +49,26 @@ export class ChannelError extends Error {
|
|||||||
export class Channels {
|
export class Channels {
|
||||||
private readonly _httpRequests: PCastHttpRequests;
|
private readonly _httpRequests: PCastHttpRequests;
|
||||||
private readonly _channelsByAlias: Map<ChannelAlias, Channel> = new Map();
|
private readonly _channelsByAlias: Map<ChannelAlias, Channel> = new Map();
|
||||||
private _initialized: Promise<void> | boolean = false;
|
|
||||||
|
|
||||||
private constructor(pcastHttpRequests: PCastHttpRequests) {
|
private constructor(pcastHttpRequests: PCastHttpRequests) {
|
||||||
this._httpRequests = pcastHttpRequests;
|
this._httpRequests = pcastHttpRequests;
|
||||||
}
|
this.initialize();
|
||||||
|
|
||||||
public static async create(pcastHttpRequests: PCastHttpRequests): Promise<Channels> {
|
|
||||||
const instance = new Channels(pcastHttpRequests);
|
|
||||||
await instance.initialize();
|
|
||||||
return instance;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async createChannel(name: string, description: string, channelOptions: string[] = []): Promise<Channel> {
|
public async createChannel(name: string, description: string, channelOptions: string[] = []): Promise<Channel> {
|
||||||
// Input validation
|
|
||||||
if (!name || name.trim().length === 0) {
|
if (!name || name.trim().length === 0) {
|
||||||
throw new ChannelError('Channel name cannot be empty', 'INVALID_NAME');
|
throw new ChannelError('Channel name cannot be empty', 'INVALID_NAME');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!description || description.trim().length === 0) {
|
if (!description || description.trim().length === 0) {
|
||||||
throw new ChannelError('Channel description cannot be empty', 'INVALID_DESCRIPTION');
|
throw new ChannelError('Channel description cannot be empty', 'INVALID_DESCRIPTION');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!Array.isArray(channelOptions)) {
|
if (!Array.isArray(channelOptions)) {
|
||||||
throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS');
|
throw new ChannelError('Channel options must be an array', 'INVALID_OPTIONS');
|
||||||
}
|
}
|
||||||
|
|
||||||
const createChannelRequestBody = {
|
const createChannel = {
|
||||||
channel: {
|
channel: {
|
||||||
name: name.trim(),
|
name: name.trim(),
|
||||||
alias: name.trim(),
|
alias: name.trim(),
|
||||||
@@ -82,15 +77,14 @@ export class Channels {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.PUT, '/channel', {
|
const route = '/channel';
|
||||||
body: JSON.stringify(createChannelRequestBody)
|
const requestOptions = {body: JSON.stringify(createChannel)};
|
||||||
});
|
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.PUT, route, requestOptions);
|
||||||
|
|
||||||
if (!response.channel) {
|
if (!response.channel) {
|
||||||
throw new ChannelError('Invalid response format - missing channel data', 'INVALID_RESPONSE');
|
throw new ChannelError('Invalid response format - missing channel data', 'INVALID_RESPONSE');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update cache with new channel
|
|
||||||
this._channelsByAlias.set(response.channel.alias, response.channel);
|
this._channelsByAlias.set(response.channel.alias, response.channel);
|
||||||
|
|
||||||
return response.channel;
|
return response.channel;
|
||||||
@@ -113,7 +107,8 @@ export class Channels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public async refreshCache(): Promise<void> {
|
public async refreshCache(): Promise<void> {
|
||||||
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<Channel | undefined> {
|
public async getChannelInfoByAlias(alias: string): Promise<Channel | undefined> {
|
||||||
@@ -125,41 +120,22 @@ export class Channels {
|
|||||||
throw new ChannelError('Either alias or channelId must be provided', 'MISSING_PARAMETER');
|
throw new ChannelError('Either alias or channelId must be provided', 'MISSING_PARAMETER');
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this._initialized === false) {
|
if (alias && this._channelsByAlias.has(alias)) {
|
||||||
await this.initialize();
|
|
||||||
} else if (this._initialized instanceof Promise) {
|
|
||||||
await this._initialized;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (alias) {
|
|
||||||
// Check cache first
|
|
||||||
if (this._channelsByAlias.has(alias)) {
|
|
||||||
return this._channelsByAlias.get(alias);
|
return this._channelsByAlias.get(alias);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 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();
|
const channelList = await this.list();
|
||||||
|
|
||||||
// Update cache
|
return alias ? channelList.find(channel => channel.alias === alias) : channelList.find(channel => channel.channelId === channelId);
|
||||||
return channelList.find(channel => channel.alias === alias);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (channelId) {
|
public async getPublisherCount(channelId: string): Promise<number> {
|
||||||
// Similar fallback
|
|
||||||
const channelList = await this.list();
|
|
||||||
|
|
||||||
return channelList.find(channel => channel.channelId === channelId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async getChannelPublisherCount(channelId: string): Promise<number> {
|
|
||||||
const response = await this._httpRequests.request<string>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/publishers/count`);
|
const response = await this._httpRequests.request<string>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}/publishers/count`);
|
||||||
|
|
||||||
return parseInt(response, 10);
|
return parseInt(response, 10);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChannelMembers(channelId: string): Promise<Member[]> {
|
public async getMembers(channelId: string): Promise<Member[]> {
|
||||||
if (!channelId || channelId.trim().length === 0) {
|
if (!channelId || channelId.trim().length === 0) {
|
||||||
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
|
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
|
||||||
}
|
}
|
||||||
@@ -173,7 +149,7 @@ export class Channels {
|
|||||||
return response.members;
|
return response.members;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getChannelMembersByAlias(alias: string): Promise<Member[]> {
|
public async getMembersByChannelAlias(alias: string): Promise<Member[]> {
|
||||||
const channel = await this.get({alias});
|
const channel = await this.get({alias});
|
||||||
|
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
@@ -183,19 +159,22 @@ export class Channels {
|
|||||||
return this.getChannelMembers(channel.channelId);
|
return this.getChannelMembers(channel.channelId);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteChannel(channelId: string): Promise<Channel> {
|
public async delete({channelId, alias}: {channelId?: string; alias?: string}): Promise<Channel> {
|
||||||
return this.delete(channelId);
|
if (!channelId && !alias) {
|
||||||
|
throw new ChannelError('Deleting a channel requires either a channelId or alias', 'INVALID_ARGUMENTS');
|
||||||
}
|
}
|
||||||
|
|
||||||
public async delete(channelId: string): Promise<Channel> {
|
const channelIdToDelete = alias ? (await this.get({alias}))?.channelId : channelId;
|
||||||
if (!channelId || channelId.trim().length === 0) {
|
|
||||||
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
|
if (!channelIdToDelete) {
|
||||||
|
throw new ChannelError('Unable to find room to delete', 'NOT_FOUND');
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.DELETE, `/channel/${encodeURIComponent(channelId)}`);
|
const route = `/channel/${encodeURIComponent(channelId)}`;
|
||||||
|
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.DELETE, route);
|
||||||
|
|
||||||
if (!response.channel) {
|
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
|
// Remove from cache if it exists
|
||||||
@@ -207,14 +186,8 @@ export class Channels {
|
|||||||
return deletedChannel;
|
return deletedChannel;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Cache management methods
|
|
||||||
public isInitialized(): boolean {
|
|
||||||
return this._initialized !== false && !(this._initialized instanceof Promise);
|
|
||||||
}
|
|
||||||
|
|
||||||
public clearCache(): void {
|
public clearCache(): void {
|
||||||
this._channelsByAlias.clear();
|
this._channelsByAlias.clear();
|
||||||
this._initialized = false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getCacheSize(): number {
|
public getCacheSize(): number {
|
||||||
@@ -222,7 +195,6 @@ export class Channels {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private async initialize(): Promise<void> {
|
private async initialize(): Promise<void> {
|
||||||
this._initialized = (async () => {
|
|
||||||
try {
|
try {
|
||||||
const channelsList = await this.list();
|
const channelsList = await this.list();
|
||||||
if (!channelsList) {
|
if (!channelsList) {
|
||||||
@@ -233,14 +205,8 @@ export class Channels {
|
|||||||
for (const channel of channelsList) {
|
for (const channel of channelsList) {
|
||||||
this._channelsByAlias.set(channel.alias, channel);
|
this._channelsByAlias.set(channel.alias, channel);
|
||||||
}
|
}
|
||||||
|
|
||||||
this._initialized = true;
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[Channels] Failed to initialize cache:', error);
|
console.error('[Channels] Failed to initialize cache:', error);
|
||||||
this._initialized = true; // Mark as initialized even on error to avoid repeated attempts
|
|
||||||
}
|
}
|
||||||
})();
|
|
||||||
|
|
||||||
await this._initialized;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,9 +10,7 @@ export class PCastHttpRequests extends HttpRequests {
|
|||||||
|
|
||||||
constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) {
|
constructor(baseUri: string, applicationCredentials: ApplicationCredentials, options: {requestTimeoutDuration?: number} = {}) {
|
||||||
const credentials = `${applicationCredentials.id}:${applicationCredentials.secret}`;
|
const credentials = `${applicationCredentials.id}:${applicationCredentials.secret}`;
|
||||||
const basic = typeof btoa === 'function'
|
const basic = typeof btoa === 'function' ? btoa(credentials) : Buffer.from(credentials, 'utf-8').toString('base64');
|
||||||
? btoa(credentials)
|
|
||||||
: Buffer.from(credentials, 'utf-8').toString('base64');
|
|
||||||
|
|
||||||
const baseHeaders = new Headers({
|
const baseHeaders = new Headers({
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import {HttpMethod} from '../net/http/HttpMethod';
|
|||||||
import type {PCastHttpRequests} from './PCastRequests';
|
import type {PCastHttpRequests} from './PCastRequests';
|
||||||
import assertUnreachable from '../lang/assertUnreachable';
|
import assertUnreachable from '../lang/assertUnreachable';
|
||||||
import {ReportKind} from './ReportKind';
|
import {ReportKind} from './ReportKind';
|
||||||
import { ViewingReportKind, ViewingReportKindMapping } from './ViewingReportKind';
|
import {ViewingReportKind, ViewingReportKindMapping} from './ViewingReportKind';
|
||||||
|
|
||||||
export type PublishingReportOptions = {
|
export type PublishingReportOptions = {
|
||||||
applicationIds?: string[];
|
applicationIds?: string[];
|
||||||
@@ -16,7 +16,6 @@ export type PublishingReportOptions = {
|
|||||||
end: string;
|
end: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
||||||
export type ViewingReportOptions = {
|
export type ViewingReportOptions = {
|
||||||
kind: ViewingReportKind;
|
kind: ViewingReportKind;
|
||||||
applicationIds?: string[];
|
applicationIds?: string[];
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import assertUnreachable from "../lang/assertUnreachable";
|
import assertUnreachable from '../lang/assertUnreachable';
|
||||||
|
|
||||||
export enum ViewingReportKind {
|
export enum ViewingReportKind {
|
||||||
RealTime = 0,
|
RealTime = 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user