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,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>;
|