refactor: useStreaming

This commit is contained in:
zerosoul
2022-03-28 20:55:51 +08:00
parent 58b66ad6a4
commit 751132e7e7
2 changed files with 92 additions and 60 deletions
+88 -54
View File
@@ -1,9 +1,11 @@
import { useState, useCallback, useEffect } from "react"; import { useEffect, useState } from "react";
import { import {
fetchEventSource, fetchEventSource,
EventStreamContentType, EventStreamContentType,
} from "@microsoft/fetch-event-source"; } from "@microsoft/fetch-event-source";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import dayjs from "dayjs";
import BASE_URL from "../../../app/config"; import BASE_URL from "../../../app/config";
import { setReady } from "../../../app/slices/ui"; import { setReady } from "../../../app/slices/ui";
import { useRenewMutation } from "../../../app/services/auth"; import { useRenewMutation } from "../../../app/services/auth";
@@ -24,7 +26,7 @@ import {
} from "../../../app/slices/contacts"; } from "../../../app/slices/contacts";
import { resetAuthData } from "../../../app/slices/auth.data"; import { resetAuthData } from "../../../app/slices/auth.data";
import chatMessageHandler from "./chat.handler"; import chatMessageHandler from "./chat.handler";
import store from "../../../app/store";
import { useDispatch, useSelector } from "react-redux"; import { useDispatch, useSelector } from "react-redux";
class RetriableError extends Error {} class RetriableError extends Error {}
class FatalError extends Error {} class FatalError extends Error {}
@@ -37,39 +39,52 @@ const getQueryString = (params = {}) => {
}); });
return sp.toString(); return sp.toString();
}; };
const StreamStatus = { // const StreamStatus = {
waiting: "waiting", // waiting: "waiting",
initializing: "initializing", // initialized: "initialized",
streaming: "streaming", // streaming: "streaming",
}; // };
let inter = null; let inter = null;
export default function useStreaming() { export default function useStreaming() {
const store = useSelector((store) => store); const [readyPullData, setReadyPullData] = useState(false);
const [
renewToken,
{ data: tokenData, isSuccess: tokenRefreshSuccess },
] = useRenewMutation();
const dispatch = useDispatch();
const [status, setStatus] = useState(StreamStatus.waiting);
const startStreaming = (token = "", refreshToken = "") => {
if (status !== StreamStatus.waiting) return;
const controller = new AbortController();
setStatus(StreamStatus.initializing);
const { const {
authData: { authData: { uid: loginUid },
token: reduxToken, channels: { byId: channelData },
refreshToken: reduxRefreshToken, ui: { ready, online },
uid: loginUid, footprint: { afterMid, usersVersion, readUsers, readChannels },
}, } = useSelector((store) => store);
footprint: { afterMid, usersVersion }, const [renewToken] = useRenewMutation();
} = store; const dispatch = useDispatch();
console.log("set uid use"); let initialized = false;
// 优先使用参数内的token let initializing = false;
const finalToken = token || reduxToken; let controller = new AbortController();
const finalRefreshToken = refreshToken || reduxRefreshToken; const startStreaming = async () => {
fetchEventSource( console.log("start streaming", initialized, initializing);
if (initialized || initializing) return;
// 如果token快要过期,先renew
const {
authData: { token, expireTime = new Date().getTime(), refreshToken },
} = store.getState();
let api_token = token;
const tokenAlmostExpire = dayjs().isAfter(new Date(expireTime - 20 * 1000));
console.log("check token expire time", tokenAlmostExpire);
if (tokenAlmostExpire) {
const {
data: { token: newToken },
isError,
} = await renewToken({
token,
refreshToken,
});
if (isError) return;
api_token = newToken;
}
// 开始初始化
initializing = true;
await fetchEventSource(
`${BASE_URL}/user/events?${getQueryString({ `${BASE_URL}/user/events?${getQueryString({
"api-key": finalToken, "api-key": api_token,
users_version: usersVersion, users_version: usersVersion,
after_mid: afterMid, after_mid: afterMid,
})}`, })}`,
@@ -77,12 +92,13 @@ export default function useStreaming() {
openWhenHidden: true, openWhenHidden: true,
signal: controller.signal, signal: controller.signal,
async onopen(response) { async onopen(response) {
initializing = false;
if ( if (
response.ok && response.ok &&
response.headers.get("content-type") === EventStreamContentType response.headers.get("content-type") === EventStreamContentType
) { ) {
console.log("sse everything ok"); console.log("sse everything ok");
setStatus(StreamStatus.streaming); initialized = true;
return; // everything's good return; // everything's good
} else if ( } else if (
response.status >= 400 && response.status >= 400 &&
@@ -90,36 +106,29 @@ export default function useStreaming() {
response.status !== 429 response.status !== 429
) { ) {
// 重新登录 // 重新登录
if (response.status == 401) {
await renewToken({
token: finalToken,
refreshToken: finalRefreshToken,
});
return;
}
// client-side errors are usually non-retriable: // client-side errors are usually non-retriable:
console.log("sse debug: open fatal");
throw new FatalError(); throw new FatalError();
} else { } else {
// server error
console.log("sse debug: open retry");
throw new RetriableError(); throw new RetriableError();
} }
}, },
onmessage(evt) { onmessage(evt) {
initializing = false;
console.log("sse message", evt.data); console.log("sse message", evt.data);
// if the server emits an error message, throw an exception // if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below: // so it gets handled by the onerror callback below:
if (evt.event === "FatalError") { if (evt.event === "FatalError") {
console.log("sse debug: error message fatal");
throw new FatalError(evt.data); throw new FatalError(evt.data);
} }
const {
ui: { ready },
footprint: { readUsers, readChannels },
channels: { byId: channelData },
} = store;
const data = JSON.parse(evt.data); const data = JSON.parse(evt.data);
const { type } = data; const { type } = data;
switch (type) { switch (type) {
case "heartbeat": case "heartbeat":
console.log("heartbeat", store, loginUid); console.log("heartbeat", loginUid);
break; break;
case "ready": case "ready":
console.log("streaming ready"); console.log("streaming ready");
@@ -231,38 +240,63 @@ export default function useStreaming() {
}, },
onclose() { onclose() {
// if the server closes the connection unexpectedly, retry: // if the server closes the connection unexpectedly, retry:
console.log("sse debug: closed");
initializing = false;
throw new RetriableError(); throw new RetriableError();
}, },
onerror(err) { onerror(err) {
initializing = false;
if (err instanceof FatalError) { if (err instanceof FatalError) {
console.log("sse debug: error fatal", err);
throw err; // rethrow to stop the operation
} else {
console.log("sse debug: error other", err);
stopStreaming();
if (inter) { if (inter) {
clearTimeout(inter); clearTimeout(inter);
} }
// 重连 // 重连
inter = setTimeout(() => { inter = setTimeout(() => {
initialized = false;
startStreaming(); startStreaming();
}, 500); }, 2000);
throw err; // rethrow to stop the operation throw err; // rethrow to stop the operation
} else {
// do nothing to automatically retry. You can also // do nothing to automatically retry. You can also
// return a specific retry interval here. // return a specific retry interval here.
} }
}, },
} }
); );
initializing = false;
// for controlling // for controlling
return controller; return controller;
}; };
useEffect(() => { const stopStreaming = () => {
if (tokenRefreshSuccess) { console.log("stop st");
const { token, refresh_token } = tokenData; if (controller && controller.abort) {
startStreaming(token, refresh_token); controller.abort();
} }
}, [tokenData, tokenRefreshSuccess]); };
const setStreamingReady = (ready) => {
setReadyPullData(ready);
};
useEffect(() => {
console.log("network changed", online, readyPullData);
if (readyPullData) {
if (online) {
startStreaming();
} else {
stopStreaming();
}
}
return () => {
stopStreaming();
};
}, [online, readyPullData]);
return { return {
initializing: status == StreamStatus.initializing, setStreamingReady,
streaming: status == StreamStatus.streaming,
startStreaming, startStreaming,
stopStreaming,
}; };
} }
+5 -7
View File
@@ -8,11 +8,11 @@ import useStreaming from "../../common/hook/useStreaming";
// const querySetting = { // const querySetting = {
// refetchOnMountOrArgChange: true, // refetchOnMountOrArgChange: true,
// }; // };
let request = null; // let request = null;
export default function usePreload() { export default function usePreload() {
const { rehydrate, rehydrated } = useRehydrate(); const { rehydrate, rehydrated } = useRehydrate();
const loginUid = useSelector((store) => store.authData.uid); const loginUid = useSelector((store) => store.authData.uid);
const { startStreaming, streaming, initializing } = useStreaming(); const { setStreamingReady } = useStreaming();
const [ const [
getContacts, getContacts,
{ {
@@ -35,9 +35,7 @@ export default function usePreload() {
initCache(); initCache();
rehydrate(); rehydrate();
return () => { return () => {
if (request) { setStreamingReady(false);
request.abort();
}
}; };
}, []); }, []);
@@ -47,10 +45,10 @@ export default function usePreload() {
getServer(); getServer();
} }
}, [rehydrated]); }, [rehydrated]);
const canStreaming = loginUid && rehydrated && !initializing && !streaming; const canStreaming = loginUid && rehydrated;
useEffect(() => { useEffect(() => {
if (canStreaming) { if (canStreaming) {
request = startStreaming(); setStreamingReady(true);
} }
}, [canStreaming]); }, [canStreaming]);