@portalshq/capability-queue-broadcast 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,246 @@
1
+ export const BodyUploadQueueItemMediaType = {
2
+ video: 'video',
3
+ image: 'image',
4
+ audio: 'audio',
5
+ };
6
+ export const HealthResponseMediaServer = {
7
+ up: 'up',
8
+ down: 'down',
9
+ };
10
+ export const JobResponseMediaType = {
11
+ video: 'video',
12
+ image: 'image',
13
+ audio: 'audio',
14
+ };
15
+ export const JobResponseStatus = {
16
+ ingesting: 'ingesting',
17
+ staged: 'staged',
18
+ queued: 'queued',
19
+ generating: 'generating',
20
+ ready: 'ready',
21
+ playing: 'playing',
22
+ done: 'done',
23
+ failed: 'failed',
24
+ };
25
+ export const QueueItemJsonMediaType = {
26
+ video: 'video',
27
+ image: 'image',
28
+ audio: 'audio',
29
+ };
30
+ export const getGetHealthUrl = () => {
31
+ return `/health`;
32
+ };
33
+ /**
34
+ * @summary Health
35
+ */
36
+ export const getHealth = async (options) => {
37
+ const res = await fetch(getGetHealthUrl(), {
38
+ ...options,
39
+ method: 'GET'
40
+ });
41
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
42
+ const data = body ? JSON.parse(body) : {};
43
+ return { data, status: res.status, headers: res.headers };
44
+ };
45
+ export const getEnqueueLegacyPromptUrl = () => {
46
+ return `/v1/prompts`;
47
+ };
48
+ /**
49
+ * @summary Submit Prompt Compat
50
+ */
51
+ export const enqueueLegacyPrompt = async (promptInput, options) => {
52
+ const res = await fetch(getEnqueueLegacyPromptUrl(), {
53
+ ...options,
54
+ method: 'POST',
55
+ headers: { 'Content-Type': 'application/json', ...options?.headers },
56
+ body: JSON.stringify(promptInput)
57
+ });
58
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
59
+ const data = body ? JSON.parse(body) : {};
60
+ return { data, status: res.status, headers: res.headers };
61
+ };
62
+ export const getGetLegacyPromptUrl = (jobId) => {
63
+ return `/v1/prompts/${jobId}`;
64
+ };
65
+ /**
66
+ * @summary Prompt Status
67
+ */
68
+ export const getLegacyPrompt = async (jobId, options) => {
69
+ const res = await fetch(getGetLegacyPromptUrl(jobId), {
70
+ ...options,
71
+ method: 'GET'
72
+ });
73
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
74
+ const data = body ? JSON.parse(body) : {};
75
+ return { data, status: res.status, headers: res.headers };
76
+ };
77
+ export const getEnqueueQueueItemUrl = () => {
78
+ return `/v1/queue`;
79
+ };
80
+ /**
81
+ * @summary Queue Json
82
+ */
83
+ export const enqueueQueueItem = async (queueItemJson, options) => {
84
+ const res = await fetch(getEnqueueQueueItemUrl(), {
85
+ ...options,
86
+ method: 'POST',
87
+ headers: { 'Content-Type': 'application/json', ...options?.headers },
88
+ body: JSON.stringify(queueItemJson)
89
+ });
90
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
91
+ const data = body ? JSON.parse(body) : {};
92
+ return { data, status: res.status, headers: res.headers };
93
+ };
94
+ export const getUploadQueuePairUrl = () => {
95
+ return `/v1/queue/pairs/upload`;
96
+ };
97
+ /**
98
+ * Atomically receive finished image and audio bytes from a remote producer.
99
+ *
100
+ * The two jobs stay `ingesting` until both files are checksum-verified and
101
+ * committed. `staged=true` persists the pair without making it eligible for
102
+ * playout; the authenticated release endpoint activates it later.
103
+ * @summary Queue Pair Upload
104
+ */
105
+ export const uploadQueuePair = async (bodyUploadQueuePair, options) => {
106
+ const formData = new FormData();
107
+ formData.append(`audio`, bodyUploadQueuePair.audio);
108
+ formData.append(`audio_sha256`, bodyUploadQueuePair.audio_sha256);
109
+ formData.append(`image`, bodyUploadQueuePair.image);
110
+ formData.append(`image_duration`, bodyUploadQueuePair.image_duration.toString());
111
+ formData.append(`image_sha256`, bodyUploadQueuePair.image_sha256);
112
+ if (bodyUploadQueuePair.staged !== undefined) {
113
+ formData.append(`staged`, bodyUploadQueuePair.staged.toString());
114
+ }
115
+ const res = await fetch(getUploadQueuePairUrl(), {
116
+ ...options,
117
+ method: 'POST',
118
+ body: formData
119
+ });
120
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
121
+ const data = body ? JSON.parse(body) : {};
122
+ return { data, status: res.status, headers: res.headers };
123
+ };
124
+ export const getGetQueuePairUrl = (pairId) => {
125
+ return `/v1/queue/pairs/${pairId}`;
126
+ };
127
+ /**
128
+ * @summary Queue Pair Status
129
+ */
130
+ export const getQueuePair = async (pairId, options) => {
131
+ const res = await fetch(getGetQueuePairUrl(pairId), {
132
+ ...options,
133
+ method: 'GET'
134
+ });
135
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
136
+ const data = body ? JSON.parse(body) : {};
137
+ return { data, status: res.status, headers: res.headers };
138
+ };
139
+ export const getReleaseQueuePairUrl = (pairId) => {
140
+ return `/v1/queue/pairs/${pairId}/release`;
141
+ };
142
+ /**
143
+ * @summary Release Queue Pair
144
+ */
145
+ export const releaseQueuePair = async (pairId, options) => {
146
+ const res = await fetch(getReleaseQueuePairUrl(pairId), {
147
+ ...options,
148
+ method: 'POST'
149
+ });
150
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
151
+ const data = body ? JSON.parse(body) : {};
152
+ return { data, status: res.status, headers: res.headers };
153
+ };
154
+ export const getGetQueueSlotUrl = (slotKey) => {
155
+ return `/v1/queue/slots/${slotKey}`;
156
+ };
157
+ /**
158
+ * @summary Queue Slot Status
159
+ */
160
+ export const getQueueSlot = async (slotKey, options) => {
161
+ const res = await fetch(getGetQueueSlotUrl(slotKey), {
162
+ ...options,
163
+ method: 'GET'
164
+ });
165
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
166
+ const data = body ? JSON.parse(body) : {};
167
+ return { data, status: res.status, headers: res.headers };
168
+ };
169
+ export const getReleaseQueueSlotUrl = (slotKey) => {
170
+ return `/v1/queue/slots/${slotKey}/release`;
171
+ };
172
+ /**
173
+ * @summary Release Queue Slot
174
+ */
175
+ export const releaseQueueSlot = async (slotKey, options) => {
176
+ const res = await fetch(getReleaseQueueSlotUrl(slotKey), {
177
+ ...options,
178
+ method: 'POST'
179
+ });
180
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
181
+ const data = body ? JSON.parse(body) : {};
182
+ return { data, status: res.status, headers: res.headers };
183
+ };
184
+ export const getUploadQueueItemUrl = () => {
185
+ return `/v1/queue/upload`;
186
+ };
187
+ /**
188
+ * @summary Queue Upload
189
+ */
190
+ export const uploadQueueItem = async (bodyUploadQueueItem, options) => {
191
+ const formData = new FormData();
192
+ if (bodyUploadQueueItem.duration !== undefined && bodyUploadQueueItem.duration !== null) {
193
+ formData.append(`duration`, bodyUploadQueueItem.duration.toString());
194
+ }
195
+ formData.append(`file`, bodyUploadQueueItem.file);
196
+ if (bodyUploadQueueItem.media_type !== undefined) {
197
+ formData.append(`media_type`, bodyUploadQueueItem.media_type);
198
+ }
199
+ if (bodyUploadQueueItem.sha256 !== undefined && bodyUploadQueueItem.sha256 !== null) {
200
+ formData.append(`sha256`, bodyUploadQueueItem.sha256);
201
+ }
202
+ if (bodyUploadQueueItem.slot_key !== undefined && bodyUploadQueueItem.slot_key !== null) {
203
+ formData.append(`slot_key`, bodyUploadQueueItem.slot_key);
204
+ }
205
+ if (bodyUploadQueueItem.staged !== undefined) {
206
+ formData.append(`staged`, bodyUploadQueueItem.staged.toString());
207
+ }
208
+ const res = await fetch(getUploadQueueItemUrl(), {
209
+ ...options,
210
+ method: 'POST',
211
+ body: formData
212
+ });
213
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
214
+ const data = body ? JSON.parse(body) : {};
215
+ return { data, status: res.status, headers: res.headers };
216
+ };
217
+ export const getGetQueueItemUrl = (jobId) => {
218
+ return `/v1/queue/${jobId}`;
219
+ };
220
+ /**
221
+ * @summary Queue Status
222
+ */
223
+ export const getQueueItem = async (jobId, options) => {
224
+ const res = await fetch(getGetQueueItemUrl(jobId), {
225
+ ...options,
226
+ method: 'GET'
227
+ });
228
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
229
+ const data = body ? JSON.parse(body) : {};
230
+ return { data, status: res.status, headers: res.headers };
231
+ };
232
+ export const getGetStreamInfoUrl = () => {
233
+ return `/v1/stream`;
234
+ };
235
+ /**
236
+ * @summary Stream Info
237
+ */
238
+ export const getStreamInfo = async (options) => {
239
+ const res = await fetch(getGetStreamInfoUrl(), {
240
+ ...options,
241
+ method: 'GET'
242
+ });
243
+ const body = [204, 205, 304].includes(res.status) ? null : await res.text();
244
+ const data = body ? JSON.parse(body) : {};
245
+ return { data, status: res.status, headers: res.headers };
246
+ };
@@ -0,0 +1,4 @@
1
+ export * from "./client.js";
2
+ export * from "./generated/api.js";
3
+ export * from "./streaming/index.js";
4
+ export * from "./monitoring/metrics.js";
package/dist/index.js ADDED
@@ -0,0 +1,7 @@
1
+ export * from "./client.js";
2
+ // Low-level OpenAPI client. Prefer QueueBroadcastClient for application code.
3
+ export * from "./generated/api.js";
4
+ // Streaming infrastructure
5
+ export * from "./streaming/index.js";
6
+ // Monitoring infrastructure
7
+ export * from "./monitoring/metrics.js";
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Simple metrics collection for monitoring streaming infrastructure.
3
+ * Designed for containerized deployment with HTTP polling instead of WebSocket.
4
+ */
5
+ export interface MetricsCollector {
6
+ /** Get current metrics snapshot */
7
+ getMetrics(): Record<string, unknown>;
8
+ /** Reset all metrics */
9
+ reset(): void;
10
+ }
11
+ export interface MetricSnapshot {
12
+ timestamp: number;
13
+ component: string;
14
+ metrics: Record<string, unknown>;
15
+ }
16
+ /**
17
+ * Simple metrics registry that can collect metrics from multiple components.
18
+ * Provides HTTP-friendly metrics output for containerized environments.
19
+ */
20
+ export declare class MetricsRegistry {
21
+ private collectors;
22
+ private history;
23
+ private latestMetrics;
24
+ private maxHistorySize;
25
+ constructor(maxHistorySize?: number);
26
+ /**
27
+ * Register a metrics collector
28
+ */
29
+ register(componentName: string, collector: MetricsCollector): void;
30
+ /**
31
+ * Unregister a metrics collector
32
+ */
33
+ unregister(componentName: string): void;
34
+ /**
35
+ * Collect metrics from all registered components
36
+ */
37
+ collectMetrics(): Record<string, MetricSnapshot>;
38
+ /**
39
+ * Get latest metrics snapshot
40
+ */
41
+ getLatestMetrics(): Record<string, MetricSnapshot> | null;
42
+ /**
43
+ * Get metrics history
44
+ */
45
+ getHistory(durationMs?: number): MetricSnapshot[];
46
+ /**
47
+ * Reset all collectors and history
48
+ */
49
+ reset(): void;
50
+ /**
51
+ * Export metrics in Prometheus-compatible format
52
+ */
53
+ exportPrometheusFormat(): string;
54
+ }
55
+ /**
56
+ * Simple metrics collector for basic counter and gauge metrics
57
+ */
58
+ export declare class SimpleMetrics implements MetricsCollector {
59
+ private counters;
60
+ private gauges;
61
+ private labels;
62
+ /**
63
+ * Increment a counter
64
+ */
65
+ incrementCounter(name: string, value?: number): void;
66
+ /**
67
+ * Set a gauge value
68
+ */
69
+ setGauge(name: string, value: number): void;
70
+ /**
71
+ * Get counter value
72
+ */
73
+ getCounter(name: string): number;
74
+ /**
75
+ * Get gauge value
76
+ */
77
+ getGauge(name: string): number;
78
+ /**
79
+ * Set a label (metadata)
80
+ */
81
+ setLabel(name: string, value: string): void;
82
+ /**
83
+ * Get all metrics
84
+ */
85
+ getMetrics(): Record<string, unknown>;
86
+ /**
87
+ * Reset all metrics
88
+ */
89
+ reset(): void;
90
+ }
@@ -0,0 +1,190 @@
1
+ /**
2
+ * Simple metrics collection for monitoring streaming infrastructure.
3
+ * Designed for containerized deployment with HTTP polling instead of WebSocket.
4
+ */
5
+ /**
6
+ * Simple metrics registry that can collect metrics from multiple components.
7
+ * Provides HTTP-friendly metrics output for containerized environments.
8
+ */
9
+ export class MetricsRegistry {
10
+ collectors = new Map();
11
+ history = [];
12
+ latestMetrics = null;
13
+ maxHistorySize;
14
+ constructor(maxHistorySize = 300) {
15
+ this.maxHistorySize = maxHistorySize;
16
+ }
17
+ /**
18
+ * Register a metrics collector
19
+ */
20
+ register(componentName, collector) {
21
+ this.collectors.set(componentName, collector);
22
+ }
23
+ /**
24
+ * Unregister a metrics collector
25
+ */
26
+ unregister(componentName) {
27
+ this.collectors.delete(componentName);
28
+ }
29
+ /**
30
+ * Collect metrics from all registered components
31
+ */
32
+ collectMetrics() {
33
+ const timestamp = Date.now();
34
+ const metrics = {};
35
+ for (const [componentName, collector] of this.collectors.entries()) {
36
+ try {
37
+ const componentMetrics = collector.getMetrics();
38
+ metrics[componentName] = {
39
+ timestamp,
40
+ component: componentName,
41
+ metrics: componentMetrics,
42
+ };
43
+ }
44
+ catch (error) {
45
+ metrics[componentName] = {
46
+ timestamp,
47
+ component: componentName,
48
+ metrics: {
49
+ error: error instanceof Error ? error.message : String(error),
50
+ status: 'error',
51
+ },
52
+ };
53
+ }
54
+ }
55
+ // Add to history
56
+ const snapshot = {
57
+ timestamp,
58
+ component: 'registry',
59
+ metrics: {
60
+ totalComponents: this.collectors.size,
61
+ componentStatuses: Object.fromEntries(Object.entries(metrics).map(([name, snapshot]) => [
62
+ name,
63
+ snapshot.metrics.error ? 'error' : 'ok',
64
+ ])),
65
+ },
66
+ };
67
+ this.history.push(snapshot);
68
+ if (this.history.length > this.maxHistorySize) {
69
+ this.history.shift();
70
+ }
71
+ this.latestMetrics = metrics;
72
+ return metrics;
73
+ }
74
+ /**
75
+ * Get latest metrics snapshot
76
+ */
77
+ getLatestMetrics() {
78
+ if (this.history.length === 0) {
79
+ return null;
80
+ }
81
+ return this.latestMetrics;
82
+ }
83
+ /**
84
+ * Get metrics history
85
+ */
86
+ getHistory(durationMs) {
87
+ if (!durationMs) {
88
+ return [...this.history];
89
+ }
90
+ const cutoff = Date.now() - durationMs;
91
+ return this.history.filter(snapshot => snapshot.timestamp >= cutoff);
92
+ }
93
+ /**
94
+ * Reset all collectors and history
95
+ */
96
+ reset() {
97
+ for (const collector of this.collectors.values()) {
98
+ try {
99
+ collector.reset();
100
+ }
101
+ catch (error) {
102
+ console.error(`Error resetting collector: ${error}`);
103
+ }
104
+ }
105
+ this.history = [];
106
+ this.latestMetrics = null;
107
+ }
108
+ /**
109
+ * Export metrics in Prometheus-compatible format
110
+ */
111
+ exportPrometheusFormat() {
112
+ const metrics = this.collectMetrics();
113
+ const lines = [];
114
+ for (const [componentName, snapshot] of Object.entries(metrics)) {
115
+ lines.push(`# Component: ${componentName}`);
116
+ lines.push(`# Timestamp: ${new Date(snapshot.timestamp).toISOString()}`);
117
+ for (const [key, value] of Object.entries(snapshot.metrics)) {
118
+ if (typeof value === 'number') {
119
+ lines.push(`${componentName}_${key} ${value}`);
120
+ }
121
+ else if (typeof value === 'boolean') {
122
+ lines.push(`${componentName}_${key} ${value ? 1 : 0}`);
123
+ }
124
+ else if (typeof value === 'string') {
125
+ lines.push(`${componentName}_${key} "${value}"`);
126
+ }
127
+ }
128
+ lines.push('');
129
+ }
130
+ return lines.join('\n');
131
+ }
132
+ }
133
+ /**
134
+ * Simple metrics collector for basic counter and gauge metrics
135
+ */
136
+ export class SimpleMetrics {
137
+ counters = new Map();
138
+ gauges = new Map();
139
+ labels = new Map();
140
+ /**
141
+ * Increment a counter
142
+ */
143
+ incrementCounter(name, value = 1) {
144
+ const current = this.counters.get(name) || 0;
145
+ this.counters.set(name, current + value);
146
+ }
147
+ /**
148
+ * Set a gauge value
149
+ */
150
+ setGauge(name, value) {
151
+ this.gauges.set(name, value);
152
+ }
153
+ /**
154
+ * Get counter value
155
+ */
156
+ getCounter(name) {
157
+ return this.counters.get(name) || 0;
158
+ }
159
+ /**
160
+ * Get gauge value
161
+ */
162
+ getGauge(name) {
163
+ return this.gauges.get(name) || 0;
164
+ }
165
+ /**
166
+ * Set a label (metadata)
167
+ */
168
+ setLabel(name, value) {
169
+ this.labels.set(name, value);
170
+ }
171
+ /**
172
+ * Get all metrics
173
+ */
174
+ getMetrics() {
175
+ const metrics = {
176
+ ...Object.fromEntries(this.counters),
177
+ ...Object.fromEntries(this.gauges),
178
+ ...Object.fromEntries(this.labels),
179
+ };
180
+ return metrics;
181
+ }
182
+ /**
183
+ * Reset all metrics
184
+ */
185
+ reset() {
186
+ this.counters.clear();
187
+ this.gauges.clear();
188
+ this.labels.clear();
189
+ }
190
+ }
@@ -0,0 +1,34 @@
1
+ /**
2
+ * Frame buffer management for real-time video streaming.
3
+ * Extracted and adapted from infinite-tv's RTMP streamer patterns.
4
+ */
5
+ export interface FrameBufferOptions {
6
+ maxSize?: number;
7
+ }
8
+ /** A bounded FIFO that retains the newest frames when a producer gets ahead. */
9
+ export declare class FrameBuffer<T> {
10
+ private readonly queue;
11
+ private readonly maxSize;
12
+ private framesDropped;
13
+ private framesAddedTotal;
14
+ constructor(options?: FrameBufferOptions);
15
+ /** Adds a frame and returns false when the oldest queued frame was dropped. */
16
+ addFrame(frame: T): boolean;
17
+ addFrameBatch(frames: readonly T[]): number;
18
+ getNextFrame(): T | null;
19
+ getStatus(): {
20
+ queueSize: number;
21
+ maxSize: number;
22
+ framesDropped: number;
23
+ framesAddedTotal: number;
24
+ utilization: number;
25
+ };
26
+ /**
27
+ * Clear the buffer
28
+ */
29
+ clear(): void;
30
+ /**
31
+ * Reset metrics
32
+ */
33
+ resetMetrics(): void;
34
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Frame buffer management for real-time video streaming.
3
+ * Extracted and adapted from infinite-tv's RTMP streamer patterns.
4
+ */
5
+ /** A bounded FIFO that retains the newest frames when a producer gets ahead. */
6
+ export class FrameBuffer {
7
+ queue = [];
8
+ maxSize;
9
+ framesDropped = 0;
10
+ framesAddedTotal = 0;
11
+ constructor(options = {}) {
12
+ this.maxSize = options.maxSize ?? 1_000;
13
+ if (!Number.isInteger(this.maxSize) || this.maxSize < 1) {
14
+ throw new TypeError("maxSize must be a positive integer");
15
+ }
16
+ }
17
+ /** Adds a frame and returns false when the oldest queued frame was dropped. */
18
+ addFrame(frame) {
19
+ const droppedFrame = this.queue.length === this.maxSize;
20
+ if (droppedFrame) {
21
+ this.queue.shift();
22
+ this.framesDropped++;
23
+ }
24
+ this.queue.push(frame);
25
+ this.framesAddedTotal++;
26
+ return !droppedFrame;
27
+ }
28
+ addFrameBatch(frames) {
29
+ let processedCount = 0;
30
+ for (const frame of frames) {
31
+ this.addFrame(frame);
32
+ processedCount++;
33
+ }
34
+ return processedCount;
35
+ }
36
+ getNextFrame() {
37
+ return this.queue.shift() ?? null;
38
+ }
39
+ getStatus() {
40
+ return {
41
+ queueSize: this.queue.length,
42
+ maxSize: this.maxSize,
43
+ framesDropped: this.framesDropped,
44
+ framesAddedTotal: this.framesAddedTotal,
45
+ utilization: this.queue.length / this.maxSize,
46
+ };
47
+ }
48
+ /**
49
+ * Clear the buffer
50
+ */
51
+ clear() {
52
+ this.queue.length = 0;
53
+ }
54
+ /**
55
+ * Reset metrics
56
+ */
57
+ resetMetrics() {
58
+ this.framesDropped = 0;
59
+ this.framesAddedTotal = 0;
60
+ }
61
+ }
@@ -0,0 +1,2 @@
1
+ export * from "./frame-buffer.js";
2
+ export * from "./rtmp/rtmp-streamer.js";
@@ -0,0 +1,2 @@
1
+ export * from "./frame-buffer.js";
2
+ export * from "./rtmp/rtmp-streamer.js";