unified-video-framework 1.4.157 → 1.4.159

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.
Files changed (67) hide show
  1. package/package.json +12 -2
  2. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts +18 -0
  3. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.d.ts.map +1 -0
  4. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js +117 -0
  5. package/packages/core/dist/analytics/adapters/PlayerAnalyticsAdapter.js.map +1 -0
  6. package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts +18 -0
  7. package/packages/core/dist/analytics/core/AnalyticsProvider.d.ts.map +1 -0
  8. package/packages/core/dist/analytics/core/AnalyticsProvider.js +99 -0
  9. package/packages/core/dist/analytics/core/AnalyticsProvider.js.map +1 -0
  10. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts +20 -0
  11. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.d.ts.map +1 -0
  12. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js +161 -0
  13. package/packages/core/dist/analytics/core/DynamicAnalyticsManager.js.map +1 -0
  14. package/packages/core/dist/analytics/core/EventBatcher.d.ts +32 -0
  15. package/packages/core/dist/analytics/core/EventBatcher.d.ts.map +1 -0
  16. package/packages/core/dist/analytics/core/EventBatcher.js +98 -0
  17. package/packages/core/dist/analytics/core/EventBatcher.js.map +1 -0
  18. package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts +19 -0
  19. package/packages/core/dist/analytics/core/PlayerAnalytics.d.ts.map +1 -0
  20. package/packages/core/dist/analytics/core/PlayerAnalytics.js +80 -0
  21. package/packages/core/dist/analytics/core/PlayerAnalytics.js.map +1 -0
  22. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts +32 -0
  23. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.d.ts.map +1 -0
  24. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js +220 -0
  25. package/packages/core/dist/analytics/examples/DynamicAnalyticsExample.js.map +1 -0
  26. package/packages/core/dist/analytics/index.d.ts +13 -0
  27. package/packages/core/dist/analytics/index.d.ts.map +1 -0
  28. package/packages/core/dist/analytics/index.js +13 -0
  29. package/packages/core/dist/analytics/index.js.map +1 -0
  30. package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts +239 -0
  31. package/packages/core/dist/analytics/types/AnalyticsTypes.d.ts.map +1 -0
  32. package/packages/core/dist/analytics/types/AnalyticsTypes.js +8 -0
  33. package/packages/core/dist/analytics/types/AnalyticsTypes.js.map +1 -0
  34. package/packages/core/dist/analytics/utils/DeviceDetection.d.ts +27 -0
  35. package/packages/core/dist/analytics/utils/DeviceDetection.d.ts.map +1 -0
  36. package/packages/core/dist/analytics/utils/DeviceDetection.js +184 -0
  37. package/packages/core/dist/analytics/utils/DeviceDetection.js.map +1 -0
  38. package/packages/core/dist/chapter-manager.d.ts +39 -0
  39. package/packages/core/dist/index.d.ts +1 -0
  40. package/packages/core/dist/index.d.ts.map +1 -1
  41. package/packages/core/dist/index.js +1 -0
  42. package/packages/core/dist/index.js.map +1 -1
  43. package/packages/core/src/analytics/README.md +902 -0
  44. package/packages/core/src/analytics/adapters/PlayerAnalyticsAdapter.ts +156 -0
  45. package/packages/core/src/analytics/core/AnalyticsProvider.ts +169 -0
  46. package/packages/core/src/analytics/core/DynamicAnalyticsManager.ts +199 -0
  47. package/packages/core/src/analytics/core/EventBatcher.ts +160 -0
  48. package/packages/core/src/analytics/core/PlayerAnalytics.ts +147 -0
  49. package/packages/core/src/analytics/index.ts +51 -0
  50. package/packages/core/src/analytics/types/AnalyticsTypes.ts +315 -0
  51. package/packages/core/src/analytics/utils/DeviceDetection.ts +220 -0
  52. package/packages/core/src/index.ts +3 -0
  53. package/packages/ios/README.md +84 -0
  54. package/packages/web/dist/WebPlayer.d.ts +6 -0
  55. package/packages/web/dist/WebPlayer.d.ts.map +1 -1
  56. package/packages/web/dist/WebPlayer.js +273 -67
  57. package/packages/web/dist/WebPlayer.js.map +1 -1
  58. package/packages/web/dist/epg/EPGController.d.ts +78 -0
  59. package/packages/web/dist/epg/EPGController.d.ts.map +1 -0
  60. package/packages/web/dist/epg/EPGController.js +476 -0
  61. package/packages/web/dist/epg/EPGController.js.map +1 -0
  62. package/packages/web/src/WebPlayer.ts +336 -85
  63. package/src/analytics/README.md +902 -0
  64. package/src/analytics/adapters/PlayerAnalyticsAdapter.ts +572 -0
  65. package/src/analytics/core/DynamicAnalyticsManager.ts +526 -0
  66. package/src/analytics/examples/DynamicAnalyticsExample.ts +324 -0
  67. package/src/analytics/index.ts +60 -0
@@ -0,0 +1,156 @@
1
+ /**
2
+ * PlayerAnalyticsAdapter - Adapter for integrating with player analytics API
3
+ * Maps internal analytics events to the player analytics system format
4
+ * Implements BaseAnalyticsProvider interface
5
+ */
6
+
7
+ import {
8
+ AnalyticsEventData,
9
+ PlayerSessionInfo,
10
+ BaseAnalyticsProvider,
11
+ PlayerAnalyticsConfig
12
+ } from '../types/AnalyticsTypes';
13
+ import { deviceDetection } from '../utils/DeviceDetection';
14
+
15
+ export class PlayerAnalyticsAdapter implements BaseAnalyticsProvider {
16
+ public name: string;
17
+ public enabled: boolean;
18
+ private config: PlayerAnalyticsConfig;
19
+ private currentSessionId: string | null = null;
20
+ private eventQueue: AnalyticsEventData[] = [];
21
+ private flushTimer: any = null;
22
+
23
+ constructor(name: string, config: PlayerAnalyticsConfig) {
24
+ this.name = name;
25
+ this.enabled = true;
26
+ this.config = config;
27
+ }
28
+
29
+ async initialize(): Promise<void> {
30
+ // No initialization needed
31
+ }
32
+
33
+ async startSession(sessionInfo: PlayerSessionInfo): Promise<string> {
34
+ const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
35
+ this.currentSessionId = sessionId;
36
+
37
+ const event: AnalyticsEventData = {
38
+ eventType: 'session_start',
39
+ timestamp: Date.now(),
40
+ video: sessionInfo.initialVideo,
41
+ device: deviceDetection.getDeviceInfo(),
42
+ metadata: {
43
+ sessionId,
44
+ playerId: this.config.playerId,
45
+ userId: sessionInfo.userId,
46
+ customData: sessionInfo.customData
47
+ }
48
+ };
49
+
50
+ await this.trackEvent(event);
51
+ return sessionId;
52
+ }
53
+
54
+ async trackEvent(event: AnalyticsEventData): Promise<void> {
55
+ if (!this.enabled) {
56
+ return;
57
+ }
58
+
59
+ this.eventQueue.push(event);
60
+
61
+ // Auto-flush if batch size reached
62
+ if (this.eventQueue.length >= (this.config.batchSize || 10)) {
63
+ await this.flush();
64
+ } else {
65
+ // Schedule flush
66
+ this.scheduleFlush();
67
+ }
68
+ }
69
+
70
+ async endSession(): Promise<void> {
71
+ if (this.currentSessionId) {
72
+ const event: AnalyticsEventData = {
73
+ eventType: 'session_end',
74
+ timestamp: Date.now(),
75
+ metadata: {
76
+ sessionId: this.currentSessionId,
77
+ playerId: this.config.playerId
78
+ }
79
+ };
80
+
81
+ await this.trackEvent(event);
82
+ await this.flush();
83
+ this.currentSessionId = null;
84
+ }
85
+ }
86
+
87
+ async flush(): Promise<void> {
88
+ if (this.eventQueue.length === 0) {
89
+ return;
90
+ }
91
+
92
+ const events = [...this.eventQueue];
93
+ this.eventQueue = [];
94
+
95
+ if (this.flushTimer) {
96
+ clearTimeout(this.flushTimer);
97
+ this.flushTimer = null;
98
+ }
99
+
100
+ try {
101
+ const payload = {
102
+ session: {
103
+ sessionId: this.currentSessionId || 'unknown',
104
+ playerId: this.config.playerId,
105
+ timestamp: Date.now(),
106
+ customData: {}
107
+ },
108
+ events
109
+ };
110
+
111
+ const response = await fetch(`${this.config.baseUrl}/analytics/player/ingest`, {
112
+ method: 'POST',
113
+ headers: {
114
+ 'Content-Type': 'application/json',
115
+ 'Authorization': `Bearer ${this.config.apiKey}`,
116
+ ...(this.config.tenantId && { 'X-Tenant-ID': this.config.tenantId })
117
+ },
118
+ body: JSON.stringify(payload)
119
+ });
120
+
121
+ if (!response.ok) {
122
+ throw new Error(`Analytics API error: ${response.status}`);
123
+ }
124
+ } catch (error) {
125
+ console.error('Failed to send analytics events:', error);
126
+ // Re-queue events on failure
127
+ this.eventQueue = [...events, ...this.eventQueue];
128
+ }
129
+ }
130
+
131
+ async destroy(): Promise<void> {
132
+ if (this.currentSessionId) {
133
+ await this.endSession();
134
+ }
135
+
136
+ if (this.flushTimer) {
137
+ clearTimeout(this.flushTimer);
138
+ this.flushTimer = null;
139
+ }
140
+
141
+ // Final flush attempt
142
+ if (this.eventQueue.length > 0) {
143
+ await this.flush();
144
+ }
145
+ }
146
+
147
+ private scheduleFlush(): void {
148
+ if (this.flushTimer) {
149
+ return;
150
+ }
151
+
152
+ this.flushTimer = setTimeout(() => {
153
+ this.flush().catch(console.error);
154
+ }, (this.config.flushInterval || 30) * 1000);
155
+ }
156
+ }
@@ -0,0 +1,169 @@
1
+ /**
2
+ * AnalyticsProvider - Base provider and factory functions
3
+ */
4
+
5
+ import {
6
+ BaseAnalyticsProvider,
7
+ AnalyticsEventData,
8
+ PlayerSessionInfo,
9
+ AnalyticsProviderType,
10
+ DynamicAnalyticsConfig,
11
+ PlayerAnalyticsConfig
12
+ } from '../types/AnalyticsTypes';
13
+ import { PlayerAnalyticsAdapter } from '../adapters/PlayerAnalyticsAdapter';
14
+
15
+ /**
16
+ * Abstract base class for analytics providers
17
+ */
18
+ export abstract class AnalyticsProvider implements BaseAnalyticsProvider {
19
+ public name: string;
20
+ public enabled: boolean;
21
+
22
+ constructor(name: string, enabled = true) {
23
+ this.name = name;
24
+ this.enabled = enabled;
25
+ }
26
+
27
+ abstract initialize(): Promise<void>;
28
+ abstract trackEvent(event: AnalyticsEventData): Promise<void>;
29
+ abstract startSession(sessionInfo: PlayerSessionInfo): Promise<string>;
30
+ abstract endSession(): Promise<void>;
31
+ abstract flush(): Promise<void>;
32
+ abstract destroy(): Promise<void>;
33
+ }
34
+
35
+ /**
36
+ * Create analytics provider based on type and configuration
37
+ */
38
+ export function createAnalyticsProvider(
39
+ type: AnalyticsProviderType,
40
+ name: string,
41
+ config: any
42
+ ): BaseAnalyticsProvider {
43
+ switch (type) {
44
+ case AnalyticsProviderType.PLAYER_ANALYTICS:
45
+ return new PlayerAnalyticsAdapter(name, config);
46
+
47
+ case AnalyticsProviderType.CUSTOM:
48
+ if (config.factory) {
49
+ return config.factory(config);
50
+ }
51
+ throw new Error(`Custom provider "${name}" requires a factory function`);
52
+
53
+ case AnalyticsProviderType.GOOGLE_ANALYTICS:
54
+ case AnalyticsProviderType.ADOBE_ANALYTICS:
55
+ default:
56
+ throw new Error(`Analytics provider type "${type}" is not supported yet`);
57
+ }
58
+ }
59
+
60
+ /**
61
+ * Get default analytics configuration
62
+ */
63
+ export function getDefaultAnalyticsConfig(): DynamicAnalyticsConfig {
64
+ return {
65
+ enabled: true,
66
+ providers: [],
67
+ globalSettings: {
68
+ enableConsoleLogging: false,
69
+ enableErrorReporting: true,
70
+ sessionTimeout: 60, // 60 minutes
71
+ defaultBatchSize: 10,
72
+ defaultFlushInterval: 30, // 30 seconds
73
+ retryAttempts: 3,
74
+ retryDelay: 1000 // 1 second
75
+ }
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Helper function to create Player Analytics provider config
81
+ */
82
+ export function createPlayerAnalyticsProviderConfig(
83
+ baseUrl: string,
84
+ apiKey: string,
85
+ playerId: string,
86
+ options: Partial<PlayerAnalyticsConfig> = {}
87
+ ): PlayerAnalyticsConfig {
88
+ return {
89
+ baseUrl,
90
+ apiKey,
91
+ playerId,
92
+ tenantId: options.tenantId,
93
+ heartbeatInterval: options.heartbeatInterval || 10,
94
+ batchSize: options.batchSize || 10,
95
+ flushInterval: options.flushInterval || 30,
96
+ enableOfflineStorage: options.enableOfflineStorage !== false,
97
+ maxRetries: options.maxRetries || 3,
98
+ retryDelay: options.retryDelay || 1000
99
+ };
100
+ }
101
+
102
+ /**
103
+ * Validate analytics configuration
104
+ */
105
+ export function validateAnalyticsConfig(config: DynamicAnalyticsConfig): void {
106
+ if (!config) {
107
+ throw new Error('Analytics configuration is required');
108
+ }
109
+
110
+ if (typeof config.enabled !== 'boolean') {
111
+ throw new Error('Analytics configuration must specify enabled state');
112
+ }
113
+
114
+ if (!Array.isArray(config.providers)) {
115
+ throw new Error('Analytics configuration must include providers array');
116
+ }
117
+
118
+ config.providers.forEach((provider, index) => {
119
+ if (!provider.name) {
120
+ throw new Error(`Provider at index ${index} must have a name`);
121
+ }
122
+
123
+ if (!provider.type) {
124
+ throw new Error(`Provider "${provider.name}" must have a type`);
125
+ }
126
+
127
+ if (typeof provider.enabled !== 'boolean') {
128
+ throw new Error(`Provider "${provider.name}" must specify enabled state`);
129
+ }
130
+
131
+ if (provider.type === AnalyticsProviderType.PLAYER_ANALYTICS) {
132
+ validatePlayerAnalyticsConfig(provider.config);
133
+ }
134
+ });
135
+ }
136
+
137
+ /**
138
+ * Validate Player Analytics specific configuration
139
+ */
140
+ function validatePlayerAnalyticsConfig(config: PlayerAnalyticsConfig): void {
141
+ if (!config.baseUrl) {
142
+ throw new Error('Player Analytics provider requires baseUrl');
143
+ }
144
+
145
+ if (!config.apiKey) {
146
+ throw new Error('Player Analytics provider requires apiKey');
147
+ }
148
+
149
+ if (!config.playerId) {
150
+ throw new Error('Player Analytics provider requires playerId');
151
+ }
152
+ }
153
+
154
+ /**
155
+ * Merge analytics configurations
156
+ */
157
+ export function mergeAnalyticsConfigs(
158
+ defaultConfig: DynamicAnalyticsConfig,
159
+ userConfig: Partial<DynamicAnalyticsConfig>
160
+ ): DynamicAnalyticsConfig {
161
+ return {
162
+ enabled: userConfig.enabled !== undefined ? userConfig.enabled : defaultConfig.enabled,
163
+ providers: userConfig.providers || defaultConfig.providers,
164
+ globalSettings: {
165
+ ...defaultConfig.globalSettings,
166
+ ...userConfig.globalSettings
167
+ }
168
+ };
169
+ }
@@ -0,0 +1,199 @@
1
+ /**
2
+ * DynamicAnalyticsManager - Main analytics system
3
+ * Manages multiple analytics providers dynamically
4
+ */
5
+
6
+ import {
7
+ DynamicAnalyticsConfig,
8
+ AnalyticsProviderType,
9
+ BaseAnalyticsProvider,
10
+ AnalyticsEventData,
11
+ VideoInfo,
12
+ PlayerState,
13
+ PlayerSessionInfo
14
+ } from '../types/AnalyticsTypes';
15
+ import { createAnalyticsProvider } from './AnalyticsProvider';
16
+
17
+ export class DynamicAnalyticsManager {
18
+ private config: DynamicAnalyticsConfig;
19
+ private providers: Map<string, BaseAnalyticsProvider> = new Map();
20
+ private currentSessionId: string | null = null;
21
+ private enabled: boolean;
22
+
23
+ constructor(config: DynamicAnalyticsConfig) {
24
+ this.config = config;
25
+ this.enabled = config.enabled;
26
+ this.initializeProviders();
27
+ }
28
+
29
+ private async initializeProviders(): Promise<void> {
30
+ for (const providerConfig of this.config.providers) {
31
+ if (providerConfig.enabled) {
32
+ try {
33
+ const provider = createAnalyticsProvider(
34
+ providerConfig.type,
35
+ providerConfig.name,
36
+ providerConfig.config
37
+ );
38
+
39
+ await provider.initialize();
40
+ this.providers.set(providerConfig.name, provider);
41
+ } catch (error) {
42
+ console.error(`Failed to initialize analytics provider "${providerConfig.name}":`, error);
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ startSession(videoInfo: VideoInfo, userInfo: any = {}): string {
49
+ if (!this.enabled) {
50
+ return '';
51
+ }
52
+
53
+ const sessionId = `sess_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
54
+ this.currentSessionId = sessionId;
55
+
56
+ const sessionInfo: PlayerSessionInfo = {
57
+ sessionId,
58
+ playerId: 'default',
59
+ startTime: Date.now(),
60
+ userId: userInfo.userId,
61
+ userType: userInfo.userType,
62
+ customData: userInfo,
63
+ initialVideo: videoInfo
64
+ };
65
+
66
+ this.providers.forEach(async (provider) => {
67
+ if (provider.enabled) {
68
+ try {
69
+ await provider.startSession(sessionInfo);
70
+ } catch (error) {
71
+ console.error(`Provider "${provider.name}" failed to start session:`, error);
72
+ }
73
+ }
74
+ });
75
+
76
+ return sessionId;
77
+ }
78
+
79
+ async endSession(): Promise<void> {
80
+ if (!this.currentSessionId || !this.enabled) {
81
+ return;
82
+ }
83
+
84
+ const promises = Array.from(this.providers.values()).map(async (provider) => {
85
+ if (provider.enabled) {
86
+ try {
87
+ await provider.endSession();
88
+ } catch (error) {
89
+ console.error(`Provider "${provider.name}" failed to end session:`, error);
90
+ }
91
+ }
92
+ });
93
+
94
+ await Promise.allSettled(promises);
95
+ this.currentSessionId = null;
96
+ }
97
+
98
+ trackEvent(eventType: string, playerState?: PlayerState, customData?: any): void {
99
+ if (!this.enabled || !this.currentSessionId) {
100
+ return;
101
+ }
102
+
103
+ const event: AnalyticsEventData = {
104
+ eventType,
105
+ timestamp: Date.now(),
106
+ currentTime: playerState?.currentTime,
107
+ player: playerState,
108
+ custom: customData
109
+ };
110
+
111
+ this.providers.forEach(async (provider) => {
112
+ if (provider.enabled) {
113
+ try {
114
+ await provider.trackEvent(event);
115
+ } catch (error) {
116
+ console.error(`Provider "${provider.name}" failed to track event:`, error);
117
+ }
118
+ }
119
+ });
120
+ }
121
+
122
+ trackCustomEvent(eventType: string, data: any): void {
123
+ if (!this.enabled) {
124
+ return;
125
+ }
126
+
127
+ const event: AnalyticsEventData = {
128
+ eventType: 'custom',
129
+ timestamp: Date.now(),
130
+ custom: { type: eventType, ...data }
131
+ };
132
+
133
+ this.providers.forEach(async (provider) => {
134
+ if (provider.enabled) {
135
+ try {
136
+ await provider.trackEvent(event);
137
+ } catch (error) {
138
+ console.error(`Provider "${provider.name}" failed to track custom event:`, error);
139
+ }
140
+ }
141
+ });
142
+ }
143
+
144
+ addProvider(name: string, type: AnalyticsProviderType, config: any): void {
145
+ try {
146
+ const provider = createAnalyticsProvider(type, name, config);
147
+ provider.initialize().then(() => {
148
+ this.providers.set(name, provider);
149
+ });
150
+ } catch (error) {
151
+ console.error(`Failed to add provider "${name}":`, error);
152
+ }
153
+ }
154
+
155
+ async removeProvider(name: string): Promise<void> {
156
+ const provider = this.providers.get(name);
157
+ if (provider) {
158
+ try {
159
+ await provider.destroy();
160
+ this.providers.delete(name);
161
+ } catch (error) {
162
+ console.error(`Failed to remove provider "${name}":`, error);
163
+ }
164
+ }
165
+ }
166
+
167
+ toggleProvider(name: string, enabled: boolean): void {
168
+ const provider = this.providers.get(name);
169
+ if (provider) {
170
+ provider.enabled = enabled;
171
+ }
172
+ }
173
+
174
+ async initialize(): Promise<void> {
175
+ // Already done in constructor, but this method is needed for interface compatibility
176
+ return Promise.resolve();
177
+ }
178
+
179
+ isEnabled(): boolean {
180
+ return this.enabled;
181
+ }
182
+
183
+ async destroy(): Promise<void> {
184
+ if (this.currentSessionId) {
185
+ await this.endSession();
186
+ }
187
+
188
+ const promises = Array.from(this.providers.values()).map(async (provider) => {
189
+ try {
190
+ await provider.destroy();
191
+ } catch (error) {
192
+ console.error(`Provider "${provider.name}" failed to destroy:`, error);
193
+ }
194
+ });
195
+
196
+ await Promise.allSettled(promises);
197
+ this.providers.clear();
198
+ }
199
+ }
@@ -0,0 +1,160 @@
1
+ /**
2
+ * EventBatcher - Batches analytics events for efficient network usage
3
+ */
4
+
5
+ import { AnalyticsEventData } from '../types/AnalyticsTypes';
6
+
7
+ export interface BatcherConfig {
8
+ maxSize?: number;
9
+ flushInterval?: number; // in milliseconds
10
+ maxWaitTime?: number; // in milliseconds
11
+ onFlush: (events: AnalyticsEventData[]) => Promise<void>;
12
+ onError?: (error: Error, events: AnalyticsEventData[]) => void;
13
+ }
14
+
15
+ export class EventBatcher {
16
+ private events: AnalyticsEventData[] = [];
17
+ private flushTimer: any = null;
18
+ private maxWaitTimer: any = null;
19
+ private destroyed = false;
20
+
21
+ private readonly maxSize: number;
22
+ private readonly flushInterval: number;
23
+ private readonly maxWaitTime: number;
24
+ private readonly onFlush: (events: AnalyticsEventData[]) => Promise<void>;
25
+ private readonly onError?: (error: Error, events: AnalyticsEventData[]) => void;
26
+
27
+ constructor(config: BatcherConfig) {
28
+ this.maxSize = config.maxSize || 10;
29
+ this.flushInterval = config.flushInterval || 30000; // 30 seconds
30
+ this.maxWaitTime = config.maxWaitTime || 60000; // 60 seconds
31
+ this.onFlush = config.onFlush;
32
+ this.onError = config.onError;
33
+ }
34
+
35
+ /**
36
+ * Add an event to the batch
37
+ */
38
+ addEvent(event: AnalyticsEventData): void {
39
+ if (this.destroyed) {
40
+ return;
41
+ }
42
+
43
+ this.events.push(event);
44
+
45
+ // Start max wait timer if this is the first event
46
+ if (this.events.length === 1) {
47
+ this.startMaxWaitTimer();
48
+ }
49
+
50
+ // Flush if batch is full
51
+ if (this.events.length >= this.maxSize) {
52
+ this.flush();
53
+ } else {
54
+ // Reset flush timer
55
+ this.resetFlushTimer();
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Manually flush all events
61
+ */
62
+ async flush(): Promise<void> {
63
+ if (this.destroyed || this.events.length === 0) {
64
+ return;
65
+ }
66
+
67
+ const eventsToFlush = [...this.events];
68
+ this.events = [];
69
+ this.clearTimers();
70
+
71
+ try {
72
+ await this.onFlush(eventsToFlush);
73
+ } catch (error) {
74
+ if (this.onError) {
75
+ this.onError(error as Error, eventsToFlush);
76
+ } else {
77
+ console.error('EventBatcher flush error:', error);
78
+ }
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Get current batch size
84
+ */
85
+ getBatchSize(): number {
86
+ return this.events.length;
87
+ }
88
+
89
+ /**
90
+ * Check if batcher has pending events
91
+ */
92
+ hasPendingEvents(): boolean {
93
+ return this.events.length > 0;
94
+ }
95
+
96
+ /**
97
+ * Destroy the batcher and flush remaining events
98
+ */
99
+ async destroy(): Promise<void> {
100
+ if (this.destroyed) {
101
+ return;
102
+ }
103
+
104
+ this.destroyed = true;
105
+ this.clearTimers();
106
+
107
+ // Flush remaining events
108
+ if (this.events.length > 0) {
109
+ await this.flush();
110
+ }
111
+ }
112
+
113
+ private resetFlushTimer(): void {
114
+ this.clearFlushTimer();
115
+
116
+ this.flushTimer = setTimeout(() => {
117
+ this.flush();
118
+ }, this.flushInterval);
119
+ }
120
+
121
+ private startMaxWaitTimer(): void {
122
+ this.clearMaxWaitTimer();
123
+
124
+ this.maxWaitTimer = setTimeout(() => {
125
+ this.flush();
126
+ }, this.maxWaitTime);
127
+ }
128
+
129
+ private clearFlushTimer(): void {
130
+ if (this.flushTimer) {
131
+ clearTimeout(this.flushTimer);
132
+ this.flushTimer = null;
133
+ }
134
+ }
135
+
136
+ private clearMaxWaitTimer(): void {
137
+ if (this.maxWaitTimer) {
138
+ clearTimeout(this.maxWaitTimer);
139
+ this.maxWaitTimer = null;
140
+ }
141
+ }
142
+
143
+ private clearTimers(): void {
144
+ this.clearFlushTimer();
145
+ this.clearMaxWaitTimer();
146
+ }
147
+ }
148
+
149
+ /**
150
+ * Create an EventBatcher with default configuration
151
+ */
152
+ export function createEventBatcher(
153
+ onFlush: (events: AnalyticsEventData[]) => Promise<void>,
154
+ options: Partial<BatcherConfig> = {}
155
+ ): EventBatcher {
156
+ return new EventBatcher({
157
+ onFlush,
158
+ ...options
159
+ });
160
+ }