feat: add ESLint configuration, update package.json scripts, and create example files for Bun integration
This commit is contained in:
9
eslint.config.ts
Normal file
9
eslint.config.ts
Normal file
@@ -0,0 +1,9 @@
|
||||
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,
|
||||
]);
|
||||
111
examples/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc
Normal file
111
examples/.cursor/rules/use-bun-instead-of-node-vite-npm-pnpm.mdc
Normal file
@@ -0,0 +1,111 @@
|
||||
---
|
||||
description: Use Bun instead of Node.js, npm, pnpm, or vite.
|
||||
globs: "*.ts, *.tsx, *.html, *.css, *.js, *.jsx, package.json"
|
||||
alwaysApply: false
|
||||
---
|
||||
|
||||
Default to using Bun instead of Node.js.
|
||||
|
||||
- Use `bun <file>` instead of `node <file>` or `ts-node <file>`
|
||||
- Use `bun test` instead of `jest` or `vitest`
|
||||
- Use `bun build <file.html|file.ts|file.css>` instead of `webpack` or `esbuild`
|
||||
- Use `bun install` instead of `npm install` or `yarn install` or `pnpm install`
|
||||
- Use `bun run <script>` instead of `npm run <script>` or `yarn run <script>` or `pnpm run <script>`
|
||||
- Bun automatically loads .env, so don't use dotenv.
|
||||
|
||||
## APIs
|
||||
|
||||
- `Bun.serve()` supports WebSockets, HTTPS, and routes. Don't use `express`.
|
||||
- `bun:sqlite` for SQLite. Don't use `better-sqlite3`.
|
||||
- `Bun.redis` for Redis. Don't use `ioredis`.
|
||||
- `Bun.sql` for Postgres. Don't use `pg` or `postgres.js`.
|
||||
- `WebSocket` is built-in. Don't use `ws`.
|
||||
- Prefer `Bun.file` over `node:fs`'s readFile/writeFile
|
||||
- Bun.$`ls` instead of execa.
|
||||
|
||||
## Testing
|
||||
|
||||
Use `bun test` to run tests.
|
||||
|
||||
```ts#index.test.ts
|
||||
import { test, expect } from "bun:test";
|
||||
|
||||
test("hello world", () => {
|
||||
expect(1).toBe(1);
|
||||
});
|
||||
```
|
||||
|
||||
## Frontend
|
||||
|
||||
Use HTML imports with `Bun.serve()`. Don't use `vite`. HTML imports fully support React, CSS, Tailwind.
|
||||
|
||||
Server:
|
||||
|
||||
```ts#index.ts
|
||||
import index from "./index.html"
|
||||
|
||||
Bun.serve({
|
||||
routes: {
|
||||
"/": index,
|
||||
"/api/users/:id": {
|
||||
GET: (req) => {
|
||||
return new Response(JSON.stringify({ id: req.params.id }));
|
||||
},
|
||||
},
|
||||
},
|
||||
// optional websocket support
|
||||
websocket: {
|
||||
open: (ws) => {
|
||||
ws.send("Hello, world!");
|
||||
},
|
||||
message: (ws, message) => {
|
||||
ws.send(message);
|
||||
},
|
||||
close: (ws) => {
|
||||
// handle close
|
||||
}
|
||||
},
|
||||
development: {
|
||||
hmr: true,
|
||||
console: true,
|
||||
}
|
||||
})
|
||||
```
|
||||
|
||||
HTML files can import .tsx, .jsx or .js files directly and Bun's bundler will transpile & bundle automatically. `<link>` tags can point to stylesheets and Bun's CSS bundler will bundle.
|
||||
|
||||
```html#index.html
|
||||
<html>
|
||||
<body>
|
||||
<h1>Hello, world!</h1>
|
||||
<script type="module" src="./frontend.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
With the following `frontend.tsx`:
|
||||
|
||||
```tsx#frontend.tsx
|
||||
import React from "react";
|
||||
|
||||
// import .css files directly and it works
|
||||
import './index.css';
|
||||
|
||||
import { createRoot } from "react-dom/client";
|
||||
|
||||
const root = createRoot(document.body);
|
||||
|
||||
export default function Frontend() {
|
||||
return <h1>Hello, world!</h1>;
|
||||
}
|
||||
|
||||
root.render(<Frontend />);
|
||||
```
|
||||
|
||||
Then, run index.ts
|
||||
|
||||
```sh
|
||||
bun --hot ./index.ts
|
||||
```
|
||||
|
||||
For more information, read the Bun API docs in `node_modules/bun-types/docs/**.md`.
|
||||
34
examples/.gitignore
vendored
Normal file
34
examples/.gitignore
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
# dependencies (bun install)
|
||||
node_modules
|
||||
|
||||
# output
|
||||
out
|
||||
dist
|
||||
*.tgz
|
||||
|
||||
# code coverage
|
||||
coverage
|
||||
*.lcov
|
||||
|
||||
# logs
|
||||
logs
|
||||
_.log
|
||||
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
|
||||
|
||||
# dotenv environment variable files
|
||||
.env
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
.env.local
|
||||
|
||||
# caches
|
||||
.eslintcache
|
||||
.cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# IntelliJ based IDEs
|
||||
.idea
|
||||
|
||||
# Finder (MacOS) folder config
|
||||
.DS_Store
|
||||
4
examples/.npmrc
Normal file
4
examples/.npmrc
Normal file
@@ -0,0 +1,4 @@
|
||||
//registry-node.techniker.me/:_authToken="${NODE_REGISTRY_AUTH_TOKEN}"
|
||||
@techniker-me:registry=https://registry-node.techniker.me
|
||||
save-exact=true
|
||||
package-lock=false
|
||||
12
examples/.prettierrc
Normal file
12
examples/.prettierrc
Normal file
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"arrowParens": "avoid",
|
||||
"bracketSameLine": true,
|
||||
"bracketSpacing": false,
|
||||
"printWidth": 160,
|
||||
"semi": true,
|
||||
"singleAttributePerLine": false,
|
||||
"singleQuote": true,
|
||||
"tabWidth": 2,
|
||||
"trailingComma": "none",
|
||||
"useTabs": false
|
||||
}
|
||||
15
examples/README.md
Normal file
15
examples/README.md
Normal file
@@ -0,0 +1,15 @@
|
||||
# examples
|
||||
|
||||
To install dependencies:
|
||||
|
||||
```bash
|
||||
bun install
|
||||
```
|
||||
|
||||
To run:
|
||||
|
||||
```bash
|
||||
bun run index.ts
|
||||
```
|
||||
|
||||
This project was created using `bun init` in bun v1.2.20. [Bun](https://bun.com) is a fast all-in-one JavaScript runtime.
|
||||
29
examples/bun.lock
Normal file
29
examples/bun.lock
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "examples",
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@types/bun": ["@types/bun@1.2.20", "", { "dependencies": { "bun-types": "1.2.20" } }, "sha512-dX3RGzQ8+KgmMw7CsW4xT5ITBSCrSbfHc36SNT31EOUg/LA9JWq0VDdEXDRSe1InVWpd2yLUM1FUF/kEOyTzYA=="],
|
||||
|
||||
"@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="],
|
||||
|
||||
"@types/react": ["@types/react@19.1.10", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-EhBeSYX0Y6ye8pNebpKrwFJq7BoQ8J5SO6NlvNwwHjSj6adXJViPQrKlsyPw7hLBLvckEMO1yxeGdR82YBBlDg=="],
|
||||
|
||||
"bun-types": ["bun-types@1.2.20", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-pxTnQYOrKvdOwyiyd/7sMt9yFOenN004Y6O4lCcCUoKVej48FS5cvTw9geRaEcB9TsDZaJKAxPTVvi8tFsVuXA=="],
|
||||
|
||||
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
|
||||
|
||||
"typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="],
|
||||
|
||||
"undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="],
|
||||
}
|
||||
}
|
||||
5
examples/bunfig.toml
Normal file
5
examples/bunfig.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
[install.lockfile]
|
||||
save = false
|
||||
|
||||
[install.scopes]
|
||||
"@techniker-me" = "https://registry-node.techniker.me"
|
||||
@@ -1,11 +0,0 @@
|
||||
import {PCastApi} from '../dist/node/index.js';
|
||||
|
||||
const applicationCredentials = {
|
||||
id: 'phenixrts.com-alex.zinn',
|
||||
secret: 'AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg'
|
||||
};
|
||||
|
||||
const pcastUri = 'https://pcast-stg.phenixrts.com';
|
||||
const pcastApi = new PCastApi(pcastUri, applicationCredentials);
|
||||
|
||||
pcastApi.channels.list().then(console.log);
|
||||
16
examples/package.json
Normal file
16
examples/package.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "examples",
|
||||
"module": "index.ts",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"devDependencies": {
|
||||
"@types/bun": "latest"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"typescript": "^5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@techniker-me/csv": "0.0.1",
|
||||
"moment": "2.30.1"
|
||||
}
|
||||
}
|
||||
37
examples/src/index.ts
Normal file
37
examples/src/index.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import PCastApi from "../../src/index";
|
||||
|
||||
const pacstUri = "https://pcast-stg.phenixrts.com"
|
||||
const application = {
|
||||
id: "phenixrts.com-alex.zinn",
|
||||
secret: "AMAsDzr.dIuGMZ.Zu52Dt~MQvP!DZwYg"
|
||||
}
|
||||
const pcastApi = new PCastApi(pacstUri, application);
|
||||
|
||||
const channelsList = await pcastApi.channels.list();
|
||||
|
||||
// const start = hrtime.bigint();
|
||||
// const publishingReportCsv = await pcastApi.reporting.generateReport(ReportKind.Publishing, {
|
||||
// start: moment().subtract(1, 'day').toISOString(),
|
||||
// end: moment().toISOString(),
|
||||
// applicationIds: [application.id]
|
||||
// });
|
||||
// const endPublishing = hrtime.bigint();
|
||||
// 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(),
|
||||
// end: moment().toISOString(),
|
||||
// applicationIds: [application.id]
|
||||
// });
|
||||
// const endViewing = hrtime.bigint();
|
||||
// console.log(`Time taken: ${Number(endViewing - endPublishing) / 1_000_000_000} seconds`);
|
||||
// // const viewingReport = await CsvParser.parse(viewingReportCsv);
|
||||
|
||||
// console.log(viewingReportCsv);
|
||||
|
||||
const channelMembers = await pcastApi.channels.getMembers(channelsList[1].channelId);
|
||||
console.log('[%o]', channelMembers);
|
||||
console.log('[%o]', channelMembers.streams);
|
||||
29
examples/tsconfig.json
Normal file
29
examples/tsconfig.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
// Environment setup & latest features
|
||||
"lib": ["ESNext"],
|
||||
"target": "ESNext",
|
||||
"module": "Preserve",
|
||||
"moduleDetection": "force",
|
||||
"jsx": "react-jsx",
|
||||
"allowJs": true,
|
||||
|
||||
// Bundler mode
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"verbatimModuleSyntax": true,
|
||||
"noEmit": true,
|
||||
|
||||
// Best practices
|
||||
"strict": true,
|
||||
"skipLibCheck": true,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"noUncheckedIndexedAccess": true,
|
||||
"noImplicitOverride": true,
|
||||
|
||||
// Some stricter flags (disabled by default)
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noPropertyAccessFromIndexSignature": false
|
||||
}
|
||||
}
|
||||
13
package.json
13
package.json
@@ -10,6 +10,9 @@
|
||||
"test:watch": "bun test --watch",
|
||||
"test:coverage": "bun test --coverage",
|
||||
"prebuild": "bun run clean",
|
||||
"prelint": "bun install",
|
||||
"lint": "eslint src/**/*.ts",
|
||||
"lint:fix": "eslint src/**/*.ts --fix",
|
||||
"build": "bun run build:node && bun run build:browser && bun run build:types",
|
||||
"build:node": "bun build src/index.ts --outdir dist/node --target node --format esm --production",
|
||||
"build:browser": "bun build src/index.ts --outdir dist/browser --target browser --format esm --production",
|
||||
@@ -19,16 +22,22 @@
|
||||
"build:types:dev": "tsc --emitDeclarationOnly --outDir dist/types",
|
||||
"prebuild:dev": "bun run clean",
|
||||
"build:dev": "bun run build:node:dev && bun run build:browser:dev && bun run build:types:dev",
|
||||
"postclean": "bun run lint",
|
||||
"clean": "rm -rf dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "9.33.0",
|
||||
"@types/bun": "latest",
|
||||
"@types/node": "24.3.0",
|
||||
"chai": "5.3.1",
|
||||
"chai-as-promised": "8.0.1",
|
||||
"eslint": "9.33.0",
|
||||
"globals": "16.3.0",
|
||||
"jiti": "2.5.1",
|
||||
"mocha": "11.7.1",
|
||||
"prettier": "3.6.2",
|
||||
"typescript": "5.9.2"
|
||||
"typescript": "5.9.2",
|
||||
"typescript-eslint": "8.40.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"phenix-edge-auth": "1.2.7"
|
||||
@@ -50,4 +59,4 @@
|
||||
"files": [
|
||||
"dist"
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import {Channels, Streams, type ApplicationCredentials, PCastHttpRequests} from './pcast';
|
||||
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;
|
||||
|
||||
constructor(pcastUri: string, applicationCredentials: ApplicationCredentials) {
|
||||
if (pcastUri.split('/').at(-1) !== 'pcast') {
|
||||
this._pcastUri = `${pcastUri}/pcast`;
|
||||
} else {
|
||||
this._pcastUri = pcastUri;
|
||||
}
|
||||
// in `src/PCastApi.ts` constructor
|
||||
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 = new Channels(this._pcastHttpRequests);
|
||||
this._streams = new Streams(this._pcastHttpRequests);
|
||||
this._reporting = new Reporting(this._pcastHttpRequests);
|
||||
}
|
||||
|
||||
get pcastUri(): string {
|
||||
@@ -25,10 +29,14 @@ export class PCastApi {
|
||||
}
|
||||
|
||||
get channels(): Channels {
|
||||
return new Channels(this._pcastHttpRequests);
|
||||
return this._channels;
|
||||
}
|
||||
|
||||
get streams(): Streams {
|
||||
return new Streams(this._pcastHttpRequests);
|
||||
return this._streams;
|
||||
}
|
||||
|
||||
get reporting(): Reporting {
|
||||
return this._reporting;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import {PCastApi} from './PCastApi';
|
||||
import {ReportKind} from './pcast/ReportKind';
|
||||
import {ViewingReportKind} from './pcast/ViewingReportKind';
|
||||
|
||||
export * from './pcast';
|
||||
export type {ReportKind, ViewingReportKind};
|
||||
export {PCastApi};
|
||||
export default {PCastApi};
|
||||
export default PCastApi;
|
||||
|
||||
@@ -38,6 +38,10 @@ export class HttpRequests {
|
||||
|
||||
if (httpMethodsThatMustNotHaveBody.includes(method)) {
|
||||
requestOptions.body = undefined;
|
||||
} else {
|
||||
if (requestOptions.body && typeof requestOptions.body !== 'string') {
|
||||
requestOptions.body = JSON.stringify(requestOptions.body);
|
||||
}
|
||||
}
|
||||
|
||||
return this.makeRequest<T>(path, requestOptions, abortController, this._requestTimeoutDuration);
|
||||
@@ -48,6 +52,7 @@ export class HttpRequests {
|
||||
|
||||
try {
|
||||
const requestPath = `${this._baseUri}${path}`;
|
||||
|
||||
const response = await fetch(requestPath, options);
|
||||
|
||||
if (!response.ok) {
|
||||
|
||||
@@ -14,6 +14,22 @@ export type Channel = {
|
||||
lastUpdated: string;
|
||||
channelId: string;
|
||||
};
|
||||
export type ChannelAlias = string;
|
||||
export type Member = {
|
||||
sessionId: string;
|
||||
screenName: string;
|
||||
role: string;
|
||||
streams: [
|
||||
{
|
||||
type: string;
|
||||
uri: string;
|
||||
audioState: string;
|
||||
videoState: string;
|
||||
}
|
||||
],
|
||||
state: string;
|
||||
lastUpdate: number;
|
||||
}
|
||||
|
||||
type GetChannelParams = {
|
||||
alias?: string;
|
||||
@@ -32,7 +48,7 @@ export class ChannelError extends Error {
|
||||
|
||||
export class Channels {
|
||||
private readonly _httpRequests: PCastHttpRequests;
|
||||
private readonly _channelsByAlias: Map<ChannelId, Channel> = new Map();
|
||||
private readonly _channelsByAlias: Map<ChannelAlias, Channel> = new Map();
|
||||
private _initialized = false;
|
||||
|
||||
constructor(pcastHttpRequests: PCastHttpRequests) {
|
||||
@@ -48,14 +64,11 @@ export class Channels {
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[Channels] [initialize] Populating local cache with [%o] channels', channelsList.length);
|
||||
|
||||
for (let channel of channelsList) {
|
||||
for (const 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);
|
||||
}
|
||||
@@ -103,8 +116,6 @@ export class Channels {
|
||||
throw new ChannelError('Invalid response format - missing channels data', 'INVALID_RESPONSE');
|
||||
}
|
||||
|
||||
console.log('[Channels] [list] Response [%o]', response);
|
||||
|
||||
return response.channels;
|
||||
}
|
||||
|
||||
@@ -123,35 +134,26 @@ export class Channels {
|
||||
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');
|
||||
}
|
||||
const channelList = await this.list();
|
||||
|
||||
// Update cache
|
||||
this._channelsByAlias.set(alias, response.channel);
|
||||
return response.channel;
|
||||
return channelList.find(channel => channel.alias === alias);
|
||||
}
|
||||
|
||||
if (channelId) {
|
||||
const response = await this._httpRequests.request<ChannelResponse>(HttpMethod.GET, `/channel/${encodeURIComponent(channelId)}`);
|
||||
const channelList = await this.list();
|
||||
|
||||
if (!response.channel) {
|
||||
throw new ChannelError(`Invalid response format for channel ID: ${channelId}`, 'INVALID_RESPONSE');
|
||||
}
|
||||
|
||||
return response.channel;
|
||||
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`);
|
||||
|
||||
return parseInt(response);
|
||||
return parseInt(response, 10);
|
||||
}
|
||||
|
||||
public async getMembers(channelId: string): Promise<Channel[]> {
|
||||
public async getMembers(channelId: string): Promise<Member[]> {
|
||||
if (!channelId || channelId.trim().length === 0) {
|
||||
throw new ChannelError('Channel ID cannot be empty', 'INVALID_CHANNEL_ID');
|
||||
}
|
||||
|
||||
@@ -1,27 +1,19 @@
|
||||
import type {Channel} from './Channels';
|
||||
import type {Channel, Member} from './Channels';
|
||||
|
||||
export default interface IResponse<Key extends string, T> {
|
||||
export default interface IResponse<T> {
|
||||
status: string;
|
||||
[key: string]: T | string;
|
||||
}
|
||||
|
||||
// More specific response types for better type safety
|
||||
export interface ChannelResponse extends IResponse<'channel', Channel> {
|
||||
export interface ChannelResponse extends IResponse<Channel> {
|
||||
channel: Channel;
|
||||
}
|
||||
|
||||
export interface ChannelsResponse extends IResponse<'channels', Channel[]> {
|
||||
export interface ChannelsResponse extends IResponse<Channel[]> {
|
||||
channels: Channel[];
|
||||
}
|
||||
|
||||
export interface MembersResponse extends IResponse<'members', Channel[]> {
|
||||
members: Channel[];
|
||||
}
|
||||
|
||||
export interface PublishingReportResponse extends IResponse<'publishingReport', string> {
|
||||
publishingReport: string;
|
||||
}
|
||||
|
||||
export interface ViewingReportResponse extends IResponse<'viewingReport', string> {
|
||||
viewingReport: string;
|
||||
export interface MembersResponse extends IResponse<Member[]> {
|
||||
members: Member[];
|
||||
}
|
||||
|
||||
@@ -9,10 +9,15 @@ export class PCastHttpRequests extends HttpRequests {
|
||||
private readonly _tenancy: string;
|
||||
|
||||
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 baseHeaders = new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
Accept: 'application/json',
|
||||
Authorization: `Basic ${btoa(`${applicationCredentials.id}:${applicationCredentials.secret}`)}`
|
||||
Authorization: `Basic ${basic}`
|
||||
});
|
||||
super(baseUri, baseHeaders, options);
|
||||
|
||||
|
||||
34
src/pcast/ReportKind.ts
Normal file
34
src/pcast/ReportKind.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import assertUnreachable from '../lang/assertUnreachable';
|
||||
|
||||
export enum ReportKind {
|
||||
Publishing = 0,
|
||||
Viewing = 1
|
||||
}
|
||||
|
||||
export type ReportKindType = 'Publishing' | 'Viewing';
|
||||
|
||||
export class ReportKindMapping {
|
||||
public static convertReportKindTypeToReportKind(reportKindType: ReportKindType): ReportKind {
|
||||
switch (reportKindType) {
|
||||
case 'Publishing':
|
||||
return ReportKind.Publishing;
|
||||
case 'Viewing':
|
||||
return ReportKind.Viewing;
|
||||
|
||||
default:
|
||||
assertUnreachable(reportKindType);
|
||||
}
|
||||
}
|
||||
|
||||
public static convertReportKindToReportKindType(reportKind: ReportKind): ReportKindType {
|
||||
switch (reportKind) {
|
||||
case ReportKind.Publishing:
|
||||
return 'Publishing';
|
||||
case ReportKind.Viewing:
|
||||
return 'Viewing';
|
||||
|
||||
default:
|
||||
assertUnreachable(reportKind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,8 @@
|
||||
import {HttpMethod} from '../net/http/HttpMethod';
|
||||
import type {PCastHttpRequests} from './PCastRequests';
|
||||
import type {PublishingReportResponse, ViewingReportResponse} from './IResponse';
|
||||
import assertUnreachable from '../lang/assertUnreachable';
|
||||
|
||||
export enum ReportKind {
|
||||
Publishing = 0,
|
||||
Viewing = 1
|
||||
}
|
||||
|
||||
export type ReportKindType = 'Publishing' | 'Viewing';
|
||||
|
||||
export class ReportKindMapping {
|
||||
public static convertReportKindTypeToReportKind(reportKindType: ReportKindType): ReportKind {
|
||||
switch (reportKindType) {
|
||||
case 'Publishing':
|
||||
return ReportKind.Publishing;
|
||||
case 'Viewing':
|
||||
return ReportKind.Viewing;
|
||||
|
||||
default:
|
||||
assertUnreachable(reportKindType);
|
||||
}
|
||||
}
|
||||
|
||||
public static convertReportKindToReportKindType(reportKind: ReportKind): ReportKindType {
|
||||
switch (reportKind) {
|
||||
case ReportKind.Publishing:
|
||||
return 'Publishing';
|
||||
case ReportKind.Viewing:
|
||||
return 'Viewing';
|
||||
|
||||
default:
|
||||
assertUnreachable(reportKind);
|
||||
}
|
||||
}
|
||||
}
|
||||
import {ReportKind} from './ReportKind';
|
||||
import { ViewingReportKind, ViewingReportKindMapping } from './ViewingReportKind';
|
||||
|
||||
export type PublishingReportOptions = {
|
||||
applicationIds?: string[];
|
||||
@@ -48,13 +16,6 @@ export type PublishingReportOptions = {
|
||||
end: string;
|
||||
};
|
||||
|
||||
export enum ViewingReportKind {
|
||||
RealTime = 0,
|
||||
HLS = 1,
|
||||
DASH = 2
|
||||
}
|
||||
|
||||
export type ViewingReportKindType = 'RealTime' | 'HLS' | 'DASH';
|
||||
|
||||
export type ViewingReportOptions = {
|
||||
kind: ViewingReportKind;
|
||||
@@ -80,21 +41,18 @@ export class Reporting {
|
||||
}
|
||||
|
||||
public async generateReport<ReportOptions extends PublishingReportOptions | ViewingReportOptions>(kind: ReportKind, options: ReportOptions): Promise<string> {
|
||||
console.log('[Reporting] generateReport [%o]', ReportKindMapping.convertReportKindToReportKindType(kind));
|
||||
|
||||
if (kind === ReportKind.Publishing) {
|
||||
return this.requestPublishingReport(options as PublishingReportOptions);
|
||||
switch (kind) {
|
||||
case ReportKind.Publishing:
|
||||
return this.requestPublishingReport(options as PublishingReportOptions);
|
||||
case ReportKind.Viewing:
|
||||
return this.requestViewingReport(options as ViewingReportOptions);
|
||||
default:
|
||||
assertUnreachable(kind);
|
||||
}
|
||||
|
||||
if (kind === ReportKind.Viewing) {
|
||||
return this.requestViewingReport(options as ViewingReportOptions);
|
||||
}
|
||||
|
||||
throw new Error(`[Reporting] Unsupported report kind: ${kind}`);
|
||||
}
|
||||
|
||||
public async requestPublishingReport(options: PublishingReportOptions): Promise<string> {
|
||||
if (!(options.start || options.end)) {
|
||||
if (!options.start || !options.end) {
|
||||
throw new Error('[Reporting] [requestPublishingReport] requires a start and end Date');
|
||||
}
|
||||
const publishingReportOptions = {
|
||||
@@ -104,30 +62,23 @@ export class Reporting {
|
||||
const requestPublishingOptions = {
|
||||
body: JSON.stringify({publishingReport: publishingReportOptions})
|
||||
};
|
||||
const response = await this._httpRequests.request<PublishingReportResponse>(HttpMethod.PUT, '/pcast/reporting/publishing', requestPublishingOptions);
|
||||
const response = await this._httpRequests.request<string>(HttpMethod.PUT, '/reporting/publishing', requestPublishingOptions);
|
||||
|
||||
if (!response.publishingReport) {
|
||||
throw new Error('[Reporting] [requestPublishingReport] Invalid response format - missing publishingReport data');
|
||||
}
|
||||
|
||||
return response.publishingReport;
|
||||
return response;
|
||||
}
|
||||
|
||||
private async requestViewingReport(options: ViewingReportOptions): Promise<string> {
|
||||
const viewingReportOptions = {
|
||||
...options
|
||||
...options,
|
||||
kind: ViewingReportKindMapping.convertViewingReportKindToViewingReportKindType(options.kind)
|
||||
};
|
||||
|
||||
const requestViewingOptions = {
|
||||
body: JSON.stringify({viewingReport: viewingReportOptions})
|
||||
};
|
||||
|
||||
const response = await this._httpRequests.request<ViewingReportResponse>(HttpMethod.PUT, '/pcast/reporting/viewing', requestViewingOptions);
|
||||
|
||||
if (!response.viewingReport) {
|
||||
throw new Error('[Reporting] [requestViewingReport] Invalid response format - missing viewingReport data');
|
||||
}
|
||||
|
||||
return response.viewingReport;
|
||||
const response = await this._httpRequests.request<string>(HttpMethod.PUT, '/reporting/viewing', requestViewingOptions);
|
||||
|
||||
return response;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ export class Streams {
|
||||
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}`, {
|
||||
const response = await this._httpRequests.request<IResponse<string>>(HttpMethod.PUT, `/stream/publish/uri/${mediaType}`, {
|
||||
body: JSON.stringify({
|
||||
token,
|
||||
uri: mediaUri,
|
||||
|
||||
37
src/pcast/ViewingReportKind.ts
Normal file
37
src/pcast/ViewingReportKind.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import assertUnreachable from "../lang/assertUnreachable";
|
||||
|
||||
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 convertViewingReportKindToViewingReportKindType(viewingReportKind: ViewingReportKind): ViewingReportKindType {
|
||||
switch (viewingReportKind) {
|
||||
case ViewingReportKind.RealTime:
|
||||
return 'RealTime';
|
||||
case ViewingReportKind.HLS:
|
||||
return 'HLS';
|
||||
case ViewingReportKind.DASH:
|
||||
return 'DASH';
|
||||
default:
|
||||
assertUnreachable(viewingReportKind);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
export * from './Channels';
|
||||
export * from './Stream';
|
||||
export * from './Reporting';
|
||||
export * from './PCastRequests';
|
||||
export * from './Stream';
|
||||
|
||||
Reference in New Issue
Block a user