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,181 @@
1
+ import {
2
+ pushLiveKitConnectError,
3
+ pushStaleEventError,
4
+ } from "../../../state/errors";
5
+ import { rtcStore } from "../../../state/store";
6
+ import { SdkEventType, eventBus } from "../../events";
7
+ import { BaseSocketHandler } from "./base.handler";
8
+ import { callJoinInfoSchema } from "./schema";
9
+ import type { CallJoinInfoEvent } from "./schema";
10
+
11
+ export class CallJoinInfoHandler extends BaseSocketHandler<CallJoinInfoEvent> {
12
+ protected readonly eventName = "call.join-info";
13
+ protected readonly schema = callJoinInfoSchema;
14
+
15
+ protected async handle(data: CallJoinInfoEvent): Promise<void> {
16
+ const currentState = rtcStore.getState();
17
+ // Get current user ID from auth instead of localParticipantId
18
+ const currentUserId = this.authManager?.getCurrentUserId();
19
+
20
+ const currentSessionId = this.getSessionId();
21
+
22
+ if (currentSessionId !== data.callId) {
23
+ this.logger.error("CallId mismatch in join-info event", {
24
+ eventCallId: data.callId,
25
+ sessionCallId: currentSessionId,
26
+ currentUserId,
27
+ });
28
+ pushStaleEventError("call.join-info", "callId mismatch", {
29
+ eventCallId: data.callId,
30
+ sessionCallId: currentSessionId,
31
+ });
32
+ return;
33
+ }
34
+
35
+ if (this.livekit?.room.state === "connected") {
36
+ this.logger.warn("Already connected to LiveKit, ignoring join-info", {
37
+ callId: data.callId,
38
+ currentUserId,
39
+ currentRoomState: this.livekit.room.state,
40
+ });
41
+ return;
42
+ }
43
+
44
+ // Store join info and set ready to join
45
+ this.updateStore((state) => {
46
+ state.session.livekitInfo = {
47
+ token: data.token,
48
+ roomName: data.roomName,
49
+ callId: data.callId,
50
+ url: data.url,
51
+ };
52
+
53
+ // Always set READY_TO_JOIN when receiving join-info
54
+ // Backend controls when to send join-info, so we trust it
55
+ state.session.status = "READY_TO_JOIN";
56
+ });
57
+
58
+ // Simplified auto-join logic: auto-join if enabled and user exists
59
+ const shouldAutoJoin = this.autoJoinConfig?.enabled && currentUserId;
60
+
61
+ if (shouldAutoJoin && this.livekit && data.url) {
62
+ try {
63
+ this.logger.info("Auto-joining LiveKit room after receiving join-info", {
64
+ callId: data.callId,
65
+ currentUserId,
66
+ });
67
+
68
+ // Update state to connecting before joining
69
+ this.updateStore((state) => {
70
+ state.session.status = "CONNECTING";
71
+ });
72
+
73
+ const joinSuccess = this.autoJoinConfig?.retryOnFailure
74
+ ? await this.retryAutoJoin(data.callId, data.token, data.url)
75
+ : await this.livekit.joinRoom(data.token, data.url).then(() => true).catch(() => false);
76
+
77
+ if (joinSuccess) {
78
+ // Update state after successful join
79
+ this.updateStore((state) => {
80
+ state.session.status = "ACTIVE";
81
+ if (currentUserId) {
82
+ // Defensive check: create participant if it doesn't exist
83
+ if (!state.room.participants[currentUserId]) {
84
+ this.logger.warn("Creating missing participant during auto-join", {
85
+ currentUserId,
86
+ callId: data.callId,
87
+ });
88
+
89
+ state.room.participants[currentUserId] = {
90
+ id: currentUserId,
91
+ role: state.session.myRole || "MEMBER",
92
+ callState: "INVITED",
93
+ invitedAt: Date.now(),
94
+ audioEnabled: true,
95
+ videoEnabled: true,
96
+ isSpeaking: false,
97
+ };
98
+ }
99
+
100
+ state.room.participants[currentUserId].callState = "JOINED";
101
+ state.room.participants[currentUserId].joinedAt = Date.now();
102
+
103
+ this.logger.debug("Participant joined during auto-join", {
104
+ participantId: currentUserId,
105
+ callState: "JOINED",
106
+ callId: data.callId,
107
+ });
108
+ }
109
+ });
110
+
111
+ // Emit participant joined event
112
+ eventBus.emit(
113
+ SdkEventType.PARTICIPANT_JOINED,
114
+ {
115
+ callId: data.callId,
116
+ participant: {
117
+ id: currentUserId,
118
+ role: currentState.session.myRole || "MEMBER",
119
+ },
120
+ timestamp: Date.now(),
121
+ },
122
+ "socket"
123
+ );
124
+
125
+ this.logger.info("Successfully auto-joined LiveKit room", {
126
+ callId: data.callId,
127
+ currentUserId,
128
+ retriesUsed: this.autoJoinConfig?.retryOnFailure,
129
+ });
130
+ } else {
131
+ throw new Error("Auto-join failed after retries");
132
+ }
133
+
134
+ } catch (error) {
135
+ this.logger.error("Failed to auto-join to LiveKit room", {
136
+ callId: data.callId,
137
+ error,
138
+ });
139
+
140
+ // Reset state on failure
141
+ this.updateStore((state) => {
142
+ state.session.status = "READY_TO_JOIN"; // Ready for manual join
143
+ if (currentUserId) {
144
+ // Defensive check: only update if participant exists
145
+ if (state.room.participants[currentUserId]) {
146
+ state.room.participants[currentUserId].callState = "LEFT";
147
+ }
148
+ }
149
+ });
150
+
151
+ pushLiveKitConnectError(
152
+ error instanceof Error ? error.message : "Unknown error",
153
+ error
154
+ );
155
+
156
+ // Don't throw error - allow manual join as fallback
157
+ this.logger.warn("Auto-join failed, user can manually join", {
158
+ callId: data.callId,
159
+ });
160
+ }
161
+ }
162
+
163
+ // Emit join info received event
164
+ eventBus.emit(
165
+ SdkEventType.JOIN_INFO_RECEIVED,
166
+ {
167
+ callId: data.callId,
168
+ participantId: currentUserId || "unknown",
169
+ timestamp: Date.now(),
170
+ hasUrl: !!data.url,
171
+ hasToken: !!data.token,
172
+ autoJoined: shouldAutoJoin,
173
+ },
174
+ "socket"
175
+ );
176
+ }
177
+
178
+ private getSessionId(): string {
179
+ return rtcStore.getState().session.id || "";
180
+ }
181
+ }
@@ -0,0 +1,42 @@
1
+ import { BaseSocketHandler } from "./base.handler";
2
+ import { callParticipantJoinedSchema } from "./schema";
3
+ import type { CallParticipantJoinedEvent } from "./schema";
4
+
5
+ export class CallParticipantJoinedHandler extends BaseSocketHandler<CallParticipantJoinedEvent> {
6
+ protected readonly eventName = "call.participant-joined";
7
+ protected readonly schema = callParticipantJoinedSchema;
8
+
9
+ protected handle(data: CallParticipantJoinedEvent): void {
10
+ this.updateStore((state) => {
11
+ const participant = state.room.participants[data.participant.id];
12
+ if (participant) {
13
+ participant.callState = "JOINED";
14
+ participant.joinedAt = data.timestamp || Date.now();
15
+
16
+ // Update profile data from socket event
17
+ if (data.participant.firstName) {
18
+ participant.firstName = data.participant.firstName;
19
+ }
20
+ if (data.participant.lastName) {
21
+ participant.lastName = data.participant.lastName;
22
+ }
23
+ if (data.participant.profilePhoto) {
24
+ participant.avatarUrl = data.participant.profilePhoto;
25
+ }
26
+
27
+ this.logger.debug("Participant state updated via socket event", {
28
+ participantId: data.participant.id,
29
+ callState: "JOINED",
30
+ callId: data.callId,
31
+ source: "call.participant-joined",
32
+ });
33
+ } else {
34
+ this.logger.warn("Participant not found for join event", {
35
+ participantId: data.participant.id,
36
+ callId: data.callId,
37
+ availableParticipants: Object.keys(state.room.participants),
38
+ });
39
+ }
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,42 @@
1
+ import { BaseSocketHandler } from "./base.handler";
2
+ import { callParticipantJoiningSchema } from "./schema";
3
+ import type { CallParticipantJoiningEvent } from "./schema";
4
+
5
+ export class CallParticipantJoiningHandler extends BaseSocketHandler<CallParticipantJoiningEvent> {
6
+ protected readonly eventName = "call.participant-joining";
7
+ protected readonly schema = callParticipantJoiningSchema;
8
+
9
+ protected handle(data: CallParticipantJoiningEvent): void {
10
+ this.updateStore((state) => {
11
+ const participant = state.room.participants[data.participant.id];
12
+ if (participant) {
13
+ participant.callState = "RINGING"; // Participant is getting ready to join
14
+ participant.joinedAt = data.timestamp || Date.now();
15
+
16
+ // Update profile data from socket event
17
+ if (data.participant.firstName) {
18
+ participant.firstName = data.participant.firstName;
19
+ }
20
+ if (data.participant.lastName) {
21
+ participant.lastName = data.participant.lastName;
22
+ }
23
+ if (data.participant.profilePhoto) {
24
+ participant.avatarUrl = data.participant.profilePhoto;
25
+ }
26
+
27
+ this.logger.debug("Participant state updated via socket event", {
28
+ participantId: data.participant.id,
29
+ callState: "RINGING",
30
+ callId: data.callId,
31
+ source: "call.participant-joining",
32
+ });
33
+ } else {
34
+ this.logger.warn("Participant not found for joining event", {
35
+ participantId: data.participant.id,
36
+ callId: data.callId,
37
+ availableParticipants: Object.keys(state.room.participants),
38
+ });
39
+ }
40
+ });
41
+ }
42
+ }
@@ -0,0 +1,31 @@
1
+ import type { Participant } from "../../../state/types";
2
+ import { BaseSocketHandler } from "./base.handler";
3
+ import { callTimeoutSchema } from "./schema";
4
+ import type { CallTimeoutEvent } from "./schema";
5
+
6
+ export class CallTimeoutHandler extends BaseSocketHandler<CallTimeoutEvent> {
7
+ protected readonly eventName = "call.timeout";
8
+ protected readonly schema = callTimeoutSchema;
9
+
10
+ protected handle(data: CallTimeoutEvent): void {
11
+ const reason = data.reason || "timeout";
12
+ this.logger.info(`Call timeout: ${reason}`, { callId: data.callId });
13
+
14
+ this.updateStore((state) => {
15
+ if (state.session.id === data.callId) {
16
+ state.session.status = "ENDED";
17
+ state.incomingCall = undefined;
18
+
19
+ // Mark all participants as left
20
+ for (const participant of Object.values(
21
+ state.room.participants
22
+ ) as Participant[]) {
23
+ participant.callState = "LEFT";
24
+ if (!participant.leftAt) {
25
+ participant.leftAt = data.timestamp || Date.now();
26
+ }
27
+ }
28
+ }
29
+ });
30
+ }
31
+ }
@@ -0,0 +1,62 @@
1
+ import type { Socket } from "socket.io-client";
2
+ import { createLogger } from "../../../utils/logger";
3
+ import type { SocketHandlerOptions } from "./base.handler";
4
+ import { CallParticipantAcceptedHandler } from "./call-accepted.handler";
5
+ import { CallCanceledHandler } from "./call-canceled.handler";
6
+ import { CallParticipantDeclinedHandler } from "./call-declined.handler";
7
+ import { CallEndedHandler } from "./call-ended.handler";
8
+ import { CallIncomingHandler } from "./call-incoming.handler";
9
+ import { CallJoinInfoHandler } from "./call-join-info.handler";
10
+ import { CallParticipantJoinedHandler } from "./call-participant-joined.handler";
11
+ import { CallParticipantJoiningHandler } from "./call-participant-joining.handler";
12
+ import { CallTimeoutHandler } from "./call-timeout.handler";
13
+ import { ParticipantLeftHandler } from "./participant-left.handler";
14
+
15
+ const logger = createLogger("socketio:registry");
16
+
17
+ export class SocketHandlerRegistry {
18
+ private handlers = new Map<string, any>();
19
+
20
+ constructor(private options: SocketHandlerOptions = {}) {
21
+ this.initializeHandlers();
22
+ }
23
+
24
+ private initializeHandlers(): void {
25
+ const handlers = [
26
+ new CallIncomingHandler(this.options),
27
+ new CallParticipantAcceptedHandler(this.options),
28
+ new CallParticipantDeclinedHandler(this.options),
29
+ new CallEndedHandler(this.options),
30
+ new CallJoinInfoHandler(this.options),
31
+ new ParticipantLeftHandler(this.options),
32
+ new CallParticipantJoiningHandler(this.options),
33
+ new CallParticipantJoinedHandler(this.options),
34
+ new CallTimeoutHandler(this.options),
35
+ new CallCanceledHandler(this.options),
36
+ ];
37
+
38
+ for (const handler of handlers) {
39
+ this.handlers.set((handler as any).eventName, handler);
40
+ }
41
+ }
42
+
43
+ registerEventListeners(socket: Socket): void {
44
+ for (const [eventName, handler] of this.handlers) {
45
+ socket.on(eventName, (rawData: any) => {
46
+ handler.handleRaw(rawData).catch((error: Error) => {
47
+ logger.error(`Handler error for ${eventName}:`, error);
48
+ });
49
+ });
50
+ }
51
+ }
52
+
53
+ removeEventListeners(socket: Socket): void {
54
+ for (const eventName of this.handlers.keys()) {
55
+ socket.off(eventName);
56
+ }
57
+ }
58
+
59
+ destroy(): void {
60
+ this.handlers.clear();
61
+ }
62
+ }
@@ -0,0 +1,21 @@
1
+ export { BaseSocketHandler } from "./base.handler";
2
+ export { CallIncomingHandler } from "./call-incoming.handler";
3
+ export { CallParticipantAcceptedHandler } from "./call-accepted.handler";
4
+ export { CallParticipantDeclinedHandler } from "./call-declined.handler";
5
+ export { CallEndedHandler } from "./call-ended.handler";
6
+ export { CallJoinInfoHandler } from "./call-join-info.handler";
7
+ export { ParticipantLeftHandler } from "./participant-left.handler";
8
+ export { CallParticipantJoiningHandler } from "./call-participant-joining.handler";
9
+ export { CallParticipantJoinedHandler } from "./call-participant-joined.handler";
10
+ export { CallTimeoutHandler } from "./call-timeout.handler";
11
+ export { CallCanceledHandler } from "./call-canceled.handler";
12
+ export { SocketHandlerRegistry } from "./handler.registry";
13
+
14
+ // Re-export schema types for convenience
15
+ export type {
16
+ CallIncomingEvent,
17
+ CallAcceptedEvent,
18
+ CallEndedEvent,
19
+ CallJoinInfoEvent,
20
+ ParticipantLeftEvent,
21
+ } from "./schema";
@@ -0,0 +1,37 @@
1
+ import { pushStaleEventError } from "../../../state/errors";
2
+ import { BaseSocketHandler } from "./base.handler";
3
+ import { participantLeftSchema } from "./schema";
4
+ import type { ParticipantLeftEvent } from "./schema";
5
+
6
+ export class ParticipantLeftHandler extends BaseSocketHandler<ParticipantLeftEvent> {
7
+ protected readonly eventName = "call.participant-left";
8
+ protected readonly schema = participantLeftSchema;
9
+
10
+ protected handle(data: ParticipantLeftEvent): void {
11
+ this.updateStore((state) => {
12
+ if (state.session.id !== data.callId) {
13
+ pushStaleEventError("call.participant-left", "callId mismatch", {
14
+ eventCallId: data.callId,
15
+ sessionCallId: state.session.id,
16
+ });
17
+ return;
18
+ }
19
+
20
+ const participant = state.room.participants[data.participant.id];
21
+ if (participant) {
22
+ participant.callState = "LEFT";
23
+ participant.leftAt = data.timestamp || Date.now();
24
+ }
25
+ });
26
+
27
+ const isLocalParticipant =
28
+ this.livekit?.room.localParticipant?.identity === data.participant.id;
29
+ if (isLocalParticipant && this.livekit) {
30
+ this.livekit.disconnect().catch((error: any) => {
31
+ this.logger.error("Error disconnecting from LiveKit after self-leave", {
32
+ error,
33
+ });
34
+ });
35
+ }
36
+ }
37
+ }
@@ -0,0 +1,130 @@
1
+ import { z } from "zod";
2
+
3
+ // Participant with a role for socket events
4
+ export const socketParticipantSchema = z.object({
5
+ id: z.string(),
6
+ firstName: z.string().nullable(),
7
+ lastName: z.string().nullable(),
8
+ username: z.string().nullable(),
9
+ profilePhoto: z.string().nullable(),
10
+ role: z.enum(["CALLER", "CALLEE", "HOST", "MEMBER"]).optional(),
11
+ });
12
+
13
+ // call.incoming event schema
14
+ export const callIncomingSchema = z.object({
15
+ callId: z.string(),
16
+ type: z.enum(["AUDIO", "VIDEO"]),
17
+ participants: z.array(socketParticipantSchema).min(1), // Required, at least 1 participant
18
+ timestamp: z.number(),
19
+ });
20
+
21
+ // call.accepted event schema (legacy)
22
+ export const callAcceptedSchema = z.object({
23
+ callId: z.string(),
24
+ by: z.object({
25
+ id: z.string(),
26
+ acceptedAt: z.number().optional(),
27
+ }),
28
+ });
29
+
30
+ // call.participant-accepted event schema
31
+ export const callParticipantAcceptedSchema = z.object({
32
+ callId: z.string(),
33
+ participantId: z.string(),
34
+ participant: socketParticipantSchema,
35
+ acceptedAt: z.string().optional(),
36
+ });
37
+
38
+ // call.join-info event schema
39
+ export const callJoinInfoSchema = z.object({
40
+ callId: z.string(),
41
+ token: z.string(),
42
+ url: z.string().optional(),
43
+ roomName: z.string(),
44
+ expiresAt: z.number().optional(),
45
+ });
46
+
47
+ // call.ended event schema
48
+ export const callEndedSchema = z.object({
49
+ callId: z.string(),
50
+ reason: z.string().optional(),
51
+ endedAt: z.string().optional(),
52
+ }).passthrough(); // Allow additional fields to pass through
53
+
54
+
55
+ // call.participant-left event schema
56
+ export const participantLeftSchema = z.object({
57
+ callId: z.string(),
58
+ participant: z.object({
59
+ id: z.string(),
60
+ name: z.string().optional(),
61
+ }),
62
+ timestamp: z.number().optional(),
63
+ });
64
+
65
+ // call.participant-joining event schema
66
+ export const callParticipantJoiningSchema = z.object({
67
+ callId: z.string(),
68
+ participantId: z.string(),
69
+ participant: socketParticipantSchema,
70
+ timestamp: z.number().optional(),
71
+ });
72
+
73
+ // call.participant-joined event schema
74
+ export const callParticipantJoinedSchema = z.object({
75
+ callId: z.string(),
76
+ participantId: z.string(),
77
+ participant: socketParticipantSchema,
78
+ timestamp: z.number().optional(),
79
+ });
80
+
81
+ // call.timeout event schema
82
+ export const callTimeoutSchema = z.object({
83
+ callId: z.string(),
84
+ reason: z.string().optional(),
85
+ timestamp: z.number().optional(),
86
+ });
87
+
88
+ // call.participant-declined event schema
89
+ export const callParticipantDeclinedSchema = z.object({
90
+ callId: z.string(),
91
+ participantId: z.string(),
92
+ participant: socketParticipantSchema,
93
+ declinedAt: z.string().optional(),
94
+ });
95
+
96
+ // call.canceled event schema
97
+ export const callCanceledSchema = z.object({
98
+ callId: z.string(),
99
+ reason: z.string().optional(),
100
+ timestamp: z.number().optional(),
101
+ by: socketParticipantSchema.optional(),
102
+ });
103
+
104
+ // Optional participant.joined event schema (for future use)
105
+ export const participantJoinedSchema = z.object({
106
+ callId: z.string(),
107
+ participant: z.object({
108
+ id: z.string(),
109
+ name: z.string().optional(),
110
+ }),
111
+ timestamp: z.number().optional(),
112
+ });
113
+
114
+ // Inferred types for use in handlers
115
+ export type CallIncomingEvent = z.infer<typeof callIncomingSchema>;
116
+ export type CallAcceptedEvent = z.infer<typeof callAcceptedSchema>;
117
+ export type CallParticipantAcceptedEvent = z.infer<typeof callParticipantAcceptedSchema>;
118
+ export type CallJoinInfoEvent = z.infer<typeof callJoinInfoSchema>;
119
+ export type CallEndedEvent = z.infer<typeof callEndedSchema>;
120
+ export type ParticipantLeftEvent = z.infer<typeof participantLeftSchema>;
121
+ export type ParticipantJoinedEvent = z.infer<typeof participantJoinedSchema>;
122
+ export type CallParticipantJoiningEvent = z.infer<
123
+ typeof callParticipantJoiningSchema
124
+ >;
125
+ export type CallParticipantJoinedEvent = z.infer<
126
+ typeof callParticipantJoinedSchema
127
+ >;
128
+ export type CallTimeoutEvent = z.infer<typeof callTimeoutSchema>;
129
+ export type CallParticipantDeclinedEvent = z.infer<typeof callParticipantDeclinedSchema>;
130
+ export type CallCanceledEvent = z.infer<typeof callCanceledSchema>;
@@ -0,0 +1,5 @@
1
+ export { SocketManager } from "./socket.manager";
2
+ export type {
3
+ ConnectionConfig,
4
+ ConnectionState,
5
+ } from "./types";