farvex 1.0.0 → 2.0.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/CHANGELOG.md +19 -0
- package/README.md +150 -224
- package/dist/index.cjs +652 -61
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +652 -61
- package/dist/index.js.map +1 -1
- package/dist/livekit/index.cjs +43 -1
- package/dist/livekit/index.cjs.map +1 -1
- package/dist/livekit/index.d.cts +18 -0
- package/dist/livekit/index.d.ts +18 -0
- package/dist/livekit/index.js +46 -0
- package/dist/livekit/index.js.map +1 -1
- package/dist/react/index.cjs +17 -2
- package/dist/react/index.cjs.map +1 -1
- package/dist/react/index.d.cts +11 -8
- package/dist/react/index.d.ts +11 -8
- package/dist/react/index.js +16 -3
- package/dist/react/index.js.map +1 -1
- package/dist/{types-DhJEeeui.d.cts → types-BjVaSMZ6.d.cts} +68 -5
- package/dist/{types-DhJEeeui.d.ts → types-BjVaSMZ6.d.ts} +68 -5
- package/package.json +3 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,24 @@
|
|
|
1
1
|
# farvex
|
|
2
2
|
|
|
3
|
+
## 2.0.0
|
|
4
|
+
|
|
5
|
+
### Major Changes
|
|
6
|
+
|
|
7
|
+
- Add a first-class current-session controller for browser media sessions.
|
|
8
|
+
|
|
9
|
+
This release introduces `client.session`, `useCurrentSession`, `useCurrentSessionActions`, and `SessionRoom` so consumers can start, accept, join, end, and render the current media session without maintaining separate join-grant state.
|
|
10
|
+
|
|
11
|
+
Breaking changes:
|
|
12
|
+
- `useSession()` and `sessions.get()` now require an explicit session ID and no longer infer the newest non-ended session.
|
|
13
|
+
- Consumers should use `client.session.start()` and `client.session.accept()` for user-facing media flows instead of low-level `client.sessions.start()` / `client.sessions.accept()`.
|
|
14
|
+
- Current session state is now represented by the SDK's `CurrentSession` snapshot rather than consumer-local LiveKit token state.
|
|
15
|
+
|
|
16
|
+
## 1.0.1
|
|
17
|
+
|
|
18
|
+
### Patch Changes
|
|
19
|
+
|
|
20
|
+
- Fix host client disposal, expire stale incoming invites, and export host start input types for frontend integrations.
|
|
21
|
+
|
|
3
22
|
## 1.0.0
|
|
4
23
|
|
|
5
24
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
# farvex
|
|
2
2
|
|
|
3
3
|
Web SDK for Callpad voice sessions. It wraps the generated session HTTP client,
|
|
4
|
-
Centrifugo realtime updates,
|
|
5
|
-
|
|
6
|
-
API clients.
|
|
4
|
+
Centrifugo realtime updates, a current browser session controller, React hooks,
|
|
5
|
+
and LiveKit helpers.
|
|
7
6
|
|
|
8
7
|
## Exports
|
|
9
8
|
|
|
10
|
-
| Entry | Purpose
|
|
11
|
-
| ---------------- |
|
|
12
|
-
| `farvex` |
|
|
13
|
-
| `farvex/react` | React provider and focused session hooks.
|
|
14
|
-
| `farvex/livekit` | LiveKit
|
|
9
|
+
| Entry | Purpose |
|
|
10
|
+
| ---------------- | ----------------------------------------- |
|
|
11
|
+
| `farvex` | Client factories and SDK/API types. |
|
|
12
|
+
| `farvex/react` | React provider and focused session hooks. |
|
|
13
|
+
| `farvex/livekit` | LiveKit re-exports and `SessionRoom`. |
|
|
15
14
|
|
|
16
15
|
## Install
|
|
17
16
|
|
|
@@ -20,309 +19,236 @@ npm install farvex livekit-client @livekit/components-react
|
|
|
20
19
|
```
|
|
21
20
|
|
|
22
21
|
`livekit-client`, `@livekit/components-react`, `react`, and `react-dom` are
|
|
23
|
-
peer dependencies. Install the LiveKit packages only in apps that render
|
|
24
|
-
media.
|
|
22
|
+
peer dependencies. Install the LiveKit packages only in apps that render media.
|
|
25
23
|
|
|
26
|
-
## Setup
|
|
27
|
-
|
|
28
|
-
The SDK needs these values from the app:
|
|
29
|
-
|
|
30
|
-
| Value | Description |
|
|
31
|
-
| -------- | ----------------------------------------------- |
|
|
32
|
-
| `apiUrl` | Base URL for the Callpad API. |
|
|
33
|
-
| `userId` | Callpad user ID for the signed-in app user. |
|
|
34
|
-
| `auth` | Function that returns a bearer token on demand. |
|
|
35
|
-
| `vendor` | Vendor slug. Required by host clients. |
|
|
36
|
-
|
|
37
|
-
`getAccessToken` is called before API requests and when realtime auth needs a
|
|
38
|
-
fresh token. Keep token minting in your app backend or existing auth layer; the
|
|
39
|
-
browser client should only receive a user-scoped token. The examples below
|
|
40
|
-
assume `getAccessToken` already exists in the app.
|
|
24
|
+
## Client Setup
|
|
41
25
|
|
|
42
26
|
```ts
|
|
43
|
-
import {
|
|
27
|
+
import { createHostClient } from "farvex";
|
|
44
28
|
|
|
45
|
-
const
|
|
29
|
+
const client = createHostClient({
|
|
46
30
|
apiUrl: "https://api.callpad.example.com",
|
|
47
|
-
|
|
31
|
+
vendor: "acme",
|
|
32
|
+
userId: user.id,
|
|
48
33
|
auth: {
|
|
49
34
|
getAccessToken,
|
|
50
|
-
onUnauthorized: () =>
|
|
51
|
-
auth.signOut();
|
|
52
|
-
},
|
|
35
|
+
onUnauthorized: () => auth.signOut(),
|
|
53
36
|
},
|
|
54
37
|
});
|
|
55
|
-
```
|
|
56
38
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
`disconnect()` when the user leaves the voice UI, or `dispose()` when the client
|
|
60
|
-
will not be reused.
|
|
61
|
-
|
|
62
|
-
```ts
|
|
63
|
-
await callpad.connect();
|
|
64
|
-
await callpad.sessions.sync();
|
|
39
|
+
await client.connect();
|
|
40
|
+
await client.sessions.sync(undefined, { mode: "replaceVisible" });
|
|
65
41
|
```
|
|
66
42
|
|
|
67
|
-
|
|
68
|
-
|
|
43
|
+
`getAccessToken` is called before REST requests and realtime token refreshes.
|
|
44
|
+
SDK methods throw `CallpadError` for API problem responses.
|
|
69
45
|
|
|
70
|
-
##
|
|
46
|
+
## API Shape
|
|
71
47
|
|
|
72
|
-
|
|
48
|
+
Use `client.session` for the current browser media session lifecycle:
|
|
73
49
|
|
|
74
50
|
```ts
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const callpad = createSessionClient({
|
|
78
|
-
apiUrl: "https://api.callpad.example.com",
|
|
79
|
-
userId: customer.id,
|
|
80
|
-
auth: { getAccessToken },
|
|
51
|
+
const current = await client.session.start({
|
|
52
|
+
target: { type: "customer", userId: customer.id },
|
|
81
53
|
});
|
|
82
54
|
|
|
83
|
-
await
|
|
55
|
+
await client.session.end();
|
|
56
|
+
```
|
|
84
57
|
|
|
85
|
-
|
|
86
|
-
vendor: "acme",
|
|
87
|
-
target: { type: "vendor_user", userId: 42 },
|
|
88
|
-
});
|
|
58
|
+
Use `client.sessions` for the low-level collection/data API:
|
|
89
59
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
60
|
+
```ts
|
|
61
|
+
const sessions = await client.sessions.sync();
|
|
62
|
+
const session = client.sessions.get(current.sessionId);
|
|
63
|
+
const canRecord = session ? client.sessions.can(session, "startRecording") : false;
|
|
93
64
|
```
|
|
94
65
|
|
|
95
|
-
|
|
96
|
-
|
|
66
|
+
The backend can support many live sessions. The browser SDK defaults to one
|
|
67
|
+
current media session per client instance. `client.session.start()` and
|
|
68
|
+
`client.session.accept()` use `rejectWhileBusy` by default, so starting or
|
|
69
|
+
accepting while a non-terminal current session exists fails locally with
|
|
70
|
+
`session.busy`. Pass `{ behavior: "allowParallel" }` only when intentionally
|
|
71
|
+
creating and selecting another backend session.
|
|
97
72
|
|
|
98
|
-
|
|
73
|
+
`StartedSession.join` from the generated API is nullable. The high-level
|
|
74
|
+
`client.session` methods require a media grant by default and fail with
|
|
75
|
+
`session.join_unavailable` if the backend creates/accepts a session but does not
|
|
76
|
+
return a browser media grant.
|
|
99
77
|
|
|
100
|
-
|
|
101
|
-
|
|
78
|
+
## React Usage
|
|
79
|
+
|
|
80
|
+
Import `farvex/react` from client components.
|
|
102
81
|
|
|
103
82
|
```tsx
|
|
104
83
|
"use client";
|
|
105
84
|
|
|
106
|
-
import { useMemo,
|
|
107
|
-
import {
|
|
108
|
-
import { CallpadProvider
|
|
109
|
-
import { LiveKitRoom, RoomAudioRenderer } from "farvex/livekit";
|
|
110
|
-
|
|
111
|
-
const vendor = "acme";
|
|
112
|
-
const vendorUserId = 42;
|
|
85
|
+
import { useMemo, type ReactNode } from "react";
|
|
86
|
+
import { createHostClient, type HostClient } from "farvex";
|
|
87
|
+
import { CallpadProvider } from "farvex/react";
|
|
113
88
|
|
|
114
|
-
export const
|
|
115
|
-
const client = useMemo<
|
|
89
|
+
export const VoiceProvider = ({ children }: { children: ReactNode }) => {
|
|
90
|
+
const client = useMemo<HostClient>(
|
|
116
91
|
() =>
|
|
117
|
-
|
|
92
|
+
createHostClient({
|
|
118
93
|
apiUrl: process.env.NEXT_PUBLIC_CALLPAD_API_URL!,
|
|
119
|
-
|
|
94
|
+
vendor: "acme",
|
|
95
|
+
userId: 42,
|
|
120
96
|
auth: { getAccessToken },
|
|
121
97
|
}),
|
|
122
|
-
[
|
|
98
|
+
[],
|
|
123
99
|
);
|
|
124
100
|
|
|
125
101
|
return (
|
|
126
102
|
<CallpadProvider client={client} connect disposeOnUnmount>
|
|
127
|
-
|
|
103
|
+
{children}
|
|
128
104
|
</CallpadProvider>
|
|
129
105
|
);
|
|
130
106
|
};
|
|
131
|
-
|
|
132
|
-
const CustomerCallButton = () => {
|
|
133
|
-
const callpad = useCallpad();
|
|
134
|
-
const [join, setJoin] = useState<VoiceJoinGrant | null>(null);
|
|
135
|
-
|
|
136
|
-
const startCall = async () => {
|
|
137
|
-
const started = await callpad.sessions.start({
|
|
138
|
-
vendor,
|
|
139
|
-
target: { type: "vendor_user", userId: vendorUserId },
|
|
140
|
-
});
|
|
141
|
-
setJoin(started.join);
|
|
142
|
-
};
|
|
143
|
-
|
|
144
|
-
return (
|
|
145
|
-
<>
|
|
146
|
-
<button type="button" onClick={startCall}>
|
|
147
|
-
Call support
|
|
148
|
-
</button>
|
|
149
|
-
{join ? <SessionMedia join={join} /> : null}
|
|
150
|
-
</>
|
|
151
|
-
);
|
|
152
|
-
};
|
|
153
|
-
|
|
154
|
-
const SessionMedia = ({ join }: { join: VoiceJoinGrant }) => (
|
|
155
|
-
<LiveKitRoom serverUrl={join.url} token={join.token} audio video={false}>
|
|
156
|
-
<RoomAudioRenderer />
|
|
157
|
-
</LiveKitRoom>
|
|
158
|
-
);
|
|
159
107
|
```
|
|
160
108
|
|
|
161
|
-
|
|
109
|
+
Hooks:
|
|
162
110
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
111
|
+
| Hook | Purpose |
|
|
112
|
+
| ---------------------------- | ------------------------------------------- |
|
|
113
|
+
| `useCallpad()` | Current SDK client. |
|
|
114
|
+
| `useStatus()` | Realtime connection status. |
|
|
115
|
+
| `useSessions()` | Visible session collection. |
|
|
116
|
+
| `useSession(id)` | Exact session lookup. Pass `null` for none. |
|
|
117
|
+
| `useCurrentSession()` | Current browser media-session snapshot. |
|
|
118
|
+
| `useCurrentSessionActions()` | Current session commands and media events. |
|
|
119
|
+
| `useIncomingInvites()` | Pending invites for the current user. |
|
|
120
|
+
| `useIncomingInvite()` | First pending invite shortcut. |
|
|
121
|
+
| `useCan(id, action)` | UI helper for low-level session actions. |
|
|
122
|
+
| `useSessionDuration(id)` | Live elapsed time for an exact session ID. |
|
|
123
|
+
|
|
124
|
+
Starting a session:
|
|
166
125
|
|
|
167
|
-
```
|
|
168
|
-
import {
|
|
126
|
+
```tsx
|
|
127
|
+
import { useCallpad, useCurrentSession } from "farvex/react";
|
|
169
128
|
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
userId: user.id,
|
|
174
|
-
auth: { getAccessToken },
|
|
175
|
-
});
|
|
129
|
+
const StartButton = () => {
|
|
130
|
+
const client = useCallpad<HostClient>();
|
|
131
|
+
const { busy } = useCurrentSession();
|
|
176
132
|
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
}
|
|
133
|
+
const start = () =>
|
|
134
|
+
client.session.start({
|
|
135
|
+
target: { type: "phone", phone: "+15551234567" },
|
|
136
|
+
});
|
|
180
137
|
|
|
181
|
-
|
|
138
|
+
return (
|
|
139
|
+
<button type="button" disabled={busy} onClick={start}>
|
|
140
|
+
Start session
|
|
141
|
+
</button>
|
|
142
|
+
);
|
|
143
|
+
};
|
|
182
144
|
```
|
|
183
145
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
When a customer calls a vendor user, the vendor app receives an incoming invite
|
|
187
|
-
over realtime. Accepting the invite returns a join grant for the vendor user.
|
|
146
|
+
Accepting an invite:
|
|
188
147
|
|
|
189
148
|
```tsx
|
|
190
|
-
"use client";
|
|
191
|
-
|
|
192
|
-
import { useState } from "react";
|
|
193
|
-
import { type VoiceJoinGrant } from "farvex";
|
|
194
149
|
import { useCallpad, useIncomingInvite } from "farvex/react";
|
|
195
|
-
import { LiveKitRoom, RoomAudioRenderer } from "farvex/livekit";
|
|
196
150
|
|
|
197
|
-
|
|
198
|
-
const
|
|
151
|
+
const IncomingInvite = () => {
|
|
152
|
+
const client = useCallpad<HostClient>();
|
|
199
153
|
const invite = useIncomingInvite();
|
|
200
|
-
const [join, setJoin] = useState<VoiceJoinGrant | null>(null);
|
|
201
154
|
|
|
202
155
|
if (!invite) {
|
|
203
156
|
return null;
|
|
204
157
|
}
|
|
205
158
|
|
|
206
|
-
const accept = async () => {
|
|
207
|
-
const started = await callpad.sessions.accept({
|
|
208
|
-
sessionId: invite.session.id,
|
|
209
|
-
participantId: invite.participant.id,
|
|
210
|
-
});
|
|
211
|
-
setJoin(started.join);
|
|
212
|
-
};
|
|
213
|
-
|
|
214
|
-
const reject = () =>
|
|
215
|
-
callpad.sessions.reject({
|
|
216
|
-
sessionId: invite.session.id,
|
|
217
|
-
participantId: invite.participant.id,
|
|
218
|
-
});
|
|
219
|
-
|
|
220
159
|
return (
|
|
221
160
|
<>
|
|
222
|
-
<button
|
|
161
|
+
<button
|
|
162
|
+
type="button"
|
|
163
|
+
onClick={() =>
|
|
164
|
+
client.session.accept({
|
|
165
|
+
sessionId: invite.session.id,
|
|
166
|
+
participantId: invite.participant.id,
|
|
167
|
+
})
|
|
168
|
+
}
|
|
169
|
+
>
|
|
223
170
|
Answer
|
|
224
171
|
</button>
|
|
225
|
-
<button
|
|
172
|
+
<button
|
|
173
|
+
type="button"
|
|
174
|
+
onClick={() =>
|
|
175
|
+
client.sessions.reject({
|
|
176
|
+
sessionId: invite.session.id,
|
|
177
|
+
participantId: invite.participant.id,
|
|
178
|
+
})
|
|
179
|
+
}
|
|
180
|
+
>
|
|
226
181
|
Decline
|
|
227
182
|
</button>
|
|
228
|
-
{join ? (
|
|
229
|
-
<LiveKitRoom serverUrl={join.url} token={join.token} audio video={false}>
|
|
230
|
-
<RoomAudioRenderer />
|
|
231
|
-
</LiveKitRoom>
|
|
232
|
-
) : null}
|
|
233
183
|
</>
|
|
234
184
|
);
|
|
235
185
|
};
|
|
236
186
|
```
|
|
237
187
|
|
|
238
|
-
|
|
188
|
+
Overlay and media should use the same current session ID:
|
|
239
189
|
|
|
240
|
-
```
|
|
241
|
-
|
|
190
|
+
```tsx
|
|
191
|
+
import { RoomAudioRenderer, SessionRoom } from "farvex/livekit";
|
|
192
|
+
import { useCurrentSession, useCurrentSessionActions, useSession } from "farvex/react";
|
|
242
193
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
194
|
+
const SessionOverlay = () => {
|
|
195
|
+
const snapshot = useCurrentSession();
|
|
196
|
+
const actions = useCurrentSessionActions();
|
|
197
|
+
const session = useSession(snapshot.current?.sessionId ?? null);
|
|
247
198
|
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
199
|
+
return (
|
|
200
|
+
<>
|
|
201
|
+
<SessionRoom session={snapshot.current} controls={actions}>
|
|
202
|
+
<RoomAudioRenderer />
|
|
203
|
+
</SessionRoom>
|
|
204
|
+
{snapshot.current && session ? (
|
|
205
|
+
<SessionPanel session={session} phase={snapshot.current.phase} />
|
|
206
|
+
) : null}
|
|
207
|
+
</>
|
|
208
|
+
);
|
|
209
|
+
};
|
|
251
210
|
```
|
|
252
211
|
|
|
253
|
-
|
|
254
|
-
gate controls before calling actions such as `accept`, `leave`, `end`, or
|
|
255
|
-
`startRecording`.
|
|
256
|
-
|
|
257
|
-
## React
|
|
212
|
+
## LiveKit
|
|
258
213
|
|
|
259
|
-
|
|
214
|
+
`farvex/livekit` re-exports first-party LiveKit primitives and adds
|
|
215
|
+
`SessionRoom`. Use `SessionRoom` for the normal SDK flow; use raw `LiveKitRoom`
|
|
216
|
+
only for advanced media ownership.
|
|
260
217
|
|
|
261
218
|
```tsx
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
import { createHostClient, type HostClient } from "farvex";
|
|
266
|
-
import {
|
|
267
|
-
CallpadProvider,
|
|
268
|
-
useCallpad,
|
|
269
|
-
useIncomingInvite,
|
|
270
|
-
useSession,
|
|
271
|
-
useStatus,
|
|
272
|
-
} from "farvex/react";
|
|
273
|
-
|
|
274
|
-
export const VoiceProvider = ({ children }: { children: ReactNode }) => {
|
|
275
|
-
const client = useMemo<HostClient>(
|
|
276
|
-
() =>
|
|
277
|
-
createHostClient({
|
|
278
|
-
apiUrl: process.env.NEXT_PUBLIC_CALLPAD_API_URL!,
|
|
279
|
-
vendor: "acme",
|
|
280
|
-
userId: 42,
|
|
281
|
-
auth: { getAccessToken },
|
|
282
|
-
}),
|
|
283
|
-
[],
|
|
284
|
-
);
|
|
285
|
-
|
|
286
|
-
return (
|
|
287
|
-
<CallpadProvider client={client} connect disposeOnUnmount>
|
|
288
|
-
{children}
|
|
289
|
-
</CallpadProvider>
|
|
290
|
-
);
|
|
291
|
-
};
|
|
219
|
+
<SessionRoom session={current} controls={client.session} audio video={false}>
|
|
220
|
+
<RoomAudioRenderer />
|
|
221
|
+
</SessionRoom>
|
|
292
222
|
```
|
|
293
223
|
|
|
294
|
-
|
|
224
|
+
`SessionRoom` renders nothing without a current session or join grant. It passes
|
|
225
|
+
`session.join.url` and `session.join.token` to LiveKit and reports connected,
|
|
226
|
+
disconnected, and error events back to the session controller when `controls` is
|
|
227
|
+
provided.
|
|
295
228
|
|
|
296
|
-
|
|
297
|
-
| ------------------------- | ------------------------------------------------- |
|
|
298
|
-
| `useCallpad()` | Current SDK client. |
|
|
299
|
-
| `useStatus()` | Realtime connection status. |
|
|
300
|
-
| `useSessions()` | Visible live sessions. |
|
|
301
|
-
| `useSession(id?)` | Specific session, or active session when omitted. |
|
|
302
|
-
| `useIncomingInvites()` | Pending invites for the current user. |
|
|
303
|
-
| `useIncomingInvite()` | First pending invite. |
|
|
304
|
-
| `useCan(id, action)` | UI helper for session actions. |
|
|
305
|
-
| `useSessionDuration(id?)` | Live elapsed time. |
|
|
306
|
-
|
|
307
|
-
Session participants and metadata are exposed on `VoiceSession.participants`.
|
|
308
|
-
Use LiveKit hooks for media participants, tracks, mute state, speaking state,
|
|
309
|
-
device selection, and room lifecycle.
|
|
229
|
+
## Ringing Timeout
|
|
310
230
|
|
|
311
|
-
|
|
231
|
+
The SDK may hide expired incoming invites locally for immediate UX, but backend
|
|
232
|
+
REST/realtime state is authoritative for final session state. Apps should render
|
|
233
|
+
terminal state from `VoiceSession.state`, realtime events, and REST
|
|
234
|
+
reconciliation rather than treating a local timer as final.
|
|
312
235
|
|
|
313
|
-
|
|
236
|
+
## Migration Notes
|
|
237
|
+
|
|
238
|
+
Replace no-ID `useSession()`:
|
|
314
239
|
|
|
315
240
|
```tsx
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
export const SessionMedia = ({ join }: { join: VoiceJoinGrant }) => (
|
|
320
|
-
<LiveKitRoom serverUrl={join.url} token={join.token} audio video={false}>
|
|
321
|
-
<RoomAudioRenderer />
|
|
322
|
-
</LiveKitRoom>
|
|
323
|
-
);
|
|
241
|
+
const { current } = useCurrentSession();
|
|
242
|
+
const session = useSession(current?.sessionId ?? null);
|
|
324
243
|
```
|
|
325
244
|
|
|
245
|
+
Replace local raw join-grant state with `client.session` and `SessionRoom`.
|
|
246
|
+
The current session stores `{ sessionId, participantId, join }`, so the overlay,
|
|
247
|
+
media room, controls, and participant UI all target the same session.
|
|
248
|
+
|
|
249
|
+
SDK source should use the exported `Nullable<T>` alias for nullable types instead
|
|
250
|
+
of direct null unions in public SDK types.
|
|
251
|
+
|
|
326
252
|
## Development
|
|
327
253
|
|
|
328
254
|
From `websdk/`:
|