vg-x07df 0.1.0
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.
- package/.azure-pipelines/publish-public.yml +37 -0
- package/.azure-pipelines/publish.yml +39 -0
- package/.changeset/README.md +8 -0
- package/.changeset/config.json +11 -0
- package/AUTO_JOIN_GUIDE.md +411 -0
- package/README.md +215 -0
- package/Screenshot 2025-09-24 at 14.34.48.png +0 -0
- package/Screenshot 2025-10-04 at 12.58.54.png +0 -0
- package/biome.json +48 -0
- package/examples/demo/.env.example +19 -0
- package/examples/demo/CHANGELOG.md +22 -0
- package/examples/demo/README.md +72 -0
- package/examples/demo/eslint.config.js +23 -0
- package/examples/demo/index.html +13 -0
- package/examples/demo/package.json +34 -0
- package/examples/demo/pnpm-lock.yaml +2098 -0
- package/examples/demo/pnpm-workspace.yaml +1 -0
- package/examples/demo/public/vite.svg +1 -0
- package/examples/demo/src/App.css +52 -0
- package/examples/demo/src/App.tsx +176 -0
- package/examples/demo/src/assets/react.svg +1 -0
- package/examples/demo/src/components/auth/LoginForm.css +144 -0
- package/examples/demo/src/components/auth/LoginForm.tsx +80 -0
- package/examples/demo/src/components/calling/AutoJoinSettings.tsx +213 -0
- package/examples/demo/src/components/calling/AutoJoinStatus.tsx +72 -0
- package/examples/demo/src/components/calling/CallInitiator.css +258 -0
- package/examples/demo/src/components/calling/CallInitiator.tsx +142 -0
- package/examples/demo/src/components/calling/CallNotifications.css +119 -0
- package/examples/demo/src/components/calling/CallNotifications.tsx +108 -0
- package/examples/demo/src/components/calling/IncomingCallModal.css +192 -0
- package/examples/demo/src/components/calling/IncomingCallModal.tsx +78 -0
- package/examples/demo/src/components/calling/MinimizedCall.css +156 -0
- package/examples/demo/src/components/calling/MinimizedCall.tsx +78 -0
- package/examples/demo/src/components/conference/ConferenceHeader.css +265 -0
- package/examples/demo/src/components/conference/ConferenceHeader.tsx +78 -0
- package/examples/demo/src/components/conference/EnhancedControlBar.css +356 -0
- package/examples/demo/src/components/conference/EnhancedControlBar.tsx +262 -0
- package/examples/demo/src/components/conference/PaginationControls.css +67 -0
- package/examples/demo/src/components/conference/PaginationControls.tsx +64 -0
- package/examples/demo/src/components/conference/ParticipantGrid.css +153 -0
- package/examples/demo/src/components/conference/ParticipantGrid.tsx +87 -0
- package/examples/demo/src/components/conference/ParticipantTile.css +210 -0
- package/examples/demo/src/components/conference/ParticipantTile.tsx +114 -0
- package/examples/demo/src/components/conference/VideoConference.css +214 -0
- package/examples/demo/src/components/conference/VideoConference.tsx +93 -0
- package/examples/demo/src/contexts/AuthContext.tsx +105 -0
- package/examples/demo/src/hooks/useAuth.ts +5 -0
- package/examples/demo/src/hooks/useCallTimer.ts +42 -0
- package/examples/demo/src/index.css +68 -0
- package/examples/demo/src/main.tsx +10 -0
- package/examples/demo/src/services/auth.service.ts +153 -0
- package/examples/demo/src/types/auth.types.ts +31 -0
- package/examples/demo/tsconfig.app.json +28 -0
- package/examples/demo/tsconfig.json +7 -0
- package/examples/demo/tsconfig.node.json +26 -0
- package/examples/demo/vite.config.ts +15 -0
- package/images/callpad-without-ai.png +0 -0
- package/package.json +28 -0
- package/packages/sdk/CHANGELOG.md +33 -0
- package/packages/sdk/LICENSE +21 -0
- package/packages/sdk/README.md +97 -0
- package/packages/sdk/documentation.md +1132 -0
- package/packages/sdk/openapi-ts.config.ts +7 -0
- package/packages/sdk/package.json +88 -0
- package/packages/sdk/src/core/auth.manager.ts +52 -0
- package/packages/sdk/src/core/events/event-bus.ts +301 -0
- package/packages/sdk/src/core/events/index.ts +8 -0
- package/packages/sdk/src/core/events/types.ts +165 -0
- package/packages/sdk/src/core/index.ts +3 -0
- package/packages/sdk/src/core/signal/api.config.ts +49 -0
- package/packages/sdk/src/core/signal/index.ts +16 -0
- package/packages/sdk/src/core/signal/signal.client.ts +101 -0
- package/packages/sdk/src/core/signal/types.ts +110 -0
- package/packages/sdk/src/core/socketio/handlers/base.handler.ts +212 -0
- package/packages/sdk/src/core/socketio/handlers/call-accepted.handler.ts +34 -0
- package/packages/sdk/src/core/socketio/handlers/call-canceled.handler.ts +34 -0
- package/packages/sdk/src/core/socketio/handlers/call-declined.handler.ts +29 -0
- package/packages/sdk/src/core/socketio/handlers/call-ended.handler.ts +40 -0
- package/packages/sdk/src/core/socketio/handlers/call-incoming.handler.ts +72 -0
- package/packages/sdk/src/core/socketio/handlers/call-join-info.handler.ts +181 -0
- package/packages/sdk/src/core/socketio/handlers/call-participant-joined.handler.ts +42 -0
- package/packages/sdk/src/core/socketio/handlers/call-participant-joining.handler.ts +42 -0
- package/packages/sdk/src/core/socketio/handlers/call-timeout.handler.ts +31 -0
- package/packages/sdk/src/core/socketio/handlers/handler.registry.ts +62 -0
- package/packages/sdk/src/core/socketio/handlers/index.ts +21 -0
- package/packages/sdk/src/core/socketio/handlers/participant-left.handler.ts +37 -0
- package/packages/sdk/src/core/socketio/handlers/schema.ts +130 -0
- package/packages/sdk/src/core/socketio/index.ts +5 -0
- package/packages/sdk/src/core/socketio/socket.manager.ts +187 -0
- package/packages/sdk/src/core/socketio/types.ts +14 -0
- package/packages/sdk/src/core/types.ts +23 -0
- package/packages/sdk/src/generated/api/core/ApiError.ts +21 -0
- package/packages/sdk/src/generated/api/core/ApiRequestOptions.ts +13 -0
- package/packages/sdk/src/generated/api/core/ApiResult.ts +7 -0
- package/packages/sdk/src/generated/api/core/CancelablePromise.ts +126 -0
- package/packages/sdk/src/generated/api/core/OpenAPI.ts +55 -0
- package/packages/sdk/src/generated/api/core/request.ts +339 -0
- package/packages/sdk/src/generated/api/index.ts +5 -0
- package/packages/sdk/src/generated/api/models.ts +219 -0
- package/packages/sdk/src/generated/api/services.ts +225 -0
- package/packages/sdk/src/hooks/index.ts +21 -0
- package/packages/sdk/src/hooks/useAutoJoin.ts +66 -0
- package/packages/sdk/src/hooks/useCallActions.ts +28 -0
- package/packages/sdk/src/hooks/useCallQuality.ts +416 -0
- package/packages/sdk/src/hooks/useCallState.ts +23 -0
- package/packages/sdk/src/hooks/useConnection.ts +15 -0
- package/packages/sdk/src/hooks/useDevices.ts +296 -0
- package/packages/sdk/src/hooks/useErrorRecovery.ts +299 -0
- package/packages/sdk/src/hooks/useErrors.ts +84 -0
- package/packages/sdk/src/hooks/useEvent.ts +188 -0
- package/packages/sdk/src/hooks/useMediaControls.ts +215 -0
- package/packages/sdk/src/hooks/useParticipantStatus.ts +318 -0
- package/packages/sdk/src/hooks/useParticipants.ts +111 -0
- package/packages/sdk/src/index.ts +66 -0
- package/packages/sdk/src/livekit/constants.ts +76 -0
- package/packages/sdk/src/livekit/device.manager.ts +172 -0
- package/packages/sdk/src/livekit/error-classifier.ts +155 -0
- package/packages/sdk/src/livekit/events/eventBridge.ts +371 -0
- package/packages/sdk/src/livekit/events/trackRegistry.ts +114 -0
- package/packages/sdk/src/livekit/index.ts +49 -0
- package/packages/sdk/src/livekit/livekit.service.ts +110 -0
- package/packages/sdk/src/livekit/media.controls.ts +315 -0
- package/packages/sdk/src/livekit/room.manager.ts +79 -0
- package/packages/sdk/src/livekit/track.utils.ts +230 -0
- package/packages/sdk/src/livekit/types.ts +135 -0
- package/packages/sdk/src/provider/RtcProvider.tsx +78 -0
- package/packages/sdk/src/services/call-actions.ts +260 -0
- package/packages/sdk/src/services/error-recovery.ts +461 -0
- package/packages/sdk/src/services/index.ts +2 -0
- package/packages/sdk/src/services/sdk-builder.ts +104 -0
- package/packages/sdk/src/state/errors.ts +163 -0
- package/packages/sdk/src/state/selectors.ts +28 -0
- package/packages/sdk/src/state/store.ts +36 -0
- package/packages/sdk/src/state/types.ts +151 -0
- package/packages/sdk/src/utils/logger.ts +183 -0
- package/packages/sdk/tsconfig.json +49 -0
- package/packages/sdk/tsup.config.ts +51 -0
- package/pnpm-workspace.yaml +4 -0
- package/tsconfig.base.json +19 -0
- package/turbo.json +34 -0
|
@@ -0,0 +1,461 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Enhanced Error Recovery Service
|
|
3
|
+
*
|
|
4
|
+
* Implements automatic error recovery and retry mechanisms for the SDK.
|
|
5
|
+
* Handles network reconnection, failed participant invitations, and media failures.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { SdkEventType, eventBus } from "../core/events";
|
|
9
|
+
import { rtcStore } from "../state/store";
|
|
10
|
+
import type { RtcError } from "../state/types";
|
|
11
|
+
import { createLogger } from "../utils/logger";
|
|
12
|
+
|
|
13
|
+
const logger = createLogger("services:error-recovery");
|
|
14
|
+
|
|
15
|
+
export interface ErrorRecoveryConfig {
|
|
16
|
+
maxRetries: number;
|
|
17
|
+
retryDelay: number;
|
|
18
|
+
exponentialBackoff: boolean;
|
|
19
|
+
recoverableErrors: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface RetryContext {
|
|
23
|
+
attempts: number;
|
|
24
|
+
lastAttempt: number;
|
|
25
|
+
error: RtcError;
|
|
26
|
+
config: ErrorRecoveryConfig;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default error recovery configuration
|
|
31
|
+
*/
|
|
32
|
+
export const DEFAULT_ERROR_RECOVERY_CONFIG: ErrorRecoveryConfig = {
|
|
33
|
+
maxRetries: 3,
|
|
34
|
+
retryDelay: 1000, // 1 second
|
|
35
|
+
exponentialBackoff: true,
|
|
36
|
+
recoverableErrors: [
|
|
37
|
+
"NETWORK_ERROR",
|
|
38
|
+
"CONNECTION_LOST",
|
|
39
|
+
"SOCKET_DISCONNECTED",
|
|
40
|
+
"LIVEKIT_CONNECTION_FAILED",
|
|
41
|
+
"MEDIA_PERMISSION_DENIED",
|
|
42
|
+
"DEVICE_SWITCH_FAILED",
|
|
43
|
+
"PARTICIPANT_INVITATION_FAILED",
|
|
44
|
+
],
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Error Recovery Service
|
|
49
|
+
*/
|
|
50
|
+
export class ErrorRecoveryService {
|
|
51
|
+
private config: ErrorRecoveryConfig;
|
|
52
|
+
private activeRetries = new Map<string, RetryContext>();
|
|
53
|
+
private reconnectionTimer: number | null = null;
|
|
54
|
+
private isRecovering = false;
|
|
55
|
+
|
|
56
|
+
constructor(config: ErrorRecoveryConfig = DEFAULT_ERROR_RECOVERY_CONFIG) {
|
|
57
|
+
this.config = config;
|
|
58
|
+
this.setupEventListeners();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
private setupEventListeners(): void {
|
|
62
|
+
// Listen for errors that might need recovery
|
|
63
|
+
eventBus.on(SdkEventType.ERROR_OCCURRED, (event) => {
|
|
64
|
+
this.handleError(event.payload);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// Listen for connection quality changes
|
|
68
|
+
eventBus.on(SdkEventType.CONNECTION_QUALITY_CHANGED, (event) => {
|
|
69
|
+
if (event.payload.quality === "lost") {
|
|
70
|
+
this.handleConnectionLoss(event.payload.participantId);
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// Listen for participant left events (might indicate network issues)
|
|
75
|
+
eventBus.on(SdkEventType.PARTICIPANT_LEFT, (event) => {
|
|
76
|
+
if (event.payload.reason === "error") {
|
|
77
|
+
this.handleParticipantError(event.payload.participantId);
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Handle error and determine if recovery should be attempted
|
|
84
|
+
*/
|
|
85
|
+
private async handleError(error: RtcError): Promise<void> {
|
|
86
|
+
logger.debug("Handling error for recovery", { error });
|
|
87
|
+
|
|
88
|
+
// Check if error is recoverable
|
|
89
|
+
if (!this.isRecoverableError(error)) {
|
|
90
|
+
logger.debug("Error is not recoverable", { code: error.code });
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Get or create retry context
|
|
95
|
+
const retryKey = this.getRetryKey(error);
|
|
96
|
+
let context = this.activeRetries.get(retryKey);
|
|
97
|
+
|
|
98
|
+
if (!context) {
|
|
99
|
+
context = {
|
|
100
|
+
attempts: 0,
|
|
101
|
+
lastAttempt: 0,
|
|
102
|
+
error,
|
|
103
|
+
config: this.config,
|
|
104
|
+
};
|
|
105
|
+
this.activeRetries.set(retryKey, context);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Check if we've exceeded max retries
|
|
109
|
+
if (context.attempts >= this.config.maxRetries) {
|
|
110
|
+
logger.warn("Max retries exceeded for error", {
|
|
111
|
+
code: error.code,
|
|
112
|
+
attempts: context.attempts,
|
|
113
|
+
});
|
|
114
|
+
this.activeRetries.delete(retryKey);
|
|
115
|
+
this.emitRecoveryFailed(error, context.attempts);
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Calculate delay with exponential backoff
|
|
120
|
+
const delay = this.calculateRetryDelay(context.attempts);
|
|
121
|
+
const now = Date.now();
|
|
122
|
+
|
|
123
|
+
// Ensure minimum delay between attempts
|
|
124
|
+
if (now - context.lastAttempt < delay) {
|
|
125
|
+
setTimeout(
|
|
126
|
+
() => this.attemptRecovery(retryKey),
|
|
127
|
+
delay - (now - context.lastAttempt)
|
|
128
|
+
);
|
|
129
|
+
return;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
await this.attemptRecovery(retryKey);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Attempt to recover from the error
|
|
137
|
+
*/
|
|
138
|
+
private async attemptRecovery(retryKey: string): Promise<void> {
|
|
139
|
+
const context = this.activeRetries.get(retryKey);
|
|
140
|
+
if (!context) return;
|
|
141
|
+
|
|
142
|
+
context.attempts++;
|
|
143
|
+
context.lastAttempt = Date.now();
|
|
144
|
+
|
|
145
|
+
logger.info("Attempting error recovery", {
|
|
146
|
+
code: context.error.code,
|
|
147
|
+
attempt: context.attempts,
|
|
148
|
+
maxRetries: this.config.maxRetries,
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
this.emitRecoveryAttempt(context.error, context.attempts);
|
|
152
|
+
|
|
153
|
+
try {
|
|
154
|
+
const success = await this.executeRecoveryStrategy(context.error);
|
|
155
|
+
|
|
156
|
+
if (success) {
|
|
157
|
+
logger.info("Error recovery successful", {
|
|
158
|
+
code: context.error.code,
|
|
159
|
+
attempts: context.attempts,
|
|
160
|
+
});
|
|
161
|
+
this.activeRetries.delete(retryKey);
|
|
162
|
+
this.emitRecoverySuccess(context.error, context.attempts);
|
|
163
|
+
} else {
|
|
164
|
+
// Recovery failed, will retry later
|
|
165
|
+
logger.debug("Recovery attempt failed", {
|
|
166
|
+
code: context.error.code,
|
|
167
|
+
attempts: context.attempts,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
} catch (recoveryError) {
|
|
171
|
+
logger.error("Recovery attempt threw error", {
|
|
172
|
+
originalError: context.error.code,
|
|
173
|
+
recoveryError,
|
|
174
|
+
attempts: context.attempts,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Execute the appropriate recovery strategy based on error type
|
|
181
|
+
*/
|
|
182
|
+
private async executeRecoveryStrategy(error: RtcError): Promise<boolean> {
|
|
183
|
+
switch (error.code) {
|
|
184
|
+
case "NETWORK_ERROR":
|
|
185
|
+
case "CONNECTION_LOST":
|
|
186
|
+
case "SOCKET_DISCONNECTED":
|
|
187
|
+
return this.recoverNetworkConnection();
|
|
188
|
+
|
|
189
|
+
case "LIVEKIT_CONNECTION_FAILED":
|
|
190
|
+
return this.recoverLivekitConnection();
|
|
191
|
+
|
|
192
|
+
case "MEDIA_PERMISSION_DENIED":
|
|
193
|
+
return this.recoverMediaPermission(error);
|
|
194
|
+
|
|
195
|
+
case "DEVICE_SWITCH_FAILED":
|
|
196
|
+
return this.recoverDeviceSwitch(error);
|
|
197
|
+
|
|
198
|
+
case "PARTICIPANT_INVITATION_FAILED":
|
|
199
|
+
return this.recoverParticipantInvitation(error);
|
|
200
|
+
|
|
201
|
+
default:
|
|
202
|
+
logger.warn("No recovery strategy for error", { code: error.code });
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Recovery strategies
|
|
209
|
+
*/
|
|
210
|
+
private async recoverNetworkConnection(): Promise<boolean> {
|
|
211
|
+
try {
|
|
212
|
+
// Check if we're already recovering to avoid duplicate attempts
|
|
213
|
+
if (this.isRecovering) {
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
this.isRecovering = true;
|
|
218
|
+
|
|
219
|
+
// Get SDK instance from store
|
|
220
|
+
const store = rtcStore.getState();
|
|
221
|
+
|
|
222
|
+
// Try to reconnect socket
|
|
223
|
+
if (store.connection && !store.connection.connected) {
|
|
224
|
+
logger.debug("Attempting socket reconnection");
|
|
225
|
+
|
|
226
|
+
// The socket manager should handle reconnection automatically
|
|
227
|
+
// We just wait a bit and check if it succeeded
|
|
228
|
+
await this.delay(2000);
|
|
229
|
+
|
|
230
|
+
const newState = rtcStore.getState();
|
|
231
|
+
const success = newState.connection.connected;
|
|
232
|
+
|
|
233
|
+
if (success) {
|
|
234
|
+
logger.info("Network connection recovered");
|
|
235
|
+
return true;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return false;
|
|
240
|
+
} catch (error) {
|
|
241
|
+
logger.error("Network recovery failed", { error });
|
|
242
|
+
return false;
|
|
243
|
+
} finally {
|
|
244
|
+
this.isRecovering = false;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
private async recoverLivekitConnection(): Promise<boolean> {
|
|
249
|
+
try {
|
|
250
|
+
// For LiveKit connection recovery, we'd need access to the SDK instance
|
|
251
|
+
// This would typically involve re-establishing the LiveKit room connection
|
|
252
|
+
logger.debug("Attempting LiveKit connection recovery");
|
|
253
|
+
|
|
254
|
+
// This is a placeholder - actual implementation would need SDK access
|
|
255
|
+
// to call something like sdk.livekit.reconnect()
|
|
256
|
+
|
|
257
|
+
return false; // Placeholder
|
|
258
|
+
} catch (error) {
|
|
259
|
+
logger.error("LiveKit recovery failed", { error });
|
|
260
|
+
return false;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
private async recoverMediaPermission(error: RtcError): Promise<boolean> {
|
|
265
|
+
try {
|
|
266
|
+
logger.debug("Attempting media permission recovery");
|
|
267
|
+
|
|
268
|
+
// For media permission recovery, we could try requesting permissions again
|
|
269
|
+
// or gracefully degrade to audio-only mode
|
|
270
|
+
|
|
271
|
+
const context = error.context;
|
|
272
|
+
if (context?.device === "camera") {
|
|
273
|
+
// Try to gracefully degrade to audio-only
|
|
274
|
+
rtcStore.getState().patch((state) => {
|
|
275
|
+
state.local.videoEnabled = false;
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
// Emit event about graceful degradation
|
|
279
|
+
eventBus.emit(
|
|
280
|
+
SdkEventType.MEDIA_DISABLED,
|
|
281
|
+
{
|
|
282
|
+
participantId: "local",
|
|
283
|
+
mediaType: "video",
|
|
284
|
+
timestamp: Date.now(),
|
|
285
|
+
},
|
|
286
|
+
"user"
|
|
287
|
+
);
|
|
288
|
+
|
|
289
|
+
logger.info(
|
|
290
|
+
"Gracefully degraded to audio-only due to camera permission"
|
|
291
|
+
);
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
return false;
|
|
296
|
+
} catch (error) {
|
|
297
|
+
logger.error("Media permission recovery failed", { error });
|
|
298
|
+
return false;
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
private async recoverDeviceSwitch(error: RtcError): Promise<boolean> {
|
|
303
|
+
try {
|
|
304
|
+
logger.debug("Attempting device switch recovery");
|
|
305
|
+
|
|
306
|
+
// For device switch recovery, we could try falling back to default device
|
|
307
|
+
// This would need access to the device manager
|
|
308
|
+
|
|
309
|
+
return false; // Placeholder
|
|
310
|
+
} catch (error) {
|
|
311
|
+
logger.error("Device switch recovery failed", { error });
|
|
312
|
+
return false;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
private async recoverParticipantInvitation(
|
|
317
|
+
error: RtcError
|
|
318
|
+
): Promise<boolean> {
|
|
319
|
+
try {
|
|
320
|
+
logger.debug("Attempting participant invitation recovery");
|
|
321
|
+
|
|
322
|
+
// For participant invitation recovery, we could retry the invitation
|
|
323
|
+
// This would need access to the call actions service
|
|
324
|
+
|
|
325
|
+
return false; // Placeholder
|
|
326
|
+
} catch (error) {
|
|
327
|
+
logger.error("Participant invitation recovery failed", { error });
|
|
328
|
+
return false;
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Handle connection loss for a specific participant
|
|
334
|
+
*/
|
|
335
|
+
private handleConnectionLoss(participantId: string): void {
|
|
336
|
+
logger.debug("Handling connection loss", { participantId });
|
|
337
|
+
|
|
338
|
+
// For local participant connection loss, trigger network recovery
|
|
339
|
+
if (participantId === "local") {
|
|
340
|
+
const networkError: RtcError = {
|
|
341
|
+
code: "CONNECTION_LOST",
|
|
342
|
+
message: "Local participant connection lost",
|
|
343
|
+
timestamp: Date.now(),
|
|
344
|
+
context: { participantId },
|
|
345
|
+
};
|
|
346
|
+
|
|
347
|
+
this.handleError(networkError);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
/**
|
|
352
|
+
* Handle participant error
|
|
353
|
+
*/
|
|
354
|
+
private handleParticipantError(participantId: string): void {
|
|
355
|
+
logger.debug("Handling participant error", { participantId });
|
|
356
|
+
|
|
357
|
+
// Could implement participant re-invitation logic here
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Utility methods
|
|
362
|
+
*/
|
|
363
|
+
private isRecoverableError(error: RtcError): boolean {
|
|
364
|
+
return this.config.recoverableErrors.includes(error.code);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
private getRetryKey(error: RtcError): string {
|
|
368
|
+
// Create a unique key for this error type and context
|
|
369
|
+
return `${error.code}-${JSON.stringify(error.context || {})}`;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
private calculateRetryDelay(attempts: number): number {
|
|
373
|
+
if (!this.config.exponentialBackoff) {
|
|
374
|
+
return this.config.retryDelay;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
// Exponential backoff: delay * (2 ^ attempts)
|
|
378
|
+
return this.config.retryDelay * 2 ** attempts;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
private delay(ms: number): Promise<void> {
|
|
382
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Event emission methods
|
|
387
|
+
*/
|
|
388
|
+
private emitRecoveryAttempt(error: RtcError, attempts: number): void {
|
|
389
|
+
eventBus.emit(
|
|
390
|
+
"recovery:attempt",
|
|
391
|
+
{
|
|
392
|
+
error,
|
|
393
|
+
attempts,
|
|
394
|
+
timestamp: Date.now(),
|
|
395
|
+
},
|
|
396
|
+
"user"
|
|
397
|
+
);
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
private emitRecoverySuccess(error: RtcError, attempts: number): void {
|
|
401
|
+
eventBus.emit(
|
|
402
|
+
"recovery:success",
|
|
403
|
+
{
|
|
404
|
+
error,
|
|
405
|
+
attempts,
|
|
406
|
+
timestamp: Date.now(),
|
|
407
|
+
},
|
|
408
|
+
"user"
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
private emitRecoveryFailed(error: RtcError, attempts: number): void {
|
|
413
|
+
eventBus.emit(
|
|
414
|
+
"recovery:failed",
|
|
415
|
+
{
|
|
416
|
+
error,
|
|
417
|
+
attempts,
|
|
418
|
+
timestamp: Date.now(),
|
|
419
|
+
},
|
|
420
|
+
"user"
|
|
421
|
+
);
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
/**
|
|
425
|
+
* Public methods for managing recovery
|
|
426
|
+
*/
|
|
427
|
+
public updateConfig(newConfig: Partial<ErrorRecoveryConfig>): void {
|
|
428
|
+
this.config = { ...this.config, ...newConfig };
|
|
429
|
+
logger.debug("Updated error recovery config", { config: this.config });
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
public getActiveRetries(): Map<string, RetryContext> {
|
|
433
|
+
return new Map(this.activeRetries);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
public cancelRetry(retryKey: string): boolean {
|
|
437
|
+
const cancelled = this.activeRetries.delete(retryKey);
|
|
438
|
+
if (cancelled) {
|
|
439
|
+
logger.debug("Cancelled retry", { retryKey });
|
|
440
|
+
}
|
|
441
|
+
return cancelled;
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
public cancelAllRetries(): void {
|
|
445
|
+
const count = this.activeRetries.size;
|
|
446
|
+
this.activeRetries.clear();
|
|
447
|
+
logger.debug("Cancelled all retries", { count });
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
public destroy(): void {
|
|
451
|
+
this.cancelAllRetries();
|
|
452
|
+
if (this.reconnectionTimer) {
|
|
453
|
+
clearTimeout(this.reconnectionTimer);
|
|
454
|
+
this.reconnectionTimer = null;
|
|
455
|
+
}
|
|
456
|
+
logger.debug("Error recovery service destroyed");
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// Global error recovery service instance
|
|
461
|
+
export const errorRecoveryService = new ErrorRecoveryService();
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { AuthManager, SocketManager } from "../core";
|
|
2
|
+
import { type ApiConfig, SignalClient, apiConfig } from "../core/signal";
|
|
3
|
+
import { type AutoJoinConfig } from "../core/types";
|
|
4
|
+
import { LiveKitService } from "../livekit";
|
|
5
|
+
import { rtcStore } from "../state/store";
|
|
6
|
+
import { type LogLevel, setGlobalLoggerOptions } from "../utils/logger";
|
|
7
|
+
import { type CallActions, createCallActions } from "./call-actions";
|
|
8
|
+
|
|
9
|
+
export interface SdkBuildOptions {
|
|
10
|
+
appId: string;
|
|
11
|
+
signalHost: string;
|
|
12
|
+
authProvider: () => string | null;
|
|
13
|
+
|
|
14
|
+
// Logging configuration
|
|
15
|
+
logLevel?: LogLevel;
|
|
16
|
+
enableDebug?: boolean;
|
|
17
|
+
|
|
18
|
+
// Custom log callback
|
|
19
|
+
log?: (level: LogLevel, message: string, meta?: any) => void;
|
|
20
|
+
|
|
21
|
+
// Auto-join configuration
|
|
22
|
+
autoJoin?: Partial<AutoJoinConfig>;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RtcSdk extends CallActions {
|
|
26
|
+
store: typeof rtcStore;
|
|
27
|
+
auth: AuthManager;
|
|
28
|
+
socket: SocketManager;
|
|
29
|
+
signal: SignalClient;
|
|
30
|
+
livekit: LiveKitService;
|
|
31
|
+
autoJoinConfig: AutoJoinConfig;
|
|
32
|
+
cleanup: () => void;
|
|
33
|
+
|
|
34
|
+
configureApi: (config: ApiConfig) => void;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
// Default auto-join configuration
|
|
38
|
+
const DEFAULT_AUTO_JOIN_CONFIG: AutoJoinConfig = {
|
|
39
|
+
enabled: true, // Everyone auto-joins by default
|
|
40
|
+
retryOnFailure: true,
|
|
41
|
+
maxRetries: 2,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export function buildSdk(opts: SdkBuildOptions): RtcSdk {
|
|
45
|
+
// Configure global logging system
|
|
46
|
+
const loggerOptions: any = {};
|
|
47
|
+
if (opts.logLevel !== undefined) {
|
|
48
|
+
loggerOptions.level = opts.logLevel;
|
|
49
|
+
}
|
|
50
|
+
if (opts.enableDebug !== undefined) {
|
|
51
|
+
loggerOptions.enableDebug = opts.enableDebug;
|
|
52
|
+
}
|
|
53
|
+
if (opts.log !== undefined) {
|
|
54
|
+
loggerOptions.customLogger = opts.log;
|
|
55
|
+
}
|
|
56
|
+
setGlobalLoggerOptions(loggerOptions);
|
|
57
|
+
|
|
58
|
+
// Merge auto-join configuration with defaults
|
|
59
|
+
const autoJoinConfig: AutoJoinConfig = {
|
|
60
|
+
...DEFAULT_AUTO_JOIN_CONFIG,
|
|
61
|
+
...opts.autoJoin,
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
// Initialize core managers
|
|
65
|
+
const auth = new AuthManager(opts.authProvider);
|
|
66
|
+
const socket = SocketManager.getInstance();
|
|
67
|
+
const signal = new SignalClient({
|
|
68
|
+
baseUrl: opts.signalHost,
|
|
69
|
+
appId: opts.appId,
|
|
70
|
+
authManager: auth,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
const livekit = new LiveKitService({
|
|
74
|
+
log: opts.log,
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
const callActions = createCallActions(signal, auth, livekit);
|
|
78
|
+
|
|
79
|
+
// Socket now handles events directly - no event bridge needed
|
|
80
|
+
|
|
81
|
+
const cleanup = () => {
|
|
82
|
+
socket.destroy();
|
|
83
|
+
livekit.destroy();
|
|
84
|
+
rtcStore.getState().reset();
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
return {
|
|
89
|
+
store: rtcStore,
|
|
90
|
+
auth,
|
|
91
|
+
socket,
|
|
92
|
+
signal,
|
|
93
|
+
livekit,
|
|
94
|
+
autoJoinConfig,
|
|
95
|
+
...callActions,
|
|
96
|
+
cleanup,
|
|
97
|
+
|
|
98
|
+
// API configuration
|
|
99
|
+
configureApi: (config: ApiConfig) => {
|
|
100
|
+
apiConfig.configure(config);
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
};
|
|
104
|
+
}
|