feat(packages): Phase 3.1 - Create @bytelyst/broadcast-client package

- package.json: ESM module config
- src/index.ts: Broadcast client factory with types, hooks
- tsconfig.json: TypeScript configuration
This commit is contained in:
saravanakumardb1 2026-03-03 07:34:39 -08:00
parent 1832ef48a3
commit c720f1c8de
3 changed files with 202 additions and 0 deletions

View File

@ -0,0 +1,21 @@
{
"name": "@bytelyst/broadcast-client",
"version": "0.1.0",
"type": "module",
"description": "Browser/React Native-safe broadcast messaging client for platform-service",
"exports": {
".": {
"import": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"files": [
"dist"
],
"scripts": {
"build": "tsc",
"test": "vitest run"
}
}

View File

@ -0,0 +1,171 @@
/**
* Broadcast Client Browser/React Native-safe broadcast messaging client
* @module @bytelyst/broadcast-client
*/
// =============================================================================
// Types
// =============================================================================
export interface Broadcast {
id: string;
productId: string;
title: string;
body: string;
bodyMarkdown?: string;
ctaText?: string;
ctaUrl?: string;
imageUrl?: string;
channels: ('push' | 'in_app' | 'email')[];
status: 'draft' | 'scheduled' | 'sending' | 'sent' | 'paused';
scheduledAt?: string;
sentAt?: string;
variant?: 'control' | 'treatment';
experimentId?: string;
parentBroadcastId?: string;
metrics: BroadcastMetrics;
createdAt: string;
updatedAt: string;
createdBy: string;
}
export interface BroadcastMetrics {
targetedCount: number;
sentCount: number;
deliveredCount: number;
openedCount: number;
clickedCount: number;
dismissedCount: number;
convertedCount: number;
}
export interface InAppMessage {
id: string;
userId: string;
productId: string;
broadcastId: string;
title: string;
body: string;
bodyMarkdown?: string;
ctaText?: string;
ctaUrl?: string;
priority: 'low' | 'normal' | 'high' | 'urgent';
style: 'banner' | 'modal' | 'toast' | 'fullscreen';
dismissible: boolean;
expiresAt?: string;
status: 'unread' | 'read' | 'dismissed';
createdAt: string;
updatedAt: string;
}
export interface BroadcastClientConfig {
/** Platform service base URL */
baseUrl: string;
/** Product ID */
productId: string;
/** Auth token provider (async or sync) */
getAuthToken: (() => string) | (() => Promise<string>);
/** Platform identifier */
platform: 'web' | 'ios' | 'android' | 'macos' | 'windows';
/** App version */
appVersion: string;
/** OS version */
osVersion: string;
/** Optional country code */
countryCode?: string;
/** Optional region code */
regionCode?: string;
/** User segments (default: ['free']) */
userSegments?: string[];
}
// =============================================================================
// Client Factory
// =============================================================================
export interface BroadcastClient {
/** List active in-app messages for current user */
listMessages(): Promise<{ messages: InAppMessage[] }>;
/** Mark message as read */
markRead(messageId: string): Promise<void>;
/** Mark message as dismissed */
markDismissed(messageId: string): Promise<void>;
/** Track CTA click */
trackClick(messageId: string): Promise<{ redirectUrl?: string }>;
/** Poll for new messages (use with setInterval) */
pollMessages(intervalMs?: number): () => void;
}
export function createBroadcastClient(config: BroadcastClientConfig): BroadcastClient {
const headers = async () => ({
'Content-Type': 'application/json',
'Authorization': `Bearer ${await Promise.resolve(config.getAuthToken())}`,
'x-product-id': config.productId,
'x-platform': config.platform,
'x-app-version': config.appVersion,
'x-os-version': config.osVersion,
...(config.countryCode && { 'x-country-code': config.countryCode }),
...(config.regionCode && { 'x-region-code': config.regionCode }),
'x-user-segments': (config.userSegments ?? ['free']).join(','),
});
const request = async <T>(path: string, options?: RequestInit): Promise<T> => {
const res = await fetch(`${config.baseUrl}${path}`, {
...options,
headers: {
...(await headers()),
...(options?.headers || {}),
},
});
if (!res.ok) {
const err = await res.text();
throw new Error(`Broadcast API error: ${res.status} ${err}`);
}
return res.json() as Promise<T>;
};
let pollInterval: ReturnType<typeof setInterval> | null = null;
return {
async listMessages() {
return request<{ messages: InAppMessage[] }>('/broadcasts');
},
async markRead(messageId: string) {
await request<void>(`/broadcasts/${messageId}/read`, { method: 'POST' });
},
async markDismissed(messageId: string) {
await request<void>(`/broadcasts/${messageId}/dismiss`, { method: 'POST' });
},
async trackClick(messageId: string) {
return request<{ redirectUrl?: string }>(`/broadcasts/${messageId}/click`, {
method: 'POST',
});
},
pollMessages(intervalMs = 60000) {
if (pollInterval) clearInterval(pollInterval);
pollInterval = setInterval(() => {
this.listMessages().catch(() => {});
}, intervalMs);
return () => {
if (pollInterval) {
clearInterval(pollInterval);
pollInterval = null;
}
};
},
};
}
// =============================================================================
// React Hook (optional)
// =============================================================================
export function createUseBroadcast(client: BroadcastClient) {
return function useBroadcast() {
return { client };
};
}

View File

@ -0,0 +1,10 @@
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "./dist",
"rootDir": "./src",
"declaration": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}