jiti-meet/react/features/mobile/external-api/middleware.js

301 lines
10 KiB
JavaScript
Raw Normal View History

// @flow
import { NativeModules } from 'react-native';
import { getAppProp } from '../../base/app';
import {
CONFERENCE_FAILED,
CONFERENCE_JOINED,
CONFERENCE_LEFT,
CONFERENCE_WILL_JOIN,
CONFERENCE_WILL_LEAVE,
JITSI_CONFERENCE_URL_KEY,
SET_ROOM,
fix(base/participants): ensure default local id outside of conference Makes sure that whenever a conference is left or switched, the local participant's id will be equal to the default value. The problem fixed by this commit is a situation where the local participant may end up sharing the same ID with it's "ghost" when rejoining a disconnected conference. The most important and easiest to hit case is when the conference is left after the CONFERENCE_FAILED event. Another rare and harder to encounter in the real world issue is where CONFERENCE_LEFT may come with the delay due to it's asynchronous nature. The step by step scenario is as follows: trying to leave a conference, but the network is not doing well, so it takes time, requests are timing out. After getting back to the welcome page the the CONFERENCE_LEFT has not arrived yet. The same conference is joined again and the load config may timeout, but it will be read from the cache. Now the network gets better and conference is joining which results in our ghost participant added to the redux state. At this point there's the root issue: two participants with the same id, because the local one was neither cleared nor set to the new one yet (PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the id). Then comes CONFERENCE_JOINED and we try to update our local id. We're updating the ID of both ghost and local participant. It could be also that the delayed CONFERENCE_LEFT comes for the old conference, but it's too late and it would update the id for both participants. The approach here reasons that the ID of the local participant may be reset as soon as the local participant and, respectively, her ID is no longer involved in a recoverable JitsiConference of interest to the user and, consequently, the app. Co-authored-by: Pawel Domas <pawel.domas@jitsi.org> Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
forEachConference,
isRoomValid
} from '../../base/conference';
import { LOAD_CONFIG_ERROR } from '../../base/config';
import { CONNECTION_FAILED } from '../../base/connection';
import { MiddlewareRegistry } from '../../base/redux';
import { toURLString } from '../../base/util';
import { ENTER_PICTURE_IN_PICTURE } from '../picture-in-picture';
/**
* Middleware that captures Redux actions and uses the ExternalAPI module to
* turn them into native events so the application knows about them.
*
* @param {Store} store - Redux store.
* @returns {Function}
*/
MiddlewareRegistry.register(store => next => action => {
const result = next(action);
const { type } = action;
switch (type) {
case CONFERENCE_FAILED: {
const { error, ...data } = action;
// XXX Certain CONFERENCE_FAILED errors are recoverable i.e. they have
// prevented the user from joining a specific conference but the app may
// be able to eventually join the conference. For example, the app will
// ask the user for a password upon
// JitsiConferenceErrors.PASSWORD_REQUIRED and will retry joining the
// conference afterwards. Such errors are to not reach the native
// counterpart of the External API (or at least not in the
// fatality/finality semantics attributed to
// conferenceFailed:/onConferenceFailed).
if (!error.recoverable) {
_sendConferenceEvent(store, /* action */ {
error: _toErrorString(error),
...data
});
}
break;
}
case CONFERENCE_JOINED:
case CONFERENCE_LEFT:
case CONFERENCE_WILL_JOIN:
case CONFERENCE_WILL_LEAVE:
_sendConferenceEvent(store, action);
break;
case CONNECTION_FAILED:
!action.error.recoverable
&& _sendConferenceFailedOnConnectionError(store, action);
break;
case ENTER_PICTURE_IN_PICTURE:
_sendEvent(store, _getSymbolDescription(type), /* data */ {});
break;
case LOAD_CONFIG_ERROR: {
const { error, locationURL } = action;
_sendEvent(
store,
_getSymbolDescription(type),
/* data */ {
error: _toErrorString(error),
url: toURLString(locationURL)
});
break;
}
case SET_ROOM:
_maybeTriggerEarlyConferenceWillJoin(store, action);
break;
}
return result;
});
/**
* Returns a {@code String} representation of a specific error {@code Object}.
*
* @param {Error|Object|string} error - The error {@code Object} to return a
* {@code String} representation of.
* @returns {string} A {@code String} representation of the specified
* {@code error}.
*/
function _toErrorString(
error: Error | { message: ?string, name: ?string } | string) {
// XXX In lib-jitsi-meet and jitsi-meet we utilize errors in the form of
// strings, Error instances, and plain objects which resemble Error.
return (
error
? typeof error === 'string'
? error
: Error.prototype.toString.apply(error)
: '');
}
2017-09-06 03:40:12 +00:00
/**
* Gets the description of a specific {@code Symbol}.
2017-09-06 03:40:12 +00:00
*
* @param {Symbol} symbol - The {@code Symbol} to retrieve the description of.
2017-09-06 03:40:12 +00:00
* @private
* @returns {string} The description of {@code symbol}.
2017-09-06 03:40:12 +00:00
*/
function _getSymbolDescription(symbol: Symbol) {
let description = symbol.toString();
if (description.startsWith('Symbol(') && description.endsWith(')')) {
description = description.slice(7, -1);
}
// The polyfill es6-symbol that we use does not appear to comply with the
// Symbol standard and, merely, adds @@ at the beginning of the description.
if (description.startsWith('@@')) {
description = description.slice(2);
}
return description;
}
/**
* If {@link SET_ROOM} action happens for a valid conference room this method
* will emit an early {@link CONFERENCE_WILL_JOIN} event to let the external API
* know that a conference is being joined. Before that happens a connection must
* be created and only then base/conference feature would emit
* {@link CONFERENCE_WILL_JOIN}. That is fine for the Jitsi Meet app, because
* that's the a conference instance gets created, but it's too late for
* the external API to learn that. The latter {@link CONFERENCE_WILL_JOIN} is
* swallowed in {@link _swallowEvent}.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _maybeTriggerEarlyConferenceWillJoin(store, action) {
const { locationURL } = store.getState()['features/base/connection'];
const { room } = action;
isRoomValid(room) && locationURL && _sendEvent(
store,
_getSymbolDescription(CONFERENCE_WILL_JOIN),
/* data */ {
url: toURLString(locationURL)
});
}
/**
* Sends an event to the native counterpart of the External API for a specific
* conference-related redux action.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _sendConferenceEvent(
store: Object,
action: {
conference: Object,
type: Symbol,
url: ?string
}) {
const { conference, type, ...data } = action;
// For these (redux) actions, conference identifies a JitsiConference
// instance. The external API cannot transport such an object so we have to
// transport an "equivalent".
if (conference) {
data.url = toURLString(conference[JITSI_CONFERENCE_URL_KEY]);
}
_swallowEvent(store, action, data)
|| _sendEvent(store, _getSymbolDescription(type), data);
2017-09-06 03:40:12 +00:00
}
/**
* Sends {@link CONFERENCE_FAILED} event when the {@link CONNECTION_FAILED}
* occurs. It should be done only if the connection fails before the conference
* instance is created. Otherwise the eventual failure event is supposed to be
* emitted by the base/conference feature.
*
* @param {Store} store - The redux store.
* @param {Action} action - The redux action.
* @returns {void}
*/
function _sendConferenceFailedOnConnectionError(store, action) {
const { locationURL } = store.getState()['features/base/connection'];
const { connection } = action;
locationURL
&& forEachConference(
store,
// If there's any conference in the base/conference state then the
// base/conference feature is supposed to emit a failure.
conference => conference.getConnection() !== connection)
&& _sendEvent(
store,
_getSymbolDescription(CONFERENCE_FAILED),
/* data */ {
url: toURLString(locationURL),
error: action.error.name
});
}
/**
* Sends a specific event to the native counterpart of the External API. Native
* apps may listen to such events via the mechanisms provided by the (native)
* mobile Jitsi Meet SDK.
*
2017-10-04 22:36:09 +00:00
* @param {Object} store - The redux store.
* @param {string} name - The name of the event to send.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code name}.
* @private
* @returns {void}
*/
function _sendEvent(store: Object, name: string, data: Object) {
// The JavaScript App needs to provide uniquely identifying information to
// the native ExternalAPI module so that the latter may match the former to
// the native JitsiMeetView which hosts it.
const externalAPIScope = getAppProp(store, 'externalAPIScope');
2017-06-10 00:17:01 +00:00
externalAPIScope
&& NativeModules.ExternalAPI.sendEvent(name, data, externalAPIScope);
}
/**
* Determines whether to not send a {@code CONFERENCE_LEFT} event to the native
* counterpart of the External API.
*
* @param {Object} store - The redux store.
* @param {Action} action - The redux action which is causing the sending of the
* event.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code action}.
* @returns {boolean} If the specified event is to not be sent, {@code true};
* otherwise, {@code false}.
*/
function _swallowConferenceLeft({ getState }, action, { url }) {
// XXX Internally, we work with JitsiConference instances. Externally
// though, we deal with URL strings. The relation between the two is many to
// one so it's technically and practically possible (by externally loading
// the same URL string multiple times) to try to send CONFERENCE_LEFT
// externally for a URL string which identifies a JitsiConference that the
// app is internally legitimately working with.
fix(base/participants): ensure default local id outside of conference Makes sure that whenever a conference is left or switched, the local participant's id will be equal to the default value. The problem fixed by this commit is a situation where the local participant may end up sharing the same ID with it's "ghost" when rejoining a disconnected conference. The most important and easiest to hit case is when the conference is left after the CONFERENCE_FAILED event. Another rare and harder to encounter in the real world issue is where CONFERENCE_LEFT may come with the delay due to it's asynchronous nature. The step by step scenario is as follows: trying to leave a conference, but the network is not doing well, so it takes time, requests are timing out. After getting back to the welcome page the the CONFERENCE_LEFT has not arrived yet. The same conference is joined again and the load config may timeout, but it will be read from the cache. Now the network gets better and conference is joining which results in our ghost participant added to the redux state. At this point there's the root issue: two participants with the same id, because the local one was neither cleared nor set to the new one yet (PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the id). Then comes CONFERENCE_JOINED and we try to update our local id. We're updating the ID of both ghost and local participant. It could be also that the delayed CONFERENCE_LEFT comes for the old conference, but it's too late and it would update the id for both participants. The approach here reasons that the ID of the local participant may be reset as soon as the local participant and, respectively, her ID is no longer involved in a recoverable JitsiConference of interest to the user and, consequently, the app. Co-authored-by: Pawel Domas <pawel.domas@jitsi.org> Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
let swallowConferenceLeft = false;
fix(base/participants): ensure default local id outside of conference Makes sure that whenever a conference is left or switched, the local participant's id will be equal to the default value. The problem fixed by this commit is a situation where the local participant may end up sharing the same ID with it's "ghost" when rejoining a disconnected conference. The most important and easiest to hit case is when the conference is left after the CONFERENCE_FAILED event. Another rare and harder to encounter in the real world issue is where CONFERENCE_LEFT may come with the delay due to it's asynchronous nature. The step by step scenario is as follows: trying to leave a conference, but the network is not doing well, so it takes time, requests are timing out. After getting back to the welcome page the the CONFERENCE_LEFT has not arrived yet. The same conference is joined again and the load config may timeout, but it will be read from the cache. Now the network gets better and conference is joining which results in our ghost participant added to the redux state. At this point there's the root issue: two participants with the same id, because the local one was neither cleared nor set to the new one yet (PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the id). Then comes CONFERENCE_JOINED and we try to update our local id. We're updating the ID of both ghost and local participant. It could be also that the delayed CONFERENCE_LEFT comes for the old conference, but it's too late and it would update the id for both participants. The approach here reasons that the ID of the local participant may be reset as soon as the local participant and, respectively, her ID is no longer involved in a recoverable JitsiConference of interest to the user and, consequently, the app. Co-authored-by: Pawel Domas <pawel.domas@jitsi.org> Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
url
&& forEachConference(getState, (conference, conferenceURL) => {
if (conferenceURL && conferenceURL.toString() === url) {
swallowConferenceLeft = true;
}
fix(base/participants): ensure default local id outside of conference Makes sure that whenever a conference is left or switched, the local participant's id will be equal to the default value. The problem fixed by this commit is a situation where the local participant may end up sharing the same ID with it's "ghost" when rejoining a disconnected conference. The most important and easiest to hit case is when the conference is left after the CONFERENCE_FAILED event. Another rare and harder to encounter in the real world issue is where CONFERENCE_LEFT may come with the delay due to it's asynchronous nature. The step by step scenario is as follows: trying to leave a conference, but the network is not doing well, so it takes time, requests are timing out. After getting back to the welcome page the the CONFERENCE_LEFT has not arrived yet. The same conference is joined again and the load config may timeout, but it will be read from the cache. Now the network gets better and conference is joining which results in our ghost participant added to the redux state. At this point there's the root issue: two participants with the same id, because the local one was neither cleared nor set to the new one yet (PARTICIPANT_JOINED come, before CONFERENCE_JOINED where we adjust the id). Then comes CONFERENCE_JOINED and we try to update our local id. We're updating the ID of both ghost and local participant. It could be also that the delayed CONFERENCE_LEFT comes for the old conference, but it's too late and it would update the id for both participants. The approach here reasons that the ID of the local participant may be reset as soon as the local participant and, respectively, her ID is no longer involved in a recoverable JitsiConference of interest to the user and, consequently, the app. Co-authored-by: Pawel Domas <pawel.domas@jitsi.org> Co-authored-by: Lyubo Marinov <lmarinov@atlassian.com>
2018-05-16 20:08:34 +00:00
return !swallowConferenceLeft;
});
return swallowConferenceLeft;
}
/**
* Determines whether to not send a specific event to the native counterpart of
* the External API.
*
* @param {Object} store - The redux store.
* @param {Action} action - The redux action which is causing the sending of the
* event.
* @param {Object} data - The details/specifics of the event to send determined
* by/associated with the specified {@code action}.
* @returns {boolean} If the specified event is to not be sent, {@code true};
* otherwise, {@code false}.
*/
function _swallowEvent(store, action, data) {
switch (action.type) {
case CONFERENCE_LEFT:
return _swallowConferenceLeft(store, action, data);
case CONFERENCE_WILL_JOIN:
// CONFERENCE_WILL_JOIN is dispatched to the external API on SET_ROOM,
// before the connection is created, so we need to swallow the original
// one emitted by base/conference.
return true;
default:
return false;
}
}