当应用程序在后台时,如何保持 redux saga 响应?
How can I keep redux saga responsive when app is in the background?
我正在使用 React-Native,我有一个管理 websocket 状态的 saga。它实现重试并接收诸如位置更改之类的调度,以通过 websocket 将其发送到服务器。我正在使用 @mauron85/react-native-background-geolocation
在我的应用程序的根组件中获取位置事件,并将它们分派给 redux。 route saga 有一个 spawned generator,它会监听这个并将它发送出去。当应用程序处于前台(当前正在使用)时,套接字可以从网络丢失中恢复并继续接收位置事件,但是当应用程序进入后台时,只要套接字处于打开状态,传奇就会响应事件并仍然可以发送它们向上。一旦我标记飞行模式并丢失套接字,传奇就会停止响应重试和其他调度事件,例如位置更新。当我删除飞行模式(同时仍在后台)时,它无法重新建立连接和接收调度。我该如何处理才能让应用程序在不转换回前台的情况下恢复。
传奇代码:
function* sender(socket: WebSocket, eventType, data?) {
const { token } = yield select(authState);
if (socket && socket.readyState === WebSocket.OPEN) {
let localData = {};
if (data) {
localData = data;
}
console.log(JSON.stringify({ type: eventType, data: { token, ...localData } }));
socket.send(JSON.stringify({ type: eventType, data: { token, ...localData } }));
if (eventType === ROUTE_SOCKET_MESSSAGE_TYPES.PING) {
const res = yield race({
success: take(ROUTE_SOCKET_MESSAGE_RECEIVED),
timeout: delay(SOCKET_PING_RESPONSE_EXPECTED)
});
if (res.timeout) {
console.log('ENCOUNTERED LAG IN MESSAGE RECEIPT');
yield put(markNetworkLost());
yield put({ type: ROUTE_SOCKET_RETRY_ACTION_TYPE });
} else {
yield put(markHasNetwork());
}
}
}
}
function* pinger(socket) {
while (true) {
yield call(sender, socket, ROUTE_SOCKET_MESSSAGE_TYPES.PING);
yield delay(FORTY_FIVE_SECONDS);
}
}
function* sendPosition(socket) {
while (true) {
const { latitude, longitude } = yield select(locationState);
if (latitude && longitude) {
yield call(positionDispatcher, socket, {
lat: latitude,
lng: longitude
});
}
yield take('locations/ON_LOCATION_CHANGE');
}
}
function* locationApiRequest() {
try {
const res = yield call(getLocation);
yield put(onPharmacyDetailsReceived(res.data));
} catch (error) {
console.log('Could not fetch pharmacy location details', error);
}
}
function* positionDispatcher(socket, position) {
yield call(sender, socket, ROUTE_SOCKET_MESSSAGE_TYPES.DRIVER_LOCATION, position);
}
function dispatchPhotoFetch(route) {
route.routeStops.forEach((stop) => {
if (stop.address.photos) {
console.log(stop.address.photos);
PhotoService.readLocalFileOrDownload(
stop.address.id,
stop.address.photos.map((photo) => new AddressPhoto(photo))
);
}
});
}
function* socketChannelListener(socketChannel, socket) {
let pingerProcess;
let positionProcess;
while (true) {
const payload = yield take(socketChannel); // take incoming message
const { type: baseSocketResponseType } = payload;
switch (baseSocketResponseType) {
case 'open':
yield fork(locationApiRequest);
pingerProcess = yield fork(pinger, socket);
positionProcess = yield fork(sendPosition, socket);
PendingTasks.activate(); // activate pending task upload when the socket opens
break;
case 'error':
console.log('ERROR', payload.reason, payload);
break;
case 'close':
console.log('CLOSE', payload.reason, payload);
if (payload.reason === 'Invalid creds') {
// console.log('ENCOUNTERED INVALID CREDENTIALS/STALE TOKEN');
yield put(autoLogout());
} else {
yield put(markNetworkLost());
yield put({ type: ROUTE_SOCKET_RETRY_ACTION_TYPE });
}
// console.log('CALLING CLOSE');
yield cancel(pingerProcess);
yield cancel(positionProcess);
return;
default:
break;
}
let type;
let data;
let operation;
let parsedData;
if (payload && payload.data) {
parsedData = JSON.parse(payload.data);
const { type: incomingType, data: incomingData, op } = parsedData;
type = incomingType;
data = incomingData;
operation = op;
}
// console.log('PARSED MESSAGE', parsedData);
switch (type) {
case ROUTE_SOCKET_TYPES.AUTH_STATUS:
switch (data) {
case PINGER_RESPONSE_TYPES.AUTH_SUCCESS:
yield put({ type: ROUTE_SOCKET_MESSAGE_RECEIVED });
break;
case PINGER_RESPONSE_TYPES.TOKEN_EXPIRED:
break;
default:
break;
}
break;
case ROUTE_SOCKET_TYPES.DRIVER_ROUTE:
console.log(
`received DRIVER_ROUTE with ${JSON.stringify(data.routeStops.length)} stop(s)`,
operation,
data
);
switch (operation) {
case DRIVER_ROUTE_OPERATIONS.I: {
const currentRoute = yield select(driverRouteState);
// if we do pick up a route..
if (!currentRoute || currentRoute.id !== data.id) {
yield fork(dispatchPhotoFetch, data);
yield put(onDriverRouteChange(data));
}
break;
}
case DRIVER_ROUTE_OPERATIONS.U:
// i believe we will ignore this if there are records in sqlite?
// create address photo objects first?
// yield put(onDriverRouteUpdate(data));
break;
case DRIVER_ROUTE_OPERATIONS.D:
// TODO: deletion of a route needs to be handled most likely.
break;
default:
break;
}
break;
case ROUTE_SOCKET_TYPES.DRIVER_ROUTE_LOG_REQUEST:
break;
default:
break;
}
}
}
function createSocketChannel(token) {
// TODO: we need to pull this from config....
const socket = new WebSocket(`${ROUTE_SOCKET_URL}${token}`);
// establishes a redux emitter that saga can wait for (like an action)
return {
socket,
socketChannel: eventChannel((emit) => {
socket.onmessage = (event) => {
// console.log('--MESSAGE received--', event);
emit(event);
};
socket.onopen = (evt) => {
emit(evt);
};
socket.onclose = (evt) => {
emit(evt);
};
socket.onerror = (evt) => {
emit(evt);
};
const unsubscribe = () => {
socket.onmessage = null;
};
return unsubscribe;
})
};
}
function* routeSocket(token): any {
const { socketChannel, socket } = yield call(createSocketChannel, token);
yield fork(socketChannelListener, socketChannel, socket);
}
// this method will be retried
export function* RouteSocketSaga(): any {
let task;
while (true) {
const routeSocketAction = yield take([
'DB_INITIALIZED',
PERSIST_REHYDRATE,
ON_AUTH_CHANGE,
ROUTE_SOCKET_RETRY_ACTION_TYPE // retry will attempt to start the saga again..
]);
console.log(routeSocketAction);
if (routeSocketAction.type === PERSIST_REHYDRATE) {
const currentRoute = yield select(driverRouteState);
if (currentRoute) {
yield fork(dispatchPhotoFetch, currentRoute);
}
}
// if the action is to retry, we will wait 5 seconds..
if (routeSocketAction.type === ROUTE_SOCKET_RETRY_ACTION_TYPE) {
yield delay(WAIT_BEFORE_RETRY_TIME);
}
const { token } = yield select(authState);
if (token) {
if (task) {
yield cancel(task);
}
task = yield fork(routeSocket, token);
}
}
}
发送位置的组件代码
BackgroundGeolocation.configure({
desiredAccuracy: BackgroundGeolocation.HIGH_ACCURACY,
stationaryRadius: 1,
distanceFilter: 1,
notificationTitle: 'Background tracking',
debug: false,
startOnBoot: false,
url: null,
stopOnTerminate: true,
locationProvider: BackgroundGeolocation.ACTIVITY_PROVIDER,
interval: 1000,
fastestInterval: 1000,
activitiesInterval: 1000,
startForeground: false,
stopOnStillActivity: false
});
BackgroundGeolocation.on('background', () => {
console.log('[INFO] App is in background');
setAppState(true);
});
BackgroundGeolocation.on('foreground', () => {
console.log('[INFO] App is in foreground');
setAppState(false);
});
BackgroundGeolocation.on('location', (location) => {
dispatch(onLocationChange({ latitude: location.latitude, longitude: location.longitude }));
console.log('LOCATION', location);
});
BackgroundGeolocation.on('stationary', (stationaryLocation) => {
console.log('STATIONARY LOCATION', stationaryLocation);
});
最终将套接字逻辑移动到我的应用程序的根组件并在那里响应位置更新,这使我能够在后台保持套接字活动并发送位置更新。例如
locationSendingEventHandler = (location) => {
const {
coords: { latitude, longitude }
} = location;
console.log('LOCATION', location);
const { token } = this.props;
let socketReopenTriggered = false;
if ((!this.socket || (this.socket && this.socket.readyState >= 2)) && token) {
// 2: CLOSING 3: CLOSED ... two cases where location event should trigger socket to re-open and there is a token
socketReopenTriggered = true;
if (this.setupSocketTimeout) {
clearTimeout(this.setupSocketTimeout);
this.setupSocketTimeout = null;
}
this.setUpSocket('from location');
}
if (!socketReopenTriggered && this.socket && this.socket.readyState === 1) {
console.log('SENDING LOCATION', location, this.socket);
this.socket.send(
JSON.stringify({
type: 'DRIVER_LOCATION',
data: { token, ...{ lat: latitude, lng: longitude } }
})
);
}
};
我正在使用 React-Native,我有一个管理 websocket 状态的 saga。它实现重试并接收诸如位置更改之类的调度,以通过 websocket 将其发送到服务器。我正在使用 @mauron85/react-native-background-geolocation
在我的应用程序的根组件中获取位置事件,并将它们分派给 redux。 route saga 有一个 spawned generator,它会监听这个并将它发送出去。当应用程序处于前台(当前正在使用)时,套接字可以从网络丢失中恢复并继续接收位置事件,但是当应用程序进入后台时,只要套接字处于打开状态,传奇就会响应事件并仍然可以发送它们向上。一旦我标记飞行模式并丢失套接字,传奇就会停止响应重试和其他调度事件,例如位置更新。当我删除飞行模式(同时仍在后台)时,它无法重新建立连接和接收调度。我该如何处理才能让应用程序在不转换回前台的情况下恢复。
传奇代码:
function* sender(socket: WebSocket, eventType, data?) {
const { token } = yield select(authState);
if (socket && socket.readyState === WebSocket.OPEN) {
let localData = {};
if (data) {
localData = data;
}
console.log(JSON.stringify({ type: eventType, data: { token, ...localData } }));
socket.send(JSON.stringify({ type: eventType, data: { token, ...localData } }));
if (eventType === ROUTE_SOCKET_MESSSAGE_TYPES.PING) {
const res = yield race({
success: take(ROUTE_SOCKET_MESSAGE_RECEIVED),
timeout: delay(SOCKET_PING_RESPONSE_EXPECTED)
});
if (res.timeout) {
console.log('ENCOUNTERED LAG IN MESSAGE RECEIPT');
yield put(markNetworkLost());
yield put({ type: ROUTE_SOCKET_RETRY_ACTION_TYPE });
} else {
yield put(markHasNetwork());
}
}
}
}
function* pinger(socket) {
while (true) {
yield call(sender, socket, ROUTE_SOCKET_MESSSAGE_TYPES.PING);
yield delay(FORTY_FIVE_SECONDS);
}
}
function* sendPosition(socket) {
while (true) {
const { latitude, longitude } = yield select(locationState);
if (latitude && longitude) {
yield call(positionDispatcher, socket, {
lat: latitude,
lng: longitude
});
}
yield take('locations/ON_LOCATION_CHANGE');
}
}
function* locationApiRequest() {
try {
const res = yield call(getLocation);
yield put(onPharmacyDetailsReceived(res.data));
} catch (error) {
console.log('Could not fetch pharmacy location details', error);
}
}
function* positionDispatcher(socket, position) {
yield call(sender, socket, ROUTE_SOCKET_MESSSAGE_TYPES.DRIVER_LOCATION, position);
}
function dispatchPhotoFetch(route) {
route.routeStops.forEach((stop) => {
if (stop.address.photos) {
console.log(stop.address.photos);
PhotoService.readLocalFileOrDownload(
stop.address.id,
stop.address.photos.map((photo) => new AddressPhoto(photo))
);
}
});
}
function* socketChannelListener(socketChannel, socket) {
let pingerProcess;
let positionProcess;
while (true) {
const payload = yield take(socketChannel); // take incoming message
const { type: baseSocketResponseType } = payload;
switch (baseSocketResponseType) {
case 'open':
yield fork(locationApiRequest);
pingerProcess = yield fork(pinger, socket);
positionProcess = yield fork(sendPosition, socket);
PendingTasks.activate(); // activate pending task upload when the socket opens
break;
case 'error':
console.log('ERROR', payload.reason, payload);
break;
case 'close':
console.log('CLOSE', payload.reason, payload);
if (payload.reason === 'Invalid creds') {
// console.log('ENCOUNTERED INVALID CREDENTIALS/STALE TOKEN');
yield put(autoLogout());
} else {
yield put(markNetworkLost());
yield put({ type: ROUTE_SOCKET_RETRY_ACTION_TYPE });
}
// console.log('CALLING CLOSE');
yield cancel(pingerProcess);
yield cancel(positionProcess);
return;
default:
break;
}
let type;
let data;
let operation;
let parsedData;
if (payload && payload.data) {
parsedData = JSON.parse(payload.data);
const { type: incomingType, data: incomingData, op } = parsedData;
type = incomingType;
data = incomingData;
operation = op;
}
// console.log('PARSED MESSAGE', parsedData);
switch (type) {
case ROUTE_SOCKET_TYPES.AUTH_STATUS:
switch (data) {
case PINGER_RESPONSE_TYPES.AUTH_SUCCESS:
yield put({ type: ROUTE_SOCKET_MESSAGE_RECEIVED });
break;
case PINGER_RESPONSE_TYPES.TOKEN_EXPIRED:
break;
default:
break;
}
break;
case ROUTE_SOCKET_TYPES.DRIVER_ROUTE:
console.log(
`received DRIVER_ROUTE with ${JSON.stringify(data.routeStops.length)} stop(s)`,
operation,
data
);
switch (operation) {
case DRIVER_ROUTE_OPERATIONS.I: {
const currentRoute = yield select(driverRouteState);
// if we do pick up a route..
if (!currentRoute || currentRoute.id !== data.id) {
yield fork(dispatchPhotoFetch, data);
yield put(onDriverRouteChange(data));
}
break;
}
case DRIVER_ROUTE_OPERATIONS.U:
// i believe we will ignore this if there are records in sqlite?
// create address photo objects first?
// yield put(onDriverRouteUpdate(data));
break;
case DRIVER_ROUTE_OPERATIONS.D:
// TODO: deletion of a route needs to be handled most likely.
break;
default:
break;
}
break;
case ROUTE_SOCKET_TYPES.DRIVER_ROUTE_LOG_REQUEST:
break;
default:
break;
}
}
}
function createSocketChannel(token) {
// TODO: we need to pull this from config....
const socket = new WebSocket(`${ROUTE_SOCKET_URL}${token}`);
// establishes a redux emitter that saga can wait for (like an action)
return {
socket,
socketChannel: eventChannel((emit) => {
socket.onmessage = (event) => {
// console.log('--MESSAGE received--', event);
emit(event);
};
socket.onopen = (evt) => {
emit(evt);
};
socket.onclose = (evt) => {
emit(evt);
};
socket.onerror = (evt) => {
emit(evt);
};
const unsubscribe = () => {
socket.onmessage = null;
};
return unsubscribe;
})
};
}
function* routeSocket(token): any {
const { socketChannel, socket } = yield call(createSocketChannel, token);
yield fork(socketChannelListener, socketChannel, socket);
}
// this method will be retried
export function* RouteSocketSaga(): any {
let task;
while (true) {
const routeSocketAction = yield take([
'DB_INITIALIZED',
PERSIST_REHYDRATE,
ON_AUTH_CHANGE,
ROUTE_SOCKET_RETRY_ACTION_TYPE // retry will attempt to start the saga again..
]);
console.log(routeSocketAction);
if (routeSocketAction.type === PERSIST_REHYDRATE) {
const currentRoute = yield select(driverRouteState);
if (currentRoute) {
yield fork(dispatchPhotoFetch, currentRoute);
}
}
// if the action is to retry, we will wait 5 seconds..
if (routeSocketAction.type === ROUTE_SOCKET_RETRY_ACTION_TYPE) {
yield delay(WAIT_BEFORE_RETRY_TIME);
}
const { token } = yield select(authState);
if (token) {
if (task) {
yield cancel(task);
}
task = yield fork(routeSocket, token);
}
}
}
发送位置的组件代码
BackgroundGeolocation.configure({
desiredAccuracy: BackgroundGeolocation.HIGH_ACCURACY,
stationaryRadius: 1,
distanceFilter: 1,
notificationTitle: 'Background tracking',
debug: false,
startOnBoot: false,
url: null,
stopOnTerminate: true,
locationProvider: BackgroundGeolocation.ACTIVITY_PROVIDER,
interval: 1000,
fastestInterval: 1000,
activitiesInterval: 1000,
startForeground: false,
stopOnStillActivity: false
});
BackgroundGeolocation.on('background', () => {
console.log('[INFO] App is in background');
setAppState(true);
});
BackgroundGeolocation.on('foreground', () => {
console.log('[INFO] App is in foreground');
setAppState(false);
});
BackgroundGeolocation.on('location', (location) => {
dispatch(onLocationChange({ latitude: location.latitude, longitude: location.longitude }));
console.log('LOCATION', location);
});
BackgroundGeolocation.on('stationary', (stationaryLocation) => {
console.log('STATIONARY LOCATION', stationaryLocation);
});
最终将套接字逻辑移动到我的应用程序的根组件并在那里响应位置更新,这使我能够在后台保持套接字活动并发送位置更新。例如
locationSendingEventHandler = (location) => {
const {
coords: { latitude, longitude }
} = location;
console.log('LOCATION', location);
const { token } = this.props;
let socketReopenTriggered = false;
if ((!this.socket || (this.socket && this.socket.readyState >= 2)) && token) {
// 2: CLOSING 3: CLOSED ... two cases where location event should trigger socket to re-open and there is a token
socketReopenTriggered = true;
if (this.setupSocketTimeout) {
clearTimeout(this.setupSocketTimeout);
this.setupSocketTimeout = null;
}
this.setUpSocket('from location');
}
if (!socketReopenTriggered && this.socket && this.socket.readyState === 1) {
console.log('SENDING LOCATION', location, this.socket);
this.socket.send(
JSON.stringify({
type: 'DRIVER_LOCATION',
data: { token, ...{ lat: latitude, lng: longitude } }
})
);
}
};