Skip to content

Commit 55e3d3a

Browse files
authored
Make useSession().start more stable (#1233)
1 parent 12b69cb commit 55e3d3a

File tree

3 files changed

+174
-129
lines changed

3 files changed

+174
-129
lines changed

.changeset/sharp-deer-peel.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@livekit/components-react': patch
3+
---
4+
5+
Make useSession().start more stable

packages/react/src/hooks/useAgent.ts

Lines changed: 117 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -345,6 +345,119 @@ export const useAgentTimeoutIdStore = (): {
345345

346346
type SessionStub = Pick<UseSessionReturn, 'connectionState' | 'room' | 'internal'>;
347347

348+
/** Internal hook used by useAgent which generates a function that when called, will return a
349+
* promise which resolves when agent.isAvailable is enabled. */
350+
function useAgentWaitUntilDerivedStates(
351+
emitter: TypedEventEmitter<AgentCallbacks>,
352+
state: AgentState,
353+
) {
354+
const stateRef = React.useRef(state);
355+
React.useEffect(() => {
356+
stateRef.current = state;
357+
}, [state]);
358+
359+
const waitUntilConnected = React.useCallback(
360+
async (signal?: AbortSignal) => {
361+
const { isConnected } = generateDerivedStateValues(stateRef.current);
362+
if (isConnected) {
363+
return;
364+
}
365+
366+
return new Promise<void>((resolve, reject) => {
367+
const stateChangedHandler = (state: AgentState) => {
368+
const { isConnected } = generateDerivedStateValues(state);
369+
if (!isConnected) {
370+
return;
371+
}
372+
cleanup();
373+
resolve();
374+
};
375+
const abortHandler = () => {
376+
cleanup();
377+
reject(new Error('useAgent(/* ... */).waitUntilConnected - signal aborted'));
378+
};
379+
380+
const cleanup = () => {
381+
emitter.off(AgentEvent.StateChanged, stateChangedHandler);
382+
signal?.removeEventListener('abort', abortHandler);
383+
};
384+
385+
emitter.on(AgentEvent.StateChanged, stateChangedHandler);
386+
signal?.addEventListener('abort', abortHandler);
387+
});
388+
},
389+
[emitter],
390+
);
391+
392+
const waitUntilCouldBeListening = React.useCallback(
393+
async (signal?: AbortSignal) => {
394+
const { canListen } = generateDerivedStateValues(stateRef.current);
395+
if (canListen) {
396+
return;
397+
}
398+
399+
return new Promise<void>((resolve, reject) => {
400+
const stateChangedHandler = (state: AgentState) => {
401+
const { canListen } = generateDerivedStateValues(state);
402+
if (!canListen) {
403+
return;
404+
}
405+
cleanup();
406+
resolve();
407+
};
408+
const abortHandler = () => {
409+
cleanup();
410+
reject(new Error('useAgent(/* ... */).waitUntilCouldBeListening - signal aborted'));
411+
};
412+
413+
const cleanup = () => {
414+
emitter.off(AgentEvent.StateChanged, stateChangedHandler);
415+
signal?.removeEventListener('abort', abortHandler);
416+
};
417+
418+
emitter.on(AgentEvent.StateChanged, stateChangedHandler);
419+
signal?.addEventListener('abort', abortHandler);
420+
});
421+
},
422+
[emitter],
423+
);
424+
425+
const waitUntilFinished = React.useCallback(
426+
async (signal?: AbortSignal) => {
427+
const { isFinished } = generateDerivedStateValues(stateRef.current);
428+
if (isFinished) {
429+
return;
430+
}
431+
432+
return new Promise<void>((resolve, reject) => {
433+
const stateChangedHandler = (state: AgentState) => {
434+
const { isFinished } = generateDerivedStateValues(state);
435+
if (!isFinished) {
436+
return;
437+
}
438+
cleanup();
439+
resolve();
440+
};
441+
const abortHandler = () => {
442+
cleanup();
443+
reject(new Error('useAgent(/* ... */).waitUntilFinished - signal aborted'));
444+
};
445+
446+
const cleanup = () => {
447+
emitter.off(AgentEvent.StateChanged, stateChangedHandler);
448+
signal?.removeEventListener('abort', abortHandler);
449+
};
450+
451+
emitter.on(AgentEvent.StateChanged, stateChangedHandler);
452+
signal?.addEventListener('abort', abortHandler);
453+
});
454+
},
455+
[emitter],
456+
);
457+
458+
return { waitUntilConnected, waitUntilCouldBeListening, waitUntilFinished };
459+
}
460+
348461
/**
349462
* useAgent encapculates all agent state, normalizing some quirks around how LiveKit Agents work.
350463
* @public
@@ -675,104 +788,8 @@ export function useAgent(session?: SessionStub): UseAgentReturn {
675788
}
676789
}, [agentParticipantAttributes, emitter, agentParticipant, state, videoTrack, audioTrack]);
677790

678-
const waitUntilConnected = React.useCallback(
679-
async (signal?: AbortSignal) => {
680-
const { isConnected } = generateDerivedStateValues(state);
681-
if (isConnected) {
682-
return;
683-
}
684-
685-
return new Promise<void>((resolve, reject) => {
686-
const stateChangedHandler = (state: AgentState) => {
687-
const { isConnected } = generateDerivedStateValues(state);
688-
if (!isConnected) {
689-
return;
690-
}
691-
cleanup();
692-
resolve();
693-
};
694-
const abortHandler = () => {
695-
cleanup();
696-
reject(new Error('useAgent.waitUntilConnected - signal aborted'));
697-
};
698-
699-
const cleanup = () => {
700-
emitter.off(AgentEvent.StateChanged, stateChangedHandler);
701-
signal?.removeEventListener('abort', abortHandler);
702-
};
703-
704-
emitter.on(AgentEvent.StateChanged, stateChangedHandler);
705-
signal?.addEventListener('abort', abortHandler);
706-
});
707-
},
708-
[state, emitter],
709-
);
710-
711-
const waitUntilCouldBeListening = React.useCallback(
712-
async (signal?: AbortSignal) => {
713-
const { canListen } = generateDerivedStateValues(state);
714-
if (canListen) {
715-
return;
716-
}
717-
718-
return new Promise<void>((resolve, reject) => {
719-
const stateChangedHandler = (state: AgentState) => {
720-
const { canListen } = generateDerivedStateValues(state);
721-
if (!canListen) {
722-
return;
723-
}
724-
cleanup();
725-
resolve();
726-
};
727-
const abortHandler = () => {
728-
cleanup();
729-
reject(new Error('useAgent.waitUntilCouldBeListening - signal aborted'));
730-
};
731-
732-
const cleanup = () => {
733-
emitter.off(AgentEvent.StateChanged, stateChangedHandler);
734-
signal?.removeEventListener('abort', abortHandler);
735-
};
736-
737-
emitter.on(AgentEvent.StateChanged, stateChangedHandler);
738-
signal?.addEventListener('abort', abortHandler);
739-
});
740-
},
741-
[state, emitter],
742-
);
743-
744-
const waitUntilFinished = React.useCallback(
745-
async (signal?: AbortSignal) => {
746-
const { isFinished } = generateDerivedStateValues(state);
747-
if (isFinished) {
748-
return;
749-
}
750-
751-
return new Promise<void>((resolve, reject) => {
752-
const stateChangedHandler = (state: AgentState) => {
753-
const { isFinished } = generateDerivedStateValues(state);
754-
if (!isFinished) {
755-
return;
756-
}
757-
cleanup();
758-
resolve();
759-
};
760-
const abortHandler = () => {
761-
cleanup();
762-
reject(new Error('useAgent.waitUntilFinished - signal aborted'));
763-
};
764-
765-
const cleanup = () => {
766-
emitter.off(AgentEvent.StateChanged, stateChangedHandler);
767-
signal?.removeEventListener('abort', abortHandler);
768-
};
769-
770-
emitter.on(AgentEvent.StateChanged, stateChangedHandler);
771-
signal?.addEventListener('abort', abortHandler);
772-
});
773-
},
774-
[state, emitter],
775-
);
791+
const { waitUntilConnected, waitUntilCouldBeListening, waitUntilFinished } =
792+
useAgentWaitUntilDerivedStates(emitter, state);
776793

777794
const waitUntilCamera = React.useCallback(
778795
(signal?: AbortSignal) => {
@@ -786,7 +803,7 @@ export function useAgent(session?: SessionStub): UseAgentReturn {
786803
};
787804
const abortHandler = () => {
788805
cleanup();
789-
reject(new Error('useAgent.waitUntilCamera - signal aborted'));
806+
reject(new Error('useAgent(/* ... */).waitUntilCamera - signal aborted'));
790807
};
791808

792809
const cleanup = () => {
@@ -813,7 +830,7 @@ export function useAgent(session?: SessionStub): UseAgentReturn {
813830
};
814831
const abortHandler = () => {
815832
cleanup();
816-
reject(new Error('useAgent.waitUntilMicrophone - signal aborted'));
833+
reject(new Error('useAgent(/* ... */).waitUntilMicrophone - signal aborted'));
817834
};
818835

819836
const cleanup = () => {

packages/react/src/hooks/useSession.ts

Lines changed: 52 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,55 @@ function areTokenSourceFetchOptionsEqual(a: TokenSourceFetchOptions, b: TokenSou
185185
return true;
186186
}
187187

188+
/** Internal hook used by useSession to manage creating a function which can be used to wait
189+
* until the session is in a given state before resolving. */
190+
function useSessionWaitUntilConnectionState(
191+
emitter: TypedEventEmitter<SessionCallbacks>,
192+
connectionState: UseSessionReturn['connectionState'],
193+
) {
194+
const connectionStateRef = React.useRef(connectionState);
195+
React.useEffect(() => {
196+
connectionStateRef.current = connectionState;
197+
}, [connectionState]);
198+
199+
const waitUntilConnectionState = React.useCallback(
200+
async (state: UseSessionReturn['connectionState'], signal?: AbortSignal) => {
201+
if (connectionStateRef.current === state) {
202+
return;
203+
}
204+
205+
return new Promise<void>((resolve, reject) => {
206+
const onceEventOccurred = (newState: UseSessionReturn['connectionState']) => {
207+
if (newState !== state) {
208+
return;
209+
}
210+
cleanup();
211+
resolve();
212+
};
213+
const abortHandler = () => {
214+
cleanup();
215+
reject(
216+
new Error(
217+
`useSession(/* ... */).waitUntilConnectionState(${state}, /* signal */) - signal aborted`,
218+
),
219+
);
220+
};
221+
222+
const cleanup = () => {
223+
emitter.off(SessionEvent.ConnectionStateChanged, onceEventOccurred);
224+
signal?.removeEventListener('abort', abortHandler);
225+
};
226+
227+
emitter.on(SessionEvent.ConnectionStateChanged, onceEventOccurred);
228+
signal?.addEventListener('abort', abortHandler);
229+
});
230+
},
231+
[emitter],
232+
);
233+
234+
return waitUntilConnectionState;
235+
}
236+
188237
/** Internal hook used by useSession to manage creating a function that properly invokes
189238
* tokenSource.fetch(...) with any fetch options */
190239
function useSessionTokenSourceFetch(
@@ -433,35 +482,9 @@ export function useSession(
433482
emitter.emit(SessionEvent.ConnectionStateChanged, conversationState.connectionState);
434483
}, [emitter, conversationState.connectionState]);
435484

436-
const waitUntilConnectionState = React.useCallback(
437-
async (state: UseSessionReturn['connectionState'], signal?: AbortSignal) => {
438-
if (conversationState.connectionState === state) {
439-
return;
440-
}
441-
442-
return new Promise<void>((resolve, reject) => {
443-
const onceEventOccurred = (newState: UseSessionReturn['connectionState']) => {
444-
if (newState !== state) {
445-
return;
446-
}
447-
cleanup();
448-
resolve();
449-
};
450-
const abortHandler = () => {
451-
cleanup();
452-
reject(new Error(`AgentSession.waitUntilRoomState(${state}, ...) - signal aborted`));
453-
};
454-
455-
const cleanup = () => {
456-
emitter.off(SessionEvent.ConnectionStateChanged, onceEventOccurred);
457-
signal?.removeEventListener('abort', abortHandler);
458-
};
459-
460-
emitter.on(SessionEvent.ConnectionStateChanged, onceEventOccurred);
461-
signal?.addEventListener('abort', abortHandler);
462-
});
463-
},
464-
[conversationState.connectionState, emitter],
485+
const waitUntilConnectionState = useSessionWaitUntilConnectionState(
486+
emitter,
487+
conversationState.connectionState,
465488
);
466489

467490
const waitUntilConnected = React.useCallback(

0 commit comments

Comments
 (0)