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,101 @@
|
|
|
1
|
+
import { CallsService } from "../../generated/api";
|
|
2
|
+
import type { CallsData } from "../../generated/api/models";
|
|
3
|
+
import { createLogger } from "../../utils/logger";
|
|
4
|
+
import type {
|
|
5
|
+
CallActionResponse,
|
|
6
|
+
CallResponse,
|
|
7
|
+
InitiateCallParams,
|
|
8
|
+
SignalClientConfig,
|
|
9
|
+
} from "./types";
|
|
10
|
+
|
|
11
|
+
export class SignalClient {
|
|
12
|
+
private config: SignalClientConfig;
|
|
13
|
+
private logger = createLogger("signal");
|
|
14
|
+
|
|
15
|
+
constructor(config: SignalClientConfig) {
|
|
16
|
+
this.config = config;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async initiate(params: InitiateCallParams): Promise<CallResponse> {
|
|
20
|
+
try {
|
|
21
|
+
return await CallsService.postSignalCalls({
|
|
22
|
+
appId: this.config.appId,
|
|
23
|
+
requestBody: {
|
|
24
|
+
mode: params.mode || "AUDIO",
|
|
25
|
+
participants: params.invitees.map((userId) => ({ userId })),
|
|
26
|
+
},
|
|
27
|
+
});
|
|
28
|
+
} catch (error) {
|
|
29
|
+
this.handleApiError("initiate", error);
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async accept(callId: string): Promise<CallActionResponse> {
|
|
35
|
+
try {
|
|
36
|
+
const response = await CallsService.postSignalCallsByCallIdAccept({
|
|
37
|
+
callId,
|
|
38
|
+
appId: this.config.appId,
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
...response,
|
|
42
|
+
callId,
|
|
43
|
+
state: "ACTIVE" as const,
|
|
44
|
+
message: "Call accepted",
|
|
45
|
+
};
|
|
46
|
+
} catch (error) {
|
|
47
|
+
this.handleApiError("accept", error);
|
|
48
|
+
throw error;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async decline(callId: string): Promise<CallActionResponse> {
|
|
53
|
+
try {
|
|
54
|
+
const response = await CallsService.postSignalCallsByCallIdDecline({
|
|
55
|
+
callId,
|
|
56
|
+
appId: this.config.appId,
|
|
57
|
+
});
|
|
58
|
+
return {
|
|
59
|
+
...response,
|
|
60
|
+
callId,
|
|
61
|
+
state: "ENDED" as const,
|
|
62
|
+
message: "Call declined",
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
this.handleApiError("decline", error);
|
|
66
|
+
throw error;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async leave(callId: string): Promise<CallActionResponse> {
|
|
71
|
+
try {
|
|
72
|
+
const response = await CallsService.postSignalCallsByCallIdLeave({
|
|
73
|
+
callId,
|
|
74
|
+
appId: this.config.appId,
|
|
75
|
+
});
|
|
76
|
+
return {
|
|
77
|
+
...response,
|
|
78
|
+
callId,
|
|
79
|
+
state: "ENDED" as const,
|
|
80
|
+
message: "Left call",
|
|
81
|
+
};
|
|
82
|
+
} catch (error) {
|
|
83
|
+
this.handleApiError("leave", error);
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
private handleApiError(operation: string, error: any): void {
|
|
89
|
+
const errorMessage =
|
|
90
|
+
error?.body?.message || error?.message || "Unknown error";
|
|
91
|
+
const errorCode = error?.status || error?.code || "UNKNOWN";
|
|
92
|
+
|
|
93
|
+
// Log error for debugging - real-time error handling happens via Socket.IO
|
|
94
|
+
this.logger.error(`Signal API error during ${operation}`, {
|
|
95
|
+
operation,
|
|
96
|
+
errorCode,
|
|
97
|
+
errorMessage,
|
|
98
|
+
error,
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { AuthManager } from "../auth.manager";
|
|
2
|
+
|
|
3
|
+
export interface SignalClientConfig {
|
|
4
|
+
baseUrl: string;
|
|
5
|
+
appId: string;
|
|
6
|
+
authManager: AuthManager;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface CallInfo {
|
|
10
|
+
id: string;
|
|
11
|
+
mode: "AUDIO" | "VIDEO";
|
|
12
|
+
state: "RINGING" | "ACTIVE" | "ON_HOLD" | "ENDED";
|
|
13
|
+
callerId: string;
|
|
14
|
+
roomName: string;
|
|
15
|
+
lkRoomSid?: string;
|
|
16
|
+
createdAt: string;
|
|
17
|
+
startedAt?: string;
|
|
18
|
+
endedAt?: string;
|
|
19
|
+
participants: CallParticipant[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface CallParticipant {
|
|
23
|
+
id: string;
|
|
24
|
+
userId: string;
|
|
25
|
+
joinedAt?: string;
|
|
26
|
+
leftAt?: string;
|
|
27
|
+
lkIdentity?: string;
|
|
28
|
+
lkParticipantSid?: string;
|
|
29
|
+
createdAt: string;
|
|
30
|
+
updatedAt: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface LiveKitJoinInfo {
|
|
34
|
+
token: string;
|
|
35
|
+
roomName: string;
|
|
36
|
+
callId: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export interface IncomingCallEvent {
|
|
40
|
+
callId: string;
|
|
41
|
+
fromUserId: string;
|
|
42
|
+
fromUserName: string;
|
|
43
|
+
fromUserAvatar?: string;
|
|
44
|
+
type: "VIDEO" | "AUDIO";
|
|
45
|
+
timestamp: number;
|
|
46
|
+
participants?: string[];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export type CallState = "RINGING" | "ACTIVE" | "ON_HOLD" | "ENDED";
|
|
50
|
+
export type CallMode = "AUDIO" | "VIDEO";
|
|
51
|
+
export type EndReason = "ENDED" | "TIMEOUT" | "ERROR" | "CANCELLED";
|
|
52
|
+
|
|
53
|
+
export class SignalError extends Error {
|
|
54
|
+
constructor(
|
|
55
|
+
message: string,
|
|
56
|
+
public code: string,
|
|
57
|
+
public statusCode?: number
|
|
58
|
+
) {
|
|
59
|
+
super(message);
|
|
60
|
+
this.name = "SignalError";
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export interface InitiateCallParams {
|
|
65
|
+
invitees: string[];
|
|
66
|
+
mode?: "AUDIO" | "VIDEO";
|
|
67
|
+
metadata?: any;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface ApiConfig {
|
|
71
|
+
baseUrl: string;
|
|
72
|
+
token?: string | (() => Promise<string> | string);
|
|
73
|
+
credentials?: "include" | "omit" | "same-origin";
|
|
74
|
+
withCredentials?: boolean;
|
|
75
|
+
headers?: Record<string, string>;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Backend API Response Types
|
|
79
|
+
export interface CallResponse {
|
|
80
|
+
id: string;
|
|
81
|
+
mode: "AUDIO" | "VIDEO";
|
|
82
|
+
state: "RINGING" | "ACTIVE" | "ON_HOLD" | "ENDED";
|
|
83
|
+
callerId: string;
|
|
84
|
+
roomName: string;
|
|
85
|
+
lkRoomSid?: string;
|
|
86
|
+
createdAt: string;
|
|
87
|
+
startedAt?: string;
|
|
88
|
+
endedAt?: string;
|
|
89
|
+
participants: Array<{
|
|
90
|
+
id: string;
|
|
91
|
+
userId: string;
|
|
92
|
+
joinedAt?: string;
|
|
93
|
+
leftAt?: string;
|
|
94
|
+
lkIdentity?: string;
|
|
95
|
+
lkParticipantSid?: string;
|
|
96
|
+
createdAt: string;
|
|
97
|
+
updatedAt: string;
|
|
98
|
+
}>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export interface CallActionResponse {
|
|
102
|
+
callId: string;
|
|
103
|
+
state: "RINGING" | "ACTIVE" | "ON_HOLD" | "ENDED";
|
|
104
|
+
message: string;
|
|
105
|
+
token?: string;
|
|
106
|
+
roomName?: string;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
// Legacy alias for backward compatibility
|
|
110
|
+
export type LeaveCallResponse = CallActionResponse;
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
import type { ZodSchema } from "zod";
|
|
2
|
+
import { pushSocketValidationError } from "../../../state/errors";
|
|
3
|
+
import { rtcStore } from "../../../state/store";
|
|
4
|
+
import { createLogger } from "../../../utils/logger";
|
|
5
|
+
import type { CallpadLogger } from "../../../utils/logger";
|
|
6
|
+
import type { AutoJoinConfig } from "../../types";
|
|
7
|
+
import type { AuthManager } from "../../auth.manager";
|
|
8
|
+
|
|
9
|
+
export interface SocketHandlerOptions {
|
|
10
|
+
livekit?: any;
|
|
11
|
+
autoJoinConfig?: AutoJoinConfig | null;
|
|
12
|
+
authManager?: AuthManager;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export abstract class BaseSocketHandler<T = any> {
|
|
16
|
+
protected abstract readonly eventName: string;
|
|
17
|
+
protected abstract readonly schema: ZodSchema<T>;
|
|
18
|
+
private _logger?: CallpadLogger;
|
|
19
|
+
|
|
20
|
+
constructor(protected readonly options: SocketHandlerOptions = {}) {}
|
|
21
|
+
|
|
22
|
+
protected get logger(): CallpadLogger {
|
|
23
|
+
if (!this._logger) {
|
|
24
|
+
this._logger = createLogger(`socketio:${this.eventName}`);
|
|
25
|
+
}
|
|
26
|
+
return this._logger;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
protected get authManager(): AuthManager | undefined {
|
|
30
|
+
return this.options.authManager;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async handleRaw(rawData: unknown): Promise<void> {
|
|
34
|
+
this.logger.info(`${this.eventName} received`, rawData);
|
|
35
|
+
|
|
36
|
+
const result = this.schema.safeParse(rawData);
|
|
37
|
+
if (!result.success) {
|
|
38
|
+
this.logger.error(
|
|
39
|
+
`${this.eventName} validation failed`,
|
|
40
|
+
result.error.issues
|
|
41
|
+
);
|
|
42
|
+
pushSocketValidationError(
|
|
43
|
+
this.eventName,
|
|
44
|
+
result.error.issues,
|
|
45
|
+
rawData,
|
|
46
|
+
(level, message, meta) => {
|
|
47
|
+
switch (level) {
|
|
48
|
+
case "debug":
|
|
49
|
+
this.logger.debug(message, meta);
|
|
50
|
+
break;
|
|
51
|
+
case "info":
|
|
52
|
+
this.logger.info(message, meta);
|
|
53
|
+
break;
|
|
54
|
+
case "warn":
|
|
55
|
+
this.logger.warn(message, meta);
|
|
56
|
+
break;
|
|
57
|
+
case "error":
|
|
58
|
+
this.logger.error(message, meta);
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
try {
|
|
67
|
+
await this.handle(result.data);
|
|
68
|
+
this.logger.debug(`${this.eventName} handled successfully`);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
this.logger.error(`${this.eventName} handler error`, error);
|
|
71
|
+
throw error;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
protected abstract handle(data: T): Promise<void> | void;
|
|
76
|
+
|
|
77
|
+
protected updateStore(updater: (state: any) => void): void {
|
|
78
|
+
rtcStore.getState().patch(updater);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
protected get livekit() {
|
|
82
|
+
return this.options.livekit;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
protected get autoJoinConfig() {
|
|
86
|
+
return this.options.autoJoinConfig;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Retry logic with exponential backoff for auto-join operations
|
|
91
|
+
*/
|
|
92
|
+
protected async retryAutoJoin(
|
|
93
|
+
callId: string,
|
|
94
|
+
token: string,
|
|
95
|
+
url: string,
|
|
96
|
+
attempt = 1
|
|
97
|
+
): Promise<boolean> {
|
|
98
|
+
const maxAttempts = this.autoJoinConfig?.maxRetries || 2;
|
|
99
|
+
|
|
100
|
+
// Initialize auto-join state on first attempt
|
|
101
|
+
if (attempt === 1) {
|
|
102
|
+
this.updateStore((state) => {
|
|
103
|
+
state.autoJoin = {
|
|
104
|
+
status: "pending",
|
|
105
|
+
attempt: 1,
|
|
106
|
+
maxAttempts,
|
|
107
|
+
startedAt: Date.now(),
|
|
108
|
+
};
|
|
109
|
+
});
|
|
110
|
+
} else {
|
|
111
|
+
// Update state for retry attempts
|
|
112
|
+
this.updateStore((state) => {
|
|
113
|
+
state.autoJoin.status = "retrying";
|
|
114
|
+
state.autoJoin.attempt = attempt;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (attempt > maxAttempts) {
|
|
119
|
+
this.logger.warn("Max retry attempts reached for auto-join", {
|
|
120
|
+
callId,
|
|
121
|
+
maxAttempts,
|
|
122
|
+
finalAttempt: attempt - 1,
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
// Mark as failed
|
|
126
|
+
this.updateStore((state) => {
|
|
127
|
+
state.autoJoin.status = "failed";
|
|
128
|
+
state.autoJoin.completedAt = Date.now();
|
|
129
|
+
state.autoJoin.lastError = "Max retry attempts reached";
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
return false;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
this.logger.info("Attempting auto-join", {
|
|
137
|
+
callId,
|
|
138
|
+
attempt,
|
|
139
|
+
maxAttempts,
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
await this.livekit?.joinRoom(token, url);
|
|
143
|
+
|
|
144
|
+
this.logger.info("Auto-join successful", {
|
|
145
|
+
callId,
|
|
146
|
+
attempt,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// Mark as succeeded
|
|
150
|
+
this.updateStore((state) => {
|
|
151
|
+
state.autoJoin.status = "succeeded";
|
|
152
|
+
state.autoJoin.completedAt = Date.now();
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
return true;
|
|
156
|
+
} catch (error) {
|
|
157
|
+
const errorMessage = error instanceof Error ? error.message : "Unknown error";
|
|
158
|
+
|
|
159
|
+
this.logger.warn("Auto-join attempt failed", {
|
|
160
|
+
callId,
|
|
161
|
+
attempt,
|
|
162
|
+
maxAttempts,
|
|
163
|
+
error: errorMessage,
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
// Update state with error
|
|
167
|
+
this.updateStore((state) => {
|
|
168
|
+
state.autoJoin.lastError = errorMessage;
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
if (attempt < maxAttempts) {
|
|
172
|
+
// Exponential backoff: 1s, 2s, 4s, etc.
|
|
173
|
+
// biome-ignore lint/style/useExponentiationOperator: <explanation>
|
|
174
|
+
const delayMs = Math.pow(2, attempt - 1) * 1000;
|
|
175
|
+
this.logger.debug("Retrying auto-join after delay", {
|
|
176
|
+
callId,
|
|
177
|
+
nextAttempt: attempt + 1,
|
|
178
|
+
delayMs,
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
await new Promise(resolve => setTimeout(resolve, delayMs));
|
|
182
|
+
return this.retryAutoJoin(callId, token, url, attempt + 1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Mark as failed after all attempts
|
|
186
|
+
this.updateStore((state) => {
|
|
187
|
+
state.autoJoin.status = "failed";
|
|
188
|
+
state.autoJoin.completedAt = Date.now();
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
return false;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Determines if an error is retryable
|
|
197
|
+
*/
|
|
198
|
+
protected isRetryableError(error: any): boolean {
|
|
199
|
+
if (!error) return false;
|
|
200
|
+
|
|
201
|
+
const errorMessage = error.message?.toLowerCase() || "";
|
|
202
|
+
const retryableErrors = [
|
|
203
|
+
"network",
|
|
204
|
+
"timeout",
|
|
205
|
+
"connection",
|
|
206
|
+
"websocket",
|
|
207
|
+
"transport",
|
|
208
|
+
];
|
|
209
|
+
|
|
210
|
+
return retryableErrors.some(keyword => errorMessage.includes(keyword));
|
|
211
|
+
}
|
|
212
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { pushStaleEventError, pushLiveKitConnectError } from "../../../state/errors";
|
|
2
|
+
import { rtcStore } from "../../../state/store";
|
|
3
|
+
import { SdkEventType, eventBus } from "../../events";
|
|
4
|
+
import { BaseSocketHandler } from "./base.handler";
|
|
5
|
+
import { callParticipantAcceptedSchema } from "./schema";
|
|
6
|
+
import type { CallParticipantAcceptedEvent } from "./schema";
|
|
7
|
+
|
|
8
|
+
export class CallParticipantAcceptedHandler extends BaseSocketHandler<CallParticipantAcceptedEvent> {
|
|
9
|
+
protected readonly eventName = "call.participant-accepted";
|
|
10
|
+
protected readonly schema = callParticipantAcceptedSchema;
|
|
11
|
+
|
|
12
|
+
protected async handle(data: CallParticipantAcceptedEvent): Promise<void> {
|
|
13
|
+
const currentState = rtcStore.getState();
|
|
14
|
+
const currentUserId = this.authManager?.getCurrentUserId();
|
|
15
|
+
|
|
16
|
+
if (currentState.session.id !== data.callId) {
|
|
17
|
+
pushStaleEventError("call.participant-accepted", "callId mismatch", {
|
|
18
|
+
eventCallId: data.callId,
|
|
19
|
+
sessionCallId: currentState.session.id,
|
|
20
|
+
});
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
this.updateStore((state) => {
|
|
25
|
+
state.session.status = "ACCEPTED";
|
|
26
|
+
|
|
27
|
+
const participant = state.room.participants[data.participantId];
|
|
28
|
+
if (participant) {
|
|
29
|
+
participant.callState = "RINGING";
|
|
30
|
+
participant.joinedAt = data.acceptedAt ? new Date(data.acceptedAt).getTime() : Date.now();
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { Participant } from "../../../state/types";
|
|
2
|
+
import { BaseSocketHandler } from "./base.handler";
|
|
3
|
+
import { callCanceledSchema } from "./schema";
|
|
4
|
+
import type { CallCanceledEvent } from "./schema";
|
|
5
|
+
|
|
6
|
+
export class CallCanceledHandler extends BaseSocketHandler<CallCanceledEvent> {
|
|
7
|
+
protected readonly eventName = "call.canceled";
|
|
8
|
+
protected readonly schema = callCanceledSchema;
|
|
9
|
+
|
|
10
|
+
protected handle(data: CallCanceledEvent): void {
|
|
11
|
+
const reason = data.reason || "canceled";
|
|
12
|
+
this.logger.info(`Call canceled: ${reason}`, {
|
|
13
|
+
callId: data.callId,
|
|
14
|
+
by: data.by?.id,
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
this.updateStore((state) => {
|
|
18
|
+
if (state.session.id === data.callId) {
|
|
19
|
+
state.session.status = "ENDED";
|
|
20
|
+
state.incomingCall = undefined;
|
|
21
|
+
|
|
22
|
+
// Clear all participants
|
|
23
|
+
for (const participant of Object.values(
|
|
24
|
+
state.room.participants
|
|
25
|
+
) as Participant[]) {
|
|
26
|
+
participant.callState = "LEFT";
|
|
27
|
+
if (!participant.leftAt) {
|
|
28
|
+
participant.leftAt = data.timestamp || Date.now();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { SdkEventType, eventBus } from "../../events";
|
|
2
|
+
import { BaseSocketHandler } from "./base.handler";
|
|
3
|
+
import { callParticipantDeclinedSchema } from "./schema";
|
|
4
|
+
import type { CallParticipantDeclinedEvent } from "./schema";
|
|
5
|
+
|
|
6
|
+
export class CallParticipantDeclinedHandler extends BaseSocketHandler<CallParticipantDeclinedEvent> {
|
|
7
|
+
protected readonly eventName = "call.participant-declined";
|
|
8
|
+
protected readonly schema = callParticipantDeclinedSchema;
|
|
9
|
+
|
|
10
|
+
protected handle(data: CallParticipantDeclinedEvent): void {
|
|
11
|
+
this.updateStore((state) => {
|
|
12
|
+
if (state.session.id === data.callId) {
|
|
13
|
+
state.session.status = "IDLE";
|
|
14
|
+
state.incomingCall = undefined;
|
|
15
|
+
}
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
eventBus.emit(
|
|
19
|
+
SdkEventType.CALL_DECLINED,
|
|
20
|
+
{
|
|
21
|
+
callId: data.callId,
|
|
22
|
+
participantId: data.participantId,
|
|
23
|
+
reason: "declined",
|
|
24
|
+
timestamp: Date.now(),
|
|
25
|
+
},
|
|
26
|
+
"socket"
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { pushStaleEventError } from "../../../state/errors";
|
|
2
|
+
import { BaseSocketHandler } from "./base.handler";
|
|
3
|
+
import { callEndedSchema } from "./schema";
|
|
4
|
+
import type { CallEndedEvent } from "./schema";
|
|
5
|
+
|
|
6
|
+
export class CallEndedHandler extends BaseSocketHandler<CallEndedEvent> {
|
|
7
|
+
protected readonly eventName = "call.ended";
|
|
8
|
+
protected readonly schema = callEndedSchema;
|
|
9
|
+
|
|
10
|
+
protected handle(data: CallEndedEvent): void {
|
|
11
|
+
this.updateStore((state) => {
|
|
12
|
+
if (state.session.id !== data.callId) {
|
|
13
|
+
pushStaleEventError("call.ended", "callId mismatch", {
|
|
14
|
+
eventCallId: data.callId,
|
|
15
|
+
sessionCallId: state.session.id,
|
|
16
|
+
});
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
state.session.status = "ENDED";
|
|
21
|
+
state.incomingCall = undefined;
|
|
22
|
+
|
|
23
|
+
for (const id of Object.keys(state.room.participants)) {
|
|
24
|
+
const participant = state.room.participants[id];
|
|
25
|
+
if (participant) {
|
|
26
|
+
participant.callState = "LEFT";
|
|
27
|
+
if (!participant.leftAt) {
|
|
28
|
+
participant.leftAt = Date.now();
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
if (this.livekit) {
|
|
35
|
+
this.livekit.disconnect().catch((error: any) => {
|
|
36
|
+
this.logger.error("Error disconnecting from LiveKit", { error });
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { BaseSocketHandler } from "./base.handler";
|
|
2
|
+
import { callIncomingSchema } from "./schema";
|
|
3
|
+
import type { CallIncomingEvent } from "./schema";
|
|
4
|
+
|
|
5
|
+
export class CallIncomingHandler extends BaseSocketHandler<CallIncomingEvent> {
|
|
6
|
+
protected readonly eventName = "call.incoming";
|
|
7
|
+
protected readonly schema = callIncomingSchema;
|
|
8
|
+
|
|
9
|
+
protected handle(data: CallIncomingEvent): void {
|
|
10
|
+
// Find caller from participants array
|
|
11
|
+
const caller = data.participants.find(
|
|
12
|
+
(p) => p.role === "CALLER" || p.role === "HOST"
|
|
13
|
+
);
|
|
14
|
+
|
|
15
|
+
if (!caller) {
|
|
16
|
+
this.logger.error("No caller found in participants", data);
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
this.updateStore((state) => {
|
|
21
|
+
state.incomingCall = {
|
|
22
|
+
callId: data.callId,
|
|
23
|
+
caller: {
|
|
24
|
+
id: caller.id,
|
|
25
|
+
name:
|
|
26
|
+
[caller.firstName, caller.lastName].filter(Boolean).join(" ") ||
|
|
27
|
+
caller.username ||
|
|
28
|
+
`Guest ${caller.id}`,
|
|
29
|
+
avatarUrl: caller.profilePhoto,
|
|
30
|
+
},
|
|
31
|
+
type: data.type,
|
|
32
|
+
timestamp: data.timestamp,
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
state.session = {
|
|
36
|
+
id: data.callId,
|
|
37
|
+
status: "RINGING",
|
|
38
|
+
mode: data.type,
|
|
39
|
+
// Identity context: incoming call, I haven't accepted yet
|
|
40
|
+
initiatedByMe: false,
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// Create unified participants from participants array
|
|
44
|
+
for (const participant of data.participants) {
|
|
45
|
+
const callState = participant.role === "CALLER" || participant.role === "HOST" ? "JOINED" : "INVITED";
|
|
46
|
+
|
|
47
|
+
state.room.participants[participant.id] = {
|
|
48
|
+
id: participant.id,
|
|
49
|
+
firstName: participant.firstName || undefined,
|
|
50
|
+
lastName: participant.lastName || undefined,
|
|
51
|
+
avatarUrl: participant.profilePhoto || undefined,
|
|
52
|
+
role: participant.role || "MEMBER",
|
|
53
|
+
callState,
|
|
54
|
+
audioEnabled: false,
|
|
55
|
+
videoEnabled: false,
|
|
56
|
+
isSpeaking: false,
|
|
57
|
+
joinedAt:
|
|
58
|
+
participant.role === "CALLER" || participant.role === "HOST"
|
|
59
|
+
? data.timestamp
|
|
60
|
+
: undefined,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
this.logger.debug("Created participant during incoming call", {
|
|
64
|
+
participantId: participant.id,
|
|
65
|
+
role: participant.role || "MEMBER",
|
|
66
|
+
callState,
|
|
67
|
+
callId: data.callId,
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
}
|