refactor: replace fetch SSE with original EventSource

This commit is contained in:
Tristan Yang
2022-10-24 22:20:15 +08:00
parent 18813a9ebb
commit 1b29f84cfa
3 changed files with 179 additions and 227 deletions
+1 -2
View File
@@ -1,12 +1,11 @@
{ {
"name": "vocechat-web", "name": "vocechat-web",
"version": "0.3.11", "version": "0.3.12",
"private": true, "private": true,
"homepage": "https://voce.chat", "homepage": "https://voce.chat",
"dependencies": { "dependencies": {
"@emoji-mart/data": "^1.0.6", "@emoji-mart/data": "^1.0.6",
"@metamask/onboarding": "^1.0.1", "@metamask/onboarding": "^1.0.1",
"@microsoft/fetch-event-source": "^2.0.1",
"@pmmmwh/react-refresh-webpack-plugin": "^0.5.8", "@pmmmwh/react-refresh-webpack-plugin": "^0.5.8",
"@react-oauth/google": "^0.2.8", "@react-oauth/google": "^0.2.8",
"@reduxjs/toolkit": "^1.8.6", "@reduxjs/toolkit": "^1.8.6",
+178 -220
View File
@@ -1,5 +1,4 @@
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { fetchEventSource, EventStreamContentType } from "@microsoft/fetch-event-source";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import BASE_URL from "../../../app/config"; import BASE_URL from "../../../app/config";
import { setReady } from "../../../app/slices/ui"; import { setReady } from "../../../app/slices/ui";
@@ -22,10 +21,6 @@ import chatMessageHandler from "./chat.handler";
import store, { useAppDispatch, useAppSelector } from "../../../app/store"; import store, { useAppDispatch, useAppSelector } from "../../../app/store";
import { ServerEvent, UsersStateEvent } from "../../../types/sse"; import { ServerEvent, UsersStateEvent } from "../../../types/sse";
class RetryError extends Error { }
class FatalError extends Error { }
const getQueryString = (params: { [key: string]: string }) => { const getQueryString = (params: { [key: string]: string }) => {
const sp = new URLSearchParams(); const sp = new URLSearchParams();
Object.entries(params).forEach(([key, val]) => { Object.entries(params).forEach(([key, val]) => {
@@ -36,7 +31,7 @@ const getQueryString = (params: { [key: string]: string }) => {
return sp.toString(); return sp.toString();
}; };
let inter: number | null = null; let inter: number | null = null;
let SSE: EventSource | null = null;
export default function useStreaming() { export default function useStreaming() {
const [readyPullData, setReadyPullData] = useState(false); const [readyPullData, setReadyPullData] = useState(false);
const { const {
@@ -48,9 +43,8 @@ export default function useStreaming() {
const loginUid = authData.user?.uid || 0; const loginUid = authData.user?.uid || 0;
let initialized = false; let initialized = false;
let initializing = false; let initializing = false;
let controller = new AbortController();
const startStreaming = async () => { const startStreaming = () => {
console.info("sse start streaming", initialized, initializing); console.info("sse start streaming", initialized, initializing);
if (initialized || initializing) return; if (initialized || initializing) return;
// 如果token快要过期,先renew // 如果token快要过期,先renew
@@ -80,238 +74,202 @@ export default function useStreaming() {
if (usersVersion !== 0) { if (usersVersion !== 0) {
params.users_version = `${usersVersion}`; params.users_version = `${usersVersion}`;
} }
await fetchEventSource(`${BASE_URL}/user/events?${getQueryString(params)}`, { // 开始初始化推送
openWhenHidden: true, SSE = new EventSource(`${BASE_URL}/user/events?${getQueryString(params)}`);
signal: controller.signal,
async onopen(response) {
initializing = false;
if (response.ok && response.headers.get("content-type") === EventStreamContentType) {
console.info("sse everything ok");
initialized = true;
return; // everything's good
} else if (response.status >= 400 && response.status < 500 && response.status !== 429) {
// 重新登录
// client-side errors are usually non-retriable:
console.info("sse debug: open fatal");
throw new FatalError();
} else {
// server error
console.info("sse debug: open retry");
throw new RetryError();
}
},
onmessage(evt) {
initializing = false;
console.info("sse message", evt.data);
// if the server emits an error message, throw an exception
// so it gets handled by the onerror callback below:
if (evt.event === "FatalError") {
console.info("sse debug: error message fatal");
throw new FatalError(evt.data);
}
const data: ServerEvent = JSON.parse(evt.data);
const { type } = data;
switch (type) {
case "heartbeat":
console.info("sse heartbeat", loginUid);
break;
case "ready":
console.info("sse streaming ready");
dispatch(setReady());
break;
case "users_snapshot":
{
console.info("sse users snapshot");
const { version } = data;
dispatch(updateUsersVersion(version));
}
break;
case "users_log":
{
const { logs } = data;
console.info("sse users change logs", logs);
dispatch(updateUsersByLogs(logs));
}
break;
case "user_settings":
case "user_settings_changed":
{
console.info("sse users settings");
Object.keys(data).forEach((key) => {
switch (key) {
case "read_index_groups":
dispatch(updateReadChannels(data[key]));
break;
case "read_index_users":
dispatch(updateReadUsers(data[key]));
break;
case "add_mute_users":
case "mute_users":
case "add_mute_groups":
case "mute_groups":
{
const arr = data[key];
if (arr && arr.length) {
const _key = key.endsWith("users") ? "add_users" : "add_groups";
dispatch(updateMute({ [_key]: arr }));
}
}
break;
case "remove_mute_users":
case "remove_mute_groups":
{
const arr = data[key];
if (arr && arr.length) {
const _key = key.endsWith("users") ? "remove_users" : "remove_groups";
dispatch(updateMute({ [_key]: arr }));
}
}
break;
default: SSE.onopen = () => {
break; initializing = false;
} initialized = true;
}); };
} SSE.onerror = (err) => {
break; console.error("sse error", err);
case "users_state":
case "users_state_changed": initializing = false;
{ stopStreaming();
let { type, ...rest } = data; if (inter) {
const onlines = clearTimeout(inter);
type == "users_state_changed" ? [rest] : (rest as UsersStateEvent).users; }
dispatch(updateUsersStatus(onlines)); // 重连
} inter = window.setTimeout(() => {
break; initialized = false;
case "kick": startStreaming();
{ }, 2000);
console.info("sse kicked"); };
switch (data.reason) { SSE.onmessage = (evt) => {
case "login_from_other_device": initializing = false;
dispatch(resetAuthData()); console.info("sse message", evt.data);
toast("kicked from the other device"); const data: ServerEvent = JSON.parse(evt.data);
const { type } = data;
switch (type) {
case "heartbeat":
console.info("sse heartbeat", loginUid);
break;
case "ready":
console.info("sse streaming ready");
dispatch(setReady());
break;
case "users_snapshot":
{
console.info("sse users snapshot");
const { version } = data;
dispatch(updateUsersVersion(version));
}
break;
case "users_log":
{
const { logs } = data;
console.info("sse users change logs", logs);
dispatch(updateUsersByLogs(logs));
}
break;
case "user_settings":
case "user_settings_changed":
{
console.info("sse users settings");
Object.keys(data).forEach((key) => {
switch (key) {
case "read_index_groups":
dispatch(updateReadChannels(data[key]));
break; break;
case "delete_user": case "read_index_users":
dispatch(resetAuthData()); dispatch(updateReadUsers(data[key]));
toast("Your account has been deleted");
break; break;
case "add_mute_users":
case "mute_users":
case "add_mute_groups":
case "mute_groups":
{
const arr = data[key];
if (arr && arr.length) {
const _key = key.endsWith("users") ? "add_users" : "add_groups";
dispatch(updateMute({ [_key]: arr }));
}
}
break;
case "remove_mute_users":
case "remove_mute_groups":
{
const arr = data[key];
if (arr && arr.length) {
const _key = key.endsWith("users") ? "remove_users" : "remove_groups";
dispatch(updateMute({ [_key]: arr }));
}
}
break;
default: default:
break; break;
} }
});
}
break;
case "users_state":
case "users_state_changed":
{
let { type, ...rest } = data;
const onlines =
type == "users_state_changed" ? [rest] : (rest as UsersStateEvent).users;
dispatch(updateUsersStatus(onlines));
}
break;
case "kick":
{
console.info("sse kicked");
switch (data.reason) {
case "login_from_other_device":
dispatch(resetAuthData());
toast("kicked from the other device");
break;
case "delete_user":
dispatch(resetAuthData());
toast("Your account has been deleted");
break;
default:
break;
} }
break; }
case "related_groups": break;
console.info("sse fill channels from streaming", data); case "related_groups":
dispatch(fillChannels(data.groups)); console.info("sse fill channels from streaming", data);
break; dispatch(fillChannels(data.groups));
case "joined_group": break;
console.info("sse joined group", data.group); case "joined_group":
dispatch(addChannel(data.group)); console.info("sse joined group", data.group);
break; dispatch(addChannel(data.group));
case "group_changed": break;
{ case "group_changed":
const { gid, ...rest } = data; {
const { gid, ...rest } = data;
dispatch(
updateChannel({
gid,
...rest
})
);
}
break;
case "user_joined_group":
{
console.info("sse new user joined group", data.gid);
const { gid, uid: uids } = data;
// 去重
dispatch(
updateChannel({
operation: "add_member",
gid,
members: uids
})
);
}
break;
case "user_leaved_group":
{
const { gid, uid: uids } = data;
if (uids.findIndex((uid) => uid == loginUid) > -1) {
dispatch(removeChannel(gid));
} else {
dispatch( dispatch(
updateChannel({ updateChannel({
gid, operation: "remove_member",
...rest
})
);
}
break;
case "user_joined_group":
{
console.info("sse new user joined group", data.gid);
const { gid, uid: uids } = data;
// 去重
dispatch(
updateChannel({
operation: "add_member",
gid, gid,
members: uids members: uids
}) })
); );
} }
break;
case "user_leaved_group":
{
const { gid, uid: uids } = data;
if (uids.findIndex((uid) => uid == loginUid) > -1) {
dispatch(removeChannel(gid));
} else {
dispatch(
updateChannel({
operation: "remove_member",
gid,
members: uids
})
);
}
}
break;
case "kick_from_group":
console.info("sse kicked from group", data.gid);
dispatch(removeChannel(data.gid));
break;
case "pinned_message_updated":
{
// const {gid,mid,msg}=data;
dispatch(updatePinMessage(data));
}
break;
case "chat":
{
chatMessageHandler(data, dispatch, {
ready,
loginUid,
readUsers,
readChannels
});
}
break;
default:
console.info("sse event data", data);
break;
}
},
onclose() {
// if the server closes the connection unexpectedly, retry:
console.info("sse debug: closed");
initializing = false;
throw new RetryError();
},
onerror(err) {
initializing = false;
if (err instanceof FatalError || err.toString().indexOf("network error") > -1) {
console.info("sse debug: error fatal", err);
throw err; // rethrow to stop the operation
} else {
console.info("sse debug: error other", err);
stopStreaming();
if (inter) {
clearTimeout(inter);
} }
// 重连 break;
inter = window.setTimeout(() => { case "kick_from_group":
initialized = false; console.info("sse kicked from group", data.gid);
startStreaming(); dispatch(removeChannel(data.gid));
}, 2000); break;
throw err; // rethrow to stop the operation case "pinned_message_updated":
// do nothing to automatically retry. You can also {
// return a specific retry interval here. // const {gid,mid,msg}=data;
} dispatch(updatePinMessage(data));
}
break;
case "chat":
{
chatMessageHandler(data, dispatch, {
ready,
loginUid,
readUsers,
readChannels
});
}
break;
default:
console.info("sse event data", data);
break;
} }
}); };
initializing = false;
// for controlling
return controller;
}; };
const stopStreaming = () => { const stopStreaming = () => {
console.info("sse stop streaming"); console.info("sse stop streaming");
if (controller && controller.abort) { if (SSE) {
controller.abort(); SSE.close();
} }
}; };
-5
View File
@@ -2296,11 +2296,6 @@
dependencies: dependencies:
bowser "^2.9.0" bowser "^2.9.0"
"@microsoft/fetch-event-source@^2.0.1":
version "2.0.1"
resolved "https://mirrors.cloud.tencent.com/npm/@microsoft%2ffetch-event-source/-/fetch-event-source-2.0.1.tgz#9ceecc94b49fbaa15666e38ae8587f64acce007d"
integrity sha512-W6CLUJ2eBMw3Rec70qrsEW0jOm/3twwJv21mrmj2yORiaVmVYGS4sSS5yUwvQc1ZlDLYGPnClVWmUUMagKNsfA==
"@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1": "@nicolo-ribaudo/eslint-scope-5-internals@5.1.1-v1":
version "5.1.1-v1" version "5.1.1-v1"
resolved "https://mirrors.cloud.tencent.com/npm/@nicolo-ribaudo%2feslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129" resolved "https://mirrors.cloud.tencent.com/npm/@nicolo-ribaudo%2feslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz#dbf733a965ca47b1973177dc0bb6c889edcfb129"