import * as React from 'react';
import {
RpcError,
type RpcInvocationData,
type PerformRpcParams,
type Serializer,
isSerializer,
type SerializerInput,
type SerializerOutput,
serializers,
} from 'livekit-client';
import { useEnsureSession } from '../context';
import { isUseSessionReturn, type UseSessionReturn } from './useSession';
// ---------------------------------------------------------------------------
// RPC types
// ---------------------------------------------------------------------------
/** @beta */
export type RpcHandler = (
payload: Input,
data: RpcInvocationData,
) => Promise;
/**
* Base RPC call parameters with an arbitrary payload type (used when the payload
* will be serialized by a serializer).
*
* @beta
*/
export type RpcCallParams = Omit & { payload: Payload };
/**
* Options for {@link (useRpc:1)}.
* @beta
*/
export type UseRpcOptions = Serializer> = {
/** Only accept RPCs from this participant. Others will receive UNSUPPORTED_METHOD. */
fromIdentity?: string;
/**
* Serializer applied to the data coming in and leaving the handler. Defaults to `serializers.json()`
*/
serializer?: S;
};
// ---------------------------------------------------------------------------
// useRpc hook
// ---------------------------------------------------------------------------
/** @beta */
export type RpcPerformFn = {
/** Serializer-wrapped call: payload is serialized and response is parsed by the serializer. */
(
params: RpcCallParams ,
serializer: Serializer,
): Promise;
/** Plain call: payload is already a string, response is returned as a string. */
(params: PerformRpcParams): Promise;
};
/** @beta */
export type UseRpcReturn = {
perform: RpcPerformFn;
};
/**
* Hook for declarative RPC method registration and outbound RPC calls.
*
* Registers a handler for an incoming RPC method and returns a `performRpc`
* function for outbound calls. The handler is registered on mount and
* unregistered on unmount. Handler identity does not matter (captured by ref),
* so inline functions work without `useCallback`.
*
* @example
* ```tsx
* const { performRpc } = useRpc(session, "getUserLocation", async (payload: { highAccuracy: boolean }) => {
* const pos = await getPosition(payload.highAccuracy);
* return { lat: pos.coords.latitude, lng: pos.coords.longitude };
* });
* ```
*
* @beta
*/
export function useRpc>(
session: UseSessionReturn,
methodName: string,
handler: RpcHandler, SerializerOutput>,
options?: UseRpcOptions,
): UseRpcReturn;
/** @beta */
export function useRpc>(
methodName: string,
handler: RpcHandler, SerializerOutput>,
options?: UseRpcOptions,
): UseRpcReturn;
/** @beta */
export function useRpc(session: UseSessionReturn): UseRpcReturn;
/** @beta */
export function useRpc(): UseRpcReturn;
export function useRpc(
methodNameOrSession?: string | UseSessionReturn,
handlerOrMethodName?: RpcHandler | string,
optionsOrHandler?: UseRpcOptions> | RpcHandler,
maybeOptions?: UseRpcOptions>,
): UseRpcReturn {
let session: UseSessionReturn | undefined;
let methodName: string | undefined;
let handler: RpcHandler | undefined;
let options: UseRpcOptions> | undefined;
if (isUseSessionReturn(methodNameOrSession)) {
session = methodNameOrSession;
methodName = handlerOrMethodName as string;
handler = optionsOrHandler as RpcHandler;
options = maybeOptions;
} else {
methodName = methodNameOrSession;
handler = handlerOrMethodName as RpcHandler;
options = optionsOrHandler as UseRpcOptions;
}
const { room } = useEnsureSession(session);
// Ref that always holds the latest handler — updated synchronously on render
const handlerRef = React.useRef(handler);
handlerRef.current = handler;
// Ref that always holds the latest options
const optionsRef = React.useRef(options);
optionsRef.current = options;
React.useEffect(() => {
if (!methodName) {
return;
}
room.registerRpcMethod(methodName, async (data: RpcInvocationData) => {
const fromIdentity = optionsRef.current?.fromIdentity;
if (fromIdentity && data.callerIdentity !== fromIdentity) {
throw RpcError.builtIn(
'UNSUPPORTED_METHOD',
`Method not available for caller ${data.callerIdentity}`,
);
}
const currentHandler = handlerRef.current;
if (!currentHandler) {
throw RpcError.builtIn(
'APPLICATION_ERROR',
`No handler registered for method "${methodName}"`,
);
}
const serializer = optionsRef.current?.serializer ?? serializers.json();
let parsed;
try {
parsed = serializer.parse(data.payload);
} catch (e) {
throw RpcError.builtIn('APPLICATION_ERROR', `Failed to parse RPC payload: ${e}`);
}
const result = await currentHandler(parsed, data);
try {
return serializer.serialize(result);
} catch (e) {
throw RpcError.builtIn('APPLICATION_ERROR', `Failed to serialize RPC response: ${e}`);
}
});
return () => {
room.unregisterRpcMethod(methodName);
};
}, [room, methodName]);
// Stable rpc calling function
const performRpc: RpcPerformFn = React.useCallback(
async (
params: RpcCallParams,
serializer: Serializer = serializers.json(),
) => {
if (isSerializer(serializer)) {
let serialized: string;
try {
serialized = serializer.serialize(params.payload);
} catch (e) {
throw RpcError.builtIn('APPLICATION_ERROR', `Failed to serialize RPC payload: ${e}`);
}
const rawResponse = await room.localParticipant.performRpc({
destinationIdentity: params.destinationIdentity,
method: params.method,
payload: serialized,
responseTimeout: params.responseTimeout,
});
try {
return serializer.parse(rawResponse);
} catch (e) {
throw RpcError.builtIn('APPLICATION_ERROR', `Failed to parse RPC response: ${e}`);
}
} else {
return room.localParticipant.performRpc(params as PerformRpcParams);
}
},
[room],
);
return React.useMemo(() => ({ perform: performRpc }), [performRpc]);
}