├── .changeset ├── angry-bags-relax.md ├── blue-actors-change.md ├── chatty-feet-train.md ├── cold-beds-guess.md ├── config.json ├── easy-ravens-rhyme.md ├── late-snails-care.md ├── legal-ways-pick.md ├── lucky-hands-obey.md ├── moody-nails-begin.md ├── plain-steaks-wash.md ├── plenty-banks-dress.md ├── rich-bugs-own.md ├── seven-owls-run.md ├── small-wasps-carry.md ├── soft-shoes-bathe.md ├── spotty-showers-cough.md ├── tasty-buckets-check.md ├── tasty-dodos-study.md ├── witty-nights-vanish.md └── yummy-mice-sing.md ├── .editorconfig ├── .github ├── actions │ ├── build │ │ └── action.yml │ ├── install │ │ └── action.yml │ ├── playwright │ │ └── action.yml │ ├── report-test-result │ │ └── action.yml │ ├── setup-js-app │ │ └── action.yml │ ├── setup-node-app │ │ └── action.yml │ └── zrok │ │ └── action.yml ├── pull_request_template.md └── workflows │ ├── browser-js-production.yml │ ├── browser-js-staging.yml │ ├── realtime-api-production.yml │ ├── realtime-api-staging.yml │ ├── release-dev.yml │ ├── unit-tests.yml │ └── update-playgrounds.yml ├── .gitignore ├── .nvmrc ├── .prettierignore ├── .prettierrc.json ├── LICENSE ├── README.md ├── babel.config.js ├── internal ├── e2e-js │ ├── .env.example │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── assets │ │ └── sw-docs.y4m │ ├── fixtures.ts │ ├── global-setup.ts │ ├── package.json │ ├── playwright.config.ts │ ├── templates │ │ └── blank │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── v2vanilla.html │ ├── tests │ │ ├── buildVideoWithFabricSDK.spec.ts │ │ ├── buildVideoWithVideoSDK.spec.ts │ │ ├── callfabric │ │ │ ├── address.spec.ts │ │ │ ├── agent_customer.spec.ts │ │ │ ├── cleanup.spec.ts │ │ │ ├── conversation.spec.ts │ │ │ ├── deviceEvent.spec.ts │ │ │ ├── deviceState.spec.ts │ │ │ ├── holdunhold.spec.ts │ │ │ ├── mirrorVideo.spec.ts │ │ │ ├── mirrorVideo.spec.ts-snapshots │ │ │ │ ├── video-flipped.png │ │ │ │ └── video-normal.png │ │ │ ├── raiseHand.spec.ts │ │ │ ├── reattach.spec.ts │ │ │ ├── relayApp.spec.ts │ │ │ ├── renegotiateAudio.spec.ts │ │ │ ├── renegotiateVideo.spec.ts │ │ │ ├── swml.spec.ts │ │ │ ├── videoRoom.spec.ts │ │ │ ├── videoRoomLayout.spec.ts │ │ │ └── websocket_reconnect.spec.ts │ │ ├── chat.spec.ts │ │ ├── pubSub.spec.ts │ │ ├── roomSession.spec.ts │ │ ├── roomSessionAudienceCount.spec.ts │ │ ├── roomSessionAutomaticStream.spec.ts │ │ ├── roomSessionBadNetwork.spec.ts │ │ ├── roomSessionCleanup.spec.ts │ │ ├── roomSessionDemote.spec.ts │ │ ├── roomSessionDemoteAudience.spec.ts │ │ ├── roomSessionDemotePromote.spec.ts │ │ ├── roomSessionDemoteReattachPromote.spec.ts │ │ ├── roomSessionDevices.spec.ts │ │ ├── roomSessionFollowLeader.spec.ts │ │ ├── roomSessionJoinFrom.spec.ts │ │ ├── roomSessionJoinUntil.spec.ts │ │ ├── roomSessionLocalStream.spec.ts │ │ ├── roomSessionLockUnlock.spec.ts │ │ ├── roomSessionMaxMembers.spec.ts │ │ ├── roomSessionMethodsOnNonExistingMembers.spec.ts │ │ ├── roomSessionMultipleStreams.spec.ts │ │ ├── roomSessionPromoteDemote.spec.ts │ │ ├── roomSessionPromoteMeta.spec.ts │ │ ├── roomSessionPromoteParticipant.spec.ts │ │ ├── roomSessionPromoteReattachDemote.spec.ts │ │ ├── roomSessionRaiseHand.spec.ts │ │ ├── roomSessionReattach.spec.ts │ │ ├── roomSessionReattachBadAuth.spec.ts │ │ ├── roomSessionReattachMultiple.spec.ts │ │ ├── roomSessionReattachScreenshare.spec.ts │ │ ├── roomSessionReattachWrongCallId.spec.ts │ │ ├── roomSessionReattachWrongProtocol.spec.ts │ │ ├── roomSessionRemoveAfterSecondsElapsed.spec.ts │ │ ├── roomSessionRemoveAllMembers.spec.ts │ │ ├── roomSessionRemoveAt.spec.ts │ │ ├── roomSessionStreaming.spec.ts │ │ ├── roomSessionStreamingAPI.spec.ts │ │ ├── roomSessionTalkingEventsParticipant.spec.ts │ │ ├── roomSessionTalkingEventsToAudience.spec.ts │ │ ├── roomSessionUnauthorized.spec.ts │ │ ├── roomSessionUpdateMedia.spec.ts │ │ ├── roomSettings.spec.ts │ │ └── v2Webrtc │ │ │ ├── v2WebrtcFromRest.spec.ts │ │ │ └── webrtcCalling.spec.ts │ ├── tsconfig.json │ └── utils.ts ├── e2e-realtime-api │ ├── .env.example │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── global-setup.ts │ ├── package.json │ ├── playwright.config.ts │ ├── src │ │ ├── chat.test.ts │ │ ├── messaging.test.ts │ │ ├── playwright │ │ │ ├── video.test.ts │ │ │ ├── videoHandRaise.test.ts │ │ │ └── videoUtils.ts │ │ ├── pubSub.test.ts │ │ ├── task.test.ts │ │ ├── utils.ts │ │ ├── voice.test.ts │ │ ├── voiceCollect │ │ │ ├── withAllListeners.test.ts │ │ │ ├── withCallListeners.test.ts │ │ │ ├── withCollectListeners.test.ts │ │ │ ├── withContinuousFalsePartialFalse.test.ts │ │ │ ├── withContinuousFalsePartialTrue&EarlyHangup.test.ts │ │ │ ├── withContinuousFalsePartialTrue.test.ts │ │ │ ├── withContinuousTruePartialFalse.test.ts │ │ │ ├── withContinuousTruePartialTrue.test.ts │ │ │ └── withDialListeners.test.ts │ │ ├── voiceDetect │ │ │ ├── withAllListeners.test.ts │ │ │ ├── withCallListeners.test.ts │ │ │ ├── withDetectListeners.test.ts │ │ │ └── withDialListeners.test.ts │ │ ├── voicePass.test.ts │ │ ├── voicePlayback │ │ │ ├── withAllListeners.test.ts │ │ │ ├── withCallListeners.test.ts │ │ │ ├── withDialListeners.test.ts │ │ │ ├── withMultiplePlaybacks.test.ts │ │ │ └── withPlaybackListeners.test.ts │ │ ├── voicePrompt │ │ │ ├── withAllListeners.test.ts │ │ │ ├── withCallListeners.test.ts │ │ │ ├── withDialListeners.test.ts │ │ │ └── withPromptListeners.test.ts │ │ ├── voiceRecord │ │ │ ├── withAllListeners.test.ts │ │ │ ├── withCallListeners.test.ts │ │ │ ├── withDialListeners.test.ts │ │ │ ├── withMultipleRecords.test.ts │ │ │ └── withRecordListeners.test.ts │ │ └── voiceTapAllListeners.test.ts │ ├── templates │ │ └── blank │ │ │ ├── index.html │ │ │ └── index.js │ └── utils.ts ├── playground-js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── example.env │ ├── index.html │ ├── package.json │ ├── src │ │ ├── chat │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── fabric-callee │ │ │ ├── firebase-messaging-sw.js │ │ │ ├── index.html │ │ │ ├── index.js │ │ │ └── sw.js │ │ ├── fabric-http │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── fabric │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── pubSub │ │ │ ├── index.html │ │ │ └── index.js │ │ ├── video │ │ │ ├── index.html │ │ │ └── index.js │ │ └── videoManager │ │ │ ├── index.html │ │ │ └── index.js │ └── vite.config.ts ├── playground-realtime-api │ ├── .env.example │ ├── .gitignore │ ├── CHANGELOG.md │ ├── index.js │ ├── package.json │ └── src │ │ ├── basic │ │ └── index.ts │ │ ├── chat │ │ └── index.ts │ │ ├── messaging │ │ └── index.ts │ │ ├── pubSub │ │ └── index.ts │ │ ├── task │ │ └── index.ts │ │ ├── voice-dtmf-loop │ │ └── index.ts │ │ ├── voice-inbound │ │ └── index.ts │ │ ├── voice │ │ └── index.ts │ │ └── with-events │ │ └── index.ts ├── playground-swaig │ ├── CHANGELOG.md │ ├── index.js │ ├── package.json │ └── src │ │ └── index.ts └── stack-tests │ ├── .gitignore │ ├── CHANGELOG.md │ ├── index.js │ ├── package.json │ └── src │ ├── chat │ ├── app.ts │ └── package.json │ ├── close-event │ ├── app.ts │ └── package.json │ ├── messaging │ ├── app.ts │ └── package.json │ ├── pubSub │ ├── app.ts │ └── package.json │ ├── task │ ├── app.ts │ └── package.json │ ├── video │ ├── app.ts │ └── package.json │ └── voice │ ├── app.ts │ └── package.json ├── package-lock.json ├── package.json ├── packages ├── compatibility-api │ └── README.md ├── core │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── BaseClient.ts │ │ ├── BaseComponent.test.ts │ │ ├── BaseComponent.ts │ │ ├── BaseConsumer.test.ts │ │ ├── BaseConsumer.ts │ │ ├── BaseJWTSession.test.ts │ │ ├── BaseJWTSession.ts │ │ ├── BaseSession.test.ts │ │ ├── BaseSession.ts │ │ ├── CustomErrors.ts │ │ ├── RPCMessages │ │ │ ├── RPCConnect.ts │ │ │ ├── RPCDisconnect.ts │ │ │ ├── RPCEventAck.ts │ │ │ ├── RPCExecute.ts │ │ │ ├── RPCPing.ts │ │ │ ├── RPCReauthenticate.ts │ │ │ ├── VertoMessages.ts │ │ │ ├── helpers.ts │ │ │ ├── index.test.ts │ │ │ └── index.ts │ │ ├── chat │ │ │ ├── BaseChat.ts │ │ │ ├── ChatMember.ts │ │ │ ├── ChatMessage.ts │ │ │ ├── applyCommonMethods.ts │ │ │ ├── index.ts │ │ │ ├── methods.ts │ │ │ ├── utils │ │ │ │ ├── index.ts │ │ │ │ ├── toInternalChatChannels.test.ts │ │ │ │ └── toInternalChatChannels.ts │ │ │ └── workers │ │ │ │ ├── chatWorker.ts │ │ │ │ └── index.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── memberPosition │ │ │ ├── index.ts │ │ │ ├── workers.test.ts │ │ │ └── workers.ts │ │ ├── pubSub │ │ │ ├── BasePubSub.ts │ │ │ ├── PubSubMessage.ts │ │ │ ├── index.ts │ │ │ └── workers │ │ │ │ ├── index.ts │ │ │ │ └── pubSubWorker.ts │ │ ├── redux │ │ │ ├── actions.ts │ │ │ ├── connect.test.ts │ │ │ ├── connect.ts │ │ │ ├── features │ │ │ │ ├── component │ │ │ │ │ ├── componentSaga.test.ts │ │ │ │ │ ├── componentSaga.ts │ │ │ │ │ ├── componentSelectors.test.ts │ │ │ │ │ ├── componentSelectors.ts │ │ │ │ │ ├── componentSlice.test.ts │ │ │ │ │ └── componentSlice.ts │ │ │ │ ├── index.ts │ │ │ │ └── session │ │ │ │ │ ├── sessionSaga.ts │ │ │ │ │ ├── sessionSelectors.ts │ │ │ │ │ ├── sessionSlice.test.ts │ │ │ │ │ └── sessionSlice.ts │ │ │ ├── index.ts │ │ │ ├── interfaces.ts │ │ │ ├── rootReducer.ts │ │ │ ├── rootSaga.test.ts │ │ │ ├── rootSaga.ts │ │ │ ├── toolkit │ │ │ │ ├── configureStore.ts │ │ │ │ ├── createAction.ts │ │ │ │ ├── createReducer.ts │ │ │ │ ├── createSlice.ts │ │ │ │ ├── devtoolsExtension.ts │ │ │ │ ├── getDefaultMiddleware.ts │ │ │ │ ├── index.ts │ │ │ │ ├── isPlainObject.ts │ │ │ │ ├── mapBuilders.ts │ │ │ │ ├── tsHelpers.ts │ │ │ │ └── utils.ts │ │ │ └── utils │ │ │ │ ├── createDestroyableSlice.test.ts │ │ │ │ ├── createDestroyableSlice.ts │ │ │ │ ├── sagaHelpers.ts │ │ │ │ ├── useInstanceMap.ts │ │ │ │ └── useSession.ts │ │ ├── rooms │ │ │ ├── RoomSessionPlayback.test.ts │ │ │ ├── RoomSessionPlayback.ts │ │ │ ├── RoomSessionRecording.test.ts │ │ │ ├── RoomSessionRecording.ts │ │ │ ├── RoomSessionStream.test.ts │ │ │ ├── RoomSessionStream.ts │ │ │ ├── index.ts │ │ │ ├── methods.test.ts │ │ │ └── methods.ts │ │ ├── setupTests.ts │ │ ├── testUtils.ts │ │ ├── types │ │ │ ├── cantina.ts │ │ │ ├── chat.ts │ │ │ ├── common.ts │ │ │ ├── conversation.ts │ │ │ ├── fabric.ts │ │ │ ├── fabricLayout.ts │ │ │ ├── fabricMember.ts │ │ │ ├── fabricRoomSession.ts │ │ │ ├── index.ts │ │ │ ├── messaging.ts │ │ │ ├── pubSub.ts │ │ │ ├── task.ts │ │ │ ├── utils.ts │ │ │ ├── video.ts │ │ │ ├── videoLayout.ts │ │ │ ├── videoMember.ts │ │ │ ├── videoPlayback.ts │ │ │ ├── videoRecording.ts │ │ │ ├── videoRoomDevice.ts │ │ │ ├── videoRoomSession.ts │ │ │ ├── videoStream.ts │ │ │ ├── voice.ts │ │ │ ├── voiceCall.ts │ │ │ ├── voiceCollect.ts │ │ │ ├── voiceConnect.ts │ │ │ ├── voiceDetect.ts │ │ │ ├── voicePlayback.ts │ │ │ ├── voicePrompt.ts │ │ │ ├── voiceRecording.ts │ │ │ ├── voiceSendDigits.ts │ │ │ └── voiceTap.ts │ │ ├── utils │ │ │ ├── EventEmitter.test.ts │ │ │ ├── EventEmitter.ts │ │ │ ├── SWCloseEvent.ts │ │ │ ├── asyncRetry.test.ts │ │ │ ├── asyncRetry.ts │ │ │ ├── common.ts │ │ │ ├── constants.ts │ │ │ ├── debounce.ts │ │ │ ├── eventUtils.ts │ │ │ ├── extendComponent.test.ts │ │ │ ├── extendComponent.ts │ │ │ ├── index.test.ts │ │ │ ├── index.ts │ │ │ ├── interfaces.ts │ │ │ ├── logger.test.ts │ │ │ ├── logger.ts │ │ │ ├── parseRPCResponse.test.ts │ │ │ ├── parseRPCResponse.ts │ │ │ ├── storage │ │ │ │ ├── index.native.ts │ │ │ │ └── index.ts │ │ │ ├── toExternalJSON.test.ts │ │ │ ├── toExternalJSON.ts │ │ │ ├── toInternalAction.test.ts │ │ │ ├── toInternalAction.ts │ │ │ ├── toInternalEventName.test.ts │ │ │ ├── toInternalEventName.ts │ │ │ ├── toSnakeCaseKeys.test.ts │ │ │ └── toSnakeCaseKeys.ts │ │ └── workers │ │ │ ├── executeActionWorker.ts │ │ │ └── index.ts │ └── tsconfig.build.json ├── js │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── BaseRoomSession.ts │ │ ├── Client.ts │ │ ├── JWTSession.test.ts │ │ ├── JWTSession.ts │ │ ├── RoomSessionDevice.test.ts │ │ ├── RoomSessionDevice.ts │ │ ├── RoomSessionScreenShare.test.ts │ │ ├── RoomSessionScreenShare.ts │ │ ├── VideoOverlays.ts │ │ ├── buildVideoElement.test.ts │ │ ├── buildVideoElement.ts │ │ ├── cantina │ │ │ ├── VideoManager.test.ts │ │ │ ├── VideoManager.ts │ │ │ ├── index.ts │ │ │ └── workers │ │ │ │ ├── index.ts │ │ │ │ ├── videoManagerRoomWorker.ts │ │ │ │ ├── videoManagerRoomsWorker.ts │ │ │ │ └── videoManagerWorker.ts │ │ ├── chat │ │ │ ├── Client.test.ts │ │ │ ├── Client.ts │ │ │ └── index.ts │ │ ├── createClient.test.ts │ │ ├── createClient.ts │ │ ├── createRoomObject.ts │ │ ├── fabric │ │ │ ├── Conversation.test.ts │ │ │ ├── Conversation.ts │ │ │ ├── ConversationAPI.ts │ │ │ ├── FabricRoomSession.test.ts │ │ │ ├── FabricRoomSession.ts │ │ │ ├── FabricRoomSessionMember.ts │ │ │ ├── HTTPClient.test.ts │ │ │ ├── HTTPClient.ts │ │ │ ├── IncomingCallManager.test.ts │ │ │ ├── IncomingCallManager.ts │ │ │ ├── SATSession.test.ts │ │ │ ├── SATSession.ts │ │ │ ├── SignalWire.test.ts │ │ │ ├── SignalWire.ts │ │ │ ├── WSClient.ts │ │ │ ├── createHttpClient.ts │ │ │ ├── createWSClient.ts │ │ │ ├── index.ts │ │ │ ├── interfaces │ │ │ │ ├── address.ts │ │ │ │ ├── capabilities.ts │ │ │ │ ├── conversation.ts │ │ │ │ ├── device.ts │ │ │ │ ├── httpClient.ts │ │ │ │ ├── incomingCallManager.ts │ │ │ │ ├── index.ts │ │ │ │ └── wsClient.ts │ │ │ ├── utils │ │ │ │ ├── capabilitiesHelpers.test.ts │ │ │ │ ├── capabilitiesHelpers.ts │ │ │ │ ├── constants.ts │ │ │ │ ├── eventMappers.ts │ │ │ │ ├── typeGuard.ts │ │ │ │ ├── validationProxy.ts │ │ │ │ └── validators.ts │ │ │ └── workers │ │ │ │ ├── callJoinWorker.ts │ │ │ │ ├── callLeftWorker.ts │ │ │ │ ├── callSegmentWorker.ts │ │ │ │ ├── conversationWorker.ts │ │ │ │ ├── fabricMemberWorker.ts │ │ │ │ ├── fabricWorker.ts │ │ │ │ ├── index.ts │ │ │ │ └── wsClientWorker.ts │ │ ├── features │ │ │ ├── actions.ts │ │ │ └── mediaElements │ │ │ │ └── mediaElementsSagas.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── joinRoom.ts │ │ ├── pubSub │ │ │ ├── Client.test.ts │ │ │ ├── Client.ts │ │ │ └── index.ts │ │ ├── setupTests.ts │ │ ├── testUtils.ts │ │ ├── utils │ │ │ ├── CloseEvent.ts │ │ │ ├── audioElement.ts │ │ │ ├── constants.ts │ │ │ ├── interfaces │ │ │ │ ├── base.ts │ │ │ │ ├── fabric.ts │ │ │ │ ├── index.ts │ │ │ │ └── video.ts │ │ │ ├── makeQueryParamsUrl.ts │ │ │ ├── paginatedResult.ts │ │ │ ├── roomSession.test.ts │ │ │ ├── roomSession.ts │ │ │ ├── storage.test.ts │ │ │ ├── storage.ts │ │ │ └── videoElement.ts │ │ ├── video │ │ │ ├── RoomSession.test.ts │ │ │ ├── RoomSession.ts │ │ │ ├── VideoRoomSession.test.ts │ │ │ ├── VideoRoomSession.ts │ │ │ ├── index.ts │ │ │ └── workers │ │ │ │ ├── childMemberJoinedWorker.test.ts │ │ │ │ ├── childMemberJoinedWorker.ts │ │ │ │ ├── index.ts │ │ │ │ ├── memberListUpdatedWorker.ts │ │ │ │ ├── videoPlaybackWorker.ts │ │ │ │ ├── videoRecordWorker.ts │ │ │ │ ├── videoStreamWorker.ts │ │ │ │ └── videoWorker.ts │ │ └── webrtc.ts │ ├── test │ │ └── resolver.js │ └── tsconfig.build.json ├── node │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── index.test.ts │ │ └── index.ts │ └── tsconfig.build.json ├── realtime-api │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── __mocks__ │ │ └── ws.js │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── BaseNamespace.test.ts │ │ ├── BaseNamespace.ts │ │ ├── ListenSubscriber.test.ts │ │ ├── ListenSubscriber.ts │ │ ├── SWClient.test.ts │ │ ├── SWClient.ts │ │ ├── Session.ts │ │ ├── SignalWire.ts │ │ ├── chat │ │ │ ├── BaseChat.test.ts │ │ │ ├── BaseChat.ts │ │ │ ├── Chat.test.ts │ │ │ ├── Chat.ts │ │ │ └── workers │ │ │ │ ├── chatWorker.ts │ │ │ │ └── index.ts │ │ ├── client │ │ │ ├── Client.ts │ │ │ ├── clientConnect.ts │ │ │ ├── clientProxyFactory.test.ts │ │ │ ├── clientProxyFactory.ts │ │ │ ├── createClient.test.ts │ │ │ ├── createClient.ts │ │ │ ├── getClient.test.ts │ │ │ ├── getClient.ts │ │ │ ├── index.ts │ │ │ └── setupClient.ts │ │ ├── common │ │ │ └── clientContext.ts │ │ ├── configure │ │ │ └── index.ts │ │ ├── decoratePromise.test.ts │ │ ├── decoratePromise.ts │ │ ├── index.ts │ │ ├── messaging │ │ │ ├── Message.ts │ │ │ ├── Messaging.test.ts │ │ │ ├── Messaging.ts │ │ │ └── workers │ │ │ │ ├── index.ts │ │ │ │ └── messagingWorker.ts │ │ ├── pubSub │ │ │ ├── PubSub.test.ts │ │ │ ├── PubSub.ts │ │ │ └── workers │ │ │ │ ├── index.ts │ │ │ │ └── pubSubWorker.ts │ │ ├── setupTests.ts │ │ ├── task │ │ │ ├── Task.test.ts │ │ │ ├── Task.ts │ │ │ └── workers │ │ │ │ ├── index.ts │ │ │ │ └── taskWorker.ts │ │ ├── testUtils.ts │ │ ├── types │ │ │ ├── chat.ts │ │ │ ├── index.ts │ │ │ ├── messaging.ts │ │ │ ├── pubSub.ts │ │ │ ├── task.ts │ │ │ ├── video.ts │ │ │ └── voice.ts │ │ ├── utils │ │ │ ├── internals.test.ts │ │ │ └── internals.ts │ │ ├── video │ │ │ ├── BaseVideo.ts │ │ │ ├── RoomSession.test.ts │ │ │ ├── RoomSession.ts │ │ │ ├── RoomSessionMember │ │ │ │ ├── RoomSessionMember.test.ts │ │ │ │ ├── RoomSessionMember.ts │ │ │ │ └── index.ts │ │ │ ├── RoomSessionPlayback │ │ │ │ ├── RoomSessionPlayback.test.ts │ │ │ │ ├── RoomSessionPlayback.ts │ │ │ │ ├── decoratePlaybackPromise.ts │ │ │ │ └── index.ts │ │ │ ├── RoomSessionRecording │ │ │ │ ├── RoomSessionRecording.test.ts │ │ │ │ ├── RoomSessionRecording.ts │ │ │ │ ├── decorateRecordingPromise.ts │ │ │ │ └── index.ts │ │ │ ├── RoomSessionStream │ │ │ │ ├── RoomSessionStream.test.ts │ │ │ │ ├── RoomSessionStream.ts │ │ │ │ ├── decorateStreamPromise.ts │ │ │ │ └── index.ts │ │ │ ├── Video.test.ts │ │ │ ├── Video.ts │ │ │ ├── methods │ │ │ │ ├── index.ts │ │ │ │ ├── methods.test.ts │ │ │ │ └── methods.ts │ │ │ └── workers │ │ │ │ ├── index.ts │ │ │ │ ├── videoCallingWorker.ts │ │ │ │ ├── videoLayoutWorker.ts │ │ │ │ ├── videoMemberWorker.ts │ │ │ │ ├── videoPlaybackWorker.ts │ │ │ │ ├── videoRecordingWorker.ts │ │ │ │ ├── videoRoomAudienceWorker.ts │ │ │ │ ├── videoRoomWorker.ts │ │ │ │ └── videoStreamWorker.ts │ │ └── voice │ │ │ ├── Call.test.ts │ │ │ ├── Call.ts │ │ │ ├── CallCollect │ │ │ ├── CallCollect.test.ts │ │ │ ├── CallCollect.ts │ │ │ ├── decorateCollectPromise.ts │ │ │ └── index.ts │ │ │ ├── CallDetect │ │ │ ├── CallDetect.test.ts │ │ │ ├── CallDetect.ts │ │ │ ├── decorateDetectPromise.ts │ │ │ └── index.ts │ │ │ ├── CallPlayback │ │ │ ├── CallPlayback.test.ts │ │ │ ├── CallPlayback.ts │ │ │ ├── decoratePlaybackPromise.ts │ │ │ └── index.ts │ │ │ ├── CallPrompt │ │ │ ├── CallPrompt.test.ts │ │ │ ├── CallPrompt.ts │ │ │ ├── decoratePromptPromise.ts │ │ │ └── index.ts │ │ │ ├── CallRecording │ │ │ ├── CallRecording.test.ts │ │ │ ├── CallRecording.ts │ │ │ ├── decorateRecordingPromise.ts │ │ │ └── index.ts │ │ │ ├── CallTap │ │ │ ├── CallTap.test.ts │ │ │ ├── CallTap.ts │ │ │ ├── decorateTapPromise.ts │ │ │ └── index.ts │ │ │ ├── DeviceBuilder.test.ts │ │ │ ├── DeviceBuilder.ts │ │ │ ├── Playlist.test.ts │ │ │ ├── Playlist.ts │ │ │ ├── Voice.test.ts │ │ │ ├── Voice.ts │ │ │ ├── utils.test.ts │ │ │ ├── utils.ts │ │ │ └── workers │ │ │ ├── VoiceCallSendDigitWorker.ts │ │ │ ├── handlers │ │ │ ├── callConnectEventsHandler.ts │ │ │ ├── callDialEventsHandler.ts │ │ │ ├── callStateEventsHandler.ts │ │ │ └── index.ts │ │ │ ├── index.ts │ │ │ ├── voiceCallCollectWorker.ts │ │ │ ├── voiceCallConnectWorker.ts │ │ │ ├── voiceCallDetectWorker.ts │ │ │ ├── voiceCallDialWorker.ts │ │ │ ├── voiceCallPlayWorker.ts │ │ │ ├── voiceCallReceiveWorker.ts │ │ │ ├── voiceCallRecordWorker.ts │ │ │ └── voiceCallTapWorker.ts │ └── tsconfig.build.json ├── swaig │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ │ ├── Server.ts │ │ ├── index.test.ts │ │ ├── index.ts │ │ ├── schemas.ts │ │ ├── swaig.test.ts │ │ ├── swaig.ts │ │ ├── types.ts │ │ └── utils.ts │ └── tsconfig.build.json ├── web-api │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── mocks │ │ ├── rest.ts │ │ └── server.ts │ ├── package.json │ ├── src │ │ ├── createRestClient.test.ts │ │ ├── createRestClient.ts │ │ ├── index.ts │ │ ├── rooms │ │ │ ├── createRoomFactory.ts │ │ │ ├── createVRTFactory.ts │ │ │ ├── deleteRoomFactory.ts │ │ │ ├── getRoomFactory.ts │ │ │ ├── listAllRoomsFactory.ts │ │ │ └── updateRoomFactory.ts │ │ ├── types.ts │ │ ├── utils │ │ │ ├── getConfig.test.ts │ │ │ ├── getConfig.ts │ │ │ ├── httpClient.test.ts │ │ │ └── httpClient.ts │ │ ├── validateRequest.test.ts │ │ └── validateRequest.ts │ └── tsconfig.build.json └── webrtc │ ├── .gitignore │ ├── CHANGELOG.md │ ├── README.md │ ├── jest.config.js │ ├── package.json │ ├── src │ ├── BaseConnection.ts │ ├── RTCPeer.ts │ ├── index.test.ts │ ├── index.ts │ ├── setupTests.ts │ ├── utils │ │ ├── constants.ts │ │ ├── deviceHelpers.test.ts │ │ ├── deviceHelpers.ts │ │ ├── enumerateDevices.ts │ │ ├── getDisplayMedia.ts │ │ ├── getUserMedia.ts │ │ ├── helpers.test.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ ├── interfaces.ts │ │ ├── permissions.ts │ │ ├── primitives.native.ts │ │ ├── primitives.ts │ │ ├── requestPermissions.ts │ │ ├── sdpHelpers.test.ts │ │ ├── sdpHelpers.ts │ │ ├── watchRTCPeerMediaPackets.ts │ │ └── webrtcHelpers.test.ts │ ├── webrtcMocks.ts │ └── workers │ │ ├── index.ts │ │ ├── promoteDemoteWorker.ts │ │ ├── roomSubscribedWorker.ts │ │ ├── sessionAuthWorker.ts │ │ ├── vertoEventWorker.test.ts │ │ └── vertoEventWorker.ts │ ├── tsconfig.build.json │ └── tsconfig.cjs.json ├── prepare_public_folder.sh ├── scripts ├── sw-build-all │ ├── CHANGELOG.md │ ├── bin │ │ └── cli.js │ ├── index.js │ └── package.json ├── sw-build │ ├── CHANGELOG.md │ ├── bin │ │ └── cli.js │ ├── index.js │ └── package.json ├── sw-common │ ├── CHANGELOG.md │ ├── index.js │ └── package.json ├── sw-release │ ├── CHANGELOG.md │ ├── bin │ │ └── cli.js │ ├── common.js │ ├── index.js │ ├── modes │ │ ├── beta.js │ │ ├── development.js │ │ ├── prepareProd.js │ │ └── production.js │ └── package.json └── sw-test │ ├── CHANGELOG.md │ ├── README.md │ ├── bin │ └── cli.js │ ├── index.js │ ├── package.json │ └── runNodeScript.js ├── sw.config.js ├── tsconfig.docs.json └── tsconfig.json /.changeset/angry-bags-relax.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': patch 3 | --- 4 | 5 | CF SDK: Optional `incomingCallHandler` parameter to `handlePushNotification()` 6 | -------------------------------------------------------------------------------- /.changeset/blue-actors-change.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Temporarily disabled convo api tests 6 | -------------------------------------------------------------------------------- /.changeset/chatty-feet-train.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Re-enable Conversation Room e2e tests 6 | -------------------------------------------------------------------------------- /.changeset/cold-beds-guess.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/core': minor 3 | '@signalwire/js': minor 4 | --- 5 | 6 | Added API request retries by default 7 | -------------------------------------------------------------------------------- /.changeset/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://unpkg.com/@changesets/config@1.5.0/schema.json", 3 | "changelog": [ 4 | "@changesets/changelog-github", 5 | { "repo": "signalwire/signalwire-js" } 6 | ], 7 | "commit": false, 8 | "linked": [], 9 | "access": "restricted", 10 | "baseBranch": "main", 11 | "updateInternalDependencies": "patch", 12 | "ignore": [] 13 | } 14 | -------------------------------------------------------------------------------- /.changeset/easy-ravens-rhyme.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/webrtc': patch 3 | '@signalwire/core': patch 4 | '@signalwire/js': patch 5 | --- 6 | 7 | Video & CF SDK: 8 | 9 | - Exposes a `cameraConstraints` and `microphoneConstraints` on the room/call object. 10 | 11 | CF SDK: 12 | 13 | - Introduces a validation proxy for the `FabricRoomSession` class. 14 | - Introduces a `CapabilityError` for the errors based on the missing capability. 15 | - Fixes the `setOutputVolume` API for Call Fabric. 16 | - Fixes the `setInputSensitivity` API param for Call Fabric. 17 | -------------------------------------------------------------------------------- /.changeset/late-snails-care.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/realtime-api': patch 3 | '@signalwire/core': patch 4 | --- 5 | 6 | Realtime-API: Bug Fix - Resubscribe topics/channels after WS reconnection 7 | 8 | Realtime-API Chat: Fix type interfaces for `getMessages` and `getMembers`. 9 | -------------------------------------------------------------------------------- /.changeset/legal-ways-pick.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': patch 3 | --- 4 | 5 | CF SDK: Fix Raise Hand call and member capability. 6 | -------------------------------------------------------------------------------- /.changeset/lucky-hands-obey.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': minor 3 | '@signalwire/webrtc': patch 4 | '@signalwire/core': patch 5 | --- 6 | 7 | CF SDK: Allow users to pass the `fromFabricAddressId` while dialing 8 | 9 | ```ts 10 | const call = await client.dial({ 11 | ....., 12 | to: ....., 13 | fromFabricAddressId: 'valid_subscriber_id', // Optional 14 | ... 15 | }) 16 | ``` 17 | -------------------------------------------------------------------------------- /.changeset/moody-nails-begin.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/webrtc': patch 3 | '@signalwire/core': patch 4 | '@signalwire/js': patch 5 | --- 6 | 7 | CF SDK: Fix the `getAddresses` TS contract with internal refactoring 8 | -------------------------------------------------------------------------------- /.changeset/plain-steaks-wash.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': patch 3 | --- 4 | 5 | resolves the user_name from the getChatMessages API 6 | -------------------------------------------------------------------------------- /.changeset/plenty-banks-dress.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/core': patch 3 | '@signalwire/js': patch 4 | --- 5 | 6 | CF SDK: Changed the type of the error param for the `expectedErrorHandler` internal handler 7 | -------------------------------------------------------------------------------- /.changeset/rich-bugs-own.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Fix conversation spec by making sure promise doesn't resolve on call logs conversation.message and also allow for GET messages response assert to include more than 2 messages in case they include call logs 6 | -------------------------------------------------------------------------------- /.changeset/seven-owls-run.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/web-api': patch 3 | --- 4 | 5 | Web-API: Upgrade @signalwire/compatibility-api package to the latest version 6 | -------------------------------------------------------------------------------- /.changeset/small-wasps-carry.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': patch 3 | --- 4 | 5 | CF SDK(bug): Include the missing Pagination type 6 | -------------------------------------------------------------------------------- /.changeset/soft-shoes-bathe.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Temporarily disable status webhook callback tests 6 | -------------------------------------------------------------------------------- /.changeset/spotty-showers-cough.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Added call status webhook test in v2Webrtc suite 6 | -------------------------------------------------------------------------------- /.changeset/tasty-buckets-check.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Renabled callfabric/conversation.spec.ts test suite 6 | -------------------------------------------------------------------------------- /.changeset/tasty-dodos-study.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': minor 3 | --- 4 | 5 | CF SDK: Support default media params 6 | 7 | ```ts 8 | await call.dial({ 9 | applyLocalVideoOverlay: false, // Should the SDK apply local video overlay? Default: true 10 | applyMemberOverlay: true, // Should the SDK apply member video overlays? Default: true 11 | stopCameraWhileMuted: true, // Should the SDK stop the camera when muted? Default: true 12 | stopMicrophoneWhileMuted: true, // Should the SDK stop the mic when muted? Default: true 13 | mirrorLocalVideoOverlay: false // Should the SDK mirror the local video overlay? Default: true 14 | }) 15 | ``` 16 | -------------------------------------------------------------------------------- /.changeset/witty-nights-vanish.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@sw-internal/e2e-js': patch 3 | --- 4 | 5 | Re-enable v2WebrtcFromRest's status callback tests and added better error handling and retry logic for zrok process 6 | -------------------------------------------------------------------------------- /.changeset/yummy-mice-sing.md: -------------------------------------------------------------------------------- 1 | --- 2 | '@signalwire/js': patch 3 | --- 4 | 5 | refactored gatChatMeassages to prevent multiple lookup of the same address 6 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | -------------------------------------------------------------------------------- /.github/actions/build/action.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | description: Build all the SDKs in the monorepo 3 | 4 | inputs: 5 | NODE_VERSION: 6 | required: false 7 | default: 20.x 8 | description: Node version to use 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - name: Cache dist folders 14 | id: cache 15 | uses: actions/cache@v4 16 | with: 17 | key: ${{ github.ref }}-${{ github.sha }} 18 | path: packages/*/dist 19 | - if: steps.cache.outputs.cache-hit != 'true' 20 | name: Build 21 | shell: bash 22 | run: npm run build 23 | -------------------------------------------------------------------------------- /.github/actions/install/action.yml: -------------------------------------------------------------------------------- 1 | name: Install 2 | description: Install dependencies for the monorepo 3 | 4 | inputs: 5 | NODE_VERSION: 6 | required: false 7 | default: 20.x 8 | description: Node version to use 9 | 10 | runs: 11 | using: composite 12 | steps: 13 | - name: Setup node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ inputs.NODE_VERSION }} 17 | cache: npm 18 | - name: Cache node_modules 19 | id: cache_node_modules 20 | uses: actions/cache@v4 21 | with: 22 | key: ${{ runner.os }}-node-modules-${{ inputs.NODE_VERSION }}-${{ hashFiles('**/package-lock.json') }} 23 | path: | 24 | node_modules 25 | packages/*/node_modules 26 | scripts/*/node_modules 27 | internal/*/node_modules 28 | - if: steps.cache_node_modules.outputs.cache-hit != 'true' 29 | name: Install Dependencies 30 | shell: bash 31 | run: npm ci 32 | -------------------------------------------------------------------------------- /.github/actions/playwright/action.yml: -------------------------------------------------------------------------------- 1 | name: Playwright 2 | description: Install playwright and required browsers 3 | 4 | inputs: 5 | PLAYWRIGHT_VERSION: 6 | required: true 7 | description: Playwright version to use 8 | 9 | runs: 10 | using: composite 11 | steps: 12 | - name: Cache playwright binaries 13 | uses: actions/cache@v4 14 | id: playwright-cache 15 | with: 16 | path: | 17 | ~/.cache/ms-playwright 18 | key: ${{ runner.os }}-playwright-${{ inputs.PLAYWRIGHT_VERSION }} 19 | 20 | - if: steps.playwright-cache.outputs.cache-hit != 'true' 21 | name: Install Playwright 22 | shell: bash 23 | run: npx playwright install --with-deps chromium 24 | -------------------------------------------------------------------------------- /.github/actions/setup-js-app/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup and Build JS 2 | description: Setup app, install JS dependencies and build 3 | 4 | inputs: 5 | NODE_VERSION: 6 | required: false 7 | default: "20.x" 8 | description: "Default node version" 9 | 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Setup node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ inputs.NODE_VERSION }} 17 | cache: 'npm' 18 | cache-dependency-path: '**/package-lock.json' 19 | - name: Install Dependencies 20 | shell: bash 21 | run: npm ci 22 | - name: Install Playwright 23 | shell: bash 24 | run: npx playwright install --with-deps chromium 25 | - name: Build 26 | shell: bash 27 | run: npm run build 28 | -------------------------------------------------------------------------------- /.github/actions/setup-node-app/action.yml: -------------------------------------------------------------------------------- 1 | name: Setup and Build Node 2 | description: Setup app, install node dependencies and build 3 | 4 | inputs: 5 | NODE_VERSION: 6 | required: false 7 | default: "20.x" 8 | description: "Default node version" 9 | 10 | runs: 11 | using: "composite" 12 | steps: 13 | - name: Setup node 14 | uses: actions/setup-node@v4 15 | with: 16 | node-version: ${{ inputs.NODE_VERSION }} 17 | cache: 'npm' 18 | cache-dependency-path: '**/package-lock.json' 19 | - name: Install Dependencies 20 | shell: bash 21 | run: npm install 22 | - name: Build 23 | shell: bash 24 | run: npm run build 25 | -------------------------------------------------------------------------------- /.github/actions/zrok/action.yml: -------------------------------------------------------------------------------- 1 | name: Zrok 2 | description: Install zrok 3 | 4 | inputs: 5 | ZROK_VERSION: 6 | required: false 7 | description: Zrok version to use 8 | default: 1.0.4 9 | ZROK_API_ENDPOINT: 10 | required: true 11 | description: Zrok apiEndpoint 12 | ZROK_TOKEN: 13 | required: true 14 | description: Zrok token 15 | runs: 16 | using: composite 17 | steps: 18 | - name: Install Zrok 19 | shell: bash 20 | run: | 21 | curl -L -O https://github.com/openziti/zrok/releases/download/v1.0.4/zrok_${{ inputs.ZROK_VERSION }}_linux_amd64.tar.gz 22 | mkdir /tmp/zrok && tar xvzf ./zrok_${{ inputs.ZROK_VERSION }}_linux_amd64.tar.gz -C /tmp/zrok 23 | mkdir -p /usr/local/bin && install /tmp/zrok/zrok /usr/local/bin/ 24 | zrok version 25 | zrok config set apiEndpoint ${{ inputs.ZROK_API_ENDPOINT }} 26 | zrok enable ${{ inputs.ZROK_TOKEN }} --headless 27 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | # Description 2 | 3 | Please include a summary of the change and which issue is fixed. 4 | 5 | ## Type of change 6 | 7 | - [ ] Internal refactoring 8 | - [ ] Bug fix (bugfix - non-breaking) 9 | - [ ] New feature (non-breaking change which adds functionality) 10 | - [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) 11 | 12 | ## Code snippets 13 | 14 | In case of new feature or breaking changes, please include code snippets. 15 | -------------------------------------------------------------------------------- /.github/workflows/release-dev.yml: -------------------------------------------------------------------------------- 1 | name: Release dev tag to NPM 2 | 3 | on: 4 | workflow_run: 5 | workflows: [Tests] 6 | branches: [main] 7 | types: 8 | - completed 9 | 10 | concurrency: 11 | group: ${{ github.workflow }}-${{ github.ref }} 12 | cancel-in-progress: true 13 | 14 | jobs: 15 | release_dev: 16 | if: ${{ github.event.workflow_run.conclusion == 'success' }} 17 | runs-on: ubuntu-latest 18 | strategy: 19 | matrix: 20 | node-version: [20.x] 21 | steps: 22 | - uses: actions/checkout@v4 23 | - uses: actions/setup-node@v4 24 | with: 25 | node-version: ${{ matrix.node-version }} 26 | registry-url: 'https://registry.npmjs.org' 27 | - run: npm i 28 | - run: npm run release:dev 29 | env: 30 | NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 31 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 32 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # misc 2 | *~ 3 | *.sw[mnpcod] 4 | *.log 5 | *.lock 6 | *.tmp 7 | *.tmp.* 8 | log.txt 9 | *.sublime-project 10 | *.sublime-workspace 11 | 12 | .idea/ 13 | .vscode/ 14 | .sass-cache/ 15 | .versions/ 16 | .devcontainer/ 17 | node_modules/ 18 | $RECYCLE.BIN/ 19 | 20 | .DS_Store 21 | Thumbs.db 22 | UserInterfaceState.xcuserstate 23 | .env 24 | .env.local 25 | .env.development.local 26 | .env.test.local 27 | .env.production.local 28 | .env.test 29 | 30 | npm-debug.log* 31 | yarn-debug.log* 32 | yarn-error.log* 33 | 34 | coverage/ 35 | public/ 36 | .devcontainer/ 37 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 14 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | build/* 2 | coverage 3 | dist/* 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 80, 3 | "trailingComma": "es5", 4 | "useTabs": false, 5 | "tabWidth": 2, 6 | "semi": false, 7 | "singleQuote": true, 8 | "jsxSingleQuote": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "always" 11 | } 12 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2018 SignalWire 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | ['@babel/preset-env', { targets: { node: 'current' } }], 4 | '@babel/preset-typescript', 5 | ], 6 | } 7 | -------------------------------------------------------------------------------- /internal/e2e-js/.env.example: -------------------------------------------------------------------------------- 1 | SW_TEST_CONFIG='{ 2 | "ignoreFiles": [], 3 | "env": { 4 | "API_HOST": "xyz.signalwire.com", 5 | "RELAY_HOST": "relay.signalwire.com", 6 | "RELAY_PROJECT": "xyz", 7 | "RELAY_TOKEN": "PTxyz", 8 | "PLAYBACK_URL": "http://xyz.test.mp4", 9 | "STREAMING_URL": "rtmp://a.rtmp.youtube.com/live2/111", 10 | "STREAM_CHECK_URL": "https://rtmp.example.com/stats", 11 | "RTMP_SERVER": "rtmp://a.rtmp.youtube.com/live2/", 12 | "RTMP_STREAM_NAME": "someName", 13 | "SAT_REFERENCE": "oauthReference", 14 | "VERTO_DOMAIN": "dev-1111.verto.example.com" 15 | } 16 | }' -------------------------------------------------------------------------------- /internal/e2e-js/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | test-results/ -------------------------------------------------------------------------------- /internal/e2e-js/assets/sw-docs.y4m: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalwire/signalwire-js/9d2638af57ccf52be93e3cc29224d5fb6c8ea6b5/internal/e2e-js/assets/sw-docs.y4m -------------------------------------------------------------------------------- /internal/e2e-js/global-setup.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test' 2 | import { createTestServer } from './utils' 3 | 4 | async function globalSetup(_config: FullConfig) { 5 | const server = await createTestServer() 6 | await server.start() 7 | 8 | return async () => { 9 | await server.close() 10 | } 11 | } 12 | 13 | export default globalSetup 14 | -------------------------------------------------------------------------------- /internal/e2e-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/e2e-js", 3 | "version": "0.0.19", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "sw-test --mode=playwright --pass-with-no-tests", 8 | "test:update:snapshots": "npm run dev --update-snapshots", 9 | "test": "" 10 | }, 11 | "dependencies": { 12 | "@sw-internal/playground-js": "0.0.15", 13 | "express": "^5.1.0", 14 | "node-fetch": "^2.6.7" 15 | }, 16 | "devDependencies": { 17 | "@playwright/test": "^1.52.0", 18 | "@types/express": "^5.0.1", 19 | "playwrigth-ws-inspector": "^1.0.0", 20 | "vite": "^6.2.0" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/e2e-js/templates/blank/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Signalwire Local Template 5 | 6 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /internal/e2e-js/templates/blank/index.js: -------------------------------------------------------------------------------- 1 | import * as SWJS from '@signalwire/js' 2 | 3 | window._SWJS = SWJS 4 | -------------------------------------------------------------------------------- /internal/e2e-js/tests/callfabric/mirrorVideo.spec.ts-snapshots/video-flipped.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalwire/signalwire-js/9d2638af57ccf52be93e3cc29224d5fb6c8ea6b5/internal/e2e-js/tests/callfabric/mirrorVideo.spec.ts-snapshots/video-flipped.png -------------------------------------------------------------------------------- /internal/e2e-js/tests/callfabric/mirrorVideo.spec.ts-snapshots/video-normal.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/signalwire/signalwire-js/9d2638af57ccf52be93e3cc29224d5fb6c8ea6b5/internal/e2e-js/tests/callfabric/mirrorVideo.spec.ts-snapshots/video-normal.png -------------------------------------------------------------------------------- /internal/e2e-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist", 6 | "rootDir": "../../" 7 | }, 8 | "include": ["./**/*.ts", "../../packages/js/src/**/*.ts", "./utils.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/.env.example: -------------------------------------------------------------------------------- 1 | SW_TEST_CONFIG='{ 2 | "ignoreFiles": [], 3 | "ignoreDirectories": [], 4 | "env": { 5 | "API_HOST": "xyz.signalwire.com", 6 | "RELAY_HOST": "relay.signalwire.com", 7 | "RELAY_PROJECT": "xyz", 8 | "RELAY_TOKEN": "PTxyz", 9 | "MESSAGING_FROM_NUMBER": "+1111", 10 | "MESSAGING_TO_NUMBER": "+1111", 11 | "MESSAGING_CONTEXT": "someContext", 12 | "DAPP_DOMAIN": "abc.signalwire.com", 13 | "PLAYBACK_URL": "http://xyz.test.mp4" 14 | } 15 | }' -------------------------------------------------------------------------------- /internal/e2e-realtime-api/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | node_modules/ 3 | /test-results/ 4 | /playwright-report/ 5 | /playwright/.cache/ 6 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/global-setup.ts: -------------------------------------------------------------------------------- 1 | import type { FullConfig } from '@playwright/test' 2 | import { createTestServer } from './utils' 3 | 4 | async function globalSetup(_config: FullConfig) { 5 | const server = await createTestServer() 6 | await server.start() 7 | 8 | return async () => { 9 | await server.close() 10 | } 11 | } 12 | 13 | export default globalSetup 14 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/e2e-realtime-api", 3 | "version": "0.1.14", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "dev:playwright": "sw-test --mode=playwright", 8 | "dev": "sw-test --mode=custom-node && npm run dev:playwright", 9 | "dev:rtonly": "sw-test --mode=custom-node", 10 | "test": "" 11 | }, 12 | "devDependencies": { 13 | "@playwright/test": "^1.35.1", 14 | "@types/tap": "^15.0.8", 15 | "esbuild-register": "^3.6.0", 16 | "vite": "^6.2.0" 17 | }, 18 | "dependencies": { 19 | "@mapbox/node-pre-gyp": "^1.0.10", 20 | "tap": "^18.7.2", 21 | "ws": "^8.17.1" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/playwright.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig, devices } from '@playwright/test' 2 | 3 | require('dotenv').config() 4 | 5 | const useDesktopChrome = { 6 | ...devices['Desktop Chrome'], 7 | launchOptions: { 8 | args: [ 9 | '--use-fake-ui-for-media-stream', 10 | '--use-fake-device-for-media-stream', 11 | ], 12 | }, 13 | } 14 | 15 | const config = defineConfig({ 16 | testDir: './src/playwright', 17 | reporter: process.env.CI ? 'github' : 'list', 18 | globalSetup: require.resolve('./global-setup'), 19 | testMatch: undefined, 20 | testIgnore: undefined, 21 | timeout: 60_000, 22 | expect: { 23 | timeout: 10_000, 24 | }, 25 | fullyParallel: true, 26 | // forbidOnly: !!process.env.CI, 27 | workers: 1, 28 | projects: [ 29 | { 30 | name: 'default', 31 | use: useDesktopChrome, 32 | }, 33 | ], 34 | }) 35 | 36 | export default config 37 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/templates/blank/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Signalwire Local Template 5 | 6 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/templates/blank/index.js: -------------------------------------------------------------------------------- 1 | import * as SWJS from '@signalwire/js' 2 | 3 | window._SWJS = SWJS 4 | -------------------------------------------------------------------------------- /internal/e2e-realtime-api/utils.ts: -------------------------------------------------------------------------------- 1 | import { createServer } from 'vite' 2 | 3 | export const SERVER_URL = 'http://localhost:1338' 4 | 5 | export const createTestServer = async () => { 6 | const targetOptions = { path: './templates/blank', port: 1338 } 7 | const server = await createServer({ 8 | configFile: false, 9 | root: targetOptions.path, 10 | server: { 11 | port: targetOptions.port, 12 | }, 13 | logLevel: 'silent', 14 | }) 15 | 16 | return { 17 | start: async () => { 18 | await server.listen() 19 | }, 20 | close: async () => { 21 | await server.close() 22 | }, 23 | url: `http://localhost:${targetOptions.port}`, 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /internal/playground-js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | dist-ssr 5 | *.local 6 | -------------------------------------------------------------------------------- /internal/playground-js/README.md: -------------------------------------------------------------------------------- 1 | # Playground 2 | 3 | After you've built the SDK at root, you can `cd` to this folder and run the playground applications with: 4 | 5 | ``` 6 | npm run dev 7 | ``` 8 | 9 | which will run a `vite` server on localhost (default port: 3000). 10 | 11 | Browse to the provided local URL (e.g. `http://localhost:3000/`) and select a playground app to use. 12 | -------------------------------------------------------------------------------- /internal/playground-js/example.env: -------------------------------------------------------------------------------- 1 | #Firebase Initilization Params 2 | VITE_FB_API_KEY= 3 | VITE_FB_AUTH_DOMAIN= 4 | VITE_FB_PROJECT_ID= 5 | VITE_FB_STORAGE_BUCKET= 6 | VITE_FB_MESSAGING_SENDER_ID= 7 | VITE_FB_APP_ID= 8 | VITE_FB_MEASUREMENT_ID= 9 | VITE_FB_VAPI_KEY= 10 | -------------------------------------------------------------------------------- /internal/playground-js/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 10 | 11 | SignalWire Playground 12 | 13 | 14 | 15 | 16 |
17 |
18 |

Playgrounds

19 |
20 |
23 |
26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 | 34 | 35 | -------------------------------------------------------------------------------- /internal/playground-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/playground-js", 3 | "version": "0.0.15", 4 | "private": true, 5 | "scripts": { 6 | "dev": "vite", 7 | "build": "vite build", 8 | "serve": "vite preview", 9 | "test": "" 10 | }, 11 | "dependencies": { 12 | "firebase": "^11.3.1", 13 | "pako": "^2.1.0" 14 | }, 15 | "devDependencies": { 16 | "vite": "^6.2.0" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /internal/playground-js/src/pubSub/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Signalwire Chat Demo 5 | 6 | 10 | 14 | 15 | 16 | 17 |

PubSub Test

18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /internal/playground-js/src/pubSub/index.js: -------------------------------------------------------------------------------- 1 | import { Chat as PubSub } from '@signalwire/js' 2 | 3 | window.connect = async ({ channels, host, token }) => { 4 | const pubSubClient = new PubSub.Client({ 5 | host, 6 | token, 7 | }) 8 | 9 | // .subscribe should be after .on but i left here for test. 10 | await pubSubClient.subscribe(channels) 11 | 12 | pubSubClient.on('message', (message) => { 13 | console.log( 14 | 'Received', 15 | message.content, 16 | 'on', 17 | message.channel, 18 | 'at', 19 | message.publishedAt 20 | ) 21 | }) 22 | 23 | await pubSubClient.publish({ 24 | channel: channels[0], 25 | content: 'Hello World', 26 | }) 27 | } 28 | -------------------------------------------------------------------------------- /internal/playground-js/src/videoManager/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Signalwire VideoManager Demo 5 | 6 | 10 | 14 | 15 | 16 | 17 |

VideoManager Test

18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /internal/playground-realtime-api/.env.example: -------------------------------------------------------------------------------- 1 | HOST=example.domain.com 2 | PROJECT=xxx 3 | TOKEN=yyy 4 | RELAY_CONTEXT=default 5 | FROM_NUMBER=+1xxx 6 | TO_NUMBER=+1yyy 7 | -------------------------------------------------------------------------------- /internal/playground-realtime-api/.gitignore: -------------------------------------------------------------------------------- 1 | .env 2 | -------------------------------------------------------------------------------- /internal/playground-realtime-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/playground-realtime-api", 3 | "version": "0.3.5", 4 | "private": true, 5 | "main": "index.js", 6 | "scripts": { 7 | "dev": "node -r dotenv/config index.js", 8 | "test": "" 9 | }, 10 | "devDependencies": { 11 | "esbuild-register": "^3.6.0", 12 | "inquirer": "^8.2.5", 13 | "node-watch": "^0.7.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/playground-realtime-api/src/basic/index.ts: -------------------------------------------------------------------------------- 1 | import { Video } from '@signalwire/realtime-api' 2 | 3 | async function run() { 4 | try { 5 | const video = new Video.Client({ 6 | // @ts-expect-error 7 | host: process.env.HOST, 8 | project: process.env.PROJECT as string, 9 | token: process.env.TOKEN as string, 10 | // debug: { logWsTraffic: true }, 11 | }) 12 | 13 | video.on('room.started', async (roomSession) => { 14 | console.log('Room started', roomSession.id) 15 | 16 | roomSession.on('member.updated', async (member) => { 17 | console.log(member) 18 | }) 19 | }) 20 | 21 | video.on('room.ended', async (roomSession) => { 22 | console.log('Room ended', roomSession.id) 23 | }) 24 | 25 | const { roomSessions } = await video.getRoomSessions() 26 | 27 | roomSessions.forEach(async (roomSession) => { 28 | console.log('RoomSession:', roomSession.id) 29 | const { recordings } = await roomSession.getRecordings() 30 | console.log('Recordings', recordings.length) 31 | recordings.forEach(async (rec) => { 32 | console.log('REC', rec.id, rec.roomSessionId, rec.state, rec.startedAt) 33 | if (rec.state === 'recording') { 34 | await rec.stop() 35 | } 36 | }) 37 | }) 38 | 39 | console.log('Client Running..') 40 | } catch (error) { 41 | console.log('', error) 42 | } 43 | } 44 | 45 | run() 46 | -------------------------------------------------------------------------------- /internal/playground-swaig/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @sw-internal/playground-swaig 2 | 3 | ## 0.1.1 4 | 5 | ### Patch Changes 6 | 7 | - [#1001](https://github.com/signalwire/signalwire-js/pull/1001) [`968d226b`](https://github.com/signalwire/signalwire-js/commit/968d226ba2791f44dea4bd1b0d173aefaf103bda) Thanks [@ayeminag](https://github.com/ayeminag)! - - API to fetch address by id and tests 8 | -------------------------------------------------------------------------------- /internal/playground-swaig/index.js: -------------------------------------------------------------------------------- 1 | const inquirer = require('inquirer') 2 | const watch = require('node-watch') 3 | const path = require('node:path') 4 | const fs = require('node:fs') 5 | const { spawn } = require('node:child_process') 6 | 7 | const runScript = (scriptFile) => { 8 | const child = spawn(...['node', ['-r', 'esbuild-register', scriptFile]]) 9 | 10 | child.stdout.on('data', (data) => { 11 | console.log(data.toString()) 12 | }) 13 | 14 | child.stderr.on('data', (data) => { 15 | console.error(`stderr: ${data}`) 16 | }) 17 | 18 | return child 19 | } 20 | 21 | async function main() { 22 | const playground = path.join(__dirname, './src/index.ts') 23 | 24 | let child 25 | watch( 26 | [ 27 | path.join(__dirname, '../../', 'packages/swaig/dist'), 28 | path.join(__dirname, 'src'), 29 | ], 30 | { recursive: true }, 31 | (_evt, name) => { 32 | if (name.endsWith('map')) { 33 | return 34 | } 35 | 36 | const newChild = runScript(playground) 37 | 38 | if (child) { 39 | child.stdin.pause() 40 | child.kill() 41 | } 42 | 43 | child = newChild 44 | } 45 | ) 46 | 47 | child = runScript(playground) 48 | } 49 | 50 | main(process.argv) 51 | -------------------------------------------------------------------------------- /internal/playground-swaig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/playground-swaig", 3 | "description": "Playground for SWAIG", 4 | "author": "SignalWire Team ", 5 | "version": "0.1.1", 6 | "private": true, 7 | "main": "index.js", 8 | "scripts": { 9 | "dev": "node -r dotenv/config index.js", 10 | "test": "" 11 | }, 12 | "keywords": [ 13 | "signalwire", 14 | "swaig", 15 | "ai", 16 | "gateway" 17 | ], 18 | "devDependencies": { 19 | "node-watch": "^0.7.4" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /internal/stack-tests/.gitignore: -------------------------------------------------------------------------------- 1 | **/dist 2 | .env 3 | -------------------------------------------------------------------------------- /internal/stack-tests/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/stack-tests", 3 | "version": "1.0.5", 4 | "description": "", 5 | "main": "index.js", 6 | "type": "module", 7 | "scripts": { 8 | "dev": "node index.js", 9 | "test": "" 10 | }, 11 | "dependencies": { 12 | "execa": "^7.1.1", 13 | "tap": "^18.7.2" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /internal/stack-tests/src/chat/app.ts: -------------------------------------------------------------------------------- 1 | import { SignalWire } from '@signalwire/realtime-api' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const client = await SignalWire({ 7 | host: process.env.RELAY_HOST || 'relay.swire.io', 8 | project: process.env.RELAY_PROJECT as string, 9 | token: process.env.RELAY_TOKEN as string, 10 | }) 11 | 12 | tap.ok(client.chat, 'client.chat is defined') 13 | tap.ok(client.chat.listen, 'client.chat.listen is defined') 14 | tap.ok(client.chat.publish, 'client.chat.publish is defined') 15 | tap.ok(client.chat.getMessages, 'client.chat.getMessages is defined') 16 | tap.ok(client.chat.getMembers, 'client.chat.getMembers is defined') 17 | tap.ok(client.chat.getMemberState, 'client.chat.getMemberState is defined') 18 | tap.ok(client.chat.setMemberState, 'client.chat.setMemberState is defined') 19 | 20 | process.exit(0) 21 | } catch (error) { 22 | console.log('', error) 23 | process.exit(1) 24 | } 25 | } 26 | 27 | run() 28 | -------------------------------------------------------------------------------- /internal/stack-tests/src/chat/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-chat", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 7 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 8 | }, 9 | "dependencies": { 10 | "@signalwire/realtime-api": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/stack-tests/src/close-event/app.ts: -------------------------------------------------------------------------------- 1 | import { SWCloseEvent } from '@signalwire/core' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const evt = new SWCloseEvent('close', { 7 | reason: 'Client-side closed', 8 | }) 9 | 10 | tap.equal(evt.type, 'close') 11 | tap.equal(evt.code, 0) 12 | tap.equal(evt.reason, 'Client-side closed') 13 | tap.equal(evt.wasClean, false) 14 | 15 | process.exit(0) 16 | } catch (error) { 17 | console.log('', error) 18 | process.exit(1) 19 | } 20 | } 21 | 22 | run() 23 | -------------------------------------------------------------------------------- /internal/stack-tests/src/close-event/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-close-event", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "", 6 | "scripts": { 7 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 8 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 9 | }, 10 | "dependencies": { 11 | "@signalwire/core": "^3.18.2" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /internal/stack-tests/src/messaging/app.ts: -------------------------------------------------------------------------------- 1 | import { SignalWire } from '@signalwire/realtime-api' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const client = await SignalWire({ 7 | host: process.env.RELAY_HOST || 'relay.swire.io', 8 | project: process.env.RELAY_PROJECT as string, 9 | token: process.env.RELAY_TOKEN as string, 10 | }) 11 | 12 | tap.ok(client.messaging, 'client.messaging is defined') 13 | tap.ok(client.messaging.listen, 'client.messaging.listen is defined') 14 | tap.ok(client.messaging.send, 'message.send is defined') 15 | tap.ok(client.disconnect, 'client.disconnect is defined') 16 | 17 | process.exit(0) 18 | } catch (error) { 19 | console.log('', error) 20 | process.exit(1) 21 | } 22 | } 23 | 24 | run() 25 | -------------------------------------------------------------------------------- /internal/stack-tests/src/messaging/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-messaging", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 7 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 8 | }, 9 | "dependencies": { 10 | "@signalwire/realtime-api": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/stack-tests/src/pubSub/app.ts: -------------------------------------------------------------------------------- 1 | import { SignalWire } from '@signalwire/realtime-api' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const client = await SignalWire({ 7 | host: process.env.RELAY_HOST || 'relay.swire.io', 8 | project: process.env.RELAY_PROJECT as string, 9 | token: process.env.RELAY_TOKEN as string, 10 | }) 11 | 12 | tap.ok(client.pubSub, 'client.pubSub is defined') 13 | tap.ok(client.pubSub.listen, 'client.pubSub.listen is defined') 14 | tap.ok(client.pubSub.publish, 'client.pubSub.publish is defined') 15 | 16 | process.exit(0) 17 | } catch (error) { 18 | console.log('', error) 19 | process.exit(1) 20 | } 21 | } 22 | 23 | run() 24 | -------------------------------------------------------------------------------- /internal/stack-tests/src/pubSub/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-pubsub", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 7 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 8 | }, 9 | "dependencies": { 10 | "@signalwire/realtime-api": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/stack-tests/src/task/app.ts: -------------------------------------------------------------------------------- 1 | import { SignalWire } from '@signalwire/realtime-api' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const client = await SignalWire({ 7 | host: process.env.RELAY_HOST || 'relay.swire.io', 8 | project: process.env.RELAY_PROJECT as string, 9 | token: process.env.RELAY_TOKEN as string, 10 | }) 11 | 12 | tap.ok(client.task, 'client.task is defined') 13 | tap.ok(client.task.listen, 'client.task.listen is defined') 14 | tap.ok(client.task.send, 'client.task.send is defined') 15 | 16 | process.exit(0) 17 | } catch (error) { 18 | console.log('', error) 19 | process.exit(1) 20 | } 21 | } 22 | 23 | run() 24 | -------------------------------------------------------------------------------- /internal/stack-tests/src/task/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-task", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 7 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 8 | }, 9 | "dependencies": { 10 | "@signalwire/realtime-api": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/stack-tests/src/video/app.ts: -------------------------------------------------------------------------------- 1 | import { SignalWire } from '@signalwire/realtime-api' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const client = await SignalWire({ 7 | host: process.env.RELAY_HOST || 'relay.swire.io', 8 | project: process.env.RELAY_PROJECT as string, 9 | token: process.env.RELAY_TOKEN as string, 10 | }) 11 | 12 | tap.ok(client.video, 'client.video is defined') 13 | tap.ok(client.video.listen, 'client.video.listen is defined') 14 | tap.ok(client.video.getRoomSessions, 'video.getRoomSessions is defined') 15 | tap.ok( 16 | client.video.getRoomSessionById, 17 | 'video.getRoomSessionById is defined' 18 | ) 19 | tap.ok(client.disconnect, 'video.disconnect is defined') 20 | 21 | process.exit(0) 22 | } catch (error) { 23 | console.log('', error) 24 | process.exit(1) 25 | } 26 | } 27 | 28 | run() 29 | -------------------------------------------------------------------------------- /internal/stack-tests/src/video/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-video", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 7 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 8 | }, 9 | "dependencies": { 10 | "@signalwire/realtime-api": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /internal/stack-tests/src/voice/app.ts: -------------------------------------------------------------------------------- 1 | import { SignalWire } from '@signalwire/realtime-api' 2 | import tap from 'tap' 3 | 4 | async function run() { 5 | try { 6 | const client = await SignalWire({ 7 | host: process.env.RELAY_HOST || 'relay.swire.io', 8 | project: process.env.RELAY_PROJECT as string, 9 | token: process.env.RELAY_TOKEN as string, 10 | }) 11 | 12 | tap.ok(client.voice, 'client.voice is defined') 13 | tap.ok(client.voice.listen, 'client.voice.listen is defined') 14 | tap.ok(client.voice.dial, 'voice.dial is defined') 15 | tap.ok(client.voice.dialPhone, 'voice.dialPhone is defined') 16 | tap.ok(client.voice.dialSip, 'voice.dialSip is defined') 17 | tap.ok(client.disconnect, 'voice.disconnect is defined') 18 | 19 | process.exit(0) 20 | } catch (error) { 21 | console.log('', error) 22 | process.exit(1) 23 | } 24 | } 25 | 26 | run() 27 | -------------------------------------------------------------------------------- /internal/stack-tests/src/voice/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "stack-tests-voice", 3 | "version": "1.0.0", 4 | "description": "", 5 | "scripts": { 6 | "build:cjs": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module CommonJS --outDir dist/cjs", 7 | "build:esm": "tsc app.ts --rootDir ./ --skipLibCheck --esModuleInterop --lib DOM --lib ES2015 --target ESNext --moduleResolution node --module ESNext --outDir dist/esm" 8 | }, 9 | "dependencies": { 10 | "@signalwire/realtime-api": "^3.1.1" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/root", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "./packages/*", 7 | "./scripts/*", 8 | "./internal/*" 9 | ], 10 | "scripts": { 11 | "changeset": "changeset", 12 | "clean": "npm exec --ws -- npx rimraf node_modules && npm exec --ws -- npx rimraf dist && npx rimraf node_modules", 13 | "test": "npm exec --ws npm run test", 14 | "build": "npm exec --ws -- npx rimraf dist && sw-build-all", 15 | "prettier": "prettier --write .", 16 | "release:dev": "sw-release --development", 17 | "release:beta": "sw-release --beta", 18 | "prepare:prod": "sw-release --prepare-prod", 19 | "release:prod": "sw-release --production" 20 | }, 21 | "dependencies": { 22 | "@babel/core": "^7.26.0", 23 | "@babel/preset-env": "^7.26.0", 24 | "@babel/preset-typescript": "^7.26.0", 25 | "@changesets/changelog-github": "^0.4.8", 26 | "@changesets/cli": "^2.26.2", 27 | "@types/jest": "^29.5.2", 28 | "babel-jest": "^29.7.0", 29 | "concurrently": "^8.2.0", 30 | "esbuild": "0.25.0", 31 | "jest": "^29.7.0", 32 | "jest-environment-jsdom": "^29.7.0", 33 | "prettier": "^2.8.8" 34 | }, 35 | "overrides": { 36 | "axios": "1.8.4" 37 | }, 38 | "devDependencies": { 39 | "@types/node": "^20.3.2", 40 | "dotenv": "^16.3.1", 41 | "jest-websocket-mock": "^2.5.0", 42 | "mock-socket": "^9.3.1", 43 | "ts-jest": "^29.2.5", 44 | "typescript": "^5.6.3" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/compatibility-api/README.md: -------------------------------------------------------------------------------- 1 | # @signalwire/compatibility-api 2 | 3 | The `@signalwire/compatibility-api` SDK has been moved in the [compatibility-api-js](https://github.com/signalwire/compatibility-api-js) repository. 4 | -------------------------------------------------------------------------------- /packages/core/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /packages/core/README.md: -------------------------------------------------------------------------------- 1 | # @signalwire/core 2 | 3 | The SignalWire Core package contains lower level objects and shared utilities for various `@signalwire` packages. 4 | 5 | **Important**: This package is intended for internal use by the SignalWire libraries. You should not use it directly in your production projects, as the APIs can and will change often. Usage of this package directly is currently not supported. 6 | 7 | ## Related 8 | 9 | - [Get Started](https://developer.signalwire.com/) 10 | - [`@signalwire/js`](https://www.npmjs.com/package/@signalwire/js) 11 | - [`@signalwire/realtime-api`](https://www.npmjs.com/package/@signalwire/realtime-api) 12 | 13 | ## License 14 | 15 | `@signalwire/core` is copyright © 2018-2023 [SignalWire](http://signalwire.com). It is free software, and may be redistributed under the terms specified in the [MIT-LICENSE](https://github.com/signalwire/signalwire-js/blob/master/LICENSE) file. 16 | -------------------------------------------------------------------------------- /packages/core/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 4 | }, 5 | setupFiles: ['./src/setupTests.ts'], 6 | } 7 | -------------------------------------------------------------------------------- /packages/core/src/BaseClient.ts: -------------------------------------------------------------------------------- 1 | import { destroyAction, initAction } from './redux' 2 | import { BaseClientOptions } from './utils/interfaces' 3 | import { BaseComponent } from './BaseComponent' 4 | import { EventEmitter } from './utils/EventEmitter' 5 | import { getAuthStatus } from './redux/features/session/sessionSelectors' 6 | 7 | export class BaseClient< 8 | EventTypes extends EventEmitter.ValidEventTypes 9 | > extends BaseComponent { 10 | constructor(public options: BaseClientOptions) { 11 | super(options) 12 | } 13 | 14 | /** 15 | * Connect the underlay WebSocket connection to the SignalWire network. 16 | * 17 | * @returns Promise that will resolve with the Client object. 18 | */ 19 | connect(): Promise { 20 | const authStatus = getAuthStatus(this.store.getState()) 21 | 22 | if (authStatus === 'unknown' || authStatus === 'unauthorized') { 23 | this.store.dispatch(initAction()) 24 | } 25 | 26 | return this._waitUntilSessionAuthorized() 27 | } 28 | 29 | /** 30 | * Disconnect the Client from the SignalWire network. 31 | */ 32 | disconnect() { 33 | this.store.dispatch(destroyAction()) 34 | } 35 | 36 | override removeAllListeners>( 37 | event?: T 38 | ) { 39 | this.sessionEventNames().forEach((eventName) => { 40 | this.sessionEmitter.off(eventName) 41 | }) 42 | 43 | return super.removeAllListeners(event) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/BaseConsumer.test.ts: -------------------------------------------------------------------------------- 1 | import { BaseConsumer, connect } from '.' 2 | import { configureFullStack } from './testUtils' 3 | 4 | describe('BaseConsumer', () => { 5 | describe('subscribe', () => { 6 | let instance: any 7 | let fullStack: ReturnType 8 | 9 | beforeEach(() => { 10 | fullStack = configureFullStack() 11 | 12 | instance = connect({ 13 | store: fullStack.store, 14 | Component: BaseConsumer, 15 | })({ 16 | emitter: fullStack.emitter, 17 | }) 18 | instance.execute = jest.fn() 19 | }) 20 | 21 | afterEach(() => { 22 | fullStack.destroy() 23 | }) 24 | 25 | it('should be idempotent', async () => { 26 | instance.on('something-1', () => {}) 27 | instance.on('something-2', () => {}) 28 | instance.on('something-2', () => {}) 29 | 30 | await instance.subscribe() 31 | await instance.subscribe() 32 | await instance.subscribe() 33 | await instance.subscribe() 34 | await instance.subscribe() 35 | expect(instance.execute).toHaveBeenCalledTimes(1) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/core/src/CustomErrors.ts: -------------------------------------------------------------------------------- 1 | export class AuthError extends Error { 2 | name = 'AuthError' 3 | 4 | constructor(public code: number, public message: string) { 5 | super(message) 6 | Object.setPrototypeOf(this, AuthError.prototype) 7 | } 8 | } 9 | 10 | export class HttpError extends Error { 11 | name = 'HttpError' 12 | 13 | constructor( 14 | public code: number, 15 | public message: string, 16 | public response?: Record 17 | ) { 18 | super(message) 19 | Object.setPrototypeOf(this, HttpError.prototype) 20 | } 21 | } 22 | 23 | export class CapabilityError extends Error { 24 | constructor(message: string) { 25 | super(message) 26 | this.name = 'CapabilityError' 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/RPCConnect.ts: -------------------------------------------------------------------------------- 1 | import { makeRPCRequest } from './helpers' 2 | 3 | type WithToken = { token: string; jwt_token?: never } 4 | type WithJWT = { token?: never; jwt_token: string } 5 | type RPCConnectAuthentication = { project?: string } & (WithToken | WithJWT) 6 | export type RPCConnectParams = { 7 | authentication: RPCConnectAuthentication 8 | version?: typeof DEFAULT_CONNECT_VERSION 9 | agent?: string 10 | protocol?: string 11 | authorization_state?: string 12 | contexts?: string[] 13 | topics?: string[] 14 | eventing?: string[] 15 | event_acks?: boolean 16 | } 17 | 18 | export const DEFAULT_CONNECT_VERSION = { 19 | major: 3, 20 | minor: 0, 21 | revision: 0, 22 | } 23 | 24 | export const UNIFIED_CONNECT_VERSION = { 25 | major: 4, 26 | minor: 0, 27 | revision: 0, 28 | } 29 | 30 | export const RPCConnect = (params: RPCConnectParams) => { 31 | return makeRPCRequest({ 32 | method: 'signalwire.connect', 33 | params: { 34 | version: DEFAULT_CONNECT_VERSION, 35 | event_acks: true, 36 | ...params, 37 | }, 38 | }) 39 | } 40 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/RPCDisconnect.ts: -------------------------------------------------------------------------------- 1 | import { makeRPCResponse } from './helpers' 2 | 3 | export const RPCDisconnectResponse = (id: string) => { 4 | return makeRPCResponse({ 5 | id, 6 | result: {}, 7 | }) 8 | } 9 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/RPCEventAck.ts: -------------------------------------------------------------------------------- 1 | import { makeRPCResponse } from './helpers'; 2 | 3 | 4 | export const RPCEventAckResponse = (id: string) => makeRPCResponse({ id, result: {} }) 5 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/RPCExecute.ts: -------------------------------------------------------------------------------- 1 | import { makeRPCRequest } from './helpers' 2 | import { JSONRPCMethod } from '../utils/interfaces' 3 | 4 | type RPCExecuteParams = { 5 | id?: string 6 | method: JSONRPCMethod 7 | params: Record 8 | } 9 | 10 | export const RPCExecute = ({ method, params }: RPCExecuteParams) => { 11 | return makeRPCRequest({ 12 | method, 13 | params, 14 | }) 15 | } 16 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/RPCPing.ts: -------------------------------------------------------------------------------- 1 | import { makeRPCRequest, makeRPCResponse } from './helpers' 2 | 3 | export const RPCPing = () => { 4 | return makeRPCRequest({ 5 | method: 'signalwire.ping', 6 | params: { 7 | timestamp: Date.now() / 1000, 8 | }, 9 | }) 10 | } 11 | 12 | export const RPCPingResponse = (id: string, timestamp?: number) => { 13 | return makeRPCResponse({ 14 | id, 15 | result: { 16 | timestamp: timestamp || Date.now() / 1000, 17 | }, 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/RPCReauthenticate.ts: -------------------------------------------------------------------------------- 1 | import { makeRPCRequest } from './helpers' 2 | 3 | export type RPCReauthenticateParams = { project: string; jwt_token: string } 4 | 5 | export const RPCReauthenticate = (authentication: RPCReauthenticateParams) => { 6 | return makeRPCRequest({ 7 | method: 'signalwire.reauthenticate', 8 | params: { 9 | authentication, 10 | }, 11 | }) 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/helpers.ts: -------------------------------------------------------------------------------- 1 | import { uuid } from '../utils' 2 | import { JSONRPCMethod } from '../utils/interfaces' 3 | 4 | interface MakeRPCRequestParams { 5 | id?: string 6 | method: JSONRPCMethod 7 | params: { 8 | // TODO: use list of types? 9 | [key: string]: any 10 | } 11 | } 12 | export const makeRPCRequest = (params: MakeRPCRequestParams) => { 13 | return { 14 | jsonrpc: '2.0' as const, 15 | id: params.id ?? uuid(), 16 | ...params, 17 | } 18 | } 19 | 20 | interface MakeRPCResponseParams { 21 | id: string 22 | result: { 23 | // TODO: use list of types? 24 | [key: string]: any 25 | } 26 | } 27 | export const makeRPCResponse = (params: MakeRPCResponseParams) => { 28 | return { 29 | jsonrpc: '2.0' as const, 30 | ...params, 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/core/src/RPCMessages/index.ts: -------------------------------------------------------------------------------- 1 | export * from './helpers' 2 | export * from './RPCConnect' 3 | export * from './RPCReauthenticate' 4 | export * from './RPCPing' 5 | export * from './RPCExecute' 6 | export * from './RPCDisconnect' 7 | export * from './VertoMessages' 8 | export * from './RPCEventAck' 9 | -------------------------------------------------------------------------------- /packages/core/src/chat/ChatMember.ts: -------------------------------------------------------------------------------- 1 | import { ChatMemberContract } from '..' 2 | 3 | /** 4 | * Represents a member in a chat. 5 | */ 6 | export class ChatMember implements ChatMemberContract { 7 | constructor(private payload: ChatMemberContract) {} 8 | 9 | /** The id of this member */ 10 | get id(): string { 11 | return this.payload.id 12 | } 13 | 14 | /** The channel of this member */ 15 | get channel(): string { 16 | return this.payload.channel 17 | } 18 | 19 | /** The state of this member */ 20 | get state(): any { 21 | return this.payload.state ?? {} 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/chat/ChatMessage.ts: -------------------------------------------------------------------------------- 1 | import { ChatMessageContract, ChatMemberContract } from '..' 2 | import { PubSubMessage } from '../pubSub' 3 | 4 | /** 5 | * Represents a message in a chat. 6 | */ 7 | export class ChatMessage extends PubSubMessage { 8 | /** The member which sent this message */ 9 | get member(): ChatMemberContract { 10 | return this.payload.member 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/core/src/chat/index.ts: -------------------------------------------------------------------------------- 1 | export * from './methods' 2 | export * from './BaseChat' 3 | export * from './ChatMessage' 4 | export * from './ChatMember' 5 | export * from './applyCommonMethods' 6 | -------------------------------------------------------------------------------- /packages/core/src/chat/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './toInternalChatChannels' 2 | 3 | export const isValidChannels = (input: unknown): input is string | string[] => { 4 | return Array.isArray(input) || typeof input === 'string' 5 | } 6 | -------------------------------------------------------------------------------- /packages/core/src/chat/utils/toInternalChatChannels.test.ts: -------------------------------------------------------------------------------- 1 | import { toInternalChatChannels } from './toInternalChatChannels' 2 | 3 | describe('toInternalChatChannels', () => { 4 | it('should convert a single string channel to array of objects', () => { 5 | const result = toInternalChatChannels('watercooler') 6 | 7 | expect(result).toStrictEqual([ 8 | { 9 | name: 'watercooler', 10 | }, 11 | ]) 12 | }) 13 | 14 | it('should convert multiple channels to array of objects', () => { 15 | const result = toInternalChatChannels(['channel1', 'channel2']) 16 | 17 | expect(result).toStrictEqual([ 18 | { 19 | name: 'channel1', 20 | }, 21 | { 22 | name: 'channel2', 23 | }, 24 | ]) 25 | }) 26 | 27 | it('should return empty array when undefined', () => { 28 | const result = toInternalChatChannels(undefined) 29 | 30 | expect(result).toStrictEqual([]) 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/core/src/chat/utils/toInternalChatChannels.ts: -------------------------------------------------------------------------------- 1 | import { InternalChatChannel } from '../..' 2 | 3 | export const toInternalChatChannels = ( 4 | channels: string | string[] | undefined 5 | ): InternalChatChannel[] => { 6 | const list = !channels || Array.isArray(channels) ? channels : [channels] 7 | 8 | if (Array.isArray(list)) { 9 | return list.map((name) => { 10 | return { 11 | name, 12 | } 13 | }) 14 | } 15 | 16 | return [] 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/chat/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chatWorker' 2 | -------------------------------------------------------------------------------- /packages/core/src/index.test.ts: -------------------------------------------------------------------------------- 1 | describe('Fake', () => { 2 | it('works', () => { 3 | expect(2).toEqual(2) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/core/src/memberPosition/index.ts: -------------------------------------------------------------------------------- 1 | export * from './workers' 2 | -------------------------------------------------------------------------------- /packages/core/src/pubSub/PubSubMessage.ts: -------------------------------------------------------------------------------- 1 | import { PubSubMessageContract } from '..' 2 | 3 | /** 4 | * Represents a message in a pubSub context. 5 | */ 6 | export class PubSubMessage< 7 | PayloadType extends PubSubMessageContract = PubSubMessageContract 8 | > implements PubSubMessageContract 9 | { 10 | constructor(protected payload: PayloadType) {} 11 | 12 | /** The id of this message */ 13 | get id(): string { 14 | return this.payload.id 15 | } 16 | 17 | /** The channel in which this message was sent */ 18 | get channel(): string { 19 | return this.payload.channel 20 | } 21 | 22 | /** The content of this message */ 23 | get content(): string { 24 | return this.payload.content 25 | } 26 | 27 | /** Any metadata associated to this message */ 28 | get meta(): any { 29 | return this.payload.meta 30 | } 31 | 32 | /** The date at which this message was published */ 33 | get publishedAt(): Date { 34 | return this.payload.publishedAt 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/pubSub/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BasePubSub' 2 | export * from './PubSubMessage' 3 | -------------------------------------------------------------------------------- /packages/core/src/pubSub/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pubSubWorker' 2 | -------------------------------------------------------------------------------- /packages/core/src/redux/features/component/componentSaga.ts: -------------------------------------------------------------------------------- 1 | import { SagaIterator } from '@redux-saga/core' 2 | import { delay, fork, put, select } from '@redux-saga/core/effects' 3 | import { componentActions } from '..' 4 | import { getComponentsToCleanup } from './componentSelectors' 5 | 6 | export function* componentCleanupSagaWorker(): SagaIterator { 7 | const toCleanup = yield select(getComponentsToCleanup) 8 | 9 | if (toCleanup.length) { 10 | yield put( 11 | componentActions.cleanup({ 12 | ids: toCleanup, 13 | }) 14 | ) 15 | } 16 | } 17 | 18 | export function* componentCleanupSaga(): SagaIterator { 19 | while (true) { 20 | yield delay(3600_000) // 1 hour 21 | yield fork(componentCleanupSagaWorker) 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/redux/features/component/componentSelectors.test.ts: -------------------------------------------------------------------------------- 1 | import { Store } from 'redux' 2 | import { configureJestStore } from '../../../testUtils' 3 | import { getComponentsToCleanup } from './componentSelectors' 4 | 5 | describe('componentSelectors', () => { 6 | let store: Store 7 | 8 | beforeEach(() => { 9 | store = configureJestStore({ 10 | preloadedState: { 11 | components: { 12 | byId: { 13 | '268b4cf8-a3c5-4003-8666-3b7a4f0a5af9': { 14 | id: '268b4cf8-a3c5-4003-8666-3b7a4f0a5af9', 15 | }, 16 | 'faa63915-3a64-4c39-acbb-06dac0758f8a': { 17 | id: 'faa63915-3a64-4c39-acbb-06dac0758f8a', 18 | responses: {}, 19 | }, 20 | 'zfaa63915-3a64-4c39-acbb-06dac0758f8a': { 21 | id: 'zfaa63915-3a64-4c39-acbb-06dac0758f8a', 22 | errors: {}, 23 | }, 24 | }, 25 | }, 26 | }, 27 | }) 28 | }) 29 | 30 | describe('getComponentsToCleanup', () => { 31 | it('should return the list of components to cleanup', () => { 32 | expect(getComponentsToCleanup(store.getState())).toEqual([ 33 | 'faa63915-3a64-4c39-acbb-06dac0758f8a', 34 | 'zfaa63915-3a64-4c39-acbb-06dac0758f8a', 35 | ]) 36 | }) 37 | }) 38 | }) 39 | -------------------------------------------------------------------------------- /packages/core/src/redux/features/component/componentSelectors.ts: -------------------------------------------------------------------------------- 1 | import { ReduxComponent, SDKState } from '../../interfaces' 2 | 3 | export const getComponent = ({ components }: SDKState, id: string) => { 4 | return components.byId?.[id] 5 | } 6 | 7 | export const getComponentsById = ({ components }: SDKState) => { 8 | return components.byId 9 | } 10 | 11 | export const getComponentsToCleanup = (state: SDKState) => { 12 | const components = getComponentsById(state) 13 | 14 | let toCleanup: Array = [] 15 | Object.keys(components).forEach((id) => { 16 | if (components[id].responses || components[id].errors) { 17 | toCleanup.push(id) 18 | } 19 | }) 20 | 21 | return toCleanup 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/redux/features/index.ts: -------------------------------------------------------------------------------- 1 | export * from './component/componentSlice' 2 | export * from './session/sessionSlice' 3 | -------------------------------------------------------------------------------- /packages/core/src/redux/features/session/sessionSelectors.ts: -------------------------------------------------------------------------------- 1 | import { SDKState } from '../../interfaces' 2 | 3 | export const getIceServers = ({ session }: SDKState) => { 4 | return session?.iceServers ?? [] 5 | } 6 | 7 | export const getSession = (store: SDKState) => { 8 | return store.session 9 | } 10 | 11 | export const getAuthStatus = ({ session }: SDKState) => { 12 | return session.authStatus 13 | } 14 | 15 | export const getAuthError = ({ session }: SDKState) => { 16 | return session.authError 17 | } 18 | 19 | export const getAuthorization = ({ session }: SDKState) => { 20 | return session.authorization 21 | } 22 | 23 | export const getAuthorizationState = ({ session }: SDKState) => { 24 | return session.authorizationState 25 | } 26 | 27 | export const getProtocol = ({ session }: SDKState) => { 28 | return session.protocol 29 | } 30 | -------------------------------------------------------------------------------- /packages/core/src/redux/rootReducer.ts: -------------------------------------------------------------------------------- 1 | import { combineReducers } from './toolkit' 2 | import { componentReducer, sessionReducer } from './features' 3 | 4 | export const rootReducer = combineReducers({ 5 | components: componentReducer, 6 | session: sessionReducer, 7 | }) 8 | -------------------------------------------------------------------------------- /packages/core/src/redux/toolkit/getDefaultMiddleware.ts: -------------------------------------------------------------------------------- 1 | import { Middleware } from 'redux' 2 | import { MiddlewareArray } from './utils' 3 | 4 | interface GetDefaultMiddlewareOptions {} 5 | 6 | export type CurriedGetDefaultMiddleware = < 7 | O extends Partial = { 8 | thunk: true 9 | immutableCheck: true 10 | serializableCheck: true 11 | } 12 | >( 13 | options?: O 14 | ) => MiddlewareArray> 15 | 16 | export function curryGetDefaultMiddleware< 17 | S = any 18 | >(): CurriedGetDefaultMiddleware { 19 | return function curriedGetDefaultMiddleware() { 20 | return [] as unknown as MiddlewareArray> 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/core/src/redux/toolkit/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Cherry-picked (and adapted) version of Redux Toolkit. API 3 | * wise it remains fully compatible but we changed a couple 4 | * of things to reduce the overall bundle size. Most 5 | * important is that our version doesn't depend on Immer and 6 | * we don't include any of the default middleares included 7 | * by RTK like thunks or inmutable state (we do this through 8 | * TS). 9 | */ 10 | export * from 'redux' 11 | export * from './createAction' 12 | export * from './configureStore' 13 | -------------------------------------------------------------------------------- /packages/core/src/redux/toolkit/isPlainObject.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Returns true if the passed value is "plain" object, i.e. an object whose 3 | * prototype is the root `Object.prototype`. This includes objects created 4 | * using object literals, but not for instance for class instances. 5 | * 6 | * @param {any} value The value to inspect. 7 | * @returns {boolean} True if the argument appears to be a plain object. 8 | * 9 | * @public 10 | */ 11 | export default function isPlainObject(value: unknown): value is object { 12 | if (typeof value !== 'object' || value === null) return false 13 | 14 | let proto = Object.getPrototypeOf(value) 15 | if (proto === null) return true 16 | 17 | let baseProto = proto 18 | while (Object.getPrototypeOf(baseProto) !== null) { 19 | baseProto = Object.getPrototypeOf(baseProto) 20 | } 21 | 22 | return proto === baseProto 23 | } 24 | -------------------------------------------------------------------------------- /packages/core/src/redux/utils/createDestroyableSlice.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SliceCaseReducers, 3 | ValidateSliceCaseReducers, 4 | createSlice, 5 | } from '../toolkit/createSlice' 6 | import { ActionReducerMapBuilder } from '../toolkit/mapBuilders' 7 | import { destroyAction } from '../actions' 8 | 9 | export const createDestroyableSlice = < 10 | T, 11 | Reducers extends SliceCaseReducers 12 | >({ 13 | name = '', 14 | initialState, 15 | reducers, 16 | extraReducers, 17 | }: { 18 | name: string 19 | initialState: T 20 | reducers: ValidateSliceCaseReducers 21 | extraReducers?: (builder: ActionReducerMapBuilder) => void 22 | }) => { 23 | return createSlice({ 24 | name, 25 | initialState, 26 | reducers, 27 | extraReducers: (builder) => { 28 | builder.addCase(destroyAction.type, () => { 29 | return initialState 30 | }) 31 | 32 | if (typeof extraReducers === 'function') { 33 | extraReducers(builder) 34 | } 35 | }, 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /packages/core/src/redux/utils/sagaHelpers.ts: -------------------------------------------------------------------------------- 1 | import { Saga } from '@redux-saga/core' 2 | import { call, spawn } from '@redux-saga/core/effects' 3 | import { getLogger } from '../../utils' 4 | 5 | export const createRestartableSaga = (saga: Saga) => { 6 | return function* () { 7 | spawn(function* () { 8 | while (true) { 9 | try { 10 | getLogger().debug('Run a restartable saga') 11 | yield call(saga) 12 | getLogger().debug( 13 | 'One of the restartable saga has ended. Restarting..' 14 | ) 15 | } catch (error) { 16 | getLogger().error('Restartable Saga Error', error) 17 | } 18 | } 19 | }) 20 | } 21 | } 22 | 23 | const defaultCatchHandler = (error: any) => 24 | getLogger().error('Catchable Saga Error', error) 25 | 26 | export const createCatchableSaga = ( 27 | saga: Saga, 28 | errorHandler = defaultCatchHandler 29 | ) => { 30 | return function* (...params: Args[]) { 31 | try { 32 | yield call(saga, ...params) 33 | } catch (error) { 34 | errorHandler(error) 35 | } 36 | } 37 | } 38 | 39 | export { eventChannel } from '@redux-saga/core' 40 | -------------------------------------------------------------------------------- /packages/core/src/redux/utils/useInstanceMap.ts: -------------------------------------------------------------------------------- 1 | import { InstanceMap } from '../../utils/interfaces' 2 | 3 | export const useInstanceMap = (): InstanceMap => { 4 | /** 5 | * Generic map stores multiple instance 6 | * For eg; 7 | * callId => CallInstance 8 | * controlId => PlaybackInstance | RecordingInstance 9 | */ 10 | const instanceMap = new Map() 11 | 12 | const getInstance = (key: string): T => { 13 | return instanceMap.get(key) as T 14 | } 15 | 16 | const setInstance = (key: string, value: T) => { 17 | instanceMap.set(key, value) 18 | return instanceMap as Map 19 | } 20 | 21 | const deleteInstance = (key: string) => { 22 | instanceMap.delete(key) 23 | return instanceMap as Map 24 | } 25 | 26 | const getAllInstances = () => { 27 | return Array.from(instanceMap.entries()) 28 | } 29 | 30 | const deleteAllInstances = () => { 31 | instanceMap.clear() 32 | return instanceMap 33 | } 34 | 35 | return { 36 | get: getInstance, 37 | set: setInstance, 38 | remove: deleteInstance, 39 | getAll: getAllInstances, 40 | deleteAll: deleteAllInstances, 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/core/src/redux/utils/useSession.ts: -------------------------------------------------------------------------------- 1 | import { BaseSession } from '../../BaseSession' 2 | import { getLogger } from '../../utils' 3 | import { getEventEmitter } from '../../utils/EventEmitter' 4 | import { 5 | ClientEvents, 6 | InternalUserOptions, 7 | SessionConstructor, 8 | } from '../../utils/interfaces' 9 | import { SessionChannel } from '../interfaces' 10 | 11 | interface UseSessionOptions { 12 | userOptions: InternalUserOptions 13 | SessionConstructor: SessionConstructor 14 | sessionChannel: SessionChannel 15 | } 16 | 17 | export const useSession = (options: UseSessionOptions) => { 18 | const { SessionConstructor, userOptions, sessionChannel } = options 19 | 20 | const sessionEmitter = getEventEmitter() 21 | 22 | let session: BaseSession | null = null 23 | 24 | const initSession = () => { 25 | session = new SessionConstructor({ 26 | ...userOptions, 27 | sessionChannel, 28 | }) 29 | return session 30 | } 31 | 32 | const getSession = () => { 33 | if (!session) { 34 | getLogger().warn('Session does not exist!') 35 | } 36 | return session 37 | } 38 | 39 | return { session, initSession, getSession, sessionEmitter } 40 | } 41 | -------------------------------------------------------------------------------- /packages/core/src/rooms/RoomSessionStream.test.ts: -------------------------------------------------------------------------------- 1 | import { configureJestStore } from '../testUtils' 2 | import { 3 | createRoomSessionStreamObject, 4 | RoomSessionStream, 5 | } from './RoomSessionStream' 6 | 7 | describe('RoomSessionStream', () => { 8 | describe('createRoomSessionStreamObject', () => { 9 | let instance: RoomSessionStream 10 | beforeEach(() => { 11 | instance = createRoomSessionStreamObject({ 12 | store: configureJestStore(), 13 | payload: { 14 | // @ts-expect-error 15 | stream: { 16 | id: 'c22d7223-5a01-49fe-8da0-46bec8e75e32', 17 | }, 18 | room_session_id: 'room-session-id', 19 | }, 20 | }) 21 | // @ts-expect-error 22 | instance.execute = jest.fn() 23 | }) 24 | 25 | it('should control an active stream', async () => { 26 | const baseExecuteParams = { 27 | method: '', 28 | params: { 29 | room_session_id: 'room-session-id', 30 | stream_id: 'c22d7223-5a01-49fe-8da0-46bec8e75e32', 31 | }, 32 | } 33 | 34 | await instance.stop() 35 | // @ts-expect-error 36 | expect(instance.execute).toHaveBeenLastCalledWith({ 37 | ...baseExecuteParams, 38 | method: 'video.stream.stop', 39 | }) 40 | }) 41 | }) 42 | }) 43 | -------------------------------------------------------------------------------- /packages/core/src/rooms/index.ts: -------------------------------------------------------------------------------- 1 | import { BaseComponent, EventEmitter } from '..' 2 | 3 | export interface BaseRoomInterface< 4 | EventTypes extends EventEmitter.ValidEventTypes 5 | > extends BaseComponent { 6 | roomId: string 7 | roomSessionId: string 8 | memberId: string 9 | } 10 | 11 | export * from './methods' 12 | export * from './RoomSessionRecording' 13 | export * from './RoomSessionPlayback' 14 | export * from './RoomSessionStream' 15 | -------------------------------------------------------------------------------- /packages/core/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | jest.mock('./utils', () => { 2 | return { 3 | ...(jest.requireActual('./utils') as any), 4 | logger: { 5 | error: jest.fn(), 6 | warn: jest.fn(), 7 | log: jest.fn(), 8 | info: jest.fn(), 9 | debug: jest.fn(), 10 | trace: jest.fn(), 11 | levels: jest.fn(), 12 | setLevel: jest.fn(), 13 | }, 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /packages/core/src/types/common.ts: -------------------------------------------------------------------------------- 1 | export type PaginationCursor = 2 | | { 3 | before: string 4 | after?: never 5 | } 6 | | { 7 | before?: never 8 | after: string 9 | } 10 | 11 | export type ClientContextMethod = 'signalwire.receive' | 'signalwire.unreceive' 12 | 13 | /** 14 | * Private common event types 15 | */ 16 | export type CallPlay = 'call.play' 17 | export type CallRecord = 'call.record' 18 | 19 | /** 20 | * Public common event types 21 | */ 22 | export type PlaybackStarted = 'playback.started' 23 | export type PlaybackUpdated = 'playback.updated' 24 | export type PlaybackEnded = 'playback.ended' 25 | export type PlaybackFailed = 'playback.failed' 26 | export type RecordingStarted = 'recording.started' 27 | export type RecordingUpdated = 'recording.updated' 28 | export type RecordingEnded = 'recording.ended' 29 | export type RecordingFailed = 'recording.failed' 30 | -------------------------------------------------------------------------------- /packages/core/src/types/conversation.ts: -------------------------------------------------------------------------------- 1 | import { SwEvent } from '..' 2 | 3 | export type ConversationMessageEventName = 'conversation.message' 4 | 5 | export interface ConversationMessageEventParams { 6 | conversation_id: string 7 | conversation_name: string 8 | details: Record 9 | hidden: boolean 10 | id: string 11 | kind?: string 12 | metadata: Record 13 | subtype: string 14 | text?: string 15 | ts: number 16 | type: string 17 | user_id: string 18 | user_name: string 19 | address_id: string 20 | from_address_id: string 21 | } 22 | 23 | export interface ConversationMessageEvent extends SwEvent { 24 | event_type: ConversationMessageEventName 25 | params: ConversationMessageEventParams 26 | } 27 | 28 | export type ConversationEvent = ConversationMessageEvent 29 | 30 | export type ConversationEventParams = ConversationMessageEventParams 31 | -------------------------------------------------------------------------------- /packages/core/src/types/fabric.ts: -------------------------------------------------------------------------------- 1 | import { MapToPubSubShape } from '../redux/interfaces' 2 | import { 3 | FabricLayoutEvent, 4 | FabricLayoutEventNames, 5 | FabricLayoutEventParams, 6 | } from './fabricLayout' 7 | import { 8 | FabricMemberEvent, 9 | FabricMemberEventNames, 10 | FabricMemberEventParams, 11 | } from './fabricMember' 12 | import { 13 | FabricRoomEvent, 14 | FabricRoomEventNames, 15 | FabricRoomEventParams, 16 | } from './fabricRoomSession' 17 | 18 | export * from './fabricRoomSession' 19 | export * from './fabricMember' 20 | export * from './fabricLayout' 21 | 22 | /** 23 | * List of all call fabric events 24 | */ 25 | export type FabricEventNames = 26 | | FabricRoomEventNames 27 | | FabricMemberEventNames 28 | | FabricLayoutEventNames 29 | 30 | export type FabricEvent = 31 | | FabricRoomEvent 32 | | FabricMemberEvent 33 | | FabricLayoutEvent 34 | 35 | export type FabricEventParams = 36 | | FabricRoomEventParams 37 | | FabricMemberEventParams 38 | | FabricLayoutEventParams 39 | 40 | export type FabricAction = MapToPubSubShape 41 | -------------------------------------------------------------------------------- /packages/core/src/types/fabricLayout.ts: -------------------------------------------------------------------------------- 1 | import { SwEvent } from '.' 2 | import { InternalVideoLayout, LayoutChanged } from './videoLayout' 3 | 4 | /** 5 | * ========== 6 | * ========== 7 | * Server-Side Events 8 | * ========== 9 | * ========== 10 | */ 11 | 12 | /** 13 | * layout.changed 14 | */ 15 | export interface FabricLayoutChangedEventParams { 16 | room_id: string 17 | room_session_id: string 18 | layout: InternalVideoLayout 19 | } 20 | 21 | export interface FabricLayoutChangedEvent extends SwEvent { 22 | event_type: LayoutChanged 23 | params: FabricLayoutChangedEventParams 24 | } 25 | 26 | export type FabricLayoutEventNames = LayoutChanged 27 | 28 | export type FabricLayoutEvent = FabricLayoutChangedEvent 29 | 30 | export type FabricLayoutEventParams = FabricLayoutChangedEventParams 31 | -------------------------------------------------------------------------------- /packages/core/src/types/task.ts: -------------------------------------------------------------------------------- 1 | import type { OnlyStateProperties, OnlyFunctionProperties } from '..' 2 | 3 | export type TaskReceivedEventName = 'task.received' 4 | 5 | export type TaskEventNames = TaskReceivedEventName 6 | 7 | export interface TaskContract {} 8 | 9 | export type TaskEntity = OnlyStateProperties 10 | export type TaskMethods = Omit< 11 | OnlyFunctionProperties, 12 | 'subscribe' | 'unsubscribe' | 'updateToken' 13 | > 14 | 15 | /** 16 | * ========== 17 | * ========== 18 | * Server-Side Events 19 | * ========== 20 | * ========== 21 | */ 22 | 23 | /** 24 | * 'queuing.relay.tasks' 25 | */ 26 | export interface TaskInboundEvent { 27 | event_type: 'queuing.relay.tasks' 28 | context: string 29 | message: Record 30 | timestamp: number 31 | space_id: string 32 | project_id: string 33 | } 34 | 35 | export type TaskEvent = TaskInboundEvent 36 | 37 | /** 38 | * TODO: update MapToPubSubShape in another PR 39 | * not used MapToPubSubShape because queuing.relay.tasks 40 | * has a different shape 41 | */ 42 | export type TaskAction = { 43 | type: TaskReceivedEventName 44 | payload: TaskInboundEvent 45 | } 46 | -------------------------------------------------------------------------------- /packages/core/src/types/videoRoomDevice.ts: -------------------------------------------------------------------------------- 1 | export type CameraUpdated = 'camera.updated' 2 | export type CameraDisconnected = 'camera.disconnected' 3 | export type MicrophoneUpdated = 'microphone.updated' 4 | export type MicrophoneDisconnected = 'microphone.disconnected' 5 | export type SpeakerUpdated = 'speaker.updated' 6 | export type SpeakerDisconnected = 'speaker.disconnected' 7 | 8 | /** 9 | * List of public event names 10 | */ 11 | 12 | export type VideoRoomDeviceUpdatedEventNames = 13 | | CameraUpdated 14 | | MicrophoneUpdated 15 | | SpeakerUpdated 16 | 17 | export type VideoRoomDeviceDisconnectedEventNames = 18 | | CameraDisconnected 19 | | MicrophoneDisconnected 20 | | SpeakerDisconnected 21 | 22 | export type VideoRoomDeviceEventNames = 23 | | VideoRoomDeviceUpdatedEventNames 24 | | VideoRoomDeviceDisconnectedEventNames 25 | 26 | export interface VideoRoomMediaDeviceInfo { 27 | deviceId: MediaDeviceInfo['deviceId'] | undefined 28 | label: MediaDeviceInfo['label'] | undefined 29 | } 30 | 31 | export interface DeviceUpdatedEventParams { 32 | previous: VideoRoomMediaDeviceInfo 33 | current: VideoRoomMediaDeviceInfo 34 | } 35 | 36 | export type DeviceDisconnectedEventParams = VideoRoomMediaDeviceInfo 37 | 38 | export type VideoRoomDeviceEventParams = 39 | | DeviceUpdatedEventParams 40 | | DeviceDisconnectedEventParams 41 | -------------------------------------------------------------------------------- /packages/core/src/types/voice.ts: -------------------------------------------------------------------------------- 1 | import { PRODUCT_PREFIX_VOICE_CALL } from '../utils/constants' 2 | 3 | export type VoiceNamespace = typeof PRODUCT_PREFIX_VOICE_CALL 4 | export type ToInternalVoiceEvent = `${VoiceNamespace}.${T}` 5 | 6 | export interface NestedArray extends Array> {} 7 | 8 | export * from './voiceCall' 9 | export * from './voicePlayback' 10 | export * from './voiceRecording' 11 | export * from './voiceDetect' 12 | export * from './voiceTap' 13 | export * from './voiceCollect' 14 | export * from './voicePrompt' 15 | export * from './voiceSendDigits' 16 | export * from './voiceConnect' 17 | -------------------------------------------------------------------------------- /packages/core/src/utils/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3' 2 | 3 | const REQUIRED_EMITTER_METHODS = [ 4 | 'on', 5 | 'off', 6 | 'once', 7 | 'removeAllListeners', 8 | 'emit', 9 | ] 10 | 11 | /** 12 | * Checks the shape of the emitter at runtime. This is useful for when 13 | * the user is using the SDK without TS 14 | */ 15 | const assertEventEmitter = (emitter: unknown): emitter is EventEmitter => { 16 | if ( 17 | emitter && 18 | typeof emitter === 'object' && 19 | REQUIRED_EMITTER_METHODS.every((name) => name in emitter) 20 | ) { 21 | return true 22 | } 23 | 24 | return false 25 | } 26 | 27 | const getEventEmitter = () => { 28 | return new EventEmitter() 29 | } 30 | 31 | export { assertEventEmitter, EventEmitter, getEventEmitter } 32 | -------------------------------------------------------------------------------- /packages/core/src/utils/SWCloseEvent.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Class representing a close event. 3 | * The `ws` package does not expose it so we can easily create one in here. 4 | * 5 | * @extends Event 6 | */ 7 | export class SWCloseEvent { 8 | public code: number 9 | public reason: string 10 | public wasClean: boolean 11 | constructor( 12 | public type: string, 13 | options: { code?: number; reason?: string; wasClean?: boolean } = {} 14 | ) { 15 | this.code = options.code === undefined ? 0 : options.code 16 | this.reason = options.reason === undefined ? '' : options.reason 17 | this.wasClean = options.wasClean === undefined ? false : options.wasClean 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/core/src/utils/common.ts: -------------------------------------------------------------------------------- 1 | import { WebRTCEventType } from '..' 2 | 3 | const UPPERCASE_REGEX = /[A-Z]/g 4 | /** 5 | * Converts values from camelCase to snake_case 6 | * @internal 7 | */ 8 | export const fromCamelToSnakeCase = (event: T): T => { 9 | // @ts-ignore 10 | return event.replace(UPPERCASE_REGEX, (letter) => { 11 | return `_${letter.toLowerCase()}` 12 | }) as T 13 | } 14 | 15 | export const WEBRTC_EVENT_TYPES: WebRTCEventType[] = [ 16 | 'webrtc.message', 17 | // 'webrtc.verto', 18 | ] 19 | export const isWebrtcEventType = ( 20 | eventType: string 21 | ): eventType is WebRTCEventType => { 22 | // @ts-expect-error 23 | return WEBRTC_EVENT_TYPES.includes(eventType) 24 | } 25 | -------------------------------------------------------------------------------- /packages/core/src/utils/eventUtils.ts: -------------------------------------------------------------------------------- 1 | 2 | 3 | export const stripNamespacePrefix = ( 4 | event: string, 5 | namespace?: string 6 | ): string => { 7 | if (namespace && typeof namespace === 'string') { 8 | const regex = new RegExp(`^${namespace}\.`) 9 | return event.replace(regex, '') 10 | } 11 | const items = event.split('.') 12 | if (items.length > 1) { 13 | items.shift() 14 | return items.join('.') 15 | } 16 | return event 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/extendComponent.test.ts: -------------------------------------------------------------------------------- 1 | import { extendComponent } from './extendComponent' 2 | 3 | describe('extendComponent', () => { 4 | it('should allow us to extend an classes with new methods', () => { 5 | class A {} 6 | const ExtendedClass = extendComponent(A, { 7 | methodA: {}, 8 | methodB: {}, 9 | methodC: {}, 10 | methodD: {}, 11 | }) 12 | 13 | const instance = new ExtendedClass() 14 | 15 | expect(instance).toHaveProperty('methodA') 16 | expect(instance).toHaveProperty('methodB') 17 | expect(instance).toHaveProperty('methodC') 18 | expect(instance).toHaveProperty('methodD') 19 | }) 20 | 21 | it('should throw if the base class already has the method defined', () => { 22 | expect(() => 23 | extendComponent( 24 | class { 25 | methodA() {} 26 | }, 27 | { 28 | methodA: {}, 29 | methodB: {}, 30 | methodC: {}, 31 | methodD: {}, 32 | } 33 | ) 34 | ).toThrow() 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/core/src/utils/extendComponent.ts: -------------------------------------------------------------------------------- 1 | import type { APIMethodsMap } from './interfaces' 2 | import type { ConstructableType } from '../types/utils' 3 | 4 | export const extendComponent = ( 5 | klass: any, 6 | methods: APIMethodsMap 7 | ) => { 8 | Object.keys(methods).forEach((methodName) => { 9 | if (klass.prototype.hasOwnProperty(methodName)) { 10 | throw new Error(`[extendComponent] Duplicated method name: ${methodName}`) 11 | } 12 | }) 13 | 14 | Object.defineProperties(klass.prototype, methods) 15 | 16 | return klass as ConstructableType 17 | } 18 | -------------------------------------------------------------------------------- /packages/core/src/utils/storage/index.native.ts: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | 3 | // TODO: Review ReactNative support 4 | 5 | import AsyncStorage from '@react-native-community/async-storage' 6 | import { mutateStorageKey, safeParseJson } from '../' 7 | 8 | const getItem = async (key: string): Promise => { 9 | try { 10 | const res = await AsyncStorage.getItem(mutateStorageKey(key)) 11 | return safeParseJson(res) 12 | } catch (error) { 13 | return null 14 | } 15 | } 16 | 17 | const setItem = (key: string, value: any): Promise => { 18 | if (typeof value === 'object') { 19 | value = JSON.stringify(value) 20 | } 21 | return AsyncStorage.setItem(mutateStorageKey(key), value) 22 | } 23 | 24 | const removeItem = (key: string): Promise => 25 | AsyncStorage.removeItem(mutateStorageKey(key)) 26 | 27 | export const localStorage = { getItem, setItem, removeItem } 28 | export const sessionStorage = { getItem, setItem, removeItem } 29 | -------------------------------------------------------------------------------- /packages/core/src/utils/toInternalAction.ts: -------------------------------------------------------------------------------- 1 | import { JSONRPCRequest } from '..' 2 | import { MapToPubSubShape } from '../redux/interfaces' 3 | import { isWebrtcEventType } from './common' 4 | 5 | export const toInternalAction = < 6 | T extends { event_type: string; params?: unknown; node_id?: string } 7 | >( 8 | event: T 9 | ) => { 10 | const { event_type, params, node_id } = event 11 | 12 | /** 13 | * queuing.relay.tasks has a slightly different shape: 14 | * no nested "params" so we return the whole event. 15 | */ 16 | if (event_type === 'queuing.relay.tasks') { 17 | return { 18 | type: event_type, 19 | payload: event, 20 | } as MapToPubSubShape 21 | } 22 | 23 | /** 24 | * `webrtc.*` events need to carry the node_id with them 25 | */ 26 | if (isWebrtcEventType(event_type) && (params as JSONRPCRequest)?.jsonrpc) { 27 | const vertoRPC = params as JSONRPCRequest 28 | if (vertoRPC.params) { 29 | vertoRPC.params.nodeId = node_id 30 | } 31 | return { 32 | type: event_type, 33 | payload: vertoRPC, 34 | } as MapToPubSubShape 35 | } 36 | 37 | return { 38 | type: event_type, 39 | payload: params, 40 | } as MapToPubSubShape 41 | } 42 | -------------------------------------------------------------------------------- /packages/core/src/utils/toInternalEventName.ts: -------------------------------------------------------------------------------- 1 | import { fromCamelToSnakeCase } from './common' 2 | import { EVENT_NAMESPACE_DIVIDER } from './constants' 3 | import { EventEmitter } from './EventEmitter' 4 | 5 | type ToInternalEventNameParams< 6 | EventTypes extends EventEmitter.ValidEventTypes 7 | > = { 8 | event: EventEmitter.EventNames 9 | namespace?: string 10 | } 11 | 12 | export const toInternalEventName = < 13 | EventTypes extends EventEmitter.ValidEventTypes 14 | >({ 15 | event, 16 | namespace, 17 | }: ToInternalEventNameParams) => { 18 | // TODO: improve types of getNamespacedEvent and fromCamelToSnakeCase 19 | if (typeof event === 'string') { 20 | // other transforms here.. 21 | event = getNamespacedEvent({ 22 | event, 23 | namespace, 24 | }) as EventEmitter.EventNames 25 | event = fromCamelToSnakeCase>(event) 26 | } 27 | 28 | return event 29 | } 30 | 31 | const getNamespacedEvent = ({ 32 | namespace, 33 | event, 34 | }: { 35 | event: string 36 | namespace?: string 37 | }) => { 38 | /** 39 | * If getNamespacedEvent is called with the `namespace` already 40 | * present in the `event` or with a falsy namespace we'll return it 41 | * as is 42 | */ 43 | if (!namespace || event.startsWith(namespace)) { 44 | return event 45 | } 46 | 47 | return `${namespace}${EVENT_NAMESPACE_DIVIDER}${event}` 48 | } 49 | -------------------------------------------------------------------------------- /packages/core/src/utils/toSnakeCaseKeys.ts: -------------------------------------------------------------------------------- 1 | import { CamelToSnakeCase } from '../types/utils' 2 | import { fromCamelToSnakeCase } from './common' 3 | 4 | type ToSnakeCaseKeys = { 5 | [Property in NonNullable as CamelToSnakeCase< 6 | Extract 7 | >]: T[Property] 8 | } 9 | 10 | export const toSnakeCaseKeys = >( 11 | obj: T, 12 | transform: (value: string) => any = (value: string) => value, 13 | result: Record = {} 14 | ) => { 15 | if (Array.isArray(obj)) { 16 | result = obj.map((item: any, index: number) => { 17 | if (typeof item === 'object') { 18 | return toSnakeCaseKeys(item, transform, result[index]) 19 | } 20 | return item 21 | }) 22 | } else { 23 | Object.keys(obj).forEach((key) => { 24 | const newKey = fromCamelToSnakeCase(key) 25 | // Both 'object's and arrays will enter this branch 26 | if (obj[key] && typeof obj[key] === 'object') { 27 | result[newKey] = toSnakeCaseKeys(obj[key], transform, result[newKey]) 28 | } else { 29 | result[newKey] = transform(obj[key]) 30 | } 31 | }) 32 | } 33 | 34 | return result as ToSnakeCaseKeys 35 | } 36 | -------------------------------------------------------------------------------- /packages/core/src/workers/executeActionWorker.ts: -------------------------------------------------------------------------------- 1 | import { call } from '@redux-saga/core/effects' 2 | import { SagaIterator } from '@redux-saga/core' 3 | import { getLogger } from '../utils/logger' 4 | import { RPCExecute } from '../RPCMessages/RPCExecute' 5 | import type { SDKWorker } from '../utils/interfaces' 6 | import type { ExecuteActionParams } from '../redux/interfaces' 7 | 8 | /** 9 | * Send a JSONRPC over the wire using session.execute and resolve/reject the promise 10 | */ 11 | export const executeActionWorker: SDKWorker = function* ( 12 | options 13 | ): SagaIterator { 14 | const { initialState, onDone, onFail, getSession } = options 15 | 16 | const { requestId: id, method, params } = initialState 17 | 18 | const session = getSession() 19 | 20 | if (!session) { 21 | const error = new Error('Session does not exist!') 22 | getLogger().error(error) 23 | onFail?.(error) 24 | return 25 | } 26 | 27 | try { 28 | let message = RPCExecute({ id, method, params }) 29 | 30 | const response = yield call(session.execute, message) 31 | onDone?.(response) 32 | } catch (error) { 33 | getLogger().warn('Execute error: ', error) 34 | onFail?.(error) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/core/src/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './executeActionWorker' 2 | -------------------------------------------------------------------------------- /packages/core/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist" 6 | }, 7 | "include": ["src"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/js/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | .parcel-cache 6 | docs 7 | -------------------------------------------------------------------------------- /packages/js/jest.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('@jest/types').Config.InitialOptions} */ 2 | module.exports = { 3 | transform: { 4 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 5 | }, 6 | resolver: '/test/resolver.js', 7 | setupFiles: ['./src/setupTests.ts'], 8 | testMatch: ['/src/**/*.test.ts'], 9 | } 10 | -------------------------------------------------------------------------------- /packages/js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/js", 3 | "description": "SignalWire JS SDK", 4 | "author": "SignalWire Team ", 5 | "license": "MIT", 6 | "version": "3.28.1", 7 | "main": "dist/index.js", 8 | "module": "dist/index.esm.js", 9 | "unpkg": "dist/index.umd.js", 10 | "files": [ 11 | "dist", 12 | "src" 13 | ], 14 | "engines": { 15 | "node": ">=14" 16 | }, 17 | "keywords": [ 18 | "signalwire", 19 | "video", 20 | "rtc", 21 | "real time communication", 22 | "webrtc", 23 | "relay", 24 | "sip" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/signalwire/signalwire-js" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/signalwire/signalwire-js/issues" 32 | }, 33 | "homepage": "https://github.com/signalwire/signalwire-js/tree/master/packages/js", 34 | "scripts": { 35 | "start": "sw-build --dev --web", 36 | "build": "tsc --project tsconfig.build.json && sw-build --web && sw-build --umd", 37 | "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", 38 | "example": "(cd examples && npm run dev)", 39 | "prepublishOnly": "npm run build" 40 | }, 41 | "dependencies": { 42 | "@signalwire/core": "4.2.1", 43 | "@signalwire/webrtc": "3.13.1", 44 | "jwt-decode": "^3.1.2" 45 | }, 46 | "devDependencies": { 47 | "@types/object-path": "^0.11.4" 48 | }, 49 | "types": "dist/js/src/index.d.ts" 50 | } 51 | -------------------------------------------------------------------------------- /packages/js/src/RoomSessionDevice.test.ts: -------------------------------------------------------------------------------- 1 | import { RoomSessionDeviceAPI } from './RoomSessionDevice' 2 | import type { RoomSessionDevice } from './RoomSessionDevice' 3 | import { configureJestStore } from './testUtils' 4 | 5 | describe('RoomDevice Object', () => { 6 | let roomDevice: RoomSessionDevice 7 | 8 | beforeEach(() => { 9 | roomDevice = new RoomSessionDeviceAPI({ 10 | store: configureJestStore(), 11 | }) as any as RoomSessionDevice 12 | // @ts-expect-error 13 | roomDevice.execute = jest.fn() 14 | }) 15 | 16 | it('should have all the custom methods defined', () => { 17 | expect(roomDevice.audioMute).toBeDefined() 18 | expect(roomDevice.audioUnmute).toBeDefined() 19 | expect(roomDevice.videoMute).toBeDefined() 20 | expect(roomDevice.videoUnmute).toBeDefined() 21 | expect(roomDevice.setMicrophoneVolume).toBeDefined() 22 | expect(roomDevice.setInputVolume).toBeDefined() 23 | expect(roomDevice.setInputSensitivity).toBeDefined() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/js/src/RoomSessionScreenShare.test.ts: -------------------------------------------------------------------------------- 1 | import { RoomSessionScreenShareAPI } from './RoomSessionScreenShare' 2 | import type { RoomSessionScreenShare } from './RoomSessionScreenShare' 3 | import { configureJestStore } from './testUtils' 4 | 5 | describe('RoomScreenShare Object', () => { 6 | let roomScreenShare: RoomSessionScreenShare 7 | 8 | beforeEach(() => { 9 | roomScreenShare = new RoomSessionScreenShareAPI({ 10 | store: configureJestStore(), 11 | }) as any as RoomSessionScreenShare 12 | // @ts-expect-error 13 | roomScreenShare.execute = jest.fn() 14 | }) 15 | 16 | it('should have all the custom methods defined', () => { 17 | expect(roomScreenShare.audioMute).toBeDefined() 18 | expect(roomScreenShare.audioUnmute).toBeDefined() 19 | expect(roomScreenShare.videoMute).toBeDefined() 20 | expect(roomScreenShare.videoUnmute).toBeDefined() 21 | expect(roomScreenShare.setMicrophoneVolume).toBeDefined() 22 | expect(roomScreenShare.setInputVolume).toBeDefined() 23 | expect(roomScreenShare.setInputSensitivity).toBeDefined() 24 | }) 25 | }) 26 | -------------------------------------------------------------------------------- /packages/js/src/cantina/index.ts: -------------------------------------------------------------------------------- 1 | export * from './VideoManager' 2 | -------------------------------------------------------------------------------- /packages/js/src/cantina/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './videoManagerWorker' 2 | -------------------------------------------------------------------------------- /packages/js/src/cantina/workers/videoManagerRoomWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | SagaIterator, 4 | MapToPubSubShape, 5 | VideoManagerRoomEvent, 6 | toExternalJSON, 7 | VideoManagerRoomEventNames, 8 | stripNamespacePrefix, 9 | } from '@signalwire/core' 10 | import { VideoManagerWorkerParams } from './videoManagerWorker' 11 | 12 | export const videoManagerRoomWorker = function* ( 13 | options: VideoManagerWorkerParams> 14 | ): SagaIterator { 15 | getLogger().trace('videoManagerRoomWorker started') 16 | const { 17 | instance: client, 18 | action: { type, payload }, 19 | } = options 20 | 21 | // For now we expose the transformed payload and not a RoomSession 22 | client.emit( 23 | stripNamespacePrefix(type) as VideoManagerRoomEventNames, 24 | toExternalJSON(payload) 25 | ) 26 | 27 | getLogger().trace('videoManagerRoomWorker ended') 28 | } 29 | -------------------------------------------------------------------------------- /packages/js/src/cantina/workers/videoManagerRoomsWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | SagaIterator, 4 | MapToPubSubShape, 5 | VideoManagerRoomsSubscribedEvent, 6 | toExternalJSON, 7 | VideoManagerRoomEventNames, 8 | stripNamespacePrefix, 9 | } from '@signalwire/core' 10 | import { VideoManagerWorkerParams } from './videoManagerWorker' 11 | 12 | export const videoManagerRoomsWorker = function* ( 13 | options: VideoManagerWorkerParams< 14 | MapToPubSubShape 15 | > 16 | ): SagaIterator { 17 | getLogger().trace('videoManagerRoomsWorker started') 18 | const { 19 | instance: client, 20 | action: { type, payload }, 21 | } = options 22 | 23 | // For now we expose the transformed payload and not a RoomSession 24 | const modPayload = { 25 | rooms: payload.rooms.map((row) => toExternalJSON(row)), 26 | } 27 | client.emit( 28 | stripNamespacePrefix(type) as VideoManagerRoomEventNames, 29 | // @ts-expect-error 30 | modPayload 31 | ) 32 | 33 | getLogger().trace('videoManagerRoomsWorker ended') 34 | } 35 | -------------------------------------------------------------------------------- /packages/js/src/chat/index.ts: -------------------------------------------------------------------------------- 1 | export { Client, ClientOptions, ClientApiEvents } from './Client' 2 | 3 | export { ChatMember, ChatMessage } from '@signalwire/core' 4 | 5 | export type { PagingCursor } from '../utils/interfaces' 6 | 7 | export type { 8 | ChatAction, 9 | ChatChannel, 10 | ChatChannelMessageEvent, 11 | ChatChannelMessageEventParams, 12 | ChatChannelState, 13 | ChatEvent, 14 | ChatGetMembersParams, 15 | ChatGetMemberStateParams, 16 | ChatGetMessagesParams, 17 | ChatMemberContract, 18 | ChatMemberEntity, 19 | ChatMemberJoinedEvent, 20 | ChatMemberJoinedEventParams, 21 | ChatMemberLeftEvent, 22 | ChatMemberLeftEventParams, 23 | ChatMemberUpdatedEvent, 24 | ChatMemberUpdatedEventParams, 25 | ChatMessageContract, 26 | ChatMessageEntity, 27 | ChatSetMemberStateParams, 28 | InternalChatMemberEntity, 29 | InternalChatMessageEntity, 30 | PaginationCursor, 31 | } from '@signalwire/core' 32 | -------------------------------------------------------------------------------- /packages/js/src/createClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ClientEvents, 3 | configureStore, 4 | connect, 5 | getEventEmitter, 6 | UserOptions, 7 | } from '@signalwire/core' 8 | import { ClientAPI, Client } from './Client' 9 | import { JWTSession } from './JWTSession' 10 | 11 | /** 12 | * With Video.createClient() you can establish a WebSocket connection 13 | * with SignalWire and interact with the client. 14 | * 15 | * ## Examples 16 | * Create a client 17 | * 18 | * @example 19 | * ```js 20 | * try { 21 | * const client = Video.createClient({ 22 | * token: '', 23 | * }) 24 | * 25 | * await client.connect() 26 | * // Your client is ready now.. 27 | * } catch (error) { 28 | * console.error('Error', error) 29 | * } 30 | * ``` 31 | * @internal 32 | */ 33 | export const createClient = (userOptions: UserOptions) => { 34 | const baseUserOptions = { 35 | ...userOptions, 36 | emitter: getEventEmitter(), 37 | } 38 | const store = configureStore({ 39 | userOptions: baseUserOptions, 40 | SessionConstructor: JWTSession, 41 | }) 42 | const client = connect>({ 43 | store, 44 | Component: ClientAPI, 45 | })(baseUserOptions) 46 | 47 | return client 48 | } 49 | -------------------------------------------------------------------------------- /packages/js/src/fabric/ConversationAPI.ts: -------------------------------------------------------------------------------- 1 | import { Conversation } from './Conversation' 2 | import { 3 | ConversationAPIGetMessagesParams, 4 | ConversationAPISendMessageParams, 5 | ConversationContract, 6 | ConversationResponse, 7 | } from './interfaces' 8 | 9 | export class ConversationAPI implements ConversationContract { 10 | constructor( 11 | private conversation: Conversation, 12 | private payload: ConversationResponse 13 | ) {} 14 | 15 | get id() { 16 | return this.payload.id 17 | } 18 | 19 | get addressId() { 20 | return this.payload.address_id 21 | } 22 | 23 | get createdAt() { 24 | return this.payload.created_at 25 | } 26 | 27 | get lastMessageAt() { 28 | return this.payload.last_message_at 29 | } 30 | 31 | get metadata() { 32 | return this.payload.metadata 33 | } 34 | 35 | get name() { 36 | return this.payload.name 37 | } 38 | 39 | sendMessage(params: ConversationAPISendMessageParams) { 40 | return this.conversation.sendMessage({ 41 | addressId: this.id, 42 | text: params.text, 43 | }) 44 | } 45 | 46 | getMessages(params?: ConversationAPIGetMessagesParams) { 47 | return this.conversation.getConversationMessages({ 48 | addressId: this.id, 49 | ...params, 50 | }) 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /packages/js/src/fabric/createWSClient.ts: -------------------------------------------------------------------------------- 1 | import { configureStore } from '@signalwire/core' 2 | import { SATSession } from './SATSession' 3 | import { WSClientOptions } from './interfaces' 4 | 5 | export const createWSClient = (options: WSClientOptions) => { 6 | const store = configureStore({ 7 | userOptions: options, 8 | SessionConstructor: SATSession, 9 | }) 10 | 11 | return { ...options, store } 12 | } 13 | -------------------------------------------------------------------------------- /packages/js/src/fabric/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * ============ CAUTION ============ 3 | * 4 | * Anything we export from here in available on the Public interface. 5 | */ 6 | 7 | export * from './SignalWire' 8 | export * from './interfaces/address' 9 | export * from './interfaces/capabilities' 10 | export * from './interfaces/conversation' 11 | export * from './interfaces/device' 12 | export * from './interfaces/incomingCallManager' 13 | 14 | export { 15 | OnlineParams, 16 | HandlePushNotificationParams, 17 | HandlePushNotificationResult, 18 | DialParams, 19 | } from './interfaces/wsClient' 20 | export { 21 | SignalWireClient, 22 | SignalWireContract, 23 | SignalWireClientParams, 24 | GetSubscriberInfoResponse, 25 | GetSubscriberInfoResult, 26 | PaginatedResponse, 27 | PaginatedResult, 28 | } from './interfaces' 29 | export { FabricRoomSession, isFabricRoomSession } from './FabricRoomSession' 30 | -------------------------------------------------------------------------------- /packages/js/src/fabric/interfaces/address.ts: -------------------------------------------------------------------------------- 1 | import { PaginatedResponse, PaginatedResult } from '.' 2 | 3 | export type ResourceType = 'app' | 'call' | 'room' | 'subscriber' 4 | 5 | export interface GetAddressResponse { 6 | id: string 7 | display_name: string 8 | name: string 9 | preview_url?: string 10 | cover_url?: string 11 | resource_id: string 12 | type: ResourceType 13 | channels: { 14 | audio?: string 15 | messaging?: string 16 | video?: string 17 | } 18 | locked: boolean 19 | created_at: string 20 | } 21 | 22 | export type Address = GetAddressResponse 23 | 24 | export interface GetAddressesParams { 25 | type?: string 26 | displayName?: string 27 | pageSize?: number 28 | sortBy?: 'name' | 'created_at' 29 | sortOrder?: 'asc' | 'desc' 30 | } 31 | 32 | export interface GetAddressByIdParams { 33 | id: string 34 | } 35 | 36 | export interface GetAddressByNameParams { 37 | name: string 38 | } 39 | 40 | export type GetAddressParams = GetAddressByIdParams | GetAddressByNameParams 41 | 42 | export type GetAddressResult = GetAddressResponse 43 | 44 | export type GetAddressesResponse = PaginatedResponse 45 | 46 | export type GetAddressesResult = PaginatedResult 47 | -------------------------------------------------------------------------------- /packages/js/src/fabric/interfaces/capabilities.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface CapabilityOnOffStateContract { 3 | on: boolean 4 | off: boolean 5 | } 6 | 7 | export interface MemberCapabilityContract { 8 | muteAudio: CapabilityOnOffStateContract 9 | muteVideo: CapabilityOnOffStateContract 10 | microphoneVolume: boolean 11 | microphoneSensitivity: boolean 12 | speakerVolume: boolean 13 | deaf: CapabilityOnOffStateContract 14 | raisehand: CapabilityOnOffStateContract 15 | position: boolean 16 | meta: boolean 17 | remove: boolean 18 | } 19 | 20 | export interface CallCapabilitiesContract { 21 | self: MemberCapabilityContract 22 | member: MemberCapabilityContract 23 | end: boolean 24 | setLayout: boolean 25 | sendDigit: boolean 26 | vmutedHide: CapabilityOnOffStateContract 27 | lock: CapabilityOnOffStateContract 28 | device: boolean 29 | screenshare: boolean 30 | } 31 | -------------------------------------------------------------------------------- /packages/js/src/fabric/interfaces/device.ts: -------------------------------------------------------------------------------- 1 | export type RegisterDeviceType = 'iOS' | 'Android' | 'Desktop' 2 | 3 | export interface RegisterDeviceParams { 4 | deviceType: RegisterDeviceType 5 | deviceToken: string 6 | } 7 | 8 | export interface UnregisterDeviceParams { 9 | id: string 10 | } 11 | 12 | export interface RegisterDeviceResponse { 13 | date_registered: Date 14 | device_name?: string 15 | device_token: string 16 | device_type: RegisterDeviceType 17 | id: string 18 | push_notification_key: string 19 | } 20 | 21 | export type RegisterDeviceResult = RegisterDeviceResponse 22 | -------------------------------------------------------------------------------- /packages/js/src/fabric/interfaces/incomingCallManager.ts: -------------------------------------------------------------------------------- 1 | import { FabricRoomSession } from '../FabricRoomSession' 2 | import { CallParams } from './wsClient' 3 | 4 | export type IncomingInviteSource = 'websocket' | 'pushNotification' 5 | 6 | export interface IncomingInvite { 7 | callID: string 8 | sdp: string 9 | caller_id_name: string 10 | caller_id_number: string 11 | callee_id_name: string 12 | callee_id_number: string 13 | display_direction: string 14 | nodeId: string 15 | } 16 | 17 | export interface IncomingInviteWithSource extends IncomingInvite { 18 | source: IncomingInviteSource 19 | } 20 | 21 | export interface IncomingCallNotification { 22 | invite: { 23 | details: IncomingInvite 24 | accept: (param: CallParams) => Promise 25 | reject: () => Promise 26 | } 27 | } 28 | export type IncomingCallHandler = ( 29 | notification: IncomingCallNotification 30 | ) => void 31 | 32 | export interface IncomingCallHandlers { 33 | all?: IncomingCallHandler 34 | pushNotification?: IncomingCallHandler 35 | websocket?: IncomingCallHandler 36 | } 37 | -------------------------------------------------------------------------------- /packages/js/src/fabric/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const PREVIOUS_CALLID_STORAGE_KEY = 'ci-SAT'; 2 | export const DEFAULT_API_REQUEST_RETRIES = 10 3 | export const DEFAULT_API_REQUEST_RETRIES_DELAY = 300 4 | export const DEFAULT_API_REQUEST_RETRIES_DELAY_INCREMENT = 100 -------------------------------------------------------------------------------- /packages/js/src/fabric/utils/typeGuard.ts: -------------------------------------------------------------------------------- 1 | import { 2 | GetAddressParams, 3 | GetAddressByNameParams, 4 | GetAddressByIdParams, 5 | GetAddressResponse, 6 | GetAddressesResponse, 7 | } from '../interfaces' 8 | 9 | export const isGetAddressByNameParams = ( 10 | obj: GetAddressParams 11 | ): obj is GetAddressByNameParams => { 12 | return obj && 'name' in obj 13 | } 14 | 15 | export const isGetAddressByIdParams = ( 16 | obj: GetAddressParams 17 | ): obj is GetAddressByIdParams => { 18 | return obj && 'id' in obj 19 | } 20 | 21 | export const isGetAddressResponse = ( 22 | obj: GetAddressResponse | GetAddressesResponse 23 | ): obj is GetAddressResponse => { 24 | return obj && 'id' in obj && 'name' in obj 25 | } 26 | 27 | export const isGetAddressesResponse = ( 28 | obj: GetAddressResponse | GetAddressesResponse 29 | ): obj is GetAddressesResponse => { 30 | return obj && 'data' in obj 31 | } 32 | -------------------------------------------------------------------------------- /packages/js/src/fabric/workers/callLeftWorker.ts: -------------------------------------------------------------------------------- 1 | import { getLogger, SagaIterator, CallLeftEvent } from '@signalwire/core' 2 | import { FabricWorkerParams } from './fabricWorker' 3 | import { FabricRoomSessionMemberAPI } from '../FabricRoomSessionMember' 4 | 5 | export const callLeftWorker = function* ( 6 | options: FabricWorkerParams 7 | ): SagaIterator { 8 | getLogger().trace('callLeftWorker started') 9 | 10 | const { 11 | action: { payload }, 12 | instance: cfRoomSession, 13 | instanceMap, 14 | } = options 15 | 16 | const { room_session_id } = payload 17 | 18 | // Remove all the member instance where roomSessionId matches 19 | instanceMap.getAll().forEach(([key, obj]) => { 20 | if ( 21 | obj instanceof FabricRoomSessionMemberAPI && 22 | obj.roomSessionId === room_session_id 23 | ) { 24 | instanceMap.remove(key) 25 | } 26 | }) 27 | 28 | cfRoomSession.emit('call.left', payload) 29 | cfRoomSession.emit('room.left', payload) 30 | 31 | getLogger().trace('callLeftWorker ended') 32 | } 33 | -------------------------------------------------------------------------------- /packages/js/src/fabric/workers/conversationWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | sagaEffects, 4 | SagaIterator, 5 | SDKWorker, 6 | SDKActions, 7 | MapToPubSubShape, 8 | ConversationEvent, 9 | } from '@signalwire/core' 10 | import { Conversation } from '../Conversation' 11 | import { WSClient } from '../WSClient' 12 | 13 | interface ConversationWorkerInitialState { 14 | conversation: Conversation 15 | } 16 | 17 | export const conversationWorker: SDKWorker = function* ( 18 | options 19 | ): SagaIterator { 20 | getLogger().debug('conversationWorker started') 21 | const { 22 | channels: { swEventChannel }, 23 | initialState, 24 | } = options 25 | 26 | const { conversation } = initialState as ConversationWorkerInitialState 27 | 28 | const isConversationEvent = (action: SDKActions) => { 29 | return action.type.startsWith('conversation.') 30 | } 31 | 32 | while (true) { 33 | const action: MapToPubSubShape = yield sagaEffects.take( 34 | swEventChannel, 35 | isConversationEvent 36 | ) 37 | conversation.handleEvent(action.payload) 38 | } 39 | 40 | getLogger().trace('conversationWorker ended') 41 | } 42 | -------------------------------------------------------------------------------- /packages/js/src/fabric/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './wsClientWorker' 2 | export * from './conversationWorker' 3 | export * from './fabricWorker' 4 | -------------------------------------------------------------------------------- /packages/js/src/features/actions.ts: -------------------------------------------------------------------------------- 1 | import { actions } from '@signalwire/core' 2 | 3 | export const audioSetSpeakerAction = actions.createAction( 4 | 'swJs/audioSetSpeakerAction' 5 | ) 6 | -------------------------------------------------------------------------------- /packages/js/src/index.test.ts: -------------------------------------------------------------------------------- 1 | describe('Web Entry Point', () => { 2 | it('should work', () => { 3 | expect(1).toEqual(1) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/js/src/joinRoom.ts: -------------------------------------------------------------------------------- 1 | import { CreateRoomObjectOptions, createRoomObject } from './createRoomObject' 2 | 3 | /** 4 | * Using Video.joinRoom() you can automatically join a room. 5 | * 6 | * @example 7 | * With an HTMLDivElement with id="root" in the DOM. 8 | * ```js 9 | * //
10 | * 11 | * try { 12 | * const roomObj = await Video.joinRoom({ 13 | * token: '', 14 | * rootElementId: 'root', 15 | * }) 16 | * 17 | * // You have joined the room.. 18 | * } catch (error) { 19 | * console.error('Error', error) 20 | * } 21 | * ``` 22 | * @deprecated Use {@link RoomSession} instead. 23 | */ 24 | export const joinRoom = (roomOptions: CreateRoomObjectOptions) => { 25 | return createRoomObject({ 26 | ...roomOptions, 27 | autoJoin: true, 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /packages/js/src/pubSub/index.ts: -------------------------------------------------------------------------------- 1 | import { PubSub } from '@signalwire/core' 2 | import { PagingCursor } from '../utils/interfaces' 3 | import PubSubMessage = PubSub.PubSubMessage 4 | 5 | export * from './Client' 6 | 7 | export { 8 | PubSubMessage, 9 | PagingCursor 10 | } 11 | -------------------------------------------------------------------------------- /packages/js/src/utils/CloseEvent.ts: -------------------------------------------------------------------------------- 1 | import { SWCloseEvent as CoreCloseEvent } from '@signalwire/core' 2 | 3 | const SwCloseEvent = 4 | typeof CloseEvent === 'function' ? CloseEvent : CoreCloseEvent 5 | 6 | export { SwCloseEvent } 7 | -------------------------------------------------------------------------------- /packages/js/src/utils/audioElement.ts: -------------------------------------------------------------------------------- 1 | const setAudioMediaTrack = ({ 2 | track, 3 | element, 4 | }: { 5 | track: MediaStreamTrack 6 | element: HTMLAudioElement 7 | }) => { 8 | element.autoplay = true 9 | // @ts-ignore 10 | element.playsinline = true 11 | element.srcObject = new MediaStream([track]) 12 | 13 | track.addEventListener('ended', () => { 14 | element.srcObject = null 15 | element.remove() 16 | }) 17 | 18 | return element 19 | } 20 | 21 | export { setAudioMediaTrack } 22 | -------------------------------------------------------------------------------- /packages/js/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | export const SCREENSHARE_AUDIO_CONSTRAINTS: MediaTrackConstraints = { 2 | echoCancellation: true, 3 | noiseSuppression: false, 4 | autoGainControl: false, 5 | // @ts-expect-error 6 | googAutoGainControl: false, 7 | } 8 | -------------------------------------------------------------------------------- /packages/js/src/utils/interfaces/index.ts: -------------------------------------------------------------------------------- 1 | export * from './base' 2 | export * from './video' 3 | export * from './fabric' 4 | -------------------------------------------------------------------------------- /packages/js/src/utils/makeQueryParamsUrl.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Constructs a URL with query parameters. 3 | * 4 | * @param path The base path of the URL. 5 | * @param queryParams An instance of URLSearchParams containing all query parameters. 6 | * @returns The constructed URL with query parameters. 7 | */ 8 | export function makeQueryParamsUrls( 9 | path: string, 10 | queryParams: URLSearchParams 11 | ): string { 12 | const queryString = queryParams.toString() 13 | return queryString ? `${path}?${queryString}` : path 14 | } 15 | -------------------------------------------------------------------------------- /packages/js/src/utils/paginatedResult.ts: -------------------------------------------------------------------------------- 1 | import { PaginatedResponse } from '../fabric/interfaces' 2 | import { CreateHttpClient } from '../fabric/createHttpClient' 3 | 4 | export function buildPaginatedResult( 5 | body: PaginatedResponse, 6 | client: CreateHttpClient 7 | ) { 8 | const anotherPage = async (url?: string) => { 9 | if (!url) return Promise.resolve(undefined) 10 | const { body } = await client>(url) 11 | return buildPaginatedResult(body, client) 12 | } 13 | 14 | return { 15 | data: body.data, 16 | self: async () => { 17 | const { self } = body.links 18 | return anotherPage(self) 19 | }, 20 | nextPage: async () => { 21 | const { next } = body.links 22 | return anotherPage(next) 23 | }, 24 | prevPage: async () => { 25 | const { prev } = body.links 26 | return anotherPage(prev) 27 | }, 28 | firstPage: async () => { 29 | const { first } = body.links 30 | return anotherPage(first) 31 | }, 32 | hasNext: Boolean(body.links.next), 33 | hasPrev: Boolean(body.links.prev), 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/js/src/utils/storage.test.ts: -------------------------------------------------------------------------------- 1 | import { sessionStorageManager } from './storage' 2 | 3 | describe('sessionStorageManager', () => { 4 | it('should return expected keys', () => { 5 | const token = 6 | 'eyJ0eXBiOiJWUlQiLCJjaCI6InJlbGF5LnNpZ25hbHdpcmUuY29tIiwiYWxnIjoiPOE1MTIifQ.eyJpYXQiOjE2ODg0NjE3MDMsImp0aSI6IjA1YWZkODczLTI4MGUtNDYzNC1iNmQzLWMwMzk4YzE2NGVlMSIsInN1YiI6IjRiN2FlNzhhLWQwMmUtNDg4OS1hNjNiLTA4YjE1NmQ1OTE2ZSIsInUiOiJqZXN0IiwiamEiOiJtZW1iZXIiLCJyIjoidGVhbS10ZXN0IiwicyI6W10sImFjciI6dHJ1ZSwibWEiOiJhbGwiLCJtdGEiOnt9LCJybXRhIjp7fX0.r1JWiEckaids8mm9ESUraCuXWc6Ysa6qMMmuNzRiOf94PO8VwoL_gIr1LDopDtffk-EFUUB4ZsOl8gK-u-qU7A' 7 | const manager = sessionStorageManager(token) 8 | expect(manager).toStrictEqual({ 9 | authStateKey: 'as-team-test', 10 | protocolKey: 'pt-team-test', 11 | callIdKey: 'ci-team-test', 12 | }) 13 | }) 14 | 15 | it('should return falsy values', () => { 16 | const token = 'foo' 17 | const manager = sessionStorageManager(token) 18 | expect(manager).toStrictEqual({ 19 | authStateKey: false, 20 | protocolKey: false, 21 | callIdKey: false, 22 | }) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /packages/js/src/utils/storage.ts: -------------------------------------------------------------------------------- 1 | import jwtDecode from 'jwt-decode' 2 | import { getLogger } from '@signalwire/core' 3 | 4 | /** 5 | * Note: ready to support RN with a "storage.native.ts" file. 6 | */ 7 | export const getStorage = () => { 8 | if (window && window.sessionStorage) { 9 | return window.sessionStorage 10 | } 11 | return undefined 12 | } 13 | 14 | export const sessionStorageManager = (token: string) => { 15 | let roomName: string = '' 16 | try { 17 | const jwtPayload = jwtDecode<{ r: string; ja: string }>(token) 18 | roomName = jwtPayload?.r ?? '' 19 | } catch (e) { 20 | try { 21 | const jwtPayload = jwtDecode<{ typ: string}>(token, {header: true}) 22 | roomName = jwtPayload.typ || '' 23 | } catch { 24 | if (process.env.NODE_ENV !== 'production') { 25 | getLogger().error('[sessionStorageManager] error decoding JWT', token) 26 | } 27 | roomName = '' 28 | } 29 | } 30 | 31 | const valid = Boolean(roomName) 32 | return { 33 | authStateKey: valid && `as-${roomName}`, 34 | protocolKey: valid && `pt-${roomName}`, 35 | callIdKey: valid && `ci-${roomName}`, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/js/src/video/RoomSession.test.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @jest-environment jsdom 3 | */ 4 | import { RoomSession, UNSAFE_PROP_ACCESS } from './RoomSession' 5 | 6 | describe('RoomSession Object', () => { 7 | it('should control which properties the user can access before connecting the room.', async () => { 8 | const roomSession = new RoomSession({ 9 | token: '', 10 | }) 11 | 12 | expect(() => roomSession.active).not.toThrow() 13 | expect(() => roomSession.memberId).not.toThrow() 14 | expect(() => roomSession.join()).not.toThrow() 15 | UNSAFE_PROP_ACCESS.map((prop) => { 16 | // @ts-expect-error 17 | expect(() => roomSession[prop]).toThrow() 18 | }) 19 | 20 | // @ts-expect-error 21 | roomSession.state = 'active' 22 | 23 | expect(() => roomSession.active).not.toThrow() 24 | expect(() => roomSession.memberId).not.toThrow() 25 | expect(() => roomSession.join()).not.toThrow() 26 | UNSAFE_PROP_ACCESS.map((prop) => { 27 | // @ts-expect-error 28 | expect(() => roomSession[prop]).not.toThrow() 29 | }) 30 | }) 31 | }) 32 | -------------------------------------------------------------------------------- /packages/js/src/video/index.ts: -------------------------------------------------------------------------------- 1 | import { createRoomObject, Room } from '../createRoomObject' 2 | import { createClient } from '../createClient' 3 | import { joinRoom } from '../joinRoom' 4 | import { MakeRoomOptions } from '../Client' 5 | import { RoomSession, RoomSessionOptions } from './RoomSession' 6 | import { RoomSessionDevice, RoomDevice } from '../RoomSessionDevice' 7 | import { 8 | RoomSessionScreenShare, 9 | RoomScreenShare, 10 | } from '../RoomSessionScreenShare' 11 | 12 | export { 13 | RoomSession, 14 | RoomSessionDevice, 15 | RoomSessionScreenShare, 16 | // Just to keep backwards compatibility. 17 | createRoomObject, 18 | joinRoom, 19 | Room, 20 | RoomDevice, 21 | RoomScreenShare, 22 | createClient, 23 | } 24 | 25 | /** @ignore */ 26 | export type { MakeRoomOptions, RoomSessionOptions } 27 | 28 | /** @ignore */ 29 | export type { 30 | DeprecatedMemberUpdatableProps, 31 | DeprecatedVideoMemberHandlerParams, 32 | VideoMemberHandlerParams, 33 | VideoMemberListUpdatedParams, 34 | } from '../utils/interfaces' 35 | 36 | export type { CreateRoomObjectOptions } from '../createRoomObject' 37 | 38 | export type { 39 | RoomSessionRecording, 40 | RoomSessionPlayback, 41 | } from '@signalwire/core' 42 | 43 | export { VideoRoomSession, isVideoRoomSession } from './VideoRoomSession' 44 | -------------------------------------------------------------------------------- /packages/js/src/video/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './memberListUpdatedWorker' 2 | export * from './childMemberJoinedWorker' 3 | export * from './videoWorker' 4 | export * from './videoPlaybackWorker' 5 | export * from './videoRecordWorker' 6 | export * from './videoStreamWorker' 7 | -------------------------------------------------------------------------------- /packages/js/src/video/workers/videoStreamWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | SagaIterator, 4 | MapToPubSubShape, 5 | VideoStreamEvent, 6 | RoomSessionStream, 7 | Rooms, 8 | } from '@signalwire/core' 9 | import { VideoWorkerParams } from './videoWorker' 10 | 11 | export const videoStreamWorker = function* ( 12 | options: VideoWorkerParams> 13 | ): SagaIterator { 14 | getLogger().trace('videoStreamWorker started') 15 | const { 16 | instance: roomSession, 17 | action: { type, payload }, 18 | instanceMap: { get, set, remove }, 19 | } = options 20 | 21 | // For now, we are not storing the RoomSession object in the instance map 22 | 23 | let streamInstance = get(payload.stream.id) 24 | if (!streamInstance) { 25 | streamInstance = Rooms.createRoomSessionStreamObject({ 26 | store: roomSession.store, 27 | payload, 28 | }) 29 | } else { 30 | streamInstance.setPayload(payload) 31 | } 32 | set(payload.stream.id, streamInstance) 33 | 34 | switch (type) { 35 | case 'video.stream.started': 36 | roomSession.emit('stream.started', streamInstance) 37 | break 38 | case 'video.stream.ended': 39 | roomSession.emit('stream.ended', streamInstance) 40 | remove(payload.stream.id) 41 | break 42 | default: 43 | getLogger().warn(`Unknown video.stream event: "${type}"`) 44 | break 45 | } 46 | 47 | getLogger().trace('videoStreamWorker ended') 48 | } 49 | -------------------------------------------------------------------------------- /packages/js/src/webrtc.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getDevices, 3 | getCameraDevices, 4 | getMicrophoneDevices, 5 | getSpeakerDevices, 6 | getDevicesWithPermissions, 7 | getCameraDevicesWithPermissions, 8 | getMicrophoneDevicesWithPermissions, 9 | getSpeakerDevicesWithPermissions, 10 | checkPermissions, 11 | checkCameraPermissions, 12 | checkMicrophonePermissions, 13 | checkSpeakerPermissions, 14 | requestPermissions, 15 | createDeviceWatcher, 16 | createCameraDeviceWatcher, 17 | createMicrophoneDeviceWatcher, 18 | createSpeakerDeviceWatcher, 19 | supportsMediaDevices, 20 | supportsGetUserMedia, 21 | supportsGetDisplayMedia, 22 | getUserMedia, 23 | getDisplayMedia, 24 | enumerateDevices, 25 | enumerateDevicesByKind, 26 | getSupportedConstraints, 27 | supportsMediaOutput, 28 | setMediaElementSinkId, 29 | stopStream, 30 | stopTrack, 31 | createMicrophoneAnalyzer, 32 | } from '@signalwire/webrtc' 33 | export type { MicrophoneAnalyzer } from '@signalwire/webrtc' 34 | -------------------------------------------------------------------------------- /packages/js/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist" 6 | }, 7 | "include": ["./src/**/*.ts", "../core/src/**/*.ts", "../webrtc/src/**/*.ts"], 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/node/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /packages/node/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/node/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/node", 3 | "description": "SignalWire Node.js SDK", 4 | "author": "SignalWire Team ", 5 | "license": "MIT", 6 | "version": "3.0.3", 7 | "main": "dist/index.node.js", 8 | "exports": { 9 | "require": "./dist/index.node.js", 10 | "default": "./dist/index.node.mjs" 11 | }, 12 | "files": [ 13 | "dist", 14 | "src" 15 | ], 16 | "engines": { 17 | "node": ">=14" 18 | }, 19 | "keywords": [ 20 | "signalwire", 21 | "video", 22 | "relay", 23 | "sip" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/signalwire/signalwire-js" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/signalwire/signalwire-js/issues" 31 | }, 32 | "homepage": "https://github.com/signalwire/signalwire-js/tree/master/packages/node", 33 | "scripts": { 34 | "start": "sw-build --dev --node", 35 | "build": "tsc --project tsconfig.build.json && sw-build --node", 36 | "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", 37 | "prepublishOnly": "npm run build" 38 | }, 39 | "dependencies": { 40 | "@signalwire/realtime-api": "^4.0.1", 41 | "@signalwire/web-api": "^3.1.2" 42 | }, 43 | "types": "dist/node/src/index.d.ts" 44 | } 45 | -------------------------------------------------------------------------------- /packages/node/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import { WebAPI } from './index' 2 | 3 | describe('WebAPI', () => { 4 | it('should expose validat', () => { 5 | expect(WebAPI.validateRequest).toBeDefined() 6 | expect(typeof WebAPI.validateRequest).toBe('function') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/node/src/index.ts: -------------------------------------------------------------------------------- 1 | import * as RealtimeAPI from '@signalwire/realtime-api' 2 | import * as WebAPI from '@signalwire/web-api' 3 | 4 | export { RealtimeAPI, WebAPI } 5 | -------------------------------------------------------------------------------- /packages/node/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist" 6 | }, 7 | "include": ["./src/**/*.ts", "../core/src/**/*.ts"], 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/realtime-api/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /packages/realtime-api/__mocks__/ws.js: -------------------------------------------------------------------------------- 1 | export { WebSocket as default } from 'mock-socket' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | // Force module uuid to resolve with the CJS entry point, because Jest does not support package.json.exports. See https://github.com/uuidjs/uuid/issues/451 4 | uuid: require.resolve('uuid'), 5 | }, 6 | transform: { 7 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 8 | }, 9 | setupFiles: ['./src/setupTests.ts'], 10 | } 11 | -------------------------------------------------------------------------------- /packages/realtime-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/realtime-api", 3 | "description": "SignalWire RealTime SDK for Node.js", 4 | "author": "SignalWire Team ", 5 | "license": "MIT", 6 | "version": "4.1.1", 7 | "main": "dist/index.node.js", 8 | "exports": { 9 | "types": "./dist/realtime-api/src/index.d.ts", 10 | "require": "./dist/index.node.js", 11 | "default": "./dist/index.node.mjs" 12 | }, 13 | "files": [ 14 | "dist", 15 | "src" 16 | ], 17 | "engines": { 18 | "node": ">=14" 19 | }, 20 | "keywords": [ 21 | "signalwire", 22 | "video", 23 | "relay", 24 | "sip" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/signalwire/signalwire-js" 29 | }, 30 | "bugs": { 31 | "url": "https://github.com/signalwire/signalwire-js/issues" 32 | }, 33 | "homepage": "https://github.com/signalwire/signalwire-js/tree/master/packages/realtime-api", 34 | "scripts": { 35 | "start": "sw-build --dev --node --watchFormat=cjs", 36 | "build": "tsc --project tsconfig.build.json && sw-build --node", 37 | "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", 38 | "prepublishOnly": "npm run build" 39 | }, 40 | "dependencies": { 41 | "@signalwire/core": "4.2.1", 42 | "ws": "^8.17.1" 43 | }, 44 | "devDependencies": { 45 | "@types/ws": "^8.5.10" 46 | }, 47 | "types": "dist/realtime-api/src/index.d.ts" 48 | } 49 | -------------------------------------------------------------------------------- /packages/realtime-api/src/Session.ts: -------------------------------------------------------------------------------- 1 | import { BaseSession, SWCloseEvent } from '@signalwire/core' 2 | import WebSocket from 'ws' 3 | 4 | export class Session extends BaseSession { 5 | public WebSocketConstructor = WebSocket 6 | public CloseEventConstructor = SWCloseEvent 7 | public agent = process.env.SDK_PKG_AGENT! 8 | } 9 | -------------------------------------------------------------------------------- /packages/realtime-api/src/SignalWire.ts: -------------------------------------------------------------------------------- 1 | import { SWClient, SWClientOptions } from './SWClient' 2 | 3 | export const SignalWire = (options: SWClientOptions): Promise => { 4 | return new Promise(async (resolve, reject) => { 5 | const swClient = new SWClient(options) 6 | 7 | try { 8 | await swClient.connect() 9 | resolve(swClient) 10 | } catch (error) { 11 | reject(error) 12 | } 13 | }) 14 | } 15 | 16 | export type { SWClient } from './SWClient' 17 | -------------------------------------------------------------------------------- /packages/realtime-api/src/chat/Chat.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@signalwire/core' 2 | import { Chat } from './Chat' 3 | import { createClient } from '../client/createClient' 4 | 5 | describe('Chat', () => { 6 | let chat: Chat 7 | const userOptions = { 8 | host: 'example.com', 9 | project: 'example.project', 10 | token: 'example.token', 11 | } 12 | const swClientMock = { 13 | userOptions, 14 | client: createClient(userOptions), 15 | } 16 | 17 | beforeEach(() => { 18 | //@ts-expect-error 19 | chat = new Chat(swClientMock) 20 | }) 21 | 22 | afterEach(() => { 23 | jest.clearAllMocks() 24 | }) 25 | 26 | it('should have an event emitter', () => { 27 | expect(chat['emitter']).toBeInstanceOf(EventEmitter) 28 | }) 29 | 30 | it('should declare the correct event map', () => { 31 | const expectedEventMap = { 32 | onMessageReceived: 'chat.message', 33 | onMemberJoined: 'chat.member.joined', 34 | onMemberUpdated: 'chat.member.updated', 35 | onMemberLeft: 'chat.member.left', 36 | } 37 | expect(chat['_eventMap']).toEqual(expectedEventMap) 38 | }) 39 | }) 40 | -------------------------------------------------------------------------------- /packages/realtime-api/src/chat/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './chatWorker' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/Client.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Note: This file will eventually replace 3 | * packages/realtime-api/src/Client.ts 4 | */ 5 | 6 | import { 7 | BaseClient as CoreBaseClient, 8 | ClientContract, 9 | ClientEvents, 10 | } from '@signalwire/core' 11 | 12 | export interface RealtimeClient 13 | extends ClientContract { 14 | /** 15 | * Connects this client to the SignalWire network. 16 | * 17 | * As a general best practice, it is suggested to connect the event listeners 18 | * *before* connecting the client, so that no events are lost. 19 | * 20 | * @returns Upon connection, asynchronously returns an instance of this same 21 | * object. 22 | * 23 | * @example 24 | * ```typescript 25 | * const client = await createClient({project, token}) 26 | * client.video.on('room.started', async (roomSession) => { }) // connect events 27 | * await client.connect() 28 | * ``` 29 | */ 30 | connect(): Promise 31 | 32 | /** 33 | * Disconnects this client from the SignalWire network. 34 | */ 35 | disconnect(): void 36 | } 37 | 38 | export class Client extends CoreBaseClient {} 39 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/clientConnect.ts: -------------------------------------------------------------------------------- 1 | import { RealtimeClient } from './Client' 2 | 3 | export const clientConnect = (client: RealtimeClient) => { 4 | /** 5 | * We swallow the (possible) error here to avoid polluting 6 | * the stdout. The error itself won't be swallowed from 7 | * the user (it will be handled by our `rootSaga`) and we 8 | * can extend that behavior by adding the following 9 | * listener: 10 | * client.on('session.auth_error', () => { ... }) 11 | */ 12 | return client.connect().catch(() => {}) 13 | } 14 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/clientProxyFactory.test.ts: -------------------------------------------------------------------------------- 1 | import { configureFullStack } from '../testUtils' 2 | import { clientProxyFactory } from './clientProxyFactory' 3 | import { getClient } from './getClient' 4 | 5 | describe('clientProxyFactory', () => { 6 | it('should handle the automatic connection every time the user attach an event', () => { 7 | const { store, emitter, destroy } = configureFullStack() 8 | const options = { 9 | project: 'a-proj', 10 | token: 'a-proj', 11 | store, 12 | emitter, 13 | cache: new Map(), 14 | } 15 | const { client } = getClient(options) 16 | const connectMock = jest.fn() 17 | const proxiedClient = clientProxyFactory(client, { connect: connectMock }) 18 | 19 | expect(connectMock).toHaveBeenCalledTimes(0) 20 | 21 | proxiedClient.on('session.connected', () => {}) 22 | proxiedClient.on('session.reconnecting', () => {}) 23 | 24 | expect(connectMock).toHaveBeenCalledTimes(2) 25 | 26 | destroy() 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/clientProxyFactory.ts: -------------------------------------------------------------------------------- 1 | import { RealtimeClient } from './Client' 2 | import { clientConnect as baseClientConnect } from './clientConnect' 3 | 4 | interface ClientInterceptors { 5 | connect?: (client: RealtimeClient) => Promise 6 | } 7 | 8 | const defaultInterceptors: ClientInterceptors = { 9 | connect: baseClientConnect, 10 | } 11 | 12 | export const clientProxyFactory = ( 13 | client: RealtimeClient, 14 | interceptors: ClientInterceptors = defaultInterceptors 15 | ) => { 16 | const clientConnect = interceptors.connect || baseClientConnect 17 | 18 | // Client interceptors 19 | const clientOn: RealtimeClient['on'] = (...args) => { 20 | clientConnect(client) 21 | 22 | return client.on(...args) 23 | } 24 | const clientOnce: RealtimeClient['once'] = (...args) => { 25 | clientConnect(client) 26 | 27 | return client.once(...args) 28 | } 29 | 30 | return new Proxy(client, { 31 | get(target: RealtimeClient, prop: keyof RealtimeClient, receiver: any) { 32 | if (prop === 'on') { 33 | return clientOn 34 | } else if (prop === 'once') { 35 | return clientOnce 36 | } 37 | 38 | return Reflect.get(target, prop, receiver) 39 | }, 40 | }) 41 | } 42 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/createClient.ts: -------------------------------------------------------------------------------- 1 | import { connect, ClientEvents, SDKStore } from '@signalwire/core' 2 | import { setupInternals } from '../utils/internals' 3 | import { Client } from './Client' 4 | 5 | export const createClient = (userOptions: { 6 | project: string 7 | token: string 8 | logLevel?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'silent' 9 | store?: SDKStore 10 | }) => { 11 | const { emitter, store } = setupInternals(userOptions) 12 | const client = connect({ 13 | store: userOptions.store ?? store, 14 | Component: Client, 15 | })({ ...userOptions, store, emitter }) 16 | 17 | return client 18 | } 19 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/getClient.test.ts: -------------------------------------------------------------------------------- 1 | import { configureFullStack } from '../testUtils' 2 | import { getClient } from './getClient' 3 | 4 | describe('getClient', () => { 5 | it.skip('should cache clients by project and key', () => { 6 | const { store, emitter, destroy } = configureFullStack() 7 | const options = { 8 | project: 'a-proj', 9 | token: 'a-proj', 10 | store, 11 | emitter, 12 | cache: new Map(), 13 | } 14 | const clientA = getClient(options) 15 | const clientB = getClient(options) 16 | const clientC = getClient({ 17 | ...options, 18 | project: 'c-project', 19 | }) 20 | const clientD = getClient({ 21 | ...options, 22 | token: 'd-project', 23 | }) 24 | 25 | expect(clientA).toEqual(clientB) 26 | expect(clientA).not.toEqual(clientC) 27 | expect(clientA).not.toEqual(clientD) 28 | expect(options.cache.size).toEqual(3) 29 | 30 | destroy() 31 | }) 32 | }) 33 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Client' 2 | export * from './clientConnect' 3 | export * from './setupClient' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/client/setupClient.ts: -------------------------------------------------------------------------------- 1 | import { getLogger, UserOptions } from '@signalwire/core' 2 | import { getCredentials } from '../utils/internals' 3 | import { clientProxyFactory } from './clientProxyFactory' 4 | import { getClient, ClientConfig, ClientCache } from './getClient' 5 | 6 | interface SetupClientOptions { 7 | project?: string 8 | token?: string 9 | logLevel?: UserOptions['logLevel'] 10 | cache?: ClientCache 11 | } 12 | 13 | export const setupClient = (userOptions?: SetupClientOptions): ClientConfig => { 14 | const credentials = getCredentials({ 15 | token: userOptions?.token, 16 | project: userOptions?.project, 17 | }) 18 | const { client, store, emitter } = getClient({ 19 | ...userOptions, 20 | ...credentials, 21 | }) 22 | 23 | // @ts-expect-error 24 | client.session.on('session.auth_error', () => { 25 | getLogger().error("Wrong credentials: couldn't connect the client.") 26 | 27 | // TODO: we can execute the future `onConnectError` from here. 28 | }) 29 | 30 | const proxiedClient = clientProxyFactory(client) 31 | 32 | return { 33 | client: proxiedClient, 34 | store, 35 | emitter, 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /packages/realtime-api/src/common/clientContext.ts: -------------------------------------------------------------------------------- 1 | import { RealtimeClient, clientConnect } from '../client/index' 2 | import type { ExecuteParams } from '@signalwire/core' 3 | 4 | export const clientContextInterceptorsFactory = (client: RealtimeClient) => { 5 | return { 6 | async addContexts(contexts: string[]) { 7 | await clientConnect(client) 8 | const executeParams: ExecuteParams = { 9 | method: 'signalwire.receive', 10 | params: { 11 | contexts, 12 | }, 13 | } 14 | 15 | // @ts-expect-error 16 | return client.execute(executeParams) 17 | }, 18 | async removeContexts(contexts: string[]) { 19 | await clientConnect(client) 20 | const executeParams: ExecuteParams = { 21 | method: 'signalwire.unreceive', 22 | params: { 23 | contexts, 24 | }, 25 | } 26 | 27 | // @ts-expect-error 28 | return client.execute(executeParams) 29 | }, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/realtime-api/src/configure/index.ts: -------------------------------------------------------------------------------- 1 | export interface GlobalConfig { 2 | token?: string 3 | project?: string 4 | } 5 | 6 | let GLOBAL_CONFIG: GlobalConfig = {} 7 | 8 | export const getConfig = (): GlobalConfig => { 9 | return GLOBAL_CONFIG 10 | } 11 | 12 | /** @ignore */ 13 | export interface ConfigOptions extends GlobalConfig { 14 | /** @internal */ 15 | cache?: GlobalConfig 16 | } 17 | 18 | /** @internal */ 19 | export const config = ({ 20 | cache = GLOBAL_CONFIG, 21 | ...options 22 | }: ConfigOptions) => { 23 | if (cache) { 24 | GLOBAL_CONFIG = cache 25 | } 26 | 27 | Object.entries(options).forEach(([key, value]) => { 28 | // TODO: filter out properties 29 | // @ts-expect-error 30 | GLOBAL_CONFIG[key] = value 31 | }) 32 | } 33 | -------------------------------------------------------------------------------- /packages/realtime-api/src/index.ts: -------------------------------------------------------------------------------- 1 | /** @ignore */ 2 | export * from './configure' 3 | 4 | export * as Messaging from './messaging/Messaging' 5 | 6 | export * as Chat from './chat/Chat' 7 | 8 | export * as PubSub from './pubSub/PubSub' 9 | 10 | export * as Task from './task/Task' 11 | 12 | export * as Voice from './voice/Voice' 13 | 14 | export * as Video from './video/Video' 15 | 16 | /** 17 | * Access all the SignalWire APIs with a single instance. You can initiate a {@link SignalWire} to 18 | * use Messaging, Chat, PubSub, Task, Voice, and Video APIs. 19 | * 20 | * @example 21 | * 22 | * The following example creates a single client and uses Task and Voice APIs. 23 | * 24 | * ```javascript 25 | * const client = await SignalWire({ 26 | * project: "", 27 | * token: "", 28 | * }) 29 | * 30 | * await client.task.listen({ 31 | * topics: ['office'], 32 | * onTaskReceived: (payload) => { 33 | * console.log('message.received', payload)} 34 | * }) 35 | * 36 | * await client.task.send({ 37 | * topic: 'office', 38 | * message: '+1yyy', 39 | * }) 40 | * 41 | * await client.voice.listen({ 42 | * topics: ['office'], 43 | * onCallReceived: (call) => { 44 | * console.log('call.received', call)} 45 | * }) 46 | * 47 | * await client.voice.dialPhone({ 48 | * from: '+1xxx', 49 | * to: '+1yyy', 50 | * }) 51 | * ``` 52 | */ 53 | export * from './SignalWire' 54 | -------------------------------------------------------------------------------- /packages/realtime-api/src/messaging/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './messagingWorker' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/src/pubSub/PubSub.test.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@signalwire/core' 2 | import { PubSub } from './PubSub' 3 | import { createClient } from '../client/createClient' 4 | 5 | describe('PubSub', () => { 6 | let pubSub: PubSub 7 | const userOptions = { 8 | host: 'example.com', 9 | project: 'example.project', 10 | token: 'example.token', 11 | } 12 | const swClientMock = { 13 | userOptions, 14 | client: createClient(userOptions), 15 | } 16 | 17 | beforeEach(() => { 18 | //@ts-expect-error 19 | pubSub = new PubSub(swClientMock) 20 | }) 21 | 22 | afterEach(() => { 23 | jest.clearAllMocks() 24 | }) 25 | 26 | it('should have an event emitter', () => { 27 | expect(pubSub['emitter']).toBeInstanceOf(EventEmitter) 28 | }) 29 | 30 | it('should declare the correct event map', () => { 31 | const expectedEventMap = { 32 | onMessageReceived: 'chat.message', 33 | } 34 | expect(pubSub['_eventMap']).toEqual(expectedEventMap) 35 | }) 36 | }) 37 | -------------------------------------------------------------------------------- /packages/realtime-api/src/pubSub/PubSub.ts: -------------------------------------------------------------------------------- 1 | import { 2 | PubSubMessageEventName, 3 | PubSubNamespace, 4 | PubSubMessage, 5 | } from '@signalwire/core' 6 | import { SWClient } from '../SWClient' 7 | import { pubSubWorker } from './workers' 8 | import { BaseChat } from '../chat/BaseChat' 9 | import { RealTimePubSubEvents } from '../types/pubSub' 10 | 11 | interface PubSubListenOptions { 12 | channels: string[] 13 | onMessageReceived?: (message: PubSubMessage) => unknown 14 | } 15 | 16 | type PubSubListenersKeys = keyof Omit< 17 | PubSubListenOptions, 18 | 'channels' | 'topics' 19 | > 20 | 21 | export class PubSub extends BaseChat< 22 | PubSubListenOptions, 23 | RealTimePubSubEvents 24 | > { 25 | protected _eventMap: Record< 26 | PubSubListenersKeys, 27 | `${PubSubNamespace}.${PubSubMessageEventName}` 28 | > = { 29 | onMessageReceived: 'chat.message', 30 | } 31 | 32 | constructor(options: SWClient) { 33 | super(options) 34 | 35 | this._client.runWorker('pubSubWorker', { 36 | worker: pubSubWorker, 37 | initialState: { 38 | pubSub: this, 39 | }, 40 | }) 41 | } 42 | } 43 | 44 | export type { PubSubMessageContract } from '@signalwire/core' 45 | -------------------------------------------------------------------------------- /packages/realtime-api/src/pubSub/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './pubSubWorker' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/src/setupTests.ts: -------------------------------------------------------------------------------- 1 | jest.mock('@signalwire/core', () => { 2 | return { 3 | ...(jest.requireActual('@signalwire/core') as any), 4 | logger: { 5 | error: jest.fn(), 6 | warn: jest.fn(), 7 | log: jest.fn(), 8 | info: jest.fn(), 9 | debug: jest.fn(), 10 | trace: jest.fn(), 11 | levels: jest.fn(), 12 | setLevel: jest.fn(), 13 | }, 14 | } 15 | }) 16 | -------------------------------------------------------------------------------- /packages/realtime-api/src/task/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './taskWorker' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/src/task/workers/taskWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | sagaEffects, 4 | SagaIterator, 5 | SDKWorker, 6 | SDKActions, 7 | TaskAction, 8 | } from '@signalwire/core' 9 | import { prefixEvent } from '../../utils/internals' 10 | import type { Client } from '../../client/Client' 11 | import { Task } from '../Task' 12 | 13 | interface TaskWorkerInitialState { 14 | task: Task 15 | } 16 | 17 | export const taskWorker: SDKWorker = function* (options): SagaIterator { 18 | getLogger().trace('taskWorker started') 19 | const { 20 | channels: { swEventChannel }, 21 | initialState, 22 | } = options 23 | 24 | const { task } = initialState as TaskWorkerInitialState 25 | 26 | function* worker(action: TaskAction) { 27 | const { context } = action.payload 28 | 29 | // @ts-expect-error 30 | task.emit(prefixEvent(context, 'task.received'), action.payload.message) 31 | } 32 | 33 | const isTaskEvent = (action: SDKActions) => 34 | action.type === 'queuing.relay.tasks' 35 | 36 | while (true) { 37 | const action = yield sagaEffects.take(swEventChannel, isTaskEvent) 38 | 39 | yield sagaEffects.fork(worker, action) 40 | } 41 | 42 | getLogger().trace('taskWorker ended') 43 | } 44 | -------------------------------------------------------------------------------- /packages/realtime-api/src/types/chat.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ChatMember, 3 | ChatMemberEventNames, 4 | ChatMessage, 5 | ChatMessageEventName, 6 | ChatNamespace, 7 | } from '@signalwire/core' 8 | 9 | export type RealTimeChatApiEventsHandlerMapping = Record< 10 | `${ChatNamespace}.${ChatMessageEventName}`, 11 | (message: ChatMessage) => void 12 | > & 13 | Record< 14 | `${ChatNamespace}.${ChatMemberEventNames}`, 15 | (member: ChatMember) => void 16 | > 17 | 18 | export type RealTimeChatEvents = { 19 | [k in keyof RealTimeChatApiEventsHandlerMapping]: RealTimeChatApiEventsHandlerMapping[k] 20 | } 21 | -------------------------------------------------------------------------------- /packages/realtime-api/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './video' 2 | export * from './task' 3 | export * from './messaging' 4 | export * from './voice' 5 | -------------------------------------------------------------------------------- /packages/realtime-api/src/types/messaging.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | MessageReceivedEventName, 3 | MessageUpdatedEventName, 4 | } from '@signalwire/core' 5 | import type { MessageContract } from '../messaging/Message' 6 | 7 | export type RealTimeMessagingApiEventsHandlerMapping = Record< 8 | MessageReceivedEventName | MessageUpdatedEventName, 9 | (message: MessageContract) => void 10 | > 11 | 12 | export type MessagingClientApiEvents = { 13 | [k in keyof RealTimeMessagingApiEventsHandlerMapping]: RealTimeMessagingApiEventsHandlerMapping[k] 14 | } 15 | -------------------------------------------------------------------------------- /packages/realtime-api/src/types/pubSub.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | PubSubMessage, 3 | PubSubMessageEventName, 4 | PubSubNamespace, 5 | } from '@signalwire/core' 6 | 7 | export type RealTimePubSubApiEventsHandlerMapping = Record< 8 | `${PubSubNamespace}.${PubSubMessageEventName}`, 9 | (message: PubSubMessage) => void 10 | > 11 | 12 | export type RealTimePubSubEvents = { 13 | [k in keyof RealTimePubSubApiEventsHandlerMapping]: RealTimePubSubApiEventsHandlerMapping[k] 14 | } 15 | -------------------------------------------------------------------------------- /packages/realtime-api/src/types/task.ts: -------------------------------------------------------------------------------- 1 | import type { TaskReceivedEventName } from '@signalwire/core' 2 | 3 | export type RealTimeTaskApiEventsHandlerMapping = Record< 4 | TaskReceivedEventName, 5 | (params: Record) => void 6 | > 7 | 8 | export type TaskClientApiEvents = { 9 | [k in keyof RealTimeTaskApiEventsHandlerMapping]: RealTimeTaskApiEventsHandlerMapping[k] 10 | } 11 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/RoomSessionMember/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RoomSessionMember' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/RoomSessionPlayback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RoomSessionPlayback' 2 | export * from './decoratePlaybackPromise' 3 | export { decoratePlaybackPromise } from './decoratePlaybackPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/RoomSessionRecording/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RoomSessionRecording' 2 | export * from './decorateRecordingPromise' 3 | export { decorateRecordingPromise } from './decorateRecordingPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/RoomSessionStream/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RoomSessionStream' 2 | export * from './decorateStreamPromise' 3 | export { decorateStreamPromise } from './decorateStreamPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/methods/index.ts: -------------------------------------------------------------------------------- 1 | import { EventEmitter } from '@signalwire/core' 2 | import { ListenSubscriber } from '../../ListenSubscriber' 3 | import type { Client } from '../../client/Client' 4 | import { 5 | RealTimeRoomEventsHandlerMapping, 6 | RealTimeRoomListeners, 7 | } from '../../types' 8 | 9 | // @ts-expect-error 10 | export interface BaseRoomInterface 11 | extends ListenSubscriber< 12 | RealTimeRoomListeners, 13 | RealTimeRoomEventsHandlerMapping 14 | > { 15 | _client: Client 16 | roomId: string 17 | roomSessionId: string 18 | memberId: string 19 | once>( 20 | event: T, 21 | fn: EventEmitter.EventListener 22 | ): void 23 | off>( 24 | event: T, 25 | fn: EventEmitter.EventListener 26 | ): void 27 | } 28 | 29 | export * as RoomMethods from './methods' 30 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './videoCallingWorker' 2 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/workers/videoLayoutWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | SagaIterator, 4 | MapToPubSubShape, 5 | VideoLayoutChangedEvent, 6 | toExternalJSON, 7 | stripNamespacePrefix, 8 | VideoLayoutEventNames, 9 | } from '@signalwire/core' 10 | import { RoomSession } from '../RoomSession' 11 | import { VideoCallWorkerParams } from './videoCallingWorker' 12 | 13 | export const videoLayoutWorker = function* ( 14 | options: VideoCallWorkerParams> 15 | ): SagaIterator { 16 | getLogger().trace('videoLayoutWorker started') 17 | const { 18 | action: { type, payload }, 19 | instanceMap: { get }, 20 | } = options 21 | 22 | const roomSessionInstance = get(payload.room_session_id) 23 | if (!roomSessionInstance) { 24 | throw new Error('Missing room session instance for playback') 25 | } 26 | 27 | // TODO: Implement a Layout object when we have a proper payload from the backend 28 | // Create a layout instance and emit that instance 29 | 30 | const event = stripNamespacePrefix(type) as VideoLayoutEventNames 31 | 32 | switch (type) { 33 | case 'video.layout.changed': 34 | roomSessionInstance.emit(event, toExternalJSON(payload)) 35 | break 36 | default: 37 | break 38 | } 39 | 40 | getLogger().trace('videoLayoutWorker ended') 41 | } 42 | -------------------------------------------------------------------------------- /packages/realtime-api/src/video/workers/videoRoomAudienceWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | SagaIterator, 4 | MapToPubSubShape, 5 | toExternalJSON, 6 | VideoRoomAudienceCountEvent, 7 | } from '@signalwire/core' 8 | import { RoomSession } from '../RoomSession' 9 | import { VideoCallWorkerParams } from './videoCallingWorker' 10 | 11 | export const videoRoomAudienceWorker = function* ( 12 | options: VideoCallWorkerParams> 13 | ): SagaIterator { 14 | getLogger().trace('videoRoomAudienceWorker started') 15 | const { 16 | action: { type, payload }, 17 | instanceMap: { get }, 18 | } = options 19 | 20 | const roomSessionInstance = get(payload.room_session_id) 21 | if (!roomSessionInstance) { 22 | throw new Error('Missing room session instance for playback') 23 | } 24 | 25 | switch (type) { 26 | case 'video.room.audience_count': 27 | // @ts-expect-error 28 | roomSessionInstance.emit('room.audienceCount', toExternalJSON(payload)) 29 | break 30 | default: 31 | break 32 | } 33 | 34 | getLogger().trace('videoRoomAudienceWorker ended') 35 | } 36 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallCollect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CallCollect' 2 | export * from './decorateCollectPromise' 3 | export { decorateCollectPromise } from './decorateCollectPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallDetect/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CallDetect' 2 | export * from './decorateDetectPromise' 3 | export { decorateDetectPromise } from './decorateDetectPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallPlayback/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CallPlayback' 2 | export * from './decoratePlaybackPromise' 3 | export { decoratePlaybackPromise } from './decoratePlaybackPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallPrompt/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CallPrompt' 2 | export * from './decoratePromptPromise' 3 | export { decoratePromptPromise } from './decoratePromptPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallRecording/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CallRecording' 2 | export * from './decorateRecordingPromise' 3 | export { decorateRecordingPromise } from './decorateRecordingPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallTap/decorateTapPromise.ts: -------------------------------------------------------------------------------- 1 | import { 2 | CallingCallTapEndState, 3 | CallingCallTapState, 4 | Promisify, 5 | } from '@signalwire/core' 6 | import { Call } from '../Call' 7 | import { CallTap } from './CallTap' 8 | import { decoratePromise } from '../../decoratePromise' 9 | import { CallTapListeners } from '../../types' 10 | 11 | export interface CallTapEnded { 12 | id: string 13 | callId: string 14 | nodeId: string 15 | controlId: string 16 | state: CallingCallTapEndState 17 | } 18 | 19 | export interface CallTapPromise 20 | extends Promise, 21 | Omit, 'state'> { 22 | onStarted: () => Promise 23 | onEnded: () => Promise 24 | listen: (listeners: CallTapListeners) => Promise<() => Promise> 25 | stop: () => Promise 26 | ended: () => Promise 27 | state: Promise 28 | } 29 | 30 | export const getters = ['id', 'callId', 'nodeId', 'controlId', 'state'] 31 | 32 | export const methods = ['stop', 'ended'] 33 | 34 | export function decorateTapPromise(this: Call, innerPromise: Promise) { 35 | // prettier-ignore 36 | return (decoratePromise).call(this, { 37 | promise: innerPromise, 38 | namespace: 'tap', 39 | methods, 40 | getters, 41 | }) as CallTapPromise 42 | } 43 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/CallTap/index.ts: -------------------------------------------------------------------------------- 1 | export * from './CallTap' 2 | export * from './decorateTapPromise' 3 | export { decorateTapPromise } from './decorateTapPromise' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/workers/handlers/callDialEventsHandler.ts: -------------------------------------------------------------------------------- 1 | import { CallingCallDialEventParams, InstanceMap } from '@signalwire/core' 2 | import { Call } from '../../Call' 3 | import { Voice } from '../../Voice' 4 | 5 | interface CallDialEventsHandlerOptions { 6 | payload: CallingCallDialEventParams 7 | instanceMap: InstanceMap 8 | voice: Voice 9 | } 10 | 11 | export function handleCallDialEvents(options: CallDialEventsHandlerOptions) { 12 | const { payload, instanceMap, voice } = options 13 | const { get } = instanceMap 14 | 15 | switch (payload.dial_state) { 16 | case 'failed': { 17 | // @ts-expect-error 18 | voice.emit('dial.failed', payload) 19 | return true 20 | } 21 | case 'answered': { 22 | const callInstance = get(payload.call.call_id) 23 | callInstance.setPayload(payload.call) 24 | // @ts-expect-error 25 | voice.emit('dial.answered', callInstance) 26 | return true 27 | } 28 | default: 29 | return false 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/workers/handlers/callStateEventsHandler.ts: -------------------------------------------------------------------------------- 1 | import { CallingCallStateEventParams, InstanceMap } from '@signalwire/core' 2 | import { RealTimeCallListeners } from '../../../types' 3 | import { Call } from '../../Call' 4 | import { Voice } from '../../Voice' 5 | 6 | interface CallStateEventsHandlerOptions { 7 | payload: CallingCallStateEventParams 8 | voice: Voice 9 | instanceMap: InstanceMap 10 | listeners?: RealTimeCallListeners 11 | } 12 | 13 | export function handleCallStateEvents(options: CallStateEventsHandlerOptions) { 14 | const { 15 | payload, 16 | voice, 17 | listeners, 18 | instanceMap: { get, set, remove }, 19 | } = options 20 | 21 | let callInstance = get(payload.call_id) 22 | if (!callInstance) { 23 | callInstance = new Call({ 24 | voice, 25 | payload, 26 | listeners, 27 | }) 28 | } else { 29 | callInstance.setPayload(payload) 30 | } 31 | set(payload.call_id, callInstance) 32 | 33 | switch (payload.call_state) { 34 | case 'ended': { 35 | callInstance.emit('call.state', callInstance) 36 | 37 | // Resolves the promise when user disconnects using a peer call instance 38 | // @ts-expect-error 39 | callInstance.emit('connect.disconnected', callInstance) 40 | remove(payload.call_id) 41 | 42 | return true 43 | } 44 | default: 45 | callInstance.emit('call.state', callInstance) 46 | return false 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/workers/handlers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './callStateEventsHandler' 2 | export * from './callConnectEventsHandler' 3 | export * from './callDialEventsHandler' 4 | -------------------------------------------------------------------------------- /packages/realtime-api/src/voice/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './voiceCallReceiveWorker' 2 | export * from './voiceCallPlayWorker' 3 | export * from './voiceCallRecordWorker' 4 | export * from './voiceCallCollectWorker' 5 | export * from './voiceCallTapWorker' 6 | export * from './voiceCallConnectWorker' 7 | export * from './voiceCallDialWorker' 8 | export * from './VoiceCallSendDigitWorker' 9 | export * from './voiceCallDetectWorker' 10 | export * from './voiceCallCollectWorker' 11 | -------------------------------------------------------------------------------- /packages/realtime-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist" 6 | }, 7 | "include": ["./src/**/*.ts", "../core/src/**/*.ts"], 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/swaig/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /packages/swaig/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | // Force module uuid to resolve with the CJS entry point, because Jest does not support package.json.exports. See https://github.com/uuidjs/uuid/issues/451 4 | uuid: require.resolve('uuid'), 5 | }, 6 | transform: { 7 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 8 | }, 9 | // setupFiles: ['./src/setupTests.ts'], 10 | } 11 | -------------------------------------------------------------------------------- /packages/swaig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/swaig", 3 | "description": "SignalWire SWAIG SDK", 4 | "author": "SignalWire Team ", 5 | "license": "MIT", 6 | "version": "0.0.1", 7 | "main": "dist/index.node.js", 8 | "exports": { 9 | "require": "./dist/index.node.js", 10 | "default": "./dist/index.node.mjs" 11 | }, 12 | "files": [ 13 | "dist", 14 | "src" 15 | ], 16 | "engines": { 17 | "node": ">=18" 18 | }, 19 | "keywords": [ 20 | "signalwire", 21 | "video", 22 | "relay", 23 | "swaig" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/signalwire/signalwire-js" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/signalwire/signalwire-js/issues" 31 | }, 32 | "homepage": "https://github.com/signalwire/signalwire-js/tree/master/packages/swaig", 33 | "scripts": { 34 | "start": "sw-build --dev --node --watchFormat=cjs", 35 | "build": "tsc --project tsconfig.build.json && sw-build --node", 36 | "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", 37 | "prepublishOnly": "npm run build" 38 | }, 39 | "dependencies": { 40 | "@fastify/basic-auth": "^6.0.1", 41 | "@fastify/swagger": "^9.2.0", 42 | "@fastify/swagger-ui": "^5.1.0", 43 | "fastify": "^5.1.0" 44 | }, 45 | "types": "dist/swaig/src/index.d.ts", 46 | "devDependencies": { 47 | "json-schema-to-ts": "^3.1.1" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /packages/swaig/src/index.test.ts: -------------------------------------------------------------------------------- 1 | import * as SDK from './index' 2 | 3 | describe('SWAIG SDK', () => { 4 | it('should export a SWAIG function', () => { 5 | expect(SDK.SWAIG).toBeDefined() 6 | expect(typeof SDK.SWAIG).toBe('function') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/swaig/src/index.ts: -------------------------------------------------------------------------------- 1 | export { SWAIG } from './swaig' 2 | 3 | export * from './types' 4 | -------------------------------------------------------------------------------- /packages/swaig/src/swaig.ts: -------------------------------------------------------------------------------- 1 | import { Server } from './Server' 2 | import type { ServerOptions } from './types' 3 | 4 | export interface SWAIGOptions extends ServerOptions {} 5 | 6 | export interface SWAIG { 7 | server: Server['instance'] 8 | addFunction: Server['defineRoute'] 9 | run: Server['run'] 10 | close: Server['close'] 11 | } 12 | 13 | export async function SWAIG(options: SWAIGOptions): Promise { 14 | const server = new Server(options) 15 | 16 | await server.init() 17 | 18 | const service = { 19 | server: server.instance, 20 | addFunction: server.defineRoute.bind(server), 21 | run: server.run.bind(server), 22 | close: server.close.bind(server), 23 | } 24 | return service 25 | } 26 | -------------------------------------------------------------------------------- /packages/swaig/src/types.ts: -------------------------------------------------------------------------------- 1 | import { FastifySwaggerUiOptions } from '@fastify/swagger-ui' 2 | import { OpenAPIV3, OpenAPIV2 } from 'openapi-types' 3 | import type { JSONSchema } from 'json-schema-to-ts' 4 | 5 | export interface Signature { 6 | function: string 7 | purpose: string 8 | argument: JSONSchema 9 | web_hook_url: string 10 | web_hook_auth_user?: string 11 | web_hook_auth_password?: string 12 | meta_data_token?: string 13 | } 14 | 15 | export interface ServerRunParams { 16 | port?: number 17 | host?: string 18 | } 19 | export interface ServerDefineRouteParams { 20 | name: string 21 | purpose: string 22 | argument: T 23 | username?: string 24 | password?: string 25 | token?: string 26 | description?: string 27 | summary?: string 28 | tags?: string[] 29 | } 30 | export interface CustomRouteHandlerResponse { 31 | response: string 32 | action?: Record[] 33 | } 34 | export type CustomRouteHandler = ( 35 | params: T, 36 | extra: any 37 | ) => Promise 38 | 39 | export interface ServerOptions { 40 | baseUrl: string 41 | username?: string 42 | password?: string 43 | token?: string 44 | documentation?: { 45 | openapi?: OpenAPIV3.Document 46 | swagger?: OpenAPIV2.Document 47 | route?: string 48 | ui?: FastifySwaggerUiOptions 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/swaig/src/utils.ts: -------------------------------------------------------------------------------- 1 | import type { FastifyRequest, FastifyReply } from 'fastify' 2 | 3 | export const swLogoUrl = 4 | process.env.SWAIG_SDK_LOGO ?? 5 | 'https://developer.signalwire.com/landing-assets/images/logo.svg' 6 | 7 | export async function validate( 8 | username: string, 9 | password: string, 10 | req: FastifyRequest, 11 | _reply: FastifyReply 12 | ) { 13 | if ( 14 | (req.webHookUsername && req.webHookUsername !== username) || 15 | (req.webHookPassword && req.webHookPassword !== password) 16 | ) { 17 | throw new Error('Unauthorized') 18 | } 19 | } 20 | 21 | export const fetchSWLogo = async (): Promise => { 22 | try { 23 | const response = await fetch(swLogoUrl) 24 | const arrayBuffer = await response.arrayBuffer() 25 | return Buffer.from(arrayBuffer) 26 | } catch (error) { 27 | console.error('Error fetching the logo', error) 28 | return undefined 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/swaig/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist", 6 | "resolveJsonModule": true 7 | }, 8 | "include": [ 9 | "./src/**/*.ts", 10 | "./package.json" 11 | ], 12 | "exclude": [ 13 | "**/*.test.ts" 14 | ] 15 | } -------------------------------------------------------------------------------- /packages/web-api/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /packages/web-api/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | transform: { 3 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /packages/web-api/mocks/rest.ts: -------------------------------------------------------------------------------- 1 | import { http as rest, HttpResponse} from 'msw' 2 | 3 | export const handlers = [ 4 | rest.get(`*/video/rooms/:id`, async ({request: req, params, }) => { 5 | 6 | if (params.id === 'timeout') { 7 | await new Promise((r) => setTimeout(r, 100000)) 8 | } else if (req.headers.get('authorization')?.includes('invalid')) { 9 | return HttpResponse.text('Unauthorized', 10 | { 11 | status: 401, 12 | statusText: 'Unauthorized', 13 | }, 14 | ) 15 | } else if (params.id === 'non-existing-id') { 16 | return HttpResponse.text('Not Found', 17 | { 18 | status: 404, 19 | statusText: 'Not Found', 20 | }, 21 | ) 22 | } 23 | 24 | return HttpResponse.json({ 25 | id: '63df00d5-85a7-4633-82e2-6d61d8143dfb', 26 | name: 'karaoke_night', 27 | display_name: 'Mandatory Fun Activities', 28 | max_participants: 5, 29 | delete_on_end: false, 30 | starts_at: '2021-04-27T19:40:21Z', 31 | ends_at: '2021-04-27T19:55:21Z', 32 | created_at: '2021-04-27T19:35:21Z', 33 | updated_at: '2021-04-27T19:35:21Z', 34 | }) 35 | 36 | }), 37 | ] 38 | -------------------------------------------------------------------------------- /packages/web-api/mocks/server.ts: -------------------------------------------------------------------------------- 1 | import { setupServer } from 'msw/node' 2 | import { handlers } from './rest' 3 | 4 | export const server = setupServer(...handlers) 5 | -------------------------------------------------------------------------------- /packages/web-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/web-api", 3 | "description": "SignalWire Web-API SDK for Node.js", 4 | "author": "SignalWire Team ", 5 | "license": "MIT", 6 | "version": "3.1.3", 7 | "main": "dist/index.node.js", 8 | "exports": { 9 | "require": "./dist/index.node.js", 10 | "default": "./dist/index.node.mjs" 11 | }, 12 | "files": [ 13 | "dist", 14 | "src" 15 | ], 16 | "engines": { 17 | "node": ">=14" 18 | }, 19 | "keywords": [ 20 | "signalwire", 21 | "video", 22 | "relay", 23 | "sip" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/signalwire/signalwire-js" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/signalwire/signalwire-js/issues" 31 | }, 32 | "homepage": "https://github.com/signalwire/signalwire-js/tree/master/packages/web-api", 33 | "scripts": { 34 | "start": "sw-build --dev --node --watchFormat=cjs", 35 | "build": "tsc --project tsconfig.build.json && sw-build --node", 36 | "test": "NODE_ENV=test jest --detectOpenHandles --forceExit", 37 | "prepublishOnly": "npm run build" 38 | }, 39 | "dependencies": { 40 | "@signalwire/compatibility-api": "^3.1.4", 41 | "@signalwire/core": "4.2.1", 42 | "node-abort-controller": "^3.1.1", 43 | "node-fetch": "^2.6.11" 44 | }, 45 | "devDependencies": { 46 | "@types/node-fetch": "^2.5.10", 47 | "msw": "^2.6.6" 48 | }, 49 | "types": "dist/web-api/src/index.d.ts" 50 | } 51 | -------------------------------------------------------------------------------- /packages/web-api/src/createRestClient.test.ts: -------------------------------------------------------------------------------- 1 | import { createRestClient } from './createRestClient' 2 | 3 | describe('createRestClient', () => { 4 | it('should return an object with all the available methods', () => { 5 | const client = createRestClient({ 6 | projectId: '', 7 | projectToken: '', 8 | spaceHost: 'space.host.io', 9 | }) 10 | 11 | expect(client).toHaveProperty('createRoom') 12 | expect(client).toHaveProperty('deleteRoom') 13 | expect(client).toHaveProperty('getRoomById') 14 | expect(client).toHaveProperty('getRoomByName') 15 | expect(client).toHaveProperty('listAllRooms') 16 | expect(client).toHaveProperty('updateRoom') 17 | expect(client).toHaveProperty('createVRT') 18 | }) 19 | 20 | it('should throw when required options are missing', () => { 21 | expect(() => createRestClient()).toThrow('Missing required options') 22 | }) 23 | }) 24 | -------------------------------------------------------------------------------- /packages/web-api/src/createRestClient.ts: -------------------------------------------------------------------------------- 1 | import { createRoomFactory } from './rooms/createRoomFactory' 2 | import { createVRTFactory } from './rooms/createVRTFactory' 3 | import { deleteRoomFactory } from './rooms/deleteRoomFactory' 4 | import { getRoomFactory } from './rooms/getRoomFactory' 5 | import { listAllRoomsFactory } from './rooms/listAllRoomsFactory' 6 | import { updateRoomFactory } from './rooms/updateRoomFactory' 7 | import { Client } from './types' 8 | import { getConfig } from './utils/getConfig' 9 | import { createHttpClient } from './utils/httpClient' 10 | 11 | /** 12 | * 13 | * NOT USED 14 | * 15 | */ 16 | export const createRestClient: Client = (options = {}) => { 17 | const config = getConfig(options) 18 | 19 | const client = createHttpClient({ 20 | baseUrl: config.baseUrl, 21 | headers: { 22 | Authorization: `Basic ${Buffer.from(config.authCreds).toString( 23 | 'base64' 24 | )}`, 25 | }, 26 | }) 27 | 28 | const createRoom = createRoomFactory(client) 29 | const createVRT = createVRTFactory(client) 30 | const deleteRoom = deleteRoomFactory(client) 31 | const getRoom = getRoomFactory(client) 32 | const listAllRooms = listAllRoomsFactory(client) 33 | const updateRoom = updateRoomFactory(client) 34 | 35 | return { 36 | createRoom, 37 | deleteRoom, 38 | ...getRoom, 39 | listAllRooms, 40 | updateRoom, 41 | createVRT, 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/web-api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { validateRequest } from './validateRequest' 2 | 3 | export { validateRequest } 4 | -------------------------------------------------------------------------------- /packages/web-api/src/rooms/createRoomFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, RoomResponse } from '../types' 2 | interface CreateRoomOptions { 3 | name: string 4 | displayName?: string 5 | maxParticipants?: number 6 | deleteOnEnd?: boolean 7 | startsAt?: string 8 | endsAt?: string 9 | } 10 | 11 | export type CreateRoom = (options: CreateRoomOptions) => Promise 12 | 13 | type CreateRoomFactory = (client: HttpClient) => CreateRoom 14 | 15 | export const createRoomFactory: CreateRoomFactory = (client) => async ( 16 | options 17 | ) => { 18 | const { 19 | name, 20 | displayName: display_name, 21 | maxParticipants: max_participants, 22 | deleteOnEnd: delete_on_end, 23 | startsAt: starts_at, 24 | endsAt: ends_at, 25 | } = options 26 | 27 | const { body } = await client('video/rooms', { 28 | method: 'POST', 29 | body: { 30 | name, 31 | display_name, 32 | max_participants, 33 | delete_on_end, 34 | starts_at, 35 | ends_at, 36 | }, 37 | }) 38 | 39 | return body 40 | } 41 | -------------------------------------------------------------------------------- /packages/web-api/src/rooms/createVRTFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '../types' 2 | 3 | interface CreateVRTOptions { 4 | roomName: string 5 | userName: string 6 | scopes?: string[] 7 | } 8 | 9 | interface CreateVRTResponse { 10 | token: string 11 | } 12 | 13 | export type CreateVRT = ( 14 | options: CreateVRTOptions 15 | ) => Promise 16 | 17 | type CreateVRTFactory = (client: HttpClient) => CreateVRT 18 | 19 | export const createVRTFactory: CreateVRTFactory = (client) => async ( 20 | options 21 | ) => { 22 | const { roomName: room_name, userName: user_name, scopes } = options 23 | const { body } = await client('video/room_tokens', { 24 | method: 'POST', 25 | body: { 26 | room_name, 27 | user_name, 28 | scopes, 29 | }, 30 | }) 31 | 32 | return body 33 | } 34 | -------------------------------------------------------------------------------- /packages/web-api/src/rooms/deleteRoomFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient } from '../types' 2 | interface DeleteRoomOptions { 3 | id: string 4 | } 5 | 6 | export type DeleteRoom = (options: DeleteRoomOptions) => Promise 7 | 8 | type DeleteRoomFactory = (client: HttpClient) => DeleteRoom 9 | 10 | export const deleteRoomFactory: DeleteRoomFactory = (client) => async ( 11 | options 12 | ) => { 13 | const { id } = options 14 | 15 | await client(`video/rooms/${id}`, { 16 | method: 'DELETE', 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /packages/web-api/src/rooms/getRoomFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, RoomResponse } from '../types' 2 | 3 | export interface GetRoomByIdOptions { 4 | id: string 5 | } 6 | 7 | export interface GetRoomByNameOptions { 8 | name: string 9 | } 10 | 11 | export type GetRoom = (options: T) => Promise 12 | 13 | type getRoomFactory = ( 14 | client: HttpClient 15 | ) => { 16 | getRoomById: GetRoom 17 | getRoomByName: GetRoom 18 | } 19 | 20 | export const getRoomFactory: getRoomFactory = (client) => { 21 | const getRoomPath = 'video/rooms' 22 | const getRoom = async ({ id }: { id: string }) => { 23 | try { 24 | const { body } = await client(`${getRoomPath}/${id}`, { 25 | method: 'GET', 26 | }) 27 | 28 | return body 29 | } catch (e) { 30 | if (e.code === 404) { 31 | return null 32 | } 33 | 34 | throw e 35 | } 36 | } 37 | 38 | return { 39 | async getRoomById(options: GetRoomByIdOptions) { 40 | return getRoom({ id: options.id }) 41 | }, 42 | async getRoomByName(options: GetRoomByNameOptions) { 43 | return getRoom({ id: options.name }) 44 | }, 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/web-api/src/rooms/listAllRoomsFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, ListAllRoomsResponse } from '../types' 2 | 3 | interface ListAllRoomsOptions { 4 | startsAfter?: string 5 | startsBefore?: string 6 | pageNumber?: number 7 | pageToken?: string 8 | pageSize?: number 9 | } 10 | 11 | export type ListAllRooms = ( 12 | options?: ListAllRoomsOptions 13 | ) => Promise 14 | 15 | type ListAllRoomsFactory = (client: HttpClient) => ListAllRooms 16 | 17 | export const listAllRoomsFactory: ListAllRoomsFactory = (client) => async ( 18 | options = {} 19 | ) => { 20 | const { 21 | startsAfter: starts_at, 22 | startsBefore: starts_before, 23 | pageNumber: page_number, 24 | pageToken: page_token, 25 | pageSize: page_size, 26 | } = options 27 | const { body } = await client('video/rooms', { 28 | method: 'GET', 29 | searchParams: { 30 | starts_at, 31 | starts_before, 32 | page_number, 33 | page_token, 34 | page_size, 35 | }, 36 | }) 37 | 38 | return body 39 | } 40 | -------------------------------------------------------------------------------- /packages/web-api/src/rooms/updateRoomFactory.ts: -------------------------------------------------------------------------------- 1 | import { HttpClient, RoomResponse } from '../types' 2 | interface UpdateRoomOptions { 3 | name: string 4 | displayName?: string 5 | maxParticipants?: number 6 | deleteOnEnd?: boolean 7 | startsAt?: string 8 | endsAt?: string 9 | } 10 | 11 | export type UpdateRoom = (options: UpdateRoomOptions) => Promise 12 | 13 | type UpdateRoomFactory = (client: HttpClient) => UpdateRoom 14 | 15 | export const updateRoomFactory: UpdateRoomFactory = (client) => async ( 16 | options 17 | ) => { 18 | const { 19 | name, 20 | displayName: display_name, 21 | maxParticipants: max_participants, 22 | deleteOnEnd: delete_on_end, 23 | startsAt: starts_at, 24 | endsAt: ends_at, 25 | } = options 26 | 27 | const { body } = await client('video/rooms', { 28 | method: 'PUT', 29 | body: { 30 | name, 31 | display_name, 32 | max_participants, 33 | delete_on_end, 34 | starts_at, 35 | ends_at, 36 | }, 37 | }) 38 | 39 | return body 40 | } 41 | -------------------------------------------------------------------------------- /packages/web-api/src/types.ts: -------------------------------------------------------------------------------- 1 | import { CreateRoom } from './rooms/createRoomFactory' 2 | import { CreateVRT } from './rooms/createVRTFactory' 3 | import { 4 | GetRoom, 5 | GetRoomByIdOptions, 6 | GetRoomByNameOptions, 7 | } from './rooms/getRoomFactory' 8 | import { ListAllRooms } from './rooms/listAllRoomsFactory' 9 | import { UpdateRoom } from './rooms/updateRoomFactory' 10 | import { ConfigParamaters } from './utils/getConfig' 11 | import { createHttpClient } from './utils/httpClient' 12 | 13 | export type HttpClient = ReturnType 14 | 15 | interface VideoSDKClient { 16 | createRoom: CreateRoom 17 | createVRT: CreateVRT 18 | listAllRooms: ListAllRooms 19 | getRoomByName: GetRoom 20 | getRoomById: GetRoom 21 | updateRoom: UpdateRoom 22 | } 23 | 24 | export interface ListAllRoomsResponse { 25 | links: { 26 | self: string 27 | first: string 28 | // TODO: validate this 29 | next: string | null 30 | } 31 | rooms: RoomResponse[] 32 | } 33 | 34 | export interface RoomResponse { 35 | id: string 36 | name: string 37 | display_name: string 38 | max_participants: number 39 | delete_on_end: boolean 40 | starts_at: string 41 | ends_at: string 42 | created_at: string 43 | updated_at: string 44 | } 45 | 46 | export type Client = (options?: ConfigParamaters) => VideoSDKClient 47 | -------------------------------------------------------------------------------- /packages/web-api/src/utils/getConfig.ts: -------------------------------------------------------------------------------- 1 | export interface ConfigParamaters { 2 | projectId?: string 3 | projectToken?: string 4 | spaceHost?: string 5 | } 6 | 7 | export type ConfigOptions = { 8 | projectId: string 9 | projectToken: string 10 | authCreds: string 11 | baseUrl: string 12 | } 13 | 14 | type GetConfig = (options?: ConfigParamaters) => ConfigOptions 15 | 16 | const getBaseUrl = (spaceHost: string) => { 17 | return `https://${spaceHost}/api/` 18 | } 19 | 20 | export const getConfig: GetConfig = (options = {}) => { 21 | const { 22 | projectId = process.env.PROJECT_ID, 23 | projectToken = process.env.PROJECT_TOKEN, 24 | spaceHost = process.env.SPACE_HOST, 25 | } = options 26 | 27 | if (!projectId || !projectToken || !spaceHost) { 28 | throw new TypeError('Missing required options') 29 | } 30 | 31 | const authCreds = `${projectId}:${projectToken}` 32 | 33 | return { 34 | projectId, 35 | projectToken, 36 | baseUrl: getBaseUrl(spaceHost), 37 | authCreds, 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/web-api/src/validateRequest.ts: -------------------------------------------------------------------------------- 1 | import { createHmac } from 'node:crypto' 2 | import { RestClient } from '@signalwire/compatibility-api' 3 | 4 | /** 5 | * Utility function to validate an incoming request is indeed from SignalWire 6 | * 7 | * @param {string} privateKey - The "SIGNING KEY", as seen in the SignalWire API page 8 | * @param {string} header - The value of the X-SignalWire-Signature header from the request 9 | * @param {string} url - The full URL (with query string) you configured to handle this request 10 | * @param {string} rawBody - The raw body of the request (JSON string) 11 | * @returns {boolean} - Whether the request is valid or not 12 | */ 13 | export const validateRequest = ( 14 | privateKey: string, 15 | header: string, 16 | url: string, 17 | rawBody: string 18 | ): boolean => { 19 | if (typeof rawBody !== 'string') { 20 | throw new TypeError( 21 | `"rawBody" is not a string. You may need to JSON.stringify the request body.` 22 | ) 23 | } 24 | const hmac = createHmac('sha1', privateKey) 25 | hmac.update(`${url}${rawBody}`) 26 | const valid = hmac.digest('hex') === header 27 | 28 | if (valid) { 29 | return true 30 | } 31 | 32 | const parsedBody = JSON.parse(rawBody) 33 | // @ts-expect-error - add suppressWarning: true 34 | return RestClient.validateRequest(privateKey, header, url, parsedBody, true) 35 | } 36 | -------------------------------------------------------------------------------- /packages/web-api/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "emitDeclarationOnly": true, 5 | "outDir": "dist", 6 | }, 7 | "include": ["./src/**/*.ts", "../core/src/**/*.ts"], 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/webrtc/.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | .DS_Store 3 | node_modules 4 | dist 5 | docs 6 | -------------------------------------------------------------------------------- /packages/webrtc/README.md: -------------------------------------------------------------------------------- 1 | # @signalwire/webrtc 2 | 3 | The SignalWire WebRTC package contains the various WebRTC object used by other SignalWire packages. 4 | 5 | ## Related 6 | 7 | - [Get Started](https://developer.signalwire.com/) 8 | - [`@signalwire/js`](https://www.npmjs.com/package/@signalwire/js) 9 | 10 | ## License 11 | 12 | `@signalwire/webrtc` is copyright © 2018-2023 [SignalWire](http://signalwire.com). It is free software, and may be redistributed under the terms specified in the [MIT-LICENSE](https://github.com/signalwire/signalwire-js/blob/master/LICENSE) file. 13 | -------------------------------------------------------------------------------- /packages/webrtc/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | moduleNameMapper: { 3 | // Force module uuid to resolve with the CJS entry point, because Jest does not support package.json.exports. See https://github.com/uuidjs/uuid/issues/451 4 | uuid: require.resolve('uuid'), 5 | }, 6 | transform: { 7 | '\\.[jt]sx?$': ['babel-jest', { configFile: './../../babel.config.js' }], 8 | }, 9 | testMatch: ['/src/**/*.test.ts'], 10 | setupFiles: ['./src/setupTests.ts'], 11 | } 12 | -------------------------------------------------------------------------------- /packages/webrtc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@signalwire/webrtc", 3 | "description": "SignalWire WebRTC library", 4 | "author": "SignalWire Team ", 5 | "license": "MIT", 6 | "version": "3.13.1", 7 | "main": "dist/cjs/webrtc/src/index.js", 8 | "module": "dist/mjs/webrtc/src/index.js", 9 | "files": [ 10 | "dist", 11 | "src" 12 | ], 13 | "engines": { 14 | "node": ">=14" 15 | }, 16 | "keywords": [ 17 | "signalwire", 18 | "audio", 19 | "video", 20 | "rtc", 21 | "real time communication", 22 | "webrtc", 23 | "relay" 24 | ], 25 | "repository": { 26 | "type": "git", 27 | "url": "https://github.com/signalwire/signalwire-js" 28 | }, 29 | "bugs": { 30 | "url": "https://github.com/signalwire/signalwire-js/issues" 31 | }, 32 | "homepage": "https://github.com/signalwire/signalwire-js/tree/master/packages/webrtc", 33 | "scripts": { 34 | "start": "tsc --watch --project tsconfig.build.json", 35 | "build": "tsc --project tsconfig.build.json && tsc --project tsconfig.cjs.json", 36 | "test": "NODE_ENV=test jest", 37 | "prepublishOnly": "npm run build" 38 | }, 39 | "dependencies": { 40 | "@signalwire/core": "4.2.1", 41 | "sdp": "^3.2.0" 42 | }, 43 | "types": "dist/cjs/webrtc/src/index.d.ts" 44 | } 45 | -------------------------------------------------------------------------------- /packages/webrtc/src/index.test.ts: -------------------------------------------------------------------------------- 1 | describe('WebRTC Entry Point', () => { 2 | it('should work', () => { 3 | expect(1).toEqual(1) 4 | }) 5 | }) 6 | -------------------------------------------------------------------------------- /packages/webrtc/src/index.ts: -------------------------------------------------------------------------------- 1 | export { 2 | getDevices, 3 | getCameraDevices, 4 | getMicrophoneDevices, 5 | getSpeakerDevices, 6 | getSpeakerById, 7 | getDevicesWithPermissions, 8 | getCameraDevicesWithPermissions, 9 | getMicrophoneDevicesWithPermissions, 10 | getSpeakerDevicesWithPermissions, 11 | assureDeviceId, 12 | assureVideoDevice, 13 | assureAudioInDevice, 14 | assureAudioOutDevice, 15 | createDeviceWatcher, 16 | createMicrophoneDeviceWatcher, 17 | createSpeakerDeviceWatcher, 18 | createCameraDeviceWatcher, 19 | createMicrophoneAnalyzer, 20 | } from './utils/deviceHelpers' 21 | export type { MicrophoneAnalyzer } from './utils/deviceHelpers' 22 | export { 23 | supportsMediaDevices, 24 | supportsGetUserMedia, 25 | supportsGetDisplayMedia, 26 | getUserMedia, 27 | getDisplayMedia, 28 | enumerateDevices, 29 | enumerateDevicesByKind, 30 | getSupportedConstraints, 31 | streamIsValid, 32 | supportsMediaOutput, 33 | setMediaElementSinkId, 34 | stopStream, 35 | stopTrack, 36 | checkPermissions, 37 | checkCameraPermissions, 38 | checkMicrophonePermissions, 39 | checkSpeakerPermissions, 40 | requestPermissions, 41 | } from './utils' 42 | export * from './utils/interfaces' 43 | export { BaseConnection, BaseConnectionOptions } from './BaseConnection' 44 | -------------------------------------------------------------------------------- /packages/webrtc/src/utils/constants.ts: -------------------------------------------------------------------------------- 1 | import { ConnectionOptions } from './interfaces' 2 | 3 | export const INVITE_VERSION = 1000 4 | 5 | export const AUDIO_CONSTRAINTS: MediaTrackConstraints = { 6 | echoCancellation: true, 7 | noiseSuppression: true, 8 | autoGainControl: true, 9 | } 10 | 11 | export const AUDIO_CONSTRAINTS_SCREENSHARE: MediaTrackConstraints = { 12 | ...AUDIO_CONSTRAINTS, 13 | noiseSuppression: false, 14 | autoGainControl: false, 15 | // @ts-expect-error 16 | googAutoGainControl: false, 17 | } 18 | 19 | export const VIDEO_CONSTRAINTS: MediaTrackConstraints = { 20 | width: { ideal: 1280, min: 320 }, 21 | height: { ideal: 720, min: 180 }, 22 | aspectRatio: { ideal: 16 / 9 }, 23 | } 24 | 25 | export const DEFAULT_CALL_OPTIONS: ConnectionOptions = { 26 | destinationNumber: 'room', 27 | remoteCallerName: 'Outbound Call', 28 | remoteCallerNumber: '', 29 | callerName: '', 30 | callerNumber: '', 31 | audio: AUDIO_CONSTRAINTS, 32 | video: VIDEO_CONSTRAINTS, 33 | useStereo: false, 34 | attach: false, 35 | screenShare: false, 36 | additionalDevice: false, 37 | userVariables: {}, 38 | requestTimeout: 10 * 1000, 39 | autoApplyMediaParams: true, 40 | iceGatheringTimeout: 2 * 1000, 41 | maxIceGatheringTimeout: 5 * 1000, 42 | maxConnectionStateTimeout: 3 * 1000, 43 | watchMediaPackets: true, 44 | watchMediaPacketsTimeout: 2 * 1000, 45 | } 46 | -------------------------------------------------------------------------------- /packages/webrtc/src/utils/enumerateDevices.ts: -------------------------------------------------------------------------------- 1 | import { getMediaDevicesApi } from './primitives' 2 | 3 | /** 4 | * Enumerates the media input and output devices available on this device. 5 | * 6 | * > 📘 7 | * > Depending on the browser, some information (such as the `label` and 8 | * > `deviceId` attributes) could be hidden until permission is granted, for 9 | * > example by calling {@link getUserMedia}. 10 | * 11 | * @example 12 | * ```typescript 13 | * await SignalWire.WebRTC.enumerateDevices() 14 | * // [ 15 | * // { 16 | * // "deviceId": "Rug5Bk...4TMhY=", 17 | * // "kind": "videoinput", 18 | * // "label": "HD FaceTime Camera", 19 | * // "groupId": "EEX/N2...AjrOs=" 20 | * // }, 21 | * // ... 22 | * // ] 23 | * ``` 24 | */ 25 | export const enumerateDevices = () => { 26 | return getMediaDevicesApi().enumerateDevices() 27 | } 28 | 29 | export const enumerateDevicesByKind = async ( 30 | filterByKind?: MediaDeviceKind 31 | ) => { 32 | let devices: MediaDeviceInfo[] = await enumerateDevices().catch( 33 | (_error) => [] 34 | ) 35 | if (filterByKind) { 36 | devices = devices.filter(({ kind }) => kind === filterByKind) 37 | } 38 | return devices 39 | } 40 | -------------------------------------------------------------------------------- /packages/webrtc/src/utils/getDisplayMedia.ts: -------------------------------------------------------------------------------- 1 | import { getMediaDevicesApi } from './primitives' 2 | 3 | /** 4 | * Prompts the user to share the screen and asynchronously returns a 5 | * [MediaStream](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream) 6 | * object associated with a display or part of it. 7 | * 8 | * @param constraints an optional 9 | * [MediaStreamConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints) 10 | * object specifying requirements for the returned [MediaStream](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream). 11 | * 12 | * @example 13 | * ```typescript 14 | * await SignalWire.WebRTC.getDisplayMedia() 15 | * // MediaStream {id: "HCXy...", active: true, ...} 16 | * ``` 17 | */ 18 | export const getDisplayMedia = (constraints?: MediaStreamConstraints) => { 19 | return getMediaDevicesApi().getDisplayMedia(constraints) 20 | } 21 | -------------------------------------------------------------------------------- /packages/webrtc/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './primitives' 2 | export * from './enumerateDevices' 3 | export * from './getUserMedia' 4 | export * from './getDisplayMedia' 5 | export * from './permissions' 6 | export * from './requestPermissions' 7 | -------------------------------------------------------------------------------- /packages/webrtc/src/utils/requestPermissions.ts: -------------------------------------------------------------------------------- 1 | import { _getMediaDeviceKindByName, stopStream } from './primitives' 2 | import { getUserMedia } from './getUserMedia' 3 | 4 | /** 5 | * Prompts the user to grant permissions for the devices matching the specified set of constraints. 6 | * @param constraints an optional [MediaStreamConstraints](https://developer.mozilla.org/en-US/docs/Web/API/MediaStreamConstraints) 7 | * object specifying requirements for the returned [MediaStream](https://developer.mozilla.org/en-US/docs/Web/API/MediaStream). 8 | * 9 | * @example 10 | * To only request audio permissions: 11 | * 12 | * ```typescript 13 | * await SignalWire.WebRTC.requestPermissions({audio: true, video: false}) 14 | * ``` 15 | * 16 | * @example 17 | * To request permissions for both audio and video, specifying constraints for the video: 18 | * ```typescript 19 | * const constraints = { 20 | * audio: true, 21 | * video: { 22 | * width: { min: 1024, ideal: 1280, max: 1920 }, 23 | * height: { min: 576, ideal: 720, max: 1080 } 24 | * } 25 | * } 26 | * await SignalWire.WebRTC.requestPermissions(constraints) 27 | * ``` 28 | */ 29 | export const requestPermissions = async ( 30 | constraints: MediaStreamConstraints 31 | ) => { 32 | try { 33 | const stream = await getUserMedia(constraints) 34 | stopStream(stream) 35 | } catch (error) { 36 | throw error 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/webrtc/src/workers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './vertoEventWorker' 2 | export * from './roomSubscribedWorker' 3 | export * from './promoteDemoteWorker' 4 | export * from './sessionAuthWorker' 5 | -------------------------------------------------------------------------------- /packages/webrtc/src/workers/sessionAuthWorker.ts: -------------------------------------------------------------------------------- 1 | import { 2 | getLogger, 3 | sagaEffects, 4 | actions, 5 | SagaIterator, 6 | SDKWorker, 7 | SDKWorkerHooks, 8 | } from '@signalwire/core' 9 | 10 | import { BaseConnection } from '../BaseConnection' 11 | 12 | type SessionAuthWorkerOnDone = (args: BaseConnection) => void 13 | type SessionAuthWorkerOnFail = (args: { error: Error }) => void 14 | 15 | export type SessionAuthWorkerHooks = SDKWorkerHooks< 16 | SessionAuthWorkerOnDone, 17 | SessionAuthWorkerOnFail 18 | > 19 | 20 | type Action = typeof actions.authSuccessAction | typeof actions.authErrorAction 21 | 22 | export const sessionAuthWorker: SDKWorker< 23 | BaseConnection, 24 | SessionAuthWorkerHooks 25 | > = function* (options): SagaIterator { 26 | getLogger().debug('sessionAuthWorker started') 27 | const { instance } = options 28 | const action: Action = yield sagaEffects.take([ 29 | actions.authSuccessAction.type, 30 | actions.authErrorAction.type, 31 | ]) 32 | 33 | switch (action.type) { 34 | case actions.authSuccessAction.type: 35 | yield sagaEffects.call([instance, instance.resume]) 36 | break 37 | case actions.authErrorAction.type: 38 | yield sagaEffects.call([instance, instance.setState], 'hangup') 39 | break 40 | } 41 | 42 | getLogger().debug('sessionAuthWorker ended') 43 | } 44 | -------------------------------------------------------------------------------- /packages/webrtc/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "target": "es2017", 5 | "outDir": "dist/mjs" 6 | }, 7 | "include": ["./src/**/*.ts", "../core/src/**/*.ts", "../webrtc/src/**/*.ts"], 8 | "exclude": ["**/*.test.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/webrtc/tsconfig.cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "module": "CommonJS", 5 | "target": "es2015", 6 | "outDir": "dist/cjs" 7 | }, 8 | "include": ["./src/**/*.ts", "../core/src/**/*.ts", "../webrtc/src/**/*.ts"], 9 | "exclude": ["**/*.test.ts"] 10 | } 11 | -------------------------------------------------------------------------------- /scripts/sw-build-all/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @sw-internal/build-all 2 | 3 | ## 0.0.3 4 | 5 | ### Patch Changes 6 | 7 | - [#1001](https://github.com/signalwire/signalwire-js/pull/1001) [`968d226b`](https://github.com/signalwire/signalwire-js/commit/968d226ba2791f44dea4bd1b0d173aefaf103bda) Thanks [@ayeminag](https://github.com/ayeminag)! - - API to fetch address by id and tests 8 | 9 | ## 0.0.2 10 | 11 | ### Patch Changes 12 | 13 | - [#434](https://github.com/signalwire/signalwire-js/pull/434) [`0677540c`](https://github.com/signalwire/signalwire-js/commit/0677540c65211570b0762ff56ed4b85b8d66e1d0) - Bubble up build errors. 14 | 15 | ## 0.0.1 16 | 17 | ### Patch Changes 18 | 19 | - [#388](https://github.com/signalwire/signalwire-js/pull/388) [`62c25d8`](https://github.com/signalwire/signalwire-js/commit/62c25d8468c37711f37c6674c24251755a4ada39) - Upgrade dependencies, add dependency visualizer to UMD build. 20 | -------------------------------------------------------------------------------- /scripts/sw-build-all/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { cli } from '../index.js' 4 | 5 | cli(process.argv) 6 | -------------------------------------------------------------------------------- /scripts/sw-build-all/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/build-all", 3 | "version": "0.0.3", 4 | "description": "A CLI for creating the build tree and concurrently building each of them", 5 | "main": "index.js", 6 | "type": "module", 7 | "private": true, 8 | "scripts": { 9 | "test": "" 10 | }, 11 | "bin": { 12 | "sw-build-all": "bin/cli.js" 13 | }, 14 | "dependencies": { 15 | "concurrently": "^8.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scripts/sw-build/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @sw-internal/build 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - [#1001](https://github.com/signalwire/signalwire-js/pull/1001) [`968d226b`](https://github.com/signalwire/signalwire-js/commit/968d226ba2791f44dea4bd1b0d173aefaf103bda) Thanks [@ayeminag](https://github.com/ayeminag)! - - API to fetch address by id and tests 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - [#605](https://github.com/signalwire/signalwire-js/pull/605) [`2f909c9e`](https://github.com/signalwire/signalwire-js/commit/2f909c9ef670eeaed7b3444b9d4bf703bfbc3a1b) - Change how the SDK agent is defined. 14 | 15 | ## 0.0.2 16 | 17 | ### Patch Changes 18 | 19 | - [#464](https://github.com/signalwire/signalwire-js/pull/464) [`2c8fc597`](https://github.com/signalwire/signalwire-js/commit/2c8fc59719e7f40c1d9b01ebf67190d358dcea46) - Upgrade dependencies. 20 | 21 | ## 0.0.1 22 | 23 | ### Patch Changes 24 | 25 | - [#388](https://github.com/signalwire/signalwire-js/pull/388) [`62c25d8`](https://github.com/signalwire/signalwire-js/commit/62c25d8468c37711f37c6674c24251755a4ada39) - Upgrade dependencies, add dependency visualizer to UMD build. 26 | -------------------------------------------------------------------------------- /scripts/sw-build/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { cli } from '../index.js' 4 | 5 | cli(process.argv) 6 | -------------------------------------------------------------------------------- /scripts/sw-build/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/build", 3 | "version": "0.0.4", 4 | "description": "A CLI for building packages using esbuild", 5 | "main": "index.js", 6 | "type": "module", 7 | "private": true, 8 | "scripts": { 9 | "test": "" 10 | }, 11 | "bin": { 12 | "sw-build": "bin/cli.js" 13 | }, 14 | "dependencies": { 15 | "@rollup/plugin-commonjs": "^22.0.0", 16 | "@rollup/plugin-node-resolve": "^13.3.0", 17 | "@rollup/plugin-replace": "^4.0.0", 18 | "@rollup/plugin-terser": "^0.4.4", 19 | "esbuild-node-externals": "^1.18.0", 20 | "rollup": "^2.74.1", 21 | "rollup-plugin-license": "^2.7.0", 22 | "rollup-plugin-typescript2": "^0.31.2", 23 | "rollup-plugin-visualizer": "^5.6.0" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /scripts/sw-common/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # @sw-internal/common 2 | 3 | ## 0.0.4 4 | 5 | ### Patch Changes 6 | 7 | - [#1001](https://github.com/signalwire/signalwire-js/pull/1001) [`968d226b`](https://github.com/signalwire/signalwire-js/commit/968d226ba2791f44dea4bd1b0d173aefaf103bda) Thanks [@ayeminag](https://github.com/ayeminag)! - - API to fetch address by id and tests 8 | 9 | ## 0.0.3 10 | 11 | ### Patch Changes 12 | 13 | - [#573](https://github.com/signalwire/signalwire-js/pull/573) [`eaed3706`](https://github.com/signalwire/signalwire-js/commit/eaed3706aff7ba009885b13f845096a3b21eca03) - [internal] Add infra for testing usages of the SDK with node 14 | 15 | ## 0.0.2 16 | 17 | ### Patch Changes 18 | 19 | - [#464](https://github.com/signalwire/signalwire-js/pull/464) [`2c8fc597`](https://github.com/signalwire/signalwire-js/commit/2c8fc59719e7f40c1d9b01ebf67190d358dcea46) - Upgrade dependencies. 20 | 21 | ## 0.0.1 22 | 23 | ### Patch Changes 24 | 25 | - [#388](https://github.com/signalwire/signalwire-js/pull/388) [`62c25d8`](https://github.com/signalwire/signalwire-js/commit/62c25d8468c37711f37c6674c24251755a4ada39) - Upgrade dependencies, add dependency visualizer to UMD build. 26 | -------------------------------------------------------------------------------- /scripts/sw-common/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/common", 3 | "version": "0.0.4", 4 | "description": "CLI utilities", 5 | "main": "index.js", 6 | "type": "module", 7 | "private": true, 8 | "dependencies": { 9 | "execa": "^7.1.1" 10 | }, 11 | "scripts": { 12 | "test": "" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /scripts/sw-release/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import { cli } from '../index.js' 4 | 5 | cli(process.argv) 6 | -------------------------------------------------------------------------------- /scripts/sw-release/modes/production.js: -------------------------------------------------------------------------------- 1 | import { isCleanGitStatus } from '@sw-internal/common' 2 | import { getBuildTask, getTestTask, publishTaskFactory } from '../common.js' 3 | 4 | const getProductionTasks = ({ flags, executer, dryRun }) => { 5 | return [ 6 | { 7 | title: '🔍 Checking Git status', 8 | task: async (_ctx, task) => { 9 | try { 10 | await isCleanGitStatus({ executer }) 11 | task.title = '🔍 Git: clean working tree!' 12 | } catch (e) { 13 | task.title = `🛑 ${e.message}` 14 | 15 | return Promise.reject('') 16 | } 17 | }, 18 | }, 19 | ...getBuildTask({ dryRun, executer }), 20 | ...getTestTask({ dryRun, executer }), 21 | ...publishTaskFactory({ 22 | npmOptions: [], 23 | publishGitTag: true, 24 | executer, 25 | dryRun, 26 | }), 27 | ] 28 | } 29 | 30 | export { getProductionTasks } 31 | -------------------------------------------------------------------------------- /scripts/sw-release/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/release", 3 | "version": "0.0.6", 4 | "description": "A CLI for publishing releases on npm.", 5 | "main": "index.js", 6 | "type": "module", 7 | "private": true, 8 | "scripts": { 9 | "test": "" 10 | }, 11 | "bin": { 12 | "sw-release": "bin/cli.js" 13 | }, 14 | "dependencies": { 15 | "@sw-internal/common": "^0.0.4", 16 | "execa": "^7.1.1", 17 | "listr2": "^4.0.5" 18 | }, 19 | "devDependencies": { 20 | "inquirer": "^8.2.5" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /scripts/sw-test/bin/cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const { cli } = require('../index.js') 4 | 5 | cli(process.argv) 6 | -------------------------------------------------------------------------------- /scripts/sw-test/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@sw-internal/test", 3 | "version": "0.0.5", 4 | "description": "A CLI for running tests using Jest", 5 | "main": "index.js", 6 | "private": true, 7 | "scripts": { 8 | "test": "" 9 | }, 10 | "bin": { 11 | "sw-test": "bin/cli.js" 12 | }, 13 | "devDependencies": { 14 | "esbuild-register": "^3.6.0" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /sw.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | agents: { 3 | // Maps packages to agent-name. Take into account that 4 | // these names are just a part within the fully 5 | // qualified agent sent to the server: 6 | // @signalwire/// 7 | byName: { 8 | '@signalwire/realtime-api': 'nodejs/realtime-api', 9 | '@signalwire/js': 'js/browser', 10 | '@signalwire/web-api': 'nodejs/web-api', 11 | '@signalwire/node': 'nodejs/node', 12 | '@signalwire/swaig': 'nodejs/swaig', 13 | }, 14 | }, 15 | utilityPackages: ['@signalwire/core', '@signalwire/webrtc'], 16 | } 17 | -------------------------------------------------------------------------------- /tsconfig.docs.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "esnext"], 4 | "module": "commonjs", 5 | "target": "es2020", 6 | 7 | "strict": true, 8 | "strictNullChecks": true, 9 | "esModuleInterop": true, 10 | "skipLibCheck": true, 11 | "strictPropertyInitialization": false, 12 | "useUnknownInCatchVariables": false, 13 | 14 | "baseUrl": ".", 15 | "paths": { 16 | "@signalwire/js": ["packages/js/src"], 17 | "@signalwire/core": ["packages/core/src"], 18 | "@signalwire/webrtc": ["packages/webrtc/src"], 19 | "@signalwire/node": ["packages/node/src"] 20 | }, 21 | }, 22 | "exclude": ["**/*.test.ts", "**/*.native.ts"] 23 | } 24 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "esnext", 4 | "lib": ["dom", "esnext"], 5 | "importHelpers": true, 6 | "declaration": true, 7 | "sourceMap": true, 8 | "strict": true, 9 | "strictNullChecks": true, 10 | "target": "ESNext", 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "noImplicitReturns": true, 14 | "noFallthroughCasesInSwitch": true, 15 | "moduleResolution": "node", 16 | "jsx": "react", 17 | "esModuleInterop": true, 18 | "allowJs": false, 19 | "baseUrl": ".", 20 | "rootDir": "packages", 21 | "typeRoots": ["./node_modules/@types", "./types"], 22 | "paths": { 23 | "@signalwire/js": ["packages/js/src"], 24 | "@signalwire/core": ["packages/core/src"], 25 | "@signalwire/webrtc": ["packages/webrtc/src"], 26 | "@signalwire/node": ["packages/node/src"] 27 | }, 28 | "composite": true, 29 | "declarationMap": true, 30 | "strictPropertyInitialization": false, 31 | "useUnknownInCatchVariables": false, 32 | "skipLibCheck": true, 33 | "forceConsistentCasingInFileNames": true 34 | }, 35 | "include": ["packages"] 36 | } 37 | --------------------------------------------------------------------------------