@signalwire/core
Version:
Shared code for the SignalWire JS SDK
532 lines (473 loc) • 12.9 kB
text/typescript
import type { SagaIterator } from '@redux-saga/types'
import { BaseSession } from '../BaseSession'
import { SDKStore } from '../redux'
import {
GLOBAL_VIDEO_EVENTS,
INTERNAL_GLOBAL_VIDEO_EVENTS,
PRODUCT_PREFIXES,
} from './constants'
import type {
CustomSaga,
SessionChannel,
SwEventChannel,
} from '../redux/interfaces'
import type { URL as NodeURL } from 'node:url'
import {
AllOrNone,
ChatJSONRPCMethod,
MessagingJSONRPCMethod,
VoiceCallMethod,
ClientContextMethod,
} from '..'
type JSONRPCParams = Record<string, any>
type JSONRPCResult = Record<string, any>
type JSONRPCError = Record<string, any>
export type VertoMethod =
| 'verto.invite'
| 'verto.attach'
| 'verto.answer'
| 'verto.info'
| 'verto.display'
| 'verto.media'
| 'verto.event'
| 'verto.bye'
| 'verto.punt'
| 'verto.broadcast'
| 'verto.subscribe'
| 'verto.unsubscribe'
| 'verto.clientReady'
| 'verto.modify'
| 'verto.mediaParams'
| 'verto.prompt'
| 'jsapi'
| 'verto.stats'
| 'verto.ping'
| 'verto.pong'
| 'verto.announce'
export type WebRTCMethod = 'video.message' | 'webrtc.verto'
export type SubscriberMethod = 'subscriber.online' | 'subscriber.offline'
export type JSONRPCMethod =
| 'signalwire.connect'
| 'signalwire.ping'
| 'signalwire.disconnect'
| 'signalwire.event'
| 'signalwire.reauthenticate'
| 'signalwire.subscribe'
| 'signalwire.unsubscribe'
| SubscriberMethod
| WebRTCMethod
| RoomMethod
| FabricMethod
| VertoMethod
| ChatJSONRPCMethod
| MessagingJSONRPCMethod
| VoiceCallMethod
| ClientContextMethod
export type JSONRPCSubscribeMethod = Extract<
JSONRPCMethod,
'signalwire.subscribe' | 'chat.subscribe'
>
export type JSONRPCUnSubscribeMethod = Extract<
JSONRPCMethod,
'signalwire.unsubscribe'
>
export interface JSONRPCRequest {
jsonrpc: '2.0'
id: string
method: JSONRPCMethod
params?: JSONRPCParams
}
export interface JSONRPCResponse {
jsonrpc: '2.0'
id: string
result?: JSONRPCResult
error?: JSONRPCError
}
export interface BaseRPCResult extends Record<string, unknown> {
code: string
message: string
}
export interface SessionOptions {
/** @internal */
host?: string
/** SignalWire project id, e.g. `a10d8a9f-2166-4e82-56ff-118bc3a4840f` */
project?: string
/** SignalWire project token, e.g. `PT9e5660c101cd140a1c93a0197640a369cf5f16975a0079c9` */
token: string
/** SignalWire contexts, e.g. 'home', 'office'.. */
contexts?: string[]
/** An alias for contexts - Topics has more priority over contexts */
topics?: string[]
// From `LogLevelDesc` of loglevel to simplify our docs
/** logging level */
logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent'
/** Callback invoked by the SDK to fetch a new token for re-authentication */
onRefreshToken?(): Promise<string>
sessionChannel?: SessionChannel
instanceMap?: InstanceMap
}
export interface UserOptions extends SessionOptions {
/** @internal */
devTools?: boolean
/** @internal */
debug?: {
logWsTraffic?: boolean
}
/** @internal */
logger?: SDKLogger
}
export interface InternalUserOptions extends UserOptions {
workers?: SDKWorker<any>[]
}
/**
* `UserOptions` is exposed to the end user and `BaseClientOptions` is
* the interface we use internally that extends the options provided.
* @internal
*/
export interface BaseClientOptions extends UserOptions {
store: SDKStore
}
export interface BaseComponentOptions {
store: SDKStore
customSagas?: CustomSaga<any>[]
}
export interface BaseComponentOptionsWithPayload<
CustomPayload extends unknown
> {
store: SDKStore
customSagas?: CustomSaga<any>[]
payload: CustomPayload
}
export interface SessionRequestObject {
rpcRequest: JSONRPCRequest
resolve: (value: unknown) => void
reject: (value: unknown) => void
}
export type MediaAllowed = 'all' | 'audio-only' | 'video-only'
type MediaDirectionAllowed = 'none' | 'receive' | 'both'
type AudioAllowed = MediaDirectionAllowed
type VideoAllowed = MediaDirectionAllowed
export type VideoMeta = Record<string, any>
export interface VideoAuthorization {
type: 'video'
project: string
project_id: string
scopes: string[]
scope_id: string
resource: string
join_as: 'member' | 'audience'
user_name: string
room?: {
name: string
display_name: string
scopes: string[]
meta: VideoMeta
}
signature: string
expires_at?: number
media_allowed?: MediaAllowed
audio_allowed: AudioAllowed
video_allowed: VideoAllowed
meta: VideoMeta
}
export type ChatAuthorizationChannels = Record<
string,
{ read?: boolean; write?: boolean }
>
export interface ChatAuthorization {
type: 'chat'
channels: ChatAuthorizationChannels
expires_at: number
member_id: string
project: string
project_id: string
resource: string
scope_id: string
scopes: string[]
signature: string
space_id: string
ttl: number
}
export interface SATAuthorization {
jti: string
project_id: string
fabric_subscriber: {
// TODO: public ?
version: number
expires_at: number
subscriber_id: string
application_id: string
project_id: string
space_id: string
}
}
export type JWTAuthorization = VideoAuthorization | ChatAuthorization
export type Authorization = JWTAuthorization | SATAuthorization
export interface RPCConnectResult {
identity: string
authorization: Authorization
protocol: string
ice_servers?: RTCIceServer[]
}
export type SessionConstructor = typeof BaseSession
export type SessionAuthStatus =
| 'unknown'
| 'authorizing'
| 'authorized'
| 'unauthorized'
export type SessionStatus =
| 'unknown'
| 'idle'
| 'reconnecting'
| 'connected'
| 'disconnecting'
| 'disconnected'
| 'auth_error'
| 'expiring'
export type SessionEvents = `session.${SessionStatus}`
export type SessionActions = 'session.forceClose'
/**
* List of all the events the client can listen to.
* @internal
*/
export type ClientEvents = Record<SessionEvents, (params?: any) => void>
export type BaseConnectionState =
| 'active'
| 'answering'
| 'destroy'
| 'early'
| 'hangup'
| 'held'
| 'new'
| 'purge'
| 'recovering'
| 'requesting'
| 'ringing'
| 'trying'
export type SessionAuthError = {
code: number
message: string
}
/**
* List of all Room methods
*/
export type RoomMethod =
| 'video.room.get'
| 'video.rooms.get'
| 'video.list_available_layouts'
| 'video.hide_video_muted'
| 'video.show_video_muted'
| 'video.members.get'
| 'video.member.audio_mute'
| 'video.member.audio_unmute'
| 'video.member.video_mute'
| 'video.member.video_unmute'
| 'video.member.deaf'
| 'video.member.undeaf'
| 'video.member.set_input_volume'
| 'video.member.set_output_volume'
| 'video.member.set_input_sensitivity'
| 'video.member.set_position'
| 'video.member.remove'
| 'video.member.get_meta'
| 'video.member.set_meta'
| 'video.member.update_meta'
| 'video.member.delete_meta'
| 'video.get_meta'
| 'video.set_meta'
| 'video.update_meta'
| 'video.delete_meta'
| 'video.set_layout'
| 'video.set_position'
| 'video.recording.list'
| 'video.recording.start'
| 'video.recording.stop'
| 'video.recording.pause'
| 'video.recording.resume'
| 'video.playback.list'
| 'video.playback.start'
| 'video.playback.pause'
| 'video.playback.resume'
| 'video.playback.stop'
| 'video.playback.set_volume'
| 'video.playback.seek_absolute'
| 'video.playback.seek_relative'
| 'video.member.demote'
| 'video.member.promote'
| 'video.stream.list'
| 'video.stream.start'
| 'video.stream.stop'
| 'video.lock'
| 'video.unlock'
| 'video.member.raisehand'
| 'video.member.lowerhand'
| 'video.prioritize_handraise'
/**
* List of all Call Fabric methods
*/
export type FabricMethod =
| 'call.mute'
| 'call.unmute'
| 'call.deaf'
| 'call.undeaf'
| 'call.layout.list'
| 'call.member.list'
| 'call.member.remove'
| 'call.member.position.set'
| 'call.layout.set'
| 'call.microphone.volume.set'
| 'call.microphone.sensitivity.set'
| 'call.speaker.volume.set'
| 'call.lock'
| 'call.unlock'
| 'call.raisehand'
| 'call.lowerhand'
| 'call.audioflags.set'
export interface WebSocketClient {
addEventListener: WebSocket['addEventListener']
removeEventListener: WebSocket['removeEventListener']
send: WebSocket['send']
close: WebSocket['close']
readyState: WebSocket['readyState']
}
export interface NodeSocketClient extends WebSocketClient {
addEventListener(
method: 'open' | 'close' | 'error' | 'message',
cb: (event: any) => void,
options?: any
): void
removeEventListener(
method: 'open' | 'close' | 'error' | 'message',
cb: (event: any) => void
): void
send(data: any, cb?: (err?: Error) => void): void
}
/**
* There's a difference in `searchParams` between URL from
* `lib` and URL from `url` (node) that makes using the same
* not possible for us.
*/
export interface NodeSocketAdapter {
new (address: string | NodeURL, options?: any): NodeSocketClient
new (
address: string | NodeURL,
protocols?: string | string[],
options?: any
): NodeSocketClient
}
export interface WebSocketAdapter {
new (url: string | URL, protocols?: string | string[]): WebSocketClient
}
export type ExecuteParams = {
method: JSONRPCMethod
params: Record<string, any>
}
export interface ExecuteExtendedOptions<InputType, OutputType, ParamsType> {
/** To transform the resolved response */
transformResolve?: ExecuteTransform<InputType, OutputType>
/** To transform the rejected response */
transformReject?: ExecuteTransform<InputType, OutputType>
/** To transform the RPC execute params */
transformParams?: ExecuteTransform<ParamsType, ExecuteParams['params']>
}
export type ExecuteTransform<InputType = unknown, OutputType = unknown> = (
payload: InputType
) => OutputType
export type APIMethodsMap<T> = {
[k in keyof T]: PropertyDescriptor
}
export type RoomCustomMethods<T> = APIMethodsMap<T>
export type EventsPrefix = '' | (typeof PRODUCT_PREFIXES)[number]
/**
* See {@link GLOBAL_VIDEO_EVENTS} for the full list of events.
*/
export type GlobalVideoEvents = (typeof GLOBAL_VIDEO_EVENTS)[number]
export type InternalGlobalVideoEvents =
(typeof INTERNAL_GLOBAL_VIDEO_EVENTS)[number]
export type BaseEventHandler = (...args: any[]) => void
export type InternalChannels = {
swEventChannel: SwEventChannel
sessionChannel: SessionChannel
}
export type SDKWorkerHooks<
OnDone = (options?: any) => void,
OnFail = (options?: any) => void
> = AllOrNone<{
onDone: OnDone
onFail: OnFail
}>
export type InstanceMap = {
get: <T extends unknown>(key: string) => T
set: <T extends unknown>(key: string, value: T) => Map<string, T>
remove: <T extends unknown>(key: string) => Map<string, T>
getAll: () => [string, unknown][]
deleteAll: () => Map<string, unknown>
}
type SDKWorkerBaseParams<T> = {
channels: InternalChannels
instance: T
runSaga: any
/**
* TODO: rename `payload` with something more explicit or
* create derived types of `SDKWorkerParams` with specific arguments (?)
* @deprecated use `initialState`
*/
payload?: any
initialState?: any
getSession: () => BaseSession | null
instanceMap: InstanceMap
dispatcher?: (
type: any,
payload: any,
channel?: SwEventChannel | SessionChannel
) => SagaIterator
}
export type SDKWorkerParams<
T,
Hooks extends SDKWorkerHooks = SDKWorkerHooks
> = SDKWorkerBaseParams<T> & Hooks
export type SDKWorker<T, Hooks extends SDKWorkerHooks = SDKWorkerHooks> = (
params: SDKWorkerParams<T, Hooks>
) => SagaIterator<any>
export type SDKWorkerDefinition<Hooks extends SDKWorkerHooks = SDKWorkerHooks> =
{
worker: SDKWorker<any>
initialState?: any
} & Hooks
interface LogFn {
<T extends object>(obj: T, msg?: string, ...args: any[]): void
(obj: unknown, msg?: string, ...args: any[]): void
(msg: string, ...args: any[]): void
}
export interface SDKLogger {
fatal: LogFn
error: LogFn
warn: LogFn
info: LogFn
debug: LogFn
trace: LogFn
}
export interface WsTrafficOptions {
type: 'send' | 'recv' | 'http'
payload: JSONRPCResponse | JSONRPCRequest | any
}
export interface InternalSDKLogger extends SDKLogger {
wsTraffic: (options: WsTrafficOptions) => void
}
export type UpdateMediaDirection =
| 'sendonly'
| 'recvonly'
| 'sendrecv'
| 'inactive'
type EnabledUpdateMedia = {
direction: Extract<UpdateMediaDirection, 'sendonly' | 'sendrecv'>
constraints?: MediaTrackConstraints
}
type DisabledUpdateMedia = {
direction: Extract<UpdateMediaDirection, 'recvonly' | 'inactive'>
constraints?: never
}
type MediaControl = EnabledUpdateMedia | DisabledUpdateMedia
export type UpdateMediaParams =
| { audio: MediaControl; video?: MediaControl }
| { audio?: MediaControl; video: MediaControl }