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.
Files changed (140) hide show
  1. package/.azure-pipelines/publish-public.yml +37 -0
  2. package/.azure-pipelines/publish.yml +39 -0
  3. package/.changeset/README.md +8 -0
  4. package/.changeset/config.json +11 -0
  5. package/AUTO_JOIN_GUIDE.md +411 -0
  6. package/README.md +215 -0
  7. package/Screenshot 2025-09-24 at 14.34.48.png +0 -0
  8. package/Screenshot 2025-10-04 at 12.58.54.png +0 -0
  9. package/biome.json +48 -0
  10. package/examples/demo/.env.example +19 -0
  11. package/examples/demo/CHANGELOG.md +22 -0
  12. package/examples/demo/README.md +72 -0
  13. package/examples/demo/eslint.config.js +23 -0
  14. package/examples/demo/index.html +13 -0
  15. package/examples/demo/package.json +34 -0
  16. package/examples/demo/pnpm-lock.yaml +2098 -0
  17. package/examples/demo/pnpm-workspace.yaml +1 -0
  18. package/examples/demo/public/vite.svg +1 -0
  19. package/examples/demo/src/App.css +52 -0
  20. package/examples/demo/src/App.tsx +176 -0
  21. package/examples/demo/src/assets/react.svg +1 -0
  22. package/examples/demo/src/components/auth/LoginForm.css +144 -0
  23. package/examples/demo/src/components/auth/LoginForm.tsx +80 -0
  24. package/examples/demo/src/components/calling/AutoJoinSettings.tsx +213 -0
  25. package/examples/demo/src/components/calling/AutoJoinStatus.tsx +72 -0
  26. package/examples/demo/src/components/calling/CallInitiator.css +258 -0
  27. package/examples/demo/src/components/calling/CallInitiator.tsx +142 -0
  28. package/examples/demo/src/components/calling/CallNotifications.css +119 -0
  29. package/examples/demo/src/components/calling/CallNotifications.tsx +108 -0
  30. package/examples/demo/src/components/calling/IncomingCallModal.css +192 -0
  31. package/examples/demo/src/components/calling/IncomingCallModal.tsx +78 -0
  32. package/examples/demo/src/components/calling/MinimizedCall.css +156 -0
  33. package/examples/demo/src/components/calling/MinimizedCall.tsx +78 -0
  34. package/examples/demo/src/components/conference/ConferenceHeader.css +265 -0
  35. package/examples/demo/src/components/conference/ConferenceHeader.tsx +78 -0
  36. package/examples/demo/src/components/conference/EnhancedControlBar.css +356 -0
  37. package/examples/demo/src/components/conference/EnhancedControlBar.tsx +262 -0
  38. package/examples/demo/src/components/conference/PaginationControls.css +67 -0
  39. package/examples/demo/src/components/conference/PaginationControls.tsx +64 -0
  40. package/examples/demo/src/components/conference/ParticipantGrid.css +153 -0
  41. package/examples/demo/src/components/conference/ParticipantGrid.tsx +87 -0
  42. package/examples/demo/src/components/conference/ParticipantTile.css +210 -0
  43. package/examples/demo/src/components/conference/ParticipantTile.tsx +114 -0
  44. package/examples/demo/src/components/conference/VideoConference.css +214 -0
  45. package/examples/demo/src/components/conference/VideoConference.tsx +93 -0
  46. package/examples/demo/src/contexts/AuthContext.tsx +105 -0
  47. package/examples/demo/src/hooks/useAuth.ts +5 -0
  48. package/examples/demo/src/hooks/useCallTimer.ts +42 -0
  49. package/examples/demo/src/index.css +68 -0
  50. package/examples/demo/src/main.tsx +10 -0
  51. package/examples/demo/src/services/auth.service.ts +153 -0
  52. package/examples/demo/src/types/auth.types.ts +31 -0
  53. package/examples/demo/tsconfig.app.json +28 -0
  54. package/examples/demo/tsconfig.json +7 -0
  55. package/examples/demo/tsconfig.node.json +26 -0
  56. package/examples/demo/vite.config.ts +15 -0
  57. package/images/callpad-without-ai.png +0 -0
  58. package/package.json +28 -0
  59. package/packages/sdk/CHANGELOG.md +33 -0
  60. package/packages/sdk/LICENSE +21 -0
  61. package/packages/sdk/README.md +97 -0
  62. package/packages/sdk/documentation.md +1132 -0
  63. package/packages/sdk/openapi-ts.config.ts +7 -0
  64. package/packages/sdk/package.json +88 -0
  65. package/packages/sdk/src/core/auth.manager.ts +52 -0
  66. package/packages/sdk/src/core/events/event-bus.ts +301 -0
  67. package/packages/sdk/src/core/events/index.ts +8 -0
  68. package/packages/sdk/src/core/events/types.ts +165 -0
  69. package/packages/sdk/src/core/index.ts +3 -0
  70. package/packages/sdk/src/core/signal/api.config.ts +49 -0
  71. package/packages/sdk/src/core/signal/index.ts +16 -0
  72. package/packages/sdk/src/core/signal/signal.client.ts +101 -0
  73. package/packages/sdk/src/core/signal/types.ts +110 -0
  74. package/packages/sdk/src/core/socketio/handlers/base.handler.ts +212 -0
  75. package/packages/sdk/src/core/socketio/handlers/call-accepted.handler.ts +34 -0
  76. package/packages/sdk/src/core/socketio/handlers/call-canceled.handler.ts +34 -0
  77. package/packages/sdk/src/core/socketio/handlers/call-declined.handler.ts +29 -0
  78. package/packages/sdk/src/core/socketio/handlers/call-ended.handler.ts +40 -0
  79. package/packages/sdk/src/core/socketio/handlers/call-incoming.handler.ts +72 -0
  80. package/packages/sdk/src/core/socketio/handlers/call-join-info.handler.ts +181 -0
  81. package/packages/sdk/src/core/socketio/handlers/call-participant-joined.handler.ts +42 -0
  82. package/packages/sdk/src/core/socketio/handlers/call-participant-joining.handler.ts +42 -0
  83. package/packages/sdk/src/core/socketio/handlers/call-timeout.handler.ts +31 -0
  84. package/packages/sdk/src/core/socketio/handlers/handler.registry.ts +62 -0
  85. package/packages/sdk/src/core/socketio/handlers/index.ts +21 -0
  86. package/packages/sdk/src/core/socketio/handlers/participant-left.handler.ts +37 -0
  87. package/packages/sdk/src/core/socketio/handlers/schema.ts +130 -0
  88. package/packages/sdk/src/core/socketio/index.ts +5 -0
  89. package/packages/sdk/src/core/socketio/socket.manager.ts +187 -0
  90. package/packages/sdk/src/core/socketio/types.ts +14 -0
  91. package/packages/sdk/src/core/types.ts +23 -0
  92. package/packages/sdk/src/generated/api/core/ApiError.ts +21 -0
  93. package/packages/sdk/src/generated/api/core/ApiRequestOptions.ts +13 -0
  94. package/packages/sdk/src/generated/api/core/ApiResult.ts +7 -0
  95. package/packages/sdk/src/generated/api/core/CancelablePromise.ts +126 -0
  96. package/packages/sdk/src/generated/api/core/OpenAPI.ts +55 -0
  97. package/packages/sdk/src/generated/api/core/request.ts +339 -0
  98. package/packages/sdk/src/generated/api/index.ts +5 -0
  99. package/packages/sdk/src/generated/api/models.ts +219 -0
  100. package/packages/sdk/src/generated/api/services.ts +225 -0
  101. package/packages/sdk/src/hooks/index.ts +21 -0
  102. package/packages/sdk/src/hooks/useAutoJoin.ts +66 -0
  103. package/packages/sdk/src/hooks/useCallActions.ts +28 -0
  104. package/packages/sdk/src/hooks/useCallQuality.ts +416 -0
  105. package/packages/sdk/src/hooks/useCallState.ts +23 -0
  106. package/packages/sdk/src/hooks/useConnection.ts +15 -0
  107. package/packages/sdk/src/hooks/useDevices.ts +296 -0
  108. package/packages/sdk/src/hooks/useErrorRecovery.ts +299 -0
  109. package/packages/sdk/src/hooks/useErrors.ts +84 -0
  110. package/packages/sdk/src/hooks/useEvent.ts +188 -0
  111. package/packages/sdk/src/hooks/useMediaControls.ts +215 -0
  112. package/packages/sdk/src/hooks/useParticipantStatus.ts +318 -0
  113. package/packages/sdk/src/hooks/useParticipants.ts +111 -0
  114. package/packages/sdk/src/index.ts +66 -0
  115. package/packages/sdk/src/livekit/constants.ts +76 -0
  116. package/packages/sdk/src/livekit/device.manager.ts +172 -0
  117. package/packages/sdk/src/livekit/error-classifier.ts +155 -0
  118. package/packages/sdk/src/livekit/events/eventBridge.ts +371 -0
  119. package/packages/sdk/src/livekit/events/trackRegistry.ts +114 -0
  120. package/packages/sdk/src/livekit/index.ts +49 -0
  121. package/packages/sdk/src/livekit/livekit.service.ts +110 -0
  122. package/packages/sdk/src/livekit/media.controls.ts +315 -0
  123. package/packages/sdk/src/livekit/room.manager.ts +79 -0
  124. package/packages/sdk/src/livekit/track.utils.ts +230 -0
  125. package/packages/sdk/src/livekit/types.ts +135 -0
  126. package/packages/sdk/src/provider/RtcProvider.tsx +78 -0
  127. package/packages/sdk/src/services/call-actions.ts +260 -0
  128. package/packages/sdk/src/services/error-recovery.ts +461 -0
  129. package/packages/sdk/src/services/index.ts +2 -0
  130. package/packages/sdk/src/services/sdk-builder.ts +104 -0
  131. package/packages/sdk/src/state/errors.ts +163 -0
  132. package/packages/sdk/src/state/selectors.ts +28 -0
  133. package/packages/sdk/src/state/store.ts +36 -0
  134. package/packages/sdk/src/state/types.ts +151 -0
  135. package/packages/sdk/src/utils/logger.ts +183 -0
  136. package/packages/sdk/tsconfig.json +49 -0
  137. package/packages/sdk/tsup.config.ts +51 -0
  138. package/pnpm-workspace.yaml +4 -0
  139. package/tsconfig.base.json +19 -0
  140. 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
+ }